intel / intel/intel-graphics-compiler
LowerGEPForPrivMem: single-index GEP with [N x i8] source element type gets an N-times over-stride (silent wrong results)
- Dominant language
- C++
- Stars
- 718
- Forks
- 191
- PR merge metrics
- No merged PRs in 30d
Description
Private-memory lowering (`LowerGEPForPrivMem`) miscompiles single-index GEPs whose source element type is an i8 array (`[N x i8]`) instead of bare `i8`. The generic aggregate path multiplies the index by the array size, producing an N-times over-stride. Wrong results are computed silently; nothing is diagnosed.
This is newly important because LLVM 23's InstCombine canonicalizes every single-index GEP over a non-i8 type into exactly that shape, so any kernel built by an LLVM 23+ SPIR-V toolchain that indexes a private array can hit it.
## Root cause
In `IGC/Compiler/CISACodeGen/LowerGEPForPrivMem.cpp`:
- `TransposeHelper::handleGEPInst` (~line 1164): the byte-offset fast path is gated on `getSourceElementType()->isIntegerTy(8)`. A GEP with source element type `[4 x i8]` is an `ArrayType`, not bare `i8`, so it misses the fast path and falls into the generic aggregate path, which computes `pScalarizedIdx = (0 + %i) * arr_sz` with `arr_sz = 4`. `HandleAllocaSources` then consumes that index in units of the alloca base type (`i32`, 4 bytes), giving a 16-byte stride where 4 was intended.
The bail-out guards key on bare `i8` as well, so `[N x i8]` slips past all of them:
- `SOALayoutChecker::MismatchDetected`: `if (gepSrcTy->isIntegerTy(8)) { canUseSOALayout = false; }`.
- The dynamic non-promoted-type-GEP loop in the same function strips arrays via `extractArrayOrVecEleType`, but only walks direct users of the alloca; the affected GEP hangs off an `OpBitcast`-derived pointer.
- The earlier hardening commits `2c42b1b7580d` ("Skip SOA promotion for variable i8 GEPs", 2025-09-24) and `b7581328b599` ("Detect type mismatch in SOA promotion when alloca with multi-byte elements is accessed through i8 GEPs", 2025-12-22) share the same blind spot.
Commit `402dd07778f6` (2026-08-10, "Fix byte-offset GEP indexing into vector SoA base types") does not fix this: it is inside the `isIntegerTy(8)` branch and never fires for `[N x i8]`. Checked against `b5d21888aef4`.
## Affected versions
| Device | IGC | Compute runtime | Result |
|---|---|---|---|
| Arc A380 (DG2) | intel-igc-core-2 2.36.3 | intel-opencl-icd 26.22.38646.4-0 | wrong |
| UHD 770 (ADL-S) | intel-igc-core-2 2.38.2 (latest release, 2026-07-14) | intel-opencl-icd 26.27.39122.11-0 | wrong |
## Reproducer
Kernel `_Z12sneaky_snakePKjS0_Piii` (HeCBench snake-cuda `sneaky_snake`), which indexes private `uint[8]` arrays. With identical input sequences and error threshold 0 the correct output is 1; the miscompiled build writes 0.
Two SPIR-V modules differ only in the shape of five access chains.
Failing (array-of-uchar source element type):
```
%_arr_uchar_uint_4 = OpTypeArray %uchar %uint_4
%p = OpBitcast %_ptr_Function__arr_uchar_uint_4 %RefsPerThread
%q = OpInBoundsPtrAccessChain %_ptr_Function__arr_uchar_uint_4 %p %idx
```
Working (bare uchar, index pre-scaled to bytes):
```
%p = OpBitcast %_ptr_Function_uchar %RefsPerThread
%byteidx = OpShiftLeftLogical %uint %idx %uint_2
%q = OpInBoundsPtrAccessChain %_ptr_Function_uchar %p %byteidx
```
The two compute the same address (index 3 -> base+12 in both).
## Evidence this is an optimizer bug, not invalid input
- `spirv-val --target-env spv1.3` (SPIRV-Tools v2025.3) exits 0 on both modules.
- The failing module computes the correct result on Intel CPU OpenCL (2024.18.12.0.05_160000).
- The failing module computes the correct result on the same GPU with `-cl-opt-disable`.
- The shape comes from LLVM itself: `InstCombinerImpl::visitGetElementPtrInst` in `llvm/lib/Transforms/InstCombine/InstructionCombining.cpp`, the block commented "Canonicalize gep %T to gep [sizeof(%T) x i8]". Both the in-tree SPIR-V backend and SPIRV-LLVM-Translator render the canonicalized IR into the access-chain form above.
Build/run of the two modules, with `ocl_run.c` a plain `clCreateProgramWithIL` + single-enqueue harness:
```
cc -o ocl_run ocl_run.c -lOpenCL
./ocl_run bad_arrayof_uchar.spv # writes 0 (wrong)
./ocl_run good_uchar_byteoffset.spv # writes 1 (correct)
./ocl_run bad_arrayof_uchar.spv -cl-opt-disable # writes 1 (correct)
```
Full `.spv` and `.ll` reproducers plus the GPU and CPU harnesses are available on request; GitHub issues do not accept file attachments through the CLI.
For context, one downstream project currently works around this by rewriting the affected GEPs back to the bare-`i8` byte-offset form before handing SPIR-V to IGC.
Contributor guide
Research direction
Start in IGC/Compiler/CISACodeGen/LowerGEPForPrivMem.cpp, especially TransposeHelper::handleGEPInst and SOALayoutChecker::MismatchDetected, and run the bad_arrayof_uchar.spv and good_uchar_byteoffset.spv cases with ocl_run.c. Trace the [N x i8] index through the bitcast and alloca handling; done means the failing module produces the correct result without -cl-opt-disable and does not receive an over-stride.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100