Support singleton dimension squeezing in dump comparator (#19566)

This commit is contained in:
fzyzcjy
2026-02-28 18:11:46 +08:00
committed by GitHub
parent 80bbd30909
commit 5705e02d28
16 changed files with 841 additions and 26 deletions
@@ -0,0 +1,109 @@
from __future__ import annotations
from typing import Optional
import torch
from einops import rearrange
from sglang.srt.debug_utils.comparator.dims import (
_SingletonDimUtil,
parse_dims,
)
from sglang.srt.debug_utils.comparator.utils import Pair, _FrozenBase
from sglang.srt.debug_utils.comparator.warning_sink import warning_sink
# --- types ---
class AxisAlignerPlan(_FrozenBase):
pattern: Pair[Optional[str]] # einops pattern per side, None = no-op
# --- planner ---
def compute_axis_aligner_plan(
dims_str_pair: Pair[Optional[str]],
) -> Optional[AxisAlignerPlan]:
if dims_str_pair.x is None or dims_str_pair.y is None:
return None
# Pair[str] after None check
dims_pair: Pair[str] = Pair(x=dims_str_pair.x, y=dims_str_pair.y)
raw_names: Pair[list[str]] = dims_pair.map(
lambda s: [spec.name for spec in parse_dims(s)]
)
filtered_names: Pair[list[str]] = dims_pair.map(
lambda s: [spec.name for spec in _SingletonDimUtil.filter_out(parse_dims(s))]
)
target_order: Optional[list[str]] = _resolve_target_order(
x_names=filtered_names.x, y_names=filtered_names.y
)
if target_order is None:
return None
pattern: Pair[Optional[str]] = raw_names.map(
lambda names: _build_pattern(source=names, target=target_order)
)
if pattern.x is None and pattern.y is None:
return None
return AxisAlignerPlan(pattern=pattern)
def _resolve_target_order(
x_names: list[str], y_names: list[str]
) -> Optional[list[str]]:
"""Determine the canonical dim order both sides should align to.
Returns y_names (the target ordering) if name sets match, or None on mismatch.
If both sides are identical, returns the shared order.
"""
if x_names == y_names:
return y_names
if set(x_names) != set(y_names):
# Local import to avoid circular dependency:
# output_types -> aligner/entrypoint/types -> axis_aligner -> output_types
from sglang.srt.debug_utils.comparator.output_types import GeneralWarning
warning_sink.add(
GeneralWarning(
category="axis_aligner_dim_mismatch",
message=(
f"AxisAligner: dim name sets differ (x={x_names}, y={y_names}), "
f"skipping axis swap"
),
)
)
return None
return y_names
def _build_pattern(*, source: list[str], target: list[str]) -> Optional[str]:
"""Build an einops rearrange pattern from source dim names to target dim names.
Returns None if source already matches target (no rearrange needed).
"""
if source == target:
return None
return f"{' '.join(source)} -> {' '.join(target)}"
# --- executor ---
def execute_axis_aligner_plan(
tensor: torch.Tensor, plan: AxisAlignerPlan, *, side: str
) -> torch.Tensor:
pattern: Optional[str] = plan.pattern.x if side == "x" else plan.pattern.y
if pattern is not None:
tensor = rearrange(tensor.rename(None), pattern)
return tensor
@@ -5,8 +5,8 @@ from typing import Optional
import torch
from sglang.srt.debug_utils.comparator.aligner.axis_swapper import (
execute_axis_swapper_plan,
from sglang.srt.debug_utils.comparator.aligner.axis_aligner import (
execute_axis_aligner_plan,
)
from sglang.srt.debug_utils.comparator.aligner.entrypoint.types import (
AlignerPerStepPlan,
@@ -65,11 +65,11 @@ def execute_aligner_plan(
y=list(step_tensors_y.values())[0],
)
# Cross-side: axis swap (rearrange x to match y's dim order)
if (swap_plan := plan.axis_swapper_plan) is not None:
# Cross-side: axis alignment (squeeze singletons + rearrange dim order)
if (aligner_plan := plan.axis_aligner_plan) is not None:
combined = Pair(
x=execute_axis_swapper_plan(tensor=combined.x, plan=swap_plan),
y=combined.y,
x=execute_axis_aligner_plan(tensor=combined.x, plan=aligner_plan, side="x"),
y=execute_axis_aligner_plan(tensor=combined.y, plan=aligner_plan, side="y"),
)
return AlignerResult(tensors=combined, failed_side_xy=None)
@@ -2,9 +2,9 @@ from __future__ import annotations
from typing import Any, Optional
from sglang.srt.debug_utils.comparator.aligner.axis_swapper import (
AxisSwapperPlan,
compute_axis_swapper_plan,
from sglang.srt.debug_utils.comparator.aligner.axis_aligner import (
AxisAlignerPlan,
compute_axis_aligner_plan,
)
from sglang.srt.debug_utils.comparator.aligner.entrypoint.types import (
AlignerPerStepPlan,
@@ -23,7 +23,11 @@ from sglang.srt.debug_utils.comparator.aligner.unsharder.parallel_info import (
from sglang.srt.debug_utils.comparator.aligner.unsharder.planner import (
compute_unsharder_plan,
)
from sglang.srt.debug_utils.comparator.dims import DimSpec, parse_dims
from sglang.srt.debug_utils.comparator.dims import (
DimSpec,
_SingletonDimUtil,
parse_dims,
)
from sglang.srt.debug_utils.comparator.utils import Pair
@@ -38,7 +42,7 @@ def compute_aligner_plan(
dims_str_pair: Pair[Optional[str]] = metas_pair.map(
lambda metas: metas[0].get("dims") if metas else None
)
axis_swapper_plan: Optional[AxisSwapperPlan] = compute_axis_swapper_plan(
axis_aligner_plan: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
dims_str_pair=dims_str_pair
)
@@ -54,7 +58,7 @@ def compute_aligner_plan(
),
),
token_aligner_plan=token_aligner_plan,
axis_swapper_plan=axis_swapper_plan,
axis_aligner_plan=axis_aligner_plan,
)
@@ -100,7 +104,7 @@ def compute_per_step_sub_plans(
if dims_str is None:
return []
dim_specs: list[DimSpec] = parse_dims(dims_str)
dim_specs: list[DimSpec] = _SingletonDimUtil.filter_out(parse_dims(dims_str))
parallel_infos = [normalize_parallel_info(meta) for meta in metas]
unsharder_plans = compute_unsharder_plan(
@@ -4,7 +4,7 @@ from typing import Annotated, Optional, Union
from pydantic import Discriminator
from sglang.srt.debug_utils.comparator.aligner.axis_swapper import AxisSwapperPlan
from sglang.srt.debug_utils.comparator.aligner.axis_aligner import AxisAlignerPlan
from sglang.srt.debug_utils.comparator.aligner.reorderer.types import ReordererPlan
from sglang.srt.debug_utils.comparator.aligner.token_aligner.types import (
TokenAlignerPlan,
@@ -27,4 +27,4 @@ class AlignerPerStepPlan(_FrozenBase):
class AlignerPlan(_FrozenBase):
per_step_plans: Pair[list[AlignerPerStepPlan]]
token_aligner_plan: Optional[TokenAlignerPlan] = None
axis_swapper_plan: Optional[AxisSwapperPlan] = None
axis_aligner_plan: Optional[AxisAlignerPlan] = None
@@ -28,7 +28,7 @@ from sglang.srt.debug_utils.comparator.dims import (
ParallelAxis,
TokenLayout,
apply_dim_names,
parse_dim_names,
resolve_dim_names,
)
from sglang.srt.debug_utils.comparator.output_types import GeneralWarning
from sglang.srt.debug_utils.comparator.warning_sink import warning_sink
@@ -230,7 +230,7 @@ def _load_and_align_aux_tensor(
if sub_plans:
dims_str: Optional[str] = metas[0].get("dims")
if dims_str is not None:
dim_names: list[str] = parse_dim_names(dims_str)
dim_names: list[str] = resolve_dim_names(dims_str)
tensors = [apply_dim_names(t, dim_names) for t in tensors]
result = execute_sub_plans(tensors=tensors, plans=sub_plans)
@@ -18,7 +18,10 @@ from sglang.srt.debug_utils.comparator.aligner.entrypoint.types import AlignerPl
from sglang.srt.debug_utils.comparator.aligner.token_aligner.types import (
TokenAlignerPlan,
)
from sglang.srt.debug_utils.comparator.dims import apply_dim_names, parse_dim_names
from sglang.srt.debug_utils.comparator.dims import (
apply_dim_names,
resolve_dim_names,
)
from sglang.srt.debug_utils.comparator.output_types import (
ComparisonRecord,
GeneralWarning,
@@ -241,7 +244,7 @@ def _apply_dim_names_from_meta(
if dims_str is None:
return tensors
dim_names: list[str] = parse_dim_names(dims_str)
dim_names: list[str] = resolve_dim_names(dims_str)
return [apply_dim_names(t, dim_names) for t in tensors]
@@ -8,6 +8,7 @@ import torch
TOKEN_DIM_NAME: str = "t"
BATCH_DIM_NAME: str = "b"
SEQ_DIM_NAME: str = "s"
SQUEEZE_DIM_NAME: str = "1"
class TokenLayout(Enum):
@@ -40,6 +41,46 @@ class DimSpec:
reduction: Optional[Reduction] = None
class _SingletonDimUtil:
"""Utilities for squeeze dims (name="1") and their singleton tensor-name mapping."""
PREFIX: str = "singleton"
@staticmethod
def is_squeeze(spec: DimSpec) -> bool:
return spec.name == SQUEEZE_DIM_NAME
@staticmethod
def filter_out(dim_specs: list[DimSpec]) -> list[DimSpec]:
return [s for s in dim_specs if not _SingletonDimUtil.is_squeeze(s)]
@staticmethod
def make_name(index: int) -> str:
return f"{_SingletonDimUtil.PREFIX}{index}"
@staticmethod
def is_singleton_name(name: str) -> bool:
return (
name.startswith(_SingletonDimUtil.PREFIX)
and name[len(_SingletonDimUtil.PREFIX) :].isdigit()
)
@staticmethod
def sanitize_names(names: list[str]) -> list[str]:
"""Replace '1' with 'singleton0', 'singleton1', ... for named tensor compatibility."""
result: list[str] = []
sq_idx: int = 0
for name in names:
if name == SQUEEZE_DIM_NAME:
result.append(_SingletonDimUtil.make_name(sq_idx))
sq_idx += 1
else:
result.append(name)
return result
_DIM_PATTERN = re.compile(r"^(?P<name>[a-zA-Z_]\w*)(?:\((?P<modifiers>[^)]+)\))?$")
_MODIFIER_FIELDS: list[tuple[type[Enum], str]] = [
@@ -55,6 +96,9 @@ for _enum_cls, _field in _MODIFIER_FIELDS:
def parse_dim(token: str) -> DimSpec:
if token == SQUEEZE_DIM_NAME:
return DimSpec(name=SQUEEZE_DIM_NAME)
match = _DIM_PATTERN.match(token)
if match is None:
raise ValueError(f"Invalid dim token: {token!r}")
@@ -84,16 +128,22 @@ def parse_dims(dims_str: str) -> list[DimSpec]:
result = [parse_dim(token) for token in dims_str.strip().split()]
names = [spec.name for spec in result]
if len(names) != len(set(names)):
duplicates = sorted({n for n in names if names.count(n) > 1})
non_squeeze_names: list[str] = [
spec.name for spec in result if not _SingletonDimUtil.is_squeeze(spec)
]
if len(non_squeeze_names) != len(set(non_squeeze_names)):
duplicates = sorted(
{n for n in non_squeeze_names if non_squeeze_names.count(n) > 1}
)
raise ValueError(f"Duplicate dim names: {duplicates}")
return result
def parse_dim_names(dims_str: str) -> list[str]:
return [spec.name for spec in parse_dims(dims_str)]
def resolve_dim_names(dims_str: str) -> list[str]:
"""Parse dims string and return tensor-compatible names ('1''singleton0', ...)."""
names: list[str] = [spec.name for spec in parse_dims(dims_str)]
return _SingletonDimUtil.sanitize_names(names)
def find_dim_index(dim_specs: list[DimSpec], name: str) -> Optional[int]:
@@ -219,8 +219,13 @@ def _format_aligner_plan(plan: AlignerPlan) -> str:
num_tokens: int = len(plan.token_aligner_plan.locators.x.steps)
lines.append(f" token_aligner: {num_tokens} tokens aligned")
if plan.axis_swapper_plan is not None:
lines.append(f" axis_swapper: {plan.axis_swapper_plan.pattern}")
if plan.axis_aligner_plan is not None:
parts: list[str] = []
if plan.axis_aligner_plan.pattern.x:
parts.append(f"x: {plan.axis_aligner_plan.pattern.x}")
if plan.axis_aligner_plan.pattern.y:
parts.append(f"y: {plan.axis_aligner_plan.pattern.y}")
lines.append(f" axis_aligner: {', '.join(parts)}")
return "\n".join(lines)
@@ -0,0 +1,165 @@
import sys
from typing import Optional
import pytest
import torch
from sglang.srt.debug_utils.comparator.aligner.axis_aligner import (
AxisAlignerPlan,
compute_axis_aligner_plan,
execute_axis_aligner_plan,
)
from sglang.srt.debug_utils.comparator.utils import Pair
from sglang.srt.debug_utils.comparator.warning_sink import warning_sink
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=15, suite="default", nightly=True)
class TestComputeAxisAlignerPlan:
def test_no_dims_returns_none(self) -> None:
assert compute_axis_aligner_plan(Pair(x=None, y=None)) is None
assert compute_axis_aligner_plan(Pair(x="t h d", y=None)) is None
assert compute_axis_aligner_plan(Pair(x=None, y="t h d")) is None
def test_same_order_returns_none(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t h d", y="t h d")
)
assert result is None
def test_different_order(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t h d", y="t d h")
)
assert result is not None
assert result.pattern.x == "t h d -> t d h"
assert result.pattern.y is None
def test_name_mismatch_returns_none_with_warning(self) -> None:
with warning_sink.context() as warnings:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t h d", y="t h e")
)
assert result is None
assert len(warnings) == 1
assert warnings[0].category == "axis_aligner_dim_mismatch"
assert "dim name sets differ" in warnings[0].message
def test_modifiers_ignored_for_name_extraction(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t h(tp) d", y="t d h(tp)")
)
assert result is not None
assert result.pattern.x == "t h d -> t d h"
def test_squeeze_only_no_swap(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t 1 h", y="t h")
)
assert result is not None
assert result.pattern.x == "t 1 h -> t h"
assert result.pattern.y is None
def test_squeeze_both_sides(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t 1 h", y="1 t h")
)
assert result is not None
assert result.pattern.x == "t 1 h -> t h"
assert result.pattern.y == "1 t h -> t h"
def test_squeeze_plus_swap(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t 1 h d", y="t d h")
)
assert result is not None
assert result.pattern.x == "t 1 h d -> t d h"
assert result.pattern.y is None
def test_squeeze_y_only(self) -> None:
result: Optional[AxisAlignerPlan] = compute_axis_aligner_plan(
Pair(x="t h", y="t 1 h")
)
assert result is not None
assert result.pattern.x is None
assert result.pattern.y == "t 1 h -> t h"
class TestExecuteAxisAlignerPlan:
def test_rearrange(self) -> None:
torch.manual_seed(42)
tensor: torch.Tensor = torch.randn(4, 8, 16).refine_names("t", "h", "d")
plan = AxisAlignerPlan(
pattern=Pair(x="t h d -> t d h", y=None),
)
result: torch.Tensor = execute_axis_aligner_plan(
tensor=tensor, plan=plan, side="x"
)
assert result.shape == (4, 16, 8)
for i in range(4):
assert torch.equal(
result[i],
tensor.rename(None)[i].T,
)
def test_execute_squeeze(self) -> None:
torch.manual_seed(42)
tensor: torch.Tensor = torch.randn(4, 1, 8).refine_names("t", "singleton0", "h")
plan = AxisAlignerPlan(
pattern=Pair(x="t 1 h -> t h", y=None),
)
result: torch.Tensor = execute_axis_aligner_plan(
tensor=tensor, plan=plan, side="x"
)
assert result.shape == (4, 8)
def test_execute_squeeze_then_swap(self) -> None:
torch.manual_seed(42)
tensor: torch.Tensor = torch.randn(4, 1, 8, 16).refine_names(
"t", "singleton0", "h", "d"
)
plan = AxisAlignerPlan(
pattern=Pair(x="t 1 h d -> t d h", y=None),
)
result: torch.Tensor = execute_axis_aligner_plan(
tensor=tensor, plan=plan, side="x"
)
assert result.shape == (4, 16, 8)
def test_execute_y_side(self) -> None:
torch.manual_seed(42)
tensor: torch.Tensor = torch.randn(4, 1, 8).refine_names("t", "singleton0", "h")
plan = AxisAlignerPlan(
pattern=Pair(x=None, y="t 1 h -> t h"),
)
result: torch.Tensor = execute_axis_aligner_plan(
tensor=tensor, plan=plan, side="y"
)
assert result.shape == (4, 8)
def test_noop_side(self) -> None:
torch.manual_seed(42)
tensor: torch.Tensor = torch.randn(4, 8, 16).refine_names("t", "h", "d")
plan = AxisAlignerPlan(
pattern=Pair(x="t h d -> t d h", y=None),
)
result: torch.Tensor = execute_axis_aligner_plan(
tensor=tensor, plan=plan, side="y"
)
assert result.shape == (4, 8, 16)
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))
@@ -639,5 +639,119 @@ class TestThdCpConcat:
)
class TestThdCpConcat:
def test_single_seq(self) -> None:
"""Single seq THD unshard: 2 ranks → per-seq concat."""
rank0 = torch.tensor([1, 2, 3]).refine_names("t")
rank1 = torch.tensor([4, 5, 6]).refine_names("t")
plan = UnsharderPlan(
axis=ParallelAxis.CP,
params=CpThdConcatParams(dim_name="t", seq_lens_per_rank=[3]),
groups=[[0, 1]],
)
with warning_sink.context():
result = execute_unsharder_plan(plan, [rank0, rank1])
assert len(result) == 1
expected = torch.tensor([1, 2, 3, 4, 5, 6])
assert torch.equal(result[0].rename(None), expected)
def test_multi_seq(self) -> None:
"""Multi-seq THD unshard: 2 ranks, seq_lens=[50, 32, 46]."""
# rank0: [seqA_r0(50) | seqB_r0(32) | pad_r0(46)]
# rank1: [seqA_r1(50) | seqB_r1(32) | pad_r1(46)]
seq_a_r0 = torch.arange(0, 50)
seq_b_r0 = torch.arange(100, 132)
pad_r0 = torch.full((46,), -1)
rank0 = torch.cat([seq_a_r0, seq_b_r0, pad_r0]).refine_names("t")
seq_a_r1 = torch.arange(50, 100)
seq_b_r1 = torch.arange(132, 164)
pad_r1 = torch.full((46,), -2)
rank1 = torch.cat([seq_a_r1, seq_b_r1, pad_r1]).refine_names("t")
plan = UnsharderPlan(
axis=ParallelAxis.CP,
params=CpThdConcatParams(dim_name="t", seq_lens_per_rank=[50, 32, 46]),
groups=[[0, 1]],
)
with warning_sink.context():
result = execute_unsharder_plan(plan, [rank0, rank1])
assert len(result) == 1
unsharded: torch.Tensor = result[0].rename(None)
# seqA: r0(50) + r1(50) = 100 tokens, values 0..99
assert torch.equal(unsharded[:100], torch.cat([seq_a_r0, seq_a_r1]))
# seqB: r0(32) + r1(32) = 64 tokens
assert torch.equal(unsharded[100:164], torch.cat([seq_b_r0, seq_b_r1]))
# pad: r0(46) + r1(46) = 92 tokens
assert torch.equal(unsharded[164:256], torch.cat([pad_r0, pad_r1]))
def test_with_hidden_dim(self) -> None:
"""THD unshard with trailing hidden dim: shape [T, H]."""
torch.manual_seed(42)
hidden: int = 4
# rank0: [seqA_r0(3, 4) | seqB_r0(2, 4)]
# rank1: [seqA_r1(3, 4) | seqB_r1(2, 4)]
seq_a_r0 = torch.randn(3, hidden)
seq_b_r0 = torch.randn(2, hidden)
rank0 = torch.cat([seq_a_r0, seq_b_r0]).refine_names("t", "h")
seq_a_r1 = torch.randn(3, hidden)
seq_b_r1 = torch.randn(2, hidden)
rank1 = torch.cat([seq_a_r1, seq_b_r1]).refine_names("t", "h")
plan = UnsharderPlan(
axis=ParallelAxis.CP,
params=CpThdConcatParams(dim_name="t", seq_lens_per_rank=[3, 2]),
groups=[[0, 1]],
)
with warning_sink.context():
result = execute_unsharder_plan(plan, [rank0, rank1])
assert len(result) == 1
unsharded: torch.Tensor = result[0].rename(None)
assert unsharded.shape == (10, hidden)
assert torch.equal(unsharded[:6], torch.cat([seq_a_r0, seq_a_r1]))
assert torch.equal(unsharded[6:10], torch.cat([seq_b_r0, seq_b_r1]))
def test_with_leading_batch_dim(self) -> None:
"""THD unshard with leading batch dim: shape [B, T, H], t is dim=1."""
torch.manual_seed(42)
batch: int = 2
hidden: int = 4
# rank0: [seqA_r0(3) | seqB_r0(2)] per batch item
# rank1: [seqA_r1(3) | seqB_r1(2)] per batch item
seq_a_r0 = torch.randn(batch, 3, hidden)
seq_b_r0 = torch.randn(batch, 2, hidden)
rank0 = torch.cat([seq_a_r0, seq_b_r0], dim=1).refine_names("b", "t", "h")
seq_a_r1 = torch.randn(batch, 3, hidden)
seq_b_r1 = torch.randn(batch, 2, hidden)
rank1 = torch.cat([seq_a_r1, seq_b_r1], dim=1).refine_names("b", "t", "h")
plan = UnsharderPlan(
axis=ParallelAxis.CP,
params=CpThdConcatParams(dim_name="t", seq_lens_per_rank=[3, 2]),
groups=[[0, 1]],
)
with warning_sink.context():
result = execute_unsharder_plan(plan, [rank0, rank1])
assert len(result) == 1
unsharded: torch.Tensor = result[0].rename(None)
assert unsharded.shape == (batch, 10, hidden)
# seqA: r0(3) + r1(3) = 6 tokens per batch
assert torch.equal(unsharded[:, :6, :], torch.cat([seq_a_r0, seq_a_r1], dim=1))
# seqB: r0(2) + r1(2) = 4 tokens per batch
assert torch.equal(
unsharded[:, 6:10, :], torch.cat([seq_b_r0, seq_b_r1], dim=1)
)
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))
@@ -6,17 +6,20 @@ import torch
from sglang.srt.debug_utils.comparator.dims import (
BATCH_DIM_NAME,
SEQ_DIM_NAME,
SQUEEZE_DIM_NAME,
TOKEN_DIM_NAME,
DimSpec,
Ordering,
ParallelAxis,
Reduction,
_SingletonDimUtil,
apply_dim_names,
find_dim_index,
parse_dim,
parse_dim_names,
parse_dims,
resolve_dim_by_name,
resolve_dim_names,
strip_dim_names,
)
from sglang.test.ci.ci_register import register_cpu_ci
@@ -72,6 +75,13 @@ class TestParseDim:
with pytest.raises(ValueError, match="Multiple reduction"):
parse_dim("h(tp,partial,partial)")
def test_squeeze_dim(self) -> None:
assert parse_dim("1") == DimSpec(name="1")
def test_squeeze_dim_rejects_modifiers(self) -> None:
with pytest.raises(ValueError, match="Invalid dim token"):
parse_dim("1(tp)")
class TestParseDims:
def test_multi_dims(self) -> None:
@@ -105,6 +115,169 @@ class TestParseDims:
with pytest.raises(ValueError, match="Duplicate"):
parse_dims("h h")
def test_with_squeeze_dims(self) -> None:
result: list[DimSpec] = parse_dims("t 1 h")
assert len(result) == 3
assert result[0] == DimSpec(name="t")
assert result[1] == DimSpec(name="1")
assert result[2] == DimSpec(name="h")
def test_multiple_squeeze_dims_no_duplicate_error(self) -> None:
result: list[DimSpec] = parse_dims("t 1 h 1 d")
assert len(result) == 5
assert result[1] == DimSpec(name="1")
assert result[3] == DimSpec(name="1")
class TestDimConstants:
def test_token_dim_name(self) -> None:
assert TOKEN_DIM_NAME == "t"
def test_batch_dim_name(self) -> None:
assert BATCH_DIM_NAME == "b"
def test_seq_dim_name(self) -> None:
assert SEQ_DIM_NAME == "s"
class TestFindDimIndex:
def test_found(self) -> None:
specs: list[DimSpec] = parse_dims("b s h d")
assert find_dim_index(specs, "s") == 1
def test_not_found(self) -> None:
specs: list[DimSpec] = parse_dims("b s h d")
assert find_dim_index(specs, "t") is None
def test_first_dim(self) -> None:
specs: list[DimSpec] = parse_dims("t h d")
assert find_dim_index(specs, "t") == 0
def test_last_dim(self) -> None:
specs: list[DimSpec] = parse_dims("b s h d")
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")
assert find_dim_index(specs, "h") == 2
def test_empty_list(self) -> None:
assert find_dim_index([], "t") is None
class TestResolveDimByName:
def test_resolve_found(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3, 4).refine_names("b", "s", "h")
assert resolve_dim_by_name(tensor, "b") == 0
assert resolve_dim_by_name(tensor, "s") == 1
assert resolve_dim_by_name(tensor, "h") == 2
def test_resolve_not_found_raises(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3).refine_names("b", "s")
with pytest.raises(ValueError, match="not in tensor names"):
resolve_dim_by_name(tensor, "h")
def test_resolve_unnamed_raises(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3)
with pytest.raises(ValueError, match="no names"):
resolve_dim_by_name(tensor, "b")
class TestApplyDimNames:
def test_apply(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3, 4)
named: torch.Tensor = apply_dim_names(tensor, ["b", "s", "h"])
assert named.names == ("b", "s", "h")
assert named.shape == (2, 3, 4)
def test_apply_preserves_data(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3)
named: torch.Tensor = apply_dim_names(tensor, ["x", "y"])
assert torch.equal(strip_dim_names(named), tensor)
class TestStripDimNames:
def test_strip(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3).refine_names("a", "b")
stripped: torch.Tensor = strip_dim_names(tensor)
assert stripped.names == (None, None)
def test_strip_already_unnamed(self) -> None:
tensor: torch.Tensor = torch.randn(2, 3)
stripped: torch.Tensor = strip_dim_names(tensor)
assert stripped.names == (None, None)
class TestResolveDimNames:
def test_no_squeeze(self) -> None:
assert resolve_dim_names("t h d") == ["t", "h", "d"]
def test_single_squeeze(self) -> None:
assert resolve_dim_names("t 1 h") == ["t", "singleton0", "h"]
def test_multiple_squeeze(self) -> None:
assert resolve_dim_names("1 t 1 h") == [
"singleton0",
"t",
"singleton1",
"h",
]
class TestSingletonDimUtilFilterOut:
def test_no_squeeze(self) -> None:
specs: list[DimSpec] = parse_dims("t h d")
assert _SingletonDimUtil.filter_out(specs) == specs
def test_with_squeeze(self) -> None:
specs: list[DimSpec] = parse_dims("t 1 h")
filtered: list[DimSpec] = _SingletonDimUtil.filter_out(specs)
assert len(filtered) == 2
assert filtered[0].name == "t"
assert filtered[1].name == "h"
def test_all_squeeze(self) -> None:
specs: list[DimSpec] = parse_dims("1 1")
assert _SingletonDimUtil.filter_out(specs) == []
class TestSingletonDimUtilIsSqueeze:
def test_squeeze(self) -> None:
assert _SingletonDimUtil.is_squeeze(DimSpec(name=SQUEEZE_DIM_NAME)) is True
def test_non_squeeze(self) -> None:
assert _SingletonDimUtil.is_squeeze(DimSpec(name="t")) is False
class TestSingletonDimUtilMakeName:
def test_indices(self) -> None:
assert _SingletonDimUtil.make_name(0) == "singleton0"
assert _SingletonDimUtil.make_name(1) == "singleton1"
assert _SingletonDimUtil.make_name(99) == "singleton99"
class TestSingletonDimUtilSanitizeNames:
def test_no_squeeze(self) -> None:
assert _SingletonDimUtil.sanitize_names(["t", "h", "d"]) == ["t", "h", "d"]
def test_single_squeeze(self) -> None:
assert _SingletonDimUtil.sanitize_names(["t", "1", "h"]) == [
"t",
"singleton0",
"h",
]
def test_multiple_squeeze(self) -> None:
assert _SingletonDimUtil.sanitize_names(["1", "t", "1", "h"]) == [
"singleton0",
"t",
"singleton1",
"h",
]
def test_empty(self) -> None:
assert _SingletonDimUtil.sanitize_names([]) == []
class TestParseDimNames:
def test_plain(self) -> None:
@@ -945,6 +945,115 @@ class TestEntrypointGroupingLogical:
assert len(recompute_warnings) > 0
class TestEntrypointAxisAligner:
"""Test cross-framework dim reordering through the full entrypoint pipeline."""
def test_axis_swap_different_dim_order(self, tmp_path, capsys):
"""Baseline dims 'b h d' vs target dims 'b d h': axis swapper rearranges baseline to match."""
torch.manual_seed(42)
full_tensor = torch.randn(4, 8, 16)
baseline_dir = tmp_path / "baseline"
target_dir = tmp_path / "target"
_create_rank_dump(
baseline_dir,
rank=0,
name="hidden",
tensor=full_tensor,
dims="b h d",
)
_create_rank_dump(
target_dir,
rank=0,
name="hidden",
tensor=full_tensor.permute(0, 2, 1).contiguous(),
dims="b d h",
)
args = _make_args(
baseline_dir / _FIXED_EXP_NAME,
target_dir / _FIXED_EXP_NAME,
diff_threshold=1e-3,
)
records = _run_and_parse(args, capsys)
comp = _assert_single_comparison_passed(records)
assert comp.name == "hidden"
assert comp.baseline.shape == [4, 16, 8]
assert comp.target.shape == [4, 16, 8]
def test_axis_swap_with_tp_unshard(self, tmp_path, capsys):
"""Baseline TP=2 with dims 'b h(tp) d' vs target TP=2 with dims 'b d h(tp)': unshard + axis swap."""
torch.manual_seed(42)
full_tensor = torch.randn(4, 8, 16)
baseline_dir = tmp_path / "baseline"
target_dir = tmp_path / "target"
_create_tp_sharded_dumps(
baseline_dir,
full_tensor=full_tensor,
name="hidden",
tp_size=2,
shard_dim=1,
dims_str="b h(tp) d",
)
_create_tp_sharded_dumps(
target_dir,
full_tensor=full_tensor.permute(0, 2, 1).contiguous(),
name="hidden",
tp_size=2,
shard_dim=2,
dims_str="b d h(tp)",
)
args = _make_args(
baseline_dir / _FIXED_EXP_NAME,
target_dir / _FIXED_EXP_NAME,
diff_threshold=1e-3,
)
records = _run_and_parse(args, capsys)
comp = _assert_single_comparison_passed(records)
assert comp.name == "hidden"
def test_squeeze_dim_one_side(self, tmp_path, capsys):
"""SGLang dims 't h' vs Megatron dims 't 1 h': axis aligner squeezes the singleton dim."""
torch.manual_seed(42)
full_tensor = torch.randn(4, 8)
baseline_dir = tmp_path / "baseline"
target_dir = tmp_path / "target"
_create_rank_dump(
baseline_dir,
rank=0,
name="hidden",
tensor=full_tensor,
dims="t h",
)
_create_rank_dump(
target_dir,
rank=0,
name="hidden",
tensor=full_tensor.unsqueeze(1),
dims="t 1 h",
)
args = _make_args(
baseline_dir / _FIXED_EXP_NAME,
target_dir / _FIXED_EXP_NAME,
diff_threshold=1e-3,
)
records = _run_and_parse(args, capsys)
comp = _assert_single_comparison_passed(records)
assert comp.name == "hidden"
assert comp.baseline.shape == [4, 8]
assert comp.target.shape == [4, 8]
class TestEntrypointAxisSwapper:
"""Test cross-framework dim reordering through the full entrypoint pipeline."""
@@ -403,5 +403,79 @@ class TestAlignerPlanInComparisonRecord:
assert "unsharder" in text
def _make_aligner_plan() -> AlignerPlan:
unsharder = UnsharderPlan(
axis=ParallelAxis.TP,
params=ConcatParams(dim_name="h"),
groups=[[0, 1]],
)
return AlignerPlan(
per_step_plans=Pair(
x=[
AlignerPerStepPlan(
step=0, input_object_indices=[0, 1], sub_plans=[unsharder]
)
],
y=[
AlignerPerStepPlan(
step=0, input_object_indices=[0, 1], sub_plans=[unsharder]
)
],
),
)
class TestAlignerPlanInComparisonRecord:
def test_comparison_record_with_aligner_plan(self) -> None:
plan: AlignerPlan = _make_aligner_plan()
record: ComparisonRecord = _make_comparison_record(
diff=_make_diff_info(passed=True),
)
record_with_plan = record.model_copy(update={"aligner_plan": plan})
assert record_with_plan.aligner_plan is not None
assert record_with_plan.aligner_plan.per_step_plans.x[0].step == 0
def test_aligner_plan_json_roundtrip(self) -> None:
plan: AlignerPlan = _make_aligner_plan()
record: ComparisonRecord = _make_comparison_record(
diff=_make_diff_info(passed=True),
)
record_with_plan = record.model_copy(update={"aligner_plan": plan})
json_str: str = record_with_plan.model_dump_json()
parsed = json.loads(json_str)
assert "aligner_plan" in parsed
assert (
parsed["aligner_plan"]["per_step_plans"]["x"][0]["sub_plans"][0]["type"]
== "unsharder"
)
roundtripped: ComparisonRecord = parse_record_json(json_str)
assert roundtripped.aligner_plan is not None
assert (
roundtripped.aligner_plan.per_step_plans.x[0].sub_plans[0].type
== "unsharder"
)
def test_comparison_record_without_aligner_plan(self) -> None:
record: ComparisonRecord = _make_comparison_record(
diff=_make_diff_info(passed=True),
)
json_str: str = record.model_dump_json()
roundtripped: ComparisonRecord = parse_record_json(json_str)
assert roundtripped.aligner_plan is None
def test_aligner_plan_text_format(self) -> None:
plan: AlignerPlan = _make_aligner_plan()
record: ComparisonRecord = _make_comparison_record(
diff=_make_diff_info(passed=True),
)
record_with_plan = record.model_copy(update={"aligner_plan": plan})
text: str = record_with_plan.to_text()
assert "Aligner Plan:" in text
assert "unsharder" in text
if __name__ == "__main__":
sys.exit(pytest.main([__file__]))
@@ -8,6 +8,9 @@ from sglang.srt.debug_utils.source_patcher.code_patcher import (
patch_function,
)
from sglang.srt.debug_utils.source_patcher.types import EditSpec, PatchSpec
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=10, suite="default")
SAMPLE_MODULE_NAME = "_source_patcher_test_fixtures.sample_module"
@@ -6,6 +6,9 @@ from types import ModuleType
import yaml
from sglang.srt.debug_utils.dumper import DumperConfig, _Dumper
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=10, suite="default")
SAMPLE_MODULE_NAME = "_source_patcher_test_fixtures.sample_module"
@@ -3,6 +3,9 @@ from pydantic import ValidationError
from sglang.srt.debug_utils.source_patcher.source_editor import apply_edits
from sglang.srt.debug_utils.source_patcher.types import EditSpec, PatchApplicationError
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=10, suite="default")
class TestApplyEdits: