📄 一句话概括
同一份代码在两个人机器上跑出两种结果,九成是依赖的版本不一样。 这一节把「装环境」拆成三道关:解依赖、锁版本、分层传。 每一道都有自己的数,不是玄学。
📝 本节和前面几节的关系
13.2 讲硬件的马力,这一节讲硬件上面那一层。机器配好只是开始,装不对照样跑不起来。 13.4 的平台会把这一节的结论直接用上:每个作业都要带上自己的镜像。
一、依赖是一张网
每个包都有好几个版本,包与包之间还有「我要至少某个版本」的要求:
| 包 | 可用版本 | 它要求 |
|---|---|---|
| 框架 | 1.0、2.0、2.3、3.0 | 数值库≥2.0、工具库≥20.0 |
| 图像库 | 8.0、9.0、10.0 | 数值库≥1.24 |
| 绘图库 | 3.5、3.7、3.8 | 数值库≥2.0、图像库≥9.0 |
| 工具库 | 20.0、21.0、22.0 | 日志库≥1.0 |
| 日志库 | 0.9、1.0、2.0 | 没有要求 |
| 六行单独看都很讲道理。可它们叠在一起,就成了一张必须同时满足的网。 | ||
| Demo 用最原始的回溯:从框架开始往下选,撞到不满足就退回上一层换版本。 | ||
| 一共只试了 7 次,就找到一组能同时满足的版本。 | ||
| 注意它挑的是「能满足的最新的」,不是最旧的。 | ||
| 能装、同时又不太老,这两件事它一步都做了。 |
二、972 种组合,只有 144 种能用
把六个包的所有版本列一遍:972 种组合。 满足全部约束的只有 144 种,占 14.8%——随便挑一组,八成半装不上。
| 谁的约束 | 要求 | 被这一条否掉的组合数 |
|---|---|---|
| 框架 | 数值库≥2.0 | 648 |
| 绘图库 | 图像库≥9.0 | 108 |
| 工具库 | 日志库≥1.0 | 72 |
| 光「框架要数值库≥2.0」这一条,就否掉了 648 种。 | ||
| 它一条不是错,三条叠起来就把选择砍掉了六分之五。 | ||
| 回溯试了 7 次,枚举是 972 种——差 139 倍。 | ||
| 这就是为什么依赖解析不能靠手挑:人算不过来,得让程序退回。 |
三、不锁版本,半年后装出另一套
假设每个包每月发一版,而且都保证「小版本不破坏兼容」。 同一份「框架≥2.0」的需求,六月装和十二月装:
| 包 | 六月装到 | 十二月装到 | 变化 |
|---|---|---|---|
| 框架 | 2.3 | 3.0 | 大版本跨了 |
| 数值库 | 1.24 | 2.0 | 大版本跨了 |
| 图像库 | 9.0 | 10.0 | 大版本跨了 |
| 绘图库 | 3.7 | 3.8 | 小版本 |
| 工具库 | 21.0 | 22.0 | 大版本跨了 |
| 日志库 | 1.0 | 2.0 | 大版本跨了 |
| 六个包里有 5 个跨了大版本。 | |||
| 「小版本不破坏兼容」这句话,在大版本面前不成立。 | |||
| 同一份需求文件、同一台机器,隔半年装出来是两套东西。 | |||
| 「在我机器上能跑」这句话,一半是这么来的。 | |||
| 治法只有一个:把解出来的那组版本连小版本号一起写死,存进文件。 | |||
| 这不是洁癖,是让别人能复现你的结果的最低要求。 |
四、镜像分层与版本链
环境不光要装对,还要能整包搬走。搬的办法是打包成镜像,分层存。
| 层 | 大小 | 多久变一次 |
|---|---|---|
| 基础系统 | 900 MB | 几乎不变 |
| 系统依赖库 | 420 MB | 很少变 |
| Python 依赖 | 610 MB | 偶尔变 |
| 项目代码 | 5 MB | 天天变 |
| 配置文件 | 1 KB | 天天变 |
| 整个镜像 1935 MB。改一行代码,要重新传多少? |
| 做法 | 每次要传 | 一天改二十次 |
|---|---|---|
| 分层(按变化频率排) | 5.001 MB | 100 MB |
| 全塞一层 | 1935 MB | 38700 MB |
| 差 387 倍。分层的规矩只有一条:变得慢的放前面,变得快的放后面。 | ||
| 写反了——先把代码拷进去再装依赖——改一行代码,装依赖那一层也得重来。 | ||
| 这是最常见的镜像写坏法。 | ||
| 最后是版本链。深度学习环境最容易出问题的,是驱动、运行时、框架三者错位: |
| 组合 | 驱动要的最低版本 | 结果 |
|---|---|---|
| 470 驱动 + 11.8 运行时 | 520 | 驱动太旧,起不来 |
| 520 驱动 + 11.8 运行时 | 520 | 能用 |
| 535 驱动 + 11.8 运行时 | 520 | 能用,向下兼容 |
| 520 驱动 + 12.2 运行时 | 525 | 运行时太新,起不来 |
| 规律是:驱动版本必须大于等于运行时要求的最低版本,高可以,低不行。 | ||
| 而框架的编译版本又要求运行时匹配——所以三个箭头是单向的: | ||
| 驱动 ≥ 运行时 ≥ 框架编译版本。 |
五、动手 Demo:装得上不等于装得对
五段:依赖求解、枚举全部组合、不锁版本的后果、镜像分层、工具链版本链。 没有一个包是装上去的,全部是自己写的版本表。 这样「为什么退回」这件事能被看清,而不是被包管理器一句报错带走。
PKGS = {u"框架": [u"1.0", u"2.0", u"2.3", u"3.0"],
u"图像库": [u"8.0", u"9.0", u"10.0"],
u"数值库": [u"1.24", u"2.0"]}
NEED = {(u"框架", u"数值库"): u"2.0"} # 包之间「我要至少某个版本」
def solve(i, pick): # 回溯:逐层往下选,撞墙就退回
if i == len(ORDER):
return dict(pick)
for v in PKGS[ORDER[i]]:
pick[ORDER[i]] = v
if not violates(pick):
r = solve(i + 1, pick)
if r:
return r
pick.pop(ORDER[i], None)
return None
实跑输出:
--------------------------------------------------------------------
装得上不等于装得对
--------------------------------------------------------------------
==============================================================
[1] 依赖求解:六个包,各自要求不同的版本
每个包有几个版本,包之间有「我要至少某个版本」的要求:
包 可用版本 它要求
框架 1.0、2.0、2.3、3.0 数值库≥2.0、工具库≥20.0
图像库 8.0、9.0、10.0 数值库≥1.24
绘图库 3.5、3.7、3.8 数值库≥2.0、图像库≥9.0
工具库 20.0、21.0、22.0 日志库≥1.0
日志库 0.9、1.0、2.0 —
下面用最原始的回溯:从「框架」开始,逐层往下选,撞到约束不满足就退回换个版本。
包 选中的版本
框架 3.0
图像库 10.0
绘图库 3.8
数值库 2.0
工具库 22.0
日志库 2.0
一共试了 7 次,找到一组能同时满足的版本。
**它挑的是「能满足的最新的」版本,试错次数远少于组合总数。**
**因为回溯会整段退回:某一层没得选,就把上一层换个版本重来。**
**这就比「把所有组合都列一遍」快得多——第 2 段会量这个差别。**
[2] 把所有组合枚举一遍,看有多少真的能用
六个包的所有版本组合,一共 972 种。
其中满足全部约束的只有 144 种,占 14.8%。
**也就是说,随便挑一组,972 次里有 828 次装不上。**
最常见的几种「卡在哪」:
谁的约束 要求 被这一条否掉的组合数
框架 数值库≥2.0 648
绘图库 图像库≥9.0 108
工具库 日志库≥1.0 72
**回溯那次只试了 7 次,这里是 972 种组合——差 139 倍。**
**每一条约束单独看都很讲道理,叠在一起就排除了八成半。**
**这就是为什么依赖解析不能靠手挑——人算不过来,得让程序回溯。**
[3] 不锁版本会怎样:六个月后装出另一套
假设每个包每月发一版,而且都保证「小版本不破坏兼容」。
同一份「框架≥2.0」的需求,六月装和十二月装:
包 六月装到的 十二月装到的 变化
框架 2.3 3.0 大版本跨了
数值库 1.24 2.0 大版本跨了
图像库 9.0 10.0 大版本跨了
绘图库 3.7 3.8 小版本
工具库 21.0 22.0 大版本跨了
日志库 1.0 2.0 大版本跨了
**六个包里有 5 个跨了大版本。**
**「小版本不破坏兼容」这句话,在大版本面前不成立。**
**所以同一份需求文件、同一台机器、隔半年装出来的是两套东西。**
**「在我机器上能跑」这句话,一半是这么来的。**
[4] 容器镜像:为什么要分层
层 大小 多久变一次
基础系统 900 MB 几乎不变
系统依赖库 420 MB 很少变
Python 依赖 610 MB 偶尔变
项目代码 5 MB 天天变
配置文件 1 KB 天天变
整个镜像 1935 MB。改一次代码要重新传多少?
做法 每次要传 一天改二十次
分层(按变化频率排) 5.001 MB 100 MB
全塞一层 1935 MB 38700 MB
**差 387 倍。**
**分层的规矩只有一条:变得慢的放前面,变得快的放后面。**
**写反了——比如先把代码拷进去再装依赖——**
**改一行代码,装依赖那一层也得重来。这是最常见的镜像写坏法。**
[5] 工具链:三个版本号必须能对上
深度学习环境最容易出问题的地方,是驱动、运行时、框架三者版本错位。
组合 驱动要的最低版本 结果
470 驱动 + 11.8 运行时 520 驱动太旧,起不来(驱动 470)
520 驱动 + 11.8 运行时 520 能用(驱动 520)
535 驱动 + 11.8 运行时 520 能用(向下兼容)(驱动 535)
520 驱动 + 12.2 运行时 525 运行时太新,起不来(驱动 520)
**规律是:驱动版本必须大于等于运行时要求的最低版本,高可以,低不行。**
**而框架的编译版本又要求运行时匹配——所以三者的箭头是单向的:**
**驱动 ≥ 运行时 ≥ 框架编译版本。**
五段连起来:**依赖之间是「互相要求版本」的网,一处不合整条退回**;
**多一个要求就可能无解,所以要把解出来的一组版本锁死**;
**不锁,半年后装出来的就是另一套**;
**镜像分层按「变化快慢」排序,能少传几百倍**;
**而驱动、运行时、框架这三层版本,只能从下往上兼容。**
第 1 段那个「7 次」值得盯住。回溯不是挨个试,它一整段地退回——某一层没得选, 就把上一层换个版本重来。 第 2 段把 972 和 7 摆在一起,是这一节最有说服力的地方。 人手工挑版本,挑的是那 14.8% 里的一个;程序挑的是「第一个能过的」。 第 3 段那张表里唯一没跨大版本的是绘图库。 这不是巧合——包越活跃,大版本蹦得越快,跨版本的概率越高。 第 4 段 387 倍这个数,是「按层存」和「一坨存」的差别。 注意最后一层只有 1 KB:配置改得最勤,却几乎不占空间,所以它放最后。
30 秒实验
- 把第 1 段的「工具库≥20.0」这条要求删掉,重跑。 能用的组合会明显变多——约束少一条,可用空间就大一圈,这也解释了为什么老项目反而好装。
- 把第 3 段的「六个月」改成「二十四个月」,重跑。 所有包都会跨大版本——时间越长,「同一份需求」这个词越没有意义。
- 把第 4 段的分层顺序倒过来(代码先放、依赖后装),重跑。 每次要传的量会跳到接近整个镜像——缓存失效的代价,就是每次从零开始。
常见误区
- 以为「装不上是网络问题」。八成是版本冲突。 把报错里的「requires」逐条念一遍,冲突点自己会浮出来。
- 以为「锁版本会导致装不到新东西」。锁的是「这一套能跑的版本」, 升级是另一件事:升完跑测试,过了再锁新的一组。
- 以为「容器解决了环境问题」。容器只保证「原样搬过去」, 它不保证「装对了」。第一层写错,整个镜像一起错。
- 以为「驱动越新越好」。能用就行。 新驱动配旧运行时是安全的,旧驱动配新运行时直接起不来——箭头只有一个方向。
自测三问(点开对答案)
1. 为什么依赖求解要用回溯,不能一次挑定? —— 因为**每一条约束都可能否决前面的选择**。 **Demo 里 972 种组合只有 144 种能用,一次挑定撞墙的概率是八成半。** **回溯的价值在于「整段退回」:某一层没得选,就换上一层。** 2. 不锁版本会怎样? —— **同一份需求文件,隔半年装出来是另一套。** **Demo 第 3 段:六个包里有 5 个跨了大版本。** **结果就是「在我机器上能跑」——这句话说的是环境,不是代码。** 3. 镜像分层为什么能省几百倍? —— 因为**只重传变的那几层**。**Demo 第 4 段:分层每次传 5.001 MB,全塞一层要传 1935 MB。** **前提是顺序对:变得慢的放前面,变得快的放后面。**算力、硬件、软件三样都齐了,最后一节把它们放进同一个池子里分。 四十个作业抢八张卡——这一节讲排队,也讲谁会饿死。
- ☐ (本节完,下一节 13.4 机器学习平台和深度学习平台)