一、是什么

让 125B 级 MoE 多模态大模型在单张 24GB 消费卡上以 4 路并发对外提供 OpenAI / Anthropic 双协议服务——上下文取满模型原生的 262,144,多模态开启,热态单流 ~105–130 tok/s、4 路合计 ~138 tok/s。

元信息

  • 引擎版本:Strata 0.1.38(源码编译,CUDA 12.4 toolkit,sm_89)+ 社区多路batch并发合并请求 + 本地加固补丁
  • 模型文件:官方固定 revision(ed59f92)的 GGUF,IQ3_XXS 76.7 GB(3-bit 混合精度 + imatrix 校准),逐文件 SHA-256 校验
  • 调用方式:https://api.ssemarket.cn/v1|/v1/chat/completions + /v1/messages|API key 鉴权
词 一句话解释
token(词元) 模型读写文本的最小单位,中文 1 token ≈ 1~1.5 个汉字;tok/s = 每秒生成的 token 数。
参数 / 125B 训练得到的约 1250 亿个数值,输入经它们计算出下一 token。
MoE(混合专家) 共 48 × 512 = 24,576 个专家子网络,每个 token 生成时仅激活 10 个,激活比 = 10/512 ≈ 2%。
路由 router 为每个 token 计算并选出 10 个相关专家的分配模块。
prefill / decode prefill 并行读入全部输入(快);decode 逐 token 串行生成输出(慢)。
KV cache 缓存已处理 token 的 K/V 中间结果以供复用,占用随上下文线性增长(26 万 token ≈ 3.1 GB,压缩存储)。
MTP / 投机解码 一次预测 4 个 token → 大模型批量验证 → 猜对即采纳,平均提速 1.6~1.8×。
量化(IQ3_XXS) 每个参数用 ≈ 3 bit 存储,体积压至原版约 1/5,精度损失很小。
显存 / 内存 / 磁盘 三级存储:速度 显存>内存>磁盘,容量 显存<内存<磁盘,数据按需逐级搬运。

整体画面(一张图):

graph LR
    U["你 / AI 应用"] -->|"一句话提问"| S["服务层(8080 端口)"]
    S -->|"读题 + 答题"| M["模型大脑(GPU1)"]
    M -->|"一块一块蹦出字块"| S
    S -->|"拼成完整回答"| U
    M -.->|"手边"| G["显存:最常翻的专家 + 对话小抄"]
    M -.->|"储藏室"| R["内存:全部 24,576 个专家"]
    M -.->|"书架"| D["磁盘:查表 + 模型原文件"]

部署日期:2026年10月5日


二、部署方法

2.1 权重与数据准备

步骤 做法 关键点
模型下载 独立下载脚本(hf-mirror 镜像,断点续传 + 字节数 + 官方 SHA-256)→ 集市云/shared/weights/ 通过ssemarket腾讯云主机在集市云机器下载模型
MTP 草稿层 mtp_fetch.py 按 HTTP Range 抠 31 个 mtp.* 张量(5.2 GB) 从 360GB/131 分片的 BF16 checkpoint 里取需要的范围
校验 pod 侧独立复核:模型 3 文件 + MTP 31/31 全部与官方一致 双路核对

2.2 引擎编译(Linux 没有官方预编译包)

  • 官方 release 只有 Windows 资产 → 必须源码编译;
  • 依赖 llama.cpp 源码(固定 commit):github.com 主站不通,用 codeload.github.com 预置进 third_party/llama.cpp/,setup 见即成全;
  • 主引擎 ~10 分钟;视觉编码器 strata-vision(CUDA)~15 分钟。

2.3 适配

问题 现象 对策
/shared 只读 setup 校验分片后要写 .done 标记 → 只读盘直接崩 pod 数据目录放符号链接指向 /shared:标记写本地、数据读共享、零拷贝;MTP 拷入 pod 后本地打包
CPU 线程超额 → CFS 节流 prompt 处理 5.5 tok/s(官方同档参考 1,750),累计被节流 2,242 秒 引擎 --pool-workers 默认"每个物理核"=76,而 cgroup 只有 16 核配额 → 改 12,节流归零
机械盘随机读 6.9 ms PLE 查表读盘拖慢每 token --ple-io ram 把 28.8GB 查表读进内存(mlock 因硬限 8MB 不可用,引擎自动降级为页缓存方案)

2.4 并发:集成社区合并请求

  • 本项目的开发者目标不做并发服务,于是社区的并发合并请求没有被理会
  • 对pr进行独立代码审查:引擎侧无内存/越界/算术缺陷;服务端有 3 个并发生命周期缺陷;
  • 本地加固补丁:①引擎重启 epoch 失效(在途批请求遇崩溃重启立即干净失败,杜绝跨请求串流)②unload 守卫 ③/v1/status 与 /props 报告真实槽位数 ④测试工具兼容单卡配置;
  • 全部改动可回滚。

三、哪一层放在哪:显存 / 内存 / 磁盘的实测分布

graph TB
    subgraph GPU1["RTX 4090 D · 24 GB 显存"]
        EC["专家缓存(自适应)≈12.8 GiB"]
        SL["批槽会话 ×4(各 0.95 GiB)<br/>QSA 常驻 KV 窗口 32768 cells + 索引器 + GDN 状态"]
        MTP["MTP 草稿层 835 MiB"]
        DEN["dense / native 投影 1,781 MiB"]
        VIS["视觉编码器 ~1.9 GiB(独立进程)"]
    end
    subgraph RAM["系统内存 100 GiB(cgroup 限额)"]
        AR["专家 arena ≈40–47 GiB(全部 24,576 个专家)"]
        KV["KV 全量 pinned 3.09 GiB(int8)"]
        PLE["PLE n-gram 表 28.8 GiB(页缓存)"]
        PK["pack / MTP 运行态 ≈2.3 GiB"]
    end
    subgraph DISK["磁盘"]
        SH["/shared 只读:模型分片 76.7 GB(符号链接接入)"]
        PKD["pack 目录:dense.bin 1.5 GB 等"]
    end
    EC <-->|"未命中经 PCIe(实测 23.4 GB/s)"| AR
    PLE -.->|"每 token 16 行(内存命中)"| RAM
层 放什么 实测数字 依据(启动日志/参数)
VRAM 24 GB 专家缓存(热专家 blob 常驻) 批前 10,243 专家 / 16.61 GiB;批 4 后自动缩至 ~12.8 GiB,实测命中 85.1% --expert-cache auto
4 个批槽会话 0.95 GiB/槽,共 3.8 GiB --batch 4 启动行
MTP 草稿层 835 MiB(专家 675 + dense 111) 启动行
dense/native 投影矩阵 300 个矩阵 / 1,781 MiB 启动行
视觉编码器(独立进程) ~1.2 GiB@1024 图像 tokens + 700 MiB 预留 --vision --vram-reserve-mib 700
KV 常驻窗口 每 QSA 层 32,768 / 262,144 cells --kv-resident 32768
RAM 100 GiB 专家 arena(全量专家) 载入 39.97 GiB(另含映射的 token embedding 260 MiB 等,总规划 ~47 GB) 启动行(载入耗时 ~760 s)
KV 全量(int8,全上下文) 3.09 GiB pinned 启动行
PLE 查表 320,001,536 行 × 160 维 28.8 GiB(页缓存;mlock 不可用降级) --ple-io ram
磁盘 /shared 模型分片 shard1 47.0 GB + shard2 28.8 GB + mmproj 0.87 GB 符号链接接入
pack / MTP dense.bin 1.5 GB;mtp tensors 5.2 GB + rt 0.8 GB

为什么这样分:dense 权重与热专家放显存(带宽最高);全部 24,576 个专家放内存(每个 token 只碰其中 10 个/层,命中率 85% 由缓存承担,其余走 CPU 池或 PCIe);PLE 查表,28.8GB 本部放在内存页缓存页。


四、跑起来的调度:从请求到输出的完整链路

4.1 进程与托管

graph TB
    P1["pod PID1(init)"] -->|"校验 pidfile+/proc cmdline 后自动拉起"| SUP["supervisord"]
    SUP --> SRV["serve/server.py(常驻, :8080)"]
    SRV -->|"stdin/stdout 文本管道<br/>GEN/BGEN · T/BT · BADM · DONE/BDONE · STOP/BSTOP"| ENG["engine/strata --serve"]
    SRV --> VISP["strata-vision(图像编码, GPU)"]
    ENG --> CUDA["CUDA kernels + CUDA graph(48 层/窗口)"]
    ENG --> POOL["CPU 专家池(12 workers + doorbell)"]

4.2 并发调度(重点)

  • HTTP 层:ThreadingHTTPServer 全部接收,不限连接数;
  • 单请求独占:走 solo 路径(GEN,带 MTP 投机解码——单流最快);
  • 第二请求到达:前一个请求被 STOP(引擎报 finish=cancel),无缝迁入批槽——携带"prompt + 已生成 tokens"续跑,prompt 缓存保证零重读;
  • 批窗口:每个 verify 窗口携带 4 个会话各一个 token——dense 权重 / 共享专家 / head 每窗口只读一遍给 4 路共用;各槽状态(GDN 递推、QSA K/V、索引器、PLE 历史)完全独立;
  • 准入:一次一个(ctl 锁串行);准入期间引擎把该请求当 GEN 1 读完 prompt 并产出第一个 token,然后 BADM 1 原子转交槽位(槽位状态用 checkpoint + 全量 KV 恢复,杜绝残留污染);
  • 槽满(>4 并发):入口排队等空槽,不拒绝;断开:BSTOP 让该槽在下一窗口干净收尾,其他槽继续;
  • 加固:引擎崩溃自动重启时,在途请求立即干净失败(503 + 原因),不串流、不挂死(epoch 机制);卸载接口在有请求时拒绝执行(实测见第六节)。
stateDiagram-v2
    state "空闲:进程常驻待命,等第一个请求" as S0
    state "solo 独跑:单流最快,MTP 草稿全开" as S1
    state "批槽并行:每窗口 4 路各推进 1 块" as S2
    state "门口排队:槽满不拒绝、不丢请求" as S3
    [*] --> S0
    S0 --> S1: 第 1 个请求进门
    S1 --> S2: 第 2 个请求到达:老请求被 STOP 携已生成内容迁入槽位
    S2 --> S2: 一个窗口一轮,直到有槽写完
    S2 --> S3: 第 5 个请求到达(4 槽全占)
    S3 --> S2: 有空槽释放(BDONE 正常收尾 / BSTOP 断连收尾)
    S2 --> S0: 全部写完

4.3 CPU 专家池与自适应缓存(引擎内部调度)

  • 路由出 10 个专家 id 后写 pinned “doorbell”,CPU 自旋收到立刻分流:显存命中→GPU 算;部分未命中→PCIe DMA(实测 23.4 GB/s);其余→CPU 就地算(AVX-512 内核,12 worker 池);
  • 专家缓存自适应换入(按实际路由统计),不做 LRU 淘汰;本部署实测命中 85.1%;
  • RouterLookahead 用下一层路由结果预取。
graph TD
    RT["Router:512 个专家里挑 10 个"] --> DB["按铃:把专家名单写到 pinned 共享便签"]
    DB --> Q{"这位专家此刻在哪?"}
    Q -->|"约 85%:显存缓存里"| GV["GPU 直接算"]
    Q -->|"部分未命中"| PC["PCIe 搬进显存<br/>实测 23.4 GB/s"]
    Q -->|"其余"| CP["CPU 池就地算<br/>AVX-512 内核 · 12 worker"]
    GV --> OUT["叠加公共专家 → 交给下一层"]
    PC --> OUT
    CP -->|"结果放回共享便签"| OUT

4.4 调度时序图(UML):一个请求从进门到进槽

sequenceDiagram
    autonumber
    participant C as 客户端
    participant S as 服务层(server.py)
    participant E as 引擎(strata)
    participant G as GPU1 + 内存

    C->>S: POST /v1/chat/completions(带 API key)
    S->>S: 验 key → 套聊天模板 → 切成字块<br/>(带图:先交 strata-vision 编码)
    alt 此刻没有别人在跑(独占)
        S->>E: GEN(读题 + 答题,允许打草稿)
    else 已有请求在跑(进批槽)
        S->>E: BGEN 槽位N(先按"独占读一次"把题读完<br/>并产出第一块)
        E-->>S: BADM 1("已转入槽位N")
        Note over S,E: 原来的独占请求会收到 STOP,<br/>无缝搬进另一个槽位继续写(不重读题)
    end
    loop 每块字块产出
        E-->>S: T / BT 槽位N(一块)
        S-->>C: 流式(SSE)推送这一块
    end
    alt 出现第 5 个并发(槽满)
        Note over S: 在门口排队等空槽,不拒绝、不丢请求
    end
    alt 客户端中途断开
        S->>E: BSTOP 槽位N(该槽这轮收尾后释放,其它槽不受影响)
    end
    E-->>S: DONE / BDONE(结束 + 统计)
    S-->>C: 收尾(usage、结束标记)

五、一个 token 生成,经过了什么

以一次对话请求为例(标 ✦ 的是本部署的配置点):

图 1:总览(先看这张定位)

flowchart LR
    S1["① 入口与调度<br/>鉴权 · 模板 · 视觉 · 选路"] --> S2["② Prefill<br/>分块读入 · 写 KV · PLE 查表"]
    S2 --> S3["③ Decode:48 层<br/>注意力 · 路由 · 专家"]
    S3 --> S4["④ 采样与提交<br/>MTP · 采样 · commit"]
    S4 --> S5["⑤ 回传<br/>T / BT · SSE / JSON"]
    S4 -. "还有 token,逐 token 循环" .-> S3

图 2:入口、鉴权与 solo/批槽选路

flowchart TB
    client(["客户端"]) --> http["HTTP :8080"]
    http --> auth{"api key 鉴权"}
    auth -- "失败" --> e401["返回 401,拦截请求"]
    e401 --> client
    auth -- "通过" --> svc["服务层 Service.run"]
    svc --> tpl["Jinja 渲染模型自带聊天模板<br/>(含思考档位指令)"]
    tpl --> tok["tokenize 分词"]
    tok --> img{"含图片?"}
    img -- "是" --> vis["strata-vision 视觉编码<br/>单图 → 1024 图像 tokens"]
    img -- "否" --> sched
    vis --> sched{"generate_batched<br/>并发调度"}
    sched -- "当前无伴跑请求(独占)" --> solo["发「GEN max_new ids」"]
    sched -- "已有请求在跑(批槽模式)" --> bgen["抢一个空槽(共 4 槽)<br/>发「BGEN slot max_new ids」"]

图 3:Prefill(读题)与批槽交接

flowchart TB
    in(["引擎 receive:GEN / BGEN ids"]) --> split["Prompt 按 8,192 token 分块<br/>走批量 prefill 路径"]
    split --> block["逐块经过 48 层"]
    block --> kvw["写入 KV:显存常驻窗口 +<br/>pinned RAM 全量(int8,3.09 GiB)"]
    block --> ple["PLE:每 token 按 3-gram 查 16 行<br/>命中内存页缓存(28.8 GiB,不读机械盘)"]
    kvw --> q{"是否 BGEN?"}
    ple --> q
    q -- "否(solo)" --> dec0["直接进入解码窗口"]
    q -- "是(BGEN)" --> first["prefill 读到第一个 token"]
    first --> hand["回「DONE」+「BADM slot 1」"]
    hand --> restore["服务端一次性转交槽位状态<br/>checkpoint + KV 全量恢复"]
    restore --> dec1["并入槽位流,进入解码窗口"]

图 4:Decode 每一步的 48 层内部(CUDA graph 捕获回放)

flowchart TB
    st(["解码一步开始"]) --> loop["进入下一层(48 层逐层串行)"]
    loop --> hc["a) hyper-connection 读取<br/>norm / down / up 三段"]
    hc --> type{"判断本层类型?"}
    type -- "GDN 层(共 36 层)" --> gdn["卷积 + 递推更新<br/>固定大小状态存于各槽 session"]
    type -- "QSA 层(共 12 层)" --> idx["索引器 append"]
    idx --> score["block 打分 → top-k 选块"]
    score --> attn["注意力计算:KV 先取显存常驻窗口 32,768 cells<br/>溢出部分从 pinned RAM 流式读取"]
    gdn --> rte
    attn --> rte["c) Router:512 专家打分 → top-10"]
    rte --> shared["共享专家:每层必算"]
    rte --> hit{"d) 10 个路由专家<br/>显存缓存命中?"}
    hit -- "命中 ~85%" --> vram["直接在显存计算"]
    hit -- "未命中 ~15%" --> way{"搬运方式"}
    way -- "CPU 池" --> cpu["doorbell + AVX-512 在 CPU 算<br/>(权重留在内存)"]
    way -- "PCIe" --> pcie["经 PCIe(实测 23.4 GB/s)搬进显存再算"]
    shared --> merge
    vram --> merge
    cpu --> merge
    pcie --> merge["本层专家输出加权合并"]
    merge --> head["e) 输出头 head → logits"]
    head --> nxt{"还有下一层?"}
    nxt -- "是,进入下一层" --> loop
    nxt -- "否,48 层走完" --> en(["交采样 / MTP 验证"])

图 5:解码窗口外的 MTP、采样、提交与回传

flowchart TB
    win(["解码窗口:本步生成 1 个 token"]) --> layers["48 层前向(见图 3)→ logits"]
    layers --> mode{"solo 请求?"}
    mode -- "是" --> mtp["MTP 草稿层(835 MiB 常驻显存)<br/>一次提 4 个草稿 token"]
    mtp --> verify["一个窗口批量验证<br/>草稿接受率 ~65%,加速 1.6–1.8×"]
    verify --> sample
    mode -- "否(批槽)" --> sample["采样:贪心(默认)<br/>或 Philox(seed, position),各槽按行独立"]
    sample --> commit["Commit:KV append / 索引器推进 /<br/>GDN 状态递进 / PLE 历史推进<br/>(批槽 commit 图紧随窗口,无需主机决策)"]
    commit --> more{"还有 token 要生成?"}
    more -- "是" --> win
    more -- "否" --> back["回传:solo「T id」/ 批槽「BT slot id」<br/>→ 服务端组装"]
    back --> rfmt{"返回方式"}
    rfmt --> sse["SSE 流式"]
    rfmt --> jpkg["整包 JSON"]
    sse --> done(["回客户端<br/>usage / 统计随行"])
    jpkg --> done

每窗口读的权重:dense+共享专家+head 只读一遍。

5.1 时序图(UML):一个 token 的诞生(独占模式)

sequenceDiagram
    autonumber
    participant S as 服务层
    participant H as 引擎主线程
    participant G as GPU1(显存)
    participant P as CPU 专家池(12 线程)
    participant R as 内存(专家仓库 + 小抄)

    S->>H: GEN(题目的所有字块)
    Note over H: 读题阶段:一次 8,192 块地整段读完<br/>(每块都记进 KV 小抄;本部署查表已在内存)
    Note over H,G: 之后进入答题循环,下面每转一圈 = 产出 1 块
    loop 48 层逐层过(整套动作事先录成 CUDA graph,逐层"回放")
        H->>G: 本层输入
        G->>G: 通道混合(Hyper-Connection:把上一层的汇总重新分配)
        alt 36 个线性注意力层(DeltaNet)
            G->>G: 递推更新,各层自带"记忆状态"
        else 12 个稀疏注意力层(QSA)
            G->>G: 拿当前问题给 KV 小抄打分,只取最相关的部分
            R-->>G: 常驻显存之外的老小抄从内存流入
        end
        G->>G: 分诊:512 个专家里挑 10 个
        alt 专家在显存缓存里(实测约 85%)
            G->>G: 直接计算
        else 未命中
            G-->>P: 按门铃(把要算的专家名单放到共享便签)
            P->>R: 从内存取这 10 个专家的权重
            P->>P: 用 AVX-512 就地算完(CPU 顶上一部分干活)
            P-->>G: 结果放回共享便签
        end
        G->>G: 叠加"公共专家"→ 交给下一层
    end
    G->>G: 输出头:算出"下一个字块"的候选分数表
    alt 独占模式下启用草稿(MTP)
        Note over G: 小助手先一口气猜 4 块 → 大模型一次验算 4 块<br/>→ 平均收下 2.4–3.2 块(实测接受率约 65%)
    end
    G-->>H: 选出一个字块(贪心取最高分 / 按种子做可复现的随机)
    H->>R: 记账:小抄补一页、各层记忆与查表指针前移
    H-->>S: 通过管道发出这一块(T)
    S-->>S: 组装成 SSE 流 / JSON 发回客户端

5.2 时序图(UML):4 路共享一个窗口(批槽模式)

sequenceDiagram
    autonumber
    participant S as 服务层
    participant E as 引擎(批窗口调度)
    participant G as GPU1
    participant A as 槽1 会话
    participant B as 槽2 会话
    participant C as 槽3 会话
    participant D as 槽4 会话

    loop 每个窗口(四路各推进 1 块)
        S-->>E: 4 个槽各带 1 个待处理字块(带着各自的进度)
        E->>G: 一个窗口一起算(4 行同过 48 层)
        Note over G: dense 权重、公共专家、输出头只读一遍,4 路分摊<br/>各槽的 KV 小抄 / 注意力记忆 / 查表历史完全独立
        G-->>E: 每槽选出 1 个字块
        E-->>A: BT 1
        E-->>B: BT 2
        E-->>C: BT 3
        E-->>D: BT 4
        E-->>S: (各槽的块分别回给各自客户端)
    end
    Note over E: 某一槽写完了 → BDONE 释放,门口排队的请求补位;<br/>新请求先"独占读一次题"再并入(不重读)

六、实测性能与可靠性

6.1 并发吞吐:

并发 每路 tok/s 合计 tok/s
1(solo,含 MTP) 76.8 – 88.1 —
2 52.0 – 55.8 104 – 110(+26~35%)
4 ~34.8 137.5 – 138.2(+56~80%)
带图请求 37.4 —

基准测试台复测(1/2/4/6/8/12 路 × 3 轮,每路生成 ~256 token):

并发 每路 tok/s 总吞吐 tok/s TTFT 中位/均值
1 76.4 74.4 0.63s / 0.68s
2 35.2 67.2 0.90s / 1.28s
4 20.4 76.9 2.68s / 2.23s
6 17.0 73.3 2.77s / 5.36s
8 15.5 83.8 9.83s / 7.38s
12 13.1 86.1 12.12s / 12.01s

并发矩阵

  • 2Hz 采样显示 GPU1 利用率在全部档位 80–100%——系统是 GPU 饱和的:总吞吐 67~86 tok/s 即当前条件的实际上限,加并发只买排队(>4 路每多一波 TTFT +~5s)。
  • 复测绝对值比验收时低约 40%(同机 GPU0 有邻居满载,MPS 共享);采样已排除 CPU 节流(0)、磁盘读(0)和外部请求混入,相对结论可信、绝对值按 ±20% 看待。
  • 冷启动首个请求 8.5 tok/s(首次触达页/专家缓存),渐热 22.9 → 稳态;重启后模型载入 ~6–13 分钟(本盘共享 I/O 限制)。

6.2 上下文长度:预填充线性增长,解码速度几乎不变

预填充(TTFT)随提示词长度线性(单路、冷内容、唯一前缀防缓存假象):

实测 prompt tokens 1 398 5 458 21 794 41 958 85 137 130 840 164 618
TTFT 2.19s* 2.45s 6.80s 12.94s 25.42s 38.27s 50.62s
预填充速度 678 2 357 3 397 3 446 3 552 3 628 3 461 tok/s

* 空闲后第一问含冷启动代价。线性拟合:TTFT ≈ 0.4s + prompt_tokens / 3 350,250k 以内无拐点。小提示词吃不满 GPU,≥32k 后稳定在 ~3.5k tok/s。

前缀复用优化:同一 64k 提示词第二次请求,TTFT 13.4s → 0.79s(17×)(服务端记 reused 42k tokens)——多轮会话天然享受这个收益。

graph LR
    A["同一段 64k 提示词"] --> B{"服务端还留着这段前缀吗?"}
    B -->|"第一次:没有"| C["整段重新预填充<br/>TTFT 13.4s"]
    B -->|"第二次:留着"| D["只补新增 token<br/>TTFT 0.79s —— 快 17 倍"]

解码速度几乎不受上下文长度影响(固定生成 512 token,逐 token 时间戳口径):

上下文 tokens 48 1 088 4 494 17 020 67 855 134 524 202 912 257 899
稳态解码 tok/s 95.6 106.1 104.2 103.1 96.8 96.6 89.5 87.8
TTFT 0.8s 2.0s 2.5s 6.3s 20.3s 40.4s 62.5s 81.6s
  • 长度涨 250 倍,解码只降 17%;MTP 草稿接受率全程 55~62% 不退化。长上下文"慢"的全部来源是预填充——258k 要等 82 秒才出第一个字。
  • 短上下文长生成的最优口径(2048 token ×3):稳态 106~113 tok/s、滑窗峰值 150+、草稿接受率 65%。
  • 读表口径:completion_tokens 会把 TTFT 摊进来(250k 时显示 5.9 tok/s,实际解码 87.8);正确算法见 6.9。

上下文
解码 vs 长度

内存代价在 RAM 不在显存:引擎 RSS 86.9 → 87.6 GiB(250k 时 +560 MiB),VRAM 余量全程 428 MiB 不变——KV int8 流式放内存成立。

上下文长度

6.3 多模态与工具调用的开销

图片 token 随分辨率缩放,≥1MP 封顶 ~1024:

合成图 256² 512² 1024² 2048² 4096² 2048×1152 JPEG
prompt tokens(含文本 32) 96 288 1 056 1 056 1 056 1 040
TTFT 1.30s 1.58s 2.33s 2.50s 3.28s 2.02s

图片(0.08–1.9 MP)每张 573~962 token;第 1 张 +1.4s,之后每张只 +0.1~0.2s(8 张共 +2.4s)——视觉编码器冷启动占大头,压缩大图不省 token 但会拖慢编码(4096² 比 1024² 慢 ~1s)。

工具 schema 直接进 prompt,每个工具 ≈ +100~170 tokens:

工具数 0 1 5 25 10(长描述)
prompt tokens 43 340 682 2 594 1 714
TTFT 0.57s 1.35s 1.55s 2.54s 1.86s

多轮工具 loop(4 轮):prompt 959 → 1 304 tokens,TTFT 0.66~2.29s(前缀缓存覆盖增长);并行工具调用(一次多个 call)实测正常。

图片数量

分辨率

6.4 思考档与长生成稳定性

reasoning_effort tok/s 生成 tokens 思考字符数
none 81 595 0
low 104 618 353
medium 109 638 296
high 76 342 393
  • low/medium 反而比 none 快(指令风格影响草稿接受率),high 最慢也最深。
  • 思考 token 计入 max_tokens:high 档 342 个 token 中大部分是思考,带思考时 max_tokens 建议 ≥1024。
  • 长生成(2048~4096 token)滑窗瞬时速度 80~155 tok/s、中位 ~115,无系统性衰减;前 ~200 token 是草稿爬坡段。

思考档
长生成

6.5 综合场景:多轮会话 与 并发长上下文

多轮图文+工具会话(6 轮):prompt 1 736 → 2 139 tokens(图片轮每张 +1024),每轮 TTFT 0.45~2.0s(图片轮最贵),前缀缓存让后续轮几乎免预填充。

并发 × 长上下文(最坏情况,单路 TTFT 作对照:32k=6.8s / 64k=12.9s):

场景 逐请求 TTFT 解码 总吞吐
2 × 32k 13.1s 1.4 tok/s 2.7 tok/s
4 × 32k 23.7s 0.9~21 tok/s 3.5 tok/s
4 × 64k 19.0 / 40.4 / 60.2 / 81.5s 0.4 tok/s 1.5 tok/s
6 × 64k 20.9 / 40.6 / 61.8 / 82.8 / 103.8 / 124.3s 0.3 tok/s 1.5 tok/s
  • 预填充在并发下串行化:4/6 路 64k 的 TTFT 恰好以 ~20.5s 等距排开(单路 12.9s,争抢下 ~20.5s/个)。
  • 预填充进行时解码被饿死(0.3~1.4 tok/s)——长上下文请求会拖住所有在途会话,agent 场景务必错峰提交。

多轮对话

6.6 资源水位

指标 实测 说明
GPU1 显存余量 恒定 428 MiB 上下文 / 图片 / 并发都不改变
GPU1 利用率 80~100% 全程饱和(唯一瓶颈)
引擎 RSS 86.9 → 87.6 GiB 随上下文缓慢增长
cgroup 内存 峰值 99.6 / 100 GiB 无余量(风险见第九节)
CPU 节流 ~0 --pool-workers 12 修复持续有效
引擎磁盘读 0 MiB PLE 全在页缓存,本批实验未触发磁盘瓶颈

6.7 正确性与健壮性

测试 结果
逐 token 一致性 4 会话 vs 单路 IDENTICAL ×4
超槽(5 路 vs 4 槽) 第 5 个等空槽,5 个回答全部对题、无串流
断连(流式中途断开) 紧随请求干净(无前请求内容)
崩溃注入(在途批请求时 kill -9 引擎) 全部请求干净快速失败(503+原因);挂起的慢客户端连接干净关闭、零串流;自动重启 418 s 恢复
鉴权/多模态/状态端点 401 正常;图像识别 5.7s 全对;concurrency: {serving: 4}
sequenceDiagram
    autonumber
    participant C1 as 客户端 B/C(批槽在途)
    participant C2 as 慢客户端 A(读一半暂停)
    participant S as 服务层
    participant E as 引擎 strata
    participant SUP as supervisord
    Note over E: 测试注入:kill -9
    S--xC1: 立即干净失败:503 + 原因(EngineDied)<br/>不串流、不挂死
    Note over S,C2: 恢复读取时:连接被干净关闭、零串流
    SUP->>E: autorestart 拉起新实例
    Note over E: 重新载入模型 ~418 s(约 7 分钟)
    C1->>S: 新请求(新 epoch)
    S->>E: GEN
    E-->>C1: 正常回答 ✓

6.8 已知限制

引擎/服务(部署期发现)

  • 批槽内不带 MTP 草稿(1 token/窗口,这就是 solo 更快的原因);惩罚类采样参数在批槽不生效;
  • 生产档 --pcie-frac 自适应:输出连贯,但理论上与单路轨迹不完全一致(要绝对一致需两个精确档开关,会慢);
  • 扩展字段 timings 并发下偶发缺失/错挂(choices/usage 核心字段正确);
  • 单个批窗口错误会终止引擎(错误后自动重启恢复);未配 engine_silence_s;
  • 本盘随机读 6.9ms(机械盘级)、共享 I/O——冷话题首问慢、批量任务(如归档上传)会抢盘。

可观测性(基准测试发现)

  • 批槽模式下 /metrics 每批只留一条请求记录(engine.last 共享),且并发时 decode_tok_s 字段会出现假值——并发场景的每请求指标必须以客户端口径为准;
  • 请求体不带 stream: true 时服务端整包返回(非流式),TTFT == 总时长,测速时容易误读;
  • 预填充速度与内容缓存命中强相关:冷内容 ~3.5k tok/s,命中前缀时快一个数量级(13.4s → 0.79s)——基准数字必须标注冷/热口径。

6.9 基准测试方法

测试目录为 ~/bench:异步流式客户端(逐 token 时间戳、工具调用累积、非流式回退兼容)+ 2Hz 系统采样器(nvidia-smi / 进程 RSS+IO / cgroup / 服务端 /metrics 对账)+ 10 个场景定义(并发 / 上下文 / 解码 / 图片×2 / 工具×2 / 思考 / 长生成 / 综合 / 并发长上下文)。


本文复刻自 iWiki 原文:消费级显卡部署大参数moe模型(qwen-3.8-flash-next)部署记录