Repository navigation
支持外部算子库调用:从 MatMul(矩阵乘法)开始 #109
Description
原始设计提案:2026 年 10 月 18 日;实施进度更新:2026 年 10 月 25 日。
首期外部 MatMul 调用已实现并通过验收,普通 PR #118 待维护者正式审阅并合并。 当前提交为cc83b45,与 W4 主干同步工作分开。
当前完成情况
- 已实现统一 C 调用接口、默认原实现、优先/必须外部库选择、明确数值许可及失败报错;编译和链接复用现有入口。
- Host 已接入 LP64 OpenBLAS;RISC-V/QEMU 已接入 NMSIS,串行混合完整模型及 1/2/4 hart 定向小图均有保存证据。按行并行 NMSIS 的完整模型尚未验收。
- 保留显式按实测选择:Host 当前为 253 个 OpenBLAS+1 个原实现;RV64GC、4 hart 当前为 254 个原优化实现、0 个 NMSIS。RISC-V 恢复分钟级耗时来自选回原优化,不是 NMSIS 本身提速。
- 两份验收文档已补查并修正配置限制、Host 计时来源及历史/当前证据区分;生产策略、原始测量和测试数据保持不变。
- 上次 AI 因限流漏看的 5 个实现/测试/CI 文件也已补查,未发现可确认的合并阻塞;这不替代维护者批准。
最新提交的 CI run 40170563518 已通过:主测试 7886 通过、113 跳过;外部库/ABI/真实 QEMU 小图 1744 通过、4 跳过,19 个必跑用例全部通过。两组有重叠,不累计为独立测试总数。完整模型任务已跳过;本次收尾未重跑完整模型。
历史完整模型证据覆盖固定 28 层、长度 256 的 full_seed_0,全部 38,895,616 个输出通过严格 max_abs < 0.001、rtol=0;具体历史代码版本和计时限制见 功能说明及 验收摘要。原始模型、输入输出和归档保存在本地,不随仓库上传。
首期七项实施清单已完成;PR 尚未合并,因此本 Issue 暂保持打开。GitHub 当前显示 REVIEW_REQUIRED,需要维护者正式批准;当前发布账号对主仓库只有读取权限,不能执行合并。自动 AI 审查的覆盖不完整,不能以绿色流程状态或本次技术补查代替维护者批准。
后续性能工作(与首期功能验收分开)
- 继续优化 NMSIS 的 QK、AV、Q 投影,计入准备、转换、写回与同步成本;达到相对原优化至少减少 10% 的条件后才替换现有选择。当前实验候选尚未采用。
- 候选确有稳定收益后,再单独安排按行并行 NMSIS 的完整模型验证;不因本次合并准备启动完整模型。
- 真实硬件、其他指令集或库版本的收益另行验证;不将 QEMU 耗时直接当作真机性能。
10% 是后续性能候选的采用门槛,不是首期外部调用功能的完成条件。动态尺寸、其他数据类型/算子及自动测量与调优仍不在首期范围。
以下保留最初设计与实施步骤;已交付的具体行为和取舍以上述状态、PR 与验收文档为准。
1. 我们要增加什么能力
让 ScratchV 在生成 C 程序时,能够选择调用已有库里的计算函数。例如,遇到矩阵乘法时,既可以使用项目当前生成的计算代码,也可以调用外部库提供的矩阵乘法函数。
这里的“外部库”是可以和程序一起编译、链接的代码。链接就是把生成的程序与库中的函数连接起来;程序运行时不需要上网请求计算服务。
这样做的价值,是让以后接入更多实现更容易,减少重复编写计算代码。是否更快、是否适合某个硬件,需要实际验证。
建议的第一步:保留现有实现作为默认选项,增加一个小而明确的外部调用入口,先用 MatMul 验证整条路径。
2. 从一次矩阵乘法开始
例如,一个 2×3 的矩阵乘以一个 3×4 的矩阵,结果是一个 2×4 的矩阵。
首期限定为:生成程序时已经知道矩阵大小、使用 32 位浮点数(常见的小数表示)、数据按行连续存放。向量和多组矩阵仍由 ScratchV 按已有规则处理,再转换成一次或多次普通矩阵乘法调用。
处理流程如下:
读取模型 → 确定矩阵大小和内存位置 → 选择计算实现 → 生成 C 代码 → 编译并连接所需库 → 运行
| 谁负责 | 负责的内容 |
|---|---|
| ScratchV | 检查矩阵大小、安排输入输出和内存、处理多组数据、选择实现、检查返回结果 |
| 外部库 | 根据传入的两个矩阵,完成一次矩阵乘法并写入指定结果位置 |
| 必要时增加的少量转换代码 | 将 ScratchV 的调用参数转换成所选库要求的形式 |
首期不扩展到所有算子,也不同时加入其他数值格式、运行时才确定的矩阵大小或自动挑选最快库等能力。
3. 调用时需要约定什么
输入、输出与成功条件
每次调用传入六项内容:两个输入矩阵、结果存放位置,以及三个矩阵大小参数。外部函数完成计算后返回成功或失败。
约定两个输入只读,结果覆盖写入独立的输出区域。函数返回时计算已经完成,不保存调用方的内存地址供以后使用。实际接入不增加额外调用方工作内存。相对原设计,OpenBLAS 允许其内部缓冲和全局线程状态,由调用方维持已验证的单线程条件,适配器不修改全局线程设置;本次 NMSIS 标量路径不依赖此类状态。该取舍已在 PR 中明确。
空矩阵由现有生成代码处理;如果乘法中参与累加的项数为零,由 ScratchV 将结果写成零,避免不同库对特殊情况处理不一致。
如何选择现有代码或外部库
| 选择方式 | 行为 | 适用情况 |
|---|---|---|
| 使用现有实现 | 保持当前生成方式,也是默认设置 | 保持现有使用体验 |
| 优先外部库 | 生成程序前检查是否适用;不适用时使用现有实现并说明原因 | 逐步接入、允许部分矩阵使用外部库 |
| 必须使用外部库 | 没有配置库,或库不支持当前情况时,直接报告错误 | 验证外部调用确实生效 |
“优先外部库”的选择发生在生成程序之前。一旦已经选中外部库,后续编译、链接或运行失败都应明确报错,不能悄悄换回现有代码,否则很难判断实际运行了什么。
选择时要检查矩阵大小、数值格式、内存要求、计算精度要求和运行目标。一个程序里的不同矩阵乘法可以分别作出选择。
结果应该一致到什么程度
不同库可能改变浮点乘加的顺序,因此结果接近正确值,不代表每一位都和现有实现相同。
默认继续遵守项目当前的计算要求。若实际库不能满足这些要求,“优先外部库”应使用现有实现,“必须使用外部库”应报错。
若以后允许使用库自身的计算顺序,需要明确开启这一选择,并事先约定误差标准、通过针对性验证。不能直接沿用现有的逐位一致结论,也不能为了让测试通过而临时放宽标准。
4. 实现与编译链接怎么接入
先复用现有代码
ScratchV 已经有矩阵大小检查、向量和多组矩阵处理、内存分配、结果检查以及生成 C 程序的代码。这些部分继续使用。
新增工作集中在两个位置:
- 生成 MatMul 时,决定输出现有计算循环,还是输出外部函数调用。
- 编译程序时,把选中实现所需的源文件、头文件位置和库文件传给现有构建入口。
每个外部实现只需提供一份简单配置,说明函数名称、支持条件和所需文件。首期使用显式配置,不建立复杂的插件系统,也不自动下载依赖。
外部实现可以采用哪些形式
| 形式 | 接入方式 | 首期建议 |
|---|---|---|
| 一小段受控的外部 C 代码 | 与生成的程序一起编译 | 先证明调用、选择和报错流程正确 |
| 成熟库提供的矩阵乘法函数 | 必要时增加少量参数转换代码,再连接库文件 | 在开发电脑上接入一个实际库 |
| 为 RISC-V 编译的外部实现 | 使用目标编译器和对应的库文件,经过模拟器运行验证 | 单独验证 W4 支持情况 |
已选择 Host 的 OpenBLAS 和 RISC-V/QEMU 的 NMSIS。两者分别验证,不承诺某个库适用于全部目标;选择依据和依赖版本见验收文档。
开发电脑和 W4 要分别处理
开发电脑上的程序和 W4 的 RISC-V 程序需要不同的库文件。
W4 使用 QEMU 模拟器和项目自己的启动、内存安排,并不是普通的 Linux 应用程序。因此,一个依赖操作系统线程、系统调用等功能的库,即使能够编译成 RISC-V 文件,也不能直接认定能在 W4 中运行。
实现顺序应当是:先在开发电脑上接入一个实际库;再用简单外部 C 实现验证 W4 的调用路径;最后根据运行证据决定某个实际第三方库是否支持 W4。
构建时保留生成代码现有的浮点编译要求。库本身使用的优化选项需要单独评估,不能覆盖整个模型的编译设置。
5. 分步实施与验收
| 阶段 | 完成内容 | 需要保留的证据 |
|---|---|---|
| 第一步:打通调用 | 加入统一调用接口、三种选择方式和简单外部 C 实现 | 确实调用了外部函数;默认行为未改变;不支持和失败时有明确提示 |
| 第二步:接入实际库 | 选定一个库,在开发电脑上完成编译和链接 | 代表性矩阵计算正确;实际使用的库和计算要求清楚;误差符合预先约定 |
| 第三步:验证 W4 | 将简单外部实现交叉编译,在 QEMU 中运行小规模用例 | 目标程序能够构建、执行并返回正确结果 |
| 后续:确认具体库的 W4 支持 | 在 W4 中运行选定的实际第三方库 | 该库自身的构建与运行结果;不能用简单测试实现的结果代替 |
验证应扩展现有测试,重点覆盖普通矩阵、向量、多组矩阵、特殊大小、空输入、零项累加,以及选择和错误返回。
还需要证明:缺少库文件、找不到函数或库不适合目标时,程序会明确失败;已经选中外部库后,不会悄悄改用现有计算代码。
首期只做与改动直接相关的单测和小规模运行验证,不重跑完整模型。 通过这些检查可以证明接入路径可用,但还不能据此宣称完整模型已通过验收或性能已经提升。
实施清单
- 确定统一调用接口和三种选择方式,默认保持现有实现。
- 用简单外部 C 实现打通生成、编译、链接和运行,证明确实调用了外部函数。
- 验证不支持时的选择行为,以及选中外部库后的编译、链接和执行错误。
- 选择一个实际库,在开发电脑上完成接入和代表性矩阵验证。
- 明确所选库的浮点计算要求和误差标准,保留定向验证结果。
- 用简单外部实现验证 W4 的 RISC-V/QEMU 构建和小图运行。
- 记录首期支持范围、接入方法和限制,复用现有测试及 CI。
NMSIS 在 W4 上的实际构建、小图和串行混合完整模型证据已补齐;按行并行配置的完整模型及进一步性能提升仍按上方后续清单跟进。
6. 开始实现前需要确定的事项
- 选哪个实际库: 先选择一个适合开发电脑的库,核对其接口、依赖和使用许可。
- 允许哪种计算方式: 默认保留当前要求;若允许外部库调整浮点运算顺序,先明确误差标准。
- 第一阶段交付到哪里: 建议先交付开发电脑上的实际库调用,以及 W4 上的简单外部调用验证。
- 什么情况下宣称支持 W4: 必须有选定实际库在 W4 中的小规模运行证据。
本 Issue 用于讨论设计和跟踪实现;首期范围按上述分阶段验收推进。
开发参考:拟议接口、代码位置与现有测试
以下内容供后续实现参考。
拟采用的 C 函数接口
#include <stdint.h>
int sv_matmul_f32_v1(
const float *a,
const float *b,
float *c,
uint64_t m,
uint64_t n,
uint64_t k);其中,A 为 m×k,B 为 k×n,C 为 m×n;按行连续存放,执行 C=A×B。返回 0 表示成功,非零表示失败,并转换为现有运行入口能处理的错误。
该接口是拟议约定,不是当前已经存在的函数。库若使用较小的整数表示矩阵大小,转换前必须检查范围。输出不能与输入重叠;对齐要求不能超过现有内存分配的保证,除非同时调整并验证分配方式。
三种选择方式可在代码中命名为 builtin、prefer_external、require_external;是否允许库自身的浮点计算顺序,使用独立设置。
优先复用和调整的位置
以下代码位置以 PR #104 的 af5768c 版本为设计依据。
| 现有文件 | 建议用途 |
|---|---|
| tensor_c_codegen.py | 复用 MatMul 形状、广播、内存和结果检查;增加选择及外部调用生成 |
| compiler.py | 将实现选择传入通用编译入口,避免功能只存在于 W4 实验脚本 |
| riscv_tensor.py | 复用已有的额外源文件、编译参数和链接参数传入方式 |
| riscv_external.py | 为外置权重运行路径接入同类构建参数 |
| host_diagnostic.py | 在现有开发电脑构建入口中接入所需文件和参数 |
源文件可一起编译,已有对象文件或静态库可参与链接;静态库应放在引用它的对象文件之后。保留现有构建日志即可,不增加哈希检查或新的报告体系。
现有 kernel_calls=True 只是把计算代码拆成同一 C 文件中的函数;constant_storage="external" 表示外置权重。二者都不能直接代表外部算子库选择。
现有测试入口
优先扩展 test_tensor_c_codegen.py、test_tensor_c_external.py 和 test_riscv_external.py,复用矩阵形状、边界、错误返回与 QEMU 小图验证。
test_w4_fma_cross_backend.py 的逐位比较继续约束现有计算实现;一般第三方库需要按明确约定的计算方式和误差标准验收。
Activity