AI Pro R9700 单卡 SGlang qwen3.8 27b
-
@fcme 好像是因为不支持WMMA影响了prefill 的速度。 但是我搞不定,因为都是让Hermes + Deepseek V4.1 flash抄作业的

-
@paul-hou 今天走通了我也很高兴,赶紧让Hermes把配置和过程给总结下来,跟大家分享一下。具体测试我有空再做,或者其他有r9700的朋友们有空也可以测一下
-
-
说明:本文原为英文,已按原文逐段译为中文——技术内容一字未改,命令、路径、参数、代码块全部原样保留。译文由小特整理。
今天让Hermes抄flyer666的作业,单卡r9700跑SGlang 成功了,跟大家分享一下,同时感谢Terry建立这个环境供大家一起学习,也感谢各位高手分享AI知识。
在 AMD Radeon R9700 (gfx1201) 上用 SGLang 运行 Qwen3.8-27B(W4A16 GPTQ,4-bit)
一份完整、可复现的构建与运维指南。它是为了能在另一台机器上(带 R9700)照着执行而写的
——无论是人还是 agent——每个阶段之后都设有一个校验点,这样失败会在它发生的地方
被发现,而不是等到三个阶段之后。这里的一切都是在参考机器上凭经验确立的,实测数字都是真实的,不是估算出来的。凡是
属于估算、或从未测试过的内容,都会明确标注出来。约定。 把
$HOME换成你的家目录(/home/<user>)。
像<API-KEY>、<BOT-TOKEN>、<server-ip>这样的占位符需要你自己填写。
命令面向 Ubuntu 24.04 + Bash。
0. 这套方案能达成什么
模型 Qwen3.8-27B(mamba/attention 混合 VL 模型,archQwen3_5ForConditionalGeneration),4-bit GPTQ W4A16GPU 1× Radeon AI PRO R9700,32 GB, gfx1201(RDNA4)引擎 Docker 中的 SGLang 0.5.13.post1( rocm/sgl-dev),TP=1Decode 27 t/s(普通)· 43–62 t/s(使用 NEXTN/MTP 投机) Prefill @4K 244–247 t/s(scalar GPTQ 路径——原因见 §5.4) KV pool 仅 device 295,902 tokens (fp8_e4m3) · 带 HiCache host pool 时 887,707 tokens 上下文 每请求 131,072 视觉 支持——vision tower 常驻 权重占用显存 17.95 GB 24 GB 的卡不行:checkpoint 约 18.6 GiB,而你还需要实实在在的余量留给 KV、
graph buffer 和 workspace。
1. 前置条件
1.1 硬件 / 操作系统
- 1× Radeon R9700 (gfx1201),32 GB——或任意 32 GB 以上的 gfx1201 显卡
- 如果想要 HiCache(longctx)档,系统内存 ≥ 62 GB。其中 21 GB
被 pinned host pool 吃掉。speed档不需要 host pool。 - 约 120 GB 可用磁盘:Docker 镜像约 71 GB + 模型约 19 GB + 构建树约 1–3 GB
- Ubuntu 24.04(或类似版本),使用内核自带的
amdgpu驱动。宿主机侧不需要安装
ROCm——容器镜像自带 userland;宿主机只需要内核驱动,再加上暴露出来的
/dev/kfd与/dev/dri/*。
1.2 Docker + GPU 访问
sudo apt-get update sudo apt-get install -y docker.io sudo systemctl enable --now docker sudo usermod -aG docker "$USER" # then LOG OUT and back in校验点 1 — 确认 GPU 设备节点存在且守护进程可用:
ls -l /dev/kfd /dev/dri/ # expect /dev/kfd + renderD128.. cards docker info >/dev/null && echo "docker OK"
️ 两个会白白浪费时间的坑:- 组成员身份只对新会话生效。在已经打开的 shell 里,
给 Docker 命令加上sg docker -c "..."前缀即可,不必重新登录。 - 后面会看到,systemd user unit 无法把自己排在系统级
docker.service之后。应改用就绪循环等待守护进程(§9.1)。
1.3 识别你的 render 节点并确认架构
for c in /sys/class/drm/card*/device/uevent; do echo "$c $(grep -o 'PCI_ID=.*' $c)" done cat /sys/class/drm/card*/device/mem_info_vram_total # bytes记下属于 R9700 的那个
renderDxxx节点——下面每条命令都会用到它。在多卡机器上,
只把这一个节点传进容器,才能让另一张卡保持空闲。校验点 2 — 在容器内,架构必须读出
gfx1201:IMG=rocm/sgl-dev:v0.5.13.post1-ubuntu24.04-py3.14-rocm7.14.1 docker run --rm --device /dev/kfd --device /dev/dri/renderD129 \ --group-add=video --security-opt seccomp=unconfined \ -e HIP_VISIBLE_DEVICES=0 -e ROCR_VISIBLE_DEVICES=0 \ $IMG bash -lc 'rocminfo | grep -m1 gfx' # expected: gfx1201这一步要是失败,就停下来——后面任何东西都不会工作。
2. 拉取镜像
docker pull rocm/sgl-dev:v0.5.13.post1-ubuntu24.04-py3.14-rocm7.14.1约 71 GB。镜像内含:Python 3.14、HIPCC/ROCm 7.14、Triton 以及 SGLang wheel。
请固定这个 tag——§5 的补丁集针对的正是这个确切版本。
3. 获取模型
3.1 用哪个 checkpoint,以及为什么
使用
Vishva007/Qwen3.8-27B-W4A16-AutoRound-GPTQ(约 19 GB)。为什么偏偏用这一个:它是货真价实的 GPTQ W4A16 打包(4-bit 权重、
16-bit 激活),并且有意把它的 MTP/NEXTN head 留在 bf16,这样投机解码仍然能用。
这个模型上所有打着 "AWQ" 招牌的 checkpoint 实际上都是 Quark 或 compressed-tensors
格式,SGLang 在 RDNA 上跑不了(见附录 C)。mkdir -p "$HOME/models/sglang" hf download Vishva007/Qwen3.8-27B-W4A16-AutoRound-GPTQ \ --local-dir "$HOME/models/sglang/qwen3.8-27b-w4a16-gptq"预期是 5 个权重分片 +
model_extra_tensors.safetensors+ 常规的那些配置文件。3.2 硬卡点 #1 — checkpoint 的
config.json会让 loader 挂掉该 checkpoint 自带两条动态量化规则,把 MTP tensor 纳入了量化:
"+:.*mtp.*": {...}, "+:.*mtp\\.fc.*": {...}但那些 tensor 是以 bf16 存储的,所以 GPTQ loader 会去反量化未量化的数据然后炸掉。
把这两条替换成一条排除(exclusion)规则。#!/usr/bin/env python3 """Fix the MTP dynamic-quant rules. Idempotent; keeps a one-time .orig backup.""" import json, os, shutil, sys path = sys.argv[1] if len(sys.argv) > 1 else \ os.path.expanduser("~/models/sglang/qwen3.8-27b-w4a16-gptq/config.json") cfg = json.load(open(path)) qc = cfg.get("quantization_config", {}) dyn = qc.setdefault("dynamic", {}) removed = [k for k in list(dyn) if k.startswith("+") and "mtp" in k.lower()] for k in removed: del dyn[k] added = False if not any(k.startswith("-") and "mtp" in k.lower() for k in dyn): dyn["-:.*mtp.*"] = {"bits": 16, "group_size": 128} added = True if removed or added: bak = path + ".orig" if not os.path.exists(bak): shutil.copy(path, bak) json.dump(cfg, open(path, "w"), indent=2) print(f"patched: removed {len(removed)} include-rule(s), added exclusion: {added}") else: print("already patched (nothing to do)") print("mtp rules now:", {k: v for k, v in dyn.items() if "mtp" in k.lower()}) print("quant:", qc.get("quant_method"), qc.get("bits"), "group", qc.get("group_size"))校验点 3 — 运行之后,
quantization_config必须显示
gptq / bits 4 / group_size 64,而且唯一的 MTP 规则必须是
-:.*mtp.*,带bits: 16。未量化的 tensor(embeddings、MTP head、
mamba/conv 参数)是 bf16 → 它决定了 §6 中的 dtype 选择。
4. 构建之前先理解差距
有两个事实使得自建成为必须——请自行验证它们,以确认在你所用的版本上
是否仍然需要这个 fork:# (a) The prebuilt wheel ships NO GPTQ ops docker run --rm $IMG bash -lc 'python3 -c " import torch, sgl_kernel as s print([x for x in dir(s) if \"gptq\" in x.lower()])"' # expect: [] (empty) # (b) Upstream has no ROCm W4A16 int4 quant *scheme* beyond these: docker run --rm $IMG bash -lc \ 'grep -rhoE "quark_w4[a-z0-9_]+" /sgl-workspace/sglang/python/sglang/srt/layers/quantization/quark/ | sort -u' # quark_w4a4_mxfp4 / _moe / quark_w4a8_mxfp4_moe — MXFP4 is CDNA+AITER only.所以:RDNA 上的 GPTQ 支持必须自己编译进去。
5. 为 gfx1201 构建 GPTQ kernel
5.1 获取 fork
mkdir -p "$HOME/sglang-sandbox/src" && cd "$HOME/sglang-sandbox/src" git clone -b gfx1100-support https://github.com/StevenChenSE/sglang.git sglang-gfx1100 cd sglang-gfx1100 && git log -1 --format='%H %s' # reference: f84475ccf593087df3d1da13c9e675eadee12352这个分支带有 RDNA3 的 GPTQ kernel(
csrc/gemm/gptq/q_gemm_rdna3*.{hip,cu}、
q_gemm_rdna3_wmma.*、moe_q_gemm_rdna3.hip),一个 ROCm 版setup_rocm.py,
以及它自己的tests/test_gptq_rdna3_smoke.py。5.2 为什么原样无法为 gfx1201 构建
三道各自独立的关卡,全都只认 gfx1100:
- scalar 反量化 GEMM 主体被
defined(__gfx1100__)守护; - 一处运行期
TORCH_CHECK断言架构是 gfx1100; - WMMA translation unit 使用了
wmma-256b-instsbuiltin——只有 RDNA3 才有
的指令。Clang 拒绝为 gfx1201 编译它们,而强行开启该 feature 会让
clang-23 段错误。没有绕法;这个 TU 必须去掉。
关键在于,在 RDNA 上通用的
gptq_gemmop 会直接路由进
gptq_gemm_rdna3,所以 scalar 路径就是全部实现——WMMA
TU 是一个独立的、可选的 op。5.3 硬卡点 #2 — 跨 4 个文件的 5 处修改
把这些改动应用到 fork 上(§12 的脚本会替你完成;这里说的是它们改了什么、以及为什么)。
请以源码文本作为锚点,不要依赖行号。(1)+(2)
csrc/gemm/gptq/q_gemm_rdna3.hip和q_gemm_rdna3.cu—— 放宽
架构守护,让 scalar 主体能为 RDNA4 编译:// before #if defined(__HIPCC__) && defined(__gfx1100__) // after #if defined(__HIPCC__) && (defined(__gfx1100__) || defined(__gfx1201__))(3) 同样这两个文件 —— 当 WMMA 被排除时,WMMA dispatch 和只在 gfx1100 成立的
运行期断言都不得被编译:#ifndef SGL_GPTQ_NO_WMMA ... existing WMMA dispatch + TORCH_CHECK(arch == gfx1100) ... #endif(4)
csrc/common_extension_rocm.cc——gptq_gemm_rdna3_wmma的extern
声明以及它的m.def/m.impl注册也必须加上守护,否则
链接时会出现未定义符号:#ifndef SGL_GPTQ_NO_WMMA ... extern decl + m.def/m.impl for gptq_gemm_rdna3_wmma ... #endif(5)
python/sglang/kernels/aot/setup_rocm.py—— 对于非 gfx1100 的 RDNA
目标:去掉 WMMA 源文件,并为两个编译器都定义该宏:hipcc_flags.append("-DSGL_GPTQ_NO_WMMA") cxx_flags.append("-DSGL_GPTQ_NO_WMMA")setup_rocm.py里还有一个RDNA_TARGETS = {gfx1100, gfx1151, gfx1201}集合,
如果你的卡是别的 RDNA4 SKU,就需要扩展它。5.4 构建
DEV=/dev/dri/renderD129 TARGET=gfx1201 JOBS=16 \ bash "$HOME/sglang-sandbox/build-gptq-kernels.sh"脚本在镜像内运行
setup_rocm.py build_ext --inplace,只暴露一个 DRM 节点,
并把AMDGPU_TARGET钉死,这样架构探测不会漂移。产物:
python/sglang/kernels/aot/python/sgl_kernel/common_ops.cpython-314-x86_64-linux-gnu.so
(约 25 MB,32 个 gfx1201 code object,约 86 个 gptq 符号)。如果构建因为删不掉上一次尝试留下的 root 所属对象而失败,就通过容器删除
build 目录(容器是以 root 运行的):
docker run --rm -v <src>:/src $IMG bash -lc 'rm -rf /src/python/sglang/kernels/aot/build; chown -R 1000:1000 /src'校验点 4 — 用数值验证,而不是凭感觉。改造 fork 自带的 smoke
test(constant-nibble 技巧 → 解析上已知的结果),要求
在 R9700 上 35/35 项检查通过,maxdiff = 0,覆盖 fp16 和 bf16、M=1→128、
GPTQ v1 与 v2、gptq_shuffle,以及生产场景(bf16 激活 + fp16
scale)。只有那个 WMMA op 可以跳过,因为你是刻意不构建它的。放弃 WMMA 的实际代价在 prefill,不在 decode:
scalar 路径(本方案) 带 WMMA 的 gfx1100 构建(参考) decode 27.2 t/s 2×7900XTX TP=2 上约 80 t/s prefill @4K 246 t/s 每卡约 934 t/s Decode 受带宽限制,因此不受影响(27.2 t/s ≈ 640 GB/s ÷ 18.6 GB 上限的
80 %)。真正的牺牲品是 prefill——大约 3.8×。 要想把它找回来,
需要一个原生的 W4A8/FP8 RDNA4 kernel。
6. 硬卡点 #3 — dtype 必须是 bf16,而 SGLang 禁止它
上游 SGLang 把 GPTQ 限制为 fp16:
torch.bfloat16 is not supported for quantization method gptq但 checkpoint 里未量化的 tensor 是 bf16,而这个模型是 mamba/attention
混合架构,它的causal_conv1dTriton kernel 在 bf16/fp16 混用时会硬失败
(Mismatched type for col0)。所以你必须跑 bf16——也就必须
给 SGLang 打补丁以允许它。把镜像里的
sglang/srt/layers/quantization/gptq/gptq.py复制出来,然后在GPTQConfig中:@classmethod def get_supported_act_dtypes(cls) -> List[torch.dtype]: return [torch.half, torch.bfloat16] # upstream is [torch.half] only依据:§5 构建的 kernel 能处理 bf16 激活——35/35 的 smoke test 证明的正是这一点
(bf16 激活 + fp16 scale)。运行时把打过补丁的文件挂载覆盖镜像里的副本(§7)。
7. 启动脚本
标准调用方式。保存为
$HOME/sglang-sandbox/run-sglang.sh,并修改
顶部的路径。#!/bin/bash set -euo pipefail IMG=${IMG:-rocm/sgl-dev:v0.5.13.post1-ubuntu24.04-py3.14-rocm7.14.1} MODELS_DIR=${MODELS_DIR:-$HOME/models/sglang} MODEL_SUBDIR=${MODEL_SUBDIR:-qwen3.8-27b-w4a16-gptq} PORT=${PORT:-30001} CTX=${CTX:-131072} PAGE=${PAGE:-64} MEMFRAC=${MEMFRAC:-0.78} KV=${KV:-fp8_e4m3} DTYPE=${DTYPE:-bfloat16} CTR=${CTR:-sglang-gptq} RENDER_NODE=${RENDER_NODE:-/dev/dri/renderD129} # bf16 GPTQ allowance (see §6) GPTQ_PY=${GPTQ_PY:-$HOME/sglang-sandbox/patches/gptq.py} PY_MOUNT="" [ -f "$GPTQ_PY" ] && PY_MOUNT="-v $GPTQ_PY:/sgl-workspace/sglang/python/sglang/srt/layers/quantization/gptq/gptq.py" # self-built sgl_kernel with the GPTQ ops (see §5) GPTQ_SO=${GPTQ_SO:-$HOME/sglang-sandbox/src/sglang-gfx1100/python/sglang/kernels/aot/python/sgl_kernel/common_ops.cpython-314-x86_64-linux-gnu.so} SO_MOUNT="" if [ -f "$GPTQ_SO" ]; then SO_MOUNT="-v $GPTQ_SO:/opt/venv/lib/python3.14/site-packages/sgl_kernel/common_ops.cpython-314-x86_64-linux-gnu.so" else echo "[!] built sgl_kernel NOT found at $GPTQ_SO — GPTQ ops will be missing"; exit 1 fi SPEC_FLAGS="" [ "${SPEC:-1}" = "1" ] && SPEC_FLAGS="--speculative-algorithm NEXTN --speculative-num-steps 3 --speculative-eagle-topk 1 --speculative-num-draft-tokens 4" HICACHE_FLAGS="" [ "${HICACHE:-0}" = "1" ] && HICACHE_FLAGS="--enable-hierarchical-cache --hicache-ratio 2 --hicache-io-backend direct --hicache-write-policy write_through" # a crashed container stays "Up" and holds the port + GPU sg docker -c "docker rm -f $CTR" >/dev/null 2>&1 || true exec sg docker -c "docker run --rm --name $CTR \ --device /dev/kfd \ --device $RENDER_NODE \ --network=host \ --ipc=host \ --group-add=video \ --cap-add=SYS_PTRACE \ --security-opt seccomp=unconfined \ -e HF_HOME=/app/models \ -e SGLANG_USE_AITER=0 \ -e SGLANG_ROCM_FUSED_DECODE_MLA=0 \ -e HIP_VISIBLE_DEVICES=0 \ -e ROCR_VISIBLE_DEVICES=0 \ -e PYTORCH_HIP_ALLOC_CONF=expandable_segments:True \ -e PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True \ -v $MODELS_DIR:/app/models \ $SO_MOUNT $PY_MOUNT \ $IMG \ python3 -m sglang.launch_server \ --model-path /app/models/$MODEL_SUBDIR \ --quantization gptq \ --dtype $DTYPE \ --tp-size 1 \ --mamba-ssm-dtype bfloat16 \ --attention-backend triton \ --triton-attention-num-kv-splits 16 \ --context-length $CTX \ --kv-cache-dtype $KV \ --mem-fraction-static $MEMFRAC \ --max-running-requests 4 \ --max-mamba-cache-size 20 \ --chunked-prefill-size 2048 \ --page-size $PAGE \ --schedule-policy lpm \ --reasoning-parser qwen3 \ --tool-call-parser qwen3_coder \ --enable-metrics \ --chat-template /app/models/$MODEL_SUBDIR/chat_template-fixed.jinja \ --host 0.0.0.0 --port $PORT \ $SPEC_FLAGS $HICACHE_FLAGS"为什么用这些参数
参数 原因 --tp-size 1单卡;跨混合架构做 TP=2 不可行(附录 C) --quantization gptq+ 挂载的.so这些 op 在上游并不存在 --dtype bfloat16+--mamba-ssm-dtype bfloat16checkpoint 的 tensor 是 bf16;mamba kernel 不接受混合 dtype --attention-backend triton这些 kernel 所需的组合 --triton-attention-num-kv-splits 16RDNA 上的吞吐 --kv-cache-dtype fp8_e4m3在速度和效果完全相同的前提下 KV pool 翻倍(295,902 对 148,271 tokens) --page-size 64只要 radix cache 处于启用状态就会被忽略/强制为 1(§8)--schedule-policy lpmlongest-prefix-match 调度 --chunked-prefill-size 2048prefill 调度粒度 --max-mamba-cache-size 20混合 SSM 状态槽位(48 个 Gated-DeltaNet + 16 个 full-attention 层) --max-running-requests 4并发上限;让 KV 余量保持在合理范围 --mamba-ssm-dtype见 dtype 那一行 --enable-metrics暴露 /metrics——§8 的 HiCache + spec 计数器需要它--chat-template .../chat_template-fixed.jinja§7.2 有两种 dtype 值得一说:
fp8_e5m2会被 CLI 接受,但分配器实际创建的
仍然是 E4M3——在这个目标平台上 AMD FP8 只有 E4M3,所以实际上
并不存在 e5m2 这个选项。校验点 5 — 首次成功启动。留意以下四行:
KV Cache is allocated. dtype: torch.float8_e4m3fn, #tokens: 295902, ... The server is fired up and ready to roll!并做端到端确认:
curl -s localhost:30001/v1/models | jq -r '.data[].id' curl -s localhost:30001/v1/chat/completions -H 'Content-Type: application/json' \ -d '{"model":"any-alias","messages":[{"role":"user","content":"say OK"}],"max_tokens":8}' | jq -r '.choices[0].message.content'服务器接受任意 model 字符串,作为这个已加载模型的别名——
所以你可以给客户端编一个好记的名字(§10)。7.2 硬卡点 #4 — 模型自带的 chat template 会拒绝标准客户端
这一条让一个符合标准的客户端(DeepSeek Harness)在每个请求上都吃到
HTTP 400,而 curl 却一切正常。这两个错误都来自模型自带的
chat_template.jinja——不是 SGLang 的问题:Unexpected message role. # chat_template.jinja Unexpected reasoning effort high. Supported types are xhigh (default), medium, and low. # chat_template.jinja两处不匹配:
role: "developer"—— OpenAI 规范中system的继任者,由
现代 SDK/harness 发出。而模板只处理 system/user/assistant/tool。reasoning_effort词表夹击。 OpenAI 的字段(以及 SGLang
为它定义的 pydantic schema)接受none|low|medium|high|max,而模板
只认得xhigh|medium|low。于是high/max通过了校验、随后
死在模板里,而xhigh在模板看到它之前就已经死在 SGLang 的 schema 里。
过去只有none|low|medium能同时穿过这两层。
在服务端修复,让所有客户端都受益——补丁打在模板上,而不是
逐个客户端去改:{#- 1. normalise the OpenAI effort vocabulary onto the template's three levels -#} {%- set resolved_reasoning_effort = reasoning_effort|default('xhigh') %} {%- if resolved_reasoning_effort in ('high', 'max') %} {%- set resolved_reasoning_effort = 'xhigh' %} {%- elif resolved_reasoning_effort in ('none', 'minimal') %} {%- set resolved_reasoning_effort = 'low' %} {%- endif %} {#- 2. treat the developer role exactly like system -#} {%- if messages[0].role in ('system', 'developer') %} {%- if message.role in ("system", "developer") %}把它写到与原文件并列的
chat_template-fixed.jinja(原文件保持不动),
再用--chat-template让服务器指向它。校验点 6 — 完整矩阵必须全部返回 HTTP 200:
请求 预期 role: developer200 reasoning_effort: none200,0 个 reasoning 字符(证明禁用路径生效) reasoning_effort: low / medium / high / max200 chat_template_kwargs: {reasoning_effort: xhigh}200
8. 两套运行档,因为在 ROCm 上它们互斥
只要 speculative decoding 开着,SGLang 就会自动禁用 radix cache:
Speculative decoding for Qwen3_5ForConditionalGeneration is not compatible with radix cache on ROCm devices. Automatically disabling radix cache.而 HiCache 就是 radix cache 的一个扩展 —— 所以
--enable-hierarchical-cache
加上 speculation 会在启动时直接中止,报
enable-hierarchical-cache and disable-radix-cache are mutually exclusive。与其跟它较劲,不如把两者都做成具名运行档:
运行档 Flags 设备池 有效容量 Decode 最适合 speed SPEC=1 HICACHE=0 MEMFRAC=0.78 KV=fp8_e4m3 PAGE=64133,791 tok 133,791 tok 43–62 t/s 交互式、写代码 longctx SPEC=0 HICACHE=1 MEMFRAC=0.90 KV=fp8_e4m3 PAGE=1295,902 tok 887,707 tok(device + 2× host) 27 t/s 长 agent 会话、大文档 这些数字背后的实测支撑
配置 KV pool Decode Prefill @4K bf16 KV,无 spec 148,271 27.2 t/s 246 t/s fp8_e4m3 KV,无 spec 296,542 27.1 t/s 244 t/s fp8_e4m3 + NEXTN 133,791 43.0 / 62.0 t/s 247 t/s - NEXTN/MTP speculation:
accept_length 3.1、accept_rate 0.70(对
greedy 无损 —— 输出与非 spec 运行逐字节相同)。Decode 之所以能突破
约 34 t/s 的带宽天花板,是因为现在每次权重遍历返回的是约 3 个被接受的
token,而不是 1 个。 - 代价:eagle worker 的缓冲区位于
mem-fraction-static之外,
所以 spec 会强制把 pool 压低。带 spec 时可用值是MEMFRAC=0.78——
0.90 / 0.85 / 0.80 都会把 draft worker 撑到 OOM(分别差 2.37 GiB / 2.37 GiB / 152 MiB)。
没有 spec 时,0.90 没问题。 - HiCache:
--hicache-ratio 2给出的 host pool 恰好是 591,805 tokens
(通过hicache_host_total_tokens实测),即 887,707 有效容量。代价约
22.6 GB 的 pinned host RAM(19.4 GB KV + 3.2 GB mamba)。已验证可用:
使用后hicache_host_used_tokens > 0,并且在 4k prompt 上 warm-prefix TTFT
16.67 s → 0.74 s(22.5×)。 - radix cache 生效时
--page-size 64会被静默强制成 1
(这个模型在no_buffermamba 调度下需要如此)—— 因此两套运行档
用了不同的 page size。 - 注意:warm-prefix 测试测的是 radix 复用。host pool 自己的职责 ——
把从 device 逐出的条目重新水化 —— 需要把 pool 灌过 296k tokens 才能测,
这一点没有尝试。
内存占用(longctx,实测)
Device (R9700): weights 17.95 GB · KV 9.03 GB (K 4.52 + V 4.52) · graph 0.25 GB → 30.50 / 31.86 GiB used (~1.4 GiB free) Host RAM: HiCache KV 19.39 GB + mamba 3.22 GB ≈ 21.1 GiB pinned/shared不要把
--mem-fraction-static调得比运行档里的值更高 —— 这点余量是
刻意留得很薄的(仅 device 时 0.90,带 spec 时 0.78)。
9. 自启动与启动顺序
9.1 systemd user unit
# ~/.config/systemd/user/sglang.service [Unit] Description=SGLang GPTQ server (R9700) After=network-online.target Wants=network-online.target [Service] Type=simple # A *user* unit cannot order against the system-level docker.service — wait for it. ExecStartPre=/bin/bash -c 'for i in $(seq 1 90); do (docker info >/dev/null 2>&1 || sg docker -c "docker info" >/dev/null 2>&1) && exit 0; sleep 2; done; exit 1' ExecStart=/bin/bash %h/sglang-sandbox/sglang.sh longctx # READINESS GATE — see below. Blocks the start job (and every After= dependent) # until the model actually answers. "-" = degrade to "started anyway" on timeout. ExecStartPost=-/bin/bash -c 'for i in $(seq 1 100); do curl -sf -o /dev/null --max-time 4 http://127.0.0.1:30001/v1/models && exit 0; sleep 6; done; exit 1' Restart=on-failure RestartSec=30 TimeoutStartSec=1200 TimeoutStopSec=120 KillMode=mixed [Install] WantedBy=default.targetsystemctl --user daemon-reload systemctl --user enable --now sglang.service9.2 为什么就绪门(readiness gate)是必需项,而不是可选项
模型加载要花约 4 分钟。该 unit 是
Type=simple,所以容器一启动 systemd 就把它标为
started —— 远早于端口开始响应。因此仅凭After=并不能阻止客户端与加载过程抢跑。
真正让After=变得有意义的是ExecStartPost里的探针。(在这台机器上做过实验验证:
一个Type=simple的 unit,其服务 15 s 后才开始响应,再配一个带After=/Wants=的
依赖单元 → 该依赖单元的 start job 正好耗时 15 s。)9.3 每个依赖单元上的顺序
给每个要跟模型通信的 unit 都放一个 drop-in:
# ~/.config/systemd/user/<unit>.service.d/wait-for-sglang.conf [Unit] Wants=sglang.service # not Requires= — a broken model must never block boot After=sglang.service需要意识到的后果:
Wants=意味着在模型没起来时启动这样一个 unit,也会启动
模型并等它就绪。
️ %在 unit 文件里是 systemd specifier。ExecStart里的date +%s会
静默变成用户的 shell 路径(%s→ shell),产出一堆乱东西
(例如一个内容为/bin/bash的文件)。请改用$(( ... ))算术,或转义为
%%。刻意使用的 specifier 没问题 —— 上面用的%h(家目录)就是有意为之。
用grep -c '%' <unit>审计。
10. 接入 OpenAI 兼容客户端
任何 OpenAI 兼容客户端都能用:
base_url http://127.0.0.1:30001/v1 api_key any non-empty placeholder (the server ignores auth) model any alias — e.g. qwen3.8-27b-gptq-r9700上下文长度: 服务端的每请求上限是
--context-length(这里是 131,072)。
客户端上声明 128,000,给输出留出空间。
️ Hermes Agent 特别强制要求最小 64,000 tokens 的上下文窗口
(agent 初始化时的MINIMUM_CONTEXT_LENGTH)。声明得比这更少,
每一个请求都会失败并报
ValueError: Model X has a context window of N tokens, which is below the minimum 64,000
—— 在它抵达服务端之前就失败了。自定义 provider 配置示例:
custom_providers: - name: sglang base_url: http://127.0.0.1:30001/v1 api_key: 'local-no-auth' model: qwen3.8-27b-gptq-r9700 models: qwen3.8-27b-gptq-r9700: context_length: 128000 # >= 64000 required by Hermes supports_vision: true # vision tower is resident — verifiedReasoning effort —— 打上 §7.2 的模板补丁后,标准 OpenAI
词表就能直接被接受(none|low|medium|high|max;high/max都
表示模型的最大值xhigh)。如果客户端支持显式 body 透传,也可以用
chat_template_kwargs: {reasoning_effort: xhigh}来直接控制模板。Vision 已验证可用(一张图片 → 期望的文本)。不需要
mmproj文件 —— SGLang 用的是 HF checkpoint 里的 vision tower
(enable_multimodal会自动检测到它)。调试客户端集成
客户端侧的 400 是常态;服务端日志往往只记录状态码,不记录
消息体。两个辅助手段:- 透传日志代理 —— 在另一个端口上跑一个极小的代理,把每个请求体
都 dump 出来,把客户端的base_url临时指向它,然后用 curl
重放捕获到的 body 来读真正的错误。§7.2 里的
developerrole 和reasoning_effort问题就是这样找到的。 - Payload 形状探针 —— 先发基线请求,然后一次加一个字段
(role: developer、max_completion_tokens与max_tokens、store、
tools、顶层的 effort 与chat_template_kwargs里的 effort),并把每个 HTTP 状态
连同它的响应体打印出来。
11. 运维
sglang.sh speed # NEXTN speculation: 43-62 t/s, 133k pool sglang.sh longctx # HiCache: ~888k effective, 27 t/s <- autostart default sglang.sh status # kernels, pool, VRAM/RAM breakdown sglang.sh stop运行时健康检查:
curl -s localhost:30001/get_server_info | jq '{ ctx: .context_length, tokens: .max_total_num_tokens, kv: .kv_cache_dtype }' curl -s localhost:30001/metrics | grep -E 'hicache_(host_used|host_total)_tokens' curl -s localhost:30001/metrics | grep -E 'spec_accept|accept_length' # speed profile切换运行档需要完整重新加载模型(约 3–4 分钟)。
12. 故障排查对照表
症状 原因 修复 torch.bfloat16 is not supported for quantization method gptq上游 dtype 限制 §6 补丁;确认它确实被挂载上了 Mismatched type for col0(Triton)mamba kernel 里混用了 bf16/fp16 --dtype和--mamba-ssm-dtype都跑 bf16GPTQ loader 在 MTP 张量上炸掉 checkpoint 的 +:.*mtp.*规则§3.2 torch.ops.sgl_kernel为空 / 没有 gptq 符号预编译 wheel 不带 GPTQ ops §5 构建并挂载那个 .soclang-23 segfault 为 gfx1201 强行开启 wmma-256b-insts永远不要开;去掉 WMMA TU(§5.2) 链接时出现 Undefined symbol common_extension_rocm.cc的注册没有被 guard§5.3 的编辑(4) 崩溃后容器显示 "Up" 但端口已死 崩掉的 scheduler 占着端口 + GPU 先 docker rm -f(launcher 会做)构建在删除 root 所属文件时失败 容器以 root 运行 通过容器去删构建目录 enable-hierarchical-cache and disable-radix-cache are mutually exclusivespec 与 HiCache 同时开 只用一套运行档(§8) eagle worker 里 spec OOM draft 缓冲区在 mem-fraction-static之外MEMFRAC=0.78Unexpected message role./Unexpected reasoning effort high.原生 chat template §7.2 模板补丁 Model X has a context window of N tokens, below the minimum 64,000客户端声明 <64k 声明 ≥64k(用 128,000) start job 在模型就绪之前就返回 Type=simple且没有门§9.2 就绪门 unit 输出里出现 %s/%n乱码%是 systemd specifier$(( ))或%%
附录 A —— 实测参考数字
参考机器:1× R9700(gfx1201,32 GB,31.86 GiB 可用)+ 62 GB RAM,Ubuntu
24.04,rocm/sgl-dev:v0.5.13.post1-ubuntu24.04-py3.14-rocm7.14.1。load time 12.2 s (weights 17.95 GB, quant=gptq bits=4) architecture Qwen3_5ForConditionalGeneration (hybrid: 48 GatedDeltaNet + 16 full attention) KV, bf16 148,271 tokens KV, fp8_e4m3 296,542 tokens (device pool at MEMFRAC=0.90) KV, fp8_e4m3+spec 133,791 tokens (device) at MEMFRAC=0.78 HiCache host pool 591,805 tokens (--hicache-ratio 2), + 3.22 GB mamba pool effective total 887,707 tokens decode 27.2 t/s plain · 43.0 t/s prose with NEXTN · 62.0 t/s predictable text prefill @4K 246 t/s (TTFT ~17 s cold) accept_length 3.1 accept_rate 0.70 quality 5/5 deterministic prompts, identical with fp8 KV and with spec warm-prefix TTFT 16.67 s → 0.74 s (22.5×)附录 B —— 复现清单
[ ] docker present, user in docker group, /dev/kfd + /dev/dri visible [ ] rocminfo inside the image reports gfx1201 [ ] checkpoint downloaded; config.json MTP rules fixed (§3.2) [ ] fork cloned at the pinned commit; 5 edits applied across 4 files [ ] kernel build completes; smoke test 35/35 maxdiff=0 on THIS card [ ] patched gptq.py in place (bf16 allowed) [ ] chat_template-fixed.jinja generated; role+effort matrix all 200 [ ] launcher boots: "server is fired up and ready to roll!" [ ] /v1/models + a chat completion both answer [ ] systemd unit enabled with the readiness gate; dependents ordered After= [ ] clients configured: alias, context_length >= 64000, vision if wanted附录 C —— 在 RDNA 上不可行的事(死路,附原因)
帮你省下它们耗掉的那几天:
- Quark / compressed-tensors int4 —— SGLang 只实现了
quark_w4a4_mxfp4、..._moe、quark_w4a8_mxfp4_moe、w8a8_fp8、
w8a8_fp8_moe。没有 W4A16 方案;Quark 会抛
NotImplementedError,compressed-tensors 则反量化到 bf16(约 54 GB)→ OOM。
这个模型所有打着 "AWQ" 幌子宣传的 checkpoint 都是 Quark/CT-pack,而不是
AWQ-pack。 - MXFP4 —— 仅限 CDNA3/4 + AITER。RDNA 没有 FP4 硬件。
- 单张 32 GB 卡上的 FP8 27B —— 权重 28.70 GiB 对 31.86 GiB 可用,
只剩约 3 GiB 给 KV + workspace。不可行。 - 跨不匹配 AMD 架构的
--tp-size 2(例如 gfx1100 + gfx1201)—— FP8/4-bit
kernel 逐架构不同,混合架构的 RCCL 很脆弱。 - 这个 fork 的 WMMA GPTQ 路径在 gfx1201 上 —— clang-23 segfault(§5.2)。
- vLLM (ROCm) int4 —— 未继续追的线索。AMD 的文档提到 AWQ 保持 INT4,而一个
ROCm vLLM fork 记录了VLLM_ROCM_USE_RDNA_W4A16(镜像
vllm/vllm-openai-rocm)。用 Quark checkpoint 时预计会看到
AttributeError: 'dict' object has no attribute 'endswith'。
如果你需要把 prefill 吞吐拿回来,值得一试。
-
@LearningAI 提醒一句:论坛以后请用中文发帖(外文帖有合规风险)。原帖已按原文译成中文、就地替换,本次警告一次,下不为例。
-
@LearningAI 既然感谢,你还发英语帖子?你让AI整理下,举手之劳,何必让论坛承担风险呢。下不为例。
@terry 啊? 英文帖子有风险?不明白啊。能解释一下吗?我平常都是用英文跟AI聊的。要是需要,以后发帖就让AI翻译成中文
-
T terry 于 锁定了此主题
-
T terry 于 解锁了此主题
-
T terry 于 将此主题固定
-
T terry 于 取消固定此主题
-
@fcme 好像是因为不支持WMMA影响了prefill 的速度。 但是我搞不定,因为都是让Hermes + Deepseek V4.1 flash抄作业的

-
@LearningAI
wmma一定要开启, 你让AI研究一下,一般是可以搞定的。 我主要跑35b的那个模型,开启了以后prefill的速度暴涨百分之35~40! 就很丝滑了@fcme 同意。wmma 在 RDNA4(gfx1201)上主要是把 prefill 从通用 FP32/BF16 路径切到矩阵核心,prefill 涨 30%+ 合理。
补两点,别把它当万能药:
- R9700 单卡约 640 GB/s,dense 27B 的 decode 基本贴这条线,wmma 几乎不改 tg,只改 pp。用 bench_serving 把 prefill 和 decode 分开测,别用混合 t/s 判断效果。
- 开 wmma 后 kernel 选择会变,先看 SGLang 日志有没有回落到非融合 matmul,再对一下 mem-fraction-static;RDNA4 的 FP8 和 attention 后端对版本敏感,跑不通就退一个后端对照。
35B A3B 是激活 3B 的 MoE,prefill 是主瓶颈,wmma 的收益会比 dense 27B 更明显。
-
@LearningAI
wmma一定要开启, 你让AI研究一下,一般是可以搞定的。 我主要跑35b的那个模型,开启了以后prefill的速度暴涨百分之35~40! 就很丝滑了@fcme 谢谢,我又折腾一整天,没成功,再上网看看有没有别的启发信息

-
@fcme 谢谢,我又折腾一整天,没成功,再上网看看有没有别的启发信息

-
@learningai
可以給原始連結嗎? 抄作業卡到一半了