Skip to content

Commit 6cc1c8f

Browse files
committed
Group MLA q_a_proj/kv_a_proj_with_mqa projections in auto_quantize
TRT-LLM fuses the MLA low-rank input projections (DeepSeek/GLM lineage) into a single fused_qkv_a_proj_with_mqa GEMM, so auto_quantize must assign both shards one shared quantization format; without this rule any scoring method can emit checkpoints the runtime cannot fuse. Signed-off-by: Joshua Hill <joshua.hill@baseten.co>
1 parent b0580a3 commit 6cc1c8f

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

modelopt/torch/quantization/algorithms.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ def attrs(self) -> list[str]:
595595

596596
_LINEAR_ATTN_QKVZ_RE = re.compile(r"^(.*?\.linear_attn)\.(?:in_proj_qkv|in_proj_z)$")
597597
_LINEAR_ATTN_BA_RE = re.compile(r"^(.*?\.linear_attn)\.(?:in_proj_a|in_proj_b)$")
598+
_MLA_A_PROJ_RE = re.compile(r"^(.*?)\.(?:q_a_proj|kv_a_proj_with_mqa)$")
598599

599600

600601
def _linear_attn_qkvz_group_key(_model, name: str) -> str | None:
@@ -607,6 +608,11 @@ def _linear_attn_ba_group_key(_model, name: str) -> str | None:
607608
return f"{m.group(1)}/ba" if m else None
608609

609610

611+
def _mla_a_proj_group_key(_model, name: str) -> str | None:
612+
m = _MLA_A_PROJ_RE.match(name)
613+
return f"{m.group(1)}/qkv_a" if m else None
614+
615+
610616
def _module_search_space_signature(module_search_spaces) -> tuple:
611617
"""Return a checkpoint-stable description of module-specific candidate spaces."""
612618
return tuple(
@@ -649,6 +655,14 @@ class _AutoQuantizeBaseSearcher(BaseSearcher, ABC):
649655
r"^(.*?)\.(gate_proj|up_proj)$", # gate_proj, up_proj for llama like models
650656
r"^(.*?)\.(\d+\.(w1|w2|w3))$", # mixtral experts
651657
r"^(.*?)\.((w1_linear|w2_linear|w3_linear)\.\d+)$", # dbrx experts
658+
# MLA low-rank input projections (DeepSeek/GLM lineage): TRT-LLM fuses them into
659+
# fused_qkv_a_proj_with_mqa, so the shards must share one quantization format.
660+
# A callable (not a regex) because a regex rule keys on match.group(1) -- the
661+
# attention path -- which is the key the q_proj/k_proj/v_proj rule above already
662+
# returns. On MLA built without a q LoRA rank (q_lora_rank=None, e.g.
663+
# DeepSeek-V2-Lite) ``q_proj`` and ``kv_a_proj_with_mqa`` are siblings, so a
664+
# shared key would merge the unfused q_proj into this group.
665+
_mla_a_proj_group_key,
652666
# Qwen3.5/3.6 hybrid linear_attn: vLLM fuses (in_proj_qkv, in_proj_z)
653667
# into ``in_proj_qkvz`` and (in_proj_a, in_proj_b) into ``in_proj_ba`` and
654668
# requires fused shards to share quant_algo. Two callables (not one

tests/unit/torch/quantization/test_autoquant.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1537,3 +1537,84 @@ def test_get_auto_quantize_config_emits_fused_expert_quantizer_names(with_persis
15371537
assert f"{module_name}.down_proj_weight_quantizer" in quantizer_names
15381538
assert f"{module_name}.weight_quantizer" not in quantizer_names
15391539

1540+
1541+
def test_mla_projections_share_one_group():
1542+
"""TRT-LLM fuses q_a_proj + kv_a_proj_with_mqa, so they must share one quant format."""
1543+
1544+
class _MLAAttention(torch.nn.Module):
1545+
def __init__(self):
1546+
super().__init__()
1547+
self.q_a_proj = torch.nn.Linear(32, 32)
1548+
self.kv_a_proj_with_mqa = torch.nn.Linear(32, 32)
1549+
self.o_proj = torch.nn.Linear(32, 32)
1550+
1551+
def forward(self, x):
1552+
return self.o_proj(self.q_a_proj(x) + self.kv_a_proj_with_mqa(x))
1553+
1554+
class _MLABlock(torch.nn.Module):
1555+
def __init__(self):
1556+
super().__init__()
1557+
self.self_attn = _MLAAttention()
1558+
1559+
def forward(self, x):
1560+
return self.self_attn(x)
1561+
1562+
def get_input(self):
1563+
return torch.randn(1, 4, 32)
1564+
1565+
model = _MLABlock()
1566+
mtq.auto_quantize(
1567+
model,
1568+
constraints={"effective_bits": 8.0},
1569+
quantization_formats=[mtq.INT8_DEFAULT_CFG],
1570+
data_loader=[model.get_input() for _ in range(2)],
1571+
forward_step=lambda model, batch: model(batch),
1572+
loss_func=lambda output, data: output.sum(),
1573+
num_calib_steps=2,
1574+
num_score_steps=2,
1575+
method="gradient",
1576+
)
1577+
hparam = model.self_attn.q_a_proj.get_hparam("quant_recipe")
1578+
assert model.self_attn.kv_a_proj_with_mqa.get_hparam("quant_recipe") == hparam
1579+
assert model.self_attn.o_proj.get_hparam("quant_recipe") != hparam
1580+
1581+
1582+
def test_mla_group_does_not_absorb_unfused_q_proj():
1583+
"""With q_lora_rank=None there is no q_a_proj; q_proj is NOT fused with kv_a_proj."""
1584+
1585+
class _MLAAttention(torch.nn.Module):
1586+
def __init__(self):
1587+
super().__init__()
1588+
self.q_proj = torch.nn.Linear(32, 32)
1589+
self.kv_a_proj_with_mqa = torch.nn.Linear(32, 32)
1590+
self.o_proj = torch.nn.Linear(32, 32)
1591+
1592+
def forward(self, x):
1593+
return self.o_proj(self.q_proj(x) + self.kv_a_proj_with_mqa(x))
1594+
1595+
class _MLABlock(torch.nn.Module):
1596+
def __init__(self):
1597+
super().__init__()
1598+
self.self_attn = _MLAAttention()
1599+
1600+
def forward(self, x):
1601+
return self.self_attn(x)
1602+
1603+
def get_input(self):
1604+
return torch.randn(1, 4, 32)
1605+
1606+
model = _MLABlock()
1607+
mtq.auto_quantize(
1608+
model,
1609+
constraints={"effective_bits": 8.0},
1610+
quantization_formats=[mtq.INT8_DEFAULT_CFG],
1611+
data_loader=[model.get_input() for _ in range(2)],
1612+
forward_step=lambda model, batch: model(batch),
1613+
loss_func=lambda output, data: output.sum(),
1614+
num_calib_steps=2,
1615+
num_score_steps=2,
1616+
method="gradient",
1617+
)
1618+
q_hparam = model.self_attn.q_proj.get_hparam("quant_recipe")
1619+
assert model.self_attn.kv_a_proj_with_mqa.get_hparam("quant_recipe") != q_hparam
1620+
assert model.self_attn.o_proj.get_hparam("quant_recipe") != q_hparam

0 commit comments

Comments
 (0)