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

매핑 표현식

매핑 표현식은 매핑을 인코딩하는 Rust 타입입니다. 먼저 위치의 Cell을 읽으십시오. 그런 다음 Index 항, 패딩 종류 또는 범위 밖 결과를 사용하여 접근을 디버깅하십시오. 이 절에서는 이러한 결과를 만들어 내는 생성자와 지원되는 동작을 정의합니다.

축 크기

axes! 매크로는 축 식별자와 그 크기를 선언합니다. 다음 선언은 이 절 전체에 적용됩니다:

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

위치 결과

m![H, W]와 같은 매핑 표현식은 각 물리적 위치를 Cell에 매핑합니다. 항을 읽기 전에 먼저 결과를 확인하십시오:

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;
axes![A = 8];
type E = m![A];
assert_eq!(E::map(0), i![A: 0]);
}

Cell::Index는 구조적 라이브 결과입니다. 이 결과는 매핑 항을 보존하며, Index::finalize()는 해당 항의 스칼라 좌표를 검증하고 모듈로 초과에 대해 오류를 반환할 수 있습니다. 패딩 위치와 범위 밖 위치에는 라이브 항이 없습니다. Cell::Index(Index::new())는 축 항이 없는 유효한 라이브 결과입니다. 이는 Cell::Padding 및 Cell::OutOfBounds와 구별되므로, 빈 Index가 라이브가 아닌 위치를 나타내는 경우는 없습니다. i! 매크로는 할당을 작성된 순서대로 결합하며, 이 순서는 합성된 매핑의 상위에서 하위로의 순서입니다. Cell::combine은 패딩에 대해 교환 법칙이 성립하지 않으므로, 해당 순서를 의도한 경우에만 할당 순서를 바꾸십시오.

M 생성자 규칙

모든 매핑 표현식은 버퍼 크기와 위치 매핑을 제공하는 M 트레이트를 구현합니다:

#![allow(unused)]
fn main() {
// Inside `furiosa_opt_std::prelude`...
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
use std::fmt::Debug;
pub trait M: Debug + Clone {
    /// The computed size for the given shape.
    const SIZE: usize;

    /// Converts the mapping expression type into a value.
    fn to_value() -> Mapping;

    /// Converts a buffer index to a physical cell result.
    fn map(i: usize) -> Cell;
}

/// A live tensor index: a map from axis identifiers to coordinate values.
pub struct Index { /* ... */ }

/// Results for live, padded, and out-of-range positions.
pub enum Cell { Index(Index), Padding(PaddingKind), OutOfBounds }

/// Constructs classified cells.
/// `i![A: 2, B: 3]` creates a live `Cell::Index` with A = 2 and B = 3;
/// non-live contributions remain `Cell::Padding` or `Cell::OutOfBounds`.
macro_rules! i {
    () => {};
    /* ... */
}
}

호스트 텐서 사용법

M 트레이트를 기반으로 만든 가장 단순한 구체 타입은 HostTensor<D, E>입니다. 이는 요소 타입이 D인 호스트 메모리 버퍼로, 그 표현은 매핑 E에 의해 완전히 결정됩니다. E는 버퍼 크기(E::SIZE)와 버퍼 위치에서 셀로의 대응(E::map)을 모두 결정합니다. HostTensor<bf16, m![A, B]>는 bf16 데이터 4,096개 요소를 담습니다. 호스트 텐서의 경우, E::map이 반환하는 각 Cell::Index(index)는 해당 물리적 위치에 저장된 구조적 Index를 식별합니다. 이를 텐서 인덱스로 취급하기 전에 Index::finalize()를 호출하여 스칼라 좌표를 검증하십시오. 초과가 있으면 이 단계에서 실패할 수 있습니다. Cell::OutOfBounds는 읽기의 경우 CellExt::finalize(PaddingKind::Top)으로, 쓰기의 경우 CellExt::finalize(PaddingKind::Bottom)으로 변환하십시오. holds의 형식적 정의는 텐서 의미론에 나와 있습니다.

HbmTensor와 DmTensor 같은 디바이스 텐서는 여러 매핑 표현식에 걸친 더 복잡한 표현을 가집니다. 자세한 내용은 공간 차원과 시간 차원을 참조하십시오.

생성자

HostTensor<D, E>의 매핑 E를 포함한 매핑 표현식은 작은 생성자를 합성하여 만들며, 각 생성자는 더 단순한 매핑을 변환하거나 결합합니다. 이러한 표현식은 산술 연산자와 유사한 연산자(/, %, 그리고 패딩을 위한 #)를 사용하여 텐서 인덱스와 선형 버퍼 인덱스 간의 매핑을 간결하게 정의합니다.

심볼

심볼은 대문자 한 글자이며, 그 크기는 형태 선언에서 정해집니다. 매핑 m![A]는 8개의 버퍼 인덱스를 축을 따라 텐서 인덱스에 선형으로 매핑합니다:

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;

axes![A = 8];

type E = m![A]; // Symbol<Ident::A, 8>

fn test_symbol() {
    assert_eq!(E::map(0), i![A: 0]);
    assert_eq!(E::map(1), i![A: 1]);
    assert_eq!(E::map(2), i![A: 2]);
    for i in 0..E::SIZE {
        assert_eq!(E::map(i), i![A: i]);
    }
    assert_eq!(E::map(E::SIZE), Cell::OutOfBounds);
}

test_symbol();
}
impl<S: AxisName> M for Symbol<S> {
    const SIZE: usize = S::SIZE;

    fn to_value() -> Mapping {
        Mapping::Symbol {
            symbol: S::NAME,
            size: S::SIZE,
        }
    }

    fn map(i: usize) -> Cell {
        if i >= S::SIZE {
            return Cell::OutOfBounds;
        }
        Cell::Index({
            let mut index = Index::new();
            index.add_term(
                Term {
                    inner: Atom::Symbol {
                        symbol: S::NAME,
                        size: S::SIZE,
                    },
                    stride: 1,
                    modulo: S::SIZE,
                },
                i,
            );
            index
        })
    }
}

Note

모든 심볼 A에 대해 0번째 인덱스 i![A: 0]은 빈 텐서 인덱스 i![]와 동등합니다.

쌍

쌍 매핑 m![A, B]는 형태가 \(\{A=8, B=512\}\)인 2D 텐서를 4,096개 요소로 이루어진 버퍼로 저장합니다. 매핑 Pair<L, R>는 두 공간의 데카르트 곱을 선형 버퍼로 매핑하며, 여기서 L은 주 차원이고 R은 부 차원입니다. 크기는 L::SIZE * R::SIZE이며, 이 매핑은 내림 나눗셈과 나머지 연산으로 인덱스를 분해합니다. 주 차원(L)의 결과가 패딩이면 부 차원(R)의 결과를 고려하기 전에 그 패딩의 종류가 우선합니다. 유효한 조합이 불가능하면 범위 밖 결과가 전파됩니다. m![A, B, C, D]는 Pair<A, Pair<B, Pair<C, D>>>로 전개되며 오른쪽 결합입니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;

axes![A = 8, B = 512];

type E = m![A, B]; // Pair<m![A], m![B]>

fn test_pair() {
    // First 512 elements hold A=0, next 512 hold A=1
    assert_eq!(E::map(0),   i![A: 0, B: 0]);
    assert_eq!(E::map(511), i![A: 0, B: 511]);
    assert_eq!(E::map(512), i![A: 1, B: 0]);
    assert_eq!(E::map(519), i![A: 1, B: 7]); // 519 == 512 * 1 + 7
    for i in 0..E::SIZE {
        assert_eq!(E::map(i), i![A: i / <m![B]>::SIZE, B: i % <m![B]>::SIZE]);
    }
    assert_eq!(E::map(E::SIZE), Cell::OutOfBounds);
}

test_pair();
}
impl<L, R> M for Pair<L, R>
where
    L: M,
    R: M,
{
    const SIZE: usize = L::SIZE * R::SIZE;

    fn to_value() -> Mapping {
        Mapping::Pair {
            left: RBox::new(L::to_value()),
            right: RBox::new(R::to_value()),
        }
    }

    fn map(i: usize) -> Cell {
        if i >= Self::SIZE {
            return Cell::OutOfBounds;
        }
        let l = L::map(i / R::SIZE);
        let r = R::map(i % R::SIZE);
        match l {
            Cell::OutOfBounds => Cell::OutOfBounds,
            Cell::Padding(kind) => Cell::Padding(kind),
            Cell::Index(mut left) => match r {
                Cell::Index(right) => {
                    left.add(right);
                    Cell::Index(left)
                }
                Cell::Padding(kind) => Cell::Padding(kind),
                Cell::OutOfBounds => Cell::OutOfBounds,
            },
        }
    }
}

항등

항등 매핑 m![1]은 버퍼 인덱스 0을 빈 텐서 인덱스 i![]로 매핑하는 단일 요소 버퍼를 생성합니다. 이는 Pair에 대한 항등 요소 역할을 합니다. m![1, A]와 m![A, 1]은 모두 m![A]와 동등합니다. 더 일반적으로, 브로드캐스트 매핑은 크기 미만의 모든 위치에 대해 동일한 유효한 빈 Cell::Index(Index::new())를 반환하고, 그 크기를 넘어서는 위치에 대해서는 Cell::OutOfBounds를 반환합니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;

type E = m![1]; // Identity

fn test_identity() {
    assert_eq!(E::map(0), i![]);
    assert_eq!(E::map(1), Cell::OutOfBounds);
}

test_identity();
}
/// The identity mapping (size-1 broadcast), the unit written `m![1]`.
pub type Identity = Broadcast<1>;

패딩

패딩은 사용하지 않는 버퍼 공간을 추가하여 데이터를 하드웨어 요구 사항에 맞게 정렬합니다. 예를 들어, DMA Engine은 행이 64바이트 경계에서 시작할 것을 요구합니다. axes![C = 13, D = 61]에서는 61이 64로 나누어떨어지지 않으므로 m![C, D]가 정렬되지 않은 행을 만듭니다. m![C, D # 64]는 행마다 추가 요소 3개를 사용하여 각 행을 64바이트 경계에 정렬함으로써 이 문제를 해결합니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;

axes![C = 13, D = 61];

type E = m![C, D # 64]; // Pair<m![C], Padding<m![D], 64>>

fn test_padding() {
    assert_eq!(E::map(0),  i![C: 0, D: 0]);
    assert_eq!(E::map(60), i![C: 0, D: 60]);
    assert_eq!(E::map(61), Cell::Padding(PaddingKind::Top));
    assert_eq!(E::map(62), Cell::Padding(PaddingKind::Top));
    assert_eq!(E::map(63), Cell::Padding(PaddingKind::Top));
    assert_eq!(E::map(64), i![C: 1, D: 0]);
}

test_padding();
}
impl<L, F, const KIND: PaddingKind> M for Padding<L, F, KIND>
where
    L: M,
    F: Count,
{
    const SIZE: usize = F::SIZE;

    fn to_value() -> Mapping {
        Mapping::Padding {
            inner: RBox::new(L::to_value()),
            padding: F::SIZE,
            kind: KIND,
        }
    }

    fn map(i: usize) -> Cell {
        if i >= F::SIZE {
            Cell::OutOfBounds
        } else if i >= L::SIZE {
            Cell::Padding(KIND)
        } else {
            L::map(i)
        }
    }
}

패딩된 위치는 그 개수뿐 아니라 내용도 타입의 일부입니다. 세 가지 종류를 추적합니다.

  • m![A # m](또는 m![A #{*} m])은 크기 m까지의 top 패딩입니다. 이 위치들은 접근할 수 있지만 임의의 값을 가집니다. 원시 DM 텐서가 이 패딩을 가집니다. #은 축약형이고, #{*}는 종류를 명시적으로 표기합니다.
  • m![A #{0} m]은 크기 m까지 0으로 채워진 패딩입니다. 이 위치들은 접근할 수 있으며 0을 가진다는 것이 알려져 있습니다. 매핑은 이 값 속성을 기록할 뿐이며, 표현식 자체가 값을 채우지는 않습니다.
  • m![A #{!} m]은 크기 m까지의 bottom 패딩입니다. 이 위치들은 접근할 수 없으며, 이 위치를 읽거나 쓰는 것은 정의되지 않은 동작입니다. 이는 컴파일러가 피해야 하는 주소를 모델링합니다.

#의 기본 종류는 top입니다. Rust 타입 수준에서는 Padding<L, SIZE, KIND>에 있는 PaddingKind const 제네릭을 통해 이를 그대로 반영합니다. Padding<L, N>은 KIND = PaddingKind::Top이고, Padding<L, N, { PaddingKind::Zero }>는 0으로 채워진 변형이며, Padding<L, N, { PaddingKind::Bottom }>은 접근할 수 없습니다.

Cell::Padding(kind)는 패딩된 범위 안에 있지만 내부 매핑 밖에 있는 위치를 나타냅니다. kind는 Top, Zero, Bottom 중 하나이며, 패딩된 범위를 넘어서는 위치는 여전히 Cell::OutOfBounds입니다. Cell 결과의 전체 정의는 위치 결과에 있습니다.

Resize

Resize는 새 크기를 넘어서는 인덱스를 잘라내어 매핑을 더 작은 논리적 크기로 제한하며, 그 범위 밖의 요소는 버립니다. 버퍼를 확장하는 패딩과 달리, Resize는 논리적 뷰를 축소합니다. 매핑 m![D = 2]는 축 D의 처음 2개 요소만 취하여 인덱스 D = 0과 D = 1을 생성합니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;

axes![C = 2, D = 3];
type E = m![C, D = 2]; // Pair<m![C], Resize<m![D], 2>>

fn test_resize() {
    assert_eq!(E::map(0), i![C: 0, D: 0]);
    assert_eq!(E::map(1), i![C: 0, D: 1]);
    assert_eq!(E::map(2), i![C: 1, D: 0]);
    assert_eq!(E::map(3), i![C: 1, D: 1]);
    assert_eq!(E::map(4), Cell::OutOfBounds);
}

test_resize();
}
impl<L, F> M for Resize<L, F>
where
    L: M,
    F: Count,
{
    const SIZE: usize = F::SIZE;

    fn to_value() -> Mapping {
        Mapping::Resize {
            inner: RBox::new(L::to_value()),
            resize: F::SIZE,
        }
    }

    fn map(i: usize) -> Cell {
        if i < F::SIZE { L::map(i) } else { Cell::OutOfBounds }
    }
}

타일링

타일링은 인덱스 뷰를 통해 구현됩니다. tile은 매핑 분할을 검증한 다음, 데이터를 복사하지 않고 메타데이터 뷰를 생성합니다. 범용 뷰 API는 TensorView::tile<I, E2, LEN>(start)입니다. I는 타일의 위치를 찾는 데 쓰이는 매핑이고, E2는 요청하는 뷰 매핑이며, LEN은 타일에 포함되는 I 셀의 개수이고, start는 I를 따라가는 논리적 시작 좌표입니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;

axes![A = 8, B = 512];

let tensor = HbmTensor::<bf16, m![1], m![A, B]>::new();
let view = tensor.view(); // HbmTensorView::<'_, bf16, m![1], m![A, B]>
let tile01 = view.tile::<m![B], 2, m![A, B = 2 # 512]>(0); // HbmTensorView::<'_, bf16, m![1], m![A, B = 2 # 512]>
let tile23 = view.tile::<m![B], 2, m![A, B = 2 # 512]>(2); // HbmTensorView::<'_, bf16, m![1], m![A, B = 2 # 512]>
}

HBM 예제는 I = m![B], LEN = 2, E2 = m![A, B = 2 # 512]를 사용합니다. B = 2 # 512 매핑은 512셀 크기의 물리적 범위 안에서 뷰에 유효한 B 위치 두 개를 부여합니다. 이 풋프린트가 없으면 분할이 원본 매핑에 대해 검증되지 않습니다. start = 0과 start = 2는 논리적 B 좌표이므로, 이 뷰들은 각각 0..2와 2..4 범위를 다룹니다. 이 값들은 시작 위치이며, 별도의 타일 번호 API가 아닙니다.

읽기 뷰는 타일 바깥의 셀을 PaddingKind::Top으로 채웁니다. 가변 뷰는 쓰기가 타일 밖으로 벗어나지 못하도록 해당 위치에 PaddingKind::Bottom을 요구합니다. HBM과 DM은 계층별 전용 타일 래퍼를 제공하는 반면, TRF, VRF, DPE는 범용 뷰를 사용합니다. HostTensor에는 뷰/타일 API가 없습니다.

스트라이드와 모듈로

스트라이드(/)와 모듈로(%)는 하나의 차원을 바깥쪽(블록 인덱스)과 안쪽(블록 내 위치)의 두 차원으로 분해합니다. 요소 512개로 이루어진 축 B를 요소 64개씩으로 된 블록 8개로 나눈다고 생각해 보십시오. 매핑 m![B / 64, B % 64]는 8 × 64 격자를 생성하며, 첫 번째 차원은 어느 블록인지를 선택하고 두 번째 차원은 해당 블록 내의 위치를 선택합니다:

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;
axes![A = 8, B = 512];
type D1 = m![B / 64]; // stride with size 8
type D2 = m![B % 64]; // modulo with size 64

type E = m![B / 64, B % 64]; // equivalent to `m![B]`

fn test_stride_modulo() {
    assert_eq!(E::map(130), i![B / 64: 2, B % 64: 2]); // block 2, position 2: B = 64*2 + 2 = 130
    assert_eq!(E::map(130), <m![B]>::map(130));               // same result as flat m![B]

    for i in 0..8 {
        assert_eq!(D1::map(i), i![B / 64: i]);
    }
    assert_eq!(D1::map(8), Cell::OutOfBounds);

    for j in 0..64 {
        assert_eq!(D2::map(j), i![B % 64: j]);
    }
    assert_eq!(D2::map(64), Cell::OutOfBounds);

    for i in 0..8 {
        for j in 0..64 {
            assert_eq!(
                E::map(64 * i + j),
                <m![B]>::map(64 * i + j),
            );
        }
    }
    assert_eq!(E::map(512), Cell::OutOfBounds);
}

test_stride_modulo();
}
impl<L, F> M for Stride<L, F>
where
    L: M,
    F: Count,
{
    const SIZE: usize = {
        assert!(L::SIZE % F::SIZE == 0, "Stride size must divide the original size");
        L::SIZE / F::SIZE
    };

    fn to_value() -> Mapping {
        Mapping::Stride {
            inner: RBox::new(L::to_value()),
            stride: F::SIZE,
        }
    }

    fn map(i: usize) -> Cell {
        if i < Self::SIZE {
            L::map(i * F::SIZE)
        } else {
            Cell::OutOfBounds
        }
    }
}

impl<L, F> M for Modulo<L, F>
where
    L: M,
    F: Count,
{
    const SIZE: usize = {
        assert!(L::SIZE % F::SIZE == 0, "Modulo size must divide the original size");
        F::SIZE
    };

    fn to_value() -> Mapping {
        Mapping::Modulo {
            inner: RBox::new(L::to_value()),
            modulo: F::SIZE,
        }
    }

    fn map(i: usize) -> Cell {
        if i < Self::SIZE {
            L::map(i % L::SIZE)
        } else {
            Cell::OutOfBounds
        }
    }
}

스트라이드와 모듈로 매핑은 표 형태로 시각화할 수 있습니다. B::SIZE = 16인 매핑 m![B / 4, B % 4]를 생각해 보십시오. 다음 표는 버퍼 인덱스가 배열되는 방식을 보여 줍니다. 각 행은 B / 4(스트라이드 축)의 특정 인덱스에 대응하고, 각 열은 B % 4(모듈로 축)의 인덱스에 대응합니다:

i![B % 4: 0]i![B % 4: 1]i![B % 4: 2]i![B % 4: 3]
i![B / 4: 0]i![B: 0]i![B: 1]i![B: 2]i![B: 3]
i![B / 4: 1]i![B: 4]i![B: 5]i![B: 6]i![B: 7]
i![B / 4: 2]i![B: 8]i![B: 9]i![B: 10]i![B: 11]
i![B / 4: 3]i![B: 12]i![B: 13]i![B: 14]i![B: 15]

모듈로는 버퍼 크기를 다루는 방식에서 리사이즈와 다음과 같이 다릅니다.

  • 리사이즈는 새 크기를 넘어서는 인덱스를 잘라내어 버퍼를 축소합니다.
  • 모듈로는 원래 버퍼 크기를 유지하면서 버퍼를 같은 크기의 블록으로 분할합니다.

복잡한 분해를 위해 이러한 연산을 중첩할 수 있습니다. 다음 예제는 B를 세 개의 차원으로 분할하며, 이때 버퍼의 비트 배치는 텐서 인덱스의 비트 배치와 다릅니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
extern crate furiosa_mapping;
use furiosa_opt_std::prelude::*;
axes![A = 8, B = 512];
// B's bits: 6 - 8,  0 - 4,          5
// Values:   0 - 7, 0 - 31,      0 - 1
type E = m![B / 64, B % 32, B / 32 % 2];

fn test_nested_stride() {
    assert_eq!(E::map(67), i![B: 97]); // 67 = 64*1 + 2*1 + 1 (i=1,j=1,k=1) → B = 64*1 + 1 + 32*1 = 97
    // Verify B=97 round-trips: 97/64=1, 97%32=1, (97/32)%2=1
    assert_eq!(97 / 64, 1);
    assert_eq!(97 % 32, 1);
    assert_eq!((97 / 32) % 2, 1);

    // buffer index: 64 * i + 2 * j + k (i = block, j = position within block, k = sub-block)
    // tensor index B: 64 * i + j + 32 * k (rearranges bit positions)
    for i in 0..8 {
        for j in 0..32 {
            for k in 0..2 {
                assert_eq!(
                    E::map(64 * i + 2 * j + k),
                    i![B: 64 * i + j + 32 * k],
                );
            }
        }
    }
    assert_eq!(E::map(512), Cell::OutOfBounds);
}

test_nested_stride();
}

이러한 비트 재배치는 뱅크 인터리빙이나 캐시 효율을 위해 주소 비트의 순서를 바꾸는 하드웨어 표현에 자연스럽게 대응됩니다. 이진수로 보면 비트 위치가 재배치되어 버퍼 001_00001_1은 B = 001_1_00001이 됩니다. 버퍼는 비트를 [8:6]_[5:1]_[0]으로 묶는 반면, B는 [8:6]_[5]_[4:0]으로 묶습니다.

타일링은 개별 요소가 아닌 블록 단위로 수행할 수 있습니다. 다음 예제는 m![B / 32]를 사용해 블록 단위로 타일링하여 서로 겹치는 타일을 만듭니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 8, B = 512];
let tensor = HbmTensor::<bf16, m![1], m![A, B]>::new();
for i in 0..15 {
    let tile = tensor.view().tile::<m![B / 32], 2, m![A, B / 32 = 2 # 16, B % 32]>(i);
}
}

B = 512일 때 차원 B / 32에는 0부터 15까지 번호가 매겨진 16개의 블록이 있습니다. 각 타일은 인덱스 i에서 시작하는 연속된 블록 2개를 가집니다. 타일 0은 블록 {0, 1}을, 타일 1은 블록 {1, 2}를 포함하며, 이런 식으로 이어져 타일 14는 블록 {14, 15}를 포함합니다. 연속된 타일끼리 블록 하나를 공유하므로 이 타일들은 서로 겹칩니다.

각 타일은 정확히 2개의 블록을 포함하므로 타일 매핑 B / 32 = 2는 블록 차원의 크기를 2로 조정합니다. 단일 블록으로 타일링하는 경우 차원의 값이 하나뿐이므로 B / 32 = 1은 항등 매핑 m![1]로 단순화됩니다.

이스케이프

복잡한 매핑은 타입 별칭을 정의하고 { ... }를 사용해 참조하십시오. 별도의 매핑 L = m![A]와 R = m![B]가 있을 때, 이를 m![{ L }, { R }]처럼 결합하면 m![A, B]와 같은 결과가 나옵니다.

#![allow(unused)]
fn main() {
extern crate furiosa_opt_std;
use furiosa_opt_std::prelude::*;
axes![A = 8, B = 512];
type L = m![A];
type R = m![B];
type E = m![{ L }, { R }]; // equivalent to `m![A, B]`

fn test_escape() {
    for i in 0..E::SIZE {
        assert_eq!(E::map(i), <m![A, B]>::map(i));
    }
}

test_escape();
}

이 이스케이프 문법은 복잡한 매핑을 이름이 붙은 재사용 가능한 구성 요소로 나눕니다.

동등한 매핑

서로 다른 생성자 조합이 같은 위치 동작을 만들어 낼 수 있습니다. 이러한 동작상의 관계는 Rust 매핑 값의 구조적 동등성과는 다릅니다. 다음 조건을 만족할 때 매핑 E1과 E2는 동등합니다.

  • E1::SIZE == E2::SIZE이고,
  • 모든 i에 대해 E1::map(i) == E2::map(i)입니다.

이 동치 관계는 반사적이고, 대칭적이며, 추이적입니다. 아래 항등식은 유용한 동작을 설명하지만, 정규화가 동치인 모든 입력에 대해 구조적으로 유일한 대표를 제공한다고 주장하지는 않습니다.

정규화에 대해 테스트된 동작은 이보다 범위가 좁습니다. 정규화를 두 번 적용해도 결과가 같은 멱등성을 가지고, 생성된 매핑 코퍼스는 테스트된 위치 동작과 패딩 동작을 유지하며, 정규화된 매핑은 매핑 표현을 거쳐 왕복 변환됩니다. 이러한 테스트는 동치성을 증명하거나, 표현의 유일성을 보장하거나, 보편적인 의미 보존을 입증하지 않습니다. 다음 항등식은 흔히 쓰이는 동치 관계를 나타냅니다.

  • 쌍의 항등원: 모든 E에 대해 E는 m![{ E }, 1]과 m![1, { E }] 모두와 동치입니다.
  • 스트라이드-모듈로 분해: 크기 E::SIZE가 n으로 나누어떨어지는 모든 E에 대해 E와 m![{ E } / n, { E } % n]은 동치입니다.
  • 쌍 사영: 모든 A와 B에 대해 m![[{ A }, { B }] / B::SIZE]는 m![A]와 동치이고, m![[{ A }, { B }] % B::SIZE]는 m![B]와 동치입니다.
  • 쌍의 결합 법칙: 모든 E1, E2, E3에 대해 m![{ E1 }, { E2 }, { E3 }], m![[{ E1 }, { E2 }], { E3 }], m![{ E1 }, [{ E2 }, { E3 }]]는 서로 동치입니다.
  • 멱등 연산: 모든 E에 대해 E는 m![{ E } / 1], m![{ E } # E::SIZE], m![{ E } = E::SIZE]와 각각 동치입니다.
  • 1에 대한 모듈로: 모든 E에 대해 m![E % 1]은 항등 매핑 m![1]과 동치입니다.

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