[furiosa-opt v0.5.1]8 KiB/slice VRF operand의 연속 vector pipeline scheduling 실패

안녕하세요.

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 소스를 별도

경로로 전달드릴 수 있습니다.

감사합니다.

안녕하세요. `8 KiB/slice` operand를 순차적으로 교체하는 긴 pipeline’ 은 8KiB 짜리 텐서들의 lifetime이 disjoint 하게 코드를 짜셨다면 가능해야 합니다.(스케줄러가 지원해야 합니다) 나머지는 표에서 언급해주신게 맞습니다.

첨부해주신 코드 부분만으로는 제가 재현이 안되는데 혹시 컴파일 실패하는 전체 커널을 첨부해주실 수 있으실까요?

안녕하세요. 최소 재현 커널 압축해서 보내드립니다.
tar -xzf /home/os0906/furiosa-opt-vrf-eviction-repro.tar.gz

확인했습니다.
지금 보니 문서에 설명이 빠져있는데, vector_stash() 는 현재 running tensor 의 일부를 vrf 에 저장해두기 때문에 (현재는 보수적으로 1024B 로 되어 있습니다) 이 용량을 고려해야 합니다. 이미 modulus_vrf 텐서가 VRF에 8KiB 를 꽉 채워쓰는데, vector_stash를 불렀기 때문에 용량 초과 에러가 난 것으로 보입니다. 첨부해주신 커널도 stash 를 부르고 소모하는 line 만 지우면 정상적으로 스케줄링됨을 확인했습니다.

문의 사항에 대해 답변해드리겠습니다.

  1. slice당 VRF 총용량과 bank 구성은 알고 계신대로 8KiB, 2 bank 가 맞습니다. visa 사용자는 bank를 고려하지 않고 커널을 짜면 밑단의 컴파일러가 알아서 bank 분배를 잘 하도록 디자인되어 있습니다. `4 KiB` operand 두 개와 `8 KiB` operand 가 동일한 VRF 자원을 사용한다고 보셔도 무방합니다.
  2. 이번 케이스는 문서화된 하드웨어 제약을 compiler가 보고한 것에 가깝겠네요.
  3. 정상적으로 지원되는 동작입니다.
  4. `to_vrf_at(0)` 는 vrf의 address 를 지정해서 allocate 하는 API 였는데, address를 명시하는 API 는 다음 release부터 삭제될 예정입니다. 앞서 말했듯 bank 선택 가능한 API 는 지원하지 않을 계획이고, 명시적인 tensor free 또한 아직 계획이 없습니다.
  5. `Main`과 `Sub` overlap 여부와는 관계없이, Main ctx 에서 요구하는 VRF 사용량이 초과해서 나는 에러였습니다. 본 이슈와 별개로 더 나은 스케줄이 가능한 상황임에도 sequential 실행을 강제하는 옵션은 존재하지 않습니다.
  6. 위 답변으로 갈음합니다.

문서에 설명이 미비해서 혼란스러우셨을 것 같습니다. 문서는 곧 고쳐두겠습니다. 감사합니다.