Spaces:
Sleeping
Sleeping
File size: 41,790 Bytes
92dcc4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 | #pragma once
#include "kernels.hpp"
#include <atomic>
#include <cstddef>
#include <cstdint>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <random>
#include <shared_mutex>
#include <string>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>
namespace cism {
constexpr std::size_t max_weight_bytes = std::size_t{64} * 1024 * 1024 * 1024;
constexpr std::size_t max_kv_bytes = std::size_t{8} * 1024 * 1024 * 1024;
std::size_t checked_mul(std::size_t a, std::size_t b, const char* label);
std::size_t checked_add(std::size_t a, std::size_t b, const char* label);
struct Config {
std::string model_type;
std::size_t hidden, intermediate, layers, heads, kv_heads, head_dim, vocab, context;
float eps, rope_theta;
bool tied;
void validate() const;
};
struct SurjoConfig {
std::string model_type = "surjo";
std::size_t hidden = 0, intermediate = 0, layers = 0;
std::size_t heads = 0, kv_heads = 0, head_dim = 0, vocab = 0, context = 0;
std::size_t prelude = 0, recurrent = 0, coda = 0, groups = 0, passes = 0, per_xsa = 0;
std::size_t gdn_v_heads = 0, gdn_k_dim = 0, gdn_v_dim = 0, conv_kernel = 4;
bool allow_neg = false, xsa_projection = true, tied = false;
float eps = 1e-5f, rope_theta = 10000.0f;
void validate() const;
std::size_t num_xsa_slots() const { return prelude + groups * passes + coda; }
bool is_xsa_layer(std::size_t li) const;
};
using WeightMap = std::map<std::string, std::vector<float>>;
using ShapeMap = std::map<std::string, std::vector<std::size_t>>;
ShapeMap weight_shapes(const Config& config, bool has_head);
ShapeMap surjo_weight_shapes(const SurjoConfig& config, bool has_head);
enum class Storage { fp32, fp16, int8, int4, fp4 };
// Persistent worker threads for row-parallel matvecs. The caller thread is
// always worker zero, so a single-threaded model never spawns anything.
// Barriers spin briefly, then sleep, keeping per-matrix handoff cost in the
// hundreds of nanoseconds while the pool idles at zero CPU when unused.
class WorkerPool {
public:
// threads = total participants including the caller; 1 means no workers.
explicit WorkerPool(std::size_t threads);
~WorkerPool();
WorkerPool(const WorkerPool&) = delete;
WorkerPool& operator=(const WorkerPool&) = delete;
std::size_t thread_count() const { return workers_ + 1; }
// task(begin, end) executes that participant's half-open row slice;
// WorkerPool computes identical slices for the caller (worker zero) and
// every background worker from the shared row count. const because Models
// own their pool behind shared_ptr<const Model> in live sessions.
void run(std::function<void(std::size_t, std::size_t)> task, std::size_t rows) const;
private:
void worker_loop(std::size_t index);
std::size_t workers_;
std::vector<std::thread> threads_;
mutable std::mutex run_mutex_;
mutable std::function<void(std::size_t, std::size_t)> task_;
mutable std::atomic<std::size_t> rows_{0};
mutable std::atomic<std::uint64_t> generation_{0};
// Cumulative completions; the target for generation G is G * workers_.
mutable std::atomic<std::size_t> completed_{0};
std::atomic<bool> stopping_{false};
};
class Matrix {
public:
Matrix() = default;
Matrix(std::vector<float> values, std::size_t rows, std::size_t cols, Storage storage);
// Row-slice executor shared by gemm() and pair fusion: runs rows
// [begin, end) with the exact default-path arithmetic (same kernels and
// order as the historical gemm bodies). Thread-safe on disjoint slices
// (reads X/weights/perm_ptr, writes disjoint out rows).
// Raw pointer to the first weight byte (any storage): lets callers
// prefetch the next layer's matrices across layer boundaries (exact-
// neutral hint; never dereferenced here).
const void* stream_head() const;
void run_rows(const float* X, float* out, std::size_t tokens,
std::size_t begin, std::size_t end, const float* perm_ptr) const;
void multiply(const std::vector<float>& input, std::vector<float>& output,
const WorkerPool* pool = nullptr, std::vector<float>* scratch = nullptr) const;
// Multi-token evaluation: X is tokens x cols (token-major), out is
// tokens x rows. Weight rows stream once for all tokens, which amortizes
// DRAM traffic for oversized models during speculative verification.
void gemm(const float* X, float* out, std::size_t tokens, const WorkerPool* pool,
std::vector<float>* scratch = nullptr) const;
void row(std::size_t index, std::vector<float>& output) const;
std::size_t bytes() const;
using Regions = std::vector<std::pair<void*, std::size_t>>;
void collect(Regions& regions) const;
// Quantized-activation dispatch (int8 activations, pmaddwd kernels).
// Only takes effect on quantized storage; fp32 matrices ignore it.
void set_act_q8(bool enabled) { act_q8_ = enabled; }
private:
std::size_t rows_ = 0, cols_ = 0;
Storage storage_ = Storage::fp32;
bool act_q8_ = false;
std::vector<float> floats_, scales_;
// FP16 (IEEE-754 binary16) weight storage: half the bytes of fp32,
// widened to fp32 on load in the dot kernels. No scales.
std::vector<std::uint16_t> fp16_;
std::vector<std::int8_t> int8_;
std::vector<std::uint8_t> int4_;
// FP4 (E2M1) nibble packing plus one E4M3 scale byte per 16 elements.
std::vector<std::uint8_t> fp4_, fp4_scales_;
};
struct Layer {
std::vector<float> input_norm, post_norm, q_norm, k_norm;
Matrix q, k, v, o, gate, up, down;
};
class Session;
class PagedSession;
class BatchSession;
// ---- Native paged KV BlockPool (PagedAttention-style, dense Llama/Qwen3) ----
// Physical KV storage is two dense pools:
// k_pages / v_pages : [num_blocks][num_layers][block_size][kv_width] fp32.
// free_stack is LIFO (allocate pops, free pushes); table maps req_id ->
// resident phys block ids in allocation order. Block size is fixed at 16
// tokens to match the Python paging simulator (src/cism/paging.py).
//
// Stream S2 (cross-request reuse / COW prefix sharing / eviction):
// - Blocks are refcounted. Plain allocate/ensure/free never share, so the
// historical LIFO exact behavior is unchanged (refcount stays 1).
// - fork(dst, src, prefix_len) shares src's whole leading prefix blocks
// without copying (refcount++); a non-aligned tail is copied once into a
// fresh private block. Writes to shared blocks must go through
// copy_on_write() first, which privatizes the block (allocate + full-block
// memcpy). Shared values are never mutated, so paged math stays
// bitwise-identical to dense.
// - Eviction is recompute-not-swap on CPU: evict_one() frees the
// newest-lowest-priority request (lowest priority value; ties broken by
// newest sequence number) and the victim recomputes from scratch on resume.
// No KV is ever swapped to host.
// - Cross-request reuse lives one level up: Model owns a SharedPagedState
// (one BlockPool per model shape, grown once, never per session), so steady
// state sessions allocate zero KV pages on the hot path.
constexpr std::size_t kPagedBlockSize = 16;
// Default cap for the shared per-model pool (env CISM_PAGED_MAX_BLOCKS
// overrides; clamped to the 8 GiB KV safety limit for the model shape).
constexpr std::size_t kPagedSharedBlocksDefault = 256;
class BlockPool {
public:
BlockPool() = default;
BlockPool(std::size_t num_blocks, std::size_t num_layers, std::size_t kv_width,
std::size_t block_size = kPagedBlockSize);
BlockPool(const BlockPool&) = delete;
BlockPool& operator=(const BlockPool&) = delete;
BlockPool(BlockPool&&) noexcept = default;
BlockPool& operator=(BlockPool&&) noexcept = default;
std::size_t num_blocks() const { return num_blocks_; }
std::size_t num_layers() const { return num_layers_; }
std::size_t kv_width() const { return kv_width_; }
std::size_t block_size() const { return block_size_; }
std::size_t blocks_needed(std::size_t seq_len) const;
bool can_allocate(std::size_t seq_len) const;
// Allocate ceil(seq_len/block_size) blocks for req_id. Throws
// std::invalid_argument on duplicate req_id, std::runtime_error when out
// of free blocks (no partial allocation). Returns a copy of the resident
// block list.
std::vector<std::size_t> allocate(std::int64_t req_id, std::size_t seq_len);
// Grow req_id to cover new_len tokens (no shrink; no-op when covered).
// Throws std::invalid_argument on unknown req_id, std::runtime_error on
// OOM (existing prefix is preserved).
std::vector<std::size_t> ensure(std::int64_t req_id, std::size_t new_len);
// Release all blocks for req_id. Throws std::invalid_argument on unknown
// req_id (covers double-free). Returns the freed block list.
// Refcount-aware: shared phys blocks are only returned to free_stack
// once their last referencing request frees them.
std::vector<std::size_t> free(std::int64_t req_id);
// Share src's leading prefix_len tokens with a new req dst (no data copy
// for whole blocks; the partial tail block, if any, is copied once into
// a fresh private block). Requires prompt[:prefix_len] to be exactly
// equal upstream (checked by the session fork path, not here). Throws
// std::invalid_argument on duplicate dst / unknown src / prefix beyond
// src's allocation, std::runtime_error on OOM (state unchanged).
// Returns a copy of dst's resident block list.
std::vector<std::size_t> fork(std::int64_t dst_req, std::int64_t src_req,
std::size_t prefix_len);
// Make the block holding position pos of req_id private: when its
// refcount is 1 this is a no-op, otherwise a fresh block is allocated,
// the full block contents (all layers/slots, K and V) are copied, and the
// table entry is swapped. Must be called before writing position pos.
// Throws std::invalid_argument on unknown req_id / out-of-range pos,
// std::runtime_error on OOM (existing mapping preserved).
void copy_on_write(std::int64_t req_id, std::size_t pos);
// Evict one request under pressure: the newest request among those with
// the lowest priority value (ties -> largest sequence number), excluding
// except_req. Returns the victim id, or -1 when nothing is evictable.
// Recompute-not-swap: the victim's blocks are released; it must restart
// from scratch (no KV is preserved, nothing is swapped to host).
std::int64_t evict_one(std::int64_t except_req = -1);
void set_priority(std::int64_t req_id, int priority);
int priority(std::int64_t req_id) const;
// Grow the pool by extra_blocks physical blocks (LIFO-appended; new pages
// zero-filled). Never shrinks. Throws on 8 GiB cap overflow.
void grow(std::size_t extra_blocks);
bool contains(std::int64_t req_id) const;
const std::vector<std::size_t>& get_blocks(std::int64_t req_id) const;
const std::vector<std::size_t>& free_stack() const { return free_stack_; }
const std::map<std::int64_t, std::vector<std::size_t>>& table() const { return table_; }
// Refcount of a physical block (0 == free). Throws on out-of-range phys.
std::size_t refcount(std::size_t phys) const;
std::size_t used() const;
// Physically occupied blocks (accounts for sharing; used() is logical and
// double-counts shared blocks by design to match the Python simulator).
std::size_t phys_used() const { return num_blocks_ - free_stack_.size(); }
std::size_t free_count() const { return free_stack_.size(); }
std::size_t total() const { return num_blocks_; }
std::tuple<std::size_t, std::size_t, std::size_t> usage() const;
std::size_t kv_bytes() const;
std::size_t kv_elements() const;
const std::vector<float>& k_pages() const { return k_pages_; }
const std::vector<float>& v_pages() const { return v_pages_; }
// Raw gather helpers: pointer to [kv_width] floats for
// (phys block, layer, slot-in-block). Caller guarantees bounds.
float* k_ptr(std::size_t phys, std::size_t layer, std::size_t slot);
const float* k_ptr(std::size_t phys, std::size_t layer, std::size_t slot) const;
float* v_ptr(std::size_t phys, std::size_t layer, std::size_t slot);
const float* v_ptr(std::size_t phys, std::size_t layer, std::size_t slot) const;
// Fast unchecked row access for the decode hot loop (same indices as
// k_ptr/v_ptr; bounds guaranteed by allocate/ensure + validated shapes).
// Plain arithmetic: bitwise-identical addressing without per-access
// overflow checks.
const float* k_row_fast(std::size_t phys, std::size_t layer, std::size_t slot) const {
return k_pages_.data() + ((phys * num_layers_ + layer) * block_size_ + slot) * kv_width_;
}
float* k_row_fast(std::size_t phys, std::size_t layer, std::size_t slot) {
return k_pages_.data() + ((phys * num_layers_ + layer) * block_size_ + slot) * kv_width_;
}
const float* v_row_fast(std::size_t phys, std::size_t layer, std::size_t slot) const {
return v_pages_.data() + ((phys * num_layers_ + layer) * block_size_ + slot) * kv_width_;
}
float* v_row_fast(std::size_t phys, std::size_t layer, std::size_t slot) {
return v_pages_.data() + ((phys * num_layers_ + layer) * block_size_ + slot) * kv_width_;
}
// Base pointer to slot 0 of (phys, layer): the hot loop advances by
// slot * kv_width and reloads the base on block boundaries, avoiding a
// division/modulo per inner-loop step.
const float* k_base_fast(std::size_t phys, std::size_t layer) const {
return k_pages_.data() + ((phys * num_layers_ + layer) * block_size_) * kv_width_;
}
const float* v_base_fast(std::size_t phys, std::size_t layer) const {
return v_pages_.data() + ((phys * num_layers_ + layer) * block_size_) * kv_width_;
}
float* k_base_fast(std::size_t phys, std::size_t layer) {
return k_pages_.data() + ((phys * num_layers_ + layer) * block_size_) * kv_width_;
}
float* v_base_fast(std::size_t phys, std::size_t layer) {
return v_pages_.data() + ((phys * num_layers_ + layer) * block_size_) * kv_width_;
}
private:
std::size_t offset_of(std::size_t phys, std::size_t layer, std::size_t slot) const;
std::size_t pop_free();
void release_table_entry(std::int64_t req_id);
std::size_t num_blocks_ = 0, num_layers_ = 0, kv_width_ = 0;
std::size_t block_size_ = kPagedBlockSize;
std::vector<float> k_pages_, v_pages_;
std::vector<std::size_t> free_stack_;
std::map<std::int64_t, std::vector<std::size_t>> table_;
// S2: per-phys refcount (0 == free), per-req priority (default 0), and a
// monotonic admission sequence for newest-lowest-priority eviction.
std::vector<std::size_t> refcounts_;
std::map<std::int64_t, int> prio_;
std::map<std::int64_t, std::uint64_t> seq_;
std::uint64_t seq_ctr_ = 0;
};
// ---- Shared cross-request paged KV store (Stream S2) ----
// One BlockPool per model shape, owned by the Model and borrowed by every
// PagedSession of that model. Pages are allocated once (grown never shrunk);
// steady-state sessions only insert/erase small table entries, so there is
// zero KV-page allocation on the hot path. All BlockPool calls must hold mu
// (PagedSession holds it exclusively for the whole forward, matching the
// WorkerPool serialization the engine already imposes).
struct SharedPagedState {
mutable std::shared_mutex mu;
BlockPool pool;
bool init = false;
std::uint64_t req_ctr = 0;
std::size_t max_blocks = 0;
};
class Model : public std::enable_shared_from_this<Model> {
public:
Model(Config config, WeightMap weights, std::string precision, std::size_t threads = 1,
const std::string& act_precision = "fp32");
// "fp32" keeps exact FP32 activations; "int8" quantizes activations per
// 32-element block (32767/absmax) for the pmaddwd kernel path.
void set_act_precision(const std::string& act_precision);
std::shared_ptr<Session> create_session(std::vector<std::int64_t> prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, std::vector<std::int64_t> eos);
std::shared_ptr<PagedSession> create_paged_session(std::vector<std::int64_t> prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, std::vector<std::int64_t> eos);
// Fork a paged session sharing src's leading prefix_len tokens (COW, no
// copy of whole blocks). Requires prompt[:prefix_len] ==
// src.history[:prefix_len] exactly; throws std::invalid_argument
// otherwise (correctness first: shared KV is only valid for identical
// prefix tokens at identical positions). The child prefills only the
// suffix; shared blocks stay read-only for the child (COW-guarded).
std::shared_ptr<PagedSession> create_paged_fork_session(
std::vector<std::int64_t> prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, std::vector<std::int64_t> eos,
std::shared_ptr<PagedSession> src, std::size_t prefix_len,
int priority = 0);
std::shared_ptr<BatchSession> create_batch_session(
std::vector<std::vector<std::int64_t>> prompts,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, std::vector<std::int64_t> eos,
// Optional per-sequence RNG seeds (serving: each job keeps its own
// serial seed, so batched sampled decode is token-identical to
// serial). Empty = legacy seed+i. Non-empty must match prompts.
const std::vector<std::uint64_t>& seq_seeds = {});
std::vector<float> logits(const std::vector<std::int64_t>& prompt);
std::vector<float> paged_logits(const std::vector<std::int64_t>& prompt);
// Teacher-forced mean NLL over tokens[1..] (truncated-window scoring:
// each window is scored with positions starting at zero).
double nll(const std::vector<std::int64_t>& tokens);
// Shared paged-KV pool observability (logical used, free, total) plus
// physically occupied blocks. Idle-safe test/ops hook: paged_pool_reset
// clears all table entries (live sessions transparently recompute from
// history on next use); max_blocks==0 keeps the current cap.
std::tuple<std::size_t, std::size_t, std::size_t> paged_pool_usage() const;
std::size_t paged_pool_phys_used() const;
std::size_t paged_pool_max_blocks() const;
void paged_pool_reset(std::size_t max_blocks = 0) const;
std::size_t thread_count() const { return pool_.thread_count(); }
// Weight memory control. x86 cannot pin CPU cache lines; lock_pages pins
// DRAM pages against OS paging, and touch() refreshes cache residency.
std::size_t lock_pages();
std::size_t unlock_pages();
std::size_t touch();
// Fast non-volatile full-weight scan for bandwidth probes; touch() is a
// deliberately serialized residency refresher, not a bandwidth benchmark.
std::size_t scan();
bool pages_locked() const { return pages_locked_.load(std::memory_order_relaxed); }
const Config& config() const { return config_; }
const std::string& precision() const { return precision_; }
const std::string& compile_key() const { return compile_key_; }
const std::string& compile_canonical() const { return compile_canonical_; }
const char* act_precision() const { return act_q8_ ? "int8" : "fp32"; }
std::size_t weight_bytes() const { return weight_bytes_; }
private:
friend class Session;
friend class PagedSession;
friend class BatchSession;
Config config_;
std::string precision_;
std::string compile_key_;
std::string compile_canonical_;
std::string act_precision_ = "fp32";
bool act_q8_ = false;
Matrix embeddings_, head_;
bool tied_head_;
std::vector<Layer> layers_;
std::vector<float> norm_, inv_freq_;
// Rotary sin/cos tables [context][head_dim/2], built once at load
// (read-only afterwards; sessions share them freely).
std::vector<float> rope_cos_, rope_sin_;
WorkerPool pool_;
std::size_t weight_bytes_ = 0;
std::atomic<bool> pages_locked_{false};
mutable std::uint64_t warm_sink_ = 0;
mutable std::uint64_t scan_sink_ = 0;
// S2 shared paged-KV store (lazy init on first paged session).
mutable std::shared_ptr<SharedPagedState> paged_state_;
mutable std::mutex paged_init_mu_;
std::shared_ptr<SharedPagedState> paged_state() const;
void collect_regions(Matrix::Regions& regions) const;
void refresh_compile_key();
};
class Session {
public:
Session(std::shared_ptr<const Model> model, const std::vector<std::int64_t>& prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, const std::vector<std::int64_t>& eos);
std::vector<std::int64_t> next_tokens(std::int64_t count, std::int64_t spec_k = 0);
// Greedy speculative verification of explicit candidates: accepts the
// argmax-matching prefix, appends the correction/bonus token, forwards it.
// Exact for temperature=0 (enforced by the SDK layer).
std::vector<std::int64_t> verify(const std::vector<std::int64_t>& candidates);
void cancel() noexcept { cancelled_.store(true, std::memory_order_relaxed); }
std::string finish_reason();
std::size_t generated_tokens();
// Lifetime speculative-decoding tally: drafted candidates proposed by
// draft() versus accepted by verify(). Zero when spec is off.
std::pair<std::size_t, std::size_t> spec_stats() const;
// Prompt-lookup drafter over the full token history (for tests/debugging).
std::vector<std::int64_t> draft(std::size_t k) const;
private:
friend class Model;
bool prefill();
bool forward(std::int64_t token);
bool forward_tokens(const std::vector<std::int64_t>& tokens);
std::int64_t sample();
std::shared_ptr<const Model> model_;
std::atomic<bool> cancelled_{false};
mutable std::mutex mutex_;
std::string finish_;
std::size_t capacity_, kv_elements_, position_ = 0, generated_ = 0, max_new_;
double temperature_, top_p_;
std::size_t top_k_;
std::mt19937_64 rng_;
std::vector<std::int64_t> eos_, prompt_, history_;
std::vector<float> keys_, values_, logits_, block_logits_;
// One caller at a time; all scratch and KV storage belongs to the session.
std::vector<float> x_, normalized_, q_, k_, v_, attention_, projected_, gate_, up_, scores_;
// Disjoint per-(token, head) score slices for pooled attention (one row
// per (t,h); see Session::forward_tokens). Persistent to avoid allocation.
std::vector<float> attn_scores_;
// Session-owned persistent scratch for int4/fp4 permuted activations (one
// session = one generation at a time under the Session mutex, so reuse is
// race-free; pool workers only read it after caller-thread sizing).
std::vector<float> perm_scratch_;
// Session-owned persistent staging for the embedding gather (sequential per
// token under the Session mutex, so reuse is race-free).
std::vector<float> row_buf_;
// Guarded by mutex_ like all other session state.
std::size_t spec_proposed_ = 0, spec_accepted_ = 0;
};
// ---- Paged decode (blocked SDPA gather over the shared BlockPool) ----
// Same arithmetic as Session::forward_tokens in the same kernel order, but KV
// lives in the model's shared BlockPool (block_size=16, S2) instead of
// contiguous [layers][capacity][kv_width] buffers. Bitwise-identical logits
// to the dense path for fp32. The dense Session API is unchanged; this is an
// additive path.
//
// Sharing model: every session holds one req_id in the shared pool. The pool
// lock is held exclusively for the whole forward (sessions already serialize
// on the engine generation lock and the WorkerPool barrier), so page
// pointers can never dangle across a concurrent grow. Evicted sessions
// (or a pool reset) drop their table entry; the next forward transparently
// recomputes from history_ (recompute-not-swap: no KV is preserved, nothing
// is swapped to host).
class PagedSession : public std::enable_shared_from_this<PagedSession> {
public:
PagedSession(std::shared_ptr<const Model> model, const std::vector<std::int64_t>& prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, const std::vector<std::int64_t>& eos);
~PagedSession();
std::vector<std::int64_t> next_tokens(std::int64_t count);
void cancel() noexcept { cancelled_.store(true, std::memory_order_relaxed); }
std::string finish_reason();
std::size_t generated_tokens();
std::size_t position() const;
std::size_t capacity() const;
const std::vector<float>& last_logits() const;
// Eviction priority (default 0; higher survives). Used only by
// BlockPool::evict_one under pool pressure.
void set_priority(int priority);
int priority() const;
// Lifetime recompute count (evictions survived via history recompute).
std::size_t recomputes() const;
// Run the prompt prefill without generating (pre-warms history_/logits_,
// e.g. to prepare a fork source for an empty-suffix child). Safe to call
// before next_tokens; a second call is a no-op.
bool prefill_fork_source();
private:
friend class Model;
// Fork constructor: prompt_suffix enumerates the non-shared tail,
// prefix_history the already-materialized shared prefix (position is set
// to prefix_history.size(); prefill forwards only the suffix). An empty
// suffix skips prefill entirely, so seed_logits must carry the source's
// last forward output (the distribution for the next token); it is
// assigned to logits_ so the first sample is exact, never garbage.
PagedSession(std::shared_ptr<const Model> model, std::vector<std::int64_t> prompt_suffix,
std::vector<std::int64_t> prefix_history,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, const std::vector<std::int64_t>& eos,
std::shared_ptr<PagedSession> src, std::size_t prefix_len, int priority,
std::vector<float> seed_logits);
void init_common();
bool prefill();
bool forward(std::int64_t token);
bool forward_tokens(const std::vector<std::int64_t>& tokens);
std::int64_t sample();
std::shared_ptr<const Model> model_;
std::atomic<bool> cancelled_{false};
mutable std::mutex mutex_;
std::string finish_;
std::size_t capacity_, position_ = 0, generated_ = 0, max_new_;
double temperature_, top_p_;
std::size_t top_k_;
std::mt19937_64 rng_;
std::vector<std::int64_t> eos_, prompt_, history_;
std::vector<float> logits_, block_logits_;
// Paged KV: borrowed shared pool (cross-request reuse) + unique req_id.
std::shared_ptr<SharedPagedState> state_;
std::int64_t req_id_ = -1;
int priority_ = 0;
std::size_t recomputes_ = 0;
// One caller at a time; all scratch belongs to the session.
std::vector<float> x_, normalized_, q_, k_, v_, attention_, projected_, gate_, up_, scores_;
// Disjoint per-(token, head) score slices for pooled attention (one row
// per (t,h), mirroring Session::forward_tokens). Persistent to avoid
// allocation; pool pages are pinned by the pool lock during fan-out.
std::vector<float> attn_scores_;
std::vector<float> perm_scratch_;
std::vector<float> row_buf_;
};
// ---- Batch section (dense Llama/Qwen3 only; Surjo later) ----
// True batched decode: one weight stream, B sequences. Per-seq prompt /
// position / generated / rng / eos / finish; concatenated X:[sumT,hidden]
// per-layer projections via a single gemm; per-seq attention/sampling loops.
// For sumT==1 the kernels run in the same order as Session::forward_tokens,
// giving bitwise-identical results to the single-sequence path.
class BatchSession {
public:
BatchSession(std::shared_ptr<const Model> model,
std::vector<std::vector<std::int64_t>> prompts,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, const std::vector<std::int64_t>& eos,
const std::vector<std::uint64_t>& seq_seeds = {});
// Generate up to `count` tokens per sequence. Returns B vectors; entry i
// holds the tokens emitted for sequence i (possibly shorter on stop).
std::vector<std::vector<std::int64_t>> next_tokens(std::int64_t count);
void cancel() noexcept { cancelled_.store(true, std::memory_order_relaxed); }
// Mark one sequence finished (takes effect on the next step boundary).
void cancel_seq(std::size_t index);
std::vector<std::string> finish_reasons();
std::vector<std::size_t> generated_tokens_list();
std::size_t batch_size() const;
private:
friend class Model;
struct SeqState {
std::vector<std::int64_t> prompt_;
std::vector<std::int64_t> history_;
std::vector<float> keys_, values_, logits_;
std::size_t capacity_ = 0, kv_elements_ = 0, position_ = 0, generated_ = 0, max_new_ = 0;
std::mt19937_64 rng_;
std::vector<std::int64_t> eos_;
std::string finish_;
};
bool prefill();
bool forward_batch(const std::vector<std::vector<std::int64_t>>& batch_tokens);
std::int64_t sample_seq(SeqState& seq);
std::shared_ptr<const Model> model_;
std::atomic<bool> cancelled_{false};
mutable std::mutex mutex_;
std::vector<SeqState> seqs_;
double temperature_ = 0, top_p_ = 1;
std::size_t top_k_ = 0;
bool prefilled_ = false;
// Shared concatenated scratch: token-major [sumT, ...].
std::vector<float> x_, normalized_, q_, k_, v_, attention_, projected_, gate_, up_, scores_;
std::vector<float> block_logits_;
std::vector<float> perm_scratch_, row_buf_;
};
struct SurjoXsaWeights {
Matrix q, k, v, o;
std::vector<float> q_norm, k_norm;
};
struct SurjoGdnWeights {
Matrix q, k, v, b, w, f0, f1, g0, g1, o_proj;
std::vector<float> q_conv, k_conv, v_conv; // flat (dim*kernel) FP32
std::vector<float> a_log, dt_bias, g_bias, o_norm;
std::size_t conv_kernel = 4;
};
struct SurjoLayer {
bool is_xsa = true;
std::vector<float> input_norm, post_norm;
SurjoXsaWeights xsa;
SurjoGdnWeights gdn;
Matrix gate, up, down;
};
class SurjoSession;
class SurjoModel : public std::enable_shared_from_this<SurjoModel> {
public:
SurjoModel(SurjoConfig config, WeightMap weights, std::string precision,
std::size_t threads = 1, const std::string& act_precision = "fp32");
void set_act_precision(const std::string& act_precision);
std::shared_ptr<SurjoSession> create_session(std::vector<std::int64_t> prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, std::vector<std::int64_t> eos);
std::vector<float> logits(const std::vector<std::int64_t>& prompt);
double nll(const std::vector<std::int64_t>& tokens);
std::size_t thread_count() const { return pool_.thread_count(); }
std::size_t lock_pages();
std::size_t unlock_pages();
std::size_t touch();
std::size_t scan();
bool pages_locked() const { return pages_locked_.load(std::memory_order_relaxed); }
const SurjoConfig& config() const { return config_; }
const std::string& precision() const { return precision_; }
std::size_t weight_bytes() const { return weight_bytes_; }
private:
friend class SurjoSession;
SurjoConfig config_;
std::string precision_;
std::string act_precision_ = "fp32";
bool act_q8_ = false;
Matrix embeddings_, head_;
bool tied_head_ = false;
std::vector<SurjoLayer> layers_;
std::vector<float> norm_, inv_freq_;
// (layer_idx, pass, slot): slot>=0 XSA KV slot, -1 GDN recurrent.
struct PlanStep { std::size_t layer, pass; int slot; };
std::vector<PlanStep> plan_;
std::size_t xsa_slots_ = 0, gdn_states_ = 0;
WorkerPool pool_;
std::size_t weight_bytes_ = 0;
std::atomic<bool> pages_locked_{false};
mutable std::uint64_t warm_sink_ = 0, scan_sink_ = 0;
void collect_regions(Matrix::Regions& regions) const;
void build_plan();
};
class SurjoSession {
public:
SurjoSession(std::shared_ptr<const SurjoModel> model, const std::vector<std::int64_t>& prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, const std::vector<std::int64_t>& eos);
std::vector<std::int64_t> next_tokens(std::int64_t count, std::int64_t spec_k = 0);
// Greedy speculative verification of explicit candidates: accepts the
// argmax-matching prefix, appends the correction/bonus token, forwards it.
// Exact for temperature=0 (enforced by the SDK layer). GDN recurrence is
// destructive, so the pre-block state (GDN S + conv FIFOs + XSA tail +
// position) is checkpointed before the block forward and restored (memcpy)
// with accepted-prefix replay on partial reject.
std::vector<std::int64_t> verify(const std::vector<std::int64_t>& candidates);
void cancel() noexcept { cancelled_.store(true, std::memory_order_relaxed); }
std::string finish_reason();
std::size_t generated_tokens();
// Lifetime speculative-decoding tally: drafted candidates proposed by
// draft() versus accepted by verify(). Zero when spec is off.
std::pair<std::size_t, std::size_t> spec_stats() const;
// Prompt-lookup drafter over the full token history (for tests/debugging).
std::vector<std::int64_t> draft(std::size_t k) const;
std::vector<std::int64_t> neural_draft(std::size_t k);
void set_draft_steps(const std::vector<std::size_t>& skipped);
private:
friend class SurjoModel;
std::vector<bool> draft_skipped_;
bool drafting_ = false;
bool prefill();
bool forward(std::int64_t token);
bool forward_tokens(const std::vector<std::int64_t>& tokens);
// Block-batched forward for T>1 (speculative verify / NLL scoring).
// Per-layer projections stream weights once for all T tokens via a single
// gemm over concatenated X:[T,hidden]; XSA attention and GDN recurrence /
// FIFO stay sequential per token in token order, so results are
// bitwise-identical to T sequential forward() calls.
bool forward_block(const std::vector<std::int64_t>& tokens);
std::int64_t sample();
std::shared_ptr<const SurjoModel> model_;
std::atomic<bool> cancelled_{false};
mutable std::mutex mutex_;
std::string finish_;
std::size_t capacity_, position_ = 0, generated_ = 0, max_new_;
double temperature_, top_p_;
std::size_t top_k_;
std::mt19937_64 rng_;
std::vector<std::int64_t> eos_, prompt_, history_;
std::size_t xsa_slots_total_ = 0, gdn_total_ = 0, gdn_v_total_ = 0;
// XSA KV: [slots, capacity, kv_width]; GDN S: [gdn_states, H*K*V]; conv FIFO.
std::vector<float> xsa_keys_, xsa_values_, gdn_state_;
std::vector<float> gdn_q_fifo_, gdn_k_fifo_, gdn_v_fifo_;
std::vector<float> logits_, block_logits_;
std::vector<float> x_, normalized_, q_, k_, v_, attention_, projected_, gate_, up_, scores_;
std::vector<float> g_, b_, w_, gv_, o_normed_;
std::vector<float> perm_scratch_, row_buf_;
// Guarded by mutex_ like all other session state.
std::size_t spec_proposed_ = 0, spec_accepted_ = 0;
};
struct FwkvConfig {
std::string model_type = "fwkv";
std::size_t d_model = 0, d_emb = 0, layers = 0, ffn_mult = 4, vocab = 0, context = 1024;
float wkv_floor = 0.1f;
bool tied = true;
void validate() const;
};
ShapeMap fwkv_weight_shapes(const FwkvConfig& config);
struct FwkvLayer {
Matrix proj_k, proj_v, proj_r, proj_out, ffn0, ffn2;
std::vector<float> w, norm_wkv_w, norm_wkv_b, norm_ffn_w, norm_ffn_b;
};
class FwkvSession;
class FwkvModel : public std::enable_shared_from_this<FwkvModel> {
public:
FwkvModel(FwkvConfig config, WeightMap weights, std::string precision,
std::size_t threads = 1, const std::string& act_precision = "fp32");
void set_act_precision(const std::string& act_precision);
std::shared_ptr<FwkvSession> create_session(std::vector<std::int64_t> prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, std::vector<std::int64_t> eos);
std::vector<float> logits(const std::vector<std::int64_t>& prompt);
double nll(const std::vector<std::int64_t>& tokens);
std::size_t thread_count() const { return pool_.thread_count(); }
std::size_t lock_pages();
std::size_t unlock_pages();
std::size_t touch();
std::size_t scan();
bool pages_locked() const { return pages_locked_.load(std::memory_order_relaxed); }
const FwkvConfig& config() const { return config_; }
const std::string& precision() const { return precision_; }
std::size_t weight_bytes() const { return weight_bytes_; }
private:
friend class FwkvSession;
FwkvConfig config_;
std::string precision_;
std::string act_precision_ = "fp32";
bool act_q8_ = false;
// Factorized tied head: shared.weight [vocab, d_emb] as a quantized
// Matrix (embed gather via row(), logits via multiply), shared.proj
// [d_model, d_emb] kept FP32 (147K mults per token either direction).
Matrix shared_emb_, shared_head_;
std::vector<float> shared_proj_;
std::vector<FwkvLayer> layers_;
std::vector<float> norm_w_, norm_b_;
WorkerPool pool_;
std::size_t weight_bytes_ = 0;
std::atomic<bool> pages_locked_{false};
mutable std::uint64_t warm_sink_ = 0, scan_sink_ = 0;
void collect_regions(Matrix::Regions& regions) const;
};
class FwkvSession {
public:
FwkvSession(std::shared_ptr<const FwkvModel> model, const std::vector<std::int64_t>& prompt,
std::int64_t max_new_tokens, double temperature, double top_p,
std::int64_t top_k, std::uint64_t seed, const std::vector<std::int64_t>& eos);
std::vector<std::int64_t> next_tokens(std::int64_t count, std::int64_t spec_k = 0);
// Greedy speculative verification of explicit candidates: accepts the
// argmax-matching prefix, appends the correction/bonus token, forwards it.
// Exact for temperature=0 (enforced by the SDK layer). The WKV recurrent
// state is destructive, so it is checkpointed (layers*d_model floats +
// position, ~40KB for Myosotis-1-base) before the block forward and
// restored (memcpy) with accepted-prefix replay on partial reject.
std::vector<std::int64_t> verify(const std::vector<std::int64_t>& candidates);
void cancel() noexcept { cancelled_.store(true, std::memory_order_relaxed); }
std::string finish_reason();
std::size_t generated_tokens();
// Lifetime speculative-decoding tally: drafted candidates proposed by
// draft() versus accepted by verify(). Zero when spec is off.
std::pair<std::size_t, std::size_t> spec_stats() const;
// Prompt-lookup drafter over the full token history (for tests/debugging).
std::vector<std::int64_t> draft(std::size_t k) const;
private:
friend class FwkvModel;
bool prefill();
bool forward(std::int64_t token);
bool forward_tokens(const std::vector<std::int64_t>& tokens);
// Block-batched forward for T>1 (speculative verify / NLL scoring).
// Per-layer projections stream weights once for all T tokens via a single
// gemm over concatenated X:[T,d_model]; WKV recurrence, LayerNorms and
// the factorized head stay sequential per token in token order, so results
// are bitwise-identical to T sequential forward() calls.
bool forward_block(const std::vector<std::int64_t>& tokens);
std::int64_t sample();
std::shared_ptr<const FwkvModel> model_;
std::atomic<bool> cancelled_{false};
mutable std::mutex mutex_;
std::string finish_;
std::size_t capacity_, position_ = 0, generated_ = 0, max_new_;
double temperature_, top_p_;
std::size_t top_k_;
std::mt19937_64 rng_;
std::vector<std::int64_t> eos_, prompt_, history_;
// WKV recurrent state: [layers, d_model], zeros at start.
std::vector<float> state_;
std::vector<float> logits_, block_logits_;
std::vector<float> x_, h_, k_, v_, r_, g_, o_, ffn_h_, emb_, x_emb_;
std::vector<float> perm_scratch_, row_buf_;
// Guarded by mutex_ like all other session state.
std::size_t spec_proposed_ = 0, spec_accepted_ = 0;
};
} // namespace cism
|