news 2026/10/5 6:22:52

ESP32-P4跑LLM提速7倍:从0.61到4.31 tok/s的优化链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32-P4跑LLM提速7倍:从0.61到4.31 tok/s的优化链路

我很早就想把这颗芯片和 LLM 这两个词放在一起,直到 ESP32-P4 出现在我桌上。双核 RISC-V、能跑到 400MHz、还带向量扩展指令,配合外挂的大容量 PSRAM,这套硬件组合天生就是为端侧推理准备的。但能跑和跑得舒服是两回事,我最初在这个板子上跑一个微型 LLM,速度只有 0.61 tok/s,生成一句话要等半天;后来一路调下来稳定到了 4.31 tok/s,差不多 7 倍,整个过程我可以拆成一条清晰的优化链路。

这篇是系列的第 00 篇,也就是总览。我打算把整条调优路线、每步的收益、踩过的坑,以及最终 4.31 tok/s 到底处在什么体验水平,一次性讲清楚。如果你手里也有一块 ESP32-P4,或者正打算在别的 MCU 上尝试本地跑 LLM,这篇可以帮你少走很多弯路。

1. 先把话说清楚:ESP32-P4 跑 LLM 到底意味着什么

1.1 这颗芯片的底牌:不是普通单片机

ESP32-P4 和我之前玩过的 ESP32-S3、C3 完全是两个物种。它是乐鑫目前性能最强的一颗通用 MCU,双核 RISC-V 架构,主频直接拉到 400MHz。更关键的是,它带向量扩展指令(RVV 1.0),也就是说芯片本身具备做矩阵运算加速的硬件基础,这一点对 LLM 推理是决定性的。

内存方面,P4 内部 SRAM 只有不到 1MB,所以必须外挂 PSRAM。我手上这块板子带了 32MB PSRAM,走的是高速并行接口,理论带宽远高于传统 SPI PSRAM。LLM 推理的特点就是权重要反复读,内存带宽直接决定每秒能吐多少个 token。你可以这样理解:CPU 算力决定上限,内存带宽决定实际速度,而 P4 这颗芯片正好在 MCU 里把这两块短板都补上了。

还要注意一个细节:P4 默认不带 WiFi 和蓝牙。这意味着模型文件不能靠边下载边用,必须预先放进 SD 卡或者外部 Flash,跑推理时再加载。你可能会觉得这不方便,但对端侧部署来说反而省心——没有射频干扰,没有协议栈抢占 CPU,整个系统更干净。

1.2 为什么我选了 llama.cpp + GGUF 这条路线

能在 MCU 上做 AI 推理的方案不少,TFLite Micro、ONNX Runtime、ESP-DL 我都试过,但最终选定 llama.cpp + GGUF,原因有三个。

第一,llama.cpp 的核心是 ggml 张量库,它天生就是为了 CPU 推理设计的,内存占用极低,也没有依赖 Python 运行时。它的算子针对不同指令集做过手工优化,RISC-V 向量扩展就是其中一条支持路径。虽然标准版本对 RISC-V 的优化还在持续完善中,但已经能跑,社区也有针对 ESP32-P4 的移植分支。

第二,GGUF 是一种单一文件格式,模型权重、词表、超参数全打包在里面。它最大的价值是支持量化存储,而且量化粒度是按 block 切的。我可以把 33M 参数的模型量化为 Q4_0,文件压到 20MB 左右,正好塞进 PSRAM。TFLite Micro 虽然也能量化,但对 LLM 这种带 KV cache、带采样器、带复杂解码逻辑的场景,它的抽象层级太低了,改起来非常费劲。

第三,llama.cpp 的命令行工具链成熟。我可以在 PC 上完成模型转换、量化、参数验证,板子上只跑一个精简的推理入口,开发调试体验比从零写一个推理器好太多。

这套组合并不新鲜,但在 ESP32-P4 上能成立,核心还是因为板子的内存和指令集撑得住。换个普通 MCU 可能模型只能放 Flash,推理速度会掉到不可用的级别。

2. 从 0.61 到 4.31:七倍性能提升是怎么拆出来的

2.1 基线 0.61 tok/s 是怎么来的

先说基线。很多人一听到 0.61 tok/s 会觉得“这板子是不是不行”,其实不是,这个数字的来源非常值得分析。

我最初跑通时,用的是一个非常简单的思路:模型文件放在 SD 卡上,llama.cpp 的标准构建,没有启用任何针对 P4 的特殊编译参数,线程数用的 1,量化档位用的 Q8_0。跑起来之后,每生成一个 token,程序都要从 SD 卡重新读取一遍相关权重。SD 卡顺序读取大概有 4MB/s,但 LLM 推理是随机访问,实际吞吐远低于这个值。模型 Q8_0 大约 35MB,每个 token 都要扫一遍,算下来 0.61 tok/s 一点不奇怪。

所以基线慢的核心瓶颈是 I/O,而不是 CPU。这个判断很重要,因为它决定了后续优化的方向——先把数据弄到内存里,再考虑计算效率。

我建议每一位想复现的朋友,第一步不要急着改代码,先搭好环境、跑出基线、记录当前速度和你认定的瓶颈。没有基线的调优就是耍流氓,改了好几个变量,最后根本说不清是哪一步起了作用。

2.2 第一刀:编译选项与向量指令

第一个动作是让 ggml 算子真正跑进 RISC-V 向量指令。默认构建可能走的是标量路径,CPU 要一条一条处理数据,矩阵乘法非常吃亏。

我用的是针对 P4 的交叉编译工具链,在编译 llama.cpp 时打开了向量扩展相关的编译参数。不同工具链支持的 march 写法有差异,我这边实际生效的指令集组合包含了向量扩展和位操作扩展。这一步只是编译层面的改动,代码逻辑完全没动,跑分就从 0.61 到了 0.98 tok/s,大约 1.6 倍提升。

这里有个很容易犯的错误:你以为启用了向量编译,但代码实际还是走回退路径。原因通常是内联汇编或 intrinsics 的条件编译没匹配上,或者跑在 cpu 核心上的线程被系统调度到了不支持向量路径的逻辑上。判断方法很简单,看编译日志里有没有打印 RISC-V vector kernels enabled,或者直接对比同一条算子在不同构建下的耗时。

2.3 第二刀:双核并行与线程绑定

P4 是双核,llama.cpp 的并行模型天然支持多线程。我把线程数从 1 调到 2,这一步带来约 1.7 倍提升:0.98 到 1.67 tok/s。

但这里有个反直觉的点:LLM 解码阶段是内存带宽密集型,CPU 常常在等数据,双核并行的收益按理说不会很高。我实测下来提升明显,说明矩阵乘法里的计算开销占比还是不小,向量单元跑起来之后,两个核心刚好能各分一块计算,把内存等待的时间折叠掉了一部分。

线程数调到 2 之后,我没有继续往上加,因为 P4 只有两个物理核心。同时我做了线程亲和性绑定,把推理线程分别固定到两个核上。别小看这一步,在嵌入环境里,系统后台任务可能导致线程在核心间来回迁移,cache 和向量寄存器状态反复重建,性能抖动非常严重。绑定之后跑分稳定了很多。

2.4 第三刀:量化档位与内存带宽

这一步是纯收益很高的改动:把模型从 Q8_0 换成 Q4_0。Q8_0 每个权重占 1 字节,Q4_0 每个权重占 0.5 字节,文件从约 35MB 降到约 19MB。

为什么要关心权重大小?因为在 decode 阶段,每生成一个 token 要把所有权重从头到尾过一遍。权重越小,内存搬运的数据越少,速度越快。这一步我实测从 1.67 到了 2.33 tok/s,约 1.4 倍。

很多人听到“量化”会担心生成质量崩掉。Q4_0 其实是一个保守的选择,每一个 block 里用一个 fp16 的 scale 做缩放,精度损失可控。我用的测试模型本身是故事生成任务,Q4_0 和 Q8_0 输出几乎没有肉眼可见的差别。

如果你想把模型压得更狠,可以考虑 Q2_K 甚至 1.58bit 的 BitNet 系模型,但那些对算子库有额外要求,P4 上不一定能直接跑,这个我后面系列会单独测。对绝大多数人来说,Q4_0 是性价比最高的起点。

2.5 第四刀:prefill/decode 分离与细项调整

到这一步,瓶颈已经从 I/O 转移到了计算与内存访问的平衡。接下来这组改动没有单一的大头,而是几个细节叠加起来:总收益约 1.5 倍,从 2.33 到 3.51 tok/s。

第一个细节是区分 prefill 和 decode 两个阶段的 batch size。prefill 阶段要处理整个 prompt 的所有 token,属于计算密集型,batch 越大向量化越充分;decode 阶段是逐 token 生成,batch 设太大反而增加延迟。我在代码里分别设置了两个参数,实测下来 prefill batch 用 512、decode batch 用 32 左右最舒服。

第二个细节是开启 Flash Attention。它减少了 KV cache 的中间读写次数,对内存带宽吃紧的 MCU 环境很友好。

第三个细节是内存对齐。PSRAM 的 DMA 访问对地址对齐敏感,我把 KV cache 和权重的缓冲区对齐到 64 字节,这一项单独看差不多 10% 的收益,但胜在零成本。

还有个小改动容易被忽略:把推理过程中的调试日志和串口打印关掉。串口输出在 MCU 上是阻塞操作,打印一条日志可能浪费几十毫秒,累计起来非常可观。

2.6 各步提升汇总

我把每一步的实测数据整理成一张表,方便你对照自己调优时的情况。

优化步骤关键改动实测 tok/s相对上一步累计倍率
基线SD 卡读取,单线程,Q8_0,默认构建0.61-1.0x
第一步启用向量指令与编译优化0.981.6x1.6x
第二步双核并行 + 线程亲和性1.671.7x2.7x
第三步Q8_0 换成 Q4_0 量化2.331.4x3.8x
第四步prefill/decode 分离、Flash Attention、内存对齐3.511.5x5.8x
第五步日志关闭、参数微调、反复打磨4.311.2x7.1x

注意看累计方向:每步都不是白做的,但每步的收益也在递减。头三步解决了“数据根本喂不过来”的问题,后面则是榨干细节。

3. 实操记录:从烧录到跑通再到调优闭环

3.1 环境准备与交叉编译

复现之前,硬件上你需要一块带 32MB PSRAM 的 ESP32-P4 开发板,以及一张 SD 卡用于存放模型。软件方面,我用了 ESP-IDF v5.3 作为基础环境,llama.cpp 则使用社区维护的 esp32 分支。

交叉编译这一步,我踩过最多的坑是工具链和分支版本不匹配。建议按这个顺序来:

# 安装 ESP-IDF,注意设置目标芯片为 esp32p4 idf.py set-target esp32p4 # 拉取 llama.cpp 的 esp32 分支 git clone --branch esp32_p4 https://github.com/your-fork/llama.cpp.git cd llama.cpp # 创建独立构建目录,避免污染 PC 端版本 mkdir build-esp32p4 && cd build-esp32p4 # cmake 关键参数,这里需要根据你用的工具链路径调整 cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/riscv32-esp32p4.toolchain.cmake \ -DCMAKE_BUILD_TYPE=Release \ -DGGML_RISCV=ON \ -DLLAMA_CURL=OFF # 编译可执行文件,之后会生成 llama-bench、llama-cli 等 make -j$(nproc)

编译成功后会得到可烧录的固件和一系列命令行工具。注意,交叉编译出来的二进制不能直接在开发机上跑,必须烧进板子。

提示:如果你的分支默认不带 riscv32 的 toolchain 文件,需要从 ESP-IDF 的 riscv32-esp-elf 工具链路径手动指定 CC 和 CXX 环境变量。我这边用的是 riscv32-esp-elf-gcc,版本 13.x。

3.2 模型转换、量化与装载

我选的测试模型是 TinyStories-33M。选它是因为参数量小、生成任务直观,而且学术界对它的行为特性研究很充分,方便验证优化过程没有把模型「调坏」。

模型转换和量化在 PC 上就能完成,不需要占板子资源。我习惯按这条命令链走:

# 1. 把 Hugging Face 格式转成 fp16 的 GGUF python3 convert_hf_to_gguf.py TinyStories-33M \ --outfile tiny33m-f16.gguf # 2. 量化为 Q4_0 ./llama-quantize tiny33m-f16.gguf tiny33m-q4_0.gguf Q4_0 # 3. 看一眼量化后的文件信息,确认大小和 tensor 类型 ./llama-gguf-info tiny33m-q4_0.gguf

生成好的 tiny33m-q4_0.gguf 大约 19MB,把它复制到 SD 卡的根目录,SD 卡格式化成 FAT32 就行。

加载方式值得单独说一句。llama.cpp 原生的模型加载默认会把权重读进内存,但如果你把模型路径指向 SD 卡,它可能走 mmap 或流式读取。这块板子上我最终是先把整个文件从 SD 卡读进 PSRAM,再做推理。这样一次加载、多次推理,显存带宽全部留给推理本身。夸张点说,这一步不改,后续所有优化都白搭。

3.3 benchmark 怎么跑才不骗自己

很多人调优失败不是因为优化没效果,而是测速方法不统一,导致判断失真。我固定了一套流程:用 llama-bench,prompt 长度固定 128 token,生成 token 数固定 64,线程数按当前测试场景设置,重复跑 3 次取中位数。

./llama-bench -m /sdcard/tiny33m-q4_0.gguf \ -p 128 -n 64 \ -t 2 -b 32 \ -r 3

跑之前还有个习惯:先连续跑两轮预热,让 PSRAM 和缓存都处于稳定状态,再记录正式数据。否则第一轮通常会偏慢,因为 SD 卡到 PSRAM 的搬运、动态内存分配都还没稳定。

另外,每次改动后务必只改一个变量。比如测线程数时保持量化、batch 不动;测量化时保持线程、batch 不动。否则一个提升可能被另一个回退抵消,你会得出完全错误的结论。

3.4 一轮完整的调优现场记录

我贴一段我自己的实验笔记,展示实际调优过程有多琐碎。

轮次改了啥观测结果结论
1初始基线,SD 卡直接读取0.61 tok/s,明显感觉在“挤牙膏”瓶颈在 I/O,先解决数据驻留
2模型预加载到 PSRAM直接到 0.82 tok/s数据驻留有效,但计算路径还是标量
3编译启用向量指令0.98 tok/s向量化有效,继续
4线程 1 改 21.35 tok/s,但抖动大线程数有效,需要绑定核心
5线程亲和性绑定1.67 tok/s,稳定输出绑定核心消除调度抖动
6Q8_0 换 Q4_02.33 tok/s量化有效,检查生成质量无明显回退
7prefill/decode batch 分离2.81 tok/s分开后 prefill 明显变快
8Flash Attention 开启3.25 tok/sKV cache 访问减少带来收益
9内存对齐 64 字节 + 关日志3.51 tok/s细节积少成多
10打磨编译参数和 batch 粒度4.31 tok/s达成目标

看到没有,第 3 轮到第 10 轮之间,每一步单独看都不惊人,但叠起来就是从 0.98 到 4.31 的跨越。优化工作本质上就是压缩每一环的浪费,没有银弹。

4. 4.31 tok/s 的真实体验与可落地的场景边界

4.1 这个速度到底什么概念

4.31 tok/s 换算一下:生成一个 token 大约 232 毫秒。一个词大概 2 到 3 个 token,所以每个词约半秒。看起来还是慢,但放在 MCU 本地推理的上下文里,它已经进入了「能等人读完」的可用区间。

我用它生成了一段约 50 词的英文小故事,耗时大约 30 秒。这个体验不像云端模型那样秒回,但想想这完全是在一块没有网络、没有操作系统的单片机上完成的,感受完全不同。如果你只需要生成一两句话的回答,比如设备状态提示、传感器数据解读、互动玩具的回应,这个速度完全够用。

对比一下其他路径:如果你试图在 ESP32-S3 上跑类似的 LLM,内存只有 8MB,模型基本塞不下;树莓派 Pico 2 虽然便宜,但主频和向量能力都差不少。P4 是当前 MCU 级别里少数能把 LLM 推理拉到“能对话”门槛的板子。

4.2 内存账本:能跑多大模型

很多人问“P4 能跑多大的模型”,答案是算出来的。以我用 TinyStories-33M 为例,Q4_0 权重约 19MB,KV cache 在 128 token 上下文下约 2MB,推理所需的临时缓冲和运行时占几 MB。所以 32MB PSRAM 勉强能覆盖,16MB 版本会非常紧张。

模型能跑多大,基本由两个因素决定:权重文件大小和 KV cache 占用。权重大小由参数量和量化位数共同决定,KV cache 可以用一个公式粗估:

KV cache 大小 = 2 × 层数 × 注意力头数 × 头维度 × 上下文长度 × 每个元素占用字节数

TinyStories-33M 大约 8 层、8 头、每头 64 维,fp16 存储,128 上下文计算出来大约是 2MB。如果换成 0.5B 模型,哪怕量化到 Q4_0,权重就要 250MB 以上,直接超出 PSRAM 极限。所以当前阶段 P4 上能跑的,基本是 30M 到 50M 参数级别的小模型,或者经过极端量化的百M级模型。

这也是我推荐 TinyStories-33M 的原因——它不是最佳模型,但它是优化流程的最佳载体。

4.3 这些场景值得做,那些场景别硬上

基于我跑通后的实际体验,我认为有几个场景是 ESP32-P4 本地 LLM 真正值得落地的。

离线口令或指令理解:嵌入式设备不联网,用一个小模型把自然语言指令解析成结构化动作,比如语音助手的离线指令槽位填充。这类任务 token 量少,延迟要求也低。状态摘要生成:把设备日志、传感器数据喂给模型,生成一段自然语言的状态汇报,这部分不要求实时性,4 tok/s 完全够用。教育或玩具场景:生成故事、成语接龙、猜谜语,模型输出长短可控,用户可以等。

有些场景我不建议硬上。比如本地跑 RAG,需要集成向量数据库和嵌入模型,内存和算力都不够,效果也不会好。再比如长文本对话,KV cache 会迅速膨胀,上下文超 256 token 后内存和速度都会崩。还有实时语音对话,端到端延迟太高,除非把 ASR、LLM、TTS 全部拆成管线并行,否则体验会很差。

4.4 后续可以往哪继续深挖

这次 4.31 tok/s 只是当前这条路线的终点,不是 P4 的天花板。我接下来打算做几件事:一是对比不同量化档位对生成质量的真实影响,不只盯着速度;二是尝试把 ESP-DL 的矩阵乘算子接到 llama.cpp 后端,看有没有额外收益;三是试一下 1.58bit 的 BitNet 类小模型,这种模型权重占用极低,理论上能解锁更大的有效参数量。

另外,llama.cpp 对 RISC-V 的支持还在快速迭代中。我用的分支可能几个月后就被上游合入更好的算子优化,所以关注上游动态比死守本地优化更重要。到时候我还会出一篇对比更新后的版本和当前版本的速度差异。

5. 踩过的坑与排查速查表(含独家心得)

5.1 坑一:模型不驻留 PSRAM,性能直接崩

这是我踩过最深的坑,也是基线 0.61 tok/s 的根源。一开始我图省事,直接让 llama.cpp 从 SD 卡路径加载模型,以为它会自己做好缓存。实际跑起来每个 token 生成都要跨过 SD 卡 I/O,速度直接崩到不可用。

后来我改成启动阶段一次性把整个 GGUF 文件读入 PSRAM 缓冲区,再让模型从内存加载,速度立刻翻倍。这个读文件的动作只在启动时发生一次,推理阶段完全在内存里做。

注意:如果你在代码里看到 llama_model_load 之类的函数,确保传入的路径指向内存缓冲区而不是直接暴露 SD 卡文件接口。更稳妥的做法是先用标准文件 API 把文件完整读出来,再交给 llama 库处理。

5.2 坑二:向量扩展编译对了,代码还是没走进向量路径

第二个坑非常隐蔽。我明明在 CMake 里开了 GGML_RISCV,编译也成功了,但跑分纹丝不动。排查后发现,部分算子因为条件编译的参数判断没匹配,运行时落到了标量回退路径,向量内核根本没有启用。

解决办法是直接查看编译产物里是否包含带向量指令的符号,以及看启动日志里的算子类型打印。我在 llama.cpp 的推理初始化日志里加了一行输出,打印当前 CPU 检测到的指令集和实际启用的内核类型,从此再也没被这个问题坑过。

5.3 坑三:跑分忽高忽低,供电和散热先背锅

有一段时间我测出来的数据波动极大,同一个配置有时 3.8,有时 4.2。排查到最后发现是供电不稳加上芯片温度过高导致的频率抖动。ESP32-P4 在高负载下功耗上升很快,如果电源纹波大,或者芯片散热不好,跑分就会有明显波动。

后来我换了一条粗一点的 USB 供电线,同时把开发板平放,芯片背面贴了块小散热片,跑分立刻稳定下来。做 benchmark 之前先预热两轮,也能消除大部分波动。

5.4 排查速查表

症状可能原因排查方向解决办法
首 token 特别慢模型启动时从 SD 卡流式加载检查模型加载路径和内存驻留启动阶段整体读入 PSRAM
生成速度和各步优化无关向量内核未启用查看编译日志与运行时日志核对 march 参数和条件编译
跑分忽高忽低供电纹波大/芯片过热降频观察连续跑分曲线换电源、加散热、预热后再测
多线程提升很小线程未被正确调度/内存带宽饱和对比单双核跑分和 CPU 占用绑定核心,确认 PSRAM 带宽
生成质量明显变差量化档位过低或模型不匹配对比 Q8/Q4 的生成样本换回 Q4_0,或换更大模型
上下文一长就 OOMKV cache 占用超预算计算 KV cache 大小缩短上下文,或换更小模型

5.5 调优顺序的几个个人心得

第一个心得:先把数据搬到内存,再谈指令集和量化。很多人在 I/O 没解决的情况下疯狂调编译参数,收益会被 I/O 瓶颈吃掉,根本看不到效果。第二步才是让 CPU 跑得更快,第三步才是把模型压小。

第二个心得:每次只改一个变量。我见过太多人同时改线程、量化、batch,最后性能变了也说不清是哪步起作用,等于白测。建议准备一个表格,像上面那样记录每一轮的输入、改动、输出,结论一目了然。

第三个心得:别执着于跑分数字,要关注生成质量和稳定性。4.31 tok/s 这个数字好看,但如果一句话里出现乱码,数字就没有意义。每次优化后,我都会跑同一段 prompt,对比生成结果是否退化。在嵌入式 AI 上,“可用”永远优先于“好看”。

第四个心得:保持耐心。从 0.61 到 4.31 不是某一项神操作,而是十几次小改进的叠加。如果你只盯着最终数字,会觉得很神奇;但如果像我一样把每一步拆开,你会发现每一步都有清晰的原因和依据。这也是我写这个系列的核心思路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 6:22:33

ArcGIS制作全国PM2.5浓度分布图:从Excel表到论文级地图的完整实操

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:22:22

uni-app小程序chooseAndUploadFile权限问题排查与修复指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:22:18

基于YOLOv8的煤矸石识别数据集:小样本目标检测实战要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:21:48

STM32CubeMX图形化配置实战:从建工程到SPI读写Flash与FreeRTOS集成

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:19:34

VTOL固定翼无人机从零装机到调试全攻略:Pixhawk+ArduPilot实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 6:19:19

人脸关键点从68点到468点:索引定义、选型避坑与实战自查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华