[furiosa-opt v0.8.1] Opt 프로파일의 device cycle 단위와 compiler schedule cycle 도메인 문의

## 요약

Opt 0.8.1로 컴파일한 커널을 물리 RNGD에서 프로파일링하면서 cycle 수치를 해석하는

데 필요한 정보가 문서에서 확인되지 않아 문의드립니다.

핵심 질문 세 가지입니다.

1. `FURIOSA_OPT_PROFILE`이 보고하는 span의 `begin`/`end`는 어느 주파수 기준입니까?

측정으로는 약 2.0 GHz이며 PE clock 1.0 GHz가 아닙니다.

2. Opt compiler schedule의 cycle은 어느 클럭 도메인입니까? 1.0 GHz로 추정했습니다.

3. 커널 내부를 여러 span으로 나눠 프로파일할 방법이 있습니까?

두 개의 서로 다른 커널로 측정했습니다. 하나는 저희가 구현 중인 보안 관련 연산의

커널(명령 1,224개)이고

다른 하나는 이 문의를 위해 만든 명령 5개짜리 복사 커널입니다. 후자는 전체 소스를

첨부할 수 있으며 그대로 재현하실 수 있습니다. 뒤쪽에 부수 질문 세 가지를 덧붙였습니다.

## 환경

| 항목 | 값 |

| — | — |

| Opt | `furiosa-opt-std 0.8.1`, `cargo-furiosa-opt 0.8.1` |

| Rust | `nightly-2026-05-01` |

| OS | Ubuntu 22.04.5 LTS, kernel 5.15.0-177 |

| Topology | `chips: 1, pes: 8` (cluster 2개) |

측정에 쓴 커널 두 가지입니다.

| | 명령 | critical path | 내용 |

| — | —: | —: | — |

| A. 실제 워크로드 | 1,224 | 292,520 cycles | 저희가 구현 중인 보안 관련 연산 |

| B. 복사 | 5 | 6,047 cycles | HBM 2 MiB를 DM으로 읽어 그대로 HBM에 씀. 연산 없음 |

B는 `#[device]` 함수 본문이 세 줄입니다.

```rust

#[device(chip = 1)]

pub fn profile_probe(ctx: &mut Device, input: &IoTensor) → IoTensor {

let dm = input.to_dm::<Cluster, Slice, Tile>(&mut ctx.tdma);

let mut output: IoTensor = HbmTensor::new();

dm.view().to_hbm_view(&mut ctx.tdma, output.view_mut());

output

}

```

## 측정 방법

`FURIOSA_OPT_PROFILE=info`와 `span::npu`를 받는 tracing subscriber를 함께 두고

실행했습니다. span의 `dur`은 SDK가 보고한 값을 그대로 쓰고 launch wall은 host에서

`Instant`로 launch 구간을 쟀습니다.

`warmup=2, iterations=20`, 결과 readback 없음(`D2H=0`)으로 반복 측정했습니다.

아래 통계는 warmup 2회를 뺀 20개 표본 기준입니다.

## 질문 1 — span의 cycle은 어느 주파수입니까

커널 A의 정상상태 `dur` 평균은 788,429 cycles인데, 이를 공개 스펙의 PE clock

1.0 GHz로 나누면 0.788 ms입니다. 같은 구간의 launch wall이 0.413 ms이므로 1.0 GHz는

성립하지 않습니다. 커널 시간이 launch wall보다 클 수 없습니다.

연속 launch의 span 시작 간격을 host 측 평균 launch wall로 나눠 역산했습니다.

| 커널 | cluster | 간격 | ÷ launch wall | 유도 주파수 |

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

| A. 실제 워크로드 | 0 | 826,620 cycles | 0.413 ms | 2.002 GHz |

| A. 실제 워크로드 | 1 | 826,749 cycles | 0.413 ms | 2.002 GHz |

| B. 복사 | 0 | 31,378 cycles | 0.0158 ms | 1.986 GHz |

| B. 복사 | 1 | 31,558 cycles | 0.0158 ms | 1.997 GHz |

명령 수가 245배 차이 나는 두 커널에서 같은 값이 나오므로 카운터는 워크로드와

무관하게 **약 2.0 GHz**로 보입니다.

- 이 값이 맞습니까?

- 이 카운터는 PE clock과 고정 비율(2배)입니까, 아니면 별도 reference timer입니까?

- 주파수 스케일링이나 칩·펌웨어 버전에 따라 달라집니까?

덧붙여 `begin` 값의 기준점도 궁금합니다. 커널 A를 단발로 두 번 실행했을 때 `begin`이

cluster 0은 825,387,506과 825,651,434, cluster 1은 509,315,547과 509,244,963으로 서로

매우 가까웠습니다. 부팅 이후 계속 도는 카운터라면 실행 시점이 다른데 이렇게 가깝기

어려워 보입니다. 이 값은 무엇을 기준으로 0에서 시작합니까? 지금은 cluster 간 비교가

불가능하다고 보고 `dur`만 쓰고 있습니다.

## 질문 2 — compiler schedule cycle은 어느 도메인입니까

schedule JSON에 클럭 정보가 없어 DMA 대역폭으로 추정했습니다. 커널 B의 schedule은

명령이 5개뿐이라 계산이 단순합니다.

| | 바이트 | 모델 소요 |

| — | —: | —: |

| DmaLoad | 2,097,152 | 2,277 cycles |

| DmaStore | 2,097,152 | 2,167 cycles |

두 가정으로 읽으면 함의 대역폭이 갈립니다.

| 가정 | DmaLoad | DmaStore | HBM3 peak(1.5 TB/s) 대비 |

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

| 1.0 GHz | 921 GB/s | 968 GB/s | 61% / 65% (가능) |

| 2.0 GHz | 1,842 GB/s | 1,936 GB/s | 123% / 129% (불가능) |

커널 A에서도 같은 결론이 나옵니다(DmaLoad 102,957,056 바이트 / 86,844 cycles →

1.0 GHz에서 1,186 GB/s, 2.0 GHz에서 2,371 GB/s로 peak의 158%).

2.0 GHz면 두 커널 모두 peak를 초과하므로 **1.0 GHz PE clock 기준**으로

판단했습니다.

- 이 해석이 맞습니까?

- schedule의 cycle과 프로파일의 device cycle을 비교하려면 어떤 환산이 정식입니까?

## 질문 3 — 커널 내부를 더 잘게 프로파일할 수 있습니까

`furiosa-opt-rt`의 `profile.rs`에 "the image names spans by the hits that begin

and end them"이라고 되어 있습니다. 커널 A와 B 모두 이미지가 `Task` 하나만 이름

붙여 cluster당 span이 1개씩만 나옵니다. 명령이 5개인 커널에서도 같으므로 커널

구조와 무관해 보입니다.

`furiosa-opt-std`에서 커널 코드용 span 선언 API를 찾지 못했습니다

(`control.rs`의 마커는 loop unroll 힌트였습니다).

- 커널 소스에서 프로파일 구간을 선언할 방법이 있습니까?

- `FURIOSA_OPT_PROFILE`을 `debug`/`trace`로 올리면 더 많은 span이 나옵니까?

(`ProfileRequest`가 level 3~5를 받아 장치로 전달하는 것은 확인했습니다.

아직 `info` 외의 값은 시도해 보지 않았습니다.)

- `image.profile().depth`를 늘리는 컴파일 옵션이 있습니까?

## 배경 — 왜 이게 필요한가

저희는 보안 관련 연산을 RNGD에 올리는 중이고 커널별 compiler cycle 합으로 전체

성능을 추정해 왔습니다. 그 추정이 실제와 얼마나 벌어지는지가 이번 측정의 동기입니다.

위 두 도메인을 확정하고 PE cycles로 환산하면 두 커널의 결과가 이렇게 갈립니다.

| 커널 | compiler 예측 | 실측(PE 환산) | 실제 / 예측 |

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

| A. 실제 워크로드 | 292,520 | 394,214 | 1.35배 |

| B. 복사 | 6,047 | 5,733 | 0.95배 |

순수 DMA 복사는 모델이 거의 맞고 연산 집약 커널에서 벌어집니다. 비율이 커널에

의존하므로 상수로 쓸 수 없다는 것까지는 확인했습니다.

커널 A의 차이 +101,694 PE cycles는 대부분 Main이 대기하는 시간으로 보입니다. 이

schedule은 critical path 292,520 중 Main을 255,782(87.4%)까지 채워 여유가 12.6%뿐인데,

실측에서는 Main 외 시간이 138,432 cycles(35.1%)로 늘어납니다. 커널 B는 Main을 쓰지

않으므로 이 현상이 없습니다.

DMA 실효 대역폭, 두 cluster의 HBM 공유, refresh/bank conflict를 후보로 보고 있으나

커널 내부 구간별 시간이 없어 어느 것이 얼마인지 가르지 못하고 있습니다. 질문 3이

해결되면 저희가 직접 좁힐 수 있습니다.

## 부수 질문

핵심 질문과 별개이며 급하지 않습니다.

### 4. launch당 고정 오버헤드 10~20 µs가 정상입니까

launch wall에서 커널이 차지하는 비중이 두 커널에서 크게 달랐는데, 차이의 절대값은

비슷했습니다.

| 커널 | launch wall | 커널(2.0 GHz 환산) | 나머지 |

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

| A. 실제 워크로드 | 413 µs | 394 µs | 19 µs (4.6%) |

| B. 복사 | 15.8 µs | 5.76 µs | 10.0 µs (63.5%) |

커널 길이가 26배 차이 나는데 나머지는 10~19 µs로 비슷합니다. 제출·완료대기에 붙는

고정 비용으로 보이는데, 이 범위가 정상입니까? 줄일 방법이 있습니까?

참고로 커널 A를 warmup 없이 단발로 실행하면 launch wall이 1.525 ms로 커지고 그중

1.124 ms(73.7%)가 커널 밖입니다. 첫 launch에만 붙는 비용이 따로 있는 듯합니다.

`warmup=2`면 충분해 보이는데 권장값이 있습니까?

### 5. `Device::new`가 317 ms 걸립니다

`Device::new(Topology { chips: 1, pes: 8 })` 한 번에 317 ms가 걸립니다. 서로 다른 두

바이너리에서 317.478 ms와 317.589 ms로 0.035% 안에서 일치했습니다. 워크로드와 무관한

고정 비용으로 보입니다.

프로세스당 한 번이라 반복 워크로드에서는 문제가 아니지만 단발 job으로 돌리는

환경에서는 큽니다. 이 값이 정상 범위입니까? 프로세스 간 재사용이나 초기화를 줄이는

방법이 있습니까?

### 6. cluster 간 부하가 갈리는 경우가 있습니까

커널 B에서 두 cluster의 `dur`이 상당히 달랐습니다.

| cluster | 평균 `dur` | 표준편차 |

| — | —: | —: |

| 0 | 11,465 cycles | 1,019 (8.9%) |

| 1 | 9,115 cycles | 337 (3.7%) |

cluster 0이 25.8% 더 오래 걸리고 변동폭도 두 배 넘게 큽니다. 같은 복사를 나눠 하는

커널인데 왜 갈리는지 궁금합니다. 커널 A에서는 788,429와 785,369로 0.31% 차이에

그쳤습니다.

## 재현 방법

커널 B의 전체 소스와 schedule을 첨부합니다. 크레이트 하나이며 소스는 500줄

남짓입니다.

```bash

RUST_MIN_STACK=67108864 cargo furiosa-opt build --release --bin rngd_opt_profile_probe

PROBE_BENCH=1 PROBE_WARMUP=2 PROBE_ITERS=20 \

FURIOSA_OPT_PROFILE=info PROBE_TRACE=stdout ./target/release/rngd_opt_profile_probe

```

출력에 host 구간과 NPU span이 Chrome trace JSON으로 함께 찍힙니다. 연속 launch의

span `begin` 간격과 `[bench] avg launch wall`을 비교하면 질문 1의 역산을 그대로

재현하실 수 있습니다.

SDK가 프로파일을 요청하려면 두 조건이 **동시에** 필요합니다

(`furiosa-opt-std/src/backend/npu/function.rs`).

```rust

profile_level() // FURIOSA_OPT_PROFILE >= info

.filter(|\_| tracing::enabled!(target: "span::npu", Level::INFO)) // span::npu subscriber

```

첨부한 `src/trace.rs`가 두 번째 조건을 충족하는 최소 subscriber입니다. 한 가지

덧붙이면, subscriber 없이 `FURIOSA_OPT_PROFILE`만 설정하면 경고 없이 프로파일링이

꺼진 채 실행됩니다. `log::warn!`으로 알리는 경로가 있으나 logger가 없으면 그 경고도

보이지 않아 처음에 원인을 찾는 데 시간이 걸렸습니다. 문서에 이 두 조건이 함께

적히면 도움이 될 것 같습니다.

최소 재현 커널 첨부합니다.

안녕하세요, 답변이 늦어서 죄송합니다. 자세한 측정과 재현 코드 감사합니다. 첨부해 주신 커널을 저희 RNGD에서 직접 돌려 보고 답변드립니다(furiosa-opt 0.8.1).

  1. span의 begin/end 기준 클럭

span 값은 각 cluster Tensor Unit Controller의 clock counter를 그대로 읽은 raw tick입니다. 측정하신 약 2.0 GHz는 평균적으로 맞습니다.
다만 TU clock은 고정 clock이 아니어서, firmware와 SoC가 동작 중에 주파수를 바꿀 수 있습니다. 그래서 tick 수를 고정 주파수로 나눈 값은 근사치입니다.

기준점은 Device::new에서 각 cluster를 초기화하는 시점입니다. process마다, cluster마다 따로 0에서 시작하므로 cluster끼리 begin을 비교할 수 없습니다. 단발 실행에서 begin 값이 비슷했던 것도 초기화부터 첫 launch까지의 시간이 매번 거의 같아서입니다. 지금처럼 dur만 쓰시는게 맞습니다.

참고로, 이 부분은 추후 고정 clock인 ARM core counter를 기준으로 TU tick을 보정해서, span을 ns 단위, launch 시작 기준으로 보고하도록 바꿔서 정확한 값으로 보고하도록 수정될 예정입니다.

  1. compiler schedule cycle의 클럭 도메인

판단하신 대로 1.0 GHz 기준, 즉 ns 단위입니다. 지금은 profile의 raw tick을 2로 나누면 schedule cycle과 비교할 수 있습니다. 위 1번처럼 TU clock이 바뀐 구간이 섞이면 오차가 생긴다는 점만 감안해 주세요. 1번 변경 후에는 두 값이 모두 ns가 되어 환산 없이 비교할 수 있고, schedule JSON에도 단위를 명시하겠습니다.

  1. 커널 내부 구간 profiling

지금도 FURIOSA_OPT_PROFILE을 올리시면 span이 더 나옵니다.

  • info: Task
  • debug: 칩 간 DMA, PCIe DMA 추가
  • trace: Main/Sub의 TU command마다, 칩 안의 tensor DMA, ClusterSync 같은 sync까지 전부

command 1,224개면 profile buffer(chunk당 8,192 hit) 안에 들어갑니다.

trace로 보실 때 주의하실 점이 있습니다. TU command span의 끝은 명령이 완료된 시점이 아니라, 같은 context의 다음 명령이 대기를 시작하는 시점에 찍힙니다. 그래서 TU command별 시간은 실제보다 길게 나올 수 있는 상한값입니다. DMA와 sync span은 이 영향을 받지 않습니다. Main이 대기하는 구간을 분석하실 때 이 점을 감안해 주세요.

  1. launch당 고정 오버헤드

저희 host에서는 정상상태 launch wall이 11.5–13.7 µs였고, 그중 커널 시간이 약 5.7 µs라 고정 비용은 약 6 µs였습니다. 10–20 µs와의 차이는 host 환경 차이로 보입니다.

첫 launch에만 붙는 비용은 device function을 처음 호출할 때 image를 parse하고 device에 load하는 비용입니다. 같은 Device에서 함수마다 한번만 발생하고 image 크기에 비례합니다. 두 번째 launch부터 정상상태이므로 warmup은 1회면 충분합니다.

현재 API는 launch가 끝나야 다음 launch를 제출할 수 있어서, 짧은 커널을 연속으로 돌리면 이 고정 비용이 매번 드러납니다. 앞 launch가 실행되는 동안 다음 launch를 미리 제출할 수 있도록 API를 개선하겠습니다.

  1. Device::new 317 ms

정상 범위입니다. Device::new는 사용하는 cluster마다 PE를 기동하고 firmware를 올리는데, 대부분의 시간이 여기서 듭니다. 초기화 과정에는 줄일 여지가 있어 개선하겠습니다. process 간 재사용은 지원하지 않으므로, 단발 job이 많다면 process를 유지하면서 요청을 받는 구조를 권합니다.

  1. cluster 간 dur 차이

부하 차이가 아니라 두 cluster의 출발 시각 차이입니다. runtime이 launch를 cluster 0, cluster 1 순서로 전달해서 cluster 1이 약 1.2 µs(약 2.5k tick) 늦게 시작합니다. 커널 끝의 ClusterSync에서 먼저 도착한 cluster 0이 그만큼 기다리고, 그 대기가 cluster 0의 dur에 포함됩니다.

trace level로 보시면 확인하실 수 있습니다. 두 cluster의 DMA 시간은 같고(약 4.4k tick), 차이는 전부 마지막 Cluster span에 있습니다(cluster 0 약 2.6k, cluster 1 약 0). cluster 0의 변동폭이 큰 것도 출발 시각 차이의 변동 때문입니다. 커널 A에서 차이가 0.3%였던 것도 같은 절대량(약 3k tick)이 긴 커널에 묻혀서입니다.

커널 전체 시간으로는 cluster 0의 dur을 쓰시면 됩니다. 늦게 출발한 cluster가 sync에 도착할 때까지를 포함하기 때문입니다. 출발 시각 차이는 launch 전달 방식을 바꿔 줄이겠습니다.


profiling 활성화 조건

지금 구조는 이렇습니다. FURIOSA_OPT_PROFILE이 device가 무엇을 기록할지(level)를 정하고, span::npu target을 받는 tracing subscriber가 결과를 어디로 보낼지를 정합니다. 받는 subscriber가 없으면 기록하지 않는데, 말씀하신 대로 이때 아무것도 알리지 않는 것은 말씀처럼 직관적이지 않아보이므로 다음 릴리즈에선 수정하겠습니다.

subscriber의 경우 직접 작성하지 않아도 아래를 이용하면 Chrome trace JSON을 얻으실 수 있습니다.


 use tracing_subscriber::prelude::*;

 let (layer, _guard) = tracing_chrometrace::ChromeLayer::with_writer(std::fs::File::create("trace.json")?);
 tracing_subscriber::registry().with(layer).init();
 // FURIOSA_OPT_PROFILE=trace 로 실행

자세한 리포트 감사합니다. 혹시 더 필요하신 내용이 있으시면 알려주세요.