-
Notifications
You must be signed in to change notification settings - Fork 787
[SYCL]Emit an error if an array size exceeds SPIR-V intermediate format limitations #2093
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How did you generate this file - big_constant.h? Is the actual initialized array required? Will the warning be generated if you just create an array with this size and capture it? E.g. const double big[65536];
A variable declaration which uses the const specifier should have an initializer. Otherwise, an error will be thrown along these lines: |
Oh right. My bad. I meant |
SYCL kernel cannot use a non-const global variable. So if you are accessing a global variable from inside a Kernel, it should have const or constexpr qualifier. From Spec:
|
I understand but why does this array size limitation apply to only global variables/constant variables, etc? If the limitation is SPIR-V instruction size, shouldn't any array captured by SYCL kernel be subject to same restrictions? |
80ed991
to
e9049c3
Compare
These SPIR-V limits are super curious but seems to be still in SPIR-V 1.5 https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#_a_id_limits_a_universal_limits :-( |
I have added the check for array size if code is compiled for SYCL devices : |
One more nuance. Big const arrays with zero initializer can be represented in SPIR-V as
in LLVM IR:
in SPIR-V:
For this case we should not emit the error. |
So perhaps the good way is to postpone the check into the LLVM-SPIRV back-end part and have some mechanism to still emit a sensible error message related to the user source code? |
e9049c3
to
850735f
Compare
I have tried to address this with my latest changes.
Can you confirm if this should also be supported? If yes, I need to make additional changes, as this case is not currently supported. |
I don't think there is such a mechanism which allows to map LLVM IR instructions back to the user source code. The closest thing I can think of is debug info, which is not always generated/available. |
I confirm: it should be supported. |
The SPIR-V limits were pushed by some who wanted to put bounds on their own implementations. But, they are 6 years old now. I think they should be revisited and bumped up. |
* Due to SPIR-V intermediate format limitations, const global arrays with number of elements more than 65532 cannot be used inside SYCL kernel. * A possible workaround is to split the array into several ones.
463cb78
to
b671be9
Compare
@bader @romanovvlad buildbot/sycl-win-x64-pr check fails for the SYCL test |
Disabling the test https://github.com/intel/llvm/pull/2173/files |
@elizabethandrews , can you take a look too? |
QualType Ty = VD->getType(); | ||
if (const auto *CAType = dyn_cast<ConstantArrayType>(Ty)) { | ||
assert(CAType); | ||
const int MaxNumElements = 65535 - 3; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this line can be removed and you can just check if arrayElements > 65532 below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should likely be constexpr at least if you're keeping the variable.
bool Sema::isArrayZeroInitialized(VarDecl *VD, bool CheckValueDependent) { | ||
assert(getLangOpts().SYCLIsDevice && | ||
"Should only be called during device compilation"); | ||
if (VD->isInvalidDecl() || !VD->hasInit() || !VD->hasGlobalStorage()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need !VD->hasGlobalStorage()
in here. You already checked it.
@@ -10908,6 +10908,9 @@ def err_sycl_restrict : Error< | |||
"|use a const static or global variable that is neither zero-initialized " | |||
"nor constant-initialized" | |||
"}0">; | |||
def err_array_num_elements_exceeded | |||
: Error<"Due to SPIR-V intermediate format limitations, constant arrays with number of elements more than 65532 cannot be used in SYCL." | |||
"You can workaround this limitation by splitting the array into several ones">; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not too fond of this diagnostic. How about -
SPIR-V intermediate format limitation (or can we just say spec here?) disallows arrays with number of
elements greater than 65532. You can work around this limitation by splitting the array.
if (VD->getInit() && !VD->getInit()->isValueDependent()) | ||
Value = VD->evaluateValue(); | ||
// zero-initialized arrays do not have any initialized elements. | ||
if (Value && Value->isArray() && Value->hasArrayFiller() && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these additional checks required to handle the case int array2[66000] = {};
? @erichkeane would you know if there is a better way to check this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is overly constrained, yes. If all you care about is whether the array is default initialized (as this seems to be doing?), I believe you can do this without the evaluation. You should be able to just do VD->getAnyInitializer(), which should give you an InitListExpr since this is an array type.
Then, you should be able to loop through its 'inits' to validate whether they evaluate to zero. This would also be able to catch the cases where you get int array[6] = {0,0};, which is ALSO zero initialized, but wouldn't be caught here as far as I can tell.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All this starts to become quite complex...
And what if the the array is not used at all and eliminated by some aggressive LLVM passes or if the code is optimized differently or...?
Here we make a lot of guesses ahead on what might happen quite later downstream in code-gen for one specific back-end...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically we don't suppress warnings based on potential optimizations. If something is illegal with NO optimizations, it also stays illegal with O3. That is, the validity of a program should never depend on the optimization settings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we agree. But here we try to guess in Sema something that is unsupported by some LLVM back-ends... If I understand the title of th PR, it is not about warning but error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is overly constrained, yes. If all you care about is whether the array is default initialized (as this seems to be doing?), I believe you can do this without the evaluation. You should be able to just do VD->getAnyInitializer(), which should give you an InitListExpr since this is an array type.
Then, you should be able to loop through its 'inits' to validate whether they evaluate to zero. This would also be able to catch the cases where you get int array[6] = {0,0};, which is ALSO zero initialized, but wouldn't be caught here as far as I can tell.
Are you referring to the if checks as overly constrained here? If yes, then Value->isArray()
had to be added because of assert checks in getArrayInitializedElts()
For the case int array[6] = {0,0};
, the first 2 elements will be set to 0 ( and the rest also set to 0) but, Value->getArrayInitializedElts() will be equal to 2
. This will not be a zero-initialized array and error will not be emitted for this case.
I think both the approaches would work, so how is one better than the other?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is overly constrained, yes. If all you care about is whether the array is default initialized (as this seems to be doing?), I believe you can do this without the evaluation. You should be able to just do VD->getAnyInitializer(), which should give you an InitListExpr since this is an array type.
Then, you should be able to loop through its 'inits' to validate whether they evaluate to zero. This would also be able to catch the cases where you get int array[6] = {0,0};, which is ALSO zero initialized, but wouldn't be caught here as far as I can tell.Are you referring to the if checks as overly constrained here? If yes, then
Value->isArray()
had to be added because of assert checks in getArrayInitializedElts()
Except you already know that the type is an array, since it is a ConstantArrayType, right?
For the case
int array[6] = {0,0};
, the first 2 elements will be set to 0 ( and the rest also set to 0) but,Value->getArrayInitializedElts() will be equal to 2
. This will not be a zero-initialized array and error will not be emitted for this case.
From my reading of the code, that WILL diagnose (int array[67000] = {0,0}), since it won't be zero initialized, right? Since you'd return false in that case here. That is distinctly the incorrect behavior.
I think both the approaches would work, so how is one better than the other?
Because looping through the inits allows us to better early-escape, evaluating the entire initializer is a REALLY expensive operation, so we can do 'at most' the same amount of work by testing each initializer individually. It also gives us the opportunity to deal with defaulted constructors on record types, which have some otherwise interesting behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that WILL diagnose (int array[67000] = {0,0}), since it won't be zero initialized, right
Right, but since the size(6) is < 65532, the error will not be emitted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note the example you quoted. I did a larger array to show that your exception here based on zero-initialized is broken. I was using the '6' simply to be different from the element count (and not 0).
If :
int array[67000] = {};
Shouldn't diagnose, neither should:
int array[67000] = {0,0,{},0};
Nor should:
DefaultConstructible DC[67000] = {};
or
DefaultConstructible DC[67000] = {{}, DefaultConstructible{}};
There is question whether
EmptyDefaultConstructible DC[67000] = {};
or
EmptyDefaultConstructible DC[67000] = {{}, EmptyDefaultConstructible{}};
Should be diagnosed.
Context.getTargetInfo().getTriple().isSPIR() && | ||
!isArrayZeroInitialized(VD, /*CheckValueDependent =*/true)) { | ||
QualType Ty = VD->getType(); | ||
if (const auto *CAType = dyn_cast<ConstantArrayType>(Ty)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you need to use getAsConstantArrayType()
here instead of dyn_cast
. In another PR, we ran into issues with arrays not being recognized correctly with dyn_cast
when it was an elaborated type. @erichkeane can you confirm?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this could very well be a typedef wrapping the constant array type.
Context.getTargetInfo().getTriple().isSPIR() && | ||
!isArrayZeroInitialized(VD, /*CheckValueDependent =*/true)) { | ||
QualType Ty = VD->getType(); | ||
if (const auto *CAType = dyn_cast<ConstantArrayType>(Ty)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, this could very well be a typedef wrapping the constant array type.
QualType Ty = VD->getType(); | ||
if (const auto *CAType = dyn_cast<ConstantArrayType>(Ty)) { | ||
assert(CAType); | ||
const int MaxNumElements = 65535 - 3; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should likely be constexpr at least if you're keeping the variable.
@@ -2077,6 +2077,21 @@ void Sema::finalizeSYCLDelayedAnalysis(const FunctionDecl *Caller, | |||
} | |||
} | |||
|
|||
bool Sema::isArrayZeroInitialized(VarDecl *VD, bool CheckValueDependent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the purpose of CheckValueDependent here? It doesn't seem to be used. Additionally, you likely don't need to use it up the call chain either.
if (VD->getInit() && !VD->getInit()->isValueDependent()) | ||
Value = VD->evaluateValue(); | ||
// zero-initialized arrays do not have any initialized elements. | ||
if (Value && Value->isArray() && Value->hasArrayFiller() && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is overly constrained, yes. If all you care about is whether the array is default initialized (as this seems to be doing?), I believe you can do this without the evaluation. You should be able to just do VD->getAnyInitializer(), which should give you an InitListExpr since this is an array type.
Then, you should be able to loop through its 'inits' to validate whether they evaluate to zero. This would also be able to catch the cases where you get int array[6] = {0,0};, which is ALSO zero initialized, but wouldn't be caught here as far as I can tell.
// RUN: %clang_cc1 -fsycl -fsycl-is-device -triple spir64-unknown -verify -fsyntax-only %s | ||
|
||
const double big[67000] = {1.0, 2.0, 3.0}; | ||
const int zero_init_array[67000] = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
another good repro is:
const int zero_init_array2[67000] = {0,0,0,constexpr_fun_that_returns_0()};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If all you care about is whether the array is default initialized
We care whether zeroinitializer
for the array will be generated in LLVM IR. Is there a good method to detect such case on the Sema level?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, you'd have to detect the actual initializers. See: https://godbolt.org/z/Yo5bcx
You probably ALSO care about this case: https://godbolt.org/z/v8MKrM
Note that the default constructed "S2" is undef, and I'm not particularly sure what differentiates that. For example, a defaulted constructor on the empty type results in it still being 'undef', but an actual constructor will zero-init and call the constructor at global_var_init: https://godbolt.org/z/fvzK97
// Emit an error if a const global array's size exceeds SPIR-V | ||
// intermediate format limitations for SPIRV compatible devices. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why you diagnose only global arrays usage?
!isArrayZeroInitialized(VD, /*CheckValueDependent =*/true)) { | ||
QualType Ty = VD->getType(); | ||
if (const auto *CAType = dyn_cast<ConstantArrayType>(Ty)) { | ||
assert(CAType); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion is weird. If CAType
is null, you won't enter this branch at all.
assert(getLangOpts().SYCLIsDevice && | ||
"Should only be called during device compilation"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see anything SYCL-specific in this function. Should we really restrict it for using by other languages/models?
// Emit an error if a const global array's size exceeds SPIR-V | ||
// intermediate format limitations for SPIRV compatible devices. | ||
else if (IsConst && VD->hasGlobalStorage() && | ||
Context.getTargetInfo().getTriple().isSPIR() && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Checking that the target triple is spir-based triple does not guarantee that the target output is SPIR-V. I'm not sure if we use device code output in non-SPIR-V form somewhere, but technically it will be weird if someone will try to emit valid LLVM IR for device code and get an error about something that is not supported in SPIR-V.
However I'm not sure that it is possible to understand that the end target is SPIR-V from FE perspective at all, since SPIR-V translation is happening inside a separate tool on the other compilation stage.
I think possible solution is a way to tell the FE that this code will be converted into SPIR-V, so the driver will do it when SPIR-V emission is required.
Signed-off-by: Lu, John <[email protected]> Original commit: KhronosGroup/SPIRV-LLVM-Translator@201b782
According to SPIR-V specification Section 2.17.Universal-Limits :
While this means that a SPIR-V module can have quantities exceeding the minimum limits (65,535 WC), the device backend compiler does not handle such SPIR-V modules.
Hence, we detect quantities exceeding the limitations in device code (which is going to be translated to SPIR-V) and emit a warning.
This is not a bug, just a UX improvement in FE.
The actual text of the warning message mentioned in this PR is open for wordsmithing.