This document proposes a next-generation quantization configuration protocol for gptqmodel.
The protocol is designed to be:
- clean and concise for humans
- pipeline / stage based
- explicit about matching and override behavior
- flexible about quantization method vs exported representation
- future-proof for weight, activation, output, and KV-cache quantization
The user-facing protocol root is intentionally shallow. It consists of:
versionstages
It may be authored through:
- a Python DSL
- YAML / JSON serialization of the same protocol
The Python and YAML forms below describe the same protocol. Python is the ergonomic builder API. YAML is the portable serialized form.
-
One matching system only. Rules match model objects. Stages do not rematch. Actions do not rematch the whole model in normal use.
-
Keep the common case short. The common case should need only:
matchweight/input/output/kv_cachepreparequantizeexport
-
Make overrides readable. A narrower rule should be able to skip quantization, replace defaults, or stop later rules without confusing
+/-syntax. -
Make partial overrides cheap. A narrower rule should be able to override only
bits,group_size, or another single leaf field without restating the full quantizer configuration. -
Separate quantization from representation.
quantizeanswers how quantized values are produced.exportanswers how those values are encoded into final tensors and metadata. -
Keep backend-specific terms internal. Terms such as
*input_quantizer,*weight_quantizer, or packer-specific tensor names should not be the primary user-facing API.
Python:
version = 2
stages = [
Stage(
name="ptq",
rules=[
Rule(
match="*",
aliases=None,
actions=[],
stop=False,
weight=None,
input=None,
output=None,
kv_cache=None,
),
],
),
]YAML:
version: 2
stages:
- name: ptq
rules:
- match: "*"
aliases: null
actions: []
stop: false
weight: null
input: null
output: null
kv_cache: nullA stage is an ordered execution boundary. A rule is the only normal matcher. Each rule may configure one or more tensor targets.
Rule.match may be either:
- a single selector string
- a list of selector strings
Selector prefixes:
- no prefix or
+:means positive/include -:means negative/exclude
This lets one rule express "match everything except ..." without adding a second skip rule.
Python:
Rule(
match=["*", "-:.*layer2.*"],
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq"},
},
)YAML:
- match:
- "*"
- "-:.*layer2.*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptqRecommended semantics:
- a rule matches if at least one positive selector matches
- any matching negative selector removes that module from the rule
*is a special match-all shorthand- every other selector string is interpreted as regex by default
- for exact module-name matches, use an anchored escaped regex such as
^model\.layers\.0\.self_attn\.q_proj$
An implementation may compile the user-facing protocol into an internal typed object such as:
Plan(version=2, stages=[...])That internal root object is for parser/runtime organization. It should not be required in user-facing examples or config files. Normal user configs have one protocol root for one quantization run or artifact, not multiple user-facing plans.
The protocol should support both of these as first-class authoring surfaces:
- Python DSL for programmatic authoring, helpers, and composition
- YAML for checked-in configs, serialization, export metadata, and non-Python tooling
Example equivalence:
Python:
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq"},
},
)YAML:
match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptqA Stage exists to define:
- execution order
- calibration / replay boundary
- save / emit boundary
A stage does not introduce a second targeting system.
Example:
Python:
stages = [
Stage(
name="balance",
rules=[
Rule(
match=".*self_attn$",
actions=[smoothquant(alpha=0.5)],
),
],
),
Stage(
name="ptq",
rules=[
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq"},
},
),
],
),
]YAML:
stages:
- name: balance
rules:
- match: ".*self_attn$"
actions:
- method: smoothquant
alpha: 0.5
- name: ptq
rules:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptqA Rule contains:
match: the only normal matcher- optional
aliases: named references relative to the matched object - optional
actions: rule-scoped operations - optional tensor-target sections:
weightinputoutputkv_cache
- optional
stop: stop applying later rules to the same matched object
Fields omitted by a narrower rule inherit from earlier matching rules unless an explicit replace mode is used.
Recommended shape:
Python:
Rule(
match="*",
aliases=None,
actions=[],
stop=False,
weight={...},
input={...},
output={...},
kv_cache={...},
)YAML:
match: "*"
aliases: null
actions: []
stop: false
weight: {}
input: {}
output: {}
kv_cache: {}Recommended match forms:
- exact module path:
"model.layers.0.self_attn.q_proj" - wildcard / glob:
"*"or"*.q_proj" - regex:
".*self_attn$"
Rules are evaluated top-to-bottom inside a stage.
There is no stage-level matcher and no normal action-level global matcher.
aliases is optional.
It exists only to name reusable relative references under the matched object. It is not a second model-wide matching language.
Example:
Python:
Rule(
match=".*self_attn$",
aliases={"proj": ["q_proj", "k_proj", "v_proj", "o_proj"]},
actions=[
record_stats(targets="@proj"),
inspect_outliers(targets="@proj"),
],
)YAML:
match: ".*self_attn$"
aliases:
proj:
- q_proj
- k_proj
- v_proj
- o_proj
actions:
- method: record_stats
targets: "@proj"
- method: inspect_outliers
targets: "@proj"Use aliases only when the same relative subset must be reused.
If the action naturally operates on the matched object, omit aliases.
actions is a rule-scoped list of operations that run in the context of the rule match.
Examples:
smoothquant(alpha=0.5)awq_balance(ratio=0.7)calibrate_router(...)
Default behavior:
- an action operates on the current rule match
- an action does not rematch the whole model
- if an action needs a reusable relative subset, it may use rule
aliases - actions should prefer the canonical structure of the matched object over user-written sub-matching in the common case
Important:
actionsare for rule-scoped or cross-target behaviorprepareis for target-local pre-quant behavior
This keeps placement clear:
- SmoothQuant or AWQ-like balancing:
actions - local weight clip / pad / smoother:
weight.prepare
The protocol supports these first-class tensor targets:
weightinputoutputkv_cache
This makes the protocol future-proof for:
- weight-only quantization
- activation quantization
- output quantization
- cache quantization
Each tensor target may define:
preparequantizeexport
Recommended shape:
Python:
weight={
"prepare": [...], # optional
"quantize": ..., # optional
"export": ..., # optional
}YAML:
weight:
prepa []
quantize: null
export: nullprepare is for target-local pre-quant transformations.
Examples that belong in weight.prepare:
clip.mad(k=2.75)clip.percentile(percentile=99.5)pad.columns(multiple=4, semantic=True)
Examples that belong in input.prepare:
clamp.range(min=-6, max=6)normalize.rms(eps=1e-5)
Placement rule:
- local target-only modification ->
prepare - cross-target or rule-context modification ->
actions
quantize defines how the target is quantized.
Examples:
gptq(bits=4, sym=True, group_size=128)rtn(bits=4, sym=True)mxfp4(mode="dynamic", block_size=32, scale_bits=8)int8(calibration=observer("max"))skip()
skip() is target-scoped.
It does not remove prepare and it does not disable other targets.
Important:
quantizeis a structured object, not a replace-only scalar- later rules may patch only specific quantizer fields
- omitted fields inherit from earlier matching rules
Example base quantizer:
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128Example narrower override:
weight:
quantize:
bits: 8The override changes only bits.
It does not require restating method, sym, or group_size.
fallback belongs inside quantize.
It is not:
- an
action - a
preparestep - an
exportsetting
It is a quantizer-local fallback policy for methods that depend on calibration or activation evidence and may not have enough usable samples for every matched unit.
This is the right place because fallback changes how the quantizer solves a target when evidence is insufficient. It does not change which modules are matched, and it does not change the exported format family.
Primary use cases:
- GPTQ with too few Hessian / activation samples for a module
- AWQ with missing or too-sparse captured activations for a layer group
- future activation-aware weight quantizers
- MoE routing cases where some experts receive little or no calibration traffic
Recommended protocol shape:
Python:
Rule(
match="*",
weight={
"quantize": {
"method": "gptq",
"bits": 4,
"group_size": 128,
"sym": True,
"fallback": {
"strategy": "rtn",
"threshold": "0.5%",
},
},
"export": {
"format": "gptq",
},
},
)YAML:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
group_size: 128
sym: true
fallback:
strategy: rtn
threshold: 0.5%
export:
format: gptqRecommended fields:
strategy: fallback quantization strategythreshold: minimum evidence threshold before fallback triggerssmooth: optional smoothing used only inside the fallback path
Example:
weight:
quantize:
method: awq
bits: 4
group_size: 128
fallback:
strategy: rtn
threshold: 1.0%
smooth:
type: mad
k: 2.75Recommended threshold semantics:
- integer / float: absolute minimum observed samples or tokens
- percent string such as
"0.5%": minimum observed coverage relative to expected calibration traffic true: enable quantizer default thresholdfalseornull: disable fallback
Initial runtime contract:
- GPTQ: evaluate fallback per matched module
- AWQ: evaluate fallback at the quantizer's natural scaling group or layer subgroup
- future methods: evaluate fallback at the quantizer's natural solve unit
The protocol should not force one global fallback scope. Fallback should use the quantizer's native solve scope.
Important separation:
quantize.method = gptqwithfallback.strategy = rtnmeans: GPTQ is still the primary method- if the module or group is under-sampled, fallback quantization uses RTN-like weight-only solving
- the rule's
exportstill controls the final encoded representation
Example:
weight:
quantize:
method: gptq
bits: 4
fallback:
strategy: rtn
threshold: 0.5%
export:
format: gptqThis does not mean "export as RTN". It means:
- primary solve path: GPTQ
- low-evidence fallback solve path: RTN
- final export family: GPTQ
That matches current gptqmodel behavior more closely than treating fallback as a separate stage or a weight-only top-level config.
Patch-first override behavior should apply here too.
Example base rule:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
group_size: 128
fallback:
strategy: rtn
threshold: 0.5%Example narrower MoE override:
- match: ".*experts\\.[0-9]+\\..*"
weight:
quantize:
fallback:
threshold: 2.0%Effective result for expert modules:
method = gptqbits = 4group_size = 128fallback.strategy = rtnfallback.threshold = 2.0%
This is how fallback should fit into the new protocol:
- nested under
target.quantize - inherited and patchable like other quantizer fields
- supported only for quantizers that actually depend on calibration / activations
- independent from
export
export defines the final encoded representation for the target.
The canonical form of export should be a structured object.
String export should be treated only as shorthand for very simple cases.
Canonical fields:
format: logical exported family such asgptq,awq,fp8,fp4,gguf,nativevariant: family-specific subtype such asgemm,gemv,e4m3fn,e5m2,nvfp4,mxfp4,q4_k_mimpl: concrete exporter or runtime implementation such asdefault,llm_awq,marlin,transformer_engine,modeloptversion: exporter layout / schema versionoptions: exporter-specific knobs that should not be promoted to top-level DSL fields
Python:
weight={
"export": {
"format": "awq",
"variant": "gemm",
"impl": "llm_awq",
"version": 2,
},
}YAML:
weight:
export:
format: awq
variant: gemm
impl: llm_awq
version: 2Examples:
{"format": "gptq"}{"format": "awq", "variant": "gemm"}{"format": "awq", "variant": "gemv"}{"format": "fp8", "variant": "e4m3fn", "impl": "transformer_engine"}{"format": "fp4", "variant": "nvfp4", "impl": "modelopt"}{"format": "gguf", "variant": "q4_k_m"}
Shorthand:
"gptq"=={"format": "gptq"}"native"=={"format": "native"}
If omitted, the engine may use the quantizer's native export.
Like quantize, export should be patchable by narrower rules.
Example:
- match: "*"
weight:
export:
format: awq
variant: gemm
impl: llm_awq
version: 2
- match: ".*small_proj$"
weight:
export:
variant: gemvEffective result for small_proj:
format = awqvariant = gemvimpl = llm_awqversion = 2
Rules should be treated as patches over an accumulated effective configuration.
This is the main simplification for dynamic overrides.
Common case:
- a broad rule defines defaults
- a narrower rule patches only the fields it wants to change
- unchanged fields inherit automatically
Python:
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq"},
},
)
Rule(
match=".*down_proj$",
weight={
"quantize": {"bits": 3},
},
)YAML:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
- match: ".*down_proj$"
weight:
quantize:
bits: 3Effective result for down_proj:
method = gptqbits = 3sym = truegroup_size = 128export.format = gptq
This is the intended replacement for the current gptqmodel dynamic override style where a base rule applies to all modules and narrower matches override only selected fields.
Patch merging should be the default. Explicit replacement should be available only as an escape hatch.
Recommended advanced control:
mode: replace
Python:
Rule(
match="layer0.qkv",
weight={
"mode": "replace",
"prepare": [pad.columns(multiple=4, semantic=True)],
"quantize": skip(),
},
)YAML:
match: "layer0.qkv"
weight:
mode: replace
prepa
- method: pad.columns
multiple: 4
semantic: true
quantize:
method: skipmode: replace is advanced.
Users should not need it for normal per-layer overrides like changing only bits.
These are different questions:
- How is the quantized state computed?
- How is that state emitted as final tensors and metadata?
Example:
Python:
weight={
"quantize": rtn(bits=4, sym=True),
"export": {"format": "gptq", "impl": "default"},
}YAML:
weight:
quantize:
method: rtn
bits: 4
sym: true
export:
format: gptq
impl: defaultThis means:
- use RTN to produce the quantized weight state
- encode that state in GPTQ-style exported tensors
For GPTQ itself:
Python:
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
}YAML:
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: defaultHere, GPTQ packing is part of export realization. It does not need to be a separate first-class user concept.
Conceptually:
W
-> quantize(method=GPTQ)
-> logical quantized state
-> export("gptq")
-> qweight + scales + qzeros + g_idx + metadata
So export is the correct user-facing property, while internal packing details remain backend implementation details.
This is also why the canonical export form should be an object rather than a string-only enum.
The protocol should expose activation quantization through tensor targets, not backend-internal names.
Example:
Python:
Rule(
match="*",
input={
"quantize": mxfp4(mode="dynamic", block_size=32, scale_bits=8),
"export": {
"format": "fp4",
"variant": "mxfp4",
"impl": "modelopt",
},
},
)YAML:
match: "*"
input:
quantize:
method: mxfp4
mode: dynamic
block_size: 32
scale_bits: 8
export:
format: fp4
variant: mxfp4
impl: modeloptThis corresponds conceptually to installing an input activation quantizer on matched modules.
In NVIDIA Model Optimizer terms, this is cleaner than exposing *input_quantizer directly.
Important:
inputmeans the activation entering the matched moduleoutputmeans the activation leaving the matched module- these are tensor-target concepts, not inserted-submodule names
If weight.quantize = gptq(...) and input.quantize = ... coexist, the weight quantizer may need to know whether it should optimize using full-precision or quantized inputs.
Recommended future-proof parameter:
Python:
gptq(
bits=4,
sym=True,
group_size=128,
activation_mode="ignore", # or "fake", later possibly "real"
)YAML:
method: gptq
bits: 4
sym: true
group_size: 128
activation_mode: ignoreMeaning:
"ignore": classic weight-only GPTQ"fake": optimize with fake-quantized inputs active"real": reserved for future real low-bit activation flow
Rules compose top-to-bottom within a stage.
Recommended semantics:
- broader rules define defaults
- narrower rules refine or override
- target sections merge recursively by default
- target-local lists such as
prepareappend by default - quantizer leaf fields such as
bits,sym, andgroup_sizeare last-match-wins - exporter leaf fields such as
format,variant,impl, andversionare last-match-wins skip()is target-scopedstop=Trueprevents later rules from changing the same matched object- if
quantize.methodchanges, previous quantizer-specific fields should be discarded unless explicitly repeated - if
export.formatchanges, previous format-specific export fields should be discarded unless explicitly repeated
Example: global default plus narrow skip
Python:
Rule(
match="*",
weight={
"prepare": [pad.columns(multiple=4, semantic=True)],
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
},
input={
"quantize": mxfp4(mode="dynamic", block_size=32, scale_bits=8),
"export": {
"format": "fp4",
"variant": "mxfp4",
"impl": "modelopt",
},
},
)
Rule(
match="layer0.qkv",
weight={
"quantize": skip(),
},
)YAML:
- match: "*"
weight:
prepa
- method: pad.columns
multiple: 4
semantic: true
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: default
input:
quantize:
method: mxfp4
mode: dynamic
block_size: 32
scale_bits: 8
export:
format: fp4
variant: mxfp4
impl: modelopt
- match: "layer0.qkv"
weight:
quantize:
method: skipEffective result for layer0.qkv:
- keep
weight.prepare = [pad.columns(...)] - skip
weight.quantize - keep default
input.quantize = mxfp4(...)
Example: base config plus bits-only override
Python:
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
},
)
Rule(
match=".*(q_proj|k_proj)$",
weight={
"quantize": {"bits": 8},
},
)YAML:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: default
- match: ".*(q_proj|k_proj)$"
weight:
quantize:
bits: 8Effective result for q_proj and k_proj:
method = gptqbits = 8sym = truegroup_size = 128
Example: explicit replacement plus stop
Python:
Rule(
match="layer0.qkv",
stop=True,
weight={
"mode": "replace",
"prepare": [pad.columns(multiple=4, semantic=True)],
"quantize": skip(),
},
)YAML:
match: "layer0.qkv"
stop: true
weight:
mode: replace
prepa
- method: pad.columns
multiple: 4
semantic: true
quantize:
method: skipThis means:
- replace inherited weight config with only the fields given here
- do not let later rules change
layer0.qkv
Within a stage, recommended engine order is:
- evaluate rules in order
- resolve matches
- resolve optional aliases
- run rule
actions - run target
prepare - collect calibration / replay data required by quantizers
- run target
quantize - run target
export - emit stage outputs
Stage order then defines the full pipeline order.
This is the intended pattern:
Python:
Rule(
match=".*self_attn$",
actions=[smoothquant(alpha=0.5)],
)
Rule(
match="*",
weight={
"prepare": [clip.mad(k=2.75)],
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
},
input={
"quantize": mxfp4(mode="dynamic", block_size=32, scale_bits=8),
"export": {
"format": "fp4",
"variant": "mxfp4",
"impl": "modelopt",
},
},
)YAML:
- match: ".*self_attn$"
actions:
- method: smoothquant
alpha: 0.5
- match: "*"
weight:
prepa
- method: clip.mad
k: 2.75
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: default
input:
quantize:
method: mxfp4
mode: dynamic
block_size: 32
scale_bits: 8
export:
format: fp4
variant: mxfp4
impl: modeloptMeaning:
- each matched attention block first receives the
smoothquant(...)action in its own rule context - later, the global rule supplies default weight and input quantization policy
- submodules that the action touches inside those attention blocks still receive the global defaults from the later rule
The user does not need to add action-level rematching such as on=[...] in the normal case.
The rule already defines the scope.
Python:
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
},
)YAML:
match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: defaultPython:
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
},
input={
"quantize": mxfp4(mode="dynamic", block_size=32, scale_bits=8),
"export": {
"format": "fp4",
"variant": "mxfp4",
"impl": "modelopt",
},
},
)YAML:
match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: default
input:
quantize:
method: mxfp4
mode: dynamic
block_size: 32
scale_bits: 8
export:
format: fp4
variant: mxfp4
impl: modeloptPython:
Rule(
match=".*self_attn$",
actions=[smoothquant(alpha=0.5)],
)YAML:
match: ".*self_attn$"
actions:
- method: smoothquant
alpha: 0.5Python:
Rule(
match="layer0.qkv",
weight={
"quantize": skip(),
},
)YAML:
match: "layer0.qkv"
weight:
quantize:
method: skipPython:
Rule(
match=".*down_proj$",
weight={
"quantize": rtn(bits=4, sym=True),
"export": {"format": "gptq", "impl": "default"},
},
)YAML:
match: ".*down_proj$"
weight:
quantize:
method: rtn
bits: 4
sym: true
export:
format: gptq
impl: defaultPython:
Rule(
match="*",
weight={
"quantize": gptq(bits=4, sym=True, group_size=128),
"export": {"format": "gptq", "impl": "default"},
},
)
Rule(
match="model.layers.0.self_attn.q_proj",
weight={
"quantize": {"bits": 2},
},
)YAML:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
export:
format: gptq
impl: default
- match: "model.layers.0.self_attn.q_proj"
weight:
quantize:
bits: 2Python:
Rule(
match="*",
weight={
"quantize": awq(bits=4, sym=True, group_size=128),
"export": {
"format": "awq",
"variant": "gemm",
"impl": "llm_awq",
"version": 2,
},
},
)
Rule(
match=".*small_proj$",
weight={
"export": {"variant": "gemv"},
},
)YAML:
- match: "*"
weight:
quantize:
method: awq
bits: 4
sym: true
group_size: 128
export:
format: awq
variant: gemm
impl: llm_awq
version: 2
- match: ".*small_proj$"
weight:
export:
variant: gemvThe examples below are translations of real repo tests into the proposed protocol. They preserve the tested quantization intent, but they do not try to mirror every harness detail such as evaluation tasks, prompt text, or temporary save paths.
Source tests:
tests/test_dynamic.py
Current tested behavior:
- base quantization is 4-bit GPTQ
- base group size is 128
up_projandgate_projare overridden to 8-bitdown_projkeeps 4-bit but overrides group size to 32
Python:
Stage(
name="ptq",
rules=[
Rule(
match="*",
weight={
"quantize": {
"method": "gptq",
"bits": 4,
"group_size": 128,
},
"export": {
"format": "gptq",
"impl": "default",
},
},
),
Rule(
match=".*\\.up_proj.*",
weight={
"quantize": {"bits": 8},
},
),
Rule(
match=".*\\.gate_proj.*",
weight={
"quantize": {"bits": 8},
},
),
Rule(
match=".*\\.down_proj.*",
weight={
"quantize": {"bits": 4, "group_size": 32},
},
),
],
)YAML:
stages:
- name: ptq
rules:
- match: "*"
weight:
quantize:
method: gptq
bits: 4
group_size: 128
export:
format: gptq
impl: default
- match: ".*\\.up_proj.*"
weight:
quantize:
bits: 8
- match: ".*\\.gate_proj.*"
weight:
quantize:
bits: 8
- match: ".*\\.down_proj.*"
weight:
quantize:
bits: 4
group_size: 32This is the clearest example of why the protocol uses patch-first override semantics. The narrower rules change only the leaf fields they care about.
Source tests:
tests/test_awq.pytests/models/awq/test_llama3_2.pytests/models/model_test.py
Current tested behavior:
- method is AWQ
- bits are 4
- group size is 128
- one tested export target is AWQ GEMM
- one tested runtime backend is
BACKEND.TORCH_AWQ
Python:
Stage(
name="ptq",
rules=[
Rule(
match="*",
weight={
"quantize": {
"method": "awq",
"bits": 4,
"group_size": 128,
"sym": True,
},
"export": {
"format": "awq",
"variant": "gemm",
},
},
),
],
)YAML:
stages:
- name: ptq
rules:
- match: "*"
weight:
quantize:
method: awq
bits: 4
group_size: 128
sym: true
export:
format: awq
variant: gemmIn tests/test_awq.py, the same pattern is also exercised with other export variants such as:
gemvgemv_fastllm_awq
That is exactly why export needs to be an object and not just a single string token.
Source tests:
tests/test_weight_only_config.py
Note:
- the repo currently exercises RTN primarily through weight-only/config and format-conversion tests rather than a
tests/models/*full-model test case
Current tested behavior:
- method is RTN
- bits are 4
- group size is 128
- smoothing uses
SmoothMAD(k=1.5) - export target is AWQ GEMM
Python:
Stage(
name="weight_only",
rules=[
Rule(
match="*",
weight={
"prepare": [
{"method": "smooth.mad", "k": 1.5},
],
"quantize": {
"method": "rtn",
"bits": 4,
"group_size": 128,
},
"export": {
"format": "awq",
"variant": "gemm",
},
},
),
],
)YAML:
stages:
- name: weight_only
rules:
- match: "*"
weight:
prepa
- method: smooth.mad
k: 1.5
quantize:
method: rtn
bits: 4
group_size: 128
export:
format: awq
variant: gemmRelated repo test:
tests/test_format_conversion_flow.pyalso verifies that RTN can export to GPTQ format withRTNConfig(bits=4, format=FORMAT.GPTQ, offload_to_disk=False)
That is a concrete existing example of the protocol's quantize != export split.
Current gptqmodel configuration is primarily weight-centric.
A straightforward migration path is:
- base
QuantizeConfig-> broad default rule dynamicpositive override -> narrower rule- partial
dynamicoverride fields -> quantizer patch fields such asweight.quantize.bits -:negative skip -> target-scopedskip()- smoothers / preprocessors ->
weight.prepare - method vs output representation split ->
quantizevsexport
This keeps current intent while making the protocol ready for activation and cache quantization.
The protocol should not make these primary user concepts:
- inserted quantizer submodule names
- internal packer tensor names
- stage-level rematching
- action-level global rematching in normal usage
The recommended protocol shape is:
Python:
version = 2
stages = [
Stage(
name="ptq",
rules=[
Rule(
match=".*self_attn$",
actions=[smoothquant(alpha=0.5)],
),
Rule(
match="*",
weight={
"prepare": [clip.mad(k=2.75)],
"quantize": gptq(
bits=4,
sym=True,
group_size=128,
activation_mode="fake",
),
"export": {"format": "gptq", "impl": "default"},
},
input={
"quantize": mxfp4(
mode="dynamic",
block_size=32,
scale_bits=8,
),
"export": {
"format": "fp4",
"variant": "mxfp4",
"impl": "modelopt",
},
},
),
Rule(
match="layer0.qkv",
weight={
"quantize": skip(),
},
),
],
),
]YAML:
version: 2
stages:
- name: ptq
rules:
- match: ".*self_attn$"
actions:
- method: smoothquant
alpha: 0.5
- match: "*"
weight:
prepa
- method: clip.mad
k: 2.75
quantize:
method: gptq
bits: 4
sym: true
group_size: 128
activation_mode: fake
export:
format: gptq
impl: default
input:
quantize:
method: mxfp4
mode: dynamic
block_size: 32
scale_bits: 8
export:
format: fp4
variant: mxfp4
impl: modelopt
- match: "layer0.qkv"
weight:
quantize:
method: skipThis keeps the model concise:
- one matcher
- optional aliases
- rule-scoped actions
- first-class tensor targets
- local
prepare - explicit
quantize - explicit
export - readable override and stop semantics