跳转至

架构概述

本文档介绍 Hyperparameter 的内部架构,包括 Rust 后端和 Python 前端如何协同工作。

整体架构

┌─────────────────────────────────────────────────────────────┐
│                     Python 用户代码                          │
│  @hp.param, scope, hp.config() 等                 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                   Python API 层                              │
│  hyperparameter/api.py, hyperparameter/cli.py               │
│  - 装饰器 (@hp.param)                                     │
│  - 上下文管理器 (scope)                                │
│  - CLI 参数解析                                             │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                   存储抽象层                                  │
│  hyperparameter/storage.py                                  │
│  - TLSKVStorage (线程本地存储)                               │
│  - 自动后端选择                                              │
└─────────────────────────────────────────────────────────────┘
              ┌───────────────┴───────────────┐
              ▼                               ▼
┌─────────────────────────┐    ┌─────────────────────────────┐
│   Rust 后端             │    │   Python 回退后端            │
│   (librbackend.so)      │    │   (纯 Python 字典)          │
│   - xxhash 键哈希       │    │   - Rust 不可用时使用        │
│   - 线程本地存储        │    │   - 相同的 API 契约          │
│   - 无锁读取            │    │                              │
└─────────────────────────┘    └─────────────────────────────┘

组件详解

1. Python API 层 (hyperparameter/api.py)

这是用户直接交互的部分。

核心类:

  • scope: 创建新参数作用域的上下文管理器。

    with hp.scope(foo=1, bar=2) as ps:
        # ps.foo() 返回 1
        # 嵌套作用域从父作用域继承
    

  • _ParamAccessor: 处理 hp.scope.x.y.z | default 语法。

    # 这个链式调用: hp.scope.model.layers.size | 10
    # 创建: _ParamAccessor(root, "model.layers.size")
    # `|` 运算符调用 get_or_else(10)
    

  • param 装饰器: 检查函数签名并注入值。

    @hp.param("model")
    def foo(hidden_size=256):  # 查找 "model.hidden_size"
        pass
    

2. 存储层 (hyperparameter/storage.py)

存储层抽象了底层的键值存储。

核心特性:

  • 线程本地存储 (TLS): 每个线程有自己的参数栈。
  • 作用域更新: 更改仅限当前作用域,退出时回滚。
  • 后端选择: 如果可用则自动使用 Rust 后端。
class TLSKVStorage:
    """带作用域栈的线程本地键值存储"""

    def enter(self):
        """将新作用域压入栈"""

    def exit(self):
        """弹出当前作用域,回滚更改"""

    def get(self, key: str) -> Any:
        """在当前作用域查找键,然后查找父作用域"""

    def put(self, key: str, value: Any):
        """仅在当前作用域设置键"""

3. Rust 后端 (src/core/, src/py/)

Rust 后端提供高性能的参数访问。

为什么用 Rust?

  1. 编译时键哈希: 像 "model.layers.size" 这样的键在编译时使用 xxhash 哈希,消除了运行时字符串哈希开销。

  2. 无锁读取: 线程本地存储意味着读取时没有互斥锁竞争。

  3. 零拷贝字符串处理: Rust 的字符串处理避免了 Python 字符串驻留的开销。

核心 Rust 组件:

// src/core/src/storage.rs
pub struct ThreadLocalStorage {
    stack: Vec<HashMap<u64, Value>>,  // 作用域栈
}

// src/core/src/xxh.rs
pub const fn xxhash(s: &str) -> u64 {
    // 编译时 xxhash64
}

// src/core/src/api.rs
pub fn get_param<T>(key_hash: u64, default: T) -> T {
    // 通过预计算哈希快速查找
}

Python 绑定 (src/py/):

使用 PyO3 将 Rust 函数暴露给 Python:

#[pyfunction]
fn get_entry(key_hash: u64) -> PyResult<PyObject> {
    // 从 Python 调用,使用预计算的哈希
}

4. 配置加载器 (hyperparameter/loader.py)

加载器处理配置文件解析和处理。

处理流水线:

文件 → 解析 → 合并 → 插值 → 校验 → 字典/对象
  1. 解析: 支持 TOML、JSON、YAML
  2. 合并: 深度合并多个配置(后者覆盖前者)
  3. 插值: 解析 ${variable} 引用
  4. 校验: 可选的基于类类型提示的 Schema 校验
def load(path, schema=None):
    config = _load_and_merge(path)
    config = _resolve_interpolations(config)
    if schema:
        return validate(config, schema)
    return config

数据流示例

让我们追踪运行以下代码时发生了什么:

import hyperparameter as hp

@hp.param("model")
def train(lr=0.001):
    print(lr)

with hp.scope(**{"model.lr": 0.01}):
    train()

逐步分析:

  1. scope(**{"model.lr": 0.01}):
  2. 创建新的 TLSKVStorage 作用域
  3. 计算哈希: xxhash("model.lr")0x1234...
  4. 存储: {0x1234...: 0.01} 到当前线程的作用域栈

  5. train() 被调用:

  6. @hp.param 包装器运行
  7. 对每个有默认值的参数 (lr=0.001):
    • 计算哈希: xxhash("model.lr")
    • 调用 storage.get_entry(0x1234...)
    • Rust 后端返回 0.01
  8. 调用 train(lr=0.01)

  9. 作用域退出:

  10. hp.scope.__exit__() 被调用
  11. 从栈中弹出作用域
  12. model.lr 不再可访问

性能特征

为什么 Hyperparameter 快

操作 Hydra/OmegaConf Hyperparameter
键查找 运行时字符串哈希 预计算 xxhash
类型检查 每次访问都检查 可选,加载时检查
线程安全 全局锁 线程本地(无锁)
内存 Python 字典 + 包装器 Rust HashMap

何时使用哪种访问模式

模式 速度 使用场景
@hp.param 注入 🚀🚀🚀 最快 热循环,性能关键
with hp.scope() as ps: ps.x 🚀🚀 快 大多数代码
hp.scope.x (全局) 🚀 中等 便捷访问,一次性访问

线程安全模型

线程 1                      线程 2
──────                      ──────
hp.scope(a=1)           
│                          scope(a=2)
│   a = 1                  │   a = 2
│                          │
└── 退出                   └── 退出
    a = 未定义                 a = 未定义

每个线程有独立的作用域栈。一个线程的更改永远不会影响另一个线程。

frozen() 用于跨线程默认值:

with hp.scope(a=1):
    hp.scope.frozen()  # 将当前作用域快照为全局默认值

# 新线程将以 a=1 作为初始状态

扩展 Hyperparameter

自定义存储后端

from hyperparameter.storage import TLSKVStorage

class RedisBackedStorage(TLSKVStorage):
    """示例: 用于分布式系统的 Redis 后端存储"""

    def get(self, key):
        # 先尝试本地
        value = super().get(key)
        if value is None:
            # 回退到 Redis
            value = self.redis.get(key)
        return value

自定义类型转换

from hyperparameter.loader import _coerce_type

def _coerce_type(value, target_type):
    # 添加自定义类型处理
    if target_type is MyCustomType:
        return MyCustomType.from_string(value)
    # ... 现有逻辑

文件结构

hyperparameter/
├── __init__.py          # 公共 API 导出
├── api.py               # 核心 Python API (scope, param)
├── cli.py               # CLI 支持 (launch, launch)
├── loader.py            # 配置加载、插值、校验
├── storage.py           # 存储抽象、TLS
└── tune.py              # 超参调优工具

src/
├── core/                # Rust 核心库
│   └── src/
│       ├── api.rs       # 公共 Rust API
│       ├── storage.rs   # 线程本地存储
│       ├── value.rs     # 值类型处理
│       └── xxh.rs       # 编译时 xxhash
├── macros/              # Rust 过程宏
└── py/                  # PyO3 Python 绑定