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(薄改)

third_pkgs/tutti-legacy/vllm-fork

KVConnector 挂点补丁约 8 文件

Python adapter(厚)

third_pkgs/tutti-legacy/engine/pkg/integration/vllm-connector/vllm_v1_adapter.py(约 1300 行)

全部 vLLM 侧调度:请求追踪、前缀命中、slot 映射、逐层 IO 编排

C++/CUDA 引擎

legacy-tutti/csrc/tutti 前身

NVMe 队列、GPU P2P DMA、GPU-direct IO kernel

核心:调度逻辑不改 vLLM,全部实现在 KVConnectorBase_V1 回调里

1.2 vLLM fork 侧修改清单(历史参考,新版全部原生具备)

  1. legacy-tutti_connector.py:64KB 对齐(_align_num_blocks_for_legacy-tutti)、 prefer_cross_layer_blocksrequires_piecewise_for_cudagraph

  2. factory.py 注册;kv_transfer_utils.pymaybe_transfer_kv_layer 装饰器;跨层 KV buffer(allocate_uniform_kv_caches)。

  3. 启动: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)

StorageRuntime(open/register_memory/submit/wait)

GPU 显存注册

register_kv_caches → DMA 注册

register_memory(MemoryView{DEVICE})

一次性下发

多次 per-layer engine 调用

submit(IoRequest[], DEVICE_EXECUTION, stream) 一次调用 = 一次 fused kernel launch

文件/条带

GPUFile + 4 盘自动条带

URI:file://(FIEMAP 一次)、striped://(N 盘融合 kernel)

partial commit

无显式契约

IoSubmitOutcome.initial_states 逐项;被拒项必须窗口化重提交

环境依赖

内核模块随引擎

内核模块 → tutti_daemon → mount 三步;daemon 仅资源 broker

关键 gap:tutti 无 Python 绑定 → T-101 补(唯一新 C++ 面)。


3. 新版 vLLM 现状(接入可行性依据)

  1. maybe_transfer_kv_layer 原生存在(kv_transfer_utils.py:15-61), 已挂 unified attention——逐层 hook 零改动可用。

  2. 跨层 KV buffer 机制原生存在(allocate_uniform_kv_caches 等)。

  3. 外部包零侵入注册kv_connector_module_path 直接 import 外部 connector 类(factory.py:105-114,LMCache 同款)——本方案核心依据。

  4. 官方参考实现齐全(LMCache/Nixl/OffloadingConnector 等)。

  5. HMA:connector 不支持时需 --disable-hybrid-kv-cache-manager

  6. 无 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 资产

位置

复用方式

single_layer_kv_transfer

csrc/cuda/mem_kernels.cu:1012(模板特化 :1131)

T-117 已平移:单层 packed ↔ paged(slot_mapping),16 种 EngineKVFormat 全覆盖

multi_layer_kv_transfer

同文件 :904(+_fused_ptr :869)

跨层 [2,L,T,H] ↔ 每层独立 paged tensor(key_value_ptrs[L]——正是 vllm register_kv_caches 形态)

load_and_reshape_flash/reshape_and_cache_back_flash

:1160/:1209

“load and reshape” 本体,语义参照

EngineKVFormat + FormatFacts

csrc/engine_kv_format.h:36-292

paged 布局描述(16 种,编译期谓词)

normalize_kv_and_discover_format

lmcache/v1/gpu_connector/utils.py:186

register_kv_caches 时运行时布局探测

TensorMemoryObj 池管理

lmcache/v1/memory_management.py:635+

槽管理参照(ref_count/pin/unpin)

平移方式:拷 mem_kernels.cu 相关部分 + engine_kv_format.hkernels/(独立包 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)。