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

Time Reducer

Time Reducer는 Packet Reducer의 [Lane, Packet] 출력을 시간 누산기로 Time에 걸쳐 OutTime으로 누산합니다.

인터페이스

.contract_time::<OutTime>()는 Time Reducer를 호출합니다. OutTime은 남는 Time 차원을 지정합니다(나머지는 합산되어 사라집니다).

impl<'l, const T: Tu, D: Scalar, Chip: M, Cluster: M, Slice: M, Lane: M, Time: M, Packet: M, B: Backend>
    ContractPacketTensor<'l, T, D, Chip, Cluster, Slice, Lane, Time, Packet, B>
{
    /// Accumulates per-cycle contractions over the `Time` dimension via the shared
    /// accumulator buffer, shrinking input `Time` to `OutTime`. The axes present in
    /// `Time` but absent from `OutTime` are reduce-added.
    #[primitive(ContractPacketTensor::contract_time)]
    pub fn contract_time<OutTime: M>(
        self,
    ) -> ContractTimeTensor<'l, T, D, Chip, Cluster, Slice, Lane, OutTime, Packet, B> {
        verify_contract_time(ContractTimeInput {
            in_time: Time::to_value(),
            out_time: OutTime::to_value(),
        });
        // Carry the deferred operands forward unreduced: the fused contraction at `contract_lane`
        // performs this Time reduction too. This stage only re-types the carrier to `OutTime`.
        ContractTimeTensor::new(self.device, self.inner, Time::to_value())
    }
}

예를 들어, 아래 커널은 2D 텐서를 B를 따라 리덕션합니다(A만 남습니다).

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

/// Reduces along B; A survives.
fn reduce_b<'l, const T: Tu>(
    // Streaming operand: Slice = m![A / 8] (256 outer A chunks across slices).
    // Time = m![B / 16, A % 8]; Packet = m![B % 16].
    // B splits across Packet (B % 16) and Time (B / 16): each cycle produces a partial sum.
    input: CollectTensor<'l, T, bf16, m![1], m![1 # 2], m![A / 8], m![B / 16, A % 8], m![B % 16]>,
    // TRF operand: single-lane weight per slice.
    trf: &TrfTensor<bf16, m![1], m![1 # 2], m![A / 8], m![1], m![B]>,
    // Output: one f32 per (slice, A % 8) cell.
) -> ContractTensor<'l, T, f32, m![1], m![1 # 2], m![A / 8], m![A % 8], m![1 # 8]> {
    input
         // Outer: Lane = m![1], OutTime = m![B / 16, A % 8], OutPacket = m![B % 16].
         .contract_outer::<m![B / 16, A % 8], m![B % 16], _, _, _>(trf)
         // Packet Reducer: OutPacket = m![1]. Collapses B % 16 spatially.
         .contract_packet::<m![1]>()
         // Time Reducer: OutTime = m![A % 8]. Accumulator receives
         // Time::SIZE = (B / 16) × (A % 8) = 2 × 8 = 16 flits; B / 16 outer
         // chunks accumulate into 8 slots indexed by A % 8.
         .contract_time::<m![A % 8]>()
         // Lane Folder: Lane folds into OutPacket. Sequential mode (Lane = m![1]).
         .contract_lane::<m![A % 8], m![1 # 8]>(LaneMode::Sequential)
}

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

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

아키텍처

Time Reducer는 Packet Reducer가 사이클마다 내보내는 [Lane, Packet] 출력을 받습니다. 하드웨어는 상류에서 Lane::SIZE ≤ 8(공간적으로 병렬인 레인)과 Packet::SIZE ≤ 32로 상한을 둡니다.

Time Reducer는 매 사이클 [Lane, Packet] 공간 그리드를 Time에 걸쳐 OutTime으로 접습니다. OutTime은 남는 차원의 상대적 순서를 보존한 Time의 부분집합이어야 합니다(verify_contract_time이 이를 강제합니다). Time에는 있지만 OutTime에는 없는 차원은 합산되어 사라지며, 그러한 차원 중 가장 바깥 차원이 플릿을 순회합니다.

InnerTime을 Time의 안쪽 비리덕션 차원(가장 바깥 리덕션 차원보다 안쪽에 있으면서 OutTime에 남는 차원)이라고 합시다. 위의 reduce_b에서 Time = m![B / 4, A % 8]이고 OutTime = m![A % 8]이므로, B / 4가 가장 바깥 리덕션 차원이며(Time::SIZE = 2 × 8 = 16개의 플릿을 순회합니다) InnerTime = m![A % 8]입니다.

누산에는 InnerTime 튜플 값마다 하나씩, InnerTime::SIZE개의 [Lane, Packet] 슬롯이 필요합니다. 튜플이 같은 플릿은 같은 슬롯에 누산됩니다. reduce_b의 경우 8개의 슬롯이 B / 4 = 2회의 반복에 걸쳐 누산되며, 플릿 15 이후 버퍼에는 최종 리덕션 결과가 담기고 버퍼는 이를 Lane Folder에 넘깁니다:

Time = m![B / 4, A % 8]
          ~~~~~  ~~~~~
        outer R  inner non-R (A % 8)

Flit sequence (B / 4 has values 0,1; A % 8 has values 0..7):

  flit #0:  B/4=0, A%8=0  ──→ ┌─────────────────┐
  flit #8:  B/4=1, A%8=0  ──→ │ slot 0 (A%8=0)  │  accumulates B for A%8=0
                              └─────────────────┘

  flit #1:  B/4=0, A%8=1  ──→ ┌─────────────────┐
  flit #9:  B/4=1, A%8=1  ──→ │ slot 1 (A%8=1)  │  accumulates B for A%8=1
                              └─────────────────┘
   ⋮

  flit #7:  B/4=0, A%8=7  ──→ ┌─────────────────┐
  flit #15: B/4=1, A%8=7  ──→ │ slot 7 (A%8=7)  │  accumulates B for A%8=7
                              └─────────────────┘

  8 non-reduce positions → 8 slots used

제약 조건

InnerTime::SIZE가 슬롯 용량(버퍼가 담을 수 있는 슬롯 수)을 넘지 않으면 매핑이 버퍼에 들어맞습니다.

슬롯 용량은 버퍼의 1,024개 셀과 하류 Lane Folder의 LaneMode에 따라 정해집니다. 각 슬롯은 LaneMode가 형태를 결정하는 [Lane, Packet] 청크이므로, 슬롯 수는 1,024를 청크당 셀 수로 나눈 값입니다.

LaneMode청크 형태청크당 셀 수슬롯 용량
Interleaved[Lane # 8, Packet]8 × Packet::SIZE128 / Packet::SIZE
Sequential[Lane, Packet # 32]Lane::SIZE × 3232 / Lane::SIZE

Lane::SIZE = 1인 하위 단계 .contract_lane(LaneMode::Sequential) 아래의 reduce_b에서는 슬롯 용량이 32이므로 InnerTime::SIZE = 8이 여유 있게 들어갑니다. InnerTime::SIZE가 슬롯 용량을 초과한다면 Time을 재구성하거나(예: B를 더 분할) LaneMode를 전환하여 처리량을 일부 포기하는 대신 슬롯 여유 공간을 확보해야 합니다.

성능

처리량은 입력 측 기준으로 사이클당 한 패킷입니다. N개의 입력을 하나의 출력으로 리덕션하므로 실효 출력 속도는 입력의 1 / N입니다.

크기가 N인 Time 차원 리덕션의 지연 시간은 약 N 사이클입니다.


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