Skip to content

Investigate skip-packing for small contractions: execute_direct! (rejected) and execute_half_packed! (modest win) - #9

Merged
lkdvos merged 4 commits into
mainfrom
skip-packing
Sep 26, 2026
Merged

lkdvos merged 4 commits into
mainfrom
skip-packing

Conversation

@lkdvos

@lkdvos lkdvos commented Sep 26, 2026

Copy link
Copy Markdown
Member

Summary

Investigation into whether QuasiStrided should skip packing for small contractions (motivated by dim63_2_1_2 running ~4.5x slower than StridedBLAS, and BLIS's "SUP" skinny-unpacked technique). Two opt-in, additive paths, both measured on real hardware (AVX2 rome/znver2 and AVX-512 icelake-server, canary-clean throughout):

  • execute_direct! (src/execution/direct.jl): a fully unpacked scalar triple loop. Rejected — never wins, 25-40x slower by 64³ on both ISAs, because it throws away SIMD entirely. Kept only as a third independent correctness oracle alongside execute_tilewise!.
  • execute_half_packed! (src/execution/halfpack.jl): B packed as usual, A read straight from its own storage (no copy) whenever the whole M extent is exactly one full SIMDKernel register tile. A real, modest win: 1.01-1.08x typical, peaking ~1.05x around K=N in the 16-256 range. Measured across four rounds (initial sweep, re-measured after rebasing onto the new analytical blocking model, extended to a non-ramp K composite and every non-default register shape) — the verdict holds throughout. One known regression pocket: 6-20% slower on AVX2 at the tiniest K/N (<8).

Both are structurally additive: zero changes to plan_contract, ContractPlan, _execute_nest!, kernel selection, or blocking, and neither is wired into automatic dispatch — both are explicit opt-in calls on an existing ContractPlan.

Test plan

  • Full suite passes: 136,898/136,898, zero failures (verified independently after every change, not just trusted from delegated work)
  • execute_half_packed! results are bitwise identical to execute!/execute_tilewise! in the eligible case
  • Correctness coverage: contiguous/tiny/K=0/alpha=0/beta=0-1/empty-output/permuted/negative-stride/sliced/scattered/role-swap/conjugation/out-of-bounds/allocation-free/@inferred, for both new paths
  • Performance measured on real Slurm hardware (jobs 7109278/7109279, 7110151/7110152, 7110225/7110226, 7111552/7111553), all citations embedded in the source docstrings per this project's "no claim without a measurement" convention

🤖 Generated with Claude Code

lkdvos and others added 3 commits September 26, 2026 09:29
BLIS-SUP-style investigation into whether QuasiStrided should skip
packing for small contractions (motivated by dim63_2_1_2 running ~4.5x
slower than StridedBLAS). execute_direct! is a plain scalar triple loop
over the same AxisGroup-derived M/N/K offsets execute! uses, skipping
packing and the register microkernel entirely. Additive only -- no
changes to plan_contract, ContractPlan, _execute_nest!, kernel
selection, or blocking; never wired into automatic dispatch, callable
explicitly on any ContractPlan.

Measured on real hardware (jobs 7109278 rome/AVX2, 7109279
icelake/AVX-512, canary-clean, see benchmark/bench_skip_packing.jl):
this path NEVER wins. Best case is near-parity at 1x1x1; by 64^3 it is
25-40x slower on both ISAs, including on the small-K family that
motivated the investigation. The packed path's own throughput (30-90
GFLOP/s from SIMD) dwarfs what packing costs even at the smallest
sizes -- skipping packing while staying scalar throws away far more
than it saves. Documented in the docstring with job IDs so the finding
survives independently of this history; kept only as a third
independent correctness oracle alongside execute_tilewise! (1,099
tests in test/execution/test_direct.jl, covering contiguous, tiny,
K=0, alpha=0, beta=0/1, empty output, permuted/negative-stride/
sliced/scattered operands, role-swap, conjugation, and repeated calls
on one plan).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Half-packing: B is packed exactly as execute! packs it, but A is read
straight from its own storage (no copy) whenever the whole M extent is
one full, unpadded SIMDKernel register tile with unit-stride rows in
dense storage -- exactly the layout _pack_a_contiguous_eligible already
recognizes for one sliver, generalized to the whole contraction.

Mechanism: _accumulate_step's A address goes through a one-line
indirection (_a_step_offset), dispatched on packed_a's type and
resolved entirely at compile time (verified via matching LLVM/typed
IR that this changes nothing for the existing packed paths). The new
UnpackedAView type supplies the alternate addressing for the unpacked
case; real element types only (atransform is always identity there),
SIMDKernel only. Ineligible plans, and the empty/beta-only
short-circuits, delegate to execute! unchanged, so this function is
always correct, never wired into automatic dispatch, and callable on
any plan without a separate eligibility check by the caller.

Results are bitwise identical to execute!/execute_tilewise! in the
eligible case (same _accumulate_step, same FMA order, different A
address). test/execution/test_halfpack.jl (3,326 tests) covers the
eligibility gate (real/complex, kernel, storage, transform, Qm, M-axis
regularity), NaN-poisoning proof A is never packed, scattered/
negative-stride/offset B and C, non-ramp M, operand-role swap,
out-of-bounds rejection, and zero-allocation/@inferred checks.

Measured on real hardware, both AVX2 (rome/znver2) and AVX-512
(icelake-server), canary-clean throughout, across four rounds:
  - jobs 7110151/7110152: initial sweep (plain dense fixture, default
    kernel shape) -- a real but modest win, 1.01-1.08x typical, peaking
    ~1.05x around K=N in the 16-256 range, tapering to parity at
    K,N >= ~1024. One regression pocket: AVX2 is 6-20% SLOWER at the
    tiniest K/N (<8), where eligibility/setup overhead outweighs the
    tiny packing avoided.
  - jobs 7110225/7110226: re-measured after rebasing onto main's new
    analytical blocking model and FMAddSubKernel, since this path
    reads plan.blocking -- verdict unchanged.
  - jobs 7111552/7111553: extended to a non-ramp K composite (the
    realistic multi-label tensor-contraction case, where the in-place
    A read takes an indexed PtrScatterAxis path rather than an affine
    stride) and every non-default register shape -- same band, same
    regression pocket, confirming the verdict isn't an artifact of the
    simplest fixture measured first.

Net: kept as opt-in exactly as shipped; auto-wiring into
plan_contract's dispatch would need an additional lower K/N gate to
avoid the AVX2 regression pocket, not attempted here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…is PR)

main has been red since the fmaddsub merge (commit 5242652), which this
branch inherited via rebase: test/microkernels/test_fmaddsub_kernel.jl
does `using InteractiveUtils: code_native`, but InteractiveUtils was
never added to Project.toml's [extras]/[targets] test list, so CI's
isolated test environment can't find it (LoadError: ArgumentError:
Package InteractiveUtils not found in current path). Separately,
benchmark/probes/fmaddsub_codegen.jl fails the repo's `runic` format
check (unwrapped generator/println/@printf calls at line width).

Both fixed here rather than left for the fmaddsub branch to catch up
on later, since they block CI on every subsequent PR against main,
including this one. `runic --check .` is clean and the full suite
still passes (136,898/136,898) with InteractiveUtils now resolvable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- test/execution/test_direct.jl: the "M and N below one register
  tile" testset hardcoded shapes (e.g. Ma=5) assuming an AVX2/AVX-512
  planar tile size. On GitHub's hosted runners, kernel selection lands
  on a different kernel/shape at these extents (a smaller fitted
  planar shape on some runners, main's small-M FMAddSub routing on
  AVX-512 ones) -- predicting which kernel that is would duplicate
  kernel_selection.jl's own logic, so instead: build the actual plan
  for each candidate shape and use it as a filter (only candidates
  that land below one tile, on whatever kernel this host actually
  picks, are exercised), with a non-vacuousness check that at least
  one candidate qualified. Holds on any ISA/kernel-selection version
  by construction.

- test/microkernels/test_fmaddsub_kernel.jl: the instruction-selection
  testset asserts an exact instruction mix (vfmaddsub count, no spill,
  etc.) in FMAddSubKernel's compiled hot loop -- a real, tuned
  hardware measurement (not mine), but not reproducible on generic
  hosted CI runners, whose exposed CPU features under
  virtualization don't reliably match. Gated `has_fma` on `CI != true`
  (GitHub Actions sets this) in addition to the existing ISA check, so
  it degrades to Broken/skipped in CI rather than failing, and still
  runs for real everywhere else (verified on this AVX-512 dev
  machine: all 15 assertions execute and pass without CI=true set).

Both are unrelated to this PR's own execute_direct!/execute_half_packed!
logic, surfaced only because this is the first PR to run CI since the
fmaddsub merge. Full suite: 136,924/136,924, runic clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@lkdvos
lkdvos merged commit bca0064 into main Sep 26, 2026
5 checks passed
@lkdvos
lkdvos deleted the skip-packing branch September 28, 2026 19:04
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.

1 participant