feat(mp): per-group (K-band) stream lengths on the contraction axis (stacks on #6) - #7
Open
heroarmor wants to merge 7 commits into
Open
feat(mp): per-group (K-band) stream lengths on the contraction axis (stacks on #6)#7heroarmor wants to merge 7 commits into
heroarmor wants to merge 7 commits into
Conversation
…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>
There was a problem hiding this comment.
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, andSCAttentionvia newsc_kbandshelpers. - Update calibration + runtime wiring around adaptive MP (table-required), including FLOP-weighted budget accounting, halve-mode guards, and
mse_relmetric 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.
|
|
||
| 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 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()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 contractionaxis is partitioned into bands of whole quantization chunks, and band
brunsrung
kat its own lengthL[b][k], under the exact per-rung iso-computeidentity that scmp_kernels#31
validates at table load.
Row dispatch is unchanged. Because
L[b][k] == stoc_len_levels[k]reproducesthe 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.
maindoes not currently run adaptive MP at all. It calls the pre-refactorconvention
while the submodule
mainpins (9358239) takes(metric, config, ...). Anyadaptive-MP forward off
mainraisesTypeError: adaptive_classify_rows() got multiple values for argument 'operator'.#6's
e061384is what fixes the call sites.I first built this as a standalone branch off
main— the Python cherry-pickedcleanly, 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_matmultakes a scalarstoc_len— the chunked MLP path buildscum_indicatoronce outside the chunk loop and every chunk shares it — so thebanding happens above the kernel: rows are grouped by rung index (not by
parent
stoc_lenvalue, since the rung is what indexes every band's ladder) andeach
(band, rung)is a separate call whose partial products are summed.The two paths band differently because they chunk differently:
sc_attention.pychunk_drequiresgranularity="per_row", and this path can runper_tensor), so a band is just a label on those chunkssc_mlp.pyGuards
All four failures below would otherwise surface only as a wrong number:
qk/avin the table → rejected atinit_adaptive_mp. They contractover
head_dim, have no band dispatch, and would run the per-row parentwhile the run carries the k-bands label.
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.--range_mp→ raises. The combined range+dynamic path has nocontraction-axis dispatch and would drop the allocation on the floor.
chunk_dor contraction width → raisesinstead of falling back to the parent.
Tooling
scripts/build_kband_table.pyadds ak_bandssection to an existingcalibrated table under a straight-line tilt, rounding down so the identity
is never overspent and handing single units back largest-lost-fraction-first.
--tilt 0must 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 rung0.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 partitioningincluding tail-chunk ordering, the controller guards, and on GPU both the
parent-equivalence and non-degeneracy gates.
scripts/verify_kband_e2e.pydrives the real_sc_linear_dynamic_mpmethodsrather 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_kernelsis 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:Submodule
Bumps
scmp_kernelstoheroarmor:feat/k-bands(acf8eca) = #6's pinc967d3a+ thek_bandsconfig layer, one commit and nothing else. The cleanversion of that layer against kernels
mainisscmp_kernels#31;
this pin exists because #6's line carries trace (#25), which never reached
kernels
main.🤖 Generated with Claude Code