Skip to content

Commit dbe8064

Browse files
committed
[manager] bound P2P host cache queries and preserve APIs
Preserve the existing HostCacheMatch and location-spec contracts by adding an explicit per-key filter. Replace request-wide P2P map/set graphs with ordered bounded reducers, keep non-progressive backends at one read, harden metrics and availability fallbacks, and add directed and million-key benchmark coverage.
1 parent dbe01da commit dbe8064

20 files changed

Lines changed: 1600 additions & 536 deletions

docs/api/report_event.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -619,7 +619,9 @@ HTTP 接口为 `POST /api/getCacheLocation`:
619619
`ST_EVENT_REPORT_L1P5``ST_EVENT_REPORT_L2` 等 backend,适合验证两种 EventReport storage 的
620620
隔离状态。
621621

622-
`location_spec_names` 不只是返回结果的投影条件,也是 backend/peer 选择前按 query key 生效的候选条件:
622+
`location_spec_names` 保持原有语义:它是应用到每个 query key 的全局返回投影;为空时返回
623+
选中 location 的全部 spec。需要在 backend/peer 选择前按 query key 限制候选时,使用新增的
624+
`per_key_location_spec_names`
623625

624626
- 为空时,location 中任意合法 spec 都可使该 location 成为候选;
625627
- 非空时,数组长度必须等于 query key 数量,且每个 name 都不能为空;第 i 个 name 只过滤第 i 个 key;
@@ -631,7 +633,7 @@ HTTP 接口为 `POST /api/getCacheLocation`:
631633

632634
因此 spec name 是 reporter 与查询方之间的稳定协议字段,不能用 object size 代替:不同 cache
633635
group 即使 byte size 相同,也必须使用不同且稳定的 spec name。调用方必须让
634-
`location_spec_names``block_keys`(或由 token 生成的 query keys)同序对齐。同一个 block key
636+
`per_key_location_spec_names``block_keys`(或由 token 生成的 query keys)同序对齐。同一个 block key
635637
可以在不同位置重复并请求不同 spec,用于 mixed-attention/Mamba groups。长度不匹配或包含空
636638
name 会返回 `INVALID_ARGUMENT`。确定性 tie-break 只消除无序遍历造成的抖动;各 key 经过
637639
spec 过滤后的候选 peer 集合仍可能不同。
@@ -662,7 +664,7 @@ HTTP 接口为 `POST /api/getHostCacheState`:
662664
"hosts": [
663665
{
664666
"host_ip_port": "10.0.0.8:8080",
665-
"local": "3",
667+
"prefix_match_blocks": "3",
666668
"p2p_1_fetch": "0",
667669
"p2p_1_total_match": "3"
668670
}
@@ -680,17 +682,17 @@ HTTP 接口为 `POST /api/getHostCacheState`:
680682
- `QT_UNSPECIFIED` 使用 RegisterInstance 时配置的 `default_query_type`
681683
- 支持 `QT_PREFIX_MATCH``QT_PREFIX_MATCH_WITH_MAMBA`,其他类型返回参数错误;
682684
- 同一个 host 在多个 backend 的有效 cache 会按 host 汇总参与匹配;
683-
- `local` 包含同一 host 的 subscriber 与 Vineyard 上报;
684-
- 非混合注意力对 full local-miss 使用 Prefix 选择远端 Vineyard;混合注意力先对
685+
- `prefix_match_blocks` 包含同一 host 的 subscriber 与 Vineyard 上报;
686+
- 非混合注意力对 full local miss 使用 Prefix 选择远端 Vineyard;混合注意力先对
685687
FullAttention group 使用 Prefix,再对 Mamba local-miss spec 使用 Coverage;
686688
- `p2p_1_fetch` 表示各 P2P 阶段实际选中并拉取的 spec 所属的去重 block key 数;
687689
- `p2p_1_total_match` 表示本地 cache 与实际选中的远端 spec 合并后的最终前缀;
688-
- 远端 P2P 候选只使用 `ST_EVENT_REPORT_L2`,且不会让 `local` 为 0 的 host 出现在响应中;
690+
- 远端 P2P 候选只使用 `ST_EVENT_REPORT_L2`,且不会让 `prefix_match_blocks` 为 0 的 host 出现在响应中;
689691
- reporter unavailable 时,该 host 对应的 event-report location 不参与匹配。
690692

691693
成功完整 snapshot 后,`GetHostCacheState` 会立即忽略完全属于旧 generation 的 location。
692694
snapshot 失败、KVCM 重启恢复或 realtime-only reporter 仍使用 soft metadata,因此
693-
`local` 在这些模式下仍可能是 false positive。
695+
`prefix_match_blocks` 在这些模式下仍可能是 false positive。
694696

695697
## 12. 节点生命周期与查询
696698

docs/design/report_event_performance.md

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,9 @@ instance group、data-storage backend 和 reporter node lock。当前实现做
9898
10. prefix 只把首个 `EC_NOENT` 当作正常终止。首个 miss 之后的 speculative read 结果不影响已经确定的
9999
前缀;但 miss 之前的 `EC_ERROR``EC_MISMATCH` 等硬错误必须原样返回,不能伪装成较短的 cache miss。
100100
普通 prefix 与 Mamba 路径遵循相同规则;
101-
11. 上述渐进取消只用于 `p2p_host_count=0`。启用 P2P 时 local miss 之后仍可能由 Vineyard peer 延续覆盖,
102-
因此当前保留完整 metadata materialization;它仍执行 serving 过滤和错误传播,但不能套用 local-only stop。
101+
11. `p2p_host_count>0` 不能把 local miss 直接当作全局 stop,因为 Vineyard peer 仍可能延续覆盖。该路径改用
102+
ordered compact visitor:普通 prefix 在线维护候选 peer 的前缀交集;Mamba 分 local、peer plan、最终投影三个
103+
有界 pass。非 local backend 只读取一次 compact batch 并在内存中重放,均不再构造逐 key 的 `map/set` 图。
103104

104105
可见性快照在首个有界 metadata range 读取之后、其 projection 开始时采集。采集前已经可见的 HOST_DOWN
105106
会被当前请求过滤;采集后的 HOST_DOWN 允许当前请求继续看到旧状态,但采集完成后本请求不再变化,下一
@@ -1704,9 +1705,10 @@ pure-local Release 验证结果:
17041705
批次会在百万 key 请求中重复约 24 万次 lookup/release shard lock;增大后续批次可摊薄 lock/unlock,同时首窗口
17051706
仍保持早停上界。16384 相对 4096 的同机 A/B 将 1M metadata/all-hit p50 从约 109.3/114.9ms 降至
17061707
94.3/103.7ms;32768 没有进一步改善 all-hit 且放大后缀过读,已撤销。
1707-
3. `p2p_host_count>0` 暂时保留完整读取。一个 host 的 local prefix 已停止时,Vineyard peer 仍可能继续覆盖后续 key,
1708-
不能把 local miss 直接当作全局取消条件。该路径仍获得 serving 状态过滤、一次 host/spec 扫描和严格错误传播,
1709-
但没有伪装成渐进路径;后续若优化,必须先设计 peer-aware stop 证明。
1708+
3. `p2p_host_count>0` 使用 peer-aware ordered reduction。普通 prefix 只为最终 top-N host 保留 peer 交集和精确
1709+
fetched-key 计数;Mamba 用三个有界 pass 分别求 local 状态、各 full group 的 prefix peer 与 state coverage peer、
1710+
最终合并结果。local backend 的常驻投影状态与 key 数无关(协议要求的去重 fetched key 向量除外);不支持
1711+
progressive read 的 backend 保留一次 compact batch,后续 pass 只重放该 batch,避免重复远端 I/O。
17101712
4. request-specific checker 除返回可见/不可见外,同时借用返回已经解析的 reporter medium/host,并声明 EventReport URI
17111713
已通过 generation fence 校验。projection 因而不再对同一 location 重复拆 location id、解析 URI 和提取 host;
17121714
medium filter 使用借用的 `string_view` hash set,generic URI path 也不再复制 path string。
@@ -1753,8 +1755,8 @@ location 指针带出 item lock 虽可少一次 owner add-ref,但 ReportEvent
17531755
- serving/WRITING/DELETING/NEW/NOT_FOUND 全状态矩阵在 p2p=0 和 p2p>0 下结果一致;Manager 端到端验证
17541756
StartWrite 在 Finish 前不可见;
17551757
- ordinary/Mamba 覆盖 first-window stop、第二并行 range timeout、较早 visitor stop 屏蔽无关后续 timeout、70 host
1756-
多 presence word、75 个 required spec 跨 word,以及 40K key 跨 4096/16384 边界;Mamba fast path 与保留的
1757-
full-materialize path 逐项差分;极端 `chunk_size=SIZE_MAX` 也会被安全收敛为单个 suffix range,不发生整数回绕;
1758+
多 presence word、75 个 required spec 跨 word,以及 40K key 跨 4096/16384 边界;极端
1759+
`chunk_size=SIZE_MAX` 也会被安全收敛为单个 suffix range,不发生整数回绕;
17581760
- 上述并行/生命周期重点用例 20 轮重复通过;MetaSearcherTest、CacheManagerTest 全量通过;
17591761
- `bazel test --config=release --nocache_test_results //kv_cache_manager/...` 为 106 个测试通过,1 个 GPU-only 测试
17601762
按预期 skip;
@@ -1775,8 +1777,8 @@ notify/join,不改变运行期 heartbeat、ReportEvent 或查询锁范围。
17751777
不再出现长等待。
17761778

17771779
local compact read 还去掉了一个确定的逐 key 冗余:未配置 revisit histogram 时不再读取
1778-
`last_access_time` atomic;访问时间仍按原语义更新,启用 histogram 时采样逻辑完全不变。另修正混合压测器读取已经
1779-
废弃的 `prefix_match_blocks` 字段所造成的假失败,改为校验当前协议的 `local` 字段
1780+
`last_access_time` atomic;访问时间仍按原语义更新,启用 histogram 时采样逻辑完全不变。后续兼容性复核确认
1781+
`prefix_match_blocks` 是既有 JSON 字段名,因此保留 tag 2 并恢复该名称,而不是继续暴露临时改名后的 `local`
17801782

17811783
最终 Release、pure-local 真实 HTTP 复核:
17821784

@@ -1817,10 +1819,9 @@ GPU-only 测试按预期跳过。上述 mixed 结果也说明渐进 query 没有
18171819
4. ordinary 与 Mamba visitor 在一个已经读取的 callback range 内,一旦所有候选 host 的单调 prefix stop 都不晚于
18181820
当前 key,就立即停止剩余 location projection。它不取消仍可能决定更短 prefix 的早期 range,也不屏蔽仍在任一
18191821
host 必需区间内的 backend error;
1820-
5. 新增 6000-key、6-host 的确定性随机差分模型,组合 medium、L1.5/L2、全部非 serving 状态、非法 URI、Eagle
1821-
pop 和 Mamba spec group,把渐进结果逐项与保留的 full-materialize 路径比较。另用 33000 key、两个不同 stop 的
1822-
host 验证:所有候选 stop 之后的 speculative timeout 可以忽略,但仍处于较长 host prefix 内的同类 timeout 必须
1823-
原样返回;
1822+
5. 新增 6000-key、6-host 的确定性随机模型,组合 medium、L1.5/L2、全部非 serving 状态、非法 URI、Eagle pop
1823+
和 Mamba spec group,比较两种查询模式的 local 投影;另用 33000 key、两个不同 stop 的 host 验证:所有候选
1824+
stop 之后的 speculative timeout 可以忽略,但仍处于较长 host prefix 内的同类 timeout 必须原样返回;
18241825
6. 全量门禁顺便暴露两个测试自身的不确定假设并已固定:MigrationManager 的 capacity partial-failure 用例不再假设
18251826
固定 key 在动态 hash seed 下必然属于不同 shard;两个直接清空 executor queue 的 stale-cache 用例会先停止
18261827
reclaimer supervisor,避免销毁它正在等待的 `packaged_task` 后随机抛出 `broken promise`。两项均只修改测试,
@@ -1842,3 +1843,25 @@ GPU-only 测试按预期跳过。上述 mixed 结果也说明渐进 query 没有
18421843
`bazel test --config=release --nocache_test_results --test_output=errors --jobs=8 //kv_cache_manager/...`
18431844
106 个可执行测试全部通过,1 个 GPU-only 测试按预期跳过。工作树迁移后复用旧绝对路径 output base 曾导致 7 个
18441845
用例在 0ms 内因 runfiles 缺失失败;独立重建后这些用例全部通过,未把基础设施假失败当作代码缺陷。
1846+
1847+
### 5.29 2026-08-11 P2P 有界归约与协议兼容性收敛
1848+
1849+
更新后的基线已经解决 `p2p_host_count=0` 的渐进读取,但 P2P 路径仍为每个 key 构造两份
1850+
`map<host, set<spec>>`。本轮删除这套逐 key 对象图:host 在请求内映射为 dense id,spec group 编码为多 word
1851+
bitmask;普通 prefix 在线维护最终 top-N host 的 peer 交集,Mamba 通过三个有界 ordered pass 保留原来的
1852+
“full group 独立选 prefix peer、state group 统一选 coverage peer”语义。非 local backend 仍只读取一次 compact
1853+
batch,后续 pass 重放该 batch。
1854+
1855+
同一 Release/O2 二进制、4 query workers、pure-local metadata、1M key、在第 1024 key 制造一个可由 Vineyard
1856+
peer 补齐的 local gap,对更新前后的 `mu-main` 使用完全相同的 benchmark:
1857+
1858+
| 场景 | 更新前 p50 | 本轮 p50 | 变化 |
1859+
| --- | ---: | ---: | ---: |
1860+
| ordinary P2P | 607.63ms | 155.27ms | -74.4% |
1861+
| Mamba P2P | 627.97ms | 351.25ms | -44.1% |
1862+
| 进程峰值 RSS | 1,342,488KiB | 585,336KiB | -56.4%(约 -739MiB) |
1863+
1864+
数字来自共享开发机,只用于同机回归,不是线上 SLA。门禁覆盖动态 top-N 淘汰、重复 block key 的 fetched count
1865+
去重、多个 full group 分别选 peer 后合并、非 local backend 单次读取、跨 64 个 spec 的 bitmask、ordered visitor
1866+
窗口/stop,以及 P2P 前缀内硬错误传播。协议侧同时恢复 `HostCacheMatch.prefix_match_blocks` 的既有 JSON 名称;
1867+
`location_spec_names` 保持全局响应投影语义,新 per-key 候选过滤使用 tag 10 的 `per_key_location_spec_names`

integration_test/meta_service/http_interface_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ def test_event_report_requested_spec_filters_before_peer_selection(self):
243243
"query_type": "QT_BATCH_GET",
244244
"block_keys": block_keys,
245245
"block_mask": {"offset": 0},
246-
"location_spec_names": ["linear_1"] * len(block_keys),
246+
"per_key_location_spec_names": ["linear_1"] * len(block_keys),
247247
"backend_selectors": [{
248248
"backend_type": "ST_EVENT_REPORT_L2",
249249
"strategy": strategy,
@@ -264,7 +264,7 @@ def test_event_report_requested_spec_filters_before_peer_selection(self):
264264
"query_type": "QT_BATCH_GET",
265265
"block_keys": block_keys,
266266
"block_mask": {"offset": 0},
267-
"location_spec_names": ["unknown_spec"] * len(block_keys),
267+
"per_key_location_spec_names": ["unknown_spec"] * len(block_keys),
268268
"backend_selectors": [{
269269
"backend_type": "ST_EVENT_REPORT_L2",
270270
"strategy": "LSS_V6D_PREFIX",

integration_test/meta_service/test_report_event.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,7 @@ def test_18_get_host_cache_state_dual_type_prefix_match(self):
10141014
"10.0.0.3:8080": 1,
10151015
}
10161016
actual = {
1017-
h["host_ip_port"]: int(h["local"])
1017+
h["host_ip_port"]: int(h["prefix_match_blocks"])
10181018
for h in resp.get("hosts", [])
10191019
}
10201020
self.assertNotIn("p2p_1_hit_count", resp)

integration_test/meta_service/test_report_event_snapshot.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ def test_11_mixed_batch(self):
10151015
"block_cache_keys": [block_key],
10161016
})
10171017
matches = {
1018-
item["host_ip_port"]: int(item["local"])
1018+
item["host_ip_port"]: int(item["prefix_match_blocks"])
10191019
for item in host_state.get("hosts", [])
10201020
}
10211021
self.assertEqual(matches.get(host), 1)
@@ -1167,7 +1167,7 @@ def test_16a_heartbeat_timeout_then_recovery(self):
11671167
"block_cache_keys": [block_key],
11681168
})
11691169
target_prefixes = {
1170-
item["host_ip_port"]: int(item["local"])
1170+
item["host_ip_port"]: int(item["prefix_match_blocks"])
11711171
for item in target_host_state.get("hosts", [])
11721172
}
11731173
self.assertEqual(target_prefixes.get(host), 1)
@@ -1284,7 +1284,7 @@ def test_16a_heartbeat_timeout_then_recovery(self):
12841284
"block_cache_keys": [block_key],
12851285
})
12861286
hidden_prefixes = {
1287-
item["host_ip_port"]: int(item["local"])
1287+
item["host_ip_port"]: int(item["prefix_match_blocks"])
12881288
for item in hidden_host_state.get("hosts", [])
12891289
}
12901290
self.assertEqual(
@@ -1388,7 +1388,7 @@ def test_16a_heartbeat_timeout_then_recovery(self):
13881388
"block_cache_keys": [block_key],
13891389
})
13901390
recovered_prefixes = {
1391-
item["host_ip_port"]: int(item["local"])
1391+
item["host_ip_port"]: int(item["prefix_match_blocks"])
13921392
for item in recovered_host_state.get("hosts", [])
13931393
}
13941394
self.assertEqual(recovered_prefixes.get(host), 1)
@@ -1678,7 +1678,7 @@ def test_16_get_host_cache_state(self):
16781678
"10.0.0.3:8080": 1,
16791679
}
16801680
actual = {
1681-
h["host_ip_port"]: int(h["local"])
1681+
h["host_ip_port"]: int(h["prefix_match_blocks"])
16821682
for h in resp.get("hosts", [])
16831683
}
16841684
for host, prefix in expected.items():
@@ -1703,7 +1703,7 @@ def test_16_get_host_cache_state(self):
17031703
["10.0.0.1:8080", "10.0.0.2:8080", "10.0.0.3:8080"],
17041704
)
17051705
large_actual = {
1706-
item["host_ip_port"]: int(item["local"])
1706+
item["host_ip_port"]: int(item["prefix_match_blocks"])
17071707
for item in large_hosts
17081708
}
17091709
self.assertEqual(large_actual["10.0.0.1:8080"], 2)
@@ -3231,7 +3231,7 @@ def test_26_unregistered_errors_remain_distinct_from_delta_only_mode(self):
32313231
"block_cache_keys": [25_000_250],
32323232
})
32333233
matches = {
3234-
item["host_ip_port"]: int(item["local"])
3234+
item["host_ip_port"]: int(item["prefix_match_blocks"])
32353235
for item in host_state.get("hosts", [])
32363236
}
32373237
self.assertEqual(matches.get(host), 1)
@@ -3422,7 +3422,7 @@ def send_first_delta(writer):
34223422
],
34233423
})
34243424
matches = {
3425-
item["host_ip_port"]: int(item["local"])
3425+
item["host_ip_port"]: int(item["prefix_match_blocks"])
34263426
for item in host_state.get("hosts", [])
34273427
}
34283428
self.assertEqual(matches.get(host), writer_count)
@@ -3536,7 +3536,7 @@ def test_31_first_delete_without_snapshot_creates_reusable_generation(self):
35363536
"block_cache_keys": [block_key],
35373537
})
35383538
empty_matches = {
3539-
item["host_ip_port"]: int(item["local"])
3539+
item["host_ip_port"]: int(item["prefix_match_blocks"])
35403540
for item in empty_host_state.get("hosts", [])
35413541
}
35423542
self.assertNotIn(host, empty_matches)
@@ -3582,7 +3582,7 @@ def test_31_first_delete_without_snapshot_creates_reusable_generation(self):
35823582
"block_cache_keys": [block_key],
35833583
})
35843584
visible_matches = {
3585-
item["host_ip_port"]: int(item["local"])
3585+
item["host_ip_port"]: int(item["prefix_match_blocks"])
35863586
for item in visible_host_state.get("hosts", [])
35873587
}
35883588
self.assertEqual(visible_matches.get(host), 1)
@@ -4520,7 +4520,7 @@ def test_21_get_host_cache_state_local_scaling(self):
45204520

45214521
def assert_response(response):
45224522
prefixes = {
4523-
item["host_ip_port"]: int(item["local"])
4523+
item["host_ip_port"]: int(item["prefix_match_blocks"])
45244524
for item in response.get("hosts", [])
45254525
}
45264526
self.assertEqual(prefixes.get(host), block_count)
@@ -4557,7 +4557,7 @@ def query_worker(worker_index):
45574557
f"worker {worker_index}: status={code}, body={body}"
45584558
)
45594559
prefixes = {
4560-
item["host_ip_port"]: int(item["local"])
4560+
item["host_ip_port"]: int(item["prefix_match_blocks"])
45614561
for item in body.get("hosts", [])
45624562
}
45634563
if prefixes.get(host) != block_count:

kv_cache_manager/data_storage/data_storage_backend.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class DataStorageBackend {
2525
virtual double GetStorageUsageRatio(const std::string &trace_id) const = 0;
2626
inline bool IsOpen() const { return is_open_.load(std::memory_order_relaxed); }
2727
inline void SetOpen(bool open) { is_open_.store(open, std::memory_order_relaxed); }
28+
// Overrides may add notifications, but must delegate to this method. Any
29+
// state published by SetAvailable(true) must be initialized before the
30+
// release store so IsAvailable()'s acquire load observes it consistently.
2831
virtual void SetAvailable(bool available) { is_available_.store(available, std::memory_order_release); }
2932
std::shared_ptr<DataStorageMetricsCollector> GetMetricsCollector() { return metrics_collector_; }
3033
virtual const StorageConfig &GetStorageConfig() { return config_; }

0 commit comments

Comments
 (0)