• 正文
  • 相关推荐
申请入驻 产业图谱

STM32N6570离线大模型终端:SmolLM2-135M从SD卡加载,单片机本地流式推理

13小时前
15
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

平时做嵌入式语音助手,常见方案是设备采集输入,再把问题发给服务器,等模型生成回答。这样做方便,但设备得联网,推理也发生在服务器上。

不过这个项目纯属整活,博主就是想试试看:把一亿多参数的模型权重放进 SD 卡,让 STM32 自己完成推理,再接上屏幕和触摸键盘,能不能做成一个离线聊天终端?

这里使用的是 STM32N6570-DK,模型选择 SmolLM2-135M-Instruct,约 1.35 亿参数。当前已经实现英文输入、多轮对话和流式显示,触屏、串口都能操作,运行时不需要电脑或网络。

1、模型选择

模型体积小,并不意味着它适合聊天。TinyStories 适合验证模型加载和文本生成,但训练目标偏向故事续写。输入一个问题,它也可能顺着问题继续编故事。想做问答终端,需要选择经过指令微调的模型,并配上对应的对话模板。

这个项目使用 SmolLM2-135M-Instruct。它有 30 层 Transformer,隐藏维度为 576,前馈层中间维度为 1536。与电脑上常见的模型相比,它的规模不大;放到单片机上,存储和计算仍然需要认真安排。

电脑端先将矩阵权重导出为逐行 INT8 格式,每行保存一个 FP32 缩放系数,再跟上整数权重。分词器也单独导出,运行时由 STM32 完成输入编码与输出解码。

文件 用途 大小
smol135m.q8.bin 量化模型权重 约 129.2 MiB
smol135m.bpe.bin 分词器 约 1.07 MiB

分词器会将一句话拆成模型使用的 token。token 可能是一个单词,也可能是单词片段、标点或特殊标记,因此“每秒生成几个 token”不能直接理解成“每秒几个单词”。

2、存储安排

完整权重有 129.2 MiB,而当前工程使用的板载 PSRAM 为 32 MiB,无法一次装下整个模型。因此,运行方式是把完整文件留在 SD 卡里,推理时按需读取,再用 RAM 缓存反复访问的数据。

SD 卡采用 FAT32,通过 FileX 文件系统访问。PSRAM 下半区划出 16 MiB 作为权重缓存,按需填充,缓存内容可以跨 token、跨对话保留;重置聊天历史不会清掉这块权重缓存。

另一部分 PSRAM 用于运行数据和显示。其中静态 arena 预留 12 MiB,保存 KV Cache、分词器数据等;当前常驻分配约 7.71 MiB,批量预填充需要时会临时借用剩余空间。

KV Cache 保存历史 token 在各层产生的 Key 和 Value,后续生成可以复用。它和权重缓存用途不同:前者随着对话变化,后者缓存固定的模型参数。

片内 SRAM 容量更紧张,主要放计算热点数据,包括 64 KiB 活跃权重缓冲、量化激活、DMA 缓冲和预填充工作区。这样可以减少热循环反复访问外部 PSRAM 的开销。

没有把完整权重写入外部 Flash。量化模型已超过板载 128 MiB Flash 的总容量,而 Flash 还要存放启动程序和应用。

3、推理加速

速度要从两头处理:一头是减少重复读取,另一头是让 CPU 更快地完成矩阵计算。只改其中一边,另一边仍可能占据大部分等待时间。

输入一句话后,模型先处理输入并建立 KV Cache,这个阶段叫预填充。随后才进入生成阶段,每次选出下一个 token,再送回模型继续计算。

如果逐个输入 token 遍历模型,同一批权重会被反复读取。当前实现改为逐层批量计算,读入一块权重后供多个输入 token 复用,最终词表投影只计算需要输出的位置。

例如 What is your name? 连同系统提示和对话模板共 30 个 token。主机验证中,这段预填充的逻辑权重读取量由原逐 token 完整前向方案约 3.78 GiB 降至约 129.2 MiB。

这里减少的是逻辑读取量和无用的词表投影,不能当成板上提速 30 倍。各个输入位置的 Transformer 计算仍然要执行,实际 SD 物理读取量还受权重缓存和预读影响。

矩阵权重已经是 INT8,当前进一步将输入激活动态量化为 INT8,每 32 个值使用一组缩放系数,再通过 Helium 指令执行整数点积。

下面是代码中的实际内核,保留了没有 MVE 时的标量路径,以及不足 16 个值的尾部处理:

staticfloatdot_q8(constint8_t *w,constint8_t *x,constfloat *scales,int n){    float result=0.0f;    for (int start=0;start<n;start+=TLLM_QGROUP) {        int end=start+TLLM_QGROUP,i=start;        if (end>n) end=n;        int32_t sum=0;#if TLLM_MVE        for (;i+15<end;i+=16) sum=vmladavaq_s8(sum,vld1q_s8(w+i),vld1q_s8(x+i));#endif        for (;i<end;i++) sum+=(int32_t)w[i]*x[i];        result+=(float)sum*scales[start/TLLM_QGROUP];    }    return result;}

vld1q_s8 一次加载 16 个有符号字节,vmladavaq_s8 将 16 对乘积累加到 INT32。每组结束后乘上激活缩放系数,外层矩阵计算还会乘上该行权重的缩放系数。

注意力、归一化、残差和 KV Cache 仍保留 FP32。动态量化会改变计算数值,因此回答也可能与 FP32 激活路径不同,工程保留了切换命令用于比较。

此外,RoPE 旋转参数在加载时预计算;SiLU 和 Softmax 使用快速指数近似,超出适用范围时回到 expf,没有启用全局 fast-math。

SDMMC 采用 4 线模式,模型读取设置两个 128 KiB 预读窗口。CPU 使用当前数据计算时,DMA 读取下一段权重,尽量把读卡时间与计算时间重叠。

下面是代码中发起预取的实际片段,位于当前请求数据复制完成之后,依赖所在函数中的窗口状态和文件对象:

    uint64_t next=(uint64_t)window_offset+SD_WINDOW_BYTES;    if (prefetch_enabled && window_file==file && !pending_file &&        next+SD_WINDOW_BYTES<=file->fx_file_current_file_size) {        int result=SD_File_PrefetchBegin(file,(uint32_t)next,                                       sd_window[window_index^1U],SD_WINDOW_BYTES);        if (result<0) return FX_IO_ERROR;        if (result==1) { pending_file=file; pending_offset=(uint32_t)next; prefetch_starts++; }    }

window_index ^ 1U 选择另一个缓冲区。只有没有在途请求、文件还剩完整窗口等条件成立时,才会尝试启动异步读取;不适合预取的请求继续使用原读取路径。

DMA 完成前,CPU 不能把目标缓冲当作有效权重使用。工程同时处理缓存维护、完成确认和异常回退,减少“传输完成了,但 CPU 读到旧数据”这类问题。

4、对话交互

界面使用 LTDC 显示,上方是聊天内容,下方是英文触屏键盘,支持大小写、数字、符号、空格和退格。摄像头功能已关闭,LCD 和触摸独立运行。

回复以 AI 标识,生成一个 token 就立即显示其可输出字节,不等整句话完成。下面是实际发送到串口和屏幕的代码片段:

constunsignedchar *piece=chat_bpe_piece(&tokenizer,next,&bytes);for (uint32_t i=0;i<bytes;i++) {unsignedchar c=piece[i];if (c>=32U || c=='n' || c=='t') {    if (!first_output) {    last_first=HAL_GetTick()-turn_start;    first_output=1;  }  char out[2]={(char)c,0}; serial(out); ChatUI_ReplyByte(c);  }}

chat_bpe_piece 取得当前 token 对应的文本,随后逐字节调用串口输出和 ChatUI_ReplyByte。显示时可能一次出现几个字母,也可能出现一个完整单词,取决于当前 token 的内容。

上下文共 128 tokens,系统提示、历史和当前问答共同占用;每轮最多生成 32 tokens。因此,问题尽量简短,较长回答可能被截断。上下文不足时,程序会提示并清空旧对话。

实际效果

目前已经跑通英文输入、离线推理和流式回复。前面的 INT8 与读卡优化后,实机使用时能感觉到速度明显改善,大概速度是10 s/tokens,速率在优化前提高大概10倍左右。过程中也尝试过四行并行矩阵内核,希望复用输入、减少重复加载。数值验证通过了,板上却比之前慢,这个原因还要探究一下。

做到这里,STM32 已经可以作为一个本地离线英文聊天终端使用。它的回答能力仍然有限,但从 SD 卡读取一亿多参数的模型,再由单片机自己算出回复、显示到屏幕上,这条完整链路已经跑通了。也算是小小的完成一次尝试。

相关推荐

嵌入式开发兴趣爱好者,热衷于电子信息技术,嵌入式开发,物联网技术等领域开发。