Fix async loading of RunAI-streamed tensors (#32896)
Co-authored-by: Danila Shtan <dan@nebius.com>
This commit is contained in:
co-authored by
Danila Shtan
parent
690de097c4
commit
5f9b0db18c
@@ -285,7 +285,13 @@ def should_async_load(weight: torch.Tensor) -> bool:
|
|||||||
For host (CPU) tensors, using a threadpool can overlap H2D copies
|
For host (CPU) tensors, using a threadpool can overlap H2D copies
|
||||||
and improve throughput. For device tensors, threading often adds overhead
|
and improve throughput. For device tensors, threading often adds overhead
|
||||||
(e.g., GIL contention) without benefit, so we do it synchronously.
|
(e.g., GIL contention) without benefit, so we do it synchronously.
|
||||||
|
|
||||||
|
RunAI-streamed tensors are zero-copy views into a reused CPU buffer. They
|
||||||
|
must be consumed synchronously before the streamer fills its next batch.
|
||||||
"""
|
"""
|
||||||
|
if getattr(weight, "_sglang_runai_streamer_tensor", False):
|
||||||
|
return False
|
||||||
|
|
||||||
device = getattr(weight, "device", None)
|
device = getattr(weight, "device", None)
|
||||||
if device is None:
|
if device is None:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import concurrent.futures
|
||||||
import sys
|
import sys
|
||||||
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@@ -7,6 +9,7 @@ from unittest.mock import patch
|
|||||||
import torch
|
import torch
|
||||||
|
|
||||||
import sglang.srt.model_loader.loader as loader_mod
|
import sglang.srt.model_loader.loader as loader_mod
|
||||||
|
import sglang.srt.model_loader.utils as model_loader_utils
|
||||||
import sglang.srt.model_loader.weight_utils as weight_utils
|
import sglang.srt.model_loader.weight_utils as weight_utils
|
||||||
from sglang.srt.configs.device_config import DeviceConfig
|
from sglang.srt.configs.device_config import DeviceConfig
|
||||||
from sglang.srt.configs.load_config import LoadConfig, LoadFormat
|
from sglang.srt.configs.load_config import LoadConfig, LoadFormat
|
||||||
@@ -110,6 +113,41 @@ class TestRunaiModelStreamerLoader(CustomTestCase):
|
|||||||
marked.fill_(2)
|
marked.fill_(2)
|
||||||
self.assertEqual(cloned.item(), 1)
|
self.assertEqual(cloned.item(), 1)
|
||||||
|
|
||||||
|
def test_runai_streamed_tensor_is_consumed_before_buffer_reuse(self):
|
||||||
|
def consume_view(mark_as_runai: bool):
|
||||||
|
shared_buffer = torch.tensor([1], dtype=torch.int32)
|
||||||
|
view = shared_buffer[:]
|
||||||
|
if mark_as_runai:
|
||||||
|
setattr(view, weight_utils.RUNAI_STREAMER_TENSOR_ATTR, True)
|
||||||
|
|
||||||
|
release_worker = threading.Event()
|
||||||
|
observed = []
|
||||||
|
futures = []
|
||||||
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
|
# Keep the sole worker busy so an async consumer cannot read the
|
||||||
|
# zero-copy view until after the simulated streamer buffer reuse.
|
||||||
|
blocker = executor.submit(release_worker.wait)
|
||||||
|
model_loader_utils.maybe_executor_submit(
|
||||||
|
executor=executor,
|
||||||
|
futures=futures,
|
||||||
|
use_async=model_loader_utils.should_async_load(view),
|
||||||
|
func=lambda tensor: observed.append(tensor.item()),
|
||||||
|
func_args=(view,),
|
||||||
|
)
|
||||||
|
shared_buffer.fill_(2)
|
||||||
|
release_worker.set()
|
||||||
|
blocker.result()
|
||||||
|
for future in futures:
|
||||||
|
future.result()
|
||||||
|
|
||||||
|
return observed, len(futures)
|
||||||
|
|
||||||
|
# The control demonstrates the race: an async consumer observes the
|
||||||
|
# overwritten buffer rather than the value present when it was queued.
|
||||||
|
self.assertEqual(consume_view(mark_as_runai=False), ([2], 1))
|
||||||
|
# A RunAI-tagged view is consumed inline before the buffer is reused.
|
||||||
|
self.assertEqual(consume_view(mark_as_runai=True), ([1], 0))
|
||||||
|
|
||||||
def test_deepseek_v4_streaming_dequant_fp8_wo_a_pairs_weight_and_scale(self):
|
def test_deepseek_v4_streaming_dequant_fp8_wo_a_pairs_weight_and_scale(self):
|
||||||
weight = torch.eye(128, dtype=torch.float32).to(torch.float8_e4m3fn)
|
weight = torch.eye(128, dtype=torch.float32).to(torch.float8_e4m3fn)
|
||||||
scale = torch.ones((1, 1), dtype=torch.float32)
|
scale = torch.ones((1, 1), dtype=torch.float32)
|
||||||
|
|||||||
Reference in New Issue
Block a user