Skip to content

Commit e72dd59

Browse files
committed
fix display bug
Signed-off-by: Zhi Yiliu <2584074296@qq.com>
1 parent 0fec6aa commit e72dd59

7 files changed

Lines changed: 68 additions & 28 deletions

File tree

docs/blogs/llm_inference/LLMparallelization.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,42 @@ Pipeline Parallelism,Expert Parallelism,Attention-FFN Decoupling
6969
Attention 的层间并行、冗余参数消除方式与线性层的方式一致,**层内并行的主要差异是 TP 和 SP**
7070

7171
### Sequence Parallelsim
72+
73+
- 只切分 Q 的序列
74+
Q 的切分后的尺寸为$[bs, heads, seq\_len/SP, head\_dim]$,按照 attention 计算:
75+
1. 求解 score,Q x K 相当于左矩阵行切,score 尺寸:$[bs, heads, seq\_len/SP, seq\_len]$
76+
2. softmax 求解的是最后一个维度,计算元素值相同,得到 attention_weights,
77+
3. attention_weights 与 V 进行矩阵乘,还是左矩阵行切运算,元素值相同,计算得到 O 的分块结果
78+
4. **将计算的 O 进行 allgather,结果相等**
79+
- 只切分 K 的序列
80+
score 尺寸是$[bs, heads, seq\_len, seq\_len/SP]$,进一步计算 softmax,由于最后一个维度的数据只有之前的一半长度,而 softmax 的计算跟整个序列相关,直接拼接会导致结果不相等。所以,**单独切 K 序列后拼接,结果不等**
81+
- 只切分 V 序列
82+
得到 attention_weights 尺寸完整,计算 attention_weights x V,因为 V 矩阵被行切,所以 attention_weights 需要列切(Matmul parallelism 的图),**最后 allreduce 能够获得完整结果**
83+
84+
- 当前常用的方式:QKV 按照相同方式切分,然后对计算结果进行修正,这个方法在 BlockwiseMulti-HeadAttention、ring attention、tree attention、merge attention 中都有应用,softmax 修正:
85+
- 每个切分 Qi 要与所有的 Ki、Vi 进行一次计算,得到 Oi
86+
- 最后的分块 Oi 结果需要进行修正
87+
88+
#### prefill
89+
90+
推理的 prefill 阶段,Q 的序列长度与 KV 保持一致,**开启 SP 后 GPU 之间需要交换 KV 值与 Q 进行运算**(ring-attention 方式)
91+
92+
- 每个 rank 的 Q 与 KV 匹配计算完后获得三个输出值,然后进行结果修正得到[O_X0, O_X1, O_X2],X 值为 rank 序号。最后每个 rank 将自己的分块结果进行聚合(加法)运算得到结果 O_X。
93+
![](img/ringattention.png)
94+
95+
#### decode
96+
97+
在 decode 阶段 Q 的长度为 1,若对应的 KV 值分散在各个 GPU 设备中,可以将 Q 复制 N 份与切片的 KV 值进行 attention 计算后,最后将结果 gather 到一个设备上再计算修正结果。
98+
![](img/decode.jpg)
99+
100+
- 这个方式意味着 GPU0 需要完成最大值、修正运算、求和运算,GPU0 可能成为瓶颈。一种 Tree attention 算法对过程做了改进,提升了通信效率。**就是把 gather 换成多次 allreduce,这种方式在跨节点场景中优势明显**
101+
102+
### Tensor Parallelism
103+
104+
Attention 的张量并行分为两个部分:QKV 运算、线性投影运算。
105+
106+
QKV 运算的 TP 切分一般**针对 heads 维度**,相当于矩阵的并行的批量运算。
107+
108+
- attention 按照 heads 进行切分
109+
- linear 按照 hidden_size / heads 进行切分
110+
![](img/tp+sp.jpg)

docs/blogs/llm_inference/cudaopt.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ warp contexts 的数量决定了 SM 上能同时并发的 block 数量
112112
- Shared Memory: 共享内存是每个线程块(block)内所有线程共享的内存空间。共享内存的访问延迟远低于全局内存
113113
- L1 / L2 Cache: L1 缓存是每个 SM 独有的,**而 L2 缓存是所有 SM 共享的**
114114

115-
<img src="img/gpu_memory.jpg" alt="memory_hierarchy" style="width:600px;" />
115+
![](img/gpu_hardware.jpg){ width="600px" }
116116

117117
:laughing:**Summary**
118118

@@ -147,7 +147,7 @@ CUDA 将计算任务组织成一个三级层次结构 。这是一个由程序
147147
- 网格 (Grid):为执行单个 Kernel 而启动的所有线程块的集合 。一个 Grid 内的所有线程都可以访问同一个全局内存空间。Grid 内的线程块被假定为独立执行,且执行顺序不确定;它们之间没有直接的同步机制。
148148

149149
线程块和网格可以被组织成一维、二维或三维的结构,这为将计算任务映射到向量、矩阵、体数据等数据结构上提供了极大的便利 。  
150-
<img src="img/grid.jpg" alt="cuda_hierarchy" style="width:600px;" />
150+
![](img/grid.jpg){ width="600px" }
151151

152152
### Kernel 执行流程
153153

@@ -176,42 +176,42 @@ CUDA 将计算任务组织成一个三级层次结构 。这是一个由程序
176176

177177
- 合并访问(理想):Warp 中的线程 i 访问内存地址 base + i。这在处理按行主序存储的矩阵的行时非常常见
178178

179-
<img src="img/coalesce-smem.png" alt="coalesce" style="width:500px;" />
179+
![](img/coalesce-smem.png){ width="500px" }
180180

181181
- 跨步访问(问题):线程 i 访问 base + i \* stride。如果步长(stride)很大,这将导致许多独立的、低效的内存事务。这在访问按行主序存储的矩阵的列时很常见
182182

183-
<img src="img/uncoalesce-smem.png" alt="coalesce" style="width:500px;" />
183+
![](img/uncoalesce-smem.png){ width="500px" }
184184

185185
- 非对齐访问:Warp 访问的起始地址未与内存事务的大小对齐
186186

187187
### Avoid Bank Conflicts in Shared Memory
188188

189189
:warning: Shared memory is organized into **32 banks**. Each bank is a slice of SRAM that can load or store **4 bytes of data every cycle**.
190-
<img src="img/smem.jpg" alt="shared_memory" style="width:500px;" />
190+
![](img/smem.jpg){ width="500px" }
191191

192192
- 当一个 Warp 中的所有 32 个线程访问全局内存中的连续位置时,硬件可以将这 32 个小的请求“合并”成一个单一、大型、高效的内存事务
193-
<img src="img/conflict-free.png" alt="bank_conflict" style="width:500px;" />
193+
![](img/conflict-free.png){ width="500px" }
194194
- 当同一个 Warp 中的两个或更多线程试图访问位于同一个内存银行中的不同地址时,就会发生银行冲突 。此时,这些访问会被串行化处理,从而降低了共享内存的有效带宽
195-
<img src="img/bank-conflict.png" alt="bank_conflict" style="width:500px;" />
195+
![](img/bank-conflict.png){ width="500px" }
196196

197197
#### Solutions
198198

199199
- Padding: 在数据结构中插入填充元素,以改变数据在内存中的布局,避免多个线程访问同一银行
200-
<img src="img/padding.jpg" alt="padding" style="width:500px;" />
200+
![](img/padding.jpg){ width="500px" }
201201
- 可能降低 SM 的 occupancy
202202
- 可能地址访问不对齐,无法使用向量化访问
203203
- **swizzling:** 重新组织数据的存储方式,使得并行访问时更少冲突(更常用):rocket:
204204

205205
- 某些 swizzling 方法在从 shared memory 读数据到 register 时不能进行 float4 的合并读取
206206

207-
<img src="img/swizzling.jpg" alt="swizzling" style="width:600px;" />
207+
![](img/swizzling.jpg){ width="600px" }
208208

209209
- 逻辑位置表示元素在矩阵中的逻辑坐标。
210210
- 物理位置表示其对应元素在实际存储数据的 shared memory 中的位置坐标。
211211

212212
> 当我们说读取矩阵的第 2 行第 3 列的元素,(2,3)就表示逻辑位置,而真正读取数据的时候,我们需要从实际存储数(2,1)的 shared memory 中对应的位置
213213
214-
<img src="img/smem2.jpg" alt="swizzling2" style="width:600px;" />
214+
![](img/smem2.jpg){ width="600px" }
215215

216216
:warning: 广播 (Broadcast): 如果一个 Warp 中的所有线程访问同一个银行中的完全相同的地址,这是一种广播操作,不会产生冲突
217217

@@ -241,4 +241,4 @@ const int b_tile_index = warp_id % 2 * 32 + lane_id % 8 * 4;
241241
- 一次性从全局内存中加载一小块 A (BM x BK) 和一小块 B (BK x BN) 到共享内存中
242242
- 一个线程块内的所有线程就可以在共享内存上快速地进行大量的计算,以完成对应的一小块 C (BM x BN) 的计算
243243
- 每个线程不再是只计算 C 块中的一个元素,而是负**责计算一个更小的结果网格**(图中是 2x2)。这样做可以进一步提升数据复用率和计算效率
244-
<img src="img/tile2.png" alt="tiling" style="width:500px;" />
244+
![](img/tile2.png){ width="600px" }
19.5 KB
Loading
364 KB
Loading
72.6 KB
Loading

docs/projects/nebula-vsearch/summary.md

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,15 @@
6969

7070
- 向量数据类型与 List 类型不同,除了比较运算符外,它**不应该支持其他的运算符**,如加减乘除等。
7171
- 实际向量类型是一个`std::vector<float>`,在存储时会将其序列化为二进制字符串存储在 RocksDB 中。
72-
<img src="img/vector.png" alt="vector" style="width:200px;">
72+
![](img/vector.png){ width="200px" }
7373

7474
### 向量存储支持(RocksDB Multi Column Family)
7575

7676
- 这里我们采用向量存储与其他图属性分离的方式,方便后续进行 ANN 索引构建。
7777
- 为了实现与其他数据的隔离,我们需要将向量数据存储在向量列族中,其他数据存储在默认列族中。
7878
> :warning:这里的 ID-VID 列族是为了后续实现 ANN 索引创建使用的
7979
80-
<img src="img/rocksdb.png" alt="column_family" style="width:200px;">
80+
![](img/rocksdb.png){ width="200px" }
8181

8282
### 向量类型的 Key 结构
8383

@@ -100,14 +100,14 @@
100100
- 向量列:专门用于 VECTOR 类型
101101
- Schema 属性选项:如 TTL、TTL_COL 等
102102

103-
<img src="img/schema.png" alt="schema" style="width:300px;">
103+
![](img/schema.png){ width="400px" }
104104

105105
此外,还需修改 RowWriter 和 RowReader 以支持向量列操作:
106106

107107
- RowWriter 将向量数据写入 RocksDB 的 vector 列族
108108
- RowReader 从 RocksDB 的 vector 列族读取向量数据
109109

110-
<img src="img/rowreader&writer.png" alt="rowwriter" style="width:600px;">
110+
![](img/rowreader&writer.png){ width="600px" }
111111

112112
### :skull: 向量属性的 DML 语句(Insert/Update/Delete/Upsert)
113113

@@ -118,17 +118,17 @@
118118
- Storage 节点接收到 Request 后,会生成 Storage 节点的计划,最后调用存储引擎接口将数据写入 RocksDB
119119
- 返回结果给 Client 端
120120

121-
<img src="img/insert_dml.png" alt="dml" style="width:400px;">
121+
![](img/insert_dml.png){ width="400px" }
122122

123123
:warning:这里面有一些细节需要注意:
124124

125125
- Storage Cache: storaged 会定期使用心跳拉取 metad 最新的 Schema 信息,更新本地的 Schema 缓存,从而获取最新的 Schema 信息。我们需要确保**向量列的信息也能正确地被缓存和更新(后续还有向量索引的更新)**
126-
<img src="img/storage_cache.png" alt="storage_cache" style="width:200px;">
126+
![](img/storage_cache.png){ width="200px" }
127127
- 为了确保 DML 操作的一致性,我们在 DML 流程中进行了修改,要求每个请求仅使用一次异步操作将数据写入。具体实现是在数据真正写入的 `Raft::commitLog()` 函数中,通过检查键(Key)的第一个字节(Type),来判断需要写入到哪个列族中。
128-
<img src="img/doput.png" alt="doput" style="width:400px;">
128+
![](img/doput.png){ width="400px" }
129129
- 除了 Graghd 会生成物理计划,实际上 Storaged 也会生成自己的执行计划,这个计划会调用存储引擎的接口将数据写入 RocksDB 中。我们需要为获取向量属性修改相关计划节点(TagNode/EdgeNode)。
130130
- 为了支持 TagNode/EdgeNode 多向量属性的支持,在 TagNode/EdgeNode 中增加了`std::vector<RowReader> vecReaders_`成员变量,用于读取多个向量属性的值。
131-
<img src="img/storage_plan.png" alt="storage_plan" style="width:400px;">
131+
![](img/storage_plan.png){ width="400px" }
132132

133133
### 向量索引功能(Ann Index in Common)
134134

@@ -245,15 +245,15 @@ CREATE EDGE ANNINDEX <index_name> ON <tag_name_list>::(<field_name>) [IF NOT EXI
245245
- `VertexID → VectorID`
246246
- 最后构建向量索引,并将其加载到内存中。
247247

248-
<img src="img/create_ann_index.png" alt="create_index" style="width:600px;">
248+
![](img/create_ann_index.png){ width="600px" }
249249

250250
:warning: Storaged 中实际上也会有一个计划,进行真正的索引创建工作:
251251

252252
1. 从 RocksDB 中扫描所有的向量数据
253253
2. 将 Vertex ID 与 Vector ID 的映射关系存储到 id-vid 列族中(:star2:**这就是为什么我们需要三个列族**)
254254
3. 构建向量索引并加载到内存中
255255

256-
<img src="img/ann_index_storage_plan.png" alt="ann_index_storage_plan" style="width:600px;">
256+
![](img/ann_index_storage_plan.png){ width="600px" }
257257

258258
### :skull::skull:ANN Search
259259

@@ -282,17 +282,17 @@ match (v:tag5:tag6) return v order by euclidean(vector(1.0,2.0,3.0), v.vec) app
282282
1. 经过优化规则处理后的物理执行计划会执行 `TagAnnIndexScan`,在 storaged 收到请求后,会生成一个内部执行计划,在 IVF 索引上进行搜索,并将结果返回给 graphd
283283
2. Graphd 调用 storaged 的 `getProp` 算子生成执行计划,以获取节点的非向量属性,最后执行 Limit 和 Project,得到最终结果
284284

285-
<img src="img/ann_search.png" alt="ann_search_overview" style="width:500px;">
285+
![](img/ann_search.png){ width="600px" }
286286

287287
#### 优化规则
288288

289289
- 对不返回向量距离的语句,优化器工作流程如下图所示:
290290

291-
<img src="img/opt-rule1.png" alt="ann_search_graphd" style="width:400px;">
291+
![](img/opt-rule1.png){ width="400px" }
292292

293293
- 对返回向量距离的语句,优化器工作流程如下图所示:
294294

295-
<img src="img/opt-rule2.png" alt="ann_search_graphd" style="width:400px;">
295+
![](img/opt-rule2.png){ width="400px" }
296296

297297
#### 执行流程
298298

@@ -302,15 +302,15 @@ match (v:tag5:tag6) return v order by euclidean(vector(1.0,2.0,3.0), v.vec) app
302302

303303
> 实际上,storaged 接收到 graphd 的请求后会启动它自己的执行器,并在其中生成 storaged 的执行计划,计划执行会从 Ann Index 中获取数据,最后返回结果给 graphd。
304304

305-
<img src="img/ann_search2.png" alt="ann_search_detailed" style="width:600px;">
305+
![](img/ann_search2.png){ width="600px" }
306306

307307
实际上,在 storaged 中,`AnnIndexScan` 的处理过程与 `IndexScan` 非常相似。
308308

309309
- 需要先进行初始化,然后执行 `doExecute`。实际的数据获取是通过调用 `resetIter` 完成的。
310310
> 此时是通过 ANN 索引获取结果(这里得到的只是向量 ID,而不是实际的顶点 ID)
311311
- 随后 `doNext` 只是简单地遍历这些结果,再调用 `AnnIndexVertexScan::getVidByVectorid() `从 RocksDB 中获取实际的 VID 数据,并将 VID 与向量距离数据一并返回给 graphd。
312312

313-
<img src="img/ann_search_storaged.png" alt="ann_index_scan" style="width:200px;">
313+
![](img/ann_search_storaged.png){ width="300px" }
314314

315315
## 功能测试
316316

@@ -336,7 +336,7 @@ match (v:tag5:tag6) return v order by euclidean(vector(1.0,2.0,3.0), v.vec) app
336336
| ANN Search | [Ann Search](https://github.com/vesoft-inc/nebula/pull/6104) |
337337

338338
- tck 测试用例结果:
339-
<img src="img/create_ann_index_test.png" alt="create_ann_index_test" style="width:600px;">
339+
![](img/create_ann_index_test.png)
340340

341341
### 单元测试
342342

mkdocs.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,10 @@ nav:
320320
- index: blogs/index.md
321321
- LLM Inference:
322322
- Introduction for LLM Inference: blogs/llm_inference/Introduction.md
323+
- CUDA Optimization: blogs/llm_inference/cudaopt.md
323324
- Parallelizatoin in ML System: blogs/llm_inference/parallelization.md
325+
- Parallelization in LLM Inference: blogs/llm_inference/LLMparallelization.md
324326
- PageAttention: blogs/llm_inference/pageattention.md
325-
- CUDA Optimization: blogs/llm_inference/cudaopt.md
326327
- nebula:
327328
- Vistor Pattern in nebula: projects/nebula-vsearch/understanding/1.visitor pattern.md
328329
- Memory Management in nebula: projects/nebula-vsearch/understanding/2.memory management.md

0 commit comments

Comments
 (0)