Skip to content

Commit 7b9c247

Browse files
committed
[spmd_types] qwen3.5 enablement
Enable Qwen3.5 input annotations, local multimodal regions, sharding configs, and torch_native GDN coverage for the spmd_types backend. Status: FSDP=2 matched default backend bitwise over 10 debug steps. TP=2 and FSDP=2+TP=2 run, but are not bitwise against default backend; grad_norm diverges from step 1. TP=2 typechecking passes for qwen35_debugmodel and qwen35_debugmodel_gdn_torch_native. ghstack-source-id: 9115a56 Pull Request resolved: #3895
1 parent a764afb commit 7b9c247

7 files changed

Lines changed: 253 additions & 95 deletions

File tree

torchtitan/distributed/spmd_types.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,9 @@ def annotate_input_spmd_types(
168168
169169
Hardcodes the standard decoder convention: inputs and positions are
170170
``S(0)@DP, S(1)@CP, R@TP``; labels are ``S(0)@DP, S(1)@CP, I@TP``.
171+
Qwen3.5 multimodal payload tensors are irregular across DP/CP shards and
172+
invariant across TP ranks; multimodal compute should handle them in local
173+
SPMD regions.
171174
Analogous to ``full_dtensor.parallelize_inputs()`` but for the
172175
``spmd_types`` path.
173176
"""
@@ -183,15 +186,27 @@ def annotate_input_spmd_types(
183186
MeshAxisName.CP: spmd.S(1),
184187
MeshAxisName.TP: spmd.I,
185188
}
189+
multimodal_type = {
190+
MeshAxisName.DP: spmd.V,
191+
MeshAxisName.CP: spmd.V,
192+
MeshAxisName.TP: spmd.I,
193+
}
186194

187195
mesh = parallel_dims.spmd_dense_mesh()
188196
with set_current_spmd_mesh(mesh):
189197
spmd.assert_type(inputs, token_type)
190198
spmd.assert_type(labels, label_type)
191-
if "positions" in extra_kwargs and isinstance(
192-
extra_kwargs["positions"], torch.Tensor
199+
for name in ("positions", "mrope_positions"):
200+
if name in extra_kwargs and isinstance(extra_kwargs[name], torch.Tensor):
201+
spmd.assert_type(extra_kwargs[name], token_type)
202+
for name in (
203+
"pixel_values",
204+
"pixel_values_videos",
205+
"grid_thw",
206+
"grid_thw_videos",
193207
):
194-
spmd.assert_type(extra_kwargs["positions"], token_type)
208+
if name in extra_kwargs and isinstance(extra_kwargs[name], torch.Tensor):
209+
spmd.assert_type(extra_kwargs[name], multimodal_type)
195210
return inputs, labels, extra_kwargs
196211

197212

torchtitan/models/qwen3_5/config_registry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,14 @@ def qwen35_debugmodel() -> Trainer.Config:
6969
)
7070

7171

72+
def qwen35_debugmodel_gdn_torch_native() -> Trainer.Config:
73+
config = qwen35_debugmodel()
74+
for layer in config.model_spec.model.layers:
75+
if layer.delta_net is not None:
76+
layer.delta_net.kernel.backend = "torch_native"
77+
return config
78+
79+
7280
def qwen35_debugmodel_moe() -> Trainer.Config:
7381
model_spec = model_registry("debugmodel_moe", moe_comm_backend="standard")
7482
return Trainer.Config(

torchtitan/models/qwen3_5/model.py

Lines changed: 91 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from dataclasses import dataclass
99
from typing import Literal
1010

11+
import spmd_types as spmd
1112
import torch
1213
import torch.nn.functional as F
1314

@@ -19,6 +20,8 @@
1920
from torch.distributed.tensor import DTensor
2021
from torch.distributed.tensor.experimental import local_map
2122

23+
from torchtitan.distributed.spmd_types import current_spmd_mesh
24+
from torchtitan.distributed.utils import get_spmd_backend
2225
from torchtitan.models.common import Conv1d, FeedForward, Linear
2326
from torchtitan.models.common.attention import AttentionMasksType, BaseAttention
2427
from torchtitan.models.common.decoder import Decoder
@@ -64,7 +67,7 @@ def _torch_native_gated_delta(
6467
k = _l2norm(k.float(), dim=-1)
6568
v, g, beta = v.float(), g.float(), beta.float()
6669

67-
output = torch.zeros(B, L, H, D_v, dtype=torch.float32, device=q.device)
70+
output = torch.zeros_like(v, dtype=torch.float32)
6871
state = torch.zeros(B, H, D_k, D_v, dtype=torch.float32, device=q.device)
6972

7073
for t in range(L):
@@ -196,29 +199,30 @@ def forward(
196199
if self.backend == "torch_native":
197200
return _torch_native_gated_delta(q, k, v, g, beta)
198201

199-
if self.backend == "fla_chunked":
200-
result = _fla_chunk_gated_delta_rule(
201-
q,
202-
k,
203-
v,
204-
g,
205-
beta,
206-
use_qk_l2norm_in_kernel=True,
207-
)
208-
elif self.backend == "fla_fused_recurrent":
209-
result = _fla_fused_recurrent_gated_delta_rule(
210-
q,
211-
k,
212-
v,
213-
g,
214-
beta=beta,
215-
use_qk_l2norm_in_kernel=True,
216-
)
217-
else:
218-
raise ValueError(
219-
f"Unknown fla_backend '{self.backend}'. "
220-
"Valid: 'fla_chunked', 'fla_fused_recurrent', 'torch_native'."
221-
)
202+
with spmd.no_typecheck():
203+
if self.backend == "fla_chunked":
204+
result = _fla_chunk_gated_delta_rule(
205+
q,
206+
k,
207+
v,
208+
g,
209+
beta,
210+
use_qk_l2norm_in_kernel=True,
211+
)
212+
elif self.backend == "fla_fused_recurrent":
213+
result = _fla_fused_recurrent_gated_delta_rule(
214+
q,
215+
k,
216+
v,
217+
g,
218+
beta=beta,
219+
use_qk_l2norm_in_kernel=True,
220+
)
221+
else:
222+
raise ValueError(
223+
f"Unknown fla_backend '{self.backend}'. "
224+
"Valid: 'fla_chunked', 'fla_fused_recurrent', 'torch_native'."
225+
)
222226

223227
# FLA kernels return (output, final_state); we only need output
224228
return result[0]
@@ -281,6 +285,20 @@ def __init__(self, config: Config):
281285

282286
def _causal_conv(self, x: torch.Tensor, conv: nn.Module) -> torch.Tensor:
283287
x = F.pad(x.transpose(1, 2), [self.conv_kernel_size - 1, 0])
288+
289+
def _conv(x_local: torch.Tensor, w_local: torch.Tensor) -> torch.Tensor:
290+
# groups == local out-channels for depthwise channel-sharded conv.
291+
# pyrefly: ignore [no-matching-overload]
292+
return F.conv1d(
293+
x_local,
294+
w_local,
295+
None,
296+
conv.stride,
297+
conv.padding,
298+
conv.dilation,
299+
w_local.size(0),
300+
)
301+
284302
if isinstance(x, DTensor):
285303
# TODO: Remove once the DTensor Conv1d dispatch fix for sharded
286304
# groups lands in a released torch. local_map runs the conv on
@@ -290,19 +308,6 @@ def _causal_conv(self, x: torch.Tensor, conv: nn.Module) -> torch.Tensor:
290308
w = conv.weight
291309
w_plc = w.placements # pyrefly: ignore [missing-attribute]
292310

293-
def _conv(x_local: torch.Tensor, w_local: torch.Tensor) -> torch.Tensor:
294-
# groups == local out-channels (depthwise, channel-sharded)
295-
# pyrefly: ignore [no-matching-overload]
296-
return F.conv1d(
297-
x_local,
298-
w_local,
299-
None,
300-
conv.stride,
301-
conv.padding,
302-
conv.dilation,
303-
w_local.size(0),
304-
)
305-
306311
conv_dt = local_map(
307312
_conv,
308313
out_placements=(x_plc,),
@@ -311,6 +316,15 @@ def _conv(x_local: torch.Tensor, w_local: torch.Tensor) -> torch.Tensor:
311316
device_mesh=x.device_mesh,
312317
)
313318
x = conv_dt(x, w) # pyrefly: ignore
319+
elif get_spmd_backend() == "spmd_types":
320+
conv_spmd = spmd.local_map(
321+
in_types=(
322+
{"dp": spmd.S(0), "cp": spmd.S(2), "tp": spmd.S(1)},
323+
{"dp": spmd.R, "cp": spmd.R, "tp": spmd.S(0)},
324+
),
325+
out_types={"dp": spmd.S(0), "cp": spmd.S(2), "tp": spmd.S(1)},
326+
)(_conv)
327+
x = conv_spmd(x, conv.weight)
314328
else:
315329
x = conv(x)
316330
return F.silu(x).transpose(1, 2)
@@ -329,9 +343,20 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
329343
xa = self.in_proj_a(x)
330344
xb = self.in_proj_b(x)
331345

332-
xq = xq.view(bs, seqlen, -1, self.key_head_dim)
333-
xk = xk.view(bs, seqlen, -1, self.key_head_dim)
334-
xv = xv.view(bs, seqlen, -1, self.value_head_dim)
346+
def local_head_split(t: torch.Tensor, head_dim: int) -> torch.Tensor:
347+
# Drop into local region, we can't propagate S(2) -> head unflatten.
348+
# TODO(pianpwk): this should be doable once spmd_types tracks sharding evenness.
349+
with spmd.local():
350+
t = t.view(bs, seqlen, -1, head_dim)
351+
if get_spmd_backend() == "spmd_types":
352+
spmd.assert_type(
353+
t, spmd.V, spmd.PartitionSpec("dp", "cp", "tp", None)
354+
)
355+
return t
356+
357+
xq = local_head_split(xq, self.key_head_dim)
358+
xk = local_head_split(xk, self.key_head_dim)
359+
xv = local_head_split(xv, self.value_head_dim)
335360

336361
# Gating signals, shape (bs, seqlen, n_value_heads):
337362
# g: decay rate per head, always negative
@@ -341,7 +366,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
341366

342367
output = self.kernel(xq, xk, xv, g, beta)
343368

344-
xz = xz.view(bs, seqlen, -1, self.value_head_dim)
369+
xz = local_head_split(xz, self.value_head_dim)
345370
output = self.norm(output, xz)
346371

347372
output = output.reshape(bs, seqlen, -1)
@@ -407,11 +432,22 @@ def forward(
407432
) -> torch.Tensor:
408433
bs, seqlen, _ = x.shape
409434

435+
def local_head_split(t: torch.Tensor, head_dim: int) -> torch.Tensor:
436+
# Drop into local region, we can't propagate S(2) -> head unflatten.
437+
# TODO(pianpwk): this should be doable once spmd_types tracks sharding evenness.
438+
with spmd.local():
439+
t = t.view(bs, seqlen, -1, head_dim)
440+
if get_spmd_backend() == "spmd_types":
441+
spmd.assert_type(
442+
t, spmd.V, spmd.PartitionSpec("dp", "cp", "tp", None)
443+
)
444+
return t
445+
410446
# wq is 2x wider: produces query + gate
411-
xq_gate = self.wq(x).view(bs, seqlen, -1, self.head_dim * 2)
447+
xq_gate = local_head_split(self.wq(x), self.head_dim * 2)
412448
xq, gate = xq_gate.chunk(2, dim=-1)
413-
xk = self.wk(x).view(bs, seqlen, -1, self.head_dim)
414-
xv = self.wv(x).view(bs, seqlen, -1, self.head_dim)
449+
xk = local_head_split(self.wk(x), self.head_dim)
450+
xv = local_head_split(self.wv(x), self.head_dim)
415451

416452
# QK norm (before RoPE)
417453
xq = self.q_norm(xq)
@@ -675,12 +711,23 @@ def _scatter_vision_embeds(
675711
Returns:
676712
Updated embeddings
677713
"""
714+
# Vision compute is TP-invariant; I->R convert to mix with text embeddings.
715+
if spmd.is_type_checking():
716+
merged_embeds = spmd.convert(
717+
merged_embeds,
718+
current_spmd_mesh().get_group("tp"), # pyrefly: ignore [missing-attribute]
719+
src=spmd.I,
720+
dst=spmd.R,
721+
backward_options={"op_dtype": merged_embeds.dtype},
722+
)
723+
678724
for item_idx, sample_idx, vision_start, n_tokens in vision_positions:
679725
inputs_embeds[
680726
sample_idx, vision_start : vision_start + n_tokens, :
681727
] = merged_embeds[item_idx, :n_tokens, :]
682728
return inputs_embeds
683729

730+
@spmd.local_map(out_types={"dp": spmd.S(0), "cp": spmd.S(1), "tp": spmd.R})
684731
def _prepare_multimodal_embeds(
685732
self,
686733
tokens: torch.Tensor,

torchtitan/models/qwen3_5/parallelize.py

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import torch
1515
import torch.nn as nn
1616
from torch.distributed._composable.fsdp import fully_shard
17-
from torch.distributed.fsdp import MixedPrecisionPolicy
17+
from torch.distributed.fsdp import DataParallelMeshDims, MixedPrecisionPolicy
1818

1919
from torchtitan.config import (
2020
CompileConfig,
@@ -30,6 +30,11 @@
3030
apply_fsdp_to_decoder,
3131
get_fsdp_reshard_after_forward_policy,
3232
)
33+
from torchtitan.distributed.full_dtensor import (
34+
resolve_fsdp_mesh,
35+
resolve_sparse_fsdp_mesh,
36+
validate_config,
37+
)
3338
from torchtitan.distributed.tensor_parallel import maybe_enable_async_tp
3439

3540

@@ -40,6 +45,7 @@ def _apply_fsdp_to_vision_encoder(
4045
reduce_dtype: torch.dtype,
4146
reshard_after_forward_policy: str = "default",
4247
pp_enabled: bool = False,
48+
dp_mesh_dims: DataParallelMeshDims | None = None,
4349
):
4450
"""FSDP the vision encoder as a single unit.
4551
@@ -56,6 +62,7 @@ def _apply_fsdp_to_vision_encoder(
5662
mesh=dp_mesh,
5763
mp_policy=mp_policy,
5864
reshard_after_forward=reshard_after_forward,
65+
dp_mesh_dims=dp_mesh_dims,
5966
)
6067

6168

@@ -90,10 +97,14 @@ def parallelize_qwen3_5(
9097
"and multimodal CP needs vision scatter before CP sharding."
9198
)
9299

93-
if parallel_dims.tp_enabled or parallel_dims.ep_enabled:
94-
if parallelism.enable_async_tensor_parallel and not model_compile_enabled:
95-
raise RuntimeError("Async TP requires torch.compile")
100+
if parallelism.enable_async_tensor_parallel and not model_compile_enabled:
101+
raise RuntimeError("Async TP requires torch.compile")
96102

103+
if parallelism.spmd_backend == "spmd_types":
104+
validate_config(parallel_dims, model)
105+
# pyrefly: ignore [not-callable]
106+
model.parallelize(parallel_dims)
107+
elif parallel_dims.tp_enabled or parallel_dims.ep_enabled:
97108
# pyrefly: ignore [not-callable]
98109
model.parallelize(parallel_dims)
99110

@@ -112,10 +123,24 @@ def parallelize_qwen3_5(
112123
# pyrefly: ignore [bad-argument-type]
113124
apply_compile(model.vision_encoder, compile_config)
114125

115-
dp_mesh_names = (
116-
["dp_replicate", "fsdp"] if parallel_dims.dp_replicate_enabled else ["fsdp"]
117-
)
118-
dp_mesh = parallel_dims.get_mesh(dp_mesh_names)
126+
if parallelism.spmd_backend == "spmd_types":
127+
dp_mesh, dp_mesh_dims = resolve_fsdp_mesh(parallel_dims)
128+
edp_mesh, edp_mesh_dims = resolve_sparse_fsdp_mesh(parallel_dims)
129+
else:
130+
dp_mesh_names = (
131+
["dp_replicate", "fsdp"] if parallel_dims.dp_replicate_enabled else ["fsdp"]
132+
)
133+
dp_mesh = parallel_dims.get_mesh(dp_mesh_names)
134+
dp_mesh_dims = None
135+
edp_mesh = None
136+
edp_mesh_dims = None
137+
if parallel_dims.ep_enabled:
138+
edp_mesh_names = (
139+
["dp_replicate", "efsdp"]
140+
if parallel_dims.dp_replicate_enabled
141+
else ["efsdp"]
142+
)
143+
edp_mesh = parallel_dims.get_optional_mesh(edp_mesh_names)
119144

120145
if model.vision_encoder is not None:
121146
_apply_fsdp_to_vision_encoder(
@@ -125,17 +150,9 @@ def parallelize_qwen3_5(
125150
reduce_dtype=TORCH_DTYPE_MAP[training.mixed_precision_reduce],
126151
reshard_after_forward_policy=parallelism.fsdp_reshard_after_forward,
127152
pp_enabled=parallel_dims.pp_enabled,
153+
dp_mesh_dims=dp_mesh_dims,
128154
)
129155

130-
edp_mesh = None
131-
if parallel_dims.ep_enabled:
132-
edp_mesh_names = (
133-
["dp_replicate", "efsdp"]
134-
if parallel_dims.dp_replicate_enabled
135-
else ["efsdp"]
136-
)
137-
edp_mesh = parallel_dims.get_optional_mesh(edp_mesh_names)
138-
139156
apply_fsdp_to_decoder(
140157
model, # pyrefly: ignore [bad-argument-type]
141158
dp_mesh,
@@ -146,6 +163,9 @@ def parallelize_qwen3_5(
146163
reshard_after_forward_policy=parallelism.fsdp_reshard_after_forward,
147164
ep_degree=parallel_dims.ep,
148165
edp_mesh=edp_mesh,
166+
dp_mesh_dims=dp_mesh_dims,
167+
edp_mesh_dims=edp_mesh_dims,
168+
enable_symm_mem=parallelism.enable_fsdp_symm_mem,
149169
)
150170

151171
return model

torchtitan/models/qwen3_5/rope.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ def _compute_mrope_cache(self, position_ids: torch.Tensor) -> torch.Tensor:
7777
if isinstance(position_ids, DTensor)
7878
else position_ids
7979
)
80-
pos = pos.to(device=rope_cache.device)
80+
if pos.device != rope_cache.device:
81+
pos = pos.to(device=rope_cache.device)
8182

8283
_maybe_check_max_pos(pos, max_valid_pos=rope_cache.shape[0] - 1)
8384
head_dim = rope_cache.shape[-1] // 2

0 commit comments

Comments
 (0)