From 090a022da873966d28969d37ec96cbe50f3188fd Mon Sep 17 00:00:00 2001 From: wangyihan Date: Tue, 21 Jul 2026 16:37:53 +0800 Subject: [PATCH] =?UTF-8?q?issue5:=20SGLang=20HiCache=20=E4=BB=A5=E5=AD=98?= =?UTF-8?q?=E4=BB=A3=E7=AE=97=E5=B8=A6=E5=AE=BD=E6=A8=A1=E5=9E=8B=E4=B8=8E?= =?UTF-8?q?=E5=9F=BA=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../issue5/benchmark/benchmark_concurrency.py | 239 ++++++++ .../issue5/benchmark/benchmark_kv_tensor.py | 248 ++++++++ .../issue5/benchmark/benchmark_prefill.py | 221 +++++++ .../issue5/benchmark/benchmark_transfer.py | 267 +++++++++ src/code/issue5/configs/rdma.yaml | 47 ++ src/code/issue5/configs/tcp.yaml | 32 ++ src/code/issue5/kvstore_mooncake.yaml | 171 ++++++ src/code/issue5/model/bandwidth_model.py | 298 ++++++++++ src/code/issue5/prefill_hicache.yaml | 174 ++++++ .../report/figures/bw_vs_concurrency.png | Bin 0 -> 30729 bytes src/code/issue5/report/figures/bw_vs_size.png | Bin 0 -> 49937 bytes .../issue5/report/figures/l2_zerocopy_bw.png | Bin 0 -> 32169 bytes .../report/figures/protocol_summary.png | Bin 0 -> 31871 bytes .../issue5/report/figures/qpm_compare.png | Bin 0 -> 27342 bytes .../issue5/report/figures/ttft_vs_load.png | Bin 0 -> 85778 bytes src/code/issue5/report/report.md | 537 ++++++++++++++++++ src/code/issue5/results/model_fit.csv | 25 + .../issue5/results/raw/conc_hihit_summary.csv | 10 + .../issue5/results/raw/conc_hihit_sweep.csv | 49 ++ src/code/issue5/results/raw/conc_summary.csv | 10 + src/code/issue5/results/raw/conc_sweep.csv | 43 ++ .../issue5/results/raw/kv_tensor_rdma.csv | 13 + src/code/issue5/results/raw/kv_tensor_tcp.csv | 13 + .../issue5/results/raw/kv_tensor_v32_rdma.csv | 13 + .../issue5/results/raw/kv_tensor_v32_tcp.csv | 13 + src/code/issue5/results/raw/prefill_local.csv | 16 + src/code/issue5/results/raw/transfer_rdma.csv | 25 + src/code/issue5/results/raw/transfer_tcp.csv | 25 + .../results/raw/transfer_zcget_rdma.csv | 13 + .../issue5/results/raw/transfer_zcget_tcp.csv | 13 + .../results/raw/transfer_zcput_rdma.csv | 13 + .../issue5/results/raw/transfer_zcput_tcp.csv | 13 + src/code/issue5/scripts/plot_results.py | 228 ++++++++ src/code/issue5/scripts/run_benchmark.sh | 137 +++++ src/code/issue5/scripts/start_kvstore.sh | 81 +++ src/code/issue5/scripts/start_prefill_v32.sh | 108 ++++ 36 files changed, 3095 insertions(+) create mode 100644 src/code/issue5/benchmark/benchmark_concurrency.py create mode 100644 src/code/issue5/benchmark/benchmark_kv_tensor.py create mode 100644 src/code/issue5/benchmark/benchmark_prefill.py create mode 100644 src/code/issue5/benchmark/benchmark_transfer.py create mode 100644 src/code/issue5/configs/rdma.yaml create mode 100644 src/code/issue5/configs/tcp.yaml create mode 100644 src/code/issue5/kvstore_mooncake.yaml create mode 100644 src/code/issue5/model/bandwidth_model.py create mode 100644 src/code/issue5/prefill_hicache.yaml create mode 100644 src/code/issue5/report/figures/bw_vs_concurrency.png create mode 100644 src/code/issue5/report/figures/bw_vs_size.png create mode 100644 src/code/issue5/report/figures/l2_zerocopy_bw.png create mode 100644 src/code/issue5/report/figures/protocol_summary.png create mode 100644 src/code/issue5/report/figures/qpm_compare.png create mode 100644 src/code/issue5/report/figures/ttft_vs_load.png create mode 100644 src/code/issue5/report/report.md create mode 100644 src/code/issue5/results/model_fit.csv create mode 100644 src/code/issue5/results/raw/conc_hihit_summary.csv create mode 100644 src/code/issue5/results/raw/conc_hihit_sweep.csv create mode 100644 src/code/issue5/results/raw/conc_summary.csv create mode 100644 src/code/issue5/results/raw/conc_sweep.csv create mode 100644 src/code/issue5/results/raw/kv_tensor_rdma.csv create mode 100644 src/code/issue5/results/raw/kv_tensor_tcp.csv create mode 100644 src/code/issue5/results/raw/kv_tensor_v32_rdma.csv create mode 100644 src/code/issue5/results/raw/kv_tensor_v32_tcp.csv create mode 100644 src/code/issue5/results/raw/prefill_local.csv create mode 100644 src/code/issue5/results/raw/transfer_rdma.csv create mode 100644 src/code/issue5/results/raw/transfer_tcp.csv create mode 100644 src/code/issue5/results/raw/transfer_zcget_rdma.csv create mode 100644 src/code/issue5/results/raw/transfer_zcget_tcp.csv create mode 100644 src/code/issue5/results/raw/transfer_zcput_rdma.csv create mode 100644 src/code/issue5/results/raw/transfer_zcput_tcp.csv create mode 100644 src/code/issue5/scripts/plot_results.py create mode 100755 src/code/issue5/scripts/run_benchmark.sh create mode 100755 src/code/issue5/scripts/start_kvstore.sh create mode 100644 src/code/issue5/scripts/start_prefill_v32.sh diff --git a/src/code/issue5/benchmark/benchmark_concurrency.py b/src/code/issue5/benchmark/benchmark_concurrency.py new file mode 100644 index 0000000..49cdbf9 --- /dev/null +++ b/src/code/issue5/benchmark/benchmark_concurrency.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +背景: 端到端命中路径被 write_through kernel 卡死, 无法在真 server 上跑并发 +命中。用实测数值驱动的离散事件重放(replay): 每个逻辑请求的服务时间由 + - 命中: T_lookup + T_restore(hN)/B_retrieve + T_prefill((1-h)N) [远端 retrieve + 后缀现算] + - 未命中: T_prefill(N) [全量重算] +其中 T_prefill(N) 用步骤5 从 L3 prefill_local.csv 拟合的 a·N+b·N² (ms), +T_restore 用 L2 kv_tensor 的 zerocopy get 带宽(TCP~1.1 / RDMA~20.7 GB/s)。 + +排队模型: c 个 prefill worker 的 FIFO 队列(GPU 算力受限, 默认 c=1 即算力串行), +Poisson 到达(率 λ req/s)。扫 λ 直到 P95 TTFT 超过 SLO -> λ_max; QPM=60·λ_max; +成本/query ∝ 固定节点机时 / QPM (归一到 no-cache 基线)。 + +SLO 对 TCP/RDMA/nocache **三者相同**(取 no-cache 单请求全量重算服务时间 × slo-factor), +保证公平对比。对每 context 各扫一遍。 + +用法 : + python3 benchmark/benchmark_concurrency.py \ + --calib results/raw/prefill_local.csv \ + --transfer-tcp results/raw/kv_tensor_tcp.csv \ + --transfer-rdma results/raw/kv_tensor_rdma.csv \ + --context 16k,24k --hit-prefix 0.5 --phit 0.5 --slo-factor 1.5 \ + --out-prefix results/raw/conc +""" +import argparse, csv, math, os, random, statistics, sys + +GB = 1 << 30 +S_TOKEN_V32 = 61 * (512 + 64) * 2 # 70272 B = 68.6 KiB/token (V3.2 MLA 单 latent) +PAGE = 64 + + +def align(n, page=PAGE): + return max(0, (n // page) * page) + + +def parse_ctx(s): + out = [] + for t in s.replace(" ", "").lower().split(","): + if not t: + continue + out.append(int(float(t[:-1]) * 1024) if t.endswith("k") else int(t)) + return out + + +def fit_tprefill(path): + """从 L3 CSV 最小二乘拟合 T_prefill(N)=a*N+b*N^2 (过原点, ms)。""" + import numpy as np + byN = {} + with open(path) as f: + for r in csv.DictReader(f): + byN.setdefault(int(r["context_length"]), []).append(float(r["recompute_ms"])) + pts = sorted((N, statistics.median(v)) for N, v in byN.items()) + Ns = np.array([p[0] for p in pts], float) + Ts = np.array([p[1] for p in pts], float) + A = np.vstack([Ns, Ns ** 2]).T + coef, *_ = np.linalg.lstsq(A, Ts, rcond=None) + return float(coef[0]), float(coef[1]), pts + + +def load_retrieve_bw(path, want="zerocopy", op="get"): + bws = [] + with open(path) as f: + for r in csv.DictReader(f): + if r.get("path") == want and r.get("op") == op and r.get("success") == "1": + try: + bws.append(float(r["effective_bandwidth_gbps"])) + except (ValueError, KeyError): + pass + return statistics.median(bws) if bws else None + + +def t_prefill(N, a, b): + return a * N + b * N * N + + +def service_ms(N, is_hit, h, a, b, b_ret_gbps, lookup_ms): + """单请求服务时间(ms)。命中: lookup+restore+后缀现算; 未命中: 全量重算。""" + if not is_hit: + return t_prefill(N, a, b) + hN = align(int(round(h * N))) + uniq = N - hN + s_kv_gb = hN * S_TOKEN_V32 / GB + t_restore = (s_kv_gb / b_ret_gbps) * 1e3 if b_ret_gbps else 0.0 + return lookup_ms + t_restore + t_prefill(uniq, a, b) + + +def simulate(lam_rps, n_req, workers, svc_fn, rng, warmup_frac=0.1): + """c-worker FIFO 队列 + Poisson 到达的离散事件重放, 返回 TTFT 列表(ms, 去 warmup)。 + FIFO+c 同构 server: 每到达分配给最早空闲的 worker。""" + free = [0.0] * workers # 各 worker 空闲时刻(ms) + t = 0.0 + ttfts = [] + for i in range(n_req): + t += rng.expovariate(lam_rps) * 1e3 # 到达间隔(ms) + w = min(range(workers), key=lambda k: free[k]) + start = max(t, free[w]) + svc = svc_fn() + finish = start + svc + free[w] = finish + ttfts.append(finish - t) # TTFT = 排队等待 + 服务 + cut = int(n_req * warmup_frac) + return ttfts[cut:] + + +def pctl(xs, p): + import numpy as np + return float(np.percentile(xs, p)) if xs else float("inf") + + +def make_svc_fn(N, phit, h, a, b, b_ret, lookup_ms, rng): + """返回一个无参函数: 每次按 phit 概率抽命中/未命中, 给出服务时间(ms)。""" + def fn(): + is_hit = (rng.random() < phit) + return service_ms(N, is_hit, h, a, b, b_ret, lookup_ms) + return fn + + +def sweep_lambda(N, phit, h, a, b, b_ret, lookup_ms, slo_ms, n_req, workers, + lam_grid, seed): + """扫 λ, 返回 (λ_max_rps, per_lambda_rows)。λ_max = P95 TTFT ≤ SLO 的最大 λ。""" + rows, lam_max = [], 0.0 + for lam in lam_grid: + rng = random.Random(seed) # 同 seed 保证各 λ 间可比 + svc = make_svc_fn(N, phit, h, a, b, b_ret, lookup_ms, rng) + ttfts = simulate(lam, n_req, workers, svc, rng) + p50, p95, p99 = pctl(ttfts, 50), pctl(ttfts, 95), pctl(ttfts, 99) + ok = p95 <= slo_ms + if ok: + lam_max = lam + rows.append(dict(lambda_rps=round(lam, 4), p50_ttft_ms=round(p50, 1), + p95_ttft_ms=round(p95, 1), p99_ttft_ms=round(p99, 1), + slo_ms=round(slo_ms, 1), within_slo=int(ok))) + if not ok and lam_max > 0: + break # 已越过 SLO, 后续更高 λ 无意义 + return lam_max, rows + + +def build_grid(base_svc_ms, workers): + """λ 网格: 以单 worker 饱和率 (workers*1000/base_svc) 为量程, 细扫到 ~1.3×。""" + sat = workers * 1000.0 / base_svc_ms # 名义饱和到达率(req/s) + return [sat * f for f in [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.75, 0.8, + 0.85, 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.3]] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--calib", default="results/raw/prefill_local.csv") + ap.add_argument("--transfer-tcp", default="results/raw/kv_tensor_tcp.csv") + ap.add_argument("--transfer-rdma", default="results/raw/kv_tensor_rdma.csv") + ap.add_argument("--context", default="8k,16k,24k") + ap.add_argument("--hit-prefix", type=float, default=0.5, help="命中请求复用的前缀比例 h") + ap.add_argument("--phit", type=float, default=0.5, help="请求命中缓存的概率") + ap.add_argument("--workers", type=int, default=1, help="并行 prefill worker 数 (算力串行=1)") + ap.add_argument("--lookup-ms", type=float, default=5.0, help="远端查表固定开销") + ap.add_argument("--slo-factor", type=float, default=1.5, help="SLO = nocache 单请求服务 × 此") + ap.add_argument("--n-req", type=int, default=4000) + ap.add_argument("--seed", type=int, default=2026) + ap.add_argument("--out-prefix", default="results/raw/conc") + args = ap.parse_args() + + a, b, pts = fit_tprefill(args.calib) + bw = {"nocache": None, + "tcp": load_retrieve_bw(args.transfer_tcp), + "rdma": load_retrieve_bw(args.transfer_rdma)} + print(f"[fit] T_prefill(N)={a:.5f}·N+{b:.3e}·N² | " + f"B_retrieve tcp={bw['tcp']:.2f} rdma={bw['rdma']:.2f} GB/s", flush=True) + print(f"[cfg] hit_prefix h={args.hit_prefix} phit={args.phit} workers={args.workers} " + f"lookup={args.lookup_ms}ms slo_factor={args.slo_factor}", flush=True) + + ctxs = parse_ctx(args.context) + all_rows, summary = [], [] + for N in ctxs: + # SLO 由 no-cache 全量重算服务时间定, 三种传输共用同一 SLO (公平) + base = t_prefill(N, a, b) + slo = base * args.slo_factor + # 平均服务时间 (用于 λ 网格量程): 取 nocache 与 hit 的期望 + for transport, b_ret in bw.items(): + # nocache = 缓存关闭 => 每请求都全量重算 (phit_eff=0); 有缓存 => 用真实 phit + phit_eff = 0.0 if transport == "nocache" else args.phit + mean_svc = (phit_eff * service_ms(N, True, args.hit_prefix, a, b, + b_ret, args.lookup_ms) + + (1 - phit_eff) * t_prefill(N, a, b)) + grid = build_grid(mean_svc, args.workers) + lam_max, rows = sweep_lambda(N, phit_eff, args.hit_prefix, a, b, b_ret, + args.lookup_ms, slo, args.n_req, args.workers, + grid, args.seed) + qpm = 60.0 * lam_max + for r in rows: + r.update(transport=transport, context_length=N, hit_prefix=args.hit_prefix, + phit=phit_eff, workers=args.workers) + all_rows.append(r) + summary.append(dict(context_length=N, transport=transport, + mean_service_ms=round(mean_svc, 1), slo_ms=round(slo, 1), + lambda_max_rps=round(lam_max, 4), qpm=round(qpm, 2))) + print(f" N={N//1024}k {transport:8} mean_svc={mean_svc:7.0f}ms " + f"SLO={slo:7.0f}ms λ_max={lam_max:.4f}rps QPM={qpm:.2f}", flush=True) + + # 每 context 打印 TCP/RDMA 相对 no-cache 的 QPM 增益 + RDMA vs TCP 成本比 + print("\n[对比] QPM 增益 (相对 no-cache) 与 成本/query (∝ 1/QPM, 归一 nocache):") + byN = {} + for s in summary: + byN.setdefault(s["context_length"], {})[s["transport"]] = s + for N, d in sorted(byN.items()): + base_qpm = d["nocache"]["qpm"] or 1e-9 + line = f" N={N//1024}k:" + for tp in ("nocache", "tcp", "rdma"): + q = d[tp]["qpm"] + gain = q / base_qpm + cost = base_qpm / q if q > 0 else float("inf") + line += f" {tp}=QPM {q:.1f}(×{gain:.2f}, cost×{cost:.2f})" + # RDMA vs TCP + if d["tcp"]["qpm"] > 0: + line += f" || rdma/tcp QPM={d['rdma']['qpm']/d['tcp']['qpm']:.2f}×" + print(line, flush=True) + + _write(args.out_prefix + "_sweep.csv", all_rows, + ["transport", "context_length", "hit_prefix", "phit", "workers", + "lambda_rps", "p50_ttft_ms", "p95_ttft_ms", "p99_ttft_ms", "slo_ms", "within_slo"]) + _write(args.out_prefix + "_summary.csv", summary, + ["context_length", "transport", "mean_service_ms", "slo_ms", "lambda_max_rps", "qpm"]) + print(f"\n[out] {args.out_prefix}_sweep.csv ({len(all_rows)} rows) + " + f"{args.out_prefix}_summary.csv ({len(summary)} rows)", flush=True) + + +def _write(path, rows, cols): + if not rows: + return + with open(path, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore") + w.writeheader() + for r in rows: + w.writerow(r) + + +if __name__ == "__main__": + main() + + diff --git a/src/code/issue5/benchmark/benchmark_kv_tensor.py b/src/code/issue5/benchmark/benchmark_kv_tensor.py new file mode 100644 index 0000000..77147b9 --- /dev/null +++ b/src/code/issue5/benchmark/benchmark_kv_tensor.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Issue5 真实 KV Tensor Transfer Microbench + +在 L1(随机 buffer) 之上, 按真实模型 config 造 **每 token KV 的 shape/dtype/分层布局**, +走同一个 mooncake store, 但**镜像 sglang hicache 的真实零拷贝路径**: + register_buffer(整块 KV buffer) -> batch_put_from / batch_get_into (逐 page, 每 page 2 个 key) +对照一条 **naive bytes 路径**(put_batch/get_batch + python bytes), 复现 L1 发现的 GET 瓶颈, +并证明零拷贝能否让 retrieve 突破 ~1.9 GB/s。仍**不跑 attention**, 因此绕开两个昆仑算子 blocker。 + +真实布局参考 sglang mooncake_store.py: + - MHA/GQA: 每 page 2 个 key ({k}, {v}); bytes_per_page = ksize_per_token * page_size + - MLA: 每 page 1 个 key; latent = (kv_lora_rank + qk_rope_head_dim) + - register_buffer(data_ptr, nbytes) 一次, 逐 page 给 (ptr+offset, size) 做 batch_put_from/get_into + - batch_put_from 返回码: 每元素 0 成功; batch_get_into 返回码: 每元素 >0(读到的字节数) 成功 + +用法 (prefill pod 内, conda env python310_torch29_cuda): + export PATH=/root/miniconda/envs/python310_torch29_cuda/bin:$PATH + TRANSPORT=tcp python3 benchmark_kv_tensor.py --model qwen3-32b --out kv_tcp.csv + TRANSPORT=rdma python3 benchmark_kv_tensor.py --model qwen3-32b --device mlx5_3 --out kv_rdma.csv +""" +import argparse, os, time, uuid, statistics, csv, sys + +GB = 1 << 30 +MiB = 1 << 20 +KiB = 1 << 10 + +# 模型 config 预设 (每 TP rank 视角): 逐层 KV, K 与 V 各一份 +# S_token(整模型/rank) = 2 * L * H_kv * D_h * b (GQA/MHA) +# = L * (kv_lora_rank + qk_rope) * b, 单 latent (MLA, 无独立 V) +MODELS = { + # Qwen3-32B: L=64, H_kv=8, D_h=128, bf16; 整模型 256 KiB/token, TP8 -> 32 KiB/token/rank + "qwen3-32b": dict(kind="mha", num_layers=64, num_kv_heads=8, head_dim=128, dtype_bytes=2), + # DeepSeek-V3.2 (MLA): L=61, kv_lora_rank=512, qk_rope_head_dim=64, bf16; 单 latent, 无独立 V + "deepseek-v32": dict(kind="mla", num_layers=61, kv_lora_rank=512, qk_rope=64, dtype_bytes=2), +} + + +def human(n): + if n >= GB: return f"{n/GB:.1f}GB" + if n >= MiB: return f"{n/MiB:.0f}MB" + return f"{n}B" + + +def parse_ctx(s): + out = [] + for tok in s.replace(" ", "").lower().split(","): + if not tok: continue + if tok.endswith("k"): out.append(int(float(tok[:-1]) * 1024)) + else: out.append(int(tok)) + return out + + +def s_token_per_rank(cfg, tp): + """每 token / 每 rank 的 KV 字节数。""" + if cfg["kind"] == "mha": + hkv = cfg["num_kv_heads"] // tp + assert hkv >= 1, "num_kv_heads 必须 >= tp" + return 2 * cfg["num_layers"] * hkv * cfg["head_dim"] * cfg["dtype_bytes"] + else: # mla: 单 latent, 逐层, 不随 TP 切分 (MLA latent 各 rank 复制/或 tp=1 视角) + return cfg["num_layers"] * (cfg["kv_lora_rank"] + cfg["qk_rope"]) * cfg["dtype_bytes"] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--master", default=os.environ.get("KV_IP", "192.0.2.10") + ":50051") + ap.add_argument("--metadata", default="http://" + os.environ.get("KV_IP", "192.0.2.10") + ":18080/metadata") + ap.add_argument("--protocol", default=os.environ.get("TRANSPORT", "tcp")) + ap.add_argument("--device", default=os.environ.get("MC_DEVICE", "")) + ap.add_argument("--local-hostname", default=os.environ.get("HOST_IP", "")) + ap.add_argument("--model", default="qwen3-32b", choices=list(MODELS)) + ap.add_argument("--tp", type=int, default=8) + ap.add_argument("--context", default="8k,32k,128k") + ap.add_argument("--page-size", type=int, default=64) + ap.add_argument("--paths", default="bytes,zerocopy") # 逗号: bytes / zerocopy + ap.add_argument("--iters", type=int, default=5) + ap.add_argument("--local-buffer-gb", type=float, default=1.0) + ap.add_argument("--out", default="/root/kv_tensor.csv") + args = ap.parse_args() + + if not args.local_hostname: + import socket + args.local_hostname = socket.gethostbyname(socket.gethostname()) + + global torch + import torch as torch + from mooncake.store import MooncakeDistributedStore + store = MooncakeDistributedStore() + ret = store.setup(args.local_hostname, args.metadata, 0, + int(args.local_buffer_gb * GB), args.protocol, + args.device, args.master) + if ret: + print(f"[FATAL] store.setup ret={ret}", file=sys.stderr); sys.exit(1) + print(f"[ok] setup protocol={args.protocol} device='{args.device}' model={args.model} " + f"tp={args.tp} local={args.local_hostname}", flush=True) + + cfg = MODELS[args.model] + st = s_token_per_rank(cfg, args.tp) + print(f"[cfg] S_token/rank = {st} B ({human(st)}), page={args.page_size} " + f"-> {human(st*args.page_size)}/page", flush=True) + + ctxs = parse_ctx(args.context) + paths = [p for p in args.paths.split(",") if p.strip()] + rows, tag = [], uuid.uuid4().hex[:8] + + try: # 清空可能的历史残留, 保证干净起点 + n = store.remove_all(); print(f"[clean] remove_all removed {n} objects", flush=True) + except Exception as e: + print(f"[clean] remove_all skip: {e}", flush=True) + + for N in ctxs: + for path in paths: + _bench(store, cfg, args, N, st, path, tag, rows) + + _write_csv(args.out, rows) + print(f"[done] wrote {len(rows)} rows -> {args.out}", flush=True) + + +def _layout(cfg, N, st, page_size): + """返回 (num_pages, keys_per_page, key_size_bytes, kv_bytes)。 + mha: 每 page 2 key(k,v), 各 (st/2)*page; mla: 每 page 1 key, st*page。""" + num_pages = N // page_size + if cfg["kind"] == "mha": + keys_per_page = 2 + key_size = (st // 2) * page_size + else: + keys_per_page = 1 + key_size = st * page_size + kv_bytes = num_pages * keys_per_page * key_size + return num_pages, keys_per_page, key_size, kv_bytes + + +def _median_bw(times, kv_bytes): + if not times: return 0.0, 0.0 + med = statistics.median(times) + bw = (kv_bytes / med / GB) if med > 0 else 0.0 + return med * 1e3, bw # ms, GB/s + + +def _bench(store, cfg, args, N, st, path, tag, rows): + ps = args.page_size + num_pages, kpp, key_size, kv_bytes = _layout(cfg, N, st, ps) + numel = kv_bytes // cfg["dtype_bytes"] # bf16 元素数 + prefix = f"kv_{tag}_{args.model}_{N}_{path}" + + # 造真实 KV: 逐元素 bf16, 值域 ~ N(0,0.1) 贴近真实激活尺度; read 端清零 + torch.manual_seed(1234) + write = (torch.randn(numel, dtype=torch.float32) * 0.1).to(torch.bfloat16).contiguous() + read = torch.zeros(numel, dtype=torch.bfloat16).contiguous() + + # 每 iter 唯一 key (含 it): 必须如此才能测到真实 PUT 传输带宽。 + # 关键坑 (见 doc/步骤3): TCP 模式下 mooncake put/batch_put_from 对**已存在的 key 是 + # no-op(按 key 去重, 直接跳过传输)** -> 若复用同一批 key, iter>=1 的 put 只有 ~3ms + # (假带宽 ~90GB/s)。故逐 iter 用唯一 key 做真插入, 并在每轮结束后 remove 释放 working + # set(对未被覆盖写的新 key, TCP/RDMA 下 remove 均返回 0), 避免 handle 累积耗尽。 + def page_keys(it): + ks = [] + for p in range(num_pages): + ks.append(f"{prefix}_{it}_{p}_k") + if kpp == 2: + ks.append(f"{prefix}_{it}_{p}_v") + return ks + + put_t, get_t, max_err, ok = [], [], -1.0, 1 + + if path == "zerocopy": + # 镜像 sglang: register_buffer 整块, batch_put_from/batch_get_into 逐 key(ptr+offset,size) + rc1 = store.register_buffer(write.data_ptr(), write.numel() * cfg["dtype_bytes"]) + rc2 = store.register_buffer(read.data_ptr(), read.numel() * cfg["dtype_bytes"]) + if rc1 or rc2: + print(f"[warn] register_buffer rc=({rc1},{rc2}) path={path} N={N}", flush=True) + wptrs = [write.data_ptr() + i * key_size for i in range(num_pages * kpp)] + rptrs = [read.data_ptr() + i * key_size for i in range(num_pages * kpp)] + sizes = [key_size] * (num_pages * kpp) + for it in range(args.iters): + ks = page_keys(it) + t = time.perf_counter(); pr = store.batch_put_from(ks, wptrs, sizes) + put_t.append(time.perf_counter() - t) + if any(r != 0 for r in pr): ok = 0 + t = time.perf_counter(); gr = store.batch_get_into(ks, rptrs, sizes) + get_t.append(time.perf_counter() - t) + if any(r <= 0 for r in gr): ok = 0 + for k in ks: # 逐轮清理 (untimed), 释放 handle + try: store.remove(k) + except Exception: pass + max_err = float((read.to(torch.float32) - write.to(torch.float32)).abs().max()) + store.unregister_buffer(write.data_ptr()); store.unregister_buffer(read.data_ptr()) + else: + max_err, ok = _bench_bytes(store, write, page_keys, key_size, + num_pages, kpp, args.iters, put_t, get_t) + # 兜底: 清掉本 context 可能残留的全部 key (remove_all 在 TCP/RDMA 下均可用) + try: store.remove_all() + except Exception: pass + + put_ms, put_bw = _median_bw(put_t, kv_bytes) + get_ms, get_bw = _median_bw(get_t, kv_bytes) + for op, ms, bw, me in (("put", put_ms, put_bw, -1.0), ("get", get_ms, get_bw, max_err)): + rows.append(dict( + transport=args.protocol, model=args.model, path=path, op=op, + context=N, tp=args.tp, page_size=ps, num_pages=num_pages, + num_keys=num_pages * kpp, kv_bytes=kv_bytes, kv_size=human(kv_bytes), + iters=args.iters, transfer_ms=round(ms, 3), + effective_bandwidth_gbps=round(bw, 3), + max_abs_error=("" if me < 0 else round(me, 6)), success=ok)) + print(f" {args.protocol:4} {path:8} {op} N={N:<7} {human(kv_bytes):>7} " + f"keys={num_pages*kpp:<5} BW={bw:.2f} GB/s {ms:.1f}ms " + f"err={'' if me<0 else round(me,5)} ok={ok}", flush=True) + del write, read + + +def _bench_bytes(store, write, page_keys, key_size, num_pages, kpp, iters, put_t, get_t): + """naive bytes 路径: put_batch/get_batch, 复现 L1 的 python bytes 拷贝瓶颈。""" + u8 = write.view(torch.uint8).contiguous() + mv = memoryview(u8.numpy()) + vals = [bytes(mv[i * key_size:(i + 1) * key_size]) for i in range(num_pages * kpp)] + max_err, ok = -1.0, 1 + for it in range(iters): + ks = page_keys(it) + t = time.perf_counter(); rc = store.put_batch(ks, vals) + put_t.append(time.perf_counter() - t) + if rc != 0: ok = 0 + t = time.perf_counter(); got = store.get_batch(ks) + get_t.append(time.perf_counter() - t) + if it == iters - 1: + bad = sum(1 for g in got if (g is None or len(g) != key_size)) + if bad: ok = 0 + max_err = 0.0 if (not bad and all(bytes(got[i]) == vals[i] + for i in range(len(vals)))) else 1.0 + for k in ks: + try: store.remove(k) + except Exception: pass + return max_err, ok + + +def _write_csv(path, rows): + if not rows: return + cols = ["transport", "model", "path", "op", "context", "tp", "page_size", + "num_pages", "num_keys", "kv_bytes", "kv_size", "iters", "transfer_ms", + "effective_bandwidth_gbps", "max_abs_error", "success"] + with open(path, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore") + w.writeheader() + for r in rows: w.writerow(r) + + +if __name__ == "__main__": + main() diff --git a/src/code/issue5/benchmark/benchmark_prefill.py b/src/code/issue5/benchmark/benchmark_prefill.py new file mode 100644 index 0000000..52c07d3 --- /dev/null +++ b/src/code/issue5/benchmark/benchmark_prefill.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Issue5 prefill_only end to end microbench + +prefill_only(max_new_tokens=0)在本地 device radix cache 下: + - 全量重算 T_prefill(N) = 冷 prefill 长度 N。 -> recompute 基线, 喂 B_crit 分母。 + - 前缀本地命中 total(N,h) = 先发 shared_prefix(hN) 预热进 radix, 不 flush, 再发 + shared_prefix + unique_suffix((1-h)N); cached_tokens=hN, + prefix 的 KV 直接复用(device 本地, restore≈0), 只现算后缀。 + - saved(N,h) = T_prefill(N) - total(N,h) = 复用 hN 前缀 KV 省下的**计算**时间。 +这是「以存代算」的**算力节省侧**真实标定(传输侧由 L2 给); 本地命中 => transfer/restore≈0。 + +命中率 h: shared_prefix 长度 = align(round(h*N), page); unique_suffix = N - prefix。 +JIT/无 cuda graph => 每个 shape 首次请求有编译毛刺, 故每 shape 先 warmup 丢弃, 再取 median。 + +用法(prefill pod 内, server 已 ready, 用 start_prefill_v32_nohicache.sh 起): + export PATH=/root/miniconda/envs/python310_torch29_cuda/bin:$PATH + python3 benchmark_prefill.py --base http://$(hostname -i):8000 --mode local \ + --transport local_radix --context 8k,32k,128k --hit 0,0.25,0.5,0.75,1.0 \ + --iters 3 --warmup 1 --out /root/prefill_local.csv +""" +import argparse, os, json, time, statistics, csv, sys, random, urllib.request + +GB = 1 << 30 +KiB = 1 << 10 +# DeepSeek-V3.2 MLA: 单 latent, 每 token KV = L*(kv_lora_rank+qk_rope)*b +V32 = dict(num_layers=61, kv_lora_rank=512, qk_rope=64, dtype_bytes=2) + + +def s_token_v32(): + return V32["num_layers"] * (V32["kv_lora_rank"] + V32["qk_rope"]) * V32["dtype_bytes"] + + +def parse_ctx(s): + out = [] + for tok in s.replace(" ", "").lower().split(","): + if not tok: + continue + out.append(int(float(tok[:-1]) * 1024) if tok.endswith("k") else int(tok)) + return out + + +def post(base, path, obj, timeout=1800): + data = json.dumps(obj).encode() + req = urllib.request.Request(base + path, data=data, + headers={"Content-Type": "application/json"}) + t = time.perf_counter() + with urllib.request.urlopen(req, timeout=timeout) as r: + body = r.read() + dt = time.perf_counter() - t + try: + return dt, json.loads(body) + except Exception: + return dt, {"_raw": body[:300].decode(errors="replace")} + + +def flush(base): + # /flush_cache 清本地 radix(host+device)。注意: 有在跑的请求时返回 400, 故只在 idle 调。 + try: + urllib.request.urlopen(urllib.request.Request(base + "/flush_cache", method="POST"), + timeout=60).read() + except Exception: + pass + time.sleep(0.4) + + +def cached_tokens(meta): + m = meta.get("meta_info", meta) if isinstance(meta, dict) else {} + return int(m.get("cached_tokens", -1)), int(m.get("prompt_tokens", -1)) + + +def align(n, page): + return max(0, (n // page) * page) + + +def make_ids(n, seed, vocab_lo=10, vocab_hi=100000): + rng = random.Random(seed) + return [rng.randint(vocab_lo, vocab_hi) for _ in range(n)] + + +def gen_prefill(base, ids, timeout=1800): + """发一个 prefill_only 请求 (max_new_tokens=0), 返回 (e2e_s, cached, prompt)。""" + body = {"input_ids": ids, "sampling_params": {"max_new_tokens": 0, "temperature": 0.0}, + "stream": False, "return_logprob": False} + dt, resp = post(base, "/generate", body, timeout=timeout) + c, p = cached_tokens(resp) + return dt, c, p, resp + + +def median_fresh(base, n, page, iters, seed0): + """冷全量 prefill 长度 n 的 median 墙钟(ms)。每次 flush + 唯一内容 => 真现算; shape 已预热。""" + ts = [] + for it in range(iters): + flush(base) + dt, c, p, _ = gen_prefill(base, make_ids(n, seed0 + it)) + ts.append(dt * 1e3) + return statistics.median(ts) + + +def warm_shape(base, n, seed): + """对某个长度 n 先发一个 throwaway 请求, 触发 JIT 编译(--disable-cuda-graph 首次有毛刺)。""" + flush(base) + try: + gen_prefill(base, make_ids(n, seed)) + except Exception: + pass + + +def bench_one(base, N, h, page, iters, warmup, st, rows, transport, mode): + Lhit = align(N, page) if h >= 1.0 else align(int(round(h * N)), page) + Luniq = N - Lhit + prefix = make_ids(Lhit, seed=42) if Lhit > 0 else [] + + # 预热 shape N (JIT), 然后测 recompute 基线 = 冷全量 T_prefill(N) + if warmup: + warm_shape(base, N, seed=90000) + cold_full_ms = median_fresh(base, N, page, iters, seed0=90001) + + # 冷 unique 部分 (1-h)N: 命中时仍需现算的后缀, 作为下界参考 + if Luniq > 0: + if warmup: + warm_shape(base, Luniq, seed=91000) + cold_uniq_ms = median_fresh(base, Luniq, page, iters, seed0=91001) + else: + cold_uniq_ms = 0.0 + + hit_ms_list, cached_list, prompt_list = [], [], [] + if Lhit > 0: + # 预热前缀进本地 radix (不 flush), 再预热一次 prefix+suffix 的完整 shape + flush(base) + gen_prefill(base, prefix) # warm prefix KV into radix (untimed) + if warmup: + gen_prefill(base, prefix + (make_ids(Luniq, seed=69999) if Luniq > 0 else [])) + for it in range(iters): + # 每 iter 唯一后缀; **不 flush** => prefix 一直在本地 radix, cached=Lhit + suffix = make_ids(Luniq, seed=70000 + it) if Luniq > 0 else [] + dt, c, p, _ = gen_prefill(base, prefix + suffix) + hit_ms_list.append(dt * 1e3) + cached_list.append(c) + prompt_list.append(p) + else: + # h=0: 命中即 cold_full 自身 (无前缀可复用) + if warmup: + warm_shape(base, N, seed=79999) + for it in range(iters): + flush(base) + dt, c, p, _ = gen_prefill(base, make_ids(N, seed=80000 + it)) + hit_ms_list.append(dt * 1e3) + cached_list.append(c) + prompt_list.append(p) + + hit_ms = statistics.median(hit_ms_list) + cached = int(statistics.median(cached_list)) if cached_list else -1 + prompt = int(statistics.median(prompt_list)) if prompt_list else -1 + kv_bytes = st * max(cached, 0) + # 本地命中: prefix KV 已在 device, restore≈0; saved = 全量重算 - 命中总时 = 省下的算力时间 + restore_ms = 0.0 + saved_ms = max(cold_full_ms - hit_ms, 0.0) + # 本地命中传输为 0 => 有效带宽不适用(留空); 远端 retrieve 带宽由 L2 给 + eff_bw = "" + + rows.append(dict( + transport=transport, context_length=N, hit_ratio=h, concurrency=1, + prefix_tokens=Lhit, uniq_tokens=Luniq, cached_tokens=cached, prompt_tokens=prompt, + kv_bytes=kv_bytes, s_token=st, + lookup_ms="", transfer_ms=0.0, restore_ms=restore_ms, + cold_uniq_ms=round(cold_uniq_ms, 2), recompute_ms=round(cold_full_ms, 2), + saved_ms=round(saved_ms, 2), total_ms=round(hit_ms, 2), + effective_bandwidth_gbps=eff_bw, max_abs_error="", success=1)) + print(f" {transport:10} N={N:<7} h={h:<4} prefix={Lhit:<7} cached={cached:<7} " + f"total={hit_ms:.0f}ms recompute={cold_full_ms:.0f}ms saved={saved_ms:.0f}ms " + f"cold_uniq={cold_uniq_ms:.0f}ms", flush=True) + + +def write_csv(path, rows): + if not rows: + return + cols = ["transport", "context_length", "hit_ratio", "concurrency", "prefix_tokens", + "uniq_tokens", "cached_tokens", "prompt_tokens", "kv_bytes", "s_token", + "lookup_ms", "transfer_ms", "restore_ms", "cold_uniq_ms", "recompute_ms", + "saved_ms", "total_ms", "effective_bandwidth_gbps", "max_abs_error", "success"] + with open(path, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore") + w.writeheader() + for r in rows: + w.writerow(r) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--base", default="http://127.0.0.1:8000") + ap.add_argument("--mode", default="local", choices=["local"], + help="local: 本地 device radix 命中(远端 write_through 被昆仑 kernel blocker 卡死)") + ap.add_argument("--transport", default="local_radix") + ap.add_argument("--context", default="8k,32k,128k") + ap.add_argument("--hit", default="0,0.25,0.5,0.75,1.0") + ap.add_argument("--page-size", type=int, default=64) + ap.add_argument("--iters", type=int, default=3) + ap.add_argument("--warmup", type=int, default=1, help="每 shape 先发几个 throwaway 触发 JIT") + ap.add_argument("--out", default="/root/prefill_local.csv") + args = ap.parse_args() + + st = s_token_v32() + print(f"[cfg] V3.2 MLA S_token/token = {st} B ({st/KiB:.1f} KiB), mode={args.mode}, base={args.base}", flush=True) + ctxs = parse_ctx(args.context) + hits = [float(x) for x in args.hit.replace(" ", "").split(",") if x] + rows = [] + for N in ctxs: + for h in hits: + try: + bench_one(args.base, N, h, args.page_size, args.iters, args.warmup, + st, rows, args.transport, args.mode) + except Exception as e: + print(f" [err] N={N} h={h}: {e}", flush=True) + write_csv(args.out, rows) + print(f"[done] wrote {len(rows)} rows -> {args.out}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/src/code/issue5/benchmark/benchmark_transfer.py b/src/code/issue5/benchmark/benchmark_transfer.py new file mode 100644 index 0000000..479f09c --- /dev/null +++ b/src/code/issue5/benchmark/benchmark_transfer.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Issue5 步骤2 / Layer1 —— Mooncake primitive microbench (不进模型) + +直接用 mooncake python client 连已起的远端 store (kvstore 节点), put/get 不同大小 buffer, +测传输层本身的带宽/延迟。TCP vs RDMA 用 TRANSPORT 环境变量或 --protocol 切换。 + +用法 (在 prefill pod 内, conda env python310_torch29_cuda): + export PATH=/root/miniconda/envs/python310_torch29_cuda/bin:$PATH + TRANSPORT=tcp python3 benchmark_transfer.py --out /root/transfer_tcp.csv + TRANSPORT=rdma python3 benchmark_transfer.py --out /root/transfer_rdma.csv --device eth1 + +setup(local_hostname, metadata_server, global_segment_size, local_buffer_size, + protocol, device_name, master_server_address) # 7 位置参数, 见 sglang mooncake_store.py +""" +import argparse, os, time, uuid, statistics, csv, sys +from concurrent.futures import ThreadPoolExecutor + +GB = 1 << 30 +MB = 1 << 20 + + +def human(n): + if n >= GB: return f"{n/GB:.0f}GB" + if n >= MB: return f"{n/MB:.0f}MB" + return f"{n}B" + + +def parse_sizes(s): + out = [] + for tok in s.split(","): + tok = tok.strip().lower() + if not tok: continue + if tok.endswith("gb"): out.append(int(float(tok[:-2]) * GB)) + elif tok.endswith("mb"): out.append(int(float(tok[:-2]) * MB)) + elif tok.endswith("kb"): out.append(int(float(tok[:-2]) * 1024)) + else: out.append(int(tok)) + return out + + +def pctl(vals, p): + if not vals: return 0.0 + xs = sorted(vals) + k = max(0, min(len(xs) - 1, int(round((p / 100.0) * (len(xs) - 1))))) + return xs[k] + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--master", default=os.environ.get("KV_IP", "192.0.2.10") + ":50051") + ap.add_argument("--metadata", default="http://" + os.environ.get("KV_IP", "192.0.2.10") + ":18080/metadata") + ap.add_argument("--protocol", default=os.environ.get("TRANSPORT", "tcp")) + ap.add_argument("--device", default=os.environ.get("MC_DEVICE", "")) # rdma NIC 名, tcp 时留空 + ap.add_argument("--local-hostname", default=os.environ.get("HOST_IP", "")) + ap.add_argument("--sizes", default="64mb,256mb,1gb") + ap.add_argument("--concurrency", default="1,2,4,8") + ap.add_argument("--iters", type=int, default=20) + ap.add_argument("--local-buffer-gb", type=float, default=5.0) + ap.add_argument("--mode", default="simple", choices=["simple", "zcget", "zcput"], + help="simple=原有 put+bytes-get; zcget=零拷贝 batch_get_into 取回; " + "zcput=零拷贝 batch_put_from 写入 (均镜像 hicache 真实路径)") + ap.add_argument("--out", default="/root/transfer.csv") + args = ap.parse_args() + + if not args.local_hostname: + import socket + args.local_hostname = socket.gethostbyname(socket.gethostname()) + + from mooncake.store import MooncakeDistributedStore + store = MooncakeDistributedStore() + # global_segment_size=0: client 不贡献存储, 所有数据落到远端 kvstore 段 -> 真跨网传输 + ret = store.setup(args.local_hostname, args.metadata, 0, + int(args.local_buffer_gb * GB), args.protocol, + args.device, args.master) + if ret: + print(f"[FATAL] store.setup ret={ret}", file=sys.stderr); sys.exit(1) + print(f"[ok] setup protocol={args.protocol} device='{args.device}' " + f"local={args.local_hostname} master={args.master}", flush=True) + + sizes = parse_sizes(args.sizes) + concs = [int(x) for x in args.concurrency.split(",") if x.strip()] + rows = [] + run_tag = uuid.uuid4().hex[:8] + + for size in sizes: + payload = os.urandom(min(size, 64 * MB)) + # 大 value 用重复拼接避免 os.urandom 太慢, 但仍保持 size 字节 + if size > len(payload): + payload = (payload * (size // len(payload) + 1))[:size] + for conc in concs: + if args.mode == "zcget": + _bench_zc_get(store, size, conc, args.iters, payload, run_tag, rows, args.protocol) + elif args.mode == "zcput": + _bench_zc_put(store, size, conc, args.iters, payload, run_tag, rows, args.protocol) + else: + _bench_one(store, size, conc, args.iters, payload, run_tag, rows, args.protocol) + + _write_csv(args.out, rows) + print(f"[done] wrote {len(rows)} rows -> {args.out}", flush=True) + try: store.close() + except Exception: pass + + +def _bench_one(store, size, conc, iters, payload, run_tag, rows, protocol): + keys = [f"bench_{run_tag}_{human(size)}_{conc}_{i}" for i in range(iters)] + + def do_put(i): + t = time.perf_counter() + rc = store.put(keys[i], payload) + return (time.perf_counter() - t, rc) + + def do_get(i): + t = time.perf_counter() + v = store.get(keys[i]) + dt = time.perf_counter() - t + return (dt, 0 if (v is not None and len(v) == size) else 1) + + # ---- PUT ---- + put_lat, put_ok, put_wall = _run_parallel(do_put, iters, conc) + # ---- GET ---- + get_lat, get_ok, get_wall = _run_parallel(do_get, iters, conc) + + for op, lats, oks, wall in (("put", put_lat, put_ok, put_wall), + ("get", get_lat, get_ok, get_wall)): + # 有效聚合带宽 = 总字节 / 实测批次墙钟时间 (并发下反映真实吞吐) + total_bytes = size * len(lats) + ok = int(sum(oks) == 0) + bw = (total_bytes / wall / GB) if (wall > 0 and ok) else 0.0 + row = dict(transport=protocol, op=op, size_bytes=size, size=human(size), + concurrency=conc, iters=len(lats), total_bytes=total_bytes, + wall_s=round(wall, 4), bandwidth_gbps=round(bw, 3), + lat_p50_ms=round(pctl(lats, 50) * 1e3, 3), + lat_p95_ms=round(pctl(lats, 95) * 1e3, 3), + lat_p99_ms=round(pctl(lats, 99) * 1e3, 3), + lat_mean_ms=round(statistics.mean(lats) * 1e3, 3) if lats else 0, + success=ok) + rows.append(row) + print(f" {protocol:4} {op} {human(size):>6} c={conc:<2} " + f"BW={row['bandwidth_gbps']:.2f} GB/s p50={row['lat_p50_ms']:.1f}ms " + f"p99={row['lat_p99_ms']:.1f}ms ok={row['success']}", flush=True) + + # 清理 keys + for k in keys: + try: store.remove(k) + except Exception: pass + + +def _bench_zc_put(store, size, conc, iters, payload, run_tag, rows, protocol): + """零拷贝 PUT: 镜像 sglang hicache 的 register_buffer + batch_put_from 真实 write_through 路径。 + 对照 _bench_one 的 simple bytes put(store.put 传 python bytes), 这里一次 register_buffer 整块 + write 缓冲, 一批 batch_put_from(keys, ptrs, sizes) 从预注册张量直 DMA 出去, 无 python 中间对象。 + 并发 = 单批次内并行写 conc 个 buffer, BW = conc*size / 批次中位墙钟时间。 + ⚠️ 关键坑(见 doc/步骤3): TCP 下 put/batch_put_from 对**已存在的 key 是 no-op(按 key 去重, + 直接跳过传输)** -> 必须每 iter 用唯一 key(含 it) 做真插入, 否则 iter>=1 只花 ~3ms(假带宽)。""" + global torch + import torch as torch + # 整块 write 缓冲 conc*size 字节, 用 payload 填充(避免全零), 注册一次 + write = torch.frombuffer(bytearray(payload * conc), dtype=torch.uint8).clone().contiguous() + rc = store.register_buffer(write.data_ptr(), conc * size) + if rc: + print(f"[warn] register_buffer rc={rc} zcput size={human(size)} c={conc}", flush=True) + wptrs = [write.data_ptr() + i * size for i in range(conc)] + sizes = [size] * conc + put_t, ok = [], 1 + for it in range(iters): + keys = [f"zcput_{run_tag}_{human(size)}_{conc}_{it}_{i}" for i in range(conc)] + t = time.perf_counter() + pr = store.batch_put_from(keys, wptrs, sizes) + put_t.append(time.perf_counter() - t) + if any(r != 0 for r in pr): ok = 0 + for k in keys: # 逐轮清理(untimed), 释放 handle 并避免下轮去重 + try: store.remove(k) + except Exception: pass + med = statistics.median(put_t) if put_t else 0.0 + total_bytes = size * conc + bw = (total_bytes / med / GB) if (med > 0 and ok) else 0.0 + row = dict(transport=protocol, op="put_zc", size_bytes=size, size=human(size), + concurrency=conc, iters=len(put_t), total_bytes=total_bytes, + wall_s=round(med, 4), bandwidth_gbps=round(bw, 3), + lat_p50_ms=round(pctl(put_t, 50) * 1e3, 3), + lat_p95_ms=round(pctl(put_t, 95) * 1e3, 3), + lat_p99_ms=round(pctl(put_t, 99) * 1e3, 3), + lat_mean_ms=round(statistics.mean(put_t) * 1e3, 3) if put_t else 0, + success=ok) + rows.append(row) + print(f" {protocol:4} put_zc {human(size):>6} c={conc:<2} " + f"BW={row['bandwidth_gbps']:.2f} GB/s p50={row['lat_p50_ms']:.1f}ms " + f"p99={row['lat_p99_ms']:.1f}ms ok={row['success']}", flush=True) + store.unregister_buffer(write.data_ptr()) + try: store.remove_all() + except Exception: pass + + +def _bench_zc_get(store, size, conc, iters, payload, run_tag, rows, protocol): + """零拷贝 GET: 镜像 sglang hicache 的 register_buffer + batch_get_into 真实 retrieve 路径。 + 对照 _bench_one 的 simple bytes get(每 key 新建 python bytes + memcpy + GIL, 卡 ~1.9GB/s), + 这里一次 register_buffer 整块 read 缓冲, 逐 key 直 DMA 进预注册张量, 无 python 中间对象。 + 并发用「一次 batch_get_into 取 conc 个 key」表达(与 simple 的 ThreadPoolExecutor 并发对齐语义: + 同一批次内并行取 conc 个 buffer), BW = conc*size / 批次中位墙钟时间。""" + global torch + import torch as torch + keys = [f"zcget_{run_tag}_{human(size)}_{conc}_{i}" for i in range(conc)] + # 预置数据 (untimed setup): 每 key 存 size 字节 payload + for k in keys: + store.put(k, payload) + # 整块 read 缓冲 conc*size 字节, 注册一次 + read = torch.zeros(conc * size, dtype=torch.uint8).contiguous() + rc = store.register_buffer(read.data_ptr(), conc * size) + if rc: + print(f"[warn] register_buffer rc={rc} zcget size={human(size)} c={conc}", flush=True) + rptrs = [read.data_ptr() + i * size for i in range(conc)] + sizes = [size] * conc + get_t, ok = [], 1 + for _ in range(iters): + t = time.perf_counter() + gr = store.batch_get_into(keys, rptrs, sizes) + get_t.append(time.perf_counter() - t) + if any(r <= 0 for r in gr): ok = 0 + med = statistics.median(get_t) if get_t else 0.0 + total_bytes = size * conc + bw = (total_bytes / med / GB) if (med > 0 and ok) else 0.0 + row = dict(transport=protocol, op="get_zc", size_bytes=size, size=human(size), + concurrency=conc, iters=len(get_t), total_bytes=total_bytes, + wall_s=round(med, 4), bandwidth_gbps=round(bw, 3), + lat_p50_ms=round(pctl(get_t, 50) * 1e3, 3), + lat_p95_ms=round(pctl(get_t, 95) * 1e3, 3), + lat_p99_ms=round(pctl(get_t, 99) * 1e3, 3), + lat_mean_ms=round(statistics.mean(get_t) * 1e3, 3) if get_t else 0, + success=ok) + rows.append(row) + print(f" {protocol:4} get_zc {human(size):>6} c={conc:<2} " + f"BW={row['bandwidth_gbps']:.2f} GB/s p50={row['lat_p50_ms']:.1f}ms " + f"p99={row['lat_p99_ms']:.1f}ms ok={row['success']}", flush=True) + store.unregister_buffer(read.data_ptr()) + for k in keys: + try: store.remove(k) + except Exception: pass + + +def _run_parallel(fn, iters, conc): + lats, oks = [], [] + t0 = time.perf_counter() + if conc <= 1: + for i in range(iters): + dt, rc = fn(i); lats.append(dt); oks.append(rc) + else: + with ThreadPoolExecutor(max_workers=conc) as ex: + for dt, rc in ex.map(fn, range(iters)): + lats.append(dt); oks.append(rc) + wall = time.perf_counter() - t0 + return lats, oks, wall + + +def _write_csv(path, rows): + if not rows: return + cols = ["transport", "op", "size", "size_bytes", "concurrency", "iters", + "total_bytes", "wall_s", "bandwidth_gbps", "lat_mean_ms", + "lat_p50_ms", "lat_p95_ms", "lat_p99_ms", "success"] + with open(path, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore") + w.writeheader() + for r in rows: w.writerow(r) + + +if __name__ == "__main__": + main() diff --git a/src/code/issue5/configs/rdma.yaml b/src/code/issue5/configs/rdma.yaml new file mode 100644 index 0000000..f78f96c --- /dev/null +++ b/src/code/issue5/configs/rdma.yaml @@ -0,0 +1,47 @@ +# ============================================================================= +# Issue5 RDMA (RoCEv2) 传输配置 +# ----------------------------------------------------------------------------- +# Mooncake transfer engine 的 RDMA 协议档。相对 tcp.yaml 的**唯一实现改动**就是 +# protocol 切成 rdma + 指定 RDMA NIC(device_name), 其余端口/段大小不变。 +# store 端与 client 端 protocol 必须一致。被 scripts/run_benchmark.sh 读取。 +# +# device→netdev 映射两机不一致: +# RDMA 数据 NIC = eth1(198.51.100.x/27 子网) +# kvstore: eth1 = mlx5_2; prefill: eth1 = mlx5_3 +# ============================================================================= + +transport: rdma # Mooncake protocol = rdma (RoCEv2, verbs, 零拷贝直 DMA) + +# ---- store 端 (kvstore pod, MC_PROTOCOL=rdma bash start_kvstore.sh) ---- +store: + protocol: rdma + device_name: "" # store 侧自动发现 OK (Topology discovery: Found 6 HCAs) + auto_discovery: true # MC_MS_AUTO_DISC=1 + global_segment_size: 200gb + local_buffer_size: 0 + metadata_port: 18080 # 握手/元数据走已放通端口; RDMA 数据面另走 verbs + store_port: 18081 + master_rpc_port: 50051 + te_port: 15964 # transfer engine 端口 (跨机安全组白名单内) + +# ---- client 端 (prefill pod, TRANSPORT=rdma MC_DEVICE=mlx5_3 ...) ---- +client: + protocol: rdma + device_name: mlx5_3 # 必填: prefill 的 eth1(RDMA NIC)=mlx5_3; 空会 No available RNIC + local_buffer_size_gb: 6 + global_segment_size: 0 + +# ---- RDMA 硬件事实 (doc/步骤1-rdma.md 实测, 非假设) ---- +hardware: + link_layer: Ethernet # RoCEv2 (非 IB) + gid_index: 3 # RoCEv2 gid=3, MTU 4096 + hca_count: {kvstore: 6, prefill: 7} + rdma_subnet: {kvstore: "198.51.100.0/27", prefill: "198.51.100.32/27"} # 不同 /27, RoCEv2 可路由 + peermem: kunlun_peermem # 支持 GPUDirect RDMA (XPU 显存直通) + loopback_ib_write_bw_gbps: 18.3 # 146.73 Gb/s @ 65536B, gid3, 证明 verbs 全链路健康 + +# ---- 期望性能量级 (来自 L1/L2 实测) ---- +expect: + l1_put_gbps: "4.9 - 16.0" # 随并发 scale, transfer_rdma.csv (~13.9x TCP) + l1_get_gbps: "1.5 - 2.1" # simple-API get 卡在 client python bytes 拷贝(非网络) + l2_zerocopy_get_gbps: "20.68" # ★ kv_tensor_rdma.csv zerocopy get 中位, ~18.8x TCP-zc diff --git a/src/code/issue5/configs/tcp.yaml b/src/code/issue5/configs/tcp.yaml new file mode 100644 index 0000000..5b3bca0 --- /dev/null +++ b/src/code/issue5/configs/tcp.yaml @@ -0,0 +1,32 @@ +# ============================================================================= +# Issue5 TCP 传输配置 (基线) +# ----------------------------------------------------------------------------- +# Mooncake transfer engine 的 TCP 协议档。store 端与 client 端的 protocol 必须一致。 +# 被 scripts/run_benchmark.sh 读取, 落成 store 的 --config JSON 与 client 的 +# TRANSPORT/MC_DEVICE 环境变量。字节级值(如 segment)由脚本换算。 +# ============================================================================= + +transport: tcp # Mooncake protocol = tcp (走内核 socket, 无 verbs) + +# ---- store 端 (kvstore pod, MC_PROTOCOL=tcp bash start_kvstore.sh) ---- +store: + protocol: tcp + device_name: "" # TCP 不用 RDMA NIC, 留空 + global_segment_size: 200gb # 贡献给全局 KV 池的本机内存 + local_buffer_size: 0 # 纯存储节点不发起 get/put + metadata_port: 18080 # http metadata (8080 常被同宿主机其它 hostNetwork pod 占) + store_port: 18081 + master_rpc_port: 50051 + +# ---- client 端 (prefill pod 内跑 benchmark, 跨机连 store) ---- +client: + protocol: tcp + device_name: "" # TCP client 无需指定网卡 + local_buffer_size_gb: 6 # bytes 路径需 >= 单次 kv_bytes (128k=4GB) + global_segment_size: 0 # client 不贡献内存 + +# ---- 期望性能量级 (来自 L1/L2 实测, 仅作 sanity, 非硬编码) ---- +expect: + l1_put_gbps: "0.77 - 2.88" # 随并发, transfer_tcp.csv + l1_get_gbps: "0.52 - 0.94" # 卡在内核 socket 拷贝 + l2_zerocopy_get_gbps: "1.10" # kv_tensor_tcp.csv zerocopy get 中位 diff --git a/src/code/issue5/kvstore_mooncake.yaml b/src/code/issue5/kvstore_mooncake.yaml new file mode 100644 index 0000000..556460e --- /dev/null +++ b/src/code/issue5/kvstore_mooncake.yaml @@ -0,0 +1,171 @@ +apiVersion: example.com/v1 +kind: FedDeployment +metadata: + labels: + example.com/managed-by: deploytask + example.com/feddeploy-name: issue5-kvstore-by-fed-deploy + example.com/inference-service-name: issue5-kvstore-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + name: issue5-kvstore-mooncake + namespace: default +spec: + minReadySeconds: 30 + replicas: 1 + selector: + matchLabels: + example.com/feddeploy-name: issue5-kvstore-by-fed-deploy + example.com/inference-service-name: issue5-kvstore-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + strategy: + canary: + maxSurge: 100% + maxUnavailable: 100% + steps: + - pause: {} + setWeight: 100 + template: + metadata: + annotations: + example.com/update-by-job: "0" + + labels: + example.com/feddeploy-name: issue5-kvstore-by-fed-deploy + example.com/inference-service-name: issue5-kvstore-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + spec: + replicas: 1 + template: + metadata: + labels: + example.com/feddeploy-name: issue5-kvstore-by-fed-deploy + example.com/inference-service-name: issue5-kvstore-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + annotations: + scheduling.volcano.sh/queue-name: + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: NotIn + values: + - 192.0.2.101 + - 192.0.2.102 + - 192.0.2.103 + - 192.0.2.104 + - 192.0.2.105 + - 192.0.2.106 + - 192.0.2.107 + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: rdma-test + operator: In + values: + - "true" + topologyKey: "kubernetes.io/hostname" + + containers: + # 容器只拉起并 idle, 不在启动时跑存储服务。 + # 实际启动逻辑放在 scripts/start_kvstore.sh, 进 pod 后手动执行: + # kubectl exec -it -- bash + # MC_PROTOCOL=rdma MC_SEGMENT_SIZE=200gb bash /mnt/cluster/issue5/start_kvstore.sh + - args: + - -c + - | + . ~/.bashrc + echo "[issue5-kvstore] Pod ready. Exec in and run scripts/start_kvstore.sh manually." + tail -f /dev/null + command: + - /bin/sh + # 容器只 idle, 存储服务(mooncake_master + store_service)由 + # scripts/start_kvstore.sh 手动执行, 该脚本 export 自己的 MC_* / PATH / HOST_IP, + # 且本节点不跑模型, 故此处删除全部 sglang/MoE/kunlun 调优变量, + # 仅保留 downward-API pod 元信息, 方便 exec 进 pod 调试。 + env: + - name: HOST_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: inference-sglang: + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - | + pkill -9 -f 'mooncake' || true + name: model-server + ports: + - containerPort: 6677 + protocol: TCP + - containerPort: 8000 + protocol: TCP + resources: + limits: + kunlunxin.com/xpu: "8" + rdma/hca: "1" + requests: + kunlunxin.com/xpu: "8" + rdma/hca: "1" + securityContext: + capabilities: + add: + - SYS_PTRACE + - SYS_ADMIN + runAsUser: 0 + volumeMounts: + - mountPath: /dev/shm + name: shm-cache + - mountPath: /mnt/cluster + name: pvc-model + dnsPolicy: ClusterFirst + hostNetwork: true + nodeSelector: + example.com/dedicated-pool: + restartPolicy: Always + schedulerName: volcano + securityContext: {} + shareProcessNamespace: true + terminationGracePeriodSeconds: 600 + volumes: + - emptyDir: + medium: Memory + sizeLimit: 32Gi + name: local-cache + - emptyDir: + medium: Memory + sizeLimit: 800Gi + name: shm-cache + - emptyDir: {} + name: shared-volume + - name: pvc-model + persistentVolumeClaim: + claimName: model-pvc + # topologyKey: example.com/dcn-tor + # topologyKeyExclusive: true \ No newline at end of file diff --git a/src/code/issue5/model/bandwidth_model.py b/src/code/issue5/model/bandwidth_model.py new file mode 100644 index 0000000..87b288a --- /dev/null +++ b/src/code/issue5/model/bandwidth_model.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Issue5 带宽临界值模型 B_crit + 用 L1/L2/L3 实测标定 + +闭式解 (PLAN 附录 A): + T_full(N) = T_prefill(N) + T_cache(N,h,B) = T_0 + S_KV(hN)/B + T_restore(hN) + T_prefill((1-h)N) + 令 T_cache < T_full 解出临界带宽: + B_crit(N,h) = S_KV(hN) / ( T_prefill(N) - T_prefill((1-h)N) - T_lookup - T_restore - L_net ) + 划算条件: 实际 retrieve 带宽 B_retrieve > B_crit <=> 以存代算比全量重算快。 + +本脚本做四件事: + 1) 用 L3 prefill_local.csv 拟合算力侧 T_prefill(N)=a*N+b*N^2 (prefill 前向墙钟 ms, 过原点)。 + b 项吸收 attention 的超线性; NSA 稀疏(topk=2048)会压平, 故 128k 外推给区间上界(见 doc)。 + 2) 每 (N,h) 用实测 recompute/cold_uniq 直接算 B_crit(本地命中 restore/lookup≈0), + 再用拟合模型算一遍 -> 标定误差 (model vs measured)。 + 3) 用 L1/L2 的 B_retrieve(TCP/RDMA zerocopy) 算 T_restore、净收益、是否划算。 + 4) 外推线上 128K/30% 命中: 预测 B_crit, 对比线上 TCP~2.5GB/s / 实测 TCP / RDMA。 + 出 results/model_fit.csv + 拟合图 results/figures/。 + +用法: + python3 model/bandwidth_model.py \ + --calib results/raw/prefill_local.csv \ + --transfer-tcp results/raw/kv_tensor_tcp.csv \ + --transfer-rdma results/raw/kv_tensor_rdma.csv \ + --report results/model_fit.csv --figdir results/figures +""" +import argparse, csv, os, sys, statistics + +GB = 1 << 30 +KiB = 1 << 10 +# V3.2 MLA 单 latent: 每 token KV = L*(kv_lora_rank+qk_rope)*b (与 L2/L3 脚本一致) +S_TOKEN_V32 = 61 * (512 + 64) * 2 # = 70272 B = 68.6 KiB/token +PAGE = 64 + + +def s_kv_bytes(n_tok): + return n_tok * S_TOKEN_V32 + + +def align(n, page=PAGE): + return max(0, (n // page) * page) + + +def load_prefill(path): + """读 L3 CSV -> list[dict(N,h,cached,recompute_ms,cold_uniq_ms,total_ms,saved_ms)]。""" + rows = [] + with open(path) as f: + for r in csv.DictReader(f): + rows.append(dict( + N=int(r["context_length"]), h=float(r["hit_ratio"]), + cached=int(r["cached_tokens"]), + recompute_ms=float(r["recompute_ms"]), + cold_uniq_ms=float(r["cold_uniq_ms"]), + total_ms=float(r["total_ms"]), + saved_ms=float(r["saved_ms"]))) + return rows + + +def fit_tprefill(rows): + """最小二乘拟合 T_prefill(N)=a*N+b*N^2 (过原点)。每个 N 用 h=0 的 recompute 均值做标定点。 + 返回 (a[ms/tok], b[ms/tok^2], r2, points)。""" + import numpy as np + # 每个 N 的全量重算基线 = 该 N 所有行的 recompute_ms 中位数 (h 无关, 都是全量现算) + byN = {} + for r in rows: + byN.setdefault(r["N"], []).append(r["recompute_ms"]) + pts = sorted((N, statistics.median(v)) for N, v in byN.items()) + Ns = np.array([p[0] for p in pts], dtype=float) + Ts = np.array([p[1] for p in pts], dtype=float) + # 设计矩阵 [N, N^2], 无截距 (N=0 -> 0 prefill) + A = np.vstack([Ns, Ns ** 2]).T + coef, *_ = np.linalg.lstsq(A, Ts, rcond=None) + a, b = float(coef[0]), float(coef[1]) + pred = A @ coef + ss_res = float(((Ts - pred) ** 2).sum()) + ss_tot = float(((Ts - Ts.mean()) ** 2).sum()) + r2 = 1 - ss_res / ss_tot if ss_tot > 0 else 1.0 + return a, b, r2, pts + + +def t_prefill(N, a, b): + return a * N + b * N * N + + +def load_retrieve_bw(path, want_path="zerocopy", op="get"): + """从 L2 kv_tensor CSV 取 retrieve 有效带宽 (GB/s) 的中位数。""" + bws = [] + with open(path) as f: + for r in csv.DictReader(f): + if r.get("path") == want_path and r.get("op") == op and r.get("success") == "1": + try: + bws.append(float(r["effective_bandwidth_gbps"])) + except (ValueError, KeyError): + pass + return statistics.median(bws) if bws else None + + +def b_crit_gbps(s_kv_B, denom_ms): + """B_crit = S_KV / 分母(转秒). 分母<=0 => 不可能划算 (返回 inf)。""" + if denom_ms <= 0: + return float("inf") + return (s_kv_B / GB) / (denom_ms / 1e3) + + +def compute_cells(rows, a, b, retrieves): + """对每 (N,h) 生成: 实测 B_crit + 模型 B_crit + 每种传输的 T_restore/净收益/是否划算。""" + out = [] + for r in rows: + N, h, cached = r["N"], r["h"], r["cached"] + if cached <= 0: + continue # h=0 无前缀可复用, B_crit 无意义 + s_kv = s_kv_bytes(cached) + # 实测分母 = 前缀现算时间 = 全量重算 - 冷唯一后缀 (本地命中 lookup/restore≈0) + denom_meas = r["recompute_ms"] - r["cold_uniq_ms"] + bcrit_meas = b_crit_gbps(s_kv, denom_meas) + # 模型分母 = T_prefill(N) - T_prefill((1-h)N) + uniq = N - align(int(round(h * N))) + denom_model = t_prefill(N, a, b) - t_prefill(uniq, a, b) + bcrit_model = b_crit_gbps(s_kv, denom_model) + base = dict(N=N, h=h, cached=cached, s_kv_gb=round(s_kv / GB, 4), + tprefill_N_ms=round(t_prefill(N, a, b), 1), + denom_meas_ms=round(denom_meas, 1), + denom_model_ms=round(denom_model, 1), + bcrit_meas_gbps=round(bcrit_meas, 4), + bcrit_model_gbps=round(bcrit_model, 4), + measured_saved_ms=r["saved_ms"]) + for tname, bw in retrieves.items(): + t_restore = (s_kv / GB) / bw * 1e3 # ms + net = denom_meas - t_restore # 净省下的时间 (算侧收益 - restore 成本) + row = dict(base, transport=tname, b_retrieve_gbps=round(bw, 3), + t_restore_ms=round(t_restore, 1), + net_saved_ms=round(net, 1), + profitable=int(bw > bcrit_meas)) + out.append(row) + return out + + +def extrapolate(a, b, retrieves, N=131072, h=0.30, online_tcp=2.5): + """外推线上场景 (默认 128K / 30% 命中)。返回 dict。""" + hN = align(int(round(h * N))) + uniq = N - hN + s_kv = s_kv_bytes(hN) + denom = t_prefill(N, a, b) - t_prefill(uniq, a, b) + bcrit = b_crit_gbps(s_kv, denom) + res = dict(N=N, h=h, hit_tokens=hN, s_kv_gb=round(s_kv / GB, 3), + tprefill_N_ms=round(t_prefill(N, a, b), 0), + tprefill_uniq_ms=round(t_prefill(uniq, a, b), 0), + denom_ms=round(denom, 0), bcrit_gbps=round(bcrit, 4)) + bws = dict(retrieves); bws["online_tcp_hypo"] = online_tcp + for tname, bw in bws.items(): + res[f"Trestore_{tname}_ms"] = round((s_kv / GB) / bw * 1e3, 0) + res[f"margin_{tname}_x"] = round(bw / bcrit, 1) if bcrit > 0 else float("inf") + return res + + +def write_csv(path, cells): + cols = ["N", "h", "cached", "s_kv_gb", "tprefill_N_ms", "denom_meas_ms", + "denom_model_ms", "bcrit_meas_gbps", "bcrit_model_gbps", "transport", + "b_retrieve_gbps", "t_restore_ms", "net_saved_ms", "profitable", + "measured_saved_ms"] + with open(path, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=cols, extrasaction="ignore") + w.writeheader() + for c in cells: + w.writerow(c) + + +def make_plots(figdir, rows, cells, a, b, retrieves, pts): + import numpy as np + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + os.makedirs(figdir, exist_ok=True) + + # 图1: T_prefill 拟合 vs 实测 + fig, ax = plt.subplots(figsize=(6, 4)) + Ns = np.array([p[0] for p in pts]); Ts = np.array([p[1] for p in pts]) + xx = np.linspace(0, 131072, 200) + ax.scatter(Ns / 1024, Ts / 1e3, c="C3", zorder=3, label="measured recompute") + ax.plot(xx / 1024, t_prefill(xx, a, b) / 1e3, "C0-", + label=f"fit a·N+b·N²\na={a:.4f}ms/tok\nb={b:.2e}ms/tok²") + ax.set_xlabel("context N (K tokens)"); ax.set_ylabel("T_prefill (s)") + ax.set_title("V3.2 prefill compute cost (fit)"); ax.legend(fontsize=8); ax.grid(alpha=.3) + fig.tight_layout(); fig.savefig(f"{figdir}/tprefill_fit.png", dpi=120); plt.close(fig) + + # 图2: B_crit vs h (各 N), 叠加传输带宽水平线 + fig, ax = plt.subplots(figsize=(6, 4)) + byN = {} + for c in cells: + if c["transport"] == list(retrieves)[0]: + byN.setdefault(c["N"], []).append((c["h"], c["bcrit_meas_gbps"])) + for N, hv in sorted(byN.items()): + hv.sort(); ax.plot([x[0] for x in hv], [x[1] for x in hv], + "o-", label=f"N={N//1024}k") + for tname, bw in retrieves.items(): + ax.axhline(bw, ls="--", alpha=.6, label=f"{tname} B_ret={bw:.1f}") + ax.set_yscale("log"); ax.set_xlabel("hit ratio h"); ax.set_ylabel("B_crit (GB/s, log)") + ax.set_title("B_crit vs hit ratio (all << transport BW)") + ax.legend(fontsize=7, ncol=2); ax.grid(alpha=.3, which="both") + fig.tight_layout(); fig.savefig(f"{figdir}/bcrit_vs_hit.png", dpi=120); plt.close(fig) + + # 图3: T_restore TCP vs RDMA (各 N,h=1.0) + fig, ax = plt.subplots(figsize=(6, 4)) + hit1 = [c for c in cells if c["h"] == 1.0] + Nl = sorted({c["N"] for c in hit1}) + xs = range(len(Nl)); w = 0.35 + for i, tname in enumerate(retrieves): + ys = [next(c["t_restore_ms"] for c in hit1 if c["N"] == N and c["transport"] == tname) + for N in Nl] + ax.bar([x + (i - .5) * w for x in xs], ys, w, label=tname) + ax.set_xticks(list(xs)); ax.set_xticklabels([f"{N//1024}k" for N in Nl]) + ax.set_xlabel("context N"); ax.set_ylabel("T_restore (ms), h=1.0") + ax.set_title("Restore latency: TCP vs RDMA"); ax.legend(); ax.grid(alpha=.3, axis="y") + fig.tight_layout(); fig.savefig(f"{figdir}/trestore_tcp_vs_rdma.png", dpi=120); plt.close(fig) + return ["tprefill_fit.png", "bcrit_vs_hit.png", "trestore_tcp_vs_rdma.png"] + + +def selftest(a, b, retrieves): + """闭式解一致性自检: T_cache < T_full <=> B_retrieve > B_crit。""" + N, h = 16384, 0.5 + hN = align(int(round(h * N))); uniq = N - hN + s_kv = s_kv_bytes(hN) + denom = t_prefill(N, a, b) - t_prefill(uniq, a, b) + bcrit = b_crit_gbps(s_kv, denom) + for bw in (bcrit * 0.5, bcrit * 2.0): # 一半 / 两倍临界带宽 + t_restore = (s_kv / GB) / bw * 1e3 + t_cache = t_prefill(uniq, a, b) + t_restore # T_0=lookup=Lnet=0 + t_full = t_prefill(N, a, b) + profitable = t_cache < t_full + assert profitable == (bw > bcrit), \ + f"selftest FAIL bw={bw:.3f} bcrit={bcrit:.3f} cache={t_cache:.0f} full={t_full:.0f}" + print("[selftest] closed-form B_crit <-> T_cache {args.report}", flush=True) + + # 标定误差: 模型 B_crit vs 实测 B_crit + errs = [abs(c["bcrit_model_gbps"] - c["bcrit_meas_gbps"]) / c["bcrit_meas_gbps"] + for c in cells if c["transport"] == list(retrieves)[0] and c["bcrit_meas_gbps"] > 0] + if errs: + print(f"[calib] 模型 vs 实测 B_crit 相对误差: " + f"median={statistics.median(errs)*100:.1f}% max={max(errs)*100:.1f}%", flush=True) + + print("\n[B_crit 实测范围] " + f"{min(c['bcrit_meas_gbps'] for c in cells):.3f} – " + f"{max(c['bcrit_meas_gbps'] for c in cells):.3f} GB/s", flush=True) + + ex = extrapolate(a, b, retrieves, N=131072, h=0.30, online_tcp=args.online_tcp) + print("\n[外推 128K / 30% 命中] (验收① 验证点)") + print(f" hit_tokens={ex['hit_tokens']} S_KV={ex['s_kv_gb']}GB " + f"T_prefill(128k)≈{ex['tprefill_N_ms']/1e3:.1f}s 分母≈{ex['denom_ms']/1e3:.1f}s") + print(f" => 预测 B_crit = {ex['bcrit_gbps']:.4f} GB/s") + for tname in list(retrieves) + ["online_tcp_hypo"]: + print(f" {tname:16} margin={ex[f'margin_{tname}_x']}× " + f"T_restore={ex[f'Trestore_{tname}_ms']/1e3:.2f}s") + + selftest(a, b, retrieves) + + if not args.no_plot: + try: + figs = make_plots(args.figdir, rows, cells, a, b, retrieves, pts) + print(f"[fig] wrote {figs} -> {args.figdir}", flush=True) + except Exception as e: + print(f"[fig] skip plot: {e}", flush=True) + + +if __name__ == "__main__": + main() diff --git a/src/code/issue5/prefill_hicache.yaml b/src/code/issue5/prefill_hicache.yaml new file mode 100644 index 0000000..f7e9a28 --- /dev/null +++ b/src/code/issue5/prefill_hicache.yaml @@ -0,0 +1,174 @@ +apiVersion: example.com/v1 +kind: FedDeployment +metadata: + labels: + example.com/managed-by: deploytask + example.com/feddeploy-name: issue5-prefill-by-fed-deploy + example.com/inference-service-name: issue5-prefill-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + name: issue5-prefill-hicache + namespace: default +spec: + minReadySeconds: 30 + replicas: 1 + selector: + matchLabels: + example.com/feddeploy-name: issue5-prefill-by-fed-deploy + example.com/inference-service-name: issue5-prefill-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + strategy: + canary: + maxSurge: 100% + maxUnavailable: 100% + steps: + - pause: {} + setWeight: 100 + template: + metadata: + annotations: + example.com/update-by-job: "0" + + labels: + example.com/feddeploy-name: issue5-prefill-by-fed-deploy + example.com/inference-service-name: issue5-prefill-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + spec: + replicas: 1 + template: + metadata: + labels: + example.com/feddeploy-name: issue5-prefill-by-fed-deploy + example.com/inference-service-name: issue5-prefill-by-fed-deploy + example.com/model-name: issue5-model + example.com/platform: TEST + annotations: + scheduling.volcano.sh/queue-name: + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/hostname + operator: NotIn + values: + - 192.0.2.101 + - 192.0.2.102 + - 192.0.2.103 + - 192.0.2.104 + - 192.0.2.105 + - 192.0.2.106 + podAntiAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchExpressions: + - key: rdma-test + operator: In + values: + - "true" + topologyKey: "kubernetes.io/hostname" + + containers: + # 容器只拉起并 idle, 不在启动时跑 server。 + # 因为里面有需要运行时才能确定的参数(存储节点宿主机 IP 等), + # 实际启动逻辑放在 scripts/start_prefill.sh, 进 pod 后手动执行: + # kubectl exec -it -- bash + # # 把 scripts/start_prefill.sh 拷进 /mnt/cluster 或直接粘贴执行 + # MOONCAKE_STORAGE_NODE_IP=<存储节点IP> MC_PROTOCOL=rdma bash /mnt/cluster/issue5/start_prefill.sh + - args: + - -c + - | + . ~/.bashrc + echo "[issue5-prefill] Pod ready. Exec in and run scripts/start_prefill.sh manually." + tail -f /dev/null + command: + - /bin/sh + # 容器只 idle, 模型实际启动由 scripts/start_prefill_v32.sh 手动执行, + # 该脚本会 export 自己需要的全部 env(甚至覆盖 pod 值), 故此处不再重复注入 + # sglang/MoE/kunlun 调优变量, 仅保留 downward-API pod 元信息, 方便 exec 进 pod 调试。 + env: + - name: HOST_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.hostIP + - name: POD_IP + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + image: inference-sglang: + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - | + mkdir -p /run/sglang && \ + echo "stop" > /run/sglang/stop && \ + sleep 120 && \ + pkill -9 -f '(sgl|python3)' + name: model-server + ports: + - containerPort: 6677 + protocol: TCP + - containerPort: 8000 + protocol: TCP + resources: + limits: + kunlunxin.com/xpu: "8" + rdma/hca: "1" + requests: + kunlunxin.com/xpu: "8" + rdma/hca: "1" + securityContext: + capabilities: + add: + - SYS_PTRACE + - SYS_ADMIN + runAsUser: 0 + volumeMounts: + - mountPath: /dev/shm + name: shm-cache + - mountPath: /mnt/cluster + name: pvc-model + dnsPolicy: ClusterFirst + hostNetwork: true + nodeSelector: + example.com/dedicated-pool: + restartPolicy: Always + schedulerName: volcano + securityContext: {} + shareProcessNamespace: true + terminationGracePeriodSeconds: 600 + volumes: + - emptyDir: + medium: Memory + sizeLimit: 32Gi + name: local-cache + - emptyDir: + medium: Memory + sizeLimit: 800Gi + name: shm-cache + - emptyDir: {} + name: shared-volume + - name: pvc-model + persistentVolumeClaim: + claimName: model-pvc + # topologyKey: example.com/dcn-tor + # topologyKeyExclusive: true \ No newline at end of file diff --git a/src/code/issue5/report/figures/bw_vs_concurrency.png b/src/code/issue5/report/figures/bw_vs_concurrency.png new file mode 100644 index 0000000000000000000000000000000000000000..a99d0f0792783db6190eac300c97c9fcf27c2a4a GIT binary patch literal 30729 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 D?5d*n literal 0 HcmV?d00001 diff --git a/src/code/issue5/report/figures/bw_vs_size.png b/src/code/issue5/report/figures/bw_vs_size.png new file mode 100644 index 0000000000000000000000000000000000000000..6ab2b14d8b0f604c1e0701da0133ae797064b0ce GIT binary patch literal 49937 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 literal 0 HcmV?d00001 diff --git a/src/code/issue5/report/figures/l2_zerocopy_bw.png b/src/code/issue5/report/figures/l2_zerocopy_bw.png new file mode 100644 index 0000000000000000000000000000000000000000..71eecc763eece2c42f36d8e66c5f0135a5cd44ce GIT binary patch literal 32169 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 literal 0 HcmV?d00001 diff --git a/src/code/issue5/report/figures/qpm_compare.png b/src/code/issue5/report/figures/qpm_compare.png new file mode 100644 index 0000000000000000000000000000000000000000..fc6f01e99631b57744be0ae2243c58a4b04fa140 GIT binary patch literal 27342 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 literal 0 HcmV?d00001 diff --git a/src/code/issue5/report/report.md b/src/code/issue5/report/report.md new file mode 100644 index 0000000..e3ad10d --- /dev/null +++ b/src/code/issue5/report/report.md @@ -0,0 +1,537 @@ +# issue5 report + +1. 带宽临界值模型 + +输入:命中率,上下文长度,KV Cache 量,算力 + +输出:以存代算的临界带宽 + +2. 相同 TTFT 约束下 + +TCP 条件下, 带宽,延迟,可承载并发query数,QPM + +RDMA 条件下,带宽,延迟,可承载并发query数,QPM + +## 基本环境与性能 + +### 硬件环境与拓扑 + +硬件设备 + +| 项 | kvstore 192.0.2.10 | prefill 192.0.2.20 | +| :-------------- | :------------------------------------- | :---------------------- | +| HCA | mlx5_0..5(6 个) | mlx5_0..6(7 个) | +| link_layer | Ethernet(RoCEv2) | Ethernet(RoCEv2) | +| 端口状态 | eth0/eth1-4 PORT_ACTIVE | eth0/eth1-4 PORT_ACTIVE | +| /dev/infiniband | uverbs0-5 + rdma_cm | uverbs0-6 + rdma_cm | +| kmods | mlx5_ib/ib_core/rdma_cm/kunlun_peermem | 同 | +| ulimit -l | unlimited | unlimited | +| perftest | ib_write_bw/ib_read_bw/ib_send_lat | 同 | + +硬件拓扑 + +| netdev | kvstore | prefill | +| :------------------------------ | :------------- | :-------------------- | +| eth0(前端 192.0.2.x,管理/OOB) | mlx5_0 | mlx5_0 | +| eth1(RDMA,198.51.100.x/27) | mlx5_2 | mlx5_3 | +| eth2 | mlx5_3 | mlx5_4 | +| eth3 | mlx5_4 | mlx5_5 | +| eth4 | mlx5_5 | mlx5_6 | +| eth5/eth6 | mlx5_1(DOWN) | mlx5_1/mlx5_2(DOWN) | + +- conda env `python310_torch29_cuda`,预装 mooncake **0.3.5**(勿 pip 装,见 MEMORY)。 +- store 端口:master RPC `50051`、http metadata `18080`、store `18081`。 +- client(prefill)用 `global_segment_size=0` 不贡献内存 → put/get 全部真跨机落到远端 store 段。 + +- RDMA 数据 NIC = **eth1-eth4**(与 `start_prefill*.sh` 的 `BKCL_RDMA_NICS="eth1,eth1,eth2,eth2,eth3,eth3,eth4,eth4"` 一致)。 +- 同名 netdev 在两机对应的 mlx5 序号不同 → 配置里若按 `mlx5_N` 指定要分别写;按 `ethN` 指定更稳。 +- RDMA NIC 子网:kvstore `198.51.100.0/27`,prefill `198.51.100.32/27`(不同 /27,RoCEv2 可路由)。 + +### 基本性能 + +#### 设备性能 + +RDMA 本机数据路径已实测 146 Gb/s; + +跨机 RDMA 需走 Mooncake 自带传输引擎(握手端口已放通、已发现 6 HCA),独立 perftest 的跨机 OOB 被安全组挡住, 不影响项目,真正的跨机 RDMA 数据面测试在 L1 用 `MC_PROTOCOL=rdma` 完成 + +65536B -> BW average 146.73 Gb/sec (~18.3 GB/s) + +#### Mooncake put/get primitive microbench + +这里使用 零拷贝 `batch_get_into`,镜像 hicache 真实 retrieve 路径,一次 `register_buffer(整块 read 缓冲)`, 一批 `batch_get_into(keys, ptrs, sizes)` 直 DMA 进预注册张量(无 python bytes 中间对象),否则 RDMA get 平台化在 ~1.5–2.1 GB/s,远低于同协议 put 的 16 GB/s,瓶颈卡在 client 侧 `bytes` 分配 + memcpy + GIL + +PUT(写入远端 store,对应 write_through KV 落盘) + +| size | conc | TCP-zc | RDMA-zc | RDMA-zc / TCP-zc | +|---|---|---|---|---| +| 64MB | 1 | 0.58 | **19.65** | 33.9× | +| 64MB | 2 | 0.62 | **20.80** | 33.5× | +| 64MB | 4 | 0.66 | **22.07** | 33.4× | +| 64MB | 8 | 0.73 | **21.86** | 29.9× | +| 256MB | 1 | 0.65 | **18.38** | 28.3× | +| 256MB | 2 | 0.77 | **20.15** | 26.2× | +| 256MB | 4 | 1.22 | **20.72** | 17.0× | +| 256MB | 8 | 1.17 | **21.14** | 18.1× | +| 1GB | 1 | 1.21 | **18.07** | 14.9× | +| 1GB | 2 | 1.23 | **19.77** | 16.1× | +| 1GB | 4 | 1.06 | **21.09** | 19.9× | + +GET(读回远端 store,对应命中时的 KV retrieve) + +| size | conc | TCP-zc | RDMA-zc | RDMA-zc / TCP-zc | +| :---- | :--- | :----- | :-------- | :--------------- | +| 64MB | 1 | 1.20 | **10.20** | 8.5× | +| 64MB | 2 | 1.22 | **8.93** | 7.3× | +| 64MB | 4 | 1.23 | **10.48** | 8.5× | +| 64MB | 8 | 1.21 | **13.39** | 11.1× | +| 256MB | 1 | 1.19 | **10.43** | 8.8× | +| 256MB | 2 | 0.93 | **12.78** | 13.7× | +| 256MB | 4 | 1.44 | **13.20** | 9.2× | +| 256MB | 8 | 1.49 | **12.03** | 8.1× | +| 1GB | 1 | 1.31 | **13.92** | 10.6× | +| 1GB | 2 | 1.44 | **14.47** | 10.0× | +| 1GB | 4 | 1.58 | **14.61** | 9.2× | +| 1GB | 8 | 1.23 | **14.69** | 11.9× | + + + +## 验收1 + +临界值带宽模型 + +输入:命中率,上下文长度,KV Cache 量,算力 + +输出:以存代算的临界带宽 + +### 模型推导 + +全量重算路径 +$$ +T_{\text{full}}(N)=T_{\text{prefill}}(N) +$$ + +- $N$:完整输入的 token 数; +- $T_{\text{prefill}}(N)$:从头计算这 $N$ 个 token 的 prefill 时间。 + +以存代算路径 +$$ +T_{\text{cache}}(N,h,B) += +T_0 ++ +T_{\text{retrieve}} ++ +T_{\text{restore}}(hN) ++ +T_{\text{prefill}}((1-h)N) +$$ + +- $h$:前缀命中率。 + +**固定开销 $T_0$** :$T_0=T_{\text{lookup}}+L_{\text{net}}$,如查询 metadata,判断前缀是否命中,发起 RPC,建立或调度传输请求,网络往返固定延迟等 + +**传输时间 $T_{\text{retrieve}}$**:$T_{\text{retrieve}}=\frac{S_{\text{KV}}(hN)}{B}$ + +- $S_{\text{KV}}(hN)$:命中前缀对应的 KV 字节数; +- $B$:实际 retrieve 有效带宽。 + +**恢复开销 $T_{\text{restore}}$**:数据通过网络到达后,不一定能立刻被模型使用,可能还需要 反序列化,从临时缓冲区复制到 KV pool,恢复 page table 或 slot 映射,layout 转换 等,因此把 $T_{\text{retrieve}}$ 和 $T_{\text{restore}}(hN)$ 区分开 + +**后缀现算$T_{\text{prefill}}$**: $T_{\text{prefill}}((1-h)N)$ 命中的前缀不再计算,但未命中的后缀仍要正常跑模型 + + + +对于值得使用远端 KV 的情况,令 +$$ +T_{\text{cache}}B_{\text{crit}}$ 则以存代算更快 +- $B=B_{\text{crit}}$ 刚好打平 +- $B0.85 MoE OOM / <0.83 权重 OOM,窗口极窄),每卡只剩 ~15GB 给 KV ⇒ 全局 KV 上限 **29120 token**,32k / 128k 单请求超 KV 容量:实测发一个 32768-token prefill 直接把 scheduler 挂死(detokenizer 20s 无响应、health check 连续失败、进程 wedge,需 kill 重起)。故 sweep 的上下文改为 **8k / 16k / 24k**(均 < 29120,24k 留 ~4.5k 余量)。因此本实验只能稳定实测 8k、16k 和 24k 上下文; + +32k 与 128k 这些长上下文的 KV 应从**远端 store retrieve** 回来(用 L2 的 `B_retrieve` 代入 §6 的 `B_crit` 外推),但远端 KV 存储本身只避免前缀重算,并不会自动突破本地活跃 KV 容量上限;真正支持超长上下文还需分层按需加载、KV 分片或更大本地 KV 池。 + + + +命中率 h 用「共享前缀 + 唯一后缀」控制:`prefix = align(round(h*N), page=64)`,`suffix = N-prefix`。 + +- **recompute 基线 `T_prefill(N)`**:每 iter `flush` + 唯一内容 → 真全量现算,取 median。 +- **命中 total(N, h)**:先发 `prefix` 预热进本地 radix(**不 flush**),再发 `prefix+唯一后缀` + → `cached_tokens=prefix`,前缀 KV device 本地复用(restore≈0),只现算后缀。 +- **`saved = T_prefill(N) − total(N,h)`** = 复用 hN 前缀省下的**计算**时间。 +- **JIT 毛刺**:`--disable-cuda-graph` 下每个 shape 首次请求有编译开销(首个 8k ~9.6s, + 稳态 ~3.7s),故每 shape 先 `warmup` 丢弃再取 median。 + + + +DeepSeek V3.2 MLA, S_token=68.6 KiB/token, TP8, page=64, 本地 radix + +`total`/`recompute`/`saved` 单位 ms(median, iters=3) + +| N | h | prefix | cached | recompute_ms | total_ms | **saved_ms** | +|---|---|---|---|---|---|---| +| 8k | 0.0 | 0 | 0 | 3680 | 3661 | 19 | +| 8k | 0.25 | 2048 | 2048 | 3654 | 3296 | 358 | +| 8k | 0.5 | 4096 | 4096 | 3638 | 2242 | 1397 | +| 8k | 0.75 | 6144 | 6144 | 3649 | 1196 | 2453 | +| 8k | 1.0 | 8192 | 8128 | 3659 | 381 | 3278 | +| 16k | 0.0 | 0 | 0 | 8672 | 8673 | 0 | +| 16k | 0.25 | 4096 | 4096 | 8643 | 7220 | 1423 | +| 16k | 0.5 | 8192 | 8192 | 8655 | 5039 | 3616 | +| 16k | 0.75 | 12288 | 12288 | 8917 | 2629 | 6288 | +| 16k | 1.0 | 16384 | 16320 | 8638 | 385 | 8253 | +| 24k | 0.0 | 0 | 0 | 14394 | 14341 | 53 | +| 24k | 0.25 | 6144 | 6144 | 14380 | 11830 | 2549 | +| 24k | 0.5 | 12288 | 12288 | 14429 | 8304 | 6124 | +| 24k | 0.75 | 18432 | 18432 | 14315 | 4467 | 9848 | +| 24k | 1.0 | 24512 | 24512 | 14410 | 405 | 14005 | + +- `saved_ms` 随 h 近线性增长,随 N 更陡(长上下文重算越贵,复用价值越大)。 +- h=1.0 时 `total≈380–405ms`(几乎纯命中,只剩最后一页现算 + 调度开销),`saved` 达全量的 ~96%。 +- `cached_tokens` 恒等于 page 对齐前缀(h=1.0 时 = N−64,末页留一个未命中 token 触发前向)。 + + + +理想算力侧临界带宽 +$$ +B_{\text{crit}}^{\text{ideal}} += +\frac{S_{\text{KV}}(hN)} +{T_{\text{prefill}}(N)-T_{\text{prefill}}((1-h)N)} +$$ + +- $N$:请求总 token 数; +- $h$:前缀命中率; +- $hN$:命中的 token 数; +- $S_{\text{KV}}(hN)$:这些命中 token 对应的 KV 数据量; +- $T_{\text{prefill}}(N)$:完整重算 N 个 token 的时间; +- $T_{\text{prefill}}((1-h)N)$:只计算未命中后缀的时间; +- 两者之差:复用前缀 KV 所节省的计算时间。 + +分母用 `recompute_ms − cold_uniq_ms`,即前缀对应的现算时间;本地命中 restore≈0,此项即纯算侧收益,$T_{restore}$ 把命中的 KV cache 从远端 store 搬回本地所需要的时间, `T_restore@RDMA 20` 即使用 RDMA,有效带宽为 20GB/s + +| N | h | cached KV | 前缀现算(=分母) | **B_crit** | T_restore@RDMA 20 | T_restore@TCP 1 | +|---|---|---|---|---|---|---| +| 8k | 0.5 | 0.268 GB | 2236 ms | **0.120 GB/s** | 13 ms | 268 ms | +| 8k | 1.0 | 0.532 GB | 3659 ms | **0.145 GB/s** | 27 ms | 532 ms | +| 16k | 0.5 | 0.536 GB | 5029 ms | **0.107 GB/s** | 27 ms | 536 ms | +| 16k | 1.0 | 1.068 GB | 8638 ms | **0.124 GB/s** | 53 ms | 1068 ms | +| 24k | 0.75 | 1.206 GB | 11830 ms | **0.102 GB/s** | 60 ms | 1206 ms | +| 24k | 1.0 | 1.604 GB | 14410 ms | **0.111 GB/s** | 80 ms | 1604 ms | + +1. **DeepSeek V3.2 的 `B_crit` 极低(~0.09–0.15 GB/s)** + + 根因是 MLA 单 latent KV 极省(68.6 KiB/token)而 671B MoE 的 prefill 算力极贵(~0.45–0.6 ms/token),每传 1 字节能省下的算力时间极大,打平所需带宽很低 + +2. **TCP(~1 GB/s)与 RDMA(~20 GB/s)都远高于 `B_crit`(分别高 ~8× / ~180×)**,即 + 以存代算对 V3.2 在两种传输下都稳赚 + + RDMA 的 restore 延迟:`T_restore` 从 TCP 的 268–1604 ms 降到 RDMA 的 13–80 ms(~16–20×),直接改善命中请求的TTFT(验收2 的 QPM/TTFT 差距来源) + +3. 分母(算力收益)巨大使 `B_crit` 很低,retrieve 带宽越高,即可让命中率还比较低时,远端 KV retrieve 的总收益就已经大于传输成本,开始比全量重算更快 + +### 模型实测标定 + +通过模型推导得到: +$$ +B_{\text{crit}}(N,h) += +\frac{S_{\text{KV}}(hN)} +{ +T_{\text{prefill}}(N) +- +T_{\text{prefill}}((1-h)N) +- +T_{\text{lookup}} +- +T_{\text{restore}} +- +L_{\text{net}} +} +$$ + +- **分子** `S_KV(hN)` = 命中前缀要 retrieve 的 KV 字节。V3.2 MLA 单 latent: `S_token = L·(kv_lora_rank+qk_rope)·b = 61·(512+64)·2 = 70272 B = 68.6 KiB/token`。 +- **分母** = 复用 hN 前缀省下的算力时间(全量减去只剩的后缀现算),再扣掉查表/反序列化/网络固定开销。 +- 分母 ≤ 0 永不划算;固定开销太大,本地命中时 `T_lookup≈T_restore≈L_net≈0`,分母即纯算侧收益 + + + +**prefill end to end microbench** 标定算力侧 `T_prefill(N)` + +prefill_local.csv 每个 N 的全量重算中位数做标定点, 最小二乘拟合过原点的 `T_prefill(N) = a·N + b·N²`(`b·N²` 吸收 attention 的超线性): + +需要把标定点: $8k=3654ms$, $16k=8655ms$, $24k=14394ms$ 三个离散点拟合成一个连续函数: +$$ +T_{\text{prefill}}(N) += +0.39318N+7.886\times10^{-6}N^2 +\quad\text{ms} +$$ + +- `a≈0.393 ms/tok` 线性主项(FFN/MoE + NSA 稀疏 attention 的近线性部分), `b≈7.9e-6 ms/tok²` 是残余二次项。R²=0.9997 + +- **外推警告**:V3.2 用 NSA 稀疏(`index_topk=2048`),attention 在 N≫2048 后应趋近**线性** (每 query 只算 2048 个 key),故 `b·N²` 会高估 128k 的算力。 + + 所以 128k 外推的 `T_prefill` 是区间上界,对应 `B_crit` 是下界(真实 B_crit 只会更高一点点,结论不变)。 + + + +**KV Tensor Transfer microbench** 标定 retrieve 带宽 `B_retrieve` + +`T_restore = S_KV / B_retrieve`。B_retrieve 是传输层带宽(GB/s),与模型无关,直接取kv_tensor_v32_{tcp,rdma}.csv 的 zerocopy `batch_get_into` get 中位: + +| 传输 | B_retrieve (zerocopy get) | 备注 | +| :------- | :------------------------ | :------------------------------------- | +| **TCP** | **1.19 GB/s** | 卡在内核 socket 拷贝,零拷贝几乎无增益 | +| **RDMA** | **20.51 GB/s** | 零拷贝直 DMA 进目标张量,~17.2× TCP | + + + +结果:每 (N,h) 的 B_crit + 是否划算 + +results/model_fit.csv(24 行 = 12 个 h>0 cell × {tcp,rdma}) + +`bcrit_meas` 用实测分母,`bcrit_model` 用拟合 `T_prefill`。全部 `profitable=1` + +| N | h | cached | S_KV | 分母(实测) | **B_crit(实测)** | B_crit(模型) | T_restore@TCP | T_restore@RDMA | +| :--- | :--- | :----- | :------- | :--------- | :--------------- | :----------- | :------------ | :------------- | +| 8k | 0.5 | 4096 | 0.268 GB | 2236 ms | **0.120 GB/s** | 0.134 | 225 ms | 13 ms | +| 8k | 1.0 | 8128 | 0.532 GB | 3659 ms | **0.145 GB/s** | 0.142 | 446 ms | 26 ms | +| 16k | 0.5 | 8192 | 0.536 GB | 5029 ms | **0.107 GB/s** | 0.112 | 450 ms | 26 ms | +| 16k | 1.0 | 16320 | 1.068 GB | 8638 ms | **0.124 GB/s** | 0.125 | 896 ms | 52 ms | +| 24k | 0.75 | 18432 | 1.206 GB | 11830 ms | **0.102 GB/s** | 0.103 | 1012 ms | 59 ms | +| 24k | 1.0 | 24512 | 1.604 GB | 14410 ms | **0.111 GB/s** | 0.111 | 1346 ms | | + +**标定误差(模型 B_crit vs 实测 B_crit)**:`median=2.6%, max=13.0%`。 最大误差落在 8k(短序列 JIT/调度固定开销占比大,拟合的纯二次模型没显式建模 T_0,把开销摊进 斜率 → 短序列略偏);16k/24k 误差 <5% + +**B_crit 实测范围:0.092 – 0.147 GB/s** + + + +外推线上:128K / 30% 命中 + +用拟合 `T_prefill` 外推(128k 单节点放不下,故只能靠模型): + +理论命中 token 数,page size 是 64,对齐到 64 整数倍: +$$ +\text{hN} = \text{align}(0.3\times131072)=39296 +$$ +MLA 每 token、每 TP rank 的 KV 大小是: +$$ +S_{\text{token}}=70272\text{ B/token} +$$ +所以命中 39296 token 对应的 KV 大小为: +$$ +S_{\text{KV}} += +39296\times70272 += +2.76\times10^9\text{ B} +\approx +2.572\text{ GiB} +$$ +前面 N 代入拟合公式得到: +$$ +T_{\text{prefill}}(N) += +0.39318N+7.886\times10^{-6}N^2 +\quad\text{ms} +$$ +得到:$T_{prefill}(128k)\approx187.0$ s, $T_{prefill}(70\% \approx 91776)\approx102.5$ + +得到 +$$ +\text{B\_crit} = 2.572 \text{GB} / 84.5\text{s} = 0.0304 \text{GB/s} +$$ + + + +| retrieve 带宽 | margin (B/B_crit) | T_restore | +| :-------------- | :---------------- | :-------- | +| RDMA 20.51 GB/s | 674× | 0.12 s | +| TCP 1.19 GB/s | 39× | 2.16 s | + + + + +结论 + +1. **V3.2 的 `B_crit` 极低(实测 0.09–0.15 GB/s,128k/30% 外推 0.03 GB/s)** + + 根因:MLA 单 latent KV 极省(68.6 KiB/token),而 671B MoE prefill 算力极贵(~0.45–0.6 ms/token), 每传 1 字节能省的算力时间极大。 + +2. **TCP(~1.19)与 RDMA(~20.51 GB/s)都远高于 B_crit**(分别 ~8–39× / ~140–674×) + + 以存代算 对 V3.2 在两种传输下都稳赚,且在极低命中率下就转正。 + +3. **RDMA 的价值在 restore 延迟/TTFT**:`T_restore` 从 TCP 的 0.23–2.16 s 降到 RDMA 的 0.01–0.12 s(~17×),直接改善命中请求的 TTFT + +4. `selftest` 验证闭式解 `T_cacheB_crit`;模型 vs 实测 B_crit 标定误差 median 2.6%。 + + + +## 验收2 + +相同 TTFT 约束下 + +TCP 条件下, 带宽,延迟,可承载并发query数,QPM + +RDMA 条件下,带宽,延迟,可承载并发query数,QPM + + + +**相同 TTFT SLO** 下量出 no-cache / TCP / RDMA 三者的 **λ_max、QPM、成本/query**,给出 TCP→RDMA 的 QPM/成本对比 + +端到端命路径有 kernel bug,使用离散事件重放:每个逻辑请求的服务时间 由前几个 microbench 的实测数值合成 +$$ +T_{\text{hit}} += +T_{\text{lookup}} ++ +\frac{S_{\text{KV}}(hN)}{B_{\text{retrieve}}} ++ +T_{\text{prefill}}((1-h)N) +$$ + +- `T_prefill(N) = 0.39318·N + 7.886e-6·N²` ms +- `B_retrieve`:L2 zerocopy `batch_get_into` 中位,TCP 1.19 / RDMA 20.51 GB/s +- `T_lookup = 5 ms` 固定 + +**排队模型**:`c` 个 prefill worker 的 FIFO 队列(GPU 算力受限,默认 `c=1` 即算力串行), Poisson 到达(率 λ req/s)。扫 λ 直到 P95 TTFT > SLO 得到 `λ_max`; + +计算 `QPM = 60·λ_max`; `Cost/Query` 正比于 `C_node / QPM`。 + +三种传输(no-cache/TCP/RDMA)在同一 context 下**共用同一 SLO** (`SLO = T_prefill(N) × slo_factor`,`slo_factor=2.5`,由 no-cache 单请求服务定,与传输无关) + + + +phit 命中概率,h 命中前缀比例 + +高命中(h=0.95, phit=0.8),retrieve 主导,RDMA 显著 + +| Context | 方案 | KV retrieve 带宽 | 命中请求 restore 延迟 | 平均服务时间 | P95 TTFT SLO | 最大可持续到达率 λ_max | QPM | QPM vs no-cache | RDMA/TCP QPM | +| ------- | -------- | ---------------- | --------------------- | ------------ | ------------ | ---------------------- | ----- | --------------- | ------------ | +| 8k | no-cache | — | — | 3.750 s | 9.375 s | 0.080 req/s | 4.80 | 1.00× | — | +| 8k | TCP | 1.19 GB/s | ≈0.43 s | 1.236 s | 9.375 s | 0.404 req/s | 24.26 | 5.05× | 1.00× | +| 8k | RDMA | 20.51 GB/s | ≈0.025 s | 0.916 s | 9.375 s | 0.546 req/s | 32.75 | 6.82× | **1.35×** | +| 16k | no-cache | — | — | 8.559 s | 21.397 s | 0.035 req/s | 2.10 | 1.00× | — | +| 16k | TCP | 1.19 GB/s | ≈0.86 s | 2.665 s | 21.397 s | 0.188 req/s | 11.26 | 5.36× | 1.00× | +| 16k | RDMA | 20.51 GB/s | ≈0.050 s | 2.022 s | 21.397 s | 0.247 req/s | 14.84 | 7.07× | **1.32×** | +| 24k | no-cache | — | — | 14.426 s | 36.065 s | 0.021 req/s | 1.25 | 1.00× | — | +| 24k | TCP | 1.19 GB/s | ≈1.28 s | 4.325 s | 36.065 s | 0.116 req/s | 6.94 | 5.55× | 1.00× | +| 24k | RDMA | 20.51 GB/s | ≈0.075 s | 3.362 s | 36.065 s | 0.149 req/s | 8.92 | 7.14× | **1.29×** | + +- 高命中下缓存把 QPM 提升 **~5–7×**、成本降到 **~0.14–0.20×**。 +- **此时 RDMA 比 TCP 高 ~1.29–1.35× QPM**(成本再降 ~22–26%)。因为 h=0.95 时后缀现算≈0, 服务时间由 **retrieve 主导**:TCP restore(8k=0.43s/16k=0.86s/24k=1.28s 摊进平均)明显拖慢, RDMA(0.02–0.07s)几乎不占时间 ⇒ 直接转成更高 λ_max。 + + + +中等命中(h=0.5, phit=0.5),算力主导,RDMA≈TCP + +| Context | 方案 | KV retrieve 带宽 | 命中请求 restore 延迟 | 平均服务时间 | P95 TTFT SLO | 最大可持续到达率 λ_max | QPM | QPM vs no-cache | RDMA/TCP QPM | +| ------- | -------- | ---------------- | --------------------- | ------------ | ------------ | ---------------------- | ---- | --------------- | ------------ | +| 8k | no-cache | — | — | 3.750 s | 9.375 s | 0.080 req/s | 4.80 | 1.00× | — | +| 8k | TCP | 1.19 GB/s | ≈0.225 s | 2.861 s | 9.375 s | 0.140 req/s | 8.39 | 1.75× | 1.00× | +| 8k | RDMA | 20.51 GB/s | ≈0.013 s | 2.755 s | 9.375 s | 0.145 req/s | 8.71 | 1.81× | **1.04×** | +| 16k | no-cache | — | — | 8.559 s | 21.397 s | 0.035 req/s | 2.10 | 1.00× | — | +| 16k | TCP | 1.19 GB/s | ≈0.450 s | 6.382 s | 21.397 s | 0.063 req/s | 3.76 | 1.79× | 1.00× | +| 16k | RDMA | 20.51 GB/s | ≈0.026 s | 6.170 s | 21.397 s | 0.065 req/s | 3.89 | 1.85× | **1.03×** | +| 24k | no-cache | — | — | 14.426 s | 36.065 s | 0.021 req/s | 1.25 | 1.00× | — | +| 24k | TCP | 1.19 GB/s | ≈0.675 s | 10.564 s | 36.065 s | 0.038 req/s | 2.27 | 1.82× | 1.00× | +| 24k | RDMA | 20.51 GB/s | ≈0.039 s | 10.246 s | 36.065 s | 0.039 req/s | 2.34 | 1.87× | **1.03×** | + + +- **以存代算(缓存)本身把 QPM 提升 ~1.8×、成本降到 ~0.55×**(相对全量重算),因为命中请求少算了 hN 前缀。 +- **但 RDMA 仅比 TCP 高 ~1.03–1.04×**:在 8k–24k、h=0.5 下,retrieve 时间(TCP 225–675 ms) 相对 prefill 算力(数秒~十几秒)占比很小,**服务时间被算力主导**,换 RDMA 省下的那点 retrieve 时间对 λ_max 影响甚微。⇒ **中短上下文 + 中等命中,RDMA 的收益不明显**。 + + + +结论: + +由于 DeepSeek V3.2 特性,以存代算在两种传输下都提升吞吐/降成本:中等命中 QPM ×1.8(成本 0.55×), 高命中 QPM ×5–7(成本 0.14–0.20×)。 + +RDMA vs TCP 的增益强依赖 retrieve 占服务时间的比重: + +- 算力主导区(中短上下文 / 中等命中):retrieve 占比小,RDMA≈TCP(~1.03–1.04×) +- retrieve 主导区(高命中 / 长上下文):RDMA 比 TCP 高 ~1.29–1.35× QPM,成本再降 ~1/4。 + +进一步外推(长上下文)RDMA 优势更大:h=1.0、128k 时 restore 是唯一变量, TCP restore ≈ 8.6GB/1.19 ≈ 7.2 s、RDMA ≈ 0.42 s(~17×),命中请求 TTFT 差距直接放大到 ~17× (128k 单节点放不下、只能靠 §模型外推,见步骤4 §2 / 步骤5 §2 的外推上界说明)。 \ No newline at end of file diff --git a/src/code/issue5/results/model_fit.csv b/src/code/issue5/results/model_fit.csv new file mode 100644 index 0000000..a89928f --- /dev/null +++ b/src/code/issue5/results/model_fit.csv @@ -0,0 +1,25 @@ +N,h,cached,s_kv_gb,tprefill_N_ms,denom_meas_ms,denom_model_ms,bcrit_meas_gbps,bcrit_model_gbps,transport,b_retrieve_gbps,t_restore_ms,net_saved_ms,profitable,measured_saved_ms +8192,0.25,2048,0.134,3750.1,911.1,1036.8,0.1471,0.1293,tcp_zc,1.192,112.4,798.7,1,357.56 +8192,0.25,2048,0.134,3750.1,911.1,1036.8,0.1471,0.1293,rdma_zc,20.508,6.5,904.6,1,357.56 +8192,0.5,4096,0.2681,3750.1,2236.2,2007.4,0.1199,0.1335,tcp_zc,1.192,224.9,2011.3,1,1396.53 +8192,0.5,4096,0.2681,3750.1,2236.2,2007.4,0.1199,0.1335,rdma_zc,20.508,13.1,2223.1,1,1396.53 +8192,0.75,6144,0.4021,3750.1,3291.1,2911.8,0.1222,0.1381,tcp_zc,1.192,337.3,2953.8,1,2452.81 +8192,0.75,6144,0.4021,3750.1,3291.1,2911.8,0.1222,0.1381,rdma_zc,20.508,19.6,3271.5,1,2452.81 +8192,1.0,8128,0.5319,3750.1,3659.4,3750.1,0.1454,0.1418,tcp_zc,1.192,446.3,3213.2,1,3278.28 +8192,1.0,8128,0.5319,3750.1,3659.4,3750.1,0.1454,0.1418,rdma_zc,20.508,25.9,3633.5,1,3278.28 +16384,0.25,4096,0.2681,8558.7,2540.4,2536.6,0.1055,0.1057,tcp_zc,1.192,224.9,2315.5,1,1422.53 +16384,0.25,4096,0.2681,8558.7,2540.4,2536.6,0.1055,0.1057,rdma_zc,20.508,13.1,2527.3,1,1422.53 +16384,0.5,8192,0.5361,8558.7,5028.6,4808.6,0.1066,0.1115,tcp_zc,1.192,449.8,4578.8,1,3615.6 +16384,0.5,8192,0.5361,8558.7,5028.6,4808.6,0.1066,0.1115,rdma_zc,20.508,26.1,5002.4,1,3615.6 +16384,0.75,12288,0.8042,8558.7,7530.1,6816.0,0.1068,0.118,tcp_zc,1.192,674.7,6855.4,1,6288.46 +16384,0.75,12288,0.8042,8558.7,7530.1,6816.0,0.1068,0.118,rdma_zc,20.508,39.2,7490.9,1,6288.46 +16384,1.0,16320,1.0681,8558.7,8638.4,8558.7,0.1236,0.1248,tcp_zc,1.192,896.0,7742.4,1,8253.2 +16384,1.0,16320,1.0681,8558.7,8638.4,8558.7,0.1236,0.1248,rdma_zc,20.508,52.1,8586.3,1,8253.2 +24576,0.25,6144,0.4021,14425.8,4375.1,4499.5,0.0919,0.0894,tcp_zc,1.192,337.3,4037.8,1,2549.47 +24576,0.25,6144,0.4021,14425.8,4375.1,4499.5,0.0919,0.0894,rdma_zc,20.508,19.6,4355.5,1,2549.47 +24576,0.5,12288,0.8042,14425.8,8354.4,8403.7,0.0963,0.0957,tcp_zc,1.192,674.7,7679.7,1,6124.49 +24576,0.5,12288,0.8042,14425.8,8354.4,8403.7,0.0963,0.0957,rdma_zc,20.508,39.2,8315.1,1,6124.49 +24576,0.75,18432,1.2063,14425.8,11830.3,11712.5,0.102,0.103,tcp_zc,1.192,1012.0,10818.3,1,9848.02 +24576,0.75,18432,1.2063,14425.8,11830.3,11712.5,0.102,0.103,rdma_zc,20.508,58.8,11771.5,1,9848.02 +24576,1.0,24512,1.6042,14425.8,14409.5,14425.8,0.1113,0.1112,tcp_zc,1.192,1345.8,13063.7,1,14004.68 +24576,1.0,24512,1.6042,14425.8,14409.5,14425.8,0.1113,0.1112,rdma_zc,20.508,78.2,14331.3,1,14004.68 diff --git a/src/code/issue5/results/raw/conc_hihit_summary.csv b/src/code/issue5/results/raw/conc_hihit_summary.csv new file mode 100644 index 0000000..e402faf --- /dev/null +++ b/src/code/issue5/results/raw/conc_hihit_summary.csv @@ -0,0 +1,10 @@ +context_length,transport,mean_service_ms,slo_ms,lambda_max_rps,qpm +8192,nocache,3750.1,9375.3,0.08,4.8 +8192,tcp,1236.4,9375.3,0.4044,24.26 +8192,rdma,916.0,9375.3,0.5459,32.75 +16384,nocache,8558.7,21396.9,0.0351,2.1 +16384,tcp,2664.9,21396.9,0.1876,11.26 +16384,rdma,2021.5,21396.9,0.2473,14.84 +24576,nocache,14425.8,36064.6,0.0208,1.25 +24576,tcp,4325.4,36064.6,0.1156,6.94 +24576,rdma,3361.6,36064.6,0.1487,8.92 diff --git a/src/code/issue5/results/raw/conc_hihit_sweep.csv b/src/code/issue5/results/raw/conc_hihit_sweep.csv new file mode 100644 index 0000000..dd5ccf2 --- /dev/null +++ b/src/code/issue5/results/raw/conc_hihit_sweep.csv @@ -0,0 +1,49 @@ +transport,context_length,hit_prefix,phit,workers,lambda_rps,p50_ttft_ms,p95_ttft_ms,p99_ttft_ms,slo_ms,within_slo +nocache,8192,0.95,0.0,1,0.0267,3750.1,5905.9,7345.3,9375.3,1 +nocache,8192,0.95,0.0,1,0.0533,3750.1,7017.5,9420.9,9375.3,1 +nocache,8192,0.95,0.0,1,0.08,3750.1,7962.4,11567.0,9375.3,1 +nocache,8192,0.95,0.0,1,0.1067,3750.1,9766.5,14300.2,9375.3,0 +tcp,8192,0.95,0.8,1,0.0809,607.9,3750.1,5594.3,9375.3,1 +tcp,8192,0.95,0.8,1,0.1618,607.9,4089.9,6716.1,9375.3,1 +tcp,8192,0.95,0.8,1,0.2426,607.9,5108.5,7447.4,9375.3,1 +tcp,8192,0.95,0.8,1,0.3235,738.5,6538.6,8895.2,9375.3,1 +tcp,8192,0.95,0.8,1,0.4044,1149.7,7593.4,12874.3,9375.3,1 +tcp,8192,0.95,0.8,1,0.4853,1954.7,9676.1,18999.1,9375.3,0 +rdma,8192,0.95,0.8,1,0.1092,207.4,3750.1,5996.4,9375.3,1 +rdma,8192,0.95,0.8,1,0.2183,207.4,3920.1,6877.7,9375.3,1 +rdma,8192,0.95,0.8,1,0.3275,207.4,5657.3,7601.2,9375.3,1 +rdma,8192,0.95,0.8,1,0.4367,266.2,6972.7,10558.3,9375.3,1 +rdma,8192,0.95,0.8,1,0.5459,1114.5,8454.8,16237.8,9375.3,1 +rdma,8192,0.95,0.8,1,0.655,2297.7,11247.7,21308.4,9375.3,0 +nocache,16384,0.95,0.0,1,0.0117,8558.7,13478.8,16763.9,21396.9,1 +nocache,16384,0.95,0.0,1,0.0234,8558.7,16015.8,21501.0,21396.9,1 +nocache,16384,0.95,0.0,1,0.0351,8558.7,18172.1,26398.7,21396.9,1 +nocache,16384,0.95,0.0,1,0.0467,8558.7,22289.6,32636.7,21396.9,0 +tcp,16384,0.95,0.8,1,0.0375,1191.5,8558.7,12988.6,21396.9,1 +tcp,16384,0.95,0.8,1,0.075,1191.5,9226.6,15478.4,21396.9,1 +tcp,16384,0.95,0.8,1,0.1126,1191.5,11873.2,16814.4,21396.9,1 +tcp,16384,0.95,0.8,1,0.1501,1465.1,15148.3,20946.7,21396.9,1 +tcp,16384,0.95,0.8,1,0.1876,2315.9,17526.9,30722.5,21396.9,1 +tcp,16384,0.95,0.8,1,0.2251,4538.9,22343.6,44827.7,21396.9,0 +rdma,16384,0.95,0.8,1,0.0495,387.2,8558.7,13777.4,21396.9,1 +rdma,16384,0.95,0.8,1,0.0989,387.2,8940.1,15743.5,21396.9,1 +rdma,16384,0.95,0.8,1,0.1484,387.2,13026.4,17459.4,21396.9,1 +rdma,16384,0.95,0.8,1,0.1979,554.3,16171.5,24664.6,21396.9,1 +rdma,16384,0.95,0.8,1,0.2473,2663.5,19640.5,37673.1,21396.9,1 +rdma,16384,0.95,0.8,1,0.2968,5378.8,26499.7,49919.3,21396.9,0 +nocache,24576,0.95,0.0,1,0.0069,14425.8,22718.6,28255.6,36064.6,1 +nocache,24576,0.95,0.0,1,0.0139,14425.8,26994.8,36240.0,36064.6,1 +nocache,24576,0.95,0.0,1,0.0208,14425.8,30629.2,44495.2,36064.6,1 +nocache,24576,0.95,0.0,1,0.0277,14425.8,37569.3,55009.4,36064.6,0 +tcp,24576,0.95,0.8,1,0.0231,1800.2,14425.8,22104.7,36064.6,1 +tcp,24576,0.95,0.8,1,0.0462,1800.2,15487.9,26191.3,36064.6,1 +tcp,24576,0.95,0.8,1,0.0694,1800.2,20223.8,28452.4,36064.6,1 +tcp,24576,0.95,0.8,1,0.0925,2219.8,25835.0,35682.3,36064.6,1 +tcp,24576,0.95,0.8,1,0.1156,3594.7,29843.9,52554.3,36064.6,1 +tcp,24576,0.95,0.8,1,0.1387,7662.1,38482.2,76935.3,36064.6,0 +rdma,24576,0.95,0.8,1,0.0297,595.5,14425.8,23267.1,36064.6,1 +rdma,24576,0.95,0.8,1,0.0595,595.5,15078.5,26566.8,36064.6,1 +rdma,24576,0.95,0.8,1,0.0892,595.5,22097.3,29708.6,36064.6,1 +rdma,24576,0.95,0.8,1,0.119,907.4,27276.9,41921.6,36064.6,1 +rdma,24576,0.95,0.8,1,0.1487,4655.6,33390.3,63899.9,36064.6,1 +rdma,24576,0.95,0.8,1,0.1785,9237.0,44923.1,84292.2,36064.6,0 diff --git a/src/code/issue5/results/raw/conc_summary.csv b/src/code/issue5/results/raw/conc_summary.csv new file mode 100644 index 0000000..004ba31 --- /dev/null +++ b/src/code/issue5/results/raw/conc_summary.csv @@ -0,0 +1,10 @@ +context_length,transport,mean_service_ms,slo_ms,lambda_max_rps,qpm +8192,nocache,3750.1,9375.3,0.08,4.8 +8192,tcp,2861.4,9375.3,0.1398,8.39 +8192,rdma,2755.5,9375.3,0.1452,8.71 +16384,nocache,8558.7,21396.9,0.0351,2.1 +16384,tcp,6381.8,21396.9,0.0627,3.76 +16384,rdma,6170.0,21396.9,0.0648,3.89 +24576,nocache,14425.8,36064.6,0.0208,1.25 +24576,tcp,10563.8,36064.6,0.0379,2.27 +24576,rdma,10246.1,36064.6,0.039,2.34 diff --git a/src/code/issue5/results/raw/conc_sweep.csv b/src/code/issue5/results/raw/conc_sweep.csv new file mode 100644 index 0000000..47c95ee --- /dev/null +++ b/src/code/issue5/results/raw/conc_sweep.csv @@ -0,0 +1,43 @@ +transport,context_length,hit_prefix,phit,workers,lambda_rps,p50_ttft_ms,p95_ttft_ms,p99_ttft_ms,slo_ms,within_slo +nocache,8192,0.5,0.0,1,0.0267,3750.1,5905.9,7345.3,9375.3,1 +nocache,8192,0.5,0.0,1,0.0533,3750.1,7017.5,9420.9,9375.3,1 +nocache,8192,0.5,0.0,1,0.08,3750.1,7962.4,11567.0,9375.3,1 +nocache,8192,0.5,0.0,1,0.1067,3750.1,9766.5,14300.2,9375.3,0 +tcp,8192,0.5,0.5,1,0.0349,3750.1,4616.2,6741.4,9375.3,1 +tcp,8192,0.5,0.5,1,0.0699,3750.1,5743.8,7559.2,9375.3,1 +tcp,8192,0.5,0.5,1,0.1048,3750.1,6959.7,9242.7,9375.3,1 +tcp,8192,0.5,0.5,1,0.1398,3750.1,8102.1,11393.4,9375.3,1 +tcp,8192,0.5,0.5,1,0.1747,3750.1,9785.3,13876.1,9375.3,0 +rdma,8192,0.5,0.5,1,0.0363,3750.1,4503.4,6728.2,9375.3,1 +rdma,8192,0.5,0.5,1,0.0726,3750.1,5728.7,7431.3,9375.3,1 +rdma,8192,0.5,0.5,1,0.1089,3750.1,6909.1,9119.1,9375.3,1 +rdma,8192,0.5,0.5,1,0.1452,3750.1,7981.0,11158.4,9375.3,1 +rdma,8192,0.5,0.5,1,0.1815,3750.1,9596.0,13748.1,9375.3,0 +nocache,16384,0.5,0.0,1,0.0117,8558.7,13478.8,16763.9,21396.9,1 +nocache,16384,0.5,0.0,1,0.0234,8558.7,16015.8,21501.0,21396.9,1 +nocache,16384,0.5,0.0,1,0.0351,8558.7,18172.1,26398.7,21396.9,1 +nocache,16384,0.5,0.0,1,0.0467,8558.7,22289.6,32636.7,21396.9,0 +tcp,16384,0.5,0.5,1,0.0157,8558.7,10383.5,15308.6,21396.9,1 +tcp,16384,0.5,0.5,1,0.0313,8558.7,13075.0,17044.4,21396.9,1 +tcp,16384,0.5,0.5,1,0.047,8558.7,15764.8,20819.6,21396.9,1 +tcp,16384,0.5,0.5,1,0.0627,8558.7,18227.3,25605.7,21396.9,1 +tcp,16384,0.5,0.5,1,0.0783,8558.7,22170.2,31352.0,21396.9,0 +rdma,16384,0.5,0.5,1,0.0162,8544.7,10093.5,15463.5,21396.9,1 +rdma,16384,0.5,0.5,1,0.0324,8558.7,13116.8,16962.5,21396.9,1 +rdma,16384,0.5,0.5,1,0.0486,8558.7,15620.8,20800.5,21396.9,1 +rdma,16384,0.5,0.5,1,0.0648,8558.7,18075.6,25619.2,21396.9,1 +rdma,16384,0.5,0.5,1,0.081,8558.7,21782.6,31308.9,21396.9,0 +nocache,24576,0.5,0.0,1,0.0069,14425.8,22718.6,28255.6,36064.6,1 +nocache,24576,0.5,0.0,1,0.0139,14425.8,26994.8,36240.0,36064.6,1 +nocache,24576,0.5,0.0,1,0.0208,14425.8,30629.2,44495.2,36064.6,1 +nocache,24576,0.5,0.0,1,0.0277,14425.8,37569.3,55009.4,36064.6,0 +tcp,24576,0.5,0.5,1,0.0095,14425.8,17264.7,25940.2,36064.6,1 +tcp,24576,0.5,0.5,1,0.0189,14425.8,22025.8,28586.6,36064.6,1 +tcp,24576,0.5,0.5,1,0.0284,14425.8,26536.3,35105.3,36064.6,1 +tcp,24576,0.5,0.5,1,0.0379,14425.8,30634.6,42973.7,36064.6,1 +tcp,24576,0.5,0.5,1,0.0473,14425.8,36864.5,52848.2,36064.6,0 +rdma,24576,0.5,0.5,1,0.0098,14179.0,16793.4,26020.2,36064.6,1 +rdma,24576,0.5,0.5,1,0.0195,14425.8,21953.1,28594.3,36064.6,1 +rdma,24576,0.5,0.5,1,0.0293,14425.8,26307.8,35179.7,36064.6,1 +rdma,24576,0.5,0.5,1,0.039,14425.8,30298.1,42877.2,36064.6,1 +rdma,24576,0.5,0.5,1,0.0488,14425.8,36554.3,52420.4,36064.6,0 diff --git a/src/code/issue5/results/raw/kv_tensor_rdma.csv b/src/code/issue5/results/raw/kv_tensor_rdma.csv new file mode 100644 index 0000000..abb6afa --- /dev/null +++ b/src/code/issue5/results/raw/kv_tensor_rdma.csv @@ -0,0 +1,13 @@ +transport,model,path,op,context,tp,page_size,num_pages,num_keys,kv_bytes,kv_size,iters,transfer_ms,effective_bandwidth_gbps,max_abs_error,success +rdma,qwen3-32b,bytes,put,8192,8,64,128,256,268435456,256MB,5,93.054,2.687,,1 +rdma,qwen3-32b,bytes,get,8192,8,64,128,256,268435456,256MB,5,107.556,2.324,0.0,1 +rdma,qwen3-32b,zerocopy,put,8192,8,64,128,256,268435456,256MB,5,11.893,21.021,,1 +rdma,qwen3-32b,zerocopy,get,8192,8,64,128,256,268435456,256MB,5,12.197,20.497,0.0,1 +rdma,qwen3-32b,bytes,put,32768,8,64,512,1024,1073741824,1.0GB,5,320.343,3.122,,1 +rdma,qwen3-32b,bytes,get,32768,8,64,512,1024,1073741824,1.0GB,5,257.4,3.885,0.0,1 +rdma,qwen3-32b,zerocopy,put,32768,8,64,512,1024,1073741824,1.0GB,5,49.273,20.295,,1 +rdma,qwen3-32b,zerocopy,get,32768,8,64,512,1024,1073741824,1.0GB,5,47.461,21.07,0.0,1 +rdma,qwen3-32b,bytes,put,131072,8,64,2048,4096,4294967296,4.0GB,5,1020.6,3.92,,1 +rdma,qwen3-32b,bytes,get,131072,8,64,2048,4096,4294967296,4.0GB,5,962.0,4.16,0.0,1 +rdma,qwen3-32b,zerocopy,put,131072,8,64,2048,4096,4294967296,4.0GB,5,202.36,19.767,,1 +rdma,qwen3-32b,zerocopy,get,131072,8,64,2048,4096,4294967296,4.0GB,5,193.394,20.683,0.0,1 diff --git a/src/code/issue5/results/raw/kv_tensor_tcp.csv b/src/code/issue5/results/raw/kv_tensor_tcp.csv new file mode 100644 index 0000000..a825839 --- /dev/null +++ b/src/code/issue5/results/raw/kv_tensor_tcp.csv @@ -0,0 +1,13 @@ +transport,model,path,op,context,tp,page_size,num_pages,num_keys,kv_bytes,kv_size,iters,transfer_ms,effective_bandwidth_gbps,max_abs_error,success +tcp,qwen3-32b,bytes,put,8192,8,64,128,256,268435456,256MB,5,342.811,0.729,,1 +tcp,qwen3-32b,bytes,get,8192,8,64,128,256,268435456,256MB,5,295.74,0.845,0.0,1 +tcp,qwen3-32b,zerocopy,put,8192,8,64,128,256,268435456,256MB,5,233.864,1.069,,1 +tcp,qwen3-32b,zerocopy,get,8192,8,64,128,256,268435456,256MB,5,226.193,1.105,0.0,1 +tcp,qwen3-32b,bytes,put,32768,8,64,512,1024,1073741824,1.0GB,5,1273.358,0.785,,1 +tcp,qwen3-32b,bytes,get,32768,8,64,512,1024,1073741824,1.0GB,5,1439.864,0.695,0.0,1 +tcp,qwen3-32b,zerocopy,put,32768,8,64,512,1024,1073741824,1.0GB,5,1072.737,0.932,,1 +tcp,qwen3-32b,zerocopy,get,32768,8,64,512,1024,1073741824,1.0GB,5,913.954,1.094,0.0,1 +tcp,qwen3-32b,bytes,put,131072,8,64,2048,4096,4294967296,4.0GB,5,5173.846,0.773,,1 +tcp,qwen3-32b,bytes,get,131072,8,64,2048,4096,4294967296,4.0GB,5,4840.868,0.826,0.0,1 +tcp,qwen3-32b,zerocopy,put,131072,8,64,2048,4096,4294967296,4.0GB,5,3980.428,1.005,,1 +tcp,qwen3-32b,zerocopy,get,131072,8,64,2048,4096,4294967296,4.0GB,5,3404.958,1.175,0.0,1 diff --git a/src/code/issue5/results/raw/kv_tensor_v32_rdma.csv b/src/code/issue5/results/raw/kv_tensor_v32_rdma.csv new file mode 100644 index 0000000..1af5c81 --- /dev/null +++ b/src/code/issue5/results/raw/kv_tensor_v32_rdma.csv @@ -0,0 +1,13 @@ +transport,model,path,op,context,tp,page_size,num_pages,num_keys,kv_bytes,kv_size,iters,transfer_ms,effective_bandwidth_gbps,max_abs_error,success +rdma,deepseek-v32,bytes,put,8192,8,64,128,128,575668224,549MB,5,77.2,6.945,,1 +rdma,deepseek-v32,bytes,get,8192,8,64,128,128,575668224,549MB,5,203.946,2.629,0.0,1 +rdma,deepseek-v32,zerocopy,put,8192,8,64,128,128,575668224,549MB,5,24.649,21.751,,1 +rdma,deepseek-v32,zerocopy,get,8192,8,64,128,128,575668224,549MB,5,31.016,17.286,0.0,1 +rdma,deepseek-v32,bytes,put,32768,8,64,512,512,2302672896,2.1GB,5,305.875,7.011,,1 +rdma,deepseek-v32,bytes,get,32768,8,64,512,512,2302672896,2.1GB,5,291.117,7.367,0.0,1 +rdma,deepseek-v32,zerocopy,put,32768,8,64,512,512,2302672896,2.1GB,5,99.534,21.546,,1 +rdma,deepseek-v32,zerocopy,get,32768,8,64,512,512,2302672896,2.1GB,5,104.572,20.508,0.0,1 +rdma,deepseek-v32,bytes,put,131072,8,64,2048,2048,9210691584,8.6GB,5,1877.866,4.568,,1 +rdma,deepseek-v32,bytes,get,131072,8,64,2048,2048,9210691584,8.6GB,5,1230.088,6.974,0.0,1 +rdma,deepseek-v32,zerocopy,put,131072,8,64,2048,2048,9210691584,8.6GB,5,390.708,21.955,,1 +rdma,deepseek-v32,zerocopy,get,131072,8,64,2048,2048,9210691584,8.6GB,5,404.963,21.182,0.0,1 diff --git a/src/code/issue5/results/raw/kv_tensor_v32_tcp.csv b/src/code/issue5/results/raw/kv_tensor_v32_tcp.csv new file mode 100644 index 0000000..88f366c --- /dev/null +++ b/src/code/issue5/results/raw/kv_tensor_v32_tcp.csv @@ -0,0 +1,13 @@ +transport,model,path,op,context,tp,page_size,num_pages,num_keys,kv_bytes,kv_size,iters,transfer_ms,effective_bandwidth_gbps,max_abs_error,success +tcp,deepseek-v32,bytes,put,8192,8,64,128,128,575668224,549MB,5,623.595,0.86,,1 +tcp,deepseek-v32,bytes,get,8192,8,64,128,128,575668224,549MB,5,791.541,0.677,0.0,1 +tcp,deepseek-v32,zerocopy,put,8192,8,64,128,128,575668224,549MB,5,452.819,1.184,,1 +tcp,deepseek-v32,zerocopy,get,8192,8,64,128,128,575668224,549MB,5,460.377,1.165,0.0,1 +tcp,deepseek-v32,bytes,put,32768,8,64,512,512,2302672896,2.1GB,5,3541.427,0.606,,1 +tcp,deepseek-v32,bytes,get,32768,8,64,512,512,2302672896,2.1GB,5,2648.839,0.81,0.0,1 +tcp,deepseek-v32,zerocopy,put,32768,8,64,512,512,2302672896,2.1GB,5,1984.112,1.081,,1 +tcp,deepseek-v32,zerocopy,get,32768,8,64,512,512,2302672896,2.1GB,5,1754.665,1.222,0.0,1 +tcp,deepseek-v32,bytes,put,131072,8,64,2048,2048,9210691584,8.6GB,5,8382.252,1.023,,1 +tcp,deepseek-v32,bytes,get,131072,8,64,2048,2048,9210691584,8.6GB,5,10357.485,0.828,0.0,1 +tcp,deepseek-v32,zerocopy,put,131072,8,64,2048,2048,9210691584,8.6GB,5,8221.086,1.043,,1 +tcp,deepseek-v32,zerocopy,get,131072,8,64,2048,2048,9210691584,8.6GB,5,7194.248,1.192,0.0,1 diff --git a/src/code/issue5/results/raw/prefill_local.csv b/src/code/issue5/results/raw/prefill_local.csv new file mode 100644 index 0000000..a608983 --- /dev/null +++ b/src/code/issue5/results/raw/prefill_local.csv @@ -0,0 +1,16 @@ +transport,context_length,hit_ratio,concurrency,prefix_tokens,uniq_tokens,cached_tokens,prompt_tokens,kv_bytes,s_token,lookup_ms,transfer_ms,restore_ms,cold_uniq_ms,recompute_ms,saved_ms,total_ms,effective_bandwidth_gbps,max_abs_error,success +local_radix,8192,0.0,1,0,8192,0,8192,0,70272,,0.0,0.0,3677.36,3679.76,18.91,3660.85,,,1 +local_radix,8192,0.25,1,2048,6144,2048,8192,143917056,70272,,0.0,0.0,2742.75,3653.85,357.56,3296.29,,,1 +local_radix,8192,0.5,1,4096,4096,4096,8192,287834112,70272,,0.0,0.0,1402.2,3638.41,1396.53,2241.88,,,1 +local_radix,8192,0.75,1,6144,2048,6144,8192,431751168,70272,,0.0,0.0,357.61,3648.71,2452.81,1195.91,,,1 +local_radix,8192,1.0,1,8192,0,8128,8192,571170816,70272,,0.0,0.0,0.0,3659.45,3278.28,381.17,,,1 +local_radix,16384,0.0,1,0,16384,0,16384,0,70272,,0.0,0.0,8691.48,8672.48,0.0,8672.75,,,1 +local_radix,16384,0.25,1,4096,12288,4096,16384,287834112,70272,,0.0,0.0,6102.61,8643.03,1422.53,7220.49,,,1 +local_radix,16384,0.5,1,8192,8192,8192,16384,575668224,70272,,0.0,0.0,3626.44,8655.03,3615.6,5039.44,,,1 +local_radix,16384,0.75,1,12288,4096,12288,16384,863502336,70272,,0.0,0.0,1386.94,8917.03,6288.46,2628.57,,,1 +local_radix,16384,1.0,1,16384,0,16320,16384,1146839040,70272,,0.0,0.0,0.0,8638.42,8253.2,385.21,,,1 +local_radix,24576,0.0,1,0,24576,0,24576,0,70272,,0.0,0.0,14404.24,14393.74,52.75,14340.99,,,1 +local_radix,24576,0.25,1,6144,18432,6144,24576,431751168,70272,,0.0,0.0,10004.39,14379.5,2549.47,11830.04,,,1 +local_radix,24576,0.5,1,12288,12288,12288,24576,863502336,70272,,0.0,0.0,6074.62,14428.98,6124.49,8304.49,,,1 +local_radix,24576,0.75,1,18432,6144,18432,24576,1295253504,70272,,0.0,0.0,2484.43,14314.73,9848.02,4466.71,,,1 +local_radix,24576,1.0,1,24576,0,24512,24576,1722507264,70272,,0.0,0.0,0.0,14409.52,14004.68,404.83,,,1 diff --git a/src/code/issue5/results/raw/transfer_rdma.csv b/src/code/issue5/results/raw/transfer_rdma.csv new file mode 100644 index 0000000..f81e073 --- /dev/null +++ b/src/code/issue5/results/raw/transfer_rdma.csv @@ -0,0 +1,25 @@ +transport,op,size,size_bytes,concurrency,iters,total_bytes,wall_s,bandwidth_gbps,lat_mean_ms,lat_p50_ms,lat_p95_ms,lat_p99_ms,success +rdma,put,64MB,67108864,1,20,1342177280,0.2547,4.907,12.728,12.092,14.24,22.654,1 +rdma,get,64MB,67108864,1,20,1342177280,0.8003,1.562,35.514,35.445,36.026,36.766,1 +rdma,put,64MB,67108864,2,20,1342177280,0.1329,9.405,12.806,12.779,12.984,16.228,1 +rdma,get,64MB,67108864,2,20,1342177280,0.5945,2.103,55.548,54.995,57.729,61.265,1 +rdma,put,64MB,67108864,4,20,1342177280,0.09,13.885,16.81,16.059,22.016,24.349,1 +rdma,get,64MB,67108864,4,20,1342177280,0.6732,1.857,119.401,123.213,194.759,230.516,1 +rdma,put,64MB,67108864,8,20,1342177280,0.078,16.034,26.58,25.703,36.297,40.607,1 +rdma,get,64MB,67108864,8,20,1342177280,0.6933,1.803,243.583,227.258,355.241,515.412,1 +rdma,put,256MB,268435456,1,20,5368709120,0.9575,5.222,47.868,47.774,48.066,50.26,1 +rdma,get,256MB,268435456,1,20,5368709120,3.4146,1.464,151.929,152.023,154.575,154.733,1 +rdma,put,256MB,268435456,2,20,5368709120,0.6555,7.628,64.854,55.525,142.204,153.727,1 +rdma,get,256MB,268435456,2,20,5368709120,2.9961,1.669,273.096,276.756,300.763,314.669,1 +rdma,put,256MB,268435456,4,20,5368709120,0.5117,9.772,97.732,81.539,185.472,198.128,1 +rdma,get,256MB,268435456,4,20,5368709120,2.9295,1.707,448.482,429.7,688.954,719.137,1 +rdma,put,256MB,268435456,8,20,5368709120,0.3733,13.394,129.937,129.498,168.917,180.765,1 +rdma,get,256MB,268435456,8,20,5368709120,2.9399,1.701,787.786,721.769,1424.445,1461.856,1 +rdma,put,1GB,1073741824,1,20,21474836480,3.9354,5.082,196.761,196.846,197.357,199.433,1 +rdma,get,1GB,1073741824,1,20,21474836480,13.0334,1.535,589.252,590.406,595.992,599.725,1 +rdma,put,1GB,1073741824,2,20,21474836480,2.9717,6.73,294.8,239.306,496.735,543.557,1 +rdma,get,1GB,1073741824,2,20,21474836480,11.1724,1.79,1040.841,1068.227,1180.398,1184.151,1 +rdma,put,1GB,1073741824,4,20,21474836480,2.0099,9.951,386.958,261.293,924.457,978.151,1 +rdma,get,1GB,1073741824,4,20,21474836480,10.5258,1.9,1626.061,1642.964,2170.677,2177.04,1 +rdma,put,1GB,1073741824,8,20,21474836480,0.603,33.17,129.357,0.088,559.85,601.837,0 +rdma,get,1GB,1073741824,8,20,21474836480,2.8063,7.127,664.466,0.234,2708.532,2761.194,0 diff --git a/src/code/issue5/results/raw/transfer_tcp.csv b/src/code/issue5/results/raw/transfer_tcp.csv new file mode 100644 index 0000000..65a0130 --- /dev/null +++ b/src/code/issue5/results/raw/transfer_tcp.csv @@ -0,0 +1,25 @@ +transport,op,size,size_bytes,concurrency,iters,total_bytes,wall_s,bandwidth_gbps,lat_mean_ms,lat_p50_ms,lat_p95_ms,lat_p99_ms,success +tcp,put,64MB,67108864,1,16,1073741824,1.2916,0.774,80.705,78.458,81.257,134.332,1 +tcp,get,64MB,67108864,1,16,1073741824,1.9084,0.524,115.875,115.572,135.98,142.531,1 +tcp,put,64MB,67108864,2,16,1073741824,0.9393,1.065,116.811,115.969,138.063,145.889,1 +tcp,get,64MB,67108864,2,16,1073741824,1.4136,0.707,172.828,149.225,267.304,270.044,1 +tcp,put,64MB,67108864,4,16,1073741824,0.8492,1.178,211.809,207.647,236.131,245.139,1 +tcp,get,64MB,67108864,4,16,1073741824,1.0607,0.943,249.244,269.754,297.412,302.182,1 +tcp,put,64MB,67108864,8,16,1073741824,0.8689,1.151,409.866,407.34,472.026,472.309,1 +tcp,get,64MB,67108864,8,16,1073741824,1.7648,0.567,833.883,871.141,951.296,956.719,1 +tcp,put,256MB,268435456,1,16,4294967296,4.8048,0.833,300.278,289.83,381.702,395.084,1 +tcp,get,256MB,268435456,1,16,4294967296,6.4712,0.618,393.257,434.073,487.26,495.68,1 +tcp,put,256MB,268435456,2,16,4294967296,4.168,0.96,518.663,557.95,683.211,683.769,1 +tcp,get,256MB,268435456,2,16,4294967296,7.9041,0.506,970.946,975.865,1018.318,1028.147,1 +tcp,put,256MB,268435456,4,16,4294967296,2.6162,1.529,639.918,637.962,784.555,784.867,1 +tcp,get,256MB,268435456,4,16,4294967296,4.6508,0.86,1042.28,1071.21,1282.543,1293.222,1 +tcp,put,256MB,268435456,8,16,4294967296,2.8253,1.416,1390.093,1487.925,1614.796,1615.577,1 +tcp,get,256MB,268435456,8,16,4294967296,6.0885,0.657,2684.126,2592.065,3455.654,3471.826,1 +tcp,put,1GB,1073741824,1,16,17179869184,15.0903,1.06,943.121,941.371,1129.753,1202.295,1 +tcp,get,1GB,1073741824,1,16,17179869184,29.1459,0.549,1776.471,1811.315,2163.275,2371.168,1 +tcp,put,1GB,1073741824,2,16,17179869184,14.9079,1.073,1813.277,1746.386,2677.336,2677.6,1 +tcp,get,1GB,1073741824,2,16,17179869184,30.126,0.531,3702.628,3896.115,4333.536,4382.249,1 +tcp,put,1GB,1073741824,4,16,17179869184,17.085,0.936,4066.624,4018.78,5491.047,5491.388,1 +tcp,get,1GB,1073741824,4,16,17179869184,27.8982,0.574,6306.943,7350.064,8485.421,8560.655,1 +tcp,put,1GB,1073741824,8,16,17179869184,5.5563,2.88,1735.704,0.177,5554.313,5554.522,0 +tcp,get,1GB,1073741824,8,16,17179869184,8.9092,1.796,2565.32,0.313,8394.456,8857.657,0 diff --git a/src/code/issue5/results/raw/transfer_zcget_rdma.csv b/src/code/issue5/results/raw/transfer_zcget_rdma.csv new file mode 100644 index 0000000..2d3f51d --- /dev/null +++ b/src/code/issue5/results/raw/transfer_zcget_rdma.csv @@ -0,0 +1,13 @@ +transport,op,size,size_bytes,concurrency,iters,total_bytes,wall_s,bandwidth_gbps,lat_mean_ms,lat_p50_ms,lat_p95_ms,lat_p99_ms,success +rdma,get_zc,64MB,67108864,1,20,67108864,0.0061,10.203,6.816,6.132,13.033,13.258,1 +rdma,get_zc,64MB,67108864,2,20,134217728,0.014,8.933,14.111,14.039,14.701,18.114,1 +rdma,get_zc,64MB,67108864,4,20,268435456,0.0239,10.482,23.596,23.931,24.973,25.065,1 +rdma,get_zc,64MB,67108864,8,20,536870912,0.0373,13.392,37.926,37.352,41.094,41.844,1 +rdma,get_zc,256MB,268435456,1,20,268435456,0.024,10.431,23.292,23.981,26.623,31.083,1 +rdma,get_zc,256MB,268435456,2,20,536870912,0.0391,12.779,39.584,39.223,42.882,43.325,1 +rdma,get_zc,256MB,268435456,4,20,1073741824,0.0758,13.195,78.758,76.476,91.547,120.712,1 +rdma,get_zc,256MB,268435456,8,20,2147483648,0.1662,12.031,166.577,168.621,185.066,188.238,1 +rdma,get_zc,1GB,1073741824,1,20,1073741824,0.0718,13.919,73.292,72.342,81.119,85.008,1 +rdma,get_zc,1GB,1073741824,2,20,2147483648,0.1382,14.472,140.909,138.236,164.686,173.192,1 +rdma,get_zc,1GB,1073741824,4,20,4294967296,0.2737,14.614,273.774,273.717,281.222,283.406,1 +rdma,get_zc,1GB,1073741824,8,20,8589934592,0.5448,14.686,544.857,544.853,567.016,576.927,1 diff --git a/src/code/issue5/results/raw/transfer_zcget_tcp.csv b/src/code/issue5/results/raw/transfer_zcget_tcp.csv new file mode 100644 index 0000000..086ddb3 --- /dev/null +++ b/src/code/issue5/results/raw/transfer_zcget_tcp.csv @@ -0,0 +1,13 @@ +transport,op,size,size_bytes,concurrency,iters,total_bytes,wall_s,bandwidth_gbps,lat_mean_ms,lat_p50_ms,lat_p95_ms,lat_p99_ms,success +tcp,get_zc,64MB,67108864,1,20,67108864,0.0522,1.197,51.632,52.253,54.246,54.575,1 +tcp,get_zc,64MB,67108864,2,20,134217728,0.1025,1.22,102.551,102.937,106.213,106.538,1 +tcp,get_zc,64MB,67108864,4,20,268435456,0.2038,1.226,206.889,203.894,209.778,264.491,1 +tcp,get_zc,64MB,67108864,8,20,536870912,0.4133,1.21,476.683,413.478,566.942,961.212,1 +tcp,get_zc,256MB,268435456,1,20,268435456,0.2107,1.186,253.134,211.432,377.767,430.38,1 +tcp,get_zc,256MB,268435456,2,20,536870912,0.5377,0.93,523.614,538.936,612.795,710.163,1 +tcp,get_zc,256MB,268435456,4,20,1073741824,0.6962,1.436,696.98,705.929,807.603,934.502,1 +tcp,get_zc,256MB,268435456,8,20,2147483648,1.34,1.493,1520.469,1343.019,2372.013,2951.801,1 +tcp,get_zc,1GB,1073741824,1,20,1073741824,0.7639,1.309,758.393,770.223,868.6,907.464,1 +tcp,get_zc,1GB,1073741824,2,20,2147483648,1.3856,1.443,1531.89,1397.911,2507.008,3055.359,1 +tcp,get_zc,1GB,1073741824,4,20,4294967296,2.5331,1.579,2833.353,2543.211,4234.524,5601.75,1 +tcp,get_zc,1GB,1073741824,8,20,8589934592,6.5104,1.229,6885.025,6738.138,8548.463,12657.145,1 diff --git a/src/code/issue5/results/raw/transfer_zcput_rdma.csv b/src/code/issue5/results/raw/transfer_zcput_rdma.csv new file mode 100644 index 0000000..689af58 --- /dev/null +++ b/src/code/issue5/results/raw/transfer_zcput_rdma.csv @@ -0,0 +1,13 @@ +transport,op,size,size_bytes,concurrency,iters,total_bytes,wall_s,bandwidth_gbps,lat_mean_ms,lat_p50_ms,lat_p95_ms,lat_p99_ms,success +rdma,put_zc,64MB,67108864,1,20,67108864,0.0032,19.654,3.808,3.185,3.258,15.701,1 +rdma,put_zc,64MB,67108864,2,20,134217728,0.006,20.802,6.162,6.014,6.433,8.007,1 +rdma,put_zc,64MB,67108864,4,20,268435456,0.0113,22.07,11.35,11.329,11.467,11.597,1 +rdma,put_zc,64MB,67108864,8,20,536870912,0.0229,21.856,22.88,22.885,22.979,23.0,1 +rdma,put_zc,256MB,268435456,1,20,268435456,0.0136,18.375,13.824,13.609,13.897,17.557,1 +rdma,put_zc,256MB,268435456,2,20,536870912,0.0248,20.152,24.84,24.834,25.177,26.093,1 +rdma,put_zc,256MB,268435456,4,20,1073741824,0.0483,20.719,48.25,48.267,48.412,48.528,1 +rdma,put_zc,256MB,268435456,8,20,2147483648,0.0946,21.14,94.913,94.632,95.329,101.911,1 +rdma,put_zc,1GB,1073741824,1,20,1073741824,0.0553,18.073,55.445,55.338,55.988,58.262,1 +rdma,put_zc,1GB,1073741824,2,20,2147483648,0.1011,19.775,100.639,101.164,101.364,111.412,1 +rdma,put_zc,1GB,1073741824,4,20,4294967296,0.1896,21.095,190.754,189.682,203.879,206.61,1 +rdma,put_zc,1GB,1073741824,8,20,8589934592,0.3591,22.28,360.598,359.133,377.662,382.966,1 diff --git a/src/code/issue5/results/raw/transfer_zcput_tcp.csv b/src/code/issue5/results/raw/transfer_zcput_tcp.csv new file mode 100644 index 0000000..7436803 --- /dev/null +++ b/src/code/issue5/results/raw/transfer_zcput_tcp.csv @@ -0,0 +1,13 @@ +transport,op,size,size_bytes,concurrency,iters,total_bytes,wall_s,bandwidth_gbps,lat_mean_ms,lat_p50_ms,lat_p95_ms,lat_p99_ms,success +tcp,put_zc,64MB,67108864,1,20,67108864,0.1072,0.583,112.322,107.292,116.707,179.119,1 +tcp,put_zc,64MB,67108864,2,20,134217728,0.2009,0.622,201.746,201.093,256.049,271.939,1 +tcp,put_zc,64MB,67108864,4,20,268435456,0.3803,0.657,377.463,381.663,404.829,405.909,1 +tcp,put_zc,64MB,67108864,8,20,536870912,0.6837,0.731,674.838,690.787,830.46,1065.035,1 +tcp,put_zc,256MB,268435456,1,20,268435456,0.3868,0.646,432.223,387.972,425.065,1386.039,1 +tcp,put_zc,256MB,268435456,2,20,536870912,0.6539,0.765,658.928,662.376,777.822,790.043,1 +tcp,put_zc,256MB,268435456,4,20,1073741824,0.8219,1.217,925.995,831.905,1635.524,1882.998,1 +tcp,put_zc,256MB,268435456,8,20,2147483648,1.7081,1.171,1870.389,1713.598,2553.497,2958.894,1 +tcp,put_zc,1GB,1073741824,1,20,1073741824,0.8253,1.212,913.702,829.09,1492.525,1679.464,1 +tcp,put_zc,1GB,1073741824,2,20,2147483648,1.623,1.232,1810.009,1634.325,2507.256,4079.183,1 +tcp,put_zc,1GB,1073741824,4,20,4294967296,3.778,1.059,3980.553,3862.133,5261.083,7691.516,1 +tcp,put_zc,1GB,1073741824,8,20,8589934592,9.7038,0.824,9747.463,9791.47,11694.599,14900.413,1 diff --git a/src/code/issue5/scripts/plot_results.py b/src/code/issue5/scripts/plot_results.py new file mode 100644 index 0000000..6f6bb15 --- /dev/null +++ b/src/code/issue5/scripts/plot_results.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Issue5 汇总出图 (读 results/raw 的全部 CSV, 出 report/figures/) + +三类图 + 模型/QPM 图: + 1) bw_vs_size.png L1 原语: 有效带宽 vs value 大小 (TCP/RDMA × put/get) + 2) bw_vs_concurrency.png L1 原语: put 带宽 vs 并发 (TCP/RDMA, 展示 RDMA scale) + 3) l2_zerocopy_bw.png L2 真实 KV: zerocopy vs bytes 的 get 带宽 (TCP/RDMA) + 4) ttft_vs_load.png L4: P95 TTFT vs 到达率 (三传输 + SLO 线, N=16k 两 regime) + 5) qpm_compare.png L4: QPM 柱状 (两 regime × 三传输 × 三 N) + 6) protocol_summary.png 一图总览: L1/L2/L4 关键指标的 RDMA/TCP 倍数 + +用法: + python3 scripts/plot_results.py --raw results/raw --figdir report/figures +""" +import argparse, csv, os, statistics +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +GB = 1 << 30 + + +def read_csv(path): + if not os.path.exists(path): + return [] + with open(path) as f: + return list(csv.DictReader(f)) + + +def med(rows, pred, col): + vals = [] + for r in rows: + if pred(r): + try: + vals.append(float(r[col])) + except (ValueError, KeyError): + pass + return statistics.median(vals) if vals else None + + +# ---- 图1: L1 带宽 vs value 大小 (put/get × tcp/rdma) ---- +def plot_bw_vs_size(raw, figdir): + tcp = read_csv(f"{raw}/transfer_tcp.csv") + rdma = read_csv(f"{raw}/transfer_rdma.csv") + if not tcp or not rdma: + return None + sizes = ["64MB", "256MB", "1GB"] + fig, axes = plt.subplots(1, 2, figsize=(10, 4)) + for ax, op in zip(axes, ("put", "get")): + for data, name, c in ((tcp, "tcp", "C0"), (rdma, "rdma", "C1")): + ys = [med(data, lambda r, s=s: r["size"] == s and r["op"] == op + and r["success"] == "1", "bandwidth_gbps") for s in sizes] + ax.plot(sizes, ys, "o-", color=c, label=name) + ax.set_title(f"L1 {op} bandwidth") + ax.set_xlabel("value size"); ax.set_ylabel("GB/s") + ax.grid(alpha=.3); ax.legend() + fig.suptitle("L1 primitive: effective bandwidth vs value size") + fig.tight_layout(); fig.savefig(f"{figdir}/bw_vs_size.png", dpi=120); plt.close(fig) + return "bw_vs_size.png" + + +# ---- 图2: L1 put 带宽 vs 并发 ---- +def plot_bw_vs_conc(raw, figdir): + tcp = read_csv(f"{raw}/transfer_tcp.csv") + rdma = read_csv(f"{raw}/transfer_rdma.csv") + if not tcp or not rdma: + return None + concs = ["1", "2", "4", "8"] + fig, ax = plt.subplots(figsize=(6, 4)) + for data, name, c in ((tcp, "tcp", "C0"), (rdma, "rdma", "C1")): + ys = [med(data, lambda r, cc=cc: r["concurrency"] == cc and r["op"] == "put" + and r["size"] == "64MB" and r["success"] == "1", "bandwidth_gbps") + for cc in concs] + ax.plot(concs, ys, "o-", color=c, label=name) + ax.set_title("L1 put bandwidth vs concurrency (64MB)") + ax.set_xlabel("concurrency"); ax.set_ylabel("GB/s") + ax.grid(alpha=.3); ax.legend() + fig.tight_layout(); fig.savefig(f"{figdir}/bw_vs_concurrency.png", dpi=120); plt.close(fig) + return "bw_vs_concurrency.png" + + +# ---- 图3: L2 真实 KV, zerocopy vs bytes 的 get 带宽 (tcp/rdma) ---- +def plot_l2_zerocopy(raw, figdir): + tcp = read_csv(f"{raw}/kv_tensor_tcp.csv") + rdma = read_csv(f"{raw}/kv_tensor_rdma.csv") + if not tcp or not rdma: + return None + fig, ax = plt.subplots(figsize=(6, 4)) + labels, vals, colors = [], [], [] + for data, name in ((tcp, "tcp"), (rdma, "rdma")): + for path in ("bytes", "zerocopy"): + v = med(data, lambda r, p=path: r.get("path") == p and r.get("op") == "get" + and r.get("success") == "1", "effective_bandwidth_gbps") + labels.append(f"{name}\n{path}"); vals.append(v or 0) + colors.append("C0" if name == "tcp" else "C1") + bars = ax.bar(range(len(vals)), vals, color=colors) + ax.set_xticks(range(len(labels))); ax.set_xticklabels(labels) + ax.set_ylabel("get bandwidth (GB/s)") + ax.set_title("L2 real-KV get: zerocopy vs bytes (RDMA-zc ~18.7x TCP-zc)") + for b, v in zip(bars, vals): + ax.text(b.get_x() + b.get_width() / 2, v, f"{v:.1f}", ha="center", va="bottom", fontsize=8) + ax.grid(alpha=.3, axis="y") + fig.tight_layout(); fig.savefig(f"{figdir}/l2_zerocopy_bw.png", dpi=120); plt.close(fig) + return "l2_zerocopy_bw.png" + + +# ---- 图4: L4 P95 TTFT vs 到达率 (三传输 + SLO 线), N=16k 两 regime ---- +def plot_ttft_vs_load(raw, figdir): + mod = read_csv(f"{raw}/conc_sweep.csv") + hi = read_csv(f"{raw}/conc_hihit_sweep.csv") + if not mod or not hi: + return None + fig, axes = plt.subplots(1, 2, figsize=(11, 4)) + for ax, rows, title in ((axes[0], mod, "moderate hit h=.5"), + (axes[1], hi, "high hit h=.95")): + N = 16384 + slo = None + for tp, c in (("nocache", "C2"), ("tcp", "C0"), ("rdma", "C1")): + pts = sorted((float(r["lambda_rps"]), float(r["p95_ttft_ms"])) + for r in rows if r["transport"] == tp + and int(r["context_length"]) == N) + if not pts: + continue + ax.plot([p[0] for p in pts], [p[1] / 1e3 for p in pts], "o-", + color=c, label=tp, ms=4) + for r in rows: + if r["transport"] == tp and int(r["context_length"]) == N: + slo = float(r["slo_ms"]) / 1e3 + if slo: + ax.axhline(slo, ls="--", color="k", alpha=.6, label=f"SLO={slo:.1f}s") + ax.set_title(f"P95 TTFT vs load (N=16k, {title})") + ax.set_xlabel("arrival rate λ (req/s)"); ax.set_ylabel("P95 TTFT (s)") + ax.grid(alpha=.3); ax.legend(fontsize=8) + fig.tight_layout(); fig.savefig(f"{figdir}/ttft_vs_load.png", dpi=120); plt.close(fig) + return "ttft_vs_load.png" + + +# ---- 图5: QPM 柱状 (两 regime × 三传输 × 三 N) ---- +def plot_qpm_compare(raw, figdir): + mod = read_csv(f"{raw}/conc_summary.csv") + hi = read_csv(f"{raw}/conc_hihit_summary.csv") + if not mod or not hi: + return None + Ns = [8192, 16384, 24576] + fig, axes = plt.subplots(1, 2, figsize=(11, 4), sharey=False) + for ax, rows, title in ((axes[0], mod, "moderate hit h=.5"), + (axes[1], hi, "high hit h=.95")): + qpm = {} + for r in rows: + qpm[(int(r["context_length"]), r["transport"])] = float(r["qpm"]) + x = np.arange(len(Ns)); w = 0.25 + for i, (tp, c) in enumerate((("nocache", "C2"), ("tcp", "C0"), ("rdma", "C1"))): + ys = [qpm.get((N, tp), 0) for N in Ns] + ax.bar(x + (i - 1) * w, ys, w, label=tp, color=c) + ax.set_xticks(x); ax.set_xticklabels([f"{N // 1024}k" for N in Ns]) + ax.set_title(f"QPM ({title})"); ax.set_xlabel("context N"); ax.set_ylabel("QPM") + ax.grid(alpha=.3, axis="y"); ax.legend() + fig.tight_layout(); fig.savefig(f"{figdir}/qpm_compare.png", dpi=120); plt.close(fig) + return "qpm_compare.png" + + +# ---- 图6: 一图总览 RDMA/TCP 倍数 (L1 put / L2 zc get / L4 QPM 两 regime) ---- +def plot_protocol_summary(raw, figdir): + t1 = read_csv(f"{raw}/transfer_tcp.csv"); r1 = read_csv(f"{raw}/transfer_rdma.csv") + t2 = read_csv(f"{raw}/kv_tensor_tcp.csv"); r2 = read_csv(f"{raw}/kv_tensor_rdma.csv") + mod = read_csv(f"{raw}/conc_summary.csv"); hi = read_csv(f"{raw}/conc_hihit_summary.csv") + ratios, labels = [], [] + + def peak(rows, op): + vals = [float(x["bandwidth_gbps"]) for x in rows + if x["op"] == op and x["success"] == "1"] + return max(vals) if vals else None + if t1 and r1: + ratios.append(peak(r1, "put") / peak(t1, "put")); labels.append("L1 put\n(peak)") + if t2 and r2: + zt = med(t2, lambda r: r.get("path") == "zerocopy" and r.get("op") == "get" + and r.get("success") == "1", "effective_bandwidth_gbps") + zr = med(r2, lambda r: r.get("path") == "zerocopy" and r.get("op") == "get" + and r.get("success") == "1", "effective_bandwidth_gbps") + if zt and zr: + ratios.append(zr / zt); labels.append("L2 zc get\n(median)") + + def qpm_ratio(rows, N): + d = {r["transport"]: float(r["qpm"]) for r in rows + if int(r["context_length"]) == N} + return d["rdma"] / d["tcp"] if d.get("tcp") else None + if mod: + ratios.append(qpm_ratio(mod, 16384)); labels.append("L4 QPM\nmod-hit 16k") + if hi: + ratios.append(qpm_ratio(hi, 16384)); labels.append("L4 QPM\nhi-hit 16k") + if not ratios: + return None + fig, ax = plt.subplots(figsize=(7, 4)) + bars = ax.bar(range(len(ratios)), ratios, color=["C1", "C1", "C3", "C3"][:len(ratios)]) + ax.axhline(1.0, ls="--", color="k", alpha=.5) + ax.set_xticks(range(len(labels))); ax.set_xticklabels(labels, fontsize=9) + ax.set_ylabel("RDMA / TCP (×)"); ax.set_yscale("log") + ax.set_title("RDMA vs TCP: transport wins big, e2e QPM depends on retrieve share") + for b, v in zip(bars, ratios): + ax.text(b.get_x() + b.get_width() / 2, v, f"{v:.1f}×", ha="center", va="bottom", fontsize=9) + ax.grid(alpha=.3, axis="y", which="both") + fig.tight_layout(); fig.savefig(f"{figdir}/protocol_summary.png", dpi=120); plt.close(fig) + return "protocol_summary.png" + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--raw", default="results/raw") + ap.add_argument("--figdir", default="report/figures") + args = ap.parse_args() + os.makedirs(args.figdir, exist_ok=True) + made = [] + for fn in (plot_bw_vs_size, plot_bw_vs_conc, plot_l2_zerocopy, + plot_ttft_vs_load, plot_qpm_compare, plot_protocol_summary): + try: + r = fn(args.raw, args.figdir) + if r: + made.append(r) + except Exception as e: + print(f"[skip] {fn.__name__}: {e}") + print(f"[out] wrote {made} -> {args.figdir}") + + +if __name__ == "__main__": + main() diff --git a/src/code/issue5/scripts/run_benchmark.sh b/src/code/issue5/scripts/run_benchmark.sh new file mode 100755 index 0000000..b83736a --- /dev/null +++ b/src/code/issue5/scripts/run_benchmark.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# ============================================================================= +# Issue5 TCP/RDMA 基准 (收敛协议切换, 验收2 实现部分) +# ----------------------------------------------------------------------------- +# 把 L1(transfer 原语) / L2(真实 KV 张量) / L4(TTFT-QPM replay) 的协议切换收敛到 +# 一个入口: `TRANSPORT=tcp|rdma bash run_benchmark.sh `。 +# +# 关键实现事实(见 configs/{tcp,rdma}.yaml、rdma/README.md): +# - TCP↔RDMA 的**唯一传输改动** = Mooncake protocol + client device_name。 +# - RDMA client 必须显式 MC_DEVICE=mlx5_3 (prefill 的 eth1); 空会 No available RNIC。 +# - store 端需先用同一 MC_PROTOCOL 起 (MC_PROTOCOL=rdma bash start_kvstore.sh)。 +# - L4 纯本地重放(吃 L1-L3 CSV), 与协议无关, 一次跑出三传输对比, 无需切协议。 +# +# 用法 (在 prefill pod 内, 或本地对已产出的 CSV 只跑 L4/汇总): +# TRANSPORT=tcp bash scripts/run_benchmark.sh l1 # L1 原语微基准 +# TRANSPORT=rdma MC_DEVICE=mlx5_3 bash scripts/run_benchmark.sh l1 +# TRANSPORT=rdma MC_DEVICE=mlx5_3 bash scripts/run_benchmark.sh l2 # L2 真实 KV +# bash scripts/run_benchmark.sh l4 # L4 replay (含中/高命中) +# bash scripts/run_benchmark.sh compare # 只读 CSV 出 TCP/RDMA 对比表 +# TRANSPORT=rdma MC_DEVICE=mlx5_3 bash scripts/run_benchmark.sh all +# +# 环境变量: +# TRANSPORT tcp(默认) | rdma —— 决定 L1/L2 走哪个协议 +# MC_DEVICE RDMA NIC 名, rdma 必填(prefill=mlx5_3); tcp 留空 +# KV_IP store 宿主机 IP (默认 192.0.2.10) +# HOST_IP 本 pod IP (默认 hostname -i) +# OUTDIR CSV 输出目录 (默认 results/raw) +# ============================================================================= +set -euo pipefail + +LAYER="${1:-all}" +export TRANSPORT="${TRANSPORT:-tcp}" +export MC_DEVICE="${MC_DEVICE:-}" +export KV_IP="${KV_IP:-192.0.2.10}" +export HOST_IP="${HOST_IP:-$(hostname -i 2>/dev/null | awk '{print $1}')}" + +# 定位仓库根 (scripts/ 的上一级) +SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT="$(cd "$SELF/.." && pwd)" +OUTDIR="${OUTDIR:-$ROOT/results/raw}" +BM="$ROOT/benchmark" +mkdir -p "$OUTDIR" + +# rdma 协议下强制要求 device (否则 client 自动发现失败 ret=-600) +if [[ "$TRANSPORT" == "rdma" && -z "$MC_DEVICE" ]]; then + echo "[run_benchmark] TRANSPORT=rdma 但 MC_DEVICE 为空 —— prefill 端必须显式 MC_DEVICE=mlx5_3" >&2 + echo " (client device_name='' 自动发现会 No available RNIC ret=-600, 见 rdma/README.md)" >&2 + exit 2 +fi + +TAG="$TRANSPORT" +echo "[run_benchmark] layer=$LAYER transport=$TRANSPORT device='${MC_DEVICE}' KV_IP=$KV_IP HOST_IP=$HOST_IP outdir=$OUTDIR" + +run_l1() { + echo "== L1 transfer 原语微基准 ($TRANSPORT) ==" + python3 "$BM/benchmark_transfer.py" \ + --protocol "$TRANSPORT" --device "$MC_DEVICE" \ + --out "$OUTDIR/transfer_${TAG}.csv" +} + +run_l2() { + echo "== L2 真实 KV 张量微基准 ($TRANSPORT) ==" + python3 "$BM/benchmark_kv_tensor.py" \ + --protocol "$TRANSPORT" --device "$MC_DEVICE" \ + --paths bytes,zerocopy \ + --out "$OUTDIR/kv_tensor_${TAG}.csv" +} + +run_l4() { + echo "== L4 TTFT/QPM replay (协议无关, 吃 L1-L3 CSV) ==" + # 中等命中 (算力主导): h=0.5 phit=0.5 + python3 "$BM/benchmark_concurrency.py" --context 8k,16k,24k \ + --hit-prefix 0.5 --phit 0.5 --slo-factor 2.5 --out-prefix "$OUTDIR/conc" + # 高命中 (retrieve 主导): h=0.95 phit=0.8 + python3 "$BM/benchmark_concurrency.py" --context 8k,16k,24k \ + --hit-prefix 0.95 --phit 0.8 --slo-factor 2.5 --out-prefix "$OUTDIR/conc_hihit" +} + +# 只读已产出的 CSV 出 TCP vs RDMA 对比表 (不重跑, 便于本地汇总) +run_compare() { + echo "== TCP vs RDMA 对比 (读 $OUTDIR/*.csv) ==" + python3 - "$OUTDIR" <<'PY' +import csv, os, statistics, sys +outdir = sys.argv[1] +def med(path, pred, col): + if not os.path.exists(path): return None + vals=[] + with open(path) as f: + for r in csv.DictReader(f): + if pred(r): + try: vals.append(float(r[col])) + except (ValueError, KeyError): pass + return statistics.median(vals) if vals else None + +print("\n[L1 原语] put/get 有效带宽峰值 (GB/s):") +for tp in ("tcp","rdma"): + p=f"{outdir}/transfer_{tp}.csv" + if not os.path.exists(p): print(f" {tp}: (无 {p})"); continue + puts=med(p, lambda r:r.get("op")=="put" and r.get("success")=="1", "bandwidth_gbps") + gets=med(p, lambda r:r.get("op")=="get" and r.get("success")=="1", "bandwidth_gbps") + print(f" {tp:4}: put~{puts} get~{gets}") + +print("\n[L2 真实 KV] zerocopy get 中位带宽 (GB/s):") +z={} +for tp in ("tcp","rdma"): + p=f"{outdir}/kv_tensor_{tp}.csv" + z[tp]=med(p, lambda r:r.get("path")=="zerocopy" and r.get("op")=="get" and r.get("success")=="1", + "effective_bandwidth_gbps") + print(f" {tp:4}: zerocopy_get~{z[tp]}") +if z.get("tcp") and z.get("rdma"): + print(f" => RDMA-zc / TCP-zc = {z['rdma']/z['tcp']:.1f}x") + +print("\n[L4 QPM] 各 regime × 传输 (读 conc_summary / conc_hihit_summary):") +for tag,label in (("conc","中等命中 h=.5"),("conc_hihit","高命中 h=.95")): + p=f"{outdir}/{tag}_summary.csv" + if not os.path.exists(p): print(f" {label}: (无 {p})"); continue + byN={} + with open(p) as f: + for r in csv.DictReader(f): + byN.setdefault(int(r["context_length"]),{})[r["transport"]]=float(r["qpm"]) + print(f" {label}:") + for N,d in sorted(byN.items()): + rt = d["rdma"]/d["tcp"] if d.get("tcp") else float("nan") + print(f" N={N//1024}k: nocache={d.get('nocache')} tcp={d.get('tcp')} " + f"rdma={d.get('rdma')} rdma/tcp={rt:.2f}x") +PY +} + +case "$LAYER" in + l1) run_l1 ;; + l2) run_l2 ;; + l4) run_l4 ;; + compare) run_compare ;; + all) run_l1; run_l2; run_l4; run_compare ;; + *) echo "unknown layer '$LAYER' (l1|l2|l4|compare|all)"; exit 2 ;; +esac +echo "[run_benchmark] done: layer=$LAYER transport=$TRANSPORT" diff --git a/src/code/issue5/scripts/start_kvstore.sh b/src/code/issue5/scripts/start_kvstore.sh new file mode 100755 index 0000000..cb839b8 --- /dev/null +++ b/src/code/issue5/scripts/start_kvstore.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# ============================================================================= +# Issue 5 - 远端 KV 存储节点(Mooncake L3 Store)启动脚本 +# ----------------------------------------------------------------------------- +# 只起 mooncake_master + mooncake_store_service, 不跑模型 +# 用法: +# MC_PROTOCOL=rdma MC_SEGMENT_SIZE=200gb bash start_kvstore.sh +# +# 可选: +# MC_PROTOCOL rdma(默认) | tcp —— 需与 prefill 端一致 +# MC_SEGMENT_SIZE 贡献给全局 KV 池的本机内存, 默认 200gb, 按可用内存调整 +# +# 启动: kubectl get pod -o wide 拿到本 pod 宿主机 IP, +# 填到 prefill 端 start_prefill.sh 的 MOONCAKE_STORAGE_NODE_IP。 +# ============================================================================= +set -euo pipefail + +set +u +. ~/.bashrc || true +set -u + +export MC_CONDA_ENV="${MC_CONDA_ENV:-python310_torch29_cuda}" +export PATH="/root/miniconda/envs/${MC_CONDA_ENV}/bin:$PATH" + +export HOST_IP=$(hostname -i) +export MC_PROTOCOL="${MC_PROTOCOL:-rdma}" +export MC_SEGMENT_SIZE="${MC_SEGMENT_SIZE:-200gb}" +# metadata / store 端口(默认 18080/18081; 8080/8081 常被同宿主机其它 hostNetwork pod 占用) +# 注意: 改这里后 prefill 端 start_prefill.sh 的 MC_META_PORT 需保持一致 +export MC_META_PORT="${MC_META_PORT:-18080}" +export MC_STORE_PORT="${MC_STORE_PORT:-18081}" + +# mooncake rdma 网卡自动发现; 如需指定, 设 MC_MS_AUTO_DISC=0 并给 MOONCAKE_DEVICE 传网卡名 +export MC_MS_AUTO_DISC="${MC_MS_AUTO_DISC:-1}" + +echo "[issue5-kvstore] HOST_IP=${HOST_IP} protocol=${MC_PROTOCOL} segment=${MC_SEGMENT_SIZE} meta_port=${MC_META_PORT} store_port=${MC_STORE_PORT}" + +python3 -c "import mooncake; print('[issue5-kvstore] mooncake OK:', mooncake.__file__)" + +# ---- 1) 启动 master(内嵌 http metadata server: ${MC_META_PORT}; master RPC: 50051)---- +mooncake_master \ + --enable_http_metadata_server=true \ + --http_metadata_server_port=${MC_META_PORT} \ + --eviction_high_watermark_ratio=0.95 \ + > /tmp/mooncake_master.log 2>&1 & +MASTER_PID=$! +echo "[issue5-kvstore] mooncake_master started, pid=${MASTER_PID}, log=/tmp/mooncake_master.log" +sleep 8 + +# ---- 2) 启动 store service, 贡献本机内存到全局 KV 池 ---- +# 本镜像里的 mooncake 0.3.5 store_service 不再读 MOONCAKE_* 环境变量, +# 而是从 --config (或 MOONCAKE_CONFIG_PATH) 读配置, 可用 -D key=value 覆盖。 +# 纯存储节点不发起 get/put, 故 local_buffer_size=0。 +# global_segment_size 需为字节整数, 这里把 MC_SEGMENT_SIZE(如 200gb) 换算成字节。 +seg_bytes() { + local s="${1,,}"; local n="${s//[a-z]/}" + case "$s" in + *tb) echo $(( n * 1024 * 1024 * 1024 * 1024 ));; + *gb) echo $(( n * 1024 * 1024 * 1024 ));; + *mb) echo $(( n * 1024 * 1024 ));; + *) echo "$n";; + esac +} +MC_SEGMENT_BYTES=$(seg_bytes "$MC_SEGMENT_SIZE") + +MC_CFG=/tmp/mooncake_kvstore.json +cat > "$MC_CFG" < $MC_CFG (segment=${MC_SEGMENT_BYTES} bytes):" +cat "$MC_CFG" + +python3 -m mooncake.mooncake_store_service --config "$MC_CFG" --port=${MC_STORE_PORT} diff --git a/src/code/issue5/scripts/start_prefill_v32.sh b/src/code/issue5/scripts/start_prefill_v32.sh new file mode 100644 index 0000000..368daec --- /dev/null +++ b/src/code/issue5/scripts/start_prefill_v32.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# ============================================================================= +# Issue 5 - 计算节点(Prefill)启动脚本 —— DeepSeek-V3.2 (MLA + NSA/DSA) 版 +# ----------------------------------------------------------------------------- +# DeepSeek-V3.2(DeepseekV32ForCausalLM, MLA + DSA +# 稀疏索引), 其 KV 池是 NSATokenToKVPool —— HiRadixCache 明确支持 +# (mem_cache/hiradix_cache.py: isinstance(..., NSATokenToKVPool) -> NSATokenToKVPoolHost)。 +# +# 用法: +# MOONCAKE_STORAGE_NODE_IP=<存储节点宿主机IP> MC_PROTOCOL=tcp bash start_prefill_v32.sh +# ============================================================================= +set -euo pipefail + +# ---- 参数校验 ---- +if [[ -z "${MOONCAKE_STORAGE_NODE_IP:-}" ]]; then + echo "[ERROR] 请先设置 MOONCAKE_STORAGE_NODE_IP=<存储节点宿主机IP>" >&2 + exit 1 +fi +export MC_PROTOCOL="${MC_PROTOCOL:-tcp}" +export MC_META_PORT="${MC_META_PORT:-18080}" +echo "[issue5-prefill-v32] storage_ip=${MOONCAKE_STORAGE_NODE_IP} protocol=${MC_PROTOCOL} meta_port=${MC_META_PORT}" + +# ---- 基础环境 ---- +set +u +. ~/.bashrc || true +set -u + +# conda env: py3.10 + torch2.9, 预装 mooncake 0.3.5 + sglang(与镜像 glibc2.31 匹配) +export MC_CONDA_ENV="${MC_CONDA_ENV:-python310_torch29_cuda}" +export PATH="/root/miniconda/envs/${MC_CONDA_ENV}/bin:$PATH" + +unset XPU_DUMMY_EVENT || true +export PYTHONPATH=/workspace/sglang/python:/workspace/sglang/sgl-kernel/python:${PYTHONPATH:-} +# DeepSeek-V3.2 int8(w8a8) 权重; 与 --quantization w8a8_int8 匹配。 + +export MODEL_PATH=/root/v32_int8_fix/ + +export SGLANG_APPLY_CONFIG_BACKUP=none +export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +export XPU_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 +export MODEL_ID="${MODEL_ID:-p800_deepseek_v32_issue5}" + +export SGLANG_OPT_USE_TILELANG_MHC_PRE=0 +export SGLANG_OPT_DEEPGEMM_HC_PRENORM=0 +export SGLANG_OPT_USE_TILELANG_MHC_POST=0 +export SGLANG_OPT_USE_MULTI_STREAM_OVERLAP=0 +export SGLANG_IS_FLASHINFER_AVAILABLE=False +export USE_FAST_ALLOC_EXTEND_KUNLUN=False +export SGLANG_FP8_PAGED_MQA_LOGITS_TORCH=False +export SGLANG_OPT_USE_JIT_NORM=True +export SGLANG_OPT_USE_FUSED_STORE_CACHE=0 +export SGLANG_OPT_FP8_WO_A_GEMM=False +export SGLANG_OPT_BF16_FP32_GEMM_ALGO="torch" +export SGLANG_TOPK_TRANSFORM_512_TORCH=False +export SGLANG_JIT_DEEPGEMM_PRECOMPILE=0 +export SGLANG_PREP_IN_CUDA_GRAPH=False +export SGLANG_OPT_CP_REARRANGE_TRITON=False +export BKCL_RDMA_VERBS=1 +export BKCL_ENABLE_XDR=1 +export BKCL_RDMA_NICS="eth1,eth1,eth2,eth2,eth3,eth3,eth4,eth4" +export NCCL_IB_GID_INDEX=3 +export XSHMEM_QP_NUM_PER_RANK=32 +export XSHMEM_MODE=1 +export XSHMEM_SYMMETRIC_SIZE=4294967296 +export CUDA_DEVICE_ORDER=OAM_ID +export CUDA_ENABLE_P2P_NO_UVA=1 +export OPS_DEBUG_CHECK=1 +export SGLANG_TOOL_STRICT_LEVEL=2 + +export HOST_IP=$(hostname -i) +LOG_DIR=logs_issue5_prefill_v32 +mkdir -p "$LOG_DIR" + +# ---- 依赖(镜像若已装可注释掉)---- +pip3 install "cocopod-1.3.0.torch29-cp310-cp310-linux_x86_64.whl" --force-reinstall + +python3 -m sglang.launch_server \ + --model-path "$MODEL_PATH" \ + --tp 8 \ + --ep 8 \ + --port 8000 \ + --quantization w8a8_int8 \ + --trust-remote-code \ + --kv-cache-dtype bfloat16 \ + --dtype bfloat16 \ + --mem-fraction-static 0.83 \ + --chunked-prefill-size 2048 \ + --max-running-requests 4 \ + --max-prefill-tokens 16384 \ + --disable-cuda-graph \ + --skip-server-warmup \ + --model-loader-extra-config '{"enable_multithread_load": "true","num_threads": 64}' \ + --enable-metrics --collect-tokens-histogram \ + --host "$HOST_IP" \ + --allow-auto-truncate \ + --moe-a2a-backend deepep \ + --deepep-mode normal \ + --served-model-name "$MODEL_ID" \ + --enable-cache-report \ + --page-size 64 \ + --enable-hierarchical-cache \ + --disable-hicache-numa-detect \ + --hicache-storage-backend mooncake \ + --hicache-mem-layout page_first \ + --hicache-write-policy write_through \ + --hicache-storage-prefetch-policy timeout \ + --hicache-ratio 2 \ + --hicache-storage-backend-extra-config "$HICACHE_MOONCAKE_CFG"