Device 2.0 Data Movement API Migration Guide

This guide helps developers migrate from legacy data movement APIs to the new Device 2.0 APIs located in tt_metal/hw/inc/api/.

Table of Contents

  1. Overview

  2. Header Files

  3. Key Classes

  4. Migration Patterns

  5. Complete Migration Examples

  6. Troubleshooting


Overview

The Device 2.0 APIs provide a more object-oriented, type-safe interface for data movement operations on Tenstorrent hardware. Key benefits include:

  • Type Safety: Template-based traits system prevents common errors at compile time

  • Cleaner Abstractions: Object-oriented wrappers around raw addresses and operations

  • Unified Interface: Consistent API patterns across different data sources/destinations

  • Better Encapsulation: State management within class instances

Header Files

Include the following headers based on your needs:

#include "api/dataflow/noc.h"        // Core NoC operations
#include "api/dataflow/circular_buffer.h" // CircularBuffer wrapper
#include "api/core_local_mem.h"  // Safe L1 memory pointers
#include "api/dataflow/endpoints.h"       // Unicast/Multicast/AllocatorBank endpoints
#include "api/dataflow/noc_semaphore.h"   // Semaphore synchronization
#include "api/tensor/noc_traits.h"          // TensorAccessor traits
#include "api/lock.h"                    // RAII lock utilities

Key Classes

Noc

The central class for NoC operations. Wraps a Noc index and provides methods for async reads, writes, multicasts, and barriers.

Noc noc;           // Uses default noc_index
Noc noc1(1);       // Explicitly use NoC 1

CircularBuffer

Provides circular buffer operations.

CircularBuffer cb(cb_id);
cb.reserve_back(num_pages);
cb.push_back(num_pages);
cb.wait_front(num_pages);
cb.pop_front(num_pages);

CoreLocalMem<T>

Provides a safe zero overhead way to access a given type in L1 memory.

CoreLocalMem<uint32_t> mem(address);
mem[0] = value;              // Array-style access
auto val = *mem;             // Dereference
mem++;                       // Pointer arithmetic

CoreLocalMem<T> (like Scratchpad<T> and LocalTensorAccessor<T>) does not expose its raw address; see Raw Addresses.

Semaphore

Provides a semaphore for synchronization.

Semaphore<> sem(semaphore_id);
sem.up(value);               // Local increment
sem.down(value);             // Blocking decrement
sem.wait(value);             // Wait for exact value
sem.wait_min(value);         // Wait for minimum value

Endpoints

Endpoints are used as sources or destinations for the Noc interface. Depending on which endpoint is provided, additional arguments can be passed in to src_args_t or dst_args_t of each Noc action to specify additional metadata such as offset or size.

  • UnicastEndpoint - For unicast NoC addresses

  • MulticastEndpoint - For multicast NoC addresses

  • PageView(accessor) / ShardView(accessor) - For a tensor by page id / shard id

  • BankView(accessor) - For a tensor by allocator bank: {.bank_id, .offset_bytes}; see Addressing a Tensor by Bank

  • AllocatorBank<AllocatorBankType> - Legacy raw bank addressing ({.bank_id, .addr} with an absolute bank address), for buffers not bound as a tensor. Prefer BankView.


Migration Patterns

NoC Operations

Async Read

Legacy API:

uint64_t src_noc_addr = get_noc_addr(noc_x, noc_y, src_addr);
noc_async_read(src_noc_addr, dst_l1_addr, size_bytes);
noc_async_read_barrier();

New API:

Noc noc;
UnicastEndpoint src;
CoreLocalMem<uint32_t> dst(dst_l1_addr);

noc.async_read(
    src,                                    // Source endpoint
    dst,                                    // Destination
    size_bytes,                             // Transfer size
    {.noc_x = x, .noc_y = y, .addr = addr}, // Source args
    {}                                      // Destination args
);
noc.async_read_barrier();

Async Write

Legacy API:

uint64_t dst_noc_addr = get_noc_addr(noc_x, noc_y, dst_addr);
noc_async_write(src_l1_addr, dst_noc_addr, size_bytes);
noc_async_write_barrier();

New API:

Noc noc;
CoreLocalMem<uint32_t> src(src_l1_addr);
UnicastEndpoint dst;

noc.async_write(
    src,                                    // Source
    dst,                                    // Destination
    size_bytes,                             // Transfer size
    {},                                     // Source args
    {.noc_x = x, .noc_y = y, .addr = addr}  // Destination args
);
noc.async_write_barrier();

Async Read with State (Optimized Repeated Reads)

Use set_async_read_state to pre-program the source location, page size, (and optionally the transaction id and/or virtual channel) into hardware registers once, then call async_read_with_state in a tight loop — the hardware retains state between calls.

Legacy API:

uint64_t src_noc_addr = get_noc_addr(noc_x, noc_y, base_addr);
noc_async_read_set_state(src_noc_addr, page_size);
for (uint32_t i = 0; i < num_pages; i++) {
    noc_async_read_with_state(src_base + i * page_size, dst_base + i * page_size, page_size);
}
noc_async_read_barrier();

New API (default options):

Noc noc;
UnicastEndpoint src;
CoreLocalMem<uint32_t> dst(dst_l1_addr);

noc.set_async_read_state(
    src, page_size, {.noc_x = x, .noc_y = y, .addr = base_addr}
);
for (uint32_t i = 0; i < num_pages; i++) {
    noc.async_read_with_state(
        src, dst, page_size,
        {.addr = src_base + i * page_size},
        {.addr = dst_base + i * page_size}
    );
}
noc.async_read_barrier();

New API (custom virtual channel via NocOptVals):

noc.set_async_read_state<NocOptions::CUSTOM_VC>(
    src, page_size, {.noc_x = x, .noc_y = y, .addr = base_addr},
    NocOptVals{.vc = my_vc}
);
for (uint32_t i = 0; i < num_pages; i++) {
    noc.async_read_with_state<NocOptions::CUSTOM_VC>(
        src, dst, page_size,
        {.addr = src_base + i * page_size},
        {.addr = dst_base + i * page_size},
        NocOptVals{.vc = my_vc}
    );
}
noc.async_read_barrier();

New API (transaction ID via NocOptVals):

noc.set_async_read_state<NocOptions::TXN_ID>(
    src, page_size, {.noc_x = x, .noc_y = y, .addr = base_addr},
    NocOptVals{.trid = curr_trid}
);
for (uint32_t i = 0; i < num_pages; i++) {
    noc.async_read_with_state<NocOptions::TXN_ID>(
        src, dst, page_size,
        {.addr = src_base + i * page_size},
        {.addr = dst_base + i * page_size},
        NocOptVals{.trid = curr_trid}
    );
}
noc.async_read_barrier<NocOptions::TXN_ID>({.trid = curr_trid});

Async Write with State (Optimized Repeated Writes)

Use set_async_write_state to program the destination location, page size, and virtual channel into hardware registers once, then call async_write_with_state in a tight loop.

Legacy API:

uint64_t dst_noc_addr = get_noc_addr(noc_x, noc_y, base_addr);
noc_async_write_one_packet_set_state(dst_noc_addr, size_bytes);
for (...) {
    noc_async_write_one_packet_with_state(src_l1_addr, dst_offset);
}

New API (default options):

Noc noc;
UnicastEndpoint dst;
CoreLocalMem<uint32_t> src(src_addr);

noc.set_async_write_state(
    dst, size_bytes, {.noc_x = x, .noc_y = y, .addr = base_addr}
);
for (...) {
    noc.async_write_with_state(
        src, dst, size_bytes, {.offset_bytes = src_offset}, {.addr = dst_offset}
    );
}

New API (custom virtual channel via NocOptVals):

noc.set_async_write_state<NocOptions::CUSTOM_VC>(
    dst, size_bytes, {.noc_x = x, .noc_y = y, .addr = base_addr},
    NocOptVals{.vc = my_vc}
);
for (...) {
    noc.async_write_with_state<NocOptions::CUSTOM_VC>(
        src, dst, size_bytes, {.offset_bytes = src_offset}, {.addr = dst_offset},
        NocOptVals{.vc = my_vc}
    );
}

Multicast Write

Legacy API:

uint64_t mcast_addr = get_noc_multicast_addr(x_start, y_start, x_end, y_end, l1_addr);
noc_async_write_multicast(src_l1_addr, mcast_addr, size_bytes, num_dests);

New API:

Noc noc;
CoreLocalMem<uint32_t> src(src_l1_addr);
CircularBuffer cb(cb_id);  // Or any destination with mcast traits

noc.async_write_multicast(
    src,
    cb,
    size_bytes,
    num_dests,
    {},  // Source args
    {.noc_x_start = x_start, .noc_y_start = y_start,
     .noc_x_end = x_end, .noc_y_end = y_end, .offset_bytes = 0}
);

Transaction ID Support

New API (Transaction IDs):

Noc noc;
constexpr uint8_t trid = 1;

// Write with transaction ID
noc.async_write<NocOptions::TXN_ID>(
    src, dst, size_bytes, src_args, dst_args,
    NocOptVals{.trid = trid}
);

// Barrier on specific transaction ID
noc.async_write_barrier<NocOptions::TXN_ID>({.trid = trid});

NocOptVals Struct

NocOptVals is an aggregate struct that carries the runtime values for optional NocOptions flags. It is accepted as the last argument of all stateful and trid-aware Noc APIs. Fields are only inspected when the matching flag is set in the template opts parameter.

struct NocOptVals {
    uint32_t vc   = NOC_UNICAST_WRITE_VC;  // used when NocOptions::CUSTOM_VC
    uint32_t trid = 0;                     // used when NocOptions::TXN_ID
};

Usage summary:

Scenario

Template opts

NocOptVals arg

Default vc, no trid

omit / NocOptions::DEFAULT

omit / {}

Custom vc

NocOptions::CUSTOM_VC

NocOptVals{.vc = v}

Transaction ID barrier/read/write

NocOptions::TXN_ID

NocOptVals{.trid = t}

Custom vc + trid

NocOptions::TXN_ID | NocOptions::CUSTOM_VC

NocOptVals{.vc = v, .trid = t}

Circular Buffer Operations

Basic Operations

Legacy API:

cb_reserve_back(cb_id, num_tiles);
uint32_t write_ptr = get_write_ptr(cb_id);
// ... write data ...
cb_push_back(cb_id, num_tiles);

cb_wait_front(cb_id, num_tiles);
uint32_t read_ptr = get_read_ptr(cb_id);
// ... read data ...
cb_pop_front(cb_id, num_tiles);

New API:

CircularBuffer cb(cb_id);

cb.reserve_back(num_tiles);
uint32_t write_ptr = cb.get_write_ptr();
// ... write data ...
cb.push_back(num_tiles);

cb.wait_front(num_tiles);
uint32_t read_ptr = cb.get_read_ptr();
// ... read data ...
cb.pop_front(num_tiles);

Using CircularBuffer with Noc

New API:

Noc noc;
CircularBuffer cb(cb_id);
UnicastEndpoint remote;

// Read into circular buffer
cb.reserve_back(1);
noc.async_read(
    remote,
    cb,  // CircularBuffer as destination
    tile_size,
    {.noc_x = x, .noc_y = y, .addr = addr},
    {.offset_bytes = 0}
);
noc.async_read_barrier();
cb.push_back(1);

Selecting Read/Write Pointer for Noc Async Read/Write API

Use use<>() to explicitly select which pointer to use:

using CircularBuffer;
using use;

CircularBuffer cb(cb_id);

// Use read pointer explicitly
auto cb_read_view = use<CircularBuffer::AddrSelector::READ_PTR>(cb);

// Use write pointer explicitly
auto cb_write_view = use<CircularBuffer::AddrSelector::WRITE_PTR>(cb);

These pointers can be passed into the NoC async read and write APIs as sources or destinations.


Semaphore Operations

Local Semaphore Operations

Legacy API:

volatile tt_l1_ptr uint32_t* sem_addr =
    reinterpret_cast<volatile tt_l1_ptr uint32_t*>(get_semaphore(sem_id));
noc_semaphore_set(sem_addr, 0);
noc_semaphore_wait(sem_addr, 1);

New API:

Semaphore<> sem(sem_id);
sem.set(0);
sem.wait(1);
// Or use sem.wait_min(1) for >= comparison

Remote Semaphore Operations

Legacy API:

uint64_t remote_sem_addr = get_noc_addr(noc_x, noc_y, get_semaphore(sem_id));
noc_semaphore_inc(remote_sem_addr, 1);

New API:

Noc noc;
Semaphore<> sem(sem_id);
sem.up(noc, noc_x, noc_y, 1);  // Atomic remote increment

Multicast Semaphore

Legacy API:

uint64_t mcast_addr = get_noc_multicast_addr(x0, y0, x1, y1, get_semaphore(sem_id));
noc_semaphore_set_multicast(local_sem_addr, mcast_addr, num_dests);

New API:

Noc noc;
Semaphore<> sem(sem_id);
sem.set_multicast<NocOptions::DEFAULT>(
    noc, x0, y0, x1, y1, num_dests
);

// Include source in multicast:
sem.set_multicast<NocOptions::MCAST_INCL_SRC>(
    noc, x0, y0, x1, y1, num_dests
);

Memory Access

Safe Local Memory Access

Legacy API:

volatile uint32_t* data = reinterpret_cast<volatile uint32_t*>(address);
uint32_t value = data[0];
data++;

New API:

CoreLocalMem<uint32_t> mem(address);
uint32_t value = mem[0];    // Bounds-checked in debug mode
mem++;                       // Type-safe pointer arithmetic

Struct Access

New API:

struct MyStruct {
    uint32_t field1;
    uint64_t field2;
};

CoreLocalMem<MyStruct> struct_mem(address);
struct_mem->field1 = 42;
struct_mem->field2 = 100;

Pointer Arithmetic

CoreLocalMem<uint32_t> mem(base_addr);

// Navigate through memory
auto mid = mem + offset;           // Offset by elements
auto diff = mid - mem;             // Difference in elements

// Iteration
for (auto ptr = mem; ptr < end; ++ptr) {
    *ptr = value;
}

Raw Addresses

CoreLocalMem<T>, Scratchpad<T> and LocalTensorAccessor<T> have no raw-address getter. Instead of extracting the address:

  • Element access: mem[i], *mem, mem->field. Use &mem[i] where an API needs a T* (e.g. __atomic_* builtins).

  • Pointer arithmetic: pad.local_mem() returns the region as a CoreLocalMem<T>, so pad.local_mem() + i points at element i.

  • NoC transfers: pass the handle itself as either endpoint, with {.offset_bytes = off} (bytes). See Local-to-Local Copies for copies within this core.

Local-to-Local Copies (NoC Loopback)

To copy between two buffers in this core’s L1 using the NoC, pass the local handles (CoreLocalMem<T>, Scratchpad<T>, LocalTensorAccessor<T>) directly as both endpoints. Do not build a loopback UnicastEndpoint from this core’s own coordinates ({.noc_x = my_x[...], .noc_y = my_y[...], .addr = ...}): that needs the raw address, and the handle already says it is local.

A local handle means “this core’s L1” in either position:

  • Local side (write source / read destination): the handle’s L1 address.

  • NoC side (read source / write destination): the same region addressed through the NoC from this core, i.e. a loopback.

A DataflowBuffer / CircularBuffer can be the local side of such a copy, but not the NoC side.

Legacy API (loopback read out of a scratchpad into a CB):

uint32_t pad_addr = ...;
noc_async_read(get_noc_addr(my_x[noc_index], my_y[noc_index], pad_addr + off), get_write_ptr(cb_id), size_bytes);

Avoid (Device 2.0 API, but hand-built loopback endpoint):

noc.async_read(UnicastEndpoint{}, dfb, size_bytes,
               {.noc_x = my_x[noc.get_noc_id()], .noc_y = my_y[noc.get_noc_id()], .addr = pad_addr + off}, {});

New API:

Scratchpad<uint32_t> pad(scratch::my_pad);
noc.async_read(pad, dfb, size_bytes, {.offset_bytes = off}, {});          // pad -> DFB entry
noc.async_write(dfb, pad, size_bytes, {}, {.offset_bytes = off});         // DFB entry -> pad
noc.async_read(pad, CoreLocalMem<uint32_t>(dst_addr), size_bytes, {}, {}); // pad -> any local region

Addressing a Tensor by Bank

When a kernel already works in allocator banks (for example the host hands it a bank id and an offset per transfer, as DRAM-sharded matmul and reshard do), address the tensor with BankView. Pass the bank id and the byte offset into the tensor in that bank. The bank’s location (an L1 bank’s worker core, or a DRAM bank and its per-bank offset) comes from the bank tables, and the tensor’s memory type selects L1 vs DRAM. The kernel never handles the tensor’s base address or DRAM coordinates. BankView can only be the NoC side of a transfer, and it works with the stateful set_async_read_state / async_read_with_state calls.

Legacy API:

uint32_t base = ...;  // tensor base address
uint64_t src = get_noc_addr_from_bank_id<true>(bank_id, base + offset);
noc_async_read(src, l1_dst_addr, size_bytes);

Avoid (Device 2.0 API, but a raw bank address):

AllocatorBank<AllocatorBankType::DRAM> bank;
noc.async_read(bank, dst, size_bytes, {.bank_id = bank_id, .addr = base + offset}, {});

New API:

const auto in1 = TensorAccessor(tensor::in1);
const BankView in1_bank(in1);
noc.async_read(in1_bank, dst, size_bytes, {.bank_id = bank_id, .offset_bytes = offset}, {});

Zeroing Memory

Zero a local-L1 buffer

Legacy API (manual loopback from the hardware zeros region):

constexpr uint32_t n = bytes_to_zero / MEM_ZEROS_SIZE;
uint64_t zeros = get_noc_addr(my_x[noc_index], my_y[noc_index], MEM_ZEROS_BASE);
for (uint32_t i = 0; i < n; ++i) {
    noc_async_read(zeros, write_addr + i * MEM_ZEROS_SIZE, MEM_ZEROS_SIZE);
}
noc_async_read_barrier();

New API:

Noc noc;
DataflowBuffer dfb(dfb_id);                // or CircularBuffer
dfb.reserve_back(1);
noc.async_write_zeros(
    dfb,                                   // Destination (DataflowBuffer or CircularBuffer)
    size_bytes,                            // Number of bytes to zero
    {.offset_bytes = 0}                    // Offset within the destination entry (optional; default {})
);
noc.write_zeros_l1_barrier();              // wait for completion before consuming
dfb.push_back(1);

async_write_zeros(dst, size_bytes, {.offset_bytes = off}) zeroes size_bytes starting at the destination’s base plus off (off defaults to 0). dst may be a CircularBuffer, DataflowBuffer, CoreLocalMem<T>, Scratchpad<T>, or LocalTensorAccessor<T>, and the base is:

dst

base

CircularBuffer / DataflowBuffer

get_write_ptr()

Scratchpad<T> / LocalTensorAccessor<T>

start of the local region

CoreLocalMem<T>

where the handle points

offset_bytes is in bytes for every type, including the typed handles whose operator[] indexes in elements.

Zeroing a CB/DFB entry is the common case: legacy kernels obtained the zero target’s address from a CB (e.g. get_write_ptr(cb_id)), so the migrated form zeroes that same CB/DFB entry directly. The raw L1 handles cover the case where the kernel has no CB/DFB to borrow:

Noc noc;

Scratchpad<uint32_t> pad(scratch::my_pad);
noc.async_write_zeros(pad, pad.size_in_bytes());           // whole region
noc.async_write_zeros(pad, 64, {.offset_bytes = 128});     // sub-range

LocalTensorAccessor<uint32_t> lta(tensor::my_local_tensor);
noc.async_write_zeros(lta, bytes);

CoreLocalMem<uint32_t> mem(addr);
noc.async_write_zeros(mem, bytes);

noc.write_zeros_l1_barrier();                              // one barrier covers the batch

Alignment and bounds

  • Destination bytes are undefined until the barrier returns. Do not read or write the zeroed window between async_write_zeros and write_zeros_l1_barrier().

  • Alignment: on Wormhole/Blackhole the zero is a NoC loopback read from the 32B-aligned hardware zeros region, so the destination address (base + offset_bytes) must be NOC_L1_READ_ALIGNMENT_BYTES (16) aligned. On Quasar the zero is an overlay iDMA transaction rather than a NoC transfer, so the NoC alignment constants do not apply and there is no alignment requirement.

  • Bounds: offset_bytes + size_bytes must stay inside the region — this is the caller’s responsibility and is not checked. Scratchpad asserts that offset_bytes alone is in range, but no destination type checks it against the transfer length, so an oversized size_bytes will run past the end of the region into whatever is next in L1.

Zero pages of a DRAM tensor

The DRAM overload streams zeros from a pre-zeroed L1 source (the reserved L1 zeros region is reclaimed on Quasar). The scratch argument accepts the same five types as the local-L1 overload.

For a CircularBuffer or DataflowBuffer the zeros are read from its front (read) pointer, while overload (1) writes its write pointer — hence the push_back/wait_front handoff below. Reuse a DataflowBuffer (or CircularBuffer) the kernel already has — do not allocate one solely for this if possible, since CB/DFB consumes Quasar tile counter resources. Zero one entry, push_back/wait_front it to the front before streaming, then pop_front to release:

Noc noc;
DataflowBuffer dfb(dfb_id);              // a DFB the kernel already owns

// 1. Pre-zero the DFB entry once
dfb.reserve_back(1);
noc.async_write_zeros(dfb, zero_bytes);
noc.write_zeros_l1_barrier();
dfb.push_back(1);

// 2. Stream zeros to DRAM pages from the (front of the) DFB
dfb.wait_front(1);                       // the zeroed entry is now the front
for (uint32_t p = page_start; p < page_end; ++p) {
    noc.async_write_zeros(
        dram_accessor,                   // Destination DRAM tensor accessor
        page_size,                       // Number of bytes to zero (per page)
        {.page_id = p},                  // Destination page args (page_id, optional offset_bytes)
        dfb                              // Pre-zeroed L1 source of zeros
    );
}
noc.write_zeros_dram_barrier();
dfb.pop_front(1);                        // release; the buffer is left as it was

Each call zeroes within a single page: offset_bytes + size_bytes must not exceed the tensor’s aligned page size. Zero a multi-page region by looping over page_id.

For the raw L1 handles (CoreLocalMem, Scratchpad, LocalTensorAccessor) the source and destination addresses are the same, so neither the read/write-pointer distinction nor the handoff applies — zero it, barrier, then pass the same handle. This is the only single-kernel form available on Quasar, where a DM kernel cannot self-loop a DFB:

Noc noc;
Scratchpad<uint32_t> pad(scratch::pad);
const auto out = TensorAccessor(tensor::out);

noc.async_write_zeros(pad, pad.size_in_bytes());   // 1. pre-zero the L1 source
noc.write_zeros_l1_barrier();

for (uint32_t p = page_start; p < page_end; ++p) { // 2. stream it to DRAM pages
    noc.async_write_zeros(out, page_size, {.page_id = p}, pad);
}
noc.write_zeros_dram_barrier();

Any buffer the kernel already owns can be reused (i.e. it needn’t be one dedicated to zeroing). The pre-zeroed prefix must cover at least min(page_size, NOC_MAX_BURST_SIZE) bytes; this is asserted for a DataflowBuffer (via get_entry_size()) and is the caller’s responsibility for the other scratch types. This overload pairs with write_zeros_dram_barrier().

Barriers and the Quasar command-buffer contract

async_write_zeros is asynchronous; pair each overload with its barrier:

Overload

Barrier

L1

noc.write_zeros_l1_barrier()

DRAM

noc.write_zeros_dram_barrier()

Quasar contract: the L1 zero borrows the overlay write command buffer (cmd buffer 0) and switches it to iDMA “zero mode”; it is restored to normal write mode only by write_zeros_l1_barrier(). Do not issue any other NoC write (noc.async_write / noc_async_write, which also use cmd buffer 0) on the same RISC between async_write_zeros and write_zeros_l1_barrier(): such a write would execute in zero mode and silently write zeros instead of its data. The rule is zero → barrier → reuse. NoC reads use a separate command buffer and are unaffected, so reads may be interleaved. On Wormhole/Blackhole there is no command-buffer borrow (the zero is a read loopback and the barrier is a plain read barrier).

For performance, issue one barrier after a batch of zeros rather than per-call, subject to the no-intervening-write rule above on Quasar.


Complete Migration Examples

Example 1: Tile Read Kernel

Legacy Kernel:

#include "api/dataflow/dataflow_api.h"

void kernel_main() {
    uint32_t src_addr = get_arg_val<uint32_t>(0);
    constexpr uint32_t cb_id = 0;
    constexpr auto tensor_args = TensorAccessorArgs<0>();

    uint32_t tile_size = get_tile_size(cb_id);
    const auto accessor = TensorAccessor(tensor_args, src_addr, tile_size);

    for (uint32_t tile_id = 0; tile_id < num_tiles; tile_id++) {
        cb_reserve_back(cb_id, 1);
        uint32_t l1_write_addr = get_write_ptr(cb_id);
        noc_async_read_tile(tile_id, accessor, l1_write_addr);
        noc_async_read_barrier();
        cb_push_back(cb_id, 1);
    }
}

Migrated Kernel:

#include "api/dataflow/dataflow_api.h"
#include "api/dataflow/noc.h"
#include "api/dataflow/circular_buffer.h"
#include "api/tensor/noc_traits.h"

void kernel_main() {
    uint32_t src_addr = get_arg_val<uint32_t>(0);
    constexpr uint32_t cb_id = 0;
    constexpr auto tensor_args = TensorAccessorArgs<0>();

    uint32_t tile_size = get_tile_size(cb_id);
    const auto accessor = TensorAccessor(tensor_args, src_addr, tile_size);

    Noc noc;
    CircularBuffer cb(cb_id);

    for (uint32_t tile_id = 0; tile_id < num_tiles; tile_id++) {
        cb.reserve_back(1);
        noc.async_read(
            accessor,
            cb,
            tile_size,
            {.page_id = tile_id},
            {.offset_bytes = 0}
        );
        noc.async_read_barrier();
        cb.push_back(1);
    }
}

Example 2: Core-to-Core Communication

Legacy Kernel:

void kernel_main() {
    uint32_t src_addr = get_arg_val<uint32_t>(0);
    uint32_t neighbor_x = get_arg_val<uint32_t>(1);
    uint32_t neighbor_y = get_arg_val<uint32_t>(2);
    uint32_t num_bytes = get_arg_val<uint32_t>(3);

    // Write to neighbor
    uint64_t dst_noc_addr = get_noc_addr(neighbor_x, neighbor_y, src_addr);
    noc_async_write(src_addr, dst_noc_addr, num_bytes);
    noc_async_write_barrier();

    // Read from neighbor
    noc_async_read(dst_noc_addr, src_addr, num_bytes);
    noc_async_read_barrier();
}

Migrated Kernel:

#include "api/dataflow/noc.h"
#include "api/core_local_mem.h"
#include "api/dataflow/endpoints.h"

void kernel_main() {
    uint32_t src_addr = get_arg_val<uint32_t>(0);
    uint32_t neighbor_x = get_arg_val<uint32_t>(1);
    uint32_t neighbor_y = get_arg_val<uint32_t>(2);
    uint32_t num_bytes = get_arg_val<uint32_t>(3);

    Noc noc;
    CoreLocalMem<uint32_t> mem(src_addr);
    UnicastEndpoint remote;

    // Write to neighbor
    noc.async_write(
        mem,
        remote,
        num_bytes,
        {},
        {.noc_x = neighbor_x, .noc_y = neighbor_y, .addr = src_addr}
    );
    noc.async_write_barrier();

    // Read from neighbor
    noc.async_read(
        remote,
        mem,
        num_bytes,
        {.noc_x = neighbor_x, .noc_y = neighbor_y, .addr = src_addr},
        {}
    );
    noc.async_read_barrier();
}

Troubleshooting

Common Issues

  1. Static assertion failure: “NoC transactions are not supported for this type”

    • Ensure your type has a noc_traits_t specialization

    • Include the appropriate header (e.g., api/tensor/noc_traits.h for TensorAccessor)

  2. “CircularBuffer without mcast range can only be used as L1 source”

    • CircularBuffers require explicit mcast range for multicast destinations

    • Use dst_args_mcast_type with proper coordinates

  3. “CoreLocalMem can only be used as local L1 source/dest”

    • CoreLocalMem represents a buffer in local memory. Therefore, providing it as a destination for a Noc async write or source for Noc async read is invalid.

    • Use UnicastEndpoint for remote memory access