DMA Engine
DMA Engine은 Tensor Unit 파이프라인을 거치지 않고 메모리 계층 간에 텐서를 직접 이동하므로, 계산 단계가 필요 없는 상주 위치 변경이나 레이아웃 변경에 사용하십시오.
Fetch, 계산, Commit 단계가 필요 없는 HBM↔HBM, HBM↔DM, DM↔DM 이동에는 DMA를 선택하십시오. 인터페이스 예제는 소스 텐서, 목적지 매핑, tdma 또는 pdma 컨텍스트가 어떻게 결합되는지 보여 줍니다. 각 전송은 서로 조율되는 두 단계를 짝지어 수행합니다.
- 읽기 Sequencer: 소스 계층에서 읽습니다.
- 쓰기 Sequencer: 대상 계층에 쓰며, 필요하면 레이아웃 변환을 함께 수행합니다.
DMA 전송은 수학적 텐서 이동입니다. 즉, 레이아웃이 달라도 출력은 입력과 동일한 수학적 텐서를 담습니다. Tensor DMA는 DMN 간, Cluster 간, Chip 간 전송을 모두 포괄하며, Chip ID는 시스템 전체에서 전역적으로 합의됩니다.
전송 처리량과 관련된 고려 사항은 최적화를 참조하십시오.
인터페이스
DMA 전송은 한 메모리 계층의 텐서를 받아 다른 (또는 같은) 계층의 텐서를 생성합니다. 커널 작성자는 소스 텐서에 대해 .to_dm(), .to_hbm() 또는 관련 메서드를 호출하면서 DmaContext를 전달합니다.
Device::tdma: Chip 내부 전송(HBM ↔ HBM, HBM ↔ DM, DM ↔ DM)을 위한 Tensor DMA 컨텍스트입니다.Device::pdma: 호스트 ↔ HBM 전송을 위한 PCIe DMA 컨텍스트입니다(PCIe DMA 참조).
impl<D: Scalar, Chip: M, Element: M, B: Backend> HbmTensor<D, Chip, Element, B> {
/// Converts to data memory tensor.
#[primitive(HbmTensor::to_dm)]
pub fn to_dm<Cluster: M, Slice: M, Element2: M>(
&self,
_dma: &mut DmaContext<{ Dma::Tensor }, B>,
) -> DmTensor<D, Chip, Cluster, Slice, Element2, B> {
assert_dma_layout::<
D,
m![{ Chip }, { Element }],
Element,
m![{ Chip }, { Cluster }, { Slice }, { Element2 }],
Element2,
>(DM_WRITE_ALIGN_BYTES);
DmTensor::from_parts(self.inner.transpose(true), None)
}
/// Reshapes the tensor to a different mapping at the same HBM address, consuming `self`.
/// The HBM analogue of [`DmTensor::reshape`]; both delegate to [`Tensor::reshape`].
///
/// # Safety
///
/// The per-level sizes (`Chip::SIZE == Chip2::SIZE`, `Element`) are asserted at compile time below
/// (see [`constraints::assert_hbm_reshape_dimension_preserved`]); the genuine precondition is
/// [`Tensor::reshape`]'s: the old and new mappings must lay the elements out in the SAME physical
/// (wire) order, so the relabel moves no data. Axis regrouping (merge/split) preserves wire order
/// and is valid; a permutation is not (use a transpose). Equal sizes do not guarantee this.
/// Consuming `self` is the safety contract made explicit: the old-shaped handle cannot survive to
/// alias the same HBM bytes under a conflicting mapping.
#[primitive(HbmTensor::reshape)]
pub unsafe fn reshape<Chip2: M, Element2: M>(self) -> HbmTensor<D, Chip2, Element2, B> {
constraints::assert_hbm_reshape_dimension_preserved::<Chip, Chip2, Element, Element2>();
let reshaped = unsafe { self.inner.reshape::<m![{ Chip2 }, { Element2 }]>() };
HbmTensor {
inner: reshaped,
buffer: self.buffer,
}
}
}
컴파일러는 소스 텐서 타입과 목적지 텐서 타입으로부터 읽기 Sequencer와 쓰기 Sequencer의 구성을 도출합니다. HBM에서 DM으로의 전송은 HBM의 Chip 차원을 유지하고 목적지의 Cluster, Slice, Element 차원을 지정합니다. DM에서 DM으로의 전송은 새로운 Chip, Cluster, Slice, Element 차원을 지정할 수 있으며, HBM 목적지는 Chip과 Element를 지정합니다.
아래 예제는 HBM에서 HBM으로의 전송 두 번을 사용하여 텐서를 [A, B, C]에서 [C, A, B]로 전치합니다.
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 8, B = 16, C = 32];
fn transpose_simple(
device: &mut Device,
input: &HbmTensor<f32, m![1], m![A, B, C]>,
) -> HbmTensor<f32, m![1], m![C, A, B]> {
// Step 1: [A, B, C] → [A, C, B]
let intermediate: HbmTensor<f32, m![1], m![A, C, B]> = input.to_hbm(&mut device.tdma);
// Step 2: [A, C, B] → [C, A, B]
intermediate.to_hbm(&mut device.tdma)
}
let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let in_hbm = HbmTensor::<f32, m![1], m![A, B, C]>::new();
let _out_hbm = transpose_simple(&mut device, &in_hbm);
}
계층을 넘나드는 전송은 목적지 타입의 매핑을 통해 레이아웃 변환도 함께 수행합니다. HBM에서 DM으로의 전송에서는 목적지 DM 텐서에 Cluster 축과 Slice 축이 추가되며, 이 축들이 하드웨어 파티션 전체에 텐서를 분산하고 브로드캐스트합니다.
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 2048];
fn hbm_to_dm(
device: &mut Device,
input: &HbmTensor<i8, m![1], m![A]>,
) -> DmTensor<i8, m![1], m![1 # 2], m![A / 8], m![A % 8]> {
input.to_dm::<m![1 # 2], m![A / 8], m![A % 8]>(&mut device.tdma)
}
let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let in_hbm = HbmTensor::<i8, m![1], m![A]>::new();
let _out_dm = hbm_to_dm(&mut device, &in_hbm);
}
여기서 2,048개 요소로 이루어진 벡터는 Slice당 256개 요소(Slice = m![A / 8]), Slice당 8개 요소(Element = m![A % 8])로 분산되어 2개의 Cluster에 걸쳐 배치됩니다.
아키텍처
각 RNGD Chip에는 DMN 쌍마다 하나씩, 총 8개의 DMA Engine이 있으며, 최대 8개의 독립적인 전송을 병렬로 실행합니다. 단일 DMA Engine은 짝을 이룬 읽기 Sequencer와 쓰기 Sequencer를 실행하며, 텐서 이동은 aggregate를 통해 여러 엔진에 분산됩니다. 아래 하위 절에서는 정적 구조, Sequencer 표현, 동적 동작, 컴파일러 도출, 집계 연산을 설명합니다.
Note
DMA는 SRAM Slice 대역폭을 충분히 활용하지 못할 수 있으므로, SRAM 간 전송에서는 Tensor Unit(Fetch Engine과 Commit Engine 경유)이 DMA보다 효율적인 경우가 많습니다. 그러나 실제로는 HBM 대역폭이 병목이 되는 경우가 일반적이므로, HBM ↔ DM 전송에서는 이 차이가 덜 중요합니다.
정적 구조
Chip, Cluster, Slice는 하드웨어의 공간 병렬성 차원입니다. Chip당 8개의 DMA Engine은 서로 다른 메모리 구성 요소 간 전송을 병렬로 수행합니다(예: 엔진 #0이 HBM ↔ DM을 처리하는 동안 엔진 #1은 DM ↔ DM을 처리합니다).
각 DMA Engine은 짝을 이루는 읽기 Sequencer와 쓰기 Sequencer를 보조를 맞춰 동시에 실행합니다. 읽기 Sequencer는 소스 주소를 순회하고, 쓰기 Sequencer는 목적지 주소를 순회합니다. 두 Sequencer는 같은 루프 횟수를 공유하지만 서로 다른 스트라이드와 기준 주소를 사용합니다. 레이아웃 변환으로 같은 논리적 요소가 소스 메모리와 목적지 메모리에 놓이는 순서가 달라지기 때문입니다. 컴파일러는 읽기 횟수와 쓰기 횟수가 같다는 점을 활용하여, 이 쌍을 루프 항목마다 짝지어진 스트라이드를 갖는 단일 Sequencer로 간결하게 표현합니다.
컴파일러는 하나의 텐서 이동을 사용 가능한 DMA Engine들에 분산합니다. 작업을 chip, cluster, slice 차원을 따라 분할하고 각 분할을 하나의 DMA Engine에 할당합니다. 어떤 DMA Engine이든 어떤 전송이든 처리할 수 있습니다. 로컬 DMN 접근이 DMN 간 접근보다 빠르므로, 컴파일러는 기본적으로 소스 DM의 로컬 DMA Engine을 선택합니다. 커널 작성자가 엔진을 명시적으로 지정할 수도 있습니다.
동종(homogeneous) 집합은 참여하는 모든 DMA Engine에 걸쳐 매개변수화된 하나의 디스크립터 템플릿을 사용합니다. 이종(heterogeneous) 집합은 각 DMN을 해당 DMN 전용 디스크립터와 짝짓는 HashMap<DmnIndex, DmaDescriptor>를 사용합니다. DM 텐서 명세는 정확한 메모리 위치를 식별하기 위해 매핑 표현식에 chip, cluster, slice를 포함해야 합니다.
동적 동작
DmaSequencer의 각 루프는 행 우선(row-major) 순서로 카운터를 증가시키며, 짝지어진 스트라이드로부터 읽기 주소와 쓰기 주소를 도출합니다. base = (0, 256³)인 아래 Sequencer의 경우는 다음과 같습니다.
[
A -> 256 : (65,536, 256),
B -> 256 : (256, 65,536),
C -> 256 : (1, 1),
] : 256
반복 i | 카운터 | 읽기 주소 | 쓰기 주소 |
|---|---|---|---|
| 0 | (0, 0, 0) | 0 | write_base |
| 1 | (0, 0, 1) | 1 | 1 + write_base |
| … | … | … | … |
| 255 | (0, 0, 255) | 255 | 255 + write_base |
| 256 | (0, 1, 0) | 256 | 65,536 + write_base |
i = a·256² + b·256 + c | (a, b, c) | i | 256·a + 256²·b + c + write_base |
stride0 = 256이면 하드웨어가 반복마다 256바이트를 읽고 쓰므로, 반복 0은 (A, B, C) = (0, 0, 0..255)의 모든 값을 하나의 패킷으로 처리합니다. 전송은 약 500사이클의 시작 지연 시간에 256 × 256사이클의 데이터 전송 시간을 더한 만큼 걸려 완료됩니다.
컴파일러 유도
소스와 대상의 텐서 매핑(In, Out)과 스트림 형태(Stream)가 주어지면, 컴파일러는 읽기 Sequencer와 쓰기 Sequencer를 다음과 같이 도출합니다:
- 읽기 Sequencer:
In을 스트림 형태에 사영하여 소스 계층에 대한 루프별 스트라이드를 생성합니다. - 쓰기 Sequencer:
Out을 스트림 형태에 사영하여 대상 계층에 대한 루프별 스트라이드를 생성합니다. - 통합 Sequencer: 두 Sequencer를 병합하여 각 항목이 읽기 스트라이드와 쓰기 스트라이드를 쌍으로 갖도록 합니다.
- 패킷 크기: 연속된 읽기/쓰기 양으로부터
stride0을 추론합니다. 읽기와 쓰기가 모두 연속된 256바이트에 접근하면 최적의stride0은 256입니다.
A=B=C=256인 m![A, B, C] → m![B, A, C]의 경우 스트림은 m![A, B, C]입니다. 컴파일러는 아래 관계식을 사용하여 각 선형 인덱스를 A, B, C에 매핑합니다. 그런 다음 쌍을 이루는 스트라이드를 유도합니다.
관계식은 m![A, B, C]::map(i) = i![A: i / 65,536, B: (i % 65,536) / 256, C: i % 256]입니다(표기법은 매핑 표현식을 참조하십시오).
read_sequencer = [
A -> 256 : 65,536,
B -> 256 : 256,
C -> 256 : 1,
] : 256, HBM @ 0
write_sequencer = [
A -> 256 : 256,
B -> 256 : 65,536,
C -> 256 : 1,
] : 256, HBM @ 256³
이들은 항목마다 쌍을 이루는 스트라이드를 갖는 하나의 DmaSequencer로 결합됩니다. 통합 DmaSequencer의 각 측(읽기 또는 쓰기)은 명확성을 위해 쌍 스트라이드 괄호를 생략하고 해당 방향의 단일 스트라이드 Sequencer로 표시할 수 있습니다.
집합 연산
집합 연산은 텐서 형태가 DMN 간에 균등하게 나누어지는지에 따라 두 가지 형식 중 하나를 취합니다.
형태가 균등하게 나누어지면, 참여하는 모든 엔진이 동종 집합 연산을 실행합니다. 모든 DMA Engine은 동일한 매개변수화된 스트림 환경(Stream = { chip, cluster, slice, time, packet })을 사용하며, 기준 주소만 서로 다릅니다.
형태가 균등하게 나누어지지 않으면, 컴파일러는 이종 집합 연산으로 대체합니다. 각 DMN은 StreamFn(chip, cluster, slice)를 통해 자체 스트림 환경을 받으며, 경계에 있는 DMN은 유효 영역을 넘어 쓰지 않도록 작업을 여러 DMA 명령으로 나눕니다. 입력 및 출력 매핑 환경(In과 Out)은 동종의 경우와 구조적으로 동일하게 유지되므로, 전체 논리적 텐서 이동은 잘 정의됩니다.
두 형식 모두에 다음 두 가지 정확성 불변 조건이 적용됩니다:
- 동일한 미디어 타입: 참여하는 모든 DMA Engine은 동일한 소스 미디어와 대상 미디어를 사용해야 합니다.
- 단일 통합 매핑: 하나의 입력 텐서 매핑과 하나의 출력 텐서 매핑이 전체 전송을 결정합니다.
명령마다 각자의 시작 지연 시간이 발생하므로, 전체 전송을 균일하게 유지하려면 DMN 간에 고르게 나누어지는 텐서 형태를 사용하는 것이 좋습니다.
제약 조건
DMA Engine은 하드웨어 수준의 정렬 규칙과 패킷 크기 규칙을 강제합니다. 이를 위반하면 단순한 성능 저하에 그치지 않고 정확성 오류나 하드웨어 예외가 발생합니다.
-
주소 정렬:
계층 읽기 쓰기 HBM 1바이트 1바이트 DM (SRAM) 1바이트 8바이트 HBM ↔ DM 전송에서는 위 표와 관계없이 읽기 주소, 쓰기 주소, 패킷 크기에 추가로 8바이트 정렬이 필요합니다. DM 규칙이 비대칭인 것은 SRAM 하드웨어가 비대칭이기 때문입니다. 읽기 포트는 바이트 선택 로직으로 임의의 바이트 범위를 추출하지만, 쓰기 포트는 8바이트 뱅크 폭 전체를 단위로 동작합니다. 따라서 정렬되지 않은 DM 쓰기는 Read-Modify-Write 연산을 유발하며, 이 연산은 쓰기 시간을 세 배로 늘리고 해당 뱅크의 다른 연산을 차단합니다.
컴파일러는 이러한 제약 조건을 하드웨어 불변 조건으로 강제합니다.
-
패킷 크기: 최대 패킷 크기는 4,096바이트이며, 이는 트랜잭션이 256 beat × 16바이트 데이터 폭을 초과할 수 없다는 AXI 프로토콜 제약 조건에 따라 정해집니다.
최적화
DMA 처리량을 결정하는 요인은 메모리 대역폭, 채널 및 DMN 인터리빙, 시작 지연 시간과 패킷 분할의 세 가지입니다.
메모리 대역폭
각 계층에는 달성 가능한 처리량의 상한이 되는 최대 대역폭이 있으며, 실제 전송 속도는 스트리밍 경로에서 가장 느린 구성 요소에 의해 제한됩니다.
| 계층 | 최대 대역폭 |
|---|---|
| HBM | Chip당 1.5 TB/s (32개 채널 × 0.75 GHz에서 채널당 48 GB/s) |
| DM | Cluster당 256 B/사이클 (DMN 인터리빙 시 DMN당 128 B/사이클) |
| PCIe | 읽기와 쓰기 모두 30 B/사이클 (PCIe DMA 참조) |
각 DMA Engine은 단독으로 사이클당 최대 256 B를 이동합니다. HBM 대역폭은 HBM 데이터를 전송하는 모든 엔진이 공유하므로, HBM을 포화시키는 총 전송량은 엔진별 합계가 아니라 1.5 TB/s의 HBM 최대 대역폭에 의해 제한됩니다.
같은 Cluster 내 DM 간 전송은 두 단계가 동일한 DM 뱅크 접근을 두고 경합하므로 읽기와 쓰기가 직렬화됩니다. HBM ↔ DM과 같은 계층 간 전송은 읽기 단계와 쓰기 단계를 파이프라인으로 처리합니다.
Note
DMA는 SRAM Slice 대역폭을 충분히 활용하지 못할 수 있으므로, SRAM 간 전송에서는 Tensor Unit(Fetch 및 Commit Engine 경유)이 DMA보다 효율적인 경우가 많습니다. 그러나 실제로는 대개 HBM 대역폭이 병목이 되므로, HBM ↔ DM 전송에서는 이 차이가 그다지 중요하지 않습니다.
채널 및 DMN 인터리빙
최대 대역폭을 유지하려면 하위 메모리 파티션 전반에 걸쳐 접근 패턴을 인터리빙해야 합니다.
HBM 채널 선택에는 주소 비트 9번부터 28번까지가 사용되며, 주소 비트 8은 스택 비트입니다. 요청을 32개 채널 전체에 분산하려면 접근 패턴이 이 비트들을 모두 토글해야 합니다. 스택 비트(주소 비트 8) 하나만 누락되어도 모든 요청이 32개 채널 중 16개로만 라우팅되어 유효 대역폭이 절반으로 줄어듭니다. 같은 HBM 뱅크에 반복적으로 접근하면 21번 이상의 행 주소 비트가 토글될 수 있습니다. 충돌 하나당 약 40사이클이 소요되며, 대역폭이 10분의 1 수준으로 떨어질 수 있습니다. FR-FCFS 메모리 스케줄링으로 처리량을 일부 회복할 수 있지만, 근본적인 비용은 여전히 큽니다.
DM 대역폭을 확보하려면 두 DMN(각각 128 B/사이클)을 번갈아 사용해야 하므로, 단일 DMN 접근 패턴은 DM 대역폭을 절반으로 줄입니다.
시작 지연 시간과 패킷 분할
각 DMA 명령은 데이터 전송이 시작되기 전에 약 500사이클의 고정 시작 지연 시간을 발생시킵니다. 여러 전송을 더 적은 수의 명령으로 결합하면 이 비용이 분산되지만, 이질적인 묶음은 DMN별 명령으로 분할되어 명령마다 지연 시간을 치르게 됩니다.
단일 명령 내에서 하드웨어는 각 패킷을 256바이트 단위로 분할하므로, n바이트 패킷은 ceil(n / 256)개의 AXI 요청이 됩니다. 따라서 4,095바이트 패킷에는 16개의 요청이 필요하며, 4,099바이트 패킷(4,096바이트 한도를 애매하게 넘어서는 소수 길이)은 여러 명령으로 분할해야 합니다. 최내곽 루프의 스트라이드(stride0)가 패킷 정렬을 결정합니다. stride0가 256바이트로 정렬되어 있으면 사이클 수는 ceil(stride0 / 256)입니다. stride0가 256바이트로 정렬되어 있지 않으면, HBM 쓰기는 256바이트 미만의 부분 블록에 대해 Read-Modify-Write 페널티를 추가로 치릅니다. HBM 읽기에는 ceil 오버헤드만 발생하며, DM 연산은 이러한 정렬 불일치의 영향을 거의 받지 않습니다.
DMA는 DM 뱅크 접근 우선순위가 가장 낮으므로, Fetch Engine이나 Commit Engine이 같은 뱅크에 64회 이상 연속으로 접근하면 DMA가 기아 상태에 빠져 NoC 타임아웃이 발생할 수 있습니다. 자세한 내용은 DM 뱅크 기아를 참조하십시오.
상세 예제
아래 예제는 대표적인 전송 패턴에 대한 구체적인 Sequencer 구성과 사이클 추정치를 제시합니다. 예제 1부터 3까지는 각 계층 쌍에 대해 잘 튜닝된 단일 엔진 사례를 다룹니다. 예제 4와 5는 10배 이상 손실이 발생하는 비정상적인 접근 패턴을 대조합니다. 예제 6은 형태가 DMN 간에 균등하게 나누어지지 않을 때의 이종 분할을 보여 줍니다.
Sequencer 구성에는 두 가지 주소 스트라이드 기호를 사용합니다:
slice_stride: Slice 내 DM 파티션 하나의 가상 주소 범위(4 MB)입니다.DMN_stride: 같은 Cluster 안에 있는 두 DMN 사이의 주소 범위입니다.
예제 1: HBM ↔ HBM 레이아웃 변환
인자:
axes![A = 8, B = 8, C = 256]dtype = i8- 소스: 오프셋
0의 HBM, 매핑m![A, B, C] - 대상: 오프셋
16,384의 HBM, 매핑m![B, A, C] - 스트림: 시간
m![A, B], 패킷m![C]
생성된 Sequencer:
read = [
A -> 8 : 2,048,
B -> 8 : 256,
C -> 256 : 1,
] : 256, HBM @ 0
write = [
A -> 8 : 256,
B -> 8 : 2,048,
C -> 256 : 1,
] : 256, HBM @ 16,384
최내곽이 아닌 스트라이드(256과 2,048)는 HBM 주소 비트 8(스택)과 11(채널)을 토글하여 모든 요청을 서로 다른 HBM 채널로 분산시키고 병렬로 실행되게 합니다. 256바이트 단일 전송은 0.75 GHz에서 Channel당 4사이클이 걸리지만, Channel 간 병렬 분산 덕분에 최대 대역폭에 가까운 대역폭이 유지됩니다. 총 시간: 1 GHz에서 약 64개의 읽기 요청 + 64개의 쓰기 요청에 시작 500사이클을 더해 ≈ 628사이클입니다.
4개의 DMA Engine이 HBM ↔ HBM 트래픽을 공유하면, 각 엔진은 0.75 TB/s의 읽기 대역폭 중 약 0.1875 TB/s를 할당받습니다. stride0 = 256이더라도 이 할당량으로는 어떤 엔진도 사이클당 요청 하나를 완료하지 못합니다.
예제 2: 전체 대역폭을 활용하는 HBM → DM
이 계층 간 전송은 HBM 채널과 두 DMN 모두에 걸쳐 인터리빙하여 읽기와 쓰기를 파이프라인으로 처리합니다.
인자:
axes![A = 256, B = 256, C = 256]dtype = i8- 소스: Chip 0의 HBM, 매핑
m![B, A, C] - 대상: Chip 0, Cluster 0, Slice 0의 DM입니다. Slice 매핑
m![A / 4], 요소 매핑m![A % 4, B, C] - 스트림: 시간
m![B, A % 4, A / 4 % 32, A / 128], 패킷m![C]
생성된 Sequencer:
read = [
B -> 256 : 65,536,
A%4 -> 4 : 256,
A/4%32 -> 32 : 1,024,
A/128 -> 2 : 32,768,
C -> 256 : 1,
] : 256, HBM @ 0
write = [
B -> 256 : 256,
A%4 -> 4 : 65,536,
A/4%32 -> 32 : slice_stride,
A/128 -> 2 : DMN_stride,
C -> 256 : 1,
] : 256, DM @ 0
HBM 쪽에서는 A/128=2에 적용된 32,768 스트라이드가 채널 간 접근을 인터리빙하며, 하드웨어 명령 큐가 65,536개 요청(256 × 4 × 32 × 2) 전체가 끊김 없이 흐르도록 유지합니다. DM 쪽에서는 slice_stride와 DMN_stride가 연속된 256바이트 쓰기를 두 DMN에 걸쳐 인터리빙하여 두 DMN 모두 사이클당 하나의 요청을 처리하도록 유지합니다. 읽기와 쓰기는 계층 간에 파이프라인으로 처리되므로, 총 시간은 대략 max(65,536 읽기 사이클, 65,536 쓰기 사이클) + 시작 500 사이클 ≈ 66,036 사이클입니다.
예제 3: 단일 Cluster 내 DM → DM
동일 Cluster 내 DM 간 전송에서는 읽기와 쓰기가 동일한 DM 뱅크 접근을 두고 경합하므로 서로 직렬화됩니다.
인자:
axes![A = 256, B = 256, C = 256]dtype = i8- 소스: Chip 0, Cluster 0, Slice 0의 DM, 요소 오프셋
0. Slice 매핑m![A / 4], 요소 매핑m![A % 4, B, C] - 목적지: Chip 0, Cluster 0, Slice 0의 DM, 요소 오프셋
4·256·256. Slice 매핑m![A / 4], 요소 매핑m![B, A % 4, C] - 스트림: 시간
m![B, A % 4, A / 4 % 32, A / 128], 패킷m![C]
생성된 Sequencer:
read = [
B -> 256 : 1,
A%4 -> 4 : 65,536,
A/4%32 -> 32 : slice_stride,
A/128 -> 2 : DMN_stride,
C -> 256 : 1,
] : 256, DM @ 0
write = [
B -> 256 : 1,024,
A%4 -> 4 : 256,
A/4%32 -> 32 : slice_stride,
A/128 -> 2 : DMN_stride,
C -> 256 : 1,
] : 256, DM @ (4·256·256)
DMN 및 Slice 인터리빙으로 각 단계는 256 B/사이클 전체를 사용하지만, 두 단계는 직렬화됩니다. 총 시간: 대략 131,072 사이클(읽기 65,536 + 쓰기 65,536) + 시작 500 사이클입니다.
Note
가능하면
C를 256의 배수로 선택하십시오.0 < r < 256인C = 256n + r의 경우, 총 데이터량은 약간만 변하더라도 각 접근이 더 많은 요청으로 분할되므로 사이클 수가n+1배로 증가합니다.
예제 4: HBM 뱅크 충돌 병리 사례
여기서 DM 인터리빙은 정상적이지만, 비정상적인 HBM 접근 패턴 때문에 잘 튜닝된 경우의 사이클 수보다 약 10배의 비용이 듭니다.
인자:
- Chip 1개(DMN 8개)
axes![A = 64, B = 2,048, C = 1,024]dtype = i8- 소스: HBM, Cluster 매핑
m![B / 1024], Slice 매핑m![B / 256 % 4, A], 요소 매핑m![B % 256, C] - 목적지: DM, Slice 매핑
m![A / 4], 요소 매핑m![B, A % 4, C] - 스트림: Cluster
m![B / 1024], Slicem![B / 256 % 4], 시간m![B % 256, C / 256, A % 32, A / 32], 패킷m![C % 256]
(cluster_i, dmn_j)별로 생성된 Sequencer:
read = [
B%256 -> 256 : 1,024,
C/256 -> 4 : 256,
A%32 -> 32 : 2²¹,
A/32 -> 2 : 2²⁶,
C%256 -> 256 : 1,
] : 256, HBM @ (i·2²⁰ + j·2¹⁸)
write = [
B%256 -> 256 : 1,024,
C/256 -> 4 : 256,
A%32 -> 32 : slice_stride,
A/32 -> 2 : DMN_stride,
C%256 -> 256 : 1,
] : 256, DM @ (cluster_i, dmn_j, 0)
A%32와 A/32의 스트라이드는 HBM 뱅크 내 행을 선택하는 HBM 주소 비트 21과 26을 토글합니다. 따라서 각 채널 내의 연속 접근은 거의 모든 요청마다 한 행을 닫고 다음 행을 열게 되어, 접근당 약 40 사이클을 소모합니다. C / 256 = 4(스트라이드 256)를 통한 Channel 인터리빙은 요청을 32개 Channel 전체에 분산시키기는 하지만, 각 Channel 내의 행 충돌 비용을 숨기지는 못합니다.
성능 분석:
- HBM 읽기: 32개 채널에 걸쳐 총 524,288개 요청 = 채널당 16,384개 × 약 40 사이클 ≈ 655,360 사이클입니다.
- DM 쓰기: DMN당 65,536개 요청을 사이클당 하나씩 처리하며, 이 시간은 읽기 지연 시간에 가려집니다.
총 소요 시간: 약 655,360 사이클 + 시작 500 사이클 ≈ 655,860 사이클입니다. FR-FCFS 스케줄링이 처리량을 일부 회복하지만, 자릿수 단위의 성능 저하는 여전히 남습니다.
예제 5: 스택 비트 누락 병리 현상
이 접근 패턴은 HBM의 스택 차원(주소 비트 8)에 걸쳐 인터리빙하지 못하므로, 모든 트래픽이 절반의 채널로만 라우팅되어 유효 대역폭이 절반으로 줄어듭니다.
인자:
- Chip 1개(DMN 8개)
axes![A = 8, B = 64, C = 8, D = 512]dtype = i8- 소스: HBM, 매핑
m![A, B, C, D] - 대상: DM, Cluster 매핑
m![A / 4], Slice 매핑m![A % 4, B], 요소 매핑m![C, D % 256] - 스트림: Cluster
m![A / 4], Slicem![A % 4], 시간m![C, B % 32, B / 32], 패킷m![D % 256]
(cluster_i, dmn_j)별로 생성된 Sequencer:
read = [
C -> 8 : 512,
B%32 -> 32 : 4,096,
B/32 -> 2 : 131,072,
D%256 -> 256 : 1,
] : 256, HBM @ (i·2²⁰ + j·2¹⁸)
write = [
C -> 8 : 256,
B%32 -> 32 : slice_stride,
B/32 -> 2 : DMN_stride,
D%256 -> 256 : 1,
] : 256, DM @ (cluster_i, dmn_j, 0)
C의 스트라이드 512는 HBM 주소 비트 8(스택 비트)을 전혀 토글하지 않으므로, 8개의 DMN이 32개 HBM 채널 중 16개에만 집중됩니다.
성능 분석:
- HBM 읽기(병목): 16개 채널에 걸쳐 총 4,096개 요청 = 채널당 256개 × 요청당 약 5.3 사이클(1 GHz 기준) ≈ 1,357 사이클입니다.
- DM 쓰기: DMN당 512개 요청이며, 읽기 뒤에서 파이프라인으로 처리됩니다.
총 소요 시간: 약 1,357 사이클 + 시작 500 사이클 ≈ 1,857 사이클입니다. 32개 채널 전체에 걸쳐 스택 비트 인터리빙을 복원하면 HBM 사이클 수가 절반으로 줄어듭니다.
예제 6: 이질적인 DMN 분할
텐서 형태가 DMN들에 균등하게 나누어떨어지지 않으면, 컴파일러는 경계 DMN의 작업을 여러 명령으로 분할하며, 각 명령은 저마다 시작 지연 시간을 치릅니다.
인자:
- Chip 4개
axes![A = 15, B = 32, C = 256, D = 8]dtype = i8- 소스: DM, (
A' = A + 1#로 표기할 때) Chip 매핑m![D / 2], Cluster 매핑m![D % 2], Slice 매핑m![A' / 4, A' / 2 % 2, B], 요소 매핑m![A' % 2, C] - 대상: HBM, Chip 매핑
m![D / 2], 요소 매핑m![D % 2, B, A, C] - 스트림(DMN별,
StreamFn(chip_i, cluster_j, slice_k)로 표현):
StreamFn(chip_i, cluster_j, slice_k) = let A' = A + 1# in
{ chip: m![(D / 2) @ i = 1], cluster: m![(D % 2) @ j = 1],
slice: m![(A' / 4) @ k = 1],
time: (k == 0,1,2): m![A' % 2, B, A' / 2 % 2, C]
(k == 3, exec #0): m![A' % 2, B, A' / 2 = 1, C]
(k == 3, exec #1): m![A' = 1, B, A' / 2 % 2 @ 1, C],
packet: m![C] }
차원 A = 15는 4개의 DMN에 나누어떨어지지 않으므로(15 = 3·4 + 3), DMN 0부터 2까지는 각각 4개 요소를 처리하고 DMN 3은 3개만 처리합니다. DMN 3에서 단일 디스크립터를 사용하면 유효 영역을 넘어 네 번째 요소를 쓰게 되므로, 컴파일러는 DMN 3의 작업을 합쳐서 정확히 3개 요소를 다루는 두 개의 명령으로 분할합니다.
성능 분석:
- DMN 0~2(각각 명령 1개): 약 256사이클 + 시작 500사이클 ≈ 756사이클입니다.
- DMN 3(명령 2개): 데이터 약 192사이클 + 시작 1,000사이클(각 500) ≈ 1,192사이클입니다.
총 소요 시간은 약 1,192사이클이며, DMN 3이 이를 결정합니다. 이러한 분할 비용을 피하려면 DMN 전체에 고르게 나누어지는 텐서 형태를 선택하십시오.
재분배 연산
SRAM 재분배는 DmTensor 또는 DmTensorView로부터 재분배 계획을 구성하고, 이를 to_dm 또는 to_dm_view로 실행합니다. 종결 메서드 이전의 모든 단계는 하나의 DMA 연산으로 융합됩니다. 이 연산들은 Chip 간 또는 Cluster 간에 부분 값을 라우팅하고, 각 대상에 필요한 샤드를 선택합니다. 이 연산들을 사용해 부분 값을 라우팅하고 선택하는 전체 리덕션 예제는 Chip 및 Cluster 리덕션을 참조하십시오.
인터페이스 요약
소스를 제공하는 차원 또는 선택된 Element 좌표를 제공하는 차원에 따라 연산을 선택하십시오.
| API | 의미 | 형태에 미치는 영향 | 실행 |
|---|---|---|---|
source.chip_shuffle(sources) | 각 대상 Chip마다 소스 Chip 하나를 선택합니다 | 없음 | 지연된 텐서 DMA |
source.cluster_swap() | 각 Chip 안에서 소스 Cluster 0과 1을 서로 교환합니다 | 없음 | 지연된 텐서 DMA |
source.chip_slice::<Axis, Output>(indices) | 각 대상 Chip마다 Axis 좌표 하나를 선택합니다 | Element를 Output으로 변경합니다 | 지연된 텐서 DMA |
source.cluster_slice::<Axis, Output>(indices) | 각 대상 Cluster마다 Axis 좌표 하나를 선택합니다 | Element를 Output으로 변경합니다 | 지연된 텐서 DMA |
tensor.asymmetric_chip_slice::<Axis, Output>(...) | 각 Chip마다 로컬 Axis 좌표 하나를 선택합니다 | Element에서 Axis를 제거합니다 | 즉시 서브 컨텍스트 병렬 복사 |
tensor.asymmetric_cluster_slice::<Axis, Output>(...) | 각 Cluster마다 로컬 Axis 좌표 하나를 선택합니다 | Element에서 Axis를 제거합니다 | 즉시 서브 컨텍스트 병렬 복사 |
source.hbm_chip_shuffle(dma, sources) | 각 대상 HBM Chip마다 소스 Chip 하나를 선택합니다 | 없음 | 즉시 HBM DMA |
이 표에서 source는 텐서 또는 뷰일 수 있습니다. SRAM 뷰는 Element 타일을 포함할 수 있지만, Chip 타일과 Cluster 타일은 재분배 입력으로 지원되지 않습니다. 네 가지 SRAM 메서드는 지연된 재분배 계획을 반환하므로, 하나의 종단 호출 전에 체이닝할 수 있습니다. to_dm은 그 계획을 새 대상 텐서로 구체화하고, to_dm_view는 같은 논리적 결과를 타일과 같은 기존 대상 뷰에 기록합니다. 축 순열은 대상 매핑에서 결정되며, 어떤 종단 호출을 사용하는지와는 무관합니다. 결과 매핑은 대상 레이아웃을 기술하지만, 각 값을 어떤 소스 배치나 Slice 좌표가 제공했는지는 기록하지 않습니다. 이러한 값 의미론을 파악하려면 재분배 단계와 그 배열을 읽으십시오. 컴파일러는 완성된 계획으로부터 동기화를 도출합니다. 소스 Chip이 바뀌면 ChipSync를 삽입하고, 소스 Cluster가 바뀌면 ClusterSync를 삽입하며, 둘 다 포함된 계획에는 둘 다 삽입합니다. 로컬 데이터를 슬라이스하기만 하는 계획에는 어떤 동기화도 삽입하지 않습니다.
Tensor DMA 의미론
아래 예시는 Chip 4개, Cluster 2개, Slice 256개, 그리고 로컬 [B, C, D] 요소 형태를 사용합니다:
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
}
표에서 in[p, k, b, c, d]는 소스 Chip p, 소스 Cluster k의 로컬 Element 좌표 [b, c, d]에 있는 입력값을 나타냅니다. 결과의 이름은 out이며, 모든 행은 동일한 활성 Slice를 가리킵니다.
Chip 셔플
chip_shuffle(sources)는 각 대상 Chip에 sources의 같은 위치에 있는 소스 Chip의 값을 전달합니다. 아래 패턴은 대상 Chip 0, 1, 2, 3이 각각 소스 Chip 1, 2, 3, 0을 읽는다는 의미입니다. 이 연산은 텐서 형태를 바꾸지 않고, 각 값을 어느 Chip이 공급하는지만 바꿉니다.
두 대상 Cluster k 중 어느 쪽이든, 그리고 모든 로컬 좌표 [b, c, d]에 대해 다음과 같습니다:
| 대상 Chip | 대상 Cluster | 출력 요소 | 소스 Chip | 소스 Cluster | 소스 요소 | 출력 값 |
|---|---|---|---|---|---|---|
| 0 | k | [b, c, d] | 1 | k | [b, c, d] | out[0, k, b, c, d] = in[1, k, b, c, d] |
| 1 | k | [b, c, d] | 2 | k | [b, c, d] | out[1, k, b, c, d] = in[2, k, b, c, d] |
| 2 | k | [b, c, d] | 3 | k | [b, c, d] | out[2, k, b, c, d] = in[3, k, b, c, d] |
| 3 | k | [b, c, d] | 0 | k | [b, c, d] | out[3, k, b, c, d] = in[0, k, b, c, d] |
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
fn chip_shuffle(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]> {
dm.chip_shuffle([1, 2, 3, 0]).to_dm(&mut device.tdma)
}
}
HBM Chip 셔플
HbmTensor::hbm_chip_shuffle와 HbmTensorView::hbm_chip_shuffle는 각 대상 Chip 슬롯을 sources가 선택한 소스 Chip에서 복사하고, 동일한 Chip 및 Element 매핑을 갖는 새 HbmTensor를 반환합니다. 소스 목록은 sources[target_chip] = source_chip 규칙을 따르며, Chip의 모든 위치에 대한 순열이어야 합니다. Chip 간 Chip 셔플은 시스템 전체에서 통용되는 전역 Chip ID를 사용합니다.
모든 요소 좌표 e에 대해 패턴 [1, 2, 3, 0]의 값 의미는 다음과 같습니다. 여기서 in[p, e]는 소스 Chip p의 입력이고, out[t, e]는 대상 Chip t의 결과입니다.
| 대상 Chip | 소스 Chip | 출력 값 |
|---|---|---|
| 0 | 1 | out[0, e] = in[1, e] |
| 1 | 2 | out[1, e] = in[2, e] |
| 2 | 3 | out[2, e] = in[3, e] |
| 3 | 0 | out[3, e] = in[0, e] |
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![HbmChip = 4, HbmElement = 256];
fn hbm_chip_shuffle(
device: &mut Device,
hbm: &HbmTensor<i32, m![HbmChip], m![HbmElement]>,
) -> HbmTensor<i32, m![HbmChip], m![HbmElement]> {
hbm.hbm_chip_shuffle(&mut device.tdma, &[1, 2, 3, 0])
}
}
두 엔진 모두 HBM 간 전송을 지원하므로 DMA 인수로는 tdma와 pdma 중 어느 것이든 사용할 수 있습니다.
Cluster 교환
cluster_swap()은 모든 Chip 안에서 소스 Cluster 0과 1을 서로 교환합니다. 텐서 형태는 바꾸지 않고, 각 값을 어느 Cluster에서 가져올지만 바꿉니다.
모든 대상 Chip p와 로컬 좌표 [b, c, d]에 대해 다음과 같습니다.
| 대상 Chip | 대상 Cluster | 출력 요소 | 소스 Chip | 소스 Cluster | 소스 요소 | 출력 값 |
|---|---|---|---|---|---|---|
p | 0 | [b, c, d] | p | 1 | [b, c, d] | out[p, 0, b, c, d] = in[p, 1, b, c, d] |
p | 1 | [b, c, d] | p | 0 | [b, c, d] | out[p, 1, b, c, d] = in[p, 0, b, c, d] |
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
fn cluster_swap(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]> {
dm.cluster_swap().to_dm(&mut device.tdma)
}
}
Chip Slice
chip_slice::<Axis, Output>(indices)는 각 대상 Chip마다 살아 있는 Axis 위치를 하나씩 선택하고, 나머지 Element 매핑을 Output으로 선언합니다. [0, 1, 2, 3]을 사용하면 대상 Chip t는 B의 위치 t를 유지하며, 결과에서는 Element에서 B가 제거됩니다.
두 대상 Cluster k 각각과 모든 결과 좌표 [c, d]에 대해 다음이 성립합니다.
| 대상 Chip | 대상 Cluster | 출력 요소 | 소스 Chip | 소스 Cluster | 소스 요소 | 출력 값 |
|---|---|---|---|---|---|---|
| 0 | k | [c, d] | 0 | k | [0, c, d] | out[0, k, c, d] = in[0, k, 0, c, d] |
| 1 | k | [c, d] | 1 | k | [1, c, d] | out[1, k, c, d] = in[1, k, 1, c, d] |
| 2 | k | [c, d] | 2 | k | [2, c, d] | out[2, k, c, d] = in[2, k, 2, c, d] |
| 3 | k | [c, d] | 3 | k | [3, c, d] | out[3, k, c, d] = in[3, k, 3, c, d] |
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
fn chip_slice(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![C, D]> {
dm.chip_slice::<m![B], m![C, D]>([0, 1, 2, 3])
.to_dm(&mut device.tdma)
}
}
Cluster Slice
cluster_slice::<Axis, Output>(indices)는 각 대상 Cluster마다 활성 Axis 위치 하나를 선택하고, 나머지 Element 매핑을 Output으로 선언합니다. [1, 0]을 사용하면 대상 Cluster 0과 1은 각각 B의 위치 1과 0을 유지하며, 결과에서는 Element에서 B가 제거됩니다.
모든 대상 Chip p와 결과 좌표 [c, d]에 대해 다음이 성립합니다:
| 대상 Chip | 대상 Cluster | 출력 요소 | 소스 Chip | 소스 Cluster | 소스 요소 | 출력 값 |
|---|---|---|---|---|---|---|
p | 0 | [c, d] | p | 0 | [1, c, d] | out[p, 0, c, d] = in[p, 0, 1, c, d] |
p | 1 | [c, d] | p | 1 | [0, c, d] | out[p, 1, c, d] = in[p, 1, 0, c, d] |
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
fn cluster_slice(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![C, D]> {
dm.cluster_slice::<m![B], m![C, D]>([1, 0])
.to_dm(&mut device.tdma)
}
}
단계 결합
재분배 단계는 물리적 순서를 따릅니다. 선택적인 Chip 셔플, 선택적인 Cluster 스왑, 0개 이상의 Chip 슬라이스, 0개 이상의 Cluster 슬라이스 순입니다. 두 종결 메서드 모두 완성된 계획을 하나의 DMA 연산으로 결합합니다. to_dm은 새 대상 텐서를 생성하고, to_dm_view는 주어진 대상 뷰에 기록합니다. Slice 메서드를 두 번 이상 호출하면 같은 DMA 안에서 여러 축이 제거됩니다.
아래의 융합 예제는 남은 모든 d에 대해 다음과 같은 배치와 요소 좌표 매핑을 만들어 냅니다.
| 대상 Chip | 대상 Cluster | 출력 요소 | 소스 Chip | 소스 Cluster | 소스 요소 | 출력 값 |
|---|---|---|---|---|---|---|
| 0 | 0 | [d] | 1 | 1 | [0, 3, d] | out[0, 0, d] = in[1, 1, 0, 3, d] |
| 0 | 1 | [d] | 1 | 0 | [0, 7, d] | out[0, 1, d] = in[1, 0, 0, 7, d] |
| 1 | 0 | [d] | 2 | 1 | [1, 3, d] | out[1, 0, d] = in[2, 1, 1, 3, d] |
| 1 | 1 | [d] | 2 | 0 | [1, 7, d] | out[1, 1, d] = in[2, 0, 1, 7, d] |
| 2 | 0 | [d] | 3 | 1 | [2, 3, d] | out[2, 0, d] = in[3, 1, 2, 3, d] |
| 2 | 1 | [d] | 3 | 0 | [2, 7, d] | out[2, 1, d] = in[3, 0, 2, 7, d] |
| 3 | 0 | [d] | 0 | 1 | [3, 3, d] | out[3, 0, d] = in[0, 1, 3, 3, d] |
| 3 | 1 | [d] | 0 | 0 | [3, 7, d] | out[3, 1, d] = in[0, 0, 3, 7, d] |
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
fn fused_redistribution(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![D]> {
dm.view()
.chip_shuffle([1, 2, 3, 0])
.cluster_swap()
.chip_slice::<m![B], m![C, D]>([0, 1, 2, 3])
.cluster_slice::<m![C], m![D]>([3, 7])
.to_dm(&mut device.tdma)
}
}
서브 컨텍스트 Chip 및 Cluster Slice
asymmetric_chip_slice와 asymmetric_cluster_slice는 서브 컨텍스트를 통해 DmTensor에서 직접 동작합니다:
#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![ChipAxis = 4, ClusterAxis = 2, B = 4, C = 8, D = 8];
fn sub_context_chip_slice(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![C, D]> {
dm.asymmetric_chip_slice::<m![B], m![C, D]>(&mut device.sub, &[3, 0, 1, 2])
}
fn sub_context_cluster_slice(
device: &mut Device,
dm: &DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![B, C, D]>,
) -> DmTensor<i32, m![ChipAxis], m![ClusterAxis], m![1 # 256], m![C, D]> {
dm.asymmetric_cluster_slice::<m![B], m![C, D]>(&mut device.sub, &[1, 0])
}
}
각 연산은 Chip 또는 Cluster 차원을 따라 데이터를 이동하지 않고 병렬 복사를 통해 하나의 축을 선택합니다.
재분배 제약 조건
cluster_swap()에는 정확히 두 개의 Cluster가 필요합니다. 소스 Chip 목록은 순열이어야 하며, 모든 활성 대상은 활성 소스 Chip 또는 Cluster 인덱스를 읽어야 합니다. Slice 배열에는 대상 Chip 또는 Cluster마다 활성 인덱스가 하나씩 있어야 합니다. 융합된 계획은 사용 가능한 동기화 프로토콜에 맞게 목적지 Chip마다 소스 Chip 하나를, 목적지 Cluster마다 소스 Cluster 하나를 선택해야 합니다. chip_slice::<m![B, C]>는 m![B, C]를 선형 인덱스가 하나인 연속된 매핑 하나로 취급합니다. 서로 떨어져 위치한 축을 Slice하려면 축마다 메서드 호출을 하나씩 체이닝하십시오.
성능
SRAM 재분배는 DMA 정렬 및 패킷 제약 조건을 따릅니다. 경험적으로, 컴파일러가 효율적인 패킷을 구성할 수 있도록 DM 쓰기가 정렬되고 연속 구간이 긴 Element 매핑을 선택하십시오. 로컬 축 하나에는 서브 컨텍스트 Slice를 사용하고, Slice를 셔플과 융합해야 하거나 여러 축을 한 번의 DMA로 Slice해야 할 때는 DmTensorView 체인을 사용하십시오.
Scatter와 Gather
Scatter와 gather는 고정된 스트라이드가 아니라 인덱스 텐서로부터 계산된 주소에서 텐서 요소를 이동합니다.
DmTensor::dma_scatter는 인덱스 텐서가 선택한 HBM 행에 DM 값을 씁니다. HbmTensor::dma_gather_scaled와 HbmTensor::dma_gather_unscaled는 HBM 행을 읽어 인덱스 텐서가 선택한 DM 행에 넣습니다. 두 gather 변형은 인덱스가 있는 위치와 그 값을 읽는 방식만 다르며, 이는 아래에서 설명합니다.
extern crate furiosa_opt_std;
extern crate tokio;
use furiosa_opt_std::prelude::*;
axes![K = 512, D = 128, C = 612, G = 512, CL = 2];
fn scatter_minimal(
device: &mut Device,
data: &HbmTensor<bf16, m![1], m![K, D]>,
index: &HbmTensor<i32, m![1], m![K]>,
output: &mut HbmTensor<bf16, m![1], m![C, D]>,
) {
let data_dm: DmTensor<bf16, m![1], m![1 # 2], m![K / 2], m![K % 2, D]> =
data.to_dm(&mut device.tdma);
data_dm.dma_scatter::<m![K], _, _>(index, output);
}
fn gather_minimal(
table: &HbmTensor<bf16, m![1], m![K, D]>,
index: &HbmTensor<i32, m![1], m![G]>,
// The gather axis itself partitions into Slice x Element (`G / 2 = 256`, a valid slice count)
// with `D` folded into the Element side alongside the `G % 2` remainder; `C = 612` (used above
// for the scatter cache) has no divisor landing on a valid 64 | 128 | 256 slice count, so the
// gather count here is the separate, slice-friendly `G` instead.
) -> DmTensor<bf16, m![1], m![1 # 2], m![G / 2], m![G % 2, D]> {
table.dma_gather_scaled(index)
}
fn gather_unscaled(
device: &mut Device,
table: &HbmTensor<bf16, m![1], m![K, D]>,
// Raw row positions per cluster. The kernel stages the index on-chip with `to_dm`;
// a real per-cluster (`CL`) partition avoids broadcast padding.
index: &HbmTensor<i32, m![1], m![CL, G]>,
) -> DmTensor<bf16, m![1], m![CL], m![G / 2], m![G % 2, D]> {
let index_dm: DmTensor<i32, m![1], m![CL], m![G / 2], m![G % 2]> =
index.to_dm(&mut device.tdma);
table.dma_gather_unscaled(&index_dm)
}
#[tokio::main]
async fn main() {
let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let index = &(HostTensor::<i32, m![K]>::zero().to_hbm(&mut device.pdma).await.unwrap());
let data = HbmTensor::<bf16, m![1], m![K, D]>::new();
let mut output_hbm = HbmTensor::<bf16, m![1], m![C, D]>::new();
scatter_minimal(&mut device, &data, &index, &mut output_hbm);
gather_minimal(&data, &(HostTensor::<i32, m![G]>::zero().to_hbm(&mut device.pdma).await.unwrap()));
let placed_index = &(HostTensor::<i32, m![CL, G]>::zero().to_hbm(&mut device.pdma).await.unwrap());
gather_unscaled(&mut device, &data, placed_index);
}
scaled 변형(dma_gather_scaled, dma_scatter)은 DRAM에 있는 HbmTensor에서 인덱스를 가져오고, 그 값을 gather/scatter 축을 따른 바이트 오프셋으로 읽습니다. r번째 행에 접근하려면 r에 한 행의 바이트 크기(해당 행의 요소 수와 요소의 바이트 크기를 곱한 값으로, 예를 들어 128 너비의 bf16 행이라면 128 * 2 = 256입니다)를 곱한 값을 전달하십시오. 반면 dma_gather_unscaled는 to_dm으로 DRAM에서 가져온 Chip 내장 DmTensor 인덱스를 받아 그 값을 원시 행 위치로 읽습니다. 이 방식은 paged-어텐션 블록 테이블처럼 Chip 내에서 계산된 인덱스에 사용합니다.
PCIe DMA
PCIe DMA(Device::pdma)는 호스트 시스템 메모리와 디바이스 HBM 사이에서 텐서를 이동합니다. 이는 Chip 내부의 Tensor DMA와는 물리적으로 별개인 엔진입니다. PCIe DMA는 호스트 ↔ HBM 전송만 담당하고, Tensor DMA는 Chip 내부의 모든 전송을 담당합니다.
커널 작성자는 HostTensor에 .to_hbm()을 호출하거나(호스트 → 디바이스) HbmTensor에 .to_host()를 호출합니다(디바이스 → 호스트). 두 연산 모두 비동기 연산입니다.
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
use rand::{rngs::SmallRng, SeedableRng};
axes![A = 8, B = 512];
async fn upload_and_download(device: &mut Device) -> Result<(), Error> {
let mut rng = SmallRng::seed_from_u64(0);
let host: HostTensor<i8, m![A, B]> = HostTensor::rand(&mut rng);
// Host → HBM (allocator-assigned address)
let hbm: HbmTensor<i8, m![A], m![B]> = host.to_hbm(&mut device.pdma).await?;
// HBM → host (back to system memory)
let _round_tripped: HostTensor<i8, m![A, B]> = hbm.to_host(&mut device.pdma).await?;
Ok(())
}
HostTensor는 Element 매핑만 가지는 반면(호스트 메모리에는 chip/cluster/slice 분할이 없습니다), 대상 HbmTensor는 여러 Chip에 분산하기 위해 Chip 축을 추가합니다. to_hbm은 새로운 Chip 및 Element 타입 매개변수를 받으므로, HBM의 대상 요소 레이아웃은 호스트 레이아웃과 다를 수 있습니다.
PCIe DMA 대역폭은 30 B/사이클로, Chip 내장 Tensor DMA(256 B/사이클)보다 한 자릿수 느립니다. 알고리즘은 호스트 ↔ 디바이스 간 트래픽을 최소화해야 합니다. 데이터는 한 번만 업로드하고, 이를 Chip 내부의 여러 연산에서 재사용하십시오.