Tutti 接入 vLLM 设计文档
架构(D-005):外部 TuttiConnectorV1,零 vLLM 侵入。 决策:
ai-orch/decisions/(D-001 红线、D-002 存储契约、D-005 架构)。 操作手册:tutti-vllm-manual.md。编排:ai-orch/+rounds/。
1. tutti 旧版(legacy)接入 vLLM 复盘(既有资产)
1.1 总体架构:三层解耦
层 |
位置 |
职责 |
|---|---|---|
vLLM fork(薄改) |
|
KVConnector 挂点补丁约 8 文件 |
Python adapter(厚) |
|
全部 vLLM 侧调度:请求追踪、前缀命中、slot 映射、逐层 IO 编排 |
C++/CUDA 引擎 |
|
NVMe 队列、GPU P2P DMA、GPU-direct IO kernel |
核心:调度逻辑不改 vLLM,全部实现在 KVConnectorBase_V1 回调里。
1.2 vLLM fork 侧修改清单(历史参考,新版全部原生具备)
legacy-tutti_connector.py:64KB 对齐(_align_num_blocks_for_legacy-tutti)、prefer_cross_layer_blocks、requires_piecewise_for_cudagraph。factory.py注册;kv_transfer_utils.py的maybe_transfer_kv_layer装饰器;跨层 KV buffer(allocate_uniform_kv_caches)。启动:
kv_transfer_config={"kv_connector": "LegacyTuttiConnector", "kv_role": "kv_both"},block_size=128=chunk_size,环境变量配置。
1.3 存储文件组织
一个 KV chunk(chunk_size token × 全部 layer × K+V)= 一个 GPUFile; 每 GPUFile 最多 4 盘各建一个物理文件条带化,per 盘大小对齐 64KB。
文件描述符 GPUFileDesc{file_id, 单层 block_size, tensor_shape[3], CompactNVMeMapping(17B)},GPU-resident。
1.4 索引设计
逻辑索引(CPU/Python):token chunk 哈希 → CacheEngineKey → GPUFileId 分配 + LRU evictor;
跨进程查询:scheduler 经 ZMQ/Redis lookup server 查 worker 的索引 (
get_num_new_matched_tokens用);物理索引(GPU/C++):CompactNVMeMapping 内联,kernel 直接算盘上 offset,不回 CPU。
1.5 读写接口与调度时机
Scheduler 进程(每调度步):
get_num_new_matched_tokens() # lookup 查外部命中 → LoadSpec
update_state_after_alloc() # 分配 block 后 can_load=True
build_connector_meta() # ReqMeta{token_ids, slot_mapping, block_ids, save/load_spec}
# slot_mapping = block_id*block_size + offset
ReqMeta.from_request_tracker() 负责 chunk 边界对齐
(discard_partial_chunks)、skip_leading_tokens 去重、slot_mapping 展开。
Worker 进程(每次 forward):
start_load_kv() # prepare_for_load/save + 预取第 0 层 + IO 预算
每层 attention 前: wait_for_layer_load(layer) # retrieve_layer(L+1) 逐层预取
每层 attention 后: save_kv_layer(layer) # store_layer(L) 逐层落盘
forward 出口: wait_for_save() # no-op(stream 异步)
overlap:StreamController(green context SM 分区)切 IO stream (load 16 SM / save 8 SM),与计算 stream 并行;逐层 prefetch 重叠 第 N+1 层读取与第 N 层计算。
IO 任务:
IOSpec{gpu_file, block_id, layer}逐层递增;请求释放时 withdraw_tasks 回收。
1.6 Debug 与 nsys 标记
Python NVTX:
_tutti_legacy_nvtx_annotate装饰器(domain=”legacy-tutti”, 函数名 hash 分色)铺满 adapter/engine;区间nvtx.start_range。C++ 侧无 NVTX(靠 kernel 名区分);独立 logger + observability 统计。
2. Tutti 与 tutti 的接入面对应关系
概念 |
tutti 前身 |
Tutti |
|---|---|---|
引擎入口 |
legacy-tutti_engine(get/put chunk) |
|
GPU 显存注册 |
register_kv_caches → DMA 注册 |
|
一次性下发 |
多次 per-layer engine 调用 |
|
文件/条带 |
GPUFile + 4 盘自动条带 |
URI: |
partial commit |
无显式契约 |
|
环境依赖 |
内核模块随引擎 |
内核模块 → tutti_daemon → mount 三步;daemon 仅资源 broker |
关键 gap:tutti 无 Python 绑定 → T-101 补(唯一新 C++ 面)。
3. 新版 vLLM 现状(接入可行性依据)
maybe_transfer_kv_layer原生存在(kv_transfer_utils.py:15-61), 已挂 unified attention——逐层 hook 零改动可用。跨层 KV buffer 机制原生存在(
allocate_uniform_kv_caches等)。外部包零侵入注册:
kv_connector_module_path直接 import 外部 connector 类(factory.py:105-114,LMCache 同款)——本方案核心依据。官方参考实现齐全(LMCache/Nixl/OffloadingConnector 等)。
HMA:connector 不支持时需
--disable-hybrid-kv-cache-manager。无 legacy-tutti/legacy-tutti/tutti 残留,代码干净。
4. TuttiConnectorV1 设计(当前权威)
4.1 组件与仓库位置(全在 tutti 仓库)
integration/vllm-connector/bindings/python/
# tutti_runtime 包:pybind 封装
# make_{stub,local_nvme,striped_nvme}_runtime
# open_batch / register_memory / submit / wait
integration/vllm-connector/engine/
core.py # TuttiEngine 唯一引擎(计划态 + 逐层执行态 + 环形窗口)
chunk_index.py # chunk hash → 文件路径 key + LRU + pin(纯 stdlib)
backend.py # StorageBackend SPI(chunk_paths/bind_staging/段搬运)
memory_backend.py # MemoryBackend(无盘可测)
integration/vllm-connector/tests/
# 按测试需求分类(唯一测试落点,见 tests/README.md):
# contract(读写接口契约) / unit(纯逻辑) / adapter /
# kernels / bindings / perf / scale / overlap
integration/vllm-connector/adapter/
vllm_adapter.py # TuttiConnectorV1(KVConnectorBase_V1) 双角色壳
# + scheduler 侧:RequestTracker/ReqMeta/slot_mapping
# (平移 legacy tutti)+ engine 计划态进程内前缀命中
worker.py # register_kv_caches → engine.bind;逐层 load/store 编排
nvtx_utils.py
integration/vllm-connector/kernels/
# tutti_kv_kernels:gather/scatter(LMCache 平移,T-117)
# LocalStoreBackend(T-115):engine/local_store_backend.py
4.2 配置
kv_transfer_config = {
"kv_connector": "TuttiConnectorV1",
"kv_connector_module_path": "adapter.connector",
"kv_role": "kv_both",
"extra_config": {"capacity_bytes": ..., # 必需;硬件无关
"blocks_per_chunk": 0}} # 0=自动(segment≥256KiB 对齐)
4.3 存储布局与对齐契约(D-002 摘要,以 ARCHITECTURE.md §0a 为准)
chunk 身份 = 文件路径 key(opaque,backend 自产自解释);文件 布局 = packed(层段 offset = layer_idx × segment_bytes)。
池化/聚合/fd 收敛/建池校验:LocalStoreBackend 私有自决,不进接口 (若做预分配:零实写,禁 fallocate——FIEMAP fail-closed)。
注册:staging 环形窗口是唯一注册对象(基址 64KiB 对齐, io_granularity=segment_bytes 预建描述符);IO 的 memory_offset/length/target_offset 4KiB 对齐;单请求 ≤ caps().max_single_io_bytes(默认 32MiB)。
submit 前
io_stream.wait_stream(gather stream);被拒请求 内部窗口重发,对上层恒 True;每层 CUDA event。
4.3a staging gather/scatter 内核(D-006:复用 LMCache 资产)
legacy 引擎即 LMCache fork 改名版,其 staging kernel 被砍只剩 DMA; 当前设计恰好接回这条路径,内核资产现成:
LMCache 资产 |
位置 |
复用方式 |
|---|---|---|
|
|
T-117 已平移:单层 packed ↔ paged(slot_mapping),16 种 EngineKVFormat 全覆盖 |
|
同文件 :904(+ |
跨层 |
|
:1160/:1209 |
“load and reshape” 本体,语义参照 |
|
|
paged 布局描述(16 种,编译期谓词) |
|
|
register_kv_caches 时运行时布局探测 |
|
|
槽管理参照(ref_count/pin/unpin) |
平移方式:拷 mem_kernels.cu 相关部分 + engine_kv_format.h 至
kernels/(独立包 tutti_kv_kernels)自建最小 torch extension——
不依赖 lmcache pip 包(Apache-2.0,保留版权头)。
staging 槽布局与 LMCache KV_2LTD([2,L,T,H])同构,kernel 参数化
适配。engine 的 gather_fn/scatter_fn 钩子即注入点(T-117 已交付
single_layer 平移内核,集成接线在后续卡完成)。
4.4 调用时序
Scheduler:
get_num_new_matched_tokens → engine.lookup_prefix(进程内,无 RPC)
update_state_after_alloc → RequestTracker 记录 + can_load
build_connector_meta → ReqMeta(slot_mapping 手算,平移 legacy tutti)
+ engine.plan_load/plan_store(chunk_id 路径 opaque 透传)
Worker:
register_kv_caches # engine.bind(staging 是唯一注册对象,注册在 backend 内部)
start_load_kv # engine.load_layer(plan, 0)
wait_for_layer_load(L) # 等 L 层句柄;engine.load_layer(plan, L+1);末层 complete_load
save_kv_layer(L) # engine.store_layer(plan, L)(与计算并行)
wait_for_save # no-op
get_finished # complete_store/complete_load 收割
Overlap 语义(与 legacy-tutti 对齐):load 层间预取 + save 逐层落盘, IO stream 与计算 stream 并行——这是选择外部 connector 路线的核心收益。
4.5 与官方 Offloading 体系的关系
刻意不依赖 vllm 的 OffloadingSpec/Manager/Worker 抽象:完整实现 KVConnectorBase_V1 回调,索引/驱逐自管(engine/chunk_index.py)。理由: 逐层 hook 粒度 + 不受 experimental API 演进影响(D-005 第 2 条)。
5. 架构契合度分析(tutti 现状 vs vLLM 需求)
5.1 模型多样性:MoE / Mamba / 混合架构 — 满足
tutti 是 opaque-byte 引擎(无 reshape/paged 逻辑,IO kernel 只做 PRP+doorbell),结构知识全在 vLLM 侧。
MoE 不改变 KV 形状;Mamba/GDN state 由 connector 的 kv_caches dict 收集(按 layer 独立 tensor 注册即可,
MambaSpec页同样走 4KiB 对齐约束,非 4KiB 倍数由 vllm 的 page_size_padded 处理或跳过)。唯一约束:各 group page_size_bytes(padded) % 4096 == 0。
5.2 GPU-NVMe 拓扑与带宽
实测拓扑:2 NUMA × 4 PCIe switch,每 switch 1 GPU(x16)+1 NVMe(x4)。 即每 GPU 直连 1 盘(distance 0),NUMA 内其余 3 盘 distance 1, 跨 NUMA distance 2。
推荐:每 NUMA 4 盘组 striped 池,同 NUMA 4 GPU 各起独立 TuttiRuntime open 同一批池文件——全部 IO NUMA 内闭合,零跨 NUMA P2P。chunk 路径空间按 rank 分区(无锁),或共享 canonical 去重(二期)。
带宽模型(分级):单盘直连 ~7GB/s;单 GPU NUMA 4 盘突发 ~28GB/s; 全机 8 盘聚合峰值 50+GB/s(保守一半 20+GB/s);稳态 8 GPU 均分 ~7GB/s/GPU (GPU x16 上行 ~32GB/s 封顶);瓶颈在盘数(8×Gen4x4 ≈ 56-60GB/s)。
NUMA0 四盘尚未 daemon bring-up(用前补三步);跨 GPU 不互访 KV (TP 分片语义 + tutti stream 归属校验),共享池无正确性风险。
5.3 条带化与持久化
striped://文件级静态条带(unit + rot),物理盘映射((offset/unit)+rot) % N隐式完成,vLLM 侧只见 opaque chunk 路径。数据持久(文件应用层创建,重启可读);索引不持久(R1 接受 重启冷缓存;V2 加 sidecar 索引 dump/load)。
5.4 内存注册:staging 是唯一注册对象
契约(实证):注册基址 64KiB 硬性(
local_nvme_data_path.cpp:985); IO memory_offset/length 4KiB(submit_one.cuh:38-43)。落地:staging 环形窗口一次注册(
io_granularity=segment_bytes预建描述符,全部 IO 走 pre-built 路径);vllm paged 池不注册—— 数据进出只经 gather/scatter kernel(HBM 域 μs 级),IO 的 GPU 端 恒为 staging 内偏移。小 block(16/32)下的粒度碎片、对齐非法、 预建描述符失效问题由此全部消解(详见 ARCHITECTURE.md §2b)。
5.5 容量与 fd 收敛(LocalStoreBackend 私有自决)
一 chunk 一文件在真实容量下是十万级 fd——fd 收敛(聚合大文件) 是真实需求,但属 backend 内部实现自决(ARCHITECTURE.md §0a): 不进接口、不进路径语义、不进 engine/adapter。
硬约束(若做预分配):禁 fallocate(FIEMAP fail-closed 拒 UNWRITTEN,须实写零);单文件物理 extent ≤ 124 (kMaxTotalExtents),实写大文件通常几个~几十个,可控。
5.6 md/dm RAID0 直通(未立项,结论存档)
md-raid0 上 FIEMAP 只给 md 逻辑偏移(ext4 的 m_pblk 是所在块设备
块号;raid0 的 bio remap 在 ext4 之下的块层,OpenCloudOS 5.4.241
raid0.c map_sector 证实)。可行路径 = snvme 块设备(保留 gendisk)
组 md0 + tutti 新增 md_raid0 resolver 复刻映射(同构盘退化为线性
公式,参数从 /sys 运行时读)。收益:单一 ext4 大文件随便放 +
模型权重挂载零修改聚合加载;代价:一个新 resolver + 部署纪律。
D-001 红线:本轮不做,如立项另出决策。
6. 调用路径图(外部 connector 版)
┌────────────────────────── Scheduler 进程 ──────────────────────────┐
│ vllm/v1/core/sched/scheduler.py(官方) │
│ get_num_new_matched_tokens / update_state_after_alloc / │
│ build_connector_meta │
│ │ 官方 KVConnectorBase_V1 扩展点 │
│ v │
│ ★ adapter.connector.TuttiConnectorV1(SCHEDULER) │
│ → vllm_adapter.py(scheduler 侧) │
│ engine 计划态:lookup_prefix / plan_load / plan_store │
│ (进程内,无 RPC;chunk_id 路径 opaque 透传) │
│ ReqMeta{token_ids, slot_mapping, block_ids, load/save_spec} │
└──────────────────┬──────────────────────────────────────────────────┘
│ KVConnectorMetadata(官方 IPC)
┌──────────────────v─────────── Worker 进程(每 GPU)─────────────────┐
│ vllm gpu_model_runner(官方)+ maybe_transfer_kv_layer 装饰器 │
│ forward 前 start_load_kv;每层前 wait_for_layer_load; │
│ 每层后 save_kv_layer;出口 wait_for_save │
│ │ │
│ v │
│ ★ adapter.connector.TuttiConnectorV1(WORKER) │
│ → worker.py WorkerImpl(纯编排翻译 + 在途上限背压) │
│ → TuttiEngine 执行态:环形窗口 staging + gather/scatter │
│ (_kernels,LMCache 平移) │
│ → StorageBackend(LocalStoreBackend:路径自产自解释、 │
│ 固定粒度 submit、partial-commit 重发;池化/fd 收敛私有) │
│ │ pybind │
│ v │
│ ★ tutti_runtime._core → tutti/presets make_*_runtime(只读链接) │
│ StorageRuntime.submit(reqs, DEVICE_EXECUTION, io_stream) │
│ → 按 DataPath 分组 → 1×H2D 描述符 → 1 次 fused kernel launch │
│ IO kernel @ io_stream ∥ 计算 stream ← overlap │
└──────────────────┬──────────────────────────────────────────────────┘
│ PCIe P2P GPU-direct DMA(不经 host)
v
NUMA 内 4 盘 striped 池(文件布局=packed;聚合/fd 收敛 backend 自决)
★ = 新增代码(全在 tutti 仓库);vllm fork 零改动。
7. 环境与操作
见 tutti-vllm-manual.md(tutti-env 完全隔离环境 / env-tutti.sh /
snvme bring-up 三步 / 冒烟与 bench 流程 / FAQ)。