执行摘要
- 一句话:按设备峰值计算 MFU,修复 FSDP 缺失吞吐指标并上线看板
- 推荐动作:值得精读。该 PR 的工程判断密度高:分子分母同落盘保证指标可审计、保持 train-step 口径避免异步误导、未知设备“不猜默认值”、阈值从常量演进为可配置 flag,都是可复用的可观测性设计模式。重点看
device_flops.py 的整词匹配实现、flops_args_from_hf_config() 的容错层级,以及 review 中三次“实测数据推翻默认假设”的往复——它们展示了如何用真实运行数据校准一个看似合理的常量。
功能与动机
PR body 明确点出核心缺口:“perf/actor_train_tflops already measures per-GPU throughput. MFU is that divided by the device's peak — and nothing in miles knew what any device's peak was, so the ratio could not be formed.” 同时:“the FSDP backend passed compute_total_fwd_flops=None, so FSDP runs had no throughput metrics at all.” 即已有吞吐指标但缺分母、FSDP 运行连吞吐指标都没有。此外 PR 还希望区分“训练步算得慢”与“训练步等 rollout 数据”两种瓶颈——文中给出 colocate 与 fully-async 下 wait_time_ratio 从 0.708 塌缩到 0.060 的对比,说明 MFU 应保持为 train-step 比率而非端到端混合数,避免“async 让训练高效 3.5 倍”的错误结论。
实现拆解
-
新增设备峰值算力表(分母来源):新建 miles/utils/device_flops.py,用 _PEAK_BF16_TFLOPS 字典登记 A100(312)、H100(989)、H100 PCIE(756)、H200(989)、GH200(989)、B200(2250)、B300(2250)、GB200/GB300(2500,单位 TFLOP/s)的 dense BF16 峰值。_words() 归一化设备名字符与大写,_MATCH_ORDER 按 key 长度降序做整词匹配,避免 "NVIDIA H100-PCIE-80GB" 这类连字符写法落空;_current_device_name() 用 @cache 缓存 torch.cuda.get_device_name() 结果,local_peak_bf16_tflops() 返回本机峰值,未知设备返回 None 而不是猜默认值。配套 --mfu-peak-tflops 参数(miles/utils/arguments.py)允许用户覆盖表内数值。
-
打通 HF 配置与 FLOPs 模型,修复 FSDP 无吞吐指标:miles/utils/flops_utils.py 新增 fwd_tflops_per_gpu(),把两后端此前各自内联的 / world_size / 1e12 约定收敛为单一函数;新增 flops_args_from_hf_config() 将 HF PreTrainedConfig 翻译成 calculate_fwd_flops() 所需的 SimpleNamespace,覆盖 GQA 的 num_key_value_heads、MoE 的 n_routed_experts / num_local_experts、DeepSeek 的 first_k_dense_replace / n_shared_experts、Qwen 的 decoder_sparse_step / mlp_only_layers 混合稀疏模式、MLA 维度,以及多模态 get_text_config() 文本塔解包。miles/backends/fsdp_utils/actor.py 由传 compute_total_fwd_flops=None 改为传真实函数,FSDP 运行首次获得与 megatron 同口径的 FLOPs 与吞吐指标。
-
指标发射与可审计性:miles/utils/train_metric_utils.py 的 log_perf_data_raw() 在已有 perf/actor_train_tflops 基础上新增 perf/actor_train_mfu(吞吐 / 设备峰值)与 perf/mfu_peak_tflops——分母随分子一起落盘,读指标的人可复算;设备未识别或没有 FLOPs 模型时,MFU 字段整体省略而非对假定峰值输出百分比。零训练时间、非主 rank 等边界照旧不产出指标。
-
Dashboard 展示与告警:miles/dashboard/advisory.py 新增 mfu_summary()(排除 step 0,汇总 latest / mean / steps / peak)与 _mfu_advisories()(稳态 step ≥ 3 且均值低于阈值时告警,消息说明“算得慢不是等数据”及其典型成因);serve.py 新增 --low-mfu-threshold 启动参数(0 关闭规则),server.py 的 /api/advisory 同时返回 mfu summary 与 advisories,保证前端 tile 与告警规则使用同一份均值口径。前端 views_timeline.js 新增 renderMfu() tile(latest、mean over steps、device peak 三个 statBox),app.js 提取公共 statBox() 组件供复用。
-
测试与文档配套:新增 tests/fast/utils/test_device_flops.py(整词匹配、大小写、PCIe 特例、GH200 不误读为 H200、未知设备返回 None)、tests/fast/utils/test_flops_from_hf_config.py(HF 与 megatron 参数在 dense / MoE / MLA / 多模态下的 FLOPs 一致性、专家宽度回退与 dense-prefix 钳制)、tests/fast/utils/test_train_metric_utils.py(MFU = 吞吐 / 峰值、分母可复算、未知峰值省略 MFU、无 FLOPs 模型不产出);扩充 tests/fast/dashboard/test_advisory.py(阈值可配置、0 禁用、step 0 排除、最少步数、无指标不告警)与 tests/fast/dashboard/test_server.py(端点同服务 MFU 与分母、advisory 返回规则所用 summary)。docs/user-guide/dashboard.md 新增 “Model FLOPs utilization” 小节,说明公式、约定、注意事项与实测基线。
关键文件:
miles/utils/device_flops.py(模块 设备算力;类别 source;类型 core-logic;符号 _words, peak_bf16_tflops, _current_device_name, local_peak_bf16_tflops): 新增的设备峰值算力表是整个 MFU 分母的来源:登记 9 款 GPU 的 dense BF16 峰值、整词匹配避免连字符设备名落空、未知设备返回 None 而非猜默认值,直接决定 MFU 数值的准确性与缺失行为。
miles/utils/flops_utils.py(模块 FLOPs 计算;类别 source;类型 core-logic;符号 fwd_tflops_per_gpu, _first, _moe_layer_pattern, flops_args_from_hf_config): 新增 flops_args_from_hf_config() 把 HF 配置翻译成 calculate_fwd_flops() 可读的 SimpleNamespace,是 FSDP 后端获得 FLOPs 指标的关键适配层;fwd_tflops_per_gpu() 统一了两后端的 / world_size / 1e12 约定。
tests/fast/utils/test_flops_from_hf_config.py(模块 单元测试;类别 test;类型 test-coverage;符号 megatron_args, hf_config, assert_same, test_dense_gqa_model_matches): 核心 parity 测试:用同一组 SEQLENS 断言 HF 配置适配后的 FLOPs 与 megatron 参数完全一致,覆盖 dense/GQA、Mixtral/gpt-oss、DeepSeek dense-prefix、Qwen sparse-step、MLA、多模态解包等全部适配分支,是防回归的主要屏障。
miles/dashboard/advisory.py(模块 效率建议;类别 source;类型 core-logic;符号 compute_advisories, mfu_summary, _mfu_advisories): 新增 mfu_summary() 与 _mfu_advisories():前者服务端统一计算均值(排除 step 0),后者在稳态 step ≥ 3 且均值低于阈值时产出告警;compute_advisories 增加 mfu / low_mfu 入参,与 serve 层共用同一 summary,保证 tile 与规则口径一致。
miles/utils/train_metric_utils.py(模块 训练指标;类别 source;类型 dependency-wiring): log_perf_data_raw() 在此发射 perf/actor_train_mfu 与 perf/mfu_peak_tflops,把设备峰值与 MFU 比值的记录逻辑落到训练侧,是整条指标链路的源头。
tests/fast/utils/test_train_metric_utils.py(模块 单元测试;类别 test;类型 test-coverage;符号 timer, logged, make_args, run): 验证 MFU 数学关系(吞吐 / 峰值)、分母随分子落盘可审计、未知峰值省略 MFU、无 FLOPs 模型不产出、零训练时间与非主 rank 边界,把新指标的行为契约固定下来。
tests/fast/utils/test_device_flops.py(模块 单元测试;类别 test;类型 test-coverage;符号 test_known_devices_resolve, test_only_pcie_h100_clocks_below_its_family, test_grace_hopper_is_not_read_as_an_h200, test_longer_key_wins_over_the_substring_it_contains): 锁定设备名匹配的全部边界:已知设备解析、PCIe 特例、GH200 不误读为 H200、整词优先、大小写不敏感、未知设备返回 None、无 CUDA 返回 None,是峰值表准确性的守护测试。
miles/dashboard/static/views_timeline.js(模块 前端视图;类别 source;类型 core-logic;符号 renderMfu, pct): 新增 MFU tile(renderMfu)与 UTIL_FILL 样式,接入 /api/advisory 返回的 mfu summary,是用户直接看到新指标的入口;statBox 组件同时落到 app.js 供复用。
tests/fast/dashboard/test_advisory.py(模块 单元测试;类别 test;类型 test-coverage;符号 _mfu_store, test_sustained_low_mfu_is_a_warning, test_healthy_mfu_is_quiet, test_threshold_is_configurable): 覆盖低 MFU 告警的完整行为矩阵:持续低值告警、健康静默、阈值可配置、0 禁用、step 0 排除、稳态步数不足不判断、无 MFU 指标不告警。
miles/dashboard/serve.py(模块 仪表盘服务;类别 source;类型 dependency-wiring): 把 DEFAULT_LOW_MFU 暴露为 --low-mfu-threshold 启动参数并传入 make_app,是 review 中“阈值交给用户”决策的落地点。
miles/backends/fsdp_utils/actor.py(模块 FSDP 后端;类别 source;类型 dependency-wiring): FSDP 后端此前传 compute_total_fwd_flops=None 导致完全没有吞吐指标,本次改为传真实 FLOPs 函数,是 FSDP 运行获得 MFU 的前提修复。
docs/user-guide/dashboard.md(模块 用户文档;类别 docs;类型 documentation): 新增 Model FLOPs utilization 章节,沉淀公式、约定、注意事项(稀疏峰值、MTP 缺口、recompute 影响)与实测基线,是团队后续校准阈值的重要参考。
关键符号:peak_bf16_tflops, local_peak_bf16_tflops, fwd_tflops_per_gpu, flops_args_from_hf_config, _moe_layer_pattern, mfu_summary, _mfu_advisories, compute_advisories, renderMfu, log_perf_data_raw
关键源码片段
miles/utils/device_flops.py
新增的设备峰值算力表是整个 MFU 分母的来源:登记 9 款 GPU 的 dense BF16 峰值、整词匹配避免连字符设备名落空、未知设备返回 None 而非猜默认值,直接决定 MFU 数值的准确性与缺失行为。
# miles/utils/device_flops.py — 设备名 → 峰值算力映射,作为 MFU 的分母
from __future__ import annotations
import re
from functools import cache
# 各芯片峰值 dense BF16 吞吐(TFLOP/s)。数据手册常宣传 2:4 稀疏值,
# 恰好是 dense 的两倍,若引用会令全部 MFU 减半,因此这里统一使用 dense 值
_PEAK_BF16_TFLOPS: dict[str, float] = {
"A100": 312.0,
"H100": 989.0,
"H100 PCIE": 756.0, # PCIe 版是 114-SM 阉割芯片;NVL 版为 132-SM 全尺寸,仍用 989.0
"H200": 989.0,
"GH200": 989.0,
"B200": 2250.0,
"B300": 2250.0, # Blackwell Ultra 的 FP8/NVFP4 提升明显,但 dense BF16 与 B200 持平
"GB200": 2500.0,
"GB300": 2500.0,
}
# 按 key 长度降序匹配,保证 "GB300" 优先于其子串 "B300"
_MATCH_ORDER: tuple[str, ...] = tuple(sorted(_PEAK_BF16_TFLOPS, key=len, reverse=True))
def _words(device_name: str) -> str:
# 统一分隔符并大写,使 "H100-PCIE-80GB" 也能命中 "H100 PCIE" 键
return f" {re.sub(r'[^A-Za-z0-9]+', ' ', device_name).upper().strip()} "
def peak_bf16_tflops(device_name: str) -> float | None:
name = _words(device_name)
for key in _MATCH_ORDER:
if f" {key} " in name:
return _PEAK_BF16_TFLOPS[key]
return None # 未识别设备不猜测峰值,上层据此省略 MFU 而非输出错误百分比
@cache
# 惰性导入 torch,避免纯 CPU / 无 CUDA 环境被拖入依赖
# 返回值缓存整次进程,设备名在运行期内不会变化
def _current_device_name() -> str | None:
import torch
if not torch.cuda.is_available():
return None
return torch.cuda.get_device_name()
def local_peak_bf16_tflops() -> float | None:
device_name = _current_device_name()
return peak_bf16_tflops(device_name) if device_name else None
miles/utils/flops_utils.py
新增 flops_args_from_hf_config() 把 HF 配置翻译成 calculate_fwd_flops() 可读的 SimpleNamespace,是 FSDP 后端获得 FLOPs 指标的关键适配层;fwd_tflops_per_gpu() 统一了两后端的 / world_size / 1e12 约定。
# miles/utils/flops_utils.py — HF 配置 → megatron FLOPs 模型参数的适配层
def fwd_tflops_per_gpu(seqlens, args, world_size):
# 收敛两后端此前各自内联的约定:总 FLOPs 除以世界大小再除以 1e12
return calculate_fwd_flops(seqlens, args) / world_size / 1e12
def _first(config, *names, default=None):
# HF 各系列字段命名不统一,依次尝试候选名,取到非 None 即返回
for name in names:
value = getattr(config, name, None)
if value is not None:
return value
return default
def _moe_layer_pattern(config, num_layers, num_experts):
# 生成逐层是否 MoE 的掩码:DeepSeek 风格用 first_k_dense_replace,
# Qwen 风格用 decoder_sparse_step + mlp_only_layers
if num_experts is None:
return None
first_dense = getattr(config, "first_k_dense_replace", None)
if first_dense is not None:
return [0 if i < first_dense else 1 for i in range(num_layers)]
mlp_only = set(getattr(config, "mlp_only_layers", None) or ())
step = getattr(config, "decoder_sparse_step", 1) or 1
return [0 if i in mlp_only or (i + 1) % step != 0 else 1 for i in range(num_layers)]
def flops_args_from_hf_config(config):
# 多模态配置先解开 text tower:按文本塔而不是视觉塔的尺寸估算 FLOPs
getter = getattr(config, "get_text_config", None)
config = (getter() if callable(getter) else getattr(config, "text_config", None)) or config
num_attention_heads = config.num_attention_heads
hidden_size = config.hidden_size
num_layers = _first(config, "num_hidden_layers", "num_layers")
assert num_layers is not None, f"no layer count on {type(config).__name__}; cannot size the FLOPs model"
num_experts = _first(config, "n_routed_experts", "num_experts", "num_local_experts")
shared_ffn = getattr(config, "shared_expert_intermediate_size", None)
# DeepSeek 风格的 n_shared_experts 换算成等效中间层宽度
if shared_ffn is None and getattr(config, "n_shared_experts", None):
shared_ffn = config.n_shared_experts * config.moe_intermediate_size
moe_ffn = getattr(config, "moe_intermediate_size", None)
# Mixtral / gpt-oss 风格没有 moe_intermediate_size,直接用 intermediate_size;
# 只有声明了专家数才回退,dense 配置保持无专家宽度
if num_experts is not None and moe_ffn is None:
moe_ffn = config.intermediate_size
return SimpleNamespace(
hidden_size=hidden_size,
num_attention_heads=num_attention_heads,
num_query_groups=_first(config, "num_key_value_heads", default=num_attention_heads),
vocab_size=config.vocab_size,
num_layers=num_layers,
ffn_hidden_size=config.intermediate_size,
kv_channels=_first(config, "head_dim", default=hidden_size // num_attention_heads),
num_experts=num_experts,
moe_ffn_hidden_size=moe_ffn,
moe_router_topk=_first(config, "num_experts_per_tok", "moe_topk", default=1),
moe_shared_expert_intermediate_size=shared_ffn,
moe_layer_freq=_moe_layer_pattern(config, num_layers, num_experts),
q_lora_rank=getattr(config, "q_lora_rank", None),
kv_lora_rank=getattr(config, "kv_lora_rank", None),
qk_head_dim=getattr(config, "qk_nope_head_dim", None) or 0,
qk_pos_emb_head_dim=getattr(config, "qk_rope_head_dim", None) or 0,
v_head_dim=getattr(config, "v_head_dim", None) or 0,
)
miles/dashboard/advisory.py
新增 mfu_summary() 与 _mfu_advisories():前者服务端统一计算均值(排除 step 0),后者在稳态 step ≥ 3 且均值低于阈值时产出告警;compute_advisories 增加 mfu / low_mfu 入参,与 serve 层共用同一 summary,保证 tile 与规则口径一致。
# miles/dashboard/advisory.py — MFU 汇总与低利用率告警(Efficiency 视图)
# 阈值默认 0.15 只是起点:健康运行的 MFU 因模型与配置跨度极大
# (小 dense 模型约 25%,带 CPU offload 的 MoE 约 5%,长上下文 K2.5 约 17%),
# 绝对阈值无法一劳永逸,serve 层提供 --low-mfu-threshold 供按负载调校
DEFAULT_LOW_MFU = 0.15
MFU_KEY = "perf/actor_train_mfu"
MFU_PEAK_KEY = "perf/mfu_peak_tflops"
MFU_STEP_KEY = "rollout/step"
MFU_MIN_STEPS = 3 # 至少 3 个稳态 step 才下结论,避免早期波动误报
def mfu_summary(store: MetricStore) -> dict | None:
series = store.metric_series([MFU_KEY, MFU_PEAK_KEY], x_key=MFU_STEP_KEY)
steady = series[MFU_KEY]["y"][1:] # 排除 warmup 的 step 0
if not steady:
return None
return dict(
latest=steady[-1],
mean=sum(steady) / len(steady),
steps=len(steady),
peak=series[MFU_PEAK_KEY]["y"][-1], # 分母随 summary 一起下发,前端可复算
)
def _mfu_advisories(summary: dict | None, low_mfu: float) -> list[Advisory]:
if low_mfu <= 0 or summary is None or summary["steps"] < MFU_MIN_STEPS:
return []
mean_mfu, peak = summary["mean"], summary["peak"]
if mean_mfu >= low_mfu:
return []
return [
Advisory(
level="warning",
message=(
f"Model FLOPs utilization averaged {mean_mfu:.1%} of the device's {peak:g} TFLOP/s "
f"over {summary['steps']} train steps — "
"the training step is computing slowly, not waiting: this ratio counts actor train time only, "
"so rollout stalls cannot depress it. Usual causes are activation recompute, a parallel split "
"that leaves ranks idle, and small or ragged micro-batches"
),
)
]
def compute_advisories(
store: MetricStore,
*,
t0: float | None = None,
t1: float | None = None,
mfu: dict | None = None, # 允许 serve 层传入已算好的 summary,保证 tile 与规则不打架
low_mfu: float = DEFAULT_LOW_MFU,
) -> list[Advisory]:
out: list[Advisory] = _mfu_advisories(mfu if mfu is not None else mfu_summary(store), low_mfu)
if not store.has_stream(Stream.ENGINE_SERIES):
return out # 没有 sglang 数据时只返回 MFU 建议
# 后续 concurrency / cache-hit / token-usage 建议逻辑保持不变(略)
args = store.meta.args if store.meta else {}
colocate = bool(args.get("colocate"))
...
评论区精华
-
H100 PCIe / NVL 峰值精度(yueming-yuan → 已解决):reviewer 指出“This might not be accurate for all specs, e.g. H100 PCIe & H100 NVL”。作者在跟进 commit ca20caf 中新增 "H100 PCIE": 756.0 行,并把子串匹配升级为整词匹配(否则 torch 上报的连字符名 NVIDIA H100-PCIE-80GB 无法命中),同时论证 H100 NVL 是全尺寸 132-SM 芯片、不应降频。“先补数据、再修匹配机制”的处理顺序值得借鉴。
-
0.15 阈值对大模型偏高(yueming-yuan → 已解决):reviewer 给出实测:“I profiled long-context k2.5 training on GB300(MLA 架构、megatron 成熟实现),it can only reach ~17-18% mfu at best config”。作者回应“will add one choice for users, where they could set the boundary by themselves workload and expected MFU”,最终 commit b17ebc55 把常量改为 DEFAULT_LOW_MFU 并经 serve.py --low-mfu-threshold 暴露、传 0 关闭。这条讨论直接改变了产品形态:从硬编码规则变成可配置的工程决策。
-
Mixtral 风格专家宽度为 None 导致崩溃(yueming-yuan → 已解决):reviewer 精准指出 moe_ffn_hidden_size 为 None 时 calculate_fwd_flops 内乘法会 TypeError。作者 commit 10b63396 在配置声明了专家数但缺少 moe_intermediate_size 时回退到 intermediate_size(dense 配置保持无专家宽度),把“崩溃”变成“正确数值”。
-
FYI 补充(yueming-yuan):reviewer 明确标注“this one is not change request, just fyi”,仅提示阈值风险,作者随后主动给用户配置出口。
- H100 PCIe / NVL 峰值数据准确性 (correctness): 作者在 commit ca20caf 中新增 "H100 PCIE": 756.0 行,并把子串匹配升级为整词匹配(torch 上报的 "NVIDIA H100-PCIE-80GB" 才能命中),同时论证 H100 NVL 是全尺寸 132-SM 芯片不应降频,保留 989.0。
- LOW_MFU=0.15 阈值对大规模模型偏高 (design): 常量改为 DEFAULT_LOW_MFU,经 serve.py --low-mfu-threshold 暴露,传 0 关闭规则;PR body 亦明确标注该阈值为 placeholder 而非校准值,欢迎 reviewer 选择只发布指标与 tile。
- Mixtral 风格专家宽度为 None 导致崩溃 (correctness): 作者在 commit 10b63396 中当配置声明专家数但缺 moe_intermediate_size 时回退到 intermediate_size(dense 配置保持无专家宽度),把崩溃变成正确数值,并补充 test_mixtral_style_experts_sized_by_plain_intermediate_size 等测试。
- 阈值讨论的非变更请求说明 (question): 作者仍主动将阈值做成可配置 flag,保留默认 0.15 但允许按负载关闭或调校。
风险与影响
- 风险:
- 峰值表为人工维护的近似值:B300 沿用 B200 的 2250(基于芯片面积与 FP8/NVFP4 提升方向的推导),FP8/NVFP4 精度下实际可达算力更高,MFU 会系统性偏低;日期较新的芯片缺行时会静默不产出 MFU,需靠
--mfu-peak-tflops 手动补。
- FLOPs 模型只覆盖 base model 前向:
--enable-mtp-training 的真实激活计算不进分子,MFU 被低估;该缺口同样影响既有 perf/actor_train_tflops,PR 中明确记录为已知缺口而非本次修复。
flops_args_from_hf_config() 内 assert num_layers is not None:遇到无法识别的 HF 配置时会让 FSDP actor 直接抛异常——此前 FSDP 根本不计算 FLOPs、没有这条路径,属新引入的崩溃面。
DEFAULT_LOW_MFU = 0.15 未校准:对 offload / recompute 重的 MoE 运行可能误报(实测健康 MoE 仅 5% 左右),已提供 --low-mfu-threshold 0 关闭规则作为缓解,但默认告警仍可能制造噪音。
- 与 #2027 的合并冲突:PR body 明确警告 #2027 重写同文件
compute_advisories,新规则是独立函数、合并时需并入其告警等级体系。
- 影响:
- 用户侧(训练工程师):获得可直接判断“算得慢还是等数据”的 MFU 指标、看板 tile 与告警;FSDP 后端用户此前完全没有吞吐指标,本次补齐并统一到 megatron 口径。
- 系统侧:指标链路新增
perf/actor_train_mfu、perf/mfu_peak_tflops 两个 key,/api/advisory 响应结构新增 mfu 字段,前端随 views_timeline.js / app.js 更新;跨后端 FLOPs 口径一致性由 parity 测试锁定。
- 团队侧:为 dashboard 模块新增一个可配置告警规则和一份公共
statBox() 前端组件;后续阈值校准依赖真实运行数据积累,文档已沉淀公式与实测基线(8xH200:dense 25%/MoE 5%)。
- 风险标记:设备峰值表人工维护近似值, MFU 阈值未校准可能误报, MTP 等附加层 FLOPs 未计入分子, HF 配置断言可能引入新崩溃面, 与 #2027 存在合并冲突
关联脉络
- PR #2027 (标题未知,见 PR body 冲突警告): PR body 明确警告:#2027 重写同文件 compute_advisories,本 PR 新增的 MFU 规则是独立函数,合并时需以一行改动并入其告警等级体系,两者必须协调。
- PR #2386 fix: drop context parallelism from the FSDP backend: 同改 miles/backends/fsdp_utils/actor.py,FSDP 后端参数与执行路径的持续演进;本 PR 又为该后端接入了真实 FLOPs 计算与吞吐指标。
- PR #2024 dashboard: read open phase markers regardless of age: 同属 dashboard / observability 模块(miles/dashboard/store.py 数据链路),本 PR 的告警与指标视图继续沿用并扩展 store 的 metric_series 能力。
参与讨论