阅读视图

发现新文章,点击刷新页面。

GPU 进阶笔记(四):NVIDIA GH200 芯片、服务器及集群组网(2024)

记录一些平时接触到的 GPU 知识。由于是笔记而非教程,因此内容不求连贯,有基础的同学可作查漏补缺之用。

水平及维护精力所限,文中不免存在错误或过时之处,请酌情参考。 传播知识,尊重劳动,年满十八周岁,转载请注明出处



1 传统原厂 GPU 服务器:Intel/AMD x86 CPU + NVIDIA GPU

2024 之前,不管是 NVIDIA 原厂还是第三方服务器厂商的 NVIDIA GPU 机器,都是以 x86 CPU 机器为底座, GPU 以 PCIe 板卡或 8 卡模组的方式连接到主板上,我们在第一篇中有过详细介绍,

典型 8 卡 A100 主机硬件拓扑

这时 CPU 和 GPU 是独立的,服务器厂商只要买 GPU 模组(例如 8*A100),都可以自己组装服务器。 至于 Intel/AMD CPU 用哪家,就看性能、成本或性价比考虑了。

2 新一代原厂 GPU 服务器:NVIDIA CPU + NVIDIA GPU

随着 2024 年 NVIDIA GH200 芯片的问世,NVIDIA 的 GPU 开始自带 CPU 了。

  • 桌面计算机时代:CPU 为主,GPU(显卡)为辅,CPU 芯片中可以集成一块 GPU 芯片, 叫集成显卡
  • AI 数据中心时代:GPU 反客为主,CPU 退居次席,GPU 芯片/板卡中集成 CPU。

所以 NVIDIA 集成度越来越高,开始提供整机或整机柜。

2.1 CPU 芯片:Grace (ARM)

基于 ARMv9 设计。

2.2 GPU 芯片:Hopper/Blackwell/…

比如 Hopper 系列,先出的 H100-80GB,后面继续迭代:

  1. H800:H100 的阉割版,
  2. H200:H100 的升级版,
  3. H20:H200 的阉割版,比 H800 还差,差多了。

算力对比:GPU Performance (Data Sheets) Quick Reference (2023)

2.3 芯片产品(命名)举例

2.3.1 Grace CPU + Hopper 200 (H200) GPU:GH200

一张板子:

NVIDIA GH200 芯片(板卡)渲染图。左:Grace CPU 芯片;右:Hopper GPU 芯片 [2]

2.3.2 Grace CPU + Blackwell 200 (B200) GPU:GB200

一个板子(模块),功耗太大,自带液冷:

NVIDIA GB200 渲染图,一个模块包括 2 Grace CPU + 4 B200 GPU,另外自带了液冷模块。 [3]

72 张 B200 组成一个原厂机柜 NVL72:

NVIDIA GB200 NVL72 机柜。 [3]

3 GH200 服务器内部设计

3.1 GH200 芯片逻辑图:CPU+GPU+RAM+VRAM 集成到单颗芯片

NVIDIA GH200 芯片(单颗)逻辑图。[2]

3.1.1 核心硬件

如上图所示,一颗 GH200 超级芯片集成了下面这些核心部件:

  1. 一颗 NVIDIA Grace CPU;
  2. 一颗 NVIDIA H200 GPU;
  3. 最多 480GB CPU 内存;
  4. 96GB 或 144GB GPU 显存。

3.1.2 芯片硬件互连

  1. CPU 通过 4 个 PCIe Gen5 x16 连接到主板,

    • 单个 PCIe Gen5 x16 的速度是双向 128GB/s,
    • 所以 4 个的总速度是 512GB/s;
  2. CPU 和 GPU 之间,通过 NVLink® Chip-2-Chip (NVLink-C2C) 技术互连,

    • 900 GB/s,比 PCIe Gen5 x16 的速度快 7 倍;
  3. GPU 互连(同主机扩跨主机):18x NVLINK4

    • 900 GB/s

NVLink-C2C 提供了一种 NVIDIA 所谓的“memory coherency”:内存/显存一致性。好处:

  • 内存+显存高达 624GB,对用户来说是统一的,可以不区分的使用;提升开发者效率;
  • CPU 和 GPU 可以同时(concurrently and transparently)访问 CPU 和 GPU 内存。
  • GPU 显存可以超分(oversubscribe),不够了就用 CPU 的内存,互连带宽够大,延迟很低。

下面再展开看看 CPU、内存、GPU 等等硬件。

3.2 CPU 和内存

3.2.1 72-core ARMv9 CPU

  • 72-core Grace CPU (Neoverse V2 Armv9 core)

3.2.2 480GB LPDDR5X (Low-Power DDR) 内存

  • 最大支持 480GB LPDDR5X 内存;
  • 500GB/s per-CPU memory bandwidth。

参考下这个速度在存储领域的位置:

Fig. Peak bandwidth of storage media, networking, and distributed storage solutions. [1]

3.2.3 三种内存对比:DDR vs. LPDDR vs. HBM

  • 普通服务器(绝大部分服务器)用的是 DDR 内存,通过主板上的 DIMM 插槽连接到 CPU,[1] 中有详细介绍;
  • 1-4 代的 LPDDR 是对应的 1-4 代 DDR 的低功耗版,常用于手机等设备。
    • LPDDR5 是独立于 DDR5 设计的,甚至比 DDR5 投产还早;
    • 直接和 CPU 焊到一起的,不可插拔,不可扩展,成本更高,但好处是速度更快
    • 还有个类似的是 GDDR,例如 RTX 4090 用的 GDDR。
  • HBM 在第一篇中已经介绍过了;

下面列个表格对比三种内存的优缺点,注意其中的高/中/低都是三者相对而言的:

  DDR LPDDR HBM
容量
速度
带宽
可扩展性
可插拔 不可 不可
成本
功耗

更多细节,见 [1]。

例如,与 8-channel DDR5(目前高端 x86 服务器的配置)相比, GH200 的 LPDDR5X 内存带宽高 53%,功耗还低 1/8

3.3 GPU 和显存

3.3.1 H200 GPU

算力见下面。

3.3.2 显存选配

支持两种显存,二选一:

  • 96GB HBM3
  • 144GB HBM3e,4.9TB/s,比 H100 SXM 的带宽高 50%;

3.4 变种:GH200 NVL2,用 NVLINK 全连接两颗 GH200

在一张板子内放两颗 GH200 芯片,CPU/GPU/RAM/VRAM 等等都翻倍,而且两颗芯片之间是全连接。

例如,对于一台能插 8 张板卡的服务器,

  • 用 GH200 芯片:CPU 和 GPU 数量 8 * {72 Grace CPU, 1 H200 GPU}
  • 用 GH200 NVL2 变种:CPU 和 GPU 数量 8 * {144 Grace CPU, 2 H200 GPU}

3.5 GH200 & GH200 NVL2 产品参数(算力)

NVIDIA GH200 产品参数。上半部分是 CPU、内存等参数,从 "FP64" 往下是 GPU 参数。[2]

4 GH200 服务器及组网

两种服务器规格,分别对应 PCIe 板卡和 NVLINK 板卡。

4.1 NVIDIA MGX with GH200:原厂主机及组网

下图是单卡 node 的一种组网方式:

NVIDIA GH200 MGX 服务器组网。每台 node 只有一片 GH200 芯片,作为 PCIe 板卡,没有 NVLINK。[2]

  1. 每台 node 只有一片 GH200 芯片(所以只有一个 GPU),作为 PCIe 板卡,没有 NVLINK;
  2. 每台 node 的网卡或加速卡 BlueField-3 (BF3) DPUs 连接到交换机;
  3. 跨 node 的 GPU 之间没有直连,而是通过主机网络(走 GPU->CPU-->NIC 出去)的方式实现通信;
  4. 适合 HPC workload、中小规模的 AI workload。

4.2 NVIDIA GH200 NVL32:原厂 32 卡机柜

通过 NVLINk 将 32 个 GH200 芯片全连接为一个逻辑 GPU 模块,所以叫 NVL32

NVIDIA GH200 NVL32 组网。[2]

  1. NVL32 模块实际形态是一个机柜
    • 一个机柜能提供 19.5TB 内存+显存;
    • NVLink TLB 能让任意一个 GPU 访问这个机柜内的任意内存/显存;

      NVIDIA GH200 NVL32 中 3 种内存/显存访问方式。[2]

    • Extended GPU Memory (EGM)
  2. 多个机柜再通过网络互连,形成集群,适合超大规模 AI workload。

5 总结

本文粗浅地整理了一些 NVIDIA GH200 相关技术知识。

其他:

参考资料

  1. Practical Storage Hierarchy and Performance: From HDDs to On-chip Caches(2024)
  2. NVIDIA GH200 Grace Hopper Superchip & Architecture, datasheet, 2024
  3. NVIDIA GB200 NVL72 Delivers Trillion-Parameter LLM Training and Real-Time Inference, 2024

Written by Human, Not by AI Written by Human, Not by AI

Practical Storage Hierarchy and Performance: From HDDs to On-chip Caches(2024)

This post summarizes bandwidths for local storage media, networking infra, as well as remote storage systems. Readers may find this helpful when identifying bottlenecks in IO-intensive applications (e.g. AI training and LLM inference).

Fig. Peak bandwidth of storage media, networking, and distributed storage solutions.

Note: this post may contain inaccurate and/or stale information.



1 Fundamentals

Before delving into the specifics of storage, let’s first go through some fundamentals about data transfer protocols.

1.1 SATA

From wikepedia SATA:

SATA (Serial AT Attachment) is a computer bus interface that connects host bus adapters to mass storage devices such as hard disk drives, optical drives, and solid-state drives.

1.1.2 Real world pictures

Fig. SATA interfaces and cables on a computer motherboard. Image source wikipedia

1.1.1 Revisions and data rates

The SATA standard has evolved through multiple revisions. The current prevalent revision is 3.0, offering a maximum IO bandwidth of 600MB/s:

Table: SATA revisions. Data source: wikipedia

Spec Raw data rate Data rate Max cable length
SATA Express 16 Gbit/s 1.97 GB/s 1m
SATA revision 3.0 6 Gbit/s 600 MB/s 1m
SATA revision 2.0 3 Gbit/s 300 MB/s 1m
SATA revision 1.0 1.5 Gbit/s 150 MB/s 1m

1.2 PCIe

From wikipedia PCIe (PCI Express):

PCI Express is high-speed serial computer expansion bus standard.

PCIe (Peripheral Component Interconnect Express) is another kind of system bus, designed to connect a variety of peripheral devices, including GPUs, NICs, sound cards, and certain storage devices.

1.1.2 Real world pictures

Fig. Various slots on a computer motherboard, from top to bottom:
PCIe x4 (e.g. for NVME SSD)
PCIe x16 (e.g. for GPU card)
PCIe x1
PCIe x16
Conventional PCI (32-bit, 5 V)
Image source wikipedia

As shown in the above picture, PCIe electrical interface is measured by the number of lanes. A lane is a single data send+receive line, functioning similarly to a “one-lane road” with traffic in both directions.

1.2.2 Generations and data rates

Each new PCIe generation doubles the bandwidth of a lane than the previous generation:

Table: PCIe Unidirectional Bandwidth. Data source: trentonsystems.com

Generation Year of Release Data Transfer Rate Bandwidth x1 Bandwidth x16
PCIe 1.0 2003 2.5 GT/s 250 MB/s 4.0 GB/s
PCIe 2.0 2007 5.0 GT/s 500 MB/s 8.0 GB/s
PCIe 3.0 2010 8.0 GT/s 1 GB/s 16 GB/s
PCIe 4.0 2017 16 GT/s 2 GB/s 32 GB/s
PCIe 5.0 2019 32 GT/s 4 GB/s 64 GB/s
PCIe 6.0 2021 64 GT/s 8 GB/s 128 GB/s

Currently, the most widely used generations are Gen4 and Gen5.

Note: Depending on the document you’re referencing, PCIe bandwidth may be presented as either unidirectional or bidirectional, with the latter indicating a bandwidth that is twice that of the former.

1.3 Summary

With the above knowledge, we can now proceed to discuss the performance characteristics of various storage devices.

2 Disk

2.1 HDD: ~200 MB/s

From wikipedia HDD:

A hard disk drive (HDD) is an electro-mechanical data storage device that stores and retrieves digital data using magnetic storage with one or more rigid rapidly rotating platters coated with magnetic material.

2.1.1 Real world pictures

A real-world picture is shown below:

Fig. Internals of a real world HDD. Image source hardwaresecrets.com

2.1.2 Supported interfaces (bus types)

HDDs connect to a motherboard over one of several bus types, such as,

  • SATA
  • SCSI
  • Serial Attached SCSI (SAS)

Below is a SATA HDD:

Fig. A real world SATA HDD. Image source hardwaresecrets.com

and how an HDD connects to a computer motherboard via SATA cables:

Fig. An HDD with SATA cables. Data source datalab247.com

2.1.3 Bandwidth: constrained by machanical factors

HDDs are machanical devices, and their peak IO performance is inherently limited by various mechanical factors, including the speed at which the actuator arm can function. The current upper limit of HDDs is ~200MB/s, which is significantly below the saturation point of a SATA 3.0 interface (600MB/s).

2.1.4 Typical latencies

Table. Latency characteristics typical of HDDs. Data source: wikipedia

Rotational speed (rpm) Average rotational latency (ms)
15,000 2
10,000 3
7,200 4.16
5,400 5.55
4,800 6.25

2.2 SATA SSD: ~600MB/s

What’s a SSD? From wikipedia SSD:

A solid-state drive (SSD) is a solid-state storage device. It provides persistent data storage using no moving parts.

Like HDDs, SSDs support several kind of bus types:

  • SATA
  • PCIe (NVME)

Let’s see the first one: SATA-interfaced SSD, or SATA SSD for short.

2.2.1 Real world pictures

SSDs are usually smaller than HDDs,

Fig. Size of different drives, left to right: HDD, SATA SSD, NVME SSD. Image source avg.com

2.2.2 Bandwidth: constrained by SATA bus

The absence of mechanical components (such as rotational arms) allows SATA SSDs to fully utilize the capabilities of the SATA bus. This results in an upper limit of 600MB/s IO bandwidth, which is 3x faster than that of SATA HDDs.

2.3 NVME SSD: ~7GB/s, ~13GB/s

Let’s now explore another type of SSD: the PCIe-based NVME SSD.

2.3.1 Real world pictures

NVME SSDs are even smaller than SATA SSDs, and they connect directly to the PCIe bus with 4x lanes instead of SATA cables,

Fig. Size of different drives, left to right: HDD, SATA SSD, NVME SSD. Image source avg.com

2.3.2 Bandwidth: contrained by PCIe bus

NVME SSDs has a peak bandwidth of 7.5GB/s over PCIe Gen4, and ~13GB/s over PCIe Gen5.

2.4 Summary

We illustrate the peak bandwidths of afore-mentioned three kinds of local storage media in a graph:

Fig. Peak bandwidths of different storage media.

These (HDDs, SSDs) are commonly called non-volatile or persistent storage media. And as the picture hints, in next chapters we’ll delve into some other kinds of storage devices.

3 DDR SDRAM (CPU Memory): ~400GB/s

DDR SDRAM nowadays serves mainly as the main memory in computers.

3.1 Real world pictures

Fig. Front and back of a DDR RAM module for desktop PCs (DIMM). Image source wikipedia

Fig. Corsair DDR-400 memory with heat spreaders. Image source wikipedia

DDR memory connects to the motherboard via DIMM slots:

Fig. Three SDRAM DIMM slots on a ABIT BP6 computer motherboard. Image source wikipedia

3.2 Bandwidth: contrained by memory clock, bus width, channel, etc

Single channel bandwidth:

  Transfer rate Bandwidth
DDR4 3.2GT/s 25.6 GB/s
DDR5 4–8GT/s 32–64 GB/s

if Multi-channel memory architecture is enabled, the peak (aggreated) bandwidth will be increased by multiple times:

Fig. Dual-channel memory slots, color-coded orange and yellow for this particular motherboard. Image source wikipedia

Such as [4],

  • Intel Xeon Gen5: up to 8 memory-channels running at up to 5600MT/s (358GB/s)
  • Intel Xeon Gen4: up to 8 memory-channels running at up to 4800MT/s (307GB/s)

3.3 Summary

DDR5 bandwidth in the hierarchy:

Fig. Peak bandwidths of different storage media.

4 GDDR SDRAM (GPU Memory): ~1000GB/s

Now let’s see another variant of DDR, commonly used in graphics cards (GPUs).

4.1 GDDR vs. DDR

From wikipedia GDDR SDRAM:

Graphics DDR SDRAM (GDDR SDRAM) is a type of synchronous dynamic random-access memory (SDRAM) specifically designed for applications requiring high bandwidth, e.g. graphics processing units (GPUs).

GDDR SDRAM is distinct from the more widely known types of DDR SDRAM, such as DDR4 and DDR5, although they share some of the same features—including double data rate (DDR) data transfers.

4.2 Real world pictures

Fig. Hynix GDDR SDRAM. Image Source: wikipedia

4.3 Bandwidth: contrained by lanes & clock rates

Unlike DDR, GDDR is directly integrated with GPU devices, bypassing the need for pluggable PCIe slots. This integration liberates GDDR from the bandwidth limitations imposed by the PCIe bus. Such as,

  • GDDR6: 1008GB/s. Peak per-pin data rate 16Gb/s, max memory bus width 384-bits.
  • GDDR6x: 1008GB/s, used by NVIDIA RTX 4090

4.4 Summary

With GDDR included:

Fig. Peak bandwidths of different storage media.

5 HBM: 1~5 TB/s

If you’d like to achieve even more higher bandwidth than GDDR, then there is an option: HBM (High Bandwidth Memory).

A great innovation but a terrible name.

5.1 What’s new

HBM is designed to provide a larger memory bus width than GDDR, resulting in larger data transfer rates.

Fig. Cut through a graphics card that uses HBM. Image Source: wikipedia

HBM sits inside the GPU die and is stacked – for example NVIDIA A800 GPU has 5 stacks of 8 HBM DRAM dies (8-Hi) each with two 512-bit channels per die, resulting in a total width of 5120-bits (5 active stacks * 2 channels * 512 bits) [3].

As another example, HBM3 (used in NVIDIA H100) also has a 5120-bit bus, and 3.35TB/s memory bandwidth,

Fig. Bandwidth of several HBM-powered GPUs from NVIDIA. Image source: nvidia.com

5.2 Real world pictures

The 4 squares in left and right are just HBM chips:

Fig. AMD Fiji, the first GPU to use HBM. Image Source: wikipedia

5.3 Bandwidth: contrained by lanes & clock rates

From wikipedia HBM

  Bandwidth Year GPU
HBM 128GB/s/package    
HBM2 256GB/s/package 2016 V100
HBM2e ~450GB/s 2018 A100, ~2TB/s; Huawei Ascend 910B
HBM3 600GB/s/site 2020 H100, 3.35TB/s
HBM3e ~1TB/s 2023 H200, 4.8TB/s

5.4 HBM-powered CPUs

HBM is not exclusive to GPU memory; it is also integrated into some CPU models, such as the Intel Xeon CPU Max Series.

5.5 Summary

This chapter concludes our exploration of dynamic RAM technologies, which includes

  • DDR DRAM
  • GDDR DRAM
  • HBM DRAM

Fig. Peak bandwidths of different storage media.

In the next, let’s see some on-chip static RAMs.

6 SRAM (on-chip): 20+ TB/s

The term “on-chip” in this post refers to memory storage that's integrated within the same silicon as the processor unit.

6.1 SRAM vs. DRAM

From wikipedia SRAM:

Static random-access memory (static RAM or SRAM) is a type of random-access memory that uses latching circuitry (flip-flop) to store each bit. SRAM is volatile memory; data is lost when power is removed.

The term static differentiates SRAM from DRAM:

  SRAM DRAM
data freshness stable in the presence of power decays in seconds, must be periodically refreshed
speed (relative) fast (10x) slow
cost (relative) high low
mainly used for cache main memory

SRAM requires more transistors per bit to implement, so it is less dense and more expensive than DRAM and also has a higher power consumption during read or write access. The power consumption of SRAM varies widely depending on how frequently it is accessed.

6.2 Cache hierarchy (L1/L2/L3/…)

In the architecture of multi-processor (CPU/GPU/…) systems, a multi-tiered static cache structure is usually used:

  • L1 cache: typically exclusive to each individual processor;
  • L2 cache: commonly accessible by a group of processors.

NVIDIA H100 chip layout (L2 cache in the middle, shared by many SM processors). Image source: nvidia.com

6.3 Groq LPU: eliminating memory bottleneck by using SRAM as main memory

From the official website: Groq is the AI infra company that builds the world’s fastest AI inference technology with both software and hardware. Groq LPU is designed to overcome two LLM bottlenecks: compute density and memory bandwidth.

  • An LPU has greater compute capacity than a GPU and CPU in regards to LLMs. This reduces the amount of time per word calculated, allowing sequences of text to be generated much faster.
  • Eliminating external memory bottlenecks (using on-chip SRAM instead) enables the LPU Inference Engine to deliver orders of magnitude better performance on LLMs compared to GPUs.

Regarding to the chip:

Fig. Die photo of 14nm ASIC implementation of the Groq TSP. Image source: groq paper [2]

The East and West hemisphere of on-chip memory module (MEM)

  • Composed of 44 parallel slices of SRAM and provides the memory concurrency necessary to fully utilize the 32 streams in each direction.
  • Each slice provides 13-bits of physical addressing of 16-byte memory words, each byte maps to a lane, for a total of 220 MiBytes of on-chip SRAM.

6.4 Bandwidth: contrained by clock rates, etc

6.5 Summary

This chapter ends our journey to various physical storage media, from machanical devices like HDDs all the way to on-chip cache. We illustrate their peak bandwidth in a picture, note that the Y-axis is log10 scaled:

Fig. Speeds of different storage media.

These are the maximum IO bandwidths when performing read/write operations on a local node.

Conversely, when considering remote I/O operations, such as those involved in distributed storage systems like Ceph, AWS S3, or NAS, a new bottleneck emerges: networking bandwidth.

7 Networking bandwidth: 400GB/s

7.1 Traditional data center: 2*{25,100,200}Gbps

For traditional data center workloads, the following per-server networking configurations are typically sufficient:

  • 2 NICs * 25Gbps/NIC, providing up to 6.25GB/s unidirectional bandwidth when operating in active-active mode;
  • 2 NICs * 100Gbps/NIC, delivering up to 25GB/s unidirectional bandwidth when operating in active-active mode;
  • 2 NICs * 200Gbps/NIC, achieving up to 50GB/s unidirectional bandwidth when operating in active-active mode.

7.2 AI data center: GPU-interconnect: 8*{100,400}Gbps

This type of networking facilitates inter-GPU communication and is not intended for general data I/O. The data transfer pathway is as follows:

            HBM <---> NIC <---> IB/RoCE <---> NIC <--> HBM
                Node1                            Node2

7.3 Networking bandwidths

Now we add networking bandwidths into our storage performance picture:

Fig. Speeds of different storage media, with networking bandwidth added.

7.4 Summary

If remote storage solutions (such as distributed file systems) is involved, and networking is fast enough, IO bottleneck would shift down to the remote storage solutions, that’s why there are some extremely high performance storage solutions dedicated for today’s AI trainings.

8 Distributed storage: aggregated 2+ TB/s

8.1 AlibabaCloud CPFS

AlibabaCloud’s Cloud Parallel File Storage (CPFS) is an exemplar of such high-performance storage solutions. It claims to offer up to 2TB/s of aggregated bandwidth.

But, note that the mentioned bandwidth is an aggregate across multiple nodes, no single node can achieve this level of IO speed. You can do some calcuatations to understand why, with PCIe bandwidth, networking bandwidth, etc;

8.2 NVME SSD powered Ceph clusters

An open-source counterpart is Ceph, which also delivers impressive results. For instance, with a cluster configuration of 68 nodes * 2 * 100Gbps/node, a user achieved aggregated throughput of 1TB/s, as documented.

8.3 Summary

Now adding distributed storage aggregated bandwidth into our graph:

Fig. Peak bandwidth of storage media, networking, and distributed storage solutions.

9 Conclusion

This post compiles bandwidth data for local storage media, networking infrastructure, and remote storage systems. With this information as reference, readers can evaluate the potential IO bottlenecks of their systems more effectively, such as GPU server IO bottleneck analysis [1]:

Fig. Bandwidths inside a 8xA100 GPU node

References

  1. Notes on High-end GPU Servers (in Chinese), 2023
  2. Think Fast: A Tensor Streaming Processor (TSP) for Accelerating Deep Learning Workloads, ISCA paper, 2020
  3. GDDR6 vs HBM - Defining GPU Memory Types, 2024
  4. 5th Generation Intel® Xeon® Scalable Processors, intel.com

Written by Human, Not by AI Written by Human, Not by AI

[译] Meta/Facebook 超大规模 AI/GPU 基础设施设计(2024)

本文翻译自 2024 年 Meta/Facebook 的一篇文章: Building Meta’s GenAI Infrastructure

  1. 两个 GPU 集群,每个集群 2.4w H100,分别用 RoCE/InfiniBand 网络;
  2. LLaMA3 就是在这两个集群上训练出来的
  3. 预计到 2024 年底,Meta AI 基础设施建设将拥有 35w 张 H100 GPU,总算力相当于约 60w 张 H100

水平及维护精力所限,译文不免存在错误或过时之处,如有疑问,请查阅原文。 传播知识,尊重劳动,年满十八周岁,转载请注明出处

以下是译文。



作为对未来人工智能的重要投资,Meta 打造了两个大规模 AI 集群,每个集群由 2.4w 张 GPU 组成, 本文分享其计算、网络、存储等设计细节。

1 第一代 GPU 集群:1.6w A100 (RSC)

Meta 很早就开始构建 AI 基础设施,但第一次对外分享是在 2022 年,介绍了我们的 Research SuperClusterRSC),它由 1.6w 个 A100 GPU 组成。

RSC 支撑了 Meta 第一代先进 AI 模型的开发,在训练 Llama/llama2、 计算机视觉、NLP、语音识别、图像生成甚至编码等 AI 工作中发挥了重要作用。

2 第二代 GPU 集群:2.4w H100

精确数字是每个集群 24,576 张 H100 GPU。

我们的新一代 AI 集群充分吸收了 RSC 的成功和经验教训,这包括,

  • 新集群致力于构建端到端的 AI 系统,特别强调研究人员和开发人员的用户体验和工作效率
  • 新集群能支持更大、更复杂的模型,为 GenAI 产品开发和 AI 研究的进步铺平了道路。

Meta 每天需要执行数以万亿计的 AI 任务,这就需要一个高度先进和灵活的基础设施。 我们自研了大部分硬件、软件和网络 fabric,使我们能进行端到端优化,确保数据中心的高效运行。

左侧:计算机柜,包括 GPU 服务器机框,置顶交换机,fabric 交换机等等;右侧:存储机柜

2.1 计算:Grand Teton GPU 主机

两个新集群都使用了 Grand Teton, 这是 Meta 开发的开放 GPU 硬件平台,我们已经将其贡献给了开放计算项目(OCP)。

从 2015 年的 Big Sur 平台开始, 我们就一直在开放设计我们的 GPU 硬件平台。

Grand Teton 实物图如下,

Image Source

  • 将 CPU 机头、GPU、交换机同步系统、电源等等集成到一个机框中,以获得更好的整体性能;
  • 提供了快速可扩展性和灵活性,设计简化,可以快速部署到数据中心,并易于维护和扩展。

结合 Open Rack 电源和机架架构 等其他内部创新,我们能为 Meta 当前和未来应用程序快速量身定制新集群。

2.2 网络

两个集群使用了不同的网络方案,但都是 400Gbps 接入。

2.2.1 集群一:400Gbps RoCE + 自研交换机

基于 RoCE 网络,使用的交换机包括

2.2.2 集群二:400Gbps InfiniBand

使用 NVIDIA Quantum2 InfiniBand fabric。

2.2.3 小结

两个方案作对比,使我们能够评估 RoCE/IB 在大规模训练中的适用性和可扩展性, 为设计和构建更大规模的集群提供了宝贵经验。 目前这两个不同组网类型的集群都能够运行大型生成式 AI 任务 (例如在 RoCE 集群上训练 Llama 3), 而没有遇到网络瓶颈。

2.3 存储

存储在 AI 训练中扮演着重要角色,然而相关的讨论确非常少。

最近的发展趋势可以看出,GenAI 任务越来越多模态,需要处理大量图像、视频和文本,因此对高性能存储的需求越来越强烈。 理想的存储方案除了提供良好的性能,还要做到低能耗

2.3.1 数据和 checkpoints 存储:FUSE + Tectonic

我们 AI 集群的数据和 checkpoint 的存储方案:

这个解决方案使得

  • 数千个 GPU 能同步保存和加载 checkpoints(对任何存储解决方案来说都是一个挑战),
  • 同时还提供了 EB 级存储系统所需的灵活性和高吞吐。

2.3.2 交互式调试:Parallel NFS

我们还与 Hammerspace 合作开发了一个并行网络文件系统(NFS), 它使工程师能够使用数千个 GPU 进行交互式调试, 因为代码改动能立即同步到环境中的所有节点。

Tectonic 分布式存储加上 Hammerspace,既能满足快速迭代,又不会限制规模。

2.3.3 大容量 SSD + 定制每个机柜的服务器数量

无论是 Tectonic 还是 Hammerspace 方案,都基于 YV3 Sierra Point server platform, 使用了我们在市场上能够买到的最新高容量 E1.S SSD

除此之外,每个机架塞的服务器数量也进行了定制,以在服务器吞吐量、机架数量和能效之间取得一个平衡。

OCP 服务器就像乐高积木,使我们的存储层能够灵活扩展到未来更大 AI 集群的需求,而且不影响日常基础设施的使用和维护操作。

3 性能

3.1 原则:性能和易用性缺一不可

我们构建大规模 AI 集群的一个原则是,同时最大化性能和易用性,而不是为了一个而牺牲另一个。 这是训练最佳 AI 模型的重要基础。

测试系统设计的扩展性的最佳方法就是先构建出一个系统,然后不断优化它,并进行实际测试(模拟器有帮助,但作用有限)。 通过这个过程,我们比较了小集群和大集群的性能,定位瓶颈在哪里。 下图显示了当大量 GPU 相互通信时(at message sizes where roofline performance is expected)的 AllGather 性能(带宽归一化到 0-100),

small cluster performance (overall communication bandwidth and utilization) reaches 90%+ out of the box, but an unoptimized large cluster performance has very poor utilization, ranging from 10% to 90%. After we optimize the full system (software, network, etc.), we see large cluster performance return to the ideal 90%+ range.

3.2 大集群优化

与优化过的小型集群性能相比,我们的大集群一开始性能是比较差的。 为了解决这个问题,我们做了如下优化:

  1. 改进 job scheduler,使其具备网络拓扑感知能力,这带来的好处:

    1. 延迟降低
    2. 转发到更上层网络(交换机)的流量减少。
  2. 结合 NVIDIA NCCL,优化了网络路由策略,以实现最优的网络利用率。

以上两项优化使大集群的性能已经接近小集群。

除此之外,我们还

  1. 训练框架和模型团队密切合作,不断改进基础设施。例如,

    1. 支持 NVIDIA H100 GPU 的新数据类型 FP8,这对训练性能大有帮助,
    2. 并行技术优化,
    3. 存储优化,
  2. 意识到可调试性(debuggability)是大规模训练的主要挑战之一。 在大规模情况下,定位到哪个 GPU 卡顿导致的整个训练作业变慢是非常困难的。 为此,我们正在构建 desync debug 或分布式 flight recorder 之类的工具,跟踪分布式训练的过程,以更快识别问题。

  3. 继续开发基础 AI 框架 PyTorch,使其能支持数万甚至数十万 GPU 进行训练。 例如,我们已经定位到进程组初始化方面的几个瓶颈,将启动时间从有时的几小时减少到几分钟。

4 对 open AI innovation 的承诺

Meta 保持对 AI 软件和硬件开放创新的承诺,我们始终相信开源硬件和软件是帮助行业解决大规模问题的有用工具。 我们将

  • 继续作为 OCP 的创始成员支持开放硬件创新,例如已经将 Grand Teton 和 Open Rack 等设计贡献给 OCP 社区。
  • 作为 PyTorch 的最大和主要贡献者,继续推动这一 AI 软件框架的开发和普及。
  • 继续致力于 AI 研究社区的开放创新。

    • 我们发起了开放创新 AI 研究社区, 旨在深化我们对如何负责任地开发和共享 AI 技术(尤其是大模型)的理解。
    • 我们还推出了 AI Alliance,这是一个由 AI 行业领先组织组成的小组,专注于在开放社区内加速负责任的 AI 创新。

我们的 AI 工作建立在开放科学和协力合作的哲学之上。

5 未来展望

本文介绍的两个 AI 训练集群是我们未来 AI 路线图的一部分。 预计到 2024 年底,Meta AI 基础设施建设将拥有 35w 张 H100 GPU,总算力相当于约 60w 张 H100

当前有效的方法可能不足以满足明天的需求,这也是为什么我们一直在各个方面不断评估和改进我们的基础设施, 包括物理硬件层、虚拟层、软件层以及更上面的业务层等等。 我们的目标是创建灵活可靠的系统,以支持日新月异的新模型和研究。


Written by Human, Not by AI Written by Human, Not by AI

[译] 大模型推理的极限:理论分析、数学建模与 CPU/GPU 实测(2024)

译者序

本文翻译自 2024 年的一篇文章: LLM inference speed of light, 分析了大模型推理的速度瓶颈及量化评估方式,并给出了一些实测数据(我们在国产模型上的实测结果也大体吻合), 对理解大模型推理内部工作机制和推理优化较有帮助。

A100-80GB PICe 推理延迟与吞吐。Image Source

译者水平有限,不免存在遗漏或错误之处。如有疑问,敬请查阅原文。

以下是译文。



摘要

在开发 calm 的过程中,我们考虑的一个核心问题是: 推理的极限在哪儿?因为我们需要以此为准绳,去衡量真实推理系统的速度。

calm 是一个基于 CUDA、完全从头开始编写的轻量级 transformer-based language models 推理实现

本文试图讨论这个极限及其影响。 如果对推导细节感兴趣,可参考这个 python notebook

1 推理机制

1.1 transformer:逐 token 生成,无法并行

当语言模型生成文本时,它是逐个 token 进行的。 可以把语言模型(特别是 decoder-only text transformer,本文统称为 LLM) 看做是一个函数

  • 输入:一个 token
  • 输出:一组概率,每个概率对应词汇表中一个 token。
  • 推理程序使用概率来指导抽样,产生(从词汇表中选择)下一个 token 作为最终输出。

词汇表(vocabulary):通常由单词、单词片段、中文汉字等组成(这些都称为 token)。 vocabulary 长什么样,可以可以看一下 bert-base-chinese 的词典 vocab.txt。 更多基础:

  1. GPT 是如何工作的:200 行 Python 代码实现一个极简 GPT(2023)
  2. Transformer 是如何工作的:600 行 Python 代码实现 self-attention 和两类 Transformer(2019)

译注。

文本生成过程就是不断重复以上过程。可以看出,在生成一个文本序列时,没有并行性的可能性

speculative execution 尝试通过一个 less accurate predictor 来实现某种程度的并行,本文不讨论。

1.2 生成过程建模:矩阵乘法

广义上,当处理一个 token 时,模型执行两种类型的操作:

  1. 矩阵-向量乘法:一个大矩阵(例如 8192x8192)乘以一个向量,得到另一个向量,
  2. attention 计算

    在生成过程中,模型不仅可以看到当前 token 的状态,还可以看到序列中所有之前 token 的内部状态 —— 这些状态被存储在一个称为 KV-cache 的结构中, 它本质上是文本中每个之前位置的 key 向量和 value 向量的集合

    attention 为当前 token 生成一个 query 向量,计算它与所有之前位置的 key 向量之间的点积, 然后归一化得到的一组标量,并通过对所有之前的 value 向量进行加权求和来计算一个 value 向量,使用点积得到最终得分。

This description omits multi-head attention and the details of “normalization” (softmax), but neither are critical for understanding the inference performance.

1.3 瓶颈分析

以上两步计算有一个重要的共同特征:从矩阵或 KV-cache 读取的每个元素,只需要进行非常少量的浮点运算

  • 矩阵-向量乘法对每个矩阵元素执行一次乘加运算(2 FLOPs);
  • attention 对每个 key 执行一次乘加,对每个 value 执行一次乘加

1.3.1 典型“算力-带宽”比

现代 CPU/GPU 的 ALU 操作(乘法、加法)内存 IO 速度要快得多。例如:

  • AMD Ryzen 7950X:67 GB/s 内存带宽和 2735 GFLOPS,Flop:byte = 40:1
  • NVIDIA GeForce RTX 4090:1008 GB/s 显存带宽和 83 TFLOPS,Flop:byte = 82:1
  • NVIDIA H100 SXM:3350 GB/s 内存带宽和 67 TFLOPS, 对于矩阵乘法,tensor core 提供 ~494 TFLOPS 稠密算力,Flop:byte = 147:1

对于 FP16/FP8 等精度较低的浮点数,比率更夸张:

  • H100 TensorCore 对于 dense FP8 矩阵的理论吞吐量为 1979 TFLOPS,FLOP:byte = 590:1

在这些场景中,无论是否使用 TensorCore 或使用什么浮点格式,ALU 都非常充足。

1.3.2 瓶颈:访存带宽

因此,transformer 这种只需要对每个元素执行两次操作的场景,必定受到访存带宽的限制。 所以,基于下面几个因素,

  1. 模型配置(参数多少)
  2. KV-cache 大小
  3. 访存带宽

我们就能估计推理过程的最短耗时。 下面以 Mistral 7B 为例来具体看看。

2 以 Mistral-7B 为例,极限推理延迟的计算

2.1 参数(权重)数量的组成/计算

Mistral-7B 有 72 亿参数(所有矩阵元素的总数是 72 亿个)。 参数的组成如下:

  1. 4096 * 32000 = 131M 用于 embedding 矩阵;
    • 4096: hidden size (tokens per hidden-vector)
    • 32000: vocabulary size

    矩阵-向量乘法中不会使用这整个大矩阵,每个 token 只读取这个矩阵中的一行,因此数据量相对很小,后面的带宽计算中将忽略这个;

  2. 32 * (4096 * (128 * 32 + 128 * 8 * 2) + 4096 * 128 * 32) = 1342M 用于计算与 attention 相关的向量;
  3. 32 * (4096 * 14336 * 3) = 5637M 用于通过 feed-forward 转换 hidden states;
  4. 4096 * 32000 = 131M 用于将 hidden states 转换为 token 概率;这与 embedding 矩阵不同,会用于矩阵乘法。

以上加起来,大约有 7111M (~7B) “活跃”参数用于矩阵乘法。

2.2 计算一个 token 所需加载的数据量

2.2.1 总数据量

如果模型使用 FP16 作为矩阵元素的类型, 那每生成一个 token,需要加载到 ALU 上的数据量

7111M params * 2Byte/param = ~14.2 GB

虽然计算下一个 token 时每个矩阵都可以复用,但硬件缓存的大小通常只有几十 MB, 矩阵无法放入缓存中,因此我们可以断定,这个生成(推理)过程的速度不会快于显存带宽

attention 计算需要读取当前 token 及前面上下文中所有 tokens 对应的 KV-cache, 所以读取的数据量取决于生成新 token 时模型看到多少前面的 token, 这包括

  1. 系统提示词(通常对用户隐藏)
  2. 用户提示词
  3. 前面的模型输出
  4. 可能还包括长聊天会话中多个用户的提示词。

2.2.2 KV-cache 部分的数据量

对于 Mistral,KV-cache

  • 为每层的每个 key 存储 8 个 128 元素向量,
  • 为每个层的每个 value 存储 8 个 128 元素向量,

这加起来,每个 token 对应 32 * 128 * 8 * 2 = 65K 个元素; 如果 KV-cache 使用 FP16,那么对于 token number P,我们需要读取 P * 130 KB 的数据。 例如, token number 1000 将需要从 KV-cache 读取 130MB 的数据。 跟 14.2GB 这个总数据量相比,这 130MB 可以忽略不计了。

2.3 以 RTX 4090 为例,极限延迟计算

根据以上数字,现在可以很容易地计算出推理所需的最小时间。

例如,在 NVIDIA RTX 4090(1008 GB/s)上,

  • 14.2GB (fp16) 需要 ~14.1ms 读取,因此可以预期对于位置靠前的 token, 每个 token 大约需要 14.1ms(KV-cache 影响可以忽略不计)。
  • 如果使用 8bit 权重,需要读取 7.1GB,这需要大约 7.0ms

这些都是理论下限,代表了生成每个 token 的最小可能时间。

2.4 ChatGLM3-6B/Qwen-7B 实测推理延迟(译注)

简单的单卡推理测试,16bit 权重,平均延迟,仅供参考:

LLM RTX 4090 24GB (2022) A100 80GB (2020) V100 32GB (2017)
ChatGLM3-6B 16ms/token 18ms/token 32ms/token
Qwen-7B 19ms/token 29ms/token 41ms/token

可以看到,单就推理速度来说,只要模型能塞进去(< 24GB),4090 与 A100 相当甚至更快,比 V100 快一倍。

说明:以上测的是 4090,不带 D4090D)。

3 数学模型和理论极限的用途

以上根据数学建模和计算得出了一些理论极限数字,接下来看看这些理论极限有什么用。

3.1 评估推理系统好坏

要接近理论极限,需要一个高质量的软件实现,以及能够达到峰值带宽的硬件。 因此如果你的软件+硬件离理论最优很远,那肯定就有问题:可能在软件方面,也可能在硬件方面。

例如,在 RTX 4090 上 calm 使用 16 位权重时达到 ~15.4 ms/tok,使用 8 位权重时达到 ~7.8 ms/tok, 达到了理论极限的 90%。

Close, but not quite there - 100% bandwidth utilization is unfortunately very hard to get close to on NVidia GPUs for this workload. Larger GPUs like H100 are even more difficult to fully saturate; on Mixtral - this is a different architecture but it obeys the same tradeoffs for single sequence generation if you only count active parameters - calm achieves ~80% of theoretically possible performance, although large denser models like Llama 70B can get closer to the peak.

在 Apple M2 Air 上使用 CPU 推理时,calmllama.cpp 只达到理论 100 GB/s 带宽的 ~65%, 然后带宽就上不去了,这暗示需要尝试 Apple iGPU 了。

3.2 指导量化

带宽与每个权重使用的 bit 数成正比;这意味着更小的权重格式(量化)能实现更低的延迟。 例如,在 RTX 4090 上 llama.cpp 使用 Mistral 7B

  • 16 bit 权重:~17.1 ms/tok(82% 的峰值)
  • 8.5 bit 权重:~10.3ms/tok (71% 的峰值)
  • 4.5 bit 权重:~6.7ms/tok (58% 的峰值)

因此对于低延迟场景,可以考虑低精度量化。

3.3 指导优化方向

除了为推理延迟提供下限外,上述建模还表明:推理过程并未充分利用算力(ALU)。 要解决这个问题,需要重新平衡 FLOP:byte 比例, speculative decoding 等技术试图部分解决这个问题。

3.3.1 批处理 batch 1 -> N:瓶颈 访存带宽 -> 算力

这里再另一种场景:多用户场景。注意到,

  • 当多个用户请求同时处理时,我们用相同的矩阵同时执行多个矩阵-向量乘法, 这里可以将多个矩阵-向量乘法变成一个矩阵-矩阵乘法
  • 对于足够大的矩阵来说,只要矩阵-矩阵乘法实现得当,速度就比访存 IO 快,

因此这种场景下,瓶颈不再是访存 IO,而是算力(ALU)。这就是为什么这种 ALU:byte 不平衡对于生产推理系统不是关键问题 —— 当使用 ChatGPT 时,你的请求与同一 GPU 上许多其他用户的请求并发评估,GPU 显存带宽利用更加高效。

3.3.2 批处理无法改善所需加载的 KV-cache 数据量

批处理通常不会减轻 KV-cache 带宽(除非多个请求共享非常大的前缀),因为 KV-cache 大小和带宽随请求数量的增加而增加,而不像权重矩阵保持不变。

像 Mistral 这样的混合专家模型(MoE)scaling 特性稍有不同:batching initially only increases the bandwidth required, but once the expert utilization becomes significant the inference becomes increasingly ALU bound.

3.4 硬件相对推理速度评估

带宽是评估推理性能的关键指标,对于模型变化/设备类型或架构来说是一个恒定的, 因此即使无法使用 batch processing,也可以用它来评估你用的硬件。

例如,NVIDIA RTX 4080 有 716 GB/s 带宽,所以可以预期它的推理速度是 RTX 4090 的 ~70% —— 注意,游戏、光线追踪或推理其他类型的神经网络等方面,相对性能可能与此不同!

4 GQA (group query attention) 的影响

Mistral-7B 是一个非常平衡的模型;在上面的所有计算中,几乎都能忽略 KV-cache 部分的 IO 开销。 这背后的原因:

  1. 较短的上下文(Mistral-7B 使用 windowed attention,限制 4096 token 的窗口),
  2. 使用了 GQA,这个是更重要的原因。

LLaMA 2:开放基础和微调聊天模型(Meta/Facebook,2023) 也使用了 GQA。

4.1 GQA 为什么能减少带宽

在 GQA 中(with a 4x ratio),为了得到 attention 的 4 个点积,

  • 不是使用 4 个 query 向量并分别与 4 个相应的 key 向量计算点积,
  • 而是只取一个 key 向量,然后执行 4 个点积。

这能够减少 KV-cache 的大小和所需带宽,也在某种程度上重新平衡了 ALU:bandwidth 比例。

4.2 有无 GQA 的数据量对比

这对于 KV-cache 内存大小也很关键,不过,这可能对短上下文模型不太明显

  • 4096 token 上下文的 Mistral 需要 0.5GiB,
  • 没有 GQA 的可比模型(如 Llama 7B)“只需要”2 GiB。

让我们看看一个最近不使用 GQA 的模型,Cohere 的 Command-R

Command-R has a large vocab (256K) and large hidden state (8192) so it spends a whopping 2B parameters on embeddings, but it reuses the same matrix for embedding and classification so we don’t need to exclude this from the inference bandwidth calculation.

模型本身有大约 35b 参数,所以以 16 位/权重计算,我们在推理期间需要为每个 token 读取 70 GB 的权重。 对于每个 token ,它需要在 KV-cache 中存储 40 * 128 * 64 * 2 = 655K 元素,以 16 位/元素计算是每个 token 1.3 MB。

因此,一个 4096 token 的上下文将需要大约 5.3GB; 与 ~70 GB 的权重相比,这已经相当显著了。然而,如果考虑到 Cohere 的模型宣传有 200K token 上下文窗口 —— 计算最后一个 token 需要读取 260 GB(还需要 260GB 的显存来存储它)!

4.3 多用户场景下 KV-cache 占用的显存规模

这么大的模型,典型的生产环境配置(单用户),

  • weights 通常使用 4bit 量化(通常的实现占用 ~4.5bit/权重)
  • KV-cache 可能会使用 8bit(FP8)值。

如果我们“保守地”假设上下文为 100K,则

  • 模型权重占 ~19.7GB
  • KV-cache 占 ~65GB

计算到最后一个 token 时,我们需要从内存中读取这么大的数据。 可以看到,突然之间,attention 计算部分的数据量(最终转变成耗时)从微不足道变成了占 ~75%

虽然 100K 上下文可能看起来有点极端,但在短上下文+多用户场景中,情况也是类似的:

  • 批处理优化将多次矩阵-向量乘法变成了一次矩阵-矩阵乘法(为一批用户请求读取一次模型权重),瓶颈来到算力(ALU),
  • 每个用户请求通常都有自己的 KV-cache

因此最终的 attention 仍然受访存带宽限制,并且需要大量内存/显存才能将所有用户请求放到单个节点!

4.4 GQA:减小从 KV-cache 加载的数据量

如果模型使用 4x GQA,KV-cache 的大小和所需带宽将会变成原来的 1/4

对于 100k+ token 的上下文场景,虽然 KV-cache 的开销仍然很大(65GB -> 16GB+),但已经进入实用范围。

4.5 GQA 的问题

对于 Cohere 的目标使用场景,引入 GQA 可能会导致模型质量有下降,具体得看他们的技术报告。

但是,纯粹从成本/性能角度来看,每个基于 transformer 的 LLM 都需要评估是否能引入 GQA,因为收益太大了。

5 总结

对于大模型推理场景,计算和访存的次数是已知的,因此可以进行数学建模,计算理论极限。 这非常有用,不仅可以用来验证推理系统的性能, 而且能预测架构变化带来的影响


Written by Human, Not by AI Written by Human, Not by AI

AI芯片与GPU的争端:上世纪的故事重演

感觉这大半年参加芯片发布活动,观察一件很有意思的事:GPU 厂商说 AI 算力的时候,都喜欢拿自己跟 CPU 去比;AI 芯片厂商在说 AI 算力的时候,又很喜欢拿自己去跟 GPU 比。CPU 在 AI 时代成为了鄙视链最底层。看起来不仅绝对性能跑不过人家,而且能效比还差那么远。

好像只要参加 AI 芯片相关的会,一定会有人提 AlexNet 创世纪般的存在,也一定有人提 OpenAI 在 2018 年年中发布的《AI 与计算》分析报告[1]。因为里面说,从 2012 年至今,最大型的 AI training 计算量每 3.4 个月就会翻一倍——所以到 2018 年实际已经涨了超过 30 万倍。如果按照摩尔定律两年翻番来算,那么其实,2012-2018 年理论上芯片性能只能涨 7 倍。

看看,“只能”涨 7 倍,CPU 是应该哭一哭的——虽然实际上就摩尔定律,这里援引的数据都不准确,人家也不是说性能翻番不是?当然了,这些都不重要。重点在于,AI 算力需求这些年大幅攀升。

AI 芯片是某一类专用芯片

感觉这大半年我都在客观上探讨一件事,在刚进入 EE Times 不久,就看到 MIT 2018 年出的一篇 paper。这篇 paper 也在我这大半年写的文章里,被反复提及,题为《通用技术计算机的衰落:为何深度学习和摩尔定律的终结正致使计算碎片化》。在整体观点上,这篇 paper 其实还十分有趣。几个月前,我把这篇 paper 的观点做了浓缩和重构,再加了一些额外的料,构成了一篇《深度学习的兴起,是通用计算的挽歌?》[2]。

各位有兴趣可以去看一下,这篇文章真的细节动人、论据充分、观点鲜明、语句通顺……这里再总结一下,其实观点很简单,就是既然 CPU 现在性能涨幅这么小,工艺节点都难推进,那这条路差不多就快走完了;然鹅,还有专用计算这条路啊:就是针对不同的应用场景,咱们开发专门的芯片,量身定制、量体裁衣:这样的话就能做到性能持续大幅攀升,与此同时效率高、功耗还大幅降低。

其实论证这个观点的过程还是非常有趣的,比如说谷歌 TPU 出现了,就是专用芯片的最佳例证,它就真的只能做一件事。

这篇 paper 看起来美好而高级、优雅而低调。不过在随后工作的时间里,我基本上是在花全部的时间,尝试听取行业内的参与者们是否赞同这一点。很遗憾的是,几乎我听到的绝大部分声音,都在彻底否定这个论调。有关这一点,实际还可以从 1 月份即将发行的《观点》杂志,我的文章《AI芯片专用好还是通用好?遥想20年前GPU也面临这一抉择》中看到比较详细的论述。这里就不提太多了(下个月就和大家见面哟)。

绝对专用芯片也就是 ASIC,的确效率高、算力也高。但它实际受到两个巨大问题的影响:

第一是成本。这是个看起来非常废话的原因。像 CPU 这种全银河系通用、只在大麦哲伦星系不通用的芯片,你在上面干什么不行啊?都可以,装个 Windows,然后跑个虚拟机,还能装 macOS,写文档、做设计、修图、剪视频、炒股票,各种软件都能跑,医疗、交通、政府、金融,咱都能干。甚至,如果你一定要跑 AI 任务,CPU 其实也可以干。

所以 CPU 的特点,就决定了,这一类产品是全局适用的。它的销量非常大,渗透在人类生活的方方面面。所以即便新制程的设计和工艺成本都在指数级攀升,造个工厂花几十亿美金才可以——未来还要更多。不过这么昂贵的制造成本,实际能够消受得起的产品类型就不多。到最具体的产品上,手机、PC 都是可以承担这个成本的,因为它们每年的销量特别大,单 iPhone 一年就卖超过 2 亿台…每个手机、PC 至少都需要 CPU 才能跑得起来。CPU 有充足的量来摊薄设计与制造成本(实际情况比这还要复杂一些,还是推荐各位去看[2]这篇文章)。

而专用芯片是完全不具备这种成本摊薄特性的:因为它只针对某一个领域。绝大部分特定领域都不具备走量来摊薄最先进制程工艺成本的能力。比如汽车,这是个看起来十分庞大的市场了,但全球销量最好的车型,年销量也不过区区两三百万——这个量对一个车型就一颗专用芯片的设计和制造成本而言,实在不是个划算的数字——采用最先进制程则几乎是没有可能的。

当然,汽车专用芯片仍然可以考虑用成本更低、更早的制程来造芯片(以及某一颗芯片用在大量车型上)——这也是现在绝大部分专用芯片的常规方案。不过当“专用”芯片所覆盖的市场容量本身就不大,以及可能比汽车市场还要小很多的时候,尤其是很多 B 端市场——天花板是明摆着的,又靠什么来抵消芯片设计与制造成本?

这个问题在 MIT 的那篇 paper 中实则有着非常翔实的论述,其中详细列出了关乎特定市场容量、专用芯片产量、专用芯片相较通用处理器的性能与效率优势有多大,这些变量相互之间是什么样的关系。在满足怎样的条件时,专用芯片可以提供更高的成本效益。

在我更早期撰写的采访文章中,针对 ASIC 制造耗费成本这件事,我们有一个更准确地认识,即《摩尔定律失效,FPGA 迎来黄金时代?》[3]。不过这篇采访文章实则忽视了一个重要事实,就是 MIT 提到的上述这几个变量关系。而且专用芯片其实没有必要采用最新的制程,依然可以在性能和能效上碾压通用芯片(或 FPGA)。我在《深度学习的兴起,是通用计算的挽歌?》文中同样浓缩了这部分理论,在文章的第三部分“专用处理器市场过小?”章节内——不过当时为了理解方便,我没有将 MIT 提到的所有变量都放到我的这篇文章中,所以仍然建议去看 MIT 的原文。

第二是通用性差异。

双重标准:通用与性能

CPU 的通用属性就决定了,它在任何一个方面,其实都很难做到精通,或者说针对任何具体应用场景的算力和效率表现其实都一般。因为 CPU 需要耗费大量面积来做多层级 cache,微架构前端也很重要,真正的执行单元所占尺寸就那么点。因为 CPU 需要处理各种类型的工作,各种条件分支之类的东西。

但 CPU 的设计和工艺都是具备相当难度的,至少显著难于绝大部分专用芯片。用 GPU 去比 CPU 的 AI 算力,这种对比的价值显然是不大的。这其中的核心就在于,GPU 实际上本来就算是一种专用芯片。至少早年,GPU 就用于图形计算,它只做这一件事。而且实际在 GPU 诞生的更早期,它本来就以 ASIC 的面貌出现——它从骨子里是一种专用芯片。用 CPU 这样绝对通用的芯片,去比较 GPU 这种专用芯片,又有什么价值?尤其如果你还比较浮点运算能力,那就更奇特了。

不过 GPU 这个类型的芯片,在发展中后期发生了一些很显著的变化。它开始越来越具备通用属性(这个转变原因也可以从《观点》杂志中找),shader 核心这种非固定功能单元的地位越来越重要。即便 GPU 仍然没有 CPU 那么通用,但 cuda 编程这种东西是现如今人尽皆知的;GPU 的可编程性,或者叫灵活性变得越来越高。所以 GPU 现在早就不只用于图形计算了。

我们说将 GPU 应用于 AI 计算,不管是云端 training 还是终端 inferencing,其本质都是 GPU 通用属性的某一个方向;AI 计算在 GPGPU 世界里,不过是其中一个组成部分罢了。只不过是因为 AI 计算这个方向实在是潜力太大了,所以 GPU 厂商开始将 AI 计算作为一个着重发展的方向来对待,以及还针对 AI 计算特别加入了一些专用单元,比如张量核心。

然鹅这个时候,AI 专用芯片华丽丽地出现了,比如谷歌的 TPU、比如特斯拉的 FSD(Full Self-Driving Computer),以及一众国内外的 AI 芯片新品。

AI 专用芯片如果专用、固化到 TPU 那样的程度,只针对卷积神经网络,采用 Systolic Array 技术;前述第一个成本问题之外,它具有的第二个局限性就在于几乎没有灵活性可言。尤其在 AI 算法每个月甚至每周都可能发生变化的情况下,芯片 18 个月开发周期,当芯片问世的时候,这颗芯片就极有可能已经落后了。

但我们仍然不得不承认,AI 芯片在它所擅长的任务上,可能具备在效能与算力上大幅领先 GPU 的能力。所以 AI 芯片厂商几乎清一色地会在发布会上宣布,自家产品可以吊打某 N 字头企业的 GPU 某明星产品。

这件事,本质上约等于拿 GPU 去和 CPU 比浮点运算能力。而且实际上,AI 芯片比较的“AI 算力”大部分情况下是低精度的,比如很多终端 inferencing 芯片 INT8 计算能力很强——那你怎么不比比双精度?因为你不能做双精度运算?这种对比是将 GPU 放在通用计算的地位上,用专用计算的 AI 芯片——包括专门设计的 cache 或 HBM、低精度执行单元等——来吊打 GPU。这同样是件没有价值的事情。

不过更有意思的是,GPU 此刻为自己辩驳的方式,大部分是说:我能做的事情更多啊,比 TPU 之流的 AI 芯片能做的事情多太多了,它们那些 AI 芯片就只能做一件事。

这属于典型的双标,在和 CPU 比较时,宣称自己 AI 算力高出一大截;在和 AI 芯片比较时,宣称自己更通用。这不是双标吗?

是否存在第三类通用芯片?

其实我花了比较长的时间去理解,为什么 MIT 的这篇 paper 并不能成立;至少它成立的概率会很低。因为就历史经验来看,它是不对的:当我们参考 CPU 和 GPU 的兴衰史,其实就很容易发现,专用芯片在大部分历史条件下都不会成为主流,而只能成为某个历史时期的特定过渡产品。有关这一点仍然推荐去看 1 月份即将发布的《观点》杂志文章,这在前文已经提到了。

不过 AI 芯片是顺应时代潮流产生的一种芯片类型——除了前文提到的 TPU、FSD 这类相对极端的绝对专用 AI 芯片,当代越来越多的 AI 芯片都已经产生弱编程特性了,就跟当年的 GPU 一样。就连 Arm 的 NPU IP 实则都融入了一定的灵活性。也就是说,如今有一大批 AI 芯片实际是具备灵活性或通用性的,它们不只能做一件事,从结构上它还为未出现的算法做考量。

Graphcore IPU、华为昇腾都在其列。也就是说,AI 芯片在 AI 计算时,不仅效率相较 GPU 更出色,而且它还具备一定的通用性。这实则才是很多 AI 芯片企业在宣传中提到,在 CPU、GPU 之外,第三类芯片出现的原因,就是 AI 芯片。它可能将拥有自己的适用领域、迭代周期、开发生态。未来 CPU、GPU 和 AI 芯片就要成为三条并行的线了。

从应用场景来看,这个观念好像是成立的。至少我觉得,它比 MIT 说的专用芯片成为未来这一点要靠谱多了。

但我仍然觉得,这种畅想的实现概率也会比较低——至少在云端 AI training 这部分市场,GPU 可能将长期占据垄断地位,且难以撼动。因为 GPU 不仅具备制程优势(有能力采用最先进制程的少数派),而且具备开发生态优势——大量开发者都愿意投入其中,因为它相比 AI 芯片,具备了先天的生态基础,且发展多年。GPU 开发生态优势巨大的程度在从上至下、上天入地、贯彻电子科技行业,GPU 是无处不在的。

当开始拼开发软件栈的时候,一场全新的厮杀战就要上演了。某种芯片进入可编程时代,经营这类芯片的公司就不只是一家芯片公司了,它对软件人力物力的投入极有可能大于硬件,以 1:10 的硬件、软件工程师比例存在于世。这个时候,企业和行业都会变得越来越庞大。在行业整体价值的复合年增长率无法满足企业的成本投入攀升时,市场会逐渐步入寡头时代。GPU 市场就是如此发展至今的。

某 N 字头企业在如今的 AI training 市场已经占据了绝对统领地位,这种地位的不可撼动性就体现在开发生态的绝对优势上。且其发展经验积累,又令其具备了充足的资本优势可持续完善这个闭环生态,从软件到硬件。这就不是哪家 AI 芯片厂商随便对比一下性能、能效足以完事儿的了。生态优势可以彻底无视性能、能效的那点差别,尤其当这种性能、能效差别并没有数量级差别时。

所以,很多 AI 芯片初创企业畅想中的第三类芯片究竟能不能成立,或许要打一个问号。

好了,本文的 YY 差不多就到这里了。很多时候,历史经验是没有任何价值的,尤其我们说“具体问题具体分析”的时候,每一个事件的发展过程都有其特殊性,那些依据历史经验做推断的过程,本质上都是在胡说八道;在新事物面前,一切规律总结都只是在博君一笑——比如上面说的这么多东西。比如在终端小型 inferencing 现场,AI 芯片是能够长期发挥价值的,这仍然可以促成生态的持续反向完善。

不过至少,AI 芯片用自己的标准去和 GPU 比算力,GPU 又用自己的标准去和 CPU 比算力,同时还宣称自己比 AI 芯片能做更多的事,本质上都是耍流氓。

参考来源:
[1] AI and Compute – OpenAI
[2] 深度学习的兴起,是通用计算的挽歌?– 欧阳洋葱
[3] 摩尔定律失效,FPGA迎来黄金时代?– 欧阳洋葱

GPU 进阶笔记(三):华为 NPU/GPU 演进(2024)

记录一些平时接触到的 GPU 知识。由于是笔记而非教程,因此内容不会追求连贯,有基础的同学可作查漏补缺之用。

水平有限,文中不免有错误或过时之处,请酌情参考。



本文内容都来自公开资料,仅供个人了解参考。 AI 相关的东西现在迭代非常快,所以部分内容可能已经过时,请注意甄别。

1 术语

CPU/GPU/NPU 等等都是硬件芯片,简单来说,晶体管既可以用来实现逻辑控制单元, 也可以用来实现运算单元(算力)。 在芯片总面积一定的情况下,就看控制和算力怎么分。

  • CPU:通用目的处理器,重逻辑控制;
  • GPU:通用目的并行处理器(GPGPU),图形处理器;
  • NPU:专用处理器,相比 CPU/GPU,擅长执行更具体的计算任务。

1.1 CPU

大部分芯片面积都用在了逻辑单元,因此逻辑控制能力强,算力弱(相对)。

1.2 GPU

大部分芯片面积用在了计算单元,因此并行计算能力强,但逻辑控制弱。 适合图像渲染、矩阵计算之类的并行计算场景。作为协处理器, 需要在 CPU 的指挥下工作,

Image Source [8]

1.3 NPU / TPU

也是协处理器。在 wikipedia 中没有专门的 NPU (Neural Processing Unit) 页面,而是归到 AI Processors 大类里面, 指的是一类特殊目的硬件加速器,更接近 ASIC,硬件实现神经网络运算, 比如张量运算、卷积、点积、激活函数、多维矩阵运算等等[7]。

如果还不清楚什么是神经网络,可以看看 以图像识别为例,关于卷积神经网络(CNN)的直观解释(2016)

在这些特殊任务上,比 CPU/GPU 这种通用处理器效率更高,功耗更小,响应更快 (比如一个时钟周期内可以完成几十万个乘法运算), 因此适合用在手机、边缘计算、物联网等等场景。

TPU:这里特制 Google 的 Tensor Processing Unit,目的跟 NPU 差不多。 [11] 对 TPU 和 GPU 的使用场景区别有一个非常形象的比喻

如果外面下雨了,你其实并不需要知道每秒到底有多少滴雨, 而只要知道雨是大还是小。 与此类似,神经网络通常不需要 16/32bit 浮点数做精确计算,可能 8bit 整型预测的精度就足以满足需求了。

Floor Plan of Google TPU die(yellow = compute, blue = data, green = I/O, red = control) [11]

1.4 小结

GPU 已经从最初的图像渲染和通用并行计算,逐步引入越来越多的神经网络功能 (比如 Tensor Cores、Transformer); 另一方面,NPU 也在神经网络的基础上,开始引入越来越强大的通用计算功能, 所以这俩有双向奔赴的趋势。

2 华为 DaVinci 架构:一种方案覆盖所有算力场景

2.1 场景、算力需求和解决方案

不同算力场景下,算力需求(TFLOPS)和内存大小(GB)的对应关系 [1]

华为的解决方案:一种架构(DaVinci),覆盖所有场景 [1]

用在几个不同产品方向上,

  1. 手机处理器,自动驾驶芯片等等
  2. 专门的 AI 处理器,使用场景类似于 GPU

2.2 Ascend NPU 设计

2017 年发布了自己的 NPU 架构,[2] 详细介绍了 DaVinci 架构的设计。 除了支持传统标量运算、矢量运行,还引入了 3D Cube 来加速矩阵运算,

Image Source [2]

单位芯片面积或者单位功耗下,性能比 CPU/GPU 大幅提升:

Image Source [2]

下面看看实际使用场景和产品系列。

3 路线一:NPU 用在手机芯片(Mobile AP SoC)

现代手机芯片不再是单功能处理器,而是集成了多种芯片的一个 片上系统(SoC), 华为 NPU 芯片就集成到麒麟手机芯片内部,随着华为 Mate 系列高端手机迭代。

Image Source [7]

比如,一些典型的功能划分 [7]:

  • CPU 主处理器,运行 app;
  • GPU 渲染、游戏等;
  • NPU 图像识别、AI 应用加速。

Mate 系列手机基本上是跟 Kirin 系列芯片一起成长的,早期的手机不是叫 “Mate XX”, 而是 “Ascend Mate XX”,从中也可以看出跟昇腾(Ascend)的渊源。

3.1 Kirin 9702017, Mate 10 系列手机

据称是第一个手机内置的 AI 处理器(NPU)[3]。 在 AI 任务上(比如手机上输入文字搜图片,涉及大批量图片识别)比 CPU 快 25~50 倍。

  • 10nm,台积电代工
  • CPU 8-core with a clockspeed of uP to 2.4GHz i.e. 4 x Cortex A73 at 2.4GHz + 4 x Cortex 53 at 1.8GHz
  • GPU 12-core Mali G72MP12 ARM GPU
  • NPU 1.92 TFLOPs FP16

3.2 Kirin 990 5G2019, Mate 30 系列手机

Kirin 990 包含了 D-lite 版本的 NPU [1]:

  • World’s st 5G SoC Poweed by 7nm+ EUV
  • World’s 1st 5G NSA & SA Flagship SoC
  • Wolrd’s 1st 16-Core Mali-G76 GPU
  • World’s 1st Big-Tiny Core Architechture NPU

麒麟 990 5G 芯片逻辑拓扑 [1]

一些硬件参数 [1,4]:

  • 台积电 7nm+ 工艺
  • CPU 8-Core
  • NPU 2+1 Core
  • GPU 16-core Mali-G76(ARM GPU)

  • GPU 16-core Mali-G76 (ARM GPU)
  • NPU
    • HUAWEI Da Vinci Architecture,
    • 2x Ascend Lite + 1x Ascend Tiny
  • 2G/3G/4G/5G Modem
  • LPDDR 4X
  • 4K HDR Video

3.3 Kirin 9000 5G2020,Mate 40 系列手机

Image Source

  • 台积电 5nm 工艺
  • GPU 24-core Mali-G78, Kirin Gaming+ 3.0
  • NPU
    • HUAWEI Da Vinci Architecture 2.0 第二代架构
    • 2x Ascend Lite + 1x Ascend Tiny

这个是台积电 5nm 工艺 [5],然后就被美国卡脖子了。 所以 Mate 50 系列用的高通处理器,Mate 60 系列重新回归麒麟处理器。

3.4 Kirin 9000s2023,Mate 60 系列手机

王者低调回归,官网没有资料。

据各路媒体分析,是中芯国际 7+nm 工艺,比上一代 9000 落后一些, 毕竟制程有差距,看看国外媒体的副标题 [6]:

It's tough to beat a 5nm processor with a 7nm chip.

Wikipedia 提供的参数 [10]:

  • SMIC 7nm FinFET
  • CPU HiSilicon Taishan microarchitecture Cortex-A510
  • GPU Maleoon 910 MP4
  • NPU 有,但是没提

3.5 小结

手机芯片系列先到这里,接下来看看作为独立卡使用的 NPU 系列。

4 路线二:NPU 用作推理/训练芯片(Ascend AI Processor)

两个产品:301 低功耗;910 高算力。

设计见 paper [2]。

4.1 产品:加速卡 Atlas 系列

型号 Atlas 200/300/500/…,包括了 NPU 在内的 SoC,用于 AI 推理和训练。

4.2 Ascend 3102019,推理

Spec

用的是 D-mini version:

纸面算力基本对标 NVIDIA T4 [9]。

4.3 Ascend 910, 2019,训练

4.3.1 Spec & Performance, vs. Google TPU

Image Source [1]

Image Source [1]

4.3.2 计算集群

Image Source [1]

Image Source [1]

4.4 Ascend 910B, 2023

GPU 进阶笔记(二):华为 Ascend 910B GPU 相关(2023)

参考资料

  1. DaVinci: A Scalable Architecture for Neural Network Computing,huawei, 2020
  2. Ascend: a Scalable and Unified Architecture for Ubiquitous Deep Neural Network Computing, HPCA, 2021
  3. Huawei Unveils The Kirin 970, The World’s First Processor With A Dedicated NPU, 2017
  4. Kirin 990 5G Data Sheet, hisilicon.com
  5. Kirin 9000 Data Sheet, hisilicon.com
  6. Huawei’s sanctions-evading Kirin 9000S processor tested: significantly behind its Kirin 9000 predecessor that used TSMC tech,2023
  7. Neural Processing Unit (NPU) Explained, 2021
  8. AI 101: GPU vs. TPU vs. NPU, 2023
  9. GPU Performance (Data Sheets) Quick Reference (2023)
  10. Wikipedia: HiSilicon, wikipedia.com
  11. An in-depth look at Google’s first Tensor Processing Unit (TPU), Google Cloud Blog, 2017

Written by Human, Not by AI Written by Human, Not by AI

Linux 容器底层工作机制:从 500 行 C 代码到生产级容器运行时(2023)

从几百行 C 代码创建一个 Linux 容器的过程,一窥内核底层技术机制及真实 container runtime 的工作原理。

Fig. Kernel machanisms that support Linux containers

本文所用的完整代码见这里

水平有限,文中不免有错误或过时之处,请酌情参考。



1 引言

从以 docker 为代表的 Linux 容器技术开始进入主流视野,到如今 k8s 一统容器编排 (甚至虚拟机编排)领域,容器技术已经在大规模生产环境使用了十年以上。 从基础设施层往上看,容器领域仍然在快速发展,新的项目层出不穷(看看 CNCF landscape 里密密麻麻的几百个项目就知道了); 但如果深入到基础设施内部,特别深入到每台主机内部, 会发现支撑容器的那些最基础技术其实都没怎么变 —— 对于基础设施工程师来说, 排查和解决一些虚拟化相关的线上疑难杂症和偶发问题,需要的正是对这些底层技术的理解和把控。

本文试图通过一段简单但又尽量全面的代码来串联起这些底层核心技术,看看一个容器是如何创建出来的。 有了对这个过程的理解,容器就不再是一个无从下手的黑盒,排查一些线上疑难杂症时也会更有方向。

1.1 Linux 容器底层机制:NS/cgroups/Capabilities/Seccomp/...

Fig. Kernel machanisms that support Linux containers

如上图所示,Linux 容器由几种内核机制组成,这里将它们分到了三个维度:

  1. namespace:一种资源视图隔离机制, 决定了进程可以看到什么,不能看到什么;例如,pid namespace 限制了进程能看到哪些其他的进程;network namespace 限制了进程能看到哪些网络设备等等。
  2. cgroup:一种限制资源使用量的机制,例如一个进程最多能使用多少内存、磁盘 I/O、CPU 等等。
  3. capabilities:拆分 root privilege,精细化用户/进程权限
  4. seccomp:内核的一种安全计算 (secure computation)机制,例如限制进程只能使用某些特定的系统调用。

以上几种机制中,cgroups 是通过文件系统完成的,其他几种都是通过系统调用完成的。

1.2 Namespaces

这里只是简单列下,方面后面理解代码。其实除了 UTS,其他 namespace 都能直接从名字看出用途,

  1. Mount:挂载空间,例如指定某个目录作为进程看到的系统跟目录、独立挂载 /proc /sys 等;
  2. PID:进程空间,例如最大进程数量是独立的;
  3. Network (netns):网络空间,例如能看到的网络设备是独立的,部分网络参数是独立的;
  4. IPC (inter-process communication)
  5. UTS (UNIX Time-Sharing):让进程可以有独立的 host name 和 domain name
  6. User ID:用的不多,例如 ubuntu 22.04 / kernel 6.1 默认还是关闭的。
  7. cgroup:独立的 cgroup 文件系统和资源管控;
  8. Time:kernel 5.6+

2 500 行 C 代码创建一个容器:ctn-d.c

这里的代码来自 Linux containers in 500 lines of code (2016)。 原文基于 Kernel 4.6,本文做了一些更新和重构。

2.0 总览:ctn-d.c

int main (int argc, char **argv) {
    // Step 1: parse CLI parameters, validate Linux kernel/cpu/..., generate hostname for container

    // Step 2: setup a socket pair for container sending messages to the parent process
    socketpair(AF_LOCAL, SOCK_SEQPACKET, 0, sockets);
    fcntl(sockets[0], F_SETFD, FD_CLOEXEC);
    config.fd = sockets[1];

    // Step 3: allocate stack space for `execve()`
    stack = malloc(STACK_SIZE));

    // Step 4: setup cgroup for the container for resource isolation
    setup_cgroups(&config);

    // Step 5: launch container 将 mount, pid, ipc, network device, hostname/domainname 放到独立的 namespace
    int flags = CLONE_NEWNS | CLONE_NEWCGROUP | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWUTS;
    child_pid = clone(child, stack + STACK_SIZE, flags | SIGCHLD, &config);

    // Step 6: error handling and cleanup
}
  1. 初始化

    1. 解析命令行参数
    2. 检查内核版本、CPU 架构等等
    3. 给容器随机生成一个 hostname
  2. 创建一个 socket pair,用于容器进程和主进程之间的通信;
  3. 分配栈空间,供后面 execve() 执行容器进程时使用;
  4. 设置 cgroups 资源隔离;
  5. 通过 clone 创建子进程,在里面通过 namespace/capabilities/seccomp 等技术实现资源管控和安全。

核心科技在 4 和 5。下面分别来看下各步骤做的事情。

另外需要说明,原作者的这段程序主要是为了研究容器安全,因此一些步骤是出于安全目的而做的, 如果忽略安全考虑,要实现类似的“创建一个容器”效果,代码可以短很多,网上也有很多例子。 本文还是基本沿用原作者的版本,但一些安全方面不详细展开,有需要可参考原文 [1]。

2.1 初始化

程序提供了三个命令行参数,

  • -u <uid> 以什么用户权限运行;
  • -m <ctn rootfs path> 镜像解压之后的目录;
  • -c <command> 容器启动后运行的命令。

程序会给容器随机生成一个 hostname,就像用 docker run 不指定容器名字的话,docker 也会自动生成一个名字。

2.2 创建 socket pair,供容器进程和父进程通信

    if (socketpair(AF_LOCAL, SOCK_SEQPACKET, 0, sockets)) {
        fprintf(stderr, "socketpair failed: %m\n");
        goto error;
    }
    if (fcntl(sockets[0], F_SETFD, FD_CLOEXEC)) {
        fprintf(stderr, "fcntl failed: %m\n");
        goto error;
    }
    config.fd = sockets[1];

子进程(容器进程)需要发消息给父进程,因此初始化一个 socketpair。 容器进程会告诉父进程是否需要设置 uid/gid mappings, 如果需要,就会执行 setgroups/setresgid/setresuid。 这些是权限相关的。

2.3 分配栈空间,供随后 execve() 执行容器启动进程使用

#define STACK_SIZE (1024 * 1024)
    char *stack = 0;
    if (!(stack = malloc(STACK_SIZE))) {
        fprintf(stderr, "=> malloc failed, out of memory?\n");
        goto error;
    }

这是后面通过 execve() 来执行容器内进程的准备工作,是标准 Linux API,也不展开。

2.4 创建 cgroup,为容器设置资源限额

cgroups 可以限制进程和进程组的资源使用量,避免有问题的容器进程影响整个系统。 它有 v1 和 v2 两个版本,

简单起见,程序里使用的 cgroupv1

struct cgrp_control {
    char control[256];
    struct cgrp_setting {
        char name[256];
        char value[256];
    } **settings;
};
struct cgrp_setting add_to_tasks = { // echo 0 > /sys/fs/cgroup/<controller>/<hostname>/tasks
    .name = "tasks",
    .value = "0"
};

struct cgrp_control *cgrps[] = {
    & (struct cgrp_control) {
        .control = "memory",
        .settings = (struct cgrp_setting *[]) {
            & (struct cgrp_setting) {
                .name = "memory.limit_in_bytes",
                .value = "1073741824"
            },
            /* & (struct cgrp_setting) { */
            /* 	.name = "memory.kmem.limit_in_bytes", */
            /* 	.value = "1073741824" */
            /* }, */
            &add_to_tasks,
            NULL
        }
    },
    & (struct cgrp_control) {
        .control = "cpu",
        .settings = (struct cgrp_setting *[]) {
            & (struct cgrp_setting) {
                .name = "cpu.shares",
                .value = "256" // CPU shares
            },
            &add_to_tasks,
            NULL
        }
    },
    & (struct cgrp_control) {
        .control = "pids",
        .settings = (struct cgrp_setting *[]) {
            & (struct cgrp_setting) {
                .name = "pids.max",
                .value = "64"
            },
            &add_to_tasks,
            NULL
        }
    },
    /* & (struct cgrp_control) { */
    /* 	.control = "blkio", */
    /* 	.settings = (struct cgrp_setting *[]) { */
    /* 		& (struct cgrp_setting) { */
    /* 			.name = "blkio.weight", */
    /* 			.value = "10" */
    /* 		}, */
    /* 		&add_to_tasks, */
    /* 		NULL */
    /* 	} */
    /* }, */
    NULL
};

int setup_cgroups(struct ctn_config *config) {
    for (struct cgrp_control **cgrp = cgrps; *cgrp; cgrp++) {
        char dir[PATH_MAX] = {0};
        snprintf(dir, sizeof(dir), "/sys/fs/cgroup/%s/%s", (*cgrp)->control, config->hostname);
        mkdir(dir, S_IRUSR | S_IWUSR | S_IXUSR);
        for (struct cgrp_setting **setting = (*cgrp)->settings; *setting; setting++) {
            char path[PATH_MAX] = {0};
            int fd = 0;
            snprintf(path, sizeof(path), "%s/%s", dir, (*setting)->name);
            fd = open(path, O_WRONLY);
            write(fd, (*setting)->value, strlen((*setting)->value));
            close(fd);
        }
    }

    setrlimit(RLIMIT_NOFILE, & (struct rlimit) {.rlim_max = 64, .rlim_cur = 64,});
    return 0;
}

设置 cgroups 的逻辑比较简单,基本上就是创建 cgroup 目录, 以及往 cgroups 配置文件写入配置。 上面的程序配置了以下几个资源限额:

  1. /sys/fs/cgroup/memory/$hostname/memory.limit_in_bytes=1GB:容器进程及其子进程使用的总内存不超过 1GB;
  2. /sys/fs/cgroup/memory/$hostname/memory.kmem.limit_in_bytes=1GB:容器进程及其子进程使用的总内存不超过 1GB;
  3. /sys/fs/cgroup/cpu/$hostname/cpu.shares=256:CPU 总 slice 是 1024,因此限制进程最多只能占用 1/4 CPU 时间;
  4. /sys/fs/cgroup/pids/$hostname/pid.max=64:允许容器进程及其子进程最多拥有 64 个 PID;
  5. /sys/fs/cgroup/blkio/$hostname/weight=50:确保容器进程的 IO 优先级比系统其他进程低。
  6. 降低文件描述符的 hard limit。fd 与 pid 类似,都是 per-user 的。这里设置上限之后, 后面还会 drop CAP_SYS_RESOURCE,因此容器内的用户是改不了的。

另外,程序还通过 add_to_tasks{memory,cpu,blkio,pids}/$hostname/tasks 文件写入 0,即实现下面的效果:

$ echo 0 > /sys/fs/cgroup/<controller>/<hostname>/tasks

tasks 里面存放的是受这个 cgroup 管控的进程 ID(PID)列表, 0 是一个特殊值,表示“执行当前写入操作的进程”

2.5 clone() 启动子进程,运行容器

fork() 是常见的创建子进程的方式,但它背后调用的是 clone(),后者暴露的细节更多, 也是创建容器的关键技术。 我们创建的子进程要能 mount 自己的根目录、设置自己的 hostname 以及做其他一些事情, 要实现这个效果,需要向 clone() 传递相应的 flags 参数

    int flags = CLONE_NEWNS | CLONE_NEWCGROUP | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNET | CLONE_NEWUTS;
    child_pid = clone(child, stack + STACK_SIZE, flags | SIGCHLD, &config);
  • flags 规定了这个新创建的子进程要有多个独立的 namespace;
  • x86 平台栈是向下增长的,因此将栈指针指向 stack+STACK_SIZE 作为起始位置;
  • 最后还还加上了 SIGCHLD 标志位,这样就能 wait 子进程了。

下面是子进程内做的事情:

int child(void *arg) {
    struct ctn_config *config = arg;
    sethostname(config->hostname, strlen(config->hostname);

    setup_mounts(config);
    setup_userns(config);
    setup_capabilities();
    setup_seccomp();
    close(config->fd);

    execve(config->argv[0], config->argv, NULL);
}

具体来看下各步骤。

2.5.1 sethostname():感知 UTS namespace

sethostname/gethostname 是 Linux 系统调用,用于设置或获取主机名(hostname)。

hostname 是 UTS namespace 隔离的, 刚才创建子进程时指定了要有独立的 UTS namespace, 因此在这个子进程内设置 hostname 时,影响的只是这个 UTS namespace 内(即这个容器内)所有进程 看到的 hostname。

2.5.2 setup_mounts():安全考虑,unmount 不需要的目录

flags 指定了子进程有独立的 mount namespace 中。 按照最小权限原则,我们应该 unmount 掉子进程不应访问的目录

int setup_mounts(struct ctn_config *config) {
    // remounting everything with MS_PRIVATE...
    mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL));

    // making a temp directory and a bind mount there...
    char mount_dir[] = "/tmp/tmp.XXXXXX";
    mkdtemp(mount_dir);
    mount(config->mount_dir, mount_dir, NULL, MS_BIND | MS_PRIVATE, NULL);

    char inner_mount_dir[] = "/tmp/tmp.XXXXXX/oldroot.XXXXXX";
    memcpy(inner_mount_dir, mount_dir, sizeof(mount_dir) - 1);
    mkdtemp(inner_mount_dir);

    // pivoting root...
    syscall(SYS_pivot_root, mount_dir, inner_mount_dir);

    char *old_root_dir = basename(inner_mount_dir);
    char old_root[sizeof(inner_mount_dir) + 1] = { "/" };
    strcpy(&old_root[1], old_root_dir);

    // unmounting old_root
    chdir("/");
    umount2(old_root, MNT_DETACH);
    rmdir(old_root);

    return 0;
}

步骤:

  1. 使用 MS_PRIVATE 重新挂载所有内容;
  2. 创建一个临时目录,并在其中创建一个子目录;
  3. 将用户命令行指定的目录(config->mount_dir)bind mount 到临时目录上;
  4. 使用 pivot_root 将 bind mount 作为根目录,并将旧的根目录挂载到内部临时目录上。 pivot_root 是一个系统调用,允许交换 / 处的挂载点与另一个挂载点。
  5. 卸载旧的根目录,删除内部临时目录。

2.5.3 setup_userns()

user namespace 实际中用的还不多,就不展开了。

int setup_userns(struct ctn_config *config) {
    // trying a user namespace...
    int has_userns = !unshare(CLONE_NEWUSER);
    write(config->fd, &has_userns, sizeof(has_userns));

    if (has_userns) {
        fprintf(stderr, "done.\n");
    } else {
        fprintf(stderr, "unsupported? continuing.\n");
    }

    // switching to uid %d / gid %d...", config->uid, config->uid
    setgroups(1, & (gid_t) { config->uid });
    setresgid(config->uid, config->uid, config->uid);
    setresuid(config->uid, config->uid, config->uid);
}

User namespace 是后来引入的一种新 namespace,但 enable user namespace 编译内 核很复杂,另外它在系统层面(system-wide)改变了 capabilities 的语义,相比解决 问题,它会带来更多的问题。更多信息见 Understanding and Hardening Linux Containers 这个功能目前还是默认关闭的。

$ sysctl kernel.unprivileged_userns_clone
sysctl: cannot stat /proc/sys/kernel/unprivileged_userns_clone: No such file or directory

2.5.4 setup_capabilities():禁用部分 capabilities

同样,基于最小权限原则,drop 所有不需要的 capabilities,

int setup_capabilities() {
    // dropping capabilities...
    int drop_caps[] = {
        CAP_AUDIT_CONTROL,
        CAP_AUDIT_READ,
        CAP_AUDIT_WRITE,
        CAP_BLOCK_SUSPEND,
        CAP_DAC_READ_SEARCH,
        CAP_FSETID,
        CAP_IPC_LOCK,
        CAP_MAC_ADMIN,
        CAP_MAC_OVERRIDE,
        CAP_MKNOD,
        CAP_SETFCAP,
        CAP_SYSLOG,
        CAP_SYS_ADMIN,
        CAP_SYS_BOOT,
        CAP_SYS_MODULE,
        CAP_SYS_NICE,
        CAP_SYS_RAWIO,
        CAP_SYS_RESOURCE,
        CAP_SYS_TIME,
        CAP_WAKE_ALARM
    };
    size_t num_caps = sizeof(drop_caps) / sizeof(*drop_caps);

    // bounding...
    for (size_t i = 0; i < num_caps; i++) {
        prctl(PR_CAPBSET_DROP, drop_caps[i], 0, 0, 0);
    }

    // inheritable...
    cap_t caps = NULL;
    if (!(caps = cap_get_proc())
            || cap_set_flag(caps, CAP_INHERITABLE, num_caps, drop_caps, CAP_CLEAR)
            || cap_set_proc(caps)) {
        if (caps) cap_free(caps);
        return 1;
    }

    cap_free(caps);
    return 0;
}

2.5.5 setup_seccomp():禁用部分系统调用

这里将一些有安全风险的系统调用都放到了黑名单,这可能不是最好的处理方式,但能够 让大家非常直观地看到底层是如何通过 seccomp 保证计算安全的:

int setup_seccomp() {
    scmp_filter_ctx ctx = NULL;

    // filtering syscalls...
    if (!(ctx = seccomp_init(SCMP_ACT_ALLOW))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(chmod), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, S_ISUID, S_ISUID))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(chmod), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, S_ISGID, S_ISGID))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(fchmod), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, S_ISUID, S_ISUID))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(fchmod), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, S_ISGID, S_ISGID))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(fchmodat), 1, SCMP_A2(SCMP_CMP_MASKED_EQ, S_ISUID, S_ISUID))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(fchmodat), 1, SCMP_A2(SCMP_CMP_MASKED_EQ, S_ISGID, S_ISGID))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(unshare), 1, SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(clone), 1, SCMP_A0(SCMP_CMP_MASKED_EQ, CLONE_NEWUSER, CLONE_NEWUSER))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(ioctl), 1, SCMP_A1(SCMP_CMP_MASKED_EQ, TIOCSTI, TIOCSTI))
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(keyctl), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(add_key), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(request_key), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(ptrace), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(mbind), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(migrate_pages), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(move_pages), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(set_mempolicy), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(userfaultfd), 0)
            || seccomp_rule_add(ctx, SCMP_FAIL, SCMP_SYS(perf_event_open), 0)
            || seccomp_attr_set(ctx, SCMP_FLTATR_CTL_NNP, 0)
            || seccomp_load(ctx)) {
                if (ctx) seccomp_release(ctx);
                return 1;
            }

    // all pass, return success
    seccomp_release(ctx);
    return 0;
}

docker 官方有一些相关文档,感兴趣可移步。

2.5.6 execve():执行指定的容器启动命令

前面资源视图隔离(namespace)、资源限额(cgroups)、文件目录(mount)、权限(capabilities)、安全(seccomp)等工作 都做好之后,就可以启动用户指定的容器进程了(类似于 docker 中的 entrypoint 或 command):

    execve(config->argv[0], config->argv, NULL);

至此,如果一切正常,容器进程就起来了。接下来我们还要容器进程与主进程的通信, 类似于真实环境中 containerd 处理来自具体容器的消息。

2.6 处理容器事件:user namespace 相关

#define USERNS_OFFSET 10000
#define USERNS_COUNT 2000

int handle_child_uid_map (pid_t child_pid, int fd) {
    int uid_map = 0;
    int has_userns = -1;
    if (read(fd, &has_userns, sizeof(has_userns)) != sizeof(has_userns)) {
        fprintf(stderr, "couldn't read from child!\n");
        return -1;
    }

    if (has_userns) {
        char path[PATH_MAX] = {0};
        for (char **file = (char *[]) { "uid_map", "gid_map", 0 }; *file; file++) {
            if (snprintf(path, sizeof(path), "/proc/%d/%s", child_pid, *file) > sizeof(path)) {
                fprintf(stderr, "snprintf too big? %m\n");
                return -1;
            }
            fprintf(stderr, "writing %s...", path);
            if ((uid_map = open(path, O_WRONLY)) == -1) {
                fprintf(stderr, "open failed: %m\n");
                return -1;
            }
            if (dprintf(uid_map, "0 %d %d\n", USERNS_OFFSET, USERNS_COUNT) == -1) {
                fprintf(stderr, "dprintf failed: %m\n");
                close(uid_map);
                return -1;
            }
            close(uid_map);
        }
    }

    if (write(fd, & (int) { 0 }, sizeof(int)) != sizeof(int)) {
        fprintf(stderr, "couldn't write: %m\n");
        return -1;
    }
    return 0;
}

3 测试

3.1 下载官方 busybox 容器镜像

3.1.1 下载镜像

这里用 docker 从官方 pull,然后保存为本地 tar 文件:

$ dk pull busybox:1.36
$ dk save busybox:1.36 -o busybox-1.36.tar

3.1.2 解压到本地目录

将 tar 文件解压,

$ sudo tar xvf busybox-1.36.tar
244ed32d6820f8861f94beda2456fa7032a832a4e7ed7e72fa66b802518f9adc/
244ed32d6820f8861f94beda2456fa7032a832a4e7ed7e72fa66b802518f9adc/VERSION
244ed32d6820f8861f94beda2456fa7032a832a4e7ed7e72fa66b802518f9adc/json
244ed32d6820f8861f94beda2456fa7032a832a4e7ed7e72fa66b802518f9adc/layer.tar
f5fb98afcf9f5c6e8e069557f605b15b52643166c82ac5695f49fc6b0be04ee8.json
manifest.json
repositories

其中的 layer.tar 是镜像本身,其他都是元数据。 我们先创建一个目录 ~/busybox-1.36-rootfs(名字随便), 然后将 layer.tar 解压到这个目录:

$ mkdir ~/busybox-1.36-rootfs #
$ tar xvf busybox-1.36/244ed32d6820f8861f94beda2456fa7032a832a4e7ed7e72fa66b802518f9adc/layer.tar -C ~/busybox-1.36-rootfs

得到的就是一个看着像 一台 Linux node 根目录的文件夹了:

$ ls ~/busybox-1.36-rootfs/
bin  dev  etc  home  lib  lib64  root  tmp  usr  var

我们一会就是用这个目录作为容器根目录来启动容器。

3.2 编译

这里是在 ubuntu22 kernel 6.1 上编译:

$ sudo apt install libseccomp-dev libcap-dev -y
$ gcc -Wl,--no-as-needed -lcap -lseccomp ctn-d.c

得到 a.out

3.3 测试

3.3.1 运行 /bin/sh:交互式

指定解压之后的 busybox 容器镜像目录作为容器根目录(-m ~/busybox-rootfs-1.36/), 以 root 权限(-u 0)运行 shell(-c /bin/sh):

$ sudo ./a.out -u 0 -m ~/busybox-rootfs-1.36/ -c /bin/sh
=> validating Linux version...6.1.11-060111-generic on x86_64.
=> generating hostname for container ... 1cd132c-ten-of-pentacles done
=> setting cgroups...memory...cpu...pids...done.
=> setting rlimit...done.
=> remounting everything with MS_PRIVATE...remounted.
=> making a temp directory and a bind mount there...
=> temp directory /tmp/tmp.j1SPbh
done.
=> pivoting root...done.
=> unmounting /oldroot.p216lf...done.
=> trying a user namespace...writing /proc/3544398/uid_map...writing /proc/3544398/gid_map...done.
=> switching to uid 0 / gid 0...done.
=> dropping capabilities...bounding...inheritable...done.
=> filtering syscalls...done.
/ #

最后一行的命令提示符变了,这就表示已经进入到了容器内的 shell, 接下来可以执行几个 busybox 镜像内有的命令来测试:

/ # whoami                       # <-- 已经进入到容器内
root
/ # hostname
17bb49-death
/ # ls
bin    dev    etc    home   lib    lib64  root   tmp    usr    var

最后通过 exit 命令正常退出 shell,由于这是容器主进程,因此 shell 退出之后容器也就退出了:

/ # exit                         # <-- 退出容器
=> cleaning cgroups...done.

3.3.2 运行 /bin/echo:一次性执行

再测试下直接在容器内执行一段任务(非交互式),运行完就会自动退出:

$ sudo ./a.out -u 0 -m ~/busybox-rootfs-1.36/ -c /bin/echo "hello from inside container"
=> validating Linux version...6.1.11-060111-generic on x86_64.
=> generating hostname for container ... 1cd132c-ten-of-pentacles done
=> setting cgroups...memory...cpu...pids...done.
=> setting rlimit...done.
=> remounting everything with MS_PRIVATE...remounted.
=> making a temp directory and a bind mount there...
=> temp directory /tmp/tmp.j1SPbh
done.
=> pivoting root...done.
=> unmounting /oldroot.p216lf...done.
=> trying a user namespace...writing /proc/3544398/uid_map...writing /proc/3544398/gid_map...done.
=> switching to uid 0 / gid 0...done.
=> dropping capabilities...bounding...inheritable...done.
=> filtering syscalls...done.
hello from inside container  #   <-- 容器内执行 echo,执行完就退出了
=> cleaning cgroups...done.

3.3.3 将自己的程序放到 busybox 容器中执行

如果你有一些没什么依赖的程序(或者依赖已经在 busybox 中了), 那将这样的程序放到 busybox 容器镜像中也是可以运行的。下面看个 golang 的例子。

源码:

package main

import "fmt"

func main() {
        fmt.Println("Hello World from Golang")
}

编译,

$ go build hello-world.go

编译生成的可执行文件没有任何依赖,因此我们将它放到 busybox 容器的可执行文件目录:

$ mv hello-world ~/busybox-rootfs-1.36/bin/

然后在容器 shell 内尝试执行:

/ # hello-world
Hello World from Golang

成功!

4 与真实系统对比:ctn-d vs. containerd+runc

通过以上内容,可以看到我们 500 来行的 C 程序可以实现创建和运行(简易版)Linux 容器的功能。 那么,在实际上它对应的是容器技术栈中的哪些组件呢?

4.1 角色和位置

4.1.1 简化版 containerd+runc

k8s 中容器相关的组件。dockerd 是可选的,较新版本的 k8s 中已经把 dockerd 这一层去掉了。[3]

上图是 k8s 创建 pod 所涉及的一些核心组件,大家对 docker/containerd 比较熟悉, 但其实 containerd 并不是一线干活的,而是跟 kubelet/docker 一样,都是负责管理工作的“经理”(container manager), 真正干活的是更底层的 runc

从软件栈上来说,本文的程序其实就是一个简化版的 containerd+runc。 参考 [3] 中 k8s 容器的创建过程中可以看到,

  • runc 是基于一个 config.json 创建容器的,本文程序中的 config 其实就是 config.json 的一个极其简化版。
  • k8s 创建容器时,是 containerd 通过 config.json 传给 runc 的;本文的程序也支持这些配置,只是简单起见在程序里 hardcode 了。

下面是一个真实 k8s pod 对应的 config.json:

(node1) $ cat /run/containerd/io.containerd.runtime.v1.linux/moby/eedd6341c/config.json | jq
{
  "process": {
    "user": {
      "uid": 0,
      "gid": 0
    },
    "args": [ ],
    ...
    "memory": {
      "limit": 2147483648,
      "swap": 2147483648,
    },
    "cpu": {
      "shares": 1024,
      "quota": 100000,
      "period": 100000,
      "cpus": "0-31"
    },
    "blockIO": {
      "weight": 0
    },
    "cgroupsPath": "/kubepods/besteffort/pod187acdb9/eedd6341c",
    "namespaces": [
      {
        "type": "mount"
      },
      {
        "type": "network"
      },
      ...
    ],
    "maskedPaths": [
      "/proc/asound",
      "/proc/acpi",
      "/proc/kcore",
      "/proc/keys",
      "/proc/latency_stats",
      "/proc/timer_list",
      "/proc/timer_stats",
      "/proc/sched_debug",
      "/proc/scsi",
      "/sys/firmware"
    ],
    "readonlyPaths": [
      "/proc/bus",
      "/proc/fs",
      "/proc/irq",
      "/proc/sys",
      "/proc/sysrq-trigger"
    ]
  }
}

4.1.2 容器运行时(container runtime)

如果根据下面的定义,本文的程序其实就是一个容器运行时

The container runtime is a software package that knows how to leverage specific features on a supported operating system to create a space to run the specified container image.

What are container runtimes?

4.2 自动解压镜像创建容器

本文创建容器时,是先手动将一个 docker 镜像解压到本地目录,然再指定这个目录创建容器。 这个过程用程序来实现也是很简单的,只需要对我们的程序稍加扩展就行, 就能直接指定 docker 镜像创建容器了。

不过这个过程(镜像打包和解压)很容易引入安全漏洞, 社区的几个相关 bug

4.3 容器网络

这个要花的篇幅就比较长了,常规网络方案来说分几步:

  1. 在宿主机上创建一个 Linux bridge;
  2. 创建一个 veth pair,一端连接到 Linux bridge,一端放到容器的 network namespace 内;
  3. 配置 IP/MAC/NAT 规则等,让容器网络能连通。

可以用 net_prio cgroup controller 对网络资源进行限额。

编程参考

// Compile: "gcc -Wall -Werror -static subverting_networking.c"
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/sockios.h>

int main (int argc, char **argv) {
	if (unshare(CLONE_NEWUSER | CLONE_NEWNET)) {
		fprintf(stderr, "++ unshare failed: %m\n");
		return 1;
	}
	/* this is how you create a bridge... */
	int sock = 0;
	if ((sock = socket(PF_LOCAL, SOCK_STREAM, 0)) == -1) {
		fprintf(stderr, "++ socket failed: %m\n");
		return 1;
	}
	if (ioctl(sock, SIOCBRADDBR, "br0")) {
		fprintf(stderr, "++ ioctl failed: %m\n");
		close(sock);
		return 1;
	}
	close(sock);
	fprintf(stderr, "++ success!\n");
	return 0;
}

4.4 特殊目录

我们容器内的目录

# ls /
bin    dev    etc    home   lib    lib64  root   tmp    usr    var

就是busybox 镜像解压之后的目录,没有任何新增或减少。 对比宿主机根目录

# In the container
# ls /
bin        dev  etc  home  lib  lib64                         root                       tmp  usr  var

# On the node
$ ls /
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

可以看到,少了 boot/mnt/opt/proc/run/sbin/srv/sys 这几个目录。

这里讨论下其中最重要的两个:/proc/sys,它们不是普通目录,也不是普通文件系统, 所以需要容器运行时单独处理,在创建容器时挂载进去。Linux node 上执行,

$ mount | egrep "(/sys|/proc)"
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
...

可以看到它们是两种不同的特殊文件系统:

  • /procproc 类型,但一般也称为 procfs
  • /syssysfs 类型。

他们都是内核暴露给用户空间的接口,绝大部分都是只读,获取内核信息。

4.4.1 /proc (procfs)

从发展过程来说 [4],先有的 /proc,后有的 /sys

/proc 这个名字上也可以看成,主要用于存储进程信息,例如每个进程都有一个对应的 /proc/<pid>/ 目录, 很多系统工具例如 ps/top/free 之类的,也是通过读取 /proc 来获取系统信息的。 但由于缺乏统一设计和管理/proc 越来越庞杂,有失控的趋势, 因此后来又设计了一个全新的文件系统,这就是 /sys

4.4.2 /sys (sysfs)

/sys 更加结构化,组织得更好。 较新的基础设施都是基于 sysfs 的,但开源产品惯性太大, Linux 惯性尤其大,所以 /proc 还是继续维护和使用的。

4.4.3 容器 /proc 存在的问题或挑战

如果看 docker/containerd/runc 之类的容器运行时,会发现它是把宿主机 /proc 挂载到了容器, 这会导致什么问题呢? 刚才说了,/proc 里面都是进程的统计信息,因此直接把宿主机 /proc 暴露给容器,将带来几方面问题:

  1. 容器内能看到宿主机以及其他容器的进程信息(虽然原则上是只读的),存在安全风险;
  2. 容器内读取某些文件,例如 /proc/meminfo,本来是希望得到容器自己的内存信息,但实际上拿到的是宿主机的,因此数据不准,

    • 人看了或很困惑,但这还是不是最重要的,
    • 如果上层应用直接依赖这个数据,比如监控系统或一些 Java 业务应用,那会直接导致业务问题。
  3. 资源视图错误:结合 1 & 2,容器看到的东西比自己应该看到的多,或者看到的资源量比自己真实能用的资源大

但要解决这个问题又是比较麻烦的,因为容器的设计目的只是通过必要的资源隔离来完成计算任务, 并不是让它像虚拟机一样作为独立机器提供给用户。 那实际中是怎么解决呢?

5 容器 /proc 信息不准解决方式

5.1 案例研究:为什么容器 /proc/meminfo 对,但 /proc/cpuinfo 不对

我们通过一个真实 k8s 环境中的例子来倒查一下。

5.1.1 查看容器 /proc/cpuinfo/proc/meminfo

在一个 k8s+docker+containerd+runc 的环境,挑一个容器,

$ k get pod -n <ns> <pod> -o yaml
...
    resources:
      requests:
        cpu: 125m
        memory: 751619276800m
      limits:
        cpu: "1"
        memory: 1Gi

这里的意思是,这个容器需要 125m = 125/1000 = 1/8 个 CPU,751 MB 内存; 最多能使用 1 个 CPU,1GB 内存

在容器里面看 /proc 挂载信息:

(container) # cat /proc/mounts | grep proc
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
lxcfs /proc/meminfo fuse.lxcfs ro,relatime,user_id=0,group_id=0,allow_other 0 0
lxcfs /proc/diskstats fuse.lxcfs ro,relatime,user_id=0,group_id=0,allow_other 0 0
proc /proc/bus proc ro,relatime 0 0
proc /proc/fs proc ro,relatime 0 0
proc /proc/irq proc ro,relatime 0 0
proc /proc/sys proc ro,relatime 0 0
proc /proc/sysrq-trigger proc ro,relatime 0 0
tmpfs /proc/acpi tmpfs ro,relatime 0 0
tmpfs /proc/kcore tmpfs rw,nosuid,size=65536k,mode=755 0 0
tmpfs /proc/keys tmpfs rw,nosuid,size=65536k,mode=755 0 0
tmpfs /proc/timer_list tmpfs rw,nosuid,size=65536k,mode=755 0 0
tmpfs /proc/sched_debug tmpfs rw,nosuid,size=65536k,mode=755 0 0
tmpfs /proc/scsi tmpfs ro,relatime 0 0

然后通过 /proc 查看 CPU 和内存:

(container) # cat /proc/cpuinfo
processor       : 0
vendor_id       : GenuineIntel
...
processor       : 31            # 32 CPU
vendor_id       : GenuineIntel

(container) # cat /proc/meminfo
MemTotal:        1048576 kB     # 1GB
MemFree:          893416 kB
MemAvailable:     893416 kB
...

可以看到,Memory 是对的,CPU 不对,实际上是宿主机的 CPU 数量。 我们接下来是看看为什么会这样。

5.1.2 定位容器 /proc/cpuinfo/proc/meminfo 挂载来源:/var/lib/lxcfs/

在宿主机上查看容器的 config.json 文件, 前面提到过,runc 就是根据这个文件创建容器的,

$ cat /run/containerd/io.containerd.runtime.v1.linux/moby/d32b1bf.config.json | jq .

里面能看到文件的挂载信息:

    {
      "destination": "/proc/meminfo",
      "type": "bind",
      "source": "/var/lib/lxcfs/proc/meminfo",
      "options": [
        "rbind",
        "ro",
        "rprivate"
      ]
    },
    {
      "destination": "/proc/diskstats",
      "type": "bind",
      "source": "/var/lib/lxcfs/proc/diskstats",
      "options": [
        "rbind",
        "ro",
        "rprivate"
      ]
    },

可以看到有 meminfo 的挂载,但是没有 cpuinfo 的挂载。

这个配置是从 k8s 一路传下来的,因此我们去 k8s 里再看看 pod spec。

5.1.3 查看 Pod lxcfs 路径挂载

$ k get pod xxx -n <ns> <pod> -o yaml
    volumeMounts:
    - mountPath: /proc/meminfo
      name: meminfo
      readOnly: true
    - mountPath: /proc/diskstats
      name: diskstats
      readOnly: true

  volumes:
  - hostPath:
      path: /var/lib/lxcfs/proc/meminfo
      type: File
    name: meminfo
  - hostPath:
      path: /var/lib/lxcfs/proc/diskstats
      type: File
    name: diskstats

确实是 pod 里面指定的,而且只有 meminfo 的挂载,没有 cpuinfo 的挂载。

到这里答案基本就猜到了:

  • 信息正确的 /proc/meminfo,是通过 lxcfs 挂载来实现的;
  • /proc/cpuinfo 不正确是因为没有配置 lxcfs 或者不支持。

5.2 解决方式总结

前面已经分析了 /proc 不准的问题,以及把它搞准所面临的挑战。 实际中怎么解决的呢?分两种。

5.2.1 从上层解决:应用主动适配 cgroup

应用程序主动适配 cgroup,需要的信息都去容器的 /sys/fs/cgroup 目录读取,不要去容器的 /proc/ 目录读。

很多监控采集程序都是这么做的,比如 k8s 用的 cadvisor,采集 cpuload,

5.2.2 从底层解决:tmpfs/lxcfs

为了兼容用户程序,用 tmpfs/lxcfs 等特殊文件系统挂载来模拟部分 /proc 文件,也是一种解决方式。

5.3 tmpfs

tmpfs (Temporary File System) 是很多 Unix-like 操作系统都支持的一种临时文件存储方案。

  • 看起来是挂载的文件系统(mounted file system);
  • 数据在内存(非持久存储),

类似的还有 RAM disk, 看起来是一个虚拟磁盘,有磁盘文件系统,但其实数据在内存中。

从上面容器的输出可以看成,下面的文件都是用 tmpfs 覆盖的,

(container) # cat /proc/mounts | grep proc
..
tmpfs /proc/acpi tmpfs ro,relatime 0 0                            # 只读
tmpfs /proc/kcore tmpfs rw,nosuid,size=65536k,mode=755 0 0        # 读写
tmpfs /proc/keys tmpfs rw,nosuid,size=65536k,mode=755 0 0         # 读写
tmpfs /proc/timer_list tmpfs rw,nosuid,size=65536k,mode=755 0 0   # 读写
tmpfs /proc/sched_debug tmpfs rw,nosuid,size=65536k,mode=755 0 0  # 读写

5.4 lxcfs

5.4.1 lxcfs+fuse 基本工作原理

lxcfs 是一个简单的用户空间文件系统,设计目的就是 workaround 当前的一些 Linux 内核限制。 提供两个主要东西:

  • 一些文件:能 bind-mount/proc,做到 CGroup-aware;
  • 一个 cgroupfs-like tree:container aware。

代码基于 libfuse 用纯 C 编写。

上面的容器输出里:

(container) # cat /proc/mounts | grep proc
...
lxcfs /proc/meminfo fuse.lxcfs ro,relatime,user_id=0,group_id=0,allow_other 0 0   # 只读
lxcfs /proc/diskstats fuse.lxcfs ro,relatime,user_id=0,group_id=0,allow_other 0 0 # 只读

用 lxcfs 接管了 /proc/meminfo/proc/diskstats,但是没有接管 /proc/cpuinfo,这也是为什么 我们前面看到 Memory 是对的,CPU 数量不对。

lxcfs 工作原理说起来非常简单:node 上起一个 daemon 进程, 劫持容器内的 /proc 读操作, 通过 fuse 文件系统转到对容器 cgroupfs 的读写,然后将信息返回给请求方,如下图所示:

Fig. lxcfs/fuse workflow: how a read operation is handled

5.4.2 组件

只有一个 daemon 进程,运行在每个 node 上:

(node) $ systemctl status lxcfs
● lxcfs.service - FUSE filesystem for LXC
   Loaded: loaded (/usr/lib/systemd/system/lxcfs.service; enabled; vendor preset: disabled)
   Active: active (running)

查看配置:

$ cat /usr/lib/systemd/system/lxcfs.service
[Unit]
Description=FUSE filesystem for LXC
ConditionVirtualization=!container
Before=lxc.service
Documentation=man:lxcfs(1)

[Service]
ExecStart=/usr/bin/lxcfs /var/lib/lxcfs/
KillMode=process
Restart=on-failure
ExecStopPost=-/bin/fusermount -u /var/lib/lxcfs # <-- fuse 挂载路径
Restart=always
RestartSec=3
Delegate=yes

5.4.3 提供的目录挂载范围

默认 fuse 挂载路径 /var/lib/lxcfs/

$ tree /var/lib/lxcfs/ -L 2
/var/lib/lxcfs/
├── cgroup
│   ├── blkio
│   ├── cpu,cpuacct
│   ├── cpuset
│   ├── devices
│   ├── freezer
│   ├── hugetlb
│   ├── memory
│   ├── name=systemd
│   ├── net_cls,net_prio
│   ├── perf_event
│   ├── pids
│   └── rdma
└── proc
    ├── cpuinfo
    ├── diskstats
    ├── meminfo
    ├── stat
    ├── swaps
    └── uptime

6 特殊容器运行时

这里列几个生产环境在用但又比较特殊的容器运行时。

6.1 NVIDIA container runtime

老的 nvidia-docker 已经不再维护,官方建议用新方案 github.com/NVIDIA/nvidia-container-toolkit

Fig. A Kubernetes node with nvidia container runtime

注册了 runcPreStart hook,在这一步去修改容器的配置,例如挂载 GPU 相关设备。 更多信息见官方 Architecture Overview

OCI v1.1.0 PreStart Hook:

The prestart hooks MUST be called as part of the create operation after the runtime environment has been created (according to the configuration in config.json) but before the pivot_root or any equivalent operation has been executed. 在 Linux 系统中,先后顺序:

  1. 创建 container namespaces
  2. 执行 prestart hook:provide an opportunity to customize the container (e.g. the network namespace could be specified in this hook). The prestart hooks MUST be called before the createRuntime hooks.

The prestart hooks’ path MUST resolve in the runtime namespace. The prestart hooks MUST be executed in the runtime namespace.

prestart hook 即将废弃,OCI 官方建议用 createRuntime, createContainer, startContainer hooks。

6.2 华为 Ascend docker runtime

github.com/Ascend/ascend-docker-runtime

功能与 NVIDIA container runtime,供容器环境使用昇腾 GPU。

$ cat /etc/docker/daemon.json
{
    "runtimes":     {
        "ascend":       {
            "path": "/usr/local/Ascend/Ascend-Docker-Runtime/ascend-docker-runtime",
            "runtimeArgs":  []
        }
    },
    "default-shm-size":     "8G",
    "default-runtime":      "ascend"
}

Fig. Huawei Ascend GPU: Container Engine Plugin. [5]

prestart hook 做的事情 [5]:

  1. Mount NPU device to the namespace of the container based on ASCEND_VISBLE_DEVICES.
  2. Configure device cgroup of the container on the host to ensure that the container can use only the specified NPU to ensure device isolation.
  3. Mount CANN Runtime Library on the host to the container namespace.

7 结束语

同样是百行代码(创建一个基本容器的代码还可以大幅缩减),创建一个 VM 的过程就跟创建一个容器就非常不同。

VM 的事情基本是内核一次性做的,更像一个黑盒,因此虚拟机时代基础设施开发者能做的事情更上层一些, 更多地是基于 KVM API 来做一些更上层的资源和状态管理工作,而深入到 KVM 内部的机会和需求都比较少,粒度更粗。

到了容器时代,上面已经看到,它不再是内核封装好的一个“黑盒”,而像是拼积木一样, 基于几种内核机制拼出来的一个东西。它在某些方面跟虚拟机很像,但通过本文应该看到, 二者又有本质的差别,尤其是在隔离和安全性方面。

实际上,关于容器的好处和场景业界都已经达成高度一致,大规模的生产部署也验证了这个方向的正确性, 但关于什么是容器现在似乎仍然没有一个普遍接受的定义 —— 也说明并不是那么重要。

希望本文能给基础设施研发/运维带来一些帮助和启发。

参考资料

  1. Linux containers in 500 lines of code
  2. (译) Control Group v2 (cgroupv2)(KernelDoc, 2021)
  3. The Mysterious Container net.core.somaxconn (2022)
  4. What is the difference between procfs and sysfs, unix.stackexchange.com
  5. Huawei Ascend GPU: Container Engine Plugin

Written by Human, Not by AI Written by Human, Not by AI

GPU Performance (Data Sheets) Quick Reference (2023)

This post provides a concise reference for the performance of popular GPU models from NVIDIA and Huawei/HiSilicon, primarily intended for personal use.



1 Introduction

Naming convention of NVIDIA GPUs

The first letter in GPU model names denote their GPU architectures, with:

  1. T for Turing;
  2. A for Ampere;
  3. V for Volta;
  4. H for Hopper; 2022
  5. L for Ada Lovelace;

2 Comparison of L2/T4/A10/A10G/V100

  L2 T4 A10 A10G A30 V100 PCIe/SMX2
Designed for Data center Data center (Desktop) Graphics-intensive workloads Desktop Desktop Data center
Year 2023 2018 2020     2017
Manufacturing   12nm 12nm 12nm    
Architecture Ada Lovelace Turing Ampere Ampere Ampere Volta
Max Power   70 watts 150 watts   165 watts 250/300watts
GPU Mem 24GB GDDR6 16GB GDDR6 24GB GDDR6 48GB GDDR6 24GB HBM2 16/32GB HBM2
GPU Mem BW 300 GB/s 400 GB/s 600 GB/s   933GB/s 900 GB/s
Interconnect PCIe Gen4 64GB/s PCIe Gen3 32GB/s PCIe Gen4 66 GB/s   PCIe Gen4 64GB/s, NVLINK 200GB/s PCIe Gen3 32GB/s, NVLINK 300GB/s
FP32 24.1 TFLOPS 8.1 TFLOPS 31.2 TFLOPS   10.3TFLOPS 14/15.7 TFLOPS
TF32 48.3 TFLOPS          
BFLOAT16 TensorCore 95.6 TFLOPS   125 TFLOPS   165 TFLOPS  
FP16 TensorCore     125 TFLOPS   165 TFLOPS  
INT8 TensorCore 193/193 TFLOPS   250 TFLOPS   330 TOPS  
INT4 TensorCore         661 TOPS  

Datasheets:

  1. T4
  2. A10
  3. A30
  4. V100-PCIe/V100-SXM2/V100S-PCIe

3 Comparison of A100/A800/H100/H800/Ascend 910B

  A800 (PCIe/SXM) A100 (PCIe/SXM) Huawei Ascend 910B H800 (PCIe/SXM) H100 (PCIe/SXM)
Year 2022 2020 2023 2022 2022
Manufacturing 7nm 7nm 7+nm 4nm 4nm
Architecture Ampere Ampere HUAWEI Da Vinci Hopper Hopper
Max Power 300/400 watt 300/400 watt 400 watt   350/700 watt
GPU Mem 80G HBM2e 80G HBM2e 64G HBM2e 80G HBM3 80G HBM3
GPU Mem BW   1935/2039 GB/s     2/3.35 TB/s
GPU Interconnect (one-to-one max bandwidth) NVLINK 400GB/s PCIe Gen4 64GB/s, NVLINK 600GB/s HCCS 56GB/s NVLINK 400GB/s PCIe Gen5 128GB/s, NVLINK 900GB/s
GPU Interconnect (one-to-many total bw) NVLINK 400GB/s PCIe Gen4 64GB/s, NVLINK 600GB/s HCCS 392GB/s NVLINK 400GB/s PCIe Gen5 128GB/s, NVLINK 900GB/s
FP32 TFLOPS   19.5     51 | 67*
TF32 (TensorFloat) TFLOPS   156 | 312*     756 | 989*
BFLOAT16 TensorCore TFLOPS   156 | 312*      
FP16 TensorCore TFLOPS   312 | 624* 320   1513 | 1979*
FP8 TensorCore TFLOPS NOT support NOT support     3026 | 3958*
INT8 TensorCore TFLOPS   624 | 1248* 640   3026/3958*

Notes:

  • *: with sparsity;

H100 vs. A100 in one word: 3x performance, 2x price.

Datasheets:

  1. A100
  2. H100
  3. Huawei Ascend-910B (404)
  4. 910 paper: Ascend: a Scalable and Unified Architecture for Ubiquitous Deep Neural Network Computing, HPCA, 2021

3.1 Note on inter-GPU bandwidth: HCCS vs. NVLINK

For 8-card A800 and 910B modules: 910B HCCS has a total bandwidth of 392GB/s, which appears to be comparable to A800 NVLink (400GB/s). However, there are some differences. To clarify them,

  • NVIDIA NVLink: full-mesh topology as below, so (bi-directional) GPU-to-GPU max bandwidth is 400GB/s (note that below is 8*A100 module, 600GB/s, 8*A800 shares a similar full-mesh topology);

  • Huawei HCCS: peer-to-peer topology (no stuffs like NVSwitch chip), so (bi-directional) GPU-to-GPU max bandwidth is 56GB/s;

4 Comparison of H20/L20/Ascend 910B

  Huawei Ascend 910B L20 (PCIe) H20 (PCIe/SXM) H100 (PCIe/SXM)
Year 2023 2023 2023 2022
Manufacturing 7+nm 4nm 4nm 4nm
Architecture HUAWEI Da Vinci Ada Lovelace Hopper Hopper
Max Power 400 watt 275W 400W 350/700 watt
GPU Mem 64G HBM2e 48G GDDR6 80G HBM3 80G HBM3
GPU Mem BW   864GB/s 4.0TB/s 2/3.35 TB/s
L2 Cache   96MB 60MB  
GPU Interconnect (one-to-one max bandwidth) HCCS 56GB/s PCIe Gen4 64GB/s PCIe Gen5 128GB/s, NVLINK 900GB/s PCIe Gen5 128GB/s, NVLINK 900GB/s
GPU Interconnect (one-to-many total bw) HCCS 392GB/s PCIe Gen4 64GB/s PCIe Gen5 128GB/s, NVLINK 900GB/s PCIe Gen5 128GB/s, NVLINK 900GB/s
FP32   59.8 TFLOPS 44 TFLOPS 51/67 TFLOPS
TF32 (TensorFloat)   59.8 TFLOPS 74 TFLOPS 756/989 TFLOPS
BFLOAT16 TensorCore   119/119 TFLOPS 148/148 TFLOPS  
FP16 TensorCore 320 TFLOPS     1513/1979 TFLOPS
FP8 TensorCore       3026/3958 TFLOPS
INT8 TensorCore 640 TFLOPS 239/239 TFLOPS 296/296 TFLOPS 3026/3958 TFLOPS

5 Notes on US “Chip Export Controls” targeting China

5.1 Export Controls 2022.10

According to Implementation of Additional Export Controls: Certain Advanced Computing and Semiconductor Manufacturing Items; Supercomputer and Semiconductor End Use; Entity List Modification, for chips that can be shipped to the Chinese market, the following conditions must be met:

  1. aggregate bidirectional transfer rate must < 600 Gbyte/s; AND,
  2. aggregated processing performance must < 4800 bit TOPS (TFLOPS), which is equivalent to:

    • < 300 TFLOPS FP16
    • < 150 TFLOPS FP32

A100 and H100 are subjected to these restrictions, that’s why there are tailored versions: A800 and H800.

5.2 Export Controls 2023.10

According to Implementation of Additional Export Controls: Certain Advanced Computing Items; Supercomputer and Semiconductor End Use; Updates and Corrections, in addition to the above 2022.10 Export Controls, chips that meet one of the following conditions are also prohibited from being sold in the Chinese market:

  1. total processing performance in 2400~4800 bit TOPS AND performance density in 1.6~5.92;

    2400 bit TOPS is equivalent to:

    • 150 TFLOPS FP16
    • 75 TFLOPS FP32
  2. total processing performance >= 1600 bit TOPS AND performance density in 3.2~5.92;

These restrictions cover most high-performance GPUs, including the old model A800. However, it should be noted that there is also room for low-computing-power but high-transfer-rate models, such as the rumored “148TFLOPS + 96GB HBM + 900GB/s NVLink” H20 GPU.


Written by Human, Not by AI Written by Human, Not by AI

GPU 进阶笔记(二):华为昇腾 910B GPU 相关(2023)

记录一些平时接触到的 GPU 知识。由于是笔记而非教程,因此内容不会追求连贯,有基础的同学可作查漏补缺之用。

水平有限,文中不免有错误或过时之处,请酌情参考。



1 术语

1.1 与 NVIDIA 术语对应关系

大部分人目前还是对 NVIDIA GPU 更熟悉,所以先做一个大致对照,方便快速了解华为 GPU 产品和生态:

NVIDIA HUAWEI 功能
GPU NPU/GPU 通用并行处理器
NVLINK HCCS GPU 卡间高速互连技术
InfiniBand HCCN RDMA 产品/工具
nvidia-smi npu-smi GPU 命令行工具
CUDA CANN GPU 编程库

说明:

  1. 华为很多地方混用术语 NPU 和 GPU,为简单起见,本文统称为 GPU;

1.2 缩写

  • NPU: Neural-network Processing Unit
  • HCCS: Huawei Cache Coherence System
  • HCCN: Huawei Cache Coherence Network
  • CANN: Huawei compute Architecture for Neural Networks

2 产品与机器

2.1 GPU 产品

  • 训练:昇腾 910B,对标 NVIDIA A100/A800算力对比
  • 推理:Atlas 300 系列,对标 NVIDIA T4;

2.2 训练机器

底座 CPU

根据 CPU 不同分为两种:

  1. x86 底座

    • 客户需要适配的工作量小一些;
  2. arm 底座:鲲鹏系列

    • 华为云上一般提供的是这种
    • 功耗低,叠加液冷,可以实现比常规 NVIDIA 服务器更好的“性能/功耗”比;

功耗

16 卡昇腾 910B 训练机器,8U,功耗对比:

  • X86: 12KW
  • ARM: 4.5KW

操作系统

华为默认是自家的欧拉操作系统 EulerOS(基于 CentOS),

$ cat /etc/os-release
EulerOS release 2.0 (SP10)
NAME="EulerOS"
VERSION="2.0 (SP10)"
ID="euleros"
VERSION_ID="2.0"
PRETTY_NAME="EulerOS 2.0 (SP10)"
ANSI_COLOR="0;31"

2.3 性能

一些公开信息:

  1. 算力指标基本对齐 NVIDIA A800,卡间互联带宽还有差距;
  2. 科大讯飞称和华为联合优化之后,在他们的场景中已经达到 A100 的性能;

910B 的官方公开信息比较少,但上一代 910 是发了 paper 的,想了解内部细节(例如 HCCS)的可参考 [2]。

3 实探:鲲鹏底座 8*910B GPU 主机

8 卡训练机器配置,来自华为云环境:

  • 机型: physical.kat2ne.48xlarge.8.ei.pod101
  • CPU: Kunpeng 920 (4*48Core@2.6GHz),ARM 架构,192
  • 内存: 24*64GB DDR4
  • 网卡: 2*100G + 8*200G
  • 浸没式液冷

3.1 CPU

$ cat /proc/cpuinfo
...
processor       : 191
BogoMIPS        : 200.00
Features        : fp asimd evtstrm aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma dcpop asimddp asimdfhm ssbs
CPU implementer : 0x48 # <-- ARM_CPU_IMP_HISI
CPU architecture: 8
CPU variant     : 0x1
CPU part        : 0xd01
CPU revision    : 0

CPU implementer 是 CPU 厂商, ARM 架构的完整列表见内核源码 arch/arm64/include/asm/cputype.h, 其中 0x48 对应的是华为海思。

3.2 网卡和网络

网卡:

$ ip addr # 输出有精简
2: enp67s0f5: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    inet 192.168.0.128/24 brd 192.168.0.255 scope global dynamic noprefixroute enp67s0f5
3: enp189s0f0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
4: enp189s0f1: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
5: enp189s0f2: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000
6: enp189s0f3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN group default qlen 1000

看到只有网卡 2 上配置了 IP 地址。3~6 是 RDMA 网卡,需要用华为的 RDMA 命令行工具 hccn_tool 来查看和修改配置:

$ hccn_tool -i 3 -status -g # 相当于 ethtool <eth NIC>
Netdev status:Settings for eth3:
        Supported ports: [ Backplane ]
        Supported link modes:   1000baseKX/Full
                                ...
                                100000baseKR4/Full
        Supported pause frame use: Symmetric
        Supports auto-negotiation: No
        Supported FEC modes: None        RS
        Advertised link modes:  Not reported
        Speed: 200000Mb/s   # <-- 200Gbps 网卡
        ...

查看一些硬件统计:

$  hccn_tool -i 3 -hw_stats -g
[devid 3] pd_alloc: 1
[devid 3] pd_dealloc: 0
[devid 3] mr_alloc: 0
[devid 3] mr_dealloc: 0
[devid 3] cq_alloc: 1
[devid 3] cq_dealloc: 0
[devid 3] qp_alloc: 1
[devid 3] qp_dealloc: 0
[devid 3] pd_active: 1
[devid 3] mr_active: 0
[devid 3] cq_active: 1
[devid 3] qp_active: 1
[devid 3] aeqe: 0
[devid 3] ceqe: 0

查看 LLDP 信息(直连的交换机):

$  hccn_tool -i 3 -lldp -g # 类似以太网中的 lldpctl/lldpcli
Chassis ID TLV
        MAC: ...
Port ID TLV
        Ifname: 400GE1/1/20:2
System Description TLV
        Versatile Routing Platform Software
VRP (R) software, Version 8.211 (DX511 V200R021C10SPC600)

Huarong DX511

System Capabilities TLV
        Enabled capabilities: Bridge, Router
Management Address TLV
        IPv4: 26.xx.xx.xx
...
Maximum Frame Size TLV
        9216
End of LLDPDU TLV

查看网卡的 IP 地址和路由:

$ hccn_tool -i 3 -ip -g
ipaddr:29.1.112.213
netmask:255.255.0.0

$  hccn_tool -i 3 -route -g
Routing table:
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         29.1.0.1        0.0.0.0         UG    0      0        0 eth3
29.1.0.0        *               255.255.0.0     U     0      0        0 eth3
127.0.0.1       *               255.255.255.255 UH    0      0        0 lo
192.168.1.0     *               255.255.255.0   U     0      0        0 end3v0
192.168.2.0     *               255.255.255.0   U     0      0        0 end3v0

RDMA 网卡的启动配置其实在配置文件,

$ cat /etc/hccn.conf # RDMA 网卡 0-7 的配置
address_0=29.1.137.205
netmask_0=255.255.0.0
netdetect_0=29.1.0.1
gateway_0=29.1.0.1
send_arp_status_0=1
...
address_7=29.1.170.143
netmask_7=255.255.0.0
netdetect_7=29.1.0.1
gateway_7=29.1.0.1
send_arp_status_7=1

RDMA ping:

$ hccn_tool -i 3 -ping -g address 29.1.137.205
device 3 PING 29.1.137.205
recv seq=0,time=1.418000ms
recv seq=1,time=0.034000ms
recv seq=2,time=0.040000ms
3 packets transmitted, 3 received, 0.00% packet loss

3.3 GPU 信息

$ npu-smi info
+------------------------------------------------------------------------------------------------+
| npu-smi 23.0.rc2                 Version: 23.0.rc2                                             |
+---------------------------+---------------+----------------------------------------------------+
| NPU   Name                | Health        | Power(W)    Temp(C)           Hugepages-Usage(page)|
| Chip                      | Bus-Id        | AICore(%)   Memory-Usage(MB)  HBM-Usage(MB)        |
+===========================+===============+====================================================+
| 0     910B1               | OK            | 88.4        46                0    / 0             |
| 0                         | 0000:C1:00.0  | 0           0    / 0          4175 / 65536         |
+===========================+===============+====================================================+
| 1     910B1               | OK            | 92.1        47                0    / 0             |
| 0                         | 0000:01:00.0  | 0           0    / 0          4175 / 65536         |
+===========================+===============+====================================================+
...
+===========================+===============+====================================================+
| 7     910B1               | OK            | 92.7        48                0    / 0             |
| 0                         | 0000:42:00.0  | 0           0    / 0          4174 / 65536         |
+===========================+===============+====================================================+
  • GPU 型号 910B1
  • 64GB HBM 显存
$ npu-smi info -h
Usage: npu-smi info <watch|proc|-h|-m|-l|-t type> [Options...]

Commands:
       watch          Show all device's status in scrolling format
       proc           Show device's matrix process status in scrolling format
       -h, --help     Show this help text and exit
       -m             Show all device's mapping information
       -l             Show all device's topology information
       -t type        Show information for type
                      type: board, flash, memory, usages, sensors, temp, power, volt, mac-addr,
                            common, health, product, ecc, ip, sys-time, i2c_check, work-mode,
                            ecc-enable, p2p-enable, ssh-enable, license, customized-info,
                            device-share, nve-level, aicpu-config, pcie-err, mcu-monitor,
                            err-count, boot-area, vnpu-mode, info-vnpu, vnpu-svm, cpu-num-cfg,
                            first-power-on-date, proc-mem, phyid-remap, vnpu-cfg-recover, key-manage,
                            template-info, pkcs-enable, p2p-mem-cfg, pwm-mode, pwm-duty-ratio,
                            boot-select, topo.

Options:
       -i %d          Card ID
       -c %d          Chip ID
       -p %d          Chip Physical ID

3.3.1 GPU 卡间互连:HCCS

角色类似于 NVIDIA NVLink。

$ npu-smi info -t topo
NPU0       NPU1       NPU2       NPU3       NPU4       NPU5       NPU6       NPU7       CPU Affinity
NPU0       X          HCCS       HCCS       HCCS       HCCS       HCCS       HCCS       HCCS       144-167
NPU1       HCCS       X          HCCS       HCCS       HCCS       HCCS       HCCS       HCCS       0-23
NPU2       HCCS       HCCS       X          HCCS       HCCS       HCCS       HCCS       HCCS       144-167
NPU3       HCCS       HCCS       HCCS       X          HCCS       HCCS       HCCS       HCCS       0-23
NPU4       HCCS       HCCS       HCCS       HCCS       X          HCCS       HCCS       HCCS       96-119
NPU5       HCCS       HCCS       HCCS       HCCS       HCCS       X          HCCS       HCCS       48-71
NPU6       HCCS       HCCS       HCCS       HCCS       HCCS       HCCS       X          HCCS       96-119
NPU7       HCCS       HCCS       HCCS       HCCS       HCCS       HCCS       HCCS       X          48-71

Legend:
  X    = Self
  SYS  = Path traversing PCIe and NUMA nodes. Nodes are connected through SMP, such as QPI, UPI.
  PHB  = Path traversing PCIe and the PCIe host bridge of a CPU.
  PIX  = Path traversing a single PCIe switch
  PXB  = Path traversing multipul PCIe switches
  HCCS = Connection traversing HCCS.

很多资料都说 910B 的卡间互连带宽是 392GB/s,看起来跟 A800 的 400GB/s 差不多了, 但其实还是有区别的,主要是互连拓扑不同导致的,详见 [1]。

3.3.2 GPU/Memory 使用率

第一个 chip 的利用率:

$ npu-smi info -t usages -i 0
        NPU ID                         : 0
        Chip Count                     : 1

        DDR Capacity(MB)               : 0
        DDR Usage Rate(%)              : 0
        DDR Hugepages Total(page)      : 0
        DDR Hugepages Usage Rate(%)    : 0
        HBM Capacity(MB)               : 65536
        HBM Usage Rate(%)              : 4
        Aicore Usage Rate(%)           : 0
        Aivector Usage Rate(%)         : 0
        Aicpu Usage Rate(%)            : 0
        Ctrlcpu Usage Rate(%)          : 0
        DDR Bandwidth Usage Rate(%)    : 0
        HBM Bandwidth Usage Rate(%)    : 0
        Chip ID                        : 0

第二个 chip 的常规利用率信息:

$ npu-smi info -t common -i 1
        NPU ID                         : 1
        Chip Count                     : 1

        Chip ID                        : 0
        Memory Usage Rate(%)           : 0
        HBM Usage Rate(%)              : 4
        Aicore Usage Rate(%)           : 0
        Aicore Freq(MHZ)               : 1800
        Aicore curFreq(MHZ)            : 800
        Aicore Count                   : 24
        Temperature(C)                 : 46
        NPU Real-time Power(W)         : 93.4

        Chip Name                      : mcu
        Temperature(C)                 : 38

参考资料

  1. GPU Performance (Data Sheets) Quick Reference (2023)
  2. Ascend: a Scalable and Unified Architecture for Ubiquitous Deep Neural Network Computing, HPCA, 2021
  3. Introduction to the npu-smi Command, huawei.com, 2023

Written by Human, Not by AI Written by Human, Not by AI

GPU 进阶笔记(一):高性能 GPU 服务器硬件拓扑与集群组网(2023)

记录一些平时接触到的 GPU 知识。由于是笔记而非教程,因此内容不会追求连贯,有基础的同学可作查漏补缺之用。

水平有限,文中不免有错误或过时之处,请酌情参考。



1 术语与基础

大模型训练一般都是用单机 8 卡 GPU 主机组成集群,机型包括 8*{A100,A800,H100,H800} 可能还会用最近即将上市的 {4,8}*L40S。 下面一台典型 8*A100 GPU 的主机内硬件拓扑:

典型 8 卡 A100 主机硬件拓扑

本节将基于这张图来介绍一些概念和术语,有基础的可直接跳过。

1.1 PCIe 交换芯片

CPU、内存、存储(NVME)、GPU、网卡等支持 PICe 的设备,都可以连接到 PCIe 总线或专门的 PCIe 交换芯片,实现互联互通。

PCIe 目前有 5 代产品,最新的是 Gen5

1.2 NVLink

定义

Wikipedia 上 NVLink 上的定义:

NVLink is a wire-based serial multi-lane near-range communications link developed by Nvidia. Unlike PCI Express, a device can consist of multiple NVLinks, and devices use mesh networking to communicate instead of a central hub. The protocol was first announced in March 2014 and uses a proprietary high-speed signaling interconnect (NVHS).

简单总结:同主机内不同 GPU 之间的一种高速互联方式,

  1. 是一种短距离通信链路,保证包的成功传输,更高性能,替代 PCIe,
  2. 支持多 lane,link 带宽随 lane 数量线性增长,
  3. 同一台 node 内的 GPU 通过 NVLink 以 full-mesh 方式(类似 spine-leaf)互联,
  4. NVIDIA 专利技术。

演进:1/2/3/4 代

主要区别是单条 NVLink 链路的 lane 数量、每个 lane 的带宽(图中给的都是双向带宽)等:

NVLink 演进。Image from: HotChips 2022 [1]

例如,

  • A100 是 2 lanes/NVSwitch * 6 NVSwitch * 50GB/s/lane= 600GB/s 双向带宽(单向 300GB/s)。注意:这是一个 GPU 到所有 NVSwitch 的总带宽
  • A800 被阉割了 4 条 lane,所以是 8 lane * 50GB/s/lane = 400GB/s 双向带宽(单向 200GB/s)。

监控

基于 DCGM 可以采集到实时 NVLink 带宽:

Metrics from dcgm-exporter [5]

1.3 NVSwitch

还是参考下图,

典型 8 卡 A100 主机硬件拓扑

NVSwitch 是 NVIDIA 的一款交换芯片,封装在 GPU module 上,并不是主机外的独立交换机

下面是真机图,浪潮的机器,图中 8 个盒子就是 8 片 A100,右边的 6 块超厚散热片下面就是 NVSwitch 芯片:

Inspur NF5488A5 NVIDIA HGX A100 8 GPU Assembly Side View. Image source: [2]

1.4 NVLink Switch

NVSwitch 听名字像是交换机,但实际上是 GPU module 上的交换芯片,用来连接同一台主机内的 GPU

2022 年,NVIDIA 把这块芯片拿出来真的做成了交换机,叫 NVLink Switch [3], 用来跨主机连接 GPU 设备

这俩名字很容易让人混淆。

1.5 HBM (High Bandwidth Memory)

由来

传统上,GPU 显存和普通内存(DDR)一样插在主板上,通过 PCIe 连接到处理器(CPU、GPU), 因此速度瓶颈在 PCIe,Gen4 是 64GB/s,Gen5 是 128GB/s。

因此,一些 GPU 厂商(不是只有 NVIDIA 一家这么做)将将多个 DDR 芯片堆叠之后与 GPU 封装到一起 (后文讲到 H100 时有图),这样每片 GPU 和它自己的显存交互时,就不用再去 PCIe 交换芯片绕一圈,速度最高可以提升一个量级。 这种“高带宽内存”(High Bandwidth Memory)缩写就是 HBM。

HBM 的市场目前被 SK 海力士和三星等韩国公司垄断。

演进:HBM 1/2/2e/3/3e

From wikipedia HBM

  Bandwidth Year GPU
HBM 128GB/s/package    
HBM2 256GB/s/package 2016 V100
HBM2e ~450GB/s 2018 A100, ~2TB/s; 华为 Ascend 910B
HBM3 600GB/s/site 2020 H100, 3.35TB/s
HBM3e ~1TB/s 2023 H200, 4.8TB/s

使用了 HBM 的近几代高端 NVIDIA GPU 显存带宽(双向),纵坐标是 TB/s。Image source: [3]

  • AMD MI300X 采用 192GB HBM3 方案,带宽 5.2TB/s
  • HBM3e 是 HBM3 的增强版,速度从 6.4GT/s 到 8GT/s。

1.6 带宽单位

大规模 GPU 训练的性能与数据传输速度有直接关系。这里面涉及到很多链路,比如 PCIe 带宽、内存带宽、NVLink 带宽、HBM 带宽、网络带宽等等。

  • 网络习惯用 bits/second (b/s) 表示之外,并且一般说的都是单向(TX/RX);
  • 其他模块带宽基本用 byte/sedond (B/s)transactions/second (T/s) 表示,并且一般都是双向总带宽

比较带宽时注意区分和转换。

2 典型 8*A100/8*A800 主机

2.1 主机内拓扑:2-2-4-6-8-8

  • 2 片 CPU(及两边的内存,NUMA)
  • 2 张存储网卡访问分布式存储,带内管理等)
  • 4 个 PCIe Gen4 Switch 芯片
  • 6 个 NVSwitch 芯片
  • 8 个 GPU
  • 8 个 GPU 专属网卡

典型 8 卡 A100 主机硬件拓扑

下面这个图画的更专业,需要更多细节的可参考:

NVIDIA DGX A100 主机(官方 8 卡机器)硬件拓扑。Image source: [4]

存储网卡

通过 PCIe 直连 CPU。用途:

  1. 从分布式存储读写数据,例如读训练数据写 checkpoint 等;
  2. 正常的 node 管理,ssh,监控采集等等。

官方推荐用 BF3 DPU。但其实只要带宽达标,用什么都行。组网经济点的话用 RoCE,追求最好的性能用 IB。

NVSwitch fabric:intra-node full-mesh

8 个 GPU 通过 6 个 NVSwitch 芯片 full-mesh 连接,这个 full-mesh 也叫 NVSwitch fabric; full-mesh 里面的每根线的带宽是 n * bw-per-nvlink-lane

  • A100 用的 NVLink3,50GB/s/lane,所以 full-mesh 里的每条线就是 12*50GB/s=600GB/s,注意这个是双向带宽,单向只有 300GB/s。
  • A800 是阉割版,12 lane 变成 8 lane,所以每条线 8*50GB/s=400GB/s,单向 200GB/s。

nvidia-smi topo 查看拓扑

下面是一台 8*A800 机器上 nvidia-smi 显示的实际拓扑(网卡两两做了 bond,NIC 0~3 都是 bond):

  • GPU 之间(左上角区域):都是 NV8,表示 8 条 NVLink 连接;
  • NIC 之间:

    • 在同一片 CPU 上:NODE,表示不需要跨 NUMA,但需要跨 PCIe 交换芯片
    • 不在同一片 CPU 上:SYS,表示需要跨 NUMA
  • GPU 和 NIC 之间:

    • 在同一片 CPU 上,且在同一个 PCIe Switch 芯片下面:NODE,表示只需要跨 PCIe 交换芯片
    • 在同一片 CPU 上,且不在同一个 PCIe Switch 芯片下面:NODE,表示需要跨 PCIe 交换芯片和 PCIe Host Bridge
    • 不在同一片 CPU 上:SYS,表示需要跨 NUMA、PCIe 交换芯片,距离最远

1.2 GPU 训练集群组网:IDC GPU fabirc

GPU node 互联架构:

计算网络

GPU 网卡直连到置顶交换机(leaf),leaf 通过 full-mesh 连接到 spine,形成跨主机 GPU 计算网络。

  • 这个网络的目的是 GPU 与其他 node 的 GPU 交换数据
  • 每个 GPU 和自己的网卡之间通过 PCIe 交换芯片连接GPU <--> PCIe Switch <--> NIC

存储网络

直连 CPU 的两张网卡,连接到另一张网络里,主要作用是读写数据,以及 SSH 管理等等。

RoCE vs. InfiniBand

不管是计算网络还是存储网络,都需要 RDMA 才能实现 AI 所需的高性能。RDMA 目前有两种选择:

  • RoCEv2:公有云卖的 8 卡 GPU 主机基本都是这种网络,比如 CX6 8*100Gbps 配置;在性能达标的前提下,(相对)便宜;
  • InfiniBand (IB):同等网卡带宽下,性能比 RoCEv2 好 20% 以上,但是价格贵一倍。

1.3 数据链路带宽瓶颈分析

单机 8 卡 A100 GPU 主机带宽瓶颈分析

几个关键链路带宽都标在图上了,

  1. 同主机 GPU 之间:走 NVLink,双向 600GB/s,单向 300GB/s
  2. 同主机 GPU 和自己的网卡之间:走 PICe Gen4 Switch 芯片,双向 64GB/s,单向 32GB/s
  3. 跨主机 GPU 之间:需要通过网卡收发数据,这个就看网卡带宽了,目前国内 A100/A800 机型配套的主流带宽是(单向) 100Gbps=12.5GB/s。 所以跨机通信相比主机内通信性能要下降很多。

    • 200Gbps==25GB/s:已经接近 PCIe Gen4 的单向带宽;
    • 400Gbps==50GB/s:已经超过 PCIe Gen4 的单向带宽。

    所以在这种机型里用 400Gbps 网卡作用不大,400Gbps 需要 PCIe Gen5 性能才能发挥出来。

3 典型 8*H100/8*H800 主机

GPU Board Form Factor 分为两种类型:

  • PCIe Gen5
  • SXM5:性能更高一些

3.1 H100 芯片 layout

下面是一片 H100 GPU 芯片的内部结构:

单片 H100 GPU 内部逻辑布局。Image source: [3]

  • 4nm 工艺;
  • 最下面一排是 18 根 Gen4 NVLink;双向总带宽 18 lanes * 25GB/s/lane = 900GB/s
  • 中间蓝色的是 L2 cache;
  • 左右两侧是 HBM 芯片,即显存;

3.2 主机内硬件拓扑

跟 A100 8 卡机结构大致类似,区别:

  1. NVSwitch 芯片从 6 个减少到了 4 个;真机图如下,

  2. 与 CPU 的互联从 PCIe Gen4 x16 升级到 PCIe Gen5 x16,双向带宽 128GB/s

3.3 组网

与 A100 也类似,只是标配改成了 400Gbps 的 CX7 网卡, 否则网络带宽与 PCIe Switch 和 NVLink/NVSwitch 之间的差距更大了。

4 典型 4*L40S/8*L40S 主机

L40S 是今年(2023)即将上市的新一代“性价比款”多功能 GPU,对标 A100。 除了不适合训练基座大模型之外(后面会看到为什么),官方的宣传里它几乎什么都能干。 价格的话,目前第三方服务器厂商给到的口头报价都是 A100 的 8 折左右

4.1 L40S vs A100 配置及特点对比

L40S 最大的特点之一是 time-to-market 时间短,也就是从订货到拿到货周期比 A100/A800/H800 快很多。 这里面技术和非技术原因都有,比如:

  • 不存在被美国禁售的功能(根据 2023.10 的新规定,已经禁售了),比如 FP64 和 NVLink 都干掉了
  • 使用 GDDR6 显存,不依赖 HBM 产能(及先进封装);

价格便宜也有几方面原因,后面会详细介绍:

  1. 大头可能来自 GPU 本身价格降低:因为去掉了一些模块和功能,或者用便宜的产品替代;
  2. 整机成本也有节省:例如去掉了一层 PCIe Gen4 Swtich;不过相比于 4x/8x GPU,整机的其他部分都可以说送的了;

4.2 L40S 与 A100 性能对比

下面是一个官方标称性能对比:

具体场景的性能对比网上也有很多官方资料,这里就不列举了。简单来,

  • 性能 1.2x ~ 2x(看具体场景)。
  • 功耗:两台 L40S 和单台 A100 差不多

需要注意,L40S 主机官方推荐的是单机 4 卡而不是 8 卡(后面会介绍为什么), 所以对比一般是用 两台 4*L40S vs 单台 8*A100。另外,很多场景的性能提升有个 大前提:网络需要是 200Gbps RoCE 或 IB 网络,接下来介绍为什么。

4.3 L40S 攒机

推荐架构:2-2-4

相比于 A100 的 2-2-4-6-8-8 架构, 官方推荐的 L40S GPU 主机是 2-2-4 架构,一台机器物理拓扑如下:

推荐单机 4 卡 L40S GPU 主机拓扑

最明显的变化是去掉了 CPU 和 GPU 之间的 PCIe Switch 芯片, 网卡和 GPU 都是直连 CPU 上自带的 PCIe Gen4 x16(64GB/s),

  • 2 片 CPU(NUMA)
  • 2 张双口 CX7 网卡(每张网卡 2*200Gbps
  • 4 片 L40S GPU
  • 另外,存储网卡只配 1 张(双口),直连在任意一片 CPU 上

这样每片 GPU 平均 200Gbps 网络带宽

不推荐架构:2-2-8

单机 8 卡 L40S GPU 主机拓扑,来自 NVIDIA L40S 官方推介材料

如图,跟单机 4 卡相比,单机 8 卡需要引入两片 PCIe Gen5 Switch 芯片:

  • 说是现在PCIe Gen5 Switch 单片价格 1w 刀(不知真假),一台机器需要 2 片;价格不划算;
  • PCIe switch 只有一家在生产,产能受限,周期很长;
  • 平摊到每片 GPU 的网络带宽减半;

4.4 组网

官方建议 4 卡机型,搭配 200Gbps RoCE/IB 组网。

4.5 数据链路带宽瓶颈分析

单机 4 卡 L40S GPU 主机带宽瓶颈分析

以同 CPU 下面的两种 L40S 为例,这里面有两条链路可选:

  1. 直接通过 CPU 处理:GPU0 <--PCIe--> CPU <--PCIe--> GPU1

    • PCIe Gen4 x16 双向 64GB/s,单向 32GB/s
    • CPU 处理瓶颈?TODO
  2. 完全绕过 CPU 处理,通过网卡去外面绕一圈再回来GPU0 <--PCIe--> NIC <-- RoCe/IB Switch --> NIC <--PCIe--> GPU1

    • PCIe Gen4 x16 双向 64GB/s,单向 32GB/s
    • 平均每个 GPU 一个单向 200Gbps 网口,单向折算 25GB/s
    • 需要 NCCL 支持,官方说新版本 NCCL 正在针对 L40S 适配,默认行为就是去外面绕一圈回来;

第二种方式看着长了很多,但官方说其实比方式一还要快很多(这里还每太搞懂,CPU 那里是怎么处理的?)—— 前提是网卡和交换机配到位:200Gbps RoCE/IB 网络。在这种网络架构下(网络带宽充足),

  • 任何两片 GPU 的通信带宽和延迟都是一样的,是否在一台机器内或一片 CPU 下面并不重要,集群可以横向扩展(scaling up,compared with scaling in);
  • GPU 机器成本降低;但其实对于那些对网络带宽要求没那么高的业务来说,是把 NVLINK 的成本转嫁给了网络,这时候必须要组建 200Gbps 网络,否则发挥不出 L40S 多卡训练的性能。

如果是方式二,同主机内 GPU 卡间的带宽瓶颈在网卡速度。即使网络是推荐的 2*CX7 配置,

  • L40S: 200Gbps(网卡单向线速)
  • A100: 300GB/s(NVLINK3 单向) == 12x200Gbps
  • A800: 200GB/s(NVLINK3 单向) == 8x200Gbps

可以看到,L40S 卡间带宽还是比 A100 NVLINK 慢了 12 倍, 比 A800 NVLink 慢了 8 倍,所以不适合数据密集交互的基础大模型训练

4.6 测试注意事项

如上,即便只测试单机 4 卡 L40S 机器,也需要搭配 200Gbps 交换机,否则卡间性能发挥不出来。

参考资料

  1. NVLink-Network Switch - NVIDIA’s Switch Chip for High Communication-Bandwidth SuperPODs, Hot Chips 2022
  2. ChatGPT Hardware a Look at 8x NVIDIA A100 Powering the Tool, 2023
  3. NVIDIA Hopper Architecture In-Depth, nvidia.com, 2022
  4. DGX A100 review: Throughput and Hardware Summary, 2020
  5. Understanding NVIDIA GPU Performance: Utilization vs. Saturation, 2023

Written by Human, Not by AI Written by Human, Not by AI

❌