File size: 40,224 Bytes
13b4736 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 | // SPDX-License-Identifier: Apache-2.0
// GENERATED (round 8) from tt-metal 8b98410e730 reader_interleaved.cpp with the head split/merge folded into the
// addressing: Q/K/V tiles are read straight from the qkv matmul output. Changes are marked MOGE_QKV. Do not edit by hand; see tools_opt/mk_qkv_kernels.py.
#include <stdint.h>
#include "api/dataflow/dataflow_api.h"
#include "api/dataflow/noc.h"
#include "api/dataflow/circular_buffer.h"
#include "api/dataflow/noc_semaphore.h"
#include "api/dataflow/endpoints.h"
#include "api/core_local_mem.h"
#include "api/tensor/noc_traits.h"
#include "ttnn/operations/transformer/sdpa/device/kernels/dataflow/dataflow_common.hpp"
// MOGE_QKV: q/k/v are column slices of one [S, QKV_ROW_TILES] tile matrix (the qkv matmul output):
// head h of q starts at tile column Q_COL0 + h * DHt, of k at K_COL0 + ..., of v at V_COL0 + ...
#ifndef QKV_ROW_TILES
#error "QKV_ROW_TILES"
#endif
template <uint32_t tile_bytes, typename ReaderType>
FORCE_INLINE void read_q_subblock_skip(const ReaderType& reader, const uint32_t cb_id, uint32_t& start_tile_id,
const uint32_t sb_start_row, const uint32_t subblock_h, const uint32_t src_rows, const uint32_t src_cols,
const uint32_t dst_cols, const uint32_t barrier_threshold, const uint32_t skip) {
Noc noc;
const uint32_t sb_tiles = subblock_h * dst_cols;
CircularBuffer cb(cb_id);
cb.reserve_back(sb_tiles);
const uint32_t base_write_ptr = cb.get_write_ptr();
uint32_t barrier_count = 0;
for (uint32_t row = sb_start_row; row < sb_start_row + subblock_h; ++row) {
const uint32_t local_row = row - sb_start_row;
uint32_t write_ptr = base_write_ptr + local_row * dst_cols * tile_bytes;
if (row < src_rows) {
for (uint32_t col = 0; col < src_cols; ++col) {
noc.async_read(reader, CoreLocalMem<uint32_t>(write_ptr), tile_bytes, {.page_id = start_tile_id++}, {});
write_ptr += tile_bytes;
if (++barrier_count == barrier_threshold) { noc.async_read_barrier(); barrier_count = 0; }
}
start_tile_id += skip;
for (uint32_t col = src_cols; col < dst_cols; ++col) {
fill_tile_zeros<tile_bytes, false>(noc, cb_id, local_row * dst_cols + col);
}
} else {
for (uint32_t col = 0; col < dst_cols; ++col) {
fill_tile_zeros<tile_bytes, false>(noc, cb_id, local_row * dst_cols + col);
}
}
}
noc.async_read_barrier();
noc.write_zeros_l1_barrier();
cb.push_back(sb_tiles);
}
// Fetch a KV chunk into L1 for forwarding. No CB lifecycle — caller manages
// cb_reserve_back / cb_push_back. Single read barrier at end for lower latency.
template <uint32_t tile_bytes, bool transpose, typename ReaderType>
FORCE_INLINE void read_chunk_for_forwarding(
const ReaderType& reader,
const uint32_t cb_id,
const uint32_t dst_addr,
uint32_t start_tile_id,
const uint32_t src_rows,
const uint32_t src_cols,
const uint32_t dst_rows,
const uint32_t dst_cols,
const uint32_t skip_src_cols = 0) {
Noc noc;
const uint32_t outer_ptr_stride = transpose ? tile_bytes : dst_cols * tile_bytes;
const uint32_t inner_ptr_stride = transpose ? tile_bytes * dst_rows : tile_bytes;
uint32_t tile_id = start_tile_id;
for (uint32_t row = 0; row < src_rows; ++row) {
uint32_t write_ptr = dst_addr + row * outer_ptr_stride;
for (uint32_t col = 0; col < src_cols; ++col) {
noc.async_read(reader, CoreLocalMem<uint32_t>(write_ptr), tile_bytes, {.page_id = tile_id++}, {});
write_ptr += inner_ptr_stride;
}
tile_id += skip_src_cols;
}
for (uint32_t row = 0; row < dst_rows; ++row) {
for (uint32_t col = 0; col < dst_cols; ++col) {
if (row < src_rows && col < src_cols) {
continue;
}
uint32_t tile_idx = transpose ? col * dst_rows + row : row * dst_cols + col;
fill_zeros_async(noc, cb_id, tile_bytes, tile_idx * tile_bytes);
}
}
// On WH/BH, async_write_zeros is implemented via noc_async_read from MEM_ZEROS_BASE,
// so async_read_barrier() covers both real reads and zero-fills on the same path.
// On Quasar, iDMA zero uses a separate completion path (iDMA ack) that needs its own barrier.
noc.async_read_barrier();
#ifdef ARCH_QUASAR
noc.write_zeros_l1_barrier();
#endif
}
void kernel_main() {
Noc noc;
constexpr uint32_t B = get_compile_time_arg_val(0);
constexpr uint32_t NQH = get_compile_time_arg_val(1);
constexpr uint32_t NKH = get_compile_time_arg_val(2);
constexpr uint32_t NVH = get_compile_time_arg_val(3);
constexpr uint32_t Sqt = get_compile_time_arg_val(4);
constexpr uint32_t Skt = get_compile_time_arg_val(5);
constexpr uint32_t valid_Sqt = get_compile_time_arg_val(6);
constexpr uint32_t valid_Skt = get_compile_time_arg_val(7);
constexpr uint32_t DHt = get_compile_time_arg_val(8);
constexpr uint32_t vDHt = get_compile_time_arg_val(9);
constexpr uint32_t Sq_chunk_t = get_compile_time_arg_val(10);
constexpr uint32_t q_num_chunks = get_compile_time_arg_val(11);
constexpr uint32_t Sk_chunk_t = get_compile_time_arg_val(12);
constexpr uint32_t k_num_chunks = get_compile_time_arg_val(13);
constexpr uint32_t num_cores = get_compile_time_arg_val(14);
constexpr uint32_t is_causal = get_compile_time_arg_val(15) == 1;
constexpr uint32_t use_provided_mask = get_compile_time_arg_val(16) == 1;
constexpr uint32_t broadcast_provided_mask_batch = get_compile_time_arg_val(17) == 1;
constexpr uint32_t broadcast_provided_mask_heads = get_compile_time_arg_val(18) == 1;
constexpr uint32_t use_padded_mask = get_compile_time_arg_val(19) == 1;
constexpr uint32_t is_chunked = get_compile_time_arg_val(20) == 1;
constexpr uint32_t block_size_t = get_compile_time_arg_val(21);
constexpr uint32_t page_table_stick_size = get_compile_time_arg_val(22);
constexpr uint32_t use_attention_sink = get_compile_time_arg_val(23) == 1;
constexpr uint32_t use_mla = get_compile_time_arg_val(24) == 1;
constexpr uint32_t mla_kv_overlap = get_compile_time_arg_val(25) == 1;
constexpr uint32_t qk_subblock_h = get_compile_time_arg_val(26);
constexpr uint32_t sliding_window_size = get_compile_time_arg_val(27);
constexpr bool use_streaming_compute = get_compile_time_arg_val(28) == 1;
// Semaphore IDs for KV chain forwarding (non-causal only, but always present in compile args)
constexpr uint32_t sender_semaphore_id = get_compile_time_arg_val(29);
constexpr uint32_t receiver_semaphore_id = get_compile_time_arg_val(30);
constexpr uint32_t valid_semaphore_id = get_compile_time_arg_val(31);
constexpr bool mcast_enabled = get_compile_time_arg_val(32) == 1;
constexpr bool use_zigzag_balancing = get_compile_time_arg_val(33) == 1;
constexpr auto q_args = TensorAccessorArgs<34>();
constexpr auto k_args = TensorAccessorArgs<q_args.next_compile_time_args_offset()>();
constexpr auto v_args = TensorAccessorArgs<k_args.next_compile_time_args_offset()>();
constexpr auto mask_args = TensorAccessorArgs<v_args.next_compile_time_args_offset()>();
constexpr auto page_table_args = TensorAccessorArgs<mask_args.next_compile_time_args_offset()>();
constexpr auto attention_sink_args = TensorAccessorArgs<page_table_args.next_compile_time_args_offset()>();
constexpr auto chunk_start_idx_args = TensorAccessorArgs<attention_sink_args.next_compile_time_args_offset()>();
uint32_t argidx = 0;
const uint32_t q_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t k_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t v_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t mask_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t page_table_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t attention_sink_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t chunk_start_idx_addr = get_arg_val<uint32_t>(argidx++);
const uint32_t core_id = get_arg_val<uint32_t>(argidx++);
const uint32_t num_phases = get_arg_val<uint32_t>(argidx++);
const uint32_t chunked_q_chunk_offset_phase_1 = get_arg_val<uint32_t>(argidx++);
const uint32_t read_offset_phase_1 = get_arg_val<uint32_t>(argidx++);
uint32_t chunked_q_chunk_offset_phase_2 = 0;
uint32_t read_offset_phase_2 = 0;
if (num_phases == 2) {
chunked_q_chunk_offset_phase_2 = get_arg_val<uint32_t>(argidx++);
read_offset_phase_2 = get_arg_val<uint32_t>(argidx++);
}
uint32_t chunked_q_chunk_offset_phase_1_local = chunked_q_chunk_offset_phase_1;
uint32_t chunked_q_chunk_offset_phase_2_local = chunked_q_chunk_offset_phase_2;
// Global Q scheduling runtime args (parsed after chain metadata so the host-side ordering
// aligns for both causal and non-causal).
uint32_t global_q_start = 0;
uint32_t global_q_count = 0;
// Parse chain metadata for KV forwarding (non-causal only)
uint32_t is_chain_participant = 0;
uint32_t is_injector = 0;
uint32_t is_sink = 0;
uint32_t chain_batch = 0;
uint32_t chain_head = 0;
uint32_t prev_physical_x = 0;
uint32_t prev_physical_y = 0;
uint32_t next_physical_x = 0;
uint32_t next_physical_y = 0;
uint32_t next_core_q_chunks = 0;
uint32_t mcast_num_dests = 0;
uint32_t mcast_sender_wait = 0;
// Initialize NOC/semaphore state for chain forwarding
uint32_t sender_wait_count = 1;
if constexpr (!is_causal) {
is_chain_participant = get_arg_val<uint32_t>(argidx++);
is_injector = get_arg_val<uint32_t>(argidx++);
is_sink = get_arg_val<uint32_t>(argidx++);
chain_batch = get_arg_val<uint32_t>(argidx++);
chain_head = get_arg_val<uint32_t>(argidx++);
argidx += 2; // skip chain_q_chunk_start, chain_q_chunk_count (host-only metadata)
prev_physical_x = get_arg_val<uint32_t>(argidx++);
prev_physical_y = get_arg_val<uint32_t>(argidx++);
next_physical_x = get_arg_val<uint32_t>(argidx++);
next_physical_y = get_arg_val<uint32_t>(argidx++);
next_core_q_chunks = get_arg_val<uint32_t>(argidx++);
mcast_num_dests = get_arg_val<uint32_t>(argidx++);
mcast_sender_wait = get_arg_val<uint32_t>(argidx++);
if (is_chain_participant) {
Semaphore<>(valid_semaphore_id).set(VALID);
if constexpr (mcast_enabled) {
if (is_injector) {
sender_wait_count = mcast_sender_wait;
}
}
}
}
// Global Q scheduling runtime args sit right after chain metadata.
global_q_start = get_arg_val<uint32_t>(argidx++);
global_q_count = get_arg_val<uint32_t>(argidx++);
// When chunked: only process K/V up to (chunk_start_idx + Q_chunk_length) tokens.
// valid_Skt_bound = min(offset_tiles + valid_Sqt, valid_Skt); cap at valid_Skt for callers that pass
// different valid_Sqt (e.g. ring_distributed uses full Q length in tiles).
constexpr uint32_t q_chunk_tiles = Sq_chunk_t * DHt;
constexpr uint32_t k_chunk_tiles = Sk_chunk_t * DHt;
constexpr uint32_t v_chunk_tiles = Sk_chunk_t * vDHt;
constexpr uint32_t cb_arg_offset = chunk_start_idx_args.next_compile_time_args_offset();
constexpr uint32_t cb_q_in = get_compile_time_arg_val(cb_arg_offset + 0);
constexpr uint32_t cb_k_in = get_compile_time_arg_val(cb_arg_offset + 1);
constexpr uint32_t cb_v_in = get_compile_time_arg_val(cb_arg_offset + 2);
constexpr uint32_t cb_mask_in = get_compile_time_arg_val(cb_arg_offset + 3);
constexpr uint32_t cb_attention_sink = get_compile_time_arg_val(cb_arg_offset + 4);
constexpr uint32_t cb_id_page_table = get_compile_time_arg_val(cb_arg_offset + 5);
constexpr uint32_t cb_id_chunk_start_idx_compute = get_compile_time_arg_val(cb_arg_offset + 6);
constexpr uint32_t cb_id_chunk_start_idx_writer = get_compile_time_arg_val(cb_arg_offset + 7);
constexpr uint32_t q_tile_bytes = get_tile_size(cb_q_in);
constexpr uint32_t k_tile_bytes = get_tile_size(cb_k_in);
constexpr uint32_t v_tile_bytes = get_tile_size(cb_v_in);
constexpr uint32_t mask_tile_bytes = use_provided_mask ? get_tile_size(cb_mask_in) : 0;
constexpr uint32_t attention_sink_tile_bytes = use_attention_sink ? get_tile_size(cb_attention_sink) : 0;
constexpr uint32_t q_heads_per_k = NQH / NKH;
constexpr uint32_t q_heads_per_v = NQH / NVH;
constexpr uint32_t q_num_subblocks = Sq_chunk_t / qk_subblock_h;
constexpr bool use_q_subblock_push = (q_num_subblocks > 1);
constexpr uint32_t barrier_threshold = get_barrier_read_threshold<q_tile_bytes, num_cores>();
const auto q_reader = TensorAccessor(q_args, q_addr);
const auto k_reader = TensorAccessor(k_args, k_addr);
const auto v_reader = TensorAccessor(v_args, v_addr);
const auto mask_reader = TensorAccessor(mask_args, mask_addr);
const auto attention_sink_reader = TensorAccessor(attention_sink_args, attention_sink_addr);
const auto chunk_start_idx_reader = TensorAccessor(chunk_start_idx_args, chunk_start_idx_addr);
constexpr uint32_t skip_src_cols = QKV_ROW_TILES - DHt; // MOGE_QKV
static_assert(!use_mla && DHt == vDHt);
[[maybe_unused]] const auto q_tile_shape = TensorTileShape(B, NQH, valid_Sqt, DHt);
[[maybe_unused]] const auto k_tile_shape = TensorTileShape(B, NKH, valid_Skt, DHt);
// If we have MLA:
// - if k and v tensors are overlapped, we want to read from the k tensor, but just a portion of it, hence setting
// the v tile shape dim to DHt (and skip accordingly based on skip_src_cols)
// - if k and v tensors are not overlapped, we want to read from the v tensor, hence setting the v tile shape dim to
// vDHt Otherwise head dim of k and v are same
[[maybe_unused]] const auto v_tile_shape = TensorTileShape(B, NVH, valid_Skt, use_mla && !mla_kv_overlap ? vDHt : DHt);
const auto attention_sink_tile_shape = TensorTileShape(B, NQH, 1, 1);
volatile tt_l1_ptr uint32_t* page_table_ptr;
CircularBuffer cb_k(cb_k_in);
CircularBuffer cb_v(cb_v_in);
CircularBuffer cb_mask(cb_mask_in);
CircularBuffer cb_attn_sink(cb_attention_sink);
CircularBuffer cb_page_table(cb_id_page_table);
uint32_t chunked_q_chunk_offset = 0;
if constexpr (is_chunked) {
if (chunk_start_idx_addr != 0) {
CircularBuffer cb_chunk_compute(cb_id_chunk_start_idx_compute);
cb_chunk_compute.reserve_back(1);
uint32_t chunk_start_write_ptr = cb_chunk_compute.get_write_ptr();
noc.async_read(
chunk_start_idx_reader, CoreLocalMem<uint32_t>(chunk_start_write_ptr), 4, {.page_id = 0}, {});
noc.async_read_barrier();
uint32_t chunk_start_idx = *reinterpret_cast<volatile tt_l1_ptr uint32_t*>(chunk_start_write_ptr);
cb_chunk_compute.push_back(1);
CircularBuffer cb_chunk_writer(cb_id_chunk_start_idx_writer);
cb_chunk_writer.reserve_back(1);
uint32_t chunk_start_write_ptr_2 = cb_chunk_writer.get_write_ptr();
noc.async_read(
chunk_start_idx_reader, CoreLocalMem<uint32_t>(chunk_start_write_ptr_2), 4, {.page_id = 0}, {});
noc.async_read_barrier();
cb_chunk_writer.push_back(1);
const uint32_t q_chunk_size = Sq_chunk_t * tt::constants::TILE_HEIGHT;
chunked_q_chunk_offset_phase_1_local = chunk_start_idx / q_chunk_size;
if (num_phases == 2) {
chunked_q_chunk_offset_phase_2_local = chunked_q_chunk_offset_phase_1_local;
}
}
}
uint32_t read_offset = 0;
for (uint32_t phase = 0; phase < num_phases; ++phase) {
if (phase == 0) {
chunked_q_chunk_offset = chunked_q_chunk_offset_phase_1_local;
read_offset = read_offset_phase_1;
} else {
chunked_q_chunk_offset = chunked_q_chunk_offset_phase_2_local;
read_offset = read_offset_phase_2;
}
uint32_t valid_Skt_bound;
if (chunk_start_idx_addr != 0) {
// Flexible or ring: cap at valid_Skt so we never read past K/V extent.
valid_Skt_bound = std::min(chunked_q_chunk_offset * Sq_chunk_t + valid_Sqt, valid_Skt);
} else {
// Legacy: extend by offset so one program can serve all chunks (valid_Skt is chunk 0's).
valid_Skt_bound = valid_Skt + chunked_q_chunk_offset * Sq_chunk_t;
}
// Global Q scheduling: iterate over a linear range of B*NQH*q_num_chunks chunks.
// - per_head_q_iter resets on (nb, nq) transition: chain forwarding's
// `q_iter < next_core_q_chunks` gate expects this (chains are non-causal only).
// - is_chunked: page-table read on nb transition, single-entry CB rotated forward.
// - use_attention_sink: pushed every iter, since compute pops Sq_chunk_t per
// sdpa_inner_loop call and under global_q each iter is exactly one call.
uint32_t prev_nb = static_cast<uint32_t>(-1);
uint32_t prev_nq = static_cast<uint32_t>(-1);
uint32_t per_head_q_iter = 0;
uint32_t mask_batch_offset = 0;
for (uint32_t global_q_iter = 0; global_q_iter < global_q_count; ++global_q_iter) {
const auto decoded =
decompose_global_q_index(global_q_start + global_q_iter, q_num_chunks, NQH, use_zigzag_balancing);
if (decoded.nb != prev_nb) {
if constexpr (!broadcast_provided_mask_batch) {
if constexpr (broadcast_provided_mask_heads) {
mask_batch_offset = decoded.nb * valid_Sqt * valid_Skt;
} else {
mask_batch_offset = decoded.nb * valid_Sqt * valid_Skt * NQH;
}
}
if constexpr (is_chunked) {
if (prev_nb != static_cast<uint32_t>(-1)) {
cb_page_table.pop_front(1);
}
cb_page_table.reserve_back(1);
page_table_ptr = read_page_table_for_batch(
noc, cb_id_page_table, decoded.nb, page_table_args, page_table_addr, page_table_stick_size);
cb_page_table.push_back(1);
}
}
if (decoded.nb != prev_nb || decoded.nq != prev_nq) {
per_head_q_iter = 0;
prev_nb = decoded.nb;
prev_nq = decoded.nq;
}
if constexpr (use_attention_sink) {
constexpr uint32_t sink_tiles = use_streaming_compute ? 1 : Sq_chunk_t;
cb_attn_sink.reserve_back(sink_tiles);
uint32_t attention_sink_write_ptr = cb_attn_sink.get_write_ptr();
const uint32_t sink_tile_id = attention_sink_tile_shape.id_of(0, decoded.nq, 0, 0);
noc.async_read(
attention_sink_reader,
CoreLocalMem<uint32_t>(attention_sink_write_ptr),
attention_sink_tile_bytes,
{.page_id = sink_tile_id},
{});
noc.async_read_barrier();
if constexpr (!use_streaming_compute) {
fill_attention_sink_tiles<attention_sink_tile_bytes>(
cb_attention_sink, sink_tiles, attention_sink_write_ptr);
}
cb_attn_sink.push_back(sink_tiles);
}
const uint32_t nb = decoded.nb;
const uint32_t nq = decoded.nq;
uint32_t q_chunk = decoded.q_chunk;
const uint32_t q_iter = per_head_q_iter;
++per_head_q_iter;
/*
Determine how many rows of Q will be read. Both start and end rows are
capped by valid_Sqt, since Sq padding is independent of Sk padding.
*/
const uint32_t q_row_start_tile = std::min(q_chunk * Sq_chunk_t, valid_Sqt);
const uint32_t q_row_end_tile = std::min(q_row_start_tile + Sq_chunk_t, valid_Sqt);
const uint32_t q_row_tile_count = q_row_end_tile - q_row_start_tile;
uint32_t q_read_tile_id = (read_offset + q_row_start_tile) * QKV_ROW_TILES + Q_COL0 + nq * DHt; // MOGE_QKV
// Q read is deferred into the K loop (k_chunk==0) for subblock interleaving.
// When use_q_subblock_push is false, Q is read in full before the K loop (original behavior).
if constexpr (!use_q_subblock_push) {
read_chunk_with_padding<q_tile_bytes>(
q_reader, cb_q_in, q_read_tile_id, q_row_tile_count, DHt, Sq_chunk_t, DHt, barrier_threshold, false,
skip_src_cols); // MOGE_QKV
}
q_chunk = chunked_q_chunk_offset + q_chunk;
uint32_t q_low_idx = q_chunk * Sq_chunk_t; // This is the sequence index of the first tile of this chunk
uint32_t q_high_idx;
if constexpr (is_causal) {
// Clamp to total K-tile extent (Skt = k_num_chunks * Sk_chunk_t). Without
// this, when Q-chunk extends past K (e.g., Sq_chunk_t > k_num_chunks*Sk_chunk_t),
// the K-loop pushes more chunks than compute consumes → CB deadlock.
const uint32_t q_high_unclamped = q_low_idx + Sq_chunk_t;
q_high_idx = q_high_unclamped < Skt ? q_high_unclamped : Skt;
} else {
q_high_idx = Skt;
}
uint32_t k_loop_start = 0;
if constexpr (use_streaming_compute && sliding_window_size > 0) {
// Must match the compute kernel's K-loop bounds (see sliding_window_geometry.hpp).
using window_geom =
SlidingWindowLoopGeometry<sliding_window_size, is_causal, tt::constants::TILE_HEIGHT>;
constexpr uint32_t left_window_tiles = window_geom::left_window_tiles;
constexpr uint32_t right_window_tiles = window_geom::right_window_tiles;
if (q_low_idx > left_window_tiles) {
k_loop_start = (q_low_idx - left_window_tiles) / Sk_chunk_t;
}
if constexpr (!is_causal) {
const uint32_t window_high_unclamped = q_low_idx + Sq_chunk_t + right_window_tiles;
q_high_idx = window_high_unclamped < Skt ? window_high_unclamped : Skt;
}
}
const uint32_t k_head = nq / q_heads_per_k;
const uint32_t v_head = nq / q_heads_per_v;
// Chain forwarding conditions are loop-invariant — compute once
bool should_forward = false;
bool should_receive = false;
if constexpr (!is_causal) {
should_forward = is_chain_participant && !is_sink && (nb == chain_batch && nq == chain_head) &&
(q_iter < next_core_q_chunks);
should_receive = is_chain_participant && !is_injector && (nb == chain_batch && nq == chain_head);
}
// loop while k_low < q_high
for (uint32_t k_chunk = k_loop_start; (k_chunk * Sk_chunk_t) < q_high_idx; ++k_chunk) {
const uint32_t kv_row_start_tile = std::min(k_chunk * Sk_chunk_t, valid_Skt_bound);
const uint32_t kv_row_end_tile = std::min(kv_row_start_tile + Sk_chunk_t, valid_Skt_bound);
const uint32_t kv_row_tile_count = kv_row_end_tile - kv_row_start_tile;
const uint32_t k_start_tile_id = kv_row_start_tile * QKV_ROW_TILES + K_COL0 + k_head * DHt; // MOGE_QKV
const uint32_t v_start_tile_id = kv_row_start_tile * QKV_ROW_TILES + V_COL0 + v_head * DHt; // MOGE_QKV
// K: either read locally (injector or not participant) or receive from previous core
uint32_t cb_k_start_address = 0;
if (should_receive) {
// Receive forwarded K chunk from previous core
cb_k.reserve_back(k_chunk_tiles);
cb_k_start_address = cb_k.get_write_ptr();
Semaphore<> receiver_sem(receiver_semaphore_id);
receiver_sem.set(INVALID);
Semaphore<>(sender_semaphore_id).up(noc, prev_physical_x, prev_physical_y, 1);
receiver_sem.wait(VALID);
cb_k.push_back(k_chunk_tiles);
} else {
// Read K chunk from DRAM
if constexpr (is_chunked) {
// Use page table to read K chunk (forwarding not supported for paged mode)
const uint32_t k_chunk_start_row_num = k_chunk * Sk_chunk_t;
read_paged_chunk_with_padding<NKH, block_size_t, DHt>(
k_reader,
cb_k_in,
k_head,
k_chunk_start_row_num,
kv_row_tile_count,
DHt,
Sk_chunk_t,
DHt,
k_tile_bytes,
barrier_threshold,
page_table_ptr,
true // transpose=true for K reads
);
} else {
if (should_forward) {
cb_k.reserve_back(k_chunk_tiles);
cb_k_start_address = cb_k.get_write_ptr();
read_chunk_for_forwarding<k_tile_bytes, true>(
k_reader,
cb_k_in,
cb_k_start_address,
k_start_tile_id,
kv_row_tile_count,
DHt,
Sk_chunk_t,
DHt,
skip_src_cols); // MOGE_QKV
} else {
read_chunk_with_padding<k_tile_bytes>(
k_reader,
cb_k_in,
k_start_tile_id,
kv_row_tile_count,
DHt,
Sk_chunk_t,
DHt,
barrier_threshold,
true, // transpose=true for K reads
skip_src_cols); // MOGE_QKV
}
}
}
// Forward K chunk to next core(s): initiate async write (NOC write channel)
// For mcast: send linked data + companion semaphore back-to-back.
// The companion must be issued immediately after the linked write —
// any NOC read barrier between them deadlocks (the read barrier
// blocks while a linked write awaits its companion).
if (should_forward) {
Semaphore<> sender_sem(sender_semaphore_id);
sender_sem.wait(sender_wait_count);
sender_sem.set(0);
if constexpr (mcast_enabled) {
noc.async_write_multicast(
CoreLocalMem<uint32_t>(cb_k_start_address),
MulticastEndpoint{},
k_chunk_tiles * k_tile_bytes,
mcast_num_dests,
{},
{.noc_x_start = prev_physical_x,
.noc_y_start = prev_physical_y,
.noc_x_end = next_physical_x,
.noc_y_end = next_physical_y,
.addr = cb_k_start_address},
true /* linked: semaphore mcast follows */);
// Companion semaphore mcast: write the local valid_semaphore value into the
// remote receiver_semaphore slot (different L1 offset). Must be issued
// back-to-back after the linked data write — any noc.async_*_barrier()
// between them deadlocks the linked transaction.
Semaphore<>(valid_semaphore_id)
.relay_multicast(
noc,
Semaphore<>(receiver_semaphore_id),
prev_physical_x,
prev_physical_y,
next_physical_x,
next_physical_y,
mcast_num_dests,
/*linked=*/false);
noc.async_writes_flushed();
if (!should_receive) {
cb_k.push_back(k_chunk_tiles);
}
} else {
noc.async_write(
CoreLocalMem<uint32_t>(cb_k_start_address),
UnicastEndpoint{},
k_chunk_tiles * k_tile_bytes,
{},
{.noc_x = next_physical_x, .noc_y = next_physical_y, .addr = cb_k_start_address});
}
}
// Mask read — safe after linked write pair is complete.
// Stream the chunk one Q-tile-row (Sk_chunk_t tiles) at a time and push each row as
// soon as it lands, so the compute kernel can start applying a q_subblock's rows
// without waiting for the whole Sq_chunk_t x Sk_chunk_t block (matches the per-subblock
// wait/pop in compute_streaming.hpp).
if constexpr (use_provided_mask) {
uint32_t mask_row_start = mask_batch_offset + q_chunk * Sq_chunk_t * valid_Skt;
if constexpr (!broadcast_provided_mask_heads) {
mask_row_start += nq * valid_Sqt * valid_Skt;
}
for (uint32_t row = 0; row < Sq_chunk_t; ++row) {
const uint32_t global_q_tile = q_chunk * Sq_chunk_t + row;
const bool q_valid = !use_padded_mask || (global_q_tile < valid_Sqt);
cb_mask.reserve_back(Sk_chunk_t);
uint32_t mask_write_ptr = cb_mask.get_write_ptr();
uint32_t barrier_count = 0;
for (uint32_t col = 0; col < Sk_chunk_t; ++col) {
const uint32_t global_k_tile = k_chunk * Sk_chunk_t + col;
const bool k_valid = !use_padded_mask || (global_k_tile < valid_Skt);
if (q_valid && k_valid) {
noc.async_read(
mask_reader,
CoreLocalMem<uint32_t>(mask_write_ptr),
mask_tile_bytes,
{.page_id = mask_row_start + global_k_tile},
{});
} else {
// reserve_back reset the write ptr to this row, so index by col.
fill_neginf_tile<mask_tile_bytes>(cb_mask_in, col);
}
mask_write_ptr += mask_tile_bytes;
if (++barrier_count == barrier_threshold) {
noc.async_read_barrier();
barrier_count = 0;
}
}
noc.async_read_barrier();
cb_mask.push_back(Sk_chunk_t);
if (q_valid) {
mask_row_start += valid_Skt;
}
}
}
// Complete K forward: flush write and signal receiver(s)
// (mcast path already completed above — companion sent with linked write)
if (should_forward) {
if constexpr (!mcast_enabled) {
noc.async_writes_flushed();
if (!should_receive) {
cb_k.push_back(k_chunk_tiles);
}
Semaphore<>(valid_semaphore_id)
.relay_unicast(noc, Semaphore<>(receiver_semaphore_id), next_physical_x, next_physical_y);
}
}
// Q subblock push: K is fully forwarded, now push Q one subblock at
// a time. Compute waits for K first (waiting for K*N tiles in cb_k_in),
// then waits for Q subblocks incrementally (accumulating waits on cb_q_in).
// Each push unblocks the next QK subblock computation.
// Placed after K forward complete so no outstanding NOC writes remain
// (noc_async_read_barrier inside read_q_subblock deadlocks on BH
// when NOC writes are in-flight).
if constexpr (use_q_subblock_push) {
if (k_chunk == k_loop_start) {
for (uint32_t q_sub = 0; q_sub < q_num_subblocks; ++q_sub) {
read_q_subblock_skip<q_tile_bytes>( // MOGE_QKV
q_reader,
cb_q_in,
q_read_tile_id,
q_sub * qk_subblock_h,
qk_subblock_h,
q_row_tile_count,
DHt,
DHt,
barrier_threshold,
skip_src_cols);
}
}
}
// V: either read locally (injector or not participant) or receive from previous core
uint32_t cb_v_start_address = 0;
if (should_receive) {
// Receive forwarded V chunk from previous core
cb_v.reserve_back(v_chunk_tiles);
cb_v_start_address = cb_v.get_write_ptr();
Semaphore<> receiver_sem(receiver_semaphore_id);
receiver_sem.set(INVALID);
Semaphore<>(sender_semaphore_id).up(noc, prev_physical_x, prev_physical_y, 1);
receiver_sem.wait(VALID);
cb_v.push_back(v_chunk_tiles);
} else {
// Read V chunk from DRAM
if constexpr (is_chunked) {
// Use page table to read V chunk (forwarding not supported for paged mode)
const uint32_t kv_chunk_start_row_num = k_chunk * Sk_chunk_t;
constexpr uint32_t head_dim = (use_mla && !mla_kv_overlap) ? vDHt : DHt;
read_paged_chunk_with_padding<NVH, block_size_t, head_dim>(
v_reader,
cb_v_in,
v_head,
kv_chunk_start_row_num,
kv_row_tile_count,
vDHt,
Sk_chunk_t,
vDHt,
v_tile_bytes,
barrier_threshold,
page_table_ptr,
false,
skip_src_cols);
} else {
if (should_forward) {
cb_v.reserve_back(v_chunk_tiles);
cb_v_start_address = cb_v.get_write_ptr();
read_chunk_for_forwarding<v_tile_bytes, false>(
v_reader,
cb_v_in,
cb_v_start_address,
v_start_tile_id,
kv_row_tile_count,
vDHt,
Sk_chunk_t,
vDHt,
skip_src_cols);
} else {
read_chunk_with_padding<v_tile_bytes>(
v_reader,
cb_v_in,
v_start_tile_id,
kv_row_tile_count,
vDHt,
Sk_chunk_t,
vDHt,
barrier_threshold,
false,
skip_src_cols);
}
}
}
// Forward V chunk to next core(s) before push_back — prevents compute from
// popping the buffer while the mcast is still reading from it.
if (should_forward) {
Semaphore<> sender_sem(sender_semaphore_id);
sender_sem.wait(sender_wait_count);
sender_sem.set(0);
if constexpr (mcast_enabled) {
noc.async_write_multicast(
CoreLocalMem<uint32_t>(cb_v_start_address),
MulticastEndpoint{},
v_chunk_tiles * v_tile_bytes,
mcast_num_dests,
{},
{.noc_x_start = prev_physical_x,
.noc_y_start = prev_physical_y,
.noc_x_end = next_physical_x,
.noc_y_end = next_physical_y,
.addr = cb_v_start_address},
true /* linked: semaphore mcast follows */);
// Companion semaphore mcast — see K path above for rationale.
Semaphore<>(valid_semaphore_id)
.relay_multicast(
noc,
Semaphore<>(receiver_semaphore_id),
prev_physical_x,
prev_physical_y,
next_physical_x,
next_physical_y,
mcast_num_dests,
/*linked=*/false);
} else {
noc.async_write(
CoreLocalMem<uint32_t>(cb_v_start_address),
UnicastEndpoint{},
v_chunk_tiles * v_tile_bytes,
{},
{.noc_x = next_physical_x, .noc_y = next_physical_y, .addr = cb_v_start_address});
}
noc.async_writes_flushed();
if constexpr (!mcast_enabled) {
Semaphore<>(valid_semaphore_id)
.relay_unicast(noc, Semaphore<>(receiver_semaphore_id), next_physical_x, next_physical_y);
}
if (!should_receive) {
cb_v.push_back(v_chunk_tiles);
}
}
} // close k_chunk
} // close global_q_iter
if constexpr (is_chunked) {
if (prev_nb != static_cast<uint32_t>(-1)) {
cb_page_table.pop_front(1);
}
}
} // close phase
}
|