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

Commit Engine

Commit Engine은 Adapter가 최종 요소 타입과 레이아웃을 선택한 뒤 Tensor Unit 스트림 패킷을 DM에 씁니다. 이는 Fetch Engine의 역방향 동작입니다. 이는 Commit Sequencer이며, 모든 Slice에서 독립적으로 실행되어 각 Slice가 자신의 로컬 DM 파티션에 기록하는 수학적 텐서 이동입니다.

계산된 Tensor Unit 스트림을 의도한 Element 레이아웃을 갖는 DM 텐서로 만들어야 할 때 Commit을 선택하십시오.

인터페이스

TuTensor는 Tensor Unit 파이프라인의 끝에서 Chip, Cluster, Slice, Time, Packet 차원을 가집니다. 이때 Time은 계산의 시간적 언롤링을 반영하며, Packet은 출력 스트림의 요소 레이아웃입니다.

.commit()은 스트림을 DM의 DmTensor에 기록합니다.

impl<'l, const T: Tu, P: CanApplyCommit, 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>
{
    /// Commits to data memory.
    #[primitive(TuTensor::commit)]
    pub fn commit<Element: M>(self) -> DmTensor<D, Chip, Cluster, Slice, Element, B> {
        verify_commit::<D, Time, Packet, Element>();
        DmTensor::from_parts(self.inner.transpose(false), None)
    }

    /// Commits to a mutable tensor view in data memory.
    #[primitive(TuTensor::commit_view)]
    pub fn commit_view<DstSlice: M, Element: M>(
        self,
        mut dst: DmTensorViewMut<'l, D, Chip, Cluster, DstSlice, Element, B>,
    ) {
        constraints::assert_slice_preserved::<Slice, DstSlice>();
        verify_commit::<D, Time, Packet, Element>();
        dst.inner.transpose(self.inner.view(), false);
    }
}

각 Slice가 자신의 DM 파티션에 독립적으로 기록하므로 .commit()은 Chip, Cluster, Slice 차원을 변경 없이 그대로 유지합니다. 출력 Element 매핑은 Time과 Packet을 대체하여 스트림이 DM에 배치되는 방식을 정의합니다. Element는 Commit Sequencer와 Commit Adapter를 모두 구성하며, 입력 스트림을 기준으로 Time 축의 순서를 바꿔 커밋 중에 전치를 수행할 수 있습니다. Element 매핑이 성능에 미치는 영향은 최적화를 참고하십시오.

다음 예제는 캐스트된 누적 결과를 bf16으로 DM에 커밋합니다. 출력 DmTensor는 256개 Slice에 걸쳐 16개 시간 단계 × 8개의 bf16 요소를 저장합니다. 여기서 D = bf16이고 Element = m![M, N # 16]입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![P = 256, M = 16, N = 8];

fn cast_commit<'l, const T: Tu>(
    input: ContractTensor<'l, T, f32, m![1], m![1 # 2], m![P], m![M], m![N]>,
) -> DmTensor<bf16, m![1], m![1 # 2], m![P], m![M, N # 16]> {
    // Cast f32 to bf16 (Cast Engine), then commit to DM (Commit Engine).
    // Input: M = 16 time steps, N = 8 f32 elements per packet (32 bytes).
    // After cast: N = 8 bf16 elements padded to 16 (32 bytes).
    // After trim: N # 16 trimmed into N = 8.
    // The sequencer writes across P = 256 slices.
    input.cast::<bf16, m![N # 16]>().commit_trim::<m![N]>().commit()
}

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

let c: ContractTensor<'_, _, f32, m![1], m![1 # 2], m![P], m![M], m![N]> = ContractTensor::new(&mut device.main, Tensor::zero());
let _o = cast_commit(c);
}

제약 조건

  • 하드웨어 차원: Chip::SIZE, Cluster::SIZE, Slice::SIZE는 하드웨어 구성과 일치해야 합니다(Sequencer 참고).
  • 주소 정렬: 모든 Sequencer 스트라이드는 8바이트의 배수여야 합니다.
  • 쓰기 단위 정렬: D[valid_size]는 8, 16, 24, 32바이트 중 하나여야 합니다(Commit Adapter의 Trimming 단계 참고).

다중 쓰기 패킷

패킷 축이 DM에서 연속적이지 않을 수 있으므로 패킷 하나를 기록하는 데 여러 번의 하드웨어 쓰기가 필요할 수 있습니다. 컴파일러는 write_size = gcd(valid_size, access_size)를 도출합니다. Commit Adapter는 valid_size를 제공하고, Sequencer는 access_size를 제공합니다. 서브 컨텍스트에서는 D[write_size]가 8바이트로 고정됩니다. 전체 사이클 수는 Time::SIZE * (valid_size / write_size)입니다. 이 나눗셈은 항상 나누어떨어집니다. 메인 컨텍스트에서는 valid_size == write_size이므로 각 패킷이 한 사이클에 커밋됩니다. 서브 컨텍스트에서는 write_size가 8바이트로 고정되고 valid_size는 (트리밍 제약 조건에 따라) 8, 16, 24, 32바이트 중 하나이므로 valid_size / write_size는 항상 1, 2, 3, 4 중 하나입니다.

#![allow(unused)]
fn main() {
#![feature(adt_const_params)]
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![M = 4, K = 2, W = 8, N = 16, L = 32];

// Compiler-generated configuration: [
//   M -> 4 : 64,  (64 == 2 * 32,  contiguous)
//   K -> 2 : 32,   (32  == 32 * 1,  contiguous)
//   M -> 32 : 1    (packet dimension, contiguous)
// ] : 8
// access_size = 64; valid_size = 8; write_size = gcd(64, 8) = 8; writes per packet = 1
fn no_transpose<'l, const T: Tu>(
    input: CastTensor<'l, T, i8, m![1], m![1 # 2], m![1 # 256], m![M, K], m![L]>,
) -> DmTensor<i8, m![1], m![1 # 2], m![1 # 256], m![M, K, L]> {
    input.commit_trim::<m![L]>().commit()
}

// Compiler-generated configuration: [
//   M -> 4 : 8,   (8  != 2 * 32, NOT contiguous)
//   K -> 2 : 32,  (32 != 8 * 1,  NOT contiguous)
//   W -> 8 : 1    (packet dimension, contiguous)
// ] : 32
// access_size = 8; valid_size = 8; write_size = gcd(8, 8) = 8; writes per packet = 1
fn transpose<'l, const T: Tu>(
    input: ContractTensor<'l, T, f32, m![1], m![1 # 2], m![1 # 256], m![M, K], m![W]>,
) -> DmTensor<f32, m![1], m![1 # 2], m![1 # 256], m![K, M, W]> {
    input.commit_trim::<m![W]>().commit()
}

// Compiler-generated configuration: [
//   M -> 4 : 8,   (8  != 2 * 32, NOT contiguous)
//   K -> 2 : 32,  (32 != 8 * 1,  NOT contiguous)
//   N -> 8 : 1    (trimmed packet dimension, contiguous)
// ] : 16
// access_size = 8; valid_size = 8 (trimmed from 16); write_size = gcd(8, 8) = 8; writes per packet = 1
fn transpose_with_trimming<'l, const T: Tu>(
    input: CastTensor<'l, T, i8, m![1], m![1 # 2], m![1 # 256], m![M, K], m![N # 32]>,
) -> DmTensor<i8, m![1], m![1 # 2], m![1 # 256], m![K, M, N]> {
    input.commit_trim::<m![N]>().commit()
}


let mut device = Device::new(Topology { chips: 1, pes: 8 }).unwrap();
let a: CastTensor<'_, _, i8, m![1], m![1 # 2], m![1 # 256], m![M, K], m![L]> = CastTensor::new(&mut device.main, Tensor::zero());
let _o = no_transpose(a);
let b: ContractTensor<'_, _, f32, m![1], m![1 # 2], m![1 # 256], m![M, K], m![W]> = ContractTensor::new(&mut device.main, Tensor::zero());
let _o = transpose(b);
let c: CastTensor<'_, _, i8, m![1], m![1 # 2], m![1 # 256], m![M, K], m![N # 32]> = CastTensor::new(&mut device.main, Tensor::zero());
let _o = transpose_with_trimming(c);
}

최적화

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

  • 순차 주소: 각 Slice 안에서 순차적인 DM 주소에 쓰면 병렬 뱅크 접근이 가능합니다(DMN당 128 B/사이클, DMN 인터리빙 사용 시 256 B/사이클). 같은 뱅크에 연속으로 64회 이상 접근하는 패턴은 DM 뱅크 기아를 유발합니다.
  • 공간 병렬성: 활성화된 모든 Slice에 쓰기를 분산하면 처리량이 최대화됩니다.
  • 정렬된 쓰기(불변 조건): 쓰기 주소와 쓰기 단위가 모두 항상 8바이트로 정렬되어 있으므로 부분 뱅크 쓰기는 절대 발생하지 않습니다. Sequencer의 스트라이드는 8바이트의 배수이며(제약 조건 참조), Commit Adapter의 Trimming 단계는 D[valid_size]를 8바이트의 배수로 유지합니다.

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