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

튜닝

튜닝은 통제된 실험입니다. 정확성을 유지하고, 기준선을 기록하고, 지원되는 레버 하나를 변경한 뒤, 변경을 유지할지 결정하기 전에 동일한 스케줄 지표를 비교합니다.

실험 통제하기

모든 후보에 대해 다음 순서를 따르십시오.

  1. 타입 검사 경로를 실행하여 매핑 및 형태 제약 조건을 검증하십시오.
  2. 관련 경계 사례를 포함하여 호스트 오라클로 CPU 테스트를 실행합니다.
  3. 기준선을 컴파일하고 해당 스케줄 makespan을 기록합니다.
  4. 릴리스, 형태, 데이터 타입, 매핑은 고정한 채 한 번에 하나의 요소만 변경하십시오.
  5. 동일한 검사를 실행하고 후보 스케줄을 덤프합니다.
  6. 정확성 검사를 여전히 통과하고 측정된 지표가 개선된 경우에만 변경을 유지하고, 그렇지 않으면 기준선으로 되돌리십시오.

이 장에서는 스케줄 makespan이 주요 정적 지표입니다. 재현 가능한 스케줄 비교나 별도로 문서화된 디바이스 증거 없이 처리량 또는 사이클 개선을 보고하지 마십시오.

레버 하나 선택하기

진단을 통해 레버가 겨냥할 의존성이나 자원을 특정한 뒤에만 레버를 선택하십시오.

  • 현재 자원이 병목이면 실행 엔진 경로를 변경합니다.
  • 스케줄에서 피할 수 있는 직렬 작업이나 잘 맞지 않는 리덕션이 드러나면 타일 또는 분할 형태를 변경하십시오.
  • 제한 구간이 이동이나 주소 의존성이면 매핑, 패딩 또는 전송 경계를 변경합니다.

이러한 선택지는 텐서 이동, 텐서 계산, 텐서 매핑 장에서 정의합니다.

루프 언롤링

#[unroll]을 사용하여 작은 정적 루프의 모든 반복을 스케줄러에 노출하십시오.

#![feature(proc_macro_hygiene, register_tool, stmt_expr_attributes)]
#![register_tool(furiosa_opt)]
extern crate furiosa_opt_std;
extern crate tokio;
use furiosa_opt_std::prelude::*;
axes![Group = 4];
fn consume(_: usize) {}
#[device]
fn example(_: &mut Device) {
#[unroll]
for group in 0..Group::SIZE {
    consume(group);
}
}
#[tokio::main]
async fn main() {
    let mut device = Device::new(example.topology()).unwrap();
    launch(example, &mut device).await.unwrap();
}

커널 크레이트에는 #![feature(proc_macro_hygiene, stmt_expr_attributes)]가 필요합니다. 반복 횟수는 컴파일 타임 상수여야 합니다. 루프는 디바이스 진입점 안에 있거나, 진입점에서 호출하는 헬퍼 안에 있을 수 있습니다. 중첩 루프는 자동으로 언롤링되지 않습니다. 모든 수준을 언롤링하려면 각 루프에 #[unroll]을 추가하십시오.

컴파일러는 #[unroll]을 적용한 뒤 남아 있는 루프가 있는지 검사합니다. Register File 재사용은 커널 어디에도 루프가 남아 있지 않을 때만 활성화됩니다. 일부 루프만 언롤링해도 해당 반복을 스케줄링에 노출할 수 있지만, 바깥 루프나 중첩 루프가 남아 있으면 루프가 없어야 가능한 최적화는 활성화되지 않습니다.

더블 버퍼링

더블 버퍼링을 사용하면 SubContext에서 다음 가중치 그룹을 스테이징하는 작업과 MainContext에서 현재 그룹을 축약하는 작업을 겹칠 수 있습니다. 스케줄러는 두 연산이 모두 같은 스케줄링 단위 안에서 보일 때만 이 겹침을 고려할 수 있습니다. 이러한 기회를 만들려면 수동 2단계 루프나 #[unroll]을 사용하십시오.

이 비교에서는 모든 사례에 20개의 가중치 그룹과 동일한 스케줄 윈도를 사용합니다.

루프 형태스케줄링 단위당 보이는 그룹 수반복 간 중첩makespan
일반 롤드 루프1아니요19,106 사이클
수동 2단계 소프트웨어 파이프라인2예15,477 사이클
완전 언롤링20예15,086 사이클

StoTrf는 가중치 그룹을 Tensor Register File(TRF)에 단계적으로 적재합니다. TRF는 각 절반에 그룹을 하나씩 배치할 수 있지만, 두 절반은 뱅크를 공유합니다. 용량과 주소 모드에 대한 자세한 내용은 Register File: 더블 버퍼링을 참조하십시오. Schedule Viewer를 사용하여 결과로 나타나는 중첩을 확인하십시오.

파란색 상자는 StoTrf를, 주황색 상자는 축약을, 초록색 띠는 측정된 중첩을 나타냅니다.

일반 롤드 루프: 19,106 사이클

스케줄러는 롤드 루프 본문에 대해 하나의 스케줄을 만들고, 모든 반복에서 그 스케줄을 되풀이합니다. 따라서 Contraction(i)와 StoTrf(i + 1)는 중첩될 수 없습니다.

#![allow(incomplete_features)]
#![feature(adt_const_params, proc_macro_hygiene, register_tool)]
#![register_tool(furiosa_opt)]
extern crate furiosa_opt_std;
extern crate tokio;
use furiosa_opt_std::prelude::*;
axes![Tok = 16, Red = 64, Out = 8, Group = 20];
use furiosa_opt_std::prelude::*;

type Chip = m![1];
type Cluster = m![Tok / 8 % 2];
type Slice = m![Tok % 8 # 256];

/// Stages and contracts one weight group per rolled iteration.
#[device(chip = 1)]
pub fn rolled(
    device: &mut Device,
    activation: &HbmTensor<bf16, Chip, m![Tok, Red]>,
    weight: &HbmTensor<bf16, Chip, m![Group, Out, Red]>,
) -> HbmTensor<bf16, Chip, m![Tok, Group, Out]> {
    let activation: DmTensor<bf16, Chip, Cluster, Slice, m![Red]> = activation.to_dm(&mut device.tdma);
    let weight: DmTensor<bf16, Chip, Cluster, Slice, m![Group, Out, Red]> = weight.to_dm(&mut device.tdma);
    let mut output: DmTensor<bf16, Chip, Cluster, Slice, m![Group, Out]> = DmTensor::new();

    for g in 0..Group::SIZE {
        let weight_group = weight.view().tile::<m![Group], 1, m![1 # 20, Out, Red]>(g);
        let weight_trf: TrfTensor<bf16, Chip, Cluster, Slice, m![Out], m![Red]> = device
            .sub
            .begin(weight_group)
            .fetch::<m![Out, Red / 16], m![Red % 16]>()
            .collect::<m![Out, Red / 16], m![Red % 16]>()
            .to_trf();

        device
            .main
            .begin(activation.view())
            .fetch::<m![Red / 16], m![Red % 16]>()
            .collect::<m![Red / 16], m![Red % 16]>()
            .contract_outer::<m![Red / 32], m![Red % 32], _, _, _>(&weight_trf)
            .contract_packet::<m![1]>()
            .contract_time::<m![1]>()
            .contract_lane::<m![1], m![Out]>(LaneMode::Interleaved)
            .cast::<bf16, m![Out # 16]>()
            .commit_trim::<m![Out]>()
            .commit_view(output.view_mut().tile::<m![Group], 1, m![1 #{!} 20, Out]>(g));
    }

    let mut result = HbmTensor::<bf16, Chip, m![Tok, Group, Out]>::new();
    output.view().to_hbm_view(&mut device.tdma, result.view_mut());
    result
}
#[tokio::main]
async fn main() {
let mut device = Device::new(rolled.topology()).unwrap();
let activation = HbmTensor::<bf16, m![1], m![Tok, Red]>::new();
let weight = HbmTensor::<bf16, m![1], m![Group, Out, Red]>::new();
let _output = launch(rolled, (&mut device, &activation, &weight)).await.unwrap();
}

수동 2단계 소프트웨어 파이프라인: 15,477 사이클

수동 파이프라인은 하나의 루프 본문에 두 그룹을 배치합니다. 이렇게 하면 Contraction(first)와 StoTrf(second)를 스케줄러가 함께 다룰 수 있습니다.

#![allow(incomplete_features)]
#![feature(adt_const_params, proc_macro_hygiene, register_tool)]
#![register_tool(furiosa_opt)]
extern crate furiosa_opt_std;
extern crate tokio;
use furiosa_opt_std::prelude::*;
axes![Tok = 16, Red = 64, Out = 8, Group = 20, Pairs = 10];
use furiosa_opt_std::prelude::*;

type Chip = m![1];
type Cluster = m![Tok / 8 % 2];
type Slice = m![Tok % 8 # 256];

/// Exposes two weight groups per iteration for software pipelining.
#[device(chip = 1)]
pub fn software_pipelined(
    device: &mut Device,
    activation: &HbmTensor<bf16, Chip, m![Tok, Red]>,
    weight: &HbmTensor<bf16, Chip, m![Group, Out, Red]>,
) -> HbmTensor<bf16, Chip, m![Tok, Group, Out]> {
    let activation: DmTensor<bf16, Chip, Cluster, Slice, m![Red]> = activation.to_dm(&mut device.tdma);
    let weight: DmTensor<bf16, Chip, Cluster, Slice, m![Group, Out, Red]> = weight.to_dm(&mut device.tdma);
    let mut output: DmTensor<bf16, Chip, Cluster, Slice, m![Group, Out]> = DmTensor::new();

    for pair in 0..Pairs::SIZE {
        let first = pair * 2;
        let second = first + 1;
        let first_weight = weight.view().tile::<m![Group], 1, m![1 # 20, Out, Red]>(first);
        let first_trf: TrfTensor<bf16, Chip, Cluster, Slice, m![Out], m![Red]> = device
            .sub
            .begin(first_weight)
            .fetch::<m![Out, Red / 16], m![Red % 16]>()
            .collect::<m![Out, Red / 16], m![Red % 16]>()
            .to_trf();
        let second_weight = weight.view().tile::<m![Group], 1, m![1 # 20, Out, Red]>(second);
        let second_trf: TrfTensor<bf16, Chip, Cluster, Slice, m![Out], m![Red]> = device
            .sub
            .begin(second_weight)
            .fetch::<m![Out, Red / 16], m![Red % 16]>()
            .collect::<m![Out, Red / 16], m![Red % 16]>()
            .to_trf();

        device
            .main
            .begin(activation.view())
            .fetch::<m![Red / 16], m![Red % 16]>()
            .collect::<m![Red / 16], m![Red % 16]>()
            .contract_outer::<m![Red / 32], m![Red % 32], _, _, _>(&first_trf)
            .contract_packet::<m![1]>()
            .contract_time::<m![1]>()
            .contract_lane::<m![1], m![Out]>(LaneMode::Interleaved)
            .cast::<bf16, m![Out # 16]>()
            .commit_trim::<m![Out]>()
            .commit_view(output.view_mut().tile::<m![Group], 1, m![1 #{!} 20, Out]>(first));
        device
            .main
            .begin(activation.view())
            .fetch::<m![Red / 16], m![Red % 16]>()
            .collect::<m![Red / 16], m![Red % 16]>()
            .contract_outer::<m![Red / 32], m![Red % 32], _, _, _>(&second_trf)
            .contract_packet::<m![1]>()
            .contract_time::<m![1]>()
            .contract_lane::<m![1], m![Out]>(LaneMode::Interleaved)
            .cast::<bf16, m![Out # 16]>()
            .commit_trim::<m![Out]>()
            .commit_view(output.view_mut().tile::<m![Group], 1, m![1 #{!} 20, Out]>(second));
    }

    let mut result = HbmTensor::<bf16, Chip, m![Tok, Group, Out]>::new();
    output.view().to_hbm_view(&mut device.tdma, result.view_mut());
    result
}
#[tokio::main]
async fn main() {
let mut device = Device::new(software_pipelined.topology()).unwrap();
let activation = HbmTensor::<bf16, m![1], m![Tok, Red]>::new();
let weight = HbmTensor::<bf16, m![1], m![Group, Out, Red]>::new();
let _output = launch(software_pipelined, (&mut device, &activation, &weight)).await.unwrap();
}

완전 언롤링: 15,086 사이클

#[unroll]은 수동으로 짝을 짓지 않고도 20개 그룹을 모두 드러냅니다.

#![allow(incomplete_features)]
#![feature(adt_const_params, proc_macro_hygiene, register_tool)]
#![register_tool(furiosa_opt)]
extern crate furiosa_opt_std;
extern crate tokio;
use furiosa_opt_std::prelude::*;
axes![Tok = 16, Red = 64, Out = 8, Group = 20];
use furiosa_opt_std::prelude::*;

type Chip = m![1];
type Cluster = m![Tok / 8 % 2];
type Slice = m![Tok % 8 # 256];

/// Exposes every weight group by fully unrolling the group loop.
#[device(chip = 1)]
pub fn unrolled(
    device: &mut Device,
    activation: &HbmTensor<bf16, Chip, m![Tok, Red]>,
    weight: &HbmTensor<bf16, Chip, m![Group, Out, Red]>,
) -> HbmTensor<bf16, Chip, m![Tok, Group, Out]> {
    let activation: DmTensor<bf16, Chip, Cluster, Slice, m![Red]> = activation.to_dm(&mut device.tdma);
    let weight: DmTensor<bf16, Chip, Cluster, Slice, m![Group, Out, Red]> = weight.to_dm(&mut device.tdma);
    let mut output: DmTensor<bf16, Chip, Cluster, Slice, m![Group, Out]> = DmTensor::new();

    #[unroll]
    for g in 0..Group::SIZE {
        let weight_group = weight.view().tile::<m![Group], 1, m![1 # 20, Out, Red]>(g);
        let weight_trf: TrfTensor<bf16, Chip, Cluster, Slice, m![Out], m![Red]> = device
            .sub
            .begin(weight_group)
            .fetch::<m![Out, Red / 16], m![Red % 16]>()
            .collect::<m![Out, Red / 16], m![Red % 16]>()
            .to_trf();

        device
            .main
            .begin(activation.view())
            .fetch::<m![Red / 16], m![Red % 16]>()
            .collect::<m![Red / 16], m![Red % 16]>()
            .contract_outer::<m![Red / 32], m![Red % 32], _, _, _>(&weight_trf)
            .contract_packet::<m![1]>()
            .contract_time::<m![1]>()
            .contract_lane::<m![1], m![Out]>(LaneMode::Interleaved)
            .cast::<bf16, m![Out # 16]>()
            .commit_trim::<m![Out]>()
            .commit_view(output.view_mut().tile::<m![Group], 1, m![1 #{!} 20, Out]>(g));
    }

    let mut result = HbmTensor::<bf16, Chip, m![Tok, Group, Out]>::new();
    output.view().to_hbm_view(&mut device.tdma, result.view_mut());
    result
}
#[tokio::main]
async fn main() {
let mut device = Device::new(unrolled.topology()).unwrap();
let activation = HbmTensor::<bf16, m![1], m![Tok, Red]>::new();
let weight = HbmTensor::<bf16, m![1], m![Group, Out, Red]>::new();
let _output = launch(unrolled, (&mut device, &activation, &weight)).await.unwrap();
}

모든 캡처는 4,000 사이클부터 8,000 사이클까지를 보여 줍니다.


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