안녕하세요.
RNGD에서 `32x64` 크기의 `i32` tile을 vector engine으로 처리하는 커널을
실험하던 중, 같은 크기의 VRF operand 하나를 사용하는 짧은 커널은 정상
컴파일되지만 해당 operand를 여러 단계에서 연속으로 교체하는 커널은 VRF
allocation 단계에서 실패하는 현상을 확인했습니다.
이 현상이 RNGD의 물리적인 VRF bank/lifetime 제약인지, 공개 API의 표현
제약인지, 아니면 Opt 0.5.1 scheduler/register allocator의 문제인지 확인을
부탁드립니다.
## 환경
```text
OS: Linux 5.15.0-177-generic
rustc: 1.97.0-nightly (2026-04-30)
toolchain: nightly-2026-05-01
cargo-furiosa-opt: 0.5.1
furiosa-opt-std: 0.5.1
target: RNGD, device(chip = 1)
```
이번 결과는 물리 NPU 실행 결과가 아니라 `cargo furiosa-opt compile`의 direct
compile 결과입니다.
## 공통 mapping
```rust
axes![
CLUSTERS = 2,
LOCAL_LIMBS = 8,
N = 256,
K = 256,
];
type Chip = m![1];
type Cluster = m![CLUSTERS];
type Slice64x32 = m![LOCAL_LIMBS, K / 32, N / 64];
type Tile64x32 = m![K % 32, N % 64];
type TileDm = DmTensor<i32, Chip, Cluster, Slice64x32, Tile64x32>;
type TileVrf = VrfTensor<i32, Chip, Cluster, Slice64x32, Tile64x32>;
```
slice 하나가 담당하는 VRF operand 크기는 다음과 같습니다.
```text
32 x 64 x sizeof(i32)
= 32 x 64 x 4
= 8,192 bytes/slice
```
전체 512 slices 기준으로는 `4,194,304 bytes`입니다. 실패 로그의 allocation
request 크기와 일치합니다.
## 성공하는 단일 8 KiB VRF probe
다음 커널은 `rhs` 하나를 VRF에 올린 뒤 `lhs + rhs`를 계산합니다.
```rust
#[device(chip = 1)]
pub fn vrf_8k_single_probe(
ctx: &mut Context,
lhs: &HbmTensor<i32, Chip, m!\[CLUSTERS, LOCAL_LIMBS, K, N\]>,
rhs: &HbmTensor<i32, Chip, m!\[CLUSTERS, LOCAL_LIMBS, K, N\]>,
) → HbmTensor<
i32,
Chip,
m!\[CLUSTERS, LOCAL_LIMBS, K / 32, N / 64, K % 32, N % 64\],
> {
let lhs: TileDm = lhs.to_dm_at(&mut ctx.tdma, 0);
let rhs: TileDm = rhs.to_dm_at(&mut ctx.tdma, 2 \* 8192);
let rhs_vrf: TileVrf = ctx
.sub
.begin(rhs.view())
.fetch::<m!\[K % 32, N / 8 % 8\], m!\[N % 8\]>()
.collect::<m!\[K % 32, N / 8 % 8\], m!\[N % 8\]>()
.to_vrf_at(0);
let output: TileDm = ctx
.main
.begin(lhs.view())
.fetch::<m!\[K % 32, N / 8 % 8\], m!\[N % 8\]>()
.collect::<m!\[K % 32, N / 8 % 8\], m!\[N % 8\]>()
.vector_init()
.vector_intra_slice_tag(TagMode::Zero)
.vector_fxp(FxpBinaryOp::AddFxp, &rhs_vrf)
.vector_final()
.commit_trim::<m!\[N % 8\]>()
.commit_at(4 \* 8192);
output.to_hbm(&mut ctx.tdma)
}
```
컴파일 결과는 다음과 같습니다.
| 항목 | 결과 |
| — | —: |
| binary 생성 | 성공, 37,305 bytes |
| total cycle | 14,048 |
| Main union | 537 cycles |
| Sub union | 1,287 cycles |
| spill | 0 |
즉 `8 KiB/slice`인 단일 VRF operand 자체는 Opt 0.5.1에서 허용되는 것으로
보입니다.
성공 probe는 다음과 같이 direct compile했습니다.
```bash
FURIOSA_OPT_OUT_DIR=/tmp/vrf-8k-single \
RUSTFLAGS=‘–cfg feature=“structured-per-limb-probes” --cfg feature=“q31-barrett-shoup-probes” --cfg feature=“logical-64-tile-probes”’ \
cargo furiosa-opt compile rngd_logical_tile_64x32_vrf_probe \
-p rngd-ntt \
–dump-schedule vrf-8k-single.schedule.json \
–dump-summary /tmp/vrf-8k-single-summary
```
## 실패하는 연속 vector pipeline
실패 커널은 같은 `TileDm`/`TileVrf` 타입으로 modular reduction을 여러 단계
연결합니다. 각 단계는 개념적으로 아래와 같습니다.
```rust
let rhs0_vrf: TileVrf = ctx.sub
.begin(rhs0.view())
.fetch::<Time, Packet>()
.collect::<Time, Packet>()
.to_vrf_at(0);
let tmp0: TileDm = ctx.main
.begin(lhs0.view())
.fetch::<Time, Packet>()
.collect::<Time, Packet>()
.vector_init()
.vector_intra_slice_tag(TagMode::Zero)
.vector_fxp(FxpBinaryOp::MulInt, &rhs0_vrf)
.vector_final()
.commit_trim::<Packet>()
.commit_at(SLOT0 \* 8192);
// rhs0_vrf의 마지막 consumer 이후 다음 operand를 같은 VRF 주소로 적재
let rhs1_vrf: TileVrf = ctx.sub
.begin(rhs1.view())
.fetch::<Time, Packet>()
.collect::<Time, Packet>()
.to_vrf_at(0);
let tmp1: TileDm = ctx.main
.begin(tmp0.view())
.fetch::<Time, Packet>()
.collect::<Time, Packet>()
.vector_init()
.vector_intra_slice_tag(TagMode::Zero)
.vector_fxp(FxpBinaryOp::AddFxp, &rhs1_vrf)
.vector_final()
.commit_trim::<Packet>()
.commit_at(SLOT1 \* 8192);
// 같은 형태의 8 KiB VRF operand 교체가 이어짐
```
처음에는 multiply와 add를 하나의 vector pipeline에서 두 VRF operand로
처리했으나 VRF allocation에 실패했습니다. 두 연산을 위와 같이 서로 다른
DM pass로 분리한 뒤에도 실패 지점만 뒤로 이동하고, 첫 full reduction
kernel은 다음 오류로 컴파일되지 않았습니다.
```bash
FURIOSA_OPT_OUT_DIR=/tmp/vrf-8k-chain \
RUSTFLAGS=‘–cfg feature=“structured-per-limb-probes” --cfg feature=“q31-barrett-shoup-probes” --cfg feature=“logical-64-tile-probes”’ \
cargo furiosa-opt compile rngd_ntt_logical_64x32_wave16_round3 \
-p rngd-ntt \
–dump-schedule vrf-8k-chain.schedule.json \
–dump-summary /tmp/vrf-8k-chain-summary
```
```text
error: furiosa-opt: …: visa: V1 failed for all operator schedule heuristics:
DfsPostOrder: BeamSearch failed after the beam shrank to zero at step 78/622;
most frequent terminal error (1/2):
Cannot find evict target for Vrf
(required: AllocRequest { wait_targets: [], size: 4194304 })
SethiUllman: BeamSearch failed after the beam shrank to zero at step 78/622;
most frequent terminal error (1/2):
Cannot find evict target for Vrf
(required: AllocRequest { wait_targets: [], size: 4194304 })
```
`4,194,304 / 512 = 8,192`이므로 실패한 allocation도 동일한
`8 KiB/slice` VRF operand로 보입니다. `wait_targets`가 비어 있는데도 기존
VRF allocation을 evict/reuse할 대상을 찾지 못하는 이유를 이해하기
어렵습니다.
참고로 동일 프로젝트의 기존 `32x32 i32` tile은 operand 하나가
`4 KiB/slice`이며, 서로 다른 두 operand를 동시에 사용하는 dual-VRF
pipeline도 정상 컴파일됩니다. 반면 `64x64 i32 = 16 KiB/slice` operand는
`insufficient register file: required 16384`로 즉시 거부됩니다.
정리하면 현재 관찰 결과는 다음과 같습니다.
| VRF 사용 형태 | Opt 0.5.1 direct compile |
| — | — |
| `4 KiB/slice` operand 하나 | 성공 |
| `4 KiB/slice` operand 두 개를 함께 사용하는 dual-VRF | 성공 |
| `8 KiB/slice` operand 하나를 사용하는 짧은 커널 | 성공, spill 0 |
| `8 KiB/slice` operand를 순차적으로 교체하는 긴 pipeline | `Cannot find evict target for Vrf` |
| `16 KiB/slice` operand 하나 | `insufficient register file` |
## 문의 사항
1. RNGD vector engine의 slice당 VRF 총용량과 bank 구성은 어떻게 되나요?
`4 KiB` operand 두 개와 `8 KiB` operand 하나가 물리적으로 동일한 자원을
사용하는지도 궁금합니다.
2. 위 `Cannot find evict target for Vrf` 오류는 문서화된 하드웨어 제약을
compiler가 보고한 것인가요, 아니면 Opt 0.5.1 scheduler/register
allocator의 한계 또는 버그에 해당하나요?
3. 하나의 VRF operand가 마지막 consumer에서 사용된 뒤, 같은 VRF 공간을
다음 operand에 재사용하는 것이 정상적으로 지원되는 동작인가요?
그렇다면 위 그래프에서 lifetime이 끝나지 않은 것으로 판단되는 이유가
무엇인지 확인 부탁드립니다.
4. 공개 API에서 VRF tensor의 lifetime 종료, release, bank 선택 또는 재사용
순서를 명시할 방법이 있나요? 현재는 각 단계에서 `to_vrf_at(0)`을
사용하고 있습니다.
5. `Main`과 `Sub` 사이의 overlap 때문에 다음 VRF operand를 미리 준비해야
해서 발생하는 제약이라면, overlap을 포기하고 완전히 순차 실행하도록
지정하는 API나 schedule hint가 있나요?
6. 현재 toolchain에서 권장되는 우회법은 `32x64` tile을 두 개의 `32x32`
view로 나눠 vector reduction을 수행하는 것뿐인지, 8 KiB operand를 그대로
유지할 수 있는 다른 공개 API가 있는지도 안내 부탁드립니다.
필요하다면 성공 probe의 전체 schedule JSON과 실패 full kernel 소스를 별도
경로로 전달드릴 수 있습니다.
감사합니다.