Skip to content

Roll back kernel trimming that cannot reach its size target - #437

Draft
pvelesko wants to merge 2 commits into
intel:masterfrom
pvelesko:fix/undo-ineffective-kernel-trimming
Draft

Roll back kernel trimming that cannot reach its size target#437
pvelesko wants to merge 2 commits into
intel:masterfrom
pvelesko:fix/undo-ineffective-kernel-trimming

Conversation

@pvelesko

Copy link
Copy Markdown

EstimateFunctionSize::trimCompilationUnit() can consume every trimming candidate and still leave the unit above KernelTotalSizeThreshold. The noinline decisions are kept anyway, which switches the module to subroutine mode: a trimmed function loses the alwaysinline attribute ProcessFuncAttributes would give it, inlining is deferred to SubroutineInliner at the end of OptimizeIR, the passes in between run several times slower on the un-inlined code, and register pressure rises enough to spill and rerun the pipeline through RetryManager. The trimming bought nothing, since the size target was missed regardless, and cost a large multiple of the compile time.

Measured on the 2.75 MB module attached to #420: expanded size 130543 against SubroutineThreshold 110000, all 108 of 108 candidates trimmed, final size 65243 against a threshold of 50000, 306 register spill and a retry. IGC_ControlKernelTotalSize=0 on the same module compiles in 5.6 s with no spill. On a real application kernel with the module cache cleared, master averaged 49.6 s and this change 6.9 s, with runs interleaved to cancel load drift.

When trimming cannot reach the threshold, roll this attempt's decisions back and keep the unit inlined. isTrimmed() identifies exactly the decisions made here, since every pool member was FA_BEST_EFFORT_INLINE when pooled (the implicit-arg pool is restored through setForceInline()). Two cases keep their trimming because it pays even when it undershoots: units above threshold * LargeKernelThresholdMultiplier, the same notion of large as isLargeKernelThresholdExceeded, and units whose trimmed functions are reachable from another compilation unit, where a subroutine removes whole duplicate copies from the module. A new regkey UndoIneffectiveKernelTrimming (default on) disables the rollback.

Two ocloc lit tests under features/kernel_trimming: one where rollback must happen (master prints The size is still above threshold even though all candidates are trimmed and keeps the subroutines), and one where a trimmed function is shared with a second unit and the trimming must be kept. Both fail on master and pass with this change. Note that IGC/ocloc_tests only builds with -DIGC_OPTION__ENABLE_OCLOC_LIT_TESTS=ON.

Fixes #420

…ed back

When checkSubroutine() finds a unit above SubroutineThreshold it calls
reduceKernelSize(), which marks candidate functions noinline until the unit
drops below KernelTotalSizeThreshold. Trimming can consume every candidate and
still end above that threshold, so it misses the objective it exists to meet
while still turning on subroutine mode for the whole module, deferring inlining
to SubroutineInliner and paying the compile-time and register-pressure cost.

undo_ineffective_trimming.ll builds a unit where that is unavoidable: three
helpers, each called five times, whose three bodies together are already larger
than the trimming target, so trimming all three cannot get under it. It checks
all three outcomes on the same module - trimming kept with the rollback
disabled, trimming rolled back with it enabled, and trimming kept regardless
once the unit is over the large-kernel threshold
(KernelTotalSizeThreshold * LargeKernelThresholdMultiplier).

undo_ineffective_trimming_shared.ll covers the second exclusion: when the
trimmed helpers are reachable from another compilation unit the subroutine
removes whole duplicate copies from the module, so the trimming is kept even
though the target is missed.

Both are ocloc tests rather than igc_opt tests, and reach the default
SubroutineThreshold of 110000 through the loop trip count rather than through a
110000-instruction .ll: with LoopCountAwareTrimming a basic block counts
blockSize * tripCount, so a 56-instruction loop body with a trip count of 200 is
worth 11200. That matters because every regkey they set - LoopCountAwareTrimming,
MaxUnrollCountForFunctionSizeAnalysis, KernelTotalSizeThreshold,
PrintControlKernelTotalSize and UndoIneffectiveKernelTrimming - is declared
releaseMode=true, and IGC/ocloc_tests gates its `regkeys` feature on
AND(Release, MSVC) rather than on Release alone. The tests therefore still run,
and still gate the fix, in a Linux Release build, which is where a regression in
this area would actually be shipped from.

Both fail without the following commit.

Signed-off-by: Paulius Velesko <pvelesko@pglc.io>
Compile time steps by an order of magnitude once a kernel's fully inlined size
crosses SubroutineThreshold. On an STK derived module (11 entry points, one
growing kernel) bmg-g21 goes from 2.1 s to 26.7 s between 1.97 MB and 2.75 MB of
SPIR-V, and the larger module also spills around 310 registers and triggers a
full recompilation.

The trigger is kernel trimming, not register pressure. At 2.75 MB the kernel's
expanded size is 130543, above SubroutineThreshold (110000), so checkSubroutine
calls reduceKernelSize. Trimming then marks all 108 of 108 candidate functions
noinline and still ends at 65243, above its KernelTotalSizeThreshold target of
50000, so it misses the objective it exists to meet while paying every cost:

  - isTrimmedFunction suppresses the alwaysinline attribute ProcessFuncAttributes
    would otherwise apply, so CheckEnableSubroutine turns on subroutine mode for
    the whole module.
  - Inlining is deferred from the start of the pipeline to SubroutineInliner at
    the end of OptimizeIR, so the switch bodies are never specialized. Every pass
    from there on then sees several times more IR.
  - The unoptimized code spills around 310 registers, so RetryManager reruns the
    entire pipeline, roughly doubling the total again.

When trimming has consumed every candidate and the unit is still above the
threshold, restore the inline decisions it made. Two cases keep their trimming,
because there it pays for itself even when it undershoots: large kernels, using
the same notion of large as isLargeKernelThresholdExceeded, and units whose
trimmed functions are reachable from another compilation unit, where keeping a
subroutine removes whole duplicate copies from the module. The second exclusion
matters on the full 10.5 MB STK module, where several kernels share the trimmed
helpers.

Measured on bmg-g21 with ocloc, regkey off versus on in the same Release build,
three interleaved repetitions each, machine load 2.2 to 2.5:

  1.18 MB   0.68 s ->  0.68 s   binary byte identical
  1.97 MB   2.14 s ->  2.11 s   binary byte identical
  2.75 MB  26.87 s ->  3.95 s   310 reg spill and 1 retry removed, binary -0.4%
  3.53 MB  39.04 s ->  6.13 s   310 reg spill and 1 retry removed, binary +2.1%
  10.5 MB 190.69 s -> 203.80 s  spills and retry unchanged, binary -1.7%

The 10.5 MB module is the one the exclusions are written for, and it is worth
being precise about what happens there: its largest unit stays trimmed, but the
guards are evaluated per unit, so the module's smaller units are still rolled
back and the output is therefore not identical. The compile time is unchanged
within noise and no spill or recompilation is added, which is the property that
matters; the module is simply not where the win is.

Controlled by UndoIneffectiveKernelTrimming, default on. With the regkey off the
pass takes exactly the path it takes today.

Fixes intel#420

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.

Compile time steps ~50x when a kernel starts spilling: 40% more SPIR-V costs 17x more time (igc 2.38.2)

1 participant