Skip to content

Commit 3229d8f

Browse files
michel2323claude
andcommitted
Fix LTS-stack silent corruption in reductions and workgroup barriers
Two distinct correctness bugs surfaced as intermittent KernelAbstractions example failures (naive_transpose, histogram) on the Aurora LTS NEO/IGC stack: - mapreducedim! over a strided/transposed input (e.g. `a == transpose(b)`, `sum(transpose(x))`, `isequal`, `ishermitian`) hit the LTS IGC miscompile of non-coalesced global loads, silently corrupting the result. Detect non-contiguous inputs and materialize them to a dense array before reducing, so every global read is coalesced. (src/mapreduce.jl) - KA `@synchronize` lowered to `barrier(0)`, emitting OpControlBarrier with SequentiallyConsistent but WITHOUT the WorkgroupMemory storage-class bit, which orders no memory per the SPIR-V spec. On the LTS stack shared-local writes were not made visible across the barrier, dropping updates (the histogram example's local-atomic accumulation lost counts). Fence local+global memory in `__synchronize`, and local memory in the mapreduce reduce_group SLM tree. (src/oneAPIKernels.jl, src/mapreduce.jl) Validated on the LTS stack: kernelabstractions 2218 pass / 0 fail; gpuarrays reductions+statistics 2962/2962; histogram 0 failures over 1100 iters; strided ==/sum/isequal/ishermitian correct. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d78ef78 commit 3229d8f

2 files changed

Lines changed: 35 additions & 2 deletions

File tree

src/mapreduce.jl

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@
3333
# perform a reduction
3434
d = 1
3535
while d < items
36-
barrier(0)
36+
# LOCAL_MEM_FENCE: `barrier(0)` lowers to an OpControlBarrier without the
37+
# WorkgroupMemory storage-class bit, which on the NEO/IGC LTS stack does not order the
38+
# shared-local tree accesses across the barrier (proven by the histogram example's lost
39+
# local-atomic updates). Fence local memory so each tree step sees the previous step's
40+
# `shared[]` writes. See ISSUE_mapreduce_corruption.md.
41+
barrier(SPIRVIntrinsics.LOCAL_MEM_FENCE)
3742
index = 2 * d * (item-1) + 1
3843
@inbounds if index <= items
3944
other_val = if index + d <= items
@@ -133,12 +138,32 @@ end
133138

134139
## COV_EXCL_STOP
135140

141+
# Aurora LTS workaround: the NEO/IGC LTS stack miscompiles *strided* (non-coalesced) global
142+
# reads inside the reduction kernel, silently corrupting results whenever an input is read
143+
# along a non-contiguous axis (e.g. `a == transpose(b)`, `sum(transpose(x))`, `ishermitian`).
144+
# Elementwise copies are NOT affected. `_dense_reduce_input` returns false for any input that
145+
# reads non-contiguous memory (a transposed/permuted/strided view, or a broadcast containing
146+
# one), so such inputs get materialized to a dense `oneArray` before reducing. See
147+
# ISSUE_mapreduce_corruption.md and the `naive_transpose` (`a == transpose(b)`) failure.
148+
@inline _dense_reduce_input(::oneArray) = true
149+
@inline _dense_reduce_input(x::Base.ReshapedArray) = _dense_reduce_input(parent(x))
150+
@inline _dense_reduce_input(::AbstractArray) = false # Transpose/Adjoint/PermutedDims/SubArray/…
151+
@inline _dense_reduce_input(::Any) = true # scalars/Refs/tuples carried in a broadcast
152+
@inline _dense_reduce_input(bc::Broadcast.Broadcasted) = all(_dense_reduce_input, bc.args)
153+
136154
function GPUArrays.mapreducedim!(f::F, op::OP, R::oneWrappedArray{T},
137155
A::Union{AbstractArray,Broadcast.Broadcasted};
138156
init=nothing) where {F, OP, T}
139157
Base.check_reducedims(R, A)
140158
length(A) == 0 && return R # isempty(::Broadcasted) iterates
141159

160+
# Aurora LTS workaround (see `_dense_reduce_input` above): materialize strided inputs to a
161+
# dense array first so every global read in the reduction kernel is coalesced.
162+
if !_dense_reduce_input(A)
163+
Acontig = Broadcast.materialize(Broadcast.broadcasted(f, A))
164+
return GPUArrays.mapreducedim!(identity, op, R, Acontig; init=init)
165+
end
166+
142167
# add singleton dimensions to the output container, if needed
143168
if ndims(R) < ndims(A)
144169
dims = Base.fill_to_length(size(R), 1, Val(ndims(A)))

src/oneAPIKernels.jl

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,15 @@ end
214214
## Synchronization and Printing
215215

216216
@device_override @inline function KA.__synchronize()
217-
barrier(0)
217+
# Fence both local and global memory across the workgroup barrier, matching CUDA
218+
# `__syncthreads` semantics. `barrier(0)` lowers to `OpControlBarrier` with
219+
# `SequentiallyConsistent` but WITHOUT the `WorkgroupMemory` storage-class bit, which the
220+
# SPIR-V spec treats as ordering *no* memory — so on the NEO/IGC LTS stack shared-local
221+
# writes (e.g. the histogram example's local-atomic accumulation) are not made visible
222+
# before the next phase reads them, silently dropping updates. Passing
223+
# `LOCAL_MEM_FENCE | GLOBAL_MEM_FENCE` ORs in the WorkgroupMemory/CrossWorkgroupMemory
224+
# fence bits. See ISSUE_mapreduce_corruption.md (the barrier WorkgroupMemory-bit regression).
225+
barrier(SPIRVIntrinsics.LOCAL_MEM_FENCE | SPIRVIntrinsics.GLOBAL_MEM_FENCE)
218226
end
219227

220228
@device_override @inline function KA.__print(args...)

0 commit comments

Comments
 (0)