Support replication axis in dump comparator (#19282)
This commit is contained in:
@@ -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,
|
||||||
*,
|
*,
|
||||||
|
|||||||
Reference in New Issue
Block a user