Python 在 AI-Infra:从语言机制到生产交付

系列精华 · 七篇正文每篇一页,按 ↓ 看机制、数字与误区

arganzheng · 2026-01-30

这个系列的一句话主张

Python 的语法很容易,Python 工程不容易——难的不是怎么写,而是语法背后哪些机制在起作用、代价是什么、什么时候不该用。

链 篇
代码怎么运转 01 语言机制与运行时
怎么写得健壮 02 类型系统与数据契约
怎么协作 03 并发、异步与任务协作
怎么扩展 04 动态机制与插件
内存怎么优化 05 内存管理
怎么确认没问题 06 测试、定位与调试
怎么交付 07 工程化与生产交付

参照系是 Java;例子取自 PyTorch、vLLM、FastAPI、Pydantic。

七篇怎么连起来

%%{init: {"flowchart": {"wrappingWidth": 190}}}%%
flowchart TB
    P1["01 语言机制"] --> P2["02 类型系统"]
    P1 --> P3["03 并发与异步"]
    P1 --> P4["04 动态机制"]
    P1 --> P5["05 内存管理"]
    P3 & P4 & P5 --> P6["06 测试与调试"]
    P2 --> P7["07 工程化交付"]
    P3 --> P7
    P6 --> P7

01 · 语言机制:每行「简单」语法背后是一个可替换的协议

import 是运行时动作;直接调 forward 绕过全部 hook

机制 规则
属性查找 数据描述符 → 实例字典 → 非数据描述符 → __getattr__
sys.path[0] 由启动方式决定(脚本目录 / 当前目录 / 空)
sys.modules 每进程一份——spawn 的子进程要重新导入
super() MRO 里的下一个,不是父类;协作式多继承漏一层之后全被跳过
生成器 break 不等于 close(),finally 未必执行

02 · 类型系统:注解由谁消费

注解只是 __annotations__;Pydantic 在类创建时构建验证树

%%{init: {"flowchart": {"wrappingWidth": 210}}}%%
flowchart LR
    A["def f(x: int) -> str<br/>class User: id: int"] --> ANN["__annotations__<br/>解释器只存、不看"]
    ANN --> S["静态消费:mypy / pyright<br/>不运行代码"]
    ANN --> D["@dataclass<br/>只读字段名与顺序,exec 生成 __init__"]
    ANN --> P["Pydantic / FastAPI / beartype<br/>get_type_hints() 读出来,运行时校验"]
    classDef hot fill:#fde68a,stroke:#b45309
    class P hot

要点

get_type_hints() 比 __annotations__ 多做三件事

要点 细节
版本:Protocol 3.8、X &#124; Y 3.10、ParamSpec 3.10、Self 3.11 get_type_hints() 比 __annotations__ 多做三件事(解析字符串、合并 MRO、去 Optional)
@runtime_checkable 只查方法存在,不查签名  

常见误区

  • 「过了 mypy 就是被检查过了」——默认不检查没标注的函数体;check_untyped_defs 或 strict

03 · 并发:按瓶颈选模型,不按 API 流行度

线程管 I/O、进程绕 GIL、asyncio 管大量协作

%%{init: {"flowchart": {"wrappingWidth": 200}}}%%
flowchart TB
    Q{"瓶颈?"} -- "等 I/O" --> IO{"并发数?"}
    IO -- "几十个" --> TH["<b>线程</b>"]
    IO -- "几千连接" --> AS["<b>asyncio</b>"]
    Q -- "纯 Python 计算" --> PR["<b>进程</b>"]
    Q -- "矩阵 / 卷积" --> NAT["不用并发<br/>原生库已多线程"]
    TH & AS & PR --> C["组合:asyncio 收 → 线程跑阻塞 → 进程预处理"]

并发 vs 并行

并发与并行的时间线示意

并发是交错推进、并行是同时执行;GIL 让 Python 的并行只在多进程

数字与陷阱

量 数
线程池默认大小 \(\min(32, \text{cpu} + 4)\)
OS 线程栈 vs 协程 约 8 MB vs 几 KB
fork 只复制调用线程;3.14 起 Linux 默认 forkserver
/dev/shm 默认 64 MB——DataLoader worker 常撞
FastAPI def 端点 线程池 40

常见误区

  • 「Future.cancel() 能停掉运行中的线程」——线程不能取消、进程只能 kill、协程在下一个 await 点协作式取消
  • 「在协程里调 requests.get() 没问题」——事件循环整个停摆;用异步客户端或 asyncio.to_thread()

04 · 动态机制与插件:只在启动时「选择」,热路径必须静态

插件化 = 注册表 + 发现 + 契约 + 边界

操作 耗时 相对
obj.attr ≈ 10 ns 1×
getattr(obj, name) ≈ 23 ns 2.3×
inspect.signature ≈ 3700 ns 约 370×

要点

三种发现机制:越灵活失败定位越难

要点 细节
注册表为空 ⇔ 模块未被导入 第一篇的 import 副作用在这里兑现
用户输入能选名字、不能造名字(getattr(module, user_input) 是漏洞)  

05 · 内存管理:多数不是「泄漏」,是「被意外长期持有」

对象释放 ≠ RSS 下降——arena 要等整个 pool 空

机制 数
引用计数 归零立即释放;循环靠分代 GC,阈值 (700, 10, 10)
pymalloc 管 ≤ 512 字节;arena 256 KB / pool 4 KB
小整数缓存 −5 到 256
torch memory_reserved ≥ memory_allocated;差值是缓存分配器

要点

先分三类再找源——释放 ≠ RSS 下降

要点 细节
定位:先分三类(仍被引用 / 分配器保留 / 原生或设备持有)再找源 tracemalloc、gc.get_referrers、torch.cuda.memory_summary

常见误区

  • 「用 __del__ 释放 GPU 句柄」——时机不可控、可能在解释器关闭时才调;用 with 或 weakref.finalize

06 · 测试、定位与调试:按症状选工具

难在按症状选工具;raise from 与上下文日志是前提

症状 工具
协程没被等待 pytest -W error::RuntimeWarning;Mock 不能 await,用 AsyncMock
Mock 没生效 替换「被测模块里实际用的名字」,不是定义处
进程卡死 / 段错误 python -X faulthandler、py-spy dump
内存增长 PYTHONTRACEMALLOC=25 记录分配栈
动态调用看不到 inspect、sys.settrace、结构化日志

常见误区

  • 「在库里 logging.basicConfig()」——它配置 root logger,篡改了应用的全局配置;库只 getLogger(__name__),最多加 NullHandler

07 · 工程化与生产交付:Java 里框架强制的事交还给你

锁文件进 CI;GPU 服务每 GPU 一个 worker

量 数
pip install torch 2 GB+;环境 5–8 GB;镜像 8–12 GB
装 torch --index-url,不用 --extra-index-url(pip 在多索引里选最高版本,可能装到 PyPI 默认变体)
CUDA 三层 驱动 / runtime / 库;同大版本向前兼容
4 个 worker × 14 GB 模型 56 GB 显存——「多开 worker 提吞吐」的代价
tag 不能是 latest

常见误区

  • 「requirements.txt 全写 == 就可复现」——传递依赖仍浮动、无 hash;锁文件带 hash,--frozen 不做一致性检查

一条贯穿线:import → 注册表 → GIL → worker 数

%%{init: {"flowchart": {"wrappingWidth": 220}}}%%
flowchart TB
    A["import 是运行时动作(01)"] --> B["顶层副作用:注册 · CUDA 初始化(01)"]
    B --> C["注册表为空 ⇔ 模块未导入(04)"]
    C --> D["发现:显式导入 / 包扫描 / 入口点(04)"]
    A --> H["spawn 重导主模块 → __main__(03)"]
    E["GIL:同一刻一个线程跑字节码(03)"] --> F["多进程是唯一绕开(03)"]
    F --> H
    F --> G["每进程各自 sys.modules(03、05)"]
    G --> I["每 GPU 一个 worker(07)"]

常见误区

  • 「import 只是声明依赖」——它执行顶层代码
  • 「model.forward(x) 与 model(x) 一样」——直接调 forward 绕过全部 hook
  • 「super() 调父类」——调的是 MRO 里的下一个
  • 「类型注解在运行时生效」——只有消费者主动读取时才生效
  • 「@dataclass 会按注解校验」——User(id="abc") 静默通过
  • 「多线程能加速 CPU 计算」——GIL;纯 Python CPU 用进程
  • 「子类自动注册应该用元类」——__init_subclass__ 几乎总是更好
  • 「RSS 不降就是泄漏」——先分三类
  • 「装 torch 用 --extra-index-url」——用 --index-url
  • 「GPU 服务多开几个 worker 提吞吐」——每个 worker 一份模型进显存

七个出口

篇 一个判据 / 一个数
01 属性查找四步;super() = MRO 下一个;sys.modules 每进程一份
02 解释器只存 __annotations__;边界校验一次、内部零开销
03 线程池 min(32, cpu + 4);线程 8 MB / 协程几 KB;/dev/shm 64 MB
04 inspect.signature 370×;能选名字不能造名字
05 GC 阈值 (700, 10, 10);pymalloc ≤ 512 B、arena 256 KB
06 AsyncMock;-X faulthandler;PYTHONTRACEMALLOC=25
07 uv sync --locked;--index-url;每 GPU 一个 worker

下一步

  • 往下:《C++ 在 AI-Infra》——Python 之下的那一层;《PyTorch 深度实践》——这些机制在 PyTorch 里的形态(__setattr__ 注册、Dispatcher、DataLoader 进程)
  • 往上:《vLLM 源码》——asyncio 引擎循环、多进程 worker、插件注册表的真实案例;《AI 平台工程》——镜像与交付
  • 原文总纲:/python-for-ai-infra.html;通关自测在系列总结
← Arganzheng's Blog