94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
233 lines
9.4 KiB
Python
233 lines
9.4 KiB
Python
"""Unit tests for OTel trace-context propagation across the diffusion disagg
|
|
JSON hop (encoder -> denoiser, denoiser -> decoder).
|
|
|
|
These exercise the serialization contract only (no GPUs, no server, no OTLP
|
|
collector required):
|
|
|
|
- ``extract_transfer_fields`` emits a JSON-safe ``_trace_state`` (W3C carrier)
|
|
when tracing is enabled, and omits it when tracing is disabled. It never
|
|
serializes the live ``TraceReqContext`` object itself.
|
|
- The ``_trace_state`` payload round-trips through ``codec.pack_tensors``
|
|
(the same ``json.dumps`` path the RDMA metadata frame uses).
|
|
- ``TraceReqContext.__setstate__`` reconstructs a live, ``is_copy=True``
|
|
context whose ``root_span_context`` is an OTel ``Context`` object.
|
|
- ``_build_disagg_req`` pops ``_trace_state`` and installs a rebuilt
|
|
``TraceReqContext`` on the receiver-side Req.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import unittest
|
|
from contextlib import contextmanager
|
|
from types import SimpleNamespace
|
|
|
|
import torch
|
|
|
|
from sglang.multimodal_gen.runtime.disaggregation.scheduler_mixin import (
|
|
SchedulerDisaggMixin,
|
|
extract_transfer_fields,
|
|
)
|
|
from sglang.multimodal_gen.runtime.disaggregation.transport.codec import pack_tensors
|
|
from sglang.multimodal_gen.runtime.pipelines_core import Req
|
|
from sglang.srt import server_args as srt_server_args_module
|
|
from sglang.srt.observability import trace as srt_trace
|
|
from sglang.srt.observability.trace import TraceNullContext, TraceReqContext
|
|
from sglang.srt.runtime_context import reset_context
|
|
from sglang.srt.server_args import set_global_server_args_for_scheduler
|
|
|
|
try:
|
|
from opentelemetry import propagate as otel_propagate
|
|
from opentelemetry import trace as otel_trace
|
|
from opentelemetry.sdk.trace import TracerProvider
|
|
|
|
_OTEL_AVAILABLE = True
|
|
except ImportError:
|
|
_OTEL_AVAILABLE = False
|
|
|
|
|
|
_OTEL_BOOTSTRAPPED = False
|
|
|
|
|
|
def _enable_minimal_otel() -> None:
|
|
"""Bootstrap just enough OTel state for TraceReqContext to produce real
|
|
spans. Idempotent — the TracerProvider can only be set once per process."""
|
|
global _OTEL_BOOTSTRAPPED
|
|
if not _OTEL_BOOTSTRAPPED:
|
|
otel_trace.set_tracer_provider(TracerProvider())
|
|
_OTEL_BOOTSTRAPPED = True
|
|
srt_trace.opentelemetry_initialized = True
|
|
srt_trace.tracer = otel_trace.get_tracer("test-diffusion-disagg")
|
|
srt_trace.trace_set_thread_info("TestThread")
|
|
|
|
|
|
@contextmanager
|
|
def _srt_trace_server_args():
|
|
try:
|
|
prev_server_args = srt_server_args_module.get_global_server_args()
|
|
except ValueError: # nothing published yet
|
|
prev_server_args = None
|
|
set_global_server_args_for_scheduler(SimpleNamespace(trace_modules="request"))
|
|
try:
|
|
yield
|
|
finally:
|
|
if prev_server_args is None:
|
|
reset_context()
|
|
else:
|
|
set_global_server_args_for_scheduler(prev_server_args)
|
|
|
|
|
|
def _traceparent_from(ctx) -> str | None:
|
|
"""Re-inject a W3C carrier from an OTel Context and return the traceparent.
|
|
|
|
Used to assert that a carrier round-trip preserves trace_id/span_id, which
|
|
is the actual correctness property (OTel's Context is a dict subclass, so
|
|
``isinstance(ctx, dict)`` isn't useful).
|
|
"""
|
|
carrier: dict = {}
|
|
otel_propagate.inject(carrier, ctx)
|
|
return carrier.get("traceparent")
|
|
|
|
|
|
def _roundtrip_scalar_fields(scalar_fields: dict) -> dict:
|
|
"""Run the actual RDMA metadata codec path: pack -> json bytes -> decode."""
|
|
metadata_bytes, _ = pack_tensors({}, scalar_fields)
|
|
decoded = json.loads(metadata_bytes.decode("utf-8"))
|
|
return decoded["scalar_fields"]
|
|
|
|
|
|
class TestDisaggTracePropagation(unittest.TestCase):
|
|
def test_transfer_keeps_seed_needed_to_rebuild_generator(self):
|
|
req = Req(request_id="test-seed", prompt="x")
|
|
req.generator = torch.Generator(device="cpu").manual_seed(req.seed)
|
|
|
|
_, scalar_fields = extract_transfer_fields(req)
|
|
|
|
self.assertEqual(scalar_fields["seed"], 42)
|
|
|
|
rebuilt = SchedulerDisaggMixin._build_disagg_req(None, dict(scalar_fields), {})
|
|
self.assertIsInstance(rebuilt.generator, torch.Generator)
|
|
self.assertEqual(rebuilt.seed, 42)
|
|
|
|
expected = torch.rand(
|
|
(), generator=torch.Generator(device="cpu").manual_seed(42)
|
|
)
|
|
actual = torch.rand((), generator=rebuilt.generator)
|
|
self.assertEqual(actual.item(), expected.item())
|
|
|
|
def test_build_disagg_req_rebuilds_generator_list(self):
|
|
scalar_fields = {
|
|
"request_id": "test-seed-list",
|
|
"prompt": "x",
|
|
"num_outputs_per_prompt": 2,
|
|
"seed": [11, 12],
|
|
}
|
|
|
|
rebuilt = SchedulerDisaggMixin._build_disagg_req(None, dict(scalar_fields), {})
|
|
|
|
self.assertEqual(rebuilt.seed, [11, 12])
|
|
self.assertEqual(len(rebuilt.generator), 2)
|
|
for seed, generator in zip(rebuilt.seed, rebuilt.generator):
|
|
expected = torch.rand(
|
|
(), generator=torch.Generator(device="cpu").manual_seed(seed)
|
|
)
|
|
actual = torch.rand((), generator=generator)
|
|
self.assertEqual(actual.item(), expected.item())
|
|
|
|
def test_tracing_disabled_omits_trace_state(self):
|
|
"""With a default TraceNullContext Req, no _trace_state is emitted and
|
|
the JSON codec does not encounter any live OTel objects."""
|
|
req = Req(request_id="test-off", prompt="x")
|
|
self.assertIsInstance(req.trace_ctx, TraceNullContext)
|
|
|
|
_, scalar_fields = extract_transfer_fields(req)
|
|
self.assertNotIn("_trace_state", scalar_fields)
|
|
# trace_ctx must never ride the JSON scalar path.
|
|
self.assertNotIn("trace_ctx", scalar_fields)
|
|
# json.dumps must succeed (this is the path that pre-fix crashed).
|
|
_roundtrip_scalar_fields(scalar_fields)
|
|
|
|
@unittest.skipUnless(_OTEL_AVAILABLE, "opentelemetry SDK not installed")
|
|
def test_tracing_enabled_state_roundtrip(self):
|
|
"""Sender emits a W3C-carrier _trace_state, it round-trips through
|
|
json encode/decode, and __setstate__ reconstructs a live is_copy=True
|
|
TraceReqContext with an OTel Context (not the raw dict)."""
|
|
_enable_minimal_otel()
|
|
|
|
with _srt_trace_server_args():
|
|
ctx = TraceReqContext(rid="test-on", role="server", module_name="request")
|
|
ctx.trace_req_start()
|
|
self.assertTrue(ctx.tracing_enable)
|
|
self.assertFalse(ctx.is_copy)
|
|
|
|
req = Req(request_id="test-on", prompt="x")
|
|
req.trace_ctx = ctx
|
|
|
|
_, scalar_fields = extract_transfer_fields(req)
|
|
self.assertNotIn("trace_ctx", scalar_fields)
|
|
self.assertIn("_trace_state", scalar_fields)
|
|
state = scalar_fields["_trace_state"]
|
|
self.assertTrue(state.get("tracing_enable"))
|
|
# W3C carrier must be present so downstream roles can nest spans.
|
|
self.assertIn("traceparent", state.get("root_span_context", {}))
|
|
|
|
decoded = _roundtrip_scalar_fields(scalar_fields)
|
|
self.assertEqual(decoded["_trace_state"], state)
|
|
|
|
rebuilt = object.__new__(TraceReqContext)
|
|
rebuilt.__setstate__(decoded["_trace_state"])
|
|
self.assertTrue(rebuilt.tracing_enable)
|
|
self.assertTrue(rebuilt.is_copy)
|
|
# The sender's traceparent must survive into the rebuilt Context so
|
|
# downstream role spans nest under the original trace_id.
|
|
self.assertEqual(
|
|
_traceparent_from(rebuilt.root_span_context),
|
|
state["root_span_context"]["traceparent"],
|
|
)
|
|
ctx.trace_req_finish()
|
|
|
|
@unittest.skipUnless(_OTEL_AVAILABLE, "opentelemetry SDK not installed")
|
|
def test_build_disagg_req_installs_rebuilt_ctx(self):
|
|
"""_build_disagg_req pops _trace_state from scalar_fields and installs
|
|
a live TraceReqContext on the rebuilt Req; the key does not leak onto
|
|
the Req as a stray attribute."""
|
|
_enable_minimal_otel()
|
|
|
|
with _srt_trace_server_args():
|
|
ctx = TraceReqContext(rid="test-brq", role="server", module_name="request")
|
|
ctx.trace_req_start()
|
|
|
|
req = Req(request_id="test-brq", prompt="x")
|
|
req.trace_ctx = ctx
|
|
_, scalar_fields = extract_transfer_fields(req)
|
|
self.assertIn("_trace_state", scalar_fields)
|
|
|
|
# _build_disagg_req is an instance method but its body does not
|
|
# touch ``self``; call via __func__ to avoid needing a real Scheduler.
|
|
rebuilt = SchedulerDisaggMixin._build_disagg_req(
|
|
None, dict(scalar_fields), {}
|
|
)
|
|
|
|
self.assertIsInstance(rebuilt.trace_ctx, TraceReqContext)
|
|
self.assertTrue(rebuilt.trace_ctx.tracing_enable)
|
|
self.assertTrue(rebuilt.trace_ctx.is_copy)
|
|
self.assertFalse(hasattr(rebuilt, "_trace_state"))
|
|
ctx.trace_req_finish()
|
|
|
|
@unittest.skipUnless(_OTEL_AVAILABLE, "opentelemetry SDK not installed")
|
|
def test_build_disagg_req_falls_back_when_tracing_off(self):
|
|
"""If the sender's context is a TraceNullContext, the receiver's Req
|
|
keeps its default TraceNullContext (no _trace_state to apply)."""
|
|
req = Req(request_id="test-brq-off", prompt="x")
|
|
self.assertIsInstance(req.trace_ctx, TraceNullContext)
|
|
|
|
_, scalar_fields = extract_transfer_fields(req)
|
|
self.assertNotIn("_trace_state", scalar_fields)
|
|
|
|
rebuilt = SchedulerDisaggMixin._build_disagg_req(None, dict(scalar_fields), {})
|
|
self.assertIsInstance(rebuilt.trace_ctx, TraceNullContext)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|