[RFC][clang][libc] Extend the `ext_vector_type` attribute to support the scalable vector sizes

This Request for Comments (RFC) proposes an extension to the Clang’s ext_vector_type attribute that would make it possible to create the variables of the scalable vector data types.

Motivation and Background

The indented beneficiary of this extension is the LLVM’s libc library in which we wish to extend the existing set of the vectorized math routines. We would like these routines to process any kind of vector data types, including the ones with fixed and scalable vector sizes. Therefore we need a portable way of expressing these vector types which would be independent of any underlying architectural implementation. A generalized vector data type (covering both fixed and scalable vector lengths) would be our ideal.

Currently, the vector data types with fixed vector sizes are being created using the Clang’s ext_vector_type attribute, which takes the vector size as its only parameter. This attribute has been added to Clang in order to introduce the OpenCL vector types, but it can also be used in normal C/C++ code for specifying vector data types in an architectural-agnostic manner. For example, in case of Arm’s NEON, the types annotated with this attribute are compatible with the vector data types annotated with the neon_vector_type attribute and can be used by the intrinsics defined in the arm-neon.h header.

This attribute plays similar role as GCC’s vector_size attribute (see [1]), yet it is so neat that there were proposals to introduce it in there too (see [2]).

In LLVM’s libc, we can see an example of using this attribute in the simd.h header of the C++ support layer:

template <typename T, size_t N = internal::native_vector_size<T>>
using simd = T [[clang::ext_vector_type(N)]];

We would wish to extend this attribute with the ability to cover the vector-length agnostic scalable vectors (e.g., ARM SVE or RISCV RVV).

Proposed extensions

As can be seen in our PoC PR (see [3]), we were discussing two possibilities:

  1. To encode scalable vector sizes as negative numbers;

  2. To add an optional second parameter of the integer logic type to the ext_vector_type attribute which would denote whether the vector length is scalable or not.

We were also considering the addition of a new attribute (namely, ext_scalable_vector_type), but this would create a lot of overlap with the existing attribute, and would not lead to a more generalized solution.

Example of using the first possibility:

In a C piece of code:

#define SCALABLE_SIZE(N) (-1 * ((signed)(N)))

typedef float svfloat1 __attribute__((ext_vector_type(SCALABLE_SIZE(1U))));
typedef float svfloat4 __attribute__((ext_vector_type(SCALABLE_SIZE(4U))));

In a C++ piece of code:

namespace cpp {

template <size_t N>
constexpr signed scalable_size = -1 * static_cast<signed>(N);

template <typename T, auto N = internal::native_vector_size<T>>
using simd = T [[clang::ext_vector_type(N)]];

} // namespace cpp

cpp::simd<int, cpp::scalable_size<1U>> sv = cpp::iota<int, cpp::scalable_size<1U>>(0);

Example of using the second possibility:

In a C piece of code:

#define SCALABLE_SIZE(N) (N), 1

typedef float svfloat1 __attribute__((ext_vector_type(SCALABLE_SIZE(1U))));
typedef float svfloat4 __attribute__((ext_vector_type(SCALABLE_SIZE(4U))));

In a C++ piece of code:

namespace cpp {

template <typename T, size_t N>
using fixed_size_simd = T [[clang::ext_vector_type(N, false)]];
template <typename T, size_t N>
using scalable_size_simd = T [[clang::ext_vector_type(N, true)]];

template <typename T, size_t N = internal::native_vector_size<T>,
          bool S = internal::native_vector_scalable<T>>
using simd = T [[clang::ext_vector_type(N, S)]];

} // namespace cpp

cpp::simd<int, 1, true>> svi = cpp::iota<int, 1, true>(0);

(Note: I have some concerns regarding how this approach could be used in the concat<>() function template, and anything alike, see [3]).

Proof of concept

The PoC code presented in [3] modifies the Clang’s SemaType.cpp routines so that the ext_vector_type attribute (extended either way) maps the demanded scalable vector types to the Clang’s built-in SVE types (when targeting CPUs with SVE). For example, __attribute((ext_vector_type(SCALABLE_SIZE(1)))) int data type is being turned into the Clang’s built-in __SVInt32_t data type. This however unearths several shortcomings in the currently existing built-in scalable vector data types (see the discussion in [3]).

Although the author is in favor of the first (using negative numbers) approach (it seems simpler in many ways) the discussion which emerged in [3] shows that the more favorable is the second (adding optional parameter) approach.

[1] Vector Extensions (Using the GNU Compiler Collection (GCC))

[2] Making sure you're not a bot!

[3] https://github.com/llvm/llvm-project/pull/183307

2 Likes

Thanks for looking at this, I’ve long wished there were a cross-platform way to expose LLVM’s vscale support through C/C++.

I’m against using negative numbers to express this, it’s not intuitive and it prevents library authors from doing function overloading. Having a boolean parameter is better, though I do wonder if there’s a better way to signal what the boolean means. I wonder if we could simply have a keyword.

using v4i = int [[clang::ext_vector_type(4, true)]];

The extended vectors are broadly based off of the OpenCL specification, The OpenCL™ C Specification. How difficult do you think it would be to support these? The question is if we have something like this with your extension, what happens?

using v4i_f = int [[clang::ext_vector_type(4, fixed)]];
using v4i_s = int [[clang::ext_vector_type(4, scalable)]];
using v4i = int [[clang::ext_vector_type(4)]]; // default fixed

Then there’s the question of all the existing elementwise / reduction builtins. I don’t think we need to do all of these at once, but a big change is that these would no longer be constexpr so we’d need to disable all that support.

How would we expect __builtin_shufflevector and __builtin_convertvector to work? We’d need some way to expose shuffling beyond the vscale factor. Converting between a scalable and non-scalable vector would be an issue. There’s also the OpenCL swizzling.

using v4i = int [[clang::ext_vector_type(4, true)]];
v4i v1 = 1; // {1, 2, 3, 4} x vscale?
v4i v2 = v1.wzyx; // {4, 3, 2, 1} x vscale? How to do inter-shuffle?

Boolean vectors are expected in scalable code as far as I know, ideally that works with the standard operators.

using v4i = int [[clang::ext_vector_type(4, true)]];
using v4b = bool [[clang::ext_vector_type(4, true)]];
v4i foo(v4b m, v4i x, v4i y)  { return m ? x : y; }

I think the type incompatibility will be a bit deeper here than expected. As far as I’m aware, sizeless vectors are based on a builtin RVV or SVE type which is likely incompatible with a lot of what we have. I think in the PR we extended the base extended vector type, but I’m wondering what kind of errors will appear in practice given all the queries for vector_size.

Overall, any code that touches the getNumElements or index access will be busted without modification. We could probably make some of this work if we replaced things with runtime loops or something.

Just some initial thoughts, hopefully we’ll be able to see this through.

How we write the “scalable ext_vector_type” as an attribute is the least of my concerns; it will almost always be behind a typedef. The real problem is the semantics: what specific vector types are allowed, and what operations do we support on these vectors?

If your answer is just “map to the corresponding native vector types”, the end result is pretty useless: you can get the same result with a few lines of templates. If you allow more general vectors or more general operations, you get into hard questions of what operations are actually useful given the performance characteristics of the hardware.

1 Like

Indeed, this is merely a first step, not something complete. There will be changes needed, some overloads required, some operators fixed, some builtins supplemented. And there will be a whole lot of test cases to cover. This is just a beginning, there’s a lot of work ahead. We proposed this extension to the attribute which is at the very bottom of it, so we could have something more general that could describe the vector data types, regardless what kind of vectors they will be. Some of the builtins behind the functions in simd.h do already accept vector types defined with the extended attribute, most of those C++ functions in simd.h would have to drop constexpr (as the IR code emitted for these builtins will be calling runtime code for vector length discovery not known at compile time for the scalable vectors), some other functions will need overloading (using SFINAE or any other C++ template metaproraming techniques). Yes, there is a lot of concerns.

Thanks for this answer @jhuber6, there will be a lot to discuss here, hopefully some of my colleagues who will actually be using what we propose/discuss here should join and address your comments.

At this moment I’d like to point at this: using negative numbers to express this, it’s not intuitive and it prevents library authors from doing function overloading. - I need some of the examples of the overloading that would be prevented. I’m starting to observe that I may be missing the point here and focusing too much on what should happen to the functions in simd.h, which is what I was pointed at when I had been asked what could be done about it.

template <uint32_t N, typename T>
void foo(T [[clang::ext_vector_type(N, false) v);

template <uint32_t N, typename T>
void foo(T [[clang::ext_vector_type(N, true) v);

vs.

template <uint32_t N, typename T>
void foo(T [[clang::ext_vector_type(N) v) {
  if constexpr (is_negative_constant(N))
    foo_fixed(v);
  else
    foo_scalable(v);
}

Handling may be different for scalable or fixed vectors, the latter is forcing people to use type traits to fish that information out instead of just letting it be overloadable.

What about the SFINAE example I’ve presented in my initial commit on [3]:

template <typename T, auto N = internal::native_vector_size<T>,
          internal::enable_if_not_scalable_size_t<N> = 0>
LIBC_INLINE constexpr static simd<T, N> splat(T v) {
  return simd<T, N>(v); // this works for fixed-lenght vectors already
}
template <typename T, auto N = internal::native_vector_size<T>,
          internal::enable_if_scalable_size_t<N> = 0>
LIBC_INLINE constexpr static simd<T, N> splat(T v) {
  simd<T, N> sv;
  size_t n = __builtin_vectorelements(simd<T, N>); // runtime query
  for (unsigned i = 0U; i < n; ++i)
    sv[i] = v;
  return sv;
}

with internal::enable_if_scalable_size_t defined as such:

namespace cpp {

template <auto N>
constexpr bool is_scalable_size_v = static_cast<signed>(N) < 0;

namespace internal {

template <auto N>
using enable_if_scalable_size_t = cpp::enable_if_t<is_scalable_size_v<N>, bool>;

template <auto N>
using enable_if_not_scalable_size_t =
    cpp::enable_if_t<!is_scalable_size_v<N>, bool>;

} // namespace internal

} // namespace cpp

NB., the fact that I had to overload splat<>() function template for scalable vs. fixed vectors reveals one of the problems that needs to be addressed: not everything works for scalable vectors yet, e.g. splat operator does not compile. And the IR this overload generates is ugly with this explicit for loop…

What I don’t like about the other approach is that this optional parameter would have to be carried on in many places, extending the function invocation length to the point where pain threshold is being exceeded. One of my colleagues responded that mixing scalable and fixed vectors in one code (one function) is something people do and would like to do. E.g. the concat<>() template function takes pair of sizes as the template params, it would have to take pair of sizes along with a pair of booleans if we would like to concatenate scalable and fixed sized vectors, it seems overly verbose…

I think we need to do some of this work before we move forward with the attribute. It’s hard to asses how useful the attribute is without a wider view of the whole design. And I’m worried that the answers to these questions will force significant changes to the design.

1 Like

Yes, I’ll be making this point too.

I have an interest in this for GCC, though GCC most likely don’t want to fully support ext_vector_type.
So at least I think it should be a new attribute.

I think it might be better to define new builtins for things like dup (splat), creating an index , extracting the first element, shift left and insert. Etc. Similar to SVE intrsinics. (many of these will correspond to GCC tree codes and internal functions :slight_smile: ).

For PERM, add __builtin_shuffle( Vector Extensions (Using the GNU Compiler Collection (GCC)) ) .

Also it might be useful to bring in and talk with some of your coworkers that work on GCC on some of the more semantics here.

1 Like

This kind of piggybacking on existing stuff will not going to fly, there will always be a fierce resistance on any companion PR. What we really need is a new type attribute, sizeless_vector_type (not piggybacking on anything ext_ that could be a foreign toolchain related or associated with HLSL or OpenCL and meet a colateral resistance from the HLSL or OpenCL people, just a clean sizeless_ prefix not associated with anyone). This attribute would cover all the RVV/SVE sizeless types in an architecture-independent manner. There will be a lot of work needed, all the builtins, all the initializators (splat and alike), all the test cases and documentation. A year of work no less.

I’m not opposed to making a new attribute for this, but if we are I would prefer that it efficiently handles non-scalable vector types too.

ext_vector_type is an implementation detail of the language native vector types, it’s not like it’s a directly exposed language feature.

Hypothetical opposition isn’t a reason to do something

Sadly, I see no other way. Both vector types require a ‘size’ parameter which means different things for both of them.

This would effectively mean ‘the second approach’ from the initial proposal (as we already rejected the idea of using negative numbers to represent the scalable ‘sizes’) minus the mapping to existing SVE/RVV types: they don’t support splat initialization neither the elementwise buitlins and my attempt to extend them with such ability faced the justified rejection. Effectively, we’d have to do it the hard way that I wanted to avoid and extend the extended vector types with the ability to also cover the sizeless vectors.

Whole this RFC is invalid. The ext_vector_type supports swizzling, which doesn’t make much sense for scalable vectors.

The lo/hi swizzling would be doable as runtime codegen, but the generic version would definitely be tough unless we define it to be only within the known size.

To summarize. The ext_vector_type attribute is backed by the class ExtVectorType : public VectorType type and there are a lot of ifs in clang’s Sema that make this type more capable than a regular (architecture specific) SIMD vector type (e.g. our beloved splat initialization in C++ is only possible with ext_vector_type). An attempt to extend the ext_vector_type attribute with the ability to cover scalable vectors merely by mapping them to exisying SVE/RVV vector types has just been rejected: these types do not support the ext-type extensions, and extending the existing SVE/RVV vector types with such capabilities would change their behavior and thus my attempt to do so has been rejected ([clang][Sema] Allow splat initialization of a sizeless vector in a C++ code by pawosm-arm · Pull Request #205432 · llvm/llvm-project · GitHub).

Implementing a scalable equivalent of the class ExtVectorType : public VectorType type, or, extending the ExtVectorType class with a coverage for the scalable vectors would be a massive endevor, and we would have to make a solid justified decision here if we want to go down that path.

Alternatively, we could propose some new attribute which for SIMD vectors would map to the underlying architecture specific vector types, and for scalable vectors it would map to the underlying architecture specific scalable vector types. It would require some involvement from other (than NEON and SVE) architectures people, it would not cover the goodies of ext_vector_type, it may meet similar level of resistance as any other proposal (even the name for this attribute may be problematic; what it could be? a portable_vector_type?), but maybe this is what we actually need… I think we need to make a collective decision here.