Files
sgl-project--sglang/python/sglang/kernel_api_logging.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

518 lines
16 KiB
Python

"""Kernel API crash debugging helpers for SGLang.
This module was developed with reference to FlashInfer's kernel API logging utility:
https://github.com/flashinfer-ai/flashinfer/blob/main/flashinfer/api_logging.py
"""
from __future__ import annotations
import fnmatch
import functools
import inspect
import json
import logging
import os
import sys
from datetime import datetime
from pathlib import Path
from typing import Any, Callable, TypeVar, overload
import torch
_logger = logging.getLogger("sglang.kernel_api")
_T = TypeVar("_T")
_F = TypeVar("_F", bound=Callable[..., Any])
def _str_with_pid(path: str) -> str:
if "%i" in path:
return path.replace("%i", str(os.getpid()))
return path
def _get_env(key: str, type: Callable[..., _T], default: _T) -> _T:
value_str = os.environ.get(key, None)
if value_str is None:
return default
try:
return type(value_str)
except Exception:
_logger.warning(
"Failed to parse environment variable %s=%r as %s, using default %r",
key,
value_str,
type.__name__,
default,
)
return default
def _parse_pattern(value: str) -> list[str]:
return [p.strip() for p in value.split(",") if p.strip()]
_KERNEL_API_LOG_LEVEL = _get_env("SGLANG_KERNEL_API_LOGLEVEL", int, 0)
_KERNEL_API_LOG_DEST = _get_env("SGLANG_KERNEL_API_LOGDEST", _str_with_pid, "stdout")
_DUMP_DIR = Path(
_get_env("SGLANG_KERNEL_API_DUMP_DIR", _str_with_pid, "sglang_kernel_api_dumps")
)
_DUMP_INCLUDE_PATTERNS = _get_env("SGLANG_KERNEL_API_DUMP_INCLUDE", _parse_pattern, [])
_DUMP_EXCLUDE_PATTERNS = _get_env("SGLANG_KERNEL_API_DUMP_EXCLUDE", _parse_pattern, [])
_dump_call_counter: dict[str, int] = {}
def _setup_logger() -> None:
for handler in list(_logger.handlers):
_logger.removeHandler(handler)
try:
handler.close()
except Exception:
pass
if _KERNEL_API_LOG_LEVEL == 0:
_logger.addHandler(logging.NullHandler())
_logger.setLevel(logging.CRITICAL + 1)
return
_logger.setLevel(logging.DEBUG)
if _KERNEL_API_LOG_DEST == "stdout":
handler = logging.StreamHandler(sys.stdout)
elif _KERNEL_API_LOG_DEST == "stderr":
handler = logging.StreamHandler(sys.stderr)
else:
handler = logging.FileHandler(_KERNEL_API_LOG_DEST, mode="a")
handler.setFormatter(logging.Formatter("%(message)s"))
_logger.addHandler(handler)
_logger.propagate = False
_setup_logger()
def _is_compiling() -> bool:
try:
if hasattr(torch, "compiler") and hasattr(torch.compiler, "is_compiling"):
return bool(torch.compiler.is_compiling())
if hasattr(torch, "_dynamo") and hasattr(torch._dynamo, "is_compiling"):
return bool(torch._dynamo.is_compiling())
except Exception:
return False
return False
def _timestamp() -> str:
return datetime.now().strftime("[%Y-%m-%d %H:%M:%S]")
def _is_cuda_graph_capture_active() -> bool:
try:
return torch.cuda.is_available() and torch.cuda.is_current_stream_capturing()
except Exception:
return False
def _append_line(lines: list[str], indent: int, text: str) -> None:
lines.append(" " * indent + text)
def _should_dump_function(func_name: str) -> bool:
if _DUMP_INCLUDE_PATTERNS and not any(
fnmatch.fnmatch(func_name, pattern) for pattern in _DUMP_INCLUDE_PATTERNS
):
return False
if _DUMP_EXCLUDE_PATTERNS and any(
fnmatch.fnmatch(func_name, pattern) for pattern in _DUMP_EXCLUDE_PATTERNS
):
return False
return True
def _serialize_tensor(tensor: torch.Tensor) -> list[str]:
lines = ["Tensor("]
_append_line(lines, 2, f"shape={tuple(tensor.shape)}")
_append_line(lines, 2, f"dtype={tensor.dtype}")
_append_line(lines, 2, f"device={tensor.device}")
_append_line(lines, 2, f"requires_grad={tensor.requires_grad}")
_append_line(lines, 2, f"is_contiguous={tensor.is_contiguous()}")
if _KERNEL_API_LOG_LEVEL >= 5:
if tensor.numel() == 0:
_append_line(lines, 2, "statistics=[empty tensor]")
elif tensor.device.type == "cuda" and _is_cuda_graph_capture_active():
_append_line(
lines, 2, "statistics=[skipped: CUDA graph capture in progress]"
)
else:
try:
detached = tensor.detach()
if detached.is_complex():
stats_source = detached.abs().float()
nan_count = int(torch.isnan(detached).sum().item())
inf_count = int(torch.isinf(detached).sum().item())
else:
stats_source = detached.float()
if detached.is_floating_point():
nan_count = int(torch.isnan(detached).sum().item())
inf_count = int(torch.isinf(detached).sum().item())
else:
nan_count = 0
inf_count = 0
_append_line(lines, 2, f"min={stats_source.min().item():.6f}")
_append_line(lines, 2, f"max={stats_source.max().item():.6f}")
_append_line(lines, 2, f"mean={stats_source.mean().item():.6f}")
_append_line(lines, 2, f"nan_count={nan_count}")
_append_line(lines, 2, f"inf_count={inf_count}")
except Exception as exc:
_append_line(
lines, 2, f"statistics=[unavailable: {type(exc).__name__}]"
)
lines.append(")")
return lines
def _serialize_value(value: Any, depth: int = 0) -> list[str]:
if depth >= 2:
return [f"{type(value).__name__}(...)"]
if isinstance(value, torch.Tensor):
return _serialize_tensor(value)
if isinstance(value, (str, int, float, bool, type(None))):
return [repr(value)]
if isinstance(value, (list, tuple)):
opener = "[" if isinstance(value, list) else "("
closer = "]" if isinstance(value, list) else ")"
lines = [opener]
for idx, item in enumerate(value[:4]):
item_lines = _serialize_value(item, depth + 1)
lines.append(f" [{idx}] {item_lines[0]}")
for extra in item_lines[1:]:
lines.append(f" {extra}")
if len(value) > 4:
lines.append(f" ... ({len(value) - 4} more items)")
lines.append(closer)
return lines
if isinstance(value, dict):
lines = ["{"]
items = list(value.items())
for key, item in items[:8]:
item_lines = _serialize_value(item, depth + 1)
lines.append(f" {key!r}: {item_lines[0]}")
for extra in item_lines[1:]:
lines.append(f" {extra}")
if len(items) > 8:
lines.append(f" ... ({len(items) - 8} more items)")
lines.append("}")
return lines
summary = [f"{type(value).__name__}("]
for attr in ("shape", "dtype", "device"):
if hasattr(value, attr):
try:
_append_line(summary, 2, f"{attr}={getattr(value, attr)}")
except Exception:
pass
if len(summary) == 1:
_append_line(summary, 2, f"repr={repr(value)[:200]}")
summary.append(")")
return summary
def _serialize_json_value(value: Any) -> Any:
if isinstance(value, torch.dtype):
return {"type": "torch.dtype", "value": str(value)}
if isinstance(value, (str, int, float, bool, type(None))):
return value
if isinstance(value, (list, tuple)):
return [_serialize_json_value(item) for item in value[:16]]
if isinstance(value, dict):
return {
str(key): _serialize_json_value(item)
for key, item in list(value.items())[:32]
}
return {"type": type(value).__name__, "repr": repr(value)[:200]}
def _collect_dump_entries(
prefix: str,
value: Any,
tensor_entries: dict[str, torch.Tensor],
metadata_entries: dict[str, Any],
) -> None:
if isinstance(value, torch.Tensor):
tensor_entries[prefix] = value.detach().cpu()
return
if isinstance(value, (list, tuple)):
for idx, item in enumerate(value):
_collect_dump_entries(
f"{prefix}_{idx}", item, tensor_entries, metadata_entries
)
metadata_entries[f"{prefix}__container"] = {
"type": type(value).__name__,
"length": len(value),
}
return
if isinstance(value, dict):
for key, item in value.items():
_collect_dump_entries(
f"{prefix}_{str(key)}", item, tensor_entries, metadata_entries
)
metadata_entries[f"{prefix}__container"] = {
"type": "dict",
"keys": [str(k) for k in value.keys()],
}
return
metadata_entries[prefix] = _serialize_json_value(value)
def _dump_metadata_path(dump_dir: Path) -> Path:
return dump_dir / "metadata.json"
def _write_dump_metadata(dump_dir: Path, metadata: dict[str, Any]) -> None:
_dump_metadata_path(dump_dir).write_text(json.dumps(metadata, indent=2))
def _read_dump_metadata(dump_dir: Path) -> dict[str, Any]:
return json.loads(_dump_metadata_path(dump_dir).read_text())
def _dump_function_inputs(
func_name: str, args: tuple[Any, ...], kwargs: dict[str, Any]
) -> Path | None:
if not _should_dump_function(func_name):
return None
_DUMP_DIR.mkdir(parents=True, exist_ok=True)
call_index = _dump_call_counter.get(func_name, 0) + 1
_dump_call_counter[func_name] = call_index
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
safe_func_name = func_name.replace("/", "_").replace("<", "_").replace(">", "_")
dump_dir = (
_DUMP_DIR
/ f"{timestamp}_pid{os.getpid()}_{safe_func_name}_call{call_index:04d}"
)
dump_dir.mkdir(parents=True, exist_ok=True)
tensor_entries: dict[str, torch.Tensor] = {}
metadata_entries: dict[str, Any] = {}
for idx, arg in enumerate(args):
_collect_dump_entries(f"arg_{idx}", arg, tensor_entries, metadata_entries)
for key, value in kwargs.items():
_collect_dump_entries(f"kwarg_{key}", value, tensor_entries, metadata_entries)
if tensor_entries:
torch.save(tensor_entries, dump_dir / "inputs.pt")
metadata = {
"function_name": func_name,
"timestamp": timestamp,
"process_id": os.getpid(),
"execution_status": "inputs_saved",
"input_metadata": metadata_entries,
"input_tensor_keys": list(tensor_entries.keys()),
"output_metadata": {},
"output_tensor_keys": [],
}
_write_dump_metadata(dump_dir, metadata)
_logger.debug("Dumped inputs to: %s", dump_dir)
return dump_dir
def _dump_function_outputs(dump_dir: Path, result: Any) -> None:
tensor_entries: dict[str, torch.Tensor] = {}
metadata_entries: dict[str, Any] = {}
_collect_dump_entries("result", result, tensor_entries, metadata_entries)
if tensor_entries:
torch.save(tensor_entries, dump_dir / "outputs.pt")
metadata = _read_dump_metadata(dump_dir)
metadata["execution_status"] = "completed"
metadata["output_metadata"] = metadata_entries
metadata["output_tensor_keys"] = list(tensor_entries.keys())
_write_dump_metadata(dump_dir, metadata)
_logger.debug("Dumped outputs to: %s", dump_dir)
def _mark_dump_exception(dump_dir: Path, exc: Exception) -> None:
metadata = _read_dump_metadata(dump_dir)
metadata["execution_status"] = "exception"
metadata["exception"] = {
"type": type(exc).__name__,
"message": str(exc),
}
_write_dump_metadata(dump_dir, metadata)
def _log_section(title: str, data: dict[str, Any]) -> None:
_logger.debug(title)
for key, value in data.items():
lines = _serialize_value(value)
_logger.debug(" %s=%s", key, lines[0])
for line in lines[1:]:
_logger.debug(" %s", line)
def _infer_func_name(func: Callable) -> str:
qualname = getattr(func, "__qualname__", getattr(func, "__name__", "unknown"))
qualname = qualname.replace(".<locals>.", ".").replace("<locals>.", "")
module = getattr(func, "__module__", "")
for prefix in ("sglang.", "sgl_kernel."):
if module.startswith(prefix):
module = module[len(prefix) :]
break
if module and module not in {"__main__", "builtins"}:
return f"{module}.{qualname}"
source_path = inspect.getsourcefile(func)
if source_path is not None:
return f"{Path(source_path).stem}.{qualname}"
return qualname
@overload
def debug_kernel_api(
func: _F,
*,
op_name: str | None = None,
) -> _F: ...
@overload
def debug_kernel_api(
*,
op_name: str | None = None,
) -> Callable[[_F], _F]: ...
def debug_kernel_api(
func: Callable | None = None,
*,
op_name: str | None = None,
) -> Callable:
# NOTE: avoid any overhead in the hot path when logging is disabled
if _KERNEL_API_LOG_LEVEL == 0:
if func is None:
return lambda f: f
return func
def decorator(f: Callable) -> Callable:
if hasattr(f, "_debug_kernel_wrapped"):
return f
@functools.wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> Any:
if _is_compiling():
return f(*args, **kwargs)
func_name = op_name or _infer_func_name(f)
dump_dir: Path | None = None
positional_args = args
try:
parameters = tuple(inspect.signature(f).parameters.values())
except (TypeError, ValueError):
parameters = ()
if args and parameters and parameters[0].name in {"self", "cls"}:
positional_args = args[1:]
_logger.debug("=" * 80)
_logger.debug("%s SGLang Kernel API Call: %s", _timestamp(), func_name)
if _KERNEL_API_LOG_LEVEL >= 3:
if positional_args:
_log_section(
"Positional input arguments:",
{f"arg[{idx}]": arg for idx, arg in enumerate(positional_args)},
)
if kwargs:
_log_section("Keyword input arguments:", kwargs)
if _KERNEL_API_LOG_LEVEL >= 10:
if _is_cuda_graph_capture_active():
_logger.debug("Tensor dump skipped: CUDA graph capture in progress")
else:
dump_dir = _dump_function_inputs(func_name, positional_args, kwargs)
try:
result = f(*args, **kwargs)
except Exception as exc:
if dump_dir is not None:
_mark_dump_exception(dump_dir, exc)
_logger.debug(
"%s SGLang Kernel API Exception: %s (%s: %s)",
_timestamp(),
func_name,
type(exc).__name__,
exc,
)
raise
if dump_dir is not None:
_dump_function_outputs(dump_dir, result)
if _KERNEL_API_LOG_LEVEL >= 3:
_log_section("Output:", {"return": result})
return result
setattr(wrapper, "_debug_kernel_wrapped", True)
return wrapper
return decorator if func is None else decorator(func)
def debug_torch_op(
op_func: Callable,
op_name: str,
*,
namespace: str = "sglang",
) -> Callable:
"""NOTE: For internal use. Prefer `debug_kernel_api` for general use cases."""
# NOTE: avoid any overhead in the hot path when logging is disabled
impl = getattr(getattr(torch.ops, namespace), op_name)
if _KERNEL_API_LOG_LEVEL == 0:
return impl
# NOTE: propagate the marker to avoid double-wrapping
if hasattr(op_func, "_debug_kernel_wrapped"):
setattr(impl, "_debug_kernel_wrapped", True)
return impl
# NOTE: redirect the function name
return debug_kernel_api(impl, op_name=_infer_func_name(op_func))
def wrap_method_with_debug_kernel_once(
obj: Any,
method_name: str,
*,
op_name: str,
marker_attr: str | None = None,
) -> Any:
# NOTE: avoid any overhead in the hot path when logging is disabled
if _KERNEL_API_LOG_LEVEL == 0:
return obj
if marker_attr is None:
marker_attr = f"_debug_kernel_{method_name}_wrapped"
if getattr(obj, marker_attr, False):
return obj
setattr(
obj,
method_name,
debug_kernel_api(getattr(obj, method_name), op_name=op_name),
)
setattr(obj, marker_attr, True)
return obj