19 KiB
Calculet NPU 总体研究报告
日期:2026-08-02
当前生产源码:fd9bd632f346085920d523dd307a3e4c11cc0a05
CalRT:0.7.6
镜像:calculet-llama:v0.4.8-release
当前模型:Qwen3-30B-A3B,双芯粒,40960 context
实施说明:本文用于建立全貌。需要直接拆研发任务时,下钻到《异步执行器与生产稳定性实施规格》《性能压测故障注入与验收规范》《新模型适配工程 Runbook》《逐算子优化实验手册》和《Batch、KV 与多模型调度实现规格》;这些文档包含类/方法、数据 schema、实验矩阵、故障注入和验收阈值。
1. 结论先行
当前产品不是 GGML 逐算子调度到 NPU 的通用 backend,而是一个“llama.cpp 服务层 + CalRT 预编译整图”的系统:llama.cpp 负责 tokenizer、请求/slot 调度和 CPU sampling;CalRT 加载预编译 calbin,按 prefill/decode 执行完整 Transformer 图。
当前系统的真实能力边界是:
| 维度 | 当前状态 | 结论 |
|---|---|---|
| 物理设备 | 1 个 PCIe VirtualDevice | 0.7.6 头文件明确写“temporary only support one physical device” |
| 板内计算 | 2 个芯粒,MCHIP | 当前模型已经双芯粒运行 |
| 并行方式 | 逐层张量并行 | 两芯粒都执行 48 层;不是 pipeline parallel,也不是 expert parallel |
| 模型 | 1 套完整 Qwen3-30B-A3B | 68 个文件是同一模型的分片、命令、ELF、元数据和 tokenizer,不是 68 个模型 |
| 硬件 batch | 1 | max_batch_size=1,无法形成硬件 continuous batching |
| 子模型 | prefill、decode | 两者不能在一次提交中混跑 |
| 上下文 | calbin 40960 | tokenizer 配置中的 131072 不是当前 NPU 可用上限 |
| 调用方式 | infer() 后立即 Wait() |
Runtime 非阻塞和 ping/pong 能力没有转化为并发 |
| 采样 | CPU | 每 token 回传 151936 个 BF16 logits,再转 FP32 采样 |
| 多卡 | 未支持 | 不能把“多芯粒”表述为“多板多卡” |
| 新模型编译 | 当前材料不能独立完成 | 缺 calcc、ONNX pass、kernel/codegen、oplib 和 calbin 打包工具 |
性能优化优先级不是先写更多单算子,而是:
- 补齐分层指标和正确基准口径。
- 修复 KV 无限忙等、错误恢复和 manifest 校验。
- 让 Runtime 的异步、ping/pong 和 buffer 复用真正进入服务路径。
- 由厂商重新编译 batch 4/8/16 decode calbin,建立 continuous batching。
- 优化长上下文 Attention/KV 带宽,以及 MoE grouped GEMM/dispatch。
- 最后再评估 expert parallel、多模型常驻和 NPU 侧 sampling。
2. 证据口径与状态定义
本文综合了 Runtime PDF、0.7.6 SDK 头文件、非 strip 动态库符号、生产源码、完整 Git 历史、OCI image history、构建缓存、完整模型产物、首轮实测和历史性能 CSV。
为避免把“接口存在”误写为“产品可用”,全文采用以下状态:
| 状态 | 含义 |
|---|---|
| C0 当前已确认 | 当前生产代码、产物或实测直接证明 |
| C1 SDK 存在但未接入 | 0.7.6 头文件和动态符号存在,当前 llama 路径没有使用或未验证 |
| C2 可工程实现 | 基于现有 Runtime/服务能力可以开发,但当前尚不存在 |
| C3 依赖厂商工具链 | 需要重新编译模型、开发 kernel、修改 Runtime 或获得私有工具 |
| C4 当前不支持/不应开放 | 版本或架构明确不支持,或接口只适合受控运维面 |
3. 构建与供应链边界
OCI history 完整记录了镜像构建过程:
- 基础镜像是 Ubuntu 22.04 amd64。
- 安装 GCC/G++ 11、CMake、Git、ELF、调试工具和 Miniconda。
- 从
/home/user1/CalCulet/calrt/hostrt以 Release/shared 方式编译、安装 Runtime。 - 安装完成后执行
rm -rf calrt/,因此最终镜像不保留 HostRT 源码。 - 构建 SystemC、fmt、yaml-cpp 等依赖。
- 使用以下核心配置编译 llama.cpp:
cmake -B build \
-DLLAMA_BUILD_COMMON=ON \
-DLLAMA_USE_CALRT=ON \
-DLLAMA_CURL=OFF \
-DCALRT_INSTALL_DIR=/usr/local/
cmake --build build -j
构建缓存进一步确认:
CMAKE_BUILD_TYPE=Release,-O3 -DNDEBUG。LLAMA_USE_CALRT=ON。GGML_CALRT=OFF。- 最终链接
/usr/local/lib/libcalrt-linux-x86_64.so.0.7.6和libelf。 - 当前 Git HEAD 与
origin/dev都是fd9bd632...。
由此得到三条边界:
- 我们能修改 llama.cpp 服务/适配层并重新构建。
- 我们能调用和封装 CalRT 0.7.6 的公开头文件/动态符号。
- 我们不能仅凭现有材料重建 calcc 图优化、NPU kernel/codegen 或生成一个新 calbin。
模型 YAML 记录的编译器版本是 calcc 575c2d708e306a60f10bc34f5087477073b58caa、TVM 7aebd1c21fa67d942a1fd93e39e8a67007f91c0c;原 ONNX 只留下路径和 SHA-256,文件本身未归档。
4. 当前端到端执行链
sequenceDiagram
participant Client as OpenAI API Client
participant Server as llama-server
participant Llama as llama.cpp Scheduler
participant CalRT as CalRT 0.7.6
participant NPU as 2-chiplet NPU
Client->>Server: chat/completions
Server->>Llama: tokenize + select slot
Llama->>Llama: split request by sequence
Llama->>CalRT: select prefill/decode model
Llama->>CalRT: set current/past sequence CSR
Llama->>CalRT: MapBuf + SliceTensor
CalRT->>NPU: H2D + submit whole graph
NPU-->>CalRT: logits + updated KV
CalRT-->>Llama: D2H + Wait completion
Llama->>Llama: BF16 to FP32 + CPU sampling
Llama-->>Server: next token
Server-->>Client: stream/non-stream response
生产源码中的具体行为:
VirtualDevice::CreateVDevice(PCIE)创建设备。- 从固定寄存器读取 CalcoreRT commit,并读取
calrt_version()。 Calbin::CreateCalbin(path)解析模型并configure()。- 根据第一个 sequence 的
n_tokens > 1选 prefill,否则选 decode。 - 子模型通过名称子串查找,而不是严格 manifest。
- 每次
calrt_infer()都创建 InputBuf/OutputBuf 对象,但映射到长期 vector backing store。 - 写入
past_kv_cur_seq_len[0]和cur_seq_len[0]。 - prefill 对 token/position 和末行 logits 做 slice。
- 对 sequence 数组逐个执行
infer()、立即Wait()。 - 将 BF16 logits 转成 FP32,并交还 llama.cpp CPU sampling。
这里最关键的判断是:infer() 本身是非阻塞提交,但当前代码紧接 Wait(),所以应用层表现仍是同步串行。
5. Qwen3-30B-A3B 结构与计算图
| 参数 | 值 |
|---|---|
| 层数 | 48 |
| hidden size | 2048 |
| Q heads | 32 |
| KV heads | 4 |
| head dim | 128 |
| experts | 128 |
| 每 token 激活 experts | 8 |
| expert intermediate | 768 |
| dense intermediate 配置值 | 6144 |
| vocab | 151936 |
| RoPE theta | 1,000,000 |
| calbin context | 40960 |
| batch | 1 |
| 量化 | W8A8/W4AF16 混合 |
| Flash Attention | 已开启 |
产物中的算子计数:
| 算子 | Prefill | Decode | 说明 |
|---|---|---|---|
| fused RMSNorm | 193 | 193 | 每层 attention/FFN 多处及尾部 |
| quantized fused matmul/add | 193 | 193 | QKV、O、router、lm_head 等 |
| sync CPU/CCU D2D | 146 | 146 | 芯粒/执行单元同步与数据交接 |
| gather tensor | 145 | 145 | 分片结果合并 |
| grouped MoE dense | 144 | 144 | 每层 gate/up/down 三组 |
| KV scatter | 96 | 96 | 每层 K/V |
| fused RoPE | 96 | 96 | 每层 Q/K |
| add/relu/residual | 96 | 96 | attention/FFN residual |
| dynamic D2D | 96 | 0 | prefill 动态跨芯粒移动 |
| SiLU/multiply/reduce | 各 48 | 各 48 | 每层 MoE |
| gate pre/post | 各 48 | 各 48 | top-k、dispatch 与恢复 |
| Flash Attention | 48 | 48 | 每层 attention |
| embedding | 1 | 1 | 只在 chip0 profile 中执行 |
| layout conversion | 2 | 0 | prefill mat/hw pattern 转换 |
| 总节点 | 1496 | 1398 | YAML 顶层 op 数 |
Decode MoE 权重 shape 的第一维是 128,且 chip0/chip1 的 profile 都包含相同的 144 个 nbu_moe_dense_group。这说明两边都有 128 experts 的权重分片;当前是矩阵维度张量并行,不是把 experts 分组放到不同芯粒。
6. 双芯粒分配与通信
四个 .profparts 文件显示:
- decode:chip0 1254 行,chip1 1253 行。
- prefill:chip0 1352 行,chip1 1351 行。
- 唯一稳定的一行差异是 embedding 只在 chip0。
- 两芯粒都执行全部 48 层的 attention、MoE、norm、residual 和 output path。
- Q、K/V、投影矩阵、MoE 权重与 vocab 输出按编译器布局分片,然后 gather/reduce。
- 每颗芯粒承担 2 个 KV heads,合计 4 个。
这属于 tensor parallel:每层都有跨芯粒同步,优势是每层计算并行,代价是每层通信。它不具备 pipeline parallel 的阶段隔离,也没有 expert parallel 的按专家路由。
最大 prefill 静态命令描述:
| 芯粒 | data_moving 条目 |
描述字节 | 主要块大小 |
|---|---|---|---|
| chip0 | 444,610 | 18.979 GiB | 245760 x 64 KiB;122880 x 32 KiB;75968 x 1 KiB |
| chip1 | 444,608 | 18.822 GiB | 245760 x 64 KiB;122880 x 32 KiB;75968 x 1 KiB |
注意:prefill 配置为 dynamic_d2d=true,且 libcpu_x86.so 导出 Process。因此这些数字是 40960 上限图的静态命令描述,不是短 prompt 的实测传输量。必须通过 Runtime trace 或 PCIe/D2D counter 测量实际长度相关流量。
7. 内存基线
| 项目 | 总量 | 每芯粒/分片 | 口径 |
|---|---|---|---|
| 参数文件 | 16.938 GiB | chip0 8.759 GiB;chip1 8.179 GiB 文件大小 | 两个 param_blk*.bin |
| 参数映射到 DRAM | 16.910 GiB | 8.745 / 8.165 GiB | memory reservation 逐段求和 |
| 参数映射到 SRAM 窗口 | 28.762 MiB | 各 14.381 MiB | memory reservation 逐段求和 |
| BF16 KV | 3.750 GiB | 1.875 GiB | 48 x 4 heads x K/V x 128 x 40960 x 2 bytes |
| DRAM reservation | 推断 28.150 GiB/双芯粒 | 14.075 GiB | 同一虚拟地址计划按芯片 mask 应用 |
| SRAM reservation | 推断 33.688 MiB/双芯粒 | 16.844 MiB | 接近每芯粒 18 MiB 物理 SRAM |
| decode tensor 地址并集 | 10.634 GiB | 每芯粒地址计划 | 含常量、KV、I/O 和 workspace;非峰值活跃内存 |
| prefill tensor 地址并集 | 14.091 GiB | 每芯粒地址计划 | 与 DRAM+SRAM reservation 基本闭合 |
| 单 token logits | 303,872 B BF16 | Host D2H | 151936 x 2 bytes |
参数、KV、workspace 的区间存在别名和生命周期复用,不能把各角色的地址覆盖量直接相加。完整映射见《模型产物与内存映射》。
device_memory_required: 16777216 MB 明显存在单位/字段问题;按字面是 16,777,216 MiB,不可能成立。它更像 16 GiB 的 KiB 数值被错误标成 MB,需要厂商确认解析口径。
8. Runtime 已有能力与当前使用差距
C0 当前已使用
- PCIe VirtualDevice 创建与状态检查。
- calbin 解析、configure、按名称获取子模型。
- Input/Output buffer、tensor MapBuf、slice。
- CSR 动态序列长度。
infer()、Wait()、H2D/D2H/infer 计时。- KV Apply/Free/RemoveTokensAtEnd/Clear 的部分路径。
- 双芯粒 MCHIP 整图执行。
C1 SDK 有但生产未利用
EnableParallelMode(bool)。SubmitJob()和infer_with_fixed_task_type()的 ping/pong 控制。- OutputBuf pending/running/done/CCU exception 状态。
- pending/finished/left job counters。
- trace 和配置 metadata dump。
- golden input/output 读取。
- KV
canAllocate、显式 Allocate、DoShift、batch layout、S8 V-cache。 - per-chip memory/register 访问。
- C API 的 block/non-block inference 封装。
C4 当前限制
- VirtualDevice 内部只有一个
shared_ptr<CalrtDevice>,不是多物理设备管理器。 - 当前 calbin batch=1、只有 prefill/decode。
RelocateTensorAddress()、AllocDevMem()只出现在 PDF,0.7.6 头文件与动态符号都不存在。- 任意寄存器、设备地址、reset、ClearAllDevMem 不应暴露到普通推理 API。
9. 当前集成的工程问题
P0 正确性与稳定性
- KV 申请使用无限
do/while(!Apply()),没有超时、退避、取消和资源耗尽错误。 - KV 更新失败直接
abort();初始化失败使用GGML_ABORT。 - KV adapter 的
seq_cp、seq_keep、seq_add、seq_div、state save/load 基本为空。 seq_rm只支持尾部删除,common-prefix 与任意区间删除不成立。- 子模型通过名称包含匹配,重复名称时最后一个命中覆盖前一个。
- CSR setter、
infer()和OutputBuf::Wait()都返回错误码,但当前直接调用没有检查,提交、执行或 D2H 失败可能继续使用无效状态/输出。 - calbin prefill 的
smodel_type错标为llm_decode。 - tokenizer 的
model_max_length=131072与模型/calbin 40960 冲突。 server.cpp只在成员定义处保护cal_ctx,部分指标读取在#ifdef外,非 CalRT 构建存在编译风险。
P0 吞吐
max_batch_size=1。- sequence 逐个循环,
infer()后立即Wait()。 - 所有 slot 共享一套 prefill/decode backing vector,不能直接多 in-flight。
- prefill/decode 不能混合提交。
P1 延迟与 CPU 开销
- 每次调用重建 InputBuf/OutputBuf,并重复查 tensor/CSR。
- Decode 全 vocab BF16 logits 回传后,先构造完整 FP32 临时 vector 再复制。
- output untile 硬编码
D0=16和 shape 分支。 - MapBuf 使用普通 vector,尚未证明是 pinned/registered host memory。
- 固定寄存器读取版本不具备芯片/固件兼容抽象。
10. 性能证据与正确口径
首轮当前版本实测:
- 27 token prefill:184.27 token/s。
- 246 token decode:58.25 token/s。
- 请求端到端:4.41 s。
历史 CSV 有 163 条数据,但有两个口径问题:
model=deepseek是client_new.py第 183 行的硬编码字符串,不能证明真实模型。- CSV 的
TTFT实际是predicted_ms + prompt_ms,是完整生成时长,不是真正首 token 延迟。
仍然可信的趋势是:
| 上下文示例 | Decode TPS | 观察 |
|---|---|---|
| 短上下文,2026-06/08 | 58-64 | 当前短请求基线 |
| 约 2K | 38-46 | KV 长度增加后明显下降 |
| 约 4K | 28-40 | 4K 附近继续退化 |
| 约 8K-16K | 20-30 为主 | 不同版本混合,不能直接横比 |
| 约 33K | 12 左右 | 长上下文已明显带宽受限 |
| 约 40K | 10-16 | 接近上限时最慢 |
这与 GQA decode 的理论特征一致:每个新 token 需要读取不断增长的 KV,Attention/KV 带宽是长上下文的第一瓶颈。MoE 权重读取则是所有 decode 长度都存在的主成本。
后续必须将指标定义为:
- TTFT:请求进入到第一个可见 token。
- TPOT:首 token 后每个输出 token 的间隔。
- ITL:逐 token inter-token latency 分布。
- E2E:请求进入到结束。
- Goodput:满足 SLO 的完成 token/s,而不是只看设备 TPS。
11. 优化路线
阶段 0:建立可信基线
- 修复 TTFT 统计和 model 标签。
- 暴露 infer/H2D/D2H/framework、KV 等待、队列等待和 sampling 时间。
- 每次运行记录 llama commit、CalRT/驱动/固件、calcc/TVM、calbin hash。
- 固定 prompt/output/tokenizer/采样参数,保留原始逐 token 时间。
阶段 1:稳定性和低风险收益
- KV 申请加入超时、退避、取消和 429/503。
- 严格 model manifest,启动时检查 I/O、dtype、layout、batch、context、chip topology。
- 建立 InputBuf/OutputBuf pool 和 handle cache。
- 复用 BF16->FP32 缓冲,或直接转换到目标 logits 区。
- interrupt/polling、普通/pinned host memory、单/双 buffer 做 A/B。
阶段 2:异步流水
- 每个 in-flight 请求独立 backing storage。
- 使用 OutputBuf 状态和 Wait,将 submit 与 completion 分离。
- 使用 ping/pong job,并测试
EnableParallelMode。 - 将 CPU sampling 与下一请求/下一阶段 NPU 工作重叠。
- prefill、decode 建立独立队列和优先级。
阶段 3:真正批处理
- 由厂商生成 batch 4/8/16 decode calbin。
- 完成 batch KV layout、slot-to-hardware-batch 映射和压实。
- continuous batching 每 step 填充活跃 sequence。
- 验证 fairness、取消、EOS、资源回收和长短请求隔离。
阶段 4:算子与图优化
- 长上下文 GQA Flash Attention、KV layout、S8 V-cache。
- router + top-k + dispatch 融合。
- 8 active experts 的 decode 专用 grouped GEMM。
- RMSNorm+QKV、Q/K norm+RoPE、O-proj+residual、FFN+residual 融合。
- NPU top-k 或 sampling,减少 303,872 B/token 的 logits 往返和 CPU 串行。
阶段 5:拓扑演进
- 单芯粒小模型/embedding,另一芯粒大模型或 reranker。
- 双芯粒 tensor parallel 大模型。
- expert parallel 的实验性新 calbin。
- 多模型常驻、动态装卸和显式内存预算。
- 多物理板卡需要 Runtime 新版本或进程级设备隔离,0.7.6 不能直接实现。
12. 下一轮实验矩阵
12.1 基础长度矩阵
- prompt:1、16、128、512、2048、4096、8192、16384、32768、40900。
- output:1、32、128、512。
- 重复:预热 3 次,正式至少 20 次;报告 P50/P95/P99。
- 指标:TTFT、TPOT、E2E、infer、H2D、D2H、framework、sampling、KV wait、CPU、DRAM。
12.2 当前 batch=1 下的并发行为
- 客户端并发:1、2、4、8、16。
- 分别测试全短、全长、10% 长+90% 短。
- 只声明队列吞吐和排队延迟,不宣称硬件 batch。
- 验证取消后 KV 是否释放,资源耗尽是否有界返回。
12.3 Runtime A/B
- interrupt vs
poll_mode=1。 - 默认 submit vs
EnableParallelMode(true)。 - 自动 task vs 固定 ping/pong。
- 普通 vector vs pinned/registered buffer。
- 当前对象创建 vs buffer pool。
- 全 logits vs NPU top-k 原型。
12.4 稳定性
- 30 分钟、2 小时、24 小时。
- context 逼近 40960。
- 客户端断开、请求取消、模型服务重启。
- 非法 calbin、超长 prompt、KV 耗尽、CCU exception。
- 设备 reset 后恢复只在独占维护窗口执行。
13. 决策建议
- 近期主线应是“稳定性 + 可观测 + 异步流水”,不要先投入多板抽象。
- 吞吐目标若高于单请求 58-64 token/s,batch decode calbin 是必要条件。
- 长上下文性能目标必须单独立项,不能用短上下文 TPS 代表。
- 新模型适配应先让厂商交付可复现的 compiler container 和一套 dense 模型示例,再做 MoE。
origin/runtime_replace的 cal-llm EngineCore 方向值得吸收,但它是厂商演进分支,不是当前生产能力;应先拿到匹配的c_api.h、libcal_llm.so、兼容矩阵和测试结果。- 对外接口应分为推理面、只读观测面和受控运维面;寄存器/任意内存/reset 不进入普通服务 API。
14. 研究后的可执行起点
后续项目研究建议从三个并行工作包进入:
- 工作包 A:在当前 commit 上修复 KV busy wait、非 CalRT 条件编译、buffer pool 和分层 metrics。
- 工作包 B:建立标准性能 harness,重测当前 calbin 的长度/并发/Runtime A/B 矩阵。
- 工作包 C:向厂商索取 compiler toolchain、batch calbin、cal-llm SDK 和接口问题答复,启动一个小型 dense 新模型适配试点。
未经新实验或厂商确认,不应将 C1/C2/C3 能力写入产品规格。