Skip to content

Commit df59387

Browse files
authored
Metal: work around a miscompile. (#862)
1 parent 0002a6b commit df59387

2 files changed

Lines changed: 70 additions & 0 deletions

File tree

src/metal.jl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,51 @@ function split_aggregate_loads!(mod::LLVM.Module)
390390
return changed
391391
end
392392

393+
# Flatten chained single-index byte `getelementptr`s into one: `gep i8, (gep i8, p, A), B`
394+
# -> `gep i8, p, (A + B)`. The AGX back-end miscompiles a 1-byte load/store made through a
395+
# chained GEP (a byte GEP whose base is another byte GEP) when the grid has exactly two
396+
# threadgroups -- the first threadgroup's access is silently dropped. LLVM deliberately keeps
397+
# such chains split: `InstCombine`'s `visitGEPOfGEP` only merges when the combined index folds
398+
# to an existing value (it bails on variable-plus-constant to avoid materializing an extra
399+
# `add`), and the Metal driver never coalesces them either. On a normal back-end the split form
400+
# is free (the constant GEP folds into the addressing mode), so this is purely an AGX defect; we
401+
# work around it by force-merging here, which only costs a cheap `add` and makes the back-end
402+
# emit correct code. Runs on the optimized, opaque-pointer IR, before AIR lowering.
403+
function merge_byte_gep_chains!(mod::LLVM.Module)
404+
changed = false
405+
i8 = LLVM.Int8Type()
406+
is_byte_gep(v) =
407+
v isa LLVM.GetElementPtrInst && length(operands(v)) == 2 &&
408+
LLVM.LLVMType(LLVM.API.LLVMGetGEPSourceElementType(v)) == i8
409+
410+
for f in functions(mod)
411+
isdeclaration(f) && continue
412+
worklist = LLVM.Instruction[]
413+
for bb in blocks(f), inst in instructions(bb)
414+
is_byte_gep(inst) && is_byte_gep(operands(inst)[1]) && push!(worklist, inst)
415+
end
416+
# process defs-before-uses (instruction order) so longer chains collapse fully:
417+
# an inner gep is rewritten before the outer one that consumes it.
418+
for gep in worklist
419+
src = operands(gep)[1]
420+
(is_byte_gep(gep) && is_byte_gep(src)) || continue # may already be merged
421+
base = operands(src)[1]
422+
inbounds = LLVM.API.LLVMIsInBounds(gep) != 0 && LLVM.API.LLVMIsInBounds(src) != 0
423+
@dispose builder=IRBuilder() begin
424+
position!(builder, gep)
425+
sum = add!(builder, operands(src)[2], operands(gep)[2])
426+
newgep = inbounds ? inbounds_gep!(builder, i8, base, [sum]) :
427+
gep!(builder, i8, base, [sum])
428+
replace_uses!(gep, newgep)
429+
end
430+
erase!(gep)
431+
isempty(uses(src)) && erase!(src)
432+
changed = true
433+
end
434+
end
435+
return changed
436+
end
437+
393438
function finish_ir!(@nospecialize(job::CompilerJob{MetalCompilerTarget}), mod::LLVM.Module,
394439
entry::LLVM.Function)
395440
entry_fn = LLVM.name(entry)
@@ -494,6 +539,11 @@ function lower_air!(@nospecialize(job::CompilerJob{MetalCompilerTarget}), mod::L
494539
end
495540
end
496541

542+
# flatten chained byte GEPs that the AGX back-end miscompiles for 1-byte accesses on a
543+
# 2-threadgroup grid (see `merge_byte_gep_chains!`). run last, after the intrinsic-lowering
544+
# cleanup above, so the merged form is what reaches the AIR downgrader / back-end.
545+
merge_byte_gep_chains!(mod)
546+
497547
return
498548
end
499549

test/metal.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,26 @@ end
139139
end
140140
end
141141

142+
@testset "byte GEP coalescing" begin
143+
# The AGX back-end miscompiles a 1-byte load/store addressed through a *chained* byte
144+
# `getelementptr` (`gep i8, (gep i8, p, i), 1`) on a grid of exactly two threadgroups,
145+
# silently dropping the first threadgroup's access. LLVM deliberately leaves such chains
146+
# split (`InstCombine`'s `visitGEPOfGEP` only merges when the combined index folds, to avoid
147+
# an extra `add`), so `merge_byte_gep_chains!` coalesces them into a single GEP during AIR
148+
# lowering. Without that pass the kernel below keeps two GEPs.
149+
mod = @eval module $(gensym())
150+
function kernel(ptr::Core.LLVMPtr{Int8,1}, i::Int64)
151+
p = ptr + i
152+
Base.unsafe_store!(p + Int64(1), Int8(0))
153+
return
154+
end
155+
end
156+
157+
air = sprint(io -> Metal.code_native(io, mod.kernel,
158+
Tuple{Core.LLVMPtr{Int8,1}, Int64}; kernel=true))
159+
@test count("getelementptr", air) == 1
160+
end
161+
142162
@testset "GC runtime input arguments" begin
143163
mod = @eval module $(gensym())
144164
mutable struct PleaseAllocate

0 commit comments

Comments
 (0)