Support multi sharding group on the same dimension in dump comparator (#19601)

This commit is contained in:
fzyzcjy
2026-03-01 10:36:48 +08:00
committed by GitHub
parent 46960e65cf
commit ea6ff7b01f
14 changed files with 469 additions and 151 deletions

View File

@@ -26,11 +26,10 @@ def compute_reorderer_plans(
plans: list[ReordererPlan] = []
for spec in dim_specs:
if (
spec.ordering is not None
and spec.ordering != Ordering.NATURAL
and spec.parallel is not None
):
for modifier in spec.parallel_modifiers:
if modifier.ordering is None or modifier.ordering == Ordering.NATURAL:
continue
if spec.name not in _ALLOWED_ZIGZAG_DIM_NAMES:
raise ValueError(
f"Zigzag ordering is only supported on sequence dims "
@@ -39,11 +38,11 @@ def compute_reorderer_plans(
f"but got dim name {spec.name!r} in {spec}"
)
if spec.ordering != Ordering.ZIGZAG:
if modifier.ordering != Ordering.ZIGZAG:
raise ValueError(
f"Unsupported ordering {spec.ordering!r} for dim {spec.name!r}"
f"Unsupported ordering {modifier.ordering!r} for dim {spec.name!r}"
)
axis_size: int = parallel_infos[0][spec.parallel].axis_size
axis_size: int = parallel_infos[0][modifier.axis].axis_size
if spec.name == TOKEN_DIM_NAME:
if thd_global_seq_lens is None:

View File

@@ -122,7 +122,7 @@ class _SGLangPlugin(_AuxFrameworkPlugin):
will be mishandled. Callers should set dims explicitly for non-zigzag CP.
"""
if ndim == 1:
return "t(cp,zigzag)"
return "t(cp:zigzag)"
raise ValueError(
f"SGLang: cannot infer dims for CP-sharded '{name}' with ndim={ndim}"
)
@@ -208,9 +208,9 @@ class _MegatronPlugin(_AuxFrameworkPlugin):
will be mishandled. Callers should set dims explicitly for non-zigzag CP.
"""
if ndim == 1:
return "t(cp,zigzag)"
return "t(cp:zigzag)"
if ndim == 2:
return "b s(cp,zigzag)"
return "b s(cp:zigzag)"
raise ValueError(
f"Megatron: cannot infer dims for CP-sharded '{name}' with ndim={ndim}"
)

View File

@@ -14,6 +14,7 @@ from sglang.srt.debug_utils.comparator.dims import (
TOKEN_DIM_NAME,
DimSpec,
ParallelAxis,
ParallelModifier,
)
# _CoordsList[tensor_index][axis] =
@@ -36,18 +37,21 @@ def compute_unsharder_plan(
if not parallel_infos:
raise ValueError("parallel_infos must not be empty")
sharded_axis_infos: dict[ParallelAxis, DimSpec] = {
spec.parallel: spec for spec in dim_specs if spec.parallel is not None
}
sharded_axes_raw: set[ParallelAxis] = set(sharded_axis_infos)
# Within each dim spec, reverse modifier order: innermost shard (rightmost) unshards first.
reversed_sharded_modifiers: list[tuple[str, ParallelModifier]] = [
(spec.name, m) for spec in dim_specs for m in reversed(spec.parallel_modifiers)
]
sharded_axes_raw: set[ParallelAxis] = {
m.axis for _, m in reversed_sharded_modifiers
}
all_axes: set[ParallelAxis] = {axis for info in parallel_infos for axis in info}
# axis annotated in dims but absent from all parallel_infos -> axis_size=1, skip
sharded_axes: set[ParallelAxis] = sharded_axes_raw & all_axes
sharded_axis_infos = {
k: v for k, v in sharded_axis_infos.items() if k in sharded_axes
}
reversed_sharded_modifiers = [
(name, m) for name, m in reversed_sharded_modifiers if m.axis in sharded_axes
]
replicated_axes: set[ParallelAxis] = all_axes - sharded_axes
if not sharded_axes and not replicated_axes:
@@ -67,14 +71,15 @@ def compute_unsharder_plan(
(axis, PickParams()) for axis in sorted(replicated_axes, key=lambda a: a.value)
] + [
(
axis,
modifier.axis,
_resolve_unshard_params(
spec=spec,
modifier=modifier,
dim_name=dim_name,
parallel_infos=parallel_infos,
thd_global_seq_lens=thd_global_seq_lens,
),
)
for axis, spec in sharded_axis_infos.items()
for dim_name, modifier in reversed_sharded_modifiers
]
plans: list[UnsharderPlan] = []
@@ -151,23 +156,20 @@ def _group_and_project(
def _resolve_unshard_params(
*,
spec: DimSpec,
modifier: ParallelModifier,
dim_name: str,
parallel_infos: list[dict[ParallelAxis, AxisInfo]],
thd_global_seq_lens: Optional[list[int]] = None,
) -> UnsharderParams:
if spec.reduction is not None:
if modifier.reduction is not None:
return ReduceSumParams()
if (
spec.name == TOKEN_DIM_NAME
and spec.parallel == ParallelAxis.CP
dim_name == TOKEN_DIM_NAME
and modifier.axis == ParallelAxis.CP
and thd_global_seq_lens is not None
):
if spec.parallel is None:
raise ValueError(
f"THD unshard requires a parallel axis on dim '{spec.name}', but got None"
)
axis_size: int = parallel_infos[0][spec.parallel].axis_size
axis_size: int = parallel_infos[0][modifier.axis].axis_size
for s in thd_global_seq_lens:
if s % axis_size != 0:
raise ValueError(
@@ -175,8 +177,6 @@ def _resolve_unshard_params(
f"Sequences must be padded to a multiple of cp_size for CP zigzag."
)
seq_lens_per_rank: list[int] = [s // axis_size for s in thd_global_seq_lens]
return CpThdConcatParams(
dim_name=spec.name, seq_lens_per_rank=seq_lens_per_rank
)
return CpThdConcatParams(dim_name=dim_name, seq_lens_per_rank=seq_lens_per_rank)
return ConcatParams(dim_name=spec.name)
return ConcatParams(dim_name=dim_name)

View File

@@ -1,10 +1,11 @@
import re
from dataclasses import dataclass
from enum import Enum
from typing import Optional
import torch
from sglang.srt.debug_utils.comparator.utils import _FrozenBase
TOKEN_DIM_NAME: str = "t"
BATCH_DIM_NAME: str = "b"
SEQ_DIM_NAME: str = "s"
@@ -33,14 +34,17 @@ class Reduction(Enum):
PARTIAL = "partial"
@dataclass(frozen=True)
class DimSpec:
name: str
parallel: Optional[ParallelAxis] = None
class ParallelModifier(_FrozenBase):
axis: ParallelAxis
ordering: Optional[Ordering] = None
reduction: Optional[Reduction] = None
class DimSpec(_FrozenBase):
name: str
parallel_modifiers: list[ParallelModifier] = []
class _SingletonDimUtil:
"""Utilities for squeeze dims (name="1") and their singleton tensor-name mapping."""
@@ -83,16 +87,60 @@ class _SingletonDimUtil:
_DIM_PATTERN = re.compile(r"^(?P<name>[a-zA-Z_]\w*)(?:\((?P<modifiers>[^)]+)\))?$")
_MODIFIER_FIELDS: list[tuple[type[Enum], str]] = [
(ParallelAxis, "parallel"),
(Ordering, "ordering"),
(Reduction, "reduction"),
]
_AXIS_LOOKUP: dict[str, ParallelAxis] = {m.value: m for m in ParallelAxis}
_QUALIFIER_LOOKUP: dict[str, Ordering | Reduction] = {
**{m.value: m for m in Ordering},
**{m.value: m for m in Reduction},
}
_MODIFIER_LOOKUP: dict[str, tuple[str, Enum]] = {}
for _enum_cls, _field in _MODIFIER_FIELDS:
for _member in _enum_cls:
_MODIFIER_LOOKUP[_member.value] = (_field, _member)
def _parse_modifier_token(modifier_token: str, dim_token: str) -> ParallelModifier:
"""Parse 'sp', 'cp:zigzag', 'tp:partial', or 'cp:zigzag+partial' → ParallelModifier.
Format: ``axis`` or ``axis:qual`` or ``axis:qual+qual``.
Colon separates axis from qualifiers; ``+`` separates multiple qualifiers.
"""
axis_str: str
qualifiers_str: str
if ":" in modifier_token:
axis_str, qualifiers_str = modifier_token.split(":", maxsplit=1)
else:
axis_str, qualifiers_str = modifier_token, ""
axis_str = axis_str.strip()
axis: Optional[ParallelAxis] = _AXIS_LOOKUP.get(axis_str)
if axis is None:
raise ValueError(
f"Unknown axis {axis_str!r} in modifier {modifier_token!r} "
f"of dim spec: {dim_token!r}"
)
ordering: Optional[Ordering] = None
reduction: Optional[Reduction] = None
for q_str in (q.strip() for q in qualifiers_str.split("+") if q.strip()):
qualifier: Optional[Ordering | Reduction] = _QUALIFIER_LOOKUP.get(q_str)
if qualifier is None:
raise ValueError(
f"Unknown qualifier {q_str!r} in modifier "
f"{modifier_token!r} of dim spec: {dim_token!r}"
)
if isinstance(qualifier, Ordering):
if ordering is not None:
raise ValueError(
f"Multiple ordering values in modifier "
f"{modifier_token!r} of dim spec: {dim_token!r}"
)
ordering = qualifier
else:
if reduction is not None:
raise ValueError(
f"Multiple reduction values in modifier "
f"{modifier_token!r} of dim spec: {dim_token!r}"
)
reduction = qualifier
return ParallelModifier(axis=axis, ordering=ordering, reduction=reduction)
def parse_dim(token: str) -> DimSpec:
@@ -103,26 +151,29 @@ def parse_dim(token: str) -> DimSpec:
if match is None:
raise ValueError(f"Invalid dim token: {token!r}")
name = match.group("name")
modifiers_str = match.group("modifiers")
name: str = match.group("name")
modifiers_str: Optional[str] = match.group("modifiers")
if modifiers_str is None:
return DimSpec(name=name)
fields: dict[str, Enum] = {}
for part in (p.strip() for p in modifiers_str.split(",")):
if part not in _MODIFIER_LOOKUP:
raise ValueError(f"Unknown modifier {part!r} in dim spec: {token!r}")
field_name, enum_value = _MODIFIER_LOOKUP[part]
if field_name in fields:
raise ValueError(f"Multiple {field_name} values in dim token: {token!r}")
fields[field_name] = enum_value
modifiers: list[ParallelModifier] = []
seen_axes: set[ParallelAxis] = set()
return DimSpec(name=name, **fields)
for modifier_token in (p.strip() for p in modifiers_str.split(",")):
modifier: ParallelModifier = _parse_modifier_token(modifier_token, token)
if modifier.axis in seen_axes:
raise ValueError(
f"Duplicate axis {modifier.axis.value!r} in dim spec: {token!r}"
)
seen_axes.add(modifier.axis)
modifiers.append(modifier)
return DimSpec(name=name, parallel_modifiers=modifiers)
def parse_dims(dims_str: str) -> list[DimSpec]:
"""Parse 'b s(cp,zigzag) h(tp) d' -> list[DimSpec]."""
"""Parse 'b s(cp:zigzag) h(tp) d' -> list[DimSpec]."""
if not dims_str.strip():
raise ValueError("dims string must not be empty")

View File

@@ -36,9 +36,6 @@ from sglang.srt.debug_utils.comparator.output_types import (
from sglang.srt.debug_utils.comparator.per_token_visualizer import (
generate_per_token_heatmap,
)
from sglang.srt.debug_utils.comparator.per_token_visualizer import (
generate_per_token_heatmap,
)
from sglang.srt.debug_utils.comparator.utils import Pair
from sglang.srt.debug_utils.dump_loader import read_meta, read_tokenizer_path
@@ -258,12 +255,6 @@ def _consume_comparison_records(
return summary, skipped_names
if visualize_per_token is not None and collected_comparisons:
generate_per_token_heatmap(
records=collected_comparisons,
output_path=visualize_per_token,
)
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()

View File

@@ -85,8 +85,8 @@ class TestComputePerStepSubPlans:
def test_zigzag_returns_both_plans(self) -> None:
result: list[AlignerPerStepSubPlan] = compute_per_step_sub_plans(
metas=[
_make_meta(dims="b s(cp,zigzag) h", cp_rank=0, cp_size=2),
_make_meta(dims="b s(cp,zigzag) h", cp_rank=1, cp_size=2),
_make_meta(dims="b s(cp:zigzag) h", cp_rank=0, cp_size=2),
_make_meta(dims="b s(cp:zigzag) h", cp_rank=1, cp_size=2),
]
)
unsharder_plans: list[UnsharderPlan] = [
@@ -177,33 +177,33 @@ class TestComputeAlignerPlan:
class TestComputePerStepSubPlansThd:
def test_thd_zigzag_returns_thd_plans(self) -> None:
"""t(cp,zigzag) h(tp) generates THD-typed unsharder + reorderer plans."""
"""t(cp:zigzag) h(tp) generates THD-typed unsharder + reorderer plans."""
thd_global_seq_lens: list[int] = [100, 64, 92]
result: list[AlignerPerStepSubPlan] = compute_per_step_sub_plans(
metas=[
_make_meta(
dims="t(cp,zigzag) h(tp)",
dims="t(cp:zigzag) h(tp)",
cp_rank=0,
cp_size=2,
tp_rank=0,
tp_size=2,
),
_make_meta(
dims="t(cp,zigzag) h(tp)",
dims="t(cp:zigzag) h(tp)",
cp_rank=0,
cp_size=2,
tp_rank=1,
tp_size=2,
),
_make_meta(
dims="t(cp,zigzag) h(tp)",
dims="t(cp:zigzag) h(tp)",
cp_rank=1,
cp_size=2,
tp_rank=0,
tp_size=2,
),
_make_meta(
dims="t(cp,zigzag) h(tp)",
dims="t(cp:zigzag) h(tp)",
cp_rank=1,
cp_size=2,
tp_rank=1,

View File

@@ -25,8 +25,8 @@ register_cpu_ci(est_time=10, suite="default", nightly=True)
class TestComputeReordererPlans:
def test_compute_reorderer_plans_zigzag(self) -> None:
"""s(cp,zigzag) produces a ReordererPlan."""
dim_specs = parse_dims("b s(cp,zigzag) h(tp)")
"""s(cp:zigzag) produces a ReordererPlan."""
dim_specs = parse_dims("b s(cp:zigzag) h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
@@ -43,8 +43,8 @@ class TestComputeReordererPlans:
assert plans[0].params.cp_size == 2
def test_compute_reorderer_plans_thd_zigzag(self) -> None:
"""t(cp,zigzag) produces a ZigzagToNaturalThdParams plan."""
dim_specs = parse_dims("t(cp,zigzag) h(tp)")
"""t(cp:zigzag) produces a ZigzagToNaturalThdParams plan."""
dim_specs = parse_dims("t(cp:zigzag) h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
@@ -64,8 +64,8 @@ class TestComputeReordererPlans:
assert plans[0].params.seq_lens == [100, 64, 92]
def test_non_seq_dim_still_raises(self) -> None:
"""Zigzag on non-sequence/non-token dim (e.g. h(cp,zigzag)) raises ValueError."""
dim_specs = parse_dims("h(cp,zigzag) d")
"""Zigzag on non-sequence/non-token dim (e.g. h(cp:zigzag)) raises ValueError."""
dim_specs = parse_dims("h(cp:zigzag) d")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2)},
]
@@ -73,8 +73,8 @@ class TestComputeReordererPlans:
compute_reorderer_plans(dim_specs=dim_specs, parallel_infos=parallel_infos)
def test_thd_zigzag_without_seq_lens_raises(self) -> None:
"""t(cp,zigzag) without thd_global_seq_lens raises ValueError."""
dim_specs = parse_dims("t(cp,zigzag) h(tp)")
"""t(cp:zigzag) without thd_global_seq_lens raises ValueError."""
dim_specs = parse_dims("t(cp:zigzag) h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
@@ -85,8 +85,8 @@ class TestComputeReordererPlans:
compute_reorderer_plans(dim_specs=dim_specs, parallel_infos=parallel_infos)
def test_thd_natural_no_reorder(self) -> None:
"""t(cp,natural) and t(cp) produce no reorder plans."""
for dims_str in ["t(cp,natural) h(tp)", "t(cp) h(tp)"]:
"""t(cp:natural) and t(cp) produce no reorder plans."""
for dims_str in ["t(cp:natural) h(tp)", "t(cp) h(tp)"]:
dim_specs = parse_dims(dims_str)
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
@@ -100,8 +100,8 @@ class TestComputeReordererPlans:
assert plans == []
def test_compute_reorderer_plans_natural(self) -> None:
"""s(cp) and s(cp,natural) produce no reorder plans."""
for dims_str in ["b s(cp) h(tp)", "b s(cp,natural) h(tp)"]:
"""s(cp) and s(cp:natural) produce no reorder plans."""
for dims_str in ["b s(cp) h(tp)", "b s(cp:natural) h(tp)"]:
dim_specs = parse_dims(dims_str)
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
@@ -141,7 +141,7 @@ class TestCpZigzagTpE2E:
}
)
dim_specs: list[DimSpec] = parse_dims("b s(cp,zigzag) h(tp)")
dim_specs: list[DimSpec] = parse_dims("b s(cp:zigzag) h(tp)")
dim_names: list[str] = [s.name for s in dim_specs]
unsharder_plans = compute_unsharder_plan(
@@ -166,5 +166,83 @@ class TestCpZigzagTpE2E:
assert torch.allclose(current[0].rename(None), full_tensor)
class TestCpZigzagSpSameDimE2E:
"""E2E test for t(cp:zigzag,sp) — two axes sharding the same token dim."""
def test_cp2_sp2_zigzag_e2e(self) -> None:
"""CP=2 zigzag + SP=2 on same token dim: full unshard + reorder round-trip.
Shard order (outer to inner, matching left-to-right in dims annotation):
1. CP zigzag splits token dim into 2 CP chunks (zigzag order)
2. SP splits each CP chunk into 2 SP chunks
Unshard order (inner to outer, right-to-left):
1. SP concat (inner): merge SP chunks back
2. CP concat (outer): merge CP chunks back
3. Zigzag reorder: restore natural token order
"""
torch.manual_seed(42)
total_tokens: int = 16
hidden: int = 8
full_tensor: torch.Tensor = torch.randn(total_tokens, hidden)
# Step 1: CP zigzag split — split into 2*cp_size=4 natural chunks, reorder by zigzag
cp_size: int = 2
sp_size: int = 2
n_natural_chunks: int = cp_size * 2
natural_chunks: list[torch.Tensor] = list(
full_tensor.chunk(n_natural_chunks, dim=0)
)
zigzag_order: list[int] = [0, 3, 1, 2]
zigzagged: torch.Tensor = torch.cat(
[natural_chunks[i] for i in zigzag_order], dim=0
)
cp_chunks: list[torch.Tensor] = list(zigzagged.chunk(cp_size, dim=0))
# Step 2: SP split within each CP chunk
tensors: list[torch.Tensor] = []
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(cp_size):
sp_chunks: list[torch.Tensor] = list(
cp_chunks[cp_rank].chunk(sp_size, dim=0)
)
for sp_rank in range(sp_size):
tensors.append(sp_chunks[sp_rank])
parallel_infos.append(
{
ParallelAxis.CP: AxisInfo(axis_rank=cp_rank, axis_size=cp_size),
ParallelAxis.SP: AxisInfo(axis_rank=sp_rank, axis_size=sp_size),
}
)
dim_specs: list[DimSpec] = parse_dims("t(cp:zigzag,sp) h")
dim_names: list[str] = [s.name for s in dim_specs]
unsharder_plans = compute_unsharder_plan(
dim_specs=dim_specs, parallel_infos=parallel_infos
)
reorderer_plans = compute_reorderer_plans(
dim_specs=dim_specs,
parallel_infos=parallel_infos,
thd_global_seq_lens=[total_tokens],
)
all_plans = [*unsharder_plans, *reorderer_plans]
assert len(unsharder_plans) == 2 # SP concat, CP concat
assert unsharder_plans[0].axis == ParallelAxis.SP
assert unsharder_plans[1].axis == ParallelAxis.CP
assert len(reorderer_plans) == 1 # zigzag reorder
current: list[torch.Tensor] = [t.refine_names(*dim_names) for t in tensors]
for plan in all_plans:
if isinstance(plan, ReordererPlan):
current = execute_reorderer_plan(plan, current)
else:
current = execute_unsharder_plan(plan, current).tensors
assert len(current) == 1
assert torch.allclose(current[0].rename(None), full_tensor)
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))

View File

@@ -90,7 +90,7 @@ class TestEnsureDimsInMetas:
assert result is metas
def test_cp_sharded_sglang_input_ids_infers_dims(self):
"""CP + input_ids in sglang infers dims 't(cp,zigzag)'."""
"""CP + input_ids in sglang infers dims 't(cp:zigzag)'."""
metas: list[dict] = [
self._make_meta(cp_size=2, cp_rank=0),
self._make_meta(cp_size=2, cp_rank=1),
@@ -99,11 +99,11 @@ class TestEnsureDimsInMetas:
name="input_ids", plugin=_sglang_plugin, metas=metas, ndim=1
)
assert result is not metas
assert result[0]["dims"] == "t(cp,zigzag)"
assert result[1]["dims"] == "t(cp,zigzag)"
assert result[0]["dims"] == "t(cp:zigzag)"
assert result[1]["dims"] == "t(cp:zigzag)"
def test_cp_sharded_sglang_positions_infers_dims(self):
"""CP + positions in sglang infers dims 't(cp,zigzag)'."""
"""CP + positions in sglang infers dims 't(cp:zigzag)'."""
metas: list[dict] = [
self._make_meta(cp_size=2, cp_rank=0),
self._make_meta(cp_size=2, cp_rank=1),
@@ -111,10 +111,10 @@ class TestEnsureDimsInMetas:
result = _ensure_dims_in_metas(
name="positions", plugin=_sglang_plugin, metas=metas, ndim=1
)
assert result[0]["dims"] == "t(cp,zigzag)"
assert result[0]["dims"] == "t(cp:zigzag)"
def test_cp_sharded_megatron_input_ids_infers_dims_1d(self):
"""CP + input_ids in megatron (1D) infers dims 't(cp,zigzag)'."""
"""CP + input_ids in megatron (1D) infers dims 't(cp:zigzag)'."""
metas: list[dict] = [
{"megatron_parallel_info": {"cp_rank": 0, "cp_size": 2}},
{"megatron_parallel_info": {"cp_rank": 1, "cp_size": 2}},
@@ -122,10 +122,10 @@ class TestEnsureDimsInMetas:
result = _ensure_dims_in_metas(
name="input_ids", plugin=_megatron_plugin, metas=metas, ndim=1
)
assert result[0]["dims"] == "t(cp,zigzag)"
assert result[0]["dims"] == "t(cp:zigzag)"
def test_cp_sharded_megatron_input_ids_infers_dims_2d(self):
"""CP + input_ids in megatron (2D) infers dims 'b s(cp,zigzag)'."""
"""CP + input_ids in megatron (2D) infers dims 'b s(cp:zigzag)'."""
metas: list[dict] = [
{"megatron_parallel_info": {"cp_rank": 0, "cp_size": 2}},
{"megatron_parallel_info": {"cp_rank": 1, "cp_size": 2}},
@@ -133,7 +133,7 @@ class TestEnsureDimsInMetas:
result = _ensure_dims_in_metas(
name="input_ids", plugin=_megatron_plugin, metas=metas, ndim=2
)
assert result[0]["dims"] == "b s(cp,zigzag)"
assert result[0]["dims"] == "b s(cp:zigzag)"
def test_cp_non_sharded_name_returns_metas_unchanged(self):
"""CP + non-sharded tensor name (seq_lens) returns metas as-is."""

View File

@@ -218,19 +218,19 @@ class TestInferCpShardedDims:
"""Tests for infer_cp_sharded_dims on each plugin."""
def test_megatron_infer_1d(self) -> None:
"""Megatron 1D → 't(cp,zigzag)'."""
"""Megatron 1D → 't(cp:zigzag)'."""
result: str = _megatron_plugin.infer_cp_sharded_dims(name="input_ids", ndim=1)
assert result == "t(cp,zigzag)"
assert result == "t(cp:zigzag)"
def test_megatron_infer_2d(self) -> None:
"""Megatron 2D → 'b s(cp,zigzag)'."""
"""Megatron 2D → 'b s(cp:zigzag)'."""
result: str = _megatron_plugin.infer_cp_sharded_dims(name="input_ids", ndim=2)
assert result == "b s(cp,zigzag)"
assert result == "b s(cp:zigzag)"
def test_sglang_infer_1d(self) -> None:
"""SGLang 1D → 't(cp,zigzag)'."""
"""SGLang 1D → 't(cp:zigzag)'."""
result: str = _sglang_plugin.infer_cp_sharded_dims(name="input_ids", ndim=1)
assert result == "t(cp,zigzag)"
assert result == "t(cp:zigzag)"
def test_megatron_infer_3d_raises(self) -> None:
"""Megatron 3D raises ValueError."""

View File

@@ -654,7 +654,7 @@ class TestReduceSum:
part_a = full_tensor * 0.6
part_b = full_tensor * 0.4
dim_specs = parse_dims("h(tp,partial) d")
dim_specs = parse_dims("h(tp:partial) d")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=i, axis_size=2)} for i in range(2)
]
@@ -676,7 +676,7 @@ class TestReduceSum:
full_tensor = torch.randn(4, 8)
parts: list[torch.Tensor] = [full_tensor * 0.25 for _ in range(4)]
dim_specs = parse_dims("h(tp,partial) d")
dim_specs = parse_dims("h(tp:partial) d")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=i, axis_size=4)} for i in range(4)
]
@@ -710,7 +710,7 @@ class TestReduceSum:
}
)
dim_specs = parse_dims("b s(cp) h(tp,partial)")
dim_specs = parse_dims("b s(cp) h(tp:partial)")
plans = compute_unsharder_plan(dim_specs, parallel_infos)
assert len(plans) == 2
@@ -739,7 +739,7 @@ class TestReduceSum:
{ParallelAxis.TP: AxisInfo(axis_rank=3, axis_size=4)},
{ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=4)},
]
dim_specs = parse_dims("h(tp,partial) d")
dim_specs = parse_dims("h(tp:partial) d")
plans = compute_unsharder_plan(dim_specs, parallel_infos)
named_parts: list[torch.Tensor] = _name_tensors(parts, dim_specs)
@@ -752,7 +752,7 @@ class TestReduceSum:
def test_reduce_preserves_named_dims(self) -> None:
"""Named tensor dimensions are preserved through reduce_sum."""
dim_specs = parse_dims("h(tp,partial) d")
dim_specs = parse_dims("h(tp:partial) d")
part_a = torch.randn(4, 8).refine_names("h", "d")
part_b = torch.randn(4, 8).refine_names("h", "d")

View File

@@ -175,7 +175,7 @@ class TestComputeUnsharderPlan:
compute_unsharder_plan(dim_specs, parallel_infos)
def test_reduction_partial_returns_reduce_sum(self) -> None:
dim_specs = parse_dims("h(tp,partial)")
dim_specs = parse_dims("h(tp:partial)")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=i, axis_size=2)} for i in range(2)
]
@@ -188,7 +188,7 @@ class TestComputeUnsharderPlan:
def test_reduction_partial_tp4(self) -> None:
"""TP=4 with partial reduction produces a single ReduceSumParams step."""
dim_specs = parse_dims("h(tp,partial)")
dim_specs = parse_dims("h(tp:partial)")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=i, axis_size=4)} for i in range(4)
]
@@ -200,7 +200,7 @@ class TestComputeUnsharderPlan:
def test_multi_axis_with_reduction_on_one(self) -> None:
"""CP concat + TP reduce produces a 2-step plan."""
dim_specs = parse_dims("s(cp) h(tp,partial)")
dim_specs = parse_dims("s(cp) h(tp:partial)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(2):
for tp_rank in range(2):
@@ -221,7 +221,7 @@ class TestComputeUnsharderPlan:
def test_reduction_scrambled_ranks(self) -> None:
"""Scrambled world_rank order with partial reduction."""
dim_specs = parse_dims("h(tp,partial)")
dim_specs = parse_dims("h(tp:partial)")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=2, axis_size=4)},
{ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=4)},
@@ -235,7 +235,7 @@ class TestComputeUnsharderPlan:
assert plans[0].groups == [[1, 3, 0, 2]]
def test_ordering_zigzag_accepted(self) -> None:
dim_specs = parse_dims("s(cp,zigzag)")
dim_specs = parse_dims("s(cp:zigzag)")
parallel_infos = [
{ParallelAxis.CP: AxisInfo(axis_rank=i, axis_size=2)} for i in range(2)
]
@@ -244,7 +244,7 @@ class TestComputeUnsharderPlan:
assert plans[0].axis == ParallelAxis.CP
def test_ordering_natural_accepted(self) -> None:
dim_specs = parse_dims("s(cp,natural)")
dim_specs = parse_dims("s(cp:natural)")
parallel_infos = [
{ParallelAxis.CP: AxisInfo(axis_rank=i, axis_size=2)} for i in range(2)
]
@@ -288,6 +288,77 @@ class TestComputeUnsharderPlan:
assert len(plans[2].groups) == 1
assert len(plans[2].groups[0]) == 2
def test_same_dim_cp_sp_plan(self) -> None:
"""t(cp:zigzag,sp) with CP=2 SP=2: SP unshards first (inner), then CP."""
dim_specs = parse_dims("t(cp:zigzag,sp) 1 h")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(2):
for sp_rank in range(2):
parallel_infos.append(
{
ParallelAxis.CP: AxisInfo(axis_rank=cp_rank, axis_size=2),
ParallelAxis.SP: AxisInfo(axis_rank=sp_rank, axis_size=2),
}
)
plans = compute_unsharder_plan(dim_specs, parallel_infos)
assert len(plans) == 2
# SP unshards first (rightmost modifier = innermost shard)
sp_plan = plans[0]
assert sp_plan.axis == ParallelAxis.SP
assert isinstance(sp_plan.params, ConcatParams)
assert sp_plan.params.dim_name == "t"
assert len(sp_plan.groups) == 2
for group in sp_plan.groups:
assert len(group) == 2
# CP unshards second (leftmost modifier = outermost shard)
cp_plan = plans[1]
assert cp_plan.axis == ParallelAxis.CP
assert isinstance(cp_plan.params, ConcatParams)
assert cp_plan.params.dim_name == "t"
assert len(cp_plan.groups) == 1
assert len(cp_plan.groups[0]) == 2
def test_same_dim_cp_sp_with_thd(self) -> None:
"""t(cp:zigzag,sp) with THD: SP → ConcatParams, CP → CpThdConcatParams."""
from sglang.srt.debug_utils.comparator.aligner.unsharder.types import (
CpThdConcatParams,
)
dim_specs = parse_dims("t(cp:zigzag,sp) h")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(2):
for sp_rank in range(2):
parallel_infos.append(
{
ParallelAxis.CP: AxisInfo(axis_rank=cp_rank, axis_size=2),
ParallelAxis.SP: AxisInfo(axis_rank=sp_rank, axis_size=2),
}
)
thd_global_seq_lens: list[int] = [100, 64]
plans = compute_unsharder_plan(
dim_specs, parallel_infos, thd_global_seq_lens=thd_global_seq_lens
)
assert len(plans) == 2
# SP unshards first: plain concat (SP is not CP, no THD special handling)
sp_plan = plans[0]
assert sp_plan.axis == ParallelAxis.SP
assert isinstance(sp_plan.params, ConcatParams)
assert sp_plan.params.dim_name == "t"
# CP unshards second: THD concat because dim is 't' + axis is CP + thd_global_seq_lens provided
cp_plan = plans[1]
assert cp_plan.axis == ParallelAxis.CP
assert isinstance(cp_plan.params, CpThdConcatParams)
assert cp_plan.params.dim_name == "t"
assert cp_plan.params.seq_lens_per_rank == [50, 32]
def test_sp_in_dims_but_not_in_parallel_info(self) -> None:
"""s(sp) in dims but SP absent from parallel_info (SP disabled), should auto-skip."""
dim_specs = parse_dims("s(sp) b h(tp)")

View File

@@ -11,6 +11,7 @@ from sglang.srt.debug_utils.comparator.dims import (
DimSpec,
Ordering,
ParallelAxis,
ParallelModifier,
Reduction,
_SingletonDimUtil,
apply_dim_names,
@@ -32,48 +33,78 @@ class TestParseDim:
assert parse_dim("b") == DimSpec(name="b")
def test_parallel_axis(self) -> None:
assert parse_dim("h(tp)") == DimSpec(name="h", parallel=ParallelAxis.TP)
assert parse_dim("h(tp)") == DimSpec(
name="h",
parallel_modifiers=[ParallelModifier(axis=ParallelAxis.TP)],
)
def test_all_parallel_axes(self) -> None:
assert parse_dim("a(tp)").parallel == ParallelAxis.TP
assert parse_dim("a(cp)").parallel == ParallelAxis.CP
assert parse_dim("a(ep)").parallel == ParallelAxis.EP
assert parse_dim("a(sp)").parallel == ParallelAxis.SP
assert parse_dim("a(tp)").parallel_modifiers[0].axis == ParallelAxis.TP
assert parse_dim("a(cp)").parallel_modifiers[0].axis == ParallelAxis.CP
assert parse_dim("a(ep)").parallel_modifiers[0].axis == ParallelAxis.EP
assert parse_dim("a(sp)").parallel_modifiers[0].axis == ParallelAxis.SP
def test_ordering(self) -> None:
assert parse_dim("s(cp,zigzag)").ordering == Ordering.ZIGZAG
assert parse_dim("s(cp,natural)").ordering == Ordering.NATURAL
assert (
parse_dim("s(cp:zigzag)").parallel_modifiers[0].ordering == Ordering.ZIGZAG
)
assert (
parse_dim("s(cp:natural)").parallel_modifiers[0].ordering
== Ordering.NATURAL
)
def test_reduction(self) -> None:
assert parse_dim("h(tp,partial)").reduction == Reduction.PARTIAL
def test_all_modifiers(self) -> None:
assert parse_dim("s(cp,zigzag,partial)") == DimSpec(
name="s",
parallel=ParallelAxis.CP,
ordering=Ordering.ZIGZAG,
reduction=Reduction.PARTIAL,
assert (
parse_dim("h(tp:partial)").parallel_modifiers[0].reduction
== Reduction.PARTIAL
)
def test_all_qualifiers(self) -> None:
assert parse_dim("s(cp:zigzag+partial)") == DimSpec(
name="s",
parallel_modifiers=[
ParallelModifier(
axis=ParallelAxis.CP,
ordering=Ordering.ZIGZAG,
reduction=Reduction.PARTIAL,
),
],
)
def test_multi_axis(self) -> None:
result: DimSpec = parse_dim("t(cp:zigzag,sp)")
assert result.name == "t"
assert len(result.parallel_modifiers) == 2
assert result.parallel_modifiers[0] == ParallelModifier(
axis=ParallelAxis.CP, ordering=Ordering.ZIGZAG
)
assert result.parallel_modifiers[1] == ParallelModifier(axis=ParallelAxis.SP)
def test_invalid_token_raises(self) -> None:
with pytest.raises(ValueError, match="Invalid dim token"):
parse_dim("h()")
with pytest.raises(ValueError, match="Invalid dim token"):
parse_dim("h(tp(x))")
def test_unknown_modifier_raises(self) -> None:
with pytest.raises(ValueError, match="Unknown modifier"):
def test_unknown_axis_raises(self) -> None:
with pytest.raises(ValueError, match="Unknown axis"):
parse_dim("h(xyz)")
with pytest.raises(ValueError, match="Unknown modifier"):
parse_dim("h(tp,foobar)")
def test_unknown_qualifier_raises(self) -> None:
with pytest.raises(ValueError, match="Unknown qualifier"):
parse_dim("h(tp:foobar)")
def test_multiple_ordering_raises(self) -> None:
with pytest.raises(ValueError, match="Multiple ordering"):
parse_dim("s(cp,zigzag,natural)")
parse_dim("s(cp:zigzag+natural)")
def test_multiple_reduction_raises(self) -> None:
with pytest.raises(ValueError, match="Multiple reduction"):
parse_dim("h(tp,partial,partial)")
parse_dim("h(tp:partial+partial)")
def test_duplicate_axis_raises(self) -> None:
with pytest.raises(ValueError, match="Duplicate axis"):
parse_dim("h(tp,tp)")
def test_squeeze_dim(self) -> None:
assert parse_dim("1") == DimSpec(name="1")
@@ -96,10 +127,18 @@ class TestParseDims:
assert parse_dims("t") == [DimSpec(name="t")]
def test_mixed_annotated(self) -> None:
assert parse_dims("b s(cp,zigzag) h(tp) d") == [
assert parse_dims("b s(cp:zigzag) h(tp) d") == [
DimSpec(name="b"),
DimSpec(name="s", parallel=ParallelAxis.CP, ordering=Ordering.ZIGZAG),
DimSpec(name="h", parallel=ParallelAxis.TP),
DimSpec(
name="s",
parallel_modifiers=[
ParallelModifier(axis=ParallelAxis.CP, ordering=Ordering.ZIGZAG),
],
),
DimSpec(
name="h",
parallel_modifiers=[ParallelModifier(axis=ParallelAxis.TP)],
),
DimSpec(name="d"),
]
@@ -158,7 +197,7 @@ class TestFindDimIndex:
assert find_dim_index(specs, "d") == 3
def test_with_modifiers(self) -> None:
specs: list[DimSpec] = parse_dims("b s(cp,zigzag) h(tp) d")
specs: list[DimSpec] = parse_dims("b s(cp:zigzag) h(tp) d")
assert find_dim_index(specs, "h") == 2
def test_empty_list(self) -> None:

View File

@@ -837,7 +837,7 @@ class TestEntrypointGroupingLogical:
tp_size=1,
seq_dim=1,
head_dim=2,
dims_str="b s(cp,zigzag) h",
dims_str="b s(cp:zigzag) h",
)
args = _make_args(
@@ -871,7 +871,7 @@ class TestEntrypointGroupingLogical:
tp_size=2,
seq_dim=1,
head_dim=2,
dims_str="b s(cp,zigzag) h(tp)",
dims_str="b s(cp:zigzag) h(tp)",
)
args = _make_args(
@@ -959,14 +959,14 @@ class TestEntrypointGroupingLogical:
full_tensor=full_baseline,
name="attn_out",
tp_size=2,
dims_str="b h(tp,partial)",
dims_str="b h(tp:partial)",
)
target_path = _create_tp_partial_dumps(
target_dir,
full_tensor=full_target,
name="attn_out",
tp_size=2,
dims_str="b h(tp,partial)",
dims_str="b h(tp:partial)",
)
args = _make_args(baseline_path, target_path, diff_threshold=0.01)
@@ -997,7 +997,7 @@ class TestEntrypointGroupingLogical:
full_tensor=target_full,
name="attn_out",
tp_size=2,
dims_str="b h(tp,partial)",
dims_str="b h(tp:partial)",
)
args = _make_args(baseline_path, target_path, diff_threshold=0.01)
@@ -1026,7 +1026,7 @@ class TestEntrypointGroupingLogical:
rank=rank,
name="hidden",
tensor=cp_chunks[cp_rank] / 2,
dims="b s(cp) h(tp,partial)",
dims="b s(cp) h(tp:partial)",
parallel_info={
"cp_rank": cp_rank,
"cp_size": 2,
@@ -1046,6 +1046,38 @@ class TestEntrypointGroupingLogical:
comp = _assert_single_comparison_passed(records)
assert comp.name == "hidden"
def test_cp_zigzag_sp_same_dim_unshard(self, tmp_path, capsys):
"""CP=2 zigzag + SP=2 on same seq dim: multi-axis unshard + reorder."""
torch.manual_seed(42)
full_baseline = torch.randn(4, 8, 6)
full_target = full_baseline + torch.randn(4, 8, 6) * 0.001
baseline_dir = tmp_path / "baseline"
target_dir = tmp_path / "target"
for side_dir, full_tensor in [
(baseline_dir, full_baseline),
(target_dir, full_target),
]:
_create_cp_zigzag_sp_sharded_dumps(
side_dir,
full_tensor=full_tensor,
name="hidden",
cp_size=2,
sp_size=2,
dims_str="b s(cp:zigzag,sp) h",
)
args = _make_args(
baseline_dir / _FIXED_EXP_NAME,
target_dir / _FIXED_EXP_NAME,
diff_threshold=0.01,
)
records, _ = _run_and_parse(args, capsys)
comp = _assert_single_comparison_passed(records)
assert comp.name == "hidden"
class TestEntrypointConcatMode:
"""Test concat token-aligner mode through the full entrypoint pipeline."""
@@ -2578,6 +2610,63 @@ def _create_cp_zigzag_tp_sharded_dumps(
return directory / _FIXED_EXP_NAME
def _create_cp_zigzag_sp_sharded_dumps(
directory: Path,
*,
full_tensor: torch.Tensor,
name: str,
cp_size: int,
sp_size: int,
dims_str: str,
seq_dim: int = 1,
num_steps: int = 1,
) -> Path:
"""Create CP-zigzag + SP sharded dump files for a seq dim (b s h format).
Shard order (outer to inner, matching left-to-right in dims annotation):
1. CP zigzag splits seq dim into cp_size chunks (zigzag order)
2. SP splits each CP chunk into sp_size chunks
"""
num_chunks: int = cp_size * 2
natural_chunks: list[torch.Tensor] = list(
full_tensor.chunk(num_chunks, dim=seq_dim)
)
zigzag_order: list[int] = []
for i in range(cp_size):
zigzag_order.append(i)
zigzag_order.append(num_chunks - 1 - i)
zigzagged: torch.Tensor = torch.cat(
[natural_chunks[idx] for idx in zigzag_order], dim=seq_dim
)
cp_chunks: list[torch.Tensor] = list(zigzagged.chunk(cp_size, dim=seq_dim))
rank: int = 0
for cp_rank in range(cp_size):
sp_chunks: list[torch.Tensor] = list(
cp_chunks[cp_rank].chunk(sp_size, dim=seq_dim)
)
for sp_rank in range(sp_size):
_create_rank_dump(
directory,
rank=rank,
name=name,
tensor=sp_chunks[sp_rank],
dims=dims_str,
parallel_info={
"cp_rank": cp_rank,
"cp_size": cp_size,
"sp_rank": sp_rank,
"sp_size": sp_size,
},
num_steps=num_steps,
)
rank += 1
return directory / _FIXED_EXP_NAME
def _create_replicated_tp_sharded_cp_dumps(
directory: Path,
*,
@@ -2772,7 +2861,7 @@ def _create_thd_cp_zigzag_dumps(
seq_lens: list[int],
cp_size: int,
total_per_rank: int,
dims_str: str = "t(cp,zigzag)",
dims_str: str = "t(cp:zigzag)",
num_steps: int = 1,
) -> Path:
"""Create THD CP-zigzag sharded dump files simulating Megatron forward.
@@ -2981,7 +3070,7 @@ class TestEntrypointThdCpZigzag:
rank=cp_rank,
name="hidden_states",
tensor=rank_hidden,
dims="t(cp,zigzag) h",
dims="t(cp:zigzag) h",
parallel_info={"cp_rank": cp_rank, "cp_size": cp_size},
framework="megatron",
extra_dumps=[

View File

@@ -2348,7 +2348,7 @@ class TestDumperDims:
)
tensor = torch.randn(4, 8, requires_grad=True)
dumper.dump("hidden", tensor, dims="b h(tp)", dims_grad="b h(tp,partial)")
dumper.dump("hidden", tensor, dims="b h(tp)", dims_grad="b h(tp:partial)")
dumper.step()
tensor.backward(torch.ones_like(tensor))
@@ -2362,10 +2362,10 @@ class TestDumperDims:
value_data = torch.load(value_file, weights_only=False)
assert value_data["meta"]["dims"] == "b h(tp)"
assert value_data["meta"]["dims_grad"] == "b h(tp,partial)"
assert value_data["meta"]["dims_grad"] == "b h(tp:partial)"
grad_data = torch.load(grad_file, weights_only=False)
assert grad_data["meta"]["dims"] == "b h(tp,partial)"
assert grad_data["meta"]["dims"] == "b h(tp:partial)"
def test_dims_grad_inherits(self, tmp_path) -> None:
dumper = _Dumper(