feat(kv-cache): add FP8 (E4M3) KV-cache export path - #447
Conversation
Performance Comparison
|
There was a problem hiding this comment.
Pull request overview
This PR adds an opt-in FP8 (E4M3FN) KV-cache export path by introducing a post-fusion IR pass that retypes com.microsoft::GroupQueryAttention cache I/O to FLOAT8E4M3FN, wires in per-layer k_scale/v_scale inputs, and sets the required quantization attributes. The feature is surfaced through optimize_model(), the Python build APIs, and the CLI mobius build command.
Changes:
- Add
Fp8KvCachePass+ JSON scale-file loader to retype KV cache I/O to FP8 and inject scale inputs/attrs. - Thread
fp8_kv_cache+kv_cache_scalesthroughoptimize_model(),build()/build_from_module(), and the CLI (--fp8-kv-cache,--kv-cache-scale-file). - Add structural + CUDA runtime tests validating graph wiring and runtime acceptance of the FP8 GQA signature.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/mobius/_passes/_fp8_kv_cache.py | New optimization pass that converts GQA KV cache I/O to FP8 and adds scale inputs/quant attrs. |
| src/mobius/_passes/_fp8_kv_cache_test.py | New tests for pass structure, scale-file parsing, and CUDA runtime execution. |
| src/mobius/_passes/init.py | Export Fp8KvCachePass from the passes package. |
| src/mobius/_optimizations.py | Add fp8_kv_cache/kv_cache_scales options and run the new pass after fusion. |
| src/mobius/_builder.py | Plumb FP8 KV-cache options through build() / build_from_module() into optimization. |
| src/mobius/main.py | Add CLI flags and load optional scale-file for both --model and --config paths. |
Suppressed comments (2)
src/mobius/_passes/_fp8_kv_cache.py:129
- Using only
layer_idto form thekv_cache.<suffix>.k_scale/.v_scaleinitializer names risks collisions for multi-cache naming schemes (e.g.past_key_values.0.self.*andpast_key_values.0.cross.*). Since the cache value name is already available, derive the suffix from it so each distinct cache gets its own scale initializer.
present_value = node.outputs[2] if len(node.outputs) > 2 else None
_retype_fp8(present_key)
_retype_fp8(present_value)
src/mobius/_passes/_fp8_kv_cache_test.py:155
- This skip condition only checks for the CUDA EP, but the FP8 KV-cache GQA kernel is hardware-gated (SM89+). Without also checking compute capability (or skipping on kernel-not-found errors), this test can fail on older CUDA GPUs.
path = tmp_path / "bad.json"
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (5)
src/mobius/_passes/_fp8_kv_cache.py:126
present_keyuseslen(node.outputs) > 2even though it reads index 1. This incorrectly returnsNonefor valid 2-output variants and makes the index contract inconsistent withpresent_value(index 2).
present_key = node.outputs[1] if len(node.outputs) > 2 else None
present_value = node.outputs[2] if len(node.outputs) > 2 else None
src/mobius/_passes/_fp8_kv_cache.py:15
- Docstring says the pass only applies when
past_key/past_valueare graph inputs, but the implementation converts any GQA node that has those inputs (including initializers, as in the CUDA runtime test). Updating the wording avoids misleading callers about when conversion occurs.
:func:`~mobius._optimizations.optimize_model`). For every decoder
``GroupQueryAttention`` node whose ``past_key`` / ``past_value`` inputs are
graph inputs it:
src/mobius/_passes/_fp8_kv_cache_test.py:244
- This test is gated only on the CUDA EP being available, but the FP8 KV-cache kernel is SM89+ only. On older GPUs with CUDA EP installed,
sess.run()will likely fail with a NOT_IMPLEMENTED/Fail error and turn into a flaky test failure. Wrap the execution in a try/except andpytest.skip(...)when the kernel isn't available on the current CUDA setup.
sess = ort.InferenceSession(str(path), providers=[_CUDA, "CPUExecutionProvider"])
outs = sess.run(
None,
{
"query": (np.random.randn(b, s, h * d) * 0.1).astype(np.float16),
src/mobius/_passes/_fp8_kv_cache.py:107
past_keyis guarded bylen(inputs) > 4, which is stricter than necessary for index 3 and makes the intent harder to read. Use independent bounds checks (> 3for index 3,> 4for index 4) so the logic is correct even if an upstream GQA variant ever exposes only some of the optional KV-cache inputs.
inputs = node.inputs
past_key = inputs[3] if len(inputs) > 4 else None
past_value = inputs[4] if len(inputs) > 4 else None
src/mobius/_optimizations.py:584
- The PR description/docstring says the option is ignored with a warning when GQA fusion is not active. Here,
gqa_activeonly checks EP/dtype capability, so if fusion didn't actually produce anyGroupQueryAttentionnodes (pattern miss, rule disabled, etc.), the pass becomes a silent no-op with no warning. Consider also checking that at least one GQA node exists before applying the pass, and warn otherwise.
if fp8_kv_cache:
gqa_active = model_role == "decoder" and dtype in caps.gqa_dtypes
if not gqa_active:
warnings.warn(
f"fp8_kv_cache=True was requested but GQA fusion is not active "
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
src/mobius/_passes/_fp8_kv_cache_test.py:312
- The CUDA runtime test allows CPU fallback (
providers=[CUDA, CPU]), so it can still pass even if the FP8 GQA node isn’t actually supported/executed on CUDA. To make this test prove CUDA support, create the session with only the CUDA EP so session creation or execution fails if CUDA can’t run the graph.
path = tmp_path / "gqa.onnx"
ir.save(model, str(path))
sess = ort.InferenceSession(str(path), providers=[_CUDA, "CPUExecutionProvider"])
outs = sess.run(
src/mobius/_passes/_fp8_kv_cache_test.py:31
sys.path.insert(0, "tests")depends on the current working directory being the repo root; running this test from another CWD can make_test_configsfail to import. Prefer computing thetests/path relative to__file__so the import is stable.
import json
import sys
import ml_dtypes
import numpy as np
import onnx_ir as ir
import onnxruntime as ort
import pytest
sys.path.insert(0, "tests")
from _test_configs import _base_config
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/mobius/_passes/_fp8_kv_cache.py:80
- _retype_fp8() updates the ValueInfo dtype but leaves an empty initializer's const_value tensor dtype unchanged. This makes the graph internally type-inconsistent (initializer tensor still FLOAT/FLOAT16 while the Value is declared FP8) and forces callers/tests to manually retag the initializer before saving/running. If empty placeholder initializers are intended to be supported, the pass should also rewrite the empty const_value tensor to FP8 (safe because size==0).
def _retype_fp8(value: ir.Value | None) -> None:
"""Retype *value* to ``FLOAT8E4M3FN`` in place, preserving its shape."""
if value is None:
return
value.type = ir.TensorType(_FP8)
src/mobius/_passes/_fp8_kv_cache.py:124
- The KV-cache input detection uses
inputs[3] if len(inputs) > 4for past_key, which is an off-by-one condition (index 3 is valid when len(inputs) > 3). It currently works because the code also requires past_value, but the condition is misleading and makes the intent harder to follow. Simplify to a singlelen(inputs) <= 4guard, then index 3/4 directly.
inputs = node.inputs
past_key = inputs[3] if len(inputs) > 4 else None
past_value = inputs[4] if len(inputs) > 4 else None
if past_key is None or past_value is None:
# Prompt-processing GQA without a KV cache — nothing to convert.
src/mobius/main.py:239
- The validation error for
--kv-cache-scale-filestill points users to the deprecated--fp8-kv-cacheflag, even though--features fp8-kv-cacheis now the canonical way to enable this mode. Updating the message to reference the feature avoids directing users toward deprecated CLI usage.
fp8_kv_cache = getattr(args, "fp8_kv_cache", False)
kv_cache_scales: dict[int, tuple[float, float]] | None = None
scale_file = getattr(args, "kv_cache_scale_file", None)
if scale_file is not None and not fp8_kv_cache:
raise SystemExit("Error: --kv-cache-scale-file can only be used with --fp8-kv-cache.")
if fp8_kv_cache and scale_file is not None:
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/mobius/_passes/_fp8_kv_cache.py:81
- _retype_fp8() updates the ValueInfo dtype but leaves an empty const_value tensor (used for placeholder initializers / constants) at its original dtype. That can create an inconsistent graph where the node input is declared FP8 but the constant/initializer is still FLOAT16/FLOAT, which can break model loading/type checking even when the tensor has zero elements.
def _retype_fp8(value: ir.Value | None) -> None:
"""Retype *value* to ``FLOAT8E4M3FN`` in place, preserving its shape."""
if value is None:
return
value.type = ir.TensorType(_FP8)
src/mobius/_passes/_fp8_kv_cache.py:137
- Fp8KvCachePass currently relies on const_value-emptiness to decide whether a KV cache is safe to retype, but it does not enforce the documented contract that past_key/past_value must be graph inputs. This allows converting caches produced by other nodes (e.g. Constant outputs), which risks retyping internal tensors that the runtime/kernel may not treat as KV-cache I/O. Add an explicit producer()==None check (graph input / initializer) before retyping.
inputs = node.inputs
past_key = inputs[3] if len(inputs) > 4 else None
past_value = inputs[4] if len(inputs) > 4 else None
if past_key is None or past_value is None:
# Prompt-processing GQA without a KV cache — nothing to convert.
continue
# Contract: only KV caches that are graph inputs (or empty
# placeholders) may be retyped. Retyping a non-empty FLOAT/FLOAT16
# initializer would declare FP8 over FLOAT bytes and corrupt the
# graph, so skip such nodes loudly rather than emit an invalid model.
if not _is_retypable_cache(past_key) or not _is_retypable_cache(past_value):
warnings.warn(
f"Fp8KvCachePass: skipping {node.name!r} — its KV cache is a "
f"non-empty initializer, not a graph input. Only graph-input "
f"KV caches can be converted to FP8.",
stacklevel=2,
)
continue
src/mobius/main.py:809
- The --kv-cache-scale-file help text says it's "Only used with --fp8-kv-cache", but that flag is deprecated and users should enable FP8 KV cache via --features fp8-kv-cache. Updating the help string will keep CLI docs consistent with the new feature mechanism.
help=(
"Optional JSON file of calibrated per-layer FP8 KV-cache scales "
"(onnxruntime-genai format: {'scales': {'k_scales': [...], "
"'v_scales': [...]}}). Only used with --fp8-kv-cache; without it "
"all layers use a unit scale of 1.0."
),
src/mobius/main.py:238
- The error message for using --kv-cache-scale-file without FP8 KV cache references only the deprecated flag (--fp8-kv-cache). Since --features fp8-kv-cache is now the canonical path, the message should mention it to avoid confusing users.
This issue also appears on line 804 of the same file.
if scale_file is not None and not fp8_kv_cache:
raise SystemExit("Error: --kv-cache-scale-file can only be used with --fp8-kv-cache.")
tianleiwu
left a comment
There was a problem hiding this comment.
The FP8 KV-cache implementation is well isolated and the latest follow-up commits address the earlier EP-gating and graph-mutation concerns. One correctness issue remains in the public Python API: direct calibration maps need the same validation as scale files before they are emitted into the graph.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src/mobius/_passes/_fp8_kv_cache.py:167
past_keyis only read whenlen(inputs) > 4, butpast_keylives at index 3; this off-by-one means a (hypothetical) GQA node with exactly 4 inputs would incorrectly be treated as having no KV cache. The guard should belen(inputs) > 3forpast_keyandlen(inputs) > 4forpast_value.
inputs = node.inputs
past_key = inputs[3] if len(inputs) > 4 else None
past_value = inputs[4] if len(inputs) > 4 else None
src/mobius/main.py:192
- The
--max-seq-lenvalidation error message only mentions the deprecated--static-cacheflag, but the new canonical path is--features static-cache. This makes the CLI error harder to act on for users following the updated docs.
_resolve_build_features(args)
# Validate --max-seq-len requires --static-cache
if args.max_seq_len is not None and not args.static_cache:
raise SystemExit("Error: --max-seq-len can only be used with --static-cache.")
src/mobius/main.py:238
- The
--kv-cache-scale-filevalidation error message only references the deprecated--fp8-kv-cacheflag, even though the canonical enablement is--features fp8-kv-cache. Updating the message will reduce confusion for users following the new--featuresdocs.
scale_file = getattr(args, "kv_cache_scale_file", None)
if scale_file is not None and not fp8_kv_cache:
raise SystemExit("Error: --kv-cache-scale-file can only be used with --fp8-kv-cache.")
Add an opt-in path that stores the com.microsoft::GroupQueryAttention past/present KV cache as FLOAT8E4M3FN instead of the model dtype, halving KV-cache memory at long context. Mirrors onnxruntime-genai's fp8_kv_cache option (microsoft/onnxruntime-genai#2351). A new post-fusion IR pass, Fp8KvCachePass, retypes each decoder GQA past_key/past_value input and present_key/present_value output to FP8, adds per-layer k_scale/v_scale FLOAT initializers at GQA input slots 12/13, and sets k_quant_type/v_quant_type="PER_TENSOR" and kv_cache_bit_width=8. The query/key/value inputs stay at the model dtype; the kernel quantizes the new K/V to FP8 on write and dequantizes on read using the scales. Scales default to a unit 1.0 (the "legacy" export shape) or come from an offline calibration file (onnxruntime-genai scales JSON format). Wired through optimize_model, build, build_from_module, and the `mobius build` CLI via --fp8-kv-cache and --kv-cache-scale-file. The option is ignored with a warning when GQA fusion is not active (e.g. non-CUDA EP or fp32), since there is no KV-cache op to convert. Verified on an H200 (SM90) with onnxruntime-gpu 1.28: the emitted FP8 GQA op loads and computes on the CUDA EP (finite fp16 output, FP8 present cache). End-to-end FP8 KV at runtime is IO-bound as device FLOAT8E4M3FN OrtValues (as onnxruntime-genai does); the Python numpy fp8 feed path is unsupported by ORT, which the runtime test works around with empty in-graph FP8 constants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Address review feedback: - Fp8KvCachePass no longer early-skips a node whose past_key is already FP8. Every step is idempotent (retype no-op, scale initializers dedup by name, attributes overwrite, inputs only grow when < 14), so a re-run can never leave a node half-converted. - load_kv_cache_scale_file now rejects non-array k_scales/v_scales and any non-finite or non-positive scale, instead of silently accepting strings/dicts or 0/NaN/inf that would corrupt quantization. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Copilot review feedback on the FP8 KV-cache pass:
- Enforce the documented contract: only retype KV caches that are graph
inputs or empty placeholders. A non-empty FLOAT/FLOAT16 initializer
cache is now skipped with a warning instead of being declared FP8 over
FLOAT bytes (which would corrupt the graph).
- Guard each present output on its own index (present_key at 1,
present_value at 2) so a hypothetical 2-output GQA cannot leave
present_key a different dtype from past_key.
- Derive scale-initializer names from the cache tensor names
(`{past}.fp8_scale`) rather than the numeric layer id, so distinct
caches at the same layer index (e.g. seq2seq self- vs cross-attention)
never share a scale initializer.
- Gate the CUDA runtime test on compute capability SM89+ (best-effort via
torch.cuda.get_device_capability), skipping on older GPUs / unknown
instead of running where the FP8 GQA kernel is absent.
- Add a test covering the non-empty-initializer skip contract.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Collect the boolean build-mode toggles under a single Rust/cargo-style
`--features` option on `mobius build`. It accepts a comma-separated list
and may be repeated:
mobius build ... --features fp8-kv-cache,static-cache
mobius build ... --features fp8-kv-cache --features static-cache
Available features: static-cache, fp8-kv-cache, text-only. Each maps to
the existing boolean build attribute via the `_BUILD_FEATURES` registry;
`_resolve_build_features` folds them in at the top of `_cmd_build` before
any validation reads them. Unknown feature names are rejected with an
error that lists the valid set.
The legacy `--static-cache`, `--fp8-kv-cache`, and `--text-only` flags
remain as deprecated aliases: they still set the same behaviour but print
a deprecation notice on stderr. Companion value args (--max-seq-len,
--kv-cache-scale-file) are unchanged.
Adds CLI tests for feature parsing, comma-separated/multiple features,
unknown-feature error, static-cache equivalence, text-only/fp8 pass-
through, and the deprecation warning. Updates docs/cli_reference.md and
CHANGELOG.md.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
Address PR review: fp8_kv_cache was applied whenever GQA fusion was active
(dtype in caps.gqa_dtypes), which includes non-CUDA EPs (CPU gqa_dtypes={FLOAT},
WebGPU float/float16) that lack the FP8 GroupQueryAttention kernel. Such builds
emitted FLOAT8E4M3FN KV-cache I/O for EPs that cannot load/run it.
- Add EpCapabilities.supports_fp8_kv_cache (default False); set True only for
CUDA (SM89+ Ada/Hopper/Blackwell ship the FP8 GQA kernel).
- Gate Fp8KvCachePass on caps.supports_fp8_kv_cache in addition to GQA activity;
otherwise warn and ignore the request.
- Add regression test: CPU/float32 (GQA active, no FP8 kernel) keeps FLOAT KV I/O.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: justinchuby <justinchuby@users.noreply.github.com>
eb9a9b7 to
c228b47
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/mobius/main.py:238
- The validation error for
--kv-cache-scale-filecurrently tells users to use--fp8-kv-cache, but--features fp8-kv-cacheis now the canonical interface. Updating this message helps keep the CLI guidance consistent with the new--featuresworkflow.
scale_file = getattr(args, "kv_cache_scale_file", None)
if scale_file is not None and not fp8_kv_cache:
raise SystemExit("Error: --kv-cache-scale-file can only be used with --fp8-kv-cache.")
src/mobius/main.py:192
- The error message for
--max-seq-lenstill points users at the deprecated--static-cacheflag. Since--features static-cacheis now the canonical way to enable static cache, the message should reference that (and ideally avoid steering users toward deprecated flags).
This issue also appears on line 236 of the same file.
# Validate --max-seq-len requires --static-cache
if args.max_seq_len is not None and not args.static_cache:
raise SystemExit("Error: --max-seq-len can only be used with --static-cache.")
|
@copilot please remove the deprecated flags |
Co-authored-by: justinchuby <11205048+justinchuby@users.noreply.github.com>
|
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.
Suppressed comments (9)
src/mobius/main.py:182
- The CLI error for
static-cache+--taskstill refers to the removed--static-cacheflag. Since static cache is now enabled via--features static-cache, update the message to point users at the correct option.
raise SystemExit("Error: --max-seq-len can only be used with --static-cache.")
src/mobius/main.py:219
- This validation error still refers to the removed
--text-onlyflag. Update it to reference--features text-onlyso CLI users can act on the message.
load_weights = not args.no_weights
src/mobius/main.py:182
- The error message references the removed
--static-cacheflag. Since static cache is now enabled via--features static-cache, this message should point users to the correct flag.
This issue also appears on line 182 of the same file.
raise SystemExit("Error: --max-seq-len can only be used with --static-cache.")
src/mobius/main.py:228
- The validation error mentions
--fp8-kv-cache, but that boolean flag was removed in favor of--features fp8-kv-cache. The message should reference the new usage.
raise SystemExit("Error: --kv-cache-scale-file can only be used with --fp8-kv-cache.")
src/mobius/main.py:765
- The
--kv-cache-scale-filehelp string refers to--fp8-kv-cache, but the CLI now enables this via--features fp8-kv-cache. This should be updated to avoid pointing users at a non-existent flag.
"Optional JSON file of calibrated per-layer FP8 KV-cache scales "
"(onnxruntime-genai format: {'scales': {'k_scales': [...], "
"'v_scales': [...]}}). Only used with --fp8-kv-cache; without it "
"all layers use a unit scale of 1.0."
docs/cli_reference.md:240
- The
--kv-cache-scale-fileoption is listed twice in the table. This looks like an accidental duplicate row and should be removed to avoid confusing readers.
| `--kv-cache-scale-file PATH` | Optional JSON file of calibrated per-layer FP8 KV-cache scales (onnxruntime-genai format). Only used with the `fp8-kv-cache` feature; without it all layers use a unit scale of 1.0. |
| `--kv-cache-scale-file PATH` | Optional JSON file of calibrated per-layer FP8 KV-cache scales (onnxruntime-genai format). Only used with the `fp8-kv-cache` feature; without it all layers use a unit scale of 1.0. |
src/mobius/main.py:722
- The
--max-seq-lenhelp text says it is only used with--static-cache, but--static-cachewas removed in favor of--features static-cache. Update the help string to avoid pointing users at a non-existent flag.
"--max-seq-len",
CHANGELOG.md:43
- This changelog section still references the removed
--text-onlyflag. Since the PR migrates build toggles to cargo-style--features, the heading and entry text should reference--features text-onlyinstead (and keep thebuild(text_only=True)API note).
### Text-only export for multimodal Gemma 4 (`--text-only`)
#### Added
src/mobius/main.py:218
- The
text-onlyvalidation errors still instruct users to use the removed--text-onlyflag. Since the PR migrates this mode to--features text-only, these messages should reference the new syntax.
This issue also appears on line 219 of the same file.
Summary
Adds an opt-in FP8 (E4M3) KV-cache export path. When enabled, each decoder
com.microsoft::GroupQueryAttentionstores its past/present key-value cache asFLOAT8E4M3FNinstead of the model dtype (fp16/bf16), halving KV-cache memory at long context. Mirrors onnxruntime-genai'sfp8_kv_cacheoption (microsoft/onnxruntime-genai#2351).How it works
A new post-fusion IR pass
Fp8KvCachePass(src/mobius/_passes/_fp8_kv_cache.py), run after the GQA fusion rules, for every decoder GQA node whose KV cache are graph inputs:past_key/past_valueinputs andpresent_key/present_valueoutputs (all graph I/O) toFLOAT8E4M3FN, preserving shapes.k_scale/v_scalescalar FLOAT initializers at GQA input slots 12/13 (ORT ≥ 1.28 signature).k_quant_type/v_quant_type = "PER_TENSOR"andkv_cache_bit_width = 8.query/key/valuestay at the model dtype — the kernel quantizes the new K/V to FP8 on write and dequantizes on read using the scales. Scales default to a unit1.0(the "legacy" export shape), or come from an offline calibration file (onnxruntime-genai{"scales": {"k_scales": [...], "v_scales": [...]}}JSON).Usage
Also exposed programmatically on
build(...)/build_from_module(...)viafp8_kv_cache=andkv_cache_scales=. The option is ignored with a warning when GQA fusion is not active (non-GQA EP or fp32), since there is no KV-cache op to convert.Verification (H200 / SM90, onnxruntime-gpu 1.28)
test_fp8_kv_gqa_runs_on_cuda, gated on CUDA availability).FLOAT8E4M3FNOrtValues (as onnxruntime-genai does). The Python numpy fp8 feed path is unsupported by ORT; the runtime test works around it with empty in-graph FP8 constants.Tests
src/mobius/_passes/_fp8_kv_cache_test.py— 11 tests: KV I/O typed FP8, GQA scale inputs + quant attrs, default unit scales, calibrated scales, disabled path, ignored+warns on non-GQA EP, idempotency, scale-file parsing/validation, and the CUDA runtime run. Existing pass/CLI/build-graph suites pass; changed files pass ruff.