enhancements: sharing mine - #51
Open
thiswillbeyourgithub wants to merge 26 commits into
Open
Conversation
…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>
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.
(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 == stackednp.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
diybranch versusonnx/neural-compressor, writtenup 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
diybranch.Correctness fixes
SmoothQuant
extra_optionsare no longer silently ignored.quantize()routes aStaticQuantConfig(withextra_options["SmoothQuant"])into
smooth_quant_entry, which calledsmoother.transform(**quant_config.get_model_params_dict()).But
get_model_params_dict()only surfaces the static-quant knobs, so NONE ofthe 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) regardlessof what the caller set.
_smoothquant_transform_params()maps the documentedextra_optionsnamesto the
transform()argument names so they take effect.quantization/algorithm_entry.py)The selected execution provider reaches the smoother calibration.
execution_provider=to theCalibrator, whose constructorparameter is
providers=, so the argument fell into**kwargsand was dropped.asked for CUDA. The provider list is now forwarded correctly.
ad8c01b)Auto-alpha search no longer runs on an exhausted dataloader.
alpha="auto"consumed the calibration reader during setup, so theper-node search saw no data and every layer silently fell back to
alpha_min.the search actually evaluates the grid (this also fixes the large-model path).
d6745e4)The alpha grid now includes both endpoints.
np.arange, which stops one step short of thestop value, so the maximum alpha (
alpha_max) was never evaluated.alpha_minandalpha_max.8d88f45)Memory and scalability
Streaming Entropy/Percentile static calibration.
the end of the loop (an in-code upstream TODO), so RAM grew as samples x tensors
and large exports OOMed.
RAM no longer depends on the sample count.
6f69d55)The auto-alpha QDQ sub-graph session is built once per node.
sample), so a wide alpha grid ballooned ORT arena memory and rebuild time.
the alpha sweep is just repeated
run()calls.594f6f9,f2a102f)Variable-length calibration windows are accepted.
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.
shape per
run(), so equal-length calibration is numerically unchanged), lettingone calibration set mix short and long clips.
0a22d36)Per-node activation caching across the alpha grid.
d6745e4)The large-model alpha search no longer leaks proto arena memory.
then recover), and even
numpy_helper.to_arraymaterializes external weights intothe 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.
and external weights are read via a throwaway
TensorProto, so the search retainsnothing.
72de740)Memory-conservative ORT sessions for the dump/calibration passes.
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.
conservative_session_resources()disables the CPU arena and pins the CUDA arenato
kSameAsRequestedgrowth, applied to both the static-calibration session and thesmoother's dump session.
6c6e0cf)Streaming per-channel percentile in the smoother calibration.
np.percentileonce 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.
the largest values per channel, each sample is now folded into a per-channel
running top-K reducer (
StreamingChannelPercentile) and freed. The reducerreproduces
np.percentilebit-for-bit (its float64 promotion andt >= 0.5lerpbranch included) and re-checks that K was large enough for the actual row count, so
a result can never be silently wrong.
e61fb24)Sliced static-calibration dump to bound VRAM.
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.
extra_options["CalibDumpBatch"] > 0the tensors are dumped in slices of thatmany 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).
a495323)New features and usability
Per-op-type alpha override in the auto-alpha search.
min:max:stepgrid, insteadof one global grid for all smoothed ops.
280b123)Sensitivity-based mixed precision (
extra_options["SmoothQuantExcludeWorst"]).normalized by that node's reference-output energy so it is comparable across nodes
(
Smoother.auto_alpha_losses).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.
89b4363)Resumable, crash-safe exports.
extra_options["SmoothQuantCheckpointDir"]plusCalibParamsCheckpointFile), the smoother persists its expensive intermediates asthey 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 percompleted node, so an interrupted multi-hour search resumes from the last completed
node), and the static-calibration quantization params.
CheckpointIntervalSec(default 1200 s) so a fast pass pays no IO, so an OOM- ortime-killed pass resumes from the last sample instead of restarting.
os.replace) so a crash mid-write never leaves atruncated 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
CalibDumpBatchor tensor set is discarded rather than misapplied.55a3c75,628f57f,9eb2d4e,0c58d7b)Per-layer alpha summary logged after the search.
search picked, so the export is auditable instead of opaque.
0a532e1)Richer quantization statistics table.
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.
e792411)Progress bars and quieter logs.
per-evaluation ORT warning spam is silenced.
82e3451,9f0e94b)