helion-jagged-and-autotuning
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseHelion: Jagged Tensors & Autotuning/Config Management
Helion:锯齿张量与自动调优/配置管理
Overview
概述
Two high-value Helion areas that are easy to get wrong or miss entirely:
- Ragged/jagged tensors — iterates variable-length inner dimensions with implicit masking, so you never hand-build masks.
hl.jagged_tile() - Autotuning & config management — autotuning is slow; Helion has a layered system (on-disk cache → saved configs → AOT heuristics) for tuning once and reusing results keyed by GPU architecture and input shape.
The single most-missed feature: AOT heuristics (
helion.experimental.aot_kernel- ) give zero-cost per-shape config selection at runtime, with automatic compute-capability fallback. If a request mentions "many GPUs," "many shapes," or "don't want to re-tune at deploy," reach for AOT, not just the cache.
python -m helion.experimental.aot_runner
Helion中两个极易出错或被完全忽略的高价值领域:
- 不规则/锯齿张量 —— 通过隐式掩码迭代可变长度的内部维度,你完全无需手动构建掩码。
hl.jagged_tile() - 自动调优与配置管理 —— 自动调优耗时较长;Helion提供了分层系统(磁盘缓存 → 已保存配置 → AOT启发式算法),实现一次调优,按GPU架构和输入形状为键复用结果。
最容易被忽略的核心特性:AOT启发式算法( + )可在运行时实现零成本的按形状配置选择,并支持自动计算能力降级。如果需求中提到“多GPU”、“多形状”或“不想在部署时重新调优”,请优先使用AOT,而不仅仅是缓存。
helion.experimental.aot_kernelpython -m helion.experimental.aot_runnerPart 1 — Ragged / Jagged Tensors
第一部分 — 不规则/锯齿张量
Data layout
数据布局
Jagged data is stored prefix-packed: a flat buffer holding all rows
concatenated, plus an tensor of length where row is
. Per-row length = .
x_datax_offsetsnum_rows + 1ix_data[x_offsets[i] : x_offsets[i+1]]ends - starts锯齿数据采用前缀打包存储:一个扁平的缓冲区存储所有拼接后的行,外加一个长度为的张量,第行对应。每行长度 = 。
x_datanum_rows + 1x_offsetsix_data[x_offsets[i] : x_offsets[i+1]]ends - startshl.jagged_tile(parent)
— the core API
hl.jagged_tile(parent)hl.jagged_tile(parent)
—— 核心API
hl.jagged_tile(parent)hl.jagged_tile(parent)hl.tile()parenthl.tile(parent.amax())tile_k.index >= parent[lane]python
import torch
import helion
import helion.language as hl
@helion.kernel()
def jagged_sum(x_data: torch.Tensor, x_offsets: torch.Tensor) -> torch.Tensor:
b = x_offsets.size(0) - 1
out = torch.zeros([b], dtype=x_data.dtype, device=x_data.device)
for tile_b in hl.tile(b):
starts = x_offsets[tile_b]
ends = x_offsets[tile_b.index + 1] # note: tile_b.index + 1 for the upper offset
lengths = ends - starts
acc = hl.zeros([tile_b], dtype=x_data.dtype)
for tile_k in hl.jagged_tile(lengths): # implicit masking, no manual mask
idx = starts[:, None] + tile_k.index[None, :]
acc = acc + x_data[idx].sum(dim=1)
out[tile_b] = acc
return outhl.jagged_tile(parent)hl.tile()parenthl.tile(parent.amax())tile_k.index >= parent[lane]python
import torch
import helion
import helion.language as hl
@helion.kernel()
def jagged_sum(x_data: torch.Tensor, x_offsets: torch.Tensor) -> torch.Tensor:
b = x_offsets.size(0) - 1
out = torch.zeros([b], dtype=x_data.dtype, device=x_data.device)
for tile_b in hl.tile(b):
starts = x_offsets[tile_b]
ends = x_offsets[tile_b.index + 1] # note: tile_b.index + 1 for the upper offset
lengths = ends - starts
acc = hl.zeros([tile_b], dtype=x_data.dtype)
for tile_k in hl.jagged_tile(lengths): # implicit masking, no manual mask
idx = starts[:, None] + tile_k.index[None, :]
acc = acc + x_data[idx].sum(dim=1)
out[tile_b] = acc
return outRules & gotchas for jagged_tile
jagged_tilejagged_tile
的规则与常见陷阱
jagged_tile- must be rank ≥ 1 (no scalars); every axis must come from an enclosing tile context. The 1-D "per-row length" case is the common one.
parent - It cannot be the outermost loop of a kernel — it needs a parent tile.
- A jagged child tile must be indexed with its parent axes: is valid,
x[tile_b, tile_k]alone is not.x[tile_k] - Nest for multi-level ragged iteration (e.g. variable rows × variable features). See
jagged_tile.jagged_mean.py - Use plain (not jagged) when the inner bound is uniform across lanes.
hl.tile() - The manual equivalent (when you need it): with
for tile_k in hl.tile(lengths.amax())passed toextra_mask=tile_k.index[None, :] < lengths[:, None].hl.load
- 必须秩≥1(不能是标量);每个轴都必须来自外层tile上下文。一维“逐行长度”是最常见的场景。
parent - 它不能作为内核的最外层循环——需要一个父tile。
- 子锯齿tile必须使用父轴进行索引:是合法的,单独使用
x[tile_b, tile_k]则不合法。x[tile_k] - 可嵌套实现多级不规则迭代(例如可变行数 × 可变特征数),参见
jagged_tile。jagged_mean.py - 当内部边界在所有通道上一致时,请使用普通的(而非锯齿版本)。
hl.tile() - 手动实现方式(当你需要时):,并向
for tile_k in hl.tile(lengths.amax())传入hl.load。extra_mask=tile_k.index[None, :] < lengths[:, None]
Reference examples (in the Helion repo examples/
)
examples/参考示例(位于Helion仓库的examples/
目录下)
examples/| File | Pattern |
|---|---|
| basic per-row reduction |
| nested |
| online/Welford reduction over ragged dim |
| per-row normalization |
| ragged + dense, manual |
| jagged matmul / attention |
| 文件 | 模式 |
|---|---|
| 基础逐行归约 |
| 嵌套 |
| 不规则维度上的在线/Welford归约 |
| 逐行归一化 |
| 不规则+密集,手动 |
| 锯齿矩阵乘法 / 注意力机制 |
Part 2 — Autotuning & Config Management
第二部分 — 自动调优与配置管理
Pick the right mechanism
选择合适的机制
dot
digraph config_mgmt {
"Need a tuned config?" [shape=diamond];
"One shape, one GPU?" [shape=diamond];
"Fixed known shapes (a handful)?" [shape=diamond];
"Many shapes / many GPUs / deploy without re-tuning?" [shape=diamond];
"Share across machines/CI?" [shape=diamond];
"Save config(); @kernel(config=...)" [shape=box];
"@kernel(configs=[...]) + key=" [shape=box];
"AOT heuristic (aot_kernel + aot_runner)" [shape=box];
"Remote cache backend" [shape=box];
"Default: on-disk autotune cache" [shape=box];
"Need a tuned config?" -> "One shape, one GPU?";
"One shape, one GPU?" -> "Save config(); @kernel(config=...)" [label="yes"];
"One shape, one GPU?" -> "Fixed known shapes (a handful)?" [label="no"];
"Fixed known shapes (a handful)?" -> "@kernel(configs=[...]) + key=" [label="yes"];
"Fixed known shapes (a handful)?" -> "Many shapes / many GPUs / deploy without re-tuning?" [label="no"];
"Many shapes / many GPUs / deploy without re-tuning?" -> "AOT heuristic (aot_kernel + aot_runner)" [label="yes"];
"Many shapes / many GPUs / deploy without re-tuning?" -> "Share across machines/CI?" [label="no"];
"Share across machines/CI?" -> "Remote cache backend" [label="yes"];
"Share across machines/CI?" -> "Default: on-disk autotune cache" [label="no"];
}dot
digraph config_mgmt {
"Need a tuned config?" [shape=diamond];
"One shape, one GPU?" [shape=diamond];
"Fixed known shapes (a handful)?" [shape=diamond];
"Many shapes / many GPUs / deploy without re-tuning?" [shape=diamond];
"Share across machines/CI?" [shape=diamond];
"Save config(); @kernel(config=...)" [shape=box];
"@kernel(configs=[...]) + key=" [shape=box];
"AOT heuristic (aot_kernel + aot_runner)" [shape=box];
"Remote cache backend" [shape=box];
"Default: on-disk autotune cache" [shape=box];
"Need a tuned config?" -> "One shape, one GPU?";
"One shape, one GPU?" -> "Save config(); @kernel(config=...)" [label="yes"];
"One shape, one GPU?" -> "Fixed known shapes (a handful)?" [label="no"];
"Fixed known shapes (a handful)?" -> "@kernel(configs=[...]) + key=" [label="yes"];
"Fixed known shapes (a handful)?" -> "Many shapes / many GPUs / deploy without re-tuning?" [label="no"];
"Many shapes / many GPUs / deploy without re-tuning?" -> "AOT heuristic (aot_kernel + aot_runner)" [label="yes"];
"Many shapes / many GPUs / deploy without re-tuning?" -> "Share across machines/CI?" [label="no"];
"Share across machines/CI?" -> "Remote cache backend" [label="yes"];
"Share across machines/CI?" -> "Default: on-disk autotune cache" [label="no"];
}Tune once, save, reload
一次调优,保存与重载
Kernel.autotune(args)helion.ConfigConfigpython
config = my_kernel.autotune(example_inputs) # returns helion.Config
config.save("configs/my_kernel.json") # atomic write
best = helion.Config.load("configs/my_kernel.json")
@helion.kernel(config=best) # one config, applied to ALL shapes/dtypes/devices
def my_kernel(x, y): ...Other methods: / , ,
(drop default-valued keys before saving).
Config.to_json().from_json(str).from_dict(d).minimize(config_spec)Tune across several representative shapes and save per-shape:
python
for tag, args in datasets.items():
my_kernel.autotune(args).save(f"configs/my_kernel_{tag}.json")Kernel.autotune(args)helion.ConfigConfigpython
config = my_kernel.autotune(example_inputs) # returns helion.Config
config.save("configs/my_kernel.json") # atomic write
best = helion.Config.load("configs/my_kernel.json")
@helion.kernel(config=best) # one config, applied to ALL shapes/dtypes/devices
def my_kernel(x, y): ...Config.to_json().from_json(str).from_dict(d).minimize(config_spec)可针对多个代表性形状进行调优,并按形状保存:
python
for tag, args in datasets.items():
my_kernel.autotune(args).save(f"configs/my_kernel_{tag}.json")The on-disk autotune cache (automatic, keyed by arch + shape)
磁盘自动调优缓存(自动生效,按架构+形状为键)
By default the first call autotunes and caches the result keyed on hardware,
specialization (shape/dtype/stride), CUDA/ROCm runtime, backend, and kernel source
hash — so an H100 and a B200, or two different shapes, get separate entries and
each is auto-selected on later runs. No code needed.
| Env var | Effect |
|---|---|
| cache root (default: torch cache dir |
| cache class: |
| ignore cached config, re-tune, write result back |
| skip cache read AND write |
| fail if no cached config (CI guard) |
| |
| wall-clock cap on tuning |
默认情况下,首次调用会执行自动调优,并以硬件、特化维度(形状/数据类型/步长)、CUDA/ROCm运行时、后端、内核源码哈希为键缓存结果——因此H100和B200,或是两种不同的形状,会有独立的缓存条目,后续运行时会自动选择对应配置。无需编写额外代码。
| 环境变量 | 作用 |
|---|---|
| 缓存根目录(默认:torch缓存目录 |
| 缓存类: |
| 忽略缓存配置,重新调优,并回写结果 |
| 跳过缓存的读取和写入 |
| 若无缓存配置则报错(CI防护) |
| |
| 调优的墙上时钟时间上限 |
Deploy a handful of known configs (configs=
+ key=
)
configs=key=部署少量已知配置(configs=
+ key=
)
configs=key=python
@helion.kernel(
configs=[helion.Config.load("small.json"), helion.Config.load("large.json")],
key=lambda x, y: helion.next_power_of_2(x.numel()), # re-benchmark bucket
static_shapes=False,
)
def my_kernel(x, y): ...Helion runs a lightweight benchmark of the listed configs the first time each
specialization key is seen and picks the fastest. controls when it
re-selects (on top of shape specialization).
key=python
@helion.kernel(
configs=[helion.Config.load("small.json"), helion.Config.load("large.json")],
key=lambda x, y: helion.next_power_of_2(x.numel()), # re-benchmark bucket
static_shapes=False,
)
def my_kernel(x, y): ...首次遇到每个特化键时,Helion会对列出的配置进行轻量级基准测试,然后选择最快的一个。参数控制何时重新选择配置(在形状特化的基础上)。
key=Seed tuning from previously cached/best configs
从先前缓存/最优配置中种子化调优
helion.from_cache()autotuner_fnpython
@helion.kernel(autotuner_fn=helion.from_cache(max_configs=5))
def my_kernel(x, y): ...helion.from_cache()autotuner_fnpython
@helion.kernel(autotuner_fn=helion.from_cache(max_configs=5))
def my_kernel(x, y): ...AOT heuristics — zero-cost per-shape selection across GPUs (the headline feature)
AOT启发式算法 —— 跨GPU零成本按形状选择配置(核心特性)
For "ship a kernel that serves many shapes on many GPUs without runtime tuning":
offline, sweep the kernel over representative shapes, tune each, and distill a
decision-tree heuristic that picks a config in microseconds at runtime.
-
Decorate withinstead of
helion.experimental.aot_kernel:helion.kernelpythonimport helion.experimental @helion.experimental.aot_kernel() # extras: batched=..., key=fn, collect_fn, measure_fn def vector_add(x, y): ... -
On the target GPU, run the AOT workflow against a benchmark that exercises the shape sweep (this is slow — full autotune per distinct shape, ~5–15 min each):bash
python -m helion.experimental.aot_runner -- python my_benchmark.py -
It emitsnext to the kernel source (e.g.
_helion_aot_<kernel>_<device>_sm<NN>.py). Commit it._helion_aot_vector_add_cuda_sm90.py -
Runtime lookup order:→ file matching the current compute capability → older compatible capabilities (e.g. on
$HELION_HEURISTIC_DIR, triessm120→sm120→sm100). One heuristic can serve multiple GPU generations. No file found → default config + one-time warning.sm90
To add a new GPU: get on that hardware, re-run , commit the new
alongside the existing ones. See for
worked examples (vector_add, softmax, layer_norm, rms_norm, cross_entropy, rope)
and for the full workflow.
aot_runner_helion_aot_*_sm<NN>.pypretuned_kernels/docs/deployment_autotuning.md适用于“发布一个内核,在多GPU上支持多种形状,无需运行时调优”的场景:离线阶段,针对代表性形状范围遍历内核,逐个调优,然后提炼出一个决策树启发式算法,可在运行时以微秒级速度选择配置。
-
使用替代
helion.experimental.aot_kernel装饰器:helion.kernelpythonimport helion.experimental @helion.experimental.aot_kernel() # extras: batched=..., key=fn, collect_fn, measure_fn def vector_add(x, y): ... -
在目标GPU上,运行AOT工作流,执行覆盖形状范围的基准测试(这一步较慢——每个不同形状都要完整自动调优,每个约5-15分钟):bash
python -m helion.experimental.aot_runner -- python my_benchmark.py -
它会在内核源码旁边生成文件(例如
_helion_aot_<kernel>_<device>_sm<NN>.py)。将该文件提交到代码库。_helion_aot_vector_add_cuda_sm90.py -
运行时查找顺序:→ 匹配当前计算能力的文件 → 更早的兼容计算能力(例如在
$HELION_HEURISTIC_DIR上,会依次尝试sm120→sm120→sm100)。一个启发式算法可支持多代GPU。若未找到文件 → 使用默认配置并给出一次性警告。sm90
要添加新GPU支持:在对应硬件上重新运行,将新生成的文件与现有文件一起提交即可。参见目录下的完整示例(vector_add、softmax、layer_norm、rms_norm、cross_entropy、rope),以及中的完整工作流说明。
aot_runner_helion_aot_*_sm<NN>.pypretuned_kernels/docs/deployment_autotuning.mdShare configs across machines (remote cache)
跨机器共享配置(远程缓存)
Implement (/, optional
for warm-start), then:
helion.autotuner.remote_cache.RemoteCacheBackendgetputlistbash
export HELION_REMOTE_CACHE_BACKEND=mypkg.cache.MyBackend # required to load it
export HELION_AUTOTUNE_CACHE=RemoteAutotuneCache # also write winners to remoteRead-through/write-through; local disk stays source of truth, remote outage
degrades gracefully. Use for team/CI/multi-node warm-starts.
实现(/方法,可选用于热启动),然后:
helion.autotuner.remote_cache.RemoteCacheBackendgetputlistbash
export HELION_REMOTE_CACHE_BACKEND=mypkg.cache.MyBackend # required to load it
export HELION_AUTOTUNE_CACHE=RemoteAutotuneCache # also write winners to remote采用读穿透/写穿透模式;本地磁盘仍是权威数据源,远程服务中断时会优雅降级。适用于团队/CI/多节点热启动场景。
Control which dimensions specialize (affects cache entries)
控制哪些维度参与特化(影响缓存条目)
| API | Where | Effect |
|---|---|---|
| decorator | specialize on exact shape/stride; best perf, one entry per shape |
| decorator | bucket dims into |
| inside kernel | that dim always a compile-time constant, every call |
| before call | specialize dims on specific tensors only |
| decorator | custom extra grouping for re-selection |
With , pin if tensors routinely
exceed elements to avoid an extra specialization boundary.
static_shapes=Falseindex_dtype=torch.int642**31| API | 位置 | 作用 |
|---|---|---|
| 装饰器 | 基于精确形状/步长特化;性能最佳,每个形状对应一个条目 |
| 装饰器 | 将维度分桶为 |
| 内核内部 | 该维度在每次调用时始终为编译时常量 |
| 调用前 | 仅对特定张量的维度进行特化 |
| 装饰器 | 自定义额外分组,用于重新选择配置 |
当时,如果张量元素数通常超过,请固定,以避免额外的特化边界。
static_shapes=False2**31index_dtype=torch.int64Advanced: manual routing / drop Helion at serving time
高级用法:手动路由 / 在服务时移除Helion依赖
python
bound = my_kernel.bind(example_inputs) # BoundKernel, tied to input types
run_small = bound.compile_config(small_cfg) # callable for a specific config
src = bound.to_triton_code(small_cfg) # export raw Triton sourcepython
bound = my_kernel.bind(example_inputs) # BoundKernel, tied to input types
run_small = bound.compile_config(small_cfg) # callable for a specific config
src = bound.to_triton_code(small_cfg) # export raw Triton sourceCommon Mistakes
常见错误
| Mistake | Fix |
|---|---|
| Hand-building masks for ragged loops | Use |
| Index with parent: |
| It needs an enclosing parent tile |
| Re-tuning on every deploy / new shape | AOT heuristics ( |
| Assuming there's no autotune CLI | There is: |
| One config fits all shapes; use |
| Re-tuning per machine for the same GPU/shape | Remote cache backend warm-starts the team |
| Editing AOT files for a new GPU by hand | Re-run |
| 错误 | 修复方法 |
|---|---|
| 为不规则循环手动构建掩码 | 使用 |
对子锯齿tile使用 | 加上父tile索引: |
将 | 它需要一个外层的父tile |
| 每次部署/遇到新形状都重新调优 | 使用AOT启发式算法( |
| 认为没有自动调优CLI | 有: |
对多变的形状使用 | 单个配置适用于所有形状;请改用 |
| 相同GPU/形状下每台机器都重新调优 | 远程缓存后端为团队提供热启动 |
| 手动编辑新GPU的AOT文件 | 在该GPU上重新运行 |