Skip to content

Commit f1f3ae8

Browse files
committed
fix(engine): localize DTensor norm output for Qwen models in TP
Qwen models have intermediate ops (aten.alias, aten.slice) between the final norm and lm_head that break DTensor dispatch under tensor parallelism. This commit: - Adds is_qwen_model() helper to identify Qwen model family. - Registers a forward hook on the final norm to redistribute its DTensor output to Replicate and convert to a local tensor. - Adjusts lm_head/score input_layouts to Replicate() for Qwen models so the downstream linear layers receive plain tensors. - Extracts backbone variable to avoid redundant attribute access. Without this fix, Qwen models crash with DTensor dispatch errors when running with TP > 1.
1 parent 1fab24a commit f1f3ae8

2 files changed

Lines changed: 46 additions & 10 deletions

File tree

areal/engine/core/model.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ def is_gemma3_model(model_type: str) -> bool:
5959
return model_type in ["gemma3"]
6060

6161

62+
def is_qwen_model(model_type: str) -> bool:
63+
return model_type.startswith("qwen")
64+
65+
6266
VALID_MOE_MODELS = [
6367
"qwen3_moe",
6468
"qwen3_vl_moe",

areal/engine/fsdp_utils/parallel.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# SPDX-License-Identifier: Apache-2.0
22

33
from dataclasses import dataclass
4+
from typing import cast
45

56
import torch
67
from torch import nn
78
from torch.distributed import ProcessGroup
89
from torch.distributed.device_mesh import DeviceMesh, init_device_mesh
910
from torch.distributed.fsdp import CPUOffloadPolicy, MixedPrecisionPolicy
10-
from torch.distributed.tensor import Replicate, Shard
11+
from torch.distributed.tensor import DTensor, Replicate, Shard
1112
from torch.distributed.tensor.parallel import (
1213
ColwiseParallel,
1314
ParallelStyle,
@@ -24,6 +25,7 @@
2425
is_gemma3_model,
2526
is_moe_model,
2627
is_qwen3_vl_model,
28+
is_qwen_model,
2729
is_valid_vision_model,
2830
)
2931
from areal.engine.fsdp_utils import apply_fsdp2
@@ -216,6 +218,17 @@ def seq_len_divisor(self) -> int:
216218
return self._ps.tp_size * self._ps.cp_size
217219

218220

221+
def _localize_dtensor_output(
222+
_module: nn.Module, _inputs: tuple[object, ...], output: object
223+
) -> object:
224+
if not isinstance(output, DTensor):
225+
return output
226+
227+
dtensor_output = cast(DTensor, output)
228+
placements = tuple(Replicate() for _ in dtensor_output.placements)
229+
return dtensor_output.redistribute(placements=placements).to_local()
230+
231+
219232
def apply_non_moe_tp(
220233
model: nn.Module,
221234
model_config: PretrainedConfig,
@@ -302,27 +315,33 @@ def apply_non_moe_tp(
302315
}
303316
)
304317

318+
# Qwen models: norm→lm_head path has DTensor-incompatible ops, so we localize
319+
# norm output to Replicate; lm_head input layout must match accordingly
320+
use_local_final_norm_output = is_qwen_model(model_config.model_type)
321+
head_input_layout = Replicate() if use_local_final_norm_output else Shard(1)
322+
305323
# For root module
306324
root_tp_plan: dict[str, ParallelStyle] = {}
307325
if hasattr(model, "lm_head") and isinstance(model.lm_head, nn.Module):
308-
# Implicitly all-gather in ColwiseParallel
309-
# Output is sharded on the last dimension (Shard(2))
326+
# Implicitly all-gather in ColwiseParallel when the input is Shard(1).
327+
# Output is sharded on the last dimension (Shard(2)).
310328
root_tp_plan["lm_head"] = ColwiseParallel(
311-
input_layouts=Shard(1),
329+
input_layouts=head_input_layout,
312330
)
313331
if hasattr(model, "score") and isinstance(model.score, nn.Module):
314332
# For PPO's critic model's score layer:
315-
# 1. The input is sharded by sequence parallelism (Shard(1))
333+
# 1. The input follows the final norm output layout
316334
# 2. `score` is a linear layer with replicated weights
317335
# 3. All-gather the output along the sequence dimension to get the full results
318336
root_tp_plan["score"] = ReplicateParallel(
319-
input_layout=Shard(1),
320-
desired_input_layout=Shard(1),
337+
input_layout=head_input_layout,
338+
desired_input_layout=head_input_layout,
321339
output_layout=Replicate(),
322340
)
323341

324342
if is_valid_vision_model(model_config.model_type):
325343
if isinstance(model.model.language_model, nn.Module):
344+
backbone = model.model.language_model
326345
# For vision-language models, avoid sharding the embedding layer because
327346
# the visual components access it without tensor parallelism support.
328347
# Instead, configure the first transformer layer to handle input
@@ -342,10 +361,10 @@ def apply_non_moe_tp(
342361
patch_qwen3_vl_deepstack_process_for_tp,
343362
)
344363

345-
patch_qwen3_vl_deepstack_process_for_tp(model.model.language_model)
364+
patch_qwen3_vl_deepstack_process_for_tp(backbone)
346365

347366
parallelize_module(
348-
model.model.language_model,
367+
backbone,
349368
device_mesh=tp_device_mesh,
350369
parallelize_plan=model_tp_plan,
351370
)
@@ -354,8 +373,9 @@ def apply_non_moe_tp(
354373
"Vision model does not have the required submodule 'model.language_model'"
355374
)
356375
else:
376+
backbone = model.model
357377
parallelize_module(
358-
model.model,
378+
backbone,
359379
device_mesh=tp_device_mesh,
360380
parallelize_plan=model_tp_plan,
361381
)
@@ -366,6 +386,18 @@ def apply_non_moe_tp(
366386
parallelize_plan=root_tp_plan,
367387
)
368388

389+
# Register norm localization hook after parallelize_module so norm is already
390+
# SequenceParallel. We reuse the `backbone` determined above to avoid
391+
# re-deriving it.
392+
if use_local_final_norm_output:
393+
norm = getattr(backbone, "norm", None)
394+
if not isinstance(norm, nn.Module):
395+
raise RuntimeError(
396+
f"Model backbone ({type(backbone).__name__}) does not have 'norm' submodule, "
397+
f"but localized norm output is required for model_type={model_config.model_type!r}."
398+
)
399+
cast(nn.Module, norm).register_forward_hook(_localize_dtensor_output)
400+
369401

370402
def parallelize_model(
371403
model: nn.Module,

0 commit comments

Comments
 (0)