Skip to content

Commit 6940d08

Browse files
authored
Merge pull request #2877 from zenonxiu81/kleidiai_sme2_matmul_explained
new learning path: KleidiAI SME2 matmul microkernel for quantized models explained
2 parents 1c8dffa + c3df642 commit 6940d08

22 files changed

Lines changed: 380 additions & 0 deletions
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: KleidiAI SME2 matmul microkernel for quantized models explained
3+
4+
draft: true
5+
cascade:
6+
draft: true
7+
8+
minutes_to_complete: 40
9+
10+
who_is_this_for: This is an advanced topic for software developers, performance engineers, and AI practitioners
11+
12+
learning_objectives:
13+
- Learn how a KleidiAI matmual microkernel performs matrix multiplication with quantized data
14+
- Learn how SME2 INT8 Outer Product Accumulate instructions are used for matrix multiplication
15+
- Learn how a KleidiAI SME2 matmul microkernel accelerates matmul operators in a Large Lanague Model
16+
- Learn how to integrate KleidiAI SME2 matmul microkernels to an AI framework or application
17+
18+
prerequisites:
19+
- Knowledge of KleidiAI and SME2
20+
21+
author: Zenon Zhilong Xiu
22+
23+
### Tags
24+
skilllevels: Advanced
25+
subjects: ML
26+
armips:
27+
- Arm C1 CPU
28+
- Arm SME2 unit
29+
tools_software_languages:
30+
- C++
31+
- KleidiAI
32+
- llama.cpp
33+
operatingsystems:
34+
- Android
35+
- Linux
36+
37+
38+
39+
further_reading:
40+
- resource:
41+
title: part 1 Arm Scalable Matrix Extension Introduction
42+
link: https://developer.arm.com/community/arm-community-blogs/b/architectures-and-processors-blog/posts/arm-scalable-matrix-extension-introduction
43+
type: blog
44+
- resource:
45+
title: part 2 Arm Scalable Matrix Extension Instructions
46+
link: https://developer.arm.com/community/arm-community-blogs/b/architectures-and-processors-blog/posts/arm-scalable-matrix-extension-introduction-p2
47+
type: blog
48+
- resource:
49+
title: part4 Arm SME2 Introduction
50+
link: https://developer.arm.com/community/arm-community-blogs/b/architectures-and-processors-blog/posts/part4-arm-sme2-introduction
51+
type: blog
52+
- resource:
53+
title: Profile llama.cpp performance with Arm Streamline and KleidiAI LLM kernels
54+
link: https://learn.arm.com/learning-paths/servers-and-cloud-computing/llama_cpp_streamline/
55+
type: blog
56+
57+
58+
59+
### FIXED, DO NOT MODIFY
60+
# ================================================================================
61+
weight: 1 # _index.md always has weight of 1 to order correctly
62+
layout: "learningpathall" # All files under learning paths have this same wrapper
63+
learning_path_main_page: "yes" # This should be surfaced when looking for related content. Only set for _index.md of learning path content.
64+
---
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
# ================================================================================
3+
# FIXED, DO NOT MODIFY THIS FILE
4+
# ================================================================================
5+
weight: 21 # The weight controls the order of the pages. _index.md always has weight 1.
6+
title: "Next Steps" # Always the same, html page title.
7+
layout: "learningpathall" # All files under learning paths have this same wrapper for Hugo processing.
8+
---
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Explain the SME2 matmul microkernel with an example - Part 1
3+
weight: 5
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
## Explain the SME2 matmul microkernel with an example - Part 1
10+
By integrating the SME2‑optimized KleidiAI kernels into llama.cpp, the heavy matrix‑multiplication workloads in the K, Q, and V computations of the attention blocks, as well as in the FFN layers, can be delegated to the SME2 matmul microkernel when running the Llama-3.2-3B-Q4_0.gguf model.
11+
In these operators, the LHS (activation) data type is FP32, while RHS (weight) type uses GGML Q4_0 quantized type.
12+
13+
To make the demonstration easier in this learning path, the LHS dimension [m, k] is simplified to [16, 64], the RHS dimension [n, k] is simplified to [64, 64], and the SME2 SVL is set as 512-bit.
14+
15+
###Packing the RHS
16+
Although the original Q4_0 RHS(weight) in the model uses INT4 quantization, it is signed INT4 quantization, rather than the unsigned INT4 quantization that the SME2 matmul microkernel requires. Moreover,the layout of the INT4 quantized data and the quantization scale does not meet the requirements of the SME2 matmul microkernel neither. Therefore, the LHS from the model needs to be converted from the signed INT4 data to unsigned INT4 and repacked.
17+
Since the RHS(weight) remains unchanged during the inference, this conversion and packing only need to be performed only once when loading the model.
18+
19+
20+
Let us have a close look at GGML Q4_0 quantization first to know how the orginal FP32 weight is quantized to Q4_0 format.
21+
In the Q4_0 model, the Q4_0 weights are stored in layout of [n, k].
22+
GGML Q4_0 quantizes weights in blocks of 32 floats. For each block, it calculates a scale for the block and then converts each value into a signed 4-bit integer. The scale is stored as FP16.
23+
Then GGML Q4_0 packs the values in a way of,
24+
- the low nibble (bits 0–3) holds the first value (even index)
25+
- and the high nibble (bits 4–7) holds the second value (odd index)
26+
Thus, each byte contains a low/high pair.
27+
The following diagram shows how GGML Q4_0 quantizes and packs the original [n, k] FP32 matrix into Q4_0 type with layout of [n, k].
28+
![Figure showing GGML Q4_0 quantization alt-text#center](images/q4_0_format.jpg "GGML Q4_0 quantization")
29+
30+
Unfortunately, the Q4_0 format does not meet the requirements of the SME2 matmul microkernel. It needs to be converted to an unsigned INT4 quantization format and repacked using the *kai_run_rhs_pack_nxk_qsi4c32ps1s0scalef16_qsu4c32s16s0_neon* function.
31+
32+
In this example, we use m=16 and k=64.
33+
- The required mr value for the SME2 matmul kernel is obtained using *kai_get_mr_matmul_clamp_f32_qai8dxp1vlx4_qsi8cxp4vlx4_1vlx4vl_sme2_mopa*. Here, mr=16.
34+
- The required nr value for the SME2 matmul kernel is obtained using *kai_get_nr_matmul_clamp_f32_qai8dxp1vlx4_qsi8cxp4vlx4_1vlx4vl_sme2_mopa*. Here, nr=64.
35+
- The required kr value for the SME2 matmul kernel is obtained using *kai_get_kr_matmul_clamp_f32_qai8dxp1vlx4_qsi8cxp4vlx4_1vlx4vl_sme2_mopa*. Here, kr=4.
36+
- The required sr value for the SME2 matmul kernel is obtained using *kai_get_sr_matmul_clamp_f32_qai8dxp1vlx4_qsi8cxp4vlx4_1vlx4vl_sme2_mopa*. Here, sr=2 (two INT4 elements in a byte).
37+
38+
The function call stack for this process in llama.cpp when loading the model is as follows:
39+
```text
40+
llama_model_load
41+
llama_model::load_tensors
42+
llama_model_loader::load_all_data
43+
ggml_backend_tensor_set
44+
ggml_backend_cpu_kleidiai_buffer_set_tensor
45+
ggml::cpu::kleidiai::tensor_traits::repack
46+
kai_run_rhs_pack_nxk_qsi4c32ps1s0scalef16_qsu4c32s16s0_neon
47+
```
48+
This process can be illustrated with the diagram below.
49+
![Figure showing RHS packing with KleidiAI alt-text#center](images/kai_kernel_packed_rhs.jpg "RHS packing with KleidiAI")
50+
51+
The numerical label of an element in the diagram is used to indicate its row and column number in the original matrix. For example ,
52+
![Figure showing Row_Col lable alt-text#center](images/row_col_lable.png "Row_Col lable")
53+
it indicates that the element locates at row 01, column 02 in the original matrix. This row and column number remains unchanged in its quantized and packed matrix, so that the location of the element can be tracked easily.
54+
55+
Now, the RHS is converted and packed into a format that can be handled by the SME2 matmul microkernel, allowing the packed RHS to be loaded into SME2 Z registers with sequential memory access. This improves memory access efficiency and reduces cache misses.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: Explain the SME2 matmul microkernel with an example - Part 2
3+
weight: 6
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
## Explain the SME2 matmul microkernel with an example - Part 2
10+
Next, the FP32 LHS (activation) needs to be quantized and packed when the llama.cpp graph runner computes the matmul nodes/operators.
11+
12+
### Quantization and Packing of the LHS
13+
Since the LHS (activation) keep changing, we need to dynamically quantize the original FP32 matrix and pack it into the qsi8d32p1vlx4 format. This can be achieved using the *kai_run_lhs_quant_pack_qsi8d32p_f32_neon* microkernel.
14+
15+
The function call stack for this process in llama.cpp is as follows:
16+
```text
17+
llama_context::decode
18+
llama_context::process_ubatch
19+
llama_context::graph_compute
20+
ggml_backend_sched_compute_splits
21+
ggml_backend_cpu_graph_compute
22+
ggml_graph_compute //tick off the compute thread
23+
ggml_graph_compute_thread //the compute thread
24+
ggml_compute_forward
25+
ggml_cpu_extra_compute_forward
26+
ggml::cpu::kleidiai::tensor_traits::compute_forward
27+
ggml::cpu::kleidiai::tensor_traits::compute_forward_q4_0
28+
kai_run_lhs_quant_pack_qsi8d32p_f32_neon
29+
```
30+
The diagram below illustrates how the RHS is quantized and packed by *kai_run_lhs_quant_pack_qsi8d32p_f32_neon*,
31+
![Figure showing Quantization and Packing of the LHS alt-text#center](images/kai_run_lhs_quant_pack_qsi8d32p_f32_neon_for_sme2.jpg "Quantization and Packing of the LHS")
32+
33+
The values of mr, nr, and kr can be obtained in the same way as described above.
34+
The mr, nr, and kr together with the matrix dimensions m and k are passed as parameters to *kai_run_lhs_quant_pack_qsi8d32p_f32_neon*. This function quantizes the FP32 LHS to signed INT8 type and packed the quantized data and quantization scales as shown in the diagram above. It divides the m x n matrix into submatrices of size mr x kr (it is 16 x 4) as shown in blocks outlined by dashed lines in the upper matrix of the diagram, and then sequentially packs the rows within each submatrix. This allows the SME2 matmul kernel to load an entire submatrix into an SME2 Z register from contiguous memory, thus reducing cache misses by avoiding loading the submatrix across multiple rows.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
title: Explain the SME2 matmul microkernel with an example- Part 3
3+
weight: 7
4+
5+
### FIXED, DO NOT MODIFY
6+
layout: learningpathall
7+
---
8+
9+
## Explain the SME2 matmul microkernel with an example - Part 3
10+
Once the required LHS and RHS are both ready, *kai_matmul_clamp_f32_qsi8d32p1vlx4_qsi4c32p4vlx4_1vlx4vl_sme2_mopa* microkernel can run now.
11+
12+
### Run the SME2 matmul microkernel
13+
The operations performed to compute an 16x64 result submatrice (four 16x16 submatrices) (1VL x 4VL) are as follows:
14+
15+
- Iterate along blocks along K dimension
16+
- Iterate in a block with step of kr (kr=4)
17+
- Load one SME2 SVL-length (512-bit) of data from the quantized and packed LHS (containing 64 INT8 values) into one SME2 Z register
18+
- Load two SME2 SVL-lengths of data from the packed RHS (containing 2 x64x2 INT4 values) into two SME2 Z registers, then use the SME2 LUTI4 lookup table instruction to convert these INT4 values into INT8 type, extending them to four SME2 Z registers (4VL).
19+
- Use the SME2 INT8 Outer Product Accumulate (MPOA) instruction to perform outer product operations with source from the Z register and each of the four Z registers, accumulates the results in four ZA tiles (which are initialized to zero). It produces intermediate results of four 16x16 output submatrices.
20+
The processes of the first itration can be illustrated in the diagram below:
21+
![Figure showing the first itration of the inner loop alt-text#center](images/run_matmul_sme2_step1.jpg "The first itration of the inner loop")
22+
The diagram below illustrates the process of the second iteration along the K dimension,
23+
![Figure showing the second itration of the inner loop alt-text#center](images/run_matmul_sme2_step2.jpg "The second itration of the inner loop")
24+
- After completing the iterations in the block, the intermediate INT32 results of four 16x16 output submatrices are dequantized with the per-block LHS and RHS scale to FP32 floats, using Floating-point Multiply (FMUL), Floating-point Multiply and Accumulate (FMLA) and Signed fixed-point Convert to Floating-point (SCVTF) vector instructions. It produces the intermediate FP32 results of four 16x16 output submatrices.
25+
- Accumulate the FP32 result above
26+
27+
After completing itration along the K dimension, the FP32 results of four 16x16 output submatrices is ready. Then, save the result into memory.
28+
29+
The code can be found [here](https://github.com/ARM-software/kleidiai/blob/main/kai/ukernels/matmul/matmul_clamp_f32_qsi8d32p_qai4c32p/kai_matmul_clamp_f32_qsi8d32p1vlx4_qai4c32p4vlx4_1vlx4vl_sme2_mopa_asm.S#L80)
30+
Some comments are added to the code to help understanding the code.
31+
```asm
32+
KAI_ASM_LABEL(label_3) // K Loop
33+
KAI_ASM_INST(0xc00800ff) // zero {za} , zeros the four ZA tile (za0.s, za1.s, za2.s, za3.s)
34+
mov x11, x4 //Set block size
35+
KAI_ASM_LABEL(label_4) // Block Loop
36+
KAI_ASM_INST(0xa0404342) //ld1w {z2.s - z3.s}, pn8/z, [x26] // load two VLs packed RHS data (64x2x2 INT4 data)
37+
addvl x26, x26, #2 // increase RHS address by two VLs
38+
ld1h {z8.h}, p0/z, [x3] //load one VL quantized and packed LHS data (64 INT8 data)
39+
addvl x3, x3, #1 // increase LHS address by one VLs
40+
KAI_ASM_INST(0xc08a4044) // luti4 {z4.b - z5.b}, zt0, z2[0] //use LUT4I instruction to convert INT4 to INT8, one source VL produces two VLs result
41+
KAI_ASM_INST(0xc08a4066) // luti4 {z6.b - z7.b}, zt0, z3[0] //use LUT4I instruction to convert INT4 to INT8, one source VL produces two VLs result
42+
KAI_ASM_INST(0xa0840100) // smopa za0.s, p0/m, p0/m, z8.b, z4.b ] //Outer Product Accumulate with the VL of LHS, the first VL of RHS and ZA0.S
43+
KAI_ASM_INST(0xa0850101) // smopa za1.s, p0/m, p0/m, z8.b, z5.b //Outer Product Accumulate with the VL of LHS, the second VL of RHS and ZA1.S
44+
KAI_ASM_INST(0xa0860102) // smopa za2.s, p0/m, p0/m, z8.b, z6.b //Outer Product Accumulate with the VL of LHS, the third VL of RHS and ZA2.S
45+
KAI_ASM_INST(0xa0870103) // smopa za3.s, p0/m, p0/m, z8.b, z7.b b //Outer Product Accumulate with the VL of LHS, the forth VL of RHS and ZA3.S
46+
47+
subs x11, x11, #4 //block_index - 4
48+
b.gt label_4 //end of block iteration?
49+
50+
// the code below performs per block dequantization of the four tiles with LHS and RHS scales
51+
mov w12, #0
52+
mov x25, x24
53+
ld1b {z17.b}, p4/z, [x3] // lhs sum
54+
ld1b {z16.b}, p4/z, [x3, #1, mul vl] // lhs scale
55+
addvl x3, x3, #2
56+
KAI_ASM_INST(0xa040c354) // ld1w { z20.s - z23.s }, pn8/z, [x26] // rhs zp
57+
KAI_ASM_INST(0xa041c340) // ld1w { z0.s - z3.s }, pn8/z, [x26, #4, mul vl ] // rhs scale
58+
addvl x26, x26, #8
59+
pfalse p3.b
60+
KAI_ASM_LABEL(label_5)
61+
// omit some codes that perform the block quantization and save the result to memory
62+
……
63+
blt label_5
64+
subs x10, x10, x4 //decrease the K index
65+
b.gt label_3 //end of K loop?
66+
67+
```
68+
In a single block loop, four pipelined SME2 INT8 MOPA instructions perform 4,096 MAC operations, calculating the intermediate results for the four 16x16 submatrices. It proves that SME2 MOPA can significantly improve matrix multiplication performance.
69+
70+
To help understand the whole process, we map the first itration of LHS and RHS quantization and packing steps, as well as SME2 outer product accumulate operation and dequantization, back to the original FP32 LHS and RHS operations. Essentially, they equally perform the operation as shown below (there might be some quantization loss),
71+
![Figure showing the original matrix representing of the first itration alt-text#center](images/run_matmul_sme2_original_present_step1.jpg "the original matrix representing of the first itration")
72+
73+
The second iteration can be mapped back to the original FP32 LHS and RHS operations as below,
74+
![Figure showing the original matrix representing of the second itration alt-text#center](images/run_matmul_sme2_original_present_step2.jpg "the original matrix representing of the second itration")
75+
76+
**Note**: In this diagram, the RHS is laid out in the dimension of [N, K], which is different from the [K, N] dimension layout of the RHS in the video demonstration of 1VLx4VL. If you interpret the RHS in the diagrams above using the [K, N] dimension, you can match the previous video demonstration with the diagrams above.
77+
78+
By repeating the submatrix computation across the M and N dimensions, the entire result matrix can be calculated. If a non-empty bias is passed to the SME2 matmul microkernel, it also adds the bias to the result matrix.
446 KB
Loading
33.2 KB
Loading
Loading
429 KB
Loading
1.2 KB
Loading

0 commit comments

Comments
 (0)