Skip to content

Commit 0b367d7

Browse files
lucasliegovind-ramnarayan
authored andcommitted
[None][feat] Add AD custom model for Qwen2 family (#219)
* [None][feat] Add AD custom model for Qwen2 family Add a lean prefill-only custom model implementation for the Qwen2 architecture family, covering Qwen2.5, QwQ, DeepSeek-R1-Distill-Qwen, and related models. The Qwen2 architecture is similar to Llama3 (GQA + SwiGLU MLP + RMSNorm + RoPE) with the key difference that Q/K/V projections have bias=True while O and MLP projections have bias=False. Uses AD canonical ops: - torch_rmsnorm for normalization - torch_rope_with_explicit_cos_sin for rotary embeddings - torch_attention for grouped-query attention (no repeat_kv needed) Files: - tensorrt_llm/_torch/auto_deploy/models/custom/modeling_qwen2.py - tests/unittest/auto_deploy/singlegpu/models/test_qwen2_modeling.py Registered for Qwen2Config covering all qwen2-based models in the registry: Qwen2.5-{0.5B,1.5B,3B,7B,14B,72B}, Qwen2.5-Coder, Qwen2.5-Math, QwQ-32B, DeepSeek-R1-Distill-Qwen-{1.5B,7B,14B,32B}, Skywork-SWE-32B, OpenReasoning-Nemotron-32B, r1-1776-distill-qwen-32b. Signed-off-by: Lucas Liebenwein <lliebenwein@nvidia.com> Signed-off-by: Lucas Liebenwein <11156568+lucaslie@users.noreply.github.com> * [None][fix] Fix causal mask in Qwen2 standalone attention/decoder-layer tests HF eager_attention_forward skips masking when attention_mask=None, making the HF reference run non-causal (full) attention. The custom AD attention uses is_causal=True. This mismatch caused RMSE ratio ~0.93 in the standalone attention and decoder-layer equivalence tests. Fix: build an additive causal mask and pass it to the HF attention call in test_qwen2_attention_equivalence and test_qwen2_decoder_layer_equivalence. The full-model tests are unaffected since Qwen2Model.forward builds the causal mask internally for both HF and custom paths. Signed-off-by: Govind Ramnarayan <105831528+govind-ramnarayan@users.noreply.github.com> * [None][fix] Use _get_hf_rotary_class() helper instead of redundant module-level import Remove the module-level try/except import of HFQwen2RotaryEmbedding and use the HFRotary variable from _get_hf_rotary_class() already called in the test. Signed-off-by: Govind Ramnarayan <105831528+govind-ramnarayan@users.noreply.github.com> --------- Signed-off-by: Lucas Liebenwein <lliebenwein@nvidia.com> Signed-off-by: Lucas Liebenwein <11156568+lucaslie@users.noreply.github.com> Signed-off-by: Govind Ramnarayan <105831528+govind-ramnarayan@users.noreply.github.com> Co-authored-by: Govind Ramnarayan <105831528+govind-ramnarayan@users.noreply.github.com>
1 parent b3a1a70 commit 0b367d7

3 files changed

Lines changed: 858 additions & 0 deletions

File tree

tensorrt_llm/_torch/auto_deploy/models/custom/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .modeling_nemotron_flash import NemotronFlashForCausalLM, NemotronFlashPreTrainedTokenizerFast
1616
from .modeling_nemotron_h import NemotronHForCausalLM
1717
from .modeling_olmo3 import Olmo3ForCausalLM
18+
from .modeling_qwen2 import Qwen2ForCausalLM
1819
from .modeling_qwen3 import Qwen3ForCausalLM
1920
from .modeling_qwen3_5 import Qwen3_5ForCausalLM, Qwen3_5ForConditionalGeneration
2021
from .modeling_qwen3_5_moe import Qwen3_5MoeForCausalLM, Qwen3_5MoeForConditionalGeneration
@@ -44,6 +45,7 @@
4445
"NemotronFlashPreTrainedTokenizerFast",
4546
"NemotronHForCausalLM",
4647
"Olmo3ForCausalLM",
48+
"Qwen2ForCausalLM",
4749
"Qwen3ForCausalLM",
4850
"Qwen3_5ForCausalLM",
4951
"Qwen3_5ForConditionalGeneration",
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Slimmed down PyTorch Qwen2 model implementation for auto_deploy export.
17+
18+
Source:
19+
https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct
20+
21+
This implementation differs from the original HuggingFace version in the following ways:
22+
* Simplified for prefill-only inference (no KV caching)
23+
* Uses auto_deploy custom ops for export compatibility
24+
* Removed flash attention variants (uses torch_attention custom op)
25+
* Removed gradient checkpointing and training code paths
26+
* Removed attention dropout (inference only)
27+
* Removed sliding window attention (AD manages masking)
28+
* No repeat_kv — AD attention ops handle GQA natively
29+
30+
The Qwen2 family (Qwen2.5, QwQ, DeepSeek-R1-Distill-Qwen, etc.) shares a single
31+
architecture with GQA, SwiGLU MLP, RMSNorm, and RoPE. Key difference from Llama:
32+
Q/K/V projections have bias=True while O and MLP projections have bias=False.
33+
"""
34+
35+
from dataclasses import dataclass
36+
from typing import Optional, Tuple
37+
38+
import torch
39+
from torch import nn
40+
from transformers.activations import ACT2FN
41+
from transformers.generation import GenerationMixin
42+
from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS
43+
from transformers.modeling_utils import PreTrainedModel
44+
from transformers.models.qwen2.configuration_qwen2 import Qwen2Config
45+
from transformers.utils import ModelOutput
46+
47+
from tensorrt_llm._torch.auto_deploy.models.hf import AutoModelForCausalLMFactory
48+
49+
50+
class Qwen2RMSNorm(nn.Module):
51+
"""RMS Normalization for Qwen2 using AutoDeploy torch_rmsnorm reference op."""
52+
53+
def __init__(self, hidden_size: int, eps: float = 1e-6):
54+
super().__init__()
55+
self.weight = nn.Parameter(torch.ones(hidden_size))
56+
self.variance_epsilon = eps
57+
58+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
59+
return torch.ops.auto_deploy.torch_rmsnorm(
60+
hidden_states, self.weight, self.variance_epsilon
61+
)
62+
63+
64+
class Qwen2RotaryEmbedding(nn.Module):
65+
"""Rotary Position Embedding for Qwen2 family.
66+
67+
Supports all rope types (default, linear, dynamic, etc.) via
68+
transformers ROPE_INIT_FUNCTIONS. Precomputes and caches cos/sin values.
69+
Slices by position_ids once and returns pre-sliced cos/sin to all layers.
70+
71+
Uses _ad_ prefix for buffer names to work with AutoDeploy's lift_to_meta.
72+
"""
73+
74+
def __init__(self, config: Qwen2Config):
75+
super().__init__()
76+
if hasattr(config, "rope_scaling") and isinstance(config.rope_scaling, dict):
77+
rope_type = config.rope_scaling.get(
78+
"rope_type", config.rope_scaling.get("type", "default")
79+
)
80+
else:
81+
rope_type = "default"
82+
83+
inv_freq, self.attention_scaling = ROPE_INIT_FUNCTIONS[rope_type](config, device=None)
84+
85+
max_pos = config.max_position_embeddings
86+
t = torch.arange(max_pos, dtype=inv_freq.dtype)
87+
freqs = torch.outer(t, inv_freq)
88+
emb = torch.cat((freqs, freqs), dim=-1)
89+
self.register_buffer("_ad_cos_cached", emb.cos() * self.attention_scaling, persistent=False)
90+
self.register_buffer("_ad_sin_cached", emb.sin() * self.attention_scaling, persistent=False)
91+
92+
def forward(
93+
self, x: torch.Tensor, position_ids: torch.Tensor
94+
) -> Tuple[torch.Tensor, torch.Tensor]:
95+
cos = self._ad_cos_cached.to(dtype=x.dtype, device=x.device)
96+
sin = self._ad_sin_cached.to(dtype=x.dtype, device=x.device)
97+
return cos[position_ids], sin[position_ids]
98+
99+
100+
class Qwen2MLP(nn.Module):
101+
"""MLP layer for Qwen2 (SwiGLU activation). All projections have bias=False."""
102+
103+
def __init__(self, config: Qwen2Config):
104+
super().__init__()
105+
self.hidden_size = config.hidden_size
106+
self.intermediate_size = config.intermediate_size
107+
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
108+
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
109+
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
110+
self.act_fn = ACT2FN[config.hidden_act]
111+
112+
def forward(self, x: torch.Tensor) -> torch.Tensor:
113+
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
114+
115+
116+
class Qwen2Attention(nn.Module):
117+
"""Grouped Query Attention for Qwen2.
118+
119+
Uses AD canonical ops for attention and RoPE. GQA is handled natively
120+
by torch_attention — no repeat_kv needed. Q/K/V projections have bias=True,
121+
O projection has bias=False.
122+
"""
123+
124+
def __init__(self, config: Qwen2Config, layer_idx: Optional[int] = None):
125+
super().__init__()
126+
self.config = config
127+
self.layer_idx = layer_idx
128+
129+
self.hidden_size = config.hidden_size
130+
self.num_heads = config.num_attention_heads
131+
self.num_kv_heads = config.num_key_value_heads
132+
self.head_dim = getattr(
133+
config, "head_dim", config.hidden_size // config.num_attention_heads
134+
)
135+
self.scaling = self.head_dim ** (-0.5)
136+
137+
# Qwen2: Q/K/V have bias=True, O has bias=False
138+
self.q_proj = nn.Linear(self.hidden_size, self.num_heads * self.head_dim, bias=True)
139+
self.k_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=True)
140+
self.v_proj = nn.Linear(self.hidden_size, self.num_kv_heads * self.head_dim, bias=True)
141+
self.o_proj = nn.Linear(self.num_heads * self.head_dim, self.hidden_size, bias=False)
142+
143+
def forward(
144+
self,
145+
hidden_states: torch.Tensor,
146+
position_embeddings: Tuple[torch.Tensor, torch.Tensor],
147+
) -> torch.Tensor:
148+
bsz, q_len, _ = hidden_states.size()
149+
150+
q = self.q_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim)
151+
k = self.k_proj(hidden_states).view(bsz, q_len, self.num_kv_heads, self.head_dim)
152+
v = self.v_proj(hidden_states).view(bsz, q_len, self.num_kv_heads, self.head_dim)
153+
154+
cos, sin = position_embeddings
155+
156+
q, k = torch.ops.auto_deploy.torch_rope_with_explicit_cos_sin(
157+
q,
158+
k,
159+
cos,
160+
sin,
161+
2, # unsqueeze_dim=2 for BSND layout
162+
)
163+
164+
attn_output = torch.ops.auto_deploy.torch_attention(
165+
q,
166+
k,
167+
v,
168+
None, # attn_mask
169+
0.0, # dropout_p
170+
True, # is_causal
171+
self.scaling,
172+
None, # sinks
173+
None, # sliding_window
174+
None, # logit_cap
175+
"bsnd",
176+
)
177+
178+
attn_output = attn_output.reshape(bsz, q_len, self.num_heads * self.head_dim)
179+
attn_output = self.o_proj(attn_output)
180+
181+
return attn_output
182+
183+
184+
class Qwen2DecoderLayer(nn.Module):
185+
"""Transformer decoder layer for Qwen2."""
186+
187+
def __init__(self, config: Qwen2Config, layer_idx: int):
188+
super().__init__()
189+
self.hidden_size = config.hidden_size
190+
191+
self.self_attn = Qwen2Attention(config, layer_idx=layer_idx)
192+
self.mlp = Qwen2MLP(config)
193+
self.input_layernorm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
194+
self.post_attention_layernorm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
195+
196+
def forward(
197+
self,
198+
hidden_states: torch.Tensor,
199+
position_embeddings: Tuple[torch.Tensor, torch.Tensor],
200+
) -> torch.Tensor:
201+
residual = hidden_states
202+
hidden_states = self.input_layernorm(hidden_states)
203+
hidden_states = self.self_attn(hidden_states, position_embeddings)
204+
hidden_states = residual + hidden_states
205+
206+
residual = hidden_states
207+
hidden_states = self.post_attention_layernorm(hidden_states)
208+
hidden_states = self.mlp(hidden_states)
209+
hidden_states = residual + hidden_states
210+
211+
return hidden_states
212+
213+
214+
@dataclass
215+
class Qwen2ModelOutput(ModelOutput):
216+
"""Output for Qwen2Model."""
217+
218+
last_hidden_state: Optional[torch.FloatTensor] = None
219+
220+
221+
@dataclass
222+
class Qwen2CausalLMOutput(ModelOutput):
223+
"""Output for Qwen2ForCausalLM."""
224+
225+
logits: Optional[torch.FloatTensor] = None
226+
227+
228+
class Qwen2PreTrainedModel(PreTrainedModel):
229+
"""Base class for Qwen2 models."""
230+
231+
config_class = Qwen2Config
232+
base_model_prefix = "model"
233+
_no_split_modules = ["Qwen2DecoderLayer"]
234+
supports_gradient_checkpointing = False
235+
236+
def _init_weights(self, module):
237+
std = self.config.initializer_range
238+
if isinstance(module, nn.Linear):
239+
module.weight.data.normal_(mean=0.0, std=std)
240+
if module.bias is not None:
241+
module.bias.data.zero_()
242+
elif isinstance(module, nn.Embedding):
243+
module.weight.data.normal_(mean=0.0, std=std)
244+
if module.padding_idx is not None:
245+
module.weight.data[module.padding_idx].zero_()
246+
247+
248+
class Qwen2Model(Qwen2PreTrainedModel):
249+
"""Qwen2 transformer decoder model."""
250+
251+
def __init__(self, config: Qwen2Config):
252+
super().__init__(config)
253+
self.config = config
254+
self.padding_idx = config.pad_token_id
255+
self.vocab_size = config.vocab_size
256+
257+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
258+
self.layers = nn.ModuleList(
259+
[Qwen2DecoderLayer(config, layer_idx=idx) for idx in range(config.num_hidden_layers)]
260+
)
261+
self.norm = Qwen2RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
262+
self.rotary_emb = Qwen2RotaryEmbedding(config)
263+
264+
self.post_init()
265+
266+
def get_input_embeddings(self):
267+
return self.embed_tokens
268+
269+
def set_input_embeddings(self, value):
270+
self.embed_tokens = value
271+
272+
def forward(
273+
self,
274+
input_ids: Optional[torch.LongTensor] = None,
275+
position_ids: Optional[torch.LongTensor] = None,
276+
inputs_embeds: Optional[torch.FloatTensor] = None,
277+
**kwargs,
278+
) -> Qwen2ModelOutput:
279+
if input_ids is not None and inputs_embeds is not None:
280+
raise ValueError("Cannot specify both input_ids and inputs_embeds")
281+
elif input_ids is None and inputs_embeds is None:
282+
raise ValueError("Must specify either input_ids or inputs_embeds")
283+
284+
assert position_ids is not None, "position_ids must be provided for AD export"
285+
286+
if inputs_embeds is None:
287+
inputs_embeds = self.embed_tokens(input_ids)
288+
289+
inputs_embeds = inputs_embeds.to(self.norm.weight.dtype)
290+
291+
position_embeddings = self.rotary_emb(inputs_embeds, position_ids)
292+
293+
hidden_states = inputs_embeds
294+
295+
for decoder_layer in self.layers:
296+
hidden_states = decoder_layer(hidden_states, position_embeddings)
297+
298+
hidden_states = self.norm(hidden_states)
299+
300+
return Qwen2ModelOutput(last_hidden_state=hidden_states)
301+
302+
303+
class Qwen2ForCausalLM(Qwen2PreTrainedModel, GenerationMixin):
304+
"""Qwen2 model with language modeling head."""
305+
306+
_tied_weights_keys = ["lm_head.weight"]
307+
308+
def __init__(self, config, **kwargs):
309+
super().__init__(config)
310+
self.model = Qwen2Model(config)
311+
self.vocab_size = config.vocab_size
312+
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
313+
314+
self.post_init()
315+
316+
def get_input_embeddings(self):
317+
return self.model.embed_tokens
318+
319+
def set_input_embeddings(self, value):
320+
self.model.embed_tokens = value
321+
322+
def get_output_embeddings(self):
323+
return self.lm_head
324+
325+
def set_output_embeddings(self, new_embeddings):
326+
self.lm_head = new_embeddings
327+
328+
def get_decoder(self):
329+
return self.model
330+
331+
def forward(
332+
self,
333+
input_ids: Optional[torch.LongTensor] = None,
334+
position_ids: Optional[torch.LongTensor] = None,
335+
inputs_embeds: Optional[torch.FloatTensor] = None,
336+
**kwargs,
337+
) -> Qwen2CausalLMOutput:
338+
assert position_ids is not None, "position_ids must be provided for AD export"
339+
outputs = self.model(
340+
input_ids=input_ids,
341+
position_ids=position_ids,
342+
inputs_embeds=inputs_embeds,
343+
**kwargs,
344+
)
345+
346+
hidden_states = outputs.last_hidden_state
347+
logits = self.lm_head(hidden_states).float()
348+
349+
return Qwen2CausalLMOutput(logits=logits)
350+
351+
352+
# Register with AutoModelForCausalLMFactory
353+
AutoModelForCausalLMFactory.register_custom_model_cls("Qwen2Config", Qwen2ForCausalLM)

0 commit comments

Comments
 (0)