LMCache MP 模式 HF Bucket L2 Adapter 实战指南:以 Hugging Face Buckets 构建持久化 KV Cache 分层存储
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
导读
本文聚焦 LMCache 多进程(MP)模式下的hfbucket二级存储(L2)适配器:它把 KV cache 对象直接落到 Hugging Face Buckets(huggingface_hub的 Buckets 存储后端)中,为最热的一级缓存(L1)之外的 warm/cold 层提供持久化、可跨实例共享的存储能力。读完本文,你将掌握hf://buckets/...句柄的解析规则、全部必填/可选配置项与典型 JSON 写法,理解其“daemon 线程上的 asyncio 事件循环 + 有界线程池”异步架构、批量写入非事务性下的失败调和机制,以及如何借助容量计量开启逐出(eviction)策略,把 HF Buckets 正确接入 LMCache 的分层 KV 缓存体系。
HF Bucket L2 适配器是什么
在 LMCache MP 模式中,KV cache 采用两层存储架构:L1(快层)负责活跃 chunk 的内存驻留,L2(持久层)负责跨生命周期、跨实例的持久化。官方文档将 L2 定义为 durable storage backends,并允许通过可重复的--l2-adapter参数挂载多个后端(参见 二级存储总览)。
hfbucket就是这样一个 L2 适配器:它把 KV cache 对象存储在 Hugging Face Bucket(Hugging Face Hub 提供的一种对象存储,API 形式与 S3/GCS 类似)中,全部通过huggingface_hub的 Buckets API 完成读写。从文档定位看(docs/source/mp/l2_storage/hfbucket.rst),它属于持久化远程后端,最适合 warm 与 cold 缓存层;而最热的一级缓存层应优先选择更低延迟的本地适配器。
与 LMCache 中其他远程后端一致,hfbucket同时存在于两条接入路径中:
- MP 模式 L2 适配器:实现于 lmcache/v1/distributed/l2_adapters/hfbucket_l2_adapter.py,由
StoreController/PrefetchController驱动,通过submit_store_task/submit_lookup_and_lock_task/submit_load_task异步完成任务; - Connector(单进程/传统路径):实现于 lmcache/v1/storage_backend/connector/hfbucket_connector.py,通过 hfbucket_adapter.py 以
plugin://hfbucket[...]URL 的形式接入。
两个实现共享同一套句柄解析、对象命名与 token 解析逻辑,本文以 MP 模式的 L2 适配器为主展开,并在涉及 connector 时给出对应源码佐证。
句柄解析:bucket_handle 的格式与语义
bucket_handle是唯一必填字段,格式为:
hf://buckets/<namespace>/<bucket>[/<prefix>]解析逻辑位于 hfbucket_connector.py 的parse_hfbucket_handle:
- 必须以
hf://buckets/开头,否则抛出ValueError; - 去除前缀后按
/切分路径; - 前两段拼接为 bucket 标识
namespace/bucket(即bucket_id); - 剩余部分(可省略)作为
object_prefix(对象前缀,用于在同一 bucket 内隔离不同用途/租户的数据)。
例如句柄hf://buckets/my-org/lmcache-kv/prod会被解析为bucket_id = "my-org/lmcache-kv"、object_prefix = "prod"。从源码看,前缀是可选的:_object_key_to_bucket_path在有前缀时生成prod/<encoded_object_name>,无前缀时直接使用编码后的对象名。
对象命名:从 MP ObjectKey 到 Bucket 对象路径
HFBucket 中每个 KV chunk 的对象名由 MP 模式的ObjectKey派生,格式为:
<model>@<kv_rank_hex>@<chunk_hash_hex>[@<cache_salt>]序列化实现在 hfbucket_l2_adapter.py 的_object_key_to_string。对照测试 tests/v1/distributed/test_hfbucket_l2_adapter.py,可确认各字段的实际格式:
| 字段 | 说明 | 示例 |
|---|---|---|
model | 模型名 | llama |
kv_rank | KV 层 rank,8 位小写十六进制 | 000000ff |
object_group_id | 对象组 ID,十六进制(未显式设置时为0) | 0 |
chunk_hash | chunk 哈希的十六进制串 | 00010203 |
cache_salt | 可选的缓存盐,追加在末尾 | user-42 |
测试test_format断言llama@000000ff@0@00010203,test_cache_salt_appended断言带盐后为llama@000000ff@0@00010203@user-42。cache_salt 的作用是隔离不同租户/用户之间内容相同的 token chunk,避免在同一个 bucket 中发生碰撞。
对象名随后经encode_hfbucket_object_name做标准 URL 编码(quote(key_str, safe=""),见 hfbucket_connector.py),再拼接可选的前缀,得到最终的 bucket 对象路径。测试test_bucket_path_uses_prefix_and_encoding验证了路径以prod/开头且剩余部分不含/。
配置字段详解
除必填的bucket_handle外,hfbucket适配器还提供以下可选字段(默认值取自 hfbucket_l2_adapter.py 的HFBucketL2AdapterConfig及其from_dict校验逻辑):
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
token_env | string | "HF_TOKEN" | 用于解析 Hugging Face 访问令牌的环境变量名;环境变量优先于直接令牌 |
token | string | 无 | 直接指定的令牌兜底,仅在token_env对应环境变量未设置时使用 |
create_bucket_if_missing | bool | false | 首次 store 时惰性创建 bucket,而不是要求它预先存在 |
download_tmp_dir | string | 系统临时目录下的lmcache-hfbucket-mp | 临时下载根目录(MP 适配器特意与 connector 的lmcache-hfbucket区分,避免冲突) |
metadata_cache_ttl_secs | float | 30.0 | 支撑 lookup 与用量统计的 path-size 元数据缓存 TTL(秒) |
num_workers | int | 4 | 执行阻塞型 Hugging Face Hub API 调用的工作线程数 |
max_capacity_gb | float | 0.0 | get_usage()使用的聚合容量;为0时禁用聚合逐出 |
eviction | dict | 无 | 可选的逐出策略,格式与L2AdapterConfigBase一致 |
从from_dict的校验实现可以看出以下约束,这些约束在配置错误时会直接以ValueError报错:
bucket_handle必须是非空字符串,且必须以hf://buckets/开头;num_workers必须是正整数(布尔值会被拒绝,num_workers <= 0也会报错);metadata_cache_ttl_secs与max_capacity_gb必须是非负数值(布尔值会被拒绝);create_bucket_if_missing必须是严格布尔值——测试test_from_dict_rejects_string_boolean验证了传入字符串"false"会抛出ValueError;- 其余所有
--l2-adapter公共字段同样适用,例如"shared": true(多实例共享同一 bucket 时建议声明,详见 二级存储总览 中关于 coordinator 事件上报与按 adapter 类型建立 key 池的说明)。
eviction子对象(由 config.py 的_parse_eviction_config解析)支持以下字段:eviction_policy(必填,"LRU"/"IsolatedLRU"/"noop")、trigger_watermark(默认0.8)、eviction_ratio(默认0.2)。
配置示例
以下三个示例完整继承自原文档(docs/source/mp/l2_storage/hfbucket.rst),可直接用于lmcache server的--l2-adapter参数:
# 最小配置:使用已存在的 bucket,令牌从 $HF_TOKEN 解析 --l2-adapter '{"type": "hfbucket", "bucket_handle": "hf://buckets/my-org/lmcache-kv/prod"}' # 首次 store 时自动创建 bucket,并将工作线程池扩到 8 --l2-adapter '{"type": "hfbucket", "bucket_handle": "hf://buckets/my-org/lmcache-kv/prod", "create_bucket_if_missing": true, "num_workers": 8}' # 启用聚合逐出并设置容量上限 50GB --l2-adapter '{"type": "hfbucket", "bucket_handle": "hf://buckets/my-org/lmcache-kv/prod", "max_capacity_gb": 50, "eviction": {"eviction_policy": "LRU", "trigger_watermark": 0.9, "eviction_ratio": 0.1}}'第三个示例的含义:当get_usage()报告的使用率超过 90%(trigger_watermark)时,逐出控制器按 LRU 策略回收约 10% 的已用容量(eviction_ratio)。该策略与 L1 的逐出相互独立——每个 L2 适配器实例拥有自己独立的逐出控制器与策略。
多适配器级联时,可重复--l2-adapter参数,各适配器按命令行中的顺序生效:StoreController会向所有适配器写入,PrefetchController在 lookup 时按顺序查询(详见 二级存储总览)。
源码实现原理:事件循环 + 有界线程池
文档明确承诺:L2 controller 线程绝不会被网络 I/O 阻塞。其实现方式是“daemon 线程上的 asyncio 事件循环 + 有界线程池”的组合,见 hfbucket_l2_adapter.py:
self._executor = ThreadPoolExecutor( max_workers=config.num_workers, thread_name_prefix="hfbucket-l2", ) self._loop = asyncio.new_event_loop() self._loop_thread = threading.Thread( target=self._run_event_loop, daemon=True, name="hfbucket-l2-adapter-loop", ) self._loop_thread.start()调用方通过submit_store_task/submit_lookup_and_lock_task/submit_load_task提交任务,内部用asyncio.run_coroutine_threadsafe把协程投递到事件循环;协程再通过loop.run_in_executor(self._executor, ...)把阻塞的 Hugging Face API 调用(upload_files/download_files/get_paths_info/delete_files)交给线程池执行。任务完成后写入_completed_*_tasks字典,并通过三个独立的 eventfd(_store_efd/_lookup_efd/_load_efd)通知外部控制器(对应get_store_event_fd/get_lookup_and_lock_event_fd/get_load_event_fd,测试test_three_distinct_fds验证了三个 fd 互不相同)。
底层 bucket 客户端HFBucketClient(hfbucket_connector.py)是huggingface_hub的轻量同步包装:
| 方法 | 底层 API | 用途 |
|---|---|---|
create_bucket | HfApi.create_bucket(bucket_id, exist_ok=True) | 惰性建桶 |
get_paths_info | HfApi.get_bucket_paths_info | 批量查询对象大小(lookup / 计量) |
list_tree | HfApi.list_bucket_tree(recursive=True, prefix=...) | 前缀递归列举 |
upload_files | HfApi.batch_bucket_files(add=...) | 单次批量上传 |
download_files | HfApi.download_bucket_files(files=...) | 批量下载 |
delete_files | HfApi.batch_bucket_files(delete=...) | 批量删除 |
Connector 同样将阻塞调用通过asyncio.to_thread抛出(见 hfbucket_connector.py),保持异步接口不阻塞事件循环。
部分失败调和:Hugging Face 批量写入的非事务性处理
文档特别强调:Hugging Face 的批量写入不是事务性的——一个批量请求可能只写入了部分对象后失败。为此,MP 适配器实现了失败调和机制:
_store_batch_sync先把(bytes, object_path)组装成additions,一次调用upload_files(hfbucket_l2_adapter.py);- 若上传抛异常,捕获后调用
_reconcile_failed_store:通过get_paths_info重新拉取这批对象路径的真实元数据(hfbucket_l2_adapter.py); - 凡是实际落盘的对象(后端能查到且 size > 0),仍会写入
_key_sizes用量表并计入 usage accounting,同时更新元数据缓存——确保后续删除与容量统计不遗漏; - 最后抛出
_PartialStoreFailure,把已落盘的对象列表带给上层,_execute_store据此生成L2StoreResult(success=False, bytes_transferred=...),即任务被标记为失败,但已写入的字节数仍被正确上报。
测试test_partial_store_failure_accounts_written_keys验证了这一行为:注入fail_upload_after = 1使批量上传在写入第一个对象后失败,随后断言第一个对象确实存在于 fake bucket 中、第二个不存在,且get_usage().total_bytes_used == 64(一个 16 元素 float32 对象的字节数)。
connector 路径的处理方式类似:_batched_put_sync上传失败时会刷新各 key 的对象大小缓存后再向上抛出(hfbucket_connector.py)。
容量计量与逐出
HFBucketL2Adapter继承L2AdapterInterface,构造时以max_capacity_gb * 1024**3计算字节容量(hfbucket_l2_adapter.py)。get_usage()的行为与 s3 适配器一致:
max_capacity_gb = 0时禁用聚合逐出,usage_fraction == -1.0(测试test_disabled_returns_minus_one断言了这一点);- 设置非零
max_capacity_gb后,get_usage()报告total_bytes_used与total_capacity_bytes,触发水位驱动的逐出控制器(测试test_usage_grows_on_store_and_shrinks_on_delete验证了 store 增长、delete 归零的完整链路)。
在 lock/delete 语义上,submit_lookup_and_lock_task命中时会给 key 的引用计数加一(_locked_keys),submit_unlock递减;delete只删除引用计数为 0 的 key。测试test_lock_blocks_delete与test_refcount_unlock验证了:处于锁定(in-flight load)的 key 不会被删除,且引用计数为多次 lookup 叠加。这与 二级存储总览 中“hfbucket 的 delete 删除 bucket 对象并释放聚合字节计量、锁定 key 跳过”的 L2 逐出支持描述一致。
此外,对象的 size 查询走 TTL 元数据缓存(默认 30 秒),每 128 次更新触发一次过期条目清理(_METADATA_CACHE_PRUNE_INTERVAL = 128),避免高频 lookup 反复打后端 API。
使用前提与注意事项
- 依赖版本:底层
HFBucketClient构造时会校验huggingface_hub版本,低于1.5.0或缺少batch_bucket_files、download_bucket_files、list_bucket_tree、bucket_info、create_bucket、get_bucket_paths_info等 Buckets API 时会抛出明确的运行时错误(hfbucket_connector.py),请确保安装huggingface_hub>=1.5.0; - 令牌解析优先级:
token_env指定的环境变量优先,其次才是token字段;两者都为空时以匿名身份访问(hfbucket_l2_adapter.py); - Connector 仅支持完整 chunk:
hfbucketconnector 只支持 full chunks,save_chunk_meta与save_unfull_chunk必须为False(缺失时save_chunk_meta会被归一化为False,见 hfbucket_connector.py);上传/下载时会校验对象大小与full_chunk_size_bytes是否一致,不一致即拒绝并记录错误日志; - 临时下载目录:load 时对象先批量下载到
download_tmp_dir下的会话子目录,再拷入 L1 的MemoryObj缓冲区,批次结束后立即清理(shutil.rmtree);适配器关闭时也会清理整个会话目录,测试test_close_cleans_temp_dir验证了这一点; - 定位建议:HF Bucket 是持久化远程后端,网络往返延迟明显高于本地存储,适合 warm/cold 层与跨实例共享场景;最热层应交给 L1 或低延迟本地 L2 适配器(如 NIXL/POSIX、fs)。
验证与调试
仓库为 MP 适配器提供了完整的单元测试(tests/v1/distributed/test_hfbucket_l2_adapter.py),覆盖对象名序列化、store/lookup/load 全流程往返(test_roundtrip_single_key)、部分命中(test_partial_hits)、load 大小不匹配拒绝(test_load_size_mismatch_returns_zero_bit)、部分失败调和、锁定删除语义、用量统计与监听器回调(test_stored_accessed_and_deleted_fire)等场景,可作为理解适配器契约的参考。
实际部署时,可在启动lmcache server时设置LMCACHE_LOG_LEVEL=DEBUG观察 L2 活动(参见 二级存储总览 的“Verifying L2 Storage”一节),并调用report_status()(hfbucket_l2_adapter.py)查看当前 adapter 的健康状态、bucket_id、前缀、已存对象数与用量——健康检查基于事件循环线程存活且未关闭,返回is_healthy、stored_object_count、current_size_bytes、max_capacity_bytes等字段,方便接入监控与告警。
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考