11# SPDX-License-Identifier: Apache-2.0
22
33from dataclasses import dataclass
4+ from typing import cast
45
56import torch
67from torch import nn
78from torch .distributed import ProcessGroup
89from torch .distributed .device_mesh import DeviceMesh , init_device_mesh
910from torch .distributed .fsdp import CPUOffloadPolicy , MixedPrecisionPolicy
10- from torch .distributed .tensor import Replicate , Shard
11+ from torch .distributed .tensor import DTensor , Replicate , Shard
1112from torch .distributed .tensor .parallel import (
1213 ColwiseParallel ,
1314 ParallelStyle ,
2425 is_gemma3_model ,
2526 is_moe_model ,
2627 is_qwen3_vl_model ,
28+ is_qwen_model ,
2729 is_valid_vision_model ,
2830)
2931from 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+
219232def 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
370402def parallelize_model (
371403 model : nn .Module ,
0 commit comments