Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fetch Engine

Fetch Engine은 DM 텐서를 읽어 Tensor Unit용 패킷 스트림을 생성하며, OutTime과 OutPacket이 이후 단계에서 소비할 스트림을 결정합니다.

DM의 데이터가 특정한 시간 순서와 패킷 순서로 Tensor Unit 스트림에 들어가야 할 때 Fetch를 선택하십시오.

인터페이스

BeginTensor는 Tensor Unit 파이프라인의 입구에서 DM에 상주하는 텐서를 나타냅니다. 이 텐서의 Time은 m![1]이고(파이프라인이 시작되기 전에는 시간 반복이 없음), Packet은 DM 내의 요소 레이아웃입니다.

BeginTensor::fetch()는 Sequencer를 실행하여 FetchTensor 패킷 스트림을 생성합니다. 이 스트림은 Fetch Adapter, Switch Engine, Collect Engine에 공급될 수 있습니다. assert_eq! 호출은 Cluster::SIZE, Slice::SIZE, 패킷 정렬에 대한 하드웨어 제약 조건을 강제합니다(제약 조건 참조).

impl<'l, const T: Tu, P: CanApplyFetch, D: Scalar, Chip: M, Cluster: M, Slice: M, Time: M, Packet: M, B: Backend>
    TuTensor<'l, T, P, D, Chip, Cluster, Slice, Time, Packet, B>
{
    /// Runs the Fetch Sequencer.
    #[primitive(TuTensor::fetch)]
    pub fn fetch<OutTime: M, OutPacket: M>(self) -> FetchTensor<'l, T, D, Chip, Cluster, Slice, OutTime, OutPacket, B> {
        verify_fetch::<Cluster, Slice, Time, Packet, OutTime, OutPacket>();
        FetchTensor::new(self.device, self.inner.transpose(true))
    }
}

텐서 매핑에서 소개했듯이, Chip, Cluster, Slice, Time, Packet 매핑은 데이터를 공간과 시간에 걸쳐 분배합니다. 각 Slice가 자신의 DM 파티션을 독립적으로 읽기 때문에 .fetch()는 Chip, Cluster, Slice 차원을 입력에서 변경하지 않고 그대로 유지합니다. 이후 Switch Engine이 Slice 간에 데이터를 이동시켜 Slice 매핑을 변경합니다. 축 리프팅은 선택한 차원에 걸쳐 서로 다른 DM 읽기 오프셋을 사용하여, 데이터를 전송하지 않고 Chip, Cluster 또는 Slice를 변경합니다.

fetch()는 Fetch Sequencer를 구성하는 OutTime 및 OutPacket 타입 매개변수를 받습니다. OutTime은 출력 스트림의 시간 단계 수를 설정하고, OutPacket은 각 패킷 내의 요소 레이아웃을 설정합니다. OutPacket 선택이 성능에 미치는 영향은 최적화를 참조하십시오.

다음 예제는 DM에서 i8 행렬을 i8 패킷 스트림으로 가져옵니다. 출력 FetchTensor는 512개의 시간 단계를 스트리밍하며, 각 시간 단계는 32개 요소로 이루어진 i8 패킷(32바이트)입니다. 여기서 OutTime = m![A]이고 OutPacket = m![B]입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![CH = 4, CL = 2, S = 256, A = 512, B = 32];

fn fetch_matrix_example<'l, const T: Tu>(
    input: BeginTensor<'l, T, i8, m![CH], m![CL], m![S], m![1], m![A, B]>,
) -> FetchTensor<'l, T, i8, m![CH], m![CL], m![S], m![A], m![B]> {
    input.fetch::<m![A], m![B]>()
}
}

Chip, Cluster, Slice는 하드웨어 공간 병렬성 차원입니다. Fetch Sequencer는 모든 Slice에서 독립적으로 실행되며, 각각 자신의 로컬 DM 파티션에서 동작합니다. 이 예제에서 CH = 4, CL = 2, S = 256은 Chip당 Cluster가 2개이고 Cluster당 Slice가 256개인 4-Chip 시스템을 나타냅니다. 각 Slice는 자신의 A×B 하위 텐서에 대해 동일한 Sequencer를 실행합니다.

축 리프팅

fetch_chip_lift, fetch_cluster_lift, fetch_slice_lift는 축을 Time에서 Chip, Cluster 또는 Slice로 옮깁니다. 일반 fetch는 모든 Chip, Cluster, Slice를 동일한 DM 오프셋에서 시작하고 Time의 축을 순회합니다. 리프트는 각 Chip, Cluster 또는 Slice에 서로 다른 읽기 베이스를 부여하므로, 리프트된 축은 병렬로 읽히고 Time에서 제거됩니다.

읽기 베이스는 시작 오프셋만 변경하므로, 리프트를 하려면 소스 영역이 선택한 차원에 걸쳐 복제되어 있어야 합니다. DM 배치는 이 복제를 브로드캐스트로 표현하며, 리프트는 이 브로드캐스트를 리프트된 축으로 대체합니다.

Slice의 경우, 데이터가 Slice 간에 이동하거나 Slice에서 Time으로 이동해야 할 때는 InterTranspose를 사용하십시오. Chip과 Cluster의 경우, 소스 영역이 복제되어 있지 않을 때는 DMA가 대안입니다.

단계차원베이스의 출처
fetch_chip_liftChip읽기 명령이 정적 base를 인코딩하므로 리프트는 설정 연산을 추가하지 않습니다
fetch_cluster_liftCluster읽기 명령이 정적 base를 인코딩하므로 리프트는 설정 연산을 추가하지 않습니다
fetch_slice_liftSlice컴파일러는 DMA, fetch 실행 컨텍스트에서의 덧셈, SFR 저장을 통해 Slice마다 런타임 base를 하나씩 구체화합니다

Slice base는 소스 텐서의 런타임 주소에 의존하므로 정적 명령 필드로 인코딩할 수 없습니다. 컴파일러는 상수 상대 오프셋을 DRAM에서 SRAM으로 복사하고, fetch 실행 컨텍스트에서 소스 주소를 더한 뒤, 그 결과를 Slice별 base SFR에 저장합니다. fetch는 이 설정이 끝날 때까지 대기하며, 이 설정은 Switch Engine의 사용자 정의 비트맵과 같은 SFR 저장 메커니즘을 사용합니다.

서브 컨텍스트 fetch의 경우 설정 저장이 fetch 자체에 필요한 SFR을 덮어씁니다. 컴파일러는 base를 기록한 뒤 해당 파라미터를 복원하며, 두 컨텍스트 모두 모든 리프트 변형을 지원합니다.

impl<
    'l,
    const T: Tu,
    P: CanApplyFetchChipLift,
    D: Scalar,
    Chip: M,
    Cluster: M,
    Slice: M,
    Time: M,
    Packet: M,
    B: Backend,
> TuTensor<'l, T, P, D, Chip, Cluster, Slice, Time, Packet, B>
{
    /// Gives each chip its own fetch base, lifting a DM axis onto `Chip`.
    #[primitive(TuTensor::fetch_chip_lift)]
    pub fn fetch_chip_lift<OutChip: M, OutTime: M>(
        self,
    ) -> FetchChipLiftTensor<'l, T, D, OutChip, Cluster, Slice, OutTime, Packet, B> {

impl<
    'l,
    const T: Tu,
    P: CanApplyFetchClusterLift,
    D: Scalar,
    Chip: M,
    Cluster: M,
    Slice: M,
    Time: M,
    Packet: M,
    B: Backend,
> TuTensor<'l, T, P, D, Chip, Cluster, Slice, Time, Packet, B>
{
    /// Gives each cluster its own fetch base, lifting a DM axis onto `Cluster`.
    #[primitive(TuTensor::fetch_cluster_lift)]
    pub fn fetch_cluster_lift<OutCluster: M, OutTime: M>(
        self,
    ) -> FetchClusterLiftTensor<'l, T, D, Chip, OutCluster, Slice, OutTime, Packet, B> {

impl<
    'l,
    const T: Tu,
    P: CanApplyFetchSliceLift,
    D: Scalar,
    Chip: M,
    Cluster: M,
    Slice: M,
    Time: M,
    Packet: M,
    B: Backend,
> TuTensor<'l, T, P, D, Chip, Cluster, Slice, Time, Packet, B>
{
    /// Gives each slice its own fetch base, lifting a DM axis onto `Slice`.
    #[primitive(TuTensor::fetch_slice_lift)]
    pub fn fetch_slice_lift<OutSlice: M, OutTime: M>(
        self,
    ) -> FetchSliceLiftTensor<'l, T, D, Chip, Cluster, OutSlice, OutTime, Packet, B> {

예시: Q를 Slice로 리프팅하기

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 128, Q = 4, V = 16];

fn fetch_halves_per_slice<'l, const T: Tu>(
    input: BeginTensor<'l, T, bf16, m![1], m![1 # 2], m![A, 2], m![1], m![Q, V]>,
) -> FetchSliceLiftTensor<'l, T, bf16, m![1], m![1 # 2], m![A, Q / 2], m![Q % 2], m![V]> {
    input
        .fetch::<m![Q], m![V]>()
        .fetch_slice_lift::<m![A, Q / 2], m![Q % 2]>()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let b: BeginTensor<'_, _, bf16, m![1], m![1 # 2], m![A, 2], m![1], m![Q, V]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_halves_per_slice(b);
}

A = 128일 때 Slice 배치 m![A, 2]는 128개의 Slice 쌍을 나타내며, 각 쌍의 두 Slice는 동일한 m![Q, V] 데이터를 보유합니다. 일반 fetch는 두 Slice 모두에서 네 개의 Q 값을 전부 읽습니다. 리프트는 브로드캐스트 2를 Q / 2로 대체하고 Q % 2를 Time에 남깁니다. 첫 번째 Slice는 base 0에서 Q = 0, 1을 읽고, 두 번째 Slice는 base 32 요소(bf16 기준 64바이트)에서 Q = 2, 3을 읽습니다.

배치base읽기시간 단계
fetch, 두 Slice 공통m![A, 2]0Q = 0, 1, 2, 34
리프트, 첫 번째 Slicem![A, Q / 2]0Q = 0, 12
리프트, 두 번째 Slicem![A, Q / 2]32개 요소Q = 2, 32

임의의 Chip, Cluster 또는 Slice의 기준 주소는 리프트된 각 축의 값에 해당 축의 원래 DM 스트라이드를 곱한 값의 합입니다. 여기서 두 번째 Slice는 Q / 2 = 1이므로 base는 1 × (Q % 2) × V = 32개 요소입니다.

요구 사항

리프트는 다음 조건을 모두 만족해야 합니다.

  • 선택한 차원의 입력 크기와 출력 크기가 일치해야 합니다.
  • 브로드캐스트만 변경할 수 있습니다. 검증기는 매핑을 크기 2 그룹 단위로 비교합니다. 변경되는 브로드캐스트 2는 크기 2의 축 구성 요소가 되어야 합니다. 브로드캐스트를 패딩으로 대체하려면 DM 배치를 재구성(reshape)하고 해당 패딩을 읽지 않은 채로 두십시오. 한 번의 리프트에서 여러 그룹을 변경할 수 있지만, 기존 축이나 크기 3의 브로드캐스트는 변경할 수 없습니다.
  • 리프트된 축은 OutTime에서 제거해야 합니다. OutPacket은 변경되지 않습니다.
  • 리프트 메서드는 Chip, Cluster, Slice 순서로 호출해야 하며, 차원마다 최대 한 번만 호출할 수 있습니다.
  • 계산된 모든 base는 8바이트의 배수여야 합니다.
  • unsafe reshape로 브로드캐스트를 도입하는 경우, 데이터가 이미 복제되어 있어야 합니다. reshape는 요소 순서만 검사하고 복제 여부는 검사하지 않으므로, 이 요구 사항을 위반하면 하드웨어와 CPU의 결과가 달라질 수 있습니다.
#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 128, H = 2, V = 16, Rep = 2, G = 2];

// Valid: DMA replicates `[H, V]` across `Rep`; reshape exposes `Rep` as a broadcast.
fn named_broadcast(
    device: &mut Device,
    input: &HbmTensor<bf16, m![1], m![A, H, V]>,
) -> DmTensor<bf16, m![1], m![1 # 2], m![A, H], m![V]> {
    let written: DmTensor<bf16, m![1], m![1 # 2], m![A, Rep], m![H, V]> =
        input.to_dm::<m![1 # 2], m![A, Rep], m![H, V]>(&mut device.tdma);
    let dm: DmTensor<bf16, m![1], m![1 # 2], m![A, 2], m![H, V]> = unsafe { written.reshape() };

    device.main
        .begin(dm.view())
        .fetch::<m![H], m![V]>()
        .fetch_slice_lift::<m![A, H], m![1]>()
        .collect::<m![1], m![V]>()
        .commit_trim::<m![V]>()
        .commit()
}

// Invalid for lifting: `G` stores different values, but reshape names it as a broadcast.
fn without_broadcast(
    device: &mut Device,
    input: &HbmTensor<bf16, m![1], m![A, G, H, V]>,
) -> DmTensor<bf16, m![1], m![1 # 2], m![A, H], m![V]> {
    let written: DmTensor<bf16, m![1], m![1 # 2], m![A, G], m![H, V]> =
        input.to_dm::<m![1 # 2], m![A, G], m![H, V]>(&mut device.tdma);
    let dm: DmTensor<bf16, m![1], m![1 # 2], m![A, 2], m![H, V]> = unsafe { written.reshape() };

    device.main
        .begin(dm.view())
        .fetch::<m![H], m![V]>()
        .fetch_slice_lift::<m![A, H], m![1]>()
        .collect::<m![1], m![V]>()
        .commit_trim::<m![V]>()
        .commit()
}
}

제약 조건

  • 하드웨어 차원: Chip::SIZE, Cluster::SIZE, Slice::SIZE는 하드웨어 구성과 일치해야 합니다(Sequencer 참조).

다중 읽기 패킷

패킷 축이 DM에서 연속적이지 않을 수 있고 하드웨어는 한 번에 최대 32바이트까지만 읽으므로, 패킷 하나를 준비하는 데 하드웨어 읽기가 여러 번 필요할 수 있습니다. 메인 컨텍스트에서 read_size는 max_access_size의 약수 중 D[read_size]가 1, 2, 4, 8, 16 또는 32바이트가 되는 가장 큰 값입니다. max_access_size에 대해서는 Sequencer 아키텍처를 참조하십시오. 서브 컨텍스트에서 read_size는 8바이트로 고정됩니다. 컴파일러는 입력 요소 타입과 Fetch Adapter 캐스트 여부로부터 read_size를 도출합니다. 사용자는 이 값을 직접 설정하지 않습니다. Packet::SIZE > read_size이면 항상 다중 읽기가 발생합니다. 예를 들어 메인 컨텍스트에서 24바이트 패킷은 read_size = 8이 되며 패킷당 3번 읽게 됩니다. 총 사이클 수는 Time::SIZE * (Packet::SIZE / read_size)입니다.

다음 예제는 형태가 m![N, C, H, W](N=4, C=3, H=4, W=16)인 동일한 i4 텐서를 서로 다른 네 가지 OutPacket 선택으로 가져옵니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![N = 4, C = 3, H = 4, W = 16];

/// Sequencer config: [N = 4 : 192, C = 3 : 64, H = 4 : 16, W = 16 : 1].
/// max_access_size = 16; read_size = 16 (8 bytes); reads per packet = 1; cycles = 48
fn fetch_batch_1<'l, const T: Tu>(
    input: BeginTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]>,
) -> FetchTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![N, C, H], m![W]> {
    input.fetch()
}

/// Sequencer config: [N = 4 : 192, C = 3 : 64, H / 2 = 2 : 32, H % 2 = 2 : 16, W = 16 : 1].
/// max_access_size = 32; read_size = 32 (16 bytes); reads per packet = 1; cycles = 24
fn fetch_batch_2<'l, const T: Tu>(
    input: BeginTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]>,
) -> FetchTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![N, C, H / 2], m![H % 2, W]> {
    input.fetch()
}

/// Sequencer config: [N = 4 : 192, C = 3 : 64, H = 4 : 16, W = 16 : 1].
/// max_access_size = 64; read_size = 64 (32 bytes); reads per packet = 1; cycles = 12
fn fetch_batch_3<'l, const T: Tu>(
    input: BeginTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]>,
) -> FetchTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![N, C], m![H, W]> {
    input.fetch()
}

/// Sequencer config: [N = 4 : 192, C = 3 : 64, H = 4 : 16, W = 16 : 1].
/// max_access_size = 192; read_size = 64 (32 bytes); reads per packet = 3; cycles = 12
fn fetch_batch_4<'l, const T: Tu>(
    input: BeginTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]>,
) -> FetchTensor<'l, T, i4, m![1], m![1 # 2], m![1 # 256], m![N], m![C, H, W]> {
    input.fetch()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let b: BeginTensor<'_, _, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_batch_1(b);

let b: BeginTensor<'_, _, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_batch_2(b);

let b: BeginTensor<'_, _, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_batch_3(b);

let b: BeginTensor<'_, _, i4, m![1], m![1 # 2], m![1 # 256], m![1], m![N, C, H, W]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_batch_4(b);
}

인터리빙

인터리빙은 매핑이 동일한 두 텐서를 하나의 Sequencer 연산으로 결합하므로, 같은 계산에 두 텐서가 모두 필요할 때 오버헤드를 줄입니다. 명시적인 Time 축이 두 텐서 사이의 교대를 인코딩합니다.

다음 예제에서 메인 컨텍스트는 begin_interleaved()를 사용하여 인터리빙된 텐서를 생성합니다. 첫 번째 시간 반복은 lhs에서, 두 번째는 rhs에서, 세 번째는 다시 lhs에서 fetch하는 식으로 진행됩니다. 하나의 fetch 연산에서 인터리브할 수 있는 텐서는 최대 두 개입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 16, B = 32, I = 2];

/// Interleaves two input tensors into a single packet stream.
/// Useful for operations like 'input1 + input2' in the Vector Engine.
/// The interleaved BeginTensor is created via Tu.begin_interleaved().
/// The `I = 2` axis in Time encodes alternation between the two tensors.
fn fetch_interleaved<'l>(
    device: &'l mut Device,
    lhs: &'l DmTensor<i8, m![1], m![1 # 2], m![1 # 256], m![A, B]>,
    rhs: &'l DmTensor<i8, m![1], m![1 # 2], m![1 # 256], m![A, B]>,
) -> FetchTensor<'l, { Tu::Main }, i8, m![1], m![1 # 2], m![1 # 256], m![A, I], m![B]> {
    device.main.begin_interleaved::<I, _, _, _, _, _>(lhs.view(), rhs.view()).fetch()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();

let lhs = DmTensor::new();
let rhs = DmTensor::new();
let _o = fetch_interleaved(&mut device, &lhs, &rhs);
}

최적화

Fetch Sequencer의 처리량은 세 가지 요인으로 결정됩니다.

  • 입력 대역폭: read_size는 DM에서의 축 연속성과 패킷 크기에 의해 제한됩니다. 인접하지 않은 축은 max_access_size를 줄이고, 그 결과 read_size도 줄어듭니다(비연속 패킷 참조). 패킷이 연속 구간보다 작아도 read_size가 제한됩니다. 더 큰 2의 거듭제곱으로 패딩하면 이 값이 커집니다(패킷 패딩 참조).

    같은 뱅크에 반복해서 접근하면 우선순위가 낮은 Commit Engine 및 DMA Engine 연산이 기아 상태에 빠질 수 있습니다. 연속 접근 한도는 64회이며, 이를 초과하면 NoC 타임아웃이 발생할 수 있습니다.

    자세한 내용은 메모리 성능을 참조하십시오.

  • 출력 대역폭: 하류의 Collect Engine은 Fetch의 패킷을 32바이트 플릿으로 변환하므로, 32바이트에 정렬되지 않는 패킷 크기는 대역폭을 낭비합니다. 20바이트 패킷은 플릿 하나를 채우고 12바이트를 0으로 패딩하므로 12 / 32 = 37.5%가 낭비됩니다. 40바이트 패킷은 플릿 두 개(총 64바이트)에 걸치며 두 번째 플릿의 마지막 24바이트를 0으로 패딩하므로 24 / 64 = 37.5%가 낭비됩니다.

  • 공간 병렬성: fetch를 여러 Slice에 분산하면 처리량이 최대화됩니다.

예제: 패킷 패딩

OutPacket을 더 큰 2의 거듭제곱 요소 수로 패딩하면 read_size가 커질 수 있습니다. 아래 세 예제는 패킷을 2바이트에서 16바이트, 32바이트로 늘려 동일한 30바이트 텐서를 각각 15, 3, 1사이클에 fetch합니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 3, B = 5, C = 2];

/// Smallest packet: only C dimension padded to 8bytes. Takes 15 cycles.
fn fetch_packet_C<'l, const T: Tu>(
    input: BeginTensor<'l, T, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![A, B, C]>,
) -> FetchTensor<'l, T, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![A, B], m![C # 8]> {
    input.fetch()
}

/// Medium packet: B and C dimensions padded to 16 bytes. Takes 3 cycles.
fn fetch_packet_BC<'l, const T: Tu>(
    input: BeginTensor<'l, T, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![A, B, C]>,
) -> FetchTensor<'l, T, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![A], m![[B, C] # 16]> {
    input.fetch()
}

/// Largest packet: all dimensions padded to 32 bytes. Takes 1 cycle.
fn fetch_packet_ABC<'l, const T: Tu>(
    input: BeginTensor<'l, T, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![A, B, C]>,
) -> FetchTensor<'l, T, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![[A, B, C] # 32]> {
    input.fetch()
}


let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let x: BeginTensor<'_, _, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![A, B, C]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_packet_C(x);
let y: BeginTensor<'_, _, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![A, B, C]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_packet_BC(y);
let z: BeginTensor<'_, _, f8e4m3, m![1], m![1 # 2], m![1 # 256], m![1], m![A, B, C]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_packet_ABC(z);
}

이 예제들에서 패딩은 실제 데이터 범위를 넘어 읽지만, 패딩 값은 계산에 영향을 주지 않으므로 안전합니다. 패딩 전략이 다르면 FetchTensor 매핑도 달라지며, 이는 하류 컴포넌트에 영향을 줄 수 있습니다.

동일한 A = 3, B = 5, C = 2 f8e4m3 텐서는 다음과 같은 패킷 매핑을 사용할 수 있습니다.

함수Fetch TimeFetch PacketFetch 작업량Collect 패딩
fetch_packet_cA, BC # 88바이트 패킷 15개패킷당 24바이트입니다.
fetch_packet_bcA[B, C] # 1616바이트 패킷 3개패킷당 16바이트입니다.
fetch_packet_abc1[A, B, C] # 3232바이트 패킷 1개없음.

8바이트 후보와 16바이트 후보는 Collect가 각 패킷을 32바이트 플릿으로 패딩해야 합니다. 32바이트 후보는 하나의 완전한 플릿으로 Collect에 도달하지만, 추가된 물리적 용량 때문에 DM 사용량이 늘어날 수 있습니다. 고정된 워크로드에서 fetch 횟수가 줄어드는 이점이 패딩 비용과 다운스트림 레이아웃 비용보다 큰지는 스케줄 비교를 통해 판단합니다.


원문 © FuriosaAI · Programming Tensor Contraction Processors · Apache License 2.0 · NOTICE
yeokja 한국어 번역 · Anthropic 사의 claude-sonnet-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다 · 이 문서는 원문을 수정한 비공식 번역본이며 Apache License 2.0으로 제공합니다.