263 lines
14 KiB
Markdown
263 lines
14 KiB
Markdown
# 张量模块
|
||
|
||
**脚本入口:** `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`。
|