feat: Add Non-intrusive Tensor Dumping for Model Inference (#10566)

This commit is contained in:
Yuhong Guo
2025-10-31 12:04:48 +08:00
committed by GitHub
parent 34c286b811
commit ab95d35fcb
6 changed files with 272 additions and 15 deletions

View File

@@ -0,0 +1,149 @@
"""
This file provides a function `register_forward_hook_for_model` that registers a forward hook on every operator of the model.
After registration, during model inference, all tensors generated throughout the forward pass will be recorded.
Usage:
Specify the output directory for dumping tensors using the argument `--debug-tensor-dump-output-folder`.
A separate directory will be created for each GPU rank, named in the format `f"TP{tp_rank}_PP{pp_rank}_Rank{rank}_pid{pid}"`.
Each complete forward pass of the model generates a `.pt` file named `f"Pass{pass_num}.pt"`, which can be loaded using `torch.load`.
The file contains a series of key-value pairs, where the keys correspond to operator names in the model
(similar to those in model.safetensors.index.json), and the values are the outputs produced by the respective operators.
"""
import logging
import os
from pathlib import Path
import torch
from sglang.srt.layers.logits_processor import LogitsProcessorOutput
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
logger = logging.getLogger(__name__)
class TensorDumper:
def __init__(
self, dump_dir: str, dump_layers: int, tp_size: int, tp_rank: int, pp_rank: int
):
self._dump_layers = dump_layers
self._forward_pass_id = 0
self._pid = os.getpid()
self._current_tensors = {}
self._base_dir = Path(dump_dir)
rank = tp_size * pp_rank + tp_rank
self._process_dir = (
self._base_dir / f"TP{tp_rank}_PP{pp_rank}_Rank{rank}_pid{self._pid}"
)
self._process_dir.mkdir(parents=True, exist_ok=True)
def get_dump_dir(self):
return str(self._process_dir)
def add_tensor(self, name, tensor_item):
if isinstance(tensor_item, (tuple, list)):
tensors = [t.cpu() for t in tensor_item if t is not None]
if len(tensors) == 1:
self._current_tensors[name] = tensors[0]
else:
self._current_tensors[name] = tensors
elif isinstance(tensor_item, torch.Tensor):
self._current_tensors[name] = tensor_item.cpu()
elif isinstance(tensor_item, LogitsProcessorOutput):
self._current_tensors[name] = tensor_item.next_token_logits.cpu()
elif isinstance(tensor_item, ForwardBatch):
self._current_tensors[name + ".forward_batch_info.input_ids"] = (
tensor_item.input_ids.cpu()
)
self._current_tensors[name + ".forward_batch_info.seq_lens"] = (
tensor_item.seq_lens.cpu()
)
self._current_tensors[name + ".forward_batch_info.positions"] = (
tensor_item.positions.cpu()
)
elif isinstance(tensor_item, PPProxyTensors):
for tensor_name in tensor_item.tensors.keys():
self._current_tensors[name + ".pp_proxy_tensors." + tensor_name] = (
tensor_item.tensors[tensor_name].cpu()
)
else:
logger.warning(f"Unsupported type: {type(tensor_item)}: {tensor_item}")
def dump_current_tensors(self):
if len(self._current_tensors) == 0:
return
tensor_file_for_pass = self._process_dir / f"Pass{self._forward_pass_id:05d}.pt"
logger.info(
f"Dump {self._forward_pass_id:05d}th pass to {tensor_file_for_pass}"
)
torch.save(self._current_tensors, str(tensor_file_for_pass))
self._current_tensors = {}
self._forward_pass_id += 1
def _add_hook_recursive(
self, model, prefix, top_level_module_name, layers_module_name
):
model_top_level_module_matched = False
layers_prefix = top_level_module_name + "." + layers_module_name
for name, module in model._modules.items():
top_level_model = False
if len(prefix) == 0:
cur_name = name
if cur_name == top_level_module_name:
model_top_level_module_matched = True
top_level_model = True
else:
cur_name = prefix + "." + name
if self._dump_layers > 0 and name.isdigit() and prefix == layers_prefix:
# If we only need n layers, skip the reset layers.
# Most models' layout is like model.layers.0.
cur_layer = int(name)
if cur_layer >= self._dump_layers:
continue
if module is not None:
_, sub_count = self._add_hook_recursive(
module, cur_name, top_level_module_name, layers_module_name
)
if sub_count == 0 or top_level_model:
# Avoid duplicated output hooks, e.g. self_attn may contain:
# self_attn.qkv_proj, self_attn.attn & self_attn.o_proj.
# Therefore, we do not need to add output hooks for self_attn,
# since the output of self_attn should be the same to self_attn.o_proj.
module.register_forward_hook(
self._dump_hook(cur_name, top_level_model)
)
return model_top_level_module_matched, len(model._modules.items())
def _dump_hook(self, tensor_name, do_dump):
def inner_dump_hook(module, input, output):
if do_dump:
# This is the top-level model, so we will record the input for it.
for item in input:
if isinstance(item, ForwardBatch):
self.add_tensor(tensor_name, item)
self.dump_current_tensors()
if output is not None:
self.add_tensor(tensor_name, output)
return inner_dump_hook
def register_forward_hook_for_model(
model, dump_dir: str, dump_layers: int, tp_size: int, tp_rank: int, pp_rank: int
):
tensor_dumper = TensorDumper(dump_dir, dump_layers, tp_size, tp_rank, pp_rank)
# Most models have the layerout like:
# XxxxForCausalLM
# (model): XxxxModel
# (layers): ModuleList
# If the model is not constructed with this layout,
# environment variable can be used to specify the module names.
top_level_module_name = os.getenv("TENSOR_DUMP_TOP_LEVEL_MODULE_NAME", "model")
layers_module_name = os.getenv("TENSOR_DUMP_LAYERS_MODULE_NAME", "layers")
model_top_level_module_matched, _ = tensor_dumper._add_hook_recursive(
model, "", top_level_module_name, layers_module_name
)
assert (
model_top_level_module_matched
), f"model should have a module named {top_level_module_name}"
return tensor_dumper

View File

@@ -38,7 +38,6 @@ from sglang.srt.layers.dp_attention import (
get_dp_device,
get_dp_dtype,
get_dp_hidden_size,
get_local_attention_dp_size,
)
from sglang.srt.layers.vocab_parallel_embedding import VocabParallelEmbedding
from sglang.srt.model_executor.forward_batch_info import (
@@ -47,7 +46,7 @@ from sglang.srt.model_executor.forward_batch_info import (
ForwardMode,
)
from sglang.srt.server_args import get_global_server_args
from sglang.srt.utils import dump_to_file, is_npu, use_intel_amx_backend
from sglang.srt.utils import is_npu, use_intel_amx_backend
logger = logging.getLogger(__name__)
@@ -252,10 +251,6 @@ class LogitsProcessor(nn.Module):
):
self.final_logit_softcapping = None
self.debug_tensor_dump_output_folder = (
get_global_server_args().debug_tensor_dump_output_folder
)
def compute_logprobs_for_multi_item_scoring(
self,
input_ids,
@@ -463,14 +458,6 @@ class LogitsProcessor(nn.Module):
logits[sample_indices] if sample_indices is not None else logits
)
if self.debug_tensor_dump_output_folder:
assert (
not self.do_tensor_parallel_all_gather
or get_local_attention_dp_size() == 1
), "dp attention + sharded lm_head doesn't support full logits"
full_logits = self._get_logits(hidden_states, lm_head, logits_metadata)
dump_to_file(self.debug_tensor_dump_output_folder, "logits", full_logits)
hidden_states_to_store: Optional[torch.Tensor] = None
if logits_metadata.capture_hidden_mode.need_capture():
if logits_metadata.capture_hidden_mode.is_full():

View File

@@ -40,6 +40,9 @@ from sglang.srt.configs.model_config import (
)
from sglang.srt.configs.update_config import adjust_config_with_unaligned_cpu_tp
from sglang.srt.constants import GPU_MEMORY_TYPE_WEIGHTS
from sglang.srt.debug_utils.tensor_dump_forward_hook import (
register_forward_hook_for_model,
)
from sglang.srt.distributed import (
get_pp_group,
get_tp_group,
@@ -791,6 +794,15 @@ class ModelRunner:
f"avail mem={after_avail_memory:.2f} GB, "
f"mem usage={self.weight_load_mem_usage:.2f} GB."
)
if self.server_args.debug_tensor_dump_output_folder is not None:
register_forward_hook_for_model(
self.model,
self.server_args.debug_tensor_dump_output_folder,
self.server_args.debug_tensor_dump_layers,
self.tp_size,
self.tp_rank,
self.pp_rank,
)
if self.server_args.elastic_ep_backend == "mooncake":
# Mooncake does not support `monitored_barrier`

View File

@@ -511,6 +511,9 @@ class ServerArgs:
# Debug tensor dumps
debug_tensor_dump_output_folder: Optional[str] = None
# -1 mean dump all layers.
debug_tensor_dump_layers: int = -1
# TODO(guoyuhong): clean the old dumper code.
debug_tensor_dump_input_file: Optional[str] = None
debug_tensor_dump_inject: bool = False
@@ -1784,7 +1787,13 @@ class ServerArgs:
)
def _handle_other_validations(self):
pass
# Handle model inference tensor dump.
if self.debug_tensor_dump_output_folder is not None:
logger.warning(
"Cuda graph and server warmup are disabled because of using tensor dump mode"
)
self.disable_cuda_graph = True
self.skip_server_warmup = True
@staticmethod
def add_cli_args(parser: argparse.ArgumentParser):
@@ -3375,6 +3384,12 @@ class ServerArgs:
default=ServerArgs.debug_tensor_dump_output_folder,
help="The output folder for dumping tensors.",
)
parser.add_argument(
"--debug-tensor-dump-layers",
type=int,
default=-1,
help="The layer number for dumping tensors.",
)
parser.add_argument(
"--debug-tensor-dump-input-file",
type=str,