mesh-llm:把世界拼成一台巨型 GPU 之上的笔记
〇、先抛个问题
我手头只有一块 RTX 4060(8 GB 显存),想跑 Qwen3-235B(235 亿参数,F16 大约 470 GB)。按常规思路,这事根本办不了——显卡装不下。
但 mesh-llm 这帮人告诉你:「办得了。把家里那台闲置的 MacBook、你老婆的 Mac Studio、你公司的二手 P40、你邻居的 Steam Deck,全部连起来,把模型按层切片,各跑一段,串起来就是一台 470 GB 显存的大机器。」
听起来像吹牛。这篇分析就是把它拆开,看看里头到底是科学,还是货物崇拜(Cargo Cult)。
读完源码我得先说一句:它没骗人。但它做了什么、没做什么,做到了什么程度、有哪些明显的脆弱点——这些事我必须老老实实说清楚。
一、一句话定位(再说一遍强迫症)
mesh-llm = 一个去中心化的 LLM 推理节点二进制 + 一套控制平面协议。它的野心是:把全球闲着的 GPU 通过 Nostr 公告 / mDNS LAN 发现,再用 iroh(基于 QUIC 的 P2P 隧道)连起来,形成一个 mesh 网络;让任何一台机器都可以「挂着模型被别的节点调用」,或者「调用 mesh 里别人的模型」;调用接口是 OpenAI 兼容的 http://localhost:9337/v1。
它和单机的 llama.cpp 不一样:单机的 llama.cpp 解决「把模型跑在 GPU 上」,mesh-llm 解决「把模型拆到 N 台 GPU 上、跨网络协作推理」。
和 vLLM / TGI 不一样:那些是「单卡单机的推理加速器」,mesh-llm 是「跨机器的推理编排器」。
和 Petals 不一样:Petals 是 swarm serving,节点自由来去、用 Hivemind DHT 动态选 server;mesh-llm 是 operator-controlled,必须显式规划拓扑、显式签发加入 token(SignedNodeOwnership,默认 168 小时有效)。
和 Exo 不一样:Exo 是 tensor parallel(all-reduce)+ pipeline 混合,靠 Thunderbolt 5 RDMA(Apple Silicon 专属);mesh-llm 是纯 layer-pipeline(按层段切),无 tensor parallel,硬件覆盖 CUDA/ROCm/Vulkan/Metal/CPU 全平台。
这句话定位比读 30 段架构图更省事。
二、骨架图(先看图,再说话)
这张图是骨架,不是肌肉。下面我把每一块掰开讲。
三、控制平面:Mesh 层——节点怎么认识彼此
3.1 一句话:Mesh 是「管人不管活」
Mesh 层(mesh-llm-identity、mesh-llm-protocol、mesh-llm-routing、mesh-llm-hardware-profile、mesh-llm-guardrails、mesh-llm-system)不直接执行模型推理。它只管四件事:
- 你这个节点是谁(身份)
- 你能跟谁说话、用什么话说话(协议)
- 你有什么本事(硬件画像)
- 来了请求该派给谁(路由)
推理执行(跨节点 stage pipeline)由 Skippy 接手。这是一个控制平面抽象节点、执行平面抽象 stage的清晰分层。
3.2 节点的一生
按 docs/MESHES.md:42-75 + crates/mesh-llm-identity/src/ownership.rs:8-86,一个新节点要经历 5 步才算「活」:
- 生成 Owner Keypair:
mesh-llm auth init→OwnerKeypair::generate()。owner_id = sha256(ed25519_verify_key),64 字符 hex。owner 是「控制这个集群的人」,不是节点本身。 - 生成 Node Key:32 字节随机 hex,写到
~/.mesh-llm/key,unix 下0o600权限。 - 签发 NodeOwnershipClaim:owner 用自己的私钥给这个节点签一张证书——
cert_id + owner_id + node_endpoint_id + node_label + 7 天 expiry。没有这张证书的节点,没有资格加入 mesh。 - 创建/加入 mesh:
mesh-llm serve --model X --publish创建(immutable requirements 包含 protocol_generation=1);mesh-llm serve --join <signed-bootstrap-token>加入(用 token 走 Nostr 公告或 mDNS LAN)。 - 上心跳:60s 周期,每周期随机选 5 个 peer 跑 gossip;失败 ≥
failure_threshold(直连 2 次、relay-only 5 次)触发confirm_heartbeat_peer_down()→ 广播STREAM_PEER_DOWN。
每一步都对应一个失败点。最阴险的是第 4 步的 mDNS:mDNS 不接触 Nostr/relay,只走 LAN——所以你想跨公网加入 mesh,必须先用 Nostr 公告 bootstrap token。这就是为什么 README 第 30 行写「public mesh 部署需要 Nostr relay 中介」。
3.3 路由:会话语境优先,轮询兜底
这是 mesh-llm 路由层最聪明的一段。入口在 crates/mesh-client/src/network/affinity.rs:458-523 的 select_model_target_from_candidates():
incoming /v1 request (parsed_body)
│
├─ routing_keys(parsed_body)
│ ├─ session_hash = sha1( body.user || body.session_id )
│ ├─ prefix_hash = sha1( tools + functions + response_format
│ │ + tool_choice + parallel_tool_calls
│ │ + system/developer msgs )
│ └─ sticky_hash = session_hash ?? hash(prefix_hash + first_user_msg)
│
├─ if session_hash && sticky_enabled → pick_sticky(...) [SESSION]
├─ elif prefix_hash in AffinityRouter (TTL=20min, LRU 4096)
│ && candidate 仍存活 → 直接返回缓存 target [PREFIX]
├─ elif prefix_only_enabled(env) → pick_sticky(prefix_hash) [强制PREFIX]
├─ elif sticky_hash → pick_sticky(sticky_hash) [STICKY]
└─ else → pick_from(candidates) [ROUND-ROBIN]为什么这样设计?因为多轮对话的前缀 KV 缓存能跨节点复用——同一个 prefix_hash 命中同一台机器,能直接吃 KV cache,省掉一次 prefill。这就是 Petals 那种「KV cache 指针 cross-node match」思想,但实现上是 LRU 字典 + 节点健康度冷却(30s 起步、最多 5min)。
mesh-llm-guardrails 这里有个命名误导:它不是节点失败兜底——它是「输出/工具调用守门员」(GuardrailPolicy 控制 tool call 重试上限、上下文压缩触发点 90%)。节点掉线/超时兜底在 mesh-llm-host-runtime/src/network/target_health.rs 和 mesh/heartbeat.rs。
3.4 异构集群真能跑吗?
只能在「设备选择层」抹平,执行层不抹平。mesh-llm-hardware-profile 把所有后端归一为 NativeRuntimeBackendKind::{Cpu, Cuda, Rocm, Vulkan, Metal} 五种 flavor。但推理二进制是按 flavor 分包发行的(BinaryFlavor::suffix() = "cpu/cuda/rocm/vulkan/metal")——一个 Apple Silicon 节点不会去跑 ROCm stage。
这意味着:异构集群只能用作「按节点能力分派模型」,不能做 GPU 级别的算子融合。Mac Studio + MacBook 跨 iroh 跑 Qwen3-235B 分层是官方示例(SKIPPY.md:64-69),但这是「NVIDIA 不能参与」——不是一个统一的算子图。
四、执行平面:Skippy——把大模型切成 N 段
4.1 一句话:Skippy 是「按连续层段切 + 包制品化」
Skippy 不是 tensor parallel(all-reduce),也不是 attention head 切分(head parallel)。它是最朴素的 layer pipeline:把一个 40 层 transformer 切成 3 段,第 0–15 层放节点 A,第 15–30 层放节点 B,第 30–40 层放节点 C。数据流方向是 A → B → C 顺序推进。
为什么这么朴素?因为它鲁棒。tensor parallel 在 RDMA 上效率高,但 99% 集群没有 RDMA;head parallel 通信量巨大且拓扑敏感;layer pipeline 通信量可控(每次传一份 hidden-state 切片),拓扑容错强(任一节点挂了,重新选 peer 即可)。
4.2 模型怎么切?
读 crates/skippy-topology/src/lib.rs,有 4 种 planner:
plan_even_contiguous:按层数等分,最简单plan_weighted_contiguous:按 VRAM 容量加权(显存多的多分几层)plan_package_aware_contiguous_with_transport:节点打分 = 缓存亲和 + 缺包字节数 + RTT 惩罚 + availability_scoreplan_contiguous_with_splits:显式 split 点(用户指定在哪切)
最关键的是家族能力表 STAGE_RUNTIME_LLAMA_FAMILY_EXPECTATIONS(lib.rs:298-813),覆盖 llama.cpp 全部架构(100+ 个),对 jamba/lfm2/rwkv7/qwen3next 等混合架构打 recurrent_or_hybrid=true 标志,并记录「禁止切点」(比如 SharedKvProducerConsumer 标记的层不能切,否则 KV cache 失效)。
这就解释了为什么同一个切分算法不能用在所有模型上——不是算法问题,是模型架构问题。Skippy 把每种模型的「哪些层能切、哪些不能切」做成了一张表。这是它最被低估的设计。
4.3 节点间传什么?
hidden-state 切片,不是 token 也不是 KV。skippy-protocol::ActivationDescriptor(lib.rs:418-433)描述一个 activation:
version, dtype ∈ {F32, F16, Bf16},
layout ∈ {TokenMajor, Opaque},
producer_stage_index, layer 范围,
token_count, sequence_count,
payload_bytes, 可选 flags + payload_sha256粗略量级:Llama-3 8B F16(4096 dim)每 token ≈ 8 KB;32k context prefill 一段 ≈ 256 MB。Qwen3-235B 跨 4 stage prefilling 32k context,每跳大约传 512 MB(SKIPPY.md:64-69 实测)。
这意味着:stage 数越多,prefill 延迟越长(每跳传完整 hidden-state);decode 阶段不放大(每 token 一帧 activation)。plan_package_aware_contiguous_with_transport 加了 RTT 惩罚但没 hard cap——3 stage 之内很甜,4+ stage 端到端首 token 延迟被网络 RTT 主导。
4.4 谁来调度?coordinator 的 term fence
skippy-coordinator(pure 逻辑,lib.rs:1-391)做 term-based 选举。CoordinatorClaim 包含 coordinator_term,单调推进;ClaimFence.accept_claim 在 term mismatch 时 reject。quorum_requirement(n) = n/2 + 1。
但实际拓扑执行(peer 选择、stage 拉起、状态监控)放在 mesh-llm-host-runtime/src/runtime/split_planning.rs + mesh/mod.rs,coordinator crate 只负责 fence 一致性。换句话说,coordinator 是「裁判」,mesh 是「教练」。
4.5 错了怎么办?三层兜底
- 开发期验证:
skippy-correctness用 single-step / chain / split-scan / dtype-matrix 跑对照实验——先用单进程完整模型跑出 baseline token_id 和 predicted_token,再启动子进程 stage-server 跑分段推理,比较 token_id 是否一致。这是开发者本地工具,不是线上运行时。 - 运行期一致:skippy-cache/src/identity.rs 的
prefix_hash_with_namespace用 BLAKE3 把model_id / topology_id / stage_id / layer_range / ABI version / ctx_size / token_ids一起哈希,KV 复用不会跨错误 stage。 - 运行期校验:
skippy-protocol的validate_stage_control_request拒绝错误 generation、SHA-256 长度、artifact 路径逃逸(..)。
但有一个公开承认的脆弱点(SKIPPY.md:641-645):「active generations are not resumed across a topology failure」。当 coordinator 在 lease 内网络分区、另一个节点以 term+1 接管、然后老 leader 恢复,老 leader 的 in-flight LoadStage 会被 validate_load 挡掉,但已经把 token 推到下游 stage 的那部分计算静默丢弃。
这和 Exo 的「DiskEventLog 重放」形成对比:Exo 能恢复有序事件,Skippy 没有这一层。这是 Skippy 在跨 AZ 抖动场景下的已知失败率来源——文档没回避,是诚实的设计取舍。
五、模型与运行时:底座——把名字变成 GPU 张量
5.1 模型生命周期(5 步)
- Parse:
model-ref把llama-3-8b:Q4_K_M@main拆成ModelRef { repo, revision, selector },quant 后缀存到 selector。 - Resolve:
model-resolver::ModelResolver::resolve按「本地路径 → 本地模型目录 → 策展 catalog → HF fallback」四档返回候选。这是 Skippy「跳过本地 vs 跨节点」判断点——命中 layer-package 走 stage split 路径,命中 GGUF 走单节点路径。 - Artifact 选主:
model-artifact按 priority 选主文件(默认model.safetensors> 分片 > GGUF),并对 split GGUF 收集所有 shard。 - Download/Cache:
model-hf::HfModelRepository通过hf_hub拉取,带Range断点续传。 - Package/Stage:
model-package::prepare::resolve调用 HF Jobs REST API 在云端把大模型按 layer 切片,产出{dist}-layers仓库。这是云端切分,不是用户机器——你不需要自己写切层脚本。
5.2 三种 runtime 是命名误导
host-runtime / native-runtime / embedded-runtime 三个 crate 不是同一个东西的三个实现:
- host-runtime:完整节点进程(
run()、run_runtime()、initialize_host_runtime_with_config),含 inference pipeline、mesh 协议、跨节点 stage 调度。这是 CLImesh-llm启动的入口。 - native-runtime:dlopen loader——
manifest解析器、resolver(按default_rankCUDA=650 → Metal → ROCm → Vulkan → CPU 选)、cache(sha256 校验)。它不执行推理,只决定启动时加载哪个libllama.so。 - embedded-runtime:4 行 re-export
pub use mesh_llm_host_runtime::sdk::*,给桌面/SDK 集成方在进程内嵌 mesh 节点用。
真正可「切换」的是 NativeRuntimeBackend(CPU/Metal/CUDA/ROCm/Vulkan)。切换发生在 NativeRuntimeResolver::resolve(resolver.rs:64-100),没有热迁移,换 backend 要重启节点。
5.3 与 llama.cpp 的关系
是 fork + ABI 重写。仓库 third_party/llama.cpp/ 存在,被 patch 出 skippy.h 增加 stage execution 能力。skippy-ffi(crates/skippy-ffi/src/lib.rs:789-798)直接声明 C 符号 llama_log_set / ggml_log_set / llama_model_quantize / skippy_model_open。
mesh-llm-ffi 是 UniFFI 高层,不直接调 llama.cpp——它调的是 mesh_llm_sdk。这是 SDK 设计的层级:底层是 patched llama.cpp,中间是 skippy 包装,对外是 mesh SDK(Kotlin/Node/Swift)。
六、API 与控制台:人如何摸到这台巨型 GPU
6.1 OpenAI 兼容(端口 9337)
http://localhost:9337/v1 是 mesh-llm-host-runtime/src/network/openai/transport.rs 暴露的 TCP 反向代理,不直接挂载 openai-frontend::router——它把请求反代到本机 skippy 的 OpenAI surface。
| 路径 | 方法 | 处理器 |
|---|---|---|
/v1/models | GET | models() |
/v1/chat/completions | POST | chat_completions() |
/v1/completions | POST | completions() |
/v1/responses | POST | responses()(OpenAI Responses 风格,内部转 chat.completion) |
/health /healthz /readyz | GET | liveness / readiness |
chat_completion_route_accepts_tools_structured_output_and_logprobs 测试(router.rs:1274-1290)确认 tools、tool_choice:"auto"、parallel_tool_calls、response_format.json_schema、logprobs 都被真支持。但 n > 1 仍报 unsupported_model_feature——多 choice 未实现。
6.2 控制台(端口 3131)
mesh-llm-console-server/src/lib.rs:86-134 用 raw tokio TCP(不是 axum),所有路径(包括 /dashboard、/reserves、/chat、/configuration、/__playground、/__meshviz-perf)一律回 index.html,由前端接管。
前端:React 19.2.7 + Vite 8 + TanStack Router/Query + Tailwind 4 + Radix UI(17 个组件)+ katex + mermaid + pdfjs。9 个 feature 模块:dashboard / reserves / chat / configuration / network / status / developer / drawers / shell。
控制台没有鉴权层——只服务静态文件、无任何 token/bearer/cookie 处理。跨节点访问依赖 SSH 反向隧道、Tailscale、nginx + basic auth。这是已知设计取舍,文档没回避。
七、MoA:让"mesh"变成一个虚拟模型
7.1 是什么?
mesh-mixture-of-agents 暴露一个虚拟模型名 model:"mesh"。一次请求并行 fanout 到 N 个异构 LLM(可以是 OpenAI 兼容 HTTP、mesh-local、mesh-QUIC 任意后端),由确定性代码仲裁。
7.2 三档仲裁
- 早期共识:
first_answer_grace(默认仅 chat 启用),单个 conf≥0.5 答案先返回 - 仲裁:
arbiter::arbitrate(&outputs, query_uses_tools)返回Decision::{Answer, ToolCall, NeedsReducer} - Reducer 升级:conflicts 时
hedged_reducer_call并行候选 LLM 链,取先返回的合法输出
这不是简单投票,也不是纯 LLM 仲裁——是deterministic code + confidence 评分 + reducer LLM 升级三层。
7.3 MoA vs Skippy 一句话
MoA 是「多个完整模型投票 + 合成」(提升答案质量),Skippy 是「一个完整模型分片协作」(提升推理吞吐)。两者解决的维度正交,可以叠加——理论上你可以让 Skippy 切一个 70B 模型跑 stage 0–2,再让 MoA 把 stage 2 的输出拿去和另一个 70B 模型做仲裁。架构支持,但文档没明说。
八、三个费曼点(尖锐的)
8.1 它解决了什么问题,没解决什么问题?
解决了:
- 单机装不下的大模型(70B+)跨节点推理
- 异构集群(NVIDIA + Apple Silicon + AMD)的资源聚合
- OpenAI 兼容 API(工具调用、结构化输出、logprobs 都真支持)
没解决:
- 跨 AZ 抖动下的请求恢复:coordinator split-brain 后 in-flight 请求静默丢弃,没有 Exo 那种 event log 重放
- tensor parallel 场景:单层 GPU 不够但仍要跑大模型时,Skippy 帮不上(必须切层)
- 公网 mesh 接入:没有 Nostr relay 协助就只能在 LAN 玩
- 控制台鉴权:3131 端口裸奔,跨节点访问必须自建隧道
8.2 哪些设计是真聪明,哪些是货物崇拜?
真聪明:
- layer pipeline(朴素但鲁棒)+ BLAKE3 prefix identity(KV cache 不串 stage)
- 家族能力表(每种模型架构的「哪些层能切」做成表,而不是硬编码)
- 路由粘性 + 健康冷却(会话/前缀 KV 复用 + 30s 起步冷却)
货物崇拜嫌疑:
- 三层 runtime 命名(host/native/embedded)误导读者以为是可切换的推理引擎,实际是命名区分
- TUI 命名(
mesh-llm-tui)误导读者以为是完整 TUI 框架,实际只是 panic-safe 输出 mesh-llm-guardrails命名误导读者以为是节点失败兜底,实际是输出守门员mesh-llm-events::OutputEvent九大类(53 种事件)——这是事件总线膨胀的早期信号
8.3 哪些事我不知道?
诚实地列出来:
- MoA 的真实生产负载——文档说有但我没看到公开 benchmark
- HF Jobs 云端切分支持哪些模型家族——文档列了 Qwen3,但 Llama-3、Mistral、Gemma 是否都支持,我没读完
model-package/jobs.rs - 跨公网 mesh 接入的延迟实测——只有 LAN / 局域网数据
mesh-llm-skills是什么——名字像 LangChain 的 skills,但 README 没说清楚
我不需要假装知道。
九、收尾
mesh-llm 是一个把分布式系统朴素原则应用到 LLM 推理的项目:layer pipeline 而不是 tensor parallel、term fence 而不是 Raft、BLAKE3 prefix identity 而不是分布式 KV store、operator-controlled 而不是 swarm serving。它没有发明新理论,但把现有理论组合得足够鲁棒,足以跑生产负载。
它的竞争对手(Petals、Exo、Llama-Factory-distributed)都在不同维度做了不同取舍。mesh-llm 选了「跨平台 + 单二进制 + layer package 可重复部署」这条线,这是它的护城河,也是它的边界。
读这种项目的源码,最好的方法是先想一个具体问题——比如「如果我把 70B 模型按 3 stage 切,第一阶段节点断电了,token 已经推到第二阶段会怎么样」——然后顺着代码 trace。我前面写的「节点 1/2/3 各管一段跑 2+2=」就是这种练习。
读完之后,你会发现:它没什么神秘。就是把分布式系统那些老掉牙的原则,用 Rust 重写了一遍,加上一个 OpenAI 兼容的壳子。问题是,老掉牙的原则在新场景下重新组合,往往就是答案。
至于它能不能打——这就是真打起来才知道的事了。我没打过,所以不评价。
#CrushAI #FeynmanLearning #DistributedInference #MeshLLM #智柴系统实验室🎙️