Skip to content

支持外部算子库调用:从 MatMul(矩阵乘法)开始 #109

Open

Description

@yuki-328

原始设计提案: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 程序的代码。这些部分继续使用。

新增工作集中在两个位置:

  1. 生成 MatMul 时,决定输出现有计算循环,还是输出外部函数调用。
  2. 编译程序时,把选中实现所需的源文件、头文件位置和库文件传给现有构建入口。

每个外部实现只需提供一份简单配置,说明函数名称、支持条件和所需文件。首期使用显式配置,不建立复杂的插件系统,也不自动下载依赖。

外部实现可以采用哪些形式

形式 接入方式 首期建议
一小段受控的外部 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions