[Core][Kernel] Share persistent workspaces for Marlin and Humming (#57421)
Signed-off-by: mgoin <mgoin64@gmail.com> Co-authored-by: ErinYin <liang.yin@daocloud.io>
This commit is contained in:
@@ -1384,24 +1384,33 @@ def test_humming_global_valid_shape_m(
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_humming_permute_scratch_is_keyed_by_runtime_topk(
|
||||
def test_humming_permute_scratch_is_shared_by_config(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
):
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import vllm.model_executor.layers.fused_moe.experts.fused_humming_moe as humming
|
||||
import vllm.model_executor.layers.fused_moe.moe_permute_unpermute as permute
|
||||
import vllm.v1.worker.workspace as workspace
|
||||
|
||||
manager = workspace.WorkspaceManager(torch.device("cpu"))
|
||||
monkeypatch.setattr(workspace, "_manager", manager)
|
||||
|
||||
scratch_topk6 = Mock()
|
||||
scratch_topk1 = Mock()
|
||||
scratch_type = Mock(side_effect=[scratch_topk6, scratch_topk1])
|
||||
monkeypatch.setattr(humming, "moe_permute_unpermute_supported", lambda: True)
|
||||
monkeypatch.setattr(humming, "MoEPermuteScratch", scratch_type)
|
||||
monkeypatch.setattr(permute, "MoEPermuteScratch", scratch_type)
|
||||
moe_config = make_dummy_moe_config(max_num_tokens=512, experts_per_token=6)
|
||||
moe_config.moe_parallel_config.dp_size = 2
|
||||
experts = SimpleNamespace(_permute_scratch={}, moe_config=moe_config)
|
||||
experts = SimpleNamespace(moe_config=moe_config)
|
||||
other_layer = SimpleNamespace(moe_config=moe_config)
|
||||
|
||||
assert humming.HummingExpertsBase._get_permute_scratch(experts, 6) is scratch_topk6
|
||||
assert (
|
||||
humming.HummingExpertsBase._get_permute_scratch(other_layer, 6) is scratch_topk6
|
||||
)
|
||||
assert humming.HummingExpertsBase._get_permute_scratch(experts, 1) is scratch_topk1
|
||||
assert humming.HummingExpertsBase._get_permute_scratch(experts, 6) is scratch_topk6
|
||||
|
||||
@@ -1412,6 +1421,11 @@ def test_humming_permute_scratch_is_keyed_by_runtime_topk(
|
||||
assert second_call.kwargs["max_num_tokens"] == 6144
|
||||
assert second_call.kwargs["topk"] == 1
|
||||
|
||||
manager.lock()
|
||||
assert humming.HummingExpertsBase._get_permute_scratch(experts, 6) is scratch_topk6
|
||||
with pytest.raises(AssertionError, match="was not allocated during warmup"):
|
||||
humming.HummingExpertsBase._get_permute_scratch(experts, 2)
|
||||
|
||||
|
||||
def test_humming_delegates_to_instance_activation():
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -15,12 +15,14 @@ from vllm.model_executor.layers.fused_moe.expert_map_manager import (
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe.moe_permute_unpermute import (
|
||||
MoEPermuteScratch,
|
||||
get_moe_permute_scratch,
|
||||
moe_permute,
|
||||
moe_permute_unpermute_supported,
|
||||
moe_unpermute,
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.utils.torch_utils import set_random_seed
|
||||
from vllm.v1.worker.workspace import current_workspace_manager
|
||||
|
||||
NUM_EXPERTS = [16, 64, 256]
|
||||
TOP_KS = [2, 6, 8]
|
||||
@@ -120,6 +122,7 @@ def torch_unpermute(
|
||||
@pytest.mark.parametrize("topk", TOP_KS)
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
@pytest.mark.parametrize("ep_size", EP_SIZE)
|
||||
@pytest.mark.parametrize("use_scratch", [False, True])
|
||||
def test_moe_permute_unpermute(
|
||||
n_token: int,
|
||||
n_hidden: int,
|
||||
@@ -127,6 +130,8 @@ def test_moe_permute_unpermute(
|
||||
n_expert: int,
|
||||
ep_size: int,
|
||||
dtype: torch.dtype,
|
||||
use_scratch: bool,
|
||||
workspace_init,
|
||||
):
|
||||
if not moe_permute_unpermute_supported():
|
||||
pytest.skip("moe_permute_unpermute is not supported on this platform.")
|
||||
@@ -160,6 +165,18 @@ def test_moe_permute_unpermute(
|
||||
expert_map=expert_map,
|
||||
)
|
||||
|
||||
scratch = None
|
||||
if use_scratch:
|
||||
scratch = get_moe_permute_scratch(
|
||||
max_num_tokens=n_token,
|
||||
topk=topk,
|
||||
num_experts=n_expert,
|
||||
num_local_experts=n_local_expert,
|
||||
device=hidden_states.device,
|
||||
hidden_size=n_hidden,
|
||||
hidden_dtype=dtype,
|
||||
)
|
||||
|
||||
(
|
||||
permuted_hidden_states,
|
||||
_,
|
||||
@@ -173,6 +190,7 @@ def test_moe_permute_unpermute(
|
||||
n_expert=n_expert,
|
||||
n_local_expert=n_local_expert,
|
||||
expert_map=expert_map,
|
||||
scratch=scratch,
|
||||
)
|
||||
|
||||
# check expert_first_token_offset
|
||||
@@ -212,20 +230,22 @@ def test_moe_permute_unpermute(
|
||||
torch.testing.assert_close(result4, gold4, atol=2e-2, rtol=0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [torch.bfloat16])
|
||||
def test_moe_permute_reuses_scratch_buffers(dtype: torch.dtype):
|
||||
@pytest.mark.parametrize("dtype", [torch.float16, torch.bfloat16])
|
||||
@pytest.mark.parametrize("n_token", [1, 33, 128])
|
||||
@pytest.mark.parametrize("topk", [1, 6, 8])
|
||||
def test_moe_permute_reuses_scratch_buffers(
|
||||
dtype: torch.dtype, n_token: int, topk: int, workspace_init
|
||||
):
|
||||
if not moe_permute_unpermute_supported():
|
||||
pytest.skip("moe_permute_unpermute is not supported on this platform.")
|
||||
|
||||
n_token = 64
|
||||
n_hidden = 2048
|
||||
n_expert = 16
|
||||
topk = 4
|
||||
hidden_states = torch.randn((n_token, n_hidden), device="cuda").to(dtype)
|
||||
gating_output = torch.randn((n_token, n_expert), device="cuda").to(dtype)
|
||||
_, topk_ids, _ = fused_topk(hidden_states, gating_output, topk, False)
|
||||
|
||||
scratch = MoEPermuteScratch(
|
||||
scratch_config = dict(
|
||||
max_num_tokens=n_token,
|
||||
topk=topk,
|
||||
num_experts=n_expert,
|
||||
@@ -234,6 +254,7 @@ def test_moe_permute_reuses_scratch_buffers(dtype: torch.dtype):
|
||||
hidden_size=n_hidden,
|
||||
hidden_dtype=hidden_states.dtype,
|
||||
)
|
||||
scratch = get_moe_permute_scratch(**scratch_config)
|
||||
|
||||
first = moe_permute(
|
||||
hidden_states=hidden_states,
|
||||
@@ -242,13 +263,26 @@ def test_moe_permute_reuses_scratch_buffers(dtype: torch.dtype):
|
||||
n_expert=n_expert,
|
||||
scratch=scratch,
|
||||
)
|
||||
second = moe_permute(
|
||||
hidden_states=hidden_states,
|
||||
a1q_scale=None,
|
||||
topk_ids=topk_ids,
|
||||
n_expert=n_expert,
|
||||
scratch=scratch,
|
||||
)
|
||||
current_workspace_manager().lock()
|
||||
assert get_moe_permute_scratch(**scratch_config) is scratch
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
second = moe_permute(
|
||||
hidden_states=hidden_states,
|
||||
a1q_scale=None,
|
||||
topk_ids=topk_ids,
|
||||
n_expert=n_expert,
|
||||
scratch=get_moe_permute_scratch(**scratch_config),
|
||||
)
|
||||
|
||||
for _ in range(2):
|
||||
hidden_states.add_(1)
|
||||
topk_ids.copy_(topk_ids.roll(1, 0))
|
||||
graph.replay()
|
||||
expected = moe_permute(hidden_states, None, topk_ids, n_expert)
|
||||
for actual, reference in zip(second, expected):
|
||||
if actual is not None:
|
||||
torch.testing.assert_close(actual, reference)
|
||||
|
||||
(
|
||||
permuted_hidden_states_1,
|
||||
@@ -288,6 +322,123 @@ def test_moe_permute_reuses_scratch_buffers(dtype: torch.dtype):
|
||||
)
|
||||
|
||||
|
||||
def test_moe_permute_scratch_reused_across_graph_sizes(workspace_init) -> None:
|
||||
"""Switching graph sizes must not expose metadata left by a larger batch."""
|
||||
if not moe_permute_unpermute_supported():
|
||||
pytest.skip("moe_permute_unpermute is not supported on this platform.")
|
||||
|
||||
device = torch.device("cuda:0")
|
||||
scratch = get_moe_permute_scratch(
|
||||
max_num_tokens=128,
|
||||
topk=6,
|
||||
num_experts=16,
|
||||
num_local_experts=16,
|
||||
device=device,
|
||||
hidden_size=2048,
|
||||
hidden_dtype=torch.bfloat16,
|
||||
)
|
||||
current_workspace_manager().lock()
|
||||
runs = []
|
||||
for n_token in (128, 1, 33):
|
||||
hidden = torch.randn((n_token, 2048), dtype=torch.bfloat16, device=device)
|
||||
topk_ids = torch.randint(16, (n_token, 6), device=device)
|
||||
moe_permute(hidden, None, topk_ids, 16, scratch=scratch)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph):
|
||||
output = moe_permute(hidden, None, topk_ids, 16, scratch=scratch)
|
||||
runs.append((graph, hidden, topk_ids, output))
|
||||
|
||||
for graph, hidden, topk_ids, output in runs + runs[::-1]:
|
||||
hidden.add_(1)
|
||||
topk_ids.copy_((topk_ids + 1) % 16)
|
||||
graph.replay()
|
||||
expected = moe_permute(hidden, None, topk_ids, 16)
|
||||
for actual, reference in zip(output, expected):
|
||||
if actual is not None:
|
||||
torch.testing.assert_close(actual, reference)
|
||||
|
||||
|
||||
def test_moe_permute_scratch_isolated_across_execution_slots(monkeypatch) -> None:
|
||||
"""Concurrent graph replays in different ubatches/lanes cannot share scratch."""
|
||||
if not moe_permute_unpermute_supported():
|
||||
pytest.skip("moe_permute_unpermute is not supported on this platform.")
|
||||
|
||||
from vllm.v1.worker import workspace
|
||||
|
||||
device = torch.device("cuda:0")
|
||||
manager = workspace.WorkspaceManager(device, num_ubatches=2, num_lanes=2)
|
||||
monkeypatch.setattr(workspace, "_manager", manager)
|
||||
ubatch = 0
|
||||
monkeypatch.setattr(workspace, "dbo_current_ubatch_id", lambda: ubatch)
|
||||
scratch_config = dict(
|
||||
max_num_tokens=64,
|
||||
topk=6,
|
||||
num_experts=16,
|
||||
num_local_experts=16,
|
||||
device=device,
|
||||
hidden_size=2048,
|
||||
hidden_dtype=torch.bfloat16,
|
||||
)
|
||||
runs = []
|
||||
for i, n_token in enumerate((1, 7, 33, 64)):
|
||||
ubatch = i // 2
|
||||
stream = torch.cuda.Stream()
|
||||
stream.wait_stream(torch.cuda.current_stream())
|
||||
with workspace.use_workspace_lane(i % 2), torch.cuda.stream(stream):
|
||||
hidden = torch.randn((n_token, 2048), dtype=torch.bfloat16, device=device)
|
||||
topk_ids = torch.randint(16, (n_token, 6), device=device)
|
||||
scratch = get_moe_permute_scratch(**scratch_config)
|
||||
moe_permute(hidden, None, topk_ids, 16, scratch=scratch)
|
||||
graph = torch.cuda.CUDAGraph()
|
||||
with torch.cuda.graph(graph, stream=stream):
|
||||
output = moe_permute(hidden, None, topk_ids, 16, scratch=scratch)
|
||||
runs.append((stream, graph, hidden, topk_ids, output, scratch))
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
|
||||
assert len({run[-1].permuted_hidden_states.data_ptr() for run in runs}) == 4
|
||||
manager.lock()
|
||||
for _ in range(3):
|
||||
for i, (stream, graph, hidden, topk_ids, _, scratch) in enumerate(runs):
|
||||
ubatch = i // 2
|
||||
with workspace.use_workspace_lane(i % 2), torch.cuda.stream(stream):
|
||||
assert get_moe_permute_scratch(**scratch_config) is scratch
|
||||
hidden.add_(1)
|
||||
topk_ids.copy_((topk_ids + 1) % 16)
|
||||
graph.replay()
|
||||
for stream, _, hidden, topk_ids, output, _ in runs:
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
expected = moe_permute(hidden, None, topk_ids, 16)
|
||||
for actual, reference in zip(output, expected):
|
||||
if actual is not None:
|
||||
torch.testing.assert_close(actual, reference)
|
||||
|
||||
|
||||
def test_moe_permute_scratch_without_manager(monkeypatch) -> None:
|
||||
"""Standalone calls get independent scratch with initialized row indices."""
|
||||
if not moe_permute_unpermute_supported():
|
||||
pytest.skip("moe_permute_unpermute is not supported on this platform.")
|
||||
|
||||
from vllm.v1.worker import workspace
|
||||
|
||||
monkeypatch.setattr(workspace, "_manager", None)
|
||||
config = dict(
|
||||
max_num_tokens=4,
|
||||
topk=2,
|
||||
num_experts=4,
|
||||
num_local_experts=4,
|
||||
device=torch.device("cuda"),
|
||||
)
|
||||
first = get_moe_permute_scratch(**config)
|
||||
second = get_moe_permute_scratch(**config)
|
||||
|
||||
assert (
|
||||
first.token_expert_indices.data_ptr() != second.token_expert_indices.data_ptr()
|
||||
)
|
||||
torch.testing.assert_close(
|
||||
first.token_expert_indices, torch.arange(8, dtype=torch.int32, device="cuda")
|
||||
)
|
||||
|
||||
|
||||
def test_moe_permute_ignores_invalid_expert_ids_with_scratch() -> None:
|
||||
if not moe_permute_unpermute_supported():
|
||||
pytest.skip("moe_permute_unpermute is not supported on this platform.")
|
||||
|
||||
@@ -17,6 +17,7 @@ from vllm.model_executor.layers.quantization.utils.int8_utils import (
|
||||
per_token_quant_int8,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
get_marlin_workspace,
|
||||
marlin_make_empty,
|
||||
marlin_make_workspace_new,
|
||||
marlin_permute_bias,
|
||||
@@ -43,6 +44,7 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
)
|
||||
from vllm.platforms import current_platform
|
||||
from vllm.scalar_type import scalar_types
|
||||
from vllm.v1.worker.workspace import current_workspace_manager
|
||||
|
||||
if current_platform.is_rocm():
|
||||
pytest.skip(
|
||||
@@ -477,6 +479,62 @@ def test_marlin_gemm(
|
||||
assert max_diff < 0.04
|
||||
|
||||
|
||||
def test_marlin_persistent_locks_cuda_graph_streams(workspace_init):
|
||||
"""Compiled GEMMs select stream-local locks at runtime and survive replay."""
|
||||
size_m, size_k, size_n = 32, 1024, 2048
|
||||
inputs = [rand_data((size_m, size_k)) for _ in range(2)]
|
||||
weight = rand_data((size_k, size_n))
|
||||
quant_type = scalar_types.uint4b8
|
||||
w_ref, qweight, scales = marlin_quantize(weight, quant_type, 128)
|
||||
zeros = marlin_make_empty(scales.device)
|
||||
streams = [torch.cuda.Stream() for _ in inputs]
|
||||
graphs = [torch.cuda.CUDAGraph() for _ in inputs]
|
||||
locks = []
|
||||
outputs = []
|
||||
|
||||
@torch.compile(backend="eager", fullgraph=True)
|
||||
def run(x):
|
||||
return torch.ops.vllm.marlin_gemm(
|
||||
x,
|
||||
qweight,
|
||||
None,
|
||||
scales,
|
||||
None,
|
||||
None,
|
||||
zeros,
|
||||
None,
|
||||
quant_type.id,
|
||||
size_m,
|
||||
size_n,
|
||||
size_k,
|
||||
use_atomic_add=False,
|
||||
use_fp32_reduce=True,
|
||||
)
|
||||
|
||||
for stream, graph, x in zip(streams, graphs, inputs):
|
||||
stream.wait_stream(torch.cuda.current_stream())
|
||||
with torch.cuda.stream(stream):
|
||||
locks.append(get_marlin_workspace(x.device))
|
||||
run(x)
|
||||
with torch.cuda.graph(graph, stream=stream):
|
||||
outputs.append(run(x))
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
|
||||
assert locks[0].data_ptr() != locks[1].data_ptr()
|
||||
current_workspace_manager().lock()
|
||||
for _ in range(3):
|
||||
for stream, graph, x, lock in zip(streams, graphs, inputs, locks):
|
||||
with torch.cuda.stream(stream):
|
||||
assert get_marlin_workspace(x.device) is lock
|
||||
x.add_(0.1)
|
||||
graph.replay()
|
||||
for stream in streams:
|
||||
torch.cuda.current_stream().wait_stream(stream)
|
||||
for x, output, lock in zip(inputs, outputs, locks):
|
||||
assert compute_max_diff(output, x @ w_ref) < 0.04
|
||||
assert torch.count_nonzero(lock) == 0
|
||||
|
||||
|
||||
def test_marlin_gemm_subset_input():
|
||||
quant_type = scalar_types.uint4b8
|
||||
group_size = 128
|
||||
|
||||
@@ -14,6 +14,7 @@ from vllm import _custom_ops as ops
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
GPTQ_MARLIN_TILE,
|
||||
apply_gptq_marlin_linear,
|
||||
get_marlin_workspace,
|
||||
marlin_make_empty,
|
||||
marlin_make_workspace_new,
|
||||
marlin_moe_padded_intermediate,
|
||||
@@ -225,7 +226,7 @@ def test_fp8_marlin_padded_round_trip(shape, use_bias):
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
workspace=layer.workspace,
|
||||
workspace=get_marlin_workspace(layer.weight.device),
|
||||
size_n=size_n,
|
||||
size_k=size_k,
|
||||
bias=layer.bias if use_bias else None,
|
||||
@@ -288,7 +289,7 @@ def test_nvfp4_marlin_padded_round_trip(shape):
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
weight_global_scale=layer.weight_global_scale,
|
||||
workspace=layer.workspace,
|
||||
workspace=get_marlin_workspace(layer.weight.device),
|
||||
size_n=size_n,
|
||||
size_k=size_k,
|
||||
)
|
||||
@@ -392,7 +393,7 @@ def test_fp8_block_marlin_padded_round_trip(shape):
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale_inv,
|
||||
workspace=layer.workspace,
|
||||
workspace=get_marlin_workspace(layer.weight.device),
|
||||
size_n=size_n,
|
||||
size_k=size_k,
|
||||
bias=None,
|
||||
@@ -440,7 +441,7 @@ def test_mxfp8_marlin_padded_round_trip(shape):
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
workspace=layer.workspace,
|
||||
workspace=get_marlin_workspace(layer.weight.device),
|
||||
size_n=size_n,
|
||||
size_k=size_k,
|
||||
)
|
||||
@@ -756,7 +757,7 @@ def test_fp8_marlin_moe_padded_round_trip(shape, quant):
|
||||
topk_ids,
|
||||
quant_type_id=scalar_types.float8_e4m3fn.id,
|
||||
global_num_experts=e,
|
||||
workspace=layer.workspace,
|
||||
workspace=get_marlin_workspace(device),
|
||||
)
|
||||
with set_current_vllm_config(VllmConfig()):
|
||||
ref = torch_experts(
|
||||
@@ -833,7 +834,7 @@ def test_mxfp8_marlin_moe_padded_round_trip(shape):
|
||||
topk_ids,
|
||||
quant_type_id=scalar_types.float8_e4m3fn.id,
|
||||
global_num_experts=e,
|
||||
workspace=layer.workspace,
|
||||
workspace=get_marlin_workspace(device),
|
||||
)
|
||||
with set_current_vllm_config(VllmConfig()):
|
||||
ref = torch_experts(
|
||||
|
||||
@@ -330,8 +330,8 @@ def _load_marlin_checkpoint_format_weights(layer):
|
||||
)
|
||||
|
||||
|
||||
def test_marlin_post_load_preserves_runtime_tensor_addresses(monkeypatch, dist_init):
|
||||
"""Marlin must reuse the workspace storage captured by CUDA graphs."""
|
||||
def test_marlin_post_load_does_not_own_workspace(monkeypatch, dist_init):
|
||||
"""Weight reload must not create layer-owned Marlin lock storage."""
|
||||
_stub_marlin_ops(monkeypatch)
|
||||
kernel = _make_marlin_kernel()
|
||||
|
||||
@@ -339,19 +339,17 @@ def test_marlin_post_load_preserves_runtime_tensor_addresses(monkeypatch, dist_i
|
||||
_load_marlin_checkpoint_format_weights(layer)
|
||||
kernel.process_weights_after_loading(layer)
|
||||
|
||||
workspace_ptr = kernel.workspace.data_ptr()
|
||||
assert not hasattr(kernel, "workspace")
|
||||
|
||||
_load_marlin_checkpoint_format_weights(layer)
|
||||
kernel.process_weights_after_loading(layer)
|
||||
|
||||
assert kernel.workspace.data_ptr() == workspace_ptr
|
||||
assert torch.all(kernel.workspace == 0)
|
||||
assert not hasattr(kernel, "workspace")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("variant", ["fp8", "mxfp8", "nvfp4"])
|
||||
def test_marlin_prepare_layer_preserves_workspace_address(monkeypatch, variant):
|
||||
"""The Marlin fallback prepare_* functions rerun on weight reload and must
|
||||
reuse the workspace storage whose address captured CUDA graphs hold."""
|
||||
def test_marlin_prepare_layer_does_not_own_workspace(monkeypatch, variant):
|
||||
"""Weight preparation must not attach runtime workspace to model layers."""
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.model_executor.layers.quantization.utils import (
|
||||
marlin_utils,
|
||||
@@ -417,34 +415,57 @@ def test_marlin_prepare_layer_preserves_workspace_address(monkeypatch, variant):
|
||||
|
||||
load_checkpoint_format_weights()
|
||||
prepare(layer)
|
||||
workspace_ptr = layer.workspace.data_ptr()
|
||||
assert not hasattr(layer, "workspace")
|
||||
|
||||
# Reload: fresh checkpoint-format tensors, prepare runs again
|
||||
load_checkpoint_format_weights()
|
||||
prepare(layer)
|
||||
|
||||
assert layer.workspace.data_ptr() == workspace_ptr
|
||||
assert torch.all(layer.workspace == 0)
|
||||
assert not hasattr(layer, "workspace")
|
||||
|
||||
|
||||
def test_marlin_make_workspace_new_rejects_incompatible_existing(monkeypatch):
|
||||
"""An incompatible existing workspace means the address captured by CUDA
|
||||
graphs is already unusable; allocating a replacement would hide that."""
|
||||
def test_marlin_workspace_uses_persistent_workspace_manager(monkeypatch):
|
||||
"""Calls reuse initialized locks; independent streams get separate storage."""
|
||||
from vllm.model_executor.layers.quantization.utils import marlin_utils
|
||||
from vllm.utils import torch_utils
|
||||
from vllm.v1.worker import workspace as workspace_module
|
||||
|
||||
monkeypatch.setattr(marlin_utils, "num_compute_units", lambda _: 4)
|
||||
device = torch.device("cpu")
|
||||
manager = workspace_module.WorkspaceManager(device)
|
||||
monkeypatch.setattr(workspace_module, "_manager", manager)
|
||||
stream = "main"
|
||||
monkeypatch.setattr(torch_utils, "current_stream", lambda: stream)
|
||||
|
||||
workspace = marlin_utils.marlin_make_workspace_new(device)
|
||||
reused = marlin_utils.marlin_make_workspace_new(device, existing=workspace)
|
||||
assert reused is workspace
|
||||
workspace = marlin_utils.get_marlin_workspace(device)
|
||||
assert workspace.shape == (4 * marlin_utils.MARLIN_MAX_BLOCKS_PER_SM,)
|
||||
assert workspace.dtype == torch.int32
|
||||
assert torch.count_nonzero(workspace) == 0
|
||||
|
||||
with pytest.raises(ValueError, match="incompatible"):
|
||||
marlin_utils.marlin_make_workspace_new(device, 4, existing=workspace)
|
||||
with pytest.raises(ValueError, match="incompatible"):
|
||||
marlin_utils.marlin_make_workspace_new(
|
||||
device, existing=workspace.to(torch.int64)
|
||||
)
|
||||
workspace.fill_(1)
|
||||
assert marlin_utils.get_marlin_workspace(device) is workspace
|
||||
assert torch.all(workspace == 1)
|
||||
|
||||
stream = "aux"
|
||||
aux_workspace = marlin_utils.get_marlin_workspace(device)
|
||||
assert aux_workspace.data_ptr() != workspace.data_ptr()
|
||||
assert torch.count_nonzero(aux_workspace) == 0
|
||||
|
||||
manager.lock()
|
||||
assert marlin_utils.get_marlin_workspace(device) is aux_workspace
|
||||
|
||||
|
||||
def test_marlin_workspace_without_manager(monkeypatch):
|
||||
from vllm.model_executor.layers.quantization.utils import marlin_utils
|
||||
from vllm.v1.worker import workspace as workspace_module
|
||||
|
||||
monkeypatch.setattr(workspace_module, "_manager", None)
|
||||
monkeypatch.setattr(marlin_utils, "num_compute_units", lambda _: 4)
|
||||
first = marlin_utils.get_marlin_workspace(torch.device("cpu"))
|
||||
second = marlin_utils.get_marlin_workspace(torch.device("cpu"))
|
||||
assert first.data_ptr() != second.data_ptr()
|
||||
assert first.shape == (4 * marlin_utils.MARLIN_MAX_BLOCKS_PER_SM,)
|
||||
assert torch.count_nonzero(first) == 0
|
||||
|
||||
|
||||
def test_model_cleanup(dist_init, default_vllm_config):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from typing import cast
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
@@ -124,3 +125,65 @@ def test_workspace_lane_validation(monkeypatch) -> None:
|
||||
|
||||
with pytest.raises(ValueError, match="at least one"):
|
||||
workspace.WorkspaceManager(torch.device("cpu"), num_lanes=0)
|
||||
|
||||
|
||||
def test_persistent_resources_are_lazy_and_isolated(monkeypatch) -> None:
|
||||
"""Only the requesting slot allocates; each ubatch/lane owns its resource."""
|
||||
active_ubatch = 0
|
||||
monkeypatch.setattr(workspace, "dbo_current_ubatch_id", lambda: active_ubatch)
|
||||
manager = workspace.WorkspaceManager(
|
||||
torch.device("cpu"), num_ubatches=2, num_lanes=2
|
||||
)
|
||||
factory = Mock(side_effect=lambda: {"buffer": torch.zeros(8)})
|
||||
resources = []
|
||||
for ubatch in range(2):
|
||||
active_ubatch = ubatch
|
||||
for lane in range(2):
|
||||
with workspace.use_workspace_lane(lane):
|
||||
resource = manager.get_persistent_resource("scratch", factory)
|
||||
resources.append(resource)
|
||||
resource["buffer"].fill_(len(resources))
|
||||
assert manager.get_persistent_resource("scratch", factory) is resource
|
||||
assert factory.call_count == len(resources)
|
||||
|
||||
assert len({r["buffer"].data_ptr() for r in resources}) == 4
|
||||
for i, resource in enumerate(resources, 1):
|
||||
assert torch.all(resource["buffer"] == i)
|
||||
|
||||
|
||||
def test_persistent_resource_lock_applies_to_each_slot(monkeypatch) -> None:
|
||||
monkeypatch.setattr(workspace, "dbo_current_ubatch_id", lambda: 0)
|
||||
manager = workspace.WorkspaceManager(torch.device("cpu"), num_lanes=2)
|
||||
factory = Mock(side_effect=object)
|
||||
first = manager.get_persistent_resource("scratch", factory)
|
||||
manager.lock()
|
||||
assert manager.get_persistent_resource("scratch", factory) is first
|
||||
with pytest.raises(AssertionError, match="was not allocated during warmup"):
|
||||
manager.get_persistent_resource("new", factory)
|
||||
with workspace.use_workspace_lane(1):
|
||||
with pytest.raises(AssertionError, match="was not allocated during warmup"):
|
||||
manager.get_persistent_resource("scratch", factory)
|
||||
assert factory.call_count == 1
|
||||
manager.unlock()
|
||||
assert manager.get_persistent_resource("scratch", factory) is not first
|
||||
assert manager.get_persistent_resource("scratch", factory) is first
|
||||
|
||||
|
||||
def test_persistent_tensor_preserves_contents_and_rejects_changes() -> None:
|
||||
"""Initialization happens once and transient scratch cannot overwrite locks."""
|
||||
manager = workspace.WorkspaceManager(torch.device("cpu"))
|
||||
first = manager.get_persistent("locks", (8,), torch.int32, zero_init=True)
|
||||
assert torch.count_nonzero(first) == 0
|
||||
first.fill_(1)
|
||||
(scratch,) = manager.get_simultaneous(((1024,), torch.uint8))
|
||||
scratch.zero_()
|
||||
assert manager.get_persistent("locks", (8,), torch.int32, zero_init=True) is first
|
||||
assert torch.all(first == 1)
|
||||
with pytest.raises(ValueError, match="requested shape"):
|
||||
manager.get_persistent("locks", (16,), torch.int32)
|
||||
with pytest.raises(ValueError, match="requested shape"):
|
||||
manager.get_persistent("locks", (8,), torch.int64)
|
||||
manager.lock()
|
||||
assert manager.get_persistent("locks", (8,), torch.int32) is first
|
||||
with pytest.raises(AssertionError, match="was not allocated during warmup"):
|
||||
manager.get_persistent("new", (8,), torch.int32)
|
||||
|
||||
@@ -10,7 +10,6 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
apply_gptq_marlin_linear,
|
||||
marlin_act_int8_process_scales,
|
||||
marlin_make_empty,
|
||||
marlin_make_workspace_new,
|
||||
marlin_pad_dim,
|
||||
marlin_pad_qweight,
|
||||
marlin_pad_scales,
|
||||
@@ -91,11 +90,6 @@ class MarlinLinearKernel(MPLinearKernel):
|
||||
size_k, size_n = c.partition_weight_shape
|
||||
padded_n, padded_k = marlin_padded_nk(size_n, size_k, c.group_size)
|
||||
|
||||
# Allocate marlin workspace, reusing existing storage on reload.
|
||||
self.workspace = marlin_make_workspace_new(
|
||||
device, existing=getattr(self, "workspace", None)
|
||||
)
|
||||
|
||||
# Default name since marlin requires empty parameter for zp,
|
||||
# TODO: remove this requirement from marlin (allow optional tensors)
|
||||
if self.w_zp_name is None:
|
||||
@@ -197,7 +191,7 @@ class MarlinLinearKernel(MPLinearKernel):
|
||||
weight=w_q,
|
||||
weight_scale=w_s,
|
||||
weight_zp=w_zp, # type: ignore
|
||||
workspace=self.workspace,
|
||||
workspace=None,
|
||||
wtype=c.weight_type,
|
||||
input_size_per_partition=c.partition_weight_shape[0],
|
||||
output_size_per_partition=c.partition_weight_shape[1],
|
||||
|
||||
@@ -58,7 +58,7 @@ class MarlinMxFp4LinearKernel(MxFp4LinearKernel):
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
weight_global_scale=None,
|
||||
workspace=layer.workspace,
|
||||
workspace=None,
|
||||
size_n=layer.output_size_per_partition,
|
||||
size_k=layer.input_size_per_partition,
|
||||
bias=bias,
|
||||
|
||||
@@ -46,7 +46,7 @@ class MarlinMxfp8LinearKernel(Mxfp8LinearKernel):
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
workspace=layer.workspace,
|
||||
workspace=None,
|
||||
size_n=layer.output_size_per_partition,
|
||||
size_k=layer.input_size_per_partition,
|
||||
bias=bias,
|
||||
|
||||
@@ -50,7 +50,7 @@ class MarlinNvFp4LinearKernel(NvFp4LinearKernel):
|
||||
weight=layer.weight,
|
||||
weight_scale=layer.weight_scale,
|
||||
weight_global_scale=layer.weight_global_scale,
|
||||
workspace=layer.workspace,
|
||||
workspace=None,
|
||||
size_n=layer.output_size_per_partition,
|
||||
size_k=layer.input_size_per_partition,
|
||||
bias=bias,
|
||||
|
||||
@@ -93,7 +93,7 @@ class MarlinFP8ScaledMMLinearKernel(FP8ScaledMMLinearKernel):
|
||||
input=x,
|
||||
weight=layer.weight,
|
||||
weight_scale=weight_scale,
|
||||
workspace=layer.workspace,
|
||||
workspace=None,
|
||||
size_n=layer.output_size_per_partition,
|
||||
size_k=layer.input_size_per_partition,
|
||||
input_dtype=self.marlin_input_dtype,
|
||||
|
||||
@@ -27,6 +27,7 @@ from vllm.model_executor.layers.fused_moe.moe_align_block_size import (
|
||||
from vllm.model_executor.layers.fused_moe.moe_fused_mul_sum import moe_fused_mul_sum
|
||||
from vllm.model_executor.layers.fused_moe.moe_permute_unpermute import (
|
||||
MoEPermuteScratch,
|
||||
get_moe_permute_scratch,
|
||||
moe_permute,
|
||||
moe_permute_unpermute_supported,
|
||||
moe_unpermute,
|
||||
@@ -176,7 +177,6 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
|
||||
max_num_tokens=max_num_tokens,
|
||||
num_dispatchers=num_dispatchers,
|
||||
)
|
||||
self._permute_scratch: dict[int, MoEPermuteScratch] = {}
|
||||
|
||||
def init_humming_moe(self):
|
||||
from vllm.utils.humming import get_heuristics_config
|
||||
@@ -259,24 +259,20 @@ class HummingExpertsBase(mk.FusedMoEExpertsModular):
|
||||
if not moe_permute_unpermute_supported():
|
||||
return None
|
||||
|
||||
scratch = self._permute_scratch.get(topk)
|
||||
if scratch is None:
|
||||
max_expanded_rows = (
|
||||
self.moe_config.max_num_tokens
|
||||
* self.moe_config.dp_size
|
||||
* self.moe_config.experts_per_token
|
||||
)
|
||||
scratch = MoEPermuteScratch(
|
||||
max_num_tokens=math.ceil(max_expanded_rows / topk),
|
||||
topk=topk,
|
||||
num_experts=self.moe_config.num_experts,
|
||||
num_local_experts=self.moe_config.num_local_experts,
|
||||
device=torch.device(self.moe_config.device),
|
||||
hidden_size=self.moe_config.hidden_dim,
|
||||
hidden_dtype=self.moe_config.in_dtype,
|
||||
)
|
||||
self._permute_scratch[topk] = scratch
|
||||
return scratch
|
||||
max_expanded_rows = (
|
||||
self.moe_config.max_num_tokens
|
||||
* self.moe_config.dp_size
|
||||
* self.moe_config.experts_per_token
|
||||
)
|
||||
return get_moe_permute_scratch(
|
||||
max_num_tokens=math.ceil(max_expanded_rows / topk),
|
||||
topk=topk,
|
||||
num_experts=self.moe_config.num_experts,
|
||||
num_local_experts=self.moe_config.num_local_experts,
|
||||
device=torch.device(self.moe_config.device),
|
||||
hidden_size=self.moe_config.hidden_dim,
|
||||
hidden_dtype=self.moe_config.in_dtype,
|
||||
)
|
||||
|
||||
def get_global_valid_shape_m(self, topk_ids: torch.Tensor):
|
||||
ctx = get_forward_context()
|
||||
|
||||
@@ -35,7 +35,7 @@ from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
|
||||
from vllm.model_executor.layers.fused_moe.utils import _resize_cache
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
get_marlin_input_dtype,
|
||||
marlin_make_workspace_new,
|
||||
get_marlin_workspace,
|
||||
marlin_moe_intermediate_size,
|
||||
marlin_quant_input,
|
||||
)
|
||||
@@ -95,7 +95,7 @@ def _fused_marlin_moe(
|
||||
N = marlin_moe_intermediate_size(w1, w2)
|
||||
w13_num_shards = 2 if activation.is_gated else 1
|
||||
if workspace is None:
|
||||
workspace = marlin_make_workspace_new(hidden_states.device, 4)
|
||||
workspace = get_marlin_workspace(hidden_states.device)
|
||||
|
||||
if intermediate_cache13 is None:
|
||||
intermediate_cache13 = torch.empty(
|
||||
|
||||
@@ -5,6 +5,11 @@ from dataclasses import dataclass, field
|
||||
|
||||
import torch
|
||||
|
||||
from vllm.v1.worker.workspace import (
|
||||
current_workspace_manager,
|
||||
is_workspace_manager_initialized,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MoEPermuteScratch:
|
||||
@@ -102,6 +107,48 @@ class MoEPermuteScratch:
|
||||
return topk_ids_int32
|
||||
|
||||
|
||||
def get_moe_permute_scratch(
|
||||
*,
|
||||
max_num_tokens: int,
|
||||
topk: int,
|
||||
num_experts: int,
|
||||
num_local_experts: int,
|
||||
device: torch.device,
|
||||
hidden_size: int | None = None,
|
||||
hidden_dtype: torch.dtype | None = None,
|
||||
) -> MoEPermuteScratch:
|
||||
"""Share scratch across sequential layers in the current ubatch and lane.
|
||||
|
||||
Without a workspace manager, allocate new scratch for each call.
|
||||
"""
|
||||
|
||||
def create_scratch() -> MoEPermuteScratch:
|
||||
return MoEPermuteScratch(
|
||||
max_num_tokens=max_num_tokens,
|
||||
topk=topk,
|
||||
num_experts=num_experts,
|
||||
num_local_experts=num_local_experts,
|
||||
device=device,
|
||||
hidden_size=hidden_size,
|
||||
hidden_dtype=hidden_dtype,
|
||||
)
|
||||
|
||||
if not is_workspace_manager_initialized():
|
||||
return create_scratch()
|
||||
|
||||
key = (
|
||||
MoEPermuteScratch,
|
||||
max_num_tokens,
|
||||
topk,
|
||||
num_experts,
|
||||
num_local_experts,
|
||||
device,
|
||||
hidden_size,
|
||||
hidden_dtype,
|
||||
)
|
||||
return current_workspace_manager().get_persistent_resource(key, create_scratch)
|
||||
|
||||
|
||||
def moe_permute(
|
||||
hidden_states: torch.Tensor,
|
||||
a1q_scale: torch.Tensor | None,
|
||||
|
||||
@@ -48,7 +48,6 @@ from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
check_marlin_supports_layer,
|
||||
check_moe_marlin_supports_layer,
|
||||
get_marlin_input_dtype,
|
||||
marlin_make_workspace_new,
|
||||
verify_marlin_supported,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
@@ -642,9 +641,6 @@ class AutoAWQMoEMethod(FusedMoEMethodBase):
|
||||
layer.register_parameter("w2_qzeros", w2_qzeros)
|
||||
set_weight_attrs(w2_qzeros, extra_weight_attrs)
|
||||
|
||||
device = layer.w13_qweight.device
|
||||
layer.workspace = marlin_make_workspace_new(device, 4)
|
||||
|
||||
def process_weights_after_loading(self, layer: RoutedExperts) -> None:
|
||||
converted = convert_to_wna16_moe_kernel_format(
|
||||
backend=self.wna16_moe_backend,
|
||||
|
||||
@@ -16,7 +16,6 @@ from vllm.model_executor.kernels.linear import (
|
||||
)
|
||||
from vllm.model_executor.layers.fused_moe import (
|
||||
FusedMoEConfig,
|
||||
FusedMoEExpertsModular,
|
||||
FusedMoEMethodBase,
|
||||
FusedMoEQuantConfig,
|
||||
FusedMoeWeightScaleSupported,
|
||||
@@ -46,7 +45,6 @@ from vllm.model_executor.layers.quantization.utils.gptq_utils import (
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
check_moe_marlin_supports_layer,
|
||||
get_marlin_input_dtype,
|
||||
marlin_make_workspace_new,
|
||||
marlin_repeat_scales_on_all_ranks,
|
||||
verify_marlin_supported,
|
||||
)
|
||||
@@ -601,12 +599,6 @@ class AutoGPTQMoEMethod(FusedMoEMethodBase):
|
||||
layer.register_parameter("w2_bias", w2_bias)
|
||||
set_weight_attrs(w2_bias, extra_weight_attrs)
|
||||
|
||||
if self.experts_cls is not None and issubclass(
|
||||
self.experts_cls, FusedMoEExpertsModular
|
||||
):
|
||||
device = layer.w13_qweight.device
|
||||
layer.workspace = marlin_make_workspace_new(device, 4)
|
||||
|
||||
def process_weights_after_loading(self, layer: RoutedExperts) -> None:
|
||||
def replace_or_register(name: str, val: torch.Tensor | None):
|
||||
if val is None:
|
||||
|
||||
-14
@@ -11,7 +11,6 @@ from compressed_tensors.quantization import (
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.fused_moe import (
|
||||
FusedMoEExpertsModular,
|
||||
RoutedExperts,
|
||||
SharedExperts,
|
||||
)
|
||||
@@ -36,7 +35,6 @@ from vllm.model_executor.layers.quantization.compressed_tensors.schemes.compress
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
check_moe_marlin_supports_config,
|
||||
get_marlin_input_dtype,
|
||||
marlin_make_workspace_new,
|
||||
)
|
||||
from vllm.model_executor.layers.quantization.utils.quant_utils import (
|
||||
GroupShape,
|
||||
@@ -466,18 +464,6 @@ class CompressedTensorsWNA16MoEMethod(CompressedTensorsMoEMethod):
|
||||
torch.nn.Parameter(w2_input_global_scale, requires_grad=False),
|
||||
)
|
||||
|
||||
# Marlin workspace — only needed for Marlin-family backends, not emulation.
|
||||
if (
|
||||
self.experts_cls is not None
|
||||
and issubclass(self.experts_cls, FusedMoEExpertsModular)
|
||||
and self.wna16_backend != WNA16MoEBackend.EMULATION
|
||||
):
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
layer.w13_weight_packed.device,
|
||||
4,
|
||||
existing=getattr(layer, "workspace", None),
|
||||
)
|
||||
|
||||
# Alias packed weights to w13_weight/w2_weight for the modular kernel interface
|
||||
layer.w13_weight = layer.w13_weight_packed
|
||||
layer.w2_weight = layer.w2_weight_packed
|
||||
|
||||
@@ -8,7 +8,6 @@ import numpy
|
||||
import torch
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm import _custom_ops as ops
|
||||
from vllm.distributed.utils import verify_group_size_divides_partition
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.fused_moe import RoutedExperts
|
||||
@@ -23,6 +22,7 @@ from vllm.platforms import current_platform
|
||||
from vllm.scalar_type import ScalarType, scalar_types
|
||||
from vllm.utils.math_utils import round_up
|
||||
from vllm.utils.platform_utils import num_compute_units
|
||||
from vllm.utils.torch_utils import direct_register_custom_op
|
||||
|
||||
from .quant_utils import pack_cols, unpack_cols
|
||||
|
||||
@@ -32,6 +32,7 @@ GPTQ_MARLIN_TILE = 16
|
||||
GPTQ_MARLIN_MIN_THREAD_N = 64
|
||||
GPTQ_MARLIN_MIN_THREAD_K = 128
|
||||
GPTQ_MARLIN_MAX_PARALLEL = 16
|
||||
MARLIN_MAX_BLOCKS_PER_SM = 4
|
||||
|
||||
MARLIN_SUPPORTED_GROUP_SIZES = [-1, 32, 64, 128]
|
||||
|
||||
@@ -407,29 +408,96 @@ def marlin_moe_intermediate_size(w1_packed: torch.Tensor, w2_packed: torch.Tenso
|
||||
def marlin_make_workspace_new(
|
||||
device: torch.device,
|
||||
max_blocks_per_sm: int = 1,
|
||||
existing: torch.Tensor | None = None,
|
||||
) -> torch.Tensor:
|
||||
# In the new marlin kernel, we use the num of threadblocks as workspace
|
||||
# size. The num of threadblocks is sms_count * max_blocks_per_sm.
|
||||
sms = num_compute_units(device.index)
|
||||
size = sms * max_blocks_per_sm
|
||||
# On weight reload, reuse the existing storage so the workspace address
|
||||
# captured by CUDA graphs stays valid.
|
||||
if existing is not None:
|
||||
if (
|
||||
existing.device != device
|
||||
or existing.dtype != torch.int
|
||||
or existing.numel() != size
|
||||
):
|
||||
raise ValueError(
|
||||
f"Existing Marlin workspace is incompatible "
|
||||
f"(device={existing.device}, dtype={existing.dtype}, "
|
||||
f"numel={existing.numel()}; expected device={device}, "
|
||||
f"dtype={torch.int}, numel={size}). Reload must reuse the "
|
||||
f"workspace storage captured by CUDA graphs."
|
||||
)
|
||||
return existing.zero_()
|
||||
return torch.zeros(size, dtype=torch.int, device=device, requires_grad=False)
|
||||
return torch.zeros(sms * max_blocks_per_sm, dtype=torch.int, device=device)
|
||||
|
||||
|
||||
def get_marlin_workspace(device: torch.device) -> torch.Tensor:
|
||||
"""Get zero-initialized locks for the current stream, ubatch, and lane.
|
||||
|
||||
Marlin resets its locks after each call, so ordered calls can reuse them.
|
||||
"""
|
||||
from vllm.utils.torch_utils import current_stream
|
||||
from vllm.v1.worker.workspace import (
|
||||
current_workspace_manager,
|
||||
is_workspace_manager_initialized,
|
||||
)
|
||||
|
||||
if not is_workspace_manager_initialized():
|
||||
return marlin_make_workspace_new(device, MARLIN_MAX_BLOCKS_PER_SM)
|
||||
|
||||
return current_workspace_manager().get_persistent(
|
||||
("marlin_locks", current_stream()),
|
||||
(num_compute_units(device.index) * MARLIN_MAX_BLOCKS_PER_SM,),
|
||||
torch.int,
|
||||
zero_init=True,
|
||||
)
|
||||
|
||||
|
||||
def _marlin_gemm(
|
||||
a: torch.Tensor,
|
||||
b_q_weight: torch.Tensor,
|
||||
b_bias: torch.Tensor | None,
|
||||
b_scales: torch.Tensor,
|
||||
a_scales: torch.Tensor | None,
|
||||
global_scale: torch.Tensor | None,
|
||||
b_zeros: torch.Tensor | None,
|
||||
workspace: torch.Tensor | None,
|
||||
b_q_type_id: int,
|
||||
size_m: int,
|
||||
size_n: int,
|
||||
size_k: int,
|
||||
use_atomic_add: bool = False,
|
||||
use_fp32_reduce: bool = False,
|
||||
is_zp_float: bool = False,
|
||||
) -> torch.Tensor:
|
||||
if workspace is None:
|
||||
workspace = get_marlin_workspace(a.device)
|
||||
return torch.ops._C.marlin_gemm(
|
||||
a,
|
||||
None,
|
||||
b_q_weight,
|
||||
b_bias,
|
||||
b_scales,
|
||||
a_scales,
|
||||
global_scale,
|
||||
b_zeros,
|
||||
workspace,
|
||||
b_q_type_id,
|
||||
size_m,
|
||||
size_n,
|
||||
size_k,
|
||||
use_atomic_add,
|
||||
use_fp32_reduce,
|
||||
is_zp_float,
|
||||
)
|
||||
|
||||
|
||||
def _marlin_gemm_fake(
|
||||
a: torch.Tensor,
|
||||
b_q_weight: torch.Tensor,
|
||||
b_bias: torch.Tensor | None,
|
||||
b_scales: torch.Tensor,
|
||||
a_scales: torch.Tensor | None,
|
||||
global_scale: torch.Tensor | None,
|
||||
b_zeros: torch.Tensor | None,
|
||||
workspace: torch.Tensor | None,
|
||||
b_q_type_id: int,
|
||||
size_m: int,
|
||||
size_n: int,
|
||||
size_k: int,
|
||||
use_atomic_add: bool = False,
|
||||
use_fp32_reduce: bool = False,
|
||||
is_zp_float: bool = False,
|
||||
) -> torch.Tensor:
|
||||
dtype = a.dtype if a.dtype in (torch.float16, torch.bfloat16) else b_scales.dtype
|
||||
return torch.empty((size_m, size_n), device=a.device, dtype=dtype)
|
||||
|
||||
|
||||
direct_register_custom_op("marlin_gemm", _marlin_gemm, fake_impl=_marlin_gemm_fake)
|
||||
|
||||
|
||||
def marlin_repeat_scales_on_all_ranks(group_size: int, is_row_parallel: bool) -> bool:
|
||||
@@ -686,7 +754,7 @@ def apply_gptq_marlin_linear(
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
weight_zp: torch.Tensor,
|
||||
workspace: torch.Tensor,
|
||||
workspace: torch.Tensor | None,
|
||||
wtype: ScalarType,
|
||||
output_size_per_partition: int,
|
||||
input_size_per_partition: int,
|
||||
@@ -723,9 +791,8 @@ def apply_gptq_marlin_linear(
|
||||
|
||||
reshaped_x, a_scales = marlin_quant_input(reshaped_x, input_dtype)
|
||||
|
||||
output = ops.marlin_gemm(
|
||||
output = torch.ops.vllm.marlin_gemm(
|
||||
reshaped_x,
|
||||
None,
|
||||
weight,
|
||||
bias,
|
||||
weight_scale,
|
||||
@@ -733,7 +800,7 @@ def apply_gptq_marlin_linear(
|
||||
None,
|
||||
weight_zp,
|
||||
workspace,
|
||||
wtype,
|
||||
wtype.id,
|
||||
size_m=reshaped_x.shape[0],
|
||||
size_n=padded_n,
|
||||
size_k=padded_k,
|
||||
|
||||
@@ -10,7 +10,6 @@ from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
USE_FP32_REDUCE_DEFAULT,
|
||||
get_marlin_input_dtype,
|
||||
marlin_make_workspace_new,
|
||||
marlin_pad_dim,
|
||||
marlin_pad_qweight,
|
||||
marlin_pad_scales,
|
||||
@@ -161,7 +160,7 @@ def apply_fp4_marlin_linear(
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
weight_global_scale: torch.Tensor | None,
|
||||
workspace: torch.Tensor,
|
||||
workspace: torch.Tensor | None,
|
||||
size_n: int,
|
||||
size_k: int,
|
||||
bias: torch.Tensor | None = None,
|
||||
@@ -196,9 +195,8 @@ def apply_fp4_marlin_linear(
|
||||
|
||||
inputs, a_scales = marlin_quant_input(inputs, torch.float8_e4m3fn)
|
||||
|
||||
output = ops.marlin_gemm(
|
||||
output = torch.ops.vllm.marlin_gemm(
|
||||
a=inputs,
|
||||
c=None,
|
||||
b_q_weight=weight,
|
||||
b_bias=bias,
|
||||
b_scales=weight_scale,
|
||||
@@ -206,7 +204,7 @@ def apply_fp4_marlin_linear(
|
||||
global_scale=weight_global_scale,
|
||||
b_zeros=None,
|
||||
workspace=workspace,
|
||||
b_q_type=scalar_types.float4_e2m1f,
|
||||
b_q_type_id=scalar_types.float4_e2m1f.id,
|
||||
size_m=reshaped_x.size(0),
|
||||
size_n=padded_n,
|
||||
size_k=padded_k,
|
||||
@@ -237,13 +235,6 @@ def prepare_fp4_layer_for_marlin(
|
||||
|
||||
assert layer.weight.shape == (part_size_n, part_size_k // 2)
|
||||
|
||||
device = layer.weight.device
|
||||
|
||||
# WORKSPACE
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
|
||||
# WEIGHT
|
||||
# Repack weights to marlin format
|
||||
qweight = layer.weight.view(torch.int32).T.contiguous()
|
||||
@@ -389,15 +380,9 @@ def prepare_nvfp4_moe_layer_for_marlin(
|
||||
return x
|
||||
return torch.nn.functional.pad(x, (0, (padded_N - N) // packing))
|
||||
|
||||
device = w13.device
|
||||
param_dtype = layer.params_dtype
|
||||
is_a_8bit = input_dtype is not None and input_dtype.itemsize == 1
|
||||
|
||||
# WORKSPACE
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, 4, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
|
||||
# WEIGHT
|
||||
# Repack weights to marlin format
|
||||
def repack_weight(weight: torch.Tensor, name: str) -> torch.Tensor:
|
||||
@@ -481,12 +466,7 @@ def prepare_moe_fp4_layer_for_marlin(
|
||||
k = layer.moe_config.hidden_dim
|
||||
n = layer.moe_config.intermediate_size_per_partition
|
||||
|
||||
# WORKSPACE
|
||||
device = layer.w13_weight.device
|
||||
param_dtype = layer.params_dtype
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, 4, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
is_a_8bit = input_dtype is not None and input_dtype.itemsize == 1
|
||||
|
||||
# WEIGHT
|
||||
|
||||
@@ -9,7 +9,6 @@ from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.quantization.utils.marlin_utils import (
|
||||
USE_FP32_REDUCE_DEFAULT,
|
||||
get_marlin_input_dtype,
|
||||
marlin_make_workspace_new,
|
||||
marlin_moe_padded_intermediate,
|
||||
marlin_pad_dim,
|
||||
marlin_pad_qweight,
|
||||
@@ -50,7 +49,7 @@ def apply_fp8_marlin_linear(
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
workspace: torch.Tensor,
|
||||
workspace: torch.Tensor | None,
|
||||
size_n: int,
|
||||
size_k: int,
|
||||
bias: torch.Tensor | None,
|
||||
@@ -80,9 +79,8 @@ def apply_fp8_marlin_linear(
|
||||
# inputs, a_scales = marlin_quant_input(inputs, torch.float8_e4m3fn)
|
||||
raise RuntimeError("Marlin W8A8 is not supported.")
|
||||
|
||||
output = ops.marlin_gemm(
|
||||
output = torch.ops.vllm.marlin_gemm(
|
||||
a=inputs,
|
||||
c=None,
|
||||
b_q_weight=weight,
|
||||
b_bias=bias,
|
||||
b_scales=weight_scale,
|
||||
@@ -90,7 +88,7 @@ def apply_fp8_marlin_linear(
|
||||
global_scale=None,
|
||||
b_zeros=None,
|
||||
workspace=workspace,
|
||||
b_q_type=scalar_types.float8_e4m3fn,
|
||||
b_q_type_id=scalar_types.float8_e4m3fn.id,
|
||||
size_m=reshaped_x.size(0),
|
||||
size_n=padded_n,
|
||||
size_k=padded_k,
|
||||
@@ -127,13 +125,6 @@ def prepare_fp8_layer_for_marlin(
|
||||
else:
|
||||
assert layer.weight.shape == (part_size_n, part_size_k)
|
||||
|
||||
device = layer.weight.device
|
||||
|
||||
# WORKSPACE
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
|
||||
# WEIGHT
|
||||
# Repack weights to marlin format
|
||||
qweight = pack_fp8_to_int32(layer.weight, size_k_first)
|
||||
@@ -241,12 +232,7 @@ def prepare_fp8_moe_layer_for_marlin(
|
||||
w13_weight_scale: torch.Tensor,
|
||||
w2_weight_scale: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Shuffle weights and scales into marlin format.
|
||||
|
||||
Note that this function has the side effect of adding a `workspace`
|
||||
attribute to the layer. This `workspace` does not need to be
|
||||
registered as a Parameter as it is not used during weight reloading.
|
||||
"""
|
||||
"""Shuffle weights and scales into marlin format."""
|
||||
logger.warning_once(
|
||||
"Your GPU does not have native support for FP8 computation but "
|
||||
"FP8 quantization is being used. Weight-only FP8 compression will "
|
||||
@@ -272,12 +258,6 @@ def prepare_fp8_moe_layer_for_marlin(
|
||||
w13_weight = _moe_pad_shard_rows(w13_weight, n, padded_n)
|
||||
w2_weight = _moe_pad_last(w2_weight, n, padded_n)
|
||||
|
||||
# WORKSPACE
|
||||
device = layer.w13_weight.device
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, 4, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
|
||||
# WEIGHT
|
||||
# Repack weights to marlin format
|
||||
def repack_weight(name: str, weight: torch.Tensor) -> torch.Tensor:
|
||||
@@ -396,7 +376,7 @@ def apply_mxfp8_marlin_linear(
|
||||
input: torch.Tensor,
|
||||
weight: torch.Tensor,
|
||||
weight_scale: torch.Tensor,
|
||||
workspace: torch.Tensor,
|
||||
workspace: torch.Tensor | None,
|
||||
size_n: int,
|
||||
size_k: int,
|
||||
bias: torch.Tensor | None = None,
|
||||
@@ -416,9 +396,8 @@ def apply_mxfp8_marlin_linear(
|
||||
dtype=input.dtype,
|
||||
)
|
||||
|
||||
output = ops.marlin_gemm(
|
||||
output = torch.ops.vllm.marlin_gemm(
|
||||
a=reshaped_x,
|
||||
c=None,
|
||||
b_q_weight=weight,
|
||||
b_bias=bias,
|
||||
b_scales=weight_scale,
|
||||
@@ -426,7 +405,7 @@ def apply_mxfp8_marlin_linear(
|
||||
global_scale=None,
|
||||
b_zeros=None,
|
||||
workspace=workspace,
|
||||
b_q_type=scalar_types.float8_e4m3fn,
|
||||
b_q_type_id=scalar_types.float8_e4m3fn.id,
|
||||
size_m=reshaped_x.size(0),
|
||||
size_n=padded_n,
|
||||
size_k=padded_k,
|
||||
@@ -451,13 +430,6 @@ def prepare_mxfp8_layer_for_marlin(layer: torch.nn.Module) -> None:
|
||||
group_size = 32 # MX standard block size
|
||||
padded_n, padded_k = marlin_padded_nk(part_size_n, part_size_k, group_size)
|
||||
|
||||
device = layer.weight.device
|
||||
|
||||
# WORKSPACE
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
|
||||
# WEIGHT - repack FP8 weights to Marlin format
|
||||
qweight = pack_fp8_to_int32(layer.weight, size_k_first=False)
|
||||
qweight = qweight.T.contiguous()
|
||||
@@ -512,7 +484,7 @@ def prepare_mxfp8_moe_layer_for_marlin(
|
||||
"""Repack MXFP8 MoE weights and scales into Marlin kernel format.
|
||||
|
||||
Args:
|
||||
layer: MoE layer (used to read params_dtype and attach workspace).
|
||||
layer: MoE layer.
|
||||
w13: [E, 2*N, K] float8_e4m3fn weights.
|
||||
w2: [E, K, N] float8_e4m3fn weights.
|
||||
w13_scale: [E, 2*N, K//32] uint8 e8m0 scales.
|
||||
@@ -537,13 +509,8 @@ def prepare_mxfp8_moe_layer_for_marlin(
|
||||
n = padded_n
|
||||
w13_n = w13.shape[1]
|
||||
|
||||
device = w13.device
|
||||
param_dtype = torch.get_default_dtype()
|
||||
|
||||
layer.workspace = marlin_make_workspace_new(
|
||||
device, 4, existing=getattr(layer, "workspace", None)
|
||||
)
|
||||
|
||||
def repack_weight(weight: torch.Tensor, name: str) -> torch.Tensor:
|
||||
if "w13" in name:
|
||||
size_n, size_k = w13_n, k
|
||||
|
||||
+75
-21
@@ -3,11 +3,12 @@
|
||||
|
||||
import inspect
|
||||
import os
|
||||
from collections.abc import Iterator
|
||||
from collections.abc import Callable, Hashable, Iterator
|
||||
from contextlib import contextmanager
|
||||
from contextvars import ContextVar
|
||||
from itertools import accumulate
|
||||
from math import prod
|
||||
from typing import TypeVar, cast
|
||||
|
||||
import torch
|
||||
|
||||
@@ -17,6 +18,7 @@ from vllm.utils.math_utils import round_up
|
||||
from vllm.v1.worker.ubatching import dbo_current_ubatch_id
|
||||
|
||||
logger = init_logger(__name__)
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def _compute_bytes(shape: tuple[int, ...], dtype: torch.dtype) -> int:
|
||||
@@ -47,8 +49,8 @@ def use_workspace_lane(lane: int) -> Iterator[None]:
|
||||
class WorkspaceManager:
|
||||
"""Manager for workspace allocation.
|
||||
|
||||
Manages one workspace buffer per active ``(ubatch, lane)`` slot.
|
||||
Can be locked to prevent further growth during execution.
|
||||
Manages shared scratch and keyed persistent resources per ``(ubatch, lane)``.
|
||||
Can be locked to prevent growth or new resources during execution.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -66,6 +68,9 @@ class WorkspaceManager:
|
||||
self._current_workspaces: list[torch.Tensor | None] = [None] * (
|
||||
self._num_ubatches * self._num_lanes
|
||||
)
|
||||
self._persistent_resources: list[dict[Hashable, object]] = [
|
||||
{} for _ in self._current_workspaces
|
||||
]
|
||||
self._locked: bool = False
|
||||
|
||||
@staticmethod
|
||||
@@ -76,10 +81,10 @@ class WorkspaceManager:
|
||||
return workspace.numel() * workspace.element_size()
|
||||
|
||||
def lock(self) -> None:
|
||||
"""Lock the workspace to prevent further growth.
|
||||
"""Lock the workspace to prevent growth or new persistent resources.
|
||||
|
||||
After locking, any attempt to allocate a larger workspace will raise
|
||||
an assertion error. This ensures workspace size is fixed during execution.
|
||||
Larger scratch requests and new persistent resources raise an assertion
|
||||
error. Existing resources retain their storage throughout execution.
|
||||
"""
|
||||
self._locked = True
|
||||
if envs.VLLM_DEBUG_WORKSPACE:
|
||||
@@ -113,6 +118,63 @@ class WorkspaceManager:
|
||||
"""Check if workspace is locked."""
|
||||
return self._locked
|
||||
|
||||
def _get_workspace_id(self) -> int:
|
||||
lane = _workspace_lane.get()
|
||||
if lane >= self._num_lanes:
|
||||
raise RuntimeError(
|
||||
f"Workspace lane {lane} is not configured; manager has "
|
||||
f"{self._num_lanes} lane(s)."
|
||||
)
|
||||
return dbo_current_ubatch_id() * self._num_lanes + lane
|
||||
|
||||
def get_persistent_resource(self, key: Hashable, factory: Callable[[], T]) -> T:
|
||||
"""Return the resource cached for ``key`` in the current ubatch and lane.
|
||||
|
||||
The first request calls ``factory`` and stores its result for the lifetime
|
||||
of this manager. Later requests return the same object without calling
|
||||
the factory again. Each ubatch/lane pair has a separate cache. Once the
|
||||
manager is locked, requesting a key missing from that cache raises an
|
||||
AssertionError.
|
||||
|
||||
Keys must identify the resource type and configuration. Callers must not
|
||||
resize or replace tensor storage held by a cached resource. Uses of the
|
||||
same resource must not overlap; include the CUDA stream in the key when
|
||||
streams can execute concurrently.
|
||||
|
||||
With torch.compile, call this inside a runtime custom op so resource
|
||||
selection happens during execution.
|
||||
"""
|
||||
resources = self._persistent_resources[self._get_workspace_id()]
|
||||
if key not in resources:
|
||||
if self._locked:
|
||||
raise AssertionError(
|
||||
f"Workspace is locked but {key!r} was not allocated during "
|
||||
"warmup for the current ubatch and lane."
|
||||
)
|
||||
resources[key] = factory()
|
||||
return cast(T, resources[key])
|
||||
|
||||
def get_persistent(
|
||||
self,
|
||||
key: Hashable,
|
||||
shape: tuple[int, ...],
|
||||
dtype: torch.dtype,
|
||||
*,
|
||||
zero_init: bool = False,
|
||||
) -> torch.Tensor:
|
||||
"""Get a fixed-shape tensor, optionally zeroed on first allocation only."""
|
||||
factory = torch.zeros if zero_init else torch.empty
|
||||
workspace = self.get_persistent_resource(
|
||||
(torch.Tensor, key),
|
||||
lambda: factory(shape, dtype=dtype, device=self._device),
|
||||
)
|
||||
if workspace.shape != shape or workspace.dtype != dtype:
|
||||
raise ValueError(
|
||||
f"Persistent workspace {key!r} has shape={workspace.shape}, "
|
||||
f"dtype={workspace.dtype}; requested shape={shape}, dtype={dtype}."
|
||||
)
|
||||
return workspace
|
||||
|
||||
def get_simultaneous(
|
||||
self, *shapes_and_dtypes: tuple[tuple[int, ...], torch.dtype]
|
||||
) -> list[torch.Tensor]:
|
||||
@@ -151,14 +213,7 @@ class WorkspaceManager:
|
||||
The current workspace tensor.
|
||||
|
||||
"""
|
||||
ubatch_id = dbo_current_ubatch_id()
|
||||
lane = _workspace_lane.get()
|
||||
if lane >= self._num_lanes:
|
||||
raise RuntimeError(
|
||||
f"Workspace lane {lane} is not configured; manager has "
|
||||
f"{self._num_lanes} lane(s)."
|
||||
)
|
||||
workspace_id = ubatch_id * self._num_lanes + lane
|
||||
workspace_id = self._get_workspace_id()
|
||||
current_workspace = self._current_workspaces[workspace_id]
|
||||
current_size = self._workspace_size_bytes(current_workspace)
|
||||
|
||||
@@ -218,8 +273,8 @@ class WorkspaceManager:
|
||||
get_caller_info(),
|
||||
current_size / _MB,
|
||||
required_bytes / _MB,
|
||||
ubatch_id,
|
||||
lane,
|
||||
workspace_id // self._num_lanes,
|
||||
workspace_id % self._num_lanes,
|
||||
)
|
||||
|
||||
return current_workspace
|
||||
@@ -277,12 +332,11 @@ def init_workspace_manager(
|
||||
|
||||
|
||||
def lock_workspace() -> None:
|
||||
"""Lock the workspace to prevent further growth.
|
||||
"""Lock the workspace to prevent growth or new persistent resources.
|
||||
|
||||
After calling this function, any attempt to allocate a workspace larger
|
||||
than the current size will raise an AssertionError. This ensures that
|
||||
workspace size is fixed during execution and prevents unexpected memory
|
||||
allocations in the hot path.
|
||||
Larger scratch requests and new persistent resources raise an AssertionError.
|
||||
All required resources must be initialized in each execution slot during
|
||||
warmup, so their storage remains fixed during execution.
|
||||
|
||||
Example:
|
||||
# During initialization
|
||||
|
||||
Reference in New Issue
Block a user