LMCache MP 模式 Trace 录制与回放:用二进制轨迹文件复现线上 KV 缓存负载
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
本篇技术指南讲解 LMCache 多进程(MP)模式下的 Trace 录制与回放机制:如何用lmcache server --trace-level storage把每个StorageManager公共 API 调用录制为可回放的二进制 trace 文件(.lct),再通过lmcache trace replay在一套全新配置的StorageManager上按原时序重放这些调用。读完本文,你将掌握回归复现、存储层延迟刻画与 L1/L2 配置调优的完整实操流程,并理解 trace 文件格式、录制器、回放驱动与观测管线在源码层面的工作原理。
功能定位:为什么需要可回放的二进制 Trace
LMCache MP 模式可以把每次StorageManager公共 API 调用记录到一个二进制trace 文件,随后用lmcache trace replay将这些调用重新对一台全新服务器发起。该特性面向三个典型场景:
- 回归排查(Regression hunting)——录制一段生产负载,然后对正在调查的构建版本回放,在离线环境复现 bug;
- 性能刻画(Performance characterization)——在贴近真实的存储级访问模式下测量 L1/L2 延迟分布,全程无需 vLLM 或 GPU;
- 配置调优(Configuration tuning)——对同一条 trace 分别回放到不同的 L1 容量、淘汰策略与 L2 adapter 上,在完全相同的输入下对比其行为差异。
需要特别澄清的是,trace 录制与--enable-tracing(OTel span)相互独立:OTel tracing 是把在线span 实时导出到 OTLP 端点用于线上观测;trace 录制则是把可回放的二进制文件持久化下来做离线分析。两者可以同时开启,互不干扰。
录制一条 Trace
录制默认关闭,只需给lmcache server加上--trace-level storage即可开启:
# 显式指定输出路径 lmcache server \ --l1-size-gb 100 --eviction-policy LRU \ --trace-level storage --trace-output /tmp/run.lct # 不指定路径时,自动落到 $TMPDIR 下带时间戳的文件 lmcache server \ --l1-size-gb 100 --eviction-policy LRU \ --trace-level storage # → INFO log: "trace recording enabled (level=storage); no # --trace-output given, writing to # /tmp/lmcache-trace-<pid>-<UTC>.lct"开启后照常驱动流量(vLLM 请求、benchmark 脚本等)。trace 文件会在收到SIGTERM时经 EventBus 停止路径被干净关闭,无需执行额外的--stop-tracing命令。
录制内容
- 每个被装饰的
StorageManager调用的完整限定名(例如StorageManager.reserve_write、StorageManager.submit_prefetch_task); - 每次调用的输入参数(
keys、layout_desc、mode、extra_count、external_request_id等); - 每次调用的 wall-clock 与 monotonic 时间戳;
- 文件头,包含文件格式版本、trace schema 版本、起始时间戳,以及当前
StorageManagerConfig的 SHA-256 摘要——回放端据此标记配置不匹配。
从源码看,录制是围绕@enable_tracing()装饰器实现的,storage_manager.py 中reserve_write、finish_write、finish_read_prefetched、submit_prefetch_task等公共方法均带有该装饰器;而read_prefetched_results是@contextmanager,其__enter__/__exit__通过装饰器无法包裹,由代码手动调用publish_call_event发布事件(见 decorator.py 中publish_call_event的设计说明)。装饰器在函数入口发布一条TRACE_CALL事件(仅输入),输出值与异常不捕获——回放时函数被重新真实执行并观察实时结果,这正是回放区别于简单日志的价值所在。
不录制内容
- KV tensor 字节:回放只演练簿记(bookkeeping)与控制器逻辑,回放时的 payload 一律为零值,因此即使长时运行,trace 文件体积也保持有界;
MPCacheServer、消息队列或 GPU 拷贝代码内部的调用:这些层不在storage这一 trace 级别的作用域内。
开销
- 关闭时:每次
StorageManager调用仅多一次布尔判断,实际零开销。对应源码是 decorator.py 中包装函数最前面的一次_tracing_enabled检查,参数自省只在 gate 打开时才进行; - 开启时:编码与文件 I/O 都发生在 EventBus drain 线程上,不在请求路径上,实际对请求延迟无可感知影响。对应实现见 recorder.py,其注释明确说明:编码(codec + msgpack)与磁盘写入在订阅回调内同步完成,EventBus drain 线程本身就承担了"第二工作线程"的角色。
检查一条 Trace
回放前,lmcache trace info会打印一屏摘要,用于确认这条 trace 覆盖的操作种类与时长是否符合预期:
lmcache trace info /tmp/run.lctTrace file: /tmp/run.lct level: storage format_version: 1 trace_schema_version: 1 duration: 226.691s sm_config_digest: 0f685d8a... total_records: 1318 ops: lmcache.v1.distributed.storage_manager.StorageManager.finish_read_prefetched: 133 lmcache.v1.distributed.storage_manager.StorageManager.finish_write: 349 lmcache.v1.distributed.storage_manager.StorageManager.read_prefetched_results.__enter__: 96 lmcache.v1.distributed.storage_manager.StorageManager.read_prefetched_results.__exit__: 96 lmcache.v1.distributed.storage_manager.StorageManager.reserve_write: 349 lmcache.v1.distributed.storage_manager.StorageManager.submit_prefetch_task: 295回放一条 Trace
lmcache trace replay FILE会把每条记录对一台全新的、由你通过 CLI 参数构建的StorageManager重新发起。关键点在于:回放侧配置由你决定,而不是从录制端复制——这正是该特性的核心价值:可以在完全相同的输入上对比不同的 L1/L2 配置。
最小调用形式:
lmcache trace replay /tmp/run.lct \ --l1-size-gb 100 --eviction-policy LRU--l1-size-gb与--eviction-policy为必填项,与lmcache server一致。server 接受的任何 storage-manager 参数在这里同样可用(--l2-adapter、--l1-use-lazy、--l2-store-policy等),完整列表通过lmcache trace replay --help查看。在 replay_command.py 中可以看到,replay 子命令直接复用add_storage_manager_args与add_observability_args,从而与 server 的参数面保持同步。
节拍控制(Pacing)
回放驱动始终遵守记录下来的调用间时序:通过time.sleep让每次 dispatch 对齐到记录的t_mono偏移。没有"尽可能快"模式——因为StorageManager的读写是异步的且调用间存在跨调用依赖(例如一次 retrieve 可能依赖更早的 L2 load 完成),压缩记录间隙会让内部队列竞态,导致不确定性的 retrieve 未命中。如果回放主机比录制主机慢,循环只会落后于记录的时间表。对应实现见 _driver.py:每次迭代计算t_wall_origin + record.t_mono作为目标时刻,尚未到达则 sleep 对齐。
输出
每次回放都会打印一张终端指标表,并默认导出按限定名聚合的 CSV:
=================== Trace Replay Result ====================== --------------------------- Overall -------------------------- Trace level: storage Records replayed: 1318 Records skipped: 0 Records failed: 0 Replay duration (s): 226.69 Config digest: match (0f685d8a) --------------------- Per-Op Latency (ms) -------------------- reserve_write count: 349 reserve_write mean: 0.16 reserve_write p50: 0.13 reserve_write p99: 0.93 ...额外的逐记录输出由下列标志控制:
| 标志 | 用途 |
|---|---|
--output-dir DIR | 聚合摘要文件的输出目录,默认当前目录 |
--no-csv | 跳过trace_replay_ops.csv导出 |
--json | 额外写出trace_replay_summary.json(每个限定名的 count / mean / p50 / p90 / p99 / min / max,以及总时长) |
--verbose | 每条记录向 stdout 打印一行[N/total] OK\|FAIL <qualname> (Xms),同时保留 INFO 日志 |
--jsonl-out PATH | 每条回放记录向PATH写一个 JSON 对象({qualname, latency_ms, failed}),便于事后分析 |
-q/--quiet | 抑制终端指标表,聚合文件仍照常写出 |
即使不加--verbose,驱动也会在 INFO 级别记录每次 dispatch:
[1/1318] OK lmcache...StorageManager.reserve_write (0.252ms) [2/1318] OK lmcache...StorageManager.finish_write (0.032ms) ...进度数字来自对 trace 文件的一次廉价预扫描,因此始终显示[N/total]而非单纯的递增计数器(见 replay_command.py)。需要说明的是,回放中因参数解码失败或无对应 handler 而被跳过的记录会计入Records skipped;若Records failed > 0,命令最终以非零退出码结束(见 replay_command.py)。
回放期间的观测与监控
回放驱动会在构造回放侧StorageManager之前初始化完整的观测 EventBus(见 _driver.py 与DEFAULT_REPLAY_OBS_CONFIG),因此回放期间 L1/L2 生命周期、淘汰 tick、store/retrieve 发布等内部事件会流过一条活跃的总线,标准订阅者(日志、指标、OTel tracing)都可以挂接上去。
lmcache trace replay支持与 server 相同的观测 CLI 标志:
| 标志 | 效果 |
|---|---|
--disable-observability | 完全关闭 EventBus,所有订阅者都不触发 |
--disable-metrics | 跳过 OTel 指标初始化与指标订阅者;只想看日志时可避免占用 Prometheus 端口 |
--disable-logging | 跳过日志订阅者 |
--enable-tracing | 启用 OTel span 订阅者,需要配合--otlp-endpoint |
--otlp-endpoint URL | 向 OTLP gRPC collector 导出指标/追踪(如http://localhost:4317);未设置时指标回退到进程内 Prometheus pull 端点 |
--prometheus-port PORT | pull 模式下 Prometheus/metrics端点端口,默认9090 |
--metrics-sample-rate FLOAT | 生命周期直方图的采样率;计数器始终统计全部事件 |
典型监控配置如下。
原始日志轨迹(SM/L1/L2 事件输出到 stdout):
LMCACHE_LOG_LEVEL=DEBUG lmcache trace replay /tmp/run.lct \ --l1-size-gb 100 --eviction-policy LRU \ --disable-metricsPrometheus 指标(pull 模式):
lmcache trace replay /tmp/run.lct \ --l1-size-gb 100 --eviction-policy LRU \ --prometheus-port 9095 # 在另一个终端抓取 http://localhost:9095/metricsOTel 指标 + 追踪导出到 collector:
lmcache trace replay /tmp/run.lct \ --l1-size-gb 100 --eviction-policy LRU \ --otlp-endpoint http://localhost:4317 \ --enable-tracing需要注意:--trace-level与--trace-output是仅录制端标志,lmcache trace replay不接受它们——回放永远不会写出新的 trace 文件。这一点在源码中有明确保障:replay_command.py 会把args.trace_level与args.trace_output静默覆写为None,使回放侧ObservabilityConfig永远不会尝试启动 recorder。
注意事项、提示与常见陷阱
回放环境不同导致 retrieve 未命中是预期行为。回放开始时 CLI 会打印一条醒目的警告横幅:
============================================================================== !! REPLAY ENVIRONMENT MISMATCH MAY CAUSE RETRIEVE MISSES !! ==============================================================================由于 KV payload 未被捕获、回放侧配置与主机速度可能不同于录制端,录制时命中的 retrieve 在回放时可能未命中——例如录制时某个异步 L2 load 在 retrieve 发起前已完成,而回放时它可能仍在飞行中。请把 retrieve 未命中计数当作关于回放环境的信号,而非 trace 本身的缺陷。该横幅的完整文本位于 replay_command.py。
Config-digest 不匹配只是提示性信息,不是致命错误。无论摘要是否匹配,回放都会照常进行。不匹配只说明回放侧StorageManagerConfig与录制时不同——这往往正是你想要的结果(在同一条 trace 上对比两套配置)。摘要对比基于同一算法:录制与回放两侧都用safe_storage_config_dict(recorder.py)把配置转为可 JSON 序列化的 dict,再做 SHA-256 哈希。
Prometheus 端口占用。server 的--prometheus-port默认9090。若lmcache trace replay与 server 同时运行、或同时跑两个回放,在相同端口上会失败。要么给次要运行传不同的--prometheus-port,要么用--disable-metrics。这也是 _driver.py 中DEFAULT_REPLAY_OBS_CONFIG默认关闭 metrics 的原因。
录制开销。录制发生在 EventBus drain 线程而非请求处理线程;gate 关闭(默认)时每次调用仅一次布尔判断,生产构建在录制关闭时无可测量成本。
Trace 文件未加密。ObjectKey的 chunk hash 等参数以明文写入。请像对待缓存 hash 日志一样对待 trace 文件。
向前兼容。文件头携带 format version 与 trace schema version,读取端拒绝未知版本而不是静默产出垃圾数据。被捕获 API 表面变化(被追踪方法新增参数、新 codec tag)会提升 schema 版本;帧结构变化提升 format version。对应实现见 reader.py,其中对 magic、format version、schema version 逐一校验,不匹配即抛错。
可扩展性。格式设计上可容纳未来的 trace 级别(mq、gpu)。在现有级别中新增一个被追踪方法,只需在录制端加装饰器、在回放端注册 handler,无需改动格式。
源码视角:Trace 文件的二进制格式
从 format.py 可以看到,trace 文件是一条"长度前缀 + msgpack"帧流:
[4字节大端帧长度][msgpack 帧] [4字节大端帧长度][msgpack 帧] ...第一帧恒为Header(含 magic 字节LMCT、format version、level、schema version、t_mono_start/t_wall_start起始时间戳、sm_config_json及其 SHA-256 摘要),其余帧均为Record(t_mono、t_wall、qualname、codec 编码后的args)。长度前缀让 reader 保持简单,且在本地文件系统上低于PIPE_BUF的帧可以原子写入、支持并发 appender。截断的尾部部分帧(如被 SIGKILL 中断)会被检测并以 WARNING 日志干净停止迭代(reader.py)。
回放是刻意设计为单线程的(_driver.py):recorder 按 EventBus drain 的顺序捕获调用,这本身就是并发生产调用的一个线性化序列;按同样顺序回放即可保持观测到的交错,无需重建线程身份。参数序列化依赖 codecs.py 中的类型编解码器注册表:msgpack 原生支持int、float、str、bytes、bool、None、list、tuple、dict,其余类型(如ObjectKey、PrefetchMode、MemoryLayoutDesc等 LMCache 特定类型)通过{"__t__": tag, "v": payload}包装显式编解码,录制端(编码)与回放端(解码)共享同一份注册表以保持格式与行为一致。回放结束后,驱动还会强制关闭 trace 中遗留的未配对上下文(如被截断的__enter__而没有对应__exit__),保证StorageManager处于一致状态(_driver.py)。
相关文档与测试
- 观测页中的 Trace Recording 章节(对应
docs/design/v1/mp_observability/目录下的观测设计文档)聚焦录制端标志的简短说明; - 仓库测试可作为行为契约参考:
tests/cli/commands/trace/test_driver.py覆盖回放驱动,tests/v1/mp_observability/trace/test_decorator.py与tests/v1/mp_observability/trace/test_recorder.py分别覆盖装饰器与录制器,适合在改动或集成时核对预期行为。
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考