[AMD] Fix no-op dtype cast in _topk_ids_logical_to_physical_dynamic on HIP (#28074)
Signed-off-by: Rita Brugarolas Brufau <rita.brugarolasbrufau@amd.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ff1fc1fbdf
commit
d6d06cdc17
@@ -111,6 +111,7 @@ def _topk_ids_logical_to_physical_dynamic(
|
|||||||
topk_ids: torch.Tensor, info: Optional[ExpertLocationDispatchInfo]
|
topk_ids: torch.Tensor, info: Optional[ExpertLocationDispatchInfo]
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
topk_ids_original_shape = topk_ids.shape
|
topk_ids_original_shape = topk_ids.shape
|
||||||
|
original_dtype = topk_ids.dtype
|
||||||
device = topk_ids.device
|
device = topk_ids.device
|
||||||
topk_ids = topk_ids.flatten()
|
topk_ids = topk_ids.flatten()
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ def _topk_ids_logical_to_physical_dynamic(
|
|||||||
)
|
)
|
||||||
topk_ids = info.partial_logical_to_all_physical_map[topk_ids, chosen_dispatch_index]
|
topk_ids = info.partial_logical_to_all_physical_map[topk_ids, chosen_dispatch_index]
|
||||||
if _is_hip:
|
if _is_hip:
|
||||||
topk_ids = topk_ids.to(topk_ids.dtype)
|
topk_ids = topk_ids.to(original_dtype)
|
||||||
|
|
||||||
topk_ids = topk_ids.view(topk_ids_original_shape)
|
topk_ids = topk_ids.view(topk_ids_original_shape)
|
||||||
return topk_ids
|
return topk_ids
|
||||||
|
|||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""Unit tests for dtype preservation in topk_ids_logical_to_physical dispatch.
|
||||||
|
|
||||||
|
Verifies that _topk_ids_logical_to_physical_static and
|
||||||
|
_topk_ids_logical_to_physical_dynamic preserve the input topk_ids dtype
|
||||||
|
even when the dispatch map uses a wider dtype (int64). This matters for
|
||||||
|
backends like MORI that read expert indices via raw data_ptr() assuming
|
||||||
|
a specific dtype (int32).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=3, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.eplb.expert_location_dispatch import (
|
||||||
|
ExpertLocationDispatchInfo,
|
||||||
|
_topk_ids_logical_to_physical_dynamic,
|
||||||
|
_topk_ids_logical_to_physical_static,
|
||||||
|
)
|
||||||
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
|
NUM_LOGICAL = 256
|
||||||
|
NUM_PHYSICAL = 256
|
||||||
|
|
||||||
|
|
||||||
|
def _make_identity_info():
|
||||||
|
"""Create dispatch info with an int64 identity map (logical == physical)."""
|
||||||
|
dispatch_map = torch.arange(NUM_LOGICAL, dtype=torch.int64)
|
||||||
|
all_physical_map = torch.arange(NUM_LOGICAL, dtype=torch.int64).unsqueeze(1)
|
||||||
|
num_valid = torch.ones(NUM_LOGICAL, dtype=torch.int64)
|
||||||
|
return ExpertLocationDispatchInfo(
|
||||||
|
ep_dispatch_algorithm="static",
|
||||||
|
partial_logical_to_rank_dispatch_physical_map=dispatch_map,
|
||||||
|
partial_logical_to_all_physical_map=all_physical_map,
|
||||||
|
partial_logical_to_all_physical_map_num_valid=num_valid,
|
||||||
|
num_physical_experts=NUM_PHYSICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_permuted_info():
|
||||||
|
"""Create dispatch info with a non-trivial int64 permutation."""
|
||||||
|
perm = torch.randperm(NUM_LOGICAL, dtype=torch.int64)
|
||||||
|
all_physical_map = perm.unsqueeze(1)
|
||||||
|
num_valid = torch.ones(NUM_LOGICAL, dtype=torch.int64)
|
||||||
|
return ExpertLocationDispatchInfo(
|
||||||
|
ep_dispatch_algorithm="static",
|
||||||
|
partial_logical_to_rank_dispatch_physical_map=perm,
|
||||||
|
partial_logical_to_all_physical_map=all_physical_map,
|
||||||
|
partial_logical_to_all_physical_map_num_valid=num_valid,
|
||||||
|
num_physical_experts=NUM_PHYSICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestStaticDispatchDtype(CustomTestCase):
|
||||||
|
"""Tests for _topk_ids_logical_to_physical_static dtype preservation."""
|
||||||
|
|
||||||
|
def test_preserves_int32_dtype_on_hip(self):
|
||||||
|
"""int32 input must produce int32 output when dispatch map is int64."""
|
||||||
|
info = _make_identity_info()
|
||||||
|
topk_ids = torch.tensor([5, 103, 206], dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_static(topk_ids, info)
|
||||||
|
self.assertEqual(result.dtype, torch.int32)
|
||||||
|
|
||||||
|
def test_preserves_int64_dtype(self):
|
||||||
|
"""int64 input with int64 map should stay int64."""
|
||||||
|
info = _make_identity_info()
|
||||||
|
topk_ids = torch.tensor([5, 103, 206], dtype=torch.int64)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_static(topk_ids, info)
|
||||||
|
self.assertEqual(result.dtype, torch.int64)
|
||||||
|
|
||||||
|
def test_values_correct_after_cast(self):
|
||||||
|
"""Remapped values are correct, not corrupted by the dtype cast."""
|
||||||
|
info = _make_permuted_info()
|
||||||
|
topk_ids = torch.tensor([0, 1, 2, 127, 255], dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_static(topk_ids, info)
|
||||||
|
expected = info.partial_logical_to_rank_dispatch_physical_map[topk_ids.long()]
|
||||||
|
self.assertTrue(torch.equal(result, expected.to(torch.int32)))
|
||||||
|
|
||||||
|
def test_batch_shape_preserved(self):
|
||||||
|
"""2-D input shape is preserved through the remap."""
|
||||||
|
info = _make_identity_info()
|
||||||
|
topk_ids = torch.randint(0, NUM_LOGICAL, (32, 8), dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_static(topk_ids, info)
|
||||||
|
self.assertEqual(result.shape, (32, 8))
|
||||||
|
self.assertEqual(result.dtype, torch.int32)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDynamicDispatchDtype(CustomTestCase):
|
||||||
|
"""Tests for _topk_ids_logical_to_physical_dynamic dtype preservation."""
|
||||||
|
|
||||||
|
def test_preserves_int32_dtype_on_hip(self):
|
||||||
|
"""int32 input must produce int32 output when dispatch map is int64."""
|
||||||
|
info = _make_permuted_info()
|
||||||
|
info.ep_dispatch_algorithm = "dynamic"
|
||||||
|
topk_ids = torch.tensor([5, 103, 206], dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_dynamic(topk_ids, info)
|
||||||
|
self.assertEqual(result.dtype, torch.int32)
|
||||||
|
|
||||||
|
def test_preserves_int64_dtype(self):
|
||||||
|
"""int64 input with int64 map should stay int64."""
|
||||||
|
info = _make_permuted_info()
|
||||||
|
info.ep_dispatch_algorithm = "dynamic"
|
||||||
|
topk_ids = torch.tensor([5, 103, 206], dtype=torch.int64)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_dynamic(topk_ids, info)
|
||||||
|
self.assertEqual(result.dtype, torch.int64)
|
||||||
|
|
||||||
|
def test_values_correct_single_candidate(self):
|
||||||
|
"""With one candidate per logical expert, dynamic matches static."""
|
||||||
|
info = _make_permuted_info()
|
||||||
|
info.ep_dispatch_algorithm = "dynamic"
|
||||||
|
topk_ids = torch.tensor([0, 1, 2, 127, 255], dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_dynamic(topk_ids, info)
|
||||||
|
expected = info.partial_logical_to_all_physical_map[topk_ids.long(), 0]
|
||||||
|
self.assertTrue(torch.equal(result, expected.to(torch.int32)))
|
||||||
|
|
||||||
|
def test_batch_shape_preserved(self):
|
||||||
|
"""2-D input shape is preserved through the remap."""
|
||||||
|
info = _make_permuted_info()
|
||||||
|
info.ep_dispatch_algorithm = "dynamic"
|
||||||
|
topk_ids = torch.randint(0, NUM_LOGICAL, (32, 8), dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_dynamic(topk_ids, info)
|
||||||
|
self.assertEqual(result.shape, (32, 8))
|
||||||
|
self.assertEqual(result.dtype, torch.int32)
|
||||||
|
|
||||||
|
def test_no_op_dtype_bug_regression(self):
|
||||||
|
"""Regression test: the old code did topk_ids.to(topk_ids.dtype) after
|
||||||
|
overwriting topk_ids with the int64 lookup result, which was a no-op.
|
||||||
|
Verify the output is actually int32, not silently int64."""
|
||||||
|
info = _make_permuted_info()
|
||||||
|
info.ep_dispatch_algorithm = "dynamic"
|
||||||
|
topk_ids = torch.randint(0, NUM_LOGICAL, (64, 8), dtype=torch.int32)
|
||||||
|
with patch("sglang.srt.eplb.expert_location_dispatch._is_hip", True):
|
||||||
|
result = _topk_ids_logical_to_physical_dynamic(topk_ids, info)
|
||||||
|
self.assertEqual(
|
||||||
|
result.dtype,
|
||||||
|
torch.int32,
|
||||||
|
"Dynamic dispatch returned int64 — the no-op dtype cast bug has regressed",
|
||||||
|
)
|
||||||
|
self.assertEqual(result.element_size(), 4)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user