Vengineerの妄想

人生を妄想しています。

Tenstorrent : TT-Metalium (その4)

はじめに

今回の TT-Metalium の例題は、DRAMからTensixへのデータ移動の例です。

サンプルプログラムの内容

下記のように、

  • CommandQueue
  • Program
  • CoreCoord

のおまじないをします。

int main(int argc, char **argv) {

    if (getenv("TT_METAL_SLOW_DISPATCH_MODE") != nullptr) {
        TT_THROW("Test not supported w/ slow dispatch, exiting");
    }

    bool pass = true;

    try {
        /*
        * Silicon accelerator setup
        */
        constexpr int device_id = 0;
        Device *device =
            CreateDevice(device_id);

        /*
        * Setup program and command queue to execute along with its buffers and kernels to use
        */
        CommandQueue& cq = device->command_queue();
        Program program = CreateProgram();

        constexpr CoreCoord core = {0, 0};

カーネル

下記の部分でカーネルを生成しています。プログラムは、"loopback_dram_copy.cpp" です。

        KernelHandle dram_copy_kernel_id = CreateKernel(
            program,
            "tt_metal/programming_examples/loopback/kernels/loopback_dram_copy.cpp",
            core,
            DataMovementConfig{.processor = DataMovementProcessor::RISCV_0, .noc = NOC::RISCV_0_default}
        );

ここでのポイントは、下記のところです。上記のプログラム、"loopback_dram_copy.cpp" が動くのは、RISCV_0 です。RISCV_0をDRAMからのデータをL1 Memoryし、その後、L1 MemoryからDRAMにデータを移動するために使います。

            DataMovementConfig{.processor = DataMovementProcessor::RISCV_0, .noc = NOC::RISCV_0_default}

具体的には、下図の右側にある Tensix core の中の RISC-V 1 が RISCV_0 になります。

下記がカーネルプログラムの "loopback_dram_copy.cpp" です。

#include <cstdint>

void kernel_main() {
    std::uint32_t l1_buffer_addr        = get_arg_val<uint32_t>(0);

    std::uint32_t dram_buffer_src_addr  = get_arg_val<uint32_t>(1);
    std::uint32_t dram_src_noc_x        = get_arg_val<uint32_t>(2);
    std::uint32_t dram_src_noc_y        = get_arg_val<uint32_t>(3);

    std::uint32_t dram_buffer_dst_addr  = get_arg_val<uint32_t>(4);
    std::uint32_t dram_dst_noc_x        = get_arg_val<uint32_t>(5);
    std::uint32_t dram_dst_noc_y        = get_arg_val<uint32_t>(6);

    std::uint32_t dram_buffer_size      = get_arg_val<uint32_t>(7);

    std::uint64_t dram_buffer_src_noc_addr = get_noc_addr(dram_src_noc_x, dram_src_noc_y, dram_buffer_src_addr);
    noc_async_read(dram_buffer_src_noc_addr, l1_buffer_addr, dram_buffer_size);
    noc_async_read_barrier();

    std::uint64_t dram_buffer_dst_noc_addr = get_noc_addr(dram_dst_noc_x, dram_dst_noc_y, dram_buffer_dst_addr);
    noc_async_write(l1_buffer_addr, dram_buffer_dst_noc_addr, dram_buffer_size);
    noc_async_write_barrier();
}

下記の部分で、DRAMのアドレスを dram_buffer_src_noc_addr に設定し、noc_async_read APIDRAM (dram_buffer_src_noc_addr ) => L1 Memory (l1_buffer_addr) にデータを移動します。noc_async_read_barrier API でデータ移動が完了するのを待ちます。

    std::uint64_t dram_buffer_src_noc_addr = get_noc_addr(dram_src_noc_x, dram_src_noc_y, dram_buffer_src_addr);
    noc_async_read(dram_buffer_src_noc_addr, l1_buffer_addr, dram_buffer_size);
    noc_async_read_barrier();

下記の部分が逆で、L1 Memory (l1_buffer_addr) => DRAM (dram_buffer_dst_noc_add) へのデータ移動です。noc_async_write APIDRAM へのデータ移動をし、noc_async_write_barrier API でデータ移動が終了するのを待ちます。

    std::uint64_t dram_buffer_dst_noc_addr = get_noc_addr(dram_dst_noc_x, dram_dst_noc_y, dram_buffer_dst_addr);
    noc_async_write(l1_buffer_addr, dram_buffer_dst_noc_addr, dram_buffer_size);
    noc_async_write_barrier();

バッファ

メインのプログラムに戻って、カーネルに接続するバッファのタイプを決めています。

        constexpr uint32_t single_tile_size = 2 * (32 * 32);
        constexpr uint32_t num_tiles = 50;
        constexpr uint32_t dram_buffer_size = single_tile_size * num_tiles;

        tt::tt_metal::InterleavedBufferConfig dram_config{
                    .device= device,
                    .size = dram_buffer_size,
                    .page_size = dram_buffer_size,
                    .buffer_type = tt::tt_metal::BufferType::DRAM
        };
        tt::tt_metal::InterleavedBufferConfig l1_config{
                    .device= device,
                    .size = dram_buffer_size,
                    .page_size = dram_buffer_size,
                    .buffer_type = tt::tt_metal::BufferType::L1
        };

DRAM 側が dram_config, L1 Memory 側が l1_config になります。違いは、.buffer_type で、tt:tt_metal::BufferType::DRAM と tt::tt_metal::BufferType::L1 の違いです。

この dram_conig と l1_config を使って、CreateBuffer API にて実際のバッファを生成します。

        auto l1_buffer = CreateBuffer(l1_config);

        auto input_dram_buffer = CreateBuffer(dram_config);
        const uint32_t input_dram_buffer_addr = input_dram_buffer->address();

        auto output_dram_buffer = CreateBuffer(dram_config);
        const uint32_t output_dram_buffer_addr = output_dram_buffer->address();
  • l1_buffer : L1 Memory内のバッファ
  • input_dram_buffer : DRAM から L1 Memoryに移動するためのバッファ
  • output_dram_buffer : L1 Memory から DRAM に移動するためのバッファ

です。

DRAM => L1 Memory に移動するためのデータの生成と、そのデータをDRAMに書き込むための部分が下記になります。

        std::vector<uint32_t> input_vec = create_random_vector_of_bfloat16(
            dram_buffer_size, 100, std::chrono::system_clock::now().time_since_epoch().count());
        EnqueueWriteBuffer(cq, input_dram_buffer, input_vec, false);

create_random_vector_if_bfloat16 API にて、BF16なランダムなデータを100個生成します。そのデータを input_dram_buffer に移動するのが EnqueueWriteBuffer API です。

カーネルの実行

下記の部分がカーネルの実行部分です。

        const std::vector<uint32_t> runtime_args = {
            l1_buffer->address(),
            input_dram_buffer->address(),
            static_cast<uint32_t>(input_dram_buffer->noc_coordinates().x),
            static_cast<uint32_t>(input_dram_buffer->noc_coordinates().y),
            output_dram_buffer->address(),
            static_cast<uint32_t>(output_dram_buffer->noc_coordinates().x),
            static_cast<uint32_t>(output_dram_buffer->noc_coordinates().y),
            l1_buffer->size()
        };

        SetRuntimeArgs(
            program,
            dram_copy_kernel_id,
            core,
            runtime_args
        );

        EnqueueProgram(cq, program, false);
        Finish(cq);

runtime_args がカーネルへの引数で、SetRuntimeArgs API にて、dram_copy_kernel_id の引数として紐づけます(実際は、カーネルを実行するコアに引数を渡しています)

EnqueueProgram API にて、カーネルを実行し、Finish APIにてカーネルの実行終了を待ちます。

期待値比較

EnqueueReadBuffer APIにて、カーネルによって書き込まれたDRAMからデータを result_vec に読み込み、input_vec と比較します。そして、CloseDevice API のおまじないをします。

        std::vector<uint32_t> result_vec;
        EnqueueReadBuffer(cq, output_dram_buffer, result_vec, true);

        pass &= input_vec == result_vec;

        pass &= CloseDevice(device);

おわりに

  • HostのメモリからDRAMへのデータ移動
  • DRAMからHostのメモリのデータ移動

この部分も、OpenCL と同じ感じですね。

次回は、カーネル内で処理をする例を見てみます。