Files
EVEngine/docs/usr/modules/tensor.md
T

263 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 张量模块
**脚本入口:** `eve.TF()`
执行 eager 张量运算,或用 Func 构图、编译并重复运行。编译管线采用类似
AITemplate 的静态编译思路:按图生成专用 GPU kernel、融合元素级算子链、静态
内存规划复用缓冲区、对 matmul 做自动调优。
## 基本用法
```squirrel
local tf = eve.TF();
local a = tf.ones2(2, 3);
local b = tf.fill2(2, 3, 2.0);
local c = tf.add(a, b);
print(c.get2(0, 0) + "\n");
```
```squirrel
// 编译一个可复用的“游戏 AI”策略网络:x @ W1 + b -> relu -> @ W2 -> softmax
local fn = tf.func();
local x = fn.input1(4);
local h = tf.matmul(tf.reshape2(x, 1, 4), W1); // W1 等 eager 张量会被捕获为常量
h = tf.add(h, b1);
h = tf.relu(h);
h = tf.matmul(h, W2);
h = tf.softmax(h, 1);
fn.setOutput(h);
local compiled = fn.compile(); // 融合 + 生成专用 kernel + 内存规划
local out = compiled.run1(state); // 每帧只做 feed + dispatch + 读回
```
## 对象关系与调用时机
`TF` 创建 eager Tensor 或 Func;Func 中的 Tensor 是 symbolic;`compile()` 返回
CompiledFunction;run 接收 eager feed 并返回 eager 输出。shape 与 dtype 必须匹配。
## AITemplate 风格编译管线
`compile()` 不再按“固定算子 + opcode”解释执行,而是:
1. **图优化**(`Optimizer`):DCE、常量折叠、元素级链融合、matmul/conv 的
bias + 激活后置融合、按生命周期做静态内存规划(不重叠的中间结果复用同一块
显存)。
2. **专用 kernel 代码生成**(`KernelGen`):每个融合组生成一份形状全部烘焙为
常量的 GLSL;softmax/layernorm 使用两遍 kernel;SDPA 使用共享内存的融合
attention;matmul 提供 naive 与 16x16 tiled 两种模板。
3. **进程内 GLSL→SPIR-V 编译**:Windows 上链接 Vulkan SDK 的 shaderc
(glslang + SPIRV-Tools)静态库,不依赖外部 glslc.exe;找不到 shaderc 时回退
到调用 glslc。
4. **matmul 自动调优**:编译时对 naive / tiled 两种变体各计时 5 次,选择更快者。
5. **整图单次提交**(`GpuBackend` + `gpgpu::Sequence`):一次 `run()` 把
placeholder 上传、所有融合组的 kernel dispatch、输出回读录制进同一个
command buffer,`submit()` 一次完成——而不是每个 kernel 各提交等待一次。
对 transformer 这类几十个 kernel 的推理图,可省掉几十次 GPU 提交往返。
当没有 Vulkan 设备或图无法降级到 GPU 时,`compile()` 自动回退到 CPU 参考解释器,
`getDevice()` 返回 `"gpu"` 或 `"cpu"`。
## 目标导向指南
### 游戏离线 AI(策略/行为模型)
用 `func()` 构图一次、每帧 `run`:`matmul` → `add`/`addScalar`(bias)→
`relu`/`gelu`/`silu` → `softmax` 得到动作概率,再用 `argmax` 取动作、用
`embedding` 查表。rank 3 的 `matmul` 支持批处理多个智能体。
### 常用 GPGPU 计算
广播二元运算(`[N,C] + [C]` 等)、`sumAxis/meanAxis/minAxis/maxAxis`、
`where`、`clamp` 都既支持 eager 也支持构图编译;大张量全量归约
(`reduceSum` 等)在超过 16384 元素时自动走 GPU reduction。
### 游戏中运行离线语音模型
语音模型常用算子都已内置:`conv1d`(特征提取)、`layernorm`/`rmsnorm`、
`gelu`/`silu`、`sdpa`/`sdpaMasked`(融合 attention,自带 softmax 与 mask)、
`embedding`、`slice`/`concat`/`permute`。`cast(x, "int32")` 得到索引张量供
`embedding` 使用。SDPA 限制 S ≤ 2048、D ≤ 512,超出自动回退 CPU。
### 快速计算地形地貌
`conv2d`/`maxpool2d`/`avgpool2d` 用于平滑、侵蚀、降采样;`resize2d`(最近邻 /
双线性)用于高度图放大;`where` + 比较组合实现掩膜;`randomUniform*` 提供种子化
噪声。这些都能构图后在 GPU 上批量处理整块地形。
### 大规模模拟
把模拟状态组织成批张量,用广播运算一次推进整批:`mulScalar`(时间步长)、
`add`/`sub`(状态更新)、`clamp`(边界)、`reduceSum`(统计)。把每帧固定形状
的更新写成 Func 编译一次,避免脚本侧逐元素循环。
### 跑一个真实的 mini LLM(TinyStories-gpt2-3M)
用预训练权重实测整个推理管线:`calum/tinystories-gpt2-3M`(8 层、d=64、16 头、
GPT-2 BPE 词表 50257,TinyStories 语料训练)。测试 [test/tensor_llm.cpp]
(../../../test/tensor_llm.cpp) 用 TF 算子逐层搭出完整 GPT-2 图:
`embedding`(token + 位置)→ 8 × { `layernormWB` → 多头 `sdpaMasked`(因果 mask)
→ 残差 → MLP(`gelu`) → 残差 } → `layernormWB` → `matmul(lm_head)` → `argmax`
贪婪采样。验证结果:
- 与 numpy 参考实现逐位置对比 softmax 概率:最大差 3e-7,top-1 一致 16/16;
- 编译路径(`func()` + `compile()`)与 eager 完全一致(logit 差 0),device 为
cpu 或 gpu(有 Vulkan 窗口时);
- 贪婪生成与 numpy 参考 24/24 token 一致,CPU eager 约 100 ms/token。
示例输出(prompt:*Once upon a time, there was a little*):
> Once upon a time, there was a little girl named Lily. She loved to play with
> her toys and make things. One day, she found a big box in...
资产(约 47 MB)已 gitignore,按以下步骤重建:
```bash
# 1) 从 https://huggingface.co/calum/tinystories-gpt2-3M 下载 4 个文件到
# test/assets/mini_llm/:pytorch_model.bin / config.json / vocab.json / merges.txt
# (文件名带 tinystories_ 前缀)
# 2) 导出引擎权重包 + token 表 + numpy 参考输出:
python scripts/export_tiny_gpt2.py
# 3) 运行测试:
build/test/unit_test.exe --testcase="tensor.llm.*"
```
### 权重量化(fp16 / fp8 / fp4 / int8 / int4)
推理管线内置权重-only 量化:把 eager 权重打包成紧凑 dtype,构图时捕获为量化
Const,CPU 解释器与 GPU kernel 都在使用点反量化,激活保持 fp32。
```squirrel
local wq = tf.quantizeWeight(W, "int8", 64); // 每 64 个元素一个 block scale
local wq = tf.quantizeWeight(W, "fp16"); // fp16 为纯 half 存储,无 scale
```
格式与布局(`Tensor.get(i)` 直接返回反量化后的值,`isQuantized()` 可查询):
- `fp16`:IEEE half,2 字节/元素,无 scale;
- `fp8`:e4m3(1 字节)+ per-group block scale;
- `fp4`:e2m1(2 元素/字节)+ per-group block scale;
- `int8` / `int4`:对称量化 + per-group block scale(int4 两元素/字节)。
支持量化的使用点:matmul 的权重输入(B)、embedding 表、lm_head(transpose 后)。
GPU 侧在生成的 GLSL kernel 内联反量化(naive matmul 与 embedding),不落回
fp32 缓冲;LN 的 scale/bias 与线性 bias 建议保持 fp32(测试默认如此)。
TinyStories 模型全矩阵量化(group=64)实测(top-1 贪婪一致率 vs fp32,16 个
预测位;权重体积 vs fp32 27.6 MB):
| dtype | 概率最大差 | top-1 | 体积 |
|-------|-----------|-------|------|
| fp16 | 0.0004 | 16/16 | 13.2 MB |
| int8 | 0.0103 | 16/16 | 7.0 MB |
| fp8 | 0.0716 | 15/16 | 6.6 MB |
| int4 | 0.1767 | 16/16 | 3.7 MB |
| fp4 | 0.3961 | 16/16 | 3.3 MB |
fp8/fp4 的 block scale 取 `maxAbs / 格式最大幅值`(e4m3=240、e2m1=6),让归一化
值铺满整个格式动态范围——若只归一化到 [0,1],e2m1 会浪费 2/3/4/6 等电平,误差
会大一个数量级(早期版本的 0.94 就是这样来的)。
量化正确性由 `tensor.quant.*` 与 `tensor.llm.quantizedDtypes` 测试覆盖(含
GPU 编译路径),资产缺失时自动跳过。
## C++ 原生 ONNX 导入与量化 GPU 推理
`tensor/OnnxModel.h` 直接导入 ONNX,保留 int8/uint8 权重、仿射 scale/zero-point
及精确 int32/int64 数据。`run()` 执行 CPU 参考路径;`runGpu()` 接收
`createOnnxGpuCompute()` 返回的引擎 Gpgpu 适配器,在 Vulkan 设备线程上同步执行。
GPU 负责量化矩阵乘、卷积、LSTM 投影、浮点神经网络计算;形状、索引、控制流、
LSTM 门控仍由 CPU 处理;动态量化在 GPU 上计算,只回读少量校验标量。
GPU 错误明确返回,不会自动改用 CPU 重试。
已用原版 Kokoro v1.1 INT8 模型跑通中文语音整图,包括 Loop/If/Sequence。
接口使用 owning Result,输出不依赖模型生命周期,随机激励可指定种子。
当前 ONNX GPU 执行已使用驻留缓冲区和批量 Sequence 提交;连续 GPU 节点之间不回读,
仅在 CPU 数据依赖边界下载。应在推理循环外创建并保留 `createOnnxGpuCompute()`
返回的 GPU 会话,跨调用复用管线、权重和缓冲区池;Graphics 销毁前自动清理,
失效会话明确拒绝执行。实测同进程第二次合成约 2.60 秒,仍需优化 LSTM CPU 门控等开销。文字前处理、Squirrel
绑定及 dialogue 播放接入不在此示例范围内。
完整构建命令、模型资源、数值契约、限制和可重复语音测试见
[原生 ONNX 示例](../../../examples/tensor/onnx/README.md)。
## 常见问题
- 对 symbolic Tensor 调用 `get()`。
- matmul 内维度不一致;rank 3 批处理要求 batch 相同。
- 每帧重新 func/compile,而不是复用 CompiledFunction。
- GPU 编译需要已创建 Vulkan 窗口(`gpgpu.isAvailable()` 为真);否则回退 CPU。
- `argmax`/`cast("int32")` 得到 int32 dtype 张量,脚本读取仍返回 float。
## API 快查
下列方法名来自当前 Squirrel 绑定;同一模块创建的辅助对象(例如 `Tensor`、
`Func`、`CompiledFunction`)的方法也列在这里。
### TF(`tf.` 方法)
- `abs()`、`add()`、`addScalar()`、`arange()`、`argmax()`
- `avgpool2d()`、`cast()`、`clamp()`、`concat2()`、`concat3()`、`concat4()`
- `constantScalar()`、`conv1d()`、`conv1dBias()`、`conv2d()`、`conv2dBias()`
- `cos()`、`div()`、`divScalar()`、`embedding()`、`exp()`、`eye()`
- `fill1()`、`fill2()`、`fill3()`、`fill4()`、`flatten()`、`func()`
- `gelu()`、`getRandomSeed()`、`layernorm()`、`layernormWB()`、`linspace()`
- `log()`、`logSoftmax()`、`matmul()`、`maxAxis()`、`maximumScalar()`
- `maxpool2d()`、`meanAxis()`、`minAxis()`、`minimumScalar()`
- `mulScalar()`、`multiply()`、`neg()`、`ones1()`、`ones2()`、`ones3()`
- `ones4()`、`ones5()`、`ones6()`、`permute2()`、`permute3()`、`permute4()`
- `permute5()`、`permute6()`、`powScalar()`、`rand1()`、`rand2()`、`rand3()`
- `quantizeWeight()`、`rand4()`、`randn1()`、`randn2()`、`randn3()`、`randn4()`、`randomNormal1()`
- `randomNormal2()`、`randomNormal3()`、`randomNormal4()`、`randomUniform1()`
- `randomUniform2()`、`randomUniform3()`、`randomUniform4()`
- `reduceMax()`、`reduceMean()`、`reduceMin()`、`reduceSum()`、`relu()`
- `reshape1()`、`reshape2()`、`reshape3()`、`reshape4()`、`reshape5()`
- `reshape6()`、`resize2d()`、`rmsnorm()`、`rmsnormW()`、`sdpa()`
- `sdpaMasked()`、`setRandomSeed()`、`sigmoid()`、`silu()`、`sin()`
- `slice()`、`softmax()`、`sqrt()`、`sub()`、`subScalar()`、`sumAxis()`
- `tanh()`、`transpose()`、`where()`、`zeros1()`、`zeros2()`、`zeros3()`
- `zeros4()`、`zeros5()`、`zeros6()`
量化相关:`quantizeWeight(t, "fp16"/"fp8"/"fp4"/"int8"/"int4", group)`;
`t.isQuantized()`;量化张量的 `get(i)` 返回反量化值。
### Tensor(`tf.*` 返回对象的 `t.` 方法)
- `abs()`、`add()`、`addInPlace()`、`addScalar()`、`addScalarInPlace()`
- `clone()`、`copyFrom()`、`cos()`、`div()`、`divScalar()`、`dot()`
- `exp()`、`fill()`、`flatten()`、`gelu()`、`get()`、`get1()`、`get2()`
- `get3()`、`get4()`、`get5()`、`get6()`、`getDevice()`、`getDim()`
- `getDim0()`、`getDim1()`、`getDim2()`、`getDim3()`、`getDim4()`、`getDim5()`
- `getDtype()`、`getRank()`、`getSize()`、`isEager()`、`isSymbolic()`、`log()`
- `matmul()`、`maximumScalar()`、`minimumScalar()`、`mulScalar()`
- `mulScalarInPlace()`、`multiply()`、`multiplyInPlace()`、`neg()`
- `powScalar()`、`reduceMax()`、`reduceMean()`、`reduceMin()`、`reduceSum()`
- `relu()`、`reluInPlace()`、`reshape1()`、`reshape2()`、`reshape3()`
- `reshape4()`、`reshape5()`、`reshape6()`、`set()`、`set1()`、`set2()`
- `set3()`、`set4()`、`set5()`、`set6()`、`sigmoid()`、`silu()`、`sin()`
- `sqrt()`、`sub()`、`subScalar()`、`tanh()`、`transpose()`
### Func(`tf.func()` 返回对象的 `fn.` 方法)
- `compile()`、`input1()`、`input2()`、`input3()`、`input4()`、`input5()`
- `input6()`、`setOutput()`
### CompiledFunction(`fn.compile()` 返回对象的 `c.` 方法)
- `getDevice()`、`getPlaceholderCount()`、`run0()`、`run1()`、`run2()`
- `run3()`、`run4()`、`run5()`、`run6()`
## 使用要点
- 模块对象和它创建的资源对象应保存在全局或实体状态中,不要在每帧重复创建。
- 带 `update(dt)` 的系统应在 `eve_update` 调用;绘制方法应在 `eve_render` 调用。
- 参数约束、默认值和返回类型以对应模块头文件及 `addFunc` 绑定为准;本文 API 快查与当前源码同步生成。
- 张量支持 rank 1–6,dtype 为 float32 / int32;二元运算支持广播。
- 原生 ONNX GPU 会话使用有界 shader 编译队列,`createOnnxGpuCompute(compilerWorkers)`
可选 1–8 个 CPU 编译线程(默认 4);同源码去重,编译器上下文和管线缓存均复用。
Vulkan 管线创建与提交仍在设备线程,`runGpu` 保持同步返回;并发数应按目标构建实测选择。
- GPU 路径:`compile()` 需要已初始化的 Vulkan Graphics(先创建窗口)。Windows
上 GLSL→SPIR-V 由链接进引擎的 shaderc 静态库完成,不需要安装 glslc。
**源码:** [`src/modules/tensor/`](../../../src/modules/tensor/)(含
`Optimizer` 图优化/融合、`KernelGen` 专用 kernel 生成、`GpuBackend` GPU 运行时)
**相关测试:** 在 [`test/`](../../../test/) 中搜索 `tensor`。