diff --git a/$f b/$f new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/cli/main.cpp b/apps/cli/main.cpp index 933192da9b..26b225976c 100644 --- a/apps/cli/main.cpp +++ b/apps/cli/main.cpp @@ -1,4 +1,5 @@ #include "options.h" +#include "product/kv_options.h" #include "product/load_progress/load_progress.h" #include "product/prompt_input/prompt_input.h" @@ -282,7 +283,19 @@ int main(int argc, char** argv) { engine_options.kv_cache = cli.kv_cache; engine_options.speculative = cli.speculative; engine_options.enable_vision = cli.enable_vision; + engine_options.yarn_enabled = cli.yarn_enabled; engine_options.use_cuda_graph = cli.use_cuda_graph; + engine_options.cold_policy = cli.cold_policy; + engine_options.cold_keep_tokens = cli.cold_keep_tokens; + engine_options.cold_host_bytes = cli.cold_host_bytes; + if (cli.kv_layer_storage_explicit) { + const auto table = ninfer::product::parse_kv_layer_storage(cli.kv_layer_storage_spec); + for (std::size_t i = 0; i < table.size(); ++i) { + engine_options.kv_layer_storage[i] = table[i]; + } + engine_options.kv_layer_storage_explicit = true; + } + engine_options.graph_capture_ceiling = cli.graph_capture_ceiling; // One CLI invocation owns exactly one request, so retained cross-request context has no // consumer and must not reserve an extra Device StateImage or run terminal capture. engine_options.context_cache.enabled = false; diff --git a/apps/cli/options.cpp b/apps/cli/options.cpp index b5c5798d9a..3a35a2c3f8 100644 --- a/apps/cli/options.cpp +++ b/apps/cli/options.cpp @@ -1,5 +1,6 @@ #include "options.h" #include "product/speculative_options.h" +#include "product/kv_options.h" #include #include @@ -78,14 +79,16 @@ std::string usage_text(const char* argv0) { " (--prompt |--messages )\n" " [--max-context N] [--kv-capacity N|auto] [--prefill-chunk N] [--max-new N]\n" " [--device N]\n" - " [--kv-dtype bf16|int8|fp8] [--spec mtp|dflash --draft-tokens N]\n" + " [--kv-dtype bf16|int8|fp8] [--kv-layer-storage SPEC] [--spec mtp|dflash --draft-tokens N]\n" " [--lm-head-draft]\n" " [--temperature F] [--top-p F] [--top-k N] [--min-p F]\n" " [--presence-penalty F] [--frequency-penalty F] [--seed N] [--greedy]\n" " [--stop-token-id N]... [--stop ]... [--reasoning-stop ]...\n" " [--raw-output] [--print-token-ids] [--no-thinking] [--thinking-budget N]\n" " [--reasoning-effort low|medium|xhigh] [--vision]\n" - " [--no-cuda-graph]\n" + " [--cold-policy none|off|window|host] [--cold-keep-tokens N]\n" + " [--cold-host-bytes N[g|m|k]]\n" + " [--no-cuda-graph] [--graph-capture-ceiling N]\n" "\n" "Streams answer content to stdout and reasoning plus diagnostics to stderr.\n" "Structured message content accepts text, image/image_url, and video/video_url parts;\n" @@ -134,7 +137,10 @@ Options parse_options(int argc, char** argv) { options.device = parse_device(value(arg)); } else if (arg == "--kv-dtype") { options.kv_cache = parse_kv_cache(value(arg)); - } else if (arg == "--spec") { + } else if (arg == "--kv-layer-storage") { + options.kv_layer_storage_spec = value(arg); + options.kv_layer_storage_explicit = true; +} else if (arg == "--spec") { options.speculative.backend = product::parse_speculative_backend(value(arg)); } else if (arg == "--draft-tokens") { options.speculative.draft_tokens = parse_u32(value(arg), "draft-tokens"); @@ -152,8 +158,23 @@ Options parse_options(int argc, char** argv) { options.reasoning_effort = parse_reasoning_effort(value(arg)); } else if (arg == "--vision") { options.enable_vision = true; + } else if (arg == "--cold-policy") { + const std::string v = value(arg); + if (v == "none" || v == "off") { options.cold_policy = ColdPolicy::None; } + else if (v == "window") { options.cold_policy = ColdPolicy::Window; } + else if (v == "host") { options.cold_policy = ColdPolicy::Host; } + else { throw std::invalid_argument("invalid cold-policy: " + v); } + options.cold_keep_tokens = 128; + } else if (arg == "--cold-keep-tokens") { + options.cold_keep_tokens = parse_u32(value(arg), "cold-keep-tokens"); + } else if (arg == "--cold-host-bytes") { + options.cold_host_bytes = parse_u32(value(arg), "cold-host-bytes"); + } else if (arg == "--graph-capture-ceiling") { + options.graph_capture_ceiling = parse_u32(value(arg), "graph-capture-ceiling"); } else if (arg == "--no-cuda-graph") { options.use_cuda_graph = false; + } else if (arg == "--yarn") { + options.yarn_enabled = true; } else if (arg == "--stop-token-id") { const std::uint32_t token = parse_u32(value(arg), "stop-token-id", true); if (token > static_cast(std::numeric_limits::max())) { @@ -207,8 +228,8 @@ Options parse_options(int argc, char** argv) { throw std::invalid_argument("--prefill-chunk must be a multiple of 128"); } if (options.kv_capacity.mode == KvCapacityMode::Explicit && - options.kv_capacity.explicit_tokens < options.max_context) { - throw std::invalid_argument("--kv-capacity must be at least --max-context"); + options.kv_capacity.explicit_tokens == 0) { + throw std::invalid_argument("--kv-capacity must be positive"); } product::validate_speculative_cli_options(options.speculative); if (options.speculative.backend == SpeculativeBackend::DFlash && options.enable_vision) { diff --git a/apps/cli/options.h b/apps/cli/options.h index 3c0c2e0960..98bb104ae1 100644 --- a/apps/cli/options.h +++ b/apps/cli/options.h @@ -27,6 +27,13 @@ struct Options { SpeculativeOptions speculative; bool enable_vision = false; bool use_cuda_graph = true; + ColdPolicy cold_policy = ColdPolicy::None; + std::uint32_t cold_keep_tokens = 128; + std::uint64_t cold_host_bytes = 4ULL << 30; + bool yarn_enabled = false; + std::string kv_layer_storage_spec; + bool kv_layer_storage_explicit = false; + std::uint32_t graph_capture_ceiling = 0; bool raw_output = false; bool print_token_ids = false; diff --git a/apps/perplexity/main.cpp b/apps/perplexity/main.cpp index 28d7503a3d..9cbb3fa6af 100644 --- a/apps/perplexity/main.cpp +++ b/apps/perplexity/main.cpp @@ -2,6 +2,7 @@ #include "evaluation.h" #include "ninfer/engine.h" +#include "product/kv_options.h" #include @@ -44,6 +45,8 @@ struct Options { std::uint32_t stride = 2048; int device = 0; ninfer::KvCacheStorage kv = ninfer::KvCacheStorage::Fp8E4M3Row256; + std::array kv_layer_storage{}; + bool kv_layer_storage_explicit = false; bool quick = false; }; @@ -52,7 +55,7 @@ struct Options { "\nusage: ninfer-perplexity " "(--corpus [--quick] | --text ) " "[--context N] [--stride N] [--device N] " - "[--kv-dtype bf16|int8|fp8] [--output ]"); + "[--kv-dtype bf16|int8|fp8] [--kv-layer-storage SPEC] [--output ]"); } template @@ -104,9 +107,15 @@ Options parse_options(int argc, char** argv) { out.kv = ninfer::KvCacheStorage::Int8Group64; } else if (dtype == "fp8") { out.kv = ninfer::KvCacheStorage::Fp8E4M3Row256; + } else if (dtype == "nvfp4") { + out.kv = ninfer::KvCacheStorage::Nvfp4Group16; } else { - usage_error("--kv-dtype must be bf16, int8, or fp8"); + usage_error("--kv-dtype must be bf16, int8, fp8, or nvfp4"); } + } else if (option == "--kv-layer-storage") { + const auto table = ninfer::product::parse_kv_layer_storage(value("--kv-layer-storage")); + out.kv_layer_storage = table; + out.kv_layer_storage_explicit = true; } else if (option == "--output") { out.output = std::filesystem::path(value("--output")); } else { @@ -201,6 +210,8 @@ int run(const Options& options) { engine_options.device = options.device; engine_options.max_context = options.context; engine_options.kv_cache = options.kv; + engine_options.kv_layer_storage = options.kv_layer_storage; + engine_options.kv_layer_storage_explicit = options.kv_layer_storage_explicit; engine_options.load_progress.callback = [&](std::string_view phase, std::uint64_t done, std::uint64_t total) { const std::uint64_t bucket = diff --git a/include/ninfer/ops/bidirectional_gqa_attention.h b/include/ninfer/ops/bidirectional_gqa_attention.h new file mode 100644 index 0000000000..ed1570f65c --- /dev/null +++ b/include/ninfer/ops/bidirectional_gqa_attention.h @@ -0,0 +1,68 @@ +#pragma once + +#include "core/arena.h" +#include "core/paged_kv_cache.h" +#include "core/tensor.h" + +#include + +#include +#include + +namespace ninfer::ops { + +/** + * Host execution-resource promise for bidirectional_gqa_attention. + * + * Device context_lengths define the exact per-row mathematical contexts. This envelope bounds + * every row so a fixed launch can be captured and replayed without a host read. + */ +struct GqaContextExecutionEnvelope { + std::uint32_t min_context = 0; + std::uint32_t max_context = 0; +}; + +/** + * Op: bidirectional grouped-query attention over persistent context and one query block + * + * For D=128, Hq=32, Hkv=8, group=4, query row i, query head h, and kvh=floor(h/4): + * + * keys = context K rows [0,L) followed logically by every live query K row [0,V) + * score = scale * dot(q[:,h,i], key[:,kvh,j]) + * prob = softmax over the complete logical key set + * ideal[:,h,i] = sum_j prob[j] * value[:,kvh,j] + * + * q/out are contiguous BF16 [128,32,W,B]. query_k/query_v are contiguous BF16 [128,8,W,B]. + * context_lengths, valid_columns, and table_rows are contiguous device I32 [B]. Row b has + * V=valid_columns[b] live query columns and reads logical context [0,context_lengths[b]) through + * table row table_rows[b]. Columns i>=V are an inert physical tail and produce zero output. + * context is a read-only paged BF16 cache with head-major page planes [128,64,Nphysical,8]. scale + * is 1/sqrt(128). + * + * There is no causal triangle: every live query row attends every other live query K/V row in the + * same batch row. Context and query K/V remain separate physical segments and every input/cache + * byte is unchanged. The oracle evaluates `ideal` naively in FP64 from represented inputs. The + * BF16 out is promoted and compared directly with that result; output storage rounding belongs to + * the Op's numerical criterion, not the oracle. out is the only observable mutation and is + * completely overwritten. The current optimized implementation domain is W=1..16 on sm_120a. + * + * The caller guarantees min_context <= L <= max_context and that every logical page intersecting + * [0,L) is materialized. The execution envelope may affect finite launch selection and workspace + * capacity, never the admitted key set or numerical result. + */ +void bidirectional_gqa_attention(const Tensor& q, const Tensor& query_k, const Tensor& query_v, + const Tensor& context_lengths, const Tensor& valid_columns, + const Tensor& table_rows, float scale, + const PagedKVBatchLayerView& context, + GqaContextExecutionEnvelope envelope, WorkspaceArena& workspace, + Tensor& out, cudaStream_t stream); + +/** + * Returns the transient arena capacity required for every T in the inclusive optimized interval. + * The execution envelope is the fixed profile; invalid profiles or intervals throw. + */ +[[nodiscard]] std::size_t bidirectional_gqa_attention_workspace_capacity_bytes( + GqaContextExecutionEnvelope envelope, std::int32_t min_tokens, std::int32_t max_tokens, + std::int32_t batch_size); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/cold_i8.h b/include/ninfer/ops/cold_i8.h new file mode 100644 index 0000000000..d5fdf5a764 --- /dev/null +++ b/include/ninfer/ops/cold_i8.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +#include + +namespace ninfer::ops { + +// Fixed raw-slot size: 16 B header + 8192 B E2M1 nibbles + 1024 B E4M3 g16 +// scales (see ops/kernel/cold_i8_kernels.cuh for the composition). +inline constexpr std::int32_t kColdI8SlotBytes = 9232; + +// Raw cold-slot codec for INT8-tier KV pages (entropy-cold revision 2b). +// +// Slot layout (9232 B, fixed, no overflow): 16 B header | 8192 B packed +// E2M1 nibbles | 1024 B E4M3 group-16 scales, both in the nvfp4 page-major +// geometry produced by entropy_cold_requant_raw(Int8G64). Measured on real +// Qwen3.8 INT8 planes: NMSE 0.012-0.014 (inside the accepted NVFP4-layer +// envelope) at 1.83x per head-page vs the raw int8 plane (16896 B). +void cold_i8_slot_pack_raw(const std::uint8_t* src_codes, const std::uint8_t* src_scales, + int kv_heads, int page_count, std::uint8_t* slots, + std::int32_t* slot_valid, int slot_bytes, cudaStream_t stream); + +// Inverse: unpack slots into the INT8 tier's native planes (int8 codes + +// fp16 group-64 scales). Used by the warm-restore path. +void cold_i8_slot_restore_raw(const std::uint8_t* slots, int kv_heads, int page_count, + std::int8_t* dst_codes, void* dst_scales_fp16, + int slot_bytes, cudaStream_t stream); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/dflash2_grouped_conv.h b/include/ninfer/ops/dflash2_grouped_conv.h new file mode 100644 index 0000000000..6ddfb8b61f --- /dev/null +++ b/include/ninfer/ops/dflash2_grouped_conv.h @@ -0,0 +1,38 @@ +#pragma once + +#include "core/tensor.h" + +#include + +#include + +namespace ninfer::ops { + +/** + * Op: DFlash2 grouped convolution pass + * + * `hidden` is contiguous BF16 [H,T]: the token-major block produced by the + * draft projections. `delta` is contiguous BF16 [N,T] with N=2*taps*G and is + * the exact token-major output of the kernel projection (N fastest). `side` + * selects the pass's tap bank. `base` is a BF16 [taps,H] Weight (one side of + * the stored [2,taps,H] base kernel). `out` is contiguous BF16 [H,T] and is + * completely overwritten. + * + * For token t with in-block position p = t & (block_size-1) and channel h in + * group g = h / group_size, + * + * out[h,t] = hidden[h,t] * (base[0,h] + delta[G*(side*taps)+g,t]) + * + sum_{tap>=1, p>=tap} hidden[h,t-tap] + * * (base[tap,h] + delta[G*(side*taps+tap)+g,t]). + * + * Cross-block reads introduced by t-tap are multiplied by the zero in-block + * position mask, so rows never leak into a neighbour block. The registered + * domain is H=5120, T=1..64, taps=2, G=320, group_size=16, block_size=8, + * side in {0,1}. Inputs and base are unchanged and the Op owns no workspace or + * persistent state. Intermediate arithmetic is FP32 and out is rounded to BF16. + */ +void dflash2_grouped_conv(const Tensor& hidden, const Tensor& delta, const Weight& base, + std::int32_t block_size, std::int32_t group_size, std::int32_t taps, + std::int32_t side, Tensor& out, cudaStream_t stream); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/dflash2_selector.h b/include/ninfer/ops/dflash2_selector.h new file mode 100644 index 0000000000..beffed7cd1 --- /dev/null +++ b/include/ninfer/ops/dflash2_selector.h @@ -0,0 +1,47 @@ +#pragma once + +#include "core/tensor.h" + +#include + +#include + +namespace ninfer::ops { + +/** + * Op: DFlash2 candidate selector with a greedy path walk + * + * `unary_logits` is contiguous BF16 [V,S*B]: full-vocab base logits of S + * proposal positions for B batch rows, token-major (each column holds one + * token's V logits). `projected_hidden` is contiguous BF16 [R,S*B]: the + * candidate selector's hidden projection of the same proposal columns. + * `predecessor_codebook` and `successor_codebook` are BF16 [V,R] Weights. + * `anchors` is contiguous I32 [B] and holds the previous target token per row. + * + * `candidates` (I32), `unary` (F32), and `scores` (F32) are caller-owned + * scratch with shapes [B,S,K], [B,S,K], and [B,S,K,K] respectively; all are + * completely overwritten. `drafts` is contiguous I32 [S*B] and receives the + * selected token for every proposal position. + * + * Selection: per row/step the top-K base logits (higher value first, lower + * token id breaking ties) form the candidate set. Each candidate c at step s + * receives the pair score + * + * score(s,p,c) = unary[s,c] + * + sum_r projected[r,s] * predecessor_codebook[pred,r] + * * successor_codebook[candidate[s,c],r], + * + * where pred is the anchor token at s=0 and candidate[s-1,p] afterwards; only + * p=0 is admitted at s=0. The walk starts at previous candidate index 0 and + * greedily picks the highest-score current candidate at every step (lowest + * candidate index breaking ties). + * + * The registered domain is V=248320, R=256, S=7, B=1..8, K=16. Inputs and + * weights are unchanged and the Op owns no workspace or persistent state. + */ +void dflash2_selector(const Tensor& unary_logits, const Tensor& projected_hidden, + const Weight& predecessor_codebook, const Weight& successor_codebook, + const Tensor& anchors, Tensor& candidates, Tensor& unary, Tensor& scores, + Tensor& drafts, std::int32_t steps, std::int32_t top_k, cudaStream_t stream); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/entropy_cold_requant.h b/include/ninfer/ops/entropy_cold_requant.h new file mode 100644 index 0000000000..f134f0d167 --- /dev/null +++ b/include/ninfer/ops/entropy_cold_requant.h @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include + +namespace ninfer::ops { + +// Cold-page requantization for the entropy slot codec (revision 2). +// Requantizes one or more stored KV page planes (E2M1 g16 NVFP4 or int8 g64) +// into fresh NVFP4 planes whose codes carry one E4M3FN scale per 64 channels +// (replicated into the four g16 scale slots). The output layout matches the +// page-major nvfp4 planes entropy_nvfp4_slot_encode_raw consumes, so the slot +// codec, decode path, scale scatter, and attention producers stay unchanged; +// the g64 requant is what makes the rANS streams compressible (measured +// 2.0-2.6 bits/code vs ~4.0 for g16 storage codes on Qwen3.8 frames). +enum class EntropyColdRequantMode : int { + Nvfp4G16 = 0, + Int8G64 = 1, + Iso3VG16 = 2, +}; + +// src planes use page-major strides: codes kv_heads*8192 (nvfp4) or +// kv_heads*16384 (int8) bytes per page, scales kv_heads*1024 (nvfp4) or +// kv_heads*512 (int8). dst planes are nvfp4 page-major: codes +// [128, 64, kv_heads, page_count], scales [16, 64, kv_heads, page_count]. +void entropy_cold_requant_raw(const std::uint8_t* src_codes, const std::uint8_t* src_scales, + EntropyColdRequantMode mode, int kv_heads, int page_count, + std::uint8_t* dst_codes, std::uint8_t* dst_scales, + cudaStream_t stream); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/entropy_nvfp4.h b/include/ninfer/ops/entropy_nvfp4.h new file mode 100644 index 0000000000..309f6f8a0f --- /dev/null +++ b/include/ninfer/ops/entropy_nvfp4.h @@ -0,0 +1,55 @@ +#pragma once + +#include "core/tensor.h" + +#include + +#include + +namespace ninfer::ops { + +/** + * Op: static order-0 rANS codec for NVFP4 E2M1 code nibbles. + * + * Symbols are 4-bit code nibbles (alphabet size 16). `codes` is packed U8 + * with the low nibble at even symbol index and the high nibble at odd symbol + * index. The codec is static rANS with SCALE_BITS=12, SCALE=4096, MASK=4095, + * RANS_BYTE_L=1<<23 and a 32-bit state. Frequencies sum to 4096 and every + * frequency is non-zero. `out` capacity must be at least + * ceil(1.1 * packed code bytes) + 64 bytes per stream. On overflow encode sets + * out_size = 0 and the caller falls back to raw packed codes. + * + * The encoder processes symbols in REVERSE order. While the state is + * >= ((RANS_BYTE_L >> SCALE_BITS) << 8) * freq[s] it emits one renorm byte + * (x & 0xff) and shifts x right by 8, repeating until it is below that bound; + * it then applies + * + * x = ((x / freq[s]) << SCALE_BITS) + (x % freq[s]) + start[s]. + * + * The stream is the renorm bytes in emission order followed by the final + * state as 4 little-endian bytes at the end. The decoder initializes the state + * from the last 4 little-endian bytes and, after each inverse state update, + * consumes renorm bytes LIFO from the end of the stream with + * x = (x << 8) | previous_byte while x < RANS_BYTE_L. + * + * Shapes: + * - Single stream: codes U8 [bytes], symbol_count I32 [1], + * frequencies U16 [16], out U8 [capacity], out_size I32 [1]. + * - Batched (B streams): codes U8 [bytes_per_stream, B], + * symbol_count I32 [B], frequencies U16 [16, B], + * out U8 [capacity_per_stream, B], out_size I32 [B]. + * + * Decode mirrors the same shapes with data U8 [capacity_per_stream, B], + * data_size I32 [B], frequencies U16 [16, B], symbol_count I32 [B], and + * codes_out U8 [bytes_per_stream, B]. For batched decode, data_size[b] bytes + * are consumed starting at data + b * capacity_per_stream. + */ +void entropy_nvfp4_encode(const Tensor& codes, const Tensor& symbol_count, + Tensor& frequencies, Tensor& out, Tensor& out_size, + cudaStream_t stream); + +void entropy_nvfp4_decode(const Tensor& data, const Tensor& data_size, + const Tensor& frequencies, const Tensor& symbol_count, + Tensor& codes_out, cudaStream_t stream); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/entropy_nvfp4_slot.h b/include/ninfer/ops/entropy_nvfp4_slot.h new file mode 100644 index 0000000000..874e9bcb93 --- /dev/null +++ b/include/ninfer/ops/entropy_nvfp4_slot.h @@ -0,0 +1,99 @@ +#pragma once + +#include "core/tensor.h" + +#include + +#include + +namespace ninfer::ops { + +// Fixed slot budget covering the rANS max: 320 B header + 32 streams x 256 B +// + 1024 B scale tail. The INT8 tier packs its raw 9232 B layout into the +// same buffer, so one pool size serves both codecs. +inline constexpr std::int32_t kEntropyNvfp4SlotBytes = 9536; + +/** + * Page-slot NVFP4 E2M1 rANS codec. One slot stores the 8192 packed code bytes + * of one (physical page, kv_head, K|V plane). Each slot contains two 32-token + * half-pages; each half-page is 16 independent rANS streams of 512 code + * nibbles. Frequencies are shared by the 16 streams of one half and stored in + * the 320-byte slot header. Stream offsets in the header are relative to the + * slot start and include the 4-byte little-endian final state at each stream + * end. See ops/kernel/entropy_nvfp4_slot.cuh for the exact layout. + * + * encode: codes is the packed page plane shaped [128, 64, kv_heads, pages] and + * scales is the matching E4M3FN page plane [16, 64, kv_heads, pages] in + * paged-cache PageMajor layout. slots is U8 [slot_bytes, kv_heads, pages]; + * slot_valid is I32 [kv_heads, pages] (1 = valid compressed slot, 0 = keep + * using the uncompressed code plane). The 1024 scale bytes of a head-page are + * stored uncompressed at the end of the slot, so the whole physical page group + * can be returned to the pool while the page is cold. + * + * decode: decodes selected half-pages into packed codes. slot_ids is I32 + * [items] with flattened slot indices page * kv_heads + head; halves is I32 + * [items] (0 or 1); codes_out is U8 [4096, items] (16 contiguous 256-byte + * streams per item). + */ +void entropy_nvfp4_slot_encode(const Tensor& codes, const Tensor& scales, Tensor& slots, + Tensor& slot_valid, cudaStream_t stream); + +// Raw one-page convenience used by runtime owners that hold non-contiguous +// paged-cache slices. Pointers must address one or more (page, kv_head) planes +// with the same PageMajor strides as the Tensor form: page stride = +// kv_heads * 8192 (codes) / kv_heads * 1024 (scales) / slot_bytes (slots) / +// kv_heads (valid). page_count is the grid.y extent. +void entropy_nvfp4_slot_encode_raw(const std::uint8_t* codes, const std::uint8_t* scales, + int kv_heads, int page_count, std::uint8_t* slots, + int slot_bytes, std::int32_t* slot_valid, + cudaStream_t stream); + +// Batched raw encode for paged owners with a logical-to-physical page map. +// codes/scales address the page-0 plane base; blockIdx.y logical page p reads +// physical page page_ids[p]. slots strides logically (page * kv_heads * +// slot_bytes) and slot_valid strides valid_page_stride elements per page. +// Pass page_ids == nullptr with valid_page_stride == kv_heads for the +// contiguous Tensor-form layout. +void entropy_nvfp4_slot_encode_raw(const std::uint8_t* codes, const std::uint8_t* scales, + int kv_heads, int page_count, std::uint8_t* slots, + int slot_bytes, std::int32_t* slot_valid, + const std::int32_t* page_ids, int valid_page_stride, + cudaStream_t stream); + +void entropy_nvfp4_slot_decode_half(const Tensor& slots, const Tensor& slot_ids, + const Tensor& halves, Tensor& codes_out, + cudaStream_t stream); + +// Raw batched half-page decode for runtime owners: slot_ids/halves are host +// pointers valid for the launch, dst receives half_bytes contiguous packed +// bytes per item. +void entropy_nvfp4_slot_decode_half_raw(const std::uint8_t* slots, int slot_bytes, + const std::int32_t* slot_ids, + const std::int32_t* halves, std::uint8_t* dst, + int half_bytes, int items, cudaStream_t stream); + +// Decodes all (kv_head, half) streams of one cold-page slot base into a +// contiguous buffer: item (head * 2 + half) holds half_bytes packed bytes. +void entropy_nvfp4_slot_decode_grid_raw(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dst, cudaStream_t stream); + +// Decodes one slot base and scatters the packed rows into the native +// NVFP4 page-major code plane (row-major [64 x 128 B] per kv_head). +// dec_scratch must hold kv_heads * 8192 bytes. +void entropy_nvfp4_slot_restore_plane_raw(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dec_scratch, + std::uint8_t* dst_codes, cudaStream_t stream); + +// Scatters the uncompressed 1024-byte scale tail of every (page, kv_head) +// slot into the matching paged scale plane. slots uses the host-cold layout: +// page stride slot_page_stride, head stride slot_bytes; scale page stride is +// scale_page_stride and page_ids supplies physical pages. +void entropy_nvfp4_slot_scales_scatter_raw(const std::uint8_t* slots, int slot_bytes, + int slot_page_stride, int kv_heads, + int page_count, const std::int32_t* page_ids, + int scale_page_stride, std::uint8_t* scales, + cudaStream_t stream); + +} // namespace ninfer::ops diff --git a/include/ninfer/ops/gqa_attention.h b/include/ninfer/ops/gqa_attention.h new file mode 100644 index 0000000000..db2d0203f6 --- /dev/null +++ b/include/ninfer/ops/gqa_attention.h @@ -0,0 +1,113 @@ +#pragma once + +#include "core/paged_kv_cache.h" +#include "core/tensor.h" + +#include // cudaStream_t + +#include +#include + +namespace ninfer::ops { + +inline constexpr std::uint32_t kGqaAttentionMaximumVisibleKeys = 1'010'000; + +struct GqaExecutionEnvelope { + std::uint32_t min_visible_keys = 0; + std::uint32_t max_visible_keys = 0; +}; + +/** + * Shared numerical contract for A1/A2/A3. + * + * Public q/k/v inputs and BF16 cache values are interpreted after their BF16 storage boundary. + * INT8-G64 cache rows use one FP16 scale for each contiguous 64-element group. For BF16 source + * values x, their exact observable encoding is: + * + * a = max_i abs(FP32(x[i])) + * scale_bits = FP16_RNE(a / 127) + * s = FP32(scale_bits) + * inv = s == 0 ? 0 : FP32(1 / s) + * code[i] = s == 0 ? 0 : I8(clamp(RNE_even(FP32(x[i]) * inv), -127, 127)) + * decode[i] = FP32(code[i]) * s + * + * A1 and A2 produce identical code and scale bits. The common ideal attention oracle uses BF16 Q + * and logical cache values (BF16 values for a BF16 cache, FP32 decode above for INT8-G64), then + * evaluates score dot products, stable softmax, and value reduction in FP64. The BF16 Op output is + * promoted to FP64 for comparison with that result. + * + * The registered INT8 implementation defines Q8-G64, paired with INT8-G64 K, as its native query + * compute profile. Its profile-defined query quantization and any narrower staging do not replace + * BF16 Q in the ideal oracle. BF16-cache and INT8-cache compute profiles therefore have separate + * named numerical criteria owned by the GQA conformance test. Those envelopes apply to the + * registered geometries, tested token extents, conformance matrix, and target-representative + * activation range; they are not a universal error bound for arbitrary adversarial BF16 tensors. + * A1 and A3 are each qualified directly against the ideal oracle. A1-versus-A3 parity is only an + * additional consistency check. + */ + +/** + * Returns the transient arena capacity required for every W in the inclusive interval at one + * exact logical batch size. Head geometry, cache dtype, and execution envelope are the fixed + * implementation profile. Invalid profiles or intervals throw; a legal B=1 prompt route may + * return zero. + */ +[[nodiscard]] std::size_t +gqa_attention_workspace_capacity_bytes(std::int32_t q_heads, DType cache_dtype, + GqaExecutionEnvelope envelope, std::int32_t batch_size, + std::int32_t min_width, std::int32_t max_width); + +/** + * A1: append K/V for B independent sequences and compute causal grouped-query attention. Let + * Vb=W when valid_columns is empty and Vb=valid_columns[b] otherwise. For row b, query head h, + * kvh=floor(h/group), 0<=j + +#include +#include + +namespace ninfer::ops { + +/** + * Host execution-resource promise for swa. + * + * positions[0,b] is row b's exact device-resident committed-context frontier. This envelope bounds + * every row so a fixed launch can be captured and replayed without a host read. + */ +struct SwaContextExecutionEnvelope { + std::uint32_t min_context = 0; + std::uint32_t max_context = 0; +}; + +/** + * Op: symmetric non-causal sliding-window grouped-query attention + * + * The fixed optimized geometry is D=128, Hq=32, Hkv=8, group=4, and a registered + * window W in {2048, 4096}. q/out are contiguous BF16 [128,32,W,B], query_k/query_v are + * contiguous BF16 [128,8,W,B], positions is contiguous device I32 [W,B], valid_columns and + * lanes are contiguous device I32 [B]. Row b has V=valid_columns[b] live query columns with + * positions[i,b]=L[b]+i for i=V are an + * inert physical tail and produce zero output. + * + * The read-only cyclic context contains committed absolute positions + * [max(0,L-window),L), with absolute position p stored at physical slot p mod padded_capacity + * (the registered window). Query K/V is a separate temporary segment at positions [L,L+V). For + * every live query position p_i, admitted populated keys satisfy abs(p_j-p_i) kv_layer_storage{}; + bool kv_layer_storage_explicit = false; SpeculativeOptions speculative; std::size_t media_cache_bytes = kDefaultMediaCacheBytes; std::size_t media_live_bytes = kDefaultMediaLiveBytes; @@ -122,8 +146,18 @@ struct EngineOptions { std::uint32_t media_preprocess_threads = 0; bool enable_vision = false; bool use_cuda_graph = true; + bool yarn_enabled = false; + // On-demand graph capture: 0 (default) captures the full decode ladder at + // startup; a positive value captures only segments fully below it and the + // runtime extends the family on demand as the decode frontier grows past + // each captured segment (one capture per growth crossing). + std::uint32_t graph_capture_ceiling = 0; ContextCacheOptions context_cache; ContextCostOptions context_cost; + ColdPolicy cold_policy = ColdPolicy::None; + std::uint32_t cold_keep_tokens = 128; + // Pinned host-memory budget for ColdPolicy::Host offload. Default 4 GiB. + std::uint64_t cold_host_bytes = 4ULL << 30; LoadProgress load_progress; }; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 9471317dba..bee7e100c3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -65,11 +65,19 @@ target_link_libraries(ninfer_nvfp4_tma PRIVATE ninfer_core CUDA::cudart CUDA::cu add_library(ninfer_ops STATIC ops/launcher/add_bias.cu ops/launcher/argmax.cu + ops/launcher/bidirectional_gqa_attention.cu ops/launcher/cast.cu ops/launcher/causal_conv1d.cu + ops/launcher/dflash2_grouped_conv.cu + ops/launcher/gqa_attention_decode.cu + ops/launcher/gqa_attention_prefill.cu + ops/launcher/dflash2_selector.cu ops/launcher/embed_gather.cu ops/launcher/gdn_gating.cu ops/launcher/gelu.cu + ops/launcher/cold_i8.cu + ops/launcher/entropy_cold_requant.cu + ops/launcher/entropy_nvfp4_slot.cu ops/launcher/l2norm.cu ops/launcher/layer_norm.cu ops/launcher/mtp_pack.cu @@ -83,6 +91,9 @@ add_library(ninfer_ops STATIC ops/launcher/rope.cu ops/launcher/sampling.cu ops/launcher/scalar.cu + ops/launcher/swa.cu + ops/kernel/gqa_isoquant_rot.cu + ops/kernel/gqa_isoquant_row_scale.cu ops/launcher/scatter.cu ops/launcher/sigmoid_gate_mul.cu ops/launcher/silu_and_mul.cu @@ -238,9 +249,16 @@ add_library(ninfer_ops STATIC ops/linear/linear.cpp ops/wrapper/add_bias.cpp ops/wrapper/argmax.cpp + ops/wrapper/bidirectional_gqa_attention.cpp ops/wrapper/cast.cpp ops/wrapper/causal_conv1d_silu.cpp + ops/wrapper/dflash2_grouped_conv.cpp + ops/wrapper/gqa_attention.cpp + ops/wrapper/dflash2_selector.cpp ops/wrapper/embedding.cpp + ops/wrapper/cold_i8.cpp + ops/wrapper/entropy_cold_requant.cpp + ops/wrapper/entropy_nvfp4_slot.cpp ops/wrapper/gdn_gating.cpp ops/wrapper/gdn_gating_proj.cpp ops/wrapper/gdn_input_proj.cpp @@ -261,6 +279,7 @@ add_library(ninfer_ops STATIC ops/wrapper/rmsnorm.cpp ops/wrapper/rope.cpp ops/wrapper/sampling.cpp + ops/wrapper/swa.cpp ops/wrapper/scalar.cpp ops/wrapper/scatter.cpp ops/wrapper/sigmoid_mul.cpp diff --git a/src/core/device.h b/src/core/device.h index b4afa8384e..2ad513a710 100644 --- a/src/core/device.h +++ b/src/core/device.h @@ -15,6 +15,8 @@ struct DeviceContext { cudaStream_t stream = nullptr; cudaStream_t transfer_stream = nullptr; cudaDeviceProp props{}; + // Static YaRN factor-4 rope extension (Text D256/R64 and DFlash D128/R128). + bool yarn_enabled = false; explicit DeviceContext(int device_id = 0); ~DeviceContext(); diff --git a/src/core/dtype.h b/src/core/dtype.h index 9d68741181..ce7134ce1e 100644 --- a/src/core/dtype.h +++ b/src/core/dtype.h @@ -14,8 +14,13 @@ enum class DType : std::uint8_t { I8 = 5, FP16 = 6, FP8_E4M3FN = 7, + // Packed E2M1 nibble plane (two codes per byte) with per-16-channel + // E4M3FN scales; see the per-layer KV storage table. + NVFP4 = 8, + ISO3 = 9, }; + std::size_t dtype_size(DType dtype); } // namespace ninfer diff --git a/src/core/paged_kv_cache.h b/src/core/paged_kv_cache.h index 9a4e079148..e281018c35 100644 --- a/src/core/paged_kv_cache.h +++ b/src/core/paged_kv_cache.h @@ -16,17 +16,48 @@ namespace ninfer { inline constexpr std::int32_t kPagedKVPageSize = 64; +// Cold-slot sentinel encoding in block tables: entries <= -2 address the +// entropy pool (slot base = -2 - entry). The pool itself is per-layer and +// owned by the decoder state; the table row only carries the slot base. +inline constexpr std::int32_t kPagedKVColdSentinelBase = -2; + +[[nodiscard]] inline bool paged_kv_is_cold(std::int32_t entry) noexcept { + return entry <= kPagedKVColdSentinelBase; +} +[[nodiscard]] inline std::int32_t paged_kv_cold_slot_base(std::int32_t entry) noexcept { + return kPagedKVColdSentinelBase - entry; +} +[[nodiscard]] inline std::int32_t paged_kv_cold_entry(std::int32_t slot_base) noexcept { + return kPagedKVColdSentinelBase - slot_base; +} + /** Non-owning, single-sequence view consumed by growing-cache Ops. */ struct PagedKVLayerView { Tensor k_pages; Tensor v_pages; Tensor k_scale_pages; Tensor v_scale_pages; + // NVFP4 tier: optional two-stage residual planes (empty when the layer + // runs without residuals) and the separate V storage format. + Tensor k_residual_pages; + Tensor k_residual_scale_pages; + Tensor v_residual_pages; + Tensor v_residual_scale_pages; Tensor block_table; + // Entropy-coded cold pool (single-sequence window view): fixed raw slots + + // validity flags. Empty when the cache has no cold pool. + Tensor cold_slots; + Tensor cold_slot_valid; + std::int32_t slot_bytes = 0; std::int32_t head_dim = 0; std::int32_t num_kv_heads = 0; + std::int32_t layer_index = 0; DType dtype = DType::BF16; std::int32_t quant_group = 0; + DType v_dtype = DType::BF16; + std::int32_t v_quant_group = 0; + std::uint32_t sliding_window_tokens = 0; + std::array layer_dtypes{}; }; /** Non-owning multi-sequence view consumed by batched growing-cache Ops. */ @@ -35,11 +66,23 @@ struct PagedKVBatchLayerView { Tensor v_pages; Tensor k_scale_pages; Tensor v_scale_pages; + Tensor k_residual_pages; + Tensor k_residual_scale_pages; + Tensor v_residual_pages; + Tensor v_residual_scale_pages; Tensor block_tables; + // Entropy-coded cold pool: fixed raw slots + validity flags per layer. + Tensor cold_slots; + Tensor cold_slot_valid; + std::int32_t slot_bytes = 0; std::int32_t head_dim = 0; std::int32_t num_kv_heads = 0; + std::int32_t layer_index = 0; DType dtype = DType::BF16; std::int32_t quant_group = 0; + DType v_dtype = DType::BF16; + std::int32_t v_quant_group = 0; + std::uint32_t sliding_window_tokens = 0; }; // A plane is storage-only. Target code assigns K/V/layer meaning to plane indices. @@ -112,6 +155,10 @@ class DeviceKVPageHandle { [[nodiscard]] bool valid() const noexcept { return owner_ != nullptr; } + // Physical page index within the owning pool (public read access for the + // cold-compression pass). + [[nodiscard]] std::int32_t index() const noexcept { return index_; } + private: friend class DeviceKVPagePool; friend class DeviceKVPageLease; @@ -351,13 +398,14 @@ class KVExecutionTablePool { [[nodiscard]] const Tensor& matrix() const noexcept { return block_tables_; } + void publish_indices(KVExecutionRowHandle row, std::uint32_t logical_begin, + std::span indices, cudaStream_t stream); private: friend class KVExecutionRowLease; [[nodiscard]] bool valid_handle(KVExecutionRowHandle handle) const noexcept; bool release_row(std::int32_t row, std::uint32_t generation) noexcept; - void publish_indices(KVExecutionRowHandle row, std::uint32_t logical_begin, - std::span indices, cudaStream_t stream); + KVExecutionTableSpec spec_; const DeviceKVPagePool* pages_ = nullptr; diff --git a/src/ops/kernel/bidirectional_gqa_attention.cuh b/src/ops/kernel/bidirectional_gqa_attention.cuh new file mode 100644 index 0000000000..65afa3bad0 --- /dev/null +++ b/src/ops/kernel/bidirectional_gqa_attention.cuh @@ -0,0 +1,719 @@ +#pragma once + +#include "ops/common/math.cuh" +#include "ops/common/mma.cuh" +#include "ops/common/warp.cuh" + +#include +#include + +#include + +namespace ninfer::ops { + +inline constexpr int kBidirectionalGqaHeadDim = 128; +inline constexpr int kBidirectionalGqaQHeads = 32; +inline constexpr int kBidirectionalGqaKVHeads = 8; +inline constexpr int kBidirectionalGqaGroup = 4; +inline constexpr int kBidirectionalGqaMaxSplit = 85; + +__device__ __forceinline__ int bidirectional_gqa_swz(int row, int col) { + return (((col >> 3) ^ (row & 7)) << 3) | (col & 7); +} + +__device__ __forceinline__ unsigned bidirectional_gqa_swz_addr(unsigned lane_base, unsigned ck, + unsigned as, unsigned r) { + return lane_base + ((ck | as) ^ r); +} + +__device__ __forceinline__ std::int64_t bidirectional_gqa_q_index(int q_head, int d, int token) { + return static_cast(d) + + static_cast(kBidirectionalGqaHeadDim) * + (static_cast(q_head) + + static_cast(kBidirectionalGqaQHeads) * token); +} + +__device__ __forceinline__ std::int64_t bidirectional_gqa_query_kv_index(int kv_head, int d, + int token) { + return static_cast(d) + + static_cast(kBidirectionalGqaHeadDim) * + (static_cast(kv_head) + + static_cast(kBidirectionalGqaKVHeads) * token); +} + +__device__ __forceinline__ std::int64_t +bidirectional_gqa_cyclic_context_index(int kv_head, int d, int position, int padded_context) { + return static_cast(d) + static_cast(kBidirectionalGqaHeadDim) * + (static_cast(position) + + static_cast(padded_context) * kv_head); +} + +template +__device__ __forceinline__ std::int64_t bidirectional_gqa_partial_index(int q_head, int d, + int token, int split) { + return static_cast(d) + + static_cast(kBidirectionalGqaHeadDim) * + (static_cast(q_head) + + static_cast(kBidirectionalGqaQHeads) * + (static_cast(token) + static_cast(Tokens) * split)); +} + +template +__device__ __forceinline__ std::int64_t bidirectional_gqa_stat_index(int q_head, int token, + int split) { + return static_cast(q_head) + + static_cast(kBidirectionalGqaQHeads) * + (static_cast(token) + static_cast(Tokens) * split); +} + +__device__ __forceinline__ void noncausal_gqa_row_to_qt(int row, int kv_head, int& q_head, + int& token) { + token = row / kBidirectionalGqaGroup; + const int q_local = row - token * kBidirectionalGqaGroup; + q_head = kv_head * kBidirectionalGqaGroup + q_local; +} + +template +__device__ __forceinline__ void +bidirectional_gqa_stage_tile(__nv_bfloat16* dst, const __nv_bfloat16* context, + const __nv_bfloat16* query, int key0, int valid_keys, bool query_tile, + int kv_head, int context_stride, int physical_page, int tid) { + constexpr int VecsPerRow = kBidirectionalGqaHeadDim / 8; + constexpr int Page = 64; + const std::int64_t paged_base = + static_cast(kBidirectionalGqaHeadDim) * + ((key0 & (Page - 1)) + Page * (physical_page + context_stride * kv_head)); + for (int chunk = tid; chunk < KeyBlock * VecsPerRow; chunk += Threads) { + const int row = chunk / VecsPerRow; + const int d = (chunk - row * VecsPerRow) * 8; + const bool live = row < valid_keys; + const int safe_row = live ? row : 0; + std::int64_t src_index; + if constexpr (CyclicSwa) { + const int context_position = (live ? key0 + row : 0) & (Window - 1); + src_index = query_tile ? bidirectional_gqa_query_kv_index(kv_head, d, safe_row) + : bidirectional_gqa_cyclic_context_index( + kv_head, d, context_position, context_stride); + } else { + src_index = query_tile + ? bidirectional_gqa_query_kv_index(kv_head, d, safe_row) + : paged_base + d + + static_cast(kBidirectionalGqaHeadDim) * safe_row; + } + const __nv_bfloat16* src = query_tile ? query + src_index : context + src_index; + __nv_bfloat16* smem = &dst[row * kBidirectionalGqaHeadDim + bidirectional_gqa_swz(row, d)]; + cp_async_zfill<16, Cache::cg>(smem, src, live ? 16 : 0); + } +} + +template +__device__ __forceinline__ void noncausal_gqa_split_partial_body( + const __nv_bfloat16* __restrict__ q, const __nv_bfloat16* __restrict__ query_k, + const __nv_bfloat16* __restrict__ query_v, const std::int32_t* __restrict__ context_state, + const std::int32_t* __restrict__ valid_columns, const std::int32_t* __restrict__ selectors, + const __nv_bfloat16* __restrict__ context_k, const __nv_bfloat16* __restrict__ context_v, + const std::int32_t* __restrict__ block_tables, int context_stride, int logical_pages, + int max_context, int split_capacity, float scale, __nv_bfloat16* __restrict__ partial_acc, + float* __restrict__ partial_m, float* __restrict__ partial_l, __nv_bfloat16* __restrict__ out) { + static_assert(Tokens >= 1 && Tokens <= 16); + static_assert(WarpsPerCta == (Tokens + 3) / 4); + static_assert(KeyBlock == 32 || KeyBlock == 64); + + constexpr int D = kBidirectionalGqaHeadDim; + constexpr int Wc = WarpsPerCta; + constexpr int Threads = Wc * 32; + constexpr int Br = Wc * 16; + constexpr int RowCount = Tokens * kBidirectionalGqaGroup; + constexpr int QKNt = KeyBlock / 8; + constexpr int QKKs = D / 16; + constexpr int PVNt = D / 8; + constexpr int PVKs = KeyBlock / 16; + constexpr int RowBytes = D * static_cast(sizeof(__nv_bfloat16)); + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + + static_assert(RowCount <= Br); + static_assert(Br <= 2 * KeyBlock); + const int kv_head = static_cast(blockIdx.x); + const int split = static_cast(blockIdx.y); + const int batch = static_cast(blockIdx.z); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + + constexpr std::int64_t QueryElements = + static_cast(D) * kBidirectionalGqaQHeads * Tokens; + constexpr std::int64_t QueryKvElements = + static_cast(D) * kBidirectionalGqaKVHeads * Tokens; + constexpr std::int64_t PartialElements = QueryElements; + constexpr std::int64_t StatElements = + static_cast(kBidirectionalGqaQHeads) * Tokens; + q += QueryElements * batch; + query_k += QueryKvElements * batch; + query_v += QueryKvElements * batch; + out += QueryElements * batch; + partial_acc += PartialElements * split_capacity * batch; + partial_m += StatElements * split_capacity * batch; + partial_l += StatElements * split_capacity * batch; + const int valid = valid_columns[batch]; + if constexpr (CyclicSwa) { + context_state += static_cast(Tokens) * batch; + const std::int64_t lane_elements = + static_cast(D) * context_stride * kBidirectionalGqaKVHeads; + context_k += lane_elements * selectors[batch]; + context_v += lane_elements * selectors[batch]; + } else { + context_state += batch; + block_tables += static_cast(logical_pages) * selectors[batch]; + } + const int length = context_state[0]; + if (kv_head >= kBidirectionalGqaKVHeads || split >= split_capacity || length < 0 || + length > max_context || valid < 1 || valid > Tokens) { + return; + } + + const int context_count = CyclicSwa ? min(length, Window - 1) : length; + const int context_start = length - context_count; + const int context_tiles = (context_count + KeyBlock - 1) / KeyBlock; + const int active_splits = context_tiles > 0 ? min(context_tiles, split_capacity) : 1; + if (split >= active_splits) { return; } + + const int tile_begin = + static_cast((static_cast(context_tiles) * split) / active_splits); + const int tile_end = + static_cast((static_cast(context_tiles) * (split + 1)) / active_splits); + const bool owns_query = split == active_splits - 1; + const int context_tile_count = tile_end - tile_begin; + const int iterations = context_tile_count + (owns_query ? 1 : 0); + + int table_group = -1; + int table_lane_page = 0; + const auto paged_page = [&](int key0) { + const int logical_page = key0 >> 6; + if constexpr (Tokens == 4) { + const int group = logical_page & ~3; + if (group != table_group) { + const int table_index = group + lane; + if (lane < 4 && table_index < logical_pages) { + table_lane_page = __ldg(block_tables + table_index); + } + table_group = group; + } + return __shfl_sync(FullMask, table_lane_page, logical_page - group); + } else { + const int physical_page = lane == 0 ? __ldg(block_tables + logical_page) : 0; + return __shfl_sync(FullMask, physical_page, 0); + } + }; + if constexpr (!CyclicSwa && Tokens == 4) { + if (context_tile_count > 0) { + const int first_logical_page = + (context_start + static_cast(tile_begin) * KeyBlock) >> 6; + const int first_group = first_logical_page & ~3; + const int table_index = first_group + lane; + if (lane < 4 && table_index < logical_pages) { + table_lane_page = __ldg(block_tables + table_index); + } + table_group = first_group; + } + } + + extern __shared__ __align__(16) __nv_bfloat16 shared[]; + __nv_bfloat16* k_s = shared; + __nv_bfloat16* v_s = shared + KeyBlock * D; + + // The two K/V buffers together hold at least Br rows. Use them once as Q staging, then retain + // all Q MMA fragments in registers for the complete split. + for (int chunk = tid; chunk < Br * (D / 8); chunk += Threads) { + const int row = chunk / (D / 8); + const int d = (chunk - row * (D / 8)) * 8; + int q_head = 0, token = 0; + noncausal_gqa_row_to_qt(row, kv_head, q_head, token); + const bool live = row < RowCount && token < valid; + const __nv_bfloat16* src = + q + bidirectional_gqa_q_index(live ? q_head : 0, d, live ? token : 0); + __nv_bfloat16* dst = &shared[row * D + bidirectional_gqa_swz(row, d)]; + cp_async_zfill<16, Cache::cg>(dst, src, live ? 16 : 0); + } + cp_commit(); + cp_wait<0>(); + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + + const int warp_row0 = warp * 16; + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + const int q_position0 = + CyclicSwa ? context_state[row0 < RowCount ? row0 / kBidirectionalGqaGroup : 0] : 0; + const int q_position1 = + CyclicSwa ? context_state[row1 < RowCount ? row1 / kBidirectionalGqaGroup : 0] : 0; + unsigned af_q[QKKs][4]; +#pragma unroll + for (int ks = 0; ks < QKKs; ++ks) { + const int row = warp_row0 + a_rowoff; + const int col = ks * 16 + a_coloff; + ldmatrix_x4(af_q[ks][0], af_q[ks][1], af_q[ks][2], af_q[ks][3], + smem_addr(&shared[row * D + bidirectional_gqa_swz(row, col)])); + } + __syncthreads(); + + float acc[PVNt][4]; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int item = 0; item < 4; ++item) { acc[n][item] = 0.0f; } + } + float m0 = -CUDART_INF_F; + float m1 = -CUDART_INF_F; + float l0 = 0.0f; + float l1 = 0.0f; + + const unsigned v_sbase = smem_addr(v_s); + const unsigned v_lane_base = v_sbase + static_cast(((lane >> 3) & 1) * 8 * RowBytes) + + static_cast(b_rin * RowBytes); + const unsigned v_as = static_cast((lane >> 4) << 4); + const unsigned v_r = static_cast(b_rin << 4); + + auto tile_metadata = [&](int iteration, bool& is_query, int& key0, int& valid_keys) { + is_query = iteration >= context_tile_count; + if (is_query) { + key0 = 0; + valid_keys = valid; + } else { + key0 = context_start + (tile_begin + iteration) * KeyBlock; + valid_keys = min(KeyBlock, length - key0); + } + }; + const auto tile_page = [&](bool is_query, int key0) { + if constexpr (CyclicSwa) { + return 0; + } else { + return is_query ? 0 : paged_page(key0); + } + }; + + bool current_is_query = false; + int current_key0 = 0; + int current_valid = 0; + tile_metadata(0, current_is_query, current_key0, current_valid); + int current_page = tile_page(current_is_query, current_key0); + bidirectional_gqa_stage_tile( + k_s, context_k, query_k, current_key0, current_valid, current_is_query, kv_head, + context_stride, current_page, tid); + cp_commit(); + + for (int iteration = 0; iteration < iterations; ++iteration) { + cp_wait<0>(); + __syncthreads(); + + bidirectional_gqa_stage_tile( + v_s, context_v, query_v, current_key0, current_valid, current_is_query, kv_head, + context_stride, current_page, tid); + cp_commit(); + + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; +#pragma unroll + for (int ks = 0; ks < QKKs; ++ks) { + unsigned bf[2]; + const int brow = nt * 8 + b_rin; + const int bcol = ks * 16 + b_koff; + ldmatrix_x2(bf[0], bf[1], + smem_addr(&k_s[brow * D + bidirectional_gqa_swz(brow, bcol)])); + mma_bf16(score[nt][0], score[nt][1], score[nt][2], score[nt][3], af_q[ks][0], + af_q[ks][1], af_q[ks][2], af_q[ks][3], bf[0], bf[1]); + } + } + + cp_wait<0>(); + __syncthreads(); + + bool next_is_query = false; + int next_key0 = 0; + int next_valid = 0; + int next_page = 0; + if (iteration + 1 < iterations) { + tile_metadata(iteration + 1, next_is_query, next_key0, next_valid); + if constexpr (CyclicSwa) { + next_page = tile_page(next_is_query, next_key0); + } else { + next_page = + !next_is_query && !current_is_query && (next_key0 >> 6) == (current_key0 >> 6) + ? current_page + : tile_page(next_is_query, next_key0); + } + bidirectional_gqa_stage_tile( + k_s, context_k, query_k, next_key0, next_valid, next_is_query, kv_head, + context_stride, next_page, tid); + cp_commit(); + } + + float block_m0 = -CUDART_INF_F; + float block_m1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const bool row0_live = row0 < RowCount && row0 / kBidirectionalGqaGroup < valid; + const bool row1_live = row1 < RowCount && row1 / kBidirectionalGqaGroup < valid; + const bool allow00 = + row0_live && col0 < current_valid && + (!CyclicSwa || current_is_query || current_key0 + col0 >= q_position0 - (Window - 1)); + const bool allow01 = + row0_live && col1 < current_valid && + (!CyclicSwa || current_is_query || current_key0 + col1 >= q_position0 - (Window - 1)); + const bool allow10 = + row1_live && col0 < current_valid && + (!CyclicSwa || current_is_query || current_key0 + col0 >= q_position1 - (Window - 1)); + const bool allow11 = + row1_live && col1 < current_valid && + (!CyclicSwa || current_is_query || current_key0 + col1 >= q_position1 - (Window - 1)); + score[nt][0] = allow00 ? score[nt][0] * scale : -CUDART_INF_F; + score[nt][1] = allow01 ? score[nt][1] * scale : -CUDART_INF_F; + score[nt][2] = allow10 ? score[nt][2] * scale : -CUDART_INF_F; + score[nt][3] = allow11 ? score[nt][3] * scale : -CUDART_INF_F; + block_m0 = fmaxf(block_m0, fmaxf(score[nt][0], score[nt][1])); + block_m1 = fmaxf(block_m1, fmaxf(score[nt][2], score[nt][3])); + } + block_m0 = warp_max<4>(block_m0, FullMask); + block_m1 = warp_max<4>(block_m1, FullMask); + + const float next_m0 = fmaxf(m0, block_m0); + const float next_m1 = fmaxf(m1, block_m1); + const float alpha0 = m0 == -CUDART_INF_F ? 0.0f : exp2_approx((m0 - next_m0) * Log2E); + const float alpha1 = m1 == -CUDART_INF_F ? 0.0f : exp2_approx((m1 - next_m1) * Log2E); + + unsigned p_frag[PVKs][4]; + float block_l0 = 0.0f; + float block_l1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const float p00 = + score[nt][0] > -CUDART_INF_F ? exp2_approx((score[nt][0] - next_m0) * Log2E) : 0.0f; + const float p01 = + score[nt][1] > -CUDART_INF_F ? exp2_approx((score[nt][1] - next_m0) * Log2E) : 0.0f; + const float p10 = + score[nt][2] > -CUDART_INF_F ? exp2_approx((score[nt][2] - next_m1) * Log2E) : 0.0f; + const float p11 = + score[nt][3] > -CUDART_INF_F ? exp2_approx((score[nt][3] - next_m1) * Log2E) : 0.0f; + block_l0 += p00 + p01; + block_l1 += p10 + p11; + const int pk = nt >> 1; + if ((nt & 1) == 0) { + p_frag[pk][0] = pack_bf16x2(p00, p01); + p_frag[pk][1] = pack_bf16x2(p10, p11); + } else { + p_frag[pk][2] = pack_bf16x2(p00, p01); + p_frag[pk][3] = pack_bf16x2(p10, p11); + } + } + block_l0 = warp_sum<4>(block_l0, FullMask); + block_l1 = warp_sum<4>(block_l1, FullMask); + + l0 = l0 * alpha0 + block_l0; + l1 = l1 * alpha1 + block_l1; + m0 = next_m0; + m1 = next_m1; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + + constexpr int PVTilePairs = (PVNt + 1) / 2; + constexpr int PVLoads = PVKs * PVTilePairs; + unsigned vf[2][4]; + ldmatrix_x4_t(vf[0][0], vf[0][1], vf[0][2], vf[0][3], + bidirectional_gqa_swz_addr(v_lane_base, 0u, v_as, v_r)); +#pragma unroll + for (int load = 0; load < PVLoads; ++load) { + const int pk = load / PVTilePairs; + const int n2 = (load % PVTilePairs) * 2; + const int cur = load & 1; + const int next = cur ^ 1; + if (load + 1 < PVLoads) { + const int next_pk = (load + 1) / PVTilePairs; + const int next_n2 = ((load + 1) % PVTilePairs) * 2; + ldmatrix_x4_t(vf[next][0], vf[next][1], vf[next][2], vf[next][3], + bidirectional_gqa_swz_addr( + v_lane_base + static_cast(next_pk * 16 * RowBytes), + static_cast(next_n2 << 4), v_as, v_r)); + } + mma_bf16(acc[n2][0], acc[n2][1], acc[n2][2], acc[n2][3], p_frag[pk][0], p_frag[pk][1], + p_frag[pk][2], p_frag[pk][3], vf[cur][0], vf[cur][1]); + if (n2 + 1 < PVNt) { + mma_bf16(acc[n2 + 1][0], acc[n2 + 1][1], acc[n2 + 1][2], acc[n2 + 1][3], + p_frag[pk][0], p_frag[pk][1], p_frag[pk][2], p_frag[pk][3], vf[cur][2], + vf[cur][3]); + } + } + + current_is_query = next_is_query; + current_key0 = next_key0; + current_valid = next_valid; + current_page = next_page; + } + + if constexpr (!DirectOutput) { + if (lid == 0) { + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < RowCount) { + int q_head = 0, token = 0; + noncausal_gqa_row_to_qt(row0, kv_head, q_head, token); + partial_m[bidirectional_gqa_stat_index(q_head, token, split)] = m0; + partial_l[bidirectional_gqa_stat_index(q_head, token, split)] = l0; + } + if (row1 < RowCount) { + int q_head = 0, token = 0; + noncausal_gqa_row_to_qt(row1, kv_head, q_head, token); + partial_m[bidirectional_gqa_stat_index(q_head, token, split)] = m1; + partial_l[bidirectional_gqa_stat_index(q_head, token, split)] = l1; + } + } + } + +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + const int d0 = n * 8 + 2 * lid; + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < RowCount) { + int q_head = 0, token = 0; + noncausal_gqa_row_to_qt(row0, kv_head, q_head, token); + if constexpr (DirectOutput) { + const float inv_l = l0 > 0.0f ? 1.0f / l0 : 0.0f; + const auto dst = bidirectional_gqa_q_index(q_head, d0, token); + store_vec(&out[dst], pack_bf16x2(acc[n][0] * inv_l, acc[n][1] * inv_l)); + } else { + const auto dst = bidirectional_gqa_partial_index(q_head, d0, token, split); + store_vec(&partial_acc[dst], pack_bf16x2(acc[n][0], acc[n][1])); + } + } + if (row1 < RowCount) { + int q_head = 0, token = 0; + noncausal_gqa_row_to_qt(row1, kv_head, q_head, token); + if constexpr (DirectOutput) { + const float inv_l = l1 > 0.0f ? 1.0f / l1 : 0.0f; + const auto dst = bidirectional_gqa_q_index(q_head, d0, token); + store_vec(&out[dst], pack_bf16x2(acc[n][2] * inv_l, acc[n][3] * inv_l)); + } else { + const auto dst = bidirectional_gqa_partial_index(q_head, d0, token, split); + store_vec(&partial_acc[dst], pack_bf16x2(acc[n][2], acc[n][3])); + } + } + } +} + +template +__launch_bounds__(WarpsPerCta * 32, 2) __global__ void bidirectional_gqa_split_partial_kernel( + const __nv_bfloat16* __restrict__ q, const __nv_bfloat16* __restrict__ query_k, + const __nv_bfloat16* __restrict__ query_v, const std::int32_t* __restrict__ context_length, + const std::int32_t* __restrict__ valid_columns, const std::int32_t* __restrict__ table_rows, + const __nv_bfloat16* __restrict__ context_k, const __nv_bfloat16* __restrict__ context_v, + const std::int32_t* __restrict__ block_tables, int physical_pages, int logical_pages, + int max_context, int split_capacity, float scale, __nv_bfloat16* __restrict__ partial_acc, + float* __restrict__ partial_m, float* __restrict__ partial_l, __nv_bfloat16* __restrict__ out) { + noncausal_gqa_split_partial_body( + q, query_k, query_v, context_length, valid_columns, table_rows, context_k, context_v, + block_tables, physical_pages, logical_pages, max_context, split_capacity, scale, + partial_acc, partial_m, partial_l, out); +} + +template +__launch_bounds__(WarpsPerCta * 32, 2) __global__ void swa_split_partial_kernel( + const __nv_bfloat16* __restrict__ q, const __nv_bfloat16* __restrict__ query_k, + const __nv_bfloat16* __restrict__ query_v, const std::int32_t* __restrict__ positions, + const std::int32_t* __restrict__ valid_columns, const std::int32_t* __restrict__ lanes, + const __nv_bfloat16* __restrict__ context_k, const __nv_bfloat16* __restrict__ context_v, + int padded_context, int max_context, int split_capacity, float scale, + __nv_bfloat16* __restrict__ partial_acc, float* __restrict__ partial_m, + float* __restrict__ partial_l, __nv_bfloat16* __restrict__ out) { + noncausal_gqa_split_partial_body( + q, query_k, query_v, positions, valid_columns, lanes, context_k, context_v, nullptr, + padded_context, 0, max_context, split_capacity, scale, partial_acc, partial_m, partial_l, + out); +} + +template +__device__ __forceinline__ void +noncausal_gqa_reduce_body(const __nv_bfloat16* __restrict__ partial_acc, + const float* __restrict__ partial_m, const float* __restrict__ partial_l, + const std::int32_t* __restrict__ context_state, + const std::int32_t* __restrict__ valid_columns, int max_context, + int split_capacity, __nv_bfloat16* __restrict__ out) { + const int q_head = static_cast(blockIdx.x); + const int token = static_cast(blockIdx.y); + const int batch = static_cast(blockIdx.z); + const int tid = static_cast(threadIdx.x); + constexpr std::int64_t QueryElements = + static_cast(kBidirectionalGqaHeadDim) * kBidirectionalGqaQHeads * Tokens; + constexpr std::int64_t StatElements = + static_cast(kBidirectionalGqaQHeads) * Tokens; + partial_acc += QueryElements * split_capacity * batch; + partial_m += StatElements * split_capacity * batch; + partial_l += StatElements * split_capacity * batch; + out += QueryElements * batch; + if constexpr (CyclicSwa) { + context_state += static_cast(Tokens) * batch; + } else { + context_state += batch; + } + const int length = context_state[0]; + if (q_head >= kBidirectionalGqaQHeads || token >= Tokens) { return; } + if (length < 0 || length > max_context || token >= valid_columns[batch]) { + if (tid < kBidirectionalGqaHeadDim) { + out[bidirectional_gqa_q_index(q_head, tid, token)] = __float2bfloat16(0.0f); + } + return; + } + + const int context_count = CyclicSwa ? min(length, Window - 1) : length; + const int context_tiles = (context_count + KeyBlock - 1) / KeyBlock; + const int active_splits = context_tiles > 0 ? min(context_tiles, split_capacity) : 1; + __shared__ float reduce[128]; + + float local_m = -CUDART_INF_F; + for (int split = tid; split < active_splits; split += blockDim.x) { + local_m = + fmaxf(local_m, partial_m[bidirectional_gqa_stat_index(q_head, token, split)]); + } + reduce[tid] = local_m; + __syncthreads(); + for (int stride = 64; stride > 0; stride >>= 1) { + if (tid < stride) { reduce[tid] = fmaxf(reduce[tid], reduce[tid + stride]); } + __syncthreads(); + } + const float global_m = reduce[0]; + __syncthreads(); + + float local_l = 0.0f; + for (int split = tid; split < active_splits; split += blockDim.x) { + const auto idx = bidirectional_gqa_stat_index(q_head, token, split); + local_l += partial_l[idx] * expf(partial_m[idx] - global_m); + } + reduce[tid] = local_l; + __syncthreads(); + for (int stride = 64; stride > 0; stride >>= 1) { + if (tid < stride) { reduce[tid] += reduce[tid + stride]; } + __syncthreads(); + } + const float global_l = reduce[0]; + + if (tid < kBidirectionalGqaHeadDim) { + float numerator = 0.0f; + for (int split = 0; split < active_splits; ++split) { + const auto stat = bidirectional_gqa_stat_index(q_head, token, split); + const float weight = expf(partial_m[stat] - global_m); + numerator += __bfloat162float(partial_acc[bidirectional_gqa_partial_index( + q_head, tid, token, split)]) * + weight; + } + const float value = global_l > 0.0f ? numerator / global_l : 0.0f; + out[bidirectional_gqa_q_index(q_head, tid, token)] = __float2bfloat16(value); + } +} + +template +__launch_bounds__(128, 2) __global__ + void bidirectional_gqa_reduce_kernel(const __nv_bfloat16* __restrict__ partial_acc, + const float* __restrict__ partial_m, + const float* __restrict__ partial_l, + const std::int32_t* __restrict__ context_length, + const std::int32_t* __restrict__ valid_columns, + int max_context, int split_capacity, + __nv_bfloat16* __restrict__ out) { + noncausal_gqa_reduce_body(partial_acc, partial_m, partial_l, + context_length, valid_columns, max_context, + split_capacity, out); +} + +template +__launch_bounds__(WarpsPerBlock * 32, 2) __global__ + void swa_reduce_kernel(const __nv_bfloat16* __restrict__ partial_acc, + const float* __restrict__ partial_m, const float* __restrict__ partial_l, + const std::int32_t* __restrict__ positions, + const std::int32_t* __restrict__ valid_columns, int max_context, + int split_capacity, __nv_bfloat16* __restrict__ out) { + static_assert(WarpsPerBlock >= 1 && WarpsPerBlock <= 8); + constexpr int MaxSplits = 128; + constexpr unsigned Mask = 0xffffffffu; + __shared__ float weights[WarpsPerBlock][MaxSplits]; + + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int batch = static_cast(blockIdx.z); + const int output_row = static_cast(blockIdx.x) * WarpsPerBlock + warp; + const int token = output_row / kBidirectionalGqaQHeads; + const int q_head = output_row - token * kBidirectionalGqaQHeads; + if (warp >= WarpsPerBlock || token >= Tokens) return; + + constexpr std::int64_t QueryElements = + static_cast(kBidirectionalGqaHeadDim) * kBidirectionalGqaQHeads * Tokens; + constexpr std::int64_t StatElements = + static_cast(kBidirectionalGqaQHeads) * Tokens; + partial_acc += QueryElements * split_capacity * batch; + partial_m += StatElements * split_capacity * batch; + partial_l += StatElements * split_capacity * batch; + positions += static_cast(Tokens) * batch; + out += QueryElements * batch; + + const int length = positions[0]; + if (length < 0 || length > max_context || token >= valid_columns[batch]) { +#pragma unroll + for (int item = 0; item < 4; ++item) { + const int d = lane + item * 32; + out[bidirectional_gqa_q_index(q_head, d, token)] = __float2bfloat16(0.0f); + } + return; + } + const int context_count = min(length, Window - 1); + const int context_tiles = (context_count + KeyBlock - 1) / KeyBlock; + const int active_splits = context_tiles > 0 ? min(context_tiles, split_capacity) : 1; + + float local_m = -CUDART_INF_F; + for (int split = lane; split < active_splits; split += 32) { + local_m = + fmaxf(local_m, partial_m[bidirectional_gqa_stat_index(q_head, token, split)]); + } + const float global_m = warp_max<32>(local_m, Mask); + + float local_l = 0.0f; + for (int split = lane; split < active_splits; split += 32) { + const auto stat = bidirectional_gqa_stat_index(q_head, token, split); + const float weight = expf(partial_m[stat] - global_m); + weights[warp][split] = weight; + local_l += partial_l[stat] * weight; + } + const float global_l = warp_sum<32>(local_l, Mask); + __syncwarp(Mask); + +#pragma unroll + for (int item = 0; item < 4; ++item) { + const int d = lane + item * 32; + float numerator = 0.0f; + for (int split = 0; split < active_splits; ++split) { + numerator += + __bfloat162float( + partial_acc[bidirectional_gqa_partial_index(q_head, d, token, split)]) * + weights[warp][split]; + } + const float value = global_l > 0.0f ? numerator / global_l : 0.0f; + out[bidirectional_gqa_q_index(q_head, d, token)] = __float2bfloat16(value); + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/cold_i8_kernels.cuh b/src/ops/kernel/cold_i8_kernels.cuh new file mode 100644 index 0000000000..9320c98bd9 --- /dev/null +++ b/src/ops/kernel/cold_i8_kernels.cuh @@ -0,0 +1,105 @@ +#pragma once + +// ninfer::ops::detail - raw cold-slot codec for INT8-tier KV pages (v2b). +// +// The INT8 tier's requantized E2M1 codes are near-uniform (measured H +// 3.6-3.9 on real planes), so rANS gains nothing; this slot format stores +// the requant output verbatim with a fixed layout and no overflow path: +// +// [ 16 B header | 8192 B packed E2M1 nibbles | 1024 B E4M3 g16 scales ] +// +// The nibble/scale planes use the same page-major geometry the NVFP4 tier +// stores (codes [128, 64, kv_heads, pages], scales [16, 64, kv_heads, +// pages]), produced by entropy_cold_requant's Int8G64 mode. Restore +// converts a slot back into the INT8 tier's native planes (int8 codes + +// fp16 group-64 scales) with the upper-bound group scale, adding <0.4% +// quantization noise on top of the requant's measured 0.012 NMSE. +// +// Kernel DEFINITIONS live only in ops/launcher/cold_i8.cu; this header +// declares them plus the shared device helpers so attention kernels can +// include it without duplicate device-link definitions. + +#include "ninfer/ops/cold_i8.h" +#include "ops/kernel/gqa_attention_kv_nvfp4.cuh" + +#include +#include + +#include + +namespace ninfer::ops::detail { + +inline constexpr int kColdI8SlotHeaderBytes = 16; +inline constexpr int kColdI8SlotCodeBytes = 8192; // 64 rows x 128 B nibbles +inline constexpr int kColdI8SlotScaleBytes = 1024; // 64 rows x 16 B E4M3 +static_assert(kColdI8SlotHeaderBytes + kColdI8SlotCodeBytes + kColdI8SlotScaleBytes == + ninfer::ops::kColdI8SlotBytes); +inline constexpr std::uint32_t kColdI8SlotMagic = 0x49384352u; // "RC8I" + +// Pack one requantized (page, head, plane) into a raw slot. src uses the +// nvfp4 page-major geometry entropy_cold_requant emits; slots layout is +// [slot_bytes, kv_heads, 2, pages] with V one nb[2] step past K. +__global__ void cold_i8_slot_pack_kernel(const std::uint8_t* __restrict__ src_codes, + const std::uint8_t* __restrict__ src_scales, + int kv_heads, + std::uint8_t* __restrict__ slots, + std::int32_t* __restrict__ slot_valid); + +// Warm restore: unpack one (page, head, plane) slot into the INT8 tier's +// native planes (int8 codes [256,64,kv_heads,pages], fp16 scales +// [4,64,kv_heads,pages]). One block per (head, page); 256 threads split +// the 64 rows. +__global__ void cold_i8_slot_restore_kernel(const std::uint8_t* __restrict__ slots, + int kv_heads, std::int8_t* __restrict__ dst_codes, + __half* __restrict__ dst_scales); + +// Slot region accessors for producers. +__device__ __forceinline__ const std::uint8_t* +cold_i8_slot_scales(const std::uint8_t* slot) { + return slot + kColdI8SlotHeaderBytes + kColdI8SlotCodeBytes; +} + +__device__ __forceinline__ const std::uint8_t* +cold_i8_slot_codes(const std::uint8_t* slot) { + return slot + kColdI8SlotHeaderBytes; +} + +// Decode one key row of a raw slot into INT8-tier native form: 256 int8 +// codes plus one fp16 scale per 64-channel group. The group scale is the +// upper bound 6*max(e4m3 sub-scales)/127 so no amax scan of the decoded +// values is needed; codes clamp at 127 so fp16 rounding-down is safe. +// Used by the warm-restore kernel and the attention producers' cold staging. +__device__ __forceinline__ void cold_i8_decode_row(const std::uint8_t* slot, int row, + std::int8_t* codes_out, // 256, d-major + __half* scales_out) { // 4 groups + const std::uint8_t* row_codes = cold_i8_slot_codes(slot) + row * 128; + const std::uint8_t* row_scales = cold_i8_slot_scales(slot) + row * 16; +#pragma unroll + for (int g = 0; g < 4; ++g) { + float mx = 0.0f; +#pragma unroll + for (int s = 0; s < 4; ++s) { + mx = fmaxf(mx, gqa_kv_nvfp4_e4m3_to_f32(row_scales[g * 4 + s])); + } + const float scale = mx * 6.0f / 127.0f; + scales_out[g] = __float2half(scale); + const float inv = scale > 0.0f ? 1.0f / scale : 0.0f; +#pragma unroll + for (int i = 0; i < 64; i += 2) { + const int d = g * 64 + i; + const std::uint8_t b = row_codes[d >> 1]; + const float v0 = gqa_kv_nvfp4_e2m1_to_f32(b & 0x0F) * + gqa_kv_nvfp4_e4m3_to_f32(row_scales[d >> 4]); + const float v1 = gqa_kv_nvfp4_e2m1_to_f32(b >> 4) * + gqa_kv_nvfp4_e4m3_to_f32(row_scales[(d + 1) >> 4]); + int c0 = __float2int_rn(v0 * inv); + int c1 = __float2int_rn(v1 * inv); + c0 = max(-127, min(127, c0)); + c1 = max(-127, min(127, c1)); + codes_out[d] = static_cast(c0); + codes_out[d + 1] = static_cast(c1); + } + } +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/kernel/dflash2_grouped_conv.cuh b/src/ops/kernel/dflash2_grouped_conv.cuh new file mode 100644 index 0000000000..8b3b5b3530 --- /dev/null +++ b/src/ops/kernel/dflash2_grouped_conv.cuh @@ -0,0 +1,44 @@ +#pragma once + +// Implements: include/ninfer/ops/dflash2_grouped_conv.h +// Match: contiguous BF16 hidden/delta/base with the documented token-major layout. + +#include + +#include + +namespace ninfer::ops { + +template +__launch_bounds__(Block) __global__ void dflash2_grouped_conv_kernel( + const __nv_bfloat16* __restrict__ hidden, const __nv_bfloat16* __restrict__ delta, + const __nv_bfloat16* __restrict__ base, __nv_bfloat16* __restrict__ out, int hidden_size, + int tokens, int taps, int groups, int group_size, int block_size, int side) { + const int h = static_cast(blockIdx.x) * Block + static_cast(threadIdx.x); + if (h >= hidden_size) { return; } + const int g = h / group_size; + for (int t = 0; t < tokens; ++t) { + const std::int64_t hidden_offset = static_cast(h) + + static_cast(hidden_size) * t; + const std::int64_t delta_n = + static_cast(groups) * (side * taps) + g; + const std::int64_t delta_offset = + delta_n + static_cast(2) * taps * groups * t; + float value = __bfloat162float(hidden[hidden_offset]) * + (__bfloat162float(base[h]) + __bfloat162float(delta[delta_offset])); + const int position = t & (block_size - 1); + for (int tap = 1; tap < taps; ++tap) { + if (position < tap) { continue; } + const std::int64_t prev = static_cast(h) + + static_cast(hidden_size) * (t - tap); + const std::int64_t coefficient = + delta_offset + static_cast(tap) * groups; + value += __bfloat162float(hidden[prev]) * + (__bfloat162float(base[static_cast(tap) * hidden_size + h]) + + __bfloat162float(delta[coefficient])); + } + out[hidden_offset] = __float2bfloat16(value); + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/dflash2_selector.cuh b/src/ops/kernel/dflash2_selector.cuh new file mode 100644 index 0000000000..296535e5b0 --- /dev/null +++ b/src/ops/kernel/dflash2_selector.cuh @@ -0,0 +1,191 @@ +#pragma once + +// Implements: include/ninfer/ops/dflash2_selector.h +// Match: token-major BF16 logits/projection and the documented scratch layouts. + +#include "ops/launcher/dflash2_selector.h" + +#include +#include + +#include + +namespace ninfer::ops { + +__device__ __forceinline__ int dflash2_selector_candidate_offset(int b, int batch, int s, + int steps, int c) { + return b + batch * (s + steps * c); +} + +__device__ __forceinline__ int dflash2_selector_score_offset(int b, int batch, int s, int steps, + int p, int c, int top_k) { + return b + batch * (s + steps * (p + top_k * c)); +} + +template +__launch_bounds__(Block) __global__ void dflash2_selector_topk_kernel( + const __nv_bfloat16* __restrict__ logits, std::int32_t* __restrict__ candidates, + float* __restrict__ unary, int vocab, int batch, int steps, int columns) { + const int column = static_cast(blockIdx.x); + const int tid = static_cast(threadIdx.x); + if (column >= columns) { return; } + + float local_value[K]; + std::int32_t local_index[K]; +#pragma unroll + for (int k = 0; k < K; ++k) { + local_value[k] = -CUDART_INF_F; + local_index[k] = vocab; + } + const __nv_bfloat16* col_logits = logits + static_cast(column) * vocab; + for (int v = tid; v < vocab; v += Block) { + const float value = __bfloat162float(col_logits[v]); + // Insertion into the sorted list while keeping (value, -id) ordering. + for (int k = 0; k < K; ++k) { + const bool better = + value > local_value[k] || (value == local_value[k] && v < local_index[k]); + if (better) { + for (int tail = K - 1; tail > k; --tail) { + local_value[tail] = local_value[tail - 1]; + local_index[tail] = local_index[tail - 1]; + } + local_value[k] = value; + local_index[k] = v; + break; + } + } + } + + __shared__ float shared_value[Block][K]; + __shared__ std::int32_t shared_index[Block][K]; +#pragma unroll + for (int k = 0; k < K; ++k) { + shared_value[tid][k] = local_value[k]; + shared_index[tid][k] = local_index[k]; + } + __syncthreads(); + + if (tid == 0) { + float merged_value[K]; + std::int32_t merged_index[K]; +#pragma unroll + for (int k = 0; k < K; ++k) { + merged_value[k] = -CUDART_INF_F; + merged_index[k] = vocab; + } + for (int thread = 0; thread < Block; ++thread) { + for (int k = 0; k < K; ++k) { + const float value = shared_value[thread][k]; + const std::int32_t index = shared_index[thread][k]; + for (int slot = 0; slot < K; ++slot) { + const bool better = + value > merged_value[slot] || + (value == merged_value[slot] && index < merged_index[slot]); + if (better) { + for (int tail = K - 1; tail > slot; --tail) { + merged_value[tail] = merged_value[tail - 1]; + merged_index[tail] = merged_index[tail - 1]; + } + merged_value[slot] = value; + merged_index[slot] = index; + break; + } + } + } + } + const int b = column / steps; + const int s = column - b * steps; + for (int k = 0; k < K; ++k) { + const int offset = + dflash2_selector_candidate_offset(b, batch, s, steps, k); + candidates[offset] = merged_index[k]; + unary[offset] = merged_value[k]; + } + } +} + +template +__launch_bounds__(Block) __global__ void dflash2_selector_scores_kernel( + const std::int32_t* __restrict__ candidates, const float* __restrict__ unary, + const __nv_bfloat16* __restrict__ projected, + const __nv_bfloat16* __restrict__ predecessor_codebook, + const __nv_bfloat16* __restrict__ successor_codebook, + const std::int32_t* __restrict__ anchors, float* __restrict__ scores, int vocab, int batch, + int steps, int top_k, int columns) { + constexpr int K = detail::kDflash2SelectorTopK; + constexpr int R = detail::kDflash2SelectorRank; + const int flat = static_cast(blockIdx.x); + const int tid = static_cast(threadIdx.x); + if (flat >= columns || tid >= K * K) { return; } + const int b = flat / steps; + const int s = flat - b * steps; + const int p = tid / K; + const int c = tid - p * K; + + __shared__ std::int32_t shared_candidates[K]; + __shared__ float shared_unary[K]; + if (tid < K) { + const int offset = dflash2_selector_candidate_offset(b, batch, s, steps, tid); + shared_candidates[tid] = candidates[offset]; + shared_unary[tid] = unary[offset]; + } + __syncthreads(); + + if (s == 0 && p > 0) { + scores[dflash2_selector_score_offset(b, batch, s, steps, p, c, top_k)] = -CUDART_INF_F; + return; + } + const std::int32_t predecessor = + s == 0 ? anchors[b] + : candidates[dflash2_selector_candidate_offset(b, batch, s - 1, steps, p)]; + const std::int32_t successor = shared_candidates[c]; + const __nv_bfloat16* hidden = projected + static_cast(R) * flat; + const __nv_bfloat16* predecessor_row = + predecessor_codebook + static_cast(predecessor) * R; + const __nv_bfloat16* successor_row = + successor_codebook + static_cast(successor) * R; + float pair = 0.0f; +#pragma unroll 8 + for (int r = 0; r < R; ++r) { + pair += __bfloat162float(hidden[r]) * __bfloat162float(predecessor_row[r]) * + __bfloat162float(successor_row[r]); + } + scores[dflash2_selector_score_offset(b, batch, s, steps, p, c, top_k)] = + pair + shared_unary[c]; +} + +__global__ void dflash2_selector_walk_kernel(const std::int32_t* __restrict__ candidates, + const float* __restrict__ scores, + std::int32_t* __restrict__ drafts, int batch, + int steps, int top_k) { + constexpr int K = detail::kDflash2SelectorTopK; + const int b = static_cast(blockIdx.x); + const int lane = static_cast(threadIdx.x); + constexpr unsigned Mask = 0xffffffffu; + int previous = 0; + for (int s = 0; s < steps; ++s) { + float value = -CUDART_INF_F; + if (lane < K) { + value = scores[dflash2_selector_score_offset(b, batch, s, steps, previous, lane, + top_k)]; + } +#pragma unroll + for (int offset = 16; offset > 0; offset >>= 1) { + value = fmaxf(value, __shfl_xor_sync(Mask, value, offset)); + } + const float best = __shfl_sync(Mask, value, 0); + const bool equal = lane < K && value == best; + const std::int32_t candidate_rank = equal ? lane : K; + std::int32_t chosen = candidate_rank; +#pragma unroll + for (int offset = 16; offset > 0; offset >>= 1) { + chosen = min(chosen, __shfl_xor_sync(Mask, chosen, offset)); + } + const std::int32_t token = + candidates[dflash2_selector_candidate_offset(b, batch, s, steps, chosen)]; + drafts[b * steps + s] = token; + previous = chosen; + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/entropy_cold_requant_kernels.cuh b/src/ops/kernel/entropy_cold_requant_kernels.cuh new file mode 100644 index 0000000000..6c8d52b273 --- /dev/null +++ b/src/ops/kernel/entropy_cold_requant_kernels.cuh @@ -0,0 +1,134 @@ +#pragma once + +// ninfer::ops::detail - cold-page requantization kernel for the entropy slot +// codec (revision 2). +// +// The revision-1 slot codec rANS-encoded the stored group-16 NVFP4 code +// nibbles directly; on real Qwen3.8 pages those codes are near-uniform +// (~4.0 bits/nibble) so the fixed-slot encoder fell back to the uncompressed +// plane and the cold pool saved nothing. Requantizing the same values with +// one E4M3 scale per 64 channels skews the code distribution enough for the +// unchanged order-0 rANS to compress it (measured 2.0-2.6 bits/code on +// captured .kvc frames; 3-bit signed requant was measured worse: 0.04-0.13 +// NMSE vs 0.011-0.022 for g64 E2M1). +// +// The kernel reads one page plane in its stored format and writes fresh +// NVFP4 planes: packed E2M1 codes plus one E4M3FN scale per 64-channel group +// replicated into the four group-16 scale slots it covers. Downstream slot +// encode, decode, scale scatter, and attention producers stay byte-identical +// to revision 1; only the codes fed into the rANS change. + +#include "ops/kernel/gqa_attention_kv_nvfp4.cuh" +#include "ops/kernel/gqa_attention_kv_quant.cuh" +// ISO3 = sign-magnitude INT3: low 3 bits magnitude 0..7, bit 3 sign. +// Inlined here to keep the op header dependency-free. +__device__ __forceinline__ std::uint8_t gqa_iso3_nibble(float value, float scale) { + float mag = roundf(fabsf(value) / scale); + if (mag > 7.0f) { mag = 7.0f; } + if (mag < 0.0f) { mag = 0.0f; } + std::uint8_t code = static_cast(mag); + if (value < 0.0f && code != 0) { code |= 0x08u; } + return code; +} +__device__ __forceinline__ float gqa_iso3_decode(std::uint8_t code) { + const float mag = static_cast(code & 0x07u); + return (code & 0x08u) != 0 ? -mag : mag; +} +#include "ops/launcher/entropy_cold_requant.h" + +#include + +#include + +namespace ninfer::ops::detail { + +// One block per (kv_head, page); 256 threads = 64 token rows x 4 groups of +// 64 channels. dst planes use the nvfp4 page-major layout the slot encoder +// expects: codes [128, 64, kv_heads, pages], scales [16, 64, kv_heads, pages]. +__global__ void entropy_cold_requant_kernel(const std::uint8_t* __restrict__ src_codes, + const std::uint8_t* __restrict__ src_scales, + ColdRequantSource mode, int kv_heads, + std::uint8_t* __restrict__ dst_codes, + std::uint8_t* __restrict__ dst_scales) { + const int head = static_cast(blockIdx.x); + const int page = static_cast(blockIdx.y); + const int token = static_cast(threadIdx.x) >> 2; + const int group = static_cast(threadIdx.x) & 3; + const int lane0 = group * 64; + + // Row strides in bytes: nvfp4 codes 256/2, nvfp4 scales 256/16, + // int8 codes 256, int8 scales 4 fp16 = 8. + const std::int64_t page_rows = static_cast(kPagedKVPageSize); + const std::int64_t head_off = + page_rows * (static_cast(head) + + static_cast(kv_heads) * static_cast(page)); + + float vals[64]; + if (mode == ColdRequantSource::Nvfp4G16) { + const std::uint8_t* codes = src_codes + 128 * head_off + 128 * token; + const std::uint8_t* scales = src_scales + 16 * head_off + 16 * token; +#pragma unroll + for (int i = 0; i < 64; ++i) { + const int d = lane0 + i; + const std::uint8_t byte = codes[d >> 1]; + const std::uint8_t nib = (d & 1) != 0 ? static_cast(byte >> 4) + : static_cast(byte & 0x0F); + vals[i] = gqa_kv_nvfp4_e2m1_to_f32(nib) * gqa_kv_nvfp4_e4m3_to_f32(scales[d >> 4]); + } + } else if (mode == ColdRequantSource::Iso3VG16) { + // The global NVFP4 tier stores V as ISO3 sign-magnitude INT3 nibbles in + // the same two-per-byte plane geometry. Requant keeps the native ISO3 + // nibble semantics so the warm producers' dequant path is unchanged; + // only the scales are re-derived per 64 channels. + const std::uint8_t* codes = src_codes + 128 * head_off + 128 * token; + const std::uint8_t* scales = src_scales + 16 * head_off + 16 * token; +#pragma unroll + for (int i = 0; i < 64; ++i) { + const int d = lane0 + i; + const std::uint8_t byte = codes[d >> 1]; + const std::uint8_t nib = (d & 1) != 0 ? static_cast(byte >> 4) + : static_cast(byte & 0x0F); + vals[i] = gqa_iso3_decode(nib) * gqa_kv_nvfp4_e4m3_to_f32(scales[d >> 4]); + } + } else { + const std::int8_t* codes = reinterpret_cast(src_codes) + 256 * head_off + + 256 * token; + const __half* scales = reinterpret_cast(src_scales + 8 * head_off + + 8 * token); + const float s = __half2float(scales[group]); +#pragma unroll + for (int i = 0; i < 64; ++i) { + vals[i] = static_cast(codes[lane0 + i]) * s; + } + } + + float amax = 0.0f; +#pragma unroll + for (int i = 0; i < 64; ++i) { amax = fmaxf(amax, fabsf(vals[i])); } + const bool iso3_out = mode == ColdRequantSource::Iso3VG16; + const std::uint8_t scale_byte = + gqa_kv_nvfp4_fp32_to_e4m3(fmaxf(amax / (iso3_out ? 7.0f : 6.0f), 0x1p-9f)); + const float s = gqa_kv_nvfp4_e4m3_to_f32(scale_byte); + + std::uint8_t* dst_c = dst_codes + 128 * head_off + 128 * token; +#pragma unroll + for (int i = 0; i < 64; i += 2) { + std::uint8_t lo; + std::uint8_t hi; + if (iso3_out) { + lo = gqa_iso3_nibble(vals[i], s); + hi = gqa_iso3_nibble(vals[i + 1], s); + } else { + lo = gqa_kv_nvfp4_e2m1_nibble(vals[i] / s); + hi = gqa_kv_nvfp4_e2m1_nibble(vals[i + 1] / s); + } + dst_c[(lane0 + i) >> 1] = static_cast(lo | (hi << 4)); + } + std::uint8_t* dst_s = dst_scales + 16 * head_off + 16 * token; + dst_s[group * 4 + 0] = scale_byte; + dst_s[group * 4 + 1] = scale_byte; + dst_s[group * 4 + 2] = scale_byte; + dst_s[group * 4 + 3] = scale_byte; +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/kernel/entropy_nvfp4_common.cuh b/src/ops/kernel/entropy_nvfp4_common.cuh new file mode 100644 index 0000000000..57494cb995 --- /dev/null +++ b/src/ops/kernel/entropy_nvfp4_common.cuh @@ -0,0 +1,72 @@ +#pragma once + +// Shared static rANS constants and device helpers for the NVFP4 E2M1 codecs. + +#include + +#include + +namespace ninfer::ops::detail { + +inline constexpr int kEntropyNvfp4ScaleBits = 12; +inline constexpr std::uint32_t kEntropyNvfp4Scale = 4096u; +inline constexpr std::uint32_t kEntropyNvfp4Mask = 4095u; +inline constexpr std::uint32_t kEntropyNvfp4RansByteL = 1u << 23; + +// Mirrors tools/calib/rans_nvfp4.py: nearest-even rounding of +// count/total*4096 with a minimum of one, then the residual is distributed to +// the current largest count (counts are decremented after each grant so a +// dominant symbol can receive several grants) or taken from the largest +// frequency above one. This keeps the CPU feasibility gate byte-compatible +// with the device codec. +__device__ inline void entropy_nvfp4_normalize_freqs(const int hist[16], int total, + std::uint16_t fs[16]) { + if (total <= 0) { + for (int s = 0; s < 16; ++s) { fs[s] = 256; } + return; + } + + int counts[16]; + int sum = 0; + for (int s = 0; s < 16; ++s) { + counts[s] = hist[s]; + const double q = + static_cast(hist[s]) * static_cast(kEntropyNvfp4Scale) / + static_cast(total); + std::uint32_t f = static_cast(__double2ll_rn(q)); + if (f < 1) { f = 1; } + fs[s] = static_cast(f); + sum += static_cast(f); + } + + int diff = static_cast(kEntropyNvfp4Scale) - sum; + while (diff > 0) { + int best = 0; + for (int s = 1; s < 16; ++s) { + if (counts[s] > counts[best]) { best = s; } + } + fs[best] = static_cast(fs[best] + 1); + counts[best] = counts[best] > 0 ? counts[best] - 1 : 0; + --diff; + } + while (diff < 0) { + int best = 0; + for (int s = 1; s < 16; ++s) { + const bool s_ok = fs[s] > 1; + const bool best_ok = fs[best] > 1; + if ((s_ok && !best_ok) || (s_ok && best_ok && fs[s] > fs[best])) { best = s; } + } + fs[best] = static_cast(fs[best] - 1); + ++diff; + } +} + +__device__ inline void entropy_nvfp4_make_start(const std::uint16_t fs[16], std::uint32_t start[16]) { + std::uint32_t acc = 0; + for (int s = 0; s < 16; ++s) { + start[s] = acc; + acc += fs[s]; + } +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/kernel/entropy_nvfp4_slot.cuh b/src/ops/kernel/entropy_nvfp4_slot.cuh new file mode 100644 index 0000000000..585d2dccd9 --- /dev/null +++ b/src/ops/kernel/entropy_nvfp4_slot.cuh @@ -0,0 +1,218 @@ +#pragma once + +// ninfer::ops - page-slot NVFP4 E2M1 rANS codec. +// +// One slot stores one (physical page, kv_head, K|V plane): the 64-token page +// is split into two 32-token half-pages, and each half-page is split into 16 +// independent rANS streams of 512 code nibbles (two contiguous 128-byte rows +// per stream). Streams are independent so a 16-thread group can decode a +// half-page in parallel; this is the granularity the attention producers use. +// +// Slot layout (all offsets relative to the slot start): +// header 320 bytes: +// magic/version/flags (8) + 2 halves x 128 bytes +// each half header: +// uint16 freqs[16] (shared by all 16 streams, normalized from their +// combined histogram), uint32 offsets[17] (start of each stream and the +// end sentinel), 28 bytes reserved. +// stream bytes: canonical rANS streams (renorm bytes in emission order, +// 4-byte final state last) packed back-to-back in stream order. +// +// A page whose codes are incompressible at the fixed stream budget keeps the +// normal code plane and clears the slot's valid flag; callers fall back to the +// uncompressed plane. + +#include "ops/kernel/entropy_nvfp4_common.cuh" + +#include + +#include + +namespace ninfer::ops::detail { + +inline constexpr std::uint32_t kEntropyNvfp4SlotMagic = 0x3156524Eu; // "NRV1" +inline constexpr std::uint16_t kEntropyNvfp4SlotVersion = 1; +inline constexpr std::uint16_t kEntropyNvfp4SlotFlagValid = 0x0001u; +inline constexpr int kEntropyNvfp4SlotHalfBytes = 4096; // 32 x 128 +inline constexpr int kEntropyNvfp4SlotStreamsPerHalf = 16; +inline constexpr int kEntropyNvfp4SlotStreamBytes = 256; // 512 nibbles +inline constexpr int kEntropyNvfp4SlotStreamSymbols = 512; +inline constexpr int kEntropyNvfp4SlotStreams = 32; +inline constexpr int kEntropyNvfp4SlotHeaderBytes = 320; +// One uncompressed E4M3FN scale byte per 16-channel group and token row: +// 16 x 64 = 1024 bytes. Scales live at the end of the slot so the whole +// physical page group (code planes and scale planes) can be freed while cold. +inline constexpr int kEntropyNvfp4SlotScaleBytes = 1024; + +struct alignas(8) EntropyNvfp4SlotHalf { + std::uint16_t freqs[16]; + std::uint32_t offsets[17]; + std::uint8_t reserved[28]; +}; +static_assert(sizeof(EntropyNvfp4SlotHalf) == 128); + +struct alignas(8) EntropyNvfp4SlotHeader { + std::uint32_t magic; + std::uint16_t version; + std::uint16_t flags; + EntropyNvfp4SlotHalf halves[2]; + std::uint8_t reserved[56]; +}; +static_assert(sizeof(EntropyNvfp4SlotHeader) == 320); + +// Read half->stream 0..15: rows half*32 + [2*stream, 2*stream+2), each row +// 128 packed code bytes. dst must hold 256 bytes. +__device__ __forceinline__ void entropy_nvfp4_slot_stream_source( + const std::uint8_t* page_codes, int half, int stream, std::uint8_t (&dst)[256]) { +#pragma unroll + for (int row = 0; row < 2; ++row) { + const int src_row = half * 32 + 2 * stream + row; + const std::uint8_t* src = page_codes + src_row * 128; +#pragma unroll + for (int i = 0; i < 128; ++i) { dst[row * 128 + i] = src[i]; } + } +} + +__device__ __forceinline__ std::uint32_t +entropy_nvfp4_slot_rans_encode_count(const std::uint8_t (&codes)[256], int count, + const std::uint16_t (&fs)[16]) { + std::uint32_t start[16]; + entropy_nvfp4_make_start(fs, start); + + std::uint32_t x = kEntropyNvfp4RansByteL; + std::uint32_t emitted = 0; + for (int i = count - 1; i >= 0; --i) { + const std::uint8_t byte = codes[i >> 1]; + const int symbol = (i & 1) ? (byte >> 4) : (byte & 0x0f); + const std::uint32_t freq = fs[symbol]; + const std::uint32_t x_max = + ((kEntropyNvfp4RansByteL >> kEntropyNvfp4ScaleBits) << 8) * freq; + while (x >= x_max) { + ++emitted; + x >>= 8; + } + x = ((x / freq) << kEntropyNvfp4ScaleBits) + (x % freq) + start[symbol]; + } + return emitted + 4; // renorm bytes + little-endian final state +} + +// Returns the stream size including the final state, or -1 on capacity +// overflow. The stream uses the canonical layout: renorm bytes in emission +// order followed by the 4-byte final state. +__device__ __forceinline__ int +entropy_nvfp4_slot_rans_encode_to(std::uint8_t* dst, int capacity, + const std::uint8_t (&codes)[256], int count, + const std::uint16_t (&fs)[16]) { + std::uint32_t start[16]; + entropy_nvfp4_make_start(fs, start); + + std::uint32_t x = kEntropyNvfp4RansByteL; + std::uint8_t* ptr = dst; + const int state_room = capacity - 4; + if (state_room < 0) { return -1; } + for (int i = count - 1; i >= 0; --i) { + const std::uint8_t byte = codes[i >> 1]; + const int symbol = (i & 1) ? (byte >> 4) : (byte & 0x0f); + const std::uint32_t freq = fs[symbol]; + const std::uint32_t x_max = + ((kEntropyNvfp4RansByteL >> kEntropyNvfp4ScaleBits) << 8) * freq; + while (x >= x_max) { + if (ptr - dst >= state_room) { return -1; } + *ptr++ = static_cast(x & 0xffu); + x >>= 8; + } + x = ((x / freq) << kEntropyNvfp4ScaleBits) + (x % freq) + start[symbol]; + } + if (ptr - dst + 4 > capacity) { return -1; } + ptr[0] = static_cast(x & 0xffu); + ptr[1] = static_cast((x >> 8) & 0xffu); + ptr[2] = static_cast((x >> 16) & 0xffu); + ptr[3] = static_cast((x >> 24) & 0xffu); + return static_cast(ptr - dst) + 4; +} + +// Decode stream `stream` of `half` from a valid slot, invoking fn(i, symbol) +// for every one of the 512 code nibbles in symbol order (i 0..511). Returns +// false when the header/stream is malformed. This is the low-level hook used +// by attention producers that want to dequantize on the fly into BF16 smem. +template +__device__ __forceinline__ bool entropy_nvfp4_slot_decode_stream_apply(const std::uint8_t* slot, + int half, int stream, + Fn&& fn) { + const EntropyNvfp4SlotHeader* header = + reinterpret_cast(slot); + if (header->magic != kEntropyNvfp4SlotMagic || + header->version != kEntropyNvfp4SlotVersion || + (header->flags & kEntropyNvfp4SlotFlagValid) == 0) { + return false; + } + const EntropyNvfp4SlotHalf& half_header = header->halves[half]; + const std::uint32_t begin = half_header.offsets[stream]; + const std::uint32_t end = half_header.offsets[stream + 1]; + if (end < begin + 4) { return false; } + const int size = static_cast(end - begin); + + std::uint16_t fs[16]; + std::uint32_t start[16]; + for (int s = 0; s < 16; ++s) { fs[s] = half_header.freqs[s]; } + entropy_nvfp4_make_start(fs, start); + + const std::uint8_t* stream_bytes = slot + begin; + std::uint32_t x = static_cast(stream_bytes[size - 4]) | + (static_cast(stream_bytes[size - 3]) << 8) | + (static_cast(stream_bytes[size - 2]) << 16) | + (static_cast(stream_bytes[size - 1]) << 24); + int pos = size - 4; + + for (int i = 0; i < kEntropyNvfp4SlotStreamSymbols; ++i) { + const std::uint32_t xmask = x & kEntropyNvfp4Mask; + int symbol = 0; + while (symbol < 15 && + !(start[symbol] <= xmask && xmask < start[symbol] + fs[symbol])) { + ++symbol; + } + fn(i, symbol); + x = fs[symbol] * (x >> kEntropyNvfp4ScaleBits) + xmask - start[symbol]; + while (x < kEntropyNvfp4RansByteL) { + if (pos == 0) { return false; } + --pos; + x = (x << 8) | stream_bytes[pos]; + } + } + return true; +} + +// Decode stream `stream` of `half` from a valid slot into 256 contiguous +// packed code bytes. Returns false when the header/stream is malformed. +__device__ __forceinline__ bool entropy_nvfp4_slot_decode_stream( + const std::uint8_t* slot, int half, int stream, std::uint8_t* dst) { + std::uint8_t packed = 0; + return entropy_nvfp4_slot_decode_stream_apply( + slot, half, stream, [&](int i, int symbol) { + const int byte_index = i >> 1; + if ((i & 1) == 0) { + packed = static_cast(symbol); + dst[byte_index] = packed; + } else { + dst[byte_index] = static_cast((symbol << 4) | packed); + } + }); +} + +// Fixed tail region holding the uncompressed E4M3FN scale page. +__device__ __forceinline__ const std::uint8_t* entropy_nvfp4_slot_scales(const std::uint8_t* slot, + int slot_bytes) { + return slot + slot_bytes - kEntropyNvfp4SlotScaleBytes; +} + +// Cooperative half-page decode: threads 0..15 each decode one 256-byte stream +// into the contiguous 4096-byte half buffer. Other threads do nothing. +__device__ __forceinline__ void entropy_nvfp4_slot_decode_half_parallel(const std::uint8_t* slot, + int half, int lane, + std::uint8_t* half_dst) { + if (lane >= kEntropyNvfp4SlotStreamsPerHalf) { return; } + entropy_nvfp4_slot_decode_stream(slot, half, lane, + half_dst + lane * kEntropyNvfp4SlotStreamBytes); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/kernel/entropy_nvfp4_slot_kernels.cuh b/src/ops/kernel/entropy_nvfp4_slot_kernels.cuh new file mode 100644 index 0000000000..fd52547f0d --- /dev/null +++ b/src/ops/kernel/entropy_nvfp4_slot_kernels.cuh @@ -0,0 +1,252 @@ +#pragma once + +// Launcher-only __global__ kernels for the page-slot NVFP4 rANS codec. Kept +// separate from entropy_nvfp4_slot.cuh so device helper headers can be included +// by attention kernels without duplicate device-link definitions. + +#include "ops/kernel/entropy_nvfp4_slot.cuh" +#include "ops/common/memory.cuh" + +#include + +#include + +namespace ninfer::ops::detail { + +// One block per (page, kv_head) slot. codes addresses a full page plane +// [128 bytes, 64 rows, kv_heads, pages] and scales addresses the matching +// [16 bytes, 64 rows, kv_heads, pages] plane in paged-cache PageMajor layout: +// page stride = kv_heads * 8192 (codes) / kv_heads * 1024 (scales), head stride +// = 8192 / 1024 bytes. Thread t owns stream (t & 15) of half (t >> 4). +__global__ void entropy_nvfp4_slot_encode_kernel(const std::uint8_t* __restrict__ codes, + const std::uint8_t* __restrict__ scales, + int kv_heads, + std::uint8_t* __restrict__ slots, + int slot_bytes, + std::int32_t* __restrict__ slot_valid, + const std::int32_t* __restrict__ page_ids, + int valid_page_stride) { + constexpr int StreamsPerHalf = kEntropyNvfp4SlotStreamsPerHalf; + constexpr int Streams = kEntropyNvfp4SlotStreams; + constexpr int HeaderBytes = kEntropyNvfp4SlotHeaderBytes; + + __shared__ std::uint32_t s_hist[2][16]; + __shared__ std::uint16_t s_freqs[2][16]; + __shared__ std::uint32_t s_sizes[2][16]; + __shared__ std::uint32_t s_prefix[2][16]; + __shared__ std::uint32_t s_data_base[2]; + __shared__ std::uint32_t s_half_total[2]; + __shared__ bool s_overflow; + + const int head = static_cast(blockIdx.x); + const int page = static_cast(blockIdx.y); + const int physical = page_ids == nullptr ? page : static_cast(page_ids[page]); + const int tid = static_cast(threadIdx.x); + const int half = tid >> 4; + const int lane = tid & 15; + + const std::int64_t head_stride = static_cast(kEntropyNvfp4SlotHalfBytes) * 2; + const std::uint8_t* page_codes = codes + (static_cast(physical) * kv_heads + head) * + head_stride; + const std::uint8_t* page_scales = + scales + (static_cast(physical) * kv_heads + head) * + kEntropyNvfp4SlotScaleBytes; + std::uint8_t* slot = slots + (static_cast(page) * kv_heads + head) * slot_bytes; + + for (int i = tid; i < slot_bytes; i += Streams) { slot[i] = 0; } + __syncthreads(); + + if (tid == 0) { + for (int h = 0; h < 2; ++h) { + for (int s = 0; s < 16; ++s) { + s_hist[h][s] = 0; + s_freqs[h][s] = 0; + s_sizes[h][s] = 0; + s_prefix[h][s] = 0; + } + } + s_overflow = false; + } + __syncthreads(); + + // --- pass A: combined histogram -> shared frequencies -> stream sizes ---- + std::uint8_t codes_stream[256]; + entropy_nvfp4_slot_stream_source(page_codes, half, lane, codes_stream); + std::uint32_t local_hist[16] = {0}; + for (int i = 0; i < kEntropyNvfp4SlotStreamSymbols; ++i) { + const std::uint8_t byte = codes_stream[i >> 1]; + ++local_hist[(i & 1) ? (byte >> 4) : (byte & 0x0f)]; + } + for (int s = 0; s < 16; ++s) { + atomicAdd(&s_hist[half][s], local_hist[s]); + } + __syncthreads(); + + if (tid == 0) { s_overflow = false; } + __syncthreads(); + if (lane == 0) { + int combined[16]; + for (int s = 0; s < 16; ++s) { + combined[s] = static_cast(s_hist[half][s]); + } + entropy_nvfp4_normalize_freqs(combined, kEntropyNvfp4SlotStreamSymbols * StreamsPerHalf, + s_freqs[half]); + } + __syncthreads(); + + { + std::uint16_t fs[16]; + for (int s = 0; s < 16; ++s) { fs[s] = s_freqs[half][s]; } + const std::uint32_t size = + entropy_nvfp4_slot_rans_encode_count(codes_stream, kEntropyNvfp4SlotStreamSymbols, fs); + s_sizes[half][lane] = size; + } + __syncthreads(); + + const int data_bytes = slot_bytes - HeaderBytes - kEntropyNvfp4SlotScaleBytes; + if (lane == 0) { + const int budget = data_bytes / Streams; + std::uint32_t prefix = 0; + bool overflow = false; + for (int s = 0; s < StreamsPerHalf; ++s) { + const std::uint32_t size = s_sizes[half][s]; + if (size > static_cast(budget)) { overflow = true; } + s_prefix[half][s] = prefix; + prefix += size; + } + s_half_total[half] = prefix; + if (prefix > static_cast(data_bytes / 2)) { overflow = true; } + if (overflow) { s_overflow = true; } + } + __syncthreads(); + if (s_overflow) { + slot_valid[static_cast(page) * valid_page_stride + head] = 0; + if (tid == 0) { + EntropyNvfp4SlotHeader* header = reinterpret_cast(slot); + header->magic = kEntropyNvfp4SlotMagic; + header->version = kEntropyNvfp4SlotVersion; + header->flags = 0; + } + return; + } + + if (tid == 0) { + const std::uint32_t half0_total = s_half_total[0]; + s_data_base[0] = HeaderBytes; + s_data_base[1] = HeaderBytes + half0_total; + } + __syncthreads(); + + const std::uint32_t base = s_data_base[half]; + if (lane == 0) { + EntropyNvfp4SlotHeader* header = reinterpret_cast(slot); + header->magic = kEntropyNvfp4SlotMagic; + header->version = kEntropyNvfp4SlotVersion; + header->flags = kEntropyNvfp4SlotFlagValid; + EntropyNvfp4SlotHalf& half_header = header->halves[half]; + for (int s = 0; s < 16; ++s) { half_header.freqs[s] = s_freqs[half][s]; } + for (int s = 0; s < 16; ++s) { + half_header.offsets[s] = base + s_prefix[half][s]; + } + half_header.offsets[16] = base + s_half_total[half]; + } + __syncthreads(); + + // --- pass B: encode to the final compacted positions ---- + { + std::uint16_t fs[16]; + for (int s = 0; s < 16; ++s) { fs[s] = s_freqs[half][s]; } + std::uint8_t* stream_dst = slot + base + s_prefix[half][lane]; + const int budget = data_bytes / Streams; + const int size = + entropy_nvfp4_slot_rans_encode_to(stream_dst, budget, codes_stream, + kEntropyNvfp4SlotStreamSymbols, fs); + if (size < 0) { + s_overflow = true; + } + } + __syncthreads(); + if (s_overflow) { + slot_valid[static_cast(page) * valid_page_stride + head] = 0; + return; + } + + // --- copy the uncompressed E4M3FN scale page to the fixed tail region ---- + { + const std::uint8_t* scale_src = page_scales; + std::uint8_t* scale_dst = slot + slot_bytes - kEntropyNvfp4SlotScaleBytes; + for (int i = tid; i < kEntropyNvfp4SlotScaleBytes; i += Streams) { + scale_dst[i] = scale_src[i]; + } + } + __syncthreads(); + slot_valid[static_cast(page) * valid_page_stride + head] = 1; +} + +// One block per output half. Thread t (0..15) decodes stream t of the +// selected half into dst + t * 256 contiguous packed bytes. +__global__ void entropy_nvfp4_slot_decode_half_kernel(const std::uint8_t* __restrict__ slots, + int slot_bytes, + const std::int32_t* __restrict__ slot_ids, + const std::int32_t* __restrict__ halves, + std::uint8_t* __restrict__ dst, + int half_bytes) { + const int item = static_cast(blockIdx.x); + const int stream = static_cast(threadIdx.x); + if (stream >= kEntropyNvfp4SlotStreamsPerHalf) { return; } + const std::int32_t slot_id = slot_ids[item]; + const std::int32_t half = halves[item]; + const std::uint8_t* slot = slots + static_cast(slot_id) * slot_bytes; + std::uint8_t* stream_dst = + dst + static_cast(item) * half_bytes + stream * kEntropyNvfp4SlotStreamBytes; + if (!entropy_nvfp4_slot_decode_stream(slot, half, stream, stream_dst)) { + for (int i = 0; i < kEntropyNvfp4SlotStreamBytes; ++i) { stream_dst[i] = 0; } + } +} + +// Direct grid decode: blockIdx.x selects the half (0/1), blockIdx.y selects +// kv_head. Thread t decodes stream t of that half into item +// (kv_head * 2 + half) of the contiguous output buffer. +__global__ void entropy_nvfp4_slot_decode_half_grid_kernel(const std::uint8_t* __restrict__ slots, + int slot_bytes, + std::int32_t slot_base, + std::uint8_t* __restrict__ dst, + int half_bytes) { + const int half = static_cast(blockIdx.x); + const int head = static_cast(blockIdx.y); + const int stream = static_cast(threadIdx.x); + if (stream >= kEntropyNvfp4SlotStreamsPerHalf) { return; } + const std::uint8_t* slot = + slots + static_cast(slot_base + head) * slot_bytes; + std::uint8_t* stream_dst = + dst + static_cast(head * 2 + half) * half_bytes + + stream * kEntropyNvfp4SlotStreamBytes; + if (!entropy_nvfp4_slot_decode_stream(slot, half, stream, stream_dst)) { + for (int i = 0; i < kEntropyNvfp4SlotStreamBytes; ++i) { stream_dst[i] = 0; } + } +} + +// Scatters the uncompressed 1024-byte scale tail of every (page, kv_head) +// slot into the matching paged scale plane. Grid = (kv_heads, page_count); +// each 64-thread block copies 16-byte vectors. slots uses the host-cold +// layout: page stride = slot_page_stride, head stride = slot_bytes. +__global__ void entropy_nvfp4_slot_scales_scatter_kernel( + const std::uint8_t* __restrict__ slots, int slot_bytes, int slot_page_stride, + const std::int32_t* __restrict__ page_ids, int scale_page_stride, + std::uint8_t* __restrict__ scales) { + constexpr int ScaleBytes = 1024; + const int head = static_cast(blockIdx.x); + const int page = static_cast(blockIdx.y); + const int vec = static_cast(threadIdx.x); + if (vec >= ScaleBytes / 16) { return; } + const std::uint8_t* src = + slots + static_cast(page) * slot_page_stride + + static_cast(head) * slot_bytes + (slot_bytes - ScaleBytes) + vec * 16; + std::uint8_t* dst = + scales + static_cast(page_ids[page]) * scale_page_stride + + static_cast(head) * ScaleBytes + vec * 16; + const uint4 value = load_vec(src); + store_vec(dst, value); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/kernel/gqa_attention_decode.cuh b/src/ops/kernel/gqa_attention_decode.cuh new file mode 100644 index 0000000000..47953579be --- /dev/null +++ b/src/ops/kernel/gqa_attention_decode.cuh @@ -0,0 +1,261 @@ +#pragma once + +// ninfer::ops - split-KV GQA small-T attention shared scaffolding. The bf16 and +// int8 partial kernels live in gqa_attention_decode_bf16.cuh and +// gqa_attention_decode_i8.cuh respectively; they are fully separate kernels (no +// shared body) so each KV format can be optimized independently. This header owns +// only what both share: layout constants, device helpers, and the split reducer. + +#include "ops/common/math.cuh" +#include "ops/common/mma.cuh" +#include "ops/common/warp.cuh" +#include "ops/kernel/gqa_attention_geometry.cuh" +#include "ops/kernel/paged_kv_address.cuh" + +#include +#include + +#include + +namespace ninfer::ops { + +inline constexpr int kGqaHeadDim = 256; + +struct GqaAppendInput { + static constexpr bool writes_cache = true; + const __nv_bfloat16* k; + const __nv_bfloat16* v; +}; + +struct GqaCachedInput { + static constexpr bool writes_cache = false; +}; + +template +__device__ __forceinline__ std::int64_t gqa_cache_index(int physical_page, int kv_head, int d, + int page_offset) { + return paged_kv_element_offset(physical_page, kv_head, + page_offset, d); +} + +template +__device__ __forceinline__ std::int64_t gqa_q_index(int q_head, int d, int token = 0) { + return static_cast(d) + static_cast(kGqaHeadDim) * + (static_cast(q_head) + + static_cast(Geometry::QHeads) * token); +} + +template +__device__ __forceinline__ std::int64_t gqa_kv_new_index(int kv_head, int d, int token = 0) { + return static_cast(d) + + static_cast(kGqaHeadDim) * + (static_cast(kv_head) + + static_cast(Geometry::KVHeads) * token); +} + +template +__device__ __forceinline__ std::int64_t gqa_partial_acc_index(int q_head, int d, int token, + int split, int tokens) { + return static_cast(d) + + static_cast(kGqaHeadDim) * + (static_cast(q_head) + + static_cast(Geometry::QHeads) * + (static_cast(token) + static_cast(tokens) * split)); +} + +template +__device__ __forceinline__ std::int64_t gqa_partial_stat_index(int q_head, int token, int split, + int tokens) { + return static_cast(q_head) + + static_cast(Geometry::QHeads) * + (static_cast(token) + static_cast(tokens) * split); +} + +template +__device__ __forceinline__ bool gqa_valid_q_head(int kv_head, int q_head) { + return kv_head >= 0 && kv_head < Geometry::KVHeads && q_head >= kv_head * Geometry::GroupSize && + q_head < (kv_head + 1) * Geometry::GroupSize && q_head < Geometry::QHeads; +} + +template +__device__ __forceinline__ int gqa_small_t_default_splits(int window) { + int target_keys_per_split = 480 / Geometry::DecodeSplitScale; + if (window <= 4096) { + target_keys_per_split = 64 / Geometry::DecodeSplitScale; + } else if (window <= 8198) { + target_keys_per_split = 128 / Geometry::DecodeSplitScale; + } else if (window <= 16390) { + target_keys_per_split = 256 / Geometry::DecodeSplitScale; + } + constexpr int kMinSplits = 4 * Geometry::DecodeSplitScale; + int splits = div_up(window, target_keys_per_split); + splits = splits > kMinSplits ? splits : kMinSplits; + return splits < Geometry::DecodeSplits ? splits : Geometry::DecodeSplits; +} + +template +__device__ __forceinline__ int gqa_small_t_active_splits(int window, int launch_capacity, + int tokens) { + if (window <= 0) { return launch_capacity; } + int splits = 0; + if constexpr (Int8) { + if (tokens == 5 && window > 128 && window <= 512) { + splits = div_up(window, 32 / Geometry::DecodeSplitScale); + } else if (tokens == 6 && window > 128 && window <= 160) { + splits = div_up(window, 24 / Geometry::DecodeSplitScale); + } else if (tokens == 6 && window > 5000 && window <= 8198) { + splits = div_up(window, 192 / Geometry::DecodeSplitScale); + constexpr int kMin = 4 * Geometry::DecodeSplitScale; + constexpr int kMax = 42 * Geometry::DecodeSplitScale; + splits = splits > kMin ? splits : kMin; + splits = splits < kMax ? splits : kMax; + } else { + splits = gqa_small_t_default_splits(window); + } + } else { + splits = gqa_small_t_default_splits(window); + } + return splits < launch_capacity ? splits : launch_capacity; +} + +__device__ __forceinline__ int gqa_small_t_tc_swz(int row, int col) { + return (((col >> 3) ^ (row & 7)) << 3) | (col & 7); +} + +__device__ __forceinline__ int gqa_small_t_tc_swz32(int row, int col) { + return (((col >> 3) ^ (row & 3)) << 3) | (col & 7); +} + +// Signed int8 QK MMA, k=32 contraction. A = 16x32 s8 (4 regs/thread, 4 s8 each), +// B = 8x32 s8 col-major (2 regs/thread), D = 16x8 s32 (4 regs/thread). The A/B +// register byte layout is identical to the m16n8k16 bf16 fragments loaded by +// ldmatrix_x4/x2 over a d-contiguous int8 tile reinterpreted as +// b16 (two packed int8 per 16-bit lane), so the same ldmatrix helpers and XOR +// swizzle feed this MMA. The s32 accumulator layout matches the bf16 f32 +// accumulator (c0/c1 -> row groupID, c2/c3 -> row groupID+8), so score +// consumption is unchanged; only per-64-group scale rescale differs. +template +__device__ __forceinline__ void gqa_small_t_tc_row_to_qt(int row, int tokens, int kv_head, + int& q_head, int& token) { + token = row / Geometry::GroupSize; + const int local_q = row - token * Geometry::GroupSize; + q_head = kv_head * Geometry::GroupSize + local_q; +} + +template +__launch_bounds__(256) __global__ void gqa_attention_small_t_reduce_output_kernel( + const __nv_bfloat16* partial_acc, const float* partial_m, const float* partial_l, + const std::int32_t* positions, const std::int32_t* valid_columns, std::int32_t tokens, + std::int32_t full_width, std::int32_t column_begin, std::int32_t batch_size, + std::int32_t split_count, __nv_bfloat16* out) { + static_assert(DChunk > 0 && DChunk <= kGqaHeadDim); + + const int q_head = static_cast(blockIdx.x); + const int d_start = static_cast(blockIdx.y) * DChunk; + const int flat_column = static_cast(blockIdx.z); + int batch = 0; + int token = flat_column; + if constexpr (MultiBatch) { + batch = flat_column / tokens; + token = flat_column - batch * tokens; + } + const int tid = threadIdx.x; + if (q_head >= Geometry::QHeads || token >= tokens) { return; } + if constexpr (MultiBatch) { + if (batch >= batch_size) { return; } + } + + if constexpr (Offset) { positions += column_begin; } + if constexpr (MultiBatch) { positions += batch * full_width; } + const int last_pos = positions[tokens - 1]; + int output_column = token; + if constexpr (Offset) { output_column += column_begin; } + if constexpr (MultiBatch) { output_column += batch * full_width; } + + if constexpr (MultiBatch) { + const std::int64_t partial_acc_row = static_cast(batch) * kGqaHeadDim * + Geometry::QHeads * tokens * split_count; + const std::int64_t partial_stat_row = + static_cast(batch) * Geometry::QHeads * tokens * split_count; + partial_acc += partial_acc_row; + partial_m += partial_stat_row; + partial_l += partial_stat_row; + } + + const int window = last_pos + 1; + const int active_split_count = + gqa_small_t_active_splits(window, split_count, tokens); + + __shared__ float reduce[256]; + + float local_m = -CUDART_INF_F; + for (int split = tid; split < active_split_count; split += blockDim.x) { + local_m = fmaxf(local_m, + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)]); + } + reduce[tid] = local_m; + __syncthreads(); + + for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) { + if (tid < stride) { reduce[tid] = fmaxf(reduce[tid], reduce[tid + stride]); } + __syncthreads(); + } + const float head_m = reduce[0]; + __syncthreads(); + + if (head_m == -CUDART_INF_F) { + const int d = d_start + tid; + if (tid < DChunk && d < kGqaHeadDim) { + out[gqa_q_index(q_head, d, output_column)] = __float2bfloat16(0.0f); + } + return; + } + + float local_l = 0.0f; + for (int split = tid; split < active_split_count; split += blockDim.x) { + const float tile_l = + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)]; + if (tile_l > 0.0f) { + local_l += + tile_l * + expf(partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] - + head_m); + } + } + reduce[tid] = local_l; + __syncthreads(); + + for (int stride = blockDim.x / 2; stride > 0; stride >>= 1) { + if (tid < stride) { reduce[tid] += reduce[tid + stride]; } + __syncthreads(); + } + const float head_l = reduce[0]; + + const int d = d_start + tid; + if (tid >= DChunk || d >= kGqaHeadDim) { return; } + + float numerator = 0.0f; + if (head_l > 0.0f) { + for (int split = 0; split < active_split_count; ++split) { + const float tile_l = + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)]; + if (tile_l <= 0.0f) { continue; } + const float weight = expf( + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] - head_m); + numerator += + __bfloat162float( + partial_acc[gqa_partial_acc_index(q_head, d, token, split, tokens)]) * + weight; + } + } + bool valid = true; + if constexpr (Masked) { + int absolute_column = token; + if constexpr (Offset) { absolute_column += column_begin; } + valid = absolute_column < valid_columns[batch]; + } + const float value = (valid && head_l > 0.0f) ? numerator / head_l : 0.0f; + out[gqa_q_index(q_head, d, output_column)] = __float2bfloat16(value); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_decode_bf16.cuh b/src/ops/kernel/gqa_attention_decode_bf16.cuh new file mode 100644 index 0000000000..dfe04e018a --- /dev/null +++ b/src/ops/kernel/gqa_attention_decode_bf16.cuh @@ -0,0 +1,429 @@ +#pragma once + +// ninfer::ops - split-KV GQA small-T attention, BF16 KV-cache partial kernel. +// Standalone from the int8 kernel (gqa_attention_decode_i8.cuh): shared scaffolding +// lives in gqa_attention_decode.cuh, but the body/append/load are not shared so the +// bf16 path can be tuned independently. Processes one KV head, one query-head +// subgroup, and one token tile; a reducer combines the split-local partials. + +#include +#include + +#include "ops/kernel/gqa_attention_decode.cuh" + +#include + +namespace ninfer::ops { + +template +__launch_bounds__(128, 2) __global__ void gqa_attention_small_t_tc_partial_bf16_kernel( + const __nv_bfloat16* q, CacheInput input, const std::int32_t* pos, __nv_bfloat16* cache_k, + __nv_bfloat16* cache_v, const std::int32_t* block_tables, const std::int32_t* valid_columns, + const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t tokens, + std::int32_t full_width, std::int32_t column_begin, std::int32_t logical_capacity, float scale, + __nv_bfloat16* partial_acc, float* partial_m, float* partial_l) { + static_assert(TokenTile >= 1 && TokenTile <= 6); + static_assert(WarpsPerCta >= 1 && WarpsPerCta <= 4); + + constexpr int Wc = WarpsPerCta; + constexpr int Br = Wc * 16; + constexpr int Bc = 32; + constexpr int D = kGqaHeadDim; + constexpr int Threads = Wc * 32; + constexpr int QKNt = Bc / 8; + constexpr int QKKs = D / 16; + constexpr int PVNt = D / 8; + constexpr int PVKs = Bc / 16; + // The YaRN-extended 1,010,000-key maximum envelope spans at most 186 pages in one 27B split. + constexpr int PageIds = 256; + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + constexpr int QkvRows = 2 * Bc; + + static_assert(QkvRows >= Br); + + __shared__ __align__(16) __nv_bfloat16 qkv_s[QkvRows * D]; + __shared__ __align__(16) __nv_bfloat16 p_s[Wc * 16 * Bc]; + __shared__ std::int32_t physical_pages_s[PageIds]; + __nv_bfloat16* k_s = qkv_s; + __nv_bfloat16* v_s = qkv_s + Bc * D; + + const int kv_head = static_cast(blockIdx.x); + const int split = static_cast(blockIdx.y); + const int batch = MultiBatch ? static_cast(blockIdx.z) : 0; + const int split_count = static_cast(gridDim.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + int valid_tokens = tokens; + if constexpr (Masked) { + const int remaining = valid_columns[batch] - column_begin; + valid_tokens = remaining <= 0 ? 0 : (remaining < tokens ? remaining : tokens); + } + const int row_count = tokens * Geometry::GroupSize; + + std::int64_t column_base = column_begin; + if constexpr (MultiBatch) { column_base += static_cast(batch) * full_width; } + q += static_cast(kGqaHeadDim) * Geometry::QHeads * column_base; + pos += column_base; + if constexpr (CacheInput::writes_cache) { + input.k += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + input.v += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + } + const int table_row = table_rows == nullptr ? 0 : table_rows[batch]; + const std::int32_t* block_table = + block_tables + static_cast(table_row) * table_stride; + if constexpr (MultiBatch) { + partial_acc += static_cast(batch) * kGqaHeadDim * Geometry::QHeads * tokens * + split_count; + partial_m += static_cast(batch) * Geometry::QHeads * tokens * split_count; + partial_l += static_cast(batch) * Geometry::QHeads * tokens * split_count; + } + + auto write_neutral = [&]() { + for (int row = tid; row < row_count; row += Threads) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = + -CUDART_INF_F; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = 0.0f; + } + } + for (int idx = tid; idx < row_count * D; idx += Threads) { + const int row = idx / D; + const int d = idx - row * D; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_acc[gqa_partial_acc_index(q_head, d, token, split, tokens)] = + __float2bfloat16(0.0f); + } + } + }; + + if (kv_head < 0 || kv_head >= Geometry::KVHeads || tokens < 1 || tokens > TokenTile || + row_count > Br || split_count <= 0) { + return; + } + if (valid_tokens == 0) { + write_neutral(); + return; + } + + const std::int32_t first_pos = pos[0]; + const std::int32_t last_pos = pos[tokens - 1]; + if (first_pos < 0 || last_pos < 0 || last_pos >= logical_capacity) { + write_neutral(); + return; + } + + const int window = last_pos + 1; + const int active_split_count = + gqa_small_t_active_splits(window, split_count, TokenTile); + if (split >= active_split_count) { return; } + + const int logical_tiles = div_up(window, Bc); + const bool tile_split = logical_tiles >= active_split_count; + const int units_per_split = + tile_split ? div_up(logical_tiles, active_split_count) : div_up(window, active_split_count); + const int split_start = split * units_per_split * (tile_split ? Bc : 1); + const int split_limit = split_start + units_per_split * (tile_split ? Bc : 1); + const int split_end = (split_limit < window) ? split_limit : window; + if (split_start >= split_end) { + write_neutral(); + return; + } + const int first_tile = (split_start / Bc) * Bc; + const int key_blocks = div_up(split_end - first_tile, Bc); + const int first_page = first_tile >> kPagedKVPageShift; + const int page_count = ((split_end - 1) >> kPagedKVPageShift) - first_page + 1; + for (int page = tid; page < page_count; page += Threads) { + physical_pages_s[page] = block_table[first_page + page]; + } + + if constexpr (CacheInput::writes_cache) { + // The owning split writes each new row. Current attention reads those rows directly from + // input below, so no split depends on another split's cache write. + for (int chunk = tid; chunk < valid_tokens * (D / 8); chunk += Threads) { + const int token = chunk / (D / 8); + const int d = (chunk - token * (D / 8)) * 8; + const int p_tok = pos[token]; + if (p_tok >= split_start && p_tok < split_end && p_tok >= 0 && + p_tok < logical_capacity) { + const std::int64_t new_off = gqa_kv_new_index(kv_head, d, token); + const int lane = tid & 31; + int physical_page = lane == 0 ? paged_kv_physical_page(block_table, p_tok) : 0; + physical_page = __shfl_sync(FullMask, physical_page, 0); + const std::int64_t cache_off = + gqa_cache_index(physical_page, kv_head, d, p_tok & kPagedKVPageMask); + store_vec(&cache_k[cache_off], load_vec(&input.k[new_off])); + store_vec(&cache_v[cache_off], load_vec(&input.v[new_off])); + } + } + __syncthreads(); + } + + for (int idx = tid; idx < Br * D; idx += Threads) { + const int row = idx / D; + const int d = idx - row * D; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + __nv_bfloat16 value = __float2bfloat16(0.0f); + if (row < row_count && gqa_valid_q_head(kv_head, q_head)) { + value = q[gqa_q_index(q_head, d, token)]; + } + qkv_s[row * D + gqa_small_t_tc_swz(row, d)] = value; + } + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + + const int warp_row0 = warp * 16; + __nv_bfloat16* p_sw = &p_s[warp * 16 * Bc]; + + unsigned af_q[QKKs][4]; +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + const int arow = warp_row0 + a_rowoff; + const int acol = k * 16 + a_coloff; + ldmatrix_x4(af_q[k][0], af_q[k][1], af_q[k][2], af_q[k][3], + smem_addr(&qkv_s[arow * D + gqa_small_t_tc_swz(arow, acol)])); + } + __syncthreads(); + int physical_page = physical_pages_s[0]; + float acc[PVNt][4]; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F, l0 = 0.0f, l1 = 0.0f; + + for (int kb = 0; kb < key_blocks; ++kb) { + const int k0 = first_tile + kb * Bc; + if (kb != 0 && (k0 & kPagedKVPageMask) == 0) { + physical_page = physical_pages_s[(k0 >> kPagedKVPageShift) - first_page]; + } + // Stage the bf16 K/V key tile with one cp.async wave (16B/thread, high MLP). + // Current-step tokens come from k_new/v_new; tail slots are zeroed. +#pragma unroll 1 + for (int chunk = tid; chunk < Bc * (D / 8); chunk += Threads) { + const int key_l = chunk / (D / 8); + const int d = (chunk - key_l * (D / 8)) * 8; + const int key = k0 + key_l; + __nv_bfloat16* k_dst = &k_s[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + __nv_bfloat16* v_dst = &v_s[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + if (key >= split_start && key < split_end) { + if constexpr (CacheInput::writes_cache) { + const int new_token = key - first_pos; + const bool from_new = + new_token >= 0 && new_token < valid_tokens && key >= first_pos; + if (from_new) { + const std::int64_t off = gqa_kv_new_index(kv_head, d, new_token); + ninfer::ops::cp_async<16>(k_dst, &input.k[off]); + ninfer::ops::cp_async<16>(v_dst, &input.v[off]); + } else { + const std::int64_t off = gqa_cache_index( + physical_page, kv_head, d, key & kPagedKVPageMask); + ninfer::ops::cp_async<16>(k_dst, &cache_k[off]); + ninfer::ops::cp_async<16>(v_dst, &cache_v[off]); + } + } else { + const std::int64_t off = gqa_cache_index(physical_page, kv_head, d, + key & kPagedKVPageMask); + ninfer::ops::cp_async<16>(k_dst, &cache_k[off]); + ninfer::ops::cp_async<16>(v_dst, &cache_v[off]); + } + } else { + store_vec(k_dst, make_int4(0, 0, 0, 0)); + store_vec(v_dst, make_int4(0, 0, 0, 0)); + } + } + ninfer::ops::cp_commit(); + ninfer::ops::cp_wait<0>(); + __syncthreads(); + + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + unsigned bf[2]; + const int brow = nt * 8 + b_rin; + const int bcol = k * 16 + b_koff; + ldmatrix_x2(bf[0], bf[1], + smem_addr(&k_s[brow * D + gqa_small_t_tc_swz(brow, bcol)])); + mma_bf16(score[nt][0], score[nt][1], score[nt][2], score[nt][3], af_q[k][0], + af_q[k][1], af_q[k][2], af_q[k][3], bf[0], bf[1]); + } + } + + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + int q_head0 = 0, token0 = 0, q_head1 = 0, token1 = 0; + gqa_small_t_tc_row_to_qt(row0, tokens, kv_head, q_head0, token0); + gqa_small_t_tc_row_to_qt(row1, tokens, kv_head, q_head1, token1); + const int qabs0 = (row0 < row_count) ? pos[token0] : -1; + const int qabs1 = (row1 < row_count) ? pos[token1] : -1; + + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const int key0 = k0 + col0; + const int key1 = col1 + k0; + score[nt][0] = + (row0 < row_count && key0 >= split_start && key0 < split_end && key0 <= qabs0) + ? score[nt][0] * scale + : -CUDART_INF_F; + score[nt][1] = + (row0 < row_count && key1 >= split_start && key1 < split_end && key1 <= qabs0) + ? score[nt][1] * scale + : -CUDART_INF_F; + score[nt][2] = + (row1 < row_count && key0 >= split_start && key0 < split_end && key0 <= qabs1) + ? score[nt][2] * scale + : -CUDART_INF_F; + score[nt][3] = + (row1 < row_count && key1 >= split_start && key1 < split_end && key1 <= qabs1) + ? score[nt][3] * scale + : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float alpha0 = (m0 == -CUDART_INF_F) ? 0.0f : exp2_approx((m0 - nm0) * Log2E); + const float alpha1 = (m1 == -CUDART_INF_F) ? 0.0f : exp2_approx((m1 - nm1) * Log2E); + + float bl0 = 0.0f, bl1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const float p00 = (nm0 > -CUDART_INF_F && score[nt][0] > -CUDART_INF_F) + ? exp2_approx((score[nt][0] - nm0) * Log2E) + : 0.0f; + const float p01 = (nm0 > -CUDART_INF_F && score[nt][1] > -CUDART_INF_F) + ? exp2_approx((score[nt][1] - nm0) * Log2E) + : 0.0f; + const float p10 = (nm1 > -CUDART_INF_F && score[nt][2] > -CUDART_INF_F) + ? exp2_approx((score[nt][2] - nm1) * Log2E) + : 0.0f; + const float p11 = (nm1 > -CUDART_INF_F && score[nt][3] > -CUDART_INF_F) + ? exp2_approx((score[nt][3] - nm1) * Log2E) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col0)] = __float2bfloat16(p00); + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col1)] = __float2bfloat16(p01); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col0)] = __float2bfloat16(p10); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col1)] = __float2bfloat16(p11); + } + bl0 = warp_sum<4>(bl0, FullMask); + bl1 = warp_sum<4>(bl1, FullMask); + + l0 = l0 * alpha0 + bl0; + l1 = l1 * alpha1 + bl1; + m0 = nm0; + m1 = nm1; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + __syncwarp(); + +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int k = 0; k < PVKs; ++k) { + unsigned pf[4]; + const int pcol = k * 16 + a_coloff; + ldmatrix_x4(pf[0], pf[1], pf[2], pf[3], + smem_addr(&p_sw[a_rowoff * Bc + gqa_small_t_tc_swz32(a_rowoff, pcol)])); + unsigned vf[2]; + const int vrow = k * 16 + b_koff + b_rin; + const int vcol = n * 8; + ldmatrix_x2_t(vf[0], vf[1], + smem_addr(&v_s[vrow * D + gqa_small_t_tc_swz(vrow, vcol)])); + mma_bf16(acc[n][0], acc[n][1], acc[n][2], acc[n][3], pf[0], pf[1], pf[2], pf[3], + vf[0], vf[1]); + } + } + __syncthreads(); + } + + if (lid == 0) { + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < row_count) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, tokens, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = m0; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = l0; + } + if (row1 < row_count) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, tokens, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = m1; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = l1; + } + } + + // MMA fragments hold each row in four-lane groups. Stage the final split-local + // accumulator through shared memory so partial_acc is written as contiguous d-vector stores. +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + const int d0 = n * 8 + 2 * lid; + const int d1 = d0 + 1; + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < row_count) { + qkv_s[row0 * D + d0] = __float2bfloat16(acc[n][0]); + qkv_s[row0 * D + d1] = __float2bfloat16(acc[n][1]); + } + if (row1 < row_count) { + qkv_s[row1 * D + d0] = __float2bfloat16(acc[n][2]); + qkv_s[row1 * D + d1] = __float2bfloat16(acc[n][3]); + } + } + __syncthreads(); + + for (int chunk = tid; chunk < row_count * (D / 8); chunk += Threads) { + const int row = chunk / (D / 8); + const int d = (chunk - row * (D / 8)) * 8; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + const std::int64_t dst = + gqa_partial_acc_index(q_head, d, token, split, tokens); + store_vec(&partial_acc[dst], load_vec(&qkv_s[row * D + d])); + } + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_decode_fp8.cuh b/src/ops/kernel/gqa_attention_decode_fp8.cuh new file mode 100644 index 0000000000..3a4c9a3b4b --- /dev/null +++ b/src/ops/kernel/gqa_attention_decode_fp8.cuh @@ -0,0 +1,512 @@ +#pragma once + +// ninfer::ops - split-KV GQA small-T attention, FP8 E4M3FN KV-cache partial +// kernel. Kept as a separate file from the BF16 kernel so the FP8 cache path +// can be tuned independently. The QK/softmax/PV tensor-core body is copied +// byte-for-byte from gqa_attention_decode_bf16.cuh; only the cache append +// (BF16 -> rotated/quantized E4M3FN planes) and the K/V tile staging +// (E4M3FN -> BF16 qkv_s) differ. + +#include +#include + +#include "ops/kernel/gqa_attention_decode.cuh" +#include "ops/kernel/gqa_attention_kv_nvfp4.cuh" +#include "ops/kernel/gqa_isoquant_rot.cuh" +#include "ops/kernel/gqa_attention_prefill_nvfp4.cuh" // gqa_prefill_nvfp4_rot + +#include + +namespace ninfer::ops { + +template +__launch_bounds__(128, 2) __global__ void gqa_attention_small_t_tc_partial_fp8_kernel( + const __nv_bfloat16* q, CacheInput input, const std::int32_t* pos, + std::uint8_t* cache_k, std::uint8_t* cache_v, + std::uint8_t* cache_k_scale, std::uint8_t* cache_v_scale, + const std::int32_t* block_tables, const std::int32_t* valid_columns, + const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t tokens, + std::int32_t full_width, std::int32_t column_begin, std::int32_t logical_capacity, float scale, + __nv_bfloat16* partial_acc, float* partial_m, float* partial_l) { + static_assert(TokenTile >= 1 && TokenTile <= 6); + static_assert(WarpsPerCta >= 1 && WarpsPerCta <= 4); + + constexpr int Wc = WarpsPerCta; + constexpr int Br = Wc * 16; + constexpr int Bc = 32; + constexpr int D = kGqaHeadDim; + constexpr int Threads = Wc * 32; + constexpr int QKNt = Bc / 8; + constexpr int QKKs = D / 16; + constexpr int PVNt = D / 8; + constexpr int PVKs = Bc / 16; + // The YaRN-extended 1,010,000-key maximum envelope spans at most 186 pages in one 27B split. + constexpr int PageIds = 256; + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + constexpr int QkvRows = 2 * Bc; + + static_assert(QkvRows >= Br); + + __shared__ __align__(16) __nv_bfloat16 qkv_s[QkvRows * D]; + __shared__ __align__(16) __nv_bfloat16 p_s[Wc * 16 * Bc]; + __shared__ std::int32_t physical_pages_s[PageIds]; + __nv_bfloat16* k_s = qkv_s; + __nv_bfloat16* v_s = qkv_s + Bc * D; + + const int kv_head = static_cast(blockIdx.x); + const int split = static_cast(blockIdx.y); + const int batch = MultiBatch ? static_cast(blockIdx.z) : 0; + const int split_count = static_cast(gridDim.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + int valid_tokens = tokens; + if constexpr (Masked) { + const int remaining = valid_columns[batch] - column_begin; + valid_tokens = remaining <= 0 ? 0 : (remaining < tokens ? remaining : tokens); + } + const int row_count = tokens * Geometry::GroupSize; + + std::int64_t column_base = column_begin; + if constexpr (MultiBatch) { column_base += static_cast(batch) * full_width; } + q += static_cast(kGqaHeadDim) * Geometry::QHeads * column_base; + pos += column_base; + if constexpr (CacheInput::writes_cache) { + input.k += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + input.v += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + } + const int table_row = table_rows == nullptr ? 0 : table_rows[batch]; + const std::int32_t* block_table = + block_tables + static_cast(table_row) * table_stride; + if constexpr (MultiBatch) { + partial_acc += static_cast(batch) * kGqaHeadDim * Geometry::QHeads * tokens * + split_count; + partial_m += static_cast(batch) * Geometry::QHeads * tokens * split_count; + partial_l += static_cast(batch) * Geometry::QHeads * tokens * split_count; + } + + auto write_neutral = [&]() { + for (int row = tid; row < row_count; row += Threads) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = + -CUDART_INF_F; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = 0.0f; + } + } + for (int idx = tid; idx < row_count * D; idx += Threads) { + const int row = idx / D; + const int d = idx - row * D; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_acc[gqa_partial_acc_index(q_head, d, token, split, tokens)] = + __float2bfloat16(0.0f); + } + } + }; + + if (kv_head < 0 || kv_head >= Geometry::KVHeads || tokens < 1 || tokens > TokenTile || + row_count > Br || split_count <= 0) { + return; + } + if (valid_tokens == 0) { + write_neutral(); + return; + } + + const std::int32_t first_pos = pos[0]; + const std::int32_t last_pos = pos[tokens - 1]; + if (first_pos < 0 || last_pos < 0 || last_pos >= logical_capacity) { + write_neutral(); + return; + } + + const int window = last_pos + 1; + const int active_split_count = + gqa_small_t_active_splits(window, split_count, TokenTile); + if (split >= active_split_count) { return; } + + const int logical_tiles = div_up(window, Bc); + const bool tile_split = logical_tiles >= active_split_count; + const int units_per_split = + tile_split ? div_up(logical_tiles, active_split_count) : div_up(window, active_split_count); + const int split_start = split * units_per_split * (tile_split ? Bc : 1); + const int split_limit = split_start + units_per_split * (tile_split ? Bc : 1); + const int split_end = (split_limit < window) ? split_limit : window; + if (split_start >= split_end) { + write_neutral(); + return; + } + const int first_tile = (split_start / Bc) * Bc; + const int key_blocks = div_up(split_end - first_tile, Bc); + const int first_page = first_tile >> kPagedKVPageShift; + const int page_count = ((split_end - 1) >> kPagedKVPageShift) - first_page + 1; + for (int page = tid; page < page_count; page += Threads) { + physical_pages_s[page] = block_table[first_page + page]; + } + + if constexpr (CacheInput::writes_cache) { + // The owning split writes each new row into the quantized E4M3FN + // planes. K is rotated per 4-channel block before quantization; V is + // gain-only. The subsequent tile staging reads every key from the + // cache, so no split depends on another split's cache write. + constexpr int kFp8Groups = D / 16; + const int append_units = valid_tokens * kFp8Groups; + for (int unit = warp; unit < append_units; unit += WarpsPerCta) { + const int group = unit % kFp8Groups; + const int token = unit / kFp8Groups; + const int p_tok = pos[token]; + if (p_tok < split_start || p_tok >= split_end || p_tok < 0 || + p_tok >= logical_capacity) { + continue; + } + int physical_page = lane == 0 ? paged_kv_physical_page(block_table, p_tok) : 0; + physical_page = __shfl_sync(FullMask, physical_page, 0); + const int page_off = p_tok & kPagedKVPageMask; + + // K: rotate the four 4-channel blocks of this 16-group, then + // quantize with one shared E4M3FN scale for the group. + float kx[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { + const int block = group * 4 + lane; + const std::int64_t src = + gqa_kv_new_index(kv_head, group * 16, token) + lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { kx[j] = __bfloat162float(input.k[src + j]); } + const float y0 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 3); + kx[0] = y0; + kx[1] = y1; + kx[2] = y2; + kx[3] = y3; + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), + fmaxf(fabsf(kx[2]), fabsf(kx[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + kmax = fmaxf(kmax, __shfl_xor_sync(FullMask, kmax, off)); + } + const float kscale = fmaxf(kmax / 448.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t base = paged_kv_element_offset( + physical_page, kv_head, page_off, group * 16 + lane * 4); +#pragma unroll + for (int j = 0; j < 4; ++j) { + cache_k[base + j] = gqa_kv_nvfp4_fp32_to_e4m3(kx[j] / kscale); + } + } + if (lane == 0) { + cache_k_scale[gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + + // V: gain-only FP8 E4M3FN quantization, no rotation. + const float v0 = lane < 16 + ? __bfloat162float(input.v[gqa_kv_new_index( + kv_head, group * 16 + lane, token)]) + : 0.0f; + float vmax = fabsf(v0); +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + vmax = fmaxf(vmax, __shfl_xor_sync(FullMask, vmax, off)); + } + const float vscale = fmaxf(vmax / 448.0f, 0.001953125f); + if (lane < 16) { + const std::int64_t base = paged_kv_element_offset( + physical_page, kv_head, page_off, group * 16 + lane); + cache_v[base] = gqa_kv_nvfp4_fp32_to_e4m3(v0 / vscale); + } + if (lane == 0) { + cache_v_scale[gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } + } + __syncthreads(); + } + + for (int idx = tid; idx < Br * D; idx += Threads) { + const int row = idx / D; + const int d = idx - row * D; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + __nv_bfloat16 value = __float2bfloat16(0.0f); + if (row < row_count && gqa_valid_q_head(kv_head, q_head)) { + value = q[gqa_q_index(q_head, d, token)]; + } + qkv_s[row * D + gqa_small_t_tc_swz(row, d)] = value; + } + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + + const int warp_row0 = warp * 16; + __nv_bfloat16* p_sw = &p_s[warp * 16 * Bc]; + + unsigned af_q[QKKs][4]; +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + const int arow = warp_row0 + a_rowoff; + const int acol = k * 16 + a_coloff; + ldmatrix_x4(af_q[k][0], af_q[k][1], af_q[k][2], af_q[k][3], + smem_addr(&qkv_s[arow * D + gqa_small_t_tc_swz(arow, acol)])); + } + __syncthreads(); + int physical_page = physical_pages_s[0]; + float acc[PVNt][4]; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F, l0 = 0.0f, l1 = 0.0f; + + for (int kb = 0; kb < key_blocks; ++kb) { + const int k0 = first_tile + kb * Bc; + if (kb != 0 && (k0 & kPagedKVPageMask) == 0) { + physical_page = physical_pages_s[(k0 >> kPagedKVPageShift) - first_page]; + } + // Stage the FP8 E4M3FN K/V key tile synchronously into the swizzled + // BF16 qkv_s tile (K at offset 0, V at offset Bc*D). Each 8-element + // chunk shares one per-16-group scale; out-of-range rows store zero. +#pragma unroll 1 + for (int chunk = tid; chunk < Bc * (D / 8); chunk += Threads) { + const int key_l = chunk / (D / 8); + const int d = (chunk - key_l * (D / 8)) * 8; + const int key = k0 + key_l; + __nv_bfloat16* k_dst = &k_s[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + __nv_bfloat16* v_dst = &v_s[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + if (key >= split_start && key < split_end) { + const int group = d >> 4; + const int page_off = key & kPagedKVPageMask; + const float k_scale = gqa_kv_nvfp4_e4m3_to_f32(cache_k_scale[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, page_off)]); + const uint2 k_raw = load_vec(&cache_k[ + paged_kv_element_offset( + physical_page, kv_head, page_off, d)]); + const std::uint8_t* k_code = reinterpret_cast(&k_raw); + unsigned k_packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const float x0 = gqa_kv_nvfp4_e4m3_to_f32(k_code[2 * i]) * k_scale; + const float x1 = gqa_kv_nvfp4_e4m3_to_f32(k_code[2 * i + 1]) * k_scale; + k_packed[i] = pack_bf16x2(x0, x1); + } + store_vec(k_dst, make_int4(static_cast(k_packed[0]), + static_cast(k_packed[1]), + static_cast(k_packed[2]), + static_cast(k_packed[3]))); + + const float v_scale = gqa_kv_nvfp4_e4m3_to_f32(cache_v_scale[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, page_off)]); + const uint2 v_raw = load_vec(&cache_v[ + paged_kv_element_offset( + physical_page, kv_head, page_off, d)]); + const std::uint8_t* v_code = reinterpret_cast(&v_raw); + unsigned v_packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const float x0 = gqa_kv_nvfp4_e4m3_to_f32(v_code[2 * i]) * v_scale; + const float x1 = gqa_kv_nvfp4_e4m3_to_f32(v_code[2 * i + 1]) * v_scale; + v_packed[i] = pack_bf16x2(x0, x1); + } + store_vec(v_dst, make_int4(static_cast(v_packed[0]), + static_cast(v_packed[1]), + static_cast(v_packed[2]), + static_cast(v_packed[3]))); + } else { + store_vec(k_dst, make_int4(0, 0, 0, 0)); + store_vec(v_dst, make_int4(0, 0, 0, 0)); + } + } + __syncthreads(); + + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + unsigned bf[2]; + const int brow = nt * 8 + b_rin; + const int bcol = k * 16 + b_koff; + ldmatrix_x2(bf[0], bf[1], + smem_addr(&k_s[brow * D + gqa_small_t_tc_swz(brow, bcol)])); + mma_bf16(score[nt][0], score[nt][1], score[nt][2], score[nt][3], af_q[k][0], + af_q[k][1], af_q[k][2], af_q[k][3], bf[0], bf[1]); + } + } + + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + int q_head0 = 0, token0 = 0, q_head1 = 0, token1 = 0; + gqa_small_t_tc_row_to_qt(row0, tokens, kv_head, q_head0, token0); + gqa_small_t_tc_row_to_qt(row1, tokens, kv_head, q_head1, token1); + const int qabs0 = (row0 < row_count) ? pos[token0] : -1; + const int qabs1 = (row1 < row_count) ? pos[token1] : -1; + + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const int key0 = k0 + col0; + const int key1 = col1 + k0; + score[nt][0] = + (row0 < row_count && key0 >= split_start && key0 < split_end && key0 <= qabs0) + ? score[nt][0] * scale + : -CUDART_INF_F; + score[nt][1] = + (row0 < row_count && key1 >= split_start && key1 < split_end && key1 <= qabs0) + ? score[nt][1] * scale + : -CUDART_INF_F; + score[nt][2] = + (row1 < row_count && key0 >= split_start && key0 < split_end && key0 <= qabs1) + ? score[nt][2] * scale + : -CUDART_INF_F; + score[nt][3] = + (row1 < row_count && key1 >= split_start && key1 < split_end && key1 <= qabs1) + ? score[nt][3] * scale + : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float alpha0 = (m0 == -CUDART_INF_F) ? 0.0f : exp2_approx((m0 - nm0) * Log2E); + const float alpha1 = (m1 == -CUDART_INF_F) ? 0.0f : exp2_approx((m1 - nm1) * Log2E); + + float bl0 = 0.0f, bl1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const float p00 = (nm0 > -CUDART_INF_F && score[nt][0] > -CUDART_INF_F) + ? exp2_approx((score[nt][0] - nm0) * Log2E) + : 0.0f; + const float p01 = (nm0 > -CUDART_INF_F && score[nt][1] > -CUDART_INF_F) + ? exp2_approx((score[nt][1] - nm0) * Log2E) + : 0.0f; + const float p10 = (nm1 > -CUDART_INF_F && score[nt][2] > -CUDART_INF_F) + ? exp2_approx((score[nt][2] - nm1) * Log2E) + : 0.0f; + const float p11 = (nm1 > -CUDART_INF_F && score[nt][3] > -CUDART_INF_F) + ? exp2_approx((score[nt][3] - nm1) * Log2E) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col0)] = __float2bfloat16(p00); + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col1)] = __float2bfloat16(p01); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col0)] = __float2bfloat16(p10); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col1)] = __float2bfloat16(p11); + } + bl0 = warp_sum<4>(bl0, FullMask); + bl1 = warp_sum<4>(bl1, FullMask); + + l0 = l0 * alpha0 + bl0; + l1 = l1 * alpha1 + bl1; + m0 = nm0; + m1 = nm1; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + __syncwarp(); + +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int k = 0; k < PVKs; ++k) { + unsigned pf[4]; + const int pcol = k * 16 + a_coloff; + ldmatrix_x4(pf[0], pf[1], pf[2], pf[3], + smem_addr(&p_sw[a_rowoff * Bc + gqa_small_t_tc_swz32(a_rowoff, pcol)])); + unsigned vf[2]; + const int vrow = k * 16 + b_koff + b_rin; + const int vcol = n * 8; + ldmatrix_x2_t(vf[0], vf[1], + smem_addr(&v_s[vrow * D + gqa_small_t_tc_swz(vrow, vcol)])); + mma_bf16(acc[n][0], acc[n][1], acc[n][2], acc[n][3], pf[0], pf[1], pf[2], pf[3], + vf[0], vf[1]); + } + } + __syncthreads(); + } + + if (lid == 0) { + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < row_count) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, tokens, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = m0; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = l0; + } + if (row1 < row_count) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, tokens, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = m1; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = l1; + } + } + + // MMA fragments hold each row in four-lane groups. Stage the final split-local + // accumulator through shared memory so partial_acc is written as contiguous d-vector stores. +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + const int d0 = n * 8 + 2 * lid; + const int d1 = d0 + 1; + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < row_count) { + qkv_s[row0 * D + d0] = __float2bfloat16(acc[n][0]); + qkv_s[row0 * D + d1] = __float2bfloat16(acc[n][1]); + } + if (row1 < row_count) { + qkv_s[row1 * D + d0] = __float2bfloat16(acc[n][2]); + qkv_s[row1 * D + d1] = __float2bfloat16(acc[n][3]); + } + } + __syncthreads(); + + for (int chunk = tid; chunk < row_count * (D / 8); chunk += Threads) { + const int row = chunk / (D / 8); + const int d = (chunk - row * (D / 8)) * 8; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + const std::int64_t dst = + gqa_partial_acc_index(q_head, d, token, split, tokens); + store_vec(&partial_acc[dst], load_vec(&qkv_s[row * D + d])); + } + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_decode_i8.cuh b/src/ops/kernel/gqa_attention_decode_i8.cuh new file mode 100644 index 0000000000..dfb1d729e9 --- /dev/null +++ b/src/ops/kernel/gqa_attention_decode_i8.cuh @@ -0,0 +1,666 @@ +#pragma once + +// ninfer::ops - split-KV GQA small-T attention, int8 KV-cache partial kernel. +// Historical design: docs/archive/optimization-era/2026-07-08-gqa-decode-int8-kernel-redesign.md. +// +// * QK runs on native m16n8k32.s8 tensor cores. Q is quantized on-chip to int8 +// per (row, 64-group); K stays int8 in the cache and is read straight into +// smem (no dequant). The int32 MMA output is rescaled per 64-group by +// qs[row,g]*ks[key,g]. This halves the QK MMA count vs bf16 and removes the +// entire K dequant. +// * PV stays bf16 (V is quantized per key, so its scale cannot be factored out +// of a key-contracted int8 accumulation): V int8 is staged, dequanted once to +// a bf16 tile, then the existing bf16 PV MMA runs. V is still read from DRAM +// as int8, so the bandwidth win is kept. +// * All keys (history AND the current/diagonal tokens) are read from the +// quantized cache; the fused append writes the new tokens first and a +// __syncthreads orders the in-block readback. No from_new special-casing. +// +// Standalone from the bf16 kernel; shared scaffolding (layout constants, ldmatrix +// helpers, the s8/bf16 MMA helpers, the reducer) lives in gqa_attention_decode.cuh. + +#include +#include +#include + +#include "ops/kernel/gqa_attention_decode.cuh" +#include "ops/kernel/gqa_attention_kv_quant.cuh" +#include "ops/kernel/cold_i8_kernels.cuh" + +#include + +namespace ninfer::ops { + +// Store one int8 code into a d-contiguous-as-b16 swizzled tile so the same +// gqa_small_t_tc_swz / ldmatrix path that serves bf16 tiles serves the int8 tile. +// A b16 lane holds two packed int8 (d even = low byte, d odd = high byte); this +// matches the byte layout a 16 B cp.async of d-contiguous cache bytes produces +// (see the design doc / kernel comments), so Q (byte stores) and K (cp.async) +// agree. +__device__ __forceinline__ void gqa_small_t_i8_store_swz(std::int8_t* tile, int row, int d, + int d_b16_stride, std::int8_t code) { + const int c = d >> 1; + const int lo = d & 1; + const int off = (row * d_b16_stride + gqa_small_t_tc_swz(row, c)) * 2 + lo; + tile[off] = code; +} + +// Decode-specialized producer/consumer kernel for T=1..6. One producer warp per +// m16 row tile computes QK + online softmax, while all CTA warps partition the +// tile's 256-wide PV output. This keeps each thread's PV accumulator at 16, 32, +// or 64 floats instead of 128 and uses otherwise-idle warps for useful output +// work. +// +// Q has a dedicated shared tile so producers can reload one 64-dimension group +// at a time. K/V codes and scales are staged asynchronously; non-producer warps +// dequantize V while producers execute QK. After both consume the code tile, the +// next K/V tile is prefetched into the same arena while the current PV runs. +template +__launch_bounds__(WarpsPerCta * 32, MinBlocksPerSm) __global__ + void gqa_attention_decode_i8_tiled_kernel( + const __nv_bfloat16* q, CacheInput input, const std::int32_t* pos, std::int8_t* cache_k_i8, + std::int8_t* cache_v_i8, __half* cache_k_scale, __half* cache_v_scale, + const std::uint8_t* cold_k_slots, const std::uint8_t* cold_v_slots, int slot_bytes, + const std::int32_t* block_tables, const std::int32_t* valid_columns, + const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t full_width, + std::int32_t column_begin, std::int32_t logical_capacity, float scale, + __nv_bfloat16* partial_acc, float* partial_m, float* partial_l) { + constexpr int Wc = WarpsPerCta; + constexpr int RowCount = TokenTile * Geometry::GroupSize; + constexpr int RowTiles = (RowCount + 15) / 16; + constexpr int Br = RowTiles * 16; + constexpr int Bc = KeyBlock; + constexpr int D = kGqaHeadDim; + constexpr int DB16 = D / 2; + constexpr int Threads = Wc * 32; + constexpr int Groups = kGqaKvQuantGroups; + constexpr int GroupKc = kGqaKvQuantGroup / 32; + constexpr int QKKs = D / 32; + constexpr int QKNt = Bc / 8; + constexpr int ConsumerWarpsPerTile = Wc / RowTiles; + constexpr int PVNtPerWarp = D / (ConsumerWarpsPerTile * 8); + constexpr int PVKs = Bc / 16; + // The YaRN-extended 1,010,000-key maximum envelope spans at most 186 pages in one 27B split. + constexpr int PageIds = 256; + constexpr int ProducerThreads = RowTiles * 32; + constexpr int VLoaderThreads = Threads - ProducerThreads; + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + + static_assert(TokenTile >= 1 && TokenTile <= 6); + static_assert(Bc == 32 || Bc == 64); + static_assert(RowTiles >= 1 && RowTiles <= 3); + static_assert(Wc % RowTiles == 0); + static_assert(PVNtPerWarp == 2 || PVNtPerWarp == 4 || PVNtPerWarp == 8 || PVNtPerWarp == 16); + static_assert(QKKs == Groups * GroupKc); + + // Keep Q in a compact dedicated tile so the producer can reload one + // 64-dimension group at a time instead of carrying all eight fragments in + // registers across the whole kernel. The main arena holds K i8, V i8, and + // V bf16 during the key loop. + __shared__ __align__(16) std::int8_t q_s[Br * D]; + __shared__ __align__(16) std::int8_t static_r_s[DynamicArena ? 16 : 4 * Bc * D]; + extern __shared__ __align__(16) std::int8_t dynamic_r_s[]; + std::int8_t* r_s = DynamicArena ? dynamic_r_s : static_r_s; + std::int8_t* q_i8 = q_s; + float* q_scale_tmp = reinterpret_cast(r_s); + std::int8_t* k_i8 = r_s; + __nv_bfloat16* q_b16 = reinterpret_cast<__nv_bfloat16*>(q_i8); + __nv_bfloat16* k_b16 = reinterpret_cast<__nv_bfloat16*>(k_i8); + std::int8_t* v_i8 = r_s + Bc * D; + __nv_bfloat16* v_bf16 = reinterpret_cast<__nv_bfloat16*>(r_s + 2 * Bc * D); + __shared__ __align__(16) __nv_bfloat16 p_s[Br * Bc]; + __shared__ float alpha_s[Br]; + __shared__ __align__(16) __half k_scale_s[Bc * Groups]; + __shared__ __align__(16) __half v_scale_s[Bc * Groups]; + __shared__ std::int32_t physical_pages_s[PageIds]; + + const int kv_head = static_cast(blockIdx.x); + const int split = static_cast(blockIdx.y); + const int batch = MultiBatch ? static_cast(blockIdx.z) : 0; + const int split_count = static_cast(gridDim.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + + int valid_tokens = TokenTile; + if constexpr (Masked) { + const int remaining = valid_columns[batch] - column_begin; + valid_tokens = remaining <= 0 ? 0 : (remaining < TokenTile ? remaining : TokenTile); + } + std::int64_t column_base = column_begin; + if constexpr (MultiBatch) { column_base += static_cast(batch) * full_width; } + q += static_cast(kGqaHeadDim) * Geometry::QHeads * column_base; + pos += column_base; + if constexpr (CacheInput::writes_cache) { + input.k += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + input.v += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + } + const int table_row = table_rows == nullptr ? 0 : table_rows[batch]; + const std::int32_t* block_table = + block_tables + static_cast(table_row) * table_stride; + if constexpr (MultiBatch) { + partial_acc += static_cast(batch) * kGqaHeadDim * Geometry::QHeads * + TokenTile * split_count; + partial_m += static_cast(batch) * Geometry::QHeads * TokenTile * split_count; + partial_l += static_cast(batch) * Geometry::QHeads * TokenTile * split_count; + } + + auto write_neutral = [&]() { + for (int row = tid; row < RowCount; row += Threads) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, TokenTile, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_m[gqa_partial_stat_index(q_head, token, split, TokenTile)] = + -CUDART_INF_F; + partial_l[gqa_partial_stat_index(q_head, token, split, TokenTile)] = 0.0f; + } + } + for (int idx = tid; idx < RowCount * D; idx += Threads) { + const int row = idx / D; + const int d = idx - row * D; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, TokenTile, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_acc[gqa_partial_acc_index(q_head, d, token, split, TokenTile)] = + __float2bfloat16(0.0f); + } + } + }; + + if (kv_head < 0 || kv_head >= Geometry::KVHeads || split_count <= 0) { return; } + if (valid_tokens == 0) { + write_neutral(); + return; + } + + const std::int32_t first_pos = pos[0]; + const std::int32_t last_pos = pos[TokenTile - 1]; + if (first_pos < 0 || last_pos < 0 || last_pos >= logical_capacity) { + write_neutral(); + return; + } + + const int window = last_pos + 1; + const int active_split_count = + gqa_small_t_active_splits(window, split_count, TokenTile); + if (split >= active_split_count) { return; } + + const int logical_tiles = div_up(window, Bc); + const bool tile_split = logical_tiles >= active_split_count; + const int units_per_split = + tile_split ? div_up(logical_tiles, active_split_count) : div_up(window, active_split_count); + const int split_start = split * units_per_split * (tile_split ? Bc : 1); + const int split_limit = split_start + units_per_split * (tile_split ? Bc : 1); + const int split_end = (split_limit < window) ? split_limit : window; + if (split_start >= split_end) { + write_neutral(); + return; + } + const int first_tile = (split_start / Bc) * Bc; + const int key_blocks = div_up(split_end - first_tile, Bc); + const int first_page = first_tile >> kPagedKVPageShift; + const int page_count = ((split_end - 1) >> kPagedKVPageShift) - first_page + 1; + for (int page = tid; page < page_count; page += Threads) { + physical_pages_s[page] = block_table[first_page + page]; + } + + if constexpr (CacheInput::writes_cache) { + // The owning split quantizes each current row before its cache tile is consumed. + for (int pair = warp; pair < valid_tokens * Groups; pair += Wc) { + const int token = pair / Groups; + const int grp = pair - token * Groups; + const int position = pos[token]; + if (position < split_start || position >= split_end) { continue; } + int physical_page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + const int page_offset = position & kPagedKVPageMask; + const int d0 = grp * kGqaKvQuantGroup + lane; + const int d1 = d0 + 32; + const std::int64_t src0 = gqa_kv_new_index(kv_head, d0, token); + const std::int64_t src1 = gqa_kv_new_index(kv_head, d1, token); + const float kv0 = __bfloat162float(input.k[src0]); + const float kv1 = __bfloat162float(input.k[src1]); + const float vv0 = __bfloat162float(input.v[src0]); + const float vv1 = __bfloat162float(input.v[src1]); + float kamax = fmaxf(fabsf(kv0), fabsf(kv1)); + float vamax = fmaxf(fabsf(vv0), fabsf(vv1)); + kamax = warp_max(kamax, FullMask); + vamax = warp_max(vamax, FullMask); + const __half ksh = __float2half_rn(kamax > 0.0f ? kamax / 127.0f : 0.0f); + const __half vsh = __float2half_rn(vamax > 0.0f ? vamax / 127.0f : 0.0f); + const float ks = __half2float(ksh); + const float vs = __half2float(vsh); + const float k_inv = ks > 0.0f ? 1.0f / ks : 0.0f; + const float v_inv = vs > 0.0f ? 1.0f / vs : 0.0f; + physical_page = __shfl_sync(FullMask, physical_page, 0); + cache_k_i8[gqa_kv_quant_code_index(physical_page, kv_head, d0, page_offset)] = + gqa_kv_quant_code(kv0, k_inv); + cache_k_i8[gqa_kv_quant_code_index(physical_page, kv_head, d1, page_offset)] = + gqa_kv_quant_code(kv1, k_inv); + cache_v_i8[gqa_kv_quant_code_index(physical_page, kv_head, d0, page_offset)] = + gqa_kv_quant_code(vv0, v_inv); + cache_v_i8[gqa_kv_quant_code_index(physical_page, kv_head, d1, page_offset)] = + gqa_kv_quant_code(vv1, v_inv); + if (lane == 0) { + const std::int64_t so = + gqa_kv_quant_scale_index(physical_page, kv_head, grp, page_offset); + cache_k_scale[so] = ksh; + cache_v_scale[so] = vsh; + } + } + __syncthreads(); + } + + for (int i = tid; i < Br * D; i += Threads) { q_i8[i] = 0; } + for (int i = tid; i < RowCount * Groups; i += Threads) { q_scale_tmp[i] = 0.0f; } + __syncthreads(); + + for (int unit = warp; unit < RowCount * Groups; unit += Wc) { + const int row = unit / Groups; + const int grp = unit - row * Groups; + const int d0 = grp * kGqaKvQuantGroup + lane; + const int d1 = d0 + 32; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, TokenTile, kv_head, q_head, token); + const float x0 = __bfloat162float(q[gqa_q_index(q_head, d0, token)]); + const float x1 = __bfloat162float(q[gqa_q_index(q_head, d1, token)]); + float amax = fmaxf(fabsf(x0), fabsf(x1)); + amax = warp_max(amax, FullMask); + const float qs = amax > 0.0f ? amax / 127.0f : 0.0f; + const float inv = qs > 0.0f ? 1.0f / qs : 0.0f; + gqa_small_t_i8_store_swz(q_i8, row, d0, DB16, gqa_kv_quant_code(x0, inv)); + gqa_small_t_i8_store_swz(q_i8, row, d1, DB16, gqa_kv_quant_code(x1, inv)); + if (lane == 0) { q_scale_tmp[row * Groups + grp] = qs; } + } + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + + float q_scale_r0[Groups]; + float q_scale_r1[Groups]; + if (warp < RowTiles) { + const int producer_row0 = warp * 16 + gid; +#pragma unroll + for (int g = 0; g < Groups; ++g) { + float qs0 = (lid == 0 && producer_row0 < RowCount) + ? q_scale_tmp[producer_row0 * Groups + g] + : 0.0f; + float qs1 = (lid == 0 && producer_row0 + 8 < RowCount) + ? q_scale_tmp[(producer_row0 + 8) * Groups + g] + : 0.0f; + q_scale_r0[g] = __shfl_sync(FullMask, qs0, gid * 4); + q_scale_r1[g] = __shfl_sync(FullMask, qs1, gid * 4); + } + } + __syncthreads(); + + float acc[PVNtPerWarp][4]; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F; + float l0 = 0.0f, l1 = 0.0f; + + auto issue_kv_tile = [&](int tile_k0, int physical_page) { + // Revision 2b cold staging: raw nibble slots hold g64-requantized + // E2M1 codes + E4M3 g16 scales; decode each key row straight into the + // kernel's native int8 codes + fp16 group scales so the QK tensor-core + // path runs unchanged. Slot addressing is region-relative: flat index + // slot * 2*KVHeads + head for both the K and V slot regions. + if (physical_page <= -2 && cold_k_slots != nullptr && + slot_bytes >= ninfer::ops::kColdI8SlotBytes) { + const std::int64_t slot_flat = + static_cast(-physical_page - 2) * (2 * Geometry::KVHeads) + + kv_head; + const std::uint8_t* k_slot = + cold_k_slots + slot_flat * slot_bytes; + const std::uint8_t* v_slot = + cold_v_slots == nullptr ? nullptr + : cold_v_slots + slot_flat * slot_bytes; + for (int key_l = tid; key_l < Bc; key_l += Threads) { + const int key = tile_k0 + key_l; + if (key >= split_start && key < split_end && v_slot != nullptr) { + const int row = key & kPagedKVPageMask; + std::int8_t row_codes[kGqaHeadDim]; + __half row_scales[kGqaKvQuantGroups]; + ninfer::ops::detail::cold_i8_decode_row(k_slot, row, row_codes, row_scales); +#pragma unroll 8 + for (int d = 0; d < kGqaHeadDim; ++d) { + ninfer::ops::gqa_small_t_i8_store_swz(k_i8, key_l, d, + kGqaHeadDim / 2, row_codes[d]); + } +#pragma unroll + for (int g = 0; g < Groups; ++g) { + k_scale_s[key_l * Groups + g] = row_scales[g]; + } + ninfer::ops::detail::cold_i8_decode_row(v_slot, row, row_codes, row_scales); +#pragma unroll 8 + for (int d = 0; d < kGqaHeadDim; ++d) { + v_i8[key_l * kGqaHeadDim + d] = row_codes[d]; + } +#pragma unroll + for (int g = 0; g < Groups; ++g) { + v_scale_s[key_l * Groups + g] = row_scales[g]; + } + } else { +#pragma unroll 1 + for (int dc = 0; dc < kGqaHeadDim / 16; ++dc) { + std::int8_t* dst = &k_i8[key_l * kGqaHeadDim + + gqa_small_t_tc_swz(key_l, dc * 8) * 2]; + ninfer::ops::store_vec(dst, make_int4(0, 0, 0, 0)); + ninfer::ops::store_vec(&v_i8[key_l * kGqaHeadDim + dc * 16], + make_int4(0, 0, 0, 0)); + } + ninfer::ops::store_vec(&k_scale_s[key_l * Groups], make_int2(0, 0)); + ninfer::ops::store_vec(&v_scale_s[key_l * Groups], make_int2(0, 0)); + } + } + ninfer::ops::cp_commit(); + return; + } + for (int key_l = tid; key_l < Bc; key_l += Threads) { + const int key = tile_k0 + key_l; + if (key >= split_start && key < split_end) { + const std::int64_t off = gqa_kv_quant_scale_index( + physical_page, kv_head, 0, key & kPagedKVPageMask); + ninfer::ops::cp_async<8>(&k_scale_s[key_l * Groups], &cache_k_scale[off]); + ninfer::ops::cp_async<8>(&v_scale_s[key_l * Groups], &cache_v_scale[off]); + } else { + store_vec(&k_scale_s[key_l * Groups], make_int2(0, 0)); + store_vec(&v_scale_s[key_l * Groups], make_int2(0, 0)); + } + } +#pragma unroll 1 + for (int chunk = tid; chunk < Bc * (D / 16); chunk += Threads) { + const int key_l = chunk / (D / 16); + const int dc = chunk - key_l * (D / 16); + const int d = dc * 16; + const int key = tile_k0 + key_l; + if (key >= split_start && key < split_end) { + const std::int64_t off = gqa_kv_quant_code_index( + physical_page, kv_head, d, key & kPagedKVPageMask); + std::int8_t* dst = &k_i8[key_l * D + gqa_small_t_tc_swz(key_l, dc * 8) * 2]; + ninfer::ops::cp_async<16>(dst, &cache_k_i8[off]); + ninfer::ops::cp_async<16>(&v_i8[key_l * D + d], &cache_v_i8[off]); + } else { + std::int8_t* dst = &k_i8[key_l * D + gqa_small_t_tc_swz(key_l, dc * 8) * 2]; + store_vec(dst, make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + d], make_int4(0, 0, 0, 0)); + } + } + ninfer::ops::cp_commit(); + }; + + int physical_page = physical_pages_s[0]; + issue_kv_tile(first_tile, physical_page); + ninfer::ops::cp_wait<0>(); + __syncthreads(); + + for (int kb = 0; kb < key_blocks; ++kb) { + const int k0 = first_tile + kb * Bc; + + // One warp per row tile produces P and alpha while the remaining warps + // stream/dequant V. + if (warp < RowTiles) { + const int producer_row_base = warp * 16; + __nv_bfloat16* p_sw = &p_s[producer_row_base * Bc]; + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = 0.0f; + score[nt][1] = 0.0f; + score[nt][2] = 0.0f; + score[nt][3] = 0.0f; + } + +#pragma unroll + for (int g = 0; g < Groups; ++g) { + unsigned af[GroupKc][4]; +#pragma unroll + for (int kk = 0; kk < GroupKc; ++kk) { + const int k = g * GroupKc + kk; + const int acol = k * 16 + a_coloff; + ldmatrix_x4( + af[kk][0], af[kk][1], af[kk][2], af[kk][3], + smem_addr(&q_b16[(producer_row_base + a_rowoff) * DB16 + + gqa_small_t_tc_swz(producer_row_base + a_rowoff, acol)])); + } + +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + int c0 = 0, c1 = 0, c2 = 0, c3 = 0; +#pragma unroll + for (int kk = 0; kk < GroupKc; ++kk) { + const int k = g * GroupKc + kk; + const int brow = nt * 8 + b_rin; + const int bcol = k * 16 + b_koff; + unsigned bf[2]; + ldmatrix_x2( + bf[0], bf[1], + smem_addr(&k_b16[brow * DB16 + gqa_small_t_tc_swz(brow, bcol)])); + mma_s8(c0, c1, c2, c3, af[kk][0], af[kk][1], af[kk][2], af[kk][3], bf[0], + bf[1]); + } + const int keya = nt * 8 + 2 * lid; + const int keyb = keya + 1; + float ka = 0.0f; + float kb2 = 0.0f; + if (gid == 0) { + ka = __half2float(k_scale_s[keya * Groups + g]); + kb2 = __half2float(k_scale_s[keyb * Groups + g]); + } + ka = __shfl_sync(FullMask, ka, lid); + kb2 = __shfl_sync(FullMask, kb2, lid); + score[nt][0] += q_scale_r0[g] * ka * static_cast(c0); + score[nt][1] += q_scale_r0[g] * kb2 * static_cast(c1); + score[nt][2] += q_scale_r1[g] * ka * static_cast(c2); + score[nt][3] += q_scale_r1[g] * kb2 * static_cast(c3); + } + } + + const int row0 = producer_row_base + gid; + const int row1 = row0 + 8; + int q_head0 = 0, token0 = 0, q_head1 = 0, token1 = 0; + gqa_small_t_tc_row_to_qt(row0, TokenTile, kv_head, q_head0, token0); + gqa_small_t_tc_row_to_qt(row1, TokenTile, kv_head, q_head1, token1); + const int qabs0 = (row0 < RowCount) ? pos[token0] : -1; + const int qabs1 = (row1 < RowCount) ? pos[token1] : -1; + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const int key0 = k0 + col0; + const int key1 = k0 + col1; + score[nt][0] = + (row0 < RowCount && key0 >= split_start && key0 < split_end && key0 <= qabs0) + ? score[nt][0] * scale + : -CUDART_INF_F; + score[nt][1] = + (row0 < RowCount && key1 >= split_start && key1 < split_end && key1 <= qabs0) + ? score[nt][1] * scale + : -CUDART_INF_F; + score[nt][2] = + (row1 < RowCount && key0 >= split_start && key0 < split_end && key0 <= qabs1) + ? score[nt][2] * scale + : -CUDART_INF_F; + score[nt][3] = + (row1 < RowCount && key1 >= split_start && key1 < split_end && key1 <= qabs1) + ? score[nt][3] * scale + : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float alpha0 = (m0 == -CUDART_INF_F) ? 0.0f : exp2_approx((m0 - nm0) * Log2E); + const float alpha1 = (m1 == -CUDART_INF_F) ? 0.0f : exp2_approx((m1 - nm1) * Log2E); + + float bl0 = 0.0f, bl1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const float p00 = (nm0 > -CUDART_INF_F && score[nt][0] > -CUDART_INF_F) + ? exp2_approx((score[nt][0] - nm0) * Log2E) + : 0.0f; + const float p01 = (nm0 > -CUDART_INF_F && score[nt][1] > -CUDART_INF_F) + ? exp2_approx((score[nt][1] - nm0) * Log2E) + : 0.0f; + const float p10 = (nm1 > -CUDART_INF_F && score[nt][2] > -CUDART_INF_F) + ? exp2_approx((score[nt][2] - nm1) * Log2E) + : 0.0f; + const float p11 = (nm1 > -CUDART_INF_F && score[nt][3] > -CUDART_INF_F) + ? exp2_approx((score[nt][3] - nm1) * Log2E) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col0)] = __float2bfloat16(p00); + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col1)] = __float2bfloat16(p01); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col0)] = __float2bfloat16(p10); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col1)] = __float2bfloat16(p11); + } + bl0 = warp_sum<4>(bl0, FullMask); + bl1 = warp_sum<4>(bl1, FullMask); + + l0 = l0 * alpha0 + bl0; + l1 = l1 * alpha1 + bl1; + m0 = nm0; + m1 = nm1; + if (lid == 0) { + alpha_s[row0] = alpha0; + alpha_s[row1] = alpha1; + } + } else { + const int loader_tid = tid - ProducerThreads; +#pragma unroll 1 + for (int chunk = loader_tid; chunk < Bc * (D / 8); chunk += VLoaderThreads) { + const int key_l = chunk / (D / 8); + const int dc = chunk - key_l * (D / 8); + const int d = dc * 8; + const int key = k0 + key_l; + __nv_bfloat16* dst = &v_bf16[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + if (key >= split_start && key < split_end) { + const int grp = d >> 6; + float vs = 0.0f; + if ((lane & 7) == 0) { vs = __half2float(v_scale_s[key_l * Groups + grp]); } + vs = __shfl_sync(FullMask, vs, grp * 8); + store_vec(dst, gqa_kv_dequant_i8x8_from(&v_i8[key_l * D + d], vs)); + } else { + store_vec(dst, make_int4(0, 0, 0, 0)); + } + } + } + __syncthreads(); + + const bool has_next = kb + 1 < key_blocks; + if (has_next) { + const int next_k0 = k0 + Bc; + if ((next_k0 & kPagedKVPageMask) == 0) { + physical_page = physical_pages_s[(next_k0 >> kPagedKVPageShift) - first_page]; + } + issue_kv_tile(next_k0, physical_page); + } + + const int consumer_tile = warp % RowTiles; + const int consumer_slice = warp / RowTiles; + const int consumer_row_base = consumer_tile * 16; + __nv_bfloat16* p_consumer = &p_s[consumer_row_base * Bc]; + const float alpha0 = alpha_s[consumer_row_base + gid]; + const float alpha1 = alpha_s[consumer_row_base + gid + 8]; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + const int global_n = consumer_slice * PVNtPerWarp + n; +#pragma unroll + for (int k = 0; k < PVKs; ++k) { + unsigned pf[4]; + const int pcol = k * 16 + a_coloff; + ldmatrix_x4( + pf[0], pf[1], pf[2], pf[3], + smem_addr(&p_consumer[a_rowoff * Bc + gqa_small_t_tc_swz32(a_rowoff, pcol)])); + unsigned vf[2]; + const int vrow = k * 16 + b_koff + b_rin; + const int vcol = global_n * 8; + ldmatrix_x2_t(vf[0], vf[1], + smem_addr(&v_bf16[vrow * D + gqa_small_t_tc_swz(vrow, vcol)])); + mma_bf16(acc[n][0], acc[n][1], acc[n][2], acc[n][3], pf[0], pf[1], pf[2], pf[3], + vf[0], vf[1]); + } + } + if (has_next) { ninfer::ops::cp_wait<0>(); } + __syncthreads(); + } + + if (warp < RowTiles && lid == 0) { + const int row0 = warp * 16 + gid; + const int row1 = row0 + 8; + if (row0 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, TokenTile, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, TokenTile)] = m0; + partial_l[gqa_partial_stat_index(q_head, token, split, TokenTile)] = l0; + } + if (row1 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, TokenTile, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, TokenTile)] = m1; + partial_l[gqa_partial_stat_index(q_head, token, split, TokenTile)] = l1; + } + } + +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + const int consumer_tile = warp % RowTiles; + const int consumer_slice = warp / RowTiles; + const int consumer_row_base = consumer_tile * 16; + const int d0 = (consumer_slice * PVNtPerWarp + n) * 8 + 2 * lid; + const int row0 = consumer_row_base + gid; + const int row1 = row0 + 8; + if (row0 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, TokenTile, kv_head, q_head, token); + const std::int64_t dst = + gqa_partial_acc_index(q_head, d0, token, split, TokenTile); + *reinterpret_cast(&partial_acc[dst]) = pack_bf16x2(acc[n][0], acc[n][1]); + } + if (row1 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, TokenTile, kv_head, q_head, token); + const std::int64_t dst = + gqa_partial_acc_index(q_head, d0, token, split, TokenTile); + *reinterpret_cast(&partial_acc[dst]) = pack_bf16x2(acc[n][2], acc[n][3]); + } + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_decode_iso3.cuh b/src/ops/kernel/gqa_attention_decode_iso3.cuh new file mode 100644 index 0000000000..021517df3a --- /dev/null +++ b/src/ops/kernel/gqa_attention_decode_iso3.cuh @@ -0,0 +1,569 @@ +#pragma once + +// ninfer::ops - split-KV GQA small-T attention, ISO3 KV-cache partial kernel. +// Kept as a separate file from the BF16 kernel so the ISO3 cache path can be +// tuned independently. The QK/softmax/PV tensor-core body is copied +// byte-for-byte from gqa_attention_decode_bf16.cuh; only the cache append +// (BF16 -> rotated/quantized sign-magnitude INT3 nibbles) and the K/V tile +// staging (packed nibbles -> BF16 qkv_s) differ. + +#include +#include + +#include "ops/kernel/gqa_attention_decode.cuh" +#include "ops/kernel/gqa_attention_kv_nvfp4.cuh" +#include "ops/kernel/gqa_isoquant_rot.cuh" +#include "ops/kernel/gqa_attention_prefill_nvfp4.cuh" // gqa_prefill_nvfp4_rot, gqa_iso3_nibble + +#include + +namespace ninfer::ops { + +template +__launch_bounds__(128, 2) __global__ void gqa_attention_small_t_tc_partial_iso3_kernel( + const __nv_bfloat16* q, CacheInput input, const std::int32_t* pos, + std::uint8_t* cache_k, std::uint8_t* cache_v, + std::uint8_t* cache_k_scale, std::uint8_t* cache_v_scale, + const std::int32_t* block_tables, const std::int32_t* valid_columns, + const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t tokens, + std::int32_t full_width, std::int32_t column_begin, std::int32_t logical_capacity, + int sliding_window, float scale, + __nv_bfloat16* partial_acc, float* partial_m, float* partial_l) { + static_assert(TokenTile >= 1 && TokenTile <= 6); + static_assert(WarpsPerCta >= 1 && WarpsPerCta <= 4); + + constexpr int Wc = WarpsPerCta; + constexpr int Br = Wc * 16; + constexpr int Bc = 32; + constexpr int D = kGqaHeadDim; + constexpr int Threads = Wc * 32; + constexpr int QKNt = Bc / 8; + constexpr int QKKs = D / 16; + constexpr int PVNt = D / 8; + constexpr int PVKs = Bc / 16; + // The YaRN-extended 1,010,000-key maximum envelope spans at most 186 pages in one 27B split. + constexpr int PageIds = 256; + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + constexpr int QkvRows = 2 * Bc; + + static_assert(QkvRows >= Br); + + __shared__ __align__(16) __nv_bfloat16 qkv_s[QkvRows * D]; + __shared__ __align__(16) __nv_bfloat16 p_s[Wc * 16 * Bc]; + __shared__ std::int32_t physical_pages_s[PageIds]; + __nv_bfloat16* k_s = qkv_s; + __nv_bfloat16* v_s = qkv_s + Bc * D; + + const int kv_head = static_cast(blockIdx.x); + const int split = static_cast(blockIdx.y); + const int batch = MultiBatch ? static_cast(blockIdx.z) : 0; + const int split_count = static_cast(gridDim.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + int valid_tokens = tokens; + if constexpr (Masked) { + const int remaining = valid_columns[batch] - column_begin; + valid_tokens = remaining <= 0 ? 0 : (remaining < tokens ? remaining : tokens); + } + const int row_count = tokens * Geometry::GroupSize; + + std::int64_t column_base = column_begin; + if constexpr (MultiBatch) { column_base += static_cast(batch) * full_width; } + q += static_cast(kGqaHeadDim) * Geometry::QHeads * column_base; + pos += column_base; + if constexpr (CacheInput::writes_cache) { + input.k += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + input.v += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + } + const int table_row = table_rows == nullptr ? 0 : table_rows[batch]; + const std::int32_t* block_table = + block_tables + static_cast(table_row) * table_stride; + if constexpr (MultiBatch) { + partial_acc += static_cast(batch) * kGqaHeadDim * Geometry::QHeads * tokens * + split_count; + partial_m += static_cast(batch) * Geometry::QHeads * tokens * split_count; + partial_l += static_cast(batch) * Geometry::QHeads * tokens * split_count; + } + + auto write_neutral = [&]() { + for (int row = tid; row < row_count; row += Threads) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = + -CUDART_INF_F; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = 0.0f; + } + } + for (int idx = tid; idx < row_count * D; idx += Threads) { + const int row = idx / D; + const int d = idx - row * D; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + partial_acc[gqa_partial_acc_index(q_head, d, token, split, tokens)] = + __float2bfloat16(0.0f); + } + } + }; + + if (kv_head < 0 || kv_head >= Geometry::KVHeads || tokens < 1 || tokens > TokenTile || + row_count > Br || split_count <= 0) { + return; + } + if (valid_tokens == 0) { + write_neutral(); + return; + } + + const std::int32_t first_pos = pos[0]; + const std::int32_t last_pos = pos[tokens - 1]; + if (first_pos < 0 || last_pos < 0 || last_pos >= logical_capacity) { + write_neutral(); + return; + } + + const int window_full = last_pos + 1; + const int token_begin = (sliding_window > 0) ? window_full - sliding_window : 0; + const int window_begin = + (sliding_window > 0) ? ((max(0, token_begin) + Bc - 1) / Bc) * Bc : 0; + const int window = window_full - window_begin; + const int active_split_count = + gqa_small_t_active_splits(window, split_count, TokenTile); + if (split >= active_split_count) { return; } + + const int logical_tiles = div_up(window, Bc); + const bool tile_split = logical_tiles >= active_split_count; + const int units_per_split = + tile_split ? div_up(logical_tiles, active_split_count) : div_up(window, active_split_count); + const int split_start = split * units_per_split * (tile_split ? Bc : 1); + const int split_limit = split_start + units_per_split * (tile_split ? Bc : 1); + const int split_end = (split_limit < window) ? split_limit : window; + if (split_start >= split_end) { + write_neutral(); + return; + } + const int first_tile = (split_start / Bc) * Bc; + const int key_blocks = div_up(split_end - first_tile, Bc); + const int first_global_page = (window_begin + first_tile) >> kPagedKVPageShift; + const int page_count = + ((window_begin + split_end - 1) >> kPagedKVPageShift) - first_global_page + 1; + for (int page = tid; page < page_count; page += Threads) { + physical_pages_s[page] = block_table[first_global_page + page]; + } + + if constexpr (CacheInput::writes_cache) { + // The owning split writes each new row into the packed ISO3 nibble + // planes. K is rotated per 4-channel block before quantization; V is + // gain-only. The subsequent tile staging reads every key from the + // cache, so no split depends on another split's cache write. + constexpr int kIso3Groups = D / 16; + const int append_units = valid_tokens * kIso3Groups; + for (int unit = warp; unit < append_units; unit += WarpsPerCta) { + const int group = unit % kIso3Groups; + const int token = unit / kIso3Groups; + const int p_tok = pos[token]; + const int split_begin = window_begin + split_start; + const int split_limit_g = window_begin + split_end; + if (p_tok < split_begin || p_tok >= split_limit_g || p_tok < 0 || + p_tok >= logical_capacity) { + continue; + } + int physical_page = lane == 0 ? paged_kv_physical_page(block_table, p_tok) : 0; + physical_page = __shfl_sync(FullMask, physical_page, 0); + const int page_off = p_tok & kPagedKVPageMask; + + // K: rotate the four 4-channel blocks of this 16-group, then + // quantize with one shared E4M3FN scale for the group. + float kx[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { + const int block = group * 4 + lane; + const std::int64_t src = + gqa_kv_new_index(kv_head, group * 16, token) + lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { kx[j] = __bfloat162float(input.k[src + j]); } + const float y0 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 3); + kx[0] = y0; + kx[1] = y1; + kx[2] = y2; + kx[3] = y3; + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), + fmaxf(fabsf(kx[2]), fabsf(kx[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + kmax = fmaxf(kmax, __shfl_xor_sync(FullMask, kmax, off)); + } + if constexpr (Nvfp4K) { + const float kscale = fmaxf(kmax / 6.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t kcode = gqa_kv_nvfp4_code_index( + physical_page, kv_head, group * 16, page_off); + cache_k[kcode + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[0] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[1] / kscale) << 4)); + cache_k[kcode + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[2] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[3] / kscale) << 4)); + } + if (lane == 0) { + cache_k_scale[gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + } else { + const float kscale = fmaxf(kmax / 7.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t kcode = gqa_kv_nvfp4_code_index( + physical_page, kv_head, group * 16, page_off); + cache_k[kcode + 2 * lane] = + static_cast(gqa_iso3_nibble(kx[0], kscale) | + (gqa_iso3_nibble(kx[1], kscale) << 4)); + cache_k[kcode + 2 * lane + 1] = + static_cast(gqa_iso3_nibble(kx[2], kscale) | + (gqa_iso3_nibble(kx[3], kscale) << 4)); + } + if (lane == 0) { + cache_k_scale[gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + } + + // V: gain-only ISO3 quantization, no rotation. + const float v0 = lane < 16 + ? __bfloat162float(input.v[gqa_kv_new_index( + kv_head, group * 16 + lane, token)]) + : 0.0f; + float vmax = fabsf(v0); +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + vmax = fmaxf(vmax, __shfl_xor_sync(FullMask, vmax, off)); + } + const float vscale = fmaxf(vmax / 7.0f, 0.001953125f); + if (lane < 8) { + const float ve = __bfloat162float(input.v[gqa_kv_new_index( + kv_head, group * 16 + lane * 2, token)]); + const float vo = __bfloat162float(input.v[gqa_kv_new_index( + kv_head, group * 16 + lane * 2 + 1, token)]); + const std::int64_t vcode = gqa_kv_nvfp4_code_index( + physical_page, kv_head, group * 16, page_off); + cache_v[vcode + lane] = + static_cast(gqa_iso3_nibble(ve, vscale) | + (gqa_iso3_nibble(vo, vscale) << 4)); + } + if (lane == 0) { + cache_v_scale[gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } + } + __syncthreads(); + } + + for (int chunk = tid; chunk < Br * (D / 8); chunk += Threads) { + const int row = chunk / (D / 8); + const int d = (chunk - row * (D / 8)) * 8; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + const bool valid = row < row_count && gqa_valid_q_head(kv_head, q_head); + float x[8]; +#pragma unroll + for (int i = 0; i < 8; ++i) { + x[i] = valid ? __bfloat162float(q[gqa_q_index(q_head, d + i, token)]) : 0.0f; + } + // K is rotated at append time; rotate Q by the same per-4-block IsoQuant + // matrix so QK^T is invariant. Applies to the ISO3 and mixed paths. + gqa_prefill_nvfp4_rotate_8(x, d); +#pragma unroll + for (int i = 0; i < 8; ++i) { + qkv_s[row * D + gqa_small_t_tc_swz(row, d + i)] = __float2bfloat16(x[i]); + } + } + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + + const int warp_row0 = warp * 16; + __nv_bfloat16* p_sw = &p_s[warp * 16 * Bc]; + + unsigned af_q[QKKs][4]; +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + const int arow = warp_row0 + a_rowoff; + const int acol = k * 16 + a_coloff; + ldmatrix_x4(af_q[k][0], af_q[k][1], af_q[k][2], af_q[k][3], + smem_addr(&qkv_s[arow * D + gqa_small_t_tc_swz(arow, acol)])); + } + __syncthreads(); + int physical_page = physical_pages_s[0]; + float acc[PVNt][4]; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F, l0 = 0.0f, l1 = 0.0f; + + for (int kb = 0; kb < key_blocks; ++kb) { + const int k0 = first_tile + kb * Bc; + const int global_k0 = window_begin + k0; + if (kb != 0 && (global_k0 & kPagedKVPageMask) == 0) { + physical_page = + physical_pages_s[(global_k0 >> kPagedKVPageShift) - first_global_page]; + } + // Stage the ISO3 K/V key tile synchronously into the swizzled BF16 + // qkv_s tile (K at offset 0, V at offset Bc*D). Each 8-element chunk + // reads four packed nibble bytes and one per-16-group scale; + // out-of-range rows store zero. +#pragma unroll 1 + for (int chunk = tid; chunk < Bc * (D / 8); chunk += Threads) { + const int key_l = chunk / (D / 8); + const int d = (chunk - key_l * (D / 8)) * 8; + const int key = global_k0 + key_l; + const int split_begin = window_begin + split_start; + const int split_limit_g = window_begin + split_end; + __nv_bfloat16* k_dst = &k_s[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + __nv_bfloat16* v_dst = &v_s[key_l * D + gqa_small_t_tc_swz(key_l, d)]; + if (key >= split_begin && key < split_limit_g) { + const int group = d >> 4; + const int page_off = key & kPagedKVPageMask; + const float k_scale = gqa_kv_nvfp4_e4m3_to_f32(cache_k_scale[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, page_off)]); + const std::int64_t k_code = gqa_kv_nvfp4_code_index( + physical_page, kv_head, d, page_off); + const unsigned k_raw = load_vec(&cache_k[k_code]); + const std::uint8_t* k_nib = reinterpret_cast(&k_raw); + unsigned k_packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + float x0; + float x1; + if constexpr (Nvfp4K) { + x0 = gqa_kv_nvfp4_e2m1_to_f32(k_nib[i] & 0x0Fu) * k_scale; + x1 = gqa_kv_nvfp4_e2m1_to_f32(k_nib[i] >> 4) * k_scale; + } else { + x0 = gqa_iso3_decode(k_nib[i] & 0x0Fu) * k_scale; + x1 = gqa_iso3_decode(k_nib[i] >> 4) * k_scale; + } + k_packed[i] = pack_bf16x2(x0, x1); + } + store_vec(k_dst, make_int4(static_cast(k_packed[0]), + static_cast(k_packed[1]), + static_cast(k_packed[2]), + static_cast(k_packed[3]))); + + const float v_scale = gqa_kv_nvfp4_e4m3_to_f32(cache_v_scale[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, page_off)]); + const std::int64_t v_code = gqa_kv_nvfp4_code_index( + physical_page, kv_head, d, page_off); + const unsigned v_raw = load_vec(&cache_v[v_code]); + const std::uint8_t* v_nib = reinterpret_cast(&v_raw); + unsigned v_packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const float x0 = gqa_iso3_decode(v_nib[i] & 0x0Fu) * v_scale; + const float x1 = gqa_iso3_decode(v_nib[i] >> 4) * v_scale; + v_packed[i] = pack_bf16x2(x0, x1); + } + store_vec(v_dst, make_int4(static_cast(v_packed[0]), + static_cast(v_packed[1]), + static_cast(v_packed[2]), + static_cast(v_packed[3]))); + } else { + store_vec(k_dst, make_int4(0, 0, 0, 0)); + store_vec(v_dst, make_int4(0, 0, 0, 0)); + } + } + __syncthreads(); + + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + unsigned bf[2]; + const int brow = nt * 8 + b_rin; + const int bcol = k * 16 + b_koff; + ldmatrix_x2(bf[0], bf[1], + smem_addr(&k_s[brow * D + gqa_small_t_tc_swz(brow, bcol)])); + mma_bf16(score[nt][0], score[nt][1], score[nt][2], score[nt][3], af_q[k][0], + af_q[k][1], af_q[k][2], af_q[k][3], bf[0], bf[1]); + } + } + + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + int q_head0 = 0, token0 = 0, q_head1 = 0, token1 = 0; + gqa_small_t_tc_row_to_qt(row0, tokens, kv_head, q_head0, token0); + gqa_small_t_tc_row_to_qt(row1, tokens, kv_head, q_head1, token1); + const int qabs0 = (row0 < row_count) ? pos[token0] : -1; + const int qabs1 = (row1 < row_count) ? pos[token1] : -1; + + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const int key0 = global_k0 + col0; + const int key1 = col1 + global_k0; + const int split_begin = window_begin + split_start; + const int split_limit_g = window_begin + split_end; + score[nt][0] = + (row0 < row_count && key0 >= split_begin && key0 < split_limit_g && key0 <= qabs0) + ? score[nt][0] * scale + : -CUDART_INF_F; + score[nt][1] = + (row0 < row_count && key1 >= split_begin && key1 < split_limit_g && key1 <= qabs0) + ? score[nt][1] * scale + : -CUDART_INF_F; + score[nt][2] = + (row1 < row_count && key0 >= split_begin && key0 < split_limit_g && key0 <= qabs1) + ? score[nt][2] * scale + : -CUDART_INF_F; + score[nt][3] = + (row1 < row_count && key1 >= split_begin && key1 < split_limit_g && key1 <= qabs1) + ? score[nt][3] * scale + : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float alpha0 = (m0 == -CUDART_INF_F) ? 0.0f : exp2_approx((m0 - nm0) * Log2E); + const float alpha1 = (m1 == -CUDART_INF_F) ? 0.0f : exp2_approx((m1 - nm1) * Log2E); + + float bl0 = 0.0f, bl1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const float p00 = (nm0 > -CUDART_INF_F && score[nt][0] > -CUDART_INF_F) + ? exp2_approx((score[nt][0] - nm0) * Log2E) + : 0.0f; + const float p01 = (nm0 > -CUDART_INF_F && score[nt][1] > -CUDART_INF_F) + ? exp2_approx((score[nt][1] - nm0) * Log2E) + : 0.0f; + const float p10 = (nm1 > -CUDART_INF_F && score[nt][2] > -CUDART_INF_F) + ? exp2_approx((score[nt][2] - nm1) * Log2E) + : 0.0f; + const float p11 = (nm1 > -CUDART_INF_F && score[nt][3] > -CUDART_INF_F) + ? exp2_approx((score[nt][3] - nm1) * Log2E) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col0)] = __float2bfloat16(p00); + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col1)] = __float2bfloat16(p01); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col0)] = __float2bfloat16(p10); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col1)] = __float2bfloat16(p11); + } + bl0 = warp_sum<4>(bl0, FullMask); + bl1 = warp_sum<4>(bl1, FullMask); + + l0 = l0 * alpha0 + bl0; + l1 = l1 * alpha1 + bl1; + m0 = nm0; + m1 = nm1; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + __syncwarp(); + +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int k = 0; k < PVKs; ++k) { + unsigned pf[4]; + const int pcol = k * 16 + a_coloff; + ldmatrix_x4(pf[0], pf[1], pf[2], pf[3], + smem_addr(&p_sw[a_rowoff * Bc + gqa_small_t_tc_swz32(a_rowoff, pcol)])); + unsigned vf[2]; + const int vrow = k * 16 + b_koff + b_rin; + const int vcol = n * 8; + ldmatrix_x2_t(vf[0], vf[1], + smem_addr(&v_s[vrow * D + gqa_small_t_tc_swz(vrow, vcol)])); + mma_bf16(acc[n][0], acc[n][1], acc[n][2], acc[n][3], pf[0], pf[1], pf[2], pf[3], + vf[0], vf[1]); + } + } + __syncthreads(); + } + + if (lid == 0) { + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < row_count) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, tokens, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = m0; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = l0; + } + if (row1 < row_count) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, tokens, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, tokens)] = m1; + partial_l[gqa_partial_stat_index(q_head, token, split, tokens)] = l1; + } + } + + // MMA fragments hold each row in four-lane groups. Stage the final split-local + // accumulator through shared memory so partial_acc is written as contiguous d-vector stores. +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + const int d0 = n * 8 + 2 * lid; + const int d1 = d0 + 1; + const int row0 = warp_row0 + gid; + const int row1 = row0 + 8; + if (row0 < row_count) { + qkv_s[row0 * D + d0] = __float2bfloat16(acc[n][0]); + qkv_s[row0 * D + d1] = __float2bfloat16(acc[n][1]); + } + if (row1 < row_count) { + qkv_s[row1 * D + d0] = __float2bfloat16(acc[n][2]); + qkv_s[row1 * D + d1] = __float2bfloat16(acc[n][3]); + } + } + __syncthreads(); + + for (int chunk = tid; chunk < row_count * (D / 8); chunk += Threads) { + const int row = chunk / (D / 8); + const int d = (chunk - row * (D / 8)) * 8; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, tokens, kv_head, q_head, token); + if (gqa_valid_q_head(kv_head, q_head)) { + const std::int64_t dst = + gqa_partial_acc_index(q_head, d, token, split, tokens); + store_vec(&partial_acc[dst], load_vec(&qkv_s[row * D + d])); + } + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_decode_nvfp4.cuh b/src/ops/kernel/gqa_attention_decode_nvfp4.cuh new file mode 100644 index 0000000000..3c96849d57 --- /dev/null +++ b/src/ops/kernel/gqa_attention_decode_nvfp4.cuh @@ -0,0 +1,973 @@ +#pragma once + +// ninfer::ops - split-KV GQA small-T attention, NVFP4 KV-cache partial kernel. +// +// * QK runs on native m16n8k64.kind::mxf4nvf4 tensor cores. Q is quantized +// on-chip to packed E2M1 with per-(row,16-group) E4M3 scales; K stays +// packed in the cache and is staged straight into shared memory. The +// hardware block-scale instruction applies both scale vectors, so the +// QK result is already in the scaled domain. +// * K cache writes apply the baked IsoQuant per-4-channel rotation, and Q +// is rotated with the same matrix before quantization, preserving QK^T. +// * PV runs on native m16n8k64.kind::mxf4nvf4 tensor cores too: P is +// folded with the V E4M3 scale, re-quantized per (row,16-d-group) to +// E2M1, and the packed V codes are re-tiled in shared memory. The MMA +// applies the folded P scale vector against a constant 1.0 scale +// vector, so the accumulated result is in the scaled domain. +// * All keys (history AND current diagonal tokens) are read from the +// quantized cache; the fused append writes new tokens first and a +// __syncthreads orders the in-block readback. +// +// This kernel is the SM120 native successor to the vLLM-side nvfp4rtx +// nvfp4-mma-v15 kernel, retargeted to NInfer's paged KV layout. + +#include +#include + +#include "ops/kernel/gqa_attention_decode.cuh" +#include "ops/kernel/gqa_attention_kv_nvfp4.cuh" +#include "ops/kernel/gqa_isoquant_rot.cuh" +#include "ops/kernel/gqa_isoquant_row_scale.cuh" +#include "ops/kernel/entropy_nvfp4_slot.cuh" +#include "ops/kernel/gqa_attention_prefill_nvfp4.cuh" // gqa_iso3_nibble / gqa_iso3_decode + +#include + +namespace ninfer::ops { +namespace { + +using namespace ninfer::ops::detail; + +constexpr float kNvfp4MinScale = 0.001953125f; // 2^-9, E4M3 smallest normal +constexpr std::uint8_t kNvfp4E4M3One = 0x38u; // E4M3FN encoding of 1.0 + +__device__ __forceinline__ void gqa_nvfp4_load_a_frag(unsigned (&frag)[4], const std::uint8_t* smem, + int lane, int k_step) { + const int row = (lane & 7) + ((lane >> 3) & 1) * 8; + const int col = (lane >> 4) * 16 + k_step * 32; + ldmatrix_x4(frag[0], frag[1], frag[2], frag[3], smem_addr(smem + row * 128 + col)); +} + +__device__ __forceinline__ void gqa_nvfp4_load_b_frag(unsigned (&frag)[2], const std::uint8_t* smem, + int lane, int n_tile, int k_step) { + const int row = (lane & 7) + n_tile * 8; + const int col = ((lane >> 3) & 1) * 16 + k_step * 32; + ldmatrix_x2(frag[0], frag[1], smem_addr(smem + row * 128 + col)); +} + +// PV repack tiles use a 64-byte row stride (16-byte k-fragment per mxf4nvf4 +// m16n8k64 operand row). +__device__ __forceinline__ void gqa_nvfp4_load_a_frag_64(unsigned (&frag)[4], + const std::uint8_t* smem, int lane) { + const int row = (lane & 7) + ((lane >> 3) & 1) * 8; + const int col = (lane >> 4) * 16; + ldmatrix_x4(frag[0], frag[1], frag[2], frag[3], smem_addr(smem + row * 64 + col)); +} + +__device__ __forceinline__ void gqa_nvfp4_load_b_frag_64(unsigned (&frag)[2], + const std::uint8_t* smem, int lane, + int n_tile) { + const int row = (lane & 7) + n_tile * 8; + const int col = ((lane >> 3) & 1) * 16; + ldmatrix_x2(frag[0], frag[1], smem_addr(smem + row * 64 + col)); +} + +__device__ __forceinline__ float gqa_nvfp4_rotated(float x0, float x1, float x2, float x3, + int block, int row) { + return gqa_isoquant_rot_value(block, row, 0) * x0 + + gqa_isoquant_rot_value(block, row, 1) * x1 + + gqa_isoquant_rot_value(block, row, 2) * x2 + + gqa_isoquant_rot_value(block, row, 3) * x3; +} + +// Lane l < 4 loads the four values of its 4-channel block, applies the baked +// SO(4) rotation, and returns the rotated block in x[]. The caller's src +// pointer ALREADY points at the 16-dimension group start. +__device__ __forceinline__ void gqa_nvfp4_load_rotate_4(float (&x)[4], const __nv_bfloat16* src, + int group, int lane) { + if (lane < 4) { + const int block = group * 4 + lane; + const int base = lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { x[j] = __bfloat162float(src[base + j]); } + const float y0 = gqa_nvfp4_rotated(x[0], x[1], x[2], x[3], block, 0); + const float y1 = gqa_nvfp4_rotated(x[0], x[1], x[2], x[3], block, 1); + const float y2 = gqa_nvfp4_rotated(x[0], x[1], x[2], x[3], block, 2); + const float y3 = gqa_nvfp4_rotated(x[0], x[1], x[2], x[3], block, 3); + x[0] = y0; + x[1] = y1; + x[2] = y2; + x[3] = y3; + } else { + x[0] = x[1] = x[2] = x[3] = 0.0f; + } +} + +// Reduction over the active lanes of one 4-channel rotated block (lanes 0..3). +__device__ __forceinline__ float gqa_nvfp4_group_max4(float local_max, unsigned full_mask) { + local_max = fmaxf(local_max, __shfl_xor_sync(full_mask, local_max, 1)); + local_max = fmaxf(local_max, __shfl_xor_sync(full_mask, local_max, 2)); + return local_max; +} + +__device__ __forceinline__ float gqa_nvfp4_group_max16(float local_max, unsigned full_mask) { +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + local_max = fmaxf(local_max, __shfl_xor_sync(full_mask, local_max, off)); + } + return local_max; +} + +} // namespace + +template +__launch_bounds__(WarpsPerCta * 32, MinBlocksPerSm) __global__ + void gqa_attention_decode_nvfp4_tiled_kernel( + const __nv_bfloat16* q, const __nv_bfloat16* input_k, const __nv_bfloat16* input_v, + const std::int32_t* pos, std::uint8_t* cache_k, + std::uint8_t* cache_v, std::uint8_t* cache_k_scale, std::uint8_t* cache_v_scale, + std::uint8_t* cache_k_residual, std::uint8_t* cache_k_residual_scale, + std::uint8_t* cache_v_residual, std::uint8_t* cache_v_residual_scale, + const std::uint8_t* cold_k_slots, const std::uint8_t* cold_v_slots, + const std::int32_t* cold_k_valid, const std::int32_t* cold_v_valid, + int slot_bytes, int sliding_window, + const std::int32_t* block_tables, const std::int32_t* valid_columns, + const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t full_width, + std::int32_t column_begin, std::int32_t logical_capacity, int layer, float scale, + __nv_bfloat16* partial_acc, float* partial_m, float* partial_l, + std::int32_t batch_size, bool masked, bool writes_cache) { + constexpr int Wc = WarpsPerCta; + constexpr int RowCount = TokenTile * Geometry::GroupSize; + constexpr int RowTiles = (RowCount + 15) / 16; + constexpr int Br = RowTiles * 16; + constexpr int Bc = KeyBlock; + constexpr int D = kGqaHeadDim; + constexpr int Threads = Wc * 32; + constexpr int Groups = kGqaKvNvfp4Groups; + constexpr int QKKs = D / 64; + constexpr int QKNt = Bc / 8; + constexpr int PVKs = Bc / 16; + constexpr int ConsumerWarpsPerTile = Wc / RowTiles; + constexpr int PVNtPerWarp = D / (ConsumerWarpsPerTile * 8); + constexpr int DgCount = PVNtPerWarp / 2; + constexpr int PageIds = 256; + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + constexpr unsigned kOnesScale = 0x38383838u; + + static_assert(TokenTile >= 1 && TokenTile <= 6); + static_assert(Bc == 32); + static_assert(RowTiles >= 1 && RowTiles <= 3); + static_assert(Wc % RowTiles == 0); + static_assert(PVNtPerWarp == 2 || PVNtPerWarp == 4 || PVNtPerWarp == 8 || PVNtPerWarp == 16); + static_assert(PVNtPerWarp >= 2 && (PVNtPerWarp % 2) == 0); + static_assert(QKKs == 4); + + // Shared arena: + // k_pk/v_pk + k_sf/v_sf: two ping-pong Bc-token tiles, so the next tile + // is prefetched while the current tile still runs native PV + // psc_s Br*64 P*V fold scales (4 bytes per row/dg) + // repack_a/b Wc*1024 per-warp P/V operand repack tiles + constexpr int kTileBytes = 4 * Bc * 128 + 4 * Bc * 16; + __shared__ __align__(16) std::uint8_t q_a[Br * 128]; + __shared__ __align__(16) std::uint8_t q_sf[Br * 16]; + __shared__ __align__(16) std::uint8_t static_r_s[DynamicArena + ? 16 + : 2 * kTileBytes + Br * 16 * 4 + + 2 * Wc * 16 * 64]; + extern __shared__ __align__(16) std::uint8_t nvfp4_dynamic_r_s[]; + std::uint8_t* r_s = DynamicArena ? nvfp4_dynamic_r_s : static_r_s; + std::uint8_t* psc_s = r_s + 2 * kTileBytes; + std::uint8_t* repack_a = psc_s + Br * 16 * 4; + std::uint8_t* repack_b = repack_a + Wc * 16 * 64; + __shared__ __align__(16) __nv_bfloat16 p_s[Br * Bc]; + __shared__ float alpha_s[Br]; + __shared__ std::int32_t physical_pages_s[PageIds]; + // Hybrid V path decodes the packed ISO3 V tile into BF16 with the exact + // full-D tc swizzle the ldmatrix PV path expects. + constexpr int kRBytes = 2 * kTileBytes + Br * 16 * 4 + 2 * Wc * 16 * 64; + __nv_bfloat16* v_bf16 = + reinterpret_cast<__nv_bfloat16*>(DynamicArena ? nvfp4_dynamic_r_s + kRBytes + : nvfp4_dynamic_r_s); + + const int kv_head = static_cast(blockIdx.x); + const int split = static_cast(blockIdx.y); + const int batch = static_cast(blockIdx.z); + const int split_count = static_cast(gridDim.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + + int valid_tokens = TokenTile; + if (masked) { + const int remaining = valid_columns[batch] - column_begin; + valid_tokens = remaining <= 0 ? 0 : (remaining < TokenTile ? remaining : TokenTile); + } + std::int64_t column_base = column_begin + static_cast(batch) * full_width; + q += static_cast(kGqaHeadDim) * Geometry::QHeads * column_base; + pos += column_base; + if (writes_cache) { + input_k += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + input_v += static_cast(kGqaHeadDim) * Geometry::KVHeads * column_base; + } + const int table_row = table_rows == nullptr ? 0 : table_rows[batch]; + const std::int32_t* block_table = + block_tables + static_cast(table_row) * table_stride; + partial_acc += static_cast(batch) * kGqaHeadDim * Geometry::QHeads * + TokenTile * split_count; + partial_m += static_cast(batch) * Geometry::QHeads * TokenTile * split_count; + partial_l += static_cast(batch) * Geometry::QHeads * TokenTile * split_count; + +#define NINFER_NVFP4_WRITE_NEUTRAL() \ + do { \ + for (int row = tid; row < RowCount; row += Threads) { \ + int q_head = 0; \ + int token = 0; \ + gqa_small_t_tc_row_to_qt(row, TokenTile, kv_head, q_head, token); \ + if (gqa_valid_q_head(kv_head, q_head)) { \ + partial_m[gqa_partial_stat_index(q_head, token, split, TokenTile)] = \ + -CUDART_INF_F; \ + partial_l[gqa_partial_stat_index(q_head, token, split, TokenTile)] = \ + 0.0f; \ + } \ + } \ + for (int idx = tid; idx < RowCount * D; idx += Threads) { \ + const int row = idx / D; \ + const int d = idx - row * D; \ + int q_head = 0; \ + int token = 0; \ + gqa_small_t_tc_row_to_qt(row, TokenTile, kv_head, q_head, token); \ + if (gqa_valid_q_head(kv_head, q_head)) { \ + partial_acc[gqa_partial_acc_index(q_head, d, token, split, \ + TokenTile)] = \ + __float2bfloat16(0.0f); \ + } \ + } \ + } while (0) + + if (kv_head < 0 || kv_head >= Geometry::KVHeads || split_count <= 0) { return; } + if (valid_tokens == 0) { + NINFER_NVFP4_WRITE_NEUTRAL(); + return; + } + + const std::int32_t first_pos = pos[0]; + const std::int32_t last_pos = pos[TokenTile - 1]; + if (first_pos < 0 || last_pos < 0 || last_pos >= logical_capacity) { + NINFER_NVFP4_WRITE_NEUTRAL(); + return; + } + + const int window_full = last_pos + 1; + // Slide in 32-key tiles: a staging tile must not straddle a 64-key page + // boundary, so the ring window start is rounded up to the Bc grid. + const int token_begin = (sliding_window > 0) ? window_full - sliding_window : 0; + const int window_begin = + (sliding_window > 0) ? ((max(0, token_begin) + Bc - 1) / Bc) * Bc : 0; + const int window = window_full - window_begin; + const int active_split_count = + gqa_small_t_active_splits(window, split_count, TokenTile); + if (split >= active_split_count) { return; } + + const int logical_tiles = div_up(window, Bc); + const bool tile_split = logical_tiles >= active_split_count; + const int units_per_split = + tile_split ? div_up(logical_tiles, active_split_count) : div_up(window, active_split_count); + const int split_start = split * units_per_split * (tile_split ? Bc : 1); + const int split_limit = split_start + units_per_split * (tile_split ? Bc : 1); + const int split_end = (split_limit < window) ? split_limit : window; + if (split_start >= split_end) { + NINFER_NVFP4_WRITE_NEUTRAL(); + return; + } + const int first_tile = (split_start / Bc) * Bc; + const int key_blocks = div_up(split_end - first_tile, Bc); + const int first_global_page = (window_begin + first_tile) >> kPagedKVPageShift; + const int page_count = + ((window_begin + split_end - 1) >> kPagedKVPageShift) - first_global_page + 1; + for (int page = tid; page < page_count; page += Threads) { + physical_pages_s[page] = block_table[first_global_page + page]; + } + + // ---- fused cache append: quantize current K/V rows into the NVFP4 planes ---- + if (writes_cache) { +_Pragma("unroll 1") + for (int pair = warp; pair < valid_tokens * Groups; pair += Wc) { + const int token = pair / Groups; + const int grp = pair - token * Groups; + const int position = pos[token]; + if (position - window_begin < split_start || + position - window_begin >= split_end) { + continue; + } + int physical_page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + physical_page = __shfl_sync(FullMask, physical_page, 0); + const int page_offset = position & kPagedKVPageMask; + const int src0 = gqa_kv_nvfp4_src_index(kv_head, grp * 16, token); + + // K: rotate per 4-channel block with the baked IsoQuant matrix and + // apply the Sinkhorn-constrained row scale before E4M3/E2M1 packing. + float kx[4]; + gqa_nvfp4_load_rotate_4(kx, input_k + src0, grp, lane); +#pragma unroll + for (int j = 0; j < 4; ++j) { + kx[j] *= gqa_kv_row_scale(layer, kv_head, grp * 16 + lane * 4 + j); + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), + fmaxf(fabsf(kx[2]), fabsf(kx[3]))); + kmax = gqa_nvfp4_group_max4(kmax, FullMask); + const float kscale = fmaxf(kmax / 6.0f, kNvfp4MinScale); + const std::int64_t kcode = + gqa_kv_nvfp4_code_index(physical_page, kv_head, grp * 16, page_offset); + if (lane < 4) { + cache_k[kcode + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[0] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[1] / kscale) << 4)); + cache_k[kcode + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[2] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[3] / kscale) << 4)); + } + if (lane == 0) { + cache_k_scale[gqa_kv_nvfp4_scale_index(physical_page, kv_head, grp, + page_offset)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + // K residual: second E2M1 stage over the first-stage error. + if (cache_k_residual != nullptr) { + float res[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { +#pragma unroll + for (int j = 0; j < 4; ++j) { + const std::uint8_t code_j = gqa_kv_nvfp4_e2m1_nibble(kx[j] / kscale); + res[j] = kx[j] - gqa_kv_nvfp4_e2m1_to_f32(code_j) * kscale; + } + } + float rmax = fmaxf(fmaxf(fabsf(res[0]), fabsf(res[1])), + fmaxf(fabsf(res[2]), fabsf(res[3]))); + rmax = gqa_nvfp4_group_max4(rmax, FullMask); + const float rscale = fmaxf(rmax / 6.0f, kNvfp4MinScale); + if (lane < 4) { + const std::int64_t rcode = + gqa_kv_nvfp4_code_index(physical_page, kv_head, grp * 16, + page_offset); + cache_k_residual[rcode + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(res[0] / rscale) | + (gqa_kv_nvfp4_e2m1_nibble(res[1] / rscale) << 4)); + cache_k_residual[rcode + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(res[2] / rscale) | + (gqa_kv_nvfp4_e2m1_nibble(res[3] / rscale) << 4)); + } + if (lane == 0) { + cache_k_residual_scale[gqa_kv_nvfp4_scale_index( + physical_page, kv_head, grp, page_offset)] = + gqa_kv_nvfp4_fp32_to_e4m3(rscale); + } + } + // V: no rotation, only per-16 gain quantization. + const float v0 = lane < 16 ? __bfloat162float(input_v[src0 + lane]) : 0.0f; + float vmax = fabsf(v0); + vmax = gqa_nvfp4_group_max16(vmax, FullMask); + const std::int64_t vcode = + gqa_kv_nvfp4_code_index(physical_page, kv_head, grp * 16, page_offset); + if constexpr (Iso3V) { + const float vscale = fmaxf(vmax / 7.0f, 0.001953125f); + if (lane < 8) { + cache_v[vcode + lane] = + static_cast(gqa_iso3_nibble(v0, vscale) | + (gqa_iso3_nibble( + __bfloat162float( + input_v[src0 + lane * 2 + 1]), + vscale) + << 4)); + } + if (lane == 0) { + cache_v_scale[gqa_kv_nvfp4_scale_index( + physical_page, kv_head, grp, page_offset)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } + } else { + const float vscale = fmaxf(vmax / 6.0f, kNvfp4MinScale); + if (lane < 8) { + cache_v[vcode + lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(v0 / vscale) | + (gqa_kv_nvfp4_e2m1_nibble( + __bfloat162float( + input_v[src0 + lane * 2 + 1]) / + vscale) + << 4)); + } + if (lane == 0) { + cache_v_scale[gqa_kv_nvfp4_scale_index( + physical_page, kv_head, grp, page_offset)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } + } + } + __syncthreads(); + } + + // ---- on-chip Q quantization (same rotation as K) ---- + for (int i = tid; i < Br * 128; i += Threads) { q_a[i] = 0; } + for (int i = tid; i < Br * 16; i += Threads) { q_sf[i] = kNvfp4E4M3One; } + __syncthreads(); + +_Pragma("unroll 1") + for (int unit = warp; unit < RowCount * Groups; unit += Wc) { + const int row = unit / Groups; + const int grp = unit - row * Groups; + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row, TokenTile, kv_head, q_head, token); + const int src = gqa_q_index(q_head, grp * 16, token); + float qx[4]; + gqa_nvfp4_load_rotate_4(qx, q + src, grp, lane); +#pragma unroll + for (int j = 0; j < 4; ++j) { + qx[j] *= gqa_kv_row_scale_inv(layer, kv_head, grp * 16 + lane * 4 + j); + } + float qmax = fmaxf(fmaxf(fabsf(qx[0]), fabsf(qx[1])), fmaxf(fabsf(qx[2]), fabsf(qx[3]))); + qmax = gqa_nvfp4_group_max4(qmax, FullMask); + const float qscale = fmaxf(qmax / 6.0f, kNvfp4MinScale); + if (lane < 4) { + q_a[row * 128 + grp * 8 + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(qx[0] / qscale) | + (gqa_kv_nvfp4_e2m1_nibble(qx[1] / qscale) << 4)); + q_a[row * 128 + grp * 8 + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(qx[2] / qscale) | + (gqa_kv_nvfp4_e2m1_nibble(qx[3] / qscale) << 4)); + } + if (lane == 0) { q_sf[row * 16 + grp] = gqa_kv_nvfp4_fp32_to_e4m3(qscale); } + } + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + float acc[PVNtPerWarp][4]; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F; + float l0 = 0.0f, l1 = 0.0f; + +#define NINFER_NVFP4_STAGE_TILE(TILE_K0, PHYSICAL_PAGE, SLOT) \ + do { \ + const int stage_tile_k0 = (TILE_K0); \ + const int stage_global_k0 = window_begin + stage_tile_k0; \ + const int stage_physical_page = (PHYSICAL_PAGE); \ + const bool stage_cold = stage_physical_page <= -2 && \ + cold_k_slots != nullptr && cold_v_slots != nullptr && \ + slot_bytes >= 1024 + 320; \ + const int stage_slot_base = stage_cold ? -stage_physical_page - 2 : 0; \ + const int stage_slot_id = stage_slot_base + kv_head; \ + const int stage_half = (stage_global_k0 & kPagedKVPageMask) >> 5; \ + const std::uint8_t* stage_k_slot = \ + stage_cold ? cold_k_slots + static_cast(stage_slot_id) * \ + slot_bytes \ + : nullptr; \ + const std::uint8_t* stage_v_slot = \ + stage_cold ? cold_v_slots + static_cast(stage_slot_id) * \ + slot_bytes \ + : nullptr; \ + std::uint8_t* stage_k_pk = r_s + (SLOT) * kTileBytes; \ + std::uint8_t* stage_k_rpk = stage_k_pk + Bc * 128; \ + std::uint8_t* stage_v_pk = stage_k_rpk + Bc * 128; \ + std::uint8_t* stage_v_rpk = stage_v_pk + Bc * 128; \ + std::uint8_t* stage_k_sf = stage_v_rpk + Bc * 128; \ + std::uint8_t* stage_k_rsf = stage_k_sf + Bc * 16; \ + std::uint8_t* stage_v_sf = stage_k_rsf + Bc * 16; \ + std::uint8_t* stage_v_rsf = stage_v_sf + Bc * 16; \ + for (int key_l = tid; key_l < Bc; key_l += Threads) { \ + const int key = stage_global_k0 + key_l; \ + if (key >= window_begin + split_start && key < window_begin + split_end) { \ + if (stage_cold) { \ + const std::uint8_t* k_scales = \ + entropy_nvfp4_slot_scales(stage_k_slot, slot_bytes); \ + const std::uint8_t* v_scales = \ + entropy_nvfp4_slot_scales(stage_v_slot, slot_bytes); \ + ninfer::ops::cp_async<16>(&stage_k_sf[key_l * 16], \ + &k_scales[(stage_half * 32 + key_l) * 16]); \ + ninfer::ops::cp_async<16>(&stage_v_sf[key_l * 16], \ + &v_scales[(stage_half * 32 + key_l) * 16]); \ + store_vec(&stage_k_rsf[key_l * 16], make_int4(0, 0, 0, 0)); \ + store_vec(&stage_v_rsf[key_l * 16], make_int4(0, 0, 0, 0)); \ + } else { \ + const std::int64_t scale_off = gqa_kv_nvfp4_scale_index( \ + stage_physical_page, kv_head, 0, key & kPagedKVPageMask); \ + ninfer::ops::cp_async<16>(&stage_k_sf[key_l * 16], &cache_k_scale[scale_off]); \ + ninfer::ops::cp_async<16>(&stage_v_sf[key_l * 16], &cache_v_scale[scale_off]); \ + if (cache_k_residual_scale != nullptr) { \ + ninfer::ops::cp_async<16>(&stage_k_rsf[key_l * 16], \ + &cache_k_residual_scale[scale_off]); \ + } else { \ + store_vec(&stage_k_rsf[key_l * 16], make_int4(0, 0, 0, 0)); \ + } \ + if (cache_v_residual_scale != nullptr) { \ + ninfer::ops::cp_async<16>(&stage_v_rsf[key_l * 16], \ + &cache_v_residual_scale[scale_off]); \ + } else { \ + store_vec(&stage_v_rsf[key_l * 16], make_int4(0, 0, 0, 0)); \ + } \ + } \ + } else { \ + store_vec(&stage_k_sf[key_l * 16], make_int4(0, 0, 0, 0)); \ + store_vec(&stage_k_rsf[key_l * 16], make_int4(0, 0, 0, 0)); \ + store_vec(&stage_v_sf[key_l * 16], make_int4(0, 0, 0, 0)); \ + store_vec(&stage_v_rsf[key_l * 16], make_int4(0, 0, 0, 0)); \ + } \ + } \ + if (stage_cold) { \ + for (int chunk = tid; chunk < Bc * 8; chunk += Threads) { \ + const int key_l = chunk >> 3; \ + const int j = chunk & 7; \ + store_vec(&stage_k_rpk[key_l * 128 + j * 16], make_int4(0, 0, 0, 0)); \ + store_vec(&stage_v_rpk[key_l * 128 + j * 16], make_int4(0, 0, 0, 0)); \ + } \ + if (tid < kEntropyNvfp4SlotStreamsPerHalf) { \ + std::uint8_t* dst = stage_k_pk + tid * kEntropyNvfp4SlotStreamBytes; \ + if (!entropy_nvfp4_slot_decode_stream(stage_k_slot, stage_half, tid, dst)) { \ + for (int i = 0; i < kEntropyNvfp4SlotStreamBytes; ++i) { dst[i] = 0; } \ + } \ + } else if (tid < 2 * kEntropyNvfp4SlotStreamsPerHalf) { \ + const int stream = tid - kEntropyNvfp4SlotStreamsPerHalf; \ + std::uint8_t* dst = stage_v_pk + stream * kEntropyNvfp4SlotStreamBytes; \ + if (!entropy_nvfp4_slot_decode_stream(stage_v_slot, stage_half, stream, \ + dst)) { \ + for (int i = 0; i < kEntropyNvfp4SlotStreamBytes; ++i) { dst[i] = 0; } \ + } \ + } \ + __syncthreads(); \ + } else { \ +_Pragma("unroll 1") \ + for (int chunk = tid; chunk < Bc * 8; chunk += Threads) { \ + const int key_l = chunk >> 3; \ + const int j = chunk & 7; \ + const int d = j * 32; \ + const int key = stage_global_k0 + key_l; \ + std::uint8_t* dst_k = &stage_k_pk[key_l * 128 + j * 16]; \ + std::uint8_t* dst_r = &stage_k_rpk[key_l * 128 + j * 16]; \ + std::uint8_t* dst_v = &stage_v_pk[key_l * 128 + j * 16]; \ + std::uint8_t* dst_vr = &stage_v_rpk[key_l * 128 + j * 16]; \ + if (key >= window_begin + split_start && key < window_begin + split_end) { \ + const std::int64_t code_off = gqa_kv_nvfp4_code_index( \ + stage_physical_page, kv_head, d, key & kPagedKVPageMask); \ + ninfer::ops::cp_async<16>(dst_k, &cache_k[code_off]); \ + if (cache_k_residual != nullptr) { \ + ninfer::ops::cp_async<16>(dst_r, &cache_k_residual[code_off]); \ + } else { \ + store_vec(dst_r, make_int4(0, 0, 0, 0)); \ + } \ + ninfer::ops::cp_async<16>(dst_v, &cache_v[code_off]); \ + if (cache_v_residual != nullptr) { \ + ninfer::ops::cp_async<16>(dst_vr, &cache_v_residual[code_off]); \ + } else { \ + store_vec(dst_vr, make_int4(0, 0, 0, 0)); \ + } \ + } else { \ + store_vec(dst_k, make_int4(0, 0, 0, 0)); \ + store_vec(dst_r, make_int4(0, 0, 0, 0)); \ + store_vec(dst_v, make_int4(0, 0, 0, 0)); \ + store_vec(dst_vr, make_int4(0, 0, 0, 0)); \ + } \ + } \ + } \ + ninfer::ops::cp_commit(); \ + } while (0) + + int physical_page = physical_pages_s[0]; + NINFER_NVFP4_STAGE_TILE(first_tile, physical_page, 0); + ninfer::ops::cp_wait<0>(); + __syncthreads(); + + for (int kb = 0; kb < key_blocks; ++kb) { + const int k0 = first_tile + kb * Bc; + const int global_k0 = window_begin + k0; + const int slot = kb & 1; + std::uint8_t* k_pk = r_s + slot * kTileBytes; + std::uint8_t* k_rpk = k_pk + Bc * 128; + std::uint8_t* v_pk = k_rpk + Bc * 128; + std::uint8_t* v_rpk = v_pk + Bc * 128; + std::uint8_t* k_sf = v_rpk + Bc * 128; + std::uint8_t* k_rsf = k_sf + Bc * 16; + std::uint8_t* v_sf = k_rsf + Bc * 16; + std::uint8_t* v_rsf = v_sf + Bc * 16; + + if (warp < RowTiles) { + const int producer_row_base = warp * 16; + __nv_bfloat16* p_sw = &p_s[producer_row_base * Bc]; + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = 0.0f; + score[nt][1] = 0.0f; + score[nt][2] = 0.0f; + score[nt][3] = 0.0f; + } + +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + unsigned af[4]; + gqa_nvfp4_load_a_frag(af, q_a + producer_row_base * 128, lane, k); + const unsigned sfa = load_vec( + q_sf + producer_row_base * 16 + (gid + (lid & 1) * 8) * 16 + k * 4); +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + unsigned bf[2]; + gqa_nvfp4_load_b_frag(bf, k_pk, lane, nt, k); + const unsigned sfb = load_vec(k_sf + (gid + nt * 8) * 16 + k * 4); + mma_nvfp4_e4m3(score[nt][0], score[nt][1], score[nt][2], score[nt][3], + af[0], af[1], af[2], af[3], bf[0], bf[1], sfa, sfb); + } + } + // Second QK pass accumulates the E2M1 residual K plane. +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + unsigned af[4]; + gqa_nvfp4_load_a_frag(af, q_a + producer_row_base * 128, lane, k); + const unsigned sfa = load_vec( + q_sf + producer_row_base * 16 + (gid + (lid & 1) * 8) * 16 + k * 4); +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + unsigned bf[2]; + gqa_nvfp4_load_b_frag(bf, k_rpk, lane, nt, k); + const unsigned sfb = + load_vec(k_rsf + (gid + nt * 8) * 16 + k * 4); + mma_nvfp4_e4m3(score[nt][0], score[nt][1], score[nt][2], score[nt][3], + af[0], af[1], af[2], af[3], bf[0], bf[1], sfa, sfb); + } + } + + const int row0 = producer_row_base + gid; + const int row1 = row0 + 8; + int q_head0 = 0, token0 = 0, q_head1 = 0, token1 = 0; + gqa_small_t_tc_row_to_qt(row0, TokenTile, kv_head, q_head0, token0); + gqa_small_t_tc_row_to_qt(row1, TokenTile, kv_head, q_head1, token1); + const int qabs0 = (row0 < RowCount) ? pos[token0] : -1; + const int qabs1 = (row1 < RowCount) ? pos[token1] : -1; + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const int key0 = global_k0 + col0; + const int key1 = global_k0 + col1; + const int split_begin = window_begin + split_start; + const int split_limit = window_begin + split_end; + score[nt][0] = + (row0 < RowCount && key0 >= split_begin && key0 < split_limit && key0 <= qabs0) + ? score[nt][0] * scale + : -CUDART_INF_F; + score[nt][1] = + (row0 < RowCount && key1 >= split_begin && key1 < split_limit && key1 <= qabs0) + ? score[nt][1] * scale + : -CUDART_INF_F; + score[nt][2] = + (row1 < RowCount && key0 >= split_begin && key0 < split_limit && key0 <= qabs1) + ? score[nt][2] * scale + : -CUDART_INF_F; + score[nt][3] = + (row1 < RowCount && key1 >= split_begin && key1 < split_limit && key1 <= qabs1) + ? score[nt][3] * scale + : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float alpha0 = (m0 == -CUDART_INF_F) ? 0.0f : exp2_approx((m0 - nm0) * Log2E); + const float alpha1 = (m1 == -CUDART_INF_F) ? 0.0f : exp2_approx((m1 - nm1) * Log2E); + + float bl0 = 0.0f, bl1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const float p00 = (nm0 > -CUDART_INF_F && score[nt][0] > -CUDART_INF_F) + ? exp2_approx((score[nt][0] - nm0) * Log2E) + : 0.0f; + const float p01 = (nm0 > -CUDART_INF_F && score[nt][1] > -CUDART_INF_F) + ? exp2_approx((score[nt][1] - nm0) * Log2E) + : 0.0f; + const float p10 = (nm1 > -CUDART_INF_F && score[nt][2] > -CUDART_INF_F) + ? exp2_approx((score[nt][2] - nm1) * Log2E) + : 0.0f; + const float p11 = (nm1 > -CUDART_INF_F && score[nt][3] > -CUDART_INF_F) + ? exp2_approx((score[nt][3] - nm1) * Log2E) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col0)] = __float2bfloat16(p00); + p_sw[gid * Bc + gqa_small_t_tc_swz32(gid, col1)] = __float2bfloat16(p01); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col0)] = __float2bfloat16(p10); + p_sw[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, col1)] = __float2bfloat16(p11); + } + bl0 = warp_sum<4>(bl0, FullMask); + bl1 = warp_sum<4>(bl1, FullMask); + + l0 = l0 * alpha0 + bl0; + l1 = l1 * alpha1 + bl1; + m0 = nm0; + m1 = nm1; + if (lid == 0) { + alpha_s[row0] = alpha0; + alpha_s[row1] = alpha1; + } + } + __syncthreads(); + + const bool has_next = kb + 1 < key_blocks; + if (has_next) { + const int next_k0 = k0 + Bc; + const int next_global_k0 = window_begin + next_k0; + if ((next_global_k0 & kPagedKVPageMask) == 0) { + physical_page = + physical_pages_s[(next_global_k0 >> kPagedKVPageShift) - first_global_page]; + } + NINFER_NVFP4_STAGE_TILE(next_k0, physical_page, (kb + 1) & 1); + } + + const int consumer_tile = warp % RowTiles; + const int consumer_slice = warp / RowTiles; + const int consumer_row_base = consumer_tile * 16; + __nv_bfloat16* p_consumer = &p_s[consumer_row_base * Bc]; + const float alpha0 = alpha_s[consumer_row_base + gid]; + const float alpha1 = alpha_s[consumer_row_base + gid + 8]; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + + if constexpr (Iso3V) { + // Hybrid PV: K keeps native mxf4nvf4 QK, V is decoded from ISO3 + // nibbles into a full-D swizzled BF16 tile and P x V runs on BF16 mma. + for (int idx = tid; idx < Bc * D; idx += Threads) { + const int pos = idx / D; + const int d = idx - pos * D; + const std::uint8_t byte = v_pk[pos * 128 + (d >> 1)]; + const std::uint8_t code = (d & 1) ? (byte >> 4) : (byte & 0x0F); + const float vscale = gqa_kv_nvfp4_e4m3_to_f32(v_sf[pos * 16 + (d >> 4)]); + float value = gqa_iso3_decode(code) * vscale; + const std::uint8_t rbyte = v_rpk[pos * 128 + (d >> 1)]; + const std::uint8_t rcode = (d & 1) ? (rbyte >> 4) : (rbyte & 0x0F); + const float vrscale = gqa_kv_nvfp4_e4m3_to_f32(v_rsf[pos * 16 + (d >> 4)]); + value += gqa_iso3_decode(rcode) * vrscale; + v_bf16[pos * D + gqa_small_t_tc_swz(pos, d)] = __float2bfloat16(value); + } + __syncthreads(); + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + for (int ddg = 0; ddg < DgCount; ++ddg) { + const int dg = consumer_slice * DgCount + ddg; + const int n0 = 2 * ddg; + for (int k = 0; k < PVKs; ++k) { + unsigned pf[4]; + const int pcol = k * 16 + a_coloff; + ldmatrix_x4(pf[0], pf[1], pf[2], pf[3], + smem_addr(&p_consumer[a_rowoff * Bc + + gqa_small_t_tc_swz32(a_rowoff, pcol)])); + for (int nt = 0; nt < 2; ++nt) { + unsigned vf[2]; + const int vrow = k * 16 + b_koff + b_rin; + const int vcol = dg * 16 + nt * 8; + ldmatrix_x2_t(vf[0], vf[1], + smem_addr(&v_bf16[vrow * D + + gqa_small_t_tc_swz(vrow, vcol)])); + float dd[4] = {acc[n0 + nt][0], acc[n0 + nt][1], + acc[n0 + nt][2], acc[n0 + nt][3]}; + mma_bf16(dd[0], dd[1], dd[2], dd[3], pf[0], pf[1], pf[2], pf[3], + vf[0], vf[1]); + acc[n0 + nt][0] = dd[0]; + acc[n0 + nt][1] = dd[1]; + acc[n0 + nt][2] = dd[2]; + acc[n0 + nt][3] = dd[3]; + } + } + } + } else { + // ---- native PV: quantize P*Vscale to E2M1 and run mxf4nvf4 mma ---- + std::uint8_t* ra = repack_a + warp * 16 * 64; + std::uint8_t* rb = repack_b + warp * 16 * 64; + const int row0 = consumer_row_base + gid; + const int row1 = row0 + 8; +#pragma unroll + for (int ddg = 0; ddg < DgCount; ++ddg) { + const int dg = consumer_slice * DgCount + ddg; + + // Fold-max of P * Vscale over this warp's 32-position row window. + float vmax0 = 0.0f; + float vmax1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int pos0 = nt * 8 + lid * 2; + const int pos1 = pos0 + 1; + const float sv0 = gqa_kv_nvfp4_e4m3_to_f32(v_sf[pos0 * 16 + dg]); + const float sv1 = gqa_kv_nvfp4_e4m3_to_f32(v_sf[pos1 * 16 + dg]); + if (row0 < RowCount) { + const float p0 = + __bfloat162float(p_consumer[gid * Bc + gqa_small_t_tc_swz32(gid, pos0)]) * + sv0; + const float p1 = + __bfloat162float(p_consumer[gid * Bc + gqa_small_t_tc_swz32(gid, pos1)]) * + sv1; + vmax0 = fmaxf(vmax0, fmaxf(fabsf(p0), fabsf(p1))); + } + if (row1 < RowCount) { + const float p0 = + __bfloat162float( + p_consumer[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, pos0)]) * + sv0; + const float p1 = + __bfloat162float( + p_consumer[(gid + 8) * Bc + gqa_small_t_tc_swz32(gid + 8, pos1)]) * + sv1; + vmax1 = fmaxf(vmax1, fmaxf(fabsf(p0), fabsf(p1))); + } + } + vmax0 = fmaxf(vmax0, __shfl_xor_sync(FullMask, vmax0, 1)); + vmax0 = fmaxf(vmax0, __shfl_xor_sync(FullMask, vmax0, 2)); + vmax1 = fmaxf(vmax1, __shfl_xor_sync(FullMask, vmax1, 1)); + vmax1 = fmaxf(vmax1, __shfl_xor_sync(FullMask, vmax1, 2)); + const float sc0 = fmaxf(vmax0 / 6.0f, kNvfp4MinScale); + const float sc1 = fmaxf(vmax1 / 6.0f, kNvfp4MinScale); + psc_s[row0 * 64 + dg * 4 + lid] = + row0 < RowCount ? gqa_kv_nvfp4_fp32_to_e4m3(sc0) : kNvfp4E4M3One; + psc_s[row1 * 64 + dg * 4 + lid] = + row1 < RowCount ? gqa_kv_nvfp4_fp32_to_e4m3(sc1) : kNvfp4E4M3One; + __syncwarp(); + +#pragma unroll + for (int i = lane; i < (16 * 64) / 16; i += 32) { + reinterpret_cast(ra)[i] = make_uint4(0, 0, 0, 0); + reinterpret_cast(rb)[i] = make_uint4(0, 0, 0, 0); + } + __syncwarp(); + + // A = packed P*Vscale values (rows 0..15, 16 bytes of k per row). + for (int i = lane; i < 16 * 16; i += 32) { + const int r = i >> 4; + const int byte = i & 15; + const int pos = byte * 2; + const int abs_row = consumer_row_base + r; + if (abs_row < RowCount) { + const float sv0 = gqa_kv_nvfp4_e4m3_to_f32(v_sf[pos * 16 + dg]); + const float sv1 = gqa_kv_nvfp4_e4m3_to_f32(v_sf[(pos + 1) * 16 + dg]); + const float f0 = + __bfloat162float( + p_consumer[r * Bc + gqa_small_t_tc_swz32(r, pos)]) * + sv0; + const float f1 = + __bfloat162float( + p_consumer[r * Bc + gqa_small_t_tc_swz32(r, pos + 1)]) * + sv1; + float sc = gqa_kv_nvfp4_e4m3_to_f32( + psc_s[abs_row * 64 + dg * 4 + (pos >> 4)]); + // The minimum fold scale (2^-9) rounds to the E4M3 zero + // code; keep the v15 guard so an all-tiny block still + // quantizes instead of dividing by zero. + if (sc < kNvfp4MinScale) { sc = kNvfp4MinScale; } + ra[r * 64 + byte] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(f0 / sc) | + (gqa_kv_nvfp4_e2m1_nibble(f1 / sc) << 4)); + } else { + ra[r * 64 + byte] = 0; + } + } + // B = packed V codes for dg's 16 output dims. + for (int i = lane; i < 16 * 16; i += 32) { + const int dd = i >> 4; + const int byte = i & 15; + const int pos = byte * 2; + const std::uint8_t b0 = v_pk[pos * 128 + dg * 8 + (dd >> 1)]; + const std::uint8_t b1 = v_pk[(pos + 1) * 128 + dg * 8 + (dd >> 1)]; + const std::uint8_t n0 = (dd & 1) ? (b0 >> 4) : (b0 & 0x0Fu); + const std::uint8_t n1 = (dd & 1) ? (b1 >> 4) : (b1 & 0x0Fu); + rb[dd * 64 + byte] = static_cast(n0 | (n1 << 4)); + } + __syncwarp(); + + unsigned af[4]; + gqa_nvfp4_load_a_frag_64(af, ra, lane); + const unsigned sfa = load_vec( + psc_s + (consumer_row_base + (gid + (lid & 1) * 8)) * 64 + dg * 4); +#pragma unroll + for (int nt = 0; nt < 2; ++nt) { + unsigned bf[2]; + gqa_nvfp4_load_b_frag_64(bf, rb, lane, nt); + float dd[4] = {acc[2 * ddg + nt][0], acc[2 * ddg + nt][1], + acc[2 * ddg + nt][2], acc[2 * ddg + nt][3]}; + mma_nvfp4_e4m3(dd[0], dd[1], dd[2], dd[3], af[0], af[1], af[2], af[3], bf[0], + bf[1], sfa, kOnesScale); + acc[2 * ddg + nt][0] = dd[0]; + acc[2 * ddg + nt][1] = dd[1]; + acc[2 * ddg + nt][2] = dd[2]; + acc[2 * ddg + nt][3] = dd[3]; + } + __syncwarp(); + } + } // Iso3V PV branch + if (has_next) { ninfer::ops::cp_wait<0>(); } + __syncthreads(); + } + + if (warp < RowTiles && lid == 0) { + const int row0 = warp * 16 + gid; + const int row1 = row0 + 8; + if (row0 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, TokenTile, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, TokenTile)] = m0; + partial_l[gqa_partial_stat_index(q_head, token, split, TokenTile)] = l0; + } + if (row1 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, TokenTile, kv_head, q_head, token); + partial_m[gqa_partial_stat_index(q_head, token, split, TokenTile)] = m1; + partial_l[gqa_partial_stat_index(q_head, token, split, TokenTile)] = l1; + } + } + +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + const int consumer_tile = warp % RowTiles; + const int consumer_slice = warp / RowTiles; + const int consumer_row_base = consumer_tile * 16; + const int d0 = (consumer_slice * PVNtPerWarp + n) * 8 + 2 * lid; + const int row0 = consumer_row_base + gid; + const int row1 = row0 + 8; + if (row0 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row0, TokenTile, kv_head, q_head, token); + const std::int64_t dst = + gqa_partial_acc_index(q_head, d0, token, split, TokenTile); + *reinterpret_cast(&partial_acc[dst]) = pack_bf16x2(acc[n][0], acc[n][1]); + } + if (row1 < RowCount) { + int q_head = 0; + int token = 0; + gqa_small_t_tc_row_to_qt(row1, TokenTile, kv_head, q_head, token); + const std::int64_t dst = + gqa_partial_acc_index(q_head, d0, token, split, TokenTile); + *reinterpret_cast(&partial_acc[dst]) = pack_bf16x2(acc[n][2], acc[n][3]); + } + } +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_geometry.cuh b/src/ops/kernel/gqa_attention_geometry.cuh new file mode 100644 index 0000000000..9f67e275d0 --- /dev/null +++ b/src/ops/kernel/gqa_attention_geometry.cuh @@ -0,0 +1,25 @@ +#pragma once + +// Exact grouped-query head geometries served by the Qwen3.6 GQA kernels. Head +// dimension, cache format, and tile policy are shared; head mapping remains a +// compile-time property so each registered shape gets an independent kernel. + +namespace ninfer::ops { + +template +struct GqaGeometry { + static_assert(QHeadsValue > 0 && KVHeadsValue > 0); + static_assert(QHeadsValue % KVHeadsValue == 0); + static_assert(DecodeSplitScaleValue > 0); + + static constexpr int QHeads = QHeadsValue; + static constexpr int KVHeads = KVHeadsValue; + static constexpr int GroupSize = QHeads / KVHeads; + static constexpr int DecodeSplitScale = DecodeSplitScaleValue; + static constexpr int DecodeSplits = 85 * DecodeSplitScale; +}; + +using Gqa27Geometry = GqaGeometry<24, 4, 1>; +using Gqa35Geometry = GqaGeometry<16, 2, 2>; + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_kv_nvfp4.cuh b/src/ops/kernel/gqa_attention_kv_nvfp4.cuh new file mode 100644 index 0000000000..4d7b4c3a5b --- /dev/null +++ b/src/ops/kernel/gqa_attention_kv_nvfp4.cuh @@ -0,0 +1,139 @@ +#pragma once + +// ninfer::ops - packed E2M1 NVFP4, per-token 16-channel-scale KV cache codec. +// +// The cache stores two planes per K/V tensor: +// * code plane: two 4-bit E2M1 words per byte, d-contiguous, leading +// extent = head_dim / 2 bytes per token row; +// * scale plane: one E4M3FN byte per contiguous 16-channel group, leading +// extent = head_dim / 16 bytes per token row. +// +// Append quantizes BF16 source values x as +// s = max(E4M3_RNE(max_i |x_i| / 6), 2^-9) +// code[i] = E2M1_round_to_nearest(x_i / s) +// decode = E2M1(code[i]) * s. +// K may carry the per-4-channel orthogonal rotation applied by the caller; +// the codec itself is rotation-agnostic. + +#include "ops/common/math.cuh" +#include "ops/common/memory.cuh" +#include "ops/kernel/paged_kv_address.cuh" + +#include + +#include + +namespace ninfer::ops { + +inline constexpr int kGqaKvNvfp4HeadDim = 256; +inline constexpr int kGqaKvNvfp4Group = 16; +inline constexpr int kGqaKvNvfp4Groups = kGqaKvNvfp4HeadDim / kGqaKvNvfp4Group; +inline constexpr int kGqaKvNvfp4CodeLead = kGqaKvNvfp4HeadDim / 2; +inline constexpr int kGqaKvNvfp4ScaleLead = kGqaKvNvfp4Groups; + +__device__ __forceinline__ std::uint8_t gqa_kv_nvfp4_e2m1_nibble(float x) { + const float a = fabsf(x); + std::uint8_t c; + if (a < 0.25f) { c = 0; } + else if (a < 0.75f) { c = 1; } + else if (a < 1.25f) { c = 2; } + else if (a < 1.75f) { c = 3; } + else if (a < 2.5f) { c = 4; } + else if (a < 3.5f) { c = 5; } + else if (a < 5.0f) { c = 6; } + else { c = 7; } + if (x < 0.0f) { c |= 0x08u; } + return c; +} + +__device__ __forceinline__ float gqa_kv_nvfp4_e2m1_to_f32(std::uint8_t code) { + const std::uint8_t mag = code & 0x07u; + float magnitude; + if (mag == 0) { magnitude = 0.0f; } + else if (mag == 1) { magnitude = 0.5f; } + else if (mag == 2) { magnitude = 1.0f; } + else if (mag == 3) { magnitude = 1.5f; } + else if (mag == 4) { magnitude = 2.0f; } + else if (mag == 5) { magnitude = 3.0f; } + else if (mag == 6) { magnitude = 4.0f; } + else { magnitude = 6.0f; } + return (code & 0x08u) != 0 ? -magnitude : magnitude; +} + +// Round-to-nearest-even E4M3FN byte. Values below the smallest normal roll up +// through the denormal mantissa; zero stays zero. +__device__ __forceinline__ std::uint8_t gqa_kv_nvfp4_fp32_to_e4m3(float x) { + if (!(x > 0.0f)) { return 0; } + const std::uint32_t bits = __float_as_uint(x); + const std::uint32_t sign = (bits >> 24) & 0x80u; + int exponent = static_cast((bits >> 23) & 0xffu) - 127 + 7; + if (exponent >= 15) { return static_cast(sign | (15u << 3) | 7u); } + if (exponent <= 0) { + // E4M3FN denormals decode as mantissa / 512 (mantissa * 2^-9), so + // the encoder must quantize x * 512, not x * 64. + int mantissa = static_cast(roundf(x * 512.0f)); + if (mantissa <= 0) { return static_cast(sign); } + if (mantissa >= 8) { return static_cast(sign | (1u << 3)); } + return static_cast(sign | mantissa); + } + std::uint32_t mantissa = (bits >> 20) & 0x7u; + const std::uint32_t guard = (bits >> 19) & 1u; + const std::uint32_t sticky = bits & 0x7ffffu; + if (guard && (sticky || (mantissa & 1u))) { + mantissa += 1; + if (mantissa > 7) { + mantissa = 0; + exponent += 1; + if (exponent >= 15) { return static_cast(sign | (15u << 3) | 7u); } + } + } + return static_cast(sign | (exponent << 3) | mantissa); +} + +__device__ __forceinline__ float gqa_kv_nvfp4_e4m3_to_f32(std::uint8_t byte) { + const int exponent = (byte >> 3) & 0x0F; + const int mantissa = byte & 0x07; + if (exponent == 0) { return static_cast(mantissa) / 512.0f; } + return ldexpf(1.0f + static_cast(mantissa) / 8.0f, exponent - 7); +} + +template +__device__ __forceinline__ std::int64_t gqa_kv_nvfp4_code_index(int physical_page, int kv_head, + int d, int page_offset) { + return paged_kv_element_offset( + physical_page, kv_head, page_offset, d >> 1); +} + +template +__device__ __forceinline__ std::int64_t gqa_kv_nvfp4_scale_index(int physical_page, int kv_head, + int group, int page_offset) { + return paged_kv_element_offset( + physical_page, kv_head, page_offset, group); +} + +template +__device__ __forceinline__ std::int64_t gqa_kv_nvfp4_src_index(int kv_head, int d, int token) { + return static_cast(d) + + static_cast(kGqaKvNvfp4HeadDim) * + (static_cast(kv_head) + + static_cast(Geometry::KVHeads) * token); +} + +// Dequantize 8 consecutive E2M1 codes (dims [d, d+8), inside one 16-group) +// with the group's E4M3 scale into 8 BF16 packed as an int4. codes8 points +// at the four packed bytes. +__device__ __forceinline__ int4 gqa_kv_dequant_nvfp4x8_from(const std::uint8_t* codes8, float scale) { + const int raw = load_vec(codes8); + const std::uint8_t* c = reinterpret_cast(&raw); + unsigned packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const float x0 = gqa_kv_nvfp4_e2m1_to_f32(c[i] & 0x0Fu) * scale; + const float x1 = gqa_kv_nvfp4_e2m1_to_f32(c[i] >> 4) * scale; + packed[i] = pack_bf16x2(x0, x1); + } + return make_int4(static_cast(packed[0]), static_cast(packed[1]), + static_cast(packed[2]), static_cast(packed[3])); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_kv_quant.cuh b/src/ops/kernel/gqa_attention_kv_quant.cuh new file mode 100644 index 0000000000..061cb726de --- /dev/null +++ b/src/ops/kernel/gqa_attention_kv_quant.cuh @@ -0,0 +1,77 @@ +#pragma once + +// ninfer::ops - signed int8, per-token group-wise KV cache codec (shared device +// helpers). Quantization (append) and dequantization (stage) are FUSED into the +// GQA attention kernels themselves (decode partial kernel, prefill fill/attention); +// this header only provides the index math, the vectorized dequant, and the scalar +// quantize helper they share. There is deliberately no standalone quant/dequant +// kernel: that would defeat the halved-bandwidth goal. + +#include "ops/common/math.cuh" +#include "ops/common/memory.cuh" +#include "ops/kernel/paged_kv_address.cuh" + +#include +#include + +#include + +namespace ninfer::ops { + +inline constexpr int kGqaKvQuantHeadDim = 256; +inline constexpr int kGqaKvQuantGroup = 64; +inline constexpr int kGqaKvQuantGroups = kGqaKvQuantHeadDim / kGqaKvQuantGroup; + +template +__device__ __forceinline__ std::int64_t gqa_kv_quant_code_index(int physical_page, int kv_head, + int d, int page_offset) { + return paged_kv_element_offset(physical_page, kv_head, + page_offset, d); +} + +template +__device__ __forceinline__ std::int64_t gqa_kv_quant_scale_index(int physical_page, int kv_head, + int group, int page_offset) { + return paged_kv_element_offset(physical_page, kv_head, + page_offset, group); +} + +template +__device__ __forceinline__ std::int64_t gqa_kv_quant_src_index(int kv_head, int d, int token) { + return static_cast(d) + + static_cast(kGqaKvQuantHeadDim) * + (static_cast(kv_head) + + static_cast(Geometry::KVHeads) * token); +} + +// Quantize one bf16 value with a precomputed 1/scale (scale is the FP16-rounded +// per-group absmax/127). Round-to-nearest-even + symmetric clamp to keep codes +// bit-identical to the CPU oracle and to bf16 parity. +__device__ __forceinline__ std::int8_t gqa_kv_quant_code(float x, float inv_scale) { + if (inv_scale == 0.0f) { return static_cast(0); } + int q = __float2int_rn(x * inv_scale); + q = max(-127, min(127, q)); + return static_cast(q); +} + +// Dequantize 8 consecutive int8 codes (dims [d, d+8), aligned to a multiple of 8 +// so they lie inside one 64-group) into 8 bf16 packed as an int4, given a pointer +// to the 8 codes and the group's dequant scale. The codes are read with ONE 64-bit +// (int2) load; the pointer may be in global or shared memory. This keeps the dequant +// ALU identical whether the codes were streamed via cp.async into smem (decode) or +// read directly from the cache (prefill). +__device__ __forceinline__ int4 gqa_kv_dequant_i8x8_from(const std::int8_t* codes8, float s) { + const int2 raw = load_vec(codes8); + const std::int8_t* c = reinterpret_cast(&raw); + unsigned packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const float x0 = static_cast(c[2 * i]) * s; + const float x1 = static_cast(c[2 * i + 1]) * s; + packed[i] = pack_bf16x2(x0, x1); + } + return make_int4(static_cast(packed[0]), static_cast(packed[1]), + static_cast(packed[2]), static_cast(packed[3])); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_prefill_bf16.cuh b/src/ops/kernel/gqa_attention_prefill_bf16.cuh new file mode 100644 index 0000000000..d2897849d1 --- /dev/null +++ b/src/ops/kernel/gqa_attention_prefill_bf16.cuh @@ -0,0 +1,454 @@ +#pragma once + +// BF16-only GQA prompt kernel. INT8 has an independent kernel body and resource +// policy in gqa_attention_prefill_i8.cuh. +// +// * Br = 64 query rows and Bc = 64 key columns per CTA tile. +// * 4 warps / 128 threads; each warp owns 16 query rows of the tile. +// * Q, K, V staged in 96 KiB of dynamic shared memory (single-buffered), with +// the cp.async of the next K/V tile overlapped against the current +// QK / PV tensor-core work (exactly FA's single-buffer overlap pattern). +// * m16n8k16 bf16 MMA for both S = Q Kᵀ and O += P V, online softmax in exp2. +// +// The op first writes the new chunk K/V into absolute positions in the paged cache, +// then computes causal GQA attention for +// every chunk token over all cached history using bottom-right causal alignment +// (query row i attends to keys [0, base_pos + i]). + +#include + +#include "ops/kernel/gqa_attention_prefill_common.cuh" + +namespace ninfer::ops { + +template +__global__ void gqa_attention_prefill_fill_bf16_kernel( + const __nv_bfloat16* __restrict__ k, const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, Metadata metadata, + __nv_bfloat16* __restrict__ cache_k, __nv_bfloat16* __restrict__ cache_v, std::int32_t width) { + constexpr int VecElems = 8; // 8 bf16 == 16 B, matching the cache row alignment. + const int tokens = metadata.valid_tokens(width); + const std::int64_t idx = static_cast(blockIdx.x) * blockDim.x + threadIdx.x; + const std::int64_t n = + static_cast(tokens) * Geometry::KVHeads * (kGqaPrefillHeadDim / VecElems); + if (idx >= n) { return; } + + const int vec = static_cast(idx % (kGqaPrefillHeadDim / VecElems)); + const int tmp = static_cast(idx / (kGqaPrefillHeadDim / VecElems)); + const int kv_head = tmp % Geometry::KVHeads; + const int token = tmp / Geometry::KVHeads; + const int d = vec * VecElems; + const int position = positions[0] + token; + const int lane = static_cast(threadIdx.x) & 31; + const std::int32_t* block_table = metadata.block_table(); + int physical_page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + const std::int64_t src_off = + static_cast(d) + + static_cast(kGqaPrefillHeadDim) * (kv_head + Geometry::KVHeads * token); + const int4 k_value = load_vec(&k[src_off]); + const int4 v_value = load_vec(&v[src_off]); + + physical_page = __shfl_sync(0xffffffffu, physical_page, 0); + + const std::int64_t cache_off = paged_kv_element_offset( + physical_page, kv_head, position & kPagedKVPageMask, d); + store_vec(&cache_k[cache_off], k_value); + store_vec(&cache_v[cache_off], v_value); +} + +// Stage one [Bc, D] K or V tile from the per-kv-head contiguous cache into the +// swizzled smem buffer. Keys beyond max_query_abs (which the causal mask always +// drops) are zeroed so the padded/uninitialized cache tail never feeds NaNs into +// the tensor cores. Mirrors FA's predicated K/V cp.async + Clear_OOB path. +template +__device__ __forceinline__ void gqa_prefill_stage_kv(__nv_bfloat16* dst, const __nv_bfloat16* cache, + int kv_head, int k0, int max_query_abs, + int physical_page, int tid) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Bc = kGqaPrefillBc; + constexpr int Threads = kGqaPrefillThreads; + constexpr int VecPerRow = D / 8; // 8 bf16 per 16B cp.async + const bool full_tile = (k0 + Bc - 1) <= max_query_abs; + // Block base pointer computed once (int64); per-element offsets stay 32-bit. + const __nv_bfloat16* cache_block = + cache + paged_kv_element_offset( + physical_page, kv_head, k0 & kPagedKVPageMask, 0); + if (full_tile) { +#pragma unroll + for (int chunk = tid; chunk < Bc * VecPerRow; chunk += Threads) { + const int key_l = chunk >> 5; // / VecPerRow (32) + const int d = (chunk & 31) << 3; // (chunk % 32) * 8 + __nv_bfloat16* p = &dst[key_l * D + gqa_prefill_swz(key_l, d)]; + cp_async<16, Cache::cg>(p, &cache_block[key_l * D + d]); + } + } else { +#pragma unroll + for (int chunk = tid; chunk < Bc * VecPerRow; chunk += Threads) { + const int key_l = chunk >> 5; // / VecPerRow (32) + const int d = (chunk & 31) << 3; // (chunk % 32) * 8 + __nv_bfloat16* p = &dst[key_l * D + gqa_prefill_swz(key_l, d)]; + if ((k0 + key_l) <= max_query_abs) { + cp_async<16, Cache::cg>(p, &cache_block[key_l * D + d]); + } else { + store_vec(p, make_int4(0, 0, 0, 0)); + } + } + } +} + +// FlashAttention-2 forward, one CTA per (query 64-row block, query head). Grid is +// (ceil(tokens/64), q_heads). seqlen_q = tokens, seqlen_k = base_pos + tokens, with +// bottom-right causal alignment (query row i sees keys [0, base_pos + i]). +template +__launch_bounds__(kGqaPrefillThreads, 1) __global__ + void gqa_attention_prefill_bf16_kernel(const __nv_bfloat16* __restrict__ q, + const __nv_bfloat16* __restrict__ cache_k, + const __nv_bfloat16* __restrict__ cache_v, + Metadata metadata, + const std::int32_t* __restrict__ positions, float scale, + __nv_bfloat16* __restrict__ out, std::int32_t width) { + constexpr int D = kGqaPrefillHeadDim; // 256 + constexpr int Br = kGqaPrefillBr; // 64 query rows + constexpr int Bc = kGqaPrefillBc; // 64 key cols + constexpr int Threads = kGqaPrefillThreads; // 128 + constexpr int QKNt = Bc / 8; // 8 QK score n-tiles + constexpr int QKKs = D / 16; // 16 QK contraction steps over head_dim + constexpr int PVNt = D / 8; // 32 PV output n-tiles + constexpr int PVKs = Bc / 16; // 4 PV contraction steps over keys + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + + static_assert(Threads == 128); + + extern __shared__ __align__(16) __nv_bfloat16 gqa_smem[]; + __nv_bfloat16* q_s = gqa_smem; // [Br, D] swizzled + __nv_bfloat16* k_s = q_s + Br * D; // [Bc, D] swizzled + __nv_bfloat16* v_s = k_s + Bc * D; // [Bc, D] swizzled + + const int q_block = static_cast(blockIdx.x); + const int q_head = static_cast(blockIdx.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + const int q0 = q_block * Br; + const int kv_head = q_head / Geometry::GroupSize; + const int tokens = metadata.valid_tokens(width); + + if (q_head >= Geometry::QHeads || q0 >= width) { return; } + if (q0 >= tokens) { + gqa_prefill_zero_output_rows(out, q_head, q0, min(q0 + Br, width), tid, Threads); + return; + } + const int base_pos = positions[0]; + const std::int32_t* block_table = metadata.block_table(); + + const int gid = lane >> 2; + const int lid = lane & 3; + + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + const int warp_row0 = warp * 16; // this warp owns rows [warp_row0, warp_row0+16) + + // Per-lane precomputed swizzled ldmatrix base addresses (see gqa_prefill_swz_addr). + const unsigned q_sbase = smem_addr(q_s); + const unsigned k_sbase = smem_addr(k_s); + const unsigned v_sbase = smem_addr(v_s); + // Q A-fragment: row = warp_row0 + a_rowoff, col = k*16 + a_coloff. + const unsigned q_lane_base = q_sbase + static_cast((warp_row0 + a_rowoff) * 512); + const unsigned q_as = static_cast((a_mat >> 1) << 4); + const unsigned q_r = static_cast(a_rin << 4); + // K B-fragment via ldmatrix.x4 (2 n-tiles/instr): lanes 16-31 fetch the +8-key + // half (extra 4096 bytes), lanes with bit3 set fetch the +8 d-contract half. + const unsigned k_lane_base = + k_sbase + static_cast(b_rin * 512) + (static_cast(lane >> 4) << 12); + const unsigned k_as = static_cast((b_koff >> 3) << 4); + const unsigned k_r = static_cast(b_rin << 4); + // V B-fragment via ldmatrix.x4.trans (2 n-tiles/instr): row = k*16 + (bit3)*8 + b_rin, + // col = n*8 + (lane>>4)*8. + const unsigned v_lane_base = v_sbase + static_cast(((lane >> 3) & 1) * 4096) + + static_cast(b_rin * 512); + const unsigned v_as = static_cast((lane >> 4) << 4); + const unsigned v_r = static_cast(b_rin << 4); + + // Stage Q into smem once via cp.async (overlaps with the K(0) prologue load + // below); it stays resident for the whole key loop. Global Q rows are 256 bf16 + // contiguous, with a token stride of 256*QHeads. + { + constexpr int VecPerRow = D / 8; + constexpr int QRowStride = D * Geometry::QHeads; // global stride between tokens + const __nv_bfloat16* q_block = q + gqa_prefill_q_index(q_head, 0, q0); + if (q0 + Br <= tokens) { +#pragma unroll + for (int chunk = tid; chunk < Br * VecPerRow; chunk += Threads) { + const int row = chunk >> 5; + const int d = (chunk & 31) << 3; + __nv_bfloat16* p = &q_s[row * D + gqa_prefill_swz(row, d)]; + cp_async<16, Cache::cg>(p, &q_block[row * QRowStride + d]); + } + } else { +#pragma unroll + for (int chunk = tid; chunk < Br * VecPerRow; chunk += Threads) { + const int row = chunk >> 5; + const int d = (chunk & 31) << 3; + __nv_bfloat16* p = &q_s[row * D + gqa_prefill_swz(row, d)]; + if (q0 + row < tokens) { + cp_async<16, Cache::cg>(p, &q_block[row * QRowStride + d]); + } else { + store_vec(p, make_int4(0, 0, 0, 0)); + } + } + } + } + + float acc[PVNt][4]; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F, l0 = 0.0f, l1 = 0.0f; + + const int tile_rows = min(Br, tokens - q0); + const int max_query_abs = base_pos + q0 + tile_rows - 1; + const int n_block_max = (max_query_abs / Bc) + 1; // n_block_min == 0 + + // Fold softmax_scale into the exp2 (FA-style): scores stay raw, so the + // per-element "* scale" multiply drops out of the QK epilogue entirely. + const float scale_l2 = scale * Log2E; + int physical_page = block_table[0]; + + // Prologue: commit Q, then kick off K(0). The loop's wait<0> below drains both. + ninfer::ops::cp_commit(); + gqa_prefill_stage_kv(k_s, cache_k, kv_head, 0, max_query_abs, physical_page, tid); + ninfer::ops::cp_commit(); + + for (int kb = 0; kb < n_block_max; ++kb) { + const int k0 = kb * Bc; + const int next_physical_page = (kb + 1 < n_block_max) ? block_table[kb + 1] : physical_page; + + ninfer::ops::cp_wait<0>(); // K(kb) landed (also publishes q_s / prev PV done) + __syncthreads(); + + // Overlap V(kb) load against the QK MMA below. + gqa_prefill_stage_kv(v_s, cache_v, kv_head, k0, max_query_abs, physical_page, + tid); + ninfer::ops::cp_commit(); + + // S = Q Kᵀ for this warp's 16 rows over all Bc keys, in registers. + // Software-pipelined like cute's gemm: issue the ldmatrix for contraction + // step k+1 while the m16n8k16 MMAs for step k run, so the LSU (ldmatrix) + // and tensor pipes overlap instead of stalling on each other. + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; + } + // Swizzled ldmatrix addresses via precomputed per-lane bases + immediates. + unsigned af[2][4]; + unsigned bf[2][QKNt][2]; + { + ldmatrix_x4(af[0][0], af[0][1], af[0][2], af[0][3], + gqa_prefill_swz_addr(q_lane_base, 0u, q_as, q_r)); +#pragma unroll + for (int nt2 = 0; nt2 < QKNt; nt2 += 2) { + ldmatrix_x4(bf[0][nt2][0], bf[0][nt2][1], bf[0][nt2 + 1][0], bf[0][nt2 + 1][1], + gqa_prefill_swz_addr(k_lane_base + static_cast(nt2 * 4096), + 0u, k_as, k_r)); + } + } +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + const int cur = k & 1; + const int nxt = cur ^ 1; + if (k + 1 < QKKs) { + const unsigned ck = static_cast((k + 1) << 5); + ldmatrix_x4(af[nxt][0], af[nxt][1], af[nxt][2], af[nxt][3], + gqa_prefill_swz_addr(q_lane_base, ck, q_as, q_r)); +#pragma unroll + for (int nt2 = 0; nt2 < QKNt; nt2 += 2) { + ldmatrix_x4( + bf[nxt][nt2][0], bf[nxt][nt2][1], bf[nxt][nt2 + 1][0], bf[nxt][nt2 + 1][1], + gqa_prefill_swz_addr(k_lane_base + static_cast(nt2 * 4096), ck, + k_as, k_r)); + } + } +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + mma_bf16(score[nt][0], score[nt][1], score[nt][2], score[nt][3], af[cur][0], + af[cur][1], af[cur][2], af[cur][3], bf[cur][nt][0], bf[cur][nt][1]); + } + } + + const int row0 = warp_row0 + gid; + const int row1 = warp_row0 + gid + 8; + const int qrow0 = q0 + row0; + const int qrow1 = q0 + row1; + const int qabs0 = (qrow0 < tokens) ? base_pos + qrow0 : -1; + const int qabs1 = (qrow1 < tokens) ? base_pos + qrow1 : -1; + const bool full_score_tile = (q0 + Br <= tokens) && ((k0 + Bc - 1) <= (base_pos + q0)); + + // block row-max on raw (unscaled) scores; scale is folded into exp2 below + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; + if (full_score_tile) { +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + } else { +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int key0 = k0 + nt * 8 + 2 * lid; + const int key1 = key0 + 1; + score[nt][0] = (qrow0 < tokens && key0 <= qabs0) ? score[nt][0] : -CUDART_INF_F; + score[nt][1] = (qrow0 < tokens && key1 <= qabs0) ? score[nt][1] : -CUDART_INF_F; + score[nt][2] = (qrow1 < tokens && key0 <= qabs1) ? score[nt][2] : -CUDART_INF_F; + score[nt][3] = (qrow1 < tokens && key1 <= qabs1) ? score[nt][3] : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float nm0_scaled = nm0 * scale_l2; + const float nm1_scaled = nm1 * scale_l2; + const float alpha0 = exp2_approx(__fmaf_rn(m0, scale_l2, -nm0_scaled)); + const float alpha1 = exp2_approx(__fmaf_rn(m1, scale_l2, -nm1_scaled)); + + // P = exp2(S - m), repacked into the PV A-fragment layout, plus local block row-sum. + // The row-sum allreduce is deferred to the epilogue; only row max must be reduced per tile. + float bl0 = 0.0f, bl1 = 0.0f; + unsigned p_frag[PVKs][4]; + if (full_score_tile) { +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const float p00 = exp2_approx(__fmaf_rn(score[nt][0], scale_l2, -nm0_scaled)); + const float p01 = exp2_approx(__fmaf_rn(score[nt][1], scale_l2, -nm0_scaled)); + const float p10 = exp2_approx(__fmaf_rn(score[nt][2], scale_l2, -nm1_scaled)); + const float p11 = exp2_approx(__fmaf_rn(score[nt][3], scale_l2, -nm1_scaled)); + bl0 += p00 + p01; + bl1 += p10 + p11; + const int pk = nt >> 1; + if ((nt & 1) == 0) { + p_frag[pk][0] = pack_bf16x2(p00, p01); + p_frag[pk][1] = pack_bf16x2(p10, p11); + } else { + p_frag[pk][2] = pack_bf16x2(p00, p01); + p_frag[pk][3] = pack_bf16x2(p10, p11); + } + } + } else { +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const float p00 = (score[nt][0] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][0], scale_l2, -nm0_scaled)) + : 0.0f; + const float p01 = (score[nt][1] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][1], scale_l2, -nm0_scaled)) + : 0.0f; + const float p10 = (score[nt][2] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][2], scale_l2, -nm1_scaled)) + : 0.0f; + const float p11 = (score[nt][3] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][3], scale_l2, -nm1_scaled)) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + const int pk = nt >> 1; + if ((nt & 1) == 0) { + p_frag[pk][0] = pack_bf16x2(p00, p01); + p_frag[pk][1] = pack_bf16x2(p10, p11); + } else { + p_frag[pk][2] = pack_bf16x2(p00, p01); + p_frag[pk][3] = pack_bf16x2(p10, p11); + } + } + } + + l0 = __fmaf_rn(l0, alpha0, bl0); + l1 = __fmaf_rn(l1, alpha1, bl1); + m0 = nm0; + m1 = nm1; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + + ninfer::ops::cp_wait<0>(); // V(kb) landed; QK done reading k_s + __syncthreads(); + + // Prefetch K(kb+1) into the (now-free) K buffer, overlapping the PV MMA. + if (kb + 1 < n_block_max) { + physical_page = next_physical_page; + gqa_prefill_stage_kv(k_s, cache_k, kv_head, (kb + 1) * Bc, max_query_abs, + physical_page, tid); + ninfer::ops::cp_commit(); + } + + // O += P V, contracting over the Bc keys. The (k, n) iteration space is + // flattened and software-pipelined: the transposed ldmatrix for the next + // V fragment is issued while the current MMA runs. + // Each x4.trans load covers 2 output n-tiles (16 dims); pipeline the next + // load against the current pair of MMAs. + constexpr int PVHalf = PVNt / 2; // 16 n-tile pairs + constexpr int PVLoads = PVKs * PVHalf; // 64 x4.trans loads + // Swizzled V x4.trans addresses via precomputed per-lane base + immediates. + unsigned vf[2][4]; + { + ldmatrix_x4_t(vf[0][0], vf[0][1], vf[0][2], vf[0][3], + gqa_prefill_swz_addr(v_lane_base, 0u, v_as, v_r)); + } +#pragma unroll + for (int li = 0; li < PVLoads; ++li) { + const int k = li / PVHalf; + const int n2 = (li % PVHalf) * 2; + const int cur = li & 1; + const int nxt = cur ^ 1; + if (li + 1 < PVLoads) { + const int k2 = (li + 1) / PVHalf; + const int n2b = ((li + 1) % PVHalf) * 2; + const unsigned ckv = static_cast(n2b << 4); + ldmatrix_x4_t(vf[nxt][0], vf[nxt][1], vf[nxt][2], vf[nxt][3], + gqa_prefill_swz_addr(v_lane_base + static_cast(k2 * 8192), + ckv, v_as, v_r)); + } + mma_bf16(acc[n2][0], acc[n2][1], acc[n2][2], acc[n2][3], p_frag[k][0], p_frag[k][1], + p_frag[k][2], p_frag[k][3], vf[cur][0], vf[cur][1]); + mma_bf16(acc[n2 + 1][0], acc[n2 + 1][1], acc[n2 + 1][2], acc[n2 + 1][3], p_frag[k][0], + p_frag[k][1], p_frag[k][2], p_frag[k][3], vf[cur][2], vf[cur][3]); + } + } + + l0 = warp_sum<4>(l0, FullMask); + l1 = warp_sum<4>(l1, FullMask); + + // Normalize once per row via reciprocal-multiply instead of 128 IEEE divides. + const float inv_l0 = (l0 > 0.0f) ? __frcp_rn(l0) : 0.0f; + const float inv_l1 = (l1 > 0.0f) ? __frcp_rn(l1) : 0.0f; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + const int d0 = n * 8 + 2 * lid; + const int qrow0 = q0 + warp_row0 + gid; + const int qrow1 = q0 + warp_row0 + gid + 8; + if (qrow0 < tokens) { + *reinterpret_cast(&out[gqa_prefill_q_index(q_head, d0, qrow0)]) = + pack_bf16x2(acc[n][0] * inv_l0, acc[n][1] * inv_l0); + } + if (qrow1 < tokens) { + *reinterpret_cast(&out[gqa_prefill_q_index(q_head, d0, qrow1)]) = + pack_bf16x2(acc[n][2] * inv_l1, acc[n][3] * inv_l1); + } + } + gqa_prefill_zero_output_rows(out, q_head, tokens, min(q0 + Br, width), tid, Threads); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_prefill_common.cuh b/src/ops/kernel/gqa_attention_prefill_common.cuh new file mode 100644 index 0000000000..cd08b52a5a --- /dev/null +++ b/src/ops/kernel/gqa_attention_prefill_common.cuh @@ -0,0 +1,98 @@ +#pragma once + +// Shared Qwen3.6 GQA dimensions and leaf PTX helpers used by the independently tuned +// BF16 and INT8 prompt kernels. This file deliberately owns no staging policy, +// shared-memory arena, warp schedule, or kernel body. + +#include "ops/common/math.cuh" +#include "ops/common/mma.cuh" +#include "ops/common/warp.cuh" +#include "ops/kernel/gqa_attention_geometry.cuh" +#include "ops/kernel/paged_kv_address.cuh" + +#include + +#include + +namespace ninfer::ops { + +inline constexpr int kGqaPrefillHeadDim = 256; + +inline constexpr int kGqaPrefillBr = 64; +inline constexpr int kGqaPrefillBc = 64; +inline constexpr int kGqaPrefillThreads = 128; +inline constexpr int kGqaPrefillSmemBytes = (kGqaPrefillBr + 2 * kGqaPrefillBc) * + kGqaPrefillHeadDim * + static_cast(sizeof(__nv_bfloat16)); + +// NVFP4 prefill runs a warp-specialized producer/consumer pair. Four producer +// warps dequantize packed K/V into two ping-pong BF16 tiles per tensor while +// four consumer warps run the BF16 tensor-core attention body. Bc=32 keeps the +// four BF16 tiles + Q tile + sync flags inside the sm_120 opt-in smem ceiling. +inline constexpr int kNvfp4PrefillBr = 64; +inline constexpr int kNvfp4PrefillBc = 32; +inline constexpr int kNvfp4PrefillThreads = 256; +inline constexpr int kNvfp4PrefillSmemBytes = + kNvfp4PrefillBr * kGqaPrefillHeadDim * static_cast(sizeof(__nv_bfloat16)) + + 4 * kNvfp4PrefillBc * kGqaPrefillHeadDim * static_cast(sizeof(__nv_bfloat16)) + 64; + +struct GqaPrefillDirectMetadata { + const std::int32_t* table; + + __device__ __forceinline__ std::int32_t valid_tokens(std::int32_t width) const { return width; } + + __device__ __forceinline__ const std::int32_t* block_table() const { return table; } +}; + +template +struct GqaPrefillBatchMetadata { + const std::int32_t* tables; + const std::int32_t* valid_columns; + const std::int32_t* table_rows; + std::int32_t table_stride; + + __device__ __forceinline__ std::int32_t valid_tokens(std::int32_t width) const { + if constexpr (Masked) { + const std::int32_t valid = valid_columns[0]; + return valid <= 0 ? 0 : (valid < width ? valid : width); + } + return width; + } + + __device__ __forceinline__ const std::int32_t* block_table() const { + return tables + static_cast(table_rows[0]) * table_stride; + } +}; + +template +__device__ __forceinline__ std::int64_t gqa_prefill_q_index(int q_head, int d, int token) { + return static_cast(d) + static_cast(kGqaPrefillHeadDim) * + (static_cast(q_head) + + static_cast(Geometry::QHeads) * token); +} + +template +__device__ __forceinline__ void gqa_prefill_zero_output_rows(__nv_bfloat16* out, int q_head, + int row_begin, int row_end, int tid, + int threads) { + if (row_begin >= row_end) { return; } + const int elements = (row_end - row_begin) * kGqaPrefillHeadDim; + for (int element = tid; element < elements; element += threads) { + const int row = row_begin + element / kGqaPrefillHeadDim; + const int d = element - (row - row_begin) * kGqaPrefillHeadDim; + out[gqa_prefill_q_index(q_head, d, row)] = __float2bfloat16(0.0f); + } +} + +// XOR-swizzled b16 element address. INT8 operands use the same layout by packing +// two consecutive signed bytes into each b16 lane before ldmatrix. +__device__ __forceinline__ int gqa_prefill_swz(int row, int col) { + return (((col >> 3) ^ (row & 7)) << 3) | (col & 7); +} + +__device__ __forceinline__ unsigned gqa_prefill_swz_addr(unsigned lane_base, unsigned ck, + unsigned as, unsigned r) { + return lane_base + ((ck | as) ^ r); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_prefill_i8.cuh b/src/ops/kernel/gqa_attention_prefill_i8.cuh new file mode 100644 index 0000000000..eb6bfd4d98 --- /dev/null +++ b/src/ops/kernel/gqa_attention_prefill_i8.cuh @@ -0,0 +1,680 @@ +#pragma once + +// INT8-native GQA prompt kernel for the registered Qwen3.6 head geometries. QK stays INT8 through +// m16n8k32.s8 Tensor Cores; V alone is dequantized with packed FP16 arithmetic while +// producer warps execute QK. Sixteen warps split each 16-row FP16 PV output across +// four 64-dimension slices. + +#include +#include +#include + +#include "ops/kernel/gqa_attention_kv_quant.cuh" +#include "ops/kernel/cold_i8_kernels.cuh" +#include "ops/kernel/gqa_attention_prefill_common.cuh" + +#include + +namespace ninfer::ops { + +inline constexpr int kGqaPrefillI8Warps = 16; +inline constexpr int kGqaPrefillI8Threads = kGqaPrefillI8Warps * 32; +inline constexpr int kGqaPrefillI8Br = 64; +inline constexpr int kGqaPrefillI8Bc = 64; +inline constexpr int kGqaPrefillI8Groups = kGqaPrefillHeadDim / kGqaKvQuantGroup; +inline constexpr int kGqaPrefillI8DB16 = kGqaPrefillHeadDim / 2; +inline constexpr int kGqaPrefillI8RowTiles = kGqaPrefillI8Br / 16; +inline constexpr int kGqaPrefillI8DConsumers = kGqaPrefillI8Warps / kGqaPrefillI8RowTiles; + +inline constexpr int kGqaPrefillI8QBytes = kGqaPrefillI8Br * kGqaPrefillHeadDim; +inline constexpr int kGqaPrefillI8QScaleBytes = + kGqaPrefillI8Br * kGqaPrefillI8Groups * static_cast(sizeof(float)); +inline constexpr int kGqaPrefillI8KBytes = kGqaPrefillI8Bc * kGqaPrefillHeadDim; +inline constexpr int kGqaPrefillI8VBytes = kGqaPrefillI8Bc * kGqaPrefillHeadDim; +inline constexpr int kGqaPrefillI8VStageBytes = + kGqaPrefillI8Bc * kGqaPrefillHeadDim * static_cast(sizeof(__half)); +inline constexpr int kGqaPrefillI8PBytes = + kGqaPrefillI8Br * kGqaPrefillI8Bc * static_cast(sizeof(__half)); +inline constexpr int kGqaPrefillI8ScaleBytes = + 2 * kGqaPrefillI8Bc * kGqaPrefillI8Groups * static_cast(sizeof(__half)); +inline constexpr int kGqaPrefillI8StatsBytes = + 2 * kGqaPrefillI8Br * static_cast(sizeof(float)); +inline constexpr int kGqaPrefillI8SmemBytes = kGqaPrefillI8QBytes + kGqaPrefillI8QScaleBytes + + kGqaPrefillI8KBytes + kGqaPrefillI8VBytes + + kGqaPrefillI8VStageBytes + kGqaPrefillI8PBytes + + kGqaPrefillI8ScaleBytes + kGqaPrefillI8StatsBytes; + +static_assert(kGqaPrefillI8Groups == 4); +static_assert(kGqaPrefillI8DConsumers == 4); +static_assert(kGqaPrefillI8SmemBytes == 92672); + +__device__ __forceinline__ void gqa_prefill_i8_store_swz(std::int8_t* tile, int row, int d, + std::int8_t code) { + const int col_b16 = d >> 1; + const int byte = d & 1; + const int off = (row * kGqaPrefillI8DB16 + gqa_prefill_swz(row, col_b16)) * 2 + byte; + tile[off] = code; +} + +__device__ __forceinline__ int gqa_prefill_i8_p_swz(int row, int col) { + if constexpr (kGqaPrefillI8Bc == 32) { return (((col >> 3) ^ (row & 3)) << 3) | (col & 7); } + return gqa_prefill_swz(row, col); +} + +__device__ __forceinline__ int4 gqa_prefill_i8_dequant_f16x8(const std::int8_t* codes8, + __half scale) { + const int2 raw = load_vec(codes8); + const std::int8_t* c = reinterpret_cast(&raw); + const __half2 s2 = __halves2half2(scale, scale); + unsigned packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + const __half2 code2 = + __floats2half2_rn(static_cast(c[2 * i]), static_cast(c[2 * i + 1])); + const __half2 value2 = __hmul2(code2, s2); + packed[i] = *reinterpret_cast(&value2); + } + return make_int4(static_cast(packed[0]), static_cast(packed[1]), + static_cast(packed[2]), static_cast(packed[3])); +} + +// Eight independent quantization units per CTA; one warp owns one +// (token, kv_head, 64-d group), with two dimensions per lane. +template +__launch_bounds__(256) __global__ + void gqa_attention_prefill_fill_i8_kernel(const __nv_bfloat16* __restrict__ k, + const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, + Metadata metadata, std::int8_t* __restrict__ cache_k, + std::int8_t* __restrict__ cache_v, + __half* __restrict__ scale_k, + __half* __restrict__ scale_v, std::int32_t width) { + constexpr int Warps = 8; + constexpr unsigned FullMask = 0xffffffffu; + const int tokens = metadata.valid_tokens(width); + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int unit = static_cast(blockIdx.x) * Warps + warp; + const int units = tokens * Geometry::KVHeads * kGqaPrefillI8Groups; + if (unit >= units) { return; } + + const int group = unit % kGqaPrefillI8Groups; + const int tmp = unit / kGqaPrefillI8Groups; + const int kv_head = tmp % Geometry::KVHeads; + const int token = tmp / Geometry::KVHeads; + const int position = positions[0] + token; + const std::int32_t* block_table = metadata.block_table(); + int page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + const int page_off = position & kPagedKVPageMask; + const int d0 = group * kGqaKvQuantGroup + lane; + const int d1 = d0 + 32; + + const std::int64_t src0 = gqa_kv_quant_src_index(kv_head, d0, token); + const std::int64_t src1 = gqa_kv_quant_src_index(kv_head, d1, token); + const float k0 = __bfloat162float(k[src0]); + const float k1 = __bfloat162float(k[src1]); + const float v0 = __bfloat162float(v[src0]); + const float v1 = __bfloat162float(v[src1]); + + float k_abs = fmaxf(fabsf(k0), fabsf(k1)); + float v_abs = fmaxf(fabsf(v0), fabsf(v1)); + k_abs = warp_max(k_abs, FullMask); + v_abs = warp_max(v_abs, FullMask); + + const __half ksh = __float2half_rn(k_abs > 0.0f ? k_abs / 127.0f : 0.0f); + const __half vsh = __float2half_rn(v_abs > 0.0f ? v_abs / 127.0f : 0.0f); + const float ks = __half2float(ksh); + const float vs = __half2float(vsh); + const float kinv = ks > 0.0f ? 1.0f / ks : 0.0f; + const float vinv = vs > 0.0f ? 1.0f / vs : 0.0f; + page = __shfl_sync(FullMask, page, 0); + + const std::int64_t code_base = + gqa_kv_quant_code_index(page, kv_head, group * kGqaKvQuantGroup, page_off); + cache_k[code_base + lane] = gqa_kv_quant_code(k0, kinv); + cache_k[code_base + lane + 32] = gqa_kv_quant_code(k1, kinv); + cache_v[code_base + lane] = gqa_kv_quant_code(v0, vinv); + cache_v[code_base + lane + 32] = gqa_kv_quant_code(v1, vinv); + if (lane == 0) { + const std::int64_t scale_off = + gqa_kv_quant_scale_index(page, kv_head, group, page_off); + scale_k[scale_off] = ksh; + scale_v[scale_off] = vsh; + } +} + +// Large appends are scheduled in absolute eight-token tiles. Eight divides P=64, so each CTA is +// page-local while an unknown base offset costs at most one empty tail CTA in the launch envelope. +template +__launch_bounds__(256) __global__ void gqa_attention_prefill_fill_i8_page_kernel( + const __nv_bfloat16* __restrict__ k, const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, Metadata metadata, + std::int8_t* __restrict__ cache_k, std::int8_t* __restrict__ cache_v, + __half* __restrict__ scale_k, __half* __restrict__ scale_v, std::int32_t width) { + constexpr int TokensPerTile = 8; + constexpr unsigned FullMask = 0xffffffffu; + const int tokens = metadata.valid_tokens(width); + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int kv_head = static_cast(blockIdx.y); + const int group = static_cast(blockIdx.z); + const int tile_delta = static_cast(blockIdx.x); + const int base_position = positions[0]; + const int tile_position = (base_position / TokensPerTile + tile_delta) * TokensPerTile; + const int logical_page = tile_position >> kPagedKVPageShift; + const int token_begin = max(0, tile_position - base_position); + const int token_end = min(tokens, tile_position + TokensPerTile - base_position); + if (token_begin >= token_end) { return; } + + const std::int32_t* block_table = metadata.block_table(); + int physical_page = lane == 0 ? block_table[logical_page] : 0; + + const int token = token_begin + warp; + const bool valid = token < token_end; + const int d0 = group * kGqaKvQuantGroup + lane; + const int d1 = d0 + 32; + float k0 = 0.0f, k1 = 0.0f, v0 = 0.0f, v1 = 0.0f; + if (valid) { + const std::int64_t src0 = gqa_kv_quant_src_index(kv_head, d0, token); + const std::int64_t src1 = gqa_kv_quant_src_index(kv_head, d1, token); + k0 = __bfloat162float(k[src0]); + k1 = __bfloat162float(k[src1]); + v0 = __bfloat162float(v[src0]); + v1 = __bfloat162float(v[src1]); + } + const float k_abs = warp_max(fmaxf(fabsf(k0), fabsf(k1)), FullMask); + const float v_abs = warp_max(fmaxf(fabsf(v0), fabsf(v1)), FullMask); + const __half ksh = __float2half_rn(k_abs > 0.0f ? k_abs / 127.0f : 0.0f); + const __half vsh = __float2half_rn(v_abs > 0.0f ? v_abs / 127.0f : 0.0f); + const float ks = __half2float(ksh); + const float vs = __half2float(vsh); + const float kinv = ks > 0.0f ? 1.0f / ks : 0.0f; + const float vinv = vs > 0.0f ? 1.0f / vs : 0.0f; + physical_page = __shfl_sync(FullMask, physical_page, 0); + if (!valid) { return; } + + const int position = base_position + token; + const int page_off = position & kPagedKVPageMask; + const std::int64_t code_base = + paged_kv_page_head_offset(physical_page, kv_head) + + static_cast(page_off) * kGqaKvQuantHeadDim + group * kGqaKvQuantGroup; + cache_k[code_base + lane] = gqa_kv_quant_code(k0, kinv); + cache_k[code_base + lane + 32] = gqa_kv_quant_code(k1, kinv); + cache_v[code_base + lane] = gqa_kv_quant_code(v0, vinv); + cache_v[code_base + lane + 32] = gqa_kv_quant_code(v1, vinv); + if (lane == 0) { + const std::int64_t scale_offset = + paged_kv_page_head_offset(physical_page, + kv_head) + + static_cast(page_off) * kGqaKvQuantGroups + group; + scale_k[scale_offset] = ksh; + scale_v[scale_offset] = vsh; + } +} + +template +__global__ __maxnreg__(120) void gqa_attention_prefill_i8_kernel( + const __nv_bfloat16* __restrict__ q, const std::int8_t* __restrict__ cache_k, + const std::int8_t* __restrict__ cache_v, const __half* __restrict__ cache_k_scale, + const __half* __restrict__ cache_v_scale, Metadata metadata, + const std::int32_t* __restrict__ positions, float scale, __nv_bfloat16* __restrict__ out, + std::int32_t width, + const std::uint8_t* __restrict__ cold_k_slots = nullptr, + const std::uint8_t* __restrict__ cold_v_slots = nullptr, + int slot_bytes = 0) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Br = kGqaPrefillI8Br; + constexpr int Bc = kGqaPrefillI8Bc; + constexpr int DB16 = kGqaPrefillI8DB16; + constexpr int Groups = kGqaPrefillI8Groups; + constexpr int GroupKc = kGqaKvQuantGroup / 32; + constexpr int QKNt = Bc / 8; + constexpr int PVNtPerWarp = D / (kGqaPrefillI8DConsumers * 8); + constexpr int PVKs = Bc / 16; + constexpr int ProducerWarps = kGqaPrefillI8RowTiles; + constexpr int VWorkerWarps = kGqaPrefillI8Warps - ProducerWarps; + constexpr int WorkerThreads = VWorkerWarps * 32; + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + + static_assert(GroupKc == 2); + static_assert(PVNtPerWarp == 8); + + extern __shared__ __align__(16) unsigned char smem_raw[]; + std::int8_t* q_i8 = reinterpret_cast(smem_raw); + float* q_scale = reinterpret_cast(q_i8 + kGqaPrefillI8QBytes); + std::int8_t* k_i8 = reinterpret_cast(reinterpret_cast(q_scale) + + kGqaPrefillI8QScaleBytes); + std::int8_t* v_i8 = k_i8 + kGqaPrefillI8KBytes; + __half* v_f16 = reinterpret_cast<__half*>(v_i8 + kGqaPrefillI8VBytes); + __half* p_s = reinterpret_cast<__half*>(reinterpret_cast(v_f16) + + kGqaPrefillI8VStageBytes); + __half* k_scale_s = + reinterpret_cast<__half*>(reinterpret_cast(p_s) + kGqaPrefillI8PBytes); + __half* v_scale_s = k_scale_s + Bc * Groups; + float* alpha_s = reinterpret_cast(v_scale_s + Bc * Groups); + float* final_l_s = alpha_s + Br; + __nv_bfloat16* q_b16 = reinterpret_cast<__nv_bfloat16*>(q_i8); + __nv_bfloat16* k_b16 = reinterpret_cast<__nv_bfloat16*>(k_i8); + + const int q_block = static_cast(blockIdx.x); + const int q_head = static_cast(blockIdx.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + const int q0 = q_block * Br; + const int kv_head = q_head / Geometry::GroupSize; + const int tokens = metadata.valid_tokens(width); + if (q_head >= Geometry::QHeads || q0 >= width) { return; } + if (q0 >= tokens) { + gqa_prefill_zero_output_rows(out, q_head, q0, min(q0 + Br, width), tid, + kGqaPrefillI8Threads); + return; + } + const int base_pos = positions[0]; + const std::int32_t* block_table = metadata.block_table(); + + const int tile_rows = min(Br, tokens - q0); + const int max_query_abs = base_pos + q0 + tile_rows - 1; + const int key_blocks = max_query_abs / Bc + 1; + + // Quantize Q cooperatively. One warp owns one (row, 64-d group) at a time. + for (int unit = warp; unit < Br * Groups; unit += kGqaPrefillI8Warps) { + const int row = unit / Groups; + const int grp = unit - row * Groups; + const int d0 = grp * kGqaKvQuantGroup + lane; + const int d1 = d0 + 32; + float x0 = 0.0f; + float x1 = 0.0f; + if (row < tile_rows) { + x0 = __bfloat162float(q[gqa_prefill_q_index(q_head, d0, q0 + row)]); + x1 = __bfloat162float(q[gqa_prefill_q_index(q_head, d1, q0 + row)]); + } + float absmax = fmaxf(fabsf(x0), fabsf(x1)); + absmax = warp_max(absmax, FullMask); + const float qs = absmax > 0.0f ? absmax / 127.0f : 0.0f; + const float inv = qs > 0.0f ? 1.0f / qs : 0.0f; + gqa_prefill_i8_store_swz(q_i8, row, d0, gqa_kv_quant_code(x0, inv)); + gqa_prefill_i8_store_swz(q_i8, row, d1, gqa_kv_quant_code(x1, inv)); + if (lane == 0) { q_scale[row * Groups + grp] = qs; } + } + __syncthreads(); + + auto issue_kv_tile = [&](int tile_k0) { + const int table_entry = block_table[tile_k0 >> kPagedKVPageShift]; + // Revision 2b cold staging: raw nibble slots decoded inline into the + // kernel's native int8 codes + fp16 g64 scales (same adapter as the + // decode kernel; QK/PV math unchanged). Cold tiles fill synchronously + // and skip the cp.async pipeline: the double-buffer prefetch would + // otherwise overwrite the tile being consumed. Cold tiles are rare + // (one per window crossing), so the lost overlap is negligible. + const bool tile_cold = table_entry <= -2 && cold_k_slots != nullptr && + cold_v_slots != nullptr && + slot_bytes >= ninfer::ops::kColdI8SlotBytes; + const std::int64_t slot_flat = + static_cast(-table_entry - 2) * (2 * Geometry::KVHeads) + kv_head; + const std::uint8_t* k_slot = tile_cold ? cold_k_slots + slot_flat * slot_bytes + : nullptr; + const std::uint8_t* v_slot = tile_cold ? cold_v_slots + slot_flat * slot_bytes + : nullptr; + if (tile_cold && v_slot != nullptr) { + for (int key_l = tid; key_l < Bc; key_l += kGqaPrefillI8Threads) { + const int key = tile_k0 + key_l; + __half* kd = &k_scale_s[key_l * Groups]; + __half* vd = &v_scale_s[key_l * Groups]; + if (key > max_query_abs) { + store_vec(kd, make_int2(0, 0)); + store_vec(vd, make_int2(0, 0)); + std::int8_t* kdst = + &k_i8[(key_l * DB16 + gqa_prefill_swz(key_l, 0)) * 2]; +#pragma unroll + for (int d = 0; d < D; d += 2) { + gqa_prefill_i8_store_swz(k_i8, key_l, d, 0); + gqa_prefill_i8_store_swz(k_i8, key_l, d + 1, 0); + } + store_vec(&v_i8[key_l * D], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 16], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 32], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 48], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 64], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 80], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 96], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 112], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 128], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 144], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 160], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 176], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 192], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 208], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 224], make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + 240], make_int4(0, 0, 0, 0)); + (void)kdst; + continue; + } + const int row = key & kPagedKVPageMask; + std::int8_t row_codes[kGqaKvQuantHeadDim]; + __half row_scales[kGqaKvQuantGroups]; + ninfer::ops::detail::cold_i8_decode_row(k_slot, row, row_codes, row_scales); +#pragma unroll 4 + for (int d = 0; d < kGqaKvQuantHeadDim; ++d) { + gqa_prefill_i8_store_swz(k_i8, key_l, d, row_codes[d]); + } +#pragma unroll + for (int g = 0; g < Groups; ++g) { kd[g] = row_scales[g]; } + ninfer::ops::detail::cold_i8_decode_row(v_slot, row, row_codes, row_scales); +#pragma unroll 4 + for (int d = 0; d < kGqaKvQuantHeadDim; ++d) { + v_i8[key_l * D + d] = row_codes[d]; + } +#pragma unroll + for (int g = 0; g < Groups; ++g) { vd[g] = row_scales[g]; } + } + ninfer::ops::cp_commit(); + ninfer::ops::cp_wait<0>(); + return; + } + for (int key_l = tid; key_l < Bc; key_l += kGqaPrefillI8Threads) { + const int key = tile_k0 + key_l; + __half* kd = &k_scale_s[key_l * Groups]; + __half* vd = &v_scale_s[key_l * Groups]; + if (key <= max_query_abs) { + const std::int64_t off = + gqa_kv_quant_scale_index(table_entry, kv_head, 0, key_l); + ninfer::ops::cp_async<8>(kd, &cache_k_scale[off]); + ninfer::ops::cp_async<8>(vd, &cache_v_scale[off]); + } else { + store_vec(kd, make_int2(0, 0)); + store_vec(vd, make_int2(0, 0)); + } + } +#pragma unroll 1 + for (int chunk = tid; chunk < Bc * (D / 16); chunk += kGqaPrefillI8Threads) { + const int key_l = chunk / (D / 16); + const int dc = chunk - key_l * (D / 16); + const int d = dc * 16; + const int key = tile_k0 + key_l; + std::int8_t* kd = &k_i8[(key_l * DB16 + gqa_prefill_swz(key_l, dc * 8)) * 2]; + std::int8_t* vd = &v_i8[key_l * D + d]; + if (key <= max_query_abs) { + const std::int64_t off = + gqa_kv_quant_code_index(table_entry, kv_head, d, key_l); + cp_async<16, Cache::cg>(kd, &cache_k[off]); + cp_async<16, Cache::cg>(vd, &cache_v[off]); + } else { + store_vec(kd, make_int4(0, 0, 0, 0)); + store_vec(vd, make_int4(0, 0, 0, 0)); + } + } + ninfer::ops::cp_commit(); + }; + + issue_kv_tile(0); + ninfer::ops::cp_wait<0>(); + __syncthreads(); + + const int gid = lane >> 2; + const int lid = lane & 3; + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int a_coloff = (a_mat >> 1) << 3; + const int b_rin = lane & 7; + const int b_koff = ((lane >> 3) & 1) << 3; + + // Keeping exactly two group scales live is the spill-free 120-register point on SM120. + // Groups 2/3 reload per key tile; retaining all four creates an 8-byte stack frame. + float q_scale_r0[Groups - 2]; + float q_scale_r1[Groups - 2]; + if (warp < ProducerWarps) { + const int scale_row0 = warp * 16 + gid; + const int scale_row1 = scale_row0 + 8; +#pragma unroll + for (int grp = 0; grp < Groups - 2; ++grp) { + float qs0 = lid == 0 ? q_scale[scale_row0 * Groups + grp] : 0.0f; + float qs1 = lid == 0 ? q_scale[scale_row1 * Groups + grp] : 0.0f; + q_scale_r0[grp] = __shfl_sync(FullMask, qs0, gid * 4); + q_scale_r1[grp] = __shfl_sync(FullMask, qs1, gid * 4); + } + } + + float acc[PVNtPerWarp][4]; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + float running_m0 = -CUDART_INF_F; + float running_m1 = -CUDART_INF_F; + float running_l0 = 0.0f; + float running_l1 = 0.0f; + const float scale_l2 = scale * Log2E; + for (int kb = 0; kb < key_blocks; ++kb) { + const int k0 = kb * Bc; + if (warp < ProducerWarps) { + const int row_base = warp * 16; + float score[QKNt][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; + } + +#pragma unroll + for (int grp = 0; grp < Groups; ++grp) { + float qs0; + float qs1; + if (grp < Groups - 2) { + qs0 = q_scale_r0[grp]; + qs1 = q_scale_r1[grp]; + } else { + const int scale_row0 = row_base + gid; + const int scale_row1 = scale_row0 + 8; + qs0 = lid == 0 ? q_scale[scale_row0 * Groups + grp] : 0.0f; + qs1 = lid == 0 ? q_scale[scale_row1 * Groups + grp] : 0.0f; + qs0 = __shfl_sync(FullMask, qs0, gid * 4); + qs1 = __shfl_sync(FullMask, qs1, gid * 4); + } + + unsigned af[GroupKc][4]; +#pragma unroll + for (int kk = 0; kk < GroupKc; ++kk) { + const int k = grp * GroupKc + kk; + const int acol = k * 16 + a_coloff; + ldmatrix_x4(af[kk][0], af[kk][1], af[kk][2], af[kk][3], + smem_addr(&q_b16[(row_base + a_rowoff) * DB16 + + gqa_prefill_swz(row_base + a_rowoff, acol)])); + } + +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + int c0 = 0, c1 = 0, c2 = 0, c3 = 0; +#pragma unroll + for (int kk = 0; kk < GroupKc; ++kk) { + const int k = grp * GroupKc + kk; + const int brow = nt * 8 + b_rin; + const int bcol = k * 16 + b_koff; + unsigned bf[2]; + ldmatrix_x2(bf[0], bf[1], + smem_addr(&k_b16[brow * DB16 + gqa_prefill_swz(brow, bcol)])); + mma_s8(c0, c1, c2, c3, af[kk][0], af[kk][1], af[kk][2], af[kk][3], bf[0], + bf[1]); + } + const int keya = nt * 8 + 2 * lid; + const int keyb = keya + 1; + float ks0 = 0.0f; + float ks1 = 0.0f; + if (gid == 0) { + ks0 = __half2float(k_scale_s[keya * Groups + grp]); + ks1 = __half2float(k_scale_s[keyb * Groups + grp]); + } + ks0 = __shfl_sync(FullMask, ks0, lid); + ks1 = __shfl_sync(FullMask, ks1, lid); + score[nt][0] = __fmaf_rn(qs0 * ks0, static_cast(c0), score[nt][0]); + score[nt][1] = __fmaf_rn(qs0 * ks1, static_cast(c1), score[nt][1]); + score[nt][2] = __fmaf_rn(qs1 * ks0, static_cast(c2), score[nt][2]); + score[nt][3] = __fmaf_rn(qs1 * ks1, static_cast(c3), score[nt][3]); + } + } + + const int row0 = row_base + gid; + const int row1 = row0 + 8; + const int qabs0 = row0 < tile_rows ? base_pos + q0 + row0 : -1; + const int qabs1 = row1 < tile_rows ? base_pos + q0 + row1 : -1; + const bool full_score_tile = q0 + Br <= tokens && k0 + Bc - 1 <= base_pos + q0; + float bm0 = -CUDART_INF_F; + float bm1 = -CUDART_INF_F; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int key0 = k0 + nt * 8 + 2 * lid; + const int key1 = key0 + 1; + if (!full_score_tile) { + score[nt][0] = key0 <= qabs0 ? score[nt][0] : -CUDART_INF_F; + score[nt][1] = key1 <= qabs0 ? score[nt][1] : -CUDART_INF_F; + score[nt][2] = key0 <= qabs1 ? score[nt][2] : -CUDART_INF_F; + score[nt][3] = key1 <= qabs1 ? score[nt][3] : -CUDART_INF_F; + } + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(running_m0, bm0); + const float nm1 = fmaxf(running_m1, bm1); + const float nm0_scaled = nm0 * scale_l2; + const float nm1_scaled = nm1 * scale_l2; + const float alpha0 = running_m0 == -CUDART_INF_F + ? 0.0f + : exp2_approx(__fmaf_rn(running_m0, scale_l2, -nm0_scaled)); + const float alpha1 = running_m1 == -CUDART_INF_F + ? 0.0f + : exp2_approx(__fmaf_rn(running_m1, scale_l2, -nm1_scaled)); + float bl0 = 0.0f; + float bl1 = 0.0f; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + const int col0 = nt * 8 + 2 * lid; + const int col1 = col0 + 1; + const float p00 = score[nt][0] > -CUDART_INF_F + ? exp2_approx(__fmaf_rn(score[nt][0], scale_l2, -nm0_scaled)) + : 0.0f; + const float p01 = score[nt][1] > -CUDART_INF_F + ? exp2_approx(__fmaf_rn(score[nt][1], scale_l2, -nm0_scaled)) + : 0.0f; + const float p10 = score[nt][2] > -CUDART_INF_F + ? exp2_approx(__fmaf_rn(score[nt][2], scale_l2, -nm1_scaled)) + : 0.0f; + const float p11 = score[nt][3] > -CUDART_INF_F + ? exp2_approx(__fmaf_rn(score[nt][3], scale_l2, -nm1_scaled)) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + p_s[row0 * Bc + gqa_prefill_i8_p_swz(row0, col0)] = __float2half_rn(p00); + p_s[row0 * Bc + gqa_prefill_i8_p_swz(row0, col1)] = __float2half_rn(p01); + p_s[row1 * Bc + gqa_prefill_i8_p_swz(row1, col0)] = __float2half_rn(p10); + p_s[row1 * Bc + gqa_prefill_i8_p_swz(row1, col1)] = __float2half_rn(p11); + } + bl0 = warp_sum<4>(bl0, FullMask); + bl1 = warp_sum<4>(bl1, FullMask); + running_l0 = __fmaf_rn(running_l0, alpha0, bl0); + running_l1 = __fmaf_rn(running_l1, alpha1, bl1); + running_m0 = nm0; + running_m1 = nm1; + if (lid == 0) { + alpha_s[row0] = alpha0; + alpha_s[row1] = alpha1; + } + } else if (warp < ProducerWarps + VWorkerWarps) { + const int worker_tid = tid - ProducerWarps * 32; +#pragma unroll 1 + for (int chunk = worker_tid; chunk < Bc * (D / 8); chunk += WorkerThreads) { + const int key_l = chunk / (D / 8); + const int dc = chunk - key_l * (D / 8); + const int d = dc * 8; + const int key = k0 + key_l; + __half* dst = &v_f16[key_l * D + gqa_prefill_swz(key_l, d)]; + if (key <= max_query_abs) { + const int grp = d >> 6; + __half vs = __float2half_rn(0.0f); + if ((lane & 7) == 0) { vs = v_scale_s[key_l * Groups + grp]; } + vs = __shfl_sync(FullMask, vs, grp * 8); + store_vec(dst, gqa_prefill_i8_dequant_f16x8(&v_i8[key_l * D + d], vs)); + } else { + store_vec(dst, make_int4(0, 0, 0, 0)); + } + } + } + __syncthreads(); + + const bool has_next = kb + 1 < key_blocks; + if (has_next) { issue_kv_tile((kb + 1) * Bc); } + + const int row_tile = warp % kGqaPrefillI8RowTiles; + const int d_slice = warp / kGqaPrefillI8RowTiles; + const int row_base = row_tile * 16; + const float alpha0 = alpha_s[row_base + gid]; + const float alpha1 = alpha_s[row_base + gid + 8]; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + +#pragma unroll + for (int k = 0; k < PVKs; ++k) { + unsigned pf[4]; + const int pcol = k * 16 + a_coloff; + ldmatrix_x4(pf[0], pf[1], pf[2], pf[3], + smem_addr(&p_s[(row_base + a_rowoff) * Bc + + gqa_prefill_i8_p_swz(row_base + a_rowoff, pcol)])); +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + const int global_n = d_slice * PVNtPerWarp + n; + unsigned vf[2]; + const int vrow = k * 16 + b_koff + b_rin; + const int vcol = global_n * 8; + ldmatrix_x2_t(vf[0], vf[1], + smem_addr(&v_f16[vrow * D + gqa_prefill_swz(vrow, vcol)])); + mma_f16(acc[n][0], acc[n][1], acc[n][2], acc[n][3], pf[0], pf[1], pf[2], pf[3], + vf[0], vf[1]); + } + } + if (has_next) { ninfer::ops::cp_wait<0>(); } + __syncthreads(); + } + + if (warp < ProducerWarps && lid == 0) { + const int row0 = warp * 16 + gid; + const int row1 = row0 + 8; + final_l_s[row0] = running_l0; + final_l_s[row1] = running_l1; + } + __syncthreads(); + + const int row_tile = warp % kGqaPrefillI8RowTiles; + const int d_slice = warp / kGqaPrefillI8RowTiles; + const int row_base = row_tile * 16; + const int row0 = row_base + gid; + const int row1 = row0 + 8; + const float inv_l0 = final_l_s[row0] > 0.0f ? __frcp_rn(final_l_s[row0]) : 0.0f; + const float inv_l1 = final_l_s[row1] > 0.0f ? __frcp_rn(final_l_s[row1]) : 0.0f; +#pragma unroll + for (int n = 0; n < PVNtPerWarp; ++n) { + const int d0 = (d_slice * PVNtPerWarp + n) * 8 + 2 * lid; + if (row0 < tile_rows) { + *reinterpret_cast( + &out[gqa_prefill_q_index(q_head, d0, q0 + row0)]) = + pack_bf16x2(acc[n][0] * inv_l0, acc[n][1] * inv_l0); + } + if (row1 < tile_rows) { + *reinterpret_cast( + &out[gqa_prefill_q_index(q_head, d0, q0 + row1)]) = + pack_bf16x2(acc[n][2] * inv_l1, acc[n][3] * inv_l1); + } + } + gqa_prefill_zero_output_rows(out, q_head, tokens, min(q0 + Br, width), tid, + kGqaPrefillI8Threads); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_attention_prefill_nvfp4.cuh b/src/ops/kernel/gqa_attention_prefill_nvfp4.cuh new file mode 100644 index 0000000000..0f00fd9fa7 --- /dev/null +++ b/src/ops/kernel/gqa_attention_prefill_nvfp4.cuh @@ -0,0 +1,1667 @@ +#pragma once + +// ninfer::ops - NVFP4 GQA prompt path. +// +// * Fill: K is rotated per 4-channel block with the baked IsoQuant matrix and +// quantized to packed E2M1 with E4M3 per-16-group scales. V is gain-only +// quantized without rotation. +// * Attention: one CTA runs a warp-specialized producer/consumer pair. +// Four producer warps stage K/V while four consumer warps run the +// FlashAttention body (QK + online softmax + PV). For NVFP4 K, QK runs +// on native m16n8k64.kind::mxf4nvf4 tensor cores with Q quantized +// on-chip to E2M1 and K staged straight from the packed cache; V keeps +// the exact BF16 PV path over the dequantized tile. FP8/ISO3 K retain +// the exact BF16 QK path. +// +// The 32-key tile keeps two ping-pong K/V buffers inside the sm_120 opt-in +// shared-memory ceiling (98.3 KiB + flags of 101.4 KiB). + +#include +#include + +#include "ops/kernel/gqa_attention_kv_nvfp4.cuh" +#include "ops/kernel/gqa_attention_prefill_common.cuh" +#include "ops/kernel/gqa_isoquant_rot.cuh" +#include "ops/kernel/gqa_isoquant_row_scale.cuh" +#include "ops/kernel/entropy_nvfp4_slot.cuh" + +#include "core/dtype.h" + +#include + +namespace ninfer::ops { +namespace { + +using namespace ninfer::ops::detail; + +__device__ __forceinline__ float gqa_prefill_nvfp4_rot(float x0, float x1, float x2, float x3, + int block, int row) { + return gqa_isoquant_rot_value(block, row, 0) * x0 + + gqa_isoquant_rot_value(block, row, 1) * x1 + + gqa_isoquant_rot_value(block, row, 2) * x2 + + gqa_isoquant_rot_value(block, row, 3) * x3; +} + +// Rotate eight contiguous dims (two 4-blocks) in registers. +__device__ __forceinline__ void gqa_prefill_nvfp4_rotate_8(float (&x)[8], int d) { + const int block0 = d >> 2; + float y0[4]; +#pragma unroll + for (int row = 0; row < 4; ++row) { + y0[row] = gqa_prefill_nvfp4_rot(x[0], x[1], x[2], x[3], block0, row); + } +#pragma unroll + for (int row = 0; row < 4; ++row) { x[row] = y0[row]; } + const int block1 = block0 + 1; + float y1[4]; +#pragma unroll + for (int row = 0; row < 4; ++row) { + y1[row] = gqa_prefill_nvfp4_rot(x[4], x[5], x[6], x[7], block1, row); + } +#pragma unroll + for (int row = 0; row < 4; ++row) { x[4 + row] = y1[row]; } +} + +__device__ __forceinline__ void gqa_prefill_bar_sync(int id, int count) { + asm volatile("bar.sync %0, %1;" ::"r"(id), "r"(count)); +} + +__device__ __forceinline__ unsigned gqa_prefill_nvfp4_nibble_bits(std::uint8_t code) { + const unsigned mag = code & 0x07u; + const unsigned small = + (mag >= 1 && mag <= 3) ? (0x3F00u + (mag - 1) * 0x80u) : 0u; + const unsigned large = (mag >= 4) ? (0x4000u + (mag - 4) * 0x40u) : 0u; + unsigned bits = small | large; + if ((code & 0x08u) != 0) { bits |= 0x8000u; } + return bits; +} + +// ISO3 = sign-magnitude INT3: low 3 bits encode magnitude 0..7, bit3 is the +// sign (1 = negative). Negative zero encodes as zero. +__device__ __forceinline__ std::uint8_t gqa_iso3_nibble(float value, float scale) { + float mag = roundf(fabsf(value) / scale); + if (mag > 7.0f) { mag = 7.0f; } + if (mag < 0.0f) { mag = 0.0f; } + std::uint8_t code = static_cast(mag); + if (value < 0.0f && code != 0) { code |= 0x08u; } + return code; +} + +__device__ __forceinline__ float gqa_iso3_decode(std::uint8_t code) { + const float mag = static_cast(code & 0x07u); + return (code & 0x08u) != 0 ? -mag : mag; +} + +// ---- native mxf4nvf4 QK staging (NVFP4 K only) ---- +// +// Q is quantized on-chip to packed E2M1 with per-(row,16-group) E4M3 scales +// and K stays packed in the cache; the block-scale mma instruction applies +// both scale vectors, so scores land in the scaled domain exactly like the +// decode kernel. The packed K tile keeps the decode kernel's 128-byte row +// layout consumed by gqa_prefill_mxf4_load_b_frag. + +constexpr float kGqaPrefillMxf4MinScale = 0.001953125f; // 2^-9, E4M3 smallest normal +constexpr std::uint8_t kGqaPrefillMxf4E4M3One = 0x38u; // E4M3FN encoding of 1.0 + +__device__ __forceinline__ void gqa_prefill_mxf4_load_a_frag(unsigned (&frag)[4], + const std::uint8_t* smem, int lane, + int k_step) { + const int row = (lane & 7) + ((lane >> 3) & 1) * 8; + const int col = (lane >> 4) * 16 + k_step * 32; + ldmatrix_x4(frag[0], frag[1], frag[2], frag[3], smem_addr(smem + row * 128 + col)); +} + +__device__ __forceinline__ void gqa_prefill_mxf4_load_b_frag(unsigned (&frag)[2], + const std::uint8_t* smem, int lane, + int n_tile, int k_step) { + const int row = (lane & 7) + n_tile * 8; + const int col = ((lane >> 3) & 1) * 16 + k_step * 32; + ldmatrix_x2(frag[0], frag[1], smem_addr(smem + row * 128 + col)); +} + +// Lane l < 4 loads its 4-channel block, applies the baked SO(4) rotation, and +// returns the rotated block in x[]. src points at the 16-d group start. +__device__ __forceinline__ void gqa_prefill_mxf4_rotate_4(float (&x)[4], + const __nv_bfloat16* src, int group, + int lane) { + if (lane < 4) { + const int block = group * 4 + lane; + const int base = lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { x[j] = __bfloat162float(src[base + j]); } + const float y0 = gqa_prefill_nvfp4_rot(x[0], x[1], x[2], x[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(x[0], x[1], x[2], x[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(x[0], x[1], x[2], x[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(x[0], x[1], x[2], x[3], block, 3); + x[0] = y0; + x[1] = y1; + x[2] = y2; + x[3] = y3; + } else { + x[0] = x[1] = x[2] = x[3] = 0.0f; + } +} + +__device__ __forceinline__ float gqa_prefill_mxf4_group_max4(float local_max, + unsigned full_mask) { + local_max = fmaxf(local_max, __shfl_xor_sync(full_mask, local_max, 1)); + local_max = fmaxf(local_max, __shfl_xor_sync(full_mask, local_max, 2)); + return local_max; +} + +// Warm producer: copy the packed 128-byte K row and its 16 E4M3 group scales +// straight into the mxf4 staging tile (one 16-byte vector per 32 dims). +template +__device__ __forceinline__ void gqa_prefill_mxf4_stage_k_packed( + std::uint8_t* k_pk, std::uint8_t* k_sf, const std::uint8_t* cache_codes, + const std::uint8_t* cache_scales, int kv_head, int k0, int valid_start, + int max_query_abs, int physical_page, int tid) { + constexpr int Bc = kNvfp4PrefillBc; + for (int row = tid; row < Bc; row += Threads) { + const int key = k0 + row; + if (key <= max_query_abs && key >= valid_start) { + const std::int64_t scale_off = + gqa_kv_nvfp4_scale_index(physical_page, kv_head, 0, + key & kPagedKVPageMask); + store_vec(&k_sf[row * 16], load_vec(&cache_scales[scale_off])); + } else { + store_vec(&k_sf[row * 16], make_int4(0, 0, 0, 0)); + } + } + for (int chunk = tid; chunk < Bc * 8; chunk += Threads) { + const int key_l = chunk >> 3; + const int j = chunk & 7; + const int d = j * 32; + const int key = k0 + key_l; + std::uint8_t* dst = &k_pk[key_l * 128 + j * 16]; + if (key <= max_query_abs && key >= valid_start) { + const std::int64_t code_off = + gqa_kv_nvfp4_code_index(physical_page, kv_head, d, + key & kPagedKVPageMask); + store_vec(dst, load_vec(&cache_codes[code_off])); + } else { + store_vec(dst, make_int4(0, 0, 0, 0)); + } + } +} + +// Cold producer: rANS stream `tid` decodes rows (2*tid, 2*tid+1) of the packed +// 128-byte-row tile directly; all producer threads copy the slot scale tail. +template +__device__ __forceinline__ void gqa_prefill_mxf4_stage_k_cold( + std::uint8_t* k_pk, std::uint8_t* k_sf, const std::uint8_t* slot, int slot_bytes, + int half, int k0, int valid_start, int max_query_abs, int tid) { + constexpr int Bc = kNvfp4PrefillBc; + if (tid < kEntropyNvfp4SlotStreamsPerHalf) { + std::uint8_t* dst = k_pk + tid * kEntropyNvfp4SlotStreamBytes; + if (!entropy_nvfp4_slot_decode_stream(slot, half, tid, dst)) { + for (int i = 0; i < kEntropyNvfp4SlotStreamBytes; ++i) { dst[i] = 0; } + } + } + const std::uint8_t* scale_tail = entropy_nvfp4_slot_scales(slot, slot_bytes); + for (int row = tid; row < Bc; row += Threads) { + const int key = k0 + row; + if (key <= max_query_abs && key >= valid_start) { + store_vec(&k_sf[row * 16], load_vec(&scale_tail[(half * 32 + row) * 16])); + } else { + store_vec(&k_sf[row * 16], make_int4(0, 0, 0, 0)); + } + } +} + +// Producer dequant: one [Bc, D] K or V tile from the packed paged cache into a +// swizzled BF16 smem buffer. Producer threads are indexed 0..127. Sixteen dims +// are decoded per iteration: four bytes of E2M1 codes + one E4M3 scale become +// four BF16x2 pairs per 8-d swizzle block, multiplied by the group scale. +template +__device__ __forceinline__ void gqa_prefill_nvfp4_stage_kv(__nv_bfloat16* dst, + const std::uint8_t* cache_codes, + const std::uint8_t* cache_scales, + int kv_head, int k0, int valid_start, + int max_query_abs, + int physical_page, int tid) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Bc = kNvfp4PrefillBc; + constexpr int VecPerRow = D / 16; // 16 chunks of 16 dims + for (int chunk = tid; chunk < Bc * VecPerRow; chunk += Threads) { + const int key_l = chunk / VecPerRow; + const int d = (chunk - key_l * VecPerRow) << 4; + const int key = k0 + key_l; + __nv_bfloat162* p0 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d)]); + __nv_bfloat162* p1 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d + 8)]); + if (key <= max_query_abs && key >= valid_start) { + const int group = d >> 4; + const float scale = gqa_kv_nvfp4_e4m3_to_f32(cache_scales[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + key & kPagedKVPageMask)]); + const __nv_bfloat162 scale2 = __floats2bfloat162_rn(scale, scale); + const std::uint8_t* codes = + &cache_codes[gqa_kv_nvfp4_code_index(physical_page, kv_head, d, + key & kPagedKVPageMask)]; + const uint2 raw = load_vec(codes); + const std::uint8_t* bytes = reinterpret_cast(&raw); + __nv_bfloat162 pair[8]; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const unsigned lo = gqa_prefill_nvfp4_nibble_bits(bytes[i] & 0x0Fu); + const unsigned hi = gqa_prefill_nvfp4_nibble_bits(bytes[i] >> 4); + const unsigned bits = lo | (hi << 16); + pair[i] = *reinterpret_cast(&bits) * scale2; + } + store_vec(p0 + 0, make_int4(*reinterpret_cast(&pair[0]), + *reinterpret_cast(&pair[1]), + *reinterpret_cast(&pair[2]), + *reinterpret_cast(&pair[3]))); + store_vec(p1 + 0, make_int4(*reinterpret_cast(&pair[4]), + *reinterpret_cast(&pair[5]), + *reinterpret_cast(&pair[6]), + *reinterpret_cast(&pair[7]))); + } else { + store_vec(p0 + 0, make_int4(0, 0, 0, 0)); + store_vec(p1 + 0, make_int4(0, 0, 0, 0)); + } + } +} + +// Cold half-page producer: thread `stream` (0..15) decodes its 512-nibble +// rANS stream directly into the swizzled BF16 tile, applying the slot's +// uncompressed E4M3FN scales on the fly. Out-of-range rows still advance the +// rANS state but store zero. scale_tail points at the slot's 1024-byte scale +// tail (both halves). +template +__device__ __forceinline__ void gqa_prefill_nvfp4_cold_decode_kv( + __nv_bfloat16* dst, const std::uint8_t* slot, const std::uint8_t* scale_tail, int half, + int k0, int valid_start, int max_query_abs, int stream) { + std::uint8_t packed[kEntropyNvfp4SlotStreamBytes]; + if (!entropy_nvfp4_slot_decode_stream(slot, half, stream, packed)) { + for (int i = 0; i < kEntropyNvfp4SlotStreamBytes; ++i) { packed[i] = 0; } + } + for (int byte_index = 0; byte_index < kEntropyNvfp4SlotStreamBytes; ++byte_index) { + const int row_in_stream = byte_index >> 7; + const int row = 2 * stream + row_in_stream; + const int byte_in_row = byte_index & 127; + const int key = k0 + row; + const std::uint8_t byte = packed[byte_index]; +#pragma unroll + for (int nibble = 0; nibble < 2; ++nibble) { + const int dim = byte_in_row * 2 + nibble; + const std::uint8_t code = nibble == 0 ? (byte & 0x0f) : (byte >> 4); + float value = 0.0f; + if (key <= max_query_abs && key >= valid_start) { + const int group = dim >> 4; + const float scale = + gqa_kv_nvfp4_e4m3_to_f32(scale_tail[(half * 32 + row) * 16 + group]); + if constexpr (Iso3) { + value = gqa_iso3_decode(code) * scale; + } else { + // Match the warm prefill producer exactly: it dequantizes the + // packed code through gqa_prefill_nvfp4_nibble_bits and + // multiplies the BF16 value by the BF16 scale. + const unsigned bits = gqa_prefill_nvfp4_nibble_bits(code); + const float decoded = + __bfloat162float(*reinterpret_cast(&bits)); + value = decoded * scale; + } + } + dst[row * 256 + gqa_prefill_swz(row, dim)] = __float2bfloat16(value); + } + } +} + +// Producer dequant for ISO3 codes: two nibbles per byte, one E4M3FN scale per +// 16-channel group. Same 16-dim iteration, code layout, and swizzled BF16 +// output as the NVFP4 producer; only the nibble decode differs. +template +__device__ __forceinline__ void gqa_prefill_iso3_stage_kv(__nv_bfloat16* dst, + const std::uint8_t* cache_codes, + const std::uint8_t* cache_scales, + int kv_head, int k0, int max_query_abs, + int physical_page, int tid) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Bc = kNvfp4PrefillBc; + constexpr int VecPerRow = D / 16; // 16 chunks of 16 dims + for (int chunk = tid; chunk < Bc * VecPerRow; chunk += Threads) { + const int key_l = chunk / VecPerRow; + const int d = (chunk - key_l * VecPerRow) << 4; + const int key = k0 + key_l; + __nv_bfloat162* p0 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d)]); + __nv_bfloat162* p1 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d + 8)]); + if (key <= max_query_abs) { + const int group = d >> 4; + const float scale = gqa_kv_nvfp4_e4m3_to_f32(cache_scales[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + key & kPagedKVPageMask)]); + const std::uint8_t* codes = + &cache_codes[gqa_kv_nvfp4_code_index(physical_page, kv_head, d, + key & kPagedKVPageMask)]; + const uint2 raw = load_vec(codes); + const std::uint8_t* bytes = reinterpret_cast(&raw); + __nv_bfloat162 pair[8]; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const float lo = gqa_iso3_decode(bytes[i] & 0x0Fu) * scale; + const float hi = gqa_iso3_decode(bytes[i] >> 4) * scale; + pair[i] = __floats2bfloat162_rn(lo, hi); + } + store_vec(p0 + 0, make_int4(*reinterpret_cast(&pair[0]), + *reinterpret_cast(&pair[1]), + *reinterpret_cast(&pair[2]), + *reinterpret_cast(&pair[3]))); + store_vec(p1 + 0, make_int4(*reinterpret_cast(&pair[4]), + *reinterpret_cast(&pair[5]), + *reinterpret_cast(&pair[6]), + *reinterpret_cast(&pair[7]))); + } else { + store_vec(p0 + 0, make_int4(0, 0, 0, 0)); + store_vec(p1 + 0, make_int4(0, 0, 0, 0)); + } + } +} + +// Adds the second ISO3 V residual stage on top of an already-staged BF16 V +// tile. The main stage must have run first so dst holds the first-stage values. +template +__device__ __forceinline__ void gqa_prefill_iso3_stage_v_residual( + __nv_bfloat16* dst, const std::uint8_t* cache_codes, const std::uint8_t* cache_scales, + int kv_head, int k0, int max_query_abs, int physical_page, int tid) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Bc = kNvfp4PrefillBc; + constexpr int VecPerRow = D / 16; + for (int chunk = tid; chunk < Bc * VecPerRow; chunk += Threads) { + const int key_l = chunk / VecPerRow; + const int d = (chunk - key_l * VecPerRow) << 4; + const int key = k0 + key_l; + __nv_bfloat162* p0 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d)]); + __nv_bfloat162* p1 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d + 8)]); + if (key <= max_query_abs) { + const int group = d >> 4; + const float scale = gqa_kv_nvfp4_e4m3_to_f32(cache_scales[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + key & kPagedKVPageMask)]); + const std::uint8_t* codes = + &cache_codes[gqa_kv_nvfp4_code_index(physical_page, kv_head, d, + key & kPagedKVPageMask)]; + const uint2 raw = load_vec(codes); + const std::uint8_t* bytes = reinterpret_cast(&raw); + __nv_bfloat162 pair[8]; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const float lo = gqa_iso3_decode(bytes[i] & 0x0Fu) * scale; + const float hi = gqa_iso3_decode(bytes[i] >> 4) * scale; + pair[i] = __floats2bfloat162_rn(lo, hi); + } + __nv_bfloat162 cur[8]; + cur[0] = load_vec<__nv_bfloat162>(p0 + 0); + cur[1] = load_vec<__nv_bfloat162>(p0 + 1); + cur[2] = load_vec<__nv_bfloat162>(p0 + 2); + cur[3] = load_vec<__nv_bfloat162>(p0 + 3); + cur[4] = load_vec<__nv_bfloat162>(p1 + 0); + cur[5] = load_vec<__nv_bfloat162>(p1 + 1); + cur[6] = load_vec<__nv_bfloat162>(p1 + 2); + cur[7] = load_vec<__nv_bfloat162>(p1 + 3); +#pragma unroll + for (int i = 0; i < 8; ++i) { + const float lo = __bfloat162float(cur[i].x) + __bfloat162float(pair[i].x); + const float hi = __bfloat162float(cur[i].y) + __bfloat162float(pair[i].y); + pair[i] = __floats2bfloat162_rn(lo, hi); + } + store_vec(p0 + 0, make_int4(*reinterpret_cast(&pair[0]), + *reinterpret_cast(&pair[1]), + *reinterpret_cast(&pair[2]), + *reinterpret_cast(&pair[3]))); + store_vec(p1 + 0, make_int4(*reinterpret_cast(&pair[4]), + *reinterpret_cast(&pair[5]), + *reinterpret_cast(&pair[6]), + *reinterpret_cast(&pair[7]))); + } + } +} + + +template +__device__ __forceinline__ void gqa_prefill_fp8_stage_kv(__nv_bfloat16* dst, + const std::uint8_t* cache_codes, + const std::uint8_t* cache_scales, + int kv_head, int k0, int max_query_abs, + int physical_page, int tid) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Bc = kNvfp4PrefillBc; + constexpr int VecPerRow = D / 16; + for (int chunk = tid; chunk < Bc * VecPerRow; chunk += Threads) { + const int key_l = chunk / VecPerRow; + const int d = (chunk - key_l * VecPerRow) << 4; + const int key = k0 + key_l; + __nv_bfloat162* p0 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d)]); + __nv_bfloat162* p1 = reinterpret_cast<__nv_bfloat162*>( + &dst[key_l * D + gqa_prefill_swz(key_l, d + 8)]); + if (key <= max_query_abs) { + const int group = d >> 4; + const float scale = gqa_kv_nvfp4_e4m3_to_f32(cache_scales[ + gqa_kv_nvfp4_scale_index(physical_page, kv_head, group, + key & kPagedKVPageMask)]); + const __nv_bfloat162 scale2 = __floats2bfloat162_rn(scale, scale); + const std::uint8_t* codes = &cache_codes[ + paged_kv_element_offset( + physical_page, kv_head, key & kPagedKVPageMask, d)]; + const uint4 raw = load_vec(codes); + const std::uint8_t* bytes = reinterpret_cast(&raw); + __nv_bfloat162 pair[8]; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const float lo = gqa_kv_nvfp4_e4m3_to_f32(bytes[2 * i]) * scale; + const float hi = gqa_kv_nvfp4_e4m3_to_f32(bytes[2 * i + 1]) * scale; + pair[i] = __floats2bfloat162_rn(lo, hi); + } + store_vec(p0, make_int4(*reinterpret_cast(&pair[0]), + *reinterpret_cast(&pair[1]), + *reinterpret_cast(&pair[2]), + *reinterpret_cast(&pair[3]))); + store_vec(p1, make_int4(*reinterpret_cast(&pair[4]), + *reinterpret_cast(&pair[5]), + *reinterpret_cast(&pair[6]), + *reinterpret_cast(&pair[7]))); + } else { + store_vec(p0, make_int4(0, 0, 0, 0)); + store_vec(p1, make_int4(0, 0, 0, 0)); + } + } +} + +} // namespace + +// One warp owns one (token, kv_head, 16-d group) unit. K rotation runs lanes +// 0..3 over the four 4-channel sub-blocks; V uses all 16 lanes. +template +__launch_bounds__(256) __global__ + void gqa_attention_prefill_fill_nvfp4_kernel(const __nv_bfloat16* __restrict__ k, + const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, + int layer, Metadata metadata, + std::uint8_t* __restrict__ cache_k, + std::uint8_t* __restrict__ cache_v, + std::uint8_t* __restrict__ scale_k, + std::uint8_t* __restrict__ scale_v, + std::uint8_t* __restrict__ cache_k_residual, + std::uint8_t* __restrict__ scale_k_residual, + std::int32_t width) { + constexpr int Warps = 8; + constexpr unsigned FullMask = 0xffffffffu; + const int tokens = metadata.valid_tokens(width); + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int unit = static_cast(blockIdx.x) * Warps + warp; + const int units = tokens * Geometry::KVHeads * kGqaKvNvfp4Groups; + if (unit >= units) { return; } + + const int group = unit % kGqaKvNvfp4Groups; + const int tmp = unit / kGqaKvNvfp4Groups; + const int kv_head = tmp % Geometry::KVHeads; + const int token = tmp / Geometry::KVHeads; + const int position = positions[0] + token; + const std::int32_t* block_table = metadata.block_table(); + int page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + page = __shfl_sync(FullMask, page, 0); + const int page_off = position & kPagedKVPageMask; + + // ---- K: rotate + pack ---- + float kx[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { + const int block = group * 4 + lane; + const std::int64_t src = + gqa_kv_nvfp4_src_index(kv_head, group * 16, token) + lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { kx[j] = __bfloat162float(k[src + j]); } + const float y0 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 3); + kx[0] = y0; + kx[1] = y1; + kx[2] = y2; + kx[3] = y3; +#pragma unroll + for (int j = 0; j < 4; ++j) { + kx[j] *= gqa_kv_row_scale(layer, kv_head, group * 16 + lane * 4 + j); + } + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), fmaxf(fabsf(kx[2]), fabsf(kx[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + kmax = fmaxf(kmax, __shfl_xor_sync(FullMask, kmax, off)); + } + const float kscale = fmaxf(kmax / 6.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t code = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_k[code + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[0] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[1] / kscale) << 4)); + cache_k[code + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[2] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[3] / kscale) << 4)); + } + if (lane == 0) { + scale_k[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + + // ---- K residual: second E2M1 stage over the first-stage error ---- + if (cache_k_residual != nullptr) { + float res[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { +#pragma unroll + for (int j = 0; j < 4; ++j) { + const std::uint8_t code_j = gqa_kv_nvfp4_e2m1_nibble(kx[j] / kscale); + res[j] = kx[j] - gqa_kv_nvfp4_e2m1_to_f32(code_j) * kscale; + } + } + float rmax = fmaxf(fmaxf(fabsf(res[0]), fabsf(res[1])), + fmaxf(fabsf(res[2]), fabsf(res[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + rmax = fmaxf(rmax, __shfl_xor_sync(FullMask, rmax, off)); + } + const float rscale = fmaxf(rmax / 6.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t rcode = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_k_residual[rcode + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(res[0] / rscale) | + (gqa_kv_nvfp4_e2m1_nibble(res[1] / rscale) << 4)); + cache_k_residual[rcode + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(res[2] / rscale) | + (gqa_kv_nvfp4_e2m1_nibble(res[3] / rscale) << 4)); + } + if (lane == 0) { + scale_k_residual[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(rscale); + } + } + + // ---- V: gain-only pack ---- + const float v0 = lane < 16 ? __bfloat162float(v[gqa_kv_nvfp4_src_index( + kv_head, group * 16 + lane, token)]) + : 0.0f; + float vmax = fabsf(v0); +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + vmax = fmaxf(vmax, __shfl_xor_sync(FullMask, vmax, off)); + } + const float vscale = fmaxf(vmax / 6.0f, 0.001953125f); + if (lane < 8) { + const float ve = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2, + token)]); + const float vo = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2 + 1, + token)]); + const std::int64_t code = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_v[code + lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(ve / vscale) | + (gqa_kv_nvfp4_e2m1_nibble(vo / vscale) << 4)); + } + if (lane == 0) { + scale_v[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } +} + +// ISO3 cache append: K is rotated per 4-channel block (same IsoQuant matrix as +// NVFP4), then both K and V quantize to packed sign-magnitude INT3 nibbles with +// one E4M3FN scale per 16-channel group. +template +__launch_bounds__(256) __global__ + void gqa_attention_prefill_fill_iso3_kernel(const __nv_bfloat16* __restrict__ k, + const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, + Metadata metadata, + std::uint8_t* __restrict__ cache_k, + std::uint8_t* __restrict__ cache_v, + std::uint8_t* __restrict__ scale_k, + std::uint8_t* __restrict__ scale_v, + std::int32_t width) { + constexpr int Warps = 8; + constexpr unsigned FullMask = 0xffffffffu; + const int tokens = metadata.valid_tokens(width); + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int unit = static_cast(blockIdx.x) * Warps + warp; + const int units = tokens * Geometry::KVHeads * kGqaKvNvfp4Groups; + if (unit >= units) { return; } + + const int group = unit % kGqaKvNvfp4Groups; + const int tmp = unit / kGqaKvNvfp4Groups; + const int kv_head = tmp % Geometry::KVHeads; + const int token = tmp / Geometry::KVHeads; + const int position = positions[0] + token; + const std::int32_t* block_table = metadata.block_table(); + int page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + page = __shfl_sync(FullMask, page, 0); + const int page_off = position & kPagedKVPageMask; + + // ---- K: rotate + pack ---- + float kx[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { + const int block = group * 4 + lane; + const std::int64_t src = + gqa_kv_nvfp4_src_index(kv_head, group * 16, token) + lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { kx[j] = __bfloat162float(k[src + j]); } + const float y0 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 3); + kx[0] = y0; + kx[1] = y1; + kx[2] = y2; + kx[3] = y3; + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), fmaxf(fabsf(kx[2]), fabsf(kx[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + kmax = fmaxf(kmax, __shfl_xor_sync(FullMask, kmax, off)); + } + const float kscale = fmaxf(kmax / 7.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t code = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_k[code + 2 * lane] = + static_cast(gqa_iso3_nibble(kx[0], kscale) | + (gqa_iso3_nibble(kx[1], kscale) << 4)); + cache_k[code + 2 * lane + 1] = + static_cast(gqa_iso3_nibble(kx[2], kscale) | + (gqa_iso3_nibble(kx[3], kscale) << 4)); + } + if (lane == 0) { + scale_k[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + + // ---- V: gain-only pack ---- + const float v0 = lane < 16 ? __bfloat162float(v[gqa_kv_nvfp4_src_index( + kv_head, group * 16 + lane, token)]) + : 0.0f; + float vmax = fabsf(v0); +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + vmax = fmaxf(vmax, __shfl_xor_sync(FullMask, vmax, off)); + } + const float vscale = fmaxf(vmax / 7.0f, 0.001953125f); + if (lane < 8) { + const float ve = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2, + token)]); + const float vo = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2 + 1, + token)]); + const std::int64_t code = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_v[code + lane] = + static_cast(gqa_iso3_nibble(ve, vscale) | + (gqa_iso3_nibble(vo, vscale) << 4)); + } + if (lane == 0) { + scale_v[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } +} + +// Mixed cache append for the K=NVFP4 / V=ISO3 global tier: K keeps the NVFP4 +// E2M1 codec after IsoQuant rotation, V stores ISO3 sign-magnitude nibbles. +template +__launch_bounds__(256) __global__ + void gqa_attention_prefill_fill_nvfp4k_iso3v_kernel( + const __nv_bfloat16* __restrict__ k, const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, int layer, Metadata metadata, + std::uint8_t* __restrict__ cache_k, std::uint8_t* __restrict__ cache_v, + std::uint8_t* __restrict__ scale_k, std::uint8_t* __restrict__ scale_v, + std::uint8_t* __restrict__ cache_k_residual, std::uint8_t* __restrict__ scale_k_residual, + std::uint8_t* __restrict__ cache_v_residual, std::uint8_t* __restrict__ scale_v_residual, + std::int32_t width) { + constexpr int Warps = 8; + constexpr unsigned FullMask = 0xffffffffu; + const int tokens = metadata.valid_tokens(width); + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int unit = static_cast(blockIdx.x) * Warps + warp; + const int units = tokens * Geometry::KVHeads * kGqaKvNvfp4Groups; + if (unit >= units) { return; } + + const int group = unit % kGqaKvNvfp4Groups; + const int tmp = unit / kGqaKvNvfp4Groups; + const int kv_head = tmp % Geometry::KVHeads; + const int token = tmp / Geometry::KVHeads; + const int position = positions[0] + token; + const std::int32_t* block_table = metadata.block_table(); + int page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + page = __shfl_sync(FullMask, page, 0); + const int page_off = position & kPagedKVPageMask; + + // ---- K: rotate + NVFP4 E2M1 pack ---- + float kx[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { + const int block = group * 4 + lane; + const std::int64_t src = + gqa_kv_nvfp4_src_index(kv_head, group * 16, token) + lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { kx[j] = __bfloat162float(k[src + j]); } + const float y0 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 3); + kx[0] = y0; + kx[1] = y1; + kx[2] = y2; + kx[3] = y3; +#pragma unroll + for (int j = 0; j < 4; ++j) { + kx[j] *= gqa_kv_row_scale(layer, kv_head, group * 16 + lane * 4 + j); + } + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), fmaxf(fabsf(kx[2]), fabsf(kx[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + kmax = fmaxf(kmax, __shfl_xor_sync(FullMask, kmax, off)); + } + const float kscale = fmaxf(kmax / 6.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t code = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_k[code + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[0] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[1] / kscale) << 4)); + cache_k[code + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(kx[2] / kscale) | + (gqa_kv_nvfp4_e2m1_nibble(kx[3] / kscale) << 4)); + } + if (lane == 0) { + scale_k[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + + // ---- K residual: second E2M1 stage over the first-stage error ---- + if (cache_k_residual != nullptr) { + float res[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { +#pragma unroll + for (int j = 0; j < 4; ++j) { + const std::uint8_t code_j = gqa_kv_nvfp4_e2m1_nibble(kx[j] / kscale); + res[j] = kx[j] - gqa_kv_nvfp4_e2m1_to_f32(code_j) * kscale; + } + } + float rmax = fmaxf(fmaxf(fabsf(res[0]), fabsf(res[1])), + fmaxf(fabsf(res[2]), fabsf(res[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + rmax = fmaxf(rmax, __shfl_xor_sync(FullMask, rmax, off)); + } + const float rscale = fmaxf(rmax / 6.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t rcode = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_k_residual[rcode + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(res[0] / rscale) | + (gqa_kv_nvfp4_e2m1_nibble(res[1] / rscale) << 4)); + cache_k_residual[rcode + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(res[2] / rscale) | + (gqa_kv_nvfp4_e2m1_nibble(res[3] / rscale) << 4)); + } + if (lane == 0) { + scale_k_residual[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(rscale); + } + } + + // ---- V: gain-only ISO3 pack ---- + const float v0 = lane < 16 ? __bfloat162float(v[gqa_kv_nvfp4_src_index( + kv_head, group * 16 + lane, token)]) + : 0.0f; + float vmax = fabsf(v0); +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + vmax = fmaxf(vmax, __shfl_xor_sync(FullMask, vmax, off)); + } + const float vscale = fmaxf(vmax / 7.0f, 0.001953125f); + if (lane < 8) { + const float ve = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2, + token)]); + const float vo = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2 + 1, + token)]); + const std::int64_t code = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_v[code + lane] = + static_cast(gqa_iso3_nibble(ve, vscale) | + (gqa_iso3_nibble(vo, vscale) << 4)); + } + if (lane == 0) { + scale_v[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } + + // ---- V residual: second ISO3 stage over the first-stage error ---- + if (cache_v_residual != nullptr) { + float res[2] = {0.0f, 0.0f}; + float rmax = 0.0f; + if (lane < 8) { + const float ve = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane * 2, + token)]); + const float vo = __bfloat162float(v[gqa_kv_nvfp4_src_index( + kv_head, group * 16 + lane * 2 + 1, token)]); + const std::uint8_t ce = gqa_iso3_nibble(ve, vscale); + const std::uint8_t co = gqa_iso3_nibble(vo, vscale); + res[0] = ve - gqa_iso3_decode(ce) * vscale; + res[1] = vo - gqa_iso3_decode(co) * vscale; + rmax = fmaxf(fabsf(res[0]), fabsf(res[1])); + } else if (lane < 16) { + const float vd = + __bfloat162float(v[gqa_kv_nvfp4_src_index(kv_head, group * 16 + lane, + token)]); + const std::uint8_t code_d = gqa_iso3_nibble(vd, vscale); + res[0] = vd - gqa_iso3_decode(code_d) * vscale; + rmax = fabsf(res[0]); + } +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + rmax = fmaxf(rmax, __shfl_xor_sync(FullMask, rmax, off)); + } + const float rvscale = fmaxf(rmax / 7.0f, 0.001953125f); + if (lane < 8) { + const std::int64_t rcode = + gqa_kv_nvfp4_code_index(page, kv_head, group * 16, page_off); + cache_v_residual[rcode + lane] = + static_cast(gqa_iso3_nibble(res[0], rvscale) | + (gqa_iso3_nibble(res[1], rvscale) << 4)); + } + if (lane == 0) { + scale_v_residual[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(rvscale); + } + } +} + +template +__launch_bounds__(256) __global__ + void gqa_attention_prefill_fill_fp8_kernel(const __nv_bfloat16* __restrict__ k, + const __nv_bfloat16* __restrict__ v, + const std::int32_t* __restrict__ positions, + Metadata metadata, + std::uint8_t* __restrict__ cache_k, + std::uint8_t* __restrict__ cache_v, + std::uint8_t* __restrict__ scale_k, + std::uint8_t* __restrict__ scale_v, + std::int32_t width) { + constexpr int Warps = 8; + constexpr unsigned FullMask = 0xffffffffu; + const int tokens = metadata.valid_tokens(width); + const int warp = static_cast(threadIdx.x) >> 5; + const int lane = static_cast(threadIdx.x) & 31; + const int unit = static_cast(blockIdx.x) * Warps + warp; + const int units = tokens * Geometry::KVHeads * kGqaKvNvfp4Groups; + if (unit >= units) { return; } + + const int group = unit % kGqaKvNvfp4Groups; + const int tmp = unit / kGqaKvNvfp4Groups; + const int kv_head = tmp % Geometry::KVHeads; + const int token = tmp / Geometry::KVHeads; + const int position = positions[0] + token; + const std::int32_t* block_table = metadata.block_table(); + int page = lane == 0 ? paged_kv_physical_page(block_table, position) : 0; + page = __shfl_sync(FullMask, page, 0); + const int page_off = position & kPagedKVPageMask; + + // ---- K: rotate + FP8 pack ---- + float kx[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + if (lane < 4) { + const int block = group * 4 + lane; + const std::int64_t src = + gqa_kv_nvfp4_src_index(kv_head, group * 16, token) + lane * 4; +#pragma unroll + for (int j = 0; j < 4; ++j) { kx[j] = __bfloat162float(k[src + j]); } + const float y0 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 0); + const float y1 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 1); + const float y2 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 2); + const float y3 = gqa_prefill_nvfp4_rot(kx[0], kx[1], kx[2], kx[3], block, 3); + kx[0] = y0; + kx[1] = y1; + kx[2] = y2; + kx[3] = y3; + } + float kmax = fmaxf(fmaxf(fabsf(kx[0]), fabsf(kx[1])), fmaxf(fabsf(kx[2]), fabsf(kx[3]))); +#pragma unroll + for (int off = 1; off <= 2; off <<= 1) { + kmax = fmaxf(kmax, __shfl_xor_sync(FullMask, kmax, off)); + } + const float kscale = fmaxf(kmax / 448.0f, 0.001953125f); + if (lane < 4) { + const std::int64_t base = paged_kv_element_offset( + page, kv_head, page_off, group * 16 + lane * 4); +#pragma unroll + for (int j = 0; j < 4; ++j) { + cache_k[base + j] = gqa_kv_nvfp4_fp32_to_e4m3(kx[j] / kscale); + } + } + if (lane == 0) { + scale_k[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(kscale); + } + + // ---- V: gain-only FP8 pack ---- + const float v0 = lane < 16 ? __bfloat162float(v[gqa_kv_nvfp4_src_index( + kv_head, group * 16 + lane, token)]) + : 0.0f; + float vmax = fabsf(v0); +#pragma unroll + for (int off = 8; off > 0; off >>= 1) { + vmax = fmaxf(vmax, __shfl_xor_sync(FullMask, vmax, off)); + } + const float vscale = fmaxf(vmax / 448.0f, 0.001953125f); + if (lane < 16) { + const std::int64_t base = paged_kv_element_offset( + page, kv_head, page_off, group * 16 + lane); + cache_v[base] = gqa_kv_nvfp4_fp32_to_e4m3(v0 / vscale); + } + if (lane == 0) { + scale_v[gqa_kv_nvfp4_scale_index(page, kv_head, group, page_off)] = + gqa_kv_nvfp4_fp32_to_e4m3(vscale); + } +} + +// Warp-specialized FlashAttention-2 forward over the packed cache. Producer +// warps dequantize; consumer warps run the exact BF16 tensor-core attention +// body with Bc = 32. +template +__launch_bounds__(kNvfp4PrefillThreads, 1) __global__ + void gqa_attention_prefill_nvfp4_kernel(const __nv_bfloat16* __restrict__ q, + const std::uint8_t* __restrict__ cache_k, + const std::uint8_t* __restrict__ cache_v, + const std::uint8_t* __restrict__ cache_k_scale, + const std::uint8_t* __restrict__ cache_v_scale, + const std::uint8_t* __restrict__ cache_k_residual, + const std::uint8_t* __restrict__ cache_k_residual_scale, + const std::uint8_t* __restrict__ cache_v_residual, + const std::uint8_t* __restrict__ cache_v_residual_scale, + const std::uint8_t* __restrict__ cold_k_slots, + const std::uint8_t* __restrict__ cold_v_slots, + const std::int32_t* __restrict__ cold_k_valid, + const std::int32_t* __restrict__ cold_v_valid, + int slot_bytes, int sliding_window, int layer, + Metadata metadata, + const std::int32_t* __restrict__ positions, float scale, + __nv_bfloat16* __restrict__ out, std::int32_t width) { + constexpr int D = kGqaPrefillHeadDim; + constexpr int Br = kGqaPrefillBr; // 64 + constexpr int Bc = kNvfp4PrefillBc; // 32 + constexpr int Threads = kNvfp4PrefillThreads; // 256 + constexpr int ProducerThreads = 128; + constexpr int QKNt = Bc / 8; // 4 + constexpr int QKKs = D / 16; // 16 + constexpr int PVNt = D / 8; // 32 + constexpr int PVKs = Bc / 16; // 2 + constexpr float Log2E = 1.4426950408889634074f; + constexpr unsigned FullMask = 0xffffffffu; + + static_assert(Threads == 256); + static_assert(ProducerThreads == 128); + static_assert(QKNt == 4); + static_assert(PVKs == 2); + static_assert(KVDType == DType::NVFP4 || KVDType == DType::FP8_E4M3FN || + KVDType == DType::ISO3); + static_assert(VVDType == DType::NVFP4 || VVDType == DType::FP8_E4M3FN || + VVDType == DType::ISO3); + + extern __shared__ __align__(16) std::uint8_t nvfp4_smem[]; + constexpr bool Mxf4QK = KVDType == DType::NVFP4; + constexpr int Mxf4QKKs = D / 64; + static_assert(!Mxf4QK || Mxf4QKKs == 4); + + __nv_bfloat16* q_s = nullptr; + std::uint8_t* q_a = nullptr; + std::uint8_t* q_sf = nullptr; + std::uint8_t* k_pk0 = nullptr; + std::uint8_t* k_sf0 = nullptr; + std::uint8_t* k_rpk0 = nullptr; + std::uint8_t* k_rsf0 = nullptr; + std::uint8_t* k_pk1 = nullptr; + std::uint8_t* k_sf1 = nullptr; + std::uint8_t* k_rpk1 = nullptr; + std::uint8_t* k_rsf1 = nullptr; + __nv_bfloat16* k_s0 = nullptr; + __nv_bfloat16* k_s1 = nullptr; + __nv_bfloat16* v_s0 = nullptr; + __nv_bfloat16* v_s1 = nullptr; + volatile std::uint32_t* flags = nullptr; + if constexpr (Mxf4QK) { + // Q packed E2M1 + scales, two packed 32-key K main/residual tiles, + // then the BF16 V tiles consumed by the BF16 PV body. + std::uint8_t* smem8 = nvfp4_smem; + q_a = smem8; // [Br, 128] + q_sf = q_a + Br * 128; // [Br, 16] + k_pk0 = q_sf + Br * 16; // [Bc, 128] + k_rpk0 = k_pk0 + Bc * 128; + k_sf0 = k_rpk0 + Bc * 128; // [Bc, 16] + k_rsf0 = k_sf0 + Bc * 16; + k_pk1 = k_rsf0 + Bc * 16; + k_rpk1 = k_pk1 + Bc * 128; + k_sf1 = k_rpk1 + Bc * 128; + k_rsf1 = k_sf1 + Bc * 16; + v_s0 = reinterpret_cast<__nv_bfloat16*>(k_rsf1 + Bc * 16); + v_s1 = v_s0 + Bc * D; + flags = reinterpret_cast(v_s1 + Bc * D); + } else { + q_s = reinterpret_cast<__nv_bfloat16*>(nvfp4_smem); // [Br, D] + k_s0 = q_s + Br * D; + k_s1 = k_s0 + Bc * D; + v_s0 = k_s1 + Bc * D; + v_s1 = v_s0 + Bc * D; + flags = reinterpret_cast(v_s1 + Bc * D); + } + + const int q_block = static_cast(blockIdx.x); + const int q_head = static_cast(blockIdx.y); + const int tid = static_cast(threadIdx.x); + const int warp = tid >> 5; + const int lane = tid & 31; + const int q0 = q_block * Br; + const int kv_head = q_head / Geometry::GroupSize; + const int tokens = metadata.valid_tokens(width); + + if (q_head >= Geometry::QHeads || q0 >= width) { return; } + if (q0 >= tokens) { + gqa_prefill_zero_output_rows(out, q_head, q0, min(q0 + Br, width), tid, Threads); + return; + } + const int base_pos = positions[0]; + const std::int32_t* block_table = metadata.block_table(); + + // ---- stage Q into smem once (all threads) ---- + if constexpr (Mxf4QK) { + // On-chip Q quantization: rotate each 4-channel block with the baked + // IsoQuant matrix, then pack per-16-group E2M1 with E4M3 scales. + for (int i = tid; i < Br * 128; i += Threads) { q_a[i] = 0; } + for (int i = tid; i < Br * 16; i += Threads) { q_sf[i] = kGqaPrefillMxf4E4M3One; } + __syncthreads(); + constexpr int Groups = kGqaKvNvfp4Groups; + const int q_rows = min(Br, tokens - q0); + for (int unit = warp; unit < q_rows * Groups; unit += 8) { + const int row = unit / Groups; + const int grp = unit - row * Groups; + const __nv_bfloat16* src = + q + gqa_prefill_q_index(q_head, grp * 16, q0 + row); + float qx[4]; + gqa_prefill_mxf4_rotate_4(qx, src, grp, lane); +#pragma unroll + for (int j = 0; j < 4; ++j) { + qx[j] *= gqa_kv_row_scale_inv(layer, kv_head, grp * 16 + lane * 4 + j); + } + float qmax = fmaxf(fmaxf(fabsf(qx[0]), fabsf(qx[1])), + fmaxf(fabsf(qx[2]), fabsf(qx[3]))); + qmax = gqa_prefill_mxf4_group_max4(qmax, FullMask); + const float qscale = fmaxf(qmax / 6.0f, kGqaPrefillMxf4MinScale); + if (lane < 4) { + q_a[row * 128 + grp * 8 + 2 * lane] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(qx[0] / qscale) | + (gqa_kv_nvfp4_e2m1_nibble(qx[1] / qscale) << 4)); + q_a[row * 128 + grp * 8 + 2 * lane + 1] = + static_cast(gqa_kv_nvfp4_e2m1_nibble(qx[2] / qscale) | + (gqa_kv_nvfp4_e2m1_nibble(qx[3] / qscale) << 4)); + } + if (lane == 0) { + q_sf[row * 16 + grp] = gqa_kv_nvfp4_fp32_to_e4m3(qscale); + } + } + } else { + constexpr int VecPerRow = D / 8; + constexpr int QRowStride = D * Geometry::QHeads; + const __nv_bfloat16* q_block = q + gqa_prefill_q_index(q_head, 0, q0); + for (int chunk = tid; chunk < Br * VecPerRow; chunk += Threads) { + const int row = chunk / VecPerRow; + const int d = (chunk - row * VecPerRow) << 3; + __nv_bfloat16* p = &q_s[row * D + gqa_prefill_swz(row, d)]; + if (q0 + row < tokens) { + float x[8]; +#pragma unroll + for (int j = 0; j < 8; ++j) { + x[j] = __bfloat162float(q_block[row * QRowStride + d + j]); + } + gqa_prefill_nvfp4_rotate_8(x, d); + unsigned packed[4]; +#pragma unroll + for (int i = 0; i < 4; ++i) { + packed[i] = pack_bf16x2(x[2 * i], x[2 * i + 1]); + } + store_vec(p, make_int4(static_cast(packed[0]), static_cast(packed[1]), + static_cast(packed[2]), static_cast(packed[3]))); + } else { + store_vec(p, make_int4(0, 0, 0, 0)); + } + } + } + + for (int i = tid; i < 8; i += Threads) { flags[i] = 0; } + if (tid == 1) { flags[1] = 1; } // K slot 0 free + if (tid == 3) { flags[3] = 1; } // K slot 1 free + if (tid == 5) { flags[5] = 1; } // V slot 0 free + if (tid == 7) { flags[7] = 1; } // V slot 1 free + __syncthreads(); + + const int tile_rows = min(Br, tokens - q0); + const int max_query_abs = base_pos + q0 + tile_rows - 1; + const int window = (sliding_window > 0 && KVDType == DType::NVFP4) ? sliding_window : 0; + const int visible_start = window > 0 ? max(0, base_pos + q0 - window + 1) : 0; + const int kb_start = visible_start / (2 * Bc); + const int n_block64 = (max_query_abs / (2 * Bc)) + 1 - kb_start; + const float scale_l2 = scale * Log2E; + + if (warp >= 4) { + // ---- producer: stage packed K and dequantized V sub-tiles into + // ping-pong smem buffers. Named barrier 0 is the full-CTA handshake; + // producer threads first decode any cold slot half-page, synchronized + // by producer-only named barrier 1. ---- + const int ptid = tid - ProducerThreads; + const auto stage_v = [&](__nv_bfloat16* v_s, int k0i, int page) { + if constexpr (VVDType == DType::FP8_E4M3FN) { + gqa_prefill_fp8_stage_kv( + v_s, cache_v, cache_v_scale, kv_head, k0i, max_query_abs, page, ptid); + } else if constexpr (VVDType == DType::ISO3) { + gqa_prefill_iso3_stage_kv( + v_s, cache_v, cache_v_scale, kv_head, k0i, max_query_abs, page, ptid); + if (cache_v_residual != nullptr) { + gqa_prefill_iso3_stage_v_residual( + v_s, cache_v_residual, cache_v_residual_scale, kv_head, k0i, + max_query_abs, page, ptid); + } + } else { + gqa_prefill_nvfp4_stage_kv( + v_s, cache_v, cache_v_scale, kv_head, k0i, visible_start, max_query_abs, page, + ptid); + } + }; + const auto stage_k_bf16 = [&](__nv_bfloat16* k_s, int k0i, int page) { + if constexpr (KVDType == DType::FP8_E4M3FN) { + gqa_prefill_fp8_stage_kv( + k_s, cache_k, cache_k_scale, kv_head, k0i, max_query_abs, page, ptid); + } else if constexpr (KVDType == DType::ISO3) { + gqa_prefill_iso3_stage_kv( + k_s, cache_k, cache_k_scale, kv_head, k0i, max_query_abs, page, ptid); + } else { + gqa_prefill_nvfp4_stage_kv( + k_s, cache_k, cache_k_scale, kv_head, k0i, visible_start, max_query_abs, page, + ptid); + } + }; + const auto stage_k_cold_bf16 = [&](__nv_bfloat16* k_s, const std::uint8_t* k_slot, + int half, int k0i) { + if (ptid < kEntropyNvfp4SlotStreamsPerHalf) { + gqa_prefill_nvfp4_cold_decode_kv( + k_s, k_slot, entropy_nvfp4_slot_scales(k_slot, slot_bytes), half, k0i, + visible_start, max_query_abs, ptid); + } + }; + for (int kb = 0; kb < n_block64; ++kb) { + const int kb64 = kb_start + kb; + const int k0 = kb64 * 2 * Bc; + const int table_entry = block_table[kb64]; + const bool cold_available = table_entry <= -2 && cold_k_slots != nullptr && + cold_v_slots != nullptr && cold_k_valid != nullptr && + cold_v_valid != nullptr && slot_bytes >= 1024 + 320; + const int slot_base = cold_available ? -table_entry - 2 : 0; + const int cold_slot_id = slot_base + kv_head; + const bool cold = cold_available && cold_k_valid[cold_slot_id] != 0 && + cold_v_valid[cold_slot_id] != 0; + const int physical_page = cold ? 0 : table_entry; + const std::uint8_t* k_slot = + cold ? cold_k_slots + static_cast(cold_slot_id) * slot_bytes + : nullptr; + const std::uint8_t* v_slot = + cold ? cold_v_slots + static_cast(cold_slot_id) * slot_bytes + : nullptr; + + // ---- half 0 (slot 0) ---- + if constexpr (Mxf4QK) { + if (cold) { + gqa_prefill_mxf4_stage_k_cold( + k_pk0, k_sf0, k_slot, slot_bytes, 0, k0, visible_start, + max_query_abs, ptid); + for (int chunk = ptid; chunk < Bc * 8; chunk += ProducerThreads) { + const int key_l = chunk >> 3; + const int j = chunk & 7; + store_vec(&k_rpk0[key_l * 128 + j * 16], make_int4(0, 0, 0, 0)); + } + for (int row = ptid; row < Bc; row += ProducerThreads) { + store_vec(&k_rsf0[row * 16], make_int4(0, 0, 0, 0)); + } + } else { + gqa_prefill_mxf4_stage_k_packed( + k_pk0, k_sf0, cache_k, cache_k_scale, kv_head, k0, visible_start, + max_query_abs, physical_page, ptid); + if (cache_k_residual != nullptr) { + gqa_prefill_mxf4_stage_k_packed( + k_rpk0, k_rsf0, cache_k_residual, cache_k_residual_scale, kv_head, k0, + visible_start, max_query_abs, physical_page, ptid); + } else { + for (int chunk = ptid; chunk < Bc * 8; chunk += ProducerThreads) { + const int key_l = chunk >> 3; + const int j = chunk & 7; + store_vec(&k_rpk0[key_l * 128 + j * 16], make_int4(0, 0, 0, 0)); + } + for (int row = ptid; row < Bc; row += ProducerThreads) { + store_vec(&k_rsf0[row * 16], make_int4(0, 0, 0, 0)); + } + } + } + } else { + if (cold) { + stage_k_cold_bf16(k_s0, k_slot, 0, k0); + } else { + stage_k_bf16(k_s0, k0, physical_page); + } + } + if (cold) { + if (ptid >= kEntropyNvfp4SlotStreamsPerHalf && + ptid < 2 * kEntropyNvfp4SlotStreamsPerHalf) { + gqa_prefill_nvfp4_cold_decode_kv( + v_s0, v_slot, entropy_nvfp4_slot_scales(v_slot, slot_bytes), 0, k0, + visible_start, max_query_abs, ptid - kEntropyNvfp4SlotStreamsPerHalf); + } + gqa_prefill_bar_sync(1, ProducerThreads); + } else { + stage_v(v_s0, k0, physical_page); + } + gqa_prefill_bar_sync(0, Threads); + + // ---- half 1 (slot 1) ---- + if constexpr (Mxf4QK) { + if (cold) { + gqa_prefill_mxf4_stage_k_cold( + k_pk1, k_sf1, k_slot, slot_bytes, 1, k0 + Bc, visible_start, + max_query_abs, ptid); + for (int chunk = ptid; chunk < Bc * 8; chunk += ProducerThreads) { + const int key_l = chunk >> 3; + const int j = chunk & 7; + store_vec(&k_rpk1[key_l * 128 + j * 16], make_int4(0, 0, 0, 0)); + } + for (int row = ptid; row < Bc; row += ProducerThreads) { + store_vec(&k_rsf1[row * 16], make_int4(0, 0, 0, 0)); + } + } else { + gqa_prefill_mxf4_stage_k_packed( + k_pk1, k_sf1, cache_k, cache_k_scale, kv_head, k0 + Bc, visible_start, + max_query_abs, physical_page, ptid); + if (cache_k_residual != nullptr) { + gqa_prefill_mxf4_stage_k_packed( + k_rpk1, k_rsf1, cache_k_residual, cache_k_residual_scale, kv_head, + k0 + Bc, visible_start, max_query_abs, physical_page, ptid); + } else { + for (int chunk = ptid; chunk < Bc * 8; chunk += ProducerThreads) { + const int key_l = chunk >> 3; + const int j = chunk & 7; + store_vec(&k_rpk1[key_l * 128 + j * 16], make_int4(0, 0, 0, 0)); + } + for (int row = ptid; row < Bc; row += ProducerThreads) { + store_vec(&k_rsf1[row * 16], make_int4(0, 0, 0, 0)); + } + } + } + } else { + if (cold) { + stage_k_cold_bf16(k_s1, k_slot, 1, k0 + Bc); + } else { + stage_k_bf16(k_s1, k0 + Bc, physical_page); + } + } + if (cold) { + if (ptid >= kEntropyNvfp4SlotStreamsPerHalf && + ptid < 2 * kEntropyNvfp4SlotStreamsPerHalf) { + gqa_prefill_nvfp4_cold_decode_kv( + v_s1, v_slot, entropy_nvfp4_slot_scales(v_slot, slot_bytes), 1, + k0 + Bc, visible_start, max_query_abs, + ptid - kEntropyNvfp4SlotStreamsPerHalf); + } + gqa_prefill_bar_sync(1, ProducerThreads); + } else { + stage_v(v_s1, k0 + Bc, physical_page); + } + gqa_prefill_bar_sync(0, Threads); + + gqa_prefill_bar_sync(0, Threads); + } + return; + } + + // ---- consumer: exact BF16 FlashAttention body over the dequantized tiles ---- + const int gid = lane >> 2; + const int lid = lane & 3; + + const int b_rin = lane & 7; + const int warp_row0 = warp * 16; + + const unsigned v_as = static_cast((lane >> 4) << 4); + const unsigned v_r = static_cast(b_rin << 4); + + float acc[PVNt][4]; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { +#pragma unroll + for (int i = 0; i < 4; ++i) { acc[n][i] = 0.0f; } + } + float m0 = -CUDART_INF_F, m1 = -CUDART_INF_F, l0 = 0.0f, l1 = 0.0f; + + constexpr int QKNt64 = 8; // 64-key score n-tiles + constexpr int PVKs64 = 4; // 64-key PV contraction groups + + const auto qk_half_mxf4 = [&](const std::uint8_t* k_pk, const std::uint8_t* k_sf, + const std::uint8_t* k_rpk, const std::uint8_t* k_rsf, + float (&score)[QKNt][4]) { +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; + } +#pragma unroll + for (int k = 0; k < Mxf4QKKs; ++k) { + unsigned af[4]; + gqa_prefill_mxf4_load_a_frag(af, q_a + warp_row0 * 128, lane, k); + const unsigned sfa = load_vec( + q_sf + warp_row0 * 16 + (gid + (lid & 1) * 8) * 16 + k * 4); +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + unsigned bf[2]; + gqa_prefill_mxf4_load_b_frag(bf, k_pk, lane, nt, k); + const unsigned sfb = load_vec(k_sf + (gid + nt * 8) * 16 + k * 4); + mma_nvfp4_e4m3(score[nt][0], score[nt][1], score[nt][2], score[nt][3], + af[0], af[1], af[2], af[3], bf[0], bf[1], sfa, sfb); + } + } + // Second pass accumulates the E2M1 residual K plane. +#pragma unroll + for (int k = 0; k < Mxf4QKKs; ++k) { + unsigned af[4]; + gqa_prefill_mxf4_load_a_frag(af, q_a + warp_row0 * 128, lane, k); + const unsigned sfa = load_vec( + q_sf + warp_row0 * 16 + (gid + (lid & 1) * 8) * 16 + k * 4); +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + unsigned bf[2]; + gqa_prefill_mxf4_load_b_frag(bf, k_rpk, lane, nt, k); + const unsigned sfb = load_vec(k_rsf + (gid + nt * 8) * 16 + k * 4); + mma_nvfp4_e4m3(score[nt][0], score[nt][1], score[nt][2], score[nt][3], + af[0], af[1], af[2], af[3], bf[0], bf[1], sfa, sfb); + } + } + }; + + const auto qk_half_bf16 = [&](const __nv_bfloat16* k_s, float (&score)[QKNt][4]) { + const int a_mat = lane >> 3; + const int a_rin = lane & 7; + const int a_rowoff = a_rin + ((a_mat & 1) << 3); + const int b_koff = ((lane >> 3) & 1) << 3; + const unsigned q_sbase = smem_addr(q_s); + const unsigned q_lane_base = + q_sbase + static_cast((warp_row0 + a_rowoff) * 512); + const unsigned q_as = static_cast((a_mat >> 1) << 4); + const unsigned q_r = static_cast(a_rin << 4); + const unsigned k_as = static_cast((b_koff >> 3) << 4); + const unsigned k_r = static_cast(b_rin << 4); + const unsigned k_sbase = smem_addr(k_s); + const unsigned k_lane_base = + k_sbase + static_cast(b_rin * 512) + + (static_cast(lane >> 4) << 12); +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score[nt][1] = score[nt][2] = score[nt][3] = 0.0f; + } + unsigned af[2][4]; + unsigned bf[2][QKNt][2]; + { + ldmatrix_x4(af[0][0], af[0][1], af[0][2], af[0][3], + gqa_prefill_swz_addr(q_lane_base, 0u, q_as, q_r)); +#pragma unroll + for (int nt2 = 0; nt2 < QKNt; nt2 += 2) { + ldmatrix_x4(bf[0][nt2][0], bf[0][nt2][1], bf[0][nt2 + 1][0], bf[0][nt2 + 1][1], + gqa_prefill_swz_addr( + k_lane_base + static_cast(nt2 * 4096), 0u, k_as, k_r)); + } + } +#pragma unroll + for (int k = 0; k < QKKs; ++k) { + const int cur = k & 1; + const int nxt = cur ^ 1; + if (k + 1 < QKKs) { + const unsigned ck = static_cast((k + 1) << 5); + ldmatrix_x4(af[nxt][0], af[nxt][1], af[nxt][2], af[nxt][3], + gqa_prefill_swz_addr(q_lane_base, ck, q_as, q_r)); +#pragma unroll + for (int nt2 = 0; nt2 < QKNt; nt2 += 2) { + ldmatrix_x4( + bf[nxt][nt2][0], bf[nxt][nt2][1], bf[nxt][nt2 + 1][0], + bf[nxt][nt2 + 1][1], + gqa_prefill_swz_addr( + k_lane_base + static_cast(nt2 * 4096), ck, k_as, k_r)); + } + } +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + mma_bf16(score[nt][0], score[nt][1], score[nt][2], score[nt][3], af[cur][0], + af[cur][1], af[cur][2], af[cur][3], bf[cur][nt][0], bf[cur][nt][1]); + } + } + }; + + for (int kb = 0; kb < n_block64; ++kb) { + const int k0 = (kb_start + kb) * 2 * Bc; + + // ---- QK^T over the two 32-key halves, then one 64-key softmax ---- + gqa_prefill_bar_sync(0, Threads); // slot 0 staged by producers + float score_a[QKNt][4]; + if constexpr (Mxf4QK) { + qk_half_mxf4(k_pk0, k_sf0, k_rpk0, k_rsf0, score_a); + } else { + qk_half_bf16(k_s0, score_a); + } + + gqa_prefill_bar_sync(0, Threads); // slot 1 staged; slot 0 read done + float score_b[QKNt][4]; + if constexpr (Mxf4QK) { + qk_half_mxf4(k_pk1, k_sf1, k_rpk1, k_rsf1, score_b); + } else { + qk_half_bf16(k_s1, score_b); + } + + float score[QKNt64][4]; +#pragma unroll + for (int nt = 0; nt < QKNt; ++nt) { + score[nt][0] = score_a[nt][0]; + score[nt][1] = score_a[nt][1]; + score[nt][2] = score_a[nt][2]; + score[nt][3] = score_a[nt][3]; + score[QKNt + nt][0] = score_b[nt][0]; + score[QKNt + nt][1] = score_b[nt][1]; + score[QKNt + nt][2] = score_b[nt][2]; + score[QKNt + nt][3] = score_b[nt][3]; + } + + const int row0 = warp_row0 + gid; + const int row1 = warp_row0 + gid + 8; + const int qrow0 = q0 + row0; + const int qrow1 = q0 + row1; + const int qabs0 = (qrow0 < tokens) ? base_pos + qrow0 : -1; + const int qabs1 = (qrow1 < tokens) ? base_pos + qrow1 : -1; + const bool full_score_tile = + (q0 + Br <= tokens) && ((k0 + 2 * Bc - 1) <= (base_pos + q0)) && + (window == 0 || k0 >= max(0, max_query_abs - window + 1)); + + float bm0 = -CUDART_INF_F, bm1 = -CUDART_INF_F; + if (full_score_tile) { +#pragma unroll + for (int nt = 0; nt < QKNt64; ++nt) { + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + } else { +#pragma unroll + for (int nt = 0; nt < QKNt64; ++nt) { + const int key0 = k0 + nt * 8 + 2 * lid; + const int key1 = key0 + 1; + const int row0_start = (window > 0 && qabs0 >= 0) ? max(0, qabs0 - window + 1) : 0; + const int row1_start = (window > 0 && qabs1 >= 0) ? max(0, qabs1 - window + 1) : 0; + score[nt][0] = (qrow0 < tokens && key0 <= qabs0 && key0 >= row0_start) + ? score[nt][0] + : -CUDART_INF_F; + score[nt][1] = (qrow0 < tokens && key1 <= qabs0 && key1 >= row0_start) + ? score[nt][1] + : -CUDART_INF_F; + score[nt][2] = (qrow1 < tokens && key0 <= qabs1 && key0 >= row1_start) + ? score[nt][2] + : -CUDART_INF_F; + score[nt][3] = (qrow1 < tokens && key1 <= qabs1 && key1 >= row1_start) + ? score[nt][3] + : -CUDART_INF_F; + bm0 = fmaxf(bm0, fmaxf(score[nt][0], score[nt][1])); + bm1 = fmaxf(bm1, fmaxf(score[nt][2], score[nt][3])); + } + } + bm0 = warp_max<4>(bm0, FullMask); + bm1 = warp_max<4>(bm1, FullMask); + + const float nm0 = fmaxf(m0, bm0); + const float nm1 = fmaxf(m1, bm1); + const float nm0_scaled = nm0 * scale_l2; + const float nm1_scaled = nm1 * scale_l2; + const float alpha0 = exp2_approx(__fmaf_rn(m0, scale_l2, -nm0_scaled)); + const float alpha1 = exp2_approx(__fmaf_rn(m1, scale_l2, -nm1_scaled)); + + float bl0 = 0.0f, bl1 = 0.0f; + unsigned p_frag[PVKs64][4]; + if (full_score_tile) { +#pragma unroll + for (int nt = 0; nt < QKNt64; ++nt) { + const float p00 = exp2_approx(__fmaf_rn(score[nt][0], scale_l2, -nm0_scaled)); + const float p01 = exp2_approx(__fmaf_rn(score[nt][1], scale_l2, -nm0_scaled)); + const float p10 = exp2_approx(__fmaf_rn(score[nt][2], scale_l2, -nm1_scaled)); + const float p11 = exp2_approx(__fmaf_rn(score[nt][3], scale_l2, -nm1_scaled)); + bl0 += p00 + p01; + bl1 += p10 + p11; + const int pk = nt >> 1; + if ((nt & 1) == 0) { + p_frag[pk][0] = pack_bf16x2(p00, p01); + p_frag[pk][1] = pack_bf16x2(p10, p11); + } else { + p_frag[pk][2] = pack_bf16x2(p00, p01); + p_frag[pk][3] = pack_bf16x2(p10, p11); + } + } + } else { +#pragma unroll + for (int nt = 0; nt < QKNt64; ++nt) { + const float p00 = (score[nt][0] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][0], scale_l2, -nm0_scaled)) + : 0.0f; + const float p01 = (score[nt][1] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][1], scale_l2, -nm0_scaled)) + : 0.0f; + const float p10 = (score[nt][2] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][2], scale_l2, -nm1_scaled)) + : 0.0f; + const float p11 = (score[nt][3] > -CUDART_INF_F) + ? exp2_approx(__fmaf_rn(score[nt][3], scale_l2, -nm1_scaled)) + : 0.0f; + bl0 += p00 + p01; + bl1 += p10 + p11; + const int pk = nt >> 1; + if ((nt & 1) == 0) { + p_frag[pk][0] = pack_bf16x2(p00, p01); + p_frag[pk][1] = pack_bf16x2(p10, p11); + } else { + p_frag[pk][2] = pack_bf16x2(p00, p01); + p_frag[pk][3] = pack_bf16x2(p10, p11); + } + } + } + + l0 = __fmaf_rn(l0, alpha0, bl0); + l1 = __fmaf_rn(l1, alpha1, bl1); + m0 = nm0; + m1 = nm1; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + acc[n][0] *= alpha0; + acc[n][1] *= alpha0; + acc[n][2] *= alpha1; + acc[n][3] *= alpha1; + } + + // ---- O += P V over the two 32-key V halves ---- + constexpr int PVHalf = PVNt / 2; + constexpr int PVLoads = PVKs * PVHalf; +#pragma unroll + for (int half = 0; half < 2; ++half) { + const __nv_bfloat16* v_s = half == 0 ? v_s0 : v_s1; + const unsigned v_sbase = smem_addr(v_s); + const unsigned v_lane_base = + v_sbase + static_cast(((lane >> 3) & 1) * 4096) + + static_cast(b_rin * 512); + unsigned vf[2][4]; + { + ldmatrix_x4_t(vf[0][0], vf[0][1], vf[0][2], vf[0][3], + gqa_prefill_swz_addr(v_lane_base, 0u, v_as, v_r)); + } +#pragma unroll + for (int li = 0; li < PVLoads; ++li) { + const int k = li / PVHalf; + const int n2 = (li % PVHalf) * 2; + const int cur = li & 1; + const int nxt = cur ^ 1; + if (li + 1 < PVLoads) { + const int k2 = (li + 1) / PVHalf; + const int n2b = ((li + 1) % PVHalf) * 2; + const unsigned ckv = static_cast(n2b << 4); + ldmatrix_x4_t(vf[nxt][0], vf[nxt][1], vf[nxt][2], vf[nxt][3], + gqa_prefill_swz_addr( + v_lane_base + static_cast(k2 * 8192), ckv, v_as, + v_r)); + } + const int pk = half * PVKs + k; + mma_bf16(acc[n2][0], acc[n2][1], acc[n2][2], acc[n2][3], p_frag[pk][0], + p_frag[pk][1], p_frag[pk][2], p_frag[pk][3], vf[cur][0], vf[cur][1]); + mma_bf16(acc[n2 + 1][0], acc[n2 + 1][1], acc[n2 + 1][2], acc[n2 + 1][3], + p_frag[pk][0], p_frag[pk][1], p_frag[pk][2], p_frag[pk][3], vf[cur][2], + vf[cur][3]); + } + } + gqa_prefill_bar_sync(0, Threads); // both halves consumed; buffers reusable + } + + l0 = warp_sum<4>(l0, FullMask); + l1 = warp_sum<4>(l1, FullMask); + + const float inv_l0 = (l0 > 0.0f) ? __frcp_rn(l0) : 0.0f; + const float inv_l1 = (l1 > 0.0f) ? __frcp_rn(l1) : 0.0f; +#pragma unroll + for (int n = 0; n < PVNt; ++n) { + const int d0 = n * 8 + 2 * lid; + const int qrow0 = q0 + warp_row0 + gid; + const int qrow1 = q0 + warp_row0 + gid + 8; + if (qrow0 < tokens) { + *reinterpret_cast(&out[gqa_prefill_q_index(q_head, d0, qrow0)]) = + pack_bf16x2(acc[n][0] * inv_l0, acc[n][1] * inv_l0); + } + if (qrow1 < tokens) { + *reinterpret_cast(&out[gqa_prefill_q_index(q_head, d0, qrow1)]) = + pack_bf16x2(acc[n][2] * inv_l1, acc[n][3] * inv_l1); + } + } + gqa_prefill_zero_output_rows(out, q_head, tokens, min(q0 + Br, width), tid, + ProducerThreads); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_isoquant_rot.cu b/src/ops/kernel/gqa_isoquant_rot.cu new file mode 100644 index 0000000000..f2e80835b0 --- /dev/null +++ b/src/ops/kernel/gqa_isoquant_rot.cu @@ -0,0 +1,73 @@ +#include "ops/kernel/gqa_isoquant_rot.cuh" + +// Baked IsoQuant per-4-channel SO(4) rotations, [64][4][4] fp32, imported +// from the nvfp4rtx offline calibration (isoquant_rot.npy). Kept in constant +// memory so the decode/prefill kernels index it with LDC instead of expanding +// the 1024-float table into per-thread unrolled code or local-memory spills. + +__constant__ float kGqaIsoquantRotDev[64][4][4] = { + {{0.40011855959892273f, 0.2743317782878876f, -0.8269611597061157f, -0.28422266244888306f}, {-0.7859097123146057f, 0.508637547492981f, -0.28316614031791687f, 0.20844843983650208f}, {-0.269147127866745f, -0.7915264964103699f, -0.48255136609077454f, 0.26113179326057434f}, {-0.3870541453361511f, -0.19878575205802917f, 0.055645596235990524f, -0.8986528515815735f}}, + {{0.6970387697219849f, -0.3005750775337219f, 0.6160241961479187f, 0.21048936247825623f}, {0.6581977605819702f, 0.19192638993263245f, -0.45768246054649353f, -0.5660978555679321f}, {-0.26886531710624695f, -0.5661023855209351f, 0.27689507603645325f, -0.7284014821052551f}, {-0.09286632388830185f, 0.7432005405426025f, 0.5782474875450134f, -0.32350999116897583f}}, + {{-0.39023974537849426f, -0.6038582921028137f, -0.5995467901229858f, 0.3515847623348236f}, {-0.6325834393501282f, 0.06928431242704391f, -0.10609539598226547f, -0.7640560865402222f}, {-0.6551622748374939f, 0.13101159036159515f, 0.572891354560852f, 0.4747566878795624f}, {0.1353275030851364f, -0.7831927537918091f, 0.5487103462219238f, -0.25925391912460327f}}, + {{-0.5436534285545349f, 0.5077739953994751f, 0.4645419418811798f, -0.4804242253303528f}, {0.35229524970054626f, 0.8597108125686646f, -0.2198578417301178f, 0.29740190505981445f}, {-0.4043574631214142f, 0.054877232760190964f, -0.8574345111846924f, -0.3135118782520294f}, {-0.6456191539764404f, 0.007169822696596384f, 0.02587495744228363f, 0.7631874084472656f}}, + {{-0.5556737780570984f, -0.743339478969574f, 0.3603149354457855f, 0.09405422955751419f}, {-0.627226710319519f, 0.30957627296447754f, -0.14602144062519073f, -0.6995905041694641f}, {0.2807663679122925f, 0.16815322637557983f, 0.8737923502922058f, -0.359696626663208f}, {0.4679567217826843f, -0.5686241388320923f, -0.2921263575553894f, -0.6102009415626526f}}, + {{-0.509796142578125f, -0.4665975868701935f, -0.3138296902179718f, -0.6510802507400513f}, {0.6973025798797607f, -0.5804732441902161f, 0.3129638433456421f, -0.2808443009853363f}, {-0.2879975140094757f, 0.28603988885879517f, 0.831121027469635f, -0.3801005482673645f}, {-0.41344210505485535f, -0.6029251217842102f, 0.33586061000823975f, 0.593923032283783f}}, + {{0.19919353723526f, -0.23175674676895142f, 0.7625843286514282f, -0.5701542496681213f}, {-0.758425235748291f, 0.5453514456748962f, 0.10998529940843582f, -0.3395382761955261f}, {-0.020229844376444817f, 0.291867733001709f, 0.631108820438385f, 0.7184049487113953f}, {0.6202450394630432f, 0.7507955431938171f, -0.08983386307954788f, -0.2086436152458191f}}, + {{0.8120933771133423f, 0.2649351954460144f, 0.4233261048793793f, -0.30184227228164673f}, {-0.04930197447538376f, 0.8632810115814209f, -0.4971083104610443f, -0.07210122793912888f}, {-0.45908311009407043f, 0.4249476492404938f, 0.7549713850021362f, 0.1966734379529953f}, {0.35681429505348206f, 0.06304576992988586f, -0.060799501836299896f, 0.9300603270530701f}}, + {{-0.4711749851703644f, 0.45827749371528625f, 0.24273380637168884f, 0.7134817242622375f}, {-0.8118565082550049f, -0.5581576824188232f, -0.08512936532497406f, -0.14866754412651062f}, {-0.03803643584251404f, 0.14695662260055542f, -0.9660311341285706f, 0.20914295315742493f}, {-0.34268996119499207f, 0.6759034395217896f, -0.024841368198394775f, -0.6519977450370789f}}, + {{0.0064468905329704285f, 0.4147374629974365f, -0.40165019035339355f, -0.816473126411438f}, {-0.5916351079940796f, -0.3300222158432007f, -0.7135268449783325f, 0.1786971241235733f}, {0.8050512075424194f, -0.28810644149780273f, -0.5068801641464233f, 0.10936067253351212f}, {-0.04264846071600914f, -0.7975417375564575f, 0.269497811794281f, -0.5380327105522156f}}, + {{-0.17752645909786224f, -0.9574539065361023f, -0.016427865251898766f, 0.22692839801311493f}, {-0.7160086631774902f, 0.030565226450562477f, 0.578667402267456f, -0.3892832100391388f}, {0.5269463658332825f, -0.28687041997909546f, 0.13691163063049316f, -0.7882183194160461f}, {-0.4220705032348633f, -0.007290000561624765f, -0.8038217425346375f, -0.41913485527038574f}}, + {{-0.20524531602859497f, -0.9146785736083984f, 0.2534736394882202f, -0.23872268199920654f}, {-0.9292886853218079f, 0.19693699479103088f, -0.23537151515483856f, -0.20552031695842743f}, {-0.25709474086761475f, 0.20570680499076843f, 0.8729984164237976f, 0.35980647802352905f}, {0.16792608797550201f, 0.2868162989616394f, 0.34383872151374817f, -0.8782437443733215f}}, + {{-0.050552498549222946f, 0.9480844140052795f, -0.30942896008491516f, 0.05323593690991402f}, {0.5954673290252686f, 0.2787375748157501f, 0.7168259620666504f, -0.23213037848472595f}, {0.6993109583854675f, -0.10787669569253922f, -0.33801984786987305f, 0.620539665222168f}, {-0.3922083079814911f, 0.10864567011594772f, 0.5255061984062195f, 0.7471358776092529f}}, + {{-0.5089328289031982f, -0.6657191514968872f, -0.2436031848192215f, -0.4883265793323517f}, {-0.7951285243034363f, 0.18664883077144623f, 0.4642583727836609f, 0.34263235330581665f}, {0.1949520856142044f, -0.6916254162788391f, 0.10452544689178467f, 0.6875481009483337f}, {0.26599180698394775f, -0.20888875424861908f, 0.8451011776924133f, -0.41402629017829895f}}, + {{-0.1843663603067398f, 0.09888805449008942f, -0.37987837195396423f, 0.9010674953460693f}, {0.6574713587760925f, -0.6542134284973145f, 0.22272230684757233f, 0.30021822452545166f}, {0.09052873402833939f, 0.5035298466682434f, 0.8042736649513245f, 0.3023342490196228f}, {0.7249448299407959f, 0.5555930733680725f, -0.39903756976127625f, -0.0808727890253067f}}, + {{0.4851854145526886f, -0.37755826115608215f, 0.5057452321052551f, 0.6051996350288391f}, {-0.21739646792411804f, 0.4551687240600586f, 0.8306167721748352f, -0.23587290942668915f}, {-0.30295515060424805f, 0.5666272044181824f, -0.17816419899463654f, 0.7452579140663147f}, {0.7909185290336609f, 0.5737637877464294f, -0.15018340945243835f, -0.15062524378299713f}}, + {{0.514838695526123f, 0.11479361355304718f, 0.48298582434654236f, 0.6989193558692932f}, {0.797002911567688f, 0.2963114082813263f, -0.24913913011550903f, -0.4635898470878601f}, {0.08698800951242447f, -0.40907612442970276f, 0.7487267255783081f, -0.5142937302589417f}, {-0.30357953906059265f, 0.8553821444511414f, 0.3795558512210846f, -0.17915944755077362f}}, + {{-0.037144020199775696f, 0.6513643860816956f, -0.6793128848075867f, 0.3359743058681488f}, {0.15698249638080597f, -0.6506251096725464f, -0.721064567565918f, -0.17919059097766876f}, {-0.15910586714744568f, 0.34335458278656006f, -0.11624587327241898f, -0.9183026552200317f}, {-0.9739928841590881f, -0.1857927143573761f, -0.07132159173488617f, 0.10831507295370102f}}, + {{0.004005261231213808f, 0.33030256628990173f, -0.830751359462738f, 0.44803622364997864f}, {0.30346882343292236f, 0.3668804168701172f, 0.5269965529441833f, 0.7039744853973389f}, {0.5612061023712158f, 0.6266392469406128f, -0.039000727236270905f, -0.5393050312995911f}, {0.770024836063385f, -0.6030110716819763f, -0.17494526505470276f, 0.11328531056642532f}}, + {{0.5699678659439087f, -0.8000922203063965f, -0.17124466598033905f, 0.07526163011789322f}, {0.10200989991426468f, 0.17123998701572418f, -0.7419055104255676f, -0.6401929259300232f}, {0.08273337036371231f, -0.14731809496879578f, 0.6308424472808838f, -0.7572914361953735f}, {0.8111016154289246f, 0.5557217597961426f, 0.14929568767547607f, 0.10487289726734161f}}, + {{-0.10766172409057617f, -0.8902167677879333f, -0.43335890769958496f, 0.09012796729803085f}, {-0.4094999134540558f, -0.03821725398302078f, -0.00932039599865675f, -0.9114616513252258f}, {0.8559963703155518f, 0.026778768748044968f, -0.34714800119400024f, -0.38215354084968567f}, {-0.29662927985191345f, 0.4531405568122864f, -0.8316258192062378f, 0.12277308106422424f}}, + {{-0.3322691023349762f, 0.6429264545440674f, -0.6832327842712402f, 0.09713833034038544f}, {0.09777072817087173f, -0.3997913897037506f, -0.5292424559593201f, -0.7419637441635132f}, {0.9344594478607178f, 0.2159302532672882f, -0.22724324464797974f, 0.16887952387332916f}, {0.082606241106987f, 0.6165927648544312f, 0.448838472366333f, -0.6415088772773743f}}, + {{0.3697178065776825f, 0.8421379327774048f, 0.21754233539104462f, -0.3267841041088104f}, {-0.06914354115724564f, -0.39320653676986694f, 0.40148642659187317f, -0.8242672681808472f}, {-0.17402398586273193f, 0.017619801685214043f, 0.8827124238014221f, 0.4361467957496643f}, {-0.9100789427757263f, 0.3686216473579407f, -0.11091792583465576f, -0.15353083610534668f}}, + {{0.16563372313976288f, 0.9412461519241333f, -0.07188396155834198f, -0.28540104627609253f}, {0.0895087942481041f, -0.29886436462402344f, -0.5003072023391724f, -0.8076886534690857f}, {-0.4804794192314148f, -0.010906978510320187f, 0.7226231694221497f, -0.49682629108428955f}, {-0.8565589189529419f, 0.15689720213413239f, -0.4715307354927063f, 0.13910022377967834f}}, + {{0.14230002462863922f, 0.1774873435497284f, -0.7592212557792664f, -0.6097803115844727f}, {0.27149054408073425f, 0.24967645108699799f, 0.646009624004364f, -0.6683009266853333f}, {0.2917814552783966f, 0.8713449835777283f, -0.05532146617770195f, 0.3905905783176422f}, {-0.9060392379760742f, 0.3832985758781433f, 0.05651690810918808f, -0.1702377051115036f}}, + {{0.7207198739051819f, -0.04105044901371002f, 0.6009096503257751f, 0.34319862723350525f}, {0.03343043476343155f, 0.4939817190170288f, 0.4260944426059723f, -0.757171094417572f}, {-0.360859215259552f, -0.7508968114852905f, 0.5073412656784058f, -0.22031699120998383f}, {-0.5909533500671387f, 0.4364068806171417f, 0.4471644163131714f, 0.5102618336677551f}}, + {{0.5800289511680603f, -0.5015589594841003f, 0.3641371726989746f, 0.5285916328430176f}, {-0.07642816007137299f, 0.15588045120239258f, 0.903907299041748f, -0.3909113109111786f}, {-0.47217297554016113f, 0.4043007493019104f, 0.215984046459198f, 0.7529571652412415f}, {0.659376859664917f, 0.748786211013794f, -0.060882192105054855f, 0.028892314061522484f}}, + {{0.4573339819908142f, -0.18032628297805786f, -0.624589741230011f, -0.6068077683448792f}, {-0.8840721845626831f, -0.1766463965177536f, -0.3523246943950653f, -0.2511567771434784f}, {0.07697128504514694f, -0.9675224423408508f, 0.17139843106269836f, 0.16911055147647858f}, {-0.05777020752429962f, -0.013374172151088715f, 0.6755571961402893f, -0.7349191904067993f}}, + {{0.2866247296333313f, 0.6034632325172424f, 0.12626126408576965f, 0.7333051562309265f}, {-0.2981235086917877f, -0.5199616551399231f, -0.4944000542163849f, 0.6295481324195862f}, {-0.8759094476699829f, 0.2140449583530426f, 0.42216038703918457f, 0.09353067725896835f}, {0.24849799275398254f, -0.5653819441795349f, 0.7492712140083313f, 0.23913322389125824f}}, + {{-0.12438716739416122f, -0.5381945967674255f, 0.4488855004310608f, 0.7024074196815491f}, {-0.7343614101409912f, -0.2457694411277771f, 0.3383850157260895f, -0.5346085429191589f}, {0.6123356819152832f, -0.055865075439214706f, 0.6926594376564026f, -0.377023845911026f}, {0.2651154398918152f, -0.804253101348877f, -0.45190736651420593f, -0.28048256039619446f}}, + {{-0.3403027057647705f, -0.8510639071464539f, 0.17662061750888824f, 0.3587331473827362f}, {0.26994213461875916f, -0.4939577579498291f, -0.2822527587413788f, -0.7768335342407227f}, {0.8329964280128479f, -0.14526806771755219f, 0.49404382705688477f, 0.20232374966144562f}, {0.3426985442638397f, -0.10292302817106247f, -0.8031558394432068f, 0.4763457775115967f}}, + {{0.8841751217842102f, -0.10138296335935593f, 0.0003210993600077927f, 0.45602157711982727f}, {0.021841583773493767f, -0.9539779424667358f, 0.1570170819759369f, -0.2545478641986847f}, {-0.3372483551502228f, -0.06492588669061661f, 0.68830806016922f, 0.6389680504798889f}, {0.32252252101898193f, 0.27464917302131653f, 0.7082213759422302f, -0.5647738575935364f}}, + {{0.8313266634941101f, 0.3362404406070709f, -0.3452971577644348f, -0.27678182721138f}, {0.46811696887016296f, -0.8498848676681519f, 0.22173863649368286f, 0.09692369401454926f}, {0.141335129737854f, 0.2684270143508911f, 0.8849244117736816f, -0.35338374972343445f}, {-0.2641719877719879f, -0.30427786707878113f, -0.22025121748447418f, -0.8883228302001953f}}, + {{0.3314296305179596f, 0.5607637763023376f, -0.5877252817153931f, -0.4798722565174103f}, {-0.7399804592132568f, 0.2261389195919037f, -0.5092035531997681f, 0.3768312335014343f}, {-0.15107771754264832f, -0.7607328295707703f, -0.44765105843544006f, -0.44505009055137634f}, {-0.5654721856117249f, 0.2359888255596161f, 0.44147390127182007f, -0.6554778814315796f}}, + {{0.3617727756500244f, 0.19202247262001038f, 0.7681522965431213f, -0.492127925157547f}, {0.38843029737472534f, -0.7917449474334717f, -0.24360811710357666f, -0.40362977981567383f}, {0.6782290935516357f, -0.07938753813505173f, 0.15745967626571655f, 0.7133788466453552f}, {-0.5081807971000671f, -0.5744258165359497f, 0.5707921385765076f, 0.2932299077510834f}}, + {{0.24152758717536926f, 0.08475516736507416f, 0.7612728476524353f, -0.5957722663879395f}, {-0.544460117816925f, 0.7139983177185059f, 0.32532045245170593f, 0.29654040932655334f}, {-0.6984413266181946f, -0.6566329598426819f, 0.28431829810142517f, -0.013263558968901634f}, {0.39674586057662964f, -0.22771935164928436f, 0.4835217595100403f, 0.7462863326072693f}}, + {{0.36633363366127014f, 0.1930859535932541f, -0.2025059014558792f, -0.8874169588088989f}, {-0.2520327866077423f, 0.7200138568878174f, 0.639796793460846f, -0.09337899833917618f}, {-0.8764473795890808f, 0.014103920198976994f, -0.4001584053039551f, -0.26742157340049744f}, {-0.18471364676952362f, -0.6664075255393982f, 0.6241191625595093f, -0.3636718690395355f}}, + {{0.24664390087127686f, 0.6567859649658203f, -0.15260367095470428f, -0.6960683465003967f}, {0.7227051258087158f, 0.35648679733276367f, 0.0014858359936624765f, 0.5921252369880676f}, {0.5884508490562439f, -0.6492829918861389f, -0.3543994128704071f, -0.3264327645301819f}, {-0.2656872570514679f, 0.14135418832302094f, -0.9225568175315857f, 0.2414918690919876f}}, + {{0.07773066312074661f, -0.4016209542751312f, 0.059427693486213684f, -0.910564124584198f}, {-0.19501110911369324f, 0.48705509305000305f, -0.8026052713394165f, -0.28385356068611145f}, {-0.8934208154678345f, 0.1926887333393097f, 0.3822169005870819f, -0.13631084561347961f}, {0.39714980125427246f, 0.7512317299842834f, 0.454096257686615f, -0.26780521869659424f}}, + {{0.2560746371746063f, 0.3309951722621918f, -0.7843618392944336f, -0.45786938071250916f}, {-0.5871301889419556f, -0.6693162322044373f, -0.45398569107055664f, -0.0345088467001915f}, {0.5124291181564331f, -0.5904866456985474f, 0.25122326612472534f, -0.57063889503479f}, {-0.5719442367553711f, 0.30624085664749146f, 0.3399415910243988f, -0.6808347105979919f}}, + {{-0.6286505460739136f, 0.7705990672111511f, -0.07695285230875015f, 0.07109072059392929f}, {0.15192918479442596f, 0.20126107335090637f, 0.9490137100219727f, 0.1891680210828781f}, {-0.7478232383728027f, -0.5623191595077515f, 0.2814255356788635f, -0.21297234296798706f}, {0.1499214470386505f, 0.22241461277008057f, 0.11937737464904785f, -0.9559311270713806f}}, + {{0.48543208837509155f, 0.6784539818763733f, 0.40504834055900574f, 0.37415480613708496f}, {-0.6116956472396851f, -0.17297720909118652f, 0.431648850440979f, 0.6399894952774048f}, {0.6132947206497192f, -0.7136677503585815f, 0.25754839181900024f, 0.21958334743976593f}, {0.11854811012744904f, 0.021391404792666435f, -0.7637303471565247f, 0.6341961622238159f}}, + {{-0.7380954027175903f, 0.20134522020816803f, -0.3402610421180725f, -0.5467154383659363f}, {-0.21427154541015625f, -0.8474301695823669f, -0.42230528593063354f, 0.24001680314540863f}, {-0.5666976571083069f, 0.27237188816070557f, 0.1719779223203659f, 0.7583475112915039f}, {0.29691195487976074f, 0.4088224768638611f, -0.8223772048950195f, 0.26154014468193054f}}, + {{-0.8659243583679199f, -0.2540542185306549f, 0.03739004209637642f, -0.42922425270080566f}, {0.145774245262146f, -0.2246638536453247f, -0.9324949383735657f, -0.24234113097190857f}, {-0.05854787677526474f, -0.8011570572853088f, 0.029272424057126045f, 0.5948635339736938f}, {-0.47486525774002075f, 0.49308210611343384f, -0.3580479323863983f, 0.634960412979126f}}, + {{-0.2489374727010727f, 0.536086916923523f, 0.4625909924507141f, -0.6607953906059265f}, {-0.15749551355838776f, 0.4876924157142639f, 0.41996830701828003f, 0.748984694480896f}, {0.8050452470779419f, 0.5371774435043335f, -0.24827060103416443f, -0.04128335043787956f}, {-0.5149053931236267f, 0.43151751160621643f, -0.7402688264846802f, 0.025830106809735298f}}, + {{-0.22975221276283264f, -0.884314239025116f, 0.4064039885997772f, -0.006176777184009552f}, {-0.6269176006317139f, -0.1233624592423439f, -0.6158393025398254f, 0.4609750211238861f}, {0.6806378364562988f, -0.24465379118919373f, -0.1372832953929901f, 0.6767792105674744f}, {-0.3015301525592804f, 0.37804052233695984f, 0.660856306552887f, 0.5739632844924927f}}, + {{-0.0661751925945282f, 0.8148077130317688f, -0.3435314893722534f, 0.46227195858955383f}, {0.4142150580883026f, -0.5014222264289856f, -0.5022196769714355f, 0.569892168045044f}, {-0.7750697731971741f, -0.22520144283771515f, -0.573495090007782f, -0.14019477367401123f}, {-0.4725606441497803f, -0.18425112962722778f, 0.5485116243362427f, 0.6647352576255798f}}, + {{0.8926838636398315f, 0.4369777739048004f, -0.06439677625894547f, 0.08954917639493942f}, {-0.015395847149193287f, 0.03848566487431526f, 0.8271288275718689f, 0.5604817271232605f}, {-0.436659574508667f, 0.8983010053634644f, -0.019634172320365906f, -0.04470168799161911f}, {0.11048507690429688f, 0.024992113932967186f, 0.5579655170440674f, -0.8220967054367065f}}, + {{0.4111645519733429f, 0.5742372870445251f, -0.6091501712799072f, -0.3607370853424072f}, {-0.16340398788452148f, 0.8090202808380127f, 0.5140188932418823f, 0.2336021512746811f}, {0.6687555909156799f, -0.05436323955655098f, -0.0383717305958271f, 0.7404986023902893f}, {-0.5975021123886108f, 0.11305927485227585f, -0.6027007699012756f, 0.5166822671890259f}}, + {{-0.4254912734031677f, -0.46468761563301086f, -0.6893864274024963f, -0.3574479818344116f}, {-0.6903859972953796f, 0.7193441390991211f, -0.020326271653175354f, -0.07414913177490234f}, {-0.5552057027816772f, -0.5084295272827148f, 0.6557233929634094f, 0.05720916762948036f}, {0.18458342552185059f, 0.09004925191402435f, 0.30718088150024414f, -0.929225504398346f}}, + {{0.00012033561506541446f, -0.20229323208332062f, -0.4953382909297943f, 0.8448179364204407f}, {-0.004171979147940874f, -0.0012389495968818665f, -0.862515926361084f, -0.5060111284255981f}, {-0.3069232702255249f, -0.9320614337921143f, 0.09950512647628784f, -0.16479773819446564f}, {0.9517250657081604f, -0.3005617558956146f, 0.028371267020702362f, -0.05547083914279938f}}, + {{0.2486010044813156f, 0.34083130955696106f, -0.20683585107326508f, -0.8827515840530396f}, {-0.23261184990406036f, -0.1264577955007553f, -0.9580034017562866f, 0.11013420671224594f}, {-0.9059162139892578f, 0.36431652307510376f, 0.1545524299144745f, -0.15067453682422638f}, {0.25180360674858093f, 0.857388973236084f, -0.12474718689918518f, 0.4311811327934265f}}, + {{0.3351062536239624f, -0.6763542294502258f, 0.6543260216712952f, -0.045893166214227676f}, {-0.8052083253860474f, -0.013644657097756863f, 0.36553966999053955f, -0.46672701835632324f}, {0.026310760527849197f, -0.5653808116912842f, -0.6347837448120117f, -0.526024580001831f}, {-0.4885193109512329f, -0.47191449999809265f, -0.18785029649734497f, 0.7094771862030029f}}, + {{-0.056359559297561646f, -0.041591089218854904f, -0.31387072801589966f, -0.9468784928321838f}, {0.24468490481376648f, -0.3629721403121948f, 0.8538455367088318f, -0.28165286779403687f}, {0.6960977911949158f, 0.7080896496772766f, 0.06995557248592377f, -0.09572399407625198f}, {-0.6726073622703552f, 0.604260265827179f, 0.4093155264854431f, -0.12218688428401947f}}, + {{0.8309203386306763f, 0.14242295920848846f, -0.519000768661499f, 0.141157329082489f}, {0.005748756695538759f, 0.6613417267799377f, 0.3683887720108032f, 0.6533633470535278f}, {0.51996248960495f, 0.04490722715854645f, 0.7206618785858154f, -0.45636463165283203f}, {0.1979326456785202f, -0.7350687980651855f, 0.2749078869819641f, 0.5873007774353027f}}, + {{-0.8342257142066956f, 0.0786316841840744f, -0.47402670979499817f, -0.2705240547657013f}, {-0.4115997552871704f, -0.7421025633811951f, 0.49551454186439514f, 0.18529656529426575f}, {0.00887125264853239f, 0.11009912192821503f, 0.49462321400642395f, -0.8620599508285522f}, {-0.36684393882751465f, 0.6564899682998657f, 0.5339587330818176f, 0.3864383101463318f}}, + {{0.6240546107292175f, 0.5411279201507568f, 0.22041618824005127f, -0.5187996625900269f}, {0.24760562181472778f, 0.17516127228736877f, -0.9497778415679932f, 0.07701948285102844f}, {-0.2512492537498474f, 0.77826327085495f, 0.1236075684428215f, 0.5620509386062622f}, {0.6972238421440125f, -0.2660927474498749f, 0.18455323576927185f, 0.639541745185852f}}, + {{-0.3337004482746124f, 0.17703938484191895f, -0.5825225114822388f, -0.7197003960609436f}, {-0.33623188734054565f, -0.8776010274887085f, -0.2924564480781555f, 0.17673082649707794f}, {-0.3158700466156006f, 0.42833173274993896f, -0.5170906186103821f, 0.6703546047210693f}, {0.8220816850662231f, -0.12249655276536942f, -0.5547558665275574f, 0.037713006138801575f}}, + {{-0.0913948193192482f, 0.505171537399292f, 0.4471118748188019f, 0.732488751411438f}, {-0.15544907748699188f, 0.12992462515830994f, -0.8805432319641113f, 0.42848432064056396f}, {-0.9213164448738098f, -0.361056923866272f, 0.13461901247501373f, 0.05188114568591118f}, {0.3444685935974121f, -0.773019552230835f, 0.08131596446037292f, 0.5264692902565002f}}, + {{0.5060872435569763f, -0.28217458724975586f, -0.6208304166793823f, 0.5280367732048035f}, {-0.7865853309631348f, -0.5361727476119995f, -0.200857475399971f, 0.23121120035648346f}, {-0.07956269383430481f, 0.2805559039115906f, 0.5007839798927307f, 0.814968466758728f}, {0.3447159230709076f, -0.7444358468055725f, 0.5687190294265747f, -0.05953970178961754f}}, + {{-0.44430842995643616f, -0.04455866292119026f, 0.36634108424186707f, 0.8163325190544128f}, {0.41614630818367004f, -0.025297733023762703f, 0.8919123411178589f, -0.17514196038246155f}, {-0.7912799715995789f, -0.06052520126104355f, 0.25939929485321045f, -0.5503858923912048f}, {-0.057343218475580215f, 0.9968507289886475f, 0.054759681224823f, -0.0013725582975894213f}}, + {{0.0926179364323616f, 0.5333411693572998f, 0.28003251552581787f, -0.792811930179596f}, {0.40545332431793213f, -0.5204694271087646f, -0.5603758692741394f, -0.5006975531578064f}, {-0.8662747740745544f, 0.026296762749552727f, -0.43827271461486816f, -0.23831385374069214f}, {0.27676257491111755f, 0.666308581829071f, -0.6445755362510681f, 0.2528984546661377f}}, + {{0.4133192002773285f, 0.38275083899497986f, 0.8261796236038208f, -0.00980697013437748f}, {-0.584749698638916f, 0.5674367547035217f, 0.03652407228946686f, 0.5785752534866333f}, {-0.35834357142448425f, -0.7157992720603943f, 0.5145338177680969f, 0.3073699474334717f}, {0.5990199446678162f, -0.13837909698486328f, -0.22660110890865326f, 0.7554324865341187f}}, + {{0.6994558572769165f, -0.6821857690811157f, -0.21111047267913818f, -0.028574516996741295f}, {-0.38532692193984985f, -0.4039255678653717f, -0.08315643668174744f, 0.8255012631416321f}, {-0.6019008755683899f, -0.5340152382850647f, -0.19258402287960052f, -0.5616534352302551f}, {0.0003105102223344147f, 0.293759286403656f, -0.954687774181366f, 0.047714173793792725f}} +}; diff --git a/src/ops/kernel/gqa_isoquant_rot.cuh b/src/ops/kernel/gqa_isoquant_rot.cuh new file mode 100644 index 0000000000..2ffd3533ee --- /dev/null +++ b/src/ops/kernel/gqa_isoquant_rot.cuh @@ -0,0 +1,19 @@ +#pragma once + +// Baked IsoQuant per-4-channel SO(4) rotations, [64][4][4] fp32, +// imported from the nvfp4rtx offline calibration (isoquant_rot.npy). +// The table lives in constant memory (gqa_isoquant_rot.cu) so kernels index +// it with LDC; a function-local constexpr copy previously made nvcc expand +// the whole table into the hot Q/K quantization loops and spill it through +// the per-thread stack. Applied to K on cache write and to Q before NVFP4 +// quantization so QK^T is preserved in the rotated domain. + +extern __constant__ float kGqaIsoquantRotDev[64][4][4]; + +namespace ninfer::ops { + +__device__ __forceinline__ float gqa_isoquant_rot_value(int block, int row, int col) { + return ::kGqaIsoquantRotDev[block][row][col]; +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/gqa_isoquant_row_scale.cu b/src/ops/kernel/gqa_isoquant_row_scale.cu new file mode 100644 index 0000000000..b989008402 --- /dev/null +++ b/src/ops/kernel/gqa_isoquant_row_scale.cu @@ -0,0 +1,102 @@ +#include "ops/kernel/gqa_isoquant_row_scale.cuh" + +// Sinkhorn-constrained per-(layer,kv_head,channel) row scales for the +// rotated NVFP4 K domain, calibrated from kvcalib-a. Bounds [0.5, 2.0]. +__constant__ unsigned short kGqaKvRowScaleDev[16][4][256] = { + { + {16128, 16128, 16128, 16268, 16128, 16290, 16128, 16128, 16384, 16128, 16295, 16340, 16384, 16299, 16284, 16128, 16190, 16384, 16384, 16384, 16128, 16384, 16384, 16140, 16155, 16384, 16384, 16152, 16158, 16384, 16384, 16157, 16234, 16146, 16128, 16128, 16269, 16128, 16384, 16175, 16128, 16384, 16185, 16128, 16182, 16128, 16128, 16128, 16384, 16158, 16134, 16128, 16128, 16128, 16273, 16148, 16384, 16384, 16128, 16128, 16384, 16128, 16301, 16276, 16129, 16296, 16147, 16208, 16273, 16332, 16128, 16384, 16128, 16128, 16384, 16384, 16348, 16384, 16128, 16134, 16128, 16134, 16184, 16238, 16384, 16158, 16183, 16384, 16128, 16384, 16128, 16161, 16384, 16128, 16206, 16128, 16370, 16384, 16384, 16301, 16128, 16128, 16384, 16384, 16137, 16128, 16179, 16384, 16384, 16128, 16135, 16128, 16187, 16260, 16384, 16128, 16128, 16208, 16384, 16128, 16128, 16128, 16384, 16225, 16128, 16384, 16134, 16384, 16227, 16227, 16384, 16265, 16365, 16279, 16155, 16128, 16282, 16384, 16167, 16148, 16204, 16384, 16291, 16379, 16128, 16196, 16211, 16162, 16288, 16266, 16216, 16128, 16128, 16128, 16376, 16128, 16290, 16384, 16264, 16197, 16225, 16128, 16283, 16200, 16128, 16384, 16201, 16128, 16353, 16331, 16128, 16384, 16196, 16234, 16366, 16384, 16150, 16159, 16287, 16179, 16128, 16384, 16303, 16185, 16128, 16384, 16384, 16128, 16162, 16384, 16163, 16384, 16163, 16259, 16384, 16219, 16128, 16384, 16202, 16178, 16129, 16384, 16128, 16128, 16173, 16128, 16267, 16328, 16353, 16320, 16384, 16208, 16384, 16172, 16158, 16170, 16191, 16384, 16128, 16384, 16279, 16352, 16141, 16159, 16128, 16384, 16384, 16158, 16310, 16232, 16240, 16265, 16333, 16288, 16303, 16267, 16136, 16192, 16128, 16381, 16184, 16318, 16384, 16171, 16384, 16241, 16144, 16128, 16128, 16323, 16223, 16384, 16220, 16218, 16146, 16130}, + {16128, 16128, 16128, 16128, 16230, 16226, 16128, 16384, 16198, 16181, 16148, 16309, 16128, 16384, 16253, 16144, 16128, 16219, 16128, 16384, 16338, 16136, 16128, 16156, 16172, 16384, 16235, 16167, 16160, 16216, 16128, 16167, 16128, 16297, 16128, 16128, 16230, 16159, 16384, 16291, 16364, 16170, 16384, 16128, 16384, 16282, 16144, 16384, 16384, 16384, 16171, 16128, 16384, 16264, 16128, 16270, 16384, 16310, 16128, 16214, 16145, 16384, 16128, 16128, 16299, 16207, 16128, 16377, 16366, 16304, 16153, 16310, 16128, 16128, 16384, 16384, 16177, 16222, 16187, 16247, 16128, 16193, 16374, 16384, 16384, 16333, 16384, 16384, 16128, 16272, 16158, 16384, 16128, 16128, 16284, 16190, 16336, 16128, 16264, 16356, 16128, 16303, 16128, 16371, 16363, 16384, 16160, 16295, 16243, 16384, 16318, 16207, 16384, 16162, 16201, 16196, 16204, 16200, 16313, 16384, 16128, 16128, 16128, 16235, 16384, 16384, 16128, 16371, 16158, 16167, 16197, 16128, 16128, 16384, 16297, 16128, 16238, 16128, 16128, 16128, 16234, 16384, 16332, 16384, 16128, 16187, 16299, 16270, 16384, 16191, 16345, 16326, 16384, 16240, 16152, 16257, 16384, 16289, 16170, 16269, 16189, 16384, 16153, 16209, 16128, 16279, 16128, 16295, 16128, 16128, 16204, 16135, 16384, 16180, 16128, 16384, 16191, 16128, 16384, 16275, 16128, 16384, 16169, 16128, 16162, 16274, 16128, 16288, 16384, 16184, 16128, 16357, 16384, 16214, 16128, 16166, 16303, 16128, 16384, 16384, 16128, 16128, 16210, 16252, 16384, 16345, 16384, 16276, 16128, 16384, 16171, 16293, 16202, 16128, 16128, 16239, 16128, 16130, 16218, 16208, 16223, 16314, 16223, 16142, 16384, 16264, 16128, 16313, 16260, 16358, 16306, 16223, 16128, 16128, 16252, 16128, 16128, 16255, 16277, 16128, 16149, 16384, 16301, 16384, 16382, 16204, 16182, 16187, 16261, 16165, 16322, 16128, 16255, 16156, 16128, 16292}, + {16128, 16128, 16128, 16258, 16384, 16128, 16128, 16211, 16384, 16128, 16384, 16160, 16384, 16373, 16294, 16128, 16136, 16384, 16384, 16128, 16345, 16128, 16128, 16161, 16156, 16254, 16128, 16128, 16156, 16358, 16384, 16150, 16128, 16384, 16128, 16128, 16300, 16229, 16128, 16128, 16235, 16384, 16384, 16318, 16128, 16384, 16233, 16128, 16165, 16133, 16164, 16255, 16128, 16384, 16189, 16128, 16299, 16128, 16156, 16306, 16266, 16128, 16264, 16269, 16128, 16333, 16384, 16320, 16148, 16166, 16264, 16137, 16345, 16128, 16384, 16302, 16160, 16246, 16384, 16341, 16384, 16270, 16384, 16256, 16384, 16161, 16223, 16384, 16258, 16235, 16384, 16355, 16128, 16128, 16297, 16217, 16128, 16270, 16384, 16175, 16384, 16128, 16384, 16156, 16294, 16128, 16384, 16128, 16258, 16384, 16346, 16242, 16297, 16131, 16360, 16273, 16257, 16384, 16144, 16141, 16128, 16128, 16344, 16181, 16384, 16384, 16128, 16355, 16128, 16128, 16131, 16384, 16266, 16128, 16238, 16384, 16158, 16128, 16272, 16212, 16384, 16145, 16384, 16146, 16219, 16384, 16128, 16138, 16158, 16183, 16143, 16188, 16384, 16384, 16147, 16197, 16128, 16175, 16128, 16128, 16330, 16128, 16128, 16182, 16384, 16214, 16271, 16321, 16128, 16128, 16173, 16128, 16169, 16144, 16244, 16267, 16136, 16384, 16215, 16255, 16178, 16211, 16384, 16384, 16269, 16294, 16192, 16378, 16368, 16369, 16181, 16261, 16128, 16356, 16259, 16335, 16155, 16384, 16166, 16243, 16384, 16384, 16268, 16384, 16154, 16384, 16384, 16130, 16384, 16256, 16184, 16128, 16201, 16153, 16384, 16331, 16218, 16384, 16164, 16211, 16146, 16384, 16161, 16384, 16172, 16384, 16384, 16204, 16128, 16286, 16257, 16128, 16131, 16128, 16248, 16128, 16128, 16202, 16128, 16244, 16260, 16128, 16237, 16128, 16128, 16384, 16194, 16319, 16128, 16318, 16299, 16231, 16128, 16128, 16384, 16384}, + {16128, 16128, 16128, 16128, 16384, 16162, 16128, 16314, 16377, 16128, 16315, 16283, 16128, 16128, 16129, 16163, 16257, 16128, 16384, 16152, 16384, 16178, 16128, 16384, 16384, 16128, 16130, 16128, 16198, 16384, 16128, 16384, 16134, 16384, 16130, 16134, 16179, 16272, 16384, 16274, 16280, 16384, 16286, 16263, 16281, 16384, 16152, 16128, 16151, 16128, 16163, 16222, 16128, 16128, 16156, 16384, 16384, 16351, 16128, 16177, 16247, 16128, 16197, 16236, 16187, 16128, 16182, 16128, 16384, 16128, 16384, 16128, 16384, 16384, 16384, 16139, 16216, 16128, 16384, 16161, 16384, 16128, 16311, 16308, 16384, 16299, 16151, 16128, 16157, 16384, 16128, 16128, 16128, 16305, 16384, 16196, 16128, 16294, 16299, 16128, 16216, 16384, 16161, 16384, 16296, 16265, 16376, 16384, 16248, 16174, 16304, 16251, 16354, 16128, 16384, 16384, 16128, 16145, 16276, 16384, 16128, 16384, 16128, 16215, 16128, 16185, 16384, 16384, 16128, 16128, 16128, 16384, 16189, 16128, 16384, 16128, 16142, 16128, 16255, 16196, 16128, 16152, 16128, 16196, 16190, 16128, 16128, 16128, 16246, 16270, 16200, 16128, 16128, 16146, 16128, 16128, 16384, 16384, 16370, 16384, 16384, 16191, 16384, 16152, 16384, 16128, 16128, 16128, 16384, 16279, 16128, 16384, 16128, 16384, 16128, 16330, 16321, 16245, 16128, 16195, 16128, 16384, 16165, 16128, 16164, 16281, 16128, 16214, 16128, 16222, 16179, 16259, 16193, 16145, 16141, 16210, 16384, 16281, 16215, 16384, 16128, 16384, 16128, 16128, 16288, 16384, 16384, 16152, 16128, 16198, 16128, 16142, 16208, 16384, 16129, 16384, 16384, 16384, 16134, 16263, 16212, 16384, 16128, 16384, 16289, 16139, 16128, 16128, 16128, 16339, 16249, 16296, 16384, 16248, 16244, 16181, 16305, 16250, 16128, 16194, 16156, 16330, 16384, 16167, 16384, 16319, 16128, 16309, 16384, 16168, 16128, 16213, 16178, 16273, 16183, 16128}, + }, + { + {16128, 16128, 16128, 16215, 16384, 16150, 16128, 16365, 16315, 16128, 16384, 16128, 16384, 16156, 16128, 16155, 16128, 16128, 16384, 16281, 16128, 16128, 16330, 16187, 16270, 16384, 16128, 16384, 16183, 16319, 16128, 16384, 16235, 16198, 16236, 16263, 16238, 16128, 16384, 16241, 16384, 16257, 16128, 16384, 16128, 16348, 16258, 16128, 16285, 16216, 16128, 16384, 16366, 16294, 16384, 16128, 16179, 16377, 16334, 16384, 16269, 16173, 16128, 16248, 16342, 16137, 16384, 16336, 16128, 16133, 16260, 16128, 16384, 16239, 16384, 16128, 16128, 16185, 16384, 16182, 16368, 16128, 16253, 16128, 16128, 16287, 16173, 16128, 16129, 16347, 16128, 16128, 16128, 16128, 16250, 16149, 16275, 16348, 16384, 16273, 16384, 16128, 16384, 16128, 16384, 16129, 16128, 16298, 16384, 16128, 16128, 16128, 16384, 16132, 16128, 16128, 16128, 16384, 16128, 16128, 16128, 16128, 16128, 16197, 16128, 16384, 16128, 16291, 16157, 16384, 16128, 16167, 16384, 16245, 16128, 16128, 16128, 16354, 16128, 16128, 16289, 16384, 16196, 16128, 16174, 16384, 16128, 16128, 16224, 16322, 16255, 16128, 16128, 16128, 16274, 16128, 16216, 16332, 16187, 16135, 16192, 16384, 16157, 16185, 16384, 16384, 16128, 16384, 16128, 16128, 16180, 16128, 16128, 16128, 16128, 16237, 16128, 16280, 16251, 16384, 16133, 16384, 16259, 16128, 16349, 16173, 16357, 16128, 16128, 16384, 16384, 16281, 16384, 16128, 16128, 16384, 16128, 16384, 16327, 16128, 16384, 16293, 16265, 16384, 16384, 16384, 16384, 16237, 16384, 16362, 16128, 16128, 16384, 16128, 16128, 16128, 16174, 16384, 16128, 16384, 16156, 16384, 16144, 16384, 16128, 16384, 16291, 16128, 16136, 16128, 16384, 16335, 16128, 16128, 16178, 16128, 16229, 16384, 16384, 16384, 16384, 16139, 16128, 16161, 16128, 16384, 16154, 16319, 16136, 16134, 16384, 16128, 16128, 16128, 16384, 16384}, + {16128, 16128, 16128, 16128, 16244, 16224, 16344, 16384, 16240, 16169, 16141, 16384, 16384, 16384, 16188, 16211, 16384, 16159, 16128, 16133, 16267, 16202, 16384, 16199, 16384, 16129, 16205, 16266, 16208, 16128, 16384, 16384, 16161, 16232, 16128, 16128, 16236, 16156, 16384, 16264, 16316, 16226, 16132, 16128, 16281, 16136, 16384, 16237, 16384, 16130, 16384, 16384, 16384, 16307, 16384, 16128, 16384, 16128, 16128, 16128, 16150, 16158, 16382, 16175, 16181, 16128, 16151, 16136, 16308, 16384, 16139, 16384, 16384, 16199, 16384, 16168, 16384, 16128, 16384, 16257, 16147, 16286, 16262, 16128, 16384, 16384, 16145, 16128, 16216, 16384, 16238, 16276, 16384, 16128, 16253, 16144, 16128, 16180, 16384, 16148, 16271, 16365, 16155, 16182, 16132, 16214, 16191, 16185, 16384, 16128, 16161, 16128, 16384, 16164, 16133, 16180, 16128, 16384, 16128, 16128, 16142, 16384, 16128, 16214, 16128, 16384, 16128, 16384, 16280, 16128, 16285, 16297, 16143, 16211, 16128, 16128, 16160, 16134, 16262, 16210, 16153, 16357, 16263, 16128, 16384, 16384, 16384, 16288, 16128, 16322, 16226, 16194, 16217, 16295, 16155, 16260, 16128, 16214, 16157, 16128, 16214, 16128, 16345, 16162, 16128, 16208, 16144, 16128, 16128, 16133, 16173, 16134, 16384, 16174, 16193, 16195, 16182, 16263, 16256, 16268, 16128, 16384, 16269, 16143, 16274, 16128, 16356, 16384, 16128, 16155, 16297, 16384, 16384, 16213, 16128, 16151, 16279, 16384, 16289, 16384, 16132, 16137, 16228, 16273, 16141, 16384, 16384, 16129, 16362, 16377, 16128, 16128, 16228, 16384, 16128, 16273, 16128, 16384, 16128, 16299, 16128, 16360, 16151, 16174, 16218, 16184, 16384, 16260, 16182, 16128, 16384, 16361, 16212, 16201, 16262, 16128, 16331, 16272, 16128, 16218, 16170, 16384, 16306, 16384, 16384, 16182, 16185, 16148, 16140, 16384, 16245, 16357, 16384, 16128, 16134, 16384}, + {16128, 16128, 16128, 16128, 16384, 16128, 16271, 16128, 16230, 16207, 16128, 16200, 16128, 16384, 16128, 16384, 16128, 16276, 16128, 16271, 16130, 16384, 16384, 16146, 16177, 16345, 16128, 16162, 16384, 16367, 16384, 16326, 16179, 16188, 16128, 16128, 16222, 16147, 16384, 16223, 16334, 16363, 16128, 16384, 16128, 16140, 16259, 16168, 16384, 16384, 16135, 16128, 16128, 16384, 16170, 16128, 16184, 16128, 16319, 16132, 16153, 16384, 16384, 16313, 16232, 16128, 16158, 16128, 16266, 16162, 16128, 16384, 16340, 16128, 16384, 16364, 16384, 16128, 16384, 16262, 16128, 16179, 16166, 16155, 16384, 16323, 16165, 16128, 16165, 16384, 16185, 16213, 16384, 16289, 16269, 16128, 16128, 16192, 16384, 16128, 16128, 16286, 16128, 16374, 16349, 16196, 16384, 16384, 16270, 16261, 16337, 16384, 16384, 16137, 16128, 16128, 16342, 16384, 16128, 16137, 16135, 16384, 16128, 16166, 16274, 16169, 16218, 16310, 16151, 16384, 16128, 16384, 16128, 16384, 16304, 16128, 16347, 16330, 16171, 16166, 16384, 16145, 16384, 16247, 16128, 16232, 16186, 16137, 16256, 16260, 16197, 16128, 16242, 16384, 16183, 16324, 16128, 16211, 16349, 16131, 16309, 16128, 16191, 16208, 16384, 16384, 16128, 16384, 16128, 16384, 16146, 16251, 16161, 16142, 16260, 16274, 16128, 16351, 16148, 16291, 16384, 16318, 16152, 16128, 16128, 16384, 16384, 16128, 16329, 16292, 16384, 16280, 16189, 16145, 16182, 16212, 16128, 16384, 16384, 16128, 16384, 16160, 16131, 16130, 16128, 16128, 16294, 16128, 16128, 16246, 16128, 16202, 16384, 16148, 16384, 16128, 16223, 16317, 16197, 16190, 16308, 16128, 16338, 16214, 16297, 16128, 16361, 16132, 16384, 16259, 16168, 16220, 16128, 16384, 16196, 16384, 16128, 16173, 16128, 16185, 16384, 16164, 16128, 16181, 16384, 16278, 16128, 16337, 16364, 16384, 16128, 16384, 16128, 16243, 16324, 16287}, + {16128, 16128, 16128, 16128, 16128, 16344, 16128, 16128, 16384, 16128, 16247, 16384, 16128, 16128, 16169, 16162, 16128, 16128, 16128, 16336, 16266, 16128, 16384, 16128, 16384, 16137, 16200, 16327, 16384, 16174, 16324, 16380, 16384, 16128, 16129, 16251, 16289, 16230, 16128, 16128, 16322, 16164, 16384, 16128, 16275, 16232, 16221, 16384, 16384, 16130, 16384, 16384, 16167, 16384, 16184, 16206, 16213, 16346, 16372, 16384, 16384, 16128, 16262, 16269, 16128, 16132, 16384, 16182, 16147, 16354, 16209, 16172, 16384, 16128, 16384, 16128, 16384, 16223, 16146, 16205, 16343, 16128, 16253, 16384, 16265, 16161, 16290, 16384, 16128, 16268, 16227, 16280, 16128, 16128, 16313, 16255, 16221, 16128, 16258, 16384, 16128, 16128, 16200, 16362, 16128, 16322, 16384, 16205, 16128, 16128, 16146, 16193, 16350, 16160, 16128, 16384, 16128, 16155, 16278, 16384, 16384, 16205, 16203, 16128, 16250, 16152, 16276, 16316, 16128, 16128, 16128, 16384, 16163, 16128, 16219, 16384, 16382, 16301, 16210, 16384, 16130, 16384, 16194, 16128, 16384, 16254, 16301, 16189, 16129, 16241, 16165, 16309, 16128, 16143, 16178, 16128, 16161, 16384, 16128, 16244, 16384, 16128, 16136, 16147, 16128, 16285, 16128, 16220, 16222, 16224, 16336, 16384, 16170, 16142, 16238, 16270, 16135, 16188, 16230, 16200, 16384, 16142, 16135, 16346, 16128, 16375, 16368, 16128, 16336, 16141, 16128, 16309, 16128, 16128, 16215, 16128, 16378, 16324, 16293, 16384, 16141, 16384, 16372, 16384, 16202, 16128, 16262, 16297, 16384, 16302, 16384, 16184, 16384, 16196, 16174, 16227, 16128, 16384, 16128, 16278, 16168, 16344, 16164, 16128, 16128, 16384, 16302, 16130, 16198, 16128, 16384, 16276, 16256, 16250, 16311, 16290, 16260, 16267, 16207, 16336, 16384, 16149, 16128, 16169, 16320, 16189, 16153, 16128, 16128, 16290, 16204, 16384, 16128, 16262, 16356, 16252}, + }, + { + {16128, 16128, 16128, 16181, 16283, 16198, 16182, 16210, 16261, 16216, 16196, 16192, 16273, 16345, 16187, 16201, 16252, 16384, 16380, 16195, 16307, 16136, 16291, 16217, 16358, 16384, 16159, 16363, 16336, 16231, 16191, 16384, 16207, 16233, 16206, 16180, 16307, 16190, 16226, 16180, 16187, 16303, 16209, 16174, 16202, 16291, 16261, 16186, 16384, 16250, 16267, 16185, 16255, 16337, 16225, 16209, 16384, 16259, 16196, 16244, 16275, 16303, 16287, 16265, 16334, 16174, 16278, 16170, 16305, 16230, 16271, 16225, 16267, 16152, 16384, 16197, 16310, 16202, 16264, 16272, 16239, 16260, 16377, 16225, 16182, 16319, 16260, 16274, 16248, 16384, 16245, 16265, 16207, 16248, 16384, 16246, 16188, 16177, 16341, 16271, 16218, 16263, 16259, 16307, 16256, 16178, 16263, 16303, 16273, 16275, 16248, 16230, 16383, 16249, 16257, 16230, 16162, 16368, 16348, 16271, 16211, 16254, 16200, 16316, 16177, 16297, 16238, 16384, 16227, 16239, 16149, 16382, 16299, 16212, 16284, 16379, 16325, 16201, 16269, 16227, 16171, 16261, 16275, 16206, 16223, 16294, 16309, 16297, 16213, 16333, 16270, 16226, 16272, 16301, 16271, 16261, 16156, 16384, 16284, 16177, 16251, 16257, 16350, 16281, 16324, 16226, 16215, 16207, 16202, 16265, 16170, 16285, 16203, 16223, 16241, 16346, 16255, 16214, 16283, 16212, 16137, 16384, 16333, 16195, 16188, 16322, 16311, 16166, 16198, 16226, 16166, 16363, 16222, 16262, 16195, 16318, 16227, 16266, 16254, 16244, 16267, 16254, 16228, 16213, 16273, 16221, 16384, 16273, 16170, 16384, 16232, 16208, 16343, 16295, 16229, 16271, 16213, 16349, 16132, 16384, 16226, 16266, 16206, 16384, 16284, 16181, 16253, 16306, 16273, 16253, 16278, 16276, 16242, 16169, 16344, 16357, 16194, 16334, 16190, 16251, 16308, 16275, 16221, 16229, 16197, 16317, 16273, 16258, 16204, 16320, 16274, 16245, 16286, 16273, 16270, 16271}, + {16128, 16128, 16128, 16200, 16290, 16214, 16187, 16223, 16264, 16224, 16198, 16189, 16290, 16308, 16193, 16235, 16183, 16278, 16277, 16233, 16279, 16238, 16182, 16187, 16367, 16206, 16249, 16209, 16305, 16202, 16230, 16318, 16232, 16258, 16224, 16196, 16319, 16205, 16233, 16186, 16194, 16307, 16213, 16176, 16215, 16276, 16283, 16169, 16384, 16384, 16172, 16199, 16262, 16310, 16296, 16328, 16295, 16216, 16286, 16342, 16317, 16161, 16289, 16307, 16249, 16152, 16299, 16230, 16342, 16175, 16324, 16184, 16249, 16174, 16384, 16270, 16195, 16301, 16285, 16290, 16225, 16180, 16320, 16232, 16265, 16340, 16324, 16307, 16196, 16384, 16213, 16274, 16253, 16196, 16384, 16267, 16347, 16208, 16371, 16206, 16256, 16172, 16257, 16305, 16215, 16308, 16233, 16297, 16234, 16216, 16257, 16192, 16384, 16242, 16193, 16223, 16272, 16354, 16264, 16213, 16361, 16338, 16199, 16270, 16244, 16328, 16185, 16384, 16260, 16272, 16260, 16379, 16278, 16229, 16235, 16203, 16289, 16196, 16260, 16224, 16196, 16273, 16291, 16220, 16233, 16246, 16263, 16269, 16255, 16357, 16239, 16212, 16230, 16248, 16254, 16220, 16176, 16331, 16280, 16234, 16284, 16326, 16218, 16293, 16128, 16320, 16239, 16135, 16207, 16268, 16207, 16271, 16222, 16336, 16203, 16373, 16249, 16205, 16277, 16312, 16269, 16319, 16301, 16192, 16243, 16279, 16257, 16257, 16250, 16303, 16206, 16384, 16286, 16273, 16180, 16275, 16147, 16384, 16313, 16162, 16294, 16266, 16282, 16301, 16272, 16135, 16359, 16296, 16172, 16384, 16252, 16245, 16310, 16200, 16248, 16274, 16232, 16224, 16340, 16342, 16201, 16282, 16238, 16314, 16218, 16285, 16233, 16271, 16237, 16258, 16247, 16195, 16291, 16158, 16384, 16275, 16175, 16384, 16260, 16171, 16329, 16260, 16220, 16270, 16240, 16322, 16250, 16232, 16176, 16384, 16189, 16241, 16230, 16272, 16198, 16293}, + {16128, 16128, 16128, 16194, 16291, 16213, 16192, 16226, 16265, 16233, 16212, 16203, 16264, 16274, 16233, 16214, 16360, 16323, 16176, 16129, 16267, 16128, 16202, 16322, 16280, 16246, 16243, 16184, 16287, 16211, 16219, 16297, 16227, 16252, 16223, 16195, 16318, 16206, 16237, 16191, 16203, 16316, 16219, 16187, 16265, 16241, 16225, 16161, 16384, 16261, 16279, 16223, 16225, 16384, 16292, 16158, 16320, 16230, 16257, 16247, 16247, 16209, 16260, 16223, 16291, 16153, 16264, 16222, 16325, 16229, 16275, 16230, 16277, 16219, 16384, 16187, 16237, 16165, 16384, 16306, 16270, 16258, 16322, 16231, 16170, 16384, 16281, 16305, 16169, 16384, 16216, 16277, 16262, 16149, 16384, 16259, 16253, 16234, 16347, 16174, 16220, 16283, 16236, 16344, 16301, 16209, 16237, 16217, 16275, 16280, 16384, 16222, 16355, 16227, 16288, 16219, 16154, 16384, 16215, 16151, 16259, 16264, 16203, 16281, 16199, 16336, 16189, 16384, 16275, 16202, 16256, 16282, 16304, 16256, 16212, 16208, 16283, 16291, 16287, 16262, 16350, 16262, 16384, 16262, 16239, 16287, 16223, 16249, 16193, 16384, 16224, 16209, 16270, 16349, 16223, 16225, 16192, 16289, 16237, 16267, 16262, 16250, 16245, 16285, 16267, 16257, 16235, 16247, 16289, 16267, 16265, 16317, 16220, 16234, 16159, 16330, 16235, 16301, 16265, 16334, 16311, 16305, 16262, 16209, 16260, 16329, 16178, 16276, 16260, 16264, 16161, 16384, 16250, 16262, 16210, 16302, 16159, 16228, 16213, 16157, 16278, 16274, 16246, 16246, 16231, 16217, 16384, 16223, 16241, 16384, 16224, 16172, 16384, 16206, 16269, 16210, 16280, 16232, 16260, 16384, 16285, 16216, 16151, 16333, 16258, 16331, 16186, 16345, 16279, 16222, 16256, 16339, 16220, 16178, 16358, 16247, 16185, 16384, 16262, 16244, 16299, 16243, 16295, 16257, 16233, 16353, 16233, 16201, 16299, 16275, 16297, 16152, 16280, 16272, 16209, 16287}, + {16128, 16128, 16128, 16177, 16287, 16205, 16185, 16220, 16258, 16211, 16194, 16190, 16241, 16256, 16200, 16214, 16228, 16162, 16283, 16262, 16300, 16171, 16209, 16228, 16321, 16142, 16247, 16143, 16275, 16308, 16155, 16316, 16198, 16222, 16201, 16177, 16312, 16198, 16228, 16184, 16185, 16305, 16202, 16171, 16259, 16257, 16214, 16155, 16384, 16175, 16378, 16361, 16265, 16384, 16358, 16128, 16384, 16375, 16142, 16130, 16281, 16218, 16241, 16271, 16180, 16206, 16301, 16210, 16285, 16273, 16250, 16271, 16311, 16145, 16384, 16243, 16197, 16289, 16272, 16282, 16253, 16200, 16322, 16195, 16220, 16358, 16291, 16292, 16205, 16384, 16203, 16236, 16211, 16272, 16384, 16254, 16241, 16212, 16348, 16297, 16275, 16199, 16294, 16311, 16215, 16266, 16239, 16260, 16274, 16216, 16310, 16169, 16384, 16239, 16260, 16269, 16169, 16376, 16279, 16201, 16328, 16276, 16294, 16218, 16134, 16384, 16172, 16384, 16264, 16218, 16266, 16322, 16271, 16240, 16257, 16237, 16384, 16199, 16279, 16256, 16171, 16188, 16278, 16313, 16256, 16270, 16185, 16204, 16184, 16384, 16237, 16223, 16333, 16303, 16266, 16288, 16329, 16384, 16262, 16342, 16239, 16249, 16194, 16310, 16170, 16344, 16186, 16275, 16286, 16310, 16221, 16384, 16305, 16280, 16139, 16346, 16241, 16203, 16294, 16305, 16166, 16250, 16246, 16259, 16267, 16270, 16251, 16283, 16267, 16312, 16205, 16384, 16201, 16274, 16232, 16293, 16244, 16223, 16193, 16238, 16290, 16288, 16241, 16261, 16384, 16166, 16384, 16225, 16384, 16384, 16300, 16187, 16347, 16314, 16275, 16228, 16151, 16168, 16339, 16315, 16187, 16295, 16207, 16363, 16251, 16262, 16286, 16317, 16254, 16156, 16320, 16217, 16226, 16239, 16327, 16229, 16176, 16384, 16263, 16197, 16336, 16180, 16233, 16173, 16257, 16328, 16235, 16232, 16205, 16379, 16319, 16177, 16201, 16293, 16302, 16261}, + }, + { + {16128, 16128, 16128, 16186, 16287, 16206, 16190, 16219, 16262, 16224, 16210, 16196, 16209, 16258, 16183, 16273, 16202, 16143, 16384, 16325, 16305, 16249, 16186, 16178, 16177, 16184, 16384, 16128, 16301, 16159, 16176, 16318, 16200, 16236, 16213, 16185, 16310, 16205, 16232, 16188, 16195, 16311, 16215, 16178, 16274, 16228, 16227, 16164, 16384, 16306, 16205, 16260, 16250, 16289, 16270, 16244, 16280, 16129, 16328, 16237, 16347, 16217, 16354, 16273, 16258, 16279, 16300, 16240, 16384, 16384, 16229, 16384, 16258, 16128, 16384, 16323, 16276, 16293, 16273, 16307, 16384, 16128, 16342, 16287, 16175, 16373, 16250, 16263, 16154, 16384, 16311, 16310, 16270, 16215, 16384, 16252, 16210, 16253, 16384, 16237, 16290, 16170, 16272, 16264, 16193, 16187, 16297, 16287, 16248, 16224, 16272, 16198, 16384, 16248, 16182, 16253, 16217, 16331, 16333, 16338, 16280, 16265, 16213, 16281, 16208, 16384, 16215, 16310, 16293, 16261, 16208, 16384, 16301, 16178, 16287, 16280, 16307, 16194, 16273, 16238, 16200, 16269, 16315, 16182, 16228, 16296, 16286, 16285, 16262, 16384, 16313, 16160, 16178, 16215, 16272, 16186, 16209, 16365, 16259, 16263, 16171, 16289, 16280, 16298, 16276, 16325, 16286, 16238, 16301, 16277, 16159, 16325, 16196, 16270, 16245, 16384, 16256, 16230, 16260, 16188, 16183, 16196, 16180, 16279, 16227, 16328, 16238, 16187, 16203, 16339, 16265, 16367, 16271, 16263, 16240, 16256, 16259, 16217, 16171, 16270, 16249, 16331, 16198, 16182, 16194, 16189, 16384, 16217, 16158, 16332, 16226, 16290, 16374, 16221, 16263, 16257, 16273, 16185, 16290, 16384, 16277, 16320, 16187, 16266, 16261, 16269, 16222, 16261, 16258, 16239, 16201, 16236, 16252, 16193, 16351, 16240, 16191, 16374, 16298, 16241, 16289, 16255, 16379, 16200, 16198, 16384, 16255, 16169, 16212, 16339, 16214, 16210, 16384, 16217, 16185, 16334}, + {16128, 16128, 16128, 16190, 16291, 16214, 16191, 16226, 16269, 16230, 16214, 16201, 16247, 16259, 16227, 16247, 16338, 16306, 16180, 16135, 16384, 16214, 16265, 16217, 16312, 16270, 16212, 16240, 16326, 16160, 16198, 16330, 16216, 16239, 16221, 16195, 16317, 16207, 16233, 16189, 16197, 16317, 16221, 16186, 16293, 16257, 16223, 16173, 16384, 16311, 16215, 16139, 16238, 16281, 16172, 16217, 16211, 16344, 16384, 16199, 16276, 16267, 16338, 16384, 16273, 16128, 16254, 16229, 16308, 16257, 16269, 16253, 16263, 16241, 16384, 16258, 16275, 16244, 16288, 16295, 16277, 16196, 16339, 16190, 16223, 16384, 16228, 16264, 16161, 16384, 16255, 16266, 16297, 16222, 16358, 16189, 16298, 16141, 16343, 16306, 16384, 16178, 16284, 16273, 16234, 16251, 16212, 16191, 16273, 16272, 16279, 16248, 16384, 16255, 16251, 16347, 16157, 16338, 16308, 16191, 16285, 16260, 16246, 16374, 16149, 16330, 16215, 16384, 16241, 16329, 16257, 16283, 16272, 16273, 16210, 16260, 16354, 16264, 16303, 16312, 16258, 16229, 16192, 16249, 16262, 16241, 16183, 16204, 16260, 16384, 16194, 16186, 16354, 16296, 16211, 16273, 16162, 16290, 16226, 16187, 16383, 16141, 16215, 16254, 16173, 16317, 16224, 16246, 16285, 16336, 16209, 16343, 16255, 16279, 16275, 16331, 16293, 16221, 16202, 16187, 16148, 16324, 16300, 16205, 16302, 16274, 16274, 16222, 16212, 16295, 16199, 16384, 16310, 16273, 16181, 16239, 16226, 16247, 16259, 16230, 16153, 16322, 16192, 16167, 16200, 16187, 16384, 16222, 16142, 16301, 16283, 16249, 16370, 16231, 16279, 16257, 16287, 16239, 16224, 16384, 16259, 16214, 16207, 16299, 16255, 16201, 16271, 16231, 16278, 16286, 16234, 16210, 16309, 16261, 16376, 16288, 16151, 16320, 16207, 16208, 16294, 16310, 16253, 16306, 16258, 16384, 16263, 16306, 16244, 16384, 16154, 16214, 16312, 16313, 16212, 16240}, + {16128, 16128, 16128, 16206, 16298, 16223, 16189, 16232, 16262, 16230, 16206, 16196, 16335, 16264, 16203, 16237, 16191, 16282, 16207, 16199, 16299, 16250, 16384, 16287, 16320, 16331, 16227, 16290, 16292, 16163, 16186, 16297, 16235, 16258, 16233, 16210, 16327, 16217, 16240, 16190, 16195, 16312, 16215, 16179, 16250, 16295, 16214, 16188, 16384, 16307, 16279, 16138, 16384, 16331, 16384, 16150, 16384, 16204, 16232, 16177, 16299, 16199, 16295, 16310, 16191, 16171, 16294, 16211, 16352, 16285, 16199, 16333, 16183, 16189, 16384, 16283, 16267, 16217, 16268, 16273, 16227, 16232, 16355, 16273, 16245, 16384, 16241, 16257, 16279, 16384, 16266, 16261, 16231, 16238, 16384, 16246, 16195, 16204, 16342, 16209, 16246, 16139, 16275, 16291, 16198, 16172, 16273, 16258, 16261, 16337, 16264, 16339, 16365, 16254, 16272, 16256, 16223, 16384, 16272, 16206, 16263, 16284, 16261, 16239, 16168, 16307, 16210, 16384, 16190, 16296, 16149, 16384, 16277, 16202, 16265, 16256, 16314, 16212, 16240, 16222, 16185, 16289, 16276, 16174, 16244, 16340, 16215, 16233, 16227, 16384, 16226, 16178, 16270, 16238, 16285, 16242, 16154, 16313, 16258, 16188, 16260, 16224, 16216, 16286, 16146, 16281, 16233, 16137, 16209, 16288, 16224, 16290, 16261, 16291, 16247, 16384, 16272, 16305, 16189, 16315, 16251, 16248, 16211, 16232, 16236, 16267, 16329, 16233, 16211, 16235, 16159, 16384, 16226, 16265, 16234, 16292, 16221, 16256, 16216, 16224, 16292, 16328, 16244, 16228, 16248, 16236, 16384, 16242, 16168, 16377, 16218, 16183, 16384, 16247, 16304, 16205, 16224, 16223, 16257, 16364, 16265, 16260, 16160, 16323, 16234, 16272, 16242, 16271, 16269, 16259, 16268, 16210, 16193, 16249, 16313, 16264, 16198, 16384, 16259, 16214, 16339, 16288, 16272, 16310, 16223, 16332, 16250, 16234, 16240, 16322, 16199, 16231, 16290, 16197, 16214, 16302}, + {16128, 16128, 16128, 16188, 16282, 16199, 16175, 16208, 16252, 16211, 16184, 16183, 16347, 16261, 16202, 16218, 16236, 16246, 16222, 16284, 16312, 16178, 16328, 16198, 16340, 16302, 16225, 16268, 16285, 16262, 16255, 16298, 16216, 16244, 16216, 16190, 16307, 16192, 16221, 16175, 16178, 16300, 16198, 16166, 16222, 16275, 16202, 16185, 16384, 16310, 16148, 16283, 16221, 16315, 16244, 16195, 16257, 16268, 16384, 16298, 16292, 16265, 16268, 16264, 16232, 16278, 16306, 16246, 16289, 16291, 16247, 16290, 16211, 16194, 16384, 16292, 16299, 16146, 16370, 16297, 16235, 16170, 16384, 16318, 16142, 16384, 16270, 16283, 16160, 16384, 16272, 16268, 16240, 16186, 16373, 16224, 16259, 16136, 16324, 16279, 16227, 16265, 16257, 16305, 16196, 16283, 16223, 16286, 16285, 16271, 16308, 16223, 16384, 16261, 16197, 16230, 16175, 16286, 16306, 16273, 16227, 16260, 16256, 16262, 16195, 16384, 16136, 16378, 16212, 16238, 16199, 16270, 16233, 16263, 16287, 16152, 16327, 16188, 16285, 16263, 16209, 16266, 16310, 16177, 16258, 16315, 16262, 16256, 16211, 16321, 16329, 16271, 16227, 16243, 16274, 16216, 16264, 16344, 16250, 16271, 16171, 16293, 16182, 16300, 16210, 16384, 16220, 16259, 16298, 16301, 16333, 16305, 16244, 16291, 16357, 16384, 16259, 16266, 16239, 16252, 16193, 16242, 16199, 16198, 16280, 16277, 16266, 16254, 16284, 16290, 16139, 16384, 16265, 16273, 16184, 16269, 16195, 16288, 16293, 16205, 16256, 16273, 16259, 16258, 16212, 16288, 16384, 16238, 16169, 16355, 16199, 16181, 16374, 16265, 16242, 16214, 16246, 16242, 16244, 16376, 16220, 16296, 16200, 16384, 16292, 16201, 16234, 16276, 16203, 16352, 16352, 16227, 16272, 16199, 16338, 16179, 16228, 16338, 16202, 16246, 16332, 16225, 16233, 16243, 16288, 16318, 16265, 16191, 16224, 16331, 16283, 16170, 16292, 16226, 16183, 16300}, + }, + { + {16128, 16128, 16128, 16180, 16290, 16210, 16187, 16221, 16263, 16227, 16211, 16198, 16307, 16301, 16186, 16223, 16221, 16261, 16203, 16164, 16180, 16162, 16325, 16241, 16179, 16201, 16384, 16128, 16384, 16217, 16268, 16343, 16212, 16230, 16217, 16188, 16317, 16203, 16235, 16188, 16192, 16314, 16216, 16181, 16207, 16260, 16247, 16177, 16348, 16322, 16259, 16284, 16183, 16279, 16276, 16237, 16368, 16263, 16208, 16291, 16289, 16226, 16286, 16321, 16197, 16170, 16260, 16271, 16289, 16254, 16298, 16217, 16222, 16223, 16384, 16206, 16187, 16201, 16321, 16299, 16220, 16164, 16319, 16273, 16291, 16317, 16221, 16252, 16179, 16384, 16290, 16262, 16233, 16147, 16307, 16197, 16204, 16215, 16384, 16172, 16278, 16235, 16272, 16304, 16291, 16199, 16325, 16231, 16260, 16190, 16282, 16164, 16308, 16257, 16354, 16195, 16192, 16384, 16274, 16275, 16262, 16290, 16270, 16222, 16144, 16384, 16169, 16384, 16261, 16384, 16130, 16348, 16264, 16206, 16284, 16234, 16285, 16186, 16304, 16278, 16195, 16351, 16261, 16147, 16384, 16384, 16384, 16311, 16213, 16335, 16351, 16165, 16266, 16281, 16239, 16256, 16172, 16332, 16258, 16216, 16219, 16313, 16203, 16293, 16200, 16289, 16173, 16178, 16225, 16335, 16212, 16315, 16268, 16295, 16260, 16384, 16262, 16271, 16237, 16238, 16149, 16346, 16310, 16246, 16271, 16331, 16199, 16193, 16165, 16278, 16215, 16384, 16236, 16267, 16214, 16380, 16241, 16282, 16284, 16227, 16265, 16240, 16292, 16324, 16231, 16194, 16384, 16282, 16206, 16384, 16199, 16172, 16384, 16229, 16257, 16224, 16202, 16231, 16268, 16360, 16159, 16270, 16262, 16292, 16271, 16219, 16262, 16299, 16260, 16304, 16309, 16286, 16268, 16276, 16324, 16207, 16179, 16384, 16364, 16212, 16306, 16222, 16227, 16216, 16187, 16316, 16223, 16207, 16192, 16281, 16263, 16170, 16276, 16210, 16240, 16282}, + {16128, 16128, 16128, 16192, 16291, 16212, 16190, 16219, 16267, 16230, 16205, 16197, 16211, 16316, 16174, 16257, 16181, 16290, 16234, 16380, 16269, 16168, 16265, 16277, 16363, 16233, 16185, 16257, 16377, 16235, 16321, 16384, 16217, 16244, 16219, 16192, 16320, 16200, 16239, 16189, 16190, 16314, 16220, 16184, 16238, 16204, 16263, 16151, 16384, 16364, 16171, 16213, 16148, 16179, 16282, 16302, 16259, 16310, 16347, 16225, 16335, 16154, 16278, 16297, 16269, 16237, 16265, 16208, 16384, 16204, 16299, 16197, 16237, 16211, 16384, 16206, 16158, 16295, 16309, 16284, 16206, 16259, 16326, 16225, 16151, 16384, 16243, 16250, 16237, 16377, 16158, 16256, 16243, 16223, 16367, 16230, 16270, 16219, 16370, 16259, 16262, 16219, 16255, 16289, 16271, 16244, 16265, 16221, 16172, 16205, 16246, 16235, 16384, 16253, 16270, 16307, 16161, 16384, 16173, 16136, 16232, 16260, 16178, 16342, 16259, 16384, 16190, 16340, 16227, 16254, 16236, 16316, 16211, 16298, 16288, 16128, 16332, 16163, 16281, 16232, 16270, 16215, 16290, 16201, 16257, 16319, 16214, 16241, 16277, 16384, 16145, 16260, 16212, 16211, 16269, 16211, 16195, 16310, 16240, 16252, 16168, 16284, 16133, 16365, 16176, 16382, 16225, 16223, 16224, 16233, 16313, 16262, 16220, 16254, 16317, 16382, 16212, 16208, 16288, 16224, 16262, 16282, 16246, 16184, 16177, 16329, 16353, 16274, 16266, 16323, 16200, 16384, 16240, 16261, 16238, 16309, 16157, 16217, 16280, 16190, 16384, 16284, 16272, 16277, 16384, 16216, 16384, 16260, 16163, 16298, 16331, 16291, 16333, 16223, 16265, 16257, 16286, 16238, 16269, 16384, 16217, 16257, 16187, 16322, 16274, 16268, 16215, 16384, 16284, 16251, 16295, 16265, 16249, 16281, 16332, 16149, 16204, 16315, 16155, 16215, 16295, 16208, 16278, 16207, 16182, 16272, 16207, 16384, 16187, 16340, 16259, 16220, 16369, 16158, 16207, 16346}, + {16128, 16128, 16128, 16190, 16290, 16211, 16188, 16222, 16264, 16238, 16208, 16194, 16278, 16315, 16189, 16219, 16241, 16176, 16330, 16195, 16317, 16272, 16269, 16164, 16268, 16128, 16245, 16128, 16326, 16227, 16244, 16384, 16212, 16239, 16222, 16195, 16316, 16203, 16233, 16186, 16184, 16308, 16224, 16181, 16212, 16273, 16273, 16185, 16384, 16289, 16266, 16138, 16211, 16269, 16240, 16257, 16322, 16233, 16276, 16262, 16247, 16258, 16190, 16173, 16183, 16200, 16331, 16209, 16374, 16197, 16246, 16233, 16221, 16303, 16384, 16263, 16261, 16289, 16315, 16324, 16229, 16213, 16315, 16198, 16241, 16384, 16307, 16375, 16260, 16367, 16219, 16275, 16247, 16350, 16384, 16265, 16265, 16230, 16353, 16212, 16311, 16250, 16275, 16276, 16257, 16273, 16304, 16267, 16213, 16234, 16236, 16236, 16384, 16229, 16238, 16238, 16257, 16285, 16246, 16243, 16232, 16253, 16266, 16266, 16336, 16298, 16176, 16384, 16308, 16221, 16180, 16384, 16268, 16208, 16284, 16254, 16299, 16261, 16231, 16221, 16189, 16272, 16232, 16238, 16315, 16302, 16214, 16228, 16215, 16344, 16184, 16293, 16316, 16347, 16257, 16280, 16147, 16349, 16293, 16157, 16250, 16213, 16249, 16279, 16238, 16340, 16230, 16287, 16384, 16305, 16128, 16384, 16179, 16302, 16209, 16354, 16265, 16193, 16238, 16276, 16285, 16272, 16336, 16301, 16269, 16311, 16210, 16251, 16194, 16232, 16163, 16384, 16321, 16263, 16205, 16258, 16203, 16275, 16289, 16213, 16305, 16281, 16266, 16268, 16207, 16226, 16384, 16210, 16224, 16384, 16303, 16171, 16329, 16202, 16271, 16231, 16284, 16236, 16245, 16384, 16239, 16282, 16158, 16369, 16203, 16231, 16263, 16295, 16243, 16274, 16284, 16209, 16239, 16241, 16352, 16209, 16128, 16349, 16296, 16148, 16348, 16283, 16289, 16295, 16140, 16270, 16225, 16349, 16260, 16305, 16201, 16246, 16256, 16267, 16181, 16281}, + {16128, 16128, 16128, 16209, 16303, 16226, 16198, 16236, 16268, 16231, 16209, 16198, 16213, 16306, 16182, 16259, 16134, 16153, 16196, 16384, 16319, 16182, 16358, 16201, 16296, 16210, 16328, 16210, 16283, 16200, 16287, 16328, 16248, 16262, 16255, 16221, 16334, 16217, 16250, 16198, 16198, 16318, 16216, 16182, 16252, 16210, 16258, 16157, 16301, 16210, 16275, 16287, 16384, 16332, 16128, 16339, 16306, 16163, 16321, 16208, 16268, 16226, 16260, 16234, 16277, 16218, 16288, 16201, 16294, 16267, 16241, 16266, 16209, 16198, 16384, 16289, 16241, 16264, 16273, 16294, 16213, 16267, 16384, 16280, 16230, 16384, 16285, 16236, 16150, 16384, 16248, 16272, 16234, 16146, 16384, 16265, 16227, 16186, 16362, 16248, 16262, 16139, 16384, 16301, 16249, 16172, 16239, 16277, 16216, 16258, 16224, 16295, 16384, 16247, 16212, 16223, 16180, 16384, 16216, 16152, 16282, 16293, 16330, 16219, 16277, 16295, 16147, 16349, 16238, 16260, 16256, 16309, 16260, 16220, 16264, 16197, 16294, 16220, 16221, 16200, 16258, 16221, 16272, 16266, 16278, 16261, 16220, 16233, 16308, 16291, 16275, 16258, 16265, 16281, 16262, 16274, 16169, 16352, 16234, 16185, 16263, 16265, 16277, 16281, 16269, 16297, 16241, 16284, 16248, 16275, 16187, 16308, 16221, 16205, 16173, 16374, 16233, 16197, 16344, 16149, 16236, 16270, 16271, 16262, 16202, 16314, 16253, 16241, 16227, 16308, 16181, 16384, 16202, 16265, 16228, 16262, 16285, 16214, 16218, 16304, 16275, 16339, 16258, 16248, 16328, 16173, 16384, 16282, 16248, 16384, 16240, 16178, 16384, 16150, 16337, 16218, 16214, 16232, 16237, 16338, 16300, 16235, 16323, 16349, 16294, 16272, 16195, 16200, 16235, 16251, 16274, 16344, 16220, 16170, 16332, 16194, 16211, 16323, 16189, 16277, 16384, 16257, 16260, 16309, 16160, 16366, 16254, 16210, 16178, 16384, 16219, 16266, 16272, 16212, 16187, 16297}, + }, + { + {16128, 16128, 16128, 16198, 16299, 16223, 16195, 16235, 16274, 16235, 16212, 16204, 16180, 16243, 16205, 16280, 16246, 16204, 16218, 16211, 16293, 16148, 16306, 16177, 16255, 16280, 16204, 16184, 16384, 16128, 16208, 16384, 16224, 16256, 16224, 16197, 16329, 16215, 16244, 16195, 16203, 16319, 16227, 16190, 16299, 16201, 16207, 16166, 16384, 16177, 16375, 16379, 16286, 16358, 16177, 16257, 16384, 16187, 16237, 16182, 16282, 16264, 16358, 16327, 16328, 16193, 16190, 16316, 16384, 16208, 16235, 16246, 16173, 16148, 16384, 16280, 16193, 16184, 16324, 16294, 16234, 16196, 16378, 16263, 16164, 16384, 16254, 16261, 16195, 16384, 16258, 16259, 16189, 16199, 16384, 16283, 16274, 16158, 16309, 16175, 16292, 16289, 16232, 16261, 16270, 16289, 16276, 16235, 16235, 16248, 16258, 16216, 16381, 16271, 16205, 16182, 16198, 16360, 16238, 16263, 16245, 16236, 16227, 16266, 16237, 16344, 16189, 16384, 16260, 16343, 16156, 16328, 16268, 16272, 16275, 16175, 16361, 16296, 16345, 16379, 16180, 16218, 16277, 16257, 16227, 16254, 16196, 16213, 16169, 16384, 16231, 16269, 16237, 16275, 16236, 16208, 16147, 16367, 16281, 16160, 16184, 16286, 16283, 16307, 16220, 16310, 16257, 16230, 16168, 16203, 16249, 16218, 16270, 16296, 16149, 16360, 16236, 16214, 16297, 16270, 16296, 16312, 16323, 16221, 16319, 16279, 16297, 16302, 16258, 16238, 16151, 16384, 16212, 16259, 16384, 16214, 16246, 16263, 16352, 16228, 16226, 16265, 16263, 16265, 16153, 16185, 16384, 16176, 16177, 16384, 16220, 16184, 16338, 16280, 16307, 16256, 16252, 16282, 16197, 16384, 16277, 16233, 16203, 16384, 16318, 16244, 16166, 16320, 16282, 16262, 16258, 16262, 16227, 16211, 16334, 16293, 16129, 16384, 16275, 16130, 16350, 16219, 16216, 16223, 16270, 16305, 16264, 16210, 16222, 16384, 16165, 16224, 16207, 16231, 16293, 16313}, + {16128, 16128, 16128, 16199, 16299, 16220, 16187, 16229, 16265, 16227, 16208, 16196, 16298, 16293, 16223, 16203, 16325, 16287, 16164, 16146, 16209, 16322, 16384, 16190, 16379, 16264, 16254, 16300, 16327, 16254, 16205, 16296, 16241, 16259, 16229, 16201, 16329, 16211, 16241, 16189, 16191, 16311, 16221, 16181, 16210, 16282, 16264, 16174, 16384, 16208, 16260, 16275, 16384, 16360, 16384, 16137, 16305, 16264, 16275, 16234, 16294, 16268, 16384, 16348, 16211, 16175, 16237, 16159, 16384, 16208, 16229, 16222, 16263, 16145, 16384, 16199, 16350, 16168, 16260, 16267, 16217, 16202, 16306, 16259, 16178, 16324, 16229, 16272, 16171, 16384, 16299, 16282, 16258, 16148, 16384, 16290, 16231, 16280, 16384, 16252, 16322, 16191, 16273, 16273, 16244, 16266, 16225, 16270, 16215, 16184, 16214, 16171, 16384, 16251, 16212, 16224, 16166, 16332, 16304, 16220, 16268, 16297, 16305, 16220, 16235, 16260, 16305, 16384, 16241, 16265, 16234, 16298, 16257, 16276, 16275, 16199, 16291, 16332, 16283, 16273, 16176, 16283, 16245, 16184, 16254, 16272, 16217, 16242, 16211, 16336, 16271, 16228, 16327, 16272, 16265, 16242, 16182, 16265, 16187, 16209, 16272, 16186, 16255, 16264, 16264, 16267, 16194, 16229, 16260, 16274, 16175, 16310, 16196, 16292, 16208, 16384, 16250, 16141, 16295, 16181, 16154, 16331, 16263, 16158, 16184, 16302, 16269, 16188, 16214, 16276, 16181, 16384, 16155, 16226, 16267, 16166, 16192, 16270, 16234, 16209, 16248, 16280, 16246, 16238, 16190, 16260, 16384, 16248, 16252, 16384, 16384, 16298, 16384, 16206, 16242, 16224, 16233, 16270, 16212, 16339, 16283, 16248, 16184, 16353, 16246, 16263, 16181, 16268, 16239, 16182, 16255, 16310, 16261, 16263, 16338, 16164, 16189, 16314, 16172, 16258, 16311, 16294, 16177, 16301, 16169, 16384, 16255, 16279, 16156, 16320, 16222, 16236, 16259, 16261, 16233, 16281}, + {16128, 16128, 16128, 16184, 16294, 16217, 16194, 16224, 16266, 16227, 16205, 16195, 16204, 16297, 16164, 16257, 16272, 16384, 16384, 16152, 16200, 16384, 16313, 16250, 16335, 16250, 16317, 16284, 16233, 16291, 16152, 16246, 16237, 16245, 16226, 16194, 16326, 16197, 16242, 16192, 16193, 16317, 16210, 16177, 16266, 16203, 16225, 16153, 16322, 16177, 16318, 16215, 16178, 16313, 16238, 16137, 16292, 16169, 16302, 16268, 16346, 16205, 16288, 16384, 16298, 16128, 16208, 16243, 16372, 16242, 16262, 16258, 16222, 16212, 16384, 16269, 16190, 16248, 16264, 16259, 16177, 16263, 16313, 16249, 16160, 16371, 16211, 16231, 16136, 16384, 16247, 16292, 16178, 16176, 16383, 16267, 16268, 16185, 16331, 16273, 16247, 16207, 16247, 16310, 16340, 16154, 16384, 16188, 16194, 16171, 16267, 16164, 16352, 16270, 16275, 16223, 16217, 16357, 16262, 16188, 16232, 16285, 16283, 16271, 16273, 16240, 16221, 16369, 16226, 16222, 16192, 16370, 16269, 16236, 16199, 16237, 16324, 16155, 16292, 16259, 16200, 16170, 16236, 16302, 16206, 16249, 16244, 16255, 16197, 16384, 16192, 16273, 16160, 16219, 16207, 16153, 16288, 16384, 16334, 16310, 16275, 16243, 16255, 16285, 16248, 16305, 16194, 16271, 16265, 16290, 16251, 16347, 16163, 16366, 16156, 16334, 16246, 16282, 16301, 16235, 16286, 16238, 16216, 16281, 16254, 16345, 16170, 16261, 16302, 16292, 16161, 16377, 16243, 16279, 16222, 16282, 16272, 16267, 16257, 16272, 16384, 16237, 16269, 16276, 16229, 16162, 16384, 16287, 16204, 16384, 16202, 16175, 16318, 16262, 16271, 16273, 16264, 16247, 16262, 16330, 16258, 16315, 16276, 16365, 16286, 16323, 16176, 16320, 16236, 16215, 16334, 16177, 16210, 16296, 16314, 16278, 16163, 16384, 16321, 16174, 16309, 16265, 16282, 16262, 16184, 16313, 16267, 16266, 16249, 16384, 16197, 16226, 16290, 16272, 16239, 16270}, + {16128, 16128, 16128, 16192, 16293, 16215, 16189, 16227, 16267, 16224, 16205, 16197, 16299, 16340, 16214, 16193, 16262, 16187, 16296, 16283, 16233, 16260, 16295, 16315, 16274, 16330, 16188, 16251, 16317, 16257, 16218, 16297, 16233, 16249, 16218, 16196, 16320, 16207, 16235, 16189, 16193, 16314, 16215, 16180, 16204, 16307, 16272, 16201, 16384, 16275, 16163, 16284, 16285, 16334, 16143, 16192, 16273, 16233, 16282, 16228, 16300, 16178, 16299, 16279, 16252, 16244, 16237, 16311, 16348, 16265, 16262, 16257, 16234, 16164, 16384, 16228, 16262, 16231, 16223, 16249, 16260, 16189, 16338, 16215, 16305, 16310, 16251, 16271, 16218, 16384, 16220, 16284, 16284, 16135, 16308, 16187, 16269, 16181, 16303, 16257, 16273, 16210, 16301, 16301, 16268, 16219, 16260, 16330, 16251, 16275, 16330, 16202, 16324, 16261, 16266, 16179, 16224, 16384, 16224, 16171, 16274, 16263, 16273, 16214, 16246, 16359, 16159, 16384, 16221, 16295, 16299, 16245, 16282, 16243, 16221, 16226, 16314, 16242, 16259, 16260, 16290, 16358, 16220, 16182, 16226, 16283, 16329, 16281, 16155, 16377, 16287, 16223, 16249, 16272, 16261, 16242, 16197, 16337, 16246, 16213, 16264, 16286, 16311, 16286, 16187, 16283, 16259, 16316, 16359, 16289, 16219, 16322, 16211, 16284, 16254, 16384, 16252, 16201, 16272, 16259, 16166, 16230, 16223, 16297, 16288, 16281, 16215, 16258, 16180, 16240, 16166, 16384, 16222, 16282, 16260, 16281, 16190, 16293, 16284, 16196, 16288, 16237, 16252, 16262, 16260, 16202, 16384, 16230, 16128, 16283, 16291, 16277, 16342, 16223, 16281, 16273, 16189, 16234, 16229, 16377, 16232, 16272, 16227, 16312, 16313, 16233, 16231, 16276, 16263, 16192, 16248, 16307, 16245, 16268, 16363, 16170, 16195, 16384, 16263, 16211, 16318, 16251, 16264, 16314, 16260, 16356, 16231, 16171, 16128, 16382, 16298, 16289, 16210, 16258, 16287, 16289}, + }, + { + {16128, 16128, 16128, 16199, 16297, 16219, 16198, 16238, 16271, 16246, 16219, 16207, 16206, 16250, 16216, 16301, 16224, 16137, 16352, 16288, 16266, 16220, 16258, 16234, 16363, 16238, 16340, 16322, 16353, 16234, 16279, 16349, 16235, 16257, 16220, 16198, 16321, 16219, 16243, 16199, 16197, 16318, 16238, 16199, 16287, 16236, 16237, 16166, 16297, 16247, 16227, 16308, 16189, 16236, 16368, 16189, 16384, 16308, 16285, 16384, 16256, 16173, 16258, 16201, 16245, 16192, 16286, 16201, 16365, 16264, 16216, 16301, 16240, 16224, 16384, 16264, 16189, 16169, 16298, 16288, 16218, 16251, 16361, 16242, 16257, 16363, 16272, 16265, 16128, 16384, 16281, 16278, 16229, 16162, 16384, 16280, 16198, 16174, 16345, 16276, 16320, 16263, 16265, 16213, 16290, 16179, 16249, 16280, 16359, 16161, 16224, 16132, 16384, 16223, 16191, 16274, 16129, 16267, 16358, 16244, 16272, 16285, 16228, 16251, 16135, 16309, 16258, 16384, 16197, 16250, 16217, 16266, 16233, 16279, 16247, 16171, 16289, 16167, 16200, 16174, 16343, 16306, 16302, 16239, 16268, 16262, 16278, 16290, 16183, 16296, 16384, 16267, 16221, 16279, 16196, 16193, 16190, 16303, 16223, 16209, 16287, 16230, 16245, 16276, 16182, 16314, 16258, 16208, 16369, 16276, 16165, 16384, 16209, 16278, 16178, 16384, 16207, 16265, 16252, 16366, 16162, 16262, 16287, 16326, 16193, 16270, 16264, 16268, 16207, 16309, 16237, 16384, 16202, 16256, 16244, 16317, 16230, 16269, 16288, 16255, 16232, 16245, 16269, 16280, 16281, 16194, 16384, 16281, 16156, 16384, 16218, 16281, 16347, 16241, 16264, 16268, 16170, 16270, 16243, 16307, 16222, 16209, 16192, 16368, 16235, 16184, 16295, 16354, 16235, 16266, 16261, 16230, 16247, 16223, 16384, 16272, 16140, 16384, 16325, 16149, 16316, 16264, 16211, 16238, 16219, 16303, 16268, 16234, 16154, 16384, 16257, 16211, 16255, 16267, 16197, 16280}, + {16128, 16128, 16128, 16197, 16297, 16222, 16205, 16234, 16276, 16238, 16212, 16208, 16222, 16265, 16213, 16263, 16177, 16229, 16230, 16251, 16384, 16263, 16193, 16293, 16309, 16153, 16317, 16159, 16342, 16174, 16257, 16384, 16211, 16243, 16221, 16198, 16323, 16218, 16251, 16203, 16205, 16332, 16221, 16180, 16246, 16258, 16226, 16172, 16384, 16185, 16258, 16167, 16293, 16302, 16363, 16175, 16295, 16132, 16301, 16204, 16310, 16150, 16349, 16302, 16277, 16158, 16274, 16212, 16384, 16384, 16216, 16384, 16290, 16179, 16384, 16271, 16187, 16222, 16261, 16256, 16246, 16237, 16298, 16260, 16166, 16335, 16223, 16269, 16234, 16384, 16219, 16236, 16269, 16279, 16384, 16226, 16252, 16178, 16337, 16207, 16188, 16140, 16285, 16322, 16258, 16206, 16219, 16266, 16203, 16231, 16282, 16195, 16384, 16243, 16265, 16290, 16256, 16384, 16197, 16169, 16282, 16305, 16239, 16255, 16231, 16304, 16180, 16345, 16175, 16384, 16285, 16282, 16298, 16203, 16269, 16283, 16336, 16251, 16290, 16274, 16257, 16256, 16269, 16251, 16242, 16320, 16227, 16243, 16238, 16300, 16306, 16294, 16275, 16294, 16191, 16195, 16218, 16332, 16247, 16265, 16293, 16168, 16215, 16265, 16149, 16368, 16271, 16191, 16295, 16276, 16143, 16384, 16285, 16245, 16186, 16323, 16259, 16273, 16210, 16309, 16169, 16319, 16317, 16213, 16318, 16257, 16207, 16258, 16195, 16294, 16230, 16384, 16272, 16241, 16215, 16291, 16246, 16200, 16196, 16265, 16277, 16286, 16278, 16282, 16273, 16153, 16384, 16224, 16137, 16384, 16235, 16318, 16362, 16285, 16289, 16208, 16227, 16285, 16178, 16384, 16128, 16282, 16256, 16243, 16206, 16291, 16273, 16265, 16239, 16159, 16346, 16251, 16197, 16150, 16307, 16148, 16239, 16384, 16309, 16203, 16347, 16271, 16198, 16273, 16277, 16350, 16237, 16380, 16152, 16384, 16172, 16264, 16205, 16224, 16281, 16306}, + {16128, 16128, 16128, 16177, 16299, 16225, 16194, 16220, 16265, 16222, 16207, 16200, 16229, 16290, 16175, 16249, 16226, 16286, 16171, 16166, 16270, 16170, 16202, 16313, 16303, 16258, 16337, 16297, 16281, 16156, 16316, 16384, 16201, 16223, 16201, 16179, 16341, 16201, 16258, 16199, 16194, 16318, 16212, 16183, 16240, 16232, 16256, 16158, 16384, 16254, 16260, 16132, 16190, 16266, 16267, 16304, 16384, 16372, 16196, 16194, 16313, 16205, 16281, 16314, 16286, 16205, 16248, 16179, 16277, 16288, 16221, 16280, 16218, 16196, 16384, 16259, 16225, 16253, 16294, 16286, 16220, 16197, 16339, 16266, 16296, 16339, 16226, 16186, 16194, 16384, 16271, 16228, 16269, 16220, 16384, 16255, 16193, 16253, 16384, 16180, 16248, 16145, 16321, 16313, 16167, 16171, 16296, 16384, 16232, 16157, 16227, 16152, 16384, 16249, 16189, 16258, 16170, 16326, 16343, 16257, 16286, 16287, 16283, 16193, 16237, 16384, 16138, 16373, 16157, 16287, 16293, 16271, 16255, 16298, 16244, 16176, 16267, 16180, 16304, 16268, 16278, 16286, 16297, 16164, 16173, 16280, 16290, 16268, 16191, 16326, 16233, 16202, 16249, 16249, 16221, 16215, 16225, 16364, 16269, 16257, 16253, 16308, 16299, 16281, 16293, 16384, 16167, 16350, 16305, 16343, 16261, 16329, 16328, 16272, 16164, 16384, 16262, 16165, 16242, 16195, 16232, 16310, 16308, 16226, 16259, 16278, 16254, 16311, 16162, 16260, 16211, 16384, 16261, 16274, 16216, 16287, 16176, 16270, 16246, 16204, 16219, 16270, 16230, 16211, 16234, 16258, 16384, 16261, 16258, 16384, 16244, 16278, 16317, 16331, 16226, 16243, 16239, 16265, 16248, 16315, 16196, 16268, 16175, 16287, 16336, 16193, 16177, 16222, 16185, 16230, 16336, 16216, 16379, 16287, 16351, 16179, 16208, 16384, 16253, 16250, 16384, 16300, 16214, 16294, 16165, 16354, 16258, 16239, 16176, 16384, 16272, 16233, 16296, 16259, 16163, 16283}, + {16128, 16128, 16128, 16191, 16295, 16217, 16193, 16236, 16271, 16242, 16213, 16203, 16217, 16269, 16197, 16267, 16246, 16293, 16318, 16238, 16283, 16158, 16302, 16206, 16384, 16361, 16164, 16384, 16336, 16180, 16216, 16316, 16218, 16247, 16213, 16188, 16319, 16215, 16236, 16193, 16197, 16314, 16227, 16189, 16273, 16230, 16250, 16170, 16347, 16276, 16273, 16292, 16202, 16384, 16196, 16163, 16352, 16216, 16272, 16218, 16245, 16207, 16289, 16217, 16221, 16227, 16269, 16283, 16350, 16214, 16268, 16220, 16239, 16234, 16384, 16228, 16268, 16207, 16282, 16275, 16302, 16215, 16326, 16189, 16184, 16348, 16264, 16257, 16274, 16384, 16215, 16272, 16211, 16225, 16384, 16266, 16248, 16182, 16327, 16181, 16265, 16134, 16288, 16304, 16336, 16148, 16252, 16200, 16273, 16384, 16384, 16267, 16384, 16261, 16216, 16195, 16169, 16324, 16284, 16268, 16259, 16263, 16227, 16258, 16238, 16352, 16169, 16384, 16217, 16195, 16205, 16323, 16293, 16184, 16298, 16284, 16333, 16242, 16270, 16276, 16198, 16239, 16308, 16261, 16222, 16279, 16275, 16267, 16192, 16355, 16321, 16217, 16384, 16290, 16261, 16335, 16154, 16293, 16258, 16198, 16259, 16253, 16266, 16290, 16176, 16283, 16210, 16171, 16183, 16240, 16201, 16264, 16169, 16358, 16282, 16384, 16284, 16235, 16264, 16227, 16275, 16257, 16227, 16247, 16308, 16271, 16211, 16269, 16267, 16323, 16198, 16381, 16266, 16249, 16194, 16200, 16184, 16273, 16311, 16203, 16243, 16384, 16230, 16205, 16222, 16148, 16381, 16246, 16176, 16359, 16281, 16261, 16295, 16267, 16238, 16290, 16146, 16268, 16184, 16309, 16298, 16247, 16145, 16355, 16301, 16151, 16285, 16241, 16259, 16265, 16346, 16191, 16321, 16226, 16374, 16282, 16250, 16384, 16266, 16243, 16374, 16232, 16163, 16248, 16233, 16332, 16247, 16212, 16297, 16313, 16235, 16167, 16202, 16202, 16265, 16314}, + }, + { + {16128, 16128, 16128, 16182, 16289, 16210, 16187, 16221, 16273, 16236, 16218, 16211, 16221, 16279, 16198, 16256, 16145, 16163, 16215, 16354, 16343, 16201, 16166, 16256, 16257, 16218, 16359, 16162, 16315, 16255, 16250, 16298, 16223, 16234, 16215, 16191, 16316, 16207, 16235, 16188, 16212, 16327, 16226, 16187, 16282, 16241, 16247, 16173, 16384, 16301, 16211, 16229, 16201, 16339, 16230, 16156, 16250, 16275, 16346, 16250, 16310, 16203, 16330, 16310, 16287, 16253, 16260, 16252, 16292, 16307, 16258, 16288, 16263, 16164, 16384, 16184, 16164, 16216, 16285, 16275, 16265, 16167, 16301, 16261, 16190, 16317, 16278, 16300, 16138, 16384, 16239, 16259, 16264, 16157, 16365, 16236, 16231, 16263, 16384, 16230, 16238, 16191, 16278, 16299, 16260, 16284, 16289, 16344, 16261, 16218, 16323, 16171, 16384, 16237, 16203, 16273, 16157, 16352, 16294, 16180, 16339, 16296, 16251, 16262, 16168, 16376, 16186, 16311, 16258, 16239, 16200, 16306, 16267, 16262, 16165, 16266, 16276, 16232, 16205, 16182, 16242, 16288, 16269, 16185, 16186, 16368, 16236, 16263, 16138, 16384, 16347, 16228, 16262, 16278, 16283, 16259, 16167, 16379, 16285, 16191, 16263, 16289, 16263, 16299, 16198, 16327, 16210, 16278, 16278, 16317, 16176, 16334, 16197, 16322, 16176, 16321, 16223, 16359, 16194, 16384, 16191, 16340, 16291, 16214, 16181, 16318, 16300, 16228, 16245, 16283, 16154, 16374, 16208, 16266, 16262, 16286, 16277, 16244, 16214, 16298, 16384, 16362, 16284, 16288, 16275, 16303, 16384, 16303, 16209, 16384, 16207, 16170, 16269, 16249, 16176, 16249, 16150, 16238, 16193, 16342, 16253, 16274, 16246, 16384, 16224, 16263, 16264, 16315, 16285, 16276, 16237, 16258, 16258, 16230, 16364, 16191, 16128, 16383, 16251, 16137, 16346, 16260, 16264, 16247, 16218, 16300, 16260, 16173, 16130, 16375, 16274, 16327, 16298, 16220, 16201, 16307}, + {16128, 16128, 16128, 16185, 16300, 16220, 16200, 16236, 16267, 16225, 16208, 16203, 16181, 16248, 16224, 16307, 16148, 16230, 16189, 16355, 16277, 16177, 16308, 16256, 16305, 16310, 16209, 16298, 16344, 16270, 16304, 16334, 16206, 16237, 16220, 16189, 16325, 16211, 16245, 16198, 16197, 16322, 16211, 16179, 16315, 16216, 16224, 16171, 16384, 16288, 16259, 16219, 16288, 16290, 16134, 16293, 16283, 16294, 16342, 16298, 16308, 16268, 16347, 16384, 16217, 16178, 16279, 16231, 16335, 16234, 16205, 16271, 16204, 16156, 16384, 16233, 16245, 16188, 16308, 16282, 16275, 16174, 16363, 16254, 16279, 16266, 16267, 16323, 16152, 16384, 16273, 16296, 16307, 16128, 16384, 16279, 16157, 16209, 16384, 16258, 16232, 16128, 16360, 16342, 16208, 16223, 16289, 16300, 16234, 16255, 16225, 16242, 16384, 16220, 16201, 16273, 16149, 16359, 16243, 16158, 16169, 16210, 16327, 16261, 16212, 16289, 16188, 16359, 16226, 16280, 16143, 16384, 16276, 16242, 16214, 16229, 16305, 16257, 16312, 16277, 16214, 16228, 16284, 16266, 16228, 16318, 16211, 16232, 16258, 16384, 16337, 16133, 16263, 16272, 16230, 16217, 16177, 16328, 16267, 16211, 16306, 16270, 16273, 16298, 16231, 16384, 16247, 16230, 16249, 16294, 16275, 16328, 16177, 16299, 16256, 16384, 16229, 16241, 16272, 16238, 16152, 16384, 16258, 16128, 16263, 16307, 16224, 16212, 16185, 16266, 16224, 16384, 16209, 16260, 16274, 16196, 16178, 16261, 16283, 16185, 16165, 16259, 16245, 16232, 16232, 16160, 16384, 16269, 16162, 16344, 16254, 16266, 16284, 16250, 16252, 16313, 16363, 16222, 16279, 16365, 16239, 16284, 16157, 16306, 16277, 16257, 16183, 16384, 16255, 16134, 16384, 16272, 16269, 16144, 16371, 16257, 16199, 16384, 16253, 16238, 16384, 16221, 16147, 16241, 16294, 16352, 16260, 16219, 16170, 16305, 16263, 16220, 16283, 16219, 16274, 16272}, + {16128, 16128, 16128, 16205, 16301, 16225, 16198, 16236, 16269, 16240, 16220, 16203, 16379, 16275, 16239, 16234, 16188, 16182, 16256, 16273, 16266, 16247, 16384, 16294, 16384, 16142, 16275, 16156, 16343, 16302, 16206, 16361, 16241, 16259, 16233, 16209, 16329, 16221, 16248, 16199, 16201, 16325, 16225, 16188, 16265, 16294, 16227, 16199, 16384, 16263, 16317, 16163, 16236, 16267, 16229, 16182, 16377, 16219, 16242, 16238, 16229, 16275, 16309, 16233, 16215, 16222, 16293, 16216, 16373, 16230, 16232, 16250, 16257, 16162, 16384, 16222, 16297, 16186, 16256, 16263, 16256, 16178, 16384, 16255, 16165, 16384, 16264, 16261, 16163, 16384, 16235, 16267, 16219, 16163, 16358, 16249, 16280, 16161, 16315, 16204, 16191, 16207, 16253, 16358, 16256, 16192, 16195, 16380, 16284, 16218, 16309, 16170, 16384, 16259, 16203, 16197, 16178, 16356, 16255, 16221, 16307, 16319, 16221, 16276, 16261, 16320, 16154, 16356, 16147, 16294, 16194, 16311, 16299, 16206, 16283, 16247, 16308, 16197, 16259, 16239, 16261, 16310, 16315, 16154, 16289, 16277, 16224, 16231, 16163, 16384, 16279, 16186, 16230, 16257, 16281, 16239, 16279, 16384, 16276, 16289, 16201, 16269, 16166, 16320, 16208, 16338, 16234, 16295, 16277, 16297, 16252, 16335, 16276, 16260, 16199, 16378, 16265, 16281, 16235, 16291, 16268, 16322, 16312, 16213, 16263, 16283, 16249, 16291, 16162, 16221, 16162, 16384, 16272, 16259, 16209, 16216, 16170, 16261, 16269, 16171, 16287, 16272, 16233, 16224, 16262, 16183, 16384, 16245, 16195, 16384, 16265, 16246, 16358, 16249, 16369, 16196, 16188, 16226, 16266, 16336, 16259, 16301, 16172, 16308, 16301, 16191, 16207, 16279, 16252, 16194, 16245, 16276, 16277, 16168, 16377, 16201, 16163, 16326, 16207, 16233, 16378, 16229, 16251, 16229, 16187, 16384, 16249, 16226, 16211, 16369, 16216, 16253, 16318, 16191, 16223, 16290}, + {16128, 16128, 16128, 16197, 16291, 16215, 16193, 16226, 16268, 16239, 16211, 16211, 16299, 16270, 16214, 16245, 16277, 16228, 16343, 16294, 16268, 16261, 16274, 16184, 16376, 16298, 16170, 16284, 16278, 16248, 16169, 16270, 16233, 16253, 16235, 16206, 16317, 16213, 16236, 16191, 16196, 16317, 16223, 16189, 16257, 16291, 16238, 16186, 16384, 16344, 16163, 16292, 16247, 16266, 16211, 16285, 16249, 16304, 16330, 16235, 16223, 16191, 16313, 16222, 16253, 16214, 16263, 16225, 16348, 16213, 16238, 16250, 16307, 16181, 16384, 16244, 16214, 16225, 16264, 16288, 16216, 16185, 16336, 16273, 16191, 16384, 16293, 16265, 16180, 16384, 16237, 16232, 16271, 16204, 16369, 16215, 16267, 16128, 16326, 16242, 16306, 16204, 16272, 16277, 16296, 16239, 16229, 16301, 16213, 16239, 16267, 16236, 16371, 16253, 16275, 16223, 16235, 16384, 16220, 16172, 16263, 16278, 16239, 16282, 16174, 16312, 16258, 16384, 16280, 16257, 16155, 16302, 16282, 16251, 16236, 16207, 16301, 16211, 16276, 16259, 16248, 16269, 16273, 16177, 16267, 16305, 16271, 16270, 16318, 16384, 16275, 16137, 16192, 16257, 16223, 16179, 16230, 16367, 16263, 16243, 16308, 16197, 16252, 16274, 16237, 16272, 16263, 16234, 16268, 16311, 16181, 16330, 16222, 16350, 16208, 16360, 16258, 16257, 16272, 16254, 16233, 16275, 16228, 16164, 16357, 16250, 16217, 16236, 16313, 16294, 16203, 16380, 16246, 16281, 16188, 16272, 16226, 16307, 16262, 16252, 16315, 16230, 16257, 16260, 16256, 16159, 16384, 16265, 16181, 16371, 16268, 16248, 16384, 16176, 16295, 16192, 16149, 16249, 16207, 16341, 16263, 16291, 16157, 16321, 16295, 16258, 16190, 16319, 16233, 16182, 16323, 16222, 16282, 16154, 16373, 16310, 16204, 16384, 16250, 16216, 16323, 16282, 16253, 16310, 16342, 16304, 16257, 16171, 16229, 16375, 16233, 16260, 16300, 16220, 16272, 16288}, + }, + { + {16128, 16128, 16128, 16203, 16311, 16233, 16217, 16253, 16285, 16258, 16234, 16220, 16248, 16305, 16209, 16270, 16160, 16173, 16249, 16347, 16365, 16258, 16276, 16278, 16193, 16248, 16330, 16145, 16323, 16176, 16228, 16323, 16226, 16253, 16243, 16209, 16335, 16234, 16260, 16217, 16213, 16335, 16252, 16205, 16268, 16260, 16275, 16191, 16348, 16235, 16290, 16260, 16266, 16293, 16252, 16240, 16384, 16384, 16145, 16128, 16266, 16193, 16305, 16266, 16261, 16215, 16294, 16209, 16384, 16266, 16265, 16259, 16234, 16170, 16384, 16303, 16270, 16202, 16274, 16301, 16250, 16207, 16338, 16258, 16179, 16350, 16260, 16273, 16196, 16384, 16257, 16271, 16227, 16158, 16384, 16249, 16214, 16159, 16322, 16223, 16219, 16199, 16281, 16329, 16275, 16304, 16207, 16270, 16251, 16215, 16262, 16190, 16384, 16237, 16263, 16270, 16161, 16384, 16182, 16128, 16296, 16292, 16188, 16284, 16202, 16384, 16129, 16382, 16217, 16219, 16198, 16314, 16283, 16264, 16271, 16187, 16333, 16246, 16302, 16276, 16222, 16290, 16285, 16187, 16237, 16281, 16236, 16222, 16224, 16348, 16279, 16213, 16260, 16277, 16215, 16256, 16258, 16315, 16189, 16301, 16257, 16243, 16292, 16289, 16186, 16363, 16250, 16255, 16280, 16316, 16276, 16297, 16216, 16304, 16157, 16322, 16267, 16297, 16221, 16269, 16155, 16314, 16281, 16224, 16265, 16288, 16213, 16226, 16267, 16292, 16175, 16384, 16239, 16274, 16225, 16221, 16253, 16242, 16252, 16266, 16214, 16286, 16223, 16207, 16229, 16219, 16384, 16245, 16187, 16360, 16274, 16232, 16384, 16211, 16275, 16230, 16253, 16350, 16181, 16350, 16203, 16259, 16196, 16281, 16271, 16198, 16198, 16324, 16258, 16275, 16309, 16301, 16218, 16235, 16325, 16218, 16210, 16375, 16232, 16230, 16358, 16232, 16195, 16249, 16240, 16341, 16236, 16225, 16261, 16309, 16236, 16201, 16290, 16154, 16204, 16351}, + {16128, 16128, 16128, 16190, 16290, 16213, 16192, 16227, 16272, 16241, 16217, 16208, 16201, 16287, 16190, 16270, 16207, 16234, 16265, 16257, 16218, 16139, 16302, 16316, 16384, 16182, 16256, 16217, 16312, 16219, 16276, 16349, 16216, 16238, 16217, 16192, 16316, 16210, 16236, 16191, 16206, 16317, 16226, 16191, 16270, 16223, 16253, 16159, 16295, 16247, 16208, 16279, 16374, 16309, 16136, 16285, 16287, 16329, 16330, 16384, 16263, 16269, 16216, 16208, 16244, 16128, 16264, 16194, 16324, 16261, 16209, 16301, 16264, 16209, 16384, 16201, 16217, 16257, 16239, 16250, 16276, 16185, 16335, 16165, 16273, 16260, 16236, 16278, 16172, 16384, 16209, 16273, 16233, 16203, 16384, 16284, 16258, 16202, 16349, 16292, 16275, 16177, 16298, 16303, 16271, 16260, 16310, 16304, 16244, 16276, 16211, 16269, 16384, 16217, 16212, 16384, 16192, 16369, 16289, 16230, 16272, 16269, 16240, 16291, 16191, 16344, 16197, 16384, 16237, 16301, 16169, 16315, 16260, 16266, 16266, 16185, 16288, 16195, 16318, 16284, 16196, 16257, 16280, 16252, 16247, 16267, 16252, 16257, 16163, 16384, 16329, 16166, 16177, 16183, 16299, 16178, 16203, 16336, 16269, 16213, 16237, 16274, 16193, 16308, 16160, 16323, 16263, 16159, 16207, 16268, 16233, 16279, 16262, 16269, 16185, 16384, 16247, 16362, 16178, 16362, 16173, 16337, 16304, 16233, 16206, 16318, 16260, 16239, 16198, 16268, 16169, 16384, 16271, 16274, 16220, 16319, 16283, 16200, 16182, 16274, 16266, 16384, 16229, 16202, 16258, 16243, 16384, 16270, 16222, 16382, 16273, 16215, 16304, 16265, 16264, 16276, 16235, 16259, 16224, 16364, 16212, 16280, 16192, 16327, 16277, 16232, 16233, 16240, 16274, 16268, 16263, 16266, 16252, 16226, 16322, 16185, 16216, 16384, 16279, 16251, 16384, 16204, 16208, 16226, 16249, 16342, 16222, 16219, 16135, 16380, 16272, 16283, 16296, 16276, 16240, 16264}, + {16128, 16128, 16128, 16209, 16317, 16249, 16217, 16258, 16287, 16256, 16234, 16224, 16220, 16271, 16230, 16292, 16267, 16251, 16247, 16235, 16318, 16287, 16299, 16175, 16384, 16156, 16153, 16168, 16330, 16206, 16229, 16359, 16249, 16260, 16237, 16213, 16348, 16247, 16264, 16220, 16221, 16338, 16245, 16207, 16295, 16258, 16241, 16180, 16384, 16269, 16234, 16211, 16171, 16191, 16240, 16384, 16375, 16298, 16176, 16154, 16287, 16259, 16308, 16270, 16278, 16245, 16241, 16259, 16321, 16233, 16274, 16236, 16257, 16205, 16384, 16264, 16234, 16177, 16316, 16280, 16276, 16244, 16341, 16239, 16200, 16314, 16251, 16285, 16195, 16384, 16260, 16295, 16238, 16171, 16384, 16277, 16249, 16218, 16353, 16267, 16290, 16213, 16278, 16303, 16254, 16234, 16264, 16280, 16245, 16239, 16212, 16229, 16377, 16257, 16250, 16264, 16192, 16344, 16291, 16209, 16312, 16305, 16248, 16230, 16174, 16364, 16187, 16384, 16259, 16275, 16128, 16342, 16274, 16226, 16204, 16241, 16309, 16229, 16203, 16206, 16164, 16270, 16267, 16222, 16274, 16305, 16215, 16228, 16178, 16384, 16296, 16173, 16296, 16275, 16271, 16264, 16138, 16334, 16274, 16168, 16273, 16282, 16293, 16300, 16256, 16322, 16248, 16263, 16270, 16321, 16181, 16333, 16217, 16240, 16195, 16357, 16259, 16195, 16287, 16237, 16185, 16281, 16274, 16221, 16202, 16264, 16281, 16261, 16154, 16267, 16217, 16384, 16251, 16268, 16267, 16258, 16265, 16271, 16234, 16300, 16276, 16280, 16250, 16241, 16227, 16164, 16384, 16267, 16208, 16336, 16264, 16182, 16324, 16182, 16280, 16261, 16183, 16215, 16275, 16361, 16206, 16256, 16199, 16302, 16276, 16189, 16261, 16272, 16255, 16223, 16298, 16245, 16276, 16236, 16373, 16215, 16200, 16384, 16265, 16210, 16315, 16220, 16240, 16229, 16210, 16336, 16252, 16281, 16172, 16384, 16290, 16269, 16221, 16238, 16225, 16300}, + {16128, 16128, 16128, 16204, 16310, 16234, 16215, 16256, 16277, 16250, 16227, 16220, 16223, 16305, 16193, 16278, 16384, 16226, 16194, 16216, 16260, 16285, 16234, 16163, 16384, 16177, 16266, 16278, 16384, 16193, 16372, 16380, 16236, 16257, 16249, 16215, 16332, 16240, 16258, 16212, 16211, 16337, 16234, 16195, 16268, 16232, 16267, 16171, 16384, 16296, 16205, 16168, 16164, 16204, 16269, 16276, 16384, 16198, 16240, 16179, 16214, 16221, 16278, 16200, 16195, 16156, 16286, 16208, 16384, 16233, 16219, 16264, 16384, 16163, 16384, 16235, 16240, 16239, 16312, 16315, 16257, 16179, 16351, 16252, 16177, 16322, 16273, 16337, 16234, 16384, 16226, 16234, 16275, 16257, 16384, 16239, 16224, 16212, 16361, 16219, 16250, 16128, 16304, 16299, 16197, 16257, 16266, 16266, 16227, 16182, 16203, 16199, 16384, 16256, 16252, 16254, 16247, 16366, 16258, 16182, 16265, 16265, 16283, 16233, 16211, 16317, 16222, 16384, 16269, 16274, 16227, 16296, 16265, 16246, 16270, 16179, 16332, 16170, 16257, 16221, 16235, 16271, 16259, 16246, 16206, 16227, 16267, 16265, 16154, 16384, 16278, 16223, 16247, 16237, 16270, 16237, 16179, 16333, 16301, 16220, 16295, 16219, 16223, 16287, 16255, 16300, 16195, 16254, 16198, 16240, 16275, 16256, 16261, 16260, 16144, 16313, 16262, 16252, 16270, 16240, 16215, 16269, 16278, 16266, 16245, 16324, 16261, 16286, 16233, 16277, 16196, 16384, 16231, 16254, 16182, 16270, 16284, 16254, 16230, 16258, 16284, 16284, 16237, 16218, 16262, 16182, 16384, 16261, 16177, 16384, 16287, 16217, 16283, 16308, 16199, 16305, 16224, 16266, 16193, 16384, 16247, 16260, 16224, 16384, 16252, 16270, 16204, 16304, 16247, 16174, 16318, 16272, 16258, 16218, 16341, 16227, 16281, 16366, 16203, 16287, 16318, 16218, 16259, 16233, 16245, 16365, 16253, 16246, 16173, 16384, 16263, 16282, 16291, 16241, 16245, 16285}, + }, + { + {16128, 16128, 16128, 16216, 16316, 16244, 16221, 16258, 16287, 16261, 16245, 16235, 16241, 16270, 16236, 16290, 16289, 16267, 16209, 16177, 16311, 16219, 16288, 16164, 16286, 16282, 16177, 16235, 16328, 16197, 16215, 16350, 16266, 16266, 16243, 16225, 16347, 16235, 16262, 16217, 16227, 16347, 16256, 16216, 16290, 16266, 16241, 16189, 16384, 16277, 16251, 16210, 16182, 16237, 16295, 16259, 16349, 16384, 16185, 16159, 16286, 16275, 16285, 16260, 16252, 16169, 16271, 16226, 16286, 16261, 16279, 16235, 16238, 16215, 16384, 16269, 16293, 16143, 16326, 16270, 16261, 16190, 16374, 16215, 16217, 16329, 16247, 16265, 16160, 16384, 16265, 16283, 16215, 16214, 16384, 16257, 16210, 16225, 16370, 16243, 16272, 16166, 16291, 16297, 16251, 16216, 16256, 16300, 16228, 16257, 16278, 16227, 16384, 16259, 16212, 16217, 16257, 16384, 16259, 16225, 16264, 16269, 16249, 16268, 16216, 16333, 16204, 16384, 16237, 16228, 16233, 16312, 16272, 16256, 16272, 16197, 16309, 16178, 16306, 16273, 16230, 16266, 16251, 16251, 16285, 16365, 16308, 16283, 16171, 16384, 16277, 16201, 16219, 16227, 16273, 16197, 16202, 16334, 16248, 16233, 16301, 16279, 16290, 16291, 16185, 16300, 16239, 16177, 16236, 16277, 16167, 16304, 16210, 16263, 16208, 16367, 16260, 16223, 16292, 16259, 16214, 16269, 16259, 16228, 16242, 16277, 16270, 16211, 16223, 16287, 16169, 16384, 16170, 16259, 16266, 16306, 16258, 16238, 16272, 16273, 16254, 16274, 16260, 16269, 16234, 16181, 16384, 16264, 16202, 16384, 16256, 16179, 16341, 16267, 16263, 16249, 16221, 16267, 16219, 16352, 16196, 16276, 16223, 16321, 16260, 16255, 16274, 16304, 16257, 16197, 16285, 16271, 16232, 16158, 16350, 16260, 16177, 16382, 16262, 16207, 16384, 16209, 16238, 16204, 16213, 16272, 16258, 16206, 16239, 16340, 16258, 16224, 16216, 16201, 16224, 16346}, + {16128, 16128, 16128, 16221, 16314, 16244, 16215, 16260, 16288, 16256, 16228, 16221, 16257, 16312, 16203, 16262, 16204, 16312, 16189, 16293, 16257, 16310, 16262, 16181, 16233, 16264, 16173, 16164, 16384, 16154, 16165, 16369, 16262, 16269, 16249, 16228, 16343, 16237, 16261, 16214, 16216, 16342, 16239, 16200, 16253, 16261, 16270, 16177, 16384, 16302, 16177, 16290, 16307, 16324, 16235, 16179, 16305, 16264, 16217, 16208, 16317, 16141, 16291, 16324, 16241, 16238, 16265, 16239, 16325, 16237, 16239, 16251, 16239, 16255, 16384, 16259, 16254, 16193, 16289, 16289, 16267, 16209, 16339, 16244, 16211, 16384, 16259, 16261, 16217, 16384, 16270, 16272, 16275, 16159, 16384, 16252, 16186, 16256, 16384, 16166, 16188, 16205, 16301, 16384, 16228, 16280, 16219, 16323, 16208, 16229, 16271, 16222, 16359, 16262, 16271, 16256, 16224, 16295, 16262, 16274, 16282, 16311, 16230, 16260, 16210, 16239, 16287, 16384, 16243, 16270, 16182, 16325, 16259, 16166, 16344, 16169, 16282, 16270, 16246, 16222, 16258, 16292, 16265, 16209, 16249, 16252, 16209, 16228, 16240, 16371, 16263, 16162, 16234, 16242, 16268, 16206, 16163, 16346, 16271, 16198, 16219, 16283, 16292, 16284, 16251, 16293, 16274, 16258, 16282, 16284, 16231, 16333, 16220, 16213, 16190, 16347, 16258, 16251, 16266, 16241, 16178, 16227, 16220, 16262, 16214, 16276, 16270, 16239, 16222, 16285, 16199, 16384, 16229, 16257, 16253, 16270, 16225, 16228, 16277, 16271, 16243, 16220, 16273, 16293, 16214, 16256, 16384, 16267, 16176, 16341, 16232, 16183, 16384, 16185, 16286, 16208, 16237, 16273, 16171, 16384, 16265, 16266, 16189, 16341, 16224, 16258, 16275, 16299, 16244, 16209, 16275, 16306, 16267, 16215, 16343, 16258, 16181, 16361, 16232, 16216, 16360, 16220, 16282, 16271, 16209, 16370, 16227, 16259, 16238, 16357, 16294, 16177, 16278, 16217, 16208, 16287}, + {16128, 16128, 16128, 16210, 16310, 16239, 16213, 16256, 16279, 16256, 16232, 16220, 16213, 16304, 16216, 16268, 16270, 16315, 16384, 16208, 16287, 16233, 16276, 16203, 16384, 16154, 16271, 16208, 16304, 16208, 16229, 16321, 16260, 16258, 16232, 16220, 16334, 16239, 16258, 16214, 16213, 16337, 16239, 16199, 16276, 16222, 16266, 16177, 16292, 16214, 16288, 16273, 16268, 16279, 16245, 16281, 16215, 16384, 16384, 16228, 16262, 16181, 16274, 16223, 16228, 16212, 16275, 16242, 16367, 16212, 16258, 16232, 16288, 16175, 16384, 16194, 16266, 16262, 16232, 16261, 16178, 16230, 16384, 16283, 16202, 16376, 16258, 16294, 16161, 16384, 16247, 16289, 16280, 16230, 16384, 16232, 16232, 16214, 16346, 16265, 16227, 16131, 16329, 16347, 16214, 16212, 16288, 16219, 16239, 16189, 16283, 16163, 16372, 16263, 16233, 16227, 16211, 16352, 16300, 16237, 16267, 16276, 16274, 16212, 16193, 16341, 16141, 16378, 16250, 16242, 16231, 16333, 16262, 16249, 16261, 16207, 16329, 16240, 16276, 16289, 16309, 16247, 16298, 16252, 16244, 16294, 16271, 16266, 16193, 16384, 16211, 16183, 16283, 16289, 16270, 16268, 16167, 16306, 16240, 16197, 16244, 16200, 16265, 16264, 16214, 16342, 16257, 16248, 16261, 16300, 16261, 16306, 16219, 16279, 16177, 16345, 16255, 16208, 16274, 16263, 16208, 16245, 16232, 16261, 16233, 16288, 16249, 16249, 16223, 16292, 16190, 16384, 16267, 16265, 16189, 16305, 16230, 16264, 16270, 16234, 16272, 16273, 16231, 16218, 16283, 16181, 16384, 16250, 16221, 16384, 16219, 16176, 16327, 16228, 16262, 16262, 16251, 16222, 16195, 16384, 16219, 16280, 16175, 16328, 16278, 16206, 16262, 16287, 16261, 16216, 16275, 16253, 16265, 16236, 16335, 16262, 16153, 16384, 16274, 16166, 16327, 16201, 16275, 16253, 16194, 16358, 16250, 16199, 16203, 16369, 16235, 16246, 16289, 16196, 16221, 16276}, + {16128, 16128, 16128, 16198, 16298, 16219, 16205, 16233, 16276, 16247, 16227, 16222, 16259, 16284, 16216, 16261, 16283, 16237, 16245, 16206, 16204, 16235, 16331, 16226, 16223, 16249, 16384, 16164, 16294, 16211, 16241, 16329, 16256, 16258, 16226, 16202, 16322, 16213, 16249, 16200, 16214, 16338, 16235, 16198, 16271, 16277, 16252, 16198, 16376, 16286, 16242, 16193, 16325, 16297, 16163, 16274, 16339, 16196, 16283, 16269, 16254, 16253, 16248, 16222, 16222, 16218, 16284, 16210, 16325, 16313, 16251, 16285, 16218, 16192, 16384, 16253, 16253, 16233, 16272, 16284, 16199, 16253, 16276, 16199, 16212, 16383, 16282, 16284, 16208, 16384, 16257, 16222, 16266, 16165, 16384, 16251, 16275, 16192, 16355, 16213, 16272, 16257, 16251, 16289, 16254, 16215, 16280, 16246, 16271, 16223, 16266, 16187, 16384, 16232, 16226, 16257, 16157, 16340, 16264, 16196, 16268, 16274, 16251, 16260, 16192, 16372, 16171, 16384, 16263, 16255, 16172, 16384, 16303, 16262, 16222, 16213, 16324, 16227, 16288, 16268, 16260, 16311, 16195, 16217, 16265, 16286, 16247, 16252, 16164, 16332, 16279, 16253, 16258, 16271, 16258, 16234, 16203, 16379, 16261, 16240, 16289, 16270, 16308, 16287, 16178, 16339, 16243, 16205, 16293, 16302, 16159, 16343, 16255, 16271, 16243, 16380, 16258, 16229, 16272, 16227, 16142, 16381, 16277, 16168, 16271, 16293, 16230, 16228, 16200, 16283, 16182, 16384, 16228, 16272, 16260, 16221, 16128, 16327, 16300, 16128, 16266, 16252, 16261, 16265, 16284, 16128, 16331, 16361, 16180, 16356, 16278, 16216, 16302, 16272, 16290, 16273, 16188, 16248, 16242, 16328, 16198, 16305, 16246, 16294, 16266, 16227, 16237, 16249, 16206, 16261, 16256, 16315, 16266, 16228, 16349, 16259, 16258, 16384, 16271, 16241, 16340, 16237, 16229, 16239, 16322, 16384, 16265, 16284, 16181, 16372, 16228, 16221, 16266, 16185, 16227, 16310}, + }, + { + {16128, 16128, 16128, 16128, 16212, 16208, 16128, 16384, 16128, 16384, 16161, 16384, 16129, 16128, 16165, 16212, 16165, 16384, 16384, 16128, 16271, 16158, 16352, 16176, 16170, 16233, 16128, 16128, 16167, 16276, 16128, 16384, 16384, 16128, 16128, 16192, 16302, 16384, 16128, 16158, 16128, 16174, 16384, 16311, 16199, 16229, 16128, 16384, 16281, 16300, 16205, 16224, 16384, 16254, 16128, 16196, 16384, 16291, 16128, 16264, 16384, 16191, 16283, 16148, 16128, 16329, 16384, 16296, 16277, 16129, 16128, 16279, 16128, 16309, 16384, 16313, 16317, 16280, 16128, 16133, 16128, 16268, 16384, 16191, 16384, 16150, 16270, 16384, 16262, 16384, 16128, 16128, 16128, 16329, 16384, 16221, 16128, 16239, 16337, 16128, 16264, 16311, 16128, 16188, 16300, 16128, 16384, 16128, 16128, 16207, 16384, 16207, 16374, 16128, 16357, 16384, 16128, 16308, 16167, 16245, 16132, 16128, 16128, 16162, 16134, 16236, 16192, 16261, 16255, 16128, 16263, 16270, 16177, 16128, 16384, 16128, 16384, 16237, 16172, 16339, 16279, 16128, 16128, 16255, 16128, 16128, 16335, 16384, 16128, 16384, 16384, 16249, 16197, 16296, 16140, 16204, 16384, 16384, 16205, 16322, 16128, 16184, 16128, 16327, 16128, 16379, 16204, 16128, 16222, 16183, 16128, 16384, 16323, 16150, 16182, 16266, 16164, 16128, 16190, 16128, 16384, 16383, 16161, 16128, 16196, 16280, 16227, 16384, 16128, 16384, 16301, 16330, 16128, 16134, 16184, 16128, 16220, 16384, 16219, 16128, 16128, 16128, 16261, 16315, 16384, 16384, 16384, 16269, 16384, 16302, 16384, 16175, 16384, 16157, 16186, 16194, 16359, 16384, 16128, 16260, 16172, 16128, 16128, 16272, 16384, 16384, 16181, 16384, 16165, 16128, 16384, 16128, 16216, 16384, 16286, 16128, 16128, 16384, 16202, 16128, 16167, 16384, 16384, 16192, 16384, 16295, 16128, 16320, 16233, 16176, 16384, 16128, 16128, 16316, 16347, 16302}, + {16128, 16128, 16128, 16128, 16384, 16128, 16384, 16128, 16140, 16144, 16243, 16128, 16384, 16151, 16384, 16384, 16293, 16128, 16384, 16186, 16384, 16134, 16128, 16177, 16177, 16240, 16130, 16130, 16384, 16153, 16325, 16345, 16384, 16128, 16306, 16281, 16301, 16128, 16138, 16384, 16128, 16384, 16128, 16128, 16363, 16128, 16384, 16128, 16311, 16128, 16335, 16128, 16384, 16270, 16128, 16274, 16249, 16172, 16180, 16325, 16234, 16128, 16218, 16216, 16319, 16203, 16384, 16128, 16310, 16134, 16128, 16285, 16364, 16144, 16384, 16128, 16170, 16188, 16128, 16156, 16384, 16128, 16259, 16384, 16128, 16384, 16173, 16384, 16216, 16384, 16189, 16218, 16384, 16300, 16308, 16142, 16384, 16384, 16266, 16384, 16270, 16384, 16173, 16172, 16307, 16384, 16168, 16294, 16384, 16128, 16128, 16128, 16349, 16247, 16202, 16328, 16128, 16322, 16175, 16279, 16132, 16384, 16128, 16265, 16128, 16308, 16136, 16281, 16145, 16161, 16178, 16128, 16145, 16260, 16188, 16128, 16384, 16128, 16384, 16215, 16178, 16353, 16333, 16128, 16128, 16128, 16317, 16384, 16265, 16288, 16250, 16128, 16338, 16379, 16184, 16384, 16384, 16384, 16173, 16323, 16128, 16248, 16128, 16290, 16384, 16155, 16130, 16176, 16296, 16200, 16128, 16384, 16384, 16176, 16128, 16384, 16174, 16184, 16384, 16128, 16132, 16384, 16225, 16156, 16128, 16384, 16384, 16128, 16128, 16384, 16273, 16338, 16384, 16157, 16128, 16384, 16176, 16134, 16373, 16186, 16138, 16128, 16160, 16159, 16183, 16128, 16261, 16346, 16384, 16384, 16128, 16128, 16196, 16140, 16128, 16204, 16128, 16128, 16192, 16215, 16134, 16384, 16178, 16384, 16148, 16175, 16384, 16292, 16213, 16270, 16128, 16132, 16239, 16281, 16267, 16128, 16200, 16384, 16384, 16188, 16172, 16384, 16368, 16202, 16128, 16195, 16141, 16128, 16128, 16297, 16320, 16257, 16315, 16212, 16384, 16213}, + {16128, 16128, 16128, 16384, 16286, 16128, 16128, 16217, 16136, 16128, 16259, 16128, 16128, 16384, 16128, 16384, 16326, 16231, 16128, 16128, 16128, 16384, 16384, 16157, 16384, 16128, 16130, 16128, 16166, 16128, 16384, 16384, 16384, 16128, 16128, 16190, 16310, 16183, 16128, 16131, 16376, 16203, 16128, 16128, 16314, 16384, 16132, 16128, 16313, 16384, 16159, 16204, 16128, 16134, 16247, 16384, 16261, 16128, 16162, 16384, 16128, 16384, 16128, 16128, 16355, 16243, 16384, 16213, 16137, 16305, 16184, 16149, 16384, 16266, 16384, 16128, 16128, 16170, 16384, 16151, 16128, 16217, 16186, 16128, 16384, 16344, 16384, 16371, 16384, 16384, 16281, 16129, 16128, 16128, 16261, 16155, 16128, 16167, 16324, 16128, 16128, 16128, 16213, 16383, 16384, 16137, 16128, 16309, 16184, 16384, 16313, 16147, 16294, 16146, 16171, 16244, 16384, 16384, 16129, 16165, 16128, 16128, 16128, 16170, 16128, 16266, 16128, 16249, 16147, 16349, 16128, 16176, 16209, 16128, 16241, 16384, 16134, 16128, 16213, 16165, 16384, 16128, 16384, 16141, 16171, 16384, 16128, 16136, 16151, 16319, 16128, 16241, 16384, 16321, 16128, 16128, 16128, 16218, 16384, 16128, 16384, 16135, 16384, 16135, 16384, 16198, 16258, 16212, 16147, 16171, 16153, 16212, 16158, 16219, 16384, 16384, 16128, 16204, 16242, 16384, 16128, 16384, 16259, 16138, 16232, 16128, 16343, 16384, 16384, 16384, 16166, 16229, 16128, 16141, 16384, 16128, 16249, 16384, 16283, 16382, 16128, 16384, 16222, 16384, 16190, 16350, 16384, 16177, 16128, 16255, 16128, 16150, 16384, 16346, 16132, 16128, 16128, 16384, 16128, 16250, 16128, 16289, 16183, 16129, 16128, 16384, 16314, 16132, 16134, 16128, 16384, 16384, 16284, 16268, 16278, 16323, 16264, 16384, 16384, 16384, 16166, 16332, 16270, 16159, 16384, 16384, 16147, 16342, 16128, 16265, 16128, 16128, 16341, 16128, 16128, 16384}, + {16128, 16128, 16128, 16275, 16363, 16128, 16128, 16163, 16276, 16276, 16128, 16165, 16384, 16134, 16384, 16384, 16147, 16128, 16379, 16287, 16265, 16159, 16384, 16152, 16128, 16128, 16384, 16128, 16384, 16342, 16384, 16329, 16180, 16384, 16128, 16128, 16274, 16128, 16128, 16344, 16224, 16384, 16326, 16267, 16171, 16162, 16128, 16347, 16260, 16128, 16326, 16132, 16128, 16376, 16189, 16128, 16384, 16271, 16128, 16257, 16226, 16221, 16368, 16384, 16147, 16239, 16130, 16202, 16384, 16128, 16304, 16128, 16384, 16128, 16384, 16128, 16200, 16128, 16372, 16157, 16131, 16128, 16164, 16184, 16128, 16346, 16128, 16128, 16128, 16384, 16384, 16182, 16227, 16361, 16274, 16134, 16138, 16128, 16155, 16216, 16128, 16128, 16384, 16384, 16384, 16174, 16128, 16311, 16149, 16268, 16142, 16136, 16364, 16217, 16133, 16384, 16128, 16149, 16384, 16369, 16133, 16128, 16128, 16227, 16128, 16220, 16384, 16335, 16159, 16345, 16128, 16190, 16281, 16143, 16128, 16384, 16288, 16204, 16384, 16289, 16128, 16152, 16156, 16277, 16226, 16384, 16128, 16128, 16198, 16277, 16223, 16128, 16202, 16285, 16130, 16216, 16384, 16384, 16342, 16384, 16251, 16128, 16197, 16162, 16263, 16369, 16384, 16187, 16384, 16384, 16243, 16284, 16128, 16384, 16128, 16319, 16140, 16128, 16258, 16156, 16197, 16210, 16384, 16384, 16183, 16274, 16209, 16384, 16384, 16146, 16128, 16282, 16384, 16195, 16128, 16155, 16128, 16384, 16280, 16128, 16129, 16384, 16384, 16384, 16210, 16128, 16225, 16384, 16128, 16384, 16384, 16249, 16384, 16384, 16131, 16128, 16152, 16384, 16128, 16384, 16128, 16384, 16146, 16384, 16323, 16129, 16128, 16128, 16169, 16128, 16384, 16128, 16384, 16244, 16279, 16137, 16201, 16262, 16192, 16265, 16164, 16344, 16299, 16171, 16128, 16162, 16137, 16384, 16128, 16329, 16234, 16211, 16321, 16384, 16281, 16128}, + }, + { + {16128, 16128, 16128, 16310, 16384, 16128, 16384, 16156, 16384, 16128, 16281, 16384, 16164, 16384, 16128, 16384, 16128, 16138, 16128, 16360, 16147, 16128, 16235, 16283, 16384, 16144, 16251, 16353, 16384, 16128, 16136, 16384, 16384, 16128, 16305, 16281, 16342, 16303, 16143, 16200, 16140, 16296, 16185, 16384, 16131, 16240, 16289, 16314, 16384, 16135, 16384, 16384, 16128, 16384, 16183, 16128, 16384, 16384, 16128, 16128, 16233, 16213, 16308, 16384, 16128, 16137, 16384, 16203, 16384, 16128, 16384, 16128, 16338, 16128, 16384, 16128, 16134, 16173, 16384, 16303, 16128, 16206, 16384, 16175, 16128, 16384, 16182, 16384, 16128, 16384, 16384, 16197, 16260, 16296, 16270, 16129, 16138, 16128, 16176, 16265, 16128, 16384, 16142, 16369, 16366, 16179, 16128, 16356, 16186, 16384, 16132, 16384, 16306, 16134, 16347, 16276, 16268, 16384, 16384, 16384, 16145, 16130, 16138, 16213, 16250, 16173, 16269, 16342, 16347, 16384, 16202, 16384, 16291, 16300, 16144, 16149, 16211, 16128, 16354, 16313, 16135, 16384, 16260, 16128, 16253, 16140, 16128, 16128, 16259, 16384, 16128, 16230, 16344, 16309, 16159, 16139, 16378, 16384, 16384, 16384, 16128, 16237, 16128, 16297, 16141, 16165, 16262, 16384, 16128, 16128, 16130, 16135, 16384, 16274, 16128, 16205, 16128, 16384, 16162, 16263, 16128, 16384, 16313, 16178, 16196, 16132, 16384, 16384, 16354, 16166, 16128, 16384, 16384, 16217, 16128, 16174, 16128, 16384, 16361, 16128, 16128, 16384, 16128, 16128, 16169, 16128, 16236, 16349, 16384, 16338, 16384, 16182, 16384, 16384, 16149, 16128, 16128, 16128, 16267, 16290, 16128, 16262, 16192, 16161, 16146, 16155, 16384, 16301, 16305, 16241, 16170, 16202, 16142, 16130, 16263, 16128, 16128, 16212, 16202, 16128, 16308, 16139, 16231, 16128, 16128, 16189, 16174, 16384, 16133, 16363, 16278, 16384, 16289, 16182, 16128, 16279}, + {16128, 16128, 16128, 16155, 16384, 16128, 16322, 16154, 16128, 16384, 16214, 16384, 16128, 16153, 16290, 16384, 16149, 16384, 16384, 16128, 16128, 16128, 16217, 16215, 16293, 16128, 16169, 16128, 16206, 16128, 16384, 16384, 16183, 16384, 16243, 16384, 16227, 16384, 16384, 16384, 16384, 16363, 16128, 16384, 16128, 16384, 16263, 16128, 16286, 16268, 16159, 16273, 16128, 16384, 16150, 16128, 16384, 16384, 16128, 16128, 16212, 16384, 16335, 16384, 16285, 16185, 16128, 16282, 16384, 16128, 16381, 16128, 16384, 16272, 16384, 16128, 16128, 16320, 16128, 16177, 16128, 16196, 16313, 16384, 16128, 16384, 16128, 16128, 16128, 16283, 16167, 16384, 16246, 16353, 16269, 16129, 16128, 16217, 16333, 16128, 16384, 16128, 16384, 16148, 16310, 16128, 16384, 16128, 16220, 16274, 16128, 16165, 16384, 16133, 16128, 16287, 16128, 16148, 16384, 16384, 16384, 16199, 16296, 16134, 16128, 16295, 16141, 16279, 16128, 16128, 16128, 16384, 16135, 16248, 16131, 16128, 16384, 16187, 16140, 16219, 16171, 16345, 16128, 16384, 16264, 16384, 16128, 16128, 16182, 16344, 16128, 16215, 16282, 16384, 16145, 16384, 16359, 16384, 16239, 16380, 16128, 16275, 16300, 16221, 16128, 16384, 16147, 16138, 16346, 16301, 16267, 16273, 16191, 16151, 16288, 16292, 16128, 16384, 16167, 16232, 16384, 16128, 16128, 16153, 16298, 16384, 16128, 16128, 16128, 16384, 16247, 16299, 16184, 16132, 16136, 16237, 16225, 16128, 16303, 16384, 16369, 16135, 16128, 16128, 16128, 16128, 16262, 16128, 16128, 16225, 16128, 16153, 16384, 16384, 16134, 16128, 16128, 16128, 16191, 16171, 16181, 16384, 16197, 16135, 16169, 16128, 16134, 16384, 16128, 16258, 16193, 16128, 16128, 16384, 16185, 16384, 16128, 16176, 16128, 16227, 16235, 16128, 16240, 16128, 16128, 16182, 16152, 16384, 16128, 16384, 16384, 16237, 16384, 16171, 16379, 16171}, + {16128, 16128, 16128, 16294, 16209, 16261, 16128, 16384, 16128, 16384, 16210, 16375, 16384, 16226, 16128, 16188, 16129, 16128, 16384, 16256, 16384, 16271, 16128, 16384, 16384, 16384, 16128, 16344, 16345, 16133, 16384, 16292, 16210, 16384, 16226, 16384, 16307, 16333, 16369, 16298, 16215, 16384, 16209, 16128, 16290, 16175, 16384, 16380, 16384, 16202, 16128, 16133, 16384, 16285, 16384, 16128, 16174, 16128, 16287, 16137, 16242, 16128, 16225, 16216, 16178, 16128, 16207, 16128, 16171, 16372, 16257, 16200, 16328, 16128, 16384, 16128, 16128, 16362, 16154, 16238, 16384, 16128, 16384, 16128, 16128, 16306, 16384, 16384, 16210, 16384, 16128, 16128, 16146, 16128, 16384, 16144, 16309, 16384, 16384, 16342, 16128, 16369, 16134, 16384, 16348, 16193, 16128, 16271, 16128, 16234, 16314, 16262, 16350, 16226, 16128, 16384, 16128, 16384, 16128, 16128, 16384, 16206, 16235, 16128, 16128, 16204, 16384, 16343, 16149, 16384, 16128, 16178, 16148, 16219, 16142, 16194, 16384, 16303, 16173, 16331, 16128, 16163, 16142, 16180, 16230, 16384, 16128, 16135, 16202, 16288, 16298, 16128, 16384, 16355, 16152, 16144, 16190, 16384, 16202, 16138, 16128, 16150, 16128, 16305, 16128, 16264, 16132, 16128, 16327, 16200, 16208, 16384, 16128, 16128, 16154, 16313, 16159, 16149, 16290, 16156, 16384, 16128, 16128, 16261, 16253, 16384, 16128, 16128, 16210, 16384, 16134, 16365, 16156, 16174, 16384, 16128, 16128, 16128, 16128, 16128, 16384, 16154, 16132, 16134, 16191, 16128, 16259, 16343, 16332, 16234, 16167, 16128, 16177, 16165, 16315, 16297, 16165, 16128, 16384, 16384, 16156, 16128, 16128, 16263, 16151, 16128, 16128, 16384, 16190, 16184, 16128, 16149, 16262, 16351, 16338, 16317, 16188, 16244, 16180, 16257, 16246, 16128, 16273, 16128, 16128, 16196, 16149, 16128, 16128, 16384, 16384, 16257, 16384, 16384, 16357, 16128}, + {16128, 16128, 16128, 16128, 16384, 16210, 16128, 16295, 16128, 16384, 16160, 16384, 16384, 16307, 16300, 16128, 16190, 16384, 16384, 16384, 16250, 16128, 16352, 16128, 16128, 16128, 16384, 16128, 16161, 16293, 16384, 16153, 16128, 16260, 16325, 16384, 16301, 16133, 16131, 16384, 16293, 16162, 16384, 16128, 16284, 16197, 16128, 16384, 16313, 16273, 16128, 16384, 16384, 16268, 16384, 16128, 16173, 16348, 16384, 16384, 16256, 16300, 16384, 16384, 16251, 16128, 16218, 16128, 16242, 16225, 16128, 16381, 16352, 16128, 16384, 16128, 16191, 16128, 16384, 16163, 16384, 16128, 16303, 16128, 16128, 16384, 16128, 16128, 16128, 16348, 16128, 16163, 16128, 16271, 16384, 16224, 16270, 16144, 16246, 16128, 16265, 16384, 16139, 16158, 16384, 16180, 16384, 16330, 16140, 16190, 16365, 16241, 16268, 16213, 16384, 16143, 16128, 16320, 16193, 16257, 16128, 16128, 16128, 16195, 16153, 16268, 16357, 16269, 16208, 16207, 16384, 16242, 16134, 16266, 16149, 16157, 16384, 16272, 16158, 16322, 16275, 16128, 16138, 16243, 16128, 16128, 16382, 16384, 16128, 16320, 16197, 16181, 16184, 16304, 16137, 16210, 16203, 16358, 16184, 16144, 16128, 16244, 16384, 16236, 16128, 16237, 16147, 16128, 16384, 16306, 16228, 16303, 16384, 16267, 16128, 16180, 16151, 16128, 16266, 16128, 16128, 16384, 16260, 16147, 16128, 16213, 16289, 16342, 16128, 16233, 16155, 16232, 16291, 16185, 16185, 16224, 16170, 16128, 16301, 16190, 16384, 16170, 16133, 16132, 16232, 16384, 16384, 16173, 16384, 16384, 16128, 16128, 16384, 16128, 16384, 16156, 16327, 16384, 16148, 16269, 16384, 16384, 16128, 16298, 16306, 16128, 16384, 16141, 16128, 16211, 16246, 16128, 16158, 16128, 16297, 16384, 16288, 16384, 16384, 16384, 16384, 16197, 16128, 16218, 16128, 16384, 16165, 16135, 16128, 16346, 16276, 16358, 16183, 16265, 16154, 16128}, + }, + { + {16128, 16128, 16128, 16288, 16297, 16265, 16128, 16384, 16384, 16128, 16384, 16384, 16384, 16316, 16237, 16128, 16128, 16128, 16128, 16333, 16174, 16151, 16158, 16374, 16134, 16254, 16188, 16150, 16130, 16280, 16384, 16129, 16268, 16170, 16144, 16128, 16267, 16128, 16147, 16384, 16384, 16239, 16128, 16128, 16309, 16130, 16384, 16266, 16265, 16254, 16160, 16203, 16273, 16197, 16325, 16211, 16384, 16384, 16128, 16128, 16184, 16264, 16272, 16384, 16128, 16269, 16384, 16277, 16286, 16300, 16133, 16384, 16133, 16128, 16326, 16128, 16128, 16153, 16384, 16269, 16384, 16206, 16238, 16147, 16128, 16384, 16128, 16128, 16128, 16365, 16166, 16230, 16225, 16188, 16235, 16128, 16128, 16231, 16345, 16128, 16128, 16128, 16274, 16331, 16267, 16384, 16165, 16274, 16384, 16140, 16323, 16128, 16205, 16208, 16384, 16128, 16277, 16384, 16128, 16128, 16128, 16128, 16330, 16206, 16281, 16161, 16319, 16360, 16128, 16128, 16128, 16384, 16141, 16253, 16138, 16214, 16384, 16307, 16174, 16384, 16384, 16384, 16200, 16128, 16163, 16128, 16128, 16128, 16223, 16340, 16128, 16277, 16384, 16143, 16128, 16290, 16359, 16384, 16384, 16384, 16128, 16384, 16208, 16249, 16384, 16384, 16128, 16384, 16128, 16384, 16150, 16143, 16345, 16209, 16128, 16153, 16150, 16128, 16384, 16196, 16167, 16187, 16384, 16361, 16269, 16139, 16345, 16128, 16384, 16150, 16128, 16287, 16128, 16131, 16384, 16225, 16283, 16384, 16247, 16128, 16345, 16128, 16189, 16241, 16384, 16128, 16251, 16128, 16128, 16384, 16128, 16202, 16198, 16384, 16128, 16384, 16128, 16129, 16193, 16162, 16128, 16259, 16210, 16128, 16266, 16128, 16128, 16128, 16355, 16195, 16160, 16208, 16384, 16176, 16241, 16156, 16234, 16384, 16384, 16384, 16384, 16375, 16149, 16384, 16384, 16237, 16128, 16128, 16128, 16275, 16128, 16139, 16384, 16128, 16128, 16384}, + {16128, 16128, 16128, 16384, 16240, 16384, 16384, 16128, 16269, 16250, 16128, 16157, 16384, 16376, 16208, 16128, 16250, 16128, 16349, 16168, 16256, 16128, 16331, 16128, 16272, 16384, 16128, 16384, 16150, 16298, 16384, 16150, 16198, 16232, 16303, 16198, 16294, 16128, 16384, 16384, 16303, 16173, 16384, 16128, 16128, 16210, 16256, 16261, 16172, 16128, 16166, 16214, 16190, 16384, 16233, 16235, 16384, 16229, 16129, 16321, 16384, 16151, 16328, 16162, 16384, 16173, 16353, 16128, 16384, 16295, 16178, 16317, 16128, 16128, 16384, 16384, 16384, 16219, 16199, 16276, 16128, 16164, 16182, 16192, 16384, 16137, 16248, 16384, 16128, 16311, 16147, 16182, 16149, 16384, 16384, 16259, 16147, 16128, 16176, 16261, 16128, 16128, 16260, 16379, 16128, 16236, 16149, 16195, 16140, 16264, 16317, 16384, 16208, 16208, 16384, 16128, 16328, 16283, 16265, 16128, 16384, 16183, 16176, 16128, 16260, 16384, 16128, 16253, 16128, 16128, 16128, 16384, 16128, 16384, 16236, 16128, 16384, 16344, 16189, 16384, 16251, 16128, 16128, 16189, 16303, 16268, 16149, 16145, 16156, 16169, 16138, 16184, 16193, 16384, 16202, 16259, 16193, 16146, 16128, 16299, 16180, 16178, 16384, 16198, 16384, 16384, 16128, 16384, 16153, 16189, 16174, 16245, 16145, 16197, 16384, 16384, 16301, 16195, 16128, 16191, 16128, 16128, 16128, 16226, 16152, 16245, 16128, 16263, 16384, 16353, 16147, 16260, 16184, 16168, 16384, 16128, 16180, 16134, 16384, 16384, 16384, 16140, 16272, 16339, 16129, 16384, 16384, 16128, 16128, 16158, 16219, 16172, 16181, 16128, 16128, 16172, 16128, 16128, 16260, 16303, 16384, 16131, 16384, 16369, 16301, 16128, 16312, 16128, 16198, 16188, 16128, 16128, 16193, 16384, 16233, 16128, 16193, 16384, 16384, 16384, 16175, 16355, 16271, 16169, 16384, 16322, 16128, 16266, 16128, 16288, 16128, 16141, 16384, 16155, 16128, 16272}, + {16128, 16128, 16128, 16151, 16196, 16310, 16304, 16384, 16150, 16384, 16384, 16129, 16128, 16384, 16224, 16131, 16384, 16128, 16128, 16136, 16146, 16384, 16137, 16128, 16348, 16298, 16128, 16269, 16384, 16384, 16384, 16384, 16128, 16147, 16338, 16384, 16355, 16128, 16384, 16242, 16261, 16384, 16272, 16306, 16128, 16355, 16238, 16131, 16276, 16128, 16384, 16128, 16241, 16190, 16276, 16280, 16154, 16128, 16267, 16128, 16384, 16128, 16210, 16326, 16128, 16128, 16384, 16137, 16239, 16235, 16128, 16384, 16384, 16128, 16380, 16286, 16384, 16175, 16154, 16216, 16128, 16179, 16384, 16209, 16128, 16254, 16384, 16384, 16238, 16208, 16384, 16292, 16181, 16277, 16224, 16128, 16384, 16290, 16200, 16384, 16384, 16128, 16323, 16156, 16207, 16162, 16384, 16384, 16128, 16128, 16128, 16142, 16384, 16133, 16128, 16128, 16128, 16179, 16384, 16128, 16286, 16154, 16141, 16282, 16167, 16260, 16210, 16252, 16139, 16286, 16128, 16384, 16132, 16128, 16384, 16128, 16218, 16384, 16128, 16128, 16128, 16128, 16384, 16148, 16128, 16175, 16178, 16177, 16178, 16384, 16128, 16138, 16244, 16128, 16137, 16384, 16384, 16384, 16212, 16275, 16289, 16154, 16384, 16138, 16384, 16128, 16128, 16128, 16384, 16207, 16128, 16384, 16128, 16128, 16128, 16251, 16158, 16128, 16384, 16227, 16189, 16160, 16384, 16384, 16284, 16134, 16384, 16128, 16128, 16128, 16128, 16273, 16140, 16161, 16384, 16128, 16141, 16128, 16347, 16331, 16128, 16384, 16269, 16384, 16168, 16334, 16384, 16185, 16128, 16384, 16384, 16273, 16384, 16128, 16384, 16128, 16216, 16384, 16128, 16181, 16128, 16344, 16128, 16128, 16320, 16128, 16384, 16137, 16128, 16128, 16331, 16128, 16200, 16128, 16299, 16192, 16233, 16384, 16384, 16384, 16384, 16381, 16131, 16384, 16128, 16130, 16131, 16377, 16128, 16234, 16128, 16128, 16128, 16290, 16384, 16231}, + {16128, 16128, 16128, 16178, 16383, 16128, 16128, 16189, 16302, 16195, 16128, 16384, 16128, 16384, 16128, 16384, 16263, 16384, 16384, 16128, 16128, 16384, 16384, 16138, 16338, 16139, 16181, 16384, 16384, 16177, 16128, 16283, 16128, 16284, 16128, 16128, 16215, 16384, 16384, 16384, 16173, 16384, 16128, 16146, 16276, 16256, 16128, 16384, 16319, 16176, 16176, 16128, 16384, 16257, 16128, 16186, 16170, 16128, 16299, 16128, 16292, 16170, 16384, 16384, 16141, 16384, 16355, 16384, 16147, 16328, 16187, 16164, 16321, 16217, 16384, 16132, 16149, 16190, 16128, 16150, 16128, 16226, 16240, 16128, 16384, 16134, 16252, 16384, 16260, 16377, 16254, 16274, 16148, 16128, 16384, 16173, 16128, 16205, 16384, 16151, 16235, 16281, 16128, 16157, 16128, 16324, 16134, 16128, 16163, 16153, 16269, 16210, 16384, 16148, 16128, 16128, 16268, 16241, 16128, 16201, 16384, 16222, 16319, 16134, 16128, 16186, 16371, 16343, 16246, 16284, 16128, 16384, 16145, 16220, 16138, 16188, 16238, 16194, 16384, 16228, 16128, 16128, 16384, 16202, 16128, 16128, 16297, 16384, 16187, 16384, 16128, 16142, 16307, 16346, 16162, 16384, 16207, 16320, 16128, 16279, 16185, 16174, 16384, 16192, 16384, 16131, 16140, 16141, 16384, 16345, 16339, 16384, 16384, 16166, 16128, 16384, 16154, 16229, 16384, 16128, 16128, 16384, 16384, 16128, 16384, 16201, 16380, 16132, 16128, 16128, 16128, 16287, 16203, 16181, 16142, 16279, 16155, 16134, 16375, 16172, 16384, 16144, 16224, 16269, 16128, 16128, 16286, 16128, 16384, 16384, 16172, 16243, 16187, 16384, 16128, 16288, 16384, 16128, 16384, 16380, 16133, 16384, 16149, 16384, 16144, 16156, 16384, 16266, 16242, 16276, 16290, 16197, 16154, 16128, 16233, 16291, 16128, 16180, 16160, 16128, 16128, 16373, 16194, 16384, 16384, 16285, 16128, 16294, 16128, 16280, 16128, 16147, 16353, 16128, 16128, 16321}, + }, + { + {16128, 16128, 16128, 16360, 16384, 16128, 16128, 16204, 16275, 16190, 16128, 16272, 16383, 16136, 16384, 16384, 16153, 16384, 16384, 16128, 16141, 16158, 16149, 16384, 16139, 16190, 16250, 16136, 16384, 16384, 16336, 16303, 16128, 16143, 16318, 16384, 16312, 16285, 16152, 16128, 16128, 16160, 16384, 16245, 16128, 16384, 16219, 16128, 16310, 16236, 16128, 16384, 16281, 16188, 16128, 16167, 16272, 16128, 16158, 16384, 16128, 16384, 16128, 16128, 16384, 16156, 16384, 16310, 16266, 16384, 16128, 16364, 16128, 16128, 16384, 16384, 16316, 16128, 16384, 16218, 16384, 16128, 16272, 16128, 16128, 16359, 16251, 16129, 16151, 16384, 16128, 16128, 16384, 16290, 16267, 16128, 16128, 16229, 16384, 16128, 16185, 16377, 16136, 16384, 16316, 16154, 16384, 16384, 16128, 16259, 16128, 16128, 16197, 16138, 16234, 16128, 16378, 16384, 16128, 16129, 16384, 16231, 16128, 16384, 16136, 16198, 16305, 16261, 16227, 16128, 16149, 16292, 16336, 16229, 16128, 16128, 16141, 16128, 16312, 16240, 16128, 16256, 16272, 16384, 16384, 16384, 16384, 16219, 16175, 16357, 16196, 16128, 16290, 16323, 16148, 16384, 16384, 16384, 16344, 16384, 16384, 16128, 16128, 16135, 16166, 16353, 16384, 16142, 16128, 16384, 16128, 16145, 16265, 16259, 16128, 16166, 16268, 16288, 16128, 16140, 16128, 16384, 16261, 16134, 16351, 16160, 16328, 16128, 16128, 16128, 16160, 16384, 16146, 16128, 16263, 16274, 16384, 16128, 16128, 16209, 16297, 16128, 16162, 16198, 16384, 16384, 16384, 16183, 16384, 16222, 16158, 16128, 16160, 16128, 16128, 16158, 16232, 16128, 16384, 16384, 16128, 16131, 16128, 16384, 16342, 16128, 16384, 16140, 16128, 16128, 16384, 16265, 16202, 16384, 16234, 16128, 16384, 16362, 16136, 16153, 16157, 16315, 16384, 16150, 16295, 16156, 16149, 16139, 16217, 16145, 16384, 16128, 16272, 16165, 16384, 16185}, + {16128, 16128, 16128, 16138, 16384, 16128, 16128, 16205, 16199, 16384, 16384, 16250, 16128, 16384, 16128, 16384, 16128, 16347, 16128, 16346, 16261, 16128, 16294, 16128, 16330, 16384, 16128, 16384, 16384, 16155, 16274, 16313, 16256, 16159, 16128, 16128, 16260, 16128, 16128, 16384, 16155, 16384, 16298, 16128, 16337, 16154, 16384, 16315, 16295, 16128, 16307, 16128, 16128, 16384, 16147, 16128, 16165, 16384, 16316, 16384, 16384, 16185, 16319, 16170, 16185, 16128, 16140, 16162, 16326, 16128, 16128, 16232, 16372, 16128, 16384, 16349, 16128, 16177, 16134, 16167, 16301, 16128, 16231, 16384, 16128, 16368, 16167, 16384, 16149, 16384, 16128, 16128, 16294, 16128, 16193, 16128, 16128, 16268, 16384, 16145, 16222, 16300, 16151, 16384, 16384, 16137, 16128, 16351, 16262, 16314, 16345, 16256, 16359, 16151, 16197, 16285, 16299, 16273, 16263, 16128, 16137, 16384, 16128, 16195, 16312, 16128, 16234, 16256, 16213, 16384, 16128, 16191, 16175, 16128, 16192, 16384, 16281, 16384, 16184, 16161, 16304, 16128, 16132, 16247, 16384, 16291, 16384, 16340, 16128, 16298, 16287, 16133, 16186, 16279, 16129, 16226, 16128, 16203, 16384, 16130, 16231, 16128, 16207, 16150, 16128, 16160, 16328, 16384, 16193, 16216, 16205, 16374, 16158, 16128, 16304, 16240, 16128, 16227, 16227, 16384, 16128, 16329, 16241, 16172, 16128, 16384, 16384, 16128, 16128, 16135, 16222, 16384, 16178, 16146, 16350, 16128, 16143, 16384, 16183, 16206, 16315, 16128, 16175, 16224, 16234, 16384, 16384, 16184, 16384, 16384, 16384, 16137, 16384, 16384, 16151, 16128, 16384, 16128, 16335, 16275, 16128, 16289, 16198, 16128, 16141, 16128, 16128, 16384, 16149, 16144, 16137, 16384, 16129, 16128, 16216, 16281, 16214, 16256, 16200, 16285, 16347, 16136, 16384, 16245, 16128, 16384, 16155, 16351, 16313, 16384, 16144, 16384, 16384, 16159, 16384, 16216}, + {16128, 16128, 16128, 16128, 16384, 16128, 16128, 16210, 16301, 16338, 16128, 16186, 16384, 16141, 16384, 16384, 16142, 16128, 16384, 16275, 16279, 16128, 16384, 16128, 16156, 16220, 16128, 16128, 16384, 16128, 16131, 16384, 16128, 16200, 16273, 16384, 16257, 16179, 16384, 16275, 16320, 16177, 16384, 16128, 16187, 16196, 16128, 16384, 16272, 16128, 16331, 16128, 16128, 16384, 16157, 16128, 16328, 16189, 16136, 16197, 16373, 16205, 16319, 16191, 16128, 16272, 16384, 16287, 16287, 16156, 16128, 16374, 16384, 16263, 16384, 16164, 16384, 16358, 16128, 16142, 16128, 16142, 16181, 16202, 16128, 16311, 16128, 16128, 16128, 16384, 16170, 16128, 16128, 16327, 16384, 16329, 16128, 16269, 16384, 16128, 16185, 16384, 16172, 16346, 16128, 16281, 16142, 16128, 16171, 16384, 16128, 16384, 16352, 16205, 16128, 16384, 16298, 16265, 16259, 16128, 16212, 16232, 16384, 16128, 16365, 16131, 16184, 16238, 16384, 16301, 16255, 16384, 16304, 16163, 16128, 16384, 16384, 16273, 16187, 16342, 16128, 16128, 16384, 16177, 16384, 16384, 16384, 16201, 16183, 16349, 16128, 16243, 16280, 16135, 16260, 16128, 16128, 16298, 16384, 16128, 16384, 16194, 16384, 16151, 16384, 16181, 16243, 16280, 16232, 16210, 16128, 16384, 16169, 16128, 16240, 16237, 16291, 16353, 16128, 16271, 16384, 16380, 16156, 16128, 16277, 16384, 16128, 16128, 16128, 16384, 16265, 16304, 16128, 16131, 16285, 16128, 16128, 16128, 16128, 16128, 16384, 16336, 16286, 16384, 16384, 16128, 16273, 16128, 16128, 16384, 16384, 16274, 16158, 16173, 16128, 16140, 16204, 16384, 16128, 16384, 16128, 16384, 16208, 16384, 16384, 16198, 16128, 16299, 16181, 16187, 16128, 16128, 16344, 16212, 16283, 16293, 16128, 16384, 16384, 16128, 16384, 16384, 16174, 16384, 16128, 16278, 16150, 16242, 16128, 16384, 16384, 16201, 16198, 16222, 16148, 16128}, + {16128, 16128, 16128, 16145, 16384, 16175, 16128, 16384, 16128, 16384, 16223, 16327, 16128, 16384, 16307, 16145, 16247, 16128, 16315, 16181, 16128, 16128, 16155, 16238, 16361, 16128, 16257, 16318, 16160, 16128, 16311, 16366, 16248, 16185, 16128, 16128, 16329, 16277, 16128, 16155, 16318, 16384, 16384, 16314, 16284, 16138, 16384, 16260, 16155, 16128, 16157, 16274, 16219, 16164, 16128, 16196, 16367, 16384, 16128, 16128, 16384, 16128, 16302, 16133, 16128, 16376, 16353, 16278, 16248, 16259, 16128, 16384, 16384, 16128, 16384, 16128, 16128, 16158, 16384, 16259, 16131, 16200, 16180, 16128, 16384, 16140, 16128, 16129, 16128, 16308, 16128, 16169, 16168, 16351, 16355, 16187, 16161, 16128, 16304, 16128, 16128, 16128, 16384, 16384, 16300, 16202, 16384, 16384, 16164, 16384, 16128, 16384, 16384, 16160, 16128, 16128, 16384, 16384, 16128, 16148, 16272, 16384, 16324, 16137, 16384, 16384, 16128, 16316, 16151, 16360, 16128, 16384, 16128, 16194, 16128, 16128, 16150, 16128, 16278, 16219, 16128, 16378, 16261, 16128, 16128, 16206, 16230, 16235, 16144, 16316, 16128, 16308, 16384, 16384, 16167, 16351, 16384, 16384, 16384, 16384, 16194, 16128, 16303, 16191, 16384, 16174, 16257, 16261, 16384, 16246, 16128, 16384, 16384, 16152, 16128, 16384, 16170, 16128, 16384, 16246, 16128, 16384, 16152, 16128, 16384, 16164, 16270, 16128, 16345, 16271, 16384, 16280, 16190, 16174, 16134, 16271, 16272, 16384, 16257, 16384, 16384, 16288, 16384, 16384, 16384, 16128, 16271, 16128, 16384, 16219, 16139, 16128, 16384, 16128, 16156, 16134, 16172, 16384, 16211, 16182, 16131, 16384, 16170, 16384, 16128, 16153, 16156, 16220, 16128, 16156, 16135, 16384, 16168, 16128, 16302, 16200, 16252, 16272, 16230, 16288, 16293, 16128, 16330, 16223, 16384, 16284, 16128, 16369, 16128, 16286, 16235, 16384, 16178, 16201, 16128, 16128}, + }, + { + {16128, 16128, 16128, 16128, 16384, 16128, 16286, 16136, 16306, 16128, 16384, 16128, 16384, 16384, 16135, 16176, 16384, 16171, 16144, 16155, 16148, 16128, 16167, 16248, 16373, 16128, 16188, 16298, 16151, 16355, 16384, 16144, 16189, 16220, 16268, 16220, 16257, 16128, 16128, 16384, 16215, 16384, 16384, 16347, 16220, 16187, 16128, 16384, 16384, 16384, 16150, 16128, 16384, 16269, 16330, 16253, 16225, 16384, 16346, 16384, 16384, 16128, 16253, 16252, 16175, 16128, 16139, 16131, 16362, 16257, 16143, 16261, 16269, 16128, 16384, 16345, 16384, 16177, 16184, 16277, 16384, 16128, 16286, 16384, 16128, 16384, 16196, 16384, 16132, 16384, 16128, 16128, 16128, 16128, 16333, 16273, 16260, 16128, 16211, 16128, 16166, 16317, 16174, 16370, 16327, 16151, 16384, 16372, 16128, 16161, 16384, 16178, 16384, 16262, 16128, 16128, 16128, 16347, 16195, 16226, 16128, 16128, 16128, 16194, 16128, 16237, 16384, 16370, 16232, 16235, 16128, 16384, 16128, 16287, 16128, 16128, 16153, 16128, 16280, 16219, 16128, 16128, 16384, 16227, 16263, 16384, 16128, 16134, 16384, 16233, 16276, 16199, 16313, 16295, 16139, 16128, 16218, 16327, 16164, 16154, 16289, 16384, 16137, 16259, 16128, 16240, 16153, 16128, 16384, 16264, 16128, 16384, 16384, 16155, 16128, 16384, 16172, 16128, 16196, 16128, 16235, 16162, 16384, 16384, 16246, 16128, 16266, 16312, 16377, 16384, 16218, 16235, 16128, 16235, 16384, 16384, 16176, 16128, 16315, 16384, 16384, 16371, 16270, 16384, 16128, 16384, 16384, 16193, 16128, 16175, 16165, 16128, 16209, 16384, 16128, 16384, 16128, 16128, 16239, 16314, 16128, 16297, 16169, 16141, 16150, 16128, 16128, 16384, 16128, 16237, 16264, 16384, 16219, 16302, 16263, 16128, 16384, 16384, 16198, 16165, 16291, 16128, 16167, 16128, 16384, 16321, 16133, 16292, 16128, 16328, 16219, 16256, 16384, 16384, 16283, 16128}, + {16128, 16128, 16128, 16384, 16174, 16191, 16128, 16371, 16384, 16128, 16276, 16343, 16128, 16384, 16284, 16130, 16136, 16128, 16366, 16270, 16258, 16128, 16312, 16128, 16158, 16209, 16128, 16128, 16138, 16170, 16128, 16146, 16134, 16384, 16131, 16136, 16188, 16146, 16384, 16128, 16128, 16384, 16292, 16128, 16128, 16384, 16208, 16128, 16278, 16284, 16157, 16152, 16384, 16239, 16384, 16128, 16272, 16128, 16194, 16384, 16128, 16128, 16325, 16128, 16170, 16128, 16137, 16132, 16297, 16128, 16128, 16254, 16307, 16128, 16384, 16346, 16128, 16307, 16128, 16165, 16384, 16128, 16201, 16384, 16128, 16301, 16179, 16128, 16384, 16384, 16291, 16149, 16273, 16128, 16384, 16283, 16128, 16191, 16384, 16162, 16272, 16319, 16143, 16160, 16303, 16153, 16384, 16384, 16384, 16128, 16133, 16128, 16178, 16384, 16299, 16128, 16128, 16384, 16128, 16128, 16359, 16240, 16128, 16384, 16128, 16343, 16137, 16326, 16257, 16147, 16341, 16194, 16133, 16210, 16128, 16128, 16384, 16138, 16384, 16384, 16384, 16131, 16384, 16191, 16216, 16384, 16128, 16145, 16234, 16384, 16128, 16187, 16274, 16129, 16260, 16128, 16384, 16384, 16384, 16384, 16216, 16272, 16276, 16187, 16128, 16277, 16128, 16182, 16157, 16174, 16221, 16253, 16128, 16384, 16384, 16384, 16141, 16128, 16384, 16274, 16128, 16347, 16137, 16128, 16283, 16128, 16346, 16332, 16211, 16384, 16140, 16384, 16384, 16189, 16128, 16151, 16215, 16384, 16215, 16345, 16384, 16151, 16215, 16266, 16160, 16384, 16384, 16146, 16384, 16256, 16173, 16128, 16384, 16133, 16134, 16134, 16384, 16128, 16384, 16275, 16277, 16128, 16384, 16206, 16384, 16141, 16128, 16128, 16179, 16166, 16128, 16128, 16384, 16379, 16263, 16286, 16255, 16384, 16384, 16384, 16230, 16220, 16128, 16180, 16271, 16194, 16199, 16171, 16128, 16262, 16128, 16139, 16181, 16272, 16128, 16166}, + {16128, 16128, 16128, 16334, 16384, 16163, 16384, 16210, 16297, 16133, 16384, 16165, 16384, 16170, 16384, 16384, 16136, 16310, 16133, 16329, 16319, 16146, 16128, 16204, 16384, 16145, 16207, 16337, 16384, 16157, 16128, 16287, 16156, 16243, 16301, 16184, 16271, 16128, 16384, 16219, 16128, 16178, 16384, 16293, 16214, 16129, 16135, 16128, 16314, 16270, 16128, 16384, 16151, 16151, 16224, 16370, 16177, 16128, 16291, 16149, 16255, 16128, 16270, 16264, 16276, 16128, 16233, 16128, 16328, 16325, 16184, 16367, 16176, 16128, 16365, 16135, 16384, 16384, 16128, 16134, 16141, 16128, 16173, 16230, 16384, 16384, 16154, 16128, 16243, 16384, 16195, 16226, 16128, 16128, 16296, 16202, 16128, 16329, 16384, 16154, 16250, 16265, 16136, 16184, 16294, 16128, 16384, 16364, 16128, 16128, 16133, 16143, 16384, 16131, 16151, 16169, 16137, 16172, 16384, 16384, 16130, 16384, 16157, 16236, 16171, 16384, 16128, 16300, 16144, 16128, 16179, 16128, 16179, 16355, 16185, 16259, 16193, 16128, 16384, 16309, 16128, 16128, 16384, 16216, 16267, 16384, 16128, 16128, 16192, 16358, 16128, 16267, 16201, 16337, 16168, 16245, 16177, 16376, 16128, 16252, 16384, 16206, 16384, 16172, 16128, 16355, 16128, 16223, 16243, 16307, 16215, 16384, 16128, 16128, 16141, 16303, 16285, 16384, 16128, 16384, 16384, 16384, 16193, 16128, 16174, 16257, 16128, 16274, 16237, 16384, 16141, 16384, 16371, 16250, 16128, 16208, 16384, 16128, 16128, 16232, 16384, 16370, 16128, 16128, 16159, 16384, 16384, 16134, 16384, 16384, 16128, 16128, 16199, 16357, 16128, 16353, 16129, 16384, 16128, 16324, 16384, 16131, 16384, 16219, 16169, 16128, 16160, 16384, 16257, 16295, 16326, 16231, 16145, 16155, 16384, 16206, 16128, 16195, 16128, 16305, 16293, 16128, 16384, 16209, 16128, 16384, 16181, 16129, 16128, 16287, 16128, 16157, 16384, 16128, 16331, 16255}, + {16128, 16128, 16128, 16384, 16211, 16384, 16384, 16128, 16384, 16328, 16128, 16128, 16128, 16128, 16128, 16179, 16241, 16128, 16384, 16145, 16264, 16166, 16356, 16142, 16128, 16128, 16383, 16128, 16384, 16128, 16128, 16339, 16384, 16128, 16128, 16159, 16264, 16188, 16128, 16128, 16384, 16327, 16128, 16384, 16363, 16384, 16153, 16128, 16384, 16128, 16384, 16384, 16128, 16128, 16173, 16384, 16384, 16384, 16128, 16128, 16384, 16128, 16284, 16209, 16152, 16128, 16128, 16128, 16279, 16128, 16128, 16229, 16128, 16128, 16384, 16384, 16128, 16239, 16128, 16167, 16384, 16128, 16278, 16128, 16384, 16151, 16238, 16384, 16128, 16384, 16384, 16151, 16141, 16128, 16384, 16135, 16349, 16128, 16256, 16353, 16384, 16128, 16295, 16160, 16128, 16271, 16384, 16213, 16128, 16196, 16384, 16201, 16384, 16130, 16128, 16142, 16352, 16384, 16128, 16129, 16384, 16166, 16160, 16128, 16161, 16163, 16334, 16330, 16168, 16128, 16143, 16128, 16196, 16128, 16280, 16384, 16312, 16384, 16180, 16158, 16173, 16273, 16128, 16284, 16209, 16384, 16128, 16129, 16128, 16384, 16384, 16325, 16265, 16271, 16128, 16128, 16128, 16193, 16128, 16128, 16128, 16222, 16128, 16261, 16384, 16384, 16128, 16384, 16210, 16249, 16214, 16384, 16207, 16128, 16163, 16213, 16128, 16355, 16142, 16278, 16183, 16172, 16384, 16384, 16138, 16249, 16128, 16307, 16128, 16128, 16128, 16313, 16152, 16181, 16384, 16128, 16261, 16289, 16205, 16384, 16384, 16372, 16128, 16128, 16162, 16384, 16384, 16128, 16384, 16384, 16155, 16219, 16384, 16384, 16144, 16128, 16262, 16128, 16384, 16384, 16149, 16128, 16128, 16264, 16310, 16179, 16128, 16264, 16163, 16128, 16384, 16273, 16128, 16128, 16384, 16230, 16384, 16384, 16216, 16185, 16257, 16180, 16128, 16170, 16384, 16290, 16128, 16346, 16128, 16384, 16384, 16208, 16128, 16128, 16384, 16384}, + }, + { + {16128, 16128, 16128, 16128, 16384, 16226, 16163, 16367, 16384, 16128, 16279, 16384, 16147, 16384, 16128, 16384, 16287, 16128, 16384, 16199, 16253, 16164, 16384, 16184, 16376, 16128, 16168, 16128, 16384, 16128, 16128, 16356, 16384, 16149, 16384, 16384, 16279, 16128, 16130, 16384, 16135, 16384, 16128, 16128, 16235, 16131, 16128, 16128, 16279, 16128, 16384, 16134, 16384, 16277, 16128, 16271, 16163, 16384, 16317, 16384, 16128, 16128, 16263, 16128, 16128, 16316, 16136, 16232, 16137, 16147, 16238, 16128, 16384, 16128, 16384, 16128, 16128, 16148, 16384, 16174, 16130, 16158, 16177, 16194, 16384, 16384, 16131, 16128, 16128, 16269, 16188, 16342, 16128, 16128, 16265, 16157, 16166, 16130, 16382, 16128, 16384, 16128, 16253, 16192, 16274, 16384, 16223, 16286, 16349, 16169, 16341, 16128, 16335, 16162, 16143, 16343, 16128, 16384, 16168, 16285, 16225, 16257, 16384, 16128, 16128, 16210, 16282, 16384, 16344, 16351, 16191, 16384, 16153, 16128, 16384, 16128, 16313, 16384, 16166, 16155, 16262, 16128, 16128, 16237, 16195, 16128, 16128, 16128, 16128, 16301, 16278, 16171, 16384, 16384, 16146, 16148, 16330, 16384, 16384, 16384, 16128, 16233, 16128, 16269, 16319, 16194, 16255, 16328, 16211, 16144, 16274, 16165, 16278, 16128, 16159, 16215, 16384, 16384, 16128, 16384, 16384, 16128, 16128, 16172, 16300, 16384, 16128, 16128, 16128, 16384, 16302, 16312, 16128, 16150, 16196, 16128, 16209, 16384, 16245, 16128, 16384, 16299, 16128, 16128, 16128, 16384, 16384, 16151, 16321, 16293, 16384, 16194, 16384, 16134, 16384, 16128, 16384, 16128, 16363, 16258, 16136, 16384, 16163, 16384, 16266, 16128, 16384, 16143, 16139, 16141, 16147, 16384, 16128, 16384, 16183, 16384, 16128, 16384, 16322, 16131, 16172, 16384, 16327, 16181, 16128, 16207, 16187, 16149, 16128, 16365, 16219, 16284, 16128, 16128, 16346, 16384}, + {16128, 16128, 16128, 16128, 16351, 16180, 16128, 16260, 16128, 16384, 16191, 16384, 16128, 16384, 16314, 16150, 16275, 16128, 16384, 16151, 16281, 16128, 16347, 16128, 16384, 16137, 16242, 16383, 16300, 16137, 16384, 16268, 16132, 16280, 16128, 16128, 16258, 16128, 16384, 16335, 16384, 16339, 16128, 16384, 16371, 16140, 16384, 16200, 16294, 16322, 16166, 16251, 16314, 16202, 16128, 16265, 16146, 16384, 16240, 16384, 16154, 16317, 16384, 16366, 16144, 16128, 16136, 16139, 16320, 16129, 16128, 16278, 16384, 16128, 16384, 16128, 16384, 16294, 16165, 16209, 16134, 16166, 16209, 16128, 16384, 16371, 16144, 16128, 16384, 16384, 16268, 16130, 16220, 16128, 16384, 16282, 16131, 16128, 16156, 16242, 16128, 16319, 16128, 16384, 16128, 16262, 16142, 16128, 16156, 16263, 16139, 16128, 16305, 16166, 16128, 16383, 16140, 16148, 16283, 16384, 16384, 16202, 16221, 16128, 16384, 16384, 16128, 16314, 16128, 16128, 16129, 16384, 16171, 16137, 16202, 16384, 16178, 16128, 16128, 16128, 16173, 16292, 16128, 16284, 16167, 16128, 16128, 16128, 16205, 16384, 16128, 16200, 16180, 16285, 16136, 16195, 16384, 16384, 16384, 16384, 16128, 16231, 16128, 16263, 16384, 16177, 16292, 16223, 16128, 16128, 16165, 16128, 16203, 16128, 16220, 16237, 16306, 16153, 16128, 16222, 16128, 16384, 16147, 16128, 16384, 16165, 16324, 16128, 16274, 16384, 16211, 16251, 16173, 16159, 16384, 16128, 16128, 16128, 16128, 16128, 16384, 16354, 16335, 16384, 16128, 16384, 16384, 16146, 16128, 16384, 16384, 16369, 16172, 16384, 16128, 16298, 16384, 16384, 16129, 16261, 16141, 16384, 16148, 16384, 16128, 16183, 16189, 16217, 16135, 16130, 16139, 16384, 16128, 16140, 16384, 16196, 16341, 16384, 16384, 16384, 16268, 16128, 16178, 16128, 16128, 16355, 16145, 16290, 16134, 16258, 16170, 16384, 16274, 16128, 16128, 16256}, + {16128, 16128, 16128, 16128, 16384, 16128, 16384, 16139, 16384, 16128, 16278, 16384, 16132, 16367, 16128, 16384, 16221, 16384, 16384, 16384, 16173, 16384, 16155, 16128, 16182, 16342, 16128, 16156, 16187, 16230, 16128, 16384, 16128, 16278, 16151, 16128, 16287, 16195, 16128, 16128, 16310, 16172, 16384, 16128, 16309, 16268, 16174, 16384, 16275, 16128, 16321, 16128, 16255, 16229, 16279, 16366, 16384, 16128, 16128, 16128, 16129, 16384, 16128, 16128, 16339, 16156, 16384, 16128, 16384, 16128, 16384, 16128, 16260, 16128, 16384, 16302, 16128, 16262, 16384, 16210, 16384, 16128, 16280, 16128, 16384, 16156, 16204, 16384, 16128, 16241, 16154, 16384, 16384, 16128, 16177, 16128, 16289, 16128, 16212, 16128, 16384, 16128, 16352, 16141, 16148, 16128, 16152, 16282, 16384, 16128, 16384, 16128, 16281, 16128, 16326, 16272, 16128, 16131, 16384, 16354, 16128, 16384, 16128, 16234, 16128, 16384, 16128, 16332, 16128, 16128, 16128, 16384, 16132, 16238, 16128, 16141, 16384, 16320, 16188, 16384, 16202, 16262, 16128, 16333, 16128, 16178, 16240, 16225, 16305, 16167, 16384, 16282, 16128, 16151, 16128, 16128, 16383, 16384, 16322, 16384, 16200, 16128, 16346, 16158, 16384, 16174, 16240, 16261, 16325, 16262, 16128, 16384, 16134, 16355, 16384, 16384, 16160, 16128, 16384, 16305, 16148, 16180, 16384, 16384, 16128, 16384, 16384, 16128, 16128, 16384, 16254, 16285, 16384, 16132, 16128, 16384, 16384, 16128, 16128, 16267, 16384, 16249, 16285, 16384, 16128, 16384, 16384, 16152, 16128, 16223, 16128, 16176, 16187, 16149, 16384, 16384, 16256, 16128, 16384, 16384, 16128, 16236, 16181, 16133, 16384, 16184, 16128, 16263, 16128, 16128, 16384, 16128, 16128, 16384, 16197, 16384, 16128, 16384, 16263, 16128, 16265, 16128, 16252, 16128, 16128, 16380, 16242, 16227, 16384, 16384, 16135, 16329, 16384, 16128, 16128, 16384}, + {16128, 16128, 16128, 16144, 16384, 16162, 16384, 16316, 16384, 16128, 16384, 16144, 16384, 16154, 16384, 16384, 16137, 16384, 16384, 16128, 16350, 16128, 16128, 16198, 16146, 16384, 16299, 16132, 16384, 16136, 16178, 16338, 16194, 16384, 16357, 16238, 16310, 16173, 16140, 16155, 16291, 16166, 16384, 16128, 16339, 16384, 16135, 16128, 16269, 16281, 16167, 16203, 16384, 16267, 16128, 16203, 16129, 16384, 16223, 16384, 16128, 16384, 16128, 16128, 16128, 16245, 16128, 16171, 16166, 16384, 16182, 16206, 16128, 16128, 16384, 16384, 16128, 16223, 16384, 16210, 16384, 16128, 16262, 16384, 16384, 16143, 16228, 16384, 16307, 16203, 16384, 16360, 16299, 16128, 16384, 16202, 16133, 16128, 16139, 16200, 16243, 16249, 16128, 16138, 16147, 16128, 16152, 16247, 16128, 16128, 16128, 16142, 16170, 16188, 16325, 16128, 16326, 16384, 16128, 16128, 16264, 16154, 16136, 16333, 16128, 16277, 16137, 16241, 16130, 16151, 16136, 16128, 16128, 16193, 16128, 16128, 16296, 16384, 16161, 16149, 16131, 16368, 16128, 16184, 16128, 16128, 16384, 16305, 16156, 16332, 16128, 16246, 16128, 16160, 16128, 16128, 16260, 16289, 16158, 16150, 16181, 16174, 16384, 16175, 16256, 16361, 16384, 16178, 16384, 16263, 16128, 16384, 16275, 16144, 16202, 16170, 16154, 16128, 16180, 16128, 16384, 16381, 16169, 16128, 16128, 16203, 16128, 16281, 16384, 16257, 16384, 16282, 16208, 16168, 16173, 16251, 16209, 16128, 16239, 16225, 16384, 16322, 16193, 16351, 16128, 16384, 16384, 16148, 16128, 16384, 16384, 16224, 16384, 16384, 16133, 16128, 16128, 16128, 16334, 16242, 16128, 16384, 16128, 16384, 16128, 16161, 16185, 16209, 16159, 16204, 16128, 16128, 16137, 16128, 16243, 16359, 16128, 16151, 16128, 16128, 16163, 16324, 16297, 16166, 16384, 16321, 16128, 16269, 16128, 16263, 16128, 16128, 16128, 16258, 16384, 16215}, + }, +}; diff --git a/src/ops/kernel/gqa_isoquant_row_scale.cuh b/src/ops/kernel/gqa_isoquant_row_scale.cuh new file mode 100644 index 0000000000..77a2f57529 --- /dev/null +++ b/src/ops/kernel/gqa_isoquant_row_scale.cuh @@ -0,0 +1,31 @@ +#pragma once + +// Sinkhorn-constrained row scales for the rotated NVFP4 K domain. +// +// For every full-attention (layer, kv_head), a token-independent per-channel +// scale s_d in [0.5, 2.0] balances rotated K row RMS before E4M3/E2M1 +// quantization. K is multiplied by s_d on cache write; Q is multiplied by +// 1/s_d before QK, so QK^T is preserved. This mainly protects low-energy +// channels whose E4M3 group scale would otherwise collapse to denormals. +// +// The table is baked from kvcalib-a and stored as BF16 words in constant +// memory (16 * 4 * 256 * 2 = 32 KiB, together with the SO(4) rotation table). + +#include + +#include + +extern __constant__ unsigned short kGqaKvRowScaleDev[16][4][256]; + +namespace ninfer::ops { + +__device__ __forceinline__ float gqa_kv_row_scale(int layer, int kv_head, int d) { + const unsigned short raw = ::kGqaKvRowScaleDev[layer][kv_head][d]; + return __bfloat162float(*reinterpret_cast(&raw)); +} + +__device__ __forceinline__ float gqa_kv_row_scale_inv(int layer, int kv_head, int d) { + return 1.0f / gqa_kv_row_scale(layer, kv_head, d); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/paged_kv_address.cuh b/src/ops/kernel/paged_kv_address.cuh index ecd756ddf8..3c9ebd234d 100644 --- a/src/ops/kernel/paged_kv_address.cuh +++ b/src/ops/kernel/paged_kv_address.cuh @@ -1,43 +1,43 @@ -#pragma once - -#include "core/paged_kv_cache.h" - -#include - -namespace ninfer::ops { - -inline constexpr int kPagedKVPageShift = 6; -inline constexpr int kPagedKVPageMask = kPagedKVPageSize - 1; - -static_assert(kPagedKVPageSize == (1 << kPagedKVPageShift)); - -__device__ __forceinline__ std::int32_t paged_kv_physical_page(const std::int32_t* block_table, - std::int32_t position) { - return block_table[position >> kPagedKVPageShift]; -} - -template -__device__ __forceinline__ std::int64_t paged_kv_page_head_offset(std::int32_t physical_page, - std::int32_t head) { - return static_cast(LeadingExtent) * kPagedKVPageSize * - (static_cast(head) + - static_cast(HeadExtent) * physical_page); -} - -template -__device__ __forceinline__ std::int64_t -paged_kv_element_offset(std::int32_t physical_page, std::int32_t head, std::int32_t page_offset, - std::int32_t leading) { - return paged_kv_page_head_offset(physical_page, head) + - static_cast(LeadingExtent) * page_offset + leading; -} - -template -__device__ __forceinline__ std::int64_t -paged_kv_element_offset(const std::int32_t* block_table, std::int32_t head, std::int32_t position, - std::int32_t leading) { - return paged_kv_element_offset( - paged_kv_physical_page(block_table, position), head, position & kPagedKVPageMask, leading); -} - -} // namespace ninfer::ops +#pragma once + +#include "core/paged_kv_cache.h" + +#include + +namespace ninfer::ops { + +inline constexpr int kPagedKVPageShift = 6; +inline constexpr int kPagedKVPageMask = kPagedKVPageSize - 1; + +static_assert(kPagedKVPageSize == (1 << kPagedKVPageShift)); + +__device__ __forceinline__ std::int32_t paged_kv_physical_page(const std::int32_t* block_table, + std::int32_t position) { + return block_table[position >> kPagedKVPageShift]; +} + +template +__device__ __forceinline__ std::int64_t paged_kv_page_head_offset(std::int32_t physical_page, + std::int32_t head) { + return static_cast(LeadingExtent) * kPagedKVPageSize * + (static_cast(head) + + static_cast(HeadExtent) * physical_page); +} + +template +__device__ __forceinline__ std::int64_t +paged_kv_element_offset(std::int32_t physical_page, std::int32_t head, std::int32_t page_offset, + std::int32_t leading) { + return paged_kv_page_head_offset(physical_page, head) + + static_cast(LeadingExtent) * page_offset + leading; +} + +template +__device__ __forceinline__ std::int64_t +paged_kv_element_offset(const std::int32_t* block_table, std::int32_t head, std::int32_t position, + std::int32_t leading) { + return paged_kv_element_offset( + paged_kv_physical_page(block_table, position), head, position & kPagedKVPageMask, leading); +} + +} // namespace ninfer::ops diff --git a/src/ops/kernel/rope.cuh b/src/ops/kernel/rope.cuh index 73ed115d83..b3245bdcd2 100644 --- a/src/ops/kernel/rope.cuh +++ b/src/ops/kernel/rope.cuh @@ -14,7 +14,9 @@ namespace ninfer::ops { enum class RopeKernelMode : std::int32_t { Text1D, + Text1DYarn4, DflashText1D, + DflashText1DYarn4, TextMrope, Vision2D, }; @@ -56,6 +58,41 @@ static __device__ __constant__ double kDflashRopeInvFrequency[64] = { 1.28639694493697462e-07, }; +static __device__ __constant__ double kTextRopeYarn4InvFrequency[32] = { + 1.000000000e+00, 6.042963902e-01, 3.651741273e-01, 2.206734069e-01, 1.333521432e-01, + 8.058421878e-02, 4.869675252e-02, 2.942727176e-02, 1.778279410e-02, 1.074607828e-02, + 6.493816316e-03, 3.924189758e-03, 2.371373706e-03, 1.433012570e-03, 8.659643234e-04, + 4.742398227e-04, 2.569350599e-04, 1.373497451e-04, 7.217387404e-05, 3.707224982e-05, + 1.844922203e-05, 8.759770071e-06, 3.849816315e-06, 2.326430102e-06, 1.405853313e-06, + 8.495520822e-07, 5.133812566e-07, 3.102344402e-07, 1.874735523e-07, 1.132895909e-07, + 6.846049086e-08, 4.137042750e-08, +}; + +static __device__ __constant__ double kDflashRopeYarn4InvFrequency[64] = { + 1.00000000000000000e+00, 7.77365030238775789e-01, 6.04296390238132863e-01, + 4.69758881670649164e-01, 3.65174127254837722e-01, 2.83873596475875456e-01, + 2.20673406908458991e-01, 1.71543789634287902e-01, 1.33352143216332403e-01, + 1.03663292843769794e-01, 8.05842187761481865e-02, 6.26433536656885587e-02, + 4.86967525165863113e-02, 3.78551524925863012e-02, 2.94272717620928173e-02, + 2.28757320031839559e-02, 1.77827941003892293e-02, 1.38237222735789964e-02, + 1.07460782832131743e-02, 8.35362546957826163e-03, 6.49381631576211298e-03, + 5.04806571666747105e-03, 3.92418975848453627e-03, 3.05052789026702539e-03, + 2.37137370566165538e-03, 1.84342299240911056e-03, 1.43301257023696268e-03, + 1.11397385999480246e-03, 8.65964323360065387e-04, 6.73170382414498242e-04, + 5.23299114681494734e-04, 4.06794432108304740e-04, 3.16227766016837939e-04, + 2.45824406892019762e-04, 1.91095297497044048e-04, 1.48550801717277505e-04, + 1.15478198468945822e-04, 8.97687132447314224e-05, 6.97830584859866353e-05, + 5.42469093701132573e-05, 4.21696503428582224e-05, 3.27812115139345850e-05, + 2.54829674797934641e-05, 1.98095677855033870e-05, 1.53992652605949185e-05, + 1.19708503049572999e-05, 9.30572040929699043e-06, 7.23394162736674728e-06, + 5.62341325190349121e-06, 4.37144481261108992e-06, 3.39820832894255927e-06, + 2.64164832038609264e-06, 2.05352502645714607e-06, 1.59633854428794220e-06, + 1.24093776075171953e-06, 9.64661619911199141e-07, 7.49894209332455848e-07, + 5.82941534713607427e-07, 4.53158363760081793e-07, 3.52269465147310129e-07, + 2.73841963426436139e-07, 2.12875166179637264e-07, 1.65481709994318135e-07, + 1.28639694493697462e-07, +}; + static __device__ __constant__ float kVisionRopeInvFrequency[18] = { 1.000000000e+00F, 5.994842503e-01F, 3.593813664e-01F, 2.154434690e-01F, 1.291549665e-01F, 7.742636827e-02F, 4.641588834e-02F, 2.782559402e-02F, 1.668100537e-02F, 1.000000000e-02F, @@ -68,9 +105,13 @@ __device__ __forceinline__ void fixed_axis_frequency(int pair, int* axis, float* if constexpr (Mode == RopeKernelMode::Vision2D) { *axis = pair / 18; *frequency = kVisionRopeInvFrequency[pair % 18]; - } else if constexpr (Mode == RopeKernelMode::DflashText1D) { + } else if constexpr (Mode == RopeKernelMode::DflashText1D || + Mode == RopeKernelMode::DflashText1DYarn4) { *axis = 0; *frequency = static_cast(kDflashRopeInvFrequency[pair]); + } else if constexpr (Mode == RopeKernelMode::Text1DYarn4) { + *axis = 0; + *frequency = kTextRopeYarn4InvFrequency[pair]; } else { *axis = Mode == RopeKernelMode::TextMrope ? pair % 3 : 0; *frequency = kTextRopeInvFrequency[pair]; @@ -87,6 +128,30 @@ __device__ __forceinline__ void fixed_sincos(const std::int32_t* positions, int const double turns = angle * kInvTwoPi; const float reduced = static_cast(angle - nearbyint(turns) * kTwoPi); sincosf(reduced, sine, cosine); + } else if constexpr (Mode == RopeKernelMode::DflashText1DYarn4) { + constexpr double kInvTwoPi = 1.59154943091895336e-01; + constexpr double kTwoPi = 6.28318530717958648e+00; + constexpr float kYarn4AttentionScaling = 1.138629436111989f; + const double angle = + static_cast(positions[token]) * + static_cast(kDflashRopeYarn4InvFrequency[pair]); + const double turns = angle * kInvTwoPi; + const float reduced = static_cast(angle - nearbyint(turns) * kTwoPi); + sincosf(reduced, sine, cosine); + *sine *= kYarn4AttentionScaling; + *cosine *= kYarn4AttentionScaling; + } else if constexpr (Mode == RopeKernelMode::Text1DYarn4) { + constexpr double kInvTwoPi = 1.59154943091895336e-01; + constexpr double kTwoPi = 6.28318530717958648e+00; + constexpr float kYarn4AttentionScaling = 1.138629436111989f; + const double angle = + static_cast(positions[token]) * + static_cast(kTextRopeYarn4InvFrequency[pair]); + const double turns = angle * kInvTwoPi; + const float reduced = static_cast(angle - nearbyint(turns) * kTwoPi); + sincosf(reduced, sine, cosine); + *sine *= kYarn4AttentionScaling; + *cosine *= kYarn4AttentionScaling; } else { int axis = 0; float frequency; @@ -120,10 +185,12 @@ __global__ void rope_fixed_kernel(const std::int32_t* positions, __nv_bfloat16* std::int64_t k_token_stride) { constexpr int kHeadDim = Mode == RopeKernelMode::Vision2D ? 72 : Mode == RopeKernelMode::DflashText1D ? 128 - : 256; + : Mode == RopeKernelMode::DflashText1DYarn4 ? 128 + : 256; constexpr int kHalf = Mode == RopeKernelMode::Vision2D ? 36 : Mode == RopeKernelMode::DflashText1D ? 64 - : 32; + : Mode == RopeKernelMode::DflashText1DYarn4 ? 64 + : 32; const int token = static_cast(blockIdx.x); if (token >= tokens) { return; } diff --git a/src/ops/kv_cache/append/kernel.cuh b/src/ops/kv_cache/append/kernel.cuh index 7ea9aebfb0..07a0f217c9 100644 --- a/src/ops/kv_cache/append/kernel.cuh +++ b/src/ops/kv_cache/append/kernel.cuh @@ -337,7 +337,6 @@ inline constexpr int kKVCacheAppendPrefixHeadDim = 128; inline constexpr int kKVCacheAppendPrefixHeads = 8; inline constexpr int kKVCacheAppendPrefixWindow = 4096; inline constexpr int kKVCacheAppendPrefixPage = 64; - __device__ __forceinline__ void kv_cache_append_prefix_copy_cyclic_unit( const __nv_bfloat16* __restrict__ k, const __nv_bfloat16* __restrict__ v, __nv_bfloat16* __restrict__ cache_k, __nv_bfloat16* __restrict__ cache_v, int token, @@ -389,6 +388,7 @@ __device__ __forceinline__ void kv_cache_append_prefix_copy_paged_unit( *reinterpret_cast(&cache_v[dst + 8]) = v1; } +template __global__ void kv_cache_append_prefix_cyclic_kernel( const __nv_bfloat16* __restrict__ k, const __nv_bfloat16* __restrict__ v, const std::int32_t* __restrict__ positions, const std::int32_t* __restrict__ counts, @@ -419,7 +419,7 @@ __global__ void kv_cache_append_prefix_cyclic_kernel( const int token = static_cast(blockIdx.x) * TokensPerBlock + local_token; if (token >= count) return; const int position = positions[token]; - const int slot = position & (kKVCacheAppendPrefixWindow - 1); + const int slot = position & (Window - 1); kv_cache_append_prefix_copy_cyclic_unit(k, v, cache_k, cache_v, token, unit_in_token, slot, padded_capacity); } diff --git a/src/ops/kv_cache/append/kv_cache_append.cpp b/src/ops/kv_cache/append/kv_cache_append.cpp index fdfcbb36db..c27b473ee9 100644 --- a/src/ops/kv_cache/append/kv_cache_append.cpp +++ b/src/ops/kv_cache/append/kv_cache_append.cpp @@ -151,8 +151,11 @@ void validate_paged_cache(const PagedKVBatchLayerView& cache, } void validate_cyclic_cache(const CyclicKVCacheLayerView& cache, - KVCacheAppendPrefixExecutionEnvelope envelope) { - if (cache.num_kv_heads != kKVHeads || cache.head_dim != kHeadDim || cache.capacity != kWindow || + KVCacheAppendPrefixExecutionEnvelope envelope, + std::uint32_t window) { + if (cache.num_kv_heads != kKVHeads || cache.head_dim != kHeadDim || + cache.capacity != window || + (window != 2048 && window != 4096) || cache.padded_capacity < cache.capacity || cache.padded_capacity > static_cast(std::numeric_limits::max()) || @@ -212,10 +215,12 @@ void kv_cache_append_prefix(const Tensor& k, const Tensor& v, const Tensor& posi void kv_cache_append_prefix(const Tensor& k, const Tensor& v, const Tensor& positions, const Tensor& counts, const Tensor& lanes, KVCacheAppendPrefixExecutionEnvelope envelope, - CyclicKVCacheLayerView cache, cudaStream_t stream) { + CyclicKVCacheLayerView cache, std::uint32_t window, + cudaStream_t stream) { const auto plan = validate_inputs(k, v, positions, counts, lanes, envelope); - validate_cyclic_cache(cache, envelope); - detail::kv_cache_append_prefix_launch(k, v, positions, counts, lanes, cache, plan, stream); + validate_cyclic_cache(cache, envelope, window); + detail::kv_cache_append_prefix_launch(k, v, positions, counts, lanes, cache, plan, window, + stream); } } // namespace ninfer::ops diff --git a/src/ops/kv_cache/append/launch.cu b/src/ops/kv_cache/append/launch.cu index 811941d135..087575c6d3 100644 --- a/src/ops/kv_cache/append/launch.cu +++ b/src/ops/kv_cache/append/launch.cu @@ -128,7 +128,8 @@ void launch_paged(const Tensor& k, const Tensor& v, const Tensor& positions, con void launch_cyclic(const Tensor& k, const Tensor& v, const Tensor& positions, const Tensor& counts, const Tensor& lanes, CyclicKVCacheLayerView cache, - const KVCacheAppendPrefixPlan& plan, cudaStream_t stream) { + const KVCacheAppendPrefixPlan& plan, std::uint32_t window, + cudaStream_t stream) { validate_plan(k, plan); if (plan.max_count == 0) return; auto* cache_k = static_cast<__nv_bfloat16*>(cache.k.data); @@ -141,10 +142,22 @@ void launch_cyclic(const Tensor& k, const Tensor& v, const Tensor& positions, co const int padded = static_cast(cache.padded_capacity); const dim3 grid(1 + (plan.max_count - 1) / 4, k.ne[3], 1); - kv_cache_append_prefix_cyclic_kernel<<>>( - input_k, input_v, pos, count, lane, cache_k, cache_v, plan.min_count, plan.max_count, - plan.tokens, padded); - CUDA_CHECK(cudaGetLastError()); + const auto launch_window = [&]() { + kv_cache_append_prefix_cyclic_kernel<<>>( + input_k, input_v, pos, count, lane, cache_k, cache_v, plan.min_count, plan.max_count, + plan.tokens, padded); + CUDA_CHECK(cudaGetLastError()); + }; + switch (window) { + case 2048: + launch_window.template operator()<2048>(); + break; + case 4096: + launch_window.template operator()<4096>(); + break; + default: + throw std::invalid_argument("kv_cache_append_prefix: unsupported cyclic window"); + } } } // namespace @@ -211,8 +224,9 @@ void kv_cache_append_prefix_launch(const Tensor& k, const Tensor& v, const Tenso void kv_cache_append_prefix_launch(const Tensor& k, const Tensor& v, const Tensor& positions, const Tensor& counts, const Tensor& lanes, CyclicKVCacheLayerView cache, - const KVCacheAppendPrefixPlan& plan, cudaStream_t stream) { - launch_cyclic(k, v, positions, counts, lanes, cache, plan, stream); + const KVCacheAppendPrefixPlan& plan, std::uint32_t window, + cudaStream_t stream) { + launch_cyclic(k, v, positions, counts, lanes, cache, plan, window, stream); } } // namespace ninfer::ops::detail diff --git a/src/ops/kv_cache/append/launch.h b/src/ops/kv_cache/append/launch.h index 250b382af3..12c7fbd665 100644 --- a/src/ops/kv_cache/append/launch.h +++ b/src/ops/kv_cache/append/launch.h @@ -28,6 +28,7 @@ void kv_cache_append_prefix_launch(const Tensor& k, const Tensor& v, const Tenso void kv_cache_append_prefix_launch(const Tensor& k, const Tensor& v, const Tensor& positions, const Tensor& counts, const Tensor& lanes, CyclicKVCacheLayerView cache, - const KVCacheAppendPrefixPlan& plan, cudaStream_t stream); + const KVCacheAppendPrefixPlan& plan, std::uint32_t window, + cudaStream_t stream); } // namespace ninfer::ops::detail diff --git a/src/ops/launcher/bidirectional_gqa_attention.cu b/src/ops/launcher/bidirectional_gqa_attention.cu new file mode 100644 index 0000000000..41c7831b48 --- /dev/null +++ b/src/ops/launcher/bidirectional_gqa_attention.cu @@ -0,0 +1,185 @@ +#include "ops/launcher/bidirectional_gqa_attention.h" + +#include "core/device.h" +#include "ops/kernel/bidirectional_gqa_attention.cuh" + +#include +#include +#include + +namespace ninfer::ops::detail { +namespace { + +template +void dispatch_token_case(Launch&& launch) { + constexpr int Warps = (Tokens + 3) / 4; + launch.template operator()(); +} + +template +void dispatch_tokens(std::int32_t tokens, Launch&& launch) { + switch (tokens) { +#define NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(TOKENS) \ + case TOKENS: \ + dispatch_token_case(launch); \ + return + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(1); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(2); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(3); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(4); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(5); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(6); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(7); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(8); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(9); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(10); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(11); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(12); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(13); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(14); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(15); + NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE(16); +#undef NINFER_BIDIRECTIONAL_GQA_TOKEN_CASE + default: + throw std::invalid_argument("bidirectional_gqa_attention: unsupported T"); + } +} + +} // namespace + +BidirectionalGqaPlan bidirectional_gqa_resolve_plan(std::int32_t tokens, + GqaContextExecutionEnvelope envelope) { + if (tokens < 1 || tokens > 16) { + throw std::invalid_argument("bidirectional_gqa_attention plan: T must be 1..16"); + } + if (envelope.min_context > envelope.max_context) { + throw std::invalid_argument("bidirectional_gqa_attention plan: invalid envelope"); + } + const std::int32_t warps = (tokens + 3) / 4; + const bool direct = envelope.max_context == 0; + const std::int32_t key_block = + direct || tokens <= 8 || envelope.max_context <= 65536u ? 32 : 64; + std::int32_t split_limit = 32; + if (tokens <= 8) { + split_limit = + envelope.max_context <= 131072u ? 32 : (envelope.max_context <= 196608u ? 48 : 64); + } else if (key_block == 64) { + split_limit = + envelope.max_context <= 131072u ? 32 : (envelope.max_context <= 196608u ? 38 : 40); + } + const std::uint32_t envelope_tiles = + (envelope.max_context + static_cast(key_block) - 1u) / + static_cast(key_block); + const std::int32_t splits = + direct ? 1 : std::min(split_limit, std::max(1, static_cast(envelope_tiles))); + return { + .route = direct ? BidirectionalGqaRoute::Direct : BidirectionalGqaRoute::SplitKv, + .tokens = tokens, + .warps = warps, + .key_block = key_block, + .split_capacity = splits, + }; +} + +const char* bidirectional_gqa_route_name(BidirectionalGqaRoute route) { + switch (route) { + case BidirectionalGqaRoute::Direct: + return "direct"; + case BidirectionalGqaRoute::SplitKv: + return "split_kv"; + } + return "unknown"; +} + +void bidirectional_gqa_attention_launch(const Tensor& q, const Tensor& query_k, + const Tensor& query_v, const Tensor& context_lengths, + const Tensor& valid_columns, const Tensor& table_rows, + float scale, const PagedKVBatchLayerView& context, + const BidirectionalGqaPlan& plan, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream) { + dispatch_tokens(q.ne[2], [&]() { + const bool direct = plan.route == BidirectionalGqaRoute::Direct; + if (plan.warps != Warps || plan.split_capacity < 1 || + plan.split_capacity > kBidirectionalGqaMaxSplit) { + throw std::invalid_argument("bidirectional_gqa_attention: inconsistent plan"); + } + if (direct) { + if (plan.key_block != 32 || plan.split_capacity != 1) { + throw std::invalid_argument("bidirectional_gqa_attention: inconsistent plan"); + } + constexpr int KeyBlock = 32; + constexpr std::size_t SmemBytes = + 2u * KeyBlock * kBidirectionalGqaHeadDim * sizeof(__nv_bfloat16); + const dim3 direct_grid(kBidirectionalGqaKVHeads, 1, q.ne[3]); + bidirectional_gqa_split_partial_kernel + <<>>( + static_cast(q.data), + static_cast(query_k.data), + static_cast(query_v.data), + static_cast(context_lengths.data), + static_cast(valid_columns.data), + static_cast(table_rows.data), + static_cast(context.k_pages.data), + static_cast(context.v_pages.data), + static_cast(context.block_tables.data), + context.k_pages.ne[2], context.block_tables.ne[0], + context.block_tables.ne[0] * kPagedKVPageSize, 1, scale, + static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data), + static_cast<__nv_bfloat16*>(out.data)); + CUDA_CHECK(cudaGetLastError()); + return; + } + + if (plan.route != BidirectionalGqaRoute::SplitKv) { + throw std::invalid_argument("bidirectional_gqa_attention: inconsistent plan"); + } + + const auto launch_split = [&]() { + constexpr std::size_t SmemBytes = + 2u * KeyBlock * kBidirectionalGqaHeadDim * sizeof(__nv_bfloat16); + const dim3 partial_grid(kBidirectionalGqaKVHeads, plan.split_capacity, q.ne[3]); + bidirectional_gqa_split_partial_kernel + <<>>( + static_cast(q.data), + static_cast(query_k.data), + static_cast(query_v.data), + static_cast(context_lengths.data), + static_cast(valid_columns.data), + static_cast(table_rows.data), + static_cast(context.k_pages.data), + static_cast(context.v_pages.data), + static_cast(context.block_tables.data), + context.k_pages.ne[2], context.block_tables.ne[0], + context.block_tables.ne[0] * kPagedKVPageSize, plan.split_capacity, scale, + static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data), + static_cast<__nv_bfloat16*>(out.data)); + CUDA_CHECK(cudaGetLastError()); + const dim3 reduce_grid(kBidirectionalGqaQHeads, Tokens, q.ne[3]); + bidirectional_gqa_reduce_kernel<<>>( + static_cast(partial_acc.data), + static_cast(partial_m.data), + static_cast(partial_l.data), + static_cast(context_lengths.data), + static_cast(valid_columns.data), + context.block_tables.ne[0] * kPagedKVPageSize, plan.split_capacity, + static_cast<__nv_bfloat16*>(out.data)); + CUDA_CHECK(cudaGetLastError()); + }; + if (plan.key_block == 32) { + launch_split.template operator()<32>(); + return; + } + if constexpr (Tokens > 8) { + if (plan.key_block == 64) { + launch_split.template operator()<64>(); + return; + } + } + throw std::invalid_argument("bidirectional_gqa_attention: inconsistent plan"); + }); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/bidirectional_gqa_attention.h b/src/ops/launcher/bidirectional_gqa_attention.h new file mode 100644 index 0000000000..5828a451e2 --- /dev/null +++ b/src/ops/launcher/bidirectional_gqa_attention.h @@ -0,0 +1,33 @@ +#pragma once + +#include "ninfer/ops/bidirectional_gqa_attention.h" + +namespace ninfer::ops::detail { + +enum class BidirectionalGqaRoute { + Direct, + SplitKv, +}; + +struct BidirectionalGqaPlan { + BidirectionalGqaRoute route = BidirectionalGqaRoute::SplitKv; + std::int32_t tokens = 0; + std::int32_t warps = 0; + std::int32_t key_block = 0; + std::int32_t split_capacity = 0; +}; + +[[nodiscard]] BidirectionalGqaPlan +bidirectional_gqa_resolve_plan(std::int32_t tokens, GqaContextExecutionEnvelope envelope); + +[[nodiscard]] const char* bidirectional_gqa_route_name(BidirectionalGqaRoute route); + +void bidirectional_gqa_attention_launch(const Tensor& q, const Tensor& query_k, + const Tensor& query_v, const Tensor& context_lengths, + const Tensor& valid_columns, const Tensor& table_rows, + float scale, const PagedKVBatchLayerView& context, + const BidirectionalGqaPlan& plan, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/cold_i8.cu b/src/ops/launcher/cold_i8.cu new file mode 100644 index 0000000000..fb689e6926 --- /dev/null +++ b/src/ops/launcher/cold_i8.cu @@ -0,0 +1,87 @@ +#include "ops/launcher/cold_i8.h" + +#include "core/device.h" +#include "ops/kernel/cold_i8_kernels.cuh" + +#include + +#include + +namespace ninfer::ops::detail { + +// Kernel definitions live in this single TU: the shared header only +// declares them so attention kernels can include its device helpers +// without duplicate device-link definitions. +__global__ void cold_i8_slot_pack_kernel(const std::uint8_t* __restrict__ src_codes, + const std::uint8_t* __restrict__ src_scales, + int kv_heads, + std::uint8_t* __restrict__ slots, + std::int32_t* __restrict__ slot_valid, + int slot_bytes) { + const int head = static_cast(blockIdx.x); + const int page = static_cast(blockIdx.y); + const std::int64_t plane = static_cast(head) + + static_cast(kv_heads) * page; + const std::uint8_t* src_c = src_codes + plane * kColdI8SlotCodeBytes; + const std::uint8_t* src_s = src_scales + plane * kColdI8SlotScaleBytes; + std::uint8_t* slot = slots + plane * slot_bytes; + if (threadIdx.x == 0) { + *reinterpret_cast(slot) = kColdI8SlotMagic; + *reinterpret_cast(slot + 4) = 1; // version + *reinterpret_cast(slot + 6) = 1; // flags: valid + } + __syncthreads(); + for (int i = static_cast(threadIdx.x); i < kColdI8SlotCodeBytes; i += 256) { + slot[kColdI8SlotHeaderBytes + i] = src_c[i]; + } + for (int i = static_cast(threadIdx.x); i < kColdI8SlotScaleBytes; i += 256) { + slot[kColdI8SlotHeaderBytes + kColdI8SlotCodeBytes + i] = src_s[i]; + } + if (threadIdx.x == 0) { + slot_valid[plane] = 1; // fixed layout: always valid, no overflow path + } +} + +__global__ void cold_i8_slot_restore_kernel(const std::uint8_t* __restrict__ slots, + int kv_heads, std::int8_t* __restrict__ dst_codes, + __half* __restrict__ dst_scales, + int slot_bytes) { + const int head = static_cast(blockIdx.x); + const int page = static_cast(blockIdx.y); + const std::int64_t plane = static_cast(head) + + static_cast(kv_heads) * page; + const std::uint8_t* slot = slots + plane * slot_bytes; + std::int8_t* codes = dst_codes + plane * (64 * 256); + __half* scales = dst_scales + plane * (64 * 4); + const int row0 = static_cast(threadIdx.x) >> 2; // 64 rows + const int lane = static_cast(threadIdx.x) & 3; // 4 quarter-rows + if (lane != 0) { return; } + std::int8_t row_codes[256]; + __half row_scales[4]; + cold_i8_decode_row(slot, row0, row_codes, row_scales); +#pragma unroll + for (int g = 0; g < 4; ++g) { scales[row0 * 4 + g] = row_scales[g]; } +#pragma unroll + for (int d = 0; d < 256; ++d) { codes[row0 * 256 + d] = row_codes[d]; } +} + + +void cold_i8_slot_pack_launch(const std::uint8_t* src_codes, const std::uint8_t* src_scales, + int kv_heads, int page_count, std::uint8_t* slots, + std::int32_t* slot_valid, int slot_bytes, cudaStream_t stream) { + const dim3 grid(kv_heads, page_count); + cold_i8_slot_pack_kernel<<>>(src_codes, src_scales, kv_heads, slots, + slot_valid, slot_bytes); + CUDA_CHECK(cudaGetLastError()); +} + +void cold_i8_slot_restore_launch(const std::uint8_t* slots, int kv_heads, int page_count, + std::int8_t* dst_codes, void* dst_scales_fp16, + int slot_bytes, cudaStream_t stream) { + const dim3 grid(kv_heads, page_count); + cold_i8_slot_restore_kernel<<>>( + slots, kv_heads, dst_codes, static_cast<__half*>(dst_scales_fp16), slot_bytes); + CUDA_CHECK(cudaGetLastError()); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/cold_i8.h b/src/ops/launcher/cold_i8.h new file mode 100644 index 0000000000..ffeafd7fba --- /dev/null +++ b/src/ops/launcher/cold_i8.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include + +namespace ninfer::ops::detail { + +void cold_i8_slot_pack_launch(const std::uint8_t* src_codes, const std::uint8_t* src_scales, + int kv_heads, int page_count, std::uint8_t* slots, + std::int32_t* slot_valid, int slot_bytes, cudaStream_t stream); + +void cold_i8_slot_restore_launch(const std::uint8_t* slots, int kv_heads, int page_count, + std::int8_t* dst_codes, void* dst_scales_fp16, + int slot_bytes, cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/dflash2_grouped_conv.cu b/src/ops/launcher/dflash2_grouped_conv.cu new file mode 100644 index 0000000000..eb050cb9c8 --- /dev/null +++ b/src/ops/launcher/dflash2_grouped_conv.cu @@ -0,0 +1,29 @@ +#include "ops/launcher/dflash2_grouped_conv.h" + +#include "core/device.h" +#include "ops/kernel/dflash2_grouped_conv.cuh" + +#include +#include + +namespace ninfer::ops::detail { + +void dflash2_grouped_conv_launch(const Tensor& hidden, const Tensor& delta, const Weight& base, + std::int32_t block_size, std::int32_t group_size, + std::int32_t taps, std::int32_t side, Tensor& out, + cudaStream_t stream) { + constexpr int kBlock = 256; + const std::int32_t hidden_size = hidden.ne[0]; + const std::int32_t tokens = hidden.ne[1]; + const std::int32_t groups = hidden_size / group_size; + const int grid = static_cast( + std::max(1, (static_cast(hidden_size) + kBlock - 1) / kBlock)); + dflash2_grouped_conv_kernel<<>>( + static_cast(hidden.data), + static_cast(delta.data), + static_cast(base.qdata), static_cast<__nv_bfloat16*>(out.data), + hidden_size, tokens, taps, groups, group_size, block_size, side); + CUDA_CHECK(cudaGetLastError()); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/dflash2_grouped_conv.h b/src/ops/launcher/dflash2_grouped_conv.h new file mode 100644 index 0000000000..4b9c9a28be --- /dev/null +++ b/src/ops/launcher/dflash2_grouped_conv.h @@ -0,0 +1,14 @@ +#pragma once + +#include "core/tensor.h" + +#include + +namespace ninfer::ops::detail { + +void dflash2_grouped_conv_launch(const Tensor& hidden, const Tensor& delta, const Weight& base, + std::int32_t block_size, std::int32_t group_size, + std::int32_t taps, std::int32_t side, Tensor& out, + cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/dflash2_selector.cu b/src/ops/launcher/dflash2_selector.cu new file mode 100644 index 0000000000..ea19d055b0 --- /dev/null +++ b/src/ops/launcher/dflash2_selector.cu @@ -0,0 +1,48 @@ +#include "ops/launcher/dflash2_selector.h" + +#include "core/device.h" +#include "ops/kernel/dflash2_selector.cuh" + +#include +#include + +namespace ninfer::ops::detail { + +void dflash2_selector_launch(const Tensor& unary_logits, const Tensor& projected_hidden, + const Weight& predecessor_codebook, + const Weight& successor_codebook, const Tensor& anchors, + Tensor& candidates, Tensor& unary, Tensor& scores, Tensor& drafts, + std::int32_t steps, std::int32_t top_k, cudaStream_t stream) { + if (steps != 7 || top_k != kDflash2SelectorTopK) { + throw std::invalid_argument("dflash2_selector: registered domain is S=7, K=16"); + } + const std::int32_t batch = anchors.ne[0]; + const std::int32_t columns = steps * batch; + constexpr int kTopK = kDflash2SelectorTopK; + + const dim3 topk_grid(columns); + dflash2_selector_topk_kernel<256, kTopK><<>>( + static_cast(unary_logits.data), + static_cast(candidates.data), static_cast(unary.data), + unary_logits.ne[0], batch, steps, columns); + CUDA_CHECK(cudaGetLastError()); + + const dim3 scores_grid(columns); + dflash2_selector_scores_kernel<256><<>>( + static_cast(candidates.data), + static_cast(unary.data), + static_cast(projected_hidden.data), + static_cast(predecessor_codebook.qdata), + static_cast(successor_codebook.qdata), + static_cast(anchors.data), static_cast(scores.data), + unary_logits.ne[0], batch, steps, top_k, columns); + CUDA_CHECK(cudaGetLastError()); + + dflash2_selector_walk_kernel<<>>( + static_cast(candidates.data), + static_cast(scores.data), static_cast(drafts.data), batch, + steps, top_k); + CUDA_CHECK(cudaGetLastError()); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/dflash2_selector.h b/src/ops/launcher/dflash2_selector.h new file mode 100644 index 0000000000..72bdae7cf1 --- /dev/null +++ b/src/ops/launcher/dflash2_selector.h @@ -0,0 +1,20 @@ +#pragma once + +#include "core/tensor.h" + +#include + +#include + +namespace ninfer::ops::detail { + +inline constexpr int kDflash2SelectorRank = 256; +inline constexpr int kDflash2SelectorTopK = 16; + +void dflash2_selector_launch(const Tensor& unary_logits, const Tensor& projected_hidden, + const Weight& predecessor_codebook, + const Weight& successor_codebook, const Tensor& anchors, + Tensor& candidates, Tensor& unary, Tensor& scores, Tensor& drafts, + std::int32_t steps, std::int32_t top_k, cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/entropy_cold_requant.cu b/src/ops/launcher/entropy_cold_requant.cu new file mode 100644 index 0000000000..ff669c981c --- /dev/null +++ b/src/ops/launcher/entropy_cold_requant.cu @@ -0,0 +1,20 @@ +#include "ops/launcher/entropy_cold_requant.h" + +#include "core/device.h" +#include "ops/kernel/entropy_cold_requant_kernels.cuh" + +#include + +namespace ninfer::ops::detail { + +void entropy_cold_requant_raw_launch(const std::uint8_t* src_codes, + const std::uint8_t* src_scales, ColdRequantSource mode, + int kv_heads, int page_count, std::uint8_t* dst_codes, + std::uint8_t* dst_scales, cudaStream_t stream) { + const dim3 grid(kv_heads, page_count); + entropy_cold_requant_kernel<<>>(src_codes, src_scales, mode, kv_heads, + dst_codes, dst_scales); + CUDA_CHECK(cudaGetLastError()); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/entropy_cold_requant.h b/src/ops/launcher/entropy_cold_requant.h new file mode 100644 index 0000000000..d45f984024 --- /dev/null +++ b/src/ops/launcher/entropy_cold_requant.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +#include + +namespace ninfer::ops::detail { + +enum class ColdRequantSource : int { + Nvfp4G16 = 0, // E2M1 nibbles + E4M3 g16 scales (K planes) + Int8G64 = 1, // int8 codes + fp16 g64 scales + Iso3VG16 = 2, // ISO3 sign-magnitude INT3 nibbles + E4M3 g16 scales (V planes) +}; + +void entropy_cold_requant_raw_launch(const std::uint8_t* src_codes, + const std::uint8_t* src_scales, ColdRequantSource mode, + int kv_heads, int page_count, std::uint8_t* dst_codes, + std::uint8_t* dst_scales, cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/entropy_nvfp4_slot.cu b/src/ops/launcher/entropy_nvfp4_slot.cu new file mode 100644 index 0000000000..de1839f7b6 --- /dev/null +++ b/src/ops/launcher/entropy_nvfp4_slot.cu @@ -0,0 +1,77 @@ +#include "ops/launcher/entropy_nvfp4_slot.h" + +#include "core/device.h" +#include "ops/kernel/entropy_nvfp4_slot_kernels.cuh" + +#include + +namespace ninfer::ops::detail { + +void entropy_nvfp4_slot_encode_launch(const std::uint8_t* codes, const std::uint8_t* scales, + int kv_heads, int page_count, std::uint8_t* slots, + int slot_bytes, std::int32_t* slot_valid, + const std::int32_t* page_ids, int valid_page_stride, + cudaStream_t stream) { + const dim3 grid(kv_heads, page_count); + entropy_nvfp4_slot_encode_kernel<<>>( + codes, scales, kv_heads, slots, slot_bytes, slot_valid, page_ids, valid_page_stride); + CUDA_CHECK(cudaGetLastError()); +} + +void entropy_nvfp4_slot_decode_grid_launch(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dst, cudaStream_t stream) { + const dim3 grid(2, kv_heads); + entropy_nvfp4_slot_decode_half_grid_kernel<<>>(slots, slot_bytes, slot_base, dst, + kEntropyNvfp4SlotHalfBytes); + CUDA_CHECK(cudaGetLastError()); +} + +void entropy_nvfp4_slot_scales_scatter_launch(const std::uint8_t* slots, int slot_bytes, + int slot_page_stride, int kv_heads, + int page_count, const std::int32_t* page_ids, + int scale_page_stride, std::uint8_t* scales, + cudaStream_t stream) { + const dim3 grid(kv_heads, page_count); + entropy_nvfp4_slot_scales_scatter_kernel<<>>( + slots, slot_bytes, slot_page_stride, page_ids, scale_page_stride, scales); + CUDA_CHECK(cudaGetLastError()); +} + +// Restore the full (page, kv_head) K|V plane from its entropy slot into the +// native NVFP4 page-major geometry. dec items (head*2+half) hold the 16 +// decoded streams; stream t covers rows (2t, 2t+1) of that half, so the +// grid copies each stream's two 128 B rows to the row-major code plane. +__global__ void entropy_nvfp4_slot_restore_plane_kernel( + const std::uint8_t* __restrict__ dec, int kv_heads, + std::uint8_t* __restrict__ dst_codes) { + const int head = static_cast(blockIdx.y); + const int half = static_cast(blockIdx.x); + const int stream = static_cast(threadIdx.x); + if (stream >= kEntropyNvfp4SlotStreamsPerHalf) { return; } + const std::uint8_t* item = + dec + static_cast(head * 2 + half) * kEntropyNvfp4SlotHalfBytes + + stream * kEntropyNvfp4SlotStreamBytes; + const int row0 = (half * kEntropyNvfp4SlotStreamsPerHalf + stream) * 2; + std::uint8_t* dst = + dst_codes + static_cast(head) * (64 * 128) + row0 * 128; +#pragma unroll + for (int i = 0; i < 128; ++i) { dst[i] = item[i]; } +#pragma unroll + for (int i = 0; i < 128; ++i) { dst[128 + i] = item[128 + i]; } +} + +void entropy_nvfp4_slot_restore_plane_launch(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dec_scratch, + std::uint8_t* dst_codes, cudaStream_t stream) { + entropy_nvfp4_slot_decode_grid_launch(slots, slot_bytes, slot_base, kv_heads, dec_scratch, + stream); + const dim3 grid(2, kv_heads); + entropy_nvfp4_slot_restore_plane_kernel<<>>( + dec_scratch, kv_heads, dst_codes); + CUDA_CHECK(cudaGetLastError()); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/entropy_nvfp4_slot.h b/src/ops/launcher/entropy_nvfp4_slot.h new file mode 100644 index 0000000000..80ef4d2500 --- /dev/null +++ b/src/ops/launcher/entropy_nvfp4_slot.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#include + +namespace ninfer::ops::detail { + +// Raw one-page convenience used by runtime owners that hold non-contiguous +// paged-cache slices. Pointers must address one or more (page, kv_head) planes +// with the same PageMajor strides as the Tensor form: page stride = +// kv_heads * 8192 (codes) / kv_heads * 1024 (scales) / slot_bytes (slots) / +// kv_heads (valid). page_count is the grid.y extent. +void entropy_nvfp4_slot_encode_launch(const std::uint8_t* codes, const std::uint8_t* scales, + int kv_heads, int page_count, std::uint8_t* slots, + int slot_bytes, std::int32_t* slot_valid, + const std::int32_t* page_ids, int valid_page_stride, + cudaStream_t stream); + +// Decodes all (kv_head, half) streams of one cold-page slot base into a +// contiguous buffer: item (head * 2 + half) holds half_bytes packed bytes. +void entropy_nvfp4_slot_decode_grid_launch(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dst, cudaStream_t stream); + +// Decodes one slot base and scatters the packed rows into the native +// NVFP4 page-major code plane (row-major [64 x 128 B] per kv_head). +// dec_scratch must hold kv_heads * 8192 bytes. +void entropy_nvfp4_slot_restore_plane_launch(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dec_scratch, + std::uint8_t* dst_codes, cudaStream_t stream); + +// Scatters the uncompressed 1024-byte scale tail of every (page, kv_head) +// slot into the matching paged scale plane. slots uses the host-cold layout: +// page stride slot_page_stride, head stride slot_bytes; scale page stride is +// scale_page_stride and page_ids supplies physical pages. +void entropy_nvfp4_slot_scales_scatter_launch(const std::uint8_t* slots, int slot_bytes, + int slot_page_stride, int kv_heads, + int page_count, const std::int32_t* page_ids, + int scale_page_stride, std::uint8_t* scales, + cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/gqa_attention.h b/src/ops/launcher/gqa_attention.h new file mode 100644 index 0000000000..a05fe9975b --- /dev/null +++ b/src/ops/launcher/gqa_attention.h @@ -0,0 +1,63 @@ +#pragma once + +// ninfer::ops::detail - private launch prototypes for gqa_attention policies. + +#include "core/paged_kv_cache.h" +#include "core/tensor.h" +#include "ninfer/ops/gqa_attention.h" + +#include + +#include + +namespace ninfer::ops::detail { + +enum class GqaAttentionRoute { SmallT, ChunkedSmallT, Prompt }; + +struct GqaSmallTInvocation { + const Tensor* valid_columns = nullptr; + const Tensor* table_rows = nullptr; + std::int32_t full_width = 0; + std::int32_t column_begin = 0; + std::int32_t width = 0; + std::int32_t batch_size = 1; +}; + +std::int32_t gqa_attention_split_capacity(std::int32_t q_heads, std::int32_t tokens, + DType cache_dtype, GqaExecutionEnvelope envelope); + +bool gqa_attention_uses_small_t(std::int32_t tokens); + +GqaAttentionRoute gqa_attention_resolve_route(std::int32_t q_heads, std::int32_t width, + std::int32_t batch_size, + GqaExecutionEnvelope envelope); + +const char* gqa_attention_route_name(GqaAttentionRoute route); + +void gqa_attention_small_t_launch(const Tensor& q, const Tensor& k, const Tensor& v, + const Tensor& positions, const Tensor& valid_columns, + const Tensor& table_rows, float scale, + PagedKVBatchLayerView cache, GqaExecutionEnvelope envelope, + std::int32_t column_begin, std::int32_t width, + Tensor& partial_acc, Tensor& partial_m, Tensor& partial_l, + Tensor& out, cudaStream_t stream); + +void gqa_attention_cached_small_t_launch(const Tensor& q, const Tensor& positions, float scale, + const PagedKVLayerView& cache, + GqaExecutionEnvelope envelope, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream); + +void gqa_attention_prompt_launch(const Tensor& q, const Tensor& k, const Tensor& v, + const Tensor& positions, const Tensor& valid_columns, + const Tensor& table_rows, float scale, PagedKVBatchLayerView cache, + Tensor& out, cudaStream_t stream); + +void gqa_kv_append_launch(const Tensor& k, const Tensor& v, const Tensor& positions, + PagedKVLayerView cache, cudaStream_t stream); + +void gqa_attention_prompt_attention_launch(const Tensor& q, const Tensor& positions, float scale, + const PagedKVLayerView& cache, Tensor& out, + cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/gqa_attention_decode.cu b/src/ops/launcher/gqa_attention_decode.cu new file mode 100644 index 0000000000..d39f78648c --- /dev/null +++ b/src/ops/launcher/gqa_attention_decode.cu @@ -0,0 +1,641 @@ +// ninfer::ops - split-KV GQA small-T launcher and unified route dispatcher. +#include "ops/launcher/gqa_attention.h" + +#include "ops/common/math.h" +#include "ops/kernel/gqa_attention_decode.cuh" +#include "ops/kernel/gqa_attention_decode_bf16.cuh" +#include "ops/kernel/gqa_attention_decode_fp8.cuh" +#include "ops/kernel/gqa_attention_decode_iso3.cuh" +#include "ops/kernel/gqa_attention_decode_i8.cuh" +#include "ops/kernel/gqa_attention_decode_nvfp4.cuh" +#include "core/device.h" // CUDA_CHECK +#include "ninfer/ops/gqa_attention.h" + +#include +#include + +namespace ninfer::ops::detail { +namespace { + +// Supplies an upper bound for the device-side active-split policy over one explicit execution +// envelope. Eager calls normally pass an exact window; graph calls pass their target-private +// replay interval. The dtype-aware wrapper below adds the measured INT8 specializations. +template +std::int32_t gqa_small_t_split_upper_bound(std::int32_t window) { + if (window <= 0) { return Geometry::DecodeSplits; } + + constexpr std::int32_t kMinSplits = 4 * Geometry::DecodeSplitScale; + std::int32_t splits = kMinSplits; + + const auto include_tier = [&](std::int32_t window_limit, std::int32_t target_keys_per_split) { + const std::int32_t tier_window = (window < window_limit) ? window : window_limit; + if (tier_window > 0) { + const std::int32_t tier_splits = div_up(tier_window, target_keys_per_split); + splits = (splits > tier_splits) ? splits : tier_splits; + } + }; + + include_tier(4096, 64 / Geometry::DecodeSplitScale); + if (window > 4096) { include_tier(8198, 128 / Geometry::DecodeSplitScale); } + if (window > 8198) { include_tier(16390, 256 / Geometry::DecodeSplitScale); } + if (window > 16390) { include_tier(window, 480 / Geometry::DecodeSplitScale); } + + return (splits < Geometry::DecodeSplits) ? splits : Geometry::DecodeSplits; +} + +template +std::int32_t gqa_small_t_split_count(std::int32_t window, std::int32_t tokens, DType kv_dtype) { + // A 64-key default split just above a 32-key boundary makes the partial + // kernel execute a nearly empty second tile. These short ranges instead + // launch one 32-key tile per split; the larger CTAs keep the small grid busy. + if (kv_dtype == DType::I8 && tokens == 5 && window > 128 && window <= 512) { + return div_up(window, 32 / Geometry::DecodeSplitScale); + } + if (kv_dtype == DType::I8 && tokens == 6 && window > 128 && window <= 160) { + return div_up(window, 24 / Geometry::DecodeSplitScale); + } + // Bc=64 is one CTA/SM on these model shapes. Keep the 8K grid at or below + // one 170-SM wave after accounting for the geometry's KV-head count. + if (kv_dtype == DType::I8 && tokens == 6 && window > 5000 && window <= 8198) { + const std::int32_t splits = div_up(window, 192 / Geometry::DecodeSplitScale); + constexpr std::int32_t kMin = 4 * Geometry::DecodeSplitScale; + constexpr std::int32_t kMax = 42 * Geometry::DecodeSplitScale; + const std::int32_t clamped = (splits > kMin) ? splits : kMin; + return (clamped < kMax) ? clamped : kMax; + } + // NVFP4 first revision: coarser splits than INT8 to cut redundant Q + // quantization and split-reduction overhead. Long contexts still scale. + if (kv_dtype == DType::NVFP4) { + const std::int32_t target = + window > 16390 ? 480 / Geometry::DecodeSplitScale + : (window > 4096 ? 256 / Geometry::DecodeSplitScale + : 64 / Geometry::DecodeSplitScale); + constexpr std::int32_t kMin = 4 * Geometry::DecodeSplitScale; + std::int32_t splits = div_up(window, target); + splits = splits > kMin ? splits : kMin; + return splits < Geometry::DecodeSplits ? splits : Geometry::DecodeSplits; + } + // BF16, FP8_E4M3FN, and ISO3 share the generic split policy. + return gqa_small_t_split_upper_bound(window); +} + +template +std::int32_t gqa_small_t_launch_capacity(GqaExecutionEnvelope envelope, std::int32_t tokens, + DType dtype) { + std::int32_t capacity = 0; + const auto include = [&](std::uint32_t window) { + if (window < envelope.min_visible_keys || window > envelope.max_visible_keys) { return; } + const auto splits = + gqa_small_t_split_count(static_cast(window), tokens, dtype); + capacity = capacity > splits ? capacity : splits; + }; + include(envelope.min_visible_keys); + include(envelope.max_visible_keys); + // The policy is monotonic inside these finite segments and may drop when crossing a boundary. + // Evaluating every segment end plus both interval ends gives the exact interval maximum. + constexpr std::uint32_t ends[] = {128, 160, 512, 4096, 5000, 8198, 16390}; + for (const std::uint32_t end : ends) { include(end); } + return capacity; +} + +template +void launch_tc_partial_bf16(const Tensor& q, CacheInput input, const Tensor& pos, float scale, + PagedKVBatchLayerView cache, const GqaSmallTInvocation& invocation, + std::int32_t logical_capacity, std::int32_t splits, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, cudaStream_t stream) { + constexpr int kBlock = 32 * WarpsPerCta; + const dim3 grid(Geometry::KVHeads, splits, invocation.batch_size); + Tensor& cache_k = cache.k_pages; + Tensor& cache_v = cache.v_pages; + // bf16 kernel uses only static smem (no dynamic staging). + gqa_attention_small_t_tc_partial_bf16_kernel<<>>( + static_cast(q.data), input, + static_cast(pos.data), static_cast<__nv_bfloat16*>(cache_k.data), + static_cast<__nv_bfloat16*>(cache_v.data), + static_cast(cache.block_tables.data), + invocation.valid_columns == nullptr + ? nullptr + : static_cast(invocation.valid_columns->data), + invocation.table_rows == nullptr + ? nullptr + : static_cast(invocation.table_rows->data), + cache.block_tables.ne[0], invocation.width, invocation.full_width, invocation.column_begin, + logical_capacity, scale, static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data)); + CUDA_CHECK(cudaGetLastError()); +} + +template +void launch_tc_partial_fp8(const Tensor& q, CacheInput input, const Tensor& pos, float scale, + PagedKVBatchLayerView cache, const GqaSmallTInvocation& invocation, + std::int32_t logical_capacity, std::int32_t splits, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, cudaStream_t stream) { + constexpr int kBlock = 32 * WarpsPerCta; + const dim3 grid(Geometry::KVHeads, splits, invocation.batch_size); + Tensor& cache_k = cache.k_pages; + Tensor& cache_v = cache.v_pages; + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + // fp8 kernel uses only static smem (no dynamic staging), same as bf16. + gqa_attention_small_t_tc_partial_fp8_kernel<<>>( + static_cast(q.data), input, + static_cast(pos.data), static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.block_tables.data), + invocation.valid_columns == nullptr + ? nullptr + : static_cast(invocation.valid_columns->data), + invocation.table_rows == nullptr + ? nullptr + : static_cast(invocation.table_rows->data), + cache.block_tables.ne[0], invocation.width, invocation.full_width, invocation.column_begin, + logical_capacity, scale, static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data)); + CUDA_CHECK(cudaGetLastError()); +} + +template +void launch_tc_partial_iso3(const Tensor& q, CacheInput input, const Tensor& pos, float scale, + PagedKVBatchLayerView cache, const GqaSmallTInvocation& invocation, + std::int32_t logical_capacity, std::int32_t splits, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, cudaStream_t stream) { + constexpr int kBlock = 32 * WarpsPerCta; + const dim3 grid(Geometry::KVHeads, splits, invocation.batch_size); + Tensor& cache_k = cache.k_pages; + Tensor& cache_v = cache.v_pages; + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + // iso3 kernel uses only static smem (no dynamic staging), same as bf16. + gqa_attention_small_t_tc_partial_iso3_kernel + <<>>( + static_cast(q.data), input, + static_cast(pos.data), static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.block_tables.data), + invocation.valid_columns == nullptr + ? nullptr + : static_cast(invocation.valid_columns->data), + invocation.table_rows == nullptr + ? nullptr + : static_cast(invocation.table_rows->data), + cache.block_tables.ne[0], invocation.width, invocation.full_width, invocation.column_begin, + logical_capacity, static_cast(cache.sliding_window_tokens), scale, + static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data)); + CUDA_CHECK(cudaGetLastError()); +} + +template +void launch_tc_partial_i8(const Tensor& q, CacheInput input, const Tensor& pos, float scale, + PagedKVBatchLayerView cache, const GqaSmallTInvocation& invocation, + std::int32_t logical_capacity, std::int32_t implementation_window, + std::int32_t splits, Tensor& partial_acc, Tensor& partial_m, + Tensor& partial_l, cudaStream_t stream) { + Tensor& cache_k = cache.k_pages; + Tensor& cache_v = cache.v_pages; + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + // Revision 2b: INT8-tier cold slots (raw nibble codec). The kernel takes + // region-relative K/V slot bases; empty tensors disable the cold branch. + const std::uint8_t* cold_k_i8 = + cache.cold_slots.data != nullptr && cache.dtype == DType::I8 + ? static_cast(cache.cold_slots.data) + : nullptr; + const std::uint8_t* cold_v_i8 = + cold_k_i8 != nullptr && cache.cold_slots.nb[2] != 0 + ? cold_k_i8 + cache.cold_slots.nb[2] + : nullptr; + auto launch = [&]() { + const dim3 grid(Geometry::KVHeads, splits, invocation.batch_size); + constexpr std::size_t kDynamicBytes = + DynamicArena ? static_cast(4 * KeyBlock * kGqaHeadDim) : 0u; + if constexpr (DynamicArena) { + static const cudaError_t attr = cudaFuncSetAttribute( + gqa_attention_decode_i8_tiled_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(kDynamicBytes)); + CUDA_CHECK(attr); + } + gqa_attention_decode_i8_tiled_kernel + <<>>( + static_cast(q.data), input, + static_cast(pos.data), static_cast(cache_k.data), + static_cast(cache_v.data), static_cast<__half*>(cache_k_scale.data), + static_cast<__half*>(cache_v_scale.data), cold_k_i8, cold_v_i8, + cache.slot_bytes, + static_cast(cache.block_tables.data), + invocation.valid_columns == nullptr + ? nullptr + : static_cast(invocation.valid_columns->data), + invocation.table_rows == nullptr + ? nullptr + : static_cast(invocation.table_rows->data), + cache.block_tables.ne[0], invocation.full_width, invocation.column_begin, + logical_capacity, scale, static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data)); + }; + // Revision 2b: INT8-tier cold slots (raw nibble codec). The kernel takes + // region-relative K/V slot bases; empty tensors disable the cold branch. + (void)cold_v_i8; + if constexpr (TokenTile == 6) { + // Small grids need more warps per CTA. From 2K to 8K, Bc=64 halves key + // loop iterations; dynamic smem avoids penalizing the long-context path. + if (implementation_window > 128 && implementation_window <= 160) { + launch.template operator()<24, 1, 32, false>(); + } else if (implementation_window <= 2054) { + launch.template operator()<12, 1, 32, false>(); + } else if (implementation_window <= 8198) { + launch.template operator()<12, 1, 64, true>(); + } else { + launch.template operator()<6, 2, 32, false>(); + } + } else if constexpr (TokenTile == 5) { + if constexpr (Geometry::GroupSize == 6) { + // Two Q row tiles for the 27B group of six. + if (implementation_window > 128 && implementation_window <= 512) { + launch.template operator()<32, 1, 32, false>(); + } else if (implementation_window <= 1029) { + launch.template operator()<16, 1, 32, false>(); + } else { + launch.template operator()<8, 2, 32, false>(); + } + } else { + // Three Q row tiles for the 35B group of eight. The 24/12-warp + // routes retain eight/four consumer warps per tile; the 6-warp + // route is reserved for long windows where CTA residency wins. + if (implementation_window > 128 && implementation_window <= 512) { + launch.template operator()<24, 1, 32, false>(); + } else if (implementation_window <= 1029) { + launch.template operator()<24, 1, 32, false>(); + } else if (implementation_window <= 4096) { + launch.template operator()<12, 1, 32, false>(); + } else { + launch.template operator()<6, 2, 32, false>(); + } + } + } else if constexpr (TokenTile == 4) { + if (implementation_window <= 1029) { + launch.template operator()<16, 1, 32, false>(); + } else { + launch.template operator()<8, 2, 32, false>(); + } + } else { + launch.template operator()<8, 2, 32, false>(); + } + CUDA_CHECK(cudaGetLastError()); +} + +template +void launch_tc_partial_nvfp4(const Tensor& q, const __nv_bfloat16* input_k, + const __nv_bfloat16* input_v, bool writes_cache, const Tensor& pos, + float scale, PagedKVBatchLayerView cache, + const GqaSmallTInvocation& invocation, + std::int32_t logical_capacity, std::int32_t implementation_window, + std::int32_t splits, Tensor& partial_acc, Tensor& partial_m, + Tensor& partial_l, cudaStream_t stream) { + Tensor& cache_k = cache.k_pages; + Tensor& cache_v = cache.v_pages; + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + const bool masked = invocation.valid_columns != nullptr; + const std::uint8_t* cold_k = static_cast(cache.cold_slots.data); + const std::uint8_t* cold_v = + cold_k == nullptr ? nullptr : cold_k + cache.cold_slots.nb[2]; + const std::int32_t* cold_k_valid = + static_cast(cache.cold_slot_valid.data); + const std::int32_t* cold_v_valid = + cold_k_valid == nullptr + ? nullptr + : reinterpret_cast( + reinterpret_cast(cold_k_valid) + + cache.cold_slot_valid.nb[1]); + auto launch = [&]() { + const dim3 grid(Geometry::KVHeads, splits, invocation.batch_size); + // Matches the arena layout in gqa_attention_decode_nvfp4_tiled_kernel: + // two ping-pong tiles (k_pk/v_pk Bc*128 each + k_sf/v_sf Bc*16 each), + // psc_s (Br*64 bytes, 64-byte row stride over RowTiles*16 rows), + // repack_a/repack_b (Wc*16*64 each). + constexpr int kTileBytes = 4 * KeyBlock * 128 + 4 * KeyBlock * 16; + constexpr int kRowTiles = (TokenTile * Geometry::GroupSize + 15) / 16; + constexpr std::size_t kRBytes = + static_cast(2 * kTileBytes + kRowTiles * 16 * 64 + + 2 * WarpsPerCta * 16 * 64); + constexpr std::size_t kVDynamicBytes = + Iso3V ? static_cast(KeyBlock) * 256ULL * 2ULL : 0ULL; + constexpr std::size_t kDynamicBytes = + (DynamicArena ? kRBytes : 0ULL) + kVDynamicBytes; + if constexpr (DynamicArena || Iso3V) { + static const cudaError_t attr = cudaFuncSetAttribute( + gqa_attention_decode_nvfp4_tiled_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, static_cast(kDynamicBytes)); + CUDA_CHECK(attr); + } + gqa_attention_decode_nvfp4_tiled_kernel + <<>>( + static_cast(q.data), input_k, input_v, + static_cast(pos.data), static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.k_residual_pages.data), + static_cast(cache.k_residual_scale_pages.data), + static_cast(cache.v_residual_pages.data), + static_cast(cache.v_residual_scale_pages.data), + cold_k, cold_v, cold_k_valid, cold_v_valid, cache.slot_bytes, + static_cast(cache.sliding_window_tokens), + static_cast(cache.block_tables.data), + invocation.valid_columns == nullptr + ? nullptr + : static_cast(invocation.valid_columns->data), + invocation.table_rows == nullptr + ? nullptr + : static_cast(invocation.table_rows->data), + cache.block_tables.ne[0], invocation.full_width, invocation.column_begin, + logical_capacity, cache.layer_index, scale, static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data), + invocation.batch_size, masked, writes_cache); + }; + // Minimal production schedule set for the first NVFP4 revision. + if constexpr (Geometry::GroupSize == 6) { + if constexpr (TokenTile <= 4) { + launch.template operator()<16, 1, 32, true>(); + } else if constexpr (TokenTile == 5) { + launch.template operator()<8, 1, 32, true>(); + } else { + launch.template operator()<12, 1, 32, true>(); + } + } else { + if constexpr (TokenTile <= 4) { + launch.template operator()<16, 1, 32, true>(); + } else { + launch.template operator()<12, 1, 32, true>(); + } + } + CUDA_CHECK(cudaGetLastError()); +} + +PagedKVBatchLayerView single_row_batch_view(const PagedKVLayerView& cache) { + return { + .k_pages = cache.k_pages, + .v_pages = cache.v_pages, + .k_scale_pages = cache.k_scale_pages, + .v_scale_pages = cache.v_scale_pages, + .k_residual_pages = cache.k_residual_pages, + .k_residual_scale_pages = cache.k_residual_scale_pages, + .v_residual_pages = cache.v_residual_pages, + .v_residual_scale_pages = cache.v_residual_scale_pages, + .block_tables = cache.block_table.view({cache.block_table.ne[0], 1}), + .cold_slots = cache.cold_slots, + .cold_slot_valid = cache.cold_slot_valid, + .slot_bytes = cache.slot_bytes, + .head_dim = cache.head_dim, + .num_kv_heads = cache.num_kv_heads, + .layer_index = cache.layer_index, + .dtype = cache.dtype, + .quant_group = cache.quant_group, + .v_dtype = cache.v_dtype, + .v_quant_group = cache.v_quant_group, + .sliding_window_tokens = cache.sliding_window_tokens, + }; +} + +} // namespace + +bool gqa_attention_uses_small_t(std::int32_t tokens) { return tokens >= 1 && tokens <= 6; } + +std::int32_t gqa_attention_split_capacity(std::int32_t q_heads, std::int32_t tokens, + DType cache_dtype, GqaExecutionEnvelope envelope) { + if (tokens < 1 || tokens > 6 || + (cache_dtype != DType::BF16 && cache_dtype != DType::I8 && + cache_dtype != DType::NVFP4 && cache_dtype != DType::FP8_E4M3FN && + cache_dtype != DType::ISO3) || + envelope.min_visible_keys == 0 || envelope.min_visible_keys > envelope.max_visible_keys) { + throw std::invalid_argument("gqa_attention split capacity: invalid profile"); + } + if (q_heads == Gqa27Geometry::QHeads) { + return gqa_small_t_launch_capacity(envelope, tokens, cache_dtype); + } + if (q_heads == Gqa35Geometry::QHeads) { + return gqa_small_t_launch_capacity(envelope, tokens, cache_dtype); + } + throw std::invalid_argument("gqa_attention split capacity: unsupported head geometry"); +} + +template +void gqa_attention_small_t_launch_for(const Tensor& q, CacheInput input, const Tensor& pos, + float scale, PagedKVBatchLayerView cache, + const GqaSmallTInvocation& invocation, + GqaExecutionEnvelope envelope, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream) { + const auto logical_capacity = static_cast(envelope.max_visible_keys); + const auto implementation_window = static_cast(envelope.max_visible_keys); + const auto splits = + gqa_small_t_launch_capacity(envelope, invocation.width, cache.dtype); + + // BF16 and FP8 keep the row-tile warp count; INT8 selects its + // producer/consumer geometry inside launch_tc_partial_i8. +#define NINFER_GQA_SMALL_T_DISPATCH(TOKENS, WARPS) \ + do { \ + const auto launch_profile = [&]() { \ + if (cache.dtype == DType::I8) { \ + launch_tc_partial_i8( \ + q, input, pos, scale, cache, invocation, logical_capacity, \ + implementation_window, splits, partial_acc, partial_m, partial_l, stream); \ + } else if (cache.dtype == DType::NVFP4 && cache.v_dtype == DType::ISO3) { \ + const __nv_bfloat16* nvfp4_k = nullptr; \ + const __nv_bfloat16* nvfp4_v = nullptr; \ + if constexpr (CacheInput::writes_cache) { \ + nvfp4_k = input.k; \ + nvfp4_v = input.v; \ + } \ + launch_tc_partial_nvfp4( \ + q, nvfp4_k, nvfp4_v, CacheInput::writes_cache, pos, scale, cache, invocation, \ + logical_capacity, implementation_window, splits, partial_acc, partial_m, \ + partial_l, stream); \ + } else if (cache.dtype == DType::NVFP4) { \ + const __nv_bfloat16* nvfp4_k = nullptr; \ + const __nv_bfloat16* nvfp4_v = nullptr; \ + if constexpr (CacheInput::writes_cache) { \ + nvfp4_k = input.k; \ + nvfp4_v = input.v; \ + } \ + launch_tc_partial_nvfp4( \ + q, nvfp4_k, nvfp4_v, CacheInput::writes_cache, pos, scale, cache, invocation, \ + logical_capacity, implementation_window, splits, partial_acc, partial_m, \ + partial_l, stream); \ + } else if (cache.dtype == DType::FP8_E4M3FN) { \ + launch_tc_partial_fp8( \ + q, input, pos, scale, cache, invocation, logical_capacity, splits, \ + partial_acc, partial_m, partial_l, stream); \ + } else if (cache.dtype == DType::ISO3) { \ + launch_tc_partial_iso3( \ + q, input, pos, scale, cache, invocation, logical_capacity, splits, \ + partial_acc, partial_m, partial_l, stream); \ + } else { \ + launch_tc_partial_bf16( \ + q, input, pos, scale, cache, invocation, logical_capacity, splits, \ + partial_acc, partial_m, partial_l, stream); \ + } \ + }; \ + const bool masked = invocation.valid_columns != nullptr; \ + if (invocation.batch_size == 1) { \ + if (masked) { \ + launch_profile.template operator()(); \ + } else { \ + launch_profile.template operator()(); \ + } \ + } else if (masked) { \ + launch_profile.template operator()(); \ + } else { \ + launch_profile.template operator()(); \ + } \ + } while (0) + + switch (invocation.width) { + case 1: + NINFER_GQA_SMALL_T_DISPATCH(1, 2); + break; + case 2: + NINFER_GQA_SMALL_T_DISPATCH(2, 4); + break; + case 3: + NINFER_GQA_SMALL_T_DISPATCH(3, 4); + break; + case 4: + NINFER_GQA_SMALL_T_DISPATCH(4, 4); + break; + case 5: + NINFER_GQA_SMALL_T_DISPATCH(5, 4); + break; + case 6: + NINFER_GQA_SMALL_T_DISPATCH(6, 4); + break; + default: + throw std::invalid_argument("gqa_attention_small_t_launch: unsupported T"); + } +#undef NINFER_GQA_SMALL_T_DISPATCH + + constexpr int kReduceBlock = 256; + constexpr int kDChunk = 64; + const dim3 reduce_grid(Geometry::QHeads, div_up(kGqaHeadDim, kDChunk), + invocation.width * invocation.batch_size); + const auto launch_reduce = [&]() { + gqa_attention_small_t_reduce_output_kernel + <<>>( + static_cast(partial_acc.data), + static_cast(partial_m.data), + static_cast(partial_l.data), + static_cast(pos.data), + invocation.valid_columns == nullptr + ? nullptr + : static_cast(invocation.valid_columns->data), + invocation.width, invocation.full_width, invocation.column_begin, + invocation.batch_size, splits, static_cast<__nv_bfloat16*>(out.data)); + }; + const bool masked = invocation.valid_columns != nullptr; + const auto launch_profile = [&]() { + if (invocation.column_begin == 0) { + launch_reduce.template operator()(); + } else { + launch_reduce.template operator()(); + } + }; + const auto launch_for_dtype = [&]() { + if (invocation.batch_size == 1) { + if (masked) { + launch_profile.template operator()(); + } else { + launch_profile.template operator()(); + } + } else if (masked) { + launch_profile.template operator()(); + } else { + launch_profile.template operator()(); + } + }; + // FP8_E4M3FN and ISO3 are quantized but deliberately use the BF16 + // (Int8=false) reducer path: gqa_small_t_split_count falls through to the + // generic BF16 policy for both dtypes, and their partial kernels compute + // active splits with gqa_small_t_active_splits. The + // Int8=true path would apply the I8 token-5/6 active-split specializations + // and disagree with the launch. + if (cache.dtype == DType::I8 || cache.dtype == DType::NVFP4) { + launch_for_dtype.template operator()(); + } else { + launch_for_dtype.template operator()(); + } + CUDA_CHECK(cudaGetLastError()); +} + +void gqa_attention_small_t_launch(const Tensor& q, const Tensor& k, const Tensor& v, + const Tensor& pos, const Tensor& valid_columns, + const Tensor& table_rows, float scale, + PagedKVBatchLayerView cache, GqaExecutionEnvelope envelope, + std::int32_t column_begin, std::int32_t width, + Tensor& partial_acc, Tensor& partial_m, Tensor& partial_l, + Tensor& out, cudaStream_t stream) { + const GqaAppendInput input{static_cast(k.data), + static_cast(v.data)}; + const GqaSmallTInvocation invocation{ + .valid_columns = valid_columns.data == nullptr ? nullptr : &valid_columns, + .table_rows = &table_rows, + .full_width = q.ne[2], + .column_begin = column_begin, + .width = width, + .batch_size = q.ne[3], + }; + if (q.ne[1] == Gqa27Geometry::QHeads) { + gqa_attention_small_t_launch_for(q, input, pos, scale, cache, invocation, + envelope, partial_acc, partial_m, partial_l, + out, stream); + return; + } + gqa_attention_small_t_launch_for(q, input, pos, scale, cache, invocation, + envelope, partial_acc, partial_m, partial_l, + out, stream); +} + +void gqa_attention_cached_small_t_launch(const Tensor& q, const Tensor& pos, float scale, + const PagedKVLayerView& cache, + GqaExecutionEnvelope envelope, Tensor& partial_acc, + Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream) { + const GqaCachedInput input{}; + const GqaSmallTInvocation invocation{ + .valid_columns = nullptr, + .table_rows = nullptr, + .full_width = q.ne[2], + .column_begin = 0, + .width = q.ne[2], + .batch_size = 1, + }; + const PagedKVBatchLayerView batch_cache = single_row_batch_view(cache); + if (q.ne[1] == Gqa27Geometry::QHeads) { + gqa_attention_small_t_launch_for(q, input, pos, scale, batch_cache, + invocation, envelope, partial_acc, + partial_m, partial_l, out, stream); + return; + } + gqa_attention_small_t_launch_for(q, input, pos, scale, batch_cache, invocation, + envelope, partial_acc, partial_m, partial_l, + out, stream); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/gqa_attention_prefill.cu b/src/ops/launcher/gqa_attention_prefill.cu new file mode 100644 index 0000000000..6daaea5781 --- /dev/null +++ b/src/ops/launcher/gqa_attention_prefill.cu @@ -0,0 +1,369 @@ +// ninfer::ops - gqa_attention prompt-scale launcher: fill k/v at device +// positions then launch causal attention over absolute cached history. +#include "ops/launcher/gqa_attention.h" + +#include "ops/common/math.h" +#include "ops/kernel/gqa_attention_prefill_bf16.cuh" +#include "ops/kernel/gqa_attention_prefill_i8.cuh" +#include "ops/kernel/gqa_attention_prefill_nvfp4.cuh" +#include "core/device.h" // CUDA_CHECK + +#include + +namespace ninfer::ops::detail { +namespace { + +template +void gqa_attention_prompt_attention_launch_for(const Tensor& q, const Tensor& positions, + float scale, const CacheView& cache, + Metadata metadata, Tensor& out, + cudaStream_t stream) { + const Tensor& cache_k = cache.k_pages; + const Tensor& cache_v = cache.v_pages; + // Both dtype-specialized kernels exceed the default 48 KiB dynamic-smem ceiling. + static const cudaError_t attr_bf16 = + cudaFuncSetAttribute(gqa_attention_prefill_bf16_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, kGqaPrefillSmemBytes); + CUDA_CHECK(attr_bf16); + static const cudaError_t attr_i8 = + cudaFuncSetAttribute(gqa_attention_prefill_i8_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, kGqaPrefillI8SmemBytes); + CUDA_CHECK(attr_i8); + static const cudaError_t attr_nvfp4 = + cudaFuncSetAttribute(gqa_attention_prefill_nvfp4_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, + kNvfp4PrefillSmemBytes); + CUDA_CHECK(attr_nvfp4); + static const cudaError_t attr_nvfp4k_iso3v = cudaFuncSetAttribute( + gqa_attention_prefill_nvfp4_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, kNvfp4PrefillSmemBytes); + CUDA_CHECK(attr_nvfp4k_iso3v); + static const cudaError_t attr_fp8 = + cudaFuncSetAttribute(gqa_attention_prefill_nvfp4_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, + kNvfp4PrefillSmemBytes); + CUDA_CHECK(attr_fp8); + static const cudaError_t attr_iso3 = + cudaFuncSetAttribute(gqa_attention_prefill_nvfp4_kernel, + cudaFuncAttributeMaxDynamicSharedMemorySize, + kNvfp4PrefillSmemBytes); + CUDA_CHECK(attr_iso3); + + const auto tokens = static_cast(q.ne[2]); + if (cache.dtype == DType::I8) { + const dim3 attention_grid(static_cast(div_up(tokens, kGqaPrefillI8Br)), + static_cast(Geometry::QHeads), 1u); + const Tensor& cache_k_scale = cache.k_scale_pages; + const Tensor& cache_v_scale = cache.v_scale_pages; + gqa_attention_prefill_i8_kernel + <<>>( + static_cast(q.data), + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), metadata, + static_cast(positions.data), scale, + static_cast<__nv_bfloat16*>(out.data), tokens); + } else if (cache.dtype == DType::NVFP4) { + const dim3 attention_grid(static_cast(div_up(tokens, kNvfp4PrefillBr)), + static_cast(Geometry::QHeads), 1u); + const Tensor& cache_k_scale = cache.k_scale_pages; + const Tensor& cache_v_scale = cache.v_scale_pages; + const std::uint8_t* cold_k = + static_cast(cache.cold_slots.data); + const std::uint8_t* cold_v = cold_k == nullptr + ? nullptr + : cold_k + cache.cold_slots.nb[2]; + const std::int32_t* cold_k_valid = + static_cast(cache.cold_slot_valid.data); + const std::int32_t* cold_v_valid = + cold_k_valid == nullptr + ? nullptr + : reinterpret_cast( + reinterpret_cast(cold_k_valid) + + cache.cold_slot_valid.nb[1]); + if (cache.v_dtype == DType::ISO3) { + gqa_attention_prefill_nvfp4_kernel + <<>>( + static_cast(q.data), + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.k_residual_pages.data), + static_cast(cache.k_residual_scale_pages.data), + static_cast(cache.v_residual_pages.data), + static_cast(cache.v_residual_scale_pages.data), + cold_k, cold_v, + cold_k_valid, cold_v_valid, cache.slot_bytes, + static_cast(cache.sliding_window_tokens), cache.layer_index, metadata, + static_cast(positions.data), scale, + static_cast<__nv_bfloat16*>(out.data), tokens); + } else { + gqa_attention_prefill_nvfp4_kernel + <<>>( + static_cast(q.data), + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.k_residual_pages.data), + static_cast(cache.k_residual_scale_pages.data), + static_cast(cache.v_residual_pages.data), + static_cast(cache.v_residual_scale_pages.data), + cold_k, cold_v, + cold_k_valid, cold_v_valid, cache.slot_bytes, + static_cast(cache.sliding_window_tokens), cache.layer_index, metadata, + static_cast(positions.data), scale, + static_cast<__nv_bfloat16*>(out.data), tokens); + } + } else if (cache.dtype == DType::ISO3) { + const dim3 attention_grid(static_cast(div_up(tokens, kNvfp4PrefillBr)), + static_cast(Geometry::QHeads), 1u); + const Tensor& cache_k_scale = cache.k_scale_pages; + const Tensor& cache_v_scale = cache.v_scale_pages; + gqa_attention_prefill_nvfp4_kernel + <<>>( + static_cast(q.data), + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), 0, 0, cache.layer_index, metadata, + static_cast(positions.data), scale, + static_cast<__nv_bfloat16*>(out.data), tokens); + } else if (cache.dtype == DType::FP8_E4M3FN) { + const dim3 attention_grid(static_cast(div_up(tokens, kNvfp4PrefillBr)), + static_cast(Geometry::QHeads), 1u); + const Tensor& cache_k_scale = cache.k_scale_pages; + const Tensor& cache_v_scale = cache.v_scale_pages; + gqa_attention_prefill_nvfp4_kernel + <<>>( + static_cast(q.data), + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), + static_cast(nullptr), 0, 0, cache.layer_index, metadata, + static_cast(positions.data), scale, + static_cast<__nv_bfloat16*>(out.data), tokens); + } else { + const dim3 attention_grid(static_cast(div_up(tokens, kGqaPrefillBr)), + static_cast(Geometry::QHeads), 1u); + gqa_attention_prefill_bf16_kernel + <<>>( + static_cast(q.data), + static_cast(cache_k.data), + static_cast(cache_v.data), metadata, + static_cast(positions.data), scale, + static_cast<__nv_bfloat16*>(out.data), tokens); + } + CUDA_CHECK(cudaGetLastError()); +} + +template +void gqa_kv_append_launch_for(const Tensor& k, const Tensor& v, const Tensor& positions, + CacheView cache, Metadata metadata, cudaStream_t stream) { + const auto tokens = static_cast(k.ne[2]); + Tensor& cache_k = cache.k_pages; + Tensor& cache_v = cache.v_pages; + if (cache.dtype == DType::I8) { + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + constexpr int kFillBlock = 256; + if (tokens >= 128 && Geometry::KVHeads == 2) { + constexpr int kPageBlock = 256; + constexpr int kTokensPerTile = 8; + const int max_tiles = div_up(tokens + kTokensPerTile - 1, kTokensPerTile); + const dim3 fill_grid(static_cast(max_tiles), + static_cast(Geometry::KVHeads), + static_cast(kGqaKvQuantGroups)); + gqa_attention_prefill_fill_i8_page_kernel + <<>>( + static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), metadata, + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast<__half*>(cache_k_scale.data), + static_cast<__half*>(cache_v_scale.data), tokens); + } else { + constexpr int kFillWarps = kFillBlock / 32; + const std::int64_t fill_units = + static_cast(tokens) * Geometry::KVHeads * kGqaKvQuantGroups; + const int fill_grid = + static_cast(div_up(fill_units, static_cast(kFillWarps))); + gqa_attention_prefill_fill_i8_kernel + <<>>( + static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), metadata, + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast<__half*>(cache_k_scale.data), + static_cast<__half*>(cache_v_scale.data), tokens); + } + CUDA_CHECK(cudaGetLastError()); + } else if (cache.dtype == DType::NVFP4) { + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + constexpr int kFillBlock = 256; + constexpr int kFillWarps = kFillBlock / 32; + const std::int64_t fill_units = + static_cast(tokens) * Geometry::KVHeads * kGqaKvNvfp4Groups; + const int fill_grid = + static_cast(div_up(fill_units, static_cast(kFillWarps))); + if (cache.v_dtype == DType::ISO3) { + gqa_attention_prefill_fill_nvfp4k_iso3v_kernel + <<>>( + static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), cache.layer_index, metadata, + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.k_residual_pages.data), + static_cast(cache.k_residual_scale_pages.data), + static_cast(cache.v_residual_pages.data), + static_cast(cache.v_residual_scale_pages.data), tokens); + } else { + gqa_attention_prefill_fill_nvfp4_kernel + <<>>( + static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), cache.layer_index, metadata, + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), + static_cast(cache.k_residual_pages.data), + static_cast(cache.k_residual_scale_pages.data), tokens); + } + CUDA_CHECK(cudaGetLastError()); + } else if (cache.dtype == DType::ISO3) { + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + constexpr int kFillBlock = 256; + constexpr int kFillWarps = kFillBlock / 32; + const std::int64_t fill_units = + static_cast(tokens) * Geometry::KVHeads * kGqaKvNvfp4Groups; + const int fill_grid = + static_cast(div_up(fill_units, static_cast(kFillWarps))); + gqa_attention_prefill_fill_iso3_kernel + <<>>( + static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), metadata, + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), tokens); + CUDA_CHECK(cudaGetLastError()); + } else if (cache.dtype == DType::FP8_E4M3FN) { + Tensor& cache_k_scale = cache.k_scale_pages; + Tensor& cache_v_scale = cache.v_scale_pages; + constexpr int kFillBlock = 256; + constexpr int kFillWarps = kFillBlock / 32; + const std::int64_t fill_units = + static_cast(tokens) * Geometry::KVHeads * kGqaKvNvfp4Groups; + const int fill_grid = + static_cast(div_up(fill_units, static_cast(kFillWarps))); + gqa_attention_prefill_fill_fp8_kernel + <<>>( + static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), metadata, + static_cast(cache_k.data), + static_cast(cache_v.data), + static_cast(cache_k_scale.data), + static_cast(cache_v_scale.data), tokens); + CUDA_CHECK(cudaGetLastError()); + } else { + constexpr int kBlock = Geometry::KVHeads == 4 ? 128 : 96; + constexpr int kFillVecElems = 8; + const std::int64_t kv_elements = static_cast(tokens) * Geometry::KVHeads * + (kGqaPrefillHeadDim / kFillVecElems); + const int fill_grid = + static_cast(div_up(kv_elements, static_cast(kBlock))); + gqa_attention_prefill_fill_bf16_kernel + <<>>(static_cast(k.data), + static_cast(v.data), + static_cast(positions.data), + metadata, static_cast<__nv_bfloat16*>(cache_k.data), + static_cast<__nv_bfloat16*>(cache_v.data), tokens); + CUDA_CHECK(cudaGetLastError()); + } +} + +} // namespace + +void gqa_attention_prompt_attention_launch(const Tensor& q, const Tensor& positions, float scale, + const PagedKVLayerView& cache, Tensor& out, + cudaStream_t stream) { + const GqaPrefillDirectMetadata metadata{ + static_cast(cache.block_table.data)}; + if (q.ne[1] == Gqa27Geometry::QHeads) { + gqa_attention_prompt_attention_launch_for(q, positions, scale, cache, + metadata, out, stream); + return; + } + gqa_attention_prompt_attention_launch_for(q, positions, scale, cache, metadata, + out, stream); +} + +void gqa_kv_append_launch(const Tensor& k, const Tensor& v, const Tensor& positions, + PagedKVLayerView cache, cudaStream_t stream) { + const GqaPrefillDirectMetadata metadata{ + static_cast(cache.block_table.data)}; + if (k.ne[1] == Gqa27Geometry::KVHeads) { + gqa_kv_append_launch_for(k, v, positions, cache, metadata, stream); + return; + } + gqa_kv_append_launch_for(k, v, positions, cache, metadata, stream); +} + +void gqa_attention_prompt_launch(const Tensor& q, const Tensor& k, const Tensor& v, + const Tensor& positions, const Tensor& valid_columns, + const Tensor& table_rows, float scale, PagedKVBatchLayerView cache, + Tensor& out, cudaStream_t stream) { + const auto launch = [&]() { + const GqaPrefillBatchMetadata metadata{ + .tables = static_cast(cache.block_tables.data), + .valid_columns = + Masked ? static_cast(valid_columns.data) : nullptr, + .table_rows = static_cast(table_rows.data), + .table_stride = cache.block_tables.ne[0], + }; + if (q.ne[1] == Gqa27Geometry::QHeads) { + gqa_kv_append_launch_for(k, v, positions, cache, metadata, stream); + gqa_attention_prompt_attention_launch_for(q, positions, scale, cache, + metadata, out, stream); + return; + } + gqa_kv_append_launch_for(k, v, positions, cache, metadata, stream); + gqa_attention_prompt_attention_launch_for(q, positions, scale, cache, + metadata, out, stream); + }; + if (valid_columns.data == nullptr) { + launch.template operator()(); + } else { + launch.template operator()(); + } +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/rope.cu b/src/ops/launcher/rope.cu index 03ca1835a9..9682fbe914 100644 --- a/src/ops/launcher/rope.cu +++ b/src/ops/launcher/rope.cu @@ -5,6 +5,7 @@ #include "ops/kernel/rope.cuh" #include +#include namespace ninfer::ops::detail { namespace { @@ -18,8 +19,9 @@ constexpr int kLargeBlockWaveCapacity = 1020; template inline constexpr bool kTextMode = - Mode == RopeKernelMode::Text1D || Mode == RopeKernelMode::TextMrope || - Mode == RopeKernelMode::DflashText1D; + Mode == RopeKernelMode::Text1D || Mode == RopeKernelMode::Text1DYarn4 || + Mode == RopeKernelMode::TextMrope || Mode == RopeKernelMode::DflashText1D || + Mode == RopeKernelMode::DflashText1DYarn4; std::int64_t token_stride(const Tensor* tensor) { return tensor == nullptr ? 0 : tensor->nb[2] / static_cast(sizeof(__nv_bfloat16)); @@ -197,4 +199,59 @@ void rope_single_launch(const Tensor& positions, int rotary_dim, float theta, Te CUDA_CHECK(cudaGetLastError()); } +// Static YaRN factor-4 rope: the same fixed geometry as the native modes but +// with the yarn4 frequency tables and the yarn4 attention scaling folded into +// the sincos coefficients. Text is D256/R64 (24Q/4K or 16Q/2K), DFlash is +// D128/R128 (32Q/8K). +void rope_yarn4_launch(const Tensor& positions, int rotary_dim, float theta, Tensor& q, Tensor& k, + cudaStream_t stream) { + if (!bf16x2_aligned(q) || !bf16x2_aligned(k)) { + throw std::invalid_argument("rope_yarn4: q/k must be bf16x2 aligned"); + } + const int axes = positions.ne[1]; + if (rotary_dim == 128 && theta == 1.0e7F && axes == 1 && q.ne[0] == 128 && q.ne[1] == 32 && + k.ne[1] == 8) { + launch_fixed(positions, &q, &k, stream); + } else if (rotary_dim == 64 && theta == 1.0e7F) { + if (q.ne[1] == 24 && k.ne[1] == 4) { + launch_fixed(positions, &q, &k, stream); + } else if (q.ne[1] == 16 && k.ne[1] == 2) { + launch_fixed(positions, &q, &k, stream); + } else { + throw std::invalid_argument("rope_yarn4: unsupported Text head geometry"); + } + } else { + throw std::invalid_argument("rope_yarn4: expected Text D256/R64 or DFlash D128/R128"); + } + CUDA_CHECK(cudaGetLastError()); +} + +void rope_yarn4_single_launch(const Tensor& positions, int rotary_dim, float theta, Tensor& x, + cudaStream_t stream) { + if (!bf16x2_aligned(x)) { + throw std::invalid_argument("rope_yarn4: tensor must be bf16x2 aligned"); + } + const int axes = positions.ne[1]; + if (rotary_dim == 128 && theta == 1.0e7F && axes == 1 && x.ne[0] == 128) { + if (x.ne[1] == 32) { + launch_fixed_single(positions, x, stream); + } else if (x.ne[1] == 8) { + launch_fixed_single(positions, x, stream); + } else { + throw std::invalid_argument("rope_yarn4: unsupported DFlash head count"); + } + } else if (rotary_dim == 64 && theta == 1.0e7F) { + if (x.ne[1] == 24 || x.ne[1] == 4) { + launch_fixed_single(positions, x, stream); + } else if (x.ne[1] == 16 || x.ne[1] == 2) { + launch_fixed_single(positions, x, stream); + } else { + throw std::invalid_argument("rope_yarn4: unsupported Text head count"); + } + } else { + throw std::invalid_argument("rope_yarn4: expected Text D256/R64 or DFlash D128/R128"); + } + CUDA_CHECK(cudaGetLastError()); +} + } // namespace ninfer::ops::detail diff --git a/src/ops/launcher/rope.h b/src/ops/launcher/rope.h index ba35837fc5..7eea862cf4 100644 --- a/src/ops/launcher/rope.h +++ b/src/ops/launcher/rope.h @@ -15,4 +15,10 @@ void rope_launch(const Tensor& positions, int rotary_dim, float theta, Tensor& q void rope_single_launch(const Tensor& positions, int rotary_dim, float theta, Tensor& x, cudaStream_t stream); +void rope_yarn4_launch(const Tensor& positions, int rotary_dim, float theta, Tensor& q, Tensor& k, + cudaStream_t stream); + +void rope_yarn4_single_launch(const Tensor& positions, int rotary_dim, float theta, Tensor& x, + cudaStream_t stream); + } // namespace ninfer::ops::detail diff --git a/src/ops/launcher/swa.cu b/src/ops/launcher/swa.cu new file mode 100644 index 0000000000..96beef1c0a --- /dev/null +++ b/src/ops/launcher/swa.cu @@ -0,0 +1,166 @@ +#include "ops/launcher/swa.h" + +#include "core/device.h" +#include "ops/kernel/bidirectional_gqa_attention.cuh" + +#include +#include +#include + +namespace ninfer::ops::detail { +namespace { + +template +void dispatch_token_case(Launch&& launch) { + constexpr int Warps = (Tokens + 3) / 4; + launch.template operator()(); +} + +template +void dispatch_tokens(std::int32_t tokens, Launch&& launch) { + switch (tokens) { +#define NINFER_SWA_TOKEN_CASE(TOKENS) \ + case TOKENS: \ + dispatch_token_case(launch); \ + return + NINFER_SWA_TOKEN_CASE(1); + NINFER_SWA_TOKEN_CASE(2); + NINFER_SWA_TOKEN_CASE(3); + NINFER_SWA_TOKEN_CASE(4); + NINFER_SWA_TOKEN_CASE(5); + NINFER_SWA_TOKEN_CASE(6); + NINFER_SWA_TOKEN_CASE(7); + NINFER_SWA_TOKEN_CASE(8); + NINFER_SWA_TOKEN_CASE(9); + NINFER_SWA_TOKEN_CASE(10); + NINFER_SWA_TOKEN_CASE(11); + NINFER_SWA_TOKEN_CASE(12); + NINFER_SWA_TOKEN_CASE(13); + NINFER_SWA_TOKEN_CASE(14); + NINFER_SWA_TOKEN_CASE(15); + NINFER_SWA_TOKEN_CASE(16); +#undef NINFER_SWA_TOKEN_CASE + default: + throw std::invalid_argument("swa: unsupported T"); + } +} + +} // namespace + +SwaPlan swa_resolve_plan(std::int32_t tokens, SwaContextExecutionEnvelope envelope, + std::uint32_t window) { + if (tokens < 1 || tokens > 16) { throw std::invalid_argument("swa plan: T must be 1..16"); } + if (window != 2048 && window != 4096) { + throw std::invalid_argument("swa plan: registered windows are 2048 and 4096"); + } + if (envelope.min_context > envelope.max_context) { + throw std::invalid_argument("swa plan: invalid envelope"); + } + // Graph envelopes whose longest context fits three key tiles avoid the second kernel and + // workspace round trip. At four tiles, split-KV is already faster for every qualified T. + constexpr std::uint32_t direct_context_limit = 96; + const bool direct = envelope.max_context <= direct_context_limit; + constexpr std::int32_t key_block = 32; + const std::uint32_t context_rows = std::min(envelope.max_context, window - 1u); + const std::int32_t context_tiles = + static_cast((context_rows + key_block - 1u) / key_block); + constexpr std::int32_t split_limit = 32; + return { + .route = direct ? SwaRoute::Direct : SwaRoute::SplitKv, + .tokens = tokens, + .warps = (tokens + 3) / 4, + .split_capacity = direct ? 1 : std::min(split_limit, std::max(1, context_tiles)), + .max_context = static_cast(envelope.max_context), + .window = window, + }; +} + +const char* swa_route_name(SwaRoute route) { + switch (route) { + case SwaRoute::Direct: + return "direct"; + case SwaRoute::SplitKv: + return "split_kv"; + } + return "unknown"; +} + +void swa_launch(const Tensor& q, const Tensor& query_k, const Tensor& query_v, + const Tensor& positions, const Tensor& valid_columns, const Tensor& lanes, + float scale, const CyclicKVCacheLayerView& context, const SwaPlan& plan, + Tensor& partial_acc, Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream) { + dispatch_tokens(q.ne[2], [&]() { + const bool direct = plan.route == SwaRoute::Direct; + if (plan.warps != Warps || plan.split_capacity < 1 || + plan.split_capacity > kSwaMaxCandidateSplit || (direct && plan.split_capacity != 1)) { + throw std::invalid_argument("swa: inconsistent plan"); + } + const auto launch_case = [&]() { + constexpr int KeyBlock = 32; + constexpr std::size_t SmemBytes = + 2u * KeyBlock * kBidirectionalGqaHeadDim * sizeof(__nv_bfloat16); + if (direct) { + const dim3 direct_grid(kBidirectionalGqaKVHeads, 1, q.ne[3]); + swa_split_partial_kernel + <<>>( + static_cast(q.data), + static_cast(query_k.data), + static_cast(query_v.data), + static_cast(positions.data), + static_cast(valid_columns.data), + static_cast(lanes.data), + static_cast(context.k.data), + static_cast(context.v.data), + static_cast(context.padded_capacity), plan.max_context, 1, scale, + static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data), + static_cast<__nv_bfloat16*>(out.data)); + CUDA_CHECK(cudaGetLastError()); + return; + } + + const dim3 partial_grid(kBidirectionalGqaKVHeads, plan.split_capacity, q.ne[3]); + swa_split_partial_kernel + <<>>( + static_cast(q.data), + static_cast(query_k.data), + static_cast(query_v.data), + static_cast(positions.data), + static_cast(valid_columns.data), + static_cast(lanes.data), + static_cast(context.k.data), + static_cast(context.v.data), + static_cast(context.padded_capacity), plan.max_context, + plan.split_capacity, scale, static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data), + static_cast<__nv_bfloat16*>(out.data)); + CUDA_CHECK(cudaGetLastError()); + + constexpr int ReduceWarps = 1; + constexpr int ReduceRows = kBidirectionalGqaQHeads * Tokens; + const dim3 reduce_grid((ReduceRows + ReduceWarps - 1) / ReduceWarps, 1, q.ne[3]); + swa_reduce_kernel + <<>>( + static_cast(partial_acc.data), + static_cast(partial_m.data), + static_cast(partial_l.data), + static_cast(positions.data), + static_cast(valid_columns.data), plan.max_context, + plan.split_capacity, static_cast<__nv_bfloat16*>(out.data)); + CUDA_CHECK(cudaGetLastError()); + }; + switch (plan.window) { + case 2048: + launch_case.template operator()<2048>(); + return; + case 4096: + launch_case.template operator()<4096>(); + return; + default: + throw std::invalid_argument("swa: unsupported window"); + } + }); +} + +} // namespace ninfer::ops::detail diff --git a/src/ops/launcher/swa.h b/src/ops/launcher/swa.h new file mode 100644 index 0000000000..d413312d92 --- /dev/null +++ b/src/ops/launcher/swa.h @@ -0,0 +1,33 @@ +#pragma once + +#include "ninfer/ops/swa.h" + +namespace ninfer::ops::detail { + +inline constexpr std::int32_t kSwaMaxCandidateSplit = 32; + +enum class SwaRoute { + Direct, + SplitKv, +}; + +struct SwaPlan { + SwaRoute route; + std::int32_t tokens; + std::int32_t warps; + std::int32_t split_capacity; + std::int32_t max_context; + std::uint32_t window; +}; + +[[nodiscard]] SwaPlan swa_resolve_plan(std::int32_t tokens, SwaContextExecutionEnvelope envelope, + std::uint32_t window); +[[nodiscard]] const char* swa_route_name(SwaRoute route); + +void swa_launch(const Tensor& q, const Tensor& query_k, const Tensor& query_v, + const Tensor& positions, const Tensor& valid_columns, const Tensor& lanes, + float scale, const CyclicKVCacheLayerView& context, const SwaPlan& plan, + Tensor& partial_acc, Tensor& partial_m, Tensor& partial_l, Tensor& out, + cudaStream_t stream); + +} // namespace ninfer::ops::detail diff --git a/src/ops/linear/bf16/bf16_dispatch.cpp b/src/ops/linear/bf16/bf16_dispatch.cpp index da0c705a8b..6170648087 100644 --- a/src/ops/linear/bf16/bf16_dispatch.cpp +++ b/src/ops/linear/bf16/bf16_dispatch.cpp @@ -9,10 +9,21 @@ namespace ninfer::ops::detail { Bf16Launch select_bf16_a16_launch(std::int32_t n, std::int32_t k, std::int32_t t) { - const bool supported_problem = (n == 14336 && k == 5120) || (n == 5120 && k == 6144); - if (!supported_problem || t <= 0) { + const bool legacy_problem = (n == 14336 && k == 5120) || (n == 5120 && k == 6144); + const bool dflash2_problem = (n == 5120 && k == 25600) || (n == 6144 && k == 5120) || + (n == 4096 && k == 5120) || (n == 1024 && k == 5120) || + (n == 5120 && k == 4096) || (n == 1280 && k == 5120) || + (n == 34816 && k == 5120) || (n == 5120 && k == 17408) || + (n == 256 && k == 5120); + if ((!legacy_problem && !dflash2_problem) || t <= 0) { throw std::invalid_argument("bf16 linear: unsupported shape or T"); } + if (dflash2_problem) { + // The generic MMA core already tiles arbitrary admitted n/k with the + // 64x128x64 production schedule; the DFlash2 draft never needs the + // fixed-shape decode/small-T specializations (T = verify width 8..64). + return launch_bf16_mma; + } if (t == 1) { return launch_bf16_decode; } const std::int32_t small_t_end = n == 5120 ? kBf16SmallTMaxTokens : kBf16LinearSmallTDispatchEnd; diff --git a/src/ops/linear/bf16/bf16_gemm_mma.cu b/src/ops/linear/bf16/bf16_gemm_mma.cu index 33df64ab2e..f4a84d336e 100644 --- a/src/ops/linear/bf16/bf16_gemm_mma.cu +++ b/src/ops/linear/bf16/bf16_gemm_mma.cu @@ -57,6 +57,42 @@ void launch_bf16_mma(const Tensor& x, const Weight& weight, Tensor& out, cudaStr launch_geometry>(x, weight, out, stream); return; } + if (weight.n == 5120 && weight.k == 25600) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 6144 && weight.k == 5120) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 4096 && weight.k == 5120) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 1024 && weight.k == 5120) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 5120 && weight.k == 4096) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 1280 && weight.k == 5120) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 34816 && weight.k == 5120) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 5120 && weight.k == 17408) { + launch_geometry>(x, weight, out, stream); + return; + } + if (weight.n == 256 && weight.k == 5120) { + launch_geometry>(x, weight, out, stream); + return; + } throw std::invalid_argument("bf16 linear MMA: unsupported exact problem"); } diff --git a/src/ops/softmax_attention/dense/causal_cache/causal_softmax_attention.cpp b/src/ops/softmax_attention/dense/causal_cache/causal_softmax_attention.cpp index 62abe13b63..b455d42784 100644 --- a/src/ops/softmax_attention/dense/causal_cache/causal_softmax_attention.cpp +++ b/src/ops/softmax_attention/dense/causal_cache/causal_softmax_attention.cpp @@ -161,7 +161,7 @@ void validate_envelope(CausalAttentionExecutionEnvelope envelope, const PagedKVL std::int32_t tokens, const char* op) { const std::uint32_t capacity = validate_cache(cache, cache.num_kv_heads, op); if (envelope.min_visible_keys == 0 || envelope.min_visible_keys > envelope.max_visible_keys || - envelope.max_visible_keys > kCausalAttentionMaximumVisibleKeys || + envelope.max_visible_keys > kCausalAttentionMaximumVisibleKeysYarn || envelope.max_visible_keys > capacity) { throw std::invalid_argument(std::string(op) + ": invalid execution envelope"); } @@ -242,7 +242,7 @@ void validate_batched_attention_tensors(const Tensor& q, const Tensor& positions const std::uint32_t capacity = validate_batch_cache(cache, kv_heads, op); if (cache.block_tables.ne[1] < batch || envelope.min_visible_keys == 0 || envelope.min_visible_keys > envelope.max_visible_keys || - envelope.max_visible_keys > kCausalAttentionMaximumVisibleKeys || + envelope.max_visible_keys > kCausalAttentionMaximumVisibleKeysYarn || envelope.max_visible_keys > capacity || envelope.max_visible_keys < static_cast(width)) { throw std::invalid_argument(std::string(op) + ": invalid execution envelope or table"); @@ -361,7 +361,7 @@ std::size_t causal_softmax_attention_workspace_capacity_bytes( if (!supported_dtype || batch_size <= 0 || batch_size > kMaximumBatchSize || min_width <= 0 || max_width < min_width || (batch_size > 1 && max_width > kMaximumVerifyTokens) || envelope.min_visible_keys == 0 || envelope.min_visible_keys > envelope.max_visible_keys || - envelope.max_visible_keys > kCausalAttentionMaximumVisibleKeys || + envelope.max_visible_keys > kCausalAttentionMaximumVisibleKeysYarn || envelope.max_visible_keys < static_cast(max_width)) { throw std::invalid_argument( "causal_softmax_attention workspace: invalid profile or interval"); diff --git a/src/ops/softmax_attention/dense/causal_cache/small_t.cu b/src/ops/softmax_attention/dense/causal_cache/small_t.cu index 6caa50ac56..999cb0f471 100644 --- a/src/ops/softmax_attention/dense/causal_cache/small_t.cu +++ b/src/ops/softmax_attention/dense/causal_cache/small_t.cu @@ -115,8 +115,10 @@ void launch_tc_partial_bf16(const Tensor& q, CacheInput input, const Tensor& pos : static_cast(invocation.table_rows->data), cache.block_tables.ne[0], invocation.width, invocation.full_width, invocation.column_begin, logical_capacity, scale, - static_cast<__nv_bfloat16*>(partial_acc.data), static_cast(partial_m.data), - static_cast(partial_l.data)); + static_cast(cache.cold_slots.data), + static_cast(cache.cold_slot_valid.data), + cache.slot_bytes, static_cast<__nv_bfloat16*>(partial_acc.data), + static_cast(partial_m.data), static_cast(partial_l.data)); CUDA_CHECK(cudaGetLastError()); } @@ -158,7 +160,10 @@ void launch_tc_partial_i8(const Tensor& q, CacheInput input, const Tensor& pos, ? nullptr : static_cast(invocation.table_rows->data), cache.block_tables.ne[0], invocation.full_width, invocation.column_begin, - logical_capacity, scale, static_cast<__nv_bfloat16*>(partial_acc.data), + logical_capacity, scale, + static_cast(cache.cold_slots.data), + static_cast(cache.cold_slot_valid.data), + cache.slot_bytes, static_cast<__nv_bfloat16*>(partial_acc.data), static_cast(partial_m.data), static_cast(partial_l.data)); }; if constexpr (TokenTile == 6) { @@ -216,6 +221,9 @@ PagedKVBatchLayerView single_row_batch_view(const PagedKVLayerView& cache) { .k_scale_pages = cache.k_scale_pages, .v_scale_pages = cache.v_scale_pages, .block_tables = cache.block_table.view({cache.block_table.ne[0], 1}), + .cold_slots = cache.cold_slots, + .cold_slot_valid = cache.cold_slot_valid, + .slot_bytes = cache.slot_bytes, .head_dim = cache.head_dim, .num_kv_heads = cache.num_kv_heads, .dtype = cache.dtype, diff --git a/src/ops/softmax_attention/dense/causal_cache/small_t_bf16.cuh b/src/ops/softmax_attention/dense/causal_cache/small_t_bf16.cuh index 2b8a53b11d..e673ccde7f 100644 --- a/src/ops/softmax_attention/dense/causal_cache/small_t_bf16.cuh +++ b/src/ops/softmax_attention/dense/causal_cache/small_t_bf16.cuh @@ -9,6 +9,7 @@ #include #include +#include "ops/kernel/cold_i8_kernels.cuh" #include "ops/softmax_attention/dense/causal_cache/small_t.cuh" #include @@ -22,6 +23,7 @@ __launch_bounds__(128, 2) __global__ void causal_attention_small_t_tc_partial_bf __nv_bfloat16* cache_v, const std::int32_t* block_tables, const std::int32_t* valid_columns, const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t tokens, std::int32_t full_width, std::int32_t column_begin, std::int32_t logical_capacity, float scale, + const std::uint8_t* cold_slots, const std::int32_t* cold_valid, std::int32_t slot_bytes, __nv_bfloat16* partial_acc, float* partial_m, float* partial_l) { static_assert(TokenTile >= 1 && TokenTile <= 6); static_assert(WarpsPerCta >= 1 && WarpsPerCta <= 4); @@ -217,6 +219,15 @@ __launch_bounds__(128, 2) __global__ void causal_attention_small_t_tc_partial_bf if (kb != 0 && (k0 & kPagedKVPageMask) == 0) { physical_page = physical_pages_s[(k0 >> kPagedKVPageShift) - first_page]; } + // A Bc=32 tile never crosses a 64-token page boundary, so the entry + // cached for this tile decides the whole tile's load path. Cold pages + // carry a sentinel (<= -2): decode E2M1 nibbles + E4M3 g16 scales + // straight from the raw slot into the bf16 tile. + const int entry = physical_pages_s[(k0 >> kPagedKVPageShift) - first_page]; + const bool cold = entry <= -2 && cold_slots != nullptr && slot_bytes >= 1024 + 320 && + cold_valid[(-entry - 2) * (2 * Geometry::KVHeads) + kv_head] != 0 && + cold_valid[(-entry - 2) * (2 * Geometry::KVHeads) + Geometry::KVHeads + + kv_head] != 0; // Stage the bf16 K/V key tile with one cp.async wave (16B/thread, high MLP). // Current-step tokens come from k_new/v_new; tail slots are zeroed. #pragma unroll 1 @@ -236,12 +247,82 @@ __launch_bounds__(128, 2) __global__ void causal_attention_small_t_tc_partial_bf kv_cache_int8_new_index(kv_head, d, new_token); ninfer::ops::cp_async<16>(k_dst, &input.k[off]); ninfer::ops::cp_async<16>(v_dst, &input.v[off]); + } else if (cold) { + const int slot_base = -entry - 2; + const std::int64_t k_off = + static_cast(slot_base * (2 * Geometry::KVHeads) + + kv_head) * + slot_bytes; + const std::int64_t v_off = k_off + static_cast( + Geometry::KVHeads) * + slot_bytes; + const std::uint8_t* k_row = detail::cold_i8_slot_codes(cold_slots + k_off) + + (key & kPagedKVPageMask) * 128; + const std::uint8_t* k_row_s = + detail::cold_i8_slot_scales(cold_slots + k_off) + + (key & kPagedKVPageMask) * 16; + const std::uint8_t* v_row = detail::cold_i8_slot_codes(cold_slots + v_off) + + (key & kPagedKVPageMask) * 128; + const std::uint8_t* v_row_s = + detail::cold_i8_slot_scales(cold_slots + v_off) + + (key & kPagedKVPageMask) * 16; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const int chan = d + i; + const std::uint8_t kb = k_row[chan >> 1]; + const std::uint8_t vb = v_row[chan >> 1]; + const float k_code = + gqa_kv_nvfp4_e2m1_to_f32((chan & 1) ? (kb >> 4) : (kb & 0x0F)); + const float v_code = + gqa_kv_nvfp4_e2m1_to_f32((chan & 1) ? (vb >> 4) : (vb & 0x0F)); + const float k_scale = + gqa_kv_nvfp4_e4m3_to_f32(k_row_s[chan >> 4]); + const float v_scale = + gqa_kv_nvfp4_e4m3_to_f32(v_row_s[chan >> 4]); + k_dst[i] = __float2bfloat16(k_code * k_scale); + v_dst[i] = __float2bfloat16(v_code * v_scale); + } } else { const std::int64_t off = causal_cache_index( physical_page, kv_head, d, key & kPagedKVPageMask); ninfer::ops::cp_async<16>(k_dst, &cache_k[off]); ninfer::ops::cp_async<16>(v_dst, &cache_v[off]); } + } else if (cold) { + const int slot_base = -entry - 2; + const std::int64_t k_off = + static_cast(slot_base * (2 * Geometry::KVHeads) + + kv_head) * + slot_bytes; + const std::int64_t v_off = k_off + static_cast( + Geometry::KVHeads) * + slot_bytes; + const std::uint8_t* k_row = detail::cold_i8_slot_codes(cold_slots + k_off) + + (key & kPagedKVPageMask) * 128; + const std::uint8_t* k_row_s = + detail::cold_i8_slot_scales(cold_slots + k_off) + + (key & kPagedKVPageMask) * 16; + const std::uint8_t* v_row = detail::cold_i8_slot_codes(cold_slots + v_off) + + (key & kPagedKVPageMask) * 128; + const std::uint8_t* v_row_s = + detail::cold_i8_slot_scales(cold_slots + v_off) + + (key & kPagedKVPageMask) * 16; +#pragma unroll + for (int i = 0; i < 8; ++i) { + const int chan = d + i; + const std::uint8_t kb = k_row[chan >> 1]; + const std::uint8_t vb = v_row[chan >> 1]; + const float k_code = + gqa_kv_nvfp4_e2m1_to_f32((chan & 1) ? (kb >> 4) : (kb & 0x0F)); + const float v_code = + gqa_kv_nvfp4_e2m1_to_f32((chan & 1) ? (vb >> 4) : (vb & 0x0F)); + const float k_scale = + gqa_kv_nvfp4_e4m3_to_f32(k_row_s[chan >> 4]); + const float v_scale = + gqa_kv_nvfp4_e4m3_to_f32(v_row_s[chan >> 4]); + k_dst[i] = __float2bfloat16(k_code * k_scale); + v_dst[i] = __float2bfloat16(v_code * v_scale); + } } else { const std::int64_t off = causal_cache_index(physical_page, kv_head, d, key & kPagedKVPageMask); diff --git a/src/ops/softmax_attention/dense/causal_cache/small_t_i8.cuh b/src/ops/softmax_attention/dense/causal_cache/small_t_i8.cuh index 2145eeb585..d67734480b 100644 --- a/src/ops/softmax_attention/dense/causal_cache/small_t_i8.cuh +++ b/src/ops/softmax_attention/dense/causal_cache/small_t_i8.cuh @@ -22,6 +22,7 @@ #include #include +#include "ops/kernel/cold_i8_kernels.cuh" #include "ops/softmax_attention/dense/causal_cache/small_t.cuh" #include "ops/kv_cache/int8_g64_codec.cuh" @@ -62,6 +63,7 @@ __launch_bounds__(WarpsPerCta * 32, MinBlocksPerSm) __global__ const std::int32_t* block_tables, const std::int32_t* valid_columns, const std::int32_t* table_rows, std::int32_t table_stride, std::int32_t full_width, std::int32_t column_begin, std::int32_t logical_capacity, float scale, + const std::uint8_t* cold_slots, const std::int32_t* cold_valid, std::int32_t slot_bytes, __nv_bfloat16* partial_acc, float* partial_m, float* partial_l) { constexpr int Wc = WarpsPerCta; constexpr int RowCount = TokenTile * Geometry::GroupSize; @@ -357,6 +359,99 @@ __launch_bounds__(WarpsPerCta * 32, MinBlocksPerSm) __global__ float l0 = 0.0f, l1 = 0.0f; auto issue_kv_tile = [&](int tile_k0, int physical_page) { + // A Bc=32 tile never crosses a 64-token page boundary. Cold pages + // carry a sentinel (<= -2): decode E2M1 nibbles + E4M3 g16 scales + // from the raw slot back into int8 codes + fp16 g64 scales, matching + // the native planes the hot path stages with cp.async. + const int entry = physical_pages_s[(tile_k0 >> kPagedKVPageShift) - first_page]; + const bool cold = entry <= -2 && cold_slots != nullptr && slot_bytes >= 1024 + 320 && + cold_valid[(-entry - 2) * (2 * Geometry::KVHeads) + kv_head] != 0 && + cold_valid[(-entry - 2) * (2 * Geometry::KVHeads) + Geometry::KVHeads + + kv_head] != 0; + if (cold) { + const int slot_base = -entry - 2; + const std::int64_t k_off = + static_cast(slot_base * (2 * Geometry::KVHeads) + kv_head) * + slot_bytes; + const std::int64_t v_off = + k_off + static_cast(Geometry::KVHeads) * slot_bytes; + for (int key_l = tid; key_l < Bc; key_l += Threads) { + const int key = tile_k0 + key_l; + if (key >= split_start && key < split_end) { + const int row = key & kPagedKVPageMask; + const std::uint8_t* k_rs = + detail::cold_i8_slot_scales(cold_slots + k_off) + row * 16; + const std::uint8_t* v_rs = + detail::cold_i8_slot_scales(cold_slots + v_off) + row * 16; +#pragma unroll + for (int g = 0; g < Groups; ++g) { + float mk = 0.0f, mv = 0.0f; +#pragma unroll + for (int s = 0; s < 4; ++s) { + mk = fmaxf(mk, gqa_kv_nvfp4_e4m3_to_f32(k_rs[g * 4 + s])); + mv = fmaxf(mv, gqa_kv_nvfp4_e4m3_to_f32(v_rs[g * 4 + s])); + } + k_scale_s[key_l * Groups + g] = __float2half(mk * 6.0f / 127.0f); + v_scale_s[key_l * Groups + g] = __float2half(mv * 6.0f / 127.0f); + } + } else { + store_vec(&k_scale_s[key_l * Groups], make_int2(0, 0)); + store_vec(&v_scale_s[key_l * Groups], make_int2(0, 0)); + } + } +#pragma unroll 1 + for (int chunk = tid; chunk < Bc * (D / 16); chunk += Threads) { + const int key_l = chunk / (D / 16); + const int dc = chunk - key_l * (D / 16); + const int d = dc * 16; + const int key = tile_k0 + key_l; + std::int8_t* dst = &k_i8[key_l * D + causal_small_t_tc_swz(key_l, dc * 8) * 2]; + if (key >= split_start && key < split_end) { + const int row = key & kPagedKVPageMask; + const std::uint8_t* k_rc = + detail::cold_i8_slot_codes(cold_slots + k_off) + row * 128; + const std::uint8_t* k_rs = + detail::cold_i8_slot_scales(cold_slots + k_off) + row * 16; + const std::uint8_t* v_rc = + detail::cold_i8_slot_codes(cold_slots + v_off) + row * 128; + const std::uint8_t* v_rs = + detail::cold_i8_slot_scales(cold_slots + v_off) + row * 16; + // 16 channels never straddle a 64-channel group, so each + // chunk recomputes its own upper-bound group scale. + const int g = d >> 6; + float mk = 0.0f, mv = 0.0f; +#pragma unroll + for (int s = 0; s < 4; ++s) { + mk = fmaxf(mk, gqa_kv_nvfp4_e4m3_to_f32(k_rs[g * 4 + s])); + mv = fmaxf(mv, gqa_kv_nvfp4_e4m3_to_f32(v_rs[g * 4 + s])); + } + const float k_inv = mk > 0.0f ? 127.0f / (mk * 6.0f) : 0.0f; + const float v_inv = mv > 0.0f ? 127.0f / (mv * 6.0f) : 0.0f; +#pragma unroll + for (int i = 0; i < 16; ++i) { + const int chan = d + i; + const std::uint8_t kb = k_rc[chan >> 1]; + const std::uint8_t vb = v_rc[chan >> 1]; + const float k_code = + gqa_kv_nvfp4_e2m1_to_f32((chan & 1) ? (kb >> 4) : (kb & 0x0F)); + const float v_code = + gqa_kv_nvfp4_e2m1_to_f32((chan & 1) ? (vb >> 4) : (vb & 0x0F)); + const float k_scale = gqa_kv_nvfp4_e4m3_to_f32(k_rs[chan >> 4]); + const float v_scale = gqa_kv_nvfp4_e4m3_to_f32(v_rs[chan >> 4]); + int kc = __float2int_rn(k_code * k_scale * k_inv); + int vc = __float2int_rn(v_code * v_scale * v_inv); + kc = max(-127, min(127, kc)); + vc = max(-127, min(127, vc)); + dst[i] = static_cast(kc); + v_i8[key_l * D + chan] = static_cast(vc); + } + } else { + store_vec(dst, make_int4(0, 0, 0, 0)); + store_vec(&v_i8[key_l * D + d], make_int4(0, 0, 0, 0)); + } + } + return; + } for (int key_l = tid; key_l < Bc; key_l += Threads) { const int key = tile_k0 + key_l; if (key >= split_start && key < split_end) { diff --git a/src/ops/wrapper/bidirectional_gqa_attention.cpp b/src/ops/wrapper/bidirectional_gqa_attention.cpp new file mode 100644 index 0000000000..335e1c8ca7 --- /dev/null +++ b/src/ops/wrapper/bidirectional_gqa_attention.cpp @@ -0,0 +1,168 @@ +#include "ninfer/ops/bidirectional_gqa_attention.h" + +#include "core/layout.h" +#include "ops/launcher/bidirectional_gqa_attention.h" + +#include +#include +#include +#include +#include +#include + +namespace ninfer::ops { +namespace { + +constexpr std::int32_t kHeadDim = 128; +constexpr std::int32_t kQHeads = 32; +constexpr std::int32_t kKVHeads = 8; +constexpr float kExpectedScale = 0.08838834764831844055f; + +void require_shape(const Tensor& tensor, std::int32_t n0, std::int32_t n1, std::int32_t n2, + std::int32_t n3, const char* op, const char* name) { + if (tensor.ne[0] != n0 || tensor.ne[1] != n1 || tensor.ne[2] != n2 || tensor.ne[3] != n3) { + throw std::invalid_argument(std::string(op) + ": invalid shape for " + name); + } +} + +void require_contiguous_nonnull(const Tensor& tensor, const char* op, const char* name) { + if (!tensor.is_contiguous()) { + throw std::invalid_argument(std::string(op) + ": " + name + " must be contiguous"); + } + if (tensor.data == nullptr) { + throw std::invalid_argument(std::string(op) + ": " + name + " data must be non-null"); + } +} + +std::uint32_t validate_context(const PagedKVBatchLayerView& context, const char* op) { + if (context.dtype != DType::BF16 || context.quant_group != 0 || + context.num_kv_heads != kKVHeads || context.head_dim != kHeadDim) { + throw std::invalid_argument(std::string(op) + ": invalid context geometry or dtype"); + } + const std::int32_t physical_pages = context.k_pages.ne[2]; + if (physical_pages <= 0 || context.v_pages.ne[2] != physical_pages || + context.block_tables.ne[0] <= 0 || context.block_tables.ne[1] <= 0) { + throw std::invalid_argument(std::string(op) + ": invalid context capacity"); + } + if (context.k_pages.dtype != DType::BF16 || context.v_pages.dtype != DType::BF16) { + throw std::invalid_argument(std::string(op) + ": context K/V must be BF16"); + } + require_shape(context.k_pages, kHeadDim, kPagedKVPageSize, physical_pages, kKVHeads, op, + "context k pages"); + require_shape(context.v_pages, kHeadDim, kPagedKVPageSize, physical_pages, kKVHeads, op, + "context v pages"); + require_contiguous_nonnull(context.k_pages, op, "context k pages"); + require_contiguous_nonnull(context.v_pages, op, "context v pages"); + require_shape(context.block_tables, context.block_tables.ne[0], context.block_tables.ne[1], 1, + 1, op, "context block tables"); + if (context.block_tables.dtype != DType::I32) { + throw std::invalid_argument(std::string(op) + ": context block tables must be I32"); + } + require_contiguous_nonnull(context.block_tables, op, "context block tables"); + if (context.k_scale_pages.data != nullptr || context.v_scale_pages.data != nullptr) { + throw std::invalid_argument(std::string(op) + ": BF16 context must not have scales"); + } + const std::uint64_t logical_capacity = + static_cast(context.block_tables.ne[0]) * kPagedKVPageSize; + if (logical_capacity > static_cast(std::numeric_limits::max())) { + throw std::overflow_error(std::string(op) + ": logical context capacity exceeds int32"); + } + return static_cast(logical_capacity); +} + +struct PartialWorkspace { + Tensor acc; + Tensor m; + Tensor l; +}; + +template +PartialWorkspace allocate_workspace(Allocator& workspace, std::int32_t tokens, std::int32_t splits, + std::int32_t batch_size) { + return { + workspace.alloc(DType::BF16, {kHeadDim, kQHeads, tokens, splits * batch_size}), + workspace.alloc(DType::FP32, {kQHeads, tokens, splits * batch_size}), + workspace.alloc(DType::FP32, {kQHeads, tokens, splits * batch_size}), + }; +} + +} // namespace + +std::size_t bidirectional_gqa_attention_workspace_capacity_bytes( + GqaContextExecutionEnvelope envelope, std::int32_t min_tokens, std::int32_t max_tokens, + std::int32_t batch_size) { + if (min_tokens < 1 || max_tokens < min_tokens || max_tokens > 16 || batch_size < 1 || + batch_size > 8 || envelope.min_context > envelope.max_context || + envelope.max_context > + static_cast(std::numeric_limits::max())) { + throw std::invalid_argument( + "bidirectional_gqa_attention workspace: invalid envelope or token interval"); + } + const auto endpoint_capacity = [&](std::int32_t tokens) { + const auto plan = detail::bidirectional_gqa_resolve_plan(tokens, envelope); + WorkspaceLayoutBuilder layout; + (void)allocate_workspace(layout, tokens, plan.split_capacity, batch_size); + return layout.peak_bytes(1); + }; + + std::size_t maximum = 0; + if (min_tokens <= 8) { maximum = endpoint_capacity(std::min(max_tokens, 8)); } + if (max_tokens >= 9) { maximum = std::max(maximum, endpoint_capacity(max_tokens)); } + return maximum; +} + +void bidirectional_gqa_attention(const Tensor& q, const Tensor& query_k, const Tensor& query_v, + const Tensor& context_lengths, const Tensor& valid_columns, + const Tensor& table_rows, float scale, + const PagedKVBatchLayerView& context, + GqaContextExecutionEnvelope envelope, WorkspaceArena& workspace, + Tensor& out, cudaStream_t stream) { + constexpr const char* op = "bidirectional_gqa_attention"; + if (q.dtype != DType::BF16 || query_k.dtype != DType::BF16 || query_v.dtype != DType::BF16 || + out.dtype != DType::BF16) { + throw std::invalid_argument("bidirectional_gqa_attention: q/k/v/out must be BF16"); + } + if (context_lengths.dtype != DType::I32 || valid_columns.dtype != DType::I32 || + table_rows.dtype != DType::I32) { + throw std::invalid_argument( + "bidirectional_gqa_attention: lengths/valid_columns/table_rows must be I32"); + } + const std::int32_t tokens = q.ne[2]; + const std::int32_t batch = q.ne[3]; + if (tokens < 1 || tokens > 16) { + throw std::invalid_argument("bidirectional_gqa_attention: optimized domain is T=1..16"); + } + if (batch < 1 || batch > 8) { + throw std::invalid_argument("bidirectional_gqa_attention: B must be 1..8"); + } + require_shape(q, kHeadDim, kQHeads, tokens, batch, op, "q"); + require_shape(query_k, kHeadDim, kKVHeads, tokens, batch, op, "query k"); + require_shape(query_v, kHeadDim, kKVHeads, tokens, batch, op, "query v"); + require_shape(context_lengths, batch, 1, 1, 1, op, "context lengths"); + require_shape(valid_columns, batch, 1, 1, 1, op, "valid columns"); + require_shape(table_rows, batch, 1, 1, 1, op, "table rows"); + require_shape(out, kHeadDim, kQHeads, tokens, batch, op, "out"); + require_contiguous_nonnull(q, op, "q"); + require_contiguous_nonnull(query_k, op, "query k"); + require_contiguous_nonnull(query_v, op, "query v"); + require_contiguous_nonnull(context_lengths, op, "context lengths"); + require_contiguous_nonnull(valid_columns, op, "valid columns"); + require_contiguous_nonnull(table_rows, op, "table rows"); + require_contiguous_nonnull(out, op, "out"); + const std::uint32_t logical_capacity = validate_context(context, op); + if (envelope.min_context > envelope.max_context || envelope.max_context > logical_capacity) { + throw std::invalid_argument("bidirectional_gqa_attention: invalid execution envelope"); + } + if (!std::isfinite(scale) || std::abs(scale - kExpectedScale) > 1e-7f) { + throw std::invalid_argument("bidirectional_gqa_attention: scale must be 1/sqrt(128)"); + } + + auto scope = workspace.scope(); + const auto plan = detail::bidirectional_gqa_resolve_plan(tokens, envelope); + PartialWorkspace partial = allocate_workspace(workspace, tokens, plan.split_capacity, batch); + detail::bidirectional_gqa_attention_launch(q, query_k, query_v, context_lengths, valid_columns, + table_rows, scale, context, plan, partial.acc, + partial.m, partial.l, out, stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/cold_i8.cpp b/src/ops/wrapper/cold_i8.cpp new file mode 100644 index 0000000000..bd13e87698 --- /dev/null +++ b/src/ops/wrapper/cold_i8.cpp @@ -0,0 +1,26 @@ +#include "ninfer/ops/cold_i8.h" + +#include "ops/launcher/cold_i8.h" + +#include + +#include + +namespace ninfer::ops { + +void cold_i8_slot_pack_raw(const std::uint8_t* src_codes, const std::uint8_t* src_scales, + int kv_heads, int page_count, std::uint8_t* slots, + std::int32_t* slot_valid, int slot_bytes, cudaStream_t stream) { + detail::cold_i8_slot_pack_launch(src_codes, src_scales, kv_heads, page_count, slots, + slot_valid, slot_bytes, stream); +} + +void cold_i8_slot_restore_raw(const std::uint8_t* slots, int kv_heads, int page_count, + std::int8_t* dst_codes, void* dst_scales_fp16, + int slot_bytes, cudaStream_t stream) { + detail::cold_i8_slot_restore_launch(slots, kv_heads, page_count, dst_codes, + static_cast<__half*>(dst_scales_fp16), slot_bytes, + stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/dflash2_grouped_conv.cpp b/src/ops/wrapper/dflash2_grouped_conv.cpp new file mode 100644 index 0000000000..fb900e4461 --- /dev/null +++ b/src/ops/wrapper/dflash2_grouped_conv.cpp @@ -0,0 +1,45 @@ +#include "ninfer/ops/dflash2_grouped_conv.h" + +#include "ops/launcher/dflash2_grouped_conv.h" + +#include +#include + +namespace ninfer::ops { + +void dflash2_grouped_conv(const Tensor& hidden, const Tensor& delta, const Weight& base, + std::int32_t block_size, std::int32_t group_size, std::int32_t taps, + std::int32_t side, Tensor& out, cudaStream_t stream) { + if (hidden.dtype != DType::BF16 || delta.dtype != DType::BF16 || out.dtype != DType::BF16) { + throw std::invalid_argument("dflash2_grouped_conv: hidden/delta/out must be BF16"); + } + if (!hidden.is_contiguous() || hidden.data == nullptr || !delta.is_contiguous() || + delta.data == nullptr || !out.is_contiguous() || out.data == nullptr) { + throw std::invalid_argument("dflash2_grouped_conv: tensors must be contiguous and non-null"); + } + const std::int32_t hidden_size = hidden.ne[0]; + const std::int32_t tokens = hidden.ne[1]; + if (hidden_size != 5120 || tokens < 1 || tokens > 64 || block_size != 8 || + group_size != 16 || taps != 2 || side < 0 || side > 1 || + (block_size & (block_size - 1)) != 0) { + throw std::invalid_argument( + "dflash2_grouped_conv: registered domain is H=5120, T=1..64, taps=2, G=16, " + "block=8, side in {0,1}"); + } + if (hidden_size % group_size != 0 || out.ne[0] != hidden_size || out.ne[1] != tokens) { + throw std::invalid_argument("dflash2_grouped_conv: invalid hidden/out shapes"); + } + const std::int32_t groups = hidden_size / group_size; + if (delta.ne[0] != 2 * taps * groups || delta.ne[1] != tokens) { + throw std::invalid_argument("dflash2_grouped_conv: invalid delta shape"); + } + if (base.qtype != QType::BF16_CTRL || base.n != taps || base.k != hidden_size || + base.qdata == nullptr) { + throw std::invalid_argument("dflash2_grouped_conv: base must be BF16 [taps,H]"); + } + + detail::dflash2_grouped_conv_launch(hidden, delta, base, block_size, group_size, taps, side, + out, stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/dflash2_selector.cpp b/src/ops/wrapper/dflash2_selector.cpp new file mode 100644 index 0000000000..0c4d4aa4ea --- /dev/null +++ b/src/ops/wrapper/dflash2_selector.cpp @@ -0,0 +1,57 @@ +#include "ninfer/ops/dflash2_selector.h" + +#include "ops/launcher/dflash2_selector.h" + +#include +#include + +namespace ninfer::ops { + +void dflash2_selector(const Tensor& unary_logits, const Tensor& projected_hidden, + const Weight& predecessor_codebook, const Weight& successor_codebook, + const Tensor& anchors, Tensor& candidates, Tensor& unary, Tensor& scores, + Tensor& drafts, std::int32_t steps, std::int32_t top_k, cudaStream_t stream) { + const auto invalid = [](const char* message) { throw std::invalid_argument(message); }; + if (unary_logits.dtype != DType::BF16 || projected_hidden.dtype != DType::BF16 || + anchors.dtype != DType::I32 || candidates.dtype != DType::I32 || + unary.dtype != DType::FP32 || scores.dtype != DType::FP32 || drafts.dtype != DType::I32) { + invalid("dflash2_selector: logits/projected must be BF16, ids I32, scores F32"); + } + const auto contiguous = [](const Tensor& tensor) { + return tensor.is_contiguous() && tensor.data != nullptr; + }; + if (!contiguous(unary_logits) || !contiguous(projected_hidden) || !contiguous(anchors) || + !contiguous(candidates) || !contiguous(unary) || !contiguous(scores) || + !contiguous(drafts)) { + invalid("dflash2_selector: tensors must be contiguous and non-null"); + } + const std::int32_t vocab = unary_logits.ne[0]; + const std::int32_t tokens = unary_logits.ne[1]; + const std::int32_t rank = projected_hidden.ne[0]; + const std::int32_t batch = anchors.ne[0]; + if (vocab != 248320 || rank != detail::kDflash2SelectorRank || steps != 7 || + top_k != detail::kDflash2SelectorTopK || batch < 1 || batch > 8 || + tokens != steps * batch || + projected_hidden.ne[1] != tokens) { + invalid("dflash2_selector: registered domain is V=248320, R=256, S=7, B=1..8, K=16"); + } + if (candidates.ne[0] != batch || candidates.ne[1] != steps || candidates.ne[2] != top_k || + unary.ne[0] != batch || unary.ne[1] != steps || unary.ne[2] != top_k || + scores.ne[0] != batch || scores.ne[1] != steps || scores.ne[2] != top_k || + scores.ne[3] != top_k || drafts.ne[0] != tokens) { + invalid("dflash2_selector: scratch or draft shapes are inconsistent"); + } + if (predecessor_codebook.qtype != QType::BF16_CTRL || + successor_codebook.qtype != QType::BF16_CTRL || predecessor_codebook.n != vocab || + predecessor_codebook.k != rank || successor_codebook.n != vocab || + successor_codebook.k != rank || predecessor_codebook.qdata == nullptr || + successor_codebook.qdata == nullptr) { + invalid("dflash2_selector: codebooks must be BF16 [V,256]"); + } + + detail::dflash2_selector_launch(unary_logits, projected_hidden, predecessor_codebook, + successor_codebook, anchors, candidates, unary, scores, drafts, + steps, top_k, stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/entropy_cold_requant.cpp b/src/ops/wrapper/entropy_cold_requant.cpp new file mode 100644 index 0000000000..63527cf1a1 --- /dev/null +++ b/src/ops/wrapper/entropy_cold_requant.cpp @@ -0,0 +1,23 @@ +#include "ninfer/ops/entropy_cold_requant.h" + +#include "ops/launcher/entropy_cold_requant.h" + +#include + +namespace ninfer::ops { + +void entropy_cold_requant_raw(const std::uint8_t* src_codes, const std::uint8_t* src_scales, + EntropyColdRequantMode mode, int kv_heads, int page_count, + std::uint8_t* dst_codes, std::uint8_t* dst_scales, + cudaStream_t stream) { + detail::ColdRequantSource source = detail::ColdRequantSource::Nvfp4G16; + if (mode == EntropyColdRequantMode::Int8G64) { + source = detail::ColdRequantSource::Int8G64; + } else if (mode == EntropyColdRequantMode::Iso3VG16) { + source = detail::ColdRequantSource::Iso3VG16; + } + detail::entropy_cold_requant_raw_launch(src_codes, src_scales, source, kv_heads, page_count, + dst_codes, dst_scales, stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/entropy_nvfp4_slot.cpp b/src/ops/wrapper/entropy_nvfp4_slot.cpp new file mode 100644 index 0000000000..7bd66bdabb --- /dev/null +++ b/src/ops/wrapper/entropy_nvfp4_slot.cpp @@ -0,0 +1,44 @@ +#include "ninfer/ops/entropy_nvfp4_slot.h" + +#include "ops/launcher/entropy_nvfp4_slot.h" + +#include + +namespace ninfer::ops { + +void entropy_nvfp4_slot_encode_raw(const std::uint8_t* codes, const std::uint8_t* scales, + int kv_heads, int page_count, std::uint8_t* slots, + int slot_bytes, std::int32_t* slot_valid, + const std::int32_t* page_ids, int valid_page_stride, + cudaStream_t stream) { + detail::entropy_nvfp4_slot_encode_launch(codes, scales, kv_heads, page_count, slots, + slot_bytes, slot_valid, page_ids, + valid_page_stride, stream); +} + +void entropy_nvfp4_slot_decode_grid_raw(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dst, cudaStream_t stream) { + detail::entropy_nvfp4_slot_decode_grid_launch(slots, slot_bytes, slot_base, kv_heads, dst, + stream); +} + +void entropy_nvfp4_slot_scales_scatter_raw(const std::uint8_t* slots, int slot_bytes, + int slot_page_stride, int kv_heads, + int page_count, const std::int32_t* page_ids, + int scale_page_stride, std::uint8_t* scales, + cudaStream_t stream) { + detail::entropy_nvfp4_slot_scales_scatter_launch(slots, slot_bytes, slot_page_stride, + kv_heads, page_count, page_ids, + scale_page_stride, scales, stream); +} + +void entropy_nvfp4_slot_restore_plane_raw(const std::uint8_t* slots, int slot_bytes, + std::int32_t slot_base, int kv_heads, + std::uint8_t* dec_scratch, + std::uint8_t* dst_codes, cudaStream_t stream) { + detail::entropy_nvfp4_slot_restore_plane_launch(slots, slot_bytes, slot_base, kv_heads, + dec_scratch, dst_codes, stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/gqa_attention.cpp b/src/ops/wrapper/gqa_attention.cpp new file mode 100644 index 0000000000..dfe108e7a8 --- /dev/null +++ b/src/ops/wrapper/gqa_attention.cpp @@ -0,0 +1,543 @@ +// ninfer::ops - GQA A1/A2/A3 validation and finite route dispatch. +#include "ninfer/ops/gqa_attention.h" + +#include "core/layout.h" +#include "ops/launcher/gqa_attention.h" + +#include +#include +#include +#include +#include +#include + +namespace ninfer::ops { +namespace { + +constexpr std::int32_t kHeadDim = 256; +constexpr std::int32_t kQuantGroup = 64; +constexpr std::int32_t kNvfp4QuantGroup = 16; +constexpr float kExpectedScale = 0.0625f; +constexpr std::int32_t kSmallTChunkTokens = 6; +constexpr std::int32_t kMaximumVerifyTokens = 16; +constexpr std::int32_t kMaximumBatchSize = 8; +constexpr std::uint32_t kTwoChunkPromptVisibleKeys = 512; +constexpr std::uint32_t kThreeChunkPromptVisibleKeys = 1024; + +std::int32_t kv_heads_for_q_heads(std::int32_t q_heads, const char* op) { + if (q_heads == 24) { return 4; } + if (q_heads == 16) { return 2; } + throw std::invalid_argument(std::string(op) + ": unsupported Q/KV head geometry"); +} + +void require_kv_heads(std::int32_t kv_heads, const char* op) { + if (kv_heads != 4 && kv_heads != 2) { + throw std::invalid_argument(std::string(op) + ": unsupported KV head geometry"); + } +} + +void require_shape(const Tensor& tensor, std::int32_t n0, std::int32_t n1, std::int32_t n2, + std::int32_t n3, const char* op, const char* name) { + if (tensor.ne[0] != n0 || tensor.ne[1] != n1 || tensor.ne[2] != n2 || tensor.ne[3] != n3) { + throw std::invalid_argument(std::string(op) + ": invalid shape for " + name); + } +} + +void require_contiguous_nonnull(const Tensor& tensor, const char* op, const char* name) { + if (!tensor.is_contiguous()) { + throw std::invalid_argument(std::string(op) + ": " + name + " must be contiguous"); + } + if (tensor.data == nullptr) { + throw std::invalid_argument(std::string(op) + ": " + name + " data must be non-null"); + } +} + +std::uint32_t validate_cache(const PagedKVLayerView& cache, std::int32_t kv_heads, const char* op) { + const bool nvfp4 = cache.dtype == DType::NVFP4; + const bool fp8 = cache.dtype == DType::FP8_E4M3FN; + const bool iso3 = cache.dtype == DType::ISO3; + const bool packed16 = nvfp4 || fp8 || iso3; + if ((cache.dtype != DType::BF16 && cache.dtype != DType::I8 && !packed16) || + cache.num_kv_heads != kv_heads || cache.head_dim != kHeadDim) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache geometry or dtype"); + } + if (cache.dtype == DType::BF16 && cache.quant_group != 0) { + throw std::invalid_argument(std::string(op) + ": BF16 KV cache must not have quant_group"); + } + if (cache.dtype == DType::I8 && cache.quant_group != kQuantGroup) { + throw std::invalid_argument(std::string(op) + ": I8 KV cache must use quant_group 64"); + } + if (packed16 && cache.quant_group != kNvfp4QuantGroup) { + throw std::invalid_argument(std::string(op) + ": packed KV cache must use quant_group 16"); + } + + const std::int32_t physical_pages = cache.k_pages.ne[3]; + const std::int32_t logical_pages = cache.block_table.ne[0]; + const std::int64_t capacity = static_cast(logical_pages) * kPagedKVPageSize; + if (physical_pages <= 0 || logical_pages <= 0 || + capacity > std::numeric_limits::max()) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache capacity"); + } + + const DType code_dtype = + cache.dtype == DType::I8 + ? DType::I8 + : (fp8 ? DType::FP8_E4M3FN : (packed16 ? DType::U8 : DType::BF16)); + if (cache.k_pages.dtype != code_dtype || cache.v_pages.dtype != code_dtype) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache code dtype"); + } + const std::int32_t code_leading = + nvfp4 ? kHeadDim / 2 : (iso3 ? kHeadDim / 2 : kHeadDim); + require_shape(cache.k_pages, code_leading, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache k pages"); + require_shape(cache.v_pages, code_leading, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache v pages"); + require_contiguous_nonnull(cache.k_pages, op, "cache k pages"); + require_contiguous_nonnull(cache.v_pages, op, "cache v pages"); + if (cache.block_table.dtype != DType::I32) { + throw std::invalid_argument(std::string(op) + ": block table must be I32"); + } + require_shape(cache.block_table, logical_pages, 1, 1, 1, op, "block table"); + require_contiguous_nonnull(cache.block_table, op, "block table"); + + if (cache.dtype == DType::BF16) { + if (cache.k_scale_pages.data != nullptr || cache.v_scale_pages.data != nullptr) { + throw std::invalid_argument(std::string(op) + ": BF16 KV cache must not have scales"); + } + return static_cast(capacity); + } + + if (packed16) { + constexpr std::int32_t groups = kHeadDim / kNvfp4QuantGroup; + if (cache.k_scale_pages.dtype != DType::FP8_E4M3FN || + cache.v_scale_pages.dtype != DType::FP8_E4M3FN) { + throw std::invalid_argument(std::string(op) + ": invalid packed KV cache scale dtype"); + } + require_shape(cache.k_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache k scale pages"); + require_shape(cache.v_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache v scale pages"); + require_contiguous_nonnull(cache.k_scale_pages, op, "cache k scale pages"); + require_contiguous_nonnull(cache.v_scale_pages, op, "cache v scale pages"); + return static_cast(capacity); + } + + constexpr std::int32_t groups = kHeadDim / kQuantGroup; + if (cache.k_scale_pages.dtype != DType::FP16 || cache.v_scale_pages.dtype != DType::FP16) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache scale dtype"); + } + require_shape(cache.k_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache k scale pages"); + require_shape(cache.v_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache v scale pages"); + require_contiguous_nonnull(cache.k_scale_pages, op, "cache k scale pages"); + require_contiguous_nonnull(cache.v_scale_pages, op, "cache v scale pages"); + return static_cast(capacity); +} + +std::uint32_t validate_batch_cache(const PagedKVBatchLayerView& cache, std::int32_t kv_heads, + const char* op) { + const bool nvfp4 = cache.dtype == DType::NVFP4; + const bool fp8 = cache.dtype == DType::FP8_E4M3FN; + const bool iso3 = cache.dtype == DType::ISO3; + const bool packed16 = nvfp4 || fp8 || iso3; + if ((cache.dtype != DType::BF16 && cache.dtype != DType::I8 && !packed16) || + cache.num_kv_heads != kv_heads || cache.head_dim != kHeadDim) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache geometry or dtype"); + } + if (cache.dtype == DType::BF16 && cache.quant_group != 0) { + throw std::invalid_argument(std::string(op) + ": BF16 KV cache must not have quant_group"); + } + if (cache.dtype == DType::I8 && cache.quant_group != kQuantGroup) { + throw std::invalid_argument(std::string(op) + ": I8 KV cache must use quant_group 64"); + } + if (packed16 && cache.quant_group != kNvfp4QuantGroup) { + throw std::invalid_argument(std::string(op) + ": packed KV cache must use quant_group 16"); + } + + const std::int32_t physical_pages = cache.k_pages.ne[3]; + const std::int32_t logical_pages = cache.block_tables.ne[0]; + const std::int32_t table_rows = cache.block_tables.ne[1]; + const std::int64_t capacity = static_cast(logical_pages) * kPagedKVPageSize; + if (physical_pages <= 0 || logical_pages <= 0 || table_rows <= 0 || + capacity > std::numeric_limits::max()) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache capacity"); + } + + const DType code_dtype = + cache.dtype == DType::I8 + ? DType::I8 + : (fp8 ? DType::FP8_E4M3FN : (packed16 ? DType::U8 : DType::BF16)); + if (cache.k_pages.dtype != code_dtype || cache.v_pages.dtype != code_dtype) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache code dtype"); + } + const std::int32_t code_leading = + nvfp4 ? kHeadDim / 2 : (iso3 ? kHeadDim / 2 : kHeadDim); + require_shape(cache.k_pages, code_leading, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache k pages"); + require_shape(cache.v_pages, code_leading, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache v pages"); + require_contiguous_nonnull(cache.k_pages, op, "cache k pages"); + require_contiguous_nonnull(cache.v_pages, op, "cache v pages"); + if (cache.block_tables.dtype != DType::I32) { + throw std::invalid_argument(std::string(op) + ": block tables must be I32"); + } + require_shape(cache.block_tables, logical_pages, table_rows, 1, 1, op, "block tables"); + require_contiguous_nonnull(cache.block_tables, op, "block tables"); + + if (cache.dtype == DType::BF16) { + if (cache.k_scale_pages.data != nullptr || cache.v_scale_pages.data != nullptr) { + throw std::invalid_argument(std::string(op) + ": BF16 KV cache must not have scales"); + } + return static_cast(capacity); + } + + if (packed16) { + constexpr std::int32_t groups = kHeadDim / kNvfp4QuantGroup; + if (cache.k_scale_pages.dtype != DType::FP8_E4M3FN || + cache.v_scale_pages.dtype != DType::FP8_E4M3FN) { + throw std::invalid_argument(std::string(op) + ": invalid NVFP4 KV cache scale dtype"); + } + require_shape(cache.k_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache k scale pages"); + require_shape(cache.v_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache v scale pages"); + require_contiguous_nonnull(cache.k_scale_pages, op, "cache k scale pages"); + require_contiguous_nonnull(cache.v_scale_pages, op, "cache v scale pages"); + return static_cast(capacity); + } + + constexpr std::int32_t groups = kHeadDim / kQuantGroup; + if (cache.k_scale_pages.dtype != DType::FP16 || cache.v_scale_pages.dtype != DType::FP16) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache scale dtype"); + } + require_shape(cache.k_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache k scale pages"); + require_shape(cache.v_scale_pages, groups, kPagedKVPageSize, kv_heads, physical_pages, op, + "cache v scale pages"); + require_contiguous_nonnull(cache.k_scale_pages, op, "cache k scale pages"); + require_contiguous_nonnull(cache.v_scale_pages, op, "cache v scale pages"); + return static_cast(capacity); +} + +void validate_envelope(GqaExecutionEnvelope envelope, const PagedKVLayerView& cache, + std::int32_t tokens, const char* op) { + const std::uint32_t capacity = validate_cache(cache, cache.num_kv_heads, op); + if (envelope.min_visible_keys == 0 || envelope.min_visible_keys > envelope.max_visible_keys || + envelope.max_visible_keys > kGqaAttentionMaximumVisibleKeys || + envelope.max_visible_keys > capacity) { + throw std::invalid_argument(std::string(op) + ": invalid execution envelope"); + } + if (envelope.max_visible_keys < static_cast(tokens)) { + throw std::invalid_argument(std::string(op) + ": execution envelope is shorter than T"); + } +} + +void validate_attention_tensors(const Tensor& q, const Tensor& positions, const Tensor& out, + const PagedKVLayerView& cache, GqaExecutionEnvelope envelope, + float scale, const char* op) { + if (q.dtype != DType::BF16 || out.dtype != DType::BF16) { + throw std::invalid_argument(std::string(op) + ": q/out must be BF16"); + } + if (positions.dtype != DType::I32) { + throw std::invalid_argument(std::string(op) + ": positions must be I32"); + } + if (!std::isfinite(scale) || std::abs(scale - kExpectedScale) > 1.0e-6f) { + throw std::invalid_argument(std::string(op) + ": scale must be 1/sqrt(256)"); + } + const std::int32_t q_heads = q.ne[1]; + const std::int32_t kv_heads = kv_heads_for_q_heads(q_heads, op); + const std::int32_t tokens = q.ne[2]; + if (tokens <= 0) { throw std::invalid_argument(std::string(op) + ": T must be positive"); } + require_shape(q, kHeadDim, q_heads, tokens, 1, op, "q"); + require_shape(positions, tokens, 1, 1, 1, op, "positions"); + require_shape(out, kHeadDim, q_heads, tokens, 1, op, "out"); + require_contiguous_nonnull(q, op, "q"); + require_contiguous_nonnull(positions, op, "positions"); + require_contiguous_nonnull(out, op, "out"); + if (cache.num_kv_heads != kv_heads) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache head geometry"); + } + validate_envelope(envelope, cache, tokens, op); +} + +void validate_batched_attention_tensors(const Tensor& q, const Tensor& positions, + const Tensor& valid_columns, const Tensor& kv_table_rows, + const Tensor& out, const PagedKVBatchLayerView& cache, + GqaExecutionEnvelope envelope, float scale, + const char* op) { + if (q.dtype != DType::BF16 || out.dtype != DType::BF16) { + throw std::invalid_argument(std::string(op) + ": q/out must be BF16"); + } + const bool masked = valid_columns.data != nullptr; + if (positions.dtype != DType::I32 || kv_table_rows.dtype != DType::I32 || + (masked && valid_columns.dtype != DType::I32)) { + throw std::invalid_argument(std::string(op) + ": batch metadata must be I32"); + } + if (!std::isfinite(scale) || std::abs(scale - kExpectedScale) > 1.0e-6f) { + throw std::invalid_argument(std::string(op) + ": scale must be 1/sqrt(256)"); + } + const std::int32_t q_heads = q.ne[1]; + const std::int32_t kv_heads = kv_heads_for_q_heads(q_heads, op); + const std::int32_t width = q.ne[2]; + const std::int32_t batch = q.ne[3]; + if (width <= 0 || batch <= 0 || batch > kMaximumBatchSize || + (batch > 1 && width > kMaximumVerifyTokens)) { + throw std::invalid_argument(std::string(op) + ": unsupported B/W domain"); + } + require_shape(q, kHeadDim, q_heads, width, batch, op, "q"); + require_shape(positions, width, batch, 1, 1, op, "positions"); + if (masked) { require_shape(valid_columns, batch, 1, 1, 1, op, "valid columns"); } + require_shape(kv_table_rows, batch, 1, 1, 1, op, "KV table rows"); + require_shape(out, kHeadDim, q_heads, width, batch, op, "out"); + require_contiguous_nonnull(q, op, "q"); + require_contiguous_nonnull(positions, op, "positions"); + if (masked) { require_contiguous_nonnull(valid_columns, op, "valid columns"); } + require_contiguous_nonnull(kv_table_rows, op, "KV table rows"); + require_contiguous_nonnull(out, op, "out"); + if (cache.num_kv_heads != kv_heads) { + throw std::invalid_argument(std::string(op) + ": invalid KV cache head geometry"); + } + const std::uint32_t capacity = validate_batch_cache(cache, kv_heads, op); + if (cache.block_tables.ne[1] < batch || envelope.min_visible_keys == 0 || + envelope.min_visible_keys > envelope.max_visible_keys || + envelope.max_visible_keys > kGqaAttentionMaximumVisibleKeys || + envelope.max_visible_keys > capacity || + envelope.max_visible_keys < static_cast(width)) { + throw std::invalid_argument(std::string(op) + ": invalid execution envelope or table"); + } +} + +struct SmallTWorkspace { + Tensor acc; + Tensor m; + Tensor l; +}; + +template +SmallTWorkspace allocate_small_t_workspace(Allocator& workspace, std::int32_t q_heads, + std::int32_t tokens, std::int32_t splits, + std::int32_t batch_size = 1) { + return { + workspace.alloc(DType::BF16, {kHeadDim, q_heads, tokens, splits * batch_size}), + workspace.alloc(DType::FP32, {q_heads, tokens, splits * batch_size}), + workspace.alloc(DType::FP32, {q_heads, tokens, splits * batch_size}), + }; +} + +template +void for_each_small_t_chunk(const Tensor& q, const Tensor& positions, WorkspaceArena& workspace, + DType cache_dtype, GqaExecutionEnvelope envelope, Tensor& out, + Launch&& launch) { + for (std::int32_t begin = 0; begin < q.ne[2]; begin += kSmallTChunkTokens) { + const std::int32_t count = std::min(kSmallTChunkTokens, q.ne[2] - begin); + auto chunk_scope = workspace.scope(); + const std::int32_t splits = + detail::gqa_attention_split_capacity(q.ne[1], count, cache_dtype, envelope); + SmallTWorkspace partial = allocate_small_t_workspace(workspace, q.ne[1], count, splits); + Tensor q_chunk = q.slice(2, begin, count); + Tensor position_chunk = positions.slice(0, begin, count); + Tensor out_chunk = out.slice(2, begin, count); + launch(begin, count, q_chunk, position_chunk, partial, out_chunk); + } +} + +void launch_chunked_small_t(const Tensor& q, const Tensor& k, const Tensor& v, + const Tensor& positions, const Tensor& valid_columns, + const Tensor& table_rows, float scale, PagedKVBatchLayerView cache, + GqaExecutionEnvelope envelope, WorkspaceArena& workspace, Tensor& out, + cudaStream_t stream) { + for (std::int32_t begin = 0; begin < q.ne[2]; begin += kSmallTChunkTokens) { + const std::int32_t count = std::min(kSmallTChunkTokens, q.ne[2] - begin); + auto chunk_scope = workspace.scope(); + const std::int32_t splits = + detail::gqa_attention_split_capacity(q.ne[1], count, cache.dtype, envelope); + SmallTWorkspace partial = + allocate_small_t_workspace(workspace, q.ne[1], count, splits, q.ne[3]); + detail::gqa_attention_small_t_launch(q, k, v, positions, valid_columns, table_rows, scale, + cache, envelope, begin, count, partial.acc, partial.m, + partial.l, out, stream); + } +} + +void launch_cached_chunked_small_t(const Tensor& q, const Tensor& positions, float scale, + const PagedKVLayerView& cache, GqaExecutionEnvelope envelope, + WorkspaceArena& workspace, Tensor& out, cudaStream_t stream) { + for_each_small_t_chunk( + q, positions, workspace, cache.dtype, envelope, out, + [&](std::int32_t, std::int32_t, const Tensor& q_chunk, const Tensor& position_chunk, + SmallTWorkspace& partial, Tensor& out_chunk) { + detail::gqa_attention_cached_small_t_launch(q_chunk, position_chunk, scale, cache, + envelope, partial.acc, partial.m, partial.l, + out_chunk, stream); + }); +} + +} // namespace + +namespace detail { + +GqaAttentionRoute gqa_attention_resolve_route(std::int32_t q_heads, std::int32_t width, + std::int32_t batch_size, + GqaExecutionEnvelope envelope) { + if (width >= 1 && width <= kSmallTChunkTokens) { return GqaAttentionRoute::SmallT; } + if (batch_size > 1) { return GqaAttentionRoute::ChunkedSmallT; } + const std::uint32_t prompt_visible_keys = + width <= 2 * kSmallTChunkTokens ? kTwoChunkPromptVisibleKeys : kThreeChunkPromptVisibleKeys; + if (q_heads == 16 && width <= kMaximumVerifyTokens && + envelope.max_visible_keys > prompt_visible_keys) { + return GqaAttentionRoute::ChunkedSmallT; + } + return GqaAttentionRoute::Prompt; +} + +const char* gqa_attention_route_name(GqaAttentionRoute route) { + switch (route) { + case GqaAttentionRoute::SmallT: + return "small_t"; + case GqaAttentionRoute::ChunkedSmallT: + return "chunked_small_t"; + case GqaAttentionRoute::Prompt: + return "prompt"; + } + return "unknown"; +} + +} // namespace detail + +std::size_t gqa_attention_workspace_capacity_bytes(std::int32_t q_heads, DType cache_dtype, + GqaExecutionEnvelope envelope, + std::int32_t batch_size, std::int32_t min_width, + std::int32_t max_width) { + (void)kv_heads_for_q_heads(q_heads, "gqa_attention workspace"); + if ((cache_dtype != DType::BF16 && cache_dtype != DType::I8 && cache_dtype != DType::NVFP4 && + cache_dtype != DType::FP8_E4M3FN && cache_dtype != DType::ISO3) || + batch_size <= 0 || + batch_size > kMaximumBatchSize || min_width <= 0 || max_width < min_width || + (batch_size > 1 && max_width > kMaximumVerifyTokens) || envelope.min_visible_keys == 0 || + envelope.min_visible_keys > envelope.max_visible_keys || + envelope.max_visible_keys > kGqaAttentionMaximumVisibleKeys || + envelope.max_visible_keys < static_cast(max_width)) { + throw std::invalid_argument("gqa_attention workspace: invalid profile or interval"); + } + + const auto chunk_capacity = [&](std::int32_t width) { + const std::int32_t splits = + detail::gqa_attention_split_capacity(q_heads, width, cache_dtype, envelope); + WorkspaceLayoutBuilder layout; + (void)allocate_small_t_workspace(layout, q_heads, width, splits, batch_size); + return layout.peak_bytes(1); + }; + const auto exact_capacity = [&](std::int32_t width) { + const detail::GqaAttentionRoute route = + detail::gqa_attention_resolve_route(q_heads, width, batch_size, envelope); + if (route == detail::GqaAttentionRoute::Prompt) { return std::size_t{0}; } + if (route == detail::GqaAttentionRoute::SmallT) { return chunk_capacity(width); } + std::size_t maximum = 0; + for (std::int32_t begin = 0; begin < width; begin += kSmallTChunkTokens) { + maximum = + std::max(maximum, chunk_capacity(std::min(kSmallTChunkTokens, width - begin))); + } + return maximum; + }; + + std::size_t maximum = 0; + if (min_width <= kMaximumVerifyTokens) { + const std::int32_t last = std::min(max_width, kMaximumVerifyTokens); + for (std::int32_t width = min_width; width <= last; ++width) { + maximum = std::max(maximum, exact_capacity(width)); + } + } + return maximum; +} + +void gqa_attention(const Tensor& q, const Tensor& k, const Tensor& v, const Tensor& positions, + const Tensor& valid_columns, const Tensor& kv_table_rows, float scale, + PagedKVBatchLayerView cache, GqaExecutionEnvelope envelope, + WorkspaceArena& workspace, Tensor& out, cudaStream_t stream) { + constexpr const char* op = "gqa_attention"; + validate_batched_attention_tensors(q, positions, valid_columns, kv_table_rows, out, cache, + envelope, scale, op); + if (k.dtype != DType::BF16 || v.dtype != DType::BF16) { + throw std::invalid_argument("gqa_attention: k/v must be BF16"); + } + const std::int32_t width = q.ne[2]; + const std::int32_t batch = q.ne[3]; + const std::int32_t kv_heads = kv_heads_for_q_heads(q.ne[1], op); + require_shape(k, kHeadDim, kv_heads, width, batch, op, "k"); + require_shape(v, kHeadDim, kv_heads, width, batch, op, "v"); + require_contiguous_nonnull(k, op, "k"); + require_contiguous_nonnull(v, op, "v"); + + auto scope = workspace.scope(); + const detail::GqaAttentionRoute route = + detail::gqa_attention_resolve_route(q.ne[1], width, batch, envelope); + if (route == detail::GqaAttentionRoute::ChunkedSmallT) { + launch_chunked_small_t(q, k, v, positions, valid_columns, kv_table_rows, scale, cache, + envelope, workspace, out, stream); + return; + } + if (route == detail::GqaAttentionRoute::SmallT) { + const std::int32_t splits = + detail::gqa_attention_split_capacity(q.ne[1], width, cache.dtype, envelope); + SmallTWorkspace partial = + allocate_small_t_workspace(workspace, q.ne[1], width, splits, batch); + detail::gqa_attention_small_t_launch(q, k, v, positions, valid_columns, kv_table_rows, + scale, cache, envelope, 0, width, partial.acc, + partial.m, partial.l, out, stream); + return; + } + detail::gqa_attention_prompt_launch(q, k, v, positions, valid_columns, kv_table_rows, scale, + cache, out, stream); +} + +void gqa_kv_append(const Tensor& k, const Tensor& v, const Tensor& positions, + PagedKVLayerView cache, cudaStream_t stream) { + constexpr const char* op = "gqa_kv_append"; + if (k.dtype != DType::BF16 || v.dtype != DType::BF16) { + throw std::invalid_argument("gqa_kv_append: k/v must be BF16"); + } + if (positions.dtype != DType::I32) { + throw std::invalid_argument("gqa_kv_append: positions must be I32"); + } + const std::int32_t kv_heads = k.ne[1]; + require_kv_heads(kv_heads, op); + const std::int32_t tokens = k.ne[2]; + if (tokens <= 0) { throw std::invalid_argument("gqa_kv_append: T must be positive"); } + require_shape(k, kHeadDim, kv_heads, tokens, 1, op, "k"); + require_shape(v, kHeadDim, kv_heads, tokens, 1, op, "v"); + require_shape(positions, tokens, 1, 1, 1, op, "positions"); + require_contiguous_nonnull(k, op, "k"); + require_contiguous_nonnull(v, op, "v"); + require_contiguous_nonnull(positions, op, "positions"); + const std::uint32_t capacity = validate_cache(cache, kv_heads, op); + if (static_cast(tokens) > capacity) { + throw std::invalid_argument("gqa_kv_append: T exceeds KV cache capacity"); + } + detail::gqa_kv_append_launch(k, v, positions, cache, stream); +} + +void gqa_attention_cached(const Tensor& q, const Tensor& positions, float scale, + const PagedKVLayerView& cache, GqaExecutionEnvelope envelope, + WorkspaceArena& workspace, Tensor& out, cudaStream_t stream) { + constexpr const char* op = "gqa_attention_cached"; + validate_attention_tensors(q, positions, out, cache, envelope, scale, op); + + auto scope = workspace.scope(); + if (detail::gqa_attention_resolve_route(q.ne[1], q.ne[2], 1, envelope) == + detail::GqaAttentionRoute::ChunkedSmallT) { + launch_cached_chunked_small_t(q, positions, scale, cache, envelope, workspace, out, stream); + return; + } + if (detail::gqa_attention_uses_small_t(q.ne[2])) { + const std::int32_t splits = + detail::gqa_attention_split_capacity(q.ne[1], q.ne[2], cache.dtype, envelope); + SmallTWorkspace partial = allocate_small_t_workspace(workspace, q.ne[1], q.ne[2], splits); + detail::gqa_attention_cached_small_t_launch(q, positions, scale, cache, envelope, + partial.acc, partial.m, partial.l, out, stream); + return; + } + detail::gqa_attention_prompt_attention_launch(q, positions, scale, cache, out, stream); +} + +} // namespace ninfer::ops diff --git a/src/ops/wrapper/rope.cpp b/src/ops/wrapper/rope.cpp index 17d574db88..3b52cbb8b0 100644 --- a/src/ops/wrapper/rope.cpp +++ b/src/ops/wrapper/rope.cpp @@ -140,4 +140,61 @@ void rope(const Tensor& positions, int rotary_dim, float theta, Tensor& x, cudaS detail::rope_single_launch(positions, rotary_dim, theta, x, stream); } +void require_yarn4_domain(float theta, int rotary_dim, std::int32_t head_dim) { + if (theta != 1.0e7F) { + throw std::invalid_argument("rope_yarn4: theta must be 1e7"); + } + const bool text_yarn4 = rotary_dim == 64 && head_dim == 256; + const bool dflash_yarn4 = rotary_dim == 128 && head_dim == 128; + if (!text_yarn4 && !dflash_yarn4) { + throw std::invalid_argument( + "rope_yarn4: expected Text D256/R64 or DFlash D128/R128"); + } +} + +void rope_yarn4(const Tensor& positions, int rotary_dim, float theta, Tensor& q, Tensor& k, + cudaStream_t stream) { + require_common(positions, rotary_dim, theta); + if (q.dtype != DType::BF16 || k.dtype != DType::BF16) { + throw std::invalid_argument("rope_yarn4: q/k must be BF16"); + } + (void)numel_allow_zero(positions, "positions"); + const std::int64_t q_numel = numel_allow_zero(q, "q"); + (void)numel_allow_zero(k, "k"); + const std::int32_t tokens = q.ne[2]; + const int axes = position_axes(positions, tokens); + const std::int32_t head_dim = axes == 2 ? kVisionDim : q.ne[0]; + const std::int32_t q_heads = q.ne[1]; + const std::int32_t k_heads = k.ne[1]; + if (axes != 1) { throw std::invalid_argument("rope_yarn4: requires 1-D positions"); } + require_yarn4_domain(theta, rotary_dim, head_dim); + require_tensor_layout(q, "q", head_dim, q_heads, tokens); + require_tensor_layout(k, "k", head_dim, k_heads, tokens); + if (q_numel == 0) { return; } + require_positions_storage(positions); + if (q.data == nullptr || k.data == nullptr) { + throw std::invalid_argument("rope_yarn4: q/k data must be non-null"); + } + detail::rope_yarn4_launch(positions, rotary_dim, theta, q, k, stream); +} + +void rope_yarn4(const Tensor& positions, int rotary_dim, float theta, Tensor& x, + cudaStream_t stream) { + require_common(positions, rotary_dim, theta); + if (x.dtype != DType::BF16) { throw std::invalid_argument("rope_yarn4: tensor must be BF16"); } + (void)numel_allow_zero(positions, "positions"); + const std::int64_t x_numel = numel_allow_zero(x, "tensor"); + const std::int32_t tokens = x.ne[2]; + const int axes = position_axes(positions, tokens); + const std::int32_t head_dim = axes == 2 ? kVisionDim : x.ne[0]; + const std::int32_t heads = x.ne[1]; + if (axes != 1) { throw std::invalid_argument("rope_yarn4: requires 1-D positions"); } + require_yarn4_domain(theta, rotary_dim, head_dim); + require_tensor_layout(x, "tensor", head_dim, heads, tokens); + if (x_numel == 0) { return; } + require_positions_storage(positions); + if (x.data == nullptr) { throw std::invalid_argument("rope_yarn4: tensor data must be non-null"); } + detail::rope_yarn4_single_launch(positions, rotary_dim, theta, x, stream); +} + } // namespace ninfer::ops diff --git a/src/ops/wrapper/swa.cpp b/src/ops/wrapper/swa.cpp new file mode 100644 index 0000000000..41df55fd28 --- /dev/null +++ b/src/ops/wrapper/swa.cpp @@ -0,0 +1,144 @@ +#include "ninfer/ops/swa.h" + +#include "core/layout.h" +#include "ops/launcher/swa.h" + +#include +#include +#include +#include +#include +#include + +namespace ninfer::ops { +namespace { + +constexpr std::int32_t kHeadDim = 128; +constexpr std::int32_t kQHeads = 32; +constexpr std::int32_t kKVHeads = 8; +constexpr float kExpectedScale = 0.08838834764831844055f; + +void require_shape(const Tensor& tensor, std::int32_t n0, std::int32_t n1, std::int32_t n2, + std::int32_t n3, const char* op, const char* name) { + if (tensor.ne[0] != n0 || tensor.ne[1] != n1 || tensor.ne[2] != n2 || tensor.ne[3] != n3) { + throw std::invalid_argument(std::string(op) + ": invalid shape for " + name); + } +} + +void require_contiguous_nonnull(const Tensor& tensor, const char* op, const char* name) { + if (!tensor.is_contiguous()) { + throw std::invalid_argument(std::string(op) + ": " + name + " must be contiguous"); + } + if (tensor.data == nullptr) { + throw std::invalid_argument(std::string(op) + ": " + name + " data must be non-null"); + } +} + +void validate_context(const CyclicKVCacheLayerView& context, std::uint32_t window, + const char* op) { + if (context.num_kv_heads != kKVHeads || context.head_dim != kHeadDim || + context.capacity != window || context.padded_capacity < context.capacity || + context.lane_capacity <= 0) { + throw std::invalid_argument(std::string(op) + ": invalid cyclic context"); + } + if (context.padded_capacity > + static_cast(std::numeric_limits::max())) { + throw std::overflow_error(std::string(op) + ": padded capacity exceeds int32"); + } + const auto padded = static_cast(context.padded_capacity); + if (context.k.dtype != DType::BF16 || context.v.dtype != DType::BF16) { + throw std::invalid_argument(std::string(op) + ": context K/V must be BF16"); + } + require_shape(context.k, kHeadDim, padded, kKVHeads, context.lane_capacity, op, "context k"); + require_shape(context.v, kHeadDim, padded, kKVHeads, context.lane_capacity, op, "context v"); + require_contiguous_nonnull(context.k, op, "context k"); + require_contiguous_nonnull(context.v, op, "context v"); +} + +struct PartialWorkspace { + Tensor acc; + Tensor m; + Tensor l; +}; + +template +PartialWorkspace allocate_workspace(Allocator& workspace, std::int32_t tokens, std::int32_t splits, + std::int32_t batch_size) { + return { + workspace.alloc(DType::BF16, {kHeadDim, kQHeads, tokens, splits * batch_size}), + workspace.alloc(DType::FP32, {kQHeads, tokens, splits * batch_size}), + workspace.alloc(DType::FP32, {kQHeads, tokens, splits * batch_size}), + }; +} + +} // namespace + +std::size_t swa_workspace_capacity_bytes(SwaContextExecutionEnvelope envelope, + std::int32_t min_tokens, std::int32_t max_tokens, + std::int32_t batch_size, std::uint32_t window) { + if (min_tokens < 1 || max_tokens < min_tokens || max_tokens > 16 || batch_size < 1 || + batch_size > 8 || envelope.min_context > envelope.max_context || + envelope.max_context > + static_cast(std::numeric_limits::max())) { + throw std::invalid_argument("swa workspace: invalid envelope or token interval"); + } + const auto plan = detail::swa_resolve_plan(max_tokens, envelope, window); + WorkspaceLayoutBuilder layout; + (void)allocate_workspace(layout, max_tokens, plan.split_capacity, batch_size); + return layout.peak_bytes(1); +} + +void swa(const Tensor& q, const Tensor& query_k, const Tensor& query_v, const Tensor& positions, + const Tensor& valid_columns, const Tensor& lanes, float scale, + const CyclicKVCacheLayerView& context, SwaContextExecutionEnvelope envelope, + std::uint32_t window, WorkspaceArena& workspace, Tensor& out, cudaStream_t stream) { + constexpr const char* op = "swa"; + if (window != 2048 && window != 4096) { + throw std::invalid_argument("swa: registered windows are 2048 and 4096"); + } + if (q.dtype != DType::BF16 || query_k.dtype != DType::BF16 || query_v.dtype != DType::BF16 || + out.dtype != DType::BF16) { + throw std::invalid_argument("swa: q/k/v/out must be BF16"); + } + if (positions.dtype != DType::I32 || valid_columns.dtype != DType::I32 || + lanes.dtype != DType::I32) { + throw std::invalid_argument("swa: positions/valid_columns/lanes must be I32"); + } + const std::int32_t tokens = q.ne[2]; + const std::int32_t batch = q.ne[3]; + if (tokens < 1 || tokens > 16) { + throw std::invalid_argument("swa: optimized domain is T=1..16"); + } + if (batch < 1 || batch > 8) { throw std::invalid_argument("swa: B must be 1..8"); } + require_shape(q, kHeadDim, kQHeads, tokens, batch, op, "q"); + require_shape(query_k, kHeadDim, kKVHeads, tokens, batch, op, "query k"); + require_shape(query_v, kHeadDim, kKVHeads, tokens, batch, op, "query v"); + require_shape(positions, tokens, batch, 1, 1, op, "positions"); + require_shape(valid_columns, batch, 1, 1, 1, op, "valid columns"); + require_shape(lanes, batch, 1, 1, 1, op, "lanes"); + require_shape(out, kHeadDim, kQHeads, tokens, batch, op, "out"); + require_contiguous_nonnull(q, op, "q"); + require_contiguous_nonnull(query_k, op, "query k"); + require_contiguous_nonnull(query_v, op, "query v"); + require_contiguous_nonnull(positions, op, "positions"); + require_contiguous_nonnull(valid_columns, op, "valid columns"); + require_contiguous_nonnull(lanes, op, "lanes"); + require_contiguous_nonnull(out, op, "out"); + validate_context(context, window, op); + if (envelope.min_context > envelope.max_context || + envelope.max_context > + static_cast(std::numeric_limits::max())) { + throw std::invalid_argument("swa: invalid execution envelope"); + } + if (!std::isfinite(scale) || std::abs(scale - kExpectedScale) > 1e-7f) { + throw std::invalid_argument("swa: scale must be 1/sqrt(128)"); + } + + auto scope = workspace.scope(); + const auto plan = detail::swa_resolve_plan(tokens, envelope, window); + PartialWorkspace partial = allocate_workspace(workspace, tokens, plan.split_capacity, batch); + detail::swa_launch(q, query_k, query_v, positions, valid_columns, lanes, scale, context, plan, + partial.acc, partial.m, partial.l, out, stream); +} + +} // namespace ninfer::ops diff --git a/src/product/kv_options.h b/src/product/kv_options.h new file mode 100644 index 0000000000..652936ba01 --- /dev/null +++ b/src/product/kv_options.h @@ -0,0 +1,87 @@ +#pragma once + +#include "ninfer/types.h" + +#include +#include +#include +#include +#include +#include + +namespace ninfer::product { + +// Per-layer KV storage spec parsing for CLI and serving. +// +// Spec grammar (comma separated): +// all: every registered full-attention layer +// shorthand for all: +// A: one layer, A in [0, 15] +// A-B: inclusive layer range A..B, A <= B in [0, 15] +// where is bf16, int8, fp8, or nvfp4. Unlisted slots stay BFloat16, +// which means "inherit the global --kv-dtype". A slot may be written exactly +// once. Cold-policy parsing lives with the cold-pool change, not here. + +[[nodiscard]] inline std::optional parse_kv_storage(std::string_view text) { + if (text == "bf16") { return KvCacheStorage::BFloat16; } + if (text == "int8") { return KvCacheStorage::Int8Group64; } + if (text == "nvfp4") { return KvCacheStorage::Nvfp4Group16; } + if (text == "fp8") { return KvCacheStorage::Fp8Group16; } + if (text == "iso3") { return KvCacheStorage::Iso3Group16; } + return std::nullopt; +} + +[[nodiscard]] inline std::array +parse_kv_layer_storage(std::string_view spec) { + std::array table{}; + if (spec.empty()) { return table; } + + std::size_t begin = 0; + while (begin < spec.size()) { + const std::size_t comma = spec.find(',', begin); + const std::string_view item = + spec.substr(begin, comma == std::string_view::npos ? spec.size() - begin + : comma - begin); + if (item.empty()) { throw std::invalid_argument("kv-layer-storage has an empty entry"); } + + const std::size_t colon = item.find(':'); + std::size_t first = 0; + std::size_t last = kKvLayerStorageSlots - 1; + std::string_view type = item; + if (colon != std::string_view::npos) { + const std::string_view layers = item.substr(0, colon); + type = item.substr(colon + 1); + if (layers == "all") { + first = 0; + last = kKvLayerStorageSlots - 1; + } else { + const std::size_t dash = layers.find('-'); + if (dash == std::string_view::npos) { + first = last = + static_cast(std::stoul(std::string(layers))); + } else { + first = static_cast(std::stoul(std::string( + layers.substr(0, dash)))); + last = static_cast(std::stoul(std::string( + layers.substr(dash + 1)))); + } + if (first > last || last >= kKvLayerStorageSlots) { + throw std::invalid_argument("kv-layer-storage layer index out of range"); + } + } + } + const auto value = parse_kv_storage(type); + if (!value) { throw std::invalid_argument("kv-layer-storage has an invalid type"); } + for (std::size_t slot = first; slot <= last; ++slot) { + if (table[slot] != KvCacheStorage::BFloat16) { + throw std::invalid_argument("kv-layer-storage slot written twice"); + } + table[slot] = *value; + } + if (comma == std::string_view::npos) { break; } + begin = comma + 1; + } + return table; +} + +} // namespace ninfer::product diff --git a/src/product/speculative_options.h b/src/product/speculative_options.h index a50ea5fc82..946956761a 100644 --- a/src/product/speculative_options.h +++ b/src/product/speculative_options.h @@ -11,6 +11,8 @@ namespace ninfer::product { [[nodiscard]] inline SpeculativeBackend parse_speculative_backend(std::string_view value) { if (value == "mtp") { return SpeculativeBackend::Mtp; } if (value == "dflash") { return SpeculativeBackend::DFlash; } + if (value == "dflash2") { return SpeculativeBackend::DFlash2; } + if (value == "auto") { return SpeculativeBackend::Auto; } throw std::invalid_argument("invalid speculative backend: " + std::string(value)); } @@ -22,6 +24,10 @@ namespace ninfer::product { return "mtp"; case SpeculativeBackend::DFlash: return "dflash"; + case SpeculativeBackend::DFlash2: + return "dflash2"; + case SpeculativeBackend::Auto: + return "auto"; } return "unknown"; } @@ -31,7 +37,7 @@ inline void validate_speculative_cli_options(const SpeculativeOptions& options) case SpeculativeBackend::None: if (options.draft_tokens != 0 || options.proposal_head != ProposalHead::Full) { throw std::invalid_argument( - "--draft-tokens and --lm-head-draft require --spec mtp|dflash"); + "--draft-tokens and --lm-head-draft require --spec mtp|dflash|dflash2"); } return; case SpeculativeBackend::Mtp: @@ -44,6 +50,20 @@ inline void validate_speculative_cli_options(const SpeculativeOptions& options) throw std::invalid_argument("--spec dflash requires --draft-tokens in [1,15]"); } return; + case SpeculativeBackend::DFlash2: + if (options.draft_tokens != 0 && options.draft_tokens != 7) { + throw std::invalid_argument("--spec dflash2 uses the fixed 7-draft block"); + } + if (options.proposal_head != ProposalHead::Full) { + throw std::invalid_argument("--spec dflash2 requires the full proposal head"); + } + return; + case SpeculativeBackend::Auto: + if (options.draft_tokens != 0 || options.proposal_head != ProposalHead::Full) { + throw std::invalid_argument( + "--draft-tokens and --lm-head-draft require --spec mtp|dflash|dflash2"); + } + return; } throw std::invalid_argument("invalid speculative backend"); } diff --git a/src/runtime/engine/engine.cpp b/src/runtime/engine/engine.cpp index 0f7f848d66..6c72c4b6e1 100644 --- a/src/runtime/engine/engine.cpp +++ b/src/runtime/engine/engine.cpp @@ -215,6 +215,7 @@ class Engine::Impl { explicit Impl(EngineOptions engine_options) : options(normalize_engine_options(std::move(engine_options))), device(options.device) { + device.yarn_enabled = options.yarn_enabled; nvtx::ScopedRange load_range(nvtx::Name::EngineLoad, nvtx::Category::Runtime); auto constructed = targets::construct_target(options, device); active = std::move(constructed.active); diff --git a/src/runtime/engine/shared_capture_planner.h b/src/runtime/engine/shared_capture_planner.h index f622b80319..4db30d4614 100644 --- a/src/runtime/engine/shared_capture_planner.h +++ b/src/runtime/engine/shared_capture_planner.h @@ -100,6 +100,10 @@ class SharedCapturePlanner { const PressureTargetHandle identity = session.identity_target(candidate); queue_.push_back(identity); std::optional incumbent; + // The identity target is already canonical in the Program session. The search budget + // bounds every canonical target admitted to this scenario, including children committed + // now but still waiting in the breadth-first queue; assessed targets are only telemetry. + std::uint32_t canonical_targets = 1; std::uint32_t targets_evaluated = 0; std::size_t cursor = 0; @@ -127,12 +131,13 @@ class SharedCapturePlanner { if (!assessment.expandable || contains(expanded_, target)) { continue; } auto prepared = session.prepare_expansion(target); - const std::uint32_t remaining = input.target_budget - targets_evaluated; + const std::uint32_t remaining = input.target_budget - canonical_targets; if (prepared.new_canonical_count() > remaining) { session.discard_expansion(std::move(prepared)); continue; } const auto children = session.commit_expansion(std::move(prepared)); + canonical_targets += children.new_canonical_count; expanded_.push_back(target); for (const PressureTargetHandle child : children.children) { if (!contains(assessed_, child) && !contains(queue_, child)) { diff --git a/src/serve/generation_service.cpp b/src/serve/generation_service.cpp index 08d0bf7227..ced6dcab66 100644 --- a/src/serve/generation_service.cpp +++ b/src/serve/generation_service.cpp @@ -1,6 +1,7 @@ #include "serve/generation_service.h" #include "product/media_acquire/acquire.h" +#include "serve/console_log.h" #include "serve/translate.h" #include @@ -234,10 +235,16 @@ GenerationService::GenerationService(ServeOptions options, LoadProgress load_pro engine_options.pending_timeout_ms = options_.pending_timeout_ms; engine_options.prefill_chunk = options_.prefill_chunk; engine_options.kv_cache = options_.kv_cache; + engine_options.kv_layer_storage = options_.kv_layer_storage; + engine_options.kv_layer_storage_explicit = options_.kv_layer_storage_explicit; engine_options.enable_vision = options_.enable_vision; + engine_options.yarn_enabled = options_.yarn_enabled; engine_options.use_cuda_graph = options_.use_cuda_graph; engine_options.speculative = options_.speculative; engine_options.context_cache = options_.context_cache; + engine_options.cold_policy = options_.cold_policy; + engine_options.cold_keep_tokens = options_.cold_keep_tokens; + engine_options.cold_host_bytes = options_.cold_host_bytes; engine_options.context_cost.preset_path = options_.context_cost_presets; engine_options.media_cache_bytes = options_.media_cache_bytes; engine_options.media_live_bytes = options_.media_live_bytes; @@ -450,10 +457,17 @@ void GenerationService::warmup() { turn.content.push_back(std::move(content)); request.messages.push_back(std::move(turn)); request.max_tokens = 4; - PreparedRequest prepared = - prepare_impl(request, GenerationConsumerMode::Aggregate, {}, {}, - CacheParticipation::Disabled, DeadlinePolicy::UnboundedStartup); - run(prepared, nullptr); + // The warmup is advisory: a failed round must not take the server down, + // the first real request surfaces the real error instead. + try { + PreparedRequest prepared = + prepare_impl(request, GenerationConsumerMode::Aggregate, {}, {}, + CacheParticipation::Disabled, DeadlinePolicy::UnboundedStartup); + run(prepared, nullptr); + } catch (const std::exception& exception) { + write_console_log(ConsoleLogLevel::Warning, + std::string("warmup failed (continuing): ") + exception.what()); + } } } // namespace ninfer::serve diff --git a/src/serve/serve_options.cpp b/src/serve/serve_options.cpp index 66fef22197..c758f8234a 100644 --- a/src/serve/serve_options.cpp +++ b/src/serve/serve_options.cpp @@ -1,4 +1,5 @@ #include "serve/serve_options.h" +#include "product/kv_options.h" #include "product/speculative_options.h" #include @@ -76,7 +77,9 @@ std::string serve_usage_text(const char* argv0) { "[--max-long-anchors-per-continuation N] " "[--request-log-jsonl FILE] " "[--response-store-max-records N] [--response-store-max-mib N] " - "[--kv-dtype bf16|int8|fp8] [--spec mtp|dflash --draft-tokens N] " + "[--kv-dtype bf16|int8|fp8] [--kv-layer-storage SPEC] [--spec mtp|dflash --draft-tokens N] " + "[--cold-policy none|window|host] [--cold-keep-tokens N] " + "[--cold-host-bytes N[g|m|k]] " "[--default-max-tokens N] [--default-thinking-budget N] " "[--vision] [--no-cuda-graph] [--no-prefix-reuse] " "[--lm-head-draft] [--no-thinking] [--preserve-thinking] [--cors] " @@ -258,6 +261,27 @@ ServeOptions parse_serve_options(int argc, char** argv) { options.device = parse_nonnegative_int(require_value("--device"), "device"); } else if (arg == "--kv-dtype") { options.kv_cache = parse_kv_dtype(require_value("--kv-dtype")); + } else if (arg == "--kv-layer-storage") { + const auto table = product::parse_kv_layer_storage(require_value("--kv-layer-storage")); + for (std::size_t i = 0; i < options.kv_layer_storage.size(); ++i) { + options.kv_layer_storage[i] = table[i]; + } + options.kv_layer_storage_explicit = true; + } else if (arg == "--cold-policy") { + const std::string_view v = require_value("--cold-policy"); + if (v == "none" || v == "off") { options.cold_policy = ColdPolicy::None; } + else if (v == "window") { options.cold_policy = ColdPolicy::Window; } + else if (v == "host") { options.cold_policy = ColdPolicy::Host; } + else { throw std::invalid_argument("invalid cold-policy: " + std::string(v)); } + } else if (arg == "--cold-keep-tokens") { + options.cold_keep_tokens = + parse_u64(require_value("--cold-keep-tokens"), "cold-keep-tokens"); + if (options.cold_keep_tokens > std::numeric_limits::max()) { + throw std::invalid_argument("--cold-keep-tokens is out of range"); + } + } else if (arg == "--cold-host-bytes") { + options.cold_host_bytes = + parse_u64(require_value("--cold-host-bytes"), "cold-host-bytes"); } else if (arg == "--spec") { options.speculative.backend = product::parse_speculative_backend(require_value("--spec")); @@ -279,6 +303,8 @@ ServeOptions parse_serve_options(int argc, char** argv) { options.enable_vision = true; } else if (arg == "--no-cuda-graph") { options.use_cuda_graph = false; + } else if (arg == "--yarn") { + options.yarn_enabled = true; } else if (arg == "--no-prefix-reuse") { options.allow_prefix_reuse = false; } else if (arg == "--lm-head-draft") { @@ -332,9 +358,12 @@ ServeOptions parse_serve_options(int argc, char** argv) { throw std::invalid_argument("--port must be in [1,65535]"); } if (options.max_context == 0) { throw std::invalid_argument("--max-context must be positive"); } + // An explicit --kv-capacity may floor the device page pool below max_context: + // YaRN extends the rope domain beyond the pool, and the cold pool recycles + // committed pages under pressure so the context can keep growing. if (options.kv_capacity.mode == KvCapacityMode::Explicit && - options.kv_capacity.explicit_tokens < options.max_context) { - throw std::invalid_argument("--kv-capacity must be at least --max-context"); + options.kv_capacity.explicit_tokens == 0) { + throw std::invalid_argument("--kv-capacity must be positive"); } if (options.max_concurrency == 0 || options.max_concurrency > kMaximumConcurrency) { throw std::invalid_argument("--max-concurrency must be in [1,8]"); diff --git a/src/serve/serve_options.h b/src/serve/serve_options.h index c529fbc84f..37a10056b3 100644 --- a/src/serve/serve_options.h +++ b/src/serve/serve_options.h @@ -1,5 +1,6 @@ #pragma once +#include "core/dtype.h" #include "ninfer/types.h" #include @@ -42,11 +43,17 @@ struct ServeOptions { std::size_t response_store_max_bytes = kDefaultResponseStoreBytes; int device = 0; KvCacheStorage kv_cache = KvCacheStorage::BFloat16; + std::array kv_layer_storage{}; + bool kv_layer_storage_explicit = false; SpeculativeOptions speculative; ContextCacheOptions context_cache; bool enable_vision = false; bool use_cuda_graph = true; + bool yarn_enabled = false; bool allow_prefix_reuse = true; + ColdPolicy cold_policy = ColdPolicy::None; + std::uint32_t cold_keep_tokens = 128; + std::uint64_t cold_host_bytes = 4ULL << 30; bool enable_thinking = true; // default thinking mode for the generation prompt (--no-thinking opts out) bool preserve_thinking = false; diff --git a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/decoder_state.h b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/decoder_state.h index f1193f588a..fc89a592ea 100644 --- a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/decoder_state.h +++ b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/decoder_state.h @@ -11,6 +11,7 @@ namespace ninfer::targets::qwen3_6 { inline constexpr std::int32_t kKvInt8QuantGroup = 64; inline constexpr std::int32_t kKvFp8QuantGroup = 256; +inline constexpr std::int32_t kNvfp4KvQuantGroup = 16; struct DecoderStateSpec { std::uint32_t full_attention_layers = 0; @@ -20,10 +21,15 @@ struct DecoderStateSpec { std::int32_t attention_head_dim = 0; DType kv_dtype = DType::BF16; std::int32_t kv_quant_group = 0; + // Per-layer storage table indexed by full-attention layer order. + // BFloat16 entries inherit kv_dtype; empty (all-BF16) inherits wholesale. + std::array layer_kv_dtypes{}; bool enable_mtp = false; std::int32_t kv_table_rows = 1; std::uint32_t text_physical_page_groups = 0; std::uint32_t mtp_physical_page_groups = 0; + // Entropy-coded cold pool capacity in pages; 0 disables the pool. + std::uint32_t max_cold_pages = 0; }; struct PagedKVCacheLayout { @@ -35,6 +41,18 @@ struct PagedKVCacheLayout { std::int32_t head_dim = 0; DType dtype = DType::BF16; std::int32_t quant_group = 0; + // Cold slots per layer: [slot_bytes, kv_heads, 2, max_cold_pages] + // plus an I32 validity plane of [kv_heads, 2, max_cold_pages]. + std::array cold_slots; + std::array cold_slot_valid; + std::int32_t slot_bytes = 0; + std::uint32_t max_cold_pages = 0; + // Resolved per-layer storage (one entry per full-attention layer). + std::array layer_dtypes{}; + // Plane offset of each layer in the page geometry (prefix sums over + // per-layer plane counts; mixed BF16/quantized tables have unequal + // strides). + std::array layer_plane_base{}; [[nodiscard]] std::size_t payload_bytes() const noexcept { return pages.payload_bytes(); } }; @@ -48,6 +66,7 @@ class PagedKVCacheView { [[nodiscard]] bool valid() const noexcept { return cache_ != nullptr; } [[nodiscard]] std::uint32_t max_context() const noexcept; + [[nodiscard]] PagedKVLayerView layer_view(std::uint32_t layer) const; private: @@ -67,7 +86,13 @@ class PagedKVCache { PagedKVCache(PagedKVCache&&) = delete; PagedKVCache& operator=(PagedKVCache&&) = delete; - [[nodiscard]] std::uint32_t max_context() const noexcept { return max_context_; } + // Cold-slot pool: fixed raw slots per (layer, kv_head, plane). + [[nodiscard]] std::int32_t slot_bytes() const noexcept { return slot_bytes_; } + [[nodiscard]] std::uint32_t max_cold_pages() const noexcept { return max_cold_pages_; } + std::int32_t allocate_cold_slot() noexcept; + void release_cold_slot(std::int32_t slot) noexcept; + +[[nodiscard]] std::uint32_t max_context() const noexcept { return max_context_; } [[nodiscard]] std::uint32_t layers() const noexcept { return layers_; } @@ -85,6 +110,10 @@ class PagedKVCache { [[nodiscard]] PagedKVBatchLayerView batch_layer_view(std::uint32_t layer) const; + [[nodiscard]] Tensor cold_slot_valid(std::uint32_t layer) const noexcept { + return layer < layers_ ? cold_slot_valid_[layer] : Tensor{}; + } + private: friend class PagedKVCacheView; [[nodiscard]] PagedKVLayerView layer_view(std::uint32_t layer, Tensor block_table) const; @@ -94,8 +123,16 @@ class PagedKVCache { std::uint32_t layers_ = 0; std::uint32_t max_context_ = 0; std::int32_t kv_heads_ = 0; + std::int32_t head_dim_ = 0; DType dtype_ = DType::BF16; + std::array cold_slots_; + std::array cold_slot_valid_; + std::int32_t slot_bytes_ = 0; + std::uint32_t max_cold_pages_ = 0; + std::vector cold_slot_used_; + std::array layer_dtypes_{}; + std::array layer_plane_base_{}; std::int32_t quant_group_ = 0; }; diff --git a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/model_view.h b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/model_view.h index e1d27e463b..919358a9a8 100644 --- a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/model_view.h +++ b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/model_view.h @@ -78,14 +78,43 @@ struct DFlashWeights { Tensor final_norm; }; +struct DFlash2LayerWeights { + Tensor input_norm; + Weight query_key_value; + Weight context_key; + Weight context_value; + Tensor query_norm; + Tensor key_norm; + Weight attention_output; + Weight attention_conv_base; + Weight attention_conv_projection; + Tensor post_attention_norm; + Weight gate_up; + Weight down; + Weight mlp_conv_base; + Weight mlp_conv_projection; +}; + +template +struct DFlash2Weights { + Weight feature_projection; + Tensor context_norm; + std::array layers; + Tensor final_norm; + Weight selector_hidden_projection; + Weight selector_predecessor_codebook; + Weight selector_successor_codebook; +}; + template + class DFlash2Payload, std::size_t FullAttentionLayers, std::size_t GdnLayers> struct ModelView { using FullLayer = FullAttentionWeights; using GdnLayer = GdnWeights; using MtpLayer = MtpWeights; using DFlash = DFlashPayload; + using DFlash2 = DFlash2Payload; DeviceArena* weights_arena = nullptr; Weight token_embedding; @@ -97,6 +126,7 @@ struct ModelView { std::optional optimized_proposal; std::optional mtp; std::optional dflash; + std::optional dflash2; std::optional vision; }; diff --git a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/round_state.h b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/round_state.h index 47b205593f..c3e58c85d6 100644 --- a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/round_state.h +++ b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/round_state.h @@ -89,6 +89,7 @@ struct DFlashDecodeEgress { std::array licensed_tokens{}; std::array licensed_counts{}; std::array accepted_drafts{}; + std::array proposal_extents{}; }; struct OrdinaryDecodeStateLayout { @@ -263,6 +264,7 @@ struct DFlashDecodeState { Tensor licensed_tokens; Tensor licensed_counts; Tensor accepted_drafts; + Tensor egress_proposal_extents; Tensor proposal_ids; Tensor proposal_positions; Tensor append_positions; diff --git a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/startup_features.h b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/startup_features.h index 67eae7f6ea..d5f54ec287 100644 --- a/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/startup_features.h +++ b/src/targets/qwen3_6/export/ninfer/targets/qwen3_6/startup_features.h @@ -19,6 +19,12 @@ struct StartupFeatures { [[nodiscard]] bool dflash() const noexcept { return speculative == SpeculativeBackend::DFlash; } + [[nodiscard]] bool dflash2() const noexcept { + return speculative == SpeculativeBackend::DFlash2; + } + + [[nodiscard]] bool dflash_like() const noexcept { return dflash() || dflash2(); } + [[nodiscard]] bool optimized_proposal() const noexcept { return speculative_enabled() && proposal_head == ProposalHead::Optimized; } diff --git a/src/targets/qwen3_6/impl/runtime/decode_impl.h b/src/targets/qwen3_6/impl/runtime/decode_impl.h index 599782d7e6..f3ad6fa514 100644 --- a/src/targets/qwen3_6/impl/runtime/decode_impl.h +++ b/src/targets/qwen3_6/impl/runtime/decode_impl.h @@ -10,7 +10,7 @@ namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule { namespace { auto ordinary_batch_body(OrdinaryBatchContext& state, std::int32_t batch_size, - ops::CausalAttentionExecutionEnvelope envelope) { + ops::GqaExecutionEnvelope envelope) { return [&state, batch_size, envelope] { if (batch_size <= 0 || batch_size > static_cast(kMaximumConcurrency)) { throw std::logic_error("ordinary decode batch state is incomplete"); @@ -51,14 +51,14 @@ auto ordinary_batch_body(OrdinaryBatchContext& state, std::int32_t batch_size, } // namespace void capture_ordinary_decode_batch(OrdinaryBatchContext& state, std::int32_t batch_size, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, DecodeGraphDefinition& definition) { auto body = ordinary_batch_body(state, batch_size, envelope); capture_graph(state, definition, body); } void ordinary_decode_batch(OrdinaryBatchContext& state, std::int32_t batch_size, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, DecodeGraphExecutable* executable) { auto body = ordinary_batch_body(state, batch_size, envelope); run_prepared(state, executable, body); diff --git a/src/targets/qwen3_6/impl/runtime/dflash2_impl.h b/src/targets/qwen3_6/impl/runtime/dflash2_impl.h new file mode 100644 index 0000000000..b47efebb97 --- /dev/null +++ b/src/targets/qwen3_6/impl/runtime/dflash2_impl.h @@ -0,0 +1,479 @@ +#include "targets/qwen3_6/impl/runtime/instance.h" +#include "targets/qwen3_6/impl/runtime/schedule.h" +#include "targets/qwen3_6/impl/runtime/workspace_recipe.h" + +#include "core/dtype.h" +#include "ninfer/ops/argmax.h" +#include "ninfer/ops/dflash2_grouped_conv.h" +#include "ninfer/ops/dflash2_selector.h" +#include "ninfer/ops/embedding.h" +#include "ninfer/ops/kv_cache_append.h" +#include "ninfer/ops/linear.h" +#include "ninfer/ops/prepare_masked_block.h" +#include "ninfer/ops/prepare_ragged_prefix.h" +#include "ninfer/ops/residual_add.h" +#include "ninfer/ops/rmsnorm.h" +#include "ninfer/ops/rope.h" +#include "ninfer/ops/scalar.h" +#include "ninfer/ops/silu_mul.h" +#include "ninfer/ops/speculative_round.h" +#include "ninfer/ops/swa.h" + +#include + +#include +#include +#include +#include + +namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule { +namespace { + +DFlash2PersistentState& dflash2_state(PrefillContext& state) { + if (state.dflash2 == nullptr) { + throw std::logic_error("DFlash2 schedule requires DFlash2 weights and state"); + } + return *state.dflash2; +} + +DFlash2PersistentState& dflash2_state(DFlash2BatchContext& state) { return state.dflash2; } + +DFlash2PersistentState& dflash2_state(DFlash2AppendContext& state) { return state.dflash2; } + +Weight conv_side_weight(const Weight& base, std::int32_t side, std::int32_t taps) { + Weight out = base; + out.qdata = static_cast(base.qdata) + + static_cast(side) * static_cast(taps) * + static_cast(base.k) * dtype_size(DType::BF16); + out.n = taps; + return out; +} + +template +DFlashFeatureSink dflash2_prefill_feature_sink_impl( + PrefillContext& state, DFlashFeatureSink::PrefillConsumer consume_prefill) { + if constexpr (!V::supports_dflash2) { + throw std::logic_error("DFlash2 feature capture is unavailable for this target"); + } else { + using Config = typename V::DFlash2Config; + return DFlashFeatureSink{ + .features = &dflash2_state(state).prefill_features, + .positions = &dflash2_state(state).prefill_positions, + .layers = std::span(Config::target_feature_layers), + .consume_prefill = std::move(consume_prefill), + }; + } +} + +template +DFlashFeatureSink dflash2_batch_feature_sink_impl(DFlash2BatchContext& state, const Tensor& lanes, + const Tensor& valid_columns, + std::int32_t width, std::int32_t batch_size) { + if constexpr (!V::supports_dflash2) { + throw std::logic_error("DFlash2 feature capture is unavailable for this target"); + } else { + using Config = typename V::DFlash2Config; + return DFlashFeatureSink{ + .batch_features = &dflash2_state(state).pending_features, + .batch_lanes = &lanes, + .batch_valid_columns = &valid_columns, + .batch_width = width, + .batch_size = batch_size, + .layers = std::span(Config::target_feature_layers), + }; + } +} + +void append_context_impl(DFlash2AppendContext& state, const Tensor& features, + const Tensor& positions, const Tensor& commit_counts, + const Tensor& lanes, const Tensor& table_rows, + ops::KVCacheAppendPrefixExecutionEnvelope envelope) { + using Config = DFlash2Config; + const std::int32_t width = features.ne[1]; + const std::int32_t batch = features.ne[2]; + const std::int32_t columns = width * batch; + if (width <= 0 || batch <= 0 || features.dtype != DType::BF16 || + features.ne[0] != Config::feature_rows || positions.dtype != DType::I32 || + positions.ne[0] != width || positions.ne[1] != batch || + commit_counts.dtype != DType::I32 || commit_counts.ne[0] != batch || + lanes.dtype != DType::I32 || lanes.ne[0] != batch) { + throw std::invalid_argument("DFlash2 context append inputs are invalid"); + } + if (!state.execution.model.dflash2.has_value()) { + throw std::logic_error("DFlash2 weights are unavailable"); + } + const auto& dflash2 = *state.execution.model.dflash2; + + // The cyclic draft cache retains only the last `local_capacity` absolute + // positions. An oversized prefill chunk can therefore skip its older + // columns and commit just the trailing window. + const bool replace_local_window = batch == 1 && width > Config::local_capacity; + const int local_offset = replace_local_window ? width - Config::local_capacity : 0; + const int local_width = replace_local_window ? Config::local_capacity : width; + Tensor local_counts = commit_counts; + ops::KVCacheAppendPrefixExecutionEnvelope local_envelope = envelope; + if (replace_local_window) { + if (!state.execution.io.dflash_prefill) { + throw std::logic_error("DFlash2 prefill count storage is unavailable"); + } + local_counts = state.execution.io.dflash_prefill->produced_count; + ops::set_i32_scalar(local_counts, Config::local_capacity, + state.execution.device.stream); + local_envelope = {static_cast(Config::local_capacity), + static_cast(Config::local_capacity)}; + } + + auto context_roots = workspace_recipe::dflash_context(state.execution.work, columns); + ops::linear(features.view({Config::feature_rows, columns}), dflash2.feature_projection, + context_roots.projected, state.execution.device.stream); + Tensor context_full = context_roots.normalized; + ops::rmsnorm(context_roots.projected, dflash2.context_norm, Config::rms_epsilon, false, + context_full, state.execution.device.stream); + Tensor context = replace_local_window + ? context_full.slice(1, local_offset, local_width) + : context_full; + Tensor local_positions = replace_local_window + ? positions.slice(0, local_offset, local_width) + : positions; + + for (int layer = 0; layer < Config::layers; ++layer) { + auto layer_scope = state.execution.work.scope(); + const auto& weight = + dflash2.layers.at(static_cast(layer)); + auto layer_roots = + workspace_recipe::dflash_context_layer(state.execution.work, local_width * batch); + Tensor key_raw = + layer_roots.key_raw.view({Config::head_dim, Config::kv_heads, local_width * batch}); + Tensor value = layer_roots.value.view({Config::head_dim, Config::kv_heads, local_width * batch}); + Tensor key_flat = key_raw.view({Config::kv_size, local_width * batch}); + Tensor value_flat = value.view({Config::kv_size, local_width * batch}); + ops::linear(context, weight.context_key, key_flat, state.execution.device.stream); + ops::linear(context, weight.context_value, value_flat, state.execution.device.stream); + Tensor key = layer_roots.key.view({Config::head_dim, Config::kv_heads, local_width * batch}); + ops::rmsnorm(key_raw, weight.key_norm, Config::rms_epsilon, false, key, + state.execution.device.stream); + ops::rope(local_positions.view({local_width * batch}), Config::head_dim, + Config::rope_theta, key, state.execution.device.stream); + Tensor key_batch = key.view({Config::head_dim, Config::kv_heads, local_width, batch}); + Tensor value_batch = value.view({Config::head_dim, Config::kv_heads, local_width, batch}); + Tensor position_batch = local_positions.view({local_width, batch}); + ops::kv_cache_append_prefix(key_batch, value_batch, position_batch, local_counts, lanes, + local_envelope, + dflash2_state(state).local_layer( + static_cast(layer)), + Config::local_window, state.execution.device.stream); + } +} + +void propose_batch_impl(DFlash2BatchContext& state, qwen3_6::DFlashDecodeState& frame, + std::int32_t batch_size, std::uint32_t k, DFlash2Envelopes envelopes) { + using Config = DFlash2Config; + const std::int32_t width = static_cast(k) + 1; + const std::int32_t columns = width * batch_size; + Tensor anchors = frame.anchors.slice(0, 0, batch_size); + Tensor frontiers = frame.execution_frontiers.slice(0, 0, batch_size); + Tensor valid_columns = frame.target_valid_columns.slice(0, 0, batch_size); + Tensor lanes = frame.active_lanes.slice(0, 0, batch_size); + Tensor ids = frame.proposal_ids.slice(1, 0, batch_size); + Tensor positions = frame.proposal_positions.slice(1, 0, batch_size); + Tensor drafts = frame.draft_tokens.slice(1, 0, batch_size); + if (!state.execution.model.dflash2.has_value()) { + throw std::logic_error("DFlash2 weights are unavailable"); + } + const auto& dflash2 = *state.execution.model.dflash2; + + state.execution.work.reset(); + Tensor attention_valid = state.execution.work.alloc(DType::I32, {batch_size}); + ops::set_i32_scalar(attention_valid, static_cast(width), + state.execution.device.stream); + (void)valid_columns; + + ops::prepare_masked_block(anchors, frontiers, attention_valid, Config::mask_token, ids, + positions, state.execution.device.stream); + Tensor residual = state.execution.work.alloc(DType::BF16, {Config::hidden, columns}); + ops::embedding(ids.view({columns}), state.execution.model.token_embedding, residual, + state.execution.device.stream); + + for (int layer = 0; layer < Config::layers; ++layer) { + const auto& weight = dflash2.layers.at(static_cast(layer)); + { + auto attention_scope = state.execution.work.scope(); + auto roots = + workspace_recipe::dflash_attention(state.execution.work, columns); + ops::rmsnorm(residual, weight.input_norm, Config::rms_epsilon, false, roots.hidden, + state.execution.device.stream); + + Tensor conv_coefficients = state.execution.work.alloc(DType::BF16, {1280, columns}); + ops::linear(roots.hidden, weight.attention_conv_projection, conv_coefficients, + state.execution.device.stream); + Tensor conv_hidden = state.execution.work.alloc(DType::BF16, {Config::hidden, columns}); + ops::dflash2_grouped_conv( + roots.hidden, conv_coefficients, + conv_side_weight(weight.attention_conv_base, 0, Config::conv_kernel_size), + static_cast(k + 1), Config::conv_group_size, + Config::conv_kernel_size, 0, conv_hidden, state.execution.device.stream); + + Tensor query_raw = roots.query_raw.view({Config::head_dim, Config::query_heads, columns}); + Tensor key_raw = roots.key_raw.view({Config::head_dim, Config::kv_heads, columns}); + Tensor value = roots.value.view({Config::head_dim, Config::kv_heads, columns}); + Tensor query_flat = query_raw.view({Config::query_size, columns}); + Tensor key_flat = key_raw.view({Config::kv_size, columns}); + Tensor value_flat = value.view({Config::kv_size, columns}); + const Weight qkv_weight = weight.query_key_value; + const std::size_t row_len = static_cast(qkv_weight.k) * dtype_size(DType::BF16); + Weight query_weight = qkv_weight; + query_weight.n = Config::query_size; + Weight key_weight = qkv_weight; + key_weight.n = Config::kv_size; + key_weight.qdata = static_cast(qkv_weight.qdata) + + static_cast(Config::query_size) * row_len; + Weight value_weight = qkv_weight; + value_weight.n = Config::kv_size; + value_weight.qdata = static_cast(qkv_weight.qdata) + + static_cast(Config::query_size + Config::kv_size) * + row_len; + ops::linear(conv_hidden, query_weight, query_flat, state.execution.device.stream); + ops::linear(conv_hidden, key_weight, key_flat, state.execution.device.stream); + ops::linear(conv_hidden, value_weight, value_flat, state.execution.device.stream); + + Tensor query = roots.query.view({Config::head_dim, Config::query_heads, columns}); + Tensor key = roots.key.view({Config::head_dim, Config::kv_heads, columns}); + ops::rmsnorm(query_raw, weight.query_norm, Config::rms_epsilon, false, query, + state.execution.device.stream); + ops::rmsnorm(key_raw, weight.key_norm, Config::rms_epsilon, false, key, + state.execution.device.stream); + ops::rope(positions.view({columns}), Config::head_dim, Config::rope_theta, query, key, + state.execution.device.stream); + Tensor query_batch = + query.view({Config::head_dim, Config::query_heads, width, batch_size}); + Tensor key_batch = + key.view({Config::head_dim, Config::kv_heads, width, batch_size}); + Tensor value_batch = + value.view({Config::head_dim, Config::kv_heads, width, batch_size}); + Tensor attention_batch = roots.attention.view( + {Config::head_dim, Config::query_heads, width, batch_size}); + ops::swa(query_batch, key_batch, value_batch, positions, attention_valid, lanes, + Config::attention_scale, + dflash2_state(state).local_layer(static_cast(layer)), + envelopes.local, Config::local_window, state.execution.work, attention_batch, + state.execution.device.stream); + + Tensor delta = roots.attention_delta.view({Config::hidden, columns}); + ops::linear(roots.attention.view({Config::query_size, columns}), + weight.attention_output, delta, state.execution.device.stream); + Tensor conv_finish = state.execution.work.alloc(DType::BF16, {Config::hidden, columns}); + ops::dflash2_grouped_conv( + delta, conv_coefficients, + conv_side_weight(weight.attention_conv_base, 1, Config::conv_kernel_size), + static_cast(k + 1), Config::conv_group_size, + Config::conv_kernel_size, 1, conv_finish, state.execution.device.stream); + ops::residual_add(conv_finish, residual, state.execution.device.stream); + } + { + auto mlp_scope = state.execution.work.scope(); + auto roots = workspace_recipe::dflash_mlp(state.execution.work, columns); + ops::rmsnorm(residual, weight.post_attention_norm, Config::rms_epsilon, false, + roots.hidden, state.execution.device.stream); + + Tensor conv_coefficients = state.execution.work.alloc(DType::BF16, {1280, columns}); + ops::linear(roots.hidden, weight.mlp_conv_projection, conv_coefficients, + state.execution.device.stream); + Tensor conv_hidden = state.execution.work.alloc(DType::BF16, {Config::hidden, columns}); + ops::dflash2_grouped_conv( + roots.hidden, conv_coefficients, + conv_side_weight(weight.mlp_conv_base, 0, Config::conv_kernel_size), + static_cast(k + 1), Config::conv_group_size, + Config::conv_kernel_size, 0, conv_hidden, state.execution.device.stream); + + Tensor gate_up = roots.gate_up.view({2 * Config::intermediate, columns}); + ops::linear(conv_hidden, weight.gate_up, gate_up, state.execution.device.stream); + ops::silu_mul(gate_up.slice(0, 0, Config::intermediate), + gate_up.slice(0, Config::intermediate, Config::intermediate), + roots.intermediate, state.execution.device.stream); + Tensor delta = roots.delta.view({Config::hidden, columns}); + ops::linear(roots.intermediate, weight.down, delta, state.execution.device.stream); + Tensor conv_finish = state.execution.work.alloc(DType::BF16, {Config::hidden, columns}); + ops::dflash2_grouped_conv( + delta, conv_coefficients, + conv_side_weight(weight.mlp_conv_base, 1, Config::conv_kernel_size), + static_cast(k + 1), Config::conv_group_size, + Config::conv_kernel_size, 1, conv_finish, state.execution.device.stream); + ops::residual_add(conv_finish, residual, state.execution.device.stream); + } + } + + Tensor packed = state.execution.work.alloc( + DType::BF16, {Config::hidden, static_cast(k) * batch_size}); + const std::size_t element_bytes = dtype_size(DType::BF16); + const std::size_t row_bytes = + static_cast(Config::hidden) * static_cast(k) * element_bytes; + const std::size_t source_pitch = + static_cast(Config::hidden) * width * element_bytes; + // DFlash2 predicts the seven masked columns (1..7); the bonus token stays + // at column 0 and is not part of the proposal block. + const auto* source = static_cast(residual.data) + + static_cast(Config::hidden) * element_bytes; + CUDA_CHECK(cudaMemcpy2DAsync(packed.data, row_bytes, source, source_pitch, row_bytes, + static_cast(batch_size), cudaMemcpyDeviceToDevice, + state.execution.device.stream)); + Tensor proposal_hidden = state.execution.work.alloc( + DType::BF16, {Config::hidden, static_cast(k) * batch_size}); + ops::rmsnorm(packed, dflash2.final_norm, Config::rms_epsilon, false, proposal_hidden, + state.execution.device.stream); + + Tensor flat_drafts = drafts.view({static_cast(k) * batch_size}); + Tensor logits = state.execution.work.alloc( + DType::BF16, {TextConfig::output_rows, static_cast(k) * batch_size}); + ops::linear(proposal_hidden, state.execution.model.output_head, logits, + state.execution.device.stream); + Tensor projected = state.execution.work.alloc( + DType::BF16, {Config::selector_rank, static_cast(k) * batch_size}); + ops::linear(proposal_hidden, dflash2.selector_hidden_projection, projected, + state.execution.device.stream); + Tensor candidates = state.execution.work.alloc( + DType::I32, {batch_size, Config::block_drafts, Config::selector_top_k}); + Tensor unary = state.execution.work.alloc( + DType::FP32, {batch_size, Config::block_drafts, Config::selector_top_k}); + Tensor scores = state.execution.work.alloc( + DType::FP32, + {batch_size, Config::block_drafts, Config::selector_top_k, Config::selector_top_k}); + ops::dflash2_selector(logits, projected, dflash2.selector_predecessor_codebook, + dflash2.selector_successor_codebook, anchors, candidates, unary, scores, + flat_drafts, Config::block_drafts, Config::selector_top_k, + state.execution.device.stream); + state.execution.work.reset(); +} + +auto dflash2_decode_batch_body(DFlash2BatchContext& state, std::int32_t batch_size, + std::uint32_t k, DFlash2Envelopes envelopes, + ops::GqaExecutionEnvelope target_envelope) { + return [&state, batch_size, k, envelopes, target_envelope] { + if (batch_size <= 0 || batch_size > static_cast(kMaximumConcurrency) || + k == 0 || k > kDFlashDecodeMaximumDrafts || k != DFlash2Config::block_drafts) { + throw std::logic_error("DFlash2 decode batch state is incomplete"); + } + qwen3_6::DFlashDecodeState& frame = state.frame; + const std::int32_t width = static_cast(k) + 1; + CUDA_CHECK(cudaMemcpyAsync(frame.ingress.data, &state.host_ingress, + sizeof(qwen3_6::DFlashDecodeIngress), cudaMemcpyHostToDevice, + state.execution.device.stream)); + + Tensor anchors = frame.anchors.slice(0, 0, batch_size); + Tensor frontiers = frame.execution_frontiers.slice(0, 0, batch_size); + Tensor context_starts = frame.context_frontiers.slice(0, 0, batch_size); + Tensor extents = frame.proposal_extents.slice(0, 0, batch_size); + Tensor valid_columns = frame.target_valid_columns.slice(0, 0, batch_size); + Tensor text_rows = frame.text_kv_table_rows.slice(0, 0, batch_size); + Tensor lanes = frame.active_lanes.slice(0, 0, batch_size); + Tensor state_sources = frame.state_source_slots.slice(0, 0, batch_size); + Tensor state_destinations = frame.state_destination_slots.slice(0, 0, batch_size); + Tensor append_positions = frame.append_positions.slice(1, 0, batch_size); + Tensor append_counts = frame.append_counts.slice(0, 0, batch_size); + Tensor drafts = frame.draft_tokens.slice(1, 0, batch_size); + Tensor verify_ids = frame.verify_ids.slice(1, 0, batch_size); + Tensor target_positions = frame.proposal_positions.slice(1, 0, batch_size); + Tensor target_tokens = frame.target_argmax.slice(1, 0, batch_size); + Tensor target_logits = frame.target_logits.slice(2, 0, batch_size); + Tensor target_hidden = frame.target_hidden.slice(2, 0, batch_size); + Tensor selected_hidden = frame.target_continuation_hidden.slice(1, 0, batch_size); + Tensor licensed_tokens = frame.licensed_tokens.slice(1, 0, batch_size); + Tensor licensed_counts = frame.licensed_counts.slice(0, 0, batch_size); + Tensor accepted = frame.accepted_drafts.slice(0, 0, batch_size); + + state.execution.work.reset(); + Tensor compact_features = state.execution.work.alloc( + DType::BF16, {DFlash2Config::feature_rows, width, batch_size}); + ops::prepare_ragged_prefix(dflash2_state(state).pending_features, lanes, context_starts, + frontiers, compact_features, append_positions, append_counts, + state.execution.device.stream); + DFlash2AppendContext append_state{.execution = state.execution, + .dflash2 = state.dflash2}; + append_context_impl(append_state, compact_features, append_positions, append_counts, lanes, + text_rows, envelopes.append); + + propose_batch_impl(state, frame, batch_size, k, envelopes); + ops::speculative_prepare_verify_ids(anchors, drafts, extents, verify_ids, + state.execution.device.stream); + + TextContext card(state.execution.device, state.execution.model, state.execution.work, {}, + state.execution.linear_attention, state.execution.io, + state.execution.prefill_hidden, state.execution.prefill_chunk, 0, {}, + &state.text_cache); + DFlashFeatureSink sink = dflash2_batch_feature_sink_impl( + state, lanes, valid_columns, width, batch_size); + target_verify_accept(state.execution, state.continuation_hidden_store, card, + TargetVerifyFrameView{ + .ids = verify_ids, + .cache_positions = target_positions, + .rope_positions = target_positions, + .valid_columns = valid_columns, + .kv_table_rows = text_rows, + .state_source_slots = state_sources, + .state_destination_slots = state_destinations, + .target_hidden = target_hidden, + .target_logits = target_logits, + .target_tokens = target_tokens, + .drafts = drafts, + .current_extents = extents, + .frontiers = frontiers, + .anchors = anchors, + .licensed_tokens = licensed_tokens, + .licensed_counts = licensed_counts, + .accepted_drafts = accepted, + .selected_hidden = selected_hidden, + .replay_records = state.execution.replay_records, + .sampling = frame.sampling, + .feature_sink = &sink, + }, + target_envelope); + CUDA_CHECK(cudaMemcpyAsync( + frame.egress_proposal_extents.slice(0, 0, batch_size).data, extents.data, + static_cast(batch_size) * sizeof(std::int32_t), cudaMemcpyDeviceToDevice, + state.execution.device.stream)); + CUDA_CHECK(cudaMemcpyAsync(&state.host_egress, frame.egress.data, + sizeof(qwen3_6::DFlashDecodeEgress), cudaMemcpyDeviceToHost, + state.execution.device.stream)); + }; +} + +} // namespace + +DFlashFeatureSink dflash2_feature_sink(PrefillContext& state, + DFlashFeatureSink::PrefillConsumer consume_prefill) { + return dflash2_prefill_feature_sink_impl(state, std::move(consume_prefill)); +} + +void dflash2_append_context(DFlash2AppendContext& state, const Tensor& features, + const Tensor& positions, const Tensor& commit_counts, + const Tensor& lanes, const Tensor& table_rows, + ops::KVCacheAppendPrefixExecutionEnvelope envelope) { + append_context_impl(state, features, positions, commit_counts, lanes, table_rows, envelope); +} + +void dflash2_append_context(PrefillContext& state, const Tensor& features, const Tensor& positions, + const Tensor& commit_counts, const Tensor& lanes, + const Tensor& table_rows, + ops::KVCacheAppendPrefixExecutionEnvelope envelope) { + if (state.dflash2 == nullptr) { + throw std::logic_error("DFlash2 context append requires DFlash2 state"); + } + DFlash2AppendContext context{.execution = state.execution, .dflash2 = *state.dflash2}; + append_context_impl(context, features, positions, commit_counts, lanes, table_rows, envelope); +} + +void capture_dflash2_decode_batch(DFlash2BatchContext& state, std::int32_t batch_size, + std::uint32_t k, DFlash2Envelopes envelopes, + ops::GqaExecutionEnvelope target_envelope, + DecodeGraphDefinition& definition) { + auto body = dflash2_decode_batch_body(state, batch_size, k, envelopes, target_envelope); + capture_graph(state, definition, body); +} + +void dflash2_decode_batch(DFlash2BatchContext& state, std::int32_t batch_size, std::uint32_t k, + DFlash2Envelopes envelopes, ops::GqaExecutionEnvelope target_envelope, + DecodeGraphExecutable* executable) { + auto body = dflash2_decode_batch_body(state, batch_size, k, envelopes, target_envelope); + run_prepared(state, executable, body); +} + +} // namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule diff --git a/src/targets/qwen3_6/impl/runtime/dflash_context.h b/src/targets/qwen3_6/impl/runtime/dflash_context.h index ecb1dd47ce..f393024b4e 100644 --- a/src/targets/qwen3_6/impl/runtime/dflash_context.h +++ b/src/targets/qwen3_6/impl/runtime/dflash_context.h @@ -26,4 +26,18 @@ struct DFlashPersistentState { cudaStream_t stream); }; +struct DFlash2PersistentState { + CyclicKVCache local; + CyclicKVCache rewrite_checkpoint_local; + Tensor prefill_features; + Tensor prefill_positions; + Tensor pending_features; + + DFlash2PersistentState(DeviceSpan backing, const DFlash2PersistentLayout& layout); + + [[nodiscard]] CyclicKVCacheLayerView local_layer(std::uint32_t layer) const; + void save_rewrite_checkpoint(std::int32_t lane, cudaStream_t stream); + void restore_rewrite_checkpoint(std::int32_t lane, cudaStream_t stream); +}; + } // namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS diff --git a/src/targets/qwen3_6/impl/runtime/dflash_context_impl.h b/src/targets/qwen3_6/impl/runtime/dflash_context_impl.h index 18b1935d42..c9c97f4283 100644 --- a/src/targets/qwen3_6/impl/runtime/dflash_context_impl.h +++ b/src/targets/qwen3_6/impl/runtime/dflash_context_impl.h @@ -38,4 +38,36 @@ void DFlashPersistentState::save_rewrite_checkpoint(std::int32_t source_slot, local.copy_slot_from(local, source_slot, destination_slot, stream); } +DFlash2PersistentState::DFlash2PersistentState(DeviceSpan backing, + const DFlash2PersistentLayout& layout) + : local(backing, layout.local), + rewrite_checkpoint_local(backing, layout.rewrite_checkpoint_local), + prefill_features(layout.prefill_features.bind(backing)), + prefill_positions(layout.prefill_positions.bind(backing)), + pending_features(layout.pending_features.bind(backing)) { + if (local.layer_count() != DFlash2Config::local_layers || + rewrite_checkpoint_local.layer_count() != DFlash2Config::local_layers || + local.capacity() != DFlash2Config::local_capacity || + rewrite_checkpoint_local.capacity() != DFlash2Config::local_capacity || + local.num_kv_heads() != DFlash2Config::kv_heads || + rewrite_checkpoint_local.num_kv_heads() != DFlash2Config::kv_heads || + local.head_dim() != DFlash2Config::head_dim || + rewrite_checkpoint_local.head_dim() != DFlash2Config::head_dim || + local.lane_capacity() != rewrite_checkpoint_local.lane_capacity()) { + throw std::invalid_argument("DFlash2 persistent cache layout is invalid"); + } +} + +CyclicKVCacheLayerView DFlash2PersistentState::local_layer(std::uint32_t layer) const { + return local.layer_view(layer); +} + +void DFlash2PersistentState::save_rewrite_checkpoint(std::int32_t lane, cudaStream_t stream) { + rewrite_checkpoint_local.copy_slot_from(local, lane, lane, stream); +} + +void DFlash2PersistentState::restore_rewrite_checkpoint(std::int32_t lane, cudaStream_t stream) { + local.copy_slot_from(rewrite_checkpoint_local, lane, lane, stream); +} + } // namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS diff --git a/src/targets/qwen3_6/impl/runtime/dflash_impl.h b/src/targets/qwen3_6/impl/runtime/dflash_impl.h index cffb1e1700..a3600a158e 100644 --- a/src/targets/qwen3_6/impl/runtime/dflash_impl.h +++ b/src/targets/qwen3_6/impl/runtime/dflash_impl.h @@ -172,7 +172,7 @@ void append_context_impl(Context& state, const Tensor& features, const Tensor& p ops::kv_cache_append_prefix( key_batch, value_batch, position_batch, local_counts, lanes, local_envelope, dflash_state(state).local_layer(static_cast(layer)), - state.execution.device.stream); + Config::local_capacity, state.execution.device.stream); } else { ops::kv_cache_append_prefix( key_batch, value_batch, position_batch, commit_counts, table_rows, envelope, @@ -326,7 +326,7 @@ void propose_batch_impl(DFlashBatchContext& state, qwen3_6::DFlashDecodeState& f auto dflash_decode_batch_body(DFlashBatchContext& state, std::int32_t batch_size, std::uint32_t k, DFlashEnvelopes envelopes, - ops::CausalAttentionExecutionEnvelope target_envelope) { + ops::GqaExecutionEnvelope target_envelope) { return [&state, batch_size, k, envelopes, target_envelope] { if (batch_size <= 0 || batch_size > static_cast(kMaximumConcurrency) || k == 0 || k > kDFlashDecodeMaximumDrafts) { @@ -440,7 +440,7 @@ void dflash_append_context(PrefillContext& state, const Tensor& features, const void capture_dflash_decode_batch(DFlashBatchContext& state, std::int32_t batch_size, std::uint32_t k, DFlashEnvelopes envelopes, - ops::CausalAttentionExecutionEnvelope target_envelope, + ops::GqaExecutionEnvelope target_envelope, DecodeGraphDefinition& definition) { auto body = dflash_decode_batch_body(state, batch_size, k, envelopes, target_envelope); capture_graph(state, definition, body); @@ -448,7 +448,7 @@ void capture_dflash_decode_batch(DFlashBatchContext& state, std::int32_t batch_s void dflash_decode_batch(DFlashBatchContext& state, std::int32_t batch_size, std::uint32_t k, DFlashEnvelopes envelopes, - ops::CausalAttentionExecutionEnvelope target_envelope, + ops::GqaExecutionEnvelope target_envelope, DecodeGraphExecutable* executable) { auto body = dflash_decode_batch_body(state, batch_size, k, envelopes, target_envelope); run_prepared(state, executable, body); diff --git a/src/targets/qwen3_6/impl/runtime/instance.h b/src/targets/qwen3_6/impl/runtime/instance.h index 3e3a7153bd..d13a9f80f7 100644 --- a/src/targets/qwen3_6/impl/runtime/instance.h +++ b/src/targets/qwen3_6/impl/runtime/instance.h @@ -16,12 +16,14 @@ using WeightsProfile = typename Variant::WeightsProfile; using TextConfig = typename Variant::TextConfig; using VisionConfig = typename Variant::VisionConfig; using DFlashConfig = typename Variant::DFlashConfig; +using DFlash2Config = typename Variant::DFlash2Config; using LoadedModelData = typename Variant::ModelView; using FullAttentionWeights = typename LoadedModelData::FullLayer; using GdnWeights = typename LoadedModelData::GdnLayer; using MlpWeights = typename Variant::PostMixerWeights; using MtpWeights = typename LoadedModelData::MtpLayer; using DFlashWeights = typename LoadedModelData::DFlash; +using DFlash2Weights = typename LoadedModelData::DFlash2; using FullAttentionProjectionWeights = typename Variant::FullAttentionProjectionWeights; using GdnProjectionWeights = typename Variant::GdnProjectionWeights; using VisionWeights = typename Variant::VisionWeights; @@ -74,4 +76,10 @@ inline std::vector dflash_graph_profiles(std::uint32_t ca return Variant::dflash_graph_profiles(capacity, draft_window, batch_size); } +inline std::vector dflash2_graph_profiles(std::uint32_t capacity, + std::uint32_t draft_window, + std::uint32_t batch_size) { + return Variant::dflash2_graph_profiles(capacity, draft_window, batch_size); +} + } // namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS diff --git a/src/targets/qwen3_6/impl/runtime/instantiate.h b/src/targets/qwen3_6/impl/runtime/instantiate.h index ab76f098b5..ff667049c4 100644 --- a/src/targets/qwen3_6/impl/runtime/instantiate.h +++ b/src/targets/qwen3_6/impl/runtime/instantiate.h @@ -21,6 +21,7 @@ #include "targets/qwen3_6/impl/runtime/graph_impl.h" #include "targets/qwen3_6/impl/runtime/speculative_target_impl.h" #include "targets/qwen3_6/impl/runtime/dflash_impl.h" +#include "targets/qwen3_6/impl/runtime/dflash2_impl.h" #include "targets/qwen3_6/impl/runtime/decode_impl.h" #include "targets/qwen3_6/impl/runtime/mtp_impl.h" #include "targets/qwen3_6/impl/runtime/request_plan_impl.h" diff --git a/src/targets/qwen3_6/impl/runtime/kv_calibration.h b/src/targets/qwen3_6/impl/runtime/kv_calibration.h new file mode 100644 index 0000000000..5cf6b94ef2 --- /dev/null +++ b/src/targets/qwen3_6/impl/runtime/kv_calibration.h @@ -0,0 +1,116 @@ +#pragma once +#include "targets/qwen3_6/impl/runtime/instance.h" +// Qwen3.6 family runtime implementation; instantiated only by exact variants. + +#include "core/device.h" +#include "core/dtype.h" +#include "core/tensor.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule { + +// Offline KV calibration capture. When EngineOptions.kv_calibration_dir is set, +// text prefill copies the exact post-RoPE K and V tensors quantized into the +// paged KV cache (per full-attention layer and per chunk) to the host and +// appends one framed binary record per layer/chunk. The Python analyzer in +// tools/calib consumes these records; inference never reads them back. +class KvCalibrationCapture { +public: + explicit KvCalibrationCapture(std::filesystem::path directory) + : directory_(std::move(directory)) { + if (directory_.empty()) { + throw std::invalid_argument("KV calibration directory must not be empty"); + } + std::filesystem::create_directories(directory_); + } + + void capture(std::uint32_t full_layer, const Tensor& k, const Tensor& v, + const Tensor& positions) { + if (k.dtype != DType::BF16 || v.dtype != DType::BF16 || positions.dtype != DType::I32 || + !k.is_contiguous() || !v.is_contiguous() || !positions.is_contiguous() || + k.data == nullptr || v.data == nullptr || positions.data == nullptr) { + throw std::invalid_argument( + "KV calibration capture requires contiguous BF16 K/V and I32 positions"); + } + if (k.ne[0] != v.ne[0] || k.ne[1] != v.ne[1] || k.ne[2] != v.ne[2] || k.ne[3] != 1 || + v.ne[3] != 1 || positions.ne[0] != k.ne[2] || positions.ne[1] != 1 || + positions.ne[2] != 1 || positions.ne[3] != 1) { + throw std::invalid_argument("KV calibration capture tensor shapes do not match"); + } + const auto head_dim = static_cast(k.ne[0]); + const auto kv_heads = static_cast(k.ne[1]); + const auto tokens = static_cast(k.ne[2]); + if (head_dim == 0 || kv_heads == 0 || tokens == 0 || + tokens > static_cast(std::numeric_limits::max())) { + throw std::invalid_argument("KV calibration capture shapes are out of range"); + } + + std::vector positions_host(tokens); + std::vector k_host(k.bytes()); + std::vector v_host(v.bytes()); + CUDA_CHECK(cudaMemcpy(positions_host.data(), positions.data, positions.bytes(), + cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(k_host.data(), k.data, k.bytes(), cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(v_host.data(), v.data, v.bytes(), cudaMemcpyDeviceToHost)); + + Header header{}; + std::memcpy(header.magic, kMagic, sizeof(header.magic)); + header.header_bytes = sizeof(Header); + header.full_layer = full_layer; + header.head_dim = head_dim; + header.kv_heads = kv_heads; + header.tokens = tokens; + header.record_index = record_index_; + header.first_position = positions_host.front(); + header.last_position = positions_host.back(); + + const std::filesystem::path path = + directory_ / (std::to_string(record_index_) + ".kvc"); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out) { throw std::runtime_error("cannot create KV calibration record: " + path.string()); } + out.write(reinterpret_cast(&header), sizeof(header)); + out.write(reinterpret_cast(positions_host.data()), + static_cast(positions_host.size() * sizeof(std::int32_t))); + out.write(reinterpret_cast(k_host.data()), + static_cast(k_host.size())); + out.write(reinterpret_cast(v_host.data()), + static_cast(v_host.size())); + if (!out) { throw std::runtime_error("failed to write KV calibration record: " + path.string()); } + ++record_index_; + } + + [[nodiscard]] std::uint32_t record_count() const noexcept { return record_index_; } + +private: + static constexpr char kMagic[16] = {'N', 'I', 'N', 'F', 'E', 'R', 'K', 'V', + 'C', 'A', 'L', '1', 0, 0, 0, 0}; + struct Header { + char magic[16]; + std::uint32_t header_bytes; + std::uint32_t full_layer; + std::uint32_t head_dim; + std::uint32_t kv_heads; + std::uint32_t tokens; + std::uint32_t record_index; + std::int32_t first_position; + std::int32_t last_position; + std::uint32_t reserved[4]; + }; + static_assert(sizeof(Header) == 64); + + std::filesystem::path directory_; + std::uint32_t record_index_ = 0; +}; + +} // namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule diff --git a/src/targets/qwen3_6/impl/runtime/layouts.h b/src/targets/qwen3_6/impl/runtime/layouts.h index 859f4f5d7e..a3ff20a5f0 100644 --- a/src/targets/qwen3_6/impl/runtime/layouts.h +++ b/src/targets/qwen3_6/impl/runtime/layouts.h @@ -31,11 +31,24 @@ struct DFlashPersistentLayout { [[nodiscard]] std::size_t kv_payload_bytes() const noexcept { return full.payload_bytes(); } }; +struct DFlash2PersistentLayout { + CyclicKVCacheLayout local; + CyclicKVCacheLayout rewrite_checkpoint_local; + TensorLayout prefill_features; + TensorLayout prefill_positions; + TensorLayout pending_features; + + [[nodiscard]] std::size_t kv_payload_bytes() const noexcept { + return local.payload_bytes() + rewrite_checkpoint_local.payload_bytes(); + } +}; + struct PersistentLayout { qwen3_6::DecoderStateLayout decoder; qwen3_6::StateImageDeviceLayout state_images; std::optional replay_records; std::optional dflash; + std::optional dflash2; qwen3_6::RoundStateLayout round; TensorLayout prefill_hidden; std::optional score_hidden; @@ -61,6 +74,8 @@ struct WorkspacePlan { std::size_t mtp_round = 0; std::size_t dflash_context = 0; std::size_t dflash_round = 0; + std::size_t dflash2_context = 0; + std::size_t dflash2_round = 0; std::size_t causal_score = 0; std::size_t general_capacity = 0; std::optional vision; @@ -70,16 +85,24 @@ struct WorkspacePlan { struct SequencePlanningInputs { WeightsProfile weights_profile; std::uint32_t capacity = 0; + // Explicit --kv-capacity page count (when set below capacity) shrinks the + // device page-pool floor: max_context then bounds only the rope domain. + std::optional kv_capacity_tokens; std::uint32_t max_concurrency = 1; std::uint32_t prefill_chunk = 0; std::uint32_t draft_window = 0; SpeculativeBackend speculative_backend = SpeculativeBackend::None; DType kv_dtype = DType::BF16; std::int32_t kv_quant_group = 0; + std::array layer_kv_dtypes{}; ProposalHead proposal_head = ProposalHead::Full; StartupFeatures features; bool use_cuda_graph = true; + std::uint32_t graph_capture_ceiling = 0; bool causal_scoring = false; + ColdPolicy cold_policy = ColdPolicy::None; + std::uint32_t cold_keep_tokens = 128; + std::uint64_t cold_host_bytes = 4ULL << 30; int device = 0; ContextCacheOptions context_cache; }; @@ -100,9 +123,14 @@ struct SequencePlanImpl { SpeculativeBackend speculative_backend = SpeculativeBackend::None; DType kv_dtype = DType::BF16; std::int32_t kv_quant_group = 0; + std::array layer_kv_dtypes{}; ProposalHead proposal_head = ProposalHead::Full; StartupFeatures features; bool use_cuda_graph = true; + ColdPolicy cold_policy = ColdPolicy::None; + std::uint32_t cold_keep_tokens = 128; + std::uint64_t cold_host_bytes = 4ULL << 30; + std::uint32_t graph_capture_ceiling = 0; bool causal_scoring = false; int device = 0; ContextCacheOptions context_cache; diff --git a/src/targets/qwen3_6/impl/runtime/layouts_impl.h b/src/targets/qwen3_6/impl/runtime/layouts_impl.h index b21165ac86..a5726adcf7 100644 --- a/src/targets/qwen3_6/impl/runtime/layouts_impl.h +++ b/src/targets/qwen3_6/impl/runtime/layouts_impl.h @@ -136,10 +136,14 @@ PersistentLayout persistent_layout(const SequencePlanImpl& plan) { .attention_head_dim = TextConfig::head_dim, .kv_dtype = plan.kv_dtype, .kv_quant_group = plan.kv_quant_group, + .layer_kv_dtypes = plan.layer_kv_dtypes, .enable_mtp = plan.features.mtp(), - .kv_table_rows = static_cast(plan.max_concurrency), + .kv_table_rows = static_cast(plan.max_concurrency + 1), .text_physical_page_groups = physical_pages, .mtp_physical_page_groups = mtp_physical_pages, + .max_cold_pages = plan.cold_policy == ColdPolicy::Window + ? plan.cold_keep_tokens / kPagedKVPageSize + 16 + : 0, }); qwen3_6::StateImageSpec state_image_spec{ .linear = @@ -199,7 +203,7 @@ PersistentLayout persistent_layout(const SequencePlanImpl& plan) { builder, KVExecutionTableSpec{ .logical_page_capacity = logical_pages, - .table_rows = static_cast(plan.max_concurrency), + .table_rows = static_cast(plan.max_concurrency + 1), }), .layers = 1, .max_context = plan.capacity, @@ -220,6 +224,30 @@ PersistentLayout persistent_layout(const SequencePlanImpl& plan) { "DFlash pending target features"); } } + if constexpr (Variant::supports_dflash2) { + if (plan.features.dflash2()) { + DFlash2PersistentLayout& dflash2 = out.dflash2.emplace(); + dflash2.local = plan_cyclic_kv_cache(builder, DFlash2Config::local_layers, + DFlash2Config::local_capacity, + DFlash2Config::kv_heads, DFlash2Config::head_dim, + static_cast(plan.max_concurrency)); + dflash2.rewrite_checkpoint_local = plan_cyclic_kv_cache( + builder, DFlash2Config::local_layers, DFlash2Config::local_capacity, + DFlash2Config::kv_heads, DFlash2Config::head_dim, + static_cast(plan.max_concurrency)); + dflash2.prefill_features = add_tensor( + builder, DType::BF16, {DFlash2Config::feature_rows, effective_prefill_chunk}, + "DFlash2 prefill target features"); + dflash2.prefill_positions = add_tensor(builder, DType::I32, {effective_prefill_chunk}, + "DFlash2 prefill target positions"); + dflash2.pending_features = add_tensor( + builder, DType::BF16, + {DFlash2Config::feature_rows, + static_cast(plan.draft_window + 1U), + static_cast(plan.max_concurrency)}, + "DFlash2 pending target features"); + } + } out.round = qwen3_6::begin_round_state_layout( builder, qwen3_6::RoundStateSpec{.hidden = TextConfig::hidden, @@ -227,7 +255,7 @@ PersistentLayout persistent_layout(const SequencePlanImpl& plan) { .batch_capacity = plan.max_concurrency, .draft_window = plan.draft_window, .enable_mtp = plan.features.mtp(), - .enable_dflash = plan.features.dflash()}); + .enable_dflash = plan.features.dflash_like()}); out.prefill_hidden = add_tensor( builder, DType::BF16, {TextConfig::hidden, effective_prefill_chunk}, "step prefill hidden"); if (plan.causal_scoring) { @@ -264,7 +292,7 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { const auto chunk = static_cast(chunk_u32); const auto drafts = static_cast(plan.draft_window); const auto verify = drafts + 1; - const ops::CausalAttentionExecutionEnvelope text_envelope{1, plan.capacity}; + const ops::GqaExecutionEnvelope text_envelope{1, plan.capacity}; const auto matrix = [](WorkspaceLayoutBuilder& layout, DType dtype, std::int32_t rows, std::int32_t tokens) { (void)layout.alloc(dtype, {rows, tokens}); }; @@ -283,15 +311,15 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { std::int32_t last, qwen3_6::TextPhase phase, std::int32_t batch_size, std::int32_t min_width, std::int32_t max_width, - ops::CausalAttentionExecutionEnvelope envelope) { + ops::GqaExecutionEnvelope envelope) { auto stage = layout.scope(); (void)workspace_recipe::text_attention_projection(layout, last); scratch(layout, Variant::attention_projection_workspace_capacity_bytes(plan.weights_profile, phase, first, last)); (void)workspace_recipe::text_attention_results(layout, last); - scratch(layout, ops::causal_softmax_attention_workspace_capacity_bytes( - {TextConfig::head_dim, TextConfig::query_heads, TextConfig::kv_heads}, - plan.kv_dtype, envelope, batch_size, min_width, max_width)); + scratch(layout, ops::gqa_attention_workspace_capacity_bytes( + TextConfig::query_heads, plan.kv_dtype, envelope, batch_size, + min_width, max_width)); scratch(layout, Variant::attention_output_projection_workspace_capacity_bytes( plan.weights_profile, phase, first, last)); }; @@ -335,7 +363,7 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { std::int32_t last, qwen3_6::TextPhase phase, GdnWorkspacePath path, std::int32_t batch_size, std::int32_t min_width, std::int32_t max_width, - ops::CausalAttentionExecutionEnvelope envelope) { + ops::GqaExecutionEnvelope envelope) { attention_stage(layout, first, last, phase, batch_size, min_width, max_width, envelope); gdn_stage(layout, first, last, phase, path, batch_size, min_width, max_width); post_mixer_stage(layout, first, last, phase); @@ -350,20 +378,19 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { (void)workspace_recipe::mtp_stem(layout, tokens, !preembedded); }; const auto mtp_full_core = [&](WorkspaceLayoutBuilder& layout, std::int32_t tokens, - ops::CausalAttentionExecutionEnvelope envelope) { + ops::GqaExecutionEnvelope envelope) { auto core = layout.scope(); mtp_stem(layout, tokens, false); (void)workspace_recipe::mtp_attention_projection(layout, tokens); scratch(layout, Variant::mtp_attention_projection_workspace_capacity_bytes(tokens, tokens)); (void)workspace_recipe::mtp_attention_results(layout, tokens); - scratch(layout, ops::causal_softmax_attention_workspace_capacity_bytes( - {TextConfig::head_dim, TextConfig::query_heads, TextConfig::kv_heads}, - plan.kv_dtype, envelope, 1, tokens, tokens)); + scratch(layout, ops::gqa_attention_workspace_capacity_bytes( + TextConfig::query_heads, plan.kv_dtype, envelope, 1, tokens, tokens)); (void)workspace_recipe::mtp_post_attention(layout, tokens); scratch(layout, Variant::mtp_post_mixer_workspace_capacity_bytes(tokens, tokens)); }; const auto mtp_full_call = [&](WorkspaceLayoutBuilder& layout, std::int32_t tokens, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, bool build_proposal) { auto call = layout.scope(); matrix(layout, DType::I32, 1, tokens); @@ -392,9 +419,8 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { matrix(layout, DType::BF16, TextConfig::query_size, 1); matrix(layout, DType::I32, 3, 1); matrix(layout, DType::BF16, TextConfig::query_size, 1); - scratch(layout, ops::causal_softmax_attention_workspace_capacity_bytes( - {TextConfig::head_dim, TextConfig::query_heads, TextConfig::kv_heads}, - plan.kv_dtype, text_envelope, 1, 1, 1)); + scratch(layout, ops::gqa_attention_workspace_capacity_bytes( + TextConfig::query_heads, plan.kv_dtype, text_envelope, 1, 1, 1)); matrix(layout, DType::BF16, TextConfig::hidden, 1); matrix(layout, DType::BF16, TextConfig::hidden, 1); scratch(layout, Variant::mtp_post_mixer_workspace_capacity_bytes(1, 1)); @@ -476,9 +502,8 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { Variant::mtp_attention_projection_workspace_capacity_bytes(tokens, tokens)); (void)workspace_recipe::mtp_attention_results(layout, tokens); scratch(layout, - ops::causal_softmax_attention_workspace_capacity_bytes( - {TextConfig::head_dim, TextConfig::query_heads, TextConfig::kv_heads}, - plan.kv_dtype, text_envelope, batch, width, width)); + ops::gqa_attention_workspace_capacity_bytes( + TextConfig::query_heads, plan.kv_dtype, text_envelope, batch, width, width)); (void)workspace_recipe::mtp_post_attention(layout, tokens); scratch(layout, Variant::mtp_post_mixer_workspace_capacity_bytes(tokens, tokens)); }; @@ -572,9 +597,110 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { } } + if (plan.features.dflash2()) { + if constexpr (!Variant::supports_dflash2) { + throw std::logic_error("unsupported target reached DFlash2 scratch planning"); + } else { + const auto dflash2_context_capacity = [&](std::int32_t tokens, bool compact_input) { + WorkspaceLayoutBuilder layout; + if (compact_input) { + matrix(layout, DType::BF16, DFlash2Config::feature_rows, tokens); + } + (void)workspace_recipe::dflash_context(layout, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, DFlash2Config::hidden, DFlash2Config::feature_rows, + ops::LinearPolicy::A16Only, tokens, tokens); + { + auto layer = layout.scope(); + (void)workspace_recipe::dflash_context_layer(layout, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, DFlash2Config::kv_size, DFlash2Config::hidden, + ops::LinearPolicy::A16Only, tokens, tokens); + } + return finish(layout); + }; + const auto dflash2_proposal_capacity = [&](std::int32_t width, std::int32_t batch) { + WorkspaceLayoutBuilder layout; + const std::int32_t tokens = width * batch; + matrix(layout, DType::BF16, DFlash2Config::hidden, tokens); + { + auto attention = layout.scope(); + (void)workspace_recipe::dflash_attention(layout, tokens); + matrix(layout, DType::BF16, 1280, tokens); + matrix(layout, DType::BF16, DFlash2Config::hidden, tokens); + matrix(layout, DType::BF16, DFlash2Config::hidden, tokens); + scratch(layout, ops::swa_workspace_capacity_bytes( + {0, plan.capacity}, width, width, batch, + DFlash2Config::local_window)); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, DFlash2Config::query_size + 2 * DFlash2Config::kv_size, + DFlash2Config::hidden, ops::LinearPolicy::A16Only, tokens, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, DFlash2Config::hidden, DFlash2Config::query_size, + ops::LinearPolicy::A16Only, tokens, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, 1280, DFlash2Config::hidden, ops::LinearPolicy::A16Only, + tokens, tokens); + } + { + auto mlp = layout.scope(); + (void)workspace_recipe::dflash_mlp(layout, tokens); + matrix(layout, DType::BF16, 1280, tokens); + matrix(layout, DType::BF16, DFlash2Config::hidden, tokens); + matrix(layout, DType::BF16, DFlash2Config::hidden, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, 2 * DFlash2Config::intermediate, DFlash2Config::hidden, + ops::LinearPolicy::A16Only, tokens, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, DFlash2Config::hidden, DFlash2Config::intermediate, + ops::LinearPolicy::A16Only, tokens, tokens); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, 1280, DFlash2Config::hidden, ops::LinearPolicy::A16Only, + tokens, tokens); + } + matrix(layout, DType::BF16, DFlash2Config::hidden, drafts * batch); + matrix(layout, DType::BF16, DFlash2Config::hidden, drafts * batch); + matrix(layout, DType::BF16, TextConfig::output_rows, drafts * batch); + matrix(layout, DType::BF16, DFlash2Config::selector_rank, drafts * batch); + (void)ops::linear_workspace_capacity_bytes( + QType::BF16_CTRL, DFlash2Config::selector_rank, DFlash2Config::hidden, + ops::LinearPolicy::A16Only, drafts * batch, drafts * batch); + matrix(layout, DType::I32, batch * DFlash2Config::block_drafts * + DFlash2Config::selector_top_k, + 1); + matrix(layout, DType::FP32, batch * DFlash2Config::block_drafts * + DFlash2Config::selector_top_k, + 1); + matrix(layout, DType::FP32, batch * DFlash2Config::block_drafts * + DFlash2Config::selector_top_k * + DFlash2Config::selector_top_k, + 1); + return finish(layout); + }; + + out.dflash2_context = dflash2_context_capacity(chunk, false); + for (std::int32_t batch = 1; batch <= static_cast(plan.max_concurrency); + ++batch) { + const std::int32_t aggregate = verify * batch; + WorkspaceLayoutBuilder target; + matrix(target, DType::BF16, TextConfig::hidden, aggregate); + target_body(target, aggregate, aggregate, qwen3_6::TextPhase::Verify, + GdnWorkspacePath::ReplayRecord, batch, verify, verify, text_envelope); + const std::size_t accept = + ops::speculative_accept_greedy_drafts_workspace_capacity_bytes( + TextConfig::token_domain, drafts, drafts, batch, batch); + const std::size_t proposal = dflash2_proposal_capacity(verify, batch); + out.dflash2_round = std::max({out.dflash2_round, finish(target), accept, + dflash2_context_capacity(aggregate, true), + proposal}); + } + } + } + out.general_capacity = std::max({out.text_prefill, out.ordinary_round, out.mtp_prefill, out.mtp_round, - out.dflash_context, out.dflash_round, out.causal_score}); + out.dflash_context, out.dflash_round, out.dflash2_context, out.dflash2_round, + out.causal_score}); out.capacity = out.general_capacity; if (plan.features.vision) { const std::uint32_t merged = static_cast( @@ -586,8 +712,14 @@ WorkspacePlan build_workspace_plan(const SequencePlanImpl& plan) { } void validate_target_options(DeviceContext& device, const EngineOptions& options) { - if (options.max_context == 0 || options.max_context > Variant::maximum_context) { - throw std::invalid_argument("max_context exceeds the variant native context capacity"); + // Static YaRN factor-4 extends the rope domain to 4x the native context. + const std::uint32_t context_limit = + options.yarn_enabled ? 4ULL * Variant::maximum_context : Variant::maximum_context; + if (options.max_context == 0 || options.max_context > context_limit) { + throw std::invalid_argument( + options.yarn_enabled + ? "max_context exceeds the variant YaRN-extended context capacity" + : "max_context exceeds the variant native context capacity"); } if (options.prefill_chunk == 0 || options.prefill_chunk % kPrefillChunkAlignment != 0) { throw std::invalid_argument("prefill_chunk must be a nonzero multiple of 128"); @@ -596,7 +728,13 @@ void validate_target_options(DeviceContext& device, const EngineOptions& options throw std::invalid_argument("max_concurrency must be in [1,8]"); } const std::uint32_t logical_pages = page_count(options.max_context); - const std::uint32_t minimum_pages = std::max(logical_pages, options.max_concurrency); + // An explicit --kv-capacity may floor the device page pool below max_context + // (YaRN extends the rope domain beyond the pool; the cold pool recycles pages). + std::uint32_t minimum_pages = std::max(logical_pages, options.max_concurrency); + if (options.kv_capacity.mode == KvCapacityMode::Explicit) { + minimum_pages = std::max(page_count(options.kv_capacity.explicit_tokens), + options.max_concurrency); + } const std::uint64_t maximum_pages64 = static_cast(options.max_concurrency) * logical_pages; if (maximum_pages64 > std::numeric_limits::max()) { @@ -604,8 +742,8 @@ void validate_target_options(DeviceContext& device, const EngineOptions& options } switch (options.kv_capacity.mode) { case KvCapacityMode::Explicit: { - if (options.kv_capacity.explicit_tokens < options.max_context) { - throw std::invalid_argument("kv_capacity must be at least max_context"); + if (options.kv_capacity.explicit_tokens == 0) { + throw std::invalid_argument("kv_capacity must be positive"); } const std::uint32_t requested_pages = page_count(options.kv_capacity.explicit_tokens); if (requested_pages < minimum_pages || requested_pages > maximum_pages64) { @@ -670,11 +808,16 @@ std::unique_ptr build_sequence_candidate(const SequencePlannin impl->proposal_head = inputs.proposal_head; impl->features = inputs.features; impl->use_cuda_graph = inputs.use_cuda_graph; + impl->cold_policy = inputs.cold_policy; + impl->cold_keep_tokens = inputs.cold_keep_tokens; + impl->cold_host_bytes = inputs.cold_host_bytes; impl->causal_scoring = inputs.causal_scoring; + impl->graph_capture_ceiling = inputs.graph_capture_ceiling; impl->device = inputs.device; impl->context_cache = inputs.context_cache; impl->kv_dtype = inputs.kv_dtype; impl->kv_quant_group = inputs.kv_quant_group; + impl->layer_kv_dtypes = inputs.layer_kv_dtypes; impl->persistent = persistent_layout(*impl); impl->workspace = build_workspace_plan(*impl); if (impl->use_cuda_graph) { @@ -733,24 +876,57 @@ make_sequence_planner_impl(DeviceContext& device, const EngineOptions& options, validate_target_options(device, options); const TargetKVCacheProfile kv_profile = target_kv_cache_profile(options.kv_cache); + std::array layer_overrides{}; + const bool has_override = options.kv_layer_storage_explicit; + if (has_override) { + for (std::size_t i = 0; i < layer_overrides.size(); ++i) { + const auto v = options.kv_layer_storage[i]; + layer_overrides[i] = v == KvCacheStorage::BFloat16 + ? DType::BF16 + : (v == KvCacheStorage::Int8Group64 + ? DType::I8 + : (v == KvCacheStorage::Nvfp4Group16 + ? DType::NVFP4 + : (v == KvCacheStorage::Fp8Group16 + ? DType::FP8_E4M3FN + : DType::BF16))); + } + } else if constexpr (Variant::supports_per_layer_kv_defaults) { + layer_overrides = Variant::default_layer_kv_dtypes( + weights_profile); + } SequencePlanningInputs inputs{ .weights_profile = weights_profile, .capacity = options.max_context, + .kv_capacity_tokens = options.kv_capacity.mode == KvCapacityMode::Explicit + ? std::optional(options.kv_capacity.explicit_tokens) + : std::nullopt, .max_concurrency = options.max_concurrency, .prefill_chunk = std::min(options.prefill_chunk, options.max_context), .draft_window = options.speculative.draft_tokens, .speculative_backend = options.speculative.backend, .kv_dtype = kv_profile.dtype, .kv_quant_group = kv_profile.quant_group, + .layer_kv_dtypes = layer_overrides, .proposal_head = options.speculative.proposal_head, .features = qwen3_6::startup_features(options), .use_cuda_graph = options.use_cuda_graph, + .graph_capture_ceiling = options.graph_capture_ceiling, .causal_scoring = options.purpose == EnginePurpose::CausalScoring, + .cold_policy = options.cold_policy, + .cold_keep_tokens = options.cold_keep_tokens, + .cold_host_bytes = options.cold_host_bytes, .device = options.device, .context_cache = options.context_cache, }; const std::uint32_t logical_pages = page_count(inputs.capacity); - const std::uint32_t minimum_pages = std::max(logical_pages, inputs.max_concurrency); + // The device page pool normally covers the full max_context; an explicit + // --kv-capacity below max_context instead floors the pool at that size so + // the rope domain (4x under YaRN) can exceed what the pool can hold. + std::uint32_t minimum_pages = std::max(logical_pages, inputs.max_concurrency); + if (inputs.kv_capacity_tokens) { + minimum_pages = std::max(page_count(*inputs.kv_capacity_tokens), inputs.max_concurrency); + } const std::uint64_t maximum_pages64 = static_cast(inputs.max_concurrency) * logical_pages; if (maximum_pages64 > std::numeric_limits::max()) { diff --git a/src/targets/qwen3_6/impl/runtime/logical_kv_store.h b/src/targets/qwen3_6/impl/runtime/logical_kv_store.h index ba172610f0..685968dd05 100644 --- a/src/targets/qwen3_6/impl/runtime/logical_kv_store.h +++ b/src/targets/qwen3_6/impl/runtime/logical_kv_store.h @@ -760,6 +760,70 @@ class LogicalKVPageStore { release_descriptor(handle, page); } + // Cold-pool transfer: detach the page's device replica (returning the + // physical page to the pool) while keeping the descriptor, so the address + // membership and its block-table slot stay stable. The page's contents + // live in a fixed cold slot; restore_from_cold brings them back on demand. + // An existing host replica is deliberately KEPT: the checkpoint restore + // path (prepare_kv_restores) needs a restorable source, and the host copy + // predates the cold requant, so it doubles as the higher-fidelity backup. + void transfer_to_cold(LogicalKVPageHandle handle, DeviceKVPageReservation& reservation) { + Page& page = require(handle); + if (page.source_pins != 0 || page.destination_pinned || !page.device_replica || + page.cold_compressed || page.references == 0) { + throw std::logic_error("logical KV page is not cold-transferable"); + } + physical_->dematerialize_one(reservation, std::move(*page.device_replica)); + page.device_replica.reset(); + page.cold_compressed = true; + } + + // Like can_dematerialize but for the cold pool: committed history pages + // (writer_references == 0) qualify, host replicas do not block (the + // transfer drops them), and catalogued/fork references are fine because + // the fork path warms cold pages before materializing them. + [[nodiscard]] bool can_cold_transfer(LogicalKVPageHandle handle) const noexcept { + if (!valid(handle)) { return false; } + const Page& page = pages_[handle.index_]; + // Cold-eligible pages are the committed history before the decode + // frontier (cold_frontier); decode never writes them again, so a live + // writer flag (which the paged store keeps until page release) does + // not block the transfer. Pins/fork ties still block, and the page + // must currently hold its device replica to copy from. + return page.references != 0 && page.source_pins == 0 && !page.destination_pinned && + page.device_replica.has_value() && !page.cold_compressed; + } + + [[nodiscard]] bool cold_compressed(LogicalKVPageHandle handle) const noexcept { + return valid(handle) && pages_[handle.index_].cold_compressed; + } + + // Cold-pool restore: allocate a fresh physical page for a cold descriptor + // and hand it back so the caller can repopulate it from the cold slot. + // The descriptor keeps its membership position and reference counts. Any + // host replica is stale after the cold restore (the device copy is now + // authoritative) and is dropped; the offload path re-attaches it later. + [[nodiscard]] DeviceKVPageHandle restore_from_cold(LogicalKVPageHandle handle, + DeviceKVPageReservation& reservation) { + Page& page = require(handle); + if (!page.cold_compressed) { + throw std::logic_error("logical KV page is not cold"); + } + if (reservation.pages() == 0) { + if (!physical_->can_resize_reservation(reservation, 1)) { + throw std::logic_error("Paged KV pool has no capacity for a cold restore"); + } + physical_->resize_reservation(reservation, 1); + } + DeviceKVPageLease lease = physical_->materialize_one(reservation); + DeviceKVPageHandle lease_handle = lease.handle(); + page.device_replica.emplace(std::move(lease)); + page.cold_compressed = false; + page.content_epoch = next_epoch(page.content_epoch); + page.host_replica.reset(); + return lease_handle; + } + [[nodiscard]] bool release(LogicalKVPageHandle handle) noexcept { if (!valid(handle)) { return false; } Page& page = pages_[handle.index_]; @@ -778,6 +842,7 @@ class LogicalKVPageStore { std::uint8_t writer_references = 0; bool destination_pinned = false; bool occupied = false; + bool cold_compressed = false; std::optional device_replica; std::optional pending_device_replica; std::optional host_replica; @@ -1648,6 +1713,47 @@ class KVAddressSpaceStore { return pages_->physical(membership(address, logical_page)); } + // Cold-pool accessors: the window maintenance path detaches retired pages + // into fixed cold slots (transfer_to_cold + sentinel block-table entries) + // and restores them on demand (restore_from_cold + physical page publish). + [[nodiscard]] bool cold_compressed(KVAddressSpaceHandle handle, + std::uint32_t logical_page) const { + const Address& address = require(handle); + if (logical_page >= address.page_count) { + throw std::out_of_range("KV cold page is outside the address space"); + } + return pages_->cold_compressed(membership(address, logical_page)); + } + + // Whether the page may be detached into a cold slot right now: not already + // cold, exclusively referenced by its writer, and free of pins/fork ties. + // Host replicas do not block (transfer_to_cold drops them). + [[nodiscard]] bool can_cold_transfer(KVAddressSpaceHandle handle, + std::uint32_t logical_page) const noexcept { + if (!valid(handle)) { return false; } + const Address& address = addresses_[handle.index_]; + if (logical_page >= address.page_count) { return false; } + const LogicalKVPageHandle& logical = membership(address, logical_page); + return pages_->can_cold_transfer(logical); + } + + void transfer_to_cold(KVAddressSpaceHandle handle, std::uint32_t logical_page) { + Address& address = require_active(handle); + if (logical_page >= address.page_count) { + throw std::out_of_range("KV cold transfer is outside the address space"); + } + pages_->transfer_to_cold(membership(address, logical_page), address.reservation); + } + + [[nodiscard]] DeviceKVPageHandle restore_from_cold(KVAddressSpaceHandle handle, + std::uint32_t logical_page) { + Address& address = require_active(handle); + if (logical_page >= address.page_count) { + throw std::out_of_range("KV cold restore is outside the address space"); + } + return pages_->restore_from_cold(membership(address, logical_page), address.reservation); + } + [[nodiscard]] std::uint64_t content_epoch(KVAddressSpaceHandle handle, std::uint32_t logical_page) const { const Address& address = require(handle); diff --git a/src/targets/qwen3_6/impl/runtime/mtp_impl.h b/src/targets/qwen3_6/impl/runtime/mtp_impl.h index 987e23ef82..93d3233b4e 100644 --- a/src/targets/qwen3_6/impl/runtime/mtp_impl.h +++ b/src/targets/qwen3_6/impl/runtime/mtp_impl.h @@ -39,7 +39,7 @@ void mtp_bridge_and_propose(PrefillContext& state, const Tensor& next_token, rope_position.size_bytes(), cudaMemcpyHostToDevice, state.execution.device.stream)); const auto bridge_visible = static_cast(position + 1); - const ops::CausalAttentionExecutionEnvelope bridge_envelope{bridge_visible, bridge_visible}; + const ops::GqaExecutionEnvelope bridge_envelope{bridge_visible, bridge_visible}; card.mtp_forward_batch(next_token, previous_hidden, position_view, bridge_envelope, mtp_hidden, build_proposal ? 0 : -1, build_proposal ? &logits : nullptr, build_proposal ? &draft0 : nullptr, &rope_position_view, next_embedding); @@ -58,7 +58,7 @@ void mtp_bridge_and_propose(PrefillContext& state, const Tensor& next_token, Tensor next_draft = state.execution.io.mtp->draft_tokens.slice(0, i, 1); Tensor next_hidden = state.execution.prefill_hidden.slice(1, i, 1); const auto visible = static_cast(position + i + 1); - const ops::CausalAttentionExecutionEnvelope envelope{visible, visible}; + const ops::GqaExecutionEnvelope envelope{visible, visible}; card.mtp_forward_ar_step(previous_token, state.execution.io.mtp->ar_hidden, ar_position, envelope, next_hidden, logits, next_draft); CUDA_CHECK(cudaMemcpyAsync(state.execution.io.mtp->ar_hidden.data, next_hidden.data, diff --git a/src/targets/qwen3_6/impl/runtime/program.h b/src/targets/qwen3_6/impl/runtime/program.h index 6c0f8d33a0..00c3d62a53 100644 --- a/src/targets/qwen3_6/impl/runtime/program.h +++ b/src/targets/qwen3_6/impl/runtime/program.h @@ -12,6 +12,7 @@ #include #include "targets/qwen3_6/impl/runtime/layouts.h" +#include "targets/qwen3_6/impl/runtime/schedule.h" #include "targets/qwen3_6/impl/runtime/dflash_context.h" #include "targets/qwen3_6/impl/runtime/host_kv_extent_store.h" #include "targets/qwen3_6/impl/runtime/logical_kv_store.h" @@ -453,6 +454,18 @@ struct SequenceState { std::vector shared_prefix_references; runtime::PrefillWork rebuild_work; std::uint32_t rebuild_tail_begin = 0; + + // Cold-pool bookkeeping: text pages currently detached into raw cold + // slots (logical page -> slot). Released with the sequence or when the + // rewrite path warms the prefix back into physical pages. + struct ColdPageEntry { + std::uint32_t page; + std::int32_t slot; + }; + std::vector cold_pages; + // First logical page not yet offered to the cold pool; compression scans + // forward from here so each round only visits the newly retired pages. + std::uint32_t cold_frontier = 0; }; struct SharedPrefixState { @@ -655,6 +668,7 @@ class ProgramImplCore { std::optional replay_records; std::optional replay_fold; std::optional dflash; + std::optional dflash2; qwen3_6::RoundState io; Tensor prefill_hidden; std::optional score_hidden; @@ -672,6 +686,7 @@ class ProgramImplCore { DecodeGraphFamily ordinary_graphs; DecodeGraphFamily mtp_graphs; DecodeGraphFamily dflash_graphs; + DecodeGraphFamily dflash2_graphs; PinnedHostBuffer round_host; std::optional score_logprobs_host; @@ -685,8 +700,28 @@ class ProgramImplCore { std::optional dflash_host; qwen3_6::DFlashDecodeIngress* dflash_host_ingress = nullptr; qwen3_6::DFlashDecodeEgress* dflash_host_egress = nullptr; + std::optional dflash2_host; + qwen3_6::DFlashDecodeIngress* dflash2_host_ingress = nullptr; + qwen3_6::DFlashDecodeEgress* dflash2_host_egress = nullptr; std::size_t workspace_logical_peak_bytes = 0; + + // Cold-pool maintenance (rev 2b): staging + per-step compress pass. + ColdPolicy cold_policy = ColdPolicy::None; + std::uint32_t cold_keep_tokens = 128; + std::uint64_t cold_host_bytes = 4ULL << 30; + void* cold_requant_codes = nullptr; + void* cold_requant_scales = nullptr; + std::uint32_t cold_requant_heads = 0; + void enqueue_cold_compressions(SequenceState& sequence); + void warm_cold_prefix(SequenceState& sequence, std::uint32_t end_page); + void restore_cold_page(SequenceState& sequence, std::uint32_t page, std::int32_t slot, + const DeviceKVPageHandle& physical); + + // On-demand graph capture state (see DecodeGraphFamily comment). + std::uint32_t graph_capture_ceiling = 0; + void extend_ordinary_graphs(std::uint32_t batch_size, std::uint32_t frontier); + schedule::ExecutionCore make_execution_core(); std::size_t vision_handoff_peak_bytes = 0; private: @@ -1162,6 +1197,10 @@ class ProgramImplCore { decode_dflash_batch(std::span lanes, std::span budgets, runtime::ExecutionTiming* failed_timing); + [[nodiscard]] runtime::BatchedGeneratedRound + decode_dflash2_batch(std::span lanes, + std::span budgets, + runtime::ExecutionTiming* failed_timing); void resize_sequence_kv_entitlement(SequenceState& sequence, std::uint32_t text_pages, std::uint32_t backend_pages); void bind_sequence_kv(SequenceState& sequence); diff --git a/src/targets/qwen3_6/impl/runtime/program_impl.h b/src/targets/qwen3_6/impl/runtime/program_impl.h index 36f04c8bdb..8ac23df596 100644 --- a/src/targets/qwen3_6/impl/runtime/program_impl.h +++ b/src/targets/qwen3_6/impl/runtime/program_impl.h @@ -1,9 +1,13 @@ #include "targets/qwen3_6/impl/runtime/instance.h" #include "targets/qwen3_6/impl/runtime/program.h" +#include "ninfer/ops/cold_i8.h" +#include "ninfer/ops/entropy_cold_requant.h" +#include "ninfer/ops/entropy_nvfp4_slot.h" #include "targets/qwen3_6/impl/runtime/rebuild_work.h" #include "core/nvtx.h" #include "targets/qwen3_6/impl/runtime/schedule.h" +#include "targets/qwen3_6/impl/runtime/spec_decision.h" #include "ninfer/ops/gdn_replay.h" #include "ninfer/ops/linear.h" #include "ninfer/ops/prepare_ragged_prefix.h" @@ -19,6 +23,7 @@ #include #include #include +#include #include #include #include @@ -603,6 +608,15 @@ schedule::DFlashEnvelopes dflash_envelopes(std::uint32_t min_frontier, std::uint }; } +schedule::DFlash2Envelopes dflash2_envelopes(std::uint32_t min_frontier, + std::uint32_t max_frontier, std::uint32_t k) { + (void)min_frontier; + return schedule::DFlash2Envelopes{ + .local = {0, max_frontier}, + .append = {0, k + 1}, + }; +} + DecodeGraphProfile& select_graph_profile(DecodeGraphFamily& family, std::uint32_t batch_size, std::uint32_t frontier, const char* label) { const auto it = std::find_if( @@ -616,6 +630,19 @@ DecodeGraphProfile& select_graph_profile(DecodeGraphFamily& family, std::uint32_ return *it; } +// True when no profile of this batch covers the frontier — the caller's cue +// to extend the family on demand (on-demand graph capture). +[[nodiscard]] inline bool graph_profile_missing(const DecodeGraphFamily& family, + std::uint32_t batch_size, + std::uint32_t frontier) noexcept { + return std::none_of(family.profiles.begin(), family.profiles.end(), + [&](const DecodeGraphProfile& profile) { + return profile.batch_size == batch_size && + profile.min_execution_frontier <= frontier && + frontier <= profile.max_execution_frontier; + }); +} + void validate_graph_profiles(const std::vector& profiles, std::uint32_t max_frontier, const char* label) { if (profiles.empty() || profiles.front().min != 0 || profiles.back().max != max_frontier) { @@ -727,6 +754,9 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence speculative_backend(plan.speculative_backend), kv_dtype(plan.kv_dtype), kv_quant_group(plan.kv_quant_group), proposal_head(plan.proposal_head), vision_enabled(plan.features.vision), use_cuda_graph(plan.use_cuda_graph), + cold_policy(plan.cold_policy), cold_keep_tokens(plan.cold_keep_tokens), + cold_host_bytes(plan.cold_host_bytes), + graph_capture_ceiling(plan.graph_capture_ceiling), causal_scoring(plan.causal_scoring), kv_payload_bytes(plan.persistent.kv_payload_bytes), graph_allowance_bytes(plan.graph_allowance_bytes), workspace_plan(plan.workspace), persistent(plan.persistent.bytes), workspace_storage(plan.workspace.capacity), @@ -750,6 +780,10 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence ? std::make_optional(sizeof(qwen3_6::DFlashDecodeIngress) + sizeof(qwen3_6::DFlashDecodeEgress)) : std::nullopt), + dflash2_host(plan.speculative_backend == SpeculativeBackend::DFlash2 + ? std::make_optional(sizeof(qwen3_6::DFlashDecodeIngress) + + sizeof(qwen3_6::DFlashDecodeEgress)) + : std::nullopt), context_source_ready_(device_in), context_completion_(device_in), context_transfer_timers_{CudaEventTimer(device_in, device_in.transfer_stream), CudaEventTimer(device_in, device_in.transfer_stream), @@ -759,15 +793,19 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence } if (model.features != plan.features || model.mtp.has_value() != plan.features.mtp() || model.dflash.has_value() != plan.features.dflash() || + model.dflash2.has_value() != plan.features.dflash2() || model.optimized_proposal.has_value() != plan.features.optimized_proposal() || model.vision.has_value() != plan.features.vision) { throw std::invalid_argument( "Qwen3.6 loaded weights do not match the frozen startup features"); } - if (model.mtp.has_value() && model.dflash.has_value()) { + if (model.mtp.has_value() && (model.dflash.has_value() || model.dflash2.has_value())) { throw std::invalid_argument("MTP and DFlash model views are mutually exclusive"); } - if (model.dflash.has_value() && model.vision.has_value()) { + if (model.dflash.has_value() && model.dflash2.has_value()) { + throw std::invalid_argument("DFlash and DFlash2 model views are mutually exclusive"); + } + if ((model.dflash.has_value() || model.dflash2.has_value()) && model.vision.has_value()) { throw std::invalid_argument("DFlash and Vision model views are mutually exclusive"); } if (workspace_plan.general_capacity == 0 || @@ -808,6 +846,16 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence }; decoder = std::make_unique(backing, plan.persistent.decoder); + if (cold_policy == ColdPolicy::Window) { + const std::int32_t requant_heads = decoder->text_kv.batch_layer_view(0).num_kv_heads; + if (requant_heads > 0) { + CUDA_CHECK(cudaMalloc(&cold_requant_codes, + 8192ULL * static_cast(requant_heads))); + CUDA_CHECK(cudaMalloc(&cold_requant_scales, + 1024ULL * static_cast(requant_heads))); + cold_requant_heads = static_cast(requant_heads); + } + } text_host_kv_page_stride = plan_host_kv_page_layout(decoder->text_kv.page_pool().geometry()).page_stride; text_kv_pages = std::make_unique( @@ -847,6 +895,10 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence if (dflash.has_value() != plan.features.dflash()) { throw std::logic_error("DFlash state does not match the frozen sequence plan"); } + if (plan.persistent.dflash2) { dflash2.emplace(backing, *plan.persistent.dflash2); } + if (dflash2.has_value() != plan.features.dflash2()) { + throw std::logic_error("DFlash2 state does not match the frozen sequence plan"); + } if (qwen3_6::PagedKVCache* backend = backend_kv_cache()) { backend_host_kv_page_stride = plan_host_kv_page_layout(backend->page_pool().geometry()).page_stride; @@ -892,10 +944,14 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence if (io.ordinary.has_value() != (speculative_backend == SpeculativeBackend::None)) { throw std::logic_error("ordinary decode frame does not match the sequence plan"); } - if (io.dflash_prefill.has_value() != (speculative_backend == SpeculativeBackend::DFlash)) { + if (io.dflash_prefill.has_value() != + (speculative_backend == SpeculativeBackend::DFlash || + speculative_backend == SpeculativeBackend::DFlash2)) { throw std::logic_error("DFlash prefill scratch does not match the sequence plan"); } - if (io.dflash_decode.has_value() != (speculative_backend == SpeculativeBackend::DFlash)) { + if (io.dflash_decode.has_value() != + (speculative_backend == SpeculativeBackend::DFlash || + speculative_backend == SpeculativeBackend::DFlash2)) { throw std::logic_error("DFlash decode frame does not match the sequence plan"); } prefill_hidden = plan.persistent.prefill_hidden.bind(backing); @@ -946,6 +1002,14 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence *dflash_host_ingress = {}; *dflash_host_egress = {}; } + if (dflash2_host) { + dflash2_host_ingress = static_cast(dflash2_host->data()); + dflash2_host_egress = reinterpret_cast( + static_cast(dflash2_host->data()) + + sizeof(qwen3_6::DFlashDecodeIngress)); + *dflash2_host_ingress = {}; + *dflash2_host_egress = {}; + } if (io.dflash_prefill) { CUDA_CHECK(cudaMemsetAsync(io.dflash_prefill->produced_count.data, 0, io.dflash_prefill->produced_count.bytes(), device.stream)); @@ -967,6 +1031,12 @@ ProgramImplCore::ProgramImplCore(const LoadedModelData& model_in, const Sequence ProgramImplCore::~ProgramImplCore() noexcept { if (device.transfer_stream != nullptr) { (void)cudaStreamSynchronize(device.transfer_stream); } if (device.stream != nullptr) { (void)cudaStreamSynchronize(device.stream); } + if (cold_requant_codes != nullptr) { + (void)cudaFree(cold_requant_codes); + (void)cudaFree(cold_requant_scales); + cold_requant_codes = nullptr; + cold_requant_scales = nullptr; + } } std::vector ProgramImplCore::causal_score(PreparedPromptData&& prompt, @@ -1067,6 +1137,7 @@ std::vector ProgramImplCore::causal_score(PreparedPromptData&& prompt, decoder->text_kv, nullptr, nullptr, + nullptr, cursor, nullptr, nullptr, @@ -1871,6 +1942,9 @@ ProgramImplCore::checkpoint_restore_requirements(const SequenceKVBundle& kv, for (std::uint32_t page = 0; page < required; ++page) { const LogicalKVPageHandle logical = addresses.logical_page(address, page); if (pages.device_resident(logical)) { continue; } + // Cold-pool pages restore in place from their raw slots; they + // need no host transfer. + if (pages.cold_compressed(logical)) { continue; } if (!pages.host_resident(logical)) { throw std::logic_error("checkpoint KV page has no restorable replica"); } @@ -4724,6 +4798,25 @@ void ProgramImplCore::prepare_materialization(MaterializationTransaction& transa for (std::uint32_t page = 0; page < mapped; ++page) { const LogicalKVPageHandle logical = addresses.logical_page(address, page); if (pages.device_resident(logical)) { continue; } + if (pages.cold_compressed(logical)) { + // Cold-pool page: its restorable source is the raw cold + // slot, not a host replica. Restore synchronously here + // (same reservation); the activation publish + // (publish_membership) republishes the physical mapping. + if (source_state == nullptr) { + throw std::logic_error("cold checkpoint page has no source bookkeeping"); + } + auto entry = std::find_if( + source_state->cold_pages.begin(), source_state->cold_pages.end(), + [page](const SequenceState::ColdPageEntry& e) { return e.page == page; }); + if (entry == source_state->cold_pages.end()) { + throw std::logic_error("cold checkpoint page has no slot record"); + } + const DeviceKVPageHandle restored = + pages.restore_from_cold(logical, reservation); + restore_cold_page(*source_state, page, entry->slot, restored); + continue; + } if (!pages.host_resident(logical) || !host_kv_extents) { throw std::logic_error("checkpoint KV page has no restorable replica"); } @@ -6550,7 +6643,11 @@ std::uint32_t ProgramImplCore::device_kv_prefix_pages(const KVAddressSpaceStore& (&addresses == text_kv_addresses.get()) ? *text_kv_pages : *backend_kv_pages; std::uint32_t resident = 0; for (std::uint32_t page = 0; page < required; ++page) { - if (pages.device_resident(addresses.logical_page(address, page))) { ++resident; } + // Cold-pool pages restore in place from their raw slots, so they + // count as resident for the restore inventory (missing_kv_restore + // skips them the same way). + const LogicalKVPageHandle logical = addresses.logical_page(address, page); + if (pages.device_resident(logical) || pages.cold_compressed(logical)) { ++resident; } } return resident; } @@ -6587,7 +6684,10 @@ std::uint32_t ProgramImplCore::shared_device_kv_prefix_pages(const KVAddressSpac std::uint32_t resident = 0; for (std::uint32_t page = 0; page < required; ++page) { const LogicalKVPageHandle logical = addresses.logical_page(address, page); - if (pages.address_references(logical) > 1 && pages.device_resident(logical)) { ++resident; } + if (pages.address_references(logical) > 1 && + (pages.device_resident(logical) || pages.cold_compressed(logical))) { + ++resident; + } } return resident; } @@ -6621,7 +6721,10 @@ ProgramImplCore::missing_shared_device_kv_prefix_pages(const KVAddressSpaceStore std::uint32_t missing = 0; for (std::uint32_t page = 0; page < required; ++page) { const LogicalKVPageHandle logical = addresses.logical_page(address, page); - if (pages.address_references(logical) > 1 && !pages.device_resident(logical)) { ++missing; } + if (pages.address_references(logical) > 1 && !pages.device_resident(logical) && + !pages.cold_compressed(logical)) { + ++missing; + } } return missing; } @@ -8494,17 +8597,20 @@ runtime::ExecutionTiming ProgramImplCore::append_forced_tokens( decoder->text_kv, decoder->mtp_cache(), dflash ? &*dflash : nullptr, + dflash2 ? &*dflash2 : nullptr, cursor, nullptr, nullptr, selectors.source, selectors.destination, 0, - dflash_host_ingress}; + dflash_host_ingress, + dflash2_host_ingress}; mark_workspace_usage(speculative_backend == SpeculativeBackend::Mtp ? workspace_plan.mtp_prefill : workspace_plan.text_prefill); - if (speculative_backend == SpeculativeBackend::DFlash) { + if (speculative_backend == SpeculativeBackend::DFlash || + speculative_backend == SpeculativeBackend::DFlash2) { mark_workspace_usage(workspace_plan.dflash_context); } const schedule::PrefillChunkResult result = schedule::prefill_text_chunk( @@ -9055,6 +9161,15 @@ void ProgramImplCore::start_sequence(std::uint32_t lane, SequenceState& sequence transaction.shared_source_index < shared_prefix_capacity && shared_prefix_slots[transaction.shared_source_index].role == SharedPrefixSlotRole::Catalogued; + // A retained source may carry cold-pool pages: warm them back into + // physical pages before any prefix fork touches the membership. + if (private_source_ready && cold_policy == ColdPolicy::Window) { + SequenceState& source = continuation_states[transaction.source_index]; + if (source.kv && source.kv->text.valid() && + text_kv_addresses->active(source.kv->text)) { + warm_cold_prefix(source, source.text_kv_valid); + } + } if (private_source_ready == shared_source_ready || transaction.reserved_state_count != state_slots || state_slots == 0 || !transaction.root_text_address || !transaction.text_prefix_fork || @@ -9430,7 +9545,8 @@ void ProgramImplCore::start_sequence(std::uint32_t lane, SequenceState& sequence ? std::min(capacity, prompt_tokens + (initial_mtp_extent == 0 ? 0U : initial_mtp_extent - 1U)) : speculative_backend == SpeculativeBackend::DFlash ? prompt_tokens - : 0U; + : speculative_backend == SpeculativeBackend::DFlash2 ? 0U + : 0U; materialize_sequence_kv(sequence, prompt_tokens, backend_materialized); install_sampling(sequence, request, request_plan.sampling); sequence.rope_delta = staged.prompt.rope_delta; @@ -9461,6 +9577,16 @@ void ProgramImplCore::start_sequence(std::uint32_t lane, SequenceState& sequence CUDA_CHECK(cudaMemcpyAsync(io.dflash_decode->ingress.data, dflash_host_ingress, sizeof(qwen3_6::DFlashDecodeIngress), cudaMemcpyHostToDevice, device.stream)); + } else if (speculative_backend == SpeculativeBackend::DFlash2) { + if (!dflash2 || !io.dflash_decode) { + throw std::logic_error("DFlash2 prefill state is incomplete"); + } + *dflash2_host_ingress = {}; + dflash2_host_ingress->active_lanes[0] = static_cast(sequence.lane); + dflash2_host_ingress->dflash_kv_table_rows[0] = 0; + CUDA_CHECK(cudaMemcpyAsync(io.dflash_decode->ingress.data, dflash2_host_ingress, + sizeof(qwen3_6::DFlashDecodeIngress), cudaMemcpyHostToDevice, + device.stream)); } staged.elapsed_seconds += std::chrono::duration(Clock::now() - started).count(); @@ -9592,7 +9718,7 @@ runtime::ExecutionTiming ProgramImplCore::resolve_pending_raw( timing.resume_submit(); replay_fold->execute(std::span(fold_rows.data(), lanes.size()), device.stream); - + if (needs_hidden_correction) { const auto batch = static_cast(lanes.size()); Tensor selector_tensor; @@ -9605,7 +9731,9 @@ runtime::ExecutionTiming ProgramImplCore::resolve_pending_raw( hidden = frame.target_hidden.slice(2, 0, batch); selected = frame.target_continuation_hidden.slice(1, 0, batch); destinations = frame.state_destination_slots.slice(0, 0, batch); - } else if (speculative_backend == SpeculativeBackend::DFlash && io.dflash_decode) { + } else if ((speculative_backend == SpeculativeBackend::DFlash || + speculative_backend == SpeculativeBackend::DFlash2) && + io.dflash_decode) { qwen3_6::DFlashDecodeState& frame = *io.dflash_decode; selector_tensor = frame.proposal_extents.slice(0, 0, batch); hidden = frame.target_hidden.slice(2, 0, batch); @@ -9623,7 +9751,7 @@ runtime::ExecutionTiming ProgramImplCore::resolve_pending_raw( device.stream); } - if (speculative_backend == SpeculativeBackend::DFlash) { + if (speculative_backend == SpeculativeBackend::DFlash) { std::array append_lanes{}; std::array append_starts{}; std::array append_counts{}; @@ -9644,7 +9772,7 @@ runtime::ExecutionTiming ProgramImplCore::resolve_pending_raw( } } - timing.begin_wait(); + timing.begin_wait(); device.synchronize(); timing.end_wait(); work.reset(); @@ -9674,7 +9802,9 @@ runtime::ExecutionTiming ProgramImplCore::resolve_pending_raw( const TokenId* token_base = speculative_backend == SpeculativeBackend::Mtp ? mtp_host_egress->licensed_tokens.data() + row * width - : dflash_host_egress->licensed_tokens.data() + row * width; + : speculative_backend == SpeculativeBackend::DFlash2 + ? dflash2_host_egress->licensed_tokens.data() + row * width + : dflash_host_egress->licensed_tokens.data() + row * width; sequence.ledger.insert(sequence.ledger.end(), token_base, token_base + committed); sequence.prefix_identity.append_generated(committed, sequence.rope_delta); sequence.prefix_digests.append_generated( @@ -10097,6 +10227,12 @@ void ProgramImplCore::release_sequence_kv(SequenceState& sequence) noexcept { } if (text_kv_addresses) { (void)text_kv_addresses->release(sequence.kv->text); } sequence.kv.reset(); + if (decoder != nullptr) { + for (const auto& cold : sequence.cold_pages) { + decoder->text_kv.release_cold_slot(cold.slot); + } + } + sequence.cold_pages.clear(); if (host_kv_extents) { (void)host_kv_extents->release_unreferenced(); } } @@ -10142,6 +10278,262 @@ void ProgramImplCore::ordered_reset(SequenceState& sequence) { sequence.dflash_context_frontier = 0; } + +// Cold-pool maintenance: pack the retired prefix of a sequence's text KV into +// raw entropy slots and detach those pages (sentinel entries in the block +// table; physical pages return to the pool). Runs when the window policy is +// active and at least cold_keep_tokens lie behind the decode frontier. The +// decode kernels read cold pages straight from the slots, so no restore is +// needed on the steady-state path. +void ProgramImplCore::enqueue_cold_compressions(SequenceState& sequence) { + if (cold_policy != ColdPolicy::Window || !sequence.kv || decoder == nullptr || + !sequence.kv->text.valid() || cold_requant_codes == nullptr) { + return; + } + KVAddressSpaceStore& store = *text_kv_addresses; + KVAddressSpaceHandle text = sequence.kv->text; + const std::uint32_t total_pages = store.mapped_pages(text); + if (total_pages == 0) { return; } + + const std::uint32_t cold_pages = + sequence.text_kv_valid > cold_keep_tokens + ? (sequence.text_kv_valid - cold_keep_tokens) / kPagedKVPageSize + : 0; + const std::uint32_t limit = cold_pages < total_pages ? cold_pages : total_pages; + if (limit == 0) { return; } + + const std::int32_t kv_heads = decoder->text_kv.batch_layer_view(0).num_kv_heads; + const std::uint32_t layers = decoder->text_kv.layers(); + // Per-layer cold packing: INT8 planes are requantized to E2M1 g64 and + // stored in raw 9232 B slots; NVFP4 planes keep their native E2M1/ISO3 + // nibbles and are rANS-encoded into entropy slots. The 9536 B slot buffer + // covers both formats, so mixed-dtype stacks pack per layer instead of + // skipping the whole cold transfer. + std::vector k_flags(static_cast(kv_heads)); + std::vector v_flags(static_cast(kv_heads)); + std::uint32_t compressed = 0; + + for (std::uint32_t page = sequence.cold_frontier; page < limit; ++page) { + if (!store.can_cold_transfer(text, page)) { continue; } + const std::int32_t slot = decoder->text_kv.allocate_cold_slot(); + if (slot < 0) { break; } // cold pool exhausted: keep the rest hot. + + bool success = true; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + const PagedKVBatchLayerView view = decoder->text_kv.batch_layer_view(layer); + const Tensor cold_slots = view.cold_slots; + if (cold_slots.data == nullptr) { continue; } + const DeviceKVPageHandle ph = store.physical_page(text, page); + const std::int32_t physical = ph.index(); + auto* k_codes = static_cast(view.k_pages.data) + + physical * view.k_pages.nb[3]; + auto* v_codes = static_cast(view.v_pages.data) + + physical * view.v_pages.nb[3]; + auto* k_scales = static_cast(view.k_scale_pages.data) + + physical * view.k_scale_pages.nb[3]; + auto* v_scales = static_cast(view.v_scale_pages.data) + + physical * view.v_scale_pages.nb[3]; + auto* k_slot = static_cast(cold_slots.data) + + static_cast(slot) * cold_slots.nb[3]; + auto* v_slot = k_slot + cold_slots.nb[2]; + auto* k_valid = static_cast(view.cold_slot_valid.data) + + static_cast(slot) * view.cold_slot_valid.nb[2]; + auto* v_valid = reinterpret_cast( + reinterpret_cast(k_valid) + view.cold_slot_valid.nb[1]); + if (view.dtype == DType::I8) { + // INT8 tier: requant to E2M1 g64 and store the raw nibble slot. + ops::entropy_cold_requant_raw( + k_codes, k_scales, ops::EntropyColdRequantMode::Int8G64, kv_heads, 1, + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), device.stream); + ops::cold_i8_slot_pack_raw( + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), kv_heads, 1, k_slot, + k_valid, view.slot_bytes, device.stream); + ops::entropy_cold_requant_raw( + v_codes, v_scales, ops::EntropyColdRequantMode::Int8G64, kv_heads, 1, + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), device.stream); + ops::cold_i8_slot_pack_raw( + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), kv_heads, 1, v_slot, + v_valid, view.slot_bytes, device.stream); + } else if (view.dtype == DType::NVFP4) { + // NVFP4 tier: keep the native E2M1 (K) / ISO3 (V) nibbles, + // requantize scales to g64 for a skew that rANS compresses, + // and encode into the entropy slot. On incompressible pages + // the slot flags stay 0 and the hot plane keeps serving. + ops::entropy_cold_requant_raw( + k_codes, k_scales, ops::EntropyColdRequantMode::Nvfp4G16, kv_heads, 1, + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), device.stream); + ops::entropy_nvfp4_slot_encode_raw( + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), kv_heads, 1, k_slot, + view.slot_bytes, k_valid, nullptr, kv_heads, device.stream); + ops::entropy_cold_requant_raw( + v_codes, v_scales, ops::EntropyColdRequantMode::Iso3VG16, kv_heads, 1, + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), device.stream); + ops::entropy_nvfp4_slot_encode_raw( + static_cast(cold_requant_codes), + static_cast(cold_requant_scales), kv_heads, 1, v_slot, + view.slot_bytes, v_valid, nullptr, kv_heads, device.stream); + } else { + success = false; // bf16/fp8 layers have no cold slot codec + break; + } + } + if (!success) { + decoder->text_kv.release_cold_slot(slot); + continue; + } + device.synchronize(); + + // A slot only counts once every head's pack kernel committed its valid + // flag; otherwise the page would decode as garbage through the slot. + // Valid tensor is [kv_heads, 2, pages] col-major: head innermost, + // page outermost, so slot pages sit at slot * 2*kv_heads. + const Tensor cold_valid = decoder->text_kv.cold_slot_valid(0); + auto* k_valid = static_cast(cold_valid.data) + + static_cast(slot) * cold_valid.nb[2]; + auto* v_valid = reinterpret_cast( + reinterpret_cast(k_valid) + cold_valid.nb[1]); + CUDA_CHECK(cudaMemcpy(k_flags.data(), k_valid, + k_flags.size() * sizeof(std::int32_t), cudaMemcpyDeviceToHost)); + CUDA_CHECK(cudaMemcpy(v_flags.data(), v_valid, + v_flags.size() * sizeof(std::int32_t), cudaMemcpyDeviceToHost)); + const bool valid = + std::all_of(k_flags.begin(), k_flags.end(), + [](std::int32_t value) { return value != 0; }) && + std::all_of(v_flags.begin(), v_flags.end(), + [](std::int32_t value) { return value != 0; }); + if (!valid) { + decoder->text_kv.release_cold_slot(slot); + continue; + } + + // Publish the sentinel and return the physical page to the pool. + const std::int32_t entry = paged_kv_cold_entry(slot); + decoder->text_kv.execution_tables().publish_indices( + store.execution_row(text).handle(), page, std::span(&entry, 1), + device.stream); + store.transfer_to_cold(text, page); + sequence.cold_pages.emplace_back(page, slot); + sequence.cold_frontier = page + 1; + ++compressed; + } + if (compressed != 0) { + device.synchronize(); + std::fprintf(stderr, "[cold] compressed %u prefix pages (kept %u+)\n", compressed, + cold_keep_tokens); + } +} + +// Restore one cold page's data from its raw slot into the physical page and +// release the slot. Shared by the rewrite warm path and the checkpoint +// restore path (which must repopulate cold pages without a host replica). +void ProgramImplCore::restore_cold_page(SequenceState& sequence, std::uint32_t page, + std::int32_t slot, const DeviceKVPageHandle& physical) { + const int kv_heads = decoder->text_kv.batch_layer_view(0).num_kv_heads; + const std::uint32_t layers = decoder->text_kv.layers(); + const std::int32_t ph_index = physical.index(); + for (std::uint32_t layer = 0; layer < layers; ++layer) { + const PagedKVBatchLayerView view = decoder->text_kv.batch_layer_view(layer); + const Tensor cold_slots = view.cold_slots; + if (cold_slots.data == nullptr) { continue; } + if (view.dtype == DType::NVFP4) { + // NVFP4 tier: rANS-decode the slot back into the native E2M1/ISO3 + // code plane, then scatter the slot's scale tail into the plane. + auto* k_slot_base = static_cast(cold_slots.data); + auto* v_slot_base = k_slot_base + cold_slots.nb[2]; + auto* k_codes_nv = static_cast(view.k_pages.data) + + static_cast(ph_index) * view.k_pages.nb[3]; + auto* v_codes_nv = static_cast(view.v_pages.data) + + static_cast(ph_index) * view.v_pages.nb[3]; + auto* k_scales_nv = static_cast(view.k_scale_pages.data) + + static_cast(ph_index) * view.k_scale_pages.nb[3]; + auto* v_scales_nv = static_cast(view.v_scale_pages.data) + + static_cast(ph_index) * view.v_scale_pages.nb[3]; + ops::entropy_nvfp4_slot_restore_plane_raw( + k_slot_base + slot * cold_slots.nb[3], view.slot_bytes, 0, kv_heads, + static_cast(cold_requant_codes), k_codes_nv, device.stream); + ops::entropy_nvfp4_slot_restore_plane_raw( + v_slot_base + slot * cold_slots.nb[3], view.slot_bytes, 0, kv_heads, + static_cast(cold_requant_codes), v_codes_nv, device.stream); + std::int32_t page_ids[1] = {ph_index}; + ops::entropy_nvfp4_slot_scales_scatter_raw( + k_slot_base + slot * cold_slots.nb[3], view.slot_bytes, + static_cast(cold_slots.nb[3]), kv_heads, 1, page_ids, + static_cast(view.k_scale_pages.nb[3]), k_scales_nv, device.stream); + ops::entropy_nvfp4_slot_scales_scatter_raw( + v_slot_base + slot * cold_slots.nb[3], static_cast(cold_slots.nb[3]), + static_cast(cold_slots.nb[3]) * kv_heads, kv_heads, 1, page_ids, + static_cast(view.v_scale_pages.nb[3]), v_scales_nv, device.stream); + } else if (view.dtype == DType::I8) { + auto* k_slot_base = static_cast(cold_slots.data); + auto* v_slot_base = k_slot_base + cold_slots.nb[2]; + auto* k_codes_i8 = static_cast(view.k_pages.data) + + static_cast(ph_index) * view.k_pages.nb[3]; + auto* v_codes_i8 = static_cast(view.v_pages.data) + + static_cast(ph_index) * view.v_pages.nb[3]; + auto* k_scales_h = static_cast( + static_cast(view.k_scale_pages.data) + + static_cast(ph_index) * view.k_scale_pages.nb[3]); + auto* v_scales_h = static_cast( + static_cast(view.v_scale_pages.data) + + static_cast(ph_index) * view.v_scale_pages.nb[3]); + ops::cold_i8_slot_restore_raw(k_slot_base + slot * cold_slots.nb[3], kv_heads, 1, + k_codes_i8, k_scales_h, view.slot_bytes, device.stream); + ops::cold_i8_slot_restore_raw(v_slot_base + slot * cold_slots.nb[3], kv_heads, 1, + v_codes_i8, v_scales_h, view.slot_bytes, device.stream); + } + } + decoder->text_kv.release_cold_slot(slot); + auto entry = std::find_if(sequence.cold_pages.begin(), sequence.cold_pages.end(), + [page](const SequenceState::ColdPageEntry& e) { + return e.page == page; + }); + if (entry != sequence.cold_pages.end()) { sequence.cold_pages.erase(entry); } + sequence.cold_frontier = 0; // pages are hot again; rescan from the front +} + +// Warm-restore the cold prefix of a sequence (rewrite/resume paths only): the +// steady-state decode path reads cold pages directly from their slots, but a +// rewrite needs real physical pages so append/fork can mutate them again. +void ProgramImplCore::warm_cold_prefix(SequenceState& sequence, std::uint32_t end_page) { + if (cold_policy != ColdPolicy::Window || !sequence.kv || decoder == nullptr || + cold_requant_codes == nullptr || sequence.cold_pages.empty()) { + return; + } + KVAddressSpaceStore& store = *text_kv_addresses; + KVAddressSpaceHandle text = sequence.kv->text; + const std::uint32_t mapped = store.mapped_pages(text); + const std::uint32_t pages = std::min(end_page, mapped); + if (pages == 0) { return; } + + std::uint32_t restored = 0; + for (std::uint32_t page = 0; page < pages; ++page) { + if (!store.cold_compressed(text, page)) { continue; } + auto entry = std::find_if(sequence.cold_pages.begin(), sequence.cold_pages.end(), + [page](const SequenceState::ColdPageEntry& e) { + return e.page == page; + }); + if (entry == sequence.cold_pages.end()) { continue; } + const DeviceKVPageHandle physical = store.restore_from_cold(text, page); + const std::int32_t ph_index = physical.index(); + restore_cold_page(sequence, page, entry->slot, physical); + decoder->text_kv.execution_tables().publish_indices( + store.execution_row(text).handle(), page, + std::span(&ph_index, 1), device.stream); + ++restored; + } + if (restored != 0) { + device.synchronize(); + std::fprintf(stderr, "[cold] restored %u prefix pages\n", restored); + } +} + void ProgramImplCore::prepare_graphs() { if (!use_cuda_graph) { return; } nvtx::ScopedRange prepare_range(nvtx::Name::CudaGraphPrepare, nvtx::Category::Graph); @@ -10247,6 +10639,11 @@ void ProgramImplCore::prepare_graphs() { dflash->pending_features.slice(2, static_cast(row), 1); CUDA_CHECK(cudaMemsetAsync(pending.data, 0, pending.bytes(), device.stream)); } + if (dflash2) { + const Tensor pending = + dflash2->pending_features.slice(2, static_cast(row), 1); + CUDA_CHECK(cudaMemsetAsync(pending.data, 0, pending.bytes(), device.stream)); + } } set_device_i32(io.pos, checked_i32(frontier, "graph representative position")); set_device_i32(io.rope_pos, checked_i32(frontier, "graph representative rope position")); @@ -10255,24 +10652,33 @@ void ProgramImplCore::prepare_graphs() { checked_i32(frontier, "graph representative MTP position")); } if (io.dflash_decode) { - *dflash_host_ingress = {}; - *dflash_host_egress = {}; + // DFlash and DFlash2 share the decode frame but keep separate + // host ingress/egress buffers; only the active backend's is + // allocated (dflash_host_* vs dflash2_host_*). + qwen3_6::DFlashDecodeIngress* ingress = + speculative_backend == SpeculativeBackend::DFlash2 ? dflash2_host_ingress + : dflash_host_ingress; + qwen3_6::DFlashDecodeEgress* egress = + speculative_backend == SpeculativeBackend::DFlash2 ? dflash2_host_egress + : dflash_host_egress; + *ingress = {}; + *egress = {}; const std::uint32_t extent = std::min(draft_window, capacity - frontier - 1U); for (std::uint32_t row = 0; row < batch_size; ++row) { - dflash_host_ingress->anchors[row] = 0; - dflash_host_ingress->execution_frontiers[row] = + ingress->anchors[row] = 0; + ingress->execution_frontiers[row] = checked_i32(frontier, "graph representative DFlash frontier"); - dflash_host_ingress->context_frontiers[row] = + ingress->context_frontiers[row] = checked_i32(frontier, "graph representative DFlash context frontier"); - dflash_host_ingress->proposal_extents[row] = static_cast(extent); - dflash_host_ingress->target_valid_columns[row] = + ingress->proposal_extents[row] = static_cast(extent); + ingress->target_valid_columns[row] = static_cast(extent + 1U); - dflash_host_ingress->text_kv_table_rows[row] = static_cast(row); - dflash_host_ingress->dflash_kv_table_rows[row] = static_cast(row); - dflash_host_ingress->active_lanes[row] = static_cast(row); - dflash_host_ingress->state_source_slots[row] = capture_state_slot(row); - dflash_host_ingress->state_destination_slots[row] = capture_state_slot(row); - dflash_host_ingress->sampling[row] = {}; + ingress->text_kv_table_rows[row] = static_cast(row); + ingress->dflash_kv_table_rows[row] = static_cast(row); + ingress->active_lanes[row] = static_cast(row); + ingress->state_source_slots[row] = capture_state_slot(row); + ingress->state_destination_slots[row] = capture_state_slot(row); + ingress->sampling[row] = {}; } } if (io.mtp_decode) { @@ -10337,6 +10743,18 @@ void ProgramImplCore::prepare_graphs() { const auto ordinary_profiles = ordinary_graph_profiles(capacity); validate_graph_profiles(ordinary_profiles, capacity - 1, "ordinary"); const std::uint32_t ordinary_batch_limit = max_concurrency; + // On-demand capture: with a positive ceiling, startup captures only + // the segments fully below it; decode extends on growth crossings and + // full coverage is revalidated after each extension. + std::vector startup_profiles; + if (graph_capture_ceiling == 0) { + startup_profiles = ordinary_profiles; + } else { + for (const GraphExecutionProfile& planned : ordinary_profiles) { + if (planned.max <= graph_capture_ceiling) { startup_profiles.push_back(planned); } + } + if (startup_profiles.empty()) { startup_profiles.push_back(ordinary_profiles.front()); } + } schedule::OrdinaryBatchContext ordinary_state{ execution_core(), decoder->text_kv, *io.ordinary, *ordinary_host_ingress, @@ -10348,9 +10766,9 @@ void ProgramImplCore::prepare_graphs() { nullptr); device.synchronize(); - ordinary_graphs.profiles.reserve(ordinary_profiles.size() * ordinary_batch_limit); + ordinary_graphs.profiles.reserve(startup_profiles.size() * ordinary_batch_limit); for (std::uint32_t batch_size = 1; batch_size <= ordinary_batch_limit; ++batch_size) { - for (const GraphExecutionProfile planned : ordinary_profiles) { + for (const GraphExecutionProfile planned : startup_profiles) { ordinary_graphs.profiles.emplace_back(); DecodeGraphProfile& profile = ordinary_graphs.profiles.back(); profile.batch_size = batch_size; @@ -10358,13 +10776,16 @@ void ProgramImplCore::prepare_graphs() { profile.max_execution_frontier = planned.max; profile.topology_class = planned.topology_class * ordinary_batch_limit + (batch_size - 1U); - const ops::CausalAttentionExecutionEnvelope envelope{planned.min + 1, + const ops::GqaExecutionEnvelope envelope{planned.min + 1, planned.max + 1}; schedule::capture_ordinary_decode_batch(ordinary_state, static_cast(batch_size), envelope, profile.definition); } } + if (graph_capture_ceiling == 0) { + validate_graph_profiles(ordinary_profiles, capacity - 1, "ordinary"); + } } if (speculative_backend == SpeculativeBackend::Mtp) { @@ -10413,7 +10834,7 @@ void ProgramImplCore::prepare_graphs() { *dflash_host_egress, state_images->continuation_hidden_store()}; const GraphExecutionProfile code_warm = batch_one_profiles.front(); - const ops::CausalAttentionExecutionEnvelope code_warm_target{ + const ops::GqaExecutionEnvelope code_warm_target{ 1, static_cast(std::min( capacity, static_cast(code_warm.max) + draft_window + 1ULL))}; prepare_representative(code_warm.min, 1); @@ -10437,7 +10858,7 @@ void ProgramImplCore::prepare_graphs() { profile.max_execution_frontier = planned.max; profile.topology_class = planned.topology_class * max_concurrency + (batch_size - 1U); - const ops::CausalAttentionExecutionEnvelope target_envelope{ + const ops::GqaExecutionEnvelope target_envelope{ 1, static_cast(std::min( capacity, static_cast(planned.max) + draft_window + 1ULL))}; @@ -10449,6 +10870,57 @@ void ProgramImplCore::prepare_graphs() { } } } + if (speculative_backend == SpeculativeBackend::DFlash2) { + const auto batch_one_profiles = dflash2_graph_profiles(capacity, draft_window, 1); + validate_graph_profiles(batch_one_profiles, capacity - 1, "DFlash2"); + schedule::DFlash2BatchContext dflash2_state{execution_core(), + decoder->text_kv, + *dflash2, + *io.dflash_decode, + *dflash2_host_ingress, + *dflash2_host_egress, + state_images->continuation_hidden_store()}; + const GraphExecutionProfile code_warm = batch_one_profiles.front(); + const ops::GqaExecutionEnvelope code_warm_target{ + 1, static_cast(std::min( + capacity, static_cast(code_warm.max) + draft_window + 1ULL))}; + std::fprintf(stderr, "[df2diag] prepare_representative min=%u\n", code_warm.min); + prepare_representative(code_warm.min, 1); + std::fprintf(stderr, "[df2diag] after prepare_representative\n"); + device.synchronize(); + std::fprintf(stderr, "[df2diag] calling dflash2_decode_batch\n"); + schedule::dflash2_decode_batch(dflash2_state, 1, draft_window, + dflash2_envelopes(code_warm.min, code_warm.max, draft_window), + code_warm_target, nullptr); + std::fprintf(stderr, "[df2diag] after dflash2_decode_batch\n"); + device.synchronize(); + + dflash2_graphs.profiles.reserve(batch_one_profiles.size() * max_concurrency); + for (std::uint32_t batch_size = 1; batch_size <= max_concurrency; ++batch_size) { + const auto planned_profiles = + batch_size == 1 ? batch_one_profiles + : dflash2_graph_profiles(capacity, draft_window, batch_size); + validate_graph_profiles(planned_profiles, capacity - 1, "DFlash2"); + for (const GraphExecutionProfile planned : planned_profiles) { + dflash2_graphs.profiles.emplace_back(); + DecodeGraphProfile& profile = dflash2_graphs.profiles.back(); + profile.batch_size = batch_size; + profile.min_execution_frontier = planned.min; + profile.max_execution_frontier = planned.max; + profile.topology_class = + planned.topology_class * max_concurrency + (batch_size - 1U); + const ops::GqaExecutionEnvelope target_envelope{ + 1, + static_cast(std::min( + capacity, static_cast(planned.max) + draft_window + 1ULL))}; + + schedule::capture_dflash2_decode_batch( + dflash2_state, static_cast(batch_size), draft_window, + dflash2_envelopes(planned.min, planned.max, draft_window), target_envelope, + profile.definition); + } + } + } if (!ordinary_graphs.profiles.empty()) { instantiate_graph_family(ordinary_graphs, "ordinary", device, prepare_representative); @@ -10459,6 +10931,9 @@ void ProgramImplCore::prepare_graphs() { if (speculative_backend == SpeculativeBackend::DFlash) { instantiate_graph_family(dflash_graphs, "DFlash", device, prepare_representative); } + if (speculative_backend == SpeculativeBackend::DFlash2) { + instantiate_graph_family(dflash2_graphs, "DFlash2", device, prepare_representative); + } clear_stable_controls(); state_images->zero_all(device.stream); @@ -10470,6 +10945,14 @@ void ProgramImplCore::prepare_graphs() { CUDA_CHECK(cudaMemsetAsync(dflash->pending_features.data, 0, dflash->pending_features.bytes(), device.stream)); } + if (dflash2) { + CUDA_CHECK(cudaMemsetAsync(dflash2->prefill_features.data, 0, + dflash2->prefill_features.bytes(), device.stream)); + CUDA_CHECK(cudaMemsetAsync(dflash2->prefill_positions.data, 0, + dflash2->prefill_positions.bytes(), device.stream)); + CUDA_CHECK(cudaMemsetAsync(dflash2->pending_features.data, 0, + dflash2->pending_features.bytes(), device.stream)); + } CUDA_CHECK(cudaMemsetAsync(token_counts.data, 0, token_counts.bytes(), device.stream)); device.synchronize(); for (std::uint32_t row = 0; row < max_concurrency; ++row) { @@ -10497,6 +10980,76 @@ void ProgramImplCore::prepare_graphs() { release_capture_rows(*text_kv_addresses, text_capture_allocations); } +schedule::ExecutionCore ProgramImplCore::make_execution_core() { + return schedule::ExecutionCore{device, + model, + work, + state_images->linear(), + replay_records ? &*replay_records : nullptr, + io, + prefill_hidden, + prefill_chunk, + proposal_head}; +} + +// On-demand graph capture: capture the missing ordinary-family segments that +// cover `frontier` for one batch size. One segment per growth crossing; each +// is captured exactly once and the family coverage check revalidates. The +// capture reuses prepare_graphs' dummy-page machinery through the address +// store: one transient address space on a dedicated execution row whose +// private page is repeated across the whole table, so arbitrary envelopes +// read/write valid addresses without disturbing any live request. +void ProgramImplCore::extend_ordinary_graphs(std::uint32_t batch_size, + std::uint32_t frontier) { + const auto ordinary_profiles = ordinary_graph_profiles(capacity); + std::vector missing; + for (const GraphExecutionProfile& planned : ordinary_profiles) { + const bool covered = + std::any_of(ordinary_graphs.profiles.begin(), ordinary_graphs.profiles.end(), + [&](const DecodeGraphProfile& profile) { + return profile.batch_size == batch_size && + profile.min_execution_frontier == planned.min && + profile.max_execution_frontier == planned.max; + }); + if (!covered && planned.min <= frontier) { missing.push_back(planned); } + } + if (missing.empty()) { return; } + // Row max_concurrency is the dedicated never-bound capture row (the + // address store is sized max_concurrency + 1); request rows 0..C-1 stay + // untouched during runtime capture. + std::optional allocation = + text_kv_addresses->create_active(1, static_cast(max_concurrency)); + if (!allocation) { throw std::bad_alloc(); } + text_kv_addresses->materialize_to_tokens(*allocation, 1, device.stream); + decoder->text_kv.execution_tables().publish_repeated( + text_kv_addresses->execution_row(*allocation).handle(), + text_kv_addresses->physical_page(*allocation, 0), + decoder->text_kv.execution_tables().logical_page_capacity(), device.stream); + device.synchronize(); + + schedule::OrdinaryBatchContext ordinary_state{ + make_execution_core(), decoder->text_kv, + *io.ordinary, *ordinary_host_ingress, + *ordinary_host_egress, state_images->continuation_hidden_store()}; + for (const GraphExecutionProfile& planned : missing) { + ordinary_graphs.profiles.emplace_back(); + DecodeGraphProfile& profile = ordinary_graphs.profiles.back(); + profile.batch_size = batch_size; + profile.min_execution_frontier = planned.min; + profile.max_execution_frontier = planned.max; + profile.topology_class = planned.topology_class * max_concurrency + (batch_size - 1U); + const ops::GqaExecutionEnvelope envelope{planned.min + 1, planned.max + 1}; + schedule::capture_ordinary_decode_batch(ordinary_state, + static_cast(batch_size), envelope, + profile.definition); + } + if (text_kv_addresses->active(*allocation)) { text_kv_addresses->deactivate(*allocation); } + (void)text_kv_addresses->release(*allocation); + std::fprintf(stderr, "[graphs] extended ordinary family: batch %u +%zu segments through " + "frontier %u\n", + batch_size, missing.size(), frontier); +} + void ProgramImplCore::install_sampling(SequenceState& sequence, RequestControl& request, const ops::SamplingConfig& config) { Tensor counts = token_counts.slice(1, static_cast(sequence.lane), 1) @@ -10667,6 +11220,7 @@ ProgramImplCore::advance_prefill(SequenceState& sequence, RequestControl& reques decoder->text_kv, decoder->mtp_cache(), dflash ? &*dflash : nullptr, + dflash2 ? &*dflash2 : nullptr, staged.cursor, static_cast( sampling_config.slice(1, static_cast(sequence.lane), 1).data), @@ -10674,7 +11228,8 @@ ProgramImplCore::advance_prefill(SequenceState& sequence, RequestControl& reques selectors.source, selectors.destination, staged.initial_mtp_extent, - dflash_host_ingress}; + dflash_host_ingress, + dflash2_host_ingress}; if (staged.mtp_bridge == MtpBridgeMode::BeforeSuffix) { if (staged.cursor != staged.base || staged.base == 0 || @@ -10769,7 +11324,8 @@ ProgramImplCore::advance_prefill(SequenceState& sequence, RequestControl& reques final_chunk_tokens = result.processed_tokens; sequence.text_kv_valid = staged.cursor; if (staged.prepare_mtp) { sequence.mtp_kv_valid = staged.cursor; } - if (speculative_backend == SpeculativeBackend::DFlash) { + if (speculative_backend == SpeculativeBackend::DFlash || + speculative_backend == SpeculativeBackend::DFlash2) { sequence.dflash_context_frontier = staged.cursor; } commit_sequence_kv(sequence, sequence.text_kv_valid, backend_kv_valid(sequence)); @@ -10956,8 +11512,18 @@ ProgramImplCore::decode_ordinary_batch(std::span lanes, submit_range.emplace(nvtx::Name::DecodeOrdinarySubmit, nvtx::Category::Decode, static_cast(lanes.size())); DecodeGraphExecutable* executable = nullptr; - ops::CausalAttentionExecutionEnvelope envelope{maximum_frontier + 1, maximum_frontier + 1}; + ops::GqaExecutionEnvelope envelope{maximum_frontier + 1, maximum_frontier + 1}; if (use_cuda_graph) { + // On-demand capture: with a startup ceiling, growth past the + // captured segments extends the family once per crossing here. + if (graph_capture_ceiling != 0 && + graph_profile_missing(ordinary_graphs, + static_cast(lanes.size()), + maximum_frontier)) { + (void)cudaStreamSynchronize(device.stream); + extend_ordinary_graphs(static_cast(lanes.size()), + maximum_frontier); + } DecodeGraphProfile& profile = select_graph_profile(ordinary_graphs, static_cast(lanes.size()), maximum_frontier, "ordinary batch"); @@ -11287,7 +11853,7 @@ ProgramImplCore::decode_dflash_batch(std::span lanes, static_cast(lanes.size())); DecodeGraphExecutable* executable = nullptr; schedule::DFlashEnvelopes envelopes = dflash_envelopes(0, maximum_frontier, draft_window); - ops::CausalAttentionExecutionEnvelope target_envelope{1, maximum_target_tokens}; + ops::GqaExecutionEnvelope target_envelope{1, maximum_target_tokens}; if (use_cuda_graph) { DecodeGraphProfile& profile = select_graph_profile(dflash_graphs, static_cast(lanes.size()), @@ -11404,7 +11970,7 @@ ProgramImplCore::decode_dflash_batch(std::span lanes, .timing = timing.finish(), }; } catch (...) { - timing.begin_wait(); + timing.begin_wait(); try { nvtx::ScopedRange wait_range(nvtx::Name::DecodeDFlashWait, nvtx::Category::Control, static_cast(lanes.size())); @@ -11420,15 +11986,220 @@ runtime::BatchedGeneratedRound ProgramImplCore::decode_raw(std::span lanes, std::span budgets, runtime::ExecutionTiming* failed_timing) { + // Cold-pool maintenance at the round boundary (window policy only). + // Every active sequence maintains its own retired prefix; multi-lane + // batches compress each lane's pages independently. + if (cold_policy == ColdPolicy::Window) { + for (const std::uint32_t lane : lanes) { + SequenceState& sequence = active_sequence(lane); + if (sequence.kv) { + enqueue_cold_compressions(sequence); + } + } + } if (speculative_backend == SpeculativeBackend::None) { return decode_ordinary_batch(lanes, budgets, failed_timing); } if (speculative_backend == SpeculativeBackend::Mtp) { return decode_mtp_batch(lanes, budgets, failed_timing); } + if (speculative_backend == SpeculativeBackend::DFlash2) { + return decode_dflash2_batch(lanes, budgets, failed_timing); + } return decode_dflash_batch(lanes, budgets, failed_timing); } +runtime::BatchedGeneratedRound +ProgramImplCore::decode_dflash2_batch(std::span lanes, + std::span budgets, + runtime::ExecutionTiming* failed_timing) { + if (speculative_backend != SpeculativeBackend::DFlash2 || !io.dflash_decode || !dflash2) { + throw std::logic_error("DFlash2 batch execution requires the DFlash2 backend"); + } + if (lanes.empty() || lanes.size() > max_concurrency || budgets.size() != lanes.size()) { + throw std::invalid_argument("DFlash2 batch membership is invalid"); + } + + const std::uint32_t width = draft_window + 1U; + std::uint32_t maximum_frontier = 0; + std::uint32_t maximum_target_tokens = 1; + for (std::size_t row = 0; row < lanes.size(); ++row) { + const std::uint32_t lane = lanes[row]; + if (lane >= max_concurrency || + std::find(lanes.begin(), lanes.begin() + static_cast(row), lane) != + lanes.begin() + static_cast(row)) { + throw std::invalid_argument("DFlash2 batch contains an invalid or duplicate lane"); + } + const SequenceState& sequence = active_sequence(lane); + const RequestControl& request = requests[lane]; + if (request.lifecycle != Lifecycle::Active || + budgets[row].generated_tokens_remaining == 0 || !sequence.kv || + text_kv_addresses->bound_row(sequence.kv->text) < 0 || sequence.execution_frontier >= capacity || + sequence.text_kv_valid != sequence.execution_frontier || + sequence.dflash_context_frontier > sequence.execution_frontier || + sequence.execution_frontier - sequence.dflash_context_frontier > width || + sequence.ledger_frontier != sequence.execution_frontier + 1 || + sequence.ledger.size() != sequence.ledger_frontier || + sequence.prefix_identity.size() != sequence.ledger_frontier) { + throw std::logic_error("DFlash2 batch row is not decode-ready"); + } + const std::uint32_t max_by_budget = budgets[row].generated_tokens_remaining > 1 + ? budgets[row].generated_tokens_remaining - 1U + : 0U; + // Length demotion (measured on the 32GiB 5090, threshold 20480): + // past ~17-20k prompt tokens the DFlash2 advantage over flat MTP + // disappears (~+2.6% at 15k), so lanes beyond the threshold draft + // nothing this round (dense single-token round through the same + // kernel/graph machinery as the spec-degrade fallback). The target + // KV cache is shared, so the row stays decode-ready; a fresh request + // below the threshold simply drafts again. + const bool lane_beyond_demote = + sequence.execution_frontier > qwen3_6::kSpecDemoteTokens; + const std::uint32_t extent = + lane_beyond_demote + ? 0U + : std::min({draft_window, max_by_budget, + capacity - sequence.execution_frontier - 1U}); + maximum_frontier = std::max(maximum_frontier, sequence.execution_frontier); + maximum_target_tokens = + std::max(maximum_target_tokens, sequence.execution_frontier + extent + 1U); + } + + runtime::ExecutionTimingRecorder timing(runtime::ExecutionTimingPhase::Submit, failed_timing); + const auto started = Clock::now(); + try { + std::optional submit_range; + submit_range.emplace(nvtx::Name::DecodeDFlashSubmit, nvtx::Category::DFlash, + static_cast(lanes.size())); + DecodeGraphExecutable* executable = nullptr; + schedule::DFlash2Envelopes envelopes = dflash2_envelopes(0, maximum_frontier, draft_window); + ops::GqaExecutionEnvelope target_envelope{1, maximum_target_tokens}; + if (use_cuda_graph) { + DecodeGraphProfile& profile = + select_graph_profile(dflash2_graphs, static_cast(lanes.size()), + maximum_frontier, "DFlash2 batch"); + executable = &install_graph_profile(dflash2_graphs, profile, "DFlash2 batch"); + envelopes = dflash2_envelopes(profile.min_execution_frontier, + profile.max_execution_frontier, draft_window); + target_envelope = { + 1, static_cast(std::min( + capacity, static_cast(profile.max_execution_frontier) + + draft_window + 1ULL))}; + } + + for (std::size_t row = 0; row < lanes.size(); ++row) { + SequenceState& sequence = active_sequence(lanes[row]); + const RequestControl& request = requests[lanes[row]]; + const std::uint32_t frontier = sequence.execution_frontier; + const std::uint32_t max_by_budget = budgets[row].generated_tokens_remaining > 1 + ? budgets[row].generated_tokens_remaining - 1U + : 0U; + const std::uint32_t extent = + std::min({draft_window, max_by_budget, capacity - frontier - 1U}); + dflash2_host_ingress->anchors[row] = sequence.ledger.back(); + dflash2_host_ingress->execution_frontiers[row] = + checked_i32(frontier, "DFlash2 batch frontier"); + dflash2_host_ingress->context_frontiers[row] = + checked_i32(sequence.dflash_context_frontier, "DFlash2 context frontier"); + dflash2_host_ingress->proposal_extents[row] = static_cast(extent); + dflash2_host_ingress->target_valid_columns[row] = static_cast(extent + 1U); + dflash2_host_ingress->text_kv_table_rows[row] = + text_kv_addresses->bound_row(sequence.kv->text); + dflash2_host_ingress->dflash_kv_table_rows[row] = 0; + dflash2_host_ingress->active_lanes[row] = static_cast(sequence.lane); + dflash2_host_ingress->sampling[row] = request.sampling_host; + // DFlash2 owns no backend KV; only the shared text cache materializes. + materialize_sequence_kv(sequence, frontier + extent + 1U, 0); + } + + schedule::DFlash2BatchContext schedule_state{ + {device, model, work, state_images->linear(), + replay_records ? &*replay_records : nullptr, io, prefill_hidden, prefill_chunk, + proposal_head}, + decoder->text_kv, + *dflash2, + *io.dflash_decode, + *dflash2_host_ingress, + *dflash2_host_egress, + state_images->continuation_hidden_store()}; + + mark_workspace_usage(workspace_plan.dflash2_round); + schedule::dflash2_decode_batch(schedule_state, static_cast(lanes.size()), + draft_window, envelopes, target_envelope, executable); + submit_range.reset(); + timing.begin_wait(); + { + nvtx::ScopedRange wait_range(nvtx::Name::DecodeDFlashWait, nvtx::Category::Control, + static_cast(lanes.size())); + device.synchronize(); + } + timing.end_wait(); + + const double seconds = std::chrono::duration(Clock::now() - started).count(); + for (std::size_t row = 0; row < lanes.size(); ++row) { + SequenceState& sequence = active_sequence(lanes[row]); + RequestControl& request = requests[lanes[row]]; + const std::uint32_t base_E = sequence.execution_frontier; + const std::uint32_t base_S = sequence.ledger_frontier; + const std::int32_t count_i = dflash2_host_egress->licensed_counts[row]; + const std::int32_t accepted_i = dflash2_host_egress->accepted_drafts[row]; + const std::uint32_t extent = + static_cast(dflash2_host_egress->proposal_extents[row]); + if (count_i <= 0 || count_i > static_cast(width) || accepted_i < 0 || + accepted_i + 1 != count_i || accepted_i > static_cast(extent) || + extent > width || + extent > static_cast(dflash2_host_ingress->proposal_extents[row]) || + static_cast(count_i) > budgets[row].generated_tokens_remaining || + static_cast(base_E) + static_cast(count_i) > + capacity) { + throw std::runtime_error("DFlash2 batch returned invalid row metadata"); + } + const std::span row_tokens(dflash2_host_egress->licensed_tokens.data() + + row * width, + static_cast(count_i)); + validate_licensed_tokens(row_tokens); + if (extent == 0) { + request.speculative_stats.fallback_steps += 1; + } else { + request.speculative_stats.rounds += 1; + request.speculative_stats.drafted_tokens += extent; + request.speculative_stats.accepted_tokens += static_cast(accepted_i); + for (std::int32_t i = 0; i < accepted_i; ++i) { + request.speculative_stats.accepted_per_position[static_cast(i)] += + 1; + } + } + sequence.dflash_context_frontier = base_E; + request.pending = PendingCandidate{ + .kind = PendingKind::Speculative, + .base_E = base_E, + .base_S = base_S, + .prompt_tokens = 0, + .produced = static_cast(count_i), + }; + request.lifecycle = Lifecycle::Pending; + request.timings.decode_seconds += seconds; + } + return runtime::BatchedGeneratedRound{ + .tokens = std::span(dflash2_host_egress->licensed_tokens.data(), + lanes.size() * width), + .row_counts = std::span(dflash2_host_egress->licensed_counts.data(), + lanes.size()), + .row_stride = width, + }; + } catch (...) { + timing.begin_wait(); + try { + nvtx::ScopedRange wait_range(nvtx::Name::DecodeDFlashWait, nvtx::Category::Control, + static_cast(lanes.size())); + device.synchronize(); + } catch (...) {} + timing.end_wait(); + clear_execution_failure_lanes(lanes); + throw; + } +} + runtime::ExecutionTiming ProgramImplCore::resolve_non_speculative_pending(SequenceState& sequence, RequestControl& request, std::uint32_t accepted_tokens, bool terminal, diff --git a/src/targets/qwen3_6/impl/runtime/request_plan_impl.h b/src/targets/qwen3_6/impl/runtime/request_plan_impl.h index 504725148e..8406d9b823 100644 --- a/src/targets/qwen3_6/impl/runtime/request_plan_impl.h +++ b/src/targets/qwen3_6/impl/runtime/request_plan_impl.h @@ -571,6 +571,15 @@ std::optional ProgramImplCore::inspect_lane( shared_source->backend_frontier < plan->reuse_base)))) { throw std::logic_error("published DFlash checkpoint is not materializable"); } + if ((is_rewrite_checkpoint_restore(plan->reuse) || + plan->reuse == ReusePath::PrivateLongAnchor || + plan->reuse == ReusePath::SharedStablePrefix) && + speculative_backend == SpeculativeBackend::DFlash2 && + (!dflash2 || + (source != nullptr && source->dflash_context_frontier < plan->reuse_base) || + (shared_source != nullptr && shared_source->backend_frontier < plan->reuse_base))) { + throw std::logic_error("published DFlash2 checkpoint is not materializable"); + } const std::optional& desired = base.rewrite_checkpoint; const bool can_retain_rewrite = @@ -802,6 +811,8 @@ std::optional ProgramImplCore::inspect_lane( for (std::uint32_t page = 0; page < required; ++page) { const LogicalKVPageHandle logical = addresses.logical_page(address, page); if (pages.device_resident(logical)) { continue; } + // Cold-pool pages restore in place from their raw slots. + if (pages.cold_compressed(logical)) { continue; } if (!pages.host_resident(logical)) { throw std::logic_error("checkpoint KV page has no restorable replica"); } diff --git a/src/targets/qwen3_6/impl/runtime/schedule.h b/src/targets/qwen3_6/impl/runtime/schedule.h index c9dc6eebec..cbdeaa088a 100644 --- a/src/targets/qwen3_6/impl/runtime/schedule.h +++ b/src/targets/qwen3_6/impl/runtime/schedule.h @@ -9,6 +9,7 @@ #include "ninfer/ops/sampling.h" #include "ninfer/ops/sliding_window_attention.h" #include "ninfer/ops/softmax_attention.h" +#include "ninfer/ops/swa.h" #include "targets/qwen3_6/impl/runtime/dflash_context.h" #include "targets/qwen3_6/impl/runtime/text_context.h" #include "targets/qwen3_6/impl/runtime/vision_context.h" @@ -47,13 +48,15 @@ struct PrefillContext { const qwen3_6::PagedKVCache& text_cache; const qwen3_6::PagedKVCache* mtp_cache; DFlashPersistentState* dflash; + DFlash2PersistentState* dflash2; std::uint32_t text_kv_base; const ops::SamplingConfig* sampling; Tensor* rewrite_checkpoint_hidden; std::int32_t state_source_slot = 0; std::int32_t state_destination_slot = 0; std::uint32_t mtp_proposal_extent = 0; - const qwen3_6::DFlashDecodeIngress* dflash_host_ingress = nullptr; + const qwen3_6::DFlashDecodeIngress* dflash_host_ingress = nullptr; + const qwen3_6::DFlashDecodeIngress* dflash2_host_ingress = nullptr; }; struct OrdinaryBatchContext { @@ -90,10 +93,25 @@ struct DFlashAppendContext { DFlashPersistentState& dflash; }; +struct DFlash2BatchContext { + ExecutionCore execution; + const qwen3_6::PagedKVCache& text_cache; + DFlash2PersistentState& dflash2; + qwen3_6::DFlashDecodeState& frame; + const qwen3_6::DFlashDecodeIngress& host_ingress; + qwen3_6::DFlashDecodeEgress& host_egress; + Tensor& continuation_hidden_store; +}; + +struct DFlash2AppendContext { + ExecutionCore execution; + DFlash2PersistentState& dflash2; +}; + struct MtpCausalAttentionEnvelopes { - ops::CausalAttentionExecutionEnvelope target_verify; - ops::CausalAttentionExecutionEnvelope batch; - std::array ar; + ops::GqaExecutionEnvelope target_verify; + ops::GqaExecutionEnvelope batch; + std::array ar; }; struct DFlashEnvelopes { @@ -102,6 +120,14 @@ struct DFlashEnvelopes { ops::KVCacheAppendPrefixExecutionEnvelope append; }; +// DFlash2 is a pure sliding-window draft: its local attention uses the swa +// kernel (symmetric window, no full-context stage), so only the local and +// append envelopes exist. +struct DFlash2Envelopes { + ops::SwaContextExecutionEnvelope local; + ops::KVCacheAppendPrefixExecutionEnvelope append; +}; + struct TargetVerifyFrameView { Tensor ids; Tensor cache_positions; @@ -131,7 +157,7 @@ void configure_text_card(TextContext& card, const ExecutionCore& execution, std::int32_t state_destination_slot, std::uint32_t mtp_proposal_extent); void target_verify_accept(ExecutionCore& execution, Tensor& continuation_hidden_store, TextContext& card, TargetVerifyFrameView frame, - ops::CausalAttentionExecutionEnvelope envelope); + ops::GqaExecutionEnvelope envelope); [[nodiscard]] PrefillChunkResult prefill_text_chunk(PrefillContext& state, std::span ids, @@ -163,10 +189,10 @@ void mtp_bridge_multimodal(PrefillContext& state, const PreparedPromptData& prom // ordinary ingress, share one model schedule, publish continuation hidden by selector, and leave // through one compact egress transfer. void capture_ordinary_decode_batch(OrdinaryBatchContext& state, std::int32_t batch_size, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, DecodeGraphDefinition& definition); void ordinary_decode_batch(OrdinaryBatchContext& state, std::int32_t batch_size, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, DecodeGraphExecutable* executable); // Executes one exact-B MTP verification/alignment/proposal transaction. Each row may carry a @@ -189,11 +215,30 @@ void dflash_append_context(PrefillContext& state, const Tensor& features, const ops::KVCacheAppendPrefixExecutionEnvelope envelope); void capture_dflash_decode_batch(DFlashBatchContext& state, std::int32_t batch_size, std::uint32_t k, DFlashEnvelopes envelopes, - ops::CausalAttentionExecutionEnvelope target_envelope, + ops::GqaExecutionEnvelope target_envelope, DecodeGraphDefinition& definition); void dflash_decode_batch(DFlashBatchContext& state, std::int32_t batch_size, std::uint32_t k, DFlashEnvelopes envelopes, - ops::CausalAttentionExecutionEnvelope target_envelope, + ops::GqaExecutionEnvelope target_envelope, DecodeGraphExecutable* executable); +[[nodiscard]] DFlashFeatureSink +dflash2_feature_sink(PrefillContext& state, DFlashFeatureSink::PrefillConsumer consume_prefill = {}); +void dflash2_append_context(DFlash2AppendContext& state, const Tensor& features, + const Tensor& positions, const Tensor& commit_counts, + const Tensor& lanes, const Tensor& table_rows, + ops::KVCacheAppendPrefixExecutionEnvelope envelope); +void dflash2_append_context(PrefillContext& state, const Tensor& features, const Tensor& positions, + const Tensor& commit_counts, const Tensor& lanes, + const Tensor& table_rows, + ops::KVCacheAppendPrefixExecutionEnvelope envelope); +void capture_dflash2_decode_batch(DFlash2BatchContext& state, std::int32_t batch_size, + std::uint32_t k, DFlash2Envelopes envelopes, + ops::GqaExecutionEnvelope target_envelope, + DecodeGraphDefinition& definition); +void dflash2_decode_batch(DFlash2BatchContext& state, std::int32_t batch_size, std::uint32_t k, + DFlash2Envelopes envelopes, + ops::GqaExecutionEnvelope target_envelope, + DecodeGraphExecutable* executable); + } // namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule diff --git a/src/targets/qwen3_6/impl/runtime/spec_decision.h b/src/targets/qwen3_6/impl/runtime/spec_decision.h new file mode 100644 index 0000000000..94b6f7a5cd --- /dev/null +++ b/src/targets/qwen3_6/impl/runtime/spec_decision.h @@ -0,0 +1,81 @@ +#pragma once + +// Per-request speculative backend decision policy (host-only, no CUDA deps). +// +// Measured on the 32 GiB RTX 5090 (Qwen3.8-27B NVFP4 artifact, 256-token +// greedy decodes, Chinese wiki prompts; /tmp/spec_threshold_results.txt): +// prompt tok | MTP d3 | DFlash2 d7 +// 2,047 | 58.4 | 75.3 (+29%) +// 4,066 | 58.3 | 71.2 (+22%) +// 7,924 | 58.8 | 71.9 (+22%) +// 11,585 | 58.1 | 60.4 (+4%) +// 15,226 | 57.6 | 59.1 (+2.6%) +// MTP is flat to 21k+; the curves cross near 17-20k. DSpark is dominated at +// every point (65.8 at 2k, 35.3 at 15k) and excluded from selection. +// +// Policy (user decision 2026-08-30): +// * admission: projected context <= kSpecDemoteTokens -> DFlash2, else MTP +// * mid-flight: while DFlash2 is active, demote the engine to MTP once ALL +// active requests' frontiers pass the threshold, or on VRAM pressure. +// Demotion is low-frequency (at most once per growth crossing) and uses +// the existing spec-degrade resume machinery. +// * promotion back to DFlash2 happens only when the engine is fully idle. + +#include +#include +#include + +namespace ninfer::targets::qwen3_6 { + +inline constexpr std::uint32_t kSpecDemoteTokens = 20480; + +enum class SpecDecision : std::uint8_t { + Mtp, + DFlash2, +}; + +struct SpecAdmissionInputs { + std::uint32_t prompt_tokens = 0; + // Generation budget if the caller supplied one; 0 = unknown. When known, + // a projection that is guaranteed to cross the threshold starts on MTP + // directly instead of demoting mid-flight. + std::uint32_t max_new_tokens = 0; + bool dflash2_available = false; + std::size_t free_device_bytes = 0; + // Projected device bytes the DFlash2 context caches need for this request + // (BF16 full-context draft caches). 0 = unknown / not tracked yet. + std::size_t dflash2_context_bytes = 0; +}; + +[[nodiscard]] inline SpecDecision choose_spec_backend(const SpecAdmissionInputs& in) noexcept { + if (!in.dflash2_available) { return SpecDecision::Mtp; } + const std::uint64_t projection = static_cast(in.prompt_tokens) + + (in.max_new_tokens != 0 ? in.max_new_tokens : 0); + if (projection > kSpecDemoteTokens) { return SpecDecision::Mtp; } + if (in.dflash2_context_bytes != 0 && in.free_device_bytes != 0 && + in.dflash2_context_bytes > in.free_device_bytes) { + return SpecDecision::Mtp; + } + return SpecDecision::DFlash2; +} + +// Engine-wide mid-flight demotion check (called at decode round boundaries; +// cheap by design). Demote when every active request has crossed the +// threshold, or when the DFlash2 footprint no longer fits free VRAM. +[[nodiscard]] inline bool should_demote_dflash2( + const std::vector& active_frontiers, std::size_t free_device_bytes, + std::size_t dflash2_footprint_bytes) noexcept { + if (active_frontiers.empty()) { return false; } + bool all_beyond = true; + for (const std::uint32_t frontier : active_frontiers) { + if (frontier <= kSpecDemoteTokens) { + all_beyond = false; + break; + } + } + if (all_beyond) { return true; } + return dflash2_footprint_bytes != 0 && free_device_bytes != 0 && + dflash2_footprint_bytes > free_device_bytes; +} + +} // namespace ninfer::targets::qwen3_6 diff --git a/src/targets/qwen3_6/impl/runtime/speculative_target_impl.h b/src/targets/qwen3_6/impl/runtime/speculative_target_impl.h index cb3f7311b4..e7baaf74e5 100644 --- a/src/targets/qwen3_6/impl/runtime/speculative_target_impl.h +++ b/src/targets/qwen3_6/impl/runtime/speculative_target_impl.h @@ -8,7 +8,7 @@ namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule { void target_verify_accept(ExecutionCore& execution, Tensor& continuation_hidden_store, TextContext& card, TargetVerifyFrameView frame, - ops::CausalAttentionExecutionEnvelope envelope) { + ops::GqaExecutionEnvelope envelope) { if (frame.replay_records == nullptr) { throw std::logic_error("speculative target verify has no ReplaySSM record storage"); } diff --git a/src/targets/qwen3_6/impl/runtime/text_context.h b/src/targets/qwen3_6/impl/runtime/text_context.h index 09eb0e182c..675c7ef460 100644 --- a/src/targets/qwen3_6/impl/runtime/text_context.h +++ b/src/targets/qwen3_6/impl/runtime/text_context.h @@ -9,6 +9,7 @@ #include "core/tensor.h" #include "core/weight.h" #include "ninfer/ops/sampling.h" +#include "ninfer/ops/gqa_attention.h" #include "ninfer/ops/softmax_attention.h" #include #include @@ -207,31 +208,31 @@ class TextContext { const Tensor& rope_positions, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, const Tensor& linear_state_destination_slots, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& hidden, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits); void target_verify_batch(const Tensor& ids, const Tensor& cache_positions, const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& hidden, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits, Tensor& target_tokens); void target_verify_batch(const Tensor& ids, const Tensor& cache_positions, const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& hidden, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits, Tensor& target_tokens, DFlashFeatureSink& sink); void mtp_forward_decode_batch(const Tensor& ids, const Tensor& hidden, const Tensor& cache_positions, const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden); void mtp_propose_batch(const Tensor& hidden, Tensor& logits, Tensor& draft_tokens); void mtp_forward_batch(const Tensor& ids, const Tensor& hidden, const Tensor& positions, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& mtp_hidden, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden, int logits_column, Tensor* logits, Tensor* draft_token, const Tensor* explicit_rope_positions = nullptr, const Tensor* input_embeddings = nullptr); void mtp_forward_ar_step(const Tensor& token, const Tensor& previous_hidden, - const Tensor& position, ops::CausalAttentionExecutionEnvelope envelope, + const Tensor& position, ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden, Tensor& logits, Tensor& draft_token); private: void bind(); @@ -252,21 +253,21 @@ class TextContext { const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& hidden, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits, Tensor& target_tokens, Tap& tap); void mtp_forward_stem(const Tensor& ids, const Tensor& hidden, const Tensor* input_embeddings, Tensor& x, Tensor& ah); void mtp_forward_tail(Tensor& x, const Tensor& ah, const Tensor& positions, const Tensor& rope_positions, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& mtp_hidden); + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden); void mtp_forward_core(const Tensor& ids, const Tensor& hidden, const Tensor& positions, const Tensor& rope_positions, - ops::CausalAttentionExecutionEnvelope envelope, Tensor& mtp_hidden, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden, const Tensor* input_embeddings); void mtp_prefill_chunk(const Tensor& ids, const Tensor& hidden, const Tensor* input_embeddings, const Tensor& positions, const Tensor& rope_positions, - ops::CausalAttentionExecutionEnvelope envelope, bool final_chunk, + ops::GqaExecutionEnvelope envelope, bool final_chunk, Tensor* final_hidden, Tensor* logits, Tensor* draft_token); void proposal_argmax(const Tensor& hidden, Tensor& logits, Tensor& proposal_tokens); @@ -306,7 +307,7 @@ class TextContext { const Tensor* active_linear_state_destination_slots_ = nullptr; const Tensor* active_valid_columns_ = nullptr; const Tensor* active_backend_kv_table_rows_ = nullptr; - const ops::CausalAttentionExecutionEnvelope* active_causal_attention_envelope_ = nullptr; + const ops::GqaExecutionEnvelope* active_causal_attention_envelope_ = nullptr; std::int32_t active_sequence_batch_ = 0; std::int32_t active_sequence_width_ = 0; std::int32_t rope_delta_ = 0; diff --git a/src/targets/qwen3_6/impl/runtime/text_context_impl.h b/src/targets/qwen3_6/impl/runtime/text_context_impl.h index f3c78dd9ae..433453beee 100644 --- a/src/targets/qwen3_6/impl/runtime/text_context_impl.h +++ b/src/targets/qwen3_6/impl/runtime/text_context_impl.h @@ -108,8 +108,8 @@ class ScopedPositions { class ScopedEnvelope { public: - ScopedEnvelope(const ops::CausalAttentionExecutionEnvelope*& slot, - const ops::CausalAttentionExecutionEnvelope& envelope) + ScopedEnvelope(const ops::GqaExecutionEnvelope*& slot, + const ops::GqaExecutionEnvelope& envelope) : slot_(slot) { slot_ = &envelope; } @@ -120,7 +120,7 @@ class ScopedEnvelope { ~ScopedEnvelope() { slot_ = nullptr; } private: - const ops::CausalAttentionExecutionEnvelope*& slot_; + const ops::GqaExecutionEnvelope*& slot_; }; template @@ -359,7 +359,7 @@ void TextContext::mtp_forward_stem(const Tensor& ids, const Tensor& hidden, void TextContext::mtp_forward_tail(Tensor& x, const Tensor& ah, const Tensor& positions, const Tensor& rope_positions, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden) { cudaStream_t s = ctx_.stream; const int T = x.ne[1]; @@ -382,7 +382,11 @@ void TextContext::mtp_forward_tail(Tensor& x, const Tensor& ah, const Tensor& po ops::rmsnorm(q, *mtp_.q_norm, kCfg.rms_eps, true, qn, s); ops::rmsnorm(k, *mtp_.k_norm, kCfg.rms_eps, true, kn, s); Tensor rope_for_op = active_sequence_batch_ != 0 ? rope_positions.view({T}) : rope_positions; - ops::rope(rope_for_op, kCfg.rotary_dim, kCfg.rope_theta, qn, kn, s); + if (ctx_.yarn_enabled) { + ops::rope_yarn4(rope_for_op, kCfg.rotary_dim, kCfg.rope_theta, qn, kn, s); + } else { + ops::rope(rope_for_op, kCfg.rotary_dim, kCfg.rope_theta, qn, kn, s); + } Tensor a = results.attention.view({kCfg.head_dim, kCfg.n_q, T}); if (active_sequence_batch_ != 0) { @@ -396,13 +400,13 @@ void TextContext::mtp_forward_tail(Tensor& x, const Tensor& ah, const Tensor& po Tensor v_batch = v.view({kCfg.head_dim, kCfg.n_kv, width, active_sequence_batch_}); Tensor a_batch = a.view({kCfg.head_dim, kCfg.n_q, width, active_sequence_batch_}); Tensor position_batch = positions.view({width, active_sequence_batch_}); - ops::causal_softmax_attention( + ops::gqa_attention( q_batch, k_batch, v_batch, position_batch, *active_valid_columns_, - *active_backend_kv_table_rows_, {kCfg.head_dim, kCfg.n_q, kCfg.n_kv}, kAttnScale, + *active_backend_kv_table_rows_, kAttnScale, batch_mtp_kv_->batch_layer_view(0), envelope, work_, a_batch, s); } else { - ops::causal_softmax_attention(qn, kn, v, positions, Tensor{}, io_.backend_kv_table_row, - {kCfg.head_dim, kCfg.n_q, kCfg.n_kv}, kAttnScale, + ops::gqa_attention(qn, kn, v, positions, Tensor{}, io_.backend_kv_table_row, + kAttnScale, batch_mtp_kv_->batch_layer_view(0), envelope, work_, a, s); } ops::sigmoid_mul(gate, a, s); @@ -426,7 +430,7 @@ void TextContext::mtp_forward_tail(Tensor& x, const Tensor& ah, const Tensor& po void TextContext::mtp_forward_core(const Tensor& ids, const Tensor& hidden, const Tensor& positions, const Tensor& rope_positions, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden, const Tensor* input_embeddings) { if (batch_mtp_kv_ == nullptr) { throw std::runtime_error("MTP forward is not enabled"); } nvtx::ScopedRange forward_range(nvtx::Name::MtpForward, nvtx::Category::Mtp, @@ -441,7 +445,7 @@ void TextContext::mtp_forward_core(const Tensor& ids, const Tensor& hidden, cons void TextContext::mtp_prefill_chunk(const Tensor& ids, const Tensor& hidden, const Tensor* input_embeddings, const Tensor& positions, const Tensor& rope_positions, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, bool final_chunk, Tensor* final_hidden, Tensor* logits, Tensor* draft_token) { if (!mtp_kv_.valid()) { throw std::runtime_error("MTP prefill is not enabled"); } @@ -492,8 +496,12 @@ void TextContext::mtp_prefill_chunk(const Tensor& ids, const Tensor& hidden, Tensor v = v_flat.view({kCfg.head_dim, kCfg.n_kv, T}); Tensor kn = work_.alloc(DType::BF16, {kCfg.head_dim, kCfg.n_kv, T}); ops::rmsnorm(k, *mtp_.k_norm, kCfg.rms_eps, true, kn, s); + if (ctx_.yarn_enabled) { + ops::rope_yarn4(rope_positions, kCfg.rotary_dim, kCfg.rope_theta, kn, s); + } else { ops::rope(rope_positions, kCfg.rotary_dim, kCfg.rope_theta, kn, s); - ops::kv_cache_append(kn, v, positions, mtp_kv_.layer_view(0), s); + } + ops::gqa_kv_append(kn, v, positions, mtp_kv_.layer_view(0), s); if (final_chunk) { const std::size_t column_bytes = @@ -532,11 +540,15 @@ void TextContext::mtp_prefill_chunk(const Tensor& ids, const Tensor& hidden, cudaMemcpyAsync(dst, src, sizeof(std::int32_t), cudaMemcpyDeviceToDevice, s)); } } + if (ctx_.yarn_enabled) { + ops::rope_yarn4(last_rope_position, kCfg.rotary_dim, kCfg.rope_theta, qn, s); + } else { ops::rope(last_rope_position, kCfg.rotary_dim, kCfg.rope_theta, qn, s); + } Tensor a = work_.alloc(DType::BF16, {kCfg.head_dim, kCfg.n_q, 1}); - ops::causal_softmax_attention_cached(qn, last_position, - {kCfg.head_dim, kCfg.n_q, kCfg.n_kv}, kAttnScale, + ops::gqa_attention_cached(qn, last_position, + kAttnScale, mtp_kv_.layer_view(0), envelope, work_, a, s); ops::sigmoid_mul(gate, a, s); @@ -577,7 +589,7 @@ void TextContext::proposal_argmax(const Tensor& hidden, Tensor& logits, Tensor& void TextContext::mtp_forward_batch(const Tensor& ids, const Tensor& hidden, const Tensor& positions, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden, int logits_column, Tensor* logits, Tensor* draft_token, const Tensor* explicit_rope_positions, const Tensor* input_embeddings) { @@ -624,7 +636,7 @@ void TextContext::mtp_forward_batch(const Tensor& ids, const Tensor& hidden, void TextContext::mtp_forward_ar_step(const Tensor& token, const Tensor& previous_hidden, const Tensor& position, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden, Tensor& logits, Tensor& draft_token) { if (batch_mtp_kv_ == nullptr) { throw std::runtime_error("MTP forward is not enabled"); } require_tensor_shape(token, DType::I32, {1}, "MTP AR token"); @@ -647,7 +659,7 @@ void TextContext::ordinary_decode_batch(const Tensor& ids, const Tensor& cache_p const Tensor& rope_positions, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, const Tensor& linear_state_destination_slots, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits) { const std::int32_t batch = ids.ne[0]; if (batch <= 0 || batch > static_cast(kMaximumConcurrency)) { @@ -693,7 +705,7 @@ void TextContext::target_verify_batch_impl(const Tensor& ids, const Tensor& cach const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits, Tensor& target_tokens, Tap& tap) { const std::int32_t width = ids.ne[0]; @@ -753,7 +765,7 @@ void TextContext::target_verify_batch(const Tensor& ids, const Tensor& cache_pos const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits, Tensor& target_tokens) { NullTap tap; target_verify_batch_impl(ids, cache_positions, rope_positions, valid_columns, kv_table_rows, @@ -765,7 +777,7 @@ void TextContext::target_verify_batch(const Tensor& ids, const Tensor& cache_pos const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, const Tensor& linear_state_source_slots, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& hidden, Tensor& logits, Tensor& target_tokens, DFlashFeatureSink& sink) { target_verify_batch_impl(ids, cache_positions, rope_positions, valid_columns, kv_table_rows, @@ -777,7 +789,7 @@ void TextContext::mtp_forward_decode_batch(const Tensor& ids, const Tensor& hidd const Tensor& cache_positions, const Tensor& rope_positions, const Tensor& valid_columns, const Tensor& kv_table_rows, - ops::CausalAttentionExecutionEnvelope envelope, + ops::GqaExecutionEnvelope envelope, Tensor& mtp_hidden) { if (batch_mtp_kv_ == nullptr) { throw std::runtime_error("MTP forward is not enabled"); } const std::int32_t width = ids.ne[0]; @@ -845,7 +857,11 @@ void TextContext::attn_mix(const FullLayerW& w, Tensor& x, int fidx, Phase ph) { const Tensor& rope_positions = active_rope_positions_ != nullptr ? *active_rope_positions_ : io_.rope_pos; Tensor rope_for_op = active_sequence_batch_ != 0 ? rope_positions.view({T}) : rope_positions; - ops::rope(rope_for_op, kCfg.rotary_dim, kCfg.rope_theta, qn, kn, s); + if (ctx_.yarn_enabled) { + ops::rope_yarn4(rope_for_op, kCfg.rotary_dim, kCfg.rope_theta, qn, kn, s); + } else { + ops::rope(rope_for_op, kCfg.rotary_dim, kCfg.rope_theta, qn, kn, s); + } Tensor a = results.attention.view({kCfg.head_dim, kCfg.n_q, T}); const Tensor& kv_table_rows = @@ -861,13 +877,13 @@ void TextContext::attn_mix(const FullLayerW& w, Tensor& x, int fidx, Phase ph) { Tensor a_batch = a.view({kCfg.head_dim, kCfg.n_q, width, active_sequence_batch_}); Tensor position_batch = cache_positions.view({width, active_sequence_batch_}); const Tensor valid = active_valid_columns_ != nullptr ? *active_valid_columns_ : Tensor{}; - ops::causal_softmax_attention(q_batch, k_batch, v_batch, position_batch, valid, - kv_table_rows, {kCfg.head_dim, kCfg.n_q, kCfg.n_kv}, + ops::gqa_attention(q_batch, k_batch, v_batch, position_batch, valid, + kv_table_rows, kAttnScale, batch_text_kv_->batch_layer_view(fidx), *active_causal_attention_envelope_, work_, a_batch, s); } else { - ops::causal_softmax_attention(qn, kn, v, cache_positions, Tensor{}, kv_table_rows, - {kCfg.head_dim, kCfg.n_q, kCfg.n_kv}, kAttnScale, + ops::gqa_attention(qn, kn, v, cache_positions, Tensor{}, kv_table_rows, + kAttnScale, batch_text_kv_->batch_layer_view(fidx), *active_causal_attention_envelope_, work_, a, s); } @@ -1168,7 +1184,7 @@ TextContext::prefill_impl(std::span ids, const TextPrefill* text_pref ScopedPositions scoped_cache(active_cache_positions_, positions); ScopedPositions scoped_rope(active_rope_positions_, rope_positions); const auto visible = static_cast(base_i + t0 + len); - const ops::CausalAttentionExecutionEnvelope chunk_envelope{visible, visible}; + const ops::GqaExecutionEnvelope chunk_envelope{visible, visible}; ScopedEnvelope scoped_envelope(active_causal_attention_envelope_, chunk_envelope); Tensor x = roots.residual; @@ -1270,7 +1286,7 @@ TextContext::prefill_impl(std::span ids, const TextPrefill* text_pref Tensor next_token = io_.mtp->draft_tokens.slice(0, i, 1); Tensor next_hidden = work_.alloc(DType::BF16, {kCfg.hidden, 1}); const auto ar_visible = static_cast(base_i + T + i); - const ops::CausalAttentionExecutionEnvelope ar_envelope{ar_visible, + const ops::GqaExecutionEnvelope ar_envelope{ar_visible, ar_visible}; mtp_forward_ar_step(prev_token, io_.mtp->ar_hidden, ar_position, ar_envelope, next_hidden, logits, next_token); diff --git a/src/targets/qwen3_6/impl/runtime/text_prefill_impl.h b/src/targets/qwen3_6/impl/runtime/text_prefill_impl.h index 40ccda9d3f..4df3fca3e2 100644 --- a/src/targets/qwen3_6/impl/runtime/text_prefill_impl.h +++ b/src/targets/qwen3_6/impl/runtime/text_prefill_impl.h @@ -14,6 +14,27 @@ namespace ninfer::targets::qwen3_6::detail::NINFER_QWEN36_RUNTIME_NS::schedule { namespace { DFlashFeatureSink make_dflash_prefill_sink(PrefillContext& state) { + if (state.execution.model.features.dflash2()) { + if (!state.execution.io.dflash_decode || state.dflash2_host_ingress == nullptr) { + throw std::logic_error("DFlash2 prefill controls are unavailable"); + } + return dflash2_feature_sink( + state, [&state](const Tensor& features, const Tensor& positions, + bool rewrite_checkpoint) { + auto& frame = *state.execution.io.dflash_decode; + Tensor count = frame.append_counts.slice(0, 0, 1); + Tensor lane = frame.state_destination_slots.slice(0, 0, 1); + Tensor row = frame.dflash_kv_table_rows.slice(0, 0, 1); + ops::set_i32_scalar(count, features.ne[1], state.execution.device.stream); + const auto exact = static_cast(features.ne[1]); + dflash2_append_context(state, features, positions, count, lane, row, {exact, exact}); + if (rewrite_checkpoint) { + state.dflash2->save_rewrite_checkpoint( + state.dflash2_host_ingress->active_lanes[0], + state.execution.device.stream); + } + }); + } if (!state.execution.io.dflash_decode || state.dflash_host_ingress == nullptr) { throw std::logic_error("DFlash prefill controls are unavailable"); } @@ -63,7 +84,7 @@ PrefillChunkResult prefill_text_chunk(PrefillContext& state, std::span(*split_frontier) : -1); const std::span prompt(ids.data(), ids.size()); - if (state.dflash != nullptr) { + if (state.dflash != nullptr || state.dflash2 != nullptr) { DFlashFeatureSink sink = make_dflash_prefill_sink(state); return card.prefill_chunk(prompt, state.text_kv_base, nominal_length, finalize_at_end, sink); @@ -76,7 +97,7 @@ PrefillChunkResult prefill_multimodal_chunk(PrefillContext& state, const Prepare std::uint32_t nominal_length, std::optional split_frontier, bool finalize_at_end) { - if (state.dflash != nullptr) { + if (state.dflash != nullptr || state.dflash2 != nullptr) { throw std::logic_error("DFlash staged multimodal prefill is unavailable"); } TextContext card(state.execution.device, state.execution.model, state.execution.work, diff --git a/src/targets/qwen3_6/impl/runtime/workspace_recipe.h b/src/targets/qwen3_6/impl/runtime/workspace_recipe.h index 576d049439..551fe49561 100644 --- a/src/targets/qwen3_6/impl/runtime/workspace_recipe.h +++ b/src/targets/qwen3_6/impl/runtime/workspace_recipe.h @@ -252,10 +252,15 @@ struct DFlashAttentionRoots { Tensor query; Tensor key; Tensor attention; + Tensor attention_delta; }; template DFlashAttentionRoots dflash_attention(Allocator& allocator, std::int32_t tokens) { + Tensor attention_delta; + if constexpr (Config::bf16_weights) { + attention_delta = matrix(allocator, DType::BF16, Config::hidden, tokens); + } return { matrix(allocator, DType::BF16, Config::hidden, tokens), matrix(allocator, DType::BF16, Config::query_size, tokens), @@ -264,19 +269,30 @@ DFlashAttentionRoots dflash_attention(Allocator& allocator, std::int32_t tokens) matrix(allocator, DType::BF16, Config::query_size, tokens), matrix(allocator, DType::BF16, Config::kv_size, tokens), matrix(allocator, DType::BF16, Config::query_size, tokens), + attention_delta, }; } struct DFlashMlpRoots { Tensor hidden; + Tensor gate_up; Tensor intermediate; + Tensor delta; }; template DFlashMlpRoots dflash_mlp(Allocator& allocator, std::int32_t tokens) { + Tensor gate_up; + Tensor delta; + if constexpr (Config::bf16_weights) { + gate_up = matrix(allocator, DType::BF16, 2 * Config::intermediate, tokens); + delta = matrix(allocator, DType::BF16, Config::hidden, tokens); + } return { matrix(allocator, DType::BF16, Config::hidden, tokens), + gate_up, matrix(allocator, DType::BF16, Config::intermediate, tokens), + delta, }; } diff --git a/src/targets/qwen3_6/impl/state/decoder_state.cpp b/src/targets/qwen3_6/impl/state/decoder_state.cpp index 5e7372df45..b39b410f2d 100644 --- a/src/targets/qwen3_6/impl/state/decoder_state.cpp +++ b/src/targets/qwen3_6/impl/state/decoder_state.cpp @@ -1,6 +1,9 @@ #include +#include "ninfer/ops/cold_i8.h" +#include "ninfer/ops/entropy_nvfp4_slot.h" #include +#include #include #include @@ -14,37 +17,89 @@ std::uint32_t page_count(std::uint32_t capacity) { PagedKVCacheLayout plan_cache(LayoutBuilder& builder, std::uint32_t layers, std::uint32_t capacity, std::int32_t kv_heads, std::int32_t head_dim, DType dtype, - std::int32_t quant_group, std::int32_t table_rows, - std::uint32_t physical_page_groups) { + std::int32_t quant_group, std::span layer_dtypes, + std::int32_t table_rows, std::uint32_t physical_page_groups) { if (layers == 0 || layers > static_cast(std::numeric_limits::max()) || kv_heads <= 0 || head_dim <= 0 || table_rows <= 0) { throw std::invalid_argument("Paged KV cache geometry is invalid"); } - const bool scaled = dtype == DType::I8 || dtype == DType::FP8_E4M3FN; - const bool valid_profile = - (dtype == DType::BF16 && quant_group == 0) || - (dtype == DType::I8 && quant_group == kKvInt8QuantGroup && head_dim % quant_group == 0) || - (dtype == DType::FP8_E4M3FN && head_dim == kKvFp8QuantGroup && - quant_group == kKvFp8QuantGroup); - if (!valid_profile) { - throw std::invalid_argument("Paged KV cache dtype or quantization is invalid"); + if (!layer_dtypes.empty() && layer_dtypes.size() < layers) { + throw std::invalid_argument("Paged KV per-layer dtype table is shorter than the layer count"); + } + // Per-layer resolution: BF16 entries inherit the global dtype. Accepted + // per-layer storages are the quantized codecs with their native group. + const auto layer_dtype = [&](std::uint32_t layer) { + const DType override_dtype = layer_dtypes.empty() ? DType::BF16 : layer_dtypes[layer]; + const DType selected = override_dtype == DType::BF16 ? dtype : override_dtype; + if (selected != DType::BF16 && selected != DType::I8 && selected != DType::NVFP4 && + selected != DType::FP8_E4M3FN) { + throw std::invalid_argument("Paged KV per-layer dtype is invalid"); + } + return selected; + }; + const auto layer_quant_group = [&](DType selected) { + return selected == DType::I8 + ? kKvInt8QuantGroup + : (selected == DType::BF16 + ? 0 + : (selected == DType::FP8_E4M3FN ? kKvFp8QuantGroup + : kNvfp4KvQuantGroup)); + }; + (void)quant_group; + for (std::uint32_t layer = 0; layer < layers; ++layer) { + const DType selected = layer_dtype(layer); + if (selected != DType::BF16) { + const std::int32_t group = layer_quant_group(selected); + if (head_dim % group != 0) { + throw std::invalid_argument("Paged KV per-layer quantization is invalid"); + } + } } const std::uint32_t logical_pages = page_count(capacity); - if (physical_page_groups < logical_pages) { - throw std::invalid_argument("Paged KV physical pages are below logical capacity"); + // An explicit --kv-capacity may floor the device page pool below max_context: + // the rope domain (4x under YaRN) can exceed the pool, and the cold pool + // recycles committed pages under pressure so the context keeps growing. + if (physical_page_groups == 0) { + throw std::invalid_argument("Paged KV physical page capacity is zero"); } KVPageGeometry geometry; - geometry.planes.reserve(static_cast(layers) * (scaled ? 4ULL : 2ULL)); + geometry.planes.reserve(static_cast(layers) * 4ULL); + std::array stored{}; + std::array plane_base{}; + std::uint32_t plane_cursor = 0; for (std::uint32_t layer = 0; layer < layers; ++layer) { - geometry.planes.push_back({dtype, head_dim, kv_heads, 256}); - geometry.planes.push_back({dtype, head_dim, kv_heads, 256}); - if (scaled) { - geometry.planes.push_back({DType::FP16, head_dim / quant_group, kv_heads, 256}); - geometry.planes.push_back({DType::FP16, head_dim / quant_group, kv_heads, 256}); + const DType selected = layer_dtype(layer); + stored[layer] = selected; + plane_base[layer] = plane_cursor; + const std::int32_t group = layer_quant_group(selected); + if (selected == DType::BF16) { + geometry.planes.push_back({DType::BF16, head_dim, kv_heads, 256}); + geometry.planes.push_back({DType::BF16, head_dim, kv_heads, 256}); + } else if (selected == DType::I8) { + geometry.planes.push_back({DType::I8, head_dim, kv_heads, 256}); + geometry.planes.push_back({DType::I8, head_dim, kv_heads, 256}); + geometry.planes.push_back({DType::FP16, head_dim / group, kv_heads, 256}); + geometry.planes.push_back({DType::FP16, head_dim / group, kv_heads, 256}); + } else if (selected == DType::FP8_E4M3FN) { + geometry.planes.push_back({DType::FP8_E4M3FN, head_dim, kv_heads, 256}); + geometry.planes.push_back({DType::FP8_E4M3FN, head_dim, kv_heads, 256}); + // FP8 per-group scales are FP16 in the production codecs; the + // attention kernels require FP16 scale planes. + geometry.planes.push_back({DType::FP16, head_dim / group, kv_heads, 256}); + geometry.planes.push_back({DType::FP16, head_dim / group, kv_heads, 256}); + } else { + // NVFP4 tier: K keeps E2M1 packed codes with per-16 E4M3FN scales; + // V stores ISO3 sign-magnitude nibbles in the same plane geometry + // (semantic split via v_dtype, no extra payload). + geometry.planes.push_back({DType::U8, head_dim / 2, kv_heads, 256}); + geometry.planes.push_back({DType::U8, head_dim / 2, kv_heads, 256}); + geometry.planes.push_back({DType::FP8_E4M3FN, head_dim / group, kv_heads, 256}); + geometry.planes.push_back({DType::FP8_E4M3FN, head_dim / group, kv_heads, 256}); } + plane_cursor += selected == DType::BF16 ? 2U : 4U; } return PagedKVCacheLayout{ .pages = plan_device_kv_page_pool( @@ -59,6 +114,8 @@ PagedKVCacheLayout plan_cache(LayoutBuilder& builder, std::uint32_t layers, std: .head_dim = head_dim, .dtype = dtype, .quant_group = quant_group, + .layer_dtypes = stored, + .layer_plane_base = plane_base, }; } @@ -66,13 +123,37 @@ PagedKVCacheLayout plan_cache(LayoutBuilder& builder, std::uint32_t layers, std: DecoderStateLayout plan_decoder_state(LayoutBuilder& builder, const DecoderStateSpec& spec) { DecoderStateLayout layout; + const std::span layer_dtypes(spec.layer_kv_dtypes.data(), + spec.full_attention_layers); layout.text_kv = plan_cache(builder, spec.full_attention_layers, spec.capacity, spec.kv_heads, spec.attention_head_dim, spec.kv_dtype, spec.kv_quant_group, - spec.kv_table_rows, spec.text_physical_page_groups); + layer_dtypes, spec.kv_table_rows, + spec.text_physical_page_groups); if (spec.enable_mtp) { layout.mtp_kv = plan_cache(builder, spec.mtp_layers, spec.capacity, spec.kv_heads, spec.attention_head_dim, spec.kv_dtype, spec.kv_quant_group, - spec.kv_table_rows, spec.mtp_physical_page_groups); + {}, spec.kv_table_rows, spec.mtp_physical_page_groups); + } + // Cold pool slots: one fixed-size slot per (page, kv_head, plane). The + // INT8 tier packs requantized E2M1 nibbles into 9232 B raw slots; the + // NVFP4 tier rANS-encodes the same geometry into slots with a 320 B + // header, 32 x 256 B stream budget, and a 1024 B scale tail. One buffer + // size serves both: 9536 B covers the rANS max; the raw I8 layout keeps + // its offsets from slot start and simply leaves the tail unused. + const std::int32_t slot_bytes = ops::kEntropyNvfp4SlotBytes; + if (spec.max_cold_pages != 0) { + const std::uint32_t cold_pages = spec.max_cold_pages; + layout.text_kv.slot_bytes = slot_bytes; + layout.text_kv.max_cold_pages = spec.max_cold_pages; + for (std::uint32_t layer = 0; layer < spec.full_attention_layers; ++layer) { + layout.text_kv.cold_slots[layer] = builder.add_tensor( + DType::U8, {slot_bytes, static_cast(spec.kv_heads), + 2, cold_pages}, + 256, "cold slots L" + std::to_string(layer)); + layout.text_kv.cold_slot_valid[layer] = builder.add_tensor( + DType::I32, {static_cast(spec.kv_heads), 2, cold_pages}, 256, + "cold slot valid L" + std::to_string(layer)); + } } return layout; } @@ -80,11 +161,38 @@ DecoderStateLayout plan_decoder_state(LayoutBuilder& builder, const DecoderState PagedKVCache::PagedKVCache(DeviceSpan backing, const PagedKVCacheLayout& layout) : pages_(backing, layout.pages), execution_tables_(backing, layout.execution_tables, pages_), layers_(layout.layers), max_context_(layout.max_context), kv_heads_(layout.kv_heads), - head_dim_(layout.head_dim), dtype_(layout.dtype), quant_group_(layout.quant_group) {} + head_dim_(layout.head_dim), dtype_(layout.dtype), quant_group_(layout.quant_group), + slot_bytes_(layout.slot_bytes), max_cold_pages_(layout.max_cold_pages), + layer_dtypes_(layout.layer_dtypes), layer_plane_base_(layout.layer_plane_base) { + cold_slot_used_.assign(max_cold_pages_, 0); + for (std::uint32_t layer = 0; layer < layers_; ++layer) { + if (layout.cold_slots[layer].region.bytes != 0) { + cold_slots_[layer] = layout.cold_slots[layer].bind(backing); + cold_slot_valid_[layer] = layout.cold_slot_valid[layer].bind(backing); + } + } +} PagedKVCacheView::PagedKVCacheView(const PagedKVCache& cache, Tensor block_table) noexcept : cache_(&cache), block_table_(block_table) {} +std::int32_t PagedKVCache::allocate_cold_slot() noexcept { + if (max_cold_pages_ == 0) { return -1; } + for (std::uint32_t slot = 0; slot < max_cold_pages_; ++slot) { + if (!cold_slot_used_[slot]) { + cold_slot_used_[slot] = true; + return static_cast(slot); + } + } + return -1; +} + +void PagedKVCache::release_cold_slot(std::int32_t slot) noexcept { + if (slot >= 0 && static_cast(slot) < max_cold_pages_) { + cold_slot_used_[slot] = false; + } +} + std::uint32_t PagedKVCacheView::max_context() const noexcept { return cache_ == nullptr ? 0 : cache_->max_context(); } @@ -103,37 +211,89 @@ PagedKVCacheView PagedKVCache::execution_view(const KVExecutionRowLease& row) co PagedKVLayerView PagedKVCache::layer_view(std::uint32_t layer, Tensor block_table) const { if (layer >= layers_) { throw std::out_of_range("Paged KV layer is out of range"); } - const bool scaled = dtype_ == DType::I8 || dtype_ == DType::FP8_E4M3FN; - const std::size_t stride = scaled ? 4ULL : 2ULL; - const std::size_t base = static_cast(layer) * stride; + // The scaled/stride decision follows the layer's resolved dtype so a + // per-layer table (PR1) can mix quantized and BF16 layers in one pool. + const DType layer_dtype = + layer_dtypes_.empty() ? dtype_ : layer_dtypes_[layer]; + const bool scaled = layer_dtype == DType::I8 || layer_dtype == DType::FP8_E4M3FN || + layer_dtype == DType::NVFP4; + const std::size_t base = layer_plane_base_.empty() + ? static_cast(layer) * (scaled ? 4ULL : 2ULL) + : layer_plane_base_[layer]; return PagedKVLayerView{ .k_pages = pages_.plane(base), .v_pages = pages_.plane(base + 1), .k_scale_pages = scaled ? pages_.plane(base + 2) : Tensor(), .v_scale_pages = scaled ? pages_.plane(base + 3) : Tensor(), .block_table = block_table, + .cold_slots = cold_slots_[layer], + .cold_slot_valid = cold_slot_valid_[layer], + .slot_bytes = slot_bytes_, .head_dim = head_dim_, .num_kv_heads = kv_heads_, - .dtype = dtype_, - .quant_group = quant_group_, + .layer_index = static_cast(layer), + .dtype = layer_dtypes_.empty() ? dtype_ : layer_dtypes_[layer], + .quant_group = layer_dtypes_.empty() + ? quant_group_ + : (layer_dtypes_[layer] == DType::I8 + ? kKvInt8QuantGroup + : (layer_dtypes_[layer] == DType::FP8_E4M3FN + ? kKvFp8QuantGroup + : (layer_dtypes_[layer] == DType::NVFP4 + ? kNvfp4KvQuantGroup + : 0))), + .v_dtype = layer_dtypes_.empty() + ? dtype_ + : (layer_dtypes_[layer] == DType::NVFP4 ? DType::ISO3 + : layer_dtypes_[layer]), + .v_quant_group = layer_dtypes_.empty() + ? quant_group_ + : (layer_dtypes_[layer] == DType::NVFP4 ? kNvfp4KvQuantGroup + : 0), }; } PagedKVBatchLayerView PagedKVCache::batch_layer_view(std::uint32_t layer) const { if (layer >= layers_) { throw std::out_of_range("Paged KV layer is out of range"); } - const bool scaled = dtype_ == DType::I8 || dtype_ == DType::FP8_E4M3FN; - const std::size_t stride = scaled ? 4ULL : 2ULL; - const std::size_t base = static_cast(layer) * stride; + // The scaled/stride decision follows the layer's resolved dtype so a + // per-layer table (PR1) can mix quantized and BF16 layers in one pool. + const DType layer_dtype = + layer_dtypes_.empty() ? dtype_ : layer_dtypes_[layer]; + const bool scaled = layer_dtype == DType::I8 || layer_dtype == DType::FP8_E4M3FN || + layer_dtype == DType::NVFP4; + const std::size_t base = layer_plane_base_.empty() + ? static_cast(layer) * (scaled ? 4ULL : 2ULL) + : layer_plane_base_[layer]; return PagedKVBatchLayerView{ .k_pages = pages_.plane(base), .v_pages = pages_.plane(base + 1), .k_scale_pages = scaled ? pages_.plane(base + 2) : Tensor(), .v_scale_pages = scaled ? pages_.plane(base + 3) : Tensor(), .block_tables = execution_tables_.matrix(), + .cold_slots = cold_slots_[layer], + .cold_slot_valid = cold_slot_valid_[layer], + .slot_bytes = slot_bytes_, .head_dim = head_dim_, .num_kv_heads = kv_heads_, - .dtype = dtype_, - .quant_group = quant_group_, + .layer_index = static_cast(layer), + .dtype = layer_dtypes_.empty() ? dtype_ : layer_dtypes_[layer], + .quant_group = layer_dtypes_.empty() + ? quant_group_ + : (layer_dtypes_[layer] == DType::I8 + ? kKvInt8QuantGroup + : (layer_dtypes_[layer] == DType::FP8_E4M3FN + ? kKvFp8QuantGroup + : (layer_dtypes_[layer] == DType::NVFP4 + ? kNvfp4KvQuantGroup + : 0))), + .v_dtype = layer_dtypes_.empty() + ? dtype_ + : (layer_dtypes_[layer] == DType::NVFP4 ? DType::ISO3 + : layer_dtypes_[layer]), + .v_quant_group = layer_dtypes_.empty() + ? quant_group_ + : (layer_dtypes_[layer] == DType::NVFP4 ? kNvfp4KvQuantGroup + : 0), }; } diff --git a/src/targets/qwen3_6/impl/state/round_state.cpp b/src/targets/qwen3_6/impl/state/round_state.cpp index b8a719a9fd..a8d441e188 100644 --- a/src/targets/qwen3_6/impl/state/round_state.cpp +++ b/src/targets/qwen3_6/impl/state/round_state.cpp @@ -338,6 +338,8 @@ DFlashDecodeState::DFlashDecodeState(DeviceSpan backing, const DFlashDecodeState egress_tensor(offsetof(DFlashDecodeEgress, licensed_counts), DType::I32, {batch}); accepted_drafts = egress_tensor(offsetof(DFlashDecodeEgress, accepted_drafts), DType::I32, {batch}); + egress_proposal_extents = + egress_tensor(offsetof(DFlashDecodeEgress, proposal_extents), DType::I32, {batch}); proposal_ids = layout.proposal_ids.bind(backing); proposal_positions = layout.proposal_positions.bind(backing); append_positions = layout.append_positions.bind(backing); diff --git a/src/targets/qwen3_6_27b/export/ninfer/targets/qwen3_6_27b/package.h b/src/targets/qwen3_6_27b/export/ninfer/targets/qwen3_6_27b/package.h index b9f1a33799..314699696f 100644 --- a/src/targets/qwen3_6_27b/export/ninfer/targets/qwen3_6_27b/package.h +++ b/src/targets/qwen3_6_27b/export/ninfer/targets/qwen3_6_27b/package.h @@ -33,6 +33,7 @@ enum class WeightsProfile : std::uint8_t { Qwen38GroupwiseInt, Qwen36Nvfp4, Qwen38Nvfp4, + Qwen38Nvfp4DFlash2, }; using Frontend = qwen3_6::Frontend; @@ -123,6 +124,10 @@ struct Package { [[nodiscard]] static ModelSamplingDefaults sampling_defaults(std::string_view model); [[nodiscard]] static WeightsProfile resolve_weights(const artifact::ArtifactIdentity& identity); + // Resolves --spec auto to a concrete backend (MTP or DFlash2) using the + // artifact weights profile and context budget. + [[nodiscard]] static EngineOptions resolved_auto_speculative(const EngineOptions& options, + WeightsProfile weights_profile); [[nodiscard]] static LoadPlan plan_load(artifact::Binder& binder, const EngineOptions& options, WeightsProfile weights_profile); [[nodiscard]] static std::unique_ptr diff --git a/src/targets/qwen3_6_27b/impl/config.h b/src/targets/qwen3_6_27b/impl/config.h index 5c30aa1896..dcce9cdba3 100644 --- a/src/targets/qwen3_6_27b/impl/config.h +++ b/src/targets/qwen3_6_27b/impl/config.h @@ -1,94 +1,148 @@ -#pragma once - -#include -#include -#include - -#include - -namespace ninfer::targets::qwen3_6_27b::detail { - -struct TextConfig { - static constexpr int hidden = 5120; - static constexpr int layers = 64; - static constexpr int intermediate = 17408; - - // The output matrix is padded for the selected kernels. Only token IDs in - // [0, token_domain) are tokenizer-addressable and valid sampling results. - static constexpr int output_rows = 248320; - static constexpr int token_domain = static_cast(qwen3_6::kTokenDomain); - - static constexpr int gdn_conv_kernel = 4; - static constexpr int gdn_conv_state_width = gdn_conv_kernel - 1; - static constexpr int gdn_key_heads = 16; - static constexpr int gdn_key_head_dim = 128; - static constexpr int gdn_value_heads = 48; - static constexpr int gdn_value_head_dim = 128; - - static constexpr int query_heads = 24; - static constexpr int kv_heads = 4; - static constexpr int head_dim = 256; - static constexpr int rotary_dim = 64; - - static constexpr int full_attention_interval = qwen3_6::kHybridAttentionInterval; - static constexpr float rms_epsilon = 1.0e-6F; - static constexpr float rope_theta = 1.0e7F; - - static constexpr int key_dim = gdn_key_heads * gdn_key_head_dim; - static constexpr int value_dim = gdn_value_heads * gdn_value_head_dim; - static constexpr int convolution_dim = 2 * key_dim + value_dim; - static constexpr int query_size = query_heads * head_dim; - static constexpr int kv_size = kv_heads * head_dim; - static constexpr int query_projection_rows = 2 * query_size; - - static constexpr int mtp_layers = 1; - static constexpr int mtp_input_rows = 2 * hidden; - static constexpr int mtp_attention_input_rows = 2 * query_size + 2 * kv_size; - static constexpr int mtp_mlp_gate_up_rows = 2 * intermediate; - - [[nodiscard]] static constexpr bool is_full_attention(int layer) { - return qwen3_6::is_full_attention_layer(layer); - } - - [[nodiscard]] static constexpr int full_attention_layers() { - return qwen3_6::full_attention_layers(layers); - } - - [[nodiscard]] static constexpr int gdn_layers() { return qwen3_6::gdn_layers(layers); } - - [[nodiscard]] static constexpr int full_attention_index(int layer) { - return qwen3_6::full_attention_index(layer); - } - - [[nodiscard]] static constexpr int gdn_index(int layer) { return qwen3_6::gdn_index(layer); } -}; - -static_assert(TextConfig::full_attention_layers() == 16); -static_assert(TextConfig::gdn_layers() == 48); - -struct VisionConfig : qwen3_6::VisionBackboneConfig { - static constexpr int output_hidden = TextConfig::hidden; -}; - -struct DFlashConfig { - static constexpr bool supported = false; - static constexpr int local_layers = 0; - static constexpr int local_capacity = 0; - static constexpr int query_heads = 0; - static constexpr int kv_heads = 0; - static constexpr int head_dim = 0; - static constexpr int feature_rows = 0; - static constexpr int hidden = 0; - static constexpr int intermediate = 0; - static constexpr int query_size = 0; - static constexpr int kv_size = 0; -}; - -inline constexpr float kAttentionScale = 0.0625F; -inline constexpr float kGdnScale = 0.08838834764831845F; -inline constexpr std::uint32_t kPrefillChunkAlignment = 128; -inline constexpr std::uint32_t kMaximumMtpDraftTokens = 5; -inline constexpr std::uint32_t kMaximumDFlashDraftTokens = 0; -inline constexpr std::uint32_t kNativeContext = 262144; - -} // namespace ninfer::targets::qwen3_6_27b::detail +#pragma once + +#include +#include +#include + +#include +#include + +namespace ninfer::targets::qwen3_6_27b::detail { + +struct TextConfig { + static constexpr int hidden = 5120; + static constexpr int layers = 64; + static constexpr int intermediate = 17408; + + // The output matrix is padded for the selected kernels. Only token IDs in + // [0, token_domain) are tokenizer-addressable and valid sampling results. + static constexpr int output_rows = 248320; + static constexpr int token_domain = static_cast(qwen3_6::kTokenDomain); + + static constexpr int gdn_conv_kernel = 4; + static constexpr int gdn_conv_state_width = gdn_conv_kernel - 1; + static constexpr int gdn_key_heads = 16; + static constexpr int gdn_key_head_dim = 128; + static constexpr int gdn_value_heads = 48; + static constexpr int gdn_value_head_dim = 128; + + static constexpr int query_heads = 24; + static constexpr int kv_heads = 4; + static constexpr int head_dim = 256; + static constexpr int rotary_dim = 64; + + static constexpr int full_attention_interval = qwen3_6::kHybridAttentionInterval; + static constexpr float rms_epsilon = 1.0e-6F; + static constexpr float rope_theta = 1.0e7F; + + static constexpr int key_dim = gdn_key_heads * gdn_key_head_dim; + static constexpr int value_dim = gdn_value_heads * gdn_value_head_dim; + static constexpr int convolution_dim = 2 * key_dim + value_dim; + static constexpr int query_size = query_heads * head_dim; + static constexpr int kv_size = kv_heads * head_dim; + static constexpr int query_projection_rows = 2 * query_size; + + static constexpr int mtp_layers = 1; + static constexpr int mtp_input_rows = 2 * hidden; + static constexpr int mtp_attention_input_rows = 2 * query_size + 2 * kv_size; + static constexpr int mtp_mlp_gate_up_rows = 2 * intermediate; + + [[nodiscard]] static constexpr bool is_full_attention(int layer) { + return qwen3_6::is_full_attention_layer(layer); + } + + [[nodiscard]] static constexpr int full_attention_layers() { + return qwen3_6::full_attention_layers(layers); + } + + [[nodiscard]] static constexpr int gdn_layers() { return qwen3_6::gdn_layers(layers); } + + [[nodiscard]] static constexpr int full_attention_index(int layer) { + return qwen3_6::full_attention_index(layer); + } + + [[nodiscard]] static constexpr int gdn_index(int layer) { return qwen3_6::gdn_index(layer); } +}; + +static_assert(TextConfig::full_attention_layers() == 16); +static_assert(TextConfig::gdn_layers() == 48); + +struct VisionConfig : qwen3_6::VisionBackboneConfig { + static constexpr int output_hidden = TextConfig::hidden; +}; + +struct DFlashConfig { + static constexpr bool supported = true; + // DSpark draft weights ship as BF16 (not W8 like the 35B DFlash draft), + // so the runtime uses the generic BF16 MMA GEMM path for projections. + static constexpr bool bf16_weights = true; + static constexpr int layers = 5; + // DSpark has no sliding-window local layers: every layer uses the full + // dual-source (target-feature context + noise rows) attention path. + // A single legacy local cyclic layer is still allocated because the + // cyclic-cache layout requires layers >= 1; full_only keeps it unused. + static constexpr bool full_only = true; + static constexpr int local_layers = 1; + static constexpr int full_layers = full_only ? layers : layers - local_layers; + static constexpr int feature_layers = 5; + static constexpr int feature_rows = feature_layers * TextConfig::hidden; + static constexpr int hidden = TextConfig::hidden; + static constexpr int intermediate = 10240; + static constexpr int query_heads = 40; + static constexpr int kv_heads = 8; + static constexpr int head_dim = 128; + static constexpr int query_size = query_heads * head_dim; + static constexpr int kv_size = kv_heads * head_dim; + static constexpr int local_capacity = 4096; + static constexpr std::uint32_t local_window = 4096; + static constexpr int mask_token = 248077; + static constexpr float rms_epsilon = 1.0e-6F; + static constexpr float rope_theta = 1.0e7F; + static constexpr float attention_scale = 0.08838834764831845F; + // SVIP self-verification length policy: stop drafting after the first + // position whose base-logit entropy exceeds threshold^2. + static constexpr float svip_entropy_threshold = 2.5F; + static constexpr std::array target_feature_layers{4, 16, 28, 40, 52}; +}; + +// DFlash2 block-diffusion draft: five sliding-window (2048) non-causal layers, +// grouped convolutions, and a rank-256 candidate selector with a 16-way path +// walk. The block always drafts seven tokens after the bonus token. +struct DFlash2Config { + static constexpr bool supported = true; + static constexpr bool bf16_weights = true; + static constexpr int layers = 5; + static constexpr bool full_only = false; + static constexpr int local_layers = 5; + static constexpr int full_layers = full_only ? layers : layers - local_layers; + static constexpr int feature_layers = 5; + static constexpr int feature_rows = feature_layers * TextConfig::hidden; + static constexpr int hidden = TextConfig::hidden; + static constexpr int intermediate = 17408; + static constexpr int query_heads = 32; + static constexpr int kv_heads = 8; + static constexpr int head_dim = 128; + static constexpr int query_size = query_heads * head_dim; + static constexpr int kv_size = kv_heads * head_dim; + static constexpr int local_capacity = 2048; + static constexpr std::uint32_t local_window = 2048; + static constexpr int mask_token = 248070; + static constexpr float rms_epsilon = 1.0e-6F; + static constexpr float rope_theta = 1.0e7F; + static constexpr float attention_scale = 0.08838834764831845F; + static constexpr int block_drafts = 7; + static constexpr int conv_group_size = 16; + static constexpr int conv_kernel_size = 2; + static constexpr int selector_rank = 256; + static constexpr int selector_top_k = 16; + static constexpr std::array target_feature_layers{5, 19, 33, 47, 61}; +}; + +inline constexpr float kAttentionScale = 0.0625F; +inline constexpr float kGdnScale = 0.08838834764831845F; +inline constexpr std::uint32_t kPrefillChunkAlignment = 128; +inline constexpr std::uint32_t kMaximumMtpDraftTokens = 5; +inline constexpr std::uint32_t kMaximumDFlashDraftTokens = 7; +inline constexpr std::uint32_t kNativeContext = 262144; + +} // namespace ninfer::targets::qwen3_6_27b::detail diff --git a/src/targets/qwen3_6_27b/impl/load/bindings.cpp b/src/targets/qwen3_6_27b/impl/load/bindings.cpp index f942a7976e..267245a9a0 100644 --- a/src/targets/qwen3_6_27b/impl/load/bindings.cpp +++ b/src/targets/qwen3_6_27b/impl/load/bindings.cpp @@ -40,6 +40,7 @@ NumericFormat endpoint_format(WeightsProfile weights_profile) { case WeightsProfile::Qwen36Nvfp4: return NumericFormat::W8G32_F16S; case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return NumericFormat::FP8_E4M3FN_ROW_BF16S; } throw std::invalid_argument("qwen3_6_27b: invalid weights profile"); @@ -431,6 +432,9 @@ ArtifactLoadPlan bind_artifact(artifact::Binder& binder, WeightsProfile weights_ case WeightsProfile::Qwen38Nvfp4: bind_qwen38_nvfp4_text_layers(binder, out); break; + case WeightsProfile::Qwen38Nvfp4DFlash2: + bind_qwen38_nvfp4_text_layers(binder, out); + break; default: throw std::invalid_argument("qwen3_6_27b: invalid weights profile"); } @@ -474,6 +478,66 @@ ArtifactLoadPlan bind_artifact(artifact::Binder& binder, WeightsProfile weights_ .format = NumericFormat::W8G32_F16S}; out.mtp.final_norm = bind_mtp("mtp/final_norm", NumericFormat::BF16, {5120}); + if (weights_profile == WeightsProfile::Qwen38Nvfp4DFlash2) { + const artifact::TensorPlacement dflash2_placement = + features.dflash2() ? artifact::TensorPlacement::Device + : artifact::TensorPlacement::ValidateOnly; + const auto bind_dflash2 = [&](std::string_view name, NumericFormat format, + std::initializer_list shape) { + return artifact::bind_tensor(binder, name, format, shape, dflash2_placement); + }; + const auto bind_dflash2_weight = [&](std::string_view name, NumericFormat format, + std::initializer_list shape) { + return WeightPlan{.object = bind_dflash2(name, format, shape), .format = format}; + }; + out.dflash2.feature_projection = bind_dflash2_weight("dflash2/feature_projection", + NumericFormat::BF16, {5120, 25600}); + out.dflash2.context_norm = + bind_dflash2("dflash2/context_norm", NumericFormat::BF16, {5120}); + for (std::size_t layer = 0; layer < DFlash2Config::layers; ++layer) { + DFlash2LayerPlan& target = out.dflash2.layers[layer]; + const std::string prefix = "dflash2/layers/" + std::to_string(layer) + "/"; + target.input_norm = bind_dflash2(prefix + "input_norm", NumericFormat::BF16, {5120}); + target.query_key_value = bind_dflash2_weight( + prefix + "attention/query_key_value", NumericFormat::BF16, {6144, 5120}); + target.context_key = bind_dflash2_weight(prefix + "attention/context_key", + NumericFormat::BF16, {1024, 5120}); + target.context_value = bind_dflash2_weight(prefix + "attention/context_value", + NumericFormat::BF16, {1024, 5120}); + target.query_norm = + bind_dflash2(prefix + "attention/query_norm", NumericFormat::BF16, {128}); + target.key_norm = + bind_dflash2(prefix + "attention/key_norm", NumericFormat::BF16, {128}); + target.attention_output = bind_dflash2_weight(prefix + "attention/output", + NumericFormat::BF16, {5120, 4096}); + target.attention_conv_base = WeightPlan{ + .object = bind_dflash2(prefix + "attention_conv/base_kernel", NumericFormat::BF16, + {2, 2, 5120}), + .format = NumericFormat::BF16}; + target.attention_conv_projection = bind_dflash2_weight( + prefix + "attention_conv/kernel_projection", NumericFormat::BF16, {1280, 5120}); + target.post_attention_norm = + bind_dflash2(prefix + "post_attention_norm", NumericFormat::BF16, {5120}); + target.mlp.gate_up = bind_dflash2_weight(prefix + "mlp/gate_up", NumericFormat::BF16, + {34816, 5120}); + target.mlp.down = + bind_dflash2_weight(prefix + "mlp/down", NumericFormat::BF16, {5120, 17408}); + target.mlp_conv_base = WeightPlan{ + .object = bind_dflash2(prefix + "mlp_conv/base_kernel", NumericFormat::BF16, + {2, 2, 5120}), + .format = NumericFormat::BF16}; + target.mlp_conv_projection = bind_dflash2_weight( + prefix + "mlp_conv/kernel_projection", NumericFormat::BF16, {1280, 5120}); + } + out.dflash2.final_norm = bind_dflash2("dflash2/final_norm", NumericFormat::BF16, {5120}); + out.dflash2.selector_hidden_projection = bind_dflash2_weight( + "dflash2/candidate_selector/hidden_projection", NumericFormat::BF16, {256, 5120}); + out.dflash2.selector_predecessor_codebook = bind_dflash2_weight( + "dflash2/candidate_selector/predecessor_codebook", NumericFormat::BF16, {248320, 256}); + out.dflash2.selector_successor_codebook = bind_dflash2_weight( + "dflash2/candidate_selector/successor_codebook", NumericFormat::BF16, {248320, 256}); + } + const artifact::TensorPlacement vision_placement = features.vision ? artifact::TensorPlacement::Device : artifact::TensorPlacement::ValidateOnly; @@ -582,6 +646,50 @@ LoadedModelData::LoadedModelData(BindingPlan plan, artifact::MaterializedArtifac NumericFormat::BF16, {5120}); } + if (plan.features.dflash2()) { + DFlash2Weights& target = runtime.dflash2.emplace(); + target.feature_projection = materialized_weight(backing, plan.dflash2.feature_projection, + 5120, 25600); + target.context_norm = artifact::materialized_tensor(backing, plan.dflash2.context_norm, + NumericFormat::BF16, {5120}); + for (std::size_t layer = 0; layer < DFlash2Config::layers; ++layer) { + const DFlash2LayerPlan& source = plan.dflash2.layers[layer]; + DFlash2LayerWeights& weights = target.layers[layer]; + weights.input_norm = artifact::materialized_tensor(backing, source.input_norm, + NumericFormat::BF16, {5120}); + weights.query_key_value = materialized_weight(backing, source.query_key_value, 6144, 5120); + weights.context_key = materialized_weight(backing, source.context_key, 1024, 5120); + weights.context_value = + materialized_weight(backing, source.context_value, 1024, 5120); + weights.query_norm = artifact::materialized_tensor(backing, source.query_norm, + NumericFormat::BF16, {128}); + weights.key_norm = + artifact::materialized_tensor(backing, source.key_norm, NumericFormat::BF16, {128}); + weights.attention_output = + materialized_weight(backing, source.attention_output, 5120, 4096); + weights.attention_conv_base = artifact::materialized_weight( + backing, source.attention_conv_base.object, NumericFormat::BF16, 4, 5120); + weights.attention_conv_projection = + materialized_weight(backing, source.attention_conv_projection, 1280, 5120); + weights.post_attention_norm = artifact::materialized_tensor( + backing, source.post_attention_norm, NumericFormat::BF16, {5120}); + weights.gate_up = materialized_weight(backing, source.mlp.gate_up, 34816, 5120); + weights.down = materialized_weight(backing, source.mlp.down, 5120, 17408); + weights.mlp_conv_base = artifact::materialized_weight( + backing, source.mlp_conv_base.object, NumericFormat::BF16, 4, 5120); + weights.mlp_conv_projection = + materialized_weight(backing, source.mlp_conv_projection, 1280, 5120); + } + target.final_norm = artifact::materialized_tensor(backing, plan.dflash2.final_norm, + NumericFormat::BF16, {5120}); + target.selector_hidden_projection = materialized_weight( + backing, plan.dflash2.selector_hidden_projection, 256, 5120); + target.selector_predecessor_codebook = materialized_weight( + backing, plan.dflash2.selector_predecessor_codebook, 248320, 256); + target.selector_successor_codebook = materialized_weight( + backing, plan.dflash2.selector_successor_codebook, 248320, 256); + } + if (plan.features.vision) { auto& vision = runtime.vision.emplace(); vision.common = qwen3_6::materialize_vision_common( diff --git a/src/targets/qwen3_6_27b/impl/load/bindings.h b/src/targets/qwen3_6_27b/impl/load/bindings.h index 70a14140fe..85a0394683 100644 --- a/src/targets/qwen3_6_27b/impl/load/bindings.h +++ b/src/targets/qwen3_6_27b/impl/load/bindings.h @@ -1,5 +1,6 @@ #pragma once +#include "targets/qwen3_6_27b/impl/config.h" #include #include #include @@ -104,6 +105,32 @@ struct MtpPlan { artifact::ObjectHandle final_norm; }; +struct DFlash2LayerPlan { + artifact::ObjectHandle input_norm; + WeightPlan query_key_value; + WeightPlan context_key; + WeightPlan context_value; + artifact::ObjectHandle query_norm; + artifact::ObjectHandle key_norm; + WeightPlan attention_output; + WeightPlan attention_conv_base; + WeightPlan attention_conv_projection; + artifact::ObjectHandle post_attention_norm; + MlpPlan mlp; + WeightPlan mlp_conv_base; + WeightPlan mlp_conv_projection; +}; + +struct DFlash2Plan { + WeightPlan feature_projection; + artifact::ObjectHandle context_norm; + std::array layers; + artifact::ObjectHandle final_norm; + WeightPlan selector_hidden_projection; + WeightPlan selector_predecessor_codebook; + WeightPlan selector_successor_codebook; +}; + struct BindingPlan { qwen3_6::FrontendResourcePlan frontend; qwen3_6::StartupFeatures features; @@ -115,6 +142,7 @@ struct BindingPlan { artifact::ObjectHandle draft_head; artifact::ObjectHandle draft_head_token_ids; MtpPlan mtp; + DFlash2Plan dflash2; qwen3_6::VisionBackbonePlan vision_backbone; qwen3_6::VisionMergerInputPlan vision_merger_input; @@ -190,10 +218,13 @@ struct MtpAttentionPayload { using RuntimeModelView = qwen3_6::ModelView, - kFullAttentionLayers, kGdnLayers>; + qwen3_6::DFlash2Weights, kFullAttentionLayers, + kGdnLayers>; using FullAttentionWeights = RuntimeModelView::FullLayer; using GdnWeights = RuntimeModelView::GdnLayer; using MtpWeights = RuntimeModelView::MtpLayer; +using DFlash2Weights = RuntimeModelView::DFlash2; +using DFlash2LayerWeights = qwen3_6::DFlash2LayerWeights; class LoadedModelData { public: diff --git a/src/targets/qwen3_6_27b/impl/package.cpp b/src/targets/qwen3_6_27b/impl/package.cpp index 67647994fe..75e6a1704a 100644 --- a/src/targets/qwen3_6_27b/impl/package.cpp +++ b/src/targets/qwen3_6_27b/impl/package.cpp @@ -95,15 +95,39 @@ Package::WeightsProfile Package::resolve_weights(const artifact::ArtifactIdentit if (identity.model_id == qwen3_8_model_id && identity.weights_id == "nvfp4") { return WeightsProfile::Qwen38Nvfp4; } + if (identity.model_id == qwen3_8_model_id && identity.weights_id == "nvfp4-dflash2") { + return WeightsProfile::Qwen38Nvfp4DFlash2; + } throw std::runtime_error("artifact identity '" + identity.model_id + "/" + identity.weights_id + "' is not supported by target '" + std::string(target_key) + "'"); } +EngineOptions Package::resolved_auto_speculative(const EngineOptions& options, + WeightsProfile weights_profile) { + EngineOptions resolved = options; + if (options.speculative.backend != SpeculativeBackend::Auto) { return resolved; } + // The artifact weights decide the backend, not the context length: a + // DFlash2 artifact has no MTP draft head (the two are mutually + // exclusive), so auto must pick DFlash2 even at long contexts - a memory + // shortfall then surfaces as a clear reservation error instead of a + // confusing weights mismatch. A non-DFlash2 artifact defaults to MTP. + if (weights_profile == detail::WeightsProfile::Qwen38Nvfp4DFlash2 && + !options.enable_vision) { + resolved.speculative.backend = SpeculativeBackend::DFlash2; + if (resolved.speculative.draft_tokens == 0) { resolved.speculative.draft_tokens = 7; } + } else { + resolved.speculative.backend = SpeculativeBackend::Mtp; + if (resolved.speculative.draft_tokens == 0) { resolved.speculative.draft_tokens = 3; } + } + return resolved; +} + Package::LoadPlan Package::plan_load(artifact::Binder& binder, const EngineOptions& options, WeightsProfile weights_profile) { + const EngineOptions resolved = Package::resolved_auto_speculative(options, weights_profile); return LoadPlan(std::make_unique( weights_profile, - detail::bind_artifact(binder, weights_profile, qwen3_6::startup_features(options)))); + detail::bind_artifact(binder, weights_profile, qwen3_6::startup_features(resolved)))); } std::unique_ptr diff --git a/src/targets/qwen3_6_27b/impl/variant.cpp b/src/targets/qwen3_6_27b/impl/variant.cpp index c2036d9145..bd05e5d091 100644 --- a/src/targets/qwen3_6_27b/impl/variant.cpp +++ b/src/targets/qwen3_6_27b/impl/variant.cpp @@ -19,6 +19,26 @@ #include "targets/qwen3_6/impl/runtime/instantiate.h" namespace ninfer::targets::qwen3_6_27b::detail { + +std::array Variant::default_layer_kv_dtypes(WeightsProfile) { + // Measured on 13.3k zh perplexity at ctx 4096/8192: the 8/8 split + // (NVFP4 {0,1,3,4,6,7,8,9}, I8 {2,5,10-15}) scores 1.335 vs all-I8 1.432 + // at ctx 4096 and 1.195 vs 1.250 at ctx 8192. Layers 13/14 then demote to + // NVFP4 for free (10L: 1.3344/1.1952, parity within noise), shrinking the + // page pool another 12.5%; 12L (1.345) and 14L (1.375) stay below all-I8. + // The budget ladder 8L->10L->12L->14L costs +0.00/+0.00/+0.01/+0.04 ppl. + // Layers 2 and 5 are the true sensitivity outliers: demoting either alone + // costs +0.20/+0.09 and the 10L "0-4,6-9,15" combo (which demotes both) + // degrades to 1.551. Per-layer NMSE/rank metrics (analyze_kv.py) do NOT + // predict these combo outcomes; the 8/8-neighborhood marginals do. + std::array table{}; + table.fill(DType::NVFP4); + for (const int layer : {2, 5, 10, 11, 12, 15}) { + table[static_cast(layer)] = DType::I8; + } + return table; +} + namespace { std::vector @@ -113,6 +133,37 @@ std::size_t post_mixer_workspace_bytes(QType gate_up_qtype, QType down_qtype, return layout.peak_bytes(1); } +std::vector +dflash_base_profiles(std::uint32_t capacity, std::uint32_t draft_window) { + if (draft_window == 0 || capacity == 0) { return {}; } + const std::uint32_t block = draft_window + 1; + const std::uint32_t max_frontier = capacity - 1; + std::vector ends{ + 96U, 127U, 511U, 1023U, 2047U, 4095U, 8191U, 16383U, 32767U, 65536U, 131072U, 196608U, + }; + const auto add_target_boundary = [&](std::uint32_t visible_end) { + if (visible_end >= block) { ends.push_back(visible_end - block); } + }; + for (const std::uint32_t visible_end : {128U, 512U, 2048U, 4096U, 8198U, 16390U, 32768U}) { + add_target_boundary(visible_end); + } + if (draft_window >= 6 && draft_window <= 15) { + add_target_boundary(draft_window <= 11 ? 512U : 1024U); + } + std::sort(ends.begin(), ends.end()); + ends.erase(std::unique(ends.begin(), ends.end()), ends.end()); + return graph_profiles_through(max_frontier, ends); +} + +bool dflash_target_uses_chunked_small_t(std::uint32_t draft_window, std::uint32_t batch_size, + std::uint32_t max_visible_keys) { + const std::uint32_t tokens = draft_window + 1; + if (tokens <= 6) { return false; } + if (batch_size > 1) { return true; } + const std::uint32_t prompt_visible_limit = tokens <= 12 ? 512U : 1024U; + return max_visible_keys > prompt_visible_limit; +} + } // namespace std::vector Variant::ordinary_graph_profiles(std::uint32_t capacity) { @@ -155,6 +206,21 @@ std::vector Variant::dflash_graph_profiles(std::uint32_t, return {}; } +std::vector Variant::dflash2_graph_profiles(std::uint32_t capacity, + std::uint32_t draft_window, + std::uint32_t batch_size) { + std::vector profiles = dflash_base_profiles(capacity, draft_window); + for (GraphExecutionProfile& profile : profiles) { + const std::uint32_t target_max = static_cast(std::min( + capacity, static_cast(profile.max) + draft_window + 1ULL)); + const bool split_swa = profile.max > 96U; + const bool chunked_target = + dflash_target_uses_chunked_small_t(draft_window, batch_size, target_max); + profile.topology_class = (chunked_target ? 2U : 0U) | (split_swa ? 1U : 0U); + } + return profiles; +} + void Variant::attention_projection(const Tensor& hidden, const FullAttentionProjectionWeights& weights, Tensor& query, Tensor& gate, Tensor& key, Tensor& value, qwen3_6::TextPhase, @@ -349,6 +415,7 @@ std::size_t Variant::attention_projection_workspace_capacity_bytes(WeightsProfil return ops::attn_input_proj_workspace_capacity_bytes( QType::NVFP4, 14336, TextConfig::hidden, kNvfp4TextPolicy, first, last); case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return ops::attn_input_proj_workspace_capacity_bytes( QType::FP8_E4M3FN_ROW_BF16S, 14336, TextConfig::hidden, kFp8TextPolicy, first, last); } @@ -369,6 +436,7 @@ std::size_t Variant::attention_output_projection_workspace_capacity_bytes( TextConfig::query_size, kNvfp4TextPolicy, first, last); case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return ops::linear_add_workspace_capacity_bytes(QType::FP8_E4M3FN_ROW_BF16S, TextConfig::hidden, TextConfig::query_size, kFp8TextPolicy, first, last); @@ -389,6 +457,7 @@ std::size_t Variant::gdn_input_projection_workspace_capacity_bytes(WeightsProfil return ops::gdn_input_proj_workspace_capacity_bytes(QType::NVFP4, 16384, TextConfig::hidden, kNvfp4TextPolicy, first, last); case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return ops::gdn_input_proj_workspace_capacity_bytes( QType::FP8_E4M3FN_ROW_BF16S, 16384, TextConfig::hidden, kFp8TextPolicy, first, last); } @@ -412,6 +481,7 @@ std::size_t Variant::gdn_input_projection_snapshot_workspace_capacity_bytes( QType::NVFP4, 16384, TextConfig::hidden, kNvfp4TextPolicy, batch_size, first, last)); case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return std::max(kMinimumLeafWorkspaceBytes, ops::gdn_input_proj_conv_snapshot_workspace_capacity_bytes( QType::FP8_E4M3FN_ROW_BF16S, 16384, TextConfig::hidden, kFp8TextPolicy, @@ -437,6 +507,7 @@ std::size_t Variant::gdn_input_projection_record_workspace_capacity_bytes( QType::NVFP4, 16384, TextConfig::hidden, kNvfp4TextPolicy, batch_size, first, last)); case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return std::max(kMinimumLeafWorkspaceBytes, ops::gdn_input_proj_conv_record_workspace_capacity_bytes( QType::FP8_E4M3FN_ROW_BF16S, 16384, TextConfig::hidden, kFp8TextPolicy, @@ -460,6 +531,7 @@ std::size_t Variant::gdn_output_projection_workspace_capacity_bytes(WeightsProfi return ops::linear_add_workspace_capacity_bytes( QType::NVFP4, TextConfig::hidden, TextConfig::value_dim, kNvfp4TextPolicy, first, last); case WeightsProfile::Qwen38Nvfp4: + case WeightsProfile::Qwen38Nvfp4DFlash2: return ops::linear_add_workspace_capacity_bytes(QType::FP8_E4M3FN_ROW_BF16S, TextConfig::hidden, TextConfig::value_dim, kFp8TextPolicy, first, last); @@ -486,6 +558,7 @@ std::size_t Variant::post_mixer_workspace_capacity_bytes(WeightsProfile weights_ return post_mixer_workspace_bytes(QType::NVFP4, QType::NVFP4, kNvfp4TextPolicy, first, last); case WeightsProfile::Qwen38Nvfp4: { + case WeightsProfile::Qwen38Nvfp4DFlash2: const std::size_t nvfp4 = post_mixer_workspace_bytes(QType::NVFP4, QType::NVFP4, kNvfp4TextPolicy, first, last); const std::size_t fp8 = post_mixer_workspace_bytes( diff --git a/src/targets/qwen3_6_27b/impl/variant.h b/src/targets/qwen3_6_27b/impl/variant.h index 75332671ad..ccfd6d54c4 100644 --- a/src/targets/qwen3_6_27b/impl/variant.h +++ b/src/targets/qwen3_6_27b/impl/variant.h @@ -15,10 +15,13 @@ using GraphExecutionProfile = qwen3_6::GraphExecutionProfile; // Compile-time data and the three closed execution leaves supplied to the Qwen3.6 family runtime. // It owns no request state, execution phase, graph object, or schedule callback. struct Variant { + static constexpr bool supports_per_layer_kv_defaults = true; + [[nodiscard]] static std::array default_layer_kv_dtypes(WeightsProfile profile); using WeightsProfile = detail::WeightsProfile; using TextConfig = detail::TextConfig; using VisionConfig = detail::VisionConfig; using DFlashConfig = detail::DFlashConfig; + using DFlash2Config = detail::DFlash2Config; using ModelView = detail::RuntimeModelView; using FullAttentionProjectionWeights = detail::FullAttentionProjectionPayload; using GdnProjectionWeights = detail::GdnProjectionPayload; @@ -35,8 +38,13 @@ struct Variant { static constexpr std::uint32_t maximum_dflash_draft_tokens = kMaximumDFlashDraftTokens; static constexpr std::uint32_t maximum_context = kNativeContext; static constexpr bool supports_dflash = DFlashConfig::supported; + static constexpr bool supports_dflash2 = DFlash2Config::supported; static constexpr std::int32_t draft_head_rows = 131072; + [[nodiscard]] static constexpr bool dflash2_weights(WeightsProfile profile) { + return profile == WeightsProfile::Qwen38Nvfp4DFlash2; + } + static void attention_projection(const Tensor& hidden, const FullAttentionProjectionWeights& weights, Tensor& query, Tensor& gate, Tensor& key, Tensor& value, @@ -125,6 +133,9 @@ struct Variant { [[nodiscard]] static std::vector dflash_graph_profiles(std::uint32_t capacity, std::uint32_t draft_window, std::uint32_t batch_size); + [[nodiscard]] static std::vector + dflash2_graph_profiles(std::uint32_t capacity, std::uint32_t draft_window, + std::uint32_t batch_size); }; } // namespace ninfer::targets::qwen3_6_27b::detail diff --git a/src/targets/qwen3_6_35b_a3b/export/ninfer/targets/qwen3_6_35b_a3b/package.h b/src/targets/qwen3_6_35b_a3b/export/ninfer/targets/qwen3_6_35b_a3b/package.h index a049706dae..f079d442d9 100644 --- a/src/targets/qwen3_6_35b_a3b/export/ninfer/targets/qwen3_6_35b_a3b/package.h +++ b/src/targets/qwen3_6_35b_a3b/export/ninfer/targets/qwen3_6_35b_a3b/package.h @@ -118,6 +118,9 @@ struct Package { [[nodiscard]] static ModelSamplingDefaults sampling_defaults(std::string_view model); [[nodiscard]] static WeightsProfile resolve_weights(const artifact::ArtifactIdentity& identity); + // Resolves --spec auto (this target has no DFlash2 weights; auto -> MTP). + [[nodiscard]] static EngineOptions resolved_auto_speculative(const EngineOptions& options, + WeightsProfile weights_profile); [[nodiscard]] static LoadPlan plan_load(artifact::Binder& binder, const EngineOptions& options, WeightsProfile weights_profile); [[nodiscard]] static std::unique_ptr diff --git a/src/targets/qwen3_6_35b_a3b/impl/config.h b/src/targets/qwen3_6_35b_a3b/impl/config.h index 7429be4744..af530d08c5 100644 --- a/src/targets/qwen3_6_35b_a3b/impl/config.h +++ b/src/targets/qwen3_6_35b_a3b/impl/config.h @@ -70,6 +70,7 @@ struct VisionConfig : qwen3_6::VisionBackboneConfig { struct DFlashConfig { static constexpr bool supported = true; + static constexpr bool bf16_weights = false; static constexpr int layers = 6; static constexpr int local_layers = 5; static constexpr int feature_layers = 8; @@ -90,6 +91,39 @@ struct DFlashConfig { 22, 27, 32, 37}; }; +// The 35B-A3B package registers no DFlash2 draft; the family runtime still +// needs a concrete config type for its compile-time aliases and recipe +// instantiations, so it mirrors the 27B DFlash2 shape. +struct DFlash2Config { + static constexpr bool supported = false; + static constexpr bool bf16_weights = true; + static constexpr int layers = 5; + static constexpr bool full_only = false; + static constexpr int local_layers = 5; + static constexpr int full_layers = 0; + static constexpr int feature_layers = 5; + static constexpr int feature_rows = feature_layers * TextConfig::hidden; + static constexpr int hidden = TextConfig::hidden; + static constexpr int intermediate = 17408; + static constexpr int query_heads = 32; + static constexpr int kv_heads = 8; + static constexpr int head_dim = 128; + static constexpr int query_size = query_heads * head_dim; + static constexpr int kv_size = kv_heads * head_dim; + static constexpr int local_capacity = 2048; + static constexpr std::uint32_t local_window = 2048; + static constexpr int mask_token = 248070; + static constexpr float rms_epsilon = 1.0e-6F; + static constexpr float rope_theta = 1.0e7F; + static constexpr float attention_scale = 0.08838834764831845F; + static constexpr int block_drafts = 7; + static constexpr int conv_group_size = 16; + static constexpr int conv_kernel_size = 2; + static constexpr int selector_rank = 256; + static constexpr int selector_top_k = 16; + static constexpr std::array target_feature_layers{5, 19, 33, 47, 61}; +}; + inline constexpr float kAttentionScale = 0.0625F; inline constexpr float kGdnScale = 0.08838834764831845F; inline constexpr std::uint32_t kPrefillChunkAlignment = 128; diff --git a/src/targets/qwen3_6_35b_a3b/impl/load/bindings.h b/src/targets/qwen3_6_35b_a3b/impl/load/bindings.h index 6015467b7c..9560558a9b 100644 --- a/src/targets/qwen3_6_35b_a3b/impl/load/bindings.h +++ b/src/targets/qwen3_6_35b_a3b/impl/load/bindings.h @@ -129,7 +129,7 @@ struct GdnProjectionPayload { using RuntimeModelView = qwen3_6::ModelView, kFullAttentionLayers, kGdnLayers>; + qwen3_6::DFlashWeights, qwen3_6::DFlash2Weights<0>, kFullAttentionLayers, kGdnLayers>; using FullAttentionWeights = RuntimeModelView::FullLayer; using GdnWeights = RuntimeModelView::GdnLayer; using MtpWeights = RuntimeModelView::MtpLayer; diff --git a/src/targets/qwen3_6_35b_a3b/impl/package.cpp b/src/targets/qwen3_6_35b_a3b/impl/package.cpp index 1bed0142e0..2d274ab63a 100644 --- a/src/targets/qwen3_6_35b_a3b/impl/package.cpp +++ b/src/targets/qwen3_6_35b_a3b/impl/package.cpp @@ -72,6 +72,17 @@ Package::WeightsProfile Package::resolve_weights(const artifact::ArtifactIdentit "' is not supported by target '" + std::string(target_key) + "'"); } +EngineOptions Package::resolved_auto_speculative(const EngineOptions& options, + WeightsProfile) { + EngineOptions resolved = options; + if (options.speculative.backend == SpeculativeBackend::Auto) { + // This target carries no DFlash2 weights; auto falls back to MTP. + resolved.speculative.backend = SpeculativeBackend::Mtp; + if (resolved.speculative.draft_tokens == 0) { resolved.speculative.draft_tokens = 3; } + } + return resolved; +} + Package::LoadPlan Package::plan_load(artifact::Binder& binder, const EngineOptions& options, WeightsProfile weights_profile) { return LoadPlan(std::make_unique( diff --git a/src/targets/qwen3_6_35b_a3b/impl/variant.cpp b/src/targets/qwen3_6_35b_a3b/impl/variant.cpp index 96ab3ac8e6..9158d98da4 100644 --- a/src/targets/qwen3_6_35b_a3b/impl/variant.cpp +++ b/src/targets/qwen3_6_35b_a3b/impl/variant.cpp @@ -14,6 +14,10 @@ #include "targets/qwen3_6/impl/runtime/instantiate.h" namespace ninfer::targets::qwen3_6_35b_a3b::detail { +std::array Variant::default_layer_kv_dtypes(WeightsProfile) { + return {}; // no per-layer calibration prior for this target +} + namespace { std::vector @@ -123,6 +127,10 @@ std::vector Variant::dflash_graph_profiles(std::uint32_t } return profiles; } +std::vector Variant::dflash2_graph_profiles(std::uint32_t, std::uint32_t, std::uint32_t) { + return {}; +} + void Variant::attention_projection(const Tensor& hidden, const FullAttentionProjectionWeights& weights, Tensor& query, diff --git a/src/targets/qwen3_6_35b_a3b/impl/variant.h b/src/targets/qwen3_6_35b_a3b/impl/variant.h index c6802e43f4..c54d754516 100644 --- a/src/targets/qwen3_6_35b_a3b/impl/variant.h +++ b/src/targets/qwen3_6_35b_a3b/impl/variant.h @@ -17,6 +17,7 @@ struct Variant { using TextConfig = detail::TextConfig; using VisionConfig = detail::VisionConfig; using DFlashConfig = detail::DFlashConfig; + using DFlash2Config = detail::DFlash2Config; using ModelView = detail::RuntimeModelView; using FullAttentionProjectionWeights = detail::AttentionProjectionPayload; using GdnProjectionWeights = detail::GdnProjectionPayload; @@ -33,6 +34,9 @@ struct Variant { static constexpr std::uint32_t maximum_dflash_draft_tokens = kMaximumDFlashDraftTokens; static constexpr std::uint32_t maximum_context = kNativeContext; static constexpr bool supports_dflash = DFlashConfig::supported; + static constexpr bool supports_per_layer_kv_defaults = false; + [[nodiscard]] static std::array default_layer_kv_dtypes(WeightsProfile profile); + static constexpr bool supports_dflash2 = DFlash2Config::supported; static constexpr std::int32_t draft_head_rows = 131072; [[nodiscard]] static std::vector @@ -42,6 +46,9 @@ struct Variant { [[nodiscard]] static std::vector dflash_graph_profiles(std::uint32_t capacity, std::uint32_t draft_window, std::uint32_t batch_size); + [[nodiscard]] static std::vector + dflash2_graph_profiles(std::uint32_t capacity, std::uint32_t draft_window, + std::uint32_t batch_size); static void attention_projection(const Tensor& hidden, const FullAttentionProjectionWeights& weights, Tensor& query, diff --git a/src/targets/registry.cpp b/src/targets/registry.cpp index 17548ebe61..dd1479595f 100644 --- a/src/targets/registry.cpp +++ b/src/targets/registry.cpp @@ -93,6 +93,12 @@ ConstructedTarget construct_registered(const EngineOptions& options, DeviceConte std::string_view target_key) { const auto& identity = reader.identity(); const auto weights_profile = Target::resolve_weights(identity); + // Resolve --spec auto up front so the planner, the load plan and the + // program all see the same concrete backend (previously only plan_load + // resolved it, so the planner built an Auto plan and the frozen startup + // features mismatch check rejected the loaded weights). + const EngineOptions resolved_options = + Target::resolved_auto_speculative(options, weights_profile); const ModelSamplingDefaults sampling_defaults = Target::sampling_defaults(identity.model_id); const runtime::ContextCostIdentity context_cost_identity{ .hardware_class = runtime::context_cost_hardware_class( @@ -104,14 +110,14 @@ ConstructedTarget construct_registered(const EngineOptions& options, DeviceConte context_cost_identity, options.context_cost.preset_path); artifact::Binder binder(reader); - auto load_plan = Target::plan_load(binder, options, weights_profile); - auto sequence_planner = Target::make_sequence_planner(device, options, weights_profile); + auto load_plan = Target::plan_load(binder, resolved_options, weights_profile); + auto sequence_planner = Target::make_sequence_planner(device, resolved_options, weights_profile); const runtime::SequenceCapacityCurve curve = sequence_planner.capacity_curve(); const std::size_t preflight_runtime_bytes = runtime_bytes_after_planned_weights(load_plan.materialization().device_capacity_bytes); - (void)runtime::resolve_kv_capacity(options.kv_capacity, curve, preflight_runtime_bytes); + (void)runtime::resolve_kv_capacity(resolved_options.kv_capacity, curve, preflight_runtime_bytes); - auto progress = artifact_progress(options.load_progress); + auto progress = artifact_progress(resolved_options.load_progress); auto materialized = artifact::materialize(reader, load_plan.materialization(), device, progress.callback ? &progress : nullptr); const artifact::MaterializationStats stats = materialized.stats(); @@ -119,13 +125,13 @@ ConstructedTarget construct_registered(const EngineOptions& options, DeviceConte auto model = Target::construct_loaded_model(std::move(load_plan), std::move(materialized)); device.synchronize(); runtime::KvCapacityResolution capacity_resolution = - runtime::resolve_kv_capacity(options.kv_capacity, curve, current_free_device_bytes()); + runtime::resolve_kv_capacity(resolved_options.kv_capacity, curve, current_free_device_bytes()); auto sequence_plan = std::move(sequence_planner).finalize(capacity_resolution.main_page_groups); if (sequence_plan.device_reservation_bytes() != capacity_resolution.runtime_reservation_bytes || sequence_plan.kv_capacity() != capacity_resolution.resolved_tokens) { throw std::logic_error("resolved KV capacity does not match the finalized target plan"); } - auto loaded = std::make_unique(std::move(model), options); + auto loaded = std::make_unique(std::move(model), resolved_options); auto instance = std::make_unique(std::move(loaded), capacity_resolution, std::move(sequence_plan), device); device.synchronize(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 18b1c70363..ddd2d5185c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -44,6 +44,12 @@ function(ninfer_add_linear_test name source) ops/linear/linear_test_common.cpp LIBRARIES ninfer_ops) endfunction() +ninfer_add_op_test(ninfer_entropy_cold_requant_test + SOURCES ops/test_entropy_cold_requant.cpp + LIBRARIES ninfer_ops) +ninfer_add_op_test(ninfer_cold_i8_test + SOURCES ops/test_cold_i8.cpp + LIBRARIES ninfer_ops) function(ninfer_add_fused_linear_test name source common) ninfer_add_op_test(${name} diff --git a/tests/ops/test_cold_i8.cpp b/tests/ops/test_cold_i8.cpp new file mode 100644 index 0000000000..e410e94d95 --- /dev/null +++ b/tests/ops/test_cold_i8.cpp @@ -0,0 +1,274 @@ +#include "ninfer/ops/cold_i8.h" +#include "ninfer/ops/entropy_cold_requant.h" +#include "ops/op_tester.h" + +#include +#include +#include +#include +#include +#include + +using namespace ninfer; +using namespace ninfer::test; + +namespace { + +constexpr int kHeadDim = 256; +constexpr int kPageRows = 64; +constexpr int kKvHeads = 4; +constexpr int kI8CodeB = kHeadDim * kPageRows; // 16384 per head +constexpr int kI8ScaleB = kHeadDim / 64 * 2 * kPageRows; // 512 fp16 bytes +constexpr int kNvCodeB = kHeadDim / 2 * kPageRows; // 8192 +constexpr int kNvScaleB = kHeadDim / 16 * kPageRows; // 1024 + +std::uint8_t e4m3_rne(float x) { + if (!(x > 0.0f)) { return 0; } + std::uint32_t bits; + std::memcpy(&bits, &x, 4); + const std::uint32_t sign = (bits >> 24) & 0x80u; + int exponent = static_cast((bits >> 23) & 0xffu) - 127 + 7; + if (exponent >= 15) { return static_cast(sign | (15u << 3) | 7u); } + if (exponent <= 0) { + int mantissa = static_cast(std::nearbyint(x * 512.0f)); + if (mantissa <= 0) { return static_cast(sign); } + if (mantissa >= 8) { return static_cast(sign | (1u << 3)); } + return static_cast(sign | mantissa); + } + std::uint32_t mantissa = (bits >> 20) & 0x7u; + const std::uint32_t guard = (bits >> 19) & 1u; + const std::uint32_t sticky = bits & 0x7ffffu; + if (guard && (sticky || (mantissa & 1u))) { + mantissa += 1; + if (mantissa > 7) { + mantissa = 0; + exponent += 1; + if (exponent >= 15) { return static_cast(sign | (15u << 3) | 7u); } + } + } + return static_cast(sign | (exponent << 3) | mantissa); +} + +float e4m3_to_f32(std::uint8_t byte) { + const int e = (byte >> 3) & 0xF; + const int m = byte & 0x7; + if (e == 0) { return static_cast(m) / 512.0f; } + return (1.0f + static_cast(m) / 8.0f) * std::pow(2.0f, static_cast(e - 7)); +} + +float e2m1_to_f32(std::uint8_t code) { + static const float mag[8] = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f}; + const float v = mag[code & 0x7]; + return (code & 0x8) != 0 ? -v : v; +} + +std::uint8_t e2m1_code(float x) { + const float a = std::fabs(x); + std::uint8_t c; + if (a < 0.25f) { c = 0; } + else if (a < 0.75f) { c = 1; } + else if (a < 1.25f) { c = 2; } + else if (a < 1.75f) { c = 3; } + else if (a < 2.5f) { c = 4; } + else if (a < 3.5f) { c = 5; } + else if (a < 5.0f) { c = 6; } + else { c = 7; } + if (x < 0.0f) { c |= 0x08u; } + return c; +} + +float half_to_float(std::uint16_t h) { + const std::uint32_t sign = (h >> 15) & 1u; + const std::uint32_t exp = (h >> 10) & 0x1Fu; + const std::uint32_t man = h & 0x3FFu; + float out; + if (exp == 0) { + out = std::ldexp(static_cast(man), -24); + } else { + out = std::ldexp(1024.0f + static_cast(man), static_cast(exp) - 25); + } + return sign != 0 ? -out : out; +} + +std::uint16_t float_to_half(float f) { + if (f <= 0.0f) { return 0; } + std::uint32_t x; + std::memcpy(&x, &f, 4); + const std::uint32_t sign = (x >> 16) & 0x8000u; + int exponent = static_cast((x >> 23) & 0xFFu) - 127 + 15; + std::uint32_t mantissa = (x >> 13) & 0x3FFu; + if (exponent <= 0) { + const std::uint32_t man_full = (x & 0x7FFFFFu) | 0x800000u; + const int shift = 14 - exponent + 1; + mantissa = man_full >> shift; + const std::uint32_t round_bit = (man_full >> (shift - 1)) & 1u; + if (round_bit != 0) { mantissa += 1; } + exponent = 0; + } else { + const std::uint32_t round_bit = (x >> 12) & 1u; + const std::uint32_t sticky = x & 0xFFFu; + if (round_bit != 0 && (sticky != 0 || (mantissa & 1u) != 0)) { + mantissa += 1; + if (mantissa > 0x3FFu) { + mantissa = 0; + exponent += 1; + } + } + } + if (exponent >= 31) { return static_cast(sign | (31u << 10)); } + return static_cast(sign | (static_cast(exponent) << 10) | + mantissa); +} + +} // namespace + +int main() { + std::mt19937 rng(20260830); + int failures = 0; + const auto check = [&](bool ok, const char* what) { + if (!ok) { + std::printf("FAIL: %s\n", what); + ++failures; + } + }; + + // 1) synthetic int8 page per head + std::vector src_codes(static_cast(kKvHeads) * kI8CodeB); + std::vector src_scales(static_cast(kKvHeads) * kPageRows * 4); + std::normal_distribution noise(0.0f, 1.0f); + std::uniform_real_distribution level(0.01f, 50.0f); + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + for (int g = 0; g < 4; ++g) { + const float amp = level(rng); + float amax = 1e-6f; + float vals[64]; + for (int i = 0; i < 64; ++i) { + float v = amp * noise(rng); + if (((row * 7 + i) % 251) == 0) { v *= 32.0f; } + vals[i] = v; + amax = std::fmax(amax, std::fabs(v)); + } + const float s = std::fmax(amax / 127.0f, 1e-30f); + const std::uint16_t sb = float_to_half(s); + src_scales[(static_cast(head) * kPageRows + row) * 4 + g] = sb; + const float sh = half_to_float(sb); + for (int i = 0; i < 64; ++i) { + float q = std::nearbyint(vals[i] / sh); + q = std::fmin(127.0f, std::fmax(-127.0f, q)); + src_codes[static_cast(head) * kI8CodeB + row * kHeadDim + + g * 64 + i] = static_cast(q); + } + } + } + } + + // 2) requant + pack on device + GuardedDeviceBuffer d_src_codes(static_cast(kKvHeads) * kI8CodeB); + GuardedDeviceBuffer d_src_scales(static_cast(kKvHeads) * kI8ScaleB); + GuardedDeviceBuffer d_rq_codes(static_cast(kKvHeads) * kNvCodeB); + GuardedDeviceBuffer d_rq_scales(static_cast(kKvHeads) * kNvScaleB); + GuardedDeviceBuffer d_slots(static_cast(kKvHeads) * 2 * ops::kColdI8SlotBytes); + GuardedDeviceBuffer d_valid(static_cast(kKvHeads) * 2 * sizeof(std::int32_t)); + GuardedDeviceBuffer d_out_codes(static_cast(kKvHeads) * kI8CodeB); + GuardedDeviceBuffer d_out_scales(static_cast(kKvHeads) * kI8ScaleB); + + d_src_codes.copy_from_host(src_codes.data(), d_src_codes.bytes()); + d_src_scales.copy_from_host(src_scales.data(), d_src_scales.bytes()); + + ops::entropy_cold_requant_raw( + static_cast(d_src_codes.data()), + static_cast(d_src_scales.data()), + ops::EntropyColdRequantMode::Int8G64, kKvHeads, 1, + static_cast(d_rq_codes.data()), + static_cast(d_rq_scales.data()), nullptr); + cuda_synchronize(); + auto* valid_k = static_cast(d_valid.data()); + auto* valid_v = valid_k + kKvHeads; + ops::cold_i8_slot_pack_raw(static_cast(d_rq_codes.data()), + static_cast(d_rq_scales.data()), kKvHeads, 1, + static_cast(d_slots.data()), valid_k, nullptr); + ops::cold_i8_slot_pack_raw( + static_cast(d_rq_codes.data()) + static_cast(kKvHeads) * kNvCodeB, + static_cast(d_rq_scales.data()) + static_cast(kKvHeads) * kNvScaleB, + kKvHeads, 1, + static_cast(d_slots.data()) + static_cast(kKvHeads) * ops::kColdI8SlotBytes, + valid_v, nullptr); + cuda_synchronize(); + ops::cold_i8_slot_restore_raw(static_cast(d_slots.data()), kKvHeads, 1, + static_cast(d_out_codes.data()), + d_out_scales.data(), nullptr); + cuda_synchronize(); + + std::vector got_codes(src_codes.size()); + std::vector got_scales(src_scales.size()); + std::vector got_valid(static_cast(kKvHeads) * 2); + d_out_codes.copy_to_host(got_codes.data(), d_out_codes.bytes()); + d_out_scales.copy_to_host(got_scales.data(), d_out_scales.bytes()); + d_valid.copy_to_host(got_valid.data(), d_valid.bytes()); + for (std::size_t i = 0; i < got_valid.size(); ++i) { + check(got_valid[i] == 1, "raw slot valid flag"); + } + + // 3) host oracle: decode -> requant g64 -> decode -> int8 re-encode with + // upper-bound group scale (mirror cold_i8_decode_row) + double num = 0.0, den = 0.0; + int byte_mismatch = 0; + int scale_mismatch = 0; + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + for (int g = 0; g < 4; ++g) { + const float sh = half_to_float( + src_scales[(static_cast(head) * kPageRows + row) * 4 + g]); + float vals[64]; + for (int i = 0; i < 64; ++i) { + vals[i] = static_cast( + src_codes[static_cast(head) * kI8CodeB + + row * kHeadDim + g * 64 + i]) * sh; + } + // requant g64 (matches entropy_cold_requant oracle) + float amax = 0.0f; + for (int i = 0; i < 64; ++i) { amax = std::fmax(amax, std::fabs(vals[i])); } + const std::uint8_t sb = e4m3_rne(std::fmax(amax / 6.0f, 0x1p-9f)); + const float s = e4m3_to_f32(sb); + float dec[64]; + for (int i = 0; i < 64; ++i) { + dec[i] = e2m1_to_f32(e2m1_code(vals[i] / s)) * s; + } + // upper-bound int8 re-encode (mirror device) + float mx = 0.0f; + for (int sub = 0; sub < 4; ++sub) { + // g64 group covers scale slots [g*4, g*4+4) of the row's + // g16 layout; requant wrote the same sb replicated. + mx = std::fmax(mx, e4m3_to_f32(sb)); + } + const float scale = mx * 6.0f / 127.0f; + const std::uint16_t want_scale = float_to_half(scale); + const std::uint16_t got_scale = + got_scales[(static_cast(head) * kPageRows + row) * 4 + g]; + if (want_scale != got_scale) { ++scale_mismatch; } + const float inv = scale > 0.0f ? 1.0f / scale : 0.0f; + const float got_sh = half_to_float(got_scale); + for (int i = 0; i < 64; ++i) { + int c = static_cast(std::nearbyint(dec[i] * inv)); + c = std::max(-127, std::min(127, c)); + const std::int8_t got = got_codes[static_cast(head) * kI8CodeB + + row * kHeadDim + g * 64 + i]; + if (got != static_cast(c)) { ++byte_mismatch; } + const float restored = static_cast(got) * got_sh; + num += static_cast(restored - vals[i]) * (restored - vals[i]); + den += static_cast(vals[i]) * vals[i]; + } + } + } + } + const double nmse = den > 0.0 ? num / den : 0.0; + check(byte_mismatch == 0, "restore codes match oracle"); + check(scale_mismatch == 0, "restore scales match oracle"); + check(nmse < 0.05, "roundtrip NMSE bound"); // synthetic spiky data; real planes measured 0.012 + std::printf("cold_i8 roundtrip: byte_mismatch=%d scale_mismatch=%d NMSE=%.5f\n", + byte_mismatch, scale_mismatch, nmse); + + if (failures == 0) { std::printf("cold_i8: all tests passed\n"); } + return failures == 0 ? 0 : 1; +} diff --git a/tests/ops/test_entropy_cold_requant.cpp b/tests/ops/test_entropy_cold_requant.cpp new file mode 100644 index 0000000000..faf8ddd9fd --- /dev/null +++ b/tests/ops/test_entropy_cold_requant.cpp @@ -0,0 +1,432 @@ +#include "ninfer/ops/entropy_cold_requant.h" +#include "ops/op_tester.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace ninfer; +using namespace ninfer::test; + +namespace { + +constexpr int kHeadDim = 256; +constexpr int kPageRows = 64; +constexpr int kKvHeads = 4; +constexpr int kNvfp4CodeB = kHeadDim / 2 * kPageRows; // 8192 per head-page +constexpr int kNvfp4ScaleB = kHeadDim / 16 * kPageRows; // 1024 +constexpr int kInt8CodeB = kHeadDim * kPageRows; // 16384 +constexpr int kInt8ScaleB = kHeadDim / 64 * 2 * kPageRows; // 512 (fp16) + +std::uint8_t e4m3_rne(float x) { + if (!(x > 0.0f)) { return 0; } + std::uint32_t bits; + std::memcpy(&bits, &x, 4); + const std::uint32_t sign = (bits >> 24) & 0x80u; + int exponent = static_cast((bits >> 23) & 0xffu) - 127 + 7; + if (exponent >= 15) { return static_cast(sign | (15u << 3) | 7u); } + if (exponent <= 0) { + int mantissa = static_cast(std::nearbyint(x * 512.0f)); + if (mantissa <= 0) { return static_cast(sign); } + if (mantissa >= 8) { return static_cast(sign | (1u << 3)); } + return static_cast(sign | mantissa); + } + std::uint32_t mantissa = (bits >> 20) & 0x7u; + const std::uint32_t guard = (bits >> 19) & 1u; + const std::uint32_t sticky = bits & 0x7ffffu; + if (guard && (sticky || (mantissa & 1u))) { + mantissa += 1; + if (mantissa > 7) { + mantissa = 0; + exponent += 1; + if (exponent >= 15) { return static_cast(sign | (15u << 3) | 7u); } + } + } + return static_cast(sign | (exponent << 3) | mantissa); +} + +float e4m3_to_f32(std::uint8_t byte) { + const int e = (byte >> 3) & 0xF; + const int m = byte & 0x7; + if (e == 0) { return static_cast(m) / 512.0f; } + return (1.0f + static_cast(m) / 8.0f) * std::pow(2.0f, static_cast(e - 7)); +} + +float e2m1_to_f32(std::uint8_t code) { + static const float mag[8] = {0.0f, 0.5f, 1.0f, 1.5f, 2.0f, 3.0f, 4.0f, 6.0f}; + const float v = mag[code & 0x7]; + return (code & 0x8) != 0 ? -v : v; +} + +std::uint8_t e2m1_code(float x) { + const float a = std::fabs(x); + std::uint8_t c; + if (a < 0.25f) { c = 0; } + else if (a < 0.75f) { c = 1; } + else if (a < 1.25f) { c = 2; } + else if (a < 1.75f) { c = 3; } + else if (a < 2.5f) { c = 4; } + else if (a < 3.5f) { c = 5; } + else if (a < 5.0f) { c = 6; } + else { c = 7; } + if (x < 0.0f) { c |= 0x08u; } + return c; +} + +float iso3_to_f32(std::uint8_t code) { + const float mag = static_cast(code & 0x7); + return (code & 0x8) != 0 ? -mag : mag; +} + +std::uint8_t iso3_code(float value, float scale) { + float mag = std::roundf(std::fabs(value) / scale); // device uses roundf + if (mag > 7.0f) { mag = 7.0f; } + if (mag < 0.0f) { mag = 0.0f; } + std::uint8_t code = static_cast(mag); + if (value < 0.0f && code != 0) { code |= 0x08u; } + return code; +} + +float half_to_float(std::uint16_t h) { + const std::uint32_t sign = (h >> 15) & 1u; + const std::uint32_t exp = (h >> 10) & 0x1Fu; + const std::uint32_t man = h & 0x3FFu; + float out; + if (exp == 0) { + out = std::ldexp(static_cast(man), -24); + } else { + out = std::ldexp(1024.0f + static_cast(man), static_cast(exp) - 25); + } + return sign != 0 ? -out : out; +} + +std::uint16_t float_to_half(float f) { + if (f <= 0.0f) { return 0; } + std::uint32_t x; + std::memcpy(&x, &f, 4); + const std::uint32_t sign = (x >> 16) & 0x8000u; + int exponent = static_cast((x >> 23) & 0xFFu) - 127 + 15; + std::uint32_t mantissa = (x >> 13) & 0x3FFu; + if (exponent <= 0) { + const std::uint32_t man_full = (x & 0x7FFFFFu) | 0x800000u; + const int shift = 14 - exponent + 1; + mantissa = man_full >> shift; + const std::uint32_t round_bit = (man_full >> (shift - 1)) & 1u; + if (round_bit != 0) { mantissa += 1; } + exponent = 0; + } else { + const std::uint32_t round_bit = (x >> 12) & 1u; + const std::uint32_t sticky = x & 0xFFFu; + if (round_bit != 0 && (sticky != 0 || (mantissa & 1u) != 0)) { + mantissa += 1; + if (mantissa > 0x3FFu) { + mantissa = 0; + exponent += 1; + } + } + } + if (exponent >= 31) { return static_cast(sign | (31u << 10)); } + return static_cast(sign | (static_cast(exponent) << 10) | + mantissa); +} + +struct PageValues { + std::vector values; + std::vector nvfp4_codes; + std::vector nvfp4_scales; + std::vector iso3_codes; + std::vector iso3_scales; + std::vector int8_codes; + std::vector int8_scales; +}; + +PageValues make_page(std::mt19937& rng) { + PageValues page; + page.values.resize(static_cast(kKvHeads) * kPageRows * kHeadDim); + std::normal_distribution noise(0.0f, 1.0f); + std::uniform_real_distribution level(0.001f, 8.0f); + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + const float amp = level(rng); + for (int d = 0; d < kHeadDim; ++d) { + float v = amp * noise(rng); + if (((head * 131 + row * 17 + d) % 4096) == 0) { v *= 64.0f; } + if (head == kKvHeads - 1 && row < 2) { v = 0.0f; } + page.values[(static_cast(head) * kPageRows + row) * kHeadDim + d] = v; + } + } + } + + page.nvfp4_codes.assign(static_cast(kKvHeads) * kNvfp4CodeB, 0); + page.nvfp4_scales.assign(static_cast(kKvHeads) * kNvfp4ScaleB, 0); + page.iso3_codes.assign(static_cast(kKvHeads) * kNvfp4CodeB, 0); + page.iso3_scales.assign(static_cast(kKvHeads) * kNvfp4ScaleB, 0); + page.int8_codes.assign(static_cast(kKvHeads) * kInt8CodeB, 0); + page.int8_scales.assign(static_cast(kKvHeads) * kPageRows * (kHeadDim / 64), 0); + + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + const std::size_t vrow = + (static_cast(head) * kPageRows + row) * kHeadDim; + for (int g = 0; g < kHeadDim / 16; ++g) { + float amax = 0.0f; + for (int i = 0; i < 16; ++i) { + amax = std::fmax(amax, std::fabs(page.values[vrow + g * 16 + i])); + } + const std::uint8_t kb = e4m3_rne(std::fmax(amax / 6.0f, 0x1p-9f)); + page.nvfp4_scales[static_cast(head) * kNvfp4ScaleB + + row * (kHeadDim / 16) + g] = kb; + const float ks = e4m3_to_f32(kb); + const std::uint8_t vb = e4m3_rne(std::fmax(amax / 7.0f, 0x1p-9f)); + page.iso3_scales[static_cast(head) * kNvfp4ScaleB + + row * (kHeadDim / 16) + g] = vb; + const float vs = e4m3_to_f32(vb); + for (int i = 0; i < 16; i += 2) { + page.nvfp4_codes[static_cast(head) * kNvfp4CodeB + + row * (kHeadDim / 2) + g * 8 + i / 2] = + static_cast( + e2m1_code(page.values[vrow + g * 16 + i] / ks) | + (e2m1_code(page.values[vrow + g * 16 + i + 1] / ks) << 4)); + page.iso3_codes[static_cast(head) * kNvfp4CodeB + + row * (kHeadDim / 2) + g * 8 + i / 2] = + static_cast( + iso3_code(page.values[vrow + g * 16 + i], vs) | + (iso3_code(page.values[vrow + g * 16 + i + 1], vs) << 4)); + } + } + for (int g = 0; g < kHeadDim / 64; ++g) { + float amax = 0.0f; + for (int i = 0; i < 64; ++i) { + amax = std::fmax(amax, std::fabs(page.values[vrow + g * 64 + i])); + } + const float s = std::fmax(amax / 127.0f, 1e-30f); + const std::uint16_t sb = float_to_half(s); + page.int8_scales[(static_cast(head) * kPageRows + row) * + (kHeadDim / 64) + g] = sb; + const float sh = half_to_float(sb); + for (int i = 0; i < 64; ++i) { + float q = std::nearbyint(page.values[vrow + g * 64 + i] / sh); + if (q > 127.0f) { q = 127.0f; } + if (q < -127.0f) { q = -127.0f; } + page.int8_codes[static_cast(head) * kInt8CodeB + + row * kHeadDim + g * 64 + i] = static_cast(q); + } + } + } + } + return page; +} + +void requant_oracle(const PageValues& page, int mode, std::vector& out_codes, + std::vector& out_scales) { + // mode: 0 = nvfp4 K source, 1 = int8 source, 2 = iso3 V source + const bool iso3_out = mode == 2; + const float scale_div = iso3_out ? 7.0f : 6.0f; + out_codes.assign(static_cast(kKvHeads) * kNvfp4CodeB, 0); + out_scales.assign(static_cast(kKvHeads) * kNvfp4ScaleB, 0); + std::vector dec(static_cast(kKvHeads) * kPageRows * kHeadDim); + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + for (int g64 = 0; g64 < kHeadDim / 64; ++g64) { + if (mode == 1) { + const std::uint16_t sb = + page.int8_scales[(static_cast(head) * kPageRows + row) * + (kHeadDim / 64) + g64]; + const float s = half_to_float(sb); + for (int i = 0; i < 64; ++i) { + const std::int8_t c = + page.int8_codes[static_cast(head) * kInt8CodeB + + row * kHeadDim + g64 * 64 + i]; + dec[(static_cast(head) * kPageRows + row) * kHeadDim + + g64 * 64 + i] = static_cast(c) * s; + } + } else { + for (int i = 0; i < 64; ++i) { + const int d = g64 * 64 + i; + const std::uint8_t byte = + (mode == 0 ? page.nvfp4_codes : page.iso3_codes) + [static_cast(head) * kNvfp4CodeB + + row * (kHeadDim / 2) + (d >> 1)]; + const std::uint8_t nib = + (d & 1) != 0 ? static_cast(byte >> 4) + : static_cast(byte & 0x0F); + const float s = e4m3_to_f32( + (mode == 0 ? page.nvfp4_scales : page.iso3_scales) + [static_cast(head) * kNvfp4ScaleB + + row * (kHeadDim / 16) + (d >> 4)]); + dec[(static_cast(head) * kPageRows + row) * kHeadDim + d] = + (mode == 0 ? e2m1_to_f32(nib) : iso3_to_f32(nib)) * s; + } + } + } + } + } + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + for (int g64 = 0; g64 < kHeadDim / 64; ++g64) { + float amax = 0.0f; + for (int i = 0; i < 64; ++i) { + amax = std::fmax( + amax, + std::fabs(dec[(static_cast(head) * kPageRows + row) * + kHeadDim + g64 * 64 + i])); + } + const std::uint8_t sb = e4m3_rne(std::fmax(amax / scale_div, 0x1p-9f)); + const float s = e4m3_to_f32(sb); + for (int i = 0; i < 64; i += 2) { + const int d0 = g64 * 64 + i; + const float v0 = + dec[(static_cast(head) * kPageRows + row) * kHeadDim + d0]; + const float v1 = dec[(static_cast(head) * kPageRows + row) * + kHeadDim + d0 + 1]; + const std::uint8_t lo = iso3_out ? iso3_code(v0, s) : e2m1_code(v0 / s); + const std::uint8_t hi = iso3_out ? iso3_code(v1, s) : e2m1_code(v1 / s); + out_codes[static_cast(head) * kNvfp4CodeB + + row * (kHeadDim / 2) + (d0 >> 1)] = + static_cast(lo | (hi << 4)); + } + for (int r = 0; r < 4; ++r) { + out_scales[static_cast(head) * kNvfp4ScaleB + + row * (kHeadDim / 16) + g64 * 4 + r] = sb; + } + } + } + } +} + +int check(bool ok, const char* what, int& failures) { + if (!ok) { + std::printf("FAIL: %s\n", what); + ++failures; + } + return failures; +} + +} // namespace + +int main() { + std::mt19937 rng(20260830); + int failures = 0; + const char* tags[3] = {"nvfp4-g16 K", "int8-g64", "iso3-g16 V"}; + + for (int mode = 0; mode < 3; ++mode) { + PageValues page = make_page(rng); + + std::vector stored(static_cast(kKvHeads) * kPageRows * kHeadDim); + for (int head = 0; head < kKvHeads; ++head) { + for (int row = 0; row < kPageRows; ++row) { + if (mode == 1) { + for (int g = 0; g < kHeadDim / 64; ++g) { + const float s = half_to_float( + page.int8_scales[(static_cast(head) * kPageRows + row) * + (kHeadDim / 64) + g]); + for (int i = 0; i < 64; ++i) { + stored[(static_cast(head) * kPageRows + row) * kHeadDim + + g * 64 + i] = + static_cast( + page.int8_codes[static_cast(head) * kInt8CodeB + + row * kHeadDim + g * 64 + i]) * s; + } + } + continue; + } + for (int g = 0; g < kHeadDim / 16; ++g) { + const std::vector& codes = + mode == 0 ? page.nvfp4_codes : page.iso3_codes; + const std::vector& scales = + mode == 0 ? page.nvfp4_scales : page.iso3_scales; + const float s = e4m3_to_f32( + scales[static_cast(head) * kNvfp4ScaleB + + row * (kHeadDim / 16) + g]); + for (int i = 0; i < 16; ++i) { + const int d = g * 16 + i; + const std::uint8_t byte = + codes[static_cast(head) * kNvfp4CodeB + + row * (kHeadDim / 2) + (d >> 1)]; + const std::uint8_t nib = + (d & 1) != 0 ? static_cast(byte >> 4) + : static_cast(byte & 0x0F); + stored[(static_cast(head) * kPageRows + row) * kHeadDim + d] = + (mode == 0 ? e2m1_to_f32(nib) : iso3_to_f32(nib)) * s; + } + } + } + } + + std::vector expect_codes, expect_scales; + requant_oracle(page, mode, expect_codes, expect_scales); + + const bool int8_source = mode == 1; + GuardedDeviceBuffer dsrc_codes(int8_source + ? static_cast(kKvHeads) * kInt8CodeB + : static_cast(kKvHeads) * kNvfp4CodeB); + GuardedDeviceBuffer dsrc_scales(int8_source + ? static_cast(kKvHeads) * kInt8ScaleB + : static_cast(kKvHeads) * kNvfp4ScaleB); + GuardedDeviceBuffer ddst_codes(static_cast(kKvHeads) * kNvfp4CodeB); + GuardedDeviceBuffer ddst_scales(static_cast(kKvHeads) * kNvfp4ScaleB); + + if (int8_source) { + dsrc_codes.copy_from_host(page.int8_codes.data(), dsrc_codes.bytes()); + dsrc_scales.copy_from_host(page.int8_scales.data(), dsrc_scales.bytes()); + } else if (mode == 0) { + dsrc_codes.copy_from_host(page.nvfp4_codes.data(), dsrc_codes.bytes()); + dsrc_scales.copy_from_host(page.nvfp4_scales.data(), dsrc_scales.bytes()); + } else { + dsrc_codes.copy_from_host(page.iso3_codes.data(), dsrc_codes.bytes()); + dsrc_scales.copy_from_host(page.iso3_scales.data(), dsrc_scales.bytes()); + } + + ops::entropy_cold_requant_raw( + static_cast(dsrc_codes.data()), + static_cast(dsrc_scales.data()), + mode == 0 ? ops::EntropyColdRequantMode::Nvfp4G16 + : mode == 1 ? ops::EntropyColdRequantMode::Int8G64 + : ops::EntropyColdRequantMode::Iso3VG16, + kKvHeads, 1, static_cast(ddst_codes.data()), + static_cast(ddst_scales.data()), nullptr); + cuda_synchronize(); + + std::vector got_codes(static_cast(kKvHeads) * kNvfp4CodeB); + std::vector got_scales(static_cast(kKvHeads) * kNvfp4ScaleB); + ddst_codes.copy_to_host(got_codes.data(), ddst_codes.bytes()); + ddst_scales.copy_to_host(got_scales.data(), ddst_scales.bytes()); + + const char* tag = tags[mode]; + const bool codes_ok = got_codes == expect_codes; + const bool scales_ok = got_scales == expect_scales; + check(codes_ok, (std::string(tag) + " requant codes match oracle").c_str(), failures); + check(scales_ok, (std::string(tag) + " requant scales match oracle").c_str(), failures); + + double num = 0.0, den = 0.0; + const bool iso3_out = mode == 2; + for (std::size_t i = 0; i < stored.size(); ++i) { + const int head = static_cast(i / (static_cast(kPageRows) * kHeadDim)); + const int row = static_cast((i / kHeadDim) % kPageRows); + const int d = static_cast(i % kHeadDim); + const std::uint8_t byte = + got_codes[static_cast(head) * kNvfp4CodeB + row * (kHeadDim / 2) + + (d >> 1)]; + const std::uint8_t nib = + (d & 1) != 0 ? static_cast(byte >> 4) + : static_cast(byte & 0x0F); + const float s = e4m3_to_f32( + got_scales[static_cast(head) * kNvfp4ScaleB + row * (kHeadDim / 16) + + (d >> 4)]); + const float out = iso3_out ? iso3_to_f32(nib) * s : e2m1_to_f32(nib) * s; + num += static_cast(out - stored[i]) * (out - stored[i]); + den += static_cast(stored[i]) * stored[i]; + } + const double nmse = den > 0.0 ? num / den : 0.0; + check(nmse < 0.03, (std::string(tag) + " requant NMSE bound").c_str(), failures); + std::printf("[%s] requant NMSE vs stored = %.5f (codes_ok=%d scales_ok=%d)\n", tag, nmse, + codes_ok ? 1 : 0, scales_ok ? 1 : 0); + } + + if (failures == 0) { std::printf("entropy_cold_requant: all tests passed\n"); } + return failures == 0 ? 0 : 1; +} diff --git a/tests/test_resource_manager.cpp b/tests/test_resource_manager.cpp index ee6667444f..84c34531a4 100644 --- a/tests/test_resource_manager.cpp +++ b/tests/test_resource_manager.cpp @@ -950,8 +950,9 @@ class FakeProgram { void invalidate_resources() noexcept { advance_revision(); } - std::size_t required_pressure_actions = 0; - std::uint32_t private_pressure_alternatives = 1; + std::size_t required_pressure_actions = 0; + std::uint32_t private_pressure_alternatives = 1; + std::optional pressure_optional_target_capacity; std::uint64_t pressure_action_immediate_ns = 100'000'000; std::uint32_t pressure_action_degradation_units = 1; bool include_cumulative_private_target = false; @@ -976,6 +977,7 @@ class FakeProgram { std::uint64_t finish_calls = 0; std::uint64_t abort_calls = 0; std::uint64_t skipped_captures = 0; + std::size_t pressure_target_count_peak = 0; std::uint32_t finish_frontier = 16; std::uint32_t started_source_id = 0; ClaimDisposition started_source_disposition = ClaimDisposition::ConsumedToActive; @@ -1037,6 +1039,8 @@ FakePressurePlanningSession::FakePressurePlanningSession( .stable_ordinal = static_cast(index), }); } + program.pressure_target_count_peak = + std::max(program.pressure_target_count_peak, targets_.size()); if (++program.planning_generation_ == 0) { ++program.planning_generation_; } ++program.pressure_planning_sessions; generation_ = program.planning_generation_; @@ -1295,6 +1299,13 @@ FakePressurePlanningSession::commit_expansion(FakePreparedPressureExpansion&& pr require(scratch_live_ && prepared.generation == generation_ && prepared.scratch_generation == scratch_generation_, "fake prepared expansion is stale"); + if (program_->pressure_optional_target_capacity) { + const std::size_t maximum = + candidates_.size() + 1U + *program_->pressure_optional_target_capacity; + if (prepared.new_count > maximum - std::min(maximum, targets_.size())) { + throw std::length_error("prepared pressure expansion exceeds the target arena"); + } + } committed_children_.clear(); std::uint32_t new_count = 0; for (Target& child : expansion_scratch_) { @@ -1312,6 +1323,8 @@ FakePressurePlanningSession::commit_expansion(FakePreparedPressureExpansion&& pr committed_children_.push_back( FakePressureTargetHandle{.generation = generation_, .index = index}); } + program_->pressure_target_count_peak = + std::max(program_->pressure_target_count_peak, targets_.size()); require(new_count == prepared.new_count, "fake expansion count changed before commit"); expansion_scratch_.clear(); scratch_live_ = false; @@ -1574,6 +1587,55 @@ void test_shared_capture_subtracts_private_transition_loss() { "shared capture gain did not subtract the private capability transition loss"); } +void test_shared_capture_budget_bounds_committed_canonical_targets() { + using Planner = ninfer::runtime::SharedCapturePlanner; + + constexpr std::size_t private_owner_count = 16; + constexpr std::size_t shared_owner_count = 4; + constexpr std::uint32_t target_budget = 64; + + FakeProgram program; + program.required_pressure_actions = private_owner_count + shared_owner_count + 1U; + program.pressure_optional_target_capacity = target_budget; + + std::array private_handles; + std::array private_owners; + std::array private_ordinals; + for (std::size_t index = 0; index < private_owner_count; ++index) { + private_handles[index] = FakeContinuationHandle(static_cast(index + 1U), 0); + private_owners[index] = &private_handles[index]; + private_ordinals[index] = static_cast(index); + } + + std::array shared_handles; + std::array shared_owners; + std::array shared_ordinals; + for (std::size_t index = 0; index < shared_owner_count; ++index) { + shared_handles[index].id = static_cast(index + 1U); + shared_owners[index] = &shared_handles[index]; + shared_ordinals[index] = static_cast(private_owner_count + index); + } + + const FakeCaptureAssessment capture{ + .shortlist_key = FakeShortlistKey{.digest = 91, .frontier = 64}, + .publishes_shared = true, + .physically_feasible = false, + }; + Planner planner; + const auto result = planner.plan(program, test_cost_model(), + Planner::Input{ + .capture = &capture, + .private_owners = private_owners, + .private_owner_ordinals = private_ordinals, + .shared_owners = shared_owners, + .shared_owner_ordinals = shared_ordinals, + .target_budget = target_budget, + }); + require(!result, "bounded infeasible shared-capture search unexpectedly found a plan"); + require(program.pressure_target_count_peak <= target_budget, + "shared-capture search committed more canonical targets than its budget"); +} + void test_equal_lower_bound_does_not_short_circuit_tie_break() { using Planner = ninfer::runtime::MaterializationPlanner; @@ -2365,6 +2427,8 @@ int main() { run_test("portfolio demand and owner aggregation", test_portfolio_demand_and_owner_aggregation); run_test("shared capture private transition loss", test_shared_capture_subtracts_private_transition_loss); + run_test("shared capture committed target budget", + test_shared_capture_budget_bounds_committed_canonical_targets); run_test("equal lower-bound tie-break", test_equal_lower_bound_does_not_short_circuit_tie_break); run_test("feasible identity pressure improvement", diff --git a/tools/calib/analyze_kv.py b/tools/calib/analyze_kv.py new file mode 100644 index 0000000000..97ea5a9238 --- /dev/null +++ b/tools/calib/analyze_kv.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python +# Offline KV dynamic-precision calibration for NInfer Qwen3.6-family artifacts. +# +# Consumes the .kvc frames produced by `ninfer-cli --kv-calib-dir DIR` (exact +# post-RoPE K and V per full-attention layer and prefill chunk) and produces a +# static per-layer dtype table in the format accepted by `--kv-layer-storage`. +# +# Implemented metrics (the four requested techniques, fused into one decision): +# MixKVQ K/V error asymmetry weighting (K weighted above V). +# TriAxialKV per-layer, per-head, per-dimension-group outlier scores. +# ARKV effective rank (spectral spread) of K and V per head. +# KVTuner greedy sensitivity ranking under an explicit memory budget. +# +# The decision space is per-layer same-dtype storage (bf16 | int8 | nvfp4); +# the runtime's layer_kv_dtypes table consumes the selected map. +import argparse +import json +import math +import struct +import sys +from pathlib import Path + +import numpy as np + +HEADER = struct.Struct("<16s6I2i4I") +MAGIC = b"NINFERKVCAL1\x00\x00\x00\x00" + +E2M1_VALUES = np.array([0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0], dtype=np.float64) +E2M1_EDGES = np.array([0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5.0], dtype=np.float64) + + +def e4m3fn(x: float) -> float: + """Mimic the runtime gqa_kv_nvfp4_fp32_to_e4m3 (positive scale path).""" + if not (x > 0.0): + return 0.0 + b = struct.unpack("> 23) & 0xFF) - 127 + 7 + if exp >= 15: + return 448.0 / 512.0 * 2**7 # saturate (0b01111111 = 448) + if exp <= 0: + mant = int(round(x * 64.0)) + if mant <= 0: + return 0.0 + if mant >= 8: + return 1.0 + return mant / 512.0 + mant = (b >> 20) & 0x7 + guard = (b >> 19) & 1 + sticky = b & 0x7FFFF + if guard and (sticky or (mant & 1)): + mant += 1 + if mant > 7: + mant = 0 + exp += 1 + if exp >= 15: + return 448.0 / 512.0 * 2**7 + return (1.0 + mant / 8.0) * 2 ** (exp - 7) + + +def quantize_e2m1_group16(x: np.ndarray) -> np.ndarray: + x = np.nan_to_num(x.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + # groups along the last axis (head dim), 16 values each. + groups = x.reshape(*x.shape[:-1], -1, 16) + amax = np.abs(groups).max(axis=-1, keepdims=True) + scale = np.maximum(amax / 6.0, 2.0**-9) + vq = np.array([e4m3fn(float(v)) for v in scale.ravel()], dtype=np.float64).reshape(scale.shape) + q = np.abs(groups) / vq + codes = np.searchsorted(E2M1_EDGES, q).astype(np.int64) + decoded = np.where(groups < 0, -1.0, 1.0) * E2M1_VALUES[codes] * vq + return decoded.reshape(x.shape) + + +def quantize_int8_group64(x: np.ndarray) -> np.ndarray: + x = np.nan_to_num(x.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + tail = x.shape[-1] % 64 + if tail: + pad = 64 - tail + x = np.concatenate([x, np.zeros((*x.shape[:-1], pad), dtype=np.float64)], axis=-1) + groups = x.reshape(*x.shape[:-1], -1, 64) + amax = np.abs(groups).max(axis=-1, keepdims=True) + scale = np.maximum(amax / 127.0, 1e-30) + scale = scale.astype(np.float16).astype(np.float64) + q = np.clip(np.round(groups / scale), -127, 127) + decoded = q * scale + if tail: + decoded = decoded[..., :tail] + return decoded.reshape(x.shape) + + +def quantize_fp8_group16(x: np.ndarray) -> np.ndarray: + x = np.nan_to_num(x.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + groups = x.reshape(*x.shape[:-1], -1, 16) + amax = np.abs(groups).max(axis=-1, keepdims=True) + scale = np.maximum(amax / 448.0, 2.0**-9) + q = np.clip(np.round(groups / scale), -448, 448) + decoded = q * scale + return decoded.reshape(x.shape) + + +def quantize_iso3_group16(x: np.ndarray) -> np.ndarray: + x = np.nan_to_num(x.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + groups = x.reshape(*x.shape[:-1], -1, 16) + amax = np.abs(groups).max(axis=-1, keepdims=True) + scale = np.maximum(amax / 7.0, 2.0**-9) + q = np.clip(np.round(groups / scale), -7, 7) + decoded = q * scale + return decoded.reshape(x.shape) + + +def nmse(x: np.ndarray, y: np.ndarray) -> float: + x = np.nan_to_num(x.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + y = np.nan_to_num(y.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + num = np.sum((x - y) ** 2) + den = np.sum(x**2) + return float(num / den) if den > 0 else 0.0 + + +def effective_rank(x: np.ndarray) -> float: + x = np.nan_to_num(x.astype(np.float64), nan=0.0, posinf=0.0, neginf=0.0) + if x.shape[0] > x.shape[1]: + eig = np.linalg.eigvalsh(x.T @ x) + else: + eig = np.linalg.eigvalsh(x @ x.T) + s = np.sqrt(np.maximum(eig, 0.0)) + if s.size == 0 or s[0] <= 0: + return 0.0 + p = s / s[0] + den = np.sum(p**2) + return float(np.sum(p) ** 2 / den) if den > 0 else 0.0 + + +def outlier_scores(x: np.ndarray) -> tuple[float, float]: + x = np.nan_to_num(np.asarray(x, dtype=np.float64), nan=0.0, posinf=0.0, neginf=0.0) + rms = math.sqrt(float(np.mean(x**2))) + if rms == 0: + return 0.0, 0.0 + head = float(np.mean((np.abs(x).max(axis=-1) > 6.0 * rms).astype(np.float64))) + dim_group = float( + np.mean( + ( + np.abs(x).max(axis=-2) + > 6.0 * rms * math.sqrt(x.shape[-2]) + ).astype(np.float64) + ) + ) + return head, dim_group + + +def load_frames(directory: Path): + frames = [] + for path in sorted(directory.glob("*.kvc")): + raw = path.read_bytes() + if len(raw) < HEADER.size: + raise SystemExit(f"truncated record: {path}") + magic, header_bytes, layer, head_dim, kv_heads, tokens, record_index, first_pos, last_pos, *_ = ( + HEADER.unpack(raw[: HEADER.size]) + ) + if magic != MAGIC or header_bytes != HEADER.size: + raise SystemExit(f"bad record header: {path}") + payload = np.frombuffer(raw, dtype=np.uint8, offset=HEADER.size) + expect = (tokens * 4) + 2 * head_dim * kv_heads * tokens * 2 + if payload.size != expect: + raise SystemExit(f"bad record payload: {path}") + pos = payload[: tokens * 4].copy().view(np.int32) + arr = np.frombuffer(payload[tokens * 4 :].tobytes(), dtype=" 16: + raise SystemExit(f"too many layers for the runtime table: {n_layers}") + + # Layer statistics accumulated over frames with token weighting. + stats = { + layer: { + "tokens": 0, + "k_nmse": {"nvfp4": 0.0, "int8": 0.0, "fp8": 0.0, "iso3": 0.0}, + "v_nmse": {"nvfp4": 0.0, "int8": 0.0, "fp8": 0.0, "iso3": 0.0}, + "k_rank": 0.0, + "v_rank": 0.0, + "k_outlier_head": 0.0, + "v_outlier_head": 0.0, + "k_outlier_group": 0.0, + "v_outlier_group": 0.0, + } + for layer in layers + } + + for frame in frames: + layer = frame["layer"] + weight = frame["tokens"] + stat = stats[layer] + stat["tokens"] += weight + k = frame["k"] + v = frame["v"] + stat["k_nmse"]["nvfp4"] += nmse(k, quantize_e2m1_group16(k)) * weight + stat["k_nmse"]["int8"] += nmse(k, quantize_int8_group64(k)) * weight + stat["k_nmse"]["fp8"] += nmse(k, quantize_fp8_group16(k)) * weight + stat["k_nmse"]["iso3"] += nmse(k, quantize_iso3_group16(k)) * weight + stat["v_nmse"]["nvfp4"] += nmse(v, quantize_e2m1_group16(v)) * weight + stat["v_nmse"]["int8"] += nmse(v, quantize_int8_group64(v)) * weight + stat["v_nmse"]["fp8"] += nmse(v, quantize_fp8_group16(v)) * weight + stat["v_nmse"]["iso3"] += nmse(v, quantize_iso3_group16(v)) * weight + for h in range(k.shape[1]): + stat["k_rank"] += effective_rank(k[:, h, :]) * weight + stat["v_rank"] += effective_rank(v[:, h, :]) * weight + ok_head, ok_group = outlier_scores(k) + ov_head, ov_group = outlier_scores(v) + stat["k_outlier_head"] += ok_head * weight + stat["v_outlier_head"] += ov_head * weight + stat["k_outlier_group"] += ok_group * weight + stat["v_outlier_group"] += ov_group * weight + + rows = [] + for layer in layers: + stat = stats[layer] + w = stat["tokens"] + k_err = stat["k_nmse"]["nvfp4"] / w + v_err = stat["v_nmse"]["nvfp4"] / w + mix_err = args.k_weight * k_err + (1.0 - args.k_weight) * v_err + triaxial = max( + stat["k_outlier_head"], stat["v_outlier_head"], + stat["k_outlier_group"], stat["v_outlier_group"], + ) / max(w, 1) + rank = 0.5 * (stat["k_rank"] / w + stat["v_rank"] / w) + # Normalized 0..1 scores across layers for greedy ranking. + rows.append( + { + "layer": layer, + "tokens": w, + "nmse_nvfp4_k": k_err, + "nmse_nvfp4_v": v_err, + "nmse_int8_k": stat["k_nmse"]["int8"] / w, + "nmse_int8_v": stat["v_nmse"]["int8"] / w, + "nmse_fp8_k": stat["k_nmse"]["fp8"] / w, + "nmse_fp8_v": stat["v_nmse"]["fp8"] / w, + "nmse_iso3_k": stat["k_nmse"]["iso3"] / w, + "nmse_iso3_v": stat["v_nmse"]["iso3"] / w, + "mixkvq_error": mix_err, + "triaxial_outlier": triaxial, + "arkv_rank": rank, + } + ) + for key in ("mixkvq_error", "triaxial_outlier", "arkv_rank"): + lo = min(row[key] for row in rows) + hi = max(row[key] for row in rows) + for row in rows: + row[f"{key}_norm"] = 0.0 if hi <= lo else (row[key] - lo) / (hi - lo) + for row in rows: + row["sensitivity"] = ( + 0.50 * row["mixkvq_error_norm"] + + 0.25 * row["triaxial_outlier_norm"] + + 0.25 * row["arkv_rank_norm"] + ) + + # Storage cost per token per layer (K+V), in bytes; nvfp4 is the budget base. + head_dim = frames[0]["k"].shape[-1] + kv_heads = frames[0]["k"].shape[1] + def cost(dtype: str) -> float: + if dtype == "nvfp4" or dtype == "iso3": + return kv_heads * (head_dim + 2 * (head_dim / 16)) + if dtype == "int8": + return kv_heads * (2 * head_dim + 4 * (head_dim / 64)) + if dtype == "fp8": + return kv_heads * (2 * head_dim + 2 * (head_dim / 16)) + return kv_heads * 4 * head_dim + + base = n_layers * cost("nvfp4") + ranked = sorted(rows, key=lambda row: row["sensitivity"], reverse=True) + table = {layer: "nvfp4" for layer in layers} + used = base + for row in ranked: + for dtype in ("fp8", "int8", "bf16"): + delta = cost(dtype) - cost(table[row["layer"]]) + if used + delta <= args.budget * base: + table[row["layer"]] = dtype + used += delta + break + + spec = ",".join(f"{layer}:{table[layer]}" for layer in layers) + report = { + "record_frames": len(frames), + "layer_count": n_layers, + "head_dim": head_dim, + "kv_heads": kv_heads, + "budget_factor": args.budget, + "k_weight": args.k_weight, + "per_layer": sorted(rows, key=lambda row: row["layer"]), + "selected_table": table, + "kv_layer_storage_spec": spec, + "relative_cost": used / base, + } + Path(args.out).write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8") + print(f"frames={len(frames)} layers={n_layers} budget={args.budget:.2f}x -> " + f"cost={used / base:.3f}x") + print("--kv-layer-storage " + spec) + + +if __name__ == "__main__": + main() diff --git a/tools/calib/pca_kv_feasibility.py b/tools/calib/pca_kv_feasibility.py new file mode 100644 index 0000000000..12ca9b32d0 --- /dev/null +++ b/tools/calib/pca_kv_feasibility.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# PCA + entropy feasibility for NVFP4 K/V code nibbles. +# +# Reproduces the production post-rotation code distribution, then tests +# per-16-channel PCA rotations and reports rANS stream sizes for the fixed +# slot codec (16 streams of 512 nibbles per half, shared per-half frequencies). +import argparse +import glob +import re +import struct +import sys +from pathlib import Path + +import numpy as np + +sys_path = Path(__file__).resolve().parents[2] / "tools" / "calib" +sys.path.insert(0, str(sys_path)) +import rans_nvfp4 as rans # noqa: E402 + + +def load_kv(path): + raw = Path(path).read_bytes() + hdr = struct.unpack("<16s6I2i4I", raw[:64]) + tokens = hdr[5] + arr = np.frombuffer(raw, dtype="thj", so4[block], x[:, :, base : base + 4]) + return out + + def apply_basis(x, basis, block): + out = np.empty_like(x) + for b in range(x.shape[-1] // block): + base = b * block + out[:, :, base : base + block] = np.einsum( + "jk,thk->thj", basis, x[:, :, base : base + block]) + return out + + for name, x in [("K", k_all), ("V", v_all)]: + so4_codes, _ = quantize_codes(apply_so4(x)) + so4_sizes = slot_sizes(so4_codes) + print(f"{name} SO(4): max={max(so4_sizes[0] + so4_sizes[1])} " + f"h0={max(so4_sizes[0])} h1={max(so4_sizes[1])} " + f"over166={max(so4_sizes[0] + so4_sizes[1]) > 166}") + + for block in (16, 64): + basis, _ = pca_basis(apply_so4(x), block) + pca_codes, _ = quantize_codes(apply_basis(apply_so4(x), basis, block)) + pca_sizes = slot_sizes(pca_codes) + print(f"{name} SO(4)+PCA{block}: max={max(pca_sizes[0] + pca_sizes[1])} " + f"h0={max(pca_sizes[0])} h1={max(pca_sizes[1])} " + f"over166={max(pca_sizes[0] + pca_sizes[1]) > 166}") + + raw_basis, _ = pca_basis(x, block) + raw_codes, _ = quantize_codes(apply_basis(x, raw_basis, block)) + raw_sizes = slot_sizes(raw_codes) + print(f"{name} PCA{block}: max={max(raw_sizes[0] + raw_sizes[1])} " + f"h0={max(raw_sizes[0])} h1={max(raw_sizes[1])} " + f"over166={max(raw_sizes[0] + raw_sizes[1]) > 166}") + + +if __name__ == "__main__": + main() diff --git a/tools/calib/rans_nvfp4.py b/tools/calib/rans_nvfp4.py new file mode 100644 index 0000000000..bae7074837 --- /dev/null +++ b/tools/calib/rans_nvfp4.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# CPU reference order-0 static rANS for NVFP4 E2M1 code nibbles. +# Feasibility gate for the entropy-coded cold KV pool. +import argparse +import glob +import struct +from pathlib import Path + +import numpy as np + +MAGIC = b"NINFERKVCAL1\x00\x00\x00\x00" +SCALE_BITS = 12 +SCALE = 1 << SCALE_BITS +MASK = SCALE - 1 +BYTE_L = 1 << 23 + + +def build_freqs(symbols): + counts = np.bincount(symbols, minlength=16).astype(np.int64) + freqs = np.maximum(1, np.round(counts / counts.sum() * SCALE).astype(np.int64)) + diff = int(SCALE - freqs.sum()) + while diff > 0: + idx = int(np.argmax(counts)) + freqs[idx] += 1 + counts[idx] = max(0, counts[idx] - 1) + diff -= 1 + while diff < 0: + idx = int(np.argmax(np.where(freqs > 1, freqs, 0))) + freqs[idx] -= 1 + diff += 1 + return freqs + + +def encode(symbols, freqs): + start = np.concatenate(([0], np.cumsum(freqs)[:-1])).astype(np.int64) + out = bytearray() + x = BYTE_L + for s in reversed(symbols): + f = int(freqs[s]) + x_max = ((BYTE_L >> SCALE_BITS) << 8) * f + while x >= x_max: + out.append(x & 0xFF) + x >>= 8 + x = ((x // f) << SCALE_BITS) + (x % f) + int(start[s]) + out.extend(x.to_bytes(4, "little")) + return bytes(out), start + + +def decode(data, freqs, start, count): + x = int.from_bytes(data[-4:], "little") + inb = bytearray(data[:-4]) + out = [] + for _ in range(count): + slot = x & MASK + s = int(np.searchsorted(start, slot, side="right") - 1) + out.append(s) + x = int(freqs[s]) * (x >> SCALE_BITS) + slot - int(start[s]) + while x < BYTE_L: + x = (x << 8) | inb.pop() + return out + + +def load_k(path): + raw = Path(path).read_bytes() + hdr = struct.unpack("<16s6I2i4I", raw[:64]) + if hdr[0] != MAGIC: + raise ValueError(path) + tokens = hdr[5] + arr = np.frombuffer(raw, dtype="