Skip to content

vISA: report GRF RA failure when the iteration budget leaves operands unallocated - #427

Open
pvelesko wants to merge 3 commits into
intel:masterfrom
pvelesko:fix/visa-ra-iteration-budget
Open

vISA: report GRF RA failure when the iteration budget leaves operands unallocated#427
pvelesko wants to merge 3 commits into
intel:masterfrom
pvelesko:fix/visa-ra-iteration-budget

Conversation

@pvelesko

@pvelesko pvelesko commented Aug 4, 2026

Copy link
Copy Markdown

coloringRegAlloc() skipped the out-of-iterations failure report whenever fail-safe RA was enabled:

if (!reserveSpillReg &&
    (failedToSpill || (!hasStackCall && iterationNo == maxRAIterations))) {

Fail-safe RA reserves GRFs so spill code can always be placed, and GraphColor.cpp:12434 commits its result with if (isColoringGood == true || reserveSpillReg), i.e. without checking whether colouring actually succeeded. That is fine when fail-safe runs as a later iteration, but entered on iteration 0 (-maxRAIterations 1) it can commit a result in which some operands still have no physical register. coloringRegAlloc() then returns VISA_SUCCESS, Optimizer::optimization() proceeds, and the first post-RA pass to read a physical register dereferences null:

#0  G4_BB_SB::getFootprintForOperand   SWSB_G4IR.cpp:5990
#7  Optimizer::addSWSBInfo             Optimizer.cpp:378
#9  Optimizer::optimization            Optimizer.cpp:1190

at

G4_VarBase *phyReg = (base->isRegVar()) ? base->asRegVar()->getPhyReg() : base;
switch (phyReg->getKind()) {   // nullptr when RA left the var unallocated

Report the exhaustion under fail-safe RA when, and only when, the allocation really is incomplete. Running the iteration counter out is not by itself evidence of failure: a complete fail-safe result is a usable kernel and must not be discarded. Reporting it unconditionally regresses -maxRAIterations 2 on the same kernel, where fail-safe allocates everything: VISA_SPILL makes Optimizer::optimization() bail, CEncoder::Compile() records SIMD_SKIP_SPILL, and since intel_reqd_sub_group_size(32) rules out the SIMD16 and SIMD8 retries, IGC emits a zebin with no kernel while ocloc still prints "Build succeeded".

The predicate scan is short-circuited behind iterationNo == maxRAIterations && reserveSpillReg, so it runs only on that path.

Two tests, gated against three builds:

build -maxRAIterations 1 -maxRAIterations 2
master rc=139 SIGSEGV 619 asm lines
unconditional report no crash, 0 asm lines 0 asm lines, kernel discarded
this change no crash, 0 asm lines 619 asm lines

Note that IGC/ocloc_tests only builds with -DIGC_OPTION__ENABLE_OCLOC_LIT_TESTS=ON.

Fixes #421

… out

GlobalRA::setupFailSafeIfNeeded() enables fail-safe RA once
getIterNo() == maxRAIterations - 1. With -maxRAIterations 1 that is iteration 0,
so fail-safe RA is on for the very first iteration. If that iteration still has
to insert spill code it creates new spill/fill temporaries and increments
iterationNo to maxRAIterations, leaving no iteration in which to allocate them,
and the post-loop failure report in coloringRegAlloc() is skipped because it is
guarded by !reserveSpillReg. coloringRegAlloc() then returns VISA_SUCCESS with
G4_RegVars whose physical register is still null, and the post-RA passes
dereference it.

  ocloc compile -file %s -device dg2 \
    -options "-cl-opt-disable -igc_opts 'VISAOptions=-maxRAIterations 1'"

The kernel holds 8 simultaneously live SIMD32 predicates, which is the point at
which the allocator first has to spill on dg2; 7 does not trigger it.
-cl-opt-disable keeps the predicates from being folded away and
intel_reqd_sub_group_size(32) stops a narrower dispatch from relieving the
pressure.

Without the following commit this invocation dies with SIGSEGV in the post-RA
scoreboard pass, so the expected "Build succeeded" never appears.

Signed-off-by: Paulius Velesko <pvelesko@pglc.io>
Companion to the -maxRAIterations 1 reproducer added earlier. Both invocations
leave the GRF RA loop in the same state: GlobalRA::setupFailSafeIfNeeded()
enables fail-safe RA for iteration maxRAIterations - 1, that iteration still
has to insert spill code, and iterationNo reaches maxRAIterations with no
iteration left to run.

What the two cases leave behind is not the same. Instrumenting coloringRegAlloc()
with the same check the post-RA passes perform on every operand

  phyReg = base->isRegVar() ? base->asRegVar()->getPhyReg() : base;

gives, on this kernel:

  device  -maxRAIterations  iterationNo  reserveSpillReg  operands  null phyReg
  dg2     1                 1            1                3254      6
  dg2     2                 2            1                3246      0
  dg2     3                 1            0                3122      0
  rpl-s   1                 1            1                8640      0
  rpl-s   2                 2            1                8642      0

At -maxRAIterations 1 on dg2 six operands are left unallocated and the compiler
segfaults on them. At -maxRAIterations 2 nothing is left unallocated. That is
what fail-safe RA is supposed to achieve: it assigns the spill/fill
temporaries it creates out of the reserved GRFs on the spot, which is why it
needs no follow-up iteration. Exhausting the iteration counter is therefore not
by itself evidence that the result is unusable, and the result was verified to
be correct: on rpl-s, which diverges the same way and does not crash, the
kernel produced at -maxRAIterations 1 and 2 computes the same values as the
host reference over 4096 work items.

Reporting the exhaustion regardless of what the fail-safe iteration achieved
throws that result away. coloringRegAlloc() returns VISA_SPILL,
Optimizer::optimization() bails out, and CEncoder::Compile()
(IGC/Compiler/CISACodeGen/CISABuilder.cpp) records SIMD_SKIP_SPILL and produces
no kernel. The kernel is intel_reqd_sub_group_size(32), so SIMD16 and SIMD8 are
not candidates, every attempt fails identically, and IGC emits a program
containing no kernel at all while ocloc still prints "Build succeeded":

  build      -maxRAIterations  bytes   sections
  reporting  2                 9160    .spv .misc.buildOptions .note .strtab
  keeping    2                 35136   .text.k .symtab .spv ... .ze_info

-asmToConsole makes the difference checkable from ocloc's output alone: a
compile that emits the kernel dumps its assembly, a compile that discarded it
dumps nothing.

Signed-off-by: Paulius Velesko <pvelesko@pglc.io>
… unallocated

coloringRegAlloc() could return VISA_SUCCESS while leaving G4_RegVars with a
null physical register, which made the compiler segfault in a later pass.

GlobalRA::setupFailSafeIfNeeded() enables fail-safe RA when
getIterNo() == maxRAIterations - 1, unconditionally, so fail-safe RA is always
on for the last permitted iteration. With -maxRAIterations 1 that is iteration
0, so it is on for the very first one. If that iteration still has to insert
spill code it creates new spill/fill temporaries and increments iterationNo to
maxRAIterations, so the loop ends with no iteration left in which to allocate
them. The post-loop failure report was guarded by !reserveSpillReg:

  if (!reserveSpillReg &&
      (failedToSpill || (!hasStackCall && iterationNo == maxRAIterations)))

on the assumption that fail-safe RA needs no follow-up iteration: it assigns
the spill/fill temporaries it creates out of the reserved GRFs as it inserts
the spill code. That holds except when fail-safe RA is entered in the very
first iteration, before the state a later iteration would have set up exists.
Then some temporaries are left with a null physical register, the report is
skipped, coloringRegAlloc() falls through to VISA_SUCCESS, and the post-RA
passes dereference the null:

  vISA::G4_BB_SB::getFootprintForOperand   visa/LocalScheduler/SWSB_G4IR.cpp
  vISA::DDD::getBucketsForOperand          visa/LocalScheduler/LocalScheduler_G4IR.cpp

both of which do

  phyReg = base->isRegVar() ? base->asRegVar()->getPhyReg() : base;
  switch (phyReg->getKind()) {

with no null check. (Which of the two faults first depends on configuration;
guarding either one would only move the crash.)

So test the failure condition directly: when the iteration budget is exhausted
under fail-safe RA, report failure iff an operand was really left without a
physical register. hasUnallocatedOperand() checks exactly what the post-RA
passes dereference. It runs at most once per kernel, only on the
exhausted-budget-under-fail-safe path, which is never reached at default
settings. A complete fail-safe result is still confirmed as before: reporting
failure for it would make Optimizer::optimization() bail out and IGC discard a
usable kernel (with intel_reqd_sub_group_size there is no narrower SIMD mode to
retry, and IGC emits a program with no kernel at all while ocloc prints "Build
succeeded").

RA then returns VISA_SPILL for the truly incomplete case and IGC's existing
retry logic recompiles the kernel successfully. The reproducers added in the
preceding commits cover both sides: ra-iteration-limit-graceful-failure.cl
segfaulted before this change and builds after it;
ra-iteration-limit-complete-failsafe-result.cl keeps its kernel.

Effect on generated code, dg2/rpl-s/bmg-g21, ocloc AOT: every output is
byte-for-byte identical to the unpatched tree across -maxRAIterations
{unset,1,2,3,4} except dg2 -maxRAIterations 1, where the unpatched compiler
segfaults and this change produces a successful build.

Signed-off-by: Paulius Velesko <pvelesko@pglc.io>
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.

SIGSEGV in vISA when the GRF RA iteration budget is exhausted under fail-safe RA (dg2, bmg)

1 participant