@@ -390,6 +390,51 @@ function split_aggregate_loads!(mod::LLVM.Module)
390390 return changed
391391end
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+
393438function 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
498548end
499549
0 commit comments