Skip to content

Commit 3e5ce2f

Browse files
lucasliebmarimuthu-nv
authored andcommitted
[None][feat] Add AD custom model for Seed-OSS family (#238)
* [None][feat] Add AD custom model for Seed-OSS family Add prefill-only custom model implementation for ByteDance-Seed/Seed-OSS-36B-Instruct using AutoDeploy canonical ops (torch_attention, torch_rope_with_explicit_cos_sin, torch_rmsnorm). Seed-OSS is a dense Llama-style model with GQA (80 Q / 8 KV heads), SwiGLU MLP, and attention_bias=True on Q/K/V projections. Includes hierarchical equivalence tests (MLP, Attention, Decoder Layer, Full Model, Export) comparing against HF reference implementation. Signed-off-by: Lucas Liebenwein <11156568+lucaslie@users.noreply.github.com> * [None][fix] Add causal mask to HF reference in attention/decoder layer tests The HF eager attention does NOT apply causal masking when attention_mask=None, while our custom model always uses is_causal=True. Provide explicit causal mask to HF reference to ensure equivalent comparison. Signed-off-by: Lucas Liebenwein <11156568+lucaslie@users.noreply.github.com> --------- Signed-off-by: Lucas Liebenwein <11156568+lucaslie@users.noreply.github.com>
1 parent 0a8c5e0 commit 3e5ce2f

3 files changed

Lines changed: 891 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
@@ -18,6 +18,7 @@
1818
from .modeling_qwen2 import Qwen2ForCausalLM
1919
from .modeling_qwen3_5_moe import Qwen3_5MoeForCausalLM, Qwen3_5MoeForConditionalGeneration
2020
from .modeling_qwen3_moe import Qwen3MoeForCausalLM
21+
from .modeling_seed_oss import SeedOssForCausalLM
2122
from .modeling_skywork_r1v2 import SkyworkR1V2ForConditionalGeneration
2223
from .modeling_smollm3 import SmolLM3ForCausalLM
2324
from .modeling_starcoder2 import Starcoder2ForCausalLM
@@ -48,6 +49,7 @@
4849
"Qwen3_5MoeForCausalLM",
4950
"Qwen3_5MoeForConditionalGeneration",
5051
"Qwen3MoeForCausalLM",
52+
"SeedOssForCausalLM",
5153
"SkyworkR1V2ForConditionalGeneration",
5254
"SmolLM3ForCausalLM",
5355
"Starcoder2ForCausalLM",
Lines changed: 379 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
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 Seed-OSS model implementation for auto_deploy export.
17+
18+
Source:
19+
https://huggingface.co/ByteDance-Seed/Seed-OSS-36B-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/residual dropout (inference only)
27+
28+
The Seed-OSS model uses Grouped Query Attention (GQA) with standard RoPE.
29+
It is a dense Llama-style model with attention_bias=True and attention_out_bias=False.
30+
"""
31+
32+
from dataclasses import dataclass
33+
from typing import Optional, Tuple
34+
35+
import torch
36+
from torch import nn
37+
from transformers.activations import ACT2FN
38+
from transformers.generation import GenerationMixin
39+
from transformers.modeling_utils import PreTrainedModel
40+
from transformers.models.seed_oss.configuration_seed_oss import SeedOssConfig
41+
from transformers.utils import ModelOutput
42+
43+
from tensorrt_llm._torch.auto_deploy.models.hf import AutoModelForCausalLMFactory
44+
45+
46+
class SeedOssRMSNorm(nn.Module):
47+
"""RMS Normalization for Seed-OSS using AutoDeploy torch_rmsnorm reference op."""
48+
49+
def __init__(self, hidden_size: int, eps: float = 1e-6):
50+
super().__init__()
51+
self.weight = nn.Parameter(torch.ones(hidden_size))
52+
self.variance_epsilon = eps
53+
54+
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
55+
return torch.ops.auto_deploy.torch_rmsnorm(
56+
hidden_states, self.weight, self.variance_epsilon
57+
)
58+
59+
60+
class SeedOssRotaryEmbedding(nn.Module):
61+
"""Rotary Position Embedding for Seed-OSS.
62+
63+
Simplified version that precomputes and caches cos/sin values.
64+
Returns pre-sliced values indexed by position_ids.
65+
66+
Uses _ad_ prefix for buffer names to work with AutoDeploy's lift_to_meta.
67+
"""
68+
69+
def __init__(
70+
self,
71+
dim: int,
72+
max_position_embeddings: int = 524288,
73+
base: float = 10000.0,
74+
):
75+
super().__init__()
76+
self.dim = dim
77+
self.max_position_embeddings = max_position_embeddings
78+
self.base = base
79+
80+
inv_freq = 1.0 / (self.base ** (torch.arange(0, self.dim, 2).float() / self.dim))
81+
self.register_buffer("inv_freq", inv_freq, persistent=False)
82+
83+
# Build cos/sin cache with AD-specific naming
84+
self._set_cos_sin_cache(max_position_embeddings)
85+
86+
def _set_cos_sin_cache(self, seq_len: int):
87+
self.max_seq_len_cached = seq_len
88+
t = torch.arange(seq_len, dtype=self.inv_freq.dtype)
89+
freqs = torch.outer(t, self.inv_freq)
90+
emb = torch.cat((freqs, freqs), dim=-1)
91+
# Use _ad_ prefix for AutoDeploy compatibility with lift_to_meta
92+
self.register_buffer("_ad_cos_cached", emb.cos(), persistent=False)
93+
self.register_buffer("_ad_sin_cached", emb.sin(), persistent=False)
94+
95+
def forward(
96+
self, x: torch.Tensor, position_ids: torch.Tensor
97+
) -> Tuple[torch.Tensor, torch.Tensor]:
98+
# Slice cos/sin by position_ids here (once) instead of in every attention layer
99+
cos = self._ad_cos_cached.to(dtype=x.dtype, device=x.device)
100+
sin = self._ad_sin_cached.to(dtype=x.dtype, device=x.device)
101+
return cos[position_ids], sin[position_ids]
102+
103+
104+
class SeedOssMLP(nn.Module):
105+
"""MLP layer for Seed-OSS (SwiGLU activation)."""
106+
107+
def __init__(self, config: SeedOssConfig):
108+
super().__init__()
109+
self.config = config
110+
self.hidden_size = config.hidden_size
111+
self.intermediate_size = config.intermediate_size
112+
self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
113+
self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=config.mlp_bias)
114+
self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=config.mlp_bias)
115+
self.act_fn = ACT2FN[config.hidden_act]
116+
117+
def forward(self, x: torch.Tensor) -> torch.Tensor:
118+
return self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
119+
120+
121+
class SeedOssAttention(nn.Module):
122+
"""Grouped Query Attention for Seed-OSS with standard RoPE.
123+
124+
Uses attention_bias on Q/K/V projections and attention_out_bias on O projection.
125+
AD canonical attention ops handle GQA natively (no repeat_kv needed).
126+
"""
127+
128+
def __init__(self, config: SeedOssConfig, layer_idx: Optional[int] = None):
129+
super().__init__()
130+
self.config = config
131+
self.layer_idx = layer_idx
132+
133+
self.hidden_size = config.hidden_size
134+
self.num_heads = config.num_attention_heads
135+
self.num_kv_heads = config.num_key_value_heads
136+
self.head_dim = config.head_dim
137+
self.scaling = self.head_dim ** (-0.5)
138+
139+
# Q/K/V/O projections
140+
self.q_proj = nn.Linear(
141+
self.hidden_size, self.num_heads * self.head_dim, bias=config.attention_bias
142+
)
143+
self.k_proj = nn.Linear(
144+
self.hidden_size, self.num_kv_heads * self.head_dim, bias=config.attention_bias
145+
)
146+
self.v_proj = nn.Linear(
147+
self.hidden_size, self.num_kv_heads * self.head_dim, bias=config.attention_bias
148+
)
149+
self.o_proj = nn.Linear(
150+
self.num_heads * self.head_dim, self.hidden_size, bias=config.attention_out_bias
151+
)
152+
153+
def forward(
154+
self,
155+
hidden_states: torch.Tensor,
156+
position_embeddings: Tuple[torch.Tensor, torch.Tensor],
157+
) -> torch.Tensor:
158+
bsz, q_len, _ = hidden_states.size()
159+
160+
# Project Q/K/V and reshape to [B, S, N, head_dim] (BSND layout)
161+
q = self.q_proj(hidden_states).view(bsz, q_len, self.num_heads, self.head_dim)
162+
k = self.k_proj(hidden_states).view(bsz, q_len, self.num_kv_heads, self.head_dim)
163+
v = self.v_proj(hidden_states).view(bsz, q_len, self.num_kv_heads, self.head_dim)
164+
165+
# Get pre-sliced cos/sin from position_embeddings (already indexed by position_ids)
166+
cos, sin = position_embeddings # [B, S, head_dim]
167+
168+
# Apply RoPE using custom op (BSND layout, unsqueeze_dim=2)
169+
q, k = torch.ops.auto_deploy.torch_rope_with_explicit_cos_sin(
170+
q,
171+
k,
172+
cos,
173+
sin,
174+
2, # unsqueeze_dim=2 for BSND layout
175+
)
176+
177+
# Attention using custom op with GQA support (BSND layout)
178+
attn_output = torch.ops.auto_deploy.torch_attention(
179+
q, # [B, S, N, head_dim]
180+
k, # [B, S, N_kv, head_dim]
181+
v, # [B, S, N_kv, head_dim]
182+
None, # attn_mask
183+
0.0, # dropout_p
184+
True, # is_causal
185+
self.scaling, # scale
186+
None, # sinks
187+
None, # sliding_window
188+
None, # logit_cap
189+
"bsnd", # layout
190+
)
191+
192+
# Reshape [B, S, N, head_dim] -> [B, S, N * head_dim] and project
193+
attn_output = attn_output.reshape(bsz, q_len, self.num_heads * self.head_dim)
194+
attn_output = self.o_proj(attn_output)
195+
196+
return attn_output
197+
198+
199+
class SeedOssDecoderLayer(nn.Module):
200+
"""Transformer decoder layer for Seed-OSS."""
201+
202+
def __init__(self, config: SeedOssConfig, layer_idx: int):
203+
super().__init__()
204+
self.hidden_size = config.hidden_size
205+
206+
self.self_attn = SeedOssAttention(config, layer_idx=layer_idx)
207+
self.mlp = SeedOssMLP(config)
208+
self.input_layernorm = SeedOssRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
209+
self.post_attention_layernorm = SeedOssRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
210+
211+
def forward(
212+
self,
213+
hidden_states: torch.Tensor,
214+
position_embeddings: Tuple[torch.Tensor, torch.Tensor],
215+
) -> torch.Tensor:
216+
# Self attention
217+
residual = hidden_states
218+
hidden_states = self.input_layernorm(hidden_states)
219+
hidden_states = self.self_attn(hidden_states, position_embeddings)
220+
hidden_states = residual + hidden_states
221+
222+
# MLP
223+
residual = hidden_states
224+
hidden_states = self.post_attention_layernorm(hidden_states)
225+
hidden_states = self.mlp(hidden_states)
226+
hidden_states = residual + hidden_states
227+
228+
return hidden_states
229+
230+
231+
@dataclass
232+
class SeedOssOutput(ModelOutput):
233+
"""Output for SeedOssModel."""
234+
235+
last_hidden_state: Optional[torch.FloatTensor] = None
236+
237+
238+
@dataclass
239+
class SeedOssCausalLMOutput(ModelOutput):
240+
"""Output for SeedOssForCausalLM."""
241+
242+
logits: Optional[torch.FloatTensor] = None
243+
244+
245+
class SeedOssPreTrainedModel(PreTrainedModel):
246+
"""Base class for Seed-OSS models."""
247+
248+
config_class = SeedOssConfig
249+
base_model_prefix = "model"
250+
_no_split_modules = ["SeedOssDecoderLayer"]
251+
supports_gradient_checkpointing = False
252+
253+
def _init_weights(self, module):
254+
std = self.config.initializer_range
255+
if isinstance(module, nn.Linear):
256+
module.weight.data.normal_(mean=0.0, std=std)
257+
if module.bias is not None:
258+
module.bias.data.zero_()
259+
elif isinstance(module, nn.Embedding):
260+
module.weight.data.normal_(mean=0.0, std=std)
261+
if module.padding_idx is not None:
262+
module.weight.data[module.padding_idx].zero_()
263+
264+
265+
class SeedOssModel(SeedOssPreTrainedModel):
266+
"""Seed-OSS transformer decoder model."""
267+
268+
def __init__(self, config: SeedOssConfig):
269+
super().__init__(config)
270+
self.config = config
271+
self.padding_idx = config.pad_token_id
272+
self.vocab_size = config.vocab_size
273+
274+
self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
275+
self.layers = nn.ModuleList(
276+
[SeedOssDecoderLayer(config, layer_idx=idx) for idx in range(config.num_hidden_layers)]
277+
)
278+
self.norm = SeedOssRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
279+
280+
# Shared rotary embedding at model level
281+
self.rotary_emb = SeedOssRotaryEmbedding(
282+
config.head_dim,
283+
max_position_embeddings=config.max_position_embeddings,
284+
base=config.rope_theta,
285+
)
286+
287+
self.post_init()
288+
289+
def get_input_embeddings(self):
290+
return self.embed_tokens
291+
292+
def set_input_embeddings(self, value):
293+
self.embed_tokens = value
294+
295+
def forward(
296+
self,
297+
input_ids: Optional[torch.LongTensor] = None,
298+
position_ids: Optional[torch.LongTensor] = None,
299+
inputs_embeds: Optional[torch.FloatTensor] = None,
300+
**kwargs,
301+
) -> SeedOssOutput:
302+
if input_ids is not None and inputs_embeds is not None:
303+
raise ValueError("Cannot specify both input_ids and inputs_embeds")
304+
elif input_ids is None and inputs_embeds is None:
305+
raise ValueError("Must specify either input_ids or inputs_embeds")
306+
307+
assert position_ids is not None, "position_ids must be provided for AD export"
308+
309+
if inputs_embeds is None:
310+
inputs_embeds = self.embed_tokens(input_ids)
311+
312+
# Cast to compute dtype (e.g., bfloat16) for FP8 models where embedding
313+
# output may be FP8 but downstream ops (RMSNorm, attention) require FP16/BF16
314+
inputs_embeds = inputs_embeds.to(self.norm.weight.dtype)
315+
316+
# Compute position embeddings once (sliced by position_ids in RoPE)
317+
position_embeddings = self.rotary_emb(inputs_embeds, position_ids)
318+
319+
hidden_states = inputs_embeds
320+
321+
for decoder_layer in self.layers:
322+
hidden_states = decoder_layer(hidden_states, position_embeddings)
323+
324+
hidden_states = self.norm(hidden_states)
325+
326+
return SeedOssOutput(last_hidden_state=hidden_states)
327+
328+
329+
class SeedOssForCausalLM(SeedOssPreTrainedModel, GenerationMixin):
330+
"""Seed-OSS model with language modeling head."""
331+
332+
_tied_weights_keys = ["lm_head.weight"]
333+
334+
def __init__(self, config, **kwargs):
335+
super().__init__(config)
336+
self.model = SeedOssModel(config)
337+
self.vocab_size = config.vocab_size
338+
self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
339+
340+
self.post_init()
341+
342+
def get_input_embeddings(self):
343+
return self.model.embed_tokens
344+
345+
def set_input_embeddings(self, value):
346+
self.model.embed_tokens = value
347+
348+
def get_output_embeddings(self):
349+
return self.lm_head
350+
351+
def set_output_embeddings(self, new_embeddings):
352+
self.lm_head = new_embeddings
353+
354+
def get_decoder(self):
355+
return self.model
356+
357+
def forward(
358+
self,
359+
input_ids: Optional[torch.LongTensor] = None,
360+
position_ids: Optional[torch.LongTensor] = None,
361+
inputs_embeds: Optional[torch.FloatTensor] = None,
362+
**kwargs,
363+
) -> SeedOssCausalLMOutput:
364+
assert position_ids is not None, "position_ids must be provided for AD export"
365+
outputs = self.model(
366+
input_ids=input_ids,
367+
position_ids=position_ids,
368+
inputs_embeds=inputs_embeds,
369+
**kwargs,
370+
)
371+
372+
hidden_states = outputs.last_hidden_state
373+
logits = self.lm_head(hidden_states).float()
374+
375+
return SeedOssCausalLMOutput(logits=logits)
376+
377+
378+
# Register with AutoModelForCausalLMFactory
379+
AutoModelForCausalLMFactory.register_custom_model_cls("SeedOssConfig", SeedOssForCausalLM)

0 commit comments

Comments
 (0)