Skip to content

[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

Closed

Conversation

srividya-sundaram
Copy link
Contributor

@srividya-sundaram srividya-sundaram commented Jul 10, 2020

  • Some limitations in SPIR-V specification affect how SYCL code can be written.
  • This PR addresses one such case where we cannot access a global variable which is a const array with more than ~65532 elements from within a SYCL kernel, because of the 65,535 wordcount limit on SPIR-V instruction. Refer : Instruction-wordcount-limit
  • The SPIRV-LLVM-Translator was updated to prevent crashing and instead emit a helpful message about this SPIR-V limitation. PR here

According to SPIR-V specification Section 2.17.Universal-Limits :

2.17. Universal Limits
These quantities are minimum limits for all implementations and validators. Implementations are allowed to support larger quantities. Specific APIs may impose larger minimums.

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.

Copy link
Contributor

@elizabethandrews elizabethandrews left a 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];

@srividya-sundaram
Copy link
Contributor Author

@elizabethandrews

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: default initialization of an object of const type 'const double [65536]'

@elizabethandrews
Copy link
Contributor

@elizabethandrews

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: default initialization of an object of const type 'const double [65536]'

Oh right. My bad. I meant double [65536];

@srividya-sundaram
Copy link
Contributor Author

@elizabethandrews

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: default initialization of an object of const type 'const double [65536]'

Oh right. My bad. I meant double [65536];

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:

Variables with static storage duration that are odr-used inside a kernel must be const or constexpr and
zero-initialized or constant-initialized

@srividya-sundaram srividya-sundaram changed the title [SYCL]Emit a warning if an array size exceeds SPIR-V intermediate format limitations [SYCL]Emit an error if an array size exceeds SPIR-V intermediate format limitations Jul 14, 2020
@elizabethandrews
Copy link
Contributor

@elizabethandrews

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: default initialization of an object of const type 'const double [65536]'

Oh right. My bad. I meant double [65536];

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:

Variables with static storage duration that are odr-used inside a kernel must be const or constexpr and
zero-initialized or constant-initialized

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?

@keryell
Copy link
Contributor

keryell commented Jul 14, 2020

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 :-(
@johnkslang I guess this will start being an issue with people writing more and more complex programs in SYCL... Perhaps we should have a SPIR-V extension to push these limits up, for example to match LLVM IR?
Anyway, in the meantime our back-end use LLVM IR, not SPIR-V, and the PTX back-end does not use SPIR-V either. It is not clear in this PR whether this check is done only for the SPIR-V device target.

@srividya-sundaram
Copy link
Contributor Author

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 :-(
@johnkslang I guess this will start being an issue with people writing more and more complex programs in SYCL... Perhaps we should have a SPIR-V extension to push these limits up, for example to match LLVM IR?
Anyway, in the meantime our back-end use LLVM IR, not SPIR-V, and the PTX back-end does not use SPIR-V either. It is not clear in this PR whether this check is done only for the SPIR-V device target.

I have added the check for array size if code is compiled for SYCL devices : if(getLangOpts().SYCLIsDevice)
Can you clarify what you mean by : whether this check is done only for the SPIR-V device target.

@AlexeySotkin
Copy link
Contributor

One more nuance. Big const arrays with zero initializer can be represented in SPIR-V as OpConstantNull. For example https://godbolt.org/z/7vhP1e :
In C/C++

int big_array[67000];
int big_array2[67000] = {};

in LLVM IR:

@big_array = dso_local global [67000 x i32] zeroinitializer, align 16
@big_array2 = dso_local global [67000 x i32] zeroinitializer, align 16

in SPIR-V:

%ulong_67000 = OpConstant %ulong 67000
%_arr_uint_ulong_67000 = OpTypeArray %uint %ulong_67000
%_ptr_Function__arr_uint_ulong_67000 = OpTypePointer Function %_arr_uint_ulong_67000
%6 = OpConstantNull %_arr_uint_ulong_67000
%big_array = OpVariable %_ptr_Function__arr_uint_ulong_67000 Function %6
%big_array2 = OpVariable %_ptr_Function__arr_uint_ulong_67000 Function %6

For this case we should not emit the error.

@keryell
Copy link
Contributor

keryell commented Jul 15, 2020

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?

@srividya-sundaram srividya-sundaram changed the title [SYCL]Emit an error if an array size exceeds SPIR-V intermediate format limitations [WIP] [SYCL]Emit an error if an array size exceeds SPIR-V intermediate format limitations Jul 15, 2020
@srividya-sundaram
Copy link
Contributor Author

@keryell @premanandrao

So this error message should be emitted only for SPIR-V device code.

I have tried to address this with my latest changes.

@AlexeySotkin @AlexeySachkov

int big_array[67000];
int big_array2[67000] = {};
For this case we should not emit the error.

Can you confirm if this should also be supported? If yes, I need to make additional changes, as this case is not currently supported.

@srividya-sundaram srividya-sundaram marked this pull request as draft July 16, 2020 04:07
@AlexeySotkin
Copy link
Contributor

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?

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.

@AlexeySotkin
Copy link
Contributor

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 confirm: it should be supported.

@johnkslang
Copy link

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.
@srividya-sundaram srividya-sundaram marked this pull request as ready for review July 23, 2020 23:53
@srividya-sundaram srividya-sundaram changed the title [WIP] [SYCL]Emit an error if an array size exceeds SPIR-V intermediate format limitations [SYCL]Emit an error if an array size exceeds SPIR-V intermediate format limitations Jul 23, 2020
@srividya-sundaram
Copy link
Contributor Author

srividya-sundaram commented Jul 24, 2020

@bader @romanovvlad buildbot/sycl-win-x64-pr check fails for the SYCL test host-interop-task/interop-task.cpp
This test also fails in the sycl branch and is not related to my changes in this PR. I have re-run the job a couple of times and that din't help either. Whom should I report this error to?

@romanovvlad
Copy link
Contributor

@bader @romanovvlad buildbot/sycl-win-x64-pr check fails for the SYCL test host-interop-task/interop-task.cpp
This test also fails in the sycl branch and is not related to my changes in this PR. I have re-run the job a couple of times and that din't help either. Whom should I report this error to?

Disabling the test https://github.com/intel/llvm/pull/2173/files

@premanandrao
Copy link
Contributor

@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;
Copy link
Contributor

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.

Copy link
Contributor

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())
Copy link
Contributor

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">;
Copy link
Contributor

@elizabethandrews elizabethandrews Jul 24, 2020

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() &&
Copy link
Contributor

@elizabethandrews elizabethandrews Jul 24, 2020

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?

Copy link
Contributor

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.

Copy link
Contributor

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...

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

@srividya-sundaram srividya-sundaram Jul 27, 2020

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.

Copy link
Contributor

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)) {
Copy link
Contributor

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?

Copy link
Contributor

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)) {
Copy link
Contributor

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;
Copy link
Contributor

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) {
Copy link
Contributor

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() &&
Copy link
Contributor

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] = {};
Copy link
Contributor

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()};

Copy link
Contributor

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?

Copy link
Contributor

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

Comment on lines +232 to +233
// Emit an error if a const global array's size exceeds SPIR-V
// intermediate format limitations for SPIRV compatible devices.
Copy link
Contributor

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);
Copy link
Contributor

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.

Comment on lines +2081 to +2082
assert(getLangOpts().SYCLIsDevice &&
"Should only be called during device compilation");
Copy link
Contributor

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() &&
Copy link
Contributor

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.

jsji pushed a commit that referenced this pull request Aug 11, 2023
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

Successfully merging this pull request may close these issues.

9 participants