yolo11n-pose:架构全解

我们的量化交付包只用了这个模型的前 11 层。这一页把完整结构拆开讲清楚:每一层是什么、 多出来的 neck 与 Pose 头在做什么、代价怎么分布,以及这些结构和我们后续几轮接口决策的关系。

所有数字均实测自 release/cricket_highlight_release/model/weights/cricket_encoder.pt, 输入 640×640、batch 1 · 2026-09-22

总览

24 层,三段式:backbone(0–10) 抽特征 → neck(11–22) 多尺度融合 → Pose 头(23) 出框和关键点。我们只要第一段。

总参数
2,874,462
yolo11n-pose,nc=1(person)
总算力 @640
7.54 G
FLOPs,batch 1
我们用的段
0–10
47.5% 参数 / 41% FLOPs
截断加速
2.3×
cuda bs=1 12.29 → 5.31 ms
特征维度
1536
4 层 × (avg+max)

结构图

虚线是我们的抽头;实线是模型自己的数据流。注意 neck 的四条跨层连接(6→12、4→15、13→18、10→21), 它们正是「融合」的实现方式,也是为什么 neck 不能脱离 backbone 单独存在。

我们的特征:avg + max 池化 → 1536 维 128+128 · 128+128 · 256+256 · 256+256 BACKBONE 0–10 逐层下采样 · 47.5% 参数 NECK 11–22 PAFPN 双向融合 · 27.6% POSE 头 23 24.9% 参数 / 37% FLOPs Pose(P3,P4,P5) → (1, 56, 8400) cv2 框 DFL 64ch · cv3 类别 1ch · cv4 关键点 51ch 8400 = 80²+40²+20² 个 anchor · stride 8/16/32
窄屏可左右拖动查看。方框里的数字是层号;括号里是该层输出的通道数与空间尺寸(640 输入)。

backbone(0–10):我们用的那一段

五次 stride-2 下采样把 640 压到 20×20,通道从 3 升到 256。n 档的缩放是 depth 0.50 / width 0.25 / max_channels 1024 —— 所以 yaml 里写 [512] 的那层实际只有 128 通道。

模块出现在一句话
Conv0,1,3,5,73×3 stride-2 卷积 + BN + SiLU,每出现一次分辨率减半
C3k22,4,6,8CSP 结构,特征劈两半、一半过 Bottleneck 再合并
SPPF9三次串联 5×5 maxpool,廉价地把感受野撑到全图
C2PSA10backbone 里唯一的注意力,2 头自注意力 + FFN

每个模块的内部结构、实际参数与量化视角见下面的 特殊层与特殊算子。

量化上的好消息:这段里最"非常规"的两个算子 —— SPPF 的串联 maxpool、C2PSA 的 softmax —— 在 QNN 上全部落 NPU,零 CPU 回退(S23 实测 177 个算子全在 NPU)。

neck(11–22):多出来的第一块

标准 PAFPN,两趟走完:先自顶向下把深层语义送到浅层,再自底向上把细节送回深层。 Upsample 和 Concat 零参数,真正花钱的是 4 个 C3k2 和 2 个下采样 Conv。

方向路径产出参数
自顶向下10 →Upsample→ 拼 6 → C3k2(13) →Upsample→ 拼 4 → C3k2(16) P3/8 64ch 80×80 — 小目标143,392
自底向上16 →Conv s2→ 拼 13 → C3k2(19) P4/16 128ch 40×40 — 中目标123,712
自底向上19 →Conv s2→ 拼 10 → C3k2(22) P5/32 256ch 20×20 — 大目标526,592

neck 的输出(16/19/22)仍然是带空间维度的特征图,这一点很关键:它是所有"要知道东西在哪"的头的输入。 我们项目里的 bowling_subject_head 就是在这三张图上做 RoIAlign。

在 P3/P4/P5 三个尺度上各挂三条并行分支(解耦头),所有尺度的结果拼成一个张量。

分支输出通道作用参数
cv264 = 16×4框回归,DFL 形式:每条边预测 16 个 bin 的分布,再取期望得到距离381,888
cv31类别分数。nc=1,只有 person48,963
cv451 = 17×3COCO-17 关键点的 (x, y, visible)284,427
输出形状
(1, 56, 8400) —— 每个 anchor 56 维 = 4 框 + 1 类 + 51 关键点
8400
80²+40²+20² = 6400+1600+400,三个尺度的 anchor 总数
还带什么
DFL 解码、anchor/stride 网格生成,推理时外加 NMS
反直觉的一点:Pose 头只有 1 层,却吃掉 37% 的算力(2.83 GFLOPs)—— 因为那三条分支要在 80×80 的高分辨率图上跑满通道。参数占比(24.9%)严重低估了它的真实成本。

特殊层与特殊算子

YOLO11 里真正需要解释的结构就这几个。每个都附上量化视角 —— 它在定点后端上是便宜还是贵、我们实测到什么。

Conv + SiLU —— 基础单元

卷积 + BatchNorm + SiLU。SiLU(x) = x · sigmoid(x), 比 ReLU 平滑、负半轴不完全截断,但代价是它在计算图里是 Sigmoid + Mul 两个算子,不是一个激活函数。

量化视角:BN 在导出时折进卷积权重(定点后端的前提)。 所有 block 的动态范围其实由 SiLU 的 Mul 输出决定 —— 我们查编码时看的就是这些 */act/Mul_output_0 张量。

C3k2 —— 主干的重复单元(layer 2/4/6/8)

CSP 思路:1×1 卷积把特征劈两半,一半走 Bottleneck、一半直连, 末尾 concat 再 1×1 融合。好处是梯度路径短、重复特征少,参数比堆卷积省。

n 档里两种内部结构并存 —— 这点容易看漏:

层内部深度
layer 2 / 4C3k=False → 普通 Bottleneck1 个,带 shortcut
layer 6 / 8C3k=True → C3k 块C3k 里再套 2 个 Bottleneck

所以越深的 C3k2 越"厚",这也是 layer 8 有 346k 参数、 layer 4 只有 26k 的原因。

SPPF —— 廉价的全图感受野(layer 9)

一个 5×5 stride=1 的 maxpool 串联跑三次,把四份结果 concat: 串两次等价于 9×9、串三次等价于 13×13。相比并联三个不同核的池化,串联把计算量压到几乎不变, 这就是名字里那个 F(Fast)。参数只有两个 1×1 卷积(256→128 和 512→256)。

量化视角:maxpool 只挑值、不产生新数值,量化上完全无害。 串联 maxpool 曾是我们担心会回退 CPU 的算子之一 —— 实测全落 NPU。

C2PSA —— backbone 里唯一的注意力(layer 10)

YOLO11 相对 v8 的主要结构改动。在 20×20 的最深层上做 2 头自注意力 + FFN,给纯卷积的 backbone 补一层全局上下文。实际参数:

num_heads
2(head_dim 64、key_dim 32、scale 0.1768)
qkv
1×1 卷积 128 → 256,一次算出 Q/K/V
pe
3×3 深度可分卷积(groups=128)—— 位置编码是卷出来的,不是加上去的
ffn
1×1 128 → 256 → 128,中间 SiLU
量化视角:注意力里的 QK^T 是全图动态范围最大的张量之一 (实测 −75.7 ~ +117.2,跨度 193)。softmax 输出则被钉在 [−1, 1]。 这两个算子当初都被列为"可能回退 CPU",实测全落 NPU。

Upsample + Concat —— neck 的黏合剂(零参数)

最近邻上采样 + 通道维拼接,不含任何权重,却是 PAFPN "把深层语义和浅层细节混起来"的全部机制。计算上是纯访存。

量化视角 —— 这是我们栽过跟头的地方: 定点后端会让 concat 的输出与各路输入共用一个编码。如果各输入幅度悬殊, 幅度大的那一路会被整块截断(我们试过在图内 concat 8 路池化,幅度差 12.8×,直接掉 ρ 0.027)。 neck 的 concat 拼的是幅度相近的卷积激活(≤1.83×),所以安全。 判据:输入幅度比超过约 3× 就要查编码。

DFL —— 把框回归变成分类(Pose 头内)

Distribution Focal Loss 的推理侧实现。每条边不直接回归一个距离, 而是预测 16 个 bin 上的概率分布,再取期望得到距离:

距离 = Σ softmax(logits[0..15])ᵢ × i      # i = 0,1,2,…,15

图里它是一个权重固定为 0,1,2,…,15 的 1×1 卷积(实测确认), 所以"求期望"这步就是一次卷积。好处是模型能表达"边界在这附近但不确定",比单点回归稳。 这也是 cv2 分支输出 64 通道的原因:4 条边 × 16 个 bin。

量化视角 —— C 臂锁 a16 的硬理由: DFL 解码之后的张量是像素坐标,实测跨度达 765(−40.8 ~ +724.4)。 per-tensor 量化下每档分辨率:int16 = 0.012 px,int8 = 3.0 px。 换句话说 a8 下光是最后这一步取整就会让框抖动 3 个像素,而我们在 a16 下实测的整条链路误差才 1.71 px。

量化视角汇总:谁的动态范围大

实测自 C 臂(backbone + neck + Pose 头)的 w8a16 QDQ 图。 「每档」= 跨度 ÷ 量化档位数,直接反映该张量能分辨多细。

张量实测范围跨度int16 每档int8 每档
DFL 解码后的框(像素坐标)−40.8 ~ +724.47650.012 px3.0 px
注意力 QKT(C2PSA)−75.7 ~ +117.21930.0030.76
注意力 ×V 之后−9.3 ~ +8.718.00.00030.071
SPPF maxpool 输出−7.4 ~ +10.618.00.00030.071
注意力 softmax−1.0 ~ +1.02.00.000030.008
类别分数 sigmoid0.0 ~ +1.01.00.000020.004
怎么用这张表:跨度大的张量是 a8 的第一批受害者。 backbone 段当初实测 a8 超线(Δ_irr −0.0164 vs 建议线 −0.005), 而 C 臂还多了 DFL 那个跨度 765 的框张量 —— 要试 a8,得先看这张表里排在最前面的几个。

代价分布

这张图解释了为什么截断能拿到 2.3× 而不是 1.9×:省掉的那一半,算力占比比参数占比更大。

backbone 0–10(我们用的) neck 11–22 Pose 头 23
参数2,874,462
FLOPs @6407.54 G
看数据表
段参数占比GFLOPs占比

逐层表

蓝色高亮行是我们抽特征的四层。from 列里出现列表的就是跨层连接 —— 这些层无法脱离前面的层单独存在。

#模块from输出stride参数GFLOPs

我们怎么用它

打分头和门控头回答的是"这一帧整体像不像一个好动作"——全局语义判断,不需要知道人在哪里。 所以我们在层 4/6/8/10 抽特征、池化成 1536 维,neck 和 Pose 头算完就扔。

1536 维是怎么拼的

位置来源特征图avgmax
0–255层 4(P3)128×80×80128128
256–511层 6(P4)128×40×40128128
512–1023层 8(P5)256×20×20256256
1024–1535层 10(C2PSA)256×20×20256256
顺序是按层交错的 [avg4, max4, avg6, max6, avg8, max8, avg10, max10],不是按 pool 分组。 写成分组顺序不报错,只会静默输出约 0 分。

为什么要写成独立 module

原来用 forward hook 取中间层,但 hook 不进 traced graph —— 导出 ONNX 时图会止于 Pose 头,抽头根本不存在。任何量化/端侧部署都必须从一个显式的单路径 module 开始, 这就是 PoseTrunk 的由来。截断后数值逐位一致(max|Δ| = 0.0)。

池化在图内还是图外

默认 pool=True:图内池化,每帧只搬 1536 个数。 pool=False 导出 4 张原始特征图(约 123 万个数,800× 的搬运量),给将来需要定位的头留路 —— 但端上 DMA 代价还没实测。

与量化交付的关系

这段结构上的取舍,直接决定了交付包三个版本的接口长相。

8 路输出 vs 图内 concat v1v2v3

HTP 的激活是 per-tensor 量化,而这 8 个 block 的幅度差十几倍(avg 块 ≲1,max 块到 ~7)。

  • 直接在图内 concat(试过):量化器给 concat 分配的共享编码是 [-0.27, 1.64], 所有 max 分支被截断,refit ρ 掉 0.027 —— 特征层仿真说"a16 下等价",被真量化器推翻。
  • concat 前逐通道缩放(v2):把 1536 个通道拉到相近幅度再拼,逐 block 误差与 8 路完全相同(Δρ −0.0013)。
  • 回到 8 路各自量化(v3,当前交付):每路自带编码,没有共享 range 的问题。
8 路方案下最隐蔽的坑:AI Hub 编译后,8 个输出被重命名为 output_0 … output_7,名字全丢、只能按位置对应。映射已在 S23 上实测验证 (端上 vs 本地 QDQ 特征相对 L2 0.00463,映射错了这个数会爆掉)。

任务头只输出 logit v3

头的 ONNX 去掉了 Sigmoid 节点,sigmoid 交给算法 pipeline。 动机是 sigmoid 饱和会压平差异(14% 样本 float32 下恰好 ==1.0,反推不回去);代价是产品阈值是概率, 拿 logit 直接比不报错、只是全错。哪些环节能留在 logit 空间,是实测出来的:

环节能否留在 logit 空间依据
门控:中值滤波 → 迟滞 → 阈值✅ 可以,逐位等价
阈值 ±0.4055
中值与迟滞只依赖大小关系、sigmoid 单调;2 万点随机检验 valid 完全一致
raw 的滑动平均(显示/排序)❌ 必须先 sigmoid mean(sigmoid) ≠ sigmoid(mean),实测最大差 0.39
soft 模式 raw × gate❌ 必须先 sigmoid 乘法在 logit 空间没有对应

padding 维持 114 v3

letterbox 居中填 114,与原版 ultralytics/yolo 约定一致。 改成 0 的代价实测是整个量化误差的 5.2 倍(raw mean|Δ| 0.05975 vs 0.01143,100 张里 19 张 [email protected] 判定翻转)—— 因为 backbone 在 114 上预训练、头在 114 的特征分布上拟合 mu/sd、校准集也全是 114。 16:9 广播素材约 44% 画面是填充,最受影响。

需要完整模型的场景,以及待探讨

这两条链路绕不开 neck + Pose 头

  • bowling_subject_head:在 neck 的 16/19/22 上 RoIAlign,框来自 Pose 头。 连 maps 变体都不够 —— 它要的不只是特征图,还要检测结果。
  • 面部可见性微调:需要 COCO-17 关键点,也就是 cv4 那条分支。

如果要把检测头也量化

难点不在 Conv。DFL 解码、anchor 网格、NMS 都不是标准卷积算子, 落 NPU 的情况要单独验证;backbone 那段已验证零回退,不能直接外推到头上。

仍未解决

  • SM8735 真机延迟(农场无此设备,S23 只是同架构代理)
  • 量化 I/O:中端档编译要求输入非浮点;改 uint8 还能把输入 DMA 砍到 1/4
  • maps 变体的 DMA 代价未实测(约 800× 数据量)

值得一试的方向

  • 产品 pipeline 的门控链改成 logit 空间(已证等价,省两次 sigmoid)
  • bowling_moment_head 换到量化 backbone 的特征源(它吃的正是这 1536 维)
  • C2PSA 那层的注意力对 int8 是否更敏感 —— 目前只在 a16 下验证过