Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify vsetvl intrinsics. #6

Closed
Hsiangkai opened this issue Apr 8, 2020 · 8 comments
Closed

Simplify vsetvl intrinsics. #6

Hsiangkai opened this issue Apr 8, 2020 · 8 comments

Comments

@Hsiangkai
Copy link
Collaborator

Treat SEW and LMUL as parameters to vsetvl, instead of a bunch of vsetvl intrinsics as the following.

size_t vsetvl_8m1 (size_t avl);
size_t vsetvl_8m2 (size_t avl);
size_t vsetvl_8m4 (size_t avl);
size_t vsetvl_8m8 (size_t avl);
size_t vsetvl_16m1 (size_t avl);
size_t vsetvl_16m2 (size_t avl);
size_t vsetvl_16m4 (size_t avl);
size_t vsetvl_16m8 (size_t avl);
size_t vsetvl_32m1 (size_t avl);
size_t vsetvl_32m2 (size_t avl);
size_t vsetvl_32m4 (size_t avl);
size_t vsetvl_32m8 (size_t avl);
size_t vsetvl_64m1 (size_t avl);
size_t vsetvl_64m2 (size_t avl);
size_t vsetvl_64m4 (size_t avl);
size_t vsetvl_64m8 (size_t avl);

@rdolbeau
Copy link
Collaborator

I have no preferences, but one comment: if it's part of the name, then it's obvious it has to be known at compile time - neither SEW not LMUL can be parametrized. If you put them as parameters, then someone (probably me at some point ;-) ) will find a use case where either one (or both) are parametrized and potentially not known at compile time. Which may or may not be legal and/or supported by an implementation...

This is already an open question for VL, as I have use case where I want/need to play with VL (upper bounded by the return value of vsetvl()) and it's not clear to me in the specifications what is legit and what isn't - i.e., what restrictions can an implementation impose...

@nick-knight
Copy link
Collaborator

The V-extension provides two instructions: vsetvli (immediate form), for when SEW and LMUL are known at compile/assembly time, and vsetvl, for when SEW and LMUL are possibly known only at runtime. The latter instruction is very useful in restoring thread context (e.g., migrating between cores), but I'm not aware of other applications.

I propose the intrinsics library offers both forms. It's tempting to simulate the non-immediate form using the immediate form invoked within a big switch statement, which the compiler could then optimize in the case where SEW and LMUL are compile-time constants. However, this would be inefficient when SEW or LMUL is not known at compile-time, since it would introduce branches in the generated assembly.

Clearly, the vsetvl version must input SEW and LMUL, so the question is really about the syntax of the vsetvli version. I'll only consider this version in the rest of this comment.

Having SEW and LMUL as arguments would involve a big switch statement, but presumably the compiler would resolve it statically. Assuming there's no overhead here, I see no technological advantage to having SEW and LMUL as arguments versus baking them into the intrinsic name. I think this is really a matter of syntactic taste.

Some (subjective) advantages of having them as arguments:

  • Fewer intrinsics to keep track of
  • Same signature as the vsetvl version

Some (subjective) advantages of having them in the intrinsic name:

  • Matches the naming scheme of other intrinsics in the proposal
  • Less error-prone
  • Emphasizes that SEW and LMUL are part of the (static) type system

I vote for the latter approach, based mainly on my belief that very few users will ever use the vsetvl (non-immediate) form.

@rdolbeau
Copy link
Collaborator

Actually, I can think of cases where an end-user will use vsetvl() over the immediate form, especially if some implementations are tolerant to arbitrary VL. The obvious two are peel loops & tail loops, where it's a lot more natural in V to use a smaller VL rather than a larger VL + masking...

@kito-cheng
Copy link
Collaborator

I can think of cases where an end-user will use vsetvl() over the immediate form,

If I understand correctly that mean the dynamic LMUL and SEW is what you want under some situation, however I think it hard to write code under current type system.

e.g.

int32_t *a;
int32_t *b;

int sew = 32;
int lmul = large_lmul ? 8 : 1;

for (; vl = vsetvl(sew, lmul, n); n -= vl) {
  va = vload (a); // Type for va ?
  ...
}

Of cause it can be resolve by write more code, but I think it hard to write and maintain?

int32_t *a;
int32_t *b;

int sew = 32;
int lmul = large_lmul ? 8 : 1;

for (; vl = vsetvl(sew, lmul, n); n -= vl) {
  if (large_lmul) {
    vint32m8_t va_32m8 = vload_i32m8 (a);
  } else {
    vint32m1_t va_32m1 = vload_i32m1 (a);
  }
  ...
}

@rdolbeau
Copy link
Collaborator

To be honest, SEW and LMUL are much less likely to change at runtime, except for the one case for SEW mentioned below.

SEW would be conceivable (e.g. genericity between FP32 and FP64), but you still have to change every intrinsics/mnemonic name - and so it will be known at compile-time anyway in practice (i.e. macro-based generic implementation such as https://github.com/rdolbeau/fftw3/tree/riscv-v). SEW is likely to only change when reinterpreting data, e.g. between 64 and 32 bits integer as done in my Chacha20 implementation - and then the VL changes conversely so that SEW*VL remains constant (we don't change the bits or how many there are in a register, we just change how we interpret them).

LMUL should basically almost always be the rounded-down-to-a-legit-value of (number of available registers)/(number of required registers), if there's enough parallelism in the algorithm. That's unlikely to change for a given implementation of an algorithm.

@nick-knight
Copy link
Collaborator

I absolutely agree users will want to dynamically change VL. Both vsetvl and vsetvli versions input an AVL argument, and return VL.

My earlier comment only concerned SEW and LMUL: Since these are part of the (static) type system in the current implementation, it is tricky to write code that changes them dynamically. Kito gave an artificial example, but I don't see any real-world value to it.

@rdolbeau is it possible for you to extract a minimal example from your "Chacha20" code, demonstrating this use-case? The type-punning I'm concerned you're suggesting can easily lead to non-portable code, since it can expose differences in the architectural parameter SLEN.

If it does prove useful in applications to type-pun on SEW and LMUL, I would prefer adding a reinterpret_cast-style intrinsic to achieve this goal within the type system.

@Hsiangkai
Copy link
Collaborator Author

Agree. We design the type system and intrinsics with static SEW and LMUL. It seems not useful to keep variable form for vsetvl intrinsics. For example, if we have run-time variables sew and lmul,

vl = vsetvl(avl, sew, lmul);  // sew and lmul are run-time variables.
vret = vadd_vv_i32m1(va, vb);

Eventually, the compiler will convert the intrinsics into

vsetvl vl, avl, ra  // Assume ra store sew and lmul run-time settings.
vsetvli x0, x0, e32,m1  // The compiler will generate this one to override the settings.
vadd.vv vret, va, vb

@Hsiangkai
Copy link
Collaborator Author

I write a draft RFC for it. It follows the naming rules of other intrinsics, i.e., encoding SEW and LMUL into intrinsic names.

https://github.com/sifive/rvv-intrinsic-doc/blob/master/rvv-intrinsic-rfc.md#configuration-setting

Please help me to review it. If we all agree to provide static version only, we could close the issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants