Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

AI 编译器 / AI Compilers

1. AI 编译器全景——为什么模型需要编译器 / The AI Compiler Landscape and Why Models Need Compilers

2. 编译原理速通——面向 ML 工程师的核心概念 / Compiler Fundamentals for Machine Learning Engineers

3. 中间表示基础——理解 IR 层级与 lowering 链路 / Intermediate Representation Levels and Lowering Pipelines

4. 计算图的构建与表示 / Building and Representing Computational Graphs

5. MLIR 架构、方言与渐进式降级 / MLIR Architecture, Dialects, and Progressive Lowering

6. 算子语义、广播、归约与形状推导 / Operator Semantics, Broadcasting, Reduction, and Shape Inference

7. 模型前端格式:ONNX、TFLite、HLO 与 SavedModel / Model Frontend Formats: ONNX, TFLite, HLO, and SavedModel

8. 图优化 Pass——经典优化在 ML 中的应用 / Graph Optimization Passes for Machine Learning

9. 算子融合——编译器最重要的性能优化 / Operator Fusion as a Core Compiler Optimization

10. 内存规划——Buffer 分配与生命周期管理 / Memory Planning, Buffer Allocation, and Lifetime Management

11. Layout 优化——数据排布转换与内存效率 / Layout Optimization for Data Movement and Memory Efficiency

12. 动态 Shape——符号分析与形状处理 / Dynamic Shapes, Symbolic Analysis, and Shape Processing

13. 硬件约束下的操作调度 / Operation Scheduling Under Hardware Constraints

14. 从模板、DSL 到 IR 降级的代码生成架构 / Code Generation Architectures from Templates and DSLs to IR Lowering

15. CPU 后端:SIMD、分块与多线程 / CPU Backends with SIMD, Tiling, and Multithreading

16. CUDA 后端:合并访存与 Tensor Core / CUDA Backends, Memory Coalescing, and Tensor Cores

17. NPU 后端:脉动阵列与端侧 AI 生态 / NPU Backends, Systolic Arrays, and Edge AI Ecosystems

18. Kernel 性能基础:Roofline 与 Occupancy / Kernel Performance Fundamentals with Roofline and Occupancy

19. CUTLASS 与分层 GEMM 模板 / CUTLASS and Hierarchical GEMM Templates

20. TVM Tensor Expression 与计算调度分离 / TVM Tensor Expressions and Compute-Schedule Separation

21. 使用 Triton 编写高性能 GPU Kernel / Triton for High-Performance GPU Kernels in Python

22. 基于成本模型与实测搜索的自动调度 / Automatic Scheduling with Cost Models and Measurement-Based Search

23. XLA 内部机制:HLO、融合与 SPMD / XLA Internals, HLO, Fusion, and SPMD

24. Torch-MLIR:从 PyTorch 算子到 MLIR 方言 / Torch-MLIR from PyTorch Operators to MLIR Dialects

25. torch.compile:Dynamo、AOTAutograd、Inductor 与 Triton / Torch Compile with Dynamo, AOTAutograd, Inductor, and Triton

26. 从 MLIR 经 LLVM 降级到机器码 / Lowering from MLIR Through LLVM to Machine Code

27. 量化——低精度推理的工程实践 / Engineering Low-Precision Inference with Quantization

28. 分布式编译与训练——多设备编排的编译器支持 / Compiler Support for Distributed Training and Multi-Device Orchestration

29. 生产调试——真实问题的编译器视角排查 / Production Debugging from the Compiler Perspective

30. 未来方向——AI 编译器的新挑战与机遇 / Future Challenges and Opportunities for AI Compilers

本页目录

📅 创建时间:2026-06-03 🏷️ 标签:#算子语义 #Broadcast #Reduce #Matmul #Shape推理 #语义陷阱 #NCHW #NHWC

📚 前置知识:[[03-graph-representation]](计算图表示) 📚 相关知识:[[06-frontend-formats]](前端格式)[[08-operator-fusion]](算子融合)


算子语义、广播、归约与形状推导 / Operator Semantics, Broadcasting, Reduction, and Shape Inference ​

┌──────────────────────────────────────────────────────────────────────────────┐
│  🎯 场景:ONNX 导出时的 Shape 不兼容错误                                        │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  你在导出 ONNX 模型时报错:                                                    │
│                                                                              │
│  RuntimeError: Got unexpected shape [batch, 3, 224, 224] for input 'x'.    │
│  Expected shape compatible with [*, 3, 224, 224]                             │
│                                                                              │
│  你检查了 PyTorch 模型,输入确实是 batch×3×224×224。                           │
│  但 ONNX exporter 报错说 shape 不兼容。                                        │
│                                                                              │
│  你不知道问题出在 broadcast、slice 还是 exporter 的 bug 里。                      │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第1节:算子语义的重要性——同名算子,不同行为 ​

1.1 语义差异的危害 ​

算子语义不一致是深度学习框架互操作性的最大障碍之一。即使两个框架都声称支持某个操作,它们的实际行为可能完全不同。

python
"""
语义差异的危害示例:

场景:把 PyTorch 模型导出到 ONNX,然后部署到 TFLite

问题链:
1. PyTorch 的 conv2d 默认是 NCHW 格式
2. ONNX 规范支持 NCHW 和 NHWC(通过 operator 属性)
3. TFLite 的 conv2d 默认是 NHWC 格式

结果:
- 如果不显式指定格式
- PyTorch → ONNX 可能产生 NCHW
- ONNX → TFLite 可能转换失败或产生错误结果

这就是为什么理解算子语义如此重要。
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

1.2 语义差异的来源 ​

来源说明示例
历史原因不同框架独立发展,命名习惯不同PyTorch 的 view vs TF 的 reshape
硬件优化不同硬件偏好不同格式GPU 偏好 NCHW,CPU 偏好 NHWC
默认值差异未指定时使用框架默认值broadcast 方向、padding 算法
精度处理浮点溢出、NaN 处理不同NaN * 0 在不同框架可能不同
索引语义行优先 vs 列优先axis=0 在不同上下文含义不同

第2节:Broadcast 语义详解 ​

2.1 NumPy/PyTorch Broadcast 规则 ​

NumPy-style broadcast 从右对齐,自动补维。规则如下:

python
import torch
import numpy as np

print("=" * 60)
print("Broadcast 规则详解")
print("=" * 60)

# 规则1:维度从右向左对齐
# 如果维度不同,小的维度被扩展(或"广播")以匹配大的

# 示例1:简单情况
a = torch.randn(3, 4)        # shape: (3, 4)
b = torch.randn(4)          # shape: (4,)
# b 被广播到 (1, 4),然后和 a 的每一行相加
result = a + b
print(f"(3,4) + (4,) = (3,4)  # b 变成 (1,4) 然后广播")

# 示例2:多维情况
a = torch.randn(5, 3, 4)    # shape: (5, 3, 4)
b = torch.randn(3, 4)        # shape: (3, 4)
# b 被广播到 (1, 3, 4) 然后和 a 相加
result = a + b
print(f"(5,3,4) + (3,4) = (5,3,4)  # b 变成 (1,3,4) 然后广播")

# 示例3:维度不兼容情况
# a = torch.randn(5, 3, 4)    # shape: (5, 3, 4)
# b = torch.randn(4, 5)        # shape: (4, 5)
# 无法广播!因为:
#   a: 5  3  4
#   b: 4  5  ← 4 ≠ 5,无法匹配

print()
print("规则2:维度必须是 1 或相同")
print("  (3,) 和 (3,) → 广播")
print("  (1,) 和 (3,) → 广播(1 被扩展到 3)")
print("  (2,) 和 (3,) → 无法广播!")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

2.2 Broadcast 的具体示例 ​

python
# ============================================================
# Broadcast 常见场景
# ============================================================

def broadcast_examples():
    print("=" * 60)
    print("Broadcast 场景示例")
    print("=" * 60)
    
    # 场景1:LayerNorm 中的 broadcast
    # 输入: (batch, seq_len, hidden_dim) = (B, S, H)
    # gamma, beta: (H,) → 被广播到 (1, 1, H)
    # mean, var: scalar → 被广播到 (B, S, 1)
    
    x = torch.randn(32, 128, 512)  # B=32, S=128, H=512
    mean = x.mean(dim=-1, keepdim=True)  # shape: (32, 128, 1)
    var = x.var(dim=-1, keepdim=True)    # shape: (32, 128, 1)
    # 归一化时,(x - mean) / sqrt(var + eps) 都是广播操作
    
    print("LayerNorm Broadcast:")
    print(f"  输入 x: {x.shape}")
    print(f"  mean: {mean.shape}")
    print(f"  var: {var.shape}")
    print(f"  x - mean: {(x - mean).shape}")  # 广播
    
    print()
    # 场景2:Attention 中的 QK^T
    # Q: (B, H, S, D) = (2, 8, 128, 64)
    # K: (B, H, D, S) = (2, 8, 64, 128)
    # QK^T: (B, H, S, S) = (2, 8, 128, 128)
    # 但 attention score 需要 scale: score / sqrt(D)
    # scale: scalar → 广播到 (B, H, S, S)
    
    Q = torch.randn(2, 8, 128, 64)
    K = torch.randn(2, 8, 64, 128)
    scale = 1.0 / (64 ** 0.5)  # scalar
    
    print("Attention Scale:")
    print(f"  Q: {Q.shape}")
    print(f"  K: {K.shape}")
    print(f"  scale: {scale} (scalar)")
    print(f"  Q @ K.transpose(-2, -1): {(Q @ K.transpose(-2, -1)).shape}")
    
    print()
    # 场景3:残差连接
    # input: (B, C, H, W)
    # residual: (B, C, H, W) 或 (C,) 或 (1, C, 1, 1)
    # broadcast 确保维度匹配
    
    input_tensor = torch.randn(4, 64, 32, 32)
    residual = torch.randn(64)  # (C,) → 被广播到 (1, 64, 1, 1)
    
    print("Residual Broadcast:")
    print(f"  input: {input_tensor.shape}")
    print(f"  residual: {residual.shape}")
    print(f"  input + residual: {(input_tensor + residual).shape}")

broadcast_examples()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

2.3 Broadcast 常见错误 ​

python
# ============================================================
# Broadcast 常见错误
# ============================================================

def broadcast_errors():
    print("=" * 60)
    print("Broadcast 错误案例")
    print("=" * 60)
    
    print()
    print("错误1:维度不匹配")
    print("-" * 40)
    
    # 错误情况
    a = torch.randn(3, 4, 5)  # (3, 4, 5)
    b = torch.randn(4, 3)      # (4, 3)
    
    print(f"  a.shape = {a.shape}")
    print(f"  b.shape = {b.shape}")
    print("  对齐方式:")
    print("    a: 3  4  5")
    print("    b:    4  3  ← 从右向左:5 vs 3(不兼容!)")
    print()
    print("  尝试广播会报错:")
    try:
        result = a + b
    except RuntimeError as e:
        print(f"  RuntimeError: {e}")
    
    print()
    print("错误2:隐式假设广播方向")
    print("-" * 40)
    
    # 很多人错误地认为 broadcast 总是从左向右
    x = torch.randn(4, 1, 8)  # (4, 1, 8)
    y = torch.randn(1, 8, 4)  # (1, 8, 4)
    
    print(f"  x.shape = {x.shape}")
    print(f"  y.shape = {y.shape}")
    print("  NumPy/PyTorch 广播(从右向左):")
    print("    x: 4  1  8")
    print("    y: 1  8  4")
    print("    结果: 4  8  8  ← 不是 (4,1,4)!")
    print(f"  实际结果: {(x + y).shape}")
    
    print()
    print("错误3:混淆 batch broadcast 和 channel broadcast")
    print("-" * 40)
    
    # 图像批次
    images = torch.randn(8, 3, 224, 224)  # (B, C, H, W)
    mean = torch.tensor([0.485, 0.456, 0.406])  # (3,) RGB 均值
    
    print(f"  images: {images.shape}")
    print(f"  mean: {mean.shape}")
    print()
    print("  如果直接减:")
    print("    mean 被广播到 (1, 3, 1, 1)")
    print("    结果: (8, 3, 224, 224) ✓")
    print(f"    实际: {(images - mean).shape}")
    print()
    print("  ⚠️ 注意:mean 是 RGB 顺序,需要确保和 C 维度匹配!")
    print("    ImageNet: mean = [0.485, 0.456, 0.406] 对应 R,G,B")

broadcast_errors()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

2.4 TensorFlow/XLA Broadcast ​

python
"""
TensorFlow Broadcast 扩展支持

TensorFlow 相比 NumPy/PyTorch 有一些扩展:

1. tf.broadcast_to:显式广播
2. tf.shape + tf.where:复杂条件广播
3. 支持 masked 操作

示例:
    import tensorflow as tf
    
    # 显式广播到指定形状
    x = tf.constant([1, 2, 3])
    y = tf.broadcast_to(x, [3, 3])
    # y = [[1, 2, 3],
    #      [1, 2, 3],
    #      [1, 2, 3]]
"""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

第3节:Reduce 操作详解 ​

3.1 Reduce 的基本概念 ​

Reduce 是将张量沿某个轴(或多个轴)聚合的操作。

python
import torch

print("=" * 60)
print("Reduce 操作详解")
print("=" * 60)

x = torch.arange(24).reshape(2, 3, 4).float()  # (2, 3, 4)
print(f"输入张量 x:\n{x}")
print(f"x.shape = {x.shape}")

print()
print("-" * 40)
print("沿 axis=0 求和(batch 维度)")
print("-" * 40)
# 结果 shape: (3, 4)
print(f"x.sum(axis=0).shape = {x.sum(axis=0).shape}")
print(f"x.sum(axis=0):\n{x.sum(axis=0)}")

print()
print("-" * 40)
print("沿 axis=1 求和(sequence 维度)")
print("-" * 40)
# 结果 shape: (2, 4)
print(f"x.sum(axis=1).shape = {x.sum(axis=1).shape}")
print(f"x.sum(axis=1):\n{x.sum(axis=1)}")

print()
print("-" * 40)
print("沿 axis=2 求和(feature 维度)")
print("-" * 40)
# 结果 shape: (2, 3)
print(f"x.sum(axis=2).shape = {x.sum(axis=2).shape}")
print(f"x.sum(axis=2):\n{x.sum(axis=2)}")

print()
print("-" * 40)
print("keepdims=True(保持维度)")
print("-" * 40)
print(f"x.sum(axis=0, keepdims=True).shape = {x.sum(axis=0, keepdims=True).shape}")
print(f"x.sum(axis=1, keepdims=True).shape = {x.sum(axis=1, keepdims=True).shape}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

3.2 Reduce 的语义差异 ​

python
# ============================================================
# PyTorch vs NumPy vs TensorFlow Reduce 差异
# ============================================================

print("=" * 60)
print("Reduce 操作跨框架差异")
print("=" * 60)

# PyTorch API
pytorch_result = torch.sum(x, dim=0)  # PyTorch 用 dim

# NumPy API
numpy_result = x.numpy().sum(axis=0)  # NumPy 用 axis

# TensorFlow API
# tf.reduce_sum(x, axis=0)  # TensorFlow 用 axis

print()
print("PyTorch: torch.sum(x, dim=0)   ← 用 dim")
print("NumPy:   x.sum(axis=0)         ← 用 axis")
print("TensorFlow: tf.reduce_sum(x, axis=0) ← 用 axis")
print()
print("注意:PyTorch 用 'dim',NumPy/TF 用 'axis',这是命名差异!")

print()
print("-" * 40)
print("keepdims 差异")
print("-" * 40)

# PyTorch
pytorch_keepdims = torch.sum(x, dim=0, keepdim=True)
print(f"PyTorch keepdim=True: {pytorch_keepdims.shape}")

# NumPy
numpy_keepdims = x.numpy().sum(axis=0, keepdims=True)
print(f"NumPy keepdims=True: {numpy_keepdims.shape}")

print()
print("⚠️ 两者结果相同,但 API 命名不同!")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

3.3 Reduce 的常见用法 ​

python
# ============================================================
# Reduce 常见场景
# ============================================================

def reduce_scenarios():
    print("=" * 60)
    print("Reduce 操作常见场景")
    print("=" * 60)
    
    print()
    print("场景1:Cross Entropy 计算中的 logsumexp")
    print("-" * 40)
    
    logits = torch.randn(32, 1000)  # (batch, classes)
    max_logits = logits.max(dim=1, keepdim=True).values  # (batch, 1)
    # 数值稳定技巧:减去 max
    stable_logits = logits - max_logits
    log_sum_exp = stable_logits.exp().sum(dim=1, keepdim=True).log()  # (batch, 1)
    
    print(f"  logits: {logits.shape}")
    print(f"  log_sum_exp: {log_sum_exp.shape}")
    print("  用于数值稳定的交叉熵计算")
    
    print()
    print("场景2:Global Average Pooling")
    print("-" * 40)
    
    feature_map = torch.randn(8, 256, 7, 7)  # (B, C, H, W)
    # 在 H 和 W 维度上做平均
    pooled = feature_map.mean(dim=[2, 3])  # (B, C)
    
    print(f"  feature_map: {feature_map.shape}")
    print(f"  pooled: {pooled.shape}")
    print("  用于 CNN 的最后阶段,将空间维度池化掉")
    
    print()
    print("场景3:Sequence Masking 中的 sum")
    print("-" * 40)
    
    # 假设有 mask 表示有效位置
    scores = torch.randn(4, 10)  # (batch, seq_len)
    mask = torch.tensor([[1, 1, 1, 0, 0, 0, 0, 0, 0, 0],
                          [1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
                          [1, 1, 1, 1, 1, 0, 0, 0, 0, 0],
                          [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]])  # (batch, seq_len)
    
    # masked sum
    masked_scores = scores * mask.float()
    summed = masked_scores.sum(dim=1)  # (batch,)
    
    print(f"  scores: {scores.shape}")
    print(f"  mask: {mask.shape}")
    print(f"  masked_scores: {masked_scores.shape}")
    print(f"  summed: {summed.shape}")

reduce_scenarios()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

第4节:Matmul 和 BatchMatmul ​

4.1 2D 矩阵乘法 ​

python
import torch

print("=" * 60)
print("2D 矩阵乘法")
print("=" * 60)

A = torch.randn(3, 4)  # (M, K)
B = torch.randn(4, 5)  # (K, N)

# torch.matmul:通用矩阵乘法
C = torch.matmul(A, B)  # (3, 5)
# 或者使用 @ 运算符
C2 = A @ B

# torch.mm:2D 专用矩阵乘法
C3 = torch.mm(A, B)

print(f"A @ B = ({A.shape} @ {B.shape}) = {C.shape}")
print(f"torch.matmul(A, B) = {C.shape}")
print(f"torch.mm(A, B) = {C3.shape}")
print()
print("数学含义:C[i][j] = sum(A[i][k] * B[k][j]) for k in 0..K-1")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

4.2 Batch 矩阵乘法 ​

python
print()
print("=" * 60)
print("Batch 矩阵乘法 (Batched Matmul)")
print("=" * 60)

# Batch Matmul:批量矩阵乘法
# A: (B, M, K)  # B 个 MxK 矩阵
# B: (B, K, N)  # B 个 KxN 矩阵
# C: (B, M, N)  # B 个 MxN 矩阵

B = 4  # batch size
M = 3  # output rows
K = 5  # inner dimension
N = 2  # output cols

A = torch.randn(B, M, K)  # (4, 3, 5)
B_mat = torch.randn(B, K, N)  # (4, 5, 2)

C = torch.bmm(A, B_mat)  # (4, 3, 2)  bmm = batched mm

print(f"A @ B = ({A.shape} @ {B_mat.shape}) = {C.shape}")
print()
print("注意:batch 维度必须匹配,但 M, K, N 可以任意")
print()

# torch.baddbmm:batch + 对角线
print("-" * 40)
print("torch.baddbmm:带偏置的 batch matmul")
print("-" * 40)
bias = torch.randn(B, M, N)  # (4, 3, 2)
result = torch.baddbmm(bias, A, B_mat)  # bias + A @ B
print(f"baddbmm(bias, A, B) = {result.shape}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

4.3 Broadcasting in Matmul ​

python
print()
print("=" * 60)
print("Matmul 中的 Broadcast")
print("=" * 60)

# torch.matmul 支持广播

# 情况1:常规 batch matmul
A = torch.randn(4, 3, 5)  # (B, M, K)
B = torch.randn(4, 5, 2)  # (B, K, N)
C = A @ B  # (4, 3, 2)
print(f"常规 BMM: {A.shape} @ {B.shape} = {C.shape}")

# 情况2:广播 batch 维度
A = torch.randn(4, 3, 5)  # (B, M, K)
B = torch.randn(5, 2)     # (K, N) → 被广播到 (1, 5, 2) 然后 (4, 5, 2)
C = A @ B
print(f"Broadcast B: {A.shape} @ {B.shape} = {C.shape}")
print("  B 从 (5,2) 广播到 (4,5,2)")

# 情况3:广播两个 batch 维度
A = torch.randn(4, 1, 5)  # (B, 1, K)
B = torch.randn(1, 3, 5, 2)  # (1, A, K, N)
C = A @ B
print(f"Broadcast both: {A.shape} @ {B.shape} = {C.shape}")
print("  A 从 (4,1,5) 广播到 (4,3,5)")
print("  B 从 (1,3,5,2) 广播到 (4,3,5,2)")
print("  结果: (4,3,5,2) → 只保留最后两个维度!")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

第5节:Convolution 变体 ​

5.1 数据格式:NCHW vs NHWC ​

python
import torch

print("=" * 60)
print("卷积数据格式:NCHW vs NHWC")
print("=" * 60)

# PyTorch 默认使用 NCHW
# TensorFlow 默认使用 NHWC

# NCHW: (Batch, Channel, Height, Width)
# NHWC: (Batch, Height, Width, Channel)

input_nchw = torch.randn(1, 3, 224, 224)  # PyTorch 格式
print(f"NCHW 格式: {input_nchw.shape}")
print(f"  B=1, C=3, H=224, W=224")
print()

# 转换为 NHWC(在 Tensor 中表示)
input_nhwc = input_nchw.permute(0, 2, 3, 1)  # (1, 224, 224, 3)
print(f"NHWC 格式: {input_nhwc.shape}")
print(f"  B=1, H=224, W=224, C=3")
print()

print("格式选择的影响:")
print("-" * 40)
print()
print("NCHW(PyTorch 偏好):")
print("  ✅ GPU 内存访问效率高(同一通道数据连续)")
print("  ✅ 卷积核是 (C_out, C_in, K, K),硬件友好")
print("  ✅ PyTorch 默认")
print()
print("NHWC(TensorFlow/CPU 偏好):")
print("  ✅ RGB 图像自然排列(像素点是连续的)")
print("  ✅ 部分 CPU 实现更快(SIMD)")
print("  ✅ 某些硬件(NPU)原生支持")
print()

print("混合格式(NCHWc / HWNC):")
print("  - 把 channel 分成多个 group")
print("  - 结合 NCHW 和 NHWC 的优点")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

5.2 Conv2d 变体参数 ​

python
# ============================================================
# Conv2d 参数详解
# ============================================================

def conv_variants():
    print("=" * 60)
    print("Conv2d 变体参数")
    print("=" * 60)
    
    x = torch.randn(1, 3, 32, 32)  # (B, C, H, W)
    
    print(f"输入: {x.shape}")
    print()
    
    # 标准卷积
    conv = torch.nn.Conv2d(3, 64, kernel_size=3, padding=1)
    print(f"标准 Conv(3,64,k=3,p=1): {conv(x).shape}")
    
    # 空洞卷积 (Dilation)
    conv_dilated = torch.nn.Conv2d(3, 64, kernel_size=3, padding=2, dilation=2)
    print(f"空洞 Conv(dilation=2): {conv_dilated(x).shape}")
    print("  ↑ 输出 H,W 不变,但感受野更大")
    
    # 分组卷积 (Groups)
    # 把输入通道分成 groups 组,每组独立卷积
    conv_groups = torch.nn.Conv2d(4, 8, kernel_size=3, padding=1, groups=2)
    # 输入必须是 4 的倍数,输出必须是 8 的倍数
    x_groups = torch.randn(1, 4, 16, 16)
    print(f"分组 Conv(4->8, groups=2): {conv_groups(x_groups).shape}")
    print("  ↑ 参数量减少一半")
    
    # Depthwise 卷积 (groups = in_channels)
    conv_depthwise = torch.nn.Conv2d(3, 3, kernel_size=3, padding=1, groups=3)
    print(f"Depthwise Conv(3,3,groups=3): {conv_depthwise(x).shape}")
    print("  ↑ 每个通道单独卷积,用于轻量化网络 (MobileNet)")
    
    # 转置卷积 (Deconv / ConvTranspose)
    conv_transpose = torch.nn.ConvTranspose2d(3, 64, kernel_size=4, stride=2, padding=1)
    print(f"Transposed Conv(3->64, s=2): {conv_transpose(x).shape}")
    print("  ↑ 扩大特征图,用于上采样/生成网络")
    
    # 1x1 卷积
    conv_1x1 = torch.nn.Conv2d(3, 64, kernel_size=1)
    print(f"1x1 Conv(3->64): {conv_1x1(x).shape}")
    print("  ↑ 只改变通道数,不改变空间维度")
    print("  ↑ 常用于通道间的信息融合 (Bottleneck)")

conv_variants()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

5.3 Convolution 输出尺寸公式 ​

python
print()
print("=" * 60)
print("卷积输出尺寸公式")
print("=" * 60)

print("""
标准卷积输出尺寸:
┌────────────────────────────────────────────────────────┐
│  output_size = floor((input_size + 2*padding -       │
│                       dilation*(kernel_size-1) - 1)  │
│                      / stride) + 1                    │
└────────────────────────────────────────────────────────┘

转置卷积输出尺寸:
┌────────────────────────────────────────────────────────┐
│  output_size = (input_size - 1) * stride              │
│                - 2 * padding                          │
│                + dilation * (kernel_size - 1)         │
│                + output_padding + 1                   │
└────────────────────────────────────────────────────────┘
""")

# 验证
x = torch.randn(1, 1, 10, 10)
conv = torch.nn.Conv2d(1, 1, kernel_size=3, stride=1, padding=0)
y = conv(x)
print(f"输入: (1, 1, 10, 10)")
print(f"Conv(k=3, s=1, p=0): 输出 {y.shape}")
print(f"验证: (10 - 3 + 0) / 1 + 1 = {int((10 - 3) / 1 + 1)}")

conv = torch.nn.Conv2d(1, 1, kernel_size=3, stride=2, padding=1)
y = conv(x)
print(f"\n输入: (1, 1, 10, 10)")
print(f"Conv(k=3, s=2, p=1): 输出 {y.shape}")
print(f"验证: (10 + 2 - 2) / 2 + 1 = {int((10 + 2*1 - 2) / 2 + 1)}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

第6节:Shape 推导与类型检查 ​

6.1 静态 Shape 推导 ​

python
print()
print("=" * 60)
print("Shape 推导(静态分析)")
print("=" * 60)

print("""
静态 Shape 推导的意义:
1. 在运行时之前就知道输出形状
2. 提前分配内存
3. 检测形状不匹配错误
4. 优化内存布局

Shape 推导规则示例:

op: matmul(A: (B, M, K), B: (B, K, N)) → (B, M, N)
op: reshape(x: (*), shape: (B, M, N)) → (B, M, N) if B*M*N = prod(*)
op: conv2d(x: (N, C_in, H, W), w: (C_out, C_in, K, K),
           stride: s, padding: p) → (N, C_out,
                                      (H + 2p - K)/s + 1,
                                      (W + 2p - K)/s + 1)
""")

# 实际代码示例
def shape_inference_examples():
    """展示如何推导 Shape"""
    
    print("Shape 推导实例:")
    print("-" * 40)
    
    # 输入
    x = torch.randn(8, 3, 224, 224)  # NCHW
    
    # Conv + BN + ReLU
    conv1 = torch.nn.Conv2d(3, 64, 7, stride=2, padding=3)
    # Shape: (N, 64, (224+6-7)/2+1, (224+6-7)/2+1) = (8, 64, 112, 112)
    
    bn1 = torch.nn.BatchNorm2d(64)
    # Shape: (8, 64, 112, 112) - BN 不改变 Shape
    
    relu = torch.nn.ReLU()
    # Shape: (8, 64, 112, 112) - ReLU 不改变 Shape
    
    print(f"输入: {x.shape}")
    x = conv1(x)
    print(f"Conv: {x.shape}")
    x = bn1(x)
    print(f"BN:   {x.shape}")
    x = relu(x)
    print(f"ReLU: {x.shape}")
    
    # 全局池化
    gap = torch.nn.AdaptiveAvgPool2d((1, 1))
    x = gap(x)
    print(f"GAP:  {x.shape}")  # (8, 64, 1, 1)
    
    # Flatten
    x = x.view(x.size(0), -1)
    print(f"View: {x.shape}")  # (8, 64)

shape_inference_examples()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

6.2 静态分析能发现什么 ​

python
print()
print("=" * 60)
print("静态 Shape 分析的能力边界")
print("=" * 60)

print("""
静态 Shape 分析 **可以** 发现的问题:

✅ Shape 不匹配:
   matmul(A: (3, 4), B: (5, 4)) → 无法相乘!3 ≠ 5 错误

✅ Broadcast 不兼容:
   (3, 4) + (5, 4) → 广播不兼容

✅ 维度约束:
   reshape(x: (24,), shape: (4, 6)) → OK
   reshape(x: (24,), shape: (4, 5)) → 4*5 ≠ 24,错误

✅ Conv 参数验证:
   groups=2 要求 in_channels % groups == 0


静态 Shape 分析 **无法** 发现的问题:

❌ 数值溢出:
   float16 的乘法可能导致溢出

❌ 逻辑错误:
   你想计算 cos(x) 但写成了 sin(x)

❌ 运行时动态形状:
   if batch_size > 32 then reshape to (64, ...) else reshape to (32, ...)

❌ 语义错误:
   axis=-1 和 axis=2 可能形状相同但语义不同
""")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

6.3 ONNX Shape 推理 ​

python
# ============================================================
# ONNX Shape 推理
# ============================================================

"""
ONNX 提供静态 Shape 推理工具:

import onnx
from onnx import shape_inference

model = onnx.load("model.onnx")

# 推理 Shape
inferred_model = shape_inference.infer_shapes(model)

# 获取输入输出 Shape
for input_tensor in inferred_model.graph.input:
    shape = [dim.dim_value for dim in input_tensor.type.tensor_type.shape.dim]
    print(f"Input: {input_tensor.name}, shape: {shape}")
"""

print()
print("ONNX Shape 推理示例:")
print("-" * 40)
print("""
当导出 ONNX 时:

torch.onnx.export(
    model,
    x,
    "model.onnx",
    input_names=["input"],
    output_names=["output"],
    dynamic_axes={
        "input": {0: "batch_size"},  # 动态 batch 维度
        "output": {0: "batch_size"}
    },
    opset_version=13  # 指定 opset 版本
)

结果:
- 如果不指定 dynamic_axes:所有维度都是静态的
- 指定后:batch_size 维度变为动态

运行时:
- 输入 shape: (?, 3, 224, 224) → batch 可以是任意值
""")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

第7节:跨框架算子对比表 ​

7.1 常见操作的框架对比 ​

操作PyTorchTensorFlowONNXNumPy
矩阵乘法A @ B / torch.mmtf.matmulMatMulA @ B
逐元素乘法A * BA * BMulA * B
广播加法A + bA + bAddA + b
Reshapex.view(...)tf.reshapeReshapex.reshape
转置x.transpose(...)tf.transposeTransposex.T
索引切片x[..., 0]x[..., 0]Slicex[..., 0]
Concatenatetorch.cattf.concatConcatnp.concatenate
堆叠torch.stacktf.stackUnsqueeze+Concatnp.stack
Reduce Sumx.sum(dim=0)tf.reduce_sumReduceSumx.sum(axis=0)
Reduce Meanx.mean(dim=0)tf.reduce_meanReduceMeanx.mean(axis=0)
SoftmaxF.softmaxtf.nn.softmaxSoftmax-
Conv2dF.conv2dtf.nn.conv2dConv-
MaxPoolF.max_pool2dtf.nn.max_poolMaxPool-
BatchNormF.batch_normtf.nn.batch_normalizationBatchNormalization-
DropoutF.dropouttf.nn.dropoutDropout-

7.2 常见语义陷阱 ​

python
print()
print("=" * 60)
print("跨框架语义陷阱汇总")
print("=" * 60)

print("""
陷阱1:Conv padding 模式
┌─────────────────────────────────────────────────────────────────┐
│  PyTorch: padding='valid' → 无 padding                          │
│  TensorFlow: padding='VALID' → 无 padding                       │
│                                                                 │
│  PyTorch: padding='same' → 输出尺寸 = ceil(input / stride)      │
│  TensorFlow: padding='SAME' → 输出尺寸 = ceil(input / stride)   │
│                                                                 │
│  ⚠️ 注意:TF 'same' 会自动计算 padding,PyTorch 需要手动           │
└─────────────────────────────────────────────────────────────────┘

陷阱2:Axis/Dim 的含义
┌─────────────────────────────────────────────────────────────────┐
│  PyTorch: dim=-1 表示最后一个维度                                │
│  NumPy:   axis=-1 表示最后一个维度                               │
│  ✓ 两者一致                                                      │
│                                                                 │
│  但 PyTorch 的 reduce 用 dim,NumPy 用 axis                       │
│  torch.sum(x, dim=0) vs x.sum(axis=0)                           │
└─────────────────────────────────────────────────────────────────┘

陷阱3:Broadcast 行为完全一致
┌─────────────────────────────────────────────────────────────────┐
│  PyTorch/NumPy: 从右向左广播                                      │
│  TensorFlow: 从右向左广播(相同)                                 │
│                                                                 │
│  ✅ 主流框架 broadcast 行为一致                                   │
│                                                                 │
│  但 PaddlePaddle 某些旧版本可能有差异                             │
└─────────────────────────────────────────────────────────────────┘

陷阱4:数据类型转换
┌─────────────────────────────────────────────────────────────────┐
│  PyTorch: tensor.float() → 转换为 float32                       │
│  TensorFlow: tf.cast(tensor, tf.float32) → 转换为 float32       │
│  ✓ API 不同但结果相同                                            │
│                                                                 │
│  ⚠️ 但精度可能不同:不同框架的浮点舍入可能略有差异                │
└─────────────────────────────────────────────────────────────────┘
""")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

升华 ​

┌──────────────────────────────────────────────────────────────────────────────┐
│  📚 算子语义:核心原则                                                       │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│  1. **同名算子可能不同义**:CrossEntropy 在不同框架有细微差异                    │
│                                                                              │
│  2. **默认值是陷阱来源**:未指定参数时框架有各自的默认值                          │
│                                                                              │
│  3. **Broadcast 从右向左**:NumPy/PyTorch/TF 一致,但小心维度对齐                 │
│                                                                              │
│  4. **Shape 推理有边界**:静态分析能发现形状问题,无法发现数值问题                │
│                                                                              │
└──────────────────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

"AI 可查 vs 必须理解"清单 ​

必须理解(不理解就等于不会):

  • 🔴 Broadcast 规则:从右对齐,自动补维,维度必须是 1 或相同
  • 🔴 Reduce 的 axis 语义:哪个维度被聚合,结果维度是多少
  • 🔴 Matmul vs 逐元素乘法:@ vs * 的区别和 broadcast 行为
  • 🔴 Conv2d 的 padding/stride/dilation:输出尺寸公式
  • 🔴 NCHW vs NHWC:不同框架的默认值和转换方法

AI 可查(知道去哪查就行):

  • ✅ 具体框架的 API 签名:文档中搜索具体函数
  • ✅ 特定算子的精确语义:ONNX 规范文档
  • ✅ 特定硬件的优化建议:NVIDIA/AMD 官方文档
  • ✅ 数据类型转换细节:具体函数的精度行为
  • ✅ 框架特定的操作:如 tf.function、torch.jit.script

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇5. MLIR 架构、方言与渐进式降级 / MLIR Architecture, Dialects, and Progressive Lowering
下一篇7. 模型前端格式:ONNX、TFLite、HLO 与 SavedModel / Model Frontend Formats: ONNX, TFLite, HLO, and SavedModel

持续记录,持续成长

Copyright © Tidenflow