Nova-Quantum 是一个很有意思的项目:一个约 41 MB 的 ISO 镜像,启动后不进入 Linux、不加载 Windows,而是直接进入一个自定义内核,由这个内核自己完成大语言模型的加载和推理。项目副标题里的 "Bootet Ohne OS" 是德语,意思就是“在没有操作系统的状态下启动”。换句话说,从 BIOS 或 UEFI 固件交棒之后,页表、内存管理、串口输出、模型文件解析、tokenizer、transformer 前向计算、采样输出,全部由项目自己的代码接管。
这种形态不是常见的“嵌入式 Linux + 推理框架”,而是把 Linux 内核、用户态、驱动、Python、PyTorch 这一整条栈全部去掉,只保留真正做推理的那部分。它的价值不在性能多强,而在于把“从按下电源键到生成第一个 token”之间的每一层细节都暴露出来。对想理解引导过程、想在受限硬件上跑 LLM、或者想从零看一遍 transformer 前向计算的开发者来说,这是一个可学习、可复现、可排查的切入点。这篇文章会围绕 Nova-Quantum 的工程形态,说明 bare-metal LLM kernel 的启动链路、ISO 结构、内核骨架、模型加载与推理实现,以及最常见的踩坑点和排查顺序。
1. Bare-Metal LLM Kernel 是什么,和常规推理栈有什么本质区别
1.1 一句话解释:内核自己就是“操作系统”
常规的 LLM 部署链路通常很长。操作系统负责内存管理、设备驱动、进程调度,Python 运行时负责解释代码,PyTorch 负责算子调度,最终才会落到矩阵乘法上。每一步都有成熟工具,但每一步也都有抽象屏蔽。
Nova-Quantum 走的是另一条路线:它自己实现一个极小的内核,这个内核只做两件事,先初始化最基本的硬件环境,再加载模型权重并执行 LLM 前向计算。没有 shell,没有文件系统驱动,没有进程模型,没有虚拟内存保护(除了最基础的页表)。启动之后直接进入推理状态。
这种设计的前提是:推理本身不需要操作系统的大部分能力。LLM 推理本质上是几百次矩阵乘法和内存搬家,需要的只是“一块能读写的内存”和“一个能做乘加的 CPU”。把这些能力直接暴露给推理代码,反而省去了中间所有转换和调度的开销。
1.2 与常规部署方式的对比
| 对比项 | Linux + Python + PyTorch | 嵌入式 Linux + C++ 推理引擎 | Nova-Quantum 这类 Bare-Metal 内核 |
|---|---|---|---|
| 启动过程 | BIOS/UEFI -> 内核 -> init -> Python 进程 | BIOS/UEFI -> 内核 -> 应用进程 | BIOS/UEFI -> GRUB -> LLM 内核 |
| 内存管理 | 内核负责,用户态通过系统调用 | 内核负责,应用通过 malloc | 内核自己管理简单堆区 |
| 设备驱动 | 完整驱动栈 | 按需裁剪 | 只做串口、内存、可能加上定时器 |
| 模型加载 | 文件系统 + 系统调用 | 文件系统或裸分区 | GRUB module 直接放进内存 |
| 体积 | 数 GB | 数百 MB | 约 41 MB ISO |
| 调试难度 | 日志、gdb、perf 齐全 | 有基础调试手段 | 主要靠 QEMU 和串口日志 |
| 学习价值 | 被抽象隔离 | 部分暴露 | 全链路暴露 |
表格里的“约 41 MB”是 Nova-Quantum ISO 的总大小,不是内核大小。这个数字决定了它在学习环境里很容易被分享和下载,也决定了模型必须经过量化,否则根本放不进去。
1.3 41 MB 这个体积意味着什么
一个 41 MB 的 ISO,并不是全部空间都装模型。用 grub-mkrescue 生成的镜像会包含 GRUB 的 BIOS 引导代码和 UEFI 运行时,通常要占 3 MB 到 6 MB;内核 ELF 包含启动汇编、驱动和推理逻辑,1 MB 到 2 MB 很常见;剩下大约 33 MB 到 36 MB 才是模型权重区域。
按 4 位块量化每参数约 4.5 比特计算,33 MB 大约能容纳 5000 万到 6000 万参数的小模型。这个规模放在大模型领域很小,但放在“一个 ISO 直接启动”的场景里是合理的。它可以完成简单的文本补全、关键词生成、表达式计算,也可以作为教学模型演示完整的 transformer 前向过程。
易误解的地方是:不要指望这个体积的模型具备 ChatGPT 级别的能力。Bare-Metal LLM kernel 的目标是“能用极简栈跑通 LLM”,而不是“用极小模型打败大模型”。理解了这一点,后续学习方向才不会跑偏。
2. 先理解从 ISO 到模型推理的整条启动链路
2.1 固件、GRUB、multiboot2 三者如何交接
Nova-Quantum 并不是从零写引导器,而是使用 GRUB2 加载自己的内核。这样做是合理的:GRUB 已经处理好了 BIOS 和 UEFI 两类固件的差异,包括文件系统读取、内存探测、图形模式初始化。内核只需要遵循 multiboot2 协议,把自己声明成 GRUB 可以识别的 ELF 文件即可。
整条链路如下:
- 机器上电,固件初始化硬件。
- 固件找到启动介质,进入 GRUB。
- GRUB 读取 ISO 里的
grub.cfg,显示菜单。 - 用户选择 Nova-Quantum,GRUB 用 multiboot2 协议加载内核 ELF。
- GRUB 同时加载
module2指定的模型文件到内存。 - CPU 跳转到内核入口
_start,内核从此完全接管。
Linux 启动时经常出现Decompressing Linux... Parsing ELF... done. Booting the kernel.这类日志,Nova-Quantum 没有解压环节,GRUB 直接按 ELF 的 program headers 把段加载到指定地址,然后跳转入口。这也是裸内核比常规系统启动更快的原因之一。
2.2 模型文件通过 GRUB module 机制加载
Bare-Metal 环境没有文件系统驱动,内核自己读 ISO 里的文件非常麻烦。Nova-Quantum 这类项目通常采用 multiboot2 的 module 机制:在grub.cfg里用module2声明一个文件,GRUB 会在加载内核后把该文件读入内存,并把它的起始地址和结束地址写进 multiboot2 信息结构里,内核通过解析这个结构就能拿到模型数据。
set timeout=3 set default=0 menuentry "Nova-Quantum (Bare-Metal LLM Kernel)" { multiboot2 /boot/nova-quantum.elf module2 /boot/model.bin }这个设计的精妙之处在于:GRUB 已经解决了文件系统解析问题,内核不需要再实现 ISO9660 驱动。代价是模型文件必须能整体放进内存,且内存映射不能和内核自身代码冲突。对 33 MB 的模型来说,这个约束完全可接受。
2.3 内核态跑推理的运行时约束
内核态推理和用户态推理有几个关键差异,直接影响代码组织方式:
- 没有标准库可用,
printf、malloc、memcpy要么自己实现,要么使用 freestanding 编译器提供的-ffreestanding模式。 - 没有系统调用,内存只能自管。内核启动后要自己把可用内存划分成代码区、堆区、模型区和 KV cache 区。
- 没有异常处理机制兜底。用户态程序段错误会终止进程,内核里访问非法地址就是 page fault,处理不好就是 triple fault,QEMU 直接重启。
- 没有调度器。推理循环是独占 CPU 的,一次 token 生成期间不能被打断。
这些约束不是缺点,而是理解 OS 内核工作原理的入口。Nova-Quantum 正好把这些约束都压缩在一个最小项目里。
3. 构建环境:工具链、依赖和目录结构
3.1 工具链清单
本地构建 Nova-Quantum 至少需要四类工具:汇编器、交叉编译器、链接器,以及生成 ISO 的 GRUB 工具。推荐环境是 Linux,Windows 下建议用 WSL 或虚拟机,避免工具链路径问题。
sudo apt update sudo apt install build-essential nasm xorriso grub-pc-bin grub-common mtools qemu-system-x86各工具作用如下:
| 工具 | 作用 | 说明 |
|---|---|---|
| nasm | 汇编启动代码 | 负责 multiboot2 头和 long mode 切换 |
| x86_64-elf-gcc | 编译 C 内核代码 | 需要 freestanding 模式 |
| x86_64-elf-ld | 链接内核 ELF | 配合自定义链接脚本 |
| xorriso | 生成 ISO | grub-mkrescue 依赖它 |
| grub-pc-bin | GRUB 二进制模块 | 提供 BIOS 引导所需文件 |
| mtools | 操作 FAT 镜像 | grub-mkrescue 的间接依赖 |
| qemu-system-x86 | 本地验证 | 不需要真机也能跑完整流程 |
交叉编译工具链如果没有安装,可以下载 x86_64-elf 版本的 GCC 工具链,也可以先确认系统自带 gcc 是否支持-ffreestanding -mcmodel=kernel。学习阶段两者都行,但建议从一开始就使用交叉编译器,减少内核对宿主环境的隐式依赖。
3.2 工程目录结构
一个可维护的 Bare-Metal LLM 内核工程,建议这样组织:
nova-quantum/ ├── Makefile ├── boot.S # multiboot2 头、栈、long mode 切换 ├── kernel.c # 内核入口、串口、模型解析、推理循环 ├── linker.ld # 内核内存布局 ├── grub.cfg # GRUB 启动菜单 ├── src/ │ ├── serial.c # 串口驱动 │ ├── vga.c # 屏幕输出 │ ├── model.c # 模型文件解析 │ ├── tokenizer.c # 字节级 BPE tokenizer │ └── transformer.c # attention、ffn、采样 ├── tools/ │ └── export_model.py # 把 PyTorch 权重导出为裸二进制格式 └── iso_root/ └── boot/ ├── grub/grub.cfg ├── nova-quantum.elf └── model.bin这里model.bin是模型权重的裸二进制文件,不是 PyTorch 的safetensors,也不是 GGUF。裸二进制的目的是让内核代码零依赖读取,文件头自己定义结构。
3.3 构建前检查清单
在写第一行代码之前,先做这几项检查,能省掉后面很多排查时间:
- 确认
nasm -v、x86_64-elf-gcc -v、grub-mkrescue --version都能正常输出版本号。 - 确认 QEMU 可用:运行
qemu-system-x86_64 --version。 - 确认
grub-pc-bin已安装,否则 grub-mkrescue 会提示缺少 BIOS 支持文件。 - 确认
model.bin文件头魔数和内核代码里定义的 magic 一致。 - 确认目标内存模式:内核代码只放在 1 MB 以上,模型模块加载地址由 GRUB 决定,需要在内核里打印出来确认。
4. 编写最小 Bare-Metal 内核骨架
4.1 引导汇编:multiboot2 头与 long mode 切换
内核第一个文件是boot.S。它做三件事:声明 multiboot2 头、初始化栈、从 32 位保护模式切换到 64 位 long mode。
multiboot2 头的结构是固定的:magic 值0xe85250d6、架构字段、长度字段、校验和,以及若干 tag。GRUB 通过扫描 ELF 前 8 KB 来寻找这个头,如果校验和错误,会出现error: no multiboot header found。
section .multiboot align 8 multiboot_start: dd 0xe85250d6 dd 0 dd multiboot_end - multiboot_start dd -(0xe85250d6 + 0 + (multiboot_end - multiboot_start)) ; end tag dw 0 dw 0 dd 8 multiboot_end: section .bss align 16 stack_bottom: resb 16384 stack_top: section .text global _start _start: cli mov esp, stack_top call init_long_mode lgdt [gdt64] jmp 0x08:long_mode_entry这里注意两点。第一,.multiboot段必须放在链接脚本最前面,保证 GRUB 能在映像开头找到。第二,进入 C 代码前必须先建立自己的栈,因为在 long mode 切换之后,call kernel_main需要rsp指向合法内存。
long mode 切换的完整代码比较长,核心步骤如下:
init_long_mode: ; 关闭分页 mov eax, cr0 and eax, 0x7fffffff mov cr0, eax ; 设置 PML4 -> PDPT -> PD,先建立临时页表 mov eax, pml4 or eax, 0x3 mov cr3, eax ; 开启 PAE mov eax, cr4 or eax, 0x20 mov cr4, eax ; 设置 EFER.LME mov ecx, 0xC0000080 rdmsr or eax, 0x100 wrmsr ; 开启分页和保护模式 mov eax, cr0 or eax, 0x80000001 mov cr0, eax ret页表使用 2 MB 大页,可以简化时间。常见做法是用 NASM 宏展开 512 项,把物理地址 0 到 1 GB 区间的每个 2 MB 页都映射一遍。这段汇编里最容易出错的地方是CR0.PG开启的时机,以及GDT是否已经加载。顺序错了就会触发 triple fault,QEMU 表现为主机直接重启。
4.2 链接脚本:把内核放到固定虚拟地址
链接脚本决定内核段在 ELF 文件里的地址布局。Bare-Metal 内核通常从1M地址开始,因为低 1 MB 区域被 BIOS、VGA 显存和固件占用。
ENTRY(_start) SECTIONS { . = 1M; .multiboot : ALIGN(8) { *(.multiboot) } .text : ALIGN(8) { *(.text*) } .rodata : ALIGN(8) { *(.rodata*) } .data : ALIGN(8) { *(.data*) } .bss : ALIGN(8) { *(COMMON) *(.bss*) } }这里的关键点是.multiboot必须放在最前面。链接完成后用objdump -h nova-quantum.elf检查.multiboot段的地址,如果它没有出现在文件开头或者地址超过 4 MB,都需要回头检查链接脚本。
4.3 C 入口:串口初始化和内存信息打印
kernel.c的第一个任务是让内核“说话”。在 bare-metal 环境里,串口是最可靠的调试输出通道。QEMU 里用-serial stdio可以把串口输出直接显示在终端。
void serial_init(void) { outb(0x3F8 + 1, 0x00); // 关闭中断 outb(0x3F8 + 3, 0x80); // 设置 DLAB outb(0x3F8 + 0, 0x03); // 波特率 38400 outb(0x3F8 + 1, 0x00); outb(0x3F8 + 3, 0x03); // 8 位数据,无校验 outb(0x3F8 + 2, 0xC7); // 开启 FIFO } void kernel_main(void* mb_info) { serial_init(); printk("Nova-Quantum: bare-metal LLM kernel\n"); printk("booted without OS (Bootet Ohne OS)\n"); printk("multiboot2 info @ 0x%x\n", (uint64_t)mb_info); for (;;) { asm volatile("hlt"); } }outb是端口 I/O 指令,需要内联汇编实现。printk是自己写的极简格式化输出函数,只支持%s、%x、%u。不要在这里引入标准库的printf,它依赖大量用户态环境。
4.4 用 GRUB 打包可启动 ISO
内核编译出 ELF 后,利用 GRUB 的grub-mkrescue生成 ISO。Makefile 至少包含编译汇编、编译 C、链接、打包四个目标。
AS = nasm CC = x86_64-elf-gcc LD = x86_64-elf-ld CFLAGS = -ffreestanding -fno-pic -fno-stack-protector -mno-red-zone -O2 LDFLAGS = -n -T linker.ld all: nova-quantum.iso boot.o: boot.S $(AS) -f elf64 boot.S -o boot.o kernel.o: kernel.c $(CC) $(CFLAGS) -c kernel.c -o kernel.o nova-quantum.elf: boot.o kernel.o $(LD) $(LDFLAGS) boot.o kernel.o -o nova-quantum.elf nova-quantum.iso: nova-quantum.elf model.bin grub.cfg rm -rf iso_root mkdir -p iso_root/boot/grub cp nova-quantum.elf iso_root/boot/ cp model.bin iso_root/boot/ cp grub.cfg iso_root/boot/grub/ grub-mkrescue -o nova-qu