使用 SGLang Kernel API 日志调试 CUDA 崩溃:从环境变量到 compute-sanitizer 的完整实战指南
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
导读
CUDA 非法内存访问、device-side assert、NaN/Inf 扩散——这类崩溃的共性是:进程在正常调试输出被刷出之前就中止了,你永远看不到触发崩溃的那批张量长什么样。SGLang 为此内置了一套基于@debug_kernel_api装饰器的内核 API 日志系统:它在内核执行之前捕获输入张量的形状、dtype、设备、连续性乃至数值统计,把"崩溃发生时到底发生了什么"记录下来。本文以该功能为主线,完整覆盖从环境变量启用、四个日志级别、崩溃安全 dump、多进程调试到与 compute-sanitizer / cuda-gdb / printf 组合定位的全流程,并结合 kernel_api_logging.py 等源码说明其底层实现原理。读完本文,你将掌握一套对 LLM 与 Diffusion 模型通用的 CUDA 崩溃排查方法论。
为什么 CUDA 崩溃需要"内核 API 日志"
问题:CUDA 错误(illegal memory access、device-side assert、out-of-bounds、NaN/Inf)往往直接中止进程。标准做法是在代码里手动打印张量,但大多数情况下崩溃发生在你还没来得及加打印的位置,而且普通 stdout 缓冲会在 abort 时丢失。
解决方案:SGLang 的@debug_kernel_api装饰器在执行前记录输入,因此即使程序中止,你依然能看到导致崩溃的调用边界上发生了什么。核心实现位于 python/sglang/kernels/kernel_api_logging.py,其设计参照了 FlashInfer 的 kernel API logging 工具。
日志覆盖范围
当前日志覆盖聚焦于 SGLang 中价值最高的内核边界:
- 通过
register_custom_op(...)注册的自定义算子; - 通过
register_custom_op_from_extern(...)注册的外部自定义算子(如 FlashInfer 等外部库的内核); - LLM 的 attention、linear、quantization 以及多平台 wrapper 入口;
- Diffusion 的 attention 实现、linear、rotary 和 custom-op wrapper 入口;
- 部分直接
torch.ops.sglang.*热点与模型级 bypass。
这意味着该日志对 LLM 和 Diffusion 内核调试都有用,但它不会自动覆盖仓库中每一个纯 PyTorch 调用。
源码中的接线方式
日志系统通过三条路径接入调用边界,你可以从源码确认其覆盖面:
- LLM 自定义算子:
register_custom_op最终通过debug_torch_op把日志装饰器挂到torch.ops.sglang.<op>上,见 python/sglang/srt/utils/custom_op.py;外部算子走register_custom_op_from_extern,同样以debug_torch_op(fn, name)收尾(同文件 L328-L337)。 - Diffusion 自定义算子:
CustomOp基类的forward直接标注@debug_kernel_api,见 python/sglang/multimodal_gen/runtime/layers/custom_op.py;对应的register_custom_op位于 python/sglang/multimodal_gen/runtime/layers/utils.py。 - AOT 编译内核:
maybe_wrap_debug_kernel提供条件包装(仅在环境变量开启时生效),被 python/sglang/kernels/aot/python/sgl_kernel/init.py 用于批量包装sgl_kernel中的导出函数。
因此,当你通过@register_custom_op注册自己的算子时,它天然处于日志覆盖范围内——这正是后文复现实验的基础。
Step 1:启用 Kernel API 日志
日志由 4 个环境变量控制,其中SGLANG_KERNEL_API_LOGLEVEL决定详细程度,SGLANG_KERNEL_API_LOGDEST决定输出位置。在源码中,这些变量在模块导入时一次性解析(kernel_api_logging.py),因此必须在启动 Python 进程前设置好。
基础日志(仅函数名,Level 1)
export SGLANG_KERNEL_API_LOGLEVEL=1 export SGLANG_KERNEL_API_LOGDEST=stdout python my_script.py输出示例(真实摘自Qwen/Qwen3-0.6B):
================================================================================ [2026-03-19 00:47:06] SGLang Kernel API Call: RMSNorm.forward ================================================================================ [2026-03-19 00:47:06] SGLang Kernel API Call: sglang.quant_method.UnquantizedLinearMethod.apply ================================================================================ [2026-03-19 00:47:06] SGLang Kernel API Call: sglang.custom_op.fused_inplace_qknorm注意函数名的三类形态:普通模块方法(RMSNorm.forward)、quant 方法(sglang.quant_method.UnquantizedLinearMethod.apply)和自定义算子(sglang.custom_op.fused_inplace_qknorm)。
详细日志(输入输出带元数据,Level 3)
export SGLANG_KERNEL_API_LOGLEVEL=3 export SGLANG_KERNEL_API_LOGDEST=debug.log python my_script.pydebug.log中的输出示例(真实摘自Qwen/Qwen3-0.6B):
================================================================================ [2026-03-19 00:47:30] SGLang Kernel API Call: sglang.quant_method.UnquantizedLinearMethod.apply Positional input arguments: arg[0]=QKVParallelLinear( repr=QKVParallelLinear(in_features=1024, output_features=4096, bias=False, tp_size=1, gather_output=False) ) arg[1]=Tensor( shape=(1, 1024) dtype=torch.bfloat16 device=cuda:0 requires_grad=False is_contiguous=True ) arg[2]=None Output: return=Tensor( shape=(1, 4096) dtype=torch.bfloat16 device=cuda:0 requires_grad=False is_contiguous=True )Level 3 已经足够你判断绝大多数形状(shape)、数据类型(dtype)和设备(device)不匹配问题。注意arg[0]是非张量对象(QKVParallelLinear模块实例),序列化器会提取shape/dtype/device属性或截断的repr;is_contiguous一栏则直接反映 stride 连续性,是排查 stride 相关内核 bug 的关键信号。
完整日志(带张量统计,Level 5)
export SGLANG_KERNEL_API_LOGLEVEL=5 export SGLANG_KERNEL_API_LOGDEST=debug.log python my_script.py额外输出(真实摘自black-forest-labs/FLUX.1-dev):
================================================================================ [2026-03-19 01:00:42] SGLANG Kernel API Call: diffusion.quant_method.UnquantizedLinearMethod.apply Positional input arguments: arg[1]=Tensor( shape=(1, 77, 768) dtype=torch.bfloat16 device=cuda:0 requires_grad=False is_contiguous=True min=-27.250000 max=28.500000 mean=0.011723 nan_count=0 inf_count=0 ) Output: return=Tensor( shape=(1, 77, 2304) dtype=torch.bfloat16 device=cuda:0 requires_grad=False is_contiguous=True min=-8.937500 max=9.375000 mean=0.009460 nan_count=0 inf_count=0 )Level 5 在 Level 3 基础上追加min/max/mean/nan_count/inf_count。统计计算由_serialize_tensor完成(kernel_api_logging.py):复数张量先取绝对值,整数张量不统计 NaN/Inf,浮点张量则逐项统计。空张量会标记为statistics=[empty tensor]。
崩溃安全 Dump(Level 10)
export SGLANG_KERNEL_API_LOGLEVEL=10 export SGLANG_KERNEL_API_LOGDEST=debug.log export SGLANG_KERNEL_API_DUMP_DIR=/tmp/sglang_kernel_api_dumps python my_script.pyLevel 10 在执行前保存输入。若内核崩溃,dump 目录中依然保留输入与异常元数据。真实Qwen/Qwen3-0.6B的 Level 10 dump 布局:
/tmp/sglang_kernel_api_validation/qwen_qwen3_0_6b_level10_dumps /tmp/sglang_kernel_api_validation/qwen_qwen3_0_6b_level10_dumps/20260319_004821_182_pid919286_RotaryEmbedding.forward_call0001 /tmp/sglang_kernel_api_validation/qwen_qwen3_0_6b_level10_dumps/20260319_004821_182_pid919286_RotaryEmbedding.forward_call0001/inputs.pt /tmp/sglang_kernel_api_validation/qwen_qwen3_0_6b_level10_dumps/20260319_004821_182_pid919286_RotaryEmbedding.forward_call0001/metadata.json /tmp/sglang_kernel_api_validation/qwen_qwen3_0_6b_level10_dumps/20260319_004821_182_pid919286_RotaryEmbedding.forward_call0001/outputs.pt每个调用对应一个以时间戳_pid_函数名_call序号命名的子目录,内含:
inputs.pt:torch.save保存的输入张量字典,key 形如arg_0、arg_1、kwarg_x(容器与嵌套元素会被展开为arg_0_0之类的前缀 key,容器结构记录在metadata.json中);outputs.pt:输出张量(仅当调用成功完成时存在);metadata.json:调用元数据。
真实metadata.json片段:
{ "function_name": "RotaryEmbedding.forward", "timestamp": "20260319_004821_182", "process_id": 919286, "execution_status": "completed", "input_tensor_keys": ["arg_0", "arg_1", "arg_2"], "output_tensor_keys": ["result_0", "result_1"] }源码层面的执行流是(kernel_api_logging.py):先_dump_function_inputs写入execution_status: "inputs_saved"的元数据与inputs.pt;随后调用被包装函数;成功则_dump_function_outputs把状态改为completed并写入outputs.pt;抛异常则_mark_dump_exception把状态改为exception并记录异常类型与消息。因此你只要看到execution_status: "exception"且缺outputs.pt,就说明崩溃发生在该调用边界内。
使用 Level 10 的注意事项:
- 若 CUDA graph capture 处于激活状态,张量 dump 会被自动跳过(避免 capture 期间触发 CUDA 错误),此时仍能得到调用日志,但没有
inputs.pt/outputs.pt; - Level 10 dump 的本质是崩溃安全的调用快照:它始终保留观察到的调用边界,但并非每个方法都能一键回放,因为部分方法依赖未序列化进 dump 的模块状态;
- 对真实模型的成功路径 Level 10 dump,通常建议在调试运行中临时关闭 CUDA graph 与 piecewise CUDA graph。
Step 2:复现一个 LLM CUDA 崩溃
先用一个最小复现脚本模拟"embedding 索引越界导致 device-side assert"的场景。脚本通过@register_custom_op注册一个会崩溃的自定义算子,从而让崩溃点正好落在日志覆盖边界上:
python3 - <<'PY' from pathlib import Path Path("/tmp/sglang_llm_crash.py").write_text( "import torch\n" "import torch.nn.functional as F\n" "from sglang.srt.utils.custom_op import register_custom_op\n\n" "def _fake_embedding(indices, table):\n" " return torch.empty((*indices.shape, table.shape[-1]), device=table.device, dtype=table.dtype)\n\n" "@register_custom_op(op_name='mock_llm_cuda_crash', fake_impl=_fake_embedding)\n" "def mock_llm_cuda_crash(indices, table):\n" " out = F.embedding(indices, table)\n" " torch.cuda.synchronize()\n" " return out\n\n" "table = torch.randn(4, 8, device='cuda', dtype=torch.float16)\n" "indices = torch.tensor([0, 7], device='cuda', dtype=torch.long)\n" "mock_llm_cuda_crash(indices, table)\n" ) PY SGLANG_KERNEL_API_LOGLEVEL=1 \ SGLANG_KERNEL_API_LOGDEST=/tmp/sglang_llm_level1.log \ python3 /tmp/sglang_llm_crash.py预期结果:
- 脚本以 CUDA
device-side assert退出; - 日志中仍保留崩溃前的最后一个 API 边界记录(即
sglang.custom_op.mock_llm_cuda_crash)。
这个脚本刻意用indices=[0, 7]配合仅 4 行的table制造越界:F.embedding查表时索引 7 超出词表规模 4,GPU 侧 assert 触发,随后torch.cuda.synchronize()把异步错误同步回主机端。
同一示例换 Level 3:
SGLANG_KERNEL_API_LOGLEVEL=3 \ SGLANG_KERNEL_API_LOGDEST=/tmp/sglang_llm_level3.log \ python3 /tmp/sglang_llm_crash.py现在日志中会出现崩溃前的张量元数据——你可以直接看到indices的形状与数值来源。
再试 Level 10:
SGLANG_KERNEL_API_LOGLEVEL=10 \ SGLANG_KERNEL_API_LOGDEST=/tmp/sglang_llm_level10.log \ SGLANG_KERNEL_API_DUMP_DIR=/tmp/sglang_llm_level10_dumps \ python3 /tmp/sglang_llm_crash.py此时你应该看到:
- 一条
sglang.custom_op.mock_llm_cuda_crash的日志条目; - 一个包含
inputs.pt的 dump 目录; metadata.json显示execution_status: "exception";- 没有
outputs.pt,因为内核在产生输出前就崩溃了。
Step 3:复现一个 Diffusion CUDA 崩溃
Diffusion 侧的自定义算子注册入口不同,位于sglang.multimodal_gen.runtime.layers.utils,其余套路完全一致:
python3 - <<'PY' from pathlib import Path Path("/tmp/sglang_diffusion_crash.py").write_text( "import torch\n" "import torch.nn.functional as F\n" "from sglang.multimodal_gen.runtime.layers.utils import register_custom_op\n\n" "def _fake_embedding(positions, cache):\n" " return torch.empty((*positions.shape, cache.shape[-1]), device=cache.device, dtype=cache.dtype)\n\n" "@register_custom_op(op_name='mock_diffusion_cuda_crash', fake_impl=_fake_embedding)\n" "def mock_diffusion_cuda_crash(positions, cache):\n" " out = F.embedding(positions, cache)\n" " torch.cuda.synchronize()\n" " return out\n\n" "cache = torch.randn(4, 64, device='cuda', dtype=torch.float16)\n" "positions = torch.tensor([0, 9], device='cuda', dtype=torch.long)\n" "mock_diffusion_cuda_crash(positions, cache)\n" ) PY SGLANG_KERNEL_API_LOGLEVEL=1 \ SGLANG_KERNEL_API_LOGDEST=/tmp/sglang_diffusion_level1.log \ python3 /tmp/sglang_diffusion_crash.py同样依次尝试 Level 3 与 Level 10:
SGLANG_KERNEL_API_LOGLEVEL=3 \ SGLANG_KERNEL_API_LOGDEST=/tmp/sglang_diffusion_level3.log \ python3 /tmp/sglang_diffusion_crash.py SGLANG_KERNEL_API_LOGLEVEL=10 \ SGLANG_KERNEL_API_LOGDEST=/tmp/sglang_diffusion_level10.log \ SGLANG_KERNEL_API_DUMP_DIR=/tmp/sglang_diffusion_level10_dumps \ python3 /tmp/sglang_diffusion_crash.py如果你的本地环境存在无关的 FlashInfer 导入问题,请先在上层 shell 中解决再运行示例;示例本身不会设置任何FLASHINFER_*环境变量。
Step 4:多进程调试
多 GPU 或 worker 进程场景下,多个 rank 会把日志写到同一个文件互相覆盖。SGLANG_KERNEL_API_LOGDEST和SGLANG_KERNEL_API_DUMP_DIR都支持%i占位符,它在进程内被替换为os.getpid()(见 kernel_api_logging.py 的_str_with_pid):
export SGLANG_KERNEL_API_LOGLEVEL=3 export SGLANG_KERNEL_API_LOGDEST=debug_rank_%i.log torchrun --nproc_per_node=4 my_script.py这会生成彼此独立的日志文件,例如debug_rank_12345.log、debug_rank_12346.log、debug_rank_12347.log、debug_rank_12348.log。
真实的多进程示例来自一次 2-GPUQwen/Qwen2.5-0.5B-Instruct运行:
/tmp/sglang_kernel_api_validation_multi/qwen_qwen2_5_0_5b_instruct_level3_950201.log /tmp/sglang_kernel_api_validation_multi/qwen_qwen2_5_0_5b_instruct_level3_950349.log /tmp/sglang_kernel_api_validation_multi/qwen_qwen2_5_0_5b_instruct_level3_950350.log /tmp/sglang_kernel_api_validation_multi/qwen_qwen2_5_0_5b_instruct_level3_950351.logLevel 10 的 dump 目录也应同样处理:
export SGLANG_KERNEL_API_LOGLEVEL=10 export SGLANG_KERNEL_API_LOGDEST=debug_rank_%i.log export SGLANG_KERNEL_API_DUMP_DIR=/tmp/sglang_kernel_api_dumps_%i这样可避免多个 rank 写入同一棵 dump 目录树。
Step 5:过滤 Level 10 Dump
Level 10 全量 dump 可能过于嘈杂。此时用通配符限制 dump 范围——SGLANG_KERNEL_API_DUMP_INCLUDE/SGLANG_KERNEL_API_DUMP_EXCLUDE采用 shell 风格通配匹配(fnmatch),且支持逗号分隔的多模式(源码解析见 kernel_api_logging.py 与_should_dump_function,同文件 L122-L131):
export SGLANG_KERNEL_API_LOGLEVEL=10 export SGLANG_KERNEL_API_LOGDEST=debug.log export SGLANG_KERNEL_API_DUMP_DIR=/tmp/sglang_kernel_api_dumps export SGLANG_KERNEL_API_DUMP_INCLUDE='sglang.custom_op.*' export SGLANG_KERNEL_API_DUMP_EXCLUDE='*.fake_impl'匹配规则:若设置了 INCLUDE,函数名必须命中至少一个模式才 dump;若设置了 EXCLUDE,命中任意一个模式即跳过。
Step 6:常见 CUDA 错误及排查要点
非法内存访问或 Device-Side Assert
典型报错:
RuntimeError: CUDA error: an illegal memory access was encountered torch.AcceleratorError: CUDA error: device-side assert triggered排查命令:
export SGLANG_KERNEL_API_LOGLEVEL=3在日志中检查:
- 张量形状(shape)
- 张量 dtype
- CUDA 与 CPU 的设备放置
- stride / 连续性(
is_contiguous) - 是否存在"只记录到输入、没有输出"的调用——这是崩溃点的直接标志
典型的形状不匹配模式:
SGLANG Kernel API Call: ... arg[0]=Tensor(shape=(..., 128), ...) # 期望的维度 arg[1]=Tensor(shape=(..., 64), ...) # 不匹配这类现象通常指向 head-dim、hidden-dim 或 cache 布局不一致,而不是随机的 CUDA 故障。
NaN 或 Inf
排查命令:
export SGLANG_KERNEL_API_LOGLEVEL=5检查字段:min、max、mean、nan_count、inf_count。
典型坏数据模式:
Tensor( ... min=-1234567.000000 # 数值异常偏大 max=9876543.000000 # 数值异常偏大 mean=nan # 已出现 NaN nan_count=128 # 找到 NaN inf_count=0 # 此处尚无 Inf )这通常意味着坏值在进入崩溃内核之前就已经存在——你需要沿调用链向前追溯,找到第一个产生坏值的位置,而不是盯着崩溃点本身。
显存不足(Out of Memory)
排查命令:
export SGLANG_KERNEL_API_LOGLEVEL=3检查:
- 异常大的张量形状
- batch size
- 序列长度
- Diffusion 场景下的帧数或图像分辨率
同时确认是否存在"本应是 per-token / per-frame 的张量,意外变成了 full-sequence / full-image 尺寸"的情况。
典型坏模式:
Tensor( shape=(1024, 8192, 128, 128) # 尺寸过大 ... )示例:从日志中定位形状 Bug
假设失败调用日志如下:
[2026-03-19 00:47:30] SGLang Kernel API Call: RotaryEmbedding.forward Positional input arguments: arg[0]=Tensor(shape=(1, 8), dtype=torch.int64, ...) arg[1]=Tensor(shape=(1, 8, 8, 256), dtype=torch.bfloat16, ...) # query 正常 arg[2]=Tensor(shape=(1, 8, 4, 64), dtype=torch.bfloat16, ...) # key 的 head_dim 不匹配你能得出什么结论:
- positions 看起来合理;
- query 看起来合理;
- key 的最后一维与预期的 rotary/head 维度不一致。
这通常意味着 bug 出在 projection 布局、head 打包或 cache 格式上,而不是 rotary 内核本身。
Step 7:与 compute-sanitizer 组合使用
对于更隐蔽的越界写等问题,把内核 API 日志与 CUDA 内存检查工具结合:
export SGLANG_KERNEL_API_LOGLEVEL=3 export SGLANG_KERNEL_API_LOGDEST=debug.log compute-sanitizer --tool memcheck python3 /tmp/sglang_llm_crash.py用debug.log查看到达崩溃 API 边界的确切输入。
典型compute-sanitizer输出:
========= COMPUTE-SANITIZER ========= Invalid __global__ write of size 4 bytes ========= at 0x1234 in SomeKernel ========= by thread (256,0,0) in block (10,0,0) ========= Address 0x... is out of bounds工作流是:用 sanitizer 输出定位崩溃的内核,用debug.log定位到达该边界之前的张量,两者交叉得到完整证据链。
如果需要更同步的主机端错误上报,可以单独把CUDA_LAUNCH_BLOCKING=1作为后续对照实验。它不属于默认工作流的一部分,因为改变执行时序可能掩盖与并发相关的问题。
Step 8:与 cuda-gdb 组合使用
需要栈回溯而非仅内存诊断时:
export SGLANG_KERNEL_API_LOGLEVEL=3 export SGLANG_KERNEL_API_LOGDEST=debug.log cuda-gdb --args python3 /tmp/sglang_llm_crash.py在cuda-gdb内部:
(cuda-gdb) run (cuda-gdb) where然后将回溯与debug.log相互对照:gdb 告诉你崩溃发生在哪个内核、哪个指令,日志告诉你到达该内核时输入张量是什么。
Step 9:内核级 printf 调试
当你拥有 CUDA 内核源码时,printf()仍是缩小坏索引、坏启动几何或状态传播问题范围的有效手段。
基本模式:
__global__ void MyKernel(const float* input, float* output, int n) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (threadIdx.x == 0 && blockIdx.x == 0) { printf("n=%d input0=%f\n", n, input[0]); } if (idx < n) { output[idx] = input[idx] * 2.0f; } }内核启动后强制刷新输出:
my_kernel(...) torch.cuda.synchronize()printf输出是异步的,不 synchronize 就可能丢失。
Warp 专用内核:选择正确的打印线程
问题:
threadIdx.x == 0只打印 block 中第一个 warp 的信息;- 对 warp-specialized 内核,这往往错过真正出错的 warp 或 specialization 组。
更好的模式(每个 warp 的第一个 lane 打印):
__global__ void WarpSpecializedKernel(...) { // 示例:每个 warp 的第一个 lane if ((threadIdx.x % 32) == 0) { printf("warp=%d\n", threadIdx.x / 32); } }或者,如果内核按更大的 specialization 组组织,改为每组打印一次而非每 block 一次。
常见错误(只有 warp 0 打印):
// 只有 warp 0 会打印 if (threadIdx.x == 0) { printf("warp=%d\n", threadIdx.x / 32); }快速参考
| 内核类型 | 打印条件 | 说明 |
|---|---|---|
| 简单内核 | threadIdx.x == 0 | 每 block 一个线程通常足够 |
| Warp 专用内核 | 每个 warp 选一个代表 lane | 例如threadIdx.x % 32 == 0 |
| 组专用内核 | 每组选一个代表 lane | 依据内核的调度布局选择 |
其他内核调试手段
assert(value >= 0.0f && "value must be non-negative"); static_assert(BLOCK_SIZE % 32 == 0, "BLOCK_SIZE must be warp aligned");static_assert能在编译期捕获 warp 对齐等结构性问题,与运行期断言互补。
环境变量参考
| 变量 | 取值 | 说明 |
|---|---|---|
SGLANG_KERNEL_API_LOGLEVEL | 0 | 关闭日志(默认值) |
1 | 仅函数名 | |
3 | 输入输出 + 元数据 | |
5 | Level 3 + 张量统计(min/max/mean/nan/inf) | |
10 | Level 5 + 崩溃安全张量 dump | |
SGLANG_KERNEL_API_LOGDEST | stdout | 输出到 stdout |
stderr | 输出到 stderr | |
<path> | 输出到文件 | |
log_%i.txt | %i展开为进程 ID | |
SGLANG_KERNEL_API_DUMP_DIR | <path> | Level 10 dump 目录(同样支持%i) |
SGLANG_KERNEL_API_DUMP_INCLUDE | 通配符列表 | 仅 dump 匹配的 API |
SGLANG_KERNEL_API_DUMP_EXCLUDE | 通配符列表 | 跳过匹配的 API |
日志系统的几个实现细节
- 零开销设计:日志级别为 0 时,
debug_kernel_api直接返回原函数,装饰器不会给热路径增加任何运行时开销(kernel_api_logging.py);maybe_wrap_debug_kernel也在环境变量为 0 时直接透传原函数(debug_utils.py)。 - 防重复包装:包装函数带有
_debug_kernel_wrapped标记,重复装饰会被安全跳过(kernel_api_logging.py)。 - torch.compile 兼容:
torch.compiler.is_compiling()为真时 wrapper 直接透传原函数,避免干扰图编译(同文件 L418-L420)。 - 统计跳过:CUDA graph capture 期间 Level 5 统计被有意跳过(标记为
statistics=[skipped: CUDA graph capture in progress]),Level 10 dump 同样跳过(Tensor dump skipped: CUDA graph capture in progress),因为统计与 dump 都需要同步/拷贝到 CPU,在 capture 期间不允许。 - 函数名推断:默认函数名由
__qualname__与模块名组合而成,并剥离sglang./sgl_kernel.前缀,得到sglang.quant_method.UnquantizedLinearMethod.apply这类紧凑名称;也支持op_name显式覆盖(同文件 L367-L384)。
最佳实践
1. 从 Level 3 开始
export SGLANG_KERNEL_API_LOGLEVEL=3Level 3 通常足以捕获错误的形状、dtype 和设备放置。
2. 数值问题用 Level 5
export SGLANG_KERNEL_API_LOGLEVEL=5怀疑 NaN/Inf 时使用。
3. 崩溃复现用 Level 10
export SGLANG_KERNEL_API_LOGLEVEL=10进程在你能检查实时张量之前就崩溃时,这是最有用的模式。
配套建议:
- 需要真实模型运行的成功路径输入/输出 dump 时,临时为该调试会话关闭 CUDA graph;
- Level 10 过于嘈杂时,配合
SGLANG_KERNEL_API_DUMP_INCLUDE/SGLANG_KERNEL_API_DUMP_EXCLUDE精确圈定 API,而不是对所有覆盖的 API 全量 dump。
4. 崩溃时写文件而非 stdout
export SGLANG_KERNEL_API_LOGDEST=crash.log进程中止时,文件日志比 stdout 更安全。
5. 生产环境关闭日志
unset SGLANG_KERNEL_API_LOGLEVEL关闭时装饰器返回原始可调用对象,不增加运行时日志开销(见上文"零开销设计")。
故障排查
没有日志输出
依次检查:
echo $SGLANG_KERNEL_API_LOGLEVEL—— 确认级别不为 0;echo $SGLANG_KERNEL_API_LOGDEST—— 确认输出目标;- 失败路径是否经过受覆盖的 API 边界——纯 PyTorch 调用默认不在覆盖范围内。
输出太多
降低级别:
export SGLANG_KERNEL_API_LOGLEVEL=3CUDA Graph Capture 期间统计被跳过
如果看到:
statistics=[skipped: CUDA graph capture in progress]这是预期行为:Level 5 统计在 CUDA graph capture 期间被有意跳过,以避免同步副作用。
CUDA Graph Capture 期间张量 dump 被跳过
如果看到:
Tensor dump skipped: CUDA graph capture in progress同样是预期行为:Level 10 dump 需要把张量拷贝到 CPU,这在 CUDA graph capture 期间不允许。
总结:一套完整的崩溃排查工作流
把以上步骤串成一条可复用的排查链路:
- 首次崩溃:
SGLANG_KERNEL_API_LOGLEVEL=3+LOGDEST=file,重跑,从日志找"有输入无输出"的边界; - 数值异常:升级到
LOGLEVEL=5,检查 min/max/mean/nan/inf,判断坏值来源; - 需要回放现场:升级到
LOGLEVEL=10+DUMP_DIR,拿到inputs.pt/metadata.json(execution_status: "exception"即崩溃点证据); - 多进程:日志与 dump 路径加
%i按 PID 隔离; - 越界写等内存问题:叠加
compute-sanitizer --tool memcheck定位内核,用日志定位输入; - 需要栈回溯:叠加
cuda-gdb,where后与日志交叉比对; - 内核自持:在自定义 CUDA 内核中按 warp/组选择代表线程
printf,配合torch.cuda.synchronize()与assert/static_assert。
这套方法论的依据都落在仓库中:日志核心实现见 python/sglang/kernels/kernel_api_logging.py,LLM 侧接入见 python/sglang/srt/utils/custom_op.py,Diffusion 侧接入见 python/sglang/multimodal_gen/runtime/layers/custom_op.py 与 python/sglang/multimodal_gen/runtime/layers/utils.py,AOT 内核条件包装见 python/sglang/kernels/aot/python/sgl_kernel/debug_utils.py。下次再遇到"莫名其妙"的 CUDA 崩溃,先开日志,再谈猜测。
【免费下载链接】sglangSGLang is a high-performance serving framework for large language models and multimodal models.项目地址: https://gitcode.com/GitHub_Trending/sg/sglang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考