finish 2025-11-21 ppt

This commit is contained in:
2025-11-18 16:37:37 +08:00
commit eba1fc681d
8 changed files with 19472 additions and 0 deletions

508
2025-11-21/main.typ Normal file
View File

@@ -0,0 +1,508 @@
#import "@preview/typslides:1.3.0": *
#import "@preview/codly:1.3.0": *
#import "@preview/codly-languages:0.1.1": *
#import "slides_component/figures.typ": *
#show: codly-init.with()
#codly(languages: codly-languages)
#show raw: set text(font: ("0xProto Nerd Font", "WenQuanYi Micro Hei"))
// Project configuration
#show: typslides.with(
ratio: "16-9",
theme: "yelly",
font: ("0xProto Nerd Font","WenQuanYi Micro Hei"),
font-size: 20pt,
link-style: "color",
show-progress: true,
)
// The front slide is the first slide of your presentation
#front-slide(
title: "MPC的通信架构设计思路",
authors: "梁俊勇",
info: [2025-11-21],
)
// Custom outline
#table-of-contents()
#title-slide()[
MPC通信常见问题
]
#slide[
在底层通信里,我们常常会遇到这样的场景: \
一段代码需要重复调用send()或者recv()方法 \
但是这里有较大的性能问题
]
#slide(title: "场景1: 一方向另一方发送多个数据")[
```python
# 发送方
for item in items:
socket_to_peer.send(item)
```
```python
# 接收方
for i in len(items):
items[i] = socket_to_peer.recv()
```
]
#slide(title: "场景2: 一方向多方发送数据")[
```python
# 发送方
for peer in peers:
socket_to_peer.send(data)
```
```python
# 接收方
for peer in peers:
data = socket_to_peer.recv()
```
]
#slide(title: "问题1: 通信中的数据依赖问题")[
这其中的问题在于,每次发送与接受,都会存在数据的依赖关系。具体表现为:
- 阻塞式操作: 同步编程中,`send()` `recv()` 是阻塞的,程序会暂停执行,直到当前通信操作完全完成。
- 严格串行: 在循环中,后一次的通信操作(无论是发送还是接收)必须等待前一次操作彻底完成后才能启动。
- 性能瓶颈: 这种强制性的顺序等待,极大地限制了通信效率,尤其是在需要与多个参与方进行大量数据交换的场景下,导致系统无法充分利用并行处理能力和网络带宽。
]
#slide(title: "以场景2为例")[
```python
# 手动展开发送方循环代码
# peers = [alice, bob, carol]
# for peer in peers:
# socket_to_peer.send(data)
socket_to_alice.send(data)
socket_to_bob.send(data) # 在alice没有确认收到数据前此行代码不会执行
socket_to_carol.send(data) # 在bob没有确认收到数据前此行代码不会执行
```
]
#slide(title: "以场景2为例")[
```python
# 手动展开接收方循环代码
# peers = [alice, bob, carol]
# for peer in peers:
# socket_to_peer.recv(data)
socket_to_alice.recv(data)
socket_to_bob.recv(data) # 在没有接收到alice数据前此行代码不会执行
socket_to_carol.recv(data) # 在没有接收到bob数据前此行代码不会执行
```
]
#slide(title: "流程图表示")[
#align(center)[#show: serial_timeline()]
]
#slide(title: "问题2: 传统通信协议的瓶颈")[
我们常用的 `TCP` `HTTP/1.1` 协议虽然可靠但在大规模、高并发的MPC场景下会遇到显著的性能瓶颈
- 连接开销大:
- TCP三次握手: 每次新建连接都需握手,带来固有延迟。
- TCP慢启动: 连接初期传输速度受限,短连接无法有效利用带宽。
- 协议效率低:
- HTTP/1.1队头阻塞: 单个慢请求会阻塞同一连接上的后续所有请求。
- 冗余的报文头: HTTP头部是文本格式存在大量冗余信息。
- 数据传输未经优化:
- 默认无压缩: 协议本身不强制压缩数据,增大了网络负载。
- 序列化开销: 使用JSON等文本格式比二进制格式体积更大处理速度更慢。
]
#title-slide[
预备知识
]
#slide()[
在我们讲解后续思路与方案之前,我们先来了解一下异步编程。
]
#slide(title: "异步编程")[
异步编程是实现并发的一种主要方式。
我们可以用在餐厅点餐的例子来理解:
- 同步(等待的方式):
你在柜台点完餐,然后就站在那里一直等到餐做好。中间什么也做不了。
- 异步(不等待的方式):
你在柜台点完餐,服务员给你一个震动的取餐器。你可以回到座位上玩手机、聊天。当取餐器震动时,你再去取餐。
在程序中:
- “点餐”就是发起一个网络请求。
- “取餐器”就是一种通知机制。
- “玩手机”就是程序在等待期间可以去执行的其他代码。
]
#slide(title: "并发与异步的关系")[
简单来说,它们是“目标”与“手段”的关系。
- 并发是我们的目标:
- 我们希望程序能同时处理多个任务,提高效率。
- 异步编程是达成目标的手段之一:
- 它是我们编写并发程序的一种具体方法。
好比我们的目标是“同时做好一顿饭(炒菜、煮饭、煲汤)”。
- 多线程方案:
- 请三个厨师,每人负责一项。速度快,但“雇佣成本”高,且需要协调。
- 异步方案:
- 一位经验丰富的厨师,他先按下电饭煲和汤煲的开关(发起异步操作),然后在等待的间隙去炒菜。通过合理安排,一个人高效地完成了所有事情。
]
#slide(title: "为什么不选择多线程方案")[
尽管多线程也能实现并发,但在网络编程中,它常常带来一些挑战:
- 数据竞争与复杂性:
- 当多个线程同时尝试修改同一份数据时,很容易出现混乱,导致程序出错。
- 就像多个人同时在同一块白板上写字,最终结果可能一团糟。
- 为了避免这种混乱,程序员需要使用复杂的“锁”机制来协调,这非常容易出错,并且难以调试。
- 特定语言的限制:
- 像Python这样的语言由于其内部机制全局解释器锁GIL即使使用多线程也无法真正同时运行多个计算任务这限制了其在CPU密集型场景的性能。
- 即使是C/C++这类语言虽然没有GIL但手动管理线程间的同步和数据安全也是一个巨大的挑战。
]
#slide(title: "为什么不选择多线程方案")[
- 逻辑直观性:
- 即使是像Rust这样能保证线程安全的语言多线程的逻辑也可能不如异步编程模型特别是async/await那样直观和易于理解尤其是在处理大量网络I/O时。
因此,在许多网络设计场景中,异步编程往往是更简洁、高效且易于维护的选择。
]
#title-slide[
通过有向无环图(DAG)实现通信批次化
]
#slide()[
我们可以将数据依赖转换成图问题,进而使用拓扑排序来批次化无依赖关系的数据。\
例如刚刚的循环发送。我们可以创建一个缓冲区,将数据先填充至缓冲区,然后在一轮循环结束的时候,统一发送。\
接收方同理,创建缓冲区,通过循环解包出对应的数据。\
]
#slide(title: "手动批次化示例")[
```python
# 手动批次化发送方代码
# items = [item1, item2, item3]
# for item in items:
# socket_to_peer.send(item)
buffer = []
for item in items:
buffer.append(item)
socket_to_peer.send(buffer)
```
]
#slide(title: "批量化流程图表示")[
#align(center)[#show: batched_timeline()]
]
#slide()[
然后是并发化,如果多个步骤可以并行进行,那么拓扑排序也可以将这些步骤一起发送处理。\
例如刚刚的发送给多个参与方的模式。我们便可使用一次并发,在等待确认的时候,发送其他数据。
]
#slide(title: "并发化示例")[
```python
# peers = [socket_to_alice, socket_to_bob, socket_to_carol]
# 创建一组并发的发送任务
tasks = [
socket_to_peer.send(data) for peer in peers
]
# 同时执行所有发送任务
await asyncio.gather(*tasks)
```
]
#slide(title: "并发流程图表示")[
#align(center)[#show: async_timeline()]
]
#slide(title: "动态DAG方案")[
主流的动态DAG框架是阿里的隐语框架 @Ma2023
#align(center)[#image("./pics/secretflow.png")]
此方案的核心思想是:
- 运行时构建计算图DAG不是预先生成的而是在程序执行过程中动态构建。这允许更灵活的编程例如计算的流程可以根据秘密计算的中间结果发生改变。
- 通用语言:开发者使用 Python 来编写MPC逻辑。
- 分布式框架底层依赖一个强大的分布式执行框架Ray @moritz2018raydistributedframeworkemerging)来调度和执行图中的计算任务。
]
#slide()[
优点:
- 灵活性高,易于上手,可以处理具有复杂、数据依赖控制流的算法。
- 中文社区,有问题回复较及时。
缺点:
- 运行时动态调度和框架自身的开销较大(如传输的是整个Python对象),性能不如静态方案。
- 框架组件多,使用起来复杂度高。
]
#slide(title: "静态DAG方案")[
主流的静态DAG框架是 MP-SPDZ @mp-spdz。其工作流程完全不同:
- 编译时构建开发者使用一种专门为MPC设计的领域特定语言DSL编写协议。
- 提前优化编译器将DSL代码转换成一个固定的、静态的计算图并进行大量优化如指令调度、通信批处理。最终生成高效的字节码。
- 虚拟机解释项目使用C++实现了一个虚拟机,在运行时加载并执行这些预先编译好的字节码。由于所有依赖关系都已确定,执行过程非常高效。
]
#slide()[
优点:
- 性能极高。由于在编译时就掌握了全部计算信息,可以进行全局优化,运行时开销非常小。
缺点:
- 灵活性差。计算流程必须在编译前完全确定,很难实现依赖于秘密数据的动态分支。
- 需要学习相应的DSL。
]
#title-slide[
选择快速的通信协议
]
#slide[
传统方案是使用同步TCP来实现。但是这个方案因为TCP的握手和同步阻塞问题性能上比较差。\
对此,我们有多种优化方案。
]
#slide(title: "TCP长链接")[
非常简单的一个方式就是使用TCP长连接。这个方案需要额外维护一个连接列表。可以解决频繁建立TCP连接的握手开销。
]
#slide(title: "TCP长链接代码示例")[
核心思路是实现一个 `ConnectionManager`,它内部持有一个 `HashMap` 作为连接池。当需要通信时,我们向管理器请求一个连接。
- 如果连接已存在,管理器直接返回它。
- 如果不存在,管理器负责建立新连接,存入池中,然后返回。
这样,无论上层逻辑是发送还是接收,都无需关心连接是否已经建立。
```rust
use std::collections::HashMap;
use std::net::{TcpStream, ToSocketAddrs};
use std::io::{self, Write};
struct ConnectionManager {
connections: HashMap<String, TcpStream>,
}
impl ConnectionManager {
// 获取或建立一个新连接
// 这个函数体现了“维护链接”:调用者只管要连接,不用管是否已存在
fn get_connection<A: ToSocketAddrs + ToString>(
&mut self, addr: A
) -> io::Result<&mut TcpStream> {
let addr_string = addr.to_string();
// .entry().or_insert_with() 是更地道的写法,这里为了清晰而展开
if !self.connections.contains_key(&addr_string) {
let stream = TcpStream::connect(addr)?;
self.connections.insert(addr_string.clone(), stream);
}
Ok(self.connections.get_mut(&addr_string).unwrap())
}
}
```
]
#slide(title: "WebSocket全双工通信")[
WebSocket 是另一种在TCP之上但比原始TCP更现代化的协议。它特别适合需要频繁、实时双向通信的场景。
- 单次握手: WebSocket通过一个类似HTTP的请求发起握手一旦成功连接就升级为WebSocket连接后续通信不再需要HTTP的开销。
- 全双工通信: 连接建立后,客户端和服务器可以随时互相发送消息,实现了真正的双向通信,延迟极低。
- 轻量级: 相比HTTPWebSocket的消息帧头非常小减少了网络开销。
对于需要参与方之间进行大量实时交互的MPC协议WebSocket是一个比传统TCP长连接或HTTP轮询更高效的选择。
]
#slide(title: "序列化:数据的高效编码")[
序列化是将内存中的数据结构如对象、结构体转换为可以存储或传输的格式如字节流的过程。在MPC中我们需要在网络间传输大量数据因此序列化的效率至关重要。
- 文本格式 (如 JSON):
- 优点: 人类可读,易于调试。
- 缺点: 体积庞大解析速度慢对于性能敏感的MPC通信是巨大的瓶颈。
- 二进制格式 (如 Protobuf, Bincode):
- 优点: 极其紧凑,解析速度飞快,是高性能场景的首选。
- 缺点: 人类不可读。
在Rust生态中`serde` 框架配合 `bincode` 库是实现高效二进制序列化的黄金搭档。
]
#slide(title: "序列化代码示例 (Rust)")[
通过 `serde`,我们只需在结构体上添加一个派生宏,就能轻松实现序列化和反序列化。
```rust
use serde::{Serialize, Deserialize};
// 1. 使用serde的宏来自动实现序列化/反序列化
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct MyData {
id: u32,
payload: String,
}
fn main() {
let original = MyData {
id: 101,
payload: "hello".to_string(),
};
// 2. 使用bincode将结构体序列化为字节
let serialized_bytes: Vec<u8> = bincode::serialize(&original).unwrap();
println!("Serialized: {:?}", &serialized_bytes);
// 3. 从字节反序列化回结构体
let deserialized: MyData = bincode::deserialize(&serialized_bytes).unwrap();
println!("Deserialized: {:?}", &deserialized);
assert_eq!(original, deserialized);
}
```
]
#slide(title: "流式压缩")[
在序列化之后我们可以通过一些压缩算法来降低传输的数据量从而减少网络传输时间和带宽消耗。流式压缩特别适用于MPC中需要传输大量中间计算结果的场景。
优点:
- 减少数据量: 直接降低网络传输的数据大小,加快传输速度。
- 降低带宽需求: 对于带宽受限的环境尤其重要。
- 实时性: 流式压缩/解压缩可以在数据传输的同时进行,不会引入额外的显著延迟。
常用算法:
- Zlib/Deflate: 广泛使用的通用压缩算法,兼顾压缩比和速度。
- Snappy: Google开发以极快的压缩和解压缩速度著称但压缩比略低于Zlib适用于对速度要求更高的场景。
- LZ4: 另一种非常快速的无损压缩算法,解压缩速度尤其快。
在选择压缩算法时需要根据具体的MPC协议和网络环境权衡压缩比、压缩/解压缩速度以及CPU开销。
]
#slide(title: "gRPC协议")[
gRPC 是由 Google 开发的一个现代、高性能的RPC远程过程调用框架它能很好地解决我们之前提到的一些通信瓶颈。
- 基于 HTTP/2
- gRPC 使用 HTTP/2 作为其传输协议天然支持多路复用。这意味着可以在单个TCP连接上同时处理多个请求和响应彻底解决了队头阻塞问题也实现了长连接复用。
- 高效的 Protobuf
- 默认使用 Protocol Buffers (Protobuf) 进行序列化。相比于 JSONProtobuf 是二进制格式,体积更小、解析更快。
- 支持流式通信:
- 除了常规的“请求-响应”模式gRPC 还原生支持客户端流、服务端流和双向流。这对于需要连续交换大量数据的MPC场景非常有用。
]
#slide(title: "QUIC协议")[
QUIC 是一个更前沿的传输层协议,它被用作 HTTP/3 的基础。它的设计目标是彻底解决TCP的固有顽疾。
- 构建于 UDP 之上:
- QUIC 抛弃了 TCP选择在更底层的 UDP 上重新实现了可靠传输、拥塞控制等功能。
- 解决了真正的队头阻塞:
- HTTP/2 在单个TCP连接上多路复用但如果一个TCP数据包丢失整个连接上的所有流都必须等待它重传。这是传输层的队头阻塞。
- QUIC 将“流”作为一等公民。每个流的数据包被独立处理,一个流的丢包不会阻塞其他流。
]
#slide(title: "QUIC协议")[
- 更快的连接建立:
- QUIC 将传输层的握手类似TCP三次握手和加密握手TLS合并了。对于已有连接它甚至可以实现 0-RTT零往返时间的连接恢复速度极快。
]
#slide(title: "协议对比")[
为了更好地理解不同通信协议的优劣我们来对比一下它们在MPC场景下的表现。
#block[
#set text(size: 16pt)
#table(
columns: (auto, auto, auto, auto),
align: (center, left, left, left),
// Header
[特性], [传统TCP/HTTP/1.1], [gRPC (HTTP/2 over TCP)], [QUIC (HTTP/3 over UDP)],
// Rows
[传输层], [TCP], [TCP], [UDP],
[队头阻塞], [应用层和传输层], [传输层 (TCP HOLB)], [ (流独立)],
[连接建立], [ (TCP 3次握手 + TLS)], [ (TCP 3次握手 + TLS)], [ (0-RTT/1-RTT)],
[多路复用], [], [ (应用层)], [ (传输层)],
[序列化], [通常文本 (如JSON)], [Protobuf (二进制)], [协议无关 (二进制)],
[性能], [较低], [中高], [],
[适用场景], [简单请求/响应], [微服务/RPC], [实时通信/高并发]
)
]
]
#slide(title: "异步TCP方案")[
使用异步的发送可以让我们无须等待I/O。也就是说我们在调用`send()`/`recv()`的时候线程不会一直阻塞而是会将CPU时间片交给其他任务。
异步编程有多种模型,最主流的是 `async/await` 方案。
]
#slide(title: "async/await 方案的优劣")[
优点:
- 代码直观: 代码的线性逻辑使其看起来像同步代码,非常易于读写和维护。
- 资源占用低: 基于无栈协程,单个任务内存开销极小,可以轻松创建海量并发,且上下文切换在用户态完成。
- 统一的错误处理: 可以使用语言内建的错误处理机制如Rust的 `?`),避免了回调地狱中的错误处理难题。
缺点:
- 心智负担: 需要理解 `Future`、执行器等概念并手动处理CPU密集型任务放入线程池
- 函数染色: `async` 关键字具有传染性,`async` 函数不能被同步代码直接调用,反之亦然,割裂了生态。
]
#slide(title: "Rust中的异步实现对比")[
在Rust生态中同时存在Stackful和Stackless两种异步实现代表了不同的设计哲学。
Stackful (`may` 框架):
- `may` 提供了类似Go Goroutine的体验每个协程拥有自己的栈。
- 开发者可以像写普通同步代码一样进行编程网络IO等操作会被框架自动调度对用户透明。
- 同样是“无色”函数,不强制区分同步和异步函数。
Stackless (`tokio` 框架):
- 这是Rust官方和社区的主流方案基于 `async/await` 语法。
- 编译器将异步代码转化为状态机,内存占用极低。
- 必须遵循 `async/await` 的语法规则,存在“有色”函数问题,但换来了更高的性能和更精细的控制。
]
#title-slide[
设计异步化的通信流程
]
#slide()[
我们目前学习的方案大部分是同步的协议,即协议的数据几乎要依赖于上一步的状态或结果。
Damgård @Dam2009 提出一个全异步的协议设计概念,即协议的正确性不依赖与消息是否在规定时间内到达。
同时这篇文章提出#link("https://github.com/mgeisler/viff")[VIFF]框架现已archive且继任为MP-SPDZ
]
#slide()[
近年来还提出了一些新的方案。如hbMPC@hbMPC 还有Dumbo-MPC@Dumbo-MPC
]
#title-slide()[
总结
]
#slide()[
综合前面讨论的通信瓶颈、异步编程思想以及各种优化协议我们可以勾勒出MPC中异步化通信流程的设计思路
- 核心思想:非阻塞与并发
- 充分利用异步编程模型确保通信操作不会阻塞主计算线程从而提高CPU利用率。
- 通信调度DAG驱动
- 将通信任务抽象为有向无环图DAG通过拓扑排序实现通信的批处理和并发执行最大限度地减少等待时间。
- 协议选择:高效可靠
- 优先选用基于HTTP/2 (如gRPC) QUIC (如HTTP/3) 的协议,利用其多路复用、快速握手和高效序列化等特性。
- 连接管理:长连接与复用
- 维护连接池,实现长连接的复用,避免频繁建立和关闭连接的开销。
]
// Bibliography
#let bib = bibliography("bibliography.bib")
#bibliography-slide(bib)