Support replication axis in dump comparator (#19282)

This commit is contained in:
fzyzcjy
2026-02-25 09:48:43 +08:00
committed by GitHub
parent 2e2b18e870
commit b7af58b9af
10 changed files with 778 additions and 62 deletions
@@ -2,30 +2,80 @@ import torch
from sglang.srt.debug_utils.comparator.aligner.unshard.types import ( from sglang.srt.debug_utils.comparator.aligner.unshard.types import (
ConcatParams, ConcatParams,
PickParams,
UnshardParams, UnshardParams,
UnshardPlan, UnshardPlan,
) )
from sglang.srt.debug_utils.comparator.dims import ParallelAxis
from sglang.srt.debug_utils.comparator.output_types import (
AlignWarning,
ReplicatedMismatchWarning,
)
def execute_unshard_plan( def execute_unshard_plan(
plan: UnshardPlan, plan: UnshardPlan,
tensors: list[torch.Tensor], tensors: list[torch.Tensor],
) -> list[torch.Tensor]: ) -> tuple[list[torch.Tensor], list[AlignWarning]]:
all_warnings: list[AlignWarning] = []
result: list[torch.Tensor] = [] result: list[torch.Tensor] = []
for group in plan.groups:
for group_idx, group in enumerate(plan.groups):
group_tensors = [tensors[i] for i in group] group_tensors = [tensors[i] for i in group]
result.append(_apply_unshard(plan.params, group_tensors)) tensor, warnings = _apply_unshard(
return result plan.params,
group_tensors,
axis=plan.axis,
group_index=group_idx,
)
result.append(tensor)
all_warnings.extend(warnings)
return result, all_warnings
def _apply_unshard( def _apply_unshard(
params: UnshardParams, ordered_tensors: list[torch.Tensor] params: UnshardParams,
) -> torch.Tensor: ordered_tensors: list[torch.Tensor],
*,
axis: ParallelAxis,
group_index: int,
) -> tuple[torch.Tensor, list[AlignWarning]]:
if isinstance(params, PickParams):
warnings = _verify_replicated_group(
ordered_tensors,
axis=axis,
group_index=group_index,
)
return ordered_tensors[0], warnings
if isinstance(params, ConcatParams): if isinstance(params, ConcatParams):
return _unshard_concat(ordered_tensors, dim=params.dim) return torch.cat(ordered_tensors, dim=params.dim), []
# Phase 2: ReduceSumParams, CpZigzagParams # Phase 2: ReduceSumParams, CpZigzagParams
raise ValueError(f"Unsupported unshard operation: {type(params).__name__}") raise ValueError(f"Unsupported unshard operation: {type(params).__name__}")
def _unshard_concat(tensors: list[torch.Tensor], dim: int) -> torch.Tensor: def _verify_replicated_group(
return torch.cat(tensors, dim=dim) ordered_tensors: list[torch.Tensor],
*,
axis: ParallelAxis,
group_index: int,
) -> list[ReplicatedMismatchWarning]:
warnings: list[ReplicatedMismatchWarning] = []
baseline = ordered_tensors[0]
for i in range(1, len(ordered_tensors)):
other = ordered_tensors[i]
if not torch.allclose(baseline, other, atol=1e-6):
warnings.append(
ReplicatedMismatchWarning(
axis=axis.value,
group_index=group_index,
differing_index=i,
baseline_index=0,
max_abs_diff=(baseline - other).abs().max().item(),
)
)
return warnings
@@ -4,6 +4,7 @@ from typing import NamedTuple
from sglang.srt.debug_utils.comparator.aligner.unshard.types import ( from sglang.srt.debug_utils.comparator.aligner.unshard.types import (
AxisInfo, AxisInfo,
ConcatParams, ConcatParams,
PickParams,
UnshardParams, UnshardParams,
UnshardPlan, UnshardPlan,
) )
@@ -32,31 +33,38 @@ def compute_unshard_plan(
for dim_idx, spec in enumerate(dim_specs) for dim_idx, spec in enumerate(dim_specs)
if spec.parallel is not None if spec.parallel is not None
} }
if not sharded_axis_infos: sharded_axes: set[ParallelAxis] = set(sharded_axis_infos)
all_axes: set[ParallelAxis] = {axis for info in parallel_infos for axis in info}
replicated_axes: set[ParallelAxis] = all_axes - sharded_axes
if not sharded_axes and not replicated_axes:
return [] return []
_validate(sharded_axes=set(sharded_axis_infos), parallel_infos=parallel_infos) _validate(
axes_to_validate=sharded_axes | replicated_axes,
parallel_infos=parallel_infos,
)
current_coords: _CoordsList = [ current_coords: _CoordsList = [
{axis: info[axis].axis_rank for axis in sharded_axis_infos} {axis: info[axis].axis_rank for axis in sharded_axes | replicated_axes}
for info in parallel_infos for info in parallel_infos
] ]
axis_and_params: list[tuple[ParallelAxis, UnshardParams]] = [
(axis, PickParams()) for axis in sorted(replicated_axes, key=lambda a: a.value)
] + [
(axis, _resolve_unshard_params(spec=spec, dim_index=dim_index))
for axis, (dim_index, spec) in sharded_axis_infos.items()
]
plans: list[UnshardPlan] = [] plans: list[UnshardPlan] = []
for axis, (dim_index, spec) in sharded_axis_infos.items(): for axis, params in axis_and_params:
result = _group_and_project( result = _group_and_project(
current_coords=current_coords, current_coords=current_coords,
target_axis=axis, target_axis=axis,
) )
plans.append(UnshardPlan(axis=axis, params=params, groups=result.groups))
plans.append(
UnshardPlan(
axis=axis,
params=_resolve_unshard_params(spec=spec, dim_index=dim_index),
groups=result.groups,
)
)
current_coords = result.projected_coords current_coords = result.projected_coords
return plans return plans
@@ -64,18 +72,18 @@ def compute_unshard_plan(
def _validate( def _validate(
*, *,
sharded_axes: set[ParallelAxis], axes_to_validate: set[ParallelAxis],
parallel_infos: list[dict[ParallelAxis, AxisInfo]], parallel_infos: list[dict[ParallelAxis, AxisInfo]],
) -> None: ) -> None:
"""Check that every rank has all sharded axes, sizes are consistent, and ranks are complete.""" """Check that every rank has all axes, sizes are consistent, and ranks are complete."""
axis_sizes: dict[ParallelAxis, int] = {} axis_sizes: dict[ParallelAxis, int] = {}
for world_rank, parallel_info in enumerate(parallel_infos): for world_rank, parallel_info in enumerate(parallel_infos):
for axis in sharded_axes: for axis in axes_to_validate:
if axis not in parallel_info: if axis not in parallel_info:
raise ValueError( raise ValueError(
f"world_rank={world_rank} missing parallel_info for " f"world_rank={world_rank} missing parallel_info for "
f"sharded axis {axis.value!r}" f"axis {axis.value!r}"
) )
axis_info = parallel_info[axis] axis_info = parallel_info[axis]
@@ -1,6 +1,8 @@
from __future__ import annotations from __future__ import annotations
from typing import Literal from typing import Annotated, Literal, Union
from pydantic import Field
from sglang.srt.debug_utils.comparator.dims import ParallelAxis from sglang.srt.debug_utils.comparator.dims import ParallelAxis
from sglang.srt.debug_utils.comparator.utils import _FrozenBase from sglang.srt.debug_utils.comparator.utils import _FrozenBase
@@ -16,7 +18,14 @@ class ConcatParams(_FrozenBase):
dim: int dim: int
UnshardParams = ConcatParams class PickParams(_FrozenBase):
op: Literal["pick"] = "pick"
UnshardParams = Annotated[
Union[ConcatParams, PickParams],
Field(discriminator="op"),
]
class UnshardPlan(_FrozenBase): class UnshardPlan(_FrozenBase):
@@ -1,7 +1,7 @@
from abc import abstractmethod from abc import abstractmethod
from typing import Annotated, Literal, Union from typing import Annotated, Literal, Union
from pydantic import Discriminator, TypeAdapter from pydantic import Discriminator, Field, TypeAdapter
from sglang.srt.debug_utils.comparator.tensor_comparison.formatter import ( from sglang.srt.debug_utils.comparator.tensor_comparison.formatter import (
format_comparison, format_comparison,
@@ -12,9 +12,38 @@ from sglang.srt.debug_utils.comparator.tensor_comparison.types import (
from sglang.srt.debug_utils.comparator.utils import _StrictBase from sglang.srt.debug_utils.comparator.utils import _StrictBase
class ReplicatedMismatchWarning(_StrictBase):
kind: Literal["replicated_mismatch"] = "replicated_mismatch"
axis: str
group_index: int
differing_index: int
baseline_index: int
max_abs_diff: float
def to_text(self) -> str:
return (
f"Replicated along {self.axis}: group {self.group_index}, "
f"index {self.differing_index} differs from {self.baseline_index} "
f"(max_abs_diff={self.max_abs_diff:.6e})"
)
AlignWarning = (
ReplicatedMismatchWarning # future: Annotated[Union[...], Discriminator("kind")]
)
class _OutputRecord(_StrictBase): class _OutputRecord(_StrictBase):
align_warnings: list[AlignWarning] = Field(default_factory=list)
@abstractmethod @abstractmethod
def to_text(self) -> str: ... def _format_body(self) -> str: ...
def to_text(self) -> str:
body = self._format_body()
if self.align_warnings:
body += "\n" + "\n".join(f" ⚠ {w.to_text()}" for w in self.align_warnings)
return body
class ConfigRecord(_OutputRecord): class ConfigRecord(_OutputRecord):
@@ -25,7 +54,7 @@ class ConfigRecord(_OutputRecord):
start_step: int start_step: int
end_step: int end_step: int
def to_text(self) -> str: def _format_body(self) -> str:
return ( return (
f"Config: baseline={self.baseline_path} target={self.target_path}\n" f"Config: baseline={self.baseline_path} target={self.target_path}\n"
f"diff_threshold={self.diff_threshold} " f"diff_threshold={self.diff_threshold} "
@@ -39,10 +68,12 @@ class SkipRecord(_OutputRecord):
reason: str reason: str
@property @property
def category(self): def category(self) -> str:
if self.align_warnings:
return "failed"
return "skipped" return "skipped"
def to_text(self) -> str: def _format_body(self) -> str:
return f"Skip: {self.name} ({self.reason})" return f"Skip: {self.name} ({self.reason})"
@@ -50,10 +81,12 @@ class ComparisonRecord(TensorComparisonInfo, _OutputRecord):
type: Literal["comparison"] = "comparison" type: Literal["comparison"] = "comparison"
@property @property
def category(self): def category(self) -> str:
if self.align_warnings:
return "failed"
return "passed" if self.diff is not None and self.diff.passed else "failed" return "passed" if self.diff is not None and self.diff.passed else "failed"
def to_text(self) -> str: def _format_body(self) -> str:
return format_comparison(self) return format_comparison(self)
@@ -64,7 +97,7 @@ class SummaryRecord(_OutputRecord):
failed: int failed: int
skipped: int skipped: int
def to_text(self) -> str: def _format_body(self) -> str:
return ( return (
f"Summary: {self.passed} passed, {self.failed} failed, " f"Summary: {self.passed} passed, {self.failed} failed, "
f"{self.skipped} skipped (total {self.total})" f"{self.skipped} skipped (total {self.total})"
@@ -20,6 +20,7 @@ from sglang.srt.debug_utils.comparator.aligner.unshard.planner import (
from sglang.srt.debug_utils.comparator.aligner.unshard.types import UnshardPlan from sglang.srt.debug_utils.comparator.aligner.unshard.types import UnshardPlan
from sglang.srt.debug_utils.comparator.dims import parse_dims from sglang.srt.debug_utils.comparator.dims import parse_dims
from sglang.srt.debug_utils.comparator.output_types import ( from sglang.srt.debug_utils.comparator.output_types import (
AlignWarning,
ComparisonRecord, ComparisonRecord,
SkipRecord, SkipRecord,
) )
@@ -50,12 +51,13 @@ def process_tensor_group(
t_extracted = _extract_tensors(t_tensors) t_extracted = _extract_tensors(t_tensors)
del b_tensors, t_tensors del b_tensors, t_tensors
b_tensor = _execute_plans(b_extracted, b_plans) b_tensor, b_warns = _execute_plans(b_extracted, b_plans)
t_tensor = _execute_plans(t_extracted, t_plans) t_tensor, t_warns = _execute_plans(t_extracted, t_plans)
all_warnings: list[AlignWarning] = b_warns + t_warns
if b_tensor is None or t_tensor is None: if b_tensor is None or t_tensor is None:
reason = "baseline_load_failed" if b_tensor is None else "target_load_failed" reason = "baseline_load_failed" if b_tensor is None else "target_load_failed"
return SkipRecord(name=name, reason=reason) return SkipRecord(name=name, reason=reason, align_warnings=all_warnings)
info = compare_tensors( info = compare_tensors(
x_baseline=b_tensor, x_baseline=b_tensor,
@@ -64,7 +66,7 @@ def process_tensor_group(
diff_threshold=diff_threshold, diff_threshold=diff_threshold,
) )
return ComparisonRecord(**info.model_dump()) return ComparisonRecord(**info.model_dump(), align_warnings=all_warnings)
def _load_tensors(filenames: list[str], base_path: Path) -> list[ValueWithMeta]: def _load_tensors(filenames: list[str], base_path: Path) -> list[ValueWithMeta]:
@@ -112,27 +114,32 @@ def _extract_tensors(
def _execute_plans( def _execute_plans(
tensors: list[torch.Tensor], tensors: list[torch.Tensor],
plans: list[Plan], plans: list[Plan],
) -> Optional[torch.Tensor]: ) -> tuple[Optional[torch.Tensor], list[AlignWarning]]:
if not tensors: if not tensors:
return None return None, []
if not plans: if not plans:
if len(tensors) != 1: if len(tensors) != 1:
return None return None, []
return tensors[0] return tensors[0], []
warnings: list[AlignWarning] = []
current = tensors current = tensors
for plan in plans: for plan in plans:
current = _execute_plan(current, plan) current, new_warnings = _execute_plan(current, plan)
warnings.extend(new_warnings)
assert len(current) == 1 assert len(current) == 1
return current[0] return current[0], warnings
def _execute_plan(tensors, plan): def _execute_plan(
tensors: list[torch.Tensor],
plan: Plan,
) -> tuple[list[torch.Tensor], list[AlignWarning]]:
if isinstance(plan, UnshardPlan): if isinstance(plan, UnshardPlan):
return execute_unshard_plan(plan, tensors) return execute_unshard_plan(plan, tensors)
elif isinstance(plan, ReorderPlan): elif isinstance(plan, ReorderPlan):
return execute_reorder_plan(plan, tensors) return execute_reorder_plan(plan, tensors), []
else: else:
raise NotImplementedError(f"Unknown {plan=}") raise NotImplementedError(f"Unknown {plan=}")
@@ -148,7 +148,7 @@ class TestCpZigzagTpE2E:
if isinstance(plan, ReorderPlan): if isinstance(plan, ReorderPlan):
current = execute_reorder_plan(plan, current) current = execute_reorder_plan(plan, current)
else: else:
current = execute_unshard_plan(plan, current) current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1 assert len(current) == 1
assert torch.allclose(current[0], full_tensor) assert torch.allclose(current[0], full_tensor)
@@ -5,12 +5,16 @@ import torch
from sglang.srt.debug_utils.comparator.aligner.unshard.executor import ( from sglang.srt.debug_utils.comparator.aligner.unshard.executor import (
_apply_unshard, _apply_unshard,
_verify_replicated_group,
execute_unshard_plan, execute_unshard_plan,
) )
from sglang.srt.debug_utils.comparator.aligner.unshard.planner import ( from sglang.srt.debug_utils.comparator.aligner.unshard.planner import (
compute_unshard_plan, compute_unshard_plan,
) )
from sglang.srt.debug_utils.comparator.aligner.unshard.types import AxisInfo from sglang.srt.debug_utils.comparator.aligner.unshard.types import (
AxisInfo,
PickParams,
)
from sglang.srt.debug_utils.comparator.dims import ParallelAxis, parse_dims from sglang.srt.debug_utils.comparator.dims import ParallelAxis, parse_dims
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
@@ -29,9 +33,10 @@ class TestExecuteUnshardPlan:
plans = compute_unshard_plan(dim_specs, parallel_infos) plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 1 assert len(plans) == 1
result = execute_unshard_plan(plans[0], shards) result, warnings = execute_unshard_plan(plans[0], shards)
assert len(result) == 1 assert len(result) == 1
assert torch.allclose(result[0], full_tensor) assert torch.allclose(result[0], full_tensor)
assert warnings == []
def test_scrambled_world_ranks_correct_result(self) -> None: def test_scrambled_world_ranks_correct_result(self) -> None:
full_tensor = torch.randn(4, 8) full_tensor = torch.randn(4, 8)
@@ -54,9 +59,10 @@ class TestExecuteUnshardPlan:
shards[1], # world_rank=3, axis_rank=1 shards[1], # world_rank=3, axis_rank=1
] ]
result = execute_unshard_plan(plans[0], tensors_ordered_by_world_rank) result, warnings = execute_unshard_plan(plans[0], tensors_ordered_by_world_rank)
assert len(result) == 1 assert len(result) == 1
assert torch.allclose(result[0], full_tensor) assert torch.allclose(result[0], full_tensor)
assert warnings == []
def test_single_step_reduces_tensor_count(self) -> None: def test_single_step_reduces_tensor_count(self) -> None:
"""8 tensors with 2 groups of 4 produce 2 output tensors.""" """8 tensors with 2 groups of 4 produce 2 output tensors."""
@@ -85,10 +91,10 @@ class TestExecuteUnshardPlan:
for tp_rank in range(4): for tp_rank in range(4):
tensors.append(source[tp_rank]) tensors.append(source[tp_rank])
intermediate = execute_unshard_plan(plans[0], tensors) intermediate, _ = execute_unshard_plan(plans[0], tensors)
assert len(intermediate) == 4 assert len(intermediate) == 4
final = execute_unshard_plan(plans[1], intermediate) final, _ = execute_unshard_plan(plans[1], intermediate)
assert len(final) == 1 assert len(final) == 1
def test_cp_tp_concat(self) -> None: def test_cp_tp_concat(self) -> None:
@@ -116,7 +122,7 @@ class TestExecuteUnshardPlan:
current = tensors current = tensors
for plan in plans: for plan in plans:
current = execute_unshard_plan(plan, current) current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1 assert len(current) == 1
assert torch.allclose(current[0], full_tensor) assert torch.allclose(current[0], full_tensor)
@@ -157,7 +163,7 @@ class TestExecuteUnshardPlan:
current = tensors current = tensors
for plan in plans: for plan in plans:
current = execute_unshard_plan(plan, current) current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1 assert len(current) == 1
assert torch.allclose(current[0], full_tensor) assert torch.allclose(current[0], full_tensor)
@@ -169,7 +175,12 @@ class TestExecuteUnshardPlan:
pass pass
with pytest.raises(ValueError, match="Unsupported unshard"): with pytest.raises(ValueError, match="Unsupported unshard"):
_apply_unshard(_FakeParams(), [torch.randn(2, 2)]) _apply_unshard(
_FakeParams(),
[torch.randn(2, 2)],
axis=ParallelAxis.TP,
group_index=0,
)
def test_cp_tp_ep_three_axis_concat(self) -> None: def test_cp_tp_ep_three_axis_concat(self) -> None:
"""CP=2 + TP=2 + EP=2: three-step unshard reconstructs original tensor.""" """CP=2 + TP=2 + EP=2: three-step unshard reconstructs original tensor."""
@@ -205,7 +216,7 @@ class TestExecuteUnshardPlan:
current = tensors current = tensors
for plan in plans: for plan in plans:
current = execute_unshard_plan(plan, current) current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1 assert len(current) == 1
assert torch.allclose(current[0], full_tensor) assert torch.allclose(current[0], full_tensor)
@@ -253,11 +264,211 @@ class TestExecuteUnshardPlan:
current = tensors current = tensors
for plan in plans: for plan in plans:
current = execute_unshard_plan(plan, current) current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1 assert len(current) == 1
assert torch.allclose(current[0], full_tensor) assert torch.allclose(current[0], full_tensor)
class TestPickOperation:
def test_pick_single_group(self) -> None:
"""PickParams picks the first tensor from a single group."""
tensor = torch.randn(4, 8)
dim_specs = parse_dims("h d")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2)},
{ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2)},
]
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 1
assert isinstance(plans[0].params, PickParams)
result, warnings = execute_unshard_plan(plans[0], [tensor, tensor.clone()])
assert len(result) == 1
assert torch.allclose(result[0], tensor)
assert warnings == []
def test_pick_multiple_groups(self) -> None:
"""PickParams with multiple groups picks one from each."""
dim_specs = parse_dims("h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
]
plans = compute_unshard_plan(dim_specs, parallel_infos)
pick_plans = [p for p in plans if isinstance(p.params, PickParams)]
assert len(pick_plans) == 1
assert pick_plans[0].axis == ParallelAxis.CP
tensor = torch.randn(4)
tensors = [tensor.clone() for _ in range(4)]
result, warnings = execute_unshard_plan(pick_plans[0], tensors)
assert len(result) == 2
assert warnings == []
def test_replicated_tp_sharded_cp_e2e(self) -> None:
"""CP2 TP2, dims='b s(cp) d': replicated TP pick + sharded CP concat round-trip."""
torch.manual_seed(42)
full_tensor = torch.randn(4, 8, 16)
cp_chunks = list(full_tensor.chunk(2, dim=1))
tensors: list[torch.Tensor] = []
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(2):
for tp_rank in range(2):
tensors.append(cp_chunks[cp_rank].clone())
parallel_infos.append(
{
ParallelAxis.CP: AxisInfo(axis_rank=cp_rank, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=tp_rank, axis_size=2),
}
)
dim_specs = parse_dims("b s(cp) d")
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 2
current = tensors
for plan in plans:
current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1
assert torch.allclose(current[0], full_tensor)
def test_fully_replicated_e2e(self) -> None:
"""CP2 TP2, dims='b h d': fully replicated → 2 pick steps → 1 tensor."""
torch.manual_seed(42)
full_tensor = torch.randn(4, 8, 16)
tensors: list[torch.Tensor] = []
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(2):
for tp_rank in range(2):
tensors.append(full_tensor.clone())
parallel_infos.append(
{
ParallelAxis.CP: AxisInfo(axis_rank=cp_rank, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=tp_rank, axis_size=2),
}
)
dim_specs = parse_dims("b h d")
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 2
assert all(isinstance(p.params, PickParams) for p in plans)
current = tensors
for plan in plans:
current, _ = execute_unshard_plan(plan, current)
assert len(current) == 1
assert torch.allclose(current[0], full_tensor)
class TestVerifyReplicatedGroup:
def test_warns_on_mismatch(self) -> None:
"""_verify_replicated_group produces warning when replicas differ."""
tensor_a = torch.ones(4)
tensor_b = torch.ones(4) + 0.1
warnings = _verify_replicated_group(
[tensor_a, tensor_b],
axis=ParallelAxis.TP,
group_index=0,
)
assert len(warnings) == 1
assert warnings[0].axis == "tp"
assert warnings[0].group_index == 0
assert warnings[0].differing_index == 1
assert warnings[0].baseline_index == 0
assert warnings[0].max_abs_diff == pytest.approx(0.1, abs=1e-5)
def test_no_warn_when_identical(self) -> None:
"""_verify_replicated_group produces no warning for identical replicas."""
tensor = torch.randn(4, 8)
warnings = _verify_replicated_group(
[tensor, tensor.clone()],
axis=ParallelAxis.TP,
group_index=0,
)
assert warnings == []
def test_multiple_mismatches(self) -> None:
"""_verify_replicated_group reports each differing replica."""
baseline = torch.zeros(4)
other_a = torch.ones(4)
other_b = torch.ones(4) * 2
warnings = _verify_replicated_group(
[baseline, other_a, other_b],
axis=ParallelAxis.CP,
group_index=1,
)
assert len(warnings) == 2
assert warnings[0].differing_index == 1
assert warnings[1].differing_index == 2
assert warnings[1].max_abs_diff == pytest.approx(2.0, abs=1e-5)
def test_execute_returns_warnings(self) -> None:
"""execute_unshard_plan returns warnings for replicated mismatch."""
dim_specs = parse_dims("h d")
parallel_infos = [
{ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2)},
{ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2)},
]
plans = compute_unshard_plan(dim_specs, parallel_infos)
tensor_a = torch.zeros(4)
tensor_b = torch.ones(4)
result, warnings = execute_unshard_plan(plans[0], [tensor_a, tensor_b])
assert len(result) == 1
assert len(warnings) == 1
assert torch.allclose(result[0], tensor_a)
def test_atol_boundary_within(self) -> None:
"""Difference exactly at atol (1e-6) → torch.allclose passes → no warning."""
baseline = torch.zeros(4)
other = torch.full((4,), 1e-6)
warnings = _verify_replicated_group(
[baseline, other],
axis=ParallelAxis.TP,
group_index=0,
)
assert warnings == []
def test_atol_boundary_exceeded(self) -> None:
"""Difference just above atol (1e-6 + 1e-9) → torch.allclose fails → warning."""
baseline = torch.zeros(4)
other = torch.full((4,), 1e-6 + 1e-9)
warnings = _verify_replicated_group(
[baseline, other],
axis=ParallelAxis.TP,
group_index=0,
)
assert len(warnings) == 1
assert warnings[0].differing_index == 1
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(pytest.main([__file__])) sys.exit(pytest.main([__file__]))
@@ -5,7 +5,11 @@ import pytest
from sglang.srt.debug_utils.comparator.aligner.unshard.planner import ( from sglang.srt.debug_utils.comparator.aligner.unshard.planner import (
compute_unshard_plan, compute_unshard_plan,
) )
from sglang.srt.debug_utils.comparator.aligner.unshard.types import AxisInfo from sglang.srt.debug_utils.comparator.aligner.unshard.types import (
AxisInfo,
ConcatParams,
PickParams,
)
from sglang.srt.debug_utils.comparator.dims import ParallelAxis, parse_dims from sglang.srt.debug_utils.comparator.dims import ParallelAxis, parse_dims
from sglang.test.ci.ci_register import register_cpu_ci from sglang.test.ci.ci_register import register_cpu_ci
@@ -228,7 +232,7 @@ class TestComputeUnshardPlan:
assert len(plans[2].groups) == 1 assert len(plans[2].groups) == 1
assert len(plans[2].groups[0]) == 2 assert len(plans[2].groups[0]) == 2
def test_replicated_axis_raises(self) -> None: def test_sharded_axis_missing_from_rank_raises(self) -> None:
"""A world_rank missing a sharded axis raises ValueError.""" """A world_rank missing a sharded axis raises ValueError."""
dim_specs = parse_dims("s(cp) h(tp)") dim_specs = parse_dims("s(cp) h(tp)")
parallel_infos = [ parallel_infos = [
@@ -238,7 +242,159 @@ class TestComputeUnshardPlan:
}, },
{ {
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2), ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
# missing TP — replicated # missing TP — sharded axis absent from rank
},
]
with pytest.raises(ValueError, match="missing parallel_info"):
compute_unshard_plan(dim_specs, parallel_infos)
class TestReplicatedAxes:
def test_replicated_tp_with_sharded_cp(self) -> None:
"""CP2 TP2, dims='b s(cp) d' → PickPlan(TP) + ConcatPlan(CP)."""
dim_specs = parse_dims("b s(cp) d")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
]
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 2
assert plans[0].axis == ParallelAxis.TP
assert isinstance(plans[0].params, PickParams)
assert len(plans[0].groups) == 2
for group in plans[0].groups:
assert len(group) == 2
assert plans[1].axis == ParallelAxis.CP
assert isinstance(plans[1].params, ConcatParams)
assert plans[1].params.dim == 1
def test_fully_replicated(self) -> None:
"""CP2 TP2, dims='b h d' → PickPlan(CP) + PickPlan(TP)."""
dim_specs = parse_dims("b h d")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
]
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 2
assert all(isinstance(p.params, PickParams) for p in plans)
axes = {p.axis for p in plans}
assert axes == {ParallelAxis.CP, ParallelAxis.TP}
def test_multiple_replicated_one_sharded(self) -> None:
"""CP2 TP2 EP2, dims='h(tp)' → PickPlan(CP) + PickPlan(EP) + ConcatPlan(TP)."""
dim_specs = parse_dims("h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = []
for cp_rank in range(2):
for ep_rank in range(2):
for tp_rank in range(2):
parallel_infos.append(
{
ParallelAxis.CP: AxisInfo(axis_rank=cp_rank, axis_size=2),
ParallelAxis.EP: AxisInfo(axis_rank=ep_rank, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=tp_rank, axis_size=2),
}
)
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 3
pick_plans = [p for p in plans if isinstance(p.params, PickParams)]
concat_plans = [p for p in plans if isinstance(p.params, ConcatParams)]
assert len(pick_plans) == 2
assert len(concat_plans) == 1
assert concat_plans[0].axis == ParallelAxis.TP
replicated_axes = {p.axis for p in pick_plans}
assert replicated_axes == {ParallelAxis.CP, ParallelAxis.EP}
def test_replicated_scrambled_ranks(self) -> None:
"""Scrambled world_rank order with replicated axis."""
dim_specs = parse_dims("h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=1, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
]
plans = compute_unshard_plan(dim_specs, parallel_infos)
assert len(plans) == 2
assert plans[0].axis == ParallelAxis.CP
assert isinstance(plans[0].params, PickParams)
assert plans[1].axis == ParallelAxis.TP
assert isinstance(plans[1].params, ConcatParams)
def test_replicated_axis_inconsistent_size_raises(self) -> None:
"""Replicated axis with inconsistent sizes raises ValueError."""
dim_specs = parse_dims("h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=4),
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
},
]
with pytest.raises(ValueError, match="Inconsistent axis_size"):
compute_unshard_plan(dim_specs, parallel_infos)
def test_replicated_axis_missing_from_rank_raises(self) -> None:
"""A rank missing a replicated axis that other ranks have raises ValueError."""
dim_specs = parse_dims("h(tp)")
parallel_infos: list[dict[ParallelAxis, AxisInfo]] = [
{
ParallelAxis.CP: AxisInfo(axis_rank=0, axis_size=2),
ParallelAxis.TP: AxisInfo(axis_rank=0, axis_size=2),
},
{
# missing CP — replicated axis absent from this rank
ParallelAxis.TP: AxisInfo(axis_rank=1, axis_size=2),
}, },
] ]
with pytest.raises(ValueError, match="missing parallel_info"): with pytest.raises(ValueError, match="missing parallel_info"):
@@ -6,6 +6,7 @@ import pytest
from sglang.srt.debug_utils.comparator.output_types import ( from sglang.srt.debug_utils.comparator.output_types import (
ComparisonRecord, ComparisonRecord,
ConfigRecord, ConfigRecord,
ReplicatedMismatchWarning,
SkipRecord, SkipRecord,
SummaryRecord, SummaryRecord,
parse_record_json, parse_record_json,
@@ -118,5 +119,71 @@ class TestRecordTypes:
assert restored == record assert restored == record
def _make_warning(**overrides) -> ReplicatedMismatchWarning:
defaults: dict = dict(
axis="tp",
group_index=0,
differing_index=1,
baseline_index=0,
max_abs_diff=0.1,
)
defaults.update(overrides)
return ReplicatedMismatchWarning(**defaults)
class TestAlignWarnings:
def test_comparison_record_failed_when_diff_passed_but_warnings(self):
"""ComparisonRecord with diff.passed=True but align_warnings → category=='failed'."""
record = ComparisonRecord(
name="hidden",
baseline=_make_tensor_info(),
target=_make_tensor_info(),
unified_shape=[4, 8],
shape_mismatch=False,
diff=_make_diff(passed=True),
align_warnings=[_make_warning()],
)
assert record.category == "failed"
def test_skip_record_failed_when_warnings(self):
"""SkipRecord with align_warnings → category=='failed' instead of 'skipped'."""
record = SkipRecord(
name="x",
reason="no_baseline",
align_warnings=[_make_warning()],
)
assert record.category == "failed"
def test_align_warnings_json_round_trip(self):
"""align_warnings survive model_dump_json → parse_record_json round-trip."""
warning = _make_warning(
axis="cp",
group_index=2,
differing_index=3,
baseline_index=0,
max_abs_diff=0.42,
)
record = ComparisonRecord(
name="mlp",
baseline=_make_tensor_info(),
target=_make_tensor_info(),
unified_shape=[4, 8],
shape_mismatch=False,
diff=_make_diff(),
align_warnings=[warning],
)
restored = parse_record_json(record.model_dump_json())
assert isinstance(restored, ComparisonRecord)
assert len(restored.align_warnings) == 1
restored_warning = restored.align_warnings[0]
assert restored_warning.axis == "cp"
assert restored_warning.group_index == 2
assert restored_warning.differing_index == 3
assert restored_warning.baseline_index == 0
assert restored_warning.max_abs_diff == pytest.approx(0.42)
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(pytest.main([__file__])) sys.exit(pytest.main([__file__]))
@@ -878,6 +878,138 @@ class TestEntrypointGroupingLogical:
assert comp.name == "hidden" assert comp.name == "hidden"
class TestEntrypointReplicatedAxis:
"""Test replicated-axis scenarios through the full entrypoint pipeline."""
def test_replicated_axis_identical_replicas_passed(self, tmp_path, capsys):
"""CP2 TP2, TP replicated and identical → passed, no align_warnings."""
torch.manual_seed(42)
full_baseline = torch.randn(4, 8, 6)
full_target = full_baseline + torch.randn(4, 8, 6) * 0.0001
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_replicated_tp_sharded_cp_dumps(
side_dir,
full_tensor=full_tensor,
name="attn_out",
cp_size=2,
tp_size=2,
seq_dim=1,
dims_str="b s(cp) d",
)
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.align_warnings == []
summary = records[-1]
assert isinstance(summary, SummaryRecord)
assert summary.passed == 1
def test_replicated_mismatch_fails(self, tmp_path, capsys):
"""CP2 TP2, TP replicas differ (> atol) → failed with align_warnings."""
torch.manual_seed(42)
full_baseline = torch.randn(4, 8, 6)
full_target = full_baseline + torch.randn(4, 8, 6) * 0.0001
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_replicated_tp_sharded_cp_dumps(
side_dir,
full_tensor=full_tensor,
name="attn_out",
cp_size=2,
tp_size=2,
seq_dim=1,
dims_str="b s(cp) d",
tp_noise=0.5,
)
args = _make_args(
baseline_dir / _FIXED_EXP_NAME,
target_dir / _FIXED_EXP_NAME,
diff_threshold=0.01,
)
records = _run_and_parse(args, capsys)
comparisons = _get_comparisons(records)
assert len(comparisons) == 1
assert comparisons[0].category == "failed"
assert len(comparisons[0].align_warnings) > 0
summary = records[-1]
assert isinstance(summary, SummaryRecord)
assert summary.failed == 1
def test_summary_counts_failed_from_align_warnings_only(self, tmp_path, capsys):
"""Diff itself passes but TP replicas differ → summary.failed=1 from align_warnings."""
torch.manual_seed(42)
full_baseline = torch.randn(4, 8, 6)
full_target = full_baseline + torch.randn(4, 8, 6) * 0.0001
baseline_dir = tmp_path / "baseline"
target_dir = tmp_path / "target"
_create_replicated_tp_sharded_cp_dumps(
baseline_dir,
full_tensor=full_baseline,
name="attn_out",
cp_size=2,
tp_size=2,
seq_dim=1,
dims_str="b s(cp) d",
tp_noise=0.5,
)
_create_replicated_tp_sharded_cp_dumps(
target_dir,
full_tensor=full_target,
name="attn_out",
cp_size=2,
tp_size=2,
seq_dim=1,
dims_str="b s(cp) d",
tp_noise=0.5,
)
args = _make_args(
baseline_dir / _FIXED_EXP_NAME,
target_dir / _FIXED_EXP_NAME,
diff_threshold=0.5,
)
records = _run_and_parse(args, capsys)
comparisons = _get_comparisons(records)
assert len(comparisons) == 1
comp = comparisons[0]
assert comp.diff is not None
assert comp.diff.passed
assert len(comp.align_warnings) > 0
assert comp.category == "failed"
summary = records[-1]
assert isinstance(summary, SummaryRecord)
assert summary.failed == 1
assert summary.passed == 0
# --------------------------- Assertion helpers ------------------- # --------------------------- Assertion helpers -------------------
@@ -1138,6 +1270,49 @@ def _create_cp_zigzag_tp_sharded_dumps(
return directory / _FIXED_EXP_NAME return directory / _FIXED_EXP_NAME
def _create_replicated_tp_sharded_cp_dumps(
directory: Path,
*,
full_tensor: torch.Tensor,
name: str,
cp_size: int,
tp_size: int,
seq_dim: int,
dims_str: str,
tp_noise: float = 0.0,
) -> Path:
"""Create CP-sharded + TP-replicated dump files from a full tensor.
CP direction: chunks along seq_dim (sharded).
TP direction: clones (replicated), with optional noise to simulate mismatch.
"""
cp_chunks: list[torch.Tensor] = list(full_tensor.chunk(cp_size, dim=seq_dim))
rank: int = 0
for cp_rank in range(cp_size):
for tp_rank in range(tp_size):
shard = cp_chunks[cp_rank].clone()
if tp_noise > 0 and tp_rank > 0:
shard = shard + torch.randn_like(shard) * tp_noise
_create_rank_dump(
directory,
rank=rank,
name=name,
tensor=shard,
dims=dims_str,
parallel_info={
"cp_rank": cp_rank,
"cp_size": cp_size,
"tp_rank": tp_rank,
"tp_size": tp_size,
},
)
rank += 1
return directory / _FIXED_EXP_NAME
def _create_tp_sharded_dumps( def _create_tp_sharded_dumps(
directory: Path, directory: Path,
*, *,