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 Adapter

Fetch Adapter는 Fetch Engine이 내보내는 패킷 스트림에 요소별 변환(타입 캐스팅, 마스킹, 테이블 룩업, 영점 차감)을 적용하며, 이 변환은 Switch Engine이 스트림을 여러 Slice로 라우팅하기 전에 이루어집니다. Fetch Engine 자체는 이러한 변환을 전혀 수행하지 않습니다. 이 변환들은 텐서 계산 아래의 이 항목에 속하며, Fetch Engine과 Switch Engine 사이에서 별도의 단계로 적용됩니다. 커널 작성자는 FetchTensor에서 단계별 메서드를 직접 조합하며, 각 호출은 다음 단계로 진행합니다.

Adapter에는 사용할 수 있는 단계가 세 개 있으며, 각 단계는 선택 사항이고 하드웨어 파이프라인 순서에 따라 스트림에서 해당 메서드를 호출하여 실행합니다. FetchTensor는 Adapter를 전혀 호출하지 않고 Switch Engine이나 Collect Engine으로 바로 흘러갈 수도 있습니다.

테이블 룩업

테이블 룩업은 fetch 단계에서 하드웨어로 가속되는 룩업 테이블을 제공합니다. 각 값은 미리 구성된 테이블의 인덱스로 취급되며, 원래 값 대신 해당 테이블 항목이 출력됩니다. 이는 Sigmoid나 GeLU 같은 비선형 활성화 함수, 또는 사용자 정의 인코딩 테이블을 사용하는 양자화 방식처럼 표준 산술 연산으로는 효율적으로 구현할 수 없는 연산에 유용합니다. 이를 통해 다음이 가능합니다.

  • 비선형 활성화: 미리 계산된 룩업 테이블을 통해 Sigmoid, GeLU 및 기타 함수를 구현합니다.
  • 사용자 정의 타입 캐스팅: 변환 테이블을 사용하여 MXFP4 / NVFP4 같은 특수 인코딩을 표준 형식으로 변환합니다.

Sigmoid와 GeLU는 Vector Engine에서 직접 표현할 수도 있으므로, 테이블 룩업은 이러한 활성화 함수를 위한 유일한 경로가 아니라 여러 선택지 중 하나입니다.

// Table lookup is a **main-context-only** hardware feature: only the main Fetch Unit's
// `mode_indirect_table` can point at a resident lookup table, so this impl is fixed to
// `{ Tu::Main }`. The sub-context Fetch Unit (used by the StoTrf weight-staging path) has no
// table-lookup register, so decoding a packed weight must happen in a main-context fetch that
// commits the decoded stream to DM, after which a plain convert-only StoTrf stages it into the TRF.
impl<
    'l,
    P: CanApplyFetchTableLookup,
    D: MaterializableScalar,
    Chip: M,
    Cluster: M,
    Slice: M,
    Time: M,
    Packet: M,
    B: Backend,
> TuTensor<'l, { Tu::Main }, P, D, Chip, Cluster, Slice, Time, Packet, B>
{
    /// Runs the Fetch Adapter's table-lookup stage (main context only).
    ///
    /// Each input value indexes a decode table selected by the `TableLookupCast` impl,
    /// not by a runtime argument: `f4e2m1` decodes to either 8-bit float through the
    /// paired 4b->8b table (the NVFP4 / MXFP4 weight path), and either 8-bit float
    /// decodes to `bf16` through the non-paired table. Chain a
    /// [`fetch_cast`](Self::fetch_cast) to widen a paired decode to `f32`; the
    /// per-block scale is applied downstream in the Vector or Contraction Engine.
    /// See the book chapter `computing-tensors/fetch-adapter.md` ("Table Lookup").
    ///
    /// Only available on the main context: the lookup table lives in a main Fetch Unit register the
    /// sub context lacks. To feed a decoded weight into a contraction, decode here and commit the
    /// `f8e4m3` stream to DM, then stage that DM tensor into the TRF with a plain convert-only StoTrf.
    #[primitive(TuTensor::fetch_table_lookup)]
    pub fn fetch_table_lookup<OutD: Scalar>(
        self,
    ) -> FetchTableLookupTensor<'l, { Tu::Main }, OutD, Chip, Cluster, Slice, Time, Packet, B>
    where
        D: TableLookupCast<OutD>,
    {
        FetchTableLookupTensor::new(self.device, self.inner.map(|v| v.lookup()))
    }
}

디코드 테이블은 런타임 인자가 아니라 입력 스칼라 타입에 따라(TableLookupCast 트레이트를 통해) 선택되며, 이는 fetch_cast가 입력 타입에 따라 변환 방식을 선택하는 것과 정확히 같습니다. 키 폭이 테이블을 결정하고, 요청된 출력 타입이 항목 인코딩을 결정합니다.

입력출력
f4e2m1f8e4m3, f8e5m2
f8e4m3bf16
f8e5m2bf16

디코딩된 8비트 부동소수점 값은 뒤이은 fetch_cast로 f32까지 확장하고, NVFP4 / MXFP4의 블록별 스케일은 다운스트림의 Vector Engine이나 Contraction Engine에서 적용하십시오(스케일은 의도적으로 테이블에 합쳐 넣지 않으며, 테이블은 블록과 무관한 16개 항목의 정적 디코드로 유지됩니다).

f4e2m1 디코드용 테이블은 fetch-sequencer 구성에 내장된 컴파일 타임 상수이므로, 이 단계의 유일한 데이터 입력은 f4e2m1 가중치 스트림입니다. 호출마다 테이블을 스테이징하는 과정은 없습니다. 16개 항목의 e2m1 디코드는 블록과 무관하며, 블록별 스케일을 여기에 포함하지 않는 이유가 바로 이것입니다. 스케일을 합쳐 넣으면 블록마다 별도의 테이블이 필요해져 공유 정적 테이블의 이점이 사라집니다.

하드웨어 Sequencer는 원시 4비트 니블이 아니라 항상 바이트 정렬된 키(1바이트 또는 2바이트)를 순회하므로, 4비트 키는 반드시 짝을 이루는 테이블을 사용해야 합니다. 따라서 f4e2m1 디코딩은 256개 항목의 바이트 인덱스 테이블로 실행됩니다. 각 바이트에는 니블 두 개가 들어 있고, 한 번의 조회로 디코딩된 f8e4m3 값 한 쌍을 얻으므로 키당 두 요소씩 디코딩합니다. 이에 따라 f4e2m1 스칼라는 니블 두 개를 담은 바이트로 모델링되며, fetch 매핑이 바이트가 아닌 요소 단위로 계산하도록 BITS = 4로 설정됩니다.

#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 8];

/// Decodes an e2m1 (NVFP4 / MXFP4) weight stream to f8e4m3 via the hardware table,
/// then widens to f32 with a following fetch_cast.
fn fetch_decode_e2m1<'l, const T: Tu>(
    input: BeginTensor<'l, T, f4e2m1, m![1], m![1], m![1], m![1], m![A]>,
) -> FetchCastTensor<'l, T, f32, m![1], m![1], m![1], m![1], m![A]> {
    input
        .fetch::<m![1], m![A]>()
        .fetch_table_lookup::<f8e4m3>()
        .fetch_cast::<f32>()
}

타입 캐스팅

fetch_cast::<OutD>()는 Time 및 Packet 매핑을 유지하면서 요소 타입을 D에서 OutD로 변환합니다. 타입 캐스팅은 지연 시간을 1~2 사이클 늘립니다. fetch_cast는 타입 변환만 수행합니다. 영점 오프셋을 담는 정수 확장(i4에서 i5로, i8에서 i9로)은 영점 차감이라는 별도의 단계이므로 fetch_cast가 i5/i9를 만드는 일은 없습니다.

impl<
    'l,
    const T: Tu,
    P: CanApplyFetchCast,
    D: MaterializableScalar,
    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 Adapter's type-casting stage.
    ///
    /// Converts the stream's element type from `D` to `OutD`. The mapping
    /// shape is preserved.
    #[primitive(TuTensor::fetch_cast)]
    pub fn fetch_cast<OutD: Scalar>(self) -> FetchCastTensor<'l, T, OutD, Chip, Cluster, Slice, Time, Packet, B>
    where
        D: FetchCast<OutD>,
    {
        FetchCastTensor::new(self.device, self.inner.map(|v| v.cast()))
    }
}

RNGD는 아래의 변환만 지원하며 그 밖의 변환은 지원하지 않습니다. 모든 확장 변환은 i32 / f32 연산 폭으로 이어지며, 유일한 축소 변환은 f32에서 bf16으로의 변환입니다.

입력출력
i4i32
i8i32
i16i32
f8e4m3f32
f8e5m2f32
bf16f32
f32bf16

특히 Fetch Adapter에는 8비트 부동소수점에서 bf16으로의 캐스트가 없으며, f32에서 8비트 부동소수점으로의 캐스트도 없습니다. f8e4m3 스트림을 bf16로 받으려면 대신 비페어 테이블 룩업 테이블로 디코딩하십시오. 8비트 부동소수점으로 만들려면 파이프라인 뒤쪽의 Cast Engine에서 좁히십시오.

아래 예제는 8개 요소로 이루어진 i8 스트림을 fetch하여 i32로 캐스팅합니다. 호출 전후로 Time 및 Packet 매핑은 변하지 않습니다.

#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 8];

/// Fetches with type casting: converts i8 storage to i32 for computation.
/// Input:   i8 [0, 1, 2, 3, 4, 5, 6, 7]
/// Output: i32 [0, 1, 2, 3, 4, 5, 6, 7]
fn fetch_with_type_cast<'l, const T: Tu>(
    input: BeginTensor<'l, T, i8, m![1], m![1], m![1], m![1], m![A]>,
) -> FetchCastTensor<'l, T, i32, m![1], m![1], m![1], m![1], m![A]> {
    input.fetch::<m![1], m![A]>().fetch_cast::<i32>()
}

let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let x: BeginTensor<'_, _, i8, m![1], m![1], m![1], m![1], m![A]> = BeginTensor::new(&mut device.main, Tensor::zero());
let _o = fetch_with_type_cast(x);

타입 캐스팅은 read_size에 추가 제한을 둡니다. fetch 한 번당 캐스트 출력은 32바이트 플릿 하나에 들어가야 합니다(Collect Engine 참조).

  • 유효한 경우:
    • i4 -> i32, read_size = 8 (4 bytes): 8 × 4 = 32 B를 생성합니다
    • i8 -> i32, read_size = 8 (8 bytes): 8 × 4 = 32 B를 생성합니다
  • 잘못된 예:
    • i4 -> i32, read_size = 16 (8 bytes): 16 × 4 = 64 B를 생성합니다
    • i8 -> i32, read_size = 16 (16 bytes): 16 × 4 = 64 B를 생성합니다

영점 차감

fetch_zero_point_sub::<OutD>(zero_point)는 각 요소에서 양자화 zero_point를 빼고, 스트림을 Contraction Engine의 스테이징 타입으로 넓힙니다. i4는 i5로, i8은 i9로 넓어집니다. i5/i9를 생성하는 단계는 이 단계뿐입니다.

impl<
    'l,
    const T: Tu,
    P: CanApplyFetchZeroPointSub,
    D: MaterializableScalar,
    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 Adapter's zero-point-subtraction stage.
    ///
    /// Subtracts `zero_point` and widens the stream from `D` to its contraction-engine staging
    /// type `OutD` (`i4 -> i5`, `i8 -> i9`), the only way to produce an i5/i9
    /// stream. The result may only feed `contract_outer`; it is not
    /// [`MaterializableScalar`], so committing or re-routing it is a compile
    /// error. The mapping shape is preserved.
    ///
    /// Panics if `zero_point` is outside the source type's range
    /// ([`FetchZeroPointSub::ZERO_POINT_RANGE`]); a zero point in range keeps
    /// every widened residual within `OutD`, so this one check (independent of
    /// the stream data) is enough.
    #[primitive(TuTensor::fetch_zero_point_sub)]
    pub fn fetch_zero_point_sub<OutD: Scalar>(
        self,
        zero_point: i32,
    ) -> FetchZeroPointSubTensor<'l, T, OutD, Chip, Cluster, Slice, Time, Packet, B>
    where
        D: FetchZeroPointSub<OutD>,
    {
        let zero_point_range = <D as FetchZeroPointSub<OutD>>::ZERO_POINT_RANGE;
        assert!(
            zero_point_range.contains(&zero_point),
            "zero_point {zero_point} is outside the source type's quantized range {zero_point_range:?}",
        );
        FetchZeroPointSubTensor::new(self.device, self.inner.map(|v| v.zero_point_sub(zero_point)))
    }
}

추가 비트가 필요한 이유

영점을 빼면 zero_point를 중심으로 한 부호 없는 양자화 값이 부호 있는 잔차로 바뀌며, 이 잔차의 범위는 더 이상 입력 폭에 들어맞지 않습니다. 대칭 부호 있는 입력의 경우 잔차는 같은 폭을 가진 두 값의 차이입니다.

  • i4 잔차: [-8, 7] - [-8, 7] = [-15, 15]이며, 이를 담으려면 i5의 [-16, 15]가 필요합니다.
  • i8 잔차: [-128, 127] - [-128, 127] = [-255, 255]이며, 이를 담으려면 i9의 [-256, 255]가 필요합니다.

따라서 뺄셈의 출력은 입력보다 1비트 더 넓습니다. 변환은 이를 런타임에 검사합니다. i5/i9 범위를 벗어나는 잔차(범위를 벗어난 zero_point 또는 입력)는 조용히 래핑되지 않고 거부됩니다.

축약 전용 스테이징

i5/i9 스트림은 Switch Engine과 Collect Engine을 거쳐 흐를 수 있지만, 그 이후에 이 스트림을 합법적으로 소비할 수 있는 유일한 소비자는 contract_outer이며, 이는 피연산자 타입에서 설명하는 대로 스트림을 가중치와 짝짓습니다. 이 스트림은 메모리에 커밋하거나, Register File에 저장하거나(to_trf/to_vrf), 전치하거나, 다른 엔진에 공급할 수 없습니다.

이 제약은 런타임 검사가 아니라 컴파일 시점에 강제됩니다. i5/i9 스트림을 contract_outer가 아닌 다른 소비자에 전달하면 컴파일 오류가 발생합니다.


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