Fix async loading of RunAI-streamed tensors (#32896)

Co-authored-by: Danila Shtan <dan@nebius.com>
This commit is contained in:
Danila Shtan
2026-07-31 21:46:33 +08:00
committed by GitHub
co-authored by Danila Shtan
parent 690de097c4
commit 5f9b0db18c
2 changed files with 44 additions and 0 deletions
+6
View File
@@ -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)