[furiosa-opt 0.4] TDMA layout broadcast의 cycle 증가와 일부 mapping의 correctness 문제

안녕하세요. GEMM 커널을 구현하다 보니 추가적인 궁금증이 생겨서 질문드립니다.

RNGD에서 일반 row-major GEMM 입력을 여러 slice로 배치하는 과정에서 TDMA 비용이 크게 늘어나는 현상을 확인했습니다. 단순한 연속 HBM→DM 복사는 문서에 설명된 startup latency 범위로 보이지만, compact source를 여러 slice로 broadcast하는 mapping에서는 source가 작아질수록 cycle이 오히려 증가하거나 결과가 틀리는 경우가 있었습니다.

- DMA Engine: DMA Engine - Programming Tensor Contraction Processors

- Memory Performance: Memory Performance - Programming Tensor Contraction Processors

## 환경

```text

Host: Ubuntu, Linux 6.8.0-124-generic, x86_64

Device: RNGD npu5

Device firmware reported by furiosa-smi: 2026.2.0 (a5db283)

Installed furiosa-driver-rngd: 2026.3.0

Installed furiosa-firmware-image-rngd: 2026.3.0

rustc: 1.97.0-nightly (2026-04-30), selected by nightly-2026-05-01

furiosa-opt-std: 0.4.0

```

## 최소 재현

모든 kernel은 HBM 입력을 같은 32 KiB DM tensor로 옮긴 뒤, correctness 확인을 위해 같은 packed HBM layout으로 다시 저장합니다.

```rust

axes![ProbePe = 8, ProbeLocalSlice = 64, ProbeValue = 32];

type ProbeCluster = m![ProbePe / 4];

type ProbeSlice = m![ProbePe % 4, ProbeLocalSlice];

type ProbeElement = m![ProbeValue];

type FullLayout = m![ProbePe, ProbeLocalSlice, ProbeValue];

type CompactF2Layout = m![ProbePe, ProbeLocalSlice / 2, ProbeValue];

type CompactF8Layout = m![ProbePe, ProbeLocalSlice / 8, ProbeValue];

type CompactF64Layout = m![ProbePe, ProbeLocalSlice / 64, ProbeValue];

let loaded: DmTensor<bf16, m![1], ProbeCluster, ProbeSlice, ProbeElement> =

input.to_dm_at(&mut ctx.tdma, 0);

loaded.to_hbm(&mut ctx.tdma)

```

`CompactF2`, `CompactF8`, `CompactF64`의 기대 결과는 각 source group을 각각 2개, 8개, 64개 local slice에 반복한 값입니다.

컴파일 명령:

```bash

FURIOSA_OPT_OUT_DIR=/tmp/tdma-f64 \

RUSTFLAGS=‘–cfg=feature=“tdma-read-probes”’ \

cargo furiosa-opt compile rngd_tdma_compact_f64 \

–exact -p rngd-bf16-matmul \

–dump-visa /tmp/tdma-f64.visa \

–dump-schedule /tmp/tdma-f64.schedule.json \

–dump-summary /tmp/tdma-f64-summary

```

다른 variant도 kernel 이름만 바꿔 같은 방식으로 컴파일했습니다.

## 결과

| HBM→DM mapping | HBM source | DM destination | DmaLoad | DmaStore | 결과 |

|—|—:|—:|—:|—:|—|

| contiguous | 32 KiB | 32 KiB | 707 cycles | 8,457 cycles | 정확 |

| compact ×2 | 16 KiB | 32 KiB | 702 cycles | 8,457 cycles | 8,191개 불일치 |

| compact ×8 | 4 KiB | 32 KiB | 733 cycles | 8,457 cycles | 14,329개 불일치 |

| compact ×64 | 512 B | 32 KiB | 5,020 cycles | 8,457 cycles | 정확 |

| prepacked | 32 KiB | 32 KiB | 707 cycles | 8,457 cycles | 정확 |

Compiler cycle은 현재 0.4 toolchain에서 다시 생성했습니다. Correctness와 host wall은 같은 환경의 물리 `npu5`에서 측정했습니다. Host wall median은 129.002–152.733 µs였지만, 이 값에는 kernel launch와 runtime synchronization이 포함되어 있으므로 TDMA engine 시간으로 해석하지 않았습니다.

32 KiB contiguous load가 707 cycles인 것은 Book에 설명된 command당 약 500-cycle startup latency를 고려하면 특별히 이상하지 않아 보입니다. 확인이 필요한 부분은 세 가지입니다.

1. 512 B source를 32 KiB DM으로 배치하는 compact ×64 load가 contiguous보다 7.10배 긴 5,020 cycles입니다.

2. compact ×2와 ×8은 compiler가 오류 없이 EDF/vISA를 생성하지만 물리 NPU 결과가 틀립니다. 반면 같은 방식의 compact ×64는 정확합니다.

3. 모든 variant에서 32 KiB DM→HBM store가 8,457 cycles로 동일하며, contiguous HBM→DM load보다 약 12배 깁니다.

compact ×64의 LIR에는 `DmaLoad` 한 개가 생성됩니다. Source 쪽 반복 축의 stride는 0이고 destination은 64개 slice로 전개됩니다. Schedule만으로는 5,020 cycles가 하나의 command 내부 직렬화 때문인지, 내부 command 분할이나 HBM/DMN interleaving 문제 때문인지 확인하기 어렵습니다.

실제 4K GEMM에서도 비슷한 결과가 나왔습니다. Compact 입력 variant의 compiler estimated I/O는 203,085 cycles, prepacked variant는 58,262 cycles였습니다. Prepacked variant가 compiler 보고 기준 DRAM data를 16.25배 더 사용했는데도 I/O estimate는 compact의 28.7%였습니다. 출력도 일반 row-major store는 145,022 cycles였지만 tiled packed store는 2,167 cycles, reducer packed store는 1,304 cycles였습니다.

## 확인 부탁드리는 내용

1. `ProbeLocalSlice / N` 형태의 HBM mapping을 전체 `ProbeLocalSlice` DM mapping으로 옮기는 것이 지원되는 broadcast 표현인지 확인 부탁드립니다.

2. 지원되는 표현이라면 compact ×2와 ×8이 compile-time 진단 없이 잘못된 값을 만드는 이유를 확인해 주실 수 있을까요?

3. compact ×64의 5,020-cycle DmaLoad는 기대되는 비용인가요? 한 개의 LIR `DmaCommand`가 내부적으로 어떻게 분할되고 직렬화되는지 확인하는 방법도 알려주시면 감사하겠습니다.

4. 32 KiB DmaStore의 8,457 cycles는 64-byte packet에 대한 HBM partial-write/RMW 비용 때문인가요? Compiler가 연속 packet을 256-byte 단위로 합치도록 mapping이나 stream을 지정하는 권장 방법이 있다면 안내 부탁드립니다.

5. Compiler artifact에서 실제 packet size, DMA command 수, homogeneous/heterogeneous aggregate 여부, HBM channel 및 stack 사용, row conflict, RMW 횟수를 확인하는 방법이 있을까요?

6. 일반 row-major GEMM 입력과 출력을 별도 prepacking 없이 효율적으로 HBM↔DM에 배치하는 권장 mapping 예제가 있다면 공유 부탁드립니다.

필요하시면 최소 재현 프로젝트를 비롯해 EDF, vISA, LIR, schedule JSON, 물리 NPU 측정 결과 JSON을 제공하겠습니다.

안녕하세요, 최소 재현을 위한 vISA 코드를 제공해주실 수 있을까요?

안녕하세요. 말씀하신 최소 재현 자료를 정리해 첨부합니다.
아래 경로는 tdma-layout-broadcast-repro/\ 폴더를 기준으로 한 상대경로입니다.

```text

tdma-layout-broadcast-repro/

├── Cargo.toml

├── Cargo.lock

├── rust-toolchain.toml

├── README.md

├── src/

│ └── lib.rs

├── generated/

│ ├── contiguous.visa

│ ├── contiguous.schedule.json

│ ├── compact_f2.visa

│ ├── compact_f2.schedule.json

│ ├── compact_f8.visa

│ ├── compact_f8.schedule.json

│ ├── compact_f64.visa

│ └── compact_f64.schedule.json

└── evidence/

├── physical-npu-result-2026-07-23.json

└── physical-npu-result-2026-07-24.json

```

재현 코드 전체는 `src/lib.rs`에 있습니다. compiler가 생성한 vISA는 `generated/*.visa`, 각 명령의 예상 cycle은 `generated/*.schedule.json`에서 확인할 수 있습니다. 물리 NPU에서 값이 맞았는지 확인한 원본 결과는 `evidence/physical-npu-result-2026-07-24.json`입니다. `2026-07-23` 파일은 같은 오답이 반복되는지 비교하려고 함께 넣었습니다.

여기서 `compact`는 host에서 데이터를 미리 복제해 둔 방식이 아닙니다. HBM에는 여러 slice가 함께 사용할 값 한 벌만 저장하고, TDMA가 그 값을 2개, 8개 또는 64개 local slice로 복제하도록 mapping했습니다. 이 때문에 HBM 입력은 각각 16 KiB, 4 KiB, 512 B로 줄지만 DM에는 모두 32 KiB가 배치됩니다. 반대로 `prepacked`는 host가 slice별 값을 미리 복제해 HBM에 32 KiB 전체를 저장하며, TDMA는 이를 1:1로 옮깁니다.

재현 코드는 복제 없는 1:1 대조군 `contiguous`와 `compact_f2`, `compact_f8`, `compact_f64`로 구성했습니다. 네 커널 모두 HBM에서 DM으로 값을 옮긴 뒤 같은 32 KiB 결과를 HBM에 다시 저장합니다. 연산은 넣지 않아 TDMA layout 차이만 남습니다.

같은 `furiosa-opt 0.4` 환경에서 첨부 소스를 다시 컴파일했으며, 결과는 아래와 같습니다.

| kernel | HBM 입력 | DM 결과 | DmaLoad | DmaStore |

|—|—:|—:|—:|—:expressionless:

| `contiguous` | 32 KiB | 32 KiB | 707 cycles | 8,457 cycles |

| `compact_f2` | 16 KiB | 32 KiB | 702 cycles | 8,457 cycles |

| `compact_f8` | 4 KiB | 32 KiB | 733 cycles | 8,457 cycles |

| `compact_f64` | 512 B | 32 KiB | 5,020 cycles | 8,457 cycles |

생성된 vISA에서 compact 입력의 반복 간격은 각각 2, 8, 64로 나타납니다. 목적지는 64개 local slice 전체입니다. compiler는 각 커널을 `DmaLoad` 하나와 `DmaStore` 하나로 lowering했습니다. schedule의 cycle도 표와 같았습니다.

별도 JSON에는 2026-07-24에 `npu5`에서 다시 측정한 원본 결과를 담았습니다. warmup 2회 후 5회 측정했으며, `contiguous`와 `compact_f64`의 결과는 정확했습니다. `compact_f2`에서는 8,191개, `compact_f8`에서는 14,329개 값이 기대값과 달랐습니다. 두 경우 모두 첫 불일치는 flat index 33이었고, 실제값은 0.0, 기대값은 1.0이었습니다. 2026-07-23 측정과 오답 위치 및 개수가 같습니다.

해당 mapping은 compiler에서 오류 없이 처리됐으며 vISA와 schedule도 생성됐습니다. schedule에 기록된 `compact_f64`의 DmaLoad는 5,020 cycles입니다. `compact_f2`와 `compact_f8`의 오답은 2026-07-24 물리 NPU 실행 결과에서 확인했습니다. 표의 DmaLoad와 DmaStore cycle은 compiler 추정치이고, JSON의 host wall time에는 kernel launch와 runtime 비용이 포함됩니다.

아래 네 가지를 확인해 주시면 감사하겠습니다.

1. `ProbeLocalSlice / N` 입력을 전체 `ProbeLocalSlice` DM layout으로 옮기는 표현이 공식적으로 지원되는 broadcast mapping인지 확인 부탁드립니다.

2. 지원되는 mapping이라면 `compact_f2`와 `compact_f8`이 compile-time 진단 없이 잘못된 결과를 내는 원인을 확인해 주실 수 있을까요?

3. 512 B를 32 KiB DM layout으로 복제하는 `compact_f64`의 DmaLoad가 5,020 cycles로 산정되는 것이 예상된 동작인지 알고 싶습니다. 이는 32 KiB를 그대로 읽는 `contiguous`의 707 cycles보다 7.10배 큽니다.

4. 모든 경우에 32 KiB DmaStore가 8,457 cycles로 동일하게 산정되는 이유도 함께 설명해 주시면 감사하겠습니다.

필요하시면 당시 사용한 전체 host 실행 harness와 EDF/LIR도 추가로 전달드리겠습니다.

1 Like

안녕하세요. 확인이 늦어 죄송합니다.

첨부해 주신 LimeWire 링크가 아직 만료된 것으로 표시되지는 않지만, 현재 다운로드가 되지 않아 재현 자료를 확인하지 못하고 있습니다. 번거로우시겠지만 기존 tdma-layout-broadcast-repro.zip 전체를 다시 업로드해 주실 수 있을까요?

가능하시다면 이전에 말씀해 주신 host 실행 harness와 EDF/LIR도 함께 포함해 주시면 원인 분석에 도움이 될 것 같습니다. 다만 우선은 기존 zip만 다시 올려 주셔도 괜찮습니다.

안녕하세요. 기존 zip 파일 재업로드합니다. opt 0.4.0 버전 기준 발생했던 문제점이라 해당 버전의 코드인 점 양해 부탁드려요.

1. `ProbeLocalSlice / N` 입력을 전체 `ProbeLocalSlice` DM layout으로 옮기는 표현이 공식적으로 지원되는 broadcast mapping인지 확인 부탁드립니다.
2. 지원되는 mapping이라면 `compact_f2`와 `compact_f8`이 compile-time 진단 없이 잘못된 결과를 내는 원인을 확인해 주실 수 있을까요?

말씀대로 input: m![A / 2] → output: m![A]와 같은 경우에 A % 2가 broadcast로 처리되지 않고있는 문제가 있었습니다. 일단은 우회책으로, input: m![A / 2], output: m![A / 2, 2] 로 설정하고, reshape으로 output을 m![A] 로 만드는 방식으로 원하는 기능을 이룰 수는 있을 듯 합니다. switch engine 및 dma에서, 이러한 경우를 broadcast로 취급하는 fix를 진행하는 중입니다.

3. 512 B를 32 KiB DM layout으로 복제하는 `compact_f64`의 DmaLoad가 5,020 cycles로 산정되는 것이 예상된 동작인지 알고 싶습니다. 이는 32 KiB를 그대로 읽는 `contiguous`의 707 cycles보다 7.10배 큽니다.

HBM의 memory geometry는 256B 단위의 channel이 총 32개 존재하고, channel당 48GB/s의 bandwidth를 가져 총 1.5TB/s의 bandwidth를 가집니다. 데이터 크기가 너무 작아서, 전체 channel을 다 활용하지 못하면서 발생한 문제로 보입니다. 관련해서, 더 자세하게 cycle estimation을 할 수 있는 cycle 모델을 문서에 업데이트할 예정입니다.

4. 모든 경우에 32 KiB DmaStore가 8,457 cycles로 동일하게 산정되는 이유도 함께 설명해 주시면 감사하겠습니다.

8개 DMA Engine이 32 KiB를 나누므로 engine당 4 KiB이고, 한 번에 쓰는 양은 Element의 64 B입니다. DM slice는 각자 주소 공간이라 packet이 slice 경계를 넘지 못하므로, HBM에서 이웃 slice의 행이 한 256 B 안에 붙어 있어도 write는 slice마다 따로 나갑니다. 결과가 engine당 64회입니다.

write 하나의 비용은 두 층으로 나뉩니다.

정렬된 write 하나가 2.4 cycle입니다. HBM 1.5 TB/s를 8개 engine이 나눠 쓰므로 engine당 몫이 약 190 B/cycle이고(실장비 5.2 Gbps 기준으로는 170 B/cycle), 256 B transaction 하나가 1.4~1.5 cycle입니다. 여기에 주소 패턴 penalty가 곱해져 실측 2.4가 됩니다. 32 KiB짜리 전송은 32개 channel을 다 채우지 못해 그 penalty를 받습니다. 여기에, 256B에 정렬되지 않은 64B write이므로, 약 50배의 RMW 페널티가 곱해집니다.

자세한 estimation 수식은 이후에 문서에 업데이트 예정입니다.

1 Like