Model compression for ONNX, tweaked for parakeet v3 int8 smooth quant
Python
1
56 commits
updated Jun 16, 2026
This is a fork of onnx/neural-compressor
whose diy branch fixes and speeds up the SmoothQuant static int8 path. It is
used (vendored as a submodule) to build the SmoothQuant int8 Parakeet TDT 0.6B v3
ASR encoder published at
Olicorne/parakeet-tdt-0.6b-v3-smoothquant-onnx,
which serves the browser app
parakeet_web. The changes
are candidates for upstream PRs; until those exist, here is what diy changes
versus the upstream repo:
Correctness:
alpha="auto" consumed the calibration reader before the search, so every layer
silently got alpha_min; the reader is now rewound and the per-node reference
activations are cached (also fixes the large-model path). (commit d6745e4)np.arange stopped one
step short of alpha_max, so the maximum alpha was never evaluated. (commit 8d88f45)execution_provider= to the Calibrator, whose constructor
parameter is providers=, so the argument fell into **kwargs and was
silently dropped: every smoother-calibration forward ran on the
CPUExecutionProvider even when the caller selected CUDA. The provider list is
now forwarded, so --ep cuda actually calibrates on the GPU. (commit ad8c01b)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, op types Conv+Gemm+MatMul+FusedConv)
no matter what the caller set. A new _smoothquant_transform_params() maps the
documented extra_options names to the transform() argument names so they
take effect. (commit d6745e4)Memory and speed:
6f69d55)594f6f9, f2a102f)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() from
the fed array, so equal-length calibration is numerically unchanged), letting one
calibration set mix short and long clips, e.g. per-language short utterances plus
full-length speeches. (commit 0a22d36)d6745e4)numpy_helper.to_array reads materialize 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 whole proto dies), so retained RAM grew with
nodes x alphas and survived into static calibration: a 0.1-step grid OOMed a real
encoder 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)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 and the smoother's dump session. (commit 6c6e0cf)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
swap-thrashed mid-pass. 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)dump_batch_size > 0 (the CalibDumpBatch extra_option) the calibrated 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 calibration ranges stay bit-identical to the single-pass
dump (it only trades extra forwards for a smaller peak). (commit a495323)Features:
min:max:step grid). (commit 280b123)0a532e1)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, trading a little file size for accuracy on the layers int8 hurts most. (commit 89b4363)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 grid 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 that produced the static-calibration partial, so a resume with a
changed CalibDumpBatch or tensor set is discarded rather than applied to the
wrong slices. (commits 55a3c75, 628f57f, 9eb2d4e, 0c58d7b)e792411)82e3451, 9f0e94b)The changes are regression-tested from the model repo
(scripts/test_quantize-int8-smoothquant.py).
This fork was developed with Claude Code.
The original upstream README follows.
Model compression for ONNX, tweaked for parakeet v3 int8 smooth quant
Python
1
56 commits
updated Jun 16, 2026
This is a fork of onnx/neural-compressor
whose diy branch fixes and speeds up the SmoothQuant static int8 path. It is
used (vendored as a submodule) to build the SmoothQuant int8 Parakeet TDT 0.6B v3
ASR encoder published at
Olicorne/parakeet-tdt-0.6b-v3-smoothquant-onnx,
which serves the browser app
parakeet_web. The changes
are candidates for upstream PRs; until those exist, here is what diy changes
versus the upstream repo:
Correctness:
alpha="auto" consumed the calibration reader before the search, so every layer
silently got alpha_min; the reader is now rewound and the per-node reference
activations are cached (also fixes the large-model path). (commit d6745e4)np.arange stopped one
step short of alpha_max, so the maximum alpha was never evaluated. (commit 8d88f45)execution_provider= to the Calibrator, whose constructor
parameter is providers=, so the argument fell into **kwargs and was
silently dropped: every smoother-calibration forward ran on the
CPUExecutionProvider even when the caller selected CUDA. The provider list is
now forwarded, so --ep cuda actually calibrates on the GPU. (commit ad8c01b)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, op types Conv+Gemm+MatMul+FusedConv)
no matter what the caller set. A new _smoothquant_transform_params() maps the
documented extra_options names to the transform() argument names so they
take effect. (commit d6745e4)Memory and speed:
6f69d55)594f6f9, f2a102f)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() from
the fed array, so equal-length calibration is numerically unchanged), letting one
calibration set mix short and long clips, e.g. per-language short utterances plus
full-length speeches. (commit 0a22d36)d6745e4)numpy_helper.to_array reads materialize 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 whole proto dies), so retained RAM grew with
nodes x alphas and survived into static calibration: a 0.1-step grid OOMed a real
encoder 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)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 and the smoother's dump session. (commit 6c6e0cf)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
swap-thrashed mid-pass. 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)dump_batch_size > 0 (the CalibDumpBatch extra_option) the calibrated 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 calibration ranges stay bit-identical to the single-pass
dump (it only trades extra forwards for a smaller peak). (commit a495323)Features:
min:max:step grid). (commit 280b123)0a532e1)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, trading a little file size for accuracy on the layers int8 hurts most. (commit 89b4363)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 grid 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 that produced the static-calibration partial, so a resume with a
changed CalibDumpBatch or tensor set is discarded rather than applied to the
wrong slices. (commits 55a3c75, 628f57f, 9eb2d4e, 0c58d7b)e792411)82e3451, 9f0e94b)The changes are regression-tested from the model repo
(scripts/test_quantize-int8-smoothquant.py).
This fork was developed with Claude Code.
The original upstream README follows.