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

Packet Reducer

Packet Reducer는 lane마다 리덕션 트리 하나를 두고, 단일 Packet 안에서 가장 안쪽의 축약 축들을 reduce-add합니다.

인터페이스

.contract_packet()는 Packet Reducer를 호출합니다. 각 lane은 Outer 단계의 OutPacket에서 물려받은 i4, i8, f8 또는 bf16 요소로 이루어진 32 B 또는 64 B Packet을 받습니다. 형식적으로는 \(\text{output}[i] = \sum_{j} \text{input}[i, j]\)를 계산하며, 여기서 i는 남는 (출력) 축들에 걸쳐 있고 j는 Packet 내부의 축약 축들에 걸쳐 있습니다.

impl<
    'l,
    const T: Tu,
    D: Scalar,
    Storage: ContractionCast<Output = D>,
    Chip: M,
    Cluster: M,
    Slice: M,
    Lane: M,
    Time: M,
    Packet: M,
    B: Backend,
> ContractOuterTensor<'l, T, D, Storage, Chip, Cluster, Slice, Lane, Time, Packet, B>
{
    /// Spatial reduction within `Packet`: validates the reduce-add along the contracted axes inside
    /// `Packet` that the fused fold at `contract_lane` will perform. `D` is the widened accumulator the
    /// deferred carrier stays keyed on; the DPE input packet is still sized in `Storage` bytes.
    #[primitive(ContractOuterTensor::contract_packet)]
    pub fn contract_packet<OutPacket: M>(
        self,
    ) -> ContractPacketTensor<'l, T, D, Chip, Cluster, Slice, Lane, Time, OutPacket, B> {
        verify_contract_packet(ContractPacketInput {
            in_packet: Packet::to_value(),
            out_packet: OutPacket::to_value(),
            element_bits: Storage::BITS,
        });
        // Carry the deferred operands forward unreduced: the fused contraction at `contract_lane`
        // performs this Packet reduction too. This stage only re-types the carrier to `OutPacket`.
        ContractPacketTensor::new(self.device, self.inner)
    }
}

아래 커널은 8개 lane을 모두 병렬로 사용합니다. 트리 깊이 5로 B의 bf16 요소 32개를 합산하여 A 위치마다 f32 하나를 생성합니다.

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

fn matmul<'l, const T: Tu>(
    input: CollectTensor<'l, T, bf16, m![1], m![1 # 2], m![1 # 256], m![A, B / 16], m![B % 16]>,
    trf: &TrfTensor<bf16, m![1], m![1 # 2], m![1 # 256], m![C], m![B]>,
) -> ContractTensor<'l, T, f32, m![1], m![1 # 2], m![1 # 256], m![A], m![C]> {
    //
    // Spatial reduction: tree depth 5 reduces 32 bf16 elements along B → f32
    // Output (Interleaved): Time = [A], Packet = [C]
    input.contract_outer::<m![A], m![B], _, _, _>(&trf)
         .contract_packet::<m![1]>()
         .contract_time::<m![A]>()
         .contract_lane::<m![A], m![C]>(LaneMode::Interleaved)
}

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

let a: CollectTensor<'_, _, bf16, m![1], m![1 # 2], m![1 # 256], m![A, B / 16], m![B % 16]> = CollectTensor::new(&mut device.main, Tensor::zero());
let b: TrfTensor<bf16, m![1], m![1 # 2], m![1 # 256], m![C], m![B]> = TrfTensor::zero();
let _o = matmul(a, &b);
}

아키텍처

ReducePacket = Packet / 2^d        for 0 ≤ d ≤ log2(Packet::SIZE)
OutPacket    = ReducePacket, outermost padding removed
               (size ≤ 32; when ReducePacket::SIZE > 32 only the innermost 32 survive)

Packet Reducer는 먼저 입력 Packet에 대해 lane마다 독립적인 리덕션 트리를 실행합니다. 깊이 0에서 트리의 리프는 입력 Packet의 요소를 담고 있으며, 이후 각 깊이에서는 쌍을 합산하여 요소 수를 절반으로 줄입니다. 최대 트리 깊이는 log2(Packet::SIZE)이므로 i4(Packet::SIZE = 128)는 7, i8 / f8(64)는 6, bf16(32)는 5입니다. 사용자의 OutPacket이 주어지면 컴파일러가 트리 깊이 d를 도출하고, 트리는 가장 안쪽의 2^d개 요소를 소비하여 ReducePacket을 생성합니다.

그다음 Packet Reducer는 ReducePacket을 OutPacket으로 잘라내는데, 하류에 있는 Time Reducer의 lane별 누산기에는 열이 32개뿐이므로 OutPacket의 크기는 최대 32개 요소로 제한됩니다. ReducePacket::SIZE > 32이면 바깥쪽 더미가 잘려 나가고 가장 안쪽의 32개 요소만 남습니다. 예를 들어 i4는 128개 요소의 Packet으로 도착하므로 d ∈ {0, 1}은 128개 또는 64개 요소의 ReducePacket을 생성하며, 둘 다 OutPacket::SIZE = 32로 잘립니다.

ReducePacket은 가장 바깥쪽 패딩(패딩된 packet 축)을 가질 수도 있으며, Packet Reducer는 그 패딩을 버리고 유효한 열만 남깁니다. 예를 들어 [A # 16]의 ReducePacket(A는 유효하며 16개 열까지 패딩됨)은 OutPacket = [A]로 리덕션됩니다. 따라서 OutPacket은 패딩이 있는 상태([A # 16])로 선언할 수도 있고 이미 제거된 상태([A])로 선언할 수도 있으며, 둘 다 같은 결과를 가리킵니다.

성능

지연 시간은 트리 깊이에 따라 달라지며, i4는 7사이클, i8/f8는 6사이클, bf16는 5사이클입니다. 요소 타입이 넓을수록 각 Packet에 들어가는 요소 수가 적으므로 깊이가 줄어듭니다. 가산기 트리는 완전히 파이프라인화되어 있으므로 깊이는 첫 출력까지의 지연 시간을 늘리지만 정상 상태의 처리량을 떨어뜨리지는 않습니다. 파이프라인이 채워지고 나면 매 사이클마다 Packet 하나가 들어가고 리덕션된 출력 하나가 나옵니다.

Lane < 8이면 비활성 lane의 리덕션 트리는 유휴 상태가 되므로 사이클당 처리량은 Lane::SIZE / 8에 비례하여 감소합니다.


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