操作指南#

本指南描述 Observable Library M2 已实现的行为,面向包用户编写;未来路线图能力会被明确标出。

完成快速开始后再使用这些示例。每个示例都保持在 0.1.0 公共边界内。

generate() 会生成什么#

generate(model, reductions=..., transforms=...) 扫描 model.named_parameters() 返回的每个条目,为每个参数和请求的 reduction 生成一个可观测量,并为每个可观测量附加相同的有序 transform 链。

M2 不公开 sources= 参数,也不生成 activation.*grad.*loss 可观测量。过滤返回列表可以减少 Runtime 工作,不过参数元数据已经完成枚举:

generated = ol.generate(model, reductions=["l2_norm"])
wanted = {"param.0.weight", "param.2.weight"}
selected = [item for item in generated if item.spec.source in wanted]

参数、gradient 和 activation 的生成阶段 template filter 属于 M3;其精确的 source 选择 API 尚未成为公共契约。

Transform 顺序与验证#

Transform 从左到右运行。例如,transforms=["center", "normalize"] 执行:

normalize(center(tensor)) -> reduction

系统不会生成子集、反向顺序或排列。每个 transform 必须在调用 generate() 前注册,重复名称会被拒绝。Registry 验证名称而非张量契约;不兼容的 rank、shape、dtype 或 device 假设会在可观测量运行时失败。

生成的可观测量会捕获已解析的 transform 和 reduction callable,因此后续 registry 变更不能静默改变已有可观测量的计算。

在线 source 生命周期#

HookSource.get("param.*", step) 直接读取当前模型参数。仅含参数的生成可观测量不需要 hooks,因此这种情况下 attach() 是可选的。

activation.*grad.* 值在 forward 和 backward 期间产生。请在这些操作前调用 source.attach(),完成后调用 source.detach()。应在 loss.backward() 之后、optimizer.step() 之前观测 gradients。

当前 attach() 行为范围较广:它在所有顶层子模块上安装 activation hooks,在所有可训练参数上安装 gradient hooks。它存储最近捕获的张量,不跟踪每个 step 的新鲜度。每次调用 observe() 前都要运行匹配的 forward/backward,否则旧值可能仍在缓存中。Activation id 使用 activation.<top-level-child-name>,gradient id 使用 grad.<parameter-name>

Loss 不会被自动发现。要使用 source="loss" 可观测量,请先调用 source.record_loss(loss, step)

一次 observe() 调用内,Runtime 会缓存每个请求的 source,因此同一张量上的多个 reduction 只执行一次 source lookup。

混合生成与自定义可观测量#

generate() 返回 list[Observable]。构造 Pack 前可以加入高级自定义可观测量:

generated = ol.generate(model, reductions=["l2_norm"])
gradient_spec = ol.ObservableSpec(
    source="grad.0.weight",
    selector="all",
    reduction="l2_norm",
    budget_hint={"compute_ms": 0.01},
)
gradient = ol.Observable(
    spec=gradient_spec,
    compute=lambda tensors, _context: tensors[gradient_spec.source].norm(),
)
runtime = ol.Runtime(ol.Pack([*generated, gradient]), source=source)

Runtime 要求每个可观测量 id 唯一。Spec 与生成项相同的自定义可观测量会被拒绝。启用 Budget 时,自定义可观测量应提供现实的 budget_hint;缺失估算按零计算。

用户自定义 Filters#

M2 提供 Filter 基类以及 & / | 组合,但没有内置 BySourceByReduction filter。用户自定义 filter 在生成后生效:

class ByReduction(ol.Filter):
    def __init__(self, name: str) -> None:
        self.name = name

    def apply(self, observables):
        return [
            item for item in observables if item.spec.reduction == self.name
        ]

keep = ByReduction("l2_norm") | ByReduction("mean")
selected = keep(observables)

这会阻止未选中的可观测量运行,但不会阻止它们最初被生成。真正的生成阶段 template filter 计划属于 M3。

Identity、结果与查询#

系统没有单独的用户可见 name 字段。observable.spec.id 是从完整 spec 确定性派生的 16 字符哈希,覆盖 source、selector、有序 transforms、reduction、temporal 设置、frequency 和 budget hint。修改任何 identity 字段都可能改变 id。

Runtime.observe() 返回 dict[observable_id, value]LocalStorage 在 SQLite 中存储元数据,在 NumPy NPZ 文件中存储数组 payload;它与 query() 使用相同的精确 id 和 step:

observable = observables[0]
values = runtime.observe(step=7)
value = values[observable.spec.id]
stored = ol.query(storage, observable.spec.id, step=7)

用于日志或用户界面时,可以派生显示标签,但不要把它当作存储键:

spec = observable.spec
chain = " -> ".join((*spec.transforms, spec.reduction))
label = f"{spec.source} | {chain}"

M2 只支持按精确 id/step 读回。按 source、reduction、tag 或显示标签查询属于后续分析工具。