[ci][cu13] Bump torch_memory_saver to 0.0.9.post1; restore manual tests (#23182)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kangyan-Zhou
2026-05-01 22:50:38 -07:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 674a80d5d8
commit cd27baaffd
3 changed files with 1 additions and 11 deletions
@@ -1,178 +0,0 @@
import asyncio
import os
import unittest
import torch
import torch.distributed as dist
from torch.distributed.device_mesh import init_device_mesh
from transformers import AutoModelForCausalLM
from sglang.srt.entrypoints.engine import Engine
from sglang.srt.weight_sync.utils import update_weights
from sglang.test.ci.ci_register import register_cuda_ci
from sglang.test.test_utils import DEFAULT_SMALL_MODEL_NAME_FOR_TEST
register_cuda_ci(est_time=32, suite="stage-b-test-1-gpu-large")
"""
# TODO: torch_memory_saver wheel is built against libcudart.so.12, fails to LD_PRELOAD in Cu13 venv. Ref: https://github.com/sgl-project/sglang/actions/runs/24604424372/job/71968573867
# Should move back to registered test after it's fixed
"""
class AsyncEngine(Engine):
def __init__(self, **kwargs):
super().__init__(**kwargs)
async def update_weights_from_tensor(self, update_weights_request):
return await self.tokenizer_manager.update_weights_from_tensor(
update_weights_request, None
)
def is_distributed_available():
"""Check if distributed training environment is available"""
required_vars = ["RANK", "WORLD_SIZE", "MASTER_ADDR", "MASTER_PORT"]
return all(var in os.environ for var in required_vars)
def setup_single_process_distributed():
"""Setup distributed environment for single process testing"""
if not is_distributed_available():
os.environ["RANK"] = "0"
os.environ["WORLD_SIZE"] = "1"
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "12356"
os.environ["LOCAL_RANK"] = "0"
class TestUtilsUpdateWeights(unittest.TestCase):
"""Test class for utils.update_weights function"""
@classmethod
def setUpClass(cls):
"""Setup distributed environment and test fixtures for the entire test class"""
cls.setup_distributed()
cls.setup_test_engine()
cls.setup_test_model()
cls.setup_device_mesh()
@classmethod
def tearDownClass(cls):
"""Cleanup after all tests"""
if hasattr(cls, "engine") and cls.engine:
cls.engine.shutdown()
# Cleanup distributed
if dist.is_initialized():
dist.destroy_process_group()
@classmethod
def setup_distributed(cls):
"""Setup distributed environment for testing"""
setup_single_process_distributed()
if not dist.is_initialized():
try:
dist.init_process_group(
backend="nccl" if torch.cuda.is_available() else "gloo"
)
except Exception as e:
raise unittest.SkipTest(
f"Could not initialize distributed backend: {e}"
)
cls.rank = dist.get_rank()
cls.world_size = dist.get_world_size()
if torch.cuda.is_available():
torch.cuda.set_device(cls.rank % torch.cuda.device_count())
# Set up environment variables
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
os.environ["NCCL_CUMEM_ENABLE"] = "0"
os.environ["CUDA_DEVICE_MAX_CONNECTIONS"] = "4"
os.environ["CUDA_MODULE_LOADING"] = "AUTO"
@classmethod
def setup_test_engine(cls):
"""Setup test engine"""
if cls.rank == 0:
cls.engine = AsyncEngine(
model_path=DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
dtype="bfloat16",
mem_fraction_static=0.3,
enable_memory_saver=True,
tp_size=cls.world_size,
disable_cuda_graph=False,
)
else:
cls.engine = None
@classmethod
def setup_test_model(cls):
"""Load test model"""
try:
cls.model = AutoModelForCausalLM.from_pretrained(
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
device_map="cpu",
trust_remote_code=True,
low_cpu_mem_usage=True,
torch_dtype=(
torch.float16 if torch.cuda.is_available() else torch.float32
),
)
except Exception as e:
raise unittest.SkipTest(f"Could not load test model: {e}")
@classmethod
def setup_device_mesh(cls):
"""Create device mesh for testing"""
if not torch.cuda.is_available():
raise unittest.SkipTest("CUDA not available for device mesh")
cls.device_mesh_key = "tp"
cls.mesh = init_device_mesh(
"cuda", (cls.world_size,), mesh_dim_names=(cls.device_mesh_key,)
)
def create_test_params_batch(self, model, num_params=64):
"""Create a batch of test parameters from the model"""
param_names = []
test_tensors = []
# Get first few parameters from the model for testing
for i, (name, tensor) in enumerate(model.named_parameters()):
if i >= num_params:
break
param_names.append(name)
# Create test tensor with known values, matching original shape and dtype
test_tensor = torch.full_like(tensor, 1.5, dtype=tensor.dtype).cuda()
test_tensors.append(test_tensor)
return list(zip(param_names, test_tensors))
def test_utils_update_weights(self):
"""Test basic functionality of utils.update_weights"""
async def async_test():
# Create test parameters batch
params_batch = self.create_test_params_batch(self.model, num_params=2)
# Test the utils.update_weights function
result = await update_weights(
engine=self.engine,
params_batch=params_batch,
device_mesh_key=self.device_mesh_key,
device_mesh=self.mesh,
load_format=None,
)
self.assertIn("Success", result)
# Run the async test
asyncio.run(async_test())
if __name__ == "__main__":
unittest.main()
@@ -1,279 +0,0 @@
"""
# TODO: Fails on cu13 venv migration. Ref: https://github.com/sgl-project/sglang/actions/runs/24616960626/job/71980705675?pr=23119
# Should move back to registered test after it's fixed
"""
import gc
import multiprocessing
import os
import traceback
import unittest
from multiprocessing import Process
from typing import Iterable, Tuple
import torch
import torch.distributed as dist
from torch.distributed.device_mesh import init_device_mesh
from transformers import AutoModelForCausalLM
from sglang.srt.entrypoints.engine import Engine as SglangEngine
from sglang.test.ci.ci_register import register_amd_ci, register_cuda_ci
from sglang.test.test_utils import (
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
DEFAULT_SMALL_MODEL_NAME_FOR_TEST_BASE,
CustomTestCase,
find_available_port,
)
register_cuda_ci(est_time=57, suite="stage-c-test-4-gpu-h100")
register_amd_ci(
est_time=64,
suite="stage-c-test-4-gpu-amd",
disabled="torch_memory_saver incompatible with ROCm (libcuda.so.1 not found)",
)
TEST_SUITE = dict(
model_path=DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
mem_fraction_static=0.83,
dp_size=2,
tp_size=2,
)
# Minimum expected memory change in MB for each operation.
# Llama-3.2-1B bf16 is ~2GB total, ~1GB per TP rank.
# KV cache with mem_fraction_static=0.83 is much larger.
MIN_DELTA_MB = 200
class EngineWrapper:
"""
A wrapper around Sglang engine to mock multi instance cases such as RL training.
"""
def __init__(
self, model_path, random_seed, mem_fraction_static, device_mesh_cpu, base_gpu_id
):
self._device_mesh_cpu = device_mesh_cpu
self._tp_rank = device_mesh_cpu.get_local_rank()
self._rank = device_mesh_cpu.get_rank()
self._tp_size = device_mesh_cpu.size()
tp_size_per_node = self._tp_size
node_rank = self._tp_rank // tp_size_per_node
first_rank_in_node = self._tp_rank % tp_size_per_node == 0
engine_kwargs = dict(
model_path=model_path,
random_seed=random_seed,
mem_fraction_static=mem_fraction_static,
base_gpu_id=base_gpu_id,
enable_memory_saver=True,
tp_size=self._tp_size,
node_rank=node_rank,
nnodes=1,
)
self._engine = None
if first_rank_in_node:
os.environ["SGLANG_BLOCK_NONZERO_RANK_CHILDREN"] = "0"
self._engine = SglangEngine(**engine_kwargs)
dist.barrier(group=self._device_mesh_cpu.get_group())
def update_weights_from_tensor(
self, named_tensors: Iterable[Tuple[str, torch.Tensor]]
):
if self._tp_rank == 0:
self._engine.update_weights_from_tensor(list(named_tensors))
self._engine.flush_cache()
dist.barrier(group=self._device_mesh_cpu.get_group())
def release_memory_occupation(self, tags):
if self._tp_rank == 0:
self._engine.release_memory_occupation(tags)
dist.barrier(group=self._device_mesh_cpu.get_group())
def resume_memory_occupation(self, tags):
if self._tp_rank == 0:
self._engine.resume_memory_occupation(tags)
dist.barrier(group=self._device_mesh_cpu.get_group())
def shutdown(self):
if self._tp_rank == 0:
self._engine.shutdown()
dist.barrier(group=self._device_mesh_cpu.get_group())
def get_gpu_memory_mb(device_id: int) -> float:
"""Return device-level GPU memory used in MB."""
free, total = torch.cuda.mem_get_info(device_id)
return (total - free) / (1024**2)
def assert_memory_decreased(before_mb, after_mb, step_name):
delta = before_mb - after_mb
assert delta > MIN_DELTA_MB, (
f"[{step_name}] Expected memory decrease > {MIN_DELTA_MB} MB, "
f"got delta={delta:.0f} MB (before={before_mb:.0f}, after={after_mb:.0f})"
)
def assert_memory_increased(before_mb, after_mb, step_name):
delta = after_mb - before_mb
assert delta > MIN_DELTA_MB, (
f"[{step_name}] Expected memory increase > {MIN_DELTA_MB} MB, "
f"got delta={delta:.0f} MB (before={before_mb:.0f}, after={after_mb:.0f})"
)
class TestMultiInstanceReleaseMemoryOccupation(CustomTestCase):
@classmethod
def setUpClass(cls):
multiprocessing.set_start_method("spawn")
def test_multi_instance_release_memory_occupation(self):
master_port = find_available_port(23456)
dp_size = TEST_SUITE["dp_size"]
tp_size = TEST_SUITE["tp_size"]
world_size = dp_size * tp_size
processes = []
output_reader, output_writer = multiprocessing.Pipe(duplex=False)
for rank in range(world_size):
p = Process(
target=_run_sglang_subprocess,
kwargs=dict(
rank=rank,
dp_size=dp_size,
tp_size=tp_size,
model_path=TEST_SUITE["model_path"],
master_port=master_port,
output_writer=output_writer,
mem_fraction_static=TEST_SUITE["mem_fraction_static"],
),
)
p.start()
processes.append(p)
for _ in range(world_size):
self.assertTrue(
output_reader.recv(), f"Subprocess fail. Check the logs above."
)
for p in processes:
p.join()
def _run_sglang_subprocess(
rank: int,
dp_size: int,
tp_size: int,
model_path: str,
master_port: int,
output_writer,
mem_fraction_static: float,
):
engine = None
try:
os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = str(master_port)
dist.init_process_group(
rank=rank,
device_id=torch.device(f"cuda:{rank}"),
world_size=dp_size * tp_size,
)
torch.cuda.set_device(rank)
base_gpu_id = rank // tp_size * tp_size
mesh_kwargs = dict(
mesh_shape=(dp_size, tp_size, 1), mesh_dim_names=["dp", "tp", "pp"]
)
inference_device_mesh_cpu = init_device_mesh("cpu", **mesh_kwargs)
# Only TP master ranks (rank % tp_size == 0) create the Engine and
# measure memory. Non-master ranks share the same GPU and would see
# device-level memory from the master's Engine workers, causing
# unpredictable assertion results.
is_tp_master = rank % tp_size == 0
engine = EngineWrapper(
model_path=model_path,
random_seed=42,
mem_fraction_static=mem_fraction_static,
device_mesh_cpu=inference_device_mesh_cpu["tp"],
base_gpu_id=base_gpu_id,
)
print(f"subprocess[{rank=}] engine created, {is_tp_master=}", flush=True)
# 1 - release kv cache
if is_tp_master:
mem_before = get_gpu_memory_mb(rank)
print(f"GPU{rank} before releasing KV cache: {mem_before:.0f} MB")
engine.release_memory_occupation(tags=["kv_cache"])
if is_tp_master:
mem_after = get_gpu_memory_mb(rank)
assert_memory_decreased(mem_before, mem_after, "release KV cache")
# 2 - release sglang weights
if is_tp_master:
mem_before = get_gpu_memory_mb(rank)
print(f"GPU{rank} before releasing weights: {mem_before:.0f} MB")
engine.release_memory_occupation(tags=["weights"])
if is_tp_master:
mem_after = get_gpu_memory_mb(rank)
assert_memory_decreased(mem_before, mem_after, "release weights")
# 3 - load hf model (TP master only)
hf_model = None
if is_tp_master:
mem_before = get_gpu_memory_mb(rank)
print(f"GPU{rank} before loading HF model: {mem_before:.0f} MB")
# Avoid device_map= which triggers accelerate dispatch hooks in
# transformers v5, preventing clean memory release on del.
hf_model = AutoModelForCausalLM.from_pretrained(
DEFAULT_SMALL_MODEL_NAME_FOR_TEST_BASE,
torch_dtype="bfloat16",
).to(f"cuda:{rank}")
mem_after = get_gpu_memory_mb(rank)
assert_memory_increased(mem_before, mem_after, "load HF model")
dist.barrier(group=inference_device_mesh_cpu["tp"].get_group())
# 4 - resume sglang weights and update from hf model
engine.resume_memory_occupation(tags=["weights"])
engine.update_weights_from_tensor(
named_tensors=list(hf_model.named_parameters()) if hf_model else []
)
# 5 - release hf model (TP master only)
if is_tp_master:
mem_before = get_gpu_memory_mb(rank)
print(f"GPU{rank} before releasing HF model: {mem_before:.0f} MB")
del hf_model
gc.collect()
torch.cuda.empty_cache()
mem_after = get_gpu_memory_mb(rank)
assert_memory_decreased(mem_before, mem_after, "release HF model")
dist.barrier(group=inference_device_mesh_cpu["tp"].get_group())
# 6 - resume kv cache
if is_tp_master:
mem_before = get_gpu_memory_mb(rank)
print(f"GPU{rank} before resuming KV cache: {mem_before:.0f} MB")
engine.resume_memory_occupation(tags=["kv_cache"])
if is_tp_master:
mem_after = get_gpu_memory_mb(rank)
assert_memory_increased(mem_before, mem_after, "resume KV cache")
print(f"GPU{rank} final memory: {mem_after:.0f} MB")
execution_ok = True
except Exception as e:
print(f"subprocess[{rank=}] has error: {e}", flush=True)
traceback.print_exc()
execution_ok = False
output_writer.send(execution_ok)
output_writer.close()
if engine:
engine.shutdown()
if __name__ == "__main__":
unittest.main()