This case analyzes and optimizes the performance of GroupedMatmul operators in the per-token quantization scenario. The computation process of a GroupedMatmul operator (written in Python code) is as follows:
1 2 3 4 5 | offset = 0 for i in range(g): mmOut = x[offset:offset + groupList[i]] * weight[i] + bias[i] y[offset:offset + groupList[i]] = Gelu(mmOut * scale[i] * pertokenScale[offset:offset + groupList[i]]) offset += groupList[i] |
The verification platform is
The following operator specifications are used as an example.
input |
shape |
data type |
format |
|---|---|---|---|
x |
(1024,1024) |
int8 |
ND |
weight |
(8,1024,8192) |
int8 |
NZ |
bias |
(8,8192) |
int32 |
ND |
groupList |
8 |
int64 |
ND |
scale |
(8,8192) |
float |
ND |
pertokenScale |
1024 |
float |
ND |
y |
(1024,8192) |
float16 |
ND |
The optimization methods are described below:
Eight cores are used for the test. That is, numBlocks is fixed at 8 in the current performance and subsequent tiling optimization.
Use the msProf tool to obtain the operator profile data.
Eight cores are used for the test. Run the msprof op command to obtain the cycle ratios of instructions.

The following figure shows the instruction pipeline chart obtained by running msprof op simulator.

Analyze the performance based on the preceding two types of data (real data and simulation data).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | uint32_t vecCount = 0; uint32_t taskRation = GetTaskRatio(); for (uint32_t offsetN = 0; offsetN < curCubeSingleN; offsetN += mnConfig.baseN) { if (unlikely(offsetN + mnConfig.baseN >= curCubeSingleN)) { curVecBaseN = curCubeSingleN - offsetN; } uint32_t alignBaseN = Ceil(curVecBaseN, uint32_t(8)) * 8; DataCopyScale(curVecBaseN, alignBaseN, scaleOffset + offsetN); uint32_t curVecBaseM = vecBaseM; uint64_t mmOutOffset = mnConfig.workSpaceOffset + offsetN * mnConfig.baseM; CrossCoreWaitFlag(SYNC_AIC_TO_AIV); for (uint32_t offsetM = 0; offsetM < curCubeSingleM; offsetM += vecBaseM) { vecCount++; if (vecCount % taskRation != subBlockIdx) { continue; // AIV0 and AIV1 are used for computing alternately. } if (unlikely(offsetM + vecBaseM >= curCubeSingleM)) { curVecBaseM = curCubeSingleM - offsetM; } // Use the AscendDequant API to perform per-channel dequantization. LocalTensor<cT::T> mmOutLocal = vecInQueue.AllocTensor<cT::T>(); DataCopyPad2D(mmOutLocal, mmOutGm[mmOutOffset + offsetM * curVecBaseN], curVecBaseM, curVecBaseN, curVecBaseN); vecInQueue.EnQue(mmOutLocal); ComputeDequantAndActivate(mnConfig, curVecBaseM, alignBaseN, curVecBaseN, offsetM); LocalTensor<DTYPE_Y> yLocal = vecOutQueue.DeQue<DTYPE_Y>(); DataCopyPad2D(yGm[outOffset + offsetM * tiling->n + offsetN], yLocal, curVecBaseM, curVecBaseN, alignBaseN, tiling->n); vecOutQueue.FreeTensor(yLocal); } ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | if ASCEND_IS_AIC { if (cubeCount >= tiling->parallNum) { // tiling->parallNum is set to 4. CrossCoreWaitFlag(SYNC_AIV_TO_AIC); } mm.SetOrgShape(mnConfig.m, tiling->n, tiling->k); mm.SetSingleShape(curSingleM, curSingleN, tiling->k); mm.SetTensorA(xGm[xOffset]); auto weightSlice = weightGm[weightOffset]; if (mnConfig.numBlocksM == 1) { weightSlice.SetL2CacheHint(CacheMode::CACHE_MODE_DISABLE); } mm.SetTensorB(weightSlice); uint64_t workspaceOffset = mnConfig.workSpaceOffset; while (mm.Iterate()) { mm.GetTensorC(mmOutGm[workspaceOffset], 0, true); CrossCoreSetFlag<2, PIPE_FIX>(SYNC_AIC_TO_AIV); workspaceOffset += (mnConfig.baseM * mnConfig.baseN); } } cubeCount++; |
1 2 3 4 | pipe->InitBuffer(scaleInQueue, 2, tiling->mmTilingData.baseN * sizeof(DTYPE_SCALE)); pipe->InitBuffer(perTokenScaleInQueue, 2, tiling->mmTilingData.baseM * sizeof(float)); pipe->InitBuffer(vecInQueue, 2, tiling->ubCalSize * sizeof(cT::T)); pipe->InitBuffer(vecOutQueue, 2, tiling->ubCalSize * sizeof(DTYPE_Y)); |
When Vector computation is waiting for the output data of Cube computation, the Cube computation needs to release the workspace to store the computation result of the next round after the Vector computation is complete. Currently, two workspaces are used for parallel Cube and Vector computations.

Vector and Cube computations may use the same workspace for data transfer, which leads to data dependency. Therefore, there are waiting intervals.
Four workspaces can be used for optimization.

After the optimization, the total time is reduced from 154.2 μs to 131.8 μs. The instruction pipeline chart shows that the gaps between the Vector and Cube computations are significantly reduced.
