Skip to content

enhancements: sharing mine - #51

Open
thiswillbeyourgithub wants to merge 26 commits into
onnx:mainfrom
thiswillbeyourgithub:diy
Open

enhancements: sharing mine#51
thiswillbeyourgithub wants to merge 26 commits into
onnx:mainfrom
thiswillbeyourgithub:diy

Conversation

@thiswillbeyourgithub

@thiswillbeyourgithub thiswillbeyourgithub commented Jun 16, 2026

Copy link
Copy Markdown

(Thank you very much for this library, it's really nice).

I am aware this is absolutely not merge-ready, I made this PR to show my changes and if you're interested I can make dedicated PRs for it.

If you're not interested and think of this as noise, then I apologize and wish you a nice day.

Context

I had to add a bunch of features to my fork that were needed to improve performance especially on avoiding OOM errors on my parakeet-v3 requantization.

More context on why I had to apply smoothquant to parakeet v3 is listed here on my hugging face repo.

This was entirely done with Claude Code Opus (and a bit of Fable), I then used it with good success and performance.

Expected Behavior & Potential Risk

It grew from frustration from OOM and limitations on alpha parameters initially.

How has this PR been tested?

These changes are regression-tested from the downstream model repo (scripts/test_quantize-int8-smoothquant.py, T1-T30), which pins the bit-identical invariants (cache on == off, sliced dump == single-pass dump, streamed top-K percentile == stacked np.percentile, variable-length auto-alpha).

I'm calling it in run.sh in my fork if you want more reproduction.

Dependency Change?

None

What this fork changes (SmoothQuant static int8 path)

These are the changes on the diy branch versus onnx/neural-compressor, written
up as candidates for upstreaming. They all sit in the SmoothQuant static int8
path and were driven by exporting a real 0.6B FastConformer ASR encoder (long
calibration windows, wide auto-alpha grids), which surfaced several upstream bugs
and scaling walls. Grouped below as correctness fixes, memory/scaling, and new
features. Commit hashes are on this fork's diy branch.

Correctness fixes

  • SmoothQuant extra_options are no longer silently ignored.

    • quantize() routes a StaticQuantConfig (with extra_options["SmoothQuant"])
      into smooth_quant_entry, which called smoother.transform(**quant_config.get_model_params_dict()).
      But get_model_params_dict() only surfaces the static-quant knobs, so NONE of
      the smooth knobs (SmoothQuantAlpha, SmoothQuantOpTypes, AutoAlphaArgs, ...)
      reached transform(): the smoother always ran with its hard-coded defaults
      (alpha=0.5, the [0.3, 0.7] auto grid, Conv+Gemm+MatMul+FusedConv) regardless
      of what the caller set.
    • A new _smoothquant_transform_params() maps the documented extra_options names
      to the transform() argument names so they take effect.
    • (in quantization/algorithm_entry.py)
  • The selected execution provider reaches the smoother calibration.

    • The smoother passed execution_provider= to the Calibrator, whose constructor
      parameter is providers=, so the argument fell into **kwargs and was dropped.
    • Every smoother-calibration forward therefore ran on CPU even when the caller
      asked for CUDA. The provider list is now forwarded correctly.
    • (commit ad8c01b)
  • Auto-alpha search no longer runs on an exhausted dataloader.

    • Upstream's alpha="auto" consumed the calibration reader during setup, so the
      per-node search saw no data and every layer silently fell back to alpha_min.
    • The reader is now rewound and the per-node reference activations are cached, so
      the search actually evaluates the grid (this also fixes the large-model path).
    • (commit d6745e4)
  • The alpha grid now includes both endpoints.

    • Upstream built the grid with np.arange, which stops one step short of the
      stop value, so the maximum alpha (alpha_max) was never evaluated.
    • The grid is now inclusive of both alpha_min and alpha_max.
    • (commit 8d88f45)

Memory and scalability

  • Streaming Entropy/Percentile static calibration.

    • Static calibration buffered every dumped activation tensor of every sample until
      the end of the loop (an in-code upstream TODO), so RAM grew as samples x tensors
      and large exports OOMed.
    • Each sample is now folded straight into its per-tensor histogram and freed; peak
      RAM no longer depends on the sample count.
    • (commit 6f69d55)
  • The auto-alpha QDQ sub-graph session is built once per node.

    • It was rebuilt once per (node, alpha), and originally once per (node, alpha,
      sample), so a wide alpha grid ballooned ORT arena memory and rebuild time.
    • Candidate weights are now fed as runtime inputs to a single per-node session, so
      the alpha sweep is just repeated run() calls.
    • (commits 594f6f9, f2a102f)
  • Variable-length calibration windows are accepted.

    • That once-per-node QDQ sub-graph baked the FIRST sample's concrete activation
      shape into its value_infos, so any later window of a different sequence length
      was rejected (Got invalid dimensions for input ... Got: N Expected: M).
      Calibration was silently constrained to uniformly-sized windows.
    • The activation input/output dims are now left dynamic (ORT resolves the real
      shape per run(), so equal-length calibration is numerically unchanged), letting
      one calibration set mix short and long clips.
    • (commit 0a22d36)
  • Per-node activation caching across the alpha grid.

    • Each alpha evaluation re-harvested the same reference activations for a node.
    • They are now harvested once per node and reused across the whole grid.
    • (commit d6745e4)
  • The large-model alpha search no longer leaks proto arena memory.

    • Each (node, alpha) evaluation rewrote the node's weight initializer twice (scale
      then recover), and even numpy_helper.to_array materializes external weights into
      the proto. Under protobuf's upb backend every such write abandons the old bytes in
      the ModelProto's arena (freed only when the proto dies), so retained RAM grew with
      nodes x alphas and survived into static calibration. A 0.1-step grid OOMed an
      export where a 0.2-step grid fit.
    • Candidate weights are now scaled in numpy and fed to the QDQ-loss session directly,
      and external weights are read via a throwaway TensorProto, so the search retains
      nothing.
    • (commit 72de740)
  • Memory-conservative ORT sessions for the dump/calibration passes.

    • The augmented dump graphs return hundreds of activation tensors per forward, and
      ORT's BFC arenas grow ahead of demand and never release, so the calibration
      sessions carried ~1 GB of allocator slack on a 0.6B encoder and could abort with a
      BFCArena allocation failure on a loaded host.
    • These sessions run only a handful of forwards, so a new
      conservative_session_resources() disables the CPU arena and pins the CUDA arena
      to kSameAsRequested growth, applied to both the static-calibration session and the
      smoother's dump session.
    • (commit 6c6e0cf)
  • Streaming per-channel percentile in the smoother calibration.

    • The smoother calibration stacked every sample's activations and called
      np.percentile once at the end, so RAM grew as samples x frames x channels
      (~6 GB per ~400 s window, hundreds of GB for a multi-window export) and thrashed.
    • Because the high percentiles SmoothQuant uses (99.999 by default) depend only on
      the largest values per channel, each sample is now folded into a per-channel
      running top-K reducer (StreamingChannelPercentile) and freed. The reducer
      reproduces np.percentile bit-for-bit (its float64 promotion and t >= 0.5 lerp
      branch included) and re-checks that K was large enough for the actual row count, so
      a result can never be silently wrong.
    • (commit e61fb24)
  • Sliced static-calibration dump to bound VRAM.

    • Static int8 calibration augments the model so EVERY calibrated tensor becomes a
      graph output, and ORT keeps all graph outputs resident for the whole forward, so a
      long window peaks every activation at once (per-layer attention-score MatMuls are
      ~0.8 GB each near a FastConformer's ~400 s reach) and overflows VRAM.
    • With extra_options["CalibDumpBatch"] > 0 the tensors are dumped in slices of that
      many graph outputs, each slice its own augment + forward over the same (rewound)
      windows, so only one slice is resident at a time. The per-tensor ranges stay
      bit-identical to the single-pass dump (it only trades extra forwards for a smaller
      peak).
    • (commit a495323)

New features and usability

  • Per-op-type alpha override in the auto-alpha search.

    • Pin an op type to a fixed alpha, or give it its own min:max:step grid, instead
      of one global grid for all smoothed ops.
    • (commit 280b123)
  • Sensitivity-based mixed precision (extra_options["SmoothQuantExcludeWorst"]).

    • The auto-alpha search records every smoothed node's best achievable QDQ loss,
      normalized by that node's reference-output energy so it is comparable across nodes
      (Smoother.auto_alpha_losses).
    • Setting the option to an int n (or a fraction in (0, 1)) keeps the n worst-quantizing
      nodes out of quantization entirely (they stay fp32), trading a little file size for
      accuracy on the layers int8 hurts most. Only searched nodes are rankable, so with a
      fixed alpha it is a logged no-op.
    • (commit 89b4363)
  • Resumable, crash-safe exports.

    • Given a checkpoint directory (extra_options["SmoothQuantCheckpointDir"] plus
      CalibParamsCheckpointFile), the smoother persists its expensive intermediates as
      they are produced and reloads whatever already exists on the next run: the smoother
      calibration, the per-node alpha search (append-only alphas.jsonl, one line per
      completed node, so an interrupted multi-hour search resumes from the last completed
      node), and the static-calibration quantization params.
    • Both per-sample calibration loops also checkpoint MID-pass, time-gated by
      CheckpointIntervalSec (default 1200 s) so a fast pass pays no IO, so an OOM- or
      time-killed pass resumes from the last sample instead of restarting.
    • All writes are atomic (temp + os.replace) so a crash mid-write never leaves a
      truncated file. Checkpoints carry no model fingerprint (the caller keys the
      directory by its full run configuration and owns invalidation) but DO record the
      slice partition behind the static-calibration partial, so a resume with a changed
      CalibDumpBatch or tensor set is discarded rather than misapplied.
    • (commits 55a3c75, 628f57f, 9eb2d4e, 0c58d7b)
  • Per-layer alpha summary logged after the search.

    • A histogram per op type plus one line per smoothed node showing the alpha the
      search picked, so the export is auditable instead of opaque.
    • (commit 0a532e1)
  • Richer quantization statistics table.

    • fp32 ops are split into "quantizable" (weight-bearing, the quantizer could convert
      them directly) versus "needs an int8 input" (weightless/pass-through ops that only
      convert inside an int8 region), and the Reshape/Transpose glue rows are always shown.
    • (commit e792411)
  • Progress bars and quieter logs.

    • tqdm progress bars over the alpha search and both calibration passes, and the
      per-evaluation ORT warning spam is silenced.
    • (commits 82e3451, 9f0e94b)

thiswillbeyourgithub and others added 26 commits June 10, 2026 08:53
…h activations

These are the fixes that the parakeet_web SmoothQuant export script
(scripts/quantize-int8-smoothquant.py) previously installed as runtime
monkeypatches; moving them into the library so the script can import a
fixed Smoother directly.

Four independent issues in the SmoothQuant path:

1. Auto-alpha ran on an exhausted dataloader. transform() drains the
   calibration reader in _dump_op_info and never rewinds before
   _auto_tune_alpha, so every _get_output_loss saw an empty reader
   (loss 0) and the per-layer optimal alpha collapsed to alpha_min for
   every node. _get_output_loss now rewinds the reader before each
   evaluation.

2. Large-model auto-alpha could not fetch its per-node tensors. The
   augment file was saved with only the model's original outputs, so
   session.run() asking for a node's intermediate in/out tensors raised
   "Invalid output name". _auto_tune_alpha now exposes every per-node
   tensor as a graph output before saving the augment file (and tears
   them back down afterwards so the returned model is unchanged).

3. Large-model auto-alpha rebuilt no session reuse. Those per-node
   activations are alpha-independent on the large path (the augment
   snapshot keeps the original weights), so they are now harvested once
   per node from a single shared session and cached across the whole
   alpha grid; only the cheap per-alpha QDQ sub-graph is recomputed.

4. smooth_quant_entry dropped the SmoothQuant* knobs. quantize() routes
   a StaticQuantConfig here, but get_model_params_dict() surfaces only
   the static-quant knobs, so Smoother.transform got none of
   SmoothQuantAlpha / SmoothQuantFolding / SmoothQuantOpTypes /
   AutoAlphaArgs and always used its hard-coded defaults. They are now
   translated to the transform() kwargs.

Also: _get_smooth_scale now skips a node whose per-channel activation
max axis and weight in-channel length disagree (e.g. FastConformer
relative-position attention MatMuls) instead of crashing on the
mismatched broadcast; such nodes fall through to plain static
quantization. And _auto_tune_alpha logs a throttled ETA for the search,
which is by far the slowest phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SmoothQuant auto-alpha search was opaque and noisy. It printed a bare
"Progress: [####] 100.00%" (utility.simple_progress_bar) from _adjust_weights,
which is called once per (node, alpha), so the bar restarted to 100% on every
evaluation and told you nothing about overall progress. On top of that, every
evaluation rebuilds an InferenceSession, and ORT logs a WARNING for each pruned
*_smooth_scale initializer (graph.cc CleanUnusedInitializersAndNodeArgs),
flooding the console with thousands of identical lines.

- Drive the auto-alpha search with a real tqdm bar ("SmoothQuant auto-alpha",
  unit=eval) over the n_nodes x n_alpha evaluations, giving a live count, rate
  and ETA. This replaces the hand-rolled throttled-ETA logging (and the
  _format_duration/time helpers it needed).
- Silence _adjust_weights' own bar while it runs inside the search (new
  self._quiet_adjust flag), and give it a descriptive tqdm bar
  ("SmoothQuant: folding scales into weights") for the final full-model apply.
- Build the smoother's InferenceSessions with log_severity_level=3 so the
  per-eval "Removing initializer" warnings no longer drown the progress bar.
- Declare the new tqdm dependency in setup.py and requirements.txt.

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
np.arange(alpha_min, alpha_max, alpha_step) is half-open and stops one step
short of alpha_max, so a request to search e.g. 0.3..0.7 silently never tried
0.7. Nudge the stop value by half a step so alpha_max is included without a
floating-point spurious extra step. Now searching a..b evaluates both a and b,
which is the expected behaviour.

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Start smooth model calibration" phase runs every calibration sample through
the augmented model in a bare while-loop with no progress output, so on a large
encoder it just appears to hang for a while. Wrap it in a tqdm bar
("SmoothQuant: collecting calibration activations", total = calibration sample
count) so the calibration pass shows live progress and ETA.

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an op_alpha argument to _auto_tune_alpha (threaded through transform's
auto_alpha_args, so no config/entry plumbing changes): a map from op type to
either a fixed float (pin that op's alpha, no search, one forward pass saved per
node) or a {alpha_min, alpha_max, alpha_step} dict (a custom grid). Op types
absent from the map use the global grid, so the default behaviour is unchanged
(op_alpha=None reproduces the old single-grid search exactly).

This lets you spend the (expensive) search budget where it matters: pin the ops
you already trust (e.g. MatMul=0.5) and only search the uncertain ones. Pinned
ops short-circuit the QDQ loss evaluation entirely. An op_alpha key that matches
no smoothed node (not in op_types, or weightless like Slice) is warned and
ignored. Grid selection is factored into _alpha_grid / _node_alpha_space.

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… per sample

In the auto-alpha loss eval, the single-node QDQ sub-graph is identical across all
calibration samples for a fixed (node, alpha), but _get_quant_dequant_output rebuilt
its ort.InferenceSession on every sample. On the large path (activations already
cached) that per-sample session build was the dominant remaining cost: ~n_samples
session constructions per (node, alpha) where one suffices.

Split _get_quant_dequant_output into _build_qdq_session (build once) + _qdq_loss
(run per sample against the prebuilt session), and hoist the build alongside the
existing once-per-call _make_sub_graph in both the small and large paths. Pure
speedup: the model fed to the session is byte-identical to before, so the selected
alphas are unchanged (the pinned-alpha tests T2/T3/T4/T5 still pass identically).
Measured ~2 builds/eval vs ~(1 + n_samples)/eval before.

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At the end of the auto-alpha search, log which alpha each smoothed layer
settled on, grouped by op type: a per-op-type value histogram (so it is
obvious at a glance whether the default just won everywhere or the search
genuinely moved layers) followed by the full per-layer list so a diverging
layer is identifiable by name. The mapping already existed (optimal_alphas);
it just was never printed. Pure formatter (_format_alpha_summary) split out
so it is unit-testable without a logger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, alpha)

The auto-alpha loss eval rebuilt the single-node QDQ sub-graph session on every
(node, alpha): n_alpha ort.InferenceSession constructions per node. Each ORT
session arena is never handed back to the OS (glibc keeps the freed pages), so a
wide alpha grid multiplied that churn until the search exhausted memory and the
OOM-killer took down unrelated processes.

The sub-graph is structurally identical for every alpha at a fixed node (same
node, same activation/weight shapes); only the quant-dequant weight VALUES change.
So feed the weight as a runtime graph INPUT instead of baking the alpha-dependent
weight as an initializer: the session is then alpha-independent and is built ONCE
per node and cached on self._sq_subgraph, fed the per-alpha weight at run time.
Session builds drop from n_nodes * n_alpha to n_nodes, so grid width no longer
drives memory.

- _make_sub_graph: weight is now a second graph input (bias / other non-weight
  inits stay baked, they are alpha-independent); _qdq_loss feeds both the
  quant-dequant activation and the quant-dequant weight per call.
- _get_output_loss: split the reference-activation harvest into
  _reference_activations (small path: per-call full-model session as before; large
  path: the existing per-node original-weight cache) and reuse the cached per-node
  sub-graph session across the alpha grid. Also drops the per-alpha set_initializer
  round-trip (which inlined external weights via a giant float list each alpha).

Pure memory/throughput change: the sub-graph fed to the session is numerically
identical to before, so the selected alphas are unchanged (the parakeet_web T2
small-path and T3 large-path searches return byte-identical per-layer alphas, and
the new T8 pins the once-per-node session-build count).

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pose

The end-of-run Quantization Statistics table lumped every fp32 node into one
column, which made the 72 weightless attention MatMuls (Q@K^T, probs@V) and
the pass-through Slices outside int8 regions look like quantization failures.
Split FP32 into 'quantizable' (weight-bearing with a weight initializer:
directly actionable) and 'needs-int8-input' (weightless or pass-through ops
that only turn int8 when an int8 region reaches them), and always include the
Reshape/Transpose glue rows since they decide whether an int8 region can reach
the weightless MatMuls. Census logic extracted into the pure
collect_op_quant_stats for testing.

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… all tensors

Upstream get_activation_tensors_calib_range only streamed MinMax; for
Entropy/Percentile it appended EVERY dumped activation tensor of EVERY
calibration sample to intermediate_tensor and collected once at the end
(the in-code TODO). RAM therefore grew linearly with samples x dumped
tensors, and a 24-window Parakeet encoder export with
--op-types MatMul,Conv,Slice,Transpose,Reshape OOMed under a 90 GB cap
during static calibration.

HistogramCollector already merges increments (combine_histogram), so the
per-tensor calibrator is now kept in name_to_calibrator for every method
and fed each sample as it arrives; the end-of-loop pass just reads out
the final ranges. Peak memory no longer depends on the sample count.
Bool-dtype outputs keep being skipped for histogram methods, matching
the old end-of-loop filter.

Numerics note: incremental histogram merging can place bin edges
slightly differently than one flatten over the full buffered list; both
are valid percentile estimates at histogram resolution.

Covered by T12 in the model repo's scripts/test_quantize-int8-smoothquant.py.

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An "About this fork" section at the top lists the SmoothQuant fixes and
features carried by the diy branch (auto-alpha rewind fix, inclusive
grid, streaming percentile calibration, per-node QDQ session, per-op
alpha, stats census, progress bars), where they are tested, and the
Parakeet model repo that uses them, so the fork is understandable even
if upstream PRs never materialize.

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The augmented dump graphs return hundreds of activation tensors per
forward pass, and ORT's BFC arenas grow ahead of demand (power-of-two
extends) and never release, so the calibration sessions carried ~1 GB of
pure allocator slack on a 0.6B encoder (measured: 9.6 -> 8.5 GB RSS
plateau) and could die with a BFCArena "Failed to allocate memory" abort
on a loaded host. These sessions only run a handful of forward passes,
so the allocator-speed trade is free: new
conservative_session_resources() disables the CPU arena and pins the
CUDA arena to kSameAsRequested growth with the heuristic cuDNN algo
search, applied to both the static-calibration session (calibrate.py)
and the smoother's dump session (smoother/calibrator.py).

Covered by T13 in the model repo's scripts/test_quantize-int8-smoothquant.py.

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The auto-alpha search rewrote each searched node's weight initializer
twice per (node, alpha) evaluation (_adjust_weights then recover), and
numpy_helper.to_array on an external-data tensor ALSO assigns
tensor.raw_data into the live proto on every read (leaving
data_location EXTERNAL, so each later read assigns again). Under
protobuf's upb backend (the default since protobuf 4) every such
assignment abandons the previous bytes in the ModelProto's arena,
which is freed only when the WHOLE proto dies. Retained RAM therefore
grew with n_nodes x n_alphas and survived past the search into static
calibration: on a real 0.6B encoder a 0.1-step grid OOMed right after
the per-layer alpha summary where a 0.2-step grid fit.

Fix, on the large-model path (the references come from the
original-weight augment session there, so nothing needs the adjusted
weight in the proto):
- _scaled_weight computes the candidate weight in numpy (extracted
  from _adjust_weights, which now delegates to it) and the search
  feeds it straight to _get_output_loss via its new weight parameter;
  no in-proto adjust/recover per evaluation at all. Bonus: each alpha
  now scales the pristine on-disk weight instead of one that drifted
  through k scale/unscale float roundtrips.
- _to_array_extern_safe reads external weights through a throwaway
  TensorProto so the bytes land in the throwaway's own arena (freed on
  return), never the model's. Used for every weight read in the
  smoother (scales, candidate scaling, recover, QDQ loss).

The small-model path keeps the in-proto adjust/recover (its reference
activations are recomputed from the live model per call, so the loss
semantics need the real adjustment; small protos keep the leak
negligible).

Measured on a forced-large 8-node chain of 4 MB weights over an
11-point grid (88 evals): retained RSS across the search dropped from
the predicted ~700 MB (2 weight-size blocks per eval) to +21 MB, which
is just the legitimate end-of-search external-data load. Covered by
T14 in the model repo's scripts/test_quantize-int8-smoothquant.py
(fails pre-fix: every searched weight shows in-proto raw_data during
the search; passes post-fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…clusion

The auto-alpha search now records every searched node's best achievable
QDQ loss, normalized by that node's reference-output energy so the values
are comparable across nodes (Smoother.auto_alpha_losses; pinned ops record
none). smooth_quant_entry consumes the new
extra_options["SmoothQuantExcludeWorst"] (an int n or a fraction in
(0,1)) by extending nodes_to_exclude with select_worst_nodes() so the
most quantization-damaged layers stay fp32: sensitivity-based mixed
precision. Covered by T15 in the model repo's test suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When given a checkpoint_dir (extra_options SmoothQuantCheckpointDir, threaded
through transform()), the smoother now persists its expensive intermediates as
they are produced and loads whatever is already there on the next run:

- _dump_op_info saves/loads the smoother calibration (smooth-calib.npz/.json:
  max_vals_per_channel, shape_info, tensors_to_node), skipping every
  calibration forward on resume.
- _auto_tune_alpha rewrites alphas.json after EVERY completed node grid (best
  alpha, normalized best QDQ loss, and the full raw per-alpha loss curve), and
  restores checkpointed nodes instead of re-searching them, so an OOM-killed
  multi-hour search resumes from the last completed node. A fully-cached
  large-model resume also skips the augment save (no weights.pb churn).
- static_quantize_entry caches the calibration quantization params
  (extra_options CalibParamsCheckpointFile); a rerun skips the augmented dump
  sessions, the RAM peak of a SmoothQuant export.

All checkpoint writes are atomic (temp + os.replace) so a crash mid-write can
never leave a truncated file. The files carry no model fingerprint on purpose:
the caller keys the directory by its full run configuration and owns
invalidation (the parakeet export script hashes its arguments + input
fingerprints into the dir name).

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One JSON object per line, appended as each node's grid completes, instead of
rewriting the whole file after every node: O(1) per node, and a crash/OOM kill
can only damage the line being written. load_alpha_checkpoint skips a corrupt
(truncated) line with a warning, so that node is simply re-searched; when a
node appears twice the later line wins. Entries gain a "node" field since the
filename no longer carries the mapping.

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ibrates on the GPU

The Calibrator ctor parameter is providers, but _dump_op_info passed
execution_provider=, which fell into **kwargs and was ignored: every
smoother-calibration forward silently ran on the CPUExecutionProvider
even when the caller selected CUDA. Pinned by a test in the model repo
(scripts/test_quantize-int8-smoothquant.py T22).

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A SmoothQuant export killed inside a calibration pass used to redo the
whole pass on resume; only completed steps were checkpointed. Now both
per-sample loops dump their in-progress state, time-gated so a fast pass
never pays the IO (checkpoint_interval_sec, default 1200 s, 0 dumps
after every sample):

- smoother calibration (the 'collecting calibration activations' pass):
  each collected sample's activations go to <checkpoint_dir>/smooth-acts/
  sample-NNNNN.npz (atomic, positional arrays keyed once by names.json).
  On the next run the contiguous prefix is restored and those forwards
  are skipped entirely; a dump for a different tensor set is discarded.
  The tail is only flushed when a periodic flush already happened, and
  the bulky per-sample files are deleted as soon as the full smooth-calib
  checkpoint supersedes them.
- static Percentile/Entropy/MinMax calibration: the streaming per-tensor
  calibrator state (running ranges / incremental histograms, small) is
  pickled to <CalibParamsCheckpointFile>.partial and already-consumed
  samples are skipped on resume; the partial is removed once the final
  params land. A corrupt partial is discarded, never crashes the run.

New extra_options key CheckpointIntervalSec threads the interval through
both smooth_quant_entry and static_quantize_entry.

Tested by T22/T23 in the model repo's
scripts/test_quantize-int8-smoothquant.py.

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The smoother calibration stacked every collected sample's activations in
RAM and called np.percentile once at the end, so memory grew as
samples x frames x channels: ~6 GB per 395 s window on a 0.6B encoder,
~444 GB for a 75-window export, which swap-thrashed the box mid-pass.
For the high percentiles SmoothQuant uses (99.999 default) the answer
only depends on the few largest values per channel, so calib_smooth now
folds each sample into a StreamingChannelPercentile reducer (per-channel
running top-K + exact row count) and frees it; the reducer reproduces
np.percentile bit-for-bit, including its float64 promotion and the
t >= 0.5 lerp rearrangement, and re-checks K sufficiency from the actual
row count so a result can never be silently wrong. Percentiles too low
to stream are rejected up front.

The mid-pass checkpoint becomes the reducer state (one atomic npz,
smooth-stream.npz) instead of raw per-sample activation dumps, which
also makes it cheap to flush; the smooth-acts machinery is removed (the
cleanup still sweeps a leftover smooth-acts/ dir from older runs).

Pinned by T22 in the model repo's scripts/test_quantize-int8-smoothquant.py
(bit-exactness across 2D/3D/4D layouts and both lerp branches, mid-pass
resume forwards count, config-mismatch discard, fast-pass no-write,
cleanup after the full checkpoint).

Built with Claude Code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ogress bar)

The static int8 calibration augments the model so EVERY calibrated tensor is a
graph output, and ORT keeps every graph output resident for the whole forward.
On a long calibration window that makes all of one window's activations peak at
once (the per-layer attention-score MatMuls are ~0.8 GB each near a FastConformer
encoder's ~400 s reach), overflowing GPU VRAM with a BFCArena "Failed to allocate"
abort even though the smoother and alpha-search passes fit (they augment far fewer
tensors per forward).

ONNXRTAugment gains dump_batch_size: when > 0, get_activation_tensors_calib_range
dumps the calibrated tensors in slices of that many graph outputs, each slice its
own augment + forward pass (the dataloader is rewound between passes), so only one
slice's outputs are retained at a time. augment_graph takes a tensor_filter to
build a per-slice graph; the model's own outputs cannot be dropped from a slice's
graph, so they are calibrated only in their owning slice (slice_owned skip). Each
tensor still sees the same windows, so the per-tensor calib range is bit-identical
to the single-pass dump; dump_batch_size only trades extra forwards for a smaller
peak. Gated to the SmoothQuant static-calib path (not augment_nodes / not
already_quantized). Threaded from static_quantize_entry via the CalibDumpBatch
extra_option.

The mid-pass .partial checkpoint is now (slice, samples-in-slice)-aware: earlier
slices are complete, a resumed run skips them and forwards only the remainder.
Backward-compatible via partial.get("batch", 0); the single-pass path is exactly
slice 0 of 1 and is unchanged. augment_graph moved out of dump_minmax into
get_activation_tensors_calib_range (the batched path re-augments per slice; calling
it in both would double-augment). Added a tqdm progress bar over the calibration
loop (one bar per slice), matching the smoother's bars. Dropped three vestigial
locals (ort_inputs_for_next_split_model, inputs_names, len_*).

Covered by T24 in the model repo (batched == single-pass bit-exact, one session
per slice, cross-slice mid-pass resume).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The mid-pass .partial records a (slice index, samples) restore point, but that
point is only meaningful against the slice partition that produced it. Resuming
with a different dump_batch_size (or a changed calibrated tensor set) re-slices
the dump, so the stored slice index then spans different tensors: the old resume
would skip the wrong slices and silently leave tensors uncalibrated, returning
incomplete/wrong ranges.

Store the partition in the partial and discard a checkpoint whose partition does
not match the current one (this also drops pre-partition checkpoints, which carry
no signature), restarting the activation calibration from scratch rather than
resuming it wrong. Mirrors the smoother's stale-checkpoint discard. The completed
quantize-params .pkl is partition-invariant and still reused as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The once-per-node QDQ sub-graph (_make_sub_graph) baked the first calibration
sample's concrete activation shape into its input/output value_infos. That
session is cached on self._sq_subgraph and reused for every sample, so any
later window with a different sequence length was rejected by ORT with
"Got invalid dimensions for input ... Got: N Expected: M". Calibration was
therefore silently constrained to uniformly-sized windows.

Leave the activation input/output dims dynamic (rank preserved, every dim
None); ORT resolves the real shape per run() from the fed array, so the
weight (a constant-shaped runtime input) is untouched and equal-length
calibration is numerically unchanged (the value_info shape is metadata that
never enters the MatMul math). One calibration set can now mix short and long
clips, e.g. per-language short utterances plus full-length speeches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Catches the "About this fork" section up to every diy commit since the
README was last meaningfully edited (05283d8):

- Correctness: the smoother passed execution_provider= where the
  Calibrator expects providers=, so --ep cuda silently calibrated on CPU
  (ad8c01b).
- Memory and speed: memory-conservative calibration sessions (6c6e0cf),
  streaming per-channel top-k percentile in the smoother calibration
  (e61fb24), and the sliced static-calibration dump that bounds VRAM via
  CalibDumpBatch (a495323).
- Features: resumable, crash-safe exports (SmoothQuantCheckpointDir /
  CalibParamsCheckpointFile / CheckpointIntervalSec, append-only
  alphas.jsonl, mid-pass per-sample checkpoints, partition-keyed
  static-calibration partial) (55a3c75, 628f57f, 9eb2d4e, 0c58d7b).

Built with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: thiswillbeyourgithub <26625900+thiswillbeyourgithub@users.noreply.github.com>
Every bullet in the "About this fork" section now links to the short
hash of the commit (or commits) that introduced it, so a reader can jump
straight to the change. Covers all 21 substantive diy commits; the three
documentation-only commits have no feature line to attach to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SmoothQuant extra_options were silently ignored upstream because
get_model_params_dict() only surfaces the static-quant knobs; the
_smoothquant_transform_params() fix in d6745e4 was never called out as a
distinct correctness item. Add it to the Correctness list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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