Skip to content

Commit f557f21

Browse files
authored
[optimizer] Add template prefix tracker and enhance offline analysis capabilities (#91)
* [optimizer] add template prefix tracker and shared analysis infrastructure * [optimizer] enhance output structure and documentation for analysis scripts - Updated README.md to provide a comprehensive overview of output directories and file structures for various analysis scripts. - Modified plotting functions to save generated charts in organized subdirectories within the specified output directory. * [optimizer] enhance block lifecycle tracking and lifespan analysis - Added a pointer to the BlockEntry in BlockLifecycleRecord to hold references during the block's lifespan. - Updated the lifecycle_plot.py script to prioritize evicted blocks for statistical annotations in the physical lifespan CDF.
1 parent 29dc424 commit f557f21

21 files changed

Lines changed: 832 additions & 89 deletions

kv_cache_manager/optimizer/analysis/script/README.md

Lines changed: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,15 @@ bazel run //kv_cache_manager/optimizer/analysis/script:optimizer_run -- -c confi
3535

3636
### 输出
3737

38-
- `<output_result_path>/*_hit_rates.csv` — 每个 instance 的命中率时序数据
39-
- `<output_result_path>/multi_instance_cache_analysis.png` — 命中率时序图(需 `--draw-chart`
40-
- `<output_result_path>/*_lifecycle.csv` — block 生命周期数据(需 `--export-lifecycle`
38+
```
39+
<output_result_path>/
40+
├── *_hit_rates.csv # 每个 instance 的命中率时序数据
41+
├── *_template_prefix_traces.csv # per-trace 模板归属明细
42+
├── *_template_prefix_summary.csv # 模板级汇总
43+
├── *_lifecycle.csv # block 生命周期数据(需 --export-lifecycle)
44+
└── timeseries/
45+
└── multi_instance_cache_analysis.png # 命中率时序图(需 --draw-chart)
46+
```
4147

4248
---
4349

@@ -106,9 +112,19 @@ bazel run //kv_cache_manager/optimizer/analysis/script:tradeoff -- \
106112

107113
### 输出
108114

109-
- `pareto_curve_<type>.png` — 单策略 Pareto 散点图
110-
- `multi_policy_<type>.png` — 多策略对比子图
111-
- `csv_results/cap_<capacity>_<policy>/` — 每次运行的 CSV(需 `--save-csv`
115+
```
116+
<output_result_path>/
117+
├── pareto/
118+
│ ├── pareto_curve_<type>.png # 单策略 Pareto 散点图
119+
│ └── multi_policy_<type>.png # 多策略对比子图
120+
├── timeseries/
121+
│ └── multi_instance_cache_analysis.png # 时序图(需 --plot-timeseries)
122+
└── csv_results/ # 需 --save-csv
123+
└── cap_<capacity>_<policy>/
124+
├── *_hit_rates.csv
125+
├── *_template_prefix_traces.csv
126+
└── *_template_prefix_summary.csv
127+
```
112128

113129
---
114130

@@ -180,9 +196,13 @@ python kv_cache_manager/optimizer/analysis/script/plot/radix_tree_plot.py \
180196

181197
### 输出
182198

183-
- `<instance>_radix_tree.json` — 前缀树结构数据
184-
- `<instance>_radix_tree.png` — 完整树可视化
185-
- `<instance>_hot_paths.png` — 热点路径可视化
199+
```
200+
<output_result_path>/
201+
└── radix_tree/
202+
├── <instance>_radix_tree.json # 前缀树结构数据
203+
├── <instance>_radix_tree.png # 完整树可视化
204+
└── <instance>_hot_paths.png # 热点路径可视化
205+
```
186206

187207
---
188208

@@ -227,10 +247,15 @@ bazel run //kv_cache_manager/optimizer/analysis/script:analyze_lifecycle -- \
227247

228248
### 输出
229249

230-
- 控制台统计报告
231-
- `<instance>_physical_lifespan_cdf.png` — Physical Lifespan CDF(全量 + Evicted)
232-
- `<instance>_active_lifespan_cdf.png` — Active Lifespan CDF
233-
- `<instance>_access_count.png` — Access Count 直方图(全量 + 去零两张子图)
250+
```
251+
<output_result_path>/
252+
└── lifecycle/
253+
├── <instance>_physical_lifespan_cdf.png # Physical Lifespan CDF(全量 + Evicted)
254+
├── <instance>_active_lifespan_cdf.png # Active Lifespan CDF
255+
└── <instance>_access_count.png # Access Count 直方图(全量 + 去零两张子图)
256+
```
257+
258+
控制台同步输出统计报告。
234259

235260
---
236261

@@ -285,4 +310,45 @@ script/
285310
├── optimizer_runner.py # optimizer 运行封装
286311
├── csv_loader.py # CSV 加载 + 容量列表
287312
└── plot_utils.py # 绘图风格 + Pareto 绘图
313+
```
314+
315+
---
316+
317+
## 输出目录总览
318+
319+
所有脚本共享同一个根目录 `<output_result_path>`(来自 config.json `output_result_path` 字段)。
320+
321+
```
322+
<output_result_path>/
323+
324+
│ # ── C++ optimizer 原始数据输出 ──────────────────────────────
325+
├── *_hit_rates.csv # 命中率时序(每条 trace 上报)
326+
├── *_template_prefix_traces.csv # per-trace 模板归属明细
327+
├── *_template_prefix_summary.csv # 模板级汇总
328+
├── *_lifecycle.csv # block 生命周期(需 --export-lifecycle)
329+
330+
│ # ── Python 图表输出 ────────────────────────────────────────
331+
├── pareto/ # tradeoff
332+
│ ├── pareto_curve_<type>.png
333+
│ └── multi_policy_<type>.png
334+
335+
├── timeseries/ # optimizer_run --draw-chart
336+
│ └── multi_instance_cache_analysis.png # tradeoff --plot-timeseries
337+
338+
├── lifecycle/ # analyze_lifecycle
339+
│ ├── *_physical_lifespan_cdf.png
340+
│ ├── *_active_lifespan_cdf.png
341+
│ └── *_access_count.png
342+
343+
├── radix_tree/ # export_tree
344+
│ ├── *_radix_tree.json
345+
│ ├── *_radix_tree.png
346+
│ └── *_hot_paths.png
347+
348+
│ # ── tradeoff --save-csv 实验中间数据 ─────────────────────────
349+
└── csv_results/
350+
└── cap_<N>_<policy>/
351+
├── *_hit_rates.csv
352+
├── *_template_prefix_traces.csv
353+
└── *_template_prefix_summary.csv
288354
```

kv_cache_manager/optimizer/analysis/script/plot/hit_rate_plot.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,49 @@ def read_csv_file(csv_file_path):
3232
print(f"Error reading {csv_file_path}: {str(e)}")
3333
return None
3434

35-
def plot_multi_instance_analysis(csv_dir):
35+
def _load_sp_cumulative(csv_dir, instance_name):
36+
"""
37+
从 template_prefix_traces.csv 计算 system prompt 累积命中率时序。
38+
39+
返回 DataFrame: [TimestampUs, AccSpHitRate]
40+
AccSpHitRate = cumsum(min(hit, template_depth)) / cumsum(total_blocks)
41+
"""
42+
basename = instance_name.replace("_hit_rates", "")
43+
sp_path = os.path.join(csv_dir, f"{basename}_template_prefix_traces.csv")
44+
if not os.path.exists(sp_path):
45+
return None
46+
47+
df = pd.read_csv(sp_path)
48+
# trace_id format: trace_<instance>_<timestamp_us>
49+
df['TimestampUs'] = df['TraceId'].str.rsplit('_', n=1).str[-1].astype(np.int64)
50+
df = df.sort_values('TimestampUs')
51+
52+
sp_hits = np.where(
53+
(df['TemplateId'] != 'NONE') & (df['TemplateDepth'] > 0),
54+
np.minimum(df['HitBlocks'].values, df['TemplateDepth'].values),
55+
0
56+
)
57+
58+
cum_sp_hits = np.cumsum(sp_hits)
59+
cum_total = np.cumsum(df['TotalBlocks'].values)
60+
61+
acc_sp_rate = np.where(cum_total > 0, cum_sp_hits / cum_total, 0.0)
62+
63+
return pd.DataFrame({
64+
'TimestampUs': df['TimestampUs'].values,
65+
'AccSpHitRate': acc_sp_rate,
66+
})
67+
68+
69+
def plot_multi_instance_analysis(csv_dir, output_dir: str = None):
70+
"""
71+
读取 csv_dir 下的命中率 CSV,生成时序分析图。
72+
73+
Args:
74+
csv_dir: CSV 数据目录
75+
output_dir: 图表根输出目录,图表保存至 output_dir/timeseries/
76+
默认为 csv_dir(向后兼容)
77+
"""
3678
csv_files = sorted(glob.glob(os.path.join(csv_dir, "*_hit_rates.csv")))
3779
if not csv_files:
3880
print(f"Error: No CSV files found in directory: {csv_dir}")
@@ -77,8 +119,8 @@ def plot_multi_instance_analysis(csv_dir):
77119
base = pd.DataFrame({'t': base_timestamps}) # 用于merge_asof
78120

79121
all_acc_hit, all_acc_external_hit, all_time_ranges = [], [], []
80-
# 用于瞬时命中率计算:累积读块数 / 累积命中块数(反推)
81122
all_acc_read_blocks, all_acc_hit_blocks, all_acc_ext_hit_blocks = [], [], []
123+
all_acc_sp_hit = []
82124
global_updates_list = []
83125
for df in dataframes:
84126
d = df.copy()
@@ -114,6 +156,22 @@ def plot_multi_instance_analysis(csv_dir):
114156
all_acc_hit_blocks.append(aligned['AccHitBlocks'].to_numpy(float))
115157
all_acc_ext_hit_blocks.append(aligned['AccExtHitBlocks'].to_numpy(float))
116158

159+
# ---- SP 累积命中率对齐 ----
160+
for idx, name in enumerate(instance_names):
161+
sp_df = _load_sp_cumulative(csv_dir, name)
162+
if sp_df is None:
163+
all_acc_sp_hit.append(None)
164+
continue
165+
sp_df['t'] = (sp_df['TimestampUs'] - min_timestamp) / 1e6
166+
sp_df = sp_df.sort_values('t')
167+
sp_aligned = pd.merge_asof(
168+
base, sp_df[['t', 'AccSpHitRate']], on='t',
169+
direction='backward', allow_exact_matches=True
170+
)
171+
t0, _ = all_time_ranges[idx]
172+
sp_aligned.loc[sp_aligned['t'] < t0, 'AccSpHitRate'] = np.nan
173+
all_acc_sp_hit.append(sp_aligned['AccSpHitRate'].to_numpy(float))
174+
117175
global_updates = pd.concat(global_updates_list, ignore_index=True)
118176
global_updates = global_updates.dropna(subset=['t', 'CachedBlocksAllInstance']).sort_values('t')
119177

@@ -233,7 +291,7 @@ def window_hit_rate(timestamps, acc_hit_blocks, acc_read_blocks, window_seconds=
233291
top_lines = [ax_top.lines[0]]
234292
bot_lines = [ax_bot.lines[0]]
235293

236-
# 上图:累计命中率
294+
# 上图:累计命中率 + system prompt 累积命中率
237295
for i, name in enumerate(instance_names):
238296
t0, t1 = all_time_ranges[i]
239297
valid = (base_timestamps >= t0) & (base_timestamps <= t1)
@@ -246,6 +304,14 @@ def window_hit_rate(timestamps, acc_hit_blocks, acc_read_blocks, window_seconds=
246304
linewidth=1.5, drawstyle='steps-post')
247305
top_lines += l1
248306

307+
if all_acc_sp_hit[i] is not None:
308+
sp_line = ax_top_r.plot(
309+
base_timestamps[valid], np.array(all_acc_sp_hit[i])[valid],
310+
color=colors[i], linestyle=':', linewidth=2.5, alpha=0.9,
311+
label=f'{name} - SP AccHitRate',
312+
drawstyle='steps-post')
313+
top_lines += sp_line
314+
249315
# 下图:时间窗口内真实命中率(累积量差值)+ 按时间降采样
250316
downsample_interval_s = 10 # 每隔 10 秒取一个代表点
251317
window_seconds = 10 # 窗口内累积命中率的统计时间跨度
@@ -300,7 +366,9 @@ def window_hit_rate(timestamps, acc_hit_blocks, acc_read_blocks, window_seconds=
300366
ax_top.set_title(f'Cache Analysis - {len(instance_names)} Instances', fontsize=15, fontweight='bold', pad=12)
301367

302368
fig.tight_layout()
303-
output_file = os.path.join(csv_dir, "multi_instance_cache_analysis.png")
369+
timeseries_dir = os.path.join(output_dir or csv_dir, "timeseries")
370+
os.makedirs(timeseries_dir, exist_ok=True)
371+
output_file = os.path.join(timeseries_dir, "multi_instance_cache_analysis.png")
304372
plt.savefig(output_file, dpi=300, bbox_inches='tight', facecolor='white')
305373
print(f"Chart saved to: {output_file}")
306374
plt.close()

kv_cache_manager/optimizer/analysis/script/plot/lifecycle_plot.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,18 @@ def _annotate_percentiles(ax, sorted_data, percentiles, x_max):
3333

3434

3535
def plot_physical_lifespan_cdf(all_sorted, evicted_sorted, instance_name, output_path):
36-
"""Physical Lifespan CDF: 全量 + Evicted"""
36+
"""Physical Lifespan CDF: 全量 + Evicted,统计标注基于 Evicted Only"""
3737
n_all, n_ev = len(all_sorted), len(evicted_sorted)
3838
if n_all == 0:
3939
print(" 跳过 Physical Lifespan CDF: 无数据")
4040
return
4141

42+
# 统计标注优先用 evicted_sorted(真实驱逐分布),无驱逐数据时 fallback 到 all
43+
stat_data = evicted_sorted if n_ev > 0 else all_sorted
44+
4245
cdf_all = np.arange(1, n_all + 1) / n_all * 100
43-
p99 = float(np.percentile(all_sorted, 99))
44-
x_max = min(p99 * 1.5, float(all_sorted[-1])) if all_sorted[-1] > 0 else 1.0
46+
p99 = float(np.percentile(stat_data, 99))
47+
x_max = min(p99 * 1.5, float(stat_data[-1])) if stat_data[-1] > 0 else 1.0
4548

4649
fig, ax = plt.subplots(figsize=(14, 8))
4750

@@ -55,14 +58,15 @@ def plot_physical_lifespan_cdf(all_sorted, evicted_sorted, instance_name, output
5558
label=f"Evicted Only (n={n_ev:,})", alpha=0.8)
5659
ax.fill_between(evicted_sorted, cdf_ev, alpha=0.1, color="red")
5760

58-
_annotate_percentiles(ax, all_sorted, [50, 75, 90, 95, 99], x_max)
61+
_annotate_percentiles(ax, stat_data, [50, 75, 90, 95, 99], x_max)
5962

60-
mean_val = float(np.mean(all_sorted))
61-
median_val = float(np.median(all_sorted))
63+
mean_val = float(np.mean(stat_data))
64+
median_val = float(np.median(stat_data))
65+
stat_label = "Evicted" if n_ev > 0 else "All"
6266
ax.axvline(mean_val, color="blue", linestyle="--", linewidth=2,
63-
label=f"Mean: {mean_val:.1f}s", alpha=0.7)
67+
label=f"Mean ({stat_label}): {mean_val:.1f}s", alpha=0.7)
6468
ax.axvline(median_val, color="orange", linestyle="--", linewidth=2,
65-
label=f"Median: {median_val:.1f}s", alpha=0.7)
69+
label=f"Median ({stat_label}): {median_val:.1f}s", alpha=0.7)
6670

6771
ax.set_xlim([0, x_max])
6872
ax.set_ylim([0, 105])

kv_cache_manager/optimizer/analysis/script/run/analyze_lifecycle.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232

3333
def analyze_single(csv_path: str, output_dir: str, stats_only: bool = False):
34-
"""分析单个 lifecycle CSV"""
34+
"""分析单个 lifecycle CSV,图表保存至 output_dir/lifecycle/"""
3535
name = Path(csv_path).stem.replace("_lifecycle", "")
3636
print(f"\n{'='*60}")
3737
print(f"分析: {name}")
@@ -48,21 +48,22 @@ def analyze_single(csv_path: str, output_dir: str, stats_only: bool = False):
4848
if stats_only:
4949
return
5050

51-
os.makedirs(output_dir, exist_ok=True)
51+
lifecycle_dir = os.path.join(output_dir, "lifecycle")
52+
os.makedirs(lifecycle_dir, exist_ok=True)
5253
plot_data = extract_plot_data(df)
5354

5455
print(f"\n生成图表:")
5556
plot_physical_lifespan_cdf(
5657
plot_data["physical_all"], plot_data["physical_evicted"],
57-
name, os.path.join(output_dir, f"{name}_physical_lifespan_cdf.png"))
58+
name, os.path.join(lifecycle_dir, f"{name}_physical_lifespan_cdf.png"))
5859

5960
plot_active_lifespan_cdf(
6061
plot_data["active_all"],
61-
name, os.path.join(output_dir, f"{name}_active_lifespan_cdf.png"))
62+
name, os.path.join(lifecycle_dir, f"{name}_active_lifespan_cdf.png"))
6263

6364
plot_access_count_histogram(
6465
plot_data["access_counts"],
65-
name, os.path.join(output_dir, f"{name}_access_count.png"))
66+
name, os.path.join(lifecycle_dir, f"{name}_access_count.png"))
6667

6768

6869
def main():

kv_cache_manager/optimizer/analysis/script/run/export_tree.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,15 @@ def main():
8484
sys.exit(1)
8585
config = config_loader.config()
8686

87-
output_dir = Path(args.output_dir) if args.output_dir else Path(config.output_result_path())
87+
root_dir = Path(args.output_dir) if args.output_dir else Path(config.output_result_path())
88+
output_dir = root_dir / "radix_tree"
8889
output_dir.mkdir(parents=True, exist_ok=True)
8990

9091
print("=" * 80)
9192
print("Radix Tree Export and Visualization")
9293
print("=" * 80)
9394
print("Config: {}".format(args.config))
94-
print("Output: {}".format(output_dir))
95+
print("Output: {}".format(root_dir))
9596
print()
9697

9798
optimizer = kvcm_py_optimizer.OptimizerManager(config)
@@ -160,7 +161,7 @@ def main():
160161
)
161162

162163
print("\n" + "=" * 80)
163-
print("Done! Output: {}".format(output_dir))
164+
print("Done! Output: {}".format(root_dir))
164165
print("=" * 80)
165166

166167
kvcm_py_optimizer.LoggerBroker.DestroyLogger()

kv_cache_manager/optimizer/analysis/script/run/optimizer_run.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def main():
8080
if args.draw_chart:
8181
t5 = time.time()
8282
print("\n[5/5] Generating charts...")
83-
plot_multi_instance_analysis(output_path)
83+
plot_multi_instance_analysis(output_path, output_path)
8484
print(" Charts done: {:.2f}s".format(time.time() - t5))
8585
else:
8686
print("\n[5/5] Skipping chart generation.")

kv_cache_manager/optimizer/analysis/script/run/tradeoff.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,8 @@ def _print_multi_policy_table(results_by_policy, policies):
105105
# 时序图
106106
# ============================================================================
107107

108-
def _plot_timeseries(csv_save_dir, results_by_policy, target_caps=None):
109-
"""为指定容量点生成命中率时序图"""
108+
def _plot_timeseries(csv_save_dir, results_by_policy, output_dir, target_caps=None):
109+
"""为指定容量点生成命中率时序图,图表保存至 output_dir/timeseries/"""
110110
print("\n" + "=" * 60)
111111
print("Generating Timeseries Plots")
112112
print("=" * 60)
@@ -118,7 +118,7 @@ def _plot_timeseries(csv_save_dir, results_by_policy, target_caps=None):
118118
if os.path.exists(cap_dir):
119119
print("Plotting {} capacity={}...".format(pol, cap))
120120
try:
121-
plot_multi_instance_analysis(cap_dir)
121+
plot_multi_instance_analysis(cap_dir, output_dir)
122122
count += 1
123123
except Exception as e:
124124
print(" Failed: {}".format(e))
@@ -264,7 +264,7 @@ def main():
264264
# ----------------------------------------------------------------
265265
has_csv = args.save_csv or args.skip_run
266266
if args.plot_timeseries and has_csv:
267-
_plot_timeseries(csv_save_dir, results_by_policy, args.plot_capacity)
267+
_plot_timeseries(csv_save_dir, results_by_policy, output_dir, args.plot_capacity)
268268
elif args.plot_timeseries and not has_csv:
269269
print("\nWarning: --plot-timeseries requires --save-csv or --skip-run")
270270

0 commit comments

Comments
 (0)