Skip to content

feat(mp): per-group (K-band) stream lengths on the contraction axis (stacks on #6) - #7

Open
heroarmor wants to merge 7 commits into
CrucibleComputingGroup:mainfrom
heroarmor:feat/k-bands
Open

feat(mp): per-group (K-band) stream lengths on the contraction axis (stacks on #6)#7
heroarmor wants to merge 7 commits into
CrucibleComputingGroup:mainfrom
heroarmor:feat/k-bands

Conversation

@heroarmor

Copy link
Copy Markdown
Contributor

Summary

MP was per-row on the token axis only: every chunk of the contraction axis ran
the same stoc_len. This adds per-group stream lengths — the contraction
axis is partitioned into bands of whole quantization chunks, and band b runs
rung k at its own length L[b][k], under the exact per-rung iso-compute
identity that scmp_kernels#31
validates at table load.

Row dispatch is unchanged. Because L[b][k] == stoc_len_levels[k] reproduces
the per-row parent exactly, the parent is a legal point in the space and the
refinement cannot lose: a tilt that does not help degenerates back to the
parent rather than costing accuracy.

⚠️ Stacks on #6 — and why it has to

main does not currently run adaptive MP at all. It calls the pre-refactor
convention

adaptive_classify_rows(row_metric, current_timestep, total_timesteps, config, operator=..., ...)

while the submodule main pins (9358239) takes (metric, config, ...). Any
adaptive-MP forward off main raises
TypeError: adaptive_classify_rows() got multiple values for argument 'operator'.
#6's e061384 is what fixes the call sites.

I first built this as a standalone branch off main — the Python cherry-picked
cleanly, but it cannot run there. So this branch is #6 + one commit. Please
merge #6 first; review this PR's single commit dfb061f.

How the banding works

sc_matmul takes a scalar stoc_len — the chunked MLP path builds
cum_indicator once outside the chunk loop and every chunk shares it — so the
banding happens above the kernel: rows are grouped by rung index (not by
parent stoc_len value, since the rung is what indexes every band's ladder) and
each (band, rung) is a separate call whose partial products are summed.

The two paths band differently because they chunk differently:

path how vs. the parent under the parent ladder
sc_attention.py already walks the axis one chunk at a time in Python (it must: chunk_d requires granularity="per_row", and this path can run per_tensor), so a band is just a label on those chunks 0.000e+00 — bit-identical
sc_mlp.py hands chunking to the kernel, so bands gather their columns and each band re-chunks at the same boundaries 6.8e-08 — fp32 summation order, nothing else

Guards

All four failures below would otherwise surface only as a wrong number:

  • qk/av in the table → rejected at init_adaptive_mp. They contract
    over head_dim, have no band dispatch, and would run the per-row parent
    while the run carries the k-bands label.
  • A band length above the halve ceiling → rejected at startup rather than
    mid-run on some later timestep. Bands raise the rungs they favour, so a
    band ladder can exceed 2**(sc_prec-1) even when the parent ladder does not.
  • k-bands + --range_mp → raises. The combined range+dynamic path has no
    contraction-axis dispatch and would drop the allocation on the floor.
  • A table priced at a different chunk_d or contraction width → raises
    instead of falling back to the parent.

Tooling

scripts/build_kband_table.py adds a k_bands section to an existing
calibrated table under a straight-line tilt, rounding down so the identity
is never overspent and handing single units back largest-lost-fraction-first.
--tilt 0 must reproduce the parent and is the sanity arm for any sweep.

This is a sweepable starting point, not a solved allocation — the LLM side
solves it by Lagrangian water-fill on measured per-band error curves.

Verified against the real calib_fix_avg32.json: budget used per rung
0.9931–1.0000, zero overspend.

Note for the halve line: at the top rung the parent already sits at the 128
ceiling, so no band can be lengthened and that rung cannot be tilted at all.
Bands only have freedom on rungs below the ceiling.

Testing

tests/test_kbands.py — 24 tests: table validation, column partitioning
including tail-chunk ordering, the controller guards, and on GPU both the
parent-equivalence and non-degeneracy gates.

scripts/verify_kband_e2e.py drives the real _sc_linear_dynamic_mp methods
rather than reimplementing the band loop, so a mistake in row indexing or
accumulation shows up there and not only in production. It also prints which
trees were imported — scmp_kernels is an editable install and does not follow
$REPO, so a gate whose log cannot prove what it ran against proves nothing.

Verified on gl1802 (job 58187543), qdit @ dfb061f + scmp_kernels @ acf8eca:

24 passed
[OK] SCMlp.mlp_fc1 parent-ladder: rel err vs parent 6.776e-08 (want < 1e-5)
[OK] SCMlp.mlp_fc1 tilted-ladder: rel err vs parent 1.119e-01 (want > 1e-4)
[OK] SCAttention.proj parent-ladder: rel err vs parent 0.000e+00 (want < 1e-5)
[OK] SCAttention.proj tilted-ladder: rel err vs parent 1.113e-01 (want > 1e-4)
RESULT: PASS

Submodule

Bumps scmp_kernels to heroarmor:feat/k-bands (acf8eca) = #6's pin
c967d3a + the k_bands config layer, one commit and nothing else. The clean
version of that layer against kernels main is
scmp_kernels#31;
this pin exists because #6's line carries trace (#25), which never reached
kernels main.

🤖 Generated with Claude Code

heroarmor and others added 7 commits July 5, 2026 14:18
…beta

- calibrate_mp_thresholds.py: budget is now sum(rows * macs * stoc_len) instead
  of sum(rows * stoc_len), so an "avgX" target means X in real compute, not row
  count. macs_per_row (out*in for linears, N*head_dim for qk/av) is threaded
  through add() -> the global lambda solve + iso-budget check. qk recording is
  switched to per-row. Validated: realized FLOP-weighted avg = 47.97 for target 48.
- sc_attention.py: _sc_qk MP is now per-query-row (||Q_row||_inf), matching the
  per_row quant scale; removed the per-head classifier and static per-head path.
- quant_sc_main.py: removed all alpha/beta remnants -- --mp_alpha/--mp_beta and
  the 14 per-op variants, the crashing AdaptiveMPConfig(alpha=,beta=,
  operator_params=) construction, and the run-name tag. --adaptive_mp now
  requires --adaptive_mp_table.
- sc_attention.py + sc_mlp.py: repaired the adaptive_classify_rows signature
  drift vs kernel 9358239 (6 call sites passed the old arg order -> TypeError,
  so the adaptive path could not run at all).

Invalidates existing calibration threshold tables (new cost model -> new
allocations); re-calibration required.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run artifacts (slurm-*.out, figures/) are regenerated, not source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
tools/: compare_old_vs_new{,_isolated,_thorough}.py, compare_vs_cpu_reference.py, sc_call_counter{,_full}.py, smoke_test_e2e.py

scripts/: global-PR calib + MP sbatch (calib_globalpr{,_flop}{,_halve}, mp_globalpr_{auto,fill,flop_halve_auto}, uniform{,_halve}_auto, opfreeze_probe) + eval/ CPU eval

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eanup

Submodule now at local branch local/trace+scramble-cache (6c4539a): PR#25 trace subsystem + cherry-picked PR#28 enable-table scramble cache-key fix + mp/config doc cleanup.

NOTE: 6c4539a is LOCAL-ONLY (not yet pushed to the heroarmor fork); a fresh 'git submodule update' cannot resolve it until pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ion; mse_rel metric

The halve MP line calibrated and ran levels above the halve-mode maximum
(2^(sc_prec-1)=128): the 256/192 levels are unrealizable on halve hardware
and silently inflated attention accuracy (up to 99% of qk rows ran 256/192-
cycle streams). Fixes, one per layer:

- scmp_kernels bump -> c967d3a: sc_matmul now raises on explicit
  stoc_len/rng_levels above 2^(sc_prec-1) under halve+bipolar (upstream
  kernels PR #30), so stale 256-max tables crash instead of overspending.
- calibrate_mp_thresholds.py: hard-validate mp_levels and budget_ref_stoc_len
  <= 2^(sc_prec-1) under --sc_halve; add --metric mse_rel (squared relative
  L2 = cosine's quadratic curvature + gain-error sensitivity; a pure 10% gain
  error scores 0.0 under cosine, 0.01 under mse_rel).
- sbatch_calib_globalpr_flop_halve.sb / sbatch_calib_globalpr_halve.sb:
  128-capped level grid, budget_ref 128, BUDGET_RATIO computed from AVG
  in-script (avg/128 — passing the old avg/256 ratios silently halved every
  budget), optional METRIC arg with suffixed OUTDIR, and calibration now runs
  at the deployment CFG (teacher_cfg_scale 1.5, batch 16 so the CFG-doubled
  batch keeps the old 32-row memory footprint) instead of cfg=0.
- sbatch_mp_globalpr_flop_halve_auto.sb: [METRIC] and [NUM_FID] args
  (metric-suffixed calib/output dirs; NUM_FID for fast probes), points at the
  128-capped tables; old 256-grid tables/arms preserved in the un-suffixed
  dirs for comparison.

Validated: avg48 recalibration hits expected_avg_stoc_len=48.00; runtime
realized per-op distribution matches the table within 1-3pp per level; on the
noise-aligned index prefix the fixed adaptive arms (cosine and mse_rel) beat
uniform48 on fidelity-to-FP (2.5% vs 5% trajectory divergence).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Small single-purpose probes developed while root-causing the halve-line
regressions; kept because each verifies an invariant worth re-checking:

- eval/sbatch_eval_uniform_halve.sb: KID + FID/IS/Prec/Recall for one
  uniform-halve arm (companion to sbatch_eval_mp_flop_halve.sb).
- eval/sbatch_eval_ab_kid100.sb: matched-index KID across probe arms + an FP
  anchor on the same indices (the anchor exposes the class-composition floor
  that dominates absolute KID at small n — FP scores the same ~30x1e-3 as SC
  arms on 10 classes).
- eval/sbatch_probe_realized_dist{,_old}.sb: validate-path run that dumps
  debug_mp_distribution.csv — compares the RUNTIME-realized per-op level
  fractions against the calibration table's intent (A4 check).
- eval/sbatch_kernel_level_err.sb: kernel-level rel-err vs stoc_len under
  halve on qk/av/mlp shapes (catches level-grid pathologies without running
  the diffusion model).
- sbatch_uniform48_rerun100.sb: regenerate uniform48 idx 0-99 into a fresh
  dir — reproducibility check. NOTE: latent noise pairs by position in each
  worker's index list, not by global index, so PSNR comparisons against runs
  with a different GPU split are only valid on the aligned worker-0 prefix.
- sbatch_only1op_sc32.sb: single-op SC isolation (everything FP except one op
  at stoc_len 32 + halve) via per-op config JSONs; the legacy layerwise flags
  are ignored whenever --sc_config supplies a precision map.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MP was per-row on the token axis only: every chunk of D ran the same stoc_len.
This lets each band of quantization chunks run its own length at each rung.

RUNTIME (sc_kbands.py, sc_mlp.py, sc_attention.py)
The kernel still takes a SCALAR stoc_len -- _sc_matmul_per_row_mlp builds
cum_indicator once outside the chunk loop and every chunk shares it -- so the
banding happens above it: rows are grouped by RUNG index (not by parent
stoc_len value, since the rung is what indexes every band's ladder) and each
(band, rung) is a separate call whose partial products are summed.

The two paths band differently because they chunk differently:

  - attention already walks D one chunk at a time in Python (it must: chunk_d
    requires granularity="per_row" and this path can run per_tensor), so a band
    is just a label on those chunks and only the stoc_len lookup changes. That
    is why it comes out BIT-IDENTICAL to the parent under the parent ladder.
  - mlp hands chunking to the kernel, so bands gather their columns and each
    band re-chunks at the same boundaries. Measured 6.8e-08 against the parent
    -- fp32 summation order, nothing else.

GUARDS (all three would otherwise surface only as a wrong number)
  - A table naming qk/av is rejected at init_adaptive_mp: they contract over
    head_dim, have no band dispatch, and would run the parent while the run
    carries the k-bands label.
  - A band length above the halve ceiling is rejected at startup rather than
    mid-run. Bands RAISE the rungs they favour, so a band ladder can exceed
    2^(sc_prec-1) even when the parent ladder does not.
  - k-bands + --range_mp raises: the combined path has no contraction-axis
    dispatch and would drop the allocation on the floor.
  - A table covering a module at a different chunk_d or contraction width
    raises instead of falling back to the parent.

TOOLING
  build_kband_table.py adds a k_bands section to an existing calibrated table
  under a straight-line tilt, rounding DOWN so the identity is never overspent
  and handing single units back largest-lost-fraction-first. --tilt 0 must
  reproduce the parent and is the sanity arm for any sweep. This is a sweepable
  starting point, NOT the solved allocation -- the LLM side solves it by
  Lagrangian water-fill on measured per-band error curves.

  Note for the halve line: at the top rung the parent already sits at the 128
  ceiling, so no band can be lengthened and that rung cannot be tilted at all.
  Bands only have freedom on rungs below the ceiling.

TESTS
  24 tests: table validation (overspend, underspend, whole-chunk rule, band-id
  contiguity, per-operator band counts, width agreement), column partitioning
  incl. the tail-chunk ordering, the controller guards, and on GPU both the
  parent-equivalence and non-degeneracy gates. verify_kband_e2e.py drives the
  real _sc_linear_dynamic_mp methods rather than reimplementing the loop.
  Verified on gl1802 (job 58184021): 24 passed, e2e PASS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@heroarmor
heroarmor requested review from Allenjin123 and a lite review from Copilot August 18, 2026 20:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends adaptive mixed precision (MP) to support per-group (K-band) stream lengths along the contraction axis, enabling each contraction-axis band to run each rung at its own stream length while preserving a per-rung iso-compute identity (so the per-row parent remains a valid degenerate point). It also adds validation/guards, calibration improvements (FLOP-weighted budgeting + halve-aware checks + new metric), and extensive tooling/tests to verify equivalence and non-degeneracy.

Changes:

  • Add K-band dispatch + validation plumbing across SCController, SCMlp, and SCAttention via new sc_kbands helpers.
  • Update calibration + runtime wiring around adaptive MP (table-required), including FLOP-weighted budget accounting, halve-mode guards, and mse_rel metric support.
  • Add tests and multiple end-to-end / cluster scripts to validate correctness and evaluate configurations.

Reviewed changes

Copilot reviewed 37 out of 38 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tools/smoke_test_e2e.py GPU smoke test for imports + basic sc_matmul correctness + deprecated-name surveillance.
tools/sc_call_counter.py Wrapper to count sc_matmul invocations during quant_sc_main.py runs.
tools/sc_call_counter_full.py Variant of call counter configured for “all ops on” uniform settings.
tools/compare_vs_cpu_reference.py Compares GPU Triton implementation vs CPU reference implementation.
tools/compare_old_vs_new.py Numerical comparison harness for old SC implementation vs new scmp_kernels.
tools/compare_old_vs_new_thorough.py Exhaustive (seed/shape/level) bit-identity verification harness.
tools/compare_old_vs_new_isolated.py Isolation tool to attribute observed diffs to quantization margin behavior.
tests/test_kbands.py Unit tests for K-band schema validation, column banding, controller guards, and equivalence.
scripts/verify_kband_e2e.py End-to-end driver exercising real _sc_linear_dynamic_mp band dispatch vs parent.
scripts/sbatch_uniform48_rerun100.sb Cluster job script for uniform48 reproducibility rerun.
scripts/sbatch_uniform_halve_auto.sb Cluster uniform generation with halve-mode + timeout-safe auto-resume.
scripts/sbatch_uniform_auto.sb Cluster uniform generation with timeout-safe auto-resume (non-halve arm).
scripts/sbatch_opfreeze_probe.sb Cluster probe forcing one op to FP-like precision to measure sensitivity.
scripts/sbatch_only1op_sc32.sb Cluster single-op isolation probe at stressed SC length.
scripts/sbatch_mp_globalpr_flop_halve_auto.sb Cluster FLOP-weighted + halve adaptive-MP generation with auto-resume.
scripts/sbatch_mp_globalpr_fill.sb Cluster global+per-row MP generation “fill” script with chaining.
scripts/sbatch_mp_globalpr_auto.sb Cluster global+per-row MP generation auto-resume script.
scripts/sbatch_calib_globalpr.sb Cluster per-row global calibration job script (row-weighted arm).
scripts/sbatch_calib_globalpr_halve.sb Cluster calibration job script with halve-aware level/budget handling.
scripts/sbatch_calib_globalpr_flop.sb Cluster FLOP-weighted calibration job script.
scripts/sbatch_calib_globalpr_flop_halve.sb Cluster FLOP-weighted + halve calibration job script.
scripts/quant_sc_main.py Requires calibrated table for adaptive MP; removes alpha/beta fallback + updates naming/logging.
scripts/eval/sbatch_probe_realized_dist.sb Probe script to compare realized MP level distributions (old vs new tables).
scripts/eval/sbatch_probe_realized_dist_old.sb Control probe for realized distribution using old table without halve.
scripts/eval/sbatch_kernel_level_err.sb Kernel-level error probe vs stoc_len under halve/no-halve.
scripts/eval/sbatch_kband_equivalence.sb Cluster gate running K-band unit tests + end-to-end equivalence check.
scripts/eval/sbatch_eval_uniform_halve.sb GPU evaluation script for a uniform (halve) config (KID/FID/etc).
scripts/eval/sbatch_eval_mp_flop_halve.sb GPU evaluation script for FLOP+halve adaptive MP configs.
scripts/eval/sbatch_eval_ab_kid100.sb One-off A/B KID evaluation on a fixed 100-image index set.
scripts/eval/eval_mp_flop_halve_cpu.sh CPU-only eval script for FLOP+halve adaptive MP configs.
scripts/calibrate_mp_thresholds.py Calibration updates: FLOP-weighted budgeting, halve guards, mse_rel, per-row qk recording, and halve propagation into probes.
scripts/build_kband_table.py Tool to add a k_bands section to a calibrated MP table (tilt-based ladder builder).
qdit/sc_integration/sc_mlp.py Implements K-band dispatch for MLP contraction-axis banding + guards for combined MP path.
qdit/sc_integration/sc_kbands.py New helper module: resolve/validate band configs, build band column indices, reject unsupported combined modes.
qdit/sc_integration/sc_controller.py Validates K-band allocations at init (supported ops + halve ceiling).
qdit/sc_integration/sc_attention.py Implements K-band dispatch for attention linears + switches QK MP to per-query-row path; adds combined-path guard.
.gitignore Ignores SLURM logs and generated figures.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tools/smoke_test_e2e.py

section("2. sc_matmul granularity sweep — basic correctness vs torch.matmul")
from scmp_kernels.sc import sc_matmul
from scmp_kernels.sc.config_helpers import make_sobol_simple_config
Comment thread tools/sc_call_counter.py
Comment on lines +10 to +11
ROOT = Path('/gpfs/accounts/nbleier_owned_root/nbleier_owned1/zhkangqi/scmp_diffusion')
sys.path.insert(0, str(ROOT))
Comment on lines +6 to +7
ROOT = Path('/gpfs/accounts/nbleier_owned_root/nbleier_owned1/zhkangqi/scmp_diffusion')
sys.path.insert(0, str(ROOT))
Comment on lines +845 to +847
for i in range(sc_flat.shape[0]):
qk_errors_by_level[self.calibrator.levels.index(sl)].append(
_METRIC_ROWS(sc_flat[i], teacher_flat[i].float())
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.

2 participants