From 0547505babbf719c9f232624175182a6942eea98 Mon Sep 17 00:00:00 2001 From: Zero Date: Tue, 20 Oct 2026 01:29:27 -0500 Subject: [PATCH] Initial public release --- .cla/signatures.md | 47 + .github/PULL_REQUEST_TEMPLATE.md | 23 + .github/workflows/contributions.yml | 43 + .gitignore | 165 + .gitmodules | 3 + CLA-CORPORATE.md | 150 + CLA.md | 155 + CONTRIBUTING.md | 177 + LICENSE | 661 + LICENSES/Apache-2.0.txt | 202 + Makefile | 1286 ++ NOTICE | 54 + README.md | 344 + SECURITY.md | 56 + cli/convert.c | 141 + cli/main.c | 289 + docs/README.md | 117 + docs/architecture/accel-foundry.md | 371 + docs/architecture/core-engine.md | 180 + docs/architecture/engine.md | 229 + docs/architecture/gguf.md | 151 + docs/architecture/llama-forward.md | 264 + docs/architecture/memory-aware.md | 264 + docs/architecture/models-qwen35moe.md | 281 + docs/architecture/qwen35moe.md | 193 + docs/architecture/rwkvx.md | 141 + docs/architecture/serve.md | 553 + docs/coding-best-practices.md | 204 + docs/guides/chat.md | 162 + docs/guides/install.md | 128 + docs/guides/pth-convert.md | 133 + docs/guides/quickstart.md | 160 + docs/reference/cli.md | 193 + docs/reference/env-flags.md | 158 + docs/reference/http-api.md | 821 ++ docs/reference/supported-models.md | 370 + evals/corpus/held_out.txt | 19 + evals/eval_quality.sh | 241 + evals/flash_spec_gate.sh | 81 + evals/prompts/coding_repetitive.txt | 52 + evals/prompts/graph_subset.txt | 43 + evals/prompts/quality_suite.txt | 133 + examples/deploy.sh | 212 + examples/models.conf | 35 + foundry | 1 + include/pantheon.h | 580 + install.sh | 108 + k80_nexn2.sh | 38 + k80_qwen36.sh | 38 + run_ornith.sh | 70 + scripts/add-file-headers.sh | 115 + scripts/assert_backend.sh | 67 + scripts/check-cla.sh | 68 + scripts/check-dco.sh | 66 + scripts/check-headers.sh | 98 + scripts/k80_serve.sh | 43 + scripts/model_fetch.sh | 52 + src/accel/backend_identity.c | 95 + src/accel/backend_identity.h | 102 + src/accel/die_layer_split.c | 69 + src/accel/die_layer_split.h | 100 + src/accel/die_manager.c | 441 + src/accel/die_manager.h | 173 + src/accel/simd.c | 259 + src/accel/simd.h | 151 + src/accel/tt/pn_tt_attention.c | 1659 +++ src/accel/tt/pn_tt_column_runs.c | 103 + src/accel/tt/pn_tt_column_runs.h | 124 + src/accel/tt/pn_tt_core_boot_guard.c | 209 + src/accel/tt/pn_tt_core_boot_guard.h | 109 + src/accel/tt/pn_tt_descriptor_census.c | 319 + src/accel/tt/pn_tt_descriptor_census.h | 234 + src/accel/tt/pn_tt_device.c | 2112 +++ src/accel/tt/pn_tt_device.h | 1340 ++ src/accel/tt/pn_tt_device_internal.h | 948 ++ src/accel/tt/pn_tt_dispatch.c | 2558 ++++ src/accel/tt/pn_tt_dispatch.h | 75 + src/accel/tt/pn_tt_eltwise_group.c | 685 + src/accel/tt/pn_tt_eltwise_group.h | 243 + src/accel/tt/pn_tt_eltwise_split.c | 81 + src/accel/tt/pn_tt_eltwise_split.h | 129 + src/accel/tt/pn_tt_fp32_strip.c | 132 + src/accel/tt/pn_tt_fp32_strip.h | 116 + src/accel/tt/pn_tt_gate.c | 271 + src/accel/tt/pn_tt_gate.h | 118 + src/accel/tt/pn_tt_gdn.c | 2703 ++++ src/accel/tt/pn_tt_gdn.h | 319 + src/accel/tt/pn_tt_gdn_checkpoint.c | 177 + src/accel/tt/pn_tt_gdn_checkpoint.h | 64 + src/accel/tt/pn_tt_gdn_core_scaling.c | 218 + src/accel/tt/pn_tt_gdn_core_scaling.h | 71 + src/accel/tt/pn_tt_gdn_layout.c | 60 + src/accel/tt/pn_tt_gdn_layout.h | 130 + src/accel/tt/pn_tt_gdnc.c | 2119 +++ src/accel/tt/pn_tt_gdnc.h | 208 + src/accel/tt/pn_tt_gdngate.c | 526 + src/accel/tt/pn_tt_gdngate.h | 93 + src/accel/tt/pn_tt_head_norm.c | 531 + src/accel/tt/pn_tt_head_norm.h | 288 + src/accel/tt/pn_tt_issue.c | 114 + src/accel/tt/pn_tt_issue.h | 179 + src/accel/tt/pn_tt_matmul_group.c | 4245 ++++++ src/accel/tt/pn_tt_matmul_group.h | 722 + src/accel/tt/pn_tt_name_list.c | 53 + src/accel/tt/pn_tt_name_list.h | 82 + src/accel/tt/pn_tt_oncard.c | 2170 +++ src/accel/tt/pn_tt_reader.c | 362 + src/accel/tt/pn_tt_reader.h | 86 + src/accel/tt/pn_tt_render.c | 738 ++ src/accel/tt/pn_tt_render.h | 206 + src/accel/tt/pn_tt_replica_map.c | 350 + src/accel/tt/pn_tt_replica_map.h | 202 + src/accel/tt/pn_tt_residency.c | 1234 ++ src/accel/tt/pn_tt_residency.h | 450 + src/accel/tt/pn_tt_stage_pipeline.c | 112 + src/accel/tt/pn_tt_stage_pipeline.h | 53 + src/core/bench.c | 61 + src/core/bench.h | 31 + src/core/bpe_pretokenize.c | 374 + src/core/bpe_pretokenize.h | 58 + src/core/chat.c | 396 + src/core/chat.h | 49 + src/core/engine.c | 4366 ++++++ src/core/engine.h | 408 + src/core/grammar_mask_shortlist.c | 137 + src/core/grammar_mask_shortlist.h | 83 + src/core/mem/pn_admission.c | 364 + src/core/mem/pn_admission.h | 311 + src/core/mem/pn_host_signals.c | 225 + src/core/mem/pn_host_signals.h | 70 + src/core/mem/pn_kv_geometry.c | 51 + src/core/mem/pn_kv_geometry.h | 56 + src/core/mem/pn_ladder.c | 114 + src/core/mem/pn_ladder.h | 103 + src/core/mem/pn_lazy_zero.c | 52 + src/core/mem/pn_lazy_zero.h | 51 + src/core/mem/pn_mem_state.h | 77 + src/core/mem/pn_memwatch.c | 462 + src/core/mem/pn_memwatch.h | 191 + src/core/ngram_draft.c | 75 + src/core/ngram_draft.h | 80 + src/core/prefix_cache.c | 899 ++ src/core/prefix_cache.h | 390 + src/core/qweight.c | 887 ++ src/core/qweight.h | 241 + src/core/sample_profiler.c | 211 + src/core/sample_profiler.h | 28 + src/core/sampling_modifiers.c | 114 + src/core/sampling_modifiers.h | 123 + src/core/session_snapshot.c | 138 + src/core/session_snapshot.h | 106 + src/formats/gguf.c | 471 + src/formats/gguf.h | 157 + src/formats/gguf_dequant.c | 443 + src/formats/gguf_dequant.h | 150 + src/formats/gguf_layer_bytes.c | 142 + src/formats/gguf_layer_bytes.h | 40 + src/memory/arena.c | 53 + src/memory/arena.h | 37 + src/models/bonsai.h | 81 + src/models/bonsai_forward.c | 104 + src/models/gpu_attn_path_tracker.c | 210 + src/models/gpu_attn_path_tracker.h | 200 + src/models/gpu_graph_capture_tracker.c | 198 + src/models/gpu_graph_capture_tracker.h | 197 + src/models/llama.c | 3066 +++++ src/models/llama.h | 465 + src/models/llama_attention_trace.c | 152 + src/models/llama_attention_trace.h | 131 + src/models/llama_forward_cuda.c | 590 + src/models/llama_forward_tt.c | 2611 ++++ src/models/llama_residual_trace.c | 76 + src/models/llama_residual_trace.h | 73 + src/models/llama_state_snapshot.c | 462 + src/models/phi3.c | 486 + src/models/phi3.h | 65 + src/models/pixtral_vision.c | 674 + src/models/pixtral_vision.h | 126 + src/models/qwen35moe.c | 1014 ++ src/models/qwen35moe.h | 824 ++ src/models/qwen35moe_attn.c | 240 + src/models/qwen35moe_attn_tt.c | 490 + src/models/qwen35moe_checkpoint_band.c | 25 + src/models/qwen35moe_checkpoint_band.h | 44 + src/models/qwen35moe_forward.c | 548 + src/models/qwen35moe_forward_cuda.c | 10933 ++++++++++++++++ src/models/qwen35moe_forward_metal.c | 394 + src/models/qwen35moe_forward_tt.c | 1626 +++ src/models/qwen35moe_gdn_normcheck_tt.c | 477 + src/models/qwen35moe_gdn_parity_tt.c | 382 + src/models/qwen35moe_gdn_strip_tt.c | 1398 ++ src/models/qwen35moe_gdn_tt.c | 279 + src/models/qwen35moe_internal.h | 809 ++ src/models/qwen35moe_kv_q4.h | 165 + src/models/qwen35moe_metal_coverage.c | 486 + src/models/qwen35moe_metal_coverage.h | 148 + src/models/qwen35moe_metal_kernel_registry.c | 98 + src/models/qwen35moe_metal_kernel_registry.h | 47 + src/models/qwen35moe_moe.c | 249 + src/models/qwen35moe_prefill_checkpoint_tt.c | 287 + src/models/qwen35moe_prefill_overlap_tt.c | 339 + src/models/qwen35moe_prefill_prefetch_tt.c | 393 + src/models/qwen35moe_prefill_tt.c | 2200 ++++ src/models/qwen35moe_prefill_tt_wall.c | 89 + src/models/qwen35moe_prefill_tt_wall.h | 53 + src/models/qwen35moe_resident_tt.c | 1274 ++ src/models/qwen35moe_ssm.c | 294 + src/models/qwen35moe_state_snapshot.c | 587 + src/models/qwen35moe_tt_internal.h | 934 ++ src/models/rwkvx.c | 2200 ++++ src/models/rwkvx.h | 43 + src/serve/attn_residency_json.c | 69 + src/serve/attn_residency_json.h | 63 + src/serve/base64.c | 110 + src/serve/base64.h | 42 + src/serve/batch_sched.c | 3138 +++++ src/serve/batch_sched.h | 1220 ++ src/serve/grammar_spec_chooser.c | 101 + src/serve/grammar_spec_chooser.h | 106 + src/serve/graph_residency_json.c | 62 + src/serve/graph_residency_json.h | 59 + src/serve/http_client.c | 273 + src/serve/http_client.h | 74 + src/serve/http_server.c | 336 + src/serve/http_server.h | 98 + src/serve/json_canonical_stream.c | 375 + src/serve/json_canonical_stream.h | 108 + src/serve/json_reader.c | 418 + src/serve/json_reader.h | 94 + src/serve/json_value_grammar.c | 312 + src/serve/json_value_grammar.h | 153 + src/serve/json_writer.c | 114 + src/serve/json_writer.h | 60 + src/serve/model_admin.c | 419 + src/serve/model_admin.h | 77 + src/serve/model_registry.c | 871 ++ src/serve/model_registry.h | 289 + src/serve/model_session.c | 187 + src/serve/model_session.h | 138 + src/serve/openai_chat.c | 2404 ++++ src/serve/openai_chat.h | 81 + src/serve/registry.c | 461 + src/serve/registry.h | 185 + src/serve/registry_client.c | 101 + src/serve/registry_client.h | 79 + src/serve/registry_wire.h | 67 + src/serve/repetition_loop_detector.c | 262 + src/serve/repetition_loop_detector.h | 190 + src/serve/request_profile.c | 160 + src/serve/request_profile.h | 99 + src/serve/response_format_grammar.c | 432 + src/serve/response_format_grammar.h | 154 + src/serve/router.c | 264 + src/serve/router.h | 53 + src/serve/router_command.c | 288 + src/serve/router_command.h | 26 + src/serve/serve_backend_guard.c | 125 + src/serve/serve_backend_guard.h | 129 + src/serve/serve_command.c | 951 ++ src/serve/serve_command.h | 28 + src/serve/serve_emitter.c | 454 + src/serve/serve_emitter.h | 124 + src/serve/serve_generation.c | 3598 +++++ src/serve/serve_generation.h | 483 + src/serve/serve_prompt_tokens.c | 425 + src/serve/serve_prompt_tokens.h | 212 + src/serve/serve_registry.c | 248 + src/serve/serve_registry.h | 91 + src/serve/serve_shutdown.c | 188 + src/serve/serve_shutdown.h | 195 + src/serve/serve_spec_lookup.c | 112 + src/serve/serve_spec_lookup.h | 74 + src/serve/session_affinity.c | 115 + src/serve/session_affinity.h | 101 + src/serve/session_key.h | 45 + src/serve/session_watch.c | 610 + src/serve/session_watch.h | 135 + src/serve/signal_handlers.c | 131 + src/serve/signal_handlers.h | 46 + src/serve/tool_call_stream.c | 292 + src/serve/tool_call_stream.h | 140 + src/serve/tool_calling.c | 1698 +++ src/serve/tool_calling.h | 401 + src/serve/tool_grammar.c | 1365 ++ src/serve/tool_grammar.h | 347 + src/serve/tool_grammar_json.c | 1047 ++ src/serve/tool_grammar_json.h | 225 + src/serve/tool_param_empty_value_rule.c | 44 + src/serve/tool_param_empty_value_rule.h | 60 + tests/ab_build_arm.sh | 108 + tests/abandoned_call_fixture.h | 33 + tests/bench_batch_decode.c | 103 + tests/bench_depth_qwen35.c | 190 + tests/bench_ngram_accept.c | 850 ++ tests/bench_sampler_temp_delta.c | 161 + tests/bench_serve_prefix_cache.c | 226 + tests/bench_spec_accept.c | 386 + tests/bench_spec_verify_gate.c | 319 + tests/bench_tt_prefill_quantum.c | 435 + tests/bench_verify_rows.c | 92 + tests/bench_vision.c | 65 + .../cuda_host_attention_reads_future.patch | 22 + tests/causality_red/run_red.sh | 107 + .../tt_chunk_attention_causal_off.patch | 13 + tests/chat_corpus.c | 190 + tests/chat_corpus.h | 91 + tests/data/chat_parity_corpus.txt | 31 + tests/data/chat_parity_corpus_crossed.txt | 34 + tests/data/chat_parity_corpus_sysonly.txt | 17 + tests/data/code_edit_prompts.txt | 592 + tests/data/ngram_chat_prompts.txt | 14 + tests/data/ngram_code_edit_prompts.txt | 595 + tests/data/ngram_code_long_prompt.txt | 434 + tests/data/ngram_prose_prompts.txt | 14 + tests/dispatch/e2e.sh | 216 + tests/dispatch/stub_upstream.py | 128 + tests/eval_ppl.c | 648 + tests/frame_capture_fixtures.h | 165 + tests/gen_golden_qwen35.c | 873 ++ tests/golden/README.md | 242 + tests/golden/moe_router_logits.bin | Bin 0 -> 1076 bytes tests/golden/moe_shared_gate_input.bin | Bin 0 -> 8249 bytes tests/golden/moe_shared_gate_scalar.bin | 2 + tests/golden/moe_shared_gate_weight.bin | Bin 0 -> 8250 bytes tests/golden/moe_topk_indices.bin | Bin 0 -> 81 bytes tests/golden/moe_topk_weights.bin | 3 + tests/golden/q3k_dequant_block0.bin | Bin 0 -> 1077 bytes tests/golden/q3k_matvec_input.bin | Bin 0 -> 4148 bytes tests/golden/q3k_matvec_output.bin | Bin 0 -> 147 bytes tests/golden/q3k_matvec_weight.bin | Bin 0 -> 10613 bytes tests/golden/qknorm_input.bin | Bin 0 -> 16432 bytes tests/golden/qknorm_output.bin | Bin 0 -> 16433 bytes tests/golden/qknorm_weight.bin | Bin 0 -> 1072 bytes tests/golden/rope_k_input.bin | Bin 0 -> 2095 bytes tests/golden/rope_k_pos0.bin | Bin 0 -> 2094 bytes tests/golden/rope_k_pos1.bin | Bin 0 -> 2094 bytes tests/golden/rope_k_pos100.bin | Bin 0 -> 2096 bytes tests/golden/rope_k_pos7.bin | Bin 0 -> 2094 bytes tests/golden/rope_q_input.bin | Bin 0 -> 16432 bytes tests/golden/rope_q_pos0.bin | Bin 0 -> 16431 bytes tests/golden/rope_q_pos1.bin | Bin 0 -> 16431 bytes tests/golden/rope_q_pos100.bin | Bin 0 -> 16433 bytes tests/golden/rope_q_pos7.bin | Bin 0 -> 16431 bytes tests/golden/ssm_conv_out.bin | Bin 0 -> 32816 bytes tests/golden/ssm_conv_qkv.bin | Bin 0 -> 32816 bytes tests/golden/ssm_conv_state_in.bin | Bin 0 -> 98358 bytes tests/golden/ssm_conv_state_out.bin | Bin 0 -> 98359 bytes tests/golden/ssm_conv_weight.bin | Bin 0 -> 131124 bytes tests/golden/ssm_gated_out.bin | Bin 0 -> 16433 bytes tests/golden/ssm_gated_weight.bin | Bin 0 -> 563 bytes tests/golden/ssm_gated_y.bin | Bin 0 -> 16431 bytes tests/golden/ssm_gated_z.bin | Bin 0 -> 16431 bytes tests/golden/ssm_recur_beta.bin | 3 + tests/golden/ssm_recur_g.bin | Bin 0 -> 173 bytes tests/golden/ssm_recur_k.bin | Bin 0 -> 16431 bytes tests/golden/ssm_recur_q.bin | Bin 0 -> 16431 bytes tests/golden/ssm_recur_state_in.bin | Bin 0 -> 2097208 bytes tests/golden/ssm_recur_state_out.bin | Bin 0 -> 2097209 bytes tests/golden/ssm_recur_v.bin | Bin 0 -> 16431 bytes tests/golden/ssm_recur_y.bin | Bin 0 -> 16431 bytes tests/ngram_accept_stream.c | 409 + tests/ngram_accept_stream.h | 183 + tests/parity_accel_arm.c | 126 + tests/parity_accel_arm.h | 103 + tests/parity_gate_criteria.c | 142 + tests/parity_gate_criteria.h | 72 + tests/parity_logit_dump.c | 202 + tests/parity_logit_dump.h | 166 + tests/parity_noise.c | 90 + tests/parity_noise.h | 44 + tests/parity_score.c | 156 + tests/parity_score.h | 112 + tests/run_tt_prefill_red.sh | 56 + tests/serve_bench_users.py | 287 + tests/serve_concurrency_smoke.sh | 156 + tests/serve_eos_repeat_smoke.sh | 143 + tests/serve_multimodel_smoke.sh | 160 + tests/serve_response_format_smoke.sh | 157 + tests/serve_session_export_smoke.sh | 232 + tests/serve_smoke.sh | 114 + tests/serve_tools_smoke.sh | 153 + tests/test_2die_swap.c | 318 + tests/test_admission_reserve.c | 103 + tests/test_arena.c | 105 + tests/test_attn_ring_retry.c | 326 + tests/test_backend_guard.c | 408 + tests/test_base64.c | 96 + tests/test_batch_decode_correct.c | 82 + tests/test_batch_decoder.c | 82 + tests/test_bpe_pretokenize.c | 333 + tests/test_bs_load.c | 88 + tests/test_build.c | 81 + tests/test_chat.c | 456 + tests/test_chat_parity_gate.c | 382 + tests/test_chat_request.c | 1693 +++ tests/test_dequant_props.c | 531 + tests/test_die_layer_split.c | 228 + tests/test_die_manager.c | 165 + tests/test_die_scratch_ctx.c | 227 + tests/test_embed_row.c | 331 + tests/test_empty_argument.c | 346 + tests/test_frame_capture.c | 356 + tests/test_gdn_layout.c | 653 + tests/test_gdnc_compact.c | 179 + tests/test_gen_bench.c | 73 + tests/test_gen_threads.c | 85 + tests/test_gguf.c | 276 + tests/test_gguf_layer_bytes.c | 360 + tests/test_grammar_arm_window.c | 365 + tests/test_grammar_mask_shortlist.c | 217 + tests/test_grammar_spec_chooser.c | 447 + tests/test_grammar_topn_sampler.c | 184 + tests/test_graph_capture_concurrency.c | 156 + tests/test_graph_capture_retry.c | 260 + tests/test_json_value_grammar.c | 242 + tests/test_kernel_parity.c | 658 + tests/test_llama_state_snapshot.c | 620 + tests/test_memwatch.c | 729 ++ tests/test_model_registry.c | 362 + tests/test_model_unload_reclaim.c | 323 + tests/test_prefill.c | 126 + tests/test_prefix_cache_qwen35.c | 329 + tests/test_prefix_cache_sys.c | 2126 +++ tests/test_prefix_projection.c | 833 ++ tests/test_pretok_props.c | 473 + tests/test_q6k_parity.c | 152 + tests/test_qmv.c | 276 + tests/test_qmv_threads.c | 101 + tests/test_qwen35_ckpt_band.c | 72 + tests/test_qwen35_prefill_causality.c | 465 + tests/test_qwen35moe.c | 134 + tests/test_qwen35moe_metal_coverage.c | 295 + tests/test_registry.c | 275 + tests/test_repetition_loop_detector.c | 572 + tests/test_response_format_grammar.c | 328 + tests/test_rwkvx.c | 388 + tests/test_sampler_parity.c | 458 + tests/test_sampling_modifiers.c | 280 + tests/test_serve_emitter.c | 714 + tests/test_serve_sched_prefix_cache.c | 279 + tests/test_serve_sched_qwen35.c | 315 + tests/test_serve_shutdown.c | 346 + tests/test_serve_spec_lookup.c | 77 + tests/test_session_affinity.c | 247 + tests/test_session_watch.c | 469 + tests/test_simd.c | 142 + tests/test_simd_props.c | 275 + tests/test_slot_free_bind_qwen35.c | 184 + tests/test_sparse_sampler_identity.c | 316 + tests/test_spec_batch.c | 400 + tests/test_spec_decode.c | 273 + tests/test_spec_generate.c | 140 + tests/test_spec_qwen35.c | 255 + tests/test_tool_call_defects.c | 715 + tests/test_tool_call_stream.c | 601 + tests/test_tool_call_truncation.c | 404 + tests/test_tool_calling.c | 1081 ++ tests/test_tool_grammar.c | 1916 +++ tests/test_tool_grammar_json.c | 1037 ++ tests/test_topn_row_exact.c | 126 + tests/test_tt_boot_guard_leftover.c | 245 + tests/test_tt_column_runs.c | 241 + tests/test_tt_descriptor_census.c | 591 + tests/test_tt_eltwise_split.c | 318 + tests/test_tt_name_list.c | 92 + tests/test_tt_prefill_parity.c | 412 + tests/test_tt_stage_pipeline.c | 68 + tests/test_two_pass_parity_gate.c | 772 ++ tests/test_users_qwen35.c | 337 + tests/test_verify_batch.c | 277 + tests/tt_prefix_cache_link_stubs.c | 38 + tools/build_mtp_gguf.py | 108 + tools/fetch_mtp_tensors.py | 76 + tools/gguf_retype.py | 388 + tools/gguf_tensor_md5.py | 89 + tools/gguf_token_text.py | 64 + tools/sample_profile_report.sh | 20 + 477 files changed, 169693 insertions(+) create mode 100644 .cla/signatures.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/contributions.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CLA-CORPORATE.md create mode 100644 CLA.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 Makefile create mode 100644 NOTICE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 cli/convert.c create mode 100644 cli/main.c create mode 100644 docs/README.md create mode 100644 docs/architecture/accel-foundry.md create mode 100644 docs/architecture/core-engine.md create mode 100644 docs/architecture/engine.md create mode 100644 docs/architecture/gguf.md create mode 100644 docs/architecture/llama-forward.md create mode 100644 docs/architecture/memory-aware.md create mode 100644 docs/architecture/models-qwen35moe.md create mode 100644 docs/architecture/qwen35moe.md create mode 100644 docs/architecture/rwkvx.md create mode 100644 docs/architecture/serve.md create mode 100644 docs/coding-best-practices.md create mode 100644 docs/guides/chat.md create mode 100644 docs/guides/install.md create mode 100644 docs/guides/pth-convert.md create mode 100644 docs/guides/quickstart.md create mode 100644 docs/reference/cli.md create mode 100644 docs/reference/env-flags.md create mode 100644 docs/reference/http-api.md create mode 100644 docs/reference/supported-models.md create mode 100644 evals/corpus/held_out.txt create mode 100755 evals/eval_quality.sh create mode 100755 evals/flash_spec_gate.sh create mode 100644 evals/prompts/coding_repetitive.txt create mode 100644 evals/prompts/graph_subset.txt create mode 100644 evals/prompts/quality_suite.txt create mode 100755 examples/deploy.sh create mode 100644 examples/models.conf create mode 160000 foundry create mode 100644 include/pantheon.h create mode 100755 install.sh create mode 100755 k80_nexn2.sh create mode 100755 k80_qwen36.sh create mode 100755 run_ornith.sh create mode 100755 scripts/add-file-headers.sh create mode 100755 scripts/assert_backend.sh create mode 100755 scripts/check-cla.sh create mode 100755 scripts/check-dco.sh create mode 100755 scripts/check-headers.sh create mode 100644 scripts/k80_serve.sh create mode 100644 scripts/model_fetch.sh create mode 100644 src/accel/backend_identity.c create mode 100644 src/accel/backend_identity.h create mode 100644 src/accel/die_layer_split.c create mode 100644 src/accel/die_layer_split.h create mode 100644 src/accel/die_manager.c create mode 100644 src/accel/die_manager.h create mode 100644 src/accel/simd.c create mode 100644 src/accel/simd.h create mode 100644 src/accel/tt/pn_tt_attention.c create mode 100644 src/accel/tt/pn_tt_column_runs.c create mode 100644 src/accel/tt/pn_tt_column_runs.h create mode 100644 src/accel/tt/pn_tt_core_boot_guard.c create mode 100644 src/accel/tt/pn_tt_core_boot_guard.h create mode 100644 src/accel/tt/pn_tt_descriptor_census.c create mode 100644 src/accel/tt/pn_tt_descriptor_census.h create mode 100644 src/accel/tt/pn_tt_device.c create mode 100644 src/accel/tt/pn_tt_device.h create mode 100644 src/accel/tt/pn_tt_device_internal.h create mode 100644 src/accel/tt/pn_tt_dispatch.c create mode 100644 src/accel/tt/pn_tt_dispatch.h create mode 100644 src/accel/tt/pn_tt_eltwise_group.c create mode 100644 src/accel/tt/pn_tt_eltwise_group.h create mode 100644 src/accel/tt/pn_tt_eltwise_split.c create mode 100644 src/accel/tt/pn_tt_eltwise_split.h create mode 100644 src/accel/tt/pn_tt_fp32_strip.c create mode 100644 src/accel/tt/pn_tt_fp32_strip.h create mode 100644 src/accel/tt/pn_tt_gate.c create mode 100644 src/accel/tt/pn_tt_gate.h create mode 100644 src/accel/tt/pn_tt_gdn.c create mode 100644 src/accel/tt/pn_tt_gdn.h create mode 100644 src/accel/tt/pn_tt_gdn_checkpoint.c create mode 100644 src/accel/tt/pn_tt_gdn_checkpoint.h create mode 100644 src/accel/tt/pn_tt_gdn_core_scaling.c create mode 100644 src/accel/tt/pn_tt_gdn_core_scaling.h create mode 100644 src/accel/tt/pn_tt_gdn_layout.c create mode 100644 src/accel/tt/pn_tt_gdn_layout.h create mode 100644 src/accel/tt/pn_tt_gdnc.c create mode 100644 src/accel/tt/pn_tt_gdnc.h create mode 100644 src/accel/tt/pn_tt_gdngate.c create mode 100644 src/accel/tt/pn_tt_gdngate.h create mode 100644 src/accel/tt/pn_tt_head_norm.c create mode 100644 src/accel/tt/pn_tt_head_norm.h create mode 100644 src/accel/tt/pn_tt_issue.c create mode 100644 src/accel/tt/pn_tt_issue.h create mode 100644 src/accel/tt/pn_tt_matmul_group.c create mode 100644 src/accel/tt/pn_tt_matmul_group.h create mode 100644 src/accel/tt/pn_tt_name_list.c create mode 100644 src/accel/tt/pn_tt_name_list.h create mode 100644 src/accel/tt/pn_tt_oncard.c create mode 100644 src/accel/tt/pn_tt_reader.c create mode 100644 src/accel/tt/pn_tt_reader.h create mode 100644 src/accel/tt/pn_tt_render.c create mode 100644 src/accel/tt/pn_tt_render.h create mode 100644 src/accel/tt/pn_tt_replica_map.c create mode 100644 src/accel/tt/pn_tt_replica_map.h create mode 100644 src/accel/tt/pn_tt_residency.c create mode 100644 src/accel/tt/pn_tt_residency.h create mode 100644 src/accel/tt/pn_tt_stage_pipeline.c create mode 100644 src/accel/tt/pn_tt_stage_pipeline.h create mode 100644 src/core/bench.c create mode 100644 src/core/bench.h create mode 100644 src/core/bpe_pretokenize.c create mode 100644 src/core/bpe_pretokenize.h create mode 100644 src/core/chat.c create mode 100644 src/core/chat.h create mode 100644 src/core/engine.c create mode 100644 src/core/engine.h create mode 100644 src/core/grammar_mask_shortlist.c create mode 100644 src/core/grammar_mask_shortlist.h create mode 100644 src/core/mem/pn_admission.c create mode 100644 src/core/mem/pn_admission.h create mode 100644 src/core/mem/pn_host_signals.c create mode 100644 src/core/mem/pn_host_signals.h create mode 100644 src/core/mem/pn_kv_geometry.c create mode 100644 src/core/mem/pn_kv_geometry.h create mode 100644 src/core/mem/pn_ladder.c create mode 100644 src/core/mem/pn_ladder.h create mode 100644 src/core/mem/pn_lazy_zero.c create mode 100644 src/core/mem/pn_lazy_zero.h create mode 100644 src/core/mem/pn_mem_state.h create mode 100644 src/core/mem/pn_memwatch.c create mode 100644 src/core/mem/pn_memwatch.h create mode 100644 src/core/ngram_draft.c create mode 100644 src/core/ngram_draft.h create mode 100644 src/core/prefix_cache.c create mode 100644 src/core/prefix_cache.h create mode 100644 src/core/qweight.c create mode 100644 src/core/qweight.h create mode 100644 src/core/sample_profiler.c create mode 100644 src/core/sample_profiler.h create mode 100644 src/core/sampling_modifiers.c create mode 100644 src/core/sampling_modifiers.h create mode 100644 src/core/session_snapshot.c create mode 100644 src/core/session_snapshot.h create mode 100644 src/formats/gguf.c create mode 100644 src/formats/gguf.h create mode 100644 src/formats/gguf_dequant.c create mode 100644 src/formats/gguf_dequant.h create mode 100644 src/formats/gguf_layer_bytes.c create mode 100644 src/formats/gguf_layer_bytes.h create mode 100644 src/memory/arena.c create mode 100644 src/memory/arena.h create mode 100644 src/models/bonsai.h create mode 100644 src/models/bonsai_forward.c create mode 100644 src/models/gpu_attn_path_tracker.c create mode 100644 src/models/gpu_attn_path_tracker.h create mode 100644 src/models/gpu_graph_capture_tracker.c create mode 100644 src/models/gpu_graph_capture_tracker.h create mode 100644 src/models/llama.c create mode 100644 src/models/llama.h create mode 100644 src/models/llama_attention_trace.c create mode 100644 src/models/llama_attention_trace.h create mode 100644 src/models/llama_forward_cuda.c create mode 100644 src/models/llama_forward_tt.c create mode 100644 src/models/llama_residual_trace.c create mode 100644 src/models/llama_residual_trace.h create mode 100644 src/models/llama_state_snapshot.c create mode 100644 src/models/phi3.c create mode 100644 src/models/phi3.h create mode 100644 src/models/pixtral_vision.c create mode 100644 src/models/pixtral_vision.h create mode 100644 src/models/qwen35moe.c create mode 100644 src/models/qwen35moe.h create mode 100644 src/models/qwen35moe_attn.c create mode 100644 src/models/qwen35moe_attn_tt.c create mode 100644 src/models/qwen35moe_checkpoint_band.c create mode 100644 src/models/qwen35moe_checkpoint_band.h create mode 100644 src/models/qwen35moe_forward.c create mode 100644 src/models/qwen35moe_forward_cuda.c create mode 100644 src/models/qwen35moe_forward_metal.c create mode 100644 src/models/qwen35moe_forward_tt.c create mode 100644 src/models/qwen35moe_gdn_normcheck_tt.c create mode 100644 src/models/qwen35moe_gdn_parity_tt.c create mode 100644 src/models/qwen35moe_gdn_strip_tt.c create mode 100644 src/models/qwen35moe_gdn_tt.c create mode 100644 src/models/qwen35moe_internal.h create mode 100644 src/models/qwen35moe_kv_q4.h create mode 100644 src/models/qwen35moe_metal_coverage.c create mode 100644 src/models/qwen35moe_metal_coverage.h create mode 100644 src/models/qwen35moe_metal_kernel_registry.c create mode 100644 src/models/qwen35moe_metal_kernel_registry.h create mode 100644 src/models/qwen35moe_moe.c create mode 100644 src/models/qwen35moe_prefill_checkpoint_tt.c create mode 100644 src/models/qwen35moe_prefill_overlap_tt.c create mode 100644 src/models/qwen35moe_prefill_prefetch_tt.c create mode 100644 src/models/qwen35moe_prefill_tt.c create mode 100644 src/models/qwen35moe_prefill_tt_wall.c create mode 100644 src/models/qwen35moe_prefill_tt_wall.h create mode 100644 src/models/qwen35moe_resident_tt.c create mode 100644 src/models/qwen35moe_ssm.c create mode 100644 src/models/qwen35moe_state_snapshot.c create mode 100644 src/models/qwen35moe_tt_internal.h create mode 100644 src/models/rwkvx.c create mode 100644 src/models/rwkvx.h create mode 100644 src/serve/attn_residency_json.c create mode 100644 src/serve/attn_residency_json.h create mode 100644 src/serve/base64.c create mode 100644 src/serve/base64.h create mode 100644 src/serve/batch_sched.c create mode 100644 src/serve/batch_sched.h create mode 100644 src/serve/grammar_spec_chooser.c create mode 100644 src/serve/grammar_spec_chooser.h create mode 100644 src/serve/graph_residency_json.c create mode 100644 src/serve/graph_residency_json.h create mode 100644 src/serve/http_client.c create mode 100644 src/serve/http_client.h create mode 100644 src/serve/http_server.c create mode 100644 src/serve/http_server.h create mode 100644 src/serve/json_canonical_stream.c create mode 100644 src/serve/json_canonical_stream.h create mode 100644 src/serve/json_reader.c create mode 100644 src/serve/json_reader.h create mode 100644 src/serve/json_value_grammar.c create mode 100644 src/serve/json_value_grammar.h create mode 100644 src/serve/json_writer.c create mode 100644 src/serve/json_writer.h create mode 100644 src/serve/model_admin.c create mode 100644 src/serve/model_admin.h create mode 100644 src/serve/model_registry.c create mode 100644 src/serve/model_registry.h create mode 100644 src/serve/model_session.c create mode 100644 src/serve/model_session.h create mode 100644 src/serve/openai_chat.c create mode 100644 src/serve/openai_chat.h create mode 100644 src/serve/registry.c create mode 100644 src/serve/registry.h create mode 100644 src/serve/registry_client.c create mode 100644 src/serve/registry_client.h create mode 100644 src/serve/registry_wire.h create mode 100644 src/serve/repetition_loop_detector.c create mode 100644 src/serve/repetition_loop_detector.h create mode 100644 src/serve/request_profile.c create mode 100644 src/serve/request_profile.h create mode 100644 src/serve/response_format_grammar.c create mode 100644 src/serve/response_format_grammar.h create mode 100644 src/serve/router.c create mode 100644 src/serve/router.h create mode 100644 src/serve/router_command.c create mode 100644 src/serve/router_command.h create mode 100644 src/serve/serve_backend_guard.c create mode 100644 src/serve/serve_backend_guard.h create mode 100644 src/serve/serve_command.c create mode 100644 src/serve/serve_command.h create mode 100644 src/serve/serve_emitter.c create mode 100644 src/serve/serve_emitter.h create mode 100644 src/serve/serve_generation.c create mode 100644 src/serve/serve_generation.h create mode 100644 src/serve/serve_prompt_tokens.c create mode 100644 src/serve/serve_prompt_tokens.h create mode 100644 src/serve/serve_registry.c create mode 100644 src/serve/serve_registry.h create mode 100644 src/serve/serve_shutdown.c create mode 100644 src/serve/serve_shutdown.h create mode 100644 src/serve/serve_spec_lookup.c create mode 100644 src/serve/serve_spec_lookup.h create mode 100644 src/serve/session_affinity.c create mode 100644 src/serve/session_affinity.h create mode 100644 src/serve/session_key.h create mode 100644 src/serve/session_watch.c create mode 100644 src/serve/session_watch.h create mode 100644 src/serve/signal_handlers.c create mode 100644 src/serve/signal_handlers.h create mode 100644 src/serve/tool_call_stream.c create mode 100644 src/serve/tool_call_stream.h create mode 100644 src/serve/tool_calling.c create mode 100644 src/serve/tool_calling.h create mode 100644 src/serve/tool_grammar.c create mode 100644 src/serve/tool_grammar.h create mode 100644 src/serve/tool_grammar_json.c create mode 100644 src/serve/tool_grammar_json.h create mode 100644 src/serve/tool_param_empty_value_rule.c create mode 100644 src/serve/tool_param_empty_value_rule.h create mode 100755 tests/ab_build_arm.sh create mode 100644 tests/abandoned_call_fixture.h create mode 100644 tests/bench_batch_decode.c create mode 100644 tests/bench_depth_qwen35.c create mode 100644 tests/bench_ngram_accept.c create mode 100644 tests/bench_sampler_temp_delta.c create mode 100644 tests/bench_serve_prefix_cache.c create mode 100644 tests/bench_spec_accept.c create mode 100644 tests/bench_spec_verify_gate.c create mode 100644 tests/bench_tt_prefill_quantum.c create mode 100644 tests/bench_verify_rows.c create mode 100644 tests/bench_vision.c create mode 100644 tests/causality_red/cuda_host_attention_reads_future.patch create mode 100755 tests/causality_red/run_red.sh create mode 100644 tests/causality_red/tt_chunk_attention_causal_off.patch create mode 100644 tests/chat_corpus.c create mode 100644 tests/chat_corpus.h create mode 100644 tests/data/chat_parity_corpus.txt create mode 100644 tests/data/chat_parity_corpus_crossed.txt create mode 100644 tests/data/chat_parity_corpus_sysonly.txt create mode 100644 tests/data/code_edit_prompts.txt create mode 100644 tests/data/ngram_chat_prompts.txt create mode 100644 tests/data/ngram_code_edit_prompts.txt create mode 100644 tests/data/ngram_code_long_prompt.txt create mode 100644 tests/data/ngram_prose_prompts.txt create mode 100755 tests/dispatch/e2e.sh create mode 100755 tests/dispatch/stub_upstream.py create mode 100644 tests/eval_ppl.c create mode 100644 tests/frame_capture_fixtures.h create mode 100644 tests/gen_golden_qwen35.c create mode 100644 tests/golden/README.md create mode 100644 tests/golden/moe_router_logits.bin create mode 100644 tests/golden/moe_shared_gate_input.bin create mode 100644 tests/golden/moe_shared_gate_scalar.bin create mode 100644 tests/golden/moe_shared_gate_weight.bin create mode 100644 tests/golden/moe_topk_indices.bin create mode 100644 tests/golden/moe_topk_weights.bin create mode 100644 tests/golden/q3k_dequant_block0.bin create mode 100644 tests/golden/q3k_matvec_input.bin create mode 100644 tests/golden/q3k_matvec_output.bin create mode 100644 tests/golden/q3k_matvec_weight.bin create mode 100644 tests/golden/qknorm_input.bin create mode 100644 tests/golden/qknorm_output.bin create mode 100644 tests/golden/qknorm_weight.bin create mode 100644 tests/golden/rope_k_input.bin create mode 100644 tests/golden/rope_k_pos0.bin create mode 100644 tests/golden/rope_k_pos1.bin create mode 100644 tests/golden/rope_k_pos100.bin create mode 100644 tests/golden/rope_k_pos7.bin create mode 100644 tests/golden/rope_q_input.bin create mode 100644 tests/golden/rope_q_pos0.bin create mode 100644 tests/golden/rope_q_pos1.bin create mode 100644 tests/golden/rope_q_pos100.bin create mode 100644 tests/golden/rope_q_pos7.bin create mode 100644 tests/golden/ssm_conv_out.bin create mode 100644 tests/golden/ssm_conv_qkv.bin create mode 100644 tests/golden/ssm_conv_state_in.bin create mode 100644 tests/golden/ssm_conv_state_out.bin create mode 100644 tests/golden/ssm_conv_weight.bin create mode 100644 tests/golden/ssm_gated_out.bin create mode 100644 tests/golden/ssm_gated_weight.bin create mode 100644 tests/golden/ssm_gated_y.bin create mode 100644 tests/golden/ssm_gated_z.bin create mode 100644 tests/golden/ssm_recur_beta.bin create mode 100644 tests/golden/ssm_recur_g.bin create mode 100644 tests/golden/ssm_recur_k.bin create mode 100644 tests/golden/ssm_recur_q.bin create mode 100644 tests/golden/ssm_recur_state_in.bin create mode 100644 tests/golden/ssm_recur_state_out.bin create mode 100644 tests/golden/ssm_recur_v.bin create mode 100644 tests/golden/ssm_recur_y.bin create mode 100644 tests/ngram_accept_stream.c create mode 100644 tests/ngram_accept_stream.h create mode 100644 tests/parity_accel_arm.c create mode 100644 tests/parity_accel_arm.h create mode 100644 tests/parity_gate_criteria.c create mode 100644 tests/parity_gate_criteria.h create mode 100644 tests/parity_logit_dump.c create mode 100644 tests/parity_logit_dump.h create mode 100644 tests/parity_noise.c create mode 100644 tests/parity_noise.h create mode 100644 tests/parity_score.c create mode 100644 tests/parity_score.h create mode 100755 tests/run_tt_prefill_red.sh create mode 100644 tests/serve_bench_users.py create mode 100755 tests/serve_concurrency_smoke.sh create mode 100755 tests/serve_eos_repeat_smoke.sh create mode 100755 tests/serve_multimodel_smoke.sh create mode 100755 tests/serve_response_format_smoke.sh create mode 100755 tests/serve_session_export_smoke.sh create mode 100755 tests/serve_smoke.sh create mode 100755 tests/serve_tools_smoke.sh create mode 100644 tests/test_2die_swap.c create mode 100644 tests/test_admission_reserve.c create mode 100644 tests/test_arena.c create mode 100644 tests/test_attn_ring_retry.c create mode 100644 tests/test_backend_guard.c create mode 100644 tests/test_base64.c create mode 100644 tests/test_batch_decode_correct.c create mode 100644 tests/test_batch_decoder.c create mode 100644 tests/test_bpe_pretokenize.c create mode 100644 tests/test_bs_load.c create mode 100644 tests/test_build.c create mode 100644 tests/test_chat.c create mode 100644 tests/test_chat_parity_gate.c create mode 100644 tests/test_chat_request.c create mode 100644 tests/test_dequant_props.c create mode 100644 tests/test_die_layer_split.c create mode 100644 tests/test_die_manager.c create mode 100644 tests/test_die_scratch_ctx.c create mode 100644 tests/test_embed_row.c create mode 100644 tests/test_empty_argument.c create mode 100644 tests/test_frame_capture.c create mode 100644 tests/test_gdn_layout.c create mode 100644 tests/test_gdnc_compact.c create mode 100644 tests/test_gen_bench.c create mode 100644 tests/test_gen_threads.c create mode 100644 tests/test_gguf.c create mode 100644 tests/test_gguf_layer_bytes.c create mode 100644 tests/test_grammar_arm_window.c create mode 100644 tests/test_grammar_mask_shortlist.c create mode 100644 tests/test_grammar_spec_chooser.c create mode 100644 tests/test_grammar_topn_sampler.c create mode 100644 tests/test_graph_capture_concurrency.c create mode 100644 tests/test_graph_capture_retry.c create mode 100644 tests/test_json_value_grammar.c create mode 100644 tests/test_kernel_parity.c create mode 100644 tests/test_llama_state_snapshot.c create mode 100644 tests/test_memwatch.c create mode 100644 tests/test_model_registry.c create mode 100644 tests/test_model_unload_reclaim.c create mode 100644 tests/test_prefill.c create mode 100644 tests/test_prefix_cache_qwen35.c create mode 100644 tests/test_prefix_cache_sys.c create mode 100644 tests/test_prefix_projection.c create mode 100644 tests/test_pretok_props.c create mode 100644 tests/test_q6k_parity.c create mode 100644 tests/test_qmv.c create mode 100644 tests/test_qmv_threads.c create mode 100644 tests/test_qwen35_ckpt_band.c create mode 100644 tests/test_qwen35_prefill_causality.c create mode 100644 tests/test_qwen35moe.c create mode 100644 tests/test_qwen35moe_metal_coverage.c create mode 100644 tests/test_registry.c create mode 100644 tests/test_repetition_loop_detector.c create mode 100644 tests/test_response_format_grammar.c create mode 100644 tests/test_rwkvx.c create mode 100644 tests/test_sampler_parity.c create mode 100644 tests/test_sampling_modifiers.c create mode 100644 tests/test_serve_emitter.c create mode 100644 tests/test_serve_sched_prefix_cache.c create mode 100644 tests/test_serve_sched_qwen35.c create mode 100644 tests/test_serve_shutdown.c create mode 100644 tests/test_serve_spec_lookup.c create mode 100644 tests/test_session_affinity.c create mode 100644 tests/test_session_watch.c create mode 100644 tests/test_simd.c create mode 100644 tests/test_simd_props.c create mode 100644 tests/test_slot_free_bind_qwen35.c create mode 100644 tests/test_sparse_sampler_identity.c create mode 100644 tests/test_spec_batch.c create mode 100644 tests/test_spec_decode.c create mode 100644 tests/test_spec_generate.c create mode 100644 tests/test_spec_qwen35.c create mode 100644 tests/test_tool_call_defects.c create mode 100644 tests/test_tool_call_stream.c create mode 100644 tests/test_tool_call_truncation.c create mode 100644 tests/test_tool_calling.c create mode 100644 tests/test_tool_grammar.c create mode 100644 tests/test_tool_grammar_json.c create mode 100644 tests/test_topn_row_exact.c create mode 100644 tests/test_tt_boot_guard_leftover.c create mode 100644 tests/test_tt_column_runs.c create mode 100644 tests/test_tt_descriptor_census.c create mode 100644 tests/test_tt_eltwise_split.c create mode 100644 tests/test_tt_name_list.c create mode 100644 tests/test_tt_prefill_parity.c create mode 100644 tests/test_tt_stage_pipeline.c create mode 100644 tests/test_two_pass_parity_gate.c create mode 100644 tests/test_users_qwen35.c create mode 100644 tests/test_verify_batch.c create mode 100644 tests/tt_prefix_cache_link_stubs.c create mode 100644 tools/build_mtp_gguf.py create mode 100644 tools/fetch_mtp_tensors.py create mode 100644 tools/gguf_retype.py create mode 100644 tools/gguf_tensor_md5.py create mode 100644 tools/gguf_token_text.py create mode 100755 tools/sample_profile_report.sh diff --git a/.cla/signatures.md b/.cla/signatures.md new file mode 100644 index 0000000..d44f158 --- /dev/null +++ b/.cla/signatures.md @@ -0,0 +1,47 @@ +# Pantheon CLA signatures + +This file is the signature roster for the Pantheon Contributor License +Agreement. Adding your entry here, in a pull request signed off by you, **is +your signature** — see [`CLA.md`](../CLA.md) for the agreement itself and +[`CLA-CORPORATE.md`](../CLA-CORPORATE.md) for organizations. + +Do not edit or remove anyone else's entry. Entries are append-only; the git +history of this file is the record of who agreed, to what, and when. + +The CI check in `.github/workflows/contributions.yml` matches the **`git-email`** +values below against the author address on every commit in a pull request. If +you commit under more than one address, list each on its own `git-email:` line. + +--- + +## Individual signatures + +```yaml +- name: David (Corey) Herrera + git-email: coreyh55@gmail.com + forge-username: dcherrera + commit-name: Zero # git author name on this project's history — same person + role: project owner and copyright holder + date: 2026-08-11 + agreement: ICLA v1.0 + statement: I have read the Pantheon ICLA and I agree to it. +``` + + + +## Corporate signatures + +Organizations sign by email (see `CLA-CORPORATE.md`); their entry is added here +after countersignature, with one `git-email:` line per authorized contributor +from Schedule A. + +```yaml +# - organization: Example, Inc. +# agreement: CCLA v1.0 +# signed: YYYY-MM-DD +# schedule-a-updated: YYYY-MM-DD +# contributors: +# - name: Contributor Name +# git-email: contributor@example.com +# forge-username: their-username +``` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..71e4116 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,23 @@ +## What this changes + + + +## Evidence + + + +## Checklist + +- [ ] **I license this contribution under the CLA in [`CLA.md`](../CLA.md)** — my entry + is in [`.cla/signatures.md`](../.cla/signatures.md). I keep my copyright. +- [ ] Every commit signed off (`git commit -s`) — `./scripts/check-dco.sh` +- [ ] Builds clean: `make USE_BLAS=1 pantheon`, zero warnings under `-Wall -Wextra` +- [ ] `make USE_BLAS=1 test` passes, and new functionality has tests +- [ ] New files carry the SPDX pair and the `@file` header block +- [ ] Docs updated if the public API changed + + diff --git a/.github/workflows/contributions.yml b/.github/workflows/contributions.yml new file mode 100644 index 0000000..49215a9 --- /dev/null +++ b/.github/workflows/contributions.yml @@ -0,0 +1,43 @@ +name: contributions + +# DCO sign-off + CLA roster check on every pull request. +# The same scripts run locally: scripts/check-dco.sh, scripts/check-cla.sh +# +# Path note: Gitea/Forgejo Actions read .gitea/workflows first and fall back to +# .github/workflows, so this one file serves both forges. + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + dco-and-cla: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Fetch base branch + env: + BASE: ${{ github.event.pull_request.base.ref }} + run: git fetch --no-tags origin "+refs/heads/$BASE:refs/remotes/origin/$BASE" + + - name: DCO sign-off + env: + BASE: ${{ github.event.pull_request.base.ref }} + run: ./scripts/check-dco.sh "origin/$BASE..HEAD" + + - name: CLA signature + env: + BASE: ${{ github.event.pull_request.base.ref }} + run: ./scripts/check-cla.sh "origin/$BASE..HEAD" + + - name: File headers on added sources + env: + BASE: ${{ github.event.pull_request.base.ref }} + run: ./scripts/check-headers.sh "origin/$BASE..HEAD" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c12be39 --- /dev/null +++ b/.gitignore @@ -0,0 +1,165 @@ +# Binaries. +# `pantheon` is now a SYMLINK to the per-backend binary the last build produced +# (Makefile: PN_BIN / PN_COMPAT_BIN); each backend also writes its own file, so +# a TT build can no longer overwrite a deployed CUDA one. All of them are +# ignored, and the combination names too, so a `make USE_CUDA=1 USE_TT=1` tree +# stays clean. +pantheon +pantheon-cpu +pantheon-cuda +pantheon-tt +pantheon-metal +pantheon-cuda-tt +pantheon-cuda-metal +pantheon-tt-metal +pantheon-cuda-tt-metal +tests/test_backend_guard +tests/test_build +tests/test_graph_capture_concurrency +tests/test_attn_ring_retry +tests/test_graph_capture_retry +tests/test_gguf +tests/test_arena +tests/test_qmv +tests/test_simd +tests/test_chat +tests/test_chat_request +tests/test_tool_calling +tests/test_tool_call_defects +tests/test_tool_call_truncation +tests/test_empty_argument +tests/test_frame_capture +tests/test_tool_grammar +tests/test_tool_grammar_json +tests/test_grammar_spec_chooser +tests/test_grammar_mask_shortlist +tests/test_grammar_topn_sampler +tests/test_serve_emitter +tests/test_tool_call_stream +tests/test_repetition_loop_detector +tests/test_registry +tests/test_base64 +tests/test_session_affinity +tests/test_session_watch +tests/test_serve_shutdown +tests/test_bs_load +tests/test_sampler_parity +tests/test_bpe_pretokenize +tests/test_rwkvx + +# Debug symbols +*.dSYM/ + +# Object files +*.o + +# macOS +.DS_Store + +# TeamIDE: only what its PM reads from git is tracked -- project.yml and the +# tasks/plans/notes/agents markdown (TeamIDE reads the kanban via `git ls-tree`, +# so a blanket .teamide/ rule would hide it). Everything else under .teamide/ is +# machine- or session-specific: subrepos.json (per-user clone URLs), chat +# history, activity log, agent runs and reports, caches. +.teamide/* +!.teamide/project.yml +!.teamide/tasks/ +!.teamide/plans/ +!.teamide/notes/ +!.teamide/agents/ + +# qwen35moe test binaries (sources tracked, binaries not) +tests/test_qwen35moe +tests/test_qwen35moe_metal_coverage +tests/test_qwen35moe_metal_coverage_fake + +# qwen35moe golden-vector generator binary (source + generated goldens tracked) +tests/gen_golden_qwen35 + +# Accelerator-parity harness binaries (sources tracked, binaries not) +tests/test_chat_parity_gate +tests/test_prefix_projection + +# qwen35moe GPU-vs-CPU parity harness binary (source tracked, binary not) +tests/test_kernel_parity + +# 2-die barswap context-swap smoke test binary (source tracked, binary not) +tests/test_2die_swap +tests/bench_batch_decode +tests/test_batch_decode_correct +tests/test_batch_decoder + +# quality-eval driver binary + run artifacts (source, prompts, corpus tracked) +tests/eval_ppl +evals/out/ + +# MemoryAware unit-test binary (source tracked, binary not) +tests/test_memwatch +tests/test_die_manager +tests/test_model_registry +tests/test_model_unload_reclaim +# A4 embed-row unit-test binary (source tracked, binary not) +tests/test_embed_row + +# Serve-scheduler + PC-batched gate binaries (source tracked, binary not) +tests/test_serve_sched_qwen35 +tests/test_serve_sched_prefix_cache +tests/test_prefix_cache_qwen35 +tests/bench_serve_prefix_cache +tests/test_prefix_cache_sys + +# Local agent/editor notes; deliberately untracked. +CLAUDE.md + +# Compiled test binaries (source tracked, binary not) +tests/test_response_format_grammar +tests/test_sampling_modifiers + +# Build artifacts (were in .git/info/exclude, which is local-only and never cloned) +/libpantheon.a +/build/ + +# Model weights — multi-GB GGUFs land here. Never track. +/models/ + +# Foundry: the public tree carries it as a git submodule at ./foundry; a private +# checkout may use a local symlink there instead, which must never be tracked. +# The Makefile finds Foundry at FOUNDRY=, else ./foundry, else ../foundry. + +# Plans, specs and benchmark write-ups live outside this repo. Symlinked in +# for local convenience; never tracked or published here. +/plans +__pycache__/ +tests/bench_sampler_temp_delta +tests/test_sparse_sampler_identity +tests/test_topn_row_exact +tests/test_grammar_arm_window +tests/test_gdn_layout +tests/test_tt_column_runs +tests/test_qwen35_ckpt_band + +# Compiled test binaries. This used to be one line per test, so every NEW test +# was untracked until someone remembered to add it -- and a `git add tests/` +# then swept 2.8 MB of build artifacts into a commit. Pattern, not a roll-call. +tests/test_* +tests/bench_* +tests/gen_golden_* +!tests/*.c +!tests/*.h + +# Parity-gate dumps and logs (L11 Mac CPU oracle): large, regenerable +tests/parity_out/ + +# Claude Code hooks are generated per machine by TeamIDE (they list local paths). +.claude/hooks/ + +# bench_ngram_accept --generate output (recorded token streams); make your own. +tests/data/ngram_accept_dumps/ + +# checkpoint-ceiling round captures (Mac side) +tests/serve_prefix_think_ckpt/out/ +# toolcall stream probe captures +tests/serve_toolcall_stream/out/ +# required-with-thinking proof captures +tests/serve_required_thinking/out/ +tests/serve_required_thinking/probe_bin/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0eeeebe --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "foundry"] + path = foundry + url = https://git.teamide.dev/David/Foundry.git diff --git a/CLA-CORPORATE.md b/CLA-CORPORATE.md new file mode 100644 index 0000000..73ce1db --- /dev/null +++ b/CLA-CORPORATE.md @@ -0,0 +1,150 @@ +# Pantheon Corporate Contributor License Agreement + +**Version 1.0** + +Use this agreement if contributions to Pantheon are being made **by employees or +contractors on behalf of an organization** that owns the resulting intellectual +property. Individuals contributing on their own behalf should use +[`CLA.md`](./CLA.md) instead. + +This agreement mirrors the Individual CLA. Its purpose is to make sure that the +entity that actually owns the work has granted the rights, so that neither of us +is relying on an individual employee's authority to license their employer's IP. + +**The organization keeps its copyright.** This is a license, not an assignment. + +--- + +## 1. Definitions + +**"I"**, **"me"**, **"my"** — David (Corey) Herrera, the copyright holder of Pantheon, +and my successors and assigns. + +**"You"**, **"your"** — the organization signing this agreement, together with +all entities that control, are controlled by, or are under common control with +it. "Control" means ownership of more than 50% of outstanding shares, or the +power to direct the entity's management. + +**"Contribution"** — any original work of authorship, including any modification +or addition to an existing work, intentionally submitted to me for inclusion in +Pantheon by you or by anyone authorized to submit on your behalf, except +anything conspicuously marked **"Not a Contribution."** + +## 2. Copyright license + +You grant me a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare derivative works of, +publicly display, publicly perform, sublicense, and distribute your Contribution +and derivative works of it. + +## 3. Patent license + +You grant me a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer your +Contribution. This license covers only those patent claims licensable by you +that are necessarily infringed by your Contribution alone, or by the combination +of your Contribution with Pantheon. + +If any entity institutes patent litigation alleging that your Contribution, or +Pantheon itself, constitutes direct or contributory patent infringement, then +every patent license granted to that entity under this agreement terminates as +of the date the litigation is filed. + +## 4. Outbound licensing — what I may do with your Contribution + +The licenses in sections 2 and 3 include the right to license and sublicense +your Contribution to others under **any terms I choose**: the project's current +outbound license (AGPL-3.0-or-later), a different open-source license if the project's +outbound license changes, or **proprietary or closed-source terms**. + +I may also **assign or transfer** these rights, in whole or in part, together +with the project or with the copyrights in it, to a successor, acquirer, or +foundation, who then holds them on these same terms. A sale, merger, or +relicensing of Pantheon does not require me to come back to you for permission. + +I am not obligated to use your Contribution, and I may modify or remove it at +any time. + +## 5. Your representations + +You represent that: + +1. **You own it and can license it.** Each Contribution is an original creation + owned by you, and the person signing below is authorized to bind you to this + agreement. +2. **Your people are authorized.** The individuals listed in Schedule A are + authorized to submit Contributions on your behalf. You will keep that list + current, and you remain responsible for Contributions submitted by anyone + you have authorized, whether or not they are listed. +3. **No hidden third-party code.** Contributions do not knowingly include + anyone else's code, except as identified under section 6. +4. **You'll tell me** if any of the above stops being true. + +## 6. Third-party submissions + +Work that is not your original creation may be submitted only if it is +identified separately in the pull request, retains its original copyright and +license notices, and is conspicuously marked +`Submitted on behalf of a third party: — license: `. I may +decline it. **GPL- or AGPL-only third-party code cannot be accepted**, because +it would prevent the project from being relicensed in the future. + +## 7. No warranty, no obligation + +Contributions are provided **"AS IS"**, without warranty or condition of any +kind, express or implied, to the maximum extent permitted by law. You are not +expected to provide support, maintenance, or updates. + +## 8. Governing law + +This agreement is governed by the laws of the State of Missouri, United States, +without regard to its conflict-of-law rules. + +## 9. Entire agreement + +This is the entire agreement between us about your Contributions, and it +supersedes any earlier understanding on the subject. If any part of it is held +unenforceable, the rest stays in force. + +--- + +## How to sign + +Corporate signatures are handled by email rather than by pull request, because +they need a signature from someone authorized to bind the organization. + +1. Email **coreyh55@gmail.com** with the subject `Pantheon CCLA — `, + including the completed block below and Schedule A. A scanned signature, a + digital signature, or a reply from an authorized signer's corporate email + address all work. +2. Once countersigned, I add an entry for your organization to + [`.cla/signatures.md`](./.cla/signatures.md), listing each authorized + contributor's commit email so CI can verify their pull requests. +3. Your contributors still sign off every commit with the DCO (`git commit -s`) + — see [`CONTRIBUTING.md`](./CONTRIBUTING.md). + +``` +Organization: ____________________________________________ +Mailing address: ____________________________________________ +Point of contact: ____________________________________________ +Contact email: ____________________________________________ + +Authorized signer + Name: ____________________________________________ + Title: ____________________________________________ + Signature: ____________________________________________ + Date: ____________________________________________ + +Agreement: Pantheon CCLA v1.0 +``` + +### Schedule A — authorized contributors + +| Full name | Commit email | Forge username | +|-----------|--------------|----------------| +| | | | + +To add or remove people later, email an updated Schedule A. Contributions from +someone not covered by a current Schedule A will be held until the list is +updated. diff --git a/CLA.md b/CLA.md new file mode 100644 index 0000000..27bf486 --- /dev/null +++ b/CLA.md @@ -0,0 +1,155 @@ +# Pantheon Individual Contributor License Agreement + +**Version 1.0** + +Thank you for contributing to Pantheon. + +This agreement lets me include your contribution in Pantheon and distribute it +under the project's licenses — the one it ships under today (AGPL-3.0-or-later) and any license the +project moves to later. + +**You keep your copyright.** This is a license, not an assignment. Your +contribution stays yours: you can use it, publish it, sell it, or license it to +anyone else, anywhere, forever, on any terms you like. + +Sign once — see [How to sign](#how-to-sign) at the end. It covers everything you +contribute to Pantheon, before and after signing. + +--- + +## 1. Definitions + +**"I"**, **"me"**, **"my"** — David (Corey) Herrera, the copyright holder of Pantheon, +and my successors and assigns. + +**"You"**, **"your"** — the individual who signs this agreement. If you are +signing on behalf of an organization, use +[`CLA-CORPORATE.md`](./CLA-CORPORATE.md) instead. + +**"Contribution"** — any original work of authorship, including any modification +or addition to an existing work, that you intentionally submit to me for +inclusion in Pantheon. "Submit" means any form of electronic, verbal, or written +communication sent to me or to the project — pull requests, patches, issue +comments, mailing-list or chat messages — except anything you conspicuously mark +**"Not a Contribution."** + +## 2. Copyright license + +You grant me a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare derivative works of, +publicly display, publicly perform, sublicense, and distribute your Contribution +and derivative works of it. + +## 3. Patent license + +You grant me a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer your +Contribution. This license covers only those patent claims licensable by you +that are necessarily infringed by your Contribution alone, or by the combination +of your Contribution with Pantheon. + +If any entity institutes patent litigation alleging that your Contribution, or +Pantheon itself, constitutes direct or contributory patent infringement, then +every patent license granted to that entity under this agreement terminates as +of the date the litigation is filed. + +## 4. Outbound licensing — what I may do with your Contribution + +The licenses in sections 2 and 3 include the right to license and sublicense +your Contribution to others under **any terms I choose**. Specifically: + +- under the project's current outbound license — AGPL-3.0-or-later; +- under a different open-source license, if the project's outbound license + changes; +- under **proprietary or closed-source terms**, as part of a commercial license + or a product. + +I may also **assign or transfer** the rights you grant here, in whole or in +part, together with the project or with the copyrights in it — to a successor, +an acquirer, or a foundation. Whoever holds them then holds them on these same +terms. A sale, merger, or relicensing of Pantheon does not require me to come +back to you for permission. + +I am not obligated to use your Contribution, and I may modify or remove it at +any time. + +## 5. Your representations + +You represent that: + +1. **It's yours.** Each Contribution is your original creation and you are + legally entitled to grant the licenses above. +2. **Your employer is handled.** If your employer has rights to intellectual + property you create, you have either received permission to contribute on + your own behalf, or your employer has waived those rights, or your employer + has signed the Corporate CLA. +3. **No hidden third-party code.** Your Contribution does not knowingly include + anyone else's code, except as identified under section 6. +4. **You'll tell me** if any of the above stops being true. + +## 6. Third-party submissions + +You may submit work that is not your original creation only if you: + +- identify it separately in the pull request, +- keep its original copyright and license notices intact, and +- mark it conspicuously as + `Submitted on behalf of a third party: — license: `. + +I may decline it. Whether it can be accepted at all depends on its license +permitting commercial distribution alongside the rest of the tree — **GPL- or +AGPL-only third-party code cannot be accepted**, because it would prevent the +project from being relicensed in the future. + +## 7. No warranty, no obligation + +Your Contribution is provided **"AS IS"**, without warranty or condition of any +kind, express or implied, to the maximum extent permitted by law. You are not +expected to provide support, maintenance, or updates for it. + +## 8. Governing law + +This agreement is governed by the laws of the State of Missouri, United States, +without regard to its conflict-of-law rules. + +## 9. Entire agreement + +This is the entire agreement between us about your Contributions, and it +supersedes any earlier understanding on the subject. If any part of it is held +unenforceable, the rest stays in force. + +--- + +## How to sign + +**1. Add yourself to [`.cla/signatures.md`](./.cla/signatures.md)** as part of +your first pull request: + +```yaml +- name: Your Full Name + git-email: you@example.com + forge-username: your-username + date: YYYY-MM-DD + agreement: ICLA v1.0 + statement: I have read the Pantheon ICLA and I agree to it. +``` + +Use the **same email address you commit with** — the CI check matches on it. If +you commit under more than one address, list each on its own `git-email:` line. + +**2. Sign off every commit** with the DCO (`git commit -s`) — see +[`CONTRIBUTING.md`](./CONTRIBUTING.md). + +That pull request, carrying your entry and signed off by you, **is your +signature**. The git history is the record: who agreed, to what version, and +when. Nothing to print, scan, or email. + +If you would rather not sign, open an issue describing your change instead of a +pull request. An idea costs nothing to accept. + +--- + +*Questions about this agreement: coreyh55@gmail.com. If something here is +ambiguous, ask rather than guessing — I would rather clarify it than have +someone rely on a reading I did not intend.* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8613c5e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,177 @@ +# Contributing to Pantheon + +Thanks for your interest in Pantheon. This document covers the basics. + +## Licensing of contributions — read this first + +Pantheon ships under **AGPL-3.0** (see [`LICENSE`](./LICENSE)). + +> **Contributions are made under the Contributor License Agreement in +> [`CLA.md`](./CLA.md)**, which grants me a broad copyright and patent license, +> including the right to sublicense your contribution under any terms and to +> transfer that right along with the project. + +**You keep your copyright.** The CLA is a license, not an assignment — nothing +is transferred away from you. Your contribution stays yours to use, publish, +sell, or license to anyone else, anywhere, forever. + +Signing takes one commit: add your entry to +[`.cla/signatures.md`](./.cla/signatures.md) in your first pull request. Nothing +to print, scan, or email. Organizations whose employees contribute on company +time sign [`CLA-CORPORATE.md`](./CLA-CORPORATE.md) by email instead. + +If you would rather not sign, open an issue describing the change instead of a +pull request. An idea costs nothing to accept. + +### Sign your commits (DCO) + +Every commit must carry a `Signed-off-by` line certifying the +[Developer Certificate of Origin](https://developercertificate.org/) — that you +wrote the code, or otherwise have the right to submit it: + +```bash +git commit -s -m "your message" +``` + +which appends: + +``` +Signed-off-by: Your Name +``` + +Use your real name and a working email address — the same address as your CLA +roster entry, since that is what CI matches on. If you are contributing work your +employer owns, confirm you actually have the right to do so before you sign off. + +Both checks run in CI on every pull request, and you can run them yourself first: + +```bash +./scripts/check-dco.sh # every commit signed off, matching its author +./scripts/check-cla.sh # every author on the CLA roster +./scripts/check-headers.sh # every source file you ADD has SPDX + the @file block +``` + +### License header on new files + +Every source file starts with: + +```c +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +``` + +Add it to new files you create. Do not edit the copyright line on existing files +— you keep your copyright in what you contribute without needing a line in the +header, and a per-file list of names becomes unmaintainable fast. `git log` and +`.cla/signatures.md` are the authoritative record of who wrote what. + +## Getting Started + +Pantheon builds on Foundry, a git submodule at `./foundry`. Clone with +`git clone --recursive https://git.teamide.dev/David/Pantheon.git`, or run +`git submodule update --init` in an existing clone. The Makefile uses, in order, +`FOUNDRY=`, then `./foundry` (the submodule or a symlink), then `../foundry` +(a sibling checkout). + +```bash +make USE_BLAS=1 pantheon # build the CLI +make USE_BLAS=1 test # build + run the test suites +make DEBUG=1 USE_BLAS=1 pantheon +make clean +``` + +| Flag | What | When | +|------|------|------| +| `USE_BLAS=1` | SGEMV/SGEMM via Accelerate or OpenBLAS | always | +| `USE_THREADS=1` | pthread pool for parallel ops | multi-core inference | +| `USE_METAL=1` | Metal GPU compute | macOS / Apple Silicon | +| `USE_CUDA=1` | CUDA via `dlopen` (cuBLAS + embedded PTX) | Linux + NVIDIA | + +## Code Style + +- **C11** (`-std=c11`); no dependencies beyond Foundry, libc, libm, pthreads and libdl + (BLAS and accelerator frameworks are opt-in build flags) +- **`pn_` prefix** on all Pantheon symbols +- Errors: return `NULL` or an error code — no `exit()` in library code +- Opaque types in the public API (`include/pantheon.h`); concrete structs stay + internal +- Callers free returned objects via `pn_free()` +- `snprintf`, never `sprintf` +- No warnings under `-Wall -Wextra` + +Broader conventions live in `coding-best-practices.md`. + +## Testing + +Tests use Foundry's `lib/test.h` framework and live in `tests/`. Add a matching +target in the `Makefile`. + +**If you change a forward pass or a kernel, show numerical parity.** The bar is +argmax-exact against the reference path (CPU, or an existing oracle) on a real +model — not "it produces plausible text." Include how you measured it in the PR. + +## Pull Requests + +Code is reviewed against +[`docs/coding-best-practices.md`](docs/coding-best-practices.md), cited by rule +number. This section covers the PR itself. + +**One logical change per PR.** + +**Size.** Reviewable lines = added + deleted, excluding generated files, pure +renames, and vendored imports. + +| Reviewable lines | | +|---|---| +| ≤ 400 | normal | +| 400–1,000 | expect a request to split | +| > 1,000, or > 15 files | state in one line why it can't be split | + +**Exempt from the size limits — but must be alone in their own PR:** + +1. **Generated sources.** State the regeneration command, toolchain version, and + target arch. Reviewed by rebuilding, not by reading. +2. **Pure moves and renames.** No content change in the same commit. +3. **Mechanical sweeps.** Include the command that reproduces the diff. +4. **Vendored or backend imports.** + +**Evidence.** + +- **Performance claim** — the command, the device, before/after in tok/s, and the + noise floor. A multiplier on its own is not a result. +- **Forward pass or kernel** — parity against the reference path, argmax-exact or + ulp-0, naming the model and quantization. +- **A test you call green** — show it can go red (rule 8). Delete the `.o` *and* + the test binary first, or you may be running the old one. +- **Untested** — say so plainly rather than leaving it implied. + +**Keep it short.** Title is `scope: imperative summary`, lowercase scope, ≤72 +characters. The body says what changed and why in a few lines. Commit messages +the same: a subject line, and a body only when the reason isn't visible in the +diff. Long rationale belongs in a spec file, not a commit message. No tool, +assistant, or generator attribution anywhere — title, body, or comments. + +**Review comments carry a severity prefix:** + +- `blocker:` — must change before merge +- `question:` — answer before merge +- `nit:` — optional, never blocks + +Unprefixed reads as `nit:`. The author does not resolve a `blocker:` thread — the +reviewer does. Approval goes stale when new commits land, unless they are a +rebase or a fixup. + +**Merge bar:** builds clean with zero warnings, tests pass, DCO and CLA checks +green, and the code meets the practices doc. + +## Reporting Bugs + +Open an issue with: + +- what you expected, and what actually happened +- minimal reproduction steps +- the model and quantization involved (many bugs are format-specific) +- your platform, compiler version, and GPU + driver if relevant + +Bugs that only appear at a particular context depth, batch size, or concurrency +level are common here — please say which, since it is usually the whole clue. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..be3f7b2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..143274b --- /dev/null +++ b/Makefile @@ -0,0 +1,1286 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# Pantheon Makefile +# +# Builds Pantheon sources + Foundry sources into a single binary. +# +# Foundry location, in precedence order: +# 1. FOUNDRY=/path/to/foundry explicit override +# 2. ./foundry the git submodule (git clone --recursive, or +# git submodule update --init), or a symlink +# 3. ../foundry sibling checkout + +CC ?= clang +# CPU target. Default -march=native: fastest, but the binary only runs on a CPU +# with the build machine's instruction set, so build on the machine that runs it. +# PORTABLE=1 builds a binary you can copy between machines: -march=x86-64-v2 on +# x86-64 (SSE4.2, no AVX; runs on any x86-64 CPU since ~2009), the compiler's +# generic default elsewhere. +ifeq ($(PORTABLE),1) + ifneq ($(filter x86_64 amd64,$(shell uname -m)),) + ARCH_FLAGS = -march=x86-64-v2 + else + ARCH_FLAGS = + endif +else + ARCH_FLAGS = -march=native +endif +CFLAGS = -Wall -Wextra -std=c11 -O3 $(ARCH_FLAGS) -ffast-math +CFLAGS += -I$(FOUNDRY) -I. +ifneq ($(shell uname -s),Darwin) + CFLAGS += -D_GNU_SOURCE +endif + +# The serve layer (pantheon serve) is thread-per-connection, so pthreads are +# linked unconditionally. This is independent of the opt-in USE_THREADS compute +# thread pool below. +CFLAGS += -pthread +LDFLAGS += -pthread + +FOUNDRY ?= $(if $(wildcard ./foundry/tensor/tensor.h),./foundry,../foundry) + +ifeq ($(wildcard $(FOUNDRY)/tensor/tensor.h),) + $(error Foundry not found at '$(FOUNDRY)'. Run 'git submodule update --init', \ +symlink a checkout to ./foundry, clone it as ../foundry, or pass FOUNDRY=/path/to/foundry) +endif + +# Debug build +ifdef DEBUG +CFLAGS = -Wall -Wextra -std=c11 -O0 -g -DDEBUG +CFLAGS += -I$(FOUNDRY) -I. +endif + +# ============================================================ +# Build provenance: the commit this binary came from. +# +# Stamped into src/accel/backend_identity.c so the process can print it next to +# its backend at startup and through `pantheon --backend`. A tree with no git +# (a tarball, a copied checkout) reports "unknown" rather than failing the +# build — the point is that the binary never has to be reverse-engineered with +# `strings`, and "unknown" is itself an answer. +# +# Deliberately appended AFTER the DEBUG block, which REASSIGNS CFLAGS: put it +# above and a DEBUG=1 build silently loses the stamp. +# ============================================================ +PN_BUILD_COMMIT := $(shell git rev-parse --short=12 HEAD 2>/dev/null || echo unknown) +PN_BUILD_DIRTY := $(shell git diff --quiet HEAD 2>/dev/null || echo -dirty) +CFLAGS += -DPN_BUILD_COMMIT='"$(PN_BUILD_COMMIT)$(PN_BUILD_DIRTY)"' + +# pthread thread pool (opt-in: make USE_THREADS=1) +ifdef USE_THREADS + CFLAGS += -DUSE_THREADS -pthread + LDFLAGS += -pthread + THREADPOOL_SRC = $(FOUNDRY)/threadpool/threadpool.c +else + THREADPOOL_SRC = +endif + +# BLAS acceleration (opt-in: make USE_BLAS=1) +ifdef USE_BLAS + CFLAGS += -DUSE_BLAS + ifeq ($(shell uname -s),Darwin) + CFLAGS += -DACCELERATE_NEW_LAPACK + LDFLAGS_BLAS = -framework Accelerate + else + CFLAGS += -DUSE_OPENBLAS + LDFLAGS_BLAS = -lopenblas + endif +else + LDFLAGS_BLAS = +endif + +# Metal GPU backend (opt-in: make USE_METAL=1, macOS only) +ifdef USE_METAL + CFLAGS += -DUSE_METAL + LDFLAGS_METAL = -framework Metal -framework MetalPerformanceShaders -framework Foundation + # fm_metal is a device/queue/pipeline core plus one unit per kernel family + # (foundry P1.0, cfa6727). Globbed, never hand-listed: the family units are + # added one per kernel through Phase 1, and a hand-listed link line fails as + # an undefined symbol at the END of a 40-source compile. + METAL_SRC = $(FOUNDRY)/metal/fm_metal.m $(wildcard $(FOUNDRY)/metal/kernels/*.m) + OBJCFLAGS = -Wall -Wextra -O3 -ffast-math -fobjc-arc -DUSE_METAL -I$(FOUNDRY) -I. + ifdef DEBUG + OBJCFLAGS = -Wall -Wextra -O0 -g -DDEBUG -fobjc-arc -DUSE_METAL -I$(FOUNDRY) -I. + endif +else + LDFLAGS_METAL = + METAL_SRC = +endif + +# CUDA GPU backend (opt-in: make USE_CUDA=1, Linux only) +ifdef USE_CUDA + CFLAGS += -DUSE_CUDA + LDFLAGS_CUDA = -ldl + # barswap.c is pulled in too: the 2-die layer-split forward in + # qwen35moe_forward_cuda.c time-multiplexes the K80 dies through the single + # MMIO slot via bs_acquire/bs_release. On a single-die host it is harmless + # (only the 2-die path calls it, gated by a configured split layer). + CUDA_SRC = $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c +else + LDFLAGS_CUDA = + CUDA_SRC = +endif + +# Tenstorrent Blackhole backend (opt-in: make USE_TT=1, Linux only) +# +# FT_TT_PHASE1B_NOC_ENABLED comes with it and is deliberately not a second knob. +# The fence exists so a DEFAULT foundry build cannot issue a NoC transaction +# during bring-up; a Pantheon serve that opens the card is past that by +# definition, and without it ft_tt_mem_info() cannot read a bank count, so +# pn_dm_init_tt() refuses every device. Coupling the two is what stops a build +# that links the backend and then silently declines to see the card. +# +# Device kernels are NOT built here: SFPI is needed only to bake them, and the kernel ELFs +# are pre-baked into tt/ft_tt_kernels.h — the same trick cuda/ uses for PTX. +# +# ft_tt_program_blob.c + ft_tt_program_resolve.c are what make that true of whole +# PROGRAMS as well, and they are named here rather than dragged in by a foundry +# header on purpose: ft_tt_program_resolve.c is the only file that pulls the +# 2.8 MB tt/ft_tt_programs.h into a link, so a build that has not adopted +# name-based program resolution links exactly what it linked before. Pantheon +# HAS adopted it (pn_tt_device.c, pn_tt_oncard.c), which is why the binary now +# carries its own Tensix programs instead of reading them out of a home +# directory at start-up. +# +# The ft_tt_udrv_* files are foundry's OWN USERSPACE DRIVER, linked in so that +# FT_TT_DRIVER=udrv can reach the card on a machine with no vendor module. +# Linking them is not using them: the default transport is still tt-kmd and +# ft_tt_runtime_open() is the only thing that ever chooses. What linking buys is +# that ONE binary runs on a board with tt-kmd and on the board that will not +# have it, which is the whole point of Phase 12 — the alternative is a second +# build configuration nobody tests until the card has already moved. +ifdef USE_TT + # tt/convert/ sources include tt/ headers by bare name (ft_tt_op_l1_layout.h), which + # resolves in the convert tools' own build via -I../.. and resolves nowhere here. + CFLAGS += -DUSE_TT -DFT_TT_PHASE1B_NOC_ENABLED -I$(FOUNDRY)/tt + TT_SRC = $(FOUNDRY)/tt/ft_tt_kmd.c \ + $(FOUNDRY)/tt/ft_tt_arch.c \ + $(FOUNDRY)/tt/ft_tt_arch_blackhole.c \ + $(FOUNDRY)/tt/ft_tt_device.c \ + $(FOUNDRY)/tt/ft_tt_tlb.c \ + $(FOUNDRY)/tt/ft_tt_arc_telemetry.c \ + $(FOUNDRY)/tt/ft_tt_coord.c \ + $(FOUNDRY)/tt/ft_tt_noc.c \ + $(FOUNDRY)/tt/ft_tt_buffer.c \ + $(FOUNDRY)/tt/ft_tt_elf.c \ + $(FOUNDRY)/tt/ft_tt_kernel_blob.c \ + $(FOUNDRY)/tt/ft_tt_program_blob.c \ + $(FOUNDRY)/tt/ft_tt_program_resolve.c \ + $(FOUNDRY)/tt/ft_tt_launch.c \ + $(FOUNDRY)/tt/ft_tt_core_status.c \ + $(FOUNDRY)/tt/ft_tt_core_readiness.c \ + $(FOUNDRY)/tt/ft_tt_multicast.c \ + $(FOUNDRY)/tt/ft_tt_group_shared_operand.c \ + $(FOUNDRY)/tt/ft_tt_dram_stripe.c \ + $(FOUNDRY)/tt/ft_tt_op_matmul_cooperative.c \ + $(FOUNDRY)/tt/ft_tt_group_broadcast_partition.c \ + $(FOUNDRY)/tt/ft_tt_op_matmul_resident_multicast.c \ + $(FOUNDRY)/tt/ft_tt_tilize.c \ + $(FOUNDRY)/tt/ft_tt_tile_row0.c \ + $(FOUNDRY)/tt/ft_tt_watchdog.c \ + $(FOUNDRY)/tt/ft_tt_queue.c \ + $(FOUNDRY)/tt/ft_tt_group.c \ + $(FOUNDRY)/tt/ft_tt_runtime.c \ + $(FOUNDRY)/tt/ft_tt_runtime_udrv.c \ + $(FOUNDRY)/tt/ft_tt_udrv_pci_sysfs.c \ + $(FOUNDRY)/tt/ft_tt_udrv_tlb_encode.c \ + $(FOUNDRY)/tt/ft_tt_udrv_claim.c \ + $(FOUNDRY)/tt/ft_tt_udrv_noc.c \ + $(FOUNDRY)/tt/ft_tt_udrv_noc_bank.c \ + $(FOUNDRY)/tt/ft_tt_udrv_arc_msg.c \ + $(FOUNDRY)/tt/ft_tt_udrv_arc_probe.c \ + $(FOUNDRY)/tt/ft_tt_udrv_reset.c \ + $(FOUNDRY)/tt/ft_tt_fttw.c \ + $(FOUNDRY)/tt/ft_tt_op_dispatch.c \ + $(FOUNDRY)/tt/ft_tt_op_matmul.c \ + $(FOUNDRY)/tt/ft_tt_op_memory.c \ + $(FOUNDRY)/tt/ft_tt_op_rms_norm.c \ + $(FOUNDRY)/tt/ft_tt_op_rope.c \ + $(FOUNDRY)/tt/ft_tt_op_sample.c \ + $(FOUNDRY)/tt/ft_tt_op_vector.c \ + $(FOUNDRY)/tt/ft_tt_op_attention.c \ + $(FOUNDRY)/tt/ft_tt_op_gdn.c \ + $(FOUNDRY)/tt/ft_tt_op_gdnc.c \ + $(FOUNDRY)/tt/ft_tt_op_replicas.c \ + $(FOUNDRY)/tt/convert/ft_bfp.c \ + $(FOUNDRY)/tt/convert/ft_float_convert.c \ + $(FOUNDRY)/tt/convert/ft_tt_precision_ledger.c \ + $(FOUNDRY)/tt/convert/ft_gguf_map.c \ + $(FOUNDRY)/tt/convert/ft_gguf_dequant.c \ + $(FOUNDRY)/tt/convert/ft_tt_weight_convert.c \ + $(FOUNDRY)/tt/convert/ft_tt_capacity_plan.c \ + $(FOUNDRY)/tt/convert/ft_tt_artifact_provenance.c \ + $(FOUNDRY)/tt/convert/ft_tt_artifact_cache.c +else + TT_SRC = +endif + +# ============================================================ +# Foundry sources we depend on +# ============================================================ + +FOUNDRY_SRC = \ + $(FOUNDRY)/tensor/tensor.c \ + $(FOUNDRY)/safetensors/safetensors.c \ + $(FOUNDRY)/tokenizer/tokenizer.c \ + $(FOUNDRY)/quantization/quantization.c \ + $(FOUNDRY)/pth/pth.c \ + $(THREADPOOL_SRC) \ + $(METAL_SRC) \ + $(CUDA_SRC) \ + $(TT_SRC) + +# ============================================================ +# Pantheon sources +# ============================================================ + +PN_SRC = \ + src/core/engine.c \ + src/core/mem/pn_host_signals.c \ + src/core/mem/pn_memwatch.c \ + src/core/mem/pn_admission.c \ + src/core/mem/pn_kv_geometry.c \ + src/core/mem/pn_ladder.c \ + src/core/mem/pn_lazy_zero.c \ + src/core/bench.c \ + src/core/sample_profiler.c \ + src/core/qweight.c \ + src/core/sampling_modifiers.c \ + src/core/grammar_mask_shortlist.c \ + src/core/chat.c \ + src/core/bpe_pretokenize.c \ + src/core/ngram_draft.c \ + src/core/prefix_cache.c \ + src/core/session_snapshot.c \ + src/memory/arena.c \ + src/accel/simd.c \ + src/accel/die_manager.c \ + src/accel/die_layer_split.c \ + src/accel/backend_identity.c \ + src/accel/tt/pn_tt_device.c \ + src/accel/tt/pn_tt_matmul_group.c \ + src/accel/tt/pn_tt_residency.c \ + src/accel/tt/pn_tt_issue.c \ + src/accel/tt/pn_tt_core_boot_guard.c \ + src/accel/tt/pn_tt_oncard.c \ + src/accel/tt/pn_tt_eltwise_group.c \ + src/accel/tt/pn_tt_eltwise_split.c \ + src/accel/tt/pn_tt_reader.c \ + src/accel/tt/pn_tt_attention.c \ + src/accel/tt/pn_tt_gdn.c \ + src/accel/tt/pn_tt_gdn_checkpoint.c \ + src/accel/tt/pn_tt_gdnc.c \ + src/accel/tt/pn_tt_gdngate.c \ + src/accel/tt/pn_tt_stage_pipeline.c \ + src/accel/tt/pn_tt_head_norm.c \ + src/accel/tt/pn_tt_render.c \ + src/accel/tt/pn_tt_gate.c \ + src/accel/tt/pn_tt_fp32_strip.c \ + src/accel/tt/pn_tt_column_runs.c \ + src/accel/tt/pn_tt_descriptor_census.c \ + src/accel/tt/pn_tt_name_list.c \ + src/accel/tt/pn_tt_gdn_layout.c \ + src/accel/tt/pn_tt_gdn_core_scaling.c \ + src/accel/tt/pn_tt_replica_map.c \ + src/accel/tt/pn_tt_dispatch.c \ + src/formats/gguf.c \ + src/formats/gguf_dequant.c \ + src/formats/gguf_layer_bytes.c \ + src/models/llama.c \ + src/models/llama_state_snapshot.c \ + src/models/llama_attention_trace.c \ + src/models/llama_residual_trace.c \ + src/models/llama_forward_cuda.c \ + src/models/llama_forward_tt.c \ + src/models/pixtral_vision.c \ + src/models/phi3.c \ + src/models/rwkvx.c \ + src/models/gpu_attn_path_tracker.c \ + src/models/gpu_graph_capture_tracker.c \ + src/models/qwen35moe.c \ + src/models/qwen35moe_attn.c \ + src/models/qwen35moe_ssm.c \ + src/models/qwen35moe_moe.c \ + src/models/qwen35moe_forward.c \ + src/models/qwen35moe_forward_cuda.c \ + src/models/qwen35moe_forward_tt.c \ + src/models/qwen35moe_resident_tt.c \ + src/models/qwen35moe_gdn_tt.c \ + src/models/qwen35moe_gdn_strip_tt.c \ + src/models/qwen35moe_attn_tt.c \ + src/models/qwen35moe_prefill_tt.c \ + src/models/qwen35moe_prefill_checkpoint_tt.c \ + src/models/qwen35moe_checkpoint_band.c \ + src/models/qwen35moe_prefill_prefetch_tt.c \ + src/models/qwen35moe_prefill_overlap_tt.c \ + src/models/qwen35moe_prefill_tt_wall.c \ + src/models/qwen35moe_gdn_parity_tt.c \ + src/models/qwen35moe_gdn_normcheck_tt.c \ + src/models/qwen35moe_forward_metal.c \ + src/models/qwen35moe_metal_kernel_registry.c \ + src/models/qwen35moe_metal_coverage.c \ + src/models/qwen35moe_state_snapshot.c \ + src/models/bonsai_forward.c + +# OpenAI-compatible HTTP serving layer (pantheon serve). Pure C + POSIX +# sockets; no extra link deps. Compiled into the CLI and the static library. +SERVE_SRC = \ + src/serve/json_reader.c \ + src/serve/json_writer.c \ + src/serve/attn_residency_json.c \ + src/serve/graph_residency_json.c \ + src/serve/http_server.c \ + src/serve/json_value_grammar.c \ + src/serve/tool_calling.c \ + src/serve/tool_grammar.c \ + src/serve/tool_grammar_json.c \ + src/serve/grammar_spec_chooser.c \ + src/serve/tool_param_empty_value_rule.c \ + src/serve/response_format_grammar.c \ + src/serve/serve_emitter.c \ + src/serve/json_canonical_stream.c \ + src/serve/tool_call_stream.c \ + src/serve/repetition_loop_detector.c \ + src/serve/serve_prompt_tokens.c \ + src/serve/serve_spec_lookup.c \ + src/serve/serve_generation.c \ + src/serve/model_registry.c \ + src/serve/model_session.c \ + src/serve/batch_sched.c \ + src/serve/base64.c \ + src/serve/model_admin.c \ + src/serve/openai_chat.c \ + src/serve/request_profile.c \ + src/serve/signal_handlers.c \ + src/serve/serve_shutdown.c \ + src/serve/serve_backend_guard.c \ + src/serve/serve_command.c \ + src/serve/session_watch.c \ + src/serve/serve_registry.c \ + src/serve/registry.c \ + src/serve/session_affinity.c \ + src/serve/registry_client.c \ + src/serve/http_client.c \ + src/serve/router.c \ + src/serve/router_command.c + +# ============================================================ +# Header dependencies — DISCOVERED, never hand-listed +# +# Every harness/test binary below is a one-shot whole-program compile whose +# prerequisites are .c files ONLY. A changed HEADER therefore left make saying +# "is up to date" and handing back the PREVIOUS binary. +# +# For $(FOUNDRY)/cuda/fc_cuda_ptx.h — the GENERATED header carrying the +# embedded PTX for every CUDA kernel (regenerated on a CUDA host by +# cuda_kernels/Makefile) — that fails in the worst possible direction: the A/B +# reads 1.000x and the bit-exactness gate passes vacuously. Both look like an +# honest "no change" rather than a broken build. +# +# The list is produced by find, NOT written by hand, so it cannot go stale the +# way this file's link lines already have once (Pantheon 8616f92, five targets +# with stale hand-maintained link lines). It is deliberately over-approximate: +# these targets recompile every .c on every build anyway, so per-target +# precision would buy nothing. The one place objects ARE compiled separately +# (libpantheon.a) uses real -MMD -MP depfiles instead. +# +# `pantheon` and `pantheon-convert` are .PHONY and so always rebuild — that is +# deliberate and is what keeps a flag change (USE_CUDA=1 etc., which make +# cannot see) from silently reusing a binary built with different flags. +# ============================================================ +PN_HDR := $(shell find -L src include cli tests -name '*.h' 2>/dev/null) +FOUNDRY_HDR := $(shell find -L $(FOUNDRY) -name '.git' -prune -o -name '*.h' -print 2>/dev/null) +ALL_HDR := $(PN_HDR) $(FOUNDRY_HDR) + +# ============================================================ +# PER-BACKEND OUTPUT NAMES — the three builds are three programs +# +# `make`, `make USE_CUDA=1` and `make USE_TT=1` produce binaries that cannot run +# each other's work, and until this block they all wrote ONE file, `pantheon`. +# Last build won, silently. That is how a CPU-only binary came to sit at the +# path a deploy config named as its binary and serve a 35B +# MoE off a 2-core no-AVX Pentium with all 8 K80 dies idle at 0 MiB, for an +# unknown period, with measurements taken against it: a Tenstorrent session ran +# `rm -f pantheon && make USE_TT=1 pantheon` in that checkout, and a later bare +# `make` left a CPU build behind. +# +# So each backend gets its OWN name. A TT build is now physically incapable of +# overwriting the deployed CUDA binary — it writes a different file. +# +# `pantheon` survives as a SYMLINK to whichever backend was built last, because +# deploy scripts, tests/serve_*.sh and the docs all name it. The symlink keeps every +# existing invocation working AND fixes the damage case: a stray TT build now +# only re-points a link, so `ln -sfn pantheon-cuda pantheon` restores a CUDA +# deployment in one command instead of a ~10-minute CUDA rebuild. The link is not the +# safety net, though — `pantheon --backend` is (check it in a deploy preflight), +# and the serve refuses to start when --dies names dies it cannot use. +# +# Combination builds get a combination name so no two configurations can ever +# collide on one path. +# ============================================================ +PN_BACKEND_SUFFIX := +ifdef USE_CUDA + PN_BACKEND_SUFFIX := $(PN_BACKEND_SUFFIX)-cuda +endif +ifdef USE_TT + PN_BACKEND_SUFFIX := $(PN_BACKEND_SUFFIX)-tt +endif +ifdef USE_METAL + PN_BACKEND_SUFFIX := $(PN_BACKEND_SUFFIX)-metal +endif +ifeq ($(PN_BACKEND_SUFFIX),) + PN_BACKEND_SUFFIX := -cpu +endif + +PN_BIN := pantheon$(PN_BACKEND_SUFFIX) +PN_COMPAT_BIN := pantheon + +# Every per-backend name this build system can emit — `clean` has to remove all +# of them, not just the one the current flags select, or a stale binary from +# another configuration outlives the clean that was supposed to remove it. +PN_ALL_BINS := pantheon-cpu pantheon-cuda pantheon-tt pantheon-metal \ + pantheon-cuda-tt pantheon-cuda-metal pantheon-tt-metal \ + pantheon-cuda-tt-metal + +# ============================================================ +# Targets +# ============================================================ + +.PHONY: all clean test pantheon pantheon-convert $(PN_BIN) + +all: pantheon + +# CLI binary, named for its backend. PHONY (as `pantheon` always was): make +# cannot see a flag change, so a rebuild is the only thing that keeps +# USE_CUDA=1 from silently reusing a CPU object graph. +$(PN_BIN): cli/main.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) + $(CC) $(CFLAGS) -o $@ cli/main.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + @echo "built $@ (backend $(PN_BACKEND_SUFFIX:-%=%), commit $(PN_BUILD_COMMIT)$(PN_BUILD_DIRTY))" + +# Back-compat: `make pantheon` still works and still leaves something runnable +# at ./pantheon — now a symlink, so the OTHER backends' binaries survive it. +# rm-then-ln rather than `ln -sfn`: the deployed ./pantheon is a regular FILE +# until the first build after this change, and rm-then-ln replaces both a file +# and a link identically on GNU and BSD. +pantheon: $(PN_BIN) + @rm -f $(PN_COMPAT_BIN) + @ln -s $(PN_BIN) $(PN_COMPAT_BIN) + @echo "./$(PN_COMPAT_BIN) -> $(PN_BIN) (verify with: ./$(PN_COMPAT_BIN) --backend)" + +# `make pantheon-cuda` WITHOUT USE_CUDA=1 must not quietly produce a CPU binary +# wearing a CUDA name — that is precisely the confusion the rename exists to +# remove, and it would be worse than the original bug because the file name +# would now be an active lie. Every backend name that is not the one these flags +# select refuses and prints the flag it wanted. +PN_WRONG_FLAG_BINS := $(filter-out $(PN_BIN),$(PN_ALL_BINS)) +.PHONY: $(PN_WRONG_FLAG_BINS) +$(PN_WRONG_FLAG_BINS): + @echo "make: '$@' needs its backend flag — these flags build $(PN_BIN)." >&2 + @echo " cuda: make USE_CUDA=1 tt: make USE_TT=1" >&2 + @echo " metal: make USE_METAL=1 cpu: make" >&2 + @false + +# ============================================================ +# Static library for embedding in another program — same objects as the CLI +# minus cli/main.c. Build with the SAME flags you intend to link against, e.g.: +# make USE_BLAS=1 USE_THREADS=1 libpantheon.a +# then link: -lpantheon -lm +# ============================================================ +LIB_OBJDIR = build/lib +LIB_C_ONLY = $(filter %.c,$(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC)) +LIB_OBJS = $(patsubst %,$(LIB_OBJDIR)/%.o,$(LIB_C_ONLY)) +ifdef USE_METAL +LIB_OBJS += $(patsubst %,$(LIB_OBJDIR)/%.o,$(METAL_SRC)) +endif +LIB_DEPS = $(LIB_OBJS:.o=.d) + +# Real depfiles: these are the only separately-compiled objects in the build, +# so the precise mechanism applies. -MMD writes +# build/lib/.c.d beside the object; -MP adds phony targets so a DELETED +# header does not wedge the build. +$(LIB_OBJDIR)/%.c.o: %.c + @mkdir -p $(dir $@) + $(CC) $(CFLAGS) -MMD -MP -c $< -o $@ + +# Same mechanism for the Objective-C Metal units. A pattern rule, not one rule +# per file, so a new kernel family unit needs no Makefile edit. +$(LIB_OBJDIR)/%.m.o: %.m + @mkdir -p $(dir $@) + $(CC) $(OBJCFLAGS) -MMD -MP -c $< -o $@ + +-include $(LIB_DEPS) + +libpantheon.a: $(LIB_OBJS) + ar rcs $@ $(LIB_OBJS) + @echo "built $@ (public header: include/pantheon.h)" + @echo "link with: -lpantheon $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) -lm" + +# Build + run tests +test: tests/test_backend_guard tests/test_build tests/test_gguf tests/test_gguf_layer_bytes tests/test_admission_reserve tests/test_die_scratch_ctx tests/test_die_layer_split tests/test_arena tests/test_qmv tests/test_simd tests/test_simd_props tests/test_chat tests/test_tool_calling tests/test_tool_call_defects tests/test_tool_call_truncation tests/test_empty_argument tests/test_frame_capture tests/test_json_value_grammar tests/test_tool_grammar tests/test_tool_grammar_json tests/test_grammar_spec_chooser tests/test_grammar_mask_shortlist tests/test_grammar_topn_sampler tests/test_response_format_grammar tests/test_sampling_modifiers tests/test_chat_request tests/test_serve_emitter tests/test_tool_call_stream tests/test_repetition_loop_detector tests/test_serve_spec_lookup tests/test_registry tests/test_base64 tests/test_prefix_cache_sys tests/test_llama_state_snapshot tests/test_prefix_projection tests/test_session_watch tests/test_session_affinity tests/test_bs_load tests/test_bpe_pretokenize tests/test_pretok_props tests/test_dequant_props tests/test_rwkvx tests/test_qwen35moe tests/test_qwen35_prefill_causality tests/test_qwen35moe_metal_coverage tests/test_qwen35moe_metal_coverage_fake tests/test_die_manager tests/test_model_registry tests/test_model_unload_reclaim tests/test_embed_row tests/test_tt_column_runs tests/test_qwen35_ckpt_band tests/test_tt_name_list tests/test_tt_eltwise_split tests/test_tt_descriptor_census tests/test_tt_stage_pipeline tests/test_graph_capture_retry tests/test_attn_ring_retry tests/test_serve_shutdown + ./tests/test_backend_guard + ./tests/test_build + ./tests/test_gguf + ./tests/test_gguf_layer_bytes + ./tests/test_admission_reserve + ./tests/test_die_scratch_ctx + ./tests/test_die_layer_split + ./tests/test_arena + ./tests/test_qmv + ./tests/test_simd + ./tests/test_simd_props + ./tests/test_chat + ./tests/test_tool_calling + ./tests/test_tool_call_defects + ./tests/test_tool_call_truncation + ./tests/test_empty_argument + ./tests/test_frame_capture + ./tests/test_json_value_grammar + ./tests/test_tool_grammar + ./tests/test_tool_grammar_json + ./tests/test_grammar_spec_chooser + ./tests/test_grammar_mask_shortlist + ./tests/test_grammar_topn_sampler + ./tests/test_grammar_topn_sampler --selftest + ./tests/test_response_format_grammar + ./tests/test_sampling_modifiers + ./tests/test_chat_request + ./tests/test_serve_emitter + ./tests/test_tool_call_stream + ./tests/test_repetition_loop_detector + ./tests/test_registry + ./tests/test_base64 + ./tests/test_prefix_cache_sys + ./tests/test_llama_state_snapshot + ./tests/test_prefix_projection + ./tests/test_session_watch + ./tests/test_session_affinity + ./tests/test_bs_load + ./tests/test_bpe_pretokenize + ./tests/test_pretok_props + ./tests/test_dequant_props + ./tests/test_rwkvx + ./tests/test_qwen35moe + ./tests/test_qwen35_prefill_causality + ./tests/test_qwen35moe_metal_coverage + ./tests/test_qwen35moe_metal_coverage_fake + ./tests/test_die_manager + ./tests/test_model_registry + ./tests/test_model_unload_reclaim + ./tests/test_embed_row + ./tests/test_tt_column_runs + ./tests/test_qwen35_ckpt_band + ./tests/test_tt_eltwise_split + ./tests/test_tt_descriptor_census + ./tests/test_graph_capture_retry + ./tests/test_graph_capture_retry --latch + ./tests/test_attn_ring_retry + ./tests/test_attn_ring_retry --latch + ./tests/test_serve_shutdown + +tests/test_build: tests/test_build.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_build.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_gguf: tests/test_gguf.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_gguf.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_gguf_layer_bytes: tests/test_gguf_layer_bytes.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_gguf_layer_bytes.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_admission_reserve: tests/test_admission_reserve.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_admission_reserve.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_die_layer_split: tests/test_die_layer_split.c src/accel/die_layer_split.c src/accel/die_layer_split.h + $(CC) $(CFLAGS) -o $@ tests/test_die_layer_split.c src/accel/die_layer_split.c -lm + +tests/test_die_scratch_ctx: tests/test_die_scratch_ctx.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_die_scratch_ctx.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_arena: tests/test_arena.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_arena.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_qmv: tests/test_qmv.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_qmv.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_simd: tests/test_simd.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_simd.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Clean-room property tests for the CPU vector kernels (spec-derived, flag-independent). +tests/test_simd_props: tests/test_simd_props.c src/accel/simd.c src/accel/simd.h \ + src/formats/gguf_dequant.c src/formats/gguf.c src/formats/gguf_dequant.h src/formats/gguf.h + $(CC) $(CFLAGS) -o $@ tests/test_simd_props.c src/accel/simd.c \ + src/formats/gguf_dequant.c src/formats/gguf.c -lm + +tests/test_chat: tests/test_chat.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_chat.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# FRAME CAPTURE: a forced-close turn whose compelled XML frame swallowed the +# call the model was still writing. Synthetic fixtures in the observed failure +# shapes (tests/frame_capture_fixtures.h). Same deps as the parser +# suite below — pure C, no engine/model/CUDA. +tests/test_frame_capture: tests/test_frame_capture.c tests/frame_capture_fixtures.h src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_frame_capture.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# tc_parse_tool_calls (+ render) is pure C string parsing over the JSON DOM; +# links only the three serve string modules, no engine/model/CUDA deps. +tests/test_tool_calling: tests/test_tool_calling.c tests/abandoned_call_fixture.h src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tool_calling.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# Structurally-wrong tool-call shapes (leaked dialect tail / reasoning leak / +# missing + unknown name) and the truncated-JSON argument repair — the two +# serve-side items from the Qwen harness audit. Same deps as the parser suite +# above: pure C, no engine/model/CUDA. +tests/test_tool_call_defects: tests/test_tool_call_defects.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tool_call_defects.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# Truncated tool calls: the grammar and the parser must agree about where a value +# ended, and a call the grammar never closed must not be handed back as a call. +# The only test that links BOTH halves — the defect lives in the gap between +# them. Still pure C: no engine/model/CUDA. +tests/test_tool_call_truncation: tests/test_tool_call_truncation.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/tool_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tool_call_truncation.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/tool_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# EMPTY IS NOT ABSENT: a parameter the model CLOSED around an empty value is +# emitted as "" rather than dropped, so the client can tell "the model meant +# empty" (edit's new_string:"" is a deletion) from "the argument never arrived" +# (a truncated call, which the client charges against a task-ending budget). +# Pure C string parsing over the JSON DOM — no engine/model/CUDA. +tests/test_empty_argument: tests/test_empty_argument.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_empty_argument.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# Incremental JSON-value well-formedness walk — the shared core that makes a +# container-typed tool argument unfailable (grammar side) and its raw splice +# safe (parser side). Pure C, libc-only: no engine/model/CUDA, fully local. +tests/test_json_value_grammar: tests/test_json_value_grammar.c src/serve/json_value_grammar.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_json_value_grammar.c src/serve/json_value_grammar.c \ + -lm $(LDFLAGS) + +# Grammar-constrained tool decoding FSM (Phase 5). Pure C string/DOM parsing +# over the tools schema; links only the grammar module + the JSON reader — no +# engine/model/CUDA deps, fully local. +tests/test_tool_grammar: tests/test_tool_grammar.c src/serve/tool_grammar.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tool_grammar.c src/serve/tool_grammar.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +tests/test_tool_grammar_json: tests/test_tool_grammar_json.c src/serve/tool_grammar_json.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tool_grammar_json.c src/serve/tool_grammar_json.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# Speculative decoding under the tool-call grammar: drafts verified through the +# grammar-aware chooser must reproduce plain constrained decoding token for +# token (self-falsifying simulation). Links the real JSON grammar and the +# parser; no engine/model/device. +tests/test_grammar_spec_chooser: tests/test_grammar_spec_chooser.c src/serve/grammar_spec_chooser.c src/core/grammar_mask_shortlist.c src/serve/tool_grammar_json.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_grammar_spec_chooser.c src/serve/grammar_spec_chooser.c src/core/grammar_mask_shortlist.c src/serve/tool_grammar_json.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# The grammar mask decided from a shortlist of the highest logits must leave the +# same greedy pick / top-K set as the full-vocabulary mask. libc-only. +tests/test_grammar_mask_shortlist: tests/test_grammar_mask_shortlist.c src/core/grammar_mask_shortlist.c src/core/sampling_modifiers.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_grammar_mask_shortlist.c src/core/grammar_mask_shortlist.c src/core/sampling_modifiers.c \ + -lm $(LDFLAGS) + +# Full-logits ARMING WINDOW (PN_GRAMMAR_ARM_WINDOW): is the row a masked step +# reads armed one token EARLY enough? Models the decode loop's ordering over +# the three real grammar walks; self-falsifying — exits 2 VACUOUS if the +# deliberately-stale policy fails to break. No engine/model/CUDA deps. +tests/test_grammar_arm_window: tests/test_grammar_arm_window.c src/serve/tool_grammar.c src/serve/json_value_grammar.c src/serve/tool_grammar_json.c src/serve/response_format_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_grammar_arm_window.c src/serve/tool_grammar.c src/serve/json_value_grammar.c src/serve/tool_grammar_json.c src/serve/response_format_grammar.c src/serve/tool_param_empty_value_rule.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# response_format (json_object) constrained-decoding FSM. Pure C char walk with +# no schema/DOM input at all — links only the grammar module. No engine/model/ +# CUDA deps, fully local. +tests/test_response_format_grammar: tests/test_response_format_grammar.c src/serve/response_format_grammar.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_response_format_grammar.c src/serve/response_format_grammar.c \ + -lm $(LDFLAGS) + +# OpenAI logits-row sampling controls (logit_bias / presence+frequency penalties / +# seeded RNG / logprobs). Pure float-array math — links only the modifier module. +tests/test_sampling_modifiers: tests/test_sampling_modifiers.c src/core/sampling_modifiers.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_sampling_modifiers.c src/core/sampling_modifiers.c \ + -lm $(LDFLAGS) + +# parse_chat_request tool_choice handling + forced_name lifetime (Phase 5 BUG 2). +# #includes openai_chat.c directly to reach its static parse fn; the request-parse +# path only touches the JSON reader + tool_calling renderer (linked), and tiny +# link stubs in the test satisfy the engine-side handlers it never calls. No +# engine/model/CUDA deps, fully local. serve_shutdown.c is linked because the +# parse's DEFAULT abort predicate is the shutdown check — every turn has to be +# stoppable by a SIGTERM — and that module is libc + pthread only. +# src/accel/backend_identity.c joins the link line because oai_handle_models now +# names the compiled backend on /v1/models. The module is a handful of string +# literals with no dependencies of its own, so it costs this harness nothing. +tests/test_chat_request: tests/test_chat_request.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c src/serve/serve_shutdown.c src/serve/openai_chat.c src/serve/tool_call_stream.c src/serve/json_canonical_stream.c src/serve/serve_emitter.c src/accel/backend_identity.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_chat_request.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c src/serve/request_profile.c src/serve/serve_shutdown.c src/serve/tool_call_stream.c src/serve/json_canonical_stream.c src/serve/serve_emitter.c src/accel/backend_identity.c \ + -lm $(LDFLAGS) + +# Stream tool-call detection (serve_generation.c emitter, Phase 2). The emitter +# lives in serve_emitter.c and depends only on libc, so this links just that one +# module — no engine/model/CUDA deps. +# Where the multi-turn prefill checkpoint sits (a band boundary, at or below the history the +# next turn resends). Pure arithmetic, host-only — no card. +tests/test_qwen35_ckpt_band: tests/test_qwen35_ckpt_band.c src/models/qwen35moe_checkpoint_band.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_qwen35_ckpt_band.c src/models/qwen35moe_checkpoint_band.c \ + -lm $(LDFLAGS) + +tests/test_serve_emitter: tests/test_serve_emitter.c src/serve/serve_emitter.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_serve_emitter.c src/serve/serve_emitter.c \ + -lm $(LDFLAGS) + +# Incremental tool-call streaming (tool_call_stream.c + json_canonical_stream.c): +# the streamed argument fragments must concatenate to exactly the arguments the +# end-of-turn parse returns. libc-only — the emitter, the streamer and the +# parser, no engine. +tests/test_tool_call_stream: tests/test_tool_call_stream.c src/serve/tool_call_stream.c src/serve/json_canonical_stream.c src/serve/serve_emitter.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tool_call_stream.c src/serve/tool_call_stream.c src/serve/json_canonical_stream.c src/serve/serve_emitter.c src/serve/tool_calling.c src/serve/json_value_grammar.c src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# Decode-time repetition-cycle detector (the saber-orch Q3_K_M collapse: one +# paragraph re-emitted 31 times, reported as a normal finish). The detector +# depends only on libc, so this links just that one module — no engine/model/ +# CUDA. The NEGATIVE tests (legitimate code, JSON, tables, log excerpts that +# must NOT trip it) are the ones that matter; run the red proof by relaxing +# RLD_DEFAULT_MIN_PERIOD to 1 and watching them go red. +tests/test_serve_spec_lookup: tests/test_serve_spec_lookup.c src/serve/serve_spec_lookup.c src/core/ngram_draft.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_serve_spec_lookup.c src/serve/serve_spec_lookup.c \ + src/core/ngram_draft.c -lm $(LDFLAGS) + +tests/test_repetition_loop_detector: tests/test_repetition_loop_detector.c src/serve/repetition_loop_detector.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_repetition_loop_detector.c \ + src/serve/repetition_loop_detector.c -lm $(LDFLAGS) + +# Dispatch-layer registry unit test (Phase G1). Links only the registry + the +# two JSON modules — no engine/model/CUDA. Host-side, deterministic (injected +# clock for the TTL test). +tests/test_registry: tests/test_registry.c src/serve/registry.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_registry.c src/serve/registry.c \ + src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# System-prefix cache unit test (SYS1). Links the prefix cache + the state +# snapshot module only; the test stubs pn_state_new/reset/free over hand-built +# qwen35moe dual states — no engine/model/CUDA, builds and runs on the Mac. +tests/test_base64: tests/test_base64.c src/serve/base64.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_base64.c src/serve/base64.c \ + -lm $(LDFLAGS) + +# Retry policy + three-state residency gauge for the CUDA-graph capture tracker +# (tasks #41/#42). Pure host state machine, no GPU, runs anywhere. Run the red +# arm too — `tests/test_graph_capture_retry --latch` replays the OLD one-shot +# latch and must fail its expectations, which is what proves the gate is not +# vacuous. +tests/test_graph_capture_retry: tests/test_graph_capture_retry.c \ + src/models/gpu_graph_capture_tracker.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_graph_capture_retry.c \ + src/models/gpu_graph_capture_tracker.c -lm $(LDFLAGS) + +# Deferred-shutdown mechanism for `pantheon serve` (SIGTERM/SIGINT retiring the +# card residencies). Links serve_shutdown.c alone — no engine, no serve, no +# card — and raises REAL signals through a real sigaction to prove the handler +# path. The retirement it gates is device-side and is proved on silicon. +tests/test_serve_shutdown: tests/test_serve_shutdown.c \ + src/serve/serve_shutdown.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_serve_shutdown.c \ + src/serve/serve_shutdown.c -lm $(LDFLAGS) + +# Bounded retry after a failed per-slot KV-ring allocation + the ondie/host +# attention residency gauge. Pure host state machine, no GPU, runs anywhere. +# Run the red arm too — `tests/test_attn_ring_retry --latch` replays the OLD +# sticky latch (one attempt per geometry, then never again) and must fail its +# expectations, which is what proves the gate is not vacuous. +tests/test_attn_ring_retry: tests/test_attn_ring_retry.c \ + src/models/gpu_attn_path_tracker.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_attn_ring_retry.c \ + src/models/gpu_attn_path_tracker.c -lm $(LDFLAGS) + +# Llama-family session snapshot (the primitive session export/restore rides on +# for every arch whose per-session state is a plain causal KV cache — the family +# the Tenstorrent backend serves). Pure host memory, no engine, no accelerator: +# the test hand-builds the tm_tensors so it links only the snapshot unit and the +# arch dispatcher. Byte comparison throughout; PCC would be blind to every +# failure this format can have. +tests/test_llama_state_snapshot: tests/test_llama_state_snapshot.c src/models/llama_state_snapshot.c src/models/qwen35moe_state_snapshot.c src/core/session_snapshot.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_llama_state_snapshot.c \ + src/models/llama_state_snapshot.c src/models/qwen35moe_state_snapshot.c \ + src/core/session_snapshot.c \ + -lm $(LDFLAGS) + +tests/test_prefix_cache_sys: tests/test_prefix_cache_sys.c tests/tt_prefix_cache_link_stubs.c src/core/prefix_cache.c src/core/session_snapshot.c src/models/qwen35moe_state_snapshot.c src/models/llama_state_snapshot.c src/serve/batch_sched.c src/serve/request_profile.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_prefix_cache_sys.c tests/tt_prefix_cache_link_stubs.c \ + src/core/prefix_cache.c src/core/session_snapshot.c \ + src/models/qwen35moe_state_snapshot.c src/models/llama_state_snapshot.c \ + src/serve/batch_sched.c src/serve/request_profile.c \ + -lm $(LDFLAGS) + +# Liveness-based session release (§C.4). Links the watch manager + prefix cache +# + snapshot machinery + http_server (hs_client_alive); mr_acquire/mr_release + +# pn_state_* are test stubs. Host-side, deterministic (manual manager + sweep +# hooks), no engine/model/CUDA. +tests/test_session_watch: tests/test_session_watch.c tests/tt_prefix_cache_link_stubs.c src/serve/session_watch.c src/core/prefix_cache.c src/core/session_snapshot.c src/models/qwen35moe_state_snapshot.c src/models/llama_state_snapshot.c src/serve/http_server.c src/serve/model_session.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_session_watch.c tests/tt_prefix_cache_link_stubs.c \ + src/serve/session_watch.c src/core/prefix_cache.c src/core/session_snapshot.c \ + src/models/qwen35moe_state_snapshot.c src/models/llama_state_snapshot.c \ + src/serve/http_server.c \ + src/serve/model_session.c src/serve/batch_sched.c src/serve/request_profile.c \ + -lm $(LDFLAGS) + +# Session-affinity unit test (PC0.2). Links the affinity map + registry + the two +# JSON modules — no engine/model/CUDA. Host-side, deterministic. +tests/test_session_affinity: tests/test_session_affinity.c src/serve/session_affinity.c src/serve/registry.c src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_session_affinity.c \ + src/serve/session_affinity.c src/serve/registry.c \ + src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) + +# Dispatch-layer end-to-end test (Phase G1+G2): boots the real `pantheon router` +# + Python stub upstreams and asserts dedup / least-busy / failover / SSE +# pass-through / TTL eviction. Mac-only (host networking; no GPU/CUDA). +# Requires the built `pantheon` binary + python3 + curl. Runs ~20s (TTL wait). +.PHONY: test-dispatch-e2e +test-dispatch-e2e: pantheon + bash tests/dispatch/e2e.sh + +# Shared by every parity harness: the paired PPL/KL/gap scorer, the accelerator-arm +# selection that refuses a vacuous CPU-vs-CPU run, and the reproducible Gaussian +# perturbations. One copy so a gate and a debug sweep can never disagree about what a +# perplexity ratio means. +PARITY_LIB_SRC = tests/parity_score.c tests/parity_accel_arm.c tests/parity_noise.c \ + tests/parity_gate_criteria.c tests/parity_logit_dump.c +PARITY_LIB_HDR = tests/parity_score.h tests/parity_accel_arm.h tests/parity_noise.h \ + tests/parity_gate_criteria.h tests/parity_logit_dump.h + +# The chat-templated, real-vocab, per-position accelerator parity GATE. Pass/fail on the +# perplexity a chat-framed prompt actually costs, not on one next-token argmax. +tests/test_chat_parity_gate: tests/test_chat_parity_gate.c tests/chat_corpus.c $(PARITY_LIB_SRC) $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) $(PARITY_LIB_HDR) tests/chat_corpus.h + $(CC) $(CFLAGS) -o $@ tests/test_chat_parity_gate.c tests/chat_corpus.c $(PARITY_LIB_SRC) $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# THE SAME GATE AS TWO SEPARATE PASSES, paired offline. The interleaved gate above puts a +# ~26 s CPU-oracle forward between two consecutive card forwards, which retires the resident +# triads: 802 / 2406 / 442 descriptor refusals against 0 on single-arm runs. This one dumps +# each arm's per-position logit rows on its own and scores them afterwards, which also makes +# the oracle pass CACHEABLE across arms (~3 h once, not per arm). The pairing step loads no +# model and opens no card, so it builds and runs with no accelerator flag at all. +tests/test_two_pass_parity_gate: tests/test_two_pass_parity_gate.c tests/chat_corpus.c $(PARITY_LIB_SRC) $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) $(PARITY_LIB_HDR) tests/chat_corpus.h + $(CC) $(CFLAGS) -o $@ tests/test_two_pass_parity_gate.c tests/chat_corpus.c $(PARITY_LIB_SRC) $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# L11b: the gdnc reader's on-card decay tile + gate runs vs the hardware multiply (FTZ+DAZ) and +# the host builders, bit for bit. Run it on an x86 host (where the builders run). +tests/test_gdnc_compact: tests/test_gdnc_compact.c $(FOUNDRY)/tt/kernels/ftgc_compact.h $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_gdnc_compact.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# PHASE B's CHEAP GATE: the prompt-lookup drafting ACCEPTANCE rate, two passes. --generate +# decodes a corpus greedily on the shipped arm and records the token ids (needs a card); +# --score replays those ids as speculative rounds (no model, no card, seconds); --selftest is +# a known-answer arm at both ends of the scale. Greedy verification accepts exactly the +# drafts that match the plain stream, so NO verify kernel is needed to measure this. +tests/bench_ngram_accept: tests/bench_ngram_accept.c tests/ngram_accept_stream.c tests/chat_corpus.c $(PARITY_LIB_SRC) $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) $(PARITY_LIB_HDR) tests/chat_corpus.h tests/ngram_accept_stream.h + $(CC) $(CFLAGS) -o $@ tests/bench_ngram_accept.c tests/ngram_accept_stream.c tests/chat_corpus.c $(PARITY_LIB_SRC) $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# The GDN host STAGER's address arithmetic, against the layout accessors the two dataflow +# kernels compute their own offsets with. Needs USE_TT=1 for the foundry tile code, but NOT a +# card: it opens no device, so it runs on the Mac where the stager is written. +tests/test_gdn_layout: tests/test_gdn_layout.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_gdn_layout.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# BYTE-EXACTNESS of the batched TT prefill against the one-at-a-time path. Byte comparison +# and not PCC: what makes a 32-row pass safe is row INDEPENDENCE, and a correlation cannot +# see independence. Needs USE_TT=1 and a card; without one it refuses rather than passing. +# THE BOOT GUARD AGAINST A REAL LEFTOVER. Two invocations, one card, NO MODEL — +# --leak strands a spinning compute thread and exits without closing the device, +# --read reports what the card says about that core writing nothing, and --arm is +# the process that used to wedge it. Needs USE_TT=1 and a card. It is a CARD ARM +# and belongs behind the flock runner (tt/tests/run_gdn_writer_capacity_arms.sh's +# pattern), but it is not a model load and must never become one. +tests/test_tt_boot_guard_leftover: tests/test_tt_boot_guard_leftover.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tt_boot_guard_leftover.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_tt_prefill_parity: tests/test_tt_prefill_parity.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_tt_prefill_parity.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# THE THREE MEASUREMENTS PHASE 4 IS GATED ON, on Ornith's own strips: what a whole-tile +# transfer costs against the row-0 one (there is no on-card row gather, so a batched pass +# reaches row r through the link or not at all), whether the 32-row tile quantum survives 32 +# LIVE rows rather than 31 zeros, and what the two mixers cost alone — they are the terms a +# batched pass cannot batch. Needs USE_TT=1 and a card. +tests/bench_tt_prefill_quantum: tests/bench_tt_prefill_quantum.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_tt_prefill_quantum.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Speculative greedy decode gate + bench (n-gram drafts + batched verify): +# the speculative token stream must be identical to plain greedy. Build with +# USE_CUDA=1 on a CUDA host; run under QWEN35_FUSE=0x37 for the strict gate. +tests/test_spec_qwen35: tests/test_spec_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_spec_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Multi-user decode gate + bench (pn_qwen35moe_decode_slots): K users, one +# token each per forward pass — each user's batched stream must be identical +# to that user decoding alone. Build with USE_CUDA=1 on a CUDA host; run under +# QWEN35_FUSE=0x37 for the strict gate. +tests/test_users_qwen35: tests/test_users_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_users_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# What temperature costs the HOST per token: times the real sampler +# (pn_sample_next_token) at temp 0 vs temp 0.7 over the sparse device-top-k +# logits buffer the serve actually hands it. The delta bounds every "move the +# sampling off the host" lever. CPU-only — no GPU, no model, runs anywhere. +tests/bench_sampler_temp_delta: tests/bench_sampler_temp_delta.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_sampler_temp_delta.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Ship gate for the sparse-survivor sampler: the fast path must return the +# SAME token as the full-vocab path for the same row, params and RNG state. +# `--selftest` proves the gate can go red (and that the fast path actually +# ran) — a green run from a gate that cannot fail means nothing. +tests/test_sparse_sampler_identity: tests/test_sparse_sampler_identity.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_sparse_sampler_identity.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# The grammar mask on a backend's top-N row (shortlist / dense chunks / whole-row +# refill) must draw the token the full mask draws on the dense row. +# `--selftest`: a subject that skips the mask must be caught. +tests/test_grammar_topn_sampler: tests/test_grammar_topn_sampler.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_grammar_topn_sampler.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Gate for the top-N exactness check the sampler uses before sampling a top-N row +# (`--selftest`: a check blind to the penalties must be caught). +tests/test_topn_row_exact: tests/test_topn_row_exact.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_topn_row_exact.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Context-DEPTH decode bench: per-user steady-state DECODE tok/s as the seeded +# context deepens (isolates the 10 O(context) attention layers). Cold-prefills a +# large seed per user (untimed), then times only the batched decode. Build with +# USE_CUDA=1 on a CUDA host; run under QWEN35_FUSE=0x37. +tests/bench_depth_qwen35: tests/bench_depth_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_depth_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Session-lifetime regression gate: bind a decode slot immediately after a +# session free. Asserts no decode slot still holds a back-pointer into the +# freed state (address compare only), then performs the real bind-after-free — +# the sequence the serve runs on every session release and idle reap. Build +# with DEBUG=1 USE_CUDA=1 on a CUDA host so the ASAN arm runs too. +tests/test_slot_free_bind_qwen35: tests/test_slot_free_bind_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_slot_free_bind_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Phase 9.1 speculative-decoding GATE bench: cost of a D-row batched verify vs +# D sequential single-token forwards, plus the batched-slots row-cost curve. +# Pure measurement, changes no engine behaviour. Build with USE_CUDA=1 on a +# CUDA host; run under QWEN35_FUSE=0x37 to match the serve's kernel selection. +tests/bench_spec_verify_gate: tests/bench_spec_verify_gate.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_spec_verify_gate.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Phase 9.2 ACCEPTANCE bench: what fraction of prompt-lookup drafts the verify +# actually accepts, on real code-edit prompts, counted separately for the +# reasoning and answer segments — the measurement 9.1's break-even has to be +# tested against. Re-runs the exactness gate too (spec stream == plain greedy). +# Build with USE_CUDA=1 on a CUDA host; run under QWEN35_FUSE=0x37. +tests/bench_spec_accept: tests/bench_spec_accept.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_spec_accept.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Serve-scheduler gate (L1/L2/L3 — batched+chunked prefill, in-driver +# sampling): concurrent sg_run_completion_batched users vs the single-user +# reference (greedy text must be identical), join-during-decode, slow-client +# isolation. Build with USE_CUDA=1 on a CUDA host; run under QWEN35_FUSE=0x37. +tests/test_serve_sched_qwen35: tests/test_serve_sched_qwen35.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_serve_sched_qwen35.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# PC-batched gate: prefix caching wired into the MULTI-TENANT batched path. +# 4 concurrent users x 3 growing turns through the batch scheduler with +# session affinity + delta-prefill; every user's per-turn reply must be +# token-identical (argmax-exact) to a non-cached single-user reference. Build +# with USE_CUDA=1 on a CUDA host; run under QWEN35_BATCH_SLOTS=1 QWEN35_FUSE=0x37. +tests/test_serve_sched_prefix_cache: tests/test_serve_sched_prefix_cache.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_serve_sched_prefix_cache.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# True per-user capacity bench — realistic growing-context workload with prefix +# caching ON (steady-state delta-prefill), K concurrent users through the +# batched serve path. Reports per-user + aggregate tok/s + cost/1M. Build with +# USE_CUDA=1 on a CUDA host; run under QWEN35_BATCH_SLOTS=1 QWEN35_FUSE=0x37. +tests/bench_serve_prefix_cache: tests/bench_serve_prefix_cache.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_serve_prefix_cache.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Prefix-cache gate + latency bench (pn_prefix_cache): a multi-turn agentic +# scenario run with caching ON (delta-prefill only) vs OFF (re-prefill the +# whole context each turn) must produce token-identical replies (argmax-exact), +# and the cached per-turn prefill must be far cheaper. Build with USE_CUDA=1 on +# a CUDA host; run under QWEN35_FUSE=0x37 for the strict argmax gate. +tests/test_prefix_cache_qwen35: tests/test_prefix_cache_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_prefix_cache_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Quality-eval driver (perplexity + greedy generation) for A/B-ing two engine +# builds whose logits are not bit-identical; driven by evals/eval_quality.sh. +# Build with USE_CUDA=1 on a CUDA host for the GPU path. Needs a model. +tests/eval_ppl: tests/eval_ppl.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/eval_ppl.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_prefix_projection: tests/test_prefix_projection.c $(PN_SRC) $(FOUNDRY_SRC) \ + src/serve/serve_prompt_tokens.c src/serve/tool_calling.c \ + src/serve/json_value_grammar.c \ + src/serve/json_reader.c src/serve/json_writer.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_prefix_projection.c $(PN_SRC) $(FOUNDRY_SRC) \ + src/serve/serve_prompt_tokens.c src/serve/tool_calling.c \ + src/serve/json_value_grammar.c \ + src/serve/json_reader.c src/serve/json_writer.c \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_bpe_pretokenize: tests/test_bpe_pretokenize.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_bpe_pretokenize.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Clean-room property tests: pre-token split oracle over every code point, and +# bit-exact dequant/half-conversion checks against per-element references. +tests/test_pretok_props: tests/test_pretok_props.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_pretok_props.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_dequant_props: tests/test_dequant_props.c src/formats/gguf_dequant.c src/formats/gguf.c \ + src/formats/gguf_dequant.h src/formats/gguf.h + $(CC) $(CFLAGS) -o $@ tests/test_dequant_props.c src/formats/gguf_dequant.c src/formats/gguf.c -lm + +tests/test_rwkvx: tests/test_rwkvx.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_rwkvx.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_qwen35moe: tests/test_qwen35moe.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_qwen35moe.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Batched-prefill causality (reference-free) on every qwen35 batched path the build has: +# CUDA forward_batch + 9b/9c, TT prefill. SKIPs without PN_QWEN35_MODEL or such a path. +# Red proof: tests/causality_red/run_red.sh. +tests/test_qwen35_prefill_causality: tests/test_qwen35_prefill_causality.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_qwen35_prefill_causality.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Metal kernel-coverage gate: every weight the forward DISPATCHES must have a +# Metal matvec kernel registered for its quant type, or the stock mixed-quant +# GGUF turns into garbage logits at Phase-5 bring-up. Built TWICE from one +# source: the `_fake` binary links a stub fm_matvec_q5k, so Q5_K must flip to +# covered while the other missing types do not. A gate that printed a constant +# could not tell the two binaries apart — that difference IS the proof it +# measures registration. Both are model-gated on PN_QWEN35_MODEL. +tests/test_qwen35moe_metal_coverage: tests/test_qwen35moe_metal_coverage.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_qwen35moe_metal_coverage.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +tests/test_qwen35moe_metal_coverage_fake: tests/test_qwen35moe_metal_coverage.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -DPN_COVERAGE_FAKE_KERNEL -o $@ \ + tests/test_qwen35moe_metal_coverage.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Losslessness gate for greedy speculative decoding: proves the speculative +# stream is bit-identical to plain greedy decode for any draft. Needs a model. +tests/test_spec_decode: tests/test_spec_decode.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_spec_decode.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# End-to-end losslessness + realized-speedup measurement for the speculative +# generation loop (pn_spec_generate). Needs a model. +tests/test_spec_generate: tests/test_spec_generate.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_spec_generate.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# M1 bench: verify-pass cost vs number of rows M (picks the default draft width +# and confirms the batched-verify payoff). Needs a model. +tests/bench_verify_rows: tests/bench_verify_rows.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/bench_verify_rows.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# M2/M3 gate: pn_llama_verify_batch ragged multi-sequence verify is bit-identical +# (memcmp) to an independent per-slot forward_batch replay. Needs a model. +tests/test_verify_batch: tests/test_verify_batch.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_verify_batch.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# M4/M5 gate: batched speculative decode (pn_batch_decoder_step_spec) emits a +# token stream bit-identical to plain forward and plain batch decode for any +# draft, across batch sizes, K widths, mixed spec/plain slots, and EOS. Needs a model. +tests/test_spec_batch: tests/test_spec_batch.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_spec_batch.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# MemoryAware Phase 1 unit test (host signals + memwatch publish/read + level +# derivation + reserve policy). Self-contained: links only the mem module + the +# host-signal reader + libm, so it builds and runs on the Mac (no model/GPU). +MEM_SRC = src/core/mem/pn_host_signals.c src/core/mem/pn_memwatch.c \ + src/core/mem/pn_admission.c src/core/mem/pn_ladder.c \ + src/serve/signal_handlers.c +tests/test_memwatch: tests/test_memwatch.c $(MEM_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_memwatch.c $(MEM_SRC) $(CUDA_SRC) -lm $(LDFLAGS) $(LDFLAGS_CUDA) + +# Die-manager unit test (Phase 7 placement primitive). Self-contained: logical +# dies only — links just the die-manager module, no model/GPU, so it builds +# and runs on the Mac. The pn_dm_init_cuda probe is validated on the K80 test machine. +tests/test_die_manager: tests/test_die_manager.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_die_manager.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Model-registry unit test (Phase 7 multi-model serving). Bookkeeping asserts +# run everywhere; the model-backed part (two names resident, generation, +# unload-drain) uses models/qwen3-0.6b-Q8_0.gguf and skips if it is absent +# (override with PN_TEST_MODEL). +tests/test_model_registry: tests/test_model_registry.c src/serve/model_registry.c src/serve/batch_sched.c src/serve/request_profile.c src/serve/serve_backend_guard.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_model_registry.c src/serve/model_registry.c src/serve/batch_sched.c src/serve/request_profile.c src/serve/serve_backend_guard.c \ + $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Unload-reclaim regression (the /admin/models/unload die leak). Proves the die +# budget comes BACK — a DIFFERENT model loads onto the SAME die in the SAME +# process, on a die sized to fit exactly one — and that an unload which cannot +# drain REFUSES instead of reporting success and stranding the dies. Same link +# set as test_model_registry; logical dies only, so it runs on the Mac. +tests/test_model_unload_reclaim: tests/test_model_unload_reclaim.c src/serve/model_registry.c src/serve/batch_sched.c src/serve/request_profile.c src/serve/serve_backend_guard.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_model_unload_reclaim.c src/serve/model_registry.c src/serve/batch_sched.c src/serve/request_profile.c src/serve/serve_backend_guard.c \ + $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# The silent-deploy class: compiled-backend identity, the refuse-to-start guard +# for a `--dies` this binary cannot honour, and the SERVED-vs-configured +# concurrency the two JSON surfaces advertise. Links the whole serve + engine +# (like test_serve_sched_qwen35) so the JSON assertions run the REAL +# oai_handle_models / ma_handle_models over a socketpair rather than a +# reimplementation of them. No GPU, no model file, no network. +tests/test_backend_guard: tests/test_backend_guard.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_backend_guard.c $(PN_SRC) $(SERVE_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# Dispatch-layer bs_get_load() accessor test (Phase G0). Exercises the pure +# slot/queue snapshot (sched==NULL single-user + hand-built cap=4 states); +# links batch_sched.c + the engine (batch_sched.c's other fns need it), no GPU. +tests/test_bs_load: tests/test_bs_load.c src/serve/batch_sched.c src/serve/request_profile.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_bs_load.c src/serve/batch_sched.c src/serve/request_profile.c \ + $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# A4: unit tests for pn_gguf_dequantize_row (per-row byte-offset math + +# per-row == slice-of-full-tensor) and pn_qwen35moe_model_embed_row. +# Part A always runs (synthetic data); Part B needs PN_TEST_MODEL (a real GGUF). +tests/test_embed_row: tests/test_embed_row.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_embed_row.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +# The column-tile permutation that de-interleaves a joint Q+gate projection. Links ONLY the +# module under test: it has no device, no artifact and no USE_TT, which is the point — the +# de-interleave is the one Phase 5 item with no proven shape to inherit, so its arithmetic +# is settled with no card and on any host. +tests/test_tt_column_runs: tests/test_tt_column_runs.c src/accel/tt/pn_tt_column_runs.c \ + src/accel/tt/pn_tt_column_runs.h + $(CC) $(CFLAGS) -o $@ tests/test_tt_column_runs.c src/accel/tt/pn_tt_column_runs.c -lm + +# The elementwise core split's tile arithmetic. Links ONLY the module under test — no device, +# no artifact, no USE_TT — because the one thing a split can get wrong that nothing reports is +# an ADDRESS, and that is settled here rather than on a card. +tests/test_tt_name_list: tests/test_tt_name_list.c \ + src/accel/tt/pn_tt_name_list.c src/accel/tt/pn_tt_name_list.h + $(CC) $(CFLAGS) -o $@ tests/test_tt_name_list.c \ + src/accel/tt/pn_tt_name_list.c -lm + +# The staging pipeline's release contract, off hardware: a released item is always built. +# Build it with -DPN_TT_STAGE_PIPELINE_RED to see it fail. +tests/test_tt_stage_pipeline: tests/test_tt_stage_pipeline.c \ + src/accel/tt/pn_tt_stage_pipeline.c src/accel/tt/pn_tt_stage_pipeline.h + $(CC) $(CFLAGS) -o $@ tests/test_tt_stage_pipeline.c src/accel/tt/pn_tt_stage_pipeline.c + +tests/test_tt_eltwise_split: tests/test_tt_eltwise_split.c \ + src/accel/tt/pn_tt_eltwise_split.c src/accel/tt/pn_tt_eltwise_split.h + $(CC) $(CFLAGS) -o $@ tests/test_tt_eltwise_split.c \ + src/accel/tt/pn_tt_eltwise_split.c -lm + +tests/test_tt_descriptor_census: tests/test_tt_descriptor_census.c \ + src/accel/tt/pn_tt_descriptor_census.c src/accel/tt/pn_tt_descriptor_census.h \ + src/accel/tt/pn_tt_name_list.c src/accel/tt/pn_tt_name_list.h + $(CC) $(CFLAGS) -o $@ tests/test_tt_descriptor_census.c \ + src/accel/tt/pn_tt_descriptor_census.c src/accel/tt/pn_tt_name_list.c -lm + +# Self-contained differential parity test for the optimized token sampler: +# diffs a verbatim copy of the original sampler against the optimized one over +# thousands of random cases. No model/engine deps, so it links only libm. +tests/test_sampler_parity: tests/test_sampler_parity.c $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/test_sampler_parity.c -lm + +# GPU-vs-CPU parity harness for the qwen35moe CUDA kernels. Always links the +# Foundry CUDA backend (dlopen, -ldl) so it builds + runs everywhere: on a +# host with no CUDA driver it self-gates on fc_available() and prints +# [SKIP] no CUDA. The actual GPU diffs run on a K80 host. The backend is +# pulled in directly (not via USE_CUDA) so the harness is self-contained. +tests/test_kernel_parity: tests/test_kernel_parity.c $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c $(ALL_HDR) + $(CC) $(CFLAGS) -DUSE_CUDA -o $@ tests/test_kernel_parity.c \ + $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c -lm -ldl $(LDFLAGS) + +# GATING smoke test for the 2-die barswap context swap (Task 1). Proves two +# live CUDA contexts (one per K80 die, both ordinal 0) coexist and survive +# bs_acquire/release multiplexing through the single MMIO slot. Self-gates on +# fc_driver_load(): on a host with no CUDA driver it prints [SKIP] no CUDA. +# Linked like test_kernel_parity (fc_cuda.c + barswap.c, dlopen -ldl). The +# operator runs this FIRST — it gates the 2-die layer-split forward. +tests/test_2die_swap: tests/test_2die_swap.c $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c $(ALL_HDR) + $(CC) $(CFLAGS) -DUSE_CUDA -o $@ tests/test_2die_swap.c \ + $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c -lm -ldl $(LDFLAGS) + +# Regression guard for the GLOBAL stream-capture bug (task #28). Captures a +# graph on one die while a second thread allocates/syncs on another, and fails +# if either side is disturbed — which is what CU_STREAM_CAPTURE_MODE_GLOBAL did +# process-wide, showing up as unexplained 900/901 in the production log. Needs +# two visible devices; self-gates to [SKIP] otherwise. Run it on idle dies, +# e.g. CUDA_VISIBLE_DEVICES=2,3 tests/test_graph_capture_concurrency. +# Linked like test_kernel_parity (fc_cuda.c + barswap.c, dlopen -ldl). +tests/test_graph_capture_concurrency: tests/test_graph_capture_concurrency.c $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c $(ALL_HDR) + $(CC) $(CFLAGS) -DUSE_CUDA -o $@ tests/test_graph_capture_concurrency.c \ + $(FOUNDRY)/cuda/fc_cuda.c $(FOUNDRY)/barswap/barswap.c -lm -ldl -lpthread $(LDFLAGS) + +# CPU-reference golden-vector generator for the qwen35moe GPU kernels (no model). +# Builds the generator, then runs it to (re)produce tests/golden/*.bin. +tests/gen_golden_qwen35: tests/gen_golden_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) $(ALL_HDR) + $(CC) $(CFLAGS) -o $@ tests/gen_golden_qwen35.c $(PN_SRC) $(FOUNDRY_SRC) \ + -lm $(LDFLAGS) $(LDFLAGS_BLAS) $(LDFLAGS_METAL) $(LDFLAGS_CUDA) + +.PHONY: golden +golden: tests/gen_golden_qwen35 + ./tests/gen_golden_qwen35 + +# Converter CLI +pantheon-convert: cli/convert.c $(FOUNDRY)/pth/pth.c $(FOUNDRY)/safetensors/safetensors.c + $(CC) $(CFLAGS) -o $@ cli/convert.c $(FOUNDRY)/pth/pth.c $(FOUNDRY)/safetensors/safetensors.c -lm + +# Clean. Test/bench/debug binaries go by pattern (sources are .c/.h), the same rule as +# .gitignore, so a new test cannot be missed. +clean: + rm -f pantheon pantheon-convert libpantheon.a $(foreach b,cpu cuda tt metal cuda-tt cuda-metal tt-metal cuda-tt-metal,pantheon-$(b)) + find tests -maxdepth 1 -type f \( -name 'test_*' -o -name 'bench_*' -o -name 'gen_golden_*' \) ! -name '*.c' ! -name '*.h' -delete + rm -rf *.dSYM tests/*.dSYM + rm -f *.o src/core/*.o src/formats/*.o src/models/*.o src/memory/*.o src/accel/*.o + rm -rf $(LIB_OBJDIR) diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c6ebb45 --- /dev/null +++ b/NOTICE @@ -0,0 +1,54 @@ +Pantheon +Copyright (C) 2026 David (Corey) Herrera and the Pantheon contributors. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU Affero General Public License as published by the Free +Software Foundation, either version 3 of the License, or (at your option) any +later version. + +This program is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along +with this program. If not, see . + +The full license text is in LICENSE. +SPDX-License-Identifier: AGPL-3.0-or-later + + +Contributions +------------- +Contributions are accepted under the Contributor License Agreement in CLA.md +(CLA-CORPORATE.md for organizations), with a Developer Certificate of Origin +sign-off on every commit. Contributors retain copyright in their contributions. +The signature roster is .cla/signatures.md. + + +Third-party code +---------------- +Pantheon has no external dependencies. It links Foundry +(https://git.teamide.dev/David/Foundry.git), which is under the same AGPL-3.0 +terms and the same copyright holder. + +src/accel/tt/pn_tt_device.c uses the ARC telemetry tag numbers and names +defined by the TelemetryTag enumeration of tt-umd +(https://github.com/tenstorrent/tt-umd), (c) 2025 Tenstorrent Inc., +licensed under the Apache License, Version 2.0. Only those numbers and names +are used; no tt-umd source code is included. A copy of the Apache License 2.0 +is in LICENSES/Apache-2.0.txt. + +Model weights executed by Pantheon carry their own licenses, entirely separate +from this one and entirely between you and whoever published them. + + +Trademarks +---------- +No right is granted to use the "Pantheon" name or any associated logo to name or +promote a derivative product. Factual statements that your product uses Pantheon +are fine. + + +Contact +------- +coreyh55@gmail.com diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7030e5 --- /dev/null +++ b/README.md @@ -0,0 +1,344 @@ +# Pantheon + +**An inference engine that assumes you don't have enough.** + +Not enough VRAM, not enough host RAM, not enough cores, not enough context, and +not enough budget for current-generation GPUs. Pantheon is a memory-aware inference +engine and OpenAI-compatible model server built for that case: a small host, older or +mixed cards, several models per process. + +It is pure C11 on top of [Foundry](https://git.teamide.dev/David/Foundry.git), its +companion tensor library (included as a git submodule). Beyond Foundry it needs only +libc, libm, pthreads and libdl; BLAS (Accelerate on macOS, OpenBLAS on Linux) is +optional, and the optional Metal backend links Apple's frameworks. Each backend builds +to a single binary. + +--- + +## What "memory-aware" actually means + +A common behavior when an engine runs out of memory is that the request fails. Pantheon +treats running out as the *normal operating condition* and degrades on a ranked ladder, +least-harmful first (`src/core/mem/pn_ladder.h`): + +| Pressure | What gives | +|---|---| +| **WATCH** | force Q4 KV cache — near-zero quality cost, 4–7× VRAM win | +| **TIGHT** | halve concurrency · clamp the prefill window · evict cold KV | +| **CRITICAL** | floor concurrency to one slot — a single request always still completes | + +Admission is checked before every load *and* every turn, against live host-RAM and VRAM +pressure, so the engine refuses work it cannot finish instead of accepting it and +falling over halfway. That layer (`src/core/mem/`) is about 2,100 lines of the codebase, +which is a fair measure of how central it is. + +**It tells the client the truth.** A prompt past the context cap is refused with a typed +`context_over_budget` carrying the actual KV numbers, so the caller can compact and +retry. Nothing is truncated silently: for an agent built on top, silent truncation would +be silent corruption. + +## What that buys you + +- **Many models, one process, many GPUs.** One port hosts N models across N devices, + routed by the request's `model` field. Repeat a `--name` on another device and it + becomes a replica, load-balanced least-busy. Load and unload at runtime without a + restart. This is what lets a 2-core / 8 GB host drive several cards. +- **Portable session state.** Conversation state — recurrent SSM *and* attention KV — + serializes to a versioned blob stamped with a model-identity tag the restore side + validates. Hybrid state is hard to serialize because the recurrent half isn't + position-addressable. It means session state can live on the user's machine instead + of your disk. +- **Reasoning that terminates.** `--reasoning-budget N` forces `` after N tokens, + so a reasoning model that runs away still produces an answer instead of burning your + context. +- **Embeddable.** C11, no C++ runtime, no exceptions, auditable dependency tree, links as + a static library. + +## Old hardware + +The CUDA backend targets **`sm_37` — Kepler, 2014**. Measured by the author on one Tesla +K80 (both GK210 dies) in a 2-core / 8 GB host: a **35B-A3B hybrid MoE served at 32K–128K +context**: Qwen3.6-35B-A3B and Nex-N2-mini (Q3_K_M) both decode at ~21 tok/s and prefill +~110-120 tok/s up to ~8k tokens ([K80 quick start](#qwen36-and-nex-n2-mini-on-a-tesla-k80), +[supported models](docs/reference/supported-models.md)). + +Correctness on that path, stated precisely: + +- With the fused attention kernels off (`QWEN35_FUSE=0x37`) and flash prefill off + (`QWEN35_FLASH=0`), the author measured the 2-die GPU forward's logits **bit-identical + to the CPU forward** (argmax and every logit). +- The default flash prefill is argmax-exact, **not** bit-exact (~1e-6 relative drift; see + [env flags](docs/reference/env-flags.md)). +- The in-tree CUDA gates (`tests/test_spec_qwen35`, `tests/test_users_qwen35`, + `tests/test_prefix_cache_qwen35`) check token-for-token identity between decode paths + under `QWEN35_FUSE=0x37`. They need a model and a GPU and are not part of `make test`. + +Kepler is the extreme case, not the point. The point is that hardware you already own +should keep working. + +--- + +## Quick start + +Prerequisites: a C11 compiler and `make`. On Linux, `USE_BLAS=1` also needs OpenBLAS +(`apt install libopenblas-dev`); on macOS it uses Accelerate. The model download below +uses the Hugging Face CLI (`pip install -U huggingface_hub`); any way of fetching the +`.gguf` works. Build on the machine you will run on: the default flags include +`-march=native`, so a binary built on a newer CPU can fail with `Illegal instruction` on +an older one. To build a binary you can copy between x86-64 machines, add `PORTABLE=1` +(`-march=x86-64-v2`, no AVX). Per-platform details: [`docs/guides/install.md`](docs/guides/install.md). + +```bash +git clone --recursive https://git.teamide.dev/David/Pantheon.git pantheon +cd pantheon && make USE_BLAS=1 pantheon +# already cloned without --recursive? git submodule update --init + +hf download bartowski/SmolLM2-135M-Instruct-GGUF \ + SmolLM2-135M-Instruct-Q8_0.gguf --local-dir models/ + +./pantheon --model models/SmolLM2-135M-Instruct-Q8_0.gguf \ + --prompt "Hello, my name is" --max-tokens 32 +``` + +Interactive chat, with the model's own template detected automatically: + +```bash +./pantheon --model models/SmolLM2-135M-Instruct-Q8_0.gguf --chat +``` + +### Ornith 1.5 on a Tenstorrent card + +Ornith 1.5 9B is the only model supported on Tenstorrent cards for now. On Linux with a +Blackhole p150a: + +```bash +git clone --recursive https://git.teamide.dev/David/Pantheon.git pantheon +cd pantheon && ./install.sh # build pantheon-tt, set up card access (asks before sudo) +./run_ornith.sh # download the model (~10 GB, once), serve on 127.0.0.1:8080 +``` + +`run_ornith.sh` fetches the files from +[dcherrera/Ornith-1.5-9B-Pantheon-TT](https://huggingface.co/dcherrera/Ornith-1.5-9B-Pantheon-TT) +and checks their sha256. The first start converts the model for the card (~10 GB more on +disk); later starts skip that. It serves an OpenAI-compatible API, model id `ornith`. Options +are in the script's header. + +### Qwen3.6 and Nex-N2-mini on a Tesla K80 + +On Linux with a Tesla K80 and the NVIDIA 470 driver. **Supported K80 models: Qwen3.6-35B-A3B +and Nex-N2-mini (both Q3_K_M), one model per card** (both dies): + +```bash +git clone --recursive https://git.teamide.dev/David/Pantheon.git pantheon +cd pantheon && ./install.sh --k80 # build pantheon-cuda (no CUDA toolkit needed) +./k80_qwen36.sh # or ./k80_nexn2.sh; downloads ~17 GB once, serves on 127.0.0.1 +``` + +Measured on one K80 in a 2-core / 8 GB host (OpenAI streaming client, warm card, runs back to back): + +| prompt | Qwen3.6 first token | Nex-N2-mini first token | decode (both) | +|---|---|---|---| +| 1k tokens | 8 s (121 tok/s) | 9 s (107 tok/s) | ~21 tok/s | +| 8k tokens | 69 s (116 tok/s) | 67 s (119 tok/s) | ~18 tok/s | +| 14k tokens | 3.0 min (79 tok/s) | 3.2 min (74 tok/s) | ~15 tok/s | +| 30k tokens | 8.4 min (60 tok/s) | 9.6 min (53 tok/s) | ~10 tok/s | + +**Good for:** chat, writing, Q&A and background jobs — short prompts, long answers. Prefix +caching is on: send a `session_id`, and send each earlier assistant turn back with its +`reasoning_content`; later turns then only prefill what's new (a 4k-token conversation's second +turn: ~38 s without the cache, 2-5 s with it). opencode does both on its own: point an +`@ai-sdk/openai-compatible` provider at the server and its second turn (14k-token system prompt) +measured 9 s instead of 170 s. One session id may carry several conversations (opencode's title +request shares it); each keeps its own cache slot. +**Not good for:** pasting long documents or codebases every request; prefill is the K80's weak +spot. Options are in each script's header. + +## Serving + +One model, no GPU required: + +```bash +./pantheon serve --model models/SmolLM2-135M-Instruct-Q8_0.gguf --port 8080 +``` + +That's a complete OpenAI-compatible server. Point any client at +`http://127.0.0.1:8080/v1` and it works. + +Everything below is optional. `--name` defaults to the file name, and `--dies` is +CUDA-only — omit it and the engine places the model itself. Repeat `--model` to serve +several from one process: + +```bash +./pantheon serve \ + --model models/big.gguf --name chat --ctx-cap 32768 \ + --model models/small.gguf --name draft \ + --port 8080 +``` + +Clients choose with the `model` field in a normal OpenAI request body. On a multi-GPU +box you can pin placement, and repeating a `--name` on another device makes it a +**replica** that load-balances least-busy: + +```bash +./pantheon serve \ + --model models/big.gguf --name chat --dies 0,1 \ + --model models/small.gguf --name worker --dies 2 \ + --model models/small.gguf --name worker --dies 3 \ + --port 8080 +``` + +Two `worker` requests then occupy devices 2 and 3 concurrently, with nothing in front of +the process doing the routing. + +### Bringing up more than a couple of models + +On a constrained host, launching every model at once is how you OOM. The pattern that +works is to start `serve` with one model and add the rest through +[`POST /admin/models/load`](docs/reference/http-api.md) **one at a time**, each gated on +free VRAM *and* host-RAM headroom, then settle-waited — visible in `/v1/models` — before +starting the next. Bring-up becomes idempotent and restartable: after a failure you +re-run and it picks up where it stopped, keeping what already loaded. + +[`examples/deploy.sh`](examples/deploy.sh) is a working template that does exactly this, +driven by a manifest ([`examples/models.conf`](examples/models.conf)): + +``` +chat | /models/chat.gguf | ctx_cap=32768 max_sessions=4 max_concurrent=2 +draft | /models/draft.gguf | +``` + +```bash +./examples/deploy.sh --plan # dry run — print the plan, change nothing +./examples/deploy.sh # reconcile (idempotent) +./examples/deploy.sh --prune # also unload anything not in the manifest +``` + +It needs **bash and curl, nothing else** — no jq, no Python, no `nvidia-smi`. GPU state +comes from the engine's own `/admin/dies`, so the same script works unchanged on CPU, +Metal and CUDA. Copy it and adapt the gates; every per-model option the `serve` command +line accepts can be set per manifest line. + +| | | +|---|---| +| **Endpoints** | `/v1/chat/completions` (SSE), `/v1/models`, `/admin/models/*`, `/admin/dies` | +| **Per-model caps** | `--ctx-cap`, `--max-sessions`, `--max-concurrent`, `--reasoning-budget` | +| **Scheduling** | batch scheduler with a bounded queue, then an honest `429` + `Retry-After` | +| **Caching** | prefix cache with longest-common-prefix reuse across turns | +| **Structured output** | grammar-constrained decoding for tool calls and `response_format` | +| **Ops** | load/unload without restart; `/v1/models` stays live | + +A `pantheon router` subcommand fans across separate serve processes. + +## Supported models + +| Family | Covers | +|---|---| +| **Llama** | Llama, Qwen2, Mistral, Mixtral — one shared forward pass | +| **Phi-3** | fused QKV, NeoX RoPE | +| **Qwen3.5 / 3.6 MoE** | hybrid gated-delta SSM + attention, grouped experts, 2-die split | +| **RWKV-X** | hybrid recurrent + sparse attention, WKV-7 kernel | +| **Pixtral** | vision-language, continuous-batch decoder for bulk OCR | + +**Formats:** GGUF (v2/v3, K-quants and legacy) and safetensors, mmap'd zero-copy. +Per-model quants, measured throughput and a runnable command: +[`docs/reference/supported-models.md`](docs/reference/supported-models.md). + +## Backends + +| Backend | Flag | Notes | +|---|---|---| +| CPU | *(default)* | NEON, AVX2, scalar | +| BLAS | `USE_BLAS=1` | Accelerate on macOS, OpenBLAS on Linux | +| Threads | `USE_THREADS=1` | pthread pool | +| Metal | `USE_METAL=1` | Apple Silicon | +| CUDA | `USE_CUDA=1` | `sm_37`+, `dlopen`ed with kernels embedded as PTX — builds without nvcc. **Supported on Tesla K80: Qwen3.6-35B-A3B and Nex-N2-mini** | +| Tenstorrent | `USE_TT=1` | Blackhole p150a, Linux only; programs embedded, no TT software stack needed to build. **Supported model: Ornith 1.5 9B only** — other models may load on the card but are not supported | + +Because CUDA is `dlopen`ed, one binary runs on a machine with no NVIDIA driver at all and +lights up the GPU path on a machine that has one. + +```bash +make USE_BLAS=1 pantheon # CLI +make USE_BLAS=1 libpantheon.a # static library, to embed +make USE_BLAS=1 test # test suites +make DEBUG=1 USE_BLAS=1 pantheon +``` + +### The CLI binary is named for its backend + +Each accelerator backend writes its **own** file, and `pantheon` is a symlink to whichever +was built last: + +| Build | Binary | `pantheon` symlink | +|---|---|---| +| `make` | `pantheon-cpu` | → `pantheon-cpu` | +| `make USE_CUDA=1` | `pantheon-cuda` | → `pantheon-cuda` | +| `make USE_TT=1` | `pantheon-tt` | → `pantheon-tt` | +| `make USE_METAL=1` | `pantheon-metal` | → `pantheon-metal` | + +They used to be one file, so the last build silently owned the deployed path — which is +how a CPU-only binary came to serve a 35B MoE with every GPU die idle. A build for one +backend can no longer overwrite another's binary; at worst it re-points the symlink, and +`ln -sfn pantheon-cuda pantheon` puts that back with no rebuild. + +Ask a binary what it is rather than inferring it from its path: + +```bash +./pantheon --backend # -> cuda|tt|metal|cpu (one token, for scripts) +./pantheon --version # -> Pantheon v0.4.0 / backend=cuda build= +curl -s localhost:8080/ | jq .backend # a running serve +scripts/assert_backend.sh ./pantheon cuda # deploy-time assertion; exit 1 on mismatch +``` + +`pantheon serve` prints `backend=... build=... accelerator=...` as its first line and the +visible die count as its second, and it **refuses to start** when `--dies` names dies this +binary has no backend for. To serve on the CPU deliberately, simply do not pass `--dies`. + +## Embedding + +Pantheon is a library first and a CLI second. The public API is +[`include/pantheon.h`](include/pantheon.h) — opaque types, explicit ownership, error codes +rather than exceptions, no global state. Link `libpantheon.a` from C or any language with +a C FFI. + +## What Pantheon is not + +Being clear about this saves everyone time: + +- **It is not built for speed on modern datacenter GPUs.** Engines designed for that + hardware will be faster there. +- **Prefix caching, structured output, n-gram speculative decoding and continuous + batching are table stakes**, not differentiators. Everyone has them. They are here + because an engine without them isn't serious, not because they're novel. +- **It is not a distributed serving framework.** One process is self-routing and bounded; + you scale by adding servers, not by packing more into one. +- **It is one person's project.** Model coverage is narrower than that of larger projects. + +## Documentation + +[`docs/`](docs/) — start at [`docs/README.md`](docs/README.md). + +- [Quickstart](docs/guides/quickstart.md) · [Chat templates](docs/guides/chat.md) · [.pth converter](docs/guides/pth-convert.md) +- [Environment flags](docs/reference/env-flags.md) · [CLI flags](docs/reference/cli.md) · [HTTP API](docs/reference/http-api.md) · [Supported models](docs/reference/supported-models.md) +- Architecture: [engine](docs/architecture/engine.md) · [serve layer](docs/architecture/serve.md) · [memory-aware layer](docs/architecture/memory-aware.md) · [die manager](docs/architecture/accel-foundry.md) · [GGUF](docs/architecture/gguf.md) · [Llama forward](docs/architecture/llama-forward.md) · [Qwen3.5-MoE](docs/architecture/models-qwen35moe.md) · [RWKV-X](docs/architecture/rwkvx.md) + +## Built on Foundry + +Pantheon uses **Foundry** for tensor compute, safetensors, tokenization, quantization, +attention primitives and the CUDA layer. Same author, same license. + +Foundry is a git submodule at `foundry/` +([`https://git.teamide.dev/David/Foundry.git`](https://git.teamide.dev/David/Foundry.git)). +The Makefile resolves it as `FOUNDRY=/path`, then `./foundry` (the submodule, or a +symlink), then `../foundry` (a sibling checkout); if none exists it stops with a message +saying so rather than a wall of missing headers. + +## License + +**AGPL-3.0** — see [`LICENSE`](LICENSE). + +Contributions come in under the CLA in [`CLA.md`](CLA.md), with a DCO sign-off on every +commit — see [`CONTRIBUTING.md`](CONTRIBUTING.md). You keep your copyright; signing is one +entry in [`.cla/signatures.md`](.cla/signatures.md), added in your first pull request. + +Copyright © 2026 David (Corey) Herrera and the Pantheon contributors. See [`NOTICE`](NOTICE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0a16a6a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,56 @@ +# Security Policy + +## Reporting a Vulnerability + +**Do not open a public issue for a security vulnerability.** + +Email **coreyh55@gmail.com** with: + +- a description of the vulnerability +- steps to reproduce +- potential impact +- a suggested fix, if you have one + +You will get an acknowledgement within 48 hours. Critical issues are aimed at a +fix within 7 days. If you want credit in the fix commit, say so and give me the +name to use. + +## Scope + +Pantheon runs untrusted model files and serves untrusted network input, so the +security-relevant surface is larger than the codebase's size suggests: + +- **`src/formats/`** — GGUF parsing, quantized-tensor dequantization, and + memory-mapped I/O. A malformed model file reaching a bad offset or a bogus + tensor shape is the highest-value bug class here. +- **`src/core/`** — tokenizer and BPE pre-tokenization; chat-template parsing. + Both consume attacker-controlled text. +- **`src/serve/`** — the HTTP/SSE server: request parsing, JSON handling, + session and slot management, sampling parameter bounds. +- **`src/memory/`** — KV cache and session state, including the paths that + export and restore session blobs. + +Out of scope: the behavior of the models themselves. Prompt injection, +jailbreaks, and model output content are properties of the weights you load, not +of this engine. + +## Supported Versions + +| Version | Supported | +|---------|-----------| +| main | Yes | + +Older tags are not patched. Fixes land on `main`. + +## Best Practices + +When contributing to Pantheon: + +- `snprintf`, never `sprintf` +- validate every offset and length read out of a model file before using it — + treat the file as hostile input, not as data you wrote +- check every `malloc`/`calloc` return for NULL +- bounds-check array indices derived from file or network data +- watch for integer overflow in size calculations, especially + `count * element_size` on values taken from a header +- no `exit()` in library code — return an error and let the caller decide diff --git a/cli/convert.c b/cli/convert.c new file mode 100644 index 0000000..99dd95e --- /dev/null +++ b/cli/convert.c @@ -0,0 +1,141 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * pantheon-convert: Convert PyTorch .pth files to safetensors format + * + * Usage: pantheon-convert -o [--inspect] + */ + +#include +#include +#include + +#include "pth/pth.h" +#include "safetensors/safetensors.h" + +static void print_usage(const char *prog) { + fprintf(stderr, "Usage: %s -o [--inspect]\n", prog); + fprintf(stderr, "\nOptions:\n"); + fprintf(stderr, " --inspect Print tensor table and exit (no conversion)\n"); +} + +int main(int argc, char **argv) { + const char *input = NULL; + const char *output = NULL; + int inspect = 0; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-o") == 0 && i + 1 < argc) { + output = argv[++i]; + } else if (strcmp(argv[i], "--inspect") == 0) { + inspect = 1; + } else if (argv[i][0] != '-') { + input = argv[i]; + } else { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + if (!input) { + fprintf(stderr, "Error: no input file specified\n"); + print_usage(argv[0]); + return 1; + } + + if (!inspect && !output) { + fprintf(stderr, "Error: no output file specified (use -o or --inspect)\n"); + print_usage(argv[0]); + return 1; + } + + /* Open .pth file */ + printf("Opening %s...\n", input); + pt_file *pth = pt_open(input); + if (!pth) { + fprintf(stderr, "Error: failed to open %s\n", input); + return 1; + } + + int n = pt_tensor_count(pth); + printf("Found %d tensors\n\n", n); + + /* Print tensor table */ + if (inspect || !output) { + printf("%-50s %-8s %-20s %12s\n", "Name", "Dtype", "Shape", "Size"); + printf("%-50s %-8s %-20s %12s\n", + "--------------------------------------------------", + "--------", "--------------------", "------------"); + + size_t total_bytes = 0; + for (int i = 0; i < n; i++) { + const pt_tensor *t = pt_tensor_at(pth, i); + if (!t) continue; + + char shape_str[128] = {0}; + int off = 0; + off += snprintf(shape_str + off, sizeof(shape_str) - off, "["); + for (int d = 0; d < t->ndim; d++) { + if (d > 0) off += snprintf(shape_str + off, sizeof(shape_str) - off, ", "); + off += snprintf(shape_str + off, sizeof(shape_str) - off, "%lld", + (long long)t->shape[d]); + } + snprintf(shape_str + off, sizeof(shape_str) - off, "]"); + + char size_str[32]; + if (t->data_size >= 1024 * 1024) + snprintf(size_str, sizeof(size_str), "%.1f MB", (double)t->data_size / (1024 * 1024)); + else if (t->data_size >= 1024) + snprintf(size_str, sizeof(size_str), "%.1f KB", (double)t->data_size / 1024); + else + snprintf(size_str, sizeof(size_str), "%zu B", t->data_size); + + printf("%-50s %-8s %-20s %12s\n", + t->name, fs_dtype_name(t->dtype), shape_str, size_str); + + total_bytes += t->data_size; + } + + printf("\nTotal: %d tensors, %.1f MB\n", n, (double)total_bytes / (1024 * 1024)); + + if (inspect) { + pt_close(pth); + return 0; + } + } + + /* Convert to safetensors */ + printf("Converting to %s...\n", output); + + fs_writer *w = fs_writer_new(); + if (!w) { + fprintf(stderr, "Error: failed to create safetensors writer\n"); + pt_close(pth); + return 1; + } + + int skipped = 0; + for (int i = 0; i < n; i++) { + const pt_tensor *t = pt_tensor_at(pth, i); + if (!t || !t->data || t->data_size == 0) { + fprintf(stderr, " Warning: skipping tensor '%s' (no data)\n", t ? t->name : "?"); + skipped++; + continue; + } + fs_writer_add_tensor(w, t->name, t->dtype, t->shape, t->ndim, t->data); + } + + if (!fs_writer_save(w, output)) { + fprintf(stderr, "Error: failed to write %s\n", output); + fs_writer_free(w); + pt_close(pth); + return 1; + } + + fs_writer_free(w); + pt_close(pth); + + printf("Done! Converted %d tensors (%d skipped)\n", n - skipped, skipped); + return 0; +} diff --git a/cli/main.c b/cli/main.c new file mode 100644 index 0000000..31d53ec --- /dev/null +++ b/cli/main.c @@ -0,0 +1,289 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include +#include +#include +#include "../include/pantheon.h" +#include "../src/accel/backend_identity.h" +#include "../src/core/bench.h" +#include "../src/core/sample_profiler.h" +#include "../src/core/engine.h" +#include "../src/core/chat.h" +#include "../src/serve/serve_command.h" +#include "../src/serve/router_command.h" +#include "tensor/tensor.h" + +#define PN_VERSION "0.4.0" + +static void print_usage(const char *prog) { + printf("Pantheon v%s — Pure C Model Hosting Engine\n\n", PN_VERSION); + printf("Usage: %s [options]\n\n", prog); + printf("Options:\n"); + printf(" --model Model file (GGUF or safetensors)\n"); + printf(" --config HuggingFace config.json (for safetensors models)\n"); + printf(" --vocab RWKV World tokenizer vocab file\n"); + printf(" --prompt Input prompt (one-shot mode; chat template applied unless --raw)\n"); + printf(" --raw One-shot --prompt: send raw text, no chat template\n"); + printf(" --chat Interactive chat mode\n"); + printf(" --system System prompt (chat mode only)\n"); + printf(" --max-tokens Maximum tokens to generate (default: 128)\n"); + printf(" --temperature Sampling temperature (default: 0.8)\n"); + printf(" --top-p Nucleus sampling threshold (default: 0.9)\n"); + printf(" --top-k Top-k sampling (default: 40)\n"); + printf(" --quantize INT8 quantize weights at load time\n"); + printf(" --bench Run benchmark (report tok/s and memory)\n"); + printf(" --help Show this help\n"); + printf(" --version Show version, compiled backend and build commit\n"); + printf(" --backend Print ONLY the compiled backend token\n"); + printf(" (cuda|tt|metal|cpu) and exit — the deploy-time\n"); + printf(" assertion that this binary is the backend the\n"); + printf(" fleet manifest expects\n"); +} + +static void stream_print_cb(const char *token, void *user_data) { + (void)user_data; + printf("%s", token); + fflush(stdout); +} + +int main(int argc, char **argv) { + pn_sample_profiler_start(); /* PN_PROFILE=: a SIGPROF sample profile, dumped at exit */ + /* Subcommand dispatch. `pantheon serve ...` starts the OpenAI HTTP server; + * everything else is the original flag-driven CLI below. */ + if (argc > 1 && strcmp(argv[1], "serve") == 0) + return pn_serve_main(argc - 1, argv + 1); + /* `pantheon router ...` runs the dispatch layer (registry + LB router). + * Pure host-side networking, no model load / CUDA. */ + if (argc > 1 && strcmp(argv[1], "router") == 0) + return pn_router_main(argc - 1, argv + 1); + + const char *model_path = NULL; + const char *config_path = NULL; + const char *vocab_path = NULL; + const char *prompt = NULL; + const char *system_prompt = NULL; + int max_tokens = 128; + int bench = 0; + int chat_mode = 0; + int quantize = 0; + int raw = 0; /* --raw: bypass the chat template in one-shot --prompt mode */ + pn_sampling_params params = pn_sampling_defaults(); + + /* Parse arguments */ + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + print_usage(argv[0]); + return 0; + } + /* `--backend` prints ONE lowercase token and nothing else, so a deploy + * script can assert on it with a plain string compare: + * [ "$("$BIN" --backend)" = cuda ] || exit 1 + * That is the whole contract — stdout is the token, exit status is 0. + * Keep it the first thing parsed and never let anything else reach + * stdout on this path. */ + if (strcmp(argv[i], "--backend") == 0) { + printf("%s\n", pn_backend_token()); + return 0; + } + if (strcmp(argv[i], "--version") == 0) { + /* First line byte-identical to what it always was — scripts parse + * it. The backend goes on a SECOND line rather than into that one. */ + printf("Pantheon v%s\n", PN_VERSION); + printf("backend=%s build=%s\n", pn_backend_token(), + pn_backend_build_commit()); + return 0; + } + if (strcmp(argv[i], "--model") == 0 && i + 1 < argc) { + model_path = argv[++i]; + } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { + config_path = argv[++i]; + } else if (strcmp(argv[i], "--vocab") == 0 && i + 1 < argc) { + vocab_path = argv[++i]; + } else if (strcmp(argv[i], "--quantize") == 0) { + quantize = 1; + } else if (strcmp(argv[i], "--prompt") == 0 && i + 1 < argc) { + prompt = argv[++i]; + } else if (strcmp(argv[i], "--chat") == 0) { + chat_mode = 1; + } else if (strcmp(argv[i], "--system") == 0 && i + 1 < argc) { + system_prompt = argv[++i]; + } else if (strcmp(argv[i], "--max-tokens") == 0 && i + 1 < argc) { + max_tokens = atoi(argv[++i]); + } else if (strcmp(argv[i], "--temperature") == 0 && i + 1 < argc) { + params.temperature = (float)atof(argv[++i]); + } else if (strcmp(argv[i], "--top-p") == 0 && i + 1 < argc) { + params.top_p = (float)atof(argv[++i]); + } else if (strcmp(argv[i], "--top-k") == 0 && i + 1 < argc) { + params.top_k = atoi(argv[++i]); + } else if (strcmp(argv[i], "--bench") == 0) { + bench = 1; + } else if (strcmp(argv[i], "--raw") == 0) { + raw = 1; + } else { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + print_usage(argv[0]); + return 1; + } + } + + /* If no model specified, just print version and verify Foundry links */ + if (!model_path) { + printf("Pantheon v%s\n", PN_VERSION); + printf("backend=%s build=%s\n", pn_backend_token(), + pn_backend_build_commit()); + printf("Built on Foundry tensor library.\n\n"); + + /* Verify Foundry links correctly */ + int64_t shape[] = {2, 3}; + tm_tensor *t = tm_ones(shape, 2, TM_F32); + if (t) { + printf("Foundry link check: OK (created %lldx%lld tensor)\n", + (long long)t->shape[0], (long long)t->shape[1]); + tm_free(t); + } else { + printf("Foundry link check: FAILED\n"); + return 1; + } + + if (!prompt && !chat_mode) { + printf("\nRun with --model --prompt to generate text.\n"); + printf("Run with --model --chat for interactive chat.\n"); + return 0; + } + } + + /* Load model */ + if (model_path) { + double t0 = pn_time_ms(); + pn_load_opts opts = { .vocab_path = vocab_path, .config_path = config_path, .quantize = quantize }; + pn_model *model = pn_load_ex(model_path, &opts); + if (!model) { + fprintf(stderr, "Failed to load model: %s\n", model_path); + return 1; + } + double load_ms = pn_time_ms() - t0; + if (bench) fprintf(stderr, " load: %.1f ms\n", load_ms); + + pn_state *state = pn_state_new(model); + if (!state) { + fprintf(stderr, "Failed to create inference state\n"); + pn_model_free(model); + return 1; + } + + if (chat_mode) { + /* Interactive chat mode */ + pn_chat_style style = (pn_chat_style)model->chat_style; + ft_tokenizer *tok = model->tokenizer; + + printf("Chat mode (%s). Type /quit to exit, /clear to reset.\n", + pn_chat_style_name(style)); + + char line[4096]; + int turn = 0; + + while (1) { + printf("> "); + fflush(stdout); + + if (!fgets(line, sizeof(line), stdin)) break; + + /* Strip trailing newline */ + size_t len = strlen(line); + if (len > 0 && line[len - 1] == '\n') line[len - 1] = '\0'; + + /* Handle commands */ + if (strcmp(line, "/quit") == 0 || strcmp(line, "/exit") == 0) break; + if (strcmp(line, "/clear") == 0) { + pn_state_reset(state); + turn = 0; + printf("Conversation cleared.\n"); + continue; + } + + /* Skip empty lines */ + if (line[0] == '\0') continue; + + /* Format and generate */ + pn_token_seq seq; + if (turn == 0) + seq = pn_chat_format_first(style, tok, system_prompt, line); + else + seq = pn_chat_format_turn(style, tok, line); + + if (!seq.tokens || seq.n_tokens == 0) { + fprintf(stderr, "chat: tokenization failed\n"); + free(seq.tokens); + continue; + } + + /* The exact token ids this turn prefills with. A parity harness takes token + * ids, so without this the only way to re-run a chat prompt against the CPU + * oracle is to guess at the template's own tokenization. */ + if (getenv("PN_PROMPT_TOKEN_DUMP")) { + fprintf(stderr, "PROMPT_TOKENS %d:", seq.n_tokens); + for (int t = 0; t < seq.n_tokens; t++) + fprintf(stderr, " %d", seq.tokens[t]); + fprintf(stderr, "\n"); + } + + pn_stream_tokens(model, state, seq.tokens, seq.n_tokens, + max_tokens, ¶ms, stream_print_cb, NULL); + printf("\n"); + free(seq.tokens); + turn++; + } + } else if (bench) { + /* Benchmark mode */ + pn_bench_result result = {0}; + result.memory_bytes = pn_memory_usage(); + + double gen_start = pn_time_ms(); + char *output = pn_generate(model, state, prompt ? prompt : "Hello", + max_tokens, ¶ms); + double gen_end = pn_time_ms(); + + if (output) { + printf("%s\n", output); + free(output); + } + + result.n_decode = max_tokens; + result.decode_ms = gen_end - gen_start; + result.memory_bytes = pn_memory_usage(); + pn_bench_report(&result); + } else { + /* One-shot generation. Apply the model's chat template (single + * turn) so output matches the reference; --raw or a model with no + * chat template falls back to raw completion. */ + const char *one_shot = prompt ? prompt : "Hello"; + pn_chat_style style = (pn_chat_style)model->chat_style; + + if (!raw && style != PN_CHAT_NONE) { + pn_token_seq seq = pn_chat_format_first(style, model->tokenizer, + system_prompt, one_shot); + if (seq.tokens && seq.n_tokens > 0) { + pn_stream_tokens(model, state, seq.tokens, seq.n_tokens, + max_tokens, ¶ms, stream_print_cb, NULL); + printf("\n"); + free(seq.tokens); + } else { + fprintf(stderr, "one-shot: chat formatting failed\n"); + free(seq.tokens); + } + } else { + char *output = pn_generate(model, state, one_shot, + max_tokens, ¶ms); + if (output) { + printf("%s\n", output); + free(output); + } + } + } + + pn_state_free(state); + pn_model_free(model); + } + + return 0; +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a51a2a8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,117 @@ +# Pantheon Documentation + +Pantheon is a pure-C11 inference engine and OpenAI-compatible model server, built on Foundry (a git submodule). Beyond Foundry it needs libc, libm, pthreads and libdl; BLAS and the Metal backend's Apple frameworks are optional. + +## ⭐ Multi-Model Smart Routing — the flagship feature + +Pantheon is a **memory-aware, multi-model server**. A **single running instance** hosts **several models across several GPUs at once** and routes every request intelligently — so a tiny host (e.g. 2 cores / 8 GB RAM) can drive a fleet of cards and run models on hardware they were never meant for. Tying *multiple* instances together is a separate future **gateway**; **one instance is fully self-routing** and needs nothing in front of it. + +**How routing works (all inside one `pantheon serve` process):** + +- One process binds **one port**. Each `--model` is an independent registry slot with its own weights, state, and die placement. A request picks its model by the **`model` field** in the JSON body, matched to each model's `--name` (one model loaded ⇒ the field is ignored). + +- **Replicas = the same `--name` on different dies.** `mr_acquire` (`src/serve/model_registry.c`) load-balances across replicas by **least-busy** (fewest in-flight refs); with batching off each replica runs on its own die, so requests execute **concurrently**: two concurrent requests to a model replicated on dies 2 and 3 keep both dies busy at once and finish in about one request's wall-clock. + +- **Memory-aware admission** ([Memory-Aware Layer](architecture/memory-aware.md)) gates every load and turn against live host-RAM + VRAM pressure, keeping the instance in a small host footprint. + +- **Optional per-model concurrency:** `QWEN35_BATCH_SLOTS=1` adds up to 8 batched slots on a single die (off by default — see [reference/env-flags.md](reference/env-flags.md)). + +**Launch it** — one instance, an orchestrator plus a worker replicated across two dies: + +```bash +./pantheon serve \ + --model /models/orchestrator.gguf --name orch --dies 0,1 \ + --model /models/worker.gguf --name worker --dies 2 \ + --model /models/worker.gguf --name worker --dies 3 \ + --port 8201 +``` + +Clients POST `/v1/chat/completions` with `"model":"orch"` or `"model":"worker"`; `worker` +requests fan out across dies 2 and 3 by least-busy, concurrently. No router needed. + +> **Known gap (roadmap):** replica selection is least-busy but **not yet session-sticky** — under concurrent load a multi-turn session can hop replicas and miss its prefix cache. Session affinity across replicas is designed but not yet shipped. + +## ⭐ Reference Library (flags · CLI · HTTP API) + +The authoritative, code-audited reference (2026-07-12) — every environment flag, CLI flag, and HTTP endpoint, with values, defaults, effects, source `file:line`, and the **parsing gotchas that bite** (e.g. `QWEN35_BATCH_SLOTS` is an *enable* flag keyed on `env[0]=='1'`, so `=8` silently disables batching; `--dies` indices are relative to `CUDA_VISIBLE_DEVICES`, not physical devices). + +- [**Environment Flags**](reference/env-flags.md) — all `QWEN35_*` / `PN_*` / `FC_*` + distributed vars (\~40 flags) + +- [**CLI Flags**](reference/cli.md) — `pantheon serve` global + per-model flags, `--dies` semantics, multi-model binding + +- [**HTTP API**](reference/http-api.md) — `/v1/models`, `/v1/chat/completions` (SSE), `/admin/models/*`, `/admin/dies` + +- [**Supported Models**](reference/supported-models.md) — every verified model with its quant, config, measured throughput and a runnable command + +## Architecture — serving-era (code-audited 2026-07-12) + +- [Serve Layer](architecture/serve.md) — request lifecycle, model registry + multi-card load-balancing, the batch scheduler, streaming/SSE, the lock table + +- [Qwen3.5-MoE Model](architecture/models-qwen35moe.md) — the qwen35moe arch (e.g. Nex-N2-mini), 2-die split/placement, KV cache, decode slots, async step machine *(supersedes the legacy `qwen35moe.md` below)* + +- [Core Engine](architecture/core-engine.md) — `pn_load`/`pn_load_on`, the forward-step async seam, admission helpers + +- [Memory-Aware Layer](architecture/memory-aware.md) — pressure levels (OK/WATCH/TIGHT/CRITICAL), pre-flight admission, the reactive ladder (why TIGHT is the mini's normal resting state) + +- [Accelerator / Die Manager + Foundry CUDA](architecture/accel-foundry.md) — die abstraction, CUDA↔die mapping under `CUDA_VISIBLE_DEVICES`, per-die VRAM budgeting, foundry primitives (streams/events/graphs), and **§7 the second backend: Tenstorrent Blackhole p150a** (`make USE_TT=1`; tt-kmd + SFPI only — no tt-metal, TT-NN, Python or C++) — what it serves today, its runtime flags, and the hazards that cost hardware + +## Guides + +- [Quickstart](guides/quickstart.md) — Build, download a model, generate text + +- [Install](guides/install.md) — Per-platform build and run (CPU, macOS, CUDA, Tenstorrent), troubleshooting + +- [Chat Templates & Interactive Chat](guides/chat.md) — Multi-turn chat, template detection, C API + +- [PyTorch .pth Converter](guides/pth-convert.md) — Convert .pth to safetensors in pure C, no Python + +## Architecture + +- [Engine Overview](architecture/engine.md) — How model loading, dispatch, and generation work, plus the vision-language encode/decode seam and continuous-batch VLM decoder for bulk OCR + +- [GGUF Format](architecture/gguf.md) — GGUF parser, quant types, v2/v3 support + +- [Llama Forward Pass](architecture/llama-forward.md) — Shared forward pass for Llama, Qwen2, Mistral, Mixtral + Phi-3 differences, batched multi-sequence decode, flash prefill, and the opt-in GPU paths + +- [Qwen3.5-MoE Forward Pass (legacy)](architecture/qwen35moe.md) — the original forward-pass writeup. **⚠ Predates the A–D serving stack; superseded by **[**architecture/models-qwen35moe.md**](architecture/models-qwen35moe.md)** (the 2026-07-12 code audit). Kept for any unique detail; prefer the new doc.** + +- [RWKV-X Forward Pass](architecture/rwkvx.md) — Hybrid recurrent + sparse attention, WKV-7 kernel, safetensors loading + +## Serving & multi-tenant + +The serving architecture itself is documented in [architecture/serve.md](architecture/serve.md); what follows is a map of the behaviour it implements. + +- **Multi-card concurrency** — the host-memory + concurrency stack that lets a small host (8 GB / 2 cores) drive up to 8 cards: the die is the atomic unit, at most one model per die, reliability first, and capacity scales by adding servers rather than packing models. + +- **Single vs multi-tenant** — one card serves either one fast user or K slower ones; the two knobs are multi-model placement and `QWEN35_BATCH_SLOTS` batching. See [reference/env-flags.md](reference/env-flags.md). + +- **Multi-model serving** — registry, `model`-field routing, runtime load/unload, die placement. + +- **Fleet bring-up** — load models one at a time through `/admin/models/load`, gated and settle-waited. [`examples/deploy.sh`](../examples/deploy.sh) is a working template. + + +## API Reference + +- [`include/pantheon.h`](../include/pantheon.h) — Public C API (opaque types, generation, sampling, vision-language describe, and the continuous-batch VLM decoder) + +- [`src/core/engine.h`](../src/core/engine.h) — Internal types: config, architecture registry, state, `pn_load_opts` + +- [`src/formats/gguf.h`](../src/formats/gguf.h) — GGUF file format types and parser + +- [`src/models/llama.h`](../src/models/llama.h) — Llama architecture weight structs and forward pass + +- [`src/models/phi3.h`](../src/models/phi3.h) — Phi-3 weight structs (fused QKV, fused gate/up) + +- [`src/models/rwkvx.h`](../src/models/rwkvx.h) — RWKV-X adapter (safetensors loading, trie tokenizer, WKV-7 forward) + +- [`src/core/chat.h`](../src/core/chat.h) — Chat template detection and formatting + +- [`src/core/qweight.h`](../src/core/qweight.h) — Quantized weight matrix (zero-copy mmap) + +- [`src/accel/simd.h`](../src/accel/simd.h) — SIMD kernels (NEON, AVX2, scalar) + +- [`src/memory/arena.h`](../src/memory/arena.h) — Bump allocator for inference scratch memory + +## Contributing + +- [`../CONTRIBUTING.md`](../CONTRIBUTING.md) — build flags, code style, testing bar, and the licensing terms contributions are made under diff --git a/docs/architecture/accel-foundry.md b/docs/architecture/accel-foundry.md new file mode 100644 index 0000000..9b67231 --- /dev/null +++ b/docs/architecture/accel-foundry.md @@ -0,0 +1,371 @@ +# Accelerator / Die-Manager Layer and Foundry CUDA Base + +Covers `src/accel/die_manager.c|h` and `foundry/cuda/fc_cuda.c|h`. + +--- + +## 1. Die abstraction + +A **die** is one GK210 GPU — the unit of VRAM ownership in Pantheon. A K80 +card physically packages **two GK210 dies** connected by an on-card PLX +switch. They appear to CUDA as **two separate devices** with independent +contexts, streams, and VRAM pools. The die manager hard-codes this pairing: +`die_id / 2 = card_id` (`pn_dm_card_of`, `die_manager.h:82`) and +`{2i, 2i+1}` are card-mates (`die_mate`, `die_manager.c:195`). The die +manager supports up to 16 dies (`PN_DM_MAX_DIES`, `die_manager.h:41`; mirroring +`PN_MEM_MAX_DIES`). + +### K80 hardware constraints + +Reflected in the kernel target (`fc_cuda.h:17`): + +- Compute capability sm_37 — no FP16 math, no tensor cores. +- `__shfl_down()` (Kepler) rather than `__shfl_down_sync()` (Pascal+). +- VRAM bandwidth ~240 GB/s; K80 decode is **launch-bound** (~80 µs/launch, + `fc_cuda.h:562`), not compute-bound — the fused-layer and graph-capture + mechanisms exist specifically to reduce launch count. + +--- + +## 2. CUDA device → die mapping under `CUDA_VISIBLE_DEVICES` + +`pn_dm_init_cuda` (`die_manager.c:72`) enumerates CUDA ordinals `[0, n_dies)` +by calling `fc_cuda_init_device(i)` for each. Because `CUDA_VISIBLE_DEVICES` +re-numbers the visible devices starting at 0, die ordinal `i` inside Pantheon +always equals CUDA device `i` within whatever the variable permits. Dies with +ordinals not visible to the process are not reachable: `cuDeviceGet(i)` returns +error 101 when `i >= CUDA_VISIBLE_DEVICES` count — the benign probe that +terminates the enumeration loop (`die_manager.c:89`): + +```c +fc_cuda_ctx *ctx = fc_cuda_init_device(i); +if (!ctx || !fc_cuda_ctx_available(ctx)) + break; /* ordinals are contiguous — stop */ +``` + +`fc_cuda_init_device` calls `p_cuDeviceGet(&ctx->device, device_id)` +(`fc_cuda.c:910`); on error it logs `cuDeviceGet(%d) failed: %d` and returns +NULL, which is the break condition above. + +--- + +## 3. VRAM budgeting and admission at the die level + +### Reserve + +The die budget is `total_vram − PN_DIE_RESERVE_BYTES` (default **1 GiB**, +`pn_memwatch.h:42`; overridable via `PN_DIE_RESERVE_BYTES`). +`pn_dm_init_cuda` computes this per die (`die_manager.c:95`): + +```c +uint64_t budget = die_total > reserve ? die_total - reserve : 0; +``` + +### External occupancy + +VRAM already consumed by another process at init time is detected and credited +to `vram_used` so Pantheon doesn't over-commit (`die_manager.c:101`): + +```c +uint64_t external = (die_total - die_free) > reserve + ? (die_total - die_free) - reserve : 0; +g_dm.die[i].vram_used = external < budget ? external : budget; +``` + +### Two tallies + +The die manager maintains two numbers per die (`die_manager.c:37`): + +| Field | Meaning | +|---|---| +| `vram_budget` | Usable bytes (total − reserve). Set at init, never changes. | +| `vram_used` | Bytes committed by bindings + external occupancy. Mutated by bind/release. | + +Free bytes at any moment = `vram_budget − vram_used` (`die_free_bytes`, +`die_manager.c:183`). + +### memwatch feed + +After computing each die's budget, `pn_dm_init_cuda` calls +`pn_memwatch_set_die(i, die_free, budget, fc_ctx_alloc_bytes(ctx))` +(`die_manager.c:105`) so `MemoryAware` admission sees the same per-die truth. + +--- + +## 4. Bind / release lifecycle + +### `pn_dm_place` (auto-placement, `die_manager.c:200`) + +Does not reserve — only selects. Accepts `n_dies_wanted` of 1 or 2. + +**2-die request:** Scans even ordinals, looking for a same-card pair `{a, a+1}` +both with `die_free_bytes ≥ per_die`. Prefers a fully-idle pair; among ties +picks the pair with the most aggregate free space (`die_manager.c:219`). + +**1-die request:** Prefers a die whose card-mate is already occupied ("pack") +so free same-card pairs remain available for future 2-die models. Among +candidates picks the most-free die (`die_manager.c:244`). + +### `pn_dm_bind` (`die_manager.c:270`) + +Commits a placement under the global mutex: + +1. Finds a free slot in `g_dm.binding[]` (cap `PN_DM_MAX_BINDINGS = 16`). +2. Re-checks `die_free_bytes(id) >= per_die` for every die in the set — + guards against TOCTOU between `pn_dm_place` and `pn_dm_bind`. +3. Writes the binding (name, die list, `bytes_per_die`). +4. Increments `g_dm.die[id].vram_used` by `per_die` for every die in the set. + +The whole operation is atomic under the lock; on any failure nothing is +partially written (`goto refuse`, `die_manager.c:313`). + +**Same-name multi-card rule** (`die_manager.c:283`): a model name may be bound +multiple times to DISJOINT die-sets (one entry per copy, for load-balanced +multi-card serving). The code **does not reject duplicate names** at bind time. +Overlap is caught by the per-die room check (step 2): if a second binding would +use the same die, `die_free_bytes` will be too small and the bind refuses. +`pn_dm_release` is first-match on name (`die_manager.c:326`), so N bindings +with the same name require N `pn_dm_release` calls to free all of them. + +### `pn_dm_release` (`die_manager.c:318`) + +Walks the binding table, finds the first matching name, subtracts +`bytes_per_die` from each die's `vram_used` (clamped to 0), zeros the slot, +and returns. Only releases one binding per call. + +--- + +## 5. Foundry CUDA primitives + +### 5.1 Driver loading (`fc_cuda.c:517`) + +`fc_driver_load()` (called lazily by `fc_cuda_init_device`): + +1. `dlopen("libcuda.so.1")` — falls back to unversioned `libcuda.so`. +2. Resolves all driver API symbols via `fc_load_cuda_symbols` + (`fc_cuda.c:414`). `_v2` variants are preferred for context/memory ops + (required by cuBLAS); a fall-back to the unversioned name is attempted for + each. +3. `cuInit(0)`. +4. `dlopen("libcublas.so.11")` (optional — custom PTX kernels work without it). + +The resolved function-pointer table is **process-global** and shared by all +contexts (`fc_cuda.c:152`). The table includes optional symbols for peer copies, +graph capture, and events; missing ones leave their pointer NULL and the +corresponding feature self-disables gracefully. + +### 5.2 Per-device context (`fc_cuda_init_device`, `fc_cuda.c:896`) + +One `fc_cuda_ctx` per die. Sequence: + +1. `p_cuDeviceGet(&ctx->device, device_id)` — binds the ordinal. +2. `p_cuCtxCreate(&ctx->context, 0, ctx->device)` — creates a CUDA context; + leaves it current. +3. `p_cuStreamCreate(&ctx->stream, 0)` — one stream per context. +4. `cublasCreate` + `cublasSetStream` on the stream (non-fatal if cuBLAS absent). +5. `fc_ctx_load_kernels` — `cuModuleLoadData` of all embedded PTX blobs into + this context's private module slots. Optional kernels (graph-capture variants, + 8-wide tile variants, slot-attention kernels) are looked up tolerantly and + left NULL if absent. +6. `fc_ctx_leave()` — pops the new context off the thread's stack. +7. `fc_registry_add(ctx)` — registers in the live-context table for + crash-time teardown. + +The driver function table is shared; module handles and kernel function handles +are **per-context** (copied into each `fc_cuda_ctx`'s module fields, +`fc_cuda.c:224`). + +### 5.3 Context isolation (push/pop, `fc_cuda.c:577`) + +Every device op calls `fc_ctx_enter(ctx)` → `cuCtxPushCurrent` before issuing +work and `fc_ctx_leave()` → `cuCtxPopCurrent` on return. This keeps concurrent +dies from bleeding into each other even when ops from different dies interleave +on the same host thread. + +### 5.4 VRAM accounting (`ctx->alloc_bytes`) + +Every `cuMemAlloc` incremented path (`fc_buffer_from_ptr_ctx` at +`fc_cuda.c:1156`, `fc_buffer_alloc_ctx` at `fc_cuda.c:1184`) adds `bytes` to +`ctx->alloc_bytes`. `fc_buffer_free` subtracts it (clamped, `fc_cuda.c:1220`). +This counter is the **predictive** side of admission; `fc_ctx_alloc_bytes(ctx)` +exposes it. `pn_dm_init_cuda` passes it to `pn_memwatch_set_die` so both +tallies start consistent. + +### 5.5 Memory operations + +| Function | Operation | +|---|---| +| `fc_buffer_from_ptr_ctx` | `cuMemAlloc` + `cuMemcpyHtoD` (blocking) | +| `fc_buffer_alloc_ctx` | `cuMemAlloc` only (output buffer) | +| `fc_buffer_free` | `cuMemFree` + decrement `alloc_bytes` | +| `fc_buffer_to_cpu_n` | `cuMemcpyDtoH` (blocking) | +| `fc_buffer_to_cpu_n_async` | `cuMemcpyDtoHAsync` on ctx stream | +| `fc_buffer_copy_d2d_ctx` | `cuMemcpyDtoDAsync` on ctx stream | +| `fc_buffer_copy_peer` | `cuMemcpyPeer` (blocking, cross-die) | +| `fc_buffer_copy_peer_async` | `cuMemcpyPeerAsync` on stream_ctx stream | +| `fc_buffer_offset_view` | Non-owning wrapper (struct + pointer offset only; no cuMemAlloc/Free) | + +Peer copies require `fc_cuda_enable_peer(a, b)` first, which calls +`cuDeviceCanAccessPeer` and `cuCtxEnablePeerAccess` in both directions +(`fc_cuda.c:1400`). Error 704 (already-enabled) is treated as success. + +### 5.6 Synchronization + +`fc_sync_ctx(ctx)` (`fc_cuda.c:4468`) calls `cuStreamSynchronize` on +`ctx->stream` (falling back to `cuCtxSynchronize` if the stream is NULL). +This blocks the host thread until all work queued on the stream is done. + +**Events** (`fc_event_*`, `fc_cuda.c:4391`): created with +`CU_EVENT_DISABLE_TIMING` (no timing overhead). `fc_event_record_ctx` queues +`cuEventRecord` on the ctx stream; `fc_event_query` calls `cuEventQuery` and +returns 0 (done), 1 (CUDA_ERROR_NOT_READY = still running), or -1 (error). +The async serve scheduler uses events to poll all 8 card streams without +blocking any host thread — the alternative, `fc_sync_ctx`, would spin/yield a +core per card and starve other cards' launch turns on a 2-core host +(`fc_cuda.c:4383`). + +### 5.7 Graph capture (`fc_graph_*`, `fc_cuda.c:4296`) + +Optional (symbols checked at load; all six must be non-NULL or `fc_graph_begin_ctx` +returns -1). `fc_graph_begin_ctx` calls `cuStreamBeginCapture(stream, mode=0 += GLOBAL)`. `fc_graph_end_ctx` calls `cuStreamEndCapture` then +`cuGraphInstantiate`. `fc_graph_launch` replays via `cuGraphLaunch`. Used by the +decode-attention block for K80 launch-bound relief (one graph replay replaces +hundreds of `cuLaunchKernel` calls). + +### 5.8 Back-compat single-die path + +`fc_init()` creates a process-default context on device 0 (`_fc_default_ctx`). +All non-`_ctx` functions (`fc_buffer_from_ptr`, `fc_sgemm`, `fc_sync`, …) +delegate to it. Multi-die callers use the explicit `fc_cuda_ctx*` variants. + +--- + +## 6. Correctness notes — multi-card same-name bindings + +`pn_dm_bind` intentionally **does not prevent duplicate names** +(`die_manager.c:283`): N copies of a model on N different card-pairs all +register under the same name to support load-balanced serving. This is correct +as long as: + +1. **Disjoint die-sets:** enforced by the per-die `die_free_bytes < per_die` + check. If two bindings were attempted on overlapping dies, the second + would see reduced free space and refuse — provided the first binding's + `bytes_per_die` correctly covers the die's actual weight bytes. +2. **N releases required:** `pn_dm_release` is first-match; releasing a + multi-copy model requires exactly N calls (one per binding slot). A single + call frees only one copy's VRAM accounting. A caller that calls release once + and assumes the die is fully reclaimed will have a stale `vram_used`. +3. **`pn_dm_get` reports only the first bound model name** for a die + (`die_manager.c:167`); `n_models` counts all bindings touching that die. + Status consumers that read only `model_name` will not see co-resident + same-card models. + +No gap in the overlap-rejection path: the per-die room check is atomic under +the global lock, so two concurrent binds on the same die cannot both succeed +if there is only room for one. + +--- + +## 7. Second backend — Tenstorrent Blackhole p150a (`USE_TT=1`) + +CUDA is not the only accelerator behind the die manager. `pn_dm_init_tt()` +(`die_manager.h:114`) enumerates a **Tenstorrent Blackhole p150a** instead of +K80 dies; the host driver is `src/accel/tt/pn_tt_{device,residency,oncard}.c` +over Foundry's `tt/` backend. + +**Host:** any Linux machine with the card; the reference host is a small 2-core / 8 GB +box (no AVX) that also drives Tesla K80s. With tt-kmd the card appears as +`/dev/tenstorrent/`. + +### Build + +```bash +make USE_TT=1 # Linux only; adds -DUSE_TT -DFT_TT_PHASE1B_NOC_ENABLED + # writes ./pantheon-tt, then points ./pantheon at it +``` + +This build writes **`pantheon-tt`**, not `pantheon` — the backends have separate +output names now, so a TT build on a host that also runs CUDA can no longer +overwrite a deployed `pantheon-cuda`. It does re-point the `pantheon` symlink, +and on a host that also serves CUDA that has to be put back: +`ln -sfn pantheon-cuda pantheon` (no rebuild needed). Confirm with +`./pantheon --backend` before deploying anything. + +`FT_TT_PHASE1B_NOC_ENABLED` is deliberately **not** a second knob: without it +`ft_tt_mem_info()` cannot read a bank count and `pn_dm_init_tt()` refuses every +device, so a build could link the backend and then silently decline to see the +card. Device kernels are **not** built here — the kernel ELFs and Tensix programs +are pre-baked into `foundry/tt/ft_tt_kernels.h` / `ft_tt_programs.h`, the same +trick `cuda/` uses for PTX, so building needs no SFPI (SFPI is only needed to +rebuild those programs). + +**Run-time dependency: a way to reach the card, nothing else.** The default +transport is tt-kmd (`/dev/tenstorrent/`); `FT_TT_DRIVER=udrv` uses Foundry's +own userspace driver over PCI sysfs with no kernel module, which needs root or the +udev rule `foundry/tt/tools/60-foundry-blackhole.rules`. No tt-metal runtime, no +TT-NN, no Python, and no C++ anywhere — host or device. That boundary is the +point of the backend, not an incidental property. Foundry's TT layer was rewritten +from public documentation and measured behaviour; no tt-metal, TT-NN, tt-umd or +tt-llk source is compiled, linked or vendored. The vLLM/tt-metal stack cannot +run on the reference host at all: 2 cores / 8 GB and **no AVX**, so TT's prebuilt +wheels SIGILL. + +### Runtime knobs + +| Variable | Effect | +|---|---| +| `PN_TT_PROGRAM_DIR` | **override only.** Read Tensix programs from this directory instead of the ones embedded in the binary. Unset — the default — means embedded. An override that cannot be satisfied REFUSES; it never falls back to embedded | +| `PN_TT_DISPATCH` | dispatch-path selection | +| `PN_TT_ONCARD` | on-card op families (reduce / eltwise / top-k) | +| `PN_TT_CORE`, `PN_TT_CORE_{REDUCE,ELTWISE,SAMPLE}` | pin a residency to a Tensix index | + +### What it can serve today + +`pantheon serve` runs **MiniCPM5-1B** on the card over the OpenAI-compatible +endpoint, multi-turn, across idle gaps: **4.37 tok/s best, 4.14 sustained** +(2026-08-15). Eight op families are green on silicon. + +Three limits to plan around: + +1. **Everything measured is ONE core of 140. Multi-core is not built.** The + forward runs 169 matvecs on a single matmul core. The figure quoted + elsewhere as *8.94 decode tok/s* is **composed** from measured per-op costs + (3.745 ms/layer × 24 + 22.0 ms lm_head) — **no end-to-end forward pass has + been run to produce it.** The 4.37 tok/s above is the measured end-to-end + number. +2. **MoE is not built.** Routed-MoE models (qwen35moe, e.g. Nex-N2) do not run + on the card. The dense qwen35 hybrid (Gated-DeltaNet + attention, e.g. + Ornith) does, byte-identical to the CPU forward (`engine.c`, `PN_ARCH_BONSAI` + branch; Foundry `ft_tt_op_gdn`). +3. **Open correctness defect.** A chat-templated prompt — i.e. every request a + serve receives — costs **+22.1% perplexity** and disagrees with the CPU + oracle on **16.9%** of decode steps. Mechanism unidentified. + +Weights are not GGUF on the card: `foundry/tt/convert/` turns a GGUF into a +tiled **FTTW** artifact (bf16 / Bfp8_b / Bfp4_b) and the loader is a byte copy. +**BFP4 is refuted on quality** (+40.2% perplexity) and is no faster than BFP8. + +### Operational hazards — these cost hardware, not a retry + +- **A wrong NoC coordinate hangs the chip and costs a physical power cycle.** + Coordinates come from ARC telemetry; never probe or guess one. +- **`PHASE1B=1` is mandatory for on-card builds**; off-hardware suites are + built without the NoC fence so they cannot reach the card even when wrong. +- **Never assert the Tensix BACKEND soft-reset bits** — it silently and + permanently corrupts that core's compute until a host reboot. +- **Retire by asking, never by reset** (`FT_TT_QUEUE_OP_STOP` / `stop_request`). + `assert_reset` on a blocked resident kernel is what wedges cores, and a + resident triad must never be parked. +- **Never more than 4 destinations per multicast** — 5 wedges reliably. +- **On-card runs are detached and polled, never held over an SSH session.** +- **Card→host DMA does not exist on Blackhole.** Host→card is 287 MB/s on the + reference host's x1 Gen2 link and readback is 2.3–4.7 MB/s, so on-card sampling is + mandatory rather than an optimisation. **A card reset drops the link to + Gen1** — retrain and verify afterwards, or every host-involved number is + silently halved. +- **On-card residencies are bounded** and exit on an idle spin limit — and a + serve idles by definition. `pn_tt_oncard_rearm()` is what recovers them. + +Full detail: `foundry/tt/DEV_NOTES.md` (the authority on hazards), +`foundry/tt/README.md`, `foundry/docs/tenstorrent.md`. diff --git a/docs/architecture/core-engine.md b/docs/architecture/core-engine.md new file mode 100644 index 0000000..c73699d --- /dev/null +++ b/docs/architecture/core-engine.md @@ -0,0 +1,180 @@ +# Core Engine API + +Source: `src/core/engine.c`, `src/core/engine.h`. + +Covers the loading surface (`pn_load`/`pn_load_on`), the model and state structs, the async forward-step seam, scratch estimation, and the admission helpers at the engine level. The memory-aware layer that gates admission is documented separately in `memory-aware.md`. + +--- + +## Public API surface + +### Loading + +```c +// Thin wrappers — all paths converge on pn_load_on. +pn_model* pn_load(const char *path); // engine.c:412 +pn_model* pn_load_ex(const char *path, const pn_load_opts *opts); // engine.c:549 +pn_model* pn_load_on(const char *path, const pn_load_opts *opts, // engine.c:553 + const int *dies, int n_dies); +``` + +`pn_load` and `pn_load_ex` are thin wrappers that call `pn_load_on` with `dies=NULL, n_dies=0`. + +`pn_load_on` is the placement-aware entry point added for multi-model serving (Phase 7). When `dies` is non-NULL the die list is recorded on the model before any GPU upload (`model_record_dies` at engine.c:538), so the subsequent `pn_qwen35moe_gpu_upload` call reads the correct target dies via `pn_qwen35moe_model_set_dies`. + +**Load opts:** +```c +typedef struct { + const char *vocab_path; // RWKV World tokenizer path + const char *config_path; // HF config.json for Qwen/LLaMA safetensors + bool quantize; // INT8-quantize at load time (RWKV-X only) +} pn_load_opts; // engine.h:116 +``` + +### pn_model struct (engine.h:74) + +```c +struct pn_model { + pn_config config; // parsed dimensions (stack) + void *weights; // arch-specific weight struct + pn_forward_fn forward; // forward pass function pointer + ft_tokenizer *tokenizer; // BPE/SP tokenizer (NULL for RWKV-X) + char *path; + int chat_style; + void *vision; // pn_vision_model* for VLMs + struct fw_pool *pool; // thread pool (USE_THREADS only) + int gpu_dies[PN_MODEL_MAX_DIES]; // explicit die placement (pn_load_on) + int n_gpu_dies; // 0 = none, env/defaults apply +}; +``` + +`forward` is the architecture dispatch: a function pointer of type `tm_tensor* (*)(pn_model*, uint32_t token, pn_state*)`. The CPU and CUDA forwards are distinct pointers; `pn_admit_gpu_resident` identifies the CUDA path by comparing `model->forward == pn_qwen35moe_forward_cuda` (engine.c:441). + +### pn_state struct (engine.h:89) + +```c +struct pn_state { + void *kv_cache; // arch-specific: pn_llama_kv_cache / pn_qwen35moe_state / rx_state + int pos; // current sequence position + pn_config *config; // pointer to model config + struct pn_arena *arena; // bump-allocator scratch (NULL for RWKV-X and Qwen35MOE) + const float *embd_override; // if set, forward uses this embedding (image token) +}; +``` + +`pn_state_new` allocates architecture-specific state: +- **Llama/Phi/Mistral/Mixtral** (engine.c:870): allocates `pn_llama_kv_cache` and an arena sized by `compute_arena_size`. +- **Qwen3.5-MoE** (engine.c:860): allocates `pn_qwen35moe_state` (dual state: SSM conv/ssm + attention KV). No arena. +- **RWKV-X** (engine.c:849): allocates `rx_state` (recurrent + KV + workspace). No arena. + +--- + +## Loading paths + +`pn_load_on` branches on file format: + +### Safetensors + +1. If `opts->config_path` is set: HF/ternary safetensors loader → `pn_llama_load_safetensors` + `pn_llama_load_hf_tokenizer`. Sets `model->forward = pn_llama_forward` (engine.c:579). +2. Otherwise: RWKV-X path → `pn_rwkvx_load` + optional vocab. Sets `model->forward = pn_rwkvx_forward` (engine.c:627). + +### GGUF + +Architecture detected from `general.architecture` metadata (engine.c:226). For Qwen3.5-MoE specifically (engine.c:694): + +1. `pn_qwen35moe_load(gguf, &config)` — zero-copy mmap weights. +2. Explicit die placement applied to the arch model: `pn_qwen35moe_model_set_dies(qw, model->gpu_dies, model->n_gpu_dies)` (engine.c:704). +3. `fc_init()` — idempotent CUDA init; no-op without a GPU. +4. `pn_qwen35moe_gpu_upload(qw)` — uploads weights to VRAM; on success sets `model->forward = pn_qwen35moe_forward_cuda` (engine.c:716). Falls back silently to `pn_qwen35moe_forward` (CPU) if the upload fails or no GPU is present. + +--- + +## Scratch (arena) estimation + +`compute_arena_size` (engine.c:357) sizes the per-state bump allocator for transformer models: + +- **Per layer**: `5*n_embd + 3*n_ff + 2*n_kv_head*head_dim` floats. MoE adds `n_experts + 2*n_experts_used + 4*n_embd + 2*n_ff` floats. +- **One-time**: `n_embd + n_head*max_seq + n_embd + n_vocab` floats. +- **max_seq cap**: `min(n_ctx, sliding_window if > 0, 4096)` — the scores buffer is bounded at 4096 (engine.c:374). +- **Alignment overhead**: 16 bytes × (n_layer×11+5) allocations, plus an additional 5 per layer for MoE. +- **Safety margin**: +25% (engine.c:405). + +The arena resets between forward calls; nothing in it persists across tokens. + +--- + +## Async forward-step seam (C2) + +For the batched GPU serving path a non-blocking single-token step is exposed (engine.h:228): + +```c +int pn_forward_step_begin(pn_model *model, uint32_t token, pn_state *state); +int pn_forward_step_poll(pn_model *model, pn_state *state, tm_tensor **logits_out); +``` + +**begin** (engine.c:1173): dispatches to `pn_qwen35moe_forward_step_begin` when `model->forward == pn_qwen35moe_forward_cuda`; returns 1 ("unsupported") for everything else — the caller uses `model->forward` blocking path instead. + +**poll** (engine.c:1182): dispatches to `pn_qwen35moe_forward_step_poll`; returns 1 while running, 0 when done (writes `*logits_out`), -1 on error. Returns -1 for unsupported models. + +The design: each connection thread owns one model slot and parks in a poll loop between `begin` and `done`. Because `begin` returns without blocking, the 2-core K80 host can keep multiple cards' steps in flight simultaneously. The per-model `gen_lock` in the serve layer guarantees one in-flight step per model at a time. + +--- + +## Admission helpers (engine-level) + +These live in `engine.c:426–534` and are the engine-level face of the memory-aware admission system (full detail in `memory-aware.md`). + +### pn_admit (engine.c:481) + +```c +pn_admit_status pn_admit(pn_model *model, int n_tokens, int *fit_tokens_out); +``` + +Called from the serve layer before any prefill work. Returns `PN_ADMIT_FIT`, `PN_ADMIT_CAPPED`, or `PN_ADMIT_DENIED`. + +Steps: +1. Fail-open if `!pn_memwatch_running()` — returns `FIT` with no check (engine.c:487). +2. `pn_memwatch_read(&st)` — reads the live seqlock-protected snapshot. +3. Determines `kv_q4` (env `QWEN35_KV_Q4`), `gpu_resident` (`forward == pn_qwen35moe_forward_cuda`), `die_id` (tightest-free-VRAM die), and `max_window` (clamped by `QWEN35_MAX_SEQ` and `model->config.n_ctx`). +4. **Ladder rung 3** (engine.c:504): calls `pn_ladder_plan_from_state` and, if `ladder.prefill_window < max_window`, clamps `max_window` and logs `[ladder] TIGHT -> prefill window clamped 16384 -> 8192`. +5. Calls `pn_admit_request` with the adjusted `max_window` and the full `pn_mem_state`. +6. Maps `PN_ADMIT_CAP` → `PN_ADMIT_CAPPED` (sets `*fit_tokens_out`), `PN_ADMIT_REJECT` → `PN_ADMIT_DENIED`. + +### Internal helpers (engine.c:427–479) + +| Helper | Purpose | +|---|---| +| `pn_admit_kv_q4()` | Returns `true` unless `QWEN35_KV_Q4=0` | +| `pn_admit_gpu_resident(model)` | `true` iff `model->forward == pn_qwen35moe_forward_cuda` | +| `pn_admit_max_window(model)` | Reads `QWEN35_MAX_SEQ`, clamps to `PN_QWEN35MOE_MAX_SEQ_CAP` and `config.n_ctx` | +| `pn_admit_worst_die(st)` | Finds the die with the least free VRAM (the conservative scheduling target) | + +--- + +## Sampler seam + +```c +uint32_t pn_sample_next_token(float *logits, int n_vocab, + pn_sampling_params *params, + uint32_t *recent_tokens, int n_recent); // engine.c:1160 +``` + +Public wrapper exposing the internal `sample_token` to the serve layer, so it does not re-implement sampling. Mutates `logits[]` in place. Not thread-safe (shared thread-local scratch buffer per vocab size). + +--- + +## Architecture registry + +`pn_get_forward_fn(pn_arch arch)` (engine.c:214) maps arch enum → forward function pointer. Llama, Qwen2, Mistral, Mixtral all share `pn_llama_forward`; only Phi and Qwen3.5-MoE have distinct implementations. + +--- + +## State lifecycle + +``` +pn_state_new(model) → allocates kv_cache + arena (arch-specific) +pn_state_reset(state) → zeros kv tensors, resets pos to 0 (engine.c:923) +pn_state_free(state) → frees kv_cache, arena, struct +``` + +`pn_state_reset` for Llama/Phi explicitly zeros every K and V tensor in the cache (engine.c:936–943). For Qwen3.5-MoE and RWKV-X it delegates to arch-specific reset functions. diff --git a/docs/architecture/engine.md b/docs/architecture/engine.md new file mode 100644 index 0000000..9dae1c2 --- /dev/null +++ b/docs/architecture/engine.md @@ -0,0 +1,229 @@ +# Engine Architecture + +The engine (`src/core/engine.c`) is Pantheon's central coordinator. It handles model loading, architecture dispatch, tokenization, sampling, and the generation loop. + +## Model Loading Pipeline + +Two entry points: `pn_load(path)` for GGUF models (legacy), and `pn_load_ex(path, opts)` for all formats including safetensors. + +``` +pn_load_ex(path, opts) + ├── pn_is_safetensors(path)? + │ ├── yes → RWKV-X path: + │ │ ├── pn_rwkvx_load(path, config, quantize) load safetensors weights + │ │ ├── pn_rwkvx_load_tokenizer(weights, vocab) load trie tokenizer from vocab file + │ │ ├── forward = pn_rwkvx_forward set forward function + │ │ └── tokenizer = NULL (RWKV-X uses internal trie tokenizer) + │ │ + │ └── no → GGUF path: + │ ├── pn_gguf_open(path) mmap file, parse header + metadata + │ ├── detect arch from metadata "general.architecture" → pn_arch enum + │ ├── pn_llama_load_qweights(gguf) load weights (architecture-specific) + │ ├── extract tokenizer from GGUF tokens + merges/scores arrays + │ ├── pn_get_forward_fn(arch) select forward pass function pointer + │ └── detect chat template style + │ + └── create thread pool (if USE_THREADS=1) +``` + +`pn_load(path)` is equivalent to `pn_load_ex(path, NULL)`. + +### Load Options + +```c +typedef struct { + const char *vocab_path; /* RWKV World tokenizer (required for safetensors) */ + bool quantize; /* INT8 quantize at load time */ +} pn_load_opts; +``` + +### Format Detection + +| Function | Detection Method | +|----------|-----------------| +| `pn_is_gguf(path)` | First 4 bytes = `"GGUF"` magic | +| `pn_is_safetensors(path)` | 8-byte LE header length < 10M, followed by `{` JSON | + +### Architecture Detection + +GGUF models: the `general.architecture` metadata key maps to: + +| Value | Enum | Forward Pass | +|-------|------|-------------| +| `"llama"` | `PN_ARCH_LLAMA` | `pn_llama_forward` | +| `"qwen2"` | `PN_ARCH_QWEN2` | `pn_llama_forward` (shared) | +| `"phi"`, `"phi3"` | `PN_ARCH_PHI` | `pn_phi3_forward` (separate) | +| `"mistral"` | `PN_ARCH_MISTRAL` | `pn_llama_forward` (shared) | +| `"llama"` + `expert_count > 0` | `PN_ARCH_MIXTRAL` | `pn_llama_forward` (shared, MoE FFN) | +| unknown | `PN_ARCH_UNKNOWN` | falls back to Llama | + +Safetensors models: architecture is always `PN_ARCH_RWKVX` (currently the only safetensors architecture). + +Qwen2 and Mistral share the Llama forward pass — architecture differences (attention biases, sliding window, metadata key prefixes) are handled by config flags. + +Phi-3 has its own forward pass (`src/models/phi3.c`) due to fused QKV weights, fused gate+up weights, and NeoX-style RoPE that differ significantly from the Llama code path. + +RWKV-X has its own complete engine (`src/models/rwkvx.c`) — a hybrid recurrent + sparse attention architecture that is fundamentally different from transformer models. See [RWKV-X Architecture](rwkvx.md) for details. + +### Config Parsing + +Model dimensions are read from GGUF metadata using architecture-specific key prefixes: + +``` +llama.* → Llama, Mistral (Mistral GGUFs use llama.* keys) +qwen2.* → Qwen2/Qwen2.5 +phi3.* → Phi-3 (read by phi3.c's own config reader) +``` + +Key parameters: + +| Config Field | GGUF Key | Default | +|-------------|----------|---------| +| `n_vocab` | `{arch}.embedding_length` | - | +| `n_embd` | `{arch}.embedding_length` | - | +| `n_head` | `{arch}.attention.head_count` | - | +| `n_kv_head` | `{arch}.attention.head_count_kv` | `n_head` | +| `n_layer` | `{arch}.block_count` | - | +| `n_ff` | `{arch}.feed_forward_length` | - | +| `n_ctx` | `{arch}.context_length` | 2048 | +| `rope_theta` | `{arch}.rope.freq_base` | 10000.0 | +| `rms_norm_eps` | `{arch}.attention.layer_norm_rms_epsilon` | 1e-5 | +| `sliding_window` | `{arch}.attention.sliding_window` | 0 (disabled) | + +## Tokenizers + +Pantheon supports three tokenizer types, routing automatically based on model architecture. + +### GGUF Tokenizer Extraction + +For GGUF models, tokenizer data is extracted from metadata arrays: + +- `tokenizer.ggml.tokens` — vocabulary (string array) +- `tokenizer.ggml.merges` — BPE merge rules (string array, BPE only) +- `tokenizer.ggml.scores` — unigram scores (float array, SentencePiece only) +- `tokenizer.ggml.bos_token_id` — beginning-of-sequence token +- `tokenizer.ggml.eos_token_id` — end-of-sequence token +- `tokenizer.ggml.model` — tokenizer type (`"gpt2"` = BPE, `"llama"` = SentencePiece) + +### Tokenizer Types + +| Type | Models | Encoding | BOS | Vocab | +|------|--------|----------|-----|-------| +| BPE | SmolLM2, Qwen2.5, Mistral v0.3 | GPT-2 byte encoding + BPE merges | No | Varies | +| SentencePiece | Mistral v0.1, Llama 1/2 | Leading space prepend + Viterbi unigram | Yes (auto) | Varies | +| RWKV World | RWKV-X | Trie-based greedy longest-match on UTF-8 bytes | No | 65,536 | + +BPE and SentencePiece tokenizers use Foundry's `ft_tokenizer` and are stored in `pn_model->tokenizer`. + +The RWKV World tokenizer is fundamentally different — it's a trie-based tokenizer that operates on raw UTF-8 bytes with greedy longest-match. It's stored inside the RWKV-X internal model struct (accessed via `pn_rwkvx_encode`/`pn_rwkvx_decode`), and `pn_model->tokenizer` is NULL for RWKV-X models. The engine's `pn_generate()`/`pn_stream()` functions detect `PN_ARCH_RWKVX` and route to the internal tokenizer automatically. + +## Generation Loop + +``` +pn_generate(model, state, prompt, max_tokens, params) + ├── encode prompt → token IDs + │ ├── BPE: gpt2_bytes_encode(prompt) → ft_encode() + │ └── SentencePiece: ft_encode_with_special(prompt, add_bos=true) + │ + ├── prefill: for each prompt token + │ └── logits = model->forward(model, token, state) + │ + ├── decode: for i in 0..max_tokens + │ ├── next_token = sample_token(logits, params) + │ ├── if next_token == EOS: break + │ └── logits = model->forward(model, next_token, state) + │ + └── decode token IDs → output string +``` + +### Sampling + +Applied in order: + +1. **Temperature** — `logits[i] /= temperature` (0 = greedy argmax) +2. **Repetition penalty** — penalize tokens in the recent window (`repeat_last_n`) +3. **Top-k** — keep only the k highest-probability tokens +4. **Top-p (nucleus)** — keep tokens until cumulative probability exceeds p +5. **Softmax** — convert to probabilities +6. **Multinomial sampling** — weighted random selection + +## Vision-Language and Bulk OCR + +The vision-language path (`pn_describe` / `pn_describe_stream`) is split into a vision-encode seam and a decode seam so a bulk pipeline can stage work and run vision and decode on different units. + +``` +pn_describe_stream(image, prompt) + ├── pn_vision_encode_rgb(image) → [n_img x n_embd] decoder-space embeds + └── pn_describe_embeds(embeds, prompt) + ├── build ChatML vision turn: + │ <|vision_start|> + <|image_pad|> * n_img + <|vision_end|> + prompt + ├── splice the supplied embeds at the image-pad positions + ├── batched prefill (pn_llama_prefill) + └── decode loop → text +``` + +- `pn_vision_encode_rgb` runs just the CPU vision encoder for one image and returns the `[n_img x n_embd]` embeddings (caller owns them). Splitting it out lets a bulk caller encode ahead of decode, or substitute an external encoder (e.g. a Core ML GPU or hand-rolled Metal ViT) that produces the same `[n_img x n_embd]` buffer. +- `pn_describe_embeds` is the decode seam: it takes pre-supplied embeds instead of encoding, and is what `pn_describe_stream` now delegates to after its own encode. The portable single-image path is unchanged. + +### Continuous-Batch VLM Decoder + +For bulk OCR (many pages at once), the pipeline drives many sequences through the batched decode (`pn_llama_forward_batch`) at the higher aggregate throughput described in [Llama Forward Pass](llama-forward.md#batched-multi-sequence-decode). + +- `pn_vlm_prefill_embeds` factors the shared prefill out of `pn_describe_embeds`: it prefills a state's KV cache from image embeds (the ChatML vision turn) and samples the first token, but does not run the decode loop. It leaves the state ready to hand to the batch decoder. +- `pn_batch_decoder` admits prefilled pages into up to `max_batch` slots. Each `step()` runs one `pn_llama_forward_batch` over all active slots with per-sequence sampling, EOS detection, and detokenization, and returns each page that finished this step as a `pn_page_out` (`page_id`, the now-recyclable `pn_state`, and the decoded text). The caller slots a fresh page into each freed slot (continuous batching). `flush()` drains all active slots for early stop. + +``` +for each page: pn_vision_encode_rgb → pn_vlm_prefill_embeds → pn_batch_decoder_admit +loop while active > 0: + n = pn_batch_decoder_step(out, max_out) # one M=B decode over all slots + for each finished page in out: emit text, recycle state, admit next page +``` + +The batched decoder is byte-identical to sequential `pn_describe_embeds`: two pages with different embeds decoded together reproduce each page's standalone greedy transcription and finish at their own lengths (verified by `test_batch_decoder`). If the model does not support batched decode (MoE, F32 legacy, ternary), `pn_batch_decoder_step` returns -1 and the caller falls back to the sequential path. + +## Memory Model + +``` +pn_model + ├── config model dimensions (stack allocated) + ├── weights architecture-specific (heap, points into mmap or owned memory) + ├── tokenizer BPE/SentencePiece tokenizer (heap, NULL for RWKV-X) + ├── forward forward pass function pointer + ├── path model file path (heap) + ├── chat_style chat template enum + └── pool thread pool (optional, heap) + +pn_state + ├── kv_cache architecture-specific state: + │ GGUF models: pn_llama_kv_cache (KV tensors per layer) + │ RWKV-X: rx_state (recurrent state + KV cache + workspace) + ├── pos current sequence position + ├── config pointer to model's config + └── arena bump allocator for per-token scratch (NULL for RWKV-X) +``` + +For GGUF models, the arena allocator eliminates per-token `malloc`/`free` calls during inference. All intermediate buffers (projections, activations, attention scores) are allocated from the arena, which resets to zero after each token. This removes ~330 allocations per token in the forward pass. + +For RWKV-X models, the engine uses its own pre-allocated workspace with ping-pong buffers. The arena is not used. + +## Weight Loading Modes + +The engine supports two weight storage modes: + +### Quantized (Phase 2, default) + +Weights stay in their original GGUF quantized format, pointing directly into mmap'd memory. Dequantization happens per-block during matrix-vector multiply (`pn_qmv`). Only embeddings and layer norms are dequantized to F32 on load. + +Benefits: ~4x less RAM for Q4 models, zero-copy weight loading. + +### F32 Legacy (Phase 1 fallback) + +All weights dequantized to F32 `tm_tensor` on load. Used as fallback for unsupported quant types. Higher memory usage but simpler code path. + +## Thread Pool + +When built with `USE_THREADS=1`, a thread pool is created at model load. The main thread participates in work (no idle main thread). Currently used for: + +- Parallel attention head computation — each head runs independently via `fw_for(pool, n_heads, fn, arg)` + +Thread count is auto-detected from hardware. diff --git a/docs/architecture/gguf.md b/docs/architecture/gguf.md new file mode 100644 index 0000000..c9896ea --- /dev/null +++ b/docs/architecture/gguf.md @@ -0,0 +1,151 @@ +# GGUF Format Support + +Pantheon's GGUF parser (`src/formats/gguf.c`) provides zero-copy model loading via mmap. Both GGUF v2 and v3 are supported. + +## File Layout + +``` +┌──────────────────────────┐ +│ Magic: "GGUF" (4 bytes) │ +│ Version: uint32 │ v2 or v3 +│ Tensor count: uint64 │ +│ Metadata count: uint64 │ +├──────────────────────────┤ +│ Metadata entries │ typed key-value pairs +│ (variable length) │ +├──────────────────────────┤ +│ Tensor info entries │ name, dims, quant type, offset +│ (variable length) │ +├──────────────────────────┤ +│ Padding │ aligned to 32 bytes (GGUF_DEFAULT_ALIGNMENT) +├──────────────────────────┤ +│ Tensor data (binary) │ mmap'd, accessed via offset +└──────────────────────────┘ +``` + +### Alignment + +The tensor data section starts at a 32-byte aligned offset from the file start. This is the GGUF spec default (`GGUF_DEFAULT_ALIGNMENT = 32`). Files can override this via the `general.alignment` metadata key. + +### v2 vs v3 + +Both versions use the same binary layout. The parser accepts either. v3 added big-endian support (Pantheon assumes little-endian). The version field is stored in `pn_gguf_file.version`. + +## Metadata Types + +13 value types supported: + +| Type | ID | Size | +|------|----|------| +| UINT8 | 0 | 1 | +| INT8 | 1 | 1 | +| UINT16 | 2 | 2 | +| INT16 | 3 | 2 | +| UINT32 | 4 | 4 | +| INT32 | 5 | 4 | +| FLOAT32 | 6 | 4 | +| BOOL | 7 | 1 | +| STRING | 8 | 8 + len | +| ARRAY | 9 | variable | +| UINT64 | 10 | 8 | +| INT64 | 11 | 8 | +| FLOAT64 | 12 | 8 | + +Strings are length-prefixed (uint64 length + raw bytes). Arrays contain a type tag + uint64 count + elements. + +## Quantization Types + +### Implemented (full dequant + fused matvec) + +| Type | ID | Bits/Weight | Block Size | Bytes/Block | Notes | +|------|----|-------------|------------|-------------|-------| +| F32 | 0 | 32 | 1 | 4 | Passthrough | +| F16 | 1 | 16 | 1 | 2 | IEEE 754 half | +| Q4_0 | 2 | 4 | 32 | 18 | scale + 4-bit nibbles | +| Q8_0 | 8 | 8 | 32 | 34 | scale + int8 values | +| Q4_K | 12 | 4.5 | 256 | 144 | 8 sub-blocks, 6-bit scales | +| Q5_K | 13 | 5.5 | 256 | 176 | 5-bit values (4 low + 1 high) | +| Q6_K | 14 | 6.5 | 256 | 210 | 6-bit values (4 low + 2 high) | + +### Format-only (defined but no dequant kernel) + +Q4_1, Q5_0, Q5_1, Q8_1, Q2_K, Q3_K, Q8_K + +### Mixed Quantization (Q4_K_M, Q5_K_M) + +GGUF files labeled "Q4_K_M" use Q4_K for most layers with Q6_K for attention output and FFN down projections. This is handled transparently — each tensor has its own quant type, and `pn_qmv` dispatches per-row based on the weight's type field. + +## Key Metadata Keys + +### Model Architecture + +| Key | Type | Description | +|-----|------|-------------| +| `general.architecture` | string | `"llama"`, `"qwen2"`, `"phi"`, `"mistral"` | +| `general.name` | string | Human-readable model name | +| `general.alignment` | uint32 | Data section alignment (default 32) | + +### Model Dimensions + +Prefixed by architecture name (`llama.*`, `qwen2.*`, etc.): + +| Key | Type | Description | +|-----|------|-------------| +| `{arch}.embedding_length` | uint32 | Hidden size (n_embd) | +| `{arch}.attention.head_count` | uint32 | Query heads | +| `{arch}.attention.head_count_kv` | uint32 | KV heads (for GQA) | +| `{arch}.block_count` | uint32 | Number of layers | +| `{arch}.feed_forward_length` | uint32 | FFN intermediate size | +| `{arch}.context_length` | uint32 | Max context length | +| `{arch}.rope.freq_base` | float32 | RoPE theta (default 10000) | +| `{arch}.attention.layer_norm_rms_epsilon` | float32 | RMSNorm epsilon | +| `{arch}.attention.sliding_window` | uint32 | Sliding window size (Mistral) | + +### Tokenizer + +| Key | Type | Description | +|-----|------|-------------| +| `tokenizer.ggml.model` | string | `"gpt2"` (BPE) or `"llama"` (SentencePiece) | +| `tokenizer.ggml.tokens` | string[] | Vocabulary tokens | +| `tokenizer.ggml.merges` | string[] | BPE merge rules (BPE only) | +| `tokenizer.ggml.scores` | float[] | Unigram scores (SentencePiece only) | +| `tokenizer.ggml.bos_token_id` | uint32 | BOS token ID | +| `tokenizer.ggml.eos_token_id` | uint32 | EOS token ID | + +## Tensor Naming Convention + +GGUF tensors follow the pattern `blk.{layer}.{component}.weight`: + +``` +token_embd.weight embedding table +blk.0.attn_norm.weight pre-attention RMSNorm +blk.0.attn_q.weight query projection +blk.0.attn_k.weight key projection +blk.0.attn_v.weight value projection +blk.0.attn_output.weight output projection +blk.0.attn_q.bias query bias (Qwen2 only) +blk.0.ffn_norm.weight pre-FFN RMSNorm +blk.0.ffn_gate.weight SwiGLU gate projection +blk.0.ffn_up.weight SwiGLU up projection +blk.0.ffn_down.weight down projection +output_norm.weight final RMSNorm +output.weight LM head (may be tied to token_embd) +``` + +## Quantized Weight Loading + +Weights are loaded as `pn_qweight` structs that point directly into mmap'd data: + +```c +typedef struct { + const void *data; /* raw quantized blocks in mmap */ + pn_gguf_quant type; /* quant format */ + int rows; /* output dimension */ + int cols; /* input dimension */ + size_t row_stride; /* bytes per row */ +} pn_qweight; +``` + +GGUF stores dimensions innermost-first. The loader reverses them to C outermost-first: `dims[n_dims-1]` = rows, `dims[0]` = cols. `row_stride` is computed from cols and the quant format's bytes-per-block. + +Matrix-vector multiply (`pn_qmv`) iterates rows, dequantizing one block (32 or 256 elements) at a time and accumulating the dot product. No full dequantization ever occurs for projection weights. diff --git a/docs/architecture/llama-forward.md b/docs/architecture/llama-forward.md new file mode 100644 index 0000000..bf80520 --- /dev/null +++ b/docs/architecture/llama-forward.md @@ -0,0 +1,264 @@ +# Llama Forward Pass + +The Llama forward pass (`src/models/llama.c`) is shared by Llama, Qwen2, Mistral, and Mixtral architectures. Architecture-specific differences (attention biases, sliding window, MoE routing, metadata keys) are handled by config flags, not separate code paths. + +Phi-3 has its own forward pass in `src/models/phi3.c` due to fused QKV/gate+up weights and NeoX-style RoPE. See the architecture differences table below. + +## Forward Pass Flow + +``` +token (uint32) + │ + ├── embed: tok_embd[token] → hidden[n_embd] + │ + ├── for each layer (0..n_layer-1): + │ │ + │ ├── RMSNorm(hidden, attn_norm) + │ │ + │ ├── Attention: + │ │ ├── Q = Wq @ normed (+bias for Qwen2) + │ │ ├── K = Wk @ normed (+bias for Qwen2) + │ │ ├── V = Wv @ normed (+bias for Qwen2) + │ │ ├── RoPE(Q, K, pos, rope_theta) + │ │ ├── Write K,V to KV cache at pos + │ │ ├── GQA: for each query head + │ │ │ ├── scores = Q_head @ K_cache^T / sqrt(head_dim) + │ │ │ ├── causal mask (future tokens = -inf) + │ │ │ ├── softmax(scores) + │ │ │ └── output = scores @ V_cache + │ │ └── O = Wo @ concat(heads) (+bias for Qwen2) + │ │ + │ ├── residual: hidden += O + │ │ + │ ├── RMSNorm(hidden, ffn_norm) + │ │ + │ ├── FFN (standard) or MoE FFN (Mixtral): + │ │ │ + │ │ ├── Standard SwiGLU (n_experts == 0): + │ │ │ ├── gate = W_gate @ normed + │ │ │ ├── up = W_up @ normed + │ │ │ ├── act = silu(gate) * up + │ │ │ └── down = W_down @ act + │ │ │ + │ │ └── MoE routing (n_experts > 0, Mixtral): + │ │ ├── gate_logits = W_router @ normed → [n_experts] + │ │ ├── probs = softmax(gate_logits) + │ │ ├── top_k = select top n_experts_used + │ │ ├── weights = renormalize(selected probs) + │ │ ├── for each selected expert e: + │ │ │ ├── gate_e = expert[e].W_gate @ normed + │ │ │ ├── up_e = expert[e].W_up @ normed + │ │ │ ├── act_e = silu(gate_e) * up_e + │ │ │ └── out_e = expert[e].W_down @ act_e + │ │ └── down = Σ (weight_e * out_e) + │ │ + │ └── residual: hidden += down + │ + ├── RMSNorm(hidden, output_norm) + │ + └── logits = W_output @ normed → float[n_vocab] +``` + +## Key Operations + +### RMSNorm + +``` +rms = sqrt(mean(x^2) + eps) +out = (x / rms) * weight +``` + +Implemented as a single SIMD kernel (`pn_vrms_norm_f32`). Default `eps = 1e-5`, configurable per model via GGUF metadata. + +### RoPE (Rotary Position Embeddings) + +Applies learned rotation to Q and K vectors based on position: + +``` +for each pair (i, i+1) in head: + freq = 1.0 / (theta ^ (2i / head_dim)) + angle = pos * freq + q[i], q[i+1] = rotate(q[i], q[i+1], angle) +``` + +`rope_theta` defaults to 10000 (Llama). Qwen2.5 uses 1,000,000 for extended context. + +### Grouped Query Attention (GQA) + +GQA reduces KV heads relative to query heads. Each KV head is shared across `n_head / n_kv_head` query heads: + +| Model | Q Heads | KV Heads | Ratio | +|-------|---------|----------|-------| +| SmolLM2-135M | 9 | 3 | 3:1 | +| Qwen2.5-0.5B | 14 | 2 | 7:1 | +| Mistral-7B | 32 | 8 | 4:1 | +| Phi-3-mini | 32 | 32 | 1:1 (MHA) | + +The forward pass maps query head `h` to KV head `h * n_kv_head / n_head`. + +### SwiGLU FFN + +``` +gate = W_gate @ x +up = W_up @ x +out = W_down @ (silu(gate) * up) +``` + +The `silu(gate) * up` step is fused into a single SIMD kernel (`pn_vsilu_mul_f32`). + +### Sliding Window (Mistral) + +When `sliding_window > 0`, the KV cache operates as a circular buffer: + +- Write position: `pos % max_seq` (wraps around) +- Readable length: `min(pos + 1, max_seq)` (prevents reading uninitialized slots) +- `max_seq = min(sliding_window, n_ctx)` + +This limits memory to a fixed window size regardless of sequence length. Standard Llama/Qwen2 use `sliding_window = 0` (full context). + +### Mixture of Experts (Mixtral) + +When `n_experts > 0`, the FFN sublayer is replaced with expert routing: + +``` +normed = RMSNorm(hidden) +gate_logits = W_router @ normed → [n_experts] +probs = softmax(gate_logits) +top_idx, top_wt = top_k(probs, n_experts_used) → select best experts +top_wt = renormalize(top_wt) → weights sum to 1 + +ffn_out = 0 +for each selected expert e: + gate = expert[e].W_gate @ normed + up = expert[e].W_up @ normed + hid = silu(gate) * up + out = expert[e].W_down @ hid + ffn_out += top_wt[e] * out + +hidden += ffn_out +``` + +**Expert weight formats in GGUF:** + +Merged (most common): 3D tensors sliced into per-expert views (zero-copy mmap) +- `blk.N.ffn_gate_exps.weight` `[n_experts, n_ff, n_embd]` +- `blk.N.ffn_up_exps.weight` `[n_experts, n_ff, n_embd]` +- `blk.N.ffn_down_exps.weight` `[n_experts, n_embd, n_ff]` (transposed) + +Split (older): individual tensors per expert +- `blk.N.ffn_gate.E.weight` `[n_ff, n_embd]` for E=0..n_experts-1 + +The loader probes for merged format first, falls back to split. + +## Architecture Differences + +### Shared Llama Forward Pass (llama.c) + +| Feature | Llama | Qwen2 | Mistral | Mixtral | +|---------|-------|-------|---------|---------| +| Attention biases | No | Yes (Q,K,V,O) | No | No | +| Sliding window | No | No | Yes (4096) | No | +| MoE experts | No | No | No | Yes (n_experts, top-k) | +| GGUF key prefix | `llama.*` | `qwen2.*` | `llama.*` | `llama.*` | +| GGUF arch string | `llama` | `qwen2` | `mistral` | `llama` (detected via `expert_count`) | +| RoPE theta | 10000 | 1000000 | 10000 | 10000 | +| RoPE style | Interleaved | Interleaved | Interleaved | Interleaved | +| Q/K/V weights | Separate | Separate | Separate | Separate | +| FFN weights | Separate | Separate | Separate | Per-expert (merged or split) | +| Tied embeddings | model-dependent | model-dependent | No | No | + +Note: Mistral GGUFs use `llama.*` metadata keys (not `mistral.*`), but `general.architecture = "mistral"`. Mixtral GGUFs use `general.architecture = "llama"` and are detected via `llama.expert_count > 0`. + +### Phi-3 (phi3.c — separate forward pass) + +| Feature | Phi-3 | +|---------|-------| +| GGUF key prefix | `phi3.*` | +| Q/K/V weights | Fused `attn_qkv.weight` [3*n_embd, n_embd] | +| Gate/Up weights | Fused `ffn_up.weight` [2*n_ff, n_embd] | +| RoPE style | NeoX (split-half pairs: `head[i], head[i+half]`) | +| RoPE dim | Configurable via `rope.dimension_count` (96 for mini) | +| Attention | MHA (n_head == n_kv_head = 32) | +| Tied embeddings | No (separate output.weight) | + +Phi-3 uses its own forward pass because the fused weight layout requires different matvec + split logic, and NeoX RoPE pairs elements differently from the interleaved style used by Llama/Qwen2/Mistral. + +## Weight Storage + +Two modes, selected automatically based on quant type support: + +### Quantized (default) + +All projection weights (`Wq`, `Wk`, `Wv`, `Wo`, `W_gate`, `W_up`, `W_down`, `W_output`) stay in mmap'd quantized form as `pn_qweight` structs. Matrix-vector multiply happens via `pn_qmv()` which dequantizes one block at a time. + +Embeddings and norms are always F32 (owned memory), since they're accessed by index (embeddings) or element-wise (norms). + +### F32 legacy + +All weights dequantized to `tm_tensor` on load. Fallback for unsupported quant types. Uses standard `tm_matmul` for projections. + +## Per-Token Memory + +All intermediate buffers are allocated from a bump arena that resets after each token: + +| Buffer | Size | Used For | +|--------|------|----------| +| `normed` | n_embd | RMSNorm output | +| `q`, `k`, `v` | n_embd | Q/K/V projections | +| `attn_out` | n_embd | Attention output | +| `ffn_gate`, `ffn_up` | n_ff | FFN intermediates | +| `ffn_act` | n_ff | SiLU activation | +| `ffn_out` | n_embd | FFN output | +| `logits_raw` | n_vocab | LM head output | +| `scores` | max_seq per head | Attention scores | + +For MoE layers (Mixtral), additional per-layer buffers: + +| Buffer | Size | Used For | +|--------|------|----------| +| `gate_logits` | n_experts | Router output | +| `ffn_out` | n_embd | Weighted expert accumulator | +| `exp_gate` | n_ff | Per-expert gate projection | +| `exp_up` | n_ff | Per-expert up projection | +| `exp_hid` | n_ff | Per-expert SiLU activation | +| `exp_out` | n_embd | Per-expert down projection | + +Arena is sized at state creation: `sum of all buffers * sizeof(float) * 1.25` (25% safety margin). Typical sizes: 1.5 KB (SmolLM2-135M), 11 KB (Mistral-7B), 19 KB (Mixtral_7Bx2). + +## Batched Multi-Sequence Decode + +`pn_llama_forward_batch()` decodes one token for each of `B` independent sequences in a single pass, for bulk throughput (e.g. transcribing many pages at once). Each `states[b]` carries its own KV cache and position, so the sequences can have different lengths and each attends only to its own cache; row `b`'s input is `tokens[b]` or, for image-token positions, an `embd_rows[b]` override. It writes `[B, n_vocab]` row-major logits and advances every state's `pos` by one. + +The batched path is restricted to the quantized, non-MoE Llama/Qwen weights. For MoE, F32 legacy, or ternary weights it returns without mutating any state so the caller can fall back to per-sequence `pn_llama_forward`. Results are bit-identical to running `B` separate sequential decodes (verified by `test_batch_decode_correct`: a `B=2` batch reproduces each sequence's standalone greedy stream with no cross-contamination). + +### Fused AMX-panel M=B matmul (`pn_qmm`) + +The projection and FFN matmuls in the batched path go through `pn_qmm` (`src/core/qweight.c`), a fused quantized `M=B` kernel: `out[b, r] = sum_c dequant(W[r, c]) * acts[b, c]`. Each weight row is dequantized once into a small L2-resident panel and multiplied against all `B` activation vectors, so the quantized weight is read and dequantized once per step while the matmul runs at AMX throughput (the panel GEMM lowers to AMX under BLAS). This avoids both the NEON-dot ceiling of the single-vector `pn_qmv` and the full-weight F32 copy to DRAM that a plain BLAS GEMM would need. + +Because the per-token matmul FLOPs are irreducible, throughput is compute-bound and plateaus rather than scaling linearly with `B`. Measured on LightOnOCR 1B Q4_K, M4, against 38.8 tok/s single-stream `pn_qmv`: + +| path | B=16 | B=32 | +|------|------|------| +| full-BLAS F32 copy | 49.7 | 46.5 (DRAM-bound, regresses) | +| NEON fused dot | 58.4 | - | +| AMX panel (`pn_qmm`) | 72.5 | 84.3 | + +The AMX panel gives roughly 2.2x aggregate CPU throughput and settles near ~85 tok/s. `B=1` is intentionally suboptimal (the panel adds a scratch pass over the fused `pn_qmv`), so single sequences should use `pn_llama_forward`. `pn_qmm` falls back to a per-`b` `pn_qmv` loop above `PN_QMM_MAX_B` (64) or for ternary or GPU-resident weights. + +## Flash (Blocked) Prefill Attention + +Prefill over a long image-token sequence was the bulk-OCR bottleneck. The original per-query attention recomputed over the whole K/V cache once per query position, giving O(P^2) DRAM traffic across the ~1369 image-token queries of a page. + +`prefill_flash_range` processes a block of queries (`PF_BQ=32`) together and streams K and V in blocks (`PF_BK=256`) that stay cache-resident and are reused across the whole query block. This cuts K/V reads from about `P` times to about `P/BQ`. The math is unchanged from the per-query path (full-precision softmax, with the scores tile kept L2-resident), and a per-query fallback is retained for the sliding-window wraparound case. It is parallelized over query heads. + +Measured: prefill ~4.3 to ~3.2 s/page (~1.33x), bulk OCR ~6.3 to ~5.2 s/page steady state. It is pure CPU, so it helps regardless of the vision backend and without contending for the GPU (unlike GPU prefill below). Output stays correct and batched decode stays bit-identical. + +## Optional GPU Paths (off by default) + +Two Metal offload paths exist behind build flags and environment switches. Both are **off by default** because, on the current hardware, they lose to the CPU AMX path or contend with the vision GPU. The shipped app builds Pantheon without `USE_METAL`, so both are inert there. + +- **GPU batched decode** (`PN_BATCH_GPU`, gated at runtime by `pn_qmm_set_gpu_decode`): uploads the Q4_K decoder weights once (`pn_qmv_gpu_upload`) and runs the `M=B` GEMM on the GPU (`fm_sgemm_q4_k`). Bit-identical to the CPU path, but not a win: `pn_qmm` stages, dispatches, syncs, and copies back per weight, so a step does ~168 CPU/GPU round-trips and the per-call sync latency dominates (~68 tok/s at `B=16` vs ~86 tok/s for the CPU AMX panel). Auto-batching cannot hide it because each GEMM's result is immediately consumed by a CPU op (bias, RoPE, attention). Beating AMX would need a GPU-resident decode (the whole layer chain on GPU, one sync per step); the flag is kept as the seam for that. + +- **GPU F16 prefill** (`PN_GPU_PREFILL`): uploads the Q4_K decoder weights once and runs each prefill projection as an F16 MPS GEMM (`fm_gemm_q4_k_f16`) instead of the CPU dequant+AMX path. Also a net loss for bulk OCR, for two reasons: prefill is attention-bound, not GEMM-bound (the O(P^2) decoder attention over the image tokens dominates, so GPU-accelerating only the GEMMs is ~1.3x, not the ~5x a GEMM microbench suggests), and it contends with Core ML vision for the single GPU, breaking the CPU-compute / GPU-vision overlap that makes bulk fast (~6.3 s/page went to ~8-10 s/page). It is kept as an opt-in for the CPU-vision case, where the GPU is otherwise idle. Setting it also forces decode back onto the CPU AMX path (via the `pn_qmm_set_gpu_decode` gate) so the prefill upload does not drag decode onto the slower GPU route. + +ABI note: `pn_qweight.gpu` is an unconditional struct field, present in every build (just `NULL` and unused without `USE_METAL`). It must not be wrapped in `#ifdef USE_METAL`: a conditional field makes the struct layout depend on the build flag, so a translation unit compiled the other way reads later fields at the wrong offset. diff --git a/docs/architecture/memory-aware.md b/docs/architecture/memory-aware.md new file mode 100644 index 0000000..2aea8d5 --- /dev/null +++ b/docs/architecture/memory-aware.md @@ -0,0 +1,264 @@ +# Memory-Aware Execution Layer + +Sources: `src/core/mem/pn_memwatch.c`, `src/core/mem/pn_memwatch.h`, `src/core/mem/pn_mem_state.h`, `src/core/mem/pn_host_signals.h`, `src/core/mem/pn_host_signals.c`, `src/core/mem/pn_ladder.c`, `src/core/mem/pn_ladder.h`, `src/core/mem/pn_admission.c`, `src/core/mem/pn_admission.h`. Entry points wired in `src/core/engine.c`, `src/serve/serve_generation.c`, `src/serve/batch_sched.c`, `src/models/qwen35moe_forward_cuda.c`. + +--- + +## Overview + +The memory-aware layer tracks host RAM pressure (PSI + `/proc/meminfo`) and per-die VRAM in a continuously updated `pn_mem_state` snapshot, derives a four-level pressure enum, and exposes two behaviors to the rest of the engine: + +1. **Pre-flight admission** (`pn_admit_request`, `pn_admit_model_load`): refuse or cap a request before any memory-growing work starts. +2. **Reactive ladder** (`pn_ladder_plan_for`): translate the current pressure level into concrete mitigation rungs (fewer slots, smaller context window, forced Q4 KV, KV eviction flag) that the serve and CUDA layers apply. + +--- + +## Published state: `pn_mem_state` (pn_mem_state.h:46) + +One immutable, value-type snapshot written by `pn_memwatch` and read by admission and the ladder. Seqlock-protected so readers never block the writer. + +```c +typedef struct { + uint64_t host_total_bytes; // MemTotal / sysctl hw.memsize + uint64_t host_avail_bytes; // MemAvailable (allocatable without swap) + double host_psi_some_avg10; // /proc/pressure/memory "some avg10" % + double host_psi_full_avg10; // "full avg10" % — whole-box stall + uint64_t cgroup_current_bytes; // memory.current (0 if not in cgroup) + uint64_t cgroup_high_bytes; // memory.high (0 == "max"/unset) + uint64_t cgroup_max_bytes; // memory.max (0 == "max"/unset) + int n_dies; + uint64_t per_die_free[16]; // cuMemGetInfo free per die + uint64_t per_die_budget[16]; // usable budget (total - reserve) per die + uint64_t per_die_used[16]; // our own alloc tally + pn_mem_level level; // derived: worst-of host + any die + uint64_t seq; // monotonic snapshot counter +} pn_mem_state; +``` + +PSI and cgroup fields are Linux-only; on macOS they read 0 (treated as no pressure). + +--- + +## pn_memwatch — the poll loop (pn_memwatch.c) + +One background thread polls host signals every `PN_MEMWATCH_POLL_MS` (250 ms, pn_memwatch.h:46). The GPU side pushes die VRAM numbers via `pn_memwatch_set_die`. + +**Seqlock protocol** (pn_memwatch.c:34): +- `g_seq` even = stable. Writer atomically increments to odd, copies state, increments back to even. +- Readers loop until `s1 == s2` (both even, copy stable). Never blocks the writer. + +**Start / stop**: +```c +int pn_memwatch_start(void); // spawns poll thread; idempotent +void pn_memwatch_stop(void); // joins thread +bool pn_memwatch_running(void); // true once started +void pn_memwatch_poll_now(void);// force one immediate poll (no wait for next tick) +``` + +`pn_memwatch_start` runs one synchronous `poll_host_once` before creating the thread so the first `pn_memwatch_read` call returns real numbers (pn_memwatch.c:292). + +**Die feed** (pn_memwatch.c:244): +```c +void pn_memwatch_set_die(int die_id, uint64_t free_bytes, + uint64_t budget_bytes, uint64_t used_bytes); +``` +Called from the GPU side after each cuMemGetInfo query. Holds `g_write_lock`, merges into the state, re-derives the level, and publishes via the seqlock. + +--- + +## Host signals (pn_host_signals.c) + +`pn_host_signals_sample` populates a `pn_host_sample` with: +- **Linux**: `/proc/meminfo` (MemTotal, MemAvailable) + `/proc/pressure/memory` (some/full avg10) + cgroup-v2 `memory.{current,high,max}`. +- **macOS / other**: sysctl `hw.memsize` total + `host_statistics64` for available; PSI/cgroup fields stay 0. + +All reads are cheap (~1 ms). The poll thread calls `pn_host_signals_sample` inside `poll_host_once` (pn_memwatch.c:217). + +--- + +## Pressure levels (pn_mem_state.h:30) + +```c +typedef enum { + PN_MEM_LEVEL_OK = 0, // ample headroom + PN_MEM_LEVEL_WATCH = 1, // headroom shrinking + PN_MEM_LEVEL_TIGHT = 2, // reserve nearly reached / PSI rising + PN_MEM_LEVEL_CRITICAL = 3, // at the reserve line / PSI stall — shed +} pn_mem_level; +``` + +### Host level derivation (pn_memwatch.c:105) + +Reserve = `max(PN_HOST_RESERVE_MIN_BYTES=1 GiB, 15% × MemTotal)` (overridable via `PN_HOST_RESERVE_BYTES`). + +| Condition | Level | +|---|---| +| `host_avail <= reserve` | CRITICAL | +| slack above reserve < reserve/4 | TIGHT | +| slack above reserve < reserve | WATCH | +| otherwise | OK | + +**PSI escalation** (pn_memwatch.c:127): +- `full_avg10 >= PN_PSI_FULL_REJECT_DEFAULT` (15%, env `PN_PSI_FULL_REJECT`) → CRITICAL +- `full_avg10 > 0` → at least TIGHT (sub-threshold blip; see note below) +- `some_avg10 >= 10%` → at least TIGHT +- `some_avg10 > 0` → at least WATCH + +**Important**: PSI avg10 decays over ~1 minute, so a transient spike (e.g. the ~90 s model load's dequant/staging burst) leaves `full_avg10` slightly above 0 for a full decay window after the box recovered. The PSI-full reject threshold (15%) is set high enough that this transient blip only escalates to TIGHT, not CRITICAL. Admission then still passes the byte-math check on a recovered box. + +### Die level derivation (pn_memwatch.c:142) + +Per die (die with `budget == 0` is skipped): + +| free VRAM | Level | +|---|---| +| 0 | CRITICAL | +| < budget/16 | TIGHT | +| < budget/8 | WATCH | +| otherwise | OK | + +Budget = total VRAM measured at boot − `PN_DIE_RESERVE_BYTES` (1 GiB default, env `PN_DIE_RESERVE_BYTES`). + +`derive_level` returns `max(host_level, die_level)` (pn_memwatch.c:163). + +### Why TIGHT is normal on the mini + +The K80 test machine has 8 GiB of host RAM. The 15% reserve = ~1.2 GiB. The Qwen3.5-MoE model at steady state after load uses ~1.1 GiB RSS on host (measured 2026-07-09). Total host allocation sits well inside the TIGHT band (`slack < reserve`) at rest. This is expected and correct: TIGHT does not refuse requests on the GPU-resident path (PSI-gated, not byte-gated), and the ladder's rung-3 prefill-window clamp and rung-2 slot reduction only engage when the level is TIGHT and the box is actually under pressure. The admission gate at CRITICAL is the shed point for new concurrency (D3), not TIGHT. + +--- + +## Admission control (pn_admission.c / pn_admission.h) + +### Cost model + +**Host activation peak** (`lut_activation_peak`, pn_admission.c:69): a 6-anchor LUT interpolated linearly. Anchors are conservative CPU-path measurements (the O(N²) attention scores dominate on CPU). On the GPU-resident path the O(N²) scores live on-die in bounded flash tiles — the host-RSS delta measured on a K80 host was ~0 at all tested lengths — so GPU-resident requests bypass this LUT entirely. + +**KV per-token cost**: `PN_ADMIT_KV_BYTES_PER_TOKEN_Q4 = 5898` (~5.76 KiB) or `PN_ADMIT_KV_BYTES_PER_TOKEN_FP32 = 40960` (~40 KiB) (pn_admission.h:60). + +**Total host cost** (pn_admission.c:98): `kv_per_tok * n_prompt + lut_activation_peak(n_prompt)`. + +### pn_admit_request (pn_admission.c:153) + +```c +void pn_admit_request(int n_prompt, bool kv_q4, bool gpu_resident, + int die_id, int max_window, const pn_mem_state *state, + pn_admit_result *out); +``` + +Decision order: +1. **PSI-full stall guard** (pn_admission.c:174): if `host_psi_full_avg10 >= reject_threshold` → REJECT ("sustained memory stall — retry"). +2. **GPU-resident fast path** (pn_admission.c:193): if `gpu_resident`, host cost is declared 0. REJECT if `host_avail` is already inside the reserve; CAP if `n_prompt > max_window`; otherwise ADMIT at full context. +3. **CPU/host-fallback path** (pn_admission.c:208): check host budget (`host_avail − reserve`) and die free VRAM against the LUT cost. Binary-search for the largest fitting window if the full prompt doesn't fit; set CAP or REJECT. + +### pn_admit_model_load (pn_admission.c:245) + +Gates GPU weight uploads. Checks `weights_bytes + kv@max_ctx + activation_reserve <= die_free − die_reserve`. Used before uploading a model to a die. + +### pn_admit_model_load_die (pn_admission.c:266) + +Per-die variant for multi-die splits: checks `weights_slice + kv_slice + PN_ADMIT_DIE_SCRATCH_BYTES (1.5 GiB)` against one die's free VRAM. Does not add the host LUT (GPU-path scratch is bounded). + +### pn_admit_load_spike (pn_admission.c:292) + +Checks that `PN_ADMIT_LOAD_SPIKE_BYTES_DEFAULT` (2.4 GiB, dequant/staging burst measured on a K80 host during upload, env `PN_LOAD_SPIKE_BYTES`) fits within host headroom before a multi-minute weight upload starts. + +### Engine-level wrapper: pn_admit (engine.c:481) + +The serve layer calls `pn_admit(model, n_tokens, &fit_tokens_out)` which: +1. Fail-opens if `!pn_memwatch_running()`. +2. Reads the live state snapshot. +3. Derives `kv_q4`, `gpu_resident`, `die_id` (worst-free-VRAM die), `max_window`. +4. Consults the ladder (rung 3, see below) to possibly clamp `max_window` before calling `pn_admit_request`. +5. Maps the verdict to the public `pn_admit_status` enum. + +Called from `sg_run_completion` (serve_generation.c:547) and `sg_run_completion_batched` (serve_generation.c:777) before any prefill starts. + +### D3 — admission backpressure into the batch scheduler + +When `sg_run_completion_batched` receives `PN_ADMIT_DENIED`, it calls `bs_notify_pressure(sched, (int)ms.level)` (serve_generation.c:786). This updates `sched->pressure_level` and broadcasts to `cv_idle` waiters. `bs_slot_acquire` (batch_sched.c:1493) then holds new requests in the queue if `pressure_level >= BS_PRESSURE_HOLD_LEVEL` (TIGHT/CRITICAL), preventing new work from being admitted until pressure clears. CRITICAL (level 3) triggers a fast-path 503 for queued waiters (batch_sched.c:1541). **D3 gates at CRITICAL, not TIGHT**: TIGHT alone does not block admission; only a DENIED verdict from `pn_admit` (which requires CRITICAL or PSI stall) pushes the pressure signal. + +--- + +## Reactive degradation ladder (pn_ladder.c / pn_ladder.h) + +The ladder is a **pure, stateless planner**: given a `pn_mem_level` and an operator config, it returns a `pn_ladder_plan` struct with absolute values (not deltas) the caller applies. + +```c +typedef struct { + pn_mem_level level; + int max_slots; // rung 2: effective concurrency ceiling (>=1) + bool force_kv_q4; // rung 4a: force Q4 KV + bool kv_evict; // rung 4b: enable low-attention KV eviction (policy flag) + int prefill_window; // rung 3: max prefill tokens (0 = no clamp) + bool degraded; // true if any rung reduced a configured value + const char *note; +} pn_ladder_plan; +``` + +`pn_ladder_plan_for` (pn_ladder.c:51) and the convenience wrapper `pn_ladder_plan_from_state` are pure — no I/O, no allocation. At `PN_MEM_LEVEL_OK` the plan is byte-identical to the operator config. + +Disable via `PN_LADDER_DISABLE` env var (`pn_ladder_disabled`, pn_ladder.c:19). + +### Rung thresholds (pn_ladder.h:45) + +| Rung | Name | Engages at | Action | +|---|---|---|---| +| 4a | force Q4 KV | WATCH | `force_kv_q4 = true` | +| 2 | shrink concurrency | TIGHT | `max_slots = cfg / 2`; CRITICAL → 1 | +| 3 | clamp prefill window | TIGHT | `prefill_window = cfg / 2`; CRITICAL → `cfg / 4` | +| 4b | KV eviction | TIGHT | `kv_evict = true` (policy flag; SnapKV selection not yet implemented) | + +Rungs are reduce-only and monotonic: a higher pressure level never relaxes a rung a lower level engaged. + +### Where the ladder is consumed + +| Call site | What it reads | File:line | +|---|---|---| +| `pn_admit` (engine.c) | Rung 3 `prefill_window` to clamp `max_window` before admission | engine.c:504 | +| `pn_qwen35moe_gpu_ladder_max_slots` (qwen35moe_forward_cuda.c) | Rung 2 `max_slots` for the GPU decode-slot ceiling | qwen35moe_forward_cuda.c:4605 | +| `bs_new` (batch_sched.c) | Rung 2 ceiling to clamp `cap` at scheduler creation | batch_sched.c:629 | +| `pn_qwen35moe_decode_slot_bind_cuda` | Rung 2 ceiling to enforce the bind guard | qwen35moe_forward_cuda.c:4624 | +| `qwen35moe_kv_init` (qwen35moe_forward.c:142) | Rung 4a `force_kv_q4` at KV cache allocation time | qwen35moe_forward.c:145 | + +**Rung 4a acts at KV-cache allocation time**, not reactively. The KV format (fp32 vs Q4) is fixed when the state is created; `force_kv_q4` only fires if the box is already at WATCH when `pn_qwen35moe_kv_init` runs (qwen35moe_forward.c:142). A live fp32 KV is never retroactively converted. + +**Rung 4b (`kv_evict`)** is a policy flag only — it is set in the plan at TIGHT but no SnapKV-style row selection code exists yet. Nothing reads this flag today in a path that would evict rows from a live KV cache. + +### The logged TIGHT clamp + +The line `[ladder] TIGHT -> prefill window clamped 16384 -> 8192 (...)` is emitted by `pn_admit` at engine.c:507 whenever `ladder.prefill_window > 0 && ladder.prefill_window < max_window`. It means the incoming request's allowed context was halved (TIGHT divides by 2, pn_ladder.h:59) before admission checked it. This happens at every request on the mini while the server is at TIGHT. + +--- + +## Concurrent-mutation hazard — segfault suspect + +This section catalogs every place the ladder or memory layer could mutate or resize a buffer that an in-flight batched forward (`pn_qwen35moe_decode_slots_cuda` / the async begin/poll step) might be reading. + +### 1. `gpu_attn_slot_ring_ensure` — KV ring reallocation (qwen35moe_forward_cuda.c:4946) + +**CRITICAL.** Called inside `pn_qwen35moe_decode_slots_cuda` at the start of each attention-layer decode step. If `kv_slots_cap_seq != max_seq` or `kv_slots_cap_n < nslots` or `kv_q4 != last`, it frees and reallocates `k_slots_g`, `v_slots_g`, and the Q4 sink/win buffers (pn_qwen35moe_forward_cuda.c:4958–4989). + +The trigger for `kv_slots_cap_n < nslots` is a new slot binding with a different `nslots` argument than the previous bind. The ladder (rung 2) can change the slot ceiling between `bs_new` time (where `cap` is clamped once at scheduler creation) and a subsequent `pn_qwen35moe_decode_slot_bind_cuda` call if the pressure level changed. However, the bind guard at qwen35moe_forward_cuda.c:4624 refuses any bind that would exceed the current ladder ceiling — so the reallocation path inside `gpu_attn_slot_ring_ensure` is only reached when `nslots` is *smaller* than expected (a bind with fewer slots, which can only shrink the allocation, not grow it). This is safe. + +**The real hazard**: the async forward step (`pn_forward_step_begin/poll`) holds no lock during its multi-phase GPU operation (qwen35moe_forward_cuda.c:7053: "the per-model gen_lock guarantees one in-flight step per model"). If `gpu_attn_slot_ring_ensure` is somehow entered while an async step is in flight on the same model — e.g. a new connection calls `decode_slot_bind` (which calls `gpu_attn_slot_ring_ensure` via the bind path) before the previous step's poll completes — the reallocation frees buffers (`fc_buffer_free(d->k_slots_g)`) that a CUDA kernel may still be accessing on-device. Whether the serve layer's GPU-turn mechanism (`bs_gpu_acquire`) fully prevents this is the question (see item 3 below). + +### 2. `conv_slots` / `ssm_slots` reallocation on slot-count change (qwen35moe_forward_cuda.c:4671) + +Inside `pn_qwen35moe_decode_slot_bind_cuda`: if `d->slots_cap < n_slots`, the per-layer conv and SSM slot buffers are freed and reallocated (`fc_buffer_free(d->conv_slots)`, then `fc_buffer_alloc_ctx`). The graph is dropped (`d->slots_graph = NULL`) so the next step recaptures against the new allocation (comment at pn_qwen35moe_forward_cuda.c:4681: "L9 — the slot state buffers just moved"). If a decode step is in flight on the GPU and uses these buffers as kernel arguments via a captured CUDA graph, freeing them while the graph executes is a use-after-free at the CUDA level. The batch_sched comment at batch_sched.c:1144 says "the GPU turn is owned … this whole section is atomic w.r.t. every other forward on this model" — but `bs_slot_bind_common` acquires the GPU turn via `bs_gpu_acquire` (batch_sched.c:1207 calls `pn_qwen35moe_decode_slot_bind` while under the prefill turn, not the decode turn). Whether the `driver_thread`'s decode pass (`decode_slots` at batch_sched.c:480) and the prefill thread's bind path are mutually exclusive depends on the GPU-turn ticket system covering both. + +### 3. `qw_defer` staging arena realloc (qwen35moe_forward_cuda.c:5057) + +`qw_defer_reset` (called at the top of each decode step at qwen35moe_forward_cuda.c:6320) may `realloc(df->stage, need)` if the staging arena is too small. The comment at pn_qwen35moe_forward_cuda.c:5049 states explicitly: "an in-flight async DtoH writes into it — moving it would corrupt earlier layers' queued copies." The code handles this by pre-sizing up front so `qw_defer_stage` never reallocs mid-step. This is safe **within a single step** but not across steps: if a new step starts and `qw_defer_reset` reallocs `df->stage` while a prior step's async DtoH (from an earlier `begin`) has not been reaped by `poll`, those async writes still point at the old buffer address. The `as_*` phase machine (qwen35moe_forward_cuda.c:572) is supposed to prevent this, but verify that `poll` always fully drains async DtoH before `begin` is allowed to reenter. + +### 4. Ladder-triggered max_slots change between scheduler creation and bind + +`pn_qwen35moe_gpu_ladder_max_slots()` is called once at `bs_new` time (batch_sched.c:629) to clamp `cap`. It is called again inside `pn_qwen35moe_decode_slot_bind_cuda` (qwen35moe_forward_cuda.c:4629) for the bind guard. If the pressure level changed between these two calls — e.g. the box was OK when the scheduler was created (large cap) but is now TIGHT/CRITICAL (smaller ceiling) — the bind guard fires with `n_slots > slot_ceiling` and returns `-1` (engine.c style: the slot bind fails and the request fails cleanly). This is not a buffer corruption hazard, but it will produce a `[ladder] refusing N decode slots` error on requests that would have been fine at scheduler creation time. + +### Summary table + +| Location | Buffer mutated | Condition | Guarded by | Hazard for in-flight? | +|---|---|---|---|---| +| `gpu_attn_slot_ring_ensure` (line 4956) | `k_slots_g`, `v_slots_g`, sink/win bufs | `nslots` or `max_seq` or `q4` changed | GPU-turn ticket (prefill) | **Investigate**: does GPU-turn cover the decode step too? | +| `decode_slot_bind_cuda` (line 4671) | `conv_slots`, `ssm_slots` per SSM layer | `slots_cap < n_slots` | GPU-turn ticket (prefill) | **Investigate**: same question | +| `qw_defer_reset` (line 5057) | `df->stage` host staging arena | first step at a new nslots | pre-size contract within a step | Safe within step; verify async DtoH is always reaped before re-entry | +| `gpu_batch_host_ensure` (batch scratch) | `b->hidden` and friends | batch width grows | GPU-turn (prefill/decode) | `slots_graph_scratch` comment (line 271) notes graph invalidation on realloc | diff --git a/docs/architecture/models-qwen35moe.md b/docs/architecture/models-qwen35moe.md new file mode 100644 index 0000000..a2355e1 --- /dev/null +++ b/docs/architecture/models-qwen35moe.md @@ -0,0 +1,281 @@ +# Qwen3.5-MoE Model Implementation + +Pantheon implementation of the Nex-N2-mini architecture: a 40-layer hybrid Gated-Delta-Net SSM / sparse GQA attention stack with 256-expert MoE on every layer. + +Source files: +- `src/models/qwen35moe.c` — loader, config parser, GGUF weight mapping +- `src/models/qwen35moe_internal.h` — config/block structs, dual-state types, cross-file kernel declarations +- `src/models/qwen35moe.h` — public API, `PN_QWEN35MOE_MAX_SEQ_CAP` +- `src/models/qwen35moe_forward.c` — CPU forward, dual-state lifecycle, KV ring init +- `src/models/qwen35moe_ssm.c` — Gated-Delta-Net SSM block kernel +- `src/models/qwen35moe_forward_cuda.c` — CUDA forward: 2-die split, GPU weight residency, decode slots, async step machine (7 348 lines) + +--- + +## Architecture + +### Dims (from `pn_qwen35moe_config`, `qwen35moe_internal.h:36–60`) + +| Field | Value | GGUF key | +|---|---|---| +| `n_layer` | 40 | `qwen35moe.block_count` | +| `full_attention_interval` | 4 | `qwen35moe.full_attention_interval` | +| `n_embd` | 2048 | `qwen35moe.embedding_length` | +| `n_head` | 16 | `qwen35moe.attention.head_count` | +| `n_head_kv` | 2 | `qwen35moe.attention.head_count_kv` | +| `head_dim` | 256 | `qwen35moe.attention.key_length` | +| `rope_dim` | 64 | `qwen35moe.rope.dimension_count` (partial NEOX) | +| `n_expert` | 256 | `qwen35moe.expert_count` | +| `n_expert_used` | 8 | `qwen35moe.expert_used_count` | +| `expert_ff` | 512 | `qwen35moe.expert_feed_forward_length` | +| `shared_ff` | 512 | `qwen35moe.expert_shared_feed_forward_length` | +| `ssm_state` | 128 | `qwen35moe.ssm.state_size` | +| `ssm_n_group` | 16 | `qwen35moe.ssm.group_count` (K-heads, GQA key/query) | +| `ssm_dt_rank` | 32 | `qwen35moe.ssm.time_step_rank` (V-heads) | +| `ssm_d_inner` | 4096 | `qwen35moe.ssm.inner_size` | +| `ssm_conv` | 4 | `qwen35moe.ssm.conv_kernel` | +| `conv_dim` | 8192 | derived: read from `blk.0.attn_qkv.weight` outermost dim (`qwen35moe.c:163–167`) | + +### Layer layout + +Layer type is set by `qwen35moe_is_attn_layer()` (`qwen35moe.c:216–220`): + +```c +return ((i + 1) % interval) == 0; /* interval default 4 */ +``` + +With `full_attention_interval=4` and 40 layers, this yields: +- Layers 3, 7, 11, 15, 19, 23, 27, 31, 35, 39 → **GQA full-attention** (10 layers, `n_attn=10`) +- All other 30 layers → **Gated-Delta-Net SSM** (`n_ssm=30`) + +`block_type[i]` and `block_index[i]` in `pn_qwen35moe_model` (`qwen35moe.c:59–60`) map any global layer index to its type and per-type slot. These are the only two arrays that must be consulted to dispatch a layer. + +### Per-layer block structure + +Every layer (SSM or attention) shares the same outer shell: + +``` +attn_norm → (SSM block | GQA attn block) → +residual + → post_attention_norm → shared-MoE FFN → +residual +``` + +(`qwen35moe_forward.c:316–369`) + +### MoE routing + +Every layer carries an identical `pn_qwen35moe_moe` block (`qwen35moe_internal.h:75–84`): + +1. **Router**: `ffn_gate_inp [n_expert, n_embd]` → logits → softmax → top-8 selection → `norm_topk_prob` renormalization → scaled by `expert_weights_scale` (default 1.0; key absent in current GGUFs, `qwen35moe.c:141–145`). +2. **Routed experts**: 256 experts, zero-copy sliced from merged 3-D GGUF tensors `blk.N.ffn_gate_exps.weight` / `ffn_up_exps.weight` / `ffn_down_exps.weight` (`qwen35moe.c:246–293`). Each expert is a SwiGLU: `silu(gate·x) * (up·x)` → `down`. `moe_out = Σ weight_e * SwiGLU_e(x)`. +3. **Shared expert** (always on): separate `ffn_gate_shexp` / `ffn_up_shexp` / `ffn_down_shexp` + a scalar sigmoid gate `ffn_gate_inp_shexp [n_embd]`. `shexp_out = silu(gate·x) * (up·x) → down`, then scaled by `sigmoid(gate_inp_shexp · x)`. +4. **Combine**: `out = moe_out + shexp_out`. Residual add is the caller's responsibility. + +(`qwen35moe_internal.h:278–308`) + +### GQA attention block (`qwen35moe_internal.h:160–196`) + +Op order for attention layers: +1. `attn_q [2*head_dim*n_head, n_embd]` → split: first `head_dim` per head is Q, second is gate z. +2. QK-RMSNorm on Q (per head via `attn_q_norm [head_dim]`) and on K (via `attn_k_norm [head_dim]`), pre-RoPE. +3. `attn_k [head_dim*n_head_kv, n_embd]` and `attn_v [head_dim*n_head_kv, n_embd]`. +4. Partial NEOX RoPE on the first `rope_dim=64` dims of every Q and K head. Multi-section RoPE is parsed (`rope_sections[4]`, `qwen35moe.c:111–130`) but collapses to standard NEOX for text. +5. GQA: 16 Q heads broadcast across 2 KV heads (each KV head serves 8 Q heads), scale `1/sqrt(256)`. K/V appended to per-layer ring before attention. +6. Output gate: `attn_out *= sigmoid(z)`. +7. `attn_output [n_embd, head_dim*n_head]`. + +### SSM (Gated-Delta-Net) block (`qwen35moe_ssm.c:229–351`) + +Op order for SSM layers: +1. Projections from `attn_norm`-ed input: `attn_qkv → qkv [conv_dim=8192]`; `attn_gate → z [ssm_d_inner=4096]`; `ssm_beta → beta [32]` → sigmoid; `ssm_alpha → alpha [32]`. +2. Decay gate per V-head (`qwen35moe_ssm.c:86–93`): `g[h] = ssm_a[h] * softplus(alpha[h] + ssm_dt[h])`. `ssm_a` is stored as `−exp(A_log)` (negative), so `exp(g) ∈ (0,1)`. +3. Causal depthwise conv1d (kernel size 4) over `conv_dim` channels + SiLU (`qwen35moe_ssm.c:110–135`). Rolls `conv_state` in place (oldest-first, channel-major layout). +4. Split convolved output into q `[128*16]`, k `[128*16]`, v `[128*32]`; L2-normalize q/k per K-head (`qwen35moe_ssm.c:70–76`); GQA-tile from 16 K-heads to 32 V-heads using interleaved tiling (`vh % n_k_heads == kh`, `qwen35moe_ssm.c:311`). +5. Delta-rule recurrence per V-head (`qwen35moe_ssm.c:163–195`): for each of 32 `[128×128]` state matrices (stored transposed, row j = column j of S): decay → pred → delta → rank-1 write → read-after-write y. Scale `1/sqrt(128)` applied once at read. +6. Gated RMSNorm: `RMSNorm(y, ssm_norm) * silu(z)` per V-head (`qwen35moe_ssm.c:206–223`), then `ssm_out [n_embd, ssm_d_inner]`. + +The recurrence is parallelized across V-heads via `fw_for` when `USE_THREADS` is set and `n_v_heads >= 4` (`qwen35moe_ssm.c:332–336`). + +--- + +## 2-Die Split and Placement + +### Constants (`qwen35moe_forward_cuda.c:89–95`) + +```c +#define GPU_DIE_SPLIT_LAYER 40 /* default = disabled (all on one die) */ +#define GPU_DIE_A_DEFAULT 0 +#define GPU_DIE_B_DEFAULT 1 +``` + +`GPU_DIE_SPLIT_LAYER=40` means no split by default; the single-die path is the validated baseline. A split is engaged only when `QWEN35_GPU_SPLIT_LAYER < n_layer` or explicit placement provides two dies. + +### Boundary resolution (`gpu_split_layer`, `qwen35moe_forward_cuda.c:696–711`) + +``` +pn_load_on(2 dies) → split = n_layer/2 = 20 +pn_load_on(1 die) → split = n_layer (single-die forced) +no placement → GPU_DIE_SPLIT_LAYER (40 = disabled) +QWEN35_GPU_SPLIT_LAYER env → overrides boundary (unless 1-die placement) +``` + +Clamped to `(0, n_layer]`. + +### Die id resolution (`gpu_die_ids`, `qwen35moe_forward_cuda.c:717–736`) + +Priority: `pn_load_on` explicit placement > `QWEN35_GPU_DIE_A` / `QWEN35_GPU_DIE_B` env vars > `GPU_DIE_A_DEFAULT=0` / `GPU_DIE_B_DEFAULT=1`. + +Die ids are ordinals relative to `CUDA_VISIBLE_DEVICES`. On a K80 host, one K80 card exposes two GK110 dies; `CUDA_VISIBLE_DEVICES` maps them to ordinals 0 and 1. + +### Weight placement + +- Die A (`ctx_a`): layers `[0, split)` + token embedding (read host-side, row uploaded per token). +- Die B (`ctx_b`): layers `[split, n_layer)` + `output_norm` + LM head. + +Each `gpu_ssm_layer.ctx` / `gpu_attn_layer.ctx` stores a back-pointer to its owning die context (`qwen35moe_forward_cuda.c:205`, `285`). + +### 2-die multiplexing (`gpu_select_die`, `qwen35moe_forward_cuda.c:751–764`) + +On legacy large-BAR K80 rigs (single MMIO slot), `barswap` time-multiplexes the two dies. The forward: +1. Acquires die A via `bs_acquire`. +2. Runs front layers on `ctx_a`. +3. Syncs, D2H copies the boundary `hidden [n_embd float32]`. +4. Acquires die B via `bs_acquire`. +5. H2D uploads `hidden` to `ctx_b`. +6. Runs back layers + LM head on `ctx_b`. + +In small-BAR mode (modern driver, real dual-device), both contexts are live simultaneously and `g->bs == NULL`; no barswap or per-layer `gpu_select_die` is needed. The decode-slot bind path and the async path only run in small-BAR mode (`qwen35moe_forward_cuda.c:4655–4658`). + +--- + +## KV Cache + +### Allocation (`qwen35moe_kv_init`, `qwen35moe_forward.c:129–176`) + +One `pn_qwen35moe_kv` per attention layer, 10 total. Layout `[n_head_kv][max_seq][head_dim]` (head-major). + +**Q4 mode** (`QWEN35_KV_Q4`, default ON, `qwen35moe_forward.c:47–56`): +- Cache stored as ggml-Q4_0 rows: `kq [n_head_kv * max_seq * pn_kvq4_row_bytes(head_dim)]`, same for `vq`. +- fp16 sink shadow `k_sink/v_sink [n_head_kv * PN_KVQ4_SINKMAX * head_dim]` — attention sink tokens kept at fp16 to prevent argmax flips on near-ties. +- fp16 window shadow `k_win/v_win [n_head_kv * PN_KVQ4_WMAX * head_dim]` — the most recent W tokens similarly kept at fp16. +- PPL delta vs fp32 KV: 0.024% (validated, `qwen35moe_forward.c:45`). +- Disable with `QWEN35_KV_Q4=0`. + +**fp32 mode**: `k/v [n_head_kv * max_seq * head_dim float32]`. + +The reactive memory ladder rung 4a (`qwen35moe_forward.c:142–152`) auto-forces Q4 if the box is under memory pressure at allocation time even without the env var. + +### KV window size + +`PN_QWEN35MOE_MAX_SEQ` = 4096 (compile-time default, `qwen35moe_forward.c:70`). + +`QWEN35_MAX_SEQ` env var overrides, clamped to: +- minimum: 16 +- maximum: `PN_QWEN35MOE_MAX_SEQ_CAP` = 262 144 (`qwen35moe.h:29`) + +The cap is the single source of truth shared between the CPU KV ring allocator (`qwen35moe_max_seq`, `qwen35moe_forward.c:76–91`) and the GPU admission gate (`gpu_admit_max_ctx`, `qwen35moe_forward_cuda.c:619–629`). Memory scales linearly: `n_head_kv * max_seq * head_dim * 8` bytes per attention layer on the host (same again on device in GPU mode). + +### Ring behavior + +KV is a fixed-size ring; `pos % max_seq` gives the slot. There is no dynamic resizing. If `pos >= max_seq` the oldest entry is overwritten. The ring is never grown after allocation. + +### Deferred host mirror (B2) + +After a GPU decode token, the GPU KV ring is the live source of truth. Writing back to the host mirror (`kv->k/v` or `kq/vq`) per token was too expensive. Instead, `kv->kv_mirror_stale = 1` is set (`qwen35moe_forward_cuda.c:3547`) and the D2H copy is deferred to `pn_qwen35moe_gpu_state_to_host()` (`qwen35moe_forward_cuda.c:6779`), which is called by the prefix cache on session eviction. After `pn_qwen35moe_state_reset()` the flag is cleared to 0 (`qwen35moe_forward.c:308`). + +--- + +## Decode Slots / Multi-User Batching + +### Slot capacity + +`PN_QWEN35MOE_GPU_MAX_SLOTS` = 16 (`qwen35moe_forward_cuda.c:115`). + +Memory cost: ~2.1 MB per SSM layer per slot for the per-slot `conv_slots / ssm_slots` images. At 16 slots across 30 SSM layers ≈ 1 GB on the die pair. + +The reactive ladder rung 2 (`pn_qwen35moe_gpu_ladder_max_slots`, `qwen35moe_forward_cuda.c:4605–4613`) caps live slots: OK → 16, TIGHT → 8, CRITICAL → 1. + +### Slot protocol + +``` +1. prefill each user's pn_state (single-token or batched) +2. immediately after each user's prefill: + pn_qwen35moe_decode_slot_bind(model, state_u, u, K) + — D2D snapshots the live GPU SSM state into slot u; + host mirror is stale after GPU prefill so bind must precede + any other user's forward +3. step all K users: pn_qwen35moe_decode_slots(model, tokens, states, K, logits) + — ONE forward pass, weights stream once for all K users +``` + +(`qwen35moe.h:126–138`) + +Slot-major per-user SSM images live in `gpu_ssm_layer.conv_slots / ssm_slots` (`qwen35moe_forward_cuda.c:247–250`). Bind snapshots via D2D from the shared `conv_state_g / ssm_state_g` right after prefill (`qwen35moe_forward_cuda.c:4616–4638`). A slot-count change invalidates all existing bindings. + +`pn_qwen35moe_decode_slot_move(src, dst, n_slots)` (`qwen35moe.h:154`) compacts the active set via D2D copy without a host round-trip. + +### Async step machine (C2, `QWEN35_ASYNC_SCHED`, default ON) + +Three-state enum (`qwen35moe_forward_cuda.c:7062–7065`): +- `QW_ASYNC_IDLE` — no step in flight +- `QW_ASYNC_WAIT_A` — 2-die only: front segment (die A) in flight, CUDA event pending +- `QW_ASYNC_WAIT_FINAL` — back segment + LM head in flight (or single-die full step) + +`pn_qwen35moe_forward_step_begin()` launches the step and returns without blocking (returns 1 = not async-eligible and runs synchronously in that case). `pn_qwen35moe_forward_step_poll()` reaps via `fc_event_query`, advances to the next phase, and on completion sets `*logits_out` and increments `state->pos`. This lets a 2-core host host keep up to 8 cards' generations pipelined. + +Requires: small-BAR mode (real dual-device, `g->bs == NULL`); LM head must have an on-device kernel (Q3_K/Q4_K/Q5_K/Q6_K/Q8_0/F32, `qwen35moe_forward_cuda.c:7083–7098`). Disable with `QWEN35_ASYNC_SCHED=0`. + +--- + +## The MTP / nextn Trap + +The GGUF metadata for Qwen3.5-MoE may advertise MTP (multi-token prediction) draft-head weights under `blk.N.nextn.*` keys. The weights **do not ship** with public GGUF conversions; `nextn_predict_layers` must always be 0. + +The loader explicitly documents this and never touches any `blk.N.nextn.*` tensor: + +```c +/* Honor the MTP trap: nextn_predict_layers=0 — never touch blk.N.nextn.* . */ +``` + +(`qwen35moe.c:475`) + +If a future GGUF includes these weights, loading them would require a separate code path not yet implemented. Attempting to load them without that path would silently skip them (no tensor lookup is performed) — so correctness is maintained but draft-head acceleration would simply not engage. + +--- + +## Gotchas and Fragile Spots + +### KV ring is fixed at allocation — no resize path + +`qwen35moe_kv_init` (`qwen35moe_forward.c:129`) allocates the ring once at `max_seq`. There is no resize-on-demand path. If the ring reallocates (e.g., `cache_cap` changes on the GPU side under a geometry change), the segment graph must be dropped and recaptured (`qwen35moe_forward_cuda.c:4941`). Under memory pressure the reactive ladder forces Q4 at init time but cannot shrink the ring after the fact. + +**Segfault risk**: if `pos >= max_seq` is not checked before the GPU KV-append kernel writes `pos % max_seq`, and the GPU ring's `cache_cap` was allocated for a different `max_seq` than the host ring, the write goes out of bounds. The GPU ring capacity is tracked separately as `d->cache_cap` (`qwen35moe_forward_cuda.c:306`); a mismatch after a partial realloc failure would be silent until the OOB write. + +### kv_mirror_stale + session switch + +After a GPU decode step, `kv->kv_mirror_stale = 1`. If a session is evicted without calling `pn_qwen35moe_gpu_state_to_host()` first (e.g., the prefix cache path is bypassed or fails partway), the host mirror remains stale. A subsequent resume re-syncs from the stale host mirror instead of the GPU ring, silently corrupting the KV cache with zeros or old content. + +The `pn_qwen35moe_gpu_forget_state()` call (`qwen35moe.h:212`) must be paired with `pn_state_reset()` on session reuse; omitting it leaves the GPU SSM buffers binding to the old session and the next prefill reads stale GPU residue. + +### GQA repeat tiling — interleaved, not blocked + +The SSM's GQA repeat tiles K-heads to V-heads using `vh % n_k_heads == kh` (interleaved), **not** the blocked `vh / kv_groups` mapping (`qwen35moe_ssm.c:311–312`). This matches `ggml_repeat`'s tiling in llama.cpp. Getting this wrong produces plausible but numerically wrong y vectors — the error is hard to detect without the oracle diff. + +### conv_state layout: channel-major, oldest-first + +`conv_state[c * (ssm_conv-1) + t]`, t=0 oldest. The roll-forward in `qwen35moe_conv1d_silu` (`qwen35moe_ssm.c:131–133`) shifts t=0..km1-2 forward and appends the new raw input at t=km1-1. The GPU path must replicate this exactly or the conv state diverges after the first token. + +### ssm_state is stored transposed + +`ssm_state[h * S * S + j * S + i] == S_h[i][j]` — row j (contiguous over i) is column j of S (`qwen35moe_internal.h:214–215`). This matches llama.cpp's `gated_delta_net` layout. Any GPU kernel that reinterprets the layout as standard row-major will compute wrong dot products. + +### Batched CUDA prefill and CUDA-graph capture ordering + +The per-layer segment graph is only captured after the layer has run DIRECTLY (uncaptured) at least once — `body_ran` gates the attempt (`qwen35moe_forward_cuda.c:229–232`). A batched prefill that precedes the first single-token forward never sets `body_ran` for those layers' fused descriptors, so the first single-token decode after a batch-only session must run uncaptured to build the fused descs. Capturing mid-allocation (before the fused descs exist) would call `cuMemAlloc` inside the capture, invalidating the graph. The `body_ran` gate prevents this. + +### Embedding: lazy vs materialized + +For untied models (a separate `output.weight` exists), `token_embd` is left `NULL` and each row is dequantized on demand from the mmap (`pn_qwen35moe_model_embed_row`, `qwen35moe.c:620–629`). For tied models, the full ~2 GB F32 embedding is materialized and also serves as the LM head (`qwen35moe.c:437–453`). The fallback for untied models where `n_embd % block_size != 0` also materializes the full heap. + +### Numeric status: A8 not done + +`qwen35moe_forward.c` and `qwen35moe_forward_cuda.c` both carry an open `@todo A8: validate logits vs the llama.cpp oracle`. End-to-end numeric correctness has not been verified. Several operations in the CUDA path have no dedicated GPU kernel and fall back to host-side computation with a D2H/H2D round trip (marked `TODO kernel:` inline in the CUDA file): joint Q+gate split, GQA head broadcast + L2-norm, decay gate / softplus, sigmoid gates, SwiGLU for non-Q3_K experts, attention softmax. diff --git a/docs/architecture/qwen35moe.md b/docs/architecture/qwen35moe.md new file mode 100644 index 0000000..3ae081c --- /dev/null +++ b/docs/architecture/qwen35moe.md @@ -0,0 +1,193 @@ +# qwen35moe — Forward-Pass Design (Pantheon port) + +Implementation blueprint for `src/models/qwen35moe.{h,c}`. Hybrid **Gated-Delta-Net SSM + sparse full-attention + shared-expert MoE** (Qwen3-Next lineage). Synthesized from llama.cpp `qwen35moe.cpp` + `delta-net-base.cpp` (math reference) and Pantheon `rwkvx.c` (structural template). + +**Structure clones `rwkvx.c`; loading follows the GGUF/llama path (`pn_qweight`, `gguf.c`), NOT rwkvx's safetensors path.** Hybrid of both. + +## Config (mini; Pro is the same arch, bigger) + +| dim | value | source key | +|---|---|---| +| n_layer | 40 | `block_count` | +| full_attention_interval | 4 → **attn when `(i+1)%4==0`** (layers 3,7,…,39 = 10 attn; rest = 30 SSM) | `full_attention_interval` | +| n_embd | 2048 | `embedding_length` | +| n_head / n_head_kv | 16 / 2 (GQA) | `attention.head_count[_kv]` | +| head_dim | 256 | `attention.key_length`/`value_length` | +| rope_dim | **64** (partial — only first 64 of 256 rotated) | `rope.dimension_count` | +| n_expert / used | 256 / 8 (+1 shared) | `expert_count`/`expert_used_count` | +| expert FF / shared FF | 512 / 512 | `expert_feed_forward_length`/`…shared…` | +| **SSM:** state (S) | 128 | `ssm.state_size` | +| SSM n_group (K-heads) | 16 | `ssm.group_count` | +| SSM dt_rank (V-heads) | 32 | `ssm.time_step_rank` | +| SSM d_inner | 4096 | `ssm.inner_size` | +| SSM conv kernel | 4 | `ssm.conv_kernel` | +| conv_dim | key·2+value = 2048·2+4096 = **8192** | derived | + +## Block scaffold — pre-norm sandwich (every layer, both types) + +``` +x → attn_norm → [SSM | full-attn block] → +x → post_attention_norm → shared-MoE FFN → +(post-attn x) +``` +Final `output_norm` once after the stack → LM head. All norms RMS. Residuals add the *un-normalized* input. (Identical to llama.cpp's loop.) + +## Full-attention block (layers `(i+1)%4==0`) +Input is `attn_norm`'d. Op order: +1. **Joint Q+gate** proj `attn_q` → `[2·head_dim·n_head]`; split: Q = first `head_dim` per head, gate `z` = second half. +2. **QK-RMSNorm on Q** (`attn_q_norm`, per-head, **before RoPE**). +3. `attn_k`,`attn_v` proj; reshape K to heads; **K-RMSNorm** (`attn_k_norm`, pre-RoPE). +4. **Partial RoPE** on Q,K — rotate only first `rope_dim=64` of each 256-dim head, pass the rest through (multi-section / IMRoPE; sections from `rope.dimension_sections`). +5. **GQA attention** (16 Q / 2 KV heads), scale `1/√head_dim`. +6. **Output gate:** `cur = attn_out * sigmoid(z)` ← note: **sigmoid** gate here. +7. Output proj `attn_output`. Residual added in the main loop. + +## SSM block — Gated-Delta-Net (layers `(i+1)%4!=0`) +Input is `attn_norm`'d. **This is the hard new kernel.** + +**Projections** (4 separate, from normed `x`): +- `attn_qkv` → fused **q|k|v** stream `[8192]` (the thing that gets convolved). +- `attn_gate` → **z** `[4096]` (output gate, used at the end). +- `ssm_beta` → `beta [32]`, then **`β = sigmoid(beta)`**. +- `ssm_alpha` → `alpha [32]`. + +**Decay gate (scalar per V-head, "GDA"):** +``` +g = (-exp(A_log)) · softplus(alpha + dt_bias) # ssm_a stored as -exp(A_log) (negative); ssm_dt = dt_bias +``` +(`ssm_a` is consumed via plain multiply — pre-baked at load.) + +**Causal conv1d** (`ssm_conv`, k=4): conv over the full 8192-channel q|k|v stream prepended with `conv_state` (last 3 timesteps) → **SiLU**. Then split into q`[128×16]`, k`[128×16]`, v`[128×32]`; **L2-normalize q and k** over head dim (not RMS); GQA-repeat q,k 16→32 heads. + +**Per-token recurrence** (per V-head `h∈32`, state `S` is `[128,128]`, `i`=key dim, `j`=value dim): +``` +S' = exp(g_t) · S_{t-1} # (a) scalar decay, whole state +pred_j = Σ_i S'[i][j] · k_t[i] # (b) read decayed state under key +delta_j = (v_t[j] − pred_j) · β_t +S_t[i][j] = S'[i][j] + k_t[i] · delta_j # (c) rank-1 delta-rule write +y_t[j] = (1/√128) · Σ_i S_t[i][j] · q_t[i] # (d) read-AFTER-write under query +``` +**Scaling caution:** apply the q/output scale **once** — the autoregressive (single-token) path is what we want for decode; use the form above and do *not* also pre-scale q. + +**Output path:** +``` +gated = RMSNorm(y, ssm_norm) · silu(z) # gated RMSNorm — silu(z), NOT sigmoid +cur = ssm_out · gated # proj [4096→n_embd] +``` +Residual added in the main loop. + +## Shared-expert MoE FFN (every layer) +On the `post_attention_norm`'d tensor: +``` +# routed: +moe_out = MoE(x, ffn_gate_inp, {ffn_gate/up/down_exps}, n_expert=256, used=8, + gate=SOFTMAX, norm_topk_prob=TRUE, scale=expert_weights_scale) # SwiGLU experts +# shared (always on): +shexp = SwiGLU(x, ffn_gate_shexp, ffn_up_shexp, ffn_down_shexp) +sgate = sigmoid(ffn_gate_inp_shexp · x) # 1 scalar/token +out = moe_out + sgate · shexp +``` +Extends Pantheon's existing Mixtral MoE (routed top-k SwiGLU) with: **softmax+norm_topk_prob gating** and the **sigmoid-gated shared-expert** add. + +## Dual state (per sequence, per layer) +- **SSM layers (×30):** conv_state `(4-1)·8192 = 24,576` f32 + ssm_state `128·4096 = 524,288` f32 = **548,864 f32/layer**. +- **Attention layers (×10):** KV cache, sized by `n_head_kv=2 × head_dim=256` per token (the *only* growing state). +- Mirror `rwkvx.c`'s dual top-level struct: parallel per-type arrays indexed by per-type index, + `block_order[]`/`block_index[]`. + +## Pantheon mapping (from `rwkvx.c`) + +**Clone wholesale:** dual-state top struct; `block_order`/`block_index` interleave + forward dispatch; model = per-type block-struct arrays + emb/output_norm/head; forward skeleton (embed → ping-pong per-layer loop → final norm → head); engine hooks (add `PN_ARCH_QWEN35MOE` enum + branches in the ~6 `engine.c` sites: `pn_load_ex`, `pn_get_forward_fn`/direct-set, `pn_state_new/free/reset`, `pn_model_free`, generate/stream tokenizer routing); adapter shim mirroring `rwkvx.h`; threadpool TLS; own preallocated workspace (not the arena); q8 matvec infra (reuse for expert matrices). + +**Add (new):** SSM state struct (conv + ssm); the Gated-Delta kernel (recurrence above); shared-MoE FFN; **RoPE + GQA** on the attention block (rwkvx's attn block has neither — borrow RoPE from the llama path). + +**Replace:** RWKV time-mix → SSM kernel; attn internals (add RoPE/GQA, sigmoid output gate); FFN → shared-MoE; RWKV trie tokenizer → Qwen **BPE** (`ft_tokenizer` via the GGUF path); `head_size=64` hardcode → config-driven. + +**Loader:** GGUF (not safetensors). Parse all `qwen35moe.*` keys; honor `nextn_predict_layers=0` and never load `blk.N.nextn.*`. Layer type from `(i+1)%4==0`. Per-layer tensors via the gguf/`pn_qweight` conventions. + +## Implementation order (= Bucket A / Phase 1 A2–A8) +1. **A2** arch+config+loader (detect, parse, GGUF, layer-type, nextn). +2. **A3** full-attention block (closest to known territory — GQA+QK-norm+partial RoPE+sigmoid gate). +3. **A4** SSM kernel (conv → recurrence → gated-norm). Validate this op in isolation first (hardest). +4. **A5** shared-MoE FFN (extend Mixtral path). +5. **A6** assemble hybrid + dual state + engine hooks. +6. **A8** validate vs the llama.cpp oracle (`temp 0`, `oracle_reference.md`). + +## Correctness watch-list +- Decay is **scalar per V-head** (GDA), not per-dim. +- conv runs over the **full 8192** stream, *then* split; q,k get **L2-norm** post-conv. +- Partial RoPE: only first **64** dims. +- Attention layer gate = **sigmoid**; SSM layer gate = **silu** (gated RMSNorm). Don't mix them up. +- Recurrence is **read-after-write** (output reads post-update state). +- Apply the q/√S scale **once**. +- `norm_topk_prob=true` (renormalize the top-8 weights). +- **GQA head broadcast in the SSM (THE A8 gibberish bug — RESOLVED):** the 16 K/Q heads repeat over 32 V-heads via `ggml_repeat`, which **interleaves**: V-head `vh` pairs with K-head `vh % n_k_heads`, i.e. `vh = r*n_k_heads + kh` — **NOT** the blocked `vh = kh*kv_groups + r`. Same trap will appear in the GPU MoE/SSM port. Verified by per-layer activation bisection vs `llama-eval-callback`. +- **ChatML reasoning prefill:** assistant turn opens with `` (`add_generation_prompt`); without it the first generated token diverges from the oracle. Pantheon: `PN_CHAT_CHATML_THINK`. +- ~~QK-norm / ssm_norm `(1+w)`~~ **RESOLVED (not the bug):** GGUF norm weights are ~1-centered (means 0.88–2.59, not ~0), so the converter baked `+1` into them → plain RMS is correct, matches llama.cpp. A8 should look at the **assembly wiring** (residual placement, norm points, kernel-handoff tensor layouts, state init), not the norm convention. + +## A2 findings — apply in A3+ (verified against the real GGUF) + +- **Exact tensor names** (the §blocks above used shorthand): `ssm_conv1d.weight`, `ssm_a` (no `.weight`), `ssm_dt.bias`, `ssm_alpha.weight`, `ssm_beta.weight`, `ssm_norm.weight`, `ssm_out.weight`; attention uses `attn_q/k/v.weight`, `attn_q_norm.weight`, `attn_k_norm.weight`, `attn_output.weight`, `attn_gate.weight`, `attn_qkv.weight`; MoE `ffn_gate_inp`, `ffn_{gate,up,down}_exps`, `ffn_{gate,up,down}_shexp`, `ffn_gate_inp_shexp`. **A2 loads by these real names.** +- **Q3_K added to the CPU path** — the engine lacked Q3_K dequant/`pn_qmv` (only Q4_K/Q5_K/Q6_K). A2 added it (validated: sane stats, no NaN). *Still needs oracle-level verification at A8 — a subtle bit error would only show as output mismatch.* +- **`rope.dimension_sections`** (the multi-section/IMRoPE array) is **not parsed in A2** — only `rope.dimension_count=64`. **A3 must parse the sections array** for partial multi-section RoPE. +- **`token_embd`** kept as dequantized F32 (owned, phi3 convention); `output.weight` (Q6_K) zero-copy `pn_qweight` with tied-embedding fallback. +- **File-split discipline (whale prevention):** A2 is one 607-LOC file. **A3 → `qwen35moe_attn.c`, A4 → `qwen35moe_ssm.c`, A5 → `qwen35moe_moe.c`** — do NOT grow `qwen35moe.c` into a 4th >1000-LOC whale. Keep `qwen35moe.c` for the model struct + load + forward orchestration. + +## K80 GPU decode — status & performance + +**N2-mini (Q3_K_M) runs on a K80 host** via `pn_qwen35moe_forward_cuda`, split across the +two dies of one card (small-BAR — both dies mapped at once, no barswap): front layers +`[0,split)` + token_embd on die A, back layers + output_norm + LM head on die B. Select with +`QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=0 QWEN35_GPU_DIE_B=1` (no barswap acquire — +`QWEN35_GPU_BARSWAP=1` opts back into the legacy single-slot path). + +**Verified single-stream decode: ~13.7 tok/s** (~0.073 s/token, 2 dies, Q3_K_M), and the GPU output +**bit-matches the CPU forward** — argmax `11,1496,271,142,79,88,30,81,30,271,220,381`, token-0 +logit 13.7252, every per-token logit ulp-0 identical. Up from a 0.2 tok/s first-light baseline (~68×). +K80 autoboost swings this ±~30% run-to-run. Actively being optimized (see headroom). + +**What got it there** (all committed): +- GPU matvec kernels for **every quant type** (Q3_K/Q4_K/Q5_K/Q6_K/Q8_0/F32) — the mixed-quant + model was host-falling-back to `pn_qmv` (3.7 s of 5.2 s/token, led by the 248K-vocab Q6_K LM head). +- **Warp-per-row** matvec (one warp/row, 32 lanes coalesce each 256-elem superblock; was + thread-strided → scattered 32-byte segments at 2–8 GB/s → 14–41 GB/s). +- **Grouped MoE** — all 8 top-k experts' gate+up+SwiGLU in one launch + fused down/accumulate (~52→10 ops/layer). +- **Parallel router top-k** (the serial thread-0 selection sort was ~134 ms/token over 40 layers). +- **GPU-resident SSM state** (kills the per-token state round-trip) + per-die scratch pool (kills ~800 alloc/free per token). +- **Quant weights repacked at upload** (`fc_repack_weight`) into an aligned, scale-pre-unpacked, + transposed device layout → coalesced `u32` loads + shared-memory activation staging. Matvec + device-bandwidth ~2× (Q8_0 21→65, Q5_K 16→35, Q3_K 15→30 GB/s), +2 GB/die. Bit-exact (CPU-replica + verifier, ulp 0). + +**Ceiling & headroom:** post-repack the matvecs are **instruction-issue-bound on Kepler** — ~65–70 GB/s +is near the sm_37 limit for *bit-exact* K-quant decode (the earlier ~170 GB/s figure was optimistic). +The remaining single-stream lever is **per-layer kernel fusion** to cut the ~18–20 ms/token of +tiny-kernel launch overhead (~100 F32 router/α/β + 94 Q8_0 launches, ~80 µs each vs ~35 µs of work) +→ ~15–20 tok/s. Beyond that, **"hundreds" is not a single-stream number on Kepler** — it comes from +**speculative block-verify** (DeepSeek DSpark: amortize the bandwidth-bound batch-1 decode across a +verified block) and **continuous batching** across concurrent users (serving-aggregate throughput). + +## Batched prefill (the serving foundation) + +`pn_qwen35moe_forward_batch_cuda` runs **n tokens of one sequence in a single pass**: token-major +`[n][dim]` activations, every per-token matvec batched into one `fc_matvec_*_nx` launch (each weight +block decoded once per 4-column tile — the dequant instruction stream amortizes over the batch), SSM +conv1d + delta-rule recurrence as **in-kernel scans** over the n positions, causal host GQA attention +per position, per-token MoE routing in batched grouped launches. Single-token decode path untouched. + +**Bit-exact check** (2-die split @20, measured with an out-of-tree harness): per-position logits of a batched +12-token prefill are **memcmp-identical (248320 floats × 12)** to the sequential single-token forward +(compare with `QWEN35_FUSE=0x37` — the fused attn qknorm+rope kernel is ~1 ulp off the standalone +pair, a pre-existing fusion contraction property). Prefill → decode continuity verified: one decode +step on top of the batched state matches the fully sequential run exactly. + +**Measured prefill throughput** (2-die split @20, boost clocks): n=32 — sequential 15.8 tok/s vs +batched **32.2 tok/s (2.04×)**; n=128 — sequential 17.8 tok/s vs batched **36.5 tok/s (2.05×)**. +Kernel-level nx speedups are 1.7–3.1× per pass (`foundry/cuda/tests/test_matvec_nx`). Remaining +prefill levers: wider column tiles, batching the host attention/Q-split glue onto the die, and +expert-grouped MoE batching (tokens sharing an expert stream its weights once). + +**Host bring-up prereqs** (else CUDA falls back to CPU or the box hangs): quirk kernel (16-die +small-BAR map), `nvidiafb` blacklisted, `NVreg_EnableMSI=0`, `NVreg_DynamicPowerManagement=0` +(all on the GRUB cmdline), persistence mode — see `foundry/barswap/k80_quirk/README.md`. Profiler: `QWEN35_PROF=1`. + +**Operational:** under sustained high-util load a marginal card can Xid-79 "fall off the bus" +(power/thermal — visible only now that the dies actually work); mitigate with `nvidia-smi -pl 110` +and/or forced-air cooling (K80s are passively cooled). diff --git a/docs/architecture/rwkvx.md b/docs/architecture/rwkvx.md new file mode 100644 index 0000000..0ed0952 --- /dev/null +++ b/docs/architecture/rwkvx.md @@ -0,0 +1,141 @@ +# RWKV-X Architecture + +RWKV-X is a hybrid model architecture that interleaves RWKV-7 recurrent blocks (~75% of layers) with sparse attention blocks (~25%). It is fundamentally different from transformer models and uses safetensors format instead of GGUF. + +## Source + +The RWKV-X engine in Pantheon (`src/models/rwkvx.c`, ~2,500 LOC) is copied from the battle-tested Foundry implementation (`foundry/rwkvx/rwkvx.c`), with Metal, CUDA, HybridMem, training, batch, instrumentation, and OpenMP support stripped. A thin adapter layer wraps it into Pantheon's `pn_model`/`pn_state`/`pn_forward_fn` interface. + +## Model Architecture + +For the 0.2B model (12 RWKV + 4 attention layers), block execution order is: + +``` +R0, R1, R2, A0, R3, R4, R5, A1, R6, R7, R8, A2, R9, R10, R11, A3 +``` + +Where `R` = RWKV-7 recurrent block, `A` = sparse attention block. + +### Forward Pass + +``` +embed(token) + → for each block in interleaved order: + if RWKV-7 block: + LayerNorm → TimeMix (WKV-7 kernel) → residual + → LayerNorm → ChannelMix FFN → residual + if attention block: + LayerNorm → Multi-Head Attention (with KV cache) → residual + → LayerNorm → ChannelMix FFN → residual + → LayerNorm → LM head → logits +``` + +### WKV-7 Kernel (Generalized Delta Rule) + +The core recurrent kernel, computed per-head per-timestep: + +``` +for each timestep t, for each head h: + state *= diag(w[t]) // decay + state += (a[t] @ state) * k^T // delta rule bonus + state += v[t] @ k[t]^T // key-value update + output[t] = r[t] @ state // readout +``` + +State is a `[head_size, head_size]` matrix per head per RWKV-7 layer — fixed size regardless of sequence length. + +### Attention Blocks + +Standard multi-head attention with KV cache. Uses cached keys and values up to a configurable max length (default 2048). No RoPE — position information comes from the surrounding RWKV-7 blocks. + +## Weight Format + +RWKV-X uses safetensors (not GGUF). Weights are loaded via Foundry's `fs_open()` (zero-copy mmap) and routed by prefix: + +| Prefix | Block Type | +|--------|-----------| +| `rwkv.blocks.N.*` | RWKV-7 recurrent block N | +| `moba.blocks.N.*` | Sparse attention block N | +| `emb.weight` | Token embeddings | +| `head.weight` | LM head | +| `ln_out.*` | Final layer norm | + +Weight transformations at load time: +- Linear weights are transposed (safetensors stores `[out, in]`, inference needs `[in, out]`) +- Singleton dimensions are squeezed +- `att.r_k` is flattened from `[n_head, head_size]` to `[n_embd]` +- Embeddings are pre-normalized (L2 norm per row) + +## INT8 Quantization + +When `--quantize` is passed, all linear projection weights are quantized to INT8 at load time using per-row symmetric quantization: + +``` +scale[row] = max(abs(row)) / 127 +quantized[row][col] = round(float[row][col] / scale[row]) +``` + +Inference uses a NEON-accelerated INT8 dot product kernel (`fast_matvec_q8`) that pre-quantizes the input vector once, then computes INT8 dot products for all output rows. This gives ~3x speedup on ARM. + +## Tokenizer + +RWKV-X uses a **trie-based World tokenizer** with 65,536 tokens. The tokenizer operates on raw UTF-8 bytes with greedy longest-match — fundamentally different from BPE or SentencePiece. + +The vocab is loaded from a text file (`rwkv_vocab_v20230424.txt`) via the `--vocab` CLI flag. Each line contains a token ID, token length, and the token's byte representation as a Python byte literal. + +The tokenizer is stored inside the RWKV-X model struct (not in `pn_model->tokenizer`). The engine routes to it via `pn_rwkvx_encode()`/`pn_rwkvx_decode()` when `config.arch == PN_ARCH_RWKVX`. + +## Dual State + +Each inference session maintains two state types in `pn_state->kv_cache`: + +- **Recurrent state**: Fixed-size per-layer matrix `[n_heads, head_size, head_size]` for RWKV-7 blocks +- **KV cache**: Key-value store for sparse attention blocks (capped at `max_cache_len`) +- **Workspace**: Pre-allocated scratch buffers (16 buffers + ping-pong pair + INT8 buffer) + +All state is encapsulated in a single opaque struct. `pn_state_reset()` zeros both the recurrent state and KV cache. + +## Adapter API + +The adapter layer in `rwkvx.c` maps Pantheon's interface to internal RWKV-X functions: + +| Pantheon API | Internal Function | +|-------------|-------------------| +| `pn_rwkvx_load()` | `rx_model_load_internal()` | +| `pn_rwkvx_free()` | `rx_model_free_internal()` + tokenizer free | +| `pn_rwkvx_forward()` | `rx_forward_internal()` | +| `pn_rwkvx_state_new()` | `rx_state_new_internal()` | +| `pn_rwkvx_state_free()` | `rx_state_free_internal()` | +| `pn_rwkvx_state_reset()` | `rx_state_reset_internal()` | +| `pn_rwkvx_encode()` | `rx_tokenizer_encode()` | +| `pn_rwkvx_decode()` | `rx_tokenizer_decode()` | + +## Usage + +```bash +# FP32 inference +./pantheon --model ../models/base/RWKV-X-0.2B-64k-Base.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello world" --max-tokens 32 + +# INT8 quantized inference (~3x faster decode on ARM) +./pantheon --model ../models/base/RWKV-X-0.2B-64k-Base.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello world" --max-tokens 32 --quantize +``` + +## What Was Stripped + +The Foundry RWKV-X engine has additional features not yet in Pantheon: + +| Feature | Why Stripped | +|---------|-------------| +| Metal GPU (`fm_*`) | Pantheon doesn't have Metal model integration yet | +| CUDA GPU (`fc_*`) | Pantheon doesn't have CUDA model integration yet | +| HybridMem (`hm_*`) | Separate feature, not part of base inference | +| Training (`rx_forward_ex`, callbacks) | Training not in Pantheon scope yet | +| Batch forward (`rx_forward_batch`) | Prefill optimization, add later | +| Instrumentation (attn_log, embed_capture) | Debug tooling, add later | +| OpenMP (`#pragma omp`) | Pantheon uses threadpool, not OpenMP | + +These can be added incrementally in future phases. diff --git a/docs/architecture/serve.md b/docs/architecture/serve.md new file mode 100644 index 0000000..f5baf84 --- /dev/null +++ b/docs/architecture/serve.md @@ -0,0 +1,553 @@ +# Serve Layer Architecture + +Pantheon's serve layer implements an OpenAI-compatible HTTP inference server. +Entry point: `src/serve/serve_command.c`. The serve layer sits above the engine +(`src/core/engine.c`) and is reached via `pantheon serve`. + +--- + +## Overview + +At startup `pn_serve_main` (`serve_command.c:206`) parses one or more `--model` +flags, loads each model into an `mr_registry`, binds a TCP socket, and enters an +infinite `accept()` loop. Every accepted connection is handed to a detached +`pthread` (`spawn_connection`, `serve_command.c:185`); if `pthread_create` fails +it falls back to handling the request inline so no connection is ever silently +dropped. + +The connection thread calls `handle_connection` → `dispatch`. Dispatch is a +straight O(route-count) `strcmp` chain (`serve_command.c:124`): + +| Method | Path | Handler | +|--------|------|---------| +| `OPTIONS` | any | CORS preflight: `hs_send_json 200 {}` | +| `GET` | `/v1/models` | `oai_handle_models` | +| `POST` | `/v1/chat/completions` | `oai_handle_chat` | +| `POST` | `/admin/models/load` | `ma_handle_load` | +| `POST` | `/admin/models/unload` | `ma_handle_unload` | +| `GET` | `/admin/dies` | `ma_handle_dies` | +| `GET` | `/` | health: `{"service":"pantheon","status":"ok"}` | + +`GET /v1/models` and admin routes never touch any generation lock; they can +respond concurrently with an in-flight generation on any model. + +### Request lifecycle (socket → SSE) + +``` +accept() + └─ spawn detached pthread + └─ hs_read_request() (http_server.c — reads headers + body) + └─ dispatch() + └─ oai_handle_chat() + ├─ jr_parse(body) + ├─ parse_chat_request() (extracts model, params, session_id) + ├─ route_model() (mr_acquire → entry + refcount++) + │ + ├─ [batched path: QWEN35_BATCH_SLOTS=1] + │ ├─ bs_slot_acquire_or_wait() (blocks up to 30 s) + │ └─ sg_run_completion_batched() → SSE + │ + └─ [single-user path: sched == NULL] + ├─ pthread_mutex_trylock(&entry->gen_lock) → 429 if busy + └─ sg_run_completion() → SSE + └─ mr_release(reg, entry) (refcount--) +``` + +--- + +## Model Registry and Routing + +**Files:** `src/serve/model_registry.h`, `src/serve/model_registry.c` + +### Data structures + +```c +/* model_registry.h:54 */ +typedef struct { + char name[MR_MAX_NAME]; /* served model id */ + pn_model *model; + pn_state *state; /* single-user state (sched==NULL) */ + pthread_mutex_t gen_lock; /* single-user generation serializer */ + pn_batch_sched *sched; /* C3: non-NULL when batching is on */ + pn_prefix_cache *prefix_cache; /* PC1: session prefix cache */ + pthread_mutex_t pc_lock; /* serializes prefix-cache ops */ + int dies[PN_MODEL_MAX_DIES]; + int n_dies; /* 0 = CPU-resident */ + uint64_t vram_bytes; + int refs; /* in-flight acquires */ + bool unloading; + bool used; +} mr_entry; + +/* model_registry.h:86 */ +typedef struct { + mr_entry entries[MR_MAX_MODELS]; /* MR_MAX_MODELS = 16 */ + int n; + pthread_mutex_t lock; + pthread_cond_t drained; /* signaled on every mr_release */ + pthread_mutex_t upload_lock; /* A3: one GPU weight upload at a time */ +} mr_registry; +``` + +### Loading (`mr_load`, `model_registry.c:90`) + +Load is a four-phase operation designed to keep the registry serviceable +throughout: + +1. **Slot reservation** (under `reg->lock`): a slot is marked `used=true` with + no model pointer yet — the name is taken but the entry is not yet acquirable + (`require_ready` skips it in `find_locked`). +2. **Placement + VRAM admission** (lock-free): auto-placement via the die manager + (`pn_dm_place`), or explicit dies from `--dies`. `pn_admit_model_load` gates + against the per-die budget using a conservative 8 k context guess + (`MR_ADMIT_CTX_GUESS = 8192`, `model_registry.c:36`). `pn_dm_bind` commits the + reservation. +3. **Weight upload** (under `reg->upload_lock`): `pn_load_on` runs while the + registry stays fully serviceable. `upload_lock` ensures at most one GPU upload + is in flight at a time, preventing two concurrent 2.4 GB transients stacking + on an 8 GB host (`model_registry.c:213`). +4. **Publish** (under `reg->lock`): model, state, locks, and die info are written + atomically and `reg->n` is incremented. After publish, the batch scheduler + (`bs_create`) and prefix cache (`pn_prefix_cache_new`) are created and + published under the lock. Both are fail-soft: failure leaves the model in + single-user mode or full-prefill mode respectively. + +### Acquire / release refcounting + +```c +mr_entry *mr_acquire(mr_registry *reg, const char *name); /* model_registry.c:322 */ +void mr_release(mr_registry *reg, mr_entry *entry); /* model_registry.c:358 */ +``` + +`mr_acquire` locks `reg->lock`, scans entries, increments `refs`, and returns +the pointer. Every user of an entry MUST pair `mr_acquire` with `mr_release`. +`mr_release` decrements `refs` and broadcasts `reg->drained`; `mr_unload` waits +on that condvar until `refs == 0` before freeing. + +### Multi-card load-balancing (`model_registry.c:329`) + +An operator may place one model id on several cards (`--dies 4,5` and `--dies +6,7` both under `--name my-model`). `mr_acquire` finds all ready entries that +share the requested name and picks the one with the fewest in-flight `refs` — +round-robin load balance at the reference granularity: + +```c +for (int i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *c = ®->entries[i]; + ... + if (!e || c->refs < e->refs) + e = c; +} +``` + +Empty/null name resolves to the first (lowest slot index) resident entry — +the back-compat single-model shortcut (`model_registry.c:344`). + +### Route model (`openai_chat.c:499`) + +```c +static mr_entry *route_model(int client_fd, mr_registry *reg, const char *name) +{ + mr_entry *entry = (mr_count(reg) <= 1) ? mr_acquire(reg, NULL) + : mr_acquire(reg, name); + ... +} +``` + +With exactly one resident model, every request routes to it regardless of the +`model` field — preserving single-model server behavior. With more than one, +an exact name match is required; an unknown name returns 404, no loaded model +returns 503. + +### `/v1/models` dedupe (`openai_chat.c:266`) + +`oai_handle_models` calls `mr_snapshot` to get all resident entries, then +dedupes by name before serializing JSON — a model placed on two cards appears +as one logical id: + +```c +bool seen = false; +for (int j = 0; j < i; j++) + if (strcmp(info[j].name, info[i].name) == 0) { seen = true; break; } +if (seen) continue; +``` + +### Unload (`mr_unload`, `model_registry.c:369`) + +Marks `unloading=true` (blocking new acquires), decrements `reg->n`, then +blocks on `reg->drained` until `refs == 0`. Detaches all pointers under the +lock and frees them outside: prefix cache → batch scheduler → die binding → +state → model. + +--- + +## Batched Decode (`batch_sched.c`) + +**Feature gate:** `QWEN35_BATCH_SLOTS=1` (env var, CUDA build only). +`bs_enabled()` (`batch_sched.c:44`) caches the result. + +When enabled, `mr_load` calls `bs_create(model, BS_MAX_USERS)` where +`BS_MAX_USERS = 8` (`batch_sched.h:173`). The scheduler owns 8 `pn_state*` +instances (one per slot) and a background driver thread. + +### Slot lifecycle + +``` +IDLE -(bs_slot_acquire/acquire_or_wait)-> PREFILL + -(bs_slot_bind / bs_slot_bind_ex)-> BOUND + -(driver first step)--------------> DECODE + -(bs_slot_release)----------------> IDLE +``` + +Only the owner thread moves a slot back to IDLE. The driver marks `finished=true` +on EOS/budget but never calls `bs_slot_release` — it always signals the owner +to drain and release. + +### GPU-ownership turnstile (`batch_sched.c:72`) + +The model's single-user forward machinery (C2 async phase machine, shared live +SSM buffers, shared GPU KV ring) supports exactly one in-flight GPU forward at +a time. Pre-C3 the per-model `gen_lock` enforced this. The batch scheduler uses +a FIFO ticket turnstile instead — `gpu_ticket_next` / `gpu_ticket_serving` / +`cv_gpu` — which prevents the free-running L3 driver from barging ahead of a +waiting prefiller: + +```c +static void bs_gpu_acquire_locked(pn_batch_sched *sched) { /* batch_sched.c:72 */ + uint64_t my = sched->gpu_ticket_next++; + while (sched->gpu_ticket_serving != my) + pthread_cond_wait(&sched->cv_gpu, &sched->mu); +} +``` + +A turn owner never `cond_wait` (chunks and steps are bounded GPU work), so +the turnstile is deadlock-free by construction. + +### D1 overflow queue + D3 admission backpressure + +`bs_slot_acquire_or_wait` (`batch_sched.c:1475`, called from `openai_chat.c:577` +with a 30 s timeout) replaces the immediate 429: + +- **Fast path:** slot free AND `pressure_level < BS_PRESSURE_HOLD_LEVEL` (3 = + CRITICAL) → acquire immediately. +- **Slow path:** enqueue (`n_queued++`) and block on `cv_idle`. + - Queue full + pressure < CRITICAL → 429 + `Retry-After: 5`. + - Queue full + CRITICAL → 503. + - Timeout after `wait_ms` → 429 + `Retry-After: 2`. + - Shutdown → 503. + +`BS_QUEUE_DEPTH_DEFAULT = 32` (`batch_sched.h:274`). Override with `BS_QUEUE_DEPTH`. + +`bs_notify_pressure` (`batch_sched.c:1589`) is called by the serve path before +every `bs_slot_acquire_or_wait` (`openai_chat.c:569`) to push the live +`pn_mem_state` into the scheduler, so the gate sees the current host state at +decision time. + +### D2 — continuous batching / dynamic-width step + +The driver compacts the active (BOUND/DECODE) slots into GPU indices +`[0, n_active)` each step. Idle slots pay zero GPU compute. When `active_set_changed` +is set (join, leave, or finish), `rebuild_gpu_map` (`batch_sched.c:237`) runs +a greedy permutation algorithm that handles arbitrary reorderings — including +cycles that require a scratch slot (`cap` index, `batch_sched.c:279`). This +fixed the K≥5-concurrent-joiners bug where the old downward-only move refusal +caused all active users to emit empty content. + +### L3 — in-driver sampling (default on) + +`bs_slot_bind_ex` (`batch_sched.c:1294`) passes the request's sampling params, +repeat-penalty window, and token budget to the driver. After each `decode_slots` +step the driver samples every slot itself — sequentially, deterministically — +and pushes the result to a per-slot output ring sized to the full budget: + +```c +uint32_t next = pn_sample_next_token(lo[g], sched->n_vocab, + &sl->sp, sl->recent, sl->n_recent); +sl->ring[sl->ring_w++] = next; /* batch_sched.c:545 */ +``` + +Owners drain the ring with `bs_slot_pop_token` and do detokenize/emitter/SSE +entirely off the step's critical path. A slow or stalled client cannot stall +the rest of the batch. + +The ring is never resized after bind (`ring_cap == budget`), so it can never +overflow and the driver never blocks on a slow consumer. + +Stop-string detection stays owner-side (text level); a slot may over-generate +past a stop string until its owner drains and releases — surplus ring tokens +are discarded. + +Disable with `QWEN35_BS_DRIVER_SAMPLE=0` to restore legacy lockstep +logits/token exchange. + +### L2 — chunked prefill with yield windows + +Prefill runs in chunks of `QWEN35_BS_PREFILL_CHUNK` tokens (default 128). +After each chunk (except the last) `bs_prefill_yield` (`batch_sched.c:1040`) +makes the slot's host mirror authoritative via `gpu_state_to_host`, releases +the GPU turn, and re-acquires it FIFO — giving the driver one decode step +between chunks. This bounds a joiner's stall on concurrent decoders to one +chunk instead of one full prompt. + +### Single-user fallback + +When `QWEN35_BATCH_SLOTS` is not set (or the build has no CUDA), `entry->sched` +is `NULL`. The serve path (`openai_chat.c:658`) falls back to: + +```c +if (pthread_mutex_trylock(&entry->gen_lock) != 0) { + hs_send_error(client_fd, 429, "server_busy", + "a generation is already in progress on this model; " + "retry shortly"); + ... +} +``` + +A second concurrent request to the same model gets an immediate 429. Requests +to different models run concurrently (each holds only its own entry's lock). + +--- + +## Generation and Streaming (`serve_generation.c`) + +### Prompt building + +`build_prompt` (`serve_generation.c:120`) tokenizes the full `messages[]` array +into ChatML format using the model's tokenizer: +`<|im_start|>role\ncontent<|im_end|>\n` for each turn, with `tool` messages +wrapped in ``. Opens the assistant turn and prefills `` +for reasoning models (`PN_CHAT_CHATML_THINK`). + +### Admission + +Before any prefill, `pn_admit` checks live host + die memory and either passes, +caps the context (sliding-window oldest-token drop), or denies. A denied +admission returns `SG_ADMIT_DENIED`, which the serve path converts to a 503 +(`openai_chat.c:683`). A capped admission sets `result->context_capped = 1` and +disables prefix caching for that turn; the client sees `"context_capped": true` +on the response. + +### Single-user completion (`sg_run_completion`, `serve_generation.c:525`) + +``` +build_prompt → pn_admit → [prefix cache acquire] → prefill chunks + → sample first token → decode loop (sg_forward_step per token) + → [prefix cache commit] → return sg_result +``` + +`sg_forward_step` (`serve_generation.c:411`) uses an async non-blocking launch + +event-poll when the model supports it (`pn_forward_step_begin/poll`), sleeping +50 µs → 500 µs between polls. This allows up to 8 per-card connection threads +to run their cards simultaneously from a 2-core host without blocking each other. + +### Batched completion (`sg_run_completion_batched`, `serve_generation.c:744`) + +Same flow as single-user but: +- Prefill takes the GPU turn via `bs_prefill_begin` or `bs_prefill_resume`. +- Chunked prefill with `bs_prefill_yield` windows (L2). +- After the last chunk, `bs_slot_bind_ex` (L3) or `bs_slot_bind` (legacy) hands + the slot to the driver. +- L3 path: drain the ring with `bs_slot_pop_token` → detokenize → emitter → SSE, + all off the critical path. +- `bs_slot_finalize` (if caching) then `bs_slot_release`. + +### Streaming (SSE) + +`run_streaming` / `run_streaming_batched` (`openai_chat.c:434,464`) call +`hs_begin_sse` and register `stream_delta_cb` as the `delta_fn`. The callback is +invoked by the emitter for each text fragment: + +```c +static void stream_delta_cb(const char *text, int is_reasoning, void *ud) { + /* openai_chat.c:397 */ + jw_raw(&jw, is_reasoning ? "\"reasoning_content\":" : "\"content\":"); + jw_string(&jw, text); + send_chunk(sc, jw.data, NULL); /* hs_send_sse */ +} +``` + +A final chunk carries `finish_reason`; then `hs_end_sse` writes `data: [DONE]`. + +### Reasoning / stop-sequence emitter + +The `emitter` struct (`serve_generation.c:179`) handles three channels: +- **in_think**: text before `` → `reasoning_content`; switches to content + channel on ``. +- **content**: after ``, before a stop string or ``. +- **in_tool**: raw text after `` → captured in `tool` channel for + structured parsing by `tool_calling.c`. + +A `carry` buffer holds the longest suffix that could start any marker or stop +string across a token boundary. + +### Prefix cache (`src/core/prefix_cache.c`, used via `serve_generation.c:574`) + +Keyed by `session_id` (from body field `session_id` or header `X-Session-Id`). +Single-user path: `pn_prefix_cache_acquire` returns the session's `pn_state` at +the delta boundary; on commit, `pn_prefix_cache_commit` records the full token +history. The `pc_lock` is not needed on the single-user path (`gen_lock` already +serializes), so `pcx.pc_lock = NULL` (`openai_chat.c:674`). + +Batched path (PC-batched): session affinity is slot-resident. `bs_slot_acquire_biased` +prefers the slot that already holds this session's warm state. `bs_prefill_resume` +checks if the slot's committed history (`hist`) is an exact prefix of the new +prompt and skips the already-committed tokens. `bs_slot_finalize` snapshots the +GPU state to host and publishes the driver-maintained `ct` (committed-token log) +as `hist` for the next turn. + +--- + +## Concurrency Model + +| Lock | Scope | What it protects | +|------|-------|-----------------| +| `reg->lock` | `mr_registry` | slot table (used/refs/unloading/n); brief critical sections only | +| `reg->drained` | condvar on `reg->lock` | unload drain wait | +| `reg->upload_lock` | `mr_registry` | one GPU weight upload in flight at a time | +| `entry->gen_lock` | `mr_entry` | single-user generation serializer (trylock → 429) | +| `entry->pc_lock` | `mr_entry` | prefix cache ops on the single-user path (unused — gen_lock covers it; kept for the batched concurrent-slot case) | +| `sched->mu` | `pn_batch_sched` | ALL slot state: status words, token/logits flags, rings, turnstile counters, D1 queue depth, D3 pressure level | +| `sched->cv_gpu` (turnstile) | under `sched->mu` | one GPU forward in flight at a time (prefill chunk, bind, or decode step) | +| `sched->cv_work` | under `sched->mu` | driver wake (gate may have opened) | +| `sched->cv_idle` | under `sched->mu` | D1 waiter wake (slot freed or pressure dropped) | +| `sl->cv` | per-slot, under `sched->mu` | driver → owner wakeup (logits/ring/finished) | + +Connection threads are otherwise independent: each owns its own +`hs_request`, `chat_request`, `sg_result`, and `emitter`. The sampler scratch +(`pn_sample_next_token`) is `_Thread_local`. + +The `accept()` loop is single-threaded but non-blocking per iteration. Heavy +work (parsing, prefill, decode, SSE write) runs entirely in per-connection +threads, so cheap routes (`/v1/models`, `/`) are never queued behind an +in-flight generation. + +--- + +## Admin API (`model_admin.c`) + +| Route | Body | Action | +|-------|------|--------| +| `POST /admin/models/load` | `{"path":"..","name":"..","dies":[..],"quantize":false}` | `mr_load` — may block ~55 s for a GPU upload | +| `POST /admin/models/unload` | `{"name":".."}` | `mr_unload` — drains in-flight refs then frees | +| `GET /admin/dies` | — | `pn_dm_get` snapshot of all dies (budget/used/model) | + +Error codes from `status_for_reason` (`model_admin.c:28`): admission/table-full +→ 507, duplicate/unloading → 409, not-found → 404, other → 500. + +A runtime `/admin/models/load` arriving while a startup upload is in flight +blocks on `reg->upload_lock` and is refused promptly by the slot scan once the +prior load finishes — never stacks two 2.4 GB transients. + +--- + +## Router Registration (`serve_registry.c`) + +**Gating:** fully off unless `PANTHEON_REGISTRY_URL` is set. `rc_enabled()` is checked at +`serve_registry_start` entry; with it unset the function returns immediately and the +serve process is byte-for-byte unchanged (`serve_command.c:350`, `serve_registry.c:78`). + +When enabled, at startup `serve_registry_start` (`serve_registry.c:76`) iterates every +loaded model (via `mr_snapshot`) and calls `rc_register` for each with: + +``` +model_id = info[i].name /* the shared served identity */ +instance_id = host:port:model /* serve_registry.c:98 — unique per process */ +endpoint = http://host:port +capacity = batch-slot count (1 if single-user) +``` + +A background heartbeat thread then re-acquires each entry every +`RW_HEARTBEAT_SECONDS` to read live load (`bs_get_load`) and POST it to the router. + +### What this means for in-process replicas + +Two replicas of `ornith` on dies 2 and 3 in **the same process** share the same +`host:port`, so they produce the same `instance_id = host:port:ornith` and **collapse +to one instance** at the router. The router sees one `ornith` endpoint, not two. + +This is correct and intentional: **in-instance replica load-balancing is handled +entirely by `mr_acquire`** (least-busy across same-name entries — see above). The router +is the **multi-instance dispatch layer** for tying separate `pantheon serve` processes +together. A single self-routing instance does not need it. + +> Cross-link: docs/README.md "Multi-Model Smart Routing" — "one instance is fully +> self-routing and needs nothing in front of it." + +--- + +## Environment Variables + +| Variable | Default | Effect | +|----------|---------|--------| +| `QWEN35_BATCH_SLOTS` | off | `1` enables multi-user batched decode (C3) | +| `QWEN35_BS_DRIVER_SAMPLE` | `1` | `0` restores legacy owner-side lockstep sampling | +| `QWEN35_BS_PREFILL_BATCH` | `1` | `0` disables batched chunk prefill (token-by-token) | +| `QWEN35_BS_PREFILL_CHUNK` | `128` | chunk size in tokens for L2 yield windows | +| `BS_QUEUE_DEPTH` | `32` | max D1 queue depth before 429 | +| `QWEN35_BS_STATS` | off | `1` logs per-step timing to stderr | +| `QWEN35_KV_Q4` | `1` | `0` disables Q4 KV quantization in admission math | +| `QWEN35_GPU_FULL_LOGITS` | off | Refused at serve startup — forces 248k-vocab D2H copy | +| `PN_ASYNC_POLL_US` | `500` | Max sleep between async forward-step event polls (µs) | +| `PANTHEON_REGISTRY_URL` | unset | Set to enable router registration (`serve_registry.c:78`); unset = no-op | +| `PANTHEON_NODE_ID` | `node0` | Node identifier sent in router registration (`serve_registry.c:83`) | +| `PANTHEON_REGISTRY_TRANSPORT` | `local` | Transport hint sent to router on registration (`serve_registry.c:84`) | + +--- + +## Known Sharp Edges + +- **`mr_unload` blocks its caller** until all in-flight `refs` drain + (`model_registry.c:389`). A long generation on the model being unloaded will + hold the admin response for its full duration. There is no timeout. + +- **Duplicate names are explicitly allowed** (`model_registry.c:119`). An + operator placing the same model id on cards 4 and 5, then unloading by name, + will unload whichever slot `find_locked` finds first — the other remains. There + is no unload-by-slot-index API; the operator must call unload twice. + +- **`make_completion_id` uses `rand()`** without seeding (`openai_chat.c:87`). + `rand()` is not seeded with `srand()`, so id generation is not + cryptographically random and starts from the same sequence on every process + restart. Collision risk is low (hex8 = 4B permutations) but completions ids + are not guaranteed unique across restarts. + +- **`pc_lock` is allocated but never taken on the single-user path** + (`openai_chat.c:674`, `pcx.pc_lock = NULL`). On the batched path each slot + owns its own `hist`/`ct` structures guarded by `sched->mu`, so `pc_lock` is + also unused there. The lock exists as a slot for a future concurrent-slot + single-cache design. + +- **Stop-string detection fires owner-side** after the driver has already + stepped tokens into the ring (L3 path). The driver steps until EOS/budget; + the owner drains until a stop string is detected in the decoded text. + Over-generation is bounded by `max_tokens` but the surplus ring tokens are + silently discarded (`serve_generation.c:930-942`). This means + `completion_tokens` in the usage field may be slightly lower than the number + of steps the driver ran. + +- **GPU lazy init hangs if called too early** (`batch_sched.c:714`). GPU lane + registration (`bs_lazy_init_gpu`) is deferred to the first `bs_slot_bind` to + avoid a CUDA context timing issue right after a long weight upload. If the + context is still unsettled when the first request arrives, the bind will hang + or fail. The scheduler is fail-soft here (bind failure → -1 → request gets a + 500), but a burst of concurrent first requests could all fail before the + context settles. + +- **The `reg->lock` is brief but coarse** during `mr_load`'s slot reservation + phase (`model_registry.c:123`). A concurrent load + multiple request threads + all briefly contend on the same mutex. In practice this is negligible (the + critical sections are O(MR_MAX_MODELS) array scans at most), but worth noting + for any future lock-free refactor. + +- **Cross-replica session affinity is not implemented.** `mr_acquire` selects the + least-busy replica by `refs` with no stickiness (`model_registry.c:329-341`). A + multi-turn session with `session_id` can hop replicas between turns, missing its + prefix cache on the new replica and paying a full reprefill. The single-die batched + path has slot-resident affinity (`bs_slot_acquire_biased`), but that only applies + within one replica. Cross-replica affinity is designed but not yet shipped. + +- **`QWEN35_GPU_FULL_LOGITS` is a hard startup guard** (`serve_command.c:279`). + Setting it causes the server to refuse to start with an explicit error. It is + a debug knob that forces the full 248k-vocab logit tensor D2H per step and + caps throughput on the K80 host link (~0.4 GB/s). The guard is intentional and + correct; do not remove it. diff --git a/docs/coding-best-practices.md b/docs/coding-best-practices.md new file mode 100644 index 0000000..d2843e5 --- /dev/null +++ b/docs/coding-best-practices.md @@ -0,0 +1,204 @@ +# Coding Best Practices — C + +The standard code in this repo is held to. PRs are reviewed against it, and review +comments cite it by rule number (`blocker: rule 3, this file is 1,400 lines`). + +Rule numbers are frozen. New rules append; they never renumber. + +The PR process itself — size, evidence, comment conventions — is in +[`../CONTRIBUTING.md`](../CONTRIBUTING.md). + +--- + +## Rules + +**1. Descriptive names.** Every name says what it is at a glance — files, functions, +types, variables. The test: would you know what it is from the tree alone, without +opening it? `pn_prefix_cache_lookup()`, not `lookup()`. Abbreviate only what is +universal (`id`, `url`, `http`, `kv`, `ctx`). + +**2. Small functions — ~100 lines of body.** Doc comments don't count. Past ~100, look +for the extraction point. One blank line between logical chunks, never two. + +**3. Small files — under 1,000 lines of code.** Comments and headers don't count. Prefer +more files with fewer functions. Every function in a file must belong there. + +**4. Library-first.** Write every function as if it will be extracted into a standalone +library — self-contained, minimal dependencies, no hard-coded project values in reusable +logic. Headers define the contract, sources hold the implementation. Extraction should +mean moving files, not rewriting them. + +**5. Folder nesting is navigation.** Group by domain, not by file type. Each directory +has one clear purpose. A directory holding one file is usually wrong. + +**6. Error transparency.** Never swallow an error. Return a code or log it — never both +nothing. No empty error paths. Callers get a typed error, not a guess. + +**7. Delete dead code.** Don't comment it out, don't leave unused functions or includes. +Git has it if you need it back. + +**8. Prove a test can fail.** A test you have never seen fail is not evidence. Break the +code it covers, watch it go red, report the failure count. + +> **Apple's `make` is 3.81 (2006)** and compares mtimes at one-second granularity. A +> source edited in the same second as its object is reported `up to date` and is **not +> rebuilt** — so the obvious mutation loop can be running the previous binary and +> reporting a vacuous gate that isn't. When proving a test red, `rm` the specific `.o` +> **and** the linked binary first. Switching build configs needs `rm -rf build` entirely. + +--- + +## File headers + +Every source file opens with the SPDX pair, then the header block: + +```c +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file prefix_cache.c + * @brief Longest-common-prefix KV reuse across turns. + * + * @status None + * @issues None + * @todo None + */ +``` + +`@status` / `@issues` / `@todo` are always present, `None` when empty — the empty slot is +the reminder that the slot exists. When you work in a file, clear resolved entries and +reset to `None`. + +**Required on every new file.** Existing coverage is partial and is being backfilled; +until that lands, CI enforces the rule on added files only. + +**Exempt:** test files, generated sources, and third-party-derived files (which keep their +upstream attribution — a Pantheon block may go below it, never in place of it). + +--- + +## Documentation + +**Public functions are documented in the header**, which is the API contract: + +```c +/** + * Look up the longest cached prefix for a token sequence. + * + * @param cache Prefix cache handle + * @param tokens Token sequence to match + * @param n Length of tokens + * @return Matched prefix length, or 0 if nothing matched + * + * Thread-safe: yes (internal locking) + * Reentrant: no + * Signal-safe: no + */ +``` + +The three safety annotations are always present. `TBD` is an acceptable answer; omitting +them is not. Internal helpers need docs only when the name and signature don't carry it. + +**Inline comments explain why, not what.** The code already says what it does. + +```c +/* K80 has no __ldg for 8-byte types — split the load or it serialises */ +``` + +--- + +## Layout + +`include/` and `src/` mirror each other. `snake_case` filenames that name their contents +(`die_manager.c`, not `dm.c`). One header + one source per logical unit. Tests mirror the +source tree. + +Every public symbol carries its module prefix — `pn_` for Pantheon, `fc_`/`tm_`/`ft_` for +Foundry. Anything not declared in a header is `static`. No exceptions. + +--- + +## C conventions + +**Formatting.** 4 spaces, no tabs. 100 columns. `char *ptr` — the star binds to the +variable. Opening brace on the **same line**, for functions and control flow alike. + +```c +int pn_cache_lookup(pn_cache *cache, const uint32_t *tokens, size_t n) { + if (!cache || !tokens) + return -PN_EINVAL; + + for (size_t i = 0; i < cache->count; i++) { + if (entry_matches(&cache->entries[i], tokens, n)) + return (int)cache->entries[i].len; + } + return 0; +} +``` + +Omit braces on single-statement bodies — unless any branch of an `if`/`else` chain needs +them, in which case every branch gets them. + +**Includes**, four groups separated by blank lines: this module's own header first (it +catches missing dependencies in that header at compile time), then standard library, then +system/platform, then project headers. + +**Errors.** Functions return `int` (0 success, negative error) or a pointer (`NULL` on +failure). Use goto-cleanup for anything acquiring more than one resource — one exit path, +deterministic teardown. Error codes are a prefixed enum with a string conversion function. + +**Memory.** `sizeof(*ptr)`, never `sizeof(Type)`. `calloc` or explicit zeroing. Document +ownership in the function doc — who allocates, who frees. Every `_open`/`_create`/`_new` +has a matching `_close`/`_destroy`/`_free`. `free(p); p = NULL;`. Arena-allocate groups +sharing a lifetime. Never return a stack address. + +**const.** Every pointer parameter is `const` unless the function must modify what it +points at. + +**Strings and buffers.** `sprintf`, `strcpy`, `strcat`, `gets`, and unbounded `scanf("%s")` +are banned — use `snprintf`, `strlcpy`, `strlcat`, `fgets`, width-limited `scanf`. Always +pass buffer sizes explicitly; check the `snprintf` return for truncation. + +**Initialize everything.** `int rc = 0;`, `char *buf = NULL;`, `struct foo f = {0};`. +Declare close to first use, except where goto-cleanup requires it earlier. + +**Opaque types** at public API boundaries — the struct definition stays in the `.c`. +Callers go through the API, never into fields. + +**Header guards** use the project prefix (`PANTHEON_PREFIX_CACHE_H`), with the guard name +repeated in the `#endif` comment, and `extern "C"` in every public header. + +**Asserts** document invariants that must never be false if the code is correct. Don't +assert things that legitimately fail — I/O, allocation, user input — those get error codes. + +**Compiler.** The standard, modelled on SQLite and comparable C projects: + +``` +-std=c11 -Wall -Wextra -Werror -pedantic +``` + +plus `-Wshadow`, `-Wstrict-prototypes`, `-Wformat=2`, `-Wnull-dereference`, +`-Wconversion`, `-fstack-protector-strong`. Zero warnings, always. + +The build does not yet meet this — it is `-Wall -Wextra -std=c11` today. Closing that gap +is tracked in the conformance spec in `plans/`; the standard above is the target the code +is held to, not a description of the current Makefile. + +--- + +## Logging + +| Level | Use | +|---|---| +| `ERROR` | operation failed, cannot continue | +| `WARN` | unexpected but recoverable | +| `INFO` | major state transitions, operation boundaries | +| `DEBUG` | decision points — which branch, and why | +| `TRACE` | entry/exit, raw values, wire data | + +`DEBUG` and `TRACE` compile out in release builds. Log at boundaries and decisions, never +inside hot loops at `INFO` or above. Never log secrets, raw binary, or the success of +trivial operations. One line per message, greppable, with timestamp, level, and source +location. + +The test: if you would add a `printf` to debug it, a `log_debug` should already be there. diff --git a/docs/guides/chat.md b/docs/guides/chat.md new file mode 100644 index 0000000..0d50c54 --- /dev/null +++ b/docs/guides/chat.md @@ -0,0 +1,162 @@ +# Chat Templates & Interactive Chat + +Pantheon v0.3.0 adds multi-turn interactive chat with automatic template detection. Each model family uses a different chat template — Pantheon detects the right one from GGUF metadata and formats messages with correct special tokens. + +## Quick Start + +```bash +./pantheon --model ../models/base/SmolLM2-135M-Instruct-Q8_0.gguf \ + --chat --system "You are a helpful assistant." +``` + +``` +Chat mode (ChatML). Type /quit to exit, /clear to reset. +> What is the capital of France? +The capital of France is Paris. +> And Germany? +The capital of Germany is Berlin. +> /quit +``` + +## CLI Flags + +| Flag | Description | +|------|-------------| +| `--chat` | Enter interactive chat mode | +| `--system ` | System prompt (optional, chat mode only) | +| `--max-tokens ` | Max tokens per response (default: 128) | + +Chat mode commands: + +| Command | Action | +|---------|--------| +| `/quit` or `/exit` | Exit chat | +| `/clear` | Reset conversation (clears KV cache, starts fresh) | + +## Supported Templates + +| Style | Models | System Prompt | +|-------|--------|---------------| +| ChatML | Qwen2.5, SmolLM2 | Yes | +| Mistral | Mistral v0.x, Mixtral | No | +| Phi-3 | Phi-3-mini | Yes | + +Detection is automatic — Pantheon reads the `tokenizer.chat_template` Jinja2 string from GGUF metadata and matches on key markers. Falls back to architecture-based detection if the template is missing. + +## How It Works + +### The Special Token Problem + +BPE models (Qwen, SmolLM2) have "added tokens" like `<|im_start|>` that exist in the vocabulary but are **not produced by BPE merge rules**. Passing `"<|im_start|>user\nHello"` through the tokenizer splits `<|im_start|>` into individual characters — wrong. + +Pantheon solves this by splitting formatted messages into **special** and **text** segments internally. Special tokens are looked up by exact string via `ft_token_to_id()`, text segments are encoded normally. The result is a correct `uint32_t[]` token array. + +SentencePiece models (Mistral, Phi-3) don't have this problem — their Viterbi algorithm matches special tokens from the vocab naturally. + +### Multi-Turn Flow + +1. **Turn 0:** Format `system + user + assistant_prefix` → prefill → generate until EOS +2. **EOS sampled** but not fed through forward pass (existing behavior) +3. **Turn 1+:** Format `EOT + user + assistant_prefix` → prefill from current position → generate until EOS +4. **KV cache persists** across turns — no recomputation of earlier turns + +### Template Formats + +**ChatML** (Qwen2.5, SmolLM2): +``` +<|im_start|>system +{system}<|im_end|> +<|im_start|>user +{user}<|im_end|> +<|im_start|>assistant +{response}<|im_end|> +<|im_start|>user +{next_user}<|im_end|> +<|im_start|>assistant +``` + +**Mistral** (Mistral, Mixtral): +``` +[BOS][INST] {user} [/INST]{response} [INST] {next_user} [/INST] +``` + +**Phi-3**: +``` +<|system|> +{system}<|end|> +<|user|> +{user}<|end|> +<|assistant|> +{response}<|end|> +<|user|> +{next_user}<|end|> +<|assistant|> +``` + +## C API + +### `pn_stream_tokens()` — Pre-tokenized Streaming + +```c +int pn_stream_tokens(pn_model *model, pn_state *state, + const uint32_t *tokens, int n_tokens, + int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data); +``` + +Like `pn_stream()` but takes pre-tokenized input. No BOS added — caller controls tokenization entirely. Returns number of tokens generated, or -1 on error. + +### Chat Template Functions (`src/core/chat.h`) + +```c +/* Detect style from Jinja2 template string */ +pn_chat_style pn_chat_detect_style(const char *jinja_template); + +/* Fallback: detect from architecture enum */ +pn_chat_style pn_chat_style_from_arch(int arch); + +/* Format first turn (system + user + assistant prefix) */ +pn_token_seq pn_chat_format_first(pn_chat_style style, ft_tokenizer *tok, + const char *system, const char *user); + +/* Format continuation turn (EOT + user + assistant prefix) */ +pn_token_seq pn_chat_format_turn(pn_chat_style style, ft_tokenizer *tok, + const char *user); +``` + +### Embedding Chat in Your App + +```c +#include "pantheon.h" +#include "src/core/engine.h" +#include "src/core/chat.h" + +pn_model *model = pn_load("model.gguf"); +pn_state *state = pn_state_new(model); +pn_sampling_params params = pn_sampling_defaults(); + +pn_chat_style style = (pn_chat_style)model->chat_style; +ft_tokenizer *tok = model->tokenizer; + +/* First turn */ +pn_token_seq seq = pn_chat_format_first(style, tok, "You are helpful.", "Hello"); +pn_stream_tokens(model, state, seq.tokens, seq.n_tokens, 128, ¶ms, on_token, NULL); +free(seq.tokens); + +/* Second turn — KV cache preserved automatically */ +seq = pn_chat_format_turn(style, tok, "Tell me more."); +pn_stream_tokens(model, state, seq.tokens, seq.n_tokens, 128, ¶ms, on_token, NULL); +free(seq.tokens); + +/* Reset for new conversation */ +pn_state_reset(state); +``` + +## Tests + +20 tests in `tests/test_chat.c` covering style detection (5), architecture mapping (6), style names (1), ChatML formatting (3), Phi-3 formatting (2), Mistral formatting (2), and passthrough (1). + +```bash +make USE_BLAS=1 test # runs all 50 tests including chat +``` diff --git a/docs/guides/install.md b/docs/guides/install.md new file mode 100644 index 0000000..4a136c1 --- /dev/null +++ b/docs/guides/install.md @@ -0,0 +1,128 @@ +# Install + +Build and run Pantheon on each supported platform. For a first run, see +[Quickstart](quickstart.md). + +## Requirements + +- A C11 compiler and `make`. +- Foundry, a git submodule at `./foundry`. Clone with + `git clone --recursive https://git.teamide.dev/David/Pantheon.git`; in an existing + clone run `git submodule update --init`. The Makefile looks for Foundry in this order: + `FOUNDRY=/path/to/foundry`, then `./foundry` (the submodule, or a symlink), then + `../foundry` (a sibling checkout). If none exists the build stops with + `Foundry not found at ...`. +- Nothing else for a CPU build. BLAS, threads and accelerators are opt-in flags. + +## Linux (CPU) + +```bash +sudo apt install libopenblas-dev # only for USE_BLAS=1 +make USE_BLAS=1 USE_THREADS=1 pantheon +``` + +- `USE_BLAS=1` links OpenBLAS (`-lopenblas`). +- `USE_THREADS=1` adds the compute thread pool; size it with `PN_THREADS` (`0` = auto). + +## macOS + +```bash +make USE_BLAS=1 USE_THREADS=1 pantheon # Accelerate +make USE_BLAS=1 USE_THREADS=1 USE_METAL=1 pantheon # + Metal GPU backend +``` + +`USE_BLAS=1` uses Apple's Accelerate framework, so no extra install is needed. +`USE_METAL=1` (macOS only) compiles Foundry's Objective-C Metal backend and links the +Metal, MetalPerformanceShaders and Foundation frameworks. + +## NVIDIA CUDA + +```bash +make USE_CUDA=1 USE_BLAS=1 pantheon # Linux only +``` + +- **No CUDA toolkit or `nvcc` needed to build.** The kernels are embedded as PTX + (ISA 7.4, target `sm_37`) and the driver is loaded with `dlopen` at run time. +- **At run time** the process needs `libcuda.so.1` (the NVIDIA driver). The PTX needs a + driver that supports CUDA 11.4 or newer; Kepler cards such as the Tesla K80 top out at the + 470 driver branch. cuBLAS (`libcublas.so.11`) is optional — without it a warning prints and + the custom kernels are used. +- With no driver or no card, the same binary runs on the CPU. `GET /` then reports + `"dies_visible": 0`. +- Pick devices with `--dies 0,1` or `CUDA_VISIBLE_DEVICES`; die numbers are relative to the + visible devices. + +## Tenstorrent + +**Ornith 1.5 9B is the only model supported on the card for now**; other models may load but +are not supported. The quick way is the two scripts at the repo root: + +```bash +./install.sh # build pantheon-tt, set up card access (udev rule; asks before sudo) +./run_ornith.sh # download + check the model, serve on 127.0.0.1:8080 +``` + +By hand: + +```bash +make USE_TT=1 pantheon # Linux only, Blackhole p150a +``` + +- **No TT software needed to build.** Tensix programs are embedded in the binary + (`foundry/tt/ft_tt_programs.h`), so neither SFPI nor tt-metal is required. +- **At run time** the card must be reachable: by default through tt-kmd + (`/dev/tenstorrent/`), or with `FT_TT_DRIVER=udrv` through Foundry's userspace driver, + which needs no kernel module but needs root or the udev rule + `foundry/tt/tools/60-foundry-blackhole.rules`. An explicit `FT_TT_DRIVER=udrv` that cannot + reach the card stops the process rather than falling back to the CPU. +- Read [accel-foundry.md §7](../architecture/accel-foundry.md) before running on the card: + what it can serve today, known limits, and hazards that cost a power cycle. + +## Which binary you built + +Each backend writes its own file and `pantheon` is a symlink to the last one built: + +| Build | Binary | +|---|---| +| `make` | `pantheon-cpu` | +| `make USE_CUDA=1` | `pantheon-cuda` | +| `make USE_TT=1` | `pantheon-tt` | +| `make USE_METAL=1` | `pantheon-metal` | + +Combined flags get combined names (e.g. `pantheon-cuda-tt`). Check what a binary is with +`./pantheon --backend` (prints `cuda`, `tt`, `metal` or `cpu`). Restore the link without +rebuilding: `ln -sfn pantheon-cuda pantheon`. + +## Hardware notes + +- **Build on the machine you run on.** The default flags include `-march=native`, so a binary + built on an AVX2 machine dies with `Illegal instruction` (SIGILL) on an older CPU such as a + no-AVX Pentium. `PORTABLE=1` builds a copyable binary instead (`-march=x86-64-v2` on x86-64, + the compiler default elsewhere), at some cost in speed. +- **Small hosts.** The `pantheon` binary is a single compiler invocation; `-j` only helps the + test build. On a 2-core host use `make -j2 ... test`. +- **Host RAM.** 8 GB is the design point: weights are mmap'd, and after a GPU upload the CUDA + paths drop the weight page cache (`QWEN35_DROP_WEIGHTS`, on by default). + +## Running the tests + +```bash +make USE_BLAS=1 test +``` + +Tests that need a model file print `[SKIP]` with the env var to set (e.g. `PN_QWEN35_MODEL`). + +## Troubleshooting + +| Symptom | Meaning / fix | +|---|---| +| `Illegal instruction` at start | Built on a newer CPU. Rebuild on the target machine. | +| `serve: REFUSED to start — ... has NO accelerator backend compiled in` | `--dies` was given to a binary without that backend. Check `./pantheon --backend`; build with `USE_CUDA=1` / `USE_TT=1`, or drop `--dies` to serve on the CPU. | +| `serve: REFUSED ... the die manager probed 0 dies` | Backend present but no device visible: driver not loaded, card missing, or `CUDA_VISIBLE_DEVICES` empty. | +| HTTP `429 server_busy` with `Retry-After` | Every decode slot is busy and the queue is full (or a 30 s queue wait timed out). Retry after the given seconds. | +| HTTP `429 model_at_session_capacity` | A new `session_id` while all `max_sessions` slots hold live sessions. Release idle sessions (`POST /session/release`) or raise `--max-sessions`. | +| HTTP `503 server_memory_constrained` | Host memory pressure: admission refused the prompt, or the queue is full under critical pressure. Transient; retry later. | +| HTTP `422 context_over_budget` | The prompt exceeds the per-session window (`kv_cap`). Not transient: shorten the conversation, then retry. Nothing is dropped silently. | +| `bpe: tokenizer.ggml.pre is unsupported; falling back to the qwen35/GPT-2 pre-tokenizer` | The model's pre-tokenizer is not implemented (e.g. SmolLM2). It still runs, but prompt token IDs may differ from the reference. See [supported-models.md](../reference/supported-models.md#known-issues). | + +Full error list: [HTTP API](../reference/http-api.md#error-responses). diff --git a/docs/guides/pth-convert.md b/docs/guides/pth-convert.md new file mode 100644 index 0000000..ac1d7f3 --- /dev/null +++ b/docs/guides/pth-convert.md @@ -0,0 +1,133 @@ +# PyTorch .pth Converter + +Convert PyTorch checkpoint files (`.pth`) to safetensors format in pure C. No Python, no pip, no dependencies. + +## Why? + +PyTorch's `.pth` format uses Python's `pickle` module, which is fundamentally insecure — `pickle.load()` can execute arbitrary code. Every existing conversion tool requires Python + PyTorch + transformers (2-10 GB of dependencies). + +Pantheon reads `.pth` files via a sandboxed C pickle interpreter that extracts tensor data without executing any embedded Python. It then writes safetensors, a secure mmap-friendly format. + +## Build + +```bash +cd Pantheon +make USE_BLAS=1 pantheon-convert +``` + +Or from Foundry (standalone, no Pantheon needed): + +```bash +cd foundry +make pth-convert +``` + +## Usage + +### Convert + +```bash +# Convert .pth to safetensors +./pantheon-convert model.pth -o model.safetensors +``` + +### Inspect + +```bash +# Print tensor table (names, shapes, dtypes, sizes) +./pantheon-convert model.pth --inspect +``` + +Output: + +``` +=== PyTorch Tensor Inspection === +File: RWKV-x070-World-0.1B-v2.8-20241210-ctx4096.pth +Tensors: 399 + + # Name DType Shape Size +--- -------------------------------- ------ ------------------------- -------- + 0 emb.weight BF16 [65536, 768] 96.0 MB + 1 blocks.0.att.x_r BF16 [768] 1.5 KB + 2 blocks.0.att.x_w BF16 [768] 1.5 KB + ... +``` + +## Example: RWKV-7 0.1B + +```bash +# 1. Download the .pth model from HuggingFace +huggingface-cli download BlinkDL/rwkv-7-world \ + RWKV-x070-World-0.1B-v2.8-20241210-ctx4096.pth \ + --local-dir models/base/ + +# 2. Convert to safetensors +./pantheon-convert \ + models/base/RWKV-x070-World-0.1B-v2.8-20241210-ctx4096.pth \ + -o models/base/RWKV-7-0.1B.safetensors + +# 3. Run inference +./pantheon --model models/base/RWKV-7-0.1B.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello" --max-tokens 32 --temperature 0 +``` + +## How It Works + +A `.pth` file is a ZIP archive containing: + +``` +archive/ + data.pkl # Pickle: OrderedDict mapping tensor names -> storage refs + data/0 # Raw bytes for storage #0 + data/1 # Raw bytes for storage #1 + ... +``` + +The converter: + +1. **ZIP reader** — mmaps the file, finds End-of-Central-Directory, parses entries +2. **Pickle interpreter** — stack-based opcode VM that extracts tensor metadata (names, shapes, dtypes, storage references). Security-whitelisted: only allows `torch._utils._rebuild_tensor_v2`, `collections.OrderedDict`, and torch storage classes. Unknown `GLOBAL`/`REDUCE` targets abort immediately. +3. **Storage resolver** — maps storage IDs to ZIP data entries, computes data pointers (zero-copy into mmap) +4. **Safetensors writer** — writes header JSON + aligned tensor data via Foundry's `fs_writer_*` API + +## Supported Dtypes + +| PyTorch Storage Class | Safetensors Dtype | Bits | +|----------------------|-------------------|------| +| `BFloat16Storage` | BF16 | 16 | +| `HalfStorage` | F16 | 16 | +| `FloatStorage` | F32 | 32 | + +## Security + +The pickle interpreter is deliberately minimal: + +- **No code execution** — only extracts tensor metadata and raw data +- **Whitelisted classes** — `GLOBAL` only allows known torch/collections classes +- **Whitelisted callables** — `REDUCE` only allows `_rebuild_tensor_v2` and `OrderedDict` +- **Immediate abort** — any unrecognized opcode or unauthorized class triggers abort with warning + +This is **not** a general-purpose pickle reader. It handles the specific pickle structure that `torch.save()` produces for state dicts. Adversarial pickles with embedded code will be rejected. + +## C API + +The pth reader is a reusable Foundry module (`pt_` prefix): + +```c +#include "pth/pth.h" + +pt_file *f = pt_open("model.pth"); +int n = pt_tensor_count(f); + +for (int i = 0; i < n; i++) { + const pt_tensor *t = pt_tensor_at(f, i); + printf("%s: %s [", t->name, fs_dtype_name(t->dtype)); + for (int d = 0; d < t->ndim; d++) + printf("%s%lld", d ? ", " : "", (long long)t->shape[d]); + printf("] %zu bytes\n", t->data_size); + // t->data points directly into mmap'd storage (zero-copy) +} + +pt_close(f); +``` diff --git a/docs/guides/quickstart.md b/docs/guides/quickstart.md new file mode 100644 index 0000000..e93175e --- /dev/null +++ b/docs/guides/quickstart.md @@ -0,0 +1,160 @@ +# Quickstart + +Build Pantheon and generate text in under a minute. + +## Build + +```bash +git clone --recursive https://git.teamide.dev/David/Pantheon.git +cd Pantheon +make USE_BLAS=1 pantheon +``` + +Foundry comes in as a git submodule at `foundry/`; in a clone made without +`--recursive`, run `git submodule update --init` first. + +Build flags: + +| Flag | What | When | +|------|------|------| +| `USE_BLAS=1` | Accelerate (macOS) / OpenBLAS (Linux) | Always | +| `USE_THREADS=1` | Thread pool for parallel attention | 1B+ models | +| `DEBUG=1` | `-O0 -g` debug build | Debugging | + +## Download a Model + +Any GGUF model from HuggingFace works. RWKV-X safetensors models are also supported. Start small: + +```bash +# SmolLM2 135M (fast, good for testing) +huggingface-cli download bartowski/SmolLM2-135M-Instruct-GGUF \ + SmolLM2-135M-Instruct-Q8_0.gguf --local-dir ../models/base/ + +# RWKV-X 0.2B (hybrid recurrent model, safetensors format) +huggingface-cli download howard-hou/RWKV-X \ + RWKV-X-0.2B-64k-Base.safetensors --local-dir ../models/base/ + +# Phi-3 mini (3.8B, good quality for size) +huggingface-cli download microsoft/Phi-3-mini-4k-instruct-gguf \ + Phi-3-mini-4k-instruct-Q4_K_M.gguf --local-dir ../models/base/ + +# Mistral 7B v0.3 (larger, needs USE_THREADS=1) +huggingface-cli download bartowski/Mistral-7B-Instruct-v0.3-GGUF \ + Mistral-7B-Instruct-v0.3-Q4_K_M.gguf --local-dir ../models/base/ +``` + +See [reference/supported-models.md](../reference/supported-models.md) for the full list of verified models. + +## Generate Text + +```bash +# GGUF model +./pantheon --model ../models/base/SmolLM2-135M-Instruct-Q8_0.gguf \ + --prompt "The meaning of life is" --max-tokens 64 + +# RWKV-X safetensors model (requires --vocab) +./pantheon --model ../models/base/RWKV-X-0.2B-64k-Base.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello world" --max-tokens 32 + +# RWKV-X with INT8 quantization (~3x faster on ARM) +./pantheon --model ../models/base/RWKV-X-0.2B-64k-Base.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello world" --max-tokens 32 --quantize +``` + +## CLI Flags + +| Flag | Default | Description | +|------|---------|-------------| +| `--model ` | (required) | Model file (GGUF or safetensors) | +| `--vocab ` | none | RWKV World tokenizer vocab file (required for safetensors) | +| `--prompt ` | `"Hello"` | Input prompt | +| `--max-tokens ` | 128 | Maximum tokens to generate | +| `--temperature ` | 0.8 | Sampling temperature (0 = greedy) | +| `--top-p ` | 0.9 | Nucleus sampling threshold | +| `--top-k ` | 40 | Top-k sampling | +| `--quantize` | off | INT8 quantize weights at load time (RWKV-X) | +| `--chat` | off | Interactive multi-turn chat mode | +| `--system ` | none | System prompt (chat mode only) | +| `--bench` | off | Report tok/s, load time, memory | +| `--help` | | Show usage | +| `--version` | | Print version | + +## Benchmark + +```bash +./pantheon --model ../models/base/SmolLM2-135M-Instruct-Q8_0.gguf \ + --prompt "The meaning of life is" --max-tokens 64 --bench --temperature 0 +``` + +``` +=== Benchmark === + load: 41.6 ms + decode: 64 tokens in 787.3 ms (81.3 tok/s) + memory: 552.7 MB RSS +================= +``` + +## C API + +Pantheon exposes a minimal C API for embedding into applications: + +```c +#include "pantheon.h" + +int main(void) { + /* GGUF model */ + pn_model *model = pn_load("model.gguf"); + + /* Or: RWKV-X safetensors model */ + pn_load_opts opts = { .vocab_path = "rwkv_vocab.txt", .quantize = true }; + pn_model *rwkvx = pn_load_ex("model.safetensors", &opts); + + pn_state *state = pn_state_new(model); + pn_sampling_params params = pn_sampling_defaults(); + params.temperature = 0.0f; /* greedy */ + + char *output = pn_generate(model, state, "Hello world", 64, ¶ms); + printf("%s\n", output); + + free(output); + pn_state_free(state); + pn_model_free(model); + return 0; +} +``` + +### Streaming + +```c +void on_token(const char *token, void *user_data) { + printf("%s", token); + fflush(stdout); +} + +pn_stream(model, state, "Hello", 128, ¶ms, on_token, NULL); +``` + +### API Functions + +| Function | Description | +|----------|-------------| +| `pn_load(path)` | Load GGUF model, returns `pn_model*` | +| `pn_load_ex(path, opts)` | Load any model (GGUF or safetensors) with options | +| `pn_model_free(model)` | Free model and weights | +| `pn_state_new(model)` | Create inference state (KV cache + arena, or RWKV-X state) | +| `pn_state_free(state)` | Free state | +| `pn_state_reset(state)` | Clear state and position (reuse state) | +| `pn_generate(model, state, prompt, max_tokens, params)` | Generate text, returns `char*` (caller frees) | +| `pn_stream(model, state, prompt, max_tokens, params, cb, ctx)` | Streaming generation via callback | +| `pn_stream_tokens(model, state, tokens, n, max, params, cb, ctx)` | Streaming from pre-tokenized input (for chat) | +| `pn_sampling_defaults()` | Default sampling params (temp=0.8, top_p=0.9, top_k=40) | + +## Run Tests + +```bash +make USE_BLAS=1 test +``` + +62 tests across 7 suites: build sanity (5), GGUF dequant (11), arena (4), quantized matvec (4), SIMD kernels (6), chat templates (20), RWKV-X (12). diff --git a/docs/reference/cli.md b/docs/reference/cli.md new file mode 100644 index 0000000..704d1ad --- /dev/null +++ b/docs/reference/cli.md @@ -0,0 +1,193 @@ +# Pantheon — CLI Reference + +Entry point: `cli/main.c`. `pantheon serve` runs the OpenAI-compatible server +(`src/serve/serve_command.c`), `pantheon router` the multi-instance dispatch layer +(`src/serve/router_command.c`); anything else is the one-shot / interactive CLI. + +--- + +## `pantheon` (one-shot and chat) + +``` +pantheon --model [--prompt [--raw] | --chat [--system ]] + [--max-tokens ] [--temperature ] [--top-p ] [--top-k ] + [--config ] [--vocab ] [--quantize] [--bench] +pantheon --version | --backend | --help +``` + +| Flag | Argument | Default | Effect | +|------|----------|---------|--------| +| `--model` | `` | — | GGUF or safetensors model. Without it, prints the version and checks Foundry links. | +| `--prompt` | `` | `"Hello"` | One-shot generation. The model's chat template is applied unless `--raw`. | +| `--raw` | *(flag)* | off | One-shot `--prompt` without the chat template. | +| `--chat` | *(flag)* | off | Interactive multi-turn chat on stdin; template detected from the GGUF. | +| `--system` | `` | unset | System prompt (chat mode only). | +| `--max-tokens` | `` | `128` | Tokens to generate. | +| `--temperature` | `` | `0.8` | Sampling temperature. | +| `--top-p` | `` | `0.9` | Nucleus sampling threshold. | +| `--top-k` | `` | `40` | Top-k sampling. | +| `--config` | `` | unset | HuggingFace `config.json` (safetensors models). | +| `--vocab` | `` | unset | RWKV World tokenizer vocab file. | +| `--quantize` | *(flag)* | off | INT8-quantize weights at load time. | +| `--bench` | *(flag)* | off | Report tok/s and memory. | +| `--version` | *(flag)* | — | `Pantheon v` then `backend= build=`. | +| `--backend` | *(flag)* | — | Prints only the compiled backend token (`cuda`, `tt`, `metal`, `cpu`). | +| `-h` / `--help` | *(flag)* | — | Usage. | + +--- + +## `pantheon serve` + +``` +pantheon serve --model [--name ] [--dies ] + [--ctx-cap ] [--reasoning-budget ] + [--max-sessions ] [--max-concurrent ] + [--model ...] + [--host ] [--port ] + [--config ] [--vocab ] + [--quantize] + [--served-name ] +``` + +`--model` is repeatable. The per-model flags `--name` and `--dies` bind to +the **most recently seen** `--model`. There is no positional association; +flags are matched by order. + +### Global flags + +| Flag | Argument | Default | Effect | +|------|----------|---------|--------| +| `--model` | `` (GGUF) | — *required, at least one* | Load and serve the model at this path. Repeatable; each occurrence starts a new model slot. Max `MR_MAX_MODELS=16`. | +| `--host` | `` | `127.0.0.1` | TCP bind address. Must be a dotted-IPv4 literal (`inet_pton`). | +| `--port` | `` | `8080` | TCP port. Parsed with `atoi`. | +| `--config` | `` | unset | HuggingFace `config.json` for safetensors models. | +| `--vocab` | `` | unset | RWKV World tokenizer vocab file. | +| `--quantize` | *(flag)* | off | INT8-quantize weights at load time. | +| `--served-name` | `` | unset | Back-compat alias: sets the **first** model's served name when it has no `--name`. Silently ignored if `--name` already set for model 0. | +| `-h` / `--help` | *(flag)* | — | Print usage and exit 0. | + +### Per-model flags + +These flags bind to the model specified by the **preceding** `--model`. If +used before any `--model` the server exits with an error. + +| Flag | Argument | Default | Effect | +|------|----------|---------|--------| +| `--name` | `` | basename of path | Served model id (the `model` field clients send in requests). Used for routing in multi-model mode. | +| `--dies` | `` | none (auto-place) | Pin to accelerator dies (CUDA or Tenstorrent builds). CUDA ordinals are relative to `CUDA_VISIBLE_DEVICES`, not physical device numbers. Max `PN_MODEL_MAX_DIES=8`. N dies split the layers N ways (VRAM headroom, not speed). A `--dies` naming dies this binary has no backend for refuses to start; omit it to serve on the CPU. | +| `--ctx-cap` | `` | `0` = engine default | Per-session context cap in tokens (the KV window). A longer prompt is refused with `context_over_budget`; nothing is silently dropped. Max 2^20. | +| `--reasoning-budget` | `` | `0` = unbounded | After `` tokens inside `` without closing it, force `` so the model answers. | +| `--max-sessions` | `` | `0` = default 8 (`BS_MAX_USERS`) | Cap on live prefix-cache sessions; a new session over the cap is refused 429/503. | +| `--max-concurrent` | `` | `0` = the resolved `--max-sessions` | Cap on simultaneous decode slots; surplus requests queue, then get 429/503. | + +### Multi-model semantics + +``` +pantheon serve \ + --model /models/big.gguf --name big --dies 0,1 \ + --model /models/small.gguf --name small --dies 2 +``` + +All models share the **single `--port`** (`serve_command.c:333`, one `hs_listen` call). + +Each `--model` occupies an independent registry slot (`model_registry.c:117–122`). Per +slot, on load: +- Always: `pn_model` (weights) + `gen_lock` (non-batched serializer). +- Non-batched path (`QWEN35_BATCH_SLOTS` unset): also gets a `pn_state`; `gen_lock` + serializes single-user access — second concurrent request to the same model → 429. +- Batched path (`QWEN35_BATCH_SLOTS=1`): gets a `pn_batch_sched` instead + (`model_registry.c:272–282`, `bs_create`); the `gen_lock`+single-`pn_state` + serializer is bypassed. Up to `BS_MAX_USERS=8` concurrent users share one model. + +Requests route by the `model` field in the JSON body (`openai_chat.c:616–629`, +`route_model` → `mr_acquire`). With exactly one loaded model, the field is ignored +(back-compat). + +GPU uploads are serialized (one at a time) via `reg->upload_lock` to prevent stacking +the ~2.4 GB host transient. + +`--served-name` affects only model 0 when `--name` was not also given for it. + +#### Replicas (same name, different dies) + +Repeating the **same `--name`** on different `--dies` creates a load-balanced replica +pool (`model_registry.c:117–119`): + +``` +pantheon serve \ + --model /models/worker.gguf --name worker --dies 2 \ + --model /models/worker.gguf --name worker --dies 3 +``` + +`mr_acquire` (`model_registry.c:322–355`) picks the **least-busy** same-name entry +(fewest in-flight `refs`); each replica has its own die and `gen_lock`, so concurrent +requests run truly in parallel across those dies. + +Session affinity across replicas within a single serve process is **not yet +implemented** — requests always go to the least-busy replica. See the +"Multi-Model Smart Routing" section in `docs/README.md`. + +### Die index semantics + +`--dies 0,1` means CUDA ordinals 0 and 1 as seen by the process — after +`CUDA_VISIBLE_DEVICES` filtering. If `CUDA_VISIBLE_DEVICES=4,5,6,7` is set, +`--dies 0,1` maps to physical cards 4 and 5. **The indices are NOT physical +device numbers.** + +### Startup sequence + +1. `pn_memwatch_start()` — host RAM poll thread starts. +2. Signal handlers installed (SIGINT/SIGTERM → clean CUDA teardown; crash + signals → `_exit()` so the kernel closes the nvidia fd and reclaims VRAM). +3. CUDA init (`fc_init()`, `pn_dm_init_cuda()`). +4. Model registry init. +5. For each `--model`: `mr_load()` (blocks; uploads weights to GPU). +6. TCP listen socket bound. +7. Accept loop: each connection spawns a detached thread. + +### Runtime behavior + +- Cheap routes (GET `/v1/models`, OPTIONS) are never queued behind a + generation. +- Per-model generation is serialized by `gen_lock`. A second request to the + same model while one is generating → immediate 429. +- With `QWEN35_BATCH_SLOTS=1`, the scheduler (`batch_sched`) replaces the + gen\_lock; up to `BS_MAX_USERS=8` concurrent users share one model. +- `QWEN35_GPU_FULL_LOGITS` being set causes immediate exit before any model + loads. + +--- + +## `pantheon router` + +``` +pantheon router [--host ] [--port ] +``` + +| Flag | Default | Effect | +|------|---------|--------| +| `--host` | `127.0.0.1` | Bind address. | +| `--port` | `8100` | Port. | + +A host-side dispatch layer over several `pantheon serve` processes: no model loads, no +GPU. Serve instances register themselves when `PANTHEON_REGISTRY_URL` points at the router, +then heartbeat; instances whose heartbeat lapses are evicted. Requests go to the +least-busy live instance of the requested model, and a session (`X-Session-Id`) sticks to +the instance that served it so its prefix cache is reused. No authentication: bind it to +a trusted interface. + +| Route | Purpose | +|-------|---------| +| `POST /registry/register` | A serve announces a model instance | +| `POST /registry/heartbeat` | Refresh liveness and load | +| `DELETE /registry/deregister` | Graceful removal | +| `GET /registry/fleet` | Model-level fleet view | +| `GET /v1/models` | Live models, deduplicated by model id | +| `POST /v1/chat/completions`, `POST /v1/completions` | Least-busy dispatch, SSE pass-through | + +--- + +## `pantheon-convert` (cli/convert.c) + +A separate binary (`make pantheon-convert`), not a subcommand: converts PyTorch `.pth` +checkpoints to safetensors. See [`../guides/pth-convert.md`](../guides/pth-convert.md). diff --git a/docs/reference/env-flags.md b/docs/reference/env-flags.md new file mode 100644 index 0000000..a602d71 --- /dev/null +++ b/docs/reference/env-flags.md @@ -0,0 +1,158 @@ +# Pantheon — Environment Flag Reference + +Every `getenv()` call in `src/` and `foundry/` is listed here. Flags are +grouped by prefix. All flags are **process-constant** unless noted: they are +read once into a static cache on first call and never re-read. + +--- + +## QWEN35\_\* — Qwen3.5-MoE model flags + +### Core KV / context window + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_KV_Q4` | `0` or anything | ON | `env[0]=='0'` disables | KV-cache format: Q4\_0 (~4–7× smaller) vs fp32. Default changed to ON (B1). **Only `=0` disables.** | `qwen35moe_forward.c:51`, `engine.c:430` | +| `QWEN35_MAX_SEQ` | integer | `4096` | `atoi`, clamped `[16, 262144]` | KV-window size per attention layer. Memory scales linearly. Ceiling `PN_QWEN35MOE_MAX_SEQ_CAP=262144`. | `qwen35moe_forward.c:81`, `qwen35moe_forward_cuda.c:621`, `engine.c:454` | +| `QWEN35_KV_Q4_WIN` | integer 0–256 | `128` | `atoi`, clamped `[0, 256]` | fp16 recent-window exemption size inside the Q4 KV cache. `0` disables the window (pure Q4). | `qwen35moe_kv_q4.h:54` | +| `QWEN35_KV_Q4_SINK` | integer 0–8 | `4` | `atoi`, clamped `[0, 8]` | fp16 attention-sink prefix kept exempt from Q4 quantization. | `qwen35moe_kv_q4.h:67` | + +### GPU placement / split + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_GPU_SPLIT_LAYER` | integer | `40` (`GPU_DIE_SPLIT_LAYER`) | `atoi`, must be `>0`; ignored when explicit 1-die placement | Layer boundary between die A and die B. Layers `[0, split)` → die A; `[split, n_layer)` → die B. Ignored when explicit single-die placement is authoritative. | `qwen35moe_forward_cuda.c:702` | +| `QWEN35_GPU_DIE_A` | integer (CUDA ordinal) | `0` | `atoi` | Die A CUDA device ordinal. Superseded by explicit `--dies` placement (warning printed). | `qwen35moe_forward_cuda.c:732` | +| `QWEN35_GPU_DIE_B` | integer (CUDA ordinal) | `1` | `atoi` | Die B CUDA device ordinal. Same supersession rule as `QWEN35_GPU_DIE_A`. | `qwen35moe_forward_cuda.c:733` | +| `QWEN35_GPU_MAX_LAYERS` | integer | unset (= all layers) | `atoi`, must be `>0` | Single-die only: upload/validate only the first N global layers. Ignored in 2-die mode. Also disables async scheduling (`QWEN35_ASYNC_SCHED`). | `qwen35moe_forward_cuda.c:1407`, `7164` | +| `QWEN35_GPU_BARSWAP` | any | unset | presence check (`== NULL` → small-BAR) | Legacy barswap (single MMIO slot) vs small-BAR mode (all dies live). Default is small-BAR (no barswap). Set to any non-empty value to restore the old barswap 2-die path. | `qwen35moe_forward_cuda.c:1416` | +| `QWEN35_DROP_WEIGHTS` | `0` or unset | unset (= drop) | `env[0]=='0'` opts out | After GPU upload, call `madvise(MADV_DONTNEED)` on the host weight mmap to free its page cache (~15 GB for a 35B MoE). **On by default** (qwen35 and llama CUDA paths); `=0` keeps the pages for host-fallback debugging. Host-fallback matvec refaults clean pages from disk. | `qwen35moe_forward_cuda.c:1935`, `llama_forward_cuda.c:211` | + +### Batched decode (C3 scheduler) + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_BATCH_SLOTS` | `1` or anything | OFF | **`env[0]=='1'`** — only the character `'1'` enables | **Enable** multi-user batched decode. Any other value (including `=8`, `=4`, `=on`) silently keeps batching OFF. | `batch_sched.c:51` | +| `QWEN35_BS_STATS` | `1` or anything | OFF | `env[0]=='1'` | Print per-step driver timing to stderr (M6 instrumentation). | `batch_sched.c:110` | +| `QWEN35_BS_DRIVER_SAMPLE` | `0` or anything | ON | `env[0]=='0'` disables | In-driver sampling (default ON). Set `=0` to restore legacy owner-side lockstep (each request thread samples its own token). | `batch_sched.c:651` | +| `BS_QUEUE_DEPTH` | integer `>0` | `32` | `atoi` | Per-model overflow queue depth (D1). Requests queue here when all slots are busy before getting 429. | `batch_sched.c:725` | +| `QWEN35_BS_PREFILL_BATCH` | `0` or anything | ON | `env[0]=='0'` disables | Enable batched prefill (one GPU pass per chunk, ~2× vs token-by-token). `=0` restores the sequential token loop. | `serve_generation.c:471` | +| `QWEN35_BS_PREFILL_CHUNK` | integer `>=0` | `128` | `atoi`; `<0` → 0 | Prefill chunk size in tokens. `0` = entire prompt in one pass (unbounded scratch). | `serve_generation.c:482` | + +### Logits / sampling + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_GPU_FULL_LOGITS` | any | unset | presence check | Force full n\_vocab (248K) D2H logits copy per token. **Serve refuses to start if this is set** — it cripples batched decode throughput (K×992 KB/step over host link). Debug/eval only. | `serve_command.c:279`, `qwen35moe_forward_cuda.c:2747` | +| `QWEN35_GPU_TOPK` | integer 0–256 | `40` | `atoi`, clamped `[0, FC_TOPK_MAX=256]` | On-die top-K reduction before D2H. `0` or forced by `QWEN35_GPU_FULL_LOGITS` → full copy. | `qwen35moe_forward_cuda.c:2751` | + +### CUDA-graph / fusion + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_GRAPH` | `0` or anything | ON | `env[0]=='0'` disables | Master gate for CUDA-graph replay of SSM layers. Disabling also disables `QWEN35_GRAPH_FULL` and `QWEN35_GRAPH_SLOTS`. | `qwen35moe_forward_cuda.c:1899` | +| `QWEN35_GRAPH_FULL` | `0` or anything | ON | `env[0]=='0'` disables | Full-step segment graphs (C1): capture every layer of a die segment (SSM+attention interleaved) into one graph, replay with one `cuGraphLaunch`. Requires `QWEN35_GRAPH`. | `qwen35moe_forward_cuda.c:1916` | +| `QWEN35_GRAPH_SLOTS` | `0` or anything | ON | `env[0]=='0'` disables | Per-SSM-layer graphs on the batched-decode (slots) path (L9). Sub-gate of `QWEN35_GRAPH`. Attention layers stay direct. | `qwen35moe_forward_cuda.c:1931` | +| `QWEN35_GRAPH_CAPTURE_FAIL_INJECT` | int N | `0` (off) | `atoi(env)` | **Test only.** Forces the first N capture attempts of EVERY graph slot to be treated as failures, so the bounded-retry path can be exercised on real hardware. `N < 5` exercises recovery, `N >= 5` exercises the permanent give-up. Never set on a serving process. | `gpu_graph_capture_tracker.c:pn_graph_capture_inject_failures` | +| `QWEN35_FUSE` | hex bitmask | `0x3F` (all on) | `strtol(env, NULL, 0)` (hex/decimal) | Fusion-site bitmask: bit0=ssm proj4, bit1=ssm out+res, bit2=attn qkv, bit3=attn norm+rope, bit4=moe pre, bit5=moe post. Set specific bits `=0` to bisect regressions. | `qwen35moe_forward_cuda.c:1887` | + +### Flash attention + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_FLASH` | `0` or anything | ON | `env[0]=='0'` disables | Flash attention for batched prefill. NOT bit-exact vs host softmax (~1e-6 relative drift). `=0` restores host-softmax path. | `qwen35moe_forward_cuda.c:3257` | +| `QWEN35_FLASH_DECODE` | `0` or anything | ON | `env[0]=='0'` disables | Flash attention for single-token decode. `=0` restores host-softmax decode path. | `qwen35moe_forward_cuda.c:3406` | +| `QWEN35_FLASH_DECODE_KPS` | integer `>=16` | `PN_QWEN35MOE_DEC_KEYS_PER_SPLIT` | `atoi`, floor 16 | Keys per decode split (tune for multi-split combine path in tests). | `qwen35moe_forward_cuda.c:3419` | +| `QWEN35_FLASH_SLOTS` | `0` or anything | ON | `env[0]=='0'` disables | Flash attention on the batched-decode (slots) path (L7). `=0` → host-softmax slots path. | `qwen35moe_forward_cuda.c:4917` | +| `QWEN35_ATTN_WINDOW` | integer `>0` | `0` (full) | `strtol` | StreamingLLM sliding-window attention recent-window size. `0`/unset = full attention. Applies to all 10 global attention layers. | `qwen35moe_forward_cuda.c:3272` | +| `QWEN35_ATTN_SINK` | integer `>=0` | `4` | `strtol` | StreamingLLM attention-sink prefix size (used with `QWEN35_ATTN_WINDOW`). | `qwen35moe_forward_cuda.c:3283` | + +### 2-die pipeline / threading + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_ASYNC_SCHED` | `0` or anything | ON | `env[0]=='0'` disables | Non-blocking async dispatch for single-token decode (C2). `=0` restores fully synchronous path. Automatically disabled when `QWEN35_GPU_MAX_LAYERS` is set or LM head needs host fallback. | `qwen35moe_forward_cuda.c:7074` | +| `QWEN35_ASYNC_KVMIRROR` | any non-`0` | OFF | `env[0] && env[0]!='0'` | Defer host-KV-mirror stores in batched decode (L8). Experimental. | `qwen35moe_forward_cuda.c:5017` | +| `QWEN35_TWO_THREAD` | any non-`0` | OFF | `env[0] && env[0]!='0'` | Two-die two-thread decode (small-BAR, 2 dies, nslots≥1). | `qwen35moe_forward_cuda.c:5965` | +| `QWEN35_DIE_PIPELINE` | any non-`0` | OFF | `env[0] && env[0]!='0'` | Software-pipeline two batch groups across the die pair so both dies compute simultaneously (L5). Requires 2 dies + grouped MoE + nslots≥2. | `qwen35moe_forward_cuda.c:5979` | +| `QWEN35_VOCAB_SPLIT` | any non-`0` | OFF | `env[0] && env[0]!='0'` | Split LM head across 2 dies (T3). Only useful in 2-die non-barswap mode with peer access. | `qwen35moe_forward_cuda.c:5991` | + +### SSM + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_SSM_2PASS` | any non-`0` | OFF | `env[0] && env[0]!='0'` | Route SSM recurrence to fused-reduction (2-pass) kernel: one LDS sweep, 3×S shared, slightly lossy by reassociation. Default OFF (bit-exact baseline). | `foundry/cuda/fc_cuda.c:3375` | + +### Debug / profiling + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `QWEN35_DEBUG` | any | unset | presence check | Enable per-layer activation dumps to stderr. Also forces full-logits D2H (`gpu_topk_k()` returns 0). | `qwen35moe_forward.c:106`, `qwen35moe_forward_cuda.c:991` | +| `QWEN35_PROF` | any | unset | presence check | Per-token wall-clock profiler: accumulates matvec time by quant type and forward phase, printed per token. Adds `fc_sync` around timed ops. | `qwen35moe_forward_cuda.c:794` | +| `QWEN35_SPROF` | any | unset | presence check | Per-step batch-decode profiler: phase breakdown printed per step, cumulative summary every 10 steps. | `qwen35moe_forward_cuda.c:930` | + +--- + +## PN\_\* — Pantheon server / memory flags + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `PN_THREADS` | integer | `0` (auto) | `atoi` | CPU thread-pool size for `USE_THREADS` builds. `0` = auto-detect. | `engine.c:772` | +| `PN_ASYNC_POLL_US` | integer 50–20000 | `500` | `atol`, clamped `[50, 20000]` | Async step poll interval in microseconds (exponential backoff up to this cap). | `serve_generation.c:422` | +| `PN_HOST_RESERVE_BYTES` | integer (bytes) | `max(1 GiB, 15% × MemTotal)` | `strtoull` | Override host RAM reserve held off-limits for admission. Values ≤0 or unparseable fall back to the formula. | `pn_memwatch.c:62` | +| `PN_DIE_RESERVE_BYTES` | integer (bytes) | `1073741824` (1 GiB) | `strtoull` | Override per-die VRAM reserve subtracted from each die's reported total before scheduling. | `pn_memwatch.c:72` | +| `PN_PSI_FULL_REJECT` | float (percent) | `15.0` | `strtod`; values ≤0 ignored | PSI-full avg10 threshold (% wall time) at/above which admission rejects new requests as CRITICAL. | `pn_memwatch.c:78` | +| `PN_LADDER_DISABLE` | any non-`0` | unset (enabled) | `env[0] && env[0]!='0'` | Disable the reactive degradation ladder entirely. The plan mirrors the config at all pressure levels. | `pn_ladder.c:21` | +| `PN_LOAD_SPIKE_BYTES` | integer (bytes) | `2516582400` (~2.4 GB) | `strtoull`; ≤0 falls back to default | Host anon RSS budget for GPU-upload transient (measured on a Tesla K80 host: ~2.4 GB for ~55 s). Admission checks this before starting a load. | `pn_admission.c:280` | +| `PN_VLM_DEBUG` | any | unset | presence check | Print top-3 logits + sampled token for the first 6 steps. Diagnoses empty VLM output. | `engine.c:1686` | +| `PN_GPU_PREFILL` | any | unset | presence check | (llama/Metal) Upload Q4\_K decoder weights + run prefill GEMMs on the GPU. Off by default (measured net loss for bulk OCR vs CPU+GPU overlap). | `llama.c:2068` | +| `PN_BATCH_GPU` | any | unset | presence check | (llama/Metal, batch≤32) Upload Q4\_K decoder weights for GPU-based batched GEMM decode. Experimental. | `llama.c:2398` | + +--- + +## FC\_\* — foundry/cuda MoE kernel tuning + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `FC_MOE_DOWN_WPP` | `1`, `2`, `4`, or `8` | `2` (`FC_MOE_DOWN_WPP_DEFAULT`) | `atoi`; only exact power-of-2 values accepted | Warps-per-pair for the cooperative MoE down-projection CUDA kernel. Values outside {1,2,4,8} are silently ignored. | `foundry/cuda/fc_cuda.c:2645` | +| `FC_MOE_DOWN_THREADS` | `64`, `128`, `256`, or `512` | `128` (`FC_MOE_DOWN_THREADS_DEFAULT`) | `atoi`; must be multiple of 32×wpp | Thread count for the MoE down kernel. Only exact values {64,128,256,512} that are a multiple of `32×wpp` accepted; others silently ignored. | `foundry/cuda/fc_cuda.c:2652` | + +--- + +## Distributed training (foundry/distributed) + +| Flag | Values | Default | Parse | Effect | Source | +|------|--------|---------|-------|--------|--------| +| `WORLD_SIZE` | integer | `1` | `atoi` | Distributed training world size. Used by `fd_init_from_env()` in the foundry trainer — not the inference server. | `foundry/distributed/distributed.c:31` | +| `RANK` | integer | `0` | `atoi` | This process's rank. Same scope as `WORLD_SIZE`. | `foundry/distributed/distributed.c:32` | + +--- + +## Key parsing gotchas + +1. **`QWEN35_BATCH_SLOTS` is an enable flag, not a slot count.** Only `env[0]=='1'` + enables batching. `QWEN35_BATCH_SLOTS=8` leaves batching OFF. + (`batch_sched.c:51`) + +2. **`QWEN35_KV_Q4` default is now ON.** Only `=0` disables. The old opt-in + value `=1` still works but is redundant. Any other value (e.g. `=true`) + also keeps it ON. (`qwen35moe_forward.c:51`) + +3. **`QWEN35_GPU_FULL_LOGITS` is blocked at serve startup.** The server refuses + to start with this set — it is not silently ignored. (`serve_command.c:279`) + +4. **`QWEN35_GPU_SPLIT_LAYER` is ignored when 1-die placement is active.** + `gpu_split_layer()` returns `n_layer` (single-die path) when `n_placed==1` + regardless of the env. (`qwen35moe_forward_cuda.c:703`) + +5. **`QWEN35_GPU_DIE_A/B` are superseded by `--dies` / `pn_load_on` placement.** + A warning is printed but they are not honoured. (`qwen35moe_forward_cuda.c:724`) + +6. **`FC_MOE_DOWN_WPP` and `FC_MOE_DOWN_THREADS` silently reject invalid values** + and keep the compiled default. No warning is emitted. (`foundry/cuda/fc_cuda.c:2645–2659`) + +7. **`QWEN35_FUSE` parses via `strtol(env, NULL, 0)`** — it accepts hex (`0x3f`) + and decimal. (`qwen35moe_forward_cuda.c:1888`) + +8. **`PN_LOAD_SPIKE_BYTES=0` falls back to the 2.4 GB default**, not zero — a + zero budget would silently disarm the host-RAM gate. (`pn_admission.c:284`) diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md new file mode 100644 index 0000000..c7c9a7a --- /dev/null +++ b/docs/reference/http-api.md @@ -0,0 +1,821 @@ +# Pantheon — HTTP API Reference + +The server speaks HTTP/1.1 over TCP. Every response carries +`Access-Control-Allow-Origin: *` and `Connection: close`. No authentication. +All bodies are JSON unless noted (SSE for streaming). + +--- + +## Routes + +| Method | Path | Handler | Audience | +|--------|------|---------|----------| +| `GET` | `/` | health | client / operator | +| `OPTIONS` | `*` | CORS preflight | client | +| `GET` | `/v1/models` | list models | client | +| `POST` | `/v1/chat/completions` | chat completion | client | +| `GET` | `/admin/models` | list live model instances | operator | +| `POST` | `/admin/models/load` | load a model | operator | +| `POST` | `/admin/models/unload` | unload a model | operator | +| `GET` | `/admin/dies` | die status | operator | +| `POST` | `/admin/session/export` | export a session's state as a blob | operator | +| `POST` | `/session/restore` | restore a session from a blob | client | +| `POST` | `/session/release` | free a session's slot | client | +| `GET` | `/session/estimate` | warm-restore vs re-prefill time estimate | client | +| `GET` | `/session/watch` | hold a session alive while connected | client | + +Any other path → `404 {"error":{"message":"unknown route","type":"not_found","code":null}}`. + +--- + +## GET / + +Health check. Always 200. + +**Response:** +```json +{"service":"pantheon","status":"ok","backend":"cuda", + "build":"5d818bfd1c9a","accelerator":true,"dies_visible":8} +``` + +| Field | Type | Meaning | +|-------|------|---------| +| `backend` | string | The accelerator backend this binary was COMPILED with: `cuda`, `tt`, `metal`, `cpu`. Compile-time constant, not a probe. | +| `build` | string | Commit this binary was built from (`-dirty` when the tree was not clean), or `unknown`. | +| `accelerator` | bool | False means every forward pass in this process runs on the host CPU, whatever the flags say. | +| `dies_visible` | int | Dies the die manager holds (`pn_dm_count()`). `0` on a CUDA build means no driver / no card / empty `CUDA_VISIBLE_DEVICES`. | + +The cheapest route on the server is also the one that answers "what am I +running?" — see [`GET /v1/models`](#backend--build--what-this-binary-actually-is) +for why that question needed an endpoint at all. + +--- + +## OPTIONS \* + +CORS preflight. Always 200 with `{}` body. + +--- + +## GET /v1/models + +**Serve's own endpoint** (`openai_chat.c:oai_handle_models`, `serve_command.c:dispatch`): +same-name replica slots (multi-die LB copies) are deduplicated — clients see one +logical id per served name. The response does **not** include `live` or `free_slots`. + +**Response — 200:** +```json +{ + "object": "list", + "backend": "cuda", + "build": "5d818bfd1c9a", + "data": [ + { + "id": "my-model", + "object": "model", + "created": 0, + "owned_by": "pantheon", + "max_sessions": 4, + "max_concurrent": 4, + "context_window": 32000, + "tool_choice_required_with_thinking": true, + "graphs": {"live": 30, "total": 40, "failed": 0, + "ineligible": 10, "pending": 0}, + "attention": {"ondie": 10, "host": 0, "total": 10} + } + ] +} +``` + +| Field | Type | Meaning | +|-------|------|---------| +| `backend` | string | **Top-level.** The accelerator backend this binary was COMPILED with: `cuda`, `tt`, `metal` or `cpu`. Compile-time constant, not a probe. | +| `build` | string | **Top-level.** The commit this binary was built from (`-dirty` suffixed when the tree was not clean), or `unknown`. | +| `id` | string | The routing key for `model` on a completion | +| `max_sessions` | int | RESIDENCY: how many conversations may hold KV at once | +| `max_concurrent` | int | CONCURRENCY: how many requests this model ACTUALLY decodes at once — the **served** value (`bs_get_load` capacity), not the configured cap. See below. | +| `context_window` | int | **Per-session context window in tokens** — see below. Omitted when unknown. | +| `tool_choice_required_with_thinking` | bool | `true` when `tool_choice: "required"` may be sent with thinking on: the think block is unconstrained and the turn ends in a parsed tool call (the think-gated grammar models ORNITH_JSON and QWEN_XML; `false` for MiniCPM's attribute dialect, which no grammar models, and for every model when `PANTHEON_TOOL_GRAMMAR=0`). | +| `graphs` | object | **CUDA-graph residency** — see below. Omitted when the model has no graph slots. | +| `attention` | object | **Attention-path residency** — see below. Omitted until the forward has reached an attention layer. | + +### `backend` / `build` — what this binary actually is + +Top-level on the response, not per-model, because it is a property of the +process: the accelerator backend it was **compiled** with (`cuda`, `tt`, `metal`, +`cpu`) and the commit it came from. + +Compile-time constant, deliberately — not a runtime probe. A serve whose binary +has no CUDA compiled in cannot use a GPU whatever the driver reports, and the +completion text is identical either way, so nothing downstream can detect it +from the output. + +The same values are on `GET /` (plus `dies_visible`), and out of band from +`pantheon --backend`, which prints one lowercase token for a deploy script to +assert against. + +### `max_concurrent` — the SERVED number, not the configured cap + +`max_concurrent` on `/v1/models` is what the model actually decodes at once +(`bs_get_load`'s capacity), which is **not** always what `--max-concurrent` asked +for. It reads `1` whenever there is no batch scheduler — no CUDA compiled in, +`QWEN35_BATCH_SLOTS` off, `bs_create` failed — and it drops below the cap when +the memory-pressure ladder clamps the slot count. + +Same argument as `context_window` below: a client sizes its in-flight requests +from this number, so advertising a cap the serve cannot honour (e.g. `2` with no +scheduler, which turns every second concurrent chat into a `429`) is worse than +advertising nothing. + +Operators still get the configured value: `/admin/models` carries **both**, as +`max_concurrent` (served) and `max_concurrent_configured` (asked for). Seeing +them differ is the signal. + +### `context_window` — the advertised window + +`context_window` is the window a client may actually plan a prompt against, in +tokens. It is the **effective** window (`pn_effective_ctx_window`): the operator's +`--ctx-cap` **after** the reactive memory ladder's runtime clamp — *not* the +configured cap, and not the model's theoretical maximum. + +That distinction is the whole point of the field. Publishing the configured cap +while admission enforces a smaller one hands the client a prefill the serve then +rejects — on a K80 that rejection lands after minutes of cold-prefill TTFT, and it +lands mid-task. + +Two properties clients depend on: + +- **It is the same number a completion reports as `usage.kv_cap`.** The model list + is the client's INITIAL plan; `kv_cap` is the LIVE truth on every turn. A client + that reads both needs no polling and no extra endpoint to notice a mid-session + clamp. +- **It is additive and omitted when unknown.** A plain OpenAI-compatible client + ignores the extra key; a client that reads it never sees a `0` it has to + special-case. An absent field means *undetected*; a client should fall back to + a conservative window rather than guess. + +Field name: Pantheon emits `context_window`. Clients that detect the window from +other servers' spellings (`context_length`, `max_model_len`, `n_ctx`, ...) need +to accept this one. + +### `graphs` — CUDA-graph residency + +Each layer of the hybrid stack can hold a captured CUDA graph that collapses its +~13-30 kernel launches into one replay. Across 40 layers those graphs are worth +about 1.8 ms/step. A capture that **fails** falls back to the direct kernel body +and produces **correct output** — so losing them costs only speed, silently. +This object is the only surface that reports it. + +| Key | Meaning | +|-----|---------| +| `live` | Slots holding a captured graph right now | +| `total` | Slots the forward has actually reached (see below) | +| `failed` | Attempted and did not stick — **the only value that is a fault** | +| `ineligible` | Gated out; this model never captures. **Not a fault.** | +| `pending` | Eligible, warming up, no attempt yet | +| `gave_up` | Subset of `failed` whose bounded retry budget is spent (omitted when 0) | + +**`ineligible` is normal.** The captured path requires the grouped MoE kernel, +which requires Q3_K expert tensors. A Q5_K_M model reports `live 0 / total 40 / +ineligible 40 / failed 0` — correct behaviour, zero exposure, and also zero of +the +4%. Read `failed`, not `live/total`, to decide whether something is wrong. + +**`total` counts only exercised slots.** A serve decoding batched never touches +its single-token graph slots, so those are not reported as missing graphs. + +A failed capture is retried on a bounded backoff and only stops after the budget +is spent, so a transient `failed` on one read that is `live` on the next is the +system self-healing, not a flapping metric. + +### `attention` — on-die vs host-softmax residency + +The same argument as `graphs`, only much sharper. Each full-attention layer +normally runs the on-die flash kernels. When it cannot it falls back to host-side +causal GQA softmax on the serve host's CPU — the exact wall this engine was built +to escape (it was 87.5% of an 8k prefill; moving attention on-die took that +prefill from 26 minutes to 3.82). The fallback returns the **same tokens**, logs +nothing per step, and on the depth bench costs **13.02 -> 1.22 tok/s** (K=2, +depth 4096) — and it worsens with context, because host softmax is O(context) +per layer per token. Without +this object a user simply waits, and nothing anywhere says why. + +| Key | Meaning | +|-----|---------| +| `ondie` | Attention layers running the on-die flash kernels right now. | +| `host` | Layers on the host-softmax fallback — **any non-zero value is a fault** | +| `total` | Layers the forward has actually reached (same rule as `graphs.total`) | +| `gave_up` | Layers whose KV-ring allocation spent its retry budget (omitted when 0) | +| `impossible` | Subset of `gave_up`: the rings cannot fit this die at all (omitted when 0) | + +`ondie + host == total`. Unlike `graphs`, there is no benign state here: `host > +0` always means this model is serving degraded, and the `reasons` histogram on +`/admin/models` says why. The two common causes are a per-slot KV-ring allocation +that did not fit (lower `--ctx-cap` or `--max-sessions`) and a session that has +run past the KV window, which wraps the ring. + +A ring allocation that fails is retried on a bounded backoff, so a `host` reading +that clears on the next poll is the system self-healing. `gave_up` is the number +that should page someone. + +No error cases (returns an empty `data` array when nothing is loaded). + +**Router's `/v1/models`** (`registry.c:reg_models_json`) is a different shape: it +aggregates across live serve instances and adds `live` (count of live instances serving +that id) and `free_slots` (summed across live replicas). Do not conflate the two. + +The router also emits `context_window`, aggregated the only honest way: the +**minimum** across that model_id's live replicas (a dispatch may land on any of +them), and **omitted entirely** if any live replica has not reported one. Serve +instances report their window on both `/registry/register` and every +`/registry/heartbeat`, because the ladder can move it at runtime. + +--- + +## POST /v1/chat/completions + +OpenAI-compatible chat completion. Supports streaming (SSE) and non-streaming. +Includes reasoning (`…` split) and tool-call support. + +### Request headers + +| Header | Required | Effect | +|--------|----------|--------| +| `Content-Type: application/json` | no | Not checked; the body is parsed as JSON regardless. | +| `X-Session-Id: ` | no | Prefix-cache session key. A body key (`session_id`, `prompt_cache_key`, `promptCacheKey`) takes precedence. opencode sends this header on every request. | + +### Request body + +```json +{ + "model": "my-model", + "messages": [ + {"role": "system", "content": "..."}, + {"role": "user", "content": "hello"} + ], + "temperature": 0.7, + "top_p": 0.95, + "top_k": 40, + "max_tokens": 1024, + "stop": ["<|im_end|>"], + "stream": false, + "tools": [...], + "tool_choice": "auto", + "session_id": "abc123" +} +``` + +| Field | Type | Default | Notes | +|-------|------|---------|-------| +| `model` | string | — | Routing key. With one loaded model, any value (or absent) routes to it. With multiple models, must match a served name exactly; unknown → 404. | +| `messages` | array | *required* | Non-empty array of `{role, content}`. Roles: `system`, `user`, `assistant`, `tool`; a missing role is treated as `user`. `content` must be a string — any other type (e.g. an array of content parts) is read as empty. | +| `messages[].reasoning_content` | string | unset | Assistant turns only. The reasoning this serve returned for that turn; replayed into the prompt so a session can resume from its prefix cache. | +| `messages[].tool_calls` | array | unset | Assistant turns only. Rendered back into the prompt in the model's tool dialect. | +| `temperature` | float | `0.7` | `<= 0` is greedy argmax. | +| `top_p` | float | `0.95` | Nucleus sampling. | +| `top_k` | int | `40` | Top-K sampling. | +| `max_tokens` | int | `1024` | Max completion tokens. `0` or negative → `512`. Clamped to 1048576. | +| `max_completion_tokens` | int | `1024` | Alias; `max_tokens` wins if both are present. | +| `stop` | string or array | unset | Stop strings. | +| `stream` | bool | `false` | `true` → SSE (`text/event-stream`). | +| `stream_options.include_usage` | bool | `false` | Streaming only: send a terminal `usage` chunk before `[DONE]`. | +| `seed` | number | unset | Makes sampling reproducible. With `n` > 1, choice *i* uses `seed + i`. | +| `presence_penalty` | float | `0` | Clamped to [-2, 2]. | +| `frequency_penalty` | float | `0` | Clamped to [-2, 2]. | +| `logit_bias` | object | unset | `{"": bias}`; bias clamped to [-100, 100]. Non-numeric keys or values are skipped. | +| `logprobs` | bool | `false` | Return per-token log-probabilities (`choices[].logprobs.content[]`, with `token`, `logprob`, `bytes`). | +| `top_logprobs` | int | `0` | Alternatives per token; clamped to [0, 20]. | +| `n` | int | `1` | Number of completions. Each is a full separate decode (only the first uses the session's prefix cache). Capped at 8; a larger `n` returns 8 and sets `n_status: "capped_to_8"`. | +| `response_format` | object | `{"type":"text"}` | `json_object` constrains the answer to valid JSON. `json_schema` gets the same JSON constraint but the schema's shape is **not** enforced. See `response_format_status` below. | +| `enable_thinking` | bool | unset | Think-style templates: `true` opens a think block, `false` prefills a closed one. Absent → the `PANTHEON_NO_THINK` env decides. | +| `reasoning_budget` | int | per-model | Force `` after N reasoning tokens. Overrides the model's `--reasoning-budget`; `0` or negative disables it. `max_reasoning_tokens` is an alias (`reasoning_budget` wins). | +| `tools` | array | unset | OpenAI-format tool definitions. Rendered into a system turn in the model's tool dialect (below). Ignored when `tool_choice` is `"none"`. | +| `tool_choice` | string or object | `"auto"` | `"none"` disables tools. `"required"` forces a call: the grammar is armed by default (`PANTHEON_TOOL_GRAMMAR=0` opts out). With thinking on the think block is unconstrained and the call is compelled from `` (models advertising `tool_choice_required_with_thinking`). `{"type":"function","function":{"name":"X"}}` is enforced only when `PANTHEON_TOOL_GRAMMAR=1`; otherwise it behaves like `"auto"`. | +| `session_id` | string | unset | Prefix-cache session key (overrides `X-Session-Id`). Enables delta prefill on later turns of the same session. | +| `prompt_cache_key` / `promptCacheKey` | string | unset | Aliases for `session_id` (OpenAI's name; opencode's camelCase body field). Order: `session_id`, `prompt_cache_key`, `promptCacheKey`, then the `X-Session-Id` header. | + +### Tool calling + +Tools are rendered into a system turn (merged with a leading `system` message) +in the dialect the routed model emits. The dialect is chosen from the model name +and architecture (`tool_calling.c:tc_dialect_for_model`), not from the GGUF chat +template: + +| Dialect | Used for | Call syntax | +|---------|----------|-------------| +| Qwen3-Coder XML | default | `VALUE` | +| MiniCPM XML | Llama-arch models whose template adds BOS (MiniCPM5) | `VALUE…` | +| JSON | served names starting `ornith` | fenced or bare `{"function"\|"name": NAME, "arguments": {…}}` | + +The model's output is parsed into OpenAI `tool_calls`, with arguments typed from +each tool's JSON schema. `PANTHEON_TOOL_GRAMMAR` decides which requests are +grammar-constrained: + +| Value | Constrained requests | +|-------|----------------------| +| unset (default) | `tool_choice: "required"` only — after reasoning, the output is one well-formed call to an offered tool, arguments typed and `required` keys present. Everything else decodes **unconstrained**. | +| `1` | every request with tools: `"auto"` masks once a call opener appears; `"required"` and a named function compel a call. | +| `0` | none — `"required"` is unconstrained too. | + +On the Tenstorrent serve a `"required"` turn keeps speculative decoding: drafts +are checked against the grammar and the output equals non-speculative +constrained decoding. On a `"required"` turn (every dialect, single-user and +batched) an end token is refused before the call opens and, inside the call, +allowed only where the call can still be closed; the serve then appends the +closing text. Any other call the model started but did not finish is left +out of `tool_calls` and reported in `tool_call_error` (`code`, optional +`function`, `parameter`, `message`) on the choice. + +`tools` and `response_format` cannot both constrain one decode: when the request +carries tools, tools win and `response_format_status` is `"ignored_tools_present"`. + +### Non-streaming response — 200 + +```json +{ + "id": "chatcmpl-a1b2c3d4", + "object": "chat.completion", + "created": 1720000000, + "model": "my-model", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "Hello!", + "reasoning_content": "...", + "tool_calls": [...] + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 12, + "completion_tokens": 7, + "total_tokens": 19, + "prompt_tokens_details": {"cached_tokens": 0}, + "kv_used": 12, + "kv_cap": 32000 + } +} +``` + +- `reasoning_content` — the text inside a `…` block; present only + when non-empty. Only think-style chat templates with thinking on produce it. + Non-think templates (plain ChatML such as SmolLM2, Mistral, Phi-3) return the + whole answer as `content`. +- `tool_calls` — present when a call was parsed; `finish_reason` is then + `"tool_calls"`. `content` is `null` for a pure tool call. +- `finish_reason` — `stop`, `length`, `tool_calls`, or `repetition_loop` (the + decode was stopped because the model was looping; outranks `tool_calls`). +- `logprobs` — on the choice, when requested. +- `prompt_tokens_details.cached_tokens` — prompt tokens reused from the prefix + cache rather than computed. Always present (`0` on a cold turn). +- `kv_used` — this session's KV occupancy in tokens after this turn. +- `kv_cap` — the **effective** per-session context window, the same number + `GET /v1/models` advertises as `context_window`, re-read every turn. If the + memory ladder moves the window mid-session, the next completion carries the + new value. Treat the model list as the initial plan and this as authoritative. +- `session_conflict: true` (inside `usage`) — two conversations are sharing one + session id, so neither can resume from cache. +- `response_format_status` (top-level, when the request sent `response_format`) + — `applied`, `schema_downgraded`, `ignored_tools_present`, + `ignored_unknown_type`, or `not_enforced` (a constraint was armed but the + output did not close a complete JSON object). +- `n_status` (top-level) — why fewer choices came back than `n` asked for: + `capped_to_8`, `partial_slot_unavailable`, `partial_generation_failed`. +- `usage` covers the whole request: the prompt is counted once, completion + tokens are summed across choices. + +A prompt longer than the effective window is **refused** with +`context_over_budget` (below); nothing is silently dropped. The +`context_capped` field the writer supports is never set by the current +generation path. + +### Streaming response — 200 (SSE) + +`Content-Type: text/event-stream`. Each chunk is: + +``` +data: {"id":"chatcmpl-...", "object":"chat.completion.chunk", "created":..., + "model":"...", "choices":[{"index":0,"delta":{...},"finish_reason":null}]} + +``` + +Delta fields emitted in order: + +1. `{"role":"assistant"}` — first chunk of each choice. +2. `{"reasoning_content":""}` — inside a think block (think-style templates only). +3. `{"content":""}` — answer text. `logprobs` rides the chunk carrying those tokens. +4. Tool-call chunks (when a call was parsed): + - Opening: `{"tool_calls":[{"index":0,"id":"...","type":"function","function":{"name":"...","arguments":""}}]}` + - Arguments: `{"tool_calls":[{"index":0,"function":{"arguments":"..."}}]}` +5. Closing chunk: `{"delta":{},"finish_reason":"..."}`. Carries `tool_call_error` + on the choice and `response_format_status` top-level when applicable. +6. With `stream_options.include_usage`: a chunk with `"choices":[]` and `usage`. +7. Terminal sentinel: `data: [DONE]` + +With `n` > 1 the choices stream one after another, told apart by `choices[].index`. +A client that disconnects aborts the turn and frees its slot. + +Errors detected after the stream opened cannot change the status code: +`context_over_budget` is sent as a terminal event +`data: {"error":{"message":"...","type":"context_over_budget","code":null,"kv_used":N,"kv_cap":M}}` +and the stream ends; other generation failures close the choice with +`finish_reason: "stop"`. + +### Error responses + +| Status | `type` | Condition | +|--------|--------|-----------| +| 400 | `invalid_request_error` | Empty body, malformed JSON, body not an object, `messages` missing or empty | +| 404 | `model_not_found` | Several models loaded and `model` matches none | +| 422 | `context_over_budget` | Prompt exceeds the effective per-session window. Not transient: shorten the conversation and retry. Non-streaming: `kv_used`/`kv_cap` are in the message text; streaming: they are fields of the error object. | +| 429 | `server_busy` | Single-user path: a generation is already running on this model. Batched path: all slots busy and the queue full, or 30 s queue wait timed out. `Retry-After` when the scheduler supplies one. | +| 429 | `model_at_session_capacity` | A new `session_id` while every session slot (`max_sessions`) holds a live session. `Retry-After: 30`; the message estimates the re-prefill time. | +| 503 | `no_models_loaded` | No model loaded | +| 503 | `server_memory_constrained` | Host memory pressure: admission refused the prompt, or the queue is full under critical pressure | +| 503 | `server_shutting_down` | Server is draining for shutdown. `Retry-After: 1` when refused at the route. | +| 500 | `server_error` | Generation failed / out of memory | + +Source: `openai_chat.c:oai_handle_chat`, `serve_generation.c:sg_run_completion`, +`serve_command.c:dispatch`. + +--- + +## POST /admin/models/load + +Operator-only. Load a model at runtime (Phase 7). Serialized against other +uploads via `reg->upload_lock` (one GPU upload in flight at a time). + +### Request body + +```json +{ + "path": "/models/my-model.gguf", + "name": "my-model", + "dies": [0, 1], + "quantize": false, + "ctx_cap": 32768, + "max_sessions": 4, + "max_concurrent": 2, + "reasoning_budget": 1024 +} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `path` | string | yes | GGUF file path | +| `name` | string | no | Served id (defaults to file basename) | +| `dies` | int array | no | Device ordinals (same `CUDA_VISIBLE_DEVICES`-relative semantics as `--dies`). Max `PN_MODEL_MAX_DIES=8`. | +| `quantize` | bool | no | INT8-quantize at load time | +| `ctx_cap` | int | no | Per-session context cap in tokens; a prompt past it is refused with `context_over_budget`. `0`/absent = engine default. Mirrors `--ctx-cap`. | +| `max_sessions` | int | no | **Residency** — how many named sessions may hold KV at once. Over the cap a new session id gets `429 model_at_session_capacity`. `0`/absent = engine default. Mirrors `--max-sessions`. | +| `max_concurrent` | int | no | **Concurrency** — how many requests may decode simultaneously (batch-scheduler slots). Surplus queues, then `429`/`503`. Distinct from `max_sessions`. `0`/absent = the resolved `max_sessions`. Mirrors `--max-concurrent`. | +| `reasoning_budget` | int | no | Force `` after N tokens of unclosed reasoning. `0`/absent = unbounded. Mirrors `--reasoning-budget`. | + +Every per-model option available on the `serve` command line is settable here, so a model +added at runtime ends up configured identically to one launched with the process. + +### Response — 200 + +```json +{ + "id": "my-model", + "object": "model", + "path": "/models/my-model.gguf", + "dies": [0, 1], + "vram_bytes": 12345678, + "max_sessions": 4, + "max_concurrent": 4, + "context_window": 32000, + "context_window_configured": 32000, + "graphs": { + "live": 30, "total": 40, "failed": 0, "gave_up": 0, + "ineligible": 10, "pending": 0, + "captures": 30, "capture_failures": 0, + "batched_attn": {"live": 10, "total": 10, "failed": 0, "ineligible": 0, "pending": 0}, + "batched_ssm": {"live": 20, "total": 30, "failed": 0, "ineligible": 10, "pending": 0} + }, + "attention": { + "ondie": 10, "host": 0, "total": 10, + "gave_up": 0, "impossible": 0, + "host_engagements": 0, "ring_alloc_failures": 0 + } +} +``` + +The operator surface carries **both** context numbers, because an operator's +question is not the client's: `context_window` is what sessions actually get right +now, `context_window_configured` is what `--ctx-cap` asked for. They are equal +unless the memory ladder is clamping this instant — and seeing them differ is +exactly the signal that memory pressure is costing context. (`/v1/models` +advertises only the effective one; a client has no use for a window it cannot +have.) Both are omitted when unknown. + +Falls back to `{"status":"loaded"}` if the row cannot be found in the snapshot (race). + +### Error responses + +| Status | `type` | Condition | +|--------|--------|-----------| +| 400 | `invalid_request_error` | Missing `path`, malformed JSON, too many dies | +| 400 | `load_failed` | `dies` named on a serve with no accelerator backend compiled in, or with no dies visible. Not retryable — the fix is a different binary (`pantheon --backend`) or an omitted `dies`. | +| 409 | `load_failed` | Model already loaded / already unloading | +| 507 | `load_failed` | Admission refused (VRAM or host), die binding conflict, registry full | +| 404 | `load_failed` | File not found | +| 500 | `load_failed` | Other load failure | + +Source: `model_admin.c:ma_handle_load`. + +--- + +## POST /admin/models/unload + +Operator-only. Unload a model by served name. Drains any in-flight generation +before freeing. + +### Request body + +```json +{"name": "my-model"} +``` + +| Field | Type | Required | Notes | +|-------|------|----------|-------| +| `name` | string | yes | Served model id to unload | + +### Response — 200 + +```json +{"unloaded": "my-model"} +``` + +### Error responses + +| Status | `type` | Condition | +|--------|--------|-----------| +| 400 | `invalid_request_error` | Missing `name`, malformed JSON | +| 404 | `unload_failed` | Name not loaded | +| 409 | `unload_failed` | Already unloading | +| 500 | `unload_failed` | Other failure | + +Source: `model_admin.c:ma_handle_unload`. + +--- + +## GET /admin/models + +Operator-only. Every **live instance**, one row each. Unlike `/v1/models` these +are **not** deduplicated by name: two instances of one model on different dies +are two rows, because to an operator they are two different things. + +The row is the same one `POST /admin/models/load` returns — this route exists so +its operator-only fields can be read at any time rather than only as a side +effect of loading a model. + +### Response — 200 + +```json +{ + "object": "list", + "data": [ + { + "id": "my-model", + "object": "model", + "path": "/models/my-model.gguf", + "dies": [2, 3], + "vram_bytes": 12345678, + "max_sessions": 4, + "max_concurrent": 4, + "max_concurrent_configured": 4, + "context_window": 32000, + "context_window_configured": 32000, + "graphs": { "live": 30, "total": 40, "failed": 0, "gave_up": 0, + "ineligible": 10, "pending": 0, + "captures": 30, "capture_failures": 0, + "batched_attn": {"live": 10, "total": 10, "failed": 0, + "ineligible": 0, "pending": 0}, + "batched_ssm": {"live": 20, "total": 30, "failed": 0, + "ineligible": 10, "pending": 0} }, + "attention": { "ondie": 8, "host": 2, "total": 10, + "gave_up": 2, "impossible": 0, + "host_engagements": 2, "ring_alloc_failures": 10, + "reasons": {"ring_alloc_failed": 2} } + } + ] +} +``` + +The operator row carries **both** concurrency numbers: `max_concurrent` is what +the model serves right now (`bs_get_load` capacity, the same value `/v1/models` +advertises) and `max_concurrent_configured` is what `--max-concurrent` asked for. +They differ when there is no batch scheduler at all (no CUDA compiled in, +`QWEN35_BATCH_SLOTS` off, `bs_create` failed) or when the memory-pressure ladder +clamped the slot count — the same split, for the same reason, as +`context_window` vs `context_window_configured`. + +The operator `graphs` object extends the `/v1/models` one with `gave_up`, the +lifetime `captures` / `capture_failures` counters, and a per-class split: +`batched_attn` and `batched_ssm` (the multi-user decode path), `single_ssm` (the +one-user per-layer path) and `segment` (the per-die full-step graph). A class +the model has never exercised is omitted rather than reported as an empty +deficit. Field meanings: see `graphs` under `GET /v1/models`. + +The operator `attention` object adds `gave_up` / `impossible`, the lifetime +`host_engagements` / `ring_alloc_failures` counters, and `reasons` — a +histogram, present only when `host > 0`, naming why each layer is off-die +(`ring_alloc_failed`, `dec_scratch_alloc_failed`, `kv_geometry_or_ring_wrap`, +`no_slot_decode_kernel`, `no_flash_kernels`, `no_batch_scratch`, +`flash_slots_off`). Field meanings: see `attention` under `GET /v1/models`. + +No error cases (returns an empty `data` array when nothing is loaded). + +Source: `model_admin.c:ma_handle_models`. + +--- + +## GET /admin/dies + +Operator-only. Snapshot of all probed accelerator dies, their VRAM budget/used, +and which model occupies them. + +### Response — 200 + +```json +{ + "object": "list", + "n_dies": 8, + "data": [ + { + "die": 0, + "card": 0, + "model": "my-model", + "n_models": 1, + "vram_budget": 12884901888, + "vram_used": 11674869760 + } + ] +} +``` + +| Field | Type | Notes | +|-------|------|-------| +| `die` | int | Die manager ordinal (0-indexed across all probed dies) | +| `card` | int | Physical card index (`pn_dm_card_of(die)`) | +| `model` | string | Name of the model occupying this die, or empty string | +| `n_models` | int | Number of models on this die (typically 0 or 1) | +| `vram_budget` | int (bytes) | Die's total VRAM minus `PN_DIE_RESERVE_BYTES` | +| `vram_used` | int (bytes) | Bytes allocated by the model manager | + +Source: `model_admin.c:ma_handle_dies`. + +--- + +## Session routes + +A session is a live prefix-cache / decode slot keyed by `session_id`. In each +route below the session id comes from the body's `session_id`, else the +`X-Session-Id` header (export reads the body only). `name` selects the model; +absent → the default model. Unknown model → `404 model_not_found`. + +One session id may carry several conversations (opencode sends its title +request and its real turns under one id). Each keeps its own cache: a turn +resumes from the conversation its prompt continues. `max_sessions` counts ids, +not conversations. On the single-user path (no batch scheduler) export takes the +id's most recently used conversation, and restore replaces the conversation the +restored `tokens` continue (else adds one); release frees all of them. + +### POST /admin/session/export + +Operator. Serialize a live session's state so it can be restored later. + +Request: `{"name": "my-model", "session_id": "abc123"}` (`session_id` required). + +Response — 200: + +```json +{"session_id":"abc123","weights_version":123,"n_tok":4096, + "blob_bytes":31457280,"blob_b64":"","tokens":[...]} +``` + +`tokens` is the committed history the blob covers; it must be sent back on +restore. + +| Status | `type` | Condition | +|--------|--------|-----------| +| 400 | `invalid_request_error` | Empty/malformed body, missing `session_id` | +| 400 | `session_export_unsupported` | The model's architecture has no session snapshot format | +| 400 | `session_caching_disabled` | The model has no session cache | +| 404 | `session_not_found` | No live session with that id | +| 429 | `session_busy` | A turn is in flight on that session; retry after it completes | +| 500 | `server_error` | Serialize / out-of-memory failure | + +Source: `model_admin.c:ma_handle_session_export`. + +### POST /session/restore + +Install an exported session into a live slot. + +Request: + +```json +{"name":"my-model","session_id":"abc123","blob":"","tokens":[...]} +``` + +`session_id`, `blob` (base64) and `tokens` are required. The blob must come from +the same model architecture and the same weights. + +Response — 200: `{"restored":true,"session_id":"abc123","n_tok":4096}` + +| Status | `type` | Condition | +|--------|--------|-----------| +| 400 | `invalid_request_error` | Empty/malformed body, missing `session_id`/`blob`/`tokens` | +| 400 | `session_caching_disabled` | The model has no session cache | +| 400 | `bad_session_blob` | Not base64, corrupt, wrong architecture, or does not fit this model | +| 409 | `weights_version_mismatch` | Blob captured under different weights — replay the conversation instead | +| 429 | `model_at_session_capacity` | No free session slot | +| 500 | `server_error` | Restore failed | + +Source: `openai_chat.c:oai_handle_session_restore`. + +### POST /session/release + +Free a session's slot now instead of waiting for the idle timeout (12 h default, +`PN_SESSION_IDLE_TTL_MS`). + +Request: `{"name": "my-model", "session_id": "abc123"}` + +Response — 200: `{"released":true,"session_id":"abc123"}`. Idempotent: an unknown +or already-freed session also returns 200. + +Errors: `400 invalid_request_error` (empty/malformed body, no session id), +`404 model_not_found`. + +Source: `openai_chat.c:oai_handle_session_release`. + +### GET /session/estimate + +`GET /session/estimate?tokens=N[&name=M][&blob_bytes=B][&upload_bps=R]` + +Arithmetic estimate of restoring a session from a blob vs re-prefilling it. +`tokens` (> 0) is required. Without `blob_bytes`, the blob is estimated at +16 KiB per token; without `upload_bps` (bytes/s), 12.5 MB/s is assumed. + +Response — 200: + +```json +{"model":"my-model","tokens":4096,"prefill_tok_per_s":17.00, + "cold_reprefill_s":240.94,"warm_restore_s":5.391,"blob_bytes_est":67108864} +``` + +`prefill_tok_per_s` is the serve's measured prefill rate (a fixed fallback +before one is measured). An unknown `name` is not an error; `model` is then `""`. + +Errors: `400 invalid_request_error` when `tokens` is missing or not positive. + +Source: `openai_chat.c:oai_handle_session_estimate`. + +### GET /session/watch + +`GET /session/watch?session_id=S[&name=M]` (or `X-Session-Id`). + +Liveness hold. On success the server answers `200` with +`Content-Type: text/event-stream`, a `: watch established` comment, and keeps the +connection open. While it is held the session is never reaped for idleness; +when it closes, the session is released after a grace period (45 s default, +`PN_SESSION_WATCH_GRACE_MS`) unless a new watch for it arrives first. + +| Status | `type` | Condition | +|--------|--------|-----------| +| 400 | `invalid_request_error` | No session id | +| 400 | `session_caching_disabled` | The model has no session cache | +| 404 | `model_not_found` | Unknown model | +| 503 | `watch_unavailable` | Watch manager not running, or its table is full | + +Source: `serve_command.c:handle_session_watch`, `session_watch.c`. + +--- + +## Error body format + +All error responses use: + +```json +{"error":{"message":"","type":"","code":null}} +``` + +The `code` field is always `null`. + +--- + +## Concurrency and connection model + +- One detached pthread per accepted TCP connection. `accept()` never blocks + behind a generation. +- On `pthread_create` failure the connection is handled inline (no request + is dropped). +- SIGPIPE is ignored so a client disconnect mid-stream cannot kill the server. +- Generation serialization: + - **Legacy path** (`sched == NULL`): `gen_lock` trylock per model. Second + concurrent request to the same model → immediate 429. + - **Batched path** (`QWEN35_BATCH_SLOTS=1`): the C3 scheduler owns + concurrency. Up to 8 concurrent users; overflow waits in a per-model FIFO + queue (default depth 32, `BS_QUEUE_DEPTH`). 30-second queue wait timeout. diff --git a/docs/reference/supported-models.md b/docs/reference/supported-models.md new file mode 100644 index 0000000..cd2c65e --- /dev/null +++ b/docs/reference/supported-models.md @@ -0,0 +1,370 @@ +# Supported Models + +Models verified to load and generate correctly with Pantheon, with the config each one +needs and a runnable command. This list grows as architecture support lands and new +GGUFs are tested. + +Hardware in the **Tested** column is what the row was actually measured on — `M4 CPU` is +Apple Silicon, `K80 GPU` is a Tesla K80 (GK210, `sm_37`) via the CUDA backend, `TT p150a` is a +Tenstorrent Blackhole p150a via the `USE_TT=1` backend. + +**Tenstorrent: Ornith-1.5-9B is the only supported model.** Other rows marked `TT p150a` ran on +the card during development; they are not supported there. To run Ornith 1.5 on a card, see the +README's "Ornith 1.5 on a Tenstorrent card". + +**Tesla K80: Qwen3.6-35B-A3B and Nex-N2-mini (Q3_K_M, one model per card) are the supported +models**, run with `./k80_qwen36.sh` / `./k80_nexn2.sh` (README "Qwen3.6 and Nex-N2-mini on a +Tesla K80"). Other `K80 GPU` rows ran on the card; they are not supported there. + +--- + +## Llama Architecture + +| Model | Quant | Params | Layers | Heads (Q/KV) | Vocab | Context | Tested | Notes | +|-------|-------|--------|--------|--------------|-------|---------|--------|-------| +| SmolLM2-135M-Instruct | Q8_0 | 135M | 30 | 9/3 | 49,152 | 8,192 | M4 CPU | 81.3 tok/s decode, 42ms load. Pre-tokenizer falls back with a warning — see [Known Issues](#known-issues). | +| MiniCPM5-1B-SFT | Q8_0 | 1B | 24 | 16/2 | 130,560 | 131,072 | TT p150a | Ran on the card (not supported there) at 4.37 tok/s (see [`../architecture/accel-foundry.md`](../architecture/accel-foundry.md) §7 for its open correctness defect). Orchestrator. arch="llama" GGUF, head_dim=128 (decoupled: n_embd/n_head=96), rope_theta=5e6, rms_eps=1e-6. No new forward — config-only. ctx capped to 4096 for KV. | + +**Source (SmolLM2):** `bartowski/SmolLM2-135M-Instruct-GGUF` on HuggingFace +**Source (MiniCPM5-1B):** `openbmb/MiniCPM5-1B-GGUF` (`MiniCPM5-1B-Q8_0.gguf`, ~1.15 GB, Apache-2.0). Loads into `PN_ARCH_LLAMA` — config-only, no new forward pass. + +**Best config:** `make USE_BLAS=1` (no threads needed, model is small) + +```bash +huggingface-cli download bartowski/SmolLM2-135M-Instruct-GGUF SmolLM2-135M-Instruct-Q8_0.gguf --local-dir models/base/ + +./pantheon --model models/base/SmolLM2-135M-Instruct-Q8_0.gguf \ + --prompt "Hello, my name is" --max-tokens 32 +``` + +--- + +## Qwen2 Architecture + +| Model | Quant | Params | Layers | Heads (Q/KV) | Vocab | Context | Tested | Notes | +|-------|-------|--------|--------|--------------|-------|---------|--------|-------| +| Qwen2.5-0.5B-Instruct | Q8_0 | 494M | 24 | 14/2 | 151,936 | 32,768 | M4 CPU | Has attention biases, tied embeddings | + +**Source:** `Qwen/Qwen2.5-0.5B-Instruct-GGUF` on HuggingFace + +**Best config:** `make USE_BLAS=1` (threads may help at this size) + +```bash +huggingface-cli download Qwen/Qwen2.5-0.5B-Instruct-GGUF qwen2.5-0.5b-instruct-q8_0.gguf --local-dir models/base/ + +./pantheon --model models/base/qwen2.5-0.5b-instruct-q8_0.gguf \ + --prompt "Hello, how are you?" --max-tokens 32 +``` + +--- + +## Mistral Architecture + +| Model | Quant | Params | Layers | Heads (Q/KV) | Vocab | Context | Sliding Window | GGUF Ver | Tested | Notes | +|-------|-------|--------|--------|--------------|-------|---------|----------------|----------|--------|-------| +| Mistral-7B-Instruct-v0.3 | Q4_K_M | 7.2B | 32 | 32/8 | 32,768 | 32,768 | 4,096 | v3 | M4 CPU | BPE tokenizer, coherent output | +| Mistral-7B-Instruct-v0.1 | Q4_K_M | 7.2B | 32 | 32/8 | 32,000 | 32,768 | — | v2 | M4 CPU | SentencePiece tokenizer, GGUF v2 | + +**Source:** `bartowski/Mistral-7B-Instruct-v0.3-GGUF`, `TheBloke/Mistral-7B-Instruct-v0.1-GGUF` on HuggingFace + +**Best config:** `make USE_BLAS=1 USE_THREADS=1` (7B model needs threads) + +```bash +# v0.3 (BPE, GGUF v3) +huggingface-cli download bartowski/Mistral-7B-Instruct-v0.3-GGUF \ + Mistral-7B-Instruct-v0.3-Q4_K_M.gguf --local-dir models/base/ + +./pantheon --model models/base/Mistral-7B-Instruct-v0.3-Q4_K_M.gguf \ + --prompt "Hello, how are you?" --max-tokens 32 + +# v0.1 (SentencePiece, GGUF v2) +huggingface-cli download TheBloke/Mistral-7B-Instruct-v0.1-GGUF \ + mistral-7b-instruct-v0.1.Q4_K_M.gguf --local-dir models/base/ + +./pantheon --model models/base/mistral-7b-instruct-v0.1.Q4_K_M.gguf \ + --prompt "Hello" --max-tokens 32 +``` + +--- + +## Phi-3 Architecture + +| Model | Quant | Params | Layers | Heads (Q/KV) | Vocab | Context | Tested | Notes | +|-------|-------|--------|--------|--------------|-------|---------|--------|-------| +| Phi-3-mini-4k-instruct | Q4_K_M | 3.8B | 32 | 32/32 | 32,064 | 4,096 | M4 CPU | MHA, NeoX RoPE, fused QKV + gate/up | + +**Source:** `microsoft/Phi-3-mini-4k-instruct-gguf` on HuggingFace + +**Best config:** `make USE_BLAS=1` (threads help at this size) + +```bash +huggingface-cli download microsoft/Phi-3-mini-4k-instruct-gguf \ + Phi-3-mini-4k-instruct-Q4_K_M.gguf --local-dir models/base/ + +./pantheon --model models/base/Phi-3-mini-4k-instruct-Q4_K_M.gguf \ + --prompt "The capital of France is" --max-tokens 32 +``` + +--- + +## Mixtral Architecture (MoE) + +| Model | Quant | Params | Layers | Heads (Q/KV) | Vocab | Experts (total/used) | Tested | Notes | +|-------|-------|--------|--------|--------------|-------|---------------------|--------|-------| +| Mixtral_7Bx2_MoE | Q4_K_M | ~13B | 32 | 32/8 | 32,000 | 2/2 | M4 CPU | Merged expert tensors, SentencePiece tokenizer | + +**Source:** `TheBloke/Mixtral_7Bx2_MoE-GGUF` on HuggingFace + +**Best config:** `make USE_BLAS=1 USE_THREADS=1` (large model needs threads) + +**Architecture notes:** +- MoE extends Llama: attention path is identical, only FFN differs +- Mixtral GGUFs use `general.architecture = "llama"`, detected via `llama.expert_count` metadata +- Per-layer FFN: router → softmax → top-k expert selection → SwiGLU per expert → weighted sum +- Supports merged (`ffn_gate_exps.weight` 3D) and split (`ffn_gate.E.weight` per expert) tensor formats + +```bash +huggingface-cli download TheBloke/Mixtral_7Bx2_MoE-GGUF \ + mixtral_7bx2_moe.Q4_K_M.gguf --local-dir models/base/ + +./pantheon --model models/base/mixtral_7bx2_moe.Q4_K_M.gguf \ + --prompt "Hello" --max-tokens 32 +``` + +--- + +## RWKV-X Architecture (Safetensors) + +| Model | Format | Params | Layers (RWKV/Attn) | Heads | Embd | Vocab | Tested | Notes | +|-------|--------|--------|-------------------|-------|------|-------|--------|-------| +| RWKV-X-0.2B-64k-Base | safetensors | 0.2B | 12/4 | 12 | 768 | 65,536 | M4 CPU | FP32 and INT8 verified | +| RWKV-X-3.6B-64k-Base | safetensors | 3.6B | 32/8 | 40 | 2,560 | 65,536 | M4 CPU | 17.2 tok/s INT8, 2.6 tok/s FP32, 8.9 GB RSS | + +**Source:** `howard-hou/RWKV-X` on HuggingFace + +**Best config:** +- 0.2B: `make USE_BLAS=1` (no threads needed). Add `--quantize` for INT8 (~3x faster decode). +- 3.6B: `make USE_BLAS=1 USE_THREADS=1`. Add `--quantize` for INT8 (~6.4x faster decode). + +**Requires:** RWKV World tokenizer vocab file (`--vocab` flag) + +```bash +# Download models +huggingface-cli download howard-hou/RWKV-X \ + RWKV-X-0.2B-64k-Base.safetensors --local-dir models/base/ + +huggingface-cli download howard-hou/RWKV-X \ + RWKV-X-3.6B-64k-Base.safetensors --local-dir models/base/ + +# 0.2B FP32 inference +./pantheon --model models/base/RWKV-X-0.2B-64k-Base.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello world" --max-tokens 32 + +# 3.6B INT8 inference (recommended) +./pantheon --model models/base/RWKV-X-3.6B-64k-Base.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "The capital of France is" --max-tokens 64 --quantize +``` + +**Architecture notes:** +- Hybrid model: RWKV-7 recurrent blocks (WKV-7 kernel) + sparse attention blocks +- 0.2B: 12 RWKV + 4 attention blocks, interleaved [R,R,R,A,R,R,R,A,...] +- 3.6B: 32 RWKV + 8 attention blocks, same interleaving pattern +- Dual state: fixed-size recurrent state per RWKV block + KV cache per attention block +- Trie-based World tokenizer (65K vocab, UTF-8 byte-level) +- Uses safetensors format (not GGUF) + +--- + +## RWKV-7 Architecture (Converted from .pth) + +| Model | Source Format | Params | Layers | Heads | Embd | Vocab | Tested | Notes | +|-------|-------------|--------|--------|-------|------|-------|--------|-------| +| RWKV-x070-World-0.1B-v2.8 | .pth → safetensors | 0.1B | 12 | 12 | 768 | 65,536 | M4 CPU | BF16, 399 tensors, converted via `pantheon-convert` | + +**Source:** `BlinkDL/rwkv-7-world` on HuggingFace (original .pth format) + +**Conversion:** Requires `pantheon-convert` to convert from PyTorch .pth to safetensors first. The .pth reader is a sandboxed pickle interpreter in pure C — no Python needed. + +**Best config:** `make USE_BLAS=1` (small model, no threads needed) + +**Requires:** RWKV World tokenizer vocab file (`--vocab` flag) + +```bash +# Download .pth model +huggingface-cli download BlinkDL/rwkv-7-world \ + RWKV-x070-World-0.1B-v2.8-20241210-ctx4096.pth \ + --local-dir models/base/ + +# Convert to safetensors (no Python needed) +./pantheon-convert models/base/RWKV-x070-World-0.1B-v2.8-20241210-ctx4096.pth \ + -o models/base/RWKV-7-0.1B.safetensors + +# Run inference +./pantheon --model models/base/RWKV-7-0.1B.safetensors \ + --vocab foundry/rwkvx/data/rwkv_vocab_v20230424.txt \ + --prompt "Hello" --max-tokens 32 +``` + +**Architecture notes:** +- Pure RWKV-7 (no sparse attention blocks, unlike RWKV-X hybrid) +- WKV-7 kernel with Generalized Delta Rule +- ChannelMix FFN with squared ReLU +- BFloat16 weights preserved through conversion (no precision loss) +- Uses same inference engine as RWKV-X (shared `pn_rwkvx_*` code path) + +--- + +## qwen35moe Architecture (Hybrid Gated-Delta SSM + Shared-Expert MoE) + +| Model | Quant | Params | Layers (SSM/Attn) | Heads (Q/KV) | Head dim | Embd | Experts (total/used+shared) | Vocab | Tested | Notes | +|-------|-------|--------|-------------------|--------------|----------|------|-----------------------------|-------|--------|-------| +| Nex-N2-mini | Q3_K_M | 35B-A3B | 30/10 (interval 4) | 16/2 | 256 | 2,048 | 256/8 + 1 | 248,320 | M4 CPU | **Oracle-validated** vs llama.cpp (layer-by-layer, `temp 0`). Reasoning model (``). load 376 ms, ~1.0 tok/s decode, 9.5 GB RSS — CPU **reference path**. | +| Nex-N2-mini | Q3_K_M | 35B-A3B | 30/10 (interval 4) | 16/2 | 256 | 2,048 | 256/8 + 1 | 248,320 | **K80 GPU (2-die)** | **Supported on K80** (`./k80_nexn2.sh`). Serve, 2026-10-20: ~21 tok/s decode at 1k context, prefill 107-119 tok/s up to 8k, 74 at 14k, 53 at 30k. Earlier single-stream CLI: ~13.7 tok/s decode — Q3_K_M split 20/20 across the two dies of one K80 (small-BAR, both dies mapped at once). GPU output **bit-matches the CPU forward** (argmax + every per-token logit, ulp 0) with `QWEN35_FUSE=0x37 QWEN35_FLASH=0`, measured by the author; default flash prefill is argmax-exact only. ~68× the CPU path; ~15–20 with kernel fusion (Kepler bit-exact ceiling). See [`../architecture/qwen35moe.md`](../architecture/qwen35moe.md). | + +| Qwen3.6-35B-A3B | Q3_K_M | 35B-A3B | 30/10 (interval 4) | 16/2 | 256 | 2,048 | 256/8 + 1 | 248,320 | **K80 GPU (2-die)** | **Supported on K80** (`./k80_qwen36.sh`). Serve, 2026-10-20: ~21 tok/s decode at 1k context, prefill 116-121 tok/s up to 8k, 79 at 14k, 60 at 30k. | + +**Source:** `nex-agi/Nex-N2-mini` (GGUF: `bartowski/nex-agi_Nex-N2-mini-GGUF`), `Qwen/Qwen3.6-35B-A3B` +(GGUF: `bartowski/Qwen_Qwen3.6-35B-A3B-GGUF`) + +**Best config (M4, oracle):** `make USE_BLAS=1` (Accelerate) — the correctness reference, not a speed target. + +**Best config (Tesla K80, the speed path):** `make USE_CUDA=1`; run the 2-die GPU decode with +`QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=0 QWEN35_GPU_DIE_B=1 ./pantheon --model --prompt `. +~13.7 tok/s single-stream; bit-matching the oracle under `QWEN35_FUSE=0x37 QWEN35_FLASH=0`. Multi-card K80 hosts may need platform setup +(PCI BAR sizing, NVIDIA driver options); Foundry's `barswap/k80_quirk/` documents it. + +**Architecture notes:** +- Hybrid (Qwen3-Next lineage): **30 Gated-Delta-Net SSM** layers + **10 sparse full-attention** layers, attention when `(i+1) % 4 == 0` +- **Shared-expert MoE every layer:** softmax + top-8/256 routed experts (SwiGLU, `norm_topk_prob`) **plus** an always-on shared expert with a sigmoid gate +- SSM block: causal conv1d (k=4) → SiLU → L2-norm q/k → GQA head-broadcast → read-after-write delta-rule recurrence (scalar GDA decay) → gated RMSNorm +- Attention block: GQA 16/2, head_dim 256, QK-RMSNorm, **partial NEOX RoPE** (first 64 of 256 dims), sigmoid output gate +- Dual state per sequence: fixed recurrent SSM state (30 layers) + KV cache (10 layers), à la RWKV-X +- Tokenizer: Qwen BPE, `tokenizer.ggml.pre = "qwen35"` pre-tokenizer; ChatML with `` reasoning prefill +- **MTP/nextn trap:** GGUF must carry `nextn_predict_layers=0`; the loader never reads `blk.N.nextn.*` +- The A8 gibberish bug was the SSM GQA head-broadcast tiling (`vh = r*n_k_heads + kh`, interleaved like `ggml_repeat`); see [`../architecture/qwen35moe.md`](../architecture/qwen35moe.md) + +```bash +# Interactive (applies ChatML + ); --temperature 0 for oracle-style greedy +./pantheon --model models/nex-agi_Nex-N2-mini-Q3_K_M.gguf --chat --temperature 0 + +# One-shot (chat template applied automatically; --raw to send raw text) +./pantheon --model models/nex-agi_Nex-N2-mini-Q3_K_M.gguf \ + --temperature 0 --max-tokens 90 --prompt "How much does the ball cost?" +``` + +--- + +## qwen35 Dense Architecture (Bonsai / Ornith) + +A GGUF with `general.architecture = "qwen35"` and a dense FFN (`blk.0.ffn_gate.weight`, no +merged expert tensors) loads as `PN_ARCH_BONSAI` (`src/models/bonsai_forward.c`): the same +Gated-DeltaNet + sparse full-attention hybrid as qwen35moe, with a dense SwiGLU FFN in place of +the MoE. A `qwen35` GGUF with routed experts loads as qwen35moe instead. + +| Model | Quant | Params | Layers (SSM/Attn) | Tested | Notes | +|-------|-------|--------|-------------------|--------|-------| +| Ternary-Bonsai-27B | Q2_0 (g128 ternary, GGUF type 142) | 27B | 48/16 | CPU | Oracle-equivalent vs PrismML's llama.cpp fork. | +| Ornith-1.5-9B | Q8_0 | 9B | 24/8 | TT p150a | **The supported Tenstorrent model.** `./install.sh && ./run_ornith.sh`. Serve: 42.9 tok/s decode, 1,630-token prompt in 2.43 s (670 tok/s), MTP speculative decoding on. Tool calls use the JSON dialect ([HTTP API](http-api.md#tool-calling)). | +| Ornith-1.0-9B | Q8_0 | 9B | — | K80 GPU, TT p150a | K80: 9.0 tok/s decode. TT (ran, not supported): output byte-identical to the CPU forward, 1.57 tok/s. Tool calls use the JSON dialect ([HTTP API](http-api.md#tool-calling)). | + +**Backends:** CPU; CUDA (single die, falls back to CPU if the upload fails); Tenstorrent +(used only when no other accelerator took the model). + +--- + +## Pixtral Vision Encoder (library only) + +`src/models/pixtral_vision.c` runs a Pixtral / Mistral-Small-3.1 vision tower with the +LightOnOCR projector from an llama.cpp-style `clip` mmproj GGUF, producing image embeddings +for a ChatML decoder (`<|vision_start|>`, `<|image_pad|>`, `<|vision_end|>` tokens). + +- **C API only:** `pn_attach_vision`, `pn_describe` / `pn_describe_stream` / `pn_describe_embeds`, + `pn_vision_encode_rgb`, and a continuous-batch decoder (`pn_batch_decoder_*`) for bulk OCR + (`include/pantheon.h`). The CLI and `pantheon serve` do not accept images; HTTP message + `content` must be a string. +- Decoding goes through the Llama-family path (`pn_llama_prefill` / `pn_llama_forward_batch`). +- Exercised by `tests/bench_vision.c`. + +--- + +## Supported Quant Types + +| Type | Bits/Weight | Block Size | Status | +|------|-------------|------------|--------| +| F32 | 32 | — | Supported | +| F16 | 16 | — | Supported | +| Q8_0 | 8 | 32 | Supported | +| Q4_0 | 4 | 32 | Supported | +| Q3_K | 3.4 | 256 | Supported | +| Q4_K | 4.5 | 256 | Supported | +| Q5_K | 5.5 | 256 | Supported | +| Q6_K | 6.5 | 256 | Supported | +| BF16 | 16 | — | Supported | +| Q2_0 (g128 ternary, type 142) | 2.1 | 128 | Supported (Bonsai) | + +Q3_K_M, Q4_K_M and Q5_K_M GGUFs mix the K-quant tiers (e.g. Q3_K for most layers with Q4_K/Q6_K for attention) — all supported. Q3_K was added for the Nex-N2-mini bring-up. + +--- + +## Hardware Tested + +| Hardware | OS | Build Command | Notes | +|----------|----|---------------|-------| +| Apple M4 | macOS 15 | `make USE_BLAS=1` | Accelerate BLAS, NEON SIMD | +| Apple M4 | macOS 15 | `make USE_BLAS=1 USE_THREADS=1` | Add threads for 1B+ models | +| Tesla K80 ×8 (16 GK210 dies, sm_37) | Ubuntu 24.04 | `make USE_CUDA=1` | CUDA 11.4 driver. N2-mini 2-die GPU decode ~13.7 tok/s. | +| Tenstorrent Blackhole p150a | Linux | `make USE_TT=1` | See [`../architecture/accel-foundry.md`](../architecture/accel-foundry.md) §7. | + +--- + +## Not Yet Tested (Should Work) + +These models use supported architectures and quant types but haven't been verified yet. + +**Llama family:** +- Llama-3.2-1B-Instruct (Q4_K_M, Q8_0) +- Llama-3.2-3B-Instruct (Q4_K_M) +- SmolLM2-360M-Instruct (Q8_0) +- TinyLlama-1.1B (Q4_0, Q8_0) + +**Qwen2 family:** +- Qwen2.5-1.5B-Instruct (Q4_K_M, Q8_0) +- Qwen2.5-3B-Instruct (Q4_K_M) +- Qwen2.5-7B-Instruct (Q4_K_M) +- Qwen2.5-Coder-1.5B-Instruct (Q8_0) + +**Mistral family:** +- Mistral-7B-Instruct-v0.2 (Q4_K_M, Q8_0) + +**Phi-3 family:** +- Phi-3-mini-128k-instruct (Q4_K_M, Q8_0) — same arch, longer context +- Phi-3-small-8k-instruct (Q4_K_M) — may have different head config +- Phi-3-medium-4k-instruct (Q4_K_M) — larger model + +**Mixtral family:** +- Mixtral-8x7B-Instruct-v0.1 (Q4_K_M) — full 8 experts, top-2 routing, ~28 GB + +--- + +## Known Issues + +- **Unsupported BPE pre-tokenizers fall back with a one-time warning.** Recognised + `tokenizer.ggml.pre` values are `qwen35`, `qwen2`, `gpt-2`, `default`, `llama-bpe`, `llama3`, + `llama-v3` (or unset). Anything else (e.g. SmolLM2's) uses the qwen35/GPT-2 pre-tokenizer and + prints once: `bpe: tokenizer.ggml.pre is unsupported; falling back to the qwen35/GPT-2 + pre-tokenizer (token IDs may not match the reference for this model)`. Prompt token IDs can then + differ from the reference tokenizer. + +--- + +## Not Yet Tested (RWKV-X) + +No untested RWKV-X models remaining. Both 0.2B and 3.6B verified. + +## Not Yet Supported + +| Architecture | Status | Blocker | +|-------------|--------|---------| +| Phi-3/4 (other variants) | Phase 3.3+ | Tested mini-4k only | diff --git a/evals/corpus/held_out.txt b/evals/corpus/held_out.txt new file mode 100644 index 0000000..a49143e --- /dev/null +++ b/evals/corpus/held_out.txt @@ -0,0 +1,19 @@ +The history of computing is a history of layered abstractions. At the bottom sit transistors, billions of them, each one a switch that knows nothing except on and off. Above them, logic gates combine switches into decisions; above the gates, arithmetic units and registers give those decisions memory and consequence. By the time a programmer types a line of Python, she is standing on perhaps a dozen layers of accumulated invention, each one built by people who mostly never met, each one designed so that the layer above could forget the details of the layer below. This forgetting is not a weakness. It is the entire point. A civilization advances, Alfred North Whitehead observed, by extending the number of important operations it can perform without thinking about them. + +Consider what happens when a modern processor executes a single instruction. The instruction is fetched from a cache that predicts what will be needed before it is asked for, decoded into micro-operations that bear little resemblance to the published instruction set, scheduled out of order across execution ports, and retired in a way that preserves the illusion that everything happened one step at a time. The processor is, in effect, lying to the programmer constantly, and the lie is benevolent: it exists so that software written in 1985 still runs correctly on silicon designed forty years later. Backward compatibility is one of the great unsung engineering achievements of the modern era, and also one of its heaviest anchors. + +Memory hierarchies tell the same story in a different register. A load from the L1 cache costs about four cycles. A load from main memory costs hundreds. A read from a spinning disk, when such things were common, cost the equivalent of millions of cycles: a delay so vast that, scaled to human time, it is the difference between grabbing a pencil from your desk and sailing to another continent to fetch one. Every performance-minded programmer eventually internalizes this pyramid, and much of the craft of optimization is simply arranging for the data you need next to be the data you touched recently. + +The neural networks that now dominate machine learning inherit all of these constraints and add their own. A large language model is, mechanically, an enormous pile of matrix multiplications. Each layer transforms a vector of activations by a matrix of learned weights, applies a nonlinearity, and passes the result upward. The transformer architecture arranges these multiplications around an attention mechanism, which lets every position in a sequence gather information from every other position, weighted by learned relevance. Attention is quadratic in sequence length, which is why context windows were short for years and why so much recent ingenuity has gone into making them longer without paying the full quadratic price. + +Quantization is one of the field's more elegant compromises. The weights of a trained network are stored as floating-point numbers, but most of that precision is wasted: the network's behavior is remarkably tolerant of coarse rounding, provided the rounding is done carefully. Group-wise quantization schemes store a small scale factor for each block of weights, spending a few high-precision bits so that many low-precision ones can follow. A model that needs seventy gigabytes in full precision can often run in under twenty with almost no measurable loss in quality, which is frequently the difference between a model that fits on your hardware and one that does not. + +There is a useful analogy between quantization and the history of audio. Early digital audio engineers feared that sixteen bits per sample would be audibly crude, yet the compact disc proved transparent for practically all listeners because the quantization noise fell below the threshold of hearing. Neural networks exhibit a similar mercy. The signal that matters, the broad statistical structure learned during training, lives in the large-scale geometry of the weight matrices, not in their sixth decimal place. Perturb every weight slightly and the model's outputs barely move; the loss surface around a trained optimum is, in most directions, astonishingly flat. + +Mixture-of-experts architectures push the economics further. Instead of one monolithic feed-forward network in each layer, the model keeps a population of smaller expert networks and a lightweight router that chooses a handful of them for every token. The result is a model whose parameter count is enormous but whose computational cost per token is modest, because only the chosen experts fire. The router's decisions are learned, not programmed, and inspecting them reveals a loose, emergent division of labor: some experts lean toward code, others toward punctuation, others toward particular languages, though the boundaries are blurry and the specialization is statistical rather than clean. + +None of this machinery would matter without software to drive it, and the software has its own quiet elegance. An inference engine must stream tens of gigabytes of weights through a processor's arithmetic units, keeping every lane busy while never stalling on memory. It must maintain a key-value cache so that generating token one thousand does not require reprocessing the previous nine hundred ninety-nine. It must sample from the model's output distribution with controlled randomness: a temperature to sharpen or soften the probabilities, a nucleus cutoff to discard the improbable tail, penalties to discourage the model from repeating itself like a skipping record. + +The economics of all this are shifting fast. A decade ago, running a capable language model required a data center; today, a quantized model of respectable ability runs on a laptop, and hobbyists coax them onto single-board computers and salvaged server hardware for the joy of it. The pattern is familiar to anyone who watched mainframes give way to minicomputers, minicomputers to desktops, desktops to phones. Computing power that begins in institutions ends in pockets. Whatever else the current wave of machine intelligence brings, it is already following the oldest trajectory in the industry: downward in cost, outward in reach, until the extraordinary becomes ambient and nobody thinks about it at all. + +Language itself is the strangest layer in the stack. A model trained only to predict the next token, given mountains of text, ends up encoding grammar, facts, idioms, arithmetic of a halting sort, and something that behaves uncannily like reasoning. Whether that constitutes understanding is a question that philosophy has not settled and engineering has not needed to. The machines work. They translate contracts, draft code, summarize meetings, and tutor students, all by doing nothing more mysterious than continuing a sequence plausibly. The gap between the humility of the mechanism and the breadth of the behavior is the central surprise of the era, and we are still learning what to make of it. diff --git a/evals/eval_quality.sh b/evals/eval_quality.sh new file mode 100755 index 0000000..11f7a19 --- /dev/null +++ b/evals/eval_quality.sh @@ -0,0 +1,241 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# eval_quality.sh — A/B quality gate for two engine builds whose logits are not +# bit-identical (relaxed float-reduction order, Q8_0 hot tensors, ...). +# +# Runs the fixed corpus perplexity and the greedy prompt suite through build A +# (reference) and build B (candidate) via tests/eval_ppl, then prints metrics +# and a single PASS/FAIL verdict. +# +# Usage: +# evals/eval_quality.sh [model.gguf] +# +# Env knobs: +# EVAL_MODEL model path (overrides arg 3) +# EVAL_MODEL_A/B per-side model path (overrides EVAL_MODEL) — for a +# model-file A/B: same binary twice, two GGUFs +# EVAL_PROMPTS prompt suite (default: evals/prompts/quality_suite.txt) +# EVAL_CORPUS ppl corpus (default: evals/corpus/held_out.txt) +# EVAL_MAX_NEW greedy tokens per prompt (default: 48) +# EVAL_PPL_TOKENS cap on corpus tokens, 0 = all (default: 0) +# EVAL_ENV_A/B extra env for each build, e.g. EVAL_ENV_A="QWEN35_GRAPH=0" +# (lets one binary A/B its own runtime variants) +# EVAL_DIE_ARG optional leading barswap die arg (legacy rigs) +# EVAL_OUT artifact dir (default: evals/out/) +# EVAL_SKIP_PPL=1 / EVAL_SKIP_GEN=1 skip a stage (for quick iteration) +# +# GPU runs: export the usual QWEN35_GPU_SPLIT_LAYER / QWEN35_GPU_DIE_A / +# QWEN35_GPU_DIE_B before invoking (shared by both builds), and use +# EVAL_ENV_A/EVAL_ENV_B for anything that must differ between them. +# +# --------------------------------------------------------------------------- +# Verdict thresholds (rationale) +# +# PPL_DELTA_MAX_PCT = 0.5 Perplexity is the core quality number. Reduction +# reordering / KV rounding moves ppl by ~0.01-0.1%; +# a real bug (wrong tensor, broken expert, bad scale) +# moves it percents to orders of magnitude. 0.5% is +# far above harness noise (0 for a fixed corpus — +# runs are deterministic) yet far below any real +# regression. +# +# EARLY_DIV_POS = 8 A benign near-tie flip almost never happens in the +# EARLY_DIV_MAX_FRAC = 0.10 first few forced tokens of a templated answer; +# systematic early divergence (>=10% of prompts +# diverging before token 8) means the candidate's +# logits are shifted, not just reordered. +# +# DIV_MAX_FRAC = 0.60 Once one near-tie token flips, all later tokens +# (warn at 0.30) differ textually, so "any divergence" over-counts; +# still, if >60% of a 60-prompt greedy suite diverges +# within 48 tokens the change is not a rounding-order +# effect. 30-60% prints a WARN: eyeball the TXT lines +# in the artifact dir for semantic equivalence. +# +# PASS = ppl delta < 0.5% AND early-divergence fraction < 10% +# AND diverged fraction <= 60%. +# A bit-exact pair (golden vs golden, GRAPH=0 vs GRAPH=1) must report +# 0 divergence and 0.000% ppl delta — that is the harness self-check. +# --------------------------------------------------------------------------- +set -u + +PPL_DELTA_MAX_PCT=0.5 +EARLY_DIV_POS=8 +EARLY_DIV_MAX_FRAC=0.10 +DIV_MAX_FRAC=0.60 +DIV_WARN_FRAC=0.30 + +usage() { echo "usage: $0 [model.gguf]" >&2; exit 2; } +[ $# -ge 2 ] || usage + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +bin_a="$1"; bin_b="$2" +model="${EVAL_MODEL:-${3:-}}" +# A model-file A/B (one binary, two GGUFs — e.g. a retyped tensor) is the mirror +# image of the build A/B this script was written for: EVAL_MODEL_A/EVAL_MODEL_B +# override the shared model per side, and you pass the same binary twice. +model_a="${EVAL_MODEL_A:-$model}" +model_b="${EVAL_MODEL_B:-$model}" +prompts="${EVAL_PROMPTS:-$repo_root/evals/prompts/quality_suite.txt}" +corpus="${EVAL_CORPUS:-$repo_root/evals/corpus/held_out.txt}" +max_new="${EVAL_MAX_NEW:-48}" +ppl_tokens="${EVAL_PPL_TOKENS:-0}" +out_dir="${EVAL_OUT:-$repo_root/evals/out/$(date +%Y%m%d_%H%M%S)}" + +[ -x "$bin_a" ] || { echo "FAIL: build A not executable: $bin_a" >&2; exit 2; } +[ -x "$bin_b" ] || { echo "FAIL: build B not executable: $bin_b" >&2; exit 2; } +[ -n "$model_a" ] && [ -f "$model_a" ] || { echo "FAIL: model A not found: '$model_a'" >&2; usage; } +[ -n "$model_b" ] && [ -f "$model_b" ] || { echo "FAIL: model B not found: '$model_b'" >&2; usage; } +[ -f "$prompts" ] || { echo "FAIL: prompts not found: $prompts" >&2; exit 2; } +[ -f "$corpus" ] || { echo "FAIL: corpus not found: $corpus" >&2; exit 2; } +mkdir -p "$out_dir" + +echo "== eval_quality ==" +echo "build A : $bin_a ${EVAL_ENV_A:+(env: $EVAL_ENV_A)}" +echo "build B : $bin_b ${EVAL_ENV_B:+(env: $EVAL_ENV_B)}" +echo "model A : $model_a" +echo "model B : $model_b" +echo "out : $out_dir" + +# run_build +run_build() { + side="$1"; mode="$2"; input="$3"; limit="$4"; outfile="$5" + bin="$bin_a"; extra="${EVAL_ENV_A:-}"; mdl="$model_a" + [ "$side" = B ] && { bin="$bin_b"; extra="${EVAL_ENV_B:-}"; mdl="$model_b"; } + # Perplexity needs the FULL logit vector: the GPU LM head defaults to a + # sparse top-k copy (-inf elsewhere), under which the true next token can be + # unrepresented -> -inf log-prob. Force full logits for ppl (the var is + # ignored on the CPU build). Greedy gen is unaffected — argmax survives top-k. + stage_env="$extra" + [ "$mode" = ppl ] && stage_env="$stage_env QWEN35_GPU_FULL_LOGITS=1" + echo "-- [$side] $mode ($(basename "$input"))" + # shellcheck disable=SC2086 + if ! env $stage_env "$bin" ${EVAL_DIE_ARG:-} "$mdl" "$mode" "$input" "$limit" \ + >"$outfile" 2>"$outfile.log"; then + echo "FAIL: build $side $mode run failed — see $outfile.log" >&2 + tail -5 "$outfile.log" >&2 + exit 1 + fi +} + +fail=0 + +# ---- perplexity ------------------------------------------------------------ +ppl_delta_pct="skipped" +if [ "${EVAL_SKIP_PPL:-0}" != 1 ]; then + run_build A ppl "$corpus" "$ppl_tokens" "$out_dir/ppl_a.txt" + run_build B ppl "$corpus" "$ppl_tokens" "$out_dir/ppl_b.txt" + ppl_a=$(sed -n 's/.*ppl=\([0-9.eE+-]*\).*/\1/p' "$out_dir/ppl_a.txt") + ppl_b=$(sed -n 's/.*ppl=\([0-9.eE+-]*\).*/\1/p' "$out_dir/ppl_b.txt") + [ -n "$ppl_a" ] && [ -n "$ppl_b" ] || { echo "FAIL: missing PPL output" >&2; exit 1; } + ppl_delta_pct=$(awk -v a="$ppl_a" -v b="$ppl_b" \ + 'BEGIN { d = (b - a) / a * 100; printf "%.4f", (d < 0 ? -d : d) }') + echo "ppl A=$ppl_a B=$ppl_b |delta|=$ppl_delta_pct%" + awk -v d="$ppl_delta_pct" -v m="$PPL_DELTA_MAX_PCT" 'BEGIN { exit !(d < m) }' \ + || { echo "GATE: ppl delta $ppl_delta_pct% >= $PPL_DELTA_MAX_PCT%"; fail=1; } + + # BATCHED perplexity — the path the serve actually runs. + # + # `ppl` above feeds one token at a time. Serve prefills a whole prompt in + # one batched call, and that is a DIFFERENT kernel path. Scoring only the + # sequential path is how the q4 window-ring hazard survived every gate: + # it cost +7.53% ppl batched (15x this threshold) while sequential ppl was + # bit-identical, so the gate that exists to catch exactly this saw nothing. + # A quality gate that measures a path the product does not use is not a + # quality gate. + run_build A pplb "$corpus" "$ppl_tokens" "$out_dir/pplb_a.txt" + run_build B pplb "$corpus" "$ppl_tokens" "$out_dir/pplb_b.txt" + pplb_a=$(sed -n 's/.*ppl=\([0-9.eE+-]*\).*/\1/p' "$out_dir/pplb_a.txt") + pplb_b=$(sed -n 's/.*ppl=\([0-9.eE+-]*\).*/\1/p' "$out_dir/pplb_b.txt") + [ -n "$pplb_a" ] && [ -n "$pplb_b" ] || { echo "FAIL: missing batched PPL output" >&2; exit 1; } + pplb_delta_pct=$(awk -v a="$pplb_a" -v b="$pplb_b" \ + 'BEGIN { d = (b - a) / a * 100; printf "%.4f", (d < 0 ? -d : d) }') + echo "ppl(batched) A=$pplb_a B=$pplb_b |delta|=$pplb_delta_pct%" + awk -v d="$pplb_delta_pct" -v m="$PPL_DELTA_MAX_PCT" 'BEGIN { exit !(d < m) }' \ + || { echo "GATE: batched ppl delta $pplb_delta_pct% >= $PPL_DELTA_MAX_PCT%"; fail=1; } + + # Sequential-vs-batched spread WITHIN each build. A/B deltas cancel a defect + # present on both sides; this catches one that is present on both. + for side in A B; do + seq_v=$([ "$side" = A ] && echo "$ppl_a" || echo "$ppl_b") + bat_v=$([ "$side" = A ] && echo "$pplb_a" || echo "$pplb_b") + spread=$(awk -v a="$seq_v" -v b="$bat_v" \ + 'BEGIN { d = (b - a) / a * 100; printf "%.4f", (d < 0 ? -d : d) }') + echo "build $side: sequential=$seq_v batched=$bat_v spread=$spread%" + awk -v d="$spread" -v m="${PPL_BATCH_SPREAD_MAX_PCT:-1.0}" 'BEGIN { exit !(d < m) }' \ + || { echo "GATE: build $side batched-vs-sequential spread $spread% >= ${PPL_BATCH_SPREAD_MAX_PCT:-1.0}% — the batched path disagrees with itself"; fail=1; } + done +fi + +# ---- A/B greedy divergence ------------------------------------------------- +if [ "${EVAL_SKIP_GEN:-0}" != 1 ]; then + run_build A gen "$prompts" "$max_new" "$out_dir/gen_a.txt" + run_build B gen "$prompts" "$max_new" "$out_dir/gen_b.txt" + awk -v early_pos="$EARLY_DIV_POS" -v early_max="$EARLY_DIV_MAX_FRAC" \ + -v div_max="$DIV_MAX_FRAC" -v div_warn="$DIV_WARN_FRAC" ' + # Parse "GEN prompt_tokens=: id id ..." from both files, then + # report per-prompt first-diff position + flip counts and gate. + /^GEN / { + idx = $2 + n = 0 + for (f = 4; f <= NF; f++) tok[n++] = $f + if (FILENAME == ARGV[1]) { + a_len[idx] = n + for (i = 0; i < n; i++) a_tok[idx, i] = tok[i] + if (idx + 1 > n_prompts) n_prompts = idx + 1 + } else { + b_len[idx] = n + for (i = 0; i < n; i++) b_tok[idx, i] = tok[i] + } + } + END { + if (n_prompts == 0) { print "GATE: no GEN output parsed"; exit 1 } + diverged = 0; early = 0; flips = 0; compared = 0 + printf "%-8s %6s %6s %10s %6s\n", "prompt", "lenA", "lenB", "first_diff", "flips" + for (p = 0; p < n_prompts; p++) { + la = a_len[p]; lb = b_len[p] + ml = la < lb ? la : lb + fd = -1; fl = 0 + for (i = 0; i < ml; i++) + if (a_tok[p, i] != b_tok[p, i]) { fl++; if (fd < 0) fd = i } + if (fd < 0 && la != lb) fd = ml # one build stopped early + flips += fl; compared += ml + if (fd >= 0) { + diverged++ + if (fd < early_pos) early++ + printf "%-8d %6d %6d %10d %6d\n", p, la, lb, fd, fl + } + } + div_frac = diverged / n_prompts + early_frac = early / n_prompts + flip_pct = compared ? flips * 100.0 / compared : 0 + printf "prompts=%d diverged=%d (%.1f%%) early(<%d)=%d (%.1f%%) token_flips=%.2f%%\n", + n_prompts, diverged, div_frac * 100, early_pos, early, + early_frac * 100, flip_pct + rc = 0 + if (early_frac >= early_max) { + printf "GATE: early divergence %.1f%% >= %.1f%%\n", + early_frac * 100, early_max * 100; rc = 1 + } + if (div_frac > div_max) { + printf "GATE: diverged fraction %.1f%% > %.1f%%\n", + div_frac * 100, div_max * 100; rc = 1 + } else if (div_frac >= div_warn) + printf "WARN: diverged fraction %.1f%% — review TXT lines for semantic equivalence\n", + div_frac * 100 + exit rc + }' "$out_dir/gen_a.txt" "$out_dir/gen_b.txt" | tee "$out_dir/divergence.txt" + [ "${PIPESTATUS[0]}" -eq 0 ] || fail=1 + # Side-by-side text for human review of diverged prompts. + grep '^TXT' "$out_dir/gen_a.txt" >"$out_dir/txt_a.txt" || true + grep '^TXT' "$out_dir/gen_b.txt" >"$out_dir/txt_b.txt" || true +fi + +echo "==================================================" +if [ "$fail" -eq 0 ]; then + echo "VERDICT: PASS (ppl |delta|=$ppl_delta_pct% < $PPL_DELTA_MAX_PCT%, no systematic divergence)" +else + echo "VERDICT: FAIL — see gates above and artifacts in $out_dir" +fi +exit "$fail" diff --git a/evals/flash_spec_gate.sh b/evals/flash_spec_gate.sh new file mode 100755 index 0000000..6c08a7e --- /dev/null +++ b/evals/flash_spec_gate.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# flash_spec_gate.sh — regression gate for the on-die flash attention (prefill) +# path, covering the two cases the original 12-token argmax reference missed. +# Both broke output correctness silently while the 12-token gate stayed green. +# +# Pass 1 — MULTI-TILE PREFILL (no spec). The 12-token reference is smaller +# than one 16-key attention tile, so the cross-tile online-softmax combine +# was never exercised. The greedy suite's prompts are 27-35 tokens (2-3 +# tiles), so QWEN35_FLASH=0 vs =1 on plain greedy decode gates the +# multi-tile prefill: argmax-exact => 0 divergence. +# +# Pass 2 — FLASH UNDER SPECULATIVE DECODE. spec-verify re-enters the BATCHED +# flash path mid-sequence (pos0 > 0), reusing the GPU KV cache. A partial +# accept leaves rejected draft rows in that GPU cache, and interleaved +# single-token host-path decodes update only the host KV mirror; the GPU +# mirror goes stale below the sync watermark. Flash+spec diverged 73% while +# flash-without-spec was clean. spec is designed lossless (GEN output is +# identical with spec on or off), so QWEN35_FLASH=0 vs =1 with +# QWEN35_SPEC=1 must also be 0 divergence. +# +# Pass 3 — BATCHED-PREFILL CAUSALITY. Reference-free: the first M per-position +# argmaxes of an M-token chunked prefill must equal the first M of an N-token +# one (N=384 > PN_KVQ4_WMAX). The q4 window ring once lapped inside a batched +# call, so rows read K/V from LATER rows; +7.53% batched perplexity while every +# A/B stayed green (plans/results/q4_win_ring_hazard.md). One build against +# itself, under the deployed cache config (QWEN35_KV_Q4=1). +# tests/test_qwen35_prefill_causality; red proof tests/causality_red/run_red.sh. +# +# NOTE: do NOT gate the flash path with a bit-exact logit memcmp +# of batched vs sequential prefill. The flash kernel reduces the softmax +# in tile order and drifts ~1e-6 vs the host's single-pass global max — it is +# argmax-exact by design, not bit-exact. The eval-harness divergence measure +# (argmax) is the correct criterion. +# +# Usage: evals/flash_spec_gate.sh [model.gguf] +# env: QWEN35_GPU_SPLIT_LAYER / QWEN35_GPU_DIE_A / QWEN35_GPU_DIE_B +# (defaults 20 / 0 / 1), EVAL_MODEL, EVAL_SKIP_PPL (default 0). +set -u + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +model="${EVAL_MODEL:-${1:-}}" +[ -n "$model" ] && [ -f "$model" ] || { echo "FAIL: model not found: '$model'" >&2; exit 2; } +eval_ppl="$repo_root/tests/eval_ppl" +[ -x "$eval_ppl" ] || { echo "FAIL: build tests/eval_ppl (make USE_CUDA=1 tests/eval_ppl)" >&2; exit 2; } +causality="$repo_root/tests/test_qwen35_prefill_causality" +[ -x "$causality" ] || { echo "FAIL: build tests/test_qwen35_prefill_causality (make USE_CUDA=1 tests/test_qwen35_prefill_causality)" >&2; exit 2; } + +: "${QWEN35_GPU_SPLIT_LAYER:=20}"; export QWEN35_GPU_SPLIT_LAYER +: "${QWEN35_GPU_DIE_A:=0}"; export QWEN35_GPU_DIE_A +: "${QWEN35_GPU_DIE_B:=1}"; export QWEN35_GPU_DIE_B +skip_ppl="${EVAL_SKIP_PPL:-0}" + +rc=0 + +echo "===== Pass 1: multi-tile prefill (FLASH=0 vs =1, plain greedy) =====" +EVAL_SKIP_PPL="$skip_ppl" EVAL_OUT="$repo_root/evals/out/flash_gate_prefill" \ +EVAL_ENV_A="QWEN35_FLASH=0" EVAL_ENV_B="QWEN35_FLASH=1" EVAL_MODEL="$model" \ + "$repo_root/evals/eval_quality.sh" "$eval_ppl" "$eval_ppl" || rc=1 + +echo "===== Pass 2: flash + speculative decode (FLASH=0 vs =1, QWEN35_SPEC=1) =====" +QWEN35_SPEC=1 EVAL_SKIP_PPL="$skip_ppl" EVAL_OUT="$repo_root/evals/out/flash_gate_spec" \ +EVAL_ENV_A="QWEN35_FLASH=0" EVAL_ENV_B="QWEN35_FLASH=1" EVAL_MODEL="$model" \ + "$repo_root/evals/eval_quality.sh" "$eval_ppl" "$eval_ppl" || rc=1 + +echo "===== Pass 3: batched-prefill causality (q4 window ring, N=384 > WMAX) =====" +caus_out="$repo_root/evals/out/causality_gate.log" +mkdir -p "$(dirname "$caus_out")" +# The test SKIPs (exit 0) without a CUDA forward, so require its PASS verdict line. +if QWEN35_KV_Q4="${QWEN35_KV_Q4:-1}" PN_QWEN35_MODEL="$model" \ + "$causality" >"$caus_out" 2>&1 && grep -q "CAUSALITY path=cuda.*PASS" "$caus_out"; then + grep -E "CAUSALITY" "$caus_out" +else + grep -E "CAUSALITY|SKIP|pos=" "$caus_out" + echo "GATE: batched prefill is not causal (or did not run) -- see $caus_out" + rc=1 +fi + +[ "$rc" -eq 0 ] && echo "flash_spec_gate: PASS" || echo "flash_spec_gate: FAIL" +exit $rc diff --git a/evals/prompts/coding_repetitive.txt b/evals/prompts/coding_repetitive.txt new file mode 100644 index 0000000..9db885a --- /dev/null +++ b/evals/prompts/coding_repetitive.txt @@ -0,0 +1,52 @@ +Add a one-line doc comment above each function and reproduce this C code otherwise unchanged: +static int clamp_int(int value, int low, int high) { + if (value < low) return low; + if (value > high) return high; + return value; +} +static float clamp_float(float value, float low, float high) { + if (value < low) return low; + if (value > high) return high; + return value; +} +static double clamp_double(double value, double low, double high) { + if (value < low) return low; + if (value > high) return high; + return value; +} +%% +Rename the variable `buf` to `buffer` everywhere and reproduce this C function otherwise unchanged: +int read_config_file(const char *path, char *buf, size_t buf_len) { + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, buf_len - 1, f); + buf[n] = '\0'; + fclose(f); + if (n == 0) return -1; + return (int)n; +} +%% +Write C getter functions int get_width(const shape_t *s), int get_height(const shape_t *s), int get_depth(const shape_t *s), int get_stride(const shape_t *s), int get_offset(const shape_t *s) for a struct shape_t with fields width, height, depth, stride, offset. Each getter returns the matching field. +%% +Continue this pattern up to case 9, keeping the exact same formatting: +switch (digit) { +case 0: return "zero"; +case 1: return "one"; +case 2: return "two"; +%% +Translate this Python function to JavaScript, keeping the same names and structure: +def sum_even_numbers(numbers): + total = 0 + for number in numbers: + if number % 2 == 0: + total = total + number + return total +%% +Fix the off-by-one bug (the loop should include the last element) and reproduce this C function otherwise unchanged: +float sum_array(const float *values, int count) { + float total = 0.0f; + for (int i = 0; i < count - 1; i++) { + total += values[i]; + } + return total; +} diff --git a/evals/prompts/graph_subset.txt b/evals/prompts/graph_subset.txt new file mode 100644 index 0000000..1a19fa0 --- /dev/null +++ b/evals/prompts/graph_subset.txt @@ -0,0 +1,43 @@ +Write a C function that reverses a null-terminated string in place. +%% +This C code crashes. Find the bug: +char *dup = malloc(strlen(s)); +strcpy(dup, s); +%% +Write a SQL query that returns the three highest-paid employees per department from a table employees(id, name, dept, salary). +%% +Complete this Python function: +def binary_search(arr, target): + lo, hi = 0, len(arr) - 1 +%% +Write a C function that computes the dot product of two float arrays of length n. +%% +Explain the difference between a mutex and a semaphore in two sentences. +%% +Write a C function int popcount(uint32_t v) without using compiler builtins. +%% +If a train travels 60 km in 45 minutes, what is its average speed in km/h? +%% +Alice is taller than Bob. Bob is taller than Carol. Carol is taller than Dave. Who is the second tallest? +%% +If today is Wednesday, what day of the week will it be in 100 days? +%% +Sort these numbers in ascending order and explain your method: 34, 7, 23, 32, 5, 62. +%% +If 5 machines take 5 minutes to make 5 widgets, how long do 100 machines take to make 100 widgets? +%% +A rectangle has perimeter 20 and area 24. What are its side lengths? +%% +Which weighs more: a kilogram of feathers or a kilogram of steel? Explain in one sentence. +%% +What are three practical tips for improving sleep quality? +%% +Draft a short, polite email declining a meeting invitation because of a schedule conflict. +%% +Explain the rules of rock-paper-scissors to someone who has never played. +%% +Translate "Good morning, how are you today?" into French and Spanish. +%% +Write a two-sentence bedtime story about a robot who learns to paint. +%% +Explain inflation in economics using a simple everyday example. \ No newline at end of file diff --git a/evals/prompts/quality_suite.txt b/evals/prompts/quality_suite.txt new file mode 100644 index 0000000..ec5301e --- /dev/null +++ b/evals/prompts/quality_suite.txt @@ -0,0 +1,133 @@ +Write a C function that reverses a null-terminated string in place. +%% +Write a Python function that returns the n-th Fibonacci number iteratively. +%% +Complete this C function: +int clamp(int v, int lo, int hi) { +%% +This C code crashes. Find the bug: +char *dup = malloc(strlen(s)); +strcpy(dup, s); +%% +Write a bash one-liner that counts the number of lines containing "ERROR" in all .log files under /var/log. +%% +Explain what this code does: +x = x ^ y; y = x ^ y; x = x ^ y; +%% +Write a SQL query that returns the three highest-paid employees per department from a table employees(id, name, dept, salary). +%% +Debug this Python: +def add_item(item, items=[]): + items.append(item) + return items +Why does the list grow across calls? +%% +Write a JavaScript function debounce(fn, ms) from scratch. +%% +Complete this Python function: +def binary_search(arr, target): + lo, hi = 0, len(arr) - 1 +%% +You have a tool named get_weather that takes {"city": string}. The user asks: "Do I need an umbrella in Paris today?" Respond with the tool call you would make, as JSON. +%% +You have tools read_file(path) and write_file(path, content). The user says: "Append a copyright line to LICENSE". List the tool calls you would make, in order, as JSON. +%% +Write a C function that computes the dot product of two float arrays of length n. +%% +What is wrong with this loop? +for (unsigned i = 10; i >= 0; i--) printf("%u\n", i); +%% +Write a Python generator that yields all primes below a given limit using a sieve. +%% +Explain the difference between a mutex and a semaphore in two sentences. +%% +Write a git command that shows the diff of the last commit only. +%% +Refactor this to remove the repeated condition: +if (a > 0) { x = 1; } +if (a > 0) { y = 2; } +if (a > 0) { z = 3; } +%% +Write a C function int popcount(uint32_t v) without using compiler builtins. +%% +What does the volatile keyword mean in C and when is it required? +%% +A farmer has 17 sheep. All but 9 run away. How many sheep are left? +%% +If a train travels 60 km in 45 minutes, what is its average speed in km/h? +%% +I have two ropes that each take exactly one hour to burn, but they burn unevenly. How can I measure 45 minutes? +%% +Which is larger: 2^10 or 10^3? Show your reasoning briefly. +%% +Alice is taller than Bob. Bob is taller than Carol. Carol is taller than Dave. Who is the second tallest? +%% +A bat and a ball cost $1.10 together. The bat costs $1.00 more than the ball. How much does the ball cost? +%% +What is 17 * 23? Compute it step by step. +%% +If today is Wednesday, what day of the week will it be in 100 days? +%% +Three friends split a restaurant bill of $75 equally, then add a 20% tip on the total. How much does each person pay? +%% +I am thinking of a number. Double it, add 10, and you get 36. What is my number? +%% +Sort these numbers in ascending order and explain your method: 34, 7, 23, 32, 5, 62. +%% +A snail climbs 3 meters up a wall each day but slides down 2 meters each night. The wall is 10 meters tall. On which day does it reach the top? +%% +How many times do the hour and minute hands of a clock overlap in 12 hours? +%% +If 5 machines take 5 minutes to make 5 widgets, how long do 100 machines take to make 100 widgets? +%% +What is the sum of the interior angles of a hexagon? +%% +Convert 0.625 to a fraction in lowest terms, showing the steps. +%% +A rectangle has perimeter 20 and area 24. What are its side lengths? +%% +Is 91 a prime number? Explain how you checked. +%% +You flip two fair coins. What is the probability of getting at least one head? +%% +Which weighs more: a kilogram of feathers or a kilogram of steel? Explain in one sentence. +%% +Explain photosynthesis to a 10-year-old in three sentences. +%% +Write a haiku about a mountain at dawn. +%% +What are three practical tips for improving sleep quality? +%% +Summarize the plot of Romeo and Juliet in two sentences. +%% +What is the capital of Australia, and why is it not Sydney? +%% +Draft a short, polite email declining a meeting invitation because of a schedule conflict. +%% +What is the difference between weather and climate? +%% +Recommend a simple dinner I can cook in 20 minutes with chicken, rice, and frozen vegetables. +%% +Explain the rules of rock-paper-scissors to someone who has never played. +%% +What causes seasons on Earth? +%% +Give me three creative names for a coffee shop next to a library. +%% +Translate "Good morning, how are you today?" into French and Spanish. +%% +What is a black hole, in plain language? +%% +List the planets of the solar system in order from the Sun. +%% +Write a two-sentence bedtime story about a robot who learns to paint. +%% +Why does bread rise when you bake it? +%% +What should I pack for a weekend hiking trip in cool weather? +%% +Explain inflation in economics using a simple everyday example. +%% +What is the difference between a virus and a bacterium? +%% +Describe the water cycle in four short steps. diff --git a/examples/deploy.sh b/examples/deploy.sh new file mode 100755 index 0000000..04135ed --- /dev/null +++ b/examples/deploy.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# deploy.sh — bring a Pantheon fleet up to a manifest, one model at a time. +# +# WHAT THIS IS +# A template. Copy it, point it at your own manifest, adapt the gates. +# It reconciles a running `pantheon serve` to models.conf by loading each +# missing model through POST /admin/models/load, one at a time, waiting for +# each to settle before starting the next. +# +# WHY ONE AT A TIME +# On a constrained host, loading everything at once is how you OOM. Each +# load is gated on free VRAM (via the engine's own /admin/dies) and host-RAM +# headroom, then settle-waited until the model actually appears in +# /v1/models. That makes bring-up idempotent and restartable: run it again +# after a failure and it picks up where it stopped. +# +# REQUIREMENTS +# bash + curl. Nothing else — no jq, no python, no nvidia-smi. GPU state +# comes from the engine, so this works the same on CPU, Metal and CUDA. +# +# USAGE +# ./deploy.sh reconcile to the manifest (idempotent) +# ./deploy.sh --plan dry run: print what would happen, change nothing +# ./deploy.sh --prune also unload anything running that isn't listed +# ./deploy.sh --file other.conf +# PANTHEON_URL=http://host:8080 ./deploy.sh +# +# EXIT CODES +# 0 all listed models are serving · 1 usage/config error · 2 a load failed +# +set -uo pipefail + +URL="${PANTHEON_URL:-http://127.0.0.1:8080}" +MANIFEST="$(dirname "$0")/models.conf" +PLAN=0 +PRUNE=0 + +# Free host RAM (MB) below which we refuse to start another load. +: "${MIN_HOST_FREE_MB:=1024}" +# Seconds to wait for a model to appear in /v1/models after a load returns. +: "${SETTLE_TIMEOUT:=300}" + +while [ $# -gt 0 ]; do + case "$1" in + --plan) PLAN=1 ;; + --prune) PRUNE=1 ;; + --file) MANIFEST="${2:?--file needs a path}"; shift ;; + --url) URL="${2:?--url needs a URL}"; shift ;; + -h|--help) sed -n '2,32p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + *) echo "unknown argument: $1 (try --help)" >&2; exit 1 ;; + esac + shift +done + +log() { printf '%s %s\n' "$(date +%H:%M:%S)" "$*"; } +die() { printf 'error: %s\n' "$*" >&2; exit 1; } + +[ -r "$MANIFEST" ] || die "manifest not readable: $MANIFEST" + +# --- engine state ----------------------------------------------------------- +# Responses are small and flat, so grep/sed is enough and keeps the dependency +# list at zero. If your manifest grows past a few dozen models, switch to jq. + +api() { curl -fsS --max-time 30 "$@" 2>/dev/null; } + +serve_up() { api "$URL/v1/models" >/dev/null; } + +loaded_models() { + api "$URL/v1/models" | tr ',' '\n' | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' +} + +model_loaded() { loaded_models | grep -qxF "$1"; } + +# Free VRAM in bytes across all devices, from the engine itself. Prints +# nothing if the engine reports no devices (CPU/Metal builds) — callers treat +# an empty answer as "no GPU gate applies". +free_vram_bytes() { + local body budget used + body="$(api "$URL/admin/dies")" || return 0 + printf '%s' "$body" | grep -q '"die"' || return 0 + budget=$(printf '%s' "$body" | tr ',' '\n' | sed -n 's/.*"vram_budget"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | paste -sd+ - | bc 2>/dev/null) + used=$(printf '%s' "$body" | tr ',' '\n' | sed -n 's/.*"vram_used"[[:space:]]*:[[:space:]]*\([0-9]*\).*/\1/p' | paste -sd+ - | bc 2>/dev/null) + [ -n "${budget:-}" ] && [ -n "${used:-}" ] && echo $(( budget - used )) +} + +# Free host RAM in MB. Linux via /proc, macOS via vm_stat. Prints nothing on +# platforms we cannot read, which disables the host gate rather than guessing. +host_free_mb() { + if [ -r /proc/meminfo ]; then + awk '/^MemAvailable:/ {print int($2/1024); exit}' /proc/meminfo + elif command -v vm_stat >/dev/null 2>&1; then + vm_stat | awk ' + /page size of/ { for (i=1;i<=NF;i++) if ($i+0>1024) ps=$i+0 } + /Pages free/ { gsub(/\./,"",$3); f=$3 } + /Pages inactive/ { gsub(/\./,"",$3); f+=$3 } + END { if (ps && f) print int(f*ps/1048576) }' + fi +} + +# --- manifest --------------------------------------------------------------- +# name | path | key=value ... -> JSON body for /admin/models/load + +build_body() { + local name="$1" path="$2" opts="$3" json kv k v + json="{\"name\":\"$name\",\"path\":\"$path\"" + for kv in $opts; do + k="${kv%%=*}"; v="${kv#*=}" + [ "$k" = "$kv" ] && { echo " ! ignoring malformed option '$kv'" >&2; continue; } + case "$k" in + dies) json="$json,\"dies\":[${v}]" ;; + quantize) json="$json,\"quantize\":${v}" ;; + ctx_cap|max_sessions|max_concurrent|reasoning_budget) + json="$json,\"$k\":${v}" ;; + *) echo " ! unknown option '$k' — skipped" >&2 ;; + esac + done + echo "$json}" +} + +settle_wait() { + local name="$1" waited=0 + while [ "$waited" -lt "$SETTLE_TIMEOUT" ]; do + model_loaded "$name" && return 0 + sleep 2; waited=$(( waited + 2 )) + done + return 1 +} + +load_model() { + local name="$1" path="$2" opts="$3" body resp + body="$(build_body "$name" "$path" "$opts")" + + local vram free_mb + vram="$(free_vram_bytes)" + [ -n "$vram" ] && log " gate: $(( vram / 1048576 )) MiB VRAM free" + free_mb="$(host_free_mb)" + if [ -n "$free_mb" ]; then + log " gate: ${free_mb} MiB host RAM free" + if [ "$free_mb" -lt "$MIN_HOST_FREE_MB" ]; then + log " REFUSED: host RAM below ${MIN_HOST_FREE_MB} MiB — stopping here" + return 1 + fi + fi + + resp="$(curl -fsS --max-time 900 -X POST "$URL/admin/models/load" \ + -H 'Content-Type: application/json' -d "$body" 2>&1)" || { + log " LOAD FAILED: ${resp:-no response}" + return 1 + } + settle_wait "$name" || { log " did not appear in /v1/models within ${SETTLE_TIMEOUT}s"; return 1; } + log " ok" +} + +# --- main ------------------------------------------------------------------- + +serve_up || die "no serve responding at $URL — start one with: pantheon serve --model " + +log "manifest: $MANIFEST" +log "target: $URL" +[ "$PLAN" = 1 ] && log "PLAN MODE — nothing will be changed" + +declare -a WANTED=() +rc=0 + +while IFS= read -r line || [ -n "$line" ]; do + line="${line%%#*}" + case "$line" in *[![:space:]]*) ;; *) continue ;; esac + + IFS='|' read -r name path opts <<< "$line" + name="$(echo "${name:-}" | xargs)" + path="$(echo "${path:-}" | xargs)" + opts="$(echo "${opts:-}" | xargs)" + [ -n "$name" ] && [ -n "$path" ] || { echo "skipping malformed line: $line" >&2; continue; } + + WANTED+=("$name") + + if model_loaded "$name"; then + log "= $name already serving" + continue + fi + if [ "$PLAN" = 1 ]; then + log "+ would load $name from $path" + log " body: $(build_body "$name" "$path" "$opts")" + continue + fi + + log "+ loading $name from $path" + load_model "$name" "$path" "$opts" || { rc=2; break; } +done < "$MANIFEST" + +if [ "$PRUNE" = 1 ]; then + while IFS= read -r running; do + [ -n "$running" ] || continue + for w in ${WANTED+"${WANTED[@]}"}; do [ "$w" = "$running" ] && continue 2; done + if [ "$PLAN" = 1 ]; then + log "- would unload $running (not in manifest)" + else + log "- unloading $running (not in manifest)" + api -X POST "$URL/admin/models/unload" \ + -H 'Content-Type: application/json' \ + -d "{\"name\":\"$running\"}" >/dev/null \ + || log " unload failed" + fi + done < <(loaded_models) +fi + +log "serving: $(loaded_models | paste -sd' ' -)" +[ "$rc" -ne 0 ] && log "stopped early — fix the failure above and re-run; loaded models are kept" +exit "$rc" diff --git a/examples/models.conf b/examples/models.conf new file mode 100644 index 0000000..540f63f --- /dev/null +++ b/examples/models.conf @@ -0,0 +1,35 @@ +# models.conf — deploy manifest for examples/deploy.sh +# +# One model per line: +# +# name | /path/to/model.gguf | key=value key=value ... +# +# Lines are loaded top to bottom, one at a time. Put the model you least want +# to lose FIRST — on a constrained host, bring-up stops when memory runs out, +# and everything above the stopping point is already serving. +# +# Blank lines and #-comments are ignored. The options field may be empty. +# +# Recognised options (all optional; they mirror the serve command-line flags +# and POST /admin/models/load fields — see docs/reference/http-api.md): +# +# dies=0,1 pin to GPU device ordinals (CUDA only; relative to +# CUDA_VISIBLE_DEVICES). Omit to let the engine place it. +# ctx_cap=32768 per-session context cap in tokens +# max_sessions=4 how many named sessions may hold KV at once +# max_concurrent=2 how many requests may decode simultaneously +# reasoning_budget=1024 force after N tokens of unclosed reasoning +# quantize=true INT8-quantize at load time +# +# --------------------------------------------------------------------------- + +# A chat model with a bounded window and two decode slots. +chat | /models/chat.gguf | ctx_cap=32768 max_sessions=4 max_concurrent=2 + +# A small model, engine-placed, all defaults. +draft | /models/draft.gguf | + +# Two replicas of the same worker on different GPUs. Same name = replicas, +# load-balanced least-busy. Requires CUDA. +# worker | /models/worker.gguf | dies=2 ctx_cap=8192 +# worker | /models/worker.gguf | dies=3 ctx_cap=8192 diff --git a/foundry b/foundry new file mode 160000 index 0000000..775df8a --- /dev/null +++ b/foundry @@ -0,0 +1 @@ +Subproject commit 775df8ab31a5bde31211090fba35b5ea8d0a4fce diff --git a/include/pantheon.h b/include/pantheon.h new file mode 100644 index 0000000..a542738 --- /dev/null +++ b/include/pantheon.h @@ -0,0 +1,580 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PANTHEON_H +#define PANTHEON_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * Pantheon — Pure C Model Hosting Engine + * + * Public API. All types are opaque — implementation hidden. + * Supports GGUF and safetensors model formats. + * Supports Llama, Qwen, Phi, Mistral, RWKV-X architectures. + * + * Text generation: pn_generate(), pn_stream(), pn_stream_tokens() + * Multi-modal: pn_infer(), pn_transcribe(), pn_synthesize(), + * pn_describe(), pn_detect(), pn_embed() + */ + +/* Opaque types */ +typedef struct pn_model pn_model; +typedef struct pn_state pn_state; + +/* Output types for multi-modal inference */ +typedef enum { + PN_OUT_LOGITS, /* Text token logits: float[n_vocab] */ + PN_OUT_EMBEDDING, /* Embedding vector: float[n_embd] */ + PN_OUT_AUDIO, /* Audio waveform: float[n_samples] */ + PN_OUT_BBOXES, /* Bounding boxes: float[n_boxes * 6] (x1,y1,x2,y2,conf,class) */ + PN_OUT_MASK, /* Segmentation mask: uint8[height * width] */ + PN_OUT_DEPTHMAP, /* Depth map: float[height * width] */ +} pn_output_type; + +/* Tagged output from pn_infer() */ +typedef struct { + pn_output_type type; + float *data; /* Output buffer (caller frees via pn_output_free) */ + int n; /* Number of primary elements (vocab size, n_samples, n_boxes, etc.) */ + int width; /* Image width (vision outputs only, 0 otherwise) */ + int height; /* Image height (vision outputs only, 0 otherwise) */ +} pn_output; + +/* + * Grammar-constrained decoding hook (n2 tool-calling Phase 5) — OPT-IN. + * + * When a sampling call carries a non-NULL `tool_grammar` (below), the sampler + * masks (logit = -INFINITY) every candidate token whose TEXT would take the + * grammar state off a valid tool-call prefix, then advances the grammar by the + * chosen token's text. This forces the model's trained XML tool-call format + * even under a heavy quant. It is entirely device-agnostic (a CPU-side logit + * mutation, like repeat_penalty) and depends only on this callback + an opaque + * grammar state — the engine does not link the grammar module or the tokenizer + * into the sampler. + * + * The hook is fully self-contained via callbacks, so the engine's sampler does + * NOT link the grammar module or the tokenizer: + * - `st` : opaque grammar state (a tg_state*, see tool_grammar.h). + * - `token_text` : id → UTF-8 text piece for a token (borrowed; NULL/empty → + * the token is treated as always-allowed, a no-op). + * - `allowed` : would appending this text keep a valid grammar prefix? + * - `advance` : walk the grammar state forward by the chosen token's text. + * - `user` : passed to token_text (e.g. the tokenizer/model handle). + * - `active` : gates the mask. While the model is still in the free + * reasoning/prose region (no tool-call opener yet) the caller + * leaves this 0 so NOTHING is masked (a legit prose answer is + * never forced into a call); it flips to 1 once a tool-call + * opener has appeared and the grammar has engaged. + * - `n_vocab` : vocab bound for the scan. + * When the whole hook pointer is NULL the sampler is byte-identical to a build + * without this feature. + */ +typedef struct { + void *st; + const char *(*token_text)(void *user, uint32_t id); + int (*allowed)(void *st, const char *text); /* 1 = allowed */ + void (*advance)(void *st, const char *text); + void *user; + int active; + int n_vocab; + /* Optional self-refresh (may be NULL). When set, the sampler calls it at + * entry to recompute `active` from the current grammar state, so the hook + * gates itself WITHOUT the caller refreshing `active` before every step. + * This is what lets the BATCHED driver (which samples per-slot in its own + * thread from the slot's copied sampling params) run the grammar correctly: + * the driver never touches `active`, the hook refreshes it in-thread. The + * single-user path can also rely on it (it still refreshes externally too — + * harmless). Returns the new active value (1 = mask this step). */ + int (*refresh)(void *st); + /* Optional (may be NULL): has the grammar reached a COMPLETE tool call? + * The BATCHED driver queries this AFTER advancing so it can finish the slot + * the instant ""/"" closes — the completion-stop for + * the batched path (mirrors the single-user loop's break-on-complete), so a + * forced call terminates right after the call instead of running to the + * max_tokens budget. Unused (NULL) in the single-user path (its serve loop + * checks completion itself). */ + int (*complete)(void *st); + /* Optional (may be NULL): is a tool-call skeleton currently being emitted + * under the grammar? The BATCHED driver's AUTO reasoning-budget force-close + * queries this so it never injects "" mid-skeleton (which would + * desync the grammar walk) — the batched twin of the single-user + * sg_tg_in_call guard. NULL → treated as "not in a call" (the single-user + * path checks in_call itself and leaves this NULL). */ + int (*in_call)(void *st); + /* Optional (may be NULL): tell the grammar its reasoning close was INJECTED + * by the serve's reasoning-budget force-close, not chosen by the model. The + * grammar cannot tell the two apart on its own — the injected "" is + * fed through `advance` like any other text — so it would compel the tool + * opener from the very next char, while the model is still mid-sentence, and + * the rest of its thought (often a complete call) lands inside a parameter + * VALUE. Called by the BATCHED driver once the injected close has drained; + * the single-user loop calls the grammar directly. NULL → the pre-fix + * behavior (compel immediately). */ + void (*reasoning_end_injected)(void *st); + /* Fail-CLOSED policy (forced-mode hardening). On a 0-survivor step (the mask + * would leave NO legal token) the sampler must NOT fall open to the model's + * own (garbage) top token — that is how empty/garbage calls escaped. When + * `fail_closed` is nonzero the sampler instead forces `forced_token` (an EOS / + * end id supplied by the caller), terminating the generation cleanly rather + * than emitting an out-of-grammar token. EOS is always a safe closed exit + * (the grammar guarantees a completed-or-abortable call, never a deadlock). + * When `fail_closed` is 0 the sampler keeps the legacy fail-open behavior + * (AUTO mode, or callers that did not set this) — so nothing regresses. */ + int fail_closed; /* 1 = force forced_token on 0-survivor */ + uint32_t forced_token; /* token forced on a 0-survivor step (EOS id) */ + /* Optional (may be NULL): may a token with NO text (an end token: EOS, + * <|im_end|>) be drawn while the mask is active? NULL = always, the + * historical rule. A "required" walk answers no before its call opens (the + * turn must make a call) and yes inside it only when the call can still be + * closed — the serve then closes it (tgj_closing_text). */ + int (*end_allowed)(void *st); +} pn_tool_grammar_hook; + +/* One entry of an OpenAI `logit_bias` map: a token id and the bias (nominally + * -100..100) ADDED to that token's raw logit before any other sampling step. */ +typedef struct { + uint32_t token_id; + float bias; +} pn_logit_bias_entry; + +/* + * Per-token logprob sink (OpenAI `logprobs` / `top_logprobs`). + * + * OFF by default (NULL in pn_sampling_params). When set, the sampler computes a + * log-softmax over the RAW logits — before logit_bias, penalties, temperature, + * top-k/top-p and the grammar mask — so the reported numbers are the model's own + * distribution, which is what the OpenAI field means (they are NOT the + * post-filter probabilities the draw was actually taken from). + * + * - `n_top` : how many ALTERNATIVES to report per step (0..PN_LOGPROBS_MAX_TOP). + * - `emit` : called once per sampled token with the chosen token's logprob and + * the top-`n_top` alternatives (descending logprob). The arrays + * are borrowed and valid only for the duration of the call. + * + * INJECTED-TOKEN CONTRACT. A caller that force-decodes a token it did not + * sample (the reasoning-budget "" close) must call `emit` for it with + * `chosen_logprob = NAN` and `n_top = 0`. The sink's record sequence is how a + * consumer pairs logprobs with the emitted token stream, so a token that + * silently produced no record would shift every later token's logprobs onto the + * wrong text — the placeholder keeps the two aligned. + */ +#define PN_LOGPROBS_MAX_TOP 20 + +typedef struct { + int n_top; + void (*emit)(void *user, uint32_t chosen, float chosen_logprob, + const uint32_t *top_ids, const float *top_logprobs, int n_top); + void *user; +} pn_logprob_sink; + +/* Sampling parameters */ +typedef struct { + float temperature; + float top_p; + int top_k; + float repeat_penalty; + int repeat_last_n; + /* OFF by default (NULL). Non-NULL → grammar-constrained tool decoding + * (Phase 5). With it NULL, sample_token is byte-identical to before. */ + const pn_tool_grammar_hook *tool_grammar; + + /* ── OpenAI sampling fields (all default to "off" = 0, so a caller that + * builds params with pn_sampling_defaults() is byte-identical to before) ── */ + + /* OpenAI `presence_penalty` / `frequency_penalty`, applied over the same + * recent-token window `repeat_last_n` feeds the repetition penalty: + * logit[t] -= frequency_penalty * count(t in window) + * logit[t] -= presence_penalty * (count(t in window) > 0) + * Unlike `repeat_penalty` (a SERVER default of 1.1) these are only ever + * non-zero because the CLIENT asked for them, so — also unlike repeat_penalty + * — they are applied on the greedy (temperature<=0) path too. See the + * ordering note in sample_token_core. */ + float presence_penalty; + float frequency_penalty; + + /* OpenAI `logit_bias`: borrowed array of id→bias entries (NULL / 0 = off). + * Added to the RAW logits first, before everything else. */ + const pn_logit_bias_entry *logit_bias; + int n_logit_bias; + + /* OpenAI `seed`: when `rng_seeded` is non-zero the random draw comes from + * `rng_state` (a per-request splitmix64/xorshift stream advanced in place) + * instead of the process-global rand(), so the same request + seed replays + * the same tokens. Zero → the global RNG, byte-identical to before. */ + int rng_seeded; + uint64_t rng_state; + + /* OpenAI `logprobs` / `top_logprobs` (NULL = off). */ + const pn_logprob_sink *logprobs; + + /* A backend that hands the sampler a TOP-N row (-INFINITY outside the N it named) + * gives a refill here: when the row cannot decide this draw exactly (the request's + * top-k could reach past the N after penalties), the sampler calls it for the dense + * row and samples that instead. NULL = rows are taken as given. Returns 0 on success. */ + int (*dense_row)(void *ctx, float *logits, int n_vocab); + void *dense_ctx; + /* Optional, with dense_row: fetch only PART of the dense row — every 16-entry + * chunk i with need[i] != 0 (ids 16*i .. 16*i+15) is written into `logits` at + * its ids, the rest is left as it was. The grammar mask uses it to read just + * the chunks holding allowed tokens instead of the whole row. NULL = whole row + * only. Same ctx as dense_row. Returns 0 on success. */ + int (*dense_chunks)(void *ctx, const unsigned char *need, float *logits, int n_vocab); +} pn_sampling_params; + +/* Generation callback — called for each new token */ +typedef void (*pn_token_callback)(const char *token, void *user_data); + +/* Stop predicate — polled during generation; return non-zero to abort early. */ +typedef int (*pn_stop_callback)(void *user_data); + +/* Return default sampling parameters */ +pn_sampling_params pn_sampling_defaults(void); + +/* Load a model from GGUF or safetensors file */ +pn_model* pn_load(const char *path); + +/* ============================================================ + * Pre-flight admission control (MemoryAware layer, §6) + * ============================================================ */ + +/* Verdict of pn_admit: whether an n_tokens prefill can run as-is, must be + * capped to a smaller window, or must be refused to protect the host. */ +typedef enum { + PN_ADMIT_FIT = 0, /* run it as requested */ + PN_ADMIT_CAPPED = 1, /* run only the first *fit_tokens_out tokens */ + PN_ADMIT_DENIED = 2, /* refuse — host/die would be exhausted */ +} pn_admit_status; + +/* + * Decide whether to admit an n_tokens prefill for `model`, BEFORE any + * memory-growing work starts. Engine-level so both the server (serve_generation) + * and direct pn_model callers (evals / eval_ppl — the path that crash-looped) + * are covered. Computes the projected host + per-die cost, compares against the + * live memory state minus reserved headroom, and returns the verdict. On + * PN_ADMIT_CAPPED, *fit_tokens_out (if non-NULL) receives the largest prefill + * length that fits. Never allocates, never blocks. If the memory-watch layer is + * not running it returns PN_ADMIT_FIT (fail-open — no behavior change). + * + * Thread-safe: yes + * Reentrant: yes + */ +pn_admit_status pn_admit(pn_model *model, int n_tokens, int *fit_tokens_out); + +/* + * The CONFIGURED per-session hard context cap for `model`, in tokens — the KV + * ring / max_seq ceiling clamped to the model's trained context, i.e. what the + * operator asked for (`--ctx-cap`). Returns 0 if unknown. Pure — never + * allocates. + * + * This is the OPERATOR's number. Never hand it to a client: under memory + * pressure admission enforces a smaller one — see pn_effective_ctx_window. + * + * Thread-safe: yes + * Reentrant: yes + */ +int pn_session_ctx_cap(const pn_model *model); + +/* + * The EFFECTIVE per-session context window for `model`, in tokens — the + * configured cap above AFTER the reactive memory ladder's runtime clamp. + * + * The two differ exactly when the ladder has shrunk the admitted prefill window + * under memory pressure, and that difference is the whole reason this call + * exists: advertising the configured cap while admission enforces a smaller one + * gives a client a window the serve will then reject (and produced the + * self-contradictory "prompt 70000 > session cap 131072" message Phase 10 + * fixed). Everything a CLIENT is told — `context_window` on GET /v1/models and + * GET /admin/models, `kv_cap` in a completion's usage object — comes from here. + * + * Returns 0 if unknown. Reads the live memwatch snapshot; never allocates. + * + * Thread-safe: yes + * Reentrant: yes + */ +int pn_effective_ctx_window(const pn_model *model); + +/* Free a loaded model */ +void pn_model_free(pn_model *model); + +/* Keep a loaded model's accelerator resident while it is idle (the serve's idle tick, PM 5). + * Cheap and safe to call any time no generation is running on the model; a no-op for backends + * with nothing to keep alive. Returns >= 0, or -1 when a keepalive failed (the next use then + * re-arms as it always has). */ +int pn_model_keepalive(pn_model *model); +/* Warm an accelerator-resident model once after load (re-arms idle families before the first + * request). Returns 0, or <0 on failure (the model still serves). No-op elsewhere. */ +int pn_model_warm(pn_model *model); + +/* Create inference state for a model */ +pn_state* pn_state_new(pn_model *model); + +/* Free inference state */ +void pn_state_free(pn_state *state); + +/* Reset inference state (clear KV cache, reset position) */ +void pn_state_reset(pn_state *state); + +/* + * Generate text from a prompt. + * + * Returns a newly allocated string (caller must free with free()). + * Returns NULL on error. + */ +char* pn_generate(pn_model *model, pn_state *state, + const char *prompt, int max_tokens, + pn_sampling_params *params); + +/* + * Streaming generation — calls callback for each token. + * + * Returns number of tokens generated, or -1 on error. + */ +int pn_stream(pn_model *model, pn_state *state, + const char *prompt, int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data); + +/* + * Streaming generation from pre-tokenized input. + * Prefills from token array, then generates until EOS or max_tokens. + * Does NOT add BOS — caller controls tokenization entirely. + * Returns number of tokens generated, or -1 on error. + */ +int pn_stream_tokens(pn_model *model, pn_state *state, + const uint32_t *tokens, int n_tokens, + int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data); + +/* --- Multi-modal inference API --- */ + +/* + * Low-level forward pass returning typed output. + * For text models, returns PN_OUT_LOGITS with float[n_vocab]. + * Caller must free result with pn_output_free(). + * Returns NULL on error. + */ +pn_output* pn_infer(pn_model *model, pn_state *state, uint32_t token); + +/* Free a pn_output returned by pn_infer() */ +void pn_output_free(pn_output *out); + +/* + * Speech-to-text: transcribe audio to text. + * Returns newly allocated string (caller frees with free()). + * Returns NULL if model does not support STT. + */ +char* pn_transcribe(pn_model *model, pn_state *state, + const float *audio_samples, int n_samples, + int sample_rate); + +/* + * Text-to-speech: synthesize audio from text. + * Writes audio samples to *out_samples (caller frees with free()). + * Returns number of samples written, or -1 on error. + * Returns -1 if model does not support TTS. + */ +int pn_synthesize(pn_model *model, pn_state *state, + const char *text, float **out_samples, + int *out_sample_rate); + +/* + * Attach a vision encoder (an llama.cpp-style "clip" mmproj GGUF) to a loaded + * text model so pn_describe() can run. Returns 0 on success, negative on error. + */ +int pn_attach_vision(pn_model *model, const char *mmproj_path); + +/* + * Vision-language: describe an image given a text prompt. + * Returns newly allocated string (caller frees with free()). + * Returns NULL if model does not support VLM. + */ +char* pn_describe(pn_model *model, pn_state *state, + const uint8_t *image_rgb, int width, int height, + const char *prompt, int max_tokens, + pn_sampling_params *params); + +/* + * Streaming variant of pn_describe: same result, but as generation proceeds it + * calls callback(full_text_so_far, user_data) after each token with the full + * decoded text up to that point (not just the new token). Pass NULL to behave + * exactly like pn_describe. If should_stop is non-NULL it is polled after each + * generated token; returning non-zero ends generation early and returns the text + * produced so far. Returns the final string (caller frees with free()). + */ +char* pn_describe_stream(pn_model *model, pn_state *state, + const uint8_t *image_rgb, int width, int height, + const char *prompt, int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data, + pn_stop_callback should_stop, void *stop_data); + +/* + * Vision-language decode from PRE-SUPPLIED image embeddings, skipping the + * internal vision encode. Does exactly what pn_describe_stream does after the + * encode: builds the <|vision_start|> + <|image_pad|> * n_img + <|vision_end|> + + * prompt sequence, splices the supplied [n_img x embd_dim] decoder-space embeds + * at the image-pad positions, batched-prefill, and generates. embd_dim must + * equal the model's decoder embedding dimension. img_embds is NOT freed here + * (the caller owns it), so an external (e.g. Core ML GPU) vision encoder can + * supply them. callback / should_stop behave as in pn_describe_stream. Returns + * newly allocated text (caller frees with free()); NULL on error. + */ +char* pn_describe_embeds(pn_model *model, pn_state *state, + const float *img_embds, int n_img, int embd_dim, + const char *prompt, int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data, + pn_stop_callback should_stop, void *stop_data); + +/* + * Run just the vision encoder for one RGB image, returning decoder-space + * embeddings [n_img x n_embd] (caller frees *embeds_out). The vision half of + * pn_describe_stream, split out so a bulk pipeline can stage embeds ahead of the + * batched decode. Returns 0 on success, -1 on error. + */ +int pn_vision_encode_rgb(pn_model *model, const uint8_t *image_rgb, int width, int height, + float **embeds_out, int *n_img_out, int *embd_dim_out); + +/* --- Continuous-batch VLM decode (bulk OCR throughput) --- */ + +/* + * Prefill a state's KV cache from pre-supplied image embeddings (the ChatML + * vision turn) and sample the first generated token, WITHOUT running the decode + * loop. Leaves the state ready to be driven by the batch decoder below (or + * pn_llama_forward_batch). Writes *first_tok_out and returns 0 on success, -1 on + * error. img_embds is NOT freed (caller owns it). + */ +int pn_vlm_prefill_embeds(pn_model *model, pn_state *state, + const float *img_embds, int n_img, int embd_dim, + const char *prompt, pn_sampling_params *params, + uint32_t *first_tok_out); + +/* Opaque continuous-batch decoder. */ +typedef struct pn_batch_decoder pn_batch_decoder; + +/* A finished page returned by the decoder: its id, its now-recyclable state, and + * the decoded UTF-8 transcription (caller frees text with free()). */ +typedef struct { + int page_id; + pn_state *state; + char *text; +} pn_page_out; + +/* + * Create a decoder that batches up to max_batch prefilled sequences through + * pn_llama_forward_batch, handling per-sequence sampling, EOS, and + * detokenization. max_tokens caps each page's output. Returns NULL on error. + */ +pn_batch_decoder* pn_batch_decoder_new(pn_model *model, int max_batch, + int max_tokens, const pn_sampling_params *params); +void pn_batch_decoder_free(pn_batch_decoder *d); + +int pn_batch_decoder_capacity(const pn_batch_decoder *d); /* max_batch */ +int pn_batch_decoder_active(const pn_batch_decoder *d); /* slots in use */ + +/* + * Admit a prefilled sequence into a free slot. state must already be prefilled + * (pn_vlm_prefill_embeds) and first_tok is that call's first_tok_out. Returns 0 + * on success, -1 if the decoder is full. The decoder borrows state until it is + * handed back via a pn_page_out. + */ +int pn_batch_decoder_admit(pn_batch_decoder *d, pn_state *state, int page_id, + uint32_t first_tok); + +/* + * Run ONE batched decode step over all active slots. Pages that finish (EOS or + * max_tokens) this step are written to out[] (up to max_out) with their text and + * their recyclable state; returns the number finished (>=0), or -1 if the model + * does not support batched decode (caller should fall back). Call repeatedly + * while pn_batch_decoder_active() > 0, admitting fresh pages into freed slots. + */ +int pn_batch_decoder_step(pn_batch_decoder *d, pn_page_out *out, int max_out); + +/* + * Finalize ALL active slots with whatever they have decoded so far (early stop / + * drain), returning the number written to out[] (up to max_out). + */ +int pn_batch_decoder_flush(pn_batch_decoder *d, pn_page_out *out, int max_out); + +/* ============================================================ + * Batched lossless speculative decode + * ============================================================ */ + +/* Draft provider: given the accepted prefix generated so far for one sequence + * (prefix[0..n_prefix-1], the model's real tokens), fill draft_out with up to + * max_k proposed next tokens and return how many were produced (0..max_k). + * Returning 0 means "no proposal" (that slot takes a single plain greedy step + * this cycle). This is the seam where an external draft source plugs in. */ +typedef int (*pn_draft_fn)(const uint32_t *prefix, int n_prefix, + uint32_t *draft_out, int max_k, void *user_data); + +/* + * Configure the decoder for batched speculative decode. max_k is the per-slot + * draft width, min_accept_frac / probe_cycles drive the per-page fallback (after + * probe_cycles speculative cycles a slot whose running acceptance fraction is + * below min_accept_frac stops drafting and rides as a plain single-token slot). + * REQUIRES the decoder's sampling temperature <= 0 (greedy): speculation is only + * lossless for greedy decode, so this returns -1 for a sampling temperature. + * Allocates the ragged verify scratch. Returns 0 on success, -1 on error. + */ +int pn_batch_decoder_spec_config(pn_batch_decoder *d, int max_k, + double min_accept_frac, int probe_cycles); + +/* + * Admit a prefilled sequence into a free slot with its own draft provider (as + * pn_batch_decoder_admit, plus draft_fn/draft_ud). Pass draft_fn = NULL to admit + * a plain (non-speculative) slot that still steps correctly under step_spec (it + * rides as a K=1 block). Requires pn_batch_decoder_spec_config first. Returns 0 + * on success, -1 if full or not configured. + */ +int pn_batch_decoder_admit_spec(pn_batch_decoder *d, pn_state *state, int page_id, + uint32_t first_tok, pn_draft_fn draft_fn, void *draft_ud); + +/* + * Run ONE batched speculative decode step. Each active slot proposes up to max_k + * drafts (via its draft_fn), all slots' [pending, drafts...] blocks are verified + * in one pn_llama_verify_batch pass, and each slot greedily accepts the drafts + * that already equal the model's own argmax, emitting the exact greedy + * continuation. The emitted token stream is therefore BIT-IDENTICAL to plain + * batched greedy decode (pn_batch_decoder_step) for ANY draft; drafts only change + * how many tokens each verify pass yields. Finished pages (EOS / max_tokens) are + * written to out[] (up to max_out); returns the number finished (>=0), or -1 if + * the model does not support batched verify (caller falls back to plain step). + */ +int pn_batch_decoder_step_spec(pn_batch_decoder *d, pn_page_out *out, int max_out); + +/* Aggregate speculative-decode accounting since pn_batch_decoder_spec_config: + * verify passes run, draft tokens proposed, and draft tokens accepted (any of + * the out pointers may be NULL). */ +void pn_batch_decoder_spec_stats(const pn_batch_decoder *d, long *passes, + long *drafted, long *accepted); + +/* + * Object detection: find objects in an image. + * Returns bounding boxes as float[n_boxes * 6]: x1, y1, x2, y2, confidence, class_id. + * Caller frees returned array with free(). + * Returns NULL if model does not support detection. + */ +float* pn_detect(pn_model *model, pn_state *state, + const uint8_t *image_rgb, int width, int height, + float confidence_threshold, int *n_boxes_out); + +/* + * Embedding extraction: compute a vector representation. + * Returns float[n_embd] (caller frees with free()). + * Returns NULL if model does not support embeddings. + */ +float* pn_embed(pn_model *model, pn_state *state, + const char *text, int *n_dims_out); + +#ifdef __cplusplus +} +#endif + +#endif /* PANTHEON_H */ diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..216f5f4 --- /dev/null +++ b/install.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Build Pantheon for a Tenstorrent Blackhole p150a and let this user drive the card, +# or (--k80) for a Tesla K80. +# +# ./install.sh TT: build, then set up card access (asks before using sudo) +# ./install.sh --yes same, without asking +# ./install.sh --k80 K80: build pantheon-cuda (no CUDA toolkit needed; the card needs +# the NVIDIA 470 driver). Then ./k80_qwen36.sh or ./k80_nexn2.sh. +# +# Card access needs no vendor kernel module: Foundry's userspace driver maps the card's +# PCI BARs. The udev rule (foundry/tt/tools/60-foundry-blackhole.rules) opens those to the +# `tenstorrent` group and sets two PCI config bits that otherwise need root on every run. +# Then run ./run_ornith.sh. +set -eu +cd "$(dirname "$0")" + +RULES_SRC=foundry/tt/tools/60-foundry-blackhole.rules +RULES_DST=/etc/udev/rules.d/60-foundry-blackhole.rules +YES=0 +K80=0 +for arg in "$@"; do + case "$arg" in + --yes) YES=1 ;; + --k80) K80=1 ;; + *) echo "install: unknown option $arg (use --yes, --k80)" >&2; exit 2 ;; + esac +done + +die() { echo "install: $*" >&2; exit 1; } + +ask() { + [ "$YES" -eq 1 ] && return 0 + printf '%s [y/N] ' "$1" + read -r reply + [ "$reply" = y ] || [ "$reply" = Y ] +} + +check_tools() { + local missing=() tool + [ "$(uname -s)" = Linux ] || die "the Tenstorrent and K80 backends are Linux only" + for tool in cc make git curl sha256sum "$@"; do + command -v "$tool" > /dev/null || missing+=("$tool") + done + [ "${#missing[@]}" -eq 0 ] || + die "missing: ${missing[*]} (Debian/Ubuntu: sudo apt install build-essential git curl pciutils)" +} + +# build_pantheon : e.g. USE_TT=1 tt -> ./pantheon-tt +build_pantheon() { + [ -f foundry/tensor/tensor.h ] || git submodule update --init foundry + echo "install: building pantheon-$2 (a few minutes on a small CPU)" + make "$1" -j"$(nproc)" "pantheon-$2" + [ "$(./pantheon-"$2" --backend)" = "$2" ] || die "built binary is not the $2 backend" +} + +# Warn (don't fail) when the K80 can't run yet: the build itself needs no driver. +check_k80_driver() { + local driver + if ! command -v nvidia-smi > /dev/null || ! driver=$(nvidia-smi --query-gpu=driver_version \ + --format=csv,noheader 2> /dev/null | head -1) || [ -z "$driver" ]; then + echo "install: WARNING: no NVIDIA driver found; a K80 needs the 470 driver to run" + return 0 + fi + echo "install: NVIDIA driver $driver" + case "$driver" in 470.*) ;; *) echo "install: WARNING: Kepler (K80) is supported by the 470 driver branch" ;; esac + nvidia-smi -L | grep -q "K80" || echo "install: WARNING: no Tesla K80 listed by nvidia-smi -L" +} + +# Compare rule lines only, so a reworded comment doesn't trigger sudo again. +card_access_ready() { + [ -f "$RULES_DST" ] && + diff -q <(grep -v '^#' "$RULES_SRC") <(grep -v '^#' "$RULES_DST") > /dev/null && + id -nG | tr ' ' '\n' | grep -qx tenstorrent +} + +setup_card_access() { + if card_access_ready; then + echo "install: card access already set up" + return 0 + fi + echo "install: card access needs sudo, once:" + echo " - create group 'tenstorrent' and add $USER to it" + echo " - install $RULES_DST, reload udev, re-trigger the card" + ask "Do it now?" || { echo "install: skipped; see the header of $RULES_SRC"; return 0; } + sudo groupadd -f tenstorrent + sudo usermod -aG tenstorrent "$USER" + sudo cp "$RULES_SRC" "$RULES_DST" + sudo udevadm control --reload + sudo udevadm trigger --subsystem-match=pci --attr-match=vendor=0x1e52 + echo "install: done. Log out and back in so the 'tenstorrent' group applies." +} + +if [ "$K80" -eq 1 ]; then + check_tools + build_pantheon USE_CUDA=1 cuda + check_k80_driver + echo "install: next, ./k80_qwen36.sh or ./k80_nexn2.sh" +else + check_tools lspci setpci + lspci -d 1e52:b140 | grep -q . || + echo "install: WARNING: no Blackhole p150a (PCI 1e52:b140) found; building anyway" + build_pantheon USE_TT=1 tt + setup_card_access + echo "install: next, ./run_ornith.sh" +fi diff --git a/k80_nexn2.sh b/k80_nexn2.sh new file mode 100755 index 0000000..386218a --- /dev/null +++ b/k80_nexn2.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Run Nex-N2-mini (35B-A3B, Q3_K_M) on one Tesla K80 as an OpenAI-compatible server with +# prefix caching (model id "nex-n2-mini"). Run ./install.sh --k80 first. +# +# ./k80_nexn2.sh serve on 127.0.0.1:8082, dies 0,1 +# HOST=0.0.0.0 PORT=8203 ./k80_nexn2.sh listen on the LAN +# DIES=2,3 ./k80_nexn2.sh use the second K80 card +# CTX_CAP=16384 MAX_SESSIONS=8 ./k80_nexn2.sh +# context window (default 32000) / cached sessions (5) +# NEXN2_DIR=/data/n2 ./k80_nexn2.sh where the model lives +# (default ~/.cache/pantheon/Nex-N2-mini) +# ./k80_nexn2.sh --download-only fetch and check the GGUF, don't serve +# extra args go to `pantheon serve` +# +# First run downloads 16.2 GB (resumable) and checks its sha256. Send `session_id` with each +# request, and send each earlier assistant turn back with its `reasoning_content`, so later +# turns reuse the cached prefix (a 4k-token turn 2: ~38 s cold, 2-5 s cached). Prefill is the +# slow part on a K80. +set -eu +cd "$(dirname "$0")" + +NAME=nex-n2-mini +GGUF=nex-agi_Nex-N2-mini-Q3_K_M.gguf +GGUF_SHA=8b05a0a21746793c0be111b4dce0318a98f6468f315669fb5e13651245f7ee58 +GGUF_GB=16 +MIRROR="${NEXN2_MIRROR:-https://huggingface.co/dcherrera/Nex-N2-mini-Pantheon-K80/resolve/main}" +UPSTREAM="${NEXN2_UPSTREAM:-https://huggingface.co/bartowski/nex-agi_Nex-N2-mini-GGUF/resolve/main}" +DIR="${NEXN2_DIR:-$HOME/.cache/pantheon/Nex-N2-mini}" +CTX_CAP="${CTX_CAP:-32000}" +MAX_SESSIONS="${MAX_SESSIONS:-5}" +REASONING_BUDGET="${REASONING_BUDGET:-2000}" +PORT="${PORT:-8082}" + +. scripts/k80_serve.sh +k80_main "$@" diff --git a/k80_qwen36.sh b/k80_qwen36.sh new file mode 100755 index 0000000..62c69df --- /dev/null +++ b/k80_qwen36.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Run Qwen3.6-35B-A3B (Q3_K_M) on one Tesla K80 as an OpenAI-compatible server with prefix +# caching (model id "qwen36"). Run ./install.sh --k80 first. +# +# ./k80_qwen36.sh serve on 127.0.0.1:8081, dies 0,1 +# HOST=0.0.0.0 PORT=8201 ./k80_qwen36.sh listen on the LAN +# DIES=2,3 ./k80_qwen36.sh use the second K80 card +# CTX_CAP=32768 MAX_SESSIONS=4 ./k80_qwen36.sh +# context window (default 131072) / cached sessions (2) +# QWEN36_DIR=/data/qwen36 ./k80_qwen36.sh where the model lives +# (default ~/.cache/pantheon/Qwen3.6-35B-A3B) +# ./k80_qwen36.sh --download-only fetch and check the GGUF, don't serve +# extra args go to `pantheon serve` +# +# First run downloads 17.1 GB (resumable) and checks its sha256. Send `session_id` with each +# request, and send each earlier assistant turn back with its `reasoning_content`, so later +# turns reuse the cached prefix (a 4k-token turn 2: ~38 s cold, 2-5 s cached). Prefill is the +# slow part on a K80. +set -eu +cd "$(dirname "$0")" + +NAME=qwen36 +GGUF=Qwen_Qwen3.6-35B-A3B-Q3_K_M.gguf +GGUF_SHA=17350b1355d32397cffe7d4b446fd9361dbb01aa26bbf41fc950aa595033794f +GGUF_GB=17 +MIRROR="${QWEN36_MIRROR:-https://huggingface.co/dcherrera/Qwen3.6-35B-A3B-Pantheon-K80/resolve/main}" +UPSTREAM="${QWEN36_UPSTREAM:-https://huggingface.co/bartowski/Qwen_Qwen3.6-35B-A3B-GGUF/resolve/main}" +DIR="${QWEN36_DIR:-$HOME/.cache/pantheon/Qwen3.6-35B-A3B}" +CTX_CAP="${CTX_CAP:-131072}" +MAX_SESSIONS="${MAX_SESSIONS:-2}" +REASONING_BUDGET="${REASONING_BUDGET:-8000}" +PORT="${PORT:-8081}" + +. scripts/k80_serve.sh +k80_main "$@" diff --git a/run_ornith.sh b/run_ornith.sh new file mode 100755 index 0000000..1e18332 --- /dev/null +++ b/run_ornith.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Run Ornith-1.5-9B on a Tenstorrent Blackhole p150a as an OpenAI-compatible server +# (model id "ornith", /v1/chat/completions). Run ./install.sh first. +# +# ./run_ornith.sh serve on 127.0.0.1:8080 +# HOST=0.0.0.0 PORT=8301 ./run_ornith.sh listen on the LAN +# CTX_CAP=32768 ./run_ornith.sh context window (default 131072) +# ./run_ornith.sh --reasoning-budget 2048 extra args go to `pantheon serve` +# ORNITH_DIR=/data/ornith ./run_ornith.sh where the model files live +# (default ~/.cache/pantheon/Ornith-1.5-9B) +# HF_TOKEN=... sent to Hugging Face if set +# ./run_ornith.sh --download-only fetch and check the files, don't serve +# ORNITH_MIRROR= ORNITH_UPSTREAM= other download locations +# +# First run: downloads ~10 GB from Hugging Face (resumable) and checks each file's sha256; +# if our mirror is unreachable, the GGUF comes from ornith-ai and the MTP draft head is built +# from it. Pantheon then converts the GGUF to the card format on the first load and caches +# it beside the GGUF (~10 GB more). Later runs start straight away. +set -eu +cd "$(dirname "$0")" + +MIRROR="${ORNITH_MIRROR:-https://huggingface.co/dcherrera/Ornith-1.5-9B-Pantheon-TT/resolve/main}" +UPSTREAM="${ORNITH_UPSTREAM:-https://huggingface.co/ornith-ai/Ornith-1.5-9B-GGUF/resolve/main}" +DIR="${ORNITH_DIR:-$HOME/.cache/pantheon/Ornith-1.5-9B}" +GGUF=Ornith-1.5-9B-Q8_0.gguf +GGUF_SHA=22086870b009dbe9815ee752c48a82de930118a7c5ce5599590892ae03b8b010 +MTP=Ornith-1.5-9B-Q8_0.bfp8.mtp.fttw +MTP_SHA=d9662d436973b0d28bd5ee09ab692a2050c12aaacdfa3aaf8027ec4173dc16fa + +die() { echo "run_ornith: $*" >&2; exit 1; } + +# The draft head is blk.32 of the GGUF; Pantheon does not build it on load, so build it here. +build_mtp() { + local venv="$DIR/.venv" conv=foundry/tt/convert/tools/build/ft_tt_convert_gguf + echo "run_ornith: building the MTP draft head from the GGUF" + command -v python3 > /dev/null || return 1 + [ -x "$venv/bin/python" ] || python3 -m venv "$venv" || return 1 + "$venv/bin/pip" -q install numpy gguf || return 1 + make -C foundry/tt/convert/tools build/ft_tt_convert_gguf || return 1 + "$venv/bin/python" tools/build_mtp_gguf.py "$DIR/$GGUF" "$DIR/mtp.gguf" || return 1 + "$conv" "$DIR/mtp.gguf" "$DIR/$MTP" --ledger bfp8 || return 1 + rm -f "$DIR/mtp.gguf" + touch "$DIR/$MTP.ok" +} + +DOWNLOAD_ONLY=0 +[ "${1:-}" = "--download-only" ] && { DOWNLOAD_ONLY=1; shift; } +[ "$DOWNLOAD_ONLY" -eq 1 ] || [ -x ./pantheon-tt ] || die "no ./pantheon-tt; run ./install.sh first" +. scripts/model_fetch.sh +mkdir -p "$DIR" +# Disk: ~10 GB for the GGUF if not here yet, ~10 GB for the card conversion unless +# --download-only or already converted, ~1 GB for the draft head and slack. +need_gb=1 +[ -f "$DIR/$GGUF" ] || need_gb=$((need_gb + 10)) +[ "$DOWNLOAD_ONLY" -eq 1 ] || [ -f "$DIR/Ornith-1.5-9B-Q8_0.bfp8.fttw" ] || need_gb=$((need_gb + 10)) +[ "$need_gb" -gt 1 ] && mf_need_disk "$need_gb" +mf_fetch "$GGUF" "$GGUF_SHA" "$MIRROR" "$UPSTREAM" || die "could not download $GGUF" +mf_fetch "$MTP" "$MTP_SHA" "$MIRROR" || build_mtp || + echo "run_ornith: WARNING: no MTP draft head; running without speculative decoding" +[ "$DOWNLOAD_ONLY" -eq 1 ] && { echo "run_ornith: files ready in $DIR"; exit 0; } + +MTP_ENV=() +mf_verified "$MTP" && MTP_ENV=(PN_TT_MTP="$DIR/$MTP") +echo "run_ornith: starting on ${HOST:-127.0.0.1}:${PORT:-8080} (first start converts for the card)" +exec env FT_TT_DRIVER="${FT_TT_DRIVER:-udrv}" ${MTP_ENV[@]+"${MTP_ENV[@]}"} ./pantheon-tt serve \ + --model "$DIR/$GGUF" --served-name ornith --ctx-cap "${CTX_CAP:-131072}" \ + --host "${HOST:-127.0.0.1}" --port "${PORT:-8080}" "$@" diff --git a/scripts/add-file-headers.sh b/scripts/add-file-headers.sh new file mode 100755 index 0000000..ac9a4d1 --- /dev/null +++ b/scripts/add-file-headers.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Insert the standard file-header block into sources that lack one. +# +# ./scripts/add-file-headers.sh # dry run — report only +# ./scripts/add-file-headers.sh --apply # write the blocks +# ./scripts/add-file-headers.sh --apply src/core # limit to a subtree +# +# Adds the SPDX pair when missing, then: +# +# /** +# * @file name.c +# * @brief TODO +# * +# * @status None +# * @issues None +# * @todo None +# */ +# +# @brief is seeded from the paired header's @brief when there is one, otherwise +# left as TODO — a one-line summary is not mechanically derivable, and a wrong +# one is worse than an absent one. scripts/check-headers.sh rejects TODO briefs +# on files a PR adds. +# +# Exempt: tests, generated sources, third-party-derived files (those keep their +# upstream attribution). +set -euo pipefail + +APPLY=0 +[ "${1-}" = "--apply" ] && { APPLY=1; shift; } +SCOPE="${1-.}" + +SPDX='/* SPDX-License-Identifier: AGPL-3.0-or-later */' +COPYRIGHT='/* Copyright (C) 2026 David (Corey) Herrera */' + +is_exempt() { + case "$1" in + tests/*|*/tests/*) return 0 ;; # test files + *_ptx.h|*ptx_modules*) return 0 ;; # generated + *) ;; + esac + # third-party attribution present + head -5 "$1" | grep -qE 'SPDX-FileCopyrightText|Apache-2\.0' && return 0 + return 1 +} + +added=0 todo=0 seeded=0 skipped=0 + +while IFS= read -r f; do + head -12 "$f" | grep -q '@file' && continue + if is_exempt "$f"; then skipped=$((skipped+1)); continue; fi + + base=$(basename "$f") + brief="TODO" + pair="${f%.*}.h" + if [ "$pair" != "$f" ] && [ -f "$pair" ]; then + p=$(grep -m1 '@brief' "$pair" 2>/dev/null | sed 's/.*@brief[[:space:]]*//' || true) + [ -n "$p" ] && brief="Implementation of ${p%.}." + fi + [ "$brief" = "TODO" ] && todo=$((todo+1)) || seeded=$((seeded+1)) + added=$((added+1)) + + [ "$APPLY" = "1" ] || continue + + tmp=$(mktemp) + if head -1 "$f" | grep -q 'SPDX-License-Identifier'; then + # SPDX already present — insert the block after the copyright line + head -2 "$f" > "$tmp" + rest=3 + else + printf '%s\n%s\n' "$SPDX" "$COPYRIGHT" > "$tmp" + rest=1 + fi + { + printf '/**\n' + printf ' * @file %s\n' "$base" + printf ' * @brief %s\n' "$brief" + printf ' *\n' + printf ' * @status None\n' + printf ' * @issues None\n' + printf ' * @todo None\n' + printf ' */\n' + } >> "$tmp" + tail -n "+$rest" "$f" >> "$tmp" + mv "$tmp" "$f" +done < <(git ls-files "$SCOPE" | grep -E '\.(c|h|cu|cuh|m)$') + +printf 'headers %s: %d file(s) — %d brief seeded from paired header, %d left TODO; %d exempt\n' \ + "$([ "$APPLY" = 1 ] && echo written || echo needed)" "$added" "$seeded" "$todo" "$skipped" + +# --- SPDX pass ------------------------------------------------------------- +# Independent of the header block: a file can carry @file and still have no SPDX +# line. Only files that are unambiguously ours get stamped. Anything whose header +# cites upstream sources is listed for manual triage instead — a port of an +# Apache-2.0 source needs Apache-2.0 plus the upstream copyright, and that is a +# per-file judgement, not a sed. +spdx_added=0 triage=0 +while IFS= read -r f; do + head -3 "$f" | grep -q 'SPDX-License-Identifier' && continue + case "$f" in tests/*|*/tests/*|*_ptx.h) continue ;; esac + if head -20 "$f" | grep -qiE 'tt-metal|tt-kmd|Tenstorrent|transliteration|rendering of|upstream'; then + triage=$((triage+1)) + [ "$APPLY" = 1 ] || printf ' triage: %s\n' "$f" + continue + fi + spdx_added=$((spdx_added+1)) + [ "$APPLY" = 1 ] || continue + tmp=$(mktemp) + printf '%s\n%s\n' "$SPDX" "$COPYRIGHT" > "$tmp" + cat "$f" >> "$tmp" + mv "$tmp" "$f" +done < <(git ls-files "$SCOPE" | grep -E '\.(c|h|cu|cuh|m)$') + +printf 'SPDX %s: %d file(s); %d need manual triage (upstream-derived)\n' \ + "$([ "$APPLY" = 1 ] && echo written || echo needed)" "$spdx_added" "$triage" +[ "$APPLY" = 1 ] || printf 'dry run — pass --apply to write\n' diff --git a/scripts/assert_backend.sh b/scripts/assert_backend.sh new file mode 100755 index 0000000..356a7c0 --- /dev/null +++ b/scripts/assert_backend.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Refuse to deploy a Pantheon binary whose COMPILED backend is not the one the +# fleet expects. One line to call, exit 0 or exit 1. +# +# scripts/assert_backend.sh +# scripts/assert_backend.sh /opt/pantheon/pantheon cuda +# +# Why this exists: `make`, `make USE_CUDA=1` and `make USE_TT=1` used to write +# the same file, so whichever ran last owned the deployed path. A CPU-only +# binary served a 35B-A3B MoE on a 2-core no-AVX Pentium with all 8 K80 dies +# idle at 0 MiB for an unknown period, and a deploy script launched it onto a GPU +# fleet manifest and reported success. The binary knew its own backend the whole +# time — nothing asked it. +# +# The binaries are now per-backend (pantheon-cuda / pantheon-tt / pantheon-cpu) +# and `pantheon` is a symlink, so this checks what the symlink RESOLVES TO by +# asking the binary itself rather than trusting the name. `--backend` prints one +# lowercase token (cuda|tt|metal|cpu) on stdout and exits 0; it loads no model +# and opens no device, so it is free to run on every deploy. +# +# A binary too old to know the flag fails CLOSED: it is by definition from +# before per-backend names existed, which is exactly the era this guards. + +set -eu + +BIN="${1:-}" +WANT="${2:-}" + +if [ -z "$BIN" ] || [ -z "$WANT" ]; then + echo "usage: $0 e.g. $0 ./pantheon cuda" >&2 + exit 2 +fi + +if [ ! -x "$BIN" ]; then + echo "assert_backend: '$BIN' is not an executable" >&2 + exit 1 +fi + +GOT=$("$BIN" --backend 2>/dev/null) || GOT="" + +if [ -z "$GOT" ]; then + echo "assert_backend: '$BIN' does not support --backend — it predates" >&2 + echo " per-backend build names and cannot be trusted to be a $WANT build." >&2 + echo " Rebuild: make USE_CUDA=1 (produces ./pantheon-cuda)" >&2 + exit 1 +fi + +if [ "$GOT" != "$WANT" ]; then + echo "assert_backend: REFUSING to deploy '$BIN' — it is a '$GOT' build, the" >&2 + echo " fleet expects '$WANT'. A '$GOT' binary would load the models and" >&2 + echo " answer every request on the host CPU with every die idle." >&2 + echo " Fix: point the deploy path at the $WANT binary —" >&2 + echo " ln -sfn pantheon-$WANT pantheon (no rebuild needed if it exists)" >&2 + if [ "$WANT" = cpu ]; then + echo " or build one: make (a plain make IS the CPU build)" >&2 + else + # The flag is the token uppercased: cuda -> USE_CUDA=1, tt -> USE_TT=1. + echo " or build one: make USE_$(printf '%s' "$WANT" | tr 'a-z' 'A-Z')=1" >&2 + fi + exit 1 +fi + +echo "assert_backend: '$BIN' is a '$GOT' build ($("$BIN" --version | tail -1))" +exit 0 diff --git a/scripts/check-cla.sh b/scripts/check-cla.sh new file mode 100755 index 0000000..1add0c0 --- /dev/null +++ b/scripts/check-cla.sh @@ -0,0 +1,68 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Verify every commit author in a range appears on the CLA signature roster. +# See CLA.md (individuals) and CLA-CORPORATE.md (organizations). +# +# scripts/check-cla.sh [range] # default: origin/main..HEAD +# +# Run it locally before opening a pull request; CI runs the same script. + +set -eu + +cd "$(git rev-parse --show-toplevel)" + +RANGE="${1:-${BASE_REF:-origin/main}..HEAD}" +ROSTER=".cla/signatures.md" + +if [ ! -f "$ROSTER" ]; then + echo "check-cla: $ROSTER is missing" >&2 + exit 1 +fi + +# Signed addresses: every `git-email:` value that is not inside a comment. +signed=$(grep -v '^[[:space:]]*#' "$ROSTER" | + sed -n 's/^[[:space:]]*-\{0,1\}[[:space:]]*git-email:[[:space:]]*//p' | + tr -d '\r' | tr '[:upper:]' '[:lower:]' | sort -u) + +authors=$(git log --no-merges --format='%ae' "$RANGE" | + tr '[:upper:]' '[:lower:]' | sort -u) + +if [ -z "$authors" ]; then + echo "check-cla: no commits in $RANGE — nothing to check" + exit 0 +fi + +fail=0 +for email in $authors; do + if printf '%s\n' "$signed" | grep -qxF "$email"; then + echo "ok $email" + else + echo "FAIL $email not on the CLA roster" + fail=1 + fi +done + +if [ "$fail" -ne 0 ]; then + cat <<'EOF' + +Contributions are accepted under the Contributor License Agreement in CLA.md. +Sign it by adding your entry to .cla/signatures.md in this pull request: + + - name: Your Full Name + git-email: you@example.com + forge-username: your-username + date: YYYY-MM-DD + agreement: ICLA v1.0 + statement: I have read the Pantheon ICLA and I agree to it. + +Use the same address you commit with. If you commit under more than one +address, list each on its own git-email: line. Organizations contributing on +behalf of employees sign CLA-CORPORATE.md by email instead. + +EOF + exit 1 +fi + +echo "check-cla: all authors have signed" diff --git a/scripts/check-dco.sh b/scripts/check-dco.sh new file mode 100755 index 0000000..95187b4 --- /dev/null +++ b/scripts/check-dco.sh @@ -0,0 +1,66 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Verify every commit in a range carries a Developer Certificate of Origin +# sign-off matching its author. See CONTRIBUTING.md. +# +# scripts/check-dco.sh [range] # default: origin/main..HEAD +# +# Run it locally before opening a pull request; CI runs the same script. + +set -eu + +cd "$(git rev-parse --show-toplevel)" + +RANGE="${1:-${BASE_REF:-origin/main}..HEAD}" + +commits=$(git rev-list --no-merges "$RANGE") +if [ -z "$commits" ]; then + echo "check-dco: no commits in $RANGE — nothing to check" + exit 0 +fi + +fail=0 +for sha in $commits; do + author_email=$(git show -s --format='%ae' "$sha") + author_name=$(git show -s --format='%an' "$sha") + subject=$(git show -s --format='%s' "$sha") + + signoffs=$(git show -s --format='%B' "$sha" | + sed -n 's/^[Ss]igned-off-by:[[:space:]]*//p' | + tr '[:upper:]' '[:lower:]') + + if [ -z "$signoffs" ]; then + echo "FAIL $sha $subject" + echo " no Signed-off-by line" + fail=1 + continue + fi + + want=$(printf '%s' "$author_email" | tr '[:upper:]' '[:lower:]') + if printf '%s\n' "$signoffs" | grep -qF "<$want>"; then + echo "ok $sha $author_name <$author_email>" + else + echo "FAIL $sha $subject" + echo " sign-off does not match the author <$author_email>" + printf ' found: %s\n' "$signoffs" + fail=1 + fi +done + +if [ "$fail" -ne 0 ]; then + cat <<'EOF' + +Every commit needs a Developer Certificate of Origin sign-off matching its +author -- see https://developercertificate.org/ and CONTRIBUTING.md. + + git commit -s -m "..." new commits + git commit --amend -s --no-edit the most recent commit + git rebase --signoff a whole branch + +EOF + exit 1 +fi + +echo "check-dco: all commits signed off" diff --git a/scripts/check-headers.sh b/scripts/check-headers.sh new file mode 100755 index 0000000..7604704 --- /dev/null +++ b/scripts/check-headers.sh @@ -0,0 +1,98 @@ +#!/bin/sh +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Verify every source file ADDED in a range carries the SPDX pair and the file +# header block. See docs/coding-best-practices.md. +# +# scripts/check-headers.sh [range] # default: origin/main..HEAD +# +# Only files the range ADDS are checked. Files that already exist are left +# alone deliberately: the rule is that everyone submitting code stays in +# compliance, not that the whole tree gets retrofitted. +# +# Exempt: tests, generated sources, and files carrying upstream third-party +# attribution (those keep their own header). +# +# Run it locally before opening a pull request; CI runs the same script. + +set -eu + +cd "$(git rev-parse --show-toplevel)" + +RANGE="${1:-${BASE_REF:-origin/main}..HEAD}" + +files=$(git diff --diff-filter=A --name-only "$RANGE" -- \ + '*.c' '*.h' '*.cu' '*.cuh' '*.m') + +if [ -z "$files" ]; then + echo "check-headers: ok (no source files added)" + exit 0 +fi + +fail=0 + +note() { + echo "check-headers: $1" >&2 + echo " $2" >&2 + fail=$((fail + 1)) +} + +OLDIFS=$IFS +IFS=' +' +for f in $files; do + IFS=$OLDIFS + + # Added and then removed again within the range. + [ -f "$f" ] || continue + + case "$f" in + tests/*|*/tests/*) continue ;; + *_ptx.h|*_ptx.cuh) continue ;; + esac + + # Third-party-derived files keep their upstream attribution. + if head -5 "$f" | grep -qE 'SPDX-FileCopyrightText|Apache-2\.0'; then + continue + fi + + if ! head -3 "$f" | grep -q 'SPDX-License-Identifier'; then + note "$f: missing the SPDX identifier" \ + "add: /* SPDX-License-Identifier: AGPL-3.0-or-later */" + continue + fi + + if ! head -16 "$f" | grep -q '@file'; then + note "$f: missing the @file header block" \ + "run: ./scripts/add-file-headers.sh --apply $f" + continue + fi + + brief=$(head -16 "$f" | sed -n 's|.*@brief[[:space:]]*||p' | head -1) + case "$brief" in + '') + note "$f: @brief is empty" \ + "one line saying what this file is for" + ;; + TODO*|todo*) + note "$f: @brief is still TODO" \ + "replace it with one line saying what this file is for" + ;; + esac + + for tag in @status @issues @todo; do + if ! head -16 "$f" | grep -q -- "$tag"; then + note "$f: header block is missing $tag" \ + "all three of @status/@issues/@todo are present, 'None' when empty" + fi + done +done +IFS=$OLDIFS + +if [ "$fail" -gt 0 ]; then + echo "check-headers: $fail problem(s) — see docs/coding-best-practices.md" >&2 + exit 1 +fi + +echo "check-headers: ok" diff --git a/scripts/k80_serve.sh b/scripts/k80_serve.sh new file mode 100644 index 0000000..225bee3 --- /dev/null +++ b/scripts/k80_serve.sh @@ -0,0 +1,43 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Shared body of the Tesla K80 run scripts (k80_qwen36.sh, k80_nexn2.sh). Sourced, not run. +# The caller sets NAME, GGUF, GGUF_SHA, GGUF_GB, MIRROR, UPSTREAM, DIR, CTX_CAP, MAX_SESSIONS, +# REASONING_BUDGET and PORT, then calls `k80_main "$@"`. +# +# One model takes both dies of one K80 (DIES, default 0,1). Prefix caching is on: the serve +# keeps MAX_SESSIONS session slots, and a client that sends `session_id` (or X-Session-Id) and +# replays each assistant turn's `reasoning_content` only prefills the new tokens on later turns. + +die() { echo "$NAME: $*" >&2; exit 1; } + +# The flags the K80 fleet runs with (measured settings, not defaults). +k80_env() { + K80_ENV=(PN_THREADS=2 PN_DIE_RESERVE_BYTES=512M PANTHEON_TOOL_GRAMMAR=1 + QWEN35_KV_Q4=1 QWEN35_BATCH_SLOTS=1 QWEN35_VOCAB_SPLIT=1 + QWEN35_VOCAB_SPLIT_FULL_LOGITS=1 QWEN35_PREFILL_PIPELINE=1 QWEN35_MOE_DEDUP=1 + QWEN35_SSM_WARP=1 PN_SPARSE_SAMPLER=1 PN_GRAMMAR_ARM_WINDOW=1 + FC_MOE_DEDUP_SHX=1 FC_MOE_DEDUP_COMPACT=1 FC_ATTN_KTILE_REG=1 FC_ATTN_VSTAGE=1) +} + +k80_main() { + local download_only=0 + [ "${1:-}" = "--download-only" ] && { download_only=1; shift; } + if [ "$download_only" -eq 0 ]; then + [ -x ./pantheon-cuda ] || die "no ./pantheon-cuda; run ./install.sh --k80 first" + command -v nvidia-smi > /dev/null && nvidia-smi -L > /dev/null 2>&1 || + die "no NVIDIA driver/GPU visible (nvidia-smi -L); a K80 needs the 470 driver" + fi + . scripts/model_fetch.sh + mkdir -p "$DIR" + [ -f "$DIR/$GGUF" ] || mf_need_disk "$((GGUF_GB + 1))" + mf_fetch "$GGUF" "$GGUF_SHA" "$MIRROR" "$UPSTREAM" || die "could not download $GGUF" + [ "$download_only" -eq 1 ] && { echo "$NAME: files ready in $DIR"; exit 0; } + + k80_env + echo "$NAME: starting on ${HOST:-127.0.0.1}:$PORT, dies ${DIES:-0,1} (loading takes a few minutes)" + exec env "${K80_ENV[@]}" ./pantheon-cuda serve \ + --model "$DIR/$GGUF" --name "$NAME" --dies "${DIES:-0,1}" \ + --ctx-cap "$CTX_CAP" --max-sessions "$MAX_SESSIONS" --reasoning-budget "$REASONING_BUDGET" \ + --host "${HOST:-127.0.0.1}" --port "$PORT" "$@" +} diff --git a/scripts/model_fetch.sh b/scripts/model_fetch.sh new file mode 100644 index 0000000..b94e861 --- /dev/null +++ b/scripts/model_fetch.sh @@ -0,0 +1,52 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# Download-and-check helpers for the model run scripts (run_ornith.sh, k80_*.sh). Sourced, +# not run. The caller sets DIR (where model files live) and defines die(). +# +# A file is good once its sha256 matched (or it was built locally); a `.ok` marker +# records that so later runs don't re-hash many GB. Downloads resume into `.part` and +# move into place only after the hash matches. HF_TOKEN, if set, is sent to Hugging Face. + +# mf_verified : already checked. +mf_verified() { + [ -f "$DIR/$1" ] && [ -f "$DIR/$1.ok" ] +} + +# mf_verify : hash it; keep and mark it only if it matches. +mf_verify() { + echo "fetch: checking $1" + [ "$(sha256sum "$DIR/$1" | cut -d' ' -f1)" = "$2" ] || { rm -f "$DIR/$1"; return 1; } + touch "$DIR/$1.ok" +} + +# mf_download +mf_download() { + local auth=() + echo "fetch: downloading $2" + [ -n "${HF_TOKEN:-}" ] && auth=(-H "Authorization: Bearer $HF_TOKEN") + curl -fL --retry 5 -C - ${auth[@]+"${auth[@]}"} -o "$DIR/$2.part" "$1/$2" || return 1 + mv "$DIR/$2.part" "$DIR/$2" + mf_verify "$2" "$3" +} + +# mf_fetch [upstream-url]: make present and checked. +# An upstream URL is tried when the mirror fails. Returns 1 if every source failed. +mf_fetch() { + mf_verified "$1" && return 0 + [ -f "$DIR/$1" ] && mf_verify "$1" "$2" && return 0 + mf_download "$3" "$1" "$2" && return 0 + rm -f "$DIR/$1.part" + [ -n "${4:-}" ] || return 1 + echo "fetch: mirror failed, trying $4" + mf_download "$4" "$1" "$2" && return 0 + rm -f "$DIR/$1.part" + return 1 +} + +# mf_need_disk : die unless DIR has free. +mf_need_disk() { + local free_gb + free_gb=$(df -Pk "$DIR" | awk 'NR == 2 { print int($4 / 1048576) }') + [ "$free_gb" -ge "$1" ] || die "$DIR has ${free_gb} GB free; this needs about $1 GB (set the model dir)" +} diff --git a/src/accel/backend_identity.c b/src/accel/backend_identity.c new file mode 100644 index 0000000..ad032f5 --- /dev/null +++ b/src/accel/backend_identity.c @@ -0,0 +1,95 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file backend_identity.c + * @brief Implementation of the compile-time backend identity (see + * backend_identity.h). + * + * Everything here is decided by the preprocessor and returned as a string + * literal. That is deliberate: the whole value of this module is that it CANNOT + * disagree with the code that was compiled. A runtime probe ("is there a CUDA + * driver?") answers a different question and is exactly the one that was + * already misleading — the incident binary had no CUDA compiled in at all, so + * no probe would have run and no probe would have complained. + * + * The combination arms (CUDA+TT etc.) exist because the Makefile permits + * `make USE_CUDA=1 USE_TT=1`. They are enumerated rather than assembled at + * runtime so every value is a literal and the accessors stay signal-safe. + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "backend_identity.h" + +/* The Makefile stamps the build commit in. A build with no git (a tarball, a + * copied tree) still compiles — it just reports "unknown" rather than lying. */ +#ifndef PN_BUILD_COMMIT +#define PN_BUILD_COMMIT "unknown" +#endif + +/* One enumeration, two spellings. Written out per combination instead of + * concatenated at runtime so both accessors return literals. */ +#if defined(USE_CUDA) +# if defined(USE_TT) +# define PN_BACKEND_NAME_STR "CUDA+TT" +# define PN_BACKEND_TOKEN_STR "cuda+tt" +# elif defined(USE_METAL) +# define PN_BACKEND_NAME_STR "CUDA+METAL" +# define PN_BACKEND_TOKEN_STR "cuda+metal" +# else +# define PN_BACKEND_NAME_STR "CUDA" +# define PN_BACKEND_TOKEN_STR "cuda" +# endif +#elif defined(USE_TT) +# if defined(USE_METAL) +# define PN_BACKEND_NAME_STR "TT+METAL" +# define PN_BACKEND_TOKEN_STR "tt+metal" +# else +# define PN_BACKEND_NAME_STR "TT" +# define PN_BACKEND_TOKEN_STR "tt" +# endif +#elif defined(USE_METAL) +# define PN_BACKEND_NAME_STR "METAL" +# define PN_BACKEND_TOKEN_STR "metal" +#else +# define PN_BACKEND_NAME_STR "CPU" +# define PN_BACKEND_TOKEN_STR "cpu" +#endif + +const char *pn_backend_name(void) +{ + return PN_BACKEND_NAME_STR; +} + +const char *pn_backend_token(void) +{ + return PN_BACKEND_TOKEN_STR; +} + +const char *pn_backend_build_commit(void) +{ + return PN_BUILD_COMMIT; +} + +bool pn_backend_has_accelerator(void) +{ +#if defined(USE_CUDA) || defined(USE_TT) || defined(USE_METAL) + return true; +#else + return false; +#endif +} + +bool pn_backend_supports_die_placement(void) +{ + /* Metal is deliberately excluded: die_manager.h has pn_dm_init_cuda and + * pn_dm_init_tt and nothing else, so on a Metal build `--dies 0` names an + * index no init path can ever register. */ +#if defined(USE_CUDA) || defined(USE_TT) + return true; +#else + return false; +#endif +} diff --git a/src/accel/backend_identity.h b/src/accel/backend_identity.h new file mode 100644 index 0000000..ac05df3 --- /dev/null +++ b/src/accel/backend_identity.h @@ -0,0 +1,102 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file backend_identity.h + * @brief What accelerator backend THIS binary was compiled with, as a value the + * process can print. Compile-time constants only — no probing, no + * driver, no allocation, safe to call from anywhere at any time. + * + * Why this exists: `make`, `make USE_CUDA=1` and `make USE_TT=1` are + * three different programs, and until the per-backend output names + * landed they all wrote the same file. A CPU-only binary sitting at the + * deployed path served a 35B MoE on a 2-core host with all 8 K80 dies + * idle for an unknown period, and answering "what am I running?" took + * `ldd` and `strings` on a remote box. The backend is a compile-time + * constant; the process should simply say so — in its startup log, on + * /v1/models, on the health route, and through `pantheon --backend` so a + * deploy script can assert on it before launching anything. + * + * pn_backend_name() is the human string ("CUDA"), pn_backend_token() the + * machine one ("cuda") — a deploy script compares the token, a log line + * prints the name. They are the same enumeration, never derived from + * each other at runtime, so neither can drift into a lie. + * + * @status Complete. CUDA/TT/METAL arms are compile-time only, so they are + * proved by the preprocessor, not by a hardware run. + * @issues None + * @todo None + */ + +#ifndef PN_BACKEND_IDENTITY_H +#define PN_BACKEND_IDENTITY_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Human-readable name of the compiled accelerator backend: "CUDA", "TT", + * "METAL", "CUDA+TT", ... or "CPU" when none is compiled in. + * + * @return A string literal — never NULL, never freed. + * + * Thread-safe: yes (returns a literal) + * Reentrant: yes + * Signal-safe: yes + */ +const char *pn_backend_name(void); + +/** + * Machine-readable form of the same value, lowercase and shell-comparable: + * "cuda", "tt", "metal", "cuda+tt", "cpu". This is what `pantheon --backend` + * prints and what a deploy script asserts against. + * + * @return A string literal — never NULL, never freed. + * + * Thread-safe: yes (returns a literal) + * Reentrant: yes + * Signal-safe: yes + */ +const char *pn_backend_token(void); + +/** + * The commit this binary was built from (short hash, with a "-dirty" suffix + * when the tree had uncommitted changes), or "unknown" when the build had no + * git available. Stamped in by the Makefile as -DPN_BUILD_COMMIT. + * + * @return A string literal — never NULL, never freed. + * + * Thread-safe: yes (returns a literal) + * Reentrant: yes + * Signal-safe: yes + */ +const char *pn_backend_build_commit(void); + +/** + * Whether ANY GPU/accelerator backend is compiled in. False means every forward + * pass in this process runs on the host CPU, whatever the flags say. + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +bool pn_backend_has_accelerator(void); + +/** + * Whether this binary carries a backend the DIE MANAGER can probe — i.e. one + * for which `--dies` means anything. CUDA and Tenstorrent have pn_dm_init_* + * paths; Metal does not (it is one implicit device, never placed by index), so + * a Metal build answers false here and true for pn_backend_has_accelerator(). + * + * This is the predicate the serve's refuse-to-start guard asks, because the + * question there is not "is there a GPU" but "can a die index be honoured". + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +bool pn_backend_supports_die_placement(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_BACKEND_IDENTITY_H */ diff --git a/src/accel/die_layer_split.c b/src/accel/die_layer_split.c new file mode 100644 index 0000000..306f20c --- /dev/null +++ b/src/accel/die_layer_split.c @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file die_layer_split.c + * @brief Implementation of the shared N-die layer partitioner. + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "die_layer_split.h" + +#include + +/* The boundary before die `idx` of an n_die-way split of n_layer layers. + * Computed in `long` because n_layer * idx overflows int for absurd inputs + * long before the caller's validation would notice, and a wrapped boundary is + * a silently-wrong split rather than a crash. */ +static int split_boundary(int n_layer, int n_die, int idx) +{ + return (int)((long)n_layer * (long)idx / (long)n_die); +} + +/* Shared validation for both entry points. */ +static int split_args_ok(int n_layer, int n_die) +{ + return n_layer > 0 && n_die >= 1 && + n_die <= PN_DIE_LAYER_SPLIT_MAX_DIES && n_die <= n_layer; +} + +int pn_plan_die_layer_split(int n_layer, int n_die, int override_boundary, + pn_die_layer_split *out) +{ + if (!out || !split_args_ok(n_layer, n_die)) + return -1; + + for (int i = 0; i < n_die; i++) { + out->layer_lo[i] = split_boundary(n_layer, n_die, i); + out->layer_hi[i] = (i == n_die - 1) ? n_layer + : split_boundary(n_layer, n_die, i + 1); + } + /* A single boundary describes a 2-die split completely and nothing wider, + * so the override applies at exactly that width. Clamped inside (0,n_layer) + * so an out-of-range knob narrows the split rather than emptying a die. */ + if (n_die == 2 && override_boundary > 0) { + int b = override_boundary; + if (b >= n_layer) + b = n_layer - 1; + out->layer_hi[0] = b; + out->layer_lo[1] = b; + } + out->n_die = n_die; + return 0; +} + +int pn_die_layer_range(int n_layer, int n_die, int idx, int *lo_out, + int *hi_out) +{ + if (!split_args_ok(n_layer, n_die) || idx < 0 || idx >= n_die) + return -1; + + if (lo_out) + *lo_out = split_boundary(n_layer, n_die, idx); + if (hi_out) + *hi_out = (idx == n_die - 1) ? n_layer + : split_boundary(n_layer, n_die, idx + 1); + return 0; +} diff --git a/src/accel/die_layer_split.h b/src/accel/die_layer_split.h new file mode 100644 index 0000000..97962fc --- /dev/null +++ b/src/accel/die_layer_split.h @@ -0,0 +1,100 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file die_layer_split.h + * @brief How a model's transformer layers divide across an N-die placement. + * ONE definition of the layer ranges, shared by the pre-load admission + * projection and the GPU forward that actually uploads them. + * + * The two used to derive the ranges separately — model_registry.c projected + * `n_layer*idx/n_placed` per die while qwen35moe_forward_cuda.c split at + * `n_layer/2` regardless of how many dies were placed. They agreed only at + * n_placed == 2, so a `--dies 5,6,7` placement bound three dies, uploaded a + * two-way split, under-projected each die's share by a third, and OOM'd. A + * projection that disagrees with placement is the bug class this module exists + * to make unrepresentable: admission and the forward call the SAME function. + * + * Quant-agnostic by construction: this module partitions LAYER INDICES. It + * never sees a tensor, a byte count or a quantization type, so the same split + * carries to Q3_K, Q4_K, Q6_K and F16 unchanged. + * + * @status Complete. Ranges, the N=2 compatibility guarantee and the refusal + * boundaries are covered by tests/test_die_layer_split.c. + * @issues None + * @todo None + */ + +#ifndef PN_DIE_LAYER_SPLIT_H +#define PN_DIE_LAYER_SPLIT_H + +/* Widest layer split this partitioner will plan. The die manager and the + * memwatch state carry 16 dies (PN_MEM_MAX_DIES), and PN_MODEL_MAX_DIES caps + * what one model may be PLACED on; this is the split planner's own ceiling and + * is deliberately the same number, so "the placement fit" and "the split fit" + * cannot disagree. Widths above it are refused, never silently narrowed. */ +#define PN_DIE_LAYER_SPLIT_MAX_DIES 16 + +/* A planned N-way layer split. Die i owns global layers [layer_lo[i], + * layer_hi[i]); the ranges are contiguous, non-empty, cover [0, n_layer) + * exactly, and differ in length by at most one. The LM head and final norm + * ride on die n_die-1 (the last), which is a convention this header states + * rather than a field: nothing here can enforce what the uploader does with + * the head, so both sides name the same rule. */ +typedef struct { + int n_die; + int layer_lo[PN_DIE_LAYER_SPLIT_MAX_DIES]; + int layer_hi[PN_DIE_LAYER_SPLIT_MAX_DIES]; +} pn_die_layer_split; + +/** + * Plans how `n_layer` layers divide across `n_die` dies. + * + * The boundary for die i is `n_layer * i / n_die` in integer arithmetic, which + * is the flattest split available and — the property the 2-die path depends on + * — reduces to exactly `n_layer / 2` at n_die == 2, the boundary the CUDA + * forward has always used. So a 2-die placement plans byte-identically to the + * hardcoded pair it replaces, for even and odd layer counts alike. Any + * remainder lands on the LATER dies, which is what makes that identity hold + * (41 layers over 2 dies is 20 + 21, not 21 + 20). + * + * @param n_layer Total transformer layers (> 0) + * @param n_die Dies to split across (1 .. PN_DIE_LAYER_SPLIT_MAX_DIES) + * @param override_boundary Manual first boundary, or <= 0 for none. Honoured + * ONLY at n_die == 2 (where a single boundary is the + * whole plan); IGNORED for every other width, because + * one number cannot describe N-1 boundaries. Clamped + * to [1, n_layer-1] so it can never empty a die. + * @param out Receives the plan. Untouched on refusal. + * @return 0 on success, -1 if refused — n_layer <= 0, n_die < 1, n_die above + * PN_DIE_LAYER_SPLIT_MAX_DIES, or n_die > n_layer (a die with no layers + * is not a narrower split, it is a bug). Callers must REFUSE on -1 + * rather than fall back to a narrower split: mis-splitting silently is + * the failure this replaces. + * + * Thread-safe: yes (pure) + * Reentrant: yes + */ +int pn_plan_die_layer_split(int n_layer, int n_die, int override_boundary, + pn_die_layer_split *out); + +/** + * The layer range for ONE die of an N-way split, without materializing the + * whole plan — the shape admission wants when it is pricing die `idx` alone. + * Identical arithmetic to pn_plan_die_layer_split; it is the same function + * viewed through one index. + * + * @param n_layer Total transformer layers (> 0) + * @param n_die Dies to split across (1 .. PN_DIE_LAYER_SPLIT_MAX_DIES) + * @param idx Which die (0 .. n_die-1) + * @param lo_out Receives the first global layer on this die (may be NULL) + * @param hi_out Receives one past the last (may be NULL) + * @return 0 on success, -1 on the same refusals as pn_plan_die_layer_split + * (plus an out-of-range idx). Outputs untouched on refusal. + * + * Thread-safe: yes (pure) + * Reentrant: yes + */ +int pn_die_layer_range(int n_layer, int n_die, int idx, int *lo_out, + int *hi_out); + +#endif /* PN_DIE_LAYER_SPLIT_H */ diff --git a/src/accel/die_manager.c b/src/accel/die_manager.c new file mode 100644 index 0000000..11e5460 --- /dev/null +++ b/src/accel/die_manager.c @@ -0,0 +1,441 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file die_manager.c + * @brief Implementation of the die manager (see die_manager.h). One + * process-global table (dies + bindings) behind a mutex, mirroring the + * singleton style of pn_memwatch. Placement policy detail lives in + * pn_dm_place below; everything else is straight accounting. + * + * @status Logical path tested on the Mac; CUDA and TT probes gated on GPU hardware. + * @issues None + * @todo None + */ + +#include "die_manager.h" + +#include +#include +#include + +#ifdef USE_CUDA +#include "cuda/fc_cuda.h" +#include "../core/mem/pn_memwatch.h" +#endif + +#ifdef USE_TT +#include "tt/ft_tt_coord.h" +#include "tt/ft_tt_device.h" +#include "tt/ft_tt_runtime.h" +#include "../core/mem/pn_memwatch.h" +#endif + +/* One resident model's reservation: which dies it holds and how many bytes it + * put on each (total split evenly across the set). */ +typedef struct { + bool used; + char name[PN_DM_NAME_MAX]; + int dies[PN_DM_MAX_DIES]; + int n_dies; + uint64_t bytes_per_die; +} dm_binding; + +typedef struct { + bool present; + uint64_t vram_budget; + uint64_t vram_used; +} dm_die; + +static struct { + pthread_mutex_t lock; + bool running; + int n_dies; + dm_die die[PN_DM_MAX_DIES]; + dm_binding binding[PN_DM_MAX_BINDINGS]; +} g_dm = { .lock = PTHREAD_MUTEX_INITIALIZER }; + +/* --- init / shutdown --------------------------------------------------- */ + +int pn_dm_init_logical(int n_dies, uint64_t budget_bytes) +{ + if (n_dies <= 0 || n_dies > PN_DM_MAX_DIES || budget_bytes == 0) + return -1; + pthread_mutex_lock(&g_dm.lock); + if (g_dm.running) { /* idempotent while running */ + pthread_mutex_unlock(&g_dm.lock); + return 0; + } + memset(g_dm.die, 0, sizeof(g_dm.die)); + memset(g_dm.binding, 0, sizeof(g_dm.binding)); + g_dm.n_dies = n_dies; + for (int i = 0; i < n_dies; i++) { + g_dm.die[i].present = true; + g_dm.die[i].vram_budget = budget_bytes; + } + g_dm.running = true; + pthread_mutex_unlock(&g_dm.lock); + return 0; +} + +#ifdef USE_CUDA +int pn_dm_init_cuda(int n_dies) +{ + if (n_dies <= 0 || n_dies > PN_DM_MAX_DIES) + return -1; + /* Driver-only gate on purpose: this runs at serve startup, before any + * model has a placement, so it must not be the thing that forces a + * process-default context onto whatever die happens to be ordinal 0. */ + if (!fc_driver_available()) + return -1; + pthread_mutex_lock(&g_dm.lock); + if (g_dm.running) { + int n = g_dm.n_dies; + pthread_mutex_unlock(&g_dm.lock); + return n; + } + memset(g_dm.die, 0, sizeof(g_dm.die)); + memset(g_dm.binding, 0, sizeof(g_dm.binding)); + int found = 0; + uint64_t reserve = pn_die_reserve_bytes(); + for (int i = 0; i < n_dies; i++) { + /* Transient bare context per die — created, read, destroyed. Probing + * with fc_cuda_init_device() stranded a full context (cuBLAS + 12 JIT'd + * PTX modules, ~124 MiB) on EVERY die the serve enumerated, including + * dies owned by another serve. A probe must leave no trace. */ + size_t die_free = 0, die_total = 0; + if (fc_cuda_probe_device(i, &die_free, &die_total) != 0 || die_total == 0) + break; /* ordinals are contiguous — stop */ + uint64_t budget = die_total > reserve ? die_total - reserve : 0; + g_dm.die[i].present = true; + g_dm.die[i].vram_budget = budget; + /* Anything already gone from `free` beyond the reserve is occupancy we + * did not create (another process / stranded VRAM) — count it so + * placement doesn't over-commit the die. */ + uint64_t external = (die_total - die_free) > reserve + ? (die_total - die_free) - reserve : 0; + g_dm.die[i].vram_used = external < budget ? external : budget; + /* 0 bytes ours: the probe context is already destroyed, and no model + * has been placed on this die yet. */ + if (pn_memwatch_running()) + pn_memwatch_set_die(i, die_free, budget, 0); + found++; + } + if (found == 0) { + pthread_mutex_unlock(&g_dm.lock); + return -1; + } + g_dm.n_dies = found; + g_dm.running = true; + pthread_mutex_unlock(&g_dm.lock); + fprintf(stderr, "die_manager: tracking %d CUDA die%s\n", + found, found == 1 ? "" : "s"); + return found; +} +#endif /* USE_CUDA */ + +#ifdef USE_TT +bool pn_dm_tt_driver_explicit(void) +{ + ft_tt_runtime_driver driver = FT_TT_RUNTIME_DRIVER_KMD; + bool explicit_choice = false; + + /* A REFUSED value (neither kmd nor udrv) counts as explicit: somebody asked + * for a transport and did not get one, which must not read as "no card". */ + if (ft_tt_runtime_driver_select(&driver, &explicit_choice) != FT_TT_OK) + return true; + return explicit_choice; +} + +/* Capacity of one Blackhole device, in bytes, or 0 if it cannot be measured. + * + * The probe leaves no trace: it opens the device, reads the grid and closes + * again. That matters for the same reason it did on CUDA — this runs at serve + * startup, before any model has a placement, and a probe that held a context + * or a TLB window would be holding it against whichever job takes the card. + * + * It goes through ft_tt_runtime_open() rather than ft_tt_init_device() so it + * probes over WHICHEVER TRANSPORT the process is configured for. Opening the + * chardev here would have refused a machine with no vendor module before the + * serve ever reached the seam — which is exactly the defect this round exists + * to remove, one layer up from where it was first seen. + * + * The capacity is NOT a constant and is deliberately not defaulted. Bank count + * comes from ARC telemetry and bank size from the architecture table selected + * at open; ft_tt_runtime_dram_bytes() answers 0 rather than reporting the + * nominal eight banks as if it had measured them, so a telemetry failure ends + * the scan instead of registering a die with an invented budget. */ +static uint64_t tt_probe_device_bytes(int device_index) +{ + ft_tt_runtime runtime; + uint64_t total; + + if (ft_tt_runtime_open(device_index, &runtime) != FT_TT_OK) + return 0; + /* ft_tt_runtime_open() already refuses a topology ft_tt_coord cannot map, + * on both transports, so a card we would not dare address never gets here + * (spec §7.3). */ + total = ft_tt_runtime_dram_bytes(&runtime); + ft_tt_runtime_close(&runtime); + return total; +} + +int pn_dm_init_tt(int n_devices) +{ + if (n_devices <= 0 || n_devices > PN_DM_MAX_DIES) + return -1; + pthread_mutex_lock(&g_dm.lock); + if (g_dm.running) { /* idempotent while running */ + int n = g_dm.n_dies; + pthread_mutex_unlock(&g_dm.lock); + return n; + } + memset(g_dm.die, 0, sizeof(g_dm.die)); + memset(g_dm.binding, 0, sizeof(g_dm.binding)); + int found = 0; + uint64_t reserve = pn_die_reserve_bytes(); + for (int i = 0; i < n_devices; i++) { + uint64_t total = tt_probe_device_bytes(i); + if (total == 0) + break; /* ordinals are contiguous — stop */ + g_dm.die[found].present = true; + g_dm.die[found].vram_budget = total > reserve ? total - reserve : 0; + /* No external-occupancy term, unlike the CUDA probe: the card exposes + * no cross-process allocation view, so `free` from a fresh context is + * always `total`. A second process holding DRAM is invisible here and + * is a reason to run one serve per card, not a number to fake. */ + g_dm.die[found].vram_used = 0; + if (pn_memwatch_running()) + pn_memwatch_set_die(found, total, g_dm.die[found].vram_budget, 0); + found++; + } + if (found == 0) { + pthread_mutex_unlock(&g_dm.lock); + return -1; + } + g_dm.n_dies = found; + g_dm.running = true; + pthread_mutex_unlock(&g_dm.lock); + fprintf(stderr, "die_manager: tracking %d Tenstorrent device%s (one die each)\n", + found, found == 1 ? "" : "s"); + return found; +} +#endif /* USE_TT */ + +void pn_dm_shutdown(void) +{ + pthread_mutex_lock(&g_dm.lock); + g_dm.running = false; + g_dm.n_dies = 0; + memset(g_dm.die, 0, sizeof(g_dm.die)); + memset(g_dm.binding, 0, sizeof(g_dm.binding)); + pthread_mutex_unlock(&g_dm.lock); +} + +/* --- queries ------------------------------------------------------------ */ + +int pn_dm_count(void) +{ + pthread_mutex_lock(&g_dm.lock); + int n = g_dm.running ? g_dm.n_dies : 0; + pthread_mutex_unlock(&g_dm.lock); + return n; +} + +/* Locked helper: does `b` cover die `id`? */ +static bool binding_has_die(const dm_binding *b, int id) +{ + for (int i = 0; i < b->n_dies; i++) + if (b->dies[i] == id) + return true; + return false; +} + +int pn_dm_get(int die_id, pn_dm_die *out) +{ + if (!out) + return -1; + pthread_mutex_lock(&g_dm.lock); + if (!g_dm.running || die_id < 0 || die_id >= g_dm.n_dies || + !g_dm.die[die_id].present) { + pthread_mutex_unlock(&g_dm.lock); + return -1; + } + memset(out, 0, sizeof(*out)); + out->id = die_id; + out->vram_budget = g_dm.die[die_id].vram_budget; + out->vram_used = g_dm.die[die_id].vram_used; + for (int i = 0; i < PN_DM_MAX_BINDINGS; i++) { + if (!g_dm.binding[i].used || !binding_has_die(&g_dm.binding[i], die_id)) + continue; + if (out->n_models == 0) { + strncpy(out->model_name, g_dm.binding[i].name, + PN_DM_NAME_MAX - 1); + out->model_name[PN_DM_NAME_MAX - 1] = '\0'; + } + out->n_models++; + } + pthread_mutex_unlock(&g_dm.lock); + return 0; +} + +/* --- placement ----------------------------------------------------------- */ + +/* Locked helpers. */ +static uint64_t die_free_bytes(int id) +{ + const dm_die *d = &g_dm.die[id]; + return d->vram_budget > d->vram_used ? d->vram_budget - d->vram_used : 0; +} + +static bool die_occupied(int id) +{ + return g_dm.die[id].vram_used > 0; +} + +/* The card mate of a die under the fixed K80 pairing (2i <-> 2i+1); -1 when + * the mate is outside the tracked range. */ +static int die_mate(int id) +{ + int mate = (id % 2 == 0) ? id + 1 : id - 1; + return (mate >= 0 && mate < g_dm.n_dies) ? mate : -1; +} + +int pn_dm_place(uint64_t need_bytes, int n_dies_wanted, int *dies_out) +{ + if (!dies_out || n_dies_wanted < 1 || n_dies_wanted > 2) + return -1; /* >2-die pipeline placement arrives with Phase 6.1 */ + + pthread_mutex_lock(&g_dm.lock); + if (!g_dm.running) { + pthread_mutex_unlock(&g_dm.lock); + return -1; + } + uint64_t per_die = need_bytes / (uint64_t)n_dies_wanted; + int rc = -1; + + if (n_dies_wanted == 2) { + /* Same-card pair with room on both halves. Prefer a fully-free pair; + * tie-break on the most free space (least loaded). */ + int best = -1; + bool best_free = false; + uint64_t best_room = 0; + for (int a = 0; a + 1 < g_dm.n_dies; a += 2) { + int b = a + 1; + if (!g_dm.die[a].present || !g_dm.die[b].present) + continue; + if (die_free_bytes(a) < per_die || die_free_bytes(b) < per_die) + continue; + bool pair_free = !die_occupied(a) && !die_occupied(b); + uint64_t room = die_free_bytes(a) + die_free_bytes(b); + if (best < 0 || (pair_free && !best_free) || + (pair_free == best_free && room > best_room)) { + best = a; + best_free = pair_free; + best_room = room; + } + } + if (best >= 0) { + dies_out[0] = best; + dies_out[1] = best + 1; + rc = 0; + } + } else { + /* Single die: pack onto a card whose mate is already occupied so free + * same-card pairs stay available for 2-die models; among candidates + * take the least-loaded (most free). */ + int best = -1; + bool best_packed = false; + uint64_t best_room = 0; + for (int i = 0; i < g_dm.n_dies; i++) { + if (!g_dm.die[i].present || die_free_bytes(i) < per_die) + continue; + int mate = die_mate(i); + bool packed = (mate >= 0 && die_occupied(mate)) || die_occupied(i); + uint64_t room = die_free_bytes(i); + if (best < 0 || (packed && !best_packed) || + (packed == best_packed && room > best_room)) { + best = i; + best_packed = packed; + best_room = room; + } + } + if (best >= 0) { + dies_out[0] = best; + rc = 0; + } + } + pthread_mutex_unlock(&g_dm.lock); + return rc; +} + +/* --- bind / release ------------------------------------------------------ */ + +int pn_dm_bind(const int *dies, int n, const char *model_name, + uint64_t total_bytes) +{ + if (!dies || n < 1 || n > PN_DM_MAX_DIES || !model_name || !*model_name) + return -1; + + pthread_mutex_lock(&g_dm.lock); + if (!g_dm.running) + goto refuse; + + /* Same-name multi-card placement: one model id may be bound to several + * DISJOINT die-sets (one binding per card) for load-balanced serving. + * Overlapping dies are still rejected by the per-die room check below, so + * here we only need a free binding slot — no duplicate-name refusal. Each + * placement gets its own binding; the registry releases one per entry on + * unload (pn_dm_release is by-name, first-match — N entries → N releases). */ + dm_binding *slot = NULL; + for (int i = 0; i < PN_DM_MAX_BINDINGS; i++) { + if (!g_dm.binding[i].used) { slot = &g_dm.binding[i]; break; } + } + if (!slot) + goto refuse; /* binding table full */ + + uint64_t per_die = total_bytes / (uint64_t)n; + for (int i = 0; i < n; i++) { + int id = dies[i]; + if (id < 0 || id >= g_dm.n_dies || !g_dm.die[id].present || + die_free_bytes(id) < per_die) + goto refuse; /* unknown die / no room */ + } + + slot->used = true; + strncpy(slot->name, model_name, PN_DM_NAME_MAX - 1); + slot->name[PN_DM_NAME_MAX - 1] = '\0'; + slot->n_dies = n; + slot->bytes_per_die = per_die; + for (int i = 0; i < n; i++) { + slot->dies[i] = dies[i]; + g_dm.die[dies[i]].vram_used += per_die; + } + pthread_mutex_unlock(&g_dm.lock); + return 0; + +refuse: + pthread_mutex_unlock(&g_dm.lock); + return -1; +} + +int pn_dm_release(const char *model_name) +{ + if (!model_name) + return -1; + pthread_mutex_lock(&g_dm.lock); + for (int i = 0; i < PN_DM_MAX_BINDINGS; i++) { + dm_binding *b = &g_dm.binding[i]; + if (!b->used || strncmp(b->name, model_name, PN_DM_NAME_MAX) != 0) + continue; + for (int j = 0; j < b->n_dies; j++) { + dm_die *d = &g_dm.die[b->dies[j]]; + d->vram_used = d->vram_used > b->bytes_per_die + ? d->vram_used - b->bytes_per_die : 0; + } + memset(b, 0, sizeof(*b)); + pthread_mutex_unlock(&g_dm.lock); + return 0; + } + pthread_mutex_unlock(&g_dm.lock); + return -1; +} diff --git a/src/accel/die_manager.h b/src/accel/die_manager.h new file mode 100644 index 0000000..36aa839 --- /dev/null +++ b/src/accel/die_manager.h @@ -0,0 +1,173 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file die_manager.h + * @brief Minimal die manager — the placement primitive under multi-model + * serving. Tracks per-die + * state (bound model names, VRAM budget/occupancy) and assigns models + * to die-sets, topology-aware: a K80 card is the die pair (2i, 2i+1) + * linked by a PLX switch, so 2-die models get a same-card pair and + * 1-die models pack onto cards whose mate is already occupied (keeping + * whole pairs free for the next 2-die model). + * + * Three init paths, one per accelerator family: pn_dm_init_logical() + * creates N logical dies with a given budget (the CPU/Mac/test path — + * placement bookkeeping needs no GPU), pn_dm_init_cuda() (hardware) probes + * real K80 dies via fc_cuda_probe_device, and pn_dm_init_tt() (hardware) + * probes Tenstorrent Blackhole cards via ft_tt_init_device + ARC + * telemetry. All three feed pn_memwatch_set_die so MemoryAware + * admission sees the same per-die truth. This module is bookkeeping + * only — it never uploads weights; pn_load_on() does the binding of a + * model to the die-set this module picks. + * + * @status Logical path exercised on the Mac (tests/test_die_manager.c). The + * CUDA probe compiles under USE_CUDA and is gated on GPU hardware (needs live + * dies to validate budgets + memwatch feed). The TT probe compiles + * under USE_TT and is gated on GPU hardware the same way. + * @issues Placement assumes the fixed K80 pairing (card = dies 2i/2i+1); a + * different topology needs a card-map input. On a TT card the pairing + * is inert (one die per device) rather than wrong. + * @todo + * - [ ] (hardware) Validate pn_dm_init_cuda budgets vs nvidia-smi on 16 dies. + */ + +#ifndef PN_DIE_MANAGER_H +#define PN_DIE_MANAGER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Hard caps: the K80 test machine has 16 GK210 dies (mirrors PN_MEM_MAX_DIES); one + * binding per resident model. */ +#define PN_DM_MAX_DIES 16 +#define PN_DM_MAX_BINDINGS 16 +#define PN_DM_NAME_MAX 64 +#define PN_DM_DIES_PER_CARD 2 + +/* Copy-out view of one die (pn_dm_get / status surfaces). */ +typedef struct { + int id; + char model_name[PN_DM_NAME_MAX]; /* first bound model, "" if free */ + int n_models; /* bindings touching this die */ + uint64_t vram_budget; /* usable bytes (total - reserve) */ + uint64_t vram_used; /* bytes reserved by bindings */ +} pn_dm_die; + +/* Start the manager with n_dies LOGICAL dies of budget_bytes each — the + * CPU/test path (a model can be "placed" without CUDA present). Idempotent + * while running (returns 0 without re-init). Returns 0 on success, -1 on a + * bad argument. + * + * Thread-safe: yes */ +int pn_dm_init_logical(int n_dies, uint64_t budget_bytes); + +#ifdef USE_CUDA +/* (hardware) Start the manager from real dies: probe CUDA ordinals [0, n_dies) + * via fc_cuda_init_device + fc_mem_info_ctx, budget = total − + * pn_die_reserve_bytes(), and feed each die into pn_memwatch_set_die when the + * watcher is running. Requires fc_init() done and fc_available() true — + * returns -1 otherwise (caller falls back to pn_dm_init_logical or CPU-only). + * Idempotent while running. Returns the number of dies probed (>0) or -1. */ +int pn_dm_init_cuda(int n_dies); +#endif + +#ifdef USE_TT +/* (hardware) Start the manager from Tenstorrent card(s): probe device ordinals + * [0, n_devices) via ft_tt_init_device + ARC telemetry, register ONE die per + * DEVICE with budget = total − pn_die_reserve_bytes(), and feed each into + * pn_memwatch_set_die when the watcher is running. Ordinals are contiguous, so + * the first one that will not open ends the scan. Idempotent while running. + * Returns the number of devices probed (>0) or -1. + * + * ONE-DEVICE-ONE-DIE, deliberately, and NOT core-group-as-unit (Blackhole spec + * §9.3). The unit this module models is a MEMORY DOMAIN — every field it + * stores is a byte budget or a byte occupancy — and a p150a has exactly one: + * ~31.88 GiB of GDDR6 that all 140 Tensix cores address over the same NoC. A + * core group is a COMPUTE partition, not a memory domain, so registering N + * groups as N dies would divide one real budget into N fictional ones (and + * refuse a model that fits the card), and would assert an independence the + * hardware does not have — Phase 6.7 measured a per-NoC-row DRAM ceiling + * shared across groups, and readers on one side of the grid slowing readers on + * the other. + * + * The K80 topology is deliberately not ported forward: pn_dm_card_of()'s + * die-pair model describes two GK210s with private VRAM behind a PLX switch, + * and there is no analogue here. At one die per device it degenerates to the + * identity rather than to something wrong. + * + * Multi-model isolation is a real requirement and it already has a mechanism + * one layer down: ft_tt_group_partition() hands each group disjoint cores AND + * disjoint DRAM banks with its own allocator. When several models share a + * card, the field to add is a CORE COUNT on the binding beside the single + * memory die — memory and compute are separate resources on this part, which + * is the one structural difference from a K80 die. + * + * Note there is no cross-process occupancy signal: ft_tt_mem_info()'s `free` + * is total minus THIS context's own allocations, so unlike the CUDA probe + * there is no stranded-memory term and vram_used starts at 0. + * + * Thread-safe: yes */ +int pn_dm_init_tt(int n_devices); + +/** + * Whether this process was TOLD which Tenstorrent transport to use + * (`FT_TT_DRIVER=kmd|udrv`) — including having named one that does not exist. + * + * The caller needs this because "no Tenstorrent device" has two meanings and + * only one of them is benign. On a machine with no card a failed pn_dm_init_tt + * is expected and serving CPU-resident is correct. When somebody named a + * transport, the same failure means the card they asked for could not be + * reached — and answering on the CPU would be the silent fallback this project + * has shipped twice, returning a plausible answer from the wrong thing. It was + * measured on 2026-08-17 that the card and the CPU fallback return + * BYTE-IDENTICAL completion text, so nothing downstream can catch it. + * + * Thread-safe: yes */ +bool pn_dm_tt_driver_explicit(void); +#endif + +/* Stop tracking and forget all dies/bindings. Safe to call if never started. */ +void pn_dm_shutdown(void); + +/* Number of tracked dies (0 = manager not running → CPU-only placement). */ +int pn_dm_count(void); + +/* Copy die_id's state into *out. Returns 0, or -1 if unknown/not running. */ +int pn_dm_get(int die_id, pn_dm_die *out); + +/* The card index a die sits on (K80 pairing: die/2). Pure. */ +static inline int pn_dm_card_of(int die_id) { return die_id / PN_DM_DIES_PER_CARD; } + +/* Pick a die-set for a model needing need_bytes across n_dies_wanted dies + * (bytes split evenly). Topology-aware: n=2 → a same-card pair, preferring a + * fully-free pair, least-loaded tie-break; n=1 → pack onto a die whose card + * mate is already occupied, least-loaded among those, else the least-loaded + * free-fitting die. Writes the chosen ids to dies_out[0..n) and returns 0, or + * -1 when nothing fits (or the manager is not running). Does NOT reserve — + * call pn_dm_bind to commit. + * + * Thread-safe: yes */ +int pn_dm_place(uint64_t need_bytes, int n_dies_wanted, int *dies_out); + +/* Commit model_name to the die-set: re-checks capacity under the lock and + * adds total_bytes/n to each die's vram_used. Rejects a duplicate name, an + * unknown die, or a set that no longer fits. Returns 0 on success, -1 on + * refusal (nothing is partially bound). + * + * Thread-safe: yes */ +int pn_dm_bind(const int *dies, int n, const char *model_name, + uint64_t total_bytes); + +/* Release model_name's binding, returning its bytes to each die. Returns 0, + * or -1 if the name is unknown. */ +int pn_dm_release(const char *model_name); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_DIE_MANAGER_H */ diff --git a/src/accel/simd.c b/src/accel/simd.c new file mode 100644 index 0000000..9947a09 --- /dev/null +++ b/src/accel/simd.c @@ -0,0 +1,259 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file simd.c + * @brief NEON / AVX2+FMA / plain-C implementations of the CPU vector kernels. + * + * Written from a behaviour specification (plans/cleanroom_r2_simd_spec.md); no ggml or + * llama.cpp source was consulted. + * + * Release builds use -ffast-math, so output bits depend on the shape of this source (which + * loops are explicit SIMD, accumulator count, statement split), not only on the maths. + * Change the shape of any loop and the bits change. + * + * @status None + * @issues - -ffast-math re-associates silu*up into (up*x)/(1+e); release and debug differ. + * - x86 vectorized silu uses a reciprocal estimate; its scalar tail divides exactly. + * - NEON Q4_0 scales per element, scalar Q4_0 gets d hoisted by the compiler, Q8_0 + * scales at the end: the paths do not agree bit-for-bit. + * - The first FMA into a zero accumulator folds to a multiply (keeps -0). + * - NaN payload choice follows compiled operand order. + * - rms computes NaN for n == 0 (nothing is written). + * - No NULL or range checks. + * - pn_vmul_f32, pn_vscale_f32, pn_vsum_f32 have no callers. + * - The shipping x86 target (the bank, no AVX/FMA) never compiles the AVX2 path. + * @todo None + */ +#include "simd.h" + +#include + +#include "../formats/gguf_dequant.h" + +#if defined(__ARM_NEON) || defined(__ARM_NEON__) +#define PN_SIMD_PATH_NEON 1 +#include +#elif defined(__AVX2__) && defined(__FMA__) +#define PN_SIMD_PATH_AVX2 1 +#include +#endif + +#ifdef PN_SIMD_PATH_AVX2 +/* Low half + high half, then two horizontal pair-adds; lane 0 holds the total. */ +static float avx2_horizontal_sum(__m256 v) { + __m128 low = _mm256_castps256_ps128(v); + __m128 high = _mm256_extractf128_ps(v, 1); + low = _mm_add_ps(low, high); + low = _mm_hadd_ps(low, low); + low = _mm_hadd_ps(low, low); + return _mm_cvtss_f32(low); +} +#endif + +float pn_dot_f32(const float *a, const float *b, int n) { + int i = 0; + float sum; +#if defined(PN_SIMD_PATH_NEON) + float32x4_t acc0 = vdupq_n_f32(0.0f); + float32x4_t acc1 = vdupq_n_f32(0.0f); + for (; i + 7 < n; i += 8) { + float32x4_t a0 = vld1q_f32(a + i); + float32x4_t b0 = vld1q_f32(b + i); + float32x4_t a1 = vld1q_f32(a + i + 4); + float32x4_t b1 = vld1q_f32(b + i + 4); + acc0 = vfmaq_f32(acc0, a0, b0); + acc1 = vfmaq_f32(acc1, a1, b1); + } + acc0 = vaddq_f32(acc0, acc1); + sum = vaddvq_f32(acc0); +#elif defined(PN_SIMD_PATH_AVX2) + __m256 acc0 = _mm256_setzero_ps(); + __m256 acc1 = _mm256_setzero_ps(); + for (; i + 15 < n; i += 16) { + __m256 a_lo = _mm256_loadu_ps(a + i); + __m256 a_hi = _mm256_loadu_ps(a + i + 8); + __m256 b_lo = _mm256_loadu_ps(b + i); + __m256 b_hi = _mm256_loadu_ps(b + i + 8); + acc0 = _mm256_fmadd_ps(a_lo, b_lo, acc0); + acc1 = _mm256_fmadd_ps(a_hi, b_hi, acc1); + } + acc0 = _mm256_add_ps(acc0, acc1); + sum = avx2_horizontal_sum(acc0); +#else + sum = 0.0f; +#endif + for (; i < n; i++) + sum += a[i] * b[i]; + return sum; +} + +void pn_vadd_f32(const float *a, const float *b, float *out, int n) { + int i = 0; +#if defined(PN_SIMD_PATH_NEON) + for (; i + 3 < n; i += 4) { + float32x4_t va = vld1q_f32(a + i); + float32x4_t vb = vld1q_f32(b + i); + vst1q_f32(out + i, vaddq_f32(va, vb)); + } +#elif defined(PN_SIMD_PATH_AVX2) + for (; i + 7 < n; i += 8) { + __m256 va = _mm256_loadu_ps(a + i); + __m256 vb = _mm256_loadu_ps(b + i); + _mm256_storeu_ps(out + i, _mm256_add_ps(va, vb)); + } +#endif + for (; i < n; i++) + out[i] = a[i] + b[i]; +} + +void pn_vmul_f32(const float *a, const float *b, float *out, int n) { + int i = 0; +#if defined(PN_SIMD_PATH_NEON) + for (; i + 3 < n; i += 4) { + float32x4_t va = vld1q_f32(a + i); + float32x4_t vb = vld1q_f32(b + i); + vst1q_f32(out + i, vmulq_f32(va, vb)); + } +#elif defined(PN_SIMD_PATH_AVX2) + for (; i + 7 < n; i += 8) { + __m256 va = _mm256_loadu_ps(a + i); + __m256 vb = _mm256_loadu_ps(b + i); + _mm256_storeu_ps(out + i, _mm256_mul_ps(va, vb)); + } +#endif + for (; i < n; i++) + out[i] = a[i] * b[i]; +} + +void pn_vscale_f32(const float *a, float scale, float *out, int n) { + int i = 0; +#if defined(PN_SIMD_PATH_NEON) + float32x4_t vscale = vdupq_n_f32(scale); + for (; i + 3 < n; i += 4) { + float32x4_t va = vld1q_f32(a + i); + vst1q_f32(out + i, vmulq_f32(va, vscale)); + } +#elif defined(PN_SIMD_PATH_AVX2) + __m256 vscale = _mm256_set1_ps(scale); + for (; i + 7 < n; i += 8) { + __m256 va = _mm256_loadu_ps(a + i); + _mm256_storeu_ps(out + i, _mm256_mul_ps(va, vscale)); + } +#endif + for (; i < n; i++) + out[i] = a[i] * scale; +} + +float pn_vsum_f32(const float *a, int n) { + int i = 0; + float sum; +#if defined(PN_SIMD_PATH_NEON) + float32x4_t acc = vdupq_n_f32(0.0f); + for (; i + 3 < n; i += 4) + acc = vaddq_f32(acc, vld1q_f32(a + i)); + sum = vaddvq_f32(acc); +#elif defined(PN_SIMD_PATH_AVX2) + __m256 acc = _mm256_setzero_ps(); + for (; i + 7 < n; i += 8) + acc = _mm256_add_ps(acc, _mm256_loadu_ps(a + i)); + sum = avx2_horizontal_sum(acc); +#else + sum = 0.0f; +#endif + for (; i < n; i++) + sum += a[i]; + return sum; +} + +/* Plain loop on every path: the compiler vectorizes it (and, under -ffast-math, reorders + * the arithmetic); hand-written SIMD here would change the output bits. */ +void pn_vsilu_mul_f32(const float *gate, const float *up, float *out, int n) { + for (int i = 0; i < n; i++) { + float x = gate[i]; + float s = x / (1.0f + expf(-x)); + out[i] = s * up[i]; + } +} + +void pn_vrms_norm_f32(const float *x, const float *w, float *out, int n, float eps) { + /* Double accumulation: callers rely on its precision over long rows. */ + double ss = 0.0; + for (int k = 0; k < n; k++) + ss += (double)x[k] * (double)x[k]; + float r = (float)(1.0 / sqrt(ss / (double)n + (double)eps)); + + int i = 0; +#if defined(PN_SIMD_PATH_NEON) + float32x4_t vr = vdupq_n_f32(r); + for (; i + 3 < n; i += 4) { + float32x4_t vx = vld1q_f32(x + i); + float32x4_t vw = vld1q_f32(w + i); + vst1q_f32(out + i, vmulq_f32(vmulq_f32(vx, vr), vw)); + } +#elif defined(PN_SIMD_PATH_AVX2) + __m256 vr = _mm256_set1_ps(r); + for (; i + 7 < n; i += 8) { + __m256 vx = _mm256_loadu_ps(x + i); + __m256 vw = _mm256_loadu_ps(w + i); + _mm256_storeu_ps(out + i, _mm256_mul_ps(_mm256_mul_ps(vx, vr), vw)); + } +#endif + for (; i < n; i++) + out[i] = x[i] * r * w[i]; +} + +float pn_dot_q4_0_f32(const void *block, const float *vec) { + const pn_block_q4_0 *blk = (const pn_block_q4_0 *)block; + float df = pn_half_to_float(blk->d); +#if defined(PN_SIMD_PATH_NEON) + float32x4_t acc0 = vdupq_n_f32(0.0f); + float32x4_t acc1 = vdupq_n_f32(0.0f); + float32x4_t vdf = vdupq_n_f32(df); + float32x4_t voffset = vdupq_n_f32(8.0f); + for (int j = 0; j < 16; j += 4) { + float lo[4]; + float hi[4]; + for (int k = 0; k < 4; k++) { + lo[k] = (float)(blk->qs[j + k] & 0x0F); + hi[k] = (float)(blk->qs[j + k] >> 4); + } + float32x4_t vlo = vmulq_f32(vsubq_f32(vld1q_f32(lo), voffset), vdf); + float32x4_t vhi = vmulq_f32(vsubq_f32(vld1q_f32(hi), voffset), vdf); + acc0 = vfmaq_f32(acc0, vlo, vld1q_f32(vec + j)); + acc1 = vfmaq_f32(acc1, vhi, vld1q_f32(vec + j + 16)); + } + return vaddvq_f32(vaddq_f32(acc0, acc1)); +#else + float sum = 0.0f; + for (int j = 0; j < 16; j++) { + int lo = blk->qs[j] & 0x0F; + int hi = blk->qs[j] >> 4; + sum += df * (float)(lo - 8) * vec[j]; + sum += df * (float)(hi - 8) * vec[j + 16]; + } + return sum; +#endif +} + +float pn_dot_q8_0_f32(const void *block, const float *vec) { + const pn_block_q8_0 *blk = (const pn_block_q8_0 *)block; + float df = pn_half_to_float(blk->d); +#if defined(PN_SIMD_PATH_NEON) + float32x4_t acc0 = vdupq_n_f32(0.0f); + float32x4_t acc1 = vdupq_n_f32(0.0f); + for (int j = 0; j < 32; j += 8) { + int16x8_t q16 = vmovl_s8(vld1_s8(blk->qs + j)); + float32x4_t q_lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(q16))); + float32x4_t q_hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(q16))); + acc0 = vfmaq_f32(acc0, q_lo, vld1q_f32(vec + j)); + acc1 = vfmaq_f32(acc1, q_hi, vld1q_f32(vec + j + 4)); + } + return df * vaddvq_f32(vaddq_f32(acc0, acc1)); +#else + float sum = 0.0f; + for (int j = 0; j < 32; j++) + sum += (float)blk->qs[j] * vec[j]; + sum *= df; + return sum; +#endif +} diff --git a/src/accel/simd.h b/src/accel/simd.h new file mode 100644 index 0000000..4693fa9 --- /dev/null +++ b/src/accel/simd.h @@ -0,0 +1,151 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file simd.h + * @brief CPU vector kernels: dot products, element-wise ops, RMS norm, Q4_0/Q8_0 block dots. + * + * Written from a behaviour specification (plans/cleanroom_r2_simd_spec.md); no ggml or + * llama.cpp source was consulted. + * + * One code path is chosen at compile time: NEON (arm64), AVX2+FMA (x86 built with both), + * otherwise plain C that the compiler may auto-vectorize. There is no runtime dispatch. + * + * Common contract for every function taking `n`: for n <= 0 nothing is read or written and + * reductions return +0.0f. Pointers need only float (4-byte) alignment. In-place calls are + * supported: out == a, out == b, out == gate, out == up, out == x, w == x, a == b. + * No NULL checks: a NULL pointer with n > 0 crashes. + * + * @status None + * @issues None + * @todo None + */ +#ifndef PN_SIMD_H +#define PN_SIMD_H + +#include + +/** + * Dot product of two float vectors. + * + * @param a First vector, n floats + * @param b Second vector, n floats + * @param n Element count + * @return Sum of a[i] * b[i]; +0.0f when n <= 0 + * + * Thread-safe: yes (pure) + * Reentrant: yes + * Signal-safe: yes + */ +float pn_dot_f32(const float *a, const float *b, int n); + +/** + * Element-wise add: out[i] = a[i] + b[i]. + * + * @param a First input, n floats + * @param b Second input, n floats + * @param out Output, n floats (may equal a or b) + * @param n Element count + * + * Thread-safe: yes (pure apart from writing out) + * Reentrant: yes + * Signal-safe: yes + */ +void pn_vadd_f32(const float *a, const float *b, float *out, int n); + +/** + * Element-wise multiply: out[i] = a[i] * b[i]. + * + * @param a First input, n floats + * @param b Second input, n floats + * @param out Output, n floats (may equal a or b) + * @param n Element count + * + * Thread-safe: yes (pure apart from writing out) + * Reentrant: yes + * Signal-safe: yes + */ +void pn_vmul_f32(const float *a, const float *b, float *out, int n); + +/** + * SwiGLU combine: out[i] = silu(gate[i]) * up[i], silu(x) = x / (1 + expf(-x)). + * + * @param gate Gate activations, n floats + * @param up Up projections, n floats + * @param out Output, n floats (may equal gate or up) + * @param n Element count + * + * Thread-safe: yes (pure apart from writing out) + * Reentrant: yes + * Signal-safe: yes (libm expf only) + */ +void pn_vsilu_mul_f32(const float *gate, const float *up, float *out, int n); + +/** + * RMS norm: out[i] = x[i] * r * w[i], r = 1 / sqrt(mean(x^2) + eps), mean taken in double. + * + * @param x Input, n floats + * @param w Per-element weight, n floats (may equal x) + * @param out Output, n floats (may equal x) + * @param n Element count + * @param eps Added to the mean square before the square root; not clamped + * + * Thread-safe: yes (pure apart from writing out) + * Reentrant: yes + * Signal-safe: yes (libm sqrt only) + */ +void pn_vrms_norm_f32(const float *x, const float *w, float *out, int n, float eps); + +/** + * Dot of one Q4_0 block (32 elements, 18 bytes) with 32 floats. + * + * @param block Q4_0 block (pn_block_q4_0); needs 2-byte alignment + * @param vec 32 floats + * @return Sum over j of (code_j - 8) * d * vec[j] + * + * Thread-safe: yes (pure) + * Reentrant: yes + * Signal-safe: yes + */ +float pn_dot_q4_0_f32(const void *block, const float *vec); + +/** + * Dot of one Q8_0 block (32 elements, 34 bytes) with 32 floats. + * + * @param block Q8_0 block (pn_block_q8_0); needs 2-byte alignment + * @param vec 32 floats + * @return d * sum over j of qs[j] * vec[j] + * + * Thread-safe: yes (pure) + * Reentrant: yes + * Signal-safe: yes + */ +float pn_dot_q8_0_f32(const void *block, const float *vec); + +/** + * Scale a vector: out[i] = a[i] * scale. + * + * @param a Input, n floats + * @param scale Multiplier + * @param out Output, n floats (may equal a) + * @param n Element count + * + * Thread-safe: yes (pure apart from writing out) + * Reentrant: yes + * Signal-safe: yes + */ +void pn_vscale_f32(const float *a, float scale, float *out, int n); + +/** + * Sum of a float vector. + * + * @param a Input, n floats + * @param n Element count + * @return Sum of a[i]; +0.0f when n <= 0 + * + * Thread-safe: yes (pure) + * Reentrant: yes + * Signal-safe: yes + */ +float pn_vsum_f32(const float *a, int n); + +#endif /* PN_SIMD_H */ diff --git a/src/accel/tt/pn_tt_attention.c b/src/accel/tt/pn_tt_attention.c new file mode 100644 index 0000000..e7b88ee --- /dev/null +++ b/src/accel/tt/pn_tt_attention.c @@ -0,0 +1,1659 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_attention.c + * @brief The three ops that have to move together — NeoX RoPE, the KV-cache append and + * flash-decode attention — on three more resident cores, with the KV cache itself + * living in card DRAM. + * + * pn_tt_device.c owns the card and the matmul, pn_tt_oncard.c owns the norm / + * elementwise / sampling residencies, and this file owns the attention triple and the + * card-resident KV regions. The public API is in pn_tt_device.h. + * + * --------------------------------------------------------------------------- + * WHY ALL THREE, AS A NUMBER + * --------------------------------------------------------------------------- + * With the norms, the SwiGLU, the residual adds and the LM head's top-N already on the card, + * the ONLY activations still crossing the link are q, k and v — because the KV append and + * the GQA softmax are host arithmetic and need all three in host memory. For MiniCPM5-1B + * that is 16x128 + 2x128 + 2x128 floats a layer, moved as logical row 0 of 64 + 8 + 8 tiles + * at 128 B a tile (the 64-byte card->host read alignment makes row 0 cost 2 x 64 B, of which + * half is discarded — ft_tt_tile_row0.h): + * + * (64 + 8 + 8) tiles x 128 B = 10,240 B a layer x 24 layers = 245,760 B a token + * 245,760 B / 4.7 MB/s = 52.3 ms + * + * against a measured readback phase of 0.052 s. **The readback IS q/k/v and nothing else.** + * Moving any ONE of the three removes nothing, because whichever is left still needs them + * host-side. Moving all three removes the phase. + * + * --------------------------------------------------------------------------- + * THE ROW-PHASE PROBLEM, AND WHY THIS FORWARD STAYS AT TILE ROW 0 + * --------------------------------------------------------------------------- + * foundry's RoPE kernel multiplies activation tile row `r` by table tile row `r`, and the + * table is built for positions `first_position + 32*b + r`. The contract those ops were + * written against therefore puts a decode token at tile row `position % 32` and carries that + * phase through the whole forward (ft_tt_op_memory.h §1.5b). + * + * **Pantheon's TT forward puts every token at row 0 and this file keeps it there.** Adopting + * the row phase would mean threading a row index through the activation upload, the download, + * the top-N selection and every strip's zero-padding invariant — a change to five ops to + * serve one. The alternative is arithmetically identical and costs one small write: + * + * **ONE row block of the rotary table lives in DRAM and its ROW 0 is rebuilt per token.** + * The block is `2 * rot_tiles` tiles (16 KiB for this model) and only row 0 is ever read, + * so the per-token write is `2 * rot_tiles * 32` bf16 values through the row-0 writer — + * **512 B, ~1.8 us at 287 MB/s** — plus 2 x rot host trig evaluations in double. + * `first_position` is then passed as 0, because the operand IS this call's row block; the + * driver's own `ft_tt_op_rope_table_offset()` must not add a second offset on top. + * + * The 67.1 MB whole-context table foundry's header describes is the PREFILL answer and is + * still the right one there. It is not the decode answer, because decode needs one row. + * + * Rows 1-31 of the table stay zero, so RoPE drives rows 1-31 of its output to zero + * (`out = x*COS + x_partner*SIN` with both factors zero). That is the same strip invariant + * pn_tt_oncard.c states, preserved rather than assumed. + * + * --------------------------------------------------------------------------- + * WHERE THE KV CACHE LIVES, AND WHAT IT DOES TO SESSION EXPORT + * --------------------------------------------------------------------------- + * See pn_tt_device.h for the decision and the arithmetic. In one line: the mirror costs 2x + * the bytes of the export it would make free, and it charges every session — so the cache + * moves to the card and export pays at export time. + * + * @status RoPE, KV append and flash decode wired into the forward behind + * `PN_TT_ONCARD=...,attention`. Opt-in only: `all` does not select it, because it + * changes the accumulation order and the home of the KV cache rather than the speed + * of an op. + * @issues The card holds ONE K/V region set. A new sequence rebinds it (stale contents are + * harmless — every position a sequence reads is one it wrote); two sequences + * ALTERNATING would overwrite each other, and that is detected to a depth of + * PN_TT_KV_DISPLACED_KEYS and refused by name beyond nothing deeper. + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_device_internal.h" + +#include +#include +#include +#include + +#include "tt/convert/ft_float_convert.h" +#include "tt/ft_tt_op_attention.h" +#include "tt/ft_tt_op_dispatch.h" +#include "tt/ft_tt_op_memory.h" +#include "tt/ft_tt_op_rope.h" +#include "tt/ft_tt_tile_row0.h" +#include "tt/ft_tt_tilize.h" + +/** The three program directories this file needs beside the matmul and on-card ones. */ +#define PN_TT_ROPE_PROGRAM "rope" +#define PN_TT_MEMORY_PROGRAM "memory" +#define PN_TT_ATTENTION_PROGRAM "attention" + +/** + * Which enabled Tensix each of the three drives. + * + * **82-84 AND NOT 75-77, WHICH IS WHERE THE OBVIOUS CHOICE COLLIDES.** The matmul group puts + * members 1..N-1 at `PN_TT_MATMUL_CORE_FIRST` = 75 upward, so a group of 8 already owns + * 75-81; core 71 is member 0 and 72-74 are the reduce, elementwise and sampling residencies. + * Booting a second program's firmware onto a core that already holds a resident triad is a + * wedged core, not a configuration — so these start past the widest group the device can be + * asked for, and `pn_tt_attention_cores_are_free()` checks it rather than trusting the + * arithmetic. + */ +#define PN_TT_CORE_ROPE_ENV "PN_TT_CORE_ROPE" +#define PN_TT_CORE_MEMORY_ENV "PN_TT_CORE_MEMORY" +#define PN_TT_CORE_ATTENTION_ENV "PN_TT_CORE_ATTENTION" +#define PN_TT_CORE_ROPE_DEFAULT 82u +#define PN_TT_CORE_MEMORY_DEFAULT 83u +#define PN_TT_CORE_ATTENTION_DEFAULT 84u + +/** THE PREFILL CHUNK MIXER'S HEAD SPLIT. `PN_TT_ATTN_CORES` members, member 0 the `attention` + * residency itself, the rest from `PN_TT_ATTN_MEMBER_CORE_FIRST` upward. 75..81 are free + * since the matmul group moved to 96. The count shrinks until it divides the Q heads. */ +#define PN_TT_ATTN_CORES_ENV "PN_TT_ATTN_CORES" +/* EIGHT since 2026-10-04: attention 0.78 -> 0.41 -> 0.23 -> 0.15 s of a prefill at + * 1/2/4/8 cores, 246.7 -> 273.7 tok/s, every arm IDENTICAL, `attn_group` red DIFFERS. */ +#define PN_TT_ATTN_CORES_DEFAULT 8u +#define PN_TT_ATTN_MEMBER_CORE_FIRST_ENV "PN_TT_ATTN_MEMBER_CORE_FIRST" +#define PN_TT_ATTN_MEMBER_CORE_DEFAULT 75u + +/** Position tiles per K/V block — the flash-decode prefetch depth, fixed by the RESIDENCY. + * + * 16 is the point every recorded attention figure was measured at (0.840 ms a descriptor at + * 8k KV, L1 flat in context at 413,696 B). It is a knob because the PADDED read window is + * `2 * block_tiles` position tiles whatever the live length is, so at short context a large + * block is work the answer does not need — and that trade is a measurement, not a guess. */ +#define PN_TT_ATTN_BLOCK_TILES_ENV "PN_TT_ATTN_BLOCK_TILES" +#define PN_TT_ATTN_BLOCK_TILES_DEFAULT 16u + +/** The rotary table's row block is rebuilt per token; this says none is current yet. */ +#define PN_TT_ROPE_NO_POSITION UINT32_MAX + +/* ================================================================ + * Arming + * ================================================================ */ + +/** The rotary width this model rotates, resolved exactly as the CPU oracle resolves it. */ +static uint32_t pn_tt_rotary_width(const pn_tt_device *device) +{ + return ft_tt_op_rope_rotary_width(device->attn_head_dim, device->attn_rope_dim); +} + +/** Tiles across one rotary table row block: `rot_tiles` cos tiles then `rot_tiles` sin. */ +static uint32_t pn_tt_rope_table_tiles(const pn_tt_device *device) +{ + return 2u * (pn_tt_rotary_width(device) / PN_TT_TILE_DIM); +} + +/** + * Allocate `bytes` of card DRAM, zeroed, round-robin across banks. + * + * Zeroing is not tidiness here either: an attention descriptor's PADDED read window reaches + * past the live sequence by up to `2*block_tiles - 1` position tiles, and those tiles are fed + * to the FPU before the tail mask kills their scores. Zero is a value the mask makes + * harmless; whatever DRAM happened to hold is not. + */ +static int pn_tt_dram_alloc_zeroed(pn_tt_device *device, size_t bytes, uint32_t *out_bank, + uint32_t *out_address) +{ + ft_tt_buffer buffer; + uint32_t bank; + + if (device->dram_bank_count == 0u) + return -1; + bank = device->strip_bank_cursor % device->dram_bank_count; + device->strip_bank_cursor++; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes, &buffer) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating %zu bytes for the attention family\n", + bytes); + return -1; + } + *out_bank = buffer.logical_bank; + *out_address = (uint32_t)buffer.address; + return pn_tt_zero_dram(device, *out_bank, *out_address, bytes); +} + +/** The flash-decode constants — reduce scaler, 1/sqrt(head_dim) and the 32 tail masks — + * built once per model and held. */ +static bool pn_tt_upload_attention_constants(pn_tt_device *device) +{ + size_t bytes = ft_tt_op_attention_constants_bytes(PN_TT_TILE_BF16); + unsigned char *constants = malloc(bytes); + bool ok; + + if (!constants) + return false; + if (ft_tt_op_attention_build_constants(device->attn_head_dim, PN_TT_TILE_BF16, constants, + bytes) != FT_TT_OK) { + free(constants); + return false; + } + ok = pn_tt_dram_alloc_zeroed(device, bytes, &device->attn_const_bank, + &device->attn_const_address) == 0 && + pn_tt_write_dram(device, device->attn_const_bank, device->attn_const_address, + constants, bytes) == 0; + free(constants); + return ok; +} + +/** One layer's K and V regions, identical geometry, on two banks so the append's write and + * the next layer's read are not usually the same endpoint. */ +static bool pn_tt_alloc_kv_regions(pn_tt_device *device, uint32_t layer_count) +{ + size_t region_bytes = (size_t)device->kv_layout.region_bytes; + uint32_t layer; + + device->kv_region = calloc(layer_count, sizeof(*device->kv_region)); + if (!device->kv_region) + return false; + device->kv_layer_count = layer_count; + for (layer = 0; layer < layer_count; layer++) { + pn_tt_kv_region *region = &device->kv_region[layer]; + + if (pn_tt_dram_alloc_zeroed(device, region_bytes, ®ion->k_bank, + ®ion->k_address) != 0 || + pn_tt_dram_alloc_zeroed(device, region_bytes, ®ion->v_bank, + ®ion->v_address) != 0) + return false; + } + return true; +} + +/** The RoPE residency. Its circular buffers are placed for the DEPTH, which has to divide + * both head_tiles and rot_tiles — and both q and k run the same shape, so one placement + * serves the whole forward. */ +static bool pn_tt_arm_rope(pn_tt_device *device) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_ROPE_CB_COUNT]; + ft_tt_op_rope_request request; + + memset(&request, 0, sizeof(request)); + request.row_blocks = 1u; + request.heads = device->attn_q_heads; + request.head_dim = device->attn_head_dim; + request.rope_dim = device->attn_rope_dim; + request.first_position = 0u; + request.prefetch_tiles = device->attn_dim_tiles; + request.input.tile_bytes = PN_TT_TILE_BF16; + request.table.tile_bytes = PN_TT_TILE_BF16; + request.output.tile_bytes = PN_TT_TILE_BF16; + request.input.address = 1u; /* placement only reads the tile sizes; a set operand is a + * precondition of the encoder, so give it one */ + request.table.address = 1u; + request.output.address = 1u; + + if (!pn_tt_prepare_family(device, &device->rope, PN_TT_ROPE_PROGRAM, "rope", + PN_TT_CORE_ROPE_ENV, PN_TT_CORE_ROPE_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_rope_place_cbs(&request, placed, device->rope.cb_buffer, + sizeof(device->rope.cb_buffer), &device->rope.cb_mask) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: the RoPE circular buffers do not fit for head_dim %u\n", + device->attn_head_dim); + pn_tt_residency_release(&device->rope); + return false; + } + return pn_tt_start_family(&device->rope); +} + +/** The memory residency, for KV_APPEND only. DM0|DM1: this family has NO compute thread. */ +static bool pn_tt_arm_memory(pn_tt_device *device) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_MEMORY_CB_COUNT]; + + if (!pn_tt_prepare_family(device, &device->memory, PN_TT_MEMORY_PROGRAM, "memory", + PN_TT_CORE_MEMORY_ENV, PN_TT_CORE_MEMORY_DEFAULT, + PN_TT_ONCARD_DM_MASK)) + return false; + /* The embedding group size is required to be non-zero even though this residency never + * runs an EMBEDDING descriptor: a zero places no CB and the kernel's grid guard would + * then refuse the opcode at run time for a build-time reason. It is `dim_tiles` and not + * the model's embedding width because nothing will ever push through it. */ + if (ft_tt_op_memory_place_cbs(device->attn_embed_tiles, device->attn_dim_tiles, + PN_TT_TILE_BF16, placed, device->memory.cb_buffer, + sizeof(device->memory.cb_buffer), &device->memory.cb_mask) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: the KV-append circular buffers do not fit at %u tiles a " + "group\n", device->attn_dim_tiles); + pn_tt_residency_release(&device->memory); + return false; + } + return pn_tt_start_family(&device->memory); +} + +/** The flash-decode residency. `block_tiles` is fixed here for its whole life — the kernel + * REFUSES a descriptor that disagrees with the buffers it was staged with. */ +static bool pn_tt_arm_flash_decode(pn_tt_device *device) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_ATTENTION_CB_COUNT]; + ft_tt_op_attention_residency residency; + + residency.q_rows = device->attn_q_rows; + residency.dim_tiles = device->attn_dim_tiles; + residency.block_tiles = device->attn_block_tiles; + residency.tile_bytes = PN_TT_TILE_BF16; + + if (!pn_tt_prepare_family(device, &device->attention, PN_TT_ATTENTION_PROGRAM, "attention", + PN_TT_CORE_ATTENTION_ENV, PN_TT_CORE_ATTENTION_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_attention_place_cbs(&residency, placed, device->attention.cb_buffer, + sizeof(device->attention.cb_buffer), + &device->attention.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the flash-decode circular buffers do not fit at block_tiles " + "%u (%zu B of arena)\n", device->attn_block_tiles, + ft_tt_op_attention_l1_bytes(&residency)); + pn_tt_residency_release(&device->attention); + return false; + } + return pn_tt_start_family(&device->attention); +} + +/** Arm ONE head-split member on `core`, with the flash-decode residency's own shape. */ +static bool pn_tt_arm_attention_member(pn_tt_device *device, pn_tt_residency *member, + uint32_t core) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_ATTENTION_CB_COUNT]; + ft_tt_op_attention_residency residency; + + residency.q_rows = device->attn_q_rows; + residency.dim_tiles = device->attn_dim_tiles; + residency.block_tiles = device->attn_block_tiles; + residency.tile_bytes = PN_TT_TILE_BF16; + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_ATTENTION_PROGRAM, + "attention-member", PN_TT_ATTN_MEMBER_CORE_FIRST_ENV, core, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_attention_place_cbs(&residency, placed, member->cb_buffer, + sizeof(member->cb_buffer), &member->cb_mask) != FT_TT_OK) { + pn_tt_residency_release(member); + return false; + } + return pn_tt_start_family(member); +} + +/** + * Arm the head-split members. A member that will not come up shrinks the split rather than + * failing attention — the split is a speed lever, member 0 alone is the shipped answer — and + * the count then shrinks further until it divides the Q heads, so every member owns the same + * number. Members past the legal count are STOPPED, not left resident. + */ +static void pn_tt_arm_attention_members(pn_tt_device *device) +{ + const uint32_t asked = pn_tt_env_u32(PN_TT_ATTN_CORES_ENV, PN_TT_ATTN_CORES_DEFAULT); + const uint32_t first = pn_tt_env_u32(PN_TT_ATTN_MEMBER_CORE_FIRST_ENV, + PN_TT_ATTN_MEMBER_CORE_DEFAULT); + uint32_t want = asked < 1u ? 1u : (asked > PN_TT_ATTN_MAX_MEMBERS ? PN_TT_ATTN_MAX_MEMBERS + : asked); + uint32_t up = 1u, armed, index; + + device->attention_members = 1u; + while (want > 1u && (device->attn_q_heads % want) != 0u) + want--; + for (index = 1u; index < want; index++) { + if (!pn_tt_arm_attention_member(device, &device->attention_member[index - 1u], + first + index - 1u)) + break; + up++; + } + armed = up; + while (armed > 1u && (device->attn_q_heads % armed) != 0u) + armed--; + for (index = armed; index < up; index++) + pn_tt_residency_release(&device->attention_member[index - 1u]); + device->attention_members = armed; + if (asked > 1u) + fprintf(stderr, "pn_tt: attention HEAD SPLIT — asked %u, running %u member(s) from core " + "%u (members 1.. on %u..), %u Q heads each\n", + asked, armed, device->attention.core_index, first, + device->attn_q_heads / armed); +} + +/** Retire and re-arm head-split member `index` (1-based) on its own core. */ +static bool pn_tt_attention_member_rearm(pn_tt_device *device, uint32_t index) +{ + pn_tt_residency *member = &device->attention_member[index - 1u]; + const uint32_t core = member->core_index; + + fprintf(stderr, "pn_tt: attention member %u refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", index, core); + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + device->oncard_restarts++; + if (!pn_tt_residency_retired(member)) { + fprintf(stderr, "pn_tt: attention member %u did not retire — REFUSING to arm a fresh " + "one on core %u\n", index, core); + return false; + } + return pn_tt_arm_attention_member(device, member, core); +} + +/** The largest block depth that both fits L1 and leaves the padded read window inside the + * cache — `2 * block_tiles` position tiles are fetched however short the sequence is. */ +static uint32_t pn_tt_choose_block_tiles(const pn_tt_device *device, uint32_t requested) +{ + uint32_t fits_l1 = ft_tt_op_attention_max_block_tiles(device->attn_q_rows, + device->attn_dim_tiles, + PN_TT_TILE_BF16); + uint32_t fits_cache = device->kv_layout.capacity_tiles / 2u; + uint32_t chosen = requested; + + if (fits_l1 != 0u && chosen > fits_l1) + chosen = fits_l1; + if (fits_cache != 0u && chosen > fits_cache) + chosen = fits_cache; + return chosen; +} + +/** + * Are the three cores this family wants held by nobody else? + * + * A core already running a resident triad is not merely busy: `pn_tt_residency_bind_core()` + * BOOTS firmware on it, and booting over five running processors is the wedge signature this + * project has paid for repeatedly. The claim is checked against the live handles rather than + * against the default arithmetic, because the defaults are all environment-overridable and an + * arithmetic check would agree with itself. + * + * @return true when all three are free; false with the clash named on stderr + */ +static bool pn_tt_attention_cores_are_free(const pn_tt_device *device) +{ + uint32_t wanted[3] = { pn_tt_env_u32(PN_TT_CORE_ROPE_ENV, PN_TT_CORE_ROPE_DEFAULT), + pn_tt_env_u32(PN_TT_CORE_MEMORY_ENV, PN_TT_CORE_MEMORY_DEFAULT), + pn_tt_env_u32(PN_TT_CORE_ATTENTION_ENV, + PN_TT_CORE_ATTENTION_DEFAULT) }; + static const char *const k_name[3] = { "rope", "memory", "attention" }; + uint32_t taken[PN_TT_MATMUL_MAX_CORES + 4u]; + uint32_t taken_count = 0u, index, member; + bool free_of_clashes = true; + + taken[taken_count++] = device->core_index; + for (member = 0; member < device->group.member_count; member++) + taken[taken_count++] = device->group.member[member].core_index; + if (device->reduce.core_booted) + taken[taken_count++] = device->reduce.core_index; + if (device->eltwise.core_booted) + taken[taken_count++] = device->eltwise.core_index; + if (device->sample.core_booted) + taken[taken_count++] = device->sample.core_index; + + for (index = 0; index < 3u; index++) { + uint32_t other; + + for (other = 0; other < taken_count; other++) { + if (wanted[index] != taken[other]) + continue; + fprintf(stderr, "pn_tt: REFUSED — the %s residency wants core %u and another " + "resident program already holds it\n", k_name[index], + wanted[index]); + free_of_clashes = false; + } + for (other = 0; other < index; other++) { + if (wanted[index] != wanted[other]) + continue; + fprintf(stderr, "pn_tt: REFUSED — the %s and %s residencies were both pointed at " + "core %u\n", k_name[other], k_name[index], wanted[index]); + free_of_clashes = false; + } + } + return free_of_clashes; +} + +int pn_tt_device_arm_attention(pn_tt_device *device, uint32_t q_heads, uint32_t kv_heads, + uint32_t head_dim, uint32_t rope_dim, double rope_theta, + uint32_t capacity_positions, uint32_t layer_count) +{ + uint32_t requested_block_tiles; + + if (!device || q_heads == 0u || kv_heads == 0u || head_dim == 0u || layer_count == 0u) + return -1; + if ((q_heads % kv_heads) != 0u || (head_dim % PN_TT_TILE_DIM) != 0u) + return -1; + if (!pn_tt_device_oncard_requested(device, PN_TT_ONCARD_ATTENTION)) + return 0; + + /* A REFUSAL, not a fallback. Attention was asked for by name; a missing program would + * otherwise leave it on the host with a different numerics and nothing saying so — the + * defect that cost a whole round on the matmul residency. */ + if (!pn_tt_program_is_present(PN_TT_ROPE_PROGRAM) || + !pn_tt_program_is_present(PN_TT_MEMORY_PROGRAM) || + !pn_tt_program_is_present(PN_TT_ATTENTION_PROGRAM)) { + fprintf(stderr, "pn_tt: REFUSED — on-card attention needs the `rope`, `memory` and " + "`attention` programs and at least one is missing from the program " + "root\n"); + return -1; + } + if (!pn_tt_attention_cores_are_free(device)) + return -1; + + device->attn_q_heads = q_heads; + device->attn_kv_heads = kv_heads; + device->attn_head_dim = head_dim; + device->attn_rope_dim = rope_dim; + device->attn_rope_theta = rope_theta; + device->attn_q_rows = q_heads / kv_heads; + device->attn_dim_tiles = head_dim / PN_TT_TILE_DIM; + device->attn_embed_tiles = device->attn_dim_tiles; + device->rope_table_position = PN_TT_ROPE_NO_POSITION; + device->rope_chunk1_first = UINT32_MAX; + + if (ft_tt_kv_cache_layout_init(kv_heads, head_dim, capacity_positions, PN_TT_TILE_BF16, + &device->kv_layout) != FT_TT_OK) { + fprintf(stderr, "pn_tt: no KV cache layout for %u heads x %u dim x %u positions\n", + kv_heads, head_dim, capacity_positions); + return -1; + } + requested_block_tiles = pn_tt_env_u32(PN_TT_ATTN_BLOCK_TILES_ENV, + PN_TT_ATTN_BLOCK_TILES_DEFAULT); + device->attn_block_tiles = pn_tt_choose_block_tiles(device, requested_block_tiles); + if (device->attn_block_tiles == 0u) { + fprintf(stderr, "pn_tt: REFUSED — a %u-position cache cannot hold one flash-decode " + "read window\n", device->kv_layout.capacity_positions); + return -1; + } + if (device->attn_block_tiles != requested_block_tiles) + fprintf(stderr, "pn_tt: flash-decode block_tiles %u -> %u (L1 and the %u-tile cache " + "capacity both bound it)\n", requested_block_tiles, + device->attn_block_tiles, device->kv_layout.capacity_tiles); + + if (!pn_tt_upload_attention_constants(device) || + pn_tt_dram_alloc_zeroed(device, (size_t)pn_tt_rope_table_tiles(device) * PN_TT_TILE_BF16, + &device->rope_table_bank, &device->rope_table_address) != 0 || + pn_tt_dram_alloc_zeroed(device, (size_t)pn_tt_rope_table_tiles(device) * PN_TT_TILE_BF16, + &device->rope_chunk1_bank, &device->rope_chunk1_address) != 0 || + !pn_tt_alloc_kv_regions(device, layer_count)) { + fprintf(stderr, "pn_tt: REFUSED — the attention family's DRAM did not allocate\n"); + return -1; + } + + device->oncard[PN_TT_ONCARD_ATTENTION] = + pn_tt_arm_rope(device) && pn_tt_arm_memory(device) && pn_tt_arm_flash_decode(device); + if (device->oncard[PN_TT_ONCARD_ATTENTION]) + pn_tt_arm_attention_members(device); + if (!device->oncard[PN_TT_ONCARD_ATTENTION]) { + fprintf(stderr, "pn_tt: REFUSED — on-card attention was asked for and one of its " + "three residencies did not arm\n"); + return -1; + } + fprintf(stderr, "pn_tt: attention on the card — %u Q heads / %u KV groups of %u rows, " + "head_dim %u, block_tiles %u, KV cache %u positions x %u layers " + "(%.1f MiB)\n", + q_heads, kv_heads, device->attn_q_rows, head_dim, device->attn_block_tiles, + device->kv_layout.capacity_positions, layer_count, + (double)device->kv_layout.region_bytes * 2.0 * (double)layer_count / + (1024.0 * 1024.0)); + return 0; +} + +/* ================================================================ + * Binding the ONE region set to a sequence + * ================================================================ */ + +int pn_tt_kv_bind(pn_tt_device *device, const void *sequence_key) +{ + uint32_t index; + + if (!device || !sequence_key) + return -1; + if (device->kv_key == sequence_key) + return 0; + for (index = 0; index < device->kv_displaced_count; index++) { + if (device->kv_displaced[index] != sequence_key) + continue; + /* This key held the region set, lost it, and is back. That is two sequences + * alternating on ONE cache, which would overwrite each other's positions and produce + * a plausible wrong answer with no error anywhere. */ + fprintf(stderr, "pn_tt: REFUSED — two sequences are alternating on the card's one KV " + "region set. On-card attention serves ONE sequence at a time.\n"); + return -1; + } + if (device->kv_key) { + if (device->kv_displaced_count < PN_TT_KV_DISPLACED_KEYS) + device->kv_displaced[device->kv_displaced_count++] = device->kv_key; + else + device->kv_displaced[device->kv_rebinds % PN_TT_KV_DISPLACED_KEYS] = device->kv_key; + device->kv_rebinds++; + fprintf(stderr, "pn_tt: the card KV region set rebound to a new sequence (%llu so " + "far); the previous one's positions are now stale\n", + (unsigned long long)device->kv_rebinds); + } + device->kv_key = sequence_key; + return 0; +} + +/* ================================================================ + * Re-arming, and one submit that survives an idle retirement + * ================================================================ */ + +/** Retire a stranded attention-family residency by ASKING and arm a fresh one on the same + * core from the shapes it was armed with. Same mechanism, same reason, as + * pn_tt_oncard.c's: a residency is BOUNDED and a serve idles between requests. */ +static bool pn_tt_attention_rearm(pn_tt_device *device, pn_tt_residency *residency) +{ + fprintf(stderr, "pn_tt: the %s residency refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", residency->name, + residency->core_index); + pn_tt_residency_stop(residency); + pn_tt_residency_release(residency); + device->oncard_restarts++; + + if (residency == &device->rope) + return pn_tt_arm_rope(device); + if (residency == &device->memory) + return pn_tt_arm_memory(device); + if (residency == &device->attention) + return pn_tt_arm_flash_decode(device); + return false; +} + +/** + * Submit one descriptor, re-arming once if the residency had retired underneath it. + * + * Every parameter block in this file addresses DRAM only — none of the three families writes + * an L1 address into its words — so the SAME words are valid against a fresh residency and + * are reused rather than re-encoded. (The sampling family is the exception, and it re-encodes + * for exactly that reason.) + */ +static int pn_tt_attention_submit(pn_tt_device *device, pn_tt_residency *residency, + uint32_t opcode, const uint32_t *words) +{ + double started = pn_tt_now_seconds(); + + if (pn_tt_residency_submit(residency, opcode, words, PN_TT_OP_TIMEOUT_MS) != 0) { + if (!pn_tt_attention_rearm(device, residency) || + pn_tt_residency_submit(residency, opcode, words, PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + device->oncard_seconds += pn_tt_now_seconds() - started; + device->oncard_ops++; + return 0; +} + +/* ================================================================ + * RoPE + * ================================================================ */ + +/** + * Rebuild row 0 of the rotary table's one row block for `position`. + * + * Per row block the layout is `rot_tiles` cos tiles then `rot_tiles` SIGNED-sin tiles: the + * sign is in the table, which is what makes every rotated tile the same three FPU passes + * with no branch (ft_tt_op_rope.h). Column c below the half carries -sin, at or above it + * +sin, and both halves share the angle of `c mod half`. + * + * The angle is computed in DOUBLE by ft_tt_op_rope_angle() — `position * freq` reaches ~1e5 + * radians at this model's context and neither bf16 nor the SFPU can reduce that. + */ +static int pn_tt_rope_table_for_position(pn_tt_device *device, uint32_t position) +{ + const uint32_t rot = pn_tt_rotary_width(device); + const uint32_t half = rot / 2u; + const uint32_t tiles = pn_tt_rope_table_tiles(device); + uint16_t *row; + uint32_t column; + int rc; + + if (device->rope_table_position == position) + return 0; + /* A CHUNK LEFT REAL ANGLES IN ROWS 1..31 AND THIS WRITE ONLY TOUCHES ROW 0. Decode's + * activations have those rows zero, so the OUTPUT would be zero either way — but the + * invariant this file states is about the TABLE, and an invariant that is true for a + * different reason than the one written down is one nobody can rely on next time. */ + if (device->rope_table_is_chunk) { + if (pn_tt_zero_dram(device, device->rope_table_bank, device->rope_table_address, + (size_t)tiles * PN_TT_TILE_BF16) != 0) + return -1; + device->rope_table_is_chunk = false; + } + row = calloc((size_t)tiles * PN_TT_TILE_DIM, sizeof(*row)); + if (!row) + return -1; + for (column = 0; column < rot; column++) { + /* RED PROOF: every token is rotated as if it were position 0 — the failure a table + * that did not follow the token would produce, and one no length or address check + * can see. */ + uint32_t angle_position = pn_tt_red("rope_position") ? 0u : position; + /* RED PROOF: the frequency ladder is REFLECTED, so slot p is rotated at the model's + * pair `half - 1 - p` while the partner still sits at `+half`. This is the exact shape + * of the "the on-card rotary re-pairs what the artifact un-paired" hypothesis, and it + * is here so `PN_TT_ROPE_CHECK` is proved able to SEE that bug rather than assumed to. + * `Q.K` is invariant under it, so the attention output barely moves — which is why an + * attn-check cannot be the instrument for this question. */ + uint32_t angle_column = pn_tt_red("rope_freq_reflect") + ? (half - 1u - (column % half)) + (column >= half ? half : 0u) + : column; + double angle = ft_tt_op_rope_angle(angle_position, angle_column, rot, + device->attn_rope_theta); + double sine = sin(angle); + + row[column] = ft_bf16_from_float((float)cos(angle)); + row[rot + column] = ft_bf16_from_float((float)(column < half ? -sine : sine)); + } + rc = ft_tt_tile_row0_write(&device->runtime.bulk_dram, + (uint16_t)device->dram[device->rope_table_bank].x, + (uint16_t)device->dram[device->rope_table_bank].y, + device->rope_table_address, tiles, row); + free(row); + if (rc != FT_TT_OK) + return -1; + device->bytes_written += (size_t)tiles * FT_TT_TILE_ROW0_CHUNKS * FT_TT_TILE_ROW0_HALF_BYTES; + device->rope_table_position = position; + return 0; +} + +/** + * Rebuild the WHOLE 32-row rotary block for positions `first .. first+31`. + * + * THE SHIPPED TABLE BUILDER, ASKED FOR ONE ROW BLOCK. `ft_tt_op_rope_build_table()` is the + * prefill answer the RoPE header describes — it tilizes with `ft_tt_tilize_strip()`, the + * same face map the activations use, which is what a table whose value varies along BOTH + * axes needs. Calling it for 32 positions rather than 131,072 costs 16 KiB of upload a + * chunk (~50 us) and reuses code that is already gated, instead of a second copy of the + * angle arithmetic beside the row-0 one. + * + * `first` must be a multiple of 32 — the ROW-PHASE CONTRACT: row r of the block carries + * position `first + r`, and the chunk's row r carries the same. The builder refuses + * anything else. + */ +static int pn_tt_rope_table_build_chunk(pn_tt_device *device, uint32_t first); +static int pn_tt_rope_table_build_chunk1(pn_tt_device *device, uint32_t first); + +/** The chunk's rotary table, with its host time booked (G0's "unclocked" split). */ +static int pn_tt_rope_table_for_chunk(pn_tt_device *device, uint32_t first, + uint32_t *out_bank, uint32_t *out_address) +{ + const double started = pn_tt_now_seconds(); + int rc; + + /* ODD CHUNKS USE THE CHUNK-ONLY TABLE. With two bands a pass rotates positions p and p+32 + * in every layer, alternately, and one cached block evicted the other each time (0.156 s at + * two bands, measured). Slot 1 is never touched by decode, so its key alone decides. */ + { + const uint32_t chunk = first / PN_TT_TILE_DIM; + const uint32_t slot = chunk % 64u; + + /* PHASE G: chunks 2.. of a tall pass — one cached block each. Chunk 0/1 keep their + * shipped slots, so a 2-band prefill and decode are untouched. */ + if (chunk >= 2u && (device->strip_tile_rows > 4u || device->prefill_tile_rows > 4u) && + !pn_tt_red("rope_one_slot")) { + if (device->rope_many_address[slot] == 0u && + pn_tt_dram_alloc_zeroed(device, + ft_tt_op_rope_table_bytes( + PN_TT_TILE_DIM, + pn_tt_rotary_width(device) / PN_TT_TILE_DIM, + PN_TT_TILE_BF16), + &device->rope_many_bank[slot], + &device->rope_many_address[slot]) != 0) + return -1; + *out_bank = device->rope_many_bank[slot]; + *out_address = device->rope_many_address[slot]; + if (device->rope_many_first[slot] == first + 1u && !pn_tt_red("rope_nocache")) + return 0; + { + const size_t bytes = ft_tt_op_rope_table_bytes( + PN_TT_TILE_DIM, pn_tt_rotary_width(device) / PN_TT_TILE_DIM, + PN_TT_TILE_BF16); + void *block = malloc(bytes); + + rc = block && ft_tt_op_rope_build_table(first, PN_TT_TILE_DIM, + device->attn_head_dim, + device->attn_rope_dim, + device->attn_rope_theta, block, + bytes) == FT_TT_OK + ? pn_tt_write_dram(device, *out_bank, *out_address, block, bytes) + : -1; + free(block); + } + /* first + 1 so a zeroed field (0) never matches chunk 0's key. */ + device->rope_many_first[slot] = rc == 0 ? first + 1u : 0u; + pn_tt_device_host_clock_add(device, PN_TT_HOST_CLOCK_ROPE_TABLE, + pn_tt_now_seconds() - started); + return rc; + } + } + if (((first / PN_TT_TILE_DIM) & 1u) != 0u && !pn_tt_red("rope_one_slot")) { + *out_bank = device->rope_chunk1_bank; + *out_address = device->rope_chunk1_address; + if (device->rope_chunk1_first == first && !pn_tt_red("rope_nocache")) + return 0; + rc = pn_tt_rope_table_build_chunk1(device, first); + device->rope_chunk1_first = rc == 0 ? first : UINT32_MAX; + pn_tt_device_host_clock_add(device, PN_TT_HOST_CLOCK_ROPE_TABLE, + pn_tt_now_seconds() - started); + return rc; + } + *out_bank = device->rope_table_bank; + *out_address = device->rope_table_address; + + /* THE CHUNK BLOCK IS CACHED BY ITS FIRST POSITION. Every attention layer of a pass — and + * both its q and its k — rotate the SAME 32 positions, so the block was being built in + * double precision and re-uploaded 16 times a pass for identical bytes: 0.316 s of a + * prefill, measured by the host clock below. A decode token (or `PN_TT_RED=rope_nocache`) + * clears `rope_table_is_chunk`, so a stale block cannot survive a mode change. */ + /* RED PROOF, `PN_TT_RED=rope_cache_any`: the key ignores the position, so passes 1.. reuse + * pass 0's angles — must DIFFER, the proof the key is load-bearing. */ + if (device->rope_table_is_chunk && + (device->rope_table_chunk_first == first || pn_tt_red("rope_cache_any")) && + !pn_tt_red("rope_nocache")) + return 0; + rc = pn_tt_rope_table_build_chunk(device, first); + if (rc == 0) + device->rope_table_chunk_first = first; + + pn_tt_device_host_clock_add(device, PN_TT_HOST_CLOCK_ROPE_TABLE, + pn_tt_now_seconds() - started); + return rc; +} + +static int pn_tt_rope_table_build_chunk(pn_tt_device *device, uint32_t first) +{ + const uint32_t rot_tiles = pn_tt_rotary_width(device) / PN_TT_TILE_DIM; + const size_t bytes = + ft_tt_op_rope_table_bytes(PN_TT_TILE_DIM, rot_tiles, PN_TT_TILE_BF16); + void *block; + int rc; + + if (bytes == 0u || bytes != (size_t)pn_tt_rope_table_tiles(device) * PN_TT_TILE_BF16) + return -1; + block = malloc(bytes); + if (!block) + return -1; + if (ft_tt_op_rope_build_table(first, PN_TT_TILE_DIM, device->attn_head_dim, + device->attn_rope_dim, device->attn_rope_theta, block, + bytes) != FT_TT_OK) { + free(block); + fprintf(stderr, "pn_tt: no rotary block for positions %u..%u\n", first, + first + PN_TT_TILE_DIM - 1u); + return -1; + } + rc = pn_tt_write_dram(device, device->rope_table_bank, device->rope_table_address, block, + bytes); + free(block); + if (rc != 0) + return -1; + /* THE ROW-0 CACHE IS NOW VOID, AND SO IS THE ROWS-1..31-ARE-ZERO INVARIANT. Decode's + * next token has to rebuild the block, not merely its row 0 — see + * pn_tt_rope_table_for_position(). */ + device->rope_table_position = PN_TT_ROPE_NO_POSITION; + device->rope_table_is_chunk = true; + return 0; +} + +/** Build the chunk block for `first` and upload it to the chunk-only table (slot 1). */ +static int pn_tt_rope_table_build_chunk1(pn_tt_device *device, uint32_t first) +{ + const uint32_t rot_tiles = pn_tt_rotary_width(device) / PN_TT_TILE_DIM; + const size_t bytes = + ft_tt_op_rope_table_bytes(PN_TT_TILE_DIM, rot_tiles, PN_TT_TILE_BF16); + void *block = malloc(bytes); + int rc; + + if (!block) + return -1; + rc = ft_tt_op_rope_build_table(first, PN_TT_TILE_DIM, device->attn_head_dim, + device->attn_rope_dim, device->attn_rope_theta, block, + bytes) == FT_TT_OK + ? pn_tt_write_dram(device, device->rope_chunk1_bank, device->rope_chunk1_address, + block, bytes) + : -1; + free(block); + return rc; +} + +int pn_tt_rope_chunk(pn_tt_device *device, const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t heads, + uint32_t first_position, uint32_t rows) +{ + ft_tt_op_rope_request request; + uint32_t words[FT_TT_OP_ROPE_WORDS]; + + if (!device || !input || !output || heads == 0u || !device->rope.running) + return -1; + if (rows == 0u || rows > PN_TT_TILE_DIM || (first_position % PN_TT_TILE_DIM) != 0u) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (input->tile_count < heads * device->attn_dim_tiles || + output->tile_count < heads * device->attn_dim_tiles) + return -1; + uint32_t table_bank = 0u, table_address = 0u; + + if (pn_tt_rope_table_for_chunk(device, first_position, &table_bank, &table_address) != 0) + return -1; + + memset(&request, 0, sizeof(request)); + request.row_blocks = 1u; + request.heads = heads; + request.head_dim = device->attn_head_dim; + request.rope_dim = device->attn_rope_dim; + /* ZERO for the same reason as decode's: the operand IS this call's row block, whose + * row r already carries `first_position + r`. `rows` is not on the wire at all — the + * kernel rotates all 32 and the strip's rows `rows`..31 are zero, so it rotates zero. */ + request.first_position = 0u; + request.prefetch_tiles = device->attn_dim_tiles; + request.input = pn_tt_operand(device, input); + request.table.x = (uint16_t)device->dram[table_bank].x; + request.table.y = (uint16_t)device->dram[table_bank].y; + request.table.address = table_address; + request.table.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + if (ft_tt_op_rope_parameters(&request, words) != FT_TT_OK) + return -1; + return pn_tt_attention_submit(device, &device->rope, FT_TT_QUEUE_OP_ROPE, words); +} + +int pn_tt_rope(pn_tt_device *device, const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t heads, uint32_t position) +{ + ft_tt_op_rope_request request; + uint32_t words[FT_TT_OP_ROPE_WORDS]; + + if (!device || !input || !output || heads == 0u || !device->rope.running) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (input->tile_count < heads * device->attn_dim_tiles || + output->tile_count < heads * device->attn_dim_tiles) + return -1; + if (pn_tt_rope_table_for_position(device, position) != 0) + return -1; + + memset(&request, 0, sizeof(request)); + request.row_blocks = 1u; + request.heads = heads; + request.head_dim = device->attn_head_dim; + request.rope_dim = device->attn_rope_dim; + /* ZERO, and it is not a lie: the operand below IS this call's row block, whose row 0 + * already carries `position`. A real first_position would make the driver add + * ft_tt_op_rope_table_offset() to an address that is already the right one. */ + request.first_position = 0u; + request.prefetch_tiles = device->attn_dim_tiles; + request.input = pn_tt_operand(device, input); + request.table.x = (uint16_t)device->dram[device->rope_table_bank].x; + request.table.y = (uint16_t)device->dram[device->rope_table_bank].y; + request.table.address = device->rope_table_address; + request.table.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + if (ft_tt_op_rope_parameters(&request, words) != FT_TT_OK) + return -1; + return pn_tt_attention_submit(device, &device->rope, FT_TT_QUEUE_OP_ROPE, words); +} + +/* ================================================================ + * KV append + * ================================================================ */ + +/** One region as an operand. K and V share a geometry and differ only in base. */ +static ft_tt_op_operand pn_tt_region_operand(const pn_tt_device *device, uint32_t layer, + bool values) +{ + const pn_tt_kv_region *region = &device->kv_region[layer]; + uint32_t bank = values ? region->v_bank : region->k_bank; + ft_tt_op_operand operand; + + operand.x = (uint16_t)device->dram[bank].x; + operand.y = (uint16_t)device->dram[bank].y; + operand.address = values ? region->v_address : region->k_address; + operand.tile_bytes = PN_TT_TILE_BF16; + return operand; +} + +int pn_tt_kv_append(pn_tt_device *device, uint32_t layer, bool values, + const pn_tt_activation *source, uint32_t position) +{ + ft_tt_op_kv_append_request request; + uint32_t words[FT_TT_OP_MEMORY_WORDS]; + + if (!device || !source || !device->memory.running || layer >= device->kv_layer_count) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + + memset(&request, 0, sizeof(request)); + request.cache = &device->kv_layout; + /* RED PROOF: the append lands one position late, so attention reads a K row that belongs + * to no token and misses this one. It stays INSIDE the region — a mutation that shortens + * or moves a transfer out of range can take the card down instead of going red + * (foundry/tt/DEV_NOTES.md §1). */ + request.position = position; + if (pn_tt_red("kv_position") && position + 1u < device->kv_layout.capacity_positions) + request.position = position + 1u; + request.token_count = 1u; + request.source = pn_tt_operand(device, source); + /* The projection's answer is logical row 0 of every tile — this forward does not carry + * the position's row phase through the activations (see the file note). */ + request.source_row0 = 0u; + request.source_tiles = device->attn_kv_heads * device->attn_dim_tiles; + request.region = pn_tt_region_operand(device, layer, values); + if (source->tile_count < request.source_tiles) + return -1; + if (ft_tt_op_kv_append_parameters(&request, words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the KV append at position %u of layer %u is not encodable\n", + position, layer); + return -1; + } + return pn_tt_attention_submit(device, &device->memory, FT_TT_QUEUE_OP_KV_APPEND, words); +} + +/* ================================================================ + * STEP 4 S2c: the verify's rows, unaligned + * ================================================================ */ + +int pn_tt_rope_rows(pn_tt_device *device, const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t heads, uint32_t first_position, + uint32_t rows) +{ + const uint32_t rot = pn_tt_rotary_width(device); + const uint32_t half = rot / 2u; + const uint32_t tiles = pn_tt_rope_table_tiles(device); + const size_t bytes = (size_t)tiles * PN_TT_TILE_BF16; + ft_tt_op_rope_request request; + uint32_t words[FT_TT_OP_ROPE_WORDS]; + uint16_t *flat, *tiled; + uint32_t r, column; + int rc; + + if (!device || !input || !output || heads == 0u || !device->rope.running || rows == 0u || + rows > PN_TT_TILE_DIM) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (input->tile_count < heads * device->attn_dim_tiles || + output->tile_count < heads * device->attn_dim_tiles) + return -1; + if (device->rope_rows_address == 0u && + pn_tt_dram_alloc_zeroed(device, bytes, &device->rope_rows_bank, + &device->rope_rows_address) != 0) + return -1; + /* Row r = position first+r, by the DECODE row's expression term for term (the decode + * table is what a single-position forward rotates with), rows past `rows` zero. Built + * ONCE a verify (every attention layer and both Q and K rotate at the same positions). */ + if (device->rope_rows_key_rows == rows && device->rope_rows_key_first == first_position) + goto submit; + flat = calloc((size_t)PN_TT_TILE_DIM * tiles * PN_TT_TILE_DIM, sizeof(*flat)); + tiled = calloc(bytes / sizeof(uint16_t), sizeof(*tiled)); + if (!flat || !tiled) { + free(flat); + free(tiled); + return -1; + } + for (r = 0; r < rows; r++) { + uint16_t *row = flat + (size_t)r * tiles * PN_TT_TILE_DIM; + + for (column = 0; column < rot; column++) { + double angle = ft_tt_op_rope_angle(first_position + r, column, rot, + device->attn_rope_theta); + double sine = sin(angle); + + row[column] = ft_bf16_from_float((float)cos(angle)); + row[rot + column] = ft_bf16_from_float((float)(column < half ? -sine : sine)); + } + } + rc = ft_tt_tilize_strip(flat, sizeof(*flat), rows, tiles * PN_TT_TILE_DIM, + (size_t)tiles * PN_TT_TILE_DIM * sizeof(*flat), tiled) == 0 + ? pn_tt_write_dram(device, device->rope_rows_bank, device->rope_rows_address, + tiled, bytes) + : -1; + free(flat); + free(tiled); + if (rc != 0) + return -1; + device->rope_rows_key_first = first_position; + device->rope_rows_key_rows = rows; + +submit: + memset(&request, 0, sizeof(request)); + request.row_blocks = 1u; + request.heads = heads; + request.head_dim = device->attn_head_dim; + request.rope_dim = device->attn_rope_dim; + request.first_position = 0u; + request.prefetch_tiles = device->attn_dim_tiles; + request.input = pn_tt_operand(device, input); + request.table.x = (uint16_t)device->dram[device->rope_rows_bank].x; + request.table.y = (uint16_t)device->dram[device->rope_rows_bank].y; + request.table.address = device->rope_rows_address; + request.table.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + if (ft_tt_op_rope_parameters(&request, words) != FT_TT_OK) + return -1; + return pn_tt_attention_submit(device, &device->rope, FT_TT_QUEUE_OP_ROPE, words); +} + +/** S2f: the staircase tile — row p*q_rows+h takes the DECODE tail-mask row for position + * first+p (the 32 tail masks' own bytes; a full last tile takes zeros), padding rows zero. */ +static int pn_tt_attn_rows_mask(pn_tt_device *device, uint32_t first_position, uint32_t rows, + uint32_t chunk) +{ + const uint32_t key = (first_position % PN_TT_TILE_DIM) | (rows << 8) | (1u << 16); + const size_t tile = PN_TT_TILE_BF16; + uint16_t out[PN_TT_TILE_BF16 / 2u]; + uint32_t p, h, c; + + if (chunk >= PN_TT_ATTN_ROWS_CHUNKS) + return -1; + if (device->attn_rows_mask_key[chunk] == key) + return 0; + if (!device->attn_constants_host) { + const size_t bytes = ft_tt_op_attention_constants_bytes(PN_TT_TILE_BF16); + + device->attn_constants_host = malloc(bytes); + if (!device->attn_constants_host || + ft_tt_op_attention_build_constants(device->attn_head_dim, PN_TT_TILE_BF16, + device->attn_constants_host, bytes) != FT_TT_OK) + return -1; + } + if (device->attn_rows_mask_address == 0u) { + /* ON THE CONSTANTS' BANK: the descriptor carries one coordinate for both. */ + ft_tt_buffer buffer; + + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, device->attn_const_bank, + tile * PN_TT_ATTN_ROWS_CHUNKS, &buffer) != FT_TT_OK) + return -1; + device->attn_rows_mask_address = (uint32_t)buffer.address; + } + memset(out, 0, sizeof(out)); + for (p = 0; p < rows; p++) { + /* RED `attn_rows_flat`: every row takes the LAST row's length — the earlier rows see + * the later drafts' K/V. Must DIFFER. */ + const uint32_t live = (first_position + (pn_tt_red("attn_rows_flat") ? rows - 1u : p)) % + PN_TT_TILE_DIM + 1u; /* in the last tile */ + const uint16_t *mask = live == PN_TT_TILE_DIM + ? NULL + : (const uint16_t *)(const void *)(device->attn_constants_host + + (FT_TT_OP_ATTENTION_CONST_MASK_TILE + + live) * tile); + + for (h = 0; h < device->attn_q_rows; h++) { + const uint32_t r = p * device->attn_q_rows + h; + + for (c = 0; c < PN_TT_TILE_DIM && mask; c++) + out[ft_tt_tile_face_index(r, c)] = mask[ft_tt_tile_face_index(r, c)]; + } + } + if (pn_tt_write_dram(device, device->attn_const_bank, + device->attn_rows_mask_address + chunk * (uint32_t)tile, out, tile) != 0) + return -1; + device->attn_rows_mask_key[chunk] = key; + return 0; +} + +/** One ROWS descriptor a KV group for strip rows [query_row, query_row + rows), which sit at + * positions first_position.. (rows * q_rows <= 32), with mask tile `chunk`. */ +static int pn_tt_attention_rows_chunk(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t first_position, + uint32_t query_row, uint32_t rows, uint32_t chunk) +{ + const uint32_t group_tiles = device->attn_q_rows * device->attn_dim_tiles; + const uint32_t kv_positions = first_position + rows; + uint32_t group; + + if (pn_tt_attn_rows_mask(device, first_position, rows, chunk) != 0) + return -1; + for (group = 0; group < device->attn_kv_heads; group++) { + ft_tt_op_attention_request request; + uint32_t words[FT_TT_OP_ATTENTION_WORDS]; + uint64_t run_offset = 0; + uint32_t run_bytes = 0; + + if (ft_tt_kv_cache_run(&device->kv_layout, group, 0u, PN_TT_TILE_DIM, &run_offset, + &run_bytes) != FT_TT_OK) + return -1; + memset(&request, 0, sizeof(request)); + request.q_rows = device->attn_q_rows; + request.dim_tiles = device->attn_dim_tiles; + request.block_tiles = device->attn_block_tiles; + request.kv_positions = kv_positions; + request.capacity_tiles = device->kv_layout.capacity_tiles; + request.query_row = query_row; + request.flags = FT_TT_OP_ATTENTION_FLAG_ROWS; + request.positions = rows; + request.rows_mask_address = device->attn_rows_mask_address + + chunk * (uint32_t)PN_TT_TILE_BF16; + request.query = pn_tt_operand(device, query); + request.query.address += group * group_tiles * PN_TT_TILE_BF16; + request.keys = pn_tt_region_operand(device, layer, false); + request.keys.address += (uint32_t)run_offset; + request.values = pn_tt_region_operand(device, layer, true); + request.values.address += (uint32_t)run_offset; + request.constants.x = (uint16_t)device->dram[device->attn_const_bank].x; + request.constants.y = (uint16_t)device->dram[device->attn_const_bank].y; + request.constants.address = device->attn_const_address; + request.constants.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + request.output.address += group * group_tiles * PN_TT_TILE_BF16; + if (ft_tt_op_attention_parameters(&request, words) != FT_TT_OK || + pn_tt_attention_submit(device, &device->attention, FT_TT_QUEUE_OP_ATTENTION, + words) != 0) + return -1; + } + return 0; +} + +int pn_tt_attention_rows(pn_tt_device *device, uint32_t layer, const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t first_position, uint32_t rows) +{ + const uint32_t group_tiles = device ? device->attn_q_rows * device->attn_dim_tiles : 0u; + const uint32_t kv_positions = first_position + rows; + uint32_t per, done, chunk; + + if (!device || !query || !output || !device->attention.running || rows == 0u || + device->attn_q_rows == 0u || + first_position / PN_TT_TILE_DIM != (kv_positions - 1u) / PN_TT_TILE_DIM) + return -1; + /* A DECODE TILE PACKS 32 / q_rows POSITIONS (8 at Ornith's 4 heads a group), so a wider + * verify (DFlash's 16) runs as chunks of that many rows, each its own descriptor and mask + * tile; the kernels' `query_row` places a chunk's rows in the strip. */ + per = PN_TT_TILE_DIM / device->attn_q_rows; + if ((rows + per - 1u) / per > PN_TT_ATTN_ROWS_CHUNKS) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (layer >= device->kv_layer_count || + query->tile_count < device->attn_kv_heads * group_tiles || + output->tile_count < device->attn_kv_heads * group_tiles) + return -1; + for (done = 0, chunk = 0; done < rows; done += per, chunk++) { + const uint32_t n = rows - done < per ? rows - done : per; + + if (pn_tt_attention_rows_chunk(device, layer, query, output, first_position + done, + done, n, chunk) != 0) + return -1; + } + return 0; +} + +int pn_tt_kv_append_row(pn_tt_device *device, uint32_t layer, bool values, + const pn_tt_activation *source, uint32_t source_row, uint32_t position) +{ + ft_tt_op_kv_append_request request; + uint32_t words[FT_TT_OP_MEMORY_WORDS]; + + if (!device || !source || !device->memory.running || layer >= device->kv_layer_count || + source_row >= PN_TT_TILE_DIM) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + memset(&request, 0, sizeof(request)); + request.cache = &device->kv_layout; + request.position = position; + request.token_count = 1u; + request.source = pn_tt_operand(device, source); + /* RED `spec_kv_row0`: every verify row appends row 0's K/V — must DIFFER. */ + request.source_row0 = pn_tt_red("spec_kv_row0") ? 0u : source_row; + request.source_tiles = device->attn_kv_heads * device->attn_dim_tiles; + request.region = pn_tt_region_operand(device, layer, values); + if (source->tile_count < request.source_tiles || + ft_tt_op_kv_append_parameters(&request, words) != FT_TT_OK) + return -1; + return pn_tt_attention_submit(device, &device->memory, FT_TT_QUEUE_OP_KV_APPEND, words); +} + +int pn_tt_kv_append_chunk(pn_tt_device *device, uint32_t layer, bool values, + const pn_tt_activation *source, uint32_t first_position, + uint32_t rows) +{ + ft_tt_op_kv_append_request request; + uint32_t words[FT_TT_OP_MEMORY_WORDS]; + + if (!device || !source || !device->memory.running || layer >= device->kv_layer_count) + return -1; + if (rows == 0u || rows > PN_TT_TILE_DIM || (first_position % PN_TT_TILE_DIM) != 0u) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + + memset(&request, 0, sizeof(request)); + request.cache = &device->kv_layout; + request.position = first_position; + /* RED PROOF: the whole chunk lands one position late, so every row is a K row that + * belongs to the token after it. It stays INSIDE the region for the same reason the + * per-position arm's does. */ + if (pn_tt_red("kv_position") && first_position + rows < device->kv_layout.capacity_positions) + request.position = first_position + 1u; + request.token_count = rows; + request.source = pn_tt_operand(device, source); + /* Row t of the strip IS position `first_position + t`, and the cache wants position p at + * row `p % 32` — identical when the first position is a multiple of 32, which is why the + * alignment is a refusal above and not a convenience. */ + request.source_row0 = 0u; + request.source_tiles = device->attn_kv_heads * device->attn_dim_tiles; + request.region = pn_tt_region_operand(device, layer, values); + if (source->tile_count < request.source_tiles) + return -1; + if (ft_tt_op_kv_append_parameters(&request, words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the %u-position KV append at %u of layer %u is not " + "encodable\n", rows, first_position, layer); + return -1; + } + return pn_tt_attention_submit(device, &device->memory, FT_TT_QUEUE_OP_KV_APPEND, words); +} + +/* ================================================================ + * Flash decode + * ================================================================ */ + +int pn_tt_attention(pn_tt_device *device, uint32_t layer, const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t kv_positions) +{ + const uint32_t group_tiles = device->attn_q_rows * device->attn_dim_tiles; + uint32_t group; + + if (!device || !query || !output || !device->attention.running) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (layer >= device->kv_layer_count || kv_positions == 0u) + return -1; + if (query->tile_count < device->attn_kv_heads * group_tiles || + output->tile_count < device->attn_kv_heads * group_tiles) + return -1; + + /* ONE DESCRIPTOR PER KV GROUP, because the unit of work is the group and not the Q head: + * the eight Q heads sharing a KV head are the eight ROWS of one tile, so the cache is + * walked once per layer per token instead of eight times (ft_tt_op_attention.h). */ + for (group = 0; group < device->attn_kv_heads; group++) { + ft_tt_op_attention_request request; + uint32_t words[FT_TT_OP_ATTENTION_WORDS]; + uint64_t run_offset = 0; + uint32_t run_bytes = 0; + + /* RED PROOF: every group reads KV head 0. Both regions are the same size and both + * are live, so nothing about the transfer changes — only which head's cache the + * second group attends to, which is exactly the class of mistake a correlation + * metric cannot see. */ + uint32_t kv_head = pn_tt_red("attn_group") ? 0u : group; + + if (ft_tt_kv_cache_run(&device->kv_layout, kv_head, 0u, PN_TT_TILE_DIM, &run_offset, + &run_bytes) != FT_TT_OK) + return -1; + + memset(&request, 0, sizeof(request)); + request.q_rows = device->attn_q_rows; + request.dim_tiles = device->attn_dim_tiles; + request.block_tiles = device->attn_block_tiles; + request.kv_positions = kv_positions; + request.capacity_tiles = device->kv_layout.capacity_tiles; + request.query_row = 0u; + request.query = pn_tt_operand(device, query); + request.query.address += group * group_tiles * PN_TT_TILE_BF16; + request.keys = pn_tt_region_operand(device, layer, false); + request.keys.address += (uint32_t)run_offset; + request.values = pn_tt_region_operand(device, layer, true); + request.values.address += (uint32_t)run_offset; + request.constants.x = (uint16_t)device->dram[device->attn_const_bank].x; + request.constants.y = (uint16_t)device->dram[device->attn_const_bank].y; + request.constants.address = device->attn_const_address; + request.constants.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + request.output.address += group * group_tiles * PN_TT_TILE_BF16; + if (ft_tt_op_attention_parameters(&request, words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: attention over %u positions is not encodable at " + "block_tiles %u against a %u-tile capacity\n", + kv_positions, device->attn_block_tiles, device->kv_layout.capacity_tiles); + return -1; + } + if (pn_tt_attention_submit(device, &device->attention, FT_TT_QUEUE_OP_ATTENTION, + words) != 0) + return -1; + } + return 0; +} + +/** Encode the chunk descriptor for Q head `head`. All words are DRAM addresses. */ +static int attention_chunk_head_words(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t first_position, + uint32_t rows, uint32_t head, + uint32_t words[FT_TT_OP_ATTENTION_WORDS]) +{ + const uint32_t head_tiles = device->attn_dim_tiles; + ft_tt_op_attention_request request; + uint64_t run_offset = 0; + uint32_t run_bytes = 0; + + /* CONTIGUOUS GQA, which is attention's rule and NOT the GDN block's interleaved one + * (spec §4). Head h reads group `h / q_rows`, the same grouping the per-position arm + * walks a group at a time. */ + uint32_t kv_head = head / device->attn_q_rows; + + /* RED PROOF: every head reads KV head 0 — the same mutation the per-position arm + * carries, and the class of mistake no correlation metric can see. */ + if (pn_tt_red("attn_group")) + kv_head = 0u; + + if (ft_tt_kv_cache_run(&device->kv_layout, kv_head, 0u, PN_TT_TILE_DIM, &run_offset, + &run_bytes) != FT_TT_OK) + return -1; + + memset(&request, 0, sizeof(request)); + request.q_rows = rows; + request.dim_tiles = head_tiles; + request.block_tiles = device->attn_block_tiles; + request.kv_positions = first_position + rows; + request.capacity_tiles = device->kv_layout.capacity_tiles; + request.query_row = 0u; + request.flags = FT_TT_OP_ATTENTION_FLAG_CHUNK; + request.query = pn_tt_operand(device, query); + request.query.address += head * head_tiles * PN_TT_TILE_BF16; + request.keys = pn_tt_region_operand(device, layer, false); + request.keys.address += (uint32_t)run_offset; + request.values = pn_tt_region_operand(device, layer, true); + request.values.address += (uint32_t)run_offset; + request.constants.x = (uint16_t)device->dram[device->attn_const_bank].x; + request.constants.y = (uint16_t)device->dram[device->attn_const_bank].y; + request.constants.address = device->attn_const_address; + request.constants.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + request.output.address += head * head_tiles * PN_TT_TILE_BF16; + if (ft_tt_op_attention_parameters(&request, words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: a %u-position chunk at %u over %u positions is not " + "encodable at block_tiles %u against a %u-tile capacity\n", + rows, first_position, first_position + rows, device->attn_block_tiles, + device->kv_layout.capacity_tiles); + return -1; + } + return 0; +} + +/** + * THE HEAD SPLIT: `attention_members` cores, each owning a contiguous range of Q heads — + * contiguous so a member's heads share KV groups. Round r enqueues one head on EVERY member + * and then awaits them all, so the members run concurrently while each still holds ONE + * descriptor's parameters at a time. A member that fails is re-armed and its head submitted + * alone; every enqueued member is awaited first, or its cursor desynchronises. + */ +static int attention_chunk_split(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *query, const pn_tt_activation *output, + uint32_t first_position, uint32_t rows) +{ + const uint32_t members = device->attention_members; + const uint32_t per = device->attn_q_heads / members; + uint32_t words[PN_TT_ATTN_MAX_MEMBERS][FT_TT_OP_ATTENTION_WORDS]; + pn_tt_residency *set[PN_TT_ATTN_MAX_MEMBERS]; + bool bad[PN_TT_ATTN_MAX_MEMBERS]; + uint32_t round, m; + + for (m = 0; m < members; m++) + set[m] = m == 0u ? &device->attention : &device->attention_member[m - 1u]; + for (round = 0; round < per; round++) { + const double started = pn_tt_now_seconds(); + int failed = 0; + + memset(bad, 0, sizeof(bad)); + for (m = 0; m < members; m++) + if (attention_chunk_head_words(device, layer, query, output, first_position, rows, + m * per + round, words[m]) != 0) + return -1; + for (m = 0; m < members; m++) + if (pn_tt_residency_enqueue(set[m], FT_TT_QUEUE_OP_ATTENTION, words[m], 1u) != 0) + failed = bad[m] = 1; + set[0]->seconds += pn_tt_now_seconds() - started; + for (m = 0; m < members; m++) { + const double at = pn_tt_now_seconds(); + double elapsed; + + if (pn_tt_residency_await(set[m], PN_TT_OP_TIMEOUT_MS) != 0) + failed = bad[m] = 1; + elapsed = pn_tt_now_seconds() - at; + device->attention_member_seconds[m] += elapsed; + set[m]->seconds += elapsed; + } + if (failed) { + for (m = 0; m < members; m++) { + if (!bad[m]) + continue; + if (!(m == 0u ? pn_tt_attention_rearm(device, set[0]) + : pn_tt_attention_member_rearm(device, m)) || + pn_tt_residency_submit(set[m], FT_TT_QUEUE_OP_ATTENTION, words[m], + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + } + device->oncard_seconds += pn_tt_now_seconds() - started; + device->oncard_ops += members; + } + return 0; +} + +int pn_tt_attention_chunk(pn_tt_device *device, uint32_t layer, const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t first_position, + uint32_t rows) +{ + const uint32_t head_tiles = device ? device->attn_dim_tiles : 0u; + uint32_t head; + + if (!device || !query || !output || !device->attention.running) + return -1; + if (rows == 0u || rows > PN_TT_TILE_DIM || (first_position % PN_TT_TILE_DIM) != 0u) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (layer >= device->kv_layer_count) + return -1; + if (query->tile_count < device->attn_q_heads * head_tiles || + output->tile_count < device->attn_q_heads * head_tiles) + return -1; + + /* ONE DESCRIPTOR PER Q HEAD, where decode issues one per KV GROUP — because the row axis + * that decode fills with the group's Q heads is the axis a chunk fills with POSITIONS, + * and a score matmul contracts over the head dimension, so two heads cannot share it. + * The trade is measured rather than argued: `q_heads` descriptors replace + * `kv_heads * rows` (16 against 128 here) at essentially the same cost each, so both the + * descriptor count and the number of KV walks fall by `32 * kv_heads / q_heads`. */ + if (device->attention_members > 1u) + return attention_chunk_split(device, layer, query, output, first_position, rows); + for (head = 0; head < device->attn_q_heads; head++) { + uint32_t words[FT_TT_OP_ATTENTION_WORDS]; + + if (attention_chunk_head_words(device, layer, query, output, first_position, rows, + head, words) != 0) + return -1; + if (pn_tt_attention_submit(device, &device->attention, FT_TT_QUEUE_OP_ATTENTION, + words) != 0) + return -1; + } + return 0; +} + +/* ================================================================ + * The export path — the only place the card cache crosses the link + * ================================================================ */ + +/** Bytes one head's `tiles` position tiles occupy, and the offset they start at. */ +static int pn_tt_kv_head_run(const pn_tt_device *device, uint32_t head, uint32_t tiles, + uint64_t *out_offset, uint32_t *out_bytes) +{ + return ft_tt_kv_cache_run(&device->kv_layout, head, 0u, tiles * PN_TT_TILE_DIM, out_offset, + out_bytes) == FT_TT_OK + ? 0 + : -1; +} + +int pn_tt_kv_download(pn_tt_device *device, uint32_t layer, bool values, + uint32_t position_count, float *out) +{ + const uint32_t dim_tiles = device ? device->attn_dim_tiles : 0u; + const uint32_t head_dim = device ? device->attn_head_dim : 0u; + uint32_t tiles, head; + uint16_t *staged; + double started; + + if (!device || !out || position_count == 0u || layer >= device->kv_layer_count) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (position_count > device->kv_layout.capacity_positions) + return -1; + tiles = (position_count + PN_TT_TILE_DIM - 1u) / PN_TT_TILE_DIM; + staged = malloc((size_t)tiles * dim_tiles * PN_TT_TILE_BF16); + if (!staged) + return -1; + started = pn_tt_now_seconds(); + + for (head = 0; head < device->attn_kv_heads; head++) { + ft_tt_op_operand region = pn_tt_region_operand(device, layer, values); + uint32_t bank = values ? device->kv_region[layer].v_bank + : device->kv_region[layer].k_bank; + uint64_t offset = 0; + uint32_t bytes = 0, position; + + if (pn_tt_kv_head_run(device, head, tiles, &offset, &bytes) != 0 || + pn_tt_read_dram(device, bank, region.address + (uint32_t)offset, staged, bytes) != + 0) { + free(staged); + return -1; + } + /* WHOLE TILES, so there is no read-alignment waste at all — which is the arithmetic + * that makes exporting at export time cheaper than mirroring per token. */ + for (position = 0; position < position_count; position++) { + const uint16_t *tile_row = staged + (size_t)(position / PN_TT_TILE_DIM) * + dim_tiles * (PN_TT_TILE_BF16 / 2u); + float *destination = out + ((size_t)head * position_count + position) * head_dim; + uint32_t element; + + for (element = 0; element < head_dim; element++) { + const uint16_t *tile = tile_row + (size_t)(element / PN_TT_TILE_DIM) * + (PN_TT_TILE_BF16 / 2u); + + destination[element] = ft_float_from_bf16( + tile[ft_tt_tile_face_index(position % PN_TT_TILE_DIM, + element % PN_TT_TILE_DIM)]); + } + } + } + free(staged); + device->readback_seconds += pn_tt_now_seconds() - started; + return 0; +} + +int pn_tt_kv_upload(pn_tt_device *device, uint32_t layer, bool values, + uint32_t position_count, const float *in) +{ + const uint32_t dim_tiles = device ? device->attn_dim_tiles : 0u; + const uint32_t head_dim = device ? device->attn_head_dim : 0u; + uint32_t tiles, head; + uint16_t *staged; + double started; + + if (!device || !in || position_count == 0u || layer >= device->kv_layer_count) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (position_count > device->kv_layout.capacity_positions) + return -1; + tiles = (position_count + PN_TT_TILE_DIM - 1u) / PN_TT_TILE_DIM; + staged = calloc((size_t)tiles * dim_tiles, PN_TT_TILE_BF16); + if (!staged) + return -1; + started = pn_tt_now_seconds(); + + for (head = 0; head < device->attn_kv_heads; head++) { + ft_tt_op_operand region = pn_tt_region_operand(device, layer, values); + uint32_t bank = values ? device->kv_region[layer].v_bank + : device->kv_region[layer].k_bank; + uint64_t offset = 0; + uint32_t bytes = 0, position; + + memset(staged, 0, (size_t)tiles * dim_tiles * PN_TT_TILE_BF16); + for (position = 0; position < position_count; position++) { + uint16_t *tile_row = staged + (size_t)(position / PN_TT_TILE_DIM) * dim_tiles * + (PN_TT_TILE_BF16 / 2u); + const float *source = in + ((size_t)head * position_count + position) * head_dim; + uint32_t element; + + for (element = 0; element < head_dim; element++) { + uint16_t *tile = tile_row + (size_t)(element / PN_TT_TILE_DIM) * + (PN_TT_TILE_BF16 / 2u); + + tile[ft_tt_tile_face_index(position % PN_TT_TILE_DIM, + element % PN_TT_TILE_DIM)] = + ft_bf16_from_float(source[element]); + } + } + if (pn_tt_kv_head_run(device, head, tiles, &offset, &bytes) != 0 || + pn_tt_write_dram(device, bank, region.address + (uint32_t)offset, staged, bytes) != + 0) { + free(staged); + return -1; + } + } + free(staged); + device->upload_seconds += pn_tt_now_seconds() - started; + return 0; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_column_runs.c b/src/accel/tt/pn_tt_column_runs.c new file mode 100644 index 0000000..d05fcc4 --- /dev/null +++ b/src/accel/tt/pn_tt_column_runs.c @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_column_runs.c + * @brief Implementation of the column-tile permutation arithmetic. See pn_tt_column_runs.h, + * which carries why a joint projection is de-interleaved in the WEIGHT. + * + * No device, no allocation, no I/O — deliberately, so it is provable with no card. + * + * @status None + * @issues None + * @todo None + */ + +#include "pn_tt_column_runs.h" + +uint32_t pn_tt_column_runs_total(const pn_tt_column_run *runs, uint32_t run_count) +{ + uint32_t total = 0u; + uint32_t index; + + if (!runs || run_count == 0u || run_count > PN_TT_COLUMN_RUNS_MAX) + return 0u; + for (index = 0; index < run_count; index++) { + if (runs[index].count == 0u) + return 0u; + /* A destination wider than any weight this backend admits is a caller bug that would + * otherwise wrap and produce a plausible small number. */ + if (total > UINT32_MAX - runs[index].count) + return 0u; + total += runs[index].count; + } + return total; +} + +bool pn_tt_column_runs_are_injective(const pn_tt_column_run *runs, uint32_t run_count, + uint32_t source_columns) +{ + uint32_t index; + + if (pn_tt_column_runs_total(runs, run_count) == 0u || source_columns == 0u) + return false; + for (index = 0; index < run_count; index++) { + uint32_t other; + + if (runs[index].first >= source_columns || + runs[index].count > source_columns - runs[index].first) + return false; + /* Pairwise rather than a bitmap: `run_count` is at most 128, so this is at worst + * 8,128 comparisons ONCE at model load, and a bitmap would need a bound on + * `source_columns` that this module has no business having. */ + for (other = 0; other < index; other++) { + const uint32_t a_end = runs[index].first + runs[index].count; + const uint32_t b_end = runs[other].first + runs[other].count; + + if (runs[index].first < b_end && runs[other].first < a_end) + return false; + } + } + return true; +} + +bool pn_tt_column_runs_map(const pn_tt_column_run *runs, uint32_t run_count, + uint32_t destination_column, uint32_t *out_source_column, + uint32_t *out_contiguous) +{ + uint32_t walked = 0u; + uint32_t index; + + if (pn_tt_column_runs_total(runs, run_count) == 0u || !out_source_column || + !out_contiguous) + return false; + for (index = 0; index < run_count; index++) { + if (destination_column < walked + runs[index].count) { + const uint32_t inside = destination_column - walked; + + *out_source_column = runs[index].first + inside; + *out_contiguous = runs[index].count - inside; + return true; + } + walked += runs[index].count; + } + return false; +} + +uint32_t pn_tt_column_runs_field(uint32_t groups, uint32_t stride, uint32_t offset, + uint32_t width, pn_tt_column_run *out_runs, uint32_t max_runs) +{ + uint32_t index; + + if (!out_runs || groups == 0u || width == 0u || groups > max_runs || + groups > PN_TT_COLUMN_RUNS_MAX) + return 0u; + /* The field has to fit inside the record, or consecutive groups would overlap and the + * injectivity check downstream would report a defect this function created. */ + if (stride < width || offset > stride - width) + return 0u; + for (index = 0; index < groups; index++) { + out_runs[index].first = index * stride + offset; + out_runs[index].count = width; + } + return groups; +} diff --git a/src/accel/tt/pn_tt_column_runs.h b/src/accel/tt/pn_tt_column_runs.h new file mode 100644 index 0000000..0a66736 --- /dev/null +++ b/src/accel/tt/pn_tt_column_runs.h @@ -0,0 +1,124 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_column_runs.h + * @brief A PERMUTATION of a weight's output columns, expressed as runs of COLUMN TILES — + * the whole of the "de-interleave a joint projection" problem, with no card in it. + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS: A JOINT PROJECTION IS INTERLEAVED PER HEAD + * --------------------------------------------------------------------------- + * Ornith's `blk.N.attn_q.weight` emits Q **and** the attention output gate from one matmul, + * interleaved per head: + * + * [ head 0 Q (head_dim) | head 0 gate (head_dim) | head 1 Q | head 1 gate | ... ] + * + * Every op downstream wants them apart and compact. The QK-RMSNorm reads head `h` as row + * block `h` at tiles `h*tile_cols`, consecutive; flash decode reads a KV group as ONE + * contiguous run of `q_rows*dim_tiles` tiles. Over the joint strip, neither lands: group + * `g`'s run would be head `2g`'s Q followed by head `2g`'s GATE. + * `foundry/tt/tests/test_attention_ops.c` asserts exactly that, so this is a measured + * requirement rather than a tidiness one. + * + * --------------------------------------------------------------------------- + * AND WHY IT IS SOLVED IN THE WEIGHT RATHER THAN PER TOKEN + * --------------------------------------------------------------------------- + * The three places it could be fixed, and what each costs: + * + * 1. a per-token card-side gather — a new op, 512 tiles a layer, 8 layers a token; + * 2. a stride word in `resident_attention_reader.c` AND one in `resident_reduce_reader.c` — + * two kernel source changes, a program rebuild, and the reduce one is under the model's + * OWN RMS norms on the shipped default path; + * 3. a PERMUTED UPLOAD of the weight at load — no kernel change, no per-token cost, and + * every downstream op runs at its already-proven shape. + * + * (3), and this file is its arithmetic. An N-major artifact lays column tile `c` as a + * contiguous `tile_rows * tile_bytes` run, so a column permutation is a permutation of + * contiguous byte ranges and the upload is one `ft_tt_fttw_upload_range()` per run per + * shard. The cost is a second copy of the tensor in card DRAM — 71.3 MiB a layer for + * Ornith's `attn_q`, 570 MiB over its 8 attention layers, against 32 GB of board memory. + * + * Nothing here touches a device, so it is provable with no card: + * `tests/test_tt_column_runs.c`. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TT_COLUMN_RUNS_H +#define PN_TT_COLUMN_RUNS_H + +#include +#include + +/** How many runs one destination weight may be assembled from. 32 heads x 2 halves is 64; + * this is the ceiling a caller's array has to satisfy and it is checked, not assumed. */ +#define PN_TT_COLUMN_RUNS_MAX 128u + +/** One contiguous run of SOURCE column tiles, in the order the DESTINATION wants them. */ +typedef struct pn_tt_column_run { + uint32_t first; /**< first source column tile */ + uint32_t count; /**< consecutive source column tiles, > 0 */ +} pn_tt_column_run; + +/** + * Destination column tiles this run list produces. + * + * @return the sum of the counts, or 0 for an empty or malformed list + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +uint32_t pn_tt_column_runs_total(const pn_tt_column_run *runs, uint32_t run_count); + +/** + * Do these runs stay inside a `source_columns`-wide weight, and does no source column appear + * twice? + * + * The second half is what makes a wrong run list a REFUSAL rather than a weight with one + * column duplicated and another missing — which would be a smoothly wrong projection with + * nothing on the card able to see it. + * + * @return true when every run is in range and the destination uses each source column at + * most once + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +bool pn_tt_column_runs_are_injective(const pn_tt_column_run *runs, uint32_t run_count, + uint32_t source_columns); + +/** + * Where destination column `destination_column` comes from, and how many destination columns + * after it are contiguous in the SOURCE too. + * + * The second output is what turns a permutation into a small number of large transfers: a + * caller uploads `*out_contiguous` columns in one range and asks again. + * + * @param out_source_column the source column tile it reads + * @param out_contiguous destination columns from here that share the run, >= 1 + * @return false when `destination_column` is past the end + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +bool pn_tt_column_runs_map(const pn_tt_column_run *runs, uint32_t run_count, + uint32_t destination_column, uint32_t *out_source_column, + uint32_t *out_contiguous); + +/** + * Build the run list that lifts ONE interleaved field out of a joint projection. + * + * Field `offset..offset+width` of each of `groups` records of `stride` column tiles. For + * Ornith's `attn_q` at head_dim 256 (dim_tiles 8): `groups` 32, `stride` 16, `width` 8, and + * `offset` 0 for Q or 8 for the gate. + * + * @param out_runs receives `groups` runs + * @param max_runs capacity of `out_runs` + * @return runs written, or 0 on a bad shape or too small an array + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +uint32_t pn_tt_column_runs_field(uint32_t groups, uint32_t stride, uint32_t offset, + uint32_t width, pn_tt_column_run *out_runs, + uint32_t max_runs); + +#endif /* PN_TT_COLUMN_RUNS_H */ diff --git a/src/accel/tt/pn_tt_core_boot_guard.c b/src/accel/tt/pn_tt_core_boot_guard.c new file mode 100644 index 0000000..73b8a29 --- /dev/null +++ b/src/accel/tt/pn_tt_core_boot_guard.c @@ -0,0 +1,209 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_core_boot_guard.c + * @brief Implementation of the pre-boot readiness gate (see pn_tt_core_boot_guard.h). + * + * @status New 2026-08-24. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_core_boot_guard.h" + +#include +#include +#include + +#include "pn_tt_residency.h" +#include "tt/ft_tt_core_readiness.h" +#include "tt/ft_tt_launch.h" +#include "tt/ft_tt_memmap.h" + +/** + * Milliseconds this guard will wait for a busy core to come free. + * + * IT IS LONGER THAN THE STOP'S GRACE ON PURPOSE, AND THE DIFFERENCE IS MEASURED. + * `pn_tt_residency_stop()` is issued straight after a refusal, so what it waits out is the + * CB-wait bound — 100,000,000 spins — and 20 s covers it. This guard waits out whatever an + * EARLIER PROCESS left, and a residency abandoned by a process that DIED sits in + * `ftq_wait_for_work()` until the IDLE bound instead: 400,000,000 spins, recorded in + * `pn_tt_device.c`'s own death-stop warning as **over 40 s**. A 20 s guard would refuse a + * core that was 30 s from clearing, and that refusal costs the whole model the + * ~26 s/position host forward — far more than the wait. + * + * The cost of the margin is zero on a healthy core: this wait does not run unless the card + * has already said a processor is executing. + */ +#define PN_TT_BOOT_GUARD_WAIT_MS 90000u + +/** Milliseconds between readiness re-reads while waiting out a spin. + * + * The stop polls at one millisecond because it is waiting on a queue field it can read in + * 3 us. A readiness read is a mailbox snapshot plus fourteen more reads, ~400 us, and the + * thing it waits for takes seconds — so polling it at 1 ms would spend a third of the wait + * inside the instrument. 50 ms costs at most 50 ms of extra wait on a spin that is orders + * of magnitude longer. */ +#define PN_TT_BOOT_GUARD_POLL_MS 50u + +/** Names in FT_TT_PROC_* order, for the ticking mask. */ +static const char *const PN_TT_BOOT_GUARD_PROCESSOR_NAME[FT_TT_PROC_COUNT] = { + "BRISC", "NCRISC", "TRISC0", "TRISC1", "TRISC2" +}; + +/** "TRISC0 TRISC2" into `out`, or "none". Never NULL, never unterminated. */ +static void describe_ticking(uint32_t mask, char *out, size_t out_bytes) +{ + unsigned processor; + size_t used = 0; + + if (!out || out_bytes == 0u) + return; + out[0] = '\0'; + for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { + int written; + + if ((mask & (1u << processor)) == 0u) + continue; + written = snprintf(out + used, out_bytes - used, "%s%s", used ? " " : "", + PN_TT_BOOT_GUARD_PROCESSOR_NAME[processor]); + if (written < 0 || (size_t)written >= out_bytes - used) + return; + used += (size_t)written; + } + if (used == 0u) + snprintf(out, out_bytes, "none"); +} + +/** + * The whole refusal, in one block, on stderr. + * + * IT IS LONG ON PURPOSE. The failure mode this guard sits in front of is not "the arm did + * not happen" — it is "the arm did not happen, the model quietly ran on the CPU at + * ~26 s/position, and the numbers looked plausible for eleven minutes". A refusal that reads + * like one more warning would reproduce exactly that. Every line here answers a question the + * person reading it is about to ask: which core, what did the card say, can I wait, what do + * I run, and what happens to the model now. + */ +static void print_refusal(const ft_tt_core_readiness *readiness, uint32_t core_index, + const char *who, const char *core_env, unsigned waited_ms) +{ + char ticking[64]; + + describe_ticking(readiness->ticking_mask, ticking, sizeof(ticking)); + fprintf(stderr, + "pn_tt: ==================================================================\n" + "pn_tt: %s — Tensix %u is NOT READY, so the %s residency is not armed.\n" + "pn_tt: the card says: %s\n" + "pn_tt: verdict '%s' · reset 0x%08x · instrn_buf 0x%08x · go 0x%02x\n" + "pn_tt: resident queue %s · control heartbeat %s · executing: %s\n", + PN_TT_BOOT_GUARD_REFUSAL_TOKEN, core_index, who, + ft_tt_core_readiness_string(readiness->state), + ft_tt_core_verdict_string(readiness->status.verdict), + readiness->status.state.reset_register, readiness->status.state.instrn_buf_status, + readiness->status.state.go_messages[0].signal, + readiness->status.queue_present ? "present" : "absent", + readiness->status.queue_heartbeat_ticking ? "ticking" : "frozen", ticking); + if (waited_ms != 0u) + fprintf(stderr, "pn_tt: waited %u ms for it to clear and it did not.\n", waited_ms); + + if (readiness->state == FT_TT_CORE_NOT_READY_STILL_EXECUTING) { + fprintf(stderr, + "pn_tt: THIS IS A LEFTOVER FROM AN EARLIER PROCESS. A refused descriptor\n" + "pn_tt: leaves a compute thread spinning out its 400,000,000-iteration\n" + "pn_tt: bound, and that thread outlives the process that started it. It\n" + "pn_tt: WILL finish — leave the core alone and try again.\n"); + } + if (readiness->state == FT_TT_CORE_NOT_READY_INSTRUCTION_FIFO) { + fprintf(stderr, + "pn_tt: THIS CORE IS ALREADY WEDGED. Rung 1 does NOT clear it. Only a\n" + "pn_tt: whole-card reset does, and that resets the WHOLE CARD:\n" + "pn_tt: sudo -n env FT_TT_DRIVER=udrv ./tt/tools/build/ft_tt_core \\\n" + "pn_tt: recover %u --rung 3 --write --then-probe --driver udrv\n", + core_index); + } + + /* NOTHING WAS WRITTEN, and saying so is load-bearing: the next question after a refusal + * is always "is the card in a worse state now than before I ran this". */ + fprintf(stderr, + "pn_tt: NOTHING WAS WRITTEN TO THIS CORE. Booting firmware onto it is the\n" + "pn_tt: wedge (instrn_buf 0x66), which is what this refusal prevented.\n" + "pn_tt: CONSEQUENCE: with this family unarmed the model may fall back to the\n" + "pn_tt: HOST forward at ~26 s/position. Watch for 'using CPU forward' and\n" + "pn_tt: treat plausible-but-very-slow numbers after this line as INVALID.\n"); + if (core_env) + fprintf(stderr, "pn_tt: To arm somewhere else instead, set %s to a free core.\n", + core_env); + fprintf(stderr, + "pn_tt: `ft_tt_core status %u --detail` reads this core without touching it.\n" + "pn_tt: ==================================================================\n", + core_index); +} + +/** One millisecond of sleep, `count` times. */ +static void nap_ms(unsigned count) +{ + struct timespec nap = { 0, 1000000L }; + unsigned index; + + for (index = 0; index < count; index++) + (void)nanosleep(&nap, NULL); +} + +bool pn_tt_core_boot_is_permitted(ft_tt_runtime *runtime, uint32_t core_index, const char *who, + const char *core_env) +{ + ft_tt_core_readiness readiness; + unsigned waited_ms = 0u; + + if (!runtime || !who) + return true; + + /* A READ THAT FAILS IS NOT A REFUSAL. + * + * This guard catches one positively-identified condition: a core the card says is busy. + * Everything else must pass. If the coordinate does not resolve or the reading itself + * errors, refusing would let a diagnostic path's own failure take the whole model to the + * ~26 s/position host forward — the exact silent degradation this round is defending + * against, arriving through the defence. The boot below has its own error path for a + * core that genuinely cannot be reached. */ + if (ft_tt_core_readiness_read(&runtime->noc, &runtime->grid, core_index, &readiness) != + FT_TT_OK) + return true; + if (readiness.ready) + return true; + + /* WAIT ONLY WHEN THE READING SAYS WAITING IS THE ANSWER. A spun-out bound finishes; a + * 0x66 core, a stalled kernel and a stuck boot do not, and PN_TT_BOOT_GUARD_WAIT_MS + * spent in front of an inevitable refusal is a minute and a half of a model load that + * nobody gets back. */ + if (readiness.waiting_can_help) { + fprintf(stderr, "pn_tt: Tensix %u is still executing — %s waits up to %u ms. This is a " + "leftover spinning compute thread, and it is bounded.\n", + core_index, who, (unsigned)PN_TT_BOOT_GUARD_WAIT_MS); + while (waited_ms < (unsigned)PN_TT_BOOT_GUARD_WAIT_MS) { + nap_ms(PN_TT_BOOT_GUARD_POLL_MS); + waited_ms += PN_TT_BOOT_GUARD_POLL_MS; + if (ft_tt_core_readiness_read(&runtime->noc, &runtime->grid, core_index, + &readiness) != FT_TT_OK) + return true; + if (readiness.ready) { + fprintf(stderr, "pn_tt: Tensix %u came free after %u ms — %s arms normally\n", + core_index, waited_ms, who); + return true; + } + /* The reading may CHANGE under us: a spin that ends inside a wedged core stops + * ticking and starts reading 0x66. Re-asking every poll is what lets the refusal + * name the state the core is actually in rather than the one it started in. */ + if (!readiness.waiting_can_help) + break; + } + } + + print_refusal(&readiness, core_index, who, core_env, waited_ms); + return false; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_core_boot_guard.h b/src/accel/tt/pn_tt_core_boot_guard.h new file mode 100644 index 0000000..22d8a8b --- /dev/null +++ b/src/accel/tt/pn_tt_core_boot_guard.h @@ -0,0 +1,109 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_core_boot_guard.h + * @brief The one question asked before ANY of this backend boots firmware onto a Tensix: + * **is that core ready, or is something still running on it?** + * + * --------------------------------------------------------------------------- + * THE FAILURE THIS EXISTS FOR — IT CROSSES A PROCESS BOUNDARY + * --------------------------------------------------------------------------- + * A refused descriptor makes a resident reader abort and leaves the compute thread spinning + * out its 400,000,000-iteration bound. That is the family's design. `pn_tt_residency_stop()` + * waits it out (20 s of grace) and `pn_tt_residency_retired()` reports whether it did, so the + * process that CAUSED the spin will not re-arm onto it — those guards landed and they fire. + * + * **They protect one process. The spinning thread outlives it.** A `serve` restart or a model + * reload is a NEW process with an empty residency table, and its ordinary first arm boots + * firmware onto a core that is still running. MEASURED ON SILICON 2026-08-21/22: that is a + * WEDGE — core 90 came back `instrn_buf_status = 0x66`, twice, and rung 1 does NOT clear it; + * only a whole-card `ft_tt_core recover --rung 3` does. Nothing in a fresh process's own + * memory can see this, so the CARD is asked. `ft_tt_core_readiness.h` is that reading, and it + * is the same verdict `ft_tt_core status` has printed since 2026-08-12. + * + * --------------------------------------------------------------------------- + * IT SITS AT THE BOOT, NOT AT ONE FAMILY'S ARM + * --------------------------------------------------------------------------- + * `pn_tt_core_holder()` already refuses a core another residency of THIS process holds, by + * name. This is the other half — the core the CARD says is busy — and it has to cover every + * family, so it is called at all three places that boot firmware: + * + * - `pn_tt_prepare_family_on_core()`, which is reduce, eltwise, topk, rope, memory, + * attention, gdn, gdnc, the GDN head-split members, the render directions and both + * head-norm slots; + * - `pn_tt_matmul_group_boot()`, members 1..N; + * - `pn_tt_device_open()`'s own core, which is matmul member 0 and the FIRST boot a serve + * takes — the one a restart lands on. + * + * --------------------------------------------------------------------------- + * WAIT, THEN REFUSE — AND THE READING DECIDES WHICH + * --------------------------------------------------------------------------- + * "Not ready" is two different things. A core spinning out a bound WILL finish, so it is + * WAITED for, on the same grace `pn_tt_residency_stop()` uses because it is the same spin. A + * core at `instrn_buf_status = 0x66` never will, and waiting on it is dead time in front of + * an inevitable refusal. `ft_tt_core_readiness`'s `waiting_can_help` separates them; this + * file does not guess. + * + * Recovery is deliberately NOT attempted. Resetting a core out of a blocked wait is how cores + * get wedged in the first place (`pn_tt_residency.h` hazard 2), and rung 3 resets the WHOLE + * CARD — which is an operator's decision, not an arming path's. + * + * --------------------------------------------------------------------------- + * THE REFUSAL IS LOUD, AND THAT IS A REQUIREMENT RATHER THAN A STYLE + * --------------------------------------------------------------------------- + * A family that fails to arm can take the WHOLE MODEL to the ~26 s/position CPU forward, and + * it did so behind a single line — `qwen35 Tenstorrent upload skipped/failed; using CPU + * forward` — which burned 11 minutes of a run before anyone noticed, because the numbers that + * came out were plausible and merely very slow. A silent degradation caused BY a reliability + * fix would be worse than the bug it fixes. So the refusal prints a block, names the core, + * quotes what the card said, gives the recovery command verbatim, and states the consequence. + * + * **`PN_TT_BOOT_GUARD_REFUSAL_TOKEN` is the stable grep handle** for a timing script's abort + * guard, next to the "using CPU forward" one every script already carries. + * + * @status New 2026-08-24. The classifier under it is covered off-hardware by + * foundry/tt/tests/test_core_readiness.c. + * @issues None + * @todo None + */ + +#ifndef PN_TT_CORE_BOOT_GUARD_H +#define PN_TT_CORE_BOOT_GUARD_H + +#ifdef USE_TT + +#include +#include + +#include "tt/ft_tt_runtime.h" + +/** The one string a script needs to grep for. Do not reword it without saying so. */ +#define PN_TT_BOOT_GUARD_REFUSAL_TOKEN "REFUSED TO BOOT" + +/** + * May firmware be booted onto `core_index` right now? + * + * Reads the core. If it is busy in a way that clears itself, waits for it — up to + * `PN_TT_RESIDENCY_STOP_GRACE_MS`, the same bound the stop uses for the same spin — and + * re-reads. Returns false with a LOUD, multi-line explanation on stderr if it is still not + * ready, or if it never could be. + * + * Writes nothing to the card, resets nothing and parks nothing. Reads only. + * + * @param runtime the open card; borrowed + * @param core_index Tensix index, as `ft_tt_coord_tensix_by_index` numbers them + * @param who what wants the core, for the message, e.g. "gdn" or "matmul member 3" + * @param core_env the environment variable that would move this family to another core, + * or NULL when there is not one + * @return true when the core may be booted. **A read that itself fails returns + * true**: this guard exists to catch a specific, positively-identified + * busy core, and turning an unreadable status into a refusal would let a + * diagnostic path's failure take the whole model to the CPU forward + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +bool pn_tt_core_boot_is_permitted(ft_tt_runtime *runtime, uint32_t core_index, const char *who, + const char *core_env); + +#endif /* USE_TT */ +#endif /* PN_TT_CORE_BOOT_GUARD_H */ diff --git a/src/accel/tt/pn_tt_descriptor_census.c b/src/accel/tt/pn_tt_descriptor_census.c new file mode 100644 index 0000000..4c52f5a --- /dev/null +++ b/src/accel/tt/pn_tt_descriptor_census.c @@ -0,0 +1,319 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_descriptor_census.c + * @brief Implementation of the (family, function, tile count) descriptor census. See + * pn_tt_descriptor_census.h, which carries why the per-residency report is not + * enough for the elementwise family and does not see the render pair at all. + * + * No device, no allocation, no I/O — deliberately, so it is provable with no card. + * + * @status None + * @issues None + * @todo None + */ + +#include "pn_tt_descriptor_census.h" + +#include + +/** + * Is `value` finite, asked of the EXPONENT BITS THROUGH A VOLATILE SLOT. + * + * `isfinite()` is foldable to a literal 1 under `-ffast-math` (which implies + * `-ffinite-math-only`), and this tree compiles with `-ffast-math`. Asking the bits is the + * recorded fix — AND THE RECORDED FIX IS NOT ENOUGH ON ITS OWN. + * + * Measured here, Apple clang, `-O3 -ffast-math`: a plain `memcpy`-then-shift written inline + * in the calling translation unit answers **1 for a positive infinity and 1 for a NaN**. The + * compiler recognises the idiom, applies `-ffinite-math-only` to it, and folds it — the exact + * fold the bit test was adopted to defeat. It is not constant propagation: it survives the + * value arriving through a `volatile uint64_t`. The same code compiled without `-ffast-math` + * answers 0 for both. + * + * The `volatile double` slot below fixes it (as does reading through a `const double *`, or + * `__attribute__((optnone))`): a volatile store forbids the compiler from reasoning about the + * value it just wrote. `tests/parity_score.c`'s `parity_double_is_finite()` has the unguarded + * shape and is safe only because it lives in its own translation unit with no LTO — turning + * LTO on would silently re-open the eleventh vacuous-gate class. + */ +static int census_double_is_finite(double value) +{ + volatile double opaque = value; + double copy = opaque; + uint64_t bits; + + memcpy(&bits, ©, sizeof(bits)); + return ((bits >> 52) & 0x7FFu) != 0x7FFu; +} + +/** Copy `source` into a key field, always NUL-terminated, truncating rather than refusing. */ +static void census_copy_key(char *destination, const char *source) +{ + size_t index = 0; + + if (!source) { + destination[0] = '\0'; + return; + } + while (index + 1u < PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES && source[index] != '\0') { + destination[index] = source[index]; + index++; + } + destination[index] = '\0'; +} + +/** Does `row` already carry this key? The key is all three fields, never two. */ +static bool census_row_matches(const pn_tt_descriptor_census_row *row, const char *family, + const char *function, uint32_t tile_count) +{ + char wanted_family[PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES]; + char wanted_function[PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES]; + + if (row->tile_count != tile_count) + return false; + census_copy_key(wanted_family, family); + census_copy_key(wanted_function, function); + return strcmp(row->family, wanted_family) == 0 && + strcmp(row->function, wanted_function) == 0; +} + +/** + * Find or create the row for a key, or say why not. + * + * @param out_row set on success only + * @return 0, or -1 with `refused` or `dropped` already accounted + */ +static int census_row_for_key(pn_tt_descriptor_census *census, const char *family, + const char *function, uint32_t tile_count, double seconds, + pn_tt_descriptor_census_row **out_row) +{ + uint32_t index; + + /* A zero width or a non-finite duration is a defect in the CALLER, and folding it into a + * row would make the fit answer with authority about a number nobody measured. */ + if (tile_count == 0u || !census_double_is_finite(seconds) || seconds < 0.0) { + census->refused++; + return -1; + } + for (index = 0; index < census->row_count; index++) { + if (census_row_matches(&census->rows[index], family, function, tile_count)) { + *out_row = &census->rows[index]; + return 0; + } + } + if (census->row_count >= PN_TT_DESCRIPTOR_CENSUS_ROWS) { + /* NOT an "other" bucket. A truncated census that still sums to the family total is + * indistinguishable from a complete one. */ + census->dropped++; + census->dropped_seconds += seconds; + return -1; + } + index = census->row_count++; + memset(&census->rows[index], 0, sizeof(census->rows[index])); + census_copy_key(census->rows[index].family, family); + census_copy_key(census->rows[index].function, function); + census->rows[index].tile_count = tile_count; + *out_row = &census->rows[index]; + return 0; +} + +int pn_tt_descriptor_census_record(pn_tt_descriptor_census *census, const char *family, + const char *function, uint32_t tile_count, double seconds) +{ + pn_tt_descriptor_census_row *row = NULL; + + if (!census || + census_row_for_key(census, family, function, tile_count, seconds, &row) != 0) + return -1; + row->ops++; + row->seconds += seconds; + return 0; +} + +int pn_tt_descriptor_census_record_recovered(pn_tt_descriptor_census *census, + const char *family, const char *function, + uint32_t tile_count, double seconds, + uint64_t rearms) +{ + pn_tt_descriptor_census_row *row = NULL; + + if (!census) + return -1; + /* A "recovered" descriptor that re-armed nothing is a CLEAN one under the wrong name, and + * accepting it would let a caller move time out of the fit by mislabelling it. */ + if (rearms == 0u) { + census->refused++; + return -1; + } + if (census_row_for_key(census, family, function, tile_count, seconds, &row) != 0) + return -1; + row->recovered_ops++; + row->recovered_seconds += seconds; + row->rearms += rearms; + return 0; +} + +bool pn_tt_descriptor_census_row_at(const pn_tt_descriptor_census *census, uint32_t index, + const pn_tt_descriptor_census_row **out_row) +{ + if (!census || index >= census->row_count) + return false; + if (out_row) + *out_row = &census->rows[index]; + return true; +} + +/** Does `row` belong to `family`? A NULL family means every row. */ +static bool census_row_in_family(const pn_tt_descriptor_census_row *row, const char *family) +{ + char wanted[PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES]; + + if (!family) + return true; + census_copy_key(wanted, family); + return strcmp(row->family, wanted) == 0; +} + +/** + * Distinct tile counts among a family's CLEAN rows, counted the O(n^2) way; n is at most 64. + * + * Clean because this is the fit's shape guard: a row that only ever refused contributes no + * equation, so counting it would let a one-point fit past the first of the two redundant + * guards and leave the determinant test alone with the job. + */ +static uint32_t census_distinct_shapes(const pn_tt_descriptor_census *census, + const char *family) +{ + uint32_t distinct = 0; + uint32_t index; + + for (index = 0; index < census->row_count; index++) { + uint32_t earlier; + bool seen = false; + + if (!census_row_in_family(&census->rows[index], family) || census->rows[index].ops == 0u) + continue; + for (earlier = 0; earlier < index && !seen; earlier++) + seen = census_row_in_family(&census->rows[earlier], family) && + census->rows[earlier].ops != 0u && + census->rows[earlier].tile_count == census->rows[index].tile_count; + if (!seen) + distinct++; + } + return distinct; +} + +int pn_tt_descriptor_census_family_totals(const pn_tt_descriptor_census *census, + const char *family, uint64_t *out_ops, + double *out_seconds, uint32_t *out_shapes) +{ + uint64_t ops = 0; + double seconds = 0.0; + uint32_t rows = 0; + uint32_t index; + + if (!census) + return -1; + for (index = 0; index < census->row_count; index++) { + if (!census_row_in_family(&census->rows[index], family)) + continue; + ops += census->rows[index].ops; + seconds += census->rows[index].seconds; + rows++; + } + if (rows == 0u) + return -1; + if (out_ops) + *out_ops = ops; + if (out_seconds) + *out_seconds = seconds; + if (out_shapes) + *out_shapes = census_distinct_shapes(census, family); + return 0; +} + +int pn_tt_descriptor_census_family_recovery(const pn_tt_descriptor_census *census, + const char *family, uint64_t *out_recovered_ops, + double *out_recovered_seconds, + uint64_t *out_rearms) +{ + uint64_t recovered_ops = 0; + double recovered_seconds = 0.0; + uint64_t rearms = 0; + uint32_t rows = 0; + uint32_t index; + + if (!census) + return -1; + for (index = 0; index < census->row_count; index++) { + if (!census_row_in_family(&census->rows[index], family)) + continue; + recovered_ops += census->rows[index].recovered_ops; + recovered_seconds += census->rows[index].recovered_seconds; + rearms += census->rows[index].rearms; + rows++; + } + /* Only "the family never ran" refuses. A family that ran and never refused answers zeros, + * because a reader has to be able to print "0 re-arms" rather than print nothing. */ + if (rows == 0u) + return -1; + if (out_recovered_ops) + *out_recovered_ops = recovered_ops; + if (out_recovered_seconds) + *out_recovered_seconds = recovered_seconds; + if (out_rearms) + *out_rearms = rearms; + return 0; +} + +int pn_tt_descriptor_census_fit(const pn_tt_descriptor_census *census, const char *family, + double *out_fixed_seconds, double *out_seconds_per_tile, + uint32_t *out_shapes) +{ + double weight_sum = 0.0, weighted_x = 0.0, weighted_y = 0.0; + double weighted_xx = 0.0, weighted_xy = 0.0; + uint32_t shapes; + uint32_t index; + double determinant; + + if (!census) + return -1; + shapes = census_distinct_shapes(census, family); + /* TWO UNKNOWNS NEED TWO EQUATIONS. A family that ran at one width cannot say which part + * of its descriptor is fixed, and answering anyway is how "958 ns per single-tile op" + * was produced from a division. */ + if (shapes < 2u) + return -1; + for (index = 0; index < census->row_count; index++) { + const pn_tt_descriptor_census_row *row = &census->rows[index]; + double weight, x, y; + + if (!census_row_in_family(row, family) || row->ops == 0u) + continue; + weight = (double)row->ops; + x = (double)row->tile_count; + y = row->seconds / weight; + weight_sum += weight; + weighted_x += weight * x; + weighted_y += weight * y; + weighted_xx += weight * x * x; + weighted_xy += weight * x * y; + } + determinant = weight_sum * weighted_xx - weighted_x * weighted_x; + /* Scale-free degeneracy test: the absolute determinant of a fit over 7,760-tile shapes is + * enormous while the same fit over 4-tile shapes is tiny, so a fixed epsilon would refuse + * one family and accept a collinear other. */ + if (!census_double_is_finite(determinant) || + determinant <= 1e-12 * weight_sum * weighted_xx) + return -1; + if (out_seconds_per_tile) + *out_seconds_per_tile = (weight_sum * weighted_xy - weighted_x * weighted_y) / + determinant; + if (out_fixed_seconds) + *out_fixed_seconds = (weighted_xx * weighted_y - weighted_x * weighted_xy) / + determinant; + if (out_shapes) + *out_shapes = shapes; + return 0; +} diff --git a/src/accel/tt/pn_tt_descriptor_census.h b/src/accel/tt/pn_tt_descriptor_census.h new file mode 100644 index 0000000..ed12069 --- /dev/null +++ b/src/accel/tt/pn_tt_descriptor_census.h @@ -0,0 +1,234 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_descriptor_census.h + * @brief Attribute on-card descriptor time by (FAMILY, FUNCTION, TILE COUNT) — one level + * below the per-residency report, with no card and no device in it. + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS: "293.8 us EACH" IS THE SAME AVERAGE, ONE LEVEL DOWN + * --------------------------------------------------------------------------- + * `qwen35_tt_report_residencies()` was written because "162 descriptors a token, 247 us + * each" averaged over families whose work differs by three orders of magnitude. It replaced + * that with a line per residency — and the line it produced for the elementwise family reads + * **136 descriptors/token, 293.8 us each**, which is the identical mistake at the next level: + * + * - the eltwise residency runs SIX different functions (silu, sigmoid, add, mul, sub, + * silu_mul), unary and binary, over at least THREE different widths (`n_embd/32`, the + * FFN width, the attention gate's width); + * - a unary SIGMOID over the gate and a binary SILU_MUL over the FFN are both "one + * descriptor", and one reads half the bytes of the other; + * - the two RENDER directions run the same three kernel sources in two other programs at + * two other page sizes, and they are **not in the residency table at all** — the + * bridge's ~0.022 s/token has only ever been a SUBTRACTION between runs. + * + * So the on-card term's largest non-matmul family has never been decomposed, and the second + * largest is invisible to the instrument. This file is the missing key. + * + * --------------------------------------------------------------------------- + * AND WHAT THE KEY BUYS THAT A COUNT CANNOT + * --------------------------------------------------------------------------- + * A family that runs the SAME program over SEVERAL widths prices its own per-descriptor + * fixed cost, from one ordinary run, with no extra card arm: + * + * seconds_per_descriptor(width) = fixed + per_tile * tile_count + * + * `pn_tt_descriptor_census_fit()` solves that line by weighted least squares over a family's + * rows. **The intercept is what merging descriptors removes and the slope is what it does + * not** — which is the one question a "N descriptors x 293.8 us" figure cannot answer, and + * the one that three levers here have already been scoped wrong from (two of which measured + * exactly 0 when built). + * + * The intercept is NOT "the host round trip" and must not be reported as one. It is + * everything that does not scale with the strip: the parameter write, the ring write, the + * head bump, the first poll's PCIe latency, the kernel's own per-descriptor prologue, the CB + * rebase and the completion post. Separating THOSE needs + * `pn_tt_residency_submit_repeat()`, which is a different instrument and a card arm. + * + * --------------------------------------------------------------------------- + * WHAT IT REFUSES, AND WHY EACH REFUSAL IS LOUD + * --------------------------------------------------------------------------- + * A census that silently drops rows reads exactly like a census of a cheaper model. So: + * + * - a table overflow does NOT fold into an "other" bucket — it increments `dropped`, and + * any reader that prints a census must print `dropped` beside it; + * - a fit over ONE distinct width is refused, not answered. Two unknowns need two + * equations, and a line through one point is the arithmetic that produced the retired + * "958 ns per single-tile op". TWO guards enforce this — the named shape count and the + * normal equation's determinant — and they are REDUNDANT WITH EACH OTHER: mutating either + * one away leaves the suite green and only removing BOTH goes red (4 failures). They are + * both kept, and that redundancy is stated here rather than left as a check nobody can + * prove red on its own; + * - a non-finite duration is refused at record time, because `failed = value > limit` is + * false for a NaN and a NaN that reaches a fit poisons every figure downstream. + * + * --------------------------------------------------------------------------- + * A DESCRIPTOR THAT REFUSED AND WAS REPLAYED IS NOT A DESCRIPTOR'S COST + * --------------------------------------------------------------------------- + * Every resident family in this backend submits under a recovery: if the residency declines + * the descriptor the host RE-ARMS it — a full teardown and relaunch — and submits again, and + * the caller's stopwatch spans all three. Folding that bracket into `seconds / ops` prices a + * teardown as arithmetic, and it has already produced a wrong headline number: PHASE CONV read + * **4,502 us a descriptor against 738.2** for no reason but a circular-buffer write pointer + * that made every CONV after a DELTA refuse. The recorded "CONV is DATA-DEPENDENT, 4,571 us on + * a degenerate stream" has the same shape and no surviving harness. + * + * So `ops` and `seconds` are the CLEAN descriptors — first attempt, retired — and a replayed + * one goes to `recovered_ops` / `recovered_seconds` / `rearms` instead. Three consequences, + * and the middle one is the point: + * + * - `pn_tt_descriptor_census_fit()` sees clean rows only, so a run with refusals fits the + * op rather than the recovery; + * - a reader that prints `us each` MUST print `rearms` beside it, exactly as it must print + * `dropped` — a contaminated figure that looks like a clean one is the whole defect; + * - `pn_tt_descriptor_census_family_totals()` answers the CLEAN totals, so wall-time + * accounting that wants everything adds `pn_tt_descriptor_census_family_recovery()`. + * + * No device, no allocation, no I/O — deliberately, so it is provable with no card. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TT_DESCRIPTOR_CENSUS_H +#define PN_TT_DESCRIPTOR_CENSUS_H + +#include +#include + +/** Distinct (family, function, tile count) keys the table holds. Ornith's decode path uses + * well under half of these; the margin is for a hybrid arm that runs more shapes. */ +#define PN_TT_DESCRIPTOR_CENSUS_ROWS 64u + +/** Key strings are COPIED, not borrowed, so a caller may pass a stack buffer. */ +#define PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES 24u + +/** One key and everything measured against it. */ +typedef struct pn_tt_descriptor_census_row { + char family[PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES]; /**< "eltwise", "render-widen", ... */ + char function[PN_TT_DESCRIPTOR_CENSUS_NAME_BYTES]; /**< "add", "silu_mul", "widen", ... */ + uint32_t tile_count; /**< tiles the descriptor covers */ + uint64_t ops; /**< CLEAN descriptors on this key — first attempt, retired */ + double seconds; /**< summed submit wall time of those clean descriptors */ + /** Descriptors the residency REFUSED and the host replayed after a re-arm, and the whole + * bracket they cost. Never folded into `seconds`: a teardown is not a descriptor's work, + * and `seconds / ops` is quoted as one. */ + uint64_t recovered_ops; + double recovered_seconds; + /** Re-arms performed inside those brackets. `>= recovered_ops`; a caller that re-arms more + * than once for one descriptor says so. */ + uint64_t rearms; +} pn_tt_descriptor_census_row; + +/** The whole instrument. Zero-initialise it; there is no constructor. */ +typedef struct pn_tt_descriptor_census { + pn_tt_descriptor_census_row rows[PN_TT_DESCRIPTOR_CENSUS_ROWS]; + uint32_t row_count; + /** Descriptors that matched no row and found no free slot, and their time. NEVER folded + * into a row: an "other" bucket makes a truncated census look complete. */ + uint64_t dropped; + double dropped_seconds; + /** Descriptors refused at record time — a bad width or a non-finite duration. Kept apart + * from `dropped` because they mean a defect upstream, not a table too small. */ + uint64_t refused; +} pn_tt_descriptor_census; + +/** + * Add one descriptor's time to its key, creating the row on first sight. + * + * @param census the table; NULL is refused + * @param family residency-level name, truncated to the key width + * @param function what the descriptor computed, truncated to the key width + * @param tile_count tiles the descriptor covers; 0 is refused + * @param seconds measured submit wall time; non-finite or negative is refused + * @return 0, or -1 with `refused` or `dropped` incremented + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_descriptor_census_record(pn_tt_descriptor_census *census, const char *family, + const char *function, uint32_t tile_count, double seconds); + +/** + * Add one REPLAYED descriptor's whole bracket — refusal, re-arm, resubmit — to its key. + * + * Identical keying and identical refusals to `pn_tt_descriptor_census_record()`; the only + * difference is which pair of counters moves, and that difference is the point. Nothing this + * function records ever reaches `seconds / ops` or the fit. + * + * @param census the table; NULL is refused + * @param family residency-level name, truncated to the key width + * @param function what the descriptor computed, truncated to the key width + * @param tile_count tiles the descriptor covers; 0 is refused + * @param seconds the WHOLE bracket, not the replay alone; non-finite or negative refused + * @param rearms re-arms performed inside the bracket; 0 is refused, because a recovered + * descriptor that re-armed nothing is a caller that mislabelled a clean one + * @return 0, or -1 with `refused` or `dropped` incremented + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_descriptor_census_record_recovered(pn_tt_descriptor_census *census, + const char *family, const char *function, + uint32_t tile_count, double seconds, + uint64_t rearms); + +/** + * Read row `index` back, in first-sight order. + * + * @return false once `index` reaches `row_count`, which is the loop's terminator + */ +bool pn_tt_descriptor_census_row_at(const pn_tt_descriptor_census *census, uint32_t index, + const pn_tt_descriptor_census_row **out_row); + +/** + * Sum one family's rows. + * + * @param family matched exactly against the recorded key; NULL sums EVERY row + * @return 0, or -1 when the family has no rows (so a caller cannot print zeros + * for a family that never ran and read it as "free") + */ +int pn_tt_descriptor_census_family_totals(const pn_tt_descriptor_census *census, + const char *family, uint64_t *out_ops, + double *out_seconds, uint32_t *out_shapes); + +/** + * The same family's REPLAYED descriptors — what `..._family_totals()` deliberately leaves out. + * + * A reader that prints a per-descriptor figure has to print `out_rearms` beside it. It is + * answered as 0/0/0 rather than refused when the family ran clean, because "the family has no + * recovery" and "the family never ran" are different facts and only the second is a refusal. + * + * @param family matched exactly against the recorded key; NULL sums EVERY row + * @param out_recovered_ops descriptors that refused and were replayed + * @param out_recovered_seconds their whole brackets, summed + * @param out_rearms re-arms performed inside them + * @return 0, or -1 when the family has no rows at all + */ +int pn_tt_descriptor_census_family_recovery(const pn_tt_descriptor_census *census, + const char *family, uint64_t *out_recovered_ops, + double *out_recovered_seconds, + uint64_t *out_rearms); + +/** + * Solve `seconds_per_descriptor = fixed + per_tile * tile_count` for one family, by least + * squares over its rows weighted by descriptor count. + * + * REFUSED unless the family shows at least two DISTINCT tile counts. Read the header's + * "what the key buys" block before quoting the intercept as a host round trip — it is not + * one, and this function cannot tell you that it is. + * + * CLEAN ROWS ONLY. A row whose descriptors all refused and were replayed contributes no + * equation, so a family that never retired a first attempt is refused rather than fitted to + * its own recovery path. + * + * @param out_fixed_seconds intercept, seconds per descriptor at zero tiles + * @param out_seconds_per_tile slope, seconds per tile + * @param out_shapes distinct tile counts the fit used; NULL to ignore + * @return 0, or -1 (no such family, one shape only, or a degenerate + * normal equation) + */ +int pn_tt_descriptor_census_fit(const pn_tt_descriptor_census *census, const char *family, + double *out_fixed_seconds, double *out_seconds_per_tile, + uint32_t *out_shapes); + +#endif /* PN_TT_DESCRIPTOR_CENSUS_H */ diff --git a/src/accel/tt/pn_tt_device.c b/src/accel/tt/pn_tt_device.c new file mode 100644 index 0000000..603105f --- /dev/null +++ b/src/accel/tt/pn_tt_device.c @@ -0,0 +1,2112 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_device.c + * @brief Implementation of the Tenstorrent device handle (see pn_tt_device.h). + * + * @status Phase 7. The card, the artifact residency and the matmul live here; the reduce, + * elementwise and sampling residencies that keep the activation ON the card between + * matmuls live in pn_tt_oncard.c. 0.568 -> 0.229 s/token with the four on-card ops. + * Tensix programs are resolved BY NAME out of the binary's own blob (Phase 12): there is + * no program path in this file, and PN_TT_PROGRAM_DIR is an override, not a default. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_device_internal.h" +#include "pn_tt_gdngate.h" +#include "pn_tt_reader.h" +#include "pn_tt_core_boot_guard.h" +#include "pn_tt_dispatch.h" +#include "pn_tt_eltwise_group.h" +#include "pn_tt_gdnc.h" +#include "pn_tt_gate.h" +#include "pn_tt_render.h" +#include "pn_tt_replica_map.h" + +#include +#include +#include +#include + +#include "tt/convert/ft_float_convert.h" +#include "tt/ft_tt_coord.h" +#include "tt/ft_tt_device.h" +#include "tt/ft_tt_kmd.h" +#include "tt/ft_tt_op_dispatch.h" +#include "tt/ft_tt_op_l1_layout.h" +#include "tt/ft_tt_op_matmul.h" +#include "tt/ft_tt_program_resolve.h" +#include "tt/ft_tt_tile_row0.h" + +/** The one-shot matmul triads, by NAME. A name is the thing a shipping binary can hold; the + * directory they were built in is a property of one machine and is gone from this file. */ +#define PN_TT_MATMUL_BFP8_PROGRAM "matmul_bfp8" +#define PN_TT_MATMUL_BFP4_PROGRAM "matmul_bfp4" + +/* PN_TT_RESIDENT_PROGRAM moved to pn_tt_device_internal.h: the matmul group's DST interlock + * reads the ARMED program's recorded build defines, so the program's NAME is no longer + * private to this file. */ + +/** `resident` (default when the program is there) or `oneshot`. Both arms are the SAME + * binary running the same kernels on the same weights, so an A/B across this knob measures + * DISPATCH and nothing else. */ +#define PN_TT_DISPATCH_ENV "PN_TT_DISPATCH" + +/** + * RED PROOF ONLY. `PN_TT_RED_BAD_DESCRIPTOR=N` malforms the Nth resident descriptor's + * parameter block, AFTER the host check that would have refused it, so the kernel's own + * refusal runs on real silicon. + * + * It is a knob and not a test-only build because the thing being proven is a property of + * the SHIPPING path — that a refused descriptor is recoverable rather than a lost core — + * and a separate binary would prove it about a different program. It defaults off, it is + * read once, and it fires once. + */ +#define PN_TT_RED_BAD_DESCRIPTOR_ENV "PN_TT_RED_BAD_DESCRIPTOR" + +/** Where to append every projection's result row. See the struct field for why bytes. */ +#define PN_TT_MATVEC_DUMP_ENV "PN_TT_MATVEC_DUMP" + +/** Which enabled Tensix to drive. 71 is in the set the bring-up log records as verified + * healthy after the recovery round; it is a default, not a constant — a serve with a + * placement should pass its own. */ +#define PN_TT_CORE_DEFAULT 71u +#define PN_TT_CORE_ENV "PN_TT_CORE" + +/** Every processor of the one-shot matmul triad. */ +#define PN_TT_TRIAD_MASK \ + ((1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1) | (1u << FT_TT_PROC_MATH0) | \ + (1u << FT_TT_PROC_MATH1) | (1u << FT_TT_PROC_MATH2)) + +/** K-tiles the reader fetches per NoC barrier. Shrunk to a divisor of Kt by the driver, so + * this is a ceiling and not a promise; 8 divides every Kt this model family uses (48, 64, + * 144) and keeps the circular buffers far inside the arena. + * + * `PN_TT_PREFETCH_TILES` overrides it, and it is a knob rather than a constant because it + * is the ONE number that moves the cooperative matmul's cost: the multicast rendezvous is + * paid ONCE PER BLOCK, so doubling the depth halves how many of them a projection pays. + * The driver still shrinks whatever is asked for to a divisor of the artifact's Kt gcd, + * and the circular buffers are `2 * depth` pages of each input — at depth 16 that is + * ~100 KiB of a 1.32 MB arena, so the ceiling is nowhere near. */ +#define PN_TT_PREFETCH_TILES_ENV "PN_TT_PREFETCH_TILES" +#define PN_TT_PREFETCH_TILES_DEFAULT 8u + +/* ================================================================ + * Small helpers + * ================================================================ */ + +double pn_tt_now_seconds(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0.0; + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +uint32_t pn_tt_env_u32(const char *name, uint32_t fallback) +{ + const char *value = getenv(name); + char *end = NULL; + unsigned long parsed; + + if (!value || !*value) + return fallback; + parsed = strtoul(value, &end, 10); + if (!end || *end != '\0') + return fallback; + return (uint32_t)parsed; +} + +/** The program directory for a weight tile size, or NULL if we hold no program for it. */ +static const ft_tt_kernel_program *pn_tt_program_for(const pn_tt_device *device, + uint32_t tile_bytes) +{ + if (tile_bytes == FT_TT_OP_TILE_BYTES_BFP8_B) + return &device->program_bfp8.program; + if (tile_bytes == FT_TT_OP_TILE_BYTES_BFP4_B && device->have_bfp4) + return &device->program_bfp4.program; + return NULL; +} + +const char *pn_tt_program_override_root(void) +{ + const char *root = getenv(PN_TT_PROGRAM_ROOT_ENV); + + return (root && *root) ? root : NULL; +} + +const char *pn_tt_program_where(const char *root) +{ + return root ? root : "this binary's embedded program blob"; +} + +/** + * Refuse two programs whose firmware differs. + * + * A kernel's text is linked at its own firmware's `__fw_export_text_end`, so booting one + * program's firmware and then loading another program's kernels is not a configuration, + * it is a wedged core — and a wedged core here costs a card-level reset. Comparing the five + * ELFs byte for byte turns that into a refusal. + * + * By NAME, not by directory: on the embedded arm the two firmware sets are usually the same + * deduplicated object, and comparing bytes rather than that pointer is deliberate — the + * shared pointer is the generator's claim and the bytes are the fact. + */ +static int pn_tt_firmware_matches(const char *name_a, const char *name_b, const char *root) +{ + ft_tt_runtime_images a, b; + unsigned processor; + int same = 1; + + if (ft_tt_program_open_firmware(name_a, root, &a) != FT_TT_OK) + return 0; + if (ft_tt_program_open_firmware(name_b, root, &b) != FT_TT_OK) { + ft_tt_runtime_images_release(&a); + return 0; + } + for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) { + if (a.elf_size[processor] != b.elf_size[processor] || + memcmp(a.elf_data[processor], b.elf_data[processor], a.elf_size[processor]) != 0) { + same = 0; + break; + } + } + ft_tt_runtime_images_release(&a); + ft_tt_runtime_images_release(&b); + return same; +} + +/* ================================================================ + * Open / close + * ================================================================ */ + +/** Resolve the core, boot its firmware, and load the matmul programs. */ +static int pn_tt_bring_up_core(pn_tt_device *device, const char *root) +{ + const char *dispatch = getenv(PN_TT_DISPATCH_ENV); + ft_tt_noc_coord core; + int rc; + + device->red_bad_descriptor = pn_tt_env_u32(PN_TT_RED_BAD_DESCRIPTOR_ENV, 0u); + device->red_split = pn_tt_matmul_group_red_mode(); + /* How many DRAM banks the hot activation strips are mirrored into. Clamped rather than + * refused, because the bank count is a property of the part and a run that asked for + * more should get every bank rather than a failure to open the card. */ + device->activation_replicas = pn_tt_env_u32(PN_TT_ACTIVATION_REPLICAS_ENV, + PN_TT_ACTIVATION_REPLICAS_DEFAULT); + if (device->activation_replicas == 0u) + device->activation_replicas = 1u; + device->core_index = pn_tt_env_u32(PN_TT_CORE_ENV, PN_TT_CORE_DEFAULT); + rc = ft_tt_coord_tensix_by_index(&device->runtime.grid, device->core_index, + FT_TT_COORD_TRANSLATED, &core); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: Tensix index %u is not on this part\n", device->core_index); + return -1; + } + device->core_x = (uint16_t)core.x; + device->core_y = (uint16_t)core.y; + + if (ft_tt_program_open_firmware(PN_TT_MATMUL_BFP8_PROGRAM, root, &device->firmware) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: no %s firmware in %s\n", PN_TT_MATMUL_BFP8_PROGRAM, + pn_tt_program_where(root)); + return -1; + } + /* Say WHERE the programs came from, once, before anything runs on them. The whole point + * of the blob is that a serve no longer depends on a directory existing, and a run that + * cannot state which arm it took is a run whose measurements name no artifact. */ + fprintf(stderr, "pn_tt: Tensix programs — source %s (%s)\n", + ft_tt_program_source_string(device->firmware.source), pn_tt_program_where(root)); + + if (ft_tt_program_open_kernels(PN_TT_MATMUL_BFP8_PROGRAM, root, PN_TT_TRIAD_MASK, + &device->program_bfp8) != FT_TT_OK) { + fprintf(stderr, "pn_tt: no Bfp8_b matmul program in %s\n", pn_tt_program_where(root)); + return -1; + } + /* Bfp4_b is optional: a model whose FFN is not block-float-4 never asks for it, and + * refusing the whole card because one program is missing would be wrong. */ + device->have_bfp4 = pn_tt_firmware_matches(PN_TT_MATMUL_BFP8_PROGRAM, + PN_TT_MATMUL_BFP4_PROGRAM, root) && + ft_tt_program_open_kernels(PN_TT_MATMUL_BFP4_PROGRAM, root, + PN_TT_TRIAD_MASK, + &device->program_bfp4) == FT_TT_OK; + if (!device->have_bfp4) + fprintf(stderr, "pn_tt: no usable Bfp4_b matmul program in %s " + "(missing, or built against different firmware)\n", + pn_tt_program_where(root)); + + /* The resident triad is NOT optional, and the previous "serve one-shot rather than + * refuse" is retracted (2026-08-15). The program went missing from the host's program + * root for a day and nothing said so: MEASURED on this card, same binary, same weights, + * same on-card families, only this knob moved — resident 0.343 s a token against + * one-shot 0.372, i.e. **+0.029 s, 1.085x**, entirely in the dispatch phase (0.156 -> + * 0.185). That is small enough to publish by accident and large enough to be wrong, and + * it is exactly the size of regression a silent fallback is good at hiding. A + * configuration this backend cannot honour is a refusal; the slow path stays reachable, + * but it has to be ASKED for. + * + * The firmware comparison is not a formality either: a kernel's text is linked at ITS + * OWN firmware's `__fw_export_text_end`, so booting one program's firmware and then + * loading another's kernels is a wedged core, and a wedged core here costs a card-level + * reset. That is why a MISMATCH is refused on the same terms as an absence. */ + device->resident_requested = !(dispatch && strcmp(dispatch, "oneshot") == 0); + device->have_resident = false; + if (device->resident_requested) { + if (!pn_tt_firmware_matches(PN_TT_MATMUL_BFP8_PROGRAM, PN_TT_RESIDENT_PROGRAM, root) || + ft_tt_program_open_kernels(PN_TT_RESIDENT_PROGRAM, root, PN_TT_TRIAD_MASK, + &device->program_resident) != FT_TT_OK) { + fprintf(stderr, + "pn_tt: REFUSED — no usable %s program in %s (missing, or " + "built against different firmware than %s).\n" + "pn_tt: one-shot dispatch re-uploads five kernel images per op and costs " + "1.085x the seconds per token (measured 0.343 -> 0.372 on MiniCPM5-1B), " + "while reporting a token that looks healthy — so falling back to it " + "silently is the defect this refusal replaces.\n" + "pn_tt: build it (foundry/tt/tests/Makefile.phase6 carries the recipe) and " + "regenerate the embedded blob (cd foundry/tt/tools && make regen-programs " + "— that cwd is required), or ask for the slow path deliberately with " + "%s=oneshot.\n", + PN_TT_RESIDENT_PROGRAM, pn_tt_program_where(root), + PN_TT_MATMUL_BFP8_PROGRAM, PN_TT_DISPATCH_ENV); + return -1; + } + device->have_resident = true; + } + + /* THE FIRST BOOT A SERVE TAKES, AND THEREFORE THE ONE A RESTART LANDS ON. This core is + * matmul member 0; if the previous process left a compute thread spinning here, this is + * where an ordinary restart wedges the card. The card is asked before anything is + * written — `pn_tt_core_boot_guard.h`. */ + if (!pn_tt_core_boot_is_permitted(&device->runtime, device->core_index, "matmul member 0", + PN_TT_CORE_ENV)) + return -1; + + rc = ft_tt_runtime_boot_core(&device->runtime.noc, device->core_x, device->core_y, + &device->firmware); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: core %u (%u,%u) firmware boot failed: %s\n", device->core_index, + core.x, core.y, ft_tt_error_string(rc)); + return -1; + } + fprintf(stderr, "pn_tt: core %u at translated (%u,%u), firmware up\n", device->core_index, + core.x, core.y); + return 0; +} + +int pn_tt_device_open(int device_index, pn_tt_device **out_device) +{ + /* NULL means the embedded blob, and NULL is the normal case. Passed through rather than + * defaulted to a path: the default IS the binary's own programs. */ + const char *root = pn_tt_program_override_root(); + pn_tt_device *device = NULL; + uint32_t bank; + int rc; + + if (!out_device) + return -1; + *out_device = NULL; + + device = calloc(1, sizeof(*device)); + if (!device) + return -1; + /* 2, which is where the artifact load has always started its bank rotation. It is a + * device field now so a post-load restage continues the same walk. */ + device->weight_bank_cursor = 2u; + /* EVERY on-card family is on by default EXCEPT attention, and the exception is per + * FAMILY rather than global — `pn_tt_device_oncard_default()` is how a backend that has + * the evidence turns it on for its own model. The qwen35/bonsai hybrid does; the llama + * backend does not, because on-card attention there fails MiniCPM5-1B's chat-parity gate + * and a shared default would have shipped that as a side effect. */ + for (uint32_t op = 0; op < PN_TT_ONCARD_COUNT; op++) + device->oncard_default[op] = (op != (uint32_t)PN_TT_ONCARD_ATTENTION); + + rc = ft_tt_runtime_open(device_index, &device->runtime); + if (rc != FT_TT_OK) { + free(device); + return -1; + } + /* Which transport served this open, stated at the boundary. `path=gpu` and a + * non-zero dispatch count prove the CARD ran the forward; this line is what + * says WHICH DRIVER it ran through, and the two questions are independent. */ + { + char address[32]; + + if (ft_tt_runtime_pci_address(&device->runtime, address, sizeof(address)) != FT_TT_OK) + (void)snprintf(address, sizeof(address), "(address unavailable)"); + fprintf(stderr, "pn_tt: device %d open over the %s transport, PCI %s\n", device_index, + ft_tt_runtime_driver_string(device->runtime.driver), address); + } + if (pn_tt_bring_up_core(device, root) != 0) + goto fail; + + device->dram_bank_count = ft_tt_coord_dram_bank_count(&device->runtime.grid); + if (device->dram_bank_count == 0 || device->dram_bank_count > FT_TT_MAX_DRAM_BANKS) + goto fail; + for (bank = 0; bank < device->dram_bank_count; bank++) { + if (ft_tt_coord_dram_bank(&device->runtime.grid, bank, FT_TT_COORD_TRANSLATED, + &device->dram[bank]) != FT_TT_OK) + goto fail; + } + if (ft_tt_allocator_init(&device->allocator, device->runtime.ctx, &device->runtime.grid, 0) != + FT_TT_OK) + goto fail; + /* CLAMPED HERE and not where the env is read, because the bank count is a property of + * the part. More mirrors than banks would have to put two of them in one bank, which is + * the exact condition mirroring exists to remove. */ + if (device->activation_replicas > device->dram_bank_count) { + fprintf(stderr, "pn_tt: %s=%u is more than this card's %u banks — using %u\n", + PN_TT_ACTIVATION_REPLICAS_ENV, device->activation_replicas, + device->dram_bank_count, device->dram_bank_count); + device->activation_replicas = device->dram_bank_count; + } + if (device->activation_replicas > PN_TT_ACTIVATION_MAX_REPLICAS) + device->activation_replicas = PN_TT_ACTIVATION_MAX_REPLICAS; + if (device->activation_replicas > 1u) + fprintf(stderr, "pn_tt: the hot activation strips are mirrored into %u DRAM banks — " + "matmul member m reads mirror m %% %u\n", + device->activation_replicas, device->activation_replicas); + /* AFTER the banks, because the group's activation door table is one assignment per + * bank, and after the core, because member 0 IS that core. */ + if (pn_tt_matmul_group_configure(device) != 0) + goto fail; + + *out_device = device; + return 0; + +fail: + pn_tt_device_close(device); + return -1; +} + +/* ================================================================ + * The resident triad + * + * The one-shot path re-uploads five kernel images per op — 36.2 µs/kB, ~0.22 ms before + * anything is computed. A resident triad is the same three kernels built to NOT return: + * five processors sit on the core reading 32-byte descriptors out of its own L1, and the + * host's per-op work becomes a parameter block, a descriptor and a 4-byte head bump with + * no round trip. ft_tt_queue.h is the protocol and states the consequence for op authors: + * DO NOT FUSE OPS TO SAVE DISPATCH — there is no dispatch to save. + * + * ALL OF IT — including the one-member case — now lives in pn_tt_matmul_group.c, because a + * projection may be split across several cores and a residency per core is what that costs. + * A group of one is this file's old path with the same core, the same queue and the same + * sixteen descriptor words, which is what makes PN_TT_MATMUL_CORES=1 a control rather than + * a second implementation. + * ================================================================ */ + +int pn_tt_device_keepalive(pn_tt_device *device, double min_idle_seconds) +{ + static int enabled = -1; + int sent; + + if (enabled < 0) { + const char *value = getenv("PN_TT_KEEPALIVE"); + + enabled = (value && *value) ? (strcmp(value, "0") != 0) : 1; + } + if (!enabled || !device || !pn_tt_dispatch_quiet() || device->gdn_pending_members != 0u) + return 0; + sent = pn_tt_residency_keepalive_all(min_idle_seconds); + { + const int group = pn_tt_matmul_group_keepalive(device, min_idle_seconds); + + sent = (sent < 0 || group < 0) ? -1 : sent + group; + } + if (sent != 0) + pn_tt_dispatch_sequence_reset(); + if (sent > 0) + device->keepalive_nops += (uint64_t)sent; + return sent; +} + +void pn_tt_device_close(pn_tt_device *device) +{ + if (!device) + return; + /* The reader thread holds its own window bank: joined before anything is torn down. */ + pn_tt_reader_close(device); + /* The residency is retired by ASKING; the one-shot path has nothing to retire, because + * it leaves the core in its firmware's GO wait, which is where a healthy idle core sits. + * Neither is PARKED, deliberately — parking costs the core its next boot even after a + * textbook-clean shutdown (foundry/tt/DEV_NOTES.md §1). */ + /* Say what the dump CONTAINS before anything can read it. A comparison of two empty + * files is green, and "the two arms agreed" would then be a statement about nothing. */ + if (device->matvec_dump) { + fprintf(stderr, "pn_tt: matvec dump — %llu result rows, %llu bytes\n", + (unsigned long long)device->matvec_dump_rows, + (unsigned long long)device->matvec_dump_bytes); + fclose(device->matvec_dump); + device->matvec_dump = NULL; + } + /* Clear the death-stop BEFORE retiring, so the driver's write cannot fire on a clean + * exit. It would be harmless (a store to L1 nobody reads), but an action the driver + * performs on every close is an action nobody can attribute to a cause — and the whole + * value of this registration is that when it DOES fire, it means the process died. */ + { + ft_tt_kmd_device *kmd = ft_tt_ctx_kmd_device(device->runtime.ctx); + + if (kmd) + (void)ft_tt_kmd_clear_noc_cleanup(kmd); + } + pn_tt_matmul_grid_close(device); + pn_tt_matmul_group_retire(device); + pn_tt_residency_release(&device->reduce); + pn_tt_residency_release(&device->reduce_member); + { + uint32_t f; + + for (f = 0; f < device->idle_fill_count; f++) + pn_tt_residency_release(&device->idle_fill[f]); + device->idle_fill_count = 0u; + } + device->reduce_members = 0u; + /* THE ELEMENTWISE SPLIT'S EXTRA MEMBERS GO FIRST, because the release below is what makes + * `eltwise_members` fall back to 0 and the set is walked from that count. A leaked triad + * is a WEDGED Tensix for the next process to ask for that core — the failure the head-norm + * slots below cost on 2026-08-21. */ + pn_tt_eltwise_group_release(device); + pn_tt_residency_release(&device->eltwise); + device->eltwise_members = 0u; + pn_tt_residency_release(&device->sample); + { + uint32_t m; + + for (m = 1u; m < device->sample_members; m++) + pn_tt_residency_release(&device->sample_member[m - 1u]); + device->sample_members = 0u; + } + pn_tt_residency_release(&device->rope); + pn_tt_residency_release(&device->memory); + pn_tt_residency_release(&device->puller); + pn_tt_residency_release(&device->attention); + for (uint32_t member = 1u; member < device->attention_members; member++) + pn_tt_residency_release(&device->attention_member[member - 1u]); + device->attention_members = 0u; + /* THE PER-PHASE FIGURE PRINTS AT RELEASE, NOT ON A TOKEN MULTIPLE, and that is a fix + * rather than a duplicate of the report line. + * + * `qwen35_tt_report_gdn()` runs under `tokens % PN_TT_REPORT == 0`, so an arm that + * CORRUPTS the model's output — which every data arm on this op does by construction — + * shortens the run below the threshold and the instrument that would have scored the arm + * silently does not print at all. Measured 2026-08-27: three CONV data arms generated 8 + * tokens against the control's 48 at `PN_TT_REPORT=48` and produced no `gdn phase` line, + * leaving a result file that reads like a completed arm with a number missing. The + * chunkwise family never had the defect because `pn_tt_gdnc_release()` prints + * unconditionally; this is the decode family being given the same property. */ + pn_tt_gdn_report_phases(device); + pn_tt_residency_release(&device->gdn); + /* The chunkwise family, which also frees its constants. A leaked triad is a WEDGED Tensix + * for the next process to ask for that core — see the per-head reduce note below. */ + pn_tt_gdnc_release(device); + pn_tt_gdngate_release(device); + /* The head split's extra members. `gdn_members` counts member 0, released above, so this + * gives back one fewer; at the shipped default of 1 the loop does not run. */ + { + uint32_t member; + + for (member = 1u; member < device->gdn_members; member++) + pn_tt_residency_release(&device->gdn_member[member - 1u]); + device->gdn_members = 0u; + } + /* THE PER-HEAD REDUCE SLOTS, WHICH THIS FUNCTION USED TO LEAK. Every other family is a + * NAMED FIELD released above; these are a SET whose size is discovered at arm time, and + * being a loop rather than a field is how they were missed. MEASURED 2026-08-21 on the + * first arm that ran attention and the GDN block together: after a clean exit + * `ft_tt_core probe` reported cores 91 and 103 as `SKIP refused: a kernel is running on + * this core` while every named residency came back PASS — and one slot leaked the same + * way on every single-width arm before it, which is why `before` probed 20 PASS and + * `after` probed 19. A leaked triad is not untidiness: the next process to ask for that + * core boots a second program's firmware onto a core that already holds one, which is a + * WEDGED Tensix rather than a configuration. A `serve` reloading a model does exactly + * that. */ + { + uint32_t slot; + + for (slot = 0; slot < device->head_norm_count; slot++) { + pn_tt_residency_release(&device->head_norm[slot].residency); + for (uint32_t m = 1u; m < device->head_norm[slot].members; m++) + pn_tt_residency_release(&device->head_norm[slot].member[m - 1u]); + device->head_norm[slot].members = 0u; + } + device->head_norm_count = 0u; + } + /* The activation strip bridge's two directions, and their zero operands with them. A + * residency this function forgets WEDGES THE NEXT PROCESS — the head-norm slots were leaked + * on every arm since the norms moved and made cores 91/103 probe "a kernel is running" + * after a clean exit. The merged gate is released for the same reason. */ + pn_tt_render_release(device); + pn_tt_gate_release(device); + free(device->kv_region); + device->kv_region = NULL; + free(device->gdn_region); + device->gdn_region = NULL; + free(device->gdn_checkpoint); + device->gdn_checkpoint = NULL; + if (device->artifact_open) + ft_tt_fttw_close(&device->artifact); + if (device->extra_open) + ft_tt_fttw_close(&device->extra_artifact); + free(device->attn_constants_host); + device->attn_constants_host = NULL; + ft_tt_runtime_images_release(&device->program_bfp8); + ft_tt_runtime_images_release(&device->program_bfp4); + ft_tt_runtime_images_release(&device->program_resident); + ft_tt_runtime_images_release(&device->firmware); + ft_tt_runtime_close(&device->runtime); + free(device->resident); + free(device->row_in); + free(device->row_out); + free(device->tile_stage); + free(device->rows_stage); + free(device->gdn_stage); + free(device->gdn_window_shadow); + free(device->gdn_qkv_history); + free(device); +} + +/* ================================================================ + * Weight residency + * ================================================================ */ + +static uint32_t pn_tt_gcd(uint32_t a, uint32_t b) +{ + while (b != 0u) { + uint32_t r = a % b; + + a = b; + b = r; + } + return a; +} + +/** + * Widest shape any matmul in the artifact will ask for, plus the two facts a RESIDENCY + * needs decided before it is launched rather than per op. + * + * A resident binary is bound to one weight tile size and its circular buffers are placed + * once, so both of those are properties OF THE ARTIFACT: `nmajor_tile_bytes` is the single + * matmul tile size, or 0 if the artifact mixes two (which needs two residencies and one + * core cannot hold two — ft_tt_op_l1_layout.h's divergence 2). `kt_gcd` is what the one + * prefetch depth has to divide, because a depth that does not divide a later descriptor's + * Kt collapses to 1 on the card and walks the CB write pointer off the block grid. + */ +static void pn_tt_scan_extents(pn_tt_device *device) +{ + uint32_t index; + uint32_t kt_gcd = 0u; + uint32_t tile_bytes = 0u; + bool mixed = false; + + for (index = 0; index < device->artifact.header.tensor_count; index++) { + const ft_tt_artifact_entry *entry = &device->artifact.directory[index]; + uint32_t entry_tile_bytes; + + if (entry->layout != FT_TT_LAYOUT_MATMUL_NMAJOR) + continue; + if (entry->tile_rows > device->max_tile_rows) + device->max_tile_rows = entry->tile_rows; + if (entry->tile_cols > device->max_tile_cols) + device->max_tile_cols = entry->tile_cols; + kt_gcd = pn_tt_gcd(kt_gcd, entry->tile_rows); + entry_tile_bytes = ft_tt_fttw_tile_bytes(entry->format); + if (tile_bytes == 0u) + tile_bytes = entry_tile_bytes; + else if (tile_bytes != entry_tile_bytes) + mixed = true; + } + device->max_in_features = device->max_tile_rows * PN_TT_TILE_DIM; + device->max_out_features = device->max_tile_cols * PN_TT_TILE_DIM; + device->artifact_kt_gcd = kt_gcd; + device->artifact_tile_bytes = mixed ? 0u : tile_bytes; +} + +/** + * Launch the residency for THIS artifact, or say in one line why it could not be. + * + * Itself never fatal, because it serves TWO callers with opposite needs: the load path, + * where not arming means the whole run is 1.9x slower and must refuse (that check is at the + * call site, on `resident_running`), and the mid-run re-arm after a refused descriptor, + * where falling through to one-shot for the rest of the token is the difference between a + * recoverable refusal and a lost token — and is counted, not hidden. + */ +static void pn_tt_arm_death_stop(pn_tt_device *device); + +static void pn_tt_arm_residency(pn_tt_device *device) +{ + uint32_t depth; + + if (!device->resident_requested) { + fprintf(stderr, "pn_tt: dispatch = one-shot (%s)\n", PN_TT_DISPATCH_ENV); + return; + } + if (device->artifact_tile_bytes != FT_TT_OP_TILE_BYTES_BFP8_B) { + /* One binary, one slab, one weight tile size. An artifact that mixes Bfp8_b and + * Bfp4_b matmul weights needs two resident programs on one core, which needs XIP or + * a second slab set and is not built. */ + fprintf(stderr, "pn_tt: artifact matmul tile size is %u B, the resident program " + "serves %u B — one-shot dispatch\n", + device->artifact_tile_bytes, FT_TT_OP_TILE_BYTES_BFP8_B); + return; + } + depth = ft_tt_op_block_depth(device->artifact_kt_gcd, + pn_tt_env_u32(PN_TT_PREFETCH_TILES_ENV, + device->prefetch_tiles_preferred + ? device->prefetch_tiles_preferred + : PN_TT_PREFETCH_TILES_DEFAULT)); + if (pn_tt_matmul_group_arm(device, device->artifact_tile_bytes, depth) != FT_TT_OK) { + fprintf(stderr, "pn_tt: residency failed to launch — one-shot dispatch\n"); + device->resident_running = false; + return; + } + fprintf(stderr, "pn_tt: resident triad up on %u core(s), weight tile %u B, depth %u " + "(Kt gcd %u)\n", + device->group.member_count, device->resident_tile_bytes, device->resident_depth, + device->artifact_kt_gcd); + pn_tt_arm_death_stop(device); +} + +/** + * Hand tt-kmd the one NoC write it will perform for us if this process dies abnormally. + * + * --------------------------------------------------------------------------- + * WHICH CORE, AND WHY IT IS A CHOICE AND NOT AN ACCIDENT + * --------------------------------------------------------------------------- + * The driver holds ONE registration per fd, so a forward that runs four matmul members plus + * the on-card families protects exactly one core and the rest keep only `idle_spin_limit`. + * This registers **member 0's queue**, which is `device->queue` on `device->core_x/core_y`, + * for three reasons that are properties of the design rather than preferences: + * + * - It is the residency that exists in EVERY configuration. `PN_TT_MATMUL_CORES=1` is the + * pre-group path and member 0 is the whole of it, so registering any other member would + * leave the default configuration unprotected. + * - It is live for essentially all of decode — 169 projections a token go through it — so a + * kill "mid-generation" almost certainly lands while this core is running or waiting. + * - It is the core a RESTARTED process brings up FIRST (`pn_tt_bring_up_core()` resolves the + * same index before the group does), so it is the core that loses the boot race, and + * booting firmware onto a core with a live resident program is the documented wedge. + * + * Re-armed on every residency arm, including the mid-run re-arm, because `ft_tt_queue_init()` + * gives the ring a fresh nonce and the address is derived from the queue rather than assumed. + * + * A failure here is reported and not fatal: an older driver without the ioctl answers EINVAL, + * and a backend that runs without the safety net is strictly what we had yesterday. + */ +static void pn_tt_arm_death_stop(pn_tt_device *device) +{ + ft_tt_kmd_device *kmd = ft_tt_ctx_kmd_device(device->runtime.ctx); + uint32_t address = ft_tt_queue_stop_request_address(&device->queue); + int rc; + + if (address == 0u) + return; + if (!kmd) { + /* The userspace-driver transport has no kernel to register the write + * with, and that is the ONE thing on ft_tt_udrv.h §5's loss list that + * cannot be replaced — the value of the ioctl is precisely that it + * survives the death of the process. Said out loud once per arm rather + * than silently skipped, because "the death-stop is armed" is a claim + * somebody will otherwise carry across a transport that cannot make it. + * The residency idle-spin bound is what remains. */ + fprintf(stderr, "pn_tt: NO DEATH-STOP on the %s transport — SET_NOC_CLEANUP is a " + "tt-kmd feature and there is no kernel here to hold it. An abnormal " + "death leaves core %u resident until its idle spin limit (measured at " + "over 40 s).\n", + ft_tt_runtime_driver_string(device->runtime.driver), device->core_index); + return; + } + rc = ft_tt_kmd_set_noc_cleanup(kmd, 0u, (uint8_t)device->core_x, (uint8_t)device->core_y, + address, FT_TT_QUEUE_STOP_REQUESTED); + if (rc != 0) { + fprintf(stderr, "pn_tt: SET_NOC_CLEANUP refused (%s) — an abnormal death will leave " + "core %u resident until its idle spin limit\n", + strerror(-rc), device->core_index); + return; + } + fprintf(stderr, "pn_tt: death-stop armed — if this process dies, tt-kmd writes " + "stop_request=1 to core %u (%u,%u) L1 0x%05x. ONE core: members 1..%u and " + "the on-card families keep only the idle spin limit.\n", + device->core_index, device->core_x, device->core_y, address, + device->group.member_count ? device->group.member_count - 1u : 0u); +} + +/** Card and host staging, sized from the extents. Allocated BEFORE the weights so the + * bump allocator's LIFO free order matches the close order. */ +static int pn_tt_alloc_scratch(pn_tt_device *device) +{ + size_t act_bytes = (size_t)device->max_tile_rows * PN_TT_TILE_BF16; + size_t out_bytes = (size_t)device->max_tile_cols * PN_TT_TILE_BF16; + + if (act_bytes == 0 || out_bytes == 0) + return -1; + /* Two different banks: the reader pulls the activation while the writer pushes the + * result, and Phase 6.7 measured two endpoints on one NoC row sharing a ceiling. With + * one core in flight it is not a throughput question, but it costs nothing to keep. */ + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, 0, act_bytes, + &device->activation) != FT_TT_OK) + return -1; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, + device->dram_bank_count > 1 ? 1u : 0u, out_bytes, + &device->result) != FT_TT_OK) + return -1; + + /* BOTH rows are sized to the widest of the two directions. They stage activation + * STRIPS as well as matvec operands now, and a strip may be an input in one op and an + * output in the next — the lm_head's logit row is read into row_out and the attention + * output is written from row_in, and neither is bounded by the other's extent. */ + device->max_row_features = device->max_in_features > device->max_out_features + ? device->max_in_features + : device->max_out_features; + device->row_in = calloc(device->max_row_features, sizeof(*device->row_in)); + device->row_out = calloc(device->max_row_features, sizeof(*device->row_out)); + if (!device->row_in || !device->row_out) + return -1; + return 0; +} + +/** + * Zero the whole activation strip on the card, ONCE. + * + * pn_tt_matvec() writes only logical row 0 of each activation tile from then on, because + * rows 1-31 are the tile quantum's zero padding and re-sending 2016 zero bytes per tile + * per token is 32x the traffic for no information. That is only correct if rows 1-31 are + * ALREADY zero — DRAM out of the bump allocator is not, and the reader feeds all 32 rows + * to the FPU. So this is the precondition ft_tt_tile_row0_write() states and cannot check. + */ +static int pn_tt_stage_activation_zeros(pn_tt_device *device) +{ + size_t act_bytes = (size_t)device->max_tile_rows * PN_TT_TILE_BF16; + const ft_tt_noc_access *bulk = &device->runtime.bulk_dram; + unsigned char *zeros = calloc(1, act_bytes); + int rc; + + if (!zeros) + return -1; + rc = bulk->write(bulk->user, (uint16_t)device->dram[device->activation.logical_bank].x, + (uint16_t)device->dram[device->activation.logical_bank].y, + device->activation.address, zeros, act_bytes); + free(zeros); + if (rc != FT_TT_OK) + return -1; + device->bytes_written += act_bytes; + return 0; +} + +int pn_tt_device_load_artifact(pn_tt_device *device, const char *path) +{ + uint32_t index; + double started; + + if (!device || !path) + return -1; + if (ft_tt_fttw_open(path, &device->artifact) != FT_TT_OK) { + fprintf(stderr, "pn_tt: cannot open artifact %s\n", path); + return -1; + } + device->artifact_open = true; + + pn_tt_scan_extents(device); + if (pn_tt_alloc_scratch(device) != 0) { + fprintf(stderr, "pn_tt: scratch allocation failed\n"); + return -1; + } + if (pn_tt_stage_activation_zeros(device) != 0) { + fprintf(stderr, "pn_tt: could not stage the activation strip\n"); + return -1; + } + /* After the scratch rows, because the dump writes through row_out. */ + if (getenv(PN_TT_MATVEC_DUMP_ENV)) { + device->matvec_dump = fopen(getenv(PN_TT_MATVEC_DUMP_ENV), "wb"); + if (!device->matvec_dump) { + fprintf(stderr, "pn_tt: cannot open %s for the matvec dump\n", + getenv(PN_TT_MATVEC_DUMP_ENV)); + return -1; + } + } + + device->resident = calloc(device->artifact.header.tensor_count, sizeof(*device->resident)); + if (!device->resident) + return -1; + + started = pn_tt_now_seconds(); + for (index = 0; index < device->artifact.header.tensor_count; index++) { + const ft_tt_artifact_entry *entry = &device->artifact.directory[index]; + pn_tt_resident *slot = &device->resident[device->resident_count]; + uint32_t tile_bytes; + + /* Only matmul weights. A tensor in SOURCE layout is either a 1-D norm the engine + * already holds as fp32, or the embedding table, which no on-card op can read in + * this format (pn_tt_device.h). Uploading either would be dead weight in DRAM. */ + if (entry->layout != FT_TT_LAYOUT_MATMUL_NMAJOR) + continue; + tile_bytes = ft_tt_fttw_tile_bytes(entry->format); + if (tile_bytes == 0 || pn_tt_program_for(device, tile_bytes) == NULL) { + fprintf(stderr, "pn_tt: no matmul program for %s (tile %u bytes)\n", entry->name, + tile_bytes); + return -1; + } + snprintf(slot->name, sizeof(slot->name), "%s", entry->name); + slot->weight.tile_rows = entry->tile_rows; + slot->weight.tile_cols = entry->tile_cols; + slot->weight.tile_bytes = tile_bytes; + slot->weight.in_features = entry->tile_rows * PN_TT_TILE_DIM; + slot->weight.out_features = entry->tile_cols * PN_TT_TILE_DIM; + /* Where it goes is the GROUP's decision, because the number of banks a weight is + * spread over is the number of cores that will read it (pn_tt_matmul_group.h). At + * one member this is the same one-bank placement it always was. */ + if (pn_tt_matmul_group_place_weight(device, entry, &device->weight_bank_cursor, + &slot->weight) != 0) + return -1; + device->resident_count++; + device->resident_bytes += entry->byte_count; + } + fprintf(stderr, "pn_tt: %u matmul weights resident, %.1f MiB, %.1f s\n", + device->resident_count, (double)device->resident_bytes / (1024.0 * 1024.0), + pn_tt_now_seconds() - started); + pn_tt_matmul_group_report(device); + if (device->resident_count == 0) + return -1; + pn_tt_arm_residency(device); + /* IF YOU DID NOT ASK FOR ONE-SHOT, YOU DO NOT GET ONE-SHOT. Every reason the residency + * did not arm — a missing program, a mismatched artifact tile size, a launch that failed + * — costs 1.085x the seconds per token and produces a token indistinguishable from a + * healthy one. `pn_tt_arm_residency()` has already printed which reason it was. */ + if (device->resident_requested && !device->resident_running) { + fprintf(stderr, "pn_tt: REFUSED — the resident dispatch path did not come up (reason " + "above). Re-run with %s=oneshot if the slow path is what you want.\n", + PN_TT_DISPATCH_ENV); + return -1; + } + return 0; +} + +int pn_tt_device_find_weight(const pn_tt_device *device, const char *name, + pn_tt_weight *out_weight) +{ + uint32_t index; + + if (!device || !name || !out_weight) + return -1; + for (index = 0; index < device->resident_count; index++) { + if (strcmp(device->resident[index].name, name) == 0) { + *out_weight = device->resident[index].weight; + return 0; + } + } + return -1; +} + +int pn_tt_device_stage_weight_columns(pn_tt_device *device, const char *source_name, + const pn_tt_column_run *runs, uint32_t run_count, + pn_tt_weight *out_weight) +{ + const ft_tt_artifact_entry *entry = NULL; + uint32_t destination_columns; + uint32_t index; + + if (!device || !source_name || !runs || !out_weight || !device->artifact_open) + return -1; + destination_columns = pn_tt_column_runs_total(runs, run_count); + if (destination_columns == 0u) + return -1; + for (index = 0; index < device->artifact.header.tensor_count; index++) { + if (strcmp(device->artifact.directory[index].name, source_name) == 0) { + entry = &device->artifact.directory[index]; + break; + } + } + if (!entry || entry->layout != FT_TT_LAYOUT_MATMUL_NMAJOR) { + fprintf(stderr, "pn_tt: %s is not an N-major matmul weight in this artifact\n", + source_name); + return -1; + } + + memset(out_weight, 0, sizeof(*out_weight)); + out_weight->tile_rows = entry->tile_rows; + out_weight->tile_cols = destination_columns; + out_weight->tile_bytes = ft_tt_fttw_tile_bytes(entry->format); + out_weight->in_features = entry->tile_rows * PN_TT_TILE_DIM; + out_weight->out_features = destination_columns * PN_TT_TILE_DIM; + if (out_weight->tile_bytes == 0u || pn_tt_program_for(device, out_weight->tile_bytes) == + NULL) { + fprintf(stderr, "pn_tt: no matmul program for %s (tile %u bytes)\n", source_name, + out_weight->tile_bytes); + return -1; + } + /* A SECOND COPY IN DRAM, and it is deliberate — the source stays resident because the + * artifact's own placement is what every other tensor's bank rotation was computed + * against, and freeing one tensor out of the middle of that would be a hole the + * allocator cannot reuse anyway. For Ornith's `attn_q` at 71.3 MiB a layer this is + * 570 MiB over 8 attention layers against 32 GB of board memory. */ + return pn_tt_matmul_group_place_weight_columns(device, entry, runs, run_count, + &device->weight_bank_cursor, out_weight); +} + +/* ================================================================ + * STEP 4 S2: the second artifact (the MTP draft head) + * ================================================================ */ + +int pn_tt_device_extra_open(pn_tt_device *device, const char *path) +{ + if (!device || !path || device->extra_open) + return -1; + if (ft_tt_fttw_open(path, &device->extra_artifact) != FT_TT_OK) { + fprintf(stderr, "pn_tt: cannot open the extra artifact %s\n", path); + return -1; + } + device->extra_open = true; + return 0; +} + +/* The placement paths read `device->artifact`; for the extra one they run with the two + * swapped, and swapped back whatever happened. */ +static void pn_tt_extra_swap(pn_tt_device *device) +{ + ft_tt_fttw t = device->artifact; + + device->artifact = device->extra_artifact; + device->extra_artifact = t; +} + +int pn_tt_device_place_extra_weight(pn_tt_device *device, const char *name, + pn_tt_weight *out_weight) +{ + const ft_tt_artifact_entry *entry; + uint32_t tile_bytes; + int rc; + + if (!device || !name || !out_weight || !device->extra_open) + return -1; + entry = ft_tt_fttw_find(&device->extra_artifact, name); + if (!entry || entry->layout != FT_TT_LAYOUT_MATMUL_NMAJOR) { + fprintf(stderr, "pn_tt: the extra artifact has no N-major %s\n", name); + return -1; + } + tile_bytes = ft_tt_fttw_tile_bytes(entry->format); + if (tile_bytes == 0u || pn_tt_program_for(device, tile_bytes) == NULL) { + fprintf(stderr, "pn_tt: no matmul program for %s (tile %u bytes)\n", name, tile_bytes); + return -1; + } + memset(out_weight, 0, sizeof(*out_weight)); + out_weight->tile_rows = entry->tile_rows; + out_weight->tile_cols = entry->tile_cols; + out_weight->tile_bytes = tile_bytes; + out_weight->in_features = entry->tile_rows * PN_TT_TILE_DIM; + out_weight->out_features = entry->tile_cols * PN_TT_TILE_DIM; + pn_tt_extra_swap(device); + rc = pn_tt_matmul_group_place_weight(device, entry, &device->weight_bank_cursor, out_weight); + pn_tt_extra_swap(device); + return rc; +} + +int pn_tt_device_stage_extra_columns(pn_tt_device *device, const char *source_name, + const pn_tt_column_run *runs, uint32_t run_count, + pn_tt_weight *out_weight) +{ + int rc; + + if (!device || !device->extra_open) + return -1; + pn_tt_extra_swap(device); + rc = pn_tt_device_stage_weight_columns(device, source_name, runs, run_count, out_weight); + pn_tt_extra_swap(device); + return rc; +} + +const float *pn_tt_device_extra_vector(const pn_tt_device *device, const char *name, uint32_t n) +{ + const ft_tt_artifact_entry *entry; + + if (!device || !name || !device->extra_open) + return NULL; + entry = ft_tt_fttw_find(&device->extra_artifact, name); + if (!entry || entry->format != FT_TT_FORMAT_FP32 || entry->byte_count != (uint64_t)n * 4u || + entry->data_offset + entry->byte_count > (uint64_t)device->extra_artifact.bytes) { + fprintf(stderr, "pn_tt: the extra artifact has no %u-float fp32 %s\n", n, name); + return NULL; + } + return (const float *)(const void *)(device->extra_artifact.base + entry->data_offset); +} + +uint32_t pn_tt_device_weight_count(const pn_tt_device *device) +{ + return device ? device->resident_count : 0u; +} + +uint64_t pn_tt_device_weight_bytes(const pn_tt_device *device) +{ + return device ? device->resident_bytes : 0u; +} + +void pn_tt_device_dram_usage(const pn_tt_device *device, uint64_t *used_out, + uint64_t *total_out) +{ + uint64_t used = 0u, total = 0u; + + if (device) { + for (uint32_t bank = 0; bank < device->dram_bank_count; bank++) { + uint64_t bank_used = + ft_tt_allocator_used_bytes(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank); + + used += bank_used; + total += bank_used + + ft_tt_allocator_free_bytes(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank); + } + } + if (used_out) + *used_out = used; + if (total_out) + *total_out = total; +} + +void pn_tt_device_report_dram(const pn_tt_device *device, const char *tag) +{ + const double gib = 1024.0 * 1024.0 * 1024.0; + uint64_t used = 0u, total = 0u; + + pn_tt_device_dram_usage(device, &used, &total); + fprintf(stderr, "pn_tt: DRAM %s — %.3f GiB used of %.3f GiB over %u bank(s), %.3f GiB " + "free\n", tag ? tag : "usage", (double)used / gib, (double)total / gib, + device ? device->dram_bank_count : 0u, (double)(total - used) / gib); +} + +int pn_tt_device_rope_is_split_half(const pn_tt_device *device) +{ + if (!device || !device->artifact.base) + return 0; + return (device->artifact.header.flags & FT_TT_ARTIFACT_FLAG_ROPE_SPLIT_HALF) != 0u; +} + +int pn_tt_device_rope_pairs_reversed(const pn_tt_device *device) +{ + if (!device || !device->artifact.base) + return 0; + return (device->artifact.header.flags & FT_TT_ARTIFACT_FLAG_ROPE_PAIRS_REVERSED) != 0u; +} + +/* ================================================================ + * The one primitive + * ================================================================ */ + +/** + * Append logical row 0 of one projection's output to the dump file. + * + * Row 0 is the whole answer at Mt = 1: the other 31 rows of every output tile are the tile + * quantum multiplied against the activation's staged zeros, so they carry no information + * about the split and dumping them would make the file 32x larger without making it + * stricter. The bytes are read back off the card, not from any host mirror — a comparison + * against something the host computed would be measuring the host. + */ +static void pn_tt_dump_result_row(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *output) +{ + const ft_tt_noc_access *bulk = &device->runtime.bulk_dram; + size_t bytes = (size_t)weight->out_features * sizeof(*device->row_out); + + if (weight->out_features > device->max_row_features) + return; + if (ft_tt_tile_row0_read(bulk, (uint16_t)device->dram[output->bank].x, + (uint16_t)device->dram[output->bank].y, output->address, + weight->tile_cols, device->row_out) != FT_TT_OK) { + fprintf(stderr, "pn_tt: matvec dump could not read the result row\n"); + return; + } + if (fwrite(device->row_out, 1, bytes, device->matvec_dump) != bytes) { + fprintf(stderr, "pn_tt: matvec dump write failed\n"); + return; + } + device->matvec_dump_bytes += bytes; + device->matvec_dump_rows++; +} + +/** + * THE DISPATCH, with both operands already on the card. + * + * Both arms compute the same thing with the same three kernels on the same weights; what + * differs is whether the five kernel images cross the link again first. The resident arm is + * taken whenever the residency is live AND the reader would accept the shape — a descriptor + * the reader refuses stops it mid-residency, so the check is a precondition here, never a + * discovery there. + */ +/** + * THE MATMUL GROUP'S IDLE RETIRE, retire AND re-arm in one step, because this file owns the + * arm. Every member triad idles out on its own counters ~3 s apart (fastest ~8 s); a + * projection sent into that window wedges a member. So a group idle past the shared limit + * with nothing outstanding is ASKED to stop — whole, since every processor still alive is in + * its idle poll — and re-armed fresh before the projection is written. + */ +static void pn_tt_resident_idle_refresh(pn_tt_device *device) +{ + double idle = 0.0; + + if (!device->resident_running || !pn_tt_matmul_group_idle_expired(device, &idle)) + return; + fprintf(stderr, "pn_tt: matmul group idle %.1f s (limit %ld ms) — retiring all %u " + "member(s) by ASKING and re-arming before a projection can reach a " + "half-exited triad\n", + idle, pn_tt_residency_idle_retire_limit_ms(), device->group.member_count); + { + const double started = pn_tt_now_seconds(); + + pn_tt_matmul_group_retire(device); + device->resident_restarts++; + device->group.idle_retires++; + pn_tt_arm_residency(device); + pn_tt_residency_lifecycle_add(pn_tt_now_seconds() - started); + } +} + +int pn_tt_matvec_dispatch(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + ft_tt_op_matmul_request request; + const ft_tt_kernel_program *program; + bool dispatched; + double phase; + int rc; + + program = pn_tt_program_for(device, weight->tile_bytes); + if (!program) + return -1; + phase = pn_tt_now_seconds(); + + dispatched = false; + pn_tt_resident_idle_refresh(device); + if (device->resident_running) { + dispatched = pn_tt_matmul_group_dispatch(device, weight, input, output) == FT_TT_OK; + if (!dispatched) { + fprintf(stderr, "pn_tt: resident matmul Mt=1 Kt=%u Nt=%u refused — retiring the " + "residency by ASKING and re-arming\n", + weight->tile_rows, weight->tile_cols); + /* A REFUSED DESCRIPTOR MUST NOT COST THE CORE. The reader leaves its loop and + * `tail` never advances, so the other four processors are waiting on work that + * will never arrive — a state only the ASK can leave, never a reset. Retire all + * five, then arm a FRESH residency: ft_tt_queue_init() re-poisons the control + * block and issues a new nonce, so the refused descriptor is not still in the + * ring for the new kernel to re-attempt and refuse again. Every member is + * retired, not only the one that refused — a partly-armed group would leave one + * slice of the next projection unwritten. */ + pn_tt_matmul_group_retire(device); + device->resident_restarts++; + pn_tt_arm_residency(device); + /* Retry ONCE on the re-armed residency. Falling through to one-shot rather than + * failing is the difference between a recoverable refusal and a lost token. */ + if (device->resident_running) + dispatched = pn_tt_matmul_group_dispatch(device, weight, input, output) == + FT_TT_OK; + if (!dispatched) { + fprintf(stderr, "pn_tt: re-armed residency refused it too\n"); + pn_tt_matmul_group_retire(device); + } + } + } + /* THE ONE-SHOT ESCAPE DOES NOT EXIST FOR A SHARDED WEIGHT, and that is arithmetic + * rather than a policy: its panels are in several banks, so there is no single grid a + * one-shot launch could read. A group wide enough to shard has traded the fallback for + * the bandwidth, and it says so here instead of computing from one bank and returning a + * plausible wrong answer. */ + if (!dispatched && !pn_tt_matmul_group_can_fall_back(device, weight)) { + fprintf(stderr, "pn_tt: a weight Kt=%u Nt=%u sharded over %u banks has no one-shot " + "grid to fall back to\n", + weight->tile_rows, weight->tile_cols, weight->shard_count); + return -1; + } + if (!dispatched) { + memset(&request, 0, sizeof(request)); + request.tile_rows = 1u; + request.tile_inner = weight->tile_rows; + request.tile_cols = weight->tile_cols; + request.prefetch_tiles = pn_tt_env_u32(PN_TT_PREFETCH_TILES_ENV, + device->prefetch_tiles_preferred + ? device->prefetch_tiles_preferred + : PN_TT_PREFETCH_TILES_DEFAULT); + /* A STATEMENT about DRAM, not a request: the artifact's MATMUL_NMAJOR layout IS the + * N-major tile grid, and a wrong value here is a wrong answer with no error. */ + request.weight_n_major = 1u; + request.activation.x = (uint16_t)device->dram[input->bank].x; + request.activation.y = (uint16_t)device->dram[input->bank].y; + request.activation.address = input->address; + request.activation.tile_bytes = PN_TT_TILE_BF16; + request.weight.x = (uint16_t)device->dram[weight->bank].x; + request.weight.y = (uint16_t)device->dram[weight->bank].y; + request.weight.address = weight->address; + request.weight.tile_bytes = weight->tile_bytes; + request.output.x = (uint16_t)device->dram[output->bank].x; + request.output.y = (uint16_t)device->dram[output->bank].y; + request.output.address = output->address; + request.output.tile_bytes = PN_TT_TILE_BF16; + + rc = ft_tt_op_matmul_run(&device->runtime.noc, device->core_x, device->core_y, program, + &request, PN_TT_OP_TIMEOUT_MS); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: matmul Mt=1 Kt=%u Nt=%u failed: %s\n", weight->tile_rows, + weight->tile_cols, ft_tt_error_string(rc)); + return -1; + } + } + device->dispatch_seconds += pn_tt_now_seconds() - phase; + device->matvec_count++; + /* OUTSIDE the timed phase: this is an instrument, and a phase split that included it + * could not be compared with a run that did not have it armed. */ + if (device->matvec_dump) + pn_tt_dump_result_row(device, weight, output); + return 0; +} + +/** The strips a projection is asked to write into have to hold it. Shared by the waiting + * and the issuing forms, because a wrong shape is a card refusal either way. */ +static bool pn_tt_matvec_strips_fit(const pn_tt_weight *weight, const pn_tt_activation *input, + const pn_tt_activation *output) +{ + if (input->tile_count < weight->tile_rows || output->tile_count < weight->tile_cols) { + fprintf(stderr, "pn_tt: matvec strip too small (Kt=%u into %u tiles, Nt=%u into %u)\n", + weight->tile_rows, input->tile_count, weight->tile_cols, output->tile_count); + return false; + } + /* THE OTHER AXIS, AND IT HAS NO CARD-SIDE CHECK AT ALL. At `Mt > 1` the writer places + * output tile `(m, n)` at `C_BASE + (m*a2 + n) * 2048` — DRAM, no bounds — so an output + * strip spanning fewer BANDS than the input is a write into whatever the allocator put + * after it, with every kernel retiring clean. Refused here, where both strips are in + * scope, rather than left to the descriptor writer's clamp: the clamp keeps the answer + * safe and SILENTLY narrows the pass, which is a lever that measures itself away. */ + if (input->active_rows > 1u && output->active_rows < input->active_rows) { + fprintf(stderr, "pn_tt: matvec spans %u tile-row bands of A into an output that spans " + "%u — the writer would place a row past the end of the destination " + "strip\n", input->active_rows, output->active_rows); + return false; + } + return true; +} + +int pn_tt_matvec_resident(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + double started; + int rc; + + if (!device || !weight || !input || !output) + return -1; + if (!pn_tt_matvec_strips_fit(weight, input, output)) + return -1; + /* A WAITING projection is one of the operations that flushes first — see + * pn_tt_matvec_enqueue()'s contract. Chained work may write the very strip this one + * reads, and the members run concurrently. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + started = pn_tt_now_seconds(); + rc = pn_tt_matvec_dispatch(device, weight, input, output); + device->matvec_seconds += pn_tt_now_seconds() - started; + return rc; +} + +uint32_t pn_tt_matvec_chain_limit(void) +{ + const char *value = getenv("PN_TT_MATVEC_CHAIN"); + long parsed = (value && *value) ? strtol(value, NULL, 10) : 0; + + if (parsed <= 0) + return PN_TT_MATVEC_MAX_CHAIN; + if (parsed > (long)PN_TT_MATVEC_CHAIN_CEILING) + return PN_TT_MATVEC_CHAIN_CEILING; + return (uint32_t)parsed; +} + +int pn_tt_matvec_enqueue(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + double started; + int rc; + + if (!device || !weight || !input || !output) + return -1; + if (!pn_tt_matvec_strips_fit(weight, input, output)) + return -1; + /* Without a residency there is nothing to chain onto: the one-shot escape launches five + * kernel images and waits, so the waiting form IS the only form it has. */ + if (device->pending_count == 0u) + pn_tt_resident_idle_refresh(device); + if (!device->resident_running) + return pn_tt_matvec_resident(device, weight, input, output); + if (device->pending_count >= pn_tt_matvec_chain_limit() && + pn_tt_matvec_flush(device) != 0) + return -1; + + started = pn_tt_now_seconds(); + rc = pn_tt_matmul_group_enqueue(device, weight, input, output); + /* ONE clock read for both accumulators. Two would charge the two phases slightly + * different amounts of the same work, and these are the numbers the sweep is read from. */ + started = pn_tt_now_seconds() - started; + device->matvec_seconds += started; + device->dispatch_seconds += started; + /* THE HOST'S OWN HALF OF THE DISPATCH PHASE, kept apart from the wait. What runs above + * is parameter words and a ring entry per member over MMIO and nothing else — no poll, + * no completion read, no card work. So this counter IS "the host in the dispatch loop", + * to the microsecond, and `dispatch - issue - flush_wait` cannot hide anywhere. */ + device->dispatch_issue_seconds += started; + device->dispatch_issues++; + if (rc != FT_TT_OK) { + /* Refused BEFORE it was issued, so nothing is outstanding for it and the waiting + * form — which owns the retire, the re-arm and the one-shot escape — can simply be + * asked for the same projection. */ + return pn_tt_matvec_resident(device, weight, input, output); + } + device->pending[device->pending_count].weight = weight; + device->pending[device->pending_count].input = *input; + device->pending[device->pending_count].output = *output; + device->pending_count++; + device->matvec_count++; + return 0; +} + +int pn_tt_matvec_flush(pn_tt_device *device) +{ + double started; + uint32_t pending; + uint32_t index; + int rc; + + if (!device || device->pending_count == 0u) + return 0; + pending = device->pending_count; + started = pn_tt_now_seconds(); + rc = pn_tt_matmul_group_flush(device); + started = pn_tt_now_seconds() - started; + device->matvec_seconds += started; + device->dispatch_seconds += started; + /* The OTHER half: `pending` descriptors' worth of card work plus ONE completion poll. + * The poll is the only host round trip in it, so this counter over `dispatch_waits` is + * an upper bound on what Phase 3 can take out of a wait and the rest is the card. */ + device->dispatch_wait_seconds += started; + device->dispatch_waits++; + device->dispatch_wait_descriptors += pending; + /* CLEARED BEFORE THE REPLAY, because the replay goes through the waiting form and that + * form flushes on entry — a count still standing here would recurse. */ + device->pending_count = 0u; + + /* A HOST-SIDE REFUSAL IS NOT A CARD FAULT, and must not be treated as one. Retiring and + * re-arming a residency because THIS side handed the accessor a bad length costs a + * recovery cycle per projection and gets nothing back — measured 2026-08-16, when a + * group-block read whose length missed the 16-byte L1 unit re-armed the group three + * times in a row and left a core unbootable. The recovery below is for a card that did + * not answer; FT_TT_EINVAL means it was never asked. */ + if (rc == FT_TT_EINVAL) { + fprintf(stderr, "pn_tt: the completion read was REFUSED before it reached the card " + "(%s) — this is a host-side defect and the residency is left alone\n", + ft_tt_error_string(rc)); + return -1; + } + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: a chained projection did not complete (%s) — retiring the " + "residency by ASKING, re-arming, and replaying %u projection(s)\n", + ft_tt_error_string(rc), pending); + pn_tt_matmul_group_retire(device); + device->resident_restarts++; + pn_tt_arm_residency(device); + device->matvec_count -= pending; + for (index = 0; index < pending; index++) { + if (pn_tt_matvec_resident(device, device->pending[index].weight, + &device->pending[index].input, + &device->pending[index].output) != 0) + return -1; + } + return 0; + } + + /* OUTSIDE the timed phase, and only now: the dump reads each projection's OUTPUT strip + * off the card, which is not written until the member that owns those columns is done. */ + if (device->matvec_dump) { + for (index = 0; index < pending; index++) + pn_tt_dump_result_row(device, device->pending[index].weight, + &device->pending[index].output); + } + return 0; +} + +int pn_tt_matvec(pn_tt_device *device, const pn_tt_weight *weight, const float *x, float *y) +{ + pn_tt_activation input, output; + const ft_tt_noc_access *bulk; + size_t in_bytes, out_bytes; + double started, phase; + int rc; + + if (!device || !weight || !x || !y) + return -1; + if (weight->in_features > device->max_in_features || + weight->out_features > device->max_out_features) + return -1; + + /* TWO accessors, and the split is not cosmetic. `runtime.noc` is UC/WORD32 and carries + * run control ONLY — the go message and the launch args, where 4-byte width is a + * correctness requirement. `bulk` is WC/BLOCK16 over ft_tt_{read,write}_dram and + * carries the activation and the result. Routing data through the register accessor + * was 71.1% of a 28 s token. */ + bulk = &device->runtime.bulk_dram; + input.bank = device->activation.logical_bank; + input.address = (uint32_t)device->activation.address; + input.tile_count = device->max_tile_rows; + output.bank = device->result.logical_bank; + output.address = (uint32_t)device->result.address; + output.tile_count = device->max_tile_cols; + in_bytes = (size_t)weight->tile_rows * FT_TT_TILE_ROW0_CHUNKS * FT_TT_TILE_ROW0_HALF_BYTES; + out_bytes = (size_t)weight->tile_cols * FT_TT_TILE_ROW0_CHUNKS * FT_TT_TILE_ROW0_READ_BYTES; + started = pn_tt_now_seconds(); + + /* One token is ONE row of a 32-row tile, and the matmul computes the other 31 anyway. + * That waste is free ON the card — it is the tile quantum (ft_tt_op_matmul.h) — but it + * is NOT free across the link, so only row 0 crosses it in either direction. Rows 1-31 + * of the activation were staged zero once, at load (pn_tt_stage_activation_zeros). */ + phase = started; + ft_bf16_from_float_array(x, device->row_in, weight->in_features); + rc = ft_tt_tile_row0_write(bulk, (uint16_t)device->dram[input.bank].x, + (uint16_t)device->dram[input.bank].y, input.address, + weight->tile_rows, device->row_in); + if (rc != FT_TT_OK) + return -1; + device->bytes_written += in_bytes; + device->upload_seconds += pn_tt_now_seconds() - phase; + + if (pn_tt_matvec_dispatch(device, weight, &input, &output) != 0) + return -1; + phase = pn_tt_now_seconds(); + + /* 2 x 64 B per output tile instead of 2048. Not 2 x 32: a 32-byte DRAM read is + * REFUSED — Blackhole's card->host alignment is 64 B — so half of each chunk is read + * and discarded, which is why the recoverable factor is 16.00x and not 32x. */ + rc = ft_tt_tile_row0_read(bulk, (uint16_t)device->dram[output.bank].x, + (uint16_t)device->dram[output.bank].y, output.address, + weight->tile_cols, device->row_out); + if (rc != FT_TT_OK) + return -1; + device->bytes_read += out_bytes; + + ft_float_from_bf16_array(device->row_out, y, weight->out_features); + device->readback_seconds += pn_tt_now_seconds() - phase; + device->matvec_seconds += pn_tt_now_seconds() - started; + return 0; +} + +uint64_t pn_tt_device_matvec_count(const pn_tt_device *device) +{ + return device ? device->matvec_count : 0u; +} + +double pn_tt_device_matvec_seconds(const pn_tt_device *device) +{ + return device ? device->matvec_seconds : 0.0; +} + +void pn_tt_device_phase_seconds(const pn_tt_device *device, double *out_upload, + double *out_dispatch, double *out_readback) +{ + if (out_upload) + *out_upload = device ? device->upload_seconds : 0.0; + if (out_dispatch) + *out_dispatch = device ? device->dispatch_seconds : 0.0; + if (out_readback) + *out_readback = device ? device->readback_seconds : 0.0; +} + +void pn_tt_device_oncard_stats(const pn_tt_device *device, double *out_seconds, + uint64_t *out_ops, uint64_t *out_restarts) +{ + if (out_seconds) + *out_seconds = device ? device->oncard_seconds : 0.0; + if (out_ops) + *out_ops = device ? device->oncard_ops : 0u; + if (out_restarts) + *out_restarts = device ? device->oncard_restarts : 0u; +} + +void pn_tt_oncard_account(pn_tt_device *device, const char *family, const char *function, + uint32_t tiles, double seconds) +{ + if (!device) + return; + device->oncard_seconds += seconds; + device->oncard_ops++; + /* A refusal here is NOT propagated to the caller: a full census table or an unexpected + * width is a defect in the INSTRUMENT, and failing a token because the instrument ran out + * of rows would be worse than the missing row. It is counted instead, and + * `pn_tt_device_census()`'s reader prints `dropped` and `refused` beside every figure. */ + (void)pn_tt_descriptor_census_record(&device->census, family, function, tiles, seconds); +} + +const pn_tt_descriptor_census *pn_tt_device_census(const pn_tt_device *device) +{ + return device ? &device->census : NULL; +} + +/** Every residency this device holds, in ONE place — because two spellings of "the set of + * residencies" is how the strip bridge stayed out of the table for a whole round while its + * cost was being estimated by subtraction. The eltwise SPLIT MEMBERS are here and are not in + * the per-family report: that report keys on a name, and a member shares member 0's. */ +#define PN_TT_RESIDENCY_TABLE_MAX \ + (9u + (PN_TT_TOPK_MAX_MEMBERS - 1u) + PN_TT_RENDER_DIRECTIONS * PN_TT_RENDER_SPLIT_MAX_MEMBERS + \ + PN_TT_HEAD_NORM_MAX_WIDTHS * PN_TT_HEAD_NORM_MAX_MEMBERS + \ + PN_TT_ELTWISE_SPLIT_MAX_MEMBERS + PN_TT_ATTN_MAX_MEMBERS) + +static unsigned residency_table(const pn_tt_device *device, + const pn_tt_residency *table[PN_TT_RESIDENCY_TABLE_MAX], + bool with_split_members) +{ + unsigned count = 0; + uint32_t slot; + + table[count++] = &device->reduce; + if (device->reduce_member.running || device->reduce_member.core_booted) + table[count++] = &device->reduce_member; + table[count++] = &device->eltwise; + table[count++] = &device->sample; + { + uint32_t m; + + for (m = 1u; m < device->sample_members; m++) + table[count++] = &device->sample_member[m - 1u]; + } + table[count++] = &device->rope; + table[count++] = &device->memory; + table[count++] = &device->attention; + table[count++] = &device->gdn; + /* THE STRIP BRIDGE WAS NOT IN THIS TABLE, and it is the second largest non-matmul term + * in the token. Its ~0.022 s/token had only ever been a SUBTRACTION between two runs, + * for the same reason the GDN phases were before they were counted directly. */ + for (slot = 0; slot < (uint32_t)PN_TT_RENDER_DIRECTIONS; slot++) + table[count++] = &device->render[slot].residency; + /* And the residency that DELETES the narrow half — counted from the first line it exists, + * so its cost is never a subtraction between two runs the way the bridge's was. */ + table[count++] = &device->gate.residency; + /* Only the LIVE head-norm slots, so a run with one head width reports one line and a + * hybrid with two reports two — the count is part of the finding. */ + for (slot = 0; slot < device->head_norm_count; slot++) + table[count++] = &device->head_norm[slot].residency; + if (with_split_members) { + uint32_t direction; + + for (slot = 1u; slot < device->eltwise_members; slot++) + table[count++] = &device->eltwise_member[slot - 1u]; + for (slot = 1u; slot < device->attention_members; slot++) + table[count++] = &device->attention_member[slot - 1u]; + for (slot = 0; slot < device->head_norm_count; slot++) + for (uint32_t m = 1u; m < device->head_norm[slot].members; m++) + table[count++] = &device->head_norm[slot].member[m - 1u]; + /* AND THE STRIP BRIDGE'S. An await this process paid that no counter walks is an + * arm reporting a saving the knob never armed — the failure `ft_tt_noc_path` was + * taught to count ACCESSES to make visible. */ + for (direction = 0; direction < (uint32_t)PN_TT_RENDER_DIRECTIONS; direction++) + for (slot = 1u; slot < device->render[direction].members; slot++) + table[count++] = &device->render[direction].member[slot - 1u]; + } + return count; +} + +bool pn_tt_device_residency_stat(const pn_tt_device *device, unsigned index, + const char **out_name, uint64_t *out_ops, double *out_seconds, + uint32_t *out_core) +{ + const pn_tt_residency *table[PN_TT_RESIDENCY_TABLE_MAX]; + unsigned count; + + if (!device) + return false; + count = residency_table(device, table, false); + if (index >= count) + return false; + if (out_name) + *out_name = table[index]->name; + if (out_ops) + *out_ops = table[index]->ops; + if (out_seconds) + *out_seconds = table[index]->seconds; + if (out_core) + *out_core = table[index]->core_index; + return true; +} + +unsigned pn_tt_device_card_clocks(const pn_tt_device *device, const char **out_names, + double *out_seconds, unsigned max) +{ + static const char *const gdnc_names[] = { "gdnc-phase0", "gdnc-conv", "gdnc-delta" }; + const pn_tt_residency *table[PN_TT_RESIDENCY_TABLE_MAX]; + unsigned count, index, written = 0; + uint32_t phase; + + if (!device || !out_names || !out_seconds) + return 0; + count = residency_table(device, table, true); + for (index = 0; index < count && written < max; index++, written++) { + out_names[written] = table[index]->name; + out_seconds[written] = table[index]->seconds; + } + for (phase = 0; phase < 3u && written < max; phase++, written++) { + out_names[written] = gdnc_names[phase]; + out_seconds[written] = device->gdnc_phase_seconds[phase]; + } + if (written < max) { + out_names[written] = "gdnc-recovered"; + out_seconds[written++] = device->gdnc_phase_recovered_seconds[0] + + device->gdnc_phase_recovered_seconds[1] + + device->gdnc_phase_recovered_seconds[2]; + } + if (written < max) { + out_names[written] = "gdnc-stage"; + out_seconds[written++] = device->gdnc_stage_build_seconds + + device->gdnc_stage_upload_seconds; + } + { + static const char *const host_names[PN_TT_HOST_CLOCK_COUNT] = { + "host-gdn-math", "host-gdn-consts", "host-rope-table" }; + unsigned clock; + + for (clock = 0; clock < (unsigned)PN_TT_HOST_CLOCK_COUNT && written < max; clock++) { + out_names[written] = host_names[clock]; + out_seconds[written++] = device->host_clock_seconds[clock]; + } + } + return written; +} + +void pn_tt_device_host_clock_add(pn_tt_device *device, pn_tt_host_clock clock, double seconds) +{ + if (device && (unsigned)clock < (unsigned)PN_TT_HOST_CLOCK_COUNT) + device->host_clock_seconds[clock] += seconds; +} + +void pn_tt_device_await_transactions(const pn_tt_device *device, uint64_t *out_awaits, + uint64_t *out_poll, uint64_t *out_settle, + unsigned *out_fast) +{ + const pn_tt_residency *table[PN_TT_RESIDENCY_TABLE_MAX]; + uint64_t awaits = 0, poll = 0, settle = 0; + unsigned fast = 0, count, index; + + if (out_awaits) + *out_awaits = 0; + if (out_poll) + *out_poll = 0; + if (out_settle) + *out_settle = 0; + if (out_fast) + *out_fast = 0; + if (!device) + return; + count = residency_table(device, table, true); + for (index = 0; index < count; index++) { + awaits += table[index]->awaits; + poll += table[index]->await_poll_transactions; + settle += table[index]->await_settle_transactions; + if (table[index]->awaits != 0u && table[index]->await_shape == PN_TT_AWAIT_LINE) + fast++; + } + if (out_awaits) + *out_awaits = awaits; + if (out_poll) + *out_poll = poll; + if (out_settle) + *out_settle = settle; + if (out_fast) + *out_fast = fast; +} + +void pn_tt_device_issue_transactions(const pn_tt_device *device, uint64_t *out_issues, + uint64_t *out_accesses, double *out_param_seconds, + double *out_ring_seconds, double *out_head_seconds, + unsigned *out_wide) +{ + const pn_tt_residency *table[PN_TT_RESIDENCY_TABLE_MAX]; + uint64_t issues = 0, accesses = 0; + double param = 0.0, ring = 0.0, head = 0.0; + unsigned wide = 0, count, index; + + if (out_issues) + *out_issues = 0; + if (out_accesses) + *out_accesses = 0; + if (out_param_seconds) + *out_param_seconds = 0.0; + if (out_ring_seconds) + *out_ring_seconds = 0.0; + if (out_head_seconds) + *out_head_seconds = 0.0; + if (out_wide) + *out_wide = 0; + if (!device) + return; + count = residency_table(device, table, true); + for (index = 0; index < count; index++) { + issues += table[index]->issues; + accesses += table[index]->issue_accesses; + param += table[index]->issue_param_seconds; + ring += table[index]->issue_ring_seconds; + head += table[index]->issue_head_seconds; + if (table[index]->issues != 0u && table[index]->issue_shape == PN_TT_ISSUE_WIDE) + wide++; + } + if (out_issues) + *out_issues = issues; + if (out_accesses) + *out_accesses = accesses; + if (out_param_seconds) + *out_param_seconds = param; + if (out_ring_seconds) + *out_ring_seconds = ring; + if (out_head_seconds) + *out_head_seconds = head; + if (out_wide) + *out_wide = wide; +} + +void pn_tt_device_dispatch_split(const pn_tt_device *device, double *out_issue_seconds, + uint64_t *out_issues, double *out_wait_seconds, + uint64_t *out_waits, uint64_t *out_wait_descriptors) +{ + if (out_issue_seconds) + *out_issue_seconds = device ? device->dispatch_issue_seconds : 0.0; + if (out_issues) + *out_issues = device ? device->dispatch_issues : 0u; + if (out_wait_seconds) + *out_wait_seconds = device ? device->dispatch_wait_seconds : 0.0; + if (out_waits) + *out_waits = device ? device->dispatch_waits : 0u; + if (out_wait_descriptors) + *out_wait_descriptors = device ? device->dispatch_wait_descriptors : 0u; +} + +void pn_tt_device_gdn_phase_stats(const pn_tt_device *device, unsigned phase, + double *out_seconds, uint64_t *out_ops) +{ + const bool ok = device && phase < 3u; + + if (out_seconds) + *out_seconds = ok ? device->gdn_phase_seconds[phase] : 0.0; + if (out_ops) + *out_ops = ok ? device->gdn_phase_ops[phase] : 0u; +} + +void pn_tt_device_gdn_phase_recovery(const pn_tt_device *device, unsigned phase, + double *out_seconds, uint64_t *out_ops, + uint64_t *out_rearms) +{ + const bool ok = device && phase < 3u; + + if (out_seconds) + *out_seconds = ok ? device->gdn_phase_recovered_seconds[phase] : 0.0; + if (out_ops) + *out_ops = ok ? device->gdn_phase_recovered_ops[phase] : 0u; + if (out_rearms) + *out_rearms = ok ? device->gdn_phase_rearms[phase] : 0u; +} + +uint64_t pn_tt_device_bytes_read(const pn_tt_device *device) +{ + return device ? device->bytes_read : 0u; +} + +uint64_t pn_tt_device_bytes_written(const pn_tt_device *device) +{ + return device ? device->bytes_written : 0u; +} + +void pn_tt_device_dispatch_counts(const pn_tt_device *device, uint64_t *out_resident, + uint64_t *out_oneshot, uint64_t *out_restarts) +{ + if (out_resident) + *out_resident = device ? device->resident_ops : 0u; + if (out_oneshot) + *out_oneshot = device ? device->matvec_count - device->resident_ops : 0u; + if (out_restarts) + *out_restarts = device ? device->resident_restarts : 0u; +} + +void pn_tt_device_window_traffic(const pn_tt_device *device, uint64_t *out_register_read, + uint64_t *out_register_written, uint64_t *out_bulk_read, + uint64_t *out_bulk_written) +{ + ft_tt_runtime_window_stats stats; + + /* Through the seam, NOT through the tt-kmd binding. Reading the binding directly is what + * made this report 0 B on every window of a udrv run that had just served a token. */ + if (!device || ft_tt_runtime_window_stats_read(&device->runtime, &stats) != FT_TT_OK) + memset(&stats, 0, sizeof(stats)); + + if (out_register_read) + *out_register_read = stats.register_read; + if (out_register_written) + *out_register_written = stats.register_written; + if (out_bulk_read) + *out_bulk_read = stats.bulk_read; + if (out_bulk_written) + *out_bulk_written = stats.bulk_written; +} + +void pn_tt_device_window_aims(const pn_tt_device *device, uint64_t *out_register_aims, + uint64_t *out_bulk_aims, uint64_t *out_failures) +{ + ft_tt_runtime_window_stats stats; + + if (!device || ft_tt_runtime_window_stats_read(&device->runtime, &stats) != FT_TT_OK) + memset(&stats, 0, sizeof(stats)); + + if (out_register_aims) + *out_register_aims = stats.register_aims; + if (out_bulk_aims) + *out_bulk_aims = stats.bulk_aims; + if (out_failures) + *out_failures = stats.aim_failures; +} + +void pn_tt_device_window_aim_seconds(const pn_tt_device *device, double *out_seconds, + double *out_verify_seconds) +{ + ft_tt_runtime_window_stats stats; + + if (!device || ft_tt_runtime_window_stats_read(&device->runtime, &stats) != FT_TT_OK) + memset(&stats, 0, sizeof(stats)); + + if (out_seconds) + *out_seconds = stats.aim_seconds; + if (out_verify_seconds) + *out_verify_seconds = stats.aim_verify_seconds; +} + +void pn_tt_device_window_bank(const pn_tt_device *device, uint32_t *out_register_windows, + uint32_t *out_bulk_windows, uint64_t *out_register_hits, + uint64_t *out_register_misses, uint64_t *out_bulk_hits, + uint64_t *out_bulk_misses, uint64_t *out_misaims, + uint64_t *out_audit_failures) +{ + ft_tt_runtime_window_stats stats; + + if (!device || ft_tt_runtime_window_stats_read(&device->runtime, &stats) != FT_TT_OK) + memset(&stats, 0, sizeof(stats)); + + if (out_register_windows) + *out_register_windows = stats.register_windows; + if (out_bulk_windows) + *out_bulk_windows = stats.bulk_windows; + if (out_register_hits) + *out_register_hits = stats.register_hits; + if (out_register_misses) + *out_register_misses = stats.register_misses; + if (out_bulk_hits) + *out_bulk_hits = stats.bulk_hits; + if (out_bulk_misses) + *out_bulk_misses = stats.bulk_misses; + if (out_misaims) + *out_misaims = stats.misaims_injected; + if (out_audit_failures) + *out_audit_failures = stats.audit_failures; +} + +/* Telemetry tag numbers and names follow tt-umd's TelemetryTag enumeration, + * (c) 2025 Tenstorrent Inc., Apache License 2.0. See NOTICE. */ +/** + * MEASURE (`PN_TT_ARC_TELEMETRY=1`): one line of ARC telemetry — clocks, power, temperature, + * limits and arbiter state (tt-umd TelemetryTag numbering). Reads only; no ARC message, no + * clock or throttler is touched. udrv transport only; silent otherwise. + */ +void pn_tt_device_telemetry_log(pn_tt_device *device, const char *label) +{ + static const uint16_t tags[] = { 14, 15, 16, 6, 7, 8, 11, 54, 63, 64, 65, 66, 67, 68, 38, + 56, 32 }; + static const char *const names[] = { "AICLK", "AXICLK", "ARCCLK", "VCORE", "TDP", "TDC", + "ASIC_TEMP", "INPUT_POWER", "AICLK_LIMIT_MAX", + "TDP_LIMIT_MAX", "AICLK_ARB_MIN", "AICLK_ARB_MAX", + "ENABLED_MIN_ARB", "ENABLED_MAX_ARB", "PCIE_USAGE", + "THM_LIMIT_THROTTLE", "HEARTBEAT" }; + enum { kCount = sizeof(tags) / sizeof(tags[0]) }; + uint32_t values[kCount]; + bool found[kCount]; + unsigned i; + int rc; + + if (!device || pn_tt_env_u32("PN_TT_ARC_TELEMETRY", 0u) == 0u || !device->runtime.udrv) + return; + rc = ft_tt_runtime_udrv_read_telemetry(device->runtime.udrv, tags, kCount, values, found); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: ARC TELEMETRY [%s] read failed rc=%d\n", label, rc); + return; + } + fprintf(stderr, "pn_tt: ARC TELEMETRY [%s]", label); + for (i = 0; i < kCount; i++) { + if (!found[i]) + fprintf(stderr, " %s=-", names[i]); + else if (tags[i] == 11) + fprintf(stderr, " %s=%.2fC", names[i], (double)values[i] / 65536.0); + else + fprintf(stderr, " %s=%u", names[i], values[i]); + } + fprintf(stderr, "\n"); +} + +/** + * STEP 3 GATE (`PN_TT_IDLE_CLOCK=1`, measure only): every resident writer adds the cycles it + * spends waiting for a NEW descriptor to its control word 60. Two snapshots over the decode + * window give each core's card-BUSY time (interval - idle). Families run one after another + * on the host's schedule, so wall - (matmul busiest member + sum over the other families' + * main residencies) is the card-idle time the host's issue/await chain puts in a token — + * the time an on-card dispatcher can remove. `stage` 0 = start snapshot, 1 = report. + */ +void pn_tt_device_idle_clock(pn_tt_device *device, int stage, double tokens) +{ + enum { kMax = PN_TT_RESIDENCY_TABLE_MAX + PN_TT_MATMUL_MAX_CORES }; + static uint32_t start_idle[kMax]; + static double start_wall; + const pn_tt_residency *table[PN_TT_RESIDENCY_TABLE_MAX]; + unsigned count, index; + double wall, matmul_max = 0.0, others = 0.0, last_main = 0.0; + + if (!device || pn_tt_env_u32("PN_TT_IDLE_CLOCK", 0u) == 0u) + return; + if (pn_tt_matvec_flush(device) != 0) + return; + wall = pn_tt_now_seconds(); + count = residency_table(device, table, false); + if (stage == 0) + start_wall = wall; + else + fprintf(stderr, "pn_tt: IDLE CLOCK over %.0f tokens, %.4f s/token wall:", tokens, + (wall - start_wall) / tokens); + for (index = 0; index < count + device->group.member_count && index < kMax; index++) { + const ft_tt_queue *queue; + const char *name; + uint32_t idle = 0u; + + if (index < count) { + if (!table[index]->running) + continue; + queue = &table[index]->queue; + name = table[index]->name; + } else { + queue = device->group.member[index - count].queue; + name = "matmul"; + if (!queue) + continue; + } + if (device->runtime.noc.read(device->runtime.noc.user, queue->x, queue->y, + queue->control_address + 60u * 4u, &idle, + sizeof(idle)) != FT_TT_OK) + continue; + if (stage == 0) { + start_idle[index] = idle; + } else { + const double idle_s = (double)(uint32_t)(idle - start_idle[index]) * 16.0 / 1.35e9; + const double busy = ((wall - start_wall) - idle_s) / tokens; + + if (idle == 0u) { + fprintf(stderr, " [%s: no clock]", name); + continue; + } + if (index < count) { + /* A SPLIT MEMBER runs CONCURRENTLY with its main (the table lists it right + * after): the family costs the slower of the two, not their sum. */ + if (strstr(name, "-member") != NULL) { + if (busy > last_main) { + others += busy - last_main; + last_main = busy; + } + } else { + others += busy; + last_main = busy; + } + fprintf(stderr, " %s %.2fms", name, busy * 1e3); + } else if (busy > matmul_max) { + matmul_max = busy; + } + } + } + if (stage != 0) { + const double per = (wall - start_wall) / tokens; + + fprintf(stderr, " | matmul busiest member %.2fms\n", matmul_max * 1e3); + fprintf(stderr, "pn_tt: IDLE CLOCK — card-busy sum %.2f ms (matmul %.2f + others %.2f) " + "of %.2f ms wall: HOST-ATTRIBUTABLE card idle %.2f ms/token\n", + (matmul_max + others) * 1e3, matmul_max * 1e3, others * 1e3, per * 1e3, + (per - matmul_max - others) * 1e3); + } +} + +/* ------------------------------------------------------------------------------------ + * M5 WAIT CLOCK REPORT. Every resident processor adds the wall-clock cycles (>> 4) it spent + * spinning in a CB wait to its status word 6 and counts the waits in word 7 + * (resident_queue_protocol.h ftq_wait_account). The processor that waits LEAST on a busy + * core is the one the other four wait for. Since the last launch of that core. + * ---------------------------------------------------------------------------------- */ +static void pn_tt_wait_line(pn_tt_device *device, const ft_tt_queue *queue, const char *label, + double tokens) +{ + static const char *names[5] = {"reader", "writer", "unpack", "math", "pack"}; + uint32_t words[5 * 8]; + uint32_t p; + + if (!queue || queue->control_address == 0u || + device->runtime.noc.read(device->runtime.noc.user, queue->x, queue->y, + queue->control_address + FT_TT_QUEUE_STATUS_OFFSET, words, + sizeof(words)) != FT_TT_OK) + return; + fprintf(stderr, "pn_tt: wait clock %-12s core (%u,%u):", label, queue->x, queue->y); + for (p = 0; p < 5u; p++) + fprintf(stderr, " %s %.2f ms (%u)", names[p], + (double)words[p * 8u + 6u] * 16.0 / 1.35e9 * 1e3 / tokens, words[p * 8u + 7u]); + fprintf(stderr, " [ms/token since launch]\n"); +} + +/* M5: the gdn reader's own clocks, control words 57-60 (resident_gdn_reader.c). */ +static void pn_tt_gdn_reader_clocks(pn_tt_device *device, const ft_tt_queue *queue, + const char *label, double tokens) +{ + uint32_t w[8]; + const double k = 16.0 / 1.35e9 * 1e3 / tokens; + + if (!queue || queue->control_address == 0u || + device->runtime.noc.read(device->runtime.noc.user, queue->x, queue->y, + queue->control_address + 56u * 4u, w, sizeof(w)) != FT_TT_OK) + return; + /* words 56..63: conv barrier, delta barrier, build, conv, delta (reader); CONV waits of + * the writer, unpack, and unpack on the reader's buffers. */ + fprintf(stderr, "pn_tt: gdn reader clocks %-8s: DELTA barrier %.2f build %.2f all %.2f | " + "CONV barrier %.2f all %.2f | CONV waits: writer %.2f unpack %.2f (on " + "reader %.2f) ms/token\n", + label, w[1] * k, w[2] * k, w[4] * k, w[0] * k, w[3] * k, w[5] * k, w[6] * k, + w[7] * k); +} + +void pn_tt_device_wait_report(pn_tt_device *device, double tokens) +{ + if (!device || tokens <= 0.0) + return; + if (device->group.member_count > 1u) + pn_tt_wait_line(device, device->group.member[1].queue, "matmul m1", tokens); + if (device->group.member_count > 0u) + pn_tt_wait_line(device, device->group.member[0].queue, "matmul m0", tokens); + if (device->gdn.running) { + pn_tt_wait_line(device, &device->gdn.queue, "gdn", tokens); + pn_tt_gdn_reader_clocks(device, &device->gdn.queue, "gdn", tokens); + } + if (device->gdn_members > 1u && device->gdn_member[0].running) { + pn_tt_wait_line(device, &device->gdn_member[0].queue, "gdn m1", tokens); + pn_tt_gdn_reader_clocks(device, &device->gdn_member[0].queue, "gdn m1", tokens); + } + if (device->reduce.running) { + uint32_t w[3]; + + pn_tt_wait_line(device, &device->reduce.queue, "reduce", tokens); + if (device->runtime.noc.read(device->runtime.noc.user, device->reduce.queue.x, + device->reduce.queue.y, + device->reduce.queue.control_address + 57u * 4u, w, + sizeof(w)) == FT_TT_OK) + fprintf(stderr, "pn_tt: reduce phase clocks: map %.2f reduce %.2f final %.2f " + "ms/token\n", + (double)w[0] * 16.0 / 1.35e9 * 1e3 / tokens, + (double)w[1] * 16.0 / 1.35e9 * 1e3 / tokens, + (double)w[2] * 16.0 / 1.35e9 * 1e3 / tokens); + } + if (device->eltwise.running) + pn_tt_wait_line(device, &device->eltwise.queue, "eltwise", tokens); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_device.h b/src/accel/tt/pn_tt_device.h new file mode 100644 index 0000000..c5d4e35 --- /dev/null +++ b/src/accel/tt/pn_tt_device.h @@ -0,0 +1,1340 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_device.h + * @brief Pantheon's handle on a Tenstorrent Blackhole card: the weight residency and the + * one primitive the model path needs — a row vector times a resident matrix, with + * the multiply executing on the card. + * + * This is the model-agnostic half of the TT backend integration. It knows about + * FTTW artifacts, DRAM residency, tile layout and matmul dispatch; it knows nothing + * about llama, attention or tokens. src/models/llama_forward_tt.c is the caller. + * + * --------------------------------------------------------------------------- + * WHAT RUNS ON THE CARD, AND WHAT IS LEFT ON THE HOST + * --------------------------------------------------------------------------- + * Every matmul weight of a converted model lives in the card's DRAM and every projection + * is a Tensix matmul. The RMS norms, the SwiGLU gate, the residual adds and the LM head's + * top-N run there too, each on its OWN core, because a resident binary is linked at a fixed + * slab and one program can be resident on a core at a time (foundry/tt/ft_tt_op_vector.h). + * They hand activations to each other through card DRAM, which the host never touches — + * that is the whole mechanism, and it is not a faster transfer but a transfer that does not + * happen. + * + * THREE THINGS ARE STILL HOST-SIDE, and two of them are FORCED rather than chosen: + * + * - **RoPE — the kernel's refusal is FIXED, and fixing it removed no transfer.** + * foundry/tt/ft_tt_op_rope.h refuses INTERLEAVED rotary pairing (2i, 2i+1), which is the + * style this model family takes, and said the fix was to permute the wq/wk output rows + * in the CONVERTER. **The converter does that now** — `--rope-pairing split-half`, with + * the choice recorded in the artifact header and read back through + * pn_tt_device_rope_is_split_half(). So a split-half artifact's q and k ARE in the form + * the kernel accepts. What it does not do on its own is remove the readback: the KV + * append and the GQA softmax are still host-side, and they need q, k and v on the host + * regardless of pairing. **RoPE, KV-append and attention have to move together or none + * of them moves anything.** + * - **Embedding.** foundry/tt/ft_tt_op_memory.h requires a ROW-MAJOR UNTILIZED table, + * because a row of a tilized table is a set of 32-byte face rows and a 32-byte DRAM read + * is not legal. The artifact stores `token_embd.weight` TILED (and block-float), so the + * on-card gather cannot read it. This one is CHEAP: it costs one 64-byte-a-tile upload of + * the first hidden state per token, in the direction the link is 60x faster. + * - **Attention.** The flash-decode op exists and is green; it stays on the host here + * because RoPE already forces q/k/v across the link, so moving it would remove no + * transfer — only the KV cache would move, and that is a separate piece of work. + * + * Which of the movable ops actually move is `PN_TT_ONCARD`, a comma list of + * `norm,swiglu,residual,topk` (or `all`, the default, or `none`). It exists so the A/B is + * across ONE binary running the same kernels on the same weights. + * + * --------------------------------------------------------------------------- + * THE DISPATCH PATH — the RESIDENT work queue, with one-shot kept as the fallback + * --------------------------------------------------------------------------- + * A projection is enqueued, not launched. Five processors sit resident on the core reading + * 32-byte descriptors out of its own L1 (foundry/tt/ft_tt_queue.h), so the host's per-op + * work is a parameter block, a descriptor and a 4-byte head bump with no round trip. The + * one-shot path it replaces — `ft_tt_op_matmul_run()` — re-uploaded five kernel images per + * op at 36.2 µs/kB, and that upload was the whole of its fixed cost. + * + * **ONE resident program build serves this model, not two, and that is a consequence of a + * PRECISION decision rather than a dispatch one.** The production ledger used to put + * `ffn_gate`/`ffn_up` at Bfp4_b, so an artifact carried two matmul tile sizes and a core + * that can hold ONE resident binary could not serve it. Those two tensors measured +9.50% + * perplexity and per-op PCC 0.991-0.993 against 0.9999 everywhere else, at no throughput + * (BFP4 is not faster than BFP8), so they moved to Bfp8_b — and the artifact now holds a + * single tile size. `rtriad_bfp8` is the whole requirement: + * RELOCATE_KERNEL_TEXT=1 READER_CTARGS=0,1,2048,1088,1 COMPUTE_CTARGS=0,1,16,1 + * WRITER_CTARGS=16,2048,1 CB_TABLE=kernels/cb_tables/matmul_bfp8_b.cbtable + * + * The one-shot arm is kept and is reachable with `PN_TT_DISPATCH=oneshot`: it is the same + * binary running the same kernels on the same weights, which is what makes an A/B across + * that knob a measurement of DISPATCH rather than of two programs. + * + * **IT IS NO LONGER A FALLBACK.** If `rtriad_bfp8` is absent, or was built against other + * firmware, or the artifact's matmul tile size is not the one it serves, the device REFUSES + * to open. Falling back cost a round: the program was missing from the host's program root + * and every timing taken in that window was silently one-shot. MEASURED, with only this + * knob moved: **0.343 -> 0.372 s a token, 1.085x**, all of it in the dispatch phase + * (0.156 -> 0.185). Small enough to publish by accident, large enough to be wrong. A slower + * path that is chosen FOR you is a wrong measurement waiting to be published; one that is + * asked for is a decision. + * The same rule now covers the on-card families: a `PN_TT_ONCARD` name whose program + * directory is missing is a refusal, not an op that quietly stays on the host. + * The mid-run re-arm after a REFUSED DESCRIPTOR is the one place one-shot is still entered + * without being asked, because there the alternative is a lost token — and it is counted in + * `pn_tt_device_dispatch_counts()`. + * + * **A refused descriptor is the hazard, and it is handled BEFORE it is sent.** The reader + * refuses a shape it cannot execute by leaving its loop; `tail` never advances, so the + * other four processors then wait on work that will never arrive — recoverable only by + * ASKING them to stop, never by a reset. So the host proves every descriptor executable + * first, and if one is refused anyway it retires all five, arms a fresh residency and + * retries once before falling back. Measured: a deliberately malformed descriptor (Nt=0) + * is refused, the residency is re-armed on the same core, the remaining ops run resident, + * the token is byte-identical, and the core still probes PASS. + * + * --------------------------------------------------------------------------- + * TWO THINGS CROSS THE LINK, AND BOTH ARE SHAPED FOR IT + * --------------------------------------------------------------------------- + * Card->host is ~4.7 MB/s with no device->host DMA, so a token is decided by how many + * bytes go over PCIe and through which accessor. Two rules hold this path: + * + * 1. **Bulk data uses the WC/BLOCK16 window, never the UC/WORD32 register one.** The + * register accessor exists for 4-byte registers and go messages, where the width is a + * correctness requirement; data inherited it and that alone was 71.1% of a 28 s token. + * ft_tt_runtime carries both windows and pn_tt_device_window_traffic() is the evidence. + * 2. **Only logical row 0 of a tile crosses**, in either direction — a matvec is Mt=1 and + * the other 31 rows are the tile quantum's zero padding (ft_tt_tile_row0.h). + * + * Together those took the token from 28.0 s to 0.57 s, and the on-card residencies above + * remove most of what was left: with norm, SwiGLU and the residual adds on the card, the + * only activations that still cross are q/k/v (the RoPE gap) and the logit row (which the + * on-card top-N removes in turn). + * + * @status Phase 7. Matmul, RMS norm, SwiGLU, residual add and top-N on the card. + * @issues Moving an op on-card MOVES ITS NUMERICS — the card computes in bf16 with an fp32 + * destination accumulator where the host used fp32 throughout, and each on-card + * stage rounds its output to bf16. That is a real change and is reported, not tuned + * away. + * @todo + * - [ ] Permute wq/wk in the converter so RoPE can move too, and q/k/v stop crossing. + */ + +#ifndef PN_TT_DEVICE_H +#define PN_TT_DEVICE_H + +#ifdef USE_TT + +#include +#include +#include + +#include "pn_tt_column_runs.h" +#include "pn_tt_descriptor_census.h" + +/** How many Tensix one projection may be split across. 16 is the cooperative driver's own + * bound and not a rectangle — a SPLIT group multicasts nothing. + * + * This was 8, the DRAM bank count, on the argument that a ninth member is a second stream + * on a bank rather than a new one. That argument is about BANDWIDTH and it still holds; + * what changed is that it was also acting as the ceiling on the experiment. Past 8 the + * bank cursor gives members `c` and `c+8` the same bank at DIFFERENT addresses — a + * per-bucket cursor, never a constant base, which is the shape that once had two members + * silently overwriting each other with every status word clean + * (foundry/tt/DEV_NOTES.md §3). + * + * RAISED TO 32 ON 2026-08-19, for the same reason it was raised from 8: the bound was + * acting as the ceiling on the experiment. With the hot A strips mirrored across banks the + * member count is no longer capped by one bank's ~59 GB/s, and the question "where does + * this actually scale to" cannot be asked at a limit of 16. It is a limit on ARRAYS here + * and on nothing else — the placement, the shard split and the door chooser are all + * written in the member count. What DOES bind past 16 is host ISSUE at 8.9 µs a member per + * projection: at 32 members that alone is 184 x 32 x 8.9 µs = 0.052 s/token, which is why + * a wide arm is an experiment and not a default. */ +#define PN_TT_MATMUL_MAX_CORES 64u + +/** Projections that may be issued before one is awaited. The ring holds 8 descriptors per + * member and `ft_tt_queue_stage()` does not check occupancy, so this is the bound that + * keeps a chain from writing over descriptors the card has not read. The longest chain the + * forward actually builds is 3 (q/k/v). */ +#define PN_TT_MATVEC_MAX_CHAIN 4u + +/** The deepest chain `PN_TT_MATVEC_CHAIN` may ask for. The ring and the parameter-slot table + * both hold 8, so 8 is the structural ceiling and not a preference; the DEFAULT stays 4, so + * a run that sets nothing is the run every measurement to date was taken on. */ +#define PN_TT_MATVEC_CHAIN_CEILING 8u + +/** + * Descriptors this run puts in flight before the host waits — the DISPATCH-phase instrument. + * + * `dispatch` is 0.305 s/token over 249 matmuls and the project has read 1.23 ms a matmul as a + * PCIe round trip. That reading is testable and this is the test: a host round trip is paid + * once per WAIT, so it divides by the chain depth, and the card's own work does not. Sweeping + * `PN_TT_MATVEC_CHAIN` over 1, 2, 4, 8 and fitting `C + F/N` separates the two — and what + * Phase 3 can remove is F, not C. + * + * Read from the environment on every call, clamped to 1..PN_TT_MATVEC_CHAIN_CEILING, and 4 + * when unset. + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +uint32_t pn_tt_matvec_chain_limit(void); + +/** One matmul weight resident in card DRAM, in the orientation the Tensix matmul wants: + * K x N = [in_features x out_features], tile grid stored N-major. Both facts come from + * the artifact's `layout` field and are checked, never assumed. */ +typedef struct pn_tt_weight { + uint32_t bank; /**< logical DRAM bank of shard 0 */ + uint32_t address; /**< byte offset within that bank */ + uint32_t tile_rows; /**< Kt = in_features / 32 */ + uint32_t tile_cols; /**< Nt = out_features / 32 of the WHOLE weight */ + uint32_t tile_bytes; /**< 1088 Bfp8_b, 576 Bfp4_b, 2048 bf16 */ + uint32_t in_features; + uint32_t out_features; + /** + * Where the group reads it from, when it was sharded over banks at load. + * + * `shard_count` is 1 for a weight that lives whole in one bank — the pre-group layout — + * in which case only element 0 is meaningful and it repeats `bank`/`address`. Anything + * else means member c reads `shard_tile_cols[c]` columns from column 0 of its OWN panel: + * the full-grid column offset is folded into `shard_address`, which is why the reader + * kernel cannot tell the difference (ft_tt_dram_shard.h). + * + * THE WIDTHS ARE PER MEMBER, not one number, because `Nt % members != 0` used to cost a + * tensor its split entirely. The remainder now goes to the first members one column + * each, so the widths differ by at most one and `shard_column_first` is the RUNNING SUM + * rather than `c * width`. MEASURED 2026-08-16: under the old rule N=5 left 3,864 of + * 3,887 projections on one core and N=7 left ALL of them, which read as a scaling + * ceiling and was a divisibility one. + */ + uint32_t shard_count; + uint32_t shard_tile_cols[PN_TT_MATMUL_MAX_CORES]; /**< this member's own Nt */ + uint32_t shard_column_first[PN_TT_MATMUL_MAX_CORES]; /**< its first column of the whole */ + uint32_t shard_address[PN_TT_MATMUL_MAX_CORES]; + uint16_t shard_x[PN_TT_MATMUL_MAX_CORES]; /**< the DOOR, translated coordinates */ + uint16_t shard_y[PN_TT_MATMUL_MAX_CORES]; + /** The LOGICAL BANK behind each door. The door is a NoC port and three of them lead to + * one bank, so the coordinate cannot be reduced back to a bank — it has to be kept. + * Only the per-bank read accounting reads this; nothing dispatches from it. */ + uint32_t shard_bank[PN_TT_MATMUL_MAX_CORES]; +} pn_tt_weight; + +/** Opaque device handle. One per resident model. */ +typedef struct pn_tt_device pn_tt_device; + +/** + * Open the card, claim a core, boot its firmware, and load the matmul programs. + * + * @param device_index 0 for the first card + * @param out_device receives an owned handle + * @return 0 on success, -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_open(int device_index, pn_tt_device **out_device); + +/** + * Retire the core by ASKING, release the window, close the card. + * + * It never parks: a parked core needs its firmware re-booted, and parking a resident + * program has broken a core's next boot twice in this project. Safe on NULL. + */ +void pn_tt_device_close(pn_tt_device *device); + +/** + * KEEPALIVE (PM 5): NOP every resident family idle >= `min_idle_seconds` (and not yet past the + * idle-retire limit), so neither the card's idle exit nor the host's retire fires and the next + * use pays no re-arm. Does nothing while a stream, replay or recording is open or gdn rows are + * deferred; re-learns the dispatch model after sending. `PN_TT_KEEPALIVE=0` off. + * @return NOPs sent (0 when nothing was due), or -1 + */ +int pn_tt_device_keepalive(pn_tt_device *device, double min_idle_seconds); + +/** + * Upload every matmul weight of an FTTW artifact into card DRAM, round-robin across the + * live DRAM banks. + * + * Only tensors the artifact marks MATMUL_NMAJOR are uploaded. Norms are 1-D fp32 and the + * engine already holds them; the embedding table is tiled block-float and no on-card op + * can read it (see the file note), so neither is moved. + * + * @param path the .fttw file + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_device_load_artifact(pn_tt_device *device, const char *path); + +/** Look up a resident weight by its artifact name, e.g. "blk.3.ffn_down.weight". + * @return 0 and fills `out_weight`, or -1 if the name is not resident. */ +int pn_tt_device_find_weight(const pn_tt_device *device, const char *name, + pn_tt_weight *out_weight); + +/** + * Stage a SECOND, column-permuted copy of an already-loaded weight into card DRAM. + * + * This is how a JOINT projection is split: Ornith's `blk.N.attn_q.weight` emits Q and the + * attention output gate interleaved per head, and every op downstream — the QK-RMSNorm's row + * blocks, flash decode's contiguous group run — needs one of them compact. Permuting the + * WEIGHT costs one upload at load and NOTHING per token; the alternatives are a per-token + * gather op or a stride word in two kernels. `pn_tt_column_runs.h` carries that comparison. + * + * The source stays resident: it is one entry in a bank rotation every other tensor was placed + * against. The cost is therefore a second copy — 71.3 MiB per attention layer for Ornith. + * + * @param source_name the artifact tensor to read, which must be MATMUL_NMAJOR + * @param runs destination-ordered runs of SOURCE column tiles + * @param out_weight fully written, including the derived `tile_cols` / `out_features` + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +/** + * STEP 4 S2 — a SECOND artifact (the MTP draft head): open it, place one of its matmul weights + * the way the device's own are placed, stage a column subset of one (as + * pn_tt_device_stage_weight_columns), or read one of its 1-D fp32 tensors (`n` floats, NULL if + * the name or the size is wrong). The mapping lives until the device closes. + */ +int pn_tt_device_extra_open(pn_tt_device *device, const char *path); +int pn_tt_device_place_extra_weight(pn_tt_device *device, const char *name, + pn_tt_weight *out_weight); +int pn_tt_device_stage_extra_columns(pn_tt_device *device, const char *source_name, + const pn_tt_column_run *runs, uint32_t run_count, + pn_tt_weight *out_weight); +const float *pn_tt_device_extra_vector(const pn_tt_device *device, const char *name, uint32_t n); + +int pn_tt_device_stage_weight_columns(pn_tt_device *device, const char *source_name, + const pn_tt_column_run *runs, uint32_t run_count, + pn_tt_weight *out_weight); + +/** How many matmul weights are resident, and how many bytes they occupy. */ +uint32_t pn_tt_device_weight_count(const pn_tt_device *device); +uint64_t pn_tt_device_weight_bytes(const pn_tt_device *device); + +/** + * Card DRAM handed out and still free, summed over the live banks. + * + * The weight figure alone does not answer a capacity question: the KV regions, the GDN + * carried state, the activation mirrors, the RoPE table and the constants all come out of + * the same arenas and none of them are weights. Every "how many sessions fit" claim so far + * has been ARITHMETIC over a bank size and a file size; this is the measurement it should + * have been checked against. + * + * @param used_out optional: bytes allocated and not yet freed + * @param total_out optional: bytes the live banks could ever hand out + * + * Thread-safe: no Reentrant: yes Signal-safe: no + */ +void pn_tt_device_dram_usage(const pn_tt_device *device, uint64_t *used_out, + uint64_t *total_out); + +/** One stderr line naming `tag`, used and total in GiB. Cheap enough to call on every arm, + * and the only place a capacity claim can be READ OFF a run rather than derived. */ +void pn_tt_device_report_dram(const pn_tt_device *device, const char *tag); + +/** + * Non-zero when the loaded artifact stores `attn_q`/`attn_k` with SPLIT-HALF rotary + * pairing — the converter's `--rope-pairing split-half`, which runs llama.cpp's HF->GGUF + * row permutation backwards. + * + * **The forward MUST read this rather than infer the pairing from the architecture.** The + * pairing is a property of the FILE, not of the model: the same GGUF converts either way, + * the two artifacts are the same size and the same shape and hold the same tensors, and + * rotating split-half rows with the interleaved form (or the reverse) is wrong with no + * symptom except the answer. + * + * @return 1 when the flag is set, 0 when it is not or no artifact is loaded + * + * Thread-safe: yes (read-only) Reentrant: yes Signal-safe: yes + */ +int pn_tt_device_rope_is_split_half(const pn_tt_device *device); + +/** + * Whether the artifact's rotary PAIR INDEX is reflected on top of split-half. + * + * `--rope-pairing split-half-reversed` is an EXPERIMENT the converter offers so the + * block-float exponent grouping over `attn_q`/`attn_k` can be drawn a THIRD way while the + * attention score stays exact. The obligation it puts on a consumer is one sentence: **slot + * `p` holds the model's rotary pair `half - 1 - p`, so apply frequency `half - 1 - p` + * there.** + * + * Deliberately a SECOND predicate rather than a third value of the first one. The storage + * really is split-half-shaped, so every caller that cares only about the TILE argument keeps + * the answer it already had, and only the two callers that care about FREQUENCY ORDER — the + * host rotary, and the on-card arm which must refuse because its table is built in slot + * order — grow a branch. + * + * @return 1 when the flag is set, 0 when it is not or no artifact is loaded + * + * Thread-safe: yes (read-only) Reentrant: yes Signal-safe: yes + */ +int pn_tt_device_rope_pairs_reversed(const pn_tt_device *device); + +/** + * y[0..N) = x[0..K) * W[K x N], with the multiply on the card. + * + * The row is bf16-rounded on the way in and on the way out, because that is the card's + * activation format. A caller comparing against an fp32 host reference should expect that + * difference and not a bit-exact answer. + * + * @param x `weight->in_features` floats + * @param y `weight->out_features` floats, written + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no (one core, one staging buffer) Reentrant: no Signal-safe: no + */ +int pn_tt_matvec(pn_tt_device *device, const pn_tt_weight *weight, const float *x, float *y); + +/* ================================================================ + * The ON-CARD forward: DRAM activation strips and the ops over them + * ================================================================ */ + +/** + * A vector living in card DRAM, in the shape the Tensix ops read: a strip of consecutive + * 32x32 bf16 tiles whose LOGICAL ROW 0 carries the value and whose other 31 rows are the + * tile quantum's zero padding. + * + * Those 31 rows must be zero and stay zero, because every op feeds all 32 to the FPU. + * pn_tt_activation_alloc() zeroes the whole strip once, and every on-card op preserves it + * for free: a matmul of a zero row is zero, silu(0)*0 is zero, 0+0 is zero, and an RMS norm + * of a zero row is 0 * gamma * rsqrt(eps). + */ +/** Mirrors one strip may have, primary included. The card has eight DRAM banks and a ninth + * copy would be a second copy in a bank that already has one. */ +#define PN_TT_ACTIVATION_MAX_REPLICAS 8u + +typedef struct pn_tt_activation { + uint32_t bank; /**< logical DRAM bank */ + uint32_t address; /**< byte offset within that bank */ + uint32_t tile_count; /**< features / 32 */ + + /* ------------------------------------------------------------------ + * MIRRORS, so N matmul members do not all read ONE bank + * ------------------------------------------------------------------ + * The card's 510.8 GB/s is EIGHT banks of ~59. A strip is allocated in one of them, and + * every member of a column-split projection reads the WHOLE strip — so a 16-member + * matmul puts sixteen readers on one bank. Measured on Ornith during decode: bank 2 at + * 43.1 GB/s, 73% of its own ceiling, with the other seven under 5 and the card at 16% + * of peak. That is why matmul card work is flat from four members to sixteen and why + * the shipped setting is four cores on a 140-Tensix part. + * + * `replica[0]` IS `{bank, address}` and is always valid, so every consumer that does + * not know about mirrors — the host uploads, the GDN op, attention, the elementwise + * reader, `pn_tt_topk` — is byte-for-byte unchanged. Only the matmul's A side indexes + * past it, with member `m` reading `replica[m % replica_count]`. + * + * `replica_count` 1 is the unreplicated strip and every branch on it is then a no-op. + * WHAT KEEPS THE MIRRORS CURRENT IS THE PRODUCER, not this struct: the shared resident + * vector writer fans a tile out to the destinations its replica table names, and a + * strip whose producer is not that writer must be replicated by whoever writes it or + * not replicated at all. A mirror nobody refreshes is stale, and a matmul member + * reading it returns a plausible wrong answer with no error anywhere. + */ + uint32_t replica_count; + uint32_t replica_bank[PN_TT_ACTIVATION_MAX_REPLICAS]; + uint32_t replica_address[PN_TT_ACTIVATION_MAX_REPLICAS]; + + /* ------------------------------------------------------------------ + * THE SECOND TILE-ROW: `Mt > 1`, and why it is three fields rather than one + * ------------------------------------------------------------------ + * A strip is `tile_rows` bands of `tile_count` tiles, TILE-ROW-MAJOR — band `m` starts at + * `m * tile_count * PN_TT_TILE_BF16`. That is the layout both matmul kernels already + * assume: the reader's A offset is `(mt + row) * tile_inner + kt` + * (resident_matmul_reader.c) and the writer's destination is `(mt + row) * pitch + nt + + * column` with the pitch in descriptor word a2 (resident_matmul_writer.c). Nothing on the + * device had to change for this; only the host ever wrote `Mt = 1`. + * + * `tile_rows` is the ALLOCATION. `active_rows` is how many bands the next op covers, and + * it is a separate number because most ops are NOT tile-row-general: an RMS norm is + * `row_blocks` (it is), an elementwise op is a wider tile count (it is), and a per-head + * norm, a RoPE, a KV append, an attention chunk and both GDN mixers have already SPENT + * their row axis on heads or on positions and can only be run ONCE PER BAND. Those are + * driven by `row_offset` instead: `pn_tt_activation_seek()` slides `address` and every + * mirror to band `m` and leaves `active_rows` at one, so an op written for a 32-row strip + * runs on band `m` UNMODIFIED and cannot tell. + * + * **THE DEFAULT IS `tile_rows = active_rows = 1, row_offset = 0`, which is the strip this + * struct described before these fields existed, byte for byte.** Decode never leaves it, + * so a decode `cmp` is the evidence that the whole mechanism is inert where it does not + * pay. + */ + uint32_t tile_rows; /**< tile-rows allocated; 1 is the pre-Mt strip */ + uint32_t active_rows; /**< tile-rows the next op spans; 1..tile_rows */ + uint32_t row_offset; /**< which tile-row `address` currently points at */ +} pn_tt_activation; + +/** The constants one RMS norm needs, resident in card DRAM: the reduce scaler tile, the eps + * tile, then gamma expanded to full tiles. Built once per norm weight, at load. */ +typedef struct pn_tt_norm_constants { + uint32_t bank; + uint32_t address; + uint32_t tile_cols; /**< width / 32 */ +} pn_tt_norm_constants; + +/** Which host-side op a run has moved onto the card. `PN_TT_ONCARD` selects them. */ +typedef enum pn_tt_oncard_op { + PN_TT_ONCARD_NORM = 0, /**< the RMS norms, on the reduce residency */ + PN_TT_ONCARD_SWIGLU, /**< silu(gate) * up, on the elementwise residency */ + PN_TT_ONCARD_RESIDUAL, /**< the two residual adds, on the same residency */ + PN_TT_ONCARD_TOPK, /**< exact top-N over the logit row */ + /** + * RoPE + KV append + flash-decode attention, on THREE more resident cores, with the KV + * cache moved into card DRAM. + * + * **OPT-IN ONLY — `PN_TT_ONCARD=all` does NOT include it, and that asymmetry is + * deliberate.** The other four move an op whose arithmetic the card reproduces in a + * different precision; this one moves WHERE THE KV CACHE LIVES and changes the order the + * attention sum accumulates in. It is a different forward, not a faster one, so it has to + * be asked for by name: `PN_TT_ONCARD=norm,swiglu,residual,topk,attention`. + */ + PN_TT_ONCARD_ATTENTION, + /** + * The whole Gated-Delta-Net block — `conv1d + SiLU` and the delta rule — on ONE more + * resident core, with the conv window and the `[S,S]` recurrent state living in card DRAM + * across tokens. + * + * **OPT-IN ONLY, and for a different reason from attention's.** Attention is opt-in + * because it is a different forward; this one is opt-in because it has NEVER EXECUTED ON + * SILICON and because, until the two per-head norms move too, it is measurably SLOWER + * than the host recurrence — the L2 norm in the middle of the block forces `conv_out` + * and `y` back across the link. `pn_tt_gdn.h` carries that arithmetic. Ask for it by + * name: `PN_TT_ONCARD=norm,swiglu,residual,topk,gdn`. + */ + PN_TT_ONCARD_GDN, + PN_TT_ONCARD_COUNT +} pn_tt_oncard_op; + +/** + * Bring up the on-card op residencies this device can serve, one core each. + * + * Deliberately never fatal, and each family is independent: a card with no elementwise + * program still runs the norms on the card and the SwiGLU on the host. What actually armed + * is `pn_tt_device_oncard()`, and the caller must ask rather than assume. + * + * The shapes are the CALLER's because they are the model's, not the artifact's: a reduce + * residency's circular buffers are placed once for one width, and an elementwise residency + * runs ONE block depth for every descriptor it will ever see, so the depth has to divide + * every tile count in `eltwise_tile_counts` and twice the depth has to divide them too (a + * descriptor that leaves the CB write pointer off the block grid walks the NEXT one up L1 — + * foundry/tt/ft_tt_op_sample.h states the invariant and the core it cost). + * + * @param norm_tile_cols width / 32 of every vector the norms reduce over + * @param eltwise_tile_counts every tile count an elementwise descriptor will carry + * @param eltwise_count how many + * @param vocab_tiles the logit row's tile count, or 0 for no sampling residency + * @return 0 (even when nothing armed), or -1 on a bad argument + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_oncard(pn_tt_device *device, uint32_t norm_tile_cols, + const uint32_t *eltwise_tile_counts, uint32_t eltwise_count, + uint32_t vocab_tiles); + +/** Did `op` actually move onto the card for this run? The caller runs the host version + * when this is false, so a missing program directory is a slower forward and never a + * wrong one. */ +bool pn_tt_device_oncard(const pn_tt_device *device, pn_tt_oncard_op op); + +/** Was `op` ASKED for, whether or not it armed? The difference matters exactly once: a + * family that was asked for and did not arm is a REFUSAL, and a family that was never asked + * for is a configuration. */ +bool pn_tt_device_oncard_requested(const pn_tt_device *device, pn_tt_oncard_op op); + +/** + * Declare what `PN_TT_ONCARD` UNSET — or `all` — selects for `op` on THIS model, and re-read + * the selection so `pn_tt_device_oncard_requested()` is answerable immediately. + * + * **A DEFAULT IS A PROPERTY OF A MODEL FAMILY, NOT OF THE CARD.** On-card attention is a + * different forward on each family and the evidence for it is measured per family: on the + * qwen35/bonsai hybrid it passes a calibrated chat-parity gate and is faster at every length, + * and on MiniCPM5-1B through `llama_forward_tt.c` it is recorded as FAILING that same gate. + * A single constant in `pn_tt_read_oncard_selection()` would have shipped the second as a + * side effect of shipping the first — and silently, because the llama backend arms attention + * LAZILY, after `pn_tt_device_arm_oncard()` has already run. + * + * An explicit `PN_TT_ONCARD` list still wins: this changes the DEFAULT and never overrides an + * operator who named something. Call it before `pn_tt_device_arm_oncard()`. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_tt_device_oncard_default(pn_tt_device *device, pn_tt_oncard_op op, bool on); + +/** + * Was `op` named in `PN_TT_ONCARD` by the OPERATOR, as opposed to being on by default? + * + * **A FAILED ARM MEANS TWO DIFFERENT THINGS AND `..._requested()` CANNOT TELL THEM APART.** + * A NAMED family that will not arm must refuse loudly — the operator asked for that forward, + * and quietly running the host block instead would be a different model reported as the same + * one. A family that is merely the DEFAULT must degrade loudly to the host block, because a + * refusal there propagates to `goto fail` in pn_qwen35moe_tt_upload and drops the ENTIRE + * model to the CPU forward: a card that cannot arm one op would serve Ornith at ~0.05 tok/s + * rather than at its host-attention speed. + * + * The distinction is inert while attention and GDN are opt-in, and becomes load-bearing the + * moment either is defaulted on. + */ +bool pn_tt_device_oncard_named(const pn_tt_device *device, pn_tt_oncard_op op); + +/** + * Allocate a `tile_count`-tile bf16 strip in card DRAM and ZERO it. + * + * The zeroing is the precondition every later row-0 write depends on and cannot check. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_activation_alloc(pn_tt_device *device, uint32_t tile_count, + pn_tt_activation *out_activation); + +/** + * The same, MIRRORED into `replicas` DISTINCT DRAM banks. + * + * `replica[0]` is exactly what `pn_tt_activation_alloc()` would have returned — same bank + * cursor, same zeroing — so a strip allocated this way is a superset of one allocated the + * ordinary way and no consumer has to know. + * + * WHAT THIS DOES NOT DO IS KEEP THEM IN STEP. Allocating the mirrors is half the mechanism; + * the other half is the producer writing all of them, which for `normed` and `ffn_swiglu` is + * the resident vector writer's replica table (`pn_tt_device_publish_replica_table`). A strip + * mirrored here and produced by something that does not fan out is a strip with seven stale + * copies, and the matmul member that reads one gets a plausible wrong answer. + * + * `replicas` 0 or 1 is exactly `pn_tt_activation_alloc()`, so a caller may pass the knob + * through without branching. More replicas than banks is a refusal, not a wrap: two mirrors + * in one bank is the failure the whole thing exists to avoid, wearing the name of the fix. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_activation_alloc_replicated(pn_tt_device *device, uint32_t tile_count, + uint32_t replicas, pn_tt_activation *out_activation); +/** As `pn_tt_activation_alloc` / `_replicated`, at an explicit height — for a strip SET used + * only on its own (PHASE G option 3's prefill-only tall strips). */ +int pn_tt_activation_alloc_rows(pn_tt_device *device, uint32_t tile_count, uint32_t tile_rows, + pn_tt_activation *out_activation); +int pn_tt_activation_alloc_replicated_rows(pn_tt_device *device, uint32_t tile_count, + uint32_t replicas, uint32_t tile_rows, + pn_tt_activation *out_activation); +/** PHASE G option 3: record the prefill-only set's height (0 = none). */ +void pn_tt_device_set_prefill_tile_rows(pn_tt_device *device, uint32_t rows); + +/** Write `count` floats into logical row 0 of a strip, bf16-rounded. Rows 1-31 are left + * alone, which is correct exactly because the strip was allocated zeroed. */ +int pn_tt_activation_upload(pn_tt_device *device, const pn_tt_activation *activation, + const float *values, uint32_t count); + +/** Read logical row 0 of a strip back as `count` floats. This is the transfer the on-card + * forward exists to avoid, so every call site is a place where an op did NOT move. */ +int pn_tt_activation_download(pn_tt_device *device, const pn_tt_activation *activation, + float *out_values, uint32_t count); + +/** + * Read only SOME of logical row 0: every 16-float chunk i (row 0 columns 16*i .. 16*i+15, + * i.e. one of a tile's two row-0 pieces) with need[i] != 0 is written into out_values at + * its columns; the rest of out_values is left alone. For a caller that needs a handful + * of entries of a wide row (the grammar mask's allowed tokens of a logits row): the cost + * is one 64 B read per needed chunk instead of two per tile of the whole row. + * + * @param need count/16 flags + * @param count a multiple of 32, no wider than the strip + * @return 0, or -1 + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_activation_download_chunks(pn_tt_device *device, const pn_tt_activation *activation, + const unsigned char *need, float *out_values, + uint32_t count); + +/** + * Read logical tile row `row` of a strip back as `count` floats — the DIAGNOSTIC reader. + * + * `pn_tt_activation_download()` reads only the two 64-byte chunks logical row 0 lives in, so + * it cannot see rows 1-31 at all. This one pulls WHOLE tiles. Rows 1-31 are the rows the strip + * invariant claims are zero and which flash decode is no longer KNOWN to leave zero + * (pn_tt_oncard.c, invariant 1) — measuring that claim is what this exists for. It moves 32x + * the bytes of the row-0 reader, so it is instrumentation and never the forward. + * + * @param row 0..31; a row outside the tile is a refusal + * @param count a multiple of 32, no wider than the strip + * @return 0, or -1 + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_activation_download_tile_row(pn_tt_device *device, const pn_tt_activation *activation, + uint32_t row, float *out_values, uint32_t count); + +/** + * The BATCHED pair: move logical rows 0..rows-1 of a strip, not only row 0. + * + * --------------------------------------------------------------------------- + * THE 31 ROWS WERE NEVER FREE FOR US, ONLY FREE FOR THE CARD + * --------------------------------------------------------------------------- + * A tile is 32x32 and a matvec is Mt=1, so the FPU already multiplies 32 rows against every + * weight tile it reads and 31 of them are the zero padding `pn_tt_activation_alloc` staged. + * The card's cost — the reader floor, the weight tiles, the descriptor — is IDENTICAL + * whether one of those rows carries a token or all 32 do. So a prefill that puts 32 prompt + * positions into one tile pays the card once for 32 tokens: not an optimisation of the + * matmul, the same matmul with its waste removed. Everything after it is unchanged — + * `pn_tt_rms_norm` already submits `row_blocks = 1`, i.e. one 32-row block, and the + * elementwise ops are per-tile — so the only thing standing between the card and a batch was + * that nothing could put more than one row across the link. + * + * **These move WHOLE tiles and that is not a regression.** The row-0 reader's 16x saving is + * a saving on ONE row; whole tiles are 16x the bytes for 32x the rows, so the batched + * transfer is 2x cheaper PER TOKEN than the row-0 one, in both directions. `values` is + * [rows x count] row-major fp32. + * + * The upload re-establishes the strip invariant on every call: `ft_tt_tilize_strip` writes + * rows `rows..31` as ZERO, so a batched write leaves the strip in exactly the state a later + * Mt=1 matvec requires and the two paths may be interleaved freely. + * + * @param rows 1..32; 0 or >32 is a refusal (a tile has 32 rows and nothing here wraps) + * @param count a multiple of 32, no wider than the strip + * @return 0, or -1 + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_activation_upload_rows(pn_tt_device *device, const pn_tt_activation *activation, + const float *values, uint32_t count, uint32_t rows); + +/** @copydoc pn_tt_activation_upload_rows */ +int pn_tt_activation_download_rows(pn_tt_device *device, const pn_tt_activation *activation, + float *out_values, uint32_t count, uint32_t rows); + +/** + * Re-stage a whole strip as ZERO — the strip invariant, restored on demand. + * + * `pn_tt_activation_alloc` zeroes once and every Mt=1 op preserves it for free, so nothing + * needed this. A batched pass DELIBERATELY fills rows 1-31 with prompt positions, and the + * card then writes 32 live rows into every output strip. Nothing in the decode path reads + * those rows — `pn_tt_topk` names row 0 explicitly and every other op is per-row — but + * leaving a batch's residue behind would make the invariant a claim about who reads what + * instead of a property of the strip. This is what returns it to a property. + * + * @return 0, or -1 + */ +int pn_tt_activation_zero(pn_tt_device *device, const pn_tt_activation *activation); + +/** `pn_tt_activation_zero` over the first `bands` tile-rows only (0 = all of them). */ +int pn_tt_activation_zero_bands(pn_tt_device *device, const pn_tt_activation *activation, + uint32_t bands); + +/** `pn_tt_activation_zero_bands` with every byte `value` (0xFF = bf16 NaN: a residue poison). */ +int pn_tt_activation_fill_bands(pn_tt_device *device, const pn_tt_activation *activation, + uint32_t bands, unsigned char value); + +/** How many 32-row bands every activation strip is allocated with. See the `tile_rows` block + * above. 1 is the shipped strip, and the whole mechanism collapses to the pre-Mt one there. */ +#define PN_TT_STRIP_TILE_ROWS_ENV "PN_TT_STRIP_TILE_ROWS" + +/** + * How many tile-rows `pn_tt_activation_alloc` gives every strip — `PN_TT_STRIP_TILE_ROWS`. + * + * Read ONCE, before any strip is allocated, because a strip whose height disagrees with its + * neighbours' is a matmul reading one band and writing another. 1 is the shipped value and + * the pre-Mt allocation byte for byte. + */ +uint32_t pn_tt_device_strip_tile_rows(pn_tt_device *device); + +/** + * The strip height THIS MODEL wants when `PN_TT_STRIP_TILE_ROWS` is unset. Must be called + * before the first strip is allocated, and only by a model whose batched prefill runs both + * chunk paths — any other model at more than one band declines its batched prefill. + * @return 0, or -1 if the height was already resolved (too late to prefer anything) + */ +int pn_tt_device_prefer_strip_tile_rows(pn_tt_device *device, uint32_t rows); + +/** + * Declare that every strip holds ONE live row, row 0 (a single-token forward), so ops may + * compute that row only (PHASE E). Rows 1-31 are then whatever the cheap path leaves — the + * zero-padding invariant holds only because their inputs are zero. Clear it before any + * multi-row op. `PN_TT_ELTWISE_ROW0`: 0 off, 1 (default) honour the promise, 2 RED: apply + * row 0 to EVERY descriptor, multi-row prefill included, which must change its output. + */ +void pn_tt_device_set_single_row(pn_tt_device *device, bool single_row); +/** S2c: the verify's live-row promise (pn_tt_device_internal.h `verify_rows`; 0 = none). */ +void pn_tt_device_set_verify_rows(pn_tt_device *device, uint32_t rows); +/** S2c: the logits row pn_tt_topk() selects over (0 = the decode row). */ +void pn_tt_device_set_topk_row(pn_tt_device *device, uint32_t row); + +/** M5: per-processor CB-wait time of the main residencies (`pn_tt: wait clock` lines). */ +void pn_tt_device_wait_report(pn_tt_device *device, double tokens); + +/** + * The matmul K-block depth this model wants when `PN_TT_PREFETCH_TILES` is unset. Call before + * the artifact loads (the residency arms there). Per model, so another model's default does + * not move as a side effect. Qwen3.5 hybrid: 4 (M1, +0.6-1.0% decode, +0.6% prefill). + */ +void pn_tt_device_prefer_prefetch_tiles(pn_tt_device *device, uint32_t tiles); + +/** M4: this model wants the pipelined matmul reader with 3-chunk CBs (env overrides + * `PN_TT_MATMUL_PIPE`, `PN_TT_MATMUL_CB_DEEP`). Call before the artifact loads. */ +void pn_tt_device_prefer_matmul_pipe(pn_tt_device *device, bool pipe); + +/** + * Point a strip at tile-row `row`, so an op written for a 32-row strip runs on band `row`. + * + * THE ONLY MECHANISM THAT REACHES THE SECOND BAND FOR THE OPS THAT CANNOT BE WIDENED, and + * they are the majority: the per-head norms spend `row_blocks` on heads + * (`pn_tt_head_norm.c`), RoPE's table is one 32-position block, the KV append's `source_row0` + * is capped at 32, the attention chunk REFUSES a non-zero `query_row` in chunk mode + * (`ft_tt_op_attention.c`), and GDN PHASE CONV refuses `first_row` outright + * (`ft_tt_op_gdnc.c`). None of them needs a new field: every one of them addresses DRAM + * through the operand's `address`, so sliding the address by a whole band is the same + * descriptor over different bytes. + * + * Slides every MIRROR by the same amount, because a matmul member reads `replica[m % + * replica_count]` and a mirror left behind is the previous band's answer with no error + * anywhere. + * + * @param row 0..tile_rows-1 + * @return 0, or -1 (out of range, or no strip) + */ +int pn_tt_activation_seek(pn_tt_activation *activation, uint32_t row); + +/** How many tile-rows the NEXT op over this strip spans. Only the ops that are genuinely + * tile-row-general read it — the RMS norm as `row_blocks`, the elementwise ops as a wider + * tile count, the matmul as `Mt`. 1 is the shipped span. */ +int pn_tt_activation_span(pn_tt_activation *activation, uint32_t rows); + +/** + * y = x * W with BOTH operands already on the card — the projection of the on-card forward. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_matvec_resident(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output); + +/** + * y = x * W, ISSUED and not awaited — the same projection, without the round trip. + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS + * --------------------------------------------------------------------------- + * The resident queue was built so the host enqueues and bumps a counter, and the forward + * then waited on every one of its 169 projections anyway. Two things cost: the host asks + * every member whether it is done (linear in the member count), and the card sits idle + * while the host issues the next descriptor. Within a layer **q/k/v are independent** and + * **gate/up are independent**, so their descriptors can sit in the ring together — the + * card runs them back to back at its own 0.39-0.45 us op-to-op, and the host waits FOUR + * times a layer instead of seven. + * + * --------------------------------------------------------------------------- + * WHAT THE CALLER OWES + * --------------------------------------------------------------------------- + * Chained projections must be INDEPENDENT: same input, disjoint outputs. They run on + * different cores concurrently, so a second projection reading the first's output would + * race even though one core's own queue is ordered. + * + * Nothing else has to be remembered. Every other device operation — a download, an upload, + * a norm, a SwiGLU, an attention, a top-N, even a waiting matvec — flushes first, so a + * missed `pn_tt_matvec_flush()` cannot produce a wrong answer, only a later wait. A rule + * enforced at ONE seam beats a rule every call site has to observe. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_matvec_enqueue(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output); + +/** Wait for every projection `pn_tt_matvec_enqueue()` has issued. A no-op when none are + * pending. @return 0, or -1 with the reason on stderr */ +int pn_tt_matvec_flush(pn_tt_device *device); + +/** + * Build one norm weight's constants and upload them. Once per norm, at load — a per-token + * upload of gamma would put back a transfer this file exists to remove. + * + * @param gamma `width` floats + * @param eps the model's rms_norm_eps + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_norm_constants_upload(pn_tt_device *device, const float *gamma, uint32_t width, + float eps, pn_tt_norm_constants *out_constants); + +/** out = in * gamma * rsqrt(mean(in^2) + eps), on the card. */ +int pn_tt_rms_norm(pn_tt_device *device, const pn_tt_norm_constants *constants, + const pn_tt_activation *input, const pn_tt_activation *output); + +/** out = a + b, on the card. */ +int pn_tt_add(pn_tt_device *device, const pn_tt_activation *a, const pn_tt_activation *b, + const pn_tt_activation *output); + +/** out = silu(a) * b, on the card — the SwiGLU gate, as ONE descriptor rather than two, + * because as two it costs a DRAM round trip of the intermediate. */ +int pn_tt_silu_mul(pn_tt_device *device, const pn_tt_activation *a, const pn_tt_activation *b, + const pn_tt_activation *output); + +/** + * out = sigmoid(gate) * b, on the card — Ornith's ATTENTION output gate. + * + * **NOT `pn_tt_silu_mul`**: `silu(x) = x*sigmoid(x)`, so the SwiGLU member is off by a factor + * of the gate itself. There is no fused `sigmoid(a)*b` in the elementwise table, so this is + * TWO descriptors through `scratch` — which is why `scratch` is a parameter rather than + * borrowed: a strip this function chose for itself could alias one of the three the caller + * passed, and an elementwise op writes its output before the next one reads its input. + * + * @param scratch a strip of at least `gate`'s tile count, distinct from `gate` and `b` + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_sigmoid_mul(pn_tt_device *device, const pn_tt_activation *gate, + const pn_tt_activation *b, const pn_tt_activation *scratch, + const pn_tt_activation *output); + +/** + * Exact top-N over a logit row that is already on the card, and NOTHING crosses the link + * but the pairs. + * + * The comparator is the deployed K80 one — highest value wins, LOWEST index breaks a tie — + * so an index this returns is comparable to a host serial argmax over the same row rather + * than merely close to it. That is the gate this op is accepted on: EXACT INDEX AGREEMENT, + * not a correlation. + * + * @param out_indices at least `count` entries + * @param out_values at least `count` entries, fp32 + * @param out_count receives how many pairs the card produced + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_topk(pn_tt_device *device, const pn_tt_activation *logits, uint32_t count, + uint32_t *out_indices, float *out_values, uint32_t *out_count); + +/* ================================================================ + * ON-CARD ATTENTION: RoPE, the KV append, and flash decode + * ================================================================ + * + * These three move together or none of them moves a byte — q, k and v cross the link for + * whichever of the three is left on the host, and that readback is 0.052 s of a 0.128 s + * token (~240 KB against a measured 4.7 MB/s card->host). + * + * --------------------------------------------------------------------------- + * WHERE THE KV CACHE LIVES, AND WHAT IT COSTS SESSION EXPORT + * --------------------------------------------------------------------------- + * On-card attention wants the KV cache in card DRAM, bf16, in the layout + * foundry/tt/ft_tt_op_memory.h PART 1 fixes (F1-F5). Session export was built on the + * opposite fact: it is free BECAUSE the cache is host fp32, so exporting reads a host + * pointer and zero bytes cross PCIe (49,152 B/token of blob, 0 B of link). + * + * **THE CACHE MOVES AND EXPORT PAYS AT EXPORT TIME, NOT PER TOKEN. The arithmetic is not + * close.** A host mirror maintained by downloading k and v every layer costs + * 2 x 8 tiles x 128 B = 2,048 B a layer = 49,152 B a token of ROW-0 reads (each 64 B chunk + * carries 32 B of payload — the read alignment, ft_tt_tile_row0.h), i.e. 10.5 ms EVERY + * token. Downloading the whole card cache once at export reads WHOLE tiles with no + * alignment waste: 24,576 B a position, 5.2 ms a position-equivalent, ~10.7 s for a + * 2048-token session. **So the mirror moves exactly 2x the bytes of the export it makes + * free, and it charges every session including the ones nobody ever exports.** A + * 2048-token session would pay 21 s of decode to save 10.7 s of export. + * + * Restore is the cheap direction and stays cheap: host->card is 287 MB/s, ~61x the reverse, + * so a 2048-token restore uploads 50.3 MB in ~0.18 s. + * + * The blob therefore becomes bf16 where it was fp32 — which is not a loss of fidelity but a + * statement of where the truth is: once attention runs on the card, the card's bf16 cache + * IS the model state, and an fp32 blob would be storing a precision the forward never had. + * + * --------------------------------------------------------------------------- + * ONE REGION SET, AND INTERLEAVED SESSIONS ARE A REFUSAL + * --------------------------------------------------------------------------- + * The card holds ONE K/V region set per model, allocated at arm time. Stale contents are + * harmless — every position a sequence reads is a position it wrote — so a NEW sequence may + * simply rebind. Two sequences ALTERNATING on one region set would silently overwrite each + * other, so a host cache key that comes back after being displaced is REFUSED by name. + */ + +/** + * Arm the RoPE, KV-append and flash-decode residencies (three more cores) and allocate the + * KV regions of every layer. + * + * Never fatal on its own: a card missing one of the three programs leaves attention on the + * host, exactly like the other families — but ONLY when attention was not asked for. When + * `PN_TT_ONCARD` names it, a missing program is a refusal (pn_tt_device_arm_oncard). + * + * @param q_heads Q heads (16 for MiniCPM5-1B) + * @param kv_heads KV heads (2); must divide q_heads + * @param head_dim multiple of 32 + * @param rope_dim rotated features, 0 for the whole head + * @param rope_theta `rope.freq_base`, read from the checkpoint + * @param capacity_positions the session's max_seq, rounded up to 32 internally + * @param layer_count layers to allocate regions for + * @return 0 (even when nothing armed), -1 on a bad argument or a refusal + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_attention(pn_tt_device *device, uint32_t q_heads, uint32_t kv_heads, + uint32_t head_dim, uint32_t rope_dim, double rope_theta, + uint32_t capacity_positions, uint32_t layer_count); + +/** + * Bind the card's ONE KV region set to a host sequence, identified by any stable pointer. + * + * A key that was displaced and comes back is REFUSED: that is two sequences alternating on + * one region set, which would be a wrong answer with no error anywhere. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_kv_bind(pn_tt_device *device, const void *sequence_key); + +/** NeoX RoPE at `position`, from `input` into `output`, for `heads` heads. + * + * Out of place on purpose: the reader runs ahead of the writer, so writing over the input + * would be an unstated dependency on that lead — the same reason the residual stream keeps + * two strips. */ +int pn_tt_rope(pn_tt_device *device, const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t heads, uint32_t position); + +/** Place one token's K (`values` false) or V (`values` true) row into layer `layer`'s cache + * region at absolute `position`. */ +/** STEP 4 S2c: rotate rows 0..rows-1 of a strip at positions first_position.. (any + * alignment; the decode row's angles), and append ONE row of a strip at `position`. */ +int pn_tt_rope_rows(pn_tt_device *device, const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t heads, uint32_t first_position, + uint32_t rows); +/** S2f: flash decode for strip rows 0..rows-1 at positions first_position.. in ONE descriptor + * a KV group (ROWS mode); every row must end in the same position tile. */ +int pn_tt_attention_rows(pn_tt_device *device, uint32_t layer, const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t first_position, uint32_t rows); +int pn_tt_kv_append_row(pn_tt_device *device, uint32_t layer, bool values, + const pn_tt_activation *source, uint32_t source_row, uint32_t position); +int pn_tt_kv_append(pn_tt_device *device, uint32_t layer, bool values, + const pn_tt_activation *source, uint32_t position); + +/** Flash-decode attention over `kv_positions` cached positions, every KV group of one + * layer, `query` in and `output` out in the model's ordinary activation layout. */ +int pn_tt_attention(pn_tt_device *device, uint32_t layer, const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t kv_positions); + +/* ================================================================ + * The CHUNK forms — the same three ops over C consecutive positions + * ================================================================ + * + * A CHUNK IS THE SAME STRIP A SINGLE POSITION IS, with rows 0..C-1 carrying positions + * instead of padding — the geometry the two chunkwise GDN mixers settled and the reason + * every activation's DRAM layout is unchanged. These three calls are what the attention + * layer needs to stop being per-position; the head norms, the sigmoid gate, the residual + * adds and all nine projections already batch, because a row reduce reduces ALONG the row + * and a matmul's output row is its input row. + * + * `first_position` MUST be a multiple of 32 for all three: RoPE's row-phase contract wants + * it (`ft_tt_op_rope.h`), the KV append's tile is then whole, and attention's causal mask is + * one tile only when the chunk IS the last position tile. `rows` is 1..32. + * + * They are not a mode: decode keeps calling the per-position forms above, which are + * untouched, and the two kinds of descriptor interleave on ONE residency over ONE KV cache. + */ + +/** NeoX RoPE for positions `first_position .. first_position + rows - 1`, carried in rows + * 0..rows-1 of `input`. Rebuilds the whole 32-row rotary block rather than its row 0. */ +int pn_tt_rope_chunk(pn_tt_device *device, const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t heads, + uint32_t first_position, uint32_t rows); + +/** Place `rows` positions of K or V into layer `layer`'s cache in ONE descriptor. At 32 + * rows and an aligned first position this is a whole-tile copy per head, not a row + * scatter — the memory kernel already had it (`resident_place_writer.c`). */ +int pn_tt_kv_append_chunk(pn_tt_device *device, uint32_t layer, bool values, + const pn_tt_activation *source, uint32_t first_position, + uint32_t rows); + +/** + * Attention for a whole chunk: ONE descriptor per Q HEAD, each serving all `rows` + * positions, against the cache up to `first_position + rows` with causality inside the + * chunk. + * + * `q_heads` descriptors replace `kv_heads * rows` — 16 against 128 at Ornith's shape, and + * MEASURED on silicon 2026-08-21 to cost 0.093 ms against a decode descriptor's 0.101 at + * kv=1024 (0.605 against 0.615 at 8192), because the score matmul already computed all 32 + * rows of its tile and threw 28 of them away. A position therefore costs 8.7x less, and the + * KV cache is walked 8x fewer times. The chunk's answer is BIT-IDENTICAL to the decode + * descriptors it replaces, row for row, max-abs 0.000e+00 over the four chunks of a + * 32+32+32+7 gate. + */ +int pn_tt_attention_chunk(pn_tt_device *device, uint32_t layer, const pn_tt_activation *query, + const pn_tt_activation *output, uint32_t first_position, + uint32_t rows); + +/** + * Read one layer's K or V back as fp32 in Pantheon's own [head][position][dim] order — the + * session-export path, and the ONLY place the card cache crosses the link. + * + * @param out `kv_heads * position_count * head_dim` floats + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_kv_download(pn_tt_device *device, uint32_t layer, bool values, + uint32_t position_count, float *out); + +/** The reverse, for restore. host->card is ~61x faster, so this is the cheap direction. */ +int pn_tt_kv_upload(pn_tt_device *device, uint32_t layer, bool values, + uint32_t position_count, const float *in); + +/** Matmuls issued, and the wall seconds spent inside pn_tt_matvec(). See + * pn_tt_device_phase_seconds() for the three-way split of that total. */ +uint64_t pn_tt_device_matvec_count(const pn_tt_device *device); +double pn_tt_device_matvec_seconds(const pn_tt_device *device); + +/** + * The three phases of every matvec, separated because they are three different machines: + * `upload` is a host tilize plus a ~287 MB/s write, `dispatch` is ft_tt_op_matmul_run() + * (five kernel images re-uploaded, CB config, GO, poll DONE) and the card's own compute, + * and `readback` is the ~2-5 MB/s return of the result plus an untilize. + * + * Splitting these is what turns "a token takes N seconds" into an attribution. Any of them + * may be NULL. + */ +void pn_tt_device_phase_seconds(const pn_tt_device *device, double *out_upload, + double *out_dispatch, double *out_readback); + +/** The FOURTH phase, kept out of the three above because it is a fourth machine: the + * enqueue-and-wait for the reduce, elementwise and sampling residencies. Work that used to + * be host arithmetic between two transfers, and is now neither. + * + * `out_restarts` counts the times one of those three residencies had to be ASKED to retire + * and re-armed. It is reported beside the matmul's own restart count and not folded into + * it, because they answer different questions: a matmul restart means a descriptor the + * reader would not execute, and an on-card restart is almost always a kernel that had + * retired on idle between two requests. Any pointer may be NULL. */ +void pn_tt_device_oncard_stats(const pn_tt_device *device, double *out_seconds, + uint64_t *out_ops, uint64_t *out_restarts); + +/** + * That fourth phase BROKEN OUT PER RESIDENCY, which is what a re-pricing needs and the sum + * above cannot give. + * + * `pn_tt_device_oncard_stats()` adds seven families together, so "247 µs a descriptor" is an + * average over a reduce, a SwiGLU, a residual add, a top-N and a GDN block — ops whose work + * differs by three orders of magnitude. Every residency already counts its own descriptors + * and its own seconds inside `pn_tt_residency_submit()`; this walks them so the report can + * print the per-family cost instead of one number that is true of nothing. + * + * @param index 0 upward, in arm order; `false` means past the end + * @return true and the four figures, or false. Any output pointer may be NULL. + */ +bool pn_tt_device_residency_stat(const pn_tt_device *device, unsigned index, + const char **out_name, uint64_t *out_ops, double *out_seconds, + uint32_t *out_core); + +/** Most clocks `pn_tt_device_card_clocks()` can return. */ +#define PN_TT_CARD_CLOCKS_MAX 64u + +/** + * EVERY CARD-SIDE CLOCK THIS DEVICE KEEPS, by name, in an order that is stable for the life of + * the device — so a caller can snapshot it around a bracket and attribute the bracket's wall + * by subtraction, entry by entry. (PHASE P G0: `sequential` in the batched prefill.) + * + * The entries are every residency INCLUDING split members (a member repeats member 0's name; + * sum by name), then the gdnc split's own clocks, which are not residencies: `gdnc-phase0`, + * `gdnc-conv`, `gdnc-delta` (clean descriptors, by `FT_TT_OP_GDNC_PHASE_*`), `gdnc-recovered` (any phase, re-armed) and + * `gdnc-stage` (the host build + upload of the decay/gate blocks). No two entries can contain + * the same second: the gdnc clocks bracket `gdnc_submit()` and the stage brackets its caller's + * staging, which runs before that. + * + * @return the number of entries written, at most `max` + */ +unsigned pn_tt_device_card_clocks(const pn_tt_device *device, const char **out_names, + double *out_seconds, unsigned max); + +/** HOST work inside a forward that no card clock contains, booked by name so the G0 + * attribution can split its "unclocked" remainder. Appear in pn_tt_device_card_clocks(). */ +typedef enum { + PN_TT_HOST_CLOCK_GDN_MATH = 0, /**< beta sigmoid + decay gate + expf, on the host */ + PN_TT_HOST_CLOCK_GDN_CONSTANTS, /**< gdn_strip_rms_constants() */ + PN_TT_HOST_CLOCK_ROPE_TABLE, /**< the rotary table built and uploaded for a chunk */ + PN_TT_HOST_CLOCK_COUNT +} pn_tt_host_clock; + +void pn_tt_device_host_clock_add(pn_tt_device *device, pn_tt_host_clock clock, double seconds); + +/** + * THE AWAIT PRICED IN PCIe ACCESSES, over every residency INCLUDING the split members — + * **SPIN SEPARATED FROM SETTLE, because pooling them hides the thing being measured.** + * + * `poll` is every read inside the wait loop. It is CARD-BOUND: it is how the host waits for + * work that has not finished, and no completion shape removes it. `settle` is the block read + * issued after the wait returned — **16 on `control`, 5 on `window`, 0 on `line`** — and it + * is the only term the shapes move, so it is the only one a ladder may be scored on. + * + * **THE SPLIT EXISTS BECAUSE THE POOLED FORM WAS MEASURED AND WAS UNREADABLE.** Two card + * builds both on the six-read `window` reported **49.0 and 54.5** reads an await, because a + * residency waiting on real card work spins ~44 times. An acceptance criterion of + * "17.0 / 6.0 / 1.0" against that number is either unreadable or read as a failed arm — the + * shape of a harness that measures nothing. + * + * @param out_fast how many residencies with at least one await took the ONE-transaction + * path — so an arm cannot report a saving the knob never armed. It is a + * count and not a flag because a queue the BLOCK16 accessor cannot reach + * falls back on its own, and a partial arm must be visible as a number. + * + * Any pointer may be NULL. + */ +void pn_tt_device_await_transactions(const pn_tt_device *device, uint64_t *out_awaits, + uint64_t *out_poll, uint64_t *out_settle, + unsigned *out_fast); + +/** + * THE ISSUE PRICED THE SAME WAY, and TIMED PER CALL so the fixed cost can be solved rather + * than assumed. + * + * One enqueue is three device writes — a 64 B parameter block, a 32 B ring slot, a 4 B head + * bump — which is **25 posted accesses** at WORD32 and **7** on the wide shape. Neither a byte + * counter nor a call counter can see that difference (same bytes, same three calls), so the + * accesses are counted at the unit of the accessor that was actually used. + * + * **The three clocks are what make the count mean something.** The recorded matmul figures put + * a 16-access write at 2.08 µs and a 9-access pair at 4.84 — which no `fixed + per_access` + * model fits, so one of the two must be dominated by a per-CALL cost. Three calls of 16, 8 and + * 1 accesses timed apart over-determine that split, and the answer decides whether a lever + * that removes ACCESSES can pay at all. + * + * @param out_wide how many residencies with at least one issue took the wide shape — so an + * arm cannot report a saving the knob never armed. + * + * Any pointer may be NULL. + */ +void pn_tt_device_issue_transactions(const pn_tt_device *device, uint64_t *out_issues, + uint64_t *out_accesses, double *out_param_seconds, + double *out_ring_seconds, double *out_head_seconds, + unsigned *out_wide); + +/** + * ONE MEMBER of the elementwise core split — its core, its descriptors and its own AWAIT. + * + * The residency stat above reports the elementwise family as ONE line, which is what it was + * until `PN_TT_ELTWISE_CORES` made it a set; a split reported as one line cannot say whether + * a member count stopped paying because the cores stopped scaling or because ONE member is + * being starved. The awaits run in member order after every enqueue, so a balanced set puts + * nearly all the time on member 0 and a straggler is the member with time of its own. + * + * @param index 0 is `eltwise` itself; `false` means past the live members + * @return true and the three figures, or false. Any output pointer may be NULL. + */ +bool pn_tt_device_eltwise_member_stat(const pn_tt_device *device, unsigned index, + uint32_t *out_core, uint64_t *out_ops, + double *out_seconds); + +/** + * The SAME on-card descriptors, keyed by (family, function, TILE COUNT) — borrowed, never + * NULL for a live device. + * + * `pn_tt_device_residency_stat()` above replaced "247 us each" with a line per family; this + * replaces the line per family with a line per SHAPE, because the elementwise residency runs + * six functions over three widths and reports one mean of them all. It is also the only + * instrument that can price a family's per-descriptor FIXED cost from an ordinary run — + * see `pn_tt_descriptor_census_fit()`. Read `dropped` and `refused` beside every figure. + */ +const pn_tt_descriptor_census *pn_tt_device_census(const pn_tt_device *device); + +/** `dispatch` split into the host's MMIO ISSUE and the completion WAIT, with the descriptor + * count each wait covered. The spec reads 1.23 ms a matmul as a PCIe round trip; these two + * counters are what says how much of it is one. Any pointer may be NULL. */ +void pn_tt_device_dispatch_split(const pn_tt_device *device, double *out_issue_seconds, + uint64_t *out_issues, double *out_wait_seconds, + uint64_t *out_waits, uint64_t *out_wait_descriptors); + +/** The GDN family's two PHASES apart. One residency runs both, so its `ops` and `seconds` + * cannot separate the conv1d from the delta rule — and those are the two terms a Phase 3 + * re-pricing has to tell apart. `phase` is `FT_TT_OP_GDN_PHASE_CONV` or `..._DELTA`. + * CLEAN DESCRIPTORS ONLY — see `pn_tt_device_gdn_phase_recovery()`. */ +void pn_tt_device_gdn_phase_stats(const pn_tt_device *device, unsigned phase, + double *out_seconds, uint64_t *out_ops); + +/** The descriptors of that phase that REFUSED and were replayed after a re-arm, and the + * brackets they cost. Deliberately not in the figure above: a re-arm is a residency teardown + * and relaunch, and folding one into `seconds / ops` prices it as the op's arithmetic — which + * is how PHASE CONV once read 4,502 us a descriptor against 738.2. Anything quoting the clean + * figure prints `out_rearms` beside it. */ +void pn_tt_device_gdn_phase_recovery(const pn_tt_device *device, unsigned phase, + double *out_seconds, uint64_t *out_ops, + uint64_t *out_rearms); + +/** Bytes read back from the card and written to it, over this device's life. The read + * figure is the one that matters: the link is ~60x slower in that direction. */ +uint64_t pn_tt_device_bytes_read(const pn_tt_device *device); +uint64_t pn_tt_device_bytes_written(const pn_tt_device *device); + +/** + * How many matvecs went through the RESIDENT queue, how many fell back to one-shot, and + * how many times a residency had to be retired mid-run. + * + * EVIDENCE, like the window counters below and for the same reason: both dispatch paths + * produce identical numbers from identical kernels, so a run that silently fell back to + * one-shot is indistinguishable from a resident one by its output. Only this counter says + * which machine ran. A resident claim with `oneshot` non-zero is a claim about a path that + * did not carry the work. Any output pointer may be NULL. + */ +void pn_tt_device_dispatch_counts(const pn_tt_device *device, uint64_t *out_resident, + uint64_t *out_oneshot, uint64_t *out_restarts); + +/** + * Bytes each of the two TLB windows has actually carried, straight out of the NoC paths' + * own counters — the register window (UC, one 32-bit access per word) and the bulk DRAM + * window (WC, 16 B units). + * + * This exists as EVIDENCE, not telemetry. The failure mode of splitting the two accessors + * is a register access silently taking the bulk path, which would not surface until + * something hung; and the reverse — data quietly still on the register path — is the + * 27 s/token bug this split exists to fix, and it looks identical from the outside. These + * four counters distinguish all four cases. Any output pointer may be NULL. + * + * TRANSPORT-AGNOSTIC as of 2026-08-17. It used to read the tt-kmd binding directly, so on + * the userspace-driver transport it reported that ZERO bytes had moved through either + * window on a run that had just served a token on the card — an instrument that reads + * clean on the path it was never taught about is worse than none at all. + */ +void pn_tt_device_window_traffic(const pn_tt_device *device, uint64_t *out_register_read, + uint64_t *out_register_written, uint64_t *out_bulk_read, + uint64_t *out_bulk_written); + +/** + * How many times each window was RE-AIMED, and how many aims failed verification. + * + * Aiming is in the hot path — the NoC layer re-points a window whenever the target moves — + * and it is the one thing the two transports do differently at a per-operation rate: the + * tt-kmd path issues an ioctl, and the userspace driver writes the registers and then READS + * ALL FOUR BACK over an uncached PCIe link. So the aim count is what converts a per-aim cost + * difference into a per-token one, and it is the first number to look at when the two + * transports disagree on wall clock. + * + * `out_failures` is udrv-only and MUST be zero: a non-zero value means an aperture was + * refused because its registers did not read back as intended, not that a wrong address was + * used. Any output pointer may be NULL. + */ +void pn_tt_device_window_aims(const pn_tt_device *device, uint64_t *out_register_aims, + uint64_t *out_bulk_aims, uint64_t *out_failures); + +/** + * WHAT THOSE AIMS COST, and the READBACK's share of it. Zero on tt-kmd, where the aim is an + * ioctl and there is no readback to separate. + * + * The aim count on its own says how often a window moved; it cannot say what that is worth, + * and the only per-aim figure this program ever had (1.23 us) is the tt-kmd ioctl's, i.e. a + * different mechanism. On the udrv path an aim is four POSTED register stores plus four + * NON-POSTED readbacks, and on the dispatch path it happens ONCE PER DESCRIPTOR — so which + * half dominates decides whether the fix is fewer readbacks or more windows. + */ +void pn_tt_device_window_aim_seconds(const pn_tt_device *device, double *out_seconds, + double *out_verify_seconds); + +/** + * THE WINDOW BANK — how many windows each kind claimed, and whether the set is absorbing the + * endpoint rotation. + * + * The aim count alone cannot distinguish "the bank is working" from "the bank was never + * armed": both read as a number. `hits` and `misses` are the mechanism measured directly, and + * the two window counts are what the knob ACHIEVED rather than what it asked for — a knob that + * reads as armed and is not is this program's most expensive class of instrument defect. + * + * REPORTED PER BANK BECAUSE THE TWO STREAMS ARE DIFFERENT MECHANISMS. The WC bank's weight + * upload walks thousands of consecutive slices inside one 2 MiB window, so a pooled hit rate + * reads ~93% however badly the dispatch path is thrashing its 26 endpoints. + * + * `misaims` is FT_TT_UDRV_RED=misaim's own count, so a red arm that never fired reads as a + * zero instead of as a silence, and `audit_failures` must be zero on every arm but that one. + * All zero on tt-kmd, which has one window per kind and no bank. Any pointer may be NULL. + */ +void pn_tt_device_window_bank(const pn_tt_device *device, uint32_t *out_register_windows, + uint32_t *out_bulk_windows, uint64_t *out_register_hits, + uint64_t *out_register_misses, uint64_t *out_bulk_hits, + uint64_t *out_bulk_misses, uint64_t *out_misaims, + uint64_t *out_audit_failures); + +#endif /* USE_TT */ +/** MEASURE (`PN_TT_ARC_TELEMETRY=1`): log one line of ARC telemetry tagged `label`. */ +void pn_tt_device_telemetry_log(pn_tt_device *device, const char *label); + +/** STEP 3 GATE (`PN_TT_IDLE_CLOCK=1`): stage 0 snapshots every writer's idle-for-work + * clock; stage 1 prints card-busy per family and the host-attributable card idle. */ +void pn_tt_device_idle_clock(pn_tt_device *device, int stage, double tokens); + +#endif /* PN_TT_DEVICE_H */ diff --git a/src/accel/tt/pn_tt_device_internal.h b/src/accel/tt/pn_tt_device_internal.h new file mode 100644 index 0000000..c1af8d7 --- /dev/null +++ b/src/accel/tt/pn_tt_device_internal.h @@ -0,0 +1,948 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_device_internal.h + * @brief The layout of `struct pn_tt_device`, shared by the two source files that own it. + * + * pn_tt_device.c owns the card, the artifact residency and the matmul; pn_tt_oncard.c + * owns the reduce/elementwise/sampling residencies and the DRAM activation buffers + * they pass to each other. They are one object and two files because they are two + * subjects, and the struct is here rather than in the public header so a caller + * still cannot reach into it. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TT_DEVICE_INTERNAL_H +#define PN_TT_DEVICE_INTERNAL_H + +/** Mask tiles for verify attention in ROWS mode: up to 4 x (32 / q_rows) rows. */ +#define PN_TT_ATTN_ROWS_CHUNKS 4u + +#ifdef USE_TT + +#include + +#include "pn_tt_descriptor_census.h" +#include "pn_tt_device.h" +#include "pn_tt_eltwise_split.h" +#include "pn_tt_fp32_strip.h" +#include "pn_tt_gdn.h" +#include "pn_tt_matmul_group.h" +#include "pn_tt_residency.h" + +#include "tt/ft_tt_buffer.h" +#include "tt/ft_tt_fttw.h" +#include "tt/ft_tt_op_memory.h" +#include "tt/ft_tt_op_vector.h" +#include "tt/ft_tt_queue.h" +#include "tt/ft_tt_runtime.h" +#include "pn_tt_stage_pipeline.h" + +/** Staging slots for PHASE DELTA — one per 16-row sub-chunk of a 32-row pass. */ +#define PN_TT_GDNC_STAGE_SLOTS 2u + +/** Most cores the prefill attention chunk mixer may split its Q heads across. */ +#define PN_TT_ATTN_MAX_MEMBERS 8u + +/** Most cores one per-head norm width may split its heads across. */ +#define PN_TT_HEAD_NORM_MAX_MEMBERS 8u + +/** Bounds every wait. There is no untimed path to the card (spec §7.3). */ +#define PN_TT_OP_TIMEOUT_MS 60000u + +#define PN_TT_TILE_DIM FT_TT_OP_TILE_DIM +#define PN_TT_TILE_BF16 FT_TT_OP_TILE_BYTES_BF16 +#define PN_TT_TILE_FP32 FT_TT_OP_TILE_BYTES_FP32 + +/** THE CEILING IS THE DST SECTION, NOT L1. A band only pays if `MB` grows with it, and + * `MB * NB <= 8` (resident_matmul_block_protocol.h) makes MB=2/NB=4 the only point on the + * lattice that beats the shipped MB=1/NB=8 — so a third band buys nothing the second did not + * and costs another pass of both chunk mixers. 4 is slack for the arm that proves that. */ +#define PN_TT_STRIP_MAX_TILE_ROWS 64u + +/** How many distinct elementwise tile counts ONE arming may be asked to serve. The forward + * asks for two (n_embd and n_ff) and the slack is so a third width needs no header change. + * The shapes are kept rather than consumed because a residency is BOUNDED — an idle kernel + * retires itself — and re-arming one needs the shapes it was armed with. */ +#define PN_TT_ONCARD_MAX_ELTWISE_SHAPES 8u + +/** How many displaced KV sequence keys are remembered, i.e. how deep an interleave this + * detects. A key that comes back after being displaced is two sequences alternating on ONE + * region set; beyond this depth the detector goes blind, which is why it is a stated bound + * and not a guarantee. */ +#define PN_TT_KV_DISPLACED_KEYS 8u + +/** One resident weight plus the name it answers to. */ +typedef struct { + char name[FT_TT_ARTIFACT_NAME_BYTES]; + pn_tt_weight weight; +} pn_tt_resident; + +/** One fp32 buffer of the GDN family in card DRAM. A bank and an offset, which is everything + * `ft_tt_op_operand` needs — the tile size is the family's, not the buffer's, because every + * circular buffer of the `gdn` program is Float32. */ +/** PM 7 ITEM 5: most GDN GATE members one device arms. */ +#define PN_TT_GDNGATE_MAX_MEMBERS 8u + +typedef struct pn_tt_gdn_buffer { + uint32_t bank; + uint32_t address; +} pn_tt_gdn_buffer; + +/** GDN residencies one head-split may hold, member 0 included. + * + * EIGHT because that is where the measurement stops, not where the card does: the core + * scaling probe reached 7.12x on 8 and the next doubling was never taken. Raising it is a + * measurement, not an edit — every extra member is a Tensix `pn_tt_prepare_family_on_core()` + * has to be able to refuse against the matmul group by name. */ +/** L3: most top-k split members, member 0 included. */ +#define PN_TT_TOPK_MAX_MEMBERS 8u +#define PN_TT_GDN_MAX_MEMBERS 16u + +/** CHUNKWISE residencies one core split may hold, member 0 included. + * + * SIXTEEN rather than the decode family's eight because the two phases divide by different + * quantities and the smaller one still admits it: PHASE CONV has **256 tile columns** at + * Ornith's conv_dim of 8192 and PHASE DELTA has **32 v-heads**, so 16 members own 16 columns + * or 2 heads each and both divide exactly. It is a ceiling on the LADDER, not a claim about + * the knee — the decode family's knee was 4 with 8 reproducibly behind it. Every extra member + * is a Tensix `pn_tt_prepare_family_on_core()` has to be able to refuse by name. */ +#define PN_TT_GDNC_MAX_MEMBERS 16u + +/** One SSM layer's GDN buffers. `weight` is a constant staged once; the other two are the + * carried state, READ AND WRITTEN by their descriptors and never downloaded. */ +typedef struct pn_tt_gdn_region { + pn_tt_gdn_buffer weight; /**< taps TAP-MAJOR planes, transposed at load */ + pn_tt_gdn_buffer window; /**< taps-1 planes, the same layout, rolled on the card */ + pn_tt_gdn_buffer state; /**< [v_heads, S, S] fp32, oracle-transposed */ +} pn_tt_gdn_region; + +/** One layer's K and V regions in card DRAM. Identical geometry and two separate bases — + * contract F1 and N4 (two regions rather than one with a fixed offset, so each is a plain + * ft_tt_op_operand). Internal because no public call takes or returns one. */ +typedef struct pn_tt_kv_region { + uint32_t k_bank, k_address; + uint32_t v_bank, v_address; +} pn_tt_kv_region; + +/** Distinct HEAD widths one model may need per-head reduces at. + * + * TWO, because Ornith needs exactly two and they are not the same number: its + * Gated-Delta-Net per-head norms are over S = 128 (Wt 4) and its attention QK-RMSNorms are + * over head_dim = 256 (Wt 8). A reduce residency is PLACED for one Wt and refuses any + * other, so these cannot share a core — see pn_tt_head_norm.h. Raising this is one more + * Tensix, which the card has; it is bounded so the collision refusal has a fixed set to + * check. */ +#define PN_TT_HEAD_NORM_MAX_WIDTHS 2u + +/** One per-head reduce residency, the ONE width it was placed for, and the ONE page size — + * which is also which reduce PROGRAM it runs, since the two are the same choice. */ +typedef struct pn_tt_head_norm_residency { + pn_tt_residency residency; + uint32_t tile_cols; /**< width/32; 0 for a slot that never armed */ + uint32_t tile_bytes; /**< PN_TT_TILE_FP32 (`reduce_fp32`) or PN_TT_TILE_BF16 (`reduce`) */ + /** THE HEAD SPLIT (`PN_TT_HEAD_NORM_CORES`): members 1..N-1 run the same program and take + * disjoint HEAD ranges. Heads are independent row blocks and the wire carries no absolute + * block index, so a member is the same descriptor at an offset base — host-only. */ + pn_tt_residency member[PN_TT_HEAD_NORM_MAX_MEMBERS - 1u]; + uint32_t members; /**< 1 = no split */ +} pn_tt_head_norm_residency; + +/** The two directions of the activation strip bridge, in the order `render[]` holds them. */ +enum { PN_TT_RENDER_WIDEN = 0, PN_TT_RENDER_NARROW = 1, PN_TT_RENDER_DIRECTIONS = 2 }; + +/** Residencies ONE DIRECTION of the bridge may hold, member 0 included. + * + * EIGHT and not sixteen, because the two directions are split independently and both sets + * have to fit above every core any other family can reach: at eight apiece they occupy + * 122..128 and 129..135 of the 122..139 the elementwise split's map leaves free. Eight also + * divides both of Ornith's render widths (256 widen, 128 narrow) and leaves the narrow slice + * at 16 tiles, still two blocks of depth 8. Raising it is more Tensix and a re-check of that + * map, not an edit here. */ +#define PN_TT_RENDER_SPLIT_MAX_MEMBERS 8u + +/** One direction of the bridge: a residency, the two page sizes it converts between, and the + * ZERO operand that turns `kVecFnAdd` into a copy. See pn_tt_render.h. */ +typedef struct pn_tt_render_residency { + pn_tt_residency residency; + /** Members 1..N-1 of the split; member 0 is `residency` above. See pn_tt_render.h. */ + pn_tt_residency member[PN_TT_RENDER_SPLIT_MAX_MEMBERS - 1u]; + uint32_t members; /**< live residencies, member 0 included; 1 when unsplit */ + /** Per-member AWAIT seconds. Kept apart from the census, which keys on the WHOLE + * descriptor's width so its `fixed + per_tile * tiles` fit stays comparable across a + * member ladder — what that cannot show is the SHAPE of the set, and "it stopped at N" + * is exactly the finding that needs it. */ + double member_seconds[PN_TT_RENDER_SPLIT_MAX_MEMBERS]; + uint32_t in_tile_bytes; + uint32_t out_tile_bytes; + uint32_t tiles; /**< the widest strip placed for; 0 when this direction never armed */ + uint32_t depth; /**< the ONE block depth every descriptor of it carries */ + uint32_t zero_bank; + uint32_t zero_address; /**< `tiles` input-format tiles, allocated zeroed, never written */ +} pn_tt_render_residency; + +/** THE MERGED GDN OUTPUT GATE — `silu(bf16 z) * fp32 y_norm -> bf16 gated` under a MIXED CB + * table, which is what lets `render_narrow` leave the decode token. No zero operand: this one + * is a real binary op, not a copy dressed as one. See pn_tt_gate.h. */ +typedef struct pn_tt_gate_residency { + pn_tt_residency residency; + uint32_t tiles; /**< the widest gate placed for; 0 when it never armed */ + uint32_t depth; /**< the ONE block depth every descriptor of it carries */ +} pn_tt_gate_residency; + +struct pn_tt_device { + ft_tt_runtime runtime; + ft_tt_runtime_images firmware; + ft_tt_runtime_images program_bfp8; + ft_tt_runtime_images program_bfp4; + bool have_bfp4; + + /* The resident triad: the same three kernels, built to not return between ops. */ + ft_tt_runtime_images program_resident; + bool have_resident; /**< the images loaded and the firmware matches */ + bool resident_requested; /**< PN_TT_DISPATCH did not say `oneshot` */ + bool resident_running; /**< a residency is live on the core RIGHT NOW */ + /** Member 0's work queue. It lives here rather than in the group because the device + * still owns exactly one residency's lifetime; the group holds the rest. */ + ft_tt_queue queue; + /** The cores one projection is split across. One member is the pre-group path. */ + pn_tt_matmul_group group; + pn_tt_red_split red_split; /**< RED PROOF: break member 1 in one named way */ + uint32_t poll_backoff_us; /**< sleep between completion polls; 0 = tight loop */ + uint32_t resident_tile_bytes; /**< the ONE weight tile size the residency serves */ + uint32_t resident_depth; /**< the ONE prefetch depth every descriptor carries */ + uint32_t artifact_tile_bytes; /**< the artifact's single matmul tile size, 0 if mixed */ + uint32_t artifact_kt_gcd; /**< every matmul Kt divides by this */ + uint64_t red_bad_descriptor; /**< RED PROOF: malform this matvec's parameters, 0 = never */ + /** RED PROOF `PN_TT_RED=fast_check`: ask the one-line completion check about one + * descriptor further on than the poll waited for. It must REFUSE; green would mean the + * fast path's check is inert and the whole member-count ladder measures nothing. */ + bool red_fast_check; + /** RED PROOF `PN_TT_RED=param_cache_hash`: collapse the cache's hash to a constant. The + * memcmp still decides, so this arm must be `cmp` IDENTICAL — which is the claim that + * the hash is a bucket index and nothing more. */ + bool red_param_cache_hash; + /** RED PROOF `PN_TT_RED=param_cache_blind`: take the first bucket occupant WITHOUT the + * memcmp. Paired with the constant hash every projection reuses block 0, so this arm + * must produce a `cmp` that DIFFERS. Green would mean the words never varied. */ + bool red_param_cache_blind; + uint64_t resident_ops; /**< matvecs executed through the queue */ + uint64_t resident_restarts; /**< times the residency had to be re-armed */ + + uint16_t core_x, core_y; + uint32_t core_index; + + ft_tt_allocator allocator; + ft_tt_noc_coord dram[FT_TT_MAX_DRAM_BANKS]; + uint32_t dram_bank_count; + + ft_tt_fttw artifact; + bool artifact_open; + /** STEP 4 S2: a second artifact (the MTP draft head) whose weights are placed on demand. */ + ft_tt_fttw extra_artifact; + bool extra_open; + pn_tt_resident *resident; + uint32_t resident_count; + uint64_t resident_bytes; + + /* ------------------------------------------------------------ + * The ON-CARD residencies — one core per op family + * ------------------------------------------------------------ + * A resident binary is linked at a fixed slab, so one program per core. The forward + * needs three families in sequence per layer, so it gets three cores and they pass + * activations through DRAM instead of through the host. + */ + pn_tt_residency reduce; /**< RMS norm (the reduce family) */ + /** L4: the second RMS-norm member — each owns half of every row's final chain and + * output (kRedFinal byte 3); both reduce the whole row. `PN_TT_REDUCE_CORES=2`. */ + pn_tt_residency reduce_member; + /** MEASURE ONLY (2026-10-05, PHASE G pre-work): `PN_TT_IDLE_FILL=N` arms N extra reduce + * residencies on cores 0..N-1 that never receive work — the price of RESIDENT-BUT-IDLE + * cores, which a full-grid prefill would add. */ + pn_tt_residency idle_fill[64]; + uint32_t idle_fill_count; + uint32_t reduce_members; + pn_tt_residency eltwise; /**< SwiGLU gate and residual add */ + pn_tt_residency sample; /**< exact top-N over the logit row */ + pn_tt_residency rope; /**< NeoX rotary, on q and k */ + pn_tt_residency memory; /**< the KV append (no compute thread: DM0|DM1 only) */ + /** STEP 3 (A): the on-card puller (BRISC only, `PN_TT_PULLER_CORE`, default 89). */ + pn_tt_residency puller; + pn_tt_residency attention; /**< flash decode, one descriptor per KV group */ + /** THE CHUNK MIXER'S HEAD SPLIT (`PN_TT_ATTN_CORES`). Members 1..N-1 run the SAME program + * on their own cores and take disjoint Q-head ranges of a prefill chunk; decode never + * uses them. Every word of an attention descriptor is a DRAM address with the head folded + * in by the host, so the split needs no kernel change. */ + pn_tt_residency attention_member[PN_TT_ATTN_MAX_MEMBERS - 1u]; + uint32_t attention_members; /**< 1 = no split */ + double attention_member_seconds[PN_TT_ATTN_MAX_MEMBERS]; + pn_tt_residency gdn; /**< the Gated-Delta-Net block, two descriptors per SSM layer */ + /** THE CHUNKWISE Gated-Delta-Net block — the same two phases over C <= 32 POSITIONS in one + * descriptor, for prefill. It carries NO STATE OF ITS OWN: the conv window and the + * recurrent state it advances are `gdn_region[]`'s, at their addresses, in their layout, + * which is what makes the prefill->decode seam free. See pn_tt_gdnc.h. */ + pn_tt_residency gdnc; + /** EXTRA CHUNKWISE residencies, behind `PN_TT_GDNC_CORES`. The work divides the same two + * ways the decode family's does and the WIRE HAS CARRIED THE SLICE SINCE THIS FAMILY + * SHIPPED (`ft_tt_op_gdnc_request.first_col/.col_count`, `kGdncColSlice`): PHASE CONV + * splits TILE COLUMNS — 256 of them at Ornith's conv_dim of 8192, and no column reads + * another column's operands or writes another column's tile — and PHASE DELTA splits + * V-HEADS, exactly as `gdn_member[]` does. Every member carries the IDENTICAL + * circular-buffer placement, because `resolve_cb_pages()` is sized from `state_tiles` and + * `conv_taps` and from neither slice. `gdnc` itself is member 0, so this array holds one + * fewer. See pn_tt_gdnc.c's PN_TT_GDNC_CORES block. */ + pn_tt_residency gdnc_member[PN_TT_GDNC_MAX_MEMBERS - 1u]; + uint32_t gdnc_members; /**< 1 when unsplit — `gdnc` alone */ + /** EXTRA GDN residencies for the PHASE DELTA head split, ON BY DEFAULT AT 4 behind + * `PN_TT_GDN_HEAD_CORES`. PHASE DELTA divides EXACTLY by v-head — measured + * `59.8 us + N x 103.4` — so N members each owning `v_heads/N` heads is the whole + * lever. **It parallelises 1.83 / 2.70 / 2.65x on 2 / 4 / 8, not the 1.96 / 3.79 / + * 7.12x the core-scaling probe reported**: that probe gave every core a DIFFERENT + * layer's state, so no two descriptors shared DRAM, where a real split shares the + * staged q|k|v and the gate block and scatters into the same four `y` tiles. Every + * member carries the IDENTICAL circular-buffer placement, because nothing this family + * places is sized from `v_heads`. `gdn` itself is member 0, so this array holds one + * fewer. */ + pn_tt_residency gdn_member[PN_TT_GDN_MAX_MEMBERS - 1u]; + uint32_t gdn_members; /**< 1 when unsplit — `gdn` alone */ + /** EXTRA ELTWISE residencies, behind `PN_TT_ELTWISE_CORES`. Every descriptor of the + * family is split into `eltwise_members` contiguous, DISJOINT tile ranges — no shared + * buffer, no shared DRAM byte, no reduction — and the set is enqueued before any member + * is awaited. `eltwise` itself is member 0, so this array holds one fewer. See + * pn_tt_eltwise_group.h for the core map and why 106 upward cannot collide. */ + pn_tt_residency eltwise_member[PN_TT_ELTWISE_SPLIT_MAX_MEMBERS - 1u]; + uint32_t eltwise_members; /**< 1 when unsplit — `eltwise` alone; 0 before it arms */ + /** PHASE W1a: how many of `eltwise_members` serve EVERY descriptor (the decode split). + * Members past it are WIDE members (`PN_TT_ELTWISE_WIDE_CORES`), which only descriptors + * of `PN_TT_ELTWISE_WIDE_MIN_TILES` or more use — the prefill SwiGLU. */ + uint32_t eltwise_decode_members; + /** PM 7: the NEXT eltwise descriptor is enqueued and NOT awaited + * (`pn_tt_eltwise_group_defer_next`); it stays pending until + * `pn_tt_eltwise_group_settle()` (or the next eltwise descriptor) awaits it. Streamed, + * that moves its puller AWAIT from straight after it to where a consumer needs it. */ + bool eltwise_defer_next; + uint32_t eltwise_pending_members; /**< members of the pending descriptor, 0 = none */ + uint32_t eltwise_pending_words[PN_TT_ELTWISE_SPLIT_MAX_MEMBERS][FT_TT_OP_VECTOR_WORDS]; + ft_tt_op_eltwise_request eltwise_pending_request; + double eltwise_pending_started; + uint64_t eltwise_deferred; /**< descriptors deferred over the process */ + /** PM 7 ITEM 5: the GDN GATE family (pn_tt_gdngate.h) — members, the per-layer constants + * table, the per-layer gate-block arrays, and the last issue (for a refused member's + * replay). 0 members = not armed: the host gate path stays. */ + pn_tt_residency gdngate[PN_TT_GDNGATE_MAX_MEMBERS]; + uint32_t gdngate_members, gdngate_layers, gdngate_heads, gdngate_max_bands; + uint32_t gdngate_scale_bits, gdngate_block_stride; + pn_tt_gdn_buffer gdngate_constants, gdngate_blocks; + uint32_t gdngate_pending[PN_TT_GDNGATE_MAX_MEMBERS]; + uint32_t gdngate_next_member; + uint32_t gdngate_cores[PN_TT_GDNGATE_MAX_MEMBERS]; + bool gdngate_failed; + uint64_t gdngate_issues; + uint64_t gdngate_flats; /**< ITEM 6: decode/verify FLAT gate descriptors */ + uint64_t gdngate_layer_issues, gdngate_settles; + double gdngate_settle_seconds; + struct { + uint32_t layer, rows, sub_rows; + pn_tt_activation alpha, beta; + } gdngate_last; + /** PHASE W: the reader thread (pn_tt_reader.h), started on first use; `reader_failed` + * once it could not be (no reader window bank), so nothing retries per call. */ + struct pn_tt_reader *reader; + bool reader_failed; + /** Each member's own AWAIT time, for the report that diagnoses the knee. The awaits run + * in member order after every enqueue, so a balanced set puts nearly all of it on member + * 0 and a STRAGGLER is the one member with time of its own. */ + double eltwise_member_seconds[PN_TT_ELTWISE_SPLIT_MAX_MEMBERS]; + /** EXTRA reduce residencies, one per distinct HEAD width, placed for that width rather + * than the model's. The GDN's per-head norms are Wt = S/32 = 4 and attention's + * QK-RMSNorms are Wt = head_dim/32 = 8, against the model's 128; a reduce residency is + * placed for one Wt — see pn_tt_head_norm.h for why they must not share a core rather + * than merely need not. */ + pn_tt_head_norm_residency head_norm[PN_TT_HEAD_NORM_MAX_WIDTHS]; + uint32_t head_norm_count; /**< live slots, in the order they armed */ + /** THE ACTIVATION STRIP BRIDGE — one residency per DIRECTION, `pn_tt_render.h`. They are + * two programs and not one because a writer is built for exactly one output page size, + * so a single triad cannot both widen and narrow. */ + pn_tt_render_residency render[PN_TT_RENDER_DIRECTIONS]; + /** THE MERGED GATE that deletes the narrow half of that bridge, `pn_tt_gate.h`. Default + * off; when it is up, `render[PN_TT_RENDER_NARROW]` issues no decode descriptors. */ + pn_tt_gate_residency gate; + bool oncard[PN_TT_ONCARD_COUNT]; + bool oncard_requested[PN_TT_ONCARD_COUNT]; + /** What `PN_TT_ONCARD` unset — or `all` — selects, PER MODEL FAMILY. It is not a + * constant because on-card attention is a different forward on each family and the + * evidence for it is per family: it is measured good on the qwen35/bonsai hybrid + * (chat parity 0.986080, +0.4% perplexity, faster at every length) and measured to FAIL + * the same gate on MiniCPM5-1B through `llama_forward_tt.c`. One global default would + * have shipped the second as a side effect of shipping the first. + * pn_tt_device_oncard_default() sets it; pn_tt_device_open() seeds it. */ + bool oncard_default[PN_TT_ONCARD_COUNT]; + /** Requested because the OPERATOR NAMED IT, as opposed to because it is the default. + * The two want opposite failure behaviour and `oncard_requested` cannot tell them + * apart — see pn_tt_device_oncard_named(). */ + bool oncard_named[PN_TT_ONCARD_COUNT]; + uint32_t reduce_tile_cols; /**< the ONE Wt every reduce descriptor carries */ + uint32_t eltwise_depth; /**< the ONE block depth every eltwise descriptor carries */ + /** The caller's promise that every strip holds ONE live row (row 0) right now — a + * single-token forward. pn_tt_device_set_single_row(). */ + bool single_row; + /** S2c: a verify's live rows are rows 0..verify_rows-1 (0 = no promise). <= 16: the + * half-tile levers (matmul M5 faces 0-1, reduce M3) hold; <= 4: eltwise E1 too (its SFPU + * row-0 body covers rows 0-3). */ + uint32_t verify_rows; + uint32_t topk_row; /**< S2c: the logits row the next top-k selects over (0 = decode) */ + /** The matmul K-block depth THIS MODEL wants when `PN_TT_PREFETCH_TILES` is unset + * (0 = the global default). pn_tt_device_prefer_prefetch_tiles(). */ + uint32_t prefetch_tiles_preferred; + uint32_t sample_tile_count; /**< the vocabulary row's tile count */ + uint32_t sample_depth; + uint32_t sample_result_address; /**< L1 on the sampling core */ + uint32_t sample_state_address; /**< L1 on the sampling core */ + /** L3 TOP-K SPLIT (`PN_TT_TOPK_CORES`): members 1..n-1 each select over a contiguous + * vocabulary slice; the host merges by (key desc, index asc), the kernel's own order. */ + pn_tt_residency sample_member[PN_TT_TOPK_MAX_MEMBERS - 1u]; + uint32_t sample_members; + uint32_t sample_member_result[PN_TT_TOPK_MAX_MEMBERS - 1u]; + uint32_t sample_member_state[PN_TT_TOPK_MAX_MEMBERS - 1u]; + /* The shapes each family was armed with, kept so a retired residency can be re-armed + * from them. reduce_tile_cols and sample_tile_count above serve the same purpose for + * their families; the elementwise counts have nowhere else to live. */ + uint32_t oncard_eltwise_tiles[PN_TT_ONCARD_MAX_ELTWISE_SHAPES]; + uint32_t oncard_eltwise_count; + uint64_t oncard_restarts; /**< times an on-card residency had to be re-armed */ + uint32_t strip_bank_cursor; /**< round-robins DRAM activation strips across banks */ + /** `PN_TT_STRIP_TILE_ROWS` — bands of 32 rows every activation strip is allocated with, + * resolved ONCE (0 means "not read yet") so a strip allocated late cannot disagree with + * one allocated early. 1 is the shipped height and the pre-Mt allocation byte for byte. */ + uint32_t strip_tile_rows; + /** The height used when `PN_TT_STRIP_TILE_ROWS` is unset; 0 = 1. Set by a MODEL whose + * batched prefill can run more than one band (pn_tt_device_prefer_strip_tile_rows()). */ + uint32_t strip_tile_rows_preferred; + /** PHASE G option 3: the height of the PREFILL-ONLY strip set (`PN_TT_PREFILL_TILE_ROWS`), + * 0 when prefill shares the decode strips. Decode strips stay `strip_tile_rows`. */ + uint32_t prefill_tile_rows; + /** `PN_TT_ACTIVATION_REPLICAS` — how many DRAM banks the HOT activation strips are + * mirrored into, so N matmul members do not all read one ~59 GB/s bank. 1 is off and + * is the pre-replication forward byte for byte. See pn_tt_replica_map.h. */ + uint32_t activation_replicas; + /** Round-robins WEIGHTS across banks, one step per tensor. It lives on the device rather + * than in `pn_tt_device_load_artifact()` because a weight re-staged after load — a + * de-interleaved half of a joint projection — must continue the same rotation rather + * than restart it and pile every restaged tensor onto one bank. Starts at 2. */ + uint32_t weight_bank_cursor; + + /* ------------------------------------------------------------ + * The attention triple — RoPE, KV append, flash decode + * ------------------------------------------------------------ + * The shapes are kept, not consumed: every one of the three residencies is BOUNDED and + * retires on idle, so re-arming one needs what it was armed with. + */ + ft_tt_kv_cache_layout kv_layout; /**< K and V share it (contract F1) */ + pn_tt_kv_region *kv_region; /**< one per layer */ + uint32_t kv_layer_count; + uint32_t attn_q_heads, attn_kv_heads, attn_head_dim, attn_rope_dim; + uint32_t attn_q_rows; /**< Q heads per KV group = q_heads / kv_heads */ + uint32_t attn_dim_tiles; /**< head_dim / 32 */ + uint32_t attn_block_tiles; /**< the ONE prefetch depth the residency was placed for */ + uint32_t attn_embed_tiles; /**< the memory family's OTHER group size, required non-zero */ + double attn_rope_theta; + /** The constants operand: the reduce scaler, 1/sqrt(head_dim) and the 32 tail masks. + * Built once per model. */ + uint32_t attn_const_bank, attn_const_address; + /** ONE row block of the rotary table, rebuilt per token so its ROW 0 carries this + * token's position — which is what lets the whole forward stay at tile row 0 instead of + * carrying the position's row phase through every op (pn_tt_attention.c). */ + uint32_t rope_table_bank, rope_table_address; + /** A SECOND chunk-only rotary table, so two bands of a pass (first positions p and p+32) + * each keep their block cached instead of evicting each other. Odd chunk indices use it; + * decode never does. */ + uint32_t rope_chunk1_bank, rope_chunk1_address; + uint32_t rope_chunk1_first; /**< first position it holds; UINT32_MAX = none */ + /** PHASE G: a whole-prompt pass rotates up to 64 chunks per attention layer, and two slots + * evicted each other 51 times a layer (+0.14 s). Chunks 2.. get a block each, built once + * per prefill and keyed by first position. Allocated lazily. */ + uint32_t rope_many_bank[64], rope_many_address[64], rope_many_first[64]; + /** S2c: the verify's rotary block (rows 0..k-1 = positions first..), decode-formula. */ + uint32_t rope_rows_bank, rope_rows_address; + uint32_t rope_rows_key_first, rope_rows_key_rows; /**< the block it holds (rows 0 = none) */ + /** S2f: the attention ROWS-mode staircase mask tile (on the constants' bank) and the + * host copy of the constants it is assembled from. */ + uint32_t attn_rows_mask_address; + /** (first % 32) | rows << 8 | 1 << 16 when built, one a CHUNK: a verify wider than + * 32 / q_rows rows runs as several ROWS descriptors, each with its own mask tile. */ + uint32_t attn_rows_mask_key[PN_TT_ATTN_ROWS_CHUNKS]; + unsigned char *attn_constants_host; + uint32_t rope_table_position; /**< the position row 0 currently holds; UINT32_MAX = none */ + /** True while the rotary block holds a CHUNK's 32 rows rather than a decode token's row + * 0 plus 31 zeros. Decode's next rebuild has to re-zero before it writes row 0, or the + * stated rows-1..31-are-zero invariant is only true by accident (of the ACTIVATION's + * padding, not the table's). */ + bool rope_table_is_chunk; + uint32_t rope_table_chunk_first; /**< first position the chunk block holds, when is_chunk */ + + /* ------------------------------------------------------------ + * The GATED-DELTA-NET family — one core, and the recurrence's state + * ------------------------------------------------------------ + * The per-layer regions are the only DRAM in this backend that is READ AND WRITTEN by the + * card and never downloaded. The five staging buffers below them are SHARED across every + * layer, because a token runs one layer at a time and nothing in them carries. + */ + pn_tt_gdn_shape gdn_shape; + pn_tt_gdn_region *gdn_region; /**< one per SSM layer */ + /** STEP 4 S2: snapshot slots of every layer's window + state for the speculative verify's + * rollback, `gdn_snapshot[slot * layers + layer]`, allocated on first use. */ + pn_tt_gdn_region *gdn_snapshot; + uint32_t gdn_snapshot_slots; + /** Multi-turn: every layer's window + state copied at a prefill pass boundary + * (pn_tt_gdn_checkpoint.c), one region a layer, allocated on the first capture. */ + pn_tt_gdn_region *gdn_checkpoint; + uint32_t gdn_state_out_plus1; /**< 0 = in place; else slot + 1 takes the state writes */ + uint32_t gdn_row; /**< S2c: the CONV/DELTA row select, 0 = decode */ + /** S2c: deferred awaits — split CONV/DELTA descriptors are enqueued and awaited together + * by pn_tt_gdn_await_deferred(); one gate block a row so queued DELTAs never share one. */ + bool gdn_defer; + uint32_t gdn_pending_members; + uint64_t keepalive_nops; /**< PM 5: NOPs the keepalive has sent */ + uint32_t gdn_pending_depth; /**< deferred descriptors a member holds since its last await */ + uint64_t gdn_defer_drains; /**< full-ring drains the deferred rows took (flow control) */ + pn_tt_gdn_buffer gdn_gate_row[16]; + uint32_t gdn_gate_rows, gdn_gate_stride; + pn_tt_gdn_buffer gdn_stream_in; /**< CONV: the qkv projection, a row-0 strip */ + /** CONV's OUTPUT **and** DELTA's INPUT — ONE buffer, because they are one geometry. Both + * are `conv_dim/32` row-0 tiles and `conv_out`'s q, k and v slices sit exactly where + * `ftg_delta_stream_base()` looks for them, so the delta rule reads the convolution's + * answer in place. All the per-head L2 norm has to do is write its answer back over the + * first `2*k_heads*state_tiles` tiles of it. */ + pn_tt_gdn_buffer gdn_stream_out; + pn_tt_gdn_buffer gdn_gate; /**< DELTA: decay[H] then beta[H], flat fp32 */ + pn_tt_gdn_buffer gdn_y; /**< DELTA: y, a row-0 strip, head-major */ + unsigned char *gdn_stage; /**< host tile staging; grown on demand */ + size_t gdn_stage_bytes; + /** `PN_TT_GDN_SELFCHECK`: read the staged qkv row and conv weight back out of DRAM once + * and compare, so a wrong GDN answer can be attributed to the host layout or to the + * card rather than to both. `gdn_weight_oracle` is layer 0's `ssm_conv1d`, borrowed. */ + bool gdn_self_checked; + bool gdn_out_checked; + /** `PN_TT_GDN_SELFCHECK`: DELTA descriptors scored against the closed form so far. It is + * a COUNT and not a flag because position 0 is the one place the carried state is + * provably zero — so every SSM layer of the FIRST token has a closed form, and one layer + * cannot say whether an error is the op's or that layer's data. */ + uint32_t gdn_delta_checks; + /** A HOST SHADOW of SSM layer 0's rolling conv window, plus the tokens scored against it. + * Position 0 is the ONE position where the window is trivially zero, so a check that + * stops there says nothing about the ROLL — and the roll is the only part of PHASE CONV + * that carries state across tokens. */ + float *gdn_window_shadow; + uint32_t gdn_window_checks; + /** Has the injective-stream map already run? `PN_TT_GDN_RAMP` poisons the layer it probes, + * so it runs once. */ + bool gdn_ramp_probed; + /** Has the CONV data arm echoed the stream it is about to convolve? Once a run — an arm + * that silently did not fire reads exactly like an arm that fired and cost nothing, and + * those two are the whole question the arm exists to separate. */ + bool gdn_synthetic_reported; + /** Whole-tile strip checks taken on the SHIPPED on-card route, capped so three layers + * over four tokens is a probe and not a log. */ + uint32_t gdn_strip_checks; + /** Has `PN_TT_GDN_CORES`' multi-core sweep already run? It borrows the carried state of + * the layers it uses, so it runs ONCE per device and at position 0. */ + bool gdn_probe_done; + /** PER-PHASE on-card accounting, indexed by `FT_TT_OP_GDN_PHASE_*` (1 and 2; slot 0 is + * never written). `oncard_seconds` is a SUM over every family, so the plan's "+0.110 s + * over 48 GDN descriptors" was a SUBTRACTION between two runs — and a subtraction cannot + * tell CONV from DELTA, which do different work and are the two things a re-pricing has + * to separate. These are measured directly, in one run. */ + double gdn_phase_seconds[3]; + uint64_t gdn_phase_ops[3]; + /** THE SAME PHASES, FOR DESCRIPTORS THAT REFUSED AND WERE REPLAYED. The submit stopwatch + * spans the refusal, the re-arm — a residency teardown and relaunch — and the resubmit, + * so a refusing run prices a teardown as the op's arithmetic. That is exactly how PHASE + * CONV once read **4,502 us a descriptor against 738.2**, and it is the shape the + * unsourced "CONV is DATA-DEPENDENT, 4,571 us on a degenerate stream" also has. Kept + * apart so `gdn_phase_seconds / gdn_phase_ops` is the CLEAN figure and a reader that + * quotes it prints `gdn_phase_rearms` beside it. */ + double gdn_phase_recovered_seconds[3]; + uint64_t gdn_phase_recovered_ops[3]; + uint64_t gdn_phase_rearms[3]; + /** Every SSM layer's qkv, as it was uploaded, so a wrong tensor coming back out of the + * card can be NAMED rather than described. `layer_count * conv_dim` floats. */ + float *gdn_qkv_history; + const float *gdn_weight_oracle; + + /* ------------------------------------------------------------ + * The CHUNKWISE family's own operands — see pn_tt_gdnc.h + * ------------------------------------------------------------ + * Three buffers, and every one of them is a CONSTANT or a per-chunk constant. The chunk's + * activations are the gdn family's staging strips and its carried state is the gdn + * family's regions, so nothing here is read back and nothing here carries. + */ + pn_tt_gdn_buffer *gdnc_constant; /**< one per SSM layer: ROW-REPLICATED tap planes, + * then the 2*taps band tiles at the end of them */ + /** PM 8: copies of each layer's conv constant in OTHER DRAM banks, so the CONV members + * do not all read one bank (`PN_TT_GDNC_CONST_REPLICAS`, 1 = none). Member m reads copy + * `m % replicas` (copy 0 = `gdnc_constant`). `layer_count * (replicas - 1)` entries. */ + pn_tt_gdn_buffer *gdnc_constant_replica; + uint32_t gdnc_constant_replicas; + uint32_t gdnc_conv_layer; /**< the layer of the CONV descriptor being submitted */ + /** THE DELTA STAGING, IN TWO SLOTS — one per sub-chunk index (`first_row / 16`), so the + * host can stage sub-chunk 1 while the card is still reading sub-chunk 0's. Each slot + * is a whole constant region (I, 2I, the strict mask, scale, then one decay-ratio tile + * per v-head) plus its gate block PINNED to the same bank, which they share a + * coordinate word with. */ + pn_tt_gdn_buffer gdnc_delta_constant[PN_TT_GDNC_STAGE_SLOTS]; + pn_tt_gdn_buffer gdnc_gate[PN_TT_GDNC_STAGE_SLOTS]; + /** What each slot holds ON THE CARD, byte for byte, so an upload can send only the units + * that changed and the card's bytes still equal the builders' by construction. */ + unsigned char *gdnc_shadow_decay[PN_TT_GDNC_STAGE_SLOTS]; + float *gdnc_shadow_gate[PN_TT_GDNC_STAGE_SLOTS]; + bool gdnc_shadow_valid[PN_TT_GDNC_STAGE_SLOTS]; + /** A slot staged ahead of its descriptor (`pn_tt_gdnc_delta_prestage_at()`), consumed by + * the descriptor whose (layer, first_row, chunk_rows) it matches. */ + struct { + bool valid; + uint32_t layer, first_row, chunk_rows; + } gdnc_prestaged[PN_TT_GDNC_STAGE_SLOTS]; + /** The worker that builds a staging's heads while this thread uploads them + * (`PN_TT_GDNC_STAGE_THREAD`). Stopped in pn_tt_gdnc_release(). */ + pn_tt_stage_pipeline gdnc_stage_pipeline; + /** ONE-SHOT host work to run between the next split's enqueue and its await. */ + int (*gdnc_overlap_fn)(void *context); + void *gdnc_overlap_context; + /** A SECOND one-shot slot, run after the first in the same window (the prefill's + * alpha/beta prefetch; the first belongs to the DELTA staging). It may re-arm itself. */ + int (*gdnc_overlap_aux_fn)(void *context); + void *gdnc_overlap_aux_context; + uint64_t gdnc_stage_writes; /**< DRAM write calls the staging made */ + uint64_t gdnc_stage_hidden_ops; /**< stagings that ran under a split's card time */ + uint64_t gdnc_stage_verify_checks; /**< PN_TT_GDNC_STAGE_VERIFY read-backs */ + double host_clock_seconds[PN_TT_HOST_CLOCK_COUNT]; /**< pn_tt_device_host_clock_add() */ + uint64_t gdnc_stage_verify_mismatches; /**< units that differed from the builders */ + unsigned char *gdnc_decay_stage; /**< v_heads fp32 tiles, contiguous, so the 128 KiB + * a layer a chunk is ONE write */ + float *gdnc_gate_stage; /**< v_heads * FT_TT_OP_GDNC_GATE_STRIDE */ + /** PER-PHASE accounting, indexed by `FT_TT_OP_GDNC_PHASE_*`. Separate from the decode + * family's because the two run different work on different cores and a prefill that + * mixes chunks with a `1..taps-2` tail runs BOTH. */ + double gdnc_phase_seconds[3]; + uint64_t gdnc_phase_ops[3]; + /** PER-MEMBER AWAIT, so a core split is visible as `members` numbers and not only as one + * that moved. The awaits are issued in order after every enqueue, so a BALANCED set puts + * nearly all of the wall on member 0 and leaves the rest near zero — the same signature + * the matmul group's per-member wait line carries, and the discriminator between a set + * that ran concurrently and one this host serialised. */ + double gdnc_member_seconds[PN_TT_GDNC_MAX_MEMBERS]; + uint64_t gdnc_member_ops[PN_TT_GDNC_MAX_MEMBERS]; + /** And the replayed ones, for the same reason the decode family keeps them apart — this + * is the family the 4,502 us reading was actually taken on. */ + double gdnc_phase_recovered_seconds[3]; + uint64_t gdnc_phase_recovered_ops[3]; + uint64_t gdnc_phase_rearms[3]; + /** THE DELTA STAGING, WHICH NO PHASE LINE COULD SEE. Every DELTA descriptor transposes + * `v_heads` decay runs and gate runs on the host and writes 151,552 B of them to DRAM + * BEFORE `gdnc_submit()` starts its stopwatch, so the cost is inside the caller's + * `sequential` bracket and attributed to nothing. Two clocks, not one: BUILD is host + * arithmetic and UPLOAD is the x1 Gen2 link, and only the second is what a smaller + * operand would move. `bytes` is carried so the rate is derived here rather than from an + * assumed operand size. */ + double gdnc_stage_build_seconds; + double gdnc_stage_upload_seconds; + uint64_t gdnc_stage_ops; + uint64_t gdnc_stage_bytes; + /** THE SPLIT'S COMPLETION SHAPE, and the clock that says what it cost. `gdnc_group` is + * true only when every armed member carries the group binding AND the block read back + * with its magic — a partial arm is not a shape, it is a set of members some of which + * post and some of which do not, and it falls back rather than running. The sequence is + * the GROUP's notion of "item N" and is stamped identically on every member of one + * issue; it never resets, so a slot holding a previous issue's number can never satisfy + * this one. */ + bool gdnc_group; + uint32_t gdnc_sequence; + double gdnc_group_seconds; + uint64_t gdnc_group_ops; + + /** Which host sequence owns the ONE region set, and the keys it displaced. A displaced + * key coming back is two sequences alternating, and it is refused by name. */ + const void *kv_key; + const void *kv_displaced[PN_TT_KV_DISPLACED_KEYS]; + uint32_t kv_displaced_count; + uint64_t kv_rebinds; + + /* Scratch, on the card: one activation strip in, one result strip out. Used only by + * the host-in/host-out pn_tt_matvec(); the on-card forward names its own strips. */ + ft_tt_buffer activation; + ft_tt_buffer result; + + /* Scratch, on the host. Only the two ROWS: a matvec moves logical row 0 of each tile + * and nothing else, so the full tile staging buffers this used to keep are gone. */ + uint16_t *row_in; /**< in_features bf16 */ + uint16_t *row_out; /**< out_features bf16 */ + uint32_t max_in_features, max_out_features, max_tile_rows, max_tile_cols; + uint32_t max_row_features; /**< the widest of the two; both host rows are this */ + + /* Scratch for the MULTI-ROW strip transfers (pn_tt_activation_{upload,download}_rows). + * A batched prefill fills rows 0..M-1 of the same tiles a matvec leaves as zero padding, + * so those two move WHOLE tiles and need the tile-shaped staging buffer the row-0 path + * deliberately does not keep. Grown on demand and freed with the device; NULL until a + * batched call asks, so a decode-only run pays nothing for them. */ + unsigned char *tile_stage; /**< tiles * 2048 B, face-ordered */ + size_t tile_stage_bytes; + uint16_t *rows_stage; /**< rows * count bf16, row-major — the tilizer's input */ + size_t rows_stage_bytes; + + /* THE MULTI-CORE CORRECTNESS GATE. `PN_TT_MATVEC_DUMP=` appends logical row 0 of + * EVERY projection's output, in dispatch order, as raw bf16 — so one run at N=1 and one + * at N=4 are two files that must be byte-identical. It is a byte comparison and not a + * score on purpose: two members overwriting each other completed every kernel, kept + * every status word clean and left the aggregate GB/s unchanged, and only bytes saw it + * (foundry/tt/DEV_NOTES.md §3). Off by default; it costs a readback per op. */ + FILE *matvec_dump; + uint64_t matvec_dump_bytes; + uint64_t matvec_dump_rows; + + /** WHAT HAS BEEN ISSUED AND NOT AWAITED, so a card refusal can be REPLAYED onto a + * re-armed residency rather than costing the token. Chained projections are + * independent by contract, so replaying them is the same computation in the same + * order — which is why the recovery can be the plain synchronous path. */ + struct { + const pn_tt_weight *weight; + pn_tt_activation input; + pn_tt_activation output; + } pending[PN_TT_MATVEC_CHAIN_CEILING]; + uint32_t pending_count; + + uint64_t matvec_count; + double matvec_seconds; + double upload_seconds; /**< bf16 convert + the row-0 host->card write */ + double dispatch_seconds; /**< ft_tt_op_matmul_run: image load, CB config, go, poll */ + /** `dispatch` split at the seam that matters: ISSUE is the host writing parameter words + * and ring entries over MMIO, WAIT is the completion poll and everything the card does + * under it. Phase 3 removes issue outright and one poll per wait; whatever is left of + * wait is the card's own work and survives it. */ + double dispatch_issue_seconds; + double dispatch_wait_seconds; + uint64_t dispatch_issues; + uint64_t dispatch_waits; + uint64_t dispatch_wait_descriptors; + double readback_seconds; /**< the row-0 card->host read + bf16 convert */ + double oncard_seconds; /**< enqueue + wait for the reduce/eltwise/sample residencies */ + uint64_t oncard_ops; + /** The SAME descriptors as `oncard_seconds`, keyed by (family, function, tile count). + * `oncard_seconds` is one number over every family and the per-residency report is one + * number over every SHAPE — the elementwise residency runs six functions over three + * widths and reports a single 293.8 us mean, and the two render residencies are not in + * that report at all. See pn_tt_descriptor_census.h. */ + pn_tt_descriptor_census census; + uint64_t bytes_read, bytes_written; +}; + +/** Seconds since an arbitrary epoch. Shared by the two files that time this device. */ +double pn_tt_now_seconds(void); + +/** + * Charge ONE retired on-card descriptor to the aggregate counters AND to the census. + * + * Every family's submit path ended in the same three lines, and the census would have been + * the fourth — copied five times, which is five places for a family to be counted in one + * instrument and not the other. It is one call now, and the two can no longer disagree. + * + * @param family residency-level name, e.g. "eltwise", "render-widen" + * @param function what the descriptor computed, e.g. "silu_mul" + * @param tiles tiles the descriptor covers — the census fit's x axis + * @param seconds measured wall time inside submit + */ +void pn_tt_oncard_account(pn_tt_device *device, const char *family, const char *function, + uint32_t tiles, double seconds); + +/** `name`'s value as an unsigned, or `fallback` when unset or unparsable. */ +uint32_t pn_tt_env_u32(const char *name, uint32_t fallback); + +/** E3 SPLIT PROBE: OR `PN_TT_VEC_PROBE` (kVecProbe* bits) into the top byte of word 3 of an + * ELTWISE or REDUCE parameter block. Timing only — an armed run's text is not evidence. */ +void pn_tt_vec_probe_apply(uint32_t *words); + +/** + * One matmul with BOTH operands already resident in card DRAM — no host transfer at all. + * + * This is the dispatch half of pn_tt_matvec(), split out so the on-card forward can chain + * projections through DRAM strips instead of through the host. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_matvec_dispatch(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output); + +/* ================================================================ + * Shared by the two residency owners — pn_tt_oncard.c and pn_tt_attention.c + * ================================================================ */ + +/** Every processor of a triad. The memory family is the exception: it has no compute + * thread and enables DM0|DM1 only (foundry/tt/ft_tt_op_memory.h PART 2). */ +#define PN_TT_ONCARD_TRIAD_MASK \ + ((1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1) | (1u << FT_TT_PROC_MATH0) | \ + (1u << FT_TT_PROC_MATH1) | (1u << FT_TT_PROC_MATH2)) + +/** The GDN triad's program name. Here rather than in pn_tt_gdn.c because TWO files need it: + * that one arms the family and pn_tt_oncard.c owns the refusal when a family asked for by + * name has no program to arm. */ +/** The RESIDENT matmul triad, built for a Bfp8_b weight. One program, not two: the + * production ledger puts every matmul weight of an artifact at Bfp8_b, so a run needs one + * resident binary and the Bfp4_b variant has no tensor to serve. That is a property of the + * ARTIFACT and is checked at load, never assumed — see pn_tt_matmul_group_arm(). + * + * HERE RATHER THAN IN pn_tt_device.c since 2026-08-27: the matmul group's DST-section + * interlock reads the ARMED image's recorded `defines=` to decide whether eight DST tiles + * are legal, so the name of the program it will arm is no longer private to the file that + * loads it. Two files naming the same program in two string literals is the shape this + * backend has already been bitten by. */ +#define PN_TT_RESIDENT_PROGRAM "rtriad_bfp8" + +#define PN_TT_GDN_PROGRAM "gdn" + +/** The CHUNKWISE triad's program name. Same reason it is here: pn_tt_gdnc.c arms the family + * and pn_tt_oncard.c owns the core-collision refusal that has to name it. */ +#define PN_TT_GDNC_PROGRAM "gdnc" + +/** Just the two data-movement processors. */ +#define PN_TT_ONCARD_DM_MASK ((1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1)) + +/** Bytes written per host->card call while zeroing or filling a DRAM region. */ +#define PN_TT_ZERO_CHUNK_BYTES 65536u + +/** The reduce family's program name. Two files arm a residency of it now — pn_tt_oncard.c + * at the model's width and pn_tt_head_norm.c at the head width. */ +#define PN_TT_REDUCE_PROGRAM_NAME "reduce" + +/** THE SAME TRIAD PLACED AT Float32 — `kernels/cb_tables/reduce_float32.cbtable`, a + * `TILE_BYTES=4096` build of the same three sources, no kernel change. + * + * It is a SECOND program and not a replacement for two reasons. The bf16 one is 9x past the + * 1e-3 gate on a NARROW norm and inside it on the model's — the error is per reduce, so the + * members that matter are the ones with few elements to average it over. And doubling the + * page halves the widest residency the arena serves, 107 tile columns against 217, so the + * model's `n_embd/32` = 128 RMS norms DO NOT FIT at Float32 and stay on `reduce`. */ +#define PN_TT_REDUCE_FP32_PROGRAM_NAME "reduce_fp32" + +/** + * Build and upload ONE reduce member's constants buffer. + * + * The scaler tile, the eps tile and gamma all depend on WHICH MEMBER, so a buffer built for + * one is wrong for another — which is why the function is a parameter rather than baked in. + * `pn_tt_norm_constants_upload()` is this at FT_TT_OP_REDUCE_FN_RMS_NORM. + * + * THE PAGE SIZE IS THE RESIDENCY'S, NOT THIS FUNCTION'S. The model's reduce residency is + * placed by `reduce_float16b.cbtable` at 2048 bytes and the per-head one by + * `reduce_float32.cbtable` at 4096; the reader issues `TILE_BYTES` reads and nothing on the + * card can catch a constants buffer built at the other one. + * + * @param gamma `width` values, or NULL for a member whose chain has no gamma step + * @param eps what the member's reduce should see added to its accumulator + * @param tile_bytes PN_TT_TILE_BF16 or PN_TT_TILE_FP32 — the page the residency was placed at + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_reduce_constants_upload(pn_tt_device *device, ft_tt_op_reduce_function function, + const float *gamma, uint32_t width, float eps, + uint32_t tile_bytes, pn_tt_norm_constants *out_constants); + +/** Is `name` the PN_TT_RED mutation this process was asked to run? Shared so the attention + * family's red proofs read the same knob as the on-card ones. */ +bool pn_tt_red(const char *name); + +/** Allocate a region shaped like SSM layer `layer`'s live window + state, on the live + * region's banks (one coordinate word carries both). Left uninitialised. 0 or -1. */ +int pn_tt_gdn_alloc_twin(pn_tt_device *device, uint32_t layer, pn_tt_gdn_region *out); +/** The bytes of one layer's carried conv window and recurrent state (unrounded). */ +void pn_tt_gdn_carried_bytes(const pn_tt_device *device, size_t *window, size_t *state); + +/** Write `bytes` of zeros at a DRAM address, in bounded chunks. */ +int pn_tt_zero_dram(pn_tt_device *device, uint32_t bank, uint32_t address, size_t bytes); +/** `PN_TT_IDLE_RETIRE_ONLY` (diagnosis): may the residency `name` be retired for idleness? */ +bool pn_tt_idle_retire_allowed(const char *name); +/** `pn_tt_zero_dram` with every byte `value` (a test poison; 0 is the zero). */ +int pn_tt_fill_dram(pn_tt_device *device, uint32_t bank, uint32_t address, size_t bytes, + unsigned char value); + +/** Write `bytes` from host memory to a DRAM address, in bounded chunks. */ +int pn_tt_write_dram(pn_tt_device *device, uint32_t bank, uint32_t address, const void *data, + size_t bytes); + +/** Read `bytes` from a DRAM address into host memory, in bounded chunks. */ +int pn_tt_read_dram(pn_tt_device *device, uint32_t bank, uint32_t address, void *out, + size_t bytes); + +/** An explicitly-named program directory to read INSTEAD of the embedded blob. */ +#define PN_TT_PROGRAM_ROOT_ENV "PN_TT_PROGRAM_DIR" + +/** + * The program root this process was told to use, or NULL for the embedded blob. + * + * NULL IS THE DEFAULT AND THE DEFAULT IS THE BINARY'S OWN PROGRAMS. There is no fallback + * path in either direction: an unset variable means embedded, and a set one means that + * directory or a refusal (`ft_tt_program_resolve.h`). The hardcoded + * absolute programs directory this replaced made Pantheon a one-machine program and + * turned a program built into the wrong root into a silent 1.085x — the `rtriad_bfp8` + * incident. + * + * One reader for the whole backend so the answer cannot differ between the matmul programs + * and the on-card families of the same run. + * + * @return the directory, or NULL when the variable is unset or empty + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +const char *pn_tt_program_override_root(void); + +/** Name the place programs are coming from, for a message that has to say one or the other. + * Takes the root so a caller cannot describe one arm while resolving through another. */ +const char *pn_tt_program_where(const char *root); + +/** + * Resolve `program` by NAME and boot its firmware on this family's core. + * + * SEPARATE FROM THE START, and the order is load-bearing: pn_tt_residency_load() zeroes the + * whole handle, so a caller that placed its circular buffers first would have them silently + * erased. Prepare, THEN place, THEN start. + */ +bool pn_tt_prepare_family(pn_tt_device *device, pn_tt_residency *residency, const char *program, + const char *name, const char *core_env, uint32_t core_default, + uint32_t processor_mask); + +/** The same, on a core the CALLER names — for arming several residencies of one family at + * once, where a single environment variable cannot say a different core for each. `core_env` + * is still taken, but only so the refusal message can name the knob that would move it. */ +bool pn_tt_prepare_family_on_core(pn_tt_device *device, pn_tt_residency *residency, + const char *program, const char *name, const char *core_env, + uint32_t core, uint32_t processor_mask); + +/** Kick a prepared residency whose circular buffers the caller has now placed. */ +bool pn_tt_start_family(pn_tt_residency *residency); +/** The family last started on the core at NoC (x, y), or "?" (PM 8, AWAIT census). */ +const char *pn_tt_core_label(uint16_t x, uint16_t y); + +/** Does this binary (or the named override root) carry the program `program`? */ +bool pn_tt_program_is_present(const char *program); + +/** + * The ONE block depth a residency can run for every tile count it will see. + * + * Both conditions are the CB-cycle invariant and neither is optional: `depth` must divide each + * run so a block is contiguous, and `2 * depth` — the page count the buffers are placed at — + * must divide it too, so each descriptor returns the write pointer to the base. A descriptor + * that leaves it elsewhere walks the next one up L1 with no error. + * + * Shared because TWO files place elementwise-shaped buffers now: pn_tt_oncard.c for the + * model's eltwise and sampling residencies, pn_tt_render.c for the two strip-bridge ones. + * + * @return the depth, or 0 when no depth serves every count + */ +uint32_t pn_tt_common_depth(const uint32_t *tile_counts, uint32_t count, uint32_t cap); + +/** Ceiling on a resident reader's block depth. Not a tuning knob: two blocks of `depth` pages + * on each of two input streams is 4 * depth * 2048 bytes of a 1.29 MiB arena, and a deeper + * block buys nothing once the reader is no longer transaction-bound. */ +#define PN_TT_ONCARD_MAX_DEPTH 32u + +#endif /* USE_TT */ +#endif /* PN_TT_DEVICE_INTERNAL_H */ diff --git a/src/accel/tt/pn_tt_dispatch.c b/src/accel/tt/pn_tt_dispatch.c new file mode 100644 index 0000000..6df07bd --- /dev/null +++ b/src/accel/tt/pn_tt_dispatch.c @@ -0,0 +1,2558 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_dispatch.c + * @brief STEP 3 (A) — the dispatch RECORDER and its host-side SHADOW. See pn_tt_dispatch.h. + * + * @status A1 observe mode; A1b model + host-side shadow verify. + * @issues None + * @todo A2 the puller's dry run; A3 live replay. + * + * WHY THE TRANSPORT AND NOT THE CALL SITES. Every host access to the card goes through one of + * the runtime's three accessors (`noc`, `bulk_l1`, `bulk_dram`). Wrapping them sees ALL of a + * token — including any write no queue API knows about — so "the token is queue traffic plus + * these boundaries" is a measured statement, not an inventory of call sites. Every resident + * queue sits at the same L1 layout (control 0x170000, head word +0x8, completion window + * +0x10, ring +0x100, rotating parameter slots +0x9000, parameter cache +0xA000, group block + * +0xF000), so the address alone classifies an access. + * + * THE MODEL (A1b). Two recorded tokens t0, t0+1 must be the same SHAPE (event for event: class, + * accessor, core, size; the address too except ring slots). Then every queue word is one of: + * - a ring descriptor's TOKEN: `nonce ^ ((index+1) * 0x9E3779B9)` (ft_tt_queue_token_at), + * index = the queue's descriptor count, which advances by a fixed n_q a token; + * - a ring SLOT address: ring + (index & (capacity-1)) * 32; + * - anything else (head values, poll counts, parameter words, a4/a5 sequences, KV + * positions): LINEAR in the token, `w(t0) + (w(t0+1) - w(t0)) * k`. + * Boundary accesses (DRAM, non-queue L1) are the host's: only their shape is modelled. The + * SHADOW predicts every queue write and every awaited count of each later token from the + * model alone and compares it, byte for byte, with what the host really did. RED + * `PN_TT_RED=dispatch_skip_patch` drops the linear strides; it must mismatch. + */ +#ifdef USE_TT + +#include "pn_tt_dispatch.h" + +#include +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "../../core/bench.h" + +#include "tt/ft_tt_op_l1_layout.h" + +enum { + TD_ACC_NOC = 0, + TD_ACC_BULK_L1 = 1, + TD_ACC_BULK_DRAM = 2, + TD_ACCESSORS = 3 +}; + +typedef enum { + TD_Q_HEAD, /* write control word 2 */ + TD_Q_RING, /* write ring slot(s) */ + TD_Q_PARAM_ROT, /* write a rotating parameter slot */ + TD_Q_PARAM_CACHE, /* write a parameter-cache block */ + TD_Q_OTHER_W, /* any other queue-region write */ + TD_Q_POLL, /* read the completion window (words 4..15) */ + TD_Q_GROUP_R, /* read a group completion block */ + TD_Q_OTHER_R, /* any other queue-region read */ + TD_B_L1_W, /* BOUNDARY: L1 write outside the queue region */ + TD_B_L1_R, + TD_B_DRAM_W, + TD_B_DRAM_R, + TD_CLASSES +} td_class; + +static const char *const td_class_name[TD_CLASSES] = { + "q-head", "q-ring", "q-param-rot", "q-param-cache", "q-other-w", "q-poll", "q-group-r", + "q-other-r", "B-l1-w", "B-l1-r", "B-dram-w", "B-dram-r" +}; + +#define TD_QUEUE_RING_OFFSET 0x100u +#define TD_TOKEN_MULTIPLIER 0x9E3779B9u +#define TD_DATA_WORDS 128u /* 512 B: the longest queue write (8 ring slots) */ +#define TD_MAX_QUEUES 256u + +typedef struct { + uint8_t cls; + uint8_t acc; + uint16_t x, y; + uint32_t addr; + uint32_t bytes; + uint32_t repeats; /* merged consecutive identical reads (a poll loop) */ + uint32_t data_at; /* index into the token's word pool (writes: payload, reads: last data) */ + uint32_t words; /* words stored at data_at */ + uint32_t queue; /* ring/head/poll: the queue's table index */ + uint32_t index; /* ring: descriptor index of the first slot written */ +} td_event; + +typedef struct { + td_event *events; + size_t count, capacity; + uint32_t *pool; + size_t pool_count, pool_capacity; +} td_token; + +typedef struct { + uint16_t x, y; + uint32_t pending; /* ring descriptors staged since the last head write */ + size_t pending_from; /* first ring event of those */ + uint32_t nonce; + bool nonce_known; + uint32_t mask; /* capacity - 1, inferred */ + uint32_t base[2]; /* head value at the start of each model token */ +} td_queue; + +typedef struct { + pn_tt_device *device; + ft_tt_noc_access original[TD_ACCESSORS]; + bool wrapped; + bool recording; + td_token *into; + td_token model[2]; + td_token current; + td_queue queues[TD_MAX_QUEUES]; + uint32_t queue_count; + bool model_ok; + bool puller_ready; + /* A3 live replay */ + bool replaying; /* this forward's queue traffic is the puller's */ + bool internal; /* our own puller enqueue/await: pass straight through */ + bool replay_dead; /* a divergence was seen: never replay again this process */ + size_t cursor; /* the model event the host is at */ + bool polling; /* the host is inside the poll loop of event `cursor` */ + uint32_t next_segment, awaited_segment, replay_k; + uint64_t replayed_tokens, divergences, launched_segments, puller_rearms; + uint64_t first_forward; + uint64_t sequence_first; /* the forward a sequence began at (1 = the process's first) */ + bool sequence_reset; /* pn_tt_dispatch_sequence_reset() was called */ + /* shadow tallies */ + uint64_t checked_tokens, shape_failures; + uint64_t compared[TD_CLASSES], mismatched[TD_CLASSES]; + unsigned mismatches_printed; +} td_recorder; + +static td_recorder g_td; + +static bool td_is_boundary(uint8_t cls) +{ + return cls >= TD_B_L1_W; +} + +static uint8_t td_classify(uint8_t acc, uint64_t addr, bool write) +{ + uint64_t offset; + + if (acc == TD_ACC_BULK_DRAM) + return write ? TD_B_DRAM_W : TD_B_DRAM_R; + if (addr < FT_TT_OP_QUEUE_BASE || addr >= FT_TT_OP_QUEUE_BASE + 0x10000u) + return write ? TD_B_L1_W : TD_B_L1_R; + offset = addr - FT_TT_OP_QUEUE_BASE; + if (write) { + if (offset == 8u) + return TD_Q_HEAD; + if (offset >= TD_QUEUE_RING_OFFSET && offset < 0x9000u) + return TD_Q_RING; + if (offset >= 0x9000u && offset < 0xA000u) + return TD_Q_PARAM_ROT; + if (offset >= 0xA000u && offset < 0xF000u) + return TD_Q_PARAM_CACHE; + return TD_Q_OTHER_W; + } + if (offset >= 0x10u && offset < 0x40u) + return TD_Q_POLL; + if (offset >= 0xF000u && offset < 0xF900u) + return TD_Q_GROUP_R; + return TD_Q_OTHER_R; +} + +static uint32_t td_queue_of(uint16_t x, uint16_t y) +{ + uint32_t i; + + for (i = 0; i < g_td.queue_count; i++) + if (g_td.queues[i].x == x && g_td.queues[i].y == y) + return i; + if (g_td.queue_count == TD_MAX_QUEUES) + return TD_MAX_QUEUES; + memset(&g_td.queues[i], 0, sizeof(g_td.queues[i])); + g_td.queues[i].x = x; + g_td.queues[i].y = y; + return g_td.queue_count++; +} + +static bool td_reserve(td_token *t, size_t words) +{ + if (t->count == t->capacity) { + size_t grow = t->capacity ? t->capacity * 2u : 16384u; + td_event *more = realloc(t->events, grow * sizeof(*more)); + + if (!more) + return false; + t->events = more; + t->capacity = grow; + } + if (t->pool_count + words > t->pool_capacity) { + size_t grow = t->pool_capacity ? t->pool_capacity * 2u : 262144u; + uint32_t *more; + + while (grow < t->pool_count + words) + grow *= 2u; + more = realloc(t->pool, grow * sizeof(*more)); + if (!more) + return false; + t->pool = more; + t->pool_capacity = grow; + } + return true; +} + +static void td_note(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, const void *data, + size_t bytes, bool write) +{ + td_token *t = g_td.into; + td_event *last, *e; + const uint8_t cls = td_classify(acc, addr, write); + size_t words = (bytes + 3u) / 4u; + + if (!g_td.recording || !t) + return; + if (words > TD_DATA_WORDS) + words = TD_DATA_WORDS; + last = t->count ? &t->events[t->count - 1u] : NULL; + /* A LONG poll reads the ERROR word (control word 9) every 128 polls to find a refusal. + * It is part of the same await: fold it in, so the poll after it merges too. */ + if (!write && last && last->cls == TD_Q_POLL && addr == FT_TT_OP_QUEUE_BASE + 9u * 4u && + bytes == 4u) { + /* (any core's: the matmul's group check reads every pending member's error word) */ + last->repeats++; + return; + } + /* A poll loop is ONE event: the same read repeated; keep the LAST answer. */ + if (!write && last && last->cls == cls && last->acc == acc && last->x == x && + last->y == y && last->addr == (uint32_t)addr && last->bytes == (uint32_t)bytes) { + last->repeats++; + memcpy(&t->pool[last->data_at], data, last->words * 4u > bytes ? bytes : last->words * 4u); + return; + } + if (!td_reserve(t, words)) { + g_td.recording = false; + return; + } + e = &t->events[t->count++]; + memset(e, 0, sizeof(*e)); + e->cls = cls; + e->acc = acc; + e->x = x; + e->y = y; + e->addr = (uint32_t)addr; + e->bytes = (uint32_t)bytes; + e->repeats = 1u; + e->data_at = (uint32_t)t->pool_count; + e->words = (uint32_t)words; + memset(&t->pool[t->pool_count], 0, words * 4u); + memcpy(&t->pool[t->pool_count], data, bytes < words * 4u ? bytes : words * 4u); + t->pool_count += words; +} + +static bool td_stream_write(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, + const void *src, size_t bytes); +static bool td_stream_read(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, void *dst, + size_t bytes); +static bool g_streaming; + +#define TD_WRAPPERS(index) \ + static int td_write_##index(void *user, uint16_t x, uint16_t y, uint64_t addr, \ + const void *src, size_t bytes) \ + { \ + (void)user; \ + if (g_streaming && !g_td.internal && td_stream_write(index, x, y, addr, src, bytes)) \ + return FT_TT_OK; \ + if (g_td.replaying && !g_td.internal && td_replay_access(index, x, y, addr, src, bytes, \ + true)) \ + return FT_TT_OK; \ + td_note(index, x, y, addr, src, bytes, true); \ + return g_td.original[index].write(g_td.original[index].user, x, y, addr, src, bytes); \ + } \ + static int td_read_##index(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, \ + size_t bytes) \ + { \ + int rc_; \ + (void)user; \ + if (g_streaming && !g_td.internal && td_stream_read(index, x, y, addr, dst, bytes)) \ + return FT_TT_OK; \ + if (g_td.replaying && !g_td.internal && \ + td_replay_access(index, x, y, addr, dst, bytes, false)) \ + return FT_TT_OK; \ + rc_ = g_td.original[index].read(g_td.original[index].user, x, y, addr, dst, bytes); \ + td_note(index, x, y, addr, dst, bytes, false); \ + return rc_; \ + } + +static bool td_replay_access(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, + const void *data, size_t bytes, bool write); + +TD_WRAPPERS(0) +TD_WRAPPERS(1) +TD_WRAPPERS(2) + +static ft_tt_noc_access *td_accessor(pn_tt_device *device, unsigned index) +{ + switch (index) { + case TD_ACC_NOC: + return &device->runtime.noc; + case TD_ACC_BULK_L1: + return &device->runtime.bulk_l1; + default: + return &device->runtime.bulk_dram; + } +} + +static void td_wrap(pn_tt_device *device) +{ + static int (*const writes[TD_ACCESSORS])(void *, uint16_t, uint16_t, uint64_t, const void *, + size_t) = { td_write_0, td_write_1, td_write_2 }; + static int (*const reads[TD_ACCESSORS])(void *, uint16_t, uint16_t, uint64_t, void *, + size_t) = { td_read_0, td_read_1, td_read_2 }; + unsigned index; + + if (g_td.wrapped) + return; + g_td.device = device; + for (index = 0; index < TD_ACCESSORS; index++) { + ft_tt_noc_access *access = td_accessor(device, index); + + g_td.original[index] = *access; + if (!access->write || !access->read) + continue; + access->write = writes[index]; + access->read = reads[index]; + } + g_td.wrapped = true; +} + +static void td_unwrap(void) +{ + unsigned index; + + if (!g_td.wrapped) + return; + for (index = 0; index < TD_ACCESSORS; index++) + *td_accessor(g_td.device, index) = g_td.original[index]; + g_td.wrapped = false; +} + +static long td_env_long(const char *name) +{ + const char *value = getenv(name); + + return (value && *value) ? strtol(value, NULL, 10) : 0; +} + +/** `PN_TT_DISPATCH`: on-card dispatch (A, the single puller). DEFAULT 1; 0 = host-issued. */ +static long td_dispatch_live(void) +{ + const char *value = getenv("PN_TT_DISPATCH"); + + return (value && *value) ? strtol(value, NULL, 10) : 1; +} + +/* --------------------------------------------------------------------------------------- + * Resolve: queue identity, descriptor indices, the count each poll waits for + * --------------------------------------------------------------------------------------- */ + +/** Attach every ring / head / poll event to its queue and give ring events their index. + * @return false if the token's queue traffic is not the shape the model assumes */ +static bool td_resolve(td_token *t) +{ + size_t i; + uint32_t q; + + for (q = 0; q < g_td.queue_count; q++) + g_td.queues[q].pending = 0u; + for (i = 0; i < t->count; i++) { + td_event *e = &t->events[i]; + + if (e->cls != TD_Q_RING && e->cls != TD_Q_HEAD && e->cls != TD_Q_POLL && + e->cls != TD_Q_PARAM_ROT && e->cls != TD_Q_PARAM_CACHE) + continue; + q = td_queue_of(e->x, e->y); + if (q == TD_MAX_QUEUES) + return false; + e->queue = q; + if (e->cls == TD_Q_RING) { + if (e->bytes % 32u != 0u) + return false; + if (g_td.queues[q].pending == 0u) + g_td.queues[q].pending_from = i; + g_td.queues[q].pending += e->bytes / 32u; + } else if (e->cls == TD_Q_HEAD) { + uint32_t head = t->pool[e->data_at]; + uint32_t index = head - g_td.queues[q].pending; + size_t k; + + /* The ring writes since the last head hold indices head-pending .. head-1. */ + for (k = g_td.queues[q].pending_from; g_td.queues[q].pending && k < i; k++) { + td_event *r = &t->events[k]; + + if (r->cls != TD_Q_RING || r->queue != q) + continue; + r->index = index; + index += r->bytes / 32u; + } + g_td.queues[q].pending = 0u; + } + } + return true; +} + +static uint32_t td_token_at(uint32_t nonce, uint32_t index) +{ + uint32_t token = nonce ^ ((index + 1u) * TD_TOKEN_MULTIPLIER); + + if (token == 0u || token == (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON) + token = nonce ^ 0xA5A5A5A5u; + return token; +} + +/** Nonce and ring capacity per queue, from the model tokens. */ +/** The queue's ring capacity - 1, read from its control block (word 1), or 0 if the read + * fails or the word is not a power of two (the caller then infers it). */ +static uint32_t td_queue_capacity_mask(const td_queue *queue) +{ + const ft_tt_noc_access *bulk = &g_td.original[TD_ACC_BULK_L1]; + uint32_t words[4]; + + if (!bulk->read || bulk->read(bulk->user, queue->x, queue->y, FT_TT_OP_QUEUE_BASE, words, + sizeof(words)) != FT_TT_OK) + return 0u; + if (words[1] == 0u || (words[1] & (words[1] - 1u)) != 0u || words[1] > 0x10000u) + return 0u; + return words[1] - 1u; +} + +static bool td_learn_queues(void) +{ + uint32_t q; + unsigned m; + size_t i; + + for (q = 0; q < g_td.queue_count; q++) { + g_td.queues[q].nonce_known = false; + g_td.queues[q].mask = 0u; + } + for (m = 0; m < 2u; m++) { + const td_token *t = &g_td.model[m]; + + for (i = 0; i < t->count; i++) { + const td_event *e = &t->events[i]; + td_queue *queue; + uint32_t slot, d; + + if (e->cls != TD_Q_RING) + continue; + queue = &g_td.queues[e->queue]; + if (!queue->nonce_known) { + queue->nonce = t->pool[e->data_at + 1u] ^ ((e->index + 1u) * TD_TOKEN_MULTIPLIER); + queue->nonce_known = true; + } + for (d = 0; d < e->bytes / 32u; d++) + if (t->pool[e->data_at + d * 8u + 1u] != td_token_at(queue->nonce, e->index + d)) + return false; + slot = (e->addr - FT_TT_OP_QUEUE_BASE - TD_QUEUE_RING_OFFSET) / 32u; + /* THE CAPACITY IS READ, NOT INFERRED. "The smallest power of two every recorded + * slot agrees with" is underdetermined for a queue with one descriptor a token: + * two tokens at indices 144, 145 of a 16-slot ring agree with capacity 2, and the + * replay then put index 146 in slot 0 (a multi-turn delta prefill after spec + * cycles: DIVERGED at k=2, a puller segment failed, the turn came back empty). + * RED `dispatch_mask_infer` keeps the inference. */ + if (queue->mask == 0u && !pn_tt_red("dispatch_mask_infer")) + queue->mask = td_queue_capacity_mask(queue); + if ((e->index & queue->mask) != slot && queue->mask != 0u && + !pn_tt_red("dispatch_mask_infer")) + return false; + while ((e->index & queue->mask) != slot) { + queue->mask = queue->mask * 2u + 1u; + if (queue->mask > 0xFFFFu) + return false; + } + } + } + return true; +} + +/* --------------------------------------------------------------------------------------- + * The shadow: predict a later token from the model, compare byte for byte + * --------------------------------------------------------------------------------------- */ + +static bool td_same_shape(const td_event *a, const td_event *b) +{ + return a->cls == b->cls && a->acc == b->acc && a->x == b->x && a->y == b->y && + a->bytes == b->bytes && (a->cls == TD_Q_RING || a->addr == b->addr); +} + +static void td_mismatch(uint64_t forward, size_t i, const char *what, uint32_t want, + uint32_t got) +{ + if (g_td.mismatches_printed >= 12u) + return; + g_td.mismatches_printed++; + fprintf(stderr, "pn_tt: DISPATCH SHADOW forward %llu event %zu %s: predicted 0x%08x, the " + "host wrote 0x%08x\n", + (unsigned long long)forward, i, what, want, got); +} + +static void td_shadow_check(uint64_t forward) +{ + const td_token *a = &g_td.model[0], *b = &g_td.model[1], *t = &g_td.current; + const uint32_t k = (uint32_t)(forward - g_td.first_forward); + const bool skip_patch = pn_tt_red("dispatch_skip_patch"); + size_t i; + + g_td.checked_tokens++; + if (t->count != a->count) { + g_td.shape_failures++; + fprintf(stderr, "pn_tt: DISPATCH SHADOW forward %llu: %zu events, the model has %zu\n", + (unsigned long long)forward, t->count, a->count); + return; + } + for (i = 0; i < t->count; i++) { + const td_event *ea = &a->events[i], *eb = &b->events[i], *et = &t->events[i]; + bool bad = false; + uint32_t w; + + if (!td_same_shape(ea, et)) { + g_td.shape_failures++; + if (g_td.mismatches_printed < 12u) { + g_td.mismatches_printed++; + fprintf(stderr, "pn_tt: DISPATCH SHADOW forward %llu event %zu: shape %s " + "(%u,%u) 0x%x %u B, the model has %s (%u,%u) 0x%x %u B\n", + (unsigned long long)forward, i, td_class_name[et->cls], et->x, et->y, + et->addr, et->bytes, td_class_name[ea->cls], ea->x, ea->y, ea->addr, + ea->bytes); + } + return; + } + if (td_is_boundary(et->cls) || et->cls == TD_Q_GROUP_R || et->cls == TD_Q_OTHER_R) + continue; + g_td.compared[et->cls]++; + if (et->cls == TD_Q_RING) { + const td_queue *queue = &g_td.queues[ea->queue]; + const uint32_t step = eb->index - ea->index; + const uint32_t index = ea->index + step * k; + const uint32_t addr = FT_TT_OP_QUEUE_BASE + TD_QUEUE_RING_OFFSET + + (index & queue->mask) * 32u; + uint32_t d, word; + + if (et->addr != addr) { + td_mismatch(forward, i, "ring slot address", addr, et->addr); + bad = true; + } + for (d = 0; !bad && d < et->bytes / 32u; d++) + for (word = 0; word < 8u; word++) { + const uint32_t at = d * 8u + word; + + w = word == 1u ? td_token_at(queue->nonce, index + d) + : a->pool[ea->data_at + at] + + (skip_patch ? 0u + : (b->pool[eb->data_at + at] - + a->pool[ea->data_at + at]) * k); + if (w != t->pool[et->data_at + at]) { + td_mismatch(forward, i, word == 1u ? "ring token" : "ring word", w, + t->pool[et->data_at + at]); + bad = true; + break; + } + } + } else { + /* Writes: every word linear. Polls: only the word that holds COMPLETED_COUNT + * (control word 5) is the await's target; the rest is the card's own state. */ + uint32_t word, first = 0u, last = et->words; + + if (et->cls == TD_Q_POLL) { + const uint32_t offset = et->addr - FT_TT_OP_QUEUE_BASE; + + if (offset > 0x14u || offset + et->bytes <= 0x14u) + continue; /* a poll that does not read the count (an error word) */ + first = (0x14u - offset) / 4u; + last = first + 1u; + } + for (word = first; word < last; word++) { + w = a->pool[ea->data_at + word] + + (skip_patch ? 0u + : (b->pool[eb->data_at + word] - a->pool[ea->data_at + word]) * + k); + if (w != t->pool[et->data_at + word]) { + td_mismatch(forward, i, td_class_name[et->cls], w, + t->pool[et->data_at + word]); + bad = true; + break; + } + } + } + if (bad) + g_td.mismatched[et->cls]++; + } +} + +static void td_shadow_report(void) +{ + unsigned cls; + uint64_t total = 0u, bad = 0u; + + for (cls = 0; cls < TD_CLASSES; cls++) { + total += g_td.compared[cls]; + bad += g_td.mismatched[cls]; + } + fprintf(stderr, "pn_tt: DISPATCH SHADOW — model from forwards %llu/%llu (%s), %u queues; " + "%llu later tokens checked, %llu shape failures, %llu of %llu predicted " + "events MISMATCHED%s\n", + (unsigned long long)g_td.first_forward, (unsigned long long)g_td.first_forward + 1u, + g_td.model_ok ? "learned" : "REFUSED", g_td.queue_count, + (unsigned long long)g_td.checked_tokens, (unsigned long long)g_td.shape_failures, + (unsigned long long)bad, (unsigned long long)total, + pn_tt_red("dispatch_skip_patch") ? " [RED dispatch_skip_patch]" : ""); + for (cls = 0; cls < TD_CLASSES; cls++) + if (g_td.compared[cls]) + fprintf(stderr, "pn_tt: DISPATCH SHADOW %-13s %9llu compared %7llu mismatched\n", + td_class_name[cls], (unsigned long long)g_td.compared[cls], + (unsigned long long)g_td.mismatched[cls]); +} + +/* --------------------------------------------------------------------------------------- + * A2: the puller's PROGRAM, built from the model, and its on-card DRY RUN + * --------------------------------------------------------------------------------------- */ + +#define TD_PROGRAM_BASE 0x26000u /* the CB arena: the puller has no CBs */ +#define TD_PROGRAM_LIMIT 0x160000u /* the puller's scratch starts here */ +#define TD_PULL_MAGIC 0x524C5044u +#define TD_PULL_OPCODE 0x14u +#define TD_PULLER_DEFAULT_CORE 89u + +typedef struct { + uint32_t *words; + size_t count, capacity; + uint32_t segments; + uint32_t segment_offset[256]; + bool ok; +} td_program; + +static td_program g_prog; +static uint64_t g_ring_patch_word[8]; /* which descriptor words move token to token */ + +static void td_emit(uint32_t word) +{ + if (!g_prog.ok) + return; + if (g_prog.count == g_prog.capacity) { + size_t grow = g_prog.capacity ? g_prog.capacity * 2u : 65536u; + uint32_t *more = realloc(g_prog.words, grow * sizeof(*more)); + + if (!more) { + g_prog.ok = false; + return; + } + g_prog.words = more; + g_prog.capacity = grow; + } + g_prog.words[g_prog.count++] = word; +} + +/** Emit base words, then {word, stride} for every word that moves between the two models. */ +static void td_emit_words(const uint32_t *base, const uint32_t *next, uint32_t n, + int skip_every_8th_1) +{ + uint32_t i, patches = 0u; + size_t header_at = g_prog.count - 4u; /* caller emitted the entry's 4 header-ish words */ + + (void)header_at; + for (i = 0; i < n; i++) + td_emit(base[i]); + for (i = 0; i < n; i++) { + if (skip_every_8th_1 && (i % 8u) == 1u) + continue; /* the ring token: computed on the card */ + if (next[i] != base[i]) { + td_emit(i); + td_emit(next[i] - base[i]); + patches++; + } + } + (void)patches; +} + +static uint32_t td_count_patches(const uint32_t *base, const uint32_t *next, uint32_t n, + int ring) +{ + uint32_t i, patches = 0u; + + for (i = 0; i < n; i++) + if (!(ring && (i % 8u) == 1u) && next[i] != base[i]) + patches++; + return patches; +} + +/** `PN_TT_DISPATCH_MCAST`: STEP 3 B, DEFAULT 1 (run_d_mcast.sh: +1.0% steady, IDENTICAL). */ +static long td_mcast_on(void) +{ + const char *value = getenv("PN_TT_DISPATCH_MCAST"); + + return (value && *value) ? strtol(value, NULL, 10) : 1; +} + +/* --------------------------------------------------------------------------------------- + * STEP 3 B: MULTICAST GROUPS. Ring slots whose bytes are identical on several queues (the + * matmul members 1..N share a nonce, an index and a parameter-cache layout) go out as ONE + * multicast to a rectangle of <= 4 member cores (the recorded wedge rule), and their heads + * as one 4-byte multicast. `PN_TT_DISPATCH_MCAST` (default 1). + * --------------------------------------------------------------------------------------- */ + +#define TD_MAX_GROUPS 64u + +typedef struct { + uint32_t queues[4]; + uint32_t count; + uint16_t x_lo, y_lo, x_hi, y_hi; +} td_group; + +static td_group g_groups[TD_MAX_GROUPS]; +static uint32_t g_group_count; +static int32_t *g_merge; /* per model event: -1 plain, >= 0 group leader, -2 merged away */ +static uint64_t g_merged_rings, g_merged_heads; + +static bool td_ring_same(size_t i, size_t j) +{ + const td_token *a = &g_td.model[0], *b = &g_td.model[1]; + const td_event *ai = &a->events[i], *aj = &a->events[j]; + const td_event *bi = &b->events[i], *bj = &b->events[j]; + const td_queue *qi = &g_td.queues[ai->queue], *qj = &g_td.queues[aj->queue]; + uint32_t w; + + if (ai->bytes != aj->bytes || ai->addr != aj->addr || ai->index != aj->index || + bi->index != bj->index || qi->nonce != qj->nonce || qi->mask != qj->mask) + return false; + for (w = 0; w < ai->words; w++) + if (a->pool[ai->data_at + w] != a->pool[aj->data_at + w] || + b->pool[bi->data_at + w] != b->pool[bj->data_at + w]) + return false; + return true; +} + +static bool td_head_same(size_t i, size_t j) +{ + const td_token *a = &g_td.model[0], *b = &g_td.model[1]; + + return a->pool[a->events[i].data_at] == a->pool[a->events[j].data_at] && + b->pool[b->events[i].data_at] == b->pool[b->events[j].data_at]; +} + +/** Partition the queues that share a nonce with another queue into row runs of <= 4 + * adjacent cores (same y, consecutive x), every cell of a run a sharing queue. */ +static void td_find_groups(void) +{ + bool used[TD_MAX_QUEUES]; + uint32_t q, r; + + memset(used, 0, sizeof(used)); + g_group_count = 0u; + for (q = 0; q < g_td.queue_count; q++) { + uint32_t peers = 0u; + + for (r = 0; r < g_td.queue_count; r++) + if (r != q && g_td.queues[r].nonce == g_td.queues[q].nonce) + peers++; + if (peers == 0u) + used[q] = true; /* not a sharing queue */ + } + for (q = 0; q < g_td.queue_count && g_group_count < TD_MAX_GROUPS; q++) { + td_group *g = &g_groups[g_group_count]; + uint32_t x; + + if (used[q]) + continue; + memset(g, 0, sizeof(*g)); + g->queues[0] = q; + g->count = 1u; + g->x_lo = g->x_hi = g_td.queues[q].x; + g->y_lo = g->y_hi = g_td.queues[q].y; + used[q] = true; + /* extend to the right along the row */ + for (x = g_td.queues[q].x + 1u; g->count < 4u; x++) { + for (r = 0; r < g_td.queue_count; r++) + if (!used[r] && g_td.queues[r].y == g->y_lo && g_td.queues[r].x == x && + g_td.queues[r].nonce == g_td.queues[q].nonce) + break; + if (r == g_td.queue_count) + break; + g->queues[g->count++] = r; + g->x_hi = (uint16_t)x; + used[r] = true; + } + if (g->count >= 2u) + g_group_count++; + } +} + +/** Mark, window by window (between awaits / boundaries), each group whose members' k-th + * ring writes are byte-identical (and k-th heads equal) as ONE multicast. */ +static bool td_plan_merges(void) +{ + const td_token *a = &g_td.model[0]; + size_t i, from = 0u; + + free(g_merge); + g_merge = malloc(a->count * sizeof(*g_merge)); + if (!g_merge) + return false; + for (i = 0; i < a->count; i++) + g_merge[i] = -1; + g_merged_rings = g_merged_heads = 0u; + if (td_mcast_on() <= 0) + return true; + td_find_groups(); + for (i = 0; i <= a->count; i++) { + size_t j, k; + uint32_t g; + + if (i < a->count && !td_is_boundary(a->events[i].cls) && a->events[i].cls != TD_Q_POLL) + continue; + /* window [from, i) */ + for (g = 0; g < g_group_count; g++) { + const td_group *grp = &g_groups[g]; + uint32_t occurrence; + + for (occurrence = 0;; occurrence++) { + size_t ring_at[4], head_at[4]; + uint32_t m, found = 0u; + + for (m = 0; m < grp->count; m++) { + uint32_t seen_r = 0u, seen_h = 0u; + + ring_at[m] = head_at[m] = (size_t)-1; + for (j = from; j < i; j++) { + const td_event *e = &a->events[j]; + + if (e->queue != grp->queues[m]) + continue; + if (e->cls == TD_Q_RING && seen_r++ == occurrence) + ring_at[m] = j; + if (e->cls == TD_Q_HEAD && seen_h++ == occurrence) + head_at[m] = j; + } + if (ring_at[m] != (size_t)-1 && head_at[m] != (size_t)-1) + found++; + } + if (found == 0u) + break; + if (found != grp->count) + continue; + for (k = 1; k < grp->count; k++) + if (!td_ring_same(ring_at[0], ring_at[k]) || + !td_head_same(head_at[0], head_at[k])) + break; + if (k != grp->count) + continue; + /* the LEADER is the earliest ring / head of the group in the window */ + { + size_t lr = ring_at[0], lh = head_at[0]; + + for (k = 1; k < grp->count; k++) { + if (ring_at[k] < lr) + lr = ring_at[k]; + if (head_at[k] < lh) + lh = head_at[k]; + } + for (k = 0; k < grp->count; k++) { + g_merge[ring_at[k]] = -2; + g_merge[head_at[k]] = -2; + } + g_merge[lr] = (int32_t)g; + g_merge[lh] = (int32_t)g; + g_merged_rings += grp->count; + g_merged_heads += grp->count; + } + } + } + from = i + 1u; + } + return true; +} + +/** Build the puller program from the two model tokens. @return false = refuse replay */ +static bool td_build_program(void) +{ + const td_token *a = &g_td.model[0], *b = &g_td.model[1]; + size_t queue_table, segment_table, group_table, i; + bool in_segment = false; + uint32_t q; + + g_prog.count = 0u; + g_prog.segments = 0u; + g_prog.ok = true; + for (i = 0; i < 16u; i++) + td_emit(0u); + queue_table = g_prog.count; + for (q = 0; q < g_td.queue_count; q++) { + td_emit((uint32_t)g_td.queues[q].x | ((uint32_t)g_td.queues[q].y << 16)); + td_emit(g_td.queues[q].nonce); + td_emit(g_td.queues[q].mask); + td_emit(0u); + } + segment_table = g_prog.count; + for (i = 0; i < 256u; i++) + td_emit(0u); + if (!td_plan_merges()) + return false; + group_table = g_prog.count; + for (q = 0; q < g_group_count; q++) { + td_emit(g_groups[q].queues[0]); + td_emit(g_groups[q].x_lo); + td_emit(g_groups[q].y_lo); + td_emit(g_groups[q].x_hi); + td_emit(g_groups[q].y_hi); + td_emit(g_groups[q].count); + td_emit(0u); + td_emit(0u); + } + for (i = 0; i < a->count && g_prog.ok; i++) { + const td_event *ea = &a->events[i], *eb = &b->events[i]; + const uint32_t *wa = &a->pool[ea->data_at], *wb = &b->pool[eb->data_at]; + + if (td_is_boundary(ea->cls)) { + if (in_segment) + td_emit(5u); /* segment end */ + in_segment = false; + continue; + } + if (ea->cls == TD_Q_GROUP_R || ea->cls == TD_Q_OTHER_R) + return false; /* a read the model has no meaning for */ + if (g_merge[i] == -2) + continue; /* written by its group's multicast */ + if (!in_segment) { + if (g_prog.segments == 256u) + return false; + g_prog.segment_offset[g_prog.segments++] = (uint32_t)(g_prog.count * 4u); + in_segment = true; + } + switch (ea->cls) { + case TD_Q_RING: { + const uint32_t n = ea->bytes / 32u; + const uint32_t patches = td_count_patches(wa, wb, 8u * n, 1); + uint32_t w8; + + for (w8 = 0; w8 < 8u * n; w8++) + if ((w8 % 8u) != 1u && wa[w8] != wb[w8]) + g_ring_patch_word[w8 % 8u]++; + + td_emit(2u | (n << 8) | (patches << 20)); + td_emit(g_merge[i] >= 0 ? 0x80000000u | (uint32_t)g_merge[i] : ea->queue); + td_emit(ea->index); + td_emit(eb->index - ea->index); + td_emit_words(wa, wb, 8u * n, 1); + break; + } + case TD_Q_HEAD: + td_emit(3u); + td_emit(g_merge[i] >= 0 ? 0x80000000u | (uint32_t)g_merge[i] : ea->queue); + td_emit(wa[0]); + td_emit(wb[0] - wa[0]); + break; + case TD_Q_POLL: { + const uint32_t offset = ea->addr - FT_TT_OP_QUEUE_BASE; + uint32_t word; + + if (offset > 0x14u || offset + ea->bytes <= 0x14u) + break; /* not a count read */ + word = (0x14u - offset) / 4u; + td_emit(4u); + td_emit(ea->queue); + td_emit(wa[word]); + td_emit(wb[word] - wa[word]); + break; + } + default: { /* parameter blocks and any other queue-region write */ + const uint32_t n = ea->bytes / 4u; + const uint32_t patches = td_count_patches(wa, wb, n, 0); + + if (ea->bytes % 4u != 0u || n > 0xFFFu) + return false; + td_emit(1u | (n << 8) | (patches << 20)); + td_emit((uint32_t)ea->x | ((uint32_t)ea->y << 16)); + td_emit(ea->addr); + td_emit_words(wa, wb, n, 0); + break; + } + } + } + if (in_segment) + td_emit(5u); + td_emit(6u); + if (!g_prog.ok) + return false; + for (i = 0; i < g_prog.segments; i++) + g_prog.words[segment_table + i] = g_prog.segment_offset[i]; + g_prog.words[0] = TD_PULL_MAGIC; + g_prog.words[1] = 1u; + g_prog.words[2] = g_td.queue_count; + g_prog.words[3] = (uint32_t)(queue_table * 4u); + g_prog.words[4] = g_prog.segments; + g_prog.words[5] = (uint32_t)(segment_table * 4u); + g_prog.words[14] = g_group_count; + g_prog.words[15] = (uint32_t)(group_table * 4u); + /* The log after the program, 16-byte aligned, up to the scratch. */ + g_prog.words[6] = (uint32_t)((TD_PROGRAM_BASE + g_prog.count * 4u + 15u) & ~15u); + if (g_prog.words[6] >= TD_PROGRAM_LIMIT) + return false; + g_prog.words[7] = TD_PROGRAM_LIMIT - g_prog.words[6]; + while (g_prog.count % 4u) + td_emit(0u); + return g_prog.ok; +} + +static bool td_puller_arm(pn_tt_device *device) +{ + const uint32_t core = pn_tt_env_u32("PN_TT_PULLER_CORE", TD_PULLER_DEFAULT_CORE); + + if (device->puller.running) + return true; + if (!pn_tt_prepare_family_on_core(device, &device->puller, "puller", "puller", + "PN_TT_PULLER_CORE", core, + (1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1)) || + pn_tt_residency_start(&device->puller) != 0) { + fprintf(stderr, "pn_tt: DISPATCH — the puller did not arm on core %u\n", core); + return false; + } + fprintf(stderr, "pn_tt: DISPATCH — puller residency up on core %u\n", core); + return true; +} + +static bool td_upload_program(pn_tt_device *device) +{ + const ft_tt_noc_access *bulk = &device->runtime.bulk_l1; + + if (bulk->write(bulk->user, device->puller.core_x, device->puller.core_y, TD_PROGRAM_BASE, + g_prog.words, g_prog.count * 4u) != FT_TT_OK) { + fprintf(stderr, "pn_tt: DISPATCH — the program did not upload\n"); + return false; + } + fprintf(stderr, "pn_tt: DISPATCH — ring words that move per token: opcode %llu a0 %llu " + "a1 %llu a2 %llu a3 %llu a4 %llu a5 %llu\n", + (unsigned long long)g_ring_patch_word[0], (unsigned long long)g_ring_patch_word[2], + (unsigned long long)g_ring_patch_word[3], (unsigned long long)g_ring_patch_word[4], + (unsigned long long)g_ring_patch_word[5], (unsigned long long)g_ring_patch_word[6], + (unsigned long long)g_ring_patch_word[7]); + fprintf(stderr, "pn_tt: DISPATCH — MULTICAST %u groups; per token %llu ring writes and %llu " + "heads go out as group multicasts\n", + g_group_count, (unsigned long long)g_merged_rings, + (unsigned long long)g_merged_heads); + { + uint32_t gi; + + for (gi = 0; gi < g_group_count && gi < 8u; gi++) + fprintf(stderr, "pn_tt: DISPATCH — group %u: (%u,%u)..(%u,%u), %u cores\n", gi, + g_groups[gi].x_lo, g_groups[gi].y_lo, g_groups[gi].x_hi, g_groups[gi].y_hi, + g_groups[gi].count); + } + fprintf(stderr, "pn_tt: DISPATCH — program %zu B, %u queues, %u segments, log at 0x%x " + "(%u B)\n", + g_prog.count * 4u, g_td.queue_count, g_prog.segments, g_prog.words[6], + g_prog.words[7]); + return true; +} + +static uint64_t g_dry_checked, g_dry_failed, g_dry_words; + +/** Run the whole token k on the puller in DRY-RUN mode and compare its log, stream for + * stream (non-head writes, heads, awaits), with what the host really did this token. */ +static void td_dry_check(pn_tt_device *device, uint64_t forward) +{ + const ft_tt_noc_access *bulk = &device->runtime.bulk_l1; + const uint32_t k = (uint32_t)(forward - g_td.first_forward); + const td_token *t = &g_td.current; + uint32_t params[FT_TT_QUEUE_PARAM_WORDS]; + uint32_t header[16], *log = NULL; + size_t i, at_w = 0u, at_h = 0u, at_a = 0u, log_words; + size_t heads_at[2] = { 0u, 0u }; + bool bad = false; + (void)heads_at; + + memset(params, 0, sizeof(params)); + params[0] = TD_PROGRAM_BASE; + params[1] = pn_tt_red("dispatch_dry_k") ? k + 1u : k; + params[2] = 0u; + params[3] = g_prog.segments - 1u; + params[4] = 1u; + g_dry_checked++; + if (pn_tt_residency_submit(&device->puller, TD_PULL_OPCODE, params, 5000u) != 0 || + bulk->read(bulk->user, device->puller.core_x, device->puller.core_y, TD_PROGRAM_BASE, + header, sizeof(header)) != FT_TT_OK) { + g_dry_failed++; + fprintf(stderr, "pn_tt: DISPATCH DRY forward %llu — the puller did not complete\n", + (unsigned long long)forward); + return; + } + if (header[8] != 0u) { + g_dry_failed++; + fprintf(stderr, "pn_tt: DISPATCH DRY forward %llu — puller error %u at entry +0x%x\n", + (unsigned long long)forward, header[8], header[9]); + return; + } + log_words = header[10] / 4u; + log = malloc((log_words + 4u) * 4u); + if (!log || bulk->read(bulk->user, device->puller.core_x, device->puller.core_y, + g_prog.words[6], log, ((header[10] + 15u) & ~15u)) != FT_TT_OK) { + free(log); + g_dry_failed++; + return; + } + g_dry_words += log_words; + /* Three cursors into the log, one per stream; each advances past records of its type. */ + for (i = 0; i < t->count && !bad; i++) { + const td_event *e = &t->events[i]; + const uint32_t *w = &t->pool[e->data_at]; + size_t *cursor; + uint32_t want_type, n, xy = (uint32_t)e->x | ((uint32_t)e->y << 16), word; + + if (td_is_boundary(e->cls) || e->cls == TD_Q_GROUP_R || e->cls == TD_Q_OTHER_R) + continue; + if (e->cls == TD_Q_POLL) { + const uint32_t offset = e->addr - FT_TT_OP_QUEUE_BASE; + + if (offset > 0x14u || offset + e->bytes <= 0x14u) + continue; + want_type = 4u; + cursor = &at_a; + w += (0x14u - offset) / 4u; + n = 1u; + } else if (e->cls == TD_Q_HEAD) { + want_type = 3u; + cursor = &at_h; + n = 1u; + } else { + want_type = e->cls == TD_Q_RING ? 2u : 1u; + cursor = &at_w; + n = e->bytes / 4u; + } + while (*cursor < log_words) { + const uint32_t type = log[*cursor]; + const bool match_stream = (want_type == 4u && type == 4u) || + (want_type == 3u && type == 3u) || + (want_type <= 2u && type <= 2u); + + if (match_stream) + break; + *cursor += 4u + log[*cursor + 3u]; + } + if (*cursor >= log_words) { + bad = true; + fprintf(stderr, "pn_tt: DISPATCH DRY forward %llu event %zu: the log ran out\n", + (unsigned long long)forward, i); + break; + } + if (log[*cursor] != want_type || log[*cursor + 1u] != xy || + (want_type != 4u && log[*cursor + 2u] != e->addr) || log[*cursor + 3u] != n) { + bad = true; + fprintf(stderr, "pn_tt: DISPATCH DRY forward %llu event %zu %s: log has type %u " + "xy 0x%x addr 0x%x n %u, the host did xy 0x%x addr 0x%x n %u\n", + (unsigned long long)forward, i, td_class_name[e->cls], log[*cursor], + log[*cursor + 1u], log[*cursor + 2u], log[*cursor + 3u], xy, e->addr, n); + break; + } + for (word = 0; word < n; word++) + if (log[*cursor + 4u + word] != w[word]) { + bad = true; + fprintf(stderr, "pn_tt: DISPATCH DRY forward %llu event %zu %s word %u: the " + "puller 0x%08x, the host 0x%08x\n", + (unsigned long long)forward, i, td_class_name[e->cls], word, + log[*cursor + 4u + word], w[word]); + break; + } + *cursor += 4u + n; + } + free(log); + if (bad) + g_dry_failed++; +} + +/* --------------------------------------------------------------------------------------- + * STEP 4 S2: the puller's data movers for the speculative verify + * --------------------------------------------------------------------------------------- */ + +#define TD_ROWCOPY_OPCODE 0x15u +#define TD_FILL_OPCODE 0x16u +#define TD_COPY_OPCODE 0x17u +/* One FILL descriptor's ceiling: the kernel's `bytes` is 32-bit; 256 MiB keeps each one + * well inside the submit timeout. */ +#define TD_FILL_MAX_BYTES (256u << 20) + +/* S2 data-mover accounting (pn_tt_move_stats). */ +static struct { uint64_t rows; double row_ms; } g_move; + +void pn_tt_move_stats(uint64_t *rows, double *row_ms) +{ + *rows = g_move.rows; + *row_ms = g_move.row_ms; +} + +/* --------------------------------------------------------------------------------------- + * S2d: STREAMED SEGMENTS. No model: while streaming, the host runs its code exactly as + * always, but its queue writes (parameter blocks, ring slots, heads), its DRAM writes and its + * aligned L1 writes are CAPTURED as literal puller entries, and its completion polls are + * answered at once (count = the queue's last head, token from the queue's nonce) while an + * AWAIT entry makes the puller wait for the same on the card. A host READ of data (or + * anything the stream cannot express) is a boundary: the captured segment is uploaded to the + * puller in one write, run, awaited, and the access then goes to the card for real. So the + * card's await->issue latency inside a segment is the puller's, not the host's round trip. + * --------------------------------------------------------------------------------------- */ + +/* The program arena is cut into TS_REGIONS regions; a captured chunk is launched into the + * next one WITHOUT waiting once it reaches TS_CHUNK_WORDS (the card runs it while the host + * captures the next), so only a host data read waits. A region is reused only after the + * PULL descriptor that ran it has completed. */ +#define TS_REGIONS 12u +#define TS_INFLIGHT 6u +#define TS_REGION_WORDS (((TD_PROGRAM_LIMIT - TD_PROGRAM_BASE) / 4u / TS_REGIONS) & ~3u) +#define TS_MAX_QUEUES 256u +#define TS_ENTRY_BASE (16u + 4u * TS_MAX_QUEUES + 4u) +#define TS_MAX_WORDS TS_REGION_WORDS +/* A chunk launches early (`PN_TT_STREAM_CHUNK` words, default 512: run_s2d_chunk.sh) so the card starts while + * the host captures the rest. */ +static uint32_t ts_chunk_words(void) +{ + static uint32_t chunk; + + if (chunk == 0u) { + const char *env = getenv("PN_TT_STREAM_CHUNK"); + const uint32_t cap = (uint32_t)(TS_REGION_WORDS - TS_ENTRY_BASE - 4096u); + + chunk = env && atoi(env) > 0 ? (uint32_t)atoi(env) : 512u; + if (chunk > cap) + chunk = cap; + } + return chunk; +} +/* After a boundary the card is idle: launch the FIRST chunk sooner (`PN_TT_STREAM_FIRST` + * words, default = the chunk size). */ +static bool g_ts_fresh; +static uint32_t ts_first_words(void) +{ + static uint32_t first; + + if (first == 0u) { + const char *env = getenv("PN_TT_STREAM_FIRST"); + + first = env && atoi(env) > 0 ? (uint32_t)atoi(env) : ts_chunk_words(); + } + return first; +} +#define TS_CHUNK_WORDS (g_ts_fresh ? ts_first_words() : ts_chunk_words()) + +typedef struct { + uint16_t x, y; + uint32_t nonce; + bool nonce_known; + uint32_t issued; /* the last head written (or read) */ + bool issued_known; + uint32_t ring_token; /* the token word of the last ring slot written since the head */ + bool ring_pending; + uint32_t heartbeat; +} ts_queue; + +static struct { + pn_tt_device *device; + uint32_t *words; /* entries at TS_ENTRY_BASE (the capture buffer) */ + uint32_t *out; /* the compact program a launch uploads */ + size_t count; /* entry words */ + ts_queue queues[TS_MAX_QUEUES]; + uint32_t queue_count; + bool failed; + uint32_t region; /* the next region to launch into */ + uint32_t region_done_at[TS_REGIONS]; /* puller issued count that ran the region (0 free) */ + uint32_t launched, completed; /* PULLs launched / known complete (puller counts) */ + uint64_t segments, entries, boundaries, polls_answered, launched_words, async_launches; + double flush_ms, upload_ms, capture_ms, wait_ms; + /* S2e census: entries by kind, words by kind, and WRITEs a recent one duplicates + * (same address, bytes and data, another core): multicast candidates. */ + uint64_t n_param, n_ring, n_dram, n_l1, n_head, n_await, n_rowcopy; + uint64_t w_param, w_ring, w_dram, w_l1, dup_writes, dup_words, dup_same_row; + struct { uint16_t x, y; uint32_t addr, bytes; uint64_t hash; } recent[32]; + uint32_t recent_at; + double last_flush_end; + uint32_t puller_busy0, puller_await0; /* control words 62/63 at stream begin */ + uint64_t bound_by_class[TD_CLASSES], bound_bytes[TD_CLASSES]; + double bound_ms_by_class[TD_CLASSES]; + struct { uint16_t x, y; uint32_t addr, bytes; uint64_t n; double ms; } l1r[16]; + uint32_t l1r_count; + double bound_drain_ms, bound_access_ms; + double puller_busy_ms, puller_await_ms; + /* PM 8 AWAIT census (`PN_TT_PULLER_AWAIT_CENSUS=1`): the puller's per-queue table read at + * stream begin and end, the differences summed here by queue index. */ + uint32_t census0[2u * TS_MAX_QUEUES]; + double census_ms[TS_MAX_QUEUES]; + uint64_t census_awaits[TS_MAX_QUEUES]; +} g_ts; + +/* The puller kernel's PULL_AWAIT_TABLE (resident_puller.c). */ +#define TS_AWAIT_TABLE 0x16D000u + +static bool ts_census_enabled(void) +{ + static int enabled = -1; + + if (enabled < 0) { + const char *env = getenv("PN_TT_PULLER_AWAIT_CENSUS"); + + enabled = env && atoi(env) != 0; + } + return enabled == 1; +} + +static void ts_census_read(uint32_t *table) +{ + const ft_tt_noc_access *noc = &g_td.original[TD_ACC_NOC]; + pn_tt_residency *puller = &g_ts.device->puller; + + (void)noc->read(noc->user, puller->core_x, puller->core_y, TS_AWAIT_TABLE, table, + 2u * TS_MAX_QUEUES * sizeof(uint32_t)); +} + +/** Add the puller's per-queue AWAIT time since the stream began to the census. */ +static void ts_census_accumulate(void) +{ + uint32_t now[2u * TS_MAX_QUEUES]; + uint32_t q; + + ts_census_read(now); + for (q = 0; q < TS_MAX_QUEUES; q++) { + g_ts.census_ms[q] += (double)(uint32_t)(now[2u * q] - g_ts.census0[2u * q]) * 16.0 / + 1.35e6; + g_ts.census_awaits[q] += (uint32_t)(now[2u * q + 1u] - g_ts.census0[2u * q + 1u]); + } +} + +/** Print the census by family (the core's label), totals over the run. */ +static void ts_census_report(void) +{ + double total = 0.0; + uint32_t q; + + if (!ts_census_enabled()) + return; + for (q = 0; q < g_ts.queue_count; q++) + total += g_ts.census_ms[q]; + fprintf(stderr, "pn_tt: AWAIT CENSUS — %.1f ms the puller spent in AWAIT polls, by queue:\n", + total); + for (q = 0; q < g_ts.queue_count; q++) + if (g_ts.census_awaits[q] != 0u) + fprintf(stderr, "pn_tt: await (%2u,%2u) %-22s %8.1f ms over %7llu awaits " + "(%.2f us each)\n", + g_ts.queues[q].x, g_ts.queues[q].y, + pn_tt_core_label(g_ts.queues[q].x, g_ts.queues[q].y), g_ts.census_ms[q], + (unsigned long long)g_ts.census_awaits[q], + g_ts.census_ms[q] * 1000.0 / (double)g_ts.census_awaits[q]); +} + +static uint32_t ts_queue_of(uint16_t x, uint16_t y) +{ + uint32_t i; + + for (i = 0; i < g_ts.queue_count; i++) + if (g_ts.queues[i].x == x && g_ts.queues[i].y == y) + return i; + if (g_ts.queue_count == TS_MAX_QUEUES) + return TS_MAX_QUEUES; + memset(&g_ts.queues[i], 0, sizeof(g_ts.queues[i])); + g_ts.queues[i].x = x; + g_ts.queues[i].y = y; + return g_ts.queue_count++; +} + +static bool ts_room(size_t words) +{ + return g_ts.words && TS_ENTRY_BASE + g_ts.count + words + 2u <= TS_MAX_WORDS; +} + +static bool ts_flush(void); +static bool ts_launch(bool wait); + +static bool ts_emit(const uint32_t *w, size_t n) +{ + if ((!ts_room(n) || g_ts.count >= TS_CHUNK_WORDS) && !ts_launch(false)) + return false; + if (!ts_room(n)) + return false; + memcpy(&g_ts.words[TS_ENTRY_BASE + g_ts.count], w, n * 4u); + g_ts.count += n; + g_ts.entries++; + return true; +} + +/** Wait until the puller has completed `target` PULLs (its completed count, read directly). */ +static bool ts_wait_puller(uint32_t target) +{ + pn_tt_residency *puller = &g_ts.device->puller; + const ft_tt_noc_access *noc = &g_td.original[TD_ACC_NOC]; + const double started = pn_time_ms(); + uint32_t fields[8]; + + if ((int32_t)(g_ts.completed - target) >= 0) + return true; + for (;;) { + if (noc->read(noc->user, puller->core_x, puller->core_y, FT_TT_OP_QUEUE_BASE + 0x10u, + fields, 16u) != FT_TT_OK) + return false; + if (fields[3] == (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON) + return false; + if ((int32_t)(fields[1] - target) >= 0) { + g_ts.completed = fields[1]; + break; + } + if (pn_time_ms() - started > 20000.0) + return false; + } + g_ts.wait_ms += pn_time_ms() - started; + return true; +} + +/** Upload the captured chunk into the next region and launch it; `wait`: and wait for every + * launched chunk (the puller's own await, so its completion bookkeeping stays exact). */ +static bool ts_launch(bool wait) +{ + pn_tt_device *device = g_ts.device; + pn_tt_residency *puller; + const ft_tt_noc_access *bulk = &g_td.original[TD_ACC_BULK_L1]; + uint32_t params[FT_TT_QUEUE_PARAM_WORDS]; + uint32_t *w = g_ts.words, q, base; + size_t total; + double started; + bool ok = true; + + if (g_ts.failed) + return false; + started = pn_time_ms(); + puller = &device->puller; + g_td.internal = true; + if (g_ts.count > 0u) { + const uint32_t r = g_ts.region; + + /* the region must be free, and the puller's ring must have room */ + if (g_ts.region_done_at[r] != 0u && !ts_wait_puller(g_ts.region_done_at[r])) + ok = false; + if (ok && g_ts.launched - g_ts.completed >= TS_INFLIGHT && + !ts_wait_puller(g_ts.launched - TS_INFLIGHT + 1u)) + ok = false; + base = TD_PROGRAM_BASE + r * TS_REGION_WORDS * 4u; + if (g_ts.last_flush_end > 0.0) + g_ts.capture_ms += started - g_ts.last_flush_end; + w[TS_ENTRY_BASE + g_ts.count++] = 5u; /* segment end */ + w[TS_ENTRY_BASE + g_ts.count++] = 6u; /* program end */ + { + /* COMPACT: header, the queues actually known, the segment table, the entries — + * a fixed 256-queue table was 1,044 words a launch. */ + const uint32_t table = 16u, seg = table + 4u * g_ts.queue_count; + const uint32_t entries = (seg + 1u + 3u) & ~3u; + uint32_t *o = g_ts.out; + + memset(o, 0, (size_t)entries * 4u); + o[0] = TD_PULL_MAGIC; + o[1] = 1u; + o[2] = g_ts.queue_count; + o[3] = table * 4u; + o[4] = 1u; + o[5] = seg * 4u; + for (q = 0; q < g_ts.queue_count; q++) { + o[table + 4u * q] = (uint32_t)g_ts.queues[q].x | ((uint32_t)g_ts.queues[q].y << 16); + o[table + 4u * q + 1u] = g_ts.queues[q].nonce; + o[table + 4u * q + 2u] = 0xFFFFu; + } + o[seg] = entries * 4u; + memcpy(&o[entries], &w[TS_ENTRY_BASE], g_ts.count * 4u); + total = entries + g_ts.count; + o[6] = base + (uint32_t)(TS_REGION_WORDS * 4u); + o[7] = 0u; + w = o; + } + { + const double up = pn_time_ms(); + + if (ok && bulk->write(bulk->user, puller->core_x, puller->core_y, base, w, + (total * 4u + 15u) & ~(size_t)15u) != FT_TT_OK) + ok = false; + g_ts.upload_ms += pn_time_ms() - up; + } + memset(params, 0, sizeof(params)); + params[0] = base; + /* Each queued PULL keeps ITS OWN parameter block: with one slot, a launch queued + * behind another rewrote the base the earlier one had not read yet (chunked launches + * skipped regions — error 4 at an await). */ + puller->param_slot = 1u + (g_ts.launched % 8u); + if (ok && pn_tt_residency_enqueue(puller, TD_PULL_OPCODE, params, 1u) != 0) + ok = false; + puller->param_slot = 0u; + g_ts_fresh = false; + if (ok) { + g_ts.launched++; + g_ts.region_done_at[r] = g_ts.launched; + g_ts.region = (r + 1u) % TS_REGIONS; + g_ts.async_launches++; + } + g_ts.launched_words += total; + g_ts.count = 0u; + g_ts.segments++; + } + if (ok && wait && g_ts.launched != g_ts.completed) { + if (pn_tt_residency_await(puller, 20000u) != 0) { + uint32_t header[16]; + + for (q = 0; q < TS_REGIONS; q++) + if (bulk->read(bulk->user, puller->core_x, puller->core_y, + TD_PROGRAM_BASE + q * TS_REGION_WORDS * 4u, header, + sizeof(header)) == FT_TT_OK && header[8] != 0u) + fprintf(stderr, "pn_tt: STREAM — region %u failed on the puller: error %u at " + "entry +0x%x (last await count %u)\n", q, header[8], + header[9], header[11]); + ok = false; + } else { + g_ts.completed = g_ts.launched; + } + } + g_td.internal = false; + g_ts.flush_ms += pn_time_ms() - started; + if (wait) { + g_ts.last_flush_end = pn_time_ms(); + g_ts_fresh = true; + } + if (!ok) + g_ts.failed = true; + return ok; +} + +/** Everything captured runs on the card and is done. */ +static bool ts_flush(void) +{ + return ts_launch(true); +} + +/** `PN_TT_STREAM_DIRECT` (default 1): WRITE DIRECT entries (no scratch copy on the puller). */ +static bool ts_direct(void) +{ + static int direct = -1; + + if (direct < 0) + direct = !getenv("PN_TT_STREAM_DIRECT") || atoi(getenv("PN_TT_STREAM_DIRECT")) != 0; + return direct != 0; +} + +static bool ts_write_entry(uint16_t x, uint16_t y, uint64_t addr, const void *src, size_t bytes) +{ + const uint8_t *p = src; + + while (bytes) { + const size_t chunk = bytes > 4000u * 4u ? 4000u * 4u : bytes; + uint32_t head[3]; + + if ((!ts_room(3u + chunk / 4u) || g_ts.count >= TS_CHUNK_WORDS) && !ts_launch(false)) + return false; + if (!ts_room(6u + chunk / 4u)) + return false; + if (ts_direct()) { + /* WRITE DIRECT: the payload starts at a 16 B boundary of the uploaded program + * (entries start 16 B-aligned), as its destination does. */ + const uint32_t pad = (uint32_t)((4u - (g_ts.count + 3u) % 4u) % 4u); + + head[0] = 8u | ((uint32_t)(chunk / 4u) << 8) | (pad << 20); + head[1] = (uint32_t)x | ((uint32_t)y << 16); + head[2] = (uint32_t)addr; + memcpy(&g_ts.words[TS_ENTRY_BASE + g_ts.count], head, sizeof(head)); + memset(&g_ts.words[TS_ENTRY_BASE + g_ts.count + 3u], 0, pad * 4u); + memcpy(&g_ts.words[TS_ENTRY_BASE + g_ts.count + 3u + pad], p, chunk); + g_ts.count += 3u + pad + chunk / 4u; + } else { + head[0] = 1u | ((uint32_t)(chunk / 4u) << 8); + head[1] = (uint32_t)x | ((uint32_t)y << 16); + head[2] = (uint32_t)addr; + memcpy(&g_ts.words[TS_ENTRY_BASE + g_ts.count], head, sizeof(head)); + memcpy(&g_ts.words[TS_ENTRY_BASE + g_ts.count + 3u], p, chunk); + g_ts.count += 3u + chunk / 4u; + } + g_ts.entries++; + p += chunk; + addr += chunk; + bytes -= chunk; + } + return true; +} + +static void ts_census(uint8_t cls, uint16_t x, uint16_t y, uint64_t addr, const void *src, + size_t bytes) +{ + const uint8_t *p = src; + uint64_t hash = 1469598103934665603ull; + uint32_t i; + size_t b; + + switch (cls) { + case TD_Q_RING: g_ts.n_ring++; g_ts.w_ring += bytes / 4u; break; + case TD_Q_PARAM_ROT: + case TD_Q_PARAM_CACHE: g_ts.n_param++; g_ts.w_param += bytes / 4u; break; + case TD_B_DRAM_W: g_ts.n_dram++; g_ts.w_dram += bytes / 4u; return; + default: g_ts.n_l1++; g_ts.w_l1 += bytes / 4u; return; + } + for (b = 0; b < bytes; b++) + hash = (hash ^ p[b]) * 1099511628211ull; + for (i = 0; i < 32u; i++) { + const uint32_t j = i; + + if (g_ts.recent[j].hash == hash && g_ts.recent[j].addr == (uint32_t)addr && + g_ts.recent[j].bytes == (uint32_t)bytes && + (g_ts.recent[j].x != x || g_ts.recent[j].y != y)) { + g_ts.dup_writes++; + g_ts.dup_words += bytes / 4u; + if (g_ts.recent[j].y == y) + g_ts.dup_same_row++; + break; + } + } + g_ts.recent[g_ts.recent_at % 32u].x = x; + g_ts.recent[g_ts.recent_at % 32u].y = y; + g_ts.recent[g_ts.recent_at % 32u].addr = (uint32_t)addr; + g_ts.recent[g_ts.recent_at % 32u].bytes = (uint32_t)bytes; + g_ts.recent[g_ts.recent_at % 32u].hash = hash; + g_ts.recent_at++; +} + +static bool td_stream_write(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, + const void *src, size_t bytes) +{ + const uint8_t cls = td_classify(acc, addr, true); + const bool aligned = (addr % 16u) == 0u && (bytes % 4u) == 0u && bytes > 0u; + + if (aligned && cls != TD_Q_HEAD) + ts_census(cls, x, y, addr, src, bytes); + + if (g_ts.failed) + return false; + switch (cls) { + case TD_Q_RING: + case TD_Q_PARAM_ROT: + case TD_Q_PARAM_CACHE: { + const uint32_t q = ts_queue_of(x, y); + + if (q == TS_MAX_QUEUES || !aligned) + break; + if (cls == TD_Q_RING && bytes >= 32u) { + /* the LAST slot's token: with the next head it names the queue's nonce */ + memcpy(&g_ts.queues[q].ring_token, (const uint8_t *)src + bytes - 32u + 4u, 4u); + g_ts.queues[q].ring_pending = true; + } + return ts_write_entry(x, y, addr, src, bytes); + } + case TD_Q_HEAD: { + const uint32_t q = ts_queue_of(x, y); + uint32_t value, entry[4]; + + if (q == TS_MAX_QUEUES || bytes != 4u) + break; + memcpy(&value, src, 4u); + if (g_ts.queues[q].ring_pending) { + g_ts.queues[q].nonce = g_ts.queues[q].ring_token ^ (value * TD_TOKEN_MULTIPLIER); + g_ts.queues[q].nonce_known = true; + g_ts.queues[q].ring_pending = false; + } + g_ts.queues[q].issued = value; + g_ts.queues[q].issued_known = true; + entry[0] = 3u; + entry[1] = q; + entry[2] = value; + entry[3] = 0u; + g_ts.n_head++; + return ts_emit(entry, 4u); + } + case TD_B_DRAM_W: + case TD_B_L1_W: + if (!aligned) + break; + return ts_write_entry(x, y, addr, src, bytes); + default: + break; + } + /* Not expressible: run what was captured, then do this one for real. */ + g_ts.boundaries++; + g_ts.bound_by_class[cls]++; + { + const double t = pn_time_ms(); + + (void)ts_flush(); + g_ts.bound_drain_ms += pn_time_ms() - t; + } + return false; +} + +static bool td_stream_read(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, void *dst, + size_t bytes) +{ + const uint8_t cls = td_classify(acc, addr, false); + + if (!g_ts.failed && cls == TD_Q_POLL && (bytes % 4u) == 0u) { + const uint32_t q = ts_queue_of(x, y); + ts_queue *queue = q < TS_MAX_QUEUES ? &g_ts.queues[q] : NULL; + + if (queue && queue->nonce_known && queue->issued_known && queue->issued > 0u) { + uint32_t window[16], entry[4]; + const uint32_t first = (uint32_t)((addr - FT_TT_OP_QUEUE_BASE) / 4u); + const size_t last = g_ts.count; + uint32_t token = queue->nonce ^ (queue->issued * TD_TOKEN_MULTIPLIER); + + if (token == 0u || token == (uint32_t)FT_TT_QUEUE_STATE_HOST_POISON) + token = queue->nonce ^ 0xA5A5A5A5u; + /* The puller waits for the same count (once a run of polls). */ + if (!(last >= 4u && g_ts.words[TS_ENTRY_BASE + last - 4u] == 4u && + g_ts.words[TS_ENTRY_BASE + last - 3u] == q && + g_ts.words[TS_ENTRY_BASE + last - 2u] == queue->issued)) { + entry[0] = 4u; + entry[1] = q; + /* RED `stream_await_early`: the puller waits for one descriptor LESS than the + * host was told is done — the next family starts on a producer's unfinished + * output; must DIFFER (never more than one behind, so no ring overrun). */ + entry[2] = queue->issued - (pn_tt_red("stream_await_early") && queue->issued > 1u + ? 1u : 0u); + entry[3] = 0u; + g_ts.n_await++; + if (!ts_emit(entry, 4u)) + return false; + } + memset(window, 0, sizeof(window)); + window[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] = queue->issued; + window[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN] = token; + window[FT_TT_QUEUE_FIELD_STATE] = (uint32_t)FT_TT_QUEUE_STATE_IDLE; + window[FT_TT_QUEUE_FIELD_HEARTBEAT] = ++queue->heartbeat; + window[FT_TT_QUEUE_FIELD_HEAD] = queue->issued; + window[FT_TT_QUEUE_FIELD_TAIL] = queue->issued; + if (first + bytes / 4u <= 16u) { + memcpy(dst, &window[first], bytes); + g_ts.polls_answered++; + return true; + } + } + } + /* A real read: everything captured must have run on the card first. */ + g_ts.boundaries++; + g_ts.bound_by_class[cls]++; + { + const double t = pn_time_ms(); + const bool flushed = ts_flush(); + + g_ts.bound_drain_ms += pn_time_ms() - t; + if (!flushed) + return false; + } + { + const double t_read = pn_time_ms(); + const int rc = g_td.original[acc].read(g_td.original[acc].user, x, y, addr, dst, bytes); + + g_ts.bound_access_ms += pn_time_ms() - t_read; + g_ts.bound_ms_by_class[cls] += pn_time_ms() - t_read; + g_ts.bound_bytes[cls] += bytes; + if (cls == TD_B_L1_R) { + uint32_t i; + static int traced; + + /* `PN_TT_STREAM_TRACE_READ=`: who reads it (once, a backtrace). */ + if (!traced && getenv("PN_TT_STREAM_TRACE_READ") && + (size_t)atoi(getenv("PN_TT_STREAM_TRACE_READ")) == bytes) { + void *frames[16]; + + traced = 1; + fprintf(stderr, "pn_tt: STREAM traced read (%u,%u) 0x%llx %zu B:\n", x, y, + (unsigned long long)addr, bytes); + backtrace_symbols_fd(frames, backtrace(frames, 16), 2); + } + for (i = 0; i < g_ts.l1r_count; i++) + if (g_ts.l1r[i].addr == (uint32_t)addr && g_ts.l1r[i].bytes == (uint32_t)bytes) + break; + if (i == g_ts.l1r_count && i < 16u) { + g_ts.l1r[i].x = x; + g_ts.l1r[i].y = y; + g_ts.l1r[i].addr = (uint32_t)addr; + g_ts.l1r[i].bytes = (uint32_t)bytes; + g_ts.l1r_count++; + } + if (i < 16u) { + g_ts.l1r[i].n++; + g_ts.l1r[i].ms += pn_time_ms() - t_read; + } + } + + /* A real completion read teaches the queue's nonce and count. */ + if (rc == FT_TT_OK && cls == TD_Q_POLL && (bytes % 4u) == 0u) { + const uint32_t q = ts_queue_of(x, y); + const uint32_t first = (uint32_t)((addr - FT_TT_OP_QUEUE_BASE) / 4u); + + if (q < TS_MAX_QUEUES && first <= FT_TT_QUEUE_FIELD_COMPLETED_COUNT && + first + bytes / 4u > FT_TT_QUEUE_FIELD_COMPLETED_TOKEN) { + const uint32_t *f = (const uint32_t *)dst; + const uint32_t count = f[FT_TT_QUEUE_FIELD_COMPLETED_COUNT - first]; + const uint32_t token = f[FT_TT_QUEUE_FIELD_COMPLETED_TOKEN - first]; + + if (count > 0u && !g_ts.queues[q].nonce_known) { + g_ts.queues[q].nonce = token ^ (count * TD_TOKEN_MULTIPLIER); + g_ts.queues[q].nonce_known = true; + } + } + } + if (rc != FT_TT_OK) + g_ts.failed = true; + /* the caller's own read must not run twice */ + return rc == FT_TT_OK; + } +} + +static bool td_stream_rowcopy(const uint32_t *params) +{ + uint32_t entry[10]; + + entry[0] = 7u; + memcpy(&entry[1], params, 9u * 4u); + g_ts.n_rowcopy++; + return ts_emit(entry, 10u); +} + +static void ts_puller_clocks(uint32_t *busy, uint32_t *await); + +bool pn_tt_dispatch_stream_begin(pn_tt_device *device) +{ + const char *env = getenv("PN_TT_SPEC_STREAM"); + + if (!device || (env && atoi(env) == 0) || !td_puller_arm(device) || g_td.replaying || + g_td.recording) + return false; + if (!g_ts.words) { + g_ts.words = calloc(TS_MAX_WORDS, sizeof(uint32_t)); + g_ts.out = calloc(TS_MAX_WORDS, sizeof(uint32_t)); + if (!g_ts.words || !g_ts.out) + return false; + } + g_ts.device = device; + g_ts.count = 0u; + g_ts.failed = false; + /* every region is free between streams (each stream ends with a full wait) */ + memset(g_ts.region_done_at, 0, sizeof(g_ts.region_done_at)); + g_ts.launched = g_ts.completed = device->puller.queue.issued; + g_ts.last_flush_end = pn_time_ms(); + /* The streamed program overwrites the puller's recorded decode program: re-learn it. */ + g_td.sequence_reset = true; + td_wrap(device); + ts_puller_clocks(&g_ts.puller_busy0, &g_ts.puller_await0); + if (ts_census_enabled()) + ts_census_read(g_ts.census0); + g_streaming = true; + return true; +} + +static void ts_puller_clocks(uint32_t *busy, uint32_t *await) +{ + const ft_tt_noc_access *noc = &g_td.original[TD_ACC_NOC]; + pn_tt_residency *puller = &g_ts.device->puller; + uint32_t w[2] = { 0u, 0u }; + + (void)noc->read(noc->user, puller->core_x, puller->core_y, FT_TT_OP_QUEUE_BASE + 62u * 4u, + w, 8u); + *busy = w[0]; + *await = w[1]; +} + +bool pn_tt_dispatch_stream_end(void) +{ + bool ok; + + if (!g_streaming) + return true; + ok = ts_flush() && !g_ts.failed; + { + uint32_t busy, wait; + + ts_puller_clocks(&busy, &wait); + g_ts.puller_busy_ms += (double)(uint32_t)(busy - g_ts.puller_busy0) * 16.0 / 1.35e6; + g_ts.puller_await_ms += (double)(uint32_t)(wait - g_ts.puller_await0) * 16.0 / 1.35e6; + if (ts_census_enabled()) + ts_census_accumulate(); + } + g_streaming = false; + td_unwrap(); + /* What the host thinks it issued stays what it issued; a failure leaves no trust. */ + if (!ok) + g_ts.queue_count = 0u; + return ok; +} + +bool pn_tt_dispatch_stream_kick(void) +{ + if (!g_streaming || g_ts.failed) + return g_streaming ? false : true; + return g_ts.count == 0u || ts_launch(false); +} + +void pn_tt_dispatch_stream_report(double cycles) +{ + ts_census_report(); + if (g_ts.segments == 0u || cycles <= 0.0) + return; + fprintf(stderr, "pn_tt: STREAM — a cycle: %.1f segments, %.0f entries, %.1f boundaries, %.0f " + "polls answered, %.0f program words, %.1f launches; %.1f ms in launch+wait (upload " + "%.1f, region waits %.1f), %.1f ms host between flushes\n", + (double)g_ts.segments / cycles, (double)g_ts.entries / cycles, + (double)g_ts.boundaries / cycles, (double)g_ts.polls_answered / cycles, + (double)g_ts.launched_words / cycles, (double)g_ts.async_launches / cycles, + g_ts.flush_ms / cycles, g_ts.upload_ms / cycles, g_ts.wait_ms / cycles, + g_ts.capture_ms / cycles); + fprintf(stderr, "pn_tt: STREAM census a cycle: param %.0f (%.0f w), ring %.0f (%.0f w), dram %.0f " + "(%.0f w), l1 %.0f (%.0f w), head %.0f, await %.0f, rowcopy %.0f; " + "duplicate writes %.0f (%.0f w), %.0f in the same NoC row\n", + g_ts.n_param / cycles, g_ts.w_param / cycles, g_ts.n_ring / cycles, + g_ts.w_ring / cycles, g_ts.n_dram / cycles, g_ts.w_dram / cycles, + g_ts.n_l1 / cycles, g_ts.w_l1 / cycles, g_ts.n_head / cycles, g_ts.n_await / cycles, + g_ts.n_rowcopy / cycles, g_ts.dup_writes / cycles, g_ts.dup_words / cycles, + g_ts.dup_same_row / cycles); + { + unsigned c; + + fprintf(stderr, "pn_tt: STREAM boundaries a cycle:"); + for (c = 0; c < TD_CLASSES; c++) + if (g_ts.bound_by_class[c]) + fprintf(stderr, " %s %.1f (%.0f B, %.1f ms)", td_class_name[c], + g_ts.bound_by_class[c] / cycles, g_ts.bound_bytes[c] / cycles, + g_ts.bound_ms_by_class[c] / cycles); + fprintf(stderr, " | drain %.1f ms, the accesses %.1f ms\n", g_ts.bound_drain_ms / cycles, + g_ts.bound_access_ms / cycles); + fprintf(stderr, "pn_tt: STREAM L1 reads a cycle:"); + for (c = 0; c < g_ts.l1r_count; c++) + fprintf(stderr, " [(%u,%u) 0x%x %u B x%.1f %.2f ms]", g_ts.l1r[c].x, g_ts.l1r[c].y, + g_ts.l1r[c].addr, g_ts.l1r[c].bytes, g_ts.l1r[c].n / cycles, + g_ts.l1r[c].ms / cycles); + fprintf(stderr, "\n"); + } + fprintf(stderr, "pn_tt: STREAM puller a cycle: %.1f ms inside PULLs, %.1f of them in AWAIT " + "polls (%.1f issuing)\n", g_ts.puller_busy_ms / cycles, + g_ts.puller_await_ms / cycles, (g_ts.puller_busy_ms - g_ts.puller_await_ms) / cycles); +} + +/** + * Submit one data-mover descriptor (ROWCOPY / FILL / COPY) to the puller and wait for it. + * + * AN IDLE PULLER IS RETIRED BY THE SUBMIT ITSELF (residency_idle_retire: idle past the limit, + * ~5 s), which then refuses the descriptor. Every data mover after a user's pause hit that: the + * FILL fell back to the 0.43 s link zero and the checkpoint restore failed (run_serve_prefill_ + * ckpt.sh, 12 s pause). Re-arm once and resubmit, re-uploading the replay program when there is + * one, as td_launch_segment() does. RED `puller_no_rearm` keeps the refusal. + */ +static int td_puller_submit(pn_tt_device *device, uint32_t opcode, const uint32_t *params) +{ + if (!td_puller_arm(device)) + return -1; + if (pn_tt_residency_submit(&device->puller, opcode, params, 5000u) == 0) + return 0; + if (pn_tt_red("puller_no_rearm") || device->puller.running || !td_puller_arm(device) || + (g_prog.count > 0u && !td_upload_program(device))) + return -1; + g_td.puller_rearms++; + fprintf(stderr, "pn_tt: DISPATCH — the puller was idle-retired; re-armed for a data " + "mover\n"); + return pn_tt_residency_submit(&device->puller, opcode, params, 5000u); +} + +int pn_tt_row_copy(pn_tt_device *device, const pn_tt_activation *src, uint32_t src_row, + const pn_tt_activation *dst, uint32_t dst_row) +{ + uint32_t params[FT_TT_QUEUE_PARAM_WORDS]; + uint32_t replica; + double started; + + if (!device || !src || !dst || src->tile_count == 0u || src->tile_count != dst->tile_count || + src->bank >= device->dram_bank_count || !td_puller_arm(device)) + return -1; + /* EVERY MIRROR of the destination: a stale mirror is a plausible wrong answer. */ + for (replica = 0; replica < dst->replica_count; replica++) { + const uint32_t bank = dst->replica_bank[replica]; + + memset(params, 0, sizeof(params)); + params[0] = device->dram[src->bank].x; + params[1] = device->dram[src->bank].y; + params[2] = src->address; + params[3] = src_row; + params[4] = device->dram[bank].x; + params[5] = device->dram[bank].y; + params[6] = dst->replica_address[replica]; + params[7] = dst_row; + params[8] = src->tile_count; + started = pn_time_ms(); + if (g_streaming) { + /* S2d: inline in the streamed segment (the puller runs it in order). */ + if (!td_stream_rowcopy(params)) + return -1; + } else if (td_puller_submit(device, TD_ROWCOPY_OPCODE, params) != 0) + return -1; + g_move.row_ms += pn_time_ms() - started; + g_move.rows++; + } + return 0; +} + +int pn_tt_fill_dram_oncard(pn_tt_device *device, uint32_t bank, uint32_t address, + size_t bytes, uint32_t value) +{ + uint32_t params[FT_TT_QUEUE_PARAM_WORDS]; + size_t offset; + + if (!device || bank >= device->dram_bank_count || bytes == 0u || (address & 63u) != 0u || + (bytes & 63u) != 0u || g_streaming || pn_tt_matvec_flush(device) != 0 || + !td_puller_arm(device)) + return -1; + for (offset = 0; offset < bytes; offset += TD_FILL_MAX_BYTES) { + const size_t n = bytes - offset < TD_FILL_MAX_BYTES ? bytes - offset : TD_FILL_MAX_BYTES; + + memset(params, 0, sizeof(params)); + params[0] = device->dram[bank].x; + params[1] = device->dram[bank].y; + params[2] = address + (uint32_t)offset; + params[3] = (uint32_t)n; + params[4] = value; + if (td_puller_submit(device, TD_FILL_OPCODE, params) != 0) + return -1; + } + return 0; +} + +int pn_tt_copy_dram_oncard(pn_tt_device *device, uint32_t src_bank, uint32_t src_address, + uint32_t dst_bank, uint32_t dst_address, size_t bytes) +{ + uint32_t params[FT_TT_QUEUE_PARAM_WORDS]; + size_t offset; + + if (!device || src_bank >= device->dram_bank_count || dst_bank >= device->dram_bank_count || + bytes == 0u || ((src_address | dst_address) & 63u) != 0u || (bytes & 63u) != 0u || + g_streaming || pn_tt_matvec_flush(device) != 0 || !td_puller_arm(device)) + return -1; + for (offset = 0; offset < bytes; offset += TD_FILL_MAX_BYTES) { + const size_t n = bytes - offset < TD_FILL_MAX_BYTES ? bytes - offset : TD_FILL_MAX_BYTES; + + memset(params, 0, sizeof(params)); + params[0] = device->dram[src_bank].x; + params[1] = device->dram[src_bank].y; + params[2] = src_address + (uint32_t)offset; + params[3] = device->dram[dst_bank].x; + params[4] = device->dram[dst_bank].y; + params[5] = dst_address + (uint32_t)offset; + params[6] = (uint32_t)n; + if (td_puller_submit(device, TD_COPY_OPCODE, params) != 0) + return -1; + } + return 0; +} + +/* --------------------------------------------------------------------------------------- + * Observe-mode report (A1) + * --------------------------------------------------------------------------------------- */ + +static void td_report(const td_token *t, uint64_t forward) +{ + uint64_t events[TD_CLASSES] = { 0 }, bytes[TD_CLASSES] = { 0 }, reads[TD_CLASSES] = { 0 }; + size_t i, segments = 0u, queue_run = 0u, longest = 0u; + unsigned cls; + + for (i = 0; i < t->count; i++) { + const td_event *e = &t->events[i]; + + events[e->cls]++; + bytes[e->cls] += (uint64_t)e->bytes * e->repeats; + reads[e->cls] += e->repeats; + if (td_is_boundary(e->cls)) { + if (queue_run) { + segments++; + if (queue_run > longest) + longest = queue_run; + } + queue_run = 0u; + } else { + queue_run++; + } + } + if (queue_run) { + segments++; + if (queue_run > longest) + longest = queue_run; + } + fprintf(stderr, "pn_tt: DISPATCH RECORD forward %llu — %zu events (poll loops merged), %zu " + "queue segments between boundaries, longest %zu events\n", + (unsigned long long)forward, t->count, segments, longest); + for (cls = 0; cls < TD_CLASSES; cls++) + if (events[cls]) + fprintf(stderr, "pn_tt: DISPATCH RECORD %-13s %7llu events %9llu accesses %10llu B\n", + td_class_name[cls], (unsigned long long)events[cls], + (unsigned long long)reads[cls], (unsigned long long)bytes[cls]); +} + +/* --------------------------------------------------------------------------------------- + * A3: LIVE REPLAY. The host runs its forward exactly as always; its queue WRITES are + * swallowed here (the puller makes them), its completion POLLS and every boundary access go + * to the card for real. So the host's own counters, checks and data flow are unchanged, and + * the puller is what the card waits on. Each segment is launched when the host reaches its + * first event; the previous segment is awaited first (errors surface there, and the + * puller's ring cannot overrun). + * --------------------------------------------------------------------------------------- */ + +static int g_event_segment_ready; /* event -> segment table built */ +static int32_t *g_event_segment; + +static void td_index_segments(void) +{ + const td_token *a = &g_td.model[0]; + size_t i; + int32_t segment = -1; + bool in_segment = false; + + free(g_event_segment); + g_event_segment = malloc(a->count * sizeof(*g_event_segment)); + if (!g_event_segment) + return; + for (i = 0; i < a->count; i++) { + if (td_is_boundary(a->events[i].cls)) { + in_segment = false; + g_event_segment[i] = -1; + continue; + } + if (!in_segment) { + segment++; + in_segment = true; + } + g_event_segment[i] = segment; + } + g_event_segment_ready = 1; +} + +/** The model's prediction for one queue WRITE at token offset k, byte for byte. */ +static bool td_predict_match(size_t i, uint32_t k, uint64_t addr, const void *data, size_t bytes) +{ + const td_token *a = &g_td.model[0], *b = &g_td.model[1]; + const td_event *ea = &a->events[i], *eb = &b->events[i]; + const uint32_t *wa = &a->pool[ea->data_at], *wb = &b->pool[eb->data_at]; + uint32_t words[TD_DATA_WORDS]; + const uint32_t n = (uint32_t)(bytes / 4u); + uint32_t word; + + if (n > TD_DATA_WORDS || bytes % 4u) + return false; + memcpy(words, data, bytes); + if (ea->cls == TD_Q_RING) { + const td_queue *queue = &g_td.queues[ea->queue]; + const uint32_t index = ea->index + (eb->index - ea->index) * k; + + if (addr != FT_TT_OP_QUEUE_BASE + TD_QUEUE_RING_OFFSET + (index & queue->mask) * 32u) + return false; + for (word = 0; word < n; word++) { + const uint32_t want = (word % 8u) == 1u + ? td_token_at(queue->nonce, index + word / 8u) + : wa[word] + (wb[word] - wa[word]) * k; + + if (want != words[word]) + return false; + } + return true; + } + if (addr != ea->addr) + return false; + for (word = 0; word < n; word++) + if (wa[word] + (wb[word] - wa[word]) * k != words[word]) + return false; + return true; +} + +static void td_diverge(const char *why, size_t i) +{ + if (g_td.divergences++ < 8u) + fprintf(stderr, "pn_tt: DISPATCH REPLAY DIVERGED at forward k=%u event %zu: %s — replay " + "is OFF from the next token\n", g_td.replay_k, i, why); + g_td.replay_dead = true; +} + +static bool td_await_puller(void); + +static bool td_launch_segment(uint32_t segment) +{ + uint32_t params[FT_TT_QUEUE_PARAM_WORDS]; + pn_tt_residency *puller = &g_td.device->puller; + bool ok = true; + + /* THE PULLER'S RING HOLDS A FEW: a segment is launched only once the previous ones are + * done (every segment ends at a boundary, which awaited them already). */ + if (g_td.awaited_segment != g_td.next_segment && !td_await_puller()) + return false; + g_td.internal = true; + memset(params, 0, sizeof(params)); + params[0] = TD_PROGRAM_BASE; + params[1] = g_td.replay_k; + params[2] = segment; + params[3] = segment; + /* REDS (resident_puller.c args[4]): patches at k+1 must DIFFER; a flipped token must be + * REFUSED by the host's completion check; dropped awaits must DIFFER or refuse. */ + params[4] = (pn_tt_red("dispatch_skip_patch") ? 2u : 0u) | + (pn_tt_red("dispatch_bad_token") ? 4u : 0u) | + (pn_tt_red("dispatch_no_wait") ? 8u : 0u) | + (pn_tt_red("dispatch_mcast_short") ? 32u : 0u); + if (ok && pn_tt_residency_enqueue(puller, TD_PULL_OPCODE, params, 1u) != 0) { + /* AN IDLE PULLER IS RETIRED BY THE ENQUEUE (residency_idle_retire): a prefill longer + * than the idle limit (~5 s: the serve's 1,630-token prompt is 7 s) between two + * replayed tokens left the next token with no puller and the request failed + * ("a segment did not launch"). Re-arm it, re-upload the program, retry once. */ + ok = !pn_tt_red("dispatch_no_rearm") && !puller->running && td_puller_arm(g_td.device) && td_upload_program(g_td.device) && + pn_tt_residency_enqueue(puller, TD_PULL_OPCODE, params, 1u) == 0; + if (ok) + g_td.puller_rearms++; + } + g_td.internal = false; + if (ok) { + g_td.next_segment = segment + 1u; + g_td.launched_segments++; + } + return ok; +} + +/** The completion window the model predicts for poll event `i` at token offset k: every + * word linear, the token word recomputed from the queue's nonce. */ +static void td_synthesize_poll(size_t i, uint32_t k, void *dst, size_t bytes) +{ + const td_token *a = &g_td.model[0], *b = &g_td.model[1]; + const td_event *ea = &a->events[i], *eb = &b->events[i]; + const uint32_t *wa = &a->pool[ea->data_at], *wb = &b->pool[eb->data_at]; + const uint32_t offset = ea->addr - FT_TT_OP_QUEUE_BASE; + uint32_t words[TD_DATA_WORDS]; + uint32_t n = (uint32_t)((bytes + 3u) / 4u), word, count = 0u; + bool have_count = false; + + if (n > ea->words) + n = ea->words; + for (word = 0; word < n; word++) { + words[word] = wa[word] + (wb[word] - wa[word]) * k; + if (offset + word * 4u == 0x14u) { + count = words[word]; + have_count = true; + } + } + for (word = 0; word < n; word++) + if (offset + word * 4u == 0x18u && have_count && count > 0u) + words[word] = td_token_at(g_td.queues[ea->queue].nonce, count - 1u); + else if (offset + word * 4u == 0x24u) + words[word] = 0u; /* error: none (the puller checked the card's) */ + memcpy(dst, words, bytes < n * 4u ? bytes : n * 4u); +} + +/** Wait for every launched segment on the puller (the card's own checks ran there). */ +static bool td_await_puller(void) +{ + pn_tt_residency *puller = &g_td.device->puller; + bool ok = true; + + if (g_td.next_segment == 0u || g_td.awaited_segment == g_td.next_segment) + return true; + g_td.internal = true; + if (pn_tt_residency_await(puller, 5000u) != 0) { + uint32_t header[16]; + const ft_tt_noc_access *bulk = &g_td.original[TD_ACC_BULK_L1]; + + if (bulk->read(bulk->user, puller->core_x, puller->core_y, TD_PROGRAM_BASE, header, + sizeof(header)) == FT_TT_OK) + { + uint32_t q, w0 = 0u, w1 = 0u; + + fprintf(stderr, "pn_tt: DISPATCH REPLAY — segment %u failed on the puller: error %u " + "at entry +0x%x (last await count %u)\n", + g_td.next_segment - 1u, header[8], header[9], header[11]); + if (header[9] / 4u + 1u < g_prog.count) { + w0 = g_prog.words[header[9] / 4u]; + w1 = g_prog.words[header[9] / 4u + 1u]; + } + fprintf(stderr, "pn_tt: DISPATCH REPLAY — that entry: type %u count %u queue word " + "0x%08x\n", w0 & 0xFFu, (w0 >> 8) & 0xFFFu, w1); + for (q = 0; q < g_td.queue_count; q++) { + uint32_t control[8]; + + if (bulk->read(bulk->user, g_td.queues[q].x, g_td.queues[q].y, + FT_TT_OP_QUEUE_BASE, control, sizeof(control)) == FT_TT_OK) + fprintf(stderr, "pn_tt: DISPATCH REPLAY — queue %u (%u,%u): head %u tail %u " + "completed %u state 0x%x\n", q, g_td.queues[q].x, + g_td.queues[q].y, control[2], control[3], control[5], control[7]); + } + } + ok = false; + } + g_td.internal = false; + g_td.awaited_segment = g_td.next_segment; + return ok; +} + +static bool td_replay_access(uint8_t acc, uint16_t x, uint16_t y, uint64_t addr, + const void *data, size_t bytes, bool write) +{ + const td_token *a = &g_td.model[0]; + const uint8_t cls = td_classify(acc, addr, write); + const td_event *e; + + if (g_td.cursor >= a->count) { + td_diverge("the host made more accesses than the model has", g_td.cursor); + g_td.replaying = false; + return false; + } + e = &a->events[g_td.cursor]; + if (cls != e->cls || x != e->x || y != e->y || bytes != e->bytes || + (cls != TD_Q_RING && addr != e->addr)) { + if (g_td.divergences < 8u) + fprintf(stderr, "pn_tt: DISPATCH REPLAY — the host did %s (%u,%u) 0x%llx %zu B; the " + "model's event %zu is %s (%u,%u) 0x%x %u B\n", + td_class_name[cls], x, y, (unsigned long long)addr, bytes, g_td.cursor, + td_class_name[e->cls], e->x, e->y, e->addr, e->bytes); + td_diverge("the access is not the model's next event", g_td.cursor); + g_td.replaying = false; + return false; + } + if (td_is_boundary(cls)) { + /* THE HOST TOUCHES DATA: everything the puller was asked for must be done. */ + if (!td_await_puller()) { + td_diverge("the puller did not complete before a boundary", g_td.cursor); + g_td.replaying = false; + return false; + } + g_td.cursor++; + return false; + } + if (g_event_segment[g_td.cursor] >= 0 && + (uint32_t)g_event_segment[g_td.cursor] == g_td.next_segment && + !td_launch_segment(g_td.next_segment)) { + td_diverge("a segment did not launch", g_td.cursor); + g_td.replaying = false; + return false; + } + if (!write) { + /* A COMPLETION POLL: answered from the model. The puller awaits the same count on + * the card and checks the same token before it issues anything after it; the host + * would only race it (a puller ahead of the host leaves the count PAST the host's). */ + td_synthesize_poll(g_td.cursor, g_td.replay_k, (void *)data, bytes); + g_td.cursor++; + return true; + } + if (!td_predict_match(g_td.cursor, g_td.replay_k, addr, data, bytes)) { + if (g_td.divergences < 8u) { + const td_event *ea = &a->events[g_td.cursor]; + const uint32_t *wa = &a->pool[ea->data_at]; + const uint32_t *wb = &g_td.model[1].pool[g_td.model[1].events[g_td.cursor].data_at]; + uint32_t word, host[TD_DATA_WORDS]; + const uint32_t n = (uint32_t)(bytes / 4u) < TD_DATA_WORDS ? (uint32_t)(bytes / 4u) + : TD_DATA_WORDS; + + memcpy(host, data, n * 4u); + fprintf(stderr, "pn_tt: DISPATCH REPLAY — mispredicted %s (%u,%u) 0x%llx %zu B", + td_class_name[cls], x, y, (unsigned long long)addr, bytes); + if (cls == TD_Q_RING) { + const td_queue *queue = &g_td.queues[ea->queue]; + const uint32_t index = ea->index + + (g_td.model[1].events[g_td.cursor].index - ea->index) * + g_td.replay_k; + + fprintf(stderr, " | model index %u mask %u -> slot address 0x%x", + index, queue->mask, + FT_TT_OP_QUEUE_BASE + TD_QUEUE_RING_OFFSET + (index & queue->mask) * 32u); + } + for (word = 0; word < n && word < 16u; word++) + if (cls != TD_Q_RING && host[word] != wa[word] + (wb[word] - wa[word]) * g_td.replay_k) + fprintf(stderr, " w%u host %08x model %08x", word, host[word], + wa[word] + (wb[word] - wa[word]) * g_td.replay_k); + fprintf(stderr, "\n"); + } + td_diverge("the host's write is not the model's prediction (the puller wrote the " + "model's)", g_td.cursor); + } + g_td.cursor++; + return true; /* swallowed: the puller wrote it */ +} + +static void td_replay_begin(pn_tt_device *device, uint64_t forward) +{ + td_wrap(device); + g_td.cursor = 0u; + g_td.polling = false; + g_td.next_segment = 0u; + g_td.awaited_segment = 0u; + g_td.replay_k = (uint32_t)(forward - g_td.first_forward); + g_td.replaying = true; +} + +static void td_replay_end(void) +{ + if (!g_td.replaying) + return; + if (g_td.polling) { + g_td.cursor++; + g_td.polling = false; + } + g_td.replaying = false; + if (g_td.cursor != g_td.model[0].count) + td_diverge("the token ended before the model did", g_td.cursor); + if (!td_await_puller()) + td_diverge("the last segment did not complete", g_td.cursor); + g_td.replayed_tokens++; + td_unwrap(); +} + +/* --------------------------------------------------------------------------------------- + * Token hooks + * --------------------------------------------------------------------------------------- */ + +void pn_tt_dispatch_token_begin(pn_tt_device *device, uint64_t forward) +{ + const long observe = td_env_long("PN_TT_DISPATCH_RECORD"); + const long live = td_dispatch_live(); + const long shadow = live > 0 ? (td_env_long("PN_TT_DISPATCH_FROM") > 0 + ? td_env_long("PN_TT_DISPATCH_FROM") : 4) + : td_env_long("PN_TT_DISPATCH_SHADOW"); + td_token *into = NULL; + + if (!device) + return; + /* A NEW SEQUENCE re-anchors the model: positions restart, so every position patch would + * be wrong. The model is re-learned on the sequence's own forwards `shadow`, `shadow+1` + * (counted from its first forward) and replay resumes after them. */ + if (g_td.sequence_reset) { + g_td.sequence_reset = false; + g_td.sequence_first = forward; + g_td.model_ok = false; + g_td.puller_ready = false; + } + if (g_td.sequence_first == 0u) + g_td.sequence_first = 1u; + { + const uint64_t first = g_td.sequence_first + (uint64_t)(shadow > 0 ? shadow : 1) - 1u; + + if (live > 0 && g_td.puller_ready && !g_td.replay_dead && forward >= first + 2u) { + td_replay_begin(device, forward); + return; + } + if (observe > 0 && forward == (uint64_t)observe) + into = &g_td.current; + if (shadow > 0 && forward >= first) { + g_td.first_forward = first; + if (forward == first) + into = &g_td.model[0]; + else if (forward == first + 1u) + into = &g_td.model[1]; + else if (g_td.model_ok) + into = &g_td.current; + } + } + if (!into) + return; + td_wrap(device); + into->count = 0u; + into->pool_count = 0u; + g_td.into = into; + g_td.recording = true; +} + +void pn_tt_dispatch_token_end(pn_tt_device *device, uint64_t forward) +{ + const long observe = td_env_long("PN_TT_DISPATCH_RECORD"); + const long live = td_dispatch_live(); + const long shadow = live > 0 ? 1 : td_env_long("PN_TT_DISPATCH_SHADOW"); + + if (g_td.replaying) { + td_replay_end(); + return; + } + if (!g_td.recording) + return; + g_td.recording = false; + td_unwrap(); + if (observe > 0 && forward == (uint64_t)observe) + td_report(g_td.into, forward); + if (shadow <= 0) + return; + if (g_td.into == &g_td.model[1]) { + g_td.model_ok = g_td.model[0].count == g_td.model[1].count && + td_resolve(&g_td.model[0]) && td_resolve(&g_td.model[1]) && + td_learn_queues(); + if (g_td.model_ok) { + size_t i; + + for (i = 0; i < g_td.model[0].count; i++) + if (!td_same_shape(&g_td.model[0].events[i], &g_td.model[1].events[i])) { + g_td.model_ok = false; + break; + } + } + if (g_td.model_ok && (td_env_long("PN_TT_DISPATCH_DRY") > 0 || live > 0)) { + g_td.puller_ready = td_build_program() && td_puller_arm(device) && + td_upload_program(device); + if (!g_td.puller_ready) + fprintf(stderr, "pn_tt: DISPATCH DRY — no program or no puller\n"); + else + td_index_segments(); + if (!g_event_segment_ready) + g_td.puller_ready = false; + } + if (!g_td.model_ok) + fprintf(stderr, "pn_tt: DISPATCH SHADOW — the two model tokens are not one shape " + "(%zu vs %zu events) or a token word did not decode; no model\n", + g_td.model[0].count, g_td.model[1].count); + } else if (g_td.into == &g_td.current && g_td.model_ok) { + if (td_resolve(&g_td.current)) { + td_shadow_check(forward); + if (g_td.puller_ready && + (forward - g_td.first_forward) % (uint64_t)td_env_long("PN_TT_DISPATCH_DRY") == 0u) + td_dry_check(device, forward); + } else { + g_td.shape_failures++; + } + } +} + +void pn_tt_dispatch_sequence_reset(void) +{ + g_td.sequence_reset = true; +} + +bool pn_tt_dispatch_quiet(void) +{ + return !g_streaming && !g_td.replaying && !g_td.recording; +} + +void pn_tt_dispatch_report(void) +{ + if (td_env_long("PN_TT_DISPATCH_SHADOW") > 0) + td_shadow_report(); + if (td_dispatch_live() > 0 && g_td.replayed_tokens && g_td.device && + g_td.device->puller.running) { + uint32_t header[16]; + const ft_tt_noc_access *bulk = &g_td.device->runtime.bulk_l1; + + if (bulk->read(bulk->user, g_td.device->puller.core_x, g_td.device->puller.core_y, + TD_PROGRAM_BASE, header, sizeof(header)) == FT_TT_OK) + fprintf(stderr, "pn_tt: DISPATCH PULLER — busy %.2f ms/token inside PULL " + "descriptors, of which %.2f ms awaiting targets\n", + (double)header[12] * 16.0 / 1.35e6 / (double)g_td.replayed_tokens, + (double)header[13] * 16.0 / 1.35e6 / (double)g_td.replayed_tokens); + } + if (td_dispatch_live() > 0) + fprintf(stderr, "pn_tt: DISPATCH REPLAY — %llu tokens replayed by the puller, %llu " + "segments launched, %llu divergences%s%s\n", + (unsigned long long)g_td.replayed_tokens, + (unsigned long long)g_td.launched_segments, + (unsigned long long)g_td.divergences, + g_td.replay_dead ? " — replay OFF" : "", + getenv("PN_TT_RED") ? " [RED armed]" : ""); + if (td_env_long("PN_TT_DISPATCH_DRY") > 0) + fprintf(stderr, "pn_tt: DISPATCH DRY — %llu tokens replayed on the puller in dry-run, " + "%llu FAILED, %llu log words compared; program %zu B, %u segments%s\n", + (unsigned long long)g_dry_checked, (unsigned long long)g_dry_failed, + (unsigned long long)g_dry_words, g_prog.count * 4u, g_prog.segments, + pn_tt_red("dispatch_dry_k") ? " [RED dispatch_dry_k]" : ""); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_dispatch.h b/src/accel/tt/pn_tt_dispatch.h new file mode 100644 index 0000000..f5570de --- /dev/null +++ b/src/accel/tt/pn_tt_dispatch.h @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_dispatch.h + * @brief STEP 3 (A) — on-card dispatch: the RECORDER. Captures every host<->card access a + * decode token makes, at the transport, classified by address. + * + * @status A1: observe mode (`PN_TT_DISPATCH_RECORD=`): wraps the runtime's three + * accessors for one decode token and prints what the token is made of — queue writes + * (parameter blocks, ring slots, head bumps), completion polls, and every OTHER access + * (the segment boundaries a puller has to hand back to the host). + * A1b (`PN_TT_DISPATCH_SHADOW=`): learn a model from two tokens and predict + * every later token's queue writes and awaited counts, compared byte for byte. + * @issues None + * @todo A2: the puller's dry run; A3: live replay. + */ +#ifndef PN_TT_DISPATCH_H +#define PN_TT_DISPATCH_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_device.h" + +/** Call at the start of a decode forward (before the embedding upload); `forward` counts + * forwards from 1. Arms the recorder when `forward` is the one asked for. */ +void pn_tt_dispatch_token_begin(pn_tt_device *device, uint64_t forward); + +/** Call after the head (top-k read back). Disarms and, after the recorded token, reports. */ +void pn_tt_dispatch_token_end(pn_tt_device *device, uint64_t forward); + +/** A new sequence (the carried state was zeroed): the model is re-learned on its forwards. */ +void pn_tt_dispatch_sequence_reset(void); +/** No stream, no replay and no recording is in progress: the host may issue outside a token + * (the keepalive's NOPs). */ +bool pn_tt_dispatch_quiet(void); + +/** STEP 4 S2 (verify data movers on the puller, armed on demand): + * row `src_row` of every tile of `src` -> row `dst_row` of `dst` (all of dst's mirrors). */ +int pn_tt_row_copy(pn_tt_device *device, const pn_tt_activation *src, uint32_t src_row, + const pn_tt_activation *dst, uint32_t dst_row); +/** FILL on the puller (`resident_puller.c` kOpFill): every 32-bit word of DRAM + * [address, address + bytes) of `bank` becomes `value`, card side. address and bytes 64 B + * multiples. -1 (nothing written) when the puller cannot arm, mid-stream, or misaligned. */ +int pn_tt_fill_dram_oncard(pn_tt_device *device, uint32_t bank, uint32_t address, + size_t bytes, uint32_t value); +/** COPY on the puller (`resident_puller.c` kOpCopy): DRAM [src_address, + bytes) of + * `src_bank` -> [dst_address, + bytes) of `dst_bank`, card side (the multi-turn checkpoint of + * the carried GDN state). Addresses and bytes 64 B multiples. Flushes first. -1 when the + * puller cannot arm, mid-stream, misaligned, or the descriptor failed (a puller program + * without the opcode refuses it: the copy is then not done, and the caller must not trust + * the destination). */ +int pn_tt_copy_dram_oncard(pn_tt_device *device, uint32_t src_bank, uint32_t src_address, + uint32_t dst_bank, uint32_t dst_address, size_t bytes); +/** Cumulative S2 row-copy count and host wall time. */ +void pn_tt_move_stats(uint64_t *rows, double *row_ms); + +/** S2d: STREAMED segments around a speculative verify / MTP drafting (pn_tt_dispatch.c). + * `PN_TT_SPEC_STREAM=0` off. begin returns false when not streaming; end returns false if a + * segment failed on the card (the caller must then treat its work as failed). */ +bool pn_tt_dispatch_stream_begin(pn_tt_device *device); +bool pn_tt_dispatch_stream_end(void); +void pn_tt_dispatch_stream_report(double cycles); +/** PM 6: launch what the stream has captured so far WITHOUT waiting (the card starts on it + * while the host waits on something else). true when nothing was streaming or the launch + * went; false if the stream has failed. */ +bool pn_tt_dispatch_stream_kick(void); + +/** A1b: print the shadow's tallies (`PN_TT_DISPATCH_SHADOW=`). */ +void pn_tt_dispatch_report(void); + +#endif /* USE_TT */ +#endif /* PN_TT_DISPATCH_H */ diff --git a/src/accel/tt/pn_tt_eltwise_group.c b/src/accel/tt/pn_tt_eltwise_group.c new file mode 100644 index 0000000..62ba62b --- /dev/null +++ b/src/accel/tt/pn_tt_eltwise_group.c @@ -0,0 +1,685 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_eltwise_group.c + * @brief Implementation of the elementwise core split. See pn_tt_eltwise_group.h — the core + * map, the knobs and the argument for why this family splits cleanly live there, and + * the tile arithmetic lives in pn_tt_eltwise_split.c where a test can reach it. + * + * @status Behind `PN_TT_ELTWISE_CORES`, default 1. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_eltwise_group.h" + +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_name_list.h" + +/** Members, and where they start. See the header for why 106 cannot collide. + * + * **DEFAULT 2, AND 2 IS THE KNEE — MEASURED 2026-08-24 ON THE SHIPPED BLOB.** The wall over + * the ladder is 5.01 / **5.17** / 4.20 / 3.18 tok/s at N = 1 / 2 / 8 / 16, against a control + * re-taken the same hour on the same binary. It is not a shape anyone predicted, and the + * census fit says exactly why: + * + * N 1 2 8 16 + * us/tile 1.999 0.998 0.000 -0.001 + * fixed -100.5 +42.2 +563.0 +1125.2 us/descriptor + * + * **THE PER-TILE TERM SCALES PERFECTLY** — 1.999 -> 0.998 is 2.00x at two members, and by + * eight there is no per-tile term left at all. The cores are not the bind. What stops it is + * the INTERCEPT, which grows ~+75-85 us for every member added, flat, whatever the width. + * So a member pays for itself only on a descriptor wide enough: saving is + * `1.999 * W * (1/N - 1/(N+1))` against ~80 us, i.e. **W >= ~143 tiles at the first + * doubling** — and Ornith's elementwise widths are 128 and 384. Measured per width at two + * members: SwiGLU Wt=384 **667.2 -> 425.6 us (1.57x)**, and the residual ADD at Wt=128 + * **110.0 -> 147.0 us, WORSE**, exactly as that line predicts. At four members the + * break-even is 572 tiles and nothing in the family is that wide. + * + * **THE INTERCEPT IS THE HOST'S SERIAL ENQUEUE, AND THE PER-MEMBER AWAITS PROVE IT.** Every + * member above 0 reports **0.0079-0.0080 s/token** — identical to three digits at N=2 and at + * N=16, where its slice is eight times smaller. A cost that does not move with the work is + * not the card. It is 58 us a descriptor of parameter write, ring write and head bump that + * the host pays for member `m` before member `m+1` is told anything. Member 0's own await + * falls 0.0202 -> 0.0081 across that same ladder, which is its work halving away. + * + * So the lever above this one is NOT more cores: it is the ISSUE path — the parameter cache + * `pn_tt_matmul_group.c` already uses to make a repeated projection cost zero device + * accesses, applied to a family that re-writes 64 bytes for all 136 descriptors of a token. + * + * **THE BLOCK DEPTH IS NOT THE CONFOUND, AND THAT WAS MEASURED RATHER THAN ASSUMED.** The + * depth moves with the member count (32 unsplit, 8 at eight members), so the ladder walks + * two knobs. `PN_TT_ELTWISE_DEPTH=8` at ONE member is 5.02 tok/s and fits -103.5 + 2.003, + * against the control's 5.01 and -100.5 + 1.999. The depth is worth **0.0%**. */ +#define PN_TT_ELTWISE_CORES_ENV "PN_TT_ELTWISE_CORES" +#define PN_TT_ELTWISE_CORES_DEFAULT 2u +#define PN_TT_ELTWISE_MEMBER_CORE_ENV "PN_TT_ELTWISE_MEMBER_CORE_FIRST" +/* 112 since the PX7 placement shipped (was 106, inside the 16-member matmul's 96..110). */ +#define PN_TT_ELTWISE_MEMBER_CORE_DEFAULT 112u + +/** + * PHASE W1a — WIDE MEMBERS for the prefill's whole-prompt descriptors. + * + * A 1,629-row prefill's SwiGLU is ONE descriptor of 51 x 384 = 19,584 tiles: 15.65 ms on the + * two decode members, 33 of them a prompt = 0.52 s of the 1.57 s `batched` line, while the + * free Tensix of the 0-70 region sit idle (2026-10-07). The fit above says the per-tile term + * scales perfectly and the cost of a member is its ~45 us serial enqueue, so a descriptor that + * long pays for many members where a decode one pays for two. + * + * `PN_TT_ELTWISE_WIDE_CORES=N` arms N MORE eltwise residencies from + * `PN_TT_ELTWISE_WIDE_CORE_FIRST` (58: 58..70 is clear of every family and of the 48-core + * prefill grid on 0..47), at member 0's placement. An SFPU descriptor of at least + * `PN_TT_ELTWISE_WIDE_MIN_TILES` (2048; 16x that for ADD/MUL) goes over EVERY member, dealt out in whole blocks of + * `2 * depth` (`pn_tt_eltwise_split_slice_blocks()`), so no width has to divide; every + * narrower descriptor takes the decode split exactly as before. Same per-element arithmetic + * on a different core: bit-exact by construction. The mirrors of each wide slice are keyed in + * that member's own replica table (`pn_tt_replica_map.c`). + */ +#define PN_TT_ELTWISE_WIDE_CORES_ENV "PN_TT_ELTWISE_WIDE_CORES" +#define PN_TT_ELTWISE_WIDE_CORE_ENV "PN_TT_ELTWISE_WIDE_CORE_FIRST" +#define PN_TT_ELTWISE_WIDE_CORE_DEFAULT 58u +#define PN_TT_ELTWISE_WIDE_MIN_TILES_ENV "PN_TT_ELTWISE_WIDE_MIN_TILES" +#define PN_TT_ELTWISE_WIDE_MIN_TILES_DEFAULT 2048u + +/** CAP on the block depth, so a member-count ladder can be run at a FIXED depth. The header + * has the confound this exists to separate. */ +#define PN_TT_ELTWISE_DEPTH_ENV "PN_TT_ELTWISE_DEPTH" + +/** + * WHICH FUNCTIONS THE SPLIT IS APPLIED TO — because THE KNEE WAS NEVER A WIDTH, AND IT MOVES + * WITH THE COMPLETION SHAPE. + * + * RE-MEASURED ON SILICON 2026-08-26, per SHAPE, at one member and at two, on TWO completion + * shapes. `K` is the break-even recovered from each pair by `T(2) = T(1)/2 + K/2`: + * + * ---- window, 5 settle reads ---- ---- line, 0 ---- + * shape N=1 N=2 K N=1 N=2 K + * SILU_MUL Wt=384 634.5 361.0 87.5 619.8 332.6 45.4 + * SILU_MUL Wt=128 235.3 161.7 88.1 220.1 132.6 45.1 + * SIGMOID Wt=128 203.6 144.6 85.6 189.0 115.7 42.4 + * ADD Wt=128 77.5 82.5 87.5 (LOSES) 62.3 53.5 44.7 (WINS) + * render-widen Wt=256 125.4 105.1 84.6 110.3 77.1 43.9 + * ------------------------------------------------------------------------------- + * **87.2 +- 1.3** **44.3 +- 1.1** + * + * **TWO SHAPES AT Wt=128 WIN AND TWO AT Wt=128 LOSE on the window, so the discriminator is + * not the width** — the recorded "W >= 143 tiles" was this statement seen through the + * family's pooled line (which re-measures at W >= 110). A descriptor is worth splitting in + * two iff its UNSPLIT TIME is above K. + * + * **AND K IS A PROPERTY OF THE AWAIT, NOT OF THE FAMILY.** 87.2 -> 44.3 when the completion + * shape went `window` -> `line`: 42.9 us, i.e. 21.5 us off each of the two members, against + * the 5 x 3.31 = 16.6 us of settle reads the line removes. Five shapes across TWO families + * (`eltwise` and `render-widen`, different programs at different page sizes) recover the same + * constant to +-2.5%, twice. + * + * **SO ON THE SHIPPED `line` DEFAULT EVERY SHAPE IN THIS FAMILY IS ABOVE THE BAR** — the + * cheapest is the residual ADD at 62.3 us against K = 44.3 — and the filter DEFAULTS TO + * UNRESTRICTED. It is kept, with the table, because it is the only way to run the family + * under `window` or `control` without paying 8.8 us on each of 64 residual adds, and because + * a knee that moves with another knob is exactly the thing a later round will re-derive + * wrongly from one row of this table. + * + * A LIST AND NOT A THRESHOLD, deliberately. A threshold read out of the live census would + * make some descriptors of one key split and others not, and the key's mean is what the + * `fixed + per_tile * tiles` fit is solved from — the instrument this whole finding came out + * of would start reporting a blend. + * + * CONTAINS, NOT SELECTS: this is a FILTER, so an unset or empty value does not restrict. + * `PN_TT_ELTWISE_SPLIT_FUNCTIONS=NOTHING_MATCHES_THIS` is the arm that proves it is live — + * measured, it puts member 1 on **0.0 descriptors/token** and every census row back on the + * one-member line (per_tile 0.977 -> 2.000). + */ +#define PN_TT_ELTWISE_SPLIT_FUNCTIONS_ENV "PN_TT_ELTWISE_SPLIT_FUNCTIONS" + +/** Is `function` one the split pays for? See the table above. + * + * READ ONCE. A `getenv` here is 136 calls a token on this family alone, and the knob cannot + * change inside a run. The names are compared WHOLE against + * `ft_tt_op_vector_function_name()`, which returns "SILU_MUL"/"SIGMOID"/"ADD"/"MUL" — + * UPPERCASE, so a lowercase list matches nothing, splits nothing, and reads exactly like a + * knob that is off. */ +static bool eltwise_split_covers(const char *function) +{ + static const char *list; + static bool cached; + + if (!cached) { + cached = true; + list = getenv(PN_TT_ELTWISE_SPLIT_FUNCTIONS_ENV); + } + return pn_tt_name_list_contains(list, function); +} + +uint32_t pn_tt_eltwise_group_members(const pn_tt_device *device) +{ + if (!device || device->eltwise_members == 0u) + return 1u; + return device->eltwise_members; +} + +bool pn_tt_device_eltwise_member_stat(const pn_tt_device *device, unsigned index, + uint32_t *out_core, uint64_t *out_ops, double *out_seconds) +{ + const pn_tt_residency *member; + + if (!device || index >= pn_tt_eltwise_group_members(device)) + return false; + member = index == 0u ? &device->eltwise : &device->eltwise_member[index - 1u]; + if (out_core) + *out_core = member->core_index; + if (out_ops) + *out_ops = member->ops; + if (out_seconds) + *out_seconds = device->eltwise_member_seconds[index]; + return true; +} + +static void eltwise_group_arm_wide(pn_tt_device *device, + const ft_tt_op_eltwise_request *placement); + +/** The widest width any descriptor of this residency carries. */ +static uint32_t eltwise_widest(const uint32_t *tile_counts, uint32_t count) +{ + uint32_t widest = 0u, index; + + for (index = 0; index < count; index++) { + if (tile_counts[index] > widest) + widest = tile_counts[index]; + } + return widest; +} + +void pn_tt_eltwise_group_plan(const pn_tt_device *device, const uint32_t *tile_counts, + uint32_t count, uint32_t *out_members, uint32_t *out_widest, + uint32_t *out_depth) +{ + uint32_t sliced[PN_TT_ONCARD_MAX_ELTWISE_SHAPES]; + uint32_t requested, members, cap, depth = 0u, index; + + if (!out_members || !out_widest || !out_depth) + return; + *out_members = 1u; + *out_widest = 0u; + *out_depth = 0u; + if (!device || !tile_counts || count == 0u || count > PN_TT_ONCARD_MAX_ELTWISE_SHAPES) + return; + + cap = pn_tt_env_u32(PN_TT_ELTWISE_DEPTH_ENV, PN_TT_ONCARD_MAX_DEPTH); + if (cap == 0u || cap > PN_TT_ONCARD_MAX_DEPTH) + cap = PN_TT_ONCARD_MAX_DEPTH; + requested = pn_tt_env_u32(PN_TT_ELTWISE_CORES_ENV, PN_TT_ELTWISE_CORES_DEFAULT); + /* THE TWO INSTRUMENTS DO NOT COMPOSE, AND THE SPLIT IS THE ONE THAT YIELDS. The + * queue-depth ladder counts K copies of one descriptor through + * `pn_tt_residency_submit()`; the split enqueues ONE descriptor per member directly, so + * `PN_TT_ONCARD_BATCH` would be inert here and its ladder would print a flat line + * produced by this file. Refusing names that. */ + if (requested > 1u && pn_tt_env_u32("PN_TT_ONCARD_BATCH", 0u) > 1u && + (!getenv("PN_TT_ONCARD_BATCH_FAMILY") || + pn_tt_name_list_contains(getenv("PN_TT_ONCARD_BATCH_FAMILY"), "eltwise"))) { + fprintf(stderr, "pn_tt: REFUSING the eltwise split — PN_TT_ONCARD_BATCH is armed for " + "this family and the split does not go through the batched submit, so " + "the ladder would measure nothing. Run one instrument at a time.\n"); + requested = 1u; + } + members = pn_tt_eltwise_split_member_limit(requested, tile_counts, count, + PN_TT_ELTWISE_MAX_MEMBERS); + /* THE DEPTH IS PART OF WHETHER A MEMBER COUNT IS LEGAL, not a consequence of it. A split + * whose slices no block depth serves is a descriptor the kernel refuses, so it shrinks + * here rather than on the card. */ + while (members > 1u) { + for (index = 0; index < count; index++) + sliced[index] = tile_counts[index] / members; + depth = pn_tt_common_depth(sliced, count, cap); + if (depth != 0u) + break; + members = pn_tt_eltwise_split_member_limit(members - 1u, tile_counts, count, + PN_TT_ELTWISE_MAX_MEMBERS); + } + if (members > 1u) { + *out_widest = eltwise_widest(sliced, count); + } else { + depth = pn_tt_common_depth(tile_counts, count, cap); + *out_widest = eltwise_widest(tile_counts, count); + } + if (requested > 1u && members != requested) + fprintf(stderr, "pn_tt: eltwise SPLIT — %u members was asked for; %u is the largest " + "that divides every width at a legal block depth\n", + requested, members); + *out_members = members; + *out_depth = depth; +} + +/** The placement every member of this device was armed at, rebuilt from what the device kept. + * It is DERIVED rather than stored because a stored copy is a second thing that must agree + * with `eltwise_depth` and the shape table, and this backend has already paid for a field + * that could disagree with the one beside it. */ +static ft_tt_op_eltwise_request eltwise_placement(const pn_tt_device *device) +{ + ft_tt_op_eltwise_request placement; + + memset(&placement, 0, sizeof(placement)); + placement.function = FT_TT_OP_VECTOR_FN_SILU_MUL; /* the widest of the six */ + /* THE DECODE SPLIT'S slice, not the whole set's: the arm placed every member (wide ones + * included) for that width, and the block depth is derived from it — dividing by the + * W1a set's 15 placed a re-armed member at another depth, and every descriptor after an + * idle re-arm was refused (run_w1a_eltwise_wide.sh round 1). */ + placement.tile_count = eltwise_widest(device->oncard_eltwise_tiles, + device->oncard_eltwise_count) / + (device->eltwise_decode_members ? device->eltwise_decode_members + : pn_tt_eltwise_group_members(device)); + placement.prefetch_tiles = device->eltwise_depth; + placement.input_a.tile_bytes = PN_TT_TILE_BF16; + placement.input_b.tile_bytes = PN_TT_TILE_BF16; + placement.output.tile_bytes = PN_TT_TILE_BF16; + return placement; +} + +void pn_tt_eltwise_group_arm(pn_tt_device *device, uint32_t members, + const ft_tt_op_eltwise_request *placement) +{ + uint32_t first, index; + + if (!device || !placement) + return; + pn_tt_eltwise_group_release(device); + memset(device->eltwise_member_seconds, 0, sizeof(device->eltwise_member_seconds)); + device->eltwise_members = 1u; + device->eltwise_decode_members = 1u; + if (members < 2u) { + eltwise_group_arm_wide(device, placement); + return; + } + first = pn_tt_env_u32(PN_TT_ELTWISE_MEMBER_CORE_ENV, PN_TT_ELTWISE_MEMBER_CORE_DEFAULT); + for (index = 0; index + 1u < members; index++) { + pn_tt_residency *member = &device->eltwise_member[index]; + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_ELTWISE_PROGRAM, + "eltwise-member", PN_TT_ELTWISE_MEMBER_CORE_ENV, + first + index, PN_TT_ONCARD_TRIAD_MASK)) + break; + if (ft_tt_op_eltwise_place_cbs(placement, placed, member->cb_buffer, + sizeof(member->cb_buffer), &member->cb_mask) != + FT_TT_OK || + !pn_tt_start_family(member)) { + pn_tt_residency_release(member); + break; + } + device->eltwise_members++; + } + /* COLLAPSE, DO NOT SHRINK. The header has the argument: a partial set is a member count + * the widths no longer divide, which is a ragged slice and a refused descriptor. */ + if (device->eltwise_members != members) { + fprintf(stderr, "pn_tt: eltwise SPLIT — asked for %u members and %u armed; COLLAPSING " + "to one core, because a partial split is a width no member divides\n", + members, device->eltwise_members); + pn_tt_eltwise_group_release(device); + eltwise_group_arm_wide(device, placement); + return; + } + fprintf(stderr, "pn_tt: eltwise SPLIT — every descriptor over %u members, core %u then " + "%u..%u, at block depth %u\n", + device->eltwise_members, device->eltwise.core_index, first, + first + device->eltwise_members - 2u, device->eltwise_depth); + eltwise_group_arm_wide(device, placement); +} + +/** W1a: arm the WIDE members after the decode split. A member that fails to arm stops the + * set where it is — the wide split is ragged by blocks, so any count is legal. */ +static void eltwise_group_arm_wide(pn_tt_device *device, const ft_tt_op_eltwise_request *placement) +{ + /* DEFAULT 13 (W1a, run_w1a_eltwise_wide.sh 2026-10-08): serve 1,630 TTFT 4.02 -> 3.25 s + * with the reader thread, decode flat; `PN_TT_ELTWISE_WIDE_CORES=0` off. */ + const uint32_t want = pn_tt_env_u32(PN_TT_ELTWISE_WIDE_CORES_ENV, 13u); + const uint32_t first = pn_tt_env_u32(PN_TT_ELTWISE_WIDE_CORE_ENV, + PN_TT_ELTWISE_WIDE_CORE_DEFAULT); + uint32_t index; + + device->eltwise_decode_members = device->eltwise_members; + for (index = 0; index < want && device->eltwise_members < PN_TT_ELTWISE_MAX_MEMBERS; + index++) { + pn_tt_residency *member = &device->eltwise_member[device->eltwise_members - 1u]; + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_ELTWISE_PROGRAM, + "eltwise-member", PN_TT_ELTWISE_WIDE_CORE_ENV, + first + index, PN_TT_ONCARD_TRIAD_MASK)) + break; + if (ft_tt_op_eltwise_place_cbs(placement, placed, member->cb_buffer, + sizeof(member->cb_buffer), &member->cb_mask) != + FT_TT_OK || + !pn_tt_start_family(member)) { + pn_tt_residency_release(member); + break; + } + device->eltwise_members++; + } + if (want) + fprintf(stderr, "pn_tt: eltwise WIDE — %u of %u extra members armed from core %u; " + "descriptors of %u+ tiles run over all %u (block %u)\n", + device->eltwise_members - device->eltwise_decode_members, want, first, + pn_tt_env_u32(PN_TT_ELTWISE_WIDE_MIN_TILES_ENV, + PN_TT_ELTWISE_WIDE_MIN_TILES_DEFAULT), + device->eltwise_members, 2u * device->eltwise_depth); +} + +uint32_t pn_tt_eltwise_group_members_for(const pn_tt_device *device, uint32_t tile_count, + uint32_t function) +{ + static uint32_t min_wide; + uint32_t min_tiles; + + if (!device) + return 1u; + if (min_wide == 0u) + min_wide = pn_tt_env_u32(PN_TT_ELTWISE_WIDE_MIN_TILES_ENV, + PN_TT_ELTWISE_WIDE_MIN_TILES_DEFAULT); + /* THE SFPU FUNCTIONS cost ~0.8 us a tile a member; ADD/MUL/SUB ~0.06 — a wide split of + * those pays its ~45 us a member enqueue only 13x wider (the residual ADD at Wt=6528 is + * 390 us on two). */ + min_tiles = (function == FT_TT_OP_VECTOR_FN_SILU_MUL || function == FT_TT_OP_VECTOR_FN_SILU || + function == FT_TT_OP_VECTOR_FN_SIGMOID) + ? min_wide : 16u * min_wide; + if (device->eltwise_decode_members == 0u || + device->eltwise_members <= device->eltwise_decode_members || tile_count < min_tiles || + device->eltwise_depth == 0u || + tile_count / (2u * device->eltwise_depth) < device->eltwise_members || + (tile_count % (2u * device->eltwise_depth)) != 0u) + return device->eltwise_decode_members ? device->eltwise_decode_members + : pn_tt_eltwise_group_members(device); + return device->eltwise_members; +} + +bool pn_tt_eltwise_group_slice(const pn_tt_device *device, uint32_t tile_count, + uint32_t members, uint32_t member, uint32_t tile_bytes, + pn_tt_eltwise_slice *out_slice) +{ + if (members > 1u && members == device->eltwise_members && + members > device->eltwise_decode_members && device->eltwise_decode_members != 0u) { + /* RED `eltwise_wide_shift`: a wide member's slice starts one block late — the slices + * then overlap and leave the last block unwritten; must DIFFER. */ + if (!pn_tt_eltwise_split_slice_blocks(tile_count, members, member, + 2u * device->eltwise_depth, tile_bytes, out_slice)) + return false; + if (member > 0u && out_slice->tile_count && pn_tt_red("eltwise_wide_shift") && + out_slice->first_tile + out_slice->tile_count < tile_count) { + out_slice->first_tile += 2u * device->eltwise_depth; + out_slice->byte_offset = out_slice->first_tile * tile_bytes; + } + return true; + } + return pn_tt_eltwise_split_slice(tile_count, members, member, tile_bytes, out_slice); +} + +void pn_tt_eltwise_group_release(pn_tt_device *device) +{ + uint32_t index; + + if (!device) + return; + for (index = 1u; index < device->eltwise_members; index++) + pn_tt_residency_release(&device->eltwise_member[index - 1u]); + device->eltwise_members = device->eltwise.running ? 1u : 0u; + device->eltwise_decode_members = device->eltwise_members; +} + +/** + * Retire ONE member of the set and arm a fresh one ON THE CORE IT ALREADY HELD. + * + * The core is captured BEFORE the release, because re-reading `core_index` off a released + * handle is how a member gets armed on core 0. Member 0 goes through the same code against + * its own env name — the alternative, `pn_tt_oncard_rearm()`, re-places the WHOLE set from + * scratch, which is the wrong operation in the middle of a descriptor whose other members + * have already retired. + * + * @return true when a fresh residency is live on that core and the slice may be replayed + */ +static bool eltwise_member_rearm(pn_tt_device *device, uint32_t member_index, + const ft_tt_op_eltwise_request *placement) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + const char *core_env = member_index == 0u ? PN_TT_CORE_ELTWISE_ENV + : PN_TT_ELTWISE_MEMBER_CORE_ENV; + const char *name = member_index == 0u ? "eltwise" : "eltwise-member"; + pn_tt_residency *member; + uint32_t core; + + if (member_index >= pn_tt_eltwise_group_members(device)) + return false; + member = member_index == 0u ? &device->eltwise : &device->eltwise_member[member_index - 1u]; + core = member->core_index; + fprintf(stderr, "pn_tt: eltwise member %u refused a descriptor — retiring it by ASKING and " + "arming a fresh one on core %u\n", member_index, core); + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + device->oncard_restarts++; + /* A core whose compute thread is still spinning out its bound is a WEDGE, not a re-arm: + * booting firmware onto it leaves `instrn_buf_status = 0x66` and needs a rung-3 recovery + * and sudo. Every other family here refuses on the same reading. */ + if (!pn_tt_residency_retired(member)) { + fprintf(stderr, "pn_tt: eltwise member %u did not retire — REFUSING to arm a fresh one " + "on core %u\n", member_index, core); + return false; + } + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_ELTWISE_PROGRAM, name, core_env, + core, PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_eltwise_place_cbs(placement, placed, member->cb_buffer, + sizeof(member->cb_buffer), &member->cb_mask) != FT_TT_OK) { + pn_tt_residency_release(member); + return false; + } + return pn_tt_start_family(member); +} + +/** Advance one operand's base to `slice`. An UNSET operand — a unary descriptor's B, which + * the encoder zeroes and the kernel never reads — is left alone, because offsetting it + * would build an address out of nothing. */ +static void eltwise_slice_operand(ft_tt_op_operand *operand, const pn_tt_eltwise_slice *slice) +{ + if (operand->tile_bytes == 0u) + return; + operand->address += slice->first_tile * operand->tile_bytes; +} + +/** Encode member `index`'s slice of `request`. @return false with the reason on stderr. */ +static bool eltwise_slice_words(const pn_tt_device *device, + const ft_tt_op_eltwise_request *request, uint32_t members, + uint32_t index, uint32_t *out_words) +{ + ft_tt_op_eltwise_request sliced = *request; + pn_tt_eltwise_slice slice; + + if (!pn_tt_eltwise_group_slice(device, request->tile_count, members, index, + PN_TT_TILE_BF16, &slice) || slice.tile_count == 0u) { + fprintf(stderr, "pn_tt: %u eltwise tiles do not divide over %u members\n", + request->tile_count, members); + return false; + } + sliced.tile_count = slice.tile_count; + sliced.prefetch_tiles = device->eltwise_depth; + eltwise_slice_operand(&sliced.input_a, &slice); + if (ft_tt_op_vector_function_is_binary(request->function)) + eltwise_slice_operand(&sliced.input_b, &slice); + eltwise_slice_operand(&sliced.output, &slice); + if (ft_tt_op_eltwise_parameters(&sliced, out_words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the eltwise slice [%u, %u) is one the family refuses\n", + slice.first_tile, slice.first_tile + slice.tile_count); + return false; + } + pn_tt_vec_probe_apply(out_words); + return true; +} + +/** The member residency `index` of the set (member 0 is `eltwise` itself). */ +static pn_tt_residency *eltwise_member_residency(pn_tt_device *device, uint32_t index) +{ + return index == 0u ? &device->eltwise : &device->eltwise_member[index - 1u]; +} + +/** + * Await every member of an enqueued descriptor, then re-arm and replay any member that was + * refused, then charge the set. `words[index]` is member `index`'s slice. + * + * PER-MEMBER RE-ARM AND REPLAY. Replaying one member's slice is the SAME COMPUTATION and not + * merely a safe one: an elementwise descriptor is a pure function of operands it does not + * modify, and the slices are disjoint, so a member that succeeded is neither re-run nor read. + * A refused descriptor also leaves no circular-buffer traffic behind it — the reader aborts + * before the rebase. (A deferred descriptor keeps this property only while its operands are + * untouched: the caller settles it before anything rewrites them.) + */ +static int eltwise_group_complete(pn_tt_device *device, const ft_tt_op_eltwise_request *request, + uint32_t (*words)[FT_TT_OP_VECTOR_WORDS], uint32_t members, + bool *bad, double started) +{ + uint32_t index; + int failed = 0; + + /* EVERY member is awaited even after one has failed. An enqueue that got as far as the + * ring has a descriptor the card will run whatever this host does next, and leaving it + * un-awaited desynchronises that member's cursor for the rest of the run. */ + for (index = 0; index < members; index++) { + pn_tt_residency *member = eltwise_member_residency(device, index); + const double member_started = pn_tt_now_seconds(); + double member_elapsed; + + if (pn_tt_residency_await(member, PN_TT_OP_TIMEOUT_MS) != 0) + bad[index] = true; + failed |= bad[index]; + member_elapsed = pn_tt_now_seconds() - member_started; + device->eltwise_member_seconds[index] += member_elapsed; + member->seconds += member_elapsed; + } + if (failed) { + const ft_tt_op_eltwise_request placement = eltwise_placement(device); + + failed = 0; + for (index = 0; index < members; index++) { + if (!bad[index]) + continue; + if (!eltwise_member_rearm(device, index, &placement) || + pn_tt_residency_submit(eltwise_member_residency(device, index), + FT_TT_QUEUE_OP_ELTWISE, words[index], + PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + } + /* Charged to the SET, once, at the WHOLE descriptor's tile count — so the census key is + * the same row at every member count and its fit is comparable across the ladder. */ + pn_tt_oncard_account(device, "eltwise", ft_tt_op_vector_function_name(request->function), + request->tile_count, pn_tt_now_seconds() - started); + return failed ? -1 : 0; +} + +void pn_tt_eltwise_group_defer_next(pn_tt_device *device) +{ + const char *env = getenv("PN_TT_ELTWISE_DEFER"); + + if (device && !(env && *env && atoi(env) == 0)) + device->eltwise_defer_next = true; +} + +uint64_t pn_tt_eltwise_group_deferred(const pn_tt_device *device) +{ + return device ? device->eltwise_deferred : 0u; +} + +int pn_tt_eltwise_group_settle(pn_tt_device *device) +{ + bool bad[PN_TT_ELTWISE_MAX_MEMBERS]; + uint32_t members; + + if (!device || device->eltwise_pending_members == 0u) + return 0; + members = device->eltwise_pending_members; + device->eltwise_pending_members = 0u; + memset(bad, 0, sizeof(bad)); + return eltwise_group_complete(device, &device->eltwise_pending_request, + device->eltwise_pending_words, members, bad, + device->eltwise_pending_started); +} + +int pn_tt_eltwise_group_submit(pn_tt_device *device, const ft_tt_op_eltwise_request *request) +{ + uint32_t words[PN_TT_ELTWISE_MAX_MEMBERS][FT_TT_OP_VECTOR_WORDS]; + bool bad[PN_TT_ELTWISE_MAX_MEMBERS]; + uint32_t members, index; + double started; + bool wide, defer, enqueue_failed = false; + + if (!device || !request) + return -1; + defer = device->eltwise_defer_next; + device->eltwise_defer_next = false; + /* The ring is FIFO, but the bookkeeping is not: a pending descriptor is awaited first. */ + if (pn_tt_eltwise_group_settle(device) != 0) + return -1; + members = pn_tt_eltwise_group_members_for(device, request->tile_count, + (uint32_t)request->function); + wide = members > device->eltwise_decode_members && device->eltwise_decode_members != 0u; + /* A WIDTH THE SET DOES NOT DIVIDE RUNS WHOLE ON MEMBER 0 rather than failing. The arm + * planned the split over every width the residency DECLARED, and a caller that reaches + * here with another one has already passed the depth guard — so member 0's placement + * serves it, and one core is slower rather than wrong. Failing instead would turn a new + * call site into a dropped descriptor, which on this family is a wrong answer with a + * return code nobody reads. Said once, because 136 descriptors a token is a flood. */ + if (!wide && (request->tile_count % members) != 0u) { + static bool narrowed; + + if (!narrowed) { + narrowed = true; + fprintf(stderr, "pn_tt: %u eltwise tiles do not divide over %u members — this " + "width runs WHOLE on member 0; the split was planned for the " + "widths the residency was armed with\n", + request->tile_count, members); + } + members = 1u; + } + /* AND A SHAPE BELOW THE BREAK-EVEN RUNS WHOLE ON MEMBER 0 TOO. The knee is a descriptor + * TIME of ~87 us and not a width — the residual ADD (77.5 us) and the attention MUL + * (69.0 us) are made SLOWER by a member, at the same Wt=128 where SILU_MUL and SIGMOID + * are made 1.46x and 1.41x faster. See the table beside + * PN_TT_ELTWISE_SPLIT_FUNCTIONS_ENV. Member 0's placement serves the whole width, so + * this is one core rather than wrong. */ + if (members > 1u && !eltwise_split_covers(ft_tt_op_vector_function_name(request->function))) + members = 1u; + memset(bad, 0, sizeof(bad)); + for (index = 0; index < members; index++) + if (!eltwise_slice_words(device, request, members, index, words[index])) + return -1; + + /* RED `eltwise_defer_drop`: a deferred descriptor is never enqueued — its output is + * whatever the strip held. Must DIFFER: proves the gate scores the deferred op. */ + if (defer && pn_tt_red("eltwise_defer_drop")) + return 0; + started = pn_tt_now_seconds(); + for (index = 0; index < members; index++) + if (pn_tt_residency_enqueue(eltwise_member_residency(device, index), + FT_TT_QUEUE_OP_ELTWISE, words[index], 1u) != 0) + enqueue_failed = bad[index] = true; + /* THE RESIDENCY CLOCKS RUN ON THIS PATH TOO (class 28: `ops != 0 && seconds == 0` read + * as a free family). The enqueue is booked on member 0 and each member's serial await on + * that member, so the rows SUM to the set's wall and no time is booked twice. */ + device->eltwise.seconds += pn_tt_now_seconds() - started; + /* PM 7 DEFERRED: the awaits wait for pn_tt_eltwise_group_settle(). A failed enqueue is + * completed (replayed) now instead, while the caller is still at this descriptor. */ + if (defer && !enqueue_failed) { + memcpy(device->eltwise_pending_words, words, sizeof(words[0]) * members); + device->eltwise_pending_request = *request; + device->eltwise_pending_started = started; + device->eltwise_pending_members = members; + device->eltwise_deferred++; + return 0; + } + return eltwise_group_complete(device, request, words, members, bad, started); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_eltwise_group.h b/src/accel/tt/pn_tt_eltwise_group.h new file mode 100644 index 0000000..e76cb40 --- /dev/null +++ b/src/accel/tt/pn_tt_eltwise_group.h @@ -0,0 +1,243 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_eltwise_group.h + * @brief SPLIT ONE ELEMENTWISE DESCRIPTOR ACROSS N TENSIX. Arms `PN_TT_ELTWISE_CORES - 1` + * extra `eltwise` residencies and gives each a contiguous, disjoint slice of every + * descriptor's tile stream. `pn_tt_eltwise_split.h` holds the arithmetic. + * + * --------------------------------------------------------------------------- + * WHY: FOUR FAMILIES, ONE CEILING, AND IT IS ONE CORE + * --------------------------------------------------------------------------- + * Measured on silicon 2026-08-24, with the host out of the token: eltwise **4.43 GB/s**, + * render 3.65, reduce ~3.8, topk 6.09 — against the **28.3 GB/s a single core's own + * drain-only arm measured**, and 510.8 for the card. Four independent families sitting 7x + * below ONE core's transaction wall and 120x below the part. The bind is the per-tile path + * on one Tensix; it is neither the byte count nor the descriptor count. + * + * Meanwhile the matmul runs 8 members and the GDN head split runs 4 (measured 2.70x at 4). + * The elementwise family ran ONE — 136 descriptors a token, 27,648 tiles, 0.0374 s, **19.7% + * of a 0.197 s token** — and nothing had ever been split across cores here. It is the + * cleanest parallelism on the card: `y = f(a, b)` per tile, no reduction, no ordering. + * + * --------------------------------------------------------------------------- + * WHAT A MEMBER IS, AND WHAT IT IS NOT + * --------------------------------------------------------------------------- + * A member is a WHOLE `eltwise` residency on its own Tensix, running the same program at the + * same circular-buffer placement, and its descriptor is the caller's with three bases + * advanced and the tile count divided. That means: + * + * - **NO BUFFER IS SHARED.** Every member has its own L1 and its own CBs. The one-producer + * rule (`resident_queue_protocol.h` rule 1) is about a CB with two writers; there is no + * such CB here, because there is no such CB between any two cores. + * - **NO DRAM BYTE IS SHARED.** Member `m` reads `[m*W, (m+1)*W)` of A and of B and writes + * the same range of the output, for `W = tile_count / N`. Pairwise disjoint by + * construction and asserted off-hardware in `tests/test_tt_eltwise_split.c`. + * - **NO KERNEL CHANGES AND NO NEW OPCODE.** The descriptor is the shipped + * `FT_TT_QUEUE_OP_ELTWISE` with different words. `resident_queue_protocol.h` is a + * content-provenance input to EVERY embedded image, so an opcode registered there would + * make all 23 programs stale; nothing is registered there. + * + * --------------------------------------------------------------------------- + * THE CORES, AND WHY THIS SET CANNOT COLLIDE + * --------------------------------------------------------------------------- + * Member 0 is `device->eltwise` on `PN_TT_CORE_ELTWISE` (73). Members 1..N-1 run upward from + * `PN_TT_ELTWISE_MEMBER_CORE_FIRST`, default **106**, and 106 is chosen as the first index + * ABOVE EVERY CORE ANY FAMILY CAN REACH AT ANY LEGAL SETTING OF ANY KNOB: + * + * 71 matmul member 0 72/73/74 reduce / eltwise / topk + * 75..105 matmul members 1..31 82/83/84 rope / memory / attention + * (PN_TT_MATMUL_MAX_CORES = 32, first = 75) + * 85/86 render widen / narrow 90 gdn member 0 + * 91, 103 the two head-norm slots 92..99 gdn head-split members (max 8) + * 95 gdnc (chunkwise) 100..102 the rectangle arm's control + * --------------------------------------------------------------------- + * 106..121 ELTWISE members 1..15 122..139 free + * + * That is a claim about the WHOLE map and not about today's defaults, which is the lesson + * the GDN family paid for: its 75 was clear of every other FAMILY and collided with matmul + * member 1 the day `PN_TT_MATMUL_CORES` moved, for 1560 mutual re-arms over 65 forwards with + * a healthy "residency up" logged each time. **A contested core does not fail; it computes + * badly** — 1.0e-2 contested against 6.9e-4 clean on the same descriptor. + * `pn_tt_prepare_family_on_core()` refuses a collision BY NAME and is still the guard; this + * default is only the first place to look, and `pn_tt_core_holder()` now walks these members + * too, so a later family that lands on one is told. + * + * --------------------------------------------------------------------------- + * THE KNOBS + * --------------------------------------------------------------------------- + * `PN_TT_ELTWISE_CORES=N` members, 1 = the pre-split path byte for byte + * `PN_TT_ELTWISE_MEMBER_CORE_FIRST=C` where members 1..N-1 start (106) + * `PN_TT_ELTWISE_DEPTH=D` CAP on the block depth, for the knee control below + * + * `PN_TT_ELTWISE_DEPTH` exists because **the depth MOVES WITH THE MEMBER COUNT and would + * otherwise be a confound in the sweep.** A residency is placed for one depth that must + * satisfy `2 * depth | tile_count` for every width it serves; Ornith's widths are 128 / 256 / + * 384, so unsplit it is 32 and at 8 members the slices are 16 / 32 / 48 and it is 8. A + * member-count ladder therefore also walks a depth ladder, and "it stopped at N" would not + * say which one stopped. `PN_TT_ELTWISE_DEPTH=8` at `PN_TT_ELTWISE_CORES=1` is the + * discriminating arm: one core, the split's depth, no split. + * + * --------------------------------------------------------------------------- + * THE ONE THING THE SPLIT BREAKS, AND IT IS NOT THE ARITHMETIC + * --------------------------------------------------------------------------- + * **A SLICE MOVES `C_BASE`, AND THE RESIDENT REPLICA TABLE IS KEYED ON THE EXACT TRIPLE.** + * `resident_vector_writer.c` mirrors an output strip by looking `(kVecCX, kVecCY, kVecCBase)` + * up in a table, and `ft_tt_op_replicas.h` says a miss "is the shipped path word for word". + * So member 0 hit the table and mirrored its slice, and every member above it MISSED and + * left its slice stale in seven banks — which the matmul members read. Every kernel retired + * clean and nothing refused, exactly as that header predicts a bad map behaves. + * + * Measured, and settled by a DISCRIMINATING NULL rather than by reading the code: with + * mirrors ON the split is `cmp` IDENTICAL at one member and 204 B / 8 B / 119 B / 57 B at + * 2 / 4 / 8 / 16 — wrong in proportion to the `(N-1)/N` of the strip that misses. With + * `PN_TT_ACTIVATION_REPLICAS=1` and no table to miss it is **IDENTICAL at 1, 2 AND 4**. The + * slice arithmetic was never the problem. + * + * `pn_tt_publish_replica_map()` now takes the split family's output strip and registers ONE + * SET PER SLICE, and publishes to the members as well — they were being armed with no table + * at all, which is not "no mirroring" but "whatever that L1 region held". + * + * @status SHIPPED ON at `PN_TT_ELTWISE_CORES=2`, the measured knee: 5.01 -> 5.17 tok/s + * against a control re-taken the same hour. `pn_tt_eltwise_group.c` carries the + * ladder, the fit and why 2 rather than 8. + * @issues None + * @todo None + */ + +#ifndef PN_TT_ELTWISE_GROUP_H +#define PN_TT_ELTWISE_GROUP_H + +#ifdef USE_TT + +#include +#include + +#include "tt/ft_tt_op_vector.h" + +#include "pn_tt_eltwise_split.h" + +typedef struct pn_tt_device pn_tt_device; + +/** Elementwise residencies one split may hold, member 0 included. The device's array holds + * one fewer, exactly as the GDN head split's does. */ +#define PN_TT_ELTWISE_MAX_MEMBERS PN_TT_ELTWISE_SPLIT_MAX_MEMBERS + +/** THE FAMILY'S PLACEMENT, in one place because two files now arm it. `pn_tt_oncard.c` arms + * member 0 and this module arms the rest, and both re-arm on a refusal — a second spelling + * of the program name or the core default would be two things that must agree. */ +#define PN_TT_ELTWISE_PROGRAM "eltwise" +#define PN_TT_CORE_ELTWISE_ENV "PN_TT_CORE_ELTWISE" +#define PN_TT_CORE_ELTWISE_DEFAULT 73u + +/** + * Decide the split for `tile_counts` BEFORE anything is placed, and report it. + * + * It runs before the placement rather than after because **the placement is sized from the + * SLICED width**: the input CBs are two blocks of the effective depth, and the depth is + * chosen from the tile counts descriptors will actually carry. Deciding the member count + * afterwards would place member 0 for a width no descriptor has. + * + * The member count is shrunk until it divides every width AND the sliced widths admit a + * common block depth. A shrink is reported, because a silent one shows up only as a number + * that did not move. + * + * @param device for the env knobs only; nothing is armed here + * @param tile_counts every width a descriptor of this residency will carry + * @param count how many of them + * @param out_members receives 1..PN_TT_ELTWISE_MAX_MEMBERS; never NULL + * @param out_widest receives the widest SLICE, for `place_cbs`; never NULL + * @param out_depth receives the ONE block depth every member runs; 0 when none serves + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_tt_eltwise_group_plan(const pn_tt_device *device, const uint32_t *tile_counts, + uint32_t count, uint32_t *out_members, uint32_t *out_widest, + uint32_t *out_depth); + +/** + * Arm members 1..`members`-1 on their own cores, with member 0's placement. + * + * They take the IDENTICAL circular-buffer placement member 0 has, and that is a property of + * the family rather than a convenience: `ft_tt_op_eltwise_place_cbs()` reads the effective + * depth and the three page sizes and nothing else, so a member owning 16 tiles places + * exactly what a member owning 128 does. + * + * **A MEMBER THAT FAILS TO ARM COLLAPSES THE SPLIT TO ONE**, loudly, rather than shrinking + * it the way the GDN head split does. A shrink there is safe because any head count divides; + * here the widths must divide the member count exactly, and 8 members shrinking to 5 is a + * ragged slice on a 128-tile residual — a descriptor the kernel refuses. Unsplit is always + * correct and always available: member 0's placement serves the whole width too, since + * `2 * depth` divides a slice and the slice divides the width. + * + * @param device the device, with `device->eltwise` already started + * @param members what `pn_tt_eltwise_group_plan()` returned + * @param placement the request member 0 was placed with + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_tt_eltwise_group_arm(pn_tt_device *device, uint32_t members, + const ft_tt_op_eltwise_request *placement); + +/** + * Run ONE elementwise descriptor across the whole set: ALL ENQUEUED BEFORE ANY IS AWAITED. + * + * The two loops being separate is the measurement and not a style. Enqueue-wait per member + * would run `members` SERIAL descriptors and print the flat line "it does not parallelise", + * produced by this function rather than by the card — the same trap `gdn_submit_delta_split()` + * and the core-scaling probe both call out. + * + * Time is charged to the SET, once, at the WHOLE descriptor's tile count, so the census key + * `(eltwise, function, tile_count)` is the same row at every member count and the + * `fixed + per_tile * tiles` fit is directly comparable across the ladder. + * + * @param device the device + * @param request the WHOLE descriptor, already validated by the caller + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_eltwise_group_submit(pn_tt_device *device, const ft_tt_op_eltwise_request *request); + +/** + * PM 7: the NEXT pn_tt_eltwise_group_submit() enqueues and returns WITHOUT awaiting; the + * descriptor stays pending until pn_tt_eltwise_group_settle() or the next eltwise descriptor. + * Streamed (the prefill band loop), the puller's AWAIT then lands where the settle is, so the + * chain's next ops run under it. The caller owns the hazard: settle before anything reads the + * output or rewrites an operand. `PN_TT_ELTWISE_DEFER=0` makes this a no-op. + */ +void pn_tt_eltwise_group_defer_next(pn_tt_device *device); + +/** Descriptors deferred so far (the report's proof the deferral is live). */ +uint64_t pn_tt_eltwise_group_deferred(const pn_tt_device *device); + +/** Await (and replay, if refused) the pending deferred descriptor. @return 0 when none. */ +int pn_tt_eltwise_group_settle(pn_tt_device *device); + +/** Release members 1..N-1 and set the count back to 1. Safe on a device that never split. + * + * A LEAKED TRIAD IS A WEDGED TENSIX for the next process to ask for that core, which is + * what the head-norm slots cost when `pn_tt_device_close()` released named fields and + * missed a set. This is a set; it is released by name from that function. */ +void pn_tt_eltwise_group_release(pn_tt_device *device); + +/** Live members, member 0 included. 1 when unsplit. + * + * The per-member ledger the report reads is `pn_tt_device_eltwise_member_stat()` in + * `pn_tt_device.h` — public, because the model's report is what consumes it. */ +uint32_t pn_tt_eltwise_group_members(const pn_tt_device *device); + +/** W1a: how many members a descriptor of `tile_count` tiles runs over — every member (decode + * + wide) for a wide descriptor, the decode split otherwise. */ +uint32_t pn_tt_eltwise_group_members_for(const pn_tt_device *device, uint32_t tile_count, + uint32_t function); + +/** Member `member`'s slice of a `tile_count`-tile descriptor run over `members` — the block + * split when `members` is the full wide set, the equal split otherwise. The replica map + * keys each member's mirrors from this, so the writer and the map cannot disagree. */ +bool pn_tt_eltwise_group_slice(const pn_tt_device *device, uint32_t tile_count, + uint32_t members, uint32_t member, uint32_t tile_bytes, + pn_tt_eltwise_slice *out_slice); + +#endif /* USE_TT */ +#endif /* PN_TT_ELTWISE_GROUP_H */ diff --git a/src/accel/tt/pn_tt_eltwise_split.c b/src/accel/tt/pn_tt_eltwise_split.c new file mode 100644 index 0000000..e06ec33 --- /dev/null +++ b/src/accel/tt/pn_tt_eltwise_split.c @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_eltwise_split.c + * @brief Implementation of the elementwise split arithmetic — member count and tile range. + * + * No device, no allocation, no I/O, and deliberately so: the header states why an address + * is the one thing here that fails silently, and this file is what a test with no card can + * hold to that. + * + * @status 0 checks failed off-hardware, `tests/test_tt_eltwise_split.c`. + * @issues None + * @todo None + */ + +#include "pn_tt_eltwise_split.h" + +uint32_t pn_tt_eltwise_split_member_limit(uint32_t requested, const uint32_t *tile_counts, + uint32_t count, uint32_t max_members) +{ + uint32_t members; + + if (!tile_counts || count == 0u || requested == 0u || max_members == 0u) + return 1u; + if (max_members > PN_TT_ELTWISE_SPLIT_MAX_MEMBERS) + max_members = PN_TT_ELTWISE_SPLIT_MAX_MEMBERS; + members = requested < max_members ? requested : max_members; + for (; members > 1u; members--) { + uint32_t index; + bool divides = true; + + for (index = 0; index < count && divides; index++) { + /* A zero width is a caller defect and cannot be split at all. Refusing the whole + * split rather than skipping the row keeps "every width this residency serves" + * literally true of the answer. */ + if (tile_counts[index] == 0u || (tile_counts[index] % members) != 0u) + divides = false; + } + if (divides) + return members; + } + return 1u; +} + +bool pn_tt_eltwise_split_slice(uint32_t tile_count, uint32_t members, uint32_t member, + uint32_t tile_bytes, pn_tt_eltwise_slice *out_slice) +{ + uint32_t per_member; + + if (!out_slice || members == 0u || members > PN_TT_ELTWISE_SPLIT_MAX_MEMBERS || + member >= members || tile_count == 0u || tile_bytes == 0u) + return false; + if ((tile_count % members) != 0u) + return false; + per_member = tile_count / members; + out_slice->first_tile = member * per_member; + out_slice->tile_count = per_member; + out_slice->byte_offset = out_slice->first_tile * tile_bytes; + return true; +} + +bool pn_tt_eltwise_split_slice_blocks(uint32_t tile_count, uint32_t members, uint32_t member, + uint32_t block_tiles, uint32_t tile_bytes, + pn_tt_eltwise_slice *out_slice) +{ + uint32_t blocks, base, extra, first_block, own; + + if (!out_slice || members == 0u || members > PN_TT_ELTWISE_SPLIT_MAX_MEMBERS || + member >= members || tile_count == 0u || tile_bytes == 0u || block_tiles == 0u || + (tile_count % block_tiles) != 0u) + return false; + blocks = tile_count / block_tiles; + base = blocks / members; + extra = blocks % members; + first_block = member * base + (member < extra ? member : extra); + own = base + (member < extra ? 1u : 0u); + out_slice->first_tile = first_block * block_tiles; + out_slice->tile_count = own * block_tiles; + out_slice->byte_offset = out_slice->first_tile * tile_bytes; + return true; +} diff --git a/src/accel/tt/pn_tt_eltwise_split.h b/src/accel/tt/pn_tt_eltwise_split.h new file mode 100644 index 0000000..b4ab064 --- /dev/null +++ b/src/accel/tt/pn_tt_eltwise_split.h @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_eltwise_split.h + * @brief The ARITHMETIC of splitting one elementwise descriptor across N Tensix: how many + * members a set of widths admits, and which contiguous tile range each member owns. + * + * It is a separate module from `pn_tt_eltwise_group.h` for the reason `pn_tt_gdn_layout.h` + * is separate from `pn_tt_gdn.c`: everything here is pure integer arithmetic over a tile + * count, so it is provable with no card, no device and no `USE_TT` — and the one thing a + * split can get wrong that no error will ever report is an address. + * + * --------------------------------------------------------------------------- + * WHY AN ELEMENTWISE OP IS THE CLEANEST SPLIT ON THE CARD + * --------------------------------------------------------------------------- + * `resident_eltwise_reader.c` reads tile `t` of a stream at `base + t * TILE_BYTES`, + * unconditionally and in order — its own header calls this "the easy case", against the + * matmul reader, which had to argue that a block's B tiles are consecutive only under an + * N-major weight grid. So member `m` of an N-way split runs the SAME descriptor with + * `tile_count / N` and every base advanced by `m * (tile_count / N) * tile_bytes`, and the + * members touch pairwise-disjoint byte ranges of A, of B and of the output. There is no + * shared operand, no reduction, no ordering: `y = f(a, b)` per tile. + * + * --------------------------------------------------------------------------- + * WHY THE SPLIT MUST DIVIDE EXACTLY, WHERE THE MATMUL GROUP'S MAY BE RAGGED + * --------------------------------------------------------------------------- + * `pn_tt_matmul_group_place_weight_columns()` hands the remainder out one column at a time + * and carries a RUNNING SUM, because its members' widths only have to concatenate. This + * family cannot do that, and the reason is the circular buffers rather than the answer: + * + * 1. **The block-depth invariant is per descriptor.** A residency is placed for ONE depth, + * and every descriptor it will ever run must satisfy `2 * depth | tile_count` — the FIFO + * write pointer wraps only on an exact hit of the limit, so a descriptor that leaves it + * elsewhere walks the NEXT one up L1 with no error (`pn_tt_common_depth()`, and the top-k + * reader that landed on the queue control block at 0x170000 without faulting). A ragged + * slice is a tile count off that grid on exactly one member. + * 2. **A set is as slow as its longest member**, and every member is a whole residency + * paying a whole descriptor's fixed cost. An uneven split buys the biggest slice's time + * and wastes the rest — the same argument `gdn_head_member_count()` makes about v-heads. + * + * So the member count is clamped to a COMMON DIVISOR of every width the residency was armed + * for, and a width that does not divide shrinks the split for all of them rather than + * splitting some descriptors and not others. One depth, one placement, every member. + * + * @status Off-hardware only so far: `tests/test_tt_eltwise_split.c`. The card arm that + * consumes it is `pn_tt_eltwise_group.h`. + * @issues None + * @todo None + */ + +#ifndef PN_TT_ELTWISE_SPLIT_H +#define PN_TT_ELTWISE_SPLIT_H + +#include +#include + +/** Elementwise residencies one split may hold, member 0 included. + * + * SIXTEEN because that is what Ornith's narrowest elementwise width admits and not because + * the card stops there: the residual adds are 128 tiles, and 128/16 = 8 tiles is still two + * blocks of depth 4. Thirty-two would leave 4 tiles a member at depth 2, which is a + * descriptor that is nearly all fixed cost. Raising it is a measurement and sixteen more + * Tensix, not an edit. */ +#define PN_TT_ELTWISE_SPLIT_MAX_MEMBERS 16u + +/** One member's share of a descriptor: a contiguous tile range and the byte offset every + * operand base advances by to reach it. */ +typedef struct pn_tt_eltwise_slice { + uint32_t first_tile; + uint32_t tile_count; + uint32_t byte_offset; /**< `first_tile * tile_bytes`, for the operand asked about */ +} pn_tt_eltwise_slice; + +/** + * The largest member count at or below `requested` that divides EVERY width exactly. + * + * Walks down from the clamp rather than taking a GCD, because the answer wanted is "the + * biggest split this residency can run", not "the biggest split that exists": `requested` is + * an operator's knob and the clamp is the array's size. + * + * @param requested what the run asked for; 0 or 1 means no split + * @param tile_counts every width a descriptor of this residency will carry + * @param count how many of them; 0 refuses + * @param max_members the caller's array bound, itself clamped to the module's + * @return 1..max_members; 1 whenever no larger count divides every width + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +uint32_t pn_tt_eltwise_split_member_limit(uint32_t requested, const uint32_t *tile_counts, + uint32_t count, uint32_t max_members); + +/** + * Which tiles member `member` of an `members`-way split owns, and at what byte offset. + * + * REFUSES rather than rounding when the split does not divide, because the caller that + * rounds here writes one member's answer over another's — a plausible wrong answer with no + * error anywhere, which is the failure this whole module exists to make unreachable. + * + * @param tile_count the WHOLE descriptor's tile count + * @param members 1..PN_TT_ELTWISE_SPLIT_MAX_MEMBERS + * @param member 0..members-1 + * @param tile_bytes the operand's page size, for the byte offset + * @param out_slice receives the range; untouched on a refusal + * @return true, or false when an argument is out of range or the split is ragged + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +bool pn_tt_eltwise_split_slice(uint32_t tile_count, uint32_t members, uint32_t member, + uint32_t tile_bytes, pn_tt_eltwise_slice *out_slice); + +/** + * PHASE W1a — the WIDE split: member `member`'s share of `tile_count` dealt out in whole + * BLOCKS of `block_tiles` (the residency's `2 * depth`), the first `blocks % members` members + * one block more. Ragged by blocks and never by tiles, so every slice stays on the CB grid the + * depth rule protects whatever the member count — which lets a prefill-wide descriptor + * (Mt x 384 tiles, Mt odd) spread over members the width does not divide. + * + * A member past the last block gets an EMPTY slice (tile_count 0) at the end of the + * descriptor; the caller skips it. The slices tile [0, tile_count) exactly. + * + * @return true, or false when an argument is out of range or `tile_count` is not whole blocks + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +bool pn_tt_eltwise_split_slice_blocks(uint32_t tile_count, uint32_t members, uint32_t member, + uint32_t block_tiles, uint32_t tile_bytes, + pn_tt_eltwise_slice *out_slice); + +#endif /* PN_TT_ELTWISE_SPLIT_H */ diff --git a/src/accel/tt/pn_tt_fp32_strip.c b/src/accel/tt/pn_tt_fp32_strip.c new file mode 100644 index 0000000..d087b5b --- /dev/null +++ b/src/accel/tt/pn_tt_fp32_strip.c @@ -0,0 +1,132 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_fp32_strip.c + * @brief Implementation of the Float32 activation strip. See pn_tt_fp32_strip.h for why it + * is a second type and not a flag on `pn_tt_activation`. + * + * @status None + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_fp32_strip.h" + +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_gdn_layout.h" + +int pn_tt_fp32_row0_move(pn_tt_device *device, uint32_t bank, uint32_t address, uint32_t tiles, + float *values, bool reading) +{ + return pn_tt_fp32_row_move(device, bank, address, 0u, tiles, values, reading); +} + +int pn_tt_fp32_row_move(pn_tt_device *device, uint32_t bank, uint32_t address, uint32_t row, + uint32_t tiles, float *values, bool reading) +{ + const ft_tt_noc_access *bulk; + uint16_t x; + uint16_t y; + uint32_t tile; + uint32_t chunk; + + if (!device || !values || bank >= device->dram_bank_count || + row >= PN_TT_GDN_LAYOUT_TILE_DIM) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + bulk = &device->runtime.bulk_dram; + x = (uint16_t)device->dram[bank].x; + y = (uint16_t)device->dram[bank].y; + for (tile = 0; tile < tiles; tile++) { + for (chunk = 0; chunk < PN_TT_GDN_LAYOUT_ROW0_CHUNKS; chunk++) { + float *host = values + (size_t)tile * PN_TT_TILE_DIM + + (size_t)chunk * PN_TT_GDN_LAYOUT_ROW0_CHUNK_ELEMS; + const uint64_t at = (uint64_t)address + pn_tt_gdn_row_offset(tile, row, chunk); + const int rc = + reading ? bulk->read(bulk->user, x, y, at, host, + PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES) + : bulk->write(bulk->user, x, y, at, host, + PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES); + + if (rc != FT_TT_OK) + return -1; + } + } + if (reading) + device->bytes_read += + (size_t)tiles * PN_TT_GDN_LAYOUT_ROW0_CHUNKS * PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES; + else + device->bytes_written += + (size_t)tiles * PN_TT_GDN_LAYOUT_ROW0_CHUNKS * PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES; + return 0; +} + +int pn_tt_fp32_strip_alloc(pn_tt_device *device, uint32_t tile_count, + pn_tt_fp32_strip *out_strip) +{ + const size_t bytes = (size_t)tile_count * PN_TT_TILE_FP32; + ft_tt_buffer buffer; + uint32_t bank; + + if (!device || !out_strip || tile_count == 0u || device->dram_bank_count == 0u) + return -1; + bank = device->strip_bank_cursor++ % device->dram_bank_count; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes, &buffer) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating a %u-tile Float32 strip\n", tile_count); + return -1; + } + out_strip->bank = buffer.logical_bank; + out_strip->address = (uint32_t)buffer.address; + out_strip->tile_count = tile_count; + /* THE PRECONDITION every row-0 write depends on and none of them can check. */ + if (pn_tt_zero_dram(device, out_strip->bank, out_strip->address, bytes) != 0) { + fprintf(stderr, "pn_tt: could not stage a %u-tile Float32 strip zeroed\n", tile_count); + return -1; + } + return 0; +} + +int pn_tt_fp32_strip_upload(pn_tt_device *device, const pn_tt_fp32_strip *strip, + const float *values, uint32_t count) +{ + if (!strip || !values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + if (count / PN_TT_TILE_DIM > strip->tile_count) + return -1; + /* The mover never writes through `values` on the outbound leg; the cast is the price of + * one function serving both directions. */ + return pn_tt_fp32_row0_move(device, strip->bank, strip->address, + count / PN_TT_TILE_DIM, (float *)values, false); +} + +int pn_tt_fp32_strip_download(pn_tt_device *device, const pn_tt_fp32_strip *strip, + float *out_values, uint32_t count) +{ + if (!strip || !out_values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + if (count / PN_TT_TILE_DIM > strip->tile_count) + return -1; + return pn_tt_fp32_row0_move(device, strip->bank, strip->address, + count / PN_TT_TILE_DIM, out_values, true); +} + +ft_tt_op_operand pn_tt_fp32_strip_operand(const pn_tt_device *device, + const pn_tt_fp32_strip *strip) +{ + ft_tt_op_operand operand; + + operand.x = (uint16_t)device->dram[strip->bank].x; + operand.y = (uint16_t)device->dram[strip->bank].y; + operand.address = strip->address; + operand.tile_bytes = PN_TT_TILE_FP32; + return operand; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_fp32_strip.h b/src/accel/tt/pn_tt_fp32_strip.h new file mode 100644 index 0000000..f859717 --- /dev/null +++ b/src/accel/tt/pn_tt_fp32_strip.h @@ -0,0 +1,116 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_fp32_strip.h + * @brief A `[1, 32*tile_count]` activation strip of **Float32** tiles — what a program + * placed at a 4096-byte page reads and writes, where `pn_tt_activation` is the + * 2048-byte bf16 one. + * + * --------------------------------------------------------------------------- + * WHY A SECOND STRIP TYPE RATHER THAN A FLAG ON THE FIRST + * --------------------------------------------------------------------------- + * `pn_tt_activation` is bf16 in three separate places — the allocation size, the row-0 + * chunk geometry (`ft_tt_tile_row0.h` reads 64 bytes and discards 32) and the operand's + * `tile_bytes` — and EVERY op in the backend but one takes it. Adding a page size to it + * would put a runtime branch on the shipped decode path to serve one probe and one + * residency, and a strip whose format is a field is a strip that can be handed to the wrong + * program with no refusal anywhere. Two types cannot be confused by the compiler. + * + * **In fp32 logical row 0 is two EXACTLY 64-byte pieces**, which is the DRAM read + * granularity — the bf16 path has to read 64 and discard 32. So this strip is cheaper per + * element to move than the bf16 one, not more expensive. + * + * The address arithmetic is `pn_tt_gdn_row0_offset()`. It is named for the family that + * needed it first, but it is the generic fp32-tile row-0 layout and it is the one piece of + * this file that is separately proven (`tests/test_gdn_layout.c`). + * + * @status Used by the per-head reduce residency (`pn_tt_head_norm.c`) and by the GDN family, + * which delegates its own row-0 mover here. + * @issues None + * @todo None + */ + +#ifndef PN_TT_FP32_STRIP_H +#define PN_TT_FP32_STRIP_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_device.h" +#include "tt/ft_tt_op_dispatch.h" + +/** One strip of consecutive Float32 tiles in card DRAM. */ +typedef struct pn_tt_fp32_strip { + uint32_t bank; /**< logical DRAM bank */ + uint32_t address; /**< byte offset within that bank */ + uint32_t tile_count; /**< features / 32 */ +} pn_tt_fp32_strip; + +/** + * Carve a `tile_count`-tile Float32 strip and stage it ZEROED. + * + * The zeroing is the precondition every row-0 write depends on and none of them can check: + * only row 0 is ever written, so rows 1-31 have to start zero and stay zero for a whole-tile + * reduce over them to contribute nothing. + * + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_fp32_strip_alloc(pn_tt_device *device, uint32_t tile_count, + pn_tt_fp32_strip *out_strip); + +/** Write `count` floats into logical row 0 of a strip, with no conversion at all. */ +int pn_tt_fp32_strip_upload(pn_tt_device *device, const pn_tt_fp32_strip *strip, + const float *values, uint32_t count); + +/** Read logical row 0 of a strip back as `count` floats. */ +int pn_tt_fp32_strip_download(pn_tt_device *device, const pn_tt_fp32_strip *strip, + float *out_values, uint32_t count); + +/** An operand descriptor for a Float32 strip: its endpoint and the 4096-byte page. */ +ft_tt_op_operand pn_tt_fp32_strip_operand(const pn_tt_device *device, + const pn_tt_fp32_strip *strip); + +/** The same for an ordinary bf16 activation strip: its endpoint and the 2048-byte page. + * + * It lives beside the fp32 one because the two are read TOGETHER now — the strip bridge and + * the per-head norms take raw operands so one body can serve both pages, and a caller + * outside `src/accel/tt/` needs to name both sides of the seam. */ +ft_tt_op_operand pn_tt_operand(const pn_tt_device *device, const pn_tt_activation *activation); + +/** + * Move logical row 0 of `tiles` consecutive fp32 tiles at a DRAM address, in either + * direction. The primitive both this file's upload/download and the GDN stager use. + * + * `values` is `tiles * 32` floats and the two pieces of each row are ADJACENT in it, so no + * host staging buffer is needed in either direction. + * + * @param reading true reads the card into `values`, false writes `values` to the card + * @return 0, or -1 + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_fp32_row0_move(pn_tt_device *device, uint32_t bank, uint32_t address, uint32_t tiles, + float *values, bool reading); + +/** + * The same move for ANY logical row of the tiles. `pn_tt_fp32_row0_move()` is `row = 0`. + * + * Only the chunk phase-isolation arm uses a non-zero row: the decode GDN family works in row + * 0 and the chunkwise family works in rows 0..C-1 of the same buffers, so isolating one of + * the two chunk kernels means shuttling row `t` to row 0 and the answer back. Every move is + * an exact fp32 copy — the arm changes WHICH KERNEL runs and nothing about the arithmetic. + * + * @param row 0..31 + * @return 0, or -1 + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_fp32_row_move(pn_tt_device *device, uint32_t bank, uint32_t address, uint32_t row, + uint32_t tiles, float *values, bool reading); + +#endif /* USE_TT */ +#endif /* PN_TT_FP32_STRIP_H */ diff --git a/src/accel/tt/pn_tt_gate.c b/src/accel/tt/pn_tt_gate.c new file mode 100644 index 0000000..26fa55d --- /dev/null +++ b/src/accel/tt/pn_tt_gate.c @@ -0,0 +1,271 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gate.c + * @brief Implementation of the merged GDN output gate. See pn_tt_gate.h — the argument for why + * a MIXED CB table is correct HERE and wrong in `gdn_float32.cbtable` lives there. + * + * @status None + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gate.h" + +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_replica_map.h" + +/** + * Which enabled Tensix the merged gate drives. + * + * 87 is free in the map `pn_tt_render.c` documents: the render pair holds 85/86, the GDN + * residency 90, rope/memory/attention 82/83/84, and eight matmul members stop at 82. It sits + * inside the range sixteen matmul members would reach, which is not an oversight — + * `pn_tt_prepare_family()` refuses a collision BY NAME against every live residency, so a run + * that wants both is told rather than silently sharing a core. + */ +#define PN_TT_CORE_GATE_NARROW_ENV "PN_TT_CORE_GATE_NARROW" +#define PN_TT_CORE_GATE_NARROW_DEFAULT 87u + +#define PN_TT_GATE_NARROW_PROGRAM "gate_narrow" +#define PN_TT_GATE_NARROW_NAME "gate-narrow" + +/** + * Whether the merged gate runs. **DEFAULT OFF — it was flipped ON on 2026-08-25 and flipped + * back the same day, because it is right in DECODE and it BREAKS THE CHUNKWISE PREFILL.** + * + * WHAT IT IS WORTH, MEASURED. Seven decode arms, one model load each, `company_after = 0` + * throughout: `cmp` **IDENTICAL** (189 B, md5 80fc6259), **20,448 -> 19,296 descriptors a + * token** (exactly 24 a token over 48 tokens, and -25,165,824 B), on-card **0.114 -> 0.112 + * s/token** against a control that read 0.114 TWICE, wall 5.17 / 5.20 control against 5.22. + * The predicted ~7.5 ms was 6x optimistic and the census says why: the merged `silu_mul` costs + * **267.9 us a descriptor against the plain one's ~170** (2.097 us/tile against 0.998), because + * the fp32 unpack of a 4,096-byte operand is real work the bf16 unpack it replaces was not + * doing. Removing `render_narrow` is worth 0.0026 s/token and the heavier multiply gives back + * ~0.0024. Red arm `PN_TT_RED=gate_narrow_swap` DIFFERS at 190 B; both collision refusals fire. + * + * --------------------------------------------------------------------------- + * WHY IT IS OFF: IT BREAKS `gdnc`, ON TWO ARMS, ABSENT FROM THE THIRD + * --------------------------------------------------------------------------- + * With the chunkwise prefill armed (`PN_TT_PREFILL=1 PN_TT_PREFILL_GDNC=1`) and this default + * ON, both a C = 32 arm and a C = 16 arm produce: + * + * pn_tt: gdnc descriptor 4 did not complete: error + * pn_tt: render-narrow descriptor 1 did not complete: error + * pn_tt: the gdnc residency refused a descriptor — retiring it and arming a fresh one on 95 + * + * after which documents 1 and 2 **DECLINE the batched prefill and run per token**. The same + * two arms with `PN_TT_GATE_NARROW=0` are clean, and one of them came back BYTE-IDENTICAL to a + * dump recorded before this residency existed — so the variable is this default and nothing + * else in the session. **The two-pass gate's `--require-prefill` refused the dump rather than + * scoring a sequential arm under a chunkwise label; that guard is the only reason this was not + * 339 records of the wrong thing.** + * + * NOT DIAGNOSED, AND DO NOT ASSUME THE CORE MAP. `gate_narrow` sits on 87 and `gdnc` on 95, so + * it is not a core collision, and `pn_tt_core_holder()` refuses those by name anyway. The open + * suspicion is arming ORDER: this residency is armed lazily, and the one thing already on + * record about lazy arming in this family is that **a residency armed after + * `pn_tt_publish_replica_map()` reads whatever was at that fixed L1 address** — the note beside + * the render pair says both of those still have that gap, and `render_narrow` is the other + * family erroring here. That is a hypothesis with an obvious first arm and no measurement yet. + */ +static bool gate_narrow_requested(void) +{ + const char *want = getenv(PN_TT_GATE_NARROW_ENV); + + return want && *want && atoi(want) != 0; +} + +int pn_tt_device_arm_gate_narrow(pn_tt_device *device, uint32_t tiles) +{ + pn_tt_gate_residency *slot; + ft_tt_op_eltwise_request request; + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + /* The depth search must not write into the residency's own CB buffer: `prepare_family` has + * not run yet, and it zeroes the whole handle. */ + unsigned char scratch_cb[sizeof(device->gate.residency.cb_buffer)]; + uint64_t scratch_mask; + uint32_t depth; + + if (!device) + return -1; + if (tiles == 0u || device->gate.residency.running) + return 0; + if (!gate_narrow_requested()) + return 0; + if (!pn_tt_program_is_present(PN_TT_GATE_NARROW_PROGRAM)) { + fprintf(stderr, "pn_tt: no `%s` program — the GDN gate keeps the render_narrow + " + "silu_mul pair\n", PN_TT_GATE_NARROW_PROGRAM); + return 0; + } + + /* THE DEEPEST BLOCK THAT BOTH DIVIDES THE RUN AND FITS THE ARENA. The B stream is 4096 B + * a page here while A, the scratch and the output are 2048, so the arena question is not + * the shipped elementwise one and `place_cbs` is the only thing that can answer it. + * Walking down from the divisor rather than refusing is what stops a page size from + * silently costing a whole residency. */ + for (depth = pn_tt_common_depth(&tiles, 1u, PN_TT_ONCARD_MAX_DEPTH); depth > 0u; + depth = pn_tt_common_depth(&tiles, 1u, depth - 1u)) { + memset(&request, 0, sizeof(request)); + request.function = FT_TT_OP_VECTOR_FN_SILU_MUL; + request.tile_count = tiles; + request.prefetch_tiles = depth; + request.input_a.tile_bytes = PN_TT_TILE_BF16; + request.input_b.tile_bytes = PN_TT_TILE_FP32; + request.output.tile_bytes = PN_TT_TILE_BF16; + if (ft_tt_op_eltwise_place_cbs(&request, placed, scratch_cb, sizeof(scratch_cb), + &scratch_mask) == FT_TT_OK) + break; + } + if (depth == 0u) { + fprintf(stderr, "pn_tt: no block depth both divides a %u-tile gate and fits the L1 " + "arena at %u B / %u B input pages\n", tiles, PN_TT_TILE_BF16, + PN_TT_TILE_FP32); + return 0; + } + + slot = &device->gate; + if (!pn_tt_prepare_family(device, &slot->residency, PN_TT_GATE_NARROW_PROGRAM, + PN_TT_GATE_NARROW_NAME, PN_TT_CORE_GATE_NARROW_ENV, + PN_TT_CORE_GATE_NARROW_DEFAULT, PN_TT_ONCARD_TRIAD_MASK)) + return 0; + memset(&request, 0, sizeof(request)); + request.function = FT_TT_OP_VECTOR_FN_SILU_MUL; + request.tile_count = tiles; + request.prefetch_tiles = depth; + request.input_a.tile_bytes = PN_TT_TILE_BF16; + request.input_b.tile_bytes = PN_TT_TILE_FP32; + request.output.tile_bytes = PN_TT_TILE_BF16; + if (ft_tt_op_eltwise_place_cbs(&request, placed, slot->residency.cb_buffer, + sizeof(slot->residency.cb_buffer), + &slot->residency.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gate-narrow circular buffers do not fit at %u tiles, " + "depth %u\n", tiles, depth); + pn_tt_residency_release(&slot->residency); + return 0; + } + if (!pn_tt_start_family(&slot->residency)) + return 0; + /* THE TABLE THIS CORE READS MUST BE A WRITTEN FACT. This residency runs + * `resident_vector_writer.c` and it arms AFTER `pn_tt_publish_replica_map()` has already + * gone out, so without this line it reads whatever is at the fixed L1 replica-table + * address — which is not "no mirroring" but "whatever a previous process left there", the + * state that cost the elementwise split its `cmp`. `ssm_gated` is not a mirrored strip, so + * EMPTY is the correct table and not a stub. */ + if (pn_tt_publish_empty_replica_map(device, &slot->residency, "the gate-narrow residency") + != 0) { + pn_tt_residency_stop(&slot->residency); + pn_tt_residency_release(&slot->residency); + return 0; + } + slot->tiles = tiles; + slot->depth = depth; + fprintf(stderr, "pn_tt: gate-narrow is up on core %u — %u tiles, silu(bf16) * fp32 -> " + "bf16, depth %u. render_narrow leaves the decode token.\n", + slot->residency.core_index, tiles, depth); + return 0; +} + +bool pn_tt_gate_narrow_is_armed(const pn_tt_device *device) +{ + return device && device->gate.residency.running; +} + +int pn_tt_gate_narrow(pn_tt_device *device, ft_tt_op_operand a_bf16, ft_tt_op_operand b_fp32, + ft_tt_op_operand out_bf16, uint32_t tiles) +{ + pn_tt_gate_residency *slot; + ft_tt_op_eltwise_request request; + uint32_t words[FT_TT_OP_VECTOR_WORDS]; + double started; + + if (!device) + return -1; + slot = &device->gate; + if (!slot->residency.running) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. A gate whose + * `z` a queued matvec is still writing reads the PREVIOUS token's activation and nothing + * anywhere refuses it. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + /* THE PAGE SIZE IS A THREE-SIDED CONTRACT WITH NO CARD-SIDE CHECK: the DRAM operand, the L1 + * circular buffer and the reader's compile-time TILE_BYTES / TILE_BYTES_B all have to + * agree, and the device can see none of it. This is the only place that can. */ + if (a_bf16.tile_bytes != PN_TT_TILE_BF16 || b_fp32.tile_bytes != PN_TT_TILE_FP32 || + out_bf16.tile_bytes != PN_TT_TILE_BF16) { + fprintf(stderr, "pn_tt: gate-narrow wants %u/%u -> %u B operands and was handed " + "%u/%u -> %u\n", PN_TT_TILE_BF16, PN_TT_TILE_FP32, PN_TT_TILE_BF16, + a_bf16.tile_bytes, b_fp32.tile_bytes, out_bf16.tile_bytes); + return -1; + } + if (tiles == 0u || tiles > slot->tiles || + ft_tt_op_block_depth(tiles, slot->depth) != slot->depth || + (tiles % (2u * slot->depth)) != 0u) { + fprintf(stderr, "pn_tt: %u tiles is not a whole number of depth-%u blocks the " + "gate-narrow residency was placed for (%u)\n", tiles, slot->depth, + slot->tiles); + return -1; + } + + memset(&request, 0, sizeof(request)); + /* RED PROOF: the two operands of the gate swap. `silu(y) * z` is not `silu(z) * y`, both + * strips are the same width, and no shape check anywhere can see the difference — the same + * mutation `pn_tt_silu_mul()` carries, on the descriptor that replaces it. */ + if (pn_tt_red("gate_narrow_swap")) { + request.function = FT_TT_OP_VECTOR_FN_SILU_MUL; + request.input_a = b_fp32; + request.input_b = a_bf16; + } else { + request.function = FT_TT_OP_VECTOR_FN_SILU_MUL; + request.input_a = a_bf16; + request.input_b = b_fp32; + } + request.tile_count = tiles; + request.prefetch_tiles = slot->depth; + request.output = out_bf16; + if (ft_tt_op_eltwise_parameters(&request, words) != FT_TT_OK) + return -1; + pn_tt_vec_probe_apply(words); + + started = pn_tt_now_seconds(); + if (pn_tt_residency_submit(&slot->residency, FT_TT_QUEUE_OP_ELTWISE, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + /* The words address DRAM only, so the same descriptor is valid for a fresh residency — + * the re-arm argument every other family here makes. */ + fprintf(stderr, "pn_tt: the gate-narrow residency refused a descriptor — retiring it " + "by ASKING and arming a fresh one\n"); + pn_tt_residency_stop(&slot->residency); + pn_tt_residency_release(&slot->residency); + device->oncard_restarts++; + if (!pn_tt_prepare_family(device, &slot->residency, PN_TT_GATE_NARROW_PROGRAM, + PN_TT_GATE_NARROW_NAME, PN_TT_CORE_GATE_NARROW_ENV, + PN_TT_CORE_GATE_NARROW_DEFAULT, PN_TT_ONCARD_TRIAD_MASK) || + !pn_tt_start_family(&slot->residency) || + pn_tt_residency_submit(&slot->residency, FT_TT_QUEUE_OP_ELTWISE, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + pn_tt_oncard_account(device, PN_TT_GATE_NARROW_NAME, "silu_mul", tiles, + pn_tt_now_seconds() - started); + return 0; +} + +void pn_tt_gate_release(pn_tt_device *device) +{ + if (!device) + return; + if (device->gate.residency.running) { + pn_tt_residency_stop(&device->gate.residency); + pn_tt_residency_release(&device->gate.residency); + } + memset(&device->gate, 0, sizeof(device->gate)); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gate.h b/src/accel/tt/pn_tt_gate.h new file mode 100644 index 0000000..45e11b7 --- /dev/null +++ b/src/accel/tt/pn_tt_gate.h @@ -0,0 +1,118 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gate.h + * @brief THE GDN OUTPUT GATE WITH THE fp32->bf16 SEAM FOLDED INTO IT — one descriptor that + * replaces `render_narrow` + `silu_mul`, reading the fp32 `y_norm` strip DIRECTLY. + * + * --------------------------------------------------------------------------- + * WHAT IT DELETES, AND WHY THAT IS FREE + * --------------------------------------------------------------------------- + * Today the GDN block's last two steps are: + * + * render_narrow(y_norm_fp32) -> y_bridge_bf16 24 descriptors/token + * silu_mul(z_bf16, y_bridge_bf16) -> gated_bf16 24 descriptors/token + * + * The first exists ONLY to change format. It reads 128 fp32 tiles, reads 128 MORE fp32 tiles + * of ZEROS (the family has no identity, so a copy is `x + 0`), and writes 128 bf16 tiles — + * 1,310,720 B a layer, 31,457,280 B a token, to move no information at all. + * + * `gate_narrow` is the SECOND descriptor with a MIXED CB table: cb 1 is Float32 while cb 0, + * cb 2 and cb 16 stay Float16_b. `conditional_destination_format()` then gives cb 1 + * `unpack_src Float32 / unpack_dst Float16_b / tile_size 4096` — verified off-hardware to be + * the SAME descriptor triple `render_narrow.cbtable` gives its own fp32 input. So the + * unpacker performs the identical truncating fp32->bf16 conversion, on the identical bits, one + * descriptor and one DRAM round trip earlier than the multiply that consumes it. **The + * arithmetic is the same and the operand the multiplier sees is the same bits.** That is why + * this is expected to be `cmp` IDENTICAL and why anything else is a finding. + * + * The one thing that had to change is `resident_eltwise_reader.c`, which carried ONE + * `TILE_BYTES` for both input streams. It now takes a fifth compile-time arg for B, refuses a + * zero on a binary descriptor rather than reading 0 bytes a tile, and every pre-existing + * recipe passes its own A size — so `eltwise`, `render_widen` and `render_narrow` rebuild + * byte-identical, which is the control that says the new argument is inert when unused. + * + * --------------------------------------------------------------------------- + * WHY NOT ON THE SHARED ELEMENTWISE RESIDENCY + * --------------------------------------------------------------------------- + * A residency runs ONE program, and a program's CB formats are baked into its compute images. + * The shipped `eltwise` residency serves the residual ADDs, the SwiGLU and attention's + * SIGMOID+MUL, all of which are bf16 on both inputs; giving it a Float32 cb 1 would make every + * one of them read a bf16 strip as fp32. So the gate gets its own core, and the descriptor + * COUNT does not change by moving it — 24 gate descriptors were already being issued, they + * are simply issued to a different residency. What goes is the 24 renders. + * + * Its width is Wt=128, which is BELOW the measured elementwise split break-even (~143 tiles at + * the first doubling; a 128-tile MUL measured 101.7 -> 146.9 us, WORSE, at two members). So a + * solo residency is not a compromise here, it is the right side of that knee. + * + * --------------------------------------------------------------------------- + * WHY NOT THE GDN WRITER — REFUTED, DO NOT RETRY + * --------------------------------------------------------------------------- + * `gdn_float32.cbtable`'s cb 16 carries the UPDATED RECURRENT STATE as well as `y`, interleaved + * per tile-row. One CB, one page size, two sinks: narrowing it to bf16 narrows the state the + * next token reads. The seam cannot move upstream of the gated RMS norm. + * + * @status DEFAULT OFF. Flipped ON 2026-08-25 and flipped BACK the same day. In DECODE it is + * right and cheap — `cmp` IDENTICAL, 20,448 -> 19,296 descriptors a token, -25,165,824 B, + * on-card 0.114 -> 0.112 s/token against a control that read 0.114 twice. With the CHUNKWISE + * PREFILL armed it puts `gdnc` and `render-narrow` into `did not complete: error` and the + * prefill silently declines to the per-token path. See pn_tt_gate.c for the two arms that + * show it and the one that does not. + * @issues Breaks `gdnc` when the chunkwise prefill is armed. Undiagnosed; not a core collision. + * @issues None + * @todo None + */ + +#ifndef PN_TT_GATE_H +#define PN_TT_GATE_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_device.h" +#include "tt/ft_tt_op_dispatch.h" + +/** The flag. Default OFF; `1` arms the merged gate and takes `render_narrow` out of decode. + * DO NOT flip the default again without a chunkwise-prefill arm beside the decode one. */ +#define PN_TT_GATE_NARROW_ENV "PN_TT_GATE_NARROW" + +/** + * Arm the merged gate for the widest `gated` strip it will be asked for. + * + * Never fatal: without the `gate_narrow` program, or with the flag unset, the caller keeps the + * render + silu_mul pair it had. The core is `PN_TT_CORE_GATE_NARROW` (87 by default — free + * between the render pair at 85/86 and the GDN residency at 90) and is refused BY NAME against + * any live family, exactly like every other residency here. + * + * @param tiles the widest bf16 output strip, in tiles; 0 skips + * @return 0 (even when nothing armed), -1 on a bad argument + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_gate_narrow(pn_tt_device *device, uint32_t tiles); + +/** Is the merged gate live? A caller that gets `false` must use render_narrow + silu_mul. */ +bool pn_tt_gate_narrow_is_armed(const pn_tt_device *device); + +/** + * `out = silu(a) * b`, with `a` and `out` bf16 tiles and `b` FP32 tiles. + * + * The operands are raw for the same reason `pn_tt_render`'s are: the three buffers this joins + * are three different C types differing only in page size. Each side's page size is checked + * here, because a 2048-byte operand under a 4096-byte stream is the failure mode with no + * card-side refusal anywhere (measured on the reduce family: constants built at 2048 under a + * 4096 program score 1.0e+00 with nothing refusing). + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gate_narrow(pn_tt_device *device, ft_tt_op_operand a_bf16, ft_tt_op_operand b_fp32, + ft_tt_op_operand out_bf16, uint32_t tiles); + +/** Release the residency. */ +void pn_tt_gate_release(pn_tt_device *device); + +#endif /* USE_TT */ +#endif /* PN_TT_GATE_H */ diff --git a/src/accel/tt/pn_tt_gdn.c b/src/accel/tt/pn_tt_gdn.c new file mode 100644 index 0000000..deb112c --- /dev/null +++ b/src/accel/tt/pn_tt_gdn.c @@ -0,0 +1,2703 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn.c + * @brief The host stager for the GATED-DELTA-NET op: DRAM allocation, the layouts + * `foundry/tt/kernels/resident_gdn_protocol.h` settles, and two descriptors a layer. + * The API and the cost model are in pn_tt_gdn.h. + * + * --------------------------------------------------------------------------- + * WHY THIS FILE INCLUDES A KERNEL HEADER + * --------------------------------------------------------------------------- + * The stager and the two dataflow kernels are the two ends of a contract that NOTHING checks + * at run time: a stager that disagrees about where a plane starts produces plausible numbers + * from the first token instead of an error. So every offset below comes from the `ftg_*` + * accessors in `resident_gdn_protocol.h` — the same functions the reader and the writer + * compute their addresses with — rather than from arithmetic respelled here. The header + * guards its one SFPI attribute so a host compiler can read it, which is the sanctioned + * escape `foundry/tt/tests/test_gdn_ops.c` already uses. + * + * --------------------------------------------------------------------------- + * THE FOUR LAYOUTS THIS FILE IS RESPONSIBLE FOR GETTING RIGHT + * --------------------------------------------------------------------------- + * 1. **The conv WEIGHT is a TRANSPOSE.** The oracle stores `w[c*taps + t]`; the card wants + * `taps` planes, plane `t` holding `w[.][t]` laid out like the stream. A reshape is the + * plausible wrong answer and it is silent. `PN_TT_RED=gdn_conv_reshape` is that mutation, + * kept as a knob so the on-card gate can be shown to catch it. + * 2. **The conv stream is a ONE-ROW tilized strip.** `qkv[c]` lands at row 0, column `c%32` + * of tile `c/32`, so the weight plane and the window plane put channel `c` at the same + * place and the compute thread's whole-tile multiply lines them up. Rows 1-31 are the + * tile quantum, and `SiLU(0) = 0` keeps them zero through the convolution — which is what + * lets this write only row 0 per token, 128 B a tile instead of 4096. + * 3. **`q` and `k` are per K-HEAD and NOT repeated.** They are tilized `[k_heads, S]` + * matrices; the reader's `vh % k_heads` is the whole of the GQA repeat. Materialising it + * doubles them. + * 4. **The gate block is not tiles.** `decay[v_heads]` then `beta[v_heads]`, flat fp32, in an + * allocation rounded up to the 64 B read granule the reader rounds its read up to. + * + * @status EXECUTED ON SILICON 2026-08-18. Every layout above is now checked BY READBACK as + * well as off hardware: `PN_TT_GDN_SELFCHECK` echoes the staged qkv row and all four + * conv weight tap planes out of card DRAM and both come back with 0 of 8192 channels + * differing, so the transpose, the one-row strip and the row-0 offsets are confirmed + * rather than inferred. + * @issues Two defects were found here on the first silicon run and BOTH were invisible to + * every off-hardware check, because both produce a descriptor that retires cleanly: + * + * 1. `kGdnActivationXy` is ONE packed coordinate for the activation IN and OUT, and + * this file round-robined DRAM banks, so the writer put `conv_out` and `y` at the + * output's offset inside the INPUT's bank. Fixed — the outputs are pinned to their + * inputs' bank and `ft_tt_op_gdn_parameters()` refuses a mismatch. + * 2. The core default was 75, which `PN_TT_MATMUL_CORES=4` hands to matmul member 1. + * 1560 mutual re-arms over 65 forwards. Fixed — 90, plus a refusal by name in + * `pn_tt_prepare_family()`. + * + * A third was found on 2026-08-18 and it was NOT in this file: the family's circular + * buffers were sized for capacity and for the PRODUCER's push grid but never for the + * CONSUMER's indexed read, so CB_VECTOR (8 pages, 7 consumed a head) and CB_STATE + * (8 pages, 3 indexed a CONV block) both walked off the end. See + * `foundry/tt/kernels/resident_gdn_protocol.h`. + * + * A FOURTH was found on 2026-08-18 BY THE PROBES IN THIS FILE and it was not in this + * file either: the rolled conv window's NEWEST plane was wrong on every CONV + * descriptor in a run EXCEPT the first. It read as "wrong from token 1", and the + * reframing is the whole diagnosis — SSM layer 0 of position 0 is the only CONV never + * preceded by a PHASE DELTA. `PN_TT_GDN_RAMP` stages an INJECTIVE stream + * (`qkv[c] = c`) so the plane names its own source, and the map read `s = t (mod 8)` + * for source tile `s` of output tile `t` at all 256 tiles: a free-running consumer on + * an 8-page buffer. CB_IN was pushed by the READER in PHASE CONV and by the COMPUTE + * thread in PHASE DELTA, and the two push primitives keep incompatible books for one + * stream register — see `foundry/tt/kernels/resident_gdn_protocol.h`. PHASE DELTA has + * its own CB now and the newest plane is byte-exact at every layer of every token. + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gdn.h" +#include "pn_tt_dispatch.h" + +#include +#include +#include +#include + +#include "pn_tt_device_internal.h" + +#include "pn_tt_fp32_strip.h" +#include "pn_tt_gdn_core_scaling.h" +#include "pn_tt_gdn_layout.h" + +#include "tt/ft_tt_op_gdn.h" + +/* See the file header: the L1 pointer attribute is the SFPI fork's, not the contract's. */ +#define FTQ_L1_PTR +#include "tt/kernels/resident_gdn_protocol.h" + +/** Which enabled Tensix it drives. A default, not a constant — and it must be a core neither + * the other six residencies (reduce 72, eltwise 73, topk 74, rope 82, memory 83, attention + * 84) NOR THE MATMUL GROUP holds. + * + * **75 was wrong and the card said so.** `PN_TT_MATMUL_CORES=4` is the shipped knee and its + * members are 71, 75, 76, 77, so the paper check against the other residencies missed the + * one set that MOVES with a configuration knob. Measured 2026-08-18: 1560 mutual re-arms + * over 65 forwards, the GDN and the matmul residency retiring each other every token, both + * logging a healthy "residency up" each time. 90 clears the group out past + * `PN_TT_MATMUL_CORES=16`, and `pn_tt_prepare_family()` now refuses a collision by name. */ +#define PN_TT_CORE_GDN_ENV "PN_TT_CORE_GDN" +#define PN_TT_CORE_GDN_DEFAULT 90u + +/** THE HEAD SPLIT. `PN_TT_GDN_HEAD_CORES=N` arms N-1 EXTRA GDN residencies and gives each + * member `v_heads/N` of the 32 v-heads; PHASE CONV stays whole on member 0. + * + * **DEFAULT 4, MEASURED ON THE SHIPPED BLOB** — 2026-08-19, `PN_TT_PROGRAM_DIR` UNSET, so + * the embedded program blob a serve runs rather than an arm tree in /tmp. PHASE DELTA + * **1835.0 -> 679.3 us, 2.70x**; the GDN block **0.0734 -> 0.0456 s/token**; taken twice + * to the digit, `cmp` IDENTICAL against the 48-token oracle on every arm, `company=0`. + * The gate this default used to carry was that the 7.12x justifying it came from a PROBE + * which replicated a whole descriptor rather than splitting one. The split has now been + * measured AS a split, on the artifact, and it is worth 2.70x — which is the number this + * default is set from. + * + * **4 IS THE KNEE AND 8 IS WORSE**: 1.83 / 2.70 / 2.65x at N = 2 / 4 / 8, N=8 + * reproducibly behind N=4 (692.0 us against 679.3). Phase 1's knee, which is why this is + * a number and not "as many as fit". + * + * **IT COSTS THE SHIPPED FORWARD NOTHING, BECAUSE THE GDN FAMILY IS OPT-IN.** An unset + * `PN_TT_ONCARD` does not arm gdn (`pn_tt_read_oncard_selection()` makes gdn and attention + * opt-in-only), so none of this runs and no extra Tensix is claimed unless a run names + * `gdn`. Measured beside the flip: the shipped wall is 0.5116 s/token either way. + * `PN_TT_GDN_HEAD_CORES=1` is still the way back to a single residency. + * + * 92 is where the members start, the same first core the core-scaling probe used — + * `pn_tt_prepare_family_on_core()` still refuses a matmul group member BY NAME, and the + * group MOVES with `PN_TT_MATMUL_CORES`, so the refusal is the guard and this default is + * only the first place to look. 90 is member 0 and 91 is the head norm's. */ +#define PN_TT_GDN_HEAD_CORES_ENV "PN_TT_GDN_HEAD_CORES" +/* EIGHT on 133..139 since 2026-10-04 (was 4 on 92..94): CONV 309 -> 267 us, DELTA 434 -> 242 us, + * decode 10.69 -> 11.34 tok/s, IDENTICAL — once the reader stopped refusing a CONV after an + * 8-member DELTA. 133..139 is above every other family's default member block. */ +#define PN_TT_GDN_HEAD_CORES_DEFAULT 16u /* CONV over 16; DELTA stays 8 (run_d_ceilings.sh) */ +#define PN_TT_GDN_MEMBER_CORE_FIRST_ENV "PN_TT_GDN_MEMBER_CORE_FIRST" +#define PN_TT_GDN_MEMBER_CORE_DEFAULT 133u + +/** Elements of one tile row, and bytes of one fp32 tile. Both are the family's, not this + * file's: every circular buffer of the GDN program is Float32, and pn_tt_gdn_layout.h holds + * the same two numbers for the arithmetic that was split out of here to be provable. */ +#define PN_TT_GDN_TILE_DIM FT_TT_OP_TILE_DIM +#define PN_TT_GDN_TILE_BYTES FT_TT_OP_GDN_FP32_TILE_BYTES + +/** The v-head ceiling this family admits — `ft_tt_op_gdn_shape_is_supported()` refuses more, + * because the count is a BYTE of the shape word. The gate block is the only operand small + * enough to stage on the stack, and this is what bounds it. */ +#define PN_TT_GDN_MAX_V_HEADS 255u + +/* ================================================================ + * Shape, operands and allocation + * ================================================================ */ + +/** The device's own view of this residency's shape, for the `ftg_*` accessors. */ +static ftg_shape gdn_shape(const pn_tt_device *device, uint32_t phase) +{ + ftg_shape shape; + + shape.phase = phase; + shape.state_tiles = device->gdn_shape.state_size / PN_TT_GDN_TILE_DIM; + shape.v_heads = device->gdn_shape.v_heads; + shape.k_heads = device->gdn_shape.k_heads; + shape.conv_tile_cols = device->gdn_shape.conv_dim / PN_TT_GDN_TILE_DIM; + shape.conv_taps = device->gdn_shape.conv_taps; + shape.tile_bytes = PN_TT_GDN_TILE_BYTES; + shape.tile_count = 0u; + return shape; +} + +/** The request describing this residency, with the operands a caller then fills in. Carries + * the CONV taps whatever the phase, because ONE CB placement serves both phases and + * `ft_tt_op_gdn_place_cbs()` sizes the weight buffers from them. */ +static ft_tt_op_gdn_request gdn_request(const pn_tt_device *device, uint32_t phase) +{ + ft_tt_op_gdn_request request; + + memset(&request, 0, sizeof(request)); + request.phase = phase; + request.state_size = device->gdn_shape.state_size; + request.v_heads = device->gdn_shape.v_heads; + request.k_heads = device->gdn_shape.k_heads; + request.conv_dim = device->gdn_shape.conv_dim; + request.conv_taps = device->gdn_shape.conv_taps; + return request; +} + +/** An fp32 operand descriptor for one of this family's DRAM buffers. */ +static ft_tt_op_operand gdn_operand(const pn_tt_device *device, const pn_tt_gdn_buffer *buffer) +{ + ft_tt_op_operand operand; + + operand.x = (uint16_t)device->dram[buffer->bank].x; + operand.y = (uint16_t)device->dram[buffer->bank].y; + operand.address = buffer->address; + operand.tile_bytes = PN_TT_GDN_TILE_BYTES; + return operand; +} + +/** "Any bank will do" — the round-robin every buffer takes unless it is PINNED to another + * buffer's endpoint. */ +#define PN_TT_GDN_ANY_BANK UINT32_MAX + +/** + * Carve one DRAM buffer in `bank`, and when asked stage it ZEROED. + * + * The zeroing is not tidiness for three of the callers. The conv window and the recurrent + * state ARE the recurrence's initial condition and the oracle's are calloc'd; the qkv stream + * is written row 0 at a time forever after, so rows 1-31 have to start zero and stay zero. + * + * **THE BANK IS AN ARGUMENT BECAUSE TWO PAIRS OF THESE BUFFERS MUST SHARE ONE.** The + * descriptor has a single packed x/y word for the activation IN and the activation OUT + * (`kGdnActivationXy`), so the two are one NoC endpoint by contract, and a round-robin that + * split them made the writer put every answer at the output's offset inside the input's bank. + * `ft_tt_op_gdn_parameters()` now refuses that descriptor; this is the side that stops + * building it. + */ +static int gdn_alloc_in(pn_tt_device *device, uint32_t bank, size_t bytes, bool zero, + pn_tt_gdn_buffer *out) +{ + ft_tt_buffer buffer; + + if (bytes == 0u) + return -1; + if (bank == PN_TT_GDN_ANY_BANK) + bank = device->strip_bank_cursor++ % device->dram_bank_count; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes, + &buffer) != FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating a %zu-byte GDN buffer\n", bytes); + return -1; + } + out->bank = buffer.logical_bank; + out->address = (uint32_t)buffer.address; + if (zero && pn_tt_zero_dram(device, out->bank, out->address, bytes) != 0) + return -1; + return 0; +} + +/** Grow the host tile-staging buffer to `bytes`. Kept on the device so a per-layer call is + * not a malloc on a two-core host. */ +static int gdn_stage_grow(pn_tt_device *device, size_t bytes) +{ + if (bytes > device->gdn_stage_bytes) { + unsigned char *grown = realloc(device->gdn_stage, bytes); + + if (!grown) + return -1; + device->gdn_stage = grown; + device->gdn_stage_bytes = bytes; + } + return 0; +} + +/* ================================================================ + * Logical row 0 of a strip of fp32 tiles + * ================================================================ */ + +/** Move row 0 of `tiles` consecutive fp32 tiles, in either direction. The arithmetic and the + * transfer are `pn_tt_fp32_strip.c`'s — the same primitive the per-head reduce residency's + * Float32 strips use, and there is no reason for this family to own a second copy. */ +static int gdn_row0_move(pn_tt_device *device, const pn_tt_gdn_buffer *buffer, uint32_t tiles, + float *values, bool reading) +{ + return pn_tt_fp32_row0_move(device, buffer->bank, buffer->address, tiles, values, reading); +} + +/* ================================================================ + * Arming + * ================================================================ */ + +/** Every per-layer buffer, and the shared per-token staging set. */ +static int gdn_alloc_regions(pn_tt_device *device, const ft_tt_op_gdn_request *request) +{ + const size_t state = ft_tt_op_gdn_state_bytes(request); + const size_t window = ft_tt_op_gdn_conv_state_bytes(request); + const size_t weight = ft_tt_op_gdn_conv_weight_bytes(request); + const size_t stream = ft_tt_op_gdn_conv_stream_bytes(request); + uint32_t layer; + + device->gdn_region = calloc(device->gdn_shape.layer_count, sizeof(*device->gdn_region)); + if (!device->gdn_region) + return -1; + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) { + pn_tt_gdn_region *region = &device->gdn_region[layer]; + + /* The weight is a CONSTANT the caller writes whole, plane by plane, so it needs no + * staging; the two pieces of carried state are the recurrence's initial condition. */ + if (gdn_alloc_in(device, PN_TT_GDN_ANY_BANK, weight, false, ®ion->weight) != 0 || + gdn_alloc_in(device, PN_TT_GDN_ANY_BANK, window, true, ®ion->window) != 0 || + gdn_alloc_in(device, PN_TT_GDN_ANY_BANK, state, true, ®ion->state) != 0) + return -1; + } + /* ONE set of staging buffers for the whole model: a token runs one layer at a time and + * nothing in them carries across a descriptor. Every one of them is staged ZEROED, because + * every one is written ROW 0 AT A TIME forever after and the strip invariant is that rows + * 1-31 start zero and stay zero. + * + * **THERE ARE THREE OF THEM, NOT FOUR, AND THAT IS THE STRIP BRIDGE.** `stream_out` is + * CONV's output AND DELTA's input: both are `conv_dim/32` row-0 tiles and conv_out's q, k + * and v slices land exactly where `ftg_delta_stream_base()` reads them, so the delta rule + * takes the convolution's answer in place and the separate `delta_in` staging buffer is + * gone. What used to sit between them — de-tilize on the host, L2-normalise, re-tilize, + * upload — is 49,408 B a layer of PCIe traffic that no longer exists. + * + * **EVERY OUTPUT IS PINNED TO ITS INPUT'S BANK.** `conv_out` shares CONV's + * `kGdnActivationXy` with the qkv stream and `y` shares DELTA's with `stream_out` — ONE + * packed coordinate word for two buffers — so a round-robin that split a pair sent the + * writer to the input's endpoint with the output's offset. It completed, wrote over + * whatever was at that offset in the input's bank, and left the output buffer exactly as + * allocated: `conv_out` unzeroed garbage and `y` zeros. All four now share `stream_in`'s + * bank, which is what makes both pairs satisfy the contract at once. */ + if (gdn_alloc_in(device, PN_TT_GDN_ANY_BANK, stream, true, &device->gdn_stream_in) != 0 || + gdn_alloc_in(device, device->gdn_stream_in.bank, stream, true, + &device->gdn_stream_out) != 0 || + gdn_alloc_in(device, PN_TT_GDN_ANY_BANK, ft_tt_op_gdn_gate_bytes(request), true, + &device->gdn_gate) != 0 || + gdn_alloc_in(device, device->gdn_stream_in.bank, + ft_tt_op_gdn_delta_output_bytes(request), true, &device->gdn_y) != 0) + return -1; + + /* THE MAP, so an overlap is readable instead of inferred. A buffer this family writes + * that lands inside another one is invisible from either end — the writer retires clean + * and the reader gets a plausible tensor — and the only place the two addresses are ever + * both in view is here. Under SELFCHECK only: 77 lines a run is a probe, not a log. */ + if (getenv("PN_TT_GDN_SELFCHECK")) { + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) { + const pn_tt_gdn_region *region = &device->gdn_region[layer]; + + fprintf(stderr, "pn_tt: gdn MAP layer %2u weight bank %u @ %08x +%zu window " + "bank %u @ %08x +%zu state bank %u @ %08x +%zu\n", + layer, region->weight.bank, region->weight.address, weight, + region->window.bank, region->window.address, window, region->state.bank, + region->state.address, state); + } + fprintf(stderr, "pn_tt: gdn MAP stream_in bank %u @ %08x +%zu stream_out/delta_in " + "bank %u @ %08x +%zu gate bank %u @ %08x y bank %u @ %08x\n", + device->gdn_stream_in.bank, device->gdn_stream_in.address, stream, + device->gdn_stream_out.bank, device->gdn_stream_out.address, stream, + device->gdn_gate.bank, device->gdn_gate.address, device->gdn_y.bank, + device->gdn_y.address); + } + fprintf(stderr, "pn_tt: gdn — %u layers x (%.2f MiB state + %.2f MiB window + %.2f MiB " + "weight) = %.2f MiB of card DRAM\n", + device->gdn_shape.layer_count, (double)state / (1024.0 * 1024.0), + (double)window / (1024.0 * 1024.0), (double)weight / (1024.0 * 1024.0), + (double)((state + window + weight) * device->gdn_shape.layer_count) / + (1024.0 * 1024.0)); + return 0; +} + +/** A positive env integer, or `fallback`. */ +static uint32_t gdn_env_u32(const char *name, uint32_t fallback) +{ + const char *value = getenv(name); + long parsed; + + if (!value || !*value) + return fallback; + parsed = strtol(value, NULL, 10); + return parsed > 0 ? (uint32_t)parsed : fallback; +} + +/** + * How many GDN residencies this run will split PHASE DELTA across. + * + * Clamped to the array, to `PN_TT_GDN_MAX_MEMBERS`, and to a DIVISOR of `v_heads` — an + * uneven split would give one member a different trip count, and a member is a whole + * residency whose descriptor cost is `59.8 us + heads x 103.4`: the set is only as fast as + * its longest member, so an uneven split pays for the biggest slice and wastes the rest. + */ +static uint32_t gdn_head_member_count(const pn_tt_device *device) +{ + uint32_t members = gdn_env_u32(PN_TT_GDN_HEAD_CORES_ENV, PN_TT_GDN_HEAD_CORES_DEFAULT); + + if (members > PN_TT_GDN_MAX_MEMBERS) + members = PN_TT_GDN_MAX_MEMBERS; + if (members > device->gdn_shape.v_heads) + members = device->gdn_shape.v_heads; + while (members > 1u && (device->gdn_shape.v_heads % members) != 0u) + members--; + return members; +} + +/** + * Arm the EXTRA GDN residencies of a head split, if this run asked for one. + * + * They take the SAME circular-buffer placement member 0 has, and that is a property of the + * family rather than a convenience: `resolve_cb_pages()` reads `state_tiles` and the CONV + * depth and nothing else, so a member owning four heads places exactly what a member owning + * thirty-two does. Sizing them any other way would be measuring a different residency from + * the one the model runs. + * + * A member that fails to arm is not fatal — the split shrinks to what came up and the run + * continues, because every member's descriptors are complete on their own. It IS reported, + * because a silent shrink would show up only as a number that did not move. + */ +static void gdn_arm_head_members(pn_tt_device *device, const ft_tt_op_gdn_request *shape) +{ + const uint32_t members = gdn_head_member_count(device); + const uint32_t first = gdn_env_u32(PN_TT_GDN_MEMBER_CORE_FIRST_ENV, + PN_TT_GDN_MEMBER_CORE_DEFAULT); + uint32_t index; + + device->gdn_members = 1u; + if (members < 2u) + return; + for (index = 0; index + 1u < members; index++) { + pn_tt_residency *member = &device->gdn_member[index]; + ft_tt_launch_cb_entry placed[FT_TT_OP_GDN_CB_COUNT]; + ft_tt_op_gdn_request request = *shape; + + request.phase = FT_TT_OP_GDN_PHASE_CONV; + request.head_first = 0u; + request.head_count = 0u; + request.activation_in.tile_bytes = PN_TT_GDN_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDN_TILE_BYTES; + /* L5: members 1..7 on `first` upward (133..139); members 8..15 on a second block + * (`PN_TT_GDN_MEMBER_CORE_SECOND`, default 50 — cores 0..70 probed healthy + * 2026-10-05 and used by nothing else). */ + const uint32_t core = index < 7u ? first + index + : gdn_env_u32("PN_TT_GDN_MEMBER_CORE_SECOND", 50u) + + (index - 7u); + + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_GDN_PROGRAM, "gdn-member", + PN_TT_GDN_MEMBER_CORE_FIRST_ENV, core, + PN_TT_ONCARD_TRIAD_MASK)) + break; + if (ft_tt_op_gdn_place_cbs(&request, placed, member->cb_buffer, + sizeof(member->cb_buffer), &member->cb_mask) != FT_TT_OK || + !pn_tt_start_family(member)) { + pn_tt_residency_release(member); + break; + } + device->gdn_members++; + } + if (device->gdn_members != members) + fprintf(stderr, "pn_tt: gdn HEAD SPLIT — asked for %u members, %u armed; the split " + "runs at %u\n", members, device->gdn_members, device->gdn_members); + fprintf(stderr, "pn_tt: gdn HEAD SPLIT — PHASE DELTA over %u members from core %u, " + "%u v-heads each; PHASE CONV stays whole on core %u\n", + device->gdn_members, device->gdn.core_index, + device->gdn_shape.v_heads / device->gdn_members, device->gdn.core_index); +} + +int pn_tt_device_arm_gdn(pn_tt_device *device, const pn_tt_gdn_shape *shape) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_GDN_CB_COUNT]; + ft_tt_op_gdn_request request; + + if (!device || !shape || shape->layer_count == 0u) + return -1; + device->gdn_shape = *shape; + request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + /* The placement needs operands only to pass the encoder's "is this set" test; the ones + * the descriptors carry are per call. Any fp32 operand answers it. */ + request.activation_in.tile_bytes = PN_TT_GDN_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDN_TILE_BYTES; + if (!ft_tt_op_gdn_shape_is_supported(&request)) { + fprintf(stderr, "pn_tt: the gdn family does not serve S=%u, %u v-heads over %u " + "k-heads, conv_dim %u, %u taps — the recurrence stays on the host\n", + shape->state_size, shape->v_heads, shape->k_heads, shape->conv_dim, + shape->conv_taps); + return 0; + } + + if (!pn_tt_prepare_family(device, &device->gdn, PN_TT_GDN_PROGRAM, "gdn", + PN_TT_CORE_GDN_ENV, PN_TT_CORE_GDN_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return 0; + if (ft_tt_op_gdn_place_cbs(&request, placed, device->gdn.cb_buffer, + sizeof(device->gdn.cb_buffer), &device->gdn.cb_mask) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdn circular buffers do not fit at S=%u, %u taps\n", + shape->state_size, shape->conv_taps); + pn_tt_residency_release(&device->gdn); + return 0; + } + if (gdn_alloc_regions(device, &request) != 0 || + gdn_stage_grow(device, ft_tt_op_gdn_conv_stream_bytes(&request)) != 0) { + fprintf(stderr, "pn_tt: the gdn family could not be staged in card DRAM\n"); + pn_tt_residency_release(&device->gdn); + /* A HALF-ALLOCATED region table is worse than none: `pn_tt_gdn_is_armed()` would + * still refuse on `running`, but a later reader of the table would find addresses for + * some layers and zeros for the rest. The DRAM itself is not reclaimed — the + * allocator frees LIFO by design — and a caller that reaches here is not continuing. */ + free(device->gdn_region); + device->gdn_region = NULL; + return 0; + } + if (!pn_tt_start_family(&device->gdn)) + return 0; + device->oncard[PN_TT_ONCARD_GDN] = true; + gdn_arm_head_members(device, &request); + return 0; +} + +bool pn_tt_gdn_is_armed(const pn_tt_device *device) +{ + return device && device->gdn.running && device->gdn_region != NULL; +} + +/** + * Zero one carried-state buffer: ON CARD by the puller's FILL (`PN_TT_ONCARD_FILL=0` off; the + * ~120 MB of a sequence start was 0.43 s over the link), else over the link. RED `fill_short` + * leaves the last 4 KiB of each buffer unzeroed — the state of the previous sequence leaks + * into the next one and must DIFFER. + */ +static int gdn_zero_buffer(pn_tt_device *device, uint32_t bank, uint32_t address, size_t bytes) +{ + const size_t want = pn_tt_red("fill_short") && bytes > 4096u ? bytes - 4096u : bytes; + + if (pn_tt_env_u32("PN_TT_ONCARD_FILL", 1u) != 0u && + pn_tt_fill_dram_oncard(device, bank, address, want, 0u) == 0) + return 0; + return pn_tt_zero_dram(device, bank, address, want); +} + +/** `PN_TT_FILL_VERIFY=1`: read every zeroed buffer back and count the 32-bit words that are + * not zero — the gate of the on-card fill is bytes, not an argmax. */ +static void gdn_state_reset_verify(pn_tt_device *device, const ft_tt_op_gdn_request *request) +{ + const size_t sizes[2] = { ft_tt_op_gdn_conv_state_bytes(request), + ft_tt_op_gdn_state_bytes(request) }; + uint64_t nonzero = 0u, words = 0u; + uint32_t *buffer = malloc(sizes[0] > sizes[1] ? sizes[0] : sizes[1]); + uint32_t layer, which; + size_t i; + + if (!buffer) + return; + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) + for (which = 0; which < 2u; which++) { + const pn_tt_gdn_buffer *b = which ? &device->gdn_region[layer].state + : &device->gdn_region[layer].window; + + if (pn_tt_read_dram(device, b->bank, b->address, buffer, sizes[which]) != 0) { + fprintf(stderr, "pn_tt: gdn — FILL VERIFY could not read layer %u\n", layer); + continue; + } + for (i = 0; i < sizes[which] / 4u; i++) + nonzero += buffer[i] != 0u; + words += sizes[which] / 4u; + } + free(buffer); + fprintf(stderr, "pn_tt: gdn — FILL VERIFY %llu of %llu state words NOT zero%s\n", + (unsigned long long)nonzero, (unsigned long long)words, + pn_tt_red("fill_short") ? " [RED fill_short]" : ""); +} + +int pn_tt_gdn_state_reset(pn_tt_device *device) +{ + ft_tt_op_gdn_request request; + uint32_t layer; + + if (!pn_tt_gdn_is_armed(device)) + return -1; + /* STEP 3 (A): a new sequence — the on-card dispatch model re-learns its positions. */ + pn_tt_dispatch_sequence_reset(); + request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) { + pn_tt_gdn_region *region = &device->gdn_region[layer]; + + if (gdn_zero_buffer(device, region->window.bank, region->window.address, + ft_tt_op_gdn_conv_state_bytes(&request)) != 0 || + gdn_zero_buffer(device, region->state.bank, region->state.address, + ft_tt_op_gdn_state_bytes(&request)) != 0) + return -1; + } + if (pn_tt_env_u32("PN_TT_FILL_VERIFY", 0u) != 0u) + gdn_state_reset_verify(device, &request); + /* Announced because "position 0 is the sequence boundary" is an ASSUMPTION about the + * caller, not something this file can check: a caller resuming from a prefix cache never + * reaches here and would inherit the previous sequence's state silently. One line a + * sequence makes the assumption observable in a log instead of only in a comment. */ + fprintf(stderr, "pn_tt: gdn — carried state zeroed for %u layers at position 0\n", + device->gdn_shape.layer_count); + return 0; +} + +/* ================================================================ + * STEP 4 S2: snapshots of the carried state (the verify's rollback) + * ================================================================ */ + +/* Every row of a verify writes its new state to a SNAPSHOT SLOT instead of over the state it + * read (FT_TT_OP_GDN_FLAG_STATE_OUT), and the slot and the live region then trade places: no + * copy anywhere. After rows 0..k-1 the live region holds the state after row k-1 and slot r + * the state BEFORE row r, so a rollback to "r rows kept" is one more trade. */ + +int pn_tt_gdn_snapshot_reserve(pn_tt_device *device, uint32_t slots) +{ + const uint32_t layers = device ? device->gdn_shape.layer_count : 0u; + pn_tt_gdn_region *more; + uint32_t s, l; + + if (!device || !pn_tt_gdn_is_armed(device) || slots > 16u) + return -1; + if (slots <= device->gdn_snapshot_slots) + return 0; + more = realloc(device->gdn_snapshot, (size_t)slots * layers * sizeof(*more)); + if (!more) + return -1; + device->gdn_snapshot = more; + for (s = device->gdn_snapshot_slots; s < slots; s++) + for (l = 0; l < layers; l++) + if (pn_tt_gdn_alloc_twin(device, l, &more[s * layers + l]) != 0) + return -1; + device->gdn_snapshot_slots = slots; + return 0; +} + +void pn_tt_gdn_carried_bytes(const pn_tt_device *device, size_t *window, size_t *state) +{ + const ft_tt_op_gdn_request request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + + *window = ft_tt_op_gdn_conv_state_bytes(&request); + *state = ft_tt_op_gdn_state_bytes(&request); +} + +int pn_tt_gdn_alloc_twin(pn_tt_device *device, uint32_t layer, pn_tt_gdn_region *out) +{ + const ft_tt_op_gdn_request request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + const size_t window = ft_tt_op_gdn_conv_state_bytes(&request); + const size_t state = ft_tt_op_gdn_state_bytes(&request); + + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count || !out) + return -1; + memset(out, 0, sizeof(*out)); + /* THE LIVE REGION'S BANK: one coordinate word carries both (ft_tt_op_gdn.c). */ + if (gdn_alloc_in(device, device->gdn_region[layer].window.bank, + (window + 63u) & ~(size_t)63u, false, &out->window) != 0 || + gdn_alloc_in(device, device->gdn_region[layer].state.bank, + (state + 63u) & ~(size_t)63u, false, &out->state) != 0) + return -1; + return 0; +} + +int pn_tt_gdn_state_out(pn_tt_device *device, int slot) +{ + if (!device || slot >= (int)device->gdn_snapshot_slots) + return -1; + device->gdn_state_out_plus1 = slot < 0 ? 0u : (uint32_t)slot + 1u; + return 0; +} + +int pn_tt_gdn_layer_swap_part(pn_tt_device *device, uint32_t layer, uint32_t slot, bool window, + bool state) +{ + pn_tt_gdn_region *live, *snap; + pn_tt_gdn_buffer t; + + if (!device || layer >= device->gdn_shape.layer_count || slot >= device->gdn_snapshot_slots) + return -1; + live = &device->gdn_region[layer]; + snap = &device->gdn_snapshot[slot * device->gdn_shape.layer_count + layer]; + if (window) { + t = live->window; + live->window = snap->window; + snap->window = t; + } + if (state) { + t = live->state; + live->state = snap->state; + snap->state = t; + } + return 0; +} + +int pn_tt_gdn_layer_swap(pn_tt_device *device, uint32_t layer, uint32_t slot) +{ + return pn_tt_gdn_layer_swap_part(device, layer, slot, true, true); +} + +int pn_tt_gdn_defer(pn_tt_device *device, bool defer) +{ + if (!device) + return -1; + device->gdn_defer = defer; + return 0; +} + +int pn_tt_gdn_await_deferred(pn_tt_device *device) +{ + uint32_t index; + int failed = 0; + + if (!device) + return -1; + for (index = 0; index < device->gdn_pending_members; index++) { + pn_tt_residency *r = index == 0u ? &device->gdn : &device->gdn_member[index - 1u]; + + if (pn_tt_residency_await(r, PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + device->gdn_pending_members = 0u; + device->gdn_pending_depth = 0u; + return failed ? -1 : 0; +} + +int pn_tt_gdn_set_row(pn_tt_device *device, uint32_t row) +{ + if (!device || row > 31u) + return -1; + /* RED `gdn_row_zero`: every row reads and writes row 0 — must DIFFER. */ + device->gdn_row = pn_tt_red("gdn_row_zero") ? 0u : row; + return 0; +} + +/** The STATE_OUT slot's buffer for this layer, or NULL (the in-place walk). */ +static const pn_tt_gdn_buffer *gdn_state_out_buffer(const pn_tt_device *device, uint32_t layer, + bool window) +{ + const pn_tt_gdn_region *snap; + + if (device->gdn_state_out_plus1 == 0u) + return NULL; + snap = &device->gdn_snapshot[(device->gdn_state_out_plus1 - 1u) * + device->gdn_shape.layer_count + layer]; + return window ? &snap->window : &snap->state; +} + +/* ================================================================ + * The conv weight — the TRANSPOSE + * ================================================================ */ + +int pn_tt_gdn_conv_weight_upload(pn_tt_device *device, uint32_t layer, const float *weight) +{ + const ftg_shape shape = gdn_shape(device, FT_TT_OP_GDN_PHASE_CONV); + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const uint32_t taps = device->gdn_shape.conv_taps; + float *plane; + uint32_t tap; + int rc = 0; + + if (!pn_tt_gdn_is_armed(device) || !weight || layer >= device->gdn_shape.layer_count) + return -1; + plane = malloc((size_t)conv_dim * sizeof(*plane)); + if (!plane) + return -1; + if (layer == 0u) + device->gdn_weight_oracle = weight; + + for (tap = 0; tap < taps && rc == 0; tap++) { + const uint32_t first = ftg_conv_plane_tile(&shape, tap, 0u); + /* THE TRANSPOSE, and the whole of it: plane `t` is the STRIDE-`taps` gather of the + * oracle's row-major `[conv_dim, taps]` starting at `t`. + * RED PROOF: `(tap * conv_dim, 1)` is the RESHAPE — a contiguous slice, the same + * bytes, the same length, a different answer, and nothing on the card can see the + * difference. It is the single most likely thing for a stager to get silently wrong, + * which is why it is a named mutation the on-card gate can be shown to catch. */ + const uint32_t gather_first = pn_tt_red("gdn_conv_reshape") ? tap * conv_dim : tap; + const uint32_t gather_stride = pn_tt_red("gdn_conv_reshape") ? 1u : taps; + + if (pn_tt_gdn_stage_plane(weight, gather_first, gather_stride, conv_dim, plane, + device->gdn_stage) != 0) + rc = -1; + else + rc = pn_tt_write_dram(device, device->gdn_region[layer].weight.bank, + device->gdn_region[layer].weight.address + + first * PN_TT_GDN_TILE_BYTES, + device->gdn_stage, + (size_t)shape.conv_tile_cols * PN_TT_GDN_TILE_BYTES); + } + free(plane); + return rc; +} + +/* ================================================================ + * Submission + * ================================================================ */ + +/** + * Retire a residency that refused a descriptor, and arm a FRESH one on the same core. + * + * The DRAM is untouched: the regions are allocated once and a retired core owns none of them, + * so the carried state a half-finished token left behind is still there. What is re-placed is + * the circular-buffer geometry, which is a property of the shape and not of the descriptor. + * + * A residency is BOUNDED on purpose — `idle_spin_limit` retires a kernel whose host has + * stopped feeding it — so this fires for a `serve` that idles between requests, not only for + * a malformed descriptor. Same mechanism as `pn_tt_oncard_rearm()`. + */ +static bool pn_tt_gdn_rearm(pn_tt_device *device) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_GDN_CB_COUNT]; + ft_tt_op_gdn_request request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + + fprintf(stderr, "pn_tt: the gdn residency refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", device->gdn.core_index); + pn_tt_residency_stop(&device->gdn); + pn_tt_residency_release(&device->gdn); + device->oncard_restarts++; + /* NOTHING BOOTS FIRMWARE ONTO A CORE THAT DID NOT RETIRE. Measured on silicon + * 2026-08-21: a refused descriptor leaves the compute thread spinning out its bound, the + * stop reported `processors still running: 0x4`, and the boot that followed WEDGED core 90 + * at `instrn_buf_status = 0x66` — a state only `ft_tt_core recover --rung 3` and sudo can + * clear. A failed descriptor is a bad token; a wedged Tensix takes the process AND the next + * one, because the next boot lands on the same core. */ + if (!pn_tt_residency_retired(&device->gdn)) { + fprintf(stderr, "pn_tt: the gdn residency did not retire — REFUSING to arm a fresh " + "one on core %u. Booting firmware onto a core whose compute thread is " + "still running is the wedge, and it needs sudo to clear.\n", + device->gdn.core_index); + return false; + } + + request.activation_in.tile_bytes = PN_TT_GDN_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDN_TILE_BYTES; + if (!pn_tt_prepare_family(device, &device->gdn, PN_TT_GDN_PROGRAM, "gdn", + PN_TT_CORE_GDN_ENV, PN_TT_CORE_GDN_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_gdn_place_cbs(&request, placed, device->gdn.cb_buffer, + sizeof(device->gdn.cb_buffer), &device->gdn.cb_mask) != + FT_TT_OK) { + pn_tt_residency_release(&device->gdn); + return false; + } + return pn_tt_start_family(&device->gdn); +} + +/** + * One descriptor, with the same re-arm-once recovery the other on-card families use. + * + * A GDN re-arm is safe in a way the sampler's is not: every address in the parameter block is + * a DRAM one, and the carried state lives in DRAM and is untouched by retiring the core. So + * the SAME words are valid against a fresh residency. + */ +/** + * A positive integer measurement knob, or 0 when it is unset or not a number. + * + * Read at every descriptor rather than cached, for the same reason `pn_tt_red()` is: what + * these change is the WHOLE run, and a knob read once at arm time cannot be turned on for a + * probe that runs after the residency is up. + */ +static uint32_t gdn_probe_knob(const char *name) +{ + const char *value = getenv(name); + long parsed; + + if (!value || !*value) + return 0u; + parsed = strtol(value, NULL, 10); + return parsed > 0 ? (uint32_t)parsed : 0u; +} + +/** + * Retire ONE head-split member and arm a fresh one ON THE CORE IT ALREADY HELD. + * + * The core is captured BEFORE the release, because `pn_tt_residency_release()` zeroes the + * handle — re-reading `core_index` afterwards would arm member 3 on core 0. Member 0 is the + * `gdn` residency itself and goes through `pn_tt_gdn_rearm()`, which is the same operation + * spelled against the environment default. + */ +static bool gdn_member_rearm(pn_tt_device *device, uint32_t member_index) +{ + pn_tt_residency *member; + ft_tt_launch_cb_entry placed[FT_TT_OP_GDN_CB_COUNT]; + ft_tt_op_gdn_request request; + uint32_t core; + + if (member_index == 0u) + return pn_tt_gdn_rearm(device); + member = &device->gdn_member[member_index - 1u]; + core = member->core_index; + fprintf(stderr, "pn_tt: gdn member %u refused a descriptor — retiring it by ASKING and " + "arming a fresh one on core %u\n", member_index, core); + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + device->oncard_restarts++; + /* The same refusal as `pn_tt_gdn_rearm()`'s, for the same reason. */ + if (!pn_tt_residency_retired(member)) { + fprintf(stderr, "pn_tt: gdn member %u did not retire — REFUSING to arm a fresh one on " + "core %u\n", member_index, core); + return false; + } + + request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + request.activation_in.tile_bytes = PN_TT_GDN_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDN_TILE_BYTES; + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_GDN_PROGRAM, "gdn-member", + PN_TT_GDN_MEMBER_CORE_FIRST_ENV, core, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_gdn_place_cbs(&request, placed, member->cb_buffer, sizeof(member->cb_buffer), + &member->cb_mask) != FT_TT_OK) { + pn_tt_residency_release(member); + return false; + } + return pn_tt_start_family(member); +} + +/** + * PHASE DELTA over a head split: one descriptor per member, ALL ENQUEUED BEFORE ANY IS + * WAITED ON. + * + * The two loops being separate is the measurement and not a style. Enqueue-wait per member + * would run `members` SERIAL descriptors and print the flat line "it does not parallelise", + * produced by this function rather than by the card — the exact failure the core-scaling + * probe's header calls out, and the reason its own loops are split the same way. + * + * NO TWO MEMBERS TOUCH THE SAME DRAM BYTE. The carried state is head-major, so a head slice + * is a contiguous, disjoint region. `y` is the one operand they share, and they share it + * SAFELY: it is a tilized [v_heads, S] matrix with one ROW per head, and the writer scatters + * each head's answer at row granularity into `head % 32`, so two members write disjoint byte + * ranges of the same tiles. The gate block is READ whole by every member, which is a read. + * + * Time is charged to the SET, once, so `us each` stays the wall a layer's DELTA costs rather + * than the sum of what its members cost in parallel. + * + * @return 0, or -1 with the run's DELTA left incomplete — there is no partial retry here, + * because a member that failed has left its slice of the recurrence un-advanced and the + * next token's state would be a mixture of two positions. + */ +void pn_tt_gdn_report_phases(const pn_tt_device *device) +{ + static const char *const name[3] = { "?", "CONV", "DELTA" }; + uint32_t phase; + + if (!device) + return; + for (phase = 1u; phase < 3u; phase++) { + const uint64_t ops = device->gdn_phase_ops[phase]; + const uint64_t recovered = device->gdn_phase_recovered_ops[phase]; + + if (ops == 0u && recovered == 0u) + continue; + fprintf(stderr, "pn_tt: gdn — %-5s %.4f s over %llu clean descriptors (%.1f us " + "each), %llu replayed over %llu re-arms (%.4f s)\n", + name[phase], device->gdn_phase_seconds[phase], (unsigned long long)ops, + ops ? 1e6 * device->gdn_phase_seconds[phase] / (double)ops : 0.0, + (unsigned long long)recovered, + (unsigned long long)device->gdn_phase_rearms[phase], + device->gdn_phase_recovered_seconds[phase]); + } +} + +/** + * Charge one submit bracket to its phase, CLEAN AND RECOVERED KEPT APART. + * + * The stopwatch every caller here holds spans the refusal, the re-arm — a residency teardown + * and relaunch — and the resubmit. Summing that into `seconds / ops` prices the teardown as + * the op's arithmetic, and it has already produced a wrong headline number: PHASE CONV read + * **4,502 us a descriptor against 738.2** when a circular-buffer write pointer made every CONV + * following a DELTA refuse, and the unsourced "CONV is DATA-DEPENDENT, 4,571 us on a + * degenerate stream" has the same shape. So a bracket that re-armed anything is charged + * elsewhere and the reader prints the count beside every figure it quotes. + * + * @param phase `FT_TT_OP_GDN_PHASE_*`; anything else is dropped rather than aliased + * @param seconds the whole bracket + * @param descriptors descriptors it covered (`PN_TT_GDN_BATCH` submits several) + * @param rearms re-arms inside it; 0 makes the bracket CLEAN + */ +static void gdn_account_phase(pn_tt_device *device, uint32_t phase, double seconds, + uint64_t descriptors, uint64_t rearms) +{ + if (phase >= 3u) + return; + if (rearms == 0u) { + device->gdn_phase_seconds[phase] += seconds; + device->gdn_phase_ops[phase] += descriptors; + return; + } + device->gdn_phase_recovered_seconds[phase] += seconds; + device->gdn_phase_recovered_ops[phase] += descriptors; + device->gdn_phase_rearms[phase] += rearms; +} + +/** `PN_TT_GDN_CONV_SPLIT=1`: split PHASE CONV across the gdn members by tile column. */ +static bool gdn_conv_split_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_GDN_CONV_SPLIT"); + + /* ON since 2026-10-04: CONV 941 -> 309 us at 4 members, decode 9.18 -> 10.7 tok/s, + * IDENTICAL; `gdn_conv_col_dup` red DIFFERS. `=0` is the whole-core CONV. */ + cached = (value && *value) ? (strcmp(value, "0") != 0) : 1; + if (cached) + fprintf(stderr, "pn_tt: gdn PHASE CONV splits by tile column over the members\n"); + } + return cached != 0; +} + +/** L5: PHASE DELTA may run over FEWER members than CONV — measured 2026-10-05 at 16: CONV + * 153 -> 120 us but DELTA 147 -> 162 us. `PN_TT_GDN_DELTA_CORES` (default: every member) + * caps DELTA's set at the first N members; it must divide v_heads or DELTA uses them all. */ +static uint32_t gdn_delta_members(const pn_tt_device *device) +{ + const uint32_t asked = pn_tt_env_u32("PN_TT_GDN_DELTA_CORES", + device->gdn_members > 8u ? 8u : device->gdn_members); + + if (asked >= 1u && asked < device->gdn_members && device->gdn_shape.v_heads % asked == 0u) + return asked; + return device->gdn_members; +} + +/** CHARACTERIZATION (2026-10-05): `PN_TT_GDN_CONV_CORES` caps CONV's set like DELTA's, so + * armed-but-idle members can be told apart from members that work. */ +static uint32_t gdn_conv_members(const pn_tt_device *device) +{ + const uint32_t asked = pn_tt_env_u32("PN_TT_GDN_CONV_CORES", device->gdn_members); + const uint32_t cols = device->gdn_shape.conv_dim / FT_TT_OP_TILE_DIM; + + if (asked >= 1u && asked < device->gdn_members && cols % asked == 0u && + (cols / asked) % ft_tt_op_gdn_conv_depth() == 0u) + return asked; + return device->gdn_members; +} + +/** MULTI-ROW DELTA: the per-row state addresses into the slot after the descriptor's own, + * on this residency, before its enqueue (and again before a replay after a re-arm). */ +static int gdn_write_row_extension(pn_tt_residency *residency, + const ft_tt_op_gdn_request *request) +{ + uint32_t extension[FT_TT_OP_GDN_WORDS]; + + if (request->row_count < 2u) + return 0; + if (ft_tt_op_gdn_row_extension(request, extension) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdn multi-row extension is one the family refuses\n"); + return -1; + } + return pn_tt_residency_write_param_extension(residency, 1u, extension); +} + +static int gdn_submit_delta_split(pn_tt_device *device, const ft_tt_op_gdn_request *request) +{ + const uint32_t members = request->phase == FT_TT_OP_GDN_PHASE_CONV + ? gdn_conv_members(device) + : gdn_delta_members(device); + /* THIS DESCRIPTOR'S head count, not the shape's — `PN_TT_GDN_HEADS` narrows the request + * and the two knobs have to compose or refuse, not silently address heads the descriptor + * does not claim. The caller has already checked that they divide. */ + const bool conv = request->phase == FT_TT_OP_GDN_PHASE_CONV; + /* DELTA divides by v-head; CONV by TILE COLUMN of the stream (each member convolves its + * columns in every tap plane, at the full plane stride — the kernels take an absolute + * column). Either way the caller has already checked that the count divides. */ + const uint32_t per_member = conv ? (request->conv_dim / FT_TT_OP_TILE_DIM) / members + : request->v_heads / members; + uint32_t words[PN_TT_GDN_MAX_MEMBERS][FT_TT_OP_GDN_WORDS]; + pn_tt_residency *set[PN_TT_GDN_MAX_MEMBERS]; + bool bad[PN_TT_GDN_MAX_MEMBERS]; + double started; + uint32_t index; + uint64_t rearms = 0; + int failed = 0; + + memset(bad, 0, sizeof(bad)); + for (index = 0; index < members; index++) { + ft_tt_op_gdn_request sliced = *request; + + if (conv) { + /* RED PROOF, `PN_TT_RED=gdn_conv_col_dup`: every member convolves member 0's + * columns, so columns past the first slice are never written — must DIFFER. */ + sliced.col_first = pn_tt_red("gdn_conv_col_dup") ? 0u : index * per_member; + sliced.col_count = per_member; + } else { + sliced.head_first = index * per_member; + sliced.head_count = per_member; + } + if (ft_tt_op_gdn_parameters(&sliced, words[index]) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdn %s slice [%u, %u) is one the family refuses\n", + conv ? "column" : "head", index * per_member, (index + 1u) * per_member); + return -1; + } + set[index] = index == 0u ? &device->gdn : &device->gdn_member[index - 1u]; + } + + /* RING FLOW CONTROL. A deferred member holds one descriptor a row, unawaited, and + * ft_tt_queue_stage() does not check occupancy: past `capacity` the host rewrote ring + * slots the card had not read yet (10+ verify rows DIFFERED on an 8-slot ring). A full + * ring drains first. RED `gdn_defer_overrun` skips the drain — must DIFFER when the rows + * exceed the ring. */ + if (device->gdn_defer && device->gdn_pending_depth >= set[0]->queue.capacity && + !pn_tt_red("gdn_defer_overrun")) { + if (pn_tt_gdn_await_deferred(device) != 0) + return -1; + if (device->gdn_defer_drains++ == 0u) + fprintf(stderr, "pn_tt: gdn — deferred rows fill the %u-slot ring; draining it " + "(flow control)\n", set[0]->queue.capacity); + } + started = pn_tt_now_seconds(); + for (index = 0; index < members; index++) { + /* S2c DEFERRED: a queued descriptor keeps its own parameter block until it runs — + * the shipped single slot would be overwritten by the next row's. */ + set[index]->param_slot = device->gdn_defer ? 1u + device->gdn_row : 0u; + if (gdn_write_row_extension(set[index], request) != 0 || + pn_tt_residency_enqueue(set[index], FT_TT_QUEUE_OP_GDN, words[index], 1u) != 0) + failed = bad[index] = 1; + set[index]->param_slot = 0u; + } + /* S2c DEFERRED: queued behind each other on every member (in-order per core is the only + * ordering a row of the verify needs from the previous row); awaited together later. */ + if (device->gdn_defer && !failed) { + if (members > device->gdn_pending_members) + device->gdn_pending_members = members; + device->gdn_pending_depth++; + device->oncard_ops += 1u; + return 0; + } + /* EVERY member is awaited even after one has failed. An enqueue that got as far as the + * ring has a descriptor the card will run whatever this host does next, and leaving it + * un-awaited desynchronises that member's cursor for the rest of the run. */ + for (index = 0; index < members; index++) + if (pn_tt_residency_await(set[index], PN_TT_OP_TIMEOUT_MS) != 0) + failed = bad[index] = 1; + + /* PER-MEMBER RE-ARM AND REPLAY, AND IT RUNS ONLY WHERE THIS FUNCTION USED TO RETURN -1. + * + * A resident triad is BOUNDED: ~40 s with no descriptor retires it. Until the chunkwise + * prefill landed, this family ran two descriptors per SSM layer per POSITION and could + * never idle that long inside a request — a batched prefill running the chunkwise mixers + * leaves it idle for the WHOLE PROMPT, which at 8k is over a minute, so the first decode + * token after one would have found four retired residencies and this function had no + * recovery at all while `gdn_submit()` beside it has had one since it was written. + * + * Replaying ONE member's slice is the same computation because a refused descriptor + * leaves no circular-buffer traffic behind it — the reader aborts before the rebase — and + * because the head slices are disjoint in DRAM: the carried state is head-major, and `y` + * is scattered at row granularity. A member that succeeded is not touched. + */ + if (failed) { + failed = 0; + for (index = 0; index < members; index++) { + if (!bad[index]) + continue; + rearms++; + if (!gdn_member_rearm(device, index) || + gdn_write_row_extension(set[index], request) != 0 || + pn_tt_residency_submit(set[index], FT_TT_QUEUE_OP_GDN, words[index], + PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + } + { + const double elapsed = pn_tt_now_seconds() - started; + + device->oncard_seconds += elapsed; + device->oncard_ops += 1u; + gdn_account_phase(device, conv ? FT_TT_OP_GDN_PHASE_CONV : FT_TT_OP_GDN_PHASE_DELTA, + elapsed, 1u, rearms); + } + return failed ? -1 : 0; +} + +static int gdn_submit(pn_tt_device *device, const ft_tt_op_gdn_request *request) +{ + /* THE QUEUE-DEPTH INSTRUMENT. `PN_TT_GDN_BATCH=K` enqueues K copies of this descriptor + * and waits ONCE, so the host's parameter write, ring write, head bump and first poll are + * paid once for K descriptors instead of K times. `seconds(K) = host_fixed + + * K * on_card_serial`, and two values of K solve both terms — which is the whole question + * "is the 2.3 ms a round trip or the card's own work". + * + * IT BREAKS THE RECURRENCE and is not a fallback: every copy runs the same parameter + * block, so copies 2..K decay and update a state the model did not ask them to. The + * TIMING is what this arm produces; the text it generates is not evidence of anything. */ + const uint32_t repeat = gdn_probe_knob("PN_TT_GDN_BATCH"); + uint32_t words[FT_TT_OP_GDN_WORDS]; + uint64_t rearms = 0; + double started; + double elapsed; + ft_tt_op_gdn_request flagged; + + /* M6: `PN_TT_GDN_NOC_REPLICATE` — the DELTA reader builds its replicated k/q tiles with + * local NoC copies (kGdnFlagNocReplicate). Same bytes; a reader-side cost only. */ + { + static int noc_replicate = -1; + + static int conv_strip = -1; + + if (noc_replicate < 0) + noc_replicate = (int)pn_tt_env_u32("PN_TT_GDN_NOC_REPLICATE", 1u); + /* M9: `PN_TT_GDN_CONV_STRIP` — PHASE CONV moves only ROW 0 of every tile it reads + * and writes (2 x 64 B instead of 4 KiB); all its arithmetic is elementwise, and + * rows 1-31 of its DRAM buffers are left as they are. ON since 2026-10-05: CONV + * 268 -> 153.5 us, decode 15.47 -> 16.00 tok/s, IDENTICAL; red DIFFERS. =0 is off. */ + if (conv_strip < 0) + conv_strip = (int)pn_tt_env_u32("PN_TT_GDN_CONV_STRIP", 1u); + flagged = *request; + if (conv_strip != 0) { + flagged.flags |= FT_TT_OP_GDN_FLAG_CONV_STRIP; + /* RED: columns 16-31 of every CONV operand stay stale — must DIFFER. */ + if (pn_tt_red("gdn_conv_strip_half")) + flagged.flags |= FT_TT_OP_GDN_FLAG_RED_STRIP_HALF; + } + request = &flagged; + if (noc_replicate != 0) { + flagged.flags |= FT_TT_OP_GDN_FLAG_NOC_REPLICATE; + if (pn_tt_red("gdn_noc_replicate_no_copy")) + flagged.flags |= FT_TT_OP_GDN_FLAG_RED_NO_COPY; + } + } + + /* THE HEAD SPLIT TAKES PHASE DELTA AND NOTHING ELSE, and only when this run armed + * members for it. `PN_TT_GDN_BATCH` is refused alongside it rather than combined: the + * batch instrument deliberately re-runs one parameter block K times and would give every + * member a different number of repeats of a broken recurrence. */ + if (gdn_delta_members(device) > 1u && request->phase == FT_TT_OP_GDN_PHASE_DELTA && + !repeat && request->v_heads >= gdn_delta_members(device) && + (request->v_heads % gdn_delta_members(device)) == 0u) + return gdn_submit_delta_split(device, request); + /* AND PHASE CONV, by tile column, behind `PN_TT_GDN_CONV_SPLIT` until it is measured: it + * needs the gdn programs that honour the column range (regenerated 2026-10). The count + * must divide the stream into slices the CONV block depth also divides. */ + if (gdn_conv_members(device) > 1u && request->phase == FT_TT_OP_GDN_PHASE_CONV && + !repeat && gdn_conv_split_enabled()) { + const uint32_t cols = request->conv_dim / FT_TT_OP_TILE_DIM; + const uint32_t per = cols / gdn_conv_members(device); + + if (per > 0u && (cols % gdn_conv_members(device)) == 0u && + (per % ft_tt_op_gdn_conv_depth()) == 0u) + return gdn_submit_delta_split(device, request); + } + + if (ft_tt_op_gdn_parameters(request, words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdn phase-%u descriptor is one the family refuses\n", + request->phase); + return -1; + } + started = pn_tt_now_seconds(); + if (gdn_write_row_extension(&device->gdn, request) != 0 || + pn_tt_residency_submit_repeat(&device->gdn, FT_TT_QUEUE_OP_GDN, words, + repeat ? repeat : 1u, PN_TT_OP_TIMEOUT_MS) != 0) { + rearms = 1u; + if (!pn_tt_gdn_rearm(device) || gdn_write_row_extension(&device->gdn, request) != 0 || + pn_tt_residency_submit_repeat(&device->gdn, FT_TT_QUEUE_OP_GDN, words, + repeat ? repeat : 1u, PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + elapsed = pn_tt_now_seconds() - started; + device->oncard_seconds += elapsed; + /* DESCRIPTORS, not host waits — so "seconds per descriptor" stays the same quantity at + * every batch depth and the two can be divided against each other. */ + device->oncard_ops += repeat ? repeat : 1u; + gdn_account_phase(device, request->phase, elapsed, repeat ? repeat : 1u, rearms); + return 0; +} + +/** + * PROVE THE HOST STAGING BEFORE BLAMING THE CARD: write the qkv row back out of DRAM and + * compare it with what went in, and read the conv weight's tap-0 plane back and compare it + * with the oracle's `w[c*taps]`. + * + * Runs once, on the first CONV of a run, under `PN_TT_GDN_SELFCHECK`. It exists because a + * wrong answer out of this op has two halves — the host's DRAM layout and the card's + * arithmetic — and nothing else in the stack can tell them apart. A clean self-check moves + * the whole search onto the card; a dirty one ends it here. + */ +static void gdn_self_check(pn_tt_device *device, const float *qkv, const float *weight, + const pn_tt_gdn_buffer *weight_buffer) +{ + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const uint32_t taps = device->gdn_shape.conv_taps; + const uint32_t tiles = conv_dim / PN_TT_GDN_TILE_DIM; + float *echo = calloc(conv_dim, sizeof(*echo)); + uint32_t index; + uint32_t tap; + uint32_t stream_bad = 0; + uint32_t weight_bad = 0; + + if (!echo) + return; + if (gdn_row0_move(device, &device->gdn_stream_in, tiles, echo, true) == 0) { + for (index = 0; index < conv_dim; index++) + if (echo[index] != qkv[index]) + stream_bad++; + } else { + stream_bad = conv_dim; + } + fprintf(stderr, "pn_tt: gdn SELFCHECK stream row 0 — %u of %u channels differ; " + "in[0..3] %g %g %g %g echo[0..3] %g %g %g %g\n", + stream_bad, conv_dim, (double)qkv[0], (double)qkv[1], (double)qkv[2], + (double)qkv[3], (double)echo[0], (double)echo[1], (double)echo[2], + (double)echo[3]); + + /* EVERY tap plane, not just tap 0. At position 0 the window is zero and the only weight + * the card touches is tap `taps-1`, so a check that stops at plane 0 leaves the one plane + * that matters unexamined. */ + for (tap = 0; weight && tap < taps; tap++) { + const pn_tt_gdn_buffer plane = { + weight_buffer->bank, + weight_buffer->address + tap * tiles * PN_TT_GDN_TILE_BYTES + }; + + weight_bad = 0; + if (gdn_row0_move(device, &plane, tiles, echo, true) != 0) { + weight_bad = conv_dim; + } else { + for (index = 0; index < conv_dim; index++) + if (echo[index] != weight[(size_t)index * taps + tap]) + weight_bad++; + } + fprintf(stderr, "pn_tt: gdn SELFCHECK conv weight tap-%u plane — %u of %u channels " + "differ; oracle[0..2] %g %g %g card[0..2] %g %g %g\n", + tap, weight_bad, conv_dim, (double)weight[tap], (double)weight[taps + tap], + (double)weight[2u * taps + tap], (double)echo[0], (double)echo[1], + (double)echo[2]); + } + free(echo); +} + +/** + * Read tile 0 of a CONV operand back WHOLE — all 32 logical rows — and report row 0 against + * the 31 pad rows. + * + * The whole conv path rests on "row 0 is live and rows 1-31 stay zero through the + * convolution and the roll, because SiLU(0) = 0". Nothing on the card enforces that, and if + * it is false the operand the NEXT token convolves is not the one this one wrote. Reading a + * whole tile is the only way to see it: the row-0 mover cannot, by construction. + */ +static void gdn_self_check_out(pn_tt_device *device, const pn_tt_gdn_buffer *buffer, + uint32_t first_tile, const char *name) +{ + float tile[PN_TT_GDN_TILE_DIM * PN_TT_GDN_TILE_DIM]; + uint32_t face; + uint32_t nonzero[4] = { 0u, 0u, 0u, 0u }; + uint32_t negative_zero = 0; + uint32_t worst_bits = 0; + + if (pn_tt_read_dram(device, buffer->bank, + buffer->address + first_tile * PN_TT_GDN_TILE_BYTES, tile, + sizeof(tile)) != 0) + return; + /* Face-ordered 2x2 grid of 16x16: face f is elements [256*f, +256). Row 0 lives in the + * first 16 of face 0 and the first 16 of face 1; everything else is pad. + * + * ASKED OF THE BITS, NOT OF `!= 0.0f`. Two reasons, and the second is the one that + * matters: this tree compiles with `-ffast-math`, so the compiler is licensed to reason + * about float comparisons; and `-0.0f != 0.0f` is FALSE, so a pad row of negative zeros + * would read as clean under a value test. Any proposal to SKIP work on the pad rows — + * a row-0-only SFPU pass over the CONV SiLU is the live one — rests on this being an + * exact bit pattern, so it is measured as one. `-0.0` is reported separately rather than + * folded in: SiLU(-0.0) is -0.0, so it is bit-safe for that lever but is still not the + * zero the strip invariant claims. */ + for (face = 0; face < 4u; face++) { + uint32_t index; + + for (index = (face < 2u) ? 16u : 0u; index < 256u; index++) { + uint32_t bits; + + memcpy(&bits, &tile[face * 256u + index], sizeof(bits)); + if (bits == 0u) + continue; + if (bits == 0x80000000u) { + negative_zero++; + continue; + } + nonzero[face]++; + if (bits > worst_bits) + worst_bits = bits; + } + } + fprintf(stderr, "pn_tt: gdn SELFCHECK %s tile %u — pad non-zero BITS by face %u/240 " + "%u/240 %u/256 %u/256, %u negative zeros, worst bits %08x; row 0 [0..3] " + "%g %g %g %g; pad [16..18] %g %g %g\n", + name, first_tile, nonzero[0], nonzero[1], nonzero[2], nonzero[3], negative_zero, + worst_bits, (double)tile[0], (double)tile[1], (double)tile[2], (double)tile[3], + (double)tile[16], (double)tile[17], (double)tile[18]); +} + +/** + * At position 0 the window is zero, so `conv_out = SiLU(qkv * w[.][taps-1])` — ONE product + * and one SFPU call, with three additions of zero between them. Scoring the card against BOTH + * the raw product and the SiLU of it says which of the two the card actually did, and how + * well: a card that matches the product is a kernel missing its SiLU, and a card that matches + * neither has an arithmetic problem in the multiply. + */ +static void gdn_self_check_arithmetic(pn_tt_device *device, const float *qkv, + const float *weight) +{ + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const uint32_t taps = device->gdn_shape.conv_taps; + float *echo = calloc(conv_dim, sizeof(*echo)); + double worst_product = 0.0; + double worst_silu = 0.0; + double scale = 0.0; + double worst_x = 0.0; + double worst_want = 0.0; + double worst_got = 0.0; + uint32_t worst_at = 0; + uint32_t index; + + if (!echo || !weight) + goto done; + if (gdn_row0_move(device, &device->gdn_stream_out, conv_dim / PN_TT_GDN_TILE_DIM, echo, + true) != 0) + goto done; + for (index = 0; index < conv_dim; index++) { + const double product = (double)qkv[index] * (double)weight[(size_t)index * taps + + (taps - 1u)]; + const double silu = product / (1.0 + exp(-product)); + const double product_gap = fabs((double)echo[index] - product); + const double silu_gap = fabs((double)echo[index] - silu); + + if (fabs(silu) > scale) + scale = fabs(silu); + if (product_gap > worst_product) + worst_product = product_gap; + if (silu_gap > worst_silu) { + worst_silu = silu_gap; + worst_at = index; + worst_x = product; + worst_want = silu; + worst_got = (double)echo[index]; + } + } + fprintf(stderr, "pn_tt: gdn SELFCHECK position-0 arithmetic — max-norm relative against " + "the RAW PRODUCT %.3e, against SiLU(product) %.3e (tensor max %.4g)\n", + scale > 0.0 ? worst_product / scale : 0.0, scale > 0.0 ? worst_silu / scale : 0.0, + scale); + fprintf(stderr, "pn_tt: gdn SELFCHECK worst SiLU channel %u: x %.6g want %.6g got " + "%.6g (sigmoid want %.6g got %.6g)\n", + worst_at, worst_x, worst_want, worst_got, + worst_x != 0.0 ? worst_want / worst_x : 0.0, + worst_x != 0.0 ? worst_got / worst_x : 0.0); +done: + free(echo); +} + +/** Read the row-0 strip of a CONV plane back and compare it against a host array. */ +static void gdn_self_check_plane(pn_tt_device *device, const pn_tt_gdn_buffer *buffer, + uint32_t first_tile, const float *expect, const char *name) +{ + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const pn_tt_gdn_buffer plane = { buffer->bank, + buffer->address + first_tile * PN_TT_GDN_TILE_BYTES }; + float *echo = calloc(conv_dim, sizeof(*echo)); + uint32_t index; + uint32_t bad = 0; + + if (!echo) + return; + if (gdn_row0_move(device, &plane, conv_dim / PN_TT_GDN_TILE_DIM, echo, true) == 0) { + for (index = 0; index < conv_dim; index++) + if (echo[index] != expect[index]) + bad++; + } else { + bad = conv_dim; + } + fprintf(stderr, "pn_tt: gdn SELFCHECK %s — %u of %u channels differ; expect[0..3] %g %g " + "%g %g card[0..3] %g %g %g %g\n", + name, bad, conv_dim, (double)expect[0], (double)expect[1], (double)expect[2], + (double)expect[3], (double)echo[0], (double)echo[1], (double)echo[2], + (double)echo[3]); + free(echo); +} + +/** + * NAME THE TENSOR THE NEWEST WINDOW PLANE HOLDS, by searching CARD DRAM rather than the host. + * + * The earlier searches asked the host's own arrays — this token's qkv, every SSM layer's qkv + * from the PREVIOUS token, every tile rotation of the stream — and all three came back + * negative. What they could not ask is "which BUFFER ON THE CARD does this plane equal", and + * that is the question a wrong DRAM address answers. Every candidate below is read back off + * the card at the moment of the miss, so a match names a buffer and an offset rather than a + * host tensor that may never have been on the card at all. + * + * The ELEMENT shift is the second half. `gdn_self_check_window()` searches TILE rotations, 32 + * channels at a step, because a circular buffer out of step by whole pages produces exactly + * that. A face-row or a within-tile offset is a different bug and lands between those steps. + */ +static void gdn_name_plane(pn_tt_device *device, const float *plane, const float *stream) +{ + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const uint32_t taps = device->gdn_shape.conv_taps; + const uint32_t tiles = conv_dim / PN_TT_GDN_TILE_DIM; + float *echo = calloc(conv_dim, sizeof(*echo)); + uint32_t best_shift = 0; + uint32_t best_off = conv_dim + 1u; + uint32_t layer; + uint32_t index; + uint32_t shift; + + if (!echo) + return; + + /* EVERY element shift, not every tile shift. */ + for (shift = 0; shift < conv_dim; shift++) { + uint32_t off = 0; + + for (index = 0; index < conv_dim; index++) + if (plane[index] != stream[(index + shift) % conv_dim]) + off++; + if (off < best_off) { + best_off = off; + best_shift = shift; + } + } + fprintf(stderr, "pn_tt: gdn NAME best ELEMENT shift against the stream: %u channels, " + "%u of %u differ\n", best_shift, best_off, conv_dim); + + /* Every OTHER buffer this family owns, at the offset a wrong address would land on. Each + * is one row-0 strip of `tiles` tiles, the same shape the plane is. */ + { + struct { + const char *name; + const pn_tt_gdn_buffer *buffer; + uint32_t first_tile; + } candidate[6]; + unsigned count = 0; + unsigned which; + + candidate[count].name = "conv_out (activation out)"; + candidate[count].buffer = &device->gdn_stream_out; + candidate[count++].first_tile = 0u; + candidate[count].name = "delta input q|k|v"; + candidate[count].buffer = &device->gdn_stream_out; + candidate[count++].first_tile = 0u; + candidate[count].name = "layer 0 recurrent state, tiles 0.."; + candidate[count].buffer = &device->gdn_region[0].state; + candidate[count++].first_tile = 0u; + candidate[count].name = "layer 0 recurrent state, tiles 256.."; + candidate[count].buffer = &device->gdn_region[0].state; + candidate[count++].first_tile = tiles; + for (which = 0; which < count; which++) { + uint32_t off = 0; + + if (gdn_row0_move(device, candidate[which].buffer, tiles, echo, true) != 0) + continue; + for (index = 0; index < conv_dim; index++) + if (plane[index] != echo[index]) + off++; + fprintf(stderr, "pn_tt: gdn NAME vs %s (tile %u): %u of %u differ; card[0..2] " + "%g %g %g\n", + candidate[which].name, candidate[which].first_tile, off, conv_dim, + (double)echo[0], (double)echo[1], (double)echo[2]); + } + } + + /* Every layer's conv weight tap plane and every layer's window plane. A wrong plane index + * or a wrong layer stride lands in one of these and nowhere else. */ + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) { + uint32_t tap; + + for (tap = 0; tap < taps; tap++) { + const pn_tt_gdn_buffer view = { + device->gdn_region[layer].weight.bank, + device->gdn_region[layer].weight.address + tap * tiles * PN_TT_GDN_TILE_BYTES + }; + uint32_t off = 0; + + if (gdn_row0_move(device, &view, tiles, echo, true) != 0) + continue; + for (index = 0; index < conv_dim; index++) + if (plane[index] != echo[index]) + off++; + if (off * 100u < conv_dim) + fprintf(stderr, "pn_tt: gdn NAME IS layer %u conv WEIGHT tap plane %u " + "(%u of %u differ)\n", layer, tap, off, conv_dim); + } + for (tap = 0; tap + 1u < taps; tap++) { + const pn_tt_gdn_buffer view = { + device->gdn_region[layer].window.bank, + device->gdn_region[layer].window.address + tap * tiles * PN_TT_GDN_TILE_BYTES + }; + uint32_t off = 0; + + if (layer == 0u && tap + 2u == taps) + continue; /* the plane itself */ + if (gdn_row0_move(device, &view, tiles, echo, true) != 0) + continue; + for (index = 0; index < conv_dim; index++) + if (plane[index] != echo[index]) + off++; + if (off * 100u < conv_dim) + fprintf(stderr, "pn_tt: gdn NAME IS layer %u WINDOW plane %u (%u of %u " + "differ)\n", layer, tap, off, conv_dim); + } + } + + /* The bit patterns, because a decimal cannot tell a shifted exponent from a wrong tile. */ + { + union { float f; uint32_t u; } value; + + fprintf(stderr, "pn_tt: gdn NAME plane[0..31] hex"); + for (index = 0; index < 32u; index++) { + value.f = plane[index]; + fprintf(stderr, " %08x", value.u); + } + fprintf(stderr, "\npn_tt: gdn NAME plane[8160..8191] hex"); + for (index = conv_dim - 32u; index < conv_dim; index++) { + value.f = plane[index]; + fprintf(stderr, " %08x", value.u); + } + fprintf(stderr, "\npn_tt: gdn NAME stream[0..31] hex"); + for (index = 0; index < 32u; index++) { + value.f = stream[index]; + fprintf(stderr, " %08x", value.u); + } + fprintf(stderr, "\n"); + /* THE CHANNELS THAT DID MATCH. Five of 8192 agreeing with the roll is not noise at + * bf16 spacing; WHERE they are says whether the plane is partly right (a short + * transfer) or right by coincidence (a different tensor that happens to collide). */ + { + unsigned found = 0; + + fprintf(stderr, "pn_tt: gdn NAME channels that DO match the stream:"); + for (index = 0; index < conv_dim && found < 16u; index++) + if (plane[index] == stream[index]) { + fprintf(stderr, " %u", index); + found++; + } + fprintf(stderr, "%s\n", found ? "" : " none"); + } + } + free(echo); +} + +/** + * SCORE THE ROLLED WINDOW ACROSS TOKENS, which position 0 cannot. + * + * `gdn_self_check_plane()` compares the newest plane against this token's qkv, and at + * position 0 that is the whole of the window — every other plane is zero, so a clean result + * says nothing about the ROLL. The roll is `window_new[t] = window_old[t+1]`, and it is the + * only part of PHASE CONV that carries state from one token to the next. + * + * The shadow here is rolled the way the contract says, from the SAME qkv the card was given, + * so a disagreement is the card's roll and not a drifting input. Layer 0 only: one layer's + * roll is the same code as every layer's, and 24 shadows would be 786 KiB of host memory for + * no extra statement. + */ +static void gdn_self_check_window(pn_tt_device *device, const pn_tt_gdn_buffer *window, + const float *qkv) +{ + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const uint32_t taps = device->gdn_shape.conv_taps; + const uint32_t tiles = conv_dim / PN_TT_GDN_TILE_DIM; + float *echo = calloc(conv_dim, sizeof(*echo)); + uint32_t plane; + + if (!echo) + return; + if (!device->gdn_window_shadow) + device->gdn_window_shadow = calloc((size_t)(taps - 1u) * conv_dim, sizeof(float)); + if (!device->gdn_window_shadow) { + free(echo); + return; + } + /* THE NEWEST PLANE IS READ BEFORE ANY OTHER TRAFFIC THIS FUNCTION MAKES. Every readback + * here goes through `pn_tt_matvec_flush()`, so a plane read after three other reads cannot + * say whether the descriptor wrote it wrong or something after the descriptor wrote over + * it. Read first, and the answer is the descriptor's. The whole TILE goes with it: rows + * 1-31 of a conv operand are zero by construction, so a plane whose pad is non-zero did + * not come from the stream at all and the search below is looking for a dense tensor. */ + gdn_self_check_out(device, window, (taps - 2u) * tiles, "window newest plane, FIRST read"); + for (plane = 0; plane + 2u < taps; plane++) + memcpy(device->gdn_window_shadow + (size_t)plane * conv_dim, + device->gdn_window_shadow + (size_t)(plane + 1u) * conv_dim, + (size_t)conv_dim * sizeof(float)); + memcpy(device->gdn_window_shadow + (size_t)(taps - 2u) * conv_dim, qkv, + (size_t)conv_dim * sizeof(float)); + + /* NAME the tensor, do not describe it. The stream and the window are the two operands the + * copy is between, and if the plane is neither then the only other thing that has been + * through this buffer is ANOTHER LAYER's qkv — so every layer's is kept and searched. */ + { + float *stream = calloc(conv_dim, sizeof(*stream)); + + if (stream && gdn_row0_move(device, &device->gdn_stream_in, tiles, stream, true) == 0) { + for (plane = 0; plane + 1u < taps; plane++) { + const pn_tt_gdn_buffer view = { + window->bank, window->address + plane * tiles * PN_TT_GDN_TILE_BYTES + }; + const float *want = device->gdn_window_shadow + (size_t)plane * conv_dim; + uint32_t bad = 0; + uint32_t vs_stream = 0; + uint32_t index; + + if (gdn_row0_move(device, &view, tiles, echo, true) != 0) { + bad = conv_dim; + vs_stream = conv_dim; + } else { + for (index = 0; index < conv_dim; index++) { + if (echo[index] != want[index]) + bad++; + if (echo[index] != stream[index]) + vs_stream++; + } + } + fprintf(stderr, "pn_tt: gdn SELFCHECK window token %u plane %u — %u of %u " + "differ from the roll, %u from the card's own qkv stream; " + "want[0..2] %g %g %g card[0..2] %g %g %g\n", + device->gdn_window_checks, plane, bad, conv_dim, vs_stream, + (double)want[0], (double)want[1], (double)want[2], (double)echo[0], + (double)echo[1], (double)echo[2]); + if (bad != 0u) { + /* IS IT THE SAME STREAM, ROTATED? A circular buffer whose producer and + * consumer are out of step by `s` pages delivers exactly that — the right + * tiles in the wrong order — and it is indistinguishable from garbage in a + * per-channel difference count. One tile is 32 channels. */ + const uint32_t tiles_of = conv_dim / PN_TT_GDN_TILE_DIM; + uint32_t shift; + + for (shift = 1u; shift < tiles_of; shift++) { + uint32_t off = 0; + + for (index = 0; index < conv_dim; index++) { + const uint32_t from = (index + shift * PN_TT_GDN_TILE_DIM) % conv_dim; + + if (echo[index] != stream[from]) + off++; + } + if (off * 100u < conv_dim) { + fprintf(stderr, "pn_tt: gdn SELFCHECK plane %u IS the qkv stream " + "ROTATED BY %u TILES (%u of %u channels differ)\n", + plane, shift, off, conv_dim); + break; + } + } + if (shift >= tiles_of) + fprintf(stderr, "pn_tt: gdn SELFCHECK plane %u is not the qkv stream " + "at ANY tile rotation\n", plane); + /* The NEWEST plane is the one this defect is about: the older planes are + * byte-exact and a search of them would only re-report the roll. */ + if (plane + 2u == taps) + gdn_name_plane(device, echo, stream); + } + } + } + free(stream); + } + device->gdn_window_checks++; + free(echo); +} + +/** + * STAGE AN INJECTIVE STREAM AND READ THE NEWEST PLANE BACK, so the transform is a MAP rather + * than a verdict. + * + * Every probe so far can only say the plane is not qkv. With `qkv[c] = c` the plane names its + * own source: `plane[c] == c` is a copy, `plane[c] == c + k` is a shift, `plane[c] == c % 256` + * is a buffer of 8 pages read 32 times over, and a value outside `[0, conv_dim)` did not come + * from the stream at all. It costs one extra CONV descriptor and it POISONS the window and + * `conv_out` for that layer, which is why it is behind its own variable and runs once. + */ +static void gdn_ramp_probe(pn_tt_device *device, uint32_t layer, + const ft_tt_op_gdn_request *template_request) +{ + const uint32_t conv_dim = device->gdn_shape.conv_dim; + const uint32_t tiles = conv_dim / PN_TT_GDN_TILE_DIM; + const uint32_t plane_tile = (device->gdn_shape.conv_taps - 2u) * tiles; + const pn_tt_gdn_buffer plane = { + device->gdn_region[layer].window.bank, + device->gdn_region[layer].window.address + plane_tile * PN_TT_GDN_TILE_BYTES + }; + ft_tt_op_gdn_request request = *template_request; + float *ramp = calloc(conv_dim, sizeof(*ramp)); + float *echo = calloc(conv_dim, sizeof(*echo)); + uint32_t identity = 0; + uint32_t in_range = 0; + uint32_t index; + + if (!ramp || !echo) + goto done; + for (index = 0; index < conv_dim; index++) + ramp[index] = (float)index; + if (gdn_row0_move(device, &device->gdn_stream_in, tiles, ramp, false) != 0) + goto done; + if (gdn_submit(device, &request) != 0) + goto done; + if (gdn_row0_move(device, &plane, tiles, echo, true) != 0) + goto done; + + for (index = 0; index < conv_dim; index++) { + if (echo[index] == (float)index) + identity++; + if (echo[index] >= 0.0f && echo[index] < (float)conv_dim && + echo[index] == (float)(int)echo[index]) + in_range++; + } + fprintf(stderr, "pn_tt: gdn RAMP layer %u — %u of %u channels are the IDENTITY, %u are a " + "whole number inside [0, %u)\n", + layer, identity, conv_dim, in_range, conv_dim); + for (index = 0; index < 64u; index += 16u) { + unsigned step; + + fprintf(stderr, "pn_tt: gdn RAMP plane[%u..%u]", index, index + 15u); + for (step = 0; step < 16u; step++) + fprintf(stderr, " %g", (double)echo[index + step]); + fprintf(stderr, "\n"); + } + fprintf(stderr, "pn_tt: gdn RAMP plane[8160..8175]"); + for (index = conv_dim - 32u; index < conv_dim - 16u; index++) + fprintf(stderr, " %g", (double)echo[index]); + fprintf(stderr, "\n"); + /* THE WHOLE TILE MAP. Output tile `t` holds column 0 of source tile `s`, so one number a + * tile says where every tile came from — and the shape of `s(t) - t` is the difference + * between a constant page offset, a drift and a wrap. */ + for (index = 0; index < tiles; index += 32u) { + unsigned step; + + fprintf(stderr, "pn_tt: gdn RAMP source tile for out tiles %3u..%3u:", index, + index + 31u); + for (step = 0; step < 32u; step++) + fprintf(stderr, " %d", (int)(echo[(index + step) * PN_TT_GDN_TILE_DIM] / 32.0f)); + fprintf(stderr, "\n"); + } +done: + free(ramp); + free(echo); +} + +/* --------------------------------------------------------------------------- + * THE CONV DATA ARM — three stopwatches, no gate + * --------------------------------------------------------------------------- + * "CONV is DATA-DEPENDENT — 1222.7 us on real activations, 4571 on a degenerate stream, + * suspect an SFPU slow path on out-of-range SiLU input" stood as an open item with no + * harness behind either half. The suspicion is refuted at the SOURCE — the SiLU stream is + * straight-line, every conditional in it is SFPU condition-code-stack predication + * (`ft_tensix_sfpu.h`'s `sfppushc`/`sfpxvif`/`sfpcompc`/`sfppopc`), so disabled lanes still + * ISSUE and only write-back is masked; `exp` is a fixed degree-5 Horner and `recip` is + * exactly two Newton steps with the second PREDICATED, not branched. These arms are what + * settle it on silicon. + * + * THREE NAMES, NOT ONE KNOB WITH A VALUE, because `pn_tt_red()` is an exact match on one + * name: an arm therefore cannot be selected by a value nobody parses, and each arm names + * itself in the log. + * + * gdn_conv_row0_uniform the shipped OCCUPANCY (row 0 live, rows 1-31 zero), benign + * magnitudes — the control for the fill machinery itself + * gdn_conv_full_uniform THE SHARP ONE. The same row-0 values, plus all 31 pad rows + * filled: **32x the non-zero SFPU operands at identical shape, + * identical traffic and an identical descriptor.** + * gdn_conv_out_of_range row-0 occupancy, values the hypothesis names: +-1e30 (SiLU + * saturates), +-1e-40 (DENORMAL), and the 0..8191 ramp + * + * Deliberately NOT infinities or NaNs: those would propagate into PHASE DELTA's recurrence + * and the arms would differ in what the DELTA descriptor beside them does, which is a second + * variable. Saturating and denormal operands are what the suspicion is actually about. + * + * Each produces a WRONG ANSWER by construction. The timing is what they are for. + */ +typedef struct pn_tt_gdn_synthetic { + const char *name; /**< NULL when no arm is selected, which is the control */ + bool full; /**< fill all 31 pad rows as well as logical row 0 */ + bool out_of_range; /**< +-1e30 and +-1e-40 instead of uniform [-1, 1] */ +} pn_tt_gdn_synthetic; + +/** + * Which arm, as FLAGS rather than a name to sniff. + * + * The first version keyed `full` off `arm[0] == 'f'` and `out_of_range` off `'o'`, which is + * correct for exactly the three names it had and silently wrong for the fourth: a + * `full_out_of_range` arm begins with 'f', so it would have filled the pad and then drawn + * every value from the BENIGN generator — a maximal-stress arm that ran no stress and read + * as a null result. The flags are what the fill actually needs, so they are what it is given. + */ +static pn_tt_gdn_synthetic gdn_synthetic_arm(void) +{ + static const pn_tt_gdn_synthetic none = { NULL, false, false }; + static const pn_tt_gdn_synthetic row0 = { "row0_uniform", false, false }; + static const pn_tt_gdn_synthetic full = { "full_uniform", true, false }; + static const pn_tt_gdn_synthetic wild = { "out_of_range", false, true }; + /* THE MAXIMAL STRESS: 992 out-of-range operands a tile instead of 32, so the SFPU sees + * a saturating or denormal value in nearly every lane of every face. */ + static const pn_tt_gdn_synthetic full_wild = { "full_out_of_range", true, true }; + + if (pn_tt_red("gdn_conv_row0_uniform")) + return row0; + if (pn_tt_red("gdn_conv_full_uniform")) + return full; + if (pn_tt_red("gdn_conv_out_of_range")) + return wild; + if (pn_tt_red("gdn_conv_full_out_of_range")) + return full_wild; + return none; +} + +/** + * One 32x32 fp32 tile of the arm's stream, FACE-ORDERED. + * + * A tile is four 16x16 faces in row-major order of the 2x2 face grid, so LOGICAL ROW 0 IS + * NOT CONTIGUOUS: columns 0-15 are elements 0-15 of face 0 and columns 16-31 are elements + * 256-271 (`ft_tt_tile_row0.h`). A contiguous 32-float write at offset 0 would land on tile + * ROWS 0 AND 1, columns 0-15 — a stream that is neither the shipped occupancy nor a full + * tile, and every figure taken against it would be of a third thing nobody asked about. + * + * THE THIRTY-TWO LIVE VALUES ARE DRAWN FIRST AND ARE THE SAME IN EVERY ARM AT THE SAME + * `full`, so `row0_uniform` and `full_uniform` differ in the PAD AND NOTHING ELSE. + */ +static float gdn_synthetic_value(const pn_tt_gdn_synthetic *arm, uint32_t *state, + uint32_t channel) +{ + /* +-1e30 (SiLU saturates), +-1e-40 (DENORMAL in fp32). No infinity, no NaN — see the + * block above `gdn_synthetic_arm()` for why those would add a second variable. */ + static const float wild[4] = { 1e30f, -1e30f, 1e-40f, -1e-40f }; + + *state = *state * 1664525u + 1013904223u; + if (arm->out_of_range) + return ((*state >> 8) & 4u) ? (float)channel : wild[(*state >> 8) & 3u]; + return (float)((double)(*state >> 8) / 8388608.0 - 1.0); +} + +static void gdn_synthetic_tile(const pn_tt_gdn_synthetic *arm, uint32_t tile_index, + float *tile) +{ + uint32_t state = 0x1234abcdu + tile_index * 2654435761u; + uint32_t column; + uint32_t index; + + memset(tile, 0, PN_TT_GDN_TILE_BYTES); + for (column = 0; column < PN_TT_GDN_TILE_DIM; column++) { + const uint32_t at = (column < 16u) ? column : (256u + column - 16u); + + tile[at] = gdn_synthetic_value(arm, &state, + tile_index * PN_TT_GDN_TILE_DIM + column); + } + if (!arm->full) + return; + /* Every element that is NOT logical row 0, from the SAME generator the live row used — + * so `full_out_of_range` really is 992 pathological operands a tile and not 992 benign + * ones wearing the arm's name. */ + for (index = 0; index < PN_TT_GDN_TILE_DIM * PN_TT_GDN_TILE_DIM; index++) { + const bool live = (index < 16u) || (index >= 256u && index < 272u); + + if (live) + continue; + tile[index] = gdn_synthetic_value(arm, &state, index); + } +} + +/** + * Overwrite the qkv stream in place, before the CONV descriptor reads it. + * + * READ BACK AND SAY SO, ONCE. An arm that silently did not fire reads exactly like an arm + * that fired and cost nothing — the whole point of these three — so the first descriptor of + * a run echoes tile 0 out of card DRAM and prints the live row AND the pad, and the control + * prints `arm=none` beside the real activations. A reader can then see WHICH stream produced + * the microsecond figure rather than trusting the environment. + * + * Writes only into `gdn_stream_in`, a buffer this file allocated and this family reads every + * token, through the ordinary bulk-write path. No new endpoint, no window aim, no tile. + */ +static int gdn_conv_synthetic(pn_tt_device *device, const pn_tt_gdn_synthetic *arm) +{ + const uint32_t tiles = device->gdn_shape.conv_dim / PN_TT_GDN_TILE_DIM; + float *tile = malloc(PN_TT_GDN_TILE_BYTES); + uint32_t index; + int rc = 0; + + if (!tile) + return -1; + /* THE WHOLE TILE IS WRITTEN IN EVERY ARM, INCLUDING THE ROW-0 ONES. Writing only the two + * 64 B row-0 pieces would leave the pad holding the previous descriptor's values, so + * `row0_uniform` and `full_uniform` would differ in the number of host writes AND in + * whether the pad was defined — two variables where the arm is asking about one. Same + * bytes, same call count, same descriptor; only the CONTENT of the pad moves. */ + for (index = 0; index < tiles && rc == 0; index++) { + gdn_synthetic_tile(arm, index, tile); + rc = pn_tt_write_dram(device, device->gdn_stream_in.bank, + device->gdn_stream_in.address + + (size_t)index * PN_TT_GDN_TILE_BYTES, + tile, PN_TT_GDN_TILE_BYTES); + } + free(tile); + return rc; +} + +/** Echo tile 0 of the stream the CONV descriptor is about to read, live row and pad. */ +static void gdn_conv_stream_echo(pn_tt_device *device, const char *arm) +{ + float tile[PN_TT_GDN_TILE_DIM * PN_TT_GDN_TILE_DIM]; + uint32_t pad_nonzero = 0; + uint32_t denormals = 0; + uint32_t huge = 0; + double smallest = 0.0; + double largest = 0.0; + uint32_t column; + uint32_t index; + + if (device->gdn_synthetic_reported) + return; + device->gdn_synthetic_reported = true; + if (pn_tt_read_dram(device, device->gdn_stream_in.bank, device->gdn_stream_in.address, + tile, sizeof(tile)) != 0) { + fprintf(stderr, "pn_tt: gdn CONV STREAM arm=%s — readback FAILED\n", + arm ? arm : "none"); + return; + } + /* Face-ordered: row 0 is the first 16 of face 0 and the first 16 of face 1. */ + for (index = 0; index < PN_TT_GDN_TILE_DIM * PN_TT_GDN_TILE_DIM; index++) { + const uint32_t face = index / 256u; + const uint32_t within = index % 256u; + + if (face < 2u && within < 16u) + continue; + if (tile[index] != 0.0f) + pad_nonzero++; + } + /* THE RANGE OF THE LIVE ROW, ASKED OF THE EXPONENT BITS. Four sample values cannot say + * whether an out-of-range arm's +-1e30 and +-1e-40 reached the card at all — the first + * four of the ramp branch are 0 1 2 3 and read exactly like a benign stream. A DENORMAL + * is what the SFPU hypothesis is about, so it is counted rather than inferred. */ + for (column = 0; column < PN_TT_GDN_TILE_DIM; column++) { + const uint32_t at = (column < 16u) ? column : (256u + column - 16u); + const double magnitude = tile[at] < 0.0f ? -(double)tile[at] : (double)tile[at]; + uint32_t bits; + + memcpy(&bits, &tile[at], sizeof(bits)); + if (((bits >> 23) & 0xFFu) == 0u && (bits & 0x7FFFFFu) != 0u) + denormals++; + if (((bits >> 23) & 0xFFu) >= 0xC0u) + huge++; + if (magnitude > largest) + largest = magnitude; + if (magnitude != 0.0 && (smallest == 0.0 || magnitude < smallest)) + smallest = magnitude; + } + fprintf(stderr, "pn_tt: gdn CONV STREAM arm=%s — tile 0 row 0 [0..3] %g %g %g %g; " + "live |x| in [%g, %g], %u denormals, %u over 2^65; " + "pad %u of 992 non-zero; pad[16..18] %g %g %g\n", + arm ? arm : "none", (double)tile[0], (double)tile[1], (double)tile[2], + (double)tile[3], smallest, largest, denormals, huge, pad_nonzero, + (double)tile[16], (double)tile[17], (double)tile[18]); +} + +/** The PHASE CONV descriptor, whatever put the qkv stream where it is. */ +static int gdn_conv_descriptor(pn_tt_device *device, uint32_t layer, + ft_tt_op_gdn_request *out_request) +{ + const pn_tt_gdn_region *region = &device->gdn_region[layer]; + const pn_tt_gdn_synthetic arm = gdn_synthetic_arm(); + ft_tt_op_gdn_request request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + + if (arm.name && gdn_conv_synthetic(device, &arm) != 0) + return -1; + gdn_conv_stream_echo(device, arm.name); + /* THE STRIP INVARIANT, ON THE SHIPPED ON-CARD ROUTE, AT MORE THAN ONE LAYER AND MORE + * THAN ONE TOKEN. + * + * `gdn_self_check_out()` above runs only on the HOST-STAGED path, where this file wrote + * the stream itself and rows 1-31 are zero because it left them zero. The shipped route + * is different in exactly the way that matters: `render_widen` converts WHOLE TILES from + * bf16, so rows 1-31 of `gdn_stream_in` are whatever the matmul left in `ssm_qkv` — not + * anything this file controls. Every proposal to skip work on the pad rows depends on + * them being an exact zero bit pattern THERE, so it is measured there. Three layers and + * four tokens: layer 0 of token 0 is the only CONV in a run never preceded by a DELTA, + * and a defect that only appears once the window has rolled is invisible in token 0. */ + if (getenv("PN_TT_GDN_SELFCHECK") && device->gdn_strip_checks < 12u && + (layer == 0u || layer == 11u || layer == 23u)) { + char label[64]; + + device->gdn_strip_checks++; + snprintf(label, sizeof(label), "STRIP stream_in L%u #%u", layer, + device->gdn_strip_checks); + gdn_self_check_out(device, &device->gdn_stream_in, 0u, label); + } + + request.activation_in = gdn_operand(device, &device->gdn_stream_in); + request.activation_out = gdn_operand(device, &device->gdn_stream_out); + request.conv_weight = gdn_operand(device, ®ion->weight); + request.state_conv = gdn_operand(device, ®ion->window); + request.row = device->gdn_row; + if (gdn_state_out_buffer(device, layer, true)) + request.state_out = gdn_operand(device, gdn_state_out_buffer(device, layer, true)); + /* The CONV half of the work knob. `PN_TT_GDN_CONV_DIM=D` convolves the first D channels + * instead of all 8,192 — fewer tile columns per tap, fewer output tiles, every address + * still inside the buffers this layer owns because `ftg_conv_plane_tile()` is + * `tap * conv_tile_cols + column` and both shrink together. A stopwatch, not a gate: the + * stream past D is left holding the previous token's values. */ + { + const uint32_t conv_dim = gdn_probe_knob("PN_TT_GDN_CONV_DIM"); + + if (conv_dim >= PN_TT_GDN_TILE_DIM && conv_dim < request.conv_dim && + (conv_dim % PN_TT_GDN_TILE_DIM) == 0u) + request.conv_dim = conv_dim; + } + *out_request = request; + return gdn_submit(device, &request); +} + +int pn_tt_gdn_conv_oncard(pn_tt_device *device, uint32_t layer) +{ + ft_tt_op_gdn_request request; + + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count) + return -1; + return gdn_conv_descriptor(device, layer, &request); +} + +int pn_tt_gdn_conv(pn_tt_device *device, uint32_t layer, const float *qkv, float *conv_out) +{ + const uint32_t tiles = device->gdn_shape.conv_dim / PN_TT_GDN_TILE_DIM; + ft_tt_op_gdn_request request; + pn_tt_gdn_region *region; + + if (!pn_tt_gdn_is_armed(device) || !qkv || !conv_out || + layer >= device->gdn_shape.layer_count) + return -1; + region = &device->gdn_region[layer]; + + /* The cast is the const the row-0 mover cannot express in one signature; the write arm + * never touches the buffer. */ + if (gdn_row0_move(device, &device->gdn_stream_in, tiles, (float *)(uintptr_t)qkv, false) != 0) + return -1; + if (!device->gdn_self_checked && getenv("PN_TT_GDN_SELFCHECK")) { + device->gdn_self_checked = true; + gdn_self_check(device, qkv, device->gdn_weight_oracle, ®ion->weight); + } + + if (gdn_conv_descriptor(device, layer, &request) != 0) + return -1; + /* WHAT RAN BEFORE THIS DESCRIPTOR, as a knob. The newest window plane is correct at + * SSM layer 0 of position 0 and wrong at every other CONV in the run, and the one thing + * that distinguishes that descriptor is that it is the first this residency ever ran — + * every other CONV follows the previous layer's PHASE DELTA. `PN_TT_GDN_DOUBLE_CONV=1` + * issues a SECOND, identical CONV so the phase that precedes one is a CONV instead. It + * rolls the window a second time and therefore breaks the recurrence, which is why it is + * a probe and not a fallback; `window_new[taps-2] = qkv` is unchanged by the repeat, so + * the newest plane is still scoreable against the same qkv. */ + if (getenv("PN_TT_GDN_DOUBLE_CONV") && gdn_submit(device, &request) != 0) + return -1; + /* The injective-stream map, on the FIRST layer that follows a PHASE DELTA and on that one + * only — layer 0 of position 0 is the descriptor that already works. */ + if (layer == 1u && !device->gdn_ramp_probed && getenv("PN_TT_GDN_RAMP")) { + device->gdn_ramp_probed = true; + gdn_ramp_probe(device, layer, &request); + } + /* THE FIRST DRAM ACCESS AFTER THE DESCRIPTOR RETIRES, before any other probe. Every other + * readback in this file runs `pn_tt_matvec_flush()` first, so a plane read after one of + * them cannot separate "the descriptor wrote it wrong" from "something the host did next + * wrote over it". This one can: nothing runs between the retire and this read. + * + * **EVERY LAYER, not only layer 0, and that is the point.** The defect reads as + * "token 0 is clean and token 1 is not", but the ONE thing that distinguishes layer 0 of + * token 0 from every other CONV in the run is that it is the FIRST DESCRIPTOR THIS + * RESIDENCY EVER RAN — every other CONV is preceded by the previous layer's PHASE DELTA. + * A per-layer figure at token 0 separates a token-carried defect from a phase-carried + * one, and the two have nothing in common. */ + if (device->gdn_window_checks < 3u && getenv("PN_TT_GDN_SELFCHECK")) { + char label[64]; + + snprintf(label, sizeof(label), "newest = qkv, layer %u", layer); + gdn_self_check_plane(device, ®ion->window, + (device->gdn_shape.conv_taps - 2u) * tiles, qkv, label); + } + if (device->gdn_self_checked && !device->gdn_out_checked && getenv("PN_TT_GDN_SELFCHECK")) { + const uint32_t last_plane = (device->gdn_shape.conv_taps - 2u) * tiles; + + device->gdn_out_checked = true; + gdn_self_check_out(device, &device->gdn_stream_out, 0u, "conv_out"); + gdn_self_check_out(device, ®ion->window, last_plane, "window newest plane"); + /* PASS B's newest window plane IS the qkv stream, copied tile for tile through + * unpack -> datacopy -> pack. It is the ONLY thing in this descriptor whose right + * answer is known without doing any arithmetic, so it separates a broken Float32 + * DATAPATH from broken Float32 ARITHMETIC. */ + gdn_self_check_plane(device, ®ion->window, last_plane, qkv, "window newest = qkv"); + gdn_self_check_arithmetic(device, qkv, device->gdn_weight_oracle); + } + /* Every layer's qkv, kept so a wrong tensor coming back can be NAMED. Written before the + * layer-0 check so the history is the PREVIOUS token's for every layer but this one. */ + if (getenv("PN_TT_GDN_SELFCHECK") && device->gdn_window_checks < 3u) { + if (!device->gdn_qkv_history) + device->gdn_qkv_history = calloc((size_t)device->gdn_shape.layer_count * + device->gdn_shape.conv_dim, + sizeof(float)); + if (device->gdn_qkv_history && layer != 0u) + memcpy(device->gdn_qkv_history + (size_t)layer * device->gdn_shape.conv_dim, qkv, + (size_t)device->gdn_shape.conv_dim * sizeof(float)); + } + /* SSM layer 0's window, for the first few tokens — the ROLL, which position 0 cannot see. + * + * The qkv STREAM is echoed at the same moment, because the newest window plane is a copy + * of it made at the END of the descriptor while the convolution read it at the START. If + * the stream no longer holds what the host wrote, the roll is innocent and something + * inside the descriptor is writing over its own input. */ + if (layer == 0u && device->gdn_window_checks < 3u && getenv("PN_TT_GDN_SELFCHECK")) { + gdn_self_check_plane(device, &device->gdn_stream_in, 0u, qkv, + "qkv stream AFTER the descriptor"); + gdn_self_check_window(device, ®ion->window, qkv); + } + + return gdn_row0_move(device, &device->gdn_stream_out, tiles, conv_out, true); +} + +/** Row 0 of `tiles` tiles starting at `first_tile` of `buffer`, in either direction. */ +static int gdn_row0_move_at(pn_tt_device *device, const pn_tt_gdn_buffer *buffer, + uint32_t first_tile, uint32_t tiles, float *values, bool reading) +{ + return pn_tt_fp32_row0_move(device, buffer->bank, + buffer->address + first_tile * PN_TT_GDN_TILE_BYTES, tiles, + values, reading); +} + +static int gdn_delta_descriptor(pn_tt_device *device, uint32_t layer); + +/** S2c: sixteen per-row gate blocks, CONTIGUOUS at a 64 B stride, allocated once. */ +static int gdn_gate_rows_ensure(pn_tt_device *device) +{ + const ft_tt_op_gdn_request delta = gdn_request(device, FT_TT_OP_GDN_PHASE_DELTA); + const uint32_t stride = (uint32_t)((ft_tt_op_gdn_gate_bytes(&delta) + 63u) & ~(size_t)63u); + pn_tt_gdn_buffer all; + uint32_t r; + + if (device->gdn_gate_rows == 16u) + return 0; + if (gdn_alloc_in(device, device->gdn_gate.bank, (size_t)stride * 16u, true, &all) != 0) + return -1; + for (r = 0; r < 16u; r++) { + device->gdn_gate_row[r].bank = all.bank; + device->gdn_gate_row[r].address = all.address + r * stride; + } + device->gdn_gate_stride = stride; + device->gdn_gate_rows = 16u; + return 0; +} + +int pn_tt_gdn_gate_target(pn_tt_device *device, uint32_t rows, uint32_t *bank, + uint32_t *address, uint32_t *row_stride) +{ + const pn_tt_gdn_buffer *to = &device->gdn_gate; + + if (!pn_tt_gdn_is_armed(device) || rows == 0u || rows > 16u || !bank || !address || + !row_stride) + return -1; + /* Where pn_tt_gdn_gate_upload{,_rows}() would have written: the row blocks for a deferred + * row or for several rows, else the single gate. */ + if (rows > 1u || device->gdn_defer) { + if (gdn_gate_rows_ensure(device) != 0 || (rows == 1u && device->gdn_row >= 16u)) + return -1; + to = rows > 1u ? &device->gdn_gate_row[0] : &device->gdn_gate_row[device->gdn_row]; + } + *bank = to->bank; + *address = to->address; + *row_stride = device->gdn_gate_rows ? device->gdn_gate_stride : 256u; + return 0; +} + +int pn_tt_gdn_gate_upload(pn_tt_device *device, const float *decay, const float *beta) +{ + const uint32_t heads = device->gdn_shape.v_heads; + float gate[2u * PN_TT_GDN_MAX_V_HEADS]; + uint32_t head; + + if (!pn_tt_gdn_is_armed(device) || !decay || !beta) + return -1; + /* `decay[v_heads]` then `beta[v_heads]`, flat fp32 — the one operand that is not tiles, + * and on the on-card path THE ONLY THING THAT STILL CROSSES THE LINK for this block: + * 256 B up a layer against the 49,408 B the activations used to cost. `decay = exp(g)` + * is applied by the caller because the SFPU has no `log` and therefore no `pow`. + * + * Only the live prefix is written; the rounding the reader's 64 B read needs was staged + * as zero at allocation. + * + * RED PROOF: `gdn_decay_raw` sends the oracle's `g` where the contract says `exp(g)`. + * NOTHING checks which of the two the card was handed — at position 0 the carried state + * is zero, so `(a)` multiplies zero by it and the two are INDISTINGUISHABLE. The mutation + * is therefore only observable from position 1, which is exactly what makes it worth + * having: it is the one arm that proves the decay word is consumed at all. */ + for (head = 0; head < heads; head++) { + gate[head] = pn_tt_red("gdn_decay_raw") ? logf(decay[head]) : decay[head]; + gate[heads + head] = beta[head]; + } + { + const pn_tt_gdn_buffer *to = &device->gdn_gate; + + /* S2c: a deferred row writes its own block (a queued DELTA still reads its row's). */ + if (device->gdn_defer) { + if (gdn_gate_rows_ensure(device) != 0 || device->gdn_row >= 16u) + return -1; + to = &device->gdn_gate_row[device->gdn_row]; + } + return pn_tt_write_dram(device, to->bank, to->address, gate, + (size_t)2u * heads * sizeof(gate[0])); + } +} + +int pn_tt_gdn_gate_upload_rows(pn_tt_device *device, uint32_t rows, const float *decay, + const float *beta) +{ + const uint32_t heads = device ? device->gdn_shape.v_heads : 0u; + float *block; + uint32_t r, head, stride_words; + int rc; + + if (!pn_tt_gdn_is_armed(device) || !decay || !beta || rows == 0u || rows > 16u || + gdn_gate_rows_ensure(device) != 0) + return -1; + stride_words = device->gdn_gate_stride / 4u; + block = calloc((size_t)rows * stride_words, sizeof(float)); + if (!block) + return -1; + for (r = 0; r < rows; r++) + for (head = 0; head < heads; head++) { + block[r * stride_words + head] = pn_tt_red("gdn_decay_raw") + ? logf(decay[r * heads + head]) + : decay[r * heads + head]; + block[r * stride_words + heads + head] = beta[r * heads + head]; + } + /* ONE write for every row's block (they are contiguous at the gate stride). */ + rc = pn_tt_write_dram(device, device->gdn_gate_row[0].bank, device->gdn_gate_row[0].address, + block, (size_t)rows * device->gdn_gate_stride); + free(block); + return rc; +} + +int pn_tt_gdn_delta_descriptor_only(pn_tt_device *device, uint32_t layer) +{ + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count) + return -1; + return gdn_delta_descriptor(device, layer); +} + +/** + * MULTI-ROW DELTA (PM 5): verify rows 0..rows-1 of `layer` in ONE descriptor per member, on the + * gate blocks pn_tt_gdn_gate_upload_rows() staged. Row r's state goes to snapshot slot r and the + * slots then trade places with the live state in row order — exactly the bookkeeping the + * one-row-a-descriptor loop leaves: live = after the last row, slot r = before row r. + * + * RED `gdn_rows_state_once`: every row's state goes to slot 0's buffer, so the slots the swaps + * bring live hold stale state — must DIFFER. RED `gdn_rows_row0`: see below. + */ +int pn_tt_gdn_delta_rows_oncard(pn_tt_device *device, uint32_t layer, uint32_t rows) +{ + ft_tt_op_gdn_request request; + uint32_t r; + + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count || rows < 2u || + rows > FT_TT_OP_GDN_MULTI_ROW_MAX || rows > device->gdn_snapshot_slots || + device->gdn_defer || gdn_gate_rows_ensure(device) != 0) + return -1; + request = gdn_request(device, FT_TT_OP_GDN_PHASE_DELTA); + request.activation_in = gdn_operand(device, &device->gdn_stream_out); + request.activation_out = gdn_operand(device, &device->gdn_y); + request.gate = gdn_operand(device, &device->gdn_gate_row[0]); + request.state_recurrent = gdn_operand(device, &device->gdn_region[layer].state); + request.row = 0u; + request.row_count = rows; + /* RED `gdn_rows_row0`: the reader stages row 0's q/k/v for every row — must DIFFER. */ + if (pn_tt_red("gdn_rows_row0")) + request.flags |= FT_TT_OP_GDN_FLAG_RED_ROWS_ROW0; + for (r = 0; r < rows; r++) { + const uint32_t slot = pn_tt_red("gdn_rows_state_once") ? 0u : r; + + request.state_rows_out[r] = gdn_operand( + device, &device->gdn_snapshot[slot * device->gdn_shape.layer_count + layer].state); + } + request.state_out = request.state_rows_out[0]; + if (gdn_submit(device, &request) != 0) + return -1; + for (r = 0; r < rows; r++) + if (pn_tt_gdn_layer_swap_part(device, layer, r, false, true) != 0) + return -1; + return 0; +} + +int pn_tt_gdn_conv_rows_oncard(pn_tt_device *device, uint32_t layer, uint32_t rows) +{ + const pn_tt_gdn_region *region; + ft_tt_op_gdn_request request; + uint32_t words[FT_TT_OP_GDN_WORDS]; + uint32_t r; + + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count || rows < 2u || + rows > FT_TT_OP_GDN_MULTI_ROW_MAX || rows > device->gdn_snapshot_slots || + device->gdn_defer || pn_tt_env_u32("PN_TT_GDN_CONV_STRIP", 1u) == 0u) + return -2; + region = &device->gdn_region[layer]; + request = gdn_request(device, FT_TT_OP_GDN_PHASE_CONV); + request.activation_in = gdn_operand(device, &device->gdn_stream_in); + request.activation_out = gdn_operand(device, &device->gdn_stream_out); + request.conv_weight = gdn_operand(device, ®ion->weight); + request.state_conv = gdn_operand(device, ®ion->window); + request.row = 0u; + request.row_count = rows; + request.flags |= FT_TT_OP_GDN_FLAG_CONV_STRIP; + /* RED `gdn_conv_rows_row0`: every row convolves row 0's stream — must DIFFER. */ + if (pn_tt_red("gdn_conv_rows_row0")) + request.flags |= FT_TT_OP_GDN_FLAG_RED_ROWS_ROW0; + for (r = 0; r < rows; r++) { + /* RED `gdn_conv_rows_window_once`: every row's window into slot 0 (the rollback then + * restores the wrong window) — must DIFFER. */ + const uint32_t slot = pn_tt_red("gdn_conv_rows_window_once") ? 0u : r; + + request.state_rows_out[r] = gdn_operand( + device, &device->gdn_snapshot[slot * device->gdn_shape.layer_count + layer].window); + } + request.state_out = request.state_rows_out[0]; + /* Refused by the encoder (a shape the family does not run): nothing ran. */ + if (ft_tt_op_gdn_parameters(&request, words) != FT_TT_OK) + return -2; + if (gdn_submit(device, &request) != 0) + return -1; + for (r = 0; r < rows; r++) + if (pn_tt_gdn_layer_swap_part(device, layer, r, true, false) != 0) + return -1; + return 0; +} + +/** + * `q | k | v` and the flat gate block, into the two DRAM buffers the DELTA reader takes. + * + * THE THREE STREAMS ARE ROW-0 STRIPS, so this is three `pn_tt_fp32_row0_move()` writes and no + * tilize buffer at all. It used to build three `[heads, state]` matrices in host scratch and + * push the whole thing as one bulk write; the strip geometry is what lets PHASE CONV's output + * BE this buffer, so on the on-card path none of these three writes happens at all. + */ +static int gdn_stage_delta_input(pn_tt_device *device, const float *q, const float *k, + const float *v, const float *decay, const float *beta) +{ + const ftg_shape shape = gdn_shape(device, FT_TT_OP_GDN_PHASE_DELTA); + /* RED PROOF: q and k swapped. Same shape, same length — (b) and (d) then use q where the + * oracle uses k and (e) uses k where it uses q, which no shape check can see. */ + const float *first_stream = pn_tt_red("gdn_qk_swap") ? k : q; + const float *second_stream = pn_tt_red("gdn_qk_swap") ? q : k; + + if (gdn_row0_move_at(device, &device->gdn_stream_out, + ftg_delta_stream_base(&shape, (uint32_t)kGdnStreamQ), + ftg_delta_stream_tiles(&shape, (uint32_t)kGdnStreamQ), + (float *)(uintptr_t)first_stream, false) != 0 || + gdn_row0_move_at(device, &device->gdn_stream_out, + ftg_delta_stream_base(&shape, (uint32_t)kGdnStreamK), + ftg_delta_stream_tiles(&shape, (uint32_t)kGdnStreamK), + (float *)(uintptr_t)second_stream, false) != 0 || + gdn_row0_move_at(device, &device->gdn_stream_out, + ftg_delta_stream_base(&shape, (uint32_t)kGdnStreamV), + ftg_delta_stream_tiles(&shape, (uint32_t)kGdnStreamV), + (float *)(uintptr_t)v, false) != 0) + return -1; + + return pn_tt_gdn_gate_upload(device, decay, beta); +} + +/** + * SCORE PHASE DELTA AGAINST ITS CLOSED FORM, at the one position where it has one. + * + * At position 0 the carried state is identically zero, so (a) decays nothing, (b)'s `pred` is + * zero, (c) is `delta_j = v_j * beta`, (d) writes `S[j][i] = k[i]*delta_j` and (e) reads it + * back as `y_j = scale * delta_j * sum_i k[i]*q[i]`. The whole of the four passes collapses to + * + * y[h][j] = scale * beta[h] * v[h][j] * (k[kh] . q[kh]), kh = h % k_heads + * + * and every term on the right is an operand this function was handed. So a disagreement is + * localised to a HEAD rather than to a tensor, which is what a per-tensor max-norm figure + * cannot do: the failure this op actually had was per-head, and it was invisible in a number + * that maximised over all 32 of them. + * + * The dot product is reported for head 0 beside the ratio, because `k . q` is the single + * scalar the whole head's answer is proportional to — a card that has the right `v` and the + * wrong dot is a reduce or a staging fault, and a card whose `y` is proportional to the right + * answer is a scale fault. + */ +static void gdn_self_check_delta(pn_tt_device *device, uint32_t layer, const float *q, + const float *k, const float *v, const float *beta, + const float *y) +{ + const uint32_t state = device->gdn_shape.state_size; + const uint32_t v_heads = device->gdn_shape.v_heads; + const uint32_t k_heads = device->gdn_shape.k_heads; + const double scale = 1.0 / sqrt((double)state); + /* The per-head detail once, then one line a layer: 24 layers x 32 heads is 768 lines of + * detail nobody reads, and the question the later layers answer is whether the error is + * the OP's or one layer's data. */ + const bool detail = device->gdn_delta_checks == 0u; + double worst_all = 0.0; + double ratio_sum = 0.0; + uint32_t ratio_count = 0; + uint32_t bad_heads = 0; + uint32_t head; + + if (detail) + fprintf(stderr, "pn_tt: gdn SELFCHECK DELTA closed form at position 0 — " + "y[h][j] = scale * beta[h] * v[h][j] * (k.q), scale %.6g\n", scale); + for (head = 0; head < v_heads; head++) { + const float *qh = q + (size_t)(head % k_heads) * state; + const float *kh = k + (size_t)(head % k_heads) * state; + const float *vh = v + (size_t)head * state; + const float *yh = y + (size_t)head * state; + double dot = 0.0; + double reference = 0.0; + double worst = 0.0; + uint32_t index; + + for (index = 0; index < state; index++) + dot += (double)kh[index] * (double)qh[index]; + for (index = 0; index < state; index++) { + const double want = scale * (double)beta[head] * (double)vh[index] * dot; + const double gap = fabs((double)yh[index] - want); + + if (fabs(want) > reference) + reference = fabs(want); + if (gap > worst) + worst = gap; + } + worst = reference > 0.0 ? worst / reference : (worst > 0.0 ? 1.0 : 0.0); + if (worst > worst_all) + worst_all = worst; + if (worst > 1.0e-3) + bad_heads++; + { + /* THE SIGN OF THE ERROR, not just its size. A Tf32 unpack TRUNCATES, so every + * multiply stage biases the magnitude DOWN by a fixed fraction and the ratio + * lands below 1 on every head — which is a different finding from rounding noise + * and points at the stage COUNT rather than at any one stage. */ + const double want = scale * (double)beta[head] * (double)vh[0] * dot; + + if (want != 0.0) { + ratio_sum += (double)yh[0] / want; + ratio_count++; + } + if (detail) + fprintf(stderr, "pn_tt: gdn SELFCHECK DELTA head %2u rel %.3e k.q %+.6g " + "beta %.6g want[0] %+.6g card[0] %+.6g\n", + head, worst, dot, (double)beta[head], want, (double)yh[0]); + } + } + fprintf(stderr, "pn_tt: gdn SELFCHECK DELTA layer %2u — %u of %u heads over 1e-3, worst " + "%.3e, mean card/want %.6f\n", + layer, bad_heads, v_heads, worst_all, + ratio_count ? ratio_sum / ratio_count : 0.0); +} + +/** The PHASE DELTA descriptor, whatever put q|k|v and the gate block where they are. */ +static int gdn_delta_descriptor(pn_tt_device *device, uint32_t layer) +{ + ft_tt_op_gdn_request request; + + request = gdn_request(device, FT_TT_OP_GDN_PHASE_DELTA); + request.activation_in = gdn_operand(device, &device->gdn_stream_out); + request.activation_out = gdn_operand(device, &device->gdn_y); + request.gate = gdn_operand(device, device->gdn_defer && device->gdn_row < device->gdn_gate_rows + ? &device->gdn_gate_row[device->gdn_row] + : &device->gdn_gate); + request.state_recurrent = gdn_operand(device, &device->gdn_region[layer].state); + request.row = device->gdn_row; + if (gdn_state_out_buffer(device, layer, false)) + request.state_out = gdn_operand(device, gdn_state_out_buffer(device, layer, false)); + /* THE WORK KNOB. `PN_TT_GDN_HEADS=N` runs the descriptor over the first N v-heads instead + * of all 32 — the compute kernel's outer loop and nothing else — so the descriptor's time + * against N says whether that time is the card's arithmetic or a fixed cost around it. + * + * THE TRAFFIC IS THE SAME SHAPE AT EVERY N, WHICH IS WHAT MAKES THIS A STOPWATCH — but + * since the streams became head-major row-0 strips the ADDRESSES move with N as well: + * `ftg_delta_stream_base()` is `k_heads * state_tiles`, so a short run reads q|k|v out of + * a shorter strip. Same tiles per head, same reads per head, different values. K-HEADS + * MOVES WITH IT because the family refuses `v_heads % k_heads != 0`. It was a + * byte-identical layout under the old dense `[heads, S]` packing; it is not now, and this + * arm was never a gate. */ + { + const uint32_t heads = gdn_probe_knob("PN_TT_GDN_HEADS"); + + if (heads > 0u && heads < request.v_heads) { + request.v_heads = heads; + request.k_heads = heads; + } + } + /* BEFORE the descriptor, at the first DELTA of the run, where the carried state it + * borrows is still the zero the sequence starts from. Does nothing unless asked. */ + pn_tt_gdn_core_scaling_probe(device, &request); + return gdn_submit(device, &request); +} + +int pn_tt_gdn_delta_oncard(pn_tt_device *device, uint32_t layer, const float *decay, + const float *beta) +{ + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count) + return -1; + /* The gate block is the ONLY operand this path uploads — 256 B a layer. q, k and v are + * already in `gdn_stream_out`: PHASE CONV wrote them there and the per-head L2 norm wrote + * its answer back over the first `2*k_heads*state_tiles` tiles of the same buffer. */ + if (pn_tt_gdn_gate_upload(device, decay, beta) != 0) + return -1; + return gdn_delta_descriptor(device, layer); +} + +int pn_tt_gdn_delta(pn_tt_device *device, uint32_t layer, const float *q, const float *k, + const float *v, const float *decay, const float *beta, float *y) +{ + const ftg_shape shape = gdn_shape(device, FT_TT_OP_GDN_PHASE_DELTA); + + if (!pn_tt_gdn_is_armed(device) || !q || !k || !v || !decay || !beta || !y || + layer >= device->gdn_shape.layer_count) + return -1; + if (gdn_stage_delta_input(device, q, k, v, decay, beta) != 0) + return -1; + if (gdn_delta_descriptor(device, layer) != 0) + return -1; + + /* y is a ROW-0 STRIP, head-major, so it comes back as row 0 of `v_heads * state_tiles` + * tiles — the same 16,384 B the [v_heads, S] matrix cost, in the same number of 64-byte + * transactions, because a whole-tile read of the dense form moved exactly the rows the + * strip skips. On the on-card path this read does not happen at all. */ + if (gdn_row0_move_at(device, &device->gdn_y, 0u, + shape.v_heads * shape.state_tiles, y, true) != 0) + return -1; + /* The FIRST TOKEN only: the closed form is a statement about a ZERO carried state, and + * every descriptor after the first token's has one this function cannot see. */ + if (device->gdn_delta_checks < device->gdn_shape.layer_count && + getenv("PN_TT_GDN_SELFCHECK")) { + gdn_self_check_delta(device, layer, q, k, v, beta, y); + device->gdn_delta_checks++; + } + return 0; +} + +/* ================================================================ + * The on-card chain's operands + * + * The strip bridge, the two per-head reduces and the elementwise gate all address these three + * buffers directly, and none of them knows what a `pn_tt_gdn_buffer` is. Handing out the + * `ft_tt_op_operand` rather than the buffer is what keeps that boundary: every one of them is + * an fp32 4096-byte-page endpoint and nothing else about this family reaches them. + * ================================================================ */ + +ft_tt_op_operand pn_tt_gdn_stream_in_operand(const pn_tt_device *device) +{ + return gdn_operand(device, &device->gdn_stream_in); +} + +ft_tt_op_operand pn_tt_gdn_stream_operand(const pn_tt_device *device) +{ + return gdn_operand(device, &device->gdn_stream_out); +} + +ft_tt_op_operand pn_tt_gdn_y_operand(const pn_tt_device *device) +{ + return gdn_operand(device, &device->gdn_y); +} + +int pn_tt_gdn_stream_read(pn_tt_device *device, uint32_t first_tile, uint32_t tiles, + float *out) +{ + if (!pn_tt_gdn_is_armed(device) || !out || tiles == 0u || + first_tile + tiles > device->gdn_shape.conv_dim / PN_TT_GDN_TILE_DIM) + return -1; + return gdn_row0_move_at(device, &device->gdn_stream_out, first_tile, tiles, out, true); +} + +/* ================================================================ + * The phase-isolation seam. See pn_tt_gdn.h for why it exists. + * ================================================================ */ + +/** The buffer and its tile count, or NULL for an unarmed family or an unknown strip. */ +static const pn_tt_gdn_buffer *gdn_strip_buffer(const pn_tt_device *device, + pn_tt_gdn_strip_id strip, uint32_t *out_tiles) +{ + switch (strip) { + case PN_TT_GDN_STRIP_STREAM_IN: + *out_tiles = pn_tt_gdn_stream_tiles(device); + return &device->gdn_stream_in; + case PN_TT_GDN_STRIP_STREAM_OUT: + *out_tiles = pn_tt_gdn_stream_tiles(device); + return &device->gdn_stream_out; + case PN_TT_GDN_STRIP_Y: + *out_tiles = pn_tt_gdn_value_tiles(device); + return &device->gdn_y; + } + return NULL; +} + +int pn_tt_gdn_strip_row_move(pn_tt_device *device, pn_tt_gdn_strip_id strip, uint32_t row, + uint32_t tiles, float *values, bool reading) +{ + uint32_t capacity = 0u; + const pn_tt_gdn_buffer *buffer; + + if (!pn_tt_gdn_is_armed(device) || !values || tiles == 0u) + return -1; + buffer = gdn_strip_buffer(device, strip, &capacity); + if (!buffer || tiles > capacity) + return -1; + return pn_tt_fp32_row_move(device, buffer->bank, buffer->address, row, tiles, values, + reading); +} + +int pn_tt_gdn_strip_zero(pn_tt_device *device, pn_tt_gdn_strip_id strip) +{ + uint32_t tiles = 0u; + const pn_tt_gdn_buffer *buffer; + + if (!pn_tt_gdn_is_armed(device)) + return -1; + buffer = gdn_strip_buffer(device, strip, &tiles); + if (!buffer || tiles == 0u) + return -1; + return pn_tt_zero_dram(device, buffer->bank, buffer->address, + (size_t)tiles * PN_TT_GDN_TILE_BYTES); +} + +uint32_t pn_tt_gdn_stream_tiles(const pn_tt_device *device) +{ + return device ? device->gdn_shape.conv_dim / PN_TT_GDN_TILE_DIM : 0u; +} + +uint32_t pn_tt_gdn_key_tiles(const pn_tt_device *device) +{ + if (!device) + return 0u; + return 2u * device->gdn_shape.k_heads * (device->gdn_shape.state_size / PN_TT_GDN_TILE_DIM); +} + +uint32_t pn_tt_gdn_value_tiles(const pn_tt_device *device) +{ + if (!device) + return 0u; + return device->gdn_shape.v_heads * (device->gdn_shape.state_size / PN_TT_GDN_TILE_DIM); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gdn.h b/src/accel/tt/pn_tt_gdn.h new file mode 100644 index 0000000..52d2509 --- /dev/null +++ b/src/accel/tt/pn_tt_gdn.h @@ -0,0 +1,319 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn.h + * @brief The GATED-DELTA-NET block on the card: one resident core running + * `foundry/tt/ft_tt_op_gdn.h`'s two phases over per-layer state that lives in card + * DRAM and is never downloaded. + * + * Model-agnostic on purpose. It knows S, the head counts, the conv width and the tap + * count; it does not know what a Qwen3.5 layer is. `src/models/qwen35moe_gdn_tt.c` + * is the caller and owns the host arithmetic this op deliberately does not do. + * + * --------------------------------------------------------------------------- + * WHAT THIS OWNS AND WHAT IT DOES NOT + * --------------------------------------------------------------------------- + * The op is TWO descriptors a layer — `conv1d + SiLU` with the rolling window, then the + * delta rule over every v-head — and between them sit two per-head NORMS that are NOT in it: + * the q/k L2 norm, and the output gated norm. Those are ordinary reduces at a SECOND width + * (`S/32` = 4 tile columns against the model's `n_embd/32` = 128) and they live on the narrow + * `reduce_fp32` residency `pn_tt_head_norm.h` places. + * + * **THERE ARE TWO CALLERS AND THEY ARE NOT THE SAME ARM.** + * + * HOST-MEDIATED `pn_tt_gdn_conv()` / `pn_tt_gdn_delta()` take and return host float + * arrays, so the two norms between them run on the CPU. That arm is SLOWER + * than the host recurrence and it is arithmetic, not a defect: the host + * still pulls the 49,408 B of projections AND now pulls `conv_out` (32,768 B) + * and `y` (16,384 B) a layer. It is the CORRECTNESS vehicle — the thing the + * on-card gate scores against the CPU oracle — and nothing else. + * THE STRIP `pn_tt_gdn_conv_oncard()` / `pn_tt_gdn_delta_oncard()` take nothing and + * BRIDGE return nothing: every operand is already in card DRAM, the norms run on + * the card between them, and the only bytes that cross are `alpha`/`beta` + * down and 256 B of gate block up. `qwen35moe_gdn_strip_tt.c` is the caller + * and readback goes 1478.09 -> 326.09 KiB/token. + * + * --------------------------------------------------------------------------- + * WHAT LIVES ON THE CARD, PER SSM LAYER, AND WHAT IT COSTS + * --------------------------------------------------------------------------- + * Every size comes from `ft_tt_op_gdn_*_bytes()` so the capacity figure and the allocation + * are one expression. At Ornith's shape (S=128, 32 v-heads, 16 k-heads, conv_dim 8192, + * 4 taps): + * + * the recurrent state 2 MiB [v_heads, S, S] fp32, oracle-transposed, READ AND WRITTEN + * the conv window 3 MiB taps-1 TAP-MAJOR PLANES of conv_dim/32 tiles, R/W + * the conv weight 4 MiB taps planes, a CONSTANT staged once, TRANSPOSED at load + * ----- + * 9 MiB a layer, 216 MiB over 24 SSM layers + * + * The 50.25 MiB the capacity plan reserves counted the window as `conv_dim * (taps-1)` fp32 + * ELEMENTS. A plane is a one-row tilized strip and the tile quantum is 32 rows, so the window + * is 32x that; carried state alone is 120 MiB. See `ft_tt_op_gdn.h`. + * + * Plus ONE shared set of per-token staging buffers — the qkv stream in, the stream out (which + * is CONV's output AND DELTA's input, one buffer since both are the same row-0 strip), the + * gate block and `y` — 2.6 MiB total, because a token runs one layer at a time and nothing + * carries between them. + * + * @status SHIPS ON. On silicon (p150a, 2026-08-21) the strip-bridge arm is `cmp` IDENTICAL + * to the 48-token oracle at **326.09 KiB/token and 3.79 tok/s**, against the control's + * 1478.09 and 2.20; the host-mediated arm beside it is IDENTICAL too, at 2630.09 and + * 1.43. `PN_TT_ONCARD=all` selects the family now — but the block WITHOUT the bridge + * is a wall regression, so a DEFAULT arm that loses the bridge gives the recurrence + * back to the host and a NAMED one keeps it. + * @issues **EVERY NUMBER MEASURED ON A CONTESTED CORE IS VOID.** Core 75 is a matmul group + * member at `PN_TT_MATMUL_CORES=4` and members run upward from it, so the contested + * set MOVES with the count. While two residencies were retiring each other the same + * CONV descriptor scored 1.0e-2 max-norm with 1957 of 8192 channels off; on an + * uncontested core it is 6.872e-04 with 3. A contested core does not fail, it + * computes badly, and both arms log a healthy "residency up". + * + * The carried state is reset when the forward is asked for position 0. A caller that + * starts mid-sequence from a prefix cache would inherit the previous sequence's + * state; the host arm has the same shape of problem and solves it by owning the + * state per sequence, which this cannot while there is one region set per model. + * @todo + * - [ ] An all-Float32 elementwise program, so the strip bridge's `silu(z) *` runs in fp32 + * and only the final `gated` is narrowed. Modelled at 1.909e-03 against 1.023e-02. + */ + +#ifndef PN_TT_GDN_H +#define PN_TT_GDN_H + +#ifdef USE_TT + +#include + +#include "pn_tt_device.h" +#include "tt/ft_tt_op_dispatch.h" + +/** The shape one model's GDN block has. Every field is read off the model's config, never + * assumed — `k_heads` must divide `v_heads` because the GQA repeat is `vh % k_heads`. */ +typedef struct pn_tt_gdn_shape { + uint32_t state_size; /**< S; head_k_dim == head_v_dim, a multiple of 32 */ + uint32_t v_heads; + uint32_t k_heads; + uint32_t conv_dim; /**< 2*key_dim + val_dim, a multiple of 32 */ + uint32_t conv_taps; /**< ssm_conv; the window is taps-1 deep */ + uint32_t layer_count; /**< SSM layers, each with its own carried state */ +} pn_tt_gdn_shape; + +/** + * Arm the GDN residency (one more core) and allocate every layer's carried state. + * + * Never fatal on its own — a card without the `gdn` program leaves the recurrence on the + * host — but `PN_TT_ONCARD=gdn` makes a MISSING program a refusal, exactly like the other + * families (pn_tt_device_arm_oncard). + * + * The conv WEIGHT is allocated here and left uninitialised: it is a per-layer constant and + * the caller uploads it with `pn_tt_gdn_conv_weight_upload()` once, at load. + * + * @return 0 (even when nothing armed), -1 on a bad argument + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_gdn(pn_tt_device *device, const pn_tt_gdn_shape *shape); + +/** Did the GDN family actually arm for this run? */ +bool pn_tt_gdn_is_armed(const pn_tt_device *device); + +/** + * TRANSPOSE one layer's conv1d weight into tap-major planes and upload it. Once, at load. + * + * `weight` is the oracle's own `layer->ssm_conv1d`: ROW-MAJOR `[conv_dim, taps]`, i.e. + * `w[c*taps + t]`. The card wants `taps` PLANES, plane `t` holding `w[.][t]` laid out exactly + * like the qkv stream. **That is a transpose and not a reshape** — a reshape gives a wrong + * answer with no error anywhere, which is why this is a named function taking the oracle's + * order rather than a memcpy at the call site. + * + * @param layer 0..layer_count-1, the SSM-layer ordinal the two calls below also take + * @param weight `conv_dim * taps` floats in the oracle's order + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdn_conv_weight_upload(pn_tt_device *device, uint32_t layer, const float *weight); + +/** Zero every layer's carried state — the conv window and the recurrent state. The start of + * a sequence, and the state this op is armed with. */ +int pn_tt_gdn_state_reset(pn_tt_device *device); + +/** STEP 4 S2 — the verify's per-row states, copy-free. Reserve `slots` snapshot slots (every + * layer's window + state, on the live region's bank); route the decode CONV/DELTA writes of + * the state to slot `slot` (-1: back in place); trade layer `layer`'s live window + state with + * slot `slot`'s. @return 0 or -1 */ +int pn_tt_gdn_snapshot_reserve(pn_tt_device *device, uint32_t slots); +int pn_tt_gdn_state_out(pn_tt_device *device, int slot); +int pn_tt_gdn_layer_swap(pn_tt_device *device, uint32_t layer, uint32_t slot); +int pn_tt_gdn_layer_swap_part(pn_tt_device *device, uint32_t layer, uint32_t slot, bool window, + bool state); +/** S2c: the row select for the next CONV / DELTA descriptors (0 = the decode row-0 walk). */ +int pn_tt_gdn_set_row(pn_tt_device *device, uint32_t row); +/** S2c: queue split CONV/DELTA descriptors without awaiting (per-row gate blocks); await them + * all with pn_tt_gdn_await_deferred() before anything reads their output. */ +int pn_tt_gdn_defer(pn_tt_device *device, bool defer); +int pn_tt_gdn_await_deferred(pn_tt_device *device); +/** S2d: every row's gate block (decay[rows][H], beta[rows][H]) in ONE write, then DELTA + * descriptors that use the staged blocks (no per-row upload). */ +int pn_tt_gdn_gate_upload_rows(pn_tt_device *device, uint32_t rows, const float *decay, + const float *beta); +int pn_tt_gdn_delta_descriptor_only(pn_tt_device *device, uint32_t layer); +/** MULTI-ROW DELTA: verify rows 0..rows-1 in one descriptor per member (see pn_tt_gdn.c). */ +int pn_tt_gdn_delta_rows_oncard(pn_tt_device *device, uint32_t layer, uint32_t rows); + +/** PM 6 MULTI-ROW CONV: the CONV of verify rows [0, rows) in ONE descriptor per member (strip + * mode): row r reads the window its one-row descriptor would have, writes its output to row r + * and its rolled window to snapshot slot r; then the window swaps in row order (live = after + * the last row, slot r = before row r). -2 when refused before anything ran (the caller may + * take the per-row walk), -1 on a card failure. */ +int pn_tt_gdn_conv_rows_oncard(pn_tt_device *device, uint32_t layer, uint32_t rows); + +/** + * The per-phase clean/replayed figures on stderr, ONCE, at release. + * + * NOT a duplicate of the `qwen35_tt: gdn phase` report line. That one runs under + * `tokens % PN_TT_REPORT == 0`, so an arm that CORRUPTS the model's output — which every + * data arm on this op does by construction — ends the run below the threshold and the + * instrument that would have scored the arm does not print at all. Measured 2026-08-27. + * Silent when the family ran nothing. + */ +void pn_tt_gdn_report_phases(const pn_tt_device *device); + +/** + * PHASE CONV: `conv_out = SiLU(conv1d(qkv))`, with the window rolled ON THE CARD. + * + * @param qkv `conv_dim` floats, the fused projection + * @param conv_out `conv_dim` floats, written + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdn_conv(pn_tt_device *device, uint32_t layer, const float *qkv, float *conv_out); + +/** + * PHASE DELTA: the four-pass delta rule over every v-head, with the `[S,S]` state read, + * updated and written back ON THE CARD. + * + * **`q` and `k` are PER K-HEAD and must NOT be GQA-repeated.** The reader does the repeat as + * one modulo; staging the repeated 32-head form doubles them and is the 1.33x-worse row of + * the split-point table. + * + * @param q `k_heads * state_size` floats, L2-normalised, un-repeated + * @param k likewise + * @param v `v_heads * state_size` floats + * @param decay `v_heads` floats — `exp(g_h)`, already through the host `softplus` and `exp` + * because there is no SFPU `log` + * @param beta `v_heads` floats, already through the sigmoid + * @param y `v_heads * state_size` floats, written + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdn_delta(pn_tt_device *device, uint32_t layer, const float *q, const float *k, + const float *v, const float *decay, const float *beta, float *y); + +/* ============================================================================ + * THE ON-CARD CHAIN — the same two descriptors with no host array anywhere + * ============================================================================ + * The five calls below are the whole of A2. The two phases become + * + * widen(ssm_qkv bf16) -> gdn_stream_in + * pn_tt_gdn_conv_oncard() -> the strip at pn_tt_gdn_stream_operand() + * an L2 head norm over its first pn_tt_gdn_key_tiles() tiles, IN PLACE + * pn_tt_gdn_delta_oncard() -> the strip at pn_tt_gdn_y_operand() + * an RMS head norm, narrow(), then silu(z) * on the elementwise residency + * + * and the only bytes that cross the link for the block are `alpha`/`beta` down and the + * 256-byte gate block up. The activations do not: readback 1478.09 -> 326.09 KiB/token. + * + * The L2 norm runs IN PLACE over the conv output because PHASE CONV's output buffer IS PHASE + * DELTA's input buffer — the two are the same 256-tile row-0 strip and q, k and v are already + * where `ftg_delta_stream_base()` reads them. In place is safe here for a pipeline reason and + * not a hopeful one: the reduce reader prefetches ONE row block, so it has read block `b` + * before the writer can have written block `b`. There is a discriminating arm for it. + * ============================================================================ */ + +/** PHASE CONV over whatever is in the stream-in buffer, writing the stream buffer. */ +int pn_tt_gdn_conv_oncard(pn_tt_device *device, uint32_t layer); + +/** PHASE DELTA over whatever is in the stream buffer, writing `y`. Uploads the 256-byte gate + * block and nothing else. */ +int pn_tt_gdn_delta_oncard(pn_tt_device *device, uint32_t layer, const float *decay, + const float *beta); + +/** PM 7 ITEM 6: where the next DELTA reads its gate — `rows` rows (1 = the single gate, or a + * deferred row's block) — so the card can write it there (gdngate FLAT). @return 0 or -1. */ +int pn_tt_gdn_gate_target(pn_tt_device *device, uint32_t rows, uint32_t *bank, + uint32_t *address, uint32_t *row_stride); + +/** `decay[v_heads]` then `beta[v_heads]`, flat fp32, into the gate buffer. Public because the + * on-card path uploads it without staging anything else. */ +int pn_tt_gdn_gate_upload(pn_tt_device *device, const float *decay, const float *beta); + +/** PHASE CONV's INPUT — where the widened `ssm_qkv` strip has to land. */ +ft_tt_op_operand pn_tt_gdn_stream_in_operand(const pn_tt_device *device); + +/** PHASE CONV's OUTPUT and PHASE DELTA's INPUT, which are ONE buffer. */ +ft_tt_op_operand pn_tt_gdn_stream_operand(const pn_tt_device *device); + +/** PHASE DELTA's OUTPUT. */ +ft_tt_op_operand pn_tt_gdn_y_operand(const pn_tt_device *device); + +/** + * Read logical row 0 of `tiles` tiles of the stream buffer, starting at `first_tile`. + * + * A PROBE, and the only reader of it: on the shipped on-card path nothing downloads this + * buffer at all. It exists so the in-place q/k L2 norm can be scored against its own preimage + * rather than asserted — see `qwen35moe_gdn_strip_tt.c`. + * + * @param out `tiles * 32` floats + * @return 0, or -1 + */ +int pn_tt_gdn_stream_read(pn_tt_device *device, uint32_t first_tile, uint32_t tiles, + float *out); + +/* ============================================================================ + * THE PHASE-ISOLATION SEAM — for attributing a chunkwise divergence, and nothing else + * ============================================================================ + * The chunkwise family (`pn_tt_gdnc.h`) writes rows 0..C-1 of these same three buffers; the + * decode family writes row 0 and ZERO ROWS 1..31, because it computes whole tiles over an + * input whose rows 1..31 are padding. So a CHUNK descriptor's answer cannot simply be handed + * to the next DECODE descriptor and back: each decode descriptor would wipe the other rows. + * + * The two calls below are what lets one phase run per position over a chunk's rows while the + * other stays chunkwise: shuttle row `t` to row 0, run the decode descriptor, take row 0 + * away again, and restore the decode family's zero-rows invariant in between. Every move is + * an exact fp32 copy over the link, so a hybrid arm's divergence from the sequential path is + * attributable to the ONE chunk kernel it still runs. + * + * SLOW ON PURPOSE — this is a quality instrument, not a speed path, and it is only ever + * reached from `PN_TT_PREFILL_GDNC_PHASE`. + * ============================================================================ */ + +/** Which of the family's three row-major fp32 strips a row move addresses. */ +typedef enum { + PN_TT_GDN_STRIP_STREAM_IN = 0, /**< PHASE CONV's input */ + PN_TT_GDN_STRIP_STREAM_OUT, /**< PHASE CONV's output, PHASE DELTA's input */ + PN_TT_GDN_STRIP_Y /**< PHASE DELTA's output */ +} pn_tt_gdn_strip_id; + +/** + * Move logical row `row` of the first `tiles` tiles of one strip, in either direction. + * + * @param values `tiles * 32` floats + * @return 0, or -1 on an unarmed family, a bad strip or a row past 31 + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_gdn_strip_row_move(pn_tt_device *device, pn_tt_gdn_strip_id strip, uint32_t row, + uint32_t tiles, float *values, bool reading); + +/** Zero one whole strip — the decode family's precondition, which a chunk descriptor's + * whole-tile write is exactly what breaks. */ +int pn_tt_gdn_strip_zero(pn_tt_device *device, pn_tt_gdn_strip_id strip); + +/** `conv_dim/32` — the whole stream. */ +uint32_t pn_tt_gdn_stream_tiles(const pn_tt_device *device); + +/** `2 * k_heads * state_size/32` — q then k, the prefix the L2 norm covers. */ +uint32_t pn_tt_gdn_key_tiles(const pn_tt_device *device); + +/** `v_heads * state_size/32` — v, and the same count as `y`. */ +uint32_t pn_tt_gdn_value_tiles(const pn_tt_device *device); + +#endif /* USE_TT */ +#endif /* PN_TT_GDN_H */ diff --git a/src/accel/tt/pn_tt_gdn_checkpoint.c b/src/accel/tt/pn_tt_gdn_checkpoint.c new file mode 100644 index 0000000..4f7afb6 --- /dev/null +++ b/src/accel/tt/pn_tt_gdn_checkpoint.c @@ -0,0 +1,177 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn_checkpoint.c + * @brief The multi-turn checkpoint of the carried GDN state. See pn_tt_gdn_checkpoint.h. + * + * @status Per-layer capture / restore through the puller's COPY; sampled verify. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gdn_checkpoint.h" + +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_dispatch.h" +#include "pn_tt_gdn.h" +#include "../../core/bench.h" + +/** The bytes each end of a verified buffer is compared over. */ +#define CKPT_VERIFY_BYTES (64u * 1024u) + +/** RED `ckpt_copy_short`: the last 4 KiB of every buffer is left. */ +#define CKPT_RED_SHORT_BYTES 4096u + +static int ckpt_reserve(pn_tt_device *device) +{ + const uint32_t layers = device->gdn_shape.layer_count; + pn_tt_gdn_region *set; + uint32_t layer; + + if (device->gdn_checkpoint) + return 0; + set = calloc(layers, sizeof(*set)); + if (!set) + return -1; + for (layer = 0; layer < layers; layer++) + if (pn_tt_gdn_alloc_twin(device, layer, &set[layer]) != 0) { + /* The DRAM already taken stays with the allocator until close; only the host + * array is dropped, and a later capture tries again. */ + free(set); + return -1; + } + device->gdn_checkpoint = set; + return 0; +} + +/** Copy one buffer card side. Both ends are exactly `bytes` long and 64 B aligned. */ +static int ckpt_copy(pn_tt_device *device, const pn_tt_gdn_buffer *from, + const pn_tt_gdn_buffer *to, size_t bytes) +{ + if (pn_tt_red("ckpt_copy_short") && bytes > CKPT_RED_SHORT_BYTES) + bytes -= CKPT_RED_SHORT_BYTES; + return pn_tt_copy_dram_oncard(device, from->bank, from->address, to->bank, to->address, + bytes); +} + +/** Copy one layer's window + state, `to_checkpoint` choosing the direction. */ +static int ckpt_copy_layer(pn_tt_device *device, uint32_t layer, bool to_checkpoint) +{ + const pn_tt_gdn_region *live = &device->gdn_region[layer]; + const pn_tt_gdn_region *saved = &device->gdn_checkpoint[layer]; + const pn_tt_gdn_region *from = to_checkpoint ? live : saved; + const pn_tt_gdn_region *to = to_checkpoint ? saved : live; + size_t window, state; + + pn_tt_gdn_carried_bytes(device, &window, &state); + if ((window & 63u) != 0u || (state & 63u) != 0u) { + fprintf(stderr, "pn_tt: gdn checkpoint — the carried buffers (%zu / %zu B) are not " + "64 B multiples; the on-card copy cannot move them\n", window, state); + return -1; + } + if (ckpt_copy(device, &from->window, &to->window, window) != 0 || + ckpt_copy(device, &from->state, &to->state, state) != 0) + return -1; + return 0; +} + +int pn_tt_gdn_checkpoint_capture_layer(pn_tt_device *device, uint32_t layer) +{ + if (!pn_tt_gdn_is_armed(device) || layer >= device->gdn_shape.layer_count || + ckpt_reserve(device) != 0) + return -1; + if (ckpt_copy_layer(device, layer, true) != 0) { + fprintf(stderr, "pn_tt: gdn checkpoint — the capture copy of layer %u failed\n", layer); + return -1; + } + return 0; +} + +int pn_tt_gdn_checkpoint_restore(pn_tt_device *device) +{ + const double started = pn_time_ms(); + uint32_t layer; + + if (!pn_tt_gdn_is_armed(device) || !device->gdn_checkpoint) + return -1; + /* Positions move backwards: the on-card dispatch model re-learns, as at a sequence start. */ + pn_tt_dispatch_sequence_reset(); + if (pn_tt_red("ckpt_restore_skip")) { + fprintf(stderr, "pn_tt: gdn checkpoint — RED ckpt_restore_skip: nothing restored\n"); + return 0; + } + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) + if (ckpt_copy_layer(device, layer, false) != 0) { + fprintf(stderr, "pn_tt: gdn checkpoint — the restore copy failed at layer %u; the " + "live state is undefined\n", layer); + return -1; + } + fprintf(stderr, "pn_tt: gdn checkpoint — restored %u layers in %.1f ms\n", + device->gdn_shape.layer_count, pn_time_ms() - started); + return 0; +} + +bool pn_tt_gdn_checkpoint_verify_wanted(void) +{ + return pn_tt_env_u32("PN_TT_CKPT_VERIFY", 0u) != 0u; +} + +/** Count differing words between the first and last CKPT_VERIFY_BYTES of two buffers. */ +static long long ckpt_verify_buffer(pn_tt_device *device, const pn_tt_gdn_buffer *a, + const pn_tt_gdn_buffer *b, size_t bytes, uint32_t *ha, + uint32_t *hb, unsigned long long *words) +{ + const size_t span = bytes < CKPT_VERIFY_BYTES ? bytes : CKPT_VERIFY_BYTES; + const size_t offsets[2] = { 0u, bytes - span }; + long long differ = 0; + size_t end, i; + + for (end = 0; end < 2u; end++) { + const uint32_t at = (uint32_t)offsets[end]; + + if (pn_tt_read_dram(device, a->bank, a->address + at, ha, span) != 0 || + pn_tt_read_dram(device, b->bank, b->address + at, hb, span) != 0) + return -1; + for (i = 0; i < span / 4u; i++) + differ += ha[i] != hb[i]; + *words += span / 4u; + } + return differ; +} + +long long pn_tt_gdn_checkpoint_verify_layer(pn_tt_device *device, uint32_t layer, + unsigned long long *words) +{ + uint32_t *ha, *hb; + long long window_differ, state_differ; + size_t window, state; + + if (!pn_tt_gdn_checkpoint_verify_wanted() || !device || !device->gdn_checkpoint || + layer >= device->gdn_shape.layer_count) + return 0; + ha = malloc(CKPT_VERIFY_BYTES); + hb = malloc(CKPT_VERIFY_BYTES); + if (!ha || !hb) { + free(ha); + free(hb); + return -1; + } + pn_tt_gdn_carried_bytes(device, &window, &state); + window_differ = ckpt_verify_buffer(device, &device->gdn_region[layer].window, + &device->gdn_checkpoint[layer].window, window, ha, hb, + words); + state_differ = ckpt_verify_buffer(device, &device->gdn_region[layer].state, + &device->gdn_checkpoint[layer].state, state, ha, hb, + words); + free(ha); + free(hb); + return window_differ < 0 || state_differ < 0 ? -1 : window_differ + state_differ; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gdn_checkpoint.h b/src/accel/tt/pn_tt_gdn_checkpoint.h new file mode 100644 index 0000000..09d06bf --- /dev/null +++ b/src/accel/tt/pn_tt_gdn_checkpoint.h @@ -0,0 +1,64 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn_checkpoint.h + * @brief The multi-turn checkpoint of the carried GDN state: every SSM layer's conv window and + * recurrent state copied, ON THE CARD, to a second region set and back. + * + * A hybrid model cannot rewind by position: the recurrent state is a fold of the whole prefix. + * The model layer takes a checkpoint at a prefill PASS boundary (`qwen35moe_prefill_tt.c`) and + * a later turn restores it and batch-prefills from there with the same passes a cold run would + * take, so its output is the cold run's. The attention K/V needs no copy: rows below the + * checkpoint are never rewritten while it is valid (the model layer enforces that). + * + * The copies are the puller's COPY op (`resident_puller.c` kOpCopy): ~120 MB card side instead + * of 22 s over a 5.3 MB/s readback. The checkpoint stays in card DRAM; nothing is written to + * the host's disk (bank-never-solos: a blob for the user's machine is a separate export). + * + * status: per-layer capture, restore and a sampled byte verify. + * issues: one checkpoint a device (one sequence on the card at a time). + * todo: none. + */ +#ifndef PN_TT_GDN_CHECKPOINT_H +#define PN_TT_GDN_CHECKPOINT_H + +#ifdef USE_TT + +#include "pn_tt_device.h" + +/** + * Copy SSM layer `layer`'s live window + state into its checkpoint twin (the set is allocated + * on first use). Called between two 32-row bands of a prefill pass, when that layer's state is + * the state at the checkpoint position. Flushes the card first. RED `ckpt_copy_short` leaves + * each buffer's last 4 KiB uncopied. + * + * @return 0, or -1 (the checkpoint must then be treated as absent) + */ +int pn_tt_gdn_checkpoint_capture_layer(pn_tt_device *device, uint32_t layer); + +/** + * Copy every layer's checkpoint twin back over the live window + state, and restart the + * dispatch model's learning (positions move backwards). RED `ckpt_restore_skip` reports + * success and copies nothing: the live state stays the turn's end and the output must DIFFER. + * + * @return 0, or -1 (nothing captured, or a copy failed: the live state is then UNDEFINED and + * the caller must start the sequence over) + */ +int pn_tt_gdn_checkpoint_restore(pn_tt_device *device); + +/** + * `PN_TT_CKPT_VERIFY=1`: compare the first and last 64 KiB of layer `layer`'s live window and + * state with its checkpoint twin over the link. Call right after that layer's capture, or after + * a restore. The gate of the copy is bytes, not an argmax. + * + * @return the 32-bit words that differ (0 when the knob is off), or -1 when a read failed; + * `*words` grows by the words compared + */ +long long pn_tt_gdn_checkpoint_verify_layer(pn_tt_device *device, uint32_t layer, + unsigned long long *words); + +/** Is `PN_TT_CKPT_VERIFY` set? */ +bool pn_tt_gdn_checkpoint_verify_wanted(void); + +#endif /* USE_TT */ +#endif /* PN_TT_GDN_CHECKPOINT_H */ diff --git a/src/accel/tt/pn_tt_gdn_core_scaling.c b/src/accel/tt/pn_tt_gdn_core_scaling.c new file mode 100644 index 0000000..2641afd --- /dev/null +++ b/src/accel/tt/pn_tt_gdn_core_scaling.c @@ -0,0 +1,218 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn_core_scaling.c + * @brief Implementation of the GDN multi-core probe. See pn_tt_gdn_core_scaling.h. + * + * @status None + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gdn_core_scaling.h" + +#include +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_gdn.h" + +/** Extra cores this probe will ever hold. 90 is the GDN's own and 91 is the head norm's, so + * the extras start after them and eight of them clears every other residency's default. */ +#define PN_TT_GDN_PROBE_MAX_EXTRA 7u + +/** First Tensix the extras take. A default, not a constant — `PN_TT_GDN_PROBE_CORE_FIRST` + * moves it, and `pn_tt_prepare_family_on_core()` still refuses a matmul member by name. */ +#define PN_TT_GDN_PROBE_CORE_FIRST_ENV "PN_TT_GDN_PROBE_CORE_FIRST" +#define PN_TT_GDN_PROBE_CORE_FIRST_DEFAULT 92u + +/** Descriptor sets timed at each width. Enough that the spread is visible and few enough + * that the probe is over in under a second. */ +#define PN_TT_GDN_PROBE_ROUNDS 16u + +static double probe_now(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0.0; + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static uint32_t probe_env_u32(const char *name, uint32_t fallback) +{ + const char *value = getenv(name); + long parsed; + + if (!value || !*value) + return fallback; + parsed = strtol(value, NULL, 10); + return parsed > 0 ? (uint32_t)parsed : fallback; +} + +/** An fp32 operand descriptor for one of this family's DRAM buffers. */ +static ft_tt_op_operand probe_operand(const pn_tt_device *device, + const pn_tt_gdn_buffer *buffer) +{ + ft_tt_op_operand operand; + + operand.x = (uint16_t)device->dram[buffer->bank].x; + operand.y = (uint16_t)device->dram[buffer->bank].y; + operand.address = buffer->address; + operand.tile_bytes = FT_TT_OP_GDN_FP32_TILE_BYTES; + return operand; +} + +/** + * Arm one extra GDN residency on `core`, with the SAME circular-buffer placement the shipped + * one has. + * + * The placement comes from a CONV-shaped request even though the probe submits DELTA + * descriptors, because one placement serves both phases — that is a property of this family + * and `pn_tt_gdn.c` arms the real residency exactly the same way. Arming it any other way + * would be measuring a different residency from the one the model runs. + */ +static bool probe_arm_one(pn_tt_device *device, pn_tt_residency *residency, uint32_t core, + const ft_tt_op_gdn_request *shape) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_GDN_CB_COUNT]; + ft_tt_op_gdn_request request = *shape; + + request.phase = FT_TT_OP_GDN_PHASE_CONV; + request.activation_in.tile_bytes = FT_TT_OP_GDN_FP32_TILE_BYTES; + request.activation_out.tile_bytes = FT_TT_OP_GDN_FP32_TILE_BYTES; + if (!pn_tt_prepare_family_on_core(device, residency, PN_TT_GDN_PROGRAM, "gdn-probe", + PN_TT_GDN_PROBE_CORE_FIRST_ENV, core, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_gdn_place_cbs(&request, placed, residency->cb_buffer, + sizeof(residency->cb_buffer), &residency->cb_mask) != FT_TT_OK) { + pn_tt_residency_release(residency); + return false; + } + if (!pn_tt_start_family(residency)) { + pn_tt_residency_release(residency); + return false; + } + return true; +} + +/** + * One timed set: `width` descriptors started before any is waited on, `rounds` times. + * + * THE ENQUEUES COME FIRST AND THE WAITS COME SECOND. Enqueue-wait per core would measure + * `width` SERIAL descriptors and print a flat line at every width — the shape of "it does + * not parallelise" produced by the harness rather than by the card, which is the failure this + * whole project keeps finding. The two loops being separate is the measurement. + */ +static double probe_time_width(pn_tt_device *device, pn_tt_residency **residency, + uint32_t width, uint32_t rounds, + const ft_tt_op_gdn_request *template_request, bool *out_ok) +{ + uint32_t words[PN_TT_GDN_PROBE_MAX_EXTRA + 1u][FT_TT_OP_GDN_WORDS]; + double started; + uint32_t round; + uint32_t index; + + /* Each core gets its OWN layer's carried state, so no two descriptors of a set touch the + * same DRAM and the timing is not a serialisation on one address. */ + for (index = 0; index < width; index++) { + ft_tt_op_gdn_request request = *template_request; + + request.state_recurrent = probe_operand(device, &device->gdn_region[index].state); + if (ft_tt_op_gdn_parameters(&request, words[index]) != FT_TT_OK) { + *out_ok = false; + return 0.0; + } + } + + started = probe_now(); + for (round = 0; round < rounds; round++) { + for (index = 0; index < width; index++) + if (pn_tt_residency_enqueue(residency[index], FT_TT_QUEUE_OP_GDN, words[index], + 1u) != 0) { + *out_ok = false; + return 0.0; + } + for (index = 0; index < width; index++) + if (pn_tt_residency_await(residency[index], PN_TT_OP_TIMEOUT_MS) != 0) { + *out_ok = false; + return 0.0; + } + } + *out_ok = true; + return probe_now() - started; +} + +void pn_tt_gdn_core_scaling_probe(pn_tt_device *device, + const ft_tt_op_gdn_request *template_request) +{ + const uint32_t asked = probe_env_u32("PN_TT_GDN_CORES", 0u); + const uint32_t first = probe_env_u32(PN_TT_GDN_PROBE_CORE_FIRST_ENV, + PN_TT_GDN_PROBE_CORE_FIRST_DEFAULT); + pn_tt_residency extra[PN_TT_GDN_PROBE_MAX_EXTRA]; + pn_tt_residency *set[PN_TT_GDN_PROBE_MAX_EXTRA + 1u]; + double one_core = 0.0; + uint32_t armed = 0; + uint32_t width; + uint32_t index; + + if (!device || !template_request || asked < 2u || device->gdn_probe_done) + return; + device->gdn_probe_done = true; + if (!pn_tt_gdn_is_armed(device)) + return; + + width = asked > PN_TT_GDN_PROBE_MAX_EXTRA + 1u ? PN_TT_GDN_PROBE_MAX_EXTRA + 1u : asked; + /* A width past the layer count would give two cores the same state region, which is the + * one thing this probe promises not to do. */ + if (width > device->gdn_shape.layer_count) + width = device->gdn_shape.layer_count; + + memset(extra, 0, sizeof(extra)); + set[0] = &device->gdn; + for (index = 0; index + 1u < width; index++) { + if (!probe_arm_one(device, &extra[index], first + index, template_request)) + break; + set[index + 1u] = &extra[index]; + armed++; + } + if (armed + 1u < width) { + fprintf(stderr, "pn_tt: gdn CORE SCALING — only %u of the %u cores armed; the sweep " + "stops there\n", armed + 1u, width); + width = armed + 1u; + } + fprintf(stderr, "pn_tt: gdn CORE SCALING — %u cores from %u, %u rounds each. THIS WRITES " + "THE CARRIED STATE OF %u LAYERS: the run's text is not evidence.\n", + width, first, PN_TT_GDN_PROBE_ROUNDS, width); + + for (index = 1u; index <= width; index <<= 1) { + bool ok = false; + const double seconds = + probe_time_width(device, set, index, PN_TT_GDN_PROBE_ROUNDS, template_request, &ok); + const double per_set = seconds / (double)PN_TT_GDN_PROBE_ROUNDS; + const double per_descriptor = per_set / (double)index; + + if (!ok) { + fprintf(stderr, "pn_tt: gdn CORE SCALING — width %u FAILED; the residencies are " + "stranded and the sweep stops\n", index); + break; + } + if (index == 1u) + one_core = per_set; + fprintf(stderr, "pn_tt: gdn CORE SCALING width %2u — %.4f s a set, %.1f us a " + "descriptor, %.2fx the throughput of one core\n", + index, per_set, 1e6 * per_descriptor, + per_set > 0.0 ? one_core * (double)index / per_set : 0.0); + } + + for (index = 0; index < armed; index++) + pn_tt_residency_release(&extra[index]); + fprintf(stderr, "pn_tt: gdn CORE SCALING — %u extra cores given back\n", armed); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gdn_core_scaling.h b/src/accel/tt/pn_tt_gdn_core_scaling.h new file mode 100644 index 0000000..2d271df --- /dev/null +++ b/src/accel/tt/pn_tt_gdn_core_scaling.h @@ -0,0 +1,71 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn_core_scaling.h + * @brief Does the GDN block go faster on more than one Tensix? An INSTRUMENT, armed only by + * `PN_TT_GDN_CORES` and never by the shipped path. + * + * --------------------------------------------------------------------------- + * WHY THE QUESTION IS OPEN AND WHY IT NEEDS SILICON + * --------------------------------------------------------------------------- + * The GDN op runs on ONE core. Its outer loop is over v-heads, and the heads are independent + * — each carries its own [S,S] state, reads its own v/decay/beta and writes its own row of y + * — so the arithmetic parallelises trivially. What is NOT known is whether the CARD does: + * the descriptor moves the carried state through DRAM, and Phase 1's core-scaling knee at + * N=4 was measured on the MATMUL path, whose traffic is a broadcast weight strip and nothing + * like this one. A knee measured on one op is not a property of the machine. + * + * So this probe asks the machine directly: run N copies of the SAME descriptor on N cores at + * once, each against a DIFFERENT SSM layer's carried state so no two touch the same DRAM, + * and compare the wall time of the set against one core's. Perfect scaling is N descriptors + * in one descriptor's time. Anything less is a shared resource, and the ratio prices it. + * + * **Full-width copies rather than a head split, deliberately.** Splitting 32 heads over N + * cores would measure the same thing and would also need N sets of per-core operand offsets + * — new address arithmetic, in a probe, whose bugs would look exactly like a scaling result. + * N full copies need no new addresses at all: the only word that changes per core is the + * state operand, and it is a region this device already allocated. If N copies cost one + * copy's time, then N head-slices certainly do. + * + * --------------------------------------------------------------------------- + * WHAT IT COSTS, WHICH IS NOT NOTHING + * --------------------------------------------------------------------------- + * The probe WRITES the carried state of the layers it borrows. It runs once, at the first + * DELTA descriptor of a run, where the state is still the zero the sequence starts from — + * but it leaves those layers holding a recurrence step the model did not ask for. **A run + * with this knob set produces text that is not evidence of anything.** It is a stopwatch. + * + * It also holds N-1 extra Tensix for the length of the sweep and then gives them back. A + * GDN triad that exits on a circular-buffer spin bound wedges its core until a rung-3 reset, + * so the extra residencies are retired by ASKING, the same way every other one is. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TT_GDN_CORE_SCALING_H +#define PN_TT_GDN_CORE_SCALING_H + +#ifdef USE_TT + +#include "pn_tt_device.h" + +#include "tt/ft_tt_op_gdn.h" + +/** + * Run the sweep, print the table on stderr, and give the extra cores back. + * + * Does nothing at all unless `PN_TT_GDN_CORES` is set to 2 or more. Safe to call on every + * DELTA descriptor — it runs once per device and remembers that it has. + * + * @param device an armed GDN device (`pn_tt_gdn_is_armed`) + * @param template a fully built DELTA request; the probe changes only its state operand + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_tt_gdn_core_scaling_probe(pn_tt_device *device, + const ft_tt_op_gdn_request *template_request); + +#endif /* USE_TT */ +#endif /* PN_TT_GDN_CORE_SCALING_H */ diff --git a/src/accel/tt/pn_tt_gdn_layout.c b/src/accel/tt/pn_tt_gdn_layout.c new file mode 100644 index 0000000..1a08a71 --- /dev/null +++ b/src/accel/tt/pn_tt_gdn_layout.c @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn_layout.c + * @brief Implementation of the GDN stager's address arithmetic. See pn_tt_gdn_layout.h. + * + * @status Pure; `tests/test_gdn_layout.c` proves it against literals derived from the + * protocol header and the CPU oracle rather than from these functions. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gdn_layout.h" + +#include + +#include "tt/ft_tt_tilize.h" + +uint32_t pn_tt_gdn_head_tile(uint32_t head, uint32_t state) +{ + return head * (state / PN_TT_GDN_LAYOUT_TILE_DIM); +} + +uint32_t pn_tt_gdn_row_offset(uint32_t tile, uint32_t row, uint32_t chunk) +{ + /* Four 16x16 faces in order, so the face holding `[row][chunk*16 .. chunk*16+15]` is + * `(row/16)*2 + chunk`, and each face is `TILE_BYTES/4` — 1024 B in fp32. Inside the face + * the row is `row % 16` rows of 16 fp32 down. */ + const uint32_t face = (row / 16u) * 2u + chunk; + + return tile * PN_TT_GDN_LAYOUT_TILE_BYTES + face * (PN_TT_GDN_LAYOUT_TILE_BYTES / 4u) + + (row % 16u) * PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES; +} + +uint32_t pn_tt_gdn_row0_offset(uint32_t tile, uint32_t chunk) +{ + /* Face 0 is elements [0,256) of the tile and face 1 is [256,512), so the two halves of + * row 0 are `TILE_BYTES/4` apart — 1024 B in fp32. */ + return pn_tt_gdn_row_offset(tile, 0u, chunk); +} + +int pn_tt_gdn_stage_plane(const float *source, uint32_t first, uint32_t stride, + uint32_t conv_dim, float *scratch, unsigned char *tiles_out) +{ + uint32_t channel; + + if (!source || !scratch || !tiles_out || conv_dim == 0u || stride == 0u || + (conv_dim % PN_TT_GDN_LAYOUT_TILE_DIM) != 0u) + return -1; + for (channel = 0; channel < conv_dim; channel++) + scratch[channel] = source[(size_t)first + (size_t)channel * stride]; + if (ft_tt_tilize_strip(scratch, sizeof(*scratch), 1u, conv_dim, + (size_t)conv_dim * sizeof(*scratch), tiles_out) != FT_TT_TILIZE_OK) + return -1; + return 0; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gdn_layout.h b/src/accel/tt/pn_tt_gdn_layout.h new file mode 100644 index 0000000..1468b32 --- /dev/null +++ b/src/accel/tt/pn_tt_gdn_layout.h @@ -0,0 +1,130 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdn_layout.h + * @brief The GDN stager's ADDRESS ARITHMETIC, with no device in it — the four conversions + * between a host float array and the DRAM layouts + * `foundry/tt/kernels/resident_gdn_protocol.h` settles. + * + * Separate from `pn_tt_gdn.c` for one reason: these are the functions a wrong answer + * hides in, and split out they are provable with no card in the machine + * (`tests/test_gdn_layout.c`). Everything left in `pn_tt_gdn.c` is allocation, + * descriptor encoding and NoC calls — things a test cannot reach anyway. + * + * --------------------------------------------------------------------------- + * WHAT "TILIZED" MEANS HERE, ONCE + * --------------------------------------------------------------------------- + * What `ft_tt_tilize_strip()` produces: `ceil(rows/32)` row blocks in order, each + * `ceil(cols/32)` FACE-ORDERED 32x32 tiles in order, so element `[r][c]` lands at tile + * `(r/32)*ceil(cols/32) + c/32`, position `[r%32][c%32]`. Every operand of this family is + * fp32, so a tile is 4096 bytes. + * + * @status Pure C, no device, no allocation beyond what the caller hands in. + * @issues None + * @todo None + */ + +#ifndef PN_TT_GDN_LAYOUT_H +#define PN_TT_GDN_LAYOUT_H + +#ifdef USE_TT + +#include + +/** Bytes of one fp32 tile, and the elements along one of its edges. */ +#define PN_TT_GDN_LAYOUT_TILE_BYTES 4096u +#define PN_TT_GDN_LAYOUT_TILE_DIM 32u + +/** The two 64-byte pieces logical row 0 of an fp32 tile is split across — columns 0-15 at the + * top of face 0, columns 16-31 at the top of face 1. */ +#define PN_TT_GDN_LAYOUT_ROW0_CHUNKS 2u +#define PN_TT_GDN_LAYOUT_ROW0_CHUNK_ELEMS 16u +#define PN_TT_GDN_LAYOUT_ROW0_CHUNK_BYTES (PN_TT_GDN_LAYOUT_ROW0_CHUNK_ELEMS * 4u) + +/** + * Byte offset, within a strip of consecutive fp32 tiles, of one of logical row 0's two + * pieces. + * + * The ONE place that layout is expressed, and separate for the reason `ft_tt_tile_row0.h` + * gives about its own: the two mutations that matter — a wrong face stride, and the two + * chunks swapped — are wrong values out of exactly this function. + * + * **In fp32 both pieces are exactly 64 bytes, which is the DRAM READ alignment.** The bf16 + * row-0 path has to read 64 and discard 32; this one wastes nothing, which is why the conv + * stream crosses the link as row 0 rather than as whole tiles. + * + * @param tile which tile of the strip + * @param chunk 0 for columns 0-15, 1 for columns 16-31 + * @return byte offset from the strip's base + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +uint32_t pn_tt_gdn_row0_offset(uint32_t tile, uint32_t chunk); + +/** + * The same offset for ANY logical row of the tile, not just row 0. + * + * An fp32 tile is four FACES of 16x16, face-ordered: face `(row/16)*2 + (col/16)` sits at + * `face * TILE_BYTES/4`, and inside a face row `row%16` sits at `(row%16) * 64`. Row 0 is the + * `row = 0` case and nothing else, so `pn_tt_gdn_row0_offset()` is written in terms of THIS + * function and the layout is still expressed once. + * + * **IT EXISTS FOR THE CHUNK PHASE-ISOLATION ARM AND NOTHING ELSE.** The decode GDN family + * reads and writes logical row 0; the chunkwise family reads and writes rows 0..C-1 of the + * SAME buffers. Attributing a divergence to one of the two chunk kernels means running the + * OTHER phase per position across a chunk's rows, which needs row `t` moved to row 0 and the + * answer moved back — exact fp32 copies, so the arm adds no arithmetic of its own. + * + * @param tile which tile of the strip + * @param row 0..31; anything else is clamped by the caller, not here + * @param chunk 0 for columns 0-15, 1 for columns 16-31 + * @return byte offset from the strip's base + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +uint32_t pn_tt_gdn_row_offset(uint32_t tile, uint32_t row, uint32_t chunk); + +/** + * Gather `conv_dim` channels at STRIDE `stride` starting at `first`, and tilize them as a + * ONE-ROW strip of `conv_dim/32` fp32 tiles. + * + * **THE CONV WEIGHT'S TAP PLANE `t` IS `(first = t, stride = taps)`, AND THAT IS THE WHOLE + * TRANSPOSE.** The oracle stores the weight row-major `[conv_dim, taps]` — `w[c*taps + t]` — + * and the card wants `taps` planes, plane `t` holding `w[.][t]` laid out exactly like the qkv + * stream. The plausible wrong answer is a RESHAPE, `(first = t*conv_dim, stride = 1)`: the + * same bytes, the same length, a different answer, and nothing on the card can see the + * difference. Spelling both as two arguments of one function is what makes the difference a + * thing a test can state. + * + * One row is `valid_rows = 1`: rows 1-31 are the tile quantum and the tilizer zeroes them, + * which is correct because the compute thread's whole-tile multiply against a zero stream row + * is zero and `SiLU(0) = 0`. + * + * @param source at least `first + (conv_dim-1)*stride + 1` floats + * @param scratch `conv_dim` floats of caller-owned scratch + * @param tiles_out `conv_dim/32 * 4096` bytes + * @return 0, or -1 on a bad argument + * + * Thread-safe: yes (caller owns both buffers) Reentrant: yes Signal-safe: no + */ +int pn_tt_gdn_stage_plane(const float *source, uint32_t first, uint32_t stride, + uint32_t conv_dim, float *scratch, unsigned char *tiles_out); + +/** + * The first tile of head `head`'s `state` values inside a HEAD-MAJOR ROW-0 STRIP. + * + * `q`, `k`, `v` at `kGdnActivationIn` and `y` at `kGdnActivationOut` are all row-0 strips now: + * a head's `state` values are `state/32` CONSECUTIVE tiles, all of them in row 0, exactly the + * geometry `pn_tt_gdn_row0_offset()` addresses and exactly the geometry PHASE CONV's output + * already has. They used to be tilized `[heads, state]` matrices with one row per head, which + * is 32x denser in DRAM and forced the host to de-tilize, L2-normalise and re-tilize between + * the two phases — 49,408 B a layer over PCIe. + * + * There is no `pn_tt_gdn_stage_stream()` any more because there is nothing left to stage: a + * row-0 strip is written and read by `pn_tt_fp32_row0_move()`, the same primitive the conv + * stream and the per-head reduce residency's strips already use. + */ +uint32_t pn_tt_gdn_head_tile(uint32_t head, uint32_t state); + +#endif /* USE_TT */ +#endif /* PN_TT_GDN_LAYOUT_H */ diff --git a/src/accel/tt/pn_tt_gdnc.c b/src/accel/tt/pn_tt_gdnc.c new file mode 100644 index 0000000..ebf9cba --- /dev/null +++ b/src/accel/tt/pn_tt_gdnc.c @@ -0,0 +1,2119 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdnc.c + * @brief The host stager for the CHUNKWISE Gated-Delta-Net op. The API, the ownership split + * and the cost model are in pn_tt_gdnc.h. + * + * --------------------------------------------------------------------------- + * WHY THIS FILE INCLUDES A KERNEL HEADER + * --------------------------------------------------------------------------- + * The same reason `pn_tt_gdn.c` does: the stager and the two dataflow kernels are the two + * ends of a contract nothing checks at run time, so every offset comes from the `ftgc_*` + * accessors in `resident_gdnc_protocol.h` — the functions the reader and the writer compute + * their addresses with — rather than from arithmetic respelled here. It also lets THIS file + * state, in code, that the window and the state tile maps are the decode family's. + * + * --------------------------------------------------------------------------- + * THE RED ARMS, AND WHAT EACH ONE PROVES + * --------------------------------------------------------------------------- + * Every one mutates ONLY WHAT IS UPLOADED — the ninth recorded vacuous-gate class in this + * project is a red arm that mutated an input the oracle also read and scored 0.000e+00. + * + * gdnc_weight_row0 stage the conv weight the DECODE family's way, live in row 0 and zero + * below. It is the plausible transcription error, and it is invisible at + * row 0 of every chunk and wrong at rows 1..C-1 — so it fires from a + * chunk's SECOND position and not from its first. + * gdnc_gate_first build the decay tile and the gate run from position 0's decay and beta + * repeated over the chunk. That is the shape of a caller who forgot the + * gate is per POSITION now, and it is likewise invisible at row 0. + * gdnc_window_wipe zero the shared conv window before every CONV descriptor. **INVISIBLE + * IN CHUNK 0**, where the window starts zero anyway, and wrong from + * chunk 1 — the boundary carry a single-chunk test cannot see. + * gdnc_state_wipe zero the shared recurrent state before every DELTA descriptor. Same + * structure, same invisibility in chunk 0. + * + * The last two are the host-level form of the two arms `foundry/tt/tests/test_phase6_gdnc.c` + * fires on the card, and they are here because the CARRY IS THIS FILE'S CLAIM: the kernels + * carry whatever they are pointed at, and what says the chunks are one sequence is that this + * file points every chunk at the SAME buffer and never touches it in between. + * + * @status SHIPPED ON 2026-08-25 at C = 16, behind `PN_TT_PREFILL_GDNC` (default ON, `0` + * declines). Scored 6/6 against an fp32-KV oracle at 339 positions. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gdnc.h" +#include "pn_tt_dispatch.h" +#include "pn_tt_stage_pipeline.h" + +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_gdn.h" + +#include "tt/ft_tt_op_gdnc.h" +#include "tt/ft_tt_tilize.h" + +/* See the file header: the L1 pointer attribute is the SFPI fork's, not the contract's. */ +#define FTQ_L1_PTR +#include "tt/kernels/ftgc_compact.h" +#include "tt/kernels/resident_gdnc_protocol.h" + +/** + * **ON BY DEFAULT as of 2026-08-25, at C = 16.** Only an explicit `0` turns it off. + * + * THE CHUNKWISE MIXERS ARE A DIFFERENT NUMERICAL PATH from the decode family, so this default + * waited on an output gate rather than on an argument, and the first gate FAILED it: at + * C = 32 the arm misses worst-document. The chunk length is the whole of that (see + * `kGdncFirstRow`), and at C = 16 — one PHASE CONV a pass, `ceil(rows/16)` PHASE DELTA + * descriptors IN PLACE — the arm clears all six criteria against an fp32-KV oracle at 339 + * positions and is BETTER THAN THE SEQUENTIAL ARM IT REPLACES on three of them: + * + * criterion (limit) C = 32 C = 16 sequential + * ppl_ratio (1.05) 1.000515 0.987406 0.978511 + * paired_t (4) 0.027 -1.866 -2.358 + * disagree (0.08) 0.079646 (+0.000354) 0.038348 (+0.041652) 0.029499 + * worst_doc (1.05) 1.048610 (+0.00139) 1.010782 (+0.039218) 1.020988 + * dnll_sd 0.355599 0.125078 0.169621 + * kl_mean 0.0440231 0.00251879 0.00424355 + * + * **THE MARGIN IS WHY C = 16 SHIPS AND C = 32 DOES NOT.** C = 32 passes disagreement by + * 0.000354 and worst-document by 0.00139, both on ONE 113-position document; C = 16 passes the + * same two by 118x and 28x those margins. + */ +#define PN_TT_GDNC_ENV "PN_TT_PREFILL_GDNC" + +/** Which Tensix the chunkwise triad runs on. A default, not a constant: it must be a core no + * other residency and no matmul group member holds, and the group MOVES with + * `PN_TT_MATMUL_CORES`. `pn_tt_prepare_family()` refuses a collision BY NAME, so this is + * only the first place to look. 95 is above the shipped four-way GDN head split (90 member 0, + * 91 the narrow head norm, 92-94 the members) and below attention's 103. */ +#define PN_TT_CORE_GDNC_ENV "PN_TT_CORE_GDNC" +#define PN_TT_CORE_GDNC_DEFAULT 95u + +/** + * THE CORE SPLIT. `PN_TT_GDNC_CORES=N` arms N-1 EXTRA chunkwise residencies and gives each a + * slice of BOTH phases: PHASE CONV `conv_tile_cols/N` tile columns, PHASE DELTA `v_heads/N` + * v-heads. + * + * **ON BY DEFAULT AT 8 since 2026-08-28**, after its own confirmation round on a fresh binary: + * 168.97 -> 195.91 tok/s (+15.9%), reproduced as a later session's control at 195.06/194.66 + * against a 0.024% drift, `cmp` IDENTICAL at every rung, decode wall untouched. Two independent + * binaries. The ladder is 169.0 / 186.6 / 195.5 / **196.0** / 187.8 at N = 1/2/4/8/16. + * + * WHY IT IS LEGAL, AND WHY THE WIRE ALREADY CARRIED IT. `ft_tt_op_gdnc_request.first_col` and + * `.col_count` (`kGdncColSlice`) have been on the wire since this family shipped, refused out + * of range by all four sides, and `foundry/tt/tests/test_phase6_gdnc.c` already proves ON + * SILICON that four column slices are BYTE-FOR-BYTE the one whole descriptor. Nothing was + * missing but the host: `pn_tt_gdnc.c` armed exactly one residency and never set the two + * fields. PHASE CONV divides exactly by channel — column `c` reads only column `c`'s input + * tile, window planes and weight planes, and writes only column `c`'s conv_out tile and window + * rows — and PHASE DELTA divides by v-head the same way the decode family's does. + * + * WHY THIS IS THE LEVER. The CONV descriptor costs **680.2 / 681.8 / 682.5 us at 32 / 16 / 8 + * LIVE ROWS** — 0.34% over a 4x range — because none of its cost depends on live rows: it is + * `conv_tile_cols = 256` iterations of `read_one_column()`, each ending in ONE NoC read barrier + * (`resident_gdnc_reader.c:292`, loop `:643-644`), and the work is per COLUMN, not per row. + * + * IT IS COMPUTE, NOT BARRIER LATENCY — corrected 2026-08-28, and the correction matters. + * The first reading here was "2.66 us a column of barrier-serialized latency", which predicts + * that batching columns behind one barrier pays. **Measured single-core, the CONV depth knob is + * FLAT: 679.1 / 680.9 / 683.8 us at D = 1/2/4.** So at 256 columns on one core the descriptor is + * bound by the ~18 tile ops a column, not by the barriers between them. The depth only becomes a + * lever ONCE THIS SPLIT HAS CUT THE COMPUTE 8x and the reader is finally the bind: at N=8 it is + * 177.5 / 163.6 / 157.9 us at D = 1/2/4. **Composition, not independence.** What the split + * itself buys is parallel compute — CONV 680.2 -> 177.1 (3.84x), DELTA 439.1 -> 143.7 (3.06x). + * + * AND THE KNEE IS 8 BECAUSE OF THE HOST, NOT THE CARD. At N=8 member 0 takes 113.1 us a + * descriptor while members 1..7 take 3.3-10.6 — the set genuinely runs concurrently — but the + * host awaits every member IN SERIES, so a marginal member costs ~3.5 us of host against a card + * saving already down to ~4. At 16 the fifteen trailing awaits add ~52 us and CONV goes back to + * 266.9. That is `wall(N) = 0.3885 + 0.00199*N + card(N)` in a second family, and it is what a + * device-side command fan-out removes. + * + * THE MEMBERS START AT 107 and that is a first place to look, not a guarantee. + * `pn_tt_prepare_family_on_core()` refuses a collision BY NAME against every live residency and + * against the matmul group, which MOVES with `PN_TT_MATMUL_CORES`. 107..121 is fifteen + * consecutive Tensix free at every shipped default of every knob in this backend (72 reduce / + * 73 eltwise / 74 sample / 75-81 the matmul group / 82-84 attention / 85,86 render / 87 the + * merged gate / 90,92-94 gdn / 91,103 head norms / 95 gdnc itself / 106 the eltwise split's + * member / 122,129 the render splits' members). The ONE family that can reach into it is the + * eltwise split at its maximum of 16 members (106..120), which is eight times its shipped + * default of two — and that is a refusal by name, not a corruption. + */ +#define PN_TT_GDNC_CORES_ENV "PN_TT_GDNC_CORES" +#define PN_TT_GDNC_CORES_DEFAULT 8u +#define PN_TT_GDNC_MEMBER_CORE_FIRST_ENV "PN_TT_GDNC_MEMBER_CORE_FIRST" +/* 114 since the PX7 placement shipped (was 107, inside the 16-member matmul's 96..110). */ +#define PN_TT_GDNC_MEMBER_CORE_DEFAULT 114u +/* PM 8 ITEM 7 step 2: members 1..N-1 on an explicit comma-separated core list (any order, + * not necessarily consecutive) instead of `first + index`. Unset = the consecutive block. */ +#define PN_TT_GDNC_MEMBER_CORE_LIST_ENV "PN_TT_GDNC_MEMBER_CORE_LIST" + +/** + * THE COMPLETION SHAPE OF THE SPLIT, AND IT IS A HAZARD BEHIND ITS OWN SPELLING. + * `PN_TT_GDNC_GROUP=i-know-this-wedges-the-posting-core` makes every member post into ONE + * group completion block and replaces the per-member await loop with member 0's count plus a + * single BLOCK16 read of the whole slot array. **`1` IS REFUSED**, because a `1` is what + * someone types who has not read the paragraph below. + * + * --------------------------------------------------------------------------- + * WHY THE SPELLING, AND IT IS THE SAME EVIDENCE `PN_TT_GROUP_COMPLETION` CARRIES + * --------------------------------------------------------------------------- + * `ftq_group_post()` is an inline cross-core NoC write from DM1, and the matmul group already + * paid for it: two armed arms WEDGED a core each — 75, then 71 — at + * `instrn_buf_status = 0x66`, which is the state where BRISC firmware cannot complete a boot + * and only a whole-card reset clears it (`pn_tt_matmul_group.c`, PN_TT_GROUP_COMPLETION_ENV). + * What survives that account: the post is **an ACCELERANT of the resident queue's own + * "retired clean, will not boot next" defect** (`DEV_NOTES.md` §1) and not the cause, because + * a DISARMED N=4 arm took all four cores after ~46x the work. Neither remoteness nor member + * index predicts which core goes. + * + * **AND ON THE MATMUL THE TRADE WAS ALREADY JUDGED NOT WORTH IT** — there the post buys + * TRANSACTION COUNT, `1 + ceil(8N/16)` for the whole group against `PN_TT_FAST_COMPLETION`'s + * one per member: 5 against 8 at N=8, better and not enough better to be worth a core. + * + * **THIS FAMILY'S CASE IS A DIFFERENT QUANTITY, WHICH IS THE ONLY REASON THE CODE EXISTS.** + * It is not transactions, it is SERIALIZATION: N independent blocking poll loops, run one + * after another. Measured at N = 8 on 2026-08-28, member 0 at 113.2 us a descriptor and + * members 1..7 at 3.3-10.6 — member 0's 0.4158 s is the card genuinely working and members + * 1..7's **0.1164 s** is the host learning, one at a time, something the card already + * finished. Against an 8.35 s prefill that is **1.4%**, which does not buy a wedge. + * The case is at N = 16, where the record has 187.8 tok/s against N = 8's 196.0 with the + * cause named as the trailing awaits — a 4.2% question, and the one arm that can say whether + * the 16-member regression is the host's or the card's. + * + * RECOVERY, KNOWN BEFORE ARMING: `ft_tt_core recover --rung 3 --driver udrv --write + * --then-probe` under sudo. Whole card, in place, no vendor driver and no PCI remove/rescan; + * measured at ~10 s wall on 2026-08-28 recovering core 90 from exactly the 0x66 state. + * + * --------------------------------------------------------------------------- + * WHAT IT IS, MECHANICALLY + * + * WHY IT EXISTS, MEASURED RATHER THAN ARGUED. At N = 8 the per-member awaits are member 0 at + * **113.2 us a descriptor** and members 1..7 at **3.3-10.6** — the set genuinely runs + * concurrently, and the host still pays every one of them IN SERIES. That is ~38 us of host a + * descriptor that the card is not spending, over 3,672 descriptors a prompt, and it is why + * the member ladder peaks at 8 and goes BACKWARDS at 16: a marginal member costs ~3.5 us of + * host against a card saving already down to ~4. + * + * NOTHING HERE IS NEW MACHINERY. `resident_gdnc_writer.c:404` already calls + * `ftq_group_post()`; the block layout, its magic, the slot read and the two-assertion check + * are `ft_tt_queue.c`'s; and `pn_tt_matmul_group.c` has run exactly this shape as its default + * since 2026-08-20 over millions of poll reads. The group binding rides in the kernel's + * RUNTIME ARGS, so this is host-only: **no kernel change and no program-blob regeneration.** + * + * IT IS NOT FREE, WHICH IS WHY IT SHIPS OFF UNTIL IT IS MEASURED. `ftq_group_post()`'s refused + * inline write is the ONE fault the protocol raises after `completed_count` is published, so + * `pn_tt_residency_start()` demotes these residencies from the `line` completion shape to + * `window` — five extra reads on the settle of the ONE await that remains. The trade is N poll + * loops for one, against five reads; it has to be read off the card, not predicted. + */ +#define PN_TT_GDNC_GROUP_ENV "PN_TT_GDNC_GROUP" +#define PN_TT_GDNC_GROUP_ACK "i-know-this-wedges-the-posting-core" +/** The block's L1 address on member 0's core. The SAME derivation `pn_tt_matmul_group.c:1177` + * makes, so the two cannot drift into two spellings of one address. */ +#define PN_TT_GDNC_GROUP_ADDRESS (FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET) + +#define PN_TT_GDNC_TILE_DIM FT_TT_OP_TILE_DIM +#define PN_TT_GDNC_TILE_BYTES FT_TT_OP_GDNC_FP32_TILE_BYTES + +/** Diff granularity: a tile FACE (16x16 fp32) for the decay tiles, one 32-float run for the + * gate block. `PN_TT_GDNC_STAGE_DIFF` defaults ON: 213.56 -> 235.5 tok/s, IDENTICAL (spec). */ +#define PN_TT_GDNC_STAGE_DECAY_UNIT 1024u +#define PN_TT_GDNC_STAGE_GATE_UNIT gdnc_stage_gate_unit() +#define PN_TT_GDNC_STAGE_DIFF_DEFAULT 1 + +/* ================================================================ + * Shape and operands + * ================================================================ */ + +/** + * The request describing this residency, with the operands a caller then fills in. + * + * IT CARRIES BOTH PHASES' SHAPES WHATEVER THE PHASE, because ONE circular-buffer placement + * serves both — a residency is armed once and then answers whatever the queue hands it, so a + * request naming only its own phase would place buffers the OTHER phase reads out of. + * `ft_tt_op_gdnc_place_cbs()` refuses one that does not. + */ +static ft_tt_op_gdnc_request gdnc_request(const pn_tt_device *device, uint32_t phase, + uint32_t chunk_rows) +{ + ft_tt_op_gdnc_request request; + + memset(&request, 0, sizeof(request)); + request.phase = phase; + request.chunk_rows = chunk_rows; + request.conv_dim = device->gdn_shape.conv_dim; + request.conv_taps = device->gdn_shape.conv_taps; + request.state_tiles = device->gdn_shape.state_size / PN_TT_GDNC_TILE_DIM; + request.v_heads = device->gdn_shape.v_heads; + request.k_heads = device->gdn_shape.k_heads; + return request; +} + +/** The device's own view of a DELTA shape, for the `ftgc_*` accessors. */ +static ftgc_shape gdnc_delta_shape(const pn_tt_device *device) +{ + ftgc_shape shape; + + memset(&shape, 0, sizeof(shape)); + shape.phase = FT_TT_OP_GDNC_PHASE_DELTA; + shape.conv_taps = device->gdn_shape.conv_taps; + shape.conv_tile_cols = device->gdn_shape.conv_dim / PN_TT_GDNC_TILE_DIM; + shape.state_tiles = device->gdn_shape.state_size / PN_TT_GDNC_TILE_DIM; + shape.v_heads = device->gdn_shape.v_heads; + shape.k_heads = device->gdn_shape.k_heads; + shape.head_count = device->gdn_shape.v_heads; + shape.tile_bytes = PN_TT_GDNC_TILE_BYTES; + return shape; +} + +/** An fp32 operand descriptor for one of this backend's DRAM buffers. */ +static ft_tt_op_operand gdnc_operand(const pn_tt_device *device, + const pn_tt_gdn_buffer *buffer) +{ + ft_tt_op_operand operand; + + operand.x = (uint16_t)device->dram[buffer->bank].x; + operand.y = (uint16_t)device->dram[buffer->bank].y; + operand.address = buffer->address; + operand.tile_bytes = PN_TT_GDNC_TILE_BYTES; + return operand; +} + +/** Carve one DRAM buffer. `PN_TT_GDNC_ANY_BANK` round-robins; anything else PINS. */ +#define PN_TT_GDNC_ANY_BANK UINT32_MAX + +static int gdnc_alloc_in(pn_tt_device *device, uint32_t bank, size_t bytes, bool zero, + pn_tt_gdn_buffer *out) +{ + ft_tt_buffer buffer; + + if (bytes == 0u) + return -1; + if (bank == PN_TT_GDNC_ANY_BANK) + bank = device->strip_bank_cursor++ % device->dram_bank_count; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes, &buffer) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating a %zu-byte GDNC buffer\n", bytes); + return -1; + } + out->bank = buffer.logical_bank; + out->address = (uint32_t)buffer.address; + if (zero && pn_tt_zero_dram(device, out->bank, out->address, bytes) != 0) + return -1; + return 0; +} + +/* ================================================================ + * Arming + * ================================================================ */ + +bool pn_tt_gdnc_is_armed(const pn_tt_device *device) +{ + return device && device->gdnc.running && device->gdnc_constant != NULL; +} + +uint32_t pn_tt_gdnc_min_chunk_rows(const pn_tt_device *device) +{ + if (!pn_tt_gdnc_is_armed(device)) + return 0u; + return device->gdn_shape.conv_taps - 1u; +} + +/** The four PHASE DELTA program constants — `I`, `2I`, the strictly-lower mask and the + * `scale` column. Identical in every layer and every chunk, so they go up once. */ +static int gdnc_upload_delta_constants(pn_tt_device *device) +{ + unsigned char *tiles = malloc(ft_tt_op_gdnc_delta_constant_tile_bytes()); + uint32_t slot; + int rc = -1; + + if (!tiles) + return -1; + if (ft_tt_op_gdnc_build_delta_constants(device->gdn_shape.state_size, tiles, + ft_tt_op_gdnc_delta_constant_tile_bytes()) == + FT_TT_OK) { + rc = 0; + for (slot = 0; slot < PN_TT_GDNC_STAGE_SLOTS && rc == 0; slot++) + if (pn_tt_write_dram(device, device->gdnc_delta_constant[slot].bank, + device->gdnc_delta_constant[slot].address, tiles, + ft_tt_op_gdnc_delta_constant_tile_bytes()) != 0) + rc = -1; + } + free(tiles); + return rc; +} + +/** + * PM 8: `PN_TT_GDNC_CONST_REPLICAS` copies of every layer's conv constant, copy r in bank + * `(bank of copy 0 + r) % banks`. Every CONV member reads its columns' weight planes — 16 KiB + * a column a band, 4 MiB a band over the set — and with one copy all of it comes from ONE + * bank. Same bytes in every copy, so the answer cannot move. + */ +#define PN_TT_GDNC_CONST_REPLICAS_ENV "PN_TT_GDNC_CONST_REPLICAS" + +static int gdnc_alloc_constant_replicas(pn_tt_device *device, size_t constant) +{ + /* DEFAULT 8 (run_pm8_conv_replicas.sh: serve warm 1,630 TTFT 2,547/2,537 -> + * 2,472/2,458 ms, CLI sequential 1.026 -> 0.967 s, IDENTICAL). `=1` off. */ + uint32_t replicas = pn_tt_env_u32(PN_TT_GDNC_CONST_REPLICAS_ENV, 8u); + const uint32_t layers = device->gdn_shape.layer_count; + uint32_t layer; + uint32_t copy; + + if (replicas > device->dram_bank_count) + replicas = device->dram_bank_count; + device->gdnc_constant_replicas = replicas < 1u ? 1u : replicas; + if (device->gdnc_constant_replicas == 1u) + return 0; + device->gdnc_constant_replica = calloc((size_t)layers * (device->gdnc_constant_replicas - 1u), + sizeof(*device->gdnc_constant_replica)); + if (!device->gdnc_constant_replica) + return -1; + for (layer = 0; layer < layers; layer++) + for (copy = 1; copy < device->gdnc_constant_replicas; copy++) + if (gdnc_alloc_in(device, + (device->gdnc_constant[layer].bank + copy) % + device->dram_bank_count, + constant, false, + &device->gdnc_constant_replica[(size_t)layer * + (device->gdnc_constant_replicas - + 1u) + + copy - 1u]) != 0) + return -1; + fprintf(stderr, "pn_tt: gdnc — conv constant in %u banks (%.1f MiB of copies)\n", + device->gdnc_constant_replicas, + (double)constant * layers * (device->gdnc_constant_replicas - 1u) / + (1024.0 * 1024.0)); + return 0; +} + +/** Copy `copy` of `layer`'s conv constant (0 = the original). */ +static const pn_tt_gdn_buffer *gdnc_constant_copy(const pn_tt_device *device, uint32_t layer, + uint32_t copy) +{ + if (copy == 0u || device->gdnc_constant_replicas <= 1u) + return &device->gdnc_constant[layer]; + return &device->gdnc_constant_replica[(size_t)layer * (device->gdnc_constant_replicas - 1u) + + copy - 1u]; +} + +static int gdnc_alloc_regions(pn_tt_device *device, const ft_tt_op_gdnc_request *conv, + const ft_tt_op_gdnc_request *delta) +{ + const size_t constant = ft_tt_op_gdnc_constant_bytes(conv); + uint32_t layer; + + device->gdnc_constant = calloc(device->gdn_shape.layer_count, + sizeof(*device->gdnc_constant)); + if (!device->gdnc_constant) + return -1; + for (layer = 0; layer < device->gdn_shape.layer_count; layer++) + if (gdnc_alloc_in(device, PN_TT_GDNC_ANY_BANK, constant, true, + &device->gdnc_constant[layer]) != 0) + return -1; + if (gdnc_alloc_constant_replicas(device, constant) != 0) + return -1; + + /* THE GATE BLOCK IS PINNED TO THE DELTA CONSTANT'S BANK. They share that operand's packed + * NoC coordinate word, exactly as the activation pair shares its own, and they fail the + * same way: the reader would pull 640 bytes from the right bank at an address belonging + * to nothing, the descriptor would retire clean, and every gate column would be whatever + * was there. Nothing on the device can catch it. */ + for (uint32_t slot = 0; slot < PN_TT_GDNC_STAGE_SLOTS; slot++) { + if (gdnc_alloc_in(device, PN_TT_GDNC_ANY_BANK, + ft_tt_op_gdnc_delta_constant_bytes(delta), true, + &device->gdnc_delta_constant[slot]) != 0 || + gdnc_alloc_in(device, device->gdnc_delta_constant[slot].bank, + ft_tt_op_gdnc_gate_bytes(delta), true, &device->gdnc_gate[slot]) != 0) + return -1; + device->gdnc_shadow_decay[slot] = calloc((size_t)device->gdn_shape.v_heads, + PN_TT_GDNC_TILE_BYTES); + device->gdnc_shadow_gate[slot] = calloc((size_t)device->gdn_shape.v_heads * + FT_TT_OP_GDNC_GATE_STRIDE, + sizeof(float)); + device->gdnc_shadow_valid[slot] = false; + device->gdnc_prestaged[slot].valid = false; + if (!device->gdnc_shadow_decay[slot] || !device->gdnc_shadow_gate[slot]) + return -1; + } + + /* The per-chunk host scratch: one decay tile per v-head, contiguous so the whole 128 KiB + * block is ONE write rather than `v_heads` of them, and one gate run per v-head. */ + device->gdnc_decay_stage = calloc((size_t)device->gdn_shape.v_heads, + PN_TT_GDNC_TILE_BYTES); + device->gdnc_gate_stage = calloc((size_t)device->gdn_shape.v_heads * + FT_TT_OP_GDNC_GATE_STRIDE, + sizeof(*device->gdnc_gate_stage)); + if (!device->gdnc_decay_stage || !device->gdnc_gate_stage) + return -1; + if (gdnc_upload_delta_constants(device) != 0) + return -1; + + fprintf(stderr, "pn_tt: gdnc — %u layers x %.2f MiB of row-replicated conv weight + band " + "= %.2f MiB, plus %.0f KiB of delta constants. The window and the " + "recurrent state are the gdn family's, byte for byte.\n", + device->gdn_shape.layer_count, (double)constant / (1024.0 * 1024.0), + (double)(constant * device->gdn_shape.layer_count) / (1024.0 * 1024.0), + (double)PN_TT_GDNC_STAGE_SLOTS * + (double)(ft_tt_op_gdnc_delta_constant_bytes(delta) + + ft_tt_op_gdnc_gate_bytes(delta)) / 1024.0); + return 0; +} + +/** + * How many members this run may split across. + * + * Clamped to the array and then shrunk until it divides BOTH quantities exactly — CONV's tile + * columns and DELTA's v-heads. Both, not either: one placement and one member set serve both + * phases, and a count that divided only one would leave the other running a ragged split whose + * longest member sets the descriptor's cost while the short ones idle. At Ornith that is + * `gcd(256, 32) = 32`, so 1/2/4/8/16 all survive and 3 collapses to 2. + */ +static uint32_t gdnc_member_count(const pn_tt_device *device) +{ + const uint32_t columns = device->gdn_shape.conv_dim / PN_TT_GDNC_TILE_DIM; + const uint32_t heads = device->gdn_shape.v_heads; + uint32_t members = pn_tt_env_u32(PN_TT_GDNC_CORES_ENV, PN_TT_GDNC_CORES_DEFAULT); + + if (members > PN_TT_GDNC_MAX_MEMBERS) + members = PN_TT_GDNC_MAX_MEMBERS; + if (columns == 0u || heads == 0u) + return 1u; + if (members > columns) + members = columns; + if (members > heads) + members = heads; + while (members > 1u && ((columns % members) != 0u || (heads % members) != 0u)) + members--; + return members; +} + +/** + * The core member `index + 1` arms on: entry `index` of `PN_TT_GDNC_MEMBER_CORE_LIST` when the + * list is set, else `first + index`. A list too short for the count returns UINT32_MAX, which + * stops the set there (the split then shrinks to a count that still divides). + */ +static uint32_t gdnc_member_core(uint32_t index, uint32_t first) +{ + const char *at = getenv(PN_TT_GDNC_MEMBER_CORE_LIST_ENV); + uint32_t position = 0u; + char *end; + + if (!at || !*at) + return first + index; + while (*at) { + const unsigned long core = strtoul(at, &end, 10); + + if (end == at) + break; + if (position++ == index) + return (uint32_t)core; + at = *end == ',' ? end + 1 : end; + } + return UINT32_MAX; +} + +/** + * Arm the EXTRA chunkwise residencies, if this run asked for a split. + * + * They take the SAME circular-buffer placement member 0 has, and that is a property of the + * family and not a convenience: `resolve_cb_pages()` reads `state_tiles` and `conv_taps` and + * nothing else, so a member owning sixteen columns places exactly what a member owning two + * hundred and fifty-six does. Sizing them any other way would be measuring a different + * residency from the one the model runs. + * + * A member that fails to arm is not fatal — the split shrinks to what came up — but the + * shrunken count must STILL DIVIDE BOTH quantities, so it walks back down to one that does + * rather than leaving a ragged set. Both facts are reported, because a silent shrink shows up + * only as a number that did not move. + */ +static void gdnc_arm_members(pn_tt_device *device, const ft_tt_op_gdnc_request *shape) +{ + const uint32_t columns = device->gdn_shape.conv_dim / PN_TT_GDNC_TILE_DIM; + const uint32_t heads = device->gdn_shape.v_heads; + const uint32_t asked = pn_tt_env_u32(PN_TT_GDNC_CORES_ENV, PN_TT_GDNC_CORES_DEFAULT); + const uint32_t members = gdnc_member_count(device); + const uint32_t first = pn_tt_env_u32(PN_TT_GDNC_MEMBER_CORE_FIRST_ENV, + PN_TT_GDNC_MEMBER_CORE_DEFAULT); + uint32_t up = 1u; + uint32_t armed; + uint32_t index; + + device->gdnc_members = 1u; + /* A COUNT THAT WAS SILENTLY REDUCED IS A KNOB THAT DID NOT ARM. `PN_TT_GDNC_CORES=3` runs + * a 2-way split and `=99` runs a 16-way one, and a sweep that did not print this would + * report two rungs as three. It prints only when the two differ, so the shipped default + * adds no line. */ + if (asked != members) + fprintf(stderr, "pn_tt: gdnc CORE SPLIT — requested %u members, running %u: the count " + "is clamped to %u and then shrunk until it divides BOTH %u tile " + "columns and %u v-heads\n", + asked, members, PN_TT_GDNC_MAX_MEMBERS, columns, heads); + if (members < 2u) + return; + for (index = 0; index + 1u < members; index++) { + pn_tt_residency *member = &device->gdnc_member[index]; + ft_tt_launch_cb_entry placed[FT_TT_OP_GDNC_CB_COUNT]; + ft_tt_op_gdnc_request request = *shape; + + request.phase = FT_TT_OP_GDNC_PHASE_CONV; + request.first_col = 0u; + request.col_count = 0u; + request.activation_in.tile_bytes = PN_TT_GDNC_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDNC_TILE_BYTES; + if (gdnc_member_core(index, first) == UINT32_MAX || + !pn_tt_prepare_family_on_core(device, member, PN_TT_GDNC_PROGRAM, "gdnc-member", + PN_TT_GDNC_MEMBER_CORE_FIRST_ENV, + gdnc_member_core(index, first), + PN_TT_ONCARD_TRIAD_MASK)) + break; + /* BEFORE THE START, because after it the binding is already in the kernel's runtime + * args. The block lives on MEMBER 0's core, which is the one the aggregated read + * addresses; a member's own slot is its index. + * + * RED PROOF: `gdnc_group_mute` leaves the LAST member unbound, so it runs its + * descriptors correctly and posts NOTHING. It is the arm that proves the group check + * reads every slot rather than only member 0's — the recorded twenty-eighth class one + * level in, where a shape that aggregated nothing would report every figure a working + * one reports. The await must TIME OUT naming that member, the shape must disarm, and + * the run must then finish `cmp` IDENTICAL at the per-member cost. */ + if (device->gdnc_group && + !(pn_tt_red("gdnc_group_mute") && index + 2u == members)) + pn_tt_residency_join_group(member, device->gdnc.core_x, device->gdnc.core_y, + PN_TT_GDNC_GROUP_ADDRESS, index + 1u); + if (ft_tt_op_gdnc_place_cbs(&request, placed, member->cb_buffer, + sizeof(member->cb_buffer), &member->cb_mask) != FT_TT_OK || + !pn_tt_start_family(member)) { + pn_tt_residency_release(member); + break; + } + up++; + } + /* A PARTIAL SET IS NOT A SMALLER SPLIT unless the smaller number still divides both. The + * members that came up past the legal count are STOPPED rather than left resident, so a + * later token cannot find a residency nothing addresses and no core is held by a triad + * this host will never feed. */ + armed = up; + while (armed > 1u && ((columns % armed) != 0u || (heads % armed) != 0u)) + armed--; + for (index = armed; index < up; index++) { + pn_tt_residency *member = &device->gdnc_member[index - 1u]; + + if (member->running) + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + } + device->gdnc_members = armed; + if (armed != members) + fprintf(stderr, "pn_tt: gdnc CORE SPLIT — asked for %u members, %u came up, %u divide " + "both %u columns and %u v-heads; the split runs at %u\n", + members, up, armed, columns, heads, armed); + if (armed < 2u) { + fprintf(stderr, "pn_tt: gdnc CORE SPLIT — NOT SPLIT; the family runs whole on core " + "%u\n", device->gdnc.core_index); + /* Member 0 armed the post on a count that then did not come up, so it has already + * paid the `line` -> `window` demotion for a group that does not exist. Nothing can + * undo that this run — the binding is in the kernel — but the SHAPE is cleared so + * `gdnc_group_ops` cannot read as armed-and-idle when the truth is not-a-group. */ + device->gdnc_group = false; + return; + } + fprintf(stderr, "pn_tt: gdnc CORE SPLIT — every descriptor over %u members, core %u then " + "%u ... %u; PHASE CONV %u of %u tile columns each, PHASE DELTA %u of %u " + "v-heads each\n", + armed, device->gdnc.core_index, gdnc_member_core(0u, first), + gdnc_member_core(armed - 2u, first), columns / armed, + columns, heads / armed, heads); + + /* THE GROUP BLOCK, WRITTEN AFTER THE MEMBER COUNT IS FINAL AND BEFORE ANY DESCRIPTOR IS + * ISSUED. The matmul group writes its block before any member is kicked; here the count + * is not known until the last member has either come up or failed, and the ordering is + * still safe for the two reasons that mattered there: no descriptor is in flight during + * arming, so nothing can post into an un-magicked block, and `ft_tt_queue_group_init()` + * ZEROES every slot, so a sequence a previous residency left cannot be read as this + * run's. + * + * AND THE MAGIC IS CHECKED ONCE, HERE. What it catches is the WRONG ADDRESS, which is a + * constant of the run and not a per-poll risk — re-reading the header on every poll is + * exactly the cost the slots-only read exists to remove. A failure DISARMS the shape + * rather than aborting: the per-member await is still correct, just slower, and a family + * that cannot read its own block must not then spin on it for a timeout. */ + if (device->gdnc_group) { + ft_tt_queue_group_slot slots[PN_TT_GDNC_MAX_MEMBERS]; + + if (ft_tt_queue_group_init(&device->runtime.noc, device->gdnc.core_x, + device->gdnc.core_y, PN_TT_GDNC_GROUP_ADDRESS, + armed) != FT_TT_OK || + ft_tt_queue_group_read(&device->runtime.noc, device->gdnc.core_x, + device->gdnc.core_y, PN_TT_GDNC_GROUP_ADDRESS, armed, + slots) != FT_TT_OK) { + fprintf(stderr, "pn_tt: gdnc GROUP COMPLETION — the block at 0x%08x on core %u " + "did not write and read back with its magic. DISARMED; the split " + "keeps the per-member await.\n", + (unsigned)PN_TT_GDNC_GROUP_ADDRESS, device->gdnc.core_index); + device->gdnc_group = false; + return; + } + fprintf(stderr, "pn_tt: gdnc GROUP COMPLETION — %u members post into one block at " + "0x%08x on core %u; the await is member 0's count plus ONE %u-byte " + "read, not %u poll loops\n", + armed, (unsigned)PN_TT_GDNC_GROUP_ADDRESS, device->gdnc.core_index, + (unsigned)((armed * 8u + 15u) & ~15u), armed); + } +} + +int pn_tt_device_arm_gdnc(pn_tt_device *device) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_GDNC_CB_COUNT]; + ft_tt_op_gdnc_request conv; + ft_tt_op_gdnc_request delta; + const char *want; + + if (!device) + return -1; + /* ON BY DEFAULT: only an explicit `0` declines. See PN_TT_GDNC_ENV for the gate that + * bought this default and for why the chunk length is 16 and not 32. */ + want = getenv(PN_TT_GDNC_ENV); + if (want && *want && atoi(want) == 0) + return 0; + /* IT OWNS NO CARRIED STATE. The window and the recurrent state it advances belong to the + * decode family, which is what makes the prefill->decode seam free — so without that + * family there is nothing for this one to carry. */ + if (!pn_tt_gdn_is_armed(device)) { + fprintf(stderr, "pn_tt: %s needs the gdn family armed — the chunkwise mixers share " + "its conv window and recurrent state byte for byte. Add `gdn` to " + "PN_TT_ONCARD.\n", PN_TT_GDNC_ENV); + return 0; + } + + conv = gdnc_request(device, FT_TT_OP_GDNC_PHASE_CONV, PN_TT_GDNC_TILE_DIM); + delta = gdnc_request(device, FT_TT_OP_GDNC_PHASE_DELTA, PN_TT_GDNC_TILE_DIM); + /* The placement needs operands only to pass the encoder's "is this set" test. */ + conv.activation_in.tile_bytes = PN_TT_GDNC_TILE_BYTES; + conv.activation_out.tile_bytes = PN_TT_GDNC_TILE_BYTES; + if (!ft_tt_op_gdnc_shape_is_supported(&conv) || !ft_tt_op_gdnc_shape_is_supported(&delta)) { + fprintf(stderr, "pn_tt: the gdnc family does not serve S=%u, %u v-heads over %u " + "k-heads, conv_dim %u, %u taps\n", + device->gdn_shape.state_size, device->gdn_shape.v_heads, + device->gdn_shape.k_heads, device->gdn_shape.conv_dim, + device->gdn_shape.conv_taps); + return 0; + } + + if (!pn_tt_prepare_family(device, &device->gdnc, PN_TT_GDNC_PROGRAM, "gdnc", + PN_TT_CORE_GDNC_ENV, PN_TT_CORE_GDNC_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return 0; + if (ft_tt_op_gdnc_place_cbs(&conv, placed, device->gdnc.cb_buffer, + sizeof(device->gdnc.cb_buffer), + &device->gdnc.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdnc circular buffers do not fit at S=%u, %u taps\n", + device->gdn_shape.state_size, device->gdn_shape.conv_taps); + pn_tt_residency_release(&device->gdnc); + return 0; + } + if (gdnc_alloc_regions(device, &conv, &delta) != 0) { + fprintf(stderr, "pn_tt: the gdnc family could not be staged in card DRAM\n"); + pn_tt_residency_release(&device->gdnc); + /* A HALF-ALLOCATED table is worse than none: a later reader would find addresses for + * some layers and zeros for the rest. The DRAM is not reclaimed — the allocator frees + * LIFO by design — and a caller that reaches here is not continuing on this path. */ + free(device->gdnc_constant); + device->gdnc_constant = NULL; + return 0; + } + /* THE COMPLETION SHAPE IS DECIDED BEFORE MEMBER 0 STARTS, because the binding rides in + * the runtime args and there is no second chance to set it. It is decided on the count + * this run WILL arm rather than on the knob alone: a family that does not split has no + * group, and arming the post anyway would cost it the `line` await shape for nothing. */ + { + const char *ask = getenv(PN_TT_GDNC_GROUP_ENV); + + device->gdnc_group = ask && strcmp(ask, PN_TT_GDNC_GROUP_ACK) == 0 && + gdnc_member_count(device) >= 2u; + /* A `1` IS REFUSED AND SAYS SO. Silently declining would make an arm that thought it + * measured the group shape measure the per-member one and report it as the group's — + * which is the shape of every vacuous gate this backend has paid for. */ + if (ask && *ask && strcmp(ask, PN_TT_GDNC_GROUP_ACK) != 0 && strcmp(ask, "0") != 0) + fprintf(stderr, "pn_tt: %s=%s is REFUSED. The group post is an accelerant of a " + "defect that has wedged two cores at instrn_buf_status 0x66 and " + "costs a whole-card `ft_tt_core recover --rung 3`. Spell it " + "`%s` if that is what you mean. The split keeps the per-member " + "await.\n", + PN_TT_GDNC_GROUP_ENV, ask, PN_TT_GDNC_GROUP_ACK); + } + device->gdnc_sequence = 0u; + if (device->gdnc_group) + pn_tt_residency_join_group(&device->gdnc, device->gdnc.core_x, device->gdnc.core_y, + PN_TT_GDNC_GROUP_ADDRESS, 0u); + if (!pn_tt_start_family(&device->gdnc)) + return 0; + /* AFTER member 0 is up, so a member cannot be armed onto a core member 0 is about to take + * and so `pn_tt_core_holder()` sees the whole set as it grows. */ + gdnc_arm_members(device, &conv); + return 0; +} + +/** + * One phase's line: the CLEAN per-descriptor figure, and the recovery that is not in it. + * + * `rearms` prints even at 0 — a contaminated figure that looks like a clean one is the whole + * defect this split exists for, and an absent count reads as a clean run. Silent when the + * phase ran nothing at all. + */ +static void gdnc_report_phase(const pn_tt_device *device, const char *name, uint32_t phase) +{ + const uint64_t ops = device->gdnc_phase_ops[phase]; + const uint64_t recovered = device->gdnc_phase_recovered_ops[phase]; + + if (ops == 0u && recovered == 0u) + return; + fprintf(stderr, "pn_tt: gdnc — %-5s %.4f s over %llu clean descriptors (%.1f us each), " + "%llu replayed over %llu re-arms (%.4f s)\n", + name, device->gdnc_phase_seconds[phase], (unsigned long long)ops, + ops ? 1e6 * device->gdnc_phase_seconds[phase] / (double)ops : 0.0, + (unsigned long long)recovered, + (unsigned long long)device->gdnc_phase_rearms[phase], + device->gdnc_phase_recovered_seconds[phase]); +} + +/** + * THE STAGING LINE — the term that was inside `sequential` and outside every phase counter. + * + * `pn_tt_gdnc_delta_oncard_at()` transposes `v_heads` decay runs into ratio tiles and gate + * runs into the flat gate block, then writes both to DRAM, all BEFORE `gdnc_submit()` marks + * its start. So the DELTA line's "us each" has never contained it, and the prefill report's + * `sequential` term has always contained it un-named. This prints it. + * + * BUILD AND UPLOAD ARE SEPARATE CLOCKS ON PURPOSE. They answer different questions: build is + * host arithmetic on a two-core box and upload is the x1 Gen2 link, and a smaller operand + * moves the second and not necessarily the first. Reporting one pooled number would make a + * lever that halves the bytes look like it half-worked. + * + * Silent when nothing staged, for the same reason `gdnc_report_phase()` is — except that + * silence here is itself readable, because a run that issued DELTA descriptors and printed + * no STAGE line is a run where this bracket did not arm. The runner scores on the token this + * function EMITS (the recorded thirtieth vacuous-gate class), so the DELTA descriptor count + * is printed BESIDE the stage count and the two must agree. + */ +static void gdnc_report_stage(const pn_tt_device *device) +{ + const uint64_t ops = device->gdnc_stage_ops; + const double total = device->gdnc_stage_build_seconds + device->gdnc_stage_upload_seconds; + /* DECIMAL MB, because every link rate this program has ever recorded (200-390 MB/s, the + * 286.3 PIO figure) is decimal and a MiB here would read as a 4.9% link regression. */ + const double megabytes = (double)device->gdnc_stage_bytes / 1e6; + + if (ops == 0u) + return; + fprintf(stderr, "pn_tt: gdnc — STAGE %.4f s over %llu stagings (%.1f us each) = " + "%.4f s build (%.1f us) + %.4f s upload (%.1f us); %.1f MB at %.1f MB/s; " + "DELTA descriptors %llu clean + %llu replayed\n", + total, (unsigned long long)ops, 1e6 * total / (double)ops, + device->gdnc_stage_build_seconds, + 1e6 * device->gdnc_stage_build_seconds / (double)ops, + device->gdnc_stage_upload_seconds, + 1e6 * device->gdnc_stage_upload_seconds / (double)ops, + megabytes, + device->gdnc_stage_upload_seconds > 0.0 + ? megabytes / device->gdnc_stage_upload_seconds : 0.0, + (unsigned long long)device->gdnc_phase_ops[FT_TT_OP_GDNC_PHASE_DELTA], + (unsigned long long)device->gdnc_phase_recovered_ops[FT_TT_OP_GDNC_PHASE_DELTA]); + /* THE TWO P3 STAGING LEVERS, as counts the arm cannot fake: how many DRAM writes the + * staging made (2 a staging when full; more, smaller ones when diffed) and how many + * stagings ran inside a split's card time. */ + fprintf(stderr, "pn_tt: gdnc — STAGE SHAPE %llu DRAM writes (%.2f a staging), %llu of %llu " + "stagings hidden under a descriptor\n", + (unsigned long long)device->gdnc_stage_writes, + (double)device->gdnc_stage_writes / (double)ops, + (unsigned long long)device->gdnc_stage_hidden_ops, (unsigned long long)ops); + if (device->gdnc_stage_verify_checks != 0u) + fprintf(stderr, "pn_tt: gdnc — STAGE VERIFY %llu units differ from the builders over " + "%llu read-backs\n", + (unsigned long long)device->gdnc_stage_verify_mismatches, + (unsigned long long)device->gdnc_stage_verify_checks); +} + +void pn_tt_gdnc_release(pn_tt_device *device) +{ + if (!device) + return; + /* WHAT THE TWO PHASES COST, PER DESCRIPTOR, measured directly rather than subtracted + * between two runs. `oncard_seconds` is a sum over every family and a subtraction cannot + * tell CONV from DELTA — which are the two things a re-pricing has to separate, because + * one is flat in the chunk size and the other divides by head. */ + gdnc_report_phase(device, "CONV", FT_TT_OP_GDNC_PHASE_CONV); + gdnc_report_phase(device, "DELTA", FT_TT_OP_GDNC_PHASE_DELTA); + gdnc_report_stage(device); + /* THE SPLIT'S OWN NUMBERS, one line per member, printed only when a split ran. `ops` + * prints beside `seconds` so a member whose clock never started is `ops != 0 && + * seconds == 0` and reads as the recorded twenty-eighth vacuous-gate class rather than as + * a free member. */ + /* THE COMPLETION SHAPE AS A NUMBER, NOT AS AN ABSENCE. A run that armed the group prints + * one await for the whole set; a run that did not prints N. Printing the shape's own + * clock beside the member rows is what stops the member table from being read as if both + * shapes produced it: on the group shape, member 0's row IS the group await and members + * 1..N-1 are silent by construction. */ + if (device->gdnc_group_ops != 0u) + fprintf(stderr, "pn_tt: gdnc — GROUP %.4f s over %llu group awaits (%.1f us each) " + "across %u members; the per-member await did not run\n", + device->gdnc_group_seconds, (unsigned long long)device->gdnc_group_ops, + 1e6 * device->gdnc_group_seconds / (double)device->gdnc_group_ops, + device->gdnc_members); + else if (device->gdnc_group) + fprintf(stderr, "pn_tt: gdnc — GROUP armed and never used: 0 group awaits\n"); + if (device->gdnc_members > 1u) { + uint32_t index; + + for (index = 0; index < device->gdnc_members; index++) { + const uint64_t ops = device->gdnc_member_ops[index]; + const double seconds = device->gdnc_member_seconds[index]; + const uint32_t core = index == 0u ? device->gdnc.core_index + : device->gdnc_member[index - 1u].core_index; + + fprintf(stderr, "pn_tt: gdnc split member %u core %3u — %.4f s of await over " + "%llu descriptors (%.1f us each)\n", + index, core, seconds, (unsigned long long)ops, + ops ? seconds * 1e6 / (double)ops : 0.0); + } + } + { + uint32_t index; + + for (index = 0; index + 1u < device->gdnc_members; index++) { + pn_tt_residency *member = &device->gdnc_member[index]; + + if (member->running) + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + } + device->gdnc_members = 0u; + } + if (device->gdnc.running) + pn_tt_residency_stop(&device->gdnc); + pn_tt_residency_release(&device->gdnc); + free(device->gdnc_constant); + free(device->gdnc_constant_replica); + device->gdnc_constant_replica = NULL; + device->gdnc_constant_replicas = 1u; + free(device->gdnc_decay_stage); + free(device->gdnc_gate_stage); + pn_tt_stage_pipeline_stop(&device->gdnc_stage_pipeline); + for (uint32_t slot = 0; slot < PN_TT_GDNC_STAGE_SLOTS; slot++) { + free(device->gdnc_shadow_decay[slot]); + free(device->gdnc_shadow_gate[slot]); + device->gdnc_shadow_decay[slot] = NULL; + device->gdnc_shadow_gate[slot] = NULL; + device->gdnc_shadow_valid[slot] = false; + device->gdnc_prestaged[slot].valid = false; + } + device->gdnc_overlap_fn = NULL; + device->gdnc_overlap_aux_fn = NULL; + device->gdnc_constant = NULL; + device->gdnc_decay_stage = NULL; + device->gdnc_gate_stage = NULL; +} + +/* ================================================================ + * The conv constant — the TRANSPOSE, ROW-REPLICATED, and the band behind it + * ================================================================ */ + +int pn_tt_gdnc_conv_weight_upload(pn_tt_device *device, uint32_t layer, const float *weight) +{ + const uint32_t taps = device->gdn_shape.conv_taps; + const uint32_t cols = device->gdn_shape.conv_dim / PN_TT_GDNC_TILE_DIM; + /* ONE TILE COLUMN AT A TIME, not one `[32, conv_dim]` strip: every row of a plane carries + * the same `taps`-strided gather, so a tile is a 32x32 plane and the whole layer never + * needs a 1 MiB row-major staging buffer on a two-core, 7 GiB host. */ + const bool row0 = pn_tt_red("gdnc_weight_row0"); + float plane[PN_TT_GDNC_TILE_DIM * PN_TT_GDNC_TILE_DIM]; + unsigned char *tiles; + unsigned char *band; + uint32_t tap; + uint32_t copy; + int rc = 0; + + if (!pn_tt_gdnc_is_armed(device) || !weight || layer >= device->gdn_shape.layer_count) + return -1; + tiles = malloc((size_t)cols * PN_TT_GDNC_TILE_BYTES); + band = malloc(ft_tt_op_gdnc_band_bytes(taps)); + if (!tiles || !band) { + free(tiles); + free(band); + return -1; + } + + for (tap = 0; tap < taps && rc == 0; tap++) { + uint32_t column; + + for (column = 0; column < cols; column++) { + uint32_t row; + uint32_t lane; + + memset(plane, 0, sizeof(plane)); + /* THE ROW REPLICATION, and the whole of it: `w[c][tap]` at EVERY row, because the + * tap coefficient is constant across the chunk's positions and the broadcast + * surface is BROADCAST_COL only — there is no row broadcast. + * RED PROOF: `gdnc_weight_row0` writes row 0 alone, which is the DECODE family's + * plane. It is right at row 0 of every chunk and zero at rows 1..C-1, so it is a + * mutation a single-POSITION comparison cannot see. */ + for (row = 0; row < (row0 ? 1u : PN_TT_GDNC_TILE_DIM); row++) + for (lane = 0; lane < PN_TT_GDNC_TILE_DIM; lane++) + plane[row * PN_TT_GDNC_TILE_DIM + lane] = + weight[((size_t)column * PN_TT_GDNC_TILE_DIM + lane) * taps + tap]; + if (ft_tt_tilize_strip(plane, sizeof(float), PN_TT_GDNC_TILE_DIM, + PN_TT_GDNC_TILE_DIM, + (size_t)PN_TT_GDNC_TILE_DIM * sizeof(float), + tiles + (size_t)column * PN_TT_GDNC_TILE_BYTES) != + FT_TT_TILIZE_OK) { + rc = -1; + break; + } + } + /* RED `gdnc_replica_zero`: the copies' weight planes are ZEROED, not written (not + * merely left unwritten: DRAM keeps the previous process's copies, which made an + * "unwritten" red VACUOUS on silicon) — must DIFFER: the members read their copy. */ + for (copy = 0; rc == 0 && copy < device->gdnc_constant_replicas; copy++) + rc = copy != 0u && pn_tt_red("gdnc_replica_zero") + ? pn_tt_zero_dram(device, gdnc_constant_copy(device, layer, copy)->bank, + gdnc_constant_copy(device, layer, copy)->address + + (size_t)tap * cols * PN_TT_GDNC_TILE_BYTES, + (size_t)cols * PN_TT_GDNC_TILE_BYTES) + : pn_tt_write_dram(device, gdnc_constant_copy(device, layer, copy)->bank, + gdnc_constant_copy(device, layer, copy)->address + + (size_t)tap * cols * PN_TT_GDNC_TILE_BYTES, + tiles, (size_t)cols * PN_TT_GDNC_TILE_BYTES); + } + + /* THE BAND RIDES AT THE END OF THE WEIGHT PLANES, at tile `taps * conv_tile_cols`. It is a + * `taps`-dependent CONSTANT — the same eight tiles in every layer of every model with this + * tap count — and it rides here rather than in a sixth DRAM endpoint because + * `FT_TT_QUEUE_PARAM_WORDS` is 16 for every family. */ + if (rc == 0 && ft_tt_op_gdnc_build_band(taps, band, ft_tt_op_gdnc_band_bytes(taps)) != + FT_TT_OK) + rc = -1; + for (copy = 0; rc == 0 && copy < device->gdnc_constant_replicas; copy++) + rc = pn_tt_write_dram(device, gdnc_constant_copy(device, layer, copy)->bank, + gdnc_constant_copy(device, layer, copy)->address + + (size_t)taps * cols * PN_TT_GDNC_TILE_BYTES, + band, ft_tt_op_gdnc_band_bytes(taps)); + free(tiles); + free(band); + return rc; +} + +/* ================================================================ + * Submission + * ================================================================ */ + +/** + * Retire a residency that refused a descriptor, and arm a FRESH one on the same core. + * + * The DRAM is untouched: the constants are allocated once and a retired core owns none of + * them, and the carried state belongs to the gdn family. What is re-placed is the + * circular-buffer geometry, which is a property of the shape rather than of the descriptor. + * A residency is BOUNDED on purpose — `idle_spin_limit` retires a kernel whose host has + * stopped feeding it — so this fires for a serve that idles between requests as well as for a + * malformed descriptor. + */ +static bool pn_tt_gdnc_rearm(pn_tt_device *device) +{ + ft_tt_launch_cb_entry placed[FT_TT_OP_GDNC_CB_COUNT]; + ft_tt_op_gdnc_request request = gdnc_request(device, FT_TT_OP_GDNC_PHASE_CONV, + PN_TT_GDNC_TILE_DIM); + + fprintf(stderr, "pn_tt: the gdnc residency refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", device->gdnc.core_index); + pn_tt_residency_stop(&device->gdnc); + pn_tt_residency_release(&device->gdnc); + device->oncard_restarts++; + /* NOTHING BOOTS FIRMWARE ONTO A CORE THAT DID NOT RETIRE. Measured on silicon + * 2026-08-21: a refused descriptor leaves the compute thread spinning out its bound, the + * stop reported `processors still running: 0x4`, and the boot that followed WEDGED core 90 + * at `instrn_buf_status = 0x66` — a state only `ft_tt_core recover --rung 3` and sudo can + * clear. A failed descriptor is a bad token; a wedged Tensix takes the process AND the next + * one, because the next boot lands on the same core. */ + if (!pn_tt_residency_retired(&device->gdnc)) { + fprintf(stderr, "pn_tt: the gdnc residency did not retire — REFUSING to arm a fresh " + "one on core %u\n", device->gdnc.core_index); + return false; + } + + request.activation_in.tile_bytes = PN_TT_GDNC_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDNC_TILE_BYTES; + if (!pn_tt_prepare_family(device, &device->gdnc, PN_TT_GDNC_PROGRAM, "gdnc", + PN_TT_CORE_GDNC_ENV, PN_TT_CORE_GDNC_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_gdnc_place_cbs(&request, placed, device->gdnc.cb_buffer, + sizeof(device->gdnc.cb_buffer), + &device->gdnc.cb_mask) != FT_TT_OK) { + pn_tt_residency_release(&device->gdnc); + return false; + } + /* THE GROUP BINDING IS RE-APPLIED, because `pn_tt_residency_release()` zeroes the handle + * and a fresh residency posts nothing. A member that came back silently un-bound would + * make every LATER group await time out — a whole run lost to one recovered descriptor, + * and the symptom would point at the group block rather than at the re-arm. */ + if (device->gdnc_group) + pn_tt_residency_join_group(&device->gdnc, device->gdnc.core_x, device->gdnc.core_y, + PN_TT_GDNC_GROUP_ADDRESS, 0u); + return pn_tt_start_family(&device->gdnc); +} + +/** + * Retire ONE split member and arm a fresh one ON THE CORE IT ALREADY HELD. + * + * The core is captured BEFORE the release, because `pn_tt_residency_release()` zeroes the + * handle — re-reading `core_index` afterwards would arm member 3 on core 0. Member 0 is the + * `gdnc` residency itself and goes through `pn_tt_gdnc_rearm()`, which is the same operation + * spelled against the environment default. + */ +static bool gdnc_member_rearm(pn_tt_device *device, uint32_t member_index) +{ + pn_tt_residency *member; + ft_tt_launch_cb_entry placed[FT_TT_OP_GDNC_CB_COUNT]; + ft_tt_op_gdnc_request request; + uint32_t core; + + if (member_index == 0u) + return pn_tt_gdnc_rearm(device); + member = &device->gdnc_member[member_index - 1u]; + core = member->core_index; + fprintf(stderr, "pn_tt: gdnc member %u refused a descriptor — retiring it by ASKING and " + "arming a fresh one on core %u\n", member_index, core); + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + device->oncard_restarts++; + /* The same refusal as `pn_tt_gdnc_rearm()`'s, for the same reason. */ + if (!pn_tt_residency_retired(member)) { + fprintf(stderr, "pn_tt: gdnc member %u did not retire — REFUSING to arm a fresh one " + "on core %u\n", member_index, core); + return false; + } + + request = gdnc_request(device, FT_TT_OP_GDNC_PHASE_CONV, PN_TT_GDNC_TILE_DIM); + request.activation_in.tile_bytes = PN_TT_GDNC_TILE_BYTES; + request.activation_out.tile_bytes = PN_TT_GDNC_TILE_BYTES; + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_GDNC_PROGRAM, "gdnc-member", + PN_TT_GDNC_MEMBER_CORE_FIRST_ENV, core, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_gdnc_place_cbs(&request, placed, member->cb_buffer, sizeof(member->cb_buffer), + &member->cb_mask) != FT_TT_OK) { + pn_tt_residency_release(member); + return false; + } + /* The same re-binding, for the same reason as `pn_tt_gdnc_rearm()`'s, and on the SLOT + * this member already held — the index, like the core, is captured before the release. */ + if (device->gdnc_group) + pn_tt_residency_join_group(member, device->gdnc.core_x, device->gdnc.core_y, + PN_TT_GDNC_GROUP_ADDRESS, member_index); + return pn_tt_start_family(member); +} + +/** The whole descriptor's slice count for a phase: CONV owns tile columns, DELTA owns + * v-heads. Both are addressed through the SAME two request words, which is why one function + * answers for both and why the split count has to divide them both. */ +static uint32_t gdnc_split_total(const ft_tt_op_gdnc_request *request) +{ + if (request->phase == FT_TT_OP_GDNC_PHASE_DELTA) + return request->v_heads; + return request->conv_dim / PN_TT_GDNC_TILE_DIM; +} + +/** + * ONE descriptor over a core split: one slice per member, ALL ENQUEUED BEFORE ANY IS AWAITED. + * + * The two loops being separate is the measurement and not a style. Enqueue-wait per member + * would run `members` SERIAL descriptors and print the flat line "it does not parallelise", + * produced by this function rather than by the card — the failure the decode family's core + * scaling probe names, and the reason its loops are split the same way. + * + * NO TWO MEMBERS TOUCH THE SAME DRAM BYTE. PHASE CONV: column `c` reads only the input tile, + * window planes and weight planes AT COLUMN `c` (`ftgc_plane_tile()` is `tap*conv_tile_cols + + * column`) and writes only column `c`'s conv_out tile and its own window rows. The band tiles + * are the one shared operand and they are read-only constants. PHASE DELTA: the carried state + * is head-major and `y` is scattered at row granularity, which is the decode family's own + * argument. `foundry/tt/tests/test_phase6_gdnc.c` proves the CONV case on silicon — four + * column slices byte-for-byte against one whole descriptor. + */ +/** + * Run the one-shot overlap hook, if any, and return the seconds it took. + * + * Called between a split's enqueue and its await, so host staging runs while the card works. + * Its seconds are RETURNED so the caller can keep them OUT of the phase clock — the stage line + * already books them, and the G0 attribution must not count one second twice. A failed hook + * is remembered in `*failed` and the await still runs: an enqueued descriptor is always + * awaited, or that member's cursor desynchronises for the rest of the run. + */ +static double gdnc_run_overlap(pn_tt_device *device, int *failed) +{ + int (*fn)(void *) = device->gdnc_overlap_fn; + void *context = device->gdnc_overlap_context; + int (*aux)(void *) = device->gdnc_overlap_aux_fn; + void *aux_context = device->gdnc_overlap_aux_context; + double at; + + if (!fn && !aux) + return 0.0; + device->gdnc_overlap_fn = NULL; + device->gdnc_overlap_context = NULL; + device->gdnc_overlap_aux_fn = NULL; + device->gdnc_overlap_aux_context = NULL; + at = pn_tt_now_seconds(); + /* A FAILED HOOK IS NOT A FAILED DESCRIPTOR. The hook only prestages; a slot it did not + * stage is staged inline by its own descriptor, exactly as with the overlap off. */ + if (fn) { + if (fn(context) == 0) + device->gdnc_stage_hidden_ops++; + else + fprintf(stderr, "pn_tt: a gdnc overlap hook failed — that slot stages inline\n"); + } + /* The aux slot's work is a READ its owner redoes inline when it did not happen. */ + if (aux) + (void)aux(aux_context); + (void)failed; + return pn_tt_now_seconds() - at; +} + +void pn_tt_gdnc_set_overlap(pn_tt_device *device, int (*fn)(void *context), void *context) +{ + if (!device) + return; + device->gdnc_overlap_fn = fn; + device->gdnc_overlap_context = context; +} + +void pn_tt_gdnc_set_overlap_aux(pn_tt_device *device, int (*fn)(void *context), void *context) +{ + if (!device) + return; + device->gdnc_overlap_aux_fn = fn; + device->gdnc_overlap_aux_context = context; +} + +static int gdnc_submit_split(pn_tt_device *device, const ft_tt_op_gdnc_request *request) +{ + const uint32_t members = device->gdnc_members; + const uint32_t per_member = gdnc_split_total(request) / members; + uint32_t words[PN_TT_GDNC_MAX_MEMBERS][FT_TT_OP_GDNC_WORDS]; + pn_tt_residency *set[PN_TT_GDNC_MAX_MEMBERS]; + bool bad[PN_TT_GDNC_MAX_MEMBERS]; + uint64_t rearms = 0; + double started; + double hook_seconds = 0.0; + int hook_failed = 0; + uint32_t index; + int failed = 0; + + memset(bad, 0, sizeof(bad)); + for (index = 0; index < members; index++) { + ft_tt_op_gdnc_request sliced = *request; + + sliced.first_col = index * per_member; + sliced.col_count = per_member; + if (request->phase == FT_TT_OP_GDNC_PHASE_CONV && device->gdnc_constant_replicas > 1u) + sliced.constant = gdnc_operand( + device, gdnc_constant_copy(device, device->gdnc_conv_layer, + index % device->gdnc_constant_replicas)); + if (ft_tt_op_gdnc_parameters(&sliced, words[index]) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdnc phase-%u slice [%u, %u) is one the family " + "refuses\n", sliced.phase, sliced.first_col, + sliced.first_col + sliced.col_count); + return -1; + } + set[index] = index == 0u ? &device->gdnc : &device->gdnc_member[index - 1u]; + } + + started = pn_tt_now_seconds(); + if (device->gdnc_group) { + uint32_t token[PN_TT_GDNC_MAX_MEMBERS]; + const uint32_t sequence = ++device->gdnc_sequence; + double at; + + memset(token, 0, sizeof(token)); + for (index = 0; index < members; index++) + if (pn_tt_residency_enqueue_group(set[index], FT_TT_QUEUE_OP_GDN_CHUNK, + words[index], 1u, sequence, &token[index]) != 0) + failed = bad[index] = 1; + hook_seconds = gdnc_run_overlap(device, &hook_failed); + /* ONE AWAIT FOR THE WHOLE SET. It is booked against MEMBER 0's row so `us each` stays + * the per-descriptor figure it has always been on both shapes — and the group's own + * clock is printed separately, so a reader can tell which shape produced it rather + * than inferring it from a number that moved. */ + at = pn_tt_now_seconds(); + if (!failed && pn_tt_residency_await_group(set, token, members, sequence, + PN_TT_OP_TIMEOUT_MS) != 0) { + failed = 1; + /* FAIL-SAFE, NOT FAIL-SLOW. A group await that did not satisfy has cost one + * timeout; leaving the shape armed would cost one PER DESCRIPTOR for the rest of + * the prompt — 3,672 of them — and the per-member await is still CORRECT, just + * slower. So the shape retires itself after the first failure and says so. This + * is also what bounds `PN_TT_RED=gdnc_group_mute`, which is otherwise a red arm + * that takes hours to report. */ + fprintf(stderr, "pn_tt: gdnc GROUP COMPLETION FAILED at sequence %u — DISARMING " + "the shape. The rest of this run awaits per member, which is the " + "same answer at the pre-group cost.\n", sequence); + device->gdnc_group = false; + } + device->gdnc_member_seconds[0] += pn_tt_now_seconds() - at; + device->gdnc_member_ops[0] += 1u; + device->gdnc_group_seconds += pn_tt_now_seconds() - at; + device->gdnc_group_ops += 1u; + /* WHICH MEMBER, ASKED ONLY ONCE THE SET HAS ALREADY FAILED. The group read says the + * set is not satisfied and names the first member that is not; the re-arm below needs + * to know about ALL of them, and one per-member await each is free on a path that is + * already returning an error. This is also what keeps every member's cursor in step: + * an enqueue that reached the ring has a descriptor the card will run whatever this + * host does next, and leaving it un-awaited desynchronises that member for the rest + * of the run. */ + if (failed) + for (index = 0; index < members; index++) + if (pn_tt_residency_await(set[index], PN_TT_OP_TIMEOUT_MS) != 0) + bad[index] = 1; + } else { + for (index = 0; index < members; index++) + if (pn_tt_residency_enqueue(set[index], FT_TT_QUEUE_OP_GDN_CHUNK, words[index], + 1u) != 0) + failed = bad[index] = 1; + hook_seconds = gdnc_run_overlap(device, &hook_failed); + /* EVERY member is awaited even after one has failed. An enqueue that got as far as the + * ring has a descriptor the card will run whatever this host does next, and leaving it + * un-awaited desynchronises that member's cursor for the rest of the run. */ + for (index = 0; index < members; index++) { + const double at = pn_tt_now_seconds(); + + if (pn_tt_residency_await(set[index], PN_TT_OP_TIMEOUT_MS) != 0) + failed = bad[index] = 1; + device->gdnc_member_seconds[index] += pn_tt_now_seconds() - at; + device->gdnc_member_ops[index] += 1u; + } + } + + /* PER-MEMBER RE-ARM AND REPLAY, only where this function would otherwise return -1. + * Replaying ONE member's slice is the same computation because a refused descriptor leaves + * no circular-buffer traffic behind it — the reader aborts before the rebase — and because + * the slices are disjoint in DRAM. A member that succeeded is not touched. */ + if (failed) { + failed = 0; + for (index = 0; index < members; index++) { + if (!bad[index]) + continue; + rearms++; + if (!gdnc_member_rearm(device, index) || + pn_tt_residency_submit(set[index], FT_TT_QUEUE_OP_GDN_CHUNK, words[index], + PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + } + { + /* THE HOOK'S SECONDS ARE THE STAGE LINE'S, not this phase's. */ + const double elapsed = pn_tt_now_seconds() - started - hook_seconds; + + device->oncard_seconds += elapsed; + device->oncard_ops += 1u; + /* ONE descriptor's worth of accounting for the whole set, so `us each` stays the + * per-DESCRIPTOR figure it has always been and a split is visible as that number + * falling rather than as the op count multiplying. */ + if (request->phase < 3u) { + if (rearms == 0u) { + device->gdnc_phase_seconds[request->phase] += elapsed; + device->gdnc_phase_ops[request->phase] += 1u; + } else { + device->gdnc_phase_recovered_seconds[request->phase] += elapsed; + device->gdnc_phase_recovered_ops[request->phase] += 1u; + device->gdnc_phase_rearms[request->phase] += rearms; + } + } + } + return (failed || hook_failed) ? -1 : 0; +} + +/** One descriptor, with the same re-arm-once recovery the other on-card families use. Safe + * for the same reason the decode family's is: every address in the parameter block is a DRAM + * one and the carried state is untouched by retiring the core. */ +static int gdnc_submit(pn_tt_device *device, const ft_tt_op_gdnc_request *request) +{ + uint32_t words[FT_TT_OP_GDNC_WORDS]; + uint64_t rearms = 0; + double started; + double elapsed; + + /* THE CORE SPLIT TAKES EVERY DESCRIPTOR THIS FAMILY ISSUES, and only when this run armed + * members for it, the count divides this DESCRIPTOR's own quantity, and the caller has not + * already sliced. An ALREADY-SLICED request is refused rather than re-sliced: two + * independent slicings of one stream is a subset of a subset with no error anywhere. */ + if (device->gdnc_members > 1u && request->col_count == 0u && request->first_col == 0u && + gdnc_split_total(request) >= device->gdnc_members && + (gdnc_split_total(request) % device->gdnc_members) == 0u) + return gdnc_submit_split(device, request); + + /* No split, no window to hide it in: the hook runs first, so its work happens exactly once + * whichever path takes the descriptor. */ + { + int hook_failed = 0; + + (void)gdnc_run_overlap(device, &hook_failed); + if (hook_failed) + return -1; + } + if (ft_tt_op_gdnc_parameters(request, words) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdnc phase-%u descriptor at %u rows is one the family " + "refuses\n", request->phase, request->chunk_rows); + return -1; + } + started = pn_tt_now_seconds(); + if (pn_tt_residency_submit(&device->gdnc, FT_TT_QUEUE_OP_GDN_CHUNK, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + rearms = 1u; + if (!pn_tt_gdnc_rearm(device) || + pn_tt_residency_submit(&device->gdnc, FT_TT_QUEUE_OP_GDN_CHUNK, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + elapsed = pn_tt_now_seconds() - started; + device->oncard_seconds += elapsed; + device->oncard_ops += 1u; + /* CLEAN AND RECOVERED KEPT APART. This bracket spans the refusal, the re-arm — a residency + * teardown and relaunch — and the resubmit, and summing that into `seconds / ops` is + * exactly how this phase once read **4,502 us a descriptor against 738.2**. */ + if (request->phase < 3u) { + if (rearms == 0u) { + device->gdnc_phase_seconds[request->phase] += elapsed; + device->gdnc_phase_ops[request->phase] += 1u; + } else { + device->gdnc_phase_recovered_seconds[request->phase] += elapsed; + device->gdnc_phase_recovered_ops[request->phase] += 1u; + device->gdnc_phase_rearms[request->phase] += rearms; + } + } + return 0; +} + +/** + * A STOPWATCH, NOT A GATE: overwrite the widened stream with the SAME uniform `[-1, 1]` + * xorshift the kernel bench feeds, so the descriptor's cost can be measured on the bench's + * data through the shipping path. + * + * It exists because the two disagree by 6.6x. `foundry/tt/tests/test_phase6_gdnc.c` measured + * PHASE CONV at **681.4 us a descriptor** at Ornith's own conv_dim = 8192 on uniform random + * activations; the shipping path measures **~4,480 us** on Ornith's real ones. The decode + * family recorded exactly this shape of dependence in the other direction — 1222.7 us on real + * activations against 4571 on a degenerate stream, suspected an SFPU slow path on + * out-of-range SiLU input — so DATA is the named hypothesis and this is the one arm that can + * confirm or refute it: same kernel, same shape, same traffic, same host path, different + * numbers in the buffer. + * + * It produces a WRONG ANSWER by construction. The timing is what it is for. + */ +static int gdnc_conv_synthetic(pn_tt_device *device) +{ + const uint32_t tiles = device->gdn_shape.conv_dim / PN_TT_GDNC_TILE_DIM; + float *tile = malloc(PN_TT_GDNC_TILE_BYTES); + uint32_t state = 0x1234abcdu; + uint32_t index; + int rc = 0; + + if (!tile) + return -1; + for (index = 0; index < PN_TT_GDNC_TILE_BYTES / sizeof(float); index++) { + state = state * 1664525u + 1013904223u; + tile[index] = (float)((double)(state >> 8) / 8388608.0 - 1.0); + } + /* The same tile at every column: the question is the VALUE RANGE the SFPU sees, and a + * per-column stream would cost 256 host writes to answer the same question. */ + for (index = 0; index < tiles && rc == 0; index++) + rc = pn_tt_write_dram(device, device->gdn_stream_in.bank, + device->gdn_stream_in.address + + (size_t)index * PN_TT_GDNC_TILE_BYTES, + tile, PN_TT_GDNC_TILE_BYTES); + free(tile); + return rc; +} + +int pn_tt_gdnc_conv_oncard(pn_tt_device *device, uint32_t layer, uint32_t chunk_rows) +{ + ft_tt_op_gdnc_request request; + + if (!pn_tt_gdnc_is_armed(device) || layer >= device->gdn_shape.layer_count) + return -1; + if (pn_tt_red("gdnc_conv_synthetic") && gdnc_conv_synthetic(device) != 0) + return -1; + request = gdnc_request(device, FT_TT_OP_GDNC_PHASE_CONV, chunk_rows); + request.activation_in = gdnc_operand(device, &device->gdn_stream_in); + request.activation_out = gdnc_operand(device, &device->gdn_stream_out); + request.constant = gdnc_operand(device, &device->gdnc_constant[layer]); + device->gdnc_conv_layer = layer; + /* THE WINDOW IS THE DECODE FAMILY'S BUFFER, AT ITS ADDRESS. `ftgc_plane_tile()` and + * `ftg_conv_plane_tile()` are the same expression, which is what makes the seam free. */ + request.state_conv = gdnc_operand(device, &device->gdn_region[layer].window); + + /* RED PROOF: the carry, at the host end. Zeroing the window before every descriptor is + * INVISIBLE IN CHUNK 0 — it starts zero at position 0 — and wrong from chunk 1 onward. + * It mutates only card state this file owns the pointer to; no oracle reads it. */ + if (pn_tt_red("gdnc_window_wipe") && + pn_tt_zero_dram(device, device->gdn_region[layer].window.bank, + device->gdn_region[layer].window.address, + ft_tt_op_gdnc_conv_state_bytes(&request)) != 0) + return -1; + return gdnc_submit(device, &request); +} + +int pn_tt_gdnc_delta_oncard(pn_tt_device *device, uint32_t layer, uint32_t chunk_rows, + const float *decay, const float *beta) +{ + return pn_tt_gdnc_delta_oncard_at(device, layer, 0u, chunk_rows, decay, beta); +} + +/** `PN_TT_GDNC_STAGE_GATE_UNIT` (64 or 128, default 64): the gate block's diff granularity. + * A gate run is 32 floats; a 16-row sub-chunk changes half of it, which 64 B units can skip. */ +static size_t gdnc_stage_gate_unit(void) +{ + static size_t cached = 0; + + if (cached == 0u) { + const char *value = getenv("PN_TT_GDNC_STAGE_GATE_UNIT"); + + /* 64 since 2026-10-04: 155.6 MB instead of 170.6 a prefill, 314.2 -> 317.0 tok/s, + * VERIFY 0. `=128` is the previous unit. */ + cached = (value && strcmp(value, "128") == 0) ? 128u : 64u; + } + return cached; +} + +/** The staging slot a sub-chunk uses: one per 16-row sub-chunk index. */ +static uint32_t gdnc_stage_slot(uint32_t first_row) +{ + return (first_row / FT_TT_OP_GDNC_SUB_CHUNK_ALIGN) % PN_TT_GDNC_STAGE_SLOTS; +} + +/** `PN_TT_GDNC_STAGE_DIFF`: send only the units that differ from what the slot holds. */ +static bool gdnc_stage_diff_enabled(void); + +/** Diff the staging only OUTSIDE a stream: under the streamed band loop (PHASE W) every + * write is a puller entry, and 64 one-unit writes a staging cost more than the 8 KiB block + * (run_w_sigprof.sh: CLI wall 2.922 -> 2.891 s with the block). Same bytes land either way. */ +static bool gdnc_stage_diff_now(void) +{ + static int env_set = -1; + + if (env_set < 0) + env_set = getenv("PN_TT_GDNC_STAGE_DIFF") && *getenv("PN_TT_GDNC_STAGE_DIFF") ? 1 : 0; + return gdnc_stage_diff_enabled() && (env_set || pn_tt_dispatch_quiet()); +} + +static bool gdnc_stage_diff_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_GDNC_STAGE_DIFF"); + + cached = (value && *value) ? (strcmp(value, "0") != 0) : PN_TT_GDNC_STAGE_DIFF_DEFAULT; + fprintf(stderr, "pn_tt: gdnc staging uploads %s\n", + cached ? "only the units that CHANGED against a shadow of the slot (diff)" + : "every byte (full)"); + } + return cached != 0; +} + +/** + * Write `bytes` of `stage` to the card at `address`, sending only the `unit`-byte runs that + * differ from `shadow` — and then make the shadow equal the stage. + * + * BIT-EXACT BY CONSTRUCTION, NOT BY A FACE MAP. The shadow is what this slot holds ON THE CARD: + * nothing else writes these regions and the kernels only read them, so after this call the + * card's bytes equal `stage` whether a unit was sent or skipped. Which faces of a decay tile + * are data, constant or zero at a given `first_row` is never assumed here; it is discovered by + * the comparison, so a C = 32 arm or a ragged last pass needs no special case. + * + * Adjacent changed units go up as ONE write — the link is transaction-count bound. + * `PN_TT_RED=gdnc_stage_shadow_lie` marks the first changed unit of each upload as already + * present, which must make the answer DIFFER: the proof the skipped bytes are load-bearing. + */ +static int gdnc_upload_diff(pn_tt_device *device, uint32_t bank, uint32_t address, + const unsigned char *stage, unsigned char *shadow, size_t bytes, + size_t unit, bool shadow_valid, size_t *moved) +{ + const bool lie = pn_tt_red("gdnc_stage_shadow_lie"); + bool lied = false; + size_t offset = 0; + + if (!shadow_valid) { + device->gdnc_stage_writes++; + if (pn_tt_write_dram(device, bank, address, stage, bytes) != 0) + return -1; + memcpy(shadow, stage, bytes); + *moved += bytes; + return 0; + } + while (offset < bytes) { + size_t start, end; + + while (offset < bytes && memcmp(stage + offset, shadow + offset, unit) == 0) + offset += unit; + if (offset >= bytes) + break; + if (lie && !lied) { + lied = true; + offset += unit; + continue; + } + start = offset; + while (offset < bytes && memcmp(stage + offset, shadow + offset, unit) != 0) + offset += unit; + end = offset; + device->gdnc_stage_writes++; + if (pn_tt_write_dram(device, bank, address + (uint32_t)start, stage + start, + end - start) != 0) + return -1; + memcpy(shadow + start, stage + start, end - start); + *moved += end - start; + } + return 0; +} + +/** + * `PN_TT_GDNC_STAGE_VERIFY=1`: read the slot back after every upload and compare it to what + * the builders produced — the DIRECT proof the diff upload is bit-exact, because a corrupted + * decay value can leave a short greedy completion unchanged (MEASURED: the shadow-lie red + * skipped 2,448 x 1 KiB and the 8-token answer stayed `cmp` IDENTICAL). Counts units that + * differ; prints the total at release. A timing arm must NOT carry it. + */ +static void gdnc_stage_verify(pn_tt_device *device, uint32_t slot, size_t decay_bytes, + size_t gate_bytes) +{ + static int armed = -1; + unsigned char *back; + size_t offset; + + if (armed < 0) { + const char *value = getenv("PN_TT_GDNC_STAGE_VERIFY"); + + armed = (value && *value && strcmp(value, "0") != 0) ? 1 : 0; + if (armed) + fprintf(stderr, "pn_tt: gdnc STAGE VERIFY armed — every slot read back and " + "compared (NOT a timing arm)\n"); + } + if (!armed) + return; + back = malloc(decay_bytes > gate_bytes ? decay_bytes : gate_bytes); + if (!back) + return; + if (pn_tt_read_dram(device, device->gdnc_delta_constant[slot].bank, + device->gdnc_delta_constant[slot].address + + ftgc_delta_decay_tile(0u) * PN_TT_GDNC_TILE_BYTES, + back, decay_bytes) == 0) { + for (offset = 0; offset < decay_bytes; offset += PN_TT_GDNC_STAGE_DECAY_UNIT) + if (memcmp(back + offset, device->gdnc_decay_stage + offset, + PN_TT_GDNC_STAGE_DECAY_UNIT) != 0) + device->gdnc_stage_verify_mismatches++; + } + if (pn_tt_read_dram(device, device->gdnc_gate[slot].bank, device->gdnc_gate[slot].address, + back, gate_bytes) == 0) { + for (offset = 0; offset < gate_bytes; offset += PN_TT_GDNC_STAGE_GATE_UNIT) + if (memcmp(back + offset, (const unsigned char *)device->gdnc_gate_stage + offset, + PN_TT_GDNC_STAGE_GATE_UNIT) != 0) + device->gdnc_stage_verify_mismatches++; + } + device->gdnc_stage_verify_checks++; + free(back); +} + +/** Upload the staged decay tiles and gate block into `slot`, full or diffed. */ +static int gdnc_upload_slot(pn_tt_device *device, uint32_t slot, size_t decay_bytes, + size_t gate_bytes, size_t *moved) +{ + const bool diff = gdnc_stage_diff_enabled(); + const bool valid = diff && device->gdnc_shadow_valid[slot]; + + if (gdnc_upload_diff(device, device->gdnc_delta_constant[slot].bank, + device->gdnc_delta_constant[slot].address + + ftgc_delta_decay_tile(0u) * PN_TT_GDNC_TILE_BYTES, + device->gdnc_decay_stage, device->gdnc_shadow_decay[slot], + decay_bytes, PN_TT_GDNC_STAGE_DECAY_UNIT, valid, moved) != 0 || + gdnc_upload_diff(device, device->gdnc_gate[slot].bank, device->gdnc_gate[slot].address, + (const unsigned char *)device->gdnc_gate_stage, + (unsigned char *)device->gdnc_shadow_gate[slot], gate_bytes, + PN_TT_GDNC_STAGE_GATE_UNIT, valid, moved) != 0) { + /* A FAILED WRITE LEAVES THE CARD UNKNOWN, so the shadow can no longer stand for it. */ + device->gdnc_shadow_valid[slot] = false; + return -1; + } + device->gdnc_shadow_valid[slot] = true; + gdnc_stage_verify(device, slot, decay_bytes, gate_bytes); + return 0; +} + +static int gdnc_stage_gate_finite(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows); + +/** One staging's inputs, for the per-head builder (inline or on the pipeline's worker). */ +typedef struct { + pn_tt_device *device; + uint32_t first_row, chunk_rows; + const float *decay, *beta; + bool gate_first; /**< `PN_TT_RED=gdnc_gate_first` */ +} gdnc_stage_job; + +/** Build heads [first, first + count) into the stage buffers. Each head writes only its own + * 4 KiB decay tile and its own gate run, which is what lets a worker build ahead of the + * upload of the heads before it. */ +static int gdnc_stage_build_heads(void *context, uint32_t first, uint32_t count) +{ + const gdnc_stage_job *job = (const gdnc_stage_job *)context; + pn_tt_device *device = job->device; + const uint32_t heads = device->gdn_shape.v_heads; + uint32_t head; + + for (head = first; head < first + count; head++) { + float head_decay[PN_TT_GDNC_TILE_DIM]; + float head_beta[PN_TT_GDNC_TILE_DIM]; + uint32_t row; + + for (row = 0; row < job->chunk_rows; row++) { + /* RED PROOF: `gdnc_gate_first` gives every position of the chunk the FIRST + * position's gate. That is the shape of a caller who kept the decode family's + * per-token gate, and it is right at row 0 and wrong at rows 1..C-1. */ + const uint32_t take = job->gate_first ? 0u : row; + + head_decay[row] = job->decay[(size_t)take * heads + head]; + head_beta[row] = job->beta[(size_t)take * heads + head]; + } + /* THE SUB-CHUNK IS ENTIRELY IN THESE TWO BUILDERS AND IN THE WRITER'S ROW RANGE. + * `first_row = 0` routes straight back into the shipping builders, so the whole-pass + * descriptor stages byte-identical operands. */ + if (ft_tt_op_gdnc_build_decay_at(head_decay, job->first_row, job->chunk_rows, + device->gdnc_decay_stage + + (size_t)head * PN_TT_GDNC_TILE_BYTES, + PN_TT_GDNC_TILE_BYTES) != FT_TT_OK || + ft_tt_op_gdnc_build_gate_at(head_decay, head_beta, job->first_row, job->chunk_rows, + device->gdn_shape.state_size, + device->gdnc_gate_stage + + (size_t)head * FT_TT_OP_GDNC_GATE_STRIDE, + FT_TT_OP_GDNC_GATE_STRIDE) != FT_TT_OK) + return -1; + } + return 0; +} + +/** `PN_TT_GDNC_STAGE_THREAD`: build the heads on a worker while the decay tiles of the heads + * already built go up — staging becomes max(build, upload) instead of their sum. */ +static bool gdnc_stage_thread_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_GDNC_STAGE_THREAD"); + + /* ON since 2026-10-04: 308.8 -> 314.4 tok/s, staging 356 -> 317 us, VERIFY 0. */ + cached = (value && *value) ? (strcmp(value, "0") != 0) : 1; + if (cached) + fprintf(stderr, "pn_tt: gdnc staging BUILDS on a worker thread while it uploads\n"); + } + return cached != 0; +} + +/** Heads per pipeline batch: 4 heads = 16 KiB of decay a batch, a few us of build. */ +#define PN_TT_GDNC_STAGE_BATCH_HEADS 4u + +/** + * The pipelined staging: the worker builds batches of heads; this thread diff-uploads each + * batch's decay tiles as soon as it is released, then the gate block once all are built. The + * bytes on the card are those of the inline path by construction — same builders, same diff, + * the only change is WHEN each head's upload starts. + */ +static int gdnc_stage_pipelined_checked(pn_tt_device *device, gdnc_stage_job *job, + uint32_t slot, uint32_t layer, size_t gate_bytes, + size_t *moved, double *build_wait) +{ + const uint32_t heads = device->gdn_shape.v_heads; + const bool valid = gdnc_stage_diff_now() && device->gdnc_shadow_valid[slot]; + const uint32_t decay_base = device->gdnc_delta_constant[slot].address + + ftgc_delta_decay_tile(0u) * PN_TT_GDNC_TILE_BYTES; + uint32_t done = 0u; + int rc = 0; + + if (pn_tt_stage_pipeline_start(&device->gdnc_stage_pipeline, gdnc_stage_build_heads, job, + heads, PN_TT_GDNC_STAGE_BATCH_HEADS) != 0) { + /* No worker: build inline, then the same uploads — correct, just not overlapped. */ + if (gdnc_stage_build_heads(job, 0u, heads) != 0 || + gdnc_stage_gate_finite(device, layer, job->first_row, job->chunk_rows) != 0) + return -1; + return gdnc_upload_slot(device, slot, (size_t)heads * PN_TT_GDNC_TILE_BYTES, + gate_bytes, moved); + } + while (done < heads && rc == 0) { + const uint32_t next = (heads - done < PN_TT_GDNC_STAGE_BATCH_HEADS) + ? heads : done + PN_TT_GDNC_STAGE_BATCH_HEADS; + const size_t offset = (size_t)done * PN_TT_GDNC_TILE_BYTES; + const double at = pn_tt_now_seconds(); + + if (pn_tt_stage_pipeline_wait(&device->gdnc_stage_pipeline, next) != 0) { + rc = -1; + break; + } + *build_wait += pn_tt_now_seconds() - at; + rc = gdnc_upload_diff(device, device->gdnc_delta_constant[slot].bank, + decay_base + (uint32_t)offset, device->gdnc_decay_stage + offset, + device->gdnc_shadow_decay[slot] + offset, + (size_t)(next - done) * PN_TT_GDNC_TILE_BYTES, + PN_TT_GDNC_STAGE_DECAY_UNIT, valid, moved); + done = next; + } + /* THE WHOLE JOB IS ALWAYS AWAITED, on success and on failure: a worker still writing the + * stage buffers when the next staging starts would corrupt it. */ + (void)pn_tt_stage_pipeline_wait(&device->gdnc_stage_pipeline, heads); + if (rc == 0) + rc = gdnc_stage_gate_finite(device, layer, job->first_row, job->chunk_rows); + if (rc == 0) + rc = gdnc_upload_diff(device, device->gdnc_gate[slot].bank, + device->gdnc_gate[slot].address, + (const unsigned char *)device->gdnc_gate_stage, + (unsigned char *)device->gdnc_shadow_gate[slot], gate_bytes, + PN_TT_GDNC_STAGE_GATE_UNIT, valid, moved); + device->gdnc_shadow_valid[slot] = rc == 0; + return rc; +} + +/** NOTHING NON-FINITE REACHES THE CARD — the gate scan, shared by both staging paths. */ +static int gdnc_stage_gate_finite(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows) +{ + const uint32_t heads = device->gdn_shape.v_heads; + + /* NOTHING NON-FINITE REACHES THE CARD, AND THIS IS A REFUSAL RATHER THAN A CLAMP. + * + * A NaN in one gate column propagates into the carried state and every position after it + * in the sequence, and the only symptom is a completion that reads as a different model — + * which is exactly how the `last / P_t` underflow presented before it was found. There is + * no arithmetic that can produce a NaN here now, so this is a check that a change to the + * builders cannot quietly break the recurrence rather than a guard against known inputs. + * It costs a linear scan of 5,120 floats against a 148 KiB DRAM write. */ + { + const size_t count = (size_t)heads * FT_TT_OP_GDNC_GATE_STRIDE; + size_t bad = 0; + size_t index; + + for (index = 0; index < count; index++) { + const float value = device->gdnc_gate_stage[index]; + + if (!(value > -1e30f && value < 1e30f)) + bad++; + } + if (bad != 0u) { + fprintf(stderr, "pn_tt: gdnc REFUSED layer %u — %zu of %zu gate floats are not " + "finite at %u rows from row %u. A NaN here reaches the carried " + "state and every position after it.\n", + layer, bad, count, chunk_rows, first_row); + return -1; + } + } + return 0; +} + + +/** L11b, ON by default (`PN_TT_GDNC_COMPACT=0` is off): stage decay|beta a head (8 KiB a + * staging) and let the reader build the decay-ratio tiles and gate runs + * (kernels/ftgc_compact.h, bit-identical to the builders) instead of uploading 4 KiB + 640 B + * a head. 1,629-row prompt: STAGE 0.72 -> 0.05 s, prefill 366 -> 404 tok/s, IDENTICAL. */ +static bool gdnc_compact_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_GDNC_COMPACT"); + + cached = (value && *value) ? (strcmp(value, "0") != 0) : 1; + if (cached) + fprintf(stderr, "pn_tt: gdnc staging is COMPACT — decay|beta up, the reader " + "builds the decay tiles and gate runs\n"); + } + return cached != 0; +} + +/** The host's own `1/sqrt(S)` exactly as the gate builder computes it: PS at P = 1. */ +static uint32_t gdnc_compact_scale_bits(const pn_tt_device *device) +{ + float one = 1.0f, zero = 0.0f, out[FT_TT_OP_GDNC_GATE_STRIDE]; + uint32_t bits = 0u; + + if (ft_tt_op_gdnc_build_gate(&one, &zero, 1u, device->gdn_shape.state_size, out, + FT_TT_OP_GDNC_GATE_STRIDE) == FT_TT_OK) + memcpy(&bits, &out[FT_TT_OP_GDNC_GATE_PS * PN_TT_GDNC_TILE_DIM], sizeof(bits)); + return bits; +} + +/** The compact staging: header (scale bits) then decay[32] | beta[32] a head, diffed up into + * the slot's gate block. The slot's decay tiles are not touched (nothing reads them). */ +static int gdnc_compact_stage(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, const float *decay, const float *beta, + uint32_t slot) +{ + const uint32_t heads = device->gdn_shape.v_heads; + const size_t words = FTGC_COMPACT_HEADER_WORDS + (size_t)heads * FTGC_COMPACT_HEAD_WORDS; + uint32_t *block = (uint32_t *)device->gdnc_gate_stage; + const bool gate_first = pn_tt_red("gdnc_gate_first"); + /* RED PROOF: every beta zero — no position writes the carried state. Must DIFFER. */ + const bool beta0 = pn_tt_red("gdnc_compact_beta0"); + double mark = pn_tt_now_seconds(); + size_t moved = 0, bad = 0; + uint32_t head, row; + + if (words > (size_t)heads * FT_TT_OP_GDNC_GATE_STRIDE) + return -1; + memset(block, 0, words * sizeof(uint32_t)); + block[0] = gdnc_compact_scale_bits(device); + for (head = 0; head < heads; head++) { + uint32_t *run = block + FTGC_COMPACT_HEADER_WORDS + (size_t)head * FTGC_COMPACT_HEAD_WORDS; + + for (row = 0; row < chunk_rows; row++) { + const uint32_t take = gate_first ? 0u : row; + const float d = decay[(size_t)take * heads + head]; + const float b = beta0 ? 0.0f : beta[(size_t)take * heads + head]; + + if (!(d > -1e30f && d < 1e30f) || !(b > -1e30f && b < 1e30f)) + bad++; + memcpy(&run[row], &d, sizeof(d)); + memcpy(&run[32u + row], &b, sizeof(b)); + } + } + if (bad != 0u) { + fprintf(stderr, "pn_tt: gdnc REFUSED layer %u — %zu non-finite decay/beta values at %u " + "rows from row %u\n", layer, bad, chunk_rows, first_row); + return -1; + } + device->gdnc_stage_build_seconds += pn_tt_now_seconds() - mark; + if (pn_tt_matvec_flush(device) != 0) + return -1; + mark = pn_tt_now_seconds(); + if (gdnc_upload_diff(device, device->gdnc_gate[slot].bank, device->gdnc_gate[slot].address, + (const unsigned char *)block, + (unsigned char *)device->gdnc_shadow_gate[slot], + words * sizeof(uint32_t), 64u, + gdnc_stage_diff_now() && device->gdnc_shadow_valid[slot], + &moved) != 0) { + device->gdnc_shadow_valid[slot] = false; + return -1; + } + device->gdnc_shadow_valid[slot] = true; + device->gdnc_stage_upload_seconds += pn_tt_now_seconds() - mark; + device->gdnc_stage_bytes += moved; + device->gdnc_stage_ops += 1u; + return 0; +} + +static int gdnc_delta_stage(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, const float *decay, const float *beta, + uint32_t slot) +{ + const uint32_t heads = device ? device->gdn_shape.v_heads : 0u; + gdnc_stage_job job; + /* P0's BRACKET. Started after the refusals so a rejected call books nothing, and closed + * before `gdnc_submit()` so the two clocks never overlap. */ + double stage_mark; + + if (!device || !device->gdnc_constant || !decay || !beta || chunk_rows == 0u || + chunk_rows > PN_TT_GDNC_TILE_DIM || layer >= device->gdn_shape.layer_count) + return -1; + /* REFUSED HERE AS WELL AS IN `ft_tt_op_gdnc_parameters()` AND ON THE DEVICE, because this + * is the only one of the three that sees the STAGING: a misaligned `first_row` would build + * `heads` decay tiles and gate runs against a window the encoder then rejects, and the + * failure would read as an encode error at a call site that had already written 148 KiB. */ + if ((first_row % FT_TT_OP_GDNC_SUB_CHUNK_ALIGN) != 0u || + first_row + chunk_rows > PN_TT_GDNC_TILE_DIM) + return -1; + + if (gdnc_compact_enabled()) + return gdnc_compact_stage(device, layer, first_row, chunk_rows, decay, beta, slot); + + job.device = device; + job.first_row = first_row; + job.chunk_rows = chunk_rows; + job.decay = decay; + job.beta = beta; + job.gate_first = pn_tt_red("gdnc_gate_first"); + + /* THE PIPELINED PATH: build on the worker, upload here, the gate after. It is not taken + * under the stage-double red (a clock arm that must resend whole blocks). */ + if (gdnc_stage_thread_enabled() && !pn_tt_red("gdnc_stage_double")) { + const size_t gate_bytes = (size_t)heads * FT_TT_OP_GDNC_GATE_STRIDE * + sizeof(*device->gdnc_gate_stage); + double build_wait = 0.0; + size_t moved = 0; + int rc; + + if (pn_tt_matvec_flush(device) != 0) + return -1; + stage_mark = pn_tt_now_seconds(); + rc = gdnc_stage_pipelined_checked(device, &job, slot, layer, gate_bytes, &moved, + &build_wait); + if (rc < 0) + return -1; + /* BUILD = the time this thread WAITED on the worker; UPLOAD = the rest. Their sum is + * the staging's wall, which is what the stage line has always reported. */ + device->gdnc_stage_build_seconds += build_wait; + device->gdnc_stage_upload_seconds += pn_tt_now_seconds() - stage_mark - build_wait; + device->gdnc_stage_bytes += moved; + device->gdnc_stage_ops += 1u; + gdnc_stage_verify(device, slot, (size_t)heads * PN_TT_GDNC_TILE_BYTES, gate_bytes); + return 0; + } + + /* ONE DECAY TILE AND ONE GATE RUN PER V-HEAD, built by the SHIPPING builders. `decay` and + * `beta` arrive ROW-MAJOR BY POSITION — which is how a batched projection produces them — + * and a head's run is the column, so this is the transpose and nothing else. */ + stage_mark = pn_tt_now_seconds(); + if (gdnc_stage_build_heads(&job, 0u, heads) != 0) + return -1; + if (gdnc_stage_gate_finite(device, layer, first_row, chunk_rows) != 0) + return -1; + + /* THE BUILD CLOCK CLOSES HERE and the UPLOAD clock opens, with the chained-projection + * flush BETWEEN them rather than inside either. `pn_tt_write_dram()` flushes on entry, so + * without this the first write's bracket would contain a CARD wait and the "MB/s" it + * derives would not be the link's. Hoisting it changes nothing about what runs — the very + * next statement would flush — it only decides which clock pays for it. */ + { + const size_t decay_bytes = (size_t)heads * PN_TT_GDNC_TILE_BYTES; + const size_t gate_bytes = (size_t)heads * FT_TT_OP_GDNC_GATE_STRIDE * + sizeof(*device->gdnc_gate_stage); + const double now = pn_tt_now_seconds(); + + device->gdnc_stage_build_seconds += now - stage_mark; + if (pn_tt_matvec_flush(device) != 0) + return -1; + stage_mark = pn_tt_now_seconds(); + + /* RED PROOF: `gdnc_stage_double` sends the SAME blocks TWICE. It must NOT change an + * answer — what is under test is a CLOCK: the STAGE line's upload term must roughly + * double and `sequential` must rise by the same seconds. */ + { + const unsigned repeats = pn_tt_red("gdnc_stage_double") ? 2u : 1u; + size_t moved = 0; + unsigned pass; + + for (pass = 0; pass < repeats; pass++) { + /* A second pass of the red arm must resend, not diff against what the first + * pass just wrote — otherwise the arm would move zero bytes. */ + if (pass > 0u) + device->gdnc_shadow_valid[slot] = false; + if (gdnc_upload_slot(device, slot, decay_bytes, gate_bytes, &moved) != 0) + return -1; + } + device->gdnc_stage_upload_seconds += pn_tt_now_seconds() - stage_mark; + device->gdnc_stage_bytes += moved; + device->gdnc_stage_ops += 1u; + } + } + return 0; +} + +int pn_tt_gdnc_delta_prestage_at(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, const float *decay, const float *beta) +{ + const uint32_t slot = gdnc_stage_slot(first_row); + + /* THE REGIONS, NOT THE RESIDENCY. A prestage runs inside a split's enqueue/await window, + * and the idle retire may have just stopped `gdnc` there (it is re-armed by the refusal + * path a moment later). Staging writes DRAM the residency does not own, so it needs only + * that the regions exist. MEASURED: requiring `gdnc.running` here aborted the first pass + * after every load gap. */ + if (!device || !device->gdnc_constant || !device->gdnc_shadow_decay[slot]) + return -1; + device->gdnc_prestaged[slot].valid = false; + if (gdnc_delta_stage(device, layer, first_row, chunk_rows, decay, beta, slot) != 0) + return -1; + device->gdnc_prestaged[slot].valid = true; + device->gdnc_prestaged[slot].layer = layer; + device->gdnc_prestaged[slot].first_row = first_row; + device->gdnc_prestaged[slot].chunk_rows = chunk_rows; + return 0; +} + +int pn_tt_gdnc_delta_oncard_at(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, const float *decay, const float *beta) +{ + const ftgc_shape shape = gdnc_delta_shape(device); + const uint32_t slot = gdnc_stage_slot(first_row); + ft_tt_op_gdnc_request request; + + if (!pn_tt_gdnc_is_armed(device)) + return -1; + /* A PRESTAGED SLOT IS USED ONLY FOR THE DESCRIPTOR IT WAS STAGED FOR, and is consumed by + * it. Anything else stages here, exactly as before the overlap existed. */ + if (device->gdnc_prestaged[slot].valid && device->gdnc_prestaged[slot].layer == layer && + device->gdnc_prestaged[slot].first_row == first_row && + device->gdnc_prestaged[slot].chunk_rows == chunk_rows) { + device->gdnc_prestaged[slot].valid = false; + } else { + device->gdnc_prestaged[slot].valid = false; + if (gdnc_delta_stage(device, layer, first_row, chunk_rows, decay, beta, slot) != 0) + return -1; + } + + request = gdnc_request(device, FT_TT_OP_GDNC_PHASE_DELTA, chunk_rows); + request.first_row = first_row; + request.activation_in = gdnc_operand(device, &device->gdn_stream_out); + request.activation_out = gdnc_operand(device, &device->gdn_y); + request.constant = gdnc_operand(device, &device->gdnc_delta_constant[slot]); + request.gate_address = device->gdnc_gate[slot].address; + if (gdnc_compact_enabled()) + request.delta_flags = FT_TT_OP_GDNC_DELTA_FLAG_COMPACT; + /* THE RECURRENT STATE IS THE DECODE FAMILY'S BUFFER, AT ITS ADDRESS — + * `ftgc_state_tile()` is `ftg_state_tile()`. */ + request.state_conv = gdnc_operand(device, &device->gdn_region[layer].state); + + /* RED PROOF: the other half of the carry, and invisible in chunk 0 for the same + * structural reason — at chunk 0 the incoming state is zero, so `K_hat M_0^T` and + * `Q_hat M_0^T` both vanish and half the transform is not exercised. */ + if (pn_tt_red("gdnc_state_wipe") && + pn_tt_zero_dram(device, device->gdn_region[layer].state.bank, + device->gdn_region[layer].state.address, + (size_t)ftgc_delta_state_tiles(&shape) * PN_TT_GDNC_TILE_BYTES) != 0) + return -1; + return gdnc_submit(device, &request); +} + +uint32_t pn_tt_gdnc_compact_scale_bits(const pn_tt_device *device) +{ + return device ? gdnc_compact_scale_bits(device) : 0u; +} + +int pn_tt_gdnc_delta_oncard_card_gate(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, uint32_t gate_address) +{ + /* SLOT 0 FOR EVERY SUB-CHUNK: the two slots exist to double-buffer the host STAGING, and + * nothing is staged here; their DELTA constants are the same bytes (one upload, both + * slots), and the card gate blocks live in slot 0's bank (the gate shares its xy). */ + const uint32_t slot = 0u; + ft_tt_op_gdnc_request request; + + if (!pn_tt_gdnc_is_armed(device) || !gdnc_compact_enabled() || chunk_rows == 0u || + chunk_rows > PN_TT_GDNC_TILE_DIM || layer >= device->gdn_shape.layer_count || + (first_row % FT_TT_OP_GDNC_SUB_CHUNK_ALIGN) != 0u || + first_row + chunk_rows > PN_TT_GDNC_TILE_DIM) + return -1; + device->gdnc_prestaged[gdnc_stage_slot(first_row)].valid = false; + request = gdnc_request(device, FT_TT_OP_GDNC_PHASE_DELTA, chunk_rows); + request.first_row = first_row; + request.activation_in = gdnc_operand(device, &device->gdn_stream_out); + request.activation_out = gdnc_operand(device, &device->gdn_y); + request.constant = gdnc_operand(device, &device->gdnc_delta_constant[slot]); + request.gate_address = gate_address; + request.delta_flags = FT_TT_OP_GDNC_DELTA_FLAG_COMPACT; + request.state_conv = gdnc_operand(device, &device->gdn_region[layer].state); + return gdnc_submit(device, &request); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gdnc.h b/src/accel/tt/pn_tt_gdnc.h new file mode 100644 index 0000000..1e6077d --- /dev/null +++ b/src/accel/tt/pn_tt_gdnc.h @@ -0,0 +1,208 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdnc.h + * @brief The CHUNKWISE Gated-Delta-Net block on the card: the same two phases the decode + * family runs, over C <= 32 POSITIONS AT ONCE, on `foundry/tt/ft_tt_op_gdnc.h`. + * + * --------------------------------------------------------------------------- + * IT BORROWS THE DECODE FAMILY'S CARRIED STATE, BYTE FOR BYTE + * --------------------------------------------------------------------------- + * This is the single most important property of this file and it was designed in rather than + * discovered: the rolling conv window and the `[v_heads, S, S]` recurrent state the chunkwise + * kernels read and write are **the buffers `pn_tt_gdn.c` allocated**, at the same addresses, + * in the same layout (`ftgc_plane_tile()` is `ftg_conv_plane_tile()`, `ftgc_state_tile()` is + * `ftg_state_tile()`). So a prompt that prefills in chunks and then decodes one token at a + * time is ONE sequence with no transcription at the seam, and the `1..taps-2` tail a chunk + * cannot carry is run through the decode family on the same bytes. + * + * The consequence for arming: **this family REQUIRES the `gdn` family to be armed** and + * refuses otherwise. It owns no carried state of its own. + * + * --------------------------------------------------------------------------- + * WHAT IT DOES OWN, AND WHY EACH IS A SECOND BUFFER + * --------------------------------------------------------------------------- + * the CONV constant PER LAYER. `taps` ROW-REPLICATED tap-major planes plus the `2*taps` + * band tiles riding at the end of them. The decode family's weight + * carries `w[c][tap]` in ROW 0 with zeros below; a chunk needs it at + * every row, because the tap coefficient is constant across positions + * and the broadcast surface is BROADCAST_COL only. Same tile count, + * same addresses, different `valid_rows` — and a SEPARATE buffer, + * because the shipped decode path's byte-identity is a hard gate and + * is not worth re-deriving through a shared operand. + * 4.03 MiB a layer at Ornith, 96.8 MiB over 24. + * the DELTA constant ONE for the model: the four program constants (`I`, `2I`, the + * strictly-lower mask, the `scale` column) and one decay-ratio tile per + * v-head. 147 KiB. The decay tiles are rebuilt every layer of every + * chunk; the four constants are written once. + * the GATE block ONE for the model, `v_heads * 160` floats, and it MUST sit in the + * DELTA constant's bank — it shares that operand's packed NoC + * coordinate word and nothing on the device can catch a violation. + * + * --------------------------------------------------------------------------- + * THE DECAY-RATIO MATRIX IS BUILT ON THE HOST, DELIBERATELY + * --------------------------------------------------------------------------- + * `ft_tt_op_gdnc_build_decay()` is a product recurrence: every entry in (0,1], no cumulative + * reciprocal, no logarithm. Both alternatives were modelled through this card's Tf32 + * truncating unpack and both are worse — an on-card doubling construction costs the whole + * precision advantage of going chunkwise (y 8.6e-03 -> 1.7e-02), and `exp(log P_t - log P_s)` + * is safe against OVERFLOW and not against the UNPACK, because cumulative logs reach -50 at + * Ornith's decays and a Tf32 truncation of -50 is 2.4% error in D before the exp is reached. + * **It costs 128 KiB a layer a chunk on the link, which is 0.33 ms a position at 32 rows, and + * that is the price of the accuracy. Do not move it to the card.** + * + * --------------------------------------------------------------------------- + * WHAT THE CALLER OWES + * --------------------------------------------------------------------------- + * 1. `chunk_rows >= conv_taps - 1` for PHASE CONV. Below that the chunk cannot REPLACE the + * window, only shift it, and both the host encoder and the device refuse. The caller + * routes a shorter tail through `pn_tt_gdn_conv_oncard()` / `pn_tt_gdn_delta_oncard()`, + * which is the same arithmetic on the same bytes. `pn_tt_gdnc_min_chunk_rows()` is that + * floor. PHASE DELTA has no floor. + * 2. **ROWS `chunk_rows..31` OF THE STREAM MUST BE ZERO.** They are computed and discarded, + * and `SiLU(0) = 0` cannot surprise anyone — a stale value there would propagate through + * arithmetic nobody inspects. In `qwen35moe_prefill_tt.c` this holds for free and by + * construction: `pn_tt_activation_upload_rows()` tilizes at `valid_rows = rows`, so the + * pass's `hidden` strip is zero above the chunk, an RMS norm of a zero row is zero, and + * an `Mt=1` matvec of a zero row is zero — so every projection is zero above the chunk. + * + * @status NEW, default OFF behind `PN_TT_PREFILL_GDNC`. The three kernels are green on + * silicon (`foundry/tt/tests/test_phase6_gdnc.c`): PHASE CONV 681.4 us a descriptor + * flat in C, PHASE DELTA 435.5 us at 32 v-heads, both parity-checked over 103 + * positions as 32+32+32+7. + * @issues The `gdnc` residency is ONE core and PHASE DELTA is not head-split here. The + * decode family measured its DELTA embarrassingly parallel in the head and the + * chunkwise one fits `13.8 us + N x 13.18`, so the split is available and unbuilt. + * @todo None + */ + +#ifndef PN_TT_GDNC_H +#define PN_TT_GDNC_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_device.h" + +/** + * Arm the chunkwise residency and allocate the three buffers above. + * + * Reads the shape off the ALREADY-ARMED `gdn` family, so the two cannot disagree about + * `conv_dim`, the tap count or the head counts — which is what makes sharing the window and + * the recurrent state safe. + * + * Never fatal on its own: a card without the `gdnc` program, a core already held, or DRAM + * that will not take the constants all leave the family unarmed and the caller on the + * per-position path. + * + * @return 0 (even when nothing armed), -1 on a bad argument or a failed allocation + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_gdnc(pn_tt_device *device); + +/** Did the chunkwise family actually arm for this run? */ +bool pn_tt_gdnc_is_armed(const pn_tt_device *device); + +/** The host's 1/sqrt(S) word, as the COMPACT block's header carries it. */ +uint32_t pn_tt_gdnc_compact_scale_bits(const pn_tt_device *device); + +/** PM 7 ITEM 5: one DELTA descriptor whose COMPACT gate block the card already computed (the + * GDN GATE family) at `gate_address`, in the DELTA constant bank. Nothing is staged. */ +int pn_tt_gdnc_delta_oncard_card_gate(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, uint32_t gate_address); + +/** The shortest chunk PHASE CONV will run — `conv_taps - 1`. Shorter tails go through the + * decode family. 0 when the family is not armed. */ +uint32_t pn_tt_gdnc_min_chunk_rows(const pn_tt_device *device); + +/** + * TRANSPOSE one layer's conv1d weight into ROW-REPLICATED tap-major planes and upload it, + * then write the `2*taps` band tiles behind them. Once, at load. + * + * `weight` is the oracle's own `layer->ssm_conv1d`: ROW-MAJOR `[conv_dim, taps]`. Same + * transpose `pn_tt_gdn_conv_weight_upload()` does, with `valid_rows` 1 -> 32. + * + * @param layer 0..layer_count-1, the SSM-layer ordinal + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdnc_conv_weight_upload(pn_tt_device *device, uint32_t layer, const float *weight); + +/** + * PHASE CONV over `chunk_rows` positions of the GDN stream-in buffer, writing the stream + * buffer and rolling the shared window ONCE, to the chunk's last `taps-1` positions. + * + * @param chunk_rows `conv_taps-1` .. 32 + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdnc_conv_oncard(pn_tt_device *device, uint32_t layer, uint32_t chunk_rows); + +/** + * PHASE DELTA over the same `chunk_rows` positions, reading q|k|v out of the stream buffer + * and writing `y`, with the shared recurrent state advanced ONCE across the whole chunk. + * + * Uploads the per-head decay-ratio tiles (128 KiB) and the gate block (20 KiB) and nothing + * else. + * + * @param decay `chunk_rows * v_heads` floats, ROW-MAJOR by position — `exp(g)`, already + * through the host `softplus` and `exp` because there is no SFPU `log` + * @param beta likewise, already through the sigmoid + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdnc_delta_oncard(pn_tt_device *device, uint32_t layer, uint32_t chunk_rows, + const float *decay, const float *beta); + +/** + * PHASE DELTA over rows `[first_row, first_row + chunk_rows)` of the SAME 32-row strip — the + * IN-PLACE SUB-CHUNK, and the reason a shorter chunk costs one extra descriptor instead of a + * row shuttle. + * + * `first_row = 0` IS `pn_tt_gdnc_delta_oncard()` — same operands, same encoding, same staged + * bytes — so a caller that never sub-chunks is unchanged by construction. + * + * WHY IT EXISTS. PHASE DELTA owns 100% of the chunkwise prefill's quality variance and the + * chunk length is the whole of it: six arms against one oracle, C = 16 passing all six + * criteria with the best figures of the ladder and C = 32 failing worst-document. Everything + * else in the SSM layer batches at 32 and is unaffected by C, so the arm that keeps the speed + * and takes the quality is ONE PHASE CONV over the whole pass, ONE L2 norm over the whole + * pass, and `ceil(rows / C)` of these. + * + * @param first_row a multiple of 16, with `first_row + chunk_rows <= 32` + * @param decay,beta `chunk_rows * v_heads` floats for THIS sub-chunk's rows, row-major by + * position. The caller slices; nothing here reads outside the sub-chunk + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_gdnc_delta_oncard_at(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, const float *decay, const float *beta); + +/** + * Stage `(layer, first_row, chunk_rows)`'s decay tiles and gate block into that sub-chunk's + * slot AHEAD of its descriptor, which then uses them instead of staging. Meant to run from the + * overlap hook, i.e. while the card is busy with something that does not read that slot. + * + * @return 0, or -1 (nothing is marked staged) + */ +int pn_tt_gdnc_delta_prestage_at(pn_tt_device *device, uint32_t layer, uint32_t first_row, + uint32_t chunk_rows, const float *decay, const float *beta); + +/** + * ONE-SHOT: run `fn(context)` inside the next gdnc descriptor, between its enqueue and its + * await — host work hidden under card time. Its seconds are booked to the stage line, not the + * phase. A failing `fn` fails that descriptor's call. + */ +void pn_tt_gdnc_set_overlap(pn_tt_device *device, int (*fn)(void *context), void *context); + +/** + * ONE-SHOT, a second slot: `fn(context)` runs in the next gdnc window AFTER the first slot's + * hook. Its result is ignored (its owner redoes the work inline when it did not happen), and it + * may set this slot again for the window after. NULL clears it. + */ +void pn_tt_gdnc_set_overlap_aux(pn_tt_device *device, int (*fn)(void *context), void *context); + +/** Retire the residency and free what arming allocated. Safe on a device that never armed. */ +void pn_tt_gdnc_release(pn_tt_device *device); + +#endif /* USE_TT */ +#endif /* PN_TT_GDNC_H */ diff --git a/src/accel/tt/pn_tt_gdngate.c b/src/accel/tt/pn_tt_gdngate.c new file mode 100644 index 0000000..c08c2dd --- /dev/null +++ b/src/accel/tt/pn_tt_gdngate.c @@ -0,0 +1,526 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdngate.c + * @brief The GDN GATE family host half. See pn_tt_gdngate.h for what and why. + * + * @status Written PM 7 (2026-10-08). Unexecuted. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_gdngate.h" + +#include +#include +#include + +#include "pn_tt_device_internal.h" + +#include "tt/ft_tt_op_dispatch.h" +#include "tt/kernels/resident_gdngate_protocol.h" + +#define PN_TT_GDNGATE_PROGRAM "gdngate" +#define PN_TT_GDNGATE_CORE_ENV "PN_TT_GDNGATE_CORE_LIST" +/** The cores no family holds with every decode family and the 48-core prefill grid (0-47) + * armed — read off a full arm's `residency up on core` census (PM 7, 2026-10-08). */ +#define PN_TT_GDNGATE_CORE_DEFAULT "121,126,127,128" +/** The two CBs: the reader's stage (two tiles + the constants, addressed) and the blocks. */ +#define PN_TT_GDNGATE_CB_STAGE 0u +#define PN_TT_GDNGATE_CB_OUT 16u +#define PN_TT_GDNGATE_CB_OUT_PAGES 2u + +/** `PN_TT_GDNGATE_CORE_LIST` (comma-separated core indices) into `cores`. @return count. */ +static uint32_t gdngate_core_list(uint32_t *cores) +{ + const char *env = getenv(PN_TT_GDNGATE_CORE_ENV); + const char *at = (env && *env) ? env : PN_TT_GDNGATE_CORE_DEFAULT; + uint32_t count = 0u; + char *end; + + while (*at && count < PN_TT_GDNGATE_MAX_MEMBERS) { + const unsigned long core = strtoul(at, &end, 10); + + if (end == at) + break; + cores[count++] = (uint32_t)core; + at = *end == ',' ? end + 1 : end; + } + return count; +} + +static uint32_t gdngate_xy(const pn_tt_device *device, uint32_t bank) +{ + return (uint32_t)device->dram[bank].x | ((uint32_t)device->dram[bank].y << 16); +} + +static uint32_t gdngate_align64(uint32_t bytes) +{ + return (bytes + 63u) & ~63u; +} + +/** Band 0's DRAM address of a strip, whatever band it is seeked to. */ +static uint32_t gdngate_band0_address(const pn_tt_activation *strip) +{ + return strip->address - strip->row_offset * strip->tile_count * PN_TT_TILE_BF16; +} + +/** Prepare, place and start member `index` on `core`. */ +static bool gdngate_arm_member(pn_tt_device *device, uint32_t index, uint32_t core) +{ + pn_tt_residency *member = &device->gdngate[index]; + ft_tt_launch_cb_entry placed[2]; + ft_tt_op_cb_request requests[2]; + + if (!pn_tt_prepare_family_on_core(device, member, PN_TT_GDNGATE_PROGRAM, "gdngate", + PN_TT_GDNGATE_CORE_ENV, core, PN_TT_ONCARD_DM_MASK)) + return false; + requests[0].index = PN_TT_GDNGATE_CB_STAGE; + requests[0].page_bytes = 2u * PN_TT_TILE_BF16 + kGgConstWords * 4u; + requests[0].page_count = 1u; + requests[1].index = PN_TT_GDNGATE_CB_OUT; + requests[1].page_bytes = gdngate_align64(ftgg_block_bytes(device->gdngate_heads)); + requests[1].page_count = PN_TT_GDNGATE_CB_OUT_PAGES; + if (ft_tt_op_cb_place(requests, 2u, placed, member->cb_buffer, sizeof(member->cb_buffer), + &member->cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the gdngate circular buffers do not fit\n"); + pn_tt_residency_release(member); + return false; + } + return pn_tt_start_family(member); +} + +/** Upload every layer's constants block: ssm_a[32] | ssm_dt[32] (zero past `heads`). */ +static int gdngate_upload_constants(pn_tt_device *device, const float *const *ssm_a, + const float *const *ssm_dt) +{ + const size_t bytes = (size_t)device->gdngate_layers * kGgConstWords * 4u; + uint32_t *table = calloc(1, bytes); + ft_tt_buffer buffer; + int rc = -1; + + if (!table) + return -1; + for (uint32_t layer = 0; layer < device->gdngate_layers; layer++) { + uint32_t *row = table + (size_t)layer * kGgConstWords; + + memcpy(row, ssm_a[layer], device->gdngate_heads * sizeof(float)); + memcpy(row + kGgMaxHeads, ssm_dt[layer], device->gdngate_heads * sizeof(float)); + } + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, 0u, bytes, &buffer) == + FT_TT_OK) { + device->gdngate_constants.bank = buffer.logical_bank; + device->gdngate_constants.address = (uint32_t)buffer.address; + rc = pn_tt_write_dram(device, buffer.logical_bank, (uint32_t)buffer.address, table, + bytes); + } + free(table); + return rc; +} + +int pn_tt_gdngate_arm(pn_tt_device *device, uint32_t layers, uint32_t heads, + const float *const *ssm_a, const float *const *ssm_dt, uint32_t max_rows, + uint32_t scale_bits) +{ + const char *env = getenv("PN_TT_GDNGATE"); + uint32_t want, index; + ft_tt_buffer buffer; + size_t array_bytes; + + if (!device || (env && *env && atoi(env) == 0)) + return -1; + if (layers == 0u || heads == 0u || heads > kGgMaxHeads || !ssm_a || !ssm_dt || + max_rows == 0u || !pn_tt_program_is_present(PN_TT_GDNGATE_PROGRAM)) { + fprintf(stderr, "pn_tt: gdngate refused — %u layer(s), %u head(s), program %s\n", + layers, heads, + pn_tt_program_is_present(PN_TT_GDNGATE_PROGRAM) ? "present" : "ABSENT"); + return -1; + } + want = gdngate_core_list(device->gdngate_cores); + if (want == 0u) + return -1; + device->gdngate_layers = layers; + device->gdngate_heads = heads; + device->gdngate_scale_bits = scale_bits; + device->gdngate_max_bands = (max_rows + kGgTileRows - 1u) / kGgTileRows; + device->gdngate_block_stride = gdngate_align64(ftgg_block_bytes(heads)); + /* THE DELTA DESCRIPTOR READS ITS GATE AT ITS CONSTANT'S xy: the card-gate DELTA always + * uses constant slot 0 (pn_tt_gdnc_delta_oncard_card_gate), so the blocks go in its bank. */ + array_bytes = (size_t)layers * 2u * device->gdngate_max_bands * device->gdngate_block_stride; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, + device->gdnc_delta_constant[0].bank, array_bytes, &buffer) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: gdngate — no room for %zu bytes of gate blocks\n", array_bytes); + return -1; + } + device->gdngate_blocks.bank = buffer.logical_bank; + device->gdngate_blocks.address = (uint32_t)buffer.address; + if (gdngate_upload_constants(device, ssm_a, ssm_dt) != 0) { + fprintf(stderr, "pn_tt: gdngate — the constants table did not upload\n"); + return -1; + } + for (index = 0; index < want; index++) { + if (!gdngate_arm_member(device, index, device->gdngate_cores[index])) + break; + device->gdngate_members = index + 1u; + } + if (device->gdngate_members == 0u) { + fprintf(stderr, "pn_tt: gdngate — no member armed\n"); + return -1; + } + fprintf(stderr, "pn_tt: gdngate — %u member(s), %u layer(s) x %u band(s) of gate blocks " + "(%zu bytes)\n", + device->gdngate_members, layers, device->gdngate_max_bands, array_bytes); + return 0; +} + +bool pn_tt_gdngate_enabled(const pn_tt_device *device) +{ + return device && device->gdngate_members > 0u; +} + +uint32_t pn_tt_gdngate_block_address(const pn_tt_device *device, uint32_t layer, uint32_t band, + uint32_t sub) +{ + const uint32_t per_layer = 2u * device->gdngate_max_bands * device->gdngate_block_stride; + + return device->gdngate_blocks.address + layer * per_layer + + (2u * band + sub) * device->gdngate_block_stride; +} + +/** The parameter words of one descriptor: bands [first, first + count) of `layer`. */ +static void gdngate_words(const pn_tt_device *device, uint32_t layer, + const pn_tt_activation *alpha, const pn_tt_activation *beta, + uint32_t rows, uint32_t sub_rows, uint32_t first, uint32_t count, + uint32_t *words) +{ + const uint32_t band_bytes = alpha->tile_count * PN_TT_TILE_BF16; + const uint32_t done = first * kGgTileRows; + const uint32_t take = rows - done < count * kGgTileRows ? rows - done : count * kGgTileRows; + + memset(words, 0, kGgWords * sizeof(uint32_t)); + words[kGgFlags] = pn_tt_red("gdngate_beta_as_decay") ? kGgFlagRedBetaAsDecay : 0u; + words[kGgRows] = take; + words[kGgBandCount] = count; + words[kGgSubRows] = sub_rows; + words[kGgHeads] = device->gdngate_heads; + words[kGgAlphaXY] = gdngate_xy(device, alpha->bank); + words[kGgAlphaBase] = gdngate_band0_address(alpha) + first * band_bytes; + words[kGgBetaXY] = gdngate_xy(device, beta->bank); + words[kGgBetaBase] = gdngate_band0_address(beta) + first * band_bytes; + words[kGgBandStride] = band_bytes; + words[kGgConstXY] = gdngate_xy(device, device->gdngate_constants.bank); + words[kGgConstBase] = device->gdngate_constants.address + layer * kGgConstWords * 4u; + words[kGgDstXY] = gdngate_xy(device, device->gdngate_blocks.bank); + words[kGgDstBase] = pn_tt_gdngate_block_address(device, layer, first, 0u); + words[kGgBlockStride] = device->gdngate_block_stride; + words[kGgScaleBits] = device->gdngate_scale_bits; +} + +int pn_tt_gdngate_issue_bands(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *alpha, const pn_tt_activation *beta, + uint32_t rows, uint32_t sub_rows, uint32_t first_band, + uint32_t band_count) +{ + const uint32_t bands = (rows + kGgTileRows - 1u) / kGgTileRows; + uint32_t members, per_member, first = first_band, index; + uint32_t words[kGgWords]; + + if (!pn_tt_gdngate_enabled(device) || !alpha || !beta || rows == 0u || band_count == 0u || + layer >= device->gdngate_layers || bands > device->gdngate_max_bands || + first_band + band_count > bands || alpha->tile_count != 1u || beta->tile_count != 1u) + return -1; + members = device->gdngate_members < band_count ? device->gdngate_members : band_count; + per_member = (band_count + members - 1u) / members; + device->gdngate_last.layer = layer; + device->gdngate_last.alpha = *alpha; + device->gdngate_last.beta = *beta; + device->gdngate_last.rows = rows; + device->gdngate_last.sub_rows = sub_rows; + for (index = 0; index < members && first < first_band + band_count; index++) { + const uint32_t end = first_band + band_count; + const uint32_t count = end - first < per_member ? end - first : per_member; + /* Round-robin over issues too, so a super-band's few bands do not all land on 0. */ + const uint32_t member = (device->gdngate_next_member + index) % device->gdngate_members; + + gdngate_words(device, layer, alpha, beta, rows, sub_rows, first, count, words); + if (pn_tt_residency_enqueue(&device->gdngate[member], FT_TT_QUEUE_OP_GDN_GATE, words, + 1u) != 0) + device->gdngate_failed = true; + device->gdngate_pending[member]++; + first += count; + } + device->gdngate_next_member = (device->gdngate_next_member + members) % + device->gdngate_members; + device->gdngate_issues++; + return 0; +} + +int pn_tt_gdngate_issue(pn_tt_device *device, uint32_t layer, const pn_tt_activation *alpha, + const pn_tt_activation *beta, uint32_t rows, uint32_t sub_rows) +{ + if (device) + device->gdngate_layer_issues++; + return pn_tt_gdngate_issue_bands(device, layer, alpha, beta, rows, sub_rows, 0u, + (rows + kGgTileRows - 1u) / kGgTileRows); +} + +/** Re-arm every member and replay the last issue, awaited. The strips are untouched (the + * caller settles before its band loop rewrites anything), so the replay is the same op. */ +static int gdngate_replay(pn_tt_device *device) +{ + uint32_t index; + + for (index = 0; index < device->gdngate_members; index++) { + const uint32_t core = device->gdngate_cores[index]; + + fprintf(stderr, "pn_tt: gdngate member %u refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", index, core); + pn_tt_residency_stop(&device->gdngate[index]); + pn_tt_residency_release(&device->gdngate[index]); + if (!gdngate_arm_member(device, index, core)) + return -1; + device->gdngate_pending[index] = 0u; + } + device->gdngate_failed = false; + if (pn_tt_gdngate_issue(device, device->gdngate_last.layer, &device->gdngate_last.alpha, + &device->gdngate_last.beta, device->gdngate_last.rows, + device->gdngate_last.sub_rows) != 0) + return -1; + for (index = 0; index < device->gdngate_members; index++) { + if (device->gdngate_pending[index] != 0u && + pn_tt_residency_await(&device->gdngate[index], PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + device->gdngate_pending[index] = 0u; + } + return device->gdngate_failed ? -1 : 0; +} + +int pn_tt_gdngate_settle(pn_tt_device *device) +{ + const double started = pn_tt_now_seconds(); + bool failed; + uint32_t index; + int rc; + + if (!pn_tt_gdngate_enabled(device)) + return -1; + failed = device->gdngate_failed; + for (index = 0; index < device->gdngate_members; index++) { + if (device->gdngate_pending[index] == 0u) + continue; + if (pn_tt_residency_await(&device->gdngate[index], PN_TT_OP_TIMEOUT_MS) != 0) + failed = true; + device->gdngate_pending[index] = 0u; + } + rc = failed ? gdngate_replay(device) : 0; + device->gdngate_settle_seconds += pn_tt_now_seconds() - started; + device->gdngate_settles++; + return rc; +} + +void pn_tt_gdngate_report(const pn_tt_device *device) +{ + if (!pn_tt_gdngate_enabled(device)) + return; + fprintf(stderr, "pn_tt: gdngate — %llu issue(s) (%llu at a layer start), %llu settle(s) " + "waiting %.4f s, %llu FLAT (cumulative)\n", + (unsigned long long)device->gdngate_issues, + (unsigned long long)device->gdngate_layer_issues, + (unsigned long long)device->gdngate_settles, device->gdngate_settle_seconds, + (unsigned long long)device->gdngate_flats); +} + +/** FLAT words for heads [first, first + count) of `heads_total`. */ +static void gdngate_flat_words(const pn_tt_device *device, uint32_t layer, + const pn_tt_activation *alpha, const pn_tt_activation *beta, + uint32_t rows, uint32_t dst_bank, uint32_t dst_address, + uint32_t row_stride, uint32_t first, uint32_t count, + uint32_t *words) +{ + gdngate_words(device, layer, alpha, beta, rows, 16u, 0u, 1u, words); + words[kGgFlags] |= kGgFlagFlat; + words[kGgSubRows] = first; + words[kGgHeads] = count; + words[kGgBandStride] = device->gdngate_heads; + words[kGgDstXY] = gdngate_xy(device, dst_bank); + words[kGgDstBase] = dst_address; + words[kGgBlockStride] = row_stride; +} + +int pn_tt_gdngate_flat(pn_tt_device *device, uint32_t layer, const pn_tt_activation *alpha, + const pn_tt_activation *beta, uint32_t rows, uint32_t dst_bank, + uint32_t dst_address, uint32_t row_stride) +{ + uint32_t words[PN_TT_GDNGATE_MAX_MEMBERS][kGgWords]; + uint32_t members, per, index; + int failed = 0; + + if (!pn_tt_gdngate_enabled(device) || !alpha || !beta || rows == 0u || rows > kGgTileRows || + layer >= device->gdngate_layers || alpha->tile_count != 1u || beta->tile_count != 1u) + return -1; + /* THE HEADS SPLIT OVER THE MEMBERS in slices of a multiple of 4 (16-byte runs), each + * member writing only its slice's two runs a row; `PN_TT_GDNGATE_FLAT_SPLIT=0` = one. */ + members = pn_tt_env_u32("PN_TT_GDNGATE_FLAT_SPLIT", 1u) ? device->gdngate_members : 1u; + while (members > 1u && (device->gdngate_heads % (members * 4u)) != 0u) + members--; + per = device->gdngate_heads / members; + for (index = 0; index < members; index++) { + gdngate_flat_words(device, layer, alpha, beta, rows, dst_bank, dst_address, row_stride, + index * per, per, words[index]); + if (pn_tt_residency_enqueue(&device->gdngate[index], FT_TT_QUEUE_OP_GDN_GATE, + words[index], 1u) != 0) + failed = 1; + } + device->gdngate_flats++; + /* Awaited here: the DELTA after it reads what they write (streamed: puller AWAITs). */ + for (index = 0; index < members; index++) { + pn_tt_residency *member = &device->gdngate[index]; + + if (pn_tt_residency_await(member, PN_TT_OP_TIMEOUT_MS) == 0) + continue; + /* A refused member (idle retire): re-armed and its slice run again, awaited. */ + fprintf(stderr, "pn_tt: gdngate member %u refused a FLAT descriptor — retiring it by " + "ASKING and arming a fresh one on core %u\n", + index, device->gdngate_cores[index]); + pn_tt_residency_stop(member); + pn_tt_residency_release(member); + if (!gdngate_arm_member(device, index, device->gdngate_cores[index]) || + pn_tt_residency_submit(member, FT_TT_QUEUE_OP_GDN_GATE, words[index], + PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + return failed ? -1 : 0; +} + +long pn_tt_gdngate_verify_layer(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *alpha, const pn_tt_activation *beta, + uint32_t rows, uint32_t sub_rows) +{ + const uint32_t bands = (rows + kGgTileRows - 1u) / kGgTileRows; + const uint32_t words = ftgg_block_bytes(device->gdngate_heads) / 4u; + uint32_t constants[kGgConstWords], expect[kGgBlockBytes / 4u], got[kGgBlockBytes / 4u]; + uint16_t tiles[2][PN_TT_TILE_BF16 / 2u]; + long bad = 0; + + if (pn_tt_read_dram(device, device->gdngate_constants.bank, + device->gdngate_constants.address + layer * kGgConstWords * 4u, + constants, sizeof(constants)) != 0) + return -1; + for (uint32_t band = 0; band < bands; band++) { + const uint32_t band_rows = rows - band * kGgTileRows < kGgTileRows + ? rows - band * kGgTileRows : kGgTileRows; + const uint32_t offset = band * alpha->tile_count * PN_TT_TILE_BF16; + + if (pn_tt_read_dram(device, alpha->bank, gdngate_band0_address(alpha) + offset, + tiles[0], sizeof(tiles[0])) != 0 || + pn_tt_read_dram(device, beta->bank, gdngate_band0_address(beta) + offset, tiles[1], + sizeof(tiles[1])) != 0) + return -1; + for (uint32_t sub = 0; sub < 2u; sub++) { + const uint32_t live = ftgg_sub_live_rows(band_rows, sub_rows, sub); + + if (live == 0u) + continue; + ftgg_build_block(expect, device->gdngate_heads, device->gdngate_scale_bits, + tiles[0], tiles[1], constants, sub * sub_rows, live, 0); + if (pn_tt_read_dram(device, device->gdngate_blocks.bank, + pn_tt_gdngate_block_address(device, layer, band, sub), got, + words * 4u) != 0) + return -1; + for (uint32_t word = 0; word < words; word++) + bad += expect[word] != got[word]; + } + } + return bad; +} + +/** Fill `count` face-ordered bf16 tiles so that, over all of them, every bf16 bit pattern + * appears exactly once at each head column after rotation `rotation` (value v lands in + * band v / 1024, row (v / 32) % 32, head (v + rotation) % 32). Beta takes the reverse. */ +static void gdngate_sweep_tiles(uint16_t *alpha, uint16_t *beta, uint32_t count, + uint32_t rotation) +{ + const uint32_t per_tile = PN_TT_TILE_BF16 / 2u; + + for (uint32_t value = 0; value < count * per_tile; value++) { + const uint32_t band = value / per_tile, row = (value / 32u) % 32u; + const uint32_t head = (value + rotation) % 32u; + const size_t at = (size_t)band * per_tile + ftgg_face_index(row, head); + + alpha[at] = (uint16_t)value; + beta[at] = (uint16_t)(0xFFFFu - value); + } +} + +long pn_tt_gdngate_sweep(pn_tt_device *device, uint32_t rotations) +{ + const uint32_t bands = 64u, rows = bands * kGgTileRows; + const size_t bytes = (size_t)bands * PN_TT_TILE_BF16; + uint16_t *host = malloc(2u * bytes); + pn_tt_activation alpha, beta; + ft_tt_buffer a_buffer, b_buffer; + double compute_seconds = 0.0; + long bad = 0, part; + + if (!host || bands > device->gdngate_max_bands || + ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, 1u, bytes, &a_buffer) != + FT_TT_OK || + ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, 2u, bytes, &b_buffer) != + FT_TT_OK) { + free(host); + return -1; + } + memset(&alpha, 0, sizeof(alpha)); + alpha.bank = a_buffer.logical_bank; + alpha.address = (uint32_t)a_buffer.address; + alpha.tile_count = 1u; + alpha.replica_count = 1u; + alpha.tile_rows = bands; + alpha.active_rows = 1u; + beta = alpha; + beta.bank = b_buffer.logical_bank; + beta.address = (uint32_t)b_buffer.address; + for (uint32_t rotation = 0; rotation < rotations && bad >= 0; rotation++) { + gdngate_sweep_tiles(host, host + bytes / 2u, bands, rotation); + if (pn_tt_write_dram(device, alpha.bank, alpha.address, host, bytes) != 0 || + pn_tt_write_dram(device, beta.bank, beta.address, host + bytes / 2u, bytes) != 0) + bad = -1; + for (uint32_t layer = 0; bad >= 0 && layer < device->gdngate_layers; layer++) { + const double started = pn_tt_now_seconds(); + int rc = pn_tt_gdngate_issue(device, layer, &alpha, &beta, rows, 16u); + + if (rc == 0) + rc = pn_tt_gdngate_settle(device); + compute_seconds += pn_tt_now_seconds() - started; + if (rc != 0 || + (part = pn_tt_gdngate_verify_layer(device, layer, &alpha, &beta, rows, 16u)) < 0) + bad = -1; + else + bad += part; + } + } + free(host); + fprintf(stderr, "pn_tt: gdngate SWEEP — every bf16 alpha and beta x %u head rotation(s) x " + "%u layers' constants: %ld word(s) differ from the host build; card " + "%.3f s over %u member(s) = %.2f us a (beta, decay) pair a member\n", + rotations, device->gdngate_layers, bad, compute_seconds, device->gdngate_members, + compute_seconds * 1e6 * device->gdngate_members / + ((double)rotations * device->gdngate_layers * rows * device->gdngate_heads)); + return bad; +} + +void pn_tt_gdngate_release(pn_tt_device *device) +{ + uint32_t index; + + if (!device) + return; + for (index = 0; index < device->gdngate_members; index++) + pn_tt_residency_release(&device->gdngate[index]); + device->gdngate_members = 0u; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_gdngate.h b/src/accel/tt/pn_tt_gdngate.h new file mode 100644 index 0000000..ba5efec --- /dev/null +++ b/src/accel/tt/pn_tt_gdngate.h @@ -0,0 +1,93 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_gdngate.h + * @brief The GDN GATE family host half: the Gated DeltaNet's per-position decay and beta + * computed ON THE CARD from the alpha/beta projection strips, as the gdnc DELTA reader's + * COMPACT gate blocks, so the prefill never reads alpha/beta back to the host. + * + * WHY THE BITS ARE THE HOST'S: the card reader and this file compile the same integer-only + * `ft_det_math.h` (foundry/tt/kernels), through the same block builder + * (`ftgg_build_block()` in resident_gdngate_protocol.h). + * + * THE SHAPE. One member per core of `PN_TT_GDNGATE_CORE_LIST` (default 121,126,127,128 — the + * cores no family holds). Each SSM layer owns a gate array of + * 2 * `max_bands` blocks in the gdnc DELTA constant bank (the DELTA descriptor reads its gate + * at the constant word's xy); block (band, sub) is at `layer_base + (2 * band + sub) * stride`. + * A layer's bands are dealt to the members in contiguous ranges, issued WITHOUT awaiting, and + * settled once before the layer's band loop (a puller AWAIT when streamed). + * + * @status Written PM 7 (2026-10-08). Unexecuted. + * @issues None + * @todo None + */ +#ifndef PANTHEON_PN_TT_GDNGATE_H +#define PANTHEON_PN_TT_GDNGATE_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_device.h" + +/** Arm the members, upload every layer's constants (`ssm_a`, `ssm_dt` for each SSM layer, row + * `layer` = 32 + 32 fp32), and allocate the gate arrays for up to `max_rows` positions a + * pass. `scale_bits` is the host's 1/sqrt(S) word (header word 0). @return 0, or -1 (the + * family is then simply not used: the host gate path stays). */ +int pn_tt_gdngate_arm(pn_tt_device *device, uint32_t layers, uint32_t heads, + const float *const *ssm_a, const float *const *ssm_dt, uint32_t max_rows, + uint32_t scale_bits); + +/** Is the family armed and wanted (`PN_TT_GDNGATE=0` off)? */ +bool pn_tt_gdngate_enabled(const pn_tt_device *device); + +/** Issue layer `layer`'s blocks for rows [0, rows) of the alpha/beta strips (any seek; band 0 + * is computed), with the DELTA sub-chunk length `sub_rows`. Not awaited. */ +int pn_tt_gdngate_issue(pn_tt_device *device, uint32_t layer, const pn_tt_activation *alpha, + const pn_tt_activation *beta, uint32_t rows, uint32_t sub_rows); + +/** The same for bands [first_band, first_band + band_count) of a `rows`-row layer — the W2 + * wavefront issues the next layer's gates super-band by super-band. */ +int pn_tt_gdngate_issue_bands(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *alpha, const pn_tt_activation *beta, + uint32_t rows, uint32_t sub_rows, uint32_t first_band, + uint32_t band_count); + +/** Await every issued descriptor (streamed: puller AWAITs). A refused member is re-armed and + * its descriptor replayed (the op is a pure function of strips nobody has rewritten yet). */ +int pn_tt_gdngate_settle(pn_tt_device *device); + +/** ITEM 6: the DECODE gate layout for rows [0, rows) of band 0 of the strips (flat + * decay[H] | beta[H] a row at `row_stride`), written at `dst_bank`/`dst_address` — where the + * gdn DELTA reads it (pn_tt_gdn_gate_target). Enqueued AND awaited. @return 0 or -1. */ +int pn_tt_gdngate_flat(pn_tt_device *device, uint32_t layer, const pn_tt_activation *alpha, + const pn_tt_activation *beta, uint32_t rows, uint32_t dst_bank, + uint32_t dst_address, uint32_t row_stride); + +/** The DRAM address of block (band, sub) of `layer`, in the gdnc DELTA constant bank. */ +uint32_t pn_tt_gdngate_block_address(const pn_tt_device *device, uint32_t layer, uint32_t band, + uint32_t sub); + +/** VERIFY (`PN_TT_GDNGATE_VERIFY=1`, gates only): read layer `layer`'s blocks for `rows` back + * and compare them word for word with the host build of the same block from host-read + * alpha/beta. @return mismatching words, or -1 on a read failure. */ +long pn_tt_gdngate_verify_layer(pn_tt_device *device, uint32_t layer, + const pn_tt_activation *alpha, const pn_tt_activation *beta, + uint32_t rows, uint32_t sub_rows); + +/** THE CARD == HOST SWEEP (`PN_TT_GDNGATE_SWEEP=`, gates only): synthetic strips + * holding every bf16 bit pattern as alpha and as beta, rotated over the head columns, through + * the family with every layer's real constants, each block compared with the host build. + * @return mismatching words, or -1. Leaves the DRAM it used allocated (a gate arm). */ +long pn_tt_gdngate_sweep(pn_tt_device *device, uint32_t rotations); + +/** One stderr line: issues (and how many at a layer start), settles and their wait, FLATs. */ +void pn_tt_gdngate_report(const pn_tt_device *device); + +/** Retire and release every member (device close). */ +void pn_tt_gdngate_release(pn_tt_device *device); + +#endif /* USE_TT */ + +#endif /* PANTHEON_PN_TT_GDNGATE_H */ diff --git a/src/accel/tt/pn_tt_head_norm.c b/src/accel/tt/pn_tt_head_norm.c new file mode 100644 index 0000000..73c7ff8 --- /dev/null +++ b/src/accel/tt/pn_tt_head_norm.c @@ -0,0 +1,531 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_head_norm.c + * @brief Implementation of the per-head reduce residencies. See pn_tt_head_norm.h — the + * layout argument that makes this file short lives there, as does the reason there + * is a SET of these keyed by width rather than one. + * + * @status None + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_head_norm.h" + +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_fp32_strip.h" + +/** + * Which enabled Tensix each slot drives. + * + * Defaults, not constants, and each must be a core no other residency holds AND that the + * matmul group cannot grow into: members run upward from `PN_TT_MATMUL_CORE_FIRST` = 96, so + * the shipped `PN_TT_MATMUL_CORES=16` stops at 110. The rest of the map, which is why the second + * slot is not 92: + * + * 71 matmul member 0 72/73/74 reduce / eltwise / sample + * 96..110 matmul members 1..15 82/83/84 rope / memory / attention + * 85/86 render-widen / -narrow 90 gdn member 0 + * 91 head norm, slot 0 92..94 gdn head-split members (4) + * 111 head norm, slot 1 112 eltwise split member + * 114..120 gdnc members + * + * SLOT 0 IS WHOEVER ARMS FIRST, not whoever needs which width. With both families up the GDN + * block's Wt=4 takes core 91 and attention's Wt=8 takes 103; with attention alone, attention + * takes 91. Nothing downstream depends on the number — routing is by WIDTH — but a probe + * reading a core map does, which is why the order is written down rather than inferred. + * + * 85 and 86 are the strip bridge's two render residencies and they are inside the range the + * matmul group grows into: at `PN_TT_MATMUL_CORES=16` members reach 90 and would collide with + * both of them and with the GDN block. The refusal below NAMES that, but the shipped count is + * 8 (members 75..81) and there is headroom to 84 only. + * + * `pn_tt_prepare_family_on_core()` refuses a collision BY NAME against every live residency, + * and that refusal is the guard — a contested core does not fail, it computes badly. + */ +#define PN_TT_CORE_HEAD_NORM_ENV "PN_TT_CORE_HEAD_NORM" +#define PN_TT_CORE_HEAD_NORM_DEFAULT 91u +#define PN_TT_CORE_HEAD_NORM_2_ENV "PN_TT_CORE_HEAD_NORM_2" +/* 111 since the PX7 placement shipped (was 103, inside the 16-member matmul's 96..110). */ +#define PN_TT_CORE_HEAD_NORM_2_DEFAULT 111u + +/** THE HEAD SPLIT, per slot. Members 1..N-1 of slot 0 run on 130..136 and of slot 1 on + * 123..128 — the two free blocks above every other family's default (render members 122 and + * 129 sit between them). Each slot's count is capped to its block so no member index can + * leave the grid. A call whose heads the count does not divide runs whole on member 0. */ +#define PN_TT_HEAD_NORM_CORES_ENV "PN_TT_HEAD_NORM_CORES" +#define PN_TT_HEAD_NORM_CORES_DEFAULT 4u /* 0.345 -> 0.150 s, prefill +3.2%, decode +3.3% */ +#define PN_TT_HEAD_NORM_MEMBER_CORE_ENV "PN_TT_HEAD_NORM_MEMBER_CORE_FIRST" +static const uint32_t g_head_norm_member_first[PN_TT_HEAD_NORM_MAX_WIDTHS] = { 130u, 123u }; +static const uint32_t g_head_norm_member_room[PN_TT_HEAD_NORM_MAX_WIDTHS] = { 7u, 6u }; + +/** Both members every slot serves. The placement is the UNION: RMS norm carries gamma, + * L2 norm does not, and a stream the residency does not carry is placed at one page — so + * arming for the union is what lets one core run both. */ +static const ft_tt_op_reduce_function g_head_norm_members[] = { FT_TT_OP_REDUCE_FN_RMS_NORM, + FT_TT_OP_REDUCE_FN_L2_NORM }; +#define PN_TT_HEAD_NORM_MEMBER_COUNT \ + (sizeof(g_head_norm_members) / sizeof(g_head_norm_members[0])) + +/** The core environment name and default for slot `index`. */ +static void pn_tt_head_norm_core_for_slot(uint32_t index, const char **out_env, + uint32_t *out_default) +{ + if (index == 0u) { + *out_env = PN_TT_CORE_HEAD_NORM_ENV; + *out_default = PN_TT_CORE_HEAD_NORM_DEFAULT; + return; + } + *out_env = PN_TT_CORE_HEAD_NORM_2_ENV; + *out_default = PN_TT_CORE_HEAD_NORM_2_DEFAULT; +} + +/** The live slot placed for `tile_cols`, or NULL. Const and non-const forms, because the + * routing question is asked from both. */ +static const pn_tt_head_norm_residency *pn_tt_head_norm_slot_const(const pn_tt_device *device, + uint32_t tile_cols) +{ + uint32_t index; + + for (index = 0; index < device->head_norm_count; index++) { + if (device->head_norm[index].tile_cols == tile_cols && + device->head_norm[index].residency.running) + return &device->head_norm[index]; + } + return NULL; +} + +static pn_tt_head_norm_residency *pn_tt_head_norm_slot(pn_tt_device *device, uint32_t tile_cols) +{ + return (pn_tt_head_norm_residency *)pn_tt_head_norm_slot_const(device, tile_cols); +} + +/** Which reduce PROGRAM serves a page size. The two are the same choice: `reduce_fp32` IS + * the `TILE_BYTES=4096` build of the same three kernel sources. */ +static const char *pn_tt_head_norm_program_for(uint32_t tile_bytes) +{ + return tile_bytes == PN_TT_TILE_FP32 ? PN_TT_REDUCE_FP32_PROGRAM_NAME + : PN_TT_REDUCE_PROGRAM_NAME; +} + +/** Arm, or re-arm after a refusal, the slot at `index` for the width and page it records. */ +static bool pn_tt_arm_head_norm_slot(pn_tt_device *device, uint32_t index) +{ + pn_tt_head_norm_residency *slot = &device->head_norm[index]; + ft_tt_op_reduce_residency residency; + ft_tt_launch_cb_entry placed[FT_TT_OP_REDUCE_CB_COUNT]; + const char *core_env; + uint32_t core_default; + + pn_tt_head_norm_core_for_slot(index, &core_env, &core_default); + if (ft_tt_op_reduce_residency_for(g_head_norm_members, PN_TT_HEAD_NORM_MEMBER_COUNT, + slot->tile_cols, slot->tile_bytes, &residency) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: no reduce residency serves the head width Wt=%u — those " + "per-head norms stay on the host\n", slot->tile_cols); + return false; + } + if (!pn_tt_prepare_family(device, &slot->residency, + pn_tt_head_norm_program_for(slot->tile_bytes), "head-norm", + core_env, core_default, PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_reduce_place_cbs(&residency, placed, slot->residency.cb_buffer, + sizeof(slot->residency.cb_buffer), + &slot->residency.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the per-head reduce circular buffers do not fit at Wt=%u\n", + slot->tile_cols); + pn_tt_residency_release(&slot->residency); + return false; + } + return pn_tt_start_family(&slot->residency); +} + +/** Arm head-split member `member` (1-based) of slot `index` on `core`. */ +static bool pn_tt_arm_head_norm_member(pn_tt_device *device, uint32_t index, uint32_t member, + uint32_t core) +{ + pn_tt_head_norm_residency *slot = &device->head_norm[index]; + pn_tt_residency *residency = &slot->member[member - 1u]; + ft_tt_op_reduce_residency shape; + ft_tt_launch_cb_entry placed[FT_TT_OP_REDUCE_CB_COUNT]; + + if (ft_tt_op_reduce_residency_for(g_head_norm_members, PN_TT_HEAD_NORM_MEMBER_COUNT, + slot->tile_cols, slot->tile_bytes, &shape) != FT_TT_OK) + return false; + if (!pn_tt_prepare_family_on_core(device, residency, + pn_tt_head_norm_program_for(slot->tile_bytes), + "head-norm-member", PN_TT_HEAD_NORM_MEMBER_CORE_ENV, + core, PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_reduce_place_cbs(&shape, placed, residency->cb_buffer, + sizeof(residency->cb_buffer), &residency->cb_mask) != + FT_TT_OK) { + pn_tt_residency_release(residency); + return false; + } + return pn_tt_start_family(residency); +} + +/** Arm slot `index`'s head-split members; a member that will not come up shrinks the split. */ +static void pn_tt_arm_head_norm_members(pn_tt_device *device, uint32_t index) +{ + pn_tt_head_norm_residency *slot = &device->head_norm[index]; + const uint32_t asked = pn_tt_env_u32(PN_TT_HEAD_NORM_CORES_ENV, + PN_TT_HEAD_NORM_CORES_DEFAULT); + const uint32_t first = g_head_norm_member_first[index]; + const uint32_t room = g_head_norm_member_room[index] + 1u; + uint32_t want = asked < 1u ? 1u : (asked > room ? room : asked); + uint32_t up = 1u, member; + + slot->members = 1u; + for (member = 1u; member < want; member++) { + if (!pn_tt_arm_head_norm_member(device, index, member, first + member - 1u)) + break; + up++; + } + slot->members = up; + if (asked > 1u) + fprintf(stderr, "pn_tt: head-norm Wt=%u HEAD SPLIT — asked %u, running %u member(s) " + "from core %u (members 1.. on %u..)\n", + slot->tile_cols, asked, up, slot->residency.core_index, first); +} + +/** Retire and re-arm member `member` (1-based) of slot `index` on its own core. */ +static bool pn_tt_head_norm_member_rearm(pn_tt_device *device, uint32_t index, uint32_t member) +{ + pn_tt_residency *residency = &device->head_norm[index].member[member - 1u]; + const uint32_t core = residency->core_index; + + fprintf(stderr, "pn_tt: head-norm member %u refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", member, core); + pn_tt_residency_stop(residency); + pn_tt_residency_release(residency); + device->oncard_restarts++; + if (!pn_tt_residency_retired(residency)) + return false; + return pn_tt_arm_head_norm_member(device, index, member, core); +} + +int pn_tt_device_arm_head_norm(pn_tt_device *device, uint32_t head_width) +{ + return pn_tt_device_arm_head_norm_at(device, head_width, PN_TT_TILE_FP32); +} + +int pn_tt_device_arm_head_norm_at(pn_tt_device *device, uint32_t head_width, + uint32_t tile_bytes) +{ + const char *program = pn_tt_head_norm_program_for(tile_bytes); + uint32_t tile_cols; + uint32_t index; + + if (!device || head_width == 0u || (head_width % PN_TT_TILE_DIM) != 0u) + return -1; + if (tile_bytes != PN_TT_TILE_FP32 && tile_bytes != PN_TT_TILE_BF16) + return -1; + tile_cols = head_width / PN_TT_TILE_DIM; + /* Idempotent per width: the second GDN layer to ask must not take a second core. A width + * already placed at ANOTHER page is a refusal rather than a second slot, because the + * routing downstream is by width and two slots at one width would be a coin toss. */ + if (pn_tt_head_norm_slot(device, tile_cols) != NULL) { + if (pn_tt_head_norm_slot(device, tile_cols)->tile_bytes != tile_bytes) { + fprintf(stderr, "pn_tt: Wt=%u is already placed at a %u-byte page and was asked " + "for at %u — one width, one residency\n", tile_cols, + pn_tt_head_norm_slot(device, tile_cols)->tile_bytes, tile_bytes); + return -1; + } + return 0; + } + if (device->head_norm_count >= PN_TT_HEAD_NORM_MAX_WIDTHS) { + fprintf(stderr, "pn_tt: %u per-head reduce residencies are already placed (Wt", + device->head_norm_count); + for (index = 0; index < device->head_norm_count; index++) + fprintf(stderr, " %u", device->head_norm[index].tile_cols); + fprintf(stderr, ") — the Wt=%u norms stay on the host. Raise " + "PN_TT_HEAD_NORM_MAX_WIDTHS.\n", tile_cols); + return 0; + } + if (!pn_tt_program_is_present(program)) { + fprintf(stderr, "pn_tt: no `%s` program — the Wt=%u per-head norms stay on the " + "host\n", program, tile_cols); + return 0; + } + + index = device->head_norm_count; + device->head_norm[index].tile_cols = tile_cols; + device->head_norm[index].tile_bytes = tile_bytes; + if (!pn_tt_arm_head_norm_slot(device, index)) { + device->head_norm[index].tile_cols = 0u; + device->head_norm[index].tile_bytes = 0u; + return 0; + } + device->head_norm_count++; + pn_tt_arm_head_norm_members(device, index); + return 0; +} + +bool pn_tt_head_norm_is_armed(const pn_tt_device *device, uint32_t head_width) +{ + if (!device) + return false; + if (head_width == 0u) + return device->head_norm_count != 0u && + device->head_norm[0].residency.running; + if ((head_width % PN_TT_TILE_DIM) != 0u) + return false; + return pn_tt_head_norm_slot_const(device, head_width / PN_TT_TILE_DIM) != NULL; +} + +int pn_tt_head_norm_constants_upload(pn_tt_device *device, ft_tt_op_reduce_function function, + const float *gamma, uint32_t width, float eps, + pn_tt_norm_constants *out_constants) +{ + uint32_t page; + + if (!device || !out_constants || width == 0u || (width % PN_TT_TILE_DIM) != 0u) + return -1; + if (pn_tt_head_norm_slot(device, width / PN_TT_TILE_DIM) == NULL) { + fprintf(stderr, "pn_tt: head-norm constants at Wt=%u, which no per-head reduce " + "residency was placed for\n", width / PN_TT_TILE_DIM); + return -1; + } + /* RED PROOF, on the card: build a Float32 residency's constants at the bf16 page under a + * program placed at 4096. Nothing refuses it — the reader simply reads 4096 bytes where + * 2048 were written — so the arm exists to show that the page size is load-bearing and + * that the probe can see it. `PN_TT_RED=head_norm_bf16_constants`. It is a NO-OP on a + * slot already at the bf16 page, which is stated rather than left to be discovered. */ + page = pn_tt_head_norm_slot(device, width / PN_TT_TILE_DIM)->tile_bytes; + if (page == PN_TT_TILE_FP32 && pn_tt_red("head_norm_bf16_constants")) + page = PN_TT_TILE_BF16; + return pn_tt_reduce_constants_upload(device, function, gamma, width, eps, page, + out_constants); +} + +/** + * THE HEAD SPLIT: member m reduces heads [m*per, (m+1)*per) — the same descriptor with + * `row_blocks = per` and the input and output bases moved by `m*per*Wt` tiles. Gamma is + * shared by every head, so the constants operand is not moved. Enqueue all, then await all; + * a member that fails is re-armed and its slice submitted alone, after every member was + * awaited (an enqueued descriptor is always awaited, or that member's cursor desynchronises). + */ +static int pn_tt_head_norm_submit_split(pn_tt_device *device, pn_tt_head_norm_residency *slot, + uint32_t index, const ft_tt_op_reduce_request *whole) +{ + const uint32_t members = slot->members; + const uint32_t per = whole->row_blocks / members; + const uint32_t step = per * whole->tile_cols * slot->tile_bytes; + uint32_t words[PN_TT_HEAD_NORM_MAX_MEMBERS][FT_TT_OP_VECTOR_WORDS]; + pn_tt_residency *set[PN_TT_HEAD_NORM_MAX_MEMBERS]; + bool bad[PN_TT_HEAD_NORM_MAX_MEMBERS]; + double started = pn_tt_now_seconds(); + int failed = 0; + uint32_t m; + + memset(bad, 0, sizeof(bad)); + for (m = 0; m < members; m++) { + ft_tt_op_reduce_request sliced = *whole; + + sliced.row_blocks = per; + sliced.input.address += m * step; + sliced.output.address += m * step; + if (ft_tt_op_reduce_parameters(&sliced, words[m]) != FT_TT_OK) + return -1; + pn_tt_vec_probe_apply(words[m]); + set[m] = m == 0u ? &slot->residency : &slot->member[m - 1u]; + } + for (m = 0; m < members; m++) + if (pn_tt_residency_enqueue(set[m], FT_TT_QUEUE_OP_REDUCE, words[m], 1u) != 0) + failed = bad[m] = 1; + set[0]->seconds += pn_tt_now_seconds() - started; + for (m = 0; m < members; m++) { + const double at = pn_tt_now_seconds(); + + if (pn_tt_residency_await(set[m], PN_TT_OP_TIMEOUT_MS) != 0) + failed = bad[m] = 1; + set[m]->seconds += pn_tt_now_seconds() - at; + } + if (!failed) + return 0; + for (m = 0; m < members; m++) { + if (!bad[m]) + continue; + if (m == 0u) { + pn_tt_residency_stop(&slot->residency); + pn_tt_residency_release(&slot->residency); + device->oncard_restarts++; + if (!pn_tt_arm_head_norm_slot(device, index)) + return -1; + } else if (!pn_tt_head_norm_member_rearm(device, index, m)) { + return -1; + } + if (pn_tt_residency_submit(set[m], FT_TT_QUEUE_OP_REDUCE, words[m], + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + return 0; +} + +/** + * The whole of a per-head reduce, over operands the caller has already described. + * + * Taking OPERANDS rather than a strip type is what lets one body serve both pages: an fp32 + * strip and a bf16 activation differ in the page size and in nothing else this function + * needs, and the page it must agree with is the SLOT's — checked here, because a 2048-byte + * operand under a 4096-byte program is the failure with no card-side refusal anywhere. + */ +static int pn_tt_head_norm_submit(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + ft_tt_op_operand input, ft_tt_op_operand output, + uint32_t input_tiles, uint32_t output_tiles) +{ + ft_tt_op_reduce_request request; + uint32_t words[FT_TT_OP_VECTOR_WORDS]; + pn_tt_head_norm_residency *slot; + uint32_t tiles; + uint32_t index; + double started; + + if (!device || !constants || heads == 0u) + return -1; + /* THE CONSTANTS CHOOSE THE CORE. They were built for one width and carry it, so routing + * on them is what makes "the wrong residency" unrepresentable rather than merely + * unlikely — a reduce at another width would read this buffer's gamma at the wrong + * stride and return a smoothly wrong norm with nothing refusing. */ + slot = pn_tt_head_norm_slot(device, constants->tile_cols); + if (!slot) { + fprintf(stderr, "pn_tt: no per-head reduce residency is placed for Wt=%u\n", + constants->tile_cols); + return -1; + } + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + tiles = heads * constants->tile_cols; + if (input_tiles < tiles || output_tiles < tiles) { + fprintf(stderr, "pn_tt: a %u-head norm at Wt=%u needs %u tiles; the strips hold %u " + "and %u\n", heads, constants->tile_cols, tiles, input_tiles, + output_tiles); + return -1; + } + if (input.tile_bytes != slot->tile_bytes || output.tile_bytes != slot->tile_bytes) { + fprintf(stderr, "pn_tt: a Wt=%u head norm was handed %u/%u-byte operands under a " + "%u-byte program — the page size is a three-sided contract with no " + "card-side check\n", constants->tile_cols, input.tile_bytes, + output.tile_bytes, slot->tile_bytes); + return -1; + } + + memset(&request, 0, sizeof(request)); + request.function = function; + /* ONE ROW BLOCK PER HEAD. The reader reads row block `b` at tiles `b*tile_cols + wt`, + * so over a [1, heads*W] strip block `h` is head `h`'s W elements — all of them in + * ROW 0, where the projection that produced them left them. pn_tt_head_norm.h has the + * argument; this line is the whole of the implementation of it. */ + request.row_blocks = heads; + request.tile_cols = constants->tile_cols; + request.prefetch_tiles = constants->tile_cols; + request.input = input; + request.constants.x = (uint16_t)device->dram[constants->bank].x; + request.constants.y = (uint16_t)device->dram[constants->bank].y; + request.constants.address = constants->address; + request.constants.tile_bytes = slot->tile_bytes; + request.output = output; + if (ft_tt_op_reduce_parameters(&request, words) != FT_TT_OK) + return -1; + pn_tt_vec_probe_apply(words); + + started = pn_tt_now_seconds(); + index = (uint32_t)(slot - &device->head_norm[0]); + if (slot->members > 1u && (heads % slot->members) == 0u) { + if (pn_tt_head_norm_submit_split(device, slot, index, &request) != 0) + return -1; + pn_tt_oncard_account(device, "head-norm", + function == FT_TT_OP_REDUCE_FN_L2_NORM ? "l2_norm" : "rms_norm", + slot->tile_cols, pn_tt_now_seconds() - started); + return 0; + } + if (pn_tt_residency_submit(&slot->residency, FT_TT_QUEUE_OP_REDUCE, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + /* The words address DRAM only, so the same descriptor is valid for a fresh + * residency — the re-arm argument pn_tt_oncard.c makes for the other three. */ + fprintf(stderr, "pn_tt: the Wt=%u head-norm residency refused a descriptor — " + "retiring it by ASKING and arming a fresh one on core %u\n", + slot->tile_cols, slot->residency.core_index); + pn_tt_residency_stop(&slot->residency); + pn_tt_residency_release(&slot->residency); + device->oncard_restarts++; + if (!pn_tt_arm_head_norm_slot(device, index) || + pn_tt_residency_submit(&slot->residency, FT_TT_QUEUE_OP_REDUCE, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + pn_tt_oncard_account(device, "head-norm", + function == FT_TT_OP_REDUCE_FN_L2_NORM ? "l2_norm" : "rms_norm", + slot->tile_cols, pn_tt_now_seconds() - started); + return 0; +} + +int pn_tt_head_norm(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + const pn_tt_fp32_strip *input, const pn_tt_fp32_strip *output) +{ + if (!device || !input || !output) + return -1; + return pn_tt_head_norm_submit(device, function, constants, heads, + pn_tt_fp32_strip_operand(device, input), + pn_tt_fp32_strip_operand(device, output), + input->tile_count, output->tile_count); +} + +int pn_tt_head_norm_bf16(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + if (!device || !input || !output) + return -1; + return pn_tt_head_norm_submit(device, function, constants, heads, + pn_tt_operand(device, input), + pn_tt_operand(device, output), input->tile_count, + output->tile_count); +} + +int pn_tt_head_norm_operands(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + ft_tt_op_operand input, ft_tt_op_operand output, + uint32_t input_tiles, uint32_t output_tiles) +{ + return pn_tt_head_norm_submit(device, function, constants, heads, input, output, + input_tiles, output_tiles); +} + +void pn_tt_head_norm_defer(pn_tt_device *device, uint32_t tile_cols, bool on) +{ + pn_tt_head_norm_residency *slot = device ? pn_tt_head_norm_slot(device, tile_cols) : NULL; + + if (!slot) + return; + slot->residency.defer_await = on; + for (uint32_t member = 1u; member < slot->members; member++) + slot->member[member - 1u].defer_await = on; +} + +int pn_tt_head_norm_settle(pn_tt_device *device, uint32_t tile_cols) +{ + pn_tt_head_norm_residency *slot = device ? pn_tt_head_norm_slot(device, tile_cols) : NULL; + int rc; + + if (!slot) + return 0; + rc = pn_tt_residency_settle(&slot->residency, PN_TT_OP_TIMEOUT_MS); + for (uint32_t member = 1u; member < slot->members; member++) + if (pn_tt_residency_settle(&slot->member[member - 1u], PN_TT_OP_TIMEOUT_MS) != 0) + rc = -1; + return rc; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_head_norm.h b/src/accel/tt/pn_tt_head_norm.h new file mode 100644 index 0000000..8cf311f --- /dev/null +++ b/src/accel/tt/pn_tt_head_norm.h @@ -0,0 +1,288 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_head_norm.h + * @brief PER-HEAD norms on EXTRA reduce residencies placed for the HEAD width — the norms + * the Gated-Delta-Net block and full attention have and the model's RMS norms do not. + * + * Model-agnostic: it knows a head count and a head width. It does not know what a + * Qwen3.5 layer is. + * + * --------------------------------------------------------------------------- + * ONE RESIDENCY PER WIDTH, AND A HYBRID MODEL NEEDS TWO + * --------------------------------------------------------------------------- + * A reduce residency is placed for ONE Wt and refuses any other (next section), so "the head + * width" is not one number for a GDN+attention hybrid. Ornith: + * + * GDN per-head norms S = 128 Wt 4 PN_TT_CORE_HEAD_NORM default 91 + * attention QK-norms head_dim = 256 Wt 8 PN_TT_CORE_HEAD_NORM_2 default 103 + * the model's own RMS n_embd = 4096 Wt 128 the `reduce` residency default 72 + * + * So this is a SET keyed by width, not a singleton. `pn_tt_device_arm_head_norm()` is + * idempotent per width and takes one more Tensix per new one; every other entry point routes + * on the width, and the width comes from the CONSTANTS the caller passes rather than from an + * argument — a constants buffer is built FOR one width, and a reduce run at another width + * would read the wrong gamma out of it and return a smoothly wrong norm. + * + * --------------------------------------------------------------------------- + * WHY A SEPARATE RESIDENCY, AND WHY THAT IS THE WHOLE OF THE COST + * --------------------------------------------------------------------------- + * A reduce residency is PLACED for one Wt: `ft_tt_op_reduce_place_cbs()` sizes CB_IN, + * CB_MID, CB_GAMMA and CB_BETA at `max_tile_cols` pages, and `pn_tt_rms_norm()` refuses a + * descriptor whose Wt is not the one it was placed for. The model's norms are + * `n_embd/32 = 128` tile columns; the GDN's per-head norms are `S/32 = 4`. So they get + * their own core, and there are 140. + * + * The two widths are not merely different, they must not SHARE: `CB_GAMMA` is + * `max_tile_cols` pages and every descriptor pushes `tile_cols` of them in ONE go, so a + * Wt=4 descriptor leaves the write pointer at page 4 of 128 and the next Wt=128 descriptor + * straddles the limit. `ft_tensix_cb_push_grid_is_safe()` catches it and REFUSES — a + * re-arm, not a wrong answer — but the refusal would land on one descriptor in two. + * + * --------------------------------------------------------------------------- + * THE LAYOUT RESULT THIS FILE EXISTS TO USE, AND IT COST NOTHING + * --------------------------------------------------------------------------- + * **A per-head norm over a ROW-VECTOR activation strip needs no reshape at all.** The + * scoping for this item assumed the head norms wanted a `[heads, S]` matrix — one row per + * head — which every tensor around them is not: `pn_tt_activation_alloc()` strips are + * `[1, 32*tile_count]`, row 0 of each tile and 31 rows of padding. + * + * `resident_reduce_reader.c` reads row block `b` at tiles `b*tile_cols + wt`, consecutive, + * and `row_blocks = tile_count / tile_cols`. So over a `[1, heads*S]` strip with + * `tile_cols = S/32`: + * + * row block h == tiles [h*S/32, +S/32) == head h's S elements, all in ROW 0 + * + * and each row block is reduced PER ROW. One descriptor, `heads` independent norms, the + * data exactly where the projection that produced it left it. The `[heads, S]` matrix + * layout would have needed a transpose of 32-element runs out of row 0 of `4*heads` tiles; + * this needs nothing. + * + * The consequence is that `gamma` is shared across heads, which is what the GDN's output + * gated norm wants — `ssm_norm` is `S` values applied identically to every v-head — and + * the q/k L2 norm has no gamma at all. + * + * --------------------------------------------------------------------------- + * THE TWO MEMBERS + * --------------------------------------------------------------------------- + * q/k L2 norm `FT_TT_OP_REDUCE_FN_L2_NORM`, no gamma, eps passed as `eps*eps` + * (foundry `ft_tt_op_vector.h` has why). Pantheon's oracle is + * `pn_qwen35moe_l2_norm()`. + * output gated `FT_TT_OP_REDUCE_FN_RMS_NORM` with gamma = `ssm_norm`, then + * `pn_tt_silu_mul(z, normed)` on the ELEMENTWISE residency. Pantheon's + * oracle is `pn_qwen35moe_gated_norm()`, which is exactly those two steps. + * attention QK `FT_TT_OP_REDUCE_FN_RMS_NORM` with gamma = `attn_q_norm` / `attn_k_norm`, + * at the ATTENTION head width, on the second slot. Pantheon's oracle is + * `qwen35moe_qk_norm_heads()` in `qwen35moe_attn.c`, which is + * `pn_vrms_norm_f32` per head with one shared weight — the same "gamma is + * shared across heads" the row-block layout gives for free. + * + * **THE QK MEMBER IS THE RMS MEMBER, AND THE RMS MEMBER IS THE ONE THAT MISSES THE GATE.** + * At Wt 4 it measured 3.165e-03 against 1e-3 (below), and the cause is the chain STEP COUNT + * rather than the width, so Wt 8 inherits it. That is a numerics change to attention's + * scores, not a rounding detail, and it is why on-card attention stays opt-in. + * + * --------------------------------------------------------------------------- + * THE RESIDENCY IS Float32, AND THAT IS WHY IT WORKS AT ALL + * --------------------------------------------------------------------------- + * The FIRST arming of this residency ran the shipped bf16 `reduce` program and MISSED THE + * GATE BY 9x: `PN_TT_GDN_NORMCHECK=1` on Ornith's real per-layer data scored the q/k L2 norm + * at **8.599e-03** max-norm relative per head and the output gated norm at **1.283e-02** + * against 1e-3, with 146,844 of 196,608 q/k elements out. + * + * **The error is per REDUCE, so it is the narrow norms that cannot afford it.** The model's + * own RMS norms average the same per-buffer rounding over 4096 elements and pass; a per-head + * norm has 128. And the lever is the CB TABLE and not the activation strip: priced apart + * off-hardware in `foundry/tt/tests/test_vector_ops.c` on this exact shape, the bf16 REDUCE + * alone is 6.126e-03 while rendering an fp32 operand into a bf16 strip adds only 1.0e-03, so + * an fp32 strip in front of a bf16 reduce would have bought nothing. + * + * So this residency arms `reduce_fp32` — `reduce_float32.cbtable` at `dst_accum_mode 1`, a + * `TILE_BYTES=4096` build of the SAME three kernel sources — and everything it touches is + * fp32 by consequence: `pn_tt_fp32_strip` in and out, and a constants buffer built at + * `PN_TT_TILE_FP32`. There is no fallback to the bf16 program: at this width that would be a + * wrong answer rather than a slow one. + * + * WHAT THAT DOES NOT BUY, AND IT IS NOT ENOUGH FOR ONE OF THE TWO MEMBERS. A Float32 CB is + * not fp32 arithmetic: the generated `chlkc_descriptors.h` for this program carries + * `unpack_src_format = 0` (Float32) beside `unpack_dst_format = 4`, and 4 is **Tf32** — E8M10, + * ten stored mantissa bits, TRUNCATED rather than rounded, ~4.9e-4 an operand and biased low. + * `math_fidelity` cannot recover it; the bits are gone before the FPU sees the operand. + * + * Modelled off-hardware at this shape (`foundry/tt/tests/test_vector_ops.c`) the two members + * land on OPPOSITE SIDES of the gate: + * + * q/k L2 bf16 7.143e-03 -> Float32 8.406e-04 CLEARS 1e-3 + * RMS bf16 8.101e-03 -> Float32 1.566e-03 DOES NOT + * + * **The difference is the CHAIN LENGTH, not the member.** The L2 member's final chain is one + * step; the RMS member's is two — gamma, then scale — and every chain step is a circular- + * buffer round trip, so every step is one more Tf32 unpack of the value. The same program run + * with an fp32 unpack the hardware does not have scores 1.0e-07 and 2.1e-07, so the ENTIRE + * Float32 residue is truncation: no CB table moves it and the next lever is the chain's step + * count, which is `resident_reduce_compute.c` and not a table. + * + * --------------------------------------------------------------------------- + * WHAT IT MEASURED ON SILICON, AND NEITHER NORM CLEARS + * --------------------------------------------------------------------------- + * `PN_TT_GDN_NORMCHECK=1` on Ornith's real per-layer data, 48 SSM layers, twice, identical + * both times, 0 refusals: + * + * bf16 `reduce` Float32 `reduce_fp32` gate + * q/k L2 8.599e-03 1.737e-03 1e-3 4.95x better, 1.7x out + * RMS alone - 3.165e-03 1e-3 3.2x out + * gated 1.283e-02 1.023e-02 1e-3 1.25x better, 10x out + * q/k elements out 146,844/196,608 25,718/196,608 5.7x fewer + * + * **The Float32 program is a real 4.95x on the q/k norm and it is not enough.** Three things + * are named by those three rows and they are different problems: + * + * 1. **Tf32 is the floor on the reduce**, and the modelled 8.406e-04 / 1.566e-03 land about + * 2x optimistic against the card. The model uses libm for the `rsqrt`; the card uses the + * SFPU's polynomial, which is a SECOND approximation the model cannot have and the + * leading candidate for that 2x. At bf16 it was invisible under a rounding 8x bigger. + * 2. **The RMS member pays two more Tf32 unpacks than the L2 member** — 3.165e-03 against + * 1.737e-03 — because its chain has two steps and every step is a CB round trip. + * 3. **`gated` barely moved because the reduce is not what bounds it.** It is `silu(z) *` + * the RMS answer on the **bf16** ELEMENTWISE residency, so the fp32 answer is rendered + * into a bf16 strip before the gate sees it. Fixing the reduce cannot help `gated` until + * that bridge goes, which is the qkv/z strip item and not this one. + * + * @status RUN ON SILICON 2026-08-19, p150a, core 91 — armed, 96 descriptors over two + * tokens, 0 refusals, 0 restarts, core `instrn_buf_status` 0x77 after. **THE LAYOUT + * RESULT ABOVE IS CONFIRMED**: `row_blocks = heads` over a `[1, heads*S]` strip + * scores 8.6e-03 against the oracle, not the O(1) a wrong axis would give. + * @issues None + * @todo None + */ + +#ifndef PN_TT_HEAD_NORM_H +#define PN_TT_HEAD_NORM_H + +#ifdef USE_TT + +#include + +#include "pn_tt_device.h" +#include "pn_tt_fp32_strip.h" +#include "tt/ft_tt_op_vector.h" + +/** + * Arm a per-head reduce residency (one more core) placed for `head_width`. + * + * IDEMPOTENT PER WIDTH: asking twice for the same width is a no-op, and asking for a second + * width takes a second core. A model with two head widths calls this twice. + * + * Never fatal: a card without the `reduce_fp32` program, a core already held, or no slot + * left leaves that width's per-head norms on the host and + * `pn_tt_head_norm_is_armed(device, width)` false. + * + * @param head_width the head width in ELEMENTS, a multiple of 32 + * @return 0 (even when nothing armed), -1 on a bad argument + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_head_norm(pn_tt_device *device, uint32_t head_width); + +/** + * The same, naming the PAGE SIZE — which is also which reduce program arms. + * + * `FT_TT_OP_TILE_BYTES_FP32` is `reduce_fp32` and what `pn_tt_device_arm_head_norm()` asks + * for; `FT_TT_OP_TILE_BYTES_BF16` is the shipped `reduce`. + * + * **bf16 IS THE RIGHT ANSWER FOR ATTENTION'S QK-NORM AND THE WRONG ONE FOR THE GDN'S.** The + * GDN norms sit in the middle of an fp32 recurrence, so the Tf32 floor is the best available + * and bf16 misses the 1e-3 gate by 9x. Flash decode is bf16 END TO END by construction — the + * KV cache is bf16 (`ft_tt_op_memory.h` §1.6), the constants are bf16, the RoPE table is + * bf16 — so a Float32 QK-norm would need a bf16->fp32 render before it and an fp32->bf16 + * render after, NEITHER OF WHICH EXISTS on the card, and the answer would then be rounded + * straight back to bf16 by the very next op. It would buy precision the next descriptor + * discards, at the cost of the strip bridge the GDN norms are already blocked on. + * + * A width already placed at the other page is a REFUSAL, not a second slot: routing is by + * width, and two slots at one width would be a coin toss. + * + * @param tile_bytes `FT_TT_OP_TILE_BYTES_FP32` or `FT_TT_OP_TILE_BYTES_BF16` — foundry's + * own names, because the page size is foundry's contract and a second + * spelling of it here would be a second thing to keep in step + * @return 0 (even when nothing armed), -1 on a bad argument or a page clash + */ +int pn_tt_device_arm_head_norm_at(pn_tt_device *device, uint32_t head_width, + uint32_t tile_bytes); + +/** Did a per-head reduce residency for THIS width arm for this run? + * `head_width` in ELEMENTS; 0 asks whether ANY did. */ +bool pn_tt_head_norm_is_armed(const pn_tt_device *device, uint32_t head_width); + +/** + * Upload one member's constants buffer: the scaler tile, the eps tile, then gamma expanded + * to full tiles when the member takes one. + * + * @param function which member the constants are FOR — the scaler and the eps tile both + * depend on it, so a buffer built for one member is wrong for another + * @param gamma `width` values, or NULL for a member with no gamma + * @param width the head width in ELEMENTS, which must be the armed one + * @param eps what the member's reduce should see added; **L2 norm takes `eps*eps`** + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_head_norm_constants_upload(pn_tt_device *device, ft_tt_op_reduce_function function, + const float *gamma, uint32_t width, float eps, + pn_tt_norm_constants *out_constants); + +/** + * One per-head norm over a row-vector strip: `heads` independent reduces of `width` + * elements each, in ONE descriptor. + * + * `input` and `output` are **Float32** strips — this residency is placed by + * `reduce_float32.cbtable` and its reader issues 4096-byte reads. Each must hold at least + * `heads * width / 32` tiles, and they MAY be the same strip — the reader has read a row block before the writer can write it, and the + * blocks do not alias. + * + * Which residency runs it is chosen by `constants->tile_cols`, so a caller cannot point a + * width at the wrong core: the constants and the placement are the same width by + * construction, and a width nothing armed is a refusal rather than a wrong reduce. + * + * @param heads row blocks, i.e. how many heads this descriptor norms + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_head_norm(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + const pn_tt_fp32_strip *input, const pn_tt_fp32_strip *output); + +/** The same over ordinary bf16 activation strips, for a slot armed at `PN_TT_TILE_BF16`. + * Handing either entry point the other page's operands is REFUSED — the page size is a + * three-sided contract and the card cannot see a violation of it. */ +int pn_tt_head_norm_bf16(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + const pn_tt_activation *input, const pn_tt_activation *output); + +/** + * The same over RAW OPERANDS, for a caller whose buffers are neither of the two strip types. + * + * The GDN block's are: `conv_out` and `y` are allocated by the GDN family and addressed by its + * own `ft_tt_op_operand` accessors, and the whole point of the on-card chain is that the norms + * read them WHERE THEY LIE instead of through a host round trip. An fp32 strip and a GDN + * buffer differ in nothing this function needs except the address, and the page each must + * carry is still checked against the SLOT's. + * + * `input_tiles` / `output_tiles` are what the caller is willing to have read and written; the + * `heads * tile_cols` this descriptor actually touches must fit inside both. + * + * **INPUT AND OUTPUT MAY BE THE SAME BUFFER.** The GDN q/k norm runs IN PLACE over the first + * `2*k_heads*state_tiles` tiles of the conv output, and that is safe by pipeline order rather + * than by luck: the reader prefetches ONE row block, so block `b` has been read before the + * writer can write block `b`. Nothing here enforces it — it is stated so that an in-place + * caller is a decision rather than an accident. + */ +int pn_tt_head_norm_operands(pn_tt_device *device, ft_tt_op_reduce_function function, + const pn_tt_norm_constants *constants, uint32_t heads, + ft_tt_op_operand input, ft_tt_op_operand output, + uint32_t input_tiles, uint32_t output_tiles); + +/** PM 7 ITEM 7: defer the awaits of the `tile_cols`-wide head-norm slot (`on`), and settle + * them. See pn_tt_residency_settle(). */ +void pn_tt_head_norm_defer(pn_tt_device *device, uint32_t tile_cols, bool on); +int pn_tt_head_norm_settle(pn_tt_device *device, uint32_t tile_cols); + +#endif /* USE_TT */ +#endif /* PN_TT_HEAD_NORM_H */ diff --git a/src/accel/tt/pn_tt_issue.c b/src/accel/tt/pn_tt_issue.c new file mode 100644 index 0000000..223e521 --- /dev/null +++ b/src/accel/tt/pn_tt_issue.c @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_issue.c + * @brief Implementation of the issue-shape selector (see pn_tt_issue.h for the whole + * argument, including the WC/UC store-ordering seam). + * + * @status Green. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_issue.h" + +#include +#include +#include + +#include "pn_tt_name_list.h" + +#define PN_TT_ISSUE_ENV "PN_TT_ISSUE" + +/** Bytes one WORD32 access moves. The other unit is FT_TT_NOC_COPY_UNIT_BYTES. */ +#define PN_TT_ISSUE_WORD_BYTES 4u + +/** Bytes one BLOCK16 access moves — the same 16 the L1 alignment rule is written in. */ +#define PN_TT_ISSUE_BLOCK_BYTES 16u + +pn_tt_issue_shape pn_tt_issue_shape_selected(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv(PN_TT_ISSUE_ENV); + + /* DEFAULT `wide` SINCE 2026-08-27, AND THE FLIP IS ITS OWN CONFIRMATION ROUND rather + * than an inference off the ladder that unblocked it. `wide` was a 5.7% REGRESSION + * while every issue paid a second TLB re-aim at 8.9 us against the 25 accesses' 0.8; + * with the window bank absorbing both aims it is a win, and the round that flipped it + * measured decode 0.1391 (word, three arms, all three to four decimals — a 0.00% + * control-vs-control drift) against 0.1381/0.1378 (wide): **+0.83%, -1.15 ms/token**. + * Prefill 165.19/165.27 -> 166.52 tok/s on the `batched prefill —` line's own + * denominator, **+0.80%** against a 0.048% drift, the gain in the `sequential` term. + * Every arm `cmp` IDENTICAL at 189 B / 80fc6259, and the two 256-token soaks are + * byte-identical to each other at 587 B / abca1fa9. The instrument that says the knob + * armed is the issue clock: 10.0-10.1 -> 5.8-6.1 us/op. See pn_tt_issue.h. */ + cached = (int)PN_TT_ISSUE_WIDE; + if (value && *value) { + if (strcmp(value, "word") == 0) + cached = (int)PN_TT_ISSUE_WORD; + else if (strcmp(value, "wide") == 0) + cached = (int)PN_TT_ISSUE_WIDE; + else + fprintf(stderr, + "pn_tt: %s=%s is not word|wide — using the default (wide)\n", + PN_TT_ISSUE_ENV, value); + } + } + return (pn_tt_issue_shape)cached; +} + +const char *pn_tt_issue_shape_string(pn_tt_issue_shape shape) +{ + return shape == PN_TT_ISSUE_WIDE ? "wide" : "word"; +} + +pn_tt_issue_shape pn_tt_issue_shape_for(const ft_tt_runtime *runtime, const ft_tt_queue *queue) +{ + if (pn_tt_issue_shape_selected() != PN_TT_ISSUE_WIDE) + return PN_TT_ISSUE_WORD; + /* A DEMOTION, NOT A FAILURE. A queue placed where a 16-byte write cannot reach it would + * otherwise refuse its first parameter block and take the whole family to the host — a + * loud failure, but a needless one when the shipped shape is right there. */ + if (!runtime || !runtime->bulk_l1.write || !ft_tt_queue_issue_is_wide_addressable(queue)) + return PN_TT_ISSUE_WORD; + return PN_TT_ISSUE_WIDE; +} + +const ft_tt_noc_access *pn_tt_issue_access(const ft_tt_runtime *runtime, + pn_tt_issue_shape shape) +{ + if (!runtime) + return NULL; + return shape == PN_TT_ISSUE_WIDE ? &runtime->bulk_l1 : &runtime->noc; +} + +uint32_t pn_tt_issue_unit_bytes(pn_tt_issue_shape shape) +{ + return shape == PN_TT_ISSUE_WIDE ? PN_TT_ISSUE_BLOCK_BYTES : PN_TT_ISSUE_WORD_BYTES; +} + +uint64_t pn_tt_issue_accesses(pn_tt_issue_shape shape, uint32_t payload_bytes) +{ + /* The head bump is ALWAYS one WORD32 access, on both shapes, because nothing else can + * carry a 4-byte write. That single word is why `wide` is 7 and not 6. */ + return (uint64_t)payload_bytes / pn_tt_issue_unit_bytes(shape) + 1u; +} + +bool pn_tt_issue_red_head_first(void) +{ + static int cached = -1; + + if (cached < 0) { + /* SELECTS, NOT CONTAINS. `pn_tt_name_list_contains()` answers TRUE for an UNSET list + * by design, so arming a red arm with it arms it on every run — measured on silicon + * 2026-08-25, and it cost a cross-agent bisect the wrong commit. */ + cached = pn_tt_name_list_selects(getenv("PN_TT_RED"), "issue_head_first") ? 1 : 0; + } + return cached != 0; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_issue.h b/src/accel/tt/pn_tt_issue.h new file mode 100644 index 0000000..b51d0c9 --- /dev/null +++ b/src/accel/tt/pn_tt_issue.h @@ -0,0 +1,179 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_issue.h + * @brief The ISSUE half of a descriptor's host cost — which accessor the parameter block and + * the ring slot are written through, and what that costs in PCIe accesses. + * + * --------------------------------------------------------------------------- + * THE OTHER HALF OF THE 58 us, AND IT IS THE SMALLER ONE + * --------------------------------------------------------------------------- + * `pn_tt_residency.c` prices the AWAIT: seventeen non-posted reads through the UC / WORD32 + * accessor, taken to six and then to one by `PN_TT_RESIDENCY_AWAIT`. The ISSUE side is the + * same arithmetic on the same link in the write direction, and it has never been cut: + * + * `write_op_parameters` 64 B -> 16 word writes, POSTED + * `stage` (one ring slot) 32 B -> 8 word writes, POSTED + * `publish` (head bump) 4 B -> 1 word write, POSTED + * ----------------------- + * 25 POSTED ACCESSES + * + * **Posted stores are not a round trip**, which is why issue was always the cheaper half and + * why the await was cut first. What the two shapes below change is the ACCESS COUNT, not the + * number of round trips — there are none. + * + * --------------------------------------------------------------------------- + * THE TWO SHAPES, AND WHY `wide` IS TWO ACCESSORS AND NOT ONE + * --------------------------------------------------------------------------- + * `word` everything through `runtime->noc` 16 + 8 + 1 = **25** what shipped + * `wide` the two 16-byte-aligned writes through `runtime->bulk_l1` (WC / BLOCK16) and + * the head bump through `runtime->noc` 4 + 2 + 1 = **7** + * + * The parameter block and the ring slot are 16 bytes aligned in ADDRESS and in LENGTH, so + * both fit the bulk L1 accessor. The head bump is four bytes at an arbitrary word and does + * not — `ft_tt_noc_check_transfer()` refuses it, which is a structural fact and not a + * convention. `ft_tt_queue_issue_is_wide_addressable()` asks that same function about a + * queue's placement, so a queue the wide shape cannot reach FALLS BACK rather than failing on + * its first descriptor. + * + * --------------------------------------------------------------------------- + * THE WC/UC STORE-ORDERING SEAM — THE ONLY THING `wide` PUTS AT RISK + * --------------------------------------------------------------------------- + * `wide` splits one descriptor's issue across TWO MAPPINGS of the same BAR: a write-combined + * window for the payload and an uncached one for the head bump that announces it. "Descriptors + * before head" is one of the two orderings the whole queue protocol rests on + * (`ft_tt_queue.h`), and a WC store is allowed to sit in the core's write-combining buffer + * from the program's point of view. If the head bump overtakes the ring write, the kernel + * executes **the descriptor that slot held one lap of the ring ago** — well formed, no kernel + * error, and `completed_count` exactly what the host expected. It is a wrong answer, not a + * hang, and the ONLY thing that separates it from a correct one is the per-index token. + * + * Three things stand between that and the model, and they are named here rather than assumed: + * + * 1. **`ft_tt_udrv_noc_transfer()` ends every WRITE slice with `ft_tt_noc_copy_store_fence()` + * — an `sfence` — unconditionally, on both widths.** So the WC payload is retired from the + * CPU before the UC head bump is issued. This is not new code and not this knob's: the + * transport has always done it, because "an unrelated module's later UC access happens to + * drain the buffer" is a timing property of one host. + * 2. **Both windows carry the SAME NoC aim template** — `ft_tt_udrv_noc_path_open()` memsets + * it and nothing calls `set_aim_template()`, so neither window selects a different + * ordering mode or a static VC. Two PCIe posted writes to one device on one path cannot + * pass each other. + * 3. **The four-agreement check is live on every descriptor**, and the token is what catches + * this. `tests/test_queue.c` publishes a head for a descriptor that was never written and + * requires the check to refuse and to name the token — with the COUNT passing and the + * kernel reporting no error, which is what makes the arm about ORDERING rather than about + * arithmetic. `PN_TT_RED=issue_head_first` is the same inversion on the card. + * + * The read direction of this seam already ships: `PN_TT_RESIDENCY_AWAIT=line` and the matmul + * group's fast completion have both polled through `bulk_l1` (WC) while the same descriptors + * were issued through `noc` (UC) since 2026-08-20. + * + * --------------------------------------------------------------------------- + * MEASURED 2026-08-26: `wide` HITS ITS ACCESS TARGET EXACTLY AND IS 5.7% SLOWER + * --------------------------------------------------------------------------- + * 25.00 -> 7.00 accesses an issue, to the digit, with the window byte counters agreeing + * (the UC window's write bytes fall by 125,354.7 B/token and the WC window's rise by + * 125,354.7). The matmul ledger goes 9.33 -> 3.08 accesses a pair. **Decode goes + * 6.51 -> 6.14 tok/s** against a same-session control drift of 0.15%, and the two 256-token + * soaks are byte-identical, so this is a clean loss and not a correctness problem. + * + * **THE ACCESSES WERE NEVER THE COST. THE TLB RE-AIM IS.** The three calls timed apart say + * so directly: on `word`, sixteen accesses cost 8.69 us and eight cost 0.14, because the + * FIRST device access of an enqueue re-aims the window and everything after it rides an + * aperture already pointed at that core. Measured on the card, an aim is **8.92 us, of which + * 98.0% is its four non-posted readback reads** (2.19 us each) — and the dispatch path takes + * **4,333 aims a token, 0.0387 s, 25.1% of the decode wall.** + * + * `wide` loses because it puts the payload on one window and the head bump on the other, so + * it pays TWO aims per issue instead of one: 6,483 aims a token, and the whole loss lands in + * the `head` clock (0.66 -> 7.83 us). + * + * **IT WAS A BLOCKED LEVER, NOT A DEAD ONE, AND THE BLOCK IS GONE.** With the aim removed, + * `word`'s 25 accesses are ~0.8 us an issue and `wide`'s 7 are ~0.2, so `wide` was projected + * at ~1.4 ms/token: worth having AFTER the aim is removed and worth -17 ms/token before it. + * Its own confirmation round measured **-1.15 ms/token** against a stated ~1.4 and flipped the + * default (see @status). It is also the instrument that priced the aim in the first place: + * two shapes differing by exactly one aim per issue is how 8.9 us was measured three + * independent ways before the aim had a clock of its own. + * + * ONE THING THE ROUND DID NOT SETTLE. The matmul group's own `dispatch split` does not + * reconcile with the wall: ISSUE falls 0.126 -> 0.075 s over 48 tokens (-1.06 ms/token) and + * WAIT rises 2.708 -> 2.760 s (+1.09 ms/token), which nets to zero, while the WALL falls + * 1.15 ms/token. The split's WAIT term is card time the host is partly running ahead of, so + * it is not a second clock on the same quantity — the wall is the instrument and the split is + * the evidence the knob armed. + * + * @status Green, and `wide` SHIPS since 2026-08-27. It was a 5.7% regression only while every + * issue paid a second TLB re-aim; with the window bank absorbing both aims its own + * confirmation round measured decode 0.1391 s/token (word, THREE arms, all three to four + * decimals — a 0.00% control-vs-control drift) against 0.1381/0.1378: **+0.83%, + * -1.15 ms/token**. Prefill 165.19/165.27 -> 166.52 tok/s on the `batched prefill —` line's + * own denominator, **+0.80%** against a 0.048% drift, the gain in the `sequential` term. + * Every arm `cmp` IDENTICAL at 189 B / 80fc6259; two 256-token soaks byte-identical at + * 587 B / abca1fa9. The instrument that says the knob armed is the issue clock, + * 10.0-10.1 -> 5.8-6.1 us/op. + * @issues None + * @todo None + */ + +#ifndef PN_TT_ISSUE_H +#define PN_TT_ISSUE_H + +#ifdef USE_TT + +#include +#include + +#include "tt/ft_tt_queue.h" +#include "tt/ft_tt_runtime.h" + +/** Which accessor a descriptor's payload is written through. */ +typedef enum pn_tt_issue_shape { + PN_TT_ISSUE_WORD = 0, /**< UC / WORD32 for all three writes — 25 accesses. */ + PN_TT_ISSUE_WIDE /**< WC / BLOCK16 for the payload, UC for the head — 7. */ +} pn_tt_issue_shape; + +/** `PN_TT_ISSUE=word|wide`, read ONCE. An unrecognised value is refused to the default and + * says so, rather than silently selecting one. */ +pn_tt_issue_shape pn_tt_issue_shape_selected(void); + +/** "word" or "wide"; never NULL. */ +const char *pn_tt_issue_shape_string(pn_tt_issue_shape shape); + +/** + * The shape actually usable for `queue` on `runtime`: `pn_tt_issue_shape_selected()` demoted + * to `PN_TT_ISSUE_WORD` when the queue's blocks are not 16-byte addressable or the runtime has + * no bulk L1 accessor. Decide it ONCE at arm time — a per-descriptor `getenv` is 562 of them a + * token on the resident families alone. + */ +pn_tt_issue_shape pn_tt_issue_shape_for(const ft_tt_runtime *runtime, const ft_tt_queue *queue); + +/** The accessor `shape` names. Never NULL for a non-NULL runtime. */ +const ft_tt_noc_access *pn_tt_issue_access(const ft_tt_runtime *runtime, + pn_tt_issue_shape shape); + +/** Bytes one PCIe access of `shape`'s payload accessor moves: 4 for WORD32, 16 for BLOCK16. + * This is the unit the whole link is priced in — a CALL count cannot tell a 4-byte write + * from a 64-byte one, and a BYTE count cannot tell sixteen words from four blocks. */ +uint32_t pn_tt_issue_unit_bytes(pn_tt_issue_shape shape); + +/** Accesses `bytes` of payload cost at `shape`, plus the head bump's single word. */ +uint64_t pn_tt_issue_accesses(pn_tt_issue_shape shape, uint32_t payload_bytes); + +/** + * RED PROOF ONLY. Is `PN_TT_RED=issue_head_first` named? The arm publishes the head BEFORE the + * descriptor is written — the exact inversion the WC/UC seam risks — so the four-agreement + * check must REFUSE and must name the TOKEN. + * + * An ordering claim that cannot be inverted cannot be tested: without this, a clean `wide` run + * is equally consistent with the check being vacuous. + * + * This is only the knob. WHEN it fires is the caller's — `pn_tt_residency.c` waits for the + * ring to have wrapped once and fires once, because a head published over a ZEROED slot is an + * unknown opcode and proves something else entirely. + */ +bool pn_tt_issue_red_head_first(void); + +#endif /* USE_TT */ +#endif /* PN_TT_ISSUE_H */ diff --git a/src/accel/tt/pn_tt_matmul_group.c b/src/accel/tt/pn_tt_matmul_group.c new file mode 100644 index 0000000..0d24dfc --- /dev/null +++ b/src/accel/tt/pn_tt_matmul_group.c @@ -0,0 +1,4245 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_matmul_group.c + * @brief Implementation of the multi-core matmul group (see pn_tt_matmul_group.h) — the + * resident triads, the load-time bank shard, and the split dispatch. + * + * Everything about ONE residency lives here too, including the single-member case: + * a group of one is the pre-group path with the same core, the same queue and the + * same descriptor words, which is what makes `PN_TT_MATMUL_CORES=1` a control rather + * than a second implementation. + * + * @status Green at N=1..8 on MiniCPM5-1B; N=1 is byte-identical to the pre-group tree, and + * so are the cooperative arms at N=4 and N=8. + * @issues The COOPERATIVE multicast is byte-exact and its trade DEPENDS ON THE PREFETCH + * DEPTH, because the rendezvous is paid once per block: at depth 8 it costs 1.24x more + * card time at N=4, at depth 16 it SAVES 1.059x at N=4 and 1.137x at N=8. It is off by + * default because it also moves the member cores onto a rectangle, and because the N=8 + * arm has left one member of the second rectangle unbootable at its next boot in five of + * five pairs, with the split control on the same cores clean immediately before each. + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_matmul_group.h" + +#include +#include +#include +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_core_boot_guard.h" + +#include "tt/ft_tt_coord.h" +#include "tt/ft_tt_op_dispatch.h" +#include "tt/ft_tt_op_l1_layout.h" +#include "tt/ft_tt_op_matmul_cooperative.h" +#include "tt/ft_tt_group_shared_operand.h" +#include "tt/ft_tt_op_matmul_resident_multicast.h" +#include "tt/ft_tt_program_resolve.h" +#include "tt/kernels/resident_matmul_block_protocol.h" + +/** How many Tensix one projection is split across. `PN_TT_MATMUL_CORES=1` is the pre-group + * path and the control arm. */ +#define PN_TT_MATMUL_CORES_ENV "PN_TT_MATMUL_CORES" + +/** + * EIGHT SINCE 2026-08-19, and it is the MEASURED WALL OPTIMUM rather than a preference. + * + * It was 1 — "multi-core dispatch is asked for, never inferred" — which meant a serve with + * no env at all ran the single-core path at 1.28 tok/s while the serve config's own + * `PN_TT_MATMUL_CORES=4` got 1.94. That gap was a configuration, not a finding. + * + * Swept with the hot A strips mirrored across banks, matmul CARD work and the WALL, every + * arm `cmp` IDENTICAL and `company=0`: + * + * members 1 2 4 8 12 16 24 32 + * card work .3949 .2001 .1045 .0611 .0559 .0594 .0850 .1114 s/token + * WALL .7858 .5932 .5035 .4687 .4743 .4860 .5318 .5764 s/token + * + * **The card bottoms at 12 and the WALL bottoms at 8, and the difference between them is + * HOST ISSUE.** Issue is 2.42 ms/token per member — fitted across the whole sweep, and + * `wall(N) = 0.3885 + 0.00242*N + card(N)` reproduces all eight measurements to 1 ms. So + * every member past 8 buys less card time than it spends on the host writing its + * descriptor, and 32 members spend 0.077 s/token of pure host issue, which alone exceeds + * the entire matmul's card work. + * + * Members run upward from PN_TT_MATMUL_CORE_FIRST (75), so eight of them occupy 75-81 plus + * member 0 at 71 — clear of the reduce/eltwise/sample residencies at 72/73/74, of the GDN + * at 90 and its head-split members from 92, and of the head norms at 91. + * + * --------------------------------------------------------------------------- + * RE-SWEPT 2026-08-20 WITH THE TWO HOST MECHANISMS ON, AND 8 STAYS + * --------------------------------------------------------------------------- + * members 1 2 4 8 12 16 24 32 + * card work .3939 .1969 .0984 .0498 .0379 .0329 .0405 .0524 s/token + * WALL .7830 .5901 .4996 .4540 .4503 .4593 .4774 .5047 s/token + * tok/s 1.28 1.69 2.00 2.20 2.22 2.18 2.09 1.98 + * + * **The card knee moved 12 -> 16 and the wall knee 8 -> 12** — the whole of the control + * curve's 12-to-16 turn-up was the host's own per-member completion read inside WAIT, and + * removing two of its three transactions removed it. `b` is 0.00199 now, so 32 members + * still spend 0.064 s/token on host issue. + * + * **THE DEFAULT STAYS 8 ANYWAY, and the reason is placement, not speed.** Twelve members + * run 75..85, which lands ON the attention family's rope/memory/attention residencies at + * **82/83/84** — `pn_tt_prepare_family()` refuses a residency on a member's core and logs + * it, so a 12-member default would silently drop Phase 5's attention back to the host the + * day it ships. The measured gain for taking that risk is 2.20 -> 2.22 tok/s, which is + * inside the 2.14/2.13 drift between this session's own opening and closing controls. + * **Moving to 12 means moving `PN_TT_MATMUL_CORE_FIRST` first and re-measuring, because + * the placement changes the NoC distances.** + * + * --------------------------------------------------------------------------- + * DONE 2026-08-28 ON PREFILL, AND THE ANSWER IS THE OPPOSITE OF THE DECODE SWEEP'S + * --------------------------------------------------------------------------- + * **"92..102 is eleven free cores" WAS WRONG ON BOTH ENDS and cost two rounds.** 95 is the + * gdnc residency, 92-94 are the GDN head-split members, and the RESIDENCY TABLE does not + * list every occupant — `head-norm-2` at 103 and the eltwise member at 106 have no row, + * because their clocks are the recorded twenty-eighth vacuous-gate class. The DEFAULTS in + * the source are the only complete map. What places clean today: + * + * PN_TT_MATMUL_CORE_FIRST=96 PN_TT_GDNC_MEMBER_CORE_FIRST=114 + * PN_TT_CORE_HEAD_NORM_2=111 PN_TT_ELTWISE_MEMBER_CORE_FIRST=112 + * + * On a 1,629-token prefill, `cmp` IDENTICAL at every rung against a **0.02%** control drift: + * + * members 8 (75..81) 8 (96..102) 12 (96..106) 16 (96..110) + * tok/s 194.77 194.62 204.16 205.78 + * batched 3.3286 3.3322 2.9317 2.7070 s + * GB/s 194.8 194.9 249.5 310.1 + * decode 7.74 — 8.36 8.74 tok/s + * + * **Moving the same eight members is FREE (-0.08%, inside drift), so NoC distance is not a + * term here.** Prefill +5.7% and DECODE +13.0% at 16 — where the recorded decode sweep above + * has 16 WORSE than 8, because on decode the per-member host term ate the card saving and on + * prefill a descriptor carries 32 positions. + * + * **AND THE HOST TERM'S IDENTITY IS TLB WINDOW RE-AIMS, MEASURED:** window aims go + * **829.2 -> 1,939.9 -> 7,566.1 a token** at N = 8/12/16 for **0.0090 -> 0.0192 -> 0.0700 + * s/token**, as the bulk bank's hit rate falls **99.74% -> 99.50% -> 98.20% on 32 windows** + * — and **95-98% of every aim is the verification READBACK**. The three writes per member + * are unchanged in count and shape at every N. At 16 the aims are **7.1% of the wall**, so + * the knee between 12 and 16 is the BANK, not the descriptors: + * `FT_TT_UDRV_NOC_BANK_MAX_WINDOWS` is a compile-time 32. + */ +/* SIXTEEN SINCE 2026-10 (PX7+PX8 flipped as ONE change, its own confirmation round): with + * the placement below and a 64-window bank, prefill 195.4 -> 213.4 and decode 7.73 -> 8.83. + * Neither half is worth much alone. */ +#define PN_TT_MATMUL_CORES_DEFAULT 16u + +/** Where members 1..N-1 sit. Member 0 keeps `PN_TT_CORE` (71). 96 is the PX7 placement: + * sixteen members run 96..110, clear of 72/73/74, attention at 82..84, the GDN at 90..95, + * head norm 91, and of head-norm-2 (111), the eltwise member (112) and gdnc (114..120), + * which moved WITH it. Was 75, which caps the group at 8 below attention. */ +#define PN_TT_MATMUL_CORE_FIRST_ENV "PN_TT_MATMUL_CORE_FIRST" +#define PN_TT_MATMUL_CORE_FIRST_DEFAULT 96u + +/** + * THE SHARED ACTIVATION, MULTICAST ONCE INSTEAD OF READ N TIMES. Off by default. + * + * At Mt = 1 every member re-reads the whole A column once per output column, so A is + * 1.76 GB of the 2.70 GB a token reads — 65% — and every one of those reads goes to the + * ONE DRAM bank holding that strip. Four cores buy 3.44x of card time and SIXTEEN buy + * 4.80x; door rotation alone is worth 1.27x at N=8, and a gain that grows with N is what + * an endpoint ceiling looks like. With this on, the leader reads A once and multicasts it. + * + * **IT MOVES THE MEMBER CORES, which is why it is opt-in rather than simply better.** A + * multicast writes every node of its bounding box, and the default member set + * {71, 75, 76, 77} is an arithmetic run whose box also contains 72, 73 and 74 — the + * reduce, eltwise and sample residencies. So the members are placed as a RECTANGLE from + * `PN_TT_CORE`, and `PN_TT_MATMUL_CORE_FIRST` is not consulted. Choose `PN_TT_CORE` so + * the rectangle clears those three: on a 14-column part, 80 puts a 1x4 group at + * translated x 13..16 of row 7 and a 2x4 group at rows 7-8, both clear of everything. + * + * `PN_TT_MATMUL_COLUMNS` overrides the width; by default it is the largest divisor of the + * member count that is at most four, so N=4 is 1x4, N=8 is 2x4 and N=6 is 2x3. Four is the + * standing hazard limit on destinations in ONE packet, and a row is one packet. + */ +#define PN_TT_MATMUL_COOPERATIVE_ENV "PN_TT_MATMUL_COOPERATIVE" +#define PN_TT_MATMUL_COLUMNS_ENV "PN_TT_MATMUL_COLUMNS" + +/** + * PLACE the members as a rectangle WITHOUT cooperating. + * + * It exists to make the A/B honest. Cooperation moves the member cores, and "the cores + * moved" and "the leader multicasts A" would otherwise be one change — so a speedup could + * be either. With this set the cores are exactly where a cooperating group puts them and + * every member still reads A from DRAM, which leaves the multicast as the only variable. + * `PN_TT_MATMUL_COOPERATIVE=1` implies it. + */ +#define PN_TT_MATMUL_RECTANGLE_ENV "PN_TT_MATMUL_RECTANGLE" + +/** + * RED PROOF ONLY. `PN_TT_RED_COOP=stale` makes the LEADER skip the payload multicast while + * still telling every receiver the block arrived, so each receiver computes from the + * PREVIOUS block's bytes. + * + * One arm, two claims, which is why it is the cooperative red proof worth having: + * - the byte comparison can still go red on the cooperative path — the same question + * PN_TT_RED_SPLIT answers for the split path; + * - the multicast is the ONLY source of A on a receiver. A receiver that were still + * reading A from DRAM — a leftover read, a role that did not take, a branch that did + * not fold — would make this arm produce IDENTICAL output, and identical output under a + * deliberate break is exactly what a vacuous gate looks like. + */ +#define PN_TT_RED_COOP_ENV "PN_TT_RED_COOP" + +/** Spins ONE rendezvous wait takes before abandoning. Bounded for the same reason every + * other wait here is: a wait that cannot expire is a wait only `assert_reset` can end, + * and asserting reset on a blocked triad is how cores were wedged. Sized well above a + * block's real cost and well below the idle bound, so a group that has fallen out of + * lockstep is reported in milliseconds rather than seconds. */ +#define PN_TT_COOP_SPIN_LIMIT 20000000u + +/** + * RED PROOF ONLY. `PN_TT_RED_SPLIT` breaks the split on member 1 in one named way, on the + * SHIPPING path, so the byte comparison against N=1 is proven able to go red on silicon: + * + * base member 1 reads member 0's weight panel — the "two units that share a resource + * will silently share a BASE ADDRESS too" defect, which produced a valid panel of + * the same matmul from a live endpoint with every status word clean + * out member 1 writes over member 0's output panel — the overwrite that byte + * comparison exists to catch and that PCC scored as fine + * narrow member 1 computes one column fewer, so exactly 32 output values of one op are + * stale. One element out of thousands, on one member of N, is the resolution a + * multicore gate has to have + * + * A knob rather than a test build, for the same reason `PN_TT_RED_BAD_DESCRIPTOR` is one: + * the claim is about the shipping path, and a separate binary proves it about a different + * program. + */ +#define PN_TT_RED_SPLIT_ENV "PN_TT_RED_SPLIT" + +/** Microseconds a completion poll sleeps between device reads. 0 — the default and what + * every recorded figure was taken with — is the tightest loop. It exists to ask whether + * watching the core costs the core; see pn_tt_group_await(). */ +#define PN_TT_POLL_BACKOFF_ENV "PN_TT_POLL_BACKOFF_US" + +/** `PN_TT_ACTIVATION_DOORS=0` leaves every member on the activation bank's OWN endpoint + * (port 0), which is what a caller got before striping existed. It is the control that + * separates "the shared A operand is the binding read" from "N cores are N cores": the + * bytes, the addresses and the kernels are identical in both arms and only the door + * moves, so a difference can only be the endpoint ceiling. Default on. */ +#define PN_TT_ACTIVATION_DOORS_ENV "PN_TT_ACTIVATION_DOORS" + +/** + * `PN_TT_MATMUL_BLOCK=N` makes the reader supply N output columns per pass instead of one. + * + * WHY IT EXISTS. At Mt = 1 the reader's A offset is `(mt*Kt + kt)` and does not depend on + * `nt`, so the unblocked loop re-reads the whole A column ONCE PER OUTPUT COLUMN. Counted + * from the descriptors that is **15.871 of the 24.303 GB a token reads, 65.3%**, and 74% of + * it comes out of the single bank the `normed` strip lives in at 43.1 of that bank's 59.2 + * GB/s ceiling while the card as a whole sits at 16% of its 510.8. At N columns per pass A + * costs 1/N of that: 12.400 GB/token at N = 4. + * + * WHY 4 IS THE CEILING. The block's N results live in DST for the whole K loop, and a DST + * SECTION is `DEST_REGISTER_HALF_SIZE >> 1` rows with `dst_accum_mode 1` — **4 tiles of + * 32x32** (ckernel.h `get_dest_max_tiles`, SyncHalf + ACCUM_MODE). The matmul CB table has + * `dst_accum_mode 1` and it is worth 13-35x accuracy, so 8 is not available by dropping it. + * A larger value would write outside the half with nothing refusing anywhere. + * + * IT IS A PER-RESIDENCY CONSTANT. CB_IN1's chunk becomes `depth * N` pages, and + * `ft_tensix_cb_block_grid_is_safe()` refuses a chunk size that CHANGES through a buffer — + * the write pointer wraps only on an exact hit of the limit. So the same value goes into + * every descriptor and the CBs are sized for it; a weight whose Nt is not a multiple of N + * takes a SHORT last block, which shortens the fill and the tile count but never the chunk. + * + * **DEFAULT 4, ON, since 2026-08-19.** Gated on the 48-token oracle and nothing softer: three + * separate NB=4 runs and one NB=2 run are `cmp` IDENTICAL (189 B, md5 `80fc6259`) beside two + * NB=1 controls, every one of them with zero residency re-arms and zero contention. Measured + * at the shipped `PN_TT_MATMUL_CORES=4`, depth 8: **24.303 -> 12.409 GB/token, matmul card + * work 0.2969 -> 0.1158 s (2.56x), wall 1.43 -> 1.92 tok/s.** `PN_TT_MATMUL_BLOCK=1` is the + * control arm. + */ +#define PN_TT_MATMUL_BLOCK_ENV "PN_TT_MATMUL_BLOCK" + +/** + * `PN_TT_MATMUL_DST_TILES=8` says the ARMED matmul kernel was built at `DstSync::SyncFull`, + * so `MB*NB` may reach eight instead of four (kernels/resident_matmul_dst_sync.h). + * + * IT IS AN ASSERTION ABOUT THE KERNEL, NOT A REQUEST OF IT, and that is why it is refused + * unless `PN_TT_PROGRAM_DIR` is set. The embedded blob's matmul images are SyncHalf by + * construction; against those, eight tiles is not a refusal anywhere — DST has no bounds + * check, the pass writes over the half the packer is reading, and every kernel retires + * clean with a smoothly wrong answer. The override is the only way an eight-tile kernel can + * be on the card, so it is the only state in which the claim can be true. + * + * WHAT IT BUYS AT Mt=1, WHICH IS EVERY DESCRIPTOR: NB=8, `bytes = a/8 + b = 1344` against + * the shipped `a/4 + b = 1600`, i.e. **1.19x on the matmul's DRAM bytes**. MB=2/NB=4's + * 1.52x needs `Mt >= 2` and is a host-shape question, not this one. + * + * MEASURED ON THE SHIPPING SHAPE 2026-08-27, one binary, two program roots differing ONLY in + * this define (their gdn images are byte-identical, checked): **A 4.051 -> 2.050 GB/token + * (1.976x) with B UNMOVED at 8.585, total 12.635 -> 10.635 = 1.1881x**, decode + * **0.1327/0.1325 -> 0.1295/0.1293 s/token, +2.47% (7.545 -> 7.725 tok/s)** against a 0.15% + * control-vs-control drift, and the matmul's own WAIT 0.0575 -> 0.0542 s/token (-5.7%). + * Prefill **166.35/166.49 -> 168.86 tok/s (+1.51%)** at a 0.084% drift, and the gain is + * ENTIRELY in the `batched` term (3.4662 -> 3.3265, -4.03%) while sequential and transfer + * move 0.10% and 0.07%. `cmp` IDENTICAL at 189 B / 80fc6259 on every arm; the two 256-token + * soaks are byte-identical at 587 B / abca1fa9. + * + * AND THE SERIALISATION IS PRICED ON ITS OWN, ON THE SAME KERNEL. A SyncFull image held at + * `PN_TT_MATMUL_BLOCK=4` reads the IDENTICAL 12.635 GB/token and runs 0.1331 against + * SyncHalf's 0.1326 — **the MATH/PACK double buffer is worth 0.38% of decode wall**, against + * the recorded HALF_SERIAL 0.52%. So the byte win alone is ~2.85% and 0.38 pp goes back. + */ +#define PN_TT_MATMUL_DST_TILES_ENV "PN_TT_MATMUL_DST_TILES" + +/** + * The shipped block: THE WHOLE DST SECTION, whatever the armed image's section is. + * + * It is not a sweep — NB=2 measures 0.1767 s of matmul card work against NB=4's 0.1158 and + * NB=1's 0.2969, and the section is what caps it. What changed on 2026-08-27 is where the + * section comes from. It used to be a CLAIM the operator made with `PN_TT_MATMUL_DST_TILES`, + * unverifiable from the host, so the block deliberately did NOT follow it: naming the section + * alone must not be able to move the shipped shape. It is now a FACT read off the armed + * compute image's own recorded `defines=`, so following it is following the kernel that is on + * the card. `PN_TT_MATMUL_BLOCK=4` remains the control arm on a SyncFull binary, and NB=8 is + * worth A 4.051 -> 2.050 GB/token with B unmoved, 1.1881x on the matmul's DRAM bytes. + */ +#define PN_TT_MATMUL_BLOCK_DEFAULT 0u + +/** + * `PN_TT_MATMUL_ROWS=M` makes the reader hold M A tile-rows and stream B past ALL of them, + * so the WEIGHT column is swept once for M output rows instead of once per row. + * + * IT IS THE MIRROR OF `PN_TT_MATMUL_BLOCK` AND THEY SHARE ONE CEILING. Over a descriptor: + * + * A tiles = Mt * ceil(Nt/NB) * Kt B tiles = ceil(Mt/MB) * Nt * Kt + * bytes = Mt * T * (a/NB + b/MB) T = sum of Kt*Nt + * + * and both blocks are live in DST across the whole K loop, so `MB * NB <= 4` — upstream's + * own assert, `dst_index + ct_dim*rt_dim - 1 < get_dest_max_tiles` + * (llk_math_matmul_api.h), and `dst_accum_mode` is worth 13-35x accuracy so 8 is not + * available by dropping it. + * + * **AND ON THIS MODEL IT IS WORTH TWO PER CENT.** The two blocks are symmetric in TILES and + * NOT in BYTES: NB divides the operand whose tile is the bf16 ACTIVATION (a = 2048 B) and MB + * divides the one whose tile is the WEIGHT (b = 1088 B at Bfp8_b, which is what Ornith's + * Q8_0 loads as). At Bfp8_b the lattice reads 1600 / 1568 / 2320 bytes a unit of T for + * (MB,NB) = (1,4) / (2,2) / (4,1), so the SHIPPED MB=1/NB=4 is already within 2.0% of the + * best pair — and at Bfp4_b it IS the best pair and MB only loses. **MB=4/NB=1 serves 128 + * prompt positions a weight sweep and reads 45% MORE than today**, because the A side grows + * by more than the B side shrank. `tt/tests/test_matmul_block.c` pins the tile law, the byte + * law and the optimum at each of the three weight formats. + * + * THE LAW WAS VALIDATED AGAINST SILICON WITH NO FITTED CONSTANT: it predicts + * `A/B = a/(4b) = 2048/4352 = 0.4706` at the shipped shape, and this file's own read report + * measures A 3.978 GB / B 8.432 GB = 0.4718 on Ornith. + * + * **THE LEVER THAT WOULD MATTER IS A BIGGER DST, NOT A BIGGER MB.** At `MB*NB <= 8` the + * Bfp8_b optimum is MB=2/NB=4 at 1056 bytes a unit — **1.52x** — and eight tiles is what + * `DstSync::SyncFull` gives at the same `dst_accum_mode 1`. That is a compute-stack change + * (three `ft_tensix_*` entry points encode SyncHalf) and it costs the MATH/PACK double + * buffer. MB is its PREREQUISITE rather than its rival: at DST 8 the optimum needs both. + * + * L1 was never the binding constraint — at depth 8 the three CBs are + * `MB*depth*2*2048 + depth*NB*2*tile + 4096` of a 1,351,680 B arena, which admits MB near + * 39, nine times what DST allows. + * + * IT ONLY BITES AT `Mt > 1`. Every descriptor this file writes today carries `Mt = 1` + * (`FT_TT_QUEUE_MATMUL_TILE_ROWS` below), which is a batch of 32 positions, and at Mt = 1 a + * row block changes the CB sizing and the chunk the reader pushes and NOTHING ELSE. That is + * what makes `PN_TT_MATMUL_ROWS=2` an INERTNESS arm for the new loops rather than a speed + * arm: it must be `cmp` IDENTICAL. + * + * DEFAULT 1, OFF. + */ +#define PN_TT_MATMUL_ROWS_ENV "PN_TT_MATMUL_ROWS" +#define PN_TT_MATMUL_ROWS_DEFAULT 1u + +/** + * THE CARD-AGGREGATED COMPLETION, AND WHY IT IS OFF. + * + * The design is right and the mechanism already exists: every member's WRITER posts + * {sequence, token} into its own slot of a block on member 0 (ft_tt_queue.h, the group + * completion block; resident_matmul_writer.c calls ftq_group_post), so "has every member + * finished item N" is ONE read of ONE core instead of N reads of N. That is what turns a + * term linear in the member count into a nearly flat one. + * + * **IT WEDGES THE POSTING CORE, and that is measured, not suspected.** MEASURED + * 2026-08-16, twice, on the shipping path: an N=2 decode with the post armed produced + * byte-identical output, retired every residency cleanly by asking (state EXIT, stop=1, + * head == tail == completed), and left **core 75 — the POSTER, not the destination — + * unbootable at its NEXT boot**, `soft reset 0x00047000`, BRISC out of reset and the + * subordinates never released. Against that: EIGHT arms at N = 1..8 with the post disarmed, + * every one byte-identical and every core PASS on the probe afterwards. + * + * That is the `noc_inline_dw_write` failure family of DEV_NOTES.md §1 — clean retirement, + * dead on the next boot — except the destination here is legal, 4-byte-aligned Tensix L1 + * and `ft_tensix_noc_inline_dword_write_l1()` refuses anything else. So the destination + * class is not the whole story and diagnosing it is a KERNEL round: the suspects are the + * shared per-(processor, NoC) inline staging slot and the two sent-counter drains + * interacting with the matmul writer's own outstanding non-posted writes. + * + * --------------------------------------------------------------------------- + * WHAT THE PLACEMENT SAYS ABOUT THOSE TWO ARMS, 2026-08-19 + * --------------------------------------------------------------------------- + * **Core 75 is not the leader and never was, and reading the recorded arms with that in + * hand sharpens "two shapes, proof of neither" considerably.** Member 0 is + * `device->core_index` (`PN_TT_CORE`, default **71**) and members 1..N-1 run upward from + * `PN_TT_MATMUL_CORE_FIRST` (**75**) — see `pn_tt_matmul_group_configure()` below. So: + * + * - N=2 is cores **71** (member 0, and where the group block lives) and **75** (member 1). + * - N=4 is **71, 75, 76, 77** — which is exactly the core set the post-DISARMED + * chat-parity wedge took, whole. That set is a fingerprint, and it identifies those + * numbers as this group rather than as an arbitrary four. + * + * **Both members post. Only one of them wedged, twice of two.** With the post armed, every + * member's DM1 calls `ftq_group_post()`; member 0's destination is its OWN L1 and members + * 1..N-1's is a REMOTE Tensix. The two armed wedges took core 75 — a member whose post + * crosses the NoC — and left core 71, which posts to itself, PASS on the probe both times. + * + * So the discriminator the recorded data appeared to support was **remote vs local + * destination**, not "the inline write" as a primitive. + * + * --------------------------------------------------------------------------- + * THAT ARM WAS RUN ON 2026-08-20 AND IT REFUTED THE REMOTENESS READING + * --------------------------------------------------------------------------- + * `PN_TT_GROUP_POST_PROBE=self` at N=4 — every member posting into its OWN L1, the host + * left on the per-member wait — ran byte-exact (`cmp` IDENTICAL, 0 refusals, 0 restarts, + * 2.02 tok/s against a 2.02 disarmed control taken minutes earlier on the same four cores, + * which probed 4 PASS) and then left **cores 71 AND 75 unbootable**: `soft reset + * 0x00047000`, BRISC running with all four subordinates held, `instrn_buf_status = 0x66`, + * 0/4 probe runs. Cores 76 and 77, local posters in the same arm, were PASS. + * + * **Core 71 is the refutation.** Member 0's destination is its own L1 under `remote` too, + * so core 71's configuration in this arm is IDENTICAL to the one it survived twice — and + * this time it wedged. "Local posting is safe, 2 of 2" was a 2-sample null, and the third + * sample killed it. The wedge does NOT follow the destination. + * + * What survives: the post is an ACCELERANT of the resident queue's own "retired clean, + * will not boot next" defect (DEV_NOTES.md §1), it hits some cores and not others in the + * same arm, and neither remoteness nor member index predicts which. So **aggregating on a + * core local to the writer is not a way out**, and the card-aggregated completion has no + * cheap placement fix left. `PN_TT_FAST_COMPLETION`, which buys most of the same + * transaction count with no cross-core write at all, is what shipped instead. + * + * It also says what the post is NOT: it is not the reason a group wedges at all. The + * disarmed N=4 arm took all four cores after ~46x the work, so the base rate is not zero + * and the post is at most an accelerant of a defect the resident queue already has + * (DEV_NOTES.md §1, "RETIRED CLEAN, WILL NOT BOOT NEXT"). + * + * **AND THE WHOLE POINT IS AVOIDABLE.** What the post buys is transaction count, and + * `PN_TT_FAST_COMPLETION` below buys most of the same thing with NO cross-core write at + * all: 3 transactions per member per flush become 1, against the post's `1 + ceil(8N/16)` + * for the whole group. At N=8 that is 8 against 5 — the post is still better, and it is not + * 4.8x better, which is what it has to be to be worth a core. + * + * So it is not a knob, it is a hazard behind its own spelling — the same shape as + * `ft_tt_core probe --i-know-this-must-be-a-vector-add-triad`. `1` is REFUSED, because a + * `1` is what someone types when they have not read this. + */ +#define PN_TT_GROUP_COMPLETION_ENV "PN_TT_GROUP_COMPLETION" +#define PN_TT_GROUP_COMPLETION_ACK "i-know-this-wedges-the-posting-core" + +/** + * THE DESTINATION ARM — is the wedge the REMOTENESS or the CALL? + * `PN_TT_GROUP_POST_PROBE=self-i-know-this-may-wedge-a-core` or `remote-...`, default OFF. + * + * --------------------------------------------------------------------------- + * WHAT THE RECORDED DATA CAN AND CANNOT SAY + * --------------------------------------------------------------------------- + * With the post armed EVERY member's DM1 calls `ftq_group_post()`. Member 0's destination + * is its own L1; members 1..N-1 write across the NoC. The two armed wedges took core 75, a + * remote poster, and core 71 was PASS both times under identical code, the same shared + * inline staging slot, the same two sent-counter drains and the same barrier. + * + * **2 of 2, and confounded.** Core 71 is also member 0, also the device's own core, also + * the one the host polls first, and also the one whose queue the group block lives in. Any + * of those could carry the difference. So the recorded arms are consistent with + * "remoteness" and do not isolate it. + * + * --------------------------------------------------------------------------- + * WHAT THIS ISOLATES + * --------------------------------------------------------------------------- + * ONE variable: the coordinates in the inline write. + * + * `self` every member, member 0 included, posts into its OWN L1 at the same offset. + * `remote` every member posts to member 0 — the recorded configuration. + * + * Same kernel image, same processor, same descriptor stream, same member set, same slot + * arithmetic, same barrier. If `self` leaves every core bootable and `remote` wedges a + * remote poster, the destination IS the mechanism and a card-aggregated completion has a + * path: aggregate on the core that is local to the writer, or do not aggregate at all. + * If `self` wedges too, the destination is NOT the mechanism, the inline write is, and the + * post is dead as a design rather than as a placement. + * + * --------------------------------------------------------------------------- + * WHY THE HOST DOES NOT MOVE WITH IT + * --------------------------------------------------------------------------- + * Both arms leave the host on the per-member completion wait. Under `self` the slots are + * scattered across N cores and an aggregated read could not work at all; under `remote` it + * could, but then the two arms would differ in two places instead of one. Keeping the host + * fixed also makes both arms byte-exact runs, so `cmp` against the oracle is a real check + * on each — which the aggregated-read arms never had for the members' own answers. + * + * The cost of being wrong is a Tensix until the next rung-3 reset, which is why the value + * is a sentence and not a `1`, and why the base rate matters: the post-DISARMED N=4 arm + * took all four cores after ~46x the work, so the resident queue has a non-zero wedge rate + * of its own and ONE clean `self` arm is not proof of safety. + * + * --------------------------------------------------------------------------- + * THE ANSWER, 2026-08-20: `self` WEDGED. IT IS THE CALL, NOT THE DESTINATION. + * --------------------------------------------------------------------------- + * One `self` arm at N=4 was enough. The disarmed control on the same four cores minutes + * earlier probed 4 PASS; the `self` arm ran byte-exact and left cores 71 and 75 at + * `0x00047000` / `instrn_buf_status 0x66`, 0/4 probe runs, with 76 and 77 PASS. **Core 71 + * posts to its own L1 under BOTH arms, so its configuration was unchanged from the two it + * survived — and it wedged.** The remote arm was never run: it would have cost a third + * core to confirm a discriminator that had just been refuted. + * + * This knob stays, because the next question about the post is which of the inline write's + * pieces does it (the shared per-(processor, NoC) staging slot, the sent-counter drains, + * the barrier against the writer's own outstanding non-posted writes), and that question is + * asked by arms of exactly this shape. It is NOT a placement knob any more. + */ +#define PN_TT_GROUP_POST_PROBE_ENV "PN_TT_GROUP_POST_PROBE" +#define PN_TT_GROUP_POST_PROBE_SELF_ACK "self-i-know-this-may-wedge-a-core" +#define PN_TT_GROUP_POST_PROBE_REMOTE_ACK "remote-i-know-this-wedges-the-posting-core" + +/** + * RED PROOF ONLY. `PN_TT_RED_GROUP=early` accepts the sequence BEFORE the one just issued, + * so the host stops waiting one descriptor short. + * + * It exists because byte-exactness at N > 1 is only evidence if the wait is real. A gate + * that would be green with NO wait at all is measuring the card's timing, not the protocol + * — and this backend has caught four vacuous gates. One descriptor early is bounded (the + * ring still holds the chain, nothing is overrun) and it is exactly the question: does the + * dump of a projection the card has not finished differ from the one it has? + */ +#define PN_TT_RED_GROUP_ENV "PN_TT_RED_GROUP" + +/** + * THE PER-MEMBER WAIT IN ONE TRANSACTION INSTEAD OF THREE. `PN_TT_FAST_COMPLETION=1`, + * DEFAULT OFF. + * + * --------------------------------------------------------------------------- + * WHAT THE THREE ARE + * --------------------------------------------------------------------------- + * `pn_tt_group_await()` asks a member two questions in three device accesses: + * + * 1. `read_field(COMPLETED_COUNT)` one 4-byte UC read, the poll + * 2. `read_completion_wide()` words 4..11, 32 bytes, TWO BLOCK16 transactions + * + * and for members 1..N-1 the poll almost always succeeds on its FIRST iteration — they run + * concurrently with member 0 and member 0 is awaited first, so by the time their turn comes + * they are already finished. Their three transactions are therefore ~10 us of host time + * that overlaps NO card work, once per member per flush. That is the per-member term. + * + * --------------------------------------------------------------------------- + * WHY ONE IS THE FLOOR WITHOUT A CROSS-CORE MECHANISM + * --------------------------------------------------------------------------- + * N members' control blocks are in N different cores' L1, so N transactions is the minimum + * any host-side scheme can reach. `ft_tt_queue_read_completion_line()` reaches it: words + * 4..7 are ONE 16-byte L1 line and carry `completed_count`, `completed_token` and `state`, + * so the poll IS the check and there is no second read. The card-aggregated post is the + * only thing that goes below N, and it costs a core (above). + * + * --------------------------------------------------------------------------- + * WHAT IT GIVES UP, AND WHY THAT IS RECOVERABLE + * --------------------------------------------------------------------------- + * `error` is word 9, in the NEXT line. It is the fourth of rule 1's four agreements and + * this path does not read it on the success side. It is still read: + * + * - every 128th poll iteration, by `pn_tt_group_check_errors()`, which is unchanged; + * - on the timeout path; + * - and a REFUSAL cannot hide behind it, because a kernel that refuses a descriptor never + * publishes completion — `completed_count` stalls, the poll never succeeds, and the + * 128th iteration names the error. The refusal path is the one this assertion exists + * for and the refusal path still runs through it. + * + * The state in which `error` is set AND completion is correct is a fault raised AFTER + * `completed_count` was published, and the protocol has exactly one: + * `ftq_group_post()`'s own refusal. So this path is REFUSED when the group post is armed — + * the two are mutually exclusive by construction, not by discipline. + * + * --------------------------------------------------------------------------- + * MEASURED ON SILICON 2026-08-20, AND IT IS NOW ON BY DEFAULT + * --------------------------------------------------------------------------- + * Sixteen arms — this flag and this flag plus the parameter cache, at N = 1, 2, 4, 8, 12, + * 16, 24, 32 — every one `cmp` IDENTICAL to the 48-token oracle, 0 refusals, 0 residency + * restarts, `company=0` on each. It moves **matmul card work, not `b`**: `b` is the ISSUE + * coefficient and this is a WAIT mechanism, so the refit reads 0.00242 control against + * 0.00245 fast. What it moves is `card(N)` — 0.0611 -> 0.0475 at 8 members, 0.0559 -> + * 0.0348 at 12, 0.0595 -> 0.0307 at 16 — and the wall, 2.13 -> 2.19 tok/s at the shipped + * member count. + * + * **The saving is 6.6-7.2 us per awaited member per flush, flat across 2..32 members**, + * which is two transactions at ~3.5 us each and is the direct measurement of a link read. + * + * THE WC/BLOCK16 TIGHT LOOP IS SAFE, and this is what settles it. The fast poll reads + * through the `bulk_l1` accessor (WC, BLOCK16) where the old one used UC/WORD32, and a + * stale WC read costs an extra iteration rather than a wrong answer — but that had never + * been done in a TIGHT loop here. The N=1 arm spins **1,048 poll iterations per flush, + * 6.49 million reads in one run**, and is byte-identical. A stale line would have shown up + * as a hang or a wrong answer at that rate and did not. + */ +#define PN_TT_FAST_COMPLETION_ENV "PN_TT_FAST_COMPLETION" +#define PN_TT_FAST_COMPLETION_DEFAULT 1u + +/** + * THE 64-BYTE PARAMETER BLOCK, WRITTEN ONCE INSTEAD OF EVERY PROJECTION. + * `PN_TT_PARAM_CACHE`, **DEFAULT ON since 2026-08-20** — the header has said so since it + * flipped and this line still said OFF, which is the kind of stale comment that gets a + * shipped default measured as an arm. + * + * --------------------------------------------------------------------------- + * WHAT THE ISSUE SIDE COSTS AND WHERE IT GOES + * --------------------------------------------------------------------------- + * Issuing one projection to one member is three host writes: 64 bytes of parameters, a + * 32-byte descriptor, a 4-byte head bump. The write direction is BANDWIDTH bound — ~53 MB/s + * through the UC path — so it is the 100 bytes that convert, and the parameters are 64 of + * them. + * + * `pn_tt_group_parameters()` derives those sixteen words from `(weight, input strip, output + * strip, member)` plus the residency's own depth, column block and cooperative role. **Every + * one of those is a constant of a decode run.** Weights are placed once at load and never + * move; the activation strips are resident at fixed addresses and each weight is used with + * exactly one (input, output) pair in the forward; depth and column block are per-residency + * by construction. So the host writes the same 64 bytes to the same member ~24 times a + * token, once per layer. + * + * --------------------------------------------------------------------------- + * WHY IT IS CONTENT-ADDRESSED AND NOT KEYED ON THE WEIGHT + * --------------------------------------------------------------------------- + * "Each weight is used with one strip pair" is TRUE of this forward and is not enforced + * anywhere, and a red arm perturbs individual words on purpose (`PN_TT_RED_SPLIT`, + * `matmul_a_spread`, `matmul_a_offset`, the malformed-descriptor proof). A cache keyed on + * the weight would serve a stale block under any of those, silently, and the failure would + * look like a wrong answer with every status word clean — the exact class this backend uses + * byte comparison to catch. + * + * Keyed on the sixteen WORDS, a hit means the block at that address holds exactly those + * words. The hash is a bucket index; the memcmp is the claim. A collision costs a miss. + * + * --------------------------------------------------------------------------- + * WHY A CACHED BLOCK IS SAFE AGAINST AN IN-FLIGHT DESCRIPTOR + * --------------------------------------------------------------------------- + * The rotating slots are indexed `descriptor index mod 8` precisely so a chained projection + * cannot overwrite parameters the card has not read yet. A cached block is **never + * rewritten and never evicted**, so that hazard does not exist for it: the block a + * descriptor points at holds the same words for the life of the residency. The cache is + * cleared at ARM, where no descriptor is in flight by construction. + * + * When the region fills (320 blocks; Ornith's whole matmul weight set is 249) a projection + * falls back to the rotating slots and is exactly as expensive as it was. `cache_full` + * counts that, so "it stopped paying" is visible rather than inferred. + * + * --------------------------------------------------------------------------- + * MEASURED ON SILICON 2026-08-20 — AND THE PREDICTION WAS WRONG ABOUT WHY + * --------------------------------------------------------------------------- + * **`b` 0.00242 -> 0.00199, which is 1.21x and not the predicted 2.4x**, over eight member + * counts each `cmp` IDENTICAL. 249 of 320 blocks used, 0 fell back at any member count, + * 304,560 hits against 6,480 misses at 32 members — one miss per projection per member, + * i.e. the first token and nothing after it. The mechanism works exactly as designed. + * + * **The prediction was wrong because the WRITE direction is TRANSACTION bound, not + * bandwidth bound.** The ledger measured the two directly: 0.928 s of issue at 8 members + * against a 53 MB/s bandwidth model's 0.150 s — a 6x miss — and 3.94 us per write against + * a link that would have to be reading 25 B/us for the bandwidth model to hold. So a + * projection's three writes cost by their COUNT, and the one this cache removes is the + * CHEAP one: the 64-byte parameter block measures **2.08 us** against **4.84 us** for the + * 32-byte descriptor and the 4-byte head bump. Taking 100 B to 36 B took the bytes by 2.8x + * and the time by 1.21x, and the difference between those two numbers is the whole finding. + * + * ON BY DEFAULT because 1.21x of `b` is real, free and byte-exact — but the honest scoping + * of the ISSUE side from here is a TRANSACTION-count one: the descriptor and its head bump + * are 4.84 us each and there are two of them, which is where the remaining 0.00199 lives. + */ +#define PN_TT_PARAM_CACHE_ENV "PN_TT_PARAM_CACHE" +#define PN_TT_PARAM_CACHE_DEFAULT 1u + +/** Open-addressed buckets. A power of two, and comfortably over the slot count so the + * table stays sparse — a full table would degrade the probe, not the answer. */ +#define PN_TT_PARAM_CACHE_BUCKETS 1024u +#define PN_TT_PARAM_CACHE_EMPTY 0xFFFFu + +/** + * FNV-1a over the sixteen words. It is a BUCKET INDEX and nothing rests on it: the memcmp + * behind it is what decides a hit, so a bad hash costs throughput and never correctness. + * + * RED PROOF `PN_TT_RED=param_cache_hash` collapses it to a constant, so every block lands + * in one bucket and the probe walks them all. **That arm must be `cmp` IDENTICAL**, and + * that is the whole claim of the previous paragraph stated in a form that can fail: if a + * degenerate hash changes the ANSWER, then the hash was load-bearing and the design is + * wrong. Its partner `param_cache_blind` removes the memcmp instead and must DIFFER. + */ +static uint32_t pn_tt_param_hash(const uint32_t *words, bool red_constant) +{ + const unsigned char *bytes = (const unsigned char *)words; + uint32_t hash = 2166136261u; + size_t index; + + if (red_constant) + return 0u; + for (index = 0; index < FT_TT_QUEUE_PARAM_WORDS * sizeof(uint32_t); index++) { + hash ^= bytes[index]; + hash *= 16777619u; + } + return hash; +} + +/** Release a member's cache. Idempotent — retire runs on partly-armed groups. */ +static void pn_tt_param_cache_free(pn_tt_matmul_member *slot) +{ + free(slot->cache_words); + free(slot->cache_slot); + slot->cache_words = NULL; + slot->cache_slot = NULL; + slot->cache_used = 0u; +} + +/** Allocate a member's cache. A failure is NOT fatal: the caller runs uncached. */ +static void pn_tt_param_cache_alloc(pn_tt_matmul_member *slot) +{ + uint32_t bucket; + + pn_tt_param_cache_free(slot); + slot->cache_words = calloc(FT_TT_QUEUE_PARAM_CACHE_SLOTS, + sizeof(uint32_t) * FT_TT_QUEUE_PARAM_WORDS); + slot->cache_slot = malloc(sizeof(uint16_t) * PN_TT_PARAM_CACHE_BUCKETS); + if (!slot->cache_words || !slot->cache_slot) { + pn_tt_param_cache_free(slot); + return; + } + for (bucket = 0; bucket < PN_TT_PARAM_CACHE_BUCKETS; bucket++) + slot->cache_slot[bucket] = PN_TT_PARAM_CACHE_EMPTY; +} + +/** + * The address this member's card-side block for `parameters` is at, writing it first if it + * is not there yet. + * + * @param out_wrote set true when a 64-byte write was made, false on a hit + * @return the address, or 0 when the caller must fall back to a rotating slot + */ +static void pn_tt_param_census(pn_tt_device *device, const uint32_t *parameters, bool full) +{ + pn_tt_matmul_group *group = &device->group; + const uint32_t kt = parameters[FT_TT_QUEUE_MATMUL_TILE_INNER]; + const uint32_t nt = parameters[FT_TT_QUEUE_MATMUL_TILE_COLS]; + uint32_t row; + + for (row = 0; row < group->param_census_rows; row++) + if (group->param_census[row].kt == kt && group->param_census[row].nt == nt) + break; + if (row == group->param_census_rows) { + if (row >= 48u) + return; + memset(&group->param_census[row], 0, sizeof(group->param_census[row])); + group->param_census[row].kt = kt; + group->param_census[row].nt = nt; + group->param_census_rows++; + } + if (full) + group->param_census[row].fallbacks++; + else + group->param_census[row].misses++; +} + +static uint32_t pn_tt_param_cache_address(pn_tt_device *device, pn_tt_matmul_member *slot, + const uint32_t *parameters, bool *out_wrote) +{ + uint32_t bucket; + uint32_t probe; + uint32_t address = 0u; + + *out_wrote = false; + if (!slot->cache_words || !slot->cache_slot) + return 0u; + + bucket = pn_tt_param_hash(parameters, device->red_param_cache_hash) & + (PN_TT_PARAM_CACHE_BUCKETS - 1u); + for (probe = 0; probe < PN_TT_PARAM_CACHE_BUCKETS; probe++) { + const uint32_t index = (bucket + probe) & (PN_TT_PARAM_CACHE_BUCKETS - 1u); + const uint16_t entry = slot->cache_slot[index]; + + if (entry == PN_TT_PARAM_CACHE_EMPTY) { + /* A MISS. Take the next block if there is one; otherwise the caller uses a + * rotating slot and pays what it paid before. */ + if (slot->cache_used >= FT_TT_QUEUE_PARAM_CACHE_SLOTS) { + slot->cache_full++; + pn_tt_param_census(device, parameters, true); + if (pn_tt_env_u32("PN_TT_PARAM_CENSUS_DIFF", 0u) != 0u) { + uint32_t best = 17u, e, w; + + for (e = 0; e < slot->cache_used && best == 17u; e++) { + uint32_t differ = 0u, at = 0u; + + for (w = 0; w < FT_TT_QUEUE_PARAM_WORDS; w++) + if (slot->cache_words[e][w] != parameters[w]) { + differ++; + at = w; + } + if (differ == 1u) + best = at; + } + device->group.param_diff_word[best]++; + } + return 0u; + } + pn_tt_param_census(device, parameters, false); + if (ft_tt_queue_write_param_cache(pn_tt_issue_access(&device->runtime, + device->group.issue_shape), + slot->queue, slot->cache_used, parameters, + &address) != FT_TT_OK) + return 0u; + memcpy(slot->cache_words[slot->cache_used], parameters, + sizeof(uint32_t) * FT_TT_QUEUE_PARAM_WORDS); + slot->cache_slot[index] = (uint16_t)slot->cache_used; + slot->cache_used++; + slot->cache_misses++; + *out_wrote = true; + return address; + } + /* THE MEMCMP IS THE CLAIM, not the hash. RED PROOF + * `PN_TT_RED=param_cache_blind` removes it — paired with the constant hash every + * projection then reuses block 0, so the arm MUST produce a `cmp` that DIFFERS. A + * blind cache that still matched the oracle would mean the sixteen words never + * varied, i.e. that this whole mechanism was measuring nothing. */ + if (device->red_param_cache_blind || + memcmp(slot->cache_words[entry], parameters, + sizeof(uint32_t) * FT_TT_QUEUE_PARAM_WORDS) == 0) { + slot->cache_hits++; + return FT_TT_QUEUE_PARAM_CACHE_ADDRESS(slot->queue->control_address, entry); + } + } + return 0u; +} + +void pn_tt_matmul_group_param_cache_reset(pn_tt_device *device) +{ + pn_tt_matmul_group *group; + uint32_t member, bucket; + + if (!device || pn_tt_env_u32("PN_TT_PARAM_CACHE_RESET", 1u) == 0u) + return; + group = &device->group; + for (member = 0; member < group->member_count; member++) { + pn_tt_matmul_member *slot = &group->member[member]; + + if (!slot->cache_slot) + continue; + for (bucket = 0; bucket < PN_TT_PARAM_CACHE_BUCKETS; bucket++) + slot->cache_slot[bucket] = PN_TT_PARAM_CACHE_EMPTY; + slot->cache_used = 0u; + } +} + +/** Ring slots per member. */ +#define PN_TT_QUEUE_CAPACITY 8u + +/** Parameter-block slots a descriptor may point at, and how many projections may be in + * flight before a flush is forced. + * + * ONE SLOT WAS ENOUGH ONLY WHILE THE HOST WAITED FOR EVERY MEMBER BEFORE ISSUING THE NEXT + * PROJECTION. Chaining two descriptors through one parameter block would have the host + * overwrite the first's parameters while the card was still reading them — a wrong matmul + * with every status word clean, which is the failure class this backend uses byte + * comparison against. The slot is the descriptor's own index modulo the count, so a block + * is reused only after the ring itself has wrapped. + */ +#define PN_TT_QUEUE_PARAM_SLOTS 8u + +/** Bounded residency (ft_tt_queue.h rule 3); neither may be 0. */ +#define PN_TT_IDLE_SPIN_LIMIT FT_TT_QUEUE_DEFAULT_IDLE_SPINS +#define PN_TT_CB_WAIT_SPIN_LIMIT 100000000u + +/** Every processor of the triad. */ +#define PN_TT_TRIAD_MASK \ + ((1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1) | (1u << FT_TT_PROC_MATH0) | \ + (1u << FT_TT_PROC_MATH1) | (1u << FT_TT_PROC_MATH2)) + +pn_tt_red_split pn_tt_matmul_group_red_mode(void) +{ + const char *value = getenv(PN_TT_RED_SPLIT_ENV); + + if (!value || !*value) + return PN_TT_RED_SPLIT_NONE; + if (strcmp(value, "base") == 0) + return PN_TT_RED_SPLIT_BASE; + if (strcmp(value, "out") == 0) + return PN_TT_RED_SPLIT_OUT; + if (strcmp(value, "narrow") == 0) + return PN_TT_RED_SPLIT_NARROW; + fprintf(stderr, "pn_tt: %s=%s is not base|out|narrow — ignored\n", PN_TT_RED_SPLIT_ENV, + value); + return PN_TT_RED_SPLIT_NONE; +} + +/* ================================================================ + * Bring-up + * ================================================================ */ + +/** + * Which DOOR of a bank each member reads an activation strip through. + * + * Every member reads the SAME bytes at the SAME address, so this changes no data and no + * kernel — it is the ~45 GB/s endpoint ceiling against the bank's ~64 and nothing else + * (ft_tt_dram_stripe.h). `ft_tt_dram_stripe_choose_ports()` rather than `member % 3` + * because the chooser is the one that knows the NoC-row cost order. + */ +static int pn_tt_group_activation_doors(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t banks[PN_TT_MATMUL_MAX_CORES]; + uint32_t bank, member; + + group->activation_bank_count = device->dram_bank_count; + for (bank = 0; bank < device->dram_bank_count; bank++) { + for (member = 0; member < group->member_count; member++) + banks[member] = bank; + if (ft_tt_dram_stripe_choose_ports(&device->runtime.grid, banks, group->member_count, + FT_TT_COORD_TRANSLATED, + group->activation_door[bank]) != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not choose activation doors for bank %u\n", bank); + return -1; + } + if (bank == 0) + group->activation_row_collisions = ft_tt_dram_stripe_shared_rows( + group->activation_door[bank], group->member_count, NULL, NULL); + } + return 0; +} + +/** + * The widest rectangle a group of `member_count` can be laid out in without any ROW + * exceeding the four-destination limit: the largest divisor of the count that is at most + * four. + * + * A row is one multicast packet, so the width IS the destination count of every packet + * except the leader's own row. Choosing a divisor rather than `min(count, 4)` is what + * keeps the layout a rectangle at counts like 6, where 1x6 would be one 5-destination + * packet and 2x3 is two 3-destination ones. + */ +static uint32_t pn_tt_group_rectangle_columns(uint32_t member_count) +{ + uint32_t columns; + + for (columns = FT_TT_GROUP_BROADCAST_HAZARD_LIMIT; columns > 1u; columns--) + if (member_count % columns == 0u) + return columns; + return 1u; +} + +/** + * Place the members of a COOPERATING group as a rectangle from member 0's core, and build + * the broadcast partition over it. + * + * Every refusal below leaves `cooperative_placed` false and the caller falls back to the + * ordinary arithmetic placement, because a group that cannot cooperate is still a perfectly + * good SPLIT group — the multicast is a speed lever, not a correctness one. What must never + * happen is cooperating over a set that is not a rectangle: the bounding box then contains + * cores this group does not own, and a multicast writes every node of its box. + */ +static int pn_tt_group_place_rectangle(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + ft_tt_noc_coord members[FT_TT_RESIDENT_MULTICAST_MAX_MEMBERS]; + uint32_t columns = pn_tt_env_u32(PN_TT_MATMUL_COLUMNS_ENV, 0u); + uint32_t column_count; + uint32_t rows; + uint32_t member; + int rc; + + if (columns == 0u) + columns = pn_tt_group_rectangle_columns(group->member_count); + if (columns > FT_TT_GROUP_BROADCAST_HAZARD_LIMIT || group->member_count % columns != 0u) { + fprintf(stderr, "pn_tt: %s=%u does not divide %u members into rows of at most %u\n", + PN_TT_MATMUL_COLUMNS_ENV, columns, group->member_count, + FT_TT_GROUP_BROADCAST_HAZARD_LIMIT); + return -1; + } + rows = group->member_count / columns; + column_count = ft_tt_coord_tensix_column_count(&device->runtime.grid); + if (column_count == 0u) { + fprintf(stderr, "pn_tt: the live grid reports no Tensix columns\n"); + return -1; + } + /* The live grid decides whether this block is a rectangle in NoC space, which it is + * NOT wherever it would straddle the columns the ARC spine and the east DRAM column + * occupy. That refusal is ft_tt_group_rectangle_coordinates()'s and it is the reason + * the coordinates are asked for rather than computed here. */ + rc = ft_tt_group_rectangle_coordinates(&device->runtime.grid, device->core_index, rows, + columns, members, + FT_TT_RESIDENT_MULTICAST_MAX_MEMBERS); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: a %ux%u block at core %u is not a multicast rectangle " + "(rc=%d) — cooperation is off, the split still runs\n", + rows, columns, device->core_index, rc); + return -1; + } + rc = ft_tt_op_matmul_resident_multicast_plan(members, group->member_count, 0u, + PN_TT_COOP_SPIN_LIMIT, &group->multicast); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: the broadcast partition over that block was refused " + "(rc=%d) — cooperation is off, the split still runs\n", rc); + return -1; + } + + for (member = 0u; member < group->member_count; member++) { + pn_tt_matmul_member *slot = &group->member[member]; + + slot->core_index = device->core_index + (member / columns) * column_count + + (member % columns); + slot->core_x = (uint16_t)members[member].x; + slot->core_y = (uint16_t)members[member].y; + } + group->cooperative_columns = columns; + group->cooperative_placed = true; + fprintf(stderr, "pn_tt: cooperative placement %ux%u from core %u — %u broadcast " + "packet(s) reach %u receivers\n", + rows, columns, device->core_index, group->multicast.partition.rectangle_count, + group->multicast.receiver_count); + return 0; +} + +/** The `-D` a SyncFull matmul compute image records in its own provenance. Spelled once. */ +#define PN_TT_MATMUL_SYNC_FULL_DEFINE "-DFT_TT_MATMUL_DST_SYNC=1" + +/** + * Was the matmul compute image THIS PROCESS WILL ARM built at `DstSync::SyncFull`? + * + * --------------------------------------------------------------------------- + * THE INTERLOCK INVERTED, AND WHY IT HAD TO BE + * --------------------------------------------------------------------------- + * This used to key on `PN_TT_PROGRAM_DIR` being set. That was a sound proxy for exactly one + * reason — "the embedded blob's matmul is SyncHalf, so an eight-tile kernel can only be on + * the card if you named a directory" — and it is a reason about the BLOB, not about DST. The + * moment the blob is regenerated at SyncFull the argument REVERSES: the embedded default IS + * the eight-tile kernel and a SyncHalf OVERRIDE DIRECTORY becomes the unsafe case, which the + * old test cannot see at all, because it reads "a directory was named" as "safe to go wide". + * Flipping its polarity would only move the hole; no version of "which directory did you + * name" survives a regeneration. + * + * SO IT ASKS THE IMAGE. `ft_tt_program_recorded_defines()` reads the `defines=` field of the + * armed program's own provenance record — embedded or override, whichever this process will + * actually load — and that answer travels with the IMAGE instead of with the operator. A + * record that predates the field, or a program with no record at all, is NOT SyncFull here: + * it is a record that cannot answer, and reading "cannot answer" as permission is precisely + * the silent failure this guard exists for. + * + * IT READS THE COMPUTE IMAGE because that is where the mode lives — + * `kernels/resident_matmul_dst_sync.h` is included by `resident_matmul_compute.c` and by + * nothing else in the tree. The one-shot `matmul_*` programs build `matmul_compute.c`, which + * does not include it, so they can never be SyncFull; and they never consult + * `dst_section_tiles`, which is what makes the first fact harmless rather than a second thing + * to guard. + */ +static bool pn_tt_matmul_group_image_is_sync_full(const char **out_where) +{ + const char *root = pn_tt_program_override_root(); + char defines[512]; + int rc; + + *out_where = pn_tt_program_where(root); + rc = ft_tt_program_recorded_defines(PN_TT_RESIDENT_PROGRAM, root, FT_TT_PROC_MATH0, + defines, sizeof(defines)); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s carries no readable build record in the %s programs " + "(rc=%d) — reading that as DstSync::SyncHalf, because a record that " + "cannot answer must never be read as permission\n", + PN_TT_RESIDENT_PROGRAM, *out_where, rc); + return false; + } + return ft_tt_program_defines_contain(defines, PN_TT_MATMUL_SYNC_FULL_DEFINE) != 0; +} + +/** + * The DST section the armed matmul kernel was built for, in 32x32 tiles. + * + * THE IMAGE DECIDES THE DEFAULT AND THE KNOB CAN ONLY NARROW IT. Unset, this is whatever the + * armed compute image RECORDS — eight at SyncFull, four otherwise — so shipping the wider + * section is a property of the blob that is on the card rather than of a variable somebody + * remembered to export. `=4` is always honoured, because four tiles fit any section, and that + * is what makes a SyncHalf-shaped CONTROL ARM available on a SyncFull binary. `=8` against an + * image that does not record SyncFull is REFUSED BY NAME and clamped to four. + */ +static uint32_t pn_tt_matmul_group_dst_section_tiles(void) +{ + const char *where = "none"; + const bool full = pn_tt_matmul_group_image_is_sync_full(&where); + const uint32_t available = + full ? FT_TT_MATMUL_DST_SECTION_TILES_FULL : FT_TT_MATMUL_DST_SECTION_TILES_HALF; + const uint32_t asked = pn_tt_env_u32(PN_TT_MATMUL_DST_TILES_ENV, available); + + if (asked == FT_TT_MATMUL_DST_SECTION_TILES_HALF) + return FT_TT_MATMUL_DST_SECTION_TILES_HALF; + if (asked != FT_TT_MATMUL_DST_SECTION_TILES_FULL) { + fprintf(stderr, "pn_tt: %s=%u is not %u (DstSync::SyncHalf) or %u (SyncFull) — using " + "%u\n", PN_TT_MATMUL_DST_TILES_ENV, asked, + FT_TT_MATMUL_DST_SECTION_TILES_HALF, FT_TT_MATMUL_DST_SECTION_TILES_FULL, + FT_TT_MATMUL_DST_SECTION_TILES_HALF); + return FT_TT_MATMUL_DST_SECTION_TILES_HALF; + } + if (!full) { + fprintf(stderr, "pn_tt: %s=%u REFUSED — the %s %s compute image does not record %s, " + "so eight tiles would write past the section with nothing refusing " + "anywhere. Using %u\n", + PN_TT_MATMUL_DST_TILES_ENV, asked, where, PN_TT_RESIDENT_PROGRAM, + PN_TT_MATMUL_SYNC_FULL_DEFINE, FT_TT_MATMUL_DST_SECTION_TILES_HALF); + return FT_TT_MATMUL_DST_SECTION_TILES_HALF; + } + fprintf(stderr, "pn_tt: matmul DST section %u tiles (DstSync::SyncFull) — the %s %s " + "compute image records %s, so MB*NB may reach %u and the MATH/PACK double " + "buffer is GONE\n", + FT_TT_MATMUL_DST_SECTION_TILES_FULL, where, PN_TT_RESIDENT_PROGRAM, + PN_TT_MATMUL_SYNC_FULL_DEFINE, FT_TT_MATMUL_DST_SECTION_TILES_FULL); + return FT_TT_MATMUL_DST_SECTION_TILES_FULL; +} + +int pn_tt_matmul_group_configure(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t first, member; + + memset(group, 0, sizeof(*group)); + group->member_count = pn_tt_env_u32(PN_TT_MATMUL_CORES_ENV, PN_TT_MATMUL_CORES_DEFAULT); + device->poll_backoff_us = pn_tt_env_u32(PN_TT_POLL_BACKOFF_ENV, 0u); + group->activation_doors = pn_tt_env_u32(PN_TT_ACTIVATION_DOORS_ENV, 1u) != 0u; + group->dst_section_tiles = pn_tt_matmul_group_dst_section_tiles(); + /* 0 is not a legal block, so it is the sentinel for "the default, which is the section + * the armed image records". Spelling it as a sentinel rather than as a constant is what + * keeps ONE statement of the default — the alternative is a second copy of the SyncHalf + * four that a SyncFull blob would silently leave behind. */ + group->column_block = pn_tt_env_u32(PN_TT_MATMUL_BLOCK_ENV, PN_TT_MATMUL_BLOCK_DEFAULT); + if (group->column_block == 0u) + group->column_block = group->dst_section_tiles; + if (group->column_block == 0u || group->column_block > group->dst_section_tiles || + (group->column_block & (group->column_block - 1u)) != 0u) { + fprintf(stderr, "pn_tt: %s=%u is not a power of two in 1..%u (the DST section at " + "dst_accum_mode 1) — using 1\n", PN_TT_MATMUL_BLOCK_ENV, + group->column_block, group->dst_section_tiles); + group->column_block = 1u; + } + if (group->column_block > 1u) + fprintf(stderr, "pn_tt: matmul column block %u — the reader supplies %u output " + "columns a pass, so A is fetched Nt/%u times instead of Nt\n", + group->column_block, group->column_block, group->column_block); + group->row_block = pn_tt_env_u32(PN_TT_MATMUL_ROWS_ENV, PN_TT_MATMUL_ROWS_DEFAULT); + if (group->row_block == 0u || (group->row_block & (group->row_block - 1u)) != 0u || + !ftmm_block_fits_section(group->row_block, group->column_block, + group->dst_section_tiles)) { + /* THE PRODUCT IS THE CEILING, not either factor: MB*NB output tiles are live in DST + * across the whole K loop and DST has no bounds check. Refused here rather than + * left to the kernel, because the host refusal is a message and the kernel's is a + * stranded triad — but the kernel checks it too, for the descriptor this host did + * not write. */ + fprintf(stderr, "pn_tt: %s=%u is not a power of two with %s=%u inside the %u-tile " + "DST section (dst_accum_mode 1) — using 1\n", PN_TT_MATMUL_ROWS_ENV, + group->row_block, PN_TT_MATMUL_BLOCK_ENV, group->column_block, + group->dst_section_tiles); + group->row_block = 1u; + } + if (group->row_block > 1u) + fprintf(stderr, "pn_tt: matmul ROW block %u — the reader holds %u A tile-rows and " + "sweeps B past all of them, so B is fetched Mt/%u times instead of " + "Mt. INERT at Mt=1, which is every descriptor today: it must be cmp " + "IDENTICAL.\n", + group->row_block, group->row_block, group->row_block); + group->red_a_spread = pn_tt_red("matmul_a_spread"); + group->red_a_offset = pn_tt_red("matmul_a_offset"); + if (group->red_a_spread) + fprintf(stderr, "pn_tt: RED PROOF matmul_a_spread — every member's A block is read " + "from a DIFFERENT BANK at the same offset. THE ANSWER IS GARBAGE; " + "this arm times the A supply and nothing else.\n"); + if (group->red_a_offset) + fprintf(stderr, "pn_tt: RED PROOF matmul_a_offset — every member's A block is read " + "from the SAME BANK 4 MiB apart. THE ANSWER IS GARBAGE; it separates " + "a channel ceiling from N readers on one 16 KiB window.\n"); + group->cooperative_requested = pn_tt_env_u32(PN_TT_MATMUL_COOPERATIVE_ENV, 0u) != 0u; + group->rectangle_requested = group->cooperative_requested || + pn_tt_env_u32(PN_TT_MATMUL_RECTANGLE_ENV, 0u) != 0u; + { + const char *red = getenv(PN_TT_RED_COOP_ENV); + + group->red_coop_stale = red && strcmp(red, "stale") == 0; + if (red && !group->red_coop_stale) + fprintf(stderr, "pn_tt: %s=%s is not `stale` — ignored\n", PN_TT_RED_COOP_ENV, red); + if (group->red_coop_stale) + fprintf(stderr, "pn_tt: RED PROOF — the leader SKIPS the payload multicast; " + "every receiver computes from the previous block\n"); + } + { + const char *ask = getenv(PN_TT_GROUP_COMPLETION_ENV); + + group->group_completion = ask && strcmp(ask, PN_TT_GROUP_COMPLETION_ACK) == 0; + if (ask && !group->group_completion && strcmp(ask, "0") != 0) + fprintf(stderr, "pn_tt: %s=%s REFUSED — the card-side post leaves the POSTING " + "core unbootable at its next boot (measured twice, 2026-08-16); " + "the only accepted value is `%s`\n", + PN_TT_GROUP_COMPLETION_ENV, ask, PN_TT_GROUP_COMPLETION_ACK); + } + { + const char *ask = getenv(PN_TT_GROUP_POST_PROBE_ENV); + + group->post_probe = PN_TT_GROUP_POST_PROBE_OFF; + if (ask && strcmp(ask, PN_TT_GROUP_POST_PROBE_SELF_ACK) == 0) + group->post_probe = PN_TT_GROUP_POST_PROBE_SELF; + else if (ask && strcmp(ask, PN_TT_GROUP_POST_PROBE_REMOTE_ACK) == 0) + group->post_probe = PN_TT_GROUP_POST_PROBE_REMOTE; + else if (ask && strcmp(ask, "0") != 0) + fprintf(stderr, "pn_tt: %s=%s REFUSED — the card-side post can leave the POSTING " + "core unbootable at its next boot; the only accepted values are " + "`%s` and `%s`\n", + PN_TT_GROUP_POST_PROBE_ENV, ask, PN_TT_GROUP_POST_PROBE_SELF_ACK, + PN_TT_GROUP_POST_PROBE_REMOTE_ACK); + } + /* Two mechanisms cannot both own where DM1 posts. The aggregated read needs every slot + * in one block; the probe's whole point is that they are not. Dropping BOTH, for the + * same reason the fast/aggregated pair drops both: a contradictory pair must not + * resolve to the arm that costs a core to be wrong about. */ + if (group->post_probe != PN_TT_GROUP_POST_PROBE_OFF && group->group_completion) { + fprintf(stderr, "pn_tt: %s and %s are mutually exclusive — both decide where DM1 " + "posts, and the aggregated READ requires every slot in one block. " + "BOTH are OFF; ask for one.\n", + PN_TT_GROUP_POST_PROBE_ENV, PN_TT_GROUP_COMPLETION_ENV); + group->post_probe = PN_TT_GROUP_POST_PROBE_OFF; + group->group_completion = false; + } + if (group->post_probe != PN_TT_GROUP_POST_PROBE_OFF && group->member_count == 1u) { + fprintf(stderr, "pn_tt: %s is meaningless at one member — there is no remote arm to " + "compare against. OFF.\n", PN_TT_GROUP_POST_PROBE_ENV); + group->post_probe = PN_TT_GROUP_POST_PROBE_OFF; + } + group->fast_completion = + pn_tt_env_u32(PN_TT_FAST_COMPLETION_ENV, PN_TT_FAST_COMPLETION_DEFAULT) != 0u; + /* MUTUALLY EXCLUSIVE BY CONSTRUCTION, AND BOTH ARE DROPPED. The line read cannot see + * `error`, and the group post is the ONE fault the protocol raises after completion is + * already published — so arming both would silently drop the only assertion that could + * catch it. + * + * BOTH, not "the fast one loses": the post costs a core, so a contradictory pair must + * not resolve to the arm that is expensive to be wrong about. Silently keeping the post + * armed here would make this very refusal a way to wedge a Tensix. */ + if (group->fast_completion && group->group_completion) { + fprintf(stderr, "pn_tt: %s and %s are mutually exclusive — the one-line completion " + "read does not carry `error`, and the group post is the only fault " + "raised AFTER completion is published. BOTH are OFF; ask for one.\n", + PN_TT_FAST_COMPLETION_ENV, PN_TT_GROUP_COMPLETION_ENV); + group->fast_completion = false; + group->group_completion = false; + } + group->param_cache = + pn_tt_env_u32(PN_TT_PARAM_CACHE_ENV, PN_TT_PARAM_CACHE_DEFAULT) != 0u; + /* THE ISSUE SHAPE, before any member arms. Each member then DEMOTES it if its own queue + * is not 16-byte addressable, so the group reports one shape and it is the one every + * member could actually use. */ + group->issue_shape = pn_tt_issue_shape_selected(); + device->red_param_cache_blind = pn_tt_red("param_cache_blind"); + /* `blind` implies the constant hash: on its own it would almost never take a wrong + * entry, and an arm that fires "almost never" is not a red proof. */ + device->red_param_cache_hash = device->red_param_cache_blind || + pn_tt_red("param_cache_hash"); + if (device->red_param_cache_hash && !device->red_param_cache_blind) + fprintf(stderr, "pn_tt: RED PROOF param_cache_hash — the cache hash is a CONSTANT, " + "so every block shares one bucket and the memcmp does all the work. " + "This arm must be `cmp` IDENTICAL.\n"); + if (device->red_param_cache_blind) + fprintf(stderr, "pn_tt: RED PROOF param_cache_blind — constant hash AND no memcmp, " + "so every projection reuses block 0. THE ANSWER IS GARBAGE; this arm " + "must be `cmp` DIFFERS.\n"); + device->red_fast_check = pn_tt_red("fast_check"); + if (device->red_fast_check) + fprintf(stderr, "pn_tt: RED PROOF fast_check — the one-line completion check is asked " + "about one descriptor MORE than the poll waited for. It must REFUSE " + "on the first flush; a run that completes proves the check is inert.\n"); + /* The two "armed" lines are AFTER the guard on purpose: a log that says a mechanism is + * armed and then says it is off is a log nobody can grep. */ + if (group->group_completion) + fprintf(stderr, "pn_tt: card-aggregated completion armed — THIS COSTS A CORE. " + "Recover with `ft_tt_core recover --rung 3 --write " + "--then-probe` and retrain the link afterwards.\n"); + if (group->post_probe != PN_TT_GROUP_POST_PROBE_OFF) + fprintf(stderr, "pn_tt: group post probe armed, destination %s — THIS MAY COST A " + "CORE. Every member's DM1 calls ftq_group_post(); the host stays on " + "the per-member wait, so the run is byte-exact either way. Recover a " + "wedged core with `ft_tt_core recover --rung 3 --driver udrv " + "--write --then-probe` and retrain the link afterwards.\n", + group->post_probe == PN_TT_GROUP_POST_PROBE_SELF ? "SELF (own L1)" + : "REMOTE (member 0)"); + if (group->fast_completion) + fprintf(stderr, "pn_tt: fast completion armed — ONE 16-byte transaction per member " + "per flush instead of three. `error` is checked every 128th poll " + "and on the timeout path, not on the success path.\n"); + if (group->param_cache) + fprintf(stderr, "pn_tt: parameter cache armed — a projection's sixteen words are " + "written to a member ONCE and pointed at afterwards, %u blocks a " + "member. ISSUE falls from 100 B in three accesses to 36 B in two.\n", + FT_TT_QUEUE_PARAM_CACHE_SLOTS); + group->group_address = FT_TT_OP_QUEUE_BASE + FT_TT_QUEUE_GROUP_OFFSET; + { + const char *red = getenv(PN_TT_RED_GROUP_ENV); + + group->red_wait_early = red && strcmp(red, "early") == 0; + if (red && !group->red_wait_early) + fprintf(stderr, "pn_tt: %s=%s is not `early` — ignored\n", PN_TT_RED_GROUP_ENV, red); + if (group->red_wait_early) + fprintf(stderr, "pn_tt: RED PROOF — the group wait accepts one descriptor EARLY\n"); + } + if (group->member_count == 0u || group->member_count > PN_TT_MATMUL_MAX_CORES) { + fprintf(stderr, "pn_tt: %s=%u is outside 1..%u\n", PN_TT_MATMUL_CORES_ENV, + group->member_count, PN_TT_MATMUL_MAX_CORES); + return -1; + } + first = pn_tt_env_u32(PN_TT_MATMUL_CORE_FIRST_ENV, PN_TT_MATMUL_CORE_FIRST_DEFAULT); + + /* Member 0 is the core pn_tt_bring_up_core() already resolved and booted, and its queue + * is the device's own. Nothing about it changes when the group is one member wide. */ + group->member[0].core_index = device->core_index; + group->member[0].core_x = device->core_x; + group->member[0].core_y = device->core_y; + group->member[0].queue = &device->queue; + group->member[0].booted = true; + + /* THE PLACEMENT IS THE COOPERATION DECISION, and it is taken before anything is + * booted. A cooperating group must be a rectangle; an ordinary split group is an + * arithmetic run, which is cheaper to place and is what every recorded number was + * taken with. A rectangle that cannot be built here is reported and falls back — the + * multicast is a speed lever, not a correctness one. */ + if (group->rectangle_requested && group->member_count > 1u && + pn_tt_group_place_rectangle(device) != 0) + fprintf(stderr, "pn_tt: a rectangle was asked for but could not be placed; running " + "as an ordinary split\n"); + + for (member = 1u; member < group->member_count; member++) { + pn_tt_matmul_member *slot = &group->member[member]; + ft_tt_noc_coord core; + + slot->queue = &slot->own_queue; + if (!group->cooperative_placed) + slot->core_index = first + member - 1u; + if (ft_tt_coord_tensix_by_index(&device->runtime.grid, slot->core_index, + FT_TT_COORD_TRANSLATED, &core) != FT_TT_OK) { + fprintf(stderr, "pn_tt: Tensix index %u is not on this part\n", slot->core_index); + return -1; + } + /* Cross-checked rather than assigned when the rectangle placed them: the + * coordinates came from ft_tt_group_rectangle_coordinates() and the INDEX from + * this file's own arithmetic, so a disagreement means one of the two is wrong + * about the live grid — and the multicast rectangle is built from the + * coordinates. */ + if (group->cooperative_placed && + (slot->core_x != (uint16_t)core.x || slot->core_y != (uint16_t)core.y)) { + fprintf(stderr, "pn_tt: member %u core %u is (%u,%u) by index and (%u,%u) by " + "rectangle\n", member, slot->core_index, core.x, core.y, + slot->core_x, slot->core_y); + return -1; + } + slot->core_x = (uint16_t)core.x; + slot->core_y = (uint16_t)core.y; + /* THE CARD IS ASKED WHETHER THIS CORE IS FREE, BEFORE THE BOOT. The group's members + * are the one set that MOVES with a knob, so they land on cores other families held + * in an earlier run — and a compute thread left spinning by that run outlives the + * process that started it. `pn_tt_core_boot_guard.h` is the whole argument. */ + if (!pn_tt_core_boot_is_permitted(&device->runtime, slot->core_index, + "a matmul group member", + PN_TT_MATMUL_CORE_FIRST_ENV)) + return -1; + /* SEQUENTIALLY, from this one process. Booting firmware on many cores from + * independent processes returns `device I/O failed` from 12-way up and leaves cores + * mid-handshake (foundry/tt/DEV_NOTES.md §1); sequential boots are the safe case. */ + if (ft_tt_runtime_boot_core(&device->runtime.noc, slot->core_x, slot->core_y, + &device->firmware) != FT_TT_OK) { + fprintf(stderr, "pn_tt: matmul member %u core %u (%u,%u) firmware boot failed\n", + member, slot->core_index, core.x, core.y); + return -1; + } + slot->booted = true; + fprintf(stderr, "pn_tt: matmul member %u = core %u at translated (%u,%u)\n", member, + slot->core_index, core.x, core.y); + } + + if (group->member_count > 1u && pn_tt_group_activation_doors(device) != 0) + return -1; + return 0; +} + +/* ================================================================ + * The residencies + * ================================================================ */ + +/** Place the triad's three circular buffers: two blocks of `depth` pages on each input so + * the reader fills one while the compute thread drains the other, two pages out. + * + * The page COUNT is `2 * depth` for a reason that outlives this function: the kernel's own + * guard demands `depth` divide the page count AND the write pointer stay on the block + * grid, and with one constant depth through a buffer of exactly two blocks both hold for + * every descriptor, forever, with no state to carry between them. */ +/* M4 (2026-10-04): the pipelined reader and 3-chunk matmul CBs, a MODEL preference + * (pn_tt_device_prefer_matmul_pipe; Qwen3.5 sets it) with env overrides `PN_TT_MATMUL_PIPE` + * and `PN_TT_MATMUL_CB_DEEP`. Read once; the residency is armed once per process shape. */ +static uint32_t g_matmul_pipe_preferred = 0u; + +void pn_tt_device_prefer_matmul_pipe(pn_tt_device *device, bool pipe) +{ + (void)device; + g_matmul_pipe_preferred = pipe ? 1u : 0u; +} + +static int pn_tt_matmul_pipe(void) +{ + return (int)pn_tt_env_u32("PN_TT_MATMUL_PIPE", g_matmul_pipe_preferred); +} + +static uint32_t pn_tt_matmul_cb_deep(void) +{ + return pn_tt_env_u32("PN_TT_MATMUL_CB_DEEP", g_matmul_pipe_preferred ? 3u : 2u) >= 3u ? 3u + : 2u; +} + +/* M5 DIAGNOSIS: `PN_TT_MATMUL_ROW0_AB=N` runs the first N single-row projections twice — + * row-0 forced OFF, then ON — and prints the two outputs' row-0 difference. -1 = no override. */ +static int g_row0_override = -1; + +static uint32_t g_stash_address; + +/** + * BYTES A-REUSE (`PN_TT_A_REUSE=`, default 128, 0 off): CB_IN0's ring is sized to exactly that many + * pages on the decode members, and descriptor word 13 bit 18 asks the reader to skip the A + * read on every pass over a row after its first. The KERNEL decides per descriptor (ring == + * Kt and MB == 1), so a projection of another Kt reads A as before. 128 = Ornith's hidden Kt: + * gate/up (3 passes a member), qkv (2) and the LM head (61) read A once. + */ +static uint32_t pn_tt_a_reuse_tiles(void) +{ + static int tiles = -1; + + if (tiles < 0) + tiles = (int)pn_tt_env_u32("PN_TT_A_REUSE", 128u); + return (uint32_t)tiles; +} + +static int pn_tt_group_place_cbs(uint32_t weight_tile_bytes, uint32_t depth, + uint32_t column_block, uint32_t row_block, + unsigned char *cb_buffer, size_t cb_bytes, + uint64_t *cb_mask, uint32_t stash_tiles) +{ + ft_tt_op_cb_request requests[4]; + ft_tt_launch_cb_entry placed[4]; + int rc; + + /* THE ACTIVATION CB CARRIES THE WHOLE ROW BLOCK, exactly as CB_IN1 carries the whole + * column block and for the same grid reason: `depth * MB` pages a pass, two passes deep, + * and the kernel's own guard refuses any descriptor whose `depth * MB` does not divide + * this count. At MB = 1 it is the `depth * 2` it has always been. */ + requests[0].index = 0u; + requests[0].page_bytes = PN_TT_TILE_BF16; + /* M4: `PN_TT_MATMUL_CB_DEEP` chunks (default 2) — 3 lets the reader run two blocks ahead + * of the compute thread (the pipelined reader needs it to keep one block BUFFERED while + * two are in flight). */ + /* PHASE G option 3: a GRID core (stash_tiles != 0) may run shallower CBs + * (`PN_TT_PREFILL_GRID_DEEP`, default the decode depth) — its B comes from the local + * stash, and two chunks instead of three leave ffn_down's 1,152-tile stash 8 KB to spare. */ + const uint32_t deep = stash_tiles != 0u + ? (pn_tt_env_u32("PN_TT_PREFILL_GRID_DEEP", 2u) >= 3u ? 3u : 2u) + : pn_tt_matmul_cb_deep(); + + requests[0].page_count = depth * row_block * deep; + /* A-REUSE: the ring is Kt pages, never less than the pipelined minimum, and a whole + * number of chunks (the kernel's push-grid guard). Not on a grid core (stash_tiles). */ + if (pn_tt_a_reuse_tiles() != 0u && stash_tiles == 0u && row_block == 1u && + pn_tt_a_reuse_tiles() >= requests[0].page_count && + pn_tt_a_reuse_tiles() % (depth * row_block) == 0u) + requests[0].page_count = pn_tt_a_reuse_tiles(); + /* THE WEIGHT CB CARRIES THE WHOLE COLUMN BLOCK: `depth * NB` pages a pass, two passes + * deep. The kernel's own grid guard reads this page count and refuses any descriptor + * whose `depth * NB` does not divide it, so a mismatch between this line and the + * descriptor word is a loud refusal rather than a write off the end of the buffer. */ + requests[1].index = 1u; + requests[1].page_bytes = weight_tile_bytes; + requests[1].page_count = depth * column_block * deep; + requests[2].index = 16u; + requests[2].page_bytes = PN_TT_TILE_BF16; + requests[2].page_count = 2u; + /* PHASE G0: the B STASH, a CB no kernel names (index 7), so the placement reserves it. */ + requests[3].index = 7u; + requests[3].page_bytes = weight_tile_bytes; + requests[3].page_count = stash_tiles; + rc = ft_tt_op_cb_place(requests, stash_tiles ? 4u : 3u, placed, cb_buffer, cb_bytes, + cb_mask); + if (rc == FT_TT_OK && stash_tiles) + g_stash_address = placed[3].address; + return rc; +} + +/** Stage and KICK one member: five images, the CB descriptor array, five runtime-arg + * blocks, the launch message, GO — and then the host walks away. */ +static int pn_tt_slot_start(pn_tt_device *device, pn_tt_matmul_member *slot, uint32_t member, + uint32_t weight_tile_bytes, uint32_t depth, bool grid); +static int pn_tt_grid_flush(pn_tt_device *device); +static void pn_tt_grid_stop(pn_tt_device *device); + +static int pn_tt_group_member_start(pn_tt_device *device, uint32_t member, + uint32_t weight_tile_bytes, uint32_t depth) +{ + return pn_tt_slot_start(device, &device->group.member[member], member, weight_tile_bytes, + depth, false); +} + +/** Stage and KICK one resident matmul triad on `slot`. `grid` = a PHASE G prefill grid core: + * no group completion, no post probe, never cooperative, and a B stash CB. */ +static int pn_tt_slot_start(pn_tt_device *device, pn_tt_matmul_member *slot, uint32_t member, + uint32_t weight_tile_bytes, uint32_t depth, bool grid) +{ + pn_tt_matmul_group *group = &device->group; + static const unsigned processors[5] = { FT_TT_PROC_DM0, FT_TT_PROC_DM1, FT_TT_PROC_MATH0, + FT_TT_PROC_MATH1, FT_TT_PROC_MATH2 }; + /* kArgCoopCount words, not FT_TT_QUEUE_KERNEL_ARG_COUNT: the reader takes the queue's + * ten plus the cooperative tail. The other four processors are still HANDED ten — the + * tail is DM0's alone, and `ftq_context_init()` reads exactly the ten it always did, so + * the other four resident families' images are untouched by its existing. */ + uint32_t args[5][kArgCoopCount]; + ft_tt_op_args blocks[5]; + unsigned index; + int rc; + + /* STEP 3 B: members 1.. share ONE nonce (`PN_TT_DISPATCH_MCAST`, default 1), so their ring words are + * byte-identical and the puller can multicast them. A token still protects each queue + * against its own stale slots; it never compared across cores. */ + { + static uint32_t shared_nonce; + const char *mcast = getenv("PN_TT_DISPATCH_MCAST"); + uint32_t nonce = ft_tt_queue_make_nonce(); + + if (!grid && member >= 1u && (!mcast || atoi(mcast) > 0)) { + if (member == 1u || shared_nonce == 0u) + shared_nonce = nonce; + nonce = shared_nonce; + } + rc = ft_tt_queue_init(&device->runtime.noc, slot->core_x, slot->core_y, + FT_TT_OP_QUEUE_BASE, PN_TT_QUEUE_CAPACITY, nonce, slot->queue); + } + if (rc != FT_TT_OK) + return rc; + /* M2: the done-sequence word (control byte 0xE0) is NOT in the 16 words queue_init + * clears, and a stale value from the previous arm would satisfy member 0's pull at a + * sequence that restarts at 1. Zeroed BEFORE the kernel launches. */ + { + uint32_t zeros[4] = {0u, 0u, 0u, 0u}; + + rc = device->runtime.noc.write(device->runtime.noc.user, slot->core_x, slot->core_y, + FT_TT_OP_QUEUE_BASE + 0xE0u, zeros, sizeof(zeros)); + if (rc != FT_TT_OK) + return rc; + } + /* THE ISSUE SHAPE, decided once per armed member and DEMOTED rather than refused if this + * queue's blocks are not 16-byte addressable. Every member is placed at the same + * `FT_TT_OP_QUEUE_BASE`, so they agree; taking the minimum makes that a fact rather than + * an assumption, and the group's counters then price what actually happened. */ + if (pn_tt_issue_shape_for(&device->runtime, slot->queue) != PN_TT_ISSUE_WIDE) + device->group.issue_shape = PN_TT_ISSUE_WORD; + + rc = ft_tt_op_program_load(&device->runtime.noc, slot->core_x, slot->core_y, + &device->program_resident.program, NULL, &slot->config); + if (rc != FT_TT_OK) + return rc; + rc = pn_tt_group_place_cbs(weight_tile_bytes, depth, group->column_block, group->row_block, + slot->cb_buffer, sizeof(slot->cb_buffer), + &slot->config.local_cb_mask, + grid ? group->grid_stash_tiles + : pn_tt_env_u32("PN_TT_G0_STASH_TILES", 0u)); + if (grid && rc == FT_TT_OK) + group->grid_stash_address = g_stash_address; + if (rc != FT_TT_OK) + return rc; + slot->config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS; + /* The reader is on BRISC and was built with NOC_INDEX=0; the firmware re-reads this from + * the launch message at run time, so the two must agree. */ + slot->config.brisc_noc_id = 0u; + + for (index = 0; index < 5u; index++) { + ft_tt_queue_kernel_binding binding; + + memset(&binding, 0, sizeof(binding)); + binding.idle_spin_limit = PN_TT_IDLE_SPIN_LIMIT; + binding.cb_wait_spin_limit = PN_TT_CB_WAIT_SPIN_LIMIT; + /* Per-processor status. "The triad is stuck" is not a diagnosis; "NCRISC is in + * CB_WAIT_FRONT at descriptor 12" is, and only these slots can say it. */ + binding.status_address = + FT_TT_QUEUE_STATUS_ADDRESS(slot->queue->control_address, processors[index]); + /* WHERE THIS MEMBER POSTS ITS COMPLETION, and ONLY on the processor that publishes + * it. The writer is DM1 (resident_matmul_writer.c is the one that calls + * ftq_group_post), so DM1 is the single writer of this member's slot — a second + * poster would give one slot two writers, which is the one property a slot array + * exists to have. The block itself lives on member 0, which is why the host can ask + * "has every member finished item N" with one read of one core. + * + * A SPLIT group is still N independent residencies with no barrier between them: + * this changes nothing about the WORK, only about who the host has to ask. */ + if (!grid && group->group_completion && group->member_count > 1u && + processors[index] == FT_TT_PROC_DM1) { + binding.group_x = group->member[0].core_x; + binding.group_y = group->member[0].core_y; + binding.group_address = group->group_address; + binding.group_member = member; + } + /* THE DESTINATION ARM. Same processor, same slot arithmetic, same address — the + * offset is a constant of the queue layout, so it names this member's own unused + * group block under `self` and member 0's under `remote`. ONE variable moves. */ + if (!grid && group->post_probe != PN_TT_GROUP_POST_PROBE_OFF && + processors[index] == FT_TT_PROC_DM1) { + const bool self = group->post_probe == PN_TT_GROUP_POST_PROBE_SELF; + + binding.group_x = self ? slot->core_x : group->member[0].core_x; + binding.group_y = self ? slot->core_y : group->member[0].core_y; + binding.group_address = group->group_address; + /* The SLOT INDEX stays `member` under both arms. Collapsing it to 0 under + * `self` would move a second variable — the address the inline write names — + * for no gain: a member's own block has a slot for it either way. */ + binding.group_member = member; + } + memset(args[index], 0, sizeof(args[index])); + rc = ft_tt_queue_kernel_args(slot->queue, &binding, args[index]); + if (rc != FT_TT_OK) + return rc; + blocks[index].processor = processors[index]; + blocks[index].values = args[index]; + blocks[index].count = FT_TT_QUEUE_KERNEL_ARG_COUNT; + /* THE COOPERATIVE TAIL, ON THE READER ONLY — AND IT IS PLANTED EVEN WHEN THIS + * GROUP DOES NOT COOPERATE. `ft_tt_launch_plant_runtime_args()` writes exactly the + * words it is given and NOTHING CLEARS L1 BETWEEN RESIDENCIES, so a member that + * held a LEADER or RECEIVER role in a previous run would still be reading it here + * — and the kernel would then refuse every solo descriptor with + * kErrCoopRoleMismatch. Measured exactly that way: an N=8 SPLIT arm on cores a + * cooperative N=4 arm had just used refused its first projection, re-armed, and + * refused again. A zero role is SOLO, which is what the memset above leaves. */ + if (processors[index] == FT_TT_PROC_DM0) { + if (!grid && group->cooperative) { + rc = ft_tt_op_matmul_resident_multicast_args(&group->multicast, member, + args[index]); + if (rc != FT_TT_OK) + return rc; + } + blocks[index].count = kArgCoopCount; + } + } + + rc = ft_tt_op_stage(&device->runtime.noc, slot->core_x, slot->core_y, &slot->config, + slot->cb_buffer, sizeof(slot->cb_buffer), blocks, 5u); + if (rc != FT_TT_OK) + return rc; + rc = ft_tt_launch_send_go(&device->runtime.noc, slot->core_x, slot->core_y); + if (rc != FT_TT_OK) + return rc; + slot->running = true; + return FT_TT_OK; +} + +/** + * Retire ONE member by ASKING, and wait for all five processors to say EXIT. + * + * The control block's own `state` cannot answer this: it has ONE writer (the reader), so a + * reader that exited while the compute thread was still spinning out a bound would read + * EXIT and be a lie. There is deliberately no reset escalation — `assert_reset` on a + * blocked resident kernel is what wedged Tensix 0 for a whole host uptime. + */ +static void pn_tt_group_member_stop(pn_tt_device *device, pn_tt_matmul_member *slot) +{ + const uint32_t mask = PN_TT_TRIAD_MASK; + uint32_t pending = mask; + unsigned attempt; + + if (!slot->running) + return; + slot->running = false; + if (ft_tt_queue_request_stop(&device->runtime.noc, slot->queue) != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not ask core %u's residency to stop\n", slot->core_index); + return; + } + /* Each poll is a device read of ~3.5 µs; 20000 of them is comfortably longer than the + * longest descriptor this path issues and still bounded. There is no untimed wait. */ + for (attempt = 0; attempt < 20000u; attempt++) { + if (ft_tt_queue_all_processors_exited(&device->runtime.noc, slot->queue, mask, + &pending) == FT_TT_OK) + return; + } + fprintf(stderr, "pn_tt: core %u's residency did not retire; processors still running: " + "0x%x (the core is left alone, NOT parked and NOT reset)\n", + slot->core_index, pending); +} + +int pn_tt_matmul_group_arm(pn_tt_device *device, uint32_t weight_tile_bytes, uint32_t depth) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t member; + + group->sequence = 0u; + group->chained = 0u; + memset(group->pending, 0, sizeof(group->pending)); + /* The sequences restart at 0: a pull target from the previous residency is unreachable. + * RED `grid_chain_stale` keeps it (and skips the grid path's chain begin) — the pre-fix + * state, which must abort a long prompt after a pause. */ + if (!pn_tt_red("grid_chain_stale")) { + group->chain_pull_members = 0u; + group->chain_pull_target = 0u; + group->chain_last_pulled = false; + } + + /* THE ARM IS WHERE COOPERATION IS DECIDED, because it is the first moment every weight + * has been placed and the answer is a property of the WHOLE weight set. It is + * all-or-nothing: a residency that cooperates on some descriptors and not others + * leaves its members' CB_IN0 write pointers at different offsets, and every later + * multicast then lands in the wrong half of the receivers' buffers with every kernel + * completing, every status word clean and only a byte comparison able to see it. */ + group->cooperative = group->cooperative_requested && group->cooperative_placed && + group->member_count > 1u && group->uneven_weights == 0u && + /* THE ROW BLOCK CHANGES WHAT AN A BLOCK IS, and the rendezvous + * counts A blocks: the leader would multicast `depth * MB` tiles + * where the receivers expect `depth`. Two mechanisms that both + * move A's supply, agreed separately, is a wrong answer with every + * status word clean — the same reason the column block disarms it, + * one operand over. The kernel refuses the pair as well. */ + group->row_block <= 1u; + if (group->cooperative_requested && group->cooperative_placed && !group->cooperative) + fprintf(stderr, "pn_tt: cooperation is OFF — %u of the placed weights do not split " + "evenly across %u members, and the rendezvous needs every member to " + "walk the same number of blocks\n", + group->uneven_weights, group->member_count); + if (group->cooperative) { + /* Staged BEFORE any member is kicked: a member whose arrival word still held the + * previous residency's block counter would sail through its first wait with + * nothing having arrived. */ + int rc = ft_tt_op_matmul_resident_multicast_reset(&device->runtime.noc, + &group->multicast); + + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not stage the rendezvous words: %s\n", + ft_tt_error_string(rc)); + return rc; + } + } + + /* THE BLOCK IS WRITTEN BEFORE ANY MEMBER IS KICKED, so no writer can post into an + * un-magicked block, and the sequences a previous residency left cannot be read as + * this one's. It lives on member 0 above the parameter blocks — ft_tt_queue.c pins + * the layout with static asserts, so nothing here re-derives the offsets. */ + if (group->group_completion && group->member_count > 1u) { + int rc = ft_tt_queue_group_init(&device->runtime.noc, group->member[0].core_x, + group->member[0].core_y, group->group_address, + group->member_count); + + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not write the group completion block: %s\n", + ft_tt_error_string(rc)); + return rc; + } + } + /* THE PROBE'S BLOCKS, for the same reason and by the same call. `self` posts land in + * N different cores' L1, so all N are written; `remote` writes only member 0's, which + * is what the aggregated path does. The card side never checks the magic — this is so + * a stale block cannot be MISREAD afterwards when the wedge is being attributed. */ + if (group->post_probe != PN_TT_GROUP_POST_PROBE_OFF) { + const uint32_t blocks = + group->post_probe == PN_TT_GROUP_POST_PROBE_SELF ? group->member_count : 1u; + uint32_t index; + + for (index = 0; index < blocks; index++) { + int rc = ft_tt_queue_group_init(&device->runtime.noc, group->member[index].core_x, + group->member[index].core_y, group->group_address, + group->member_count); + + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not write the probe's group block on member " + "%u (core %u): %s\n", index, group->member[index].core_index, + ft_tt_error_string(rc)); + return rc; + } + } + } + + for (member = 0; member < group->member_count; member++) { + int rc; + + /* CLEARED AT ARM, WHICH IS THE ONLY MOMENT IT IS SAFE. Nothing clears a core's L1 + * between residencies, so a table carried across an arm would hand a descriptor an + * address holding the PREVIOUS residency's words — the same class of defect as the + * cooperative role a stale runtime-arg word left behind. No descriptor is in flight + * here by construction. */ + if (group->param_cache) + pn_tt_param_cache_alloc(&group->member[member]); + rc = pn_tt_group_member_start(device, member, weight_tile_bytes, depth); + + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: matmul member %u (core %u) did not arm: %s\n", member, + group->member[member].core_index, ft_tt_error_string(rc)); + /* Partly armed is not a state anything may run in: a projection would be split + * across members that are not all listening, and the missing member's columns + * would be whatever the output strip held last. */ + pn_tt_matmul_group_retire(device); + return rc; + } + } + /* THE MAGIC IS CHECKED ONCE, HERE, and never again — re-reading sixteen header words + * on every poll is exactly the cost the slots-only read exists to remove. What it + * catches is the wrong address, which is a constant of the run, not a per-poll risk. */ + if (group->group_completion && group->member_count > 1u) { + ft_tt_queue_group_slot slots[PN_TT_MATMUL_MAX_CORES]; + int rc = ft_tt_queue_group_read(&device->runtime.noc, group->member[0].core_x, + group->member[0].core_y, group->group_address, + group->member_count, slots); + + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: the group completion block does not read back with its " + "magic: %s\n", ft_tt_error_string(rc)); + pn_tt_matmul_group_retire(device); + return rc; + } + } + group->pull_table_ok = false; + group->pull = pn_tt_env_u32("PN_TT_MATMUL_PULL", 1u) != 0u; + group->red_pull_skip = pn_tt_red("matmul_pull_skip"); + if (group->pull && group->member_count > 1u && group->member_count <= 64u && + !group->cooperative) { + uint32_t table[2u + 64u]; + uint32_t back[2u + 64u]; + const uint32_t words = 2u + group->member_count; + const uint32_t address = FT_TT_OP_QUEUE_BASE + 0xF900u; + uint32_t m; + + table[0] = 0x46545150u; /* "FTQP", kPullMagic */ + table[1] = group->member_count; + for (m = 0; m < group->member_count; m++) + table[2u + m] = (uint32_t)group->member[m].core_x | + ((uint32_t)group->member[m].core_y << 16); + if (device->runtime.noc.write(device->runtime.noc.user, group->member[0].core_x, + group->member[0].core_y, address, table, + words * 4u) == FT_TT_OK && + device->runtime.noc.read(device->runtime.noc.user, group->member[0].core_x, + group->member[0].core_y, address, back, + words * 4u) == FT_TT_OK && + memcmp(table, back, words * 4u) == 0) + group->pull_table_ok = true; + fprintf(stderr, "pn_tt: matmul PULL completion %s — member 0 (core %u) reads %u " + "members' done-sequence before posting%s\n", + group->pull_table_ok ? "ARMED" : "REFUSED (the table did not read back)", + group->member[0].core_index, group->member_count, + group->red_pull_skip ? "; RED PROOF: the pull is SKIPPED" : ""); + } + device->resident_tile_bytes = weight_tile_bytes; + device->resident_depth = depth; + device->resident_running = true; + group->last_alive_seconds = pn_tt_now_seconds(); + return FT_TT_OK; +} + +void pn_tt_matmul_group_retire(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t member; + + device->resident_running = false; + /* NOT the prefill grid: an idle refresh retires and re-arms the decode group at a + * prefill's first projection, and the grid it would kill is the one about to be used. + * The grid is retired by pn_tt_matmul_grid_end() and at device close. */ + for (member = 0; member < group->member_count; member++) { + pn_tt_group_member_stop(device, &group->member[member]); + /* AFTER the stop, not before: the card must not be executing a descriptor that + * points into a region this host copy is the only record of. */ + pn_tt_param_cache_free(&group->member[member]); + } +} + +/* ================================================================ + * The load-time shard + * ================================================================ */ + +/** + * Split one N-major weight over `member_count` banks, or say why it stayed whole. + * + * The split is a byte-run relocation and nothing else: an N-major tile grid indexes + * `nt * Kt + kt`, so columns [c*cc, (c+1)*cc) are the contiguous run + * `[c*cc*Kt*tile_bytes, (c+1)*cc*Kt*tile_bytes)` of the canonical payload. Member c then + * reads a grid `cc` columns wide from column 0 of its own panel, which is why the reader + * kernel cannot tell a sharded weight from an unsharded one. + * + * The cooperative planner is what REFUSES a shape here — a `tile_cols` the member count + * does not divide, a zero dimension, a shard list that does not cover every member — and + * what counts the doors that would share a NoC row. Its column arithmetic is then checked + * against this function's own, so the two cannot drift apart silently. + * + * @return 0 (sharded or deliberately left whole), or -1 with the reason on stderr + */ +/** + * Upload destination columns `[first, first+count)` of `entry` into one panel. + * + * `runs` NULL is the IDENTITY and is one `ft_tt_fttw_upload_range()` — byte for byte what + * this path always did, which is the property that lets every ordinary weight go through the + * permuting code with nothing about it changing. A run list issues one range per run the + * panel intersects, because an N-major artifact lays column tile `c` as a contiguous + * `column_bytes` run and a permutation of columns is therefore a permutation of byte ranges. + * + * @return 0, or -1 with the reason on stderr + */ +static int pn_tt_upload_panel_columns(pn_tt_device *device, const ft_tt_artifact_entry *entry, + const pn_tt_column_run *runs, uint32_t run_count, + uint64_t column_bytes, uint32_t first, uint32_t count, + uint16_t door_x, uint16_t door_y, uint32_t address) +{ + uint32_t written = 0u; + + if (!runs) { + return ft_tt_fttw_upload_range(&device->artifact, entry, + (uint64_t)first * column_bytes, + (uint64_t)count * column_bytes, + &device->runtime.bulk_dram, door_x, door_y, address, + 0u) == FT_TT_OK + ? 0 + : -1; + } + while (written < count) { + uint32_t source = 0u, contiguous = 0u, piece; + + if (!pn_tt_column_runs_map(runs, run_count, first + written, &source, &contiguous)) { + fprintf(stderr, "pn_tt: %s destination column %u is past the run list\n", + entry->name, first + written); + return -1; + } + piece = contiguous < count - written ? contiguous : count - written; + if (ft_tt_fttw_upload_range(&device->artifact, entry, (uint64_t)source * column_bytes, + (uint64_t)piece * column_bytes, &device->runtime.bulk_dram, + door_x, door_y, + address + (uint32_t)((uint64_t)written * column_bytes), + 0u) != FT_TT_OK) { + fprintf(stderr, "pn_tt: upload failed for %s columns %u..%u\n", entry->name, + source, source + piece); + return -1; + } + written += piece; + } + return 0; +} + +/** One bank, one buffer, one upload — the pre-group placement, kept as the arm a sharded + * weight is compared against and the only one a `Nt % N != 0` tensor can take. */ +static int pn_tt_group_place_whole(pn_tt_device *device, const ft_tt_artifact_entry *entry, + const pn_tt_column_run *runs, uint32_t run_count, + uint32_t bank, pn_tt_weight *weight) +{ + const uint64_t column_bytes = (uint64_t)weight->tile_rows * weight->tile_bytes; + const uint64_t bytes = (uint64_t)weight->tile_cols * column_bytes; + ft_tt_buffer buffer; + + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes, &buffer) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full at %s\n", entry->name); + return -1; + } + if (pn_tt_upload_panel_columns(device, entry, runs, run_count, column_bytes, 0u, + weight->tile_cols, + (uint16_t)device->dram[buffer.logical_bank].x, + (uint16_t)device->dram[buffer.logical_bank].y, + (uint32_t)buffer.address) != 0) + return -1; + weight->bank = buffer.logical_bank; + weight->address = (uint32_t)buffer.address; + weight->shard_count = 1u; + weight->shard_tile_cols[0] = weight->tile_cols; + weight->shard_column_first[0] = 0u; + weight->shard_address[0] = weight->address; + weight->shard_x[0] = (uint16_t)device->dram[buffer.logical_bank].x; + weight->shard_y[0] = (uint16_t)device->dram[buffer.logical_bank].y; + weight->shard_bank[0] = buffer.logical_bank; + device->bytes_written += bytes; + return 0; +} + +int pn_tt_matmul_group_place_weight(pn_tt_device *device, const ft_tt_artifact_entry *entry, + uint32_t *bank_cursor, pn_tt_weight *weight) +{ + return pn_tt_matmul_group_place_weight_columns(device, entry, NULL, 0u, bank_cursor, + weight); +} + +int pn_tt_matmul_group_place_weight_columns(pn_tt_device *device, + const ft_tt_artifact_entry *entry, + const pn_tt_column_run *runs, uint32_t run_count, + uint32_t *bank_cursor, pn_tt_weight *weight) +{ + pn_tt_matmul_group *group = &device->group; + ft_tt_op_matmul_cooperative_request request; + ft_tt_op_matmul_cooperative_plan plan; + ft_tt_dram_endpoint doors[PN_TT_MATMUL_MAX_CORES]; + uint32_t banks[PN_TT_MATMUL_MAX_CORES]; + ft_tt_buffer buffer[PN_TT_MATMUL_MAX_CORES]; + uint32_t members = group->member_count; + uint64_t column_bytes = (uint64_t)weight->tile_rows * weight->tile_bytes; + uint32_t columns[PN_TT_MATMUL_MAX_CORES]; + uint32_t first[PN_TT_MATMUL_MAX_CORES]; + uint64_t panel_bytes[PN_TT_MATMUL_MAX_CORES]; + uint32_t base_columns, remainder, running; + uint32_t member; + + /* THE PERMUTATION IS REFUSED HERE OR NOWHERE. Downstream this is a byte offset, so a run + * list that leaves the tensor, or uses one source column twice, is a plausible-looking + * weight with one column duplicated and another missing — a smoothly wrong projection + * that neither the planner nor the card can see. pn_tt_column_runs.h states it. */ + if (runs && (pn_tt_column_runs_total(runs, run_count) != weight->tile_cols || + !pn_tt_column_runs_are_injective(runs, run_count, entry->tile_cols))) { + fprintf(stderr, "pn_tt: the column permutation for %s does not produce %u columns " + "injectively out of %u\n", entry->name, weight->tile_cols, + entry->tile_cols); + return -1; + } + + /* THE REMAINDER GOES TO THE FIRST MEMBERS, one column each, so the widths differ by at + * most one and every column of the weight is owned exactly once. `first` is the running + * sum, which is what makes the panels concatenate at Mt = 1 when they are not all the + * same width. */ + base_columns = members > 0u ? weight->tile_cols / members : 0u; + remainder = members > 0u ? weight->tile_cols % members : 0u; + running = 0u; + for (member = 0; member < members; member++) { + columns[member] = base_columns + (member < remainder ? 1u : 0u); + first[member] = running; + panel_bytes[member] = (uint64_t)columns[member] * column_bytes; + running += columns[member]; + } + + /* The tile grid and the payload must agree before a byte offset into the payload means + * anything. They always have; this is the check that says so rather than assumes it. + * A member with ZERO columns is a descriptor the resident reader refuses, and a refusal + * on the card strands a triad — so too few columns for the group keeps the weight whole + * rather than sending one. */ + /* The SOURCE grid and the payload, not the destination's: a permuted placement is a + * subset, so holding the destination width against the file's byte count would call + * every de-interleaved half a mismatch. */ + if (members < 2u || base_columns == 0u || + (uint64_t)entry->tile_cols * column_bytes != entry->byte_count) { + if (members >= 2u && base_columns != 0u) + fprintf(stderr, "pn_tt: %s is %llu B but Kt*Nt*tile is %llu — not sharding it\n", + entry->name, (unsigned long long)entry->byte_count, + (unsigned long long)((uint64_t)entry->tile_cols * column_bytes)); + group->unsharded_weights++; + /* A weight that is not split at all leaves the members walking different block + * counts, which is the same desynchronisation a ragged split causes and is fatal + * to the rendezvous for exactly the same reason. */ + group->uneven_weights++; + if (pn_tt_group_place_whole(device, entry, runs, run_count, + *bank_cursor % device->dram_bank_count, weight) != 0) + return -1; + *bank_cursor = (*bank_cursor + 1u) % device->dram_bank_count; + return 0; + } + if (remainder != 0u) { + group->ragged_weights++; + /* COOPERATION CANNOT SURVIVE A RAGGED SPLIT, and this is where that is decided. + * The per-block rendezvous is correct only because every member walks the same + * number of blocks in the same order, and blocks are Mt * Nt_local * Kt/depth — so + * members of different widths fall out of step on the first descriptor and stay + * out of step. SPLIT has no rendezvous and takes the ragged shape happily, which is + * why this counts rather than refuses. */ + group->uneven_weights++; + } + + for (member = 0; member < members; member++) + banks[member] = (*bank_cursor + member) % device->dram_bank_count; + if (ft_tt_dram_stripe_choose_ports(&device->runtime.grid, banks, members, + FT_TT_COORD_TRANSLATED, doors) != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not choose weight doors for %s\n", entry->name); + return -1; + } + + memset(&request, 0, sizeof(request)); + request.tile_rows = 1u; /* Mt: a token is one row, and the panels concatenate only here */ + request.tile_inner = weight->tile_rows; + request.tile_cols = weight->tile_cols; + request.prefetch_tiles = device->resident_depth ? device->resident_depth : 1u; + request.weight_n_major = 1u; + request.mode = FT_TT_OP_MATMUL_SPLIT; + request.spin_limit = 1u; /* unread outside COOPERATIVE; never 0, which is unbounded */ + request.activation.x = (uint16_t)device->dram[0].x; + request.activation.y = (uint16_t)device->dram[0].y; + request.activation.tile_bytes = PN_TT_TILE_BF16; + request.weight.tile_bytes = weight->tile_bytes; + request.output.tile_bytes = PN_TT_TILE_BF16; + request.weight_shard_count = members; + + for (member = 0; member < members; member++) { + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, banks[member], + (size_t)panel_bytes[member], &buffer[member]) != FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full at %s shard %u (bank %u)\n", entry->name, member, + banks[member]); + return -1; + } + request.weight_shards[member].endpoint = doors[member]; + request.weight_shards[member].address = (uint32_t)buffer[member].address; + } + + if (ft_tt_op_matmul_cooperative_plan_build(NULL, members, 0u, &request, &plan) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the cooperative planner refused %s over %u members\n", + entry->name, members); + return -1; + } + if (plan.row_collisions > group->worst_weight_row_collisions) + group->worst_weight_row_collisions = plan.row_collisions; + + for (member = 0; member < members; member++) { + const ft_tt_op_matmul_cooperative_member *slice = &plan.member[member]; + + /* The planner's split and this function's byte arithmetic must be the SAME split. + * They are derived independently — one from the planner's own remainder rule, one + * from a payload offset — so disagreeing is a wrong answer with no error anywhere, + * and a RAGGED split is exactly where two independent derivations can drift. */ + if (slice->column_count != columns[member] || slice->column_first != first[member]) { + fprintf(stderr, "pn_tt: %s shard %u disagrees with the plan (%u..%u vs %u..%u)\n", + entry->name, member, slice->column_first, + slice->column_first + slice->column_count, first[member], + first[member] + columns[member]); + return -1; + } + if (pn_tt_upload_panel_columns(device, entry, runs, run_count, column_bytes, + first[member], columns[member], + (uint16_t)doors[member].coord.x, + (uint16_t)doors[member].coord.y, + (uint32_t)buffer[member].address) != 0) { + fprintf(stderr, "pn_tt: upload failed for %s shard %u\n", entry->name, member); + return -1; + } + weight->shard_address[member] = (uint32_t)buffer[member].address; + weight->shard_x[member] = (uint16_t)doors[member].coord.x; + weight->shard_y[member] = (uint16_t)doors[member].coord.y; + weight->shard_tile_cols[member] = columns[member]; + weight->shard_column_first[member] = first[member]; + weight->shard_bank[member] = banks[member]; + device->bytes_written += panel_bytes[member]; + } + + weight->bank = buffer[0].logical_bank; + weight->address = (uint32_t)buffer[0].address; + weight->shard_count = members; + group->sharded_weights++; + /* Offset the next tensor's bank run by ONE, not by `members`: successive projections + * are serial, so what matters is that no bank is the first of every tensor, and a + * stride of one visits every bank however many members there are. */ + *bank_cursor = (*bank_cursor + 1u) % device->dram_bank_count; + return 0; +} + +/* ================================================================ + * Dispatch + * ================================================================ */ + +/** + * Is this shape one the resident reader will execute rather than REFUSE? + * + * A host mirror of `descriptor_is_executable()` in resident_matmul_reader.c, and it exists + * because the two answers have very different costs: refused here is a return value, and + * refused on the card stops that member's reader mid-residency. `columns` is the MEMBER's + * Nt, not the weight's — a member with zero columns is the refusal this catches. + */ +static bool pn_tt_group_descriptor_is_executable(const pn_tt_device *device, + const pn_tt_weight *weight, uint32_t columns) +{ + const uint32_t block = device->group.column_block ? device->group.column_block : 1u; + const uint32_t rows = device->group.row_block ? device->group.row_block : 1u; + const uint32_t deep = pn_tt_matmul_cb_deep(); + uint32_t pages = device->resident_depth * rows * deep; + uint32_t weight_pages = device->resident_depth * block * deep; + uint32_t chunk = device->resident_depth * block; + uint32_t a_chunk = device->resident_depth * rows; + + if (weight->tile_bytes != device->resident_tile_bytes) + return false; /* the residency is bound to ONE weight tile size */ + if (weight->tile_rows == 0u || columns == 0u) + return false; + /* MB * NB OUTPUT TILES ARE LIVE IN DST AT ONCE. The kernel refuses this too; asked here + * so a mis-set knob is a message rather than a stranded triad. */ + if (!ftmm_block_fits_section(rows, block, device->group.dst_section_tiles)) + return false; + /* The kernel silently collapses a depth that does not divide Kt to 1 — which is correct + * but would push single pages through a buffer sized in blocks of `depth`, leaving the + * write pointer off the block grid for every LATER descriptor. Refuse instead. */ + if (device->resident_depth == 0u || weight->tile_rows % device->resident_depth != 0u) + return false; + /* CB_IN0's chunk is the WHOLE row block whatever this descriptor's Mt is, the mirror of + * the CB_IN1 condition below. `Mt < MB` is legal — it takes a short block. */ + if (a_chunk == 0u || a_chunk > pages || pages % a_chunk != 0u) + return false; + /* CB_IN1's chunk is the WHOLE column block whatever this weight's Nt is, so the grid + * condition is on `depth * NB` and not on the columns. `columns < NB` is legal and + * common — 48 of Ornith's 249 weights are Nt = 1. */ + if (chunk > weight_pages || weight_pages % chunk != 0u) + return false; + /* The MOP's SrcA stride is `depth * page` in a 16-bit SETDMAREG halfword; the compute + * thread's page unit is 16 B, so this is the only shape that could truncate silently. */ + if (block > 1u && (uint64_t)chunk * device->resident_tile_bytes / 16u > 0xFFFFu) + return false; + return true; +} + +/** + * Which MIRROR of the A strip member `m` reads. + * + * `m % replica_count`, and that is the whole descriptor side of the bank spread — dispatch + * already writes a separate parameter block per member, so naming a different address in + * each is an index and not a protocol change. + * + * IT IS ROUND-ROBIN AND NOT `m / (members / replicas)` DELIBERATELY. The two agree when the + * replica count divides the member count and disagree when it does not, and the modulo is + * the one that spreads a RAGGED count: at 6 members over 4 mirrors it gives 2/2/1/1 readers + * a bank, where the division gives 4/2/0/0 and leaves half the banks idle. + * + * A strip with one mirror returns 0 for every member, which is the pre-replication + * descriptor word for word. + */ +static uint32_t pn_tt_group_replica_index(const pn_tt_activation *input, uint32_t member) +{ + if (!input || input->replica_count <= 1u) + return 0u; + return member % input->replica_count; +} + +/** The bank member `m` really reads A from, red arms included. ONE expression, because it is + * needed twice — in the descriptor and in the per-bank read accounting — and two copies of + * it would print a bank table describing a run that did not happen. */ +static uint32_t pn_tt_group_activation_bank(const pn_tt_matmul_group *group, + const pn_tt_activation *input, uint32_t member) +{ + const uint32_t bank = input->replica_bank[pn_tt_group_replica_index(input, member)]; + + if (group->red_a_spread && group->activation_doors && group->member_count > 1u && + bank < group->activation_bank_count) + return (bank + member) % group->activation_bank_count; + return bank; +} + +/** + * `Mt` for this projection: how many 32-row bands of A one descriptor sweeps the weight past. + * + * ONE EXPRESSION, because it is needed in the parameter block AND in the read accounting, and + * two copies of it would print a byte table describing a descriptor that was not issued. + */ +static uint32_t pn_tt_group_tile_rows(const pn_tt_activation *input, + const pn_tt_activation *output) +{ + uint32_t rows = input && input->active_rows ? input->active_rows : 1u; + + if (output && output->active_rows && output->active_rows < rows) + rows = output->active_rows; + return rows; +} + +/** The sixteen descriptor words for ONE member of ONE projection. */ +static void pn_tt_group_parameters(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, + const pn_tt_activation *output, uint32_t member, + bool split, uint32_t *parameters) +{ + const pn_tt_matmul_group *group = &device->group; + uint32_t columns = split ? weight->shard_tile_cols[member] : weight->tile_cols; + uint32_t column_first = split ? weight->shard_column_first[member] : 0u; + uint32_t weight_address = split ? weight->shard_address[member] : weight->address; + uint16_t weight_x = split ? weight->shard_x[member] + : (uint16_t)device->dram[weight->bank].x; + uint16_t weight_y = split ? weight->shard_y[member] + : (uint16_t)device->dram[weight->bank].y; + /* WHICH MIRROR, AND THEREFORE WHICH BANK. A strip with one mirror resolves to the + * strip's own bank and address, so an unreplicated run is the pre-replication + * descriptor word for word. */ + const uint32_t replica = pn_tt_group_replica_index(input, member); + const uint32_t activation_bank = pn_tt_group_activation_bank(group, input, member); + uint32_t activation_address = input->replica_address[replica]; + uint16_t activation_x = (uint16_t)device->dram[activation_bank].x; + uint16_t activation_y = (uint16_t)device->dram[activation_bank].y; + + /* A DOOR, not a different memory: members that landed on the SAME bank read the same + * bytes at the same address through a different NoC port of it. It is the cheapest + * bandwidth lever there is and it moves nothing (ft_tt_dram_stripe.h) — but it is a + * lever against the ~45 GB/s ENDPOINT ceiling and not against the ~59 GB/s BANK, which + * is what the mirrors above are for. Left at the operand's own coordinate when the + * group is one member wide, so N=1 is the pre-group descriptor word for word. */ + if (group->activation_doors && group->member_count > 1u && + activation_bank < group->activation_bank_count) { + activation_x = (uint16_t)group->activation_door[activation_bank][member].coord.x; + activation_y = (uint16_t)group->activation_door[activation_bank][member].coord.y; + } + /* RED PROOF `PN_TT_RED=matmul_a_spread` moved the BANK above and leaves the address + * word alone, so the member reads whatever that bank holds at the strip's offset — + * garbage, deliberately. It is the timing arm the production mirrors replace, kept + * because it is the one that isolates the bank from everything else. */ + + memset(parameters, 0, FT_TT_QUEUE_PARAM_WORDS * sizeof(parameters[0])); + /* Mt. A decode token is one tile-row and that is the literal this used to be; a batched + * prefill pass is `active_rows` bands of 32 positions, and the weight is swept ONCE for + * `ceil(Mt/MB)` of them rather than once for each. + * + * THE SPAN IS THE NARROWER OF THE TWO STRIPS, not the input's. The reader walks A at + * `(mt + row) * tile_inner + kt` and the writer places C at `(mt + row) * a2 + nt + + * column`, so an Mt the OUTPUT cannot hold is a write past the end of the destination + * buffer with every kernel retiring clean and DRAM having no bounds check. The clamp is + * the one place that can see both. */ + parameters[FT_TT_QUEUE_MATMUL_TILE_ROWS] = + pn_tt_group_tile_rows(input, output); + parameters[FT_TT_QUEUE_MATMUL_TILE_INNER] = weight->tile_rows; + parameters[FT_TT_QUEUE_MATMUL_TILE_COLS] = columns; + parameters[FT_TT_QUEUE_MATMUL_A_X] = activation_x; + parameters[FT_TT_QUEUE_MATMUL_A_Y] = activation_y; + /* RED PROOF `PN_TT_RED=matmul_a_offset`: same BANK, different ADDRESS. It is the other + * half of the pair. `matmul_a_spread` moves the bank AND the address range at once, so + * on its own it cannot tell a channel ceiling from the fact that N members are hammering + * ONE 16 KiB window — the 59.2 GB/s per-bank figure in the spec was measured with each + * core on a DISJOINT 256 MiB region, which is the opposite access pattern. This arm + * keeps the bank and disjoins the windows, so the two together separate them. + * 4 MiB is well inside a bank and well past any strip. **The answer is garbage.** */ + if (group->red_a_offset) + activation_address += member * (4u << 20); + parameters[FT_TT_QUEUE_MATMUL_A_BASE] = activation_address; + parameters[FT_TT_QUEUE_MATMUL_B_X] = weight_x; + parameters[FT_TT_QUEUE_MATMUL_B_Y] = weight_y; + parameters[FT_TT_QUEUE_MATMUL_B_BASE] = weight_address; + parameters[FT_TT_QUEUE_MATMUL_C_X] = (uint16_t)device->dram[output->bank].x; + parameters[FT_TT_QUEUE_MATMUL_C_Y] = (uint16_t)device->dram[output->bank].y; + /* The panels CONCATENATE, which is the whole correctness claim: at Mt=1 member c's + * `columns` output tiles land exactly where the single-core writer would have put + * columns [column_first, column_first + columns). The offset is the RUNNING SUM and not + * `member * columns` — those agree only while every member is the same width, and a + * ragged split is precisely where they stop agreeing. */ + parameters[FT_TT_QUEUE_MATMUL_C_BASE] = + output->address + column_first * PN_TT_TILE_BF16; + parameters[FT_TT_QUEUE_MATMUL_DEPTH] = device->resident_depth; + /* A STATEMENT about DRAM, not a request: the artifact's MATMUL_NMAJOR layout IS the + * N-major tile grid, and a wrong value here is a wrong answer with no error anywhere. */ + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] = 1u; + /* M4 PIPELINED READER (bit 8; resident_matmul_reader.c read_matmul_pipelined): + * `PN_TT_MATMUL_PIPE`; `PN_TT_RED=matmul_pipe_no_wait` (bit 9) never waits and must DIFFER. */ + { + const int pipe = pn_tt_matmul_pipe(); + + if (pipe != 0) + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 8; + if (pipe != 0 && pn_tt_red("matmul_pipe_no_wait")) + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 9; + /* M5 ROW-0 (bit 10): a single-token forward's matmuls read A faces 0-1 only and + * multiply in0 as a 16x32 half tile. Needs the pipelined reader. `PN_TT_MATMUL_ROW0` + * 0 off, 1 honour the single-row promise. */ + { + static int row0 = -1; + + if (row0 < 0) + row0 = (int)pn_tt_env_u32("PN_TT_MATMUL_ROW0", 1u); + if (g_row0_override >= 0 ? (g_row0_override != 0 && pipe != 0) + : (row0 != 0 && pipe != 0 && + (device->single_row || + (device->verify_rows && device->verify_rows <= 16u)))) { + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 10; + if (row0 == 2) /* DIAGNOSTIC: reader row-0, compute full (bit 17) */ + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 17; + if (pn_tt_red("matmul_row0_face0")) + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 11; + if (pn_tt_env_u32("PN_TT_MATMUL_ROW0_DIAG", 0u) != 0u) + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 16; + } + } + /* BYTES A-REUSE (bit 18; bit 19 RED = skip regardless of the ring). */ + if (pipe != 0 && pn_tt_a_reuse_tiles() != 0u) + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 18; + if (pipe != 0 && pn_tt_red("matmul_a_reuse")) + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= 1u << 19; + /* M5: `PN_TT_MATMUL_PROBE` = word-13 bits 12-15 (1 no read, 2 no compute, 4 no write, + * 8 FLUSH writer) — resident_queue_protocol.h kMatmulProbe*. */ + parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] |= + (pn_tt_env_u32("PN_TT_MATMUL_PROBE", 0u) & 0xFu) << 12; + } + /* THE SAME VALUE ON EVERY DESCRIPTOR OF THE RESIDENCY, because CB_IN1's chunk is + * `depth * NB` and a chunk size that changes walks the write pointer off the grid the + * next descriptor needs. A weight narrower than NB is not an exception — it takes a + * short last block, which shortens the fill and not the chunk. + * + * ONE WORD, TWO BLOCKS. The row block rides in bits 16..23 of the same word and obeys + * the same per-residency rule for the same reason, one CB over. At `row_block == 1` the + * encoder returns the pre-encoding word BIT FOR BIT, which is what makes a decode `cmp` + * evidence rather than a coincidence (tt/kernels/resident_matmul_block_protocol.h). */ + parameters[FT_TT_QUEUE_MATMUL_COLUMN_BLOCK] = + ftmm_block_word(group->row_block, group->column_block); + /* ALL-OR-NOTHING, and it is `split` that decides it rather than the weight: a + * cooperating residency's members must agree on every descriptor, and the kernel + * REFUSES one whose word disagrees with its role. A weight that did not split would + * therefore have to stop the whole residency cooperating, which is what + * `group->uneven_weights` does at arm time — by the time a descriptor is written, + * `group->cooperative` already means "every weight splits". */ + parameters[FT_TT_QUEUE_MATMUL_COOPERATIVE] = + (split && device->group.cooperative) + ? (device->group.red_coop_stale ? FT_TT_RESIDENT_COOP_DESCRIPTOR_RED_SKIP_PAYLOAD + : FT_TT_RESIDENT_COOP_DESCRIPTOR_COOPERATE) + : FT_TT_RESIDENT_COOP_DESCRIPTOR_SOLO; + + if (split && member == 1u && device->red_split != PN_TT_RED_SPLIT_NONE) { + switch (device->red_split) { + case PN_TT_RED_SPLIT_BASE: + parameters[FT_TT_QUEUE_MATMUL_B_BASE] = weight->shard_address[0]; + parameters[FT_TT_QUEUE_MATMUL_B_X] = weight->shard_x[0]; + parameters[FT_TT_QUEUE_MATMUL_B_Y] = weight->shard_y[0]; + break; + case PN_TT_RED_SPLIT_OUT: + parameters[FT_TT_QUEUE_MATMUL_C_BASE] = output->address; + break; + case PN_TT_RED_SPLIT_NARROW: + if (columns > 1u) + parameters[FT_TT_QUEUE_MATMUL_TILE_COLS] = columns - 1u; + break; + default: + break; + } + } +} + +/** + * Wait for `expected` descriptors on one member — or for its kernel to say it refused one, + * which is a different thing and must not be waited out. + * + * `ft_tt_queue_wait_completed()` polls the count alone, so a refusal costs the whole + * timeout before anyone looks at the error word. The count is polled every iteration and + * the error word every 128th, which finds a refusal inside a millisecond for 0.8% more + * reads. + */ +static int pn_tt_group_await(pn_tt_device *device, pn_tt_matmul_member *slot, + uint32_t expected) +{ + const double deadline = pn_tt_now_seconds() + (double)PN_TT_OP_TIMEOUT_MS / 1000.0; + uint32_t fields[FT_TT_QUEUE_FIELD_COUNT]; + const char *reason = NULL; + unsigned poll = 0; + + /* THE ONE-TRANSACTION PATH. The poll and the check are the same 16-byte read, so a + * member that is already finished — which is every member but the straggler — costs ONE + * transaction instead of three. `error` is not in the line; see PN_TT_FAST_COMPLETION_ENV + * for the four places it is still read and why a refusal cannot hide behind it. */ + if (device->group.fast_completion) { + for (;;) { + int rc = ft_tt_queue_read_completion_line(&device->runtime.bulk_l1, slot->queue, + fields); + + device->group.poll_reads++; + device->group.wait_reads++; + device->group.wait_read_bytes += 16u; + device->group.wait_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] >= expected) + break; + if (device->poll_backoff_us) + usleep(device->poll_backoff_us); + if ((++poll & 127u) == 0u) { + uint32_t error = 0; + + rc = ft_tt_queue_read_field(&device->runtime.noc, slot->queue, + FT_TT_QUEUE_FIELD_ERROR, &error); + device->group.wait_reads++; + device->group.wait_read_bytes += 4u; + device->group.wait_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (error != FT_TT_QUEUE_KERR_NONE) + return FT_TT_ERROR; + if (pn_tt_now_seconds() > deadline) + return FT_TT_EIO; + } + } + /* RED PROOF `PN_TT_RED=fast_check`. The POLL is left alone and the CHECK is asked + * about one descriptor further on, so the wait still ends when the card really is + * finished and only the four-agreement check is under test. Green means the check + * on this path is INERT — the same failure class as the four vacuous gates this + * backend has already found — and every arm of the ladder is meaningless. It must + * refuse, on the first flush, naming `completed_count`. */ + if (ft_tt_queue_check_completion_line(slot->queue, fields, + device->red_fast_check ? expected + 1u : expected, + &reason) != FT_TT_OK) { + /* The FULL block on the way to a failure, and it is the ONLY place the dropped + * assertion has to be recovered: a caller that reports a refusal must be able to + * name `error`, and one more read costs nothing here. */ + (void)ft_tt_queue_read_control(&device->runtime.noc, slot->queue, fields); + fprintf(stderr, "pn_tt: core %u completion refused: %s (state %s, error %s)\n", + slot->core_index, reason ? reason : "?", + ft_tt_queue_state_string(fields[FT_TT_QUEUE_FIELD_STATE]), + ft_tt_queue_kernel_error_string(fields[FT_TT_QUEUE_FIELD_ERROR])); + return FT_TT_ERROR; + } + return FT_TT_OK; + } + + for (;;) { + uint32_t value = 0; + int rc = ft_tt_queue_read_field(&device->runtime.noc, slot->queue, + FT_TT_QUEUE_FIELD_COMPLETED_COUNT, &value); + + device->group.poll_reads++; + device->group.wait_reads++; + device->group.wait_read_bytes += 4u; + device->group.wait_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (value >= expected) + break; + /* A COMPLETION POLL IS A NoC READ INTO THE POLLED CORE'S OWN L1, and that L1 is + * what its reader is filling and its writer is draining. Backing the poll off is + * therefore an experiment about CONTENTION, not about latency: if the op gets + * faster while being watched less often, the watching was costing the op. + * 0 (the default) is the tightest loop and what every recorded figure was taken + * with. */ + if (device->poll_backoff_us) + usleep(device->poll_backoff_us); + if ((++poll & 127u) == 0u) { + uint32_t error = 0; + + rc = ft_tt_queue_read_field(&device->runtime.noc, slot->queue, + FT_TT_QUEUE_FIELD_ERROR, &error); + if (rc != FT_TT_OK) + return rc; + device->group.wait_reads++; + device->group.wait_read_bytes += 4u; + device->group.wait_transactions += 1u; + if (error != FT_TT_QUEUE_KERR_NONE) + return FT_TT_ERROR; + if (pn_tt_now_seconds() > deadline) + return FT_TT_EIO; + } + } + /* Completion is four independent agreements, not a count: the state is not the host's + * poison, the count is exact, the TOKEN is this run's nonce for this descriptor, and the + * kernel reports no error. Each of them fails a different lie about work that did not + * happen (ft_tt_queue.h rule 1). + * + * THE WIDE READ, and it is what the per-member term costs. This read is paid ONCE PER + * MEMBER PER PROJECTION, so its TRANSACTION COUNT is the slope of the whole design in + * the core count. Three widths have been measured on it: + * + * 64 B through the UC/WORD32 accessor 16 transactions ~53 us (the original) + * the five-word window, WORD32 5 transactions ~21 us + * words 4..11, BLOCK16 over L1 2 transactions ~7 us (this) + * + * The bytes went UP from 20 to 32 and the cost went DOWN, which is the whole lesson of + * this link: reads are transaction-count bound, so the fix for a hot-path device read is + * to make it WIDER, not smaller. Every one of the four assertions is still inside it. */ + device->group.wait_reads++; + device->group.wait_read_bytes += 32u; + device->group.wait_transactions += 2u; + if (ft_tt_queue_read_completion_wide(&device->runtime.bulk_l1, slot->queue, fields) != + FT_TT_OK) + return FT_TT_ERROR; + if (ft_tt_queue_check_completion(slot->queue, fields, expected, &reason) != FT_TT_OK) { + /* The FULL block only on the way to a failure, where HEAD, TAIL and the idle spins + * are what a human needs and one more read costs nothing. */ + (void)ft_tt_queue_read_control(&device->runtime.noc, slot->queue, fields); + fprintf(stderr, "pn_tt: core %u completion refused: %s (state %s, error %s)\n", + slot->core_index, reason ? reason : "?", + ft_tt_queue_state_string(fields[FT_TT_QUEUE_FIELD_STATE]), + ft_tt_queue_kernel_error_string(fields[FT_TT_QUEUE_FIELD_ERROR])); + return FT_TT_ERROR; + } + return FT_TT_OK; +} + +/** + * ASK EVERY MEMBER AT ONCE, by reading the block they post into. + * + * --------------------------------------------------------------------------- + * WHY THIS IS A DIFFERENT COST AND NOT THE SAME COST REARRANGED + * --------------------------------------------------------------------------- + * Reads on this link are TRANSACTION-COUNT bound (DEV_NOTES.md §6: 8-byte loads read + * 2.3 MB/s, 16-byte loads 4.7). Asking members one at a time costs a five-word control + * read EACH — five transactions a member, ~21 us, 169 times a token. The members' slots + * are CONTIGUOUS in member 0's L1, so the same question is 8N bytes of one core, and + * through the BLOCK16 L1 accessor that is `ceil(8N / 16)` transactions instead of 5N. + * At N=8 it is 4 against 40. + * + * IT MUST NOT BE THE POLL, THOUGH. The block costs N/2 transactions and a single control + * word costs one, so a loop that read the block every iteration would be N/2 times more + * expensive than the loop it replaces. So the wait is in two stages: + * + * 1. poll ONE word — member 0's own `completed_count`. Member 0 is the straggler by + * construction: it is handed the widest panel of a ragged split, and the recorded + * per-member wait puts ~90% of the group's on it. This is the same poll, at the same + * cost, as the single-core path. + * 2. read the WHOLE slot array once, wide, and check every member. + * + * If a member is somehow still behind, stage 2 simply repeats — which is correct and rare, + * and the shape it degrades to is the one that was cheap in the first place. + * + * TWO ASSERTIONS PER MEMBER, and each fails a different lie. The SEQUENCE says how far that + * member got; the TOKEN is the nonce this run stamped on that member's own descriptor and + * says it executed THIS one. The token is compared per member against that member's OWN + * queue rather than one group-wide value, because a solo projection advances member 0's + * issue counter and nobody else's — after the first weight too narrow to split, the members + * are permanently out of step and a single expected token could never match again. + */ +static bool pn_tt_group_slots_satisfied(const pn_tt_matmul_group *group, + const ft_tt_queue_group_slot *slots) +{ + uint32_t member; + + for (member = 0; member < group->member_count; member++) { + uint32_t want = group->pending_sequence[member]; + + if (!group->pending[member]) + continue; + if (group->red_wait_early) { + /* One SHORT — and the token assertion goes with it, because the token names the + * exact descriptor and leaving it in would make the arm wait the full amount, + * which would prove nothing. */ + if (want > 1u && slots[member].sequence >= want - 1u) + continue; + } else if (slots[member].sequence >= want && + slots[member].token == group->pending_token[member]) { + continue; + } + return false; + } + return true; +} + +/** Has any member's kernel REFUSED a descriptor? A refusal posts nothing, so a sequence + * that never advances is all the block would ever show and the host would pay the whole + * timeout for something the card knew in microseconds. */ +static int pn_tt_group_check_errors(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t member; + + for (member = 0; member < group->member_count; member++) { + uint32_t error = 0; + int rc; + + if (!group->pending[member]) + continue; + rc = ft_tt_queue_read_field(&device->runtime.noc, group->member[member].queue, + FT_TT_QUEUE_FIELD_ERROR, &error); + if (rc != FT_TT_OK) + return rc; + if (error != FT_TT_QUEUE_KERR_NONE) { + fprintf(stderr, "pn_tt: core %u refused a descriptor: %s\n", + group->member[member].core_index, + ft_tt_queue_kernel_error_string(error)); + return FT_TT_ERROR; + } + } + return FT_TT_OK; +} + +static int pn_tt_group_await_all(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + const double deadline = pn_tt_now_seconds() + (double)PN_TT_OP_TIMEOUT_MS / 1000.0; + ft_tt_queue_group_slot slots[PN_TT_MATMUL_MAX_CORES]; + const uint32_t expected = group->member[0].queue->issued - + (group->red_wait_early && group->member[0].queue->issued > 0u ? 1u + : 0u); + unsigned poll = 0; + + /* Stage 1 — ONE word, member 0, exactly the single-core poll. */ + for (;;) { + uint32_t value = 0; + int rc = ft_tt_queue_read_field(&device->runtime.noc, group->member[0].queue, + FT_TT_QUEUE_FIELD_COMPLETED_COUNT, &value); + + group->poll_reads++; + group->wait_reads++; + group->wait_read_bytes += 4u; + group->wait_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (value >= expected) + break; + if (device->poll_backoff_us) + usleep(device->poll_backoff_us); + if ((++poll & 127u) == 0u) { + rc = pn_tt_group_check_errors(device); + if (rc != FT_TT_OK) + return rc; + if (pn_tt_now_seconds() > deadline) { + fprintf(stderr, "pn_tt: member 0 did not reach %u descriptors in %u ms\n", + expected, (unsigned)PN_TT_OP_TIMEOUT_MS); + return FT_TT_EIO; + } + } + } + + /* Stage 2 — the whole slot array, wide, through the BLOCK16 L1 accessor. */ + for (;;) { + int rc = ft_tt_queue_group_read_slots(&device->runtime.bulk_l1, group->member[0].core_x, + group->member[0].core_y, group->group_address, + group->member_count, slots); + + group->group_reads++; + group->wait_reads++; + /* `ceil(8N/16)` 16-byte units, which is the whole reason the block exists. */ + group->wait_read_bytes += ((uint64_t)group->member_count * 8u + 15u) & ~(uint64_t)15u; + group->wait_transactions += ((uint64_t)group->member_count * 8u + 15u) / 16u; + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not read the group completion block: %s\n", + ft_tt_error_string(rc)); + return rc; + } + if (pn_tt_group_slots_satisfied(group, slots)) + return FT_TT_OK; + rc = pn_tt_group_check_errors(device); + if (rc != FT_TT_OK) + return rc; + if (pn_tt_now_seconds() > deadline) { + fprintf(stderr, "pn_tt: the group did not reach sequence %u in %u ms\n", + group->sequence, (unsigned)PN_TT_OP_TIMEOUT_MS); + return FT_TT_EIO; + } + } +} + +/** + * Count the DRAM reads this descriptor will make the card issue, from the descriptor itself. + * + * `resident_matmul_reader.c` is three nested loops over `mt < Mt`, `nt < Nt step NB`, + * `kt < Kt step depth`, and in every one of those passes it issues ONE A read of + * `depth * 2048 B` and `columns` B reads of `depth * tile_bytes` each. So the byte totals + * are a function of the shape words, the depth and the column block, with nothing to measure + * and nothing to estimate — and the DEPTH DROPS OUT of both, because a bigger block is + * proportionally fewer of them. + * + * The kernel collapses a depth that does not divide Kt to 1, and that collapse is mirrored + * here rather than assumed away: it changes the request SIZE, which is the thing this + * accounting exists to report, even though it leaves the totals alone. + * + * A IS COUNTED Nt/NB TIMES ON PURPOSE. Its offset does not depend on `nt`, so at Mt = 1 the + * same Kt tiles are re-fetched for every PASS the member makes over its columns. At NB = 1 + * that is every column and A is 65.3% of the token; NB is the lever that divides it. + */ +static void pn_tt_group_count_reads(pn_tt_device *device, const pn_tt_weight *weight, + const uint32_t *parameters, uint32_t a_bank, + uint32_t b_bank, bool first_member) +{ + pn_tt_matmul_group *group = &device->group; + const uint64_t tile_rows = parameters[FT_TT_QUEUE_MATMUL_TILE_ROWS]; + const uint64_t tile_cols = parameters[FT_TT_QUEUE_MATMUL_TILE_COLS]; + const uint64_t tile_inner = parameters[FT_TT_QUEUE_MATMUL_TILE_INNER]; + const uint32_t asked = parameters[FT_TT_QUEUE_MATMUL_DEPTH]; + const uint64_t depth = (asked > 0u && tile_inner % asked == 0u) ? asked : 1u; + const uint32_t block_word = parameters[FT_TT_QUEUE_MATMUL_COLUMN_BLOCK]; + const uint64_t block = ftmm_column_block(block_word); + const uint64_t rows = ftmm_row_block(block_word); + uint64_t column_blocks, row_blocks, blocks, a_bytes, b_bytes; + + if (tile_rows == 0u || tile_cols == 0u || tile_inner == 0u) + return; + /* A IS COUNTED ONCE PER ROW PER COLUMN BLOCK, B ONCE PER COLUMN PER ROW BLOCK — that + * symmetry IS the lever, and it is why the two divisors go into DIFFERENT counters. The + * last block of either axis is short, which costs A nothing (the pass happens either + * way) and costs B nothing (only the real columns are read). + * + * FALSIFIABLE: `a_bytes` must NOT move with MB and `b_bytes` must NOT move with NB. An + * arm reporting a saving the knob never armed is visible as those two numbers. */ + column_blocks = (tile_cols + block - 1u) / block; + row_blocks = (tile_rows + rows - 1u) / rows; + blocks = row_blocks * column_blocks * (tile_inner / depth); + a_bytes = tile_rows * column_blocks * tile_inner * (uint64_t)PN_TT_TILE_BF16; + /* A-REUSE: the kernel's own condition (ring == Kt, MB == 1) — A once per row. */ + if ((parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR] & (1u << 18)) != 0u && rows == 1u && + pn_tt_a_reuse_tiles() == tile_inner) + a_bytes = tile_rows * tile_inner * (uint64_t)PN_TT_TILE_BF16; + b_bytes = row_blocks * tile_cols * tile_inner * (uint64_t)device->resident_tile_bytes; + + group->read_blocks += blocks; + group->a_read_bytes += a_bytes; + group->b_read_bytes += b_bytes; + { + /* The reader's own a_half test (resident_matmul_reader.c): row-0 bit, Mt 1, MB 1. */ + const uint32_t flags = parameters[FT_TT_QUEUE_MATMUL_B_N_MAJOR]; + const bool half = (flags & (1u << 10)) != 0u && tile_rows == 1u && rows == 1u; + const uint64_t wire_a = half ? a_bytes / 2u : a_bytes; + uint32_t row; + + group->wire_a_bytes += wire_a; + if (tile_rows > 1u) { + group->multi_row_a_bytes += a_bytes; + group->multi_row_b_bytes += b_bytes; + } + for (row = 0; row < group->read_census_rows; row++) + if (group->read_census[row].kt == weight->tile_rows && + group->read_census[row].nt == weight->tile_cols) + break; + if (row == group->read_census_rows && row < 48u) { + memset(&group->read_census[row], 0, sizeof(group->read_census[row])); + group->read_census[row].kt = weight->tile_rows; + group->read_census[row].nt = weight->tile_cols; + group->read_census[row].tile_bytes = device->resident_tile_bytes; + group->read_census_rows++; + } + if (row < group->read_census_rows) { + if (first_member) { + group->read_census[row].calls++; + if (tile_rows > 1u) + group->read_census[row].multi_row_calls++; + } + group->read_census[row].a_bytes += a_bytes; + group->read_census[row].wire_a_bytes += wire_a; + group->read_census[row].b_bytes += b_bytes; + } + } + if (a_bank < FT_TT_MAX_DRAM_BANKS) + group->bank_read_bytes[a_bank] += a_bytes; + if (b_bank < FT_TT_MAX_DRAM_BANKS) + group->bank_read_bytes[b_bank] += b_bytes; +} + +/** + * ISSUE one projection: N parameter blocks, N descriptors, N head bumps, and no wait. + * + * ISSUE EVERYTHING BEFORE WAITING FOR ANYTHING. The members are independent residencies, so + * a member that is waited on before the last one has been issued would have the other + * members' issue cost serialised behind its own compute. + */ +/** + * A NEW CHAIN starts with no member-0 PULL owed. Called by BOTH issue paths: a chain that + * began on the prefill GRID used to skip this, so its first decode-group projection (a solo + * `ssm_alpha` after the grid's split `qkv` and `z`) inherited the PREVIOUS chain's pull — + * 16 members up to a sequence of the last decode — and after an idle re-arm (sequences back + * at 0) member 0 waited on a sequence no member would reach: a long prompt after a pause + * aborted. The arm clears it too. RED `grid_chain_stale` skips both and must abort that shape + * (skipping only one is IDENTICAL: either reset alone closes it — measured). + */ +static void pn_tt_group_chain_begin(pn_tt_matmul_group *group, double now, bool grid) +{ + if (group->chained != 0u || (grid && pn_tt_red("grid_chain_stale"))) + return; + group->chain_pull_members = 0u; + group->chain_last_pulled = false; + group->chain_started = now; + group->chain_bytes_from = group->a_read_bytes + group->b_read_bytes; + group->chain_projections = 0u; +} + +static int pn_tt_group_issue(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output, + bool split, uint32_t members) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t parameters[FT_TT_QUEUE_PARAM_WORDS]; + double issued_from = pn_tt_now_seconds(); + + pn_tt_group_chain_begin(group, issued_from, false); + device->group.chain_projections++; + uint32_t member; + + /* 0 means "post nothing" to the kernel, so the sequence skips it on the wrap rather + * than silently disarming every member's post for one projection. */ + if (++group->sequence == 0u) + group->sequence = 1u; + + for (member = 0; member < members; member++) { + pn_tt_matmul_member *slot = &group->member[member]; + ft_tt_queue_descriptor descriptor; + uint32_t address = 0u; + uint32_t index; + int rc; + + pn_tt_group_parameters(device, weight, input, output, member, split, parameters); + + /* RED PROOF. Nt = 0 is the one malformation that is unambiguously a REFUSAL and not + * a wrong answer. Injected AFTER the host check on purpose — the point is to run the + * KERNEL's refusal on real silicon. */ + if (member == 0u && device->red_bad_descriptor != 0u && + device->matvec_count + 1u == device->red_bad_descriptor) { + fprintf(stderr, "pn_tt: RED PROOF — sending a malformed descriptor (Nt=0) as " + "matvec %llu\n", (unsigned long long)device->red_bad_descriptor); + parameters[FT_TT_QUEUE_MATMUL_TILE_COLS] = 0u; + device->red_bad_descriptor = 0u; + } + + /* The parameter slot is THIS descriptor's own index modulo the slot count, so a + * chained projection cannot overwrite the parameters of one the card has not read + * yet. It is read before the enqueue, which is what makes it that descriptor's. */ + index = slot->queue->issued; + /* THE CACHED BLOCK FIRST. A hit costs no device access at all; a miss costs the + * same 64-byte write as the rotating path and never happens again for those words. + * A 0 address means "not cached and not cacheable right now" — fall through. */ + address = 0u; + if (group->param_cache) { + bool wrote = false; + + address = pn_tt_param_cache_address(device, slot, parameters, &wrote); + if (address != 0u && wrote) { + group->issue_writes++; + group->issue_write_bytes += FT_TT_QUEUE_MATMUL_PARAM_BYTES; + group->issue_accesses += FT_TT_QUEUE_MATMUL_PARAM_BYTES / + pn_tt_issue_unit_bytes(group->issue_shape); + } + } + if (address == 0u) { + rc = ft_tt_queue_write_op_parameters(pn_tt_issue_access(&device->runtime, + group->issue_shape), + slot->queue, + index & (PN_TT_QUEUE_PARAM_SLOTS - 1u), + parameters, &address); + /* THE LEDGER, at the point of issue. Three writes per member per projection — + * 64 B of parameters, a 32 B descriptor and a 4 B head bump — and the write + * direction is BANDWIDTH bound, so it is the BYTES that convert, not the count. + * `ft_tt_queue_enqueue()` is the other two and is counted below it. */ + group->issue_writes++; + group->issue_write_bytes += FT_TT_QUEUE_MATMUL_PARAM_BYTES; + group->issue_accesses += + FT_TT_QUEUE_MATMUL_PARAM_BYTES / pn_tt_issue_unit_bytes(group->issue_shape); + if (rc != FT_TT_OK) + return rc; + } + + memset(&descriptor, 0, sizeof(descriptor)); + descriptor.opcode = FT_TT_QUEUE_OP_MATMUL; + descriptor.a0 = address; + /* a1 is the DEBUG starve count — trailing K-blocks the reader deliberately withholds + * so the compute thread is left in a real cb_wait_front. 0 for every real request. */ + descriptor.a1 = (split && members > 1u) ? group->g0_a1 : 0u; + /* a3 = M2 PULL: member 0 of a split projection waits for `members` on the card. */ + if (member == 0u && group->pull_table_ok && split && members > 1u) { + descriptor.a3 = members | (group->red_pull_skip ? 0x80000000u : 0u); + descriptor.a4 = group->sequence; + group->chain_pull_members = members; + group->chain_pull_target = group->sequence; + group->chain_last_pulled = true; + } else if (member == 0u && group->pull_table_ok && !split && + group->chain_pull_members > 1u) { + /* A SOLO projection after a split one in the same chain: member 0 still + * pulls the members of that split up to ITS sequence (v2). */ + descriptor.a3 = group->chain_pull_members | + (group->red_pull_skip ? 0x80000000u : 0u); + descriptor.a4 = group->chain_pull_target; + group->chain_last_pulled = true; + } else if (member == 0u) { + group->chain_last_pulled = false; + } + /* a2 is the WRITER'S DESTINATION ROW PITCH in tiles, and it is the weight's FULL Nt + * rather than this member's slice: at Mt > 1 the panels concatenate along columns + * INSIDE each output row, so the pitch is the whole row. **Left at 0 while Mt is 1**, + * which is every descriptor today — 0 means `tile_cols` in the kernel and a single + * tile-row's destinations are the running counter either way, so the wire bytes do + * not move and a decode `cmp` is about the loops rather than about this word. */ + descriptor.a2 = weight->tile_rows > 0u && parameters[FT_TT_QUEUE_MATMUL_TILE_ROWS] > 1u + ? weight->tile_cols + : 0u; + /* a5 is the GROUP SEQUENCE, and it is what the writer posts beside the token. */ + descriptor.a5 = group->sequence; + /* STAGE THEN PUBLISH, NOT `enqueue` — the two halves go through DIFFERENT accessors + * on the wide shape, and the head bump is on `noc` on both because a four-byte write + * cannot come through the 16-byte L1 accessor at all (pn_tt_issue.h). */ + rc = ft_tt_queue_stage(pn_tt_issue_access(&device->runtime, group->issue_shape), + slot->queue, &descriptor, 1u); + if (rc == FT_TT_OK) + rc = ft_tt_queue_publish(&device->runtime.noc, slot->queue); + /* Two writes: the ring slot, then the head bump that publishes it. The head is a + * separate write BY REQUIREMENT — it is what makes the descriptor visible, so it + * cannot be merged with the payload it announces. */ + group->issue_writes += 2u; + group->issue_write_bytes += FT_TT_QUEUE_DESCRIPTOR_BYTES + 4u; + /* THE ACCESSES, which the bytes cannot say: a 32 B ring slot is eight WORD32 accesses + * or two BLOCK16 ones, and the head bump is one word on both shapes. The parameter + * block is added at its own call site above, because a cache HIT makes none. */ + group->issue_accesses += + FT_TT_QUEUE_DESCRIPTOR_BYTES / pn_tt_issue_unit_bytes(group->issue_shape) + 1u; + group->issue_pairs++; + if (rc != FT_TT_OK) + return rc; + group->pending_sequence[member] = group->sequence; + group->pending_token[member] = ft_tt_queue_token_at(slot->queue, index); + group->pending[member] = true; + /* The bank A is REALLY read from, which is neither `input->bank` once the strip has + * mirrors nor under the spread arm. Attributing those bytes to the nominal bank + * would print a per-bank line showing one bank at several times its own ceiling, + * and the per-bank table is the ONLY evidence that the spread worked at all — a + * matmul that got faster for some other reason looks identical in every other + * counter. ONE expression, shared with `pn_tt_group_parameters`. */ + pn_tt_group_count_reads(device, weight, parameters, + pn_tt_group_activation_bank(group, input, member), + split ? weight->shard_bank[member] : weight->bank, + member == 0u); + } + + group->chain_issue_end = pn_tt_now_seconds(); + group->issue_seconds += group->chain_issue_end - issued_from; + group->projections++; + group->chained++; + device->resident_ops++; + if (split) + group->split_ops++; + else + group->solo_ops++; + return FT_TT_OK; +} + +int pn_tt_matmul_group_flush(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + double waiting_from, mark; + double census_done_await = 0.0, census_busy_await = 0.0; + uint64_t census_done_count = 0u, census_busy_count = 0u, census_polls = 0u; + uint32_t member; + int rc = FT_TT_OK; + + if (group->chained == 0u) + return FT_TT_OK; + if (group->grid_running) { + int grc = pn_tt_grid_flush(device); + + if (grc != FT_TT_OK) + return grc; + } + group->flushes++; + waiting_from = pn_tt_now_seconds(); + + if (group->pull_table_ok && group->chain_last_pulled && group->pending[0]) { + /* M2: member 0 posted only after every member reached its sequence, and every + * member runs its descriptors in order, so member 0's count covers the chain. */ + group->wait_pairs++; + rc = pn_tt_group_await(device, &group->member[0], group->member[0].queue->issued); + mark = pn_tt_now_seconds(); + group->member_wait_seconds[0] += mark - waiting_from; + group->pulled_flushes++; + census_busy_await = mark - waiting_from; + census_busy_count = 1u; + } else if (group->group_completion && group->member_count > 1u) { + rc = pn_tt_group_await_all(device); + mark = pn_tt_now_seconds(); + /* The whole wait is charged to member 0, because that is the core the host read — + * a per-member split is not observable through an aggregated poll, and inventing + * one would report a number nothing measured. */ + group->member_wait_seconds[0] += mark - waiting_from; + } else { + mark = waiting_from; + census_done_await = census_busy_await = 0.0; + census_done_count = census_busy_count = census_polls = 0u; + for (member = 0; member < group->member_count; member++) { + double member_from = mark; + const uint64_t polls_from = group->poll_reads; + + if (!group->pending[member]) + continue; + group->wait_pairs++; + rc = pn_tt_group_await(device, &group->member[member], + group->member[member].queue->issued); + mark = pn_tt_now_seconds(); + group->member_wait_seconds[member] += mark - member_from; + census_polls += group->poll_reads - polls_from; + if (group->poll_reads - polls_from <= 1u) { + census_done_await += mark - member_from; + census_done_count++; + } else { + census_busy_await += mark - member_from; + census_busy_count++; + } + if (rc != FT_TT_OK) + break; + } + mark = pn_tt_now_seconds(); + } + + group->wait_seconds += mark - waiting_from; + { + const uint64_t bytes = + group->a_read_bytes + group->b_read_bytes - group->chain_bytes_from; + uint32_t row; + + for (row = 0; row < group->flush_census_rows; row++) + if (group->flush_census[row].bytes == bytes) + break; + if (row == group->flush_census_rows && row < 32u) { + memset(&group->flush_census[row], 0, sizeof(group->flush_census[row])); + group->flush_census[row].bytes = bytes; + group->flush_census_rows++; + } + if (row < group->flush_census_rows) { + group->flush_census[row].flushes++; + group->flush_census[row].projections += group->chain_projections; + group->flush_census[row].span += mark - group->chain_started; + group->flush_census[row].wait += mark - waiting_from; + group->flush_census[row].issue += group->chain_issue_end - group->chain_started; + group->flush_census[row].gap += waiting_from - group->chain_issue_end; + group->flush_census[row].done_await += census_done_await; + group->flush_census[row].done_count += census_done_count; + group->flush_census[row].busy_await += census_busy_await; + group->flush_census[row].busy_count += census_busy_count; + group->flush_census[row].polls += census_polls; + } else { + group->flush_census_dropped++; + } + } + group->chained = 0u; + memset(group->pending, 0, sizeof(group->pending)); + if (rc == FT_TT_OK) + group->last_alive_seconds = mark; + return rc; +} + +/** + * Has the group sat idle past the shared limit with NOTHING outstanding? Asks nothing of the + * card; the retire and re-arm are the caller's (`pn_tt_resident_idle_refresh()`, which owns + * the arm). `PN_TT_RED=matmul_idle_retire_off` answers false always, for the arm that proves + * an idle group wedges without the guard. + */ +bool pn_tt_matmul_group_idle_expired(const pn_tt_device *device, double *out_idle_seconds) +{ + const pn_tt_matmul_group *group = &device->group; + const long limit_ms = pn_tt_residency_idle_retire_limit_ms(); + double idle; + + if (limit_ms == 0 || group->chained != 0u || group->last_alive_seconds <= 0.0 || + pn_tt_red("matmul_idle_retire_off") || !pn_tt_idle_retire_allowed("matmul")) + return false; + idle = pn_tt_now_seconds() - group->last_alive_seconds; + if (out_idle_seconds) + *out_idle_seconds = idle; + return idle * 1000.0 >= (double)limit_ms; +} + +/** + * KEEPALIVE (PM 5): one NOP descriptor (a0 0, a5 0 = post nothing) to every running member of + * an IDLE decode group — nothing chained, nothing pending, idle >= `min_idle_seconds` and not + * yet past the retire limit — staged and published exactly as a projection is, then each + * member awaited on its own count. The group's sequence, tokens and PULL table are untouched: + * a NOP posts nothing and the next projection's expectations come from `queue->issued`. + * @return members NOP'd, 0 if not due, -1 on a failure (the next projection then retires and + * re-arms the group as it always has) + */ +int pn_tt_matmul_group_keepalive(pn_tt_device *device, double min_idle_seconds) +{ + pn_tt_matmul_group *group = &device->group; + const double limit = (double)pn_tt_residency_idle_retire_limit_ms() / 1000.0; + double idle; + uint32_t member; + int sent = 0; + + if (!device->resident_running || group->chained != 0u || group->last_alive_seconds <= 0.0) + return 0; + for (member = 0; member < group->member_count; member++) + if (group->pending[member]) + return 0; + idle = pn_tt_now_seconds() - group->last_alive_seconds; + if (idle < min_idle_seconds || (limit > 0.0 && idle >= limit - 0.5)) + return 0; + for (member = 0; member < group->member_count; member++) { + pn_tt_matmul_member *slot = &group->member[member]; + ft_tt_queue_descriptor descriptor; + + if (!slot->running) + continue; + memset(&descriptor, 0, sizeof(descriptor)); + descriptor.opcode = FT_TT_QUEUE_OP_NOP; + if (ft_tt_queue_stage(pn_tt_issue_access(&device->runtime, group->issue_shape), + slot->queue, &descriptor, 1u) != FT_TT_OK || + ft_tt_queue_publish(&device->runtime.noc, slot->queue) != FT_TT_OK) + return -1; + sent++; + } + for (member = 0; member < group->member_count; member++) + if (group->member[member].running && + pn_tt_group_await(device, &group->member[member], + group->member[member].queue->issued) != FT_TT_OK) + return -1; + group->last_alive_seconds = pn_tt_now_seconds(); + return sent; +} + +/* =========================================================================== + * PHASE G — THE PREFILL GRID + * =========================================================================== */ + +/** Boot the grid's firmware ONCE (a process's first prefill). Sequential, like the members. */ +static int pn_tt_grid_boot(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + /* DEFAULT 48 (G option 3 ships with on-card dispatch: its decode tax is 0.1% on + * replayed tokens, run_d_ceilings.sh). 0 = the decode group serves the prefill. */ + const uint32_t count = pn_tt_env_u32("PN_TT_PREFILL_GRID", 48u); + const uint32_t first = pn_tt_env_u32("PN_TT_PREFILL_GRID_FIRST", 0u); + uint32_t index; + + if (group->grid_booted) + return 0; + if (count == 0u) + return -1; + if (count > 64u || group->member_count < 2u || count % group->member_count != 0u) { + fprintf(stderr, "pn_tt: PN_TT_PREFILL_GRID=%u must be a multiple of the %u decode " + "members, at most 64 — the grid stays off\n", + count, group->member_count); + return -1; + } + for (index = 0; index < count; index++) { + pn_tt_matmul_member *slot = &group->grid[index]; + ft_tt_noc_coord core; + + memset(slot, 0, sizeof(*slot)); + slot->queue = &slot->own_queue; + slot->core_index = first + index; + if (ft_tt_coord_tensix_by_index(&device->runtime.grid, slot->core_index, + FT_TT_COORD_TRANSLATED, &core) != FT_TT_OK) + return -1; + slot->core_x = (uint16_t)core.x; + slot->core_y = (uint16_t)core.y; + if (!pn_tt_core_boot_is_permitted(&device->runtime, slot->core_index, + "a prefill grid core", "PN_TT_PREFILL_GRID_FIRST") || + ft_tt_runtime_boot_core(&device->runtime.noc, slot->core_x, slot->core_y, + &device->firmware) != FT_TT_OK) { + fprintf(stderr, "pn_tt: prefill grid core %u could not be booted\n", + slot->core_index); + return -1; + } + slot->booted = true; + } + group->grid_count = count; + group->grid_pieces = count / group->member_count; + group->grid_stash_tiles = pn_tt_env_u32("PN_TT_PREFILL_GRID_STASH", 1152u); + group->grid_booted = true; + fprintf(stderr, "pn_tt: prefill GRID — %u cores from %u, %u column pieces per decode " + "shard, B stash %u tiles\n", + count, first, group->grid_pieces, group->grid_stash_tiles); + return 0; +} + +static void pn_tt_grid_stop(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t index; + + for (index = 0; index < group->grid_count; index++) + pn_tt_group_member_stop(device, &group->grid[index]); + group->grid_running = false; + memset(group->grid_pending, 0, sizeof(group->grid_pending)); + group->grid_detached = 0u; +} + +/** + * PHASE G option 3 — PARK THE GRID after the prefill (`PN_TT_PREFILL_GRID_PARK=1`). A grid + * left booted (firmware idle in GO-wait) costs decode ~1.1% (G2 round 2). Every RISC of every + * grid core whose residency EXITED is held in reset, and the next prefill boots the grid + * again. A core whose residency did not exit is left alone, never reset under a kernel. + * STANDING HAZARD: re-booting has wedged cores here (BRISC stalled in `device_setup()`), so + * this is opt-in until its own wedge gate (`PN_TT_PREFILL_GRID_CYCLES`) passes. + * @return how many cores were parked + */ +static uint32_t pn_tt_grid_park(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t index, parked = 0u; + + for (index = 0; index < group->grid_count; index++) { + pn_tt_matmul_member *slot = &group->grid[index]; + uint32_t pending = PN_TT_TRIAD_MASK; + + if (!slot->booted) + continue; + if (ft_tt_queue_all_processors_exited(&device->runtime.noc, slot->queue, + PN_TT_TRIAD_MASK, &pending) != FT_TT_OK) { + fprintf(stderr, "pn_tt: grid core %u has not exited (0x%x) — NOT parked\n", + slot->core_index, pending); + continue; + } + if (ft_tt_launch_assert_reset(&device->runtime.noc, slot->core_x, slot->core_y, + FT_TT_SOFT_RESET_ALL_RISCS) != FT_TT_OK) { + fprintf(stderr, "pn_tt: grid core %u could not be parked\n", slot->core_index); + continue; + } + slot->booted = false; + parked++; + } + if (parked == group->grid_count) + group->grid_booted = false; + return parked; +} + +/** The grid's wedge gate: `PN_TT_PREFILL_GRID_CYCLES=N` boot -> start -> stop -> park the + * whole grid N times before the first real prefill, and say how many cycles went clean. */ +static void pn_tt_grid_cycle_gate(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + const uint32_t cycles = pn_tt_env_u32("PN_TT_PREFILL_GRID_CYCLES", 0u); + static bool done; + uint32_t cycle, index, clean = 0u; + + if (done || cycles == 0u) + return; + done = true; + for (cycle = 0; cycle < cycles; cycle++) { + bool ok = pn_tt_grid_boot(device) == 0; + + for (index = 0; ok && index < group->grid_count; index++) { + if (pn_tt_slot_start(device, &group->grid[index], index, + device->resident_tile_bytes, device->resident_depth, + true) != FT_TT_OK) { + fprintf(stderr, "pn_tt: GRID CYCLE %u: core %u did not start\n", cycle, + group->grid[index].core_index); + ok = false; + break; + } + group->grid[index].running = true; + } + pn_tt_grid_stop(device); + if (pn_tt_grid_park(device) != group->grid_count) + ok = false; + if (!ok) { + fprintf(stderr, "pn_tt: GRID CYCLE %u FAILED — stopping the gate\n", cycle); + break; + } + clean++; + } + fprintf(stderr, "pn_tt: GRID CYCLE GATE — %u of %u boot/start/stop/park cycles clean\n", + clean, cycles); +} + +int pn_tt_matmul_grid_begin(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t index; + + if (group->grid_running || !device->resident_running) + return 0; + pn_tt_grid_cycle_gate(device); + if (pn_tt_grid_boot(device) != 0) + return 0; /* off: the decode group serves the prefill as before */ + /* MEASURE (G option 3 tax bisect): `PN_TT_GRID_BISECT=1` boot then park, no start; + * `=2` boot, start, stop, park — the grid then serves nothing (prefill as without G). */ + { + const uint32_t bisect = pn_tt_env_u32("PN_TT_GRID_BISECT", 0u); + + if (bisect == 2u) + for (index = 0; index < group->grid_count; index++) + if (pn_tt_slot_start(device, &group->grid[index], index, + device->resident_tile_bytes, device->resident_depth, + true) == FT_TT_OK) + group->grid[index].running = true; + if (bisect != 0u) { + pn_tt_grid_stop(device); + pn_tt_device_telemetry_log(device, "bisect booted"); + fprintf(stderr, "pn_tt: GRID BISECT %u — %u of %u cores parked, grid off\n", + bisect, pn_tt_grid_park(device), group->grid_count); + return 0; + } + } + for (index = 0; index < group->grid_count; index++) { + if (pn_tt_slot_start(device, &group->grid[index], index, device->resident_tile_bytes, + device->resident_depth, true) != FT_TT_OK) { + fprintf(stderr, "pn_tt: prefill grid core %u did not start — grid off for this " + "prefill\n", group->grid[index].core_index); + pn_tt_grid_stop(device); + return 0; + } + group->grid[index].running = true; + } + group->grid_running = true; + pn_tt_device_telemetry_log(device, "grid started"); + return 0; +} + +/** A grid flush failed: every grid core's control block and its five processors' status, so + * "a chained projection did not complete" names a core, an error and a place. */ +static void pn_tt_grid_failure_dump(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t index, shown = 0u; + + for (index = 0; index < group->grid_count && shown < 48u; index++) { + pn_tt_matmul_member *slot = &group->grid[index]; + uint32_t fields[FT_TT_QUEUE_FIELD_COUNT]; + uint32_t status[FT_TT_QUEUE_STATUS_WORDS]; + unsigned proc; + + if (ft_tt_queue_read_control(&device->runtime.noc, slot->queue, fields) != FT_TT_OK) + continue; + if (fields[FT_TT_QUEUE_FIELD_ERROR] == FT_TT_QUEUE_KERR_NONE && + fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] >= slot->queue->issued) + continue; + shown++; + fprintf(stderr, "pn_tt: GRID FAIL core %u (%u,%u): state %s error %s head %u tail %u " + "completed %u issued %u last %s\n", + slot->core_index, slot->core_x, slot->core_y, + ft_tt_queue_state_string(fields[FT_TT_QUEUE_FIELD_STATE]), + ft_tt_queue_kernel_error_string(fields[FT_TT_QUEUE_FIELD_ERROR]), + fields[FT_TT_QUEUE_FIELD_HEAD], fields[FT_TT_QUEUE_FIELD_TAIL], + fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT], slot->queue->issued, + ft_tt_queue_opcode_string(fields[FT_TT_QUEUE_FIELD_LAST_OPCODE])); + for (proc = 0; proc < 5u; proc++) + if (ft_tt_queue_read_status(&device->runtime.noc, slot->queue, proc, status) == + FT_TT_OK) + fprintf(stderr, "pn_tt: proc %u: %08x %08x %08x %08x %08x %08x %08x %08x\n", + proc, status[0], status[1], status[2], status[3], status[4], status[5], + status[6], status[7]); + } + fprintf(stderr, "pn_tt: GRID FAIL — %u of %u grid cores not complete or in error\n", shown, + group->grid_count); +} + +static int pn_tt_grid_flush(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + const double from = pn_tt_now_seconds(); + uint32_t index; + int rc = FT_TT_OK; + + for (index = 0; index < group->grid_count; index++) { + if (!group->grid_pending[index]) + continue; + if (rc == FT_TT_OK) + rc = pn_tt_group_await(device, &group->grid[index], + group->grid[index].queue->issued); + group->grid_pending[index] = false; + } + if (rc != FT_TT_OK) + pn_tt_grid_failure_dump(device); + group->grid_seconds += pn_tt_now_seconds() - from; + return rc; +} + +void pn_tt_matmul_grid_close(pn_tt_device *device) +{ + if (device->group.grid_running) + pn_tt_grid_stop(device); +} + +void pn_tt_matmul_grid_end(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + + if (!group->grid_running) + return; + (void)pn_tt_grid_flush(device); + pn_tt_grid_stop(device); + pn_tt_device_telemetry_log(device, "grid end (before park)"); + if (pn_tt_env_u32("PN_TT_PREFILL_GRID_PARK", 1u) != 0u) + fprintf(stderr, "pn_tt: prefill grid PARKED — %u of %u cores held in reset\n", + pn_tt_grid_park(device), group->grid_count); + fprintf(stderr, "pn_tt: prefill grid — %llu projections (%llu from the L1 stash, %llu read " + "B from DRAM), %.3f s awaiting\n", + (unsigned long long)group->grid_projections, + (unsigned long long)group->grid_stashed, + (unsigned long long)group->grid_fallbacks, group->grid_seconds); +} + +/** One multi-row projection over the grid: every decode shard cut into `grid_pieces` column + * pieces, piece p of shard m on grid core m * pieces + p. */ +static int pn_tt_grid_issue_descriptors(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, + const pn_tt_activation *output) +{ + pn_tt_matmul_group *group = &device->group; + const uint32_t kt = weight->tile_rows; + const uint32_t pieces = group->grid_pieces; + const bool red = pn_tt_red("grid_piece_shift"); + uint32_t parameters[FT_TT_QUEUE_PARAM_WORDS]; + uint32_t m, p; + + for (m = 0; m < weight->shard_count; m++) { + const uint32_t cols = weight->shard_tile_cols[m]; + uint32_t first_local = 0u; + + for (p = 0; p < pieces; p++) { + const uint32_t g = m * pieces + p; + pn_tt_matmul_member *slot = &group->grid[g]; + const uint32_t pc = cols / pieces + (p < cols % pieces ? 1u : 0u); + const uint32_t replica = input->replica_count > 1u ? g % input->replica_count : 0u; + ft_tt_queue_descriptor descriptor; + uint32_t address = 0u; + int rc; + + if (pc == 0u) + continue; + pn_tt_group_parameters(device, weight, input, output, m, true, parameters); + parameters[FT_TT_QUEUE_MATMUL_TILE_COLS] = pc; + /* RED PROOF `grid_piece_shift`: every piece reads its B one column late — must + * DIFFER (the last piece of a shard reads past its slice). */ + parameters[FT_TT_QUEUE_MATMUL_B_BASE] += + (first_local + (red ? 1u : 0u)) * kt * weight->tile_bytes; + parameters[FT_TT_QUEUE_MATMUL_C_BASE] += first_local * PN_TT_TILE_BF16; + parameters[FT_TT_QUEUE_MATMUL_A_X] = + (uint16_t)device->dram[input->replica_bank[replica]].x; + parameters[FT_TT_QUEUE_MATMUL_A_Y] = + (uint16_t)device->dram[input->replica_bank[replica]].y; + parameters[FT_TT_QUEUE_MATMUL_A_BASE] = input->replica_address[replica]; + /* DIAGNOSIS `PN_TT_GRID_TRACE=1`: grid core 0's and 23's words, every projection. */ + if ((g == 0u || g == 23u) && pn_tt_env_u32("PN_TT_GRID_TRACE", 0u) != 0u) + fprintf(stderr, "pn_tt: GRID TRACE g%u issued %u slot %u: Mt %u Kt %u Nt %u A " + "(%u,%u)+%x B (%u,%u)+%x C (%u,%u)+%x depth %u flags %x block " + "%x coop %u stash %x\n", + g, slot->queue->issued, + slot->queue->issued & (PN_TT_QUEUE_PARAM_SLOTS - 1u), parameters[0], + parameters[1], parameters[2], parameters[3], parameters[4], + parameters[5], parameters[6], parameters[7], parameters[8], + parameters[9], parameters[10], parameters[11], parameters[12], + parameters[13], parameters[15], parameters[14], + group->grid_stash_address); + rc = ft_tt_queue_write_op_parameters( + pn_tt_issue_access(&device->runtime, group->issue_shape), slot->queue, + slot->queue->issued & (PN_TT_QUEUE_PARAM_SLOTS - 1u), parameters, &address); + if (rc != FT_TT_OK) + return rc; + memset(&descriptor, 0, sizeof(descriptor)); + descriptor.opcode = FT_TT_QUEUE_OP_MATMUL; + descriptor.a0 = address; + if (group->grid_stash_address != 0u && + (uint64_t)pc * kt <= group->grid_stash_tiles) { + descriptor.a1 = 0x80000000u | (group->grid_stash_address >> 4); + group->grid_stashed++; + } else { + group->grid_fallbacks++; + } + descriptor.a2 = parameters[FT_TT_QUEUE_MATMUL_TILE_ROWS] > 1u ? weight->tile_cols + : 0u; + rc = ft_tt_queue_stage(pn_tt_issue_access(&device->runtime, group->issue_shape), + slot->queue, &descriptor, 1u); + if (rc == FT_TT_OK) + rc = ft_tt_queue_publish(&device->runtime.noc, slot->queue); + if (rc != FT_TT_OK) + return rc; + group->grid_pending[g] = true; + first_local += pc; + } + } + group->grid_projections++; + return FT_TT_OK; +} + +static int pn_tt_grid_issue(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + pn_tt_matmul_group *group = &device->group; + int rc; + + /* The grid's rings hold 8; a chain longer than 6 is flushed first. Detached projections + * (PHASE W2) count against the same ring. */ + if (group->chained + group->grid_detached >= 6u) { + rc = group->chained ? pn_tt_matmul_group_flush(device) : FT_TT_OK; + if (rc == FT_TT_OK && group->grid_detached) + rc = pn_tt_matmul_grid_detached_wait(device); + if (rc != FT_TT_OK) + return rc; + } + pn_tt_group_chain_begin(group, pn_tt_now_seconds(), true); + rc = pn_tt_grid_issue_descriptors(device, weight, input, output); + if (rc == FT_TT_OK) + group->chained++; + return rc; +} + +/* ---- PHASE W2: DETACHED grid projections --------------------------------------------------- + * A projection the prefill overlaps with the GDN chain: issued to the grid like any other but + * NOT part of the matvec chain (no `chained`, no device `pending_count`), so the chain's own + * ops, which flush the chain on entry, never wait for it. The caller polls it between chain + * steps and waits for it before anything reads its output. Same descriptors, same cores, same + * K order as the chained path: bit-identical by construction. */ + +int pn_tt_matmul_grid_issue_detached(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, + const pn_tt_activation *output) +{ + pn_tt_matmul_group *group = &device->group; + int rc; + + if (!device || !weight || !input || !output || !group->grid_running || + weight->shard_count != group->member_count || input->replica_count < 2u) + return FT_TT_EINVAL; + if (group->chained + group->grid_detached >= 6u) { + rc = group->chained ? pn_tt_matmul_group_flush(device) : FT_TT_OK; + if (rc == FT_TT_OK && group->grid_detached) + rc = pn_tt_matmul_grid_detached_wait(device); + if (rc != FT_TT_OK) + return rc; + } + rc = pn_tt_grid_issue_descriptors(device, weight, input, output); + if (rc == FT_TT_OK) + group->grid_detached++; + return rc; +} + +bool pn_tt_matmul_group_member0_completion(const pn_tt_device *device, uint16_t *out_x, + uint16_t *out_y, uint32_t *out_control, + uint32_t *out_issued) +{ + const ft_tt_queue *queue; + + if (!device || !device->resident_running || !device->group.member[0].queue) + return false; + queue = device->group.member[0].queue; + *out_x = queue->x; + *out_y = queue->y; + *out_control = queue->control_address; + *out_issued = queue->issued; + return true; +} + +bool pn_tt_matmul_grid_is_running(const pn_tt_device *device) +{ + return device && device->group.grid_running; +} + +int pn_tt_matmul_grid_detached_poll(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + uint32_t fields[FT_TT_QUEUE_FIELD_COUNT]; + uint32_t index; + + if (group->grid_detached == 0u) + return 1; + for (index = 0; index < group->grid_count; index++) { + if (!group->grid_pending[index]) + continue; + if (ft_tt_queue_read_completion_line(&device->runtime.bulk_l1, group->grid[index].queue, + fields) != FT_TT_OK) + return -1; + if (fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] < group->grid[index].queue->issued) + return 0; + } + return pn_tt_matmul_grid_detached_wait(device) == FT_TT_OK ? 1 : -1; +} + +int pn_tt_matmul_grid_detached_wait(pn_tt_device *device) +{ + pn_tt_matmul_group *group = &device->group; + int rc; + + if (group->grid_detached == 0u) + return FT_TT_OK; + rc = pn_tt_grid_flush(device); + group->grid_detached = 0u; + return rc; +} + +int pn_tt_matmul_group_enqueue(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + pn_tt_matmul_group *group = &device->group; + /* A weight the member count could not divide stayed whole in ONE bank, so it can only + * be read as one grid — by member 0. That is a real, reported cost of a group size, not + * a silent degradation. */ + bool split = weight->shard_count > 1u; + uint32_t members = split ? weight->shard_count : 1u; + uint32_t member; + + if (!device->resident_running) + return FT_TT_ERROR; + if (group->member_count < members) + return FT_TT_EINVAL; + /* PER MEMBER, because a ragged split gives them different widths and the shape the + * reader refuses is a MEMBER's Nt, not the weight's. */ + for (member = 0; member < members; member++) { + uint32_t columns = split ? weight->shard_tile_cols[member] : weight->tile_cols; + + if (!pn_tt_group_descriptor_is_executable(device, weight, columns)) + return FT_TT_EINVAL; + } + /* PHASE G: a multi-row projection of a sharded weight goes to the prefill grid. */ + /* Only for MIRRORED inputs: 48 cores reading one bank are capped at its ~60 GB/s (G0), + * slower than the decode group (`PN_TT_PREFILL_GRID_ANY=1` overrides). */ + if (group->grid_running && split && weight->shard_count == group->member_count && + pn_tt_group_tile_rows(input, output) > 1u && + (input->replica_count > 1u || pn_tt_env_u32("PN_TT_PREFILL_GRID_ANY", 0u) != 0u)) + return pn_tt_grid_issue(device, weight, input, output); + /* THE RING IS THE BOUND, not the caller's judgement: ft_tt_queue_stage() does not check + * occupancy, so a chain longer than the ring would write over descriptors the card has + * not read. Flushing here keeps every caller correct without every caller counting. */ + if (group->chained >= pn_tt_matvec_chain_limit()) { + int rc = pn_tt_matmul_group_flush(device); + + if (rc != FT_TT_OK) + return rc; + } + { + static int ab_left = -1; + + if (ab_left < 0) + ab_left = (int)pn_tt_env_u32("PN_TT_MATMUL_ROW0_AB", 0u); + if (ab_left > 0 && device->single_row) { + const uint32_t n = weight->tile_cols * 32u; + float *ref = calloc(n, sizeof(float)); + float *got = calloc(n, sizeof(float)); + double max_diff = 0.0, mean_ref = 0.0, dot = 0.0, rr = 0.0, gg = 0.0; + uint32_t i, worst = 0u; + int rc; + + ab_left--; + if (!ref || !got) { + free(ref); + free(got); + return FT_TT_ERROR; + } + rc = pn_tt_matmul_group_flush(device); + g_row0_override = 0; + if (rc == FT_TT_OK) + rc = pn_tt_group_issue(device, weight, input, output, split, members); + if (rc == FT_TT_OK) + rc = pn_tt_matmul_group_flush(device); + if (rc == FT_TT_OK && pn_tt_activation_download(device, output, ref, n) != 0) + rc = FT_TT_ERROR; + g_row0_override = 1; + if (rc == FT_TT_OK) + rc = pn_tt_group_issue(device, weight, input, output, split, members); + if (rc == FT_TT_OK) + rc = pn_tt_matmul_group_flush(device); + if (rc == FT_TT_OK && pn_tt_activation_download(device, output, got, n) != 0) + rc = FT_TT_ERROR; + /* A SECOND row-0 run of the same projection, and the per-member split of the + * first one: is a wrong answer the FIRST row-0 descriptor a member ever runs? */ + { + float *again = calloc(n, sizeof(float)); + double second = 0.0; + uint32_t m, col = 0u; + + if (again && rc == FT_TT_OK && + pn_tt_group_issue(device, weight, input, output, split, members) == FT_TT_OK && + pn_tt_matmul_group_flush(device) == FT_TT_OK && + pn_tt_activation_download(device, output, again, n) == 0) { + for (i = 0; i < n; i++) + if (fabs((double)again[i] - ref[i]) > second) + second = fabs((double)again[i] - ref[i]); + fprintf(stderr, "pn_tt: ROW0 A/B — second row-0 run max |diff| %.6g; first " + "run per member:", second); + for (m = 0; m < members; m++) { + const uint32_t cols = split ? weight->shard_tile_cols[m] : weight->tile_cols; + double md = 0.0; + + for (i = col * 32u; i < (col + cols) * 32u && i < n; i++) + if (fabs((double)got[i] - ref[i]) > md) + md = fabs((double)got[i] - ref[i]); + fprintf(stderr, " m%u %.3g", m, md); + col += cols; + } + fprintf(stderr, "\npn_tt: ROW0 A/B — first run per tile (max |diff|, member 0 and 1):"); + for (i = 0; i < 32u && i * 32u < n; i++) { + double td = 0.0; + uint32_t e; + + for (e = i * 32u; e < (i + 1u) * 32u; e++) + if (fabs((double)got[e] - ref[e]) > td) + td = fabs((double)got[e] - ref[e]); + fprintf(stderr, " %.2g", td); + } + fprintf(stderr, "\npn_tt: ROW0 A/B — tile 1 cols: "); + for (i = 32u; i < 64u && i < n; i++) + fprintf(stderr, " %.3g/%.3g", (double)ref[i], (double)got[i]); + fprintf(stderr, "\n"); + } + free(again); + } + g_row0_override = -1; + for (i = 0; rc == FT_TT_OK && i < n; i++) { + const double d = fabs((double)got[i] - (double)ref[i]); + + if (d > max_diff) { + max_diff = d; + worst = i; + } + mean_ref += fabs((double)ref[i]); + dot += (double)got[i] * ref[i]; + rr += (double)ref[i] * ref[i]; + gg += (double)got[i] * got[i]; + } + fprintf(stderr, + "pn_tt: ROW0 A/B — Kt %u Nt %u split %d: max |diff| %.6g at %u (ref %.6g " + "got %.6g), mean |ref| %.6g, got.ref/ref.ref %.6f, |got|/|ref| %.6f; ref[0..3] " + "%.5g %.5g %.5g %.5g got[0..3] %.5g %.5g %.5g %.5g; ref[16..17] %.5g %.5g " + "got[16..17] %.5g %.5g\n", + weight->tile_rows, weight->tile_cols, split ? 1 : 0, max_diff, worst, + (double)ref[worst], (double)got[worst], n ? mean_ref / n : 0.0, + rr > 0 ? dot / rr : 0.0, rr > 0 ? sqrt(gg / rr) : 0.0, (double)ref[0], + (double)ref[1], (double)ref[2], (double)ref[3], (double)got[0], + (double)got[1], (double)got[2], (double)got[3], n > 17 ? (double)ref[16] : 0.0, + n > 17 ? (double)ref[17] : 0.0, n > 17 ? (double)got[16] : 0.0, + n > 17 ? (double)got[17] : 0.0); + free(ref); + free(got); + return rc; + } + } + return pn_tt_group_issue(device, weight, input, output, split, members); +} + +int pn_tt_matmul_group_g0(pn_tt_device *device, const pn_tt_weight *weight) +{ + pn_tt_matmul_group *group = &device->group; + const uint32_t mode = pn_tt_env_u32("PN_TT_G0", 0u); + const uint32_t mt = pn_tt_env_u32("PN_TT_G0_ROWS", 51u); + const uint32_t kt = weight->tile_rows; + const uint32_t nt = weight->tile_cols; + const size_t a_bytes = (size_t)mt * kt * PN_TT_TILE_BF16; + const size_t c_bytes = (size_t)mt * nt * PN_TT_TILE_BF16; + ft_tt_buffer a_buf, c_buf; + pn_tt_activation in, out; + uint16_t *host = NULL; + uint64_t fnv = 1469598103934665603ull; + uint32_t state = 12345u, row, member; + double started, elapsed; + size_t index; + int rc = -1; + + if (mode == 0u || mt == 0u) + return 0; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, 3u, a_bytes, &a_buf) != + FT_TT_OK || + ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, 4u, c_bytes, &c_buf) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: G0 — DRAM allocation failed\n"); + return -1; + } + host = malloc(a_bytes > c_bytes ? a_bytes : c_bytes); + if (!host) + return -1; + /* Deterministic bf16 in [-1, 1): exponent 126 or below, random mantissa and sign. */ + for (index = 0; index < a_bytes / 2u; index++) { + state = state * 1664525u + 1013904223u; + host[index] = (uint16_t)(((state >> 16) & 0x807Fu) | (0x3C00u + ((state >> 8) & 0x0380u))); + } + memset(&in, 0, sizeof(in)); + memset(&out, 0, sizeof(out)); + in.bank = 3u; + in.address = (uint32_t)a_buf.address; + in.tile_count = kt; + in.replica_count = 1u; + in.replica_bank[0] = in.bank; + in.replica_address[0] = in.address; + out.bank = 4u; + out.address = (uint32_t)c_buf.address; + out.tile_count = nt; + out.replica_count = 1u; + out.replica_bank[0] = out.bank; + out.replica_address[0] = out.address; + if (pn_tt_write_dram(device, in.bank, in.address, host, a_bytes) != 0 || + pn_tt_zero_dram(device, out.bank, out.address, c_bytes) != 0) + goto done; + /* A MIRRORED over `PN_TT_G0_REPLICAS` banks (default 8): 48 members on one bank's ~59 GB/s + * is what bound the first G0 round. Member m reads mirror m % R (the decode mirrors' rule). */ + { + const uint32_t replicas = pn_tt_env_u32("PN_TT_G0_REPLICAS", 8u); + uint32_t r; + + for (r = 1u; r < replicas && r < PN_TT_ACTIVATION_MAX_REPLICAS; r++) { + ft_tt_buffer mirror; + const uint32_t bank = (3u + r) % device->dram_bank_count; + + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, a_bytes, + &mirror) != FT_TT_OK || + pn_tt_write_dram(device, bank, (uint32_t)mirror.address, host, a_bytes) != 0) + break; + in.replica_bank[r] = bank; + in.replica_address[r] = (uint32_t)mirror.address; + in.replica_count = r + 1u; + } + } + group->g0_a1 = 0u; + if (mode >= 3u) { + uint32_t widest = 0u; + + for (member = 0; member < group->member_count; member++) + if (weight->shard_tile_cols[member] > widest) + widest = weight->shard_tile_cols[member]; + if (g_stash_address == 0u || + (uint64_t)widest * kt > (uint64_t)pn_tt_env_u32("PN_TT_G0_STASH_TILES", 0u)) { + fprintf(stderr, "pn_tt: G0 — no stash big enough (%u x %u tiles)\n", widest, kt); + goto done; + } + group->g0_a1 = 0x80000000u | (g_stash_address >> 4) | (mode == 4u ? 0x40000000u : 0u); + } + started = pn_tt_now_seconds(); + if (mode == 1u) { + for (row = 0; row < mt; row += 2u) { + pn_tt_activation pin = in, pout = out; + const uint32_t span = mt - row < 2u ? mt - row : 2u; + + { + uint32_t r; + + pin.address += row * kt * PN_TT_TILE_BF16; + for (r = 0; r < pin.replica_count; r++) + pin.replica_address[r] += row * kt * PN_TT_TILE_BF16; + } + pin.tile_rows = pin.active_rows = span; + pout.address += row * nt * PN_TT_TILE_BF16; + pout.replica_address[0] = pout.address; + pout.tile_rows = pout.active_rows = span; + if (pn_tt_matmul_group_dispatch(device, weight, &pin, &pout) != FT_TT_OK) + goto done; + } + } else { + in.tile_rows = in.active_rows = mt; + out.tile_rows = out.active_rows = mt; + if (pn_tt_matmul_group_dispatch(device, weight, &in, &out) != FT_TT_OK) + goto done; + } + elapsed = pn_tt_now_seconds() - started; + group->g0_a1 = 0u; + if (pn_tt_read_dram(device, out.bank, out.address, host, c_bytes) != 0) + goto done; + for (index = 0; index < c_bytes; index++) { + fnv ^= ((const unsigned char *)host)[index]; + fnv *= 1099511628211ull; + } + fprintf(stderr, "pn_tt: G0 mode %u — Mt %u Kt %u Nt %u over %u members: %.3f ms, %.2f " + "TFLOP/s, output fnv %016llx\n", + mode, mt, kt, nt, group->member_count, elapsed * 1e3, + 2.0 * mt * 32.0 * kt * 32.0 * nt * 32.0 / elapsed / 1e12, (unsigned long long)fnv); + rc = 0; +done: + group->g0_a1 = 0u; + free(host); + return rc; +} + +int pn_tt_matmul_group_dispatch(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + int rc = pn_tt_matmul_group_enqueue(device, weight, input, output); + + if (rc != FT_TT_OK) + return rc; + return pn_tt_matmul_group_flush(device); +} + +bool pn_tt_matmul_group_can_fall_back(const pn_tt_device *device, const pn_tt_weight *weight) +{ + (void)device; + /* A sharded weight is physically in several banks; there is no single grid for a + * one-shot launch to read, so the one-shot escape does not exist for it. */ + return weight->shard_count <= 1u; +} + +void pn_tt_matmul_group_report(const pn_tt_device *device) +{ + const pn_tt_matmul_group *group = &device->group; + uint32_t member; + + if (group->member_count == 1u) + return; + fprintf(stderr, "pn_tt: matmul group — %u members on cores", group->member_count); + for (member = 0; member < group->member_count; member++) + fprintf(stderr, " %u", group->member[member].core_index); + fprintf(stderr, "; %u weights sharded over %u banks (%u of them RAGGED — widths differ " + "by one), %u left whole (Nt below the member count); weight-door NoC row " + "collisions worst %u, activation door rotation %s (row collisions %u); " + "completion %s; card-side post %s\n", + group->sharded_weights, group->member_count, group->ragged_weights, + group->unsharded_weights, group->worst_weight_row_collisions, + group->activation_doors ? "ON" : "OFF", group->activation_row_collisions, + group->group_completion ? "AGGREGATED on member 0" : "per member", + group->group_completion ? "to member 0" + : group->post_probe == PN_TT_GROUP_POST_PROBE_SELF ? "PROBE self" + : group->post_probe == PN_TT_GROUP_POST_PROBE_REMOTE ? "PROBE remote" + : "disarmed"); +} + +void pn_tt_matmul_group_op_counts(const pn_tt_device *device, uint64_t *out_split, + uint64_t *out_solo) +{ + if (out_split) + *out_split = device ? device->group.split_ops : 0u; + if (out_solo) + *out_solo = device ? device->group.solo_ops : 0u; +} + +/** + * THE LINK LEDGER: the per-member host term PREDICTED from counted accesses, beside the + * fitted one. + * + * `wall(N) = 0.3885 + 0.00242*N + card(N)` fits every measured member count, but a fit + * names a SEAM. This names the MECHANISM, from quantities counted at the point of issue and + * the link's own two rates (DEV_NOTES.md §6): + * + * - the WRITE direction is bandwidth bound, ~53 MB/s through the UC path; + * - the READ direction is transaction-count bound, ~3.4 us per access of 16 bytes or 4. + * + * The per-member term is then `issue_bytes/N/53e6 + wait_transactions/N * 3.4e-6` per token, + * and it either reproduces 2.42 ms or one of the two is wrong. **That disagreement is the + * useful outcome**, which is why both numbers are printed rather than one. + * + * `PN_TT_LINK_WRITE_MBPS` and `PN_TT_LINK_READ_US` exist so a session that re-measures the + * link (a Gen1 retrain moves the first by 2x) can re-quote this without a rebuild. + */ +static void pn_tt_matmul_group_link_report(const pn_tt_device *device) +{ + const pn_tt_matmul_group *group = &device->group; + const double write_mbps = (double)pn_tt_env_u32("PN_TT_LINK_WRITE_MBPS", 53u); + const double read_us = (double)pn_tt_env_u32("PN_TT_LINK_READ_NS", 3400u) / 1000.0; + double issue_seconds, wait_seconds; + + if (group->flushes == 0u || group->member_count == 0u) + return; + issue_seconds = (double)group->issue_write_bytes / (write_mbps * 1e6); + wait_seconds = (double)group->wait_transactions * read_us * 1e-6; + fprintf(stderr, + "llama_tt: link ledger — %llu projections, %llu flushes, %u members. " + "ISSUE %llu writes over %llu issued pairs / %llu B (%.2f B and %.2f writes a " + "pair) on the %s shape, %llu PCIe accesses (%.2f a pair). " + "WAIT %llu reads / %llu transactions over %llu awaited pairs (%.2f a " + "pair).\n", + (unsigned long long)group->projections, (unsigned long long)group->flushes, + group->member_count, (unsigned long long)group->issue_writes, + (unsigned long long)group->issue_pairs, + (unsigned long long)group->issue_write_bytes, + group->issue_pairs + ? (double)group->issue_write_bytes / (double)group->issue_pairs + : 0.0, + group->issue_pairs ? (double)group->issue_writes / (double)group->issue_pairs + : 0.0, + pn_tt_issue_shape_string(group->issue_shape), + (unsigned long long)group->issue_accesses, + group->issue_pairs ? (double)group->issue_accesses / (double)group->issue_pairs + : 0.0, + (unsigned long long)group->wait_reads, + (unsigned long long)group->wait_transactions, + (unsigned long long)group->wait_pairs, + group->wait_pairs ? (double)group->wait_transactions / (double)group->wait_pairs + : 0.0); + if (group->param_cache) { + uint64_t hits = 0u, misses = 0u, full = 0u; + uint32_t used = 0u, member; + + for (member = 0; member < group->member_count; member++) { + hits += group->member[member].cache_hits; + misses += group->member[member].cache_misses; + full += group->member[member].cache_full; + if (group->member[member].cache_used > used) + used = group->member[member].cache_used; + } + fprintf(stderr, + "llama_tt: parameter cache — %llu hits, %llu misses, %llu fell back (region " + "full); %u of %u blocks used on the deepest member. A hit is a 64-byte host " + "write NOT made.\n", + (unsigned long long)hits, (unsigned long long)misses, + (unsigned long long)full, used, FT_TT_QUEUE_PARAM_CACHE_SLOTS); + } + fprintf(stderr, + "llama_tt: link ledger — PREDICTED host cost at %.0f MB/s write and %.1f us a " + "read: issue %.4f s, wait %.4f s, total %.4f s. MEASURED issue %.4f s, wait " + "%.4f s. A gap here is the prediction or the counters, not both.\n", + write_mbps, read_us, issue_seconds, wait_seconds, issue_seconds + wait_seconds, + group->issue_seconds, group->wait_seconds); + /* THE RATE THE RUN ITSELF MEASURED, which is the only one that cannot be stale. The + * bandwidth model above predicted 0.150 s of issue against 0.928 measured at 8 members + * — a 6x miss — because the write direction is TRANSACTION bound like the read one and + * not bandwidth bound at all. Printing per-access seconds for both directions makes + * that visible in one line instead of being derived by hand from two. */ + fprintf(stderr, + "llama_tt: link ledger — MEASURED per access: issue %.2f us a write, %.2f us an " + "issued pair; wait %.2f us a transaction, %.2f us an awaited pair.\n", + group->issue_writes + ? group->issue_seconds * 1e6 / (double)group->issue_writes : 0.0, + group->issue_pairs + ? group->issue_seconds * 1e6 / (double)group->issue_pairs : 0.0, + group->wait_transactions + ? group->wait_seconds * 1e6 / (double)group->wait_transactions : 0.0, + group->wait_pairs + ? group->wait_seconds * 1e6 / (double)group->wait_pairs : 0.0); +} + +void pn_tt_matmul_group_time_report(const pn_tt_device *device) +{ + const pn_tt_matmul_group *group; + uint64_t ops; + uint32_t member; + + if (!device) + return; + group = &device->group; + ops = group->split_ops + group->solo_ops; + if (ops == 0u) + return; + fprintf(stderr, "llama_tt: dispatch split — issue %.3f s (%.1f us/op), wait %.3f s " + "(%.1f us/op), %llu poll reads, %llu wide group reads (%.2f/op)\n", + group->issue_seconds, group->issue_seconds * 1e6 / (double)ops, + group->wait_seconds, group->wait_seconds * 1e6 / (double)ops, + (unsigned long long)group->poll_reads, (unsigned long long)group->group_reads, + (double)group->group_reads / (double)ops); + pn_tt_matmul_group_link_report(device); + if (group->member_count == 1u) + return; + fprintf(stderr, "llama_tt: per-member wait —"); + for (member = 0; member < group->member_count; member++) + fprintf(stderr, " core %u %.3f s", group->member[member].core_index, + group->member_wait_seconds[member]); + fprintf(stderr, " (members run concurrently and are awaited in order, so a balanced " + "group puts nearly all of it on the first)\n"); +} + +void pn_tt_matmul_group_read_report(const pn_tt_device *device, double tokens) +{ + const pn_tt_matmul_group *group; + uint64_t total; + double seconds; + uint32_t bank; + + if (!device || tokens <= 0.0) + return; + group = &device->group; + total = group->a_read_bytes + group->b_read_bytes; + if (total == 0u) + return; + /* WAIT, not the whole dispatch phase: it is the only interval in which the card is the + * thing running. ISSUE overlaps card work by construction, so charging the reads to + * issue+wait would understate the rate by exactly the overlap. */ + seconds = group->wait_seconds > 0.0 ? group->wait_seconds : 1e-9; + fprintf(stderr, + "llama_tt: matmul DRAM reads — A %.3f + B %.3f = %.3f GB/token (A is %.1f%%) over " + "%.0f A-blocks/token; %.1f GB/s aggregate in %.4f s/token of WAIT, %u member(s); " + "blocks MBxNB %ux%u of %u DST tiles; requests A %u B / B %u B, %u outstanding " + "then a barrier\n", + (double)group->a_read_bytes / 1e9 / tokens, + (double)group->b_read_bytes / 1e9 / tokens, (double)total / 1e9 / tokens, + 100.0 * (double)group->a_read_bytes / (double)total, + (double)group->read_blocks / tokens, (double)total / seconds / 1e9, + group->wait_seconds / tokens, group->member_count, group->row_block, + group->column_block, group->dst_section_tiles, + /* The A REQUEST does not grow with MB — a row block is MB separate reads of + * `depth` contiguous tiles, one per row, so only the count moves. */ + device->resident_depth * PN_TT_TILE_BF16, + device->resident_depth * device->resident_tile_bytes, + group->row_block + group->column_block); + if (pn_tt_env_u32("PN_TT_READ_CENSUS", 0u) != 0u) { + uint64_t weight_sum = 0u; + uint32_t row; + + fprintf(stderr, + "llama_tt: read census — booked A %.3f, wire A %.3f, B %.3f GB/token; of which " + "multi-row (Mt>1) A %.3f B %.3f GB/token\n", + (double)group->a_read_bytes / 1e9 / tokens, + (double)group->wire_a_bytes / 1e9 / tokens, + (double)group->b_read_bytes / 1e9 / tokens, + (double)group->multi_row_a_bytes / 1e9 / tokens, + (double)group->multi_row_b_bytes / 1e9 / tokens); + for (row = 0; row < group->read_census_rows; row++) { + const uint64_t weight_bytes = (uint64_t)group->read_census[row].kt * + group->read_census[row].nt * + group->read_census[row].tile_bytes; + + weight_sum += weight_bytes; + fprintf(stderr, + "llama_tt: read census Kt %4u Nt %5u — %.2f calls/token (%llu multi-row), " + "weight %.2f MB x calls = %.1f MB; B %.1f MB, wire A %.1f MB (booked " + "%.1f) per token\n", + group->read_census[row].kt, group->read_census[row].nt, + (double)group->read_census[row].calls / tokens, + (unsigned long long)group->read_census[row].multi_row_calls, + (double)weight_bytes / 1e6, + (double)weight_bytes * (double)group->read_census[row].calls / tokens / 1e6, + (double)group->read_census[row].b_bytes / tokens / 1e6, + (double)group->read_census[row].wire_a_bytes / tokens / 1e6, + (double)group->read_census[row].a_bytes / tokens / 1e6); + } + fprintf(stderr, "llama_tt: read census — distinct weight shapes sum %.3f GB (one " + "of each shape)\n", (double)weight_sum / 1e9); + fprintf(stderr, "llama_tt: param census — fallback differs from a cached block in " + "ONE word:"); + for (row = 0; row < 18u; row++) + if (group->param_diff_word[row]) + fprintf(stderr, " w%u=%.1f/token", row, + (double)group->param_diff_word[row] / tokens); + fprintf(stderr, " (w17 = no single-word match)\n"); + for (row = 0; row < group->param_census_rows; row++) + fprintf(stderr, "llama_tt: param census Kt %4u member-Nt %5u — %llu misses " + "(blocks cached), %.2f fallbacks/token\n", + group->param_census[row].kt, group->param_census[row].nt, + (unsigned long long)group->param_census[row].misses, + (double)group->param_census[row].fallbacks / tokens); + } + fprintf(stderr, "llama_tt: matmul per-bank reads —"); + for (bank = 0; bank < device->dram_bank_count && bank < FT_TT_MAX_DRAM_BANKS; bank++) + fprintf(stderr, " b%u %.1f", bank, + (double)group->bank_read_bytes[bank] / seconds / 1e9); + fprintf(stderr, " GB/s (one bank's measured ceiling is 59.2-59.5 GB/s, the card's 510.8)\n"); + + /* M1 CARD-BUSY CLOCK, read from each member's writer (control words 12-15, + * resident_matmul_writer.c): busy = descriptor pickup to completion post, idle = the gaps + * between. The FRACTION needs no clock rate; ms/token assumes the 1350 MHz AICLK. */ + { + uint32_t m; + double busy_max = 0.0, busy_sum = 0.0; + + fprintf(stderr, "llama_tt: matmul card-busy —"); + /* M8: member 0's pull wait (writer word 61), so its busy can be read as work. */ + if (group->member_count > 0u && group->member[0].queue) { + uint32_t pull = 0u; + + if (device->runtime.noc.read(device->runtime.noc.user, group->member[0].queue->x, + group->member[0].queue->y, + group->member[0].queue->control_address + 61u * 4u, + &pull, sizeof(pull)) == FT_TT_OK) + fprintf(stderr, " [m0 pull wait %.2fms]", (double)pull * 16.0 / 1.35e9 * 1e3 / + tokens); + } + for (m = 0; m < group->member_count; m++) { + uint32_t busy = 0u, idle = 0u, count = 0u; + + if (!group->member[m].queue || + ft_tt_queue_read_field(&device->runtime.noc, group->member[m].queue, + (ft_tt_queue_control_field)13, &busy) != FT_TT_OK || + ft_tt_queue_read_field(&device->runtime.noc, group->member[m].queue, + (ft_tt_queue_control_field)14, &idle) != FT_TT_OK || + ft_tt_queue_read_field(&device->runtime.noc, group->member[m].queue, + (ft_tt_queue_control_field)15, &count) != FT_TT_OK) + continue; + { + const double busy_s = (double)busy * 16.0 / 1.35e9; + const double frac = (busy + idle) ? (double)busy / ((double)busy + idle) : 0.0; + + if (busy_s > busy_max) + busy_max = busy_s; + busy_sum += busy_s; + fprintf(stderr, " m%u %.1f%% %.2fms %.0fus/d", m, 100.0 * frac, + busy_s * 1e3 / tokens, count ? busy_s * 1e6 / count : 0.0); + } + } + fprintf(stderr, " | busiest %.2f ms/token, mean %.2f (since the last arm; 1350 MHz)\n", + busy_max * 1e3 / tokens, + group->member_count ? busy_sum * 1e3 / tokens / group->member_count : 0.0); + } + + /* THE FLUSH CENSUS and its fit, span = fixed + bytes / rate, weighted by flushes. */ + { + double sw = 0.0, sx = 0.0, sy = 0.0, sxx = 0.0, sxy = 0.0, span_total = 0.0; + uint32_t row; + + for (row = 0; row < group->flush_census_rows; row++) { + const double n = (double)group->flush_census[row].flushes; + const double x = (double)group->flush_census[row].bytes; + const double y = group->flush_census[row].span / (n > 0.0 ? n : 1.0); + + fprintf(stderr, + "llama_tt: matmul flush census — %8.3f MB x %6.2f/token (%.2f proj each): " + "span %7.1f us, wait %7.1f us, %6.1f GB/s over span\n", + x / 1e6, n / tokens, + (double)group->flush_census[row].projections / (n > 0.0 ? n : 1.0), + y * 1e6, group->flush_census[row].wait / (n > 0.0 ? n : 1.0) * 1e6, + y > 0.0 ? x / y / 1e9 : 0.0); + { + const pn_tt_matmul_group *gg = group; + const double nn = n > 0.0 ? n : 1.0; + + fprintf(stderr, + "llama_tt: split — issue %.1f us, gap %.1f us, await %.1f us = " + "busy %.1f us (%.2f members) + done-on-arrival %.1f us (%.2f members, " + "%.2f us each), %.1f polls\n", + gg->flush_census[row].issue / nn * 1e6, + gg->flush_census[row].gap / nn * 1e6, + gg->flush_census[row].wait / nn * 1e6, + gg->flush_census[row].busy_await / nn * 1e6, + (double)gg->flush_census[row].busy_count / nn, + gg->flush_census[row].done_await / nn * 1e6, + (double)gg->flush_census[row].done_count / nn, + gg->flush_census[row].done_count + ? gg->flush_census[row].done_await / + (double)gg->flush_census[row].done_count * 1e6 + : 0.0, + (double)gg->flush_census[row].polls / nn); + } + sw += n; sx += n * x; sy += n * y; sxx += n * x * x; sxy += n * x * y; + span_total += group->flush_census[row].span; + } + if (group->flush_census_rows >= 2u && sw * sxx - sx * sx > 0.0) { + const double slope = (sw * sxy - sx * sy) / (sw * sxx - sx * sx); + const double fixed = (sy - slope * sx) / sw; + + fprintf(stderr, + "llama_tt: matmul flush FIT — fixed %.1f us/flush x %.1f flushes/token = " + "%.4f s/token, streaming %.1f GB/s; span %.4f s/token; %llu dropped\n", + fixed * 1e6, sw / tokens, fixed * sw / tokens, + slope > 0.0 ? 1.0 / slope / 1e9 : 0.0, span_total / tokens, + (unsigned long long)group->flush_census_dropped); + } + } +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_matmul_group.h b/src/accel/tt/pn_tt_matmul_group.h new file mode 100644 index 0000000..1d97976 --- /dev/null +++ b/src/accel/tt/pn_tt_matmul_group.h @@ -0,0 +1,722 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_matmul_group.h + * @brief The GROUP of Tensix cores one projection is split across, and the load-time bank + * shard that makes the split worth having. + * + * --------------------------------------------------------------------------- + * WHAT THIS IS, AND WHAT IT IS NOT + * --------------------------------------------------------------------------- + * Until now `pn_tt_matvec` drove ONE core: 169 projections a token, one after another, on + * Tensix 71. This spreads each projection's OUTPUT COLUMNS over N cores — the SPLIT arm of + * foundry/tt/ft_tt_op_matmul_cooperative.h — and keeps the resident work queue, so the + * per-op cost stays a parameter block and a 4-byte head bump per member rather than five + * kernel images per member. + * + * **It is SPLIT, not COOPERATIVE, and the reason is structural rather than a preference.** + * COOPERATIVE multicasts the shared A block from a leader, and that rendezvous lives in + * `cooperative_matmul_reader.c`, a ONE-SHOT program. The resident matmul triad this backend + * dispatches through (`rtriad_bfp8`) has no multicast, no rectangle and no per-block + * barrier. Running the cooperative reader instead would put the per-op kernel-image upload + * back — 36.2 µs/kB x 5,384 B = 218 µs, per member, per op, i.e. 147 ms a token at N=4 + * against a 231 ms token. So the multicast is left for a resident cooperative triad, and + * what is wired here is the arm that needs no new kernel at all. + * + * --------------------------------------------------------------------------- + * WHY NO KERNEL CHANGED + * --------------------------------------------------------------------------- + * The artifact stores every matmul weight N-MAJOR (tile index `nt * Kt + kt`), and the + * resident reader indexes it exactly that way (`resident_matmul_reader.c:157`). So member + * c's column slice is an ALREADY CONTIGUOUS RUN OF BYTES, and "member c computes columns + * [c*cc, (c+1)*cc)" is expressed by handing that member + * + * B_BASE = its own panel's base Nt = cc C_BASE = output + c*cc*2048 + * + * in the descriptor words that already exist. The writer streams `Mt*Nt` tiles from + * `C_BASE` (`resident_matmul_writer.c:63-74`), so at Mt = 1 the N panels CONCATENATE into + * exactly the single-core output row, tile for tile and byte for byte. That is what makes + * the correctness gate a byte comparison against N = 1 rather than a correlation. + * + * --------------------------------------------------------------------------- + * THE MEASURED CONSTRAINTS THIS RESPECTS + * --------------------------------------------------------------------------- + * (plan Phase 6.7 / foundry/tt/ft_tt_dram_stripe.h — these are design inputs, not trivia) + * + * ENDPOINT ~45 GB/s, NoC ROW ~60, BANK ~64, CARD 502. `ft_tt_coord_dram_bank()` is port 0 + * of three, so N members left on one operand's own coordinate share ONE door. + * -> the weight is SHARDED over N banks at load, and each member's DOOR into its bank + * comes from `ft_tt_dram_stripe_choose_ports()`, never from `port = c % 3`, which + * collides the moment the bank list holds a b and a b+4. + * -> the activation is one strip in one bank; its N readers get N DOORS of that bank + * from the same chooser, which is what separates ~45 GB/s from ~64. + * + * OUTSTANDING READS ARE PER ENDPOINT AND MUST BE BALANCED. The chooser is greedy in + * reader order and differs by at most one stream between doors; `busiest`/`quietest` are + * reported at load so an unbalanced map is visible rather than inferred. + * + * THE 128 KiB ADDRESS PERIOD. A shard moves a panel's BASE, never its stride: the reader + * still walks `(nt * Kt + kt) * tile_bytes` inside one member's panel, so the stride is + * `Kt * 1088` exactly as it is today and no shard can manufacture a 128 KiB row stride + * that was not already there. + * + * NEVER MORE THAN 4 DESTINATIONS PER MULTICAST. Nothing here multicasts. + * + * --------------------------------------------------------------------------- + * WHAT A MEMBER COUNT COSTS WHEN IT DOES NOT DIVIDE + * --------------------------------------------------------------------------- + * `Nt % N != 0` used to be REFUSED for that tensor, which cost it its split entirely — and + * that refusal was COOPERATIVE's, whose reason is the per-block multicast rendezvous. + * **SPLIT has no rendezvous**, so it now takes a RAGGED split: the remainder goes one column + * each to the first members, widths differ by at most one, and `shard_column_first` is the + * running sum. MEASURED 2026-08-16: under the old rule N=5 left 3,864 of 3,887 projections + * on one core and N=7 left ALL of them, which read as a scaling ceiling for a whole round. + * + * A weight NARROWER than the member count still stays whole — a member with zero columns is + * a descriptor the card refuses, and a refusal there strands a triad. It is counted + * (`solo_ops`, `unsharded_weights`) and printed, because a run that quietly left a quarter + * of its projections on one core would report a disappointing speedup and no reason. + * + * @status Wired into pn_tt_matvec_dispatch() and pn_tt_matvec_enqueue(). The cooperative + * multicast path (PN_TT_MATMUL_COOPERATIVE) is byte-exact at N=4 and N=8, saves 1.06-1.14x + * of card time at prefetch depth 16, and is OFF by default because it moves the member + * cores onto a rectangle and the N=8 teardown wedge is unresolved. + * PN_TT_MATMUL_CORES=1 is byte-for-byte the single-core path and is the default; N = 1..8, + * 12 and 16 are byte-identical to it on silicon. + * @issues The card-side completion post wedges the POSTING core at its next boot, so + * `PN_TT_GROUP_COMPLETION` is off and gated behind an acknowledgement. Kernel work. + * @todo None + */ + +#ifndef PN_TT_MATMUL_GROUP_H +#define PN_TT_MATMUL_GROUP_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_column_runs.h" +#include "pn_tt_device.h" +#include "pn_tt_issue.h" + +#include "tt/ft_tt_dram_stripe.h" +#include "tt/ft_tt_fttw.h" +#include "tt/ft_tt_launch.h" +#include "tt/ft_tt_op_matmul_resident_multicast.h" +#include "tt/ft_tt_queue.h" + +/** One member of the group: a core, its resident work queue, and the launch state that + * staged it. Member 0's queue is the device's own, so this struct holds a POINTER rather + * than the queue — one residency has one owner and it is still pn_tt_device.c. */ +typedef struct pn_tt_matmul_member { + uint32_t core_index; + uint16_t core_x, core_y; + ft_tt_queue *queue; /**< member 0: &device->queue; the rest: &own_queue */ + ft_tt_queue own_queue; /**< storage for members 1..N-1 */ + ft_tt_launch_config config; + unsigned char cb_buffer[FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4]; + bool booted; /**< firmware is up on this core */ + bool running; /**< a residency is live on it RIGHT NOW */ + + /** + * THE PARAMETER CACHE, host side: the sixteen words of every block already written into + * this member's `FT_TT_QUEUE_PARAM_CACHE` region, in slot order. + * + * Heap, allocated at arm and freed at retire, because 320 blocks x 64 B x 32 members is + * 640 KiB and this struct is embedded in `pn_tt_device`. + * + * `cache_slot[h]` is an open-addressed index into it, so a lookup is one FNV-1a over 64 + * bytes plus ONE memcmp — not a scan. The memcmp is not an optimisation, it is the + * correctness claim: a hit means the block at that address holds exactly these words, + * and a hash collision therefore costs a miss and never a wrong descriptor. + */ + uint32_t (*cache_words)[FT_TT_QUEUE_PARAM_WORDS]; + uint16_t *cache_slot; /**< PN_TT_PARAM_CACHE_BUCKETS entries; 0xFFFF = empty */ + uint32_t cache_used; /**< blocks written so far; never decreases within a residency */ + uint64_t cache_hits; /**< projections that reused a block, i.e. writes NOT made */ + uint64_t cache_misses; /**< projections that had to write one */ + uint64_t cache_full; /**< projections that fell back to the rotating slots */ +} pn_tt_matmul_member; + +/** The group, and what the striping rules said about the map it produced. */ +typedef struct pn_tt_matmul_group { + /** 1 = today's single-core path, and every branch here is then a no-op. */ + uint32_t member_count; + pn_tt_matmul_member member[PN_TT_MATMUL_MAX_CORES]; + + /** + * The DOOR each member reads an activation strip through, per bank the strip may live + * in. Precomputed because the strip's bank moves per call (the on-card forward + * round-robins strips across banks) while the assignment for a given bank does not. + */ + ft_tt_dram_endpoint activation_door[FT_TT_MAX_DRAM_BANKS][PN_TT_MATMUL_MAX_CORES]; + uint32_t activation_bank_count; + bool activation_doors; /**< PN_TT_ACTIVATION_DOORS; the control arm, default on */ + + /** Reported, never refused: pairs of doors in one plan that share a NoC row, worst over + * the weights, and the balance the chooser achieved on the widest shard map. */ + uint32_t worst_weight_row_collisions; + uint32_t activation_row_collisions; + uint32_t sharded_weights; /**< weights split over member_count banks */ + uint32_t unsharded_weights; /**< weights too narrow to give every member a column */ + uint32_t ragged_weights; /**< sharded weights whose members are not all one width */ + + uint64_t split_ops; /**< projections dispatched across every member */ + uint64_t solo_ops; /**< projections that ran on member 0 alone */ + + /* ------------------------------------------------------------------ + * THE SHARED ACTIVATION, MULTICAST INSTEAD OF RE-READ + * ------------------------------------------------------------------ + * At Mt = 1 every member re-reads the whole A column once per output column, so A is + * 1.76 GB of the 2.70 GB a token reads — 65% — and all of it comes from the ONE bank + * holding that strip. That is why four cores buy 3.44x of card time and sixteen buy + * 4.80x, and why door rotation alone is worth 1.27x at N=8: a gain that GROWS with N + * is what an endpoint ceiling looks like. With cooperation the LEADER reads A once and + * multicasts it as ceil(receivers / 4) packets. + * + * It is OPT-IN and it MOVES THE CORES, which is the reason it is not simply a better + * default. A multicast writes every node of its bounding box, and the default member + * set {71, 75, 76, 77} is an arithmetic run whose box also holds 72, 73 and 74 — the + * reduce, eltwise and sample residencies. So a cooperating group is placed as a + * RECTANGLE from `device->core_index`, and that placement has to be chosen rather than + * inherited. + */ + /** PN_TT_MATMUL_COOPERATIVE. Off by default. */ + bool cooperative_requested; + /** PN_TT_MATMUL_RECTANGLE, or implied by the above. Placing the members as a rectangle + * WITHOUT cooperating is the control arm: it moves the cores exactly where a + * cooperating group puts them and leaves the multicast as the only variable. */ + bool rectangle_requested; + /** RED PROOF, `PN_TT_RED_COOP=stale`: the leader skips the payload multicast. See the + * .c for the two claims one arm settles. */ + bool red_coop_stale; + /** The members are a rectangle and the broadcast partition built. Independent of + * whether the WEIGHTS allow it, which is only known once they are all placed. */ + bool cooperative_placed; + /** ARMED: placed, more than one member, and every weight splits evenly. Cooperation is + * ALL-OR-NOTHING for a residency — a group that cooperates on some descriptors and + * not others drifts its members' CB_IN0 write pointers apart, and every later + * multicast then lands in the wrong half of the receivers' buffers with every kernel + * completing and every status word clean. */ + bool cooperative; + /** Columns of the member rectangle; rows are member_count / columns. */ + uint32_t cooperative_columns; + /** Weights whose Nt does not divide by the member count. Any at all disarms + * cooperation for the whole residency, and the count is what says why. */ + uint32_t uneven_weights; + /** The roles, the geometry and the three rendezvous addresses. Valid only while + * `cooperative_placed`. */ + ft_tt_op_matmul_resident_multicast multicast; + + /* ------------------------------------------------------------------ + * COMPLETION THE CARD AGGREGATES, so the host polls ONE core, not N + * ------------------------------------------------------------------ + * Every member's WRITER already posts {sequence, token} into its own slot of a block + * on member 0 (ft_tt_queue.h, the group completion block) — the mechanism shipped with + * the resident triad and nothing here used it. Asking each member separately cost a + * five-word control read PER MEMBER PER PROJECTION, ~21 us, i.e. 21N us an op and + * 426 ms a token at N = 120. The slots are CONTIGUOUS in one core's L1, so the same + * question costs `ceil(8N / 16)` device accesses of ONE core instead: reads on this + * link are transaction-count bound, and that is the difference between a term linear + * in the member count and one that is nearly flat in it. + * + * **AND IT IS OFF, because it costs a core.** The post leaves the POSTING core + * unbootable at its next boot — measured twice, 2026-08-16; the .c has the evidence and + * the acknowledgement that arms it. What actually brought the per-member term down was + * the WIDTH of the read, not moving where the question is asked. + */ + /** Monotone, stamped into descriptor word a5. Never 0 — 0 means "post nothing". */ + uint32_t sequence; + /** The sequence and token each member was last given, and whether it owes anything. */ + uint32_t pending_sequence[PN_TT_MATMUL_MAX_CORES]; + uint32_t pending_token[PN_TT_MATMUL_MAX_CORES]; + bool pending[PN_TT_MATMUL_MAX_CORES]; + /** Projections enqueued since the last flush. Bounded by the ring, not by taste. */ + uint32_t chained; + /** **OFF unless asked for by its full spelling** — the post wedges the POSTING core at + * its next boot (see the .c). Off, the host polls every member's own control block, + * which is 2 transactions each through the BLOCK16 L1 accessor. */ + bool group_completion; + /** L1 address of the block on member 0, or 0 before it is written. */ + uint32_t group_address; + + /** + * THE DESTINATION ARM. `PN_TT_GROUP_POST_PROBE`, default OFF. + * + * The two recorded wedges took a REMOTE poster and left the LOCAL one (member 0, + * posting into its own L1 under identical code) PASS both times. That is 2 of 2 and it + * makes "remote destination" the discriminator the data supports — but the two arms + * that produced it also differed in which member they were, so it is evidence and not + * proof. This separates the destination from everything else: the SAME + * `ftq_group_post()` on the SAME processor of the SAME members, with only the NoC + * coordinates it writes to changing. + * + * `self` points every member's post at its OWN coordinates; `remote` points every + * member's post at member 0, which is the recorded configuration. Both leave the HOST + * on the ordinary per-member completion wait, so both arms are byte-exact runs and + * `cmp` means something on each — the probe is about the card-side write, and the host + * read direction cannot wedge a Tensix. + * + * NOT the same object as `group_completion`: that one changes what the HOST reads and + * needs every member's post to land in one block. This changes only where the card + * writes, and is mutually exclusive with it for that reason. + * + * **RUN 2026-08-20: `self` WEDGED cores 71 and 75 of 71/75/76/77, after a disarmed + * control on the same four probed 4 PASS.** Core 71 posts locally under both arms, so + * the configuration it survived twice is the one that broke it — the wedge does NOT + * follow the remote destination, and aggregating local to the writer is not a way out. + * See the .c. + */ + enum { + PN_TT_GROUP_POST_PROBE_OFF = 0, + PN_TT_GROUP_POST_PROBE_SELF, + PN_TT_GROUP_POST_PROBE_REMOTE + } post_probe; + /** RED PROOF, `PN_TT_RED_GROUP=early`: stop waiting one descriptor short. See the .c. */ + bool red_wait_early; + + /** + * WHERE THE DISPATCH PHASE ACTUALLY GOES, split at the only seam that separates the + * host from the card: `issue` is N parameter blocks, N descriptors and N head bumps, + * and nothing in it waits for the card; `wait` is everything after the last head bump. + * + * `member_wait` is the same clock charged to whichever member was still running when + * its turn to be awaited came. Members are awaited in index order and run + * concurrently, so a balanced group puts nearly all of it on member 0 and nearly none + * on the rest — and a STRAGGLER is the one whose share is not nearly none. That is the + * only way to tell "the split is uneven" from "every core is slower". + */ + double issue_seconds; + double wait_seconds; + + /* FLUSH CENSUS (M1): one row per distinct byte total a flush covers. `span` runs from the + * chain's FIRST issue to its completion, so it holds the card's whole work for the chain + * whatever the issue overlapped; `wait` is the host's await alone. Fit across rows: + * span = fixed + bytes / rate separates a per-flush fixed cost from the streaming rate. */ + double chain_started; + double chain_issue_end; + uint64_t chain_bytes_from; + uint32_t chain_projections; + uint32_t flush_census_rows; + struct { + uint64_t bytes; + uint64_t flushes; + uint64_t projections; + double span; + double wait; + double issue; /**< first issue -> last issue of the chain (the STAGGER) */ + double gap; /**< last issue -> flush entered (host work in between) */ + double done_await; /**< awaits that found the member ALREADY done (one poll) */ + uint64_t done_count; + double busy_await; /**< awaits that had to poll more than once */ + uint64_t busy_count; + uint64_t polls; + } flush_census[32]; + uint64_t flush_census_dropped; + double member_wait_seconds[PN_TT_MATMUL_MAX_CORES]; + uint64_t poll_reads; /**< 4-byte control reads the completion polls spent */ + uint64_t group_reads; /**< wide reads of the slot array — `ceil(8N/16)` accesses each */ + + /** + * THE TRANSACTION LEDGER — what the per-member host term is actually MADE OF. + * + * `wall(N) = 0.3885 + 0.00242*N + card(N)` fits all eight member counts, but a FITTED + * coefficient cannot say which device accesses it is. These counters can: every host + * access this file makes is counted here at the point of issue, split by phase and by + * direction, with its byte count beside it. The link's own rates then convert them — + * writes are BANDWIDTH bound (~53 MB/s through the UC path) and reads are + * TRANSACTION-COUNT bound (~3.4 us each, 16 bytes or 4, DEV_NOTES.md §6) — so + * + * per-member host seconds = issue_bytes / 53e6 + wait_transactions * 3.4e-6 + * + * is a PREDICTION of the fitted coefficient from counted quantities, and the two either + * agree or one of them is wrong. That is the instrument this round is missing: without + * it, "host ISSUE is 2.42 ms/token per member" names a seam and not a mechanism. + * + * `wait_transactions` counts 16-byte units for the BLOCK16 accessor and one per 4-byte + * word for the UC one, because that is what the link charges for. + */ + uint64_t issue_writes; /**< host->card writes in pn_tt_group_issue() */ + uint64_t issue_write_bytes; /**< and their bytes */ + uint64_t wait_reads; /**< card->host reads in the flush path */ + uint64_t wait_read_bytes; /**< and their bytes */ + uint64_t wait_transactions; /**< what the LINK charges for those reads */ + uint64_t flushes; /**< how many times the chain was drained */ + uint64_t projections; /**< how many projections were issued */ + /** THE DENOMINATOR, and it is NOT `projections * member_count`. A weight whose `Nt` the + * member count could not divide stays whole in one bank and is issued to member 0 + * ALONE — 48 of Ornith's 249 — so the nominal product over-counts the pairs that + * actually cost a device access by ~20% and every per-member figure derived from it is + * low by the same factor. These count the pairs that happened. */ + uint64_t issue_pairs; /**< (projection, member) pairs actually issued */ + uint64_t wait_pairs; /**< (flush, member) pairs actually awaited */ + + /** THE ISSUE IN PCIe ACCESSES, which is what `wait_transactions` already is for the read + * direction. `issue_write_bytes` cannot answer this: 64 bytes is SIXTEEN accesses through + * the UC/WORD32 accessor and FOUR through the BLOCK16 one, and a cache HIT writes no + * bytes at all and still costs the ring slot and the head bump. */ + uint64_t issue_accesses; + + /** Which accessor this group's payload writes go through — `PN_TT_ISSUE`, decided ONCE + * when a member's queue is armed. See pn_tt_issue.h for the WC/UC store-ordering seam. */ + pn_tt_issue_shape issue_shape; + + /** **THE 64-BYTE PARAMETER WRITE, MADE ONCE INSTEAD OF EVERY PROJECTION**, + * `PN_TT_PARAM_CACHE`, **DEFAULT ON since 2026-08-20**. The sixteen words of a + * projection are a function of `(weight, input strip, output strip, member)` and all + * four are constants of a decode run, so the same block is written ~24 times a token. + * Cached, ISSUE falls from three accesses per member per projection to two — and it is + * the COUNT that pays, not the bytes: **`b` 0.00242 -> 0.00199, 1.21x, not the 2.4x + * the bandwidth model predicted.** See the .c. */ + bool param_cache; + + /** **ONE TRANSACTION PER MEMBER PER FLUSH instead of three**, `PN_TT_FAST_COMPLETION`, + * **DEFAULT ON since 2026-08-20**. The poll and the completion check become the same + * 16-byte read of the control block's second line — which carries `completed_count`, + * `completed_token` and `state` but NOT `error`. Worth **6.6-7.2 us per awaited member + * per flush, flat from 2 to 32 members**, i.e. matmul card work 0.0611 -> 0.0475 at the + * shipped 8. See the .c for what it drops, why that is recoverable, and why the WC + * tight loop is safe. */ + bool fast_completion; + /** M2 PULL COMPLETION: member 0's writer NoC-reads every member's done-sequence before + * posting, so a flush whose chain ENDED in a split projection awaits member 0 alone. + * `PN_TT_MATMUL_PULL`; `pull_table_ok` is set only once the table read back exact. */ + bool pull; + bool pull_table_ok; + bool red_pull_skip; + bool chain_last_pulled; + uint32_t chain_pull_members; + uint32_t chain_pull_target; + uint64_t pulled_flushes; + /** PHASE G0: the descriptor word a1 every issue carries (0 = the shipped path; bit 31 = + * B stash at L1 address (a1 & 0xFFFFFF) << 4, bit 30 = RED skip the stash load) and the + * stash CB's placed address (`PN_TT_G0_STASH_TILES` > 0 at arm). */ + uint32_t g0_a1; + uint32_t stash_address; + + /* ------------------------------------------------------------------ + * PHASE G — the PREFILL GRID: `PN_TT_PREFILL_GRID` resident matmul triads (default 0 = + * off) on cores `PN_TT_PREFILL_GRID_FIRST`.., booted once, started at a prefill's first + * multi-row projection and retired at its end. Each decode member's weight shard is cut + * into `grid_pieces` column pieces, one per grid core, read ONCE into that core's L1 + * stash when it fits (`grid_stash_tiles`); A comes from the strip's mirrors. + * ------------------------------------------------------------------ */ + pn_tt_matmul_member grid[64]; + uint32_t grid_count; + uint32_t grid_pieces; + uint32_t grid_stash_tiles; + uint32_t grid_stash_address; + uint32_t grid_tile_bytes, grid_depth; + bool grid_booted; + bool grid_running; + bool grid_pending[64]; + uint32_t grid_detached; /**< PHASE W2: detached projections outstanding on the grid */ + uint64_t grid_projections, grid_stashed, grid_fallbacks; + double grid_seconds; + + /* ------------------------------------------------------------------ + * THE DRAM READS THE CARD ACTUALLY ISSUES, counted from the descriptors + * ------------------------------------------------------------------ + * Not an estimate. `resident_matmul_reader.c` walks `Mt * ceil(Nt/NB) * (Kt / depth)` + * passes and issues exactly ONE A read of `depth * 2048 B` and `columns` B reads of + * `depth * tile_bytes` in each, so these totals are arithmetic over the shape words this + * file writes into the descriptor. They are kept because the recorded "29.4 GB/s + * aggregate" was computed from the WEIGHT bytes alone, and the weight is the SMALLER half. + * + * A IS RE-READ ONCE PER PASS OVER THE COLUMNS. At Mt = 1 the A offset is `(mt*Kt + kt)`, + * which does not depend on `nt` — so at NB = 1 the same Kt tiles are fetched again for + * every one of the member's Nt columns and A's bytes are `2048/1088 = 1.882x` the + * weight's, whatever the shape, all of it out of the ONE bank the activation strip lives + * in. `column_block` below is the divisor. + * + * FALSIFIABLE, and that is what makes it worth printing: `b_read_bytes` per token must + * reproduce the artifact's own matmul weight bytes, which the load path reports + * independently, AND IT MUST NOT MOVE WITH `column_block` — blocking `nt` changes when B + * is read, never how much. If either fails this counter is wrong and nothing derived + * from it holds. + */ + uint64_t a_read_bytes; + uint64_t b_read_bytes; + uint64_t read_blocks; + uint64_t bank_read_bytes[FT_TT_MAX_DRAM_BANKS]; + /** + * BYTES CENSUS (printed under `PN_TT_READ_CENSUS=1`; counting is free and always on). + * `a_read_bytes` books every A tile at 2048 B; `wire_a_bytes` is what the reader really + * fetches (a row-0 half tile is 1024 B). Multi-row descriptors (Mt > 1: the batched + * prefill) are kept apart, because the report divides by DECODE forwards. Per weight + * shape (Kt, whole Nt): projections issued, bytes booked, and the weight's own bytes. + */ + uint64_t wire_a_bytes; + uint64_t multi_row_a_bytes; + uint64_t multi_row_b_bytes; + struct { + uint32_t kt, nt, tile_bytes; + uint64_t calls, multi_row_calls, a_bytes, wire_a_bytes, b_bytes; + } read_census[48]; + uint32_t read_census_rows; + /** PARAM-CACHE CENSUS (printed with the read census): per weight shape (Kt, member Nt), + * misses (a block written into the cache) and fallbacks (cache full, rotating slot). */ + struct { + uint32_t kt, nt; + uint64_t misses, fallbacks; + } param_census[48]; + uint32_t param_census_rows; + /** `PN_TT_PARAM_CENSUS_DIFF=1`: on a fallback, the one word in which it differs from a + * cached block (17 = differs in more than one word / nothing close). */ + uint64_t param_diff_word[18]; + + /** Output columns the reader supplies per pass, `PN_TT_MATMUL_BLOCK`, default 1 (off). + * A PER-RESIDENCY CONSTANT: CB_IN1's chunk is `depth * column_block` pages and the + * kernel's grid guard refuses a chunk size that CHANGES through a buffer, so the same + * value goes into every descriptor and sizes every CB. Capped at 4, which is the DST + * section at `dst_accum_mode 1`. It divides A's 65.3% of the token's DRAM reads by + * itself and leaves B's bytes and B's request size untouched. */ + uint32_t column_block; + + /** Output ROWS the reader supplies per pass, `PN_TT_MATMUL_ROWS`, default 1 (off), and + * the mirror of `column_block` in every respect. It divides B's bytes — the WEIGHT + * column is swept once for `row_block` output rows instead of once per row — and leaves + * A's alone, exactly as `column_block` does the other way round. + * + * THE TWO SHARE THE DST SECTION AND THAT IS THE CEILING: `row_block * column_block <= 4` + * at `dst_accum_mode 1` (kernels/resident_matmul_block_protocol.h carries the upstream + * assert it comes from). + * + * **AND ON A Q8_0 MODEL IT IS WORTH TWO PER CENT.** The traffic is + * `Mt*T*(a/NB + b/MB)`: NB divides the operand whose tile is the bf16 ACTIVATION + * (2048 B) and MB divides the one whose tile is the WEIGHT (1088 B at Bfp8_b). So the + * two blocks are symmetric in TILES and not in BYTES, and the budget belongs to NB. At + * Bfp8_b the lattice reads 1600 / 1568 / 2320 bytes a unit for (1,4) / (2,2) / (4,1) — + * the shipped setting is already within 2% of the best, and at Bfp4_b it IS the best. + * MB=4/NB=1 serves 128 prompt positions a weight sweep and reads **45% MORE**, because + * the A side grows by more than the B side shrank. + * + * The law was validated against silicon with no fitted constant: it predicts + * `A/B = a/(4b) = 0.4706` at the shipped shape, and Ornith measures 0.4718. + * + * **THE LEVER THAT WOULD MATTER IS A BIGGER DST** — `dst_section_tiles` below. At + * `MB*NB <= 8` the Bfp8_b optimum is MB=2/NB=4 at 1056 bytes a unit (**1.52x**), but + * that point needs `Mt >= 2` and NO DESCRIPTOR IN THIS TREE HAS ONE, so what is + * reachable there is MB=1/NB=8 at 1344 bytes (**1.19x**). + * + * L1 is nowhere near binding: at depth 8 the CBs are `MB*depth*2*2048 + + * depth*NB*2*tile + 4096` of a 1,351,680 B arena, which admits MB near 39. + * + * IT ONLY BITES AT `Mt > 1`, which is a batch of more than 32 prompt positions. At the + * Mt = 1 every descriptor in the tree carries today it changes the CB sizing and the + * chunk the reader pushes and NOTHING ELSE — which is what makes `PN_TT_MATMUL_ROWS=2` + * a `cmp`-identity arm for the new loops rather than a speed arm. `tile_rows` is + * written from a LITERAL 1 in `pn_tt_group_write_parameters()` and in the request + * builder, on the decode path and the batched-prefill path alike, so `Mt >= 2` is a + * host-shape change and not a matmul one. */ + uint32_t row_block; + + /** The DST section the ARMED KERNEL was built for, in 32x32 tiles: 4 at + * `DstSync::SyncHalf` (the embedded blob) and 8 at `SyncFull` + * (kernels/resident_matmul_dst_sync.h). It is the ceiling on `row_block * + * column_block`. + * + * IT IS A PROPERTY OF THE KERNEL AND THE HOST CANNOT SEE IT. One Pantheon binary drives + * whichever matmul images are resolved, and a section the host thinks is bigger than + * the kernel's is not a refusal — DST has no bounds check, so the pass writes over the + * half the packer is reading and every kernel retires clean. That is why 8 is refused + * unless a program-directory override (`PN_TT_PROGRAM_DIR`) is in force: the embedded + * blob's matmul is SyncHalf by construction, so the only way an 8-tile kernel can be + * running is off a directory the caller named. */ + uint32_t dst_section_tiles; + + /** RED PROOF, `PN_TT_RED=matmul_a_spread`: member c reads its A block through a door of + * bank `(activation bank + c) % banks` instead of the activation's own. The BYTES, the + * request size and the block count are untouched and only the SOURCE BANK moves, so it + * isolates "one bank cannot supply A" from every other candidate for the N=4 knee. + * **It reads the wrong bytes and the answer is garbage** — a timing arm, nothing else. */ + bool red_a_spread; + /** RED PROOF, `PN_TT_RED=matmul_a_offset`: same bank, addresses 4 MiB apart. The PAIR + * separates a per-channel ceiling from N members hammering ONE 16 KiB window, which + * `matmul_a_spread` alone cannot because it moves both at once. Also garbage. */ + bool red_a_offset; + + /** THE IDLE RETIRE, as `pn_tt_residency` has it (pn_tt_residency.c). The group keeps its + * own queues, so the residency layer's guard never saw it: a serve idle past a member's + * idle bound sent its first projection into a half-exited triad. Set at arm and after + * every completed flush; checked before an enqueue with nothing outstanding. */ + double last_alive_seconds; + uint64_t idle_retires; +} pn_tt_matmul_group; + +/** Which named way `PN_TT_RED_SPLIT` breaks member 1. See the .c for what each proves. */ +typedef enum pn_tt_red_split { + PN_TT_RED_SPLIT_NONE = 0, + PN_TT_RED_SPLIT_BASE, + PN_TT_RED_SPLIT_OUT, + PN_TT_RED_SPLIT_NARROW +} pn_tt_red_split; + +/** `PN_TT_RED_SPLIT`'s value, or NONE when unset or unrecognised (which it says). */ +pn_tt_red_split pn_tt_matmul_group_red_mode(void); + +/** + * Read `PN_TT_MATMUL_CORES`, resolve members 1..N-1, and boot their firmware. + * + * Member 0 is whatever core pn_tt_bring_up_core() already claimed, so a one-member group + * touches no core the pre-group tree did not. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_matmul_group_configure(pn_tt_device *device); + +/** Arm a resident triad on every member, or retire the ones that came up and fail. */ +int pn_tt_matmul_group_arm(pn_tt_device *device, uint32_t weight_tile_bytes, uint32_t depth); + +/** Retire every member's residency by ASKING. Never parks and never resets. */ +void pn_tt_matmul_group_retire(pn_tt_device *device); + +/** PM 5 keepalive: NOP an idle decode group's members (see pn_tt_matmul_group.c). */ +int pn_tt_matmul_group_keepalive(pn_tt_device *device, double min_idle_seconds); + +/** Idle past `PN_TT_RESIDENCY_IDLE_RETIRE_MS` with nothing outstanding? See the .c. */ +bool pn_tt_matmul_group_idle_expired(const pn_tt_device *device, double *out_idle_seconds); + +/** + * Place one weight in DRAM: sharded over `member_count` banks when the member count + * divides its `tile_cols`, whole in one bank otherwise. + * + * @param entry the artifact directory entry, already known MATMUL_NMAJOR + * @param bank_cursor advanced by one bank per tensor, so no bank is every tensor's first + * @param weight shape fields (`tile_rows`, `tile_cols`, `tile_bytes`) filled by the + * caller; every placement field is written here + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_matmul_group_place_weight(pn_tt_device *device, const ft_tt_artifact_entry *entry, + uint32_t *bank_cursor, pn_tt_weight *weight); + +/** + * The same, over a PERMUTATION of the source tensor's output columns. + * + * This is how a joint projection is de-interleaved: `runs` names, in destination order, the + * runs of source column tiles the new weight is built from, so the file is read once per run + * per shard and nothing is permuted at run time. `runs` NULL with `run_count` 0 is the + * identity and is byte for byte what `pn_tt_matmul_group_place_weight()` does — the two share + * this body precisely so an ordinary weight cannot take a different path from a permuted one. + * + * REFUSES a run list that leaves the source or uses a source column twice, because + * downstream it is only a byte offset: the failure mode is a weight with one column + * duplicated and another missing, which is a smoothly wrong projection nothing else can see. + * + * @param runs destination-ordered runs of SOURCE column tiles, or NULL for identity + * @param run_count how many + * @param weight `tile_rows`, `tile_bytes` and the DESTINATION `tile_cols` filled by the + * caller; every placement field is written here + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_matmul_group_place_weight_columns(pn_tt_device *device, + const ft_tt_artifact_entry *entry, + const pn_tt_column_run *runs, uint32_t run_count, + uint32_t *bank_cursor, pn_tt_weight *weight); + +/** One projection across the group, issued AND awaited. @return FT_TT_OK, or a foundry + * error code. Exactly `pn_tt_matmul_group_enqueue()` followed by + * `pn_tt_matmul_group_flush()`, and the only caller that wants both is one whose next + * read is of this projection's own output. */ +int pn_tt_matmul_group_dispatch(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output); + +/** + * ISSUE one projection and DO NOT WAIT for it. + * + * The resident queue was built so the host enqueues and bumps a counter — *"do not fuse ops + * to save dispatch, there is no dispatch to save"* (ft_tt_queue.h) — and the forward then + * waited on all 169 of them anyway. Within a layer **q/k/v are independent** and **gate/up + * are independent**, so their descriptors can sit in the ring together: the card runs them + * back to back at its own 0.39-0.45 us op-to-op instead of one host round trip apiece, and + * the host's issue of the second is overlapped with the card's execution of the first. + * + * The caller owes a `pn_tt_matmul_group_flush()` before it reads ANY of the outputs, and + * before any op on another core consumes them. Chained descriptors must be independent: + * the members run concurrently, so a second projection reading the first's output would + * race even though one core's own queue is ordered. + * + * @return FT_TT_OK, or a foundry error code + */ +int pn_tt_matmul_group_enqueue(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, const pn_tt_activation *output); + +/** Wait for every projection enqueued since the last flush. A no-op when nothing is + * pending, so a caller may flush defensively. @return FT_TT_OK, or a foundry error code. */ +/** PHASE G0 MEASURE (`PN_TT_G0`): one projection over `PN_TT_G0_ROWS` (default 51) tile-rows + * of synthetic activation — 1 chunked (2 rows a pass), 2 full M, 3 full M + B stash, + * 4 full M + RED stash — timed, with an FNV-1a of the output for cross-arm comparison. */ +int pn_tt_matmul_group_g0(pn_tt_device *device, const pn_tt_weight *weight); + +/** PHASE G: start the prefill grid (`PN_TT_PREFILL_GRID` cores; 0 = off, a no-op) for a + * prefill, and retire it after. Multi-row projections of sharded weights then run on it. */ +int pn_tt_matmul_grid_begin(pn_tt_device *device); +void pn_tt_matmul_grid_end(pn_tt_device *device); +void pn_tt_matmul_grid_close(pn_tt_device *device); + +/** PHASE W2: issue a grid projection OUTSIDE the matvec chain (the chain's flushes never wait + * for it). Needs the grid running, a sharded weight and a mirrored input. */ +int pn_tt_matmul_grid_issue_detached(pn_tt_device *device, const pn_tt_weight *weight, + const pn_tt_activation *input, + const pn_tt_activation *output); +/** One non-blocking look: 1 every detached projection is done (and checked), 0 still running, + * -1 an error. */ +int pn_tt_matmul_grid_detached_poll(pn_tt_device *device); +/** Wait for every detached projection. FT_TT_OK or an error code. */ +int pn_tt_matmul_grid_detached_wait(pn_tt_device *device); +/** PHASE W wavefront: where the decode group's member 0 counts EXECUTED descriptors, and how + * many have been issued to it — a reader gate for a solo projection (Nt below the member + * count runs on member 0 alone). */ +bool pn_tt_matmul_group_member0_completion(const pn_tt_device *device, uint16_t *out_x, + uint16_t *out_y, uint32_t *out_control, + uint32_t *out_issued); +/** Is the prefill grid running (a detached issue would be accepted)? */ +bool pn_tt_matmul_grid_is_running(const pn_tt_device *device); + +int pn_tt_matmul_group_flush(pn_tt_device *device); + +/** Whether the one-shot escape can read this weight at all — false once it is sharded. */ +bool pn_tt_matmul_group_can_fall_back(const pn_tt_device *device, const pn_tt_weight *weight); + +/** One line about the map the striping rules produced. Silent for a one-member group. */ +void pn_tt_matmul_group_report(const pn_tt_device *device); + +/** Projections that went across the group, and those that ran on member 0 alone. */ +void pn_tt_matmul_group_op_counts(const pn_tt_device *device, uint64_t *out_split, + uint64_t *out_solo); + +/** One line splitting the dispatch phase into host issue, card wait and per-member wait. + * Silent for a one-member group only in the per-member half; the split itself is printed + * either way, because "is the host issuing or is the card computing" is a question the + * single-core path never answered either. */ +void pn_tt_matmul_group_time_report(const pn_tt_device *device); + +/** + * Two lines saying what the matmul reads from DRAM, and from which bank. + * + * The bytes are counted from the descriptors rather than measured, because the reader's + * loop makes them exact (see `a_read_bytes`). The point of printing them is that the + * recorded "29.4 GB/s aggregate" divided the WEIGHT bytes by the card time, and the weight + * is 35% of what the card reads — the activation, re-read once per output column and served + * by ONE bank, is the other 65%. + * + * @param tokens forwards this run measured, so the totals are per token and can be checked + * against the artifact's own weight bytes + */ +void pn_tt_matmul_group_read_report(const pn_tt_device *device, double tokens); + +/** + * STEP 3a — forget every member's cached parameter blocks (host side only; the card's blocks + * are simply overwritten later). Called once a batched prefill has flushed: the prefill's + * blocks (no row-0 bit, word 13) otherwise fill all 320 slots and every decode block then + * falls back to a rotating-slot write (census: 1,632 fallbacks a token, all differing in + * word 13 alone). `PN_TT_PARAM_CACHE_RESET` default 1, 0 = keep (the old behaviour). + * The caller guarantees nothing is in flight. + */ +void pn_tt_matmul_group_param_cache_reset(pn_tt_device *device); + +#endif /* USE_TT */ +#endif /* PN_TT_MATMUL_GROUP_H */ diff --git a/src/accel/tt/pn_tt_name_list.c b/src/accel/tt/pn_tt_name_list.c new file mode 100644 index 0000000..8c99383 --- /dev/null +++ b/src/accel/tt/pn_tt_name_list.c @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_name_list.c + * @brief Implementation of exact comma-separated-list membership. See pn_tt_name_list.h, + * which carries why a filter that matches nothing is indistinguishable from a filter + * that is off. + * + * No allocation, no I/O, no device — deliberately, so it is provable with no card. + * + * @status None + * @issues None + * @todo None + */ + +#include "pn_tt_name_list.h" + +#include + +bool pn_tt_name_list_contains(const char *list, const char *name) +{ + size_t length; + + /* An UNSET knob does not restrict. This is the asymmetry the header names, and it is why + * a caller can never tell an empty list from a matching one — nor should need to. */ + if (!list || !*list) + return true; + if (!name || !*name) + return false; + length = strlen(name); + for (;;) { + const char *comma = strchr(list, ','); + size_t span = comma ? (size_t)(comma - list) : strlen(list); + + /* Compared WHOLE, so `render` does not take `render-widen`. */ + if (span == length && strncmp(list, name, span) == 0) + return true; + if (!comma) + return false; + list = comma + 1; + } +} + +bool pn_tt_name_list_selects(const char *list, const char *name) +{ + /* THE ONE LINE THAT IS THE WHOLE POINT. An unset selector selects NOTHING, where an unset + * filter admits everything — see the header for the silicon this cost. Everything after + * this guard is deliberately the SAME walk, so the two can only ever disagree about the + * empty list and never about a membership. */ + if (!list || !*list) + return false; + return pn_tt_name_list_contains(list, name); +} diff --git a/src/accel/tt/pn_tt_name_list.h b/src/accel/tt/pn_tt_name_list.h new file mode 100644 index 0000000..10048d0 --- /dev/null +++ b/src/accel/tt/pn_tt_name_list.h @@ -0,0 +1,82 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_name_list.h + * @brief Exact membership in a comma-separated name list — the shape every `PN_TT_*` env + * knob that names families, cores or arms is parsed with. + * + * --------------------------------------------------------------------------- + * WHY IT IS ITS OWN FILE + * --------------------------------------------------------------------------- + * A list filter that matches NOTHING and a filter that is simply off look identical from the + * outside: both leave the default behaviour in place. So a knob whose whole job is to + * restrict something — `PN_TT_ONCARD_BATCH_FAMILY` restricts the queue-depth instrument to + * the families whose descriptors are idempotent — fails SILENTLY into "the instrument did + * nothing", and the reading is "the lever is worth nothing" rather than "the filter is + * broken". That is the shape of every vacuous gate recorded on this project. + * + * Pulling the four lines out means they are provable with no card, no device and no + * `USE_TT`, which is the only way that failure gets caught before it produces a number. + * + * --------------------------------------------------------------------------- + * WHAT "EXACT" MEANS, AND WHY IT IS NOT A SUBSTRING TEST + * --------------------------------------------------------------------------- + * The names it matches are residency names, and two of them are `render-widen` and + * `render-narrow`. A substring test on `render` would take both — which is sometimes what a + * caller wants and never what it SAID. Each comma-separated element must equal the whole + * name, so a caller that wants both writes both. + * + * No allocation, no I/O, no device. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TT_NAME_LIST_H +#define PN_TT_NAME_LIST_H + +#include + +/** + * Is `name` one of `list`'s comma-separated elements, compared whole? + * + * @param list e.g. "eltwise,render-widen"; NULL or "" means EVERY name (an unset knob does + * not restrict), which is the one asymmetry in here and the reason it is stated + * in the signature rather than left to a caller's `if` + * @param name the candidate; NULL or "" is never a member of a non-empty list + * @return true when `list` is empty, or when some element equals `name` exactly + * + * Thread-safe: yes (pure) Reentrant: yes Signal-safe: yes + */ +bool pn_tt_name_list_contains(const char *list, const char *name); + +/** + * Does `list` SELECT `name`? The exact dual of the above: **an unset list selects NOTHING.** + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS, AND IT IS NOT A STYLE PREFERENCE + * --------------------------------------------------------------------------- + * `pn_tt_name_list_contains()` is a FILTER — "unset means every family" — and it answers + * **true** for a NULL list by design. A RED ARM is a SELECTOR, and arming one with the + * filter INVERTS it: with the knob unset the arm is armed, on every run, for everyone. + * + * That is not hypothetical. `PN_TT_RED=residency_await_early` was armed with the filter, so + * every unnamed run checked completion against `expected + 1` and **every descriptor of + * every family was refused at token 0**, with "completed_count does not match the number + * enqueued". It cost four card sessions and a cross-agent bisect, and was mis-attributed to + * an unrelated commit on the way. Measured on silicon 2026-08-25. + * + * So the two live side by side with OPPOSITE unset behaviour, each named for what it does, + * and `tests/test_tt_name_list.c` asserts the asymmetry in both directions — because the + * defect is invisible at the call site: the two calls read identically. + * + * @param list NULL or "" selects NOTHING and returns false + * @param name NULL or "" is never selected + * @return true only when some element of a NON-EMPTY `list` equals `name` exactly + * + * Thread-safe: yes (pure) Reentrant: yes Signal-safe: yes + */ +bool pn_tt_name_list_selects(const char *list, const char *name); + +#endif /* PN_TT_NAME_LIST_H */ diff --git a/src/accel/tt/pn_tt_oncard.c b/src/accel/tt/pn_tt_oncard.c new file mode 100644 index 0000000..5af5133 --- /dev/null +++ b/src/accel/tt/pn_tt_oncard.c @@ -0,0 +1,2170 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_oncard.c + * @brief The ops that used to be host arithmetic between two PCIe transfers: the RMS norms, + * the SwiGLU gate, the residual adds and the LM head's top-N — each on its own + * resident core, passing activations to the matmul through card DRAM. + * + * pn_tt_device.c owns the card and the matmul; this file owns the other three + * residencies and the DRAM strips they share. The public API is in pn_tt_device.h. + * + * --------------------------------------------------------------------------- + * WHY THIS IS THE WHOLE REMAINING WALL, AS A NUMBER + * --------------------------------------------------------------------------- + * A token was 0.568 s of which readback was 0.412 — 73% — because every projection's result + * crossed a link that does ~4.7 MB/s card->host with no device->host DMA. Nothing about + * that transfer can be made faster; the only move left is not to make it. Each op moved here + * removes the readback of what it consumes AND the upload of what it produces, and the ones + * that remain (q/k/v, for host RoPE) are remaining because of a named capability gap and not + * because of a preference. + * + * --------------------------------------------------------------------------- + * THE THREE INVARIANTS A CALLER OF THESE OPS IS RESPONSIBLE FOR + * --------------------------------------------------------------------------- + * 1. **Rows 1-31 of every strip are zero and stay zero.** A matvec is Mt=1; the other 31 + * rows are the tile quantum and the FPU sees all of them. pn_tt_activation_alloc() zeroes + * once, and every op here preserves it for free — a matmul of a zero row is zero, + * silu(0)*0 is zero, 0+0 is zero, and an RMS norm of a zero row is 0*gamma*rsqrt(eps). + * An op that did NOT preserve it would corrupt every later token, silently. + * - **MEASURED ON THE ON-CARD ATTENTION ARM 2026-08-16, and the invariant HOLDS — it was + * never weakened.** The worry was that flash decode gathers a KV group's eight Q heads + * into the eight ROWS of a staging tile, so rows 1-31 of `attn` — and through the + * residual add, of `hidden` — might not be zero there. That was recorded as INFERRED + * harmless and NOT measured. It is measured now, with the whole-tile reader + * `pn_tt_activation_download_tile_row()` over every one of rows 1-31 of both strips, + * layers rotating with the position: **max|v| = 0 exactly, 0 non-finite.** The gather is + * internal staging; the writeback lands each of the group's eight Q heads at ROW 0 of + * its own `dim_tiles` tiles, which is why the invariant survives. + * - **The census carries its own control**, because a reader that returned zeros would + * have reported the invariant holding. Row 0 read through the SAME whole-tile path + * comes back non-zero (0.68 on `attn`, 3.97 on `hidden`) and agrees with the shipping + * row-0 reader — different bytes, different path — to **exactly 0**. + * 2. **ONE block depth for every descriptor of one residency.** Circular buffers are placed + * at launch. A descriptor whose depth or tile count leaves the CB write pointer off the + * block grid makes the NEXT descriptor step OVER the buffer limit instead of wrapping, + * after which every block walks further up L1 with no fault and no error — measured, and + * it landed a read on the queue control block (foundry/tt/ft_tt_op_sample.h). So the + * depth is chosen ONCE, at arm time, against every tile count the run will use, and a + * shape that does not fit is refused rather than served. + * 3. **A residency that refuses a descriptor is STRANDED, not wedged.** The reader leaves + * its loop and `tail` never advances, so the other four processors wait forever on work + * that will not arrive. pn_tt_residency_submit() sees it in ~1 ms rather than at the + * timeout, and the recovery is to retire by ASKING and re-arm — never a reset, never a + * park. + * + * --------------------------------------------------------------------------- + * THE THREE PROGRAMS, AND HOW THEY ARE BUILT + * --------------------------------------------------------------------------- + * `reduce`, `eltwise` and `topk` ride in the binary's embedded blob beside the matmul ones, + * and are rebuilt into it with `cd foundry/tt/tools && make regen-programs`. The recipes + * below are what the blob was generated FROM; `PN_TT_PROGRAM_DIR` reads a directory built + * this way instead, for a developer testing a program they just built. Every + * compile-time argument below is read off the kernels' own `ft_tensix_compile_time_arg()` + * indices rather than carried over from another op — carrying them over is what produced the + * Makefile.memory bug. From foundry/tt, with RELOCATE_KERNEL_TEXT=1 on all three: + * + * eltwise READER_CTARGS=0,1,2048,1 COMPUTE_CTARGS=0,1,2,16,1 WRITER_CTARGS=16,2048,1 + * CB_TABLE=kernels/cb_tables/eltwise_float16b.cbtable + * resident_eltwise_reader.c resident_eltwise_compute.c resident_vector_writer.c + * reduce READER_CTARGS=0,1,5,6,7,2048,1 COMPUTE_CTARGS=0,1,2,3,4,5,6,7,8,9,16,1 + * WRITER_CTARGS=16,2048,1 CB_TABLE=kernels/cb_tables/reduce_float16b.cbtable + * resident_reduce_reader.c resident_reduce_compute.c resident_vector_writer.c + * topk resident_topk_reader.c resident_topk_compute.c resident_topk_select.c + * + * `tools/check_tensix_program_sources.sh ` is not optional afterwards: all seven + * directories once carried `.src` sidecars naming sources deleted weeks earlier, and every + * green they produced came from images nobody could rebuild. + * + * @status Norm, SwiGLU, residual add and top-N green on silicon, and each residency is + * re-armed when it refuses a descriptor (pn_tt_oncard_rearm) — which is what a + * serve needs, because its residencies idle between requests and retire. The GDN + * family is selected here and armed in pn_tt_gdn.c; it has never run on silicon. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_device_internal.h" +#include "pn_tt_core_boot_guard.h" +#include "pn_tt_eltwise_group.h" + +#include +#include +#include + +#include "tt/convert/ft_float_convert.h" +#include "tt/ft_tt_op_dispatch.h" +#include "tt/ft_tt_op_l1_layout.h" +#include "tt/ft_tt_op_sample.h" +#include "tt/ft_tt_op_vector.h" +#include "tt/ft_tt_program_resolve.h" +#include "tt/ft_tt_tile_row0.h" +#include "tt/ft_tt_tilize.h" + +/** The three programs this file arms, by NAME. Each is a whole program — five kernel images + * plus the firmware they were linked against — and each gets its own core. They come out of + * the binary's own blob unless PN_TT_PROGRAM_DIR names a directory instead. + * + * `PN_TT_ELTWISE_PROGRAM` and `PN_TT_CORE_ELTWISE_*` are in `pn_tt_eltwise_group.h`, because + * that module arms members of the same family on other cores and re-arms member 0 on a + * refusal — one spelling, one default, one place. */ +#define PN_TT_REDUCE_PROGRAM "reduce" +#define PN_TT_SAMPLE_PROGRAM "topk" + +/** Which enabled Tensix each residency drives. Defaults, not constants — a serve with a + * placement should pass its own, and these must be three cores nobody else holds. */ +#define PN_TT_CORE_REDUCE_ENV "PN_TT_CORE_REDUCE" +#define PN_TT_CORE_SAMPLE_ENV "PN_TT_CORE_SAMPLE" +#define PN_TT_CORE_REDUCE_DEFAULT 72u +#define PN_TT_CORE_SAMPLE_DEFAULT 74u + +/** Which host ops this run moves onto the card: a comma list of the names below, or `all` + * (the default) or `none`. It exists so the A/B is across ONE binary running the same + * kernels on the same weights — the same reason PN_TT_DISPATCH does. */ +#define PN_TT_ONCARD_ENV "PN_TT_ONCARD" + +/** Pairs the sampling op selects. The forward needs the argmax; a handful more costs + * nothing on the card and makes the result block readable as evidence rather than as one + * number that could be anything. */ +#define PN_TT_TOPK_SELECT 8u +/** Per-call N ceiling (the kernel's kTopkMaxSelect): sampled requests ask for 64. */ +#define PN_TT_TOPK_SELECT_MAX 64u + +/** The greedy sampler state this file arms: temperature <= 0 is a pure argmax on the RAW + * values, which is exactly what a host serial scan over the same row computes — that is + * what makes EXACT INDEX AGREEMENT the honest gate rather than a correlation. */ +#define PN_TT_TOPK_RNG_SEED 0x9E3779B9u + +static const char *const pn_tt_oncard_names[PN_TT_ONCARD_COUNT] = { "norm", "swiglu", + "residual", "topk", + "attention", "gdn" }; + +/** + * RED PROOFS. `PN_TT_RED=` deliberately breaks ONE on-card op so the acceptance gate + * can be shown to CATCH it. + * + * It is a knob and not a test-only build because what is being proven is a property of the + * SHIPPING path — that the 20-probe argmax set is still sensitive to each op after that op + * stopped producing a number the host ever looks at. A separate binary would prove it about + * a different program. Every mutation below changes the op's ARITHMETIC and none of them + * changes a length, an address or a tile count: a mutation that SHORTENS a transfer can + * reach zero and take the card down instead of going red (DEV_NOTES.md §1). + * + * norm_eps the reduce's eps is 1e6x too large — a wrong norm, smoothly + * swiglu_swap silu(up) * gate instead of silu(gate) * up — a swapped operand + * residual_sub hidden - project instead of hidden + project + * topk_row select over logical row 1 of each tile, which is the zero padding + * rope_position the rotary table is built for position 0 instead of the token's + * attn_group every attention descriptor reads KV group 0 + * kv_position the append lands one position late + */ +#define PN_TT_RED_ENV "PN_TT_RED" + +/** Is `name` the mutation this process was asked to run? Read every call; it fires for the + * whole run rather than once, because these break an op and not a single descriptor. */ +bool pn_tt_red(const char *name) +{ + const char *value = getenv(PN_TT_RED_ENV); + + return value && *value && strcmp(value, name) == 0; +} + +/* ================================================================ + * Selection + * ================================================================ */ + +/** Does `list` (a comma-separated PN_TT_ONCARD value) name `word`? */ +static bool pn_tt_list_contains(const char *list, const char *word) +{ + size_t length = strlen(word); + const char *cursor = list; + + while (*cursor) { + const char *comma = strchr(cursor, ','); + size_t span = comma ? (size_t)(comma - cursor) : strlen(cursor); + + if (span == length && strncmp(cursor, word, length) == 0) + return true; + if (!comma) + break; + cursor = comma + 1; + } + return false; +} + +/** + * Read PN_TT_ONCARD into `device->oncard_requested`. Unset means all of them. + * + * **THE EXCEPTION TO `all` IS NOW PER MODEL FAMILY, NOT A CONSTANT.** There were two + * globally opt-in families; GDN is measured away entirely, and attention is on for the family + * that has the evidence and off for the one that does not — `device->oncard_default[]`, set + * by `pn_tt_device_oncard_default()` from the backend that owns the model. + * + * GDN was opt-in because it was slower than the host recurrence until the two per-head norms + * moved with it. They moved: the strip bridge runs the block whole on the card at readback + * 326.09 KiB/token against 1478.09 and 3.79 tok/s against 2.20, `cmp` IDENTICAL. + * + * ATTENTION was opt-in because it moves WHERE THE KV CACHE LIVES, changes the order the + * attention sum accumulates in, and changes what session export costs — a different forward + * rather than a faster one. Each of those three is now settled rather than argued: + * + * - the FORWARD passes a calibrated chat-parity gate — ratio 0.986080, t -1.608, disagree + * 2.76%, worst document 0.989, all five criteria, against a gate proven to FAIL at + * sigma 0.2 (9.09% disagree) and 0.4. It costs +0.4% perplexity, agreed by two + * instruments on two corpora, and the 1e-3 intermediate bar it misses is the bar an arm + * whose 48-token output is BYTE-IDENTICAL to the oracle also misses; + * - the SPEED is better at every length measured — with the GDN block armed beside it, + * 3.79 -> 5.02 tok/s at bench, and readback 326.09 -> 6.09 KiB/token, which is the top-N + * and the 24 gate blocks and nothing else; + * - SESSION EXPORT is the one real cost and it is now REFUSED rather than served wrong: + * `prefix_cache.c` asks `pn_qwen35moe_model_tt_kv_on_card()` before capturing, because + * the host K/V mirror is not written on this arm and a capture would record zeros. + * + * And it is the only path to long context on a 7.1 GB host: the host fp32 mirror is 65,536 B + * a position, so a 256k session is 16 GiB on the host arm and nothing on this one. + * + * **NONE OF THAT EVIDENCE TRANSFERS TO THE LLAMA FAMILY, AND A GLOBAL DEFAULT WOULD HAVE + * SHIPPED IT ANYWAY.** `llama_forward_tt.c` reads the same flag and arms the same op family + * for MiniCPM5-1B, where on-card attention is recorded as FAILING the chat-parity gate — and + * it arms LAZILY, after `pn_tt_device_arm_oncard()`, so nothing on that path would have + * mentioned the change. That is why the default is a device field a backend sets for its own + * model rather than a constant in this function. + * + * **A DEFAULT AND A NAMED FAMILY WANT OPPOSITE BEHAVIOUR ON A FAILED ARM, WHICH IS WHY + * `oncard_named` EXISTS AND WHY IT IS NOW LOAD-BEARING FOR TWO FAMILIES.** GDN without the + * bridge is 1.43 tok/s, so `qwen35moe_resident_tt.c` gives the block back to the host when it + * armed by DEFAULT and the bridge did not come up. Attention that fails to arm by DEFAULT + * runs the HOST attention block — correct, O(context), not the card arm — whereas one asked + * for BY NAME refuses, because `return -1` there reaches `goto fail` in + * pn_qwen35moe_tt_upload and drops the whole model to the CPU forward at ~0.05 tok/s. That is + * the right price for an explicit request and the wrong one for a default. + */ +static void pn_tt_read_oncard_selection(pn_tt_device *device) +{ + const char *value = getenv(PN_TT_ONCARD_ENV); + unsigned index; + + for (index = 0; index < PN_TT_ONCARD_COUNT; index++) { + /* NAMED is tracked apart from REQUESTED because the two want OPPOSITE behaviour when + * a family fails to arm, and one flag cannot express both. See the header above. */ + device->oncard_named[index] = + value && *value && strcmp(value, "all") != 0 && strcmp(value, "none") != 0 && + pn_tt_list_contains(value, pn_tt_oncard_names[index]); + if (!value || !*value || strcmp(value, "all") == 0) + device->oncard_requested[index] = device->oncard_default[index]; + else if (strcmp(value, "none") == 0) + device->oncard_requested[index] = false; + else + device->oncard_requested[index] = device->oncard_named[index]; + } +} + +void pn_tt_device_oncard_default(pn_tt_device *device, pn_tt_oncard_op op, bool on) +{ + if (!device || (unsigned)op >= PN_TT_ONCARD_COUNT) + return; + device->oncard_default[op] = on; + /* RE-READ, so the caller may ask `..._oncard_requested()` before anything is armed. It is + * a getenv and a few strcmps, and `pn_tt_device_arm_oncard()` reads it again — but + * without this the answer before the arm is whatever calloc left, and one caller already + * relies on it: the attention sigmoid gate is a THIRD elementwise width that has to be + * declared to `place_cbs`, and it is chosen from this question. */ + pn_tt_read_oncard_selection(device); +} + +bool pn_tt_device_oncard_requested(const pn_tt_device *device, pn_tt_oncard_op op) +{ + if (!device || (unsigned)op >= PN_TT_ONCARD_COUNT) + return false; + return device->oncard_requested[op]; +} + +bool pn_tt_device_oncard_named(const pn_tt_device *device, pn_tt_oncard_op op) +{ + if (!device || (unsigned)op >= PN_TT_ONCARD_COUNT) + return false; + return device->oncard_named[op]; +} + +bool pn_tt_device_oncard(const pn_tt_device *device, pn_tt_oncard_op op) +{ + if (!device || (unsigned)op >= PN_TT_ONCARD_COUNT) + return false; + return device->oncard[op]; +} + +/* ================================================================ + * DRAM activation strips + * ================================================================ */ + +int pn_tt_zero_dram(pn_tt_device *device, uint32_t bank, uint32_t address, size_t bytes) +{ + return pn_tt_fill_dram(device, bank, address, bytes, 0u); +} + +int pn_tt_fill_dram(pn_tt_device *device, uint32_t bank, uint32_t address, size_t bytes, + unsigned char value) +{ + const ft_tt_noc_access *bulk = &device->runtime.bulk_dram; + unsigned char *zeros = calloc(1, PN_TT_ZERO_CHUNK_BYTES); + size_t offset = 0; + int rc = FT_TT_OK; + + if (!zeros) + return -1; + if (value != 0u) + memset(zeros, value, PN_TT_ZERO_CHUNK_BYTES); + /* EVERY DEVICE OPERATION BUT THE ENQUEUE ITSELF WAITS FIRST. A chained projection + * may still be writing the strip this one touches, and the members run + * concurrently — so the rule lives at ONE seam instead of at every call site + * (pn_tt_device.h, pn_tt_matvec_enqueue). It is a branch when nothing is pending. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + while (offset < bytes && rc == FT_TT_OK) { + size_t chunk = bytes - offset; + + if (chunk > PN_TT_ZERO_CHUNK_BYTES) + chunk = PN_TT_ZERO_CHUNK_BYTES; + rc = bulk->write(bulk->user, (uint16_t)device->dram[bank].x, + (uint16_t)device->dram[bank].y, (uint64_t)address + offset, zeros, + chunk); + offset += chunk; + } + free(zeros); + if (rc != FT_TT_OK) + return -1; + device->bytes_written += bytes; + return 0; +} + +int pn_tt_write_dram(pn_tt_device *device, uint32_t bank, uint32_t address, const void *data, + size_t bytes) +{ + const ft_tt_noc_access *bulk = &device->runtime.bulk_dram; + const unsigned char *source = (const unsigned char *)data; + size_t offset = 0; + int rc = FT_TT_OK; + + if (!device || !data || bank >= device->dram_bank_count) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + while (offset < bytes && rc == FT_TT_OK) { + size_t chunk = bytes - offset; + + if (chunk > PN_TT_ZERO_CHUNK_BYTES) + chunk = PN_TT_ZERO_CHUNK_BYTES; + rc = bulk->write(bulk->user, (uint16_t)device->dram[bank].x, + (uint16_t)device->dram[bank].y, (uint64_t)address + offset, + source + offset, chunk); + offset += chunk; + } + if (rc != FT_TT_OK) + return -1; + device->bytes_written += bytes; + return 0; +} + +int pn_tt_read_dram(pn_tt_device *device, uint32_t bank, uint32_t address, void *out, + size_t bytes) +{ + const ft_tt_noc_access *bulk = &device->runtime.bulk_dram; + unsigned char *destination = (unsigned char *)out; + size_t offset = 0; + int rc = FT_TT_OK; + + if (!device || !out || bank >= device->dram_bank_count) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + while (offset < bytes && rc == FT_TT_OK) { + size_t chunk = bytes - offset; + + if (chunk > PN_TT_ZERO_CHUNK_BYTES) + chunk = PN_TT_ZERO_CHUNK_BYTES; + rc = bulk->read(bulk->user, (uint16_t)device->dram[bank].x, + (uint16_t)device->dram[bank].y, (uint64_t)address + offset, + destination + offset, chunk); + offset += chunk; + } + if (rc != FT_TT_OK) + return -1; + device->bytes_read += bytes; + return 0; +} + +int pn_tt_activation_alloc(pn_tt_device *device, uint32_t tile_count, + pn_tt_activation *out_activation) +{ + /* EVERY STRIP GETS THE SAME HEIGHT, resolved once. A strip one band tall beside strips + * two bands tall is a matmul reading band 1 of A and writing band 1 of a C that has none + * — off the end of the buffer, with every kernel retiring clean. (The one exception is a + * whole SET of another height used only on its own — the prefill-only tall set.) */ + return pn_tt_activation_alloc_rows(device, tile_count, pn_tt_device_strip_tile_rows(device), + out_activation); +} + +int pn_tt_activation_alloc_rows(pn_tt_device *device, uint32_t tile_count, uint32_t tile_rows, + pn_tt_activation *out_activation) +{ + ft_tt_buffer buffer; + uint32_t bank; + + if (!device || !out_activation || tile_count == 0u || tile_rows == 0u || + tile_rows > PN_TT_STRIP_MAX_TILE_ROWS || device->dram_bank_count == 0u) + return -1; + /* Round-robin the strips across banks so the reader pulling an operand and the writer + * pushing a result are not usually the same endpoint — Phase 6.7 measured two endpoints + * on one NoC row sharing a ceiling. */ + bank = device->strip_bank_cursor % device->dram_bank_count; + device->strip_bank_cursor++; + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, + (size_t)tile_count * tile_rows * PN_TT_TILE_BF16, + &buffer) != FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating a %u-tile x %u-band activation strip\n", + tile_count, tile_rows); + return -1; + } + memset(out_activation, 0, sizeof(*out_activation)); + out_activation->bank = buffer.logical_bank; + out_activation->address = (uint32_t)buffer.address; + out_activation->tile_count = tile_count; + out_activation->tile_rows = tile_rows; + out_activation->active_rows = 1u; + out_activation->row_offset = 0u; + /* Mirror 0 IS the strip, always, so every caller of every accessor below can index + * replica 0 without asking whether this strip was replicated. */ + out_activation->replica_count = 1u; + out_activation->replica_bank[0] = out_activation->bank; + out_activation->replica_address[0] = out_activation->address; + /* THE PRECONDITION every row-0 write depends on and none of them can check. */ + if (pn_tt_zero_dram(device, out_activation->bank, out_activation->address, + (size_t)tile_count * tile_rows * PN_TT_TILE_BF16) != 0) { + fprintf(stderr, "pn_tt: could not stage a %u-tile strip zeroed\n", tile_count); + return -1; + } + return 0; +} + +int pn_tt_activation_alloc_replicated(pn_tt_device *device, uint32_t tile_count, + uint32_t replicas, pn_tt_activation *out_activation) +{ + return pn_tt_activation_alloc_replicated_rows(device, tile_count, replicas, + pn_tt_device_strip_tile_rows(device), + out_activation); +} + +int pn_tt_activation_alloc_replicated_rows(pn_tt_device *device, uint32_t tile_count, + uint32_t replicas, uint32_t tile_rows, + pn_tt_activation *out_activation) +{ + uint32_t index; + + if (!device || !out_activation) + return -1; + if (pn_tt_activation_alloc_rows(device, tile_count, tile_rows, out_activation) != 0) + return -1; + if (replicas <= 1u) + return 0; + /* MORE MIRRORS THAN BANKS IS A REFUSAL, NOT A WRAP. Wrapping would put two mirrors in + * one bank, which is the exact condition this mechanism exists to remove, arriving + * under the name of the fix and reporting the higher replica count. */ + if (replicas > PN_TT_ACTIVATION_MAX_REPLICAS || replicas > device->dram_bank_count) { + fprintf(stderr, "pn_tt: %u replicas of a %u-tile strip is more than the %u banks " + "this card has\n", replicas, tile_count, device->dram_bank_count); + return -1; + } + + for (index = 1u; index < replicas; index++) { + ft_tt_buffer buffer; + /* STEP ONE BANK PER MIRROR from the strip's own. The strips themselves are already + * round-robined, so consecutive strips start in consecutive banks and a stride of + * one visits every bank whatever the replica count is. */ + const uint32_t bank = (out_activation->bank + index) % device->dram_bank_count; + + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, bank, + (size_t)tile_count * out_activation->tile_rows * PN_TT_TILE_BF16, + &buffer) != FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating mirror %u of a %u-tile strip " + "(bank %u)\n", index, tile_count, bank); + return -1; + } + out_activation->replica_bank[index] = buffer.logical_bank; + out_activation->replica_address[index] = (uint32_t)buffer.address; + /* Every mirror carries the strip invariant too: rows 1-31 zero. A mirror the + * producer has not written yet is read by a matmul member from token 0. */ + if (pn_tt_zero_dram(device, buffer.logical_bank, (uint32_t)buffer.address, + (size_t)tile_count * out_activation->tile_rows * + PN_TT_TILE_BF16) != 0) { + fprintf(stderr, "pn_tt: could not stage mirror %u zeroed\n", index); + return -1; + } + out_activation->replica_count = index + 1u; + } + return 0; +} + +int pn_tt_activation_upload(pn_tt_device *device, const pn_tt_activation *activation, + const float *values, uint32_t count) +{ + const ft_tt_noc_access *bulk; + uint32_t tiles; + double started; + int rc; + + if (!device || !activation || !values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + tiles = count / PN_TT_TILE_DIM; + if (tiles > activation->tile_count || count > device->max_row_features) + return -1; + bulk = &device->runtime.bulk_dram; + started = pn_tt_now_seconds(); + + ft_bf16_from_float_array(values, device->row_in, count); + /* EVERY MIRROR, or the ones this call did not write go stale and a matmul member reads + * the previous token's value. The rounding is done once and the same bf16 row is sent + * to each, so the mirrors are bit-identical by construction rather than by two + * conversions agreeing. */ + for (uint32_t replica = 0u; replica < activation->replica_count; replica++) { + const uint32_t bank = activation->replica_bank[replica]; + + rc = ft_tt_tile_row0_write(bulk, (uint16_t)device->dram[bank].x, + (uint16_t)device->dram[bank].y, + activation->replica_address[replica], tiles, device->row_in); + if (rc != FT_TT_OK) + return -1; + device->bytes_written += + (size_t)tiles * FT_TT_TILE_ROW0_CHUNKS * FT_TT_TILE_ROW0_HALF_BYTES; + } + device->upload_seconds += pn_tt_now_seconds() - started; + return 0; +} + +int pn_tt_activation_download(pn_tt_device *device, const pn_tt_activation *activation, + float *out_values, uint32_t count) +{ + const ft_tt_noc_access *bulk; + uint32_t tiles; + double started; + int rc; + + if (!device || !activation || !out_values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + tiles = count / PN_TT_TILE_DIM; + if (tiles > activation->tile_count || count > device->max_row_features) + return -1; + bulk = &device->runtime.bulk_dram; + started = pn_tt_now_seconds(); + + rc = ft_tt_tile_row0_read(bulk, (uint16_t)device->dram[activation->bank].x, + (uint16_t)device->dram[activation->bank].y, activation->address, + tiles, device->row_out); + if (rc != FT_TT_OK) + return -1; + device->bytes_read += (size_t)tiles * FT_TT_TILE_ROW0_CHUNKS * FT_TT_TILE_ROW0_READ_BYTES; + ft_float_from_bf16_array(device->row_out, out_values, count); + device->readback_seconds += pn_tt_now_seconds() - started; + return 0; +} + +int pn_tt_activation_download_chunks(pn_tt_device *device, const pn_tt_activation *activation, + const unsigned char *need, float *out_values, + uint32_t count) +{ + const ft_tt_noc_access *bulk; + uint16_t chunk_buf[FT_TT_TILE_ROW0_READ_BYTES / sizeof(uint16_t)]; + uint32_t tiles, tile, chunk, chunks_read = 0u; + double started; + + if (!device || !activation || !need || !out_values || count == 0u || + (count % PN_TT_TILE_DIM) != 0u) + return -1; + if (pn_tt_matvec_flush(device) != 0) + return -1; + tiles = count / PN_TT_TILE_DIM; + if (tiles > activation->tile_count || count > device->max_row_features) + return -1; + bulk = &device->runtime.bulk_dram; + started = pn_tt_now_seconds(); + for (tile = 0u; tile < tiles; tile++) { + for (chunk = 0u; chunk < FT_TT_TILE_ROW0_CHUNKS; chunk++) { + const uint32_t at = tile * FT_TT_TILE_ROW0_CHUNKS + chunk; + int rc; + + if (!need[at]) + continue; + rc = bulk->read(bulk->user, (uint16_t)device->dram[activation->bank].x, + (uint16_t)device->dram[activation->bank].y, + (uint64_t)activation->address + + ft_tt_tile_row0_chunk_offset(tile, chunk), + chunk_buf, sizeof(chunk_buf)); + if (rc != FT_TT_OK) + return -1; + ft_float_from_bf16_array(chunk_buf, out_values + (size_t)at * FT_TT_TILE_ROW0_HALF_ELEMS, + FT_TT_TILE_ROW0_HALF_ELEMS); + chunks_read++; + } + } + device->bytes_read += (size_t)chunks_read * FT_TT_TILE_ROW0_READ_BYTES; + device->readback_seconds += pn_tt_now_seconds() - started; + return 0; +} + +/* ================================================================ + * The batched pair — rows 0..M-1 of a strip + * + * See pn_tt_device.h for why the 31 padding rows are the batch dimension and why moving + * whole tiles is CHEAPER per token than the row-0 reader, not more expensive. + * ================================================================ */ + +/** Grow the two staging buffers to hold `tiles` face-ordered tiles and `rows * count` + * bf16 elements. Kept on the device so a per-layer call is not a malloc. */ +static int pn_tt_rows_stage(pn_tt_device *device, uint32_t tiles, uint32_t rows, + uint32_t count) +{ + size_t need_tiles = (size_t)tiles * PN_TT_TILE_BF16; + size_t need_rows = (size_t)rows * count * sizeof(uint16_t); + + if (need_tiles > device->tile_stage_bytes) { + unsigned char *grown = realloc(device->tile_stage, need_tiles); + + if (!grown) + return -1; + device->tile_stage = grown; + device->tile_stage_bytes = need_tiles; + } + if (need_rows > device->rows_stage_bytes) { + uint16_t *grown = realloc(device->rows_stage, need_rows); + + if (!grown) + return -1; + device->rows_stage = grown; + device->rows_stage_bytes = need_rows; + } + return 0; +} + +int pn_tt_activation_upload_rows(pn_tt_device *device, const pn_tt_activation *activation, + const float *values, uint32_t count, uint32_t rows) +{ + uint32_t tiles, row; + double started; + + if (!device || !activation || !values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + /* A tile has 32 rows and nothing here wraps to a second tile row: asking for more is a + * refusal, never a silent truncation. */ + if (rows == 0u || rows > PN_TT_TILE_DIM) + return -1; + if (rows == 1u) + return pn_tt_activation_upload(device, activation, values, count); + tiles = count / PN_TT_TILE_DIM; + if (tiles > activation->tile_count) + return -1; + if (pn_tt_rows_stage(device, tiles, rows, count) != 0) + return -1; + + started = pn_tt_now_seconds(); + for (row = 0; row < rows; row++) + ft_bf16_from_float_array(values + (size_t)row * count, + device->rows_stage + (size_t)row * count, count); + /* valid_rows = rows: the tilizer zeroes rows `rows`..31, which is exactly the strip + * invariant a later Mt=1 matvec depends on. The batched write RESTORES it rather than + * relying on the allocation-time staging surviving. */ + if (ft_tt_tilize_strip(device->rows_stage, sizeof(uint16_t), rows, count, + (size_t)count * sizeof(uint16_t), + device->tile_stage) != FT_TT_TILIZE_OK) + return -1; + for (uint32_t replica = 0u; replica < activation->replica_count; replica++) + if (pn_tt_write_dram(device, activation->replica_bank[replica], + activation->replica_address[replica], device->tile_stage, + (size_t)tiles * PN_TT_TILE_BF16) != 0) + return -1; + device->upload_seconds += pn_tt_now_seconds() - started; + return 0; +} + +int pn_tt_activation_download_rows(pn_tt_device *device, const pn_tt_activation *activation, + float *out_values, uint32_t count, uint32_t rows) +{ + uint32_t tiles, tile, column, row; + double started; + + if (!device || !activation || !out_values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + if (rows == 0u || rows > PN_TT_TILE_DIM) + return -1; + if (rows == 1u) + return pn_tt_activation_download(device, activation, out_values, count); + tiles = count / PN_TT_TILE_DIM; + if (tiles > activation->tile_count) + return -1; + if (pn_tt_rows_stage(device, tiles, rows, count) != 0) + return -1; + + started = pn_tt_now_seconds(); + /* S2e (`PN_TT_ROWS_FACES`, default 1): rows <= 16 live in faces 0-1 only — read those + * face rows (two runs a tile, 64 B-rounded) instead of the whole tile. */ + if (rows <= 16u && tiles <= 4u && pn_tt_env_u32("PN_TT_ROWS_FACES", 1u) != 0u) { + const size_t run = ((size_t)rows * 32u + 63u) & ~(size_t)63u; + + for (tile = 0; tile < tiles; tile++) + if (pn_tt_read_dram(device, activation->bank, + activation->address + tile * PN_TT_TILE_BF16, + (unsigned char *)device->tile_stage + (size_t)tile * PN_TT_TILE_BF16, + run) != 0 || + pn_tt_read_dram(device, activation->bank, + activation->address + tile * PN_TT_TILE_BF16 + 512u, + (unsigned char *)device->tile_stage + (size_t)tile * PN_TT_TILE_BF16 + + 512u, + run) != 0) + return -1; + } else + /* ONE read of the whole strip, then the de-tilize on the host. Reading it row by row + * would be `rows` times the transactions for the same bytes, and this link is + * transaction-count bound (Phase 7.8a). */ + if (pn_tt_read_dram(device, activation->bank, activation->address, device->tile_stage, + (size_t)tiles * PN_TT_TILE_BF16) != 0) + return -1; + for (tile = 0; tile < tiles; tile++) { + const uint16_t *tile_base = + (const uint16_t *)device->tile_stage + (size_t)tile * (PN_TT_TILE_BF16 / 2u); + + for (row = 0; row < rows; row++) + for (column = 0; column < PN_TT_TILE_DIM; column++) + out_values[(size_t)row * count + tile * PN_TT_TILE_DIM + column] = + ft_float_from_bf16(tile_base[ft_tt_tile_face_index(row, column)]); + } + device->readback_seconds += pn_tt_now_seconds() - started; + return 0; +} + +int pn_tt_activation_zero(pn_tt_device *device, const pn_tt_activation *activation) +{ + return pn_tt_activation_zero_bands(device, activation, 0u); +} + +int pn_tt_activation_zero_bands(pn_tt_device *device, const pn_tt_activation *activation, + uint32_t bands) +{ + return pn_tt_activation_fill_bands(device, activation, bands, 0u); +} + +int pn_tt_activation_fill_bands(pn_tt_device *device, const pn_tt_activation *activation, + uint32_t bands, unsigned char value) +{ + if (!device || !activation) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. Zeroing + * a strip a queued descriptor is still writing would be a race that reads as a wrong + * answer one op later. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + /* THE WHOLE ALLOCATION, NOT THE BAND THE STRIP IS CURRENTLY POINTED AT. A seek slides + * `address`, so zeroing from there would leave the bands before it carrying the last + * pass's positions — which is exactly the residue the restage exists to remove. Backing + * the address out by `row_offset` bands is the only way to say "all of it" from a strip + * that may be mid-seek, and at `row_offset = 0` it is the pre-Mt call byte for byte. */ + for (uint32_t replica = 0u; replica < activation->replica_count; replica++) { + const size_t band = (size_t)activation->tile_count * PN_TT_TILE_BF16; + uint32_t rows = activation->tile_rows ? activation->tile_rows : 1u; + + /* PHASE G: a tall strip is zeroed only over the bands a pass actually wrote — the + * whole 64-band allocation of every strip was ~2 GB over PCIe (10 s). */ + if (bands != 0u && bands < rows) + rows = bands; + + if (pn_tt_fill_dram(device, activation->replica_bank[replica], + activation->replica_address[replica] - + (uint32_t)((size_t)activation->row_offset * band), + band * rows, value) != 0) + return -1; + } + return 0; +} + +void pn_tt_vec_probe_apply(uint32_t *words) +{ + static int probe = -1; + + if (probe < 0) + probe = (int)(pn_tt_env_u32("PN_TT_VEC_PROBE", 0u) & 0xFFu); + if (probe != 0 && words) + words[FT_TT_OP_VECTOR_DEPTH] |= (uint32_t)probe << 24; +} + +void pn_tt_device_prefer_prefetch_tiles(pn_tt_device *device, uint32_t tiles) +{ + if (device) + device->prefetch_tiles_preferred = tiles; +} + +void pn_tt_device_set_single_row(pn_tt_device *device, bool single_row) +{ + if (device) + device->single_row = single_row; +} + +void pn_tt_device_set_topk_row(pn_tt_device *device, uint32_t row) +{ + if (device && row < 32u) + device->topk_row = row; +} + +void pn_tt_device_set_verify_rows(pn_tt_device *device, uint32_t rows) +{ + if (device) + device->verify_rows = rows; +} + +void pn_tt_device_set_prefill_tile_rows(pn_tt_device *device, uint32_t rows) +{ + if (device) + device->prefill_tile_rows = rows; +} + +int pn_tt_device_prefer_strip_tile_rows(pn_tt_device *device, uint32_t rows) +{ + if (!device || device->strip_tile_rows != 0u) + return -1; + device->strip_tile_rows_preferred = rows; + return 0; +} + +uint32_t pn_tt_device_strip_tile_rows(pn_tt_device *device) +{ + if (!device) + return 1u; + if (device->strip_tile_rows == 0u) { + uint32_t rows = pn_tt_env_u32(PN_TT_STRIP_TILE_ROWS_ENV, + device->strip_tile_rows_preferred + ? device->strip_tile_rows_preferred : 1u); + + /* A POWER OF TWO AND NO WIDER THAN THE PASS THE MIXERS CAN COVER. Every band costs a + * separate pass of both chunk mixers (they refuse >32 rows by name in five places), + * so an illegal value collapses to 1 rather than half-arming: a strip taller than the + * pass would leave bands nobody wrote and a matmul would read them. */ + if (rows == 0u || (rows & (rows - 1u)) != 0u || rows > PN_TT_STRIP_MAX_TILE_ROWS) { + fprintf(stderr, "pn_tt: %s=%u is not a power of two in 1..%u — using 1\n", + PN_TT_STRIP_TILE_ROWS_ENV, rows, PN_TT_STRIP_MAX_TILE_ROWS); + rows = 1u; + } + if (rows > 1u) + fprintf(stderr, "pn_tt: strip tile-rows %u — every activation strip is %u bands of " + "32 rows, so a batched prefill pass carries %u positions and the " + "matmul sweeps the weights once for all of them\n", + rows, rows, rows * PN_TT_TILE_DIM); + device->strip_tile_rows = rows; + } + return device->strip_tile_rows; +} + +int pn_tt_activation_seek(pn_tt_activation *activation, uint32_t row) +{ + const size_t band = activation ? (size_t)activation->tile_count * PN_TT_TILE_BF16 : 0u; + int64_t delta; + + if (!activation || activation->tile_rows == 0u || row >= activation->tile_rows) + return -1; + if (row == activation->row_offset) + return 0; + delta = ((int64_t)row - (int64_t)activation->row_offset) * (int64_t)band; + activation->address = (uint32_t)((int64_t)activation->address + delta); + for (uint32_t replica = 0u; replica < activation->replica_count; replica++) + activation->replica_address[replica] = + (uint32_t)((int64_t)activation->replica_address[replica] + delta); + activation->row_offset = row; + return 0; +} + +int pn_tt_activation_span(pn_tt_activation *activation, uint32_t rows) +{ + if (!activation || rows == 0u) + return -1; + /* THE SPAN IS BOUNDED BY WHAT IS LEFT ABOVE THE SEEK, not by the allocation: a strip + * pointed at band 1 of two has one band above it, and an op told to span two would run + * off the end of the buffer into whatever the allocator put there next. */ + if (activation->row_offset + rows > activation->tile_rows) + return -1; + activation->active_rows = rows; + return 0; +} + +int pn_tt_activation_download_tile_row(pn_tt_device *device, const pn_tt_activation *activation, + uint32_t row, float *out_values, uint32_t count) +{ + uint32_t tiles, tile, column; + uint16_t *staged; + + if (!device || !activation || !out_values || count == 0u || (count % PN_TT_TILE_DIM) != 0u) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (row >= PN_TT_TILE_DIM) + return -1; + tiles = count / PN_TT_TILE_DIM; + if (tiles > activation->tile_count) + return -1; + staged = malloc((size_t)tiles * PN_TT_TILE_BF16); + if (!staged) + return -1; + /* WHOLE tiles, not the row-0 reader: rows 1-31 are exactly what this exists to see, and + * ft_tt_tile_row0_read() reads only the two 64-byte chunks row 0 lives in. */ + if (pn_tt_read_dram(device, activation->bank, activation->address, staged, + (size_t)tiles * PN_TT_TILE_BF16) != 0) { + free(staged); + return -1; + } + for (tile = 0; tile < tiles; tile++) { + const uint16_t *tile_base = staged + (size_t)tile * (PN_TT_TILE_BF16 / 2u); + + for (column = 0; column < PN_TT_TILE_DIM; column++) + out_values[tile * PN_TT_TILE_DIM + column] = + ft_float_from_bf16(tile_base[ft_tt_tile_face_index(row, column)]); + } + free(staged); + return 0; +} + +ft_tt_op_operand pn_tt_operand(const pn_tt_device *device, const pn_tt_activation *activation) +{ + ft_tt_op_operand operand; + + operand.x = (uint16_t)device->dram[activation->bank].x; + operand.y = (uint16_t)device->dram[activation->bank].y; + operand.address = activation->address; + operand.tile_bytes = PN_TT_TILE_BF16; + return operand; +} + +/* ================================================================ + * Arming the three residencies + * ================================================================ */ + +/** + * The ONE block depth a residency can run for every tile count it will see. + * + * Both conditions are the CB-cycle invariant and neither is optional: `depth` must divide + * each run so a block is contiguous, and `2 * depth` — the page count the buffers are placed + * at — must divide it too, so each descriptor returns the write pointer to the base. A + * descriptor that leaves it elsewhere walks the next one up L1 with no error. + * + * @return the depth, or 0 when no depth serves every count + */ +uint32_t pn_tt_common_depth(const uint32_t *tile_counts, uint32_t count, uint32_t cap) +{ + uint32_t depth; + + if (!tile_counts || count == 0u || cap == 0u) + return 0u; + for (depth = cap; depth >= 1u; depth--) { + uint32_t index; + bool serves = true; + + for (index = 0; index < count && serves; index++) { + if (tile_counts[index] == 0u || (tile_counts[index] % (2u * depth)) != 0u) + serves = false; + else if (ft_tt_op_block_depth(tile_counts[index], depth) != depth) + serves = false; + } + if (serves) + return depth; + } + return 0u; +} + +bool pn_tt_prepare_family(pn_tt_device *device, pn_tt_residency *residency, const char *program, + const char *name, const char *core_env, uint32_t core_default, + uint32_t processor_mask) +{ + return pn_tt_prepare_family_on_core(device, residency, program, name, core_env, + pn_tt_env_u32(core_env, core_default), processor_mask); +} + +/** + * Who, if anyone, already holds `core` — by NAME, so the refusal can say it. + * + * `except` is the residency being prepared, which is already in the table it walks and would + * otherwise report itself on a re-arm. + * + * @return the holder's name, or NULL when the core is free + */ +static const char *pn_tt_core_holder(const pn_tt_device *device, uint32_t core, + const pn_tt_residency *except) +{ + /* EIGHT named residencies are pushed below, not six. At the shipped four-way head split + * with both head-norm widths live the table holds 12 of the 15 the old sizing gave, so it + * never overflowed — but a split of 8 with two widths needs 16, and the failure would be a + * stack write past the array on exactly the hybrid configuration this check exists for. + * `pn_tt_device_residency_stat()` sizes the same list at 7 and is the one that was right. */ + const pn_tt_residency *others[10u + (PN_TT_TOPK_MAX_MEMBERS - 1u) + + PN_TT_RENDER_DIRECTIONS * PN_TT_RENDER_SPLIT_MAX_MEMBERS + + (PN_TT_GDN_MAX_MEMBERS - 1u) + + (PN_TT_GDNC_MAX_MEMBERS - 1u) + + (PN_TT_ELTWISE_MAX_MEMBERS - 1u) + + (PN_TT_ATTN_MAX_MEMBERS - 1u) + + PN_TT_HEAD_NORM_MAX_WIDTHS * PN_TT_HEAD_NORM_MAX_MEMBERS]; + uint32_t count = 0u; + uint32_t index; + + /* Member 0 of the matmul group is the DEVICE's own core, and the group holds the rest. + * The group is checked first because it is the one set that MOVES with a knob. */ + if (device->core_index == core) + return "matmul member 0"; + for (index = 0; index < device->group.member_count; index++) { + if (device->group.member[index].core_index == core) + return "a matmul group member"; + } + + others[count++] = &device->reduce; + others[count++] = &device->reduce_member; + others[count++] = &device->eltwise; + others[count++] = &device->sample; + for (index = 0; index + 1u < device->sample_members; index++) + others[count++] = &device->sample_member[index]; + others[count++] = &device->rope; + others[count++] = &device->memory; + others[count++] = &device->attention; + others[count++] = &device->gdn; + /* EIGHT. The chunkwise family is a second Tensix running a second program against the + * SAME carried state, so a collision between it and anything else is the 1560-mutual- + * re-arm failure with a correct answer at the far end of it. */ + others[count++] = &device->gdnc; + /* THE STRIP BRIDGE AND THE MERGED GATE WERE NOT IN THIS TABLE AT ALL, and `pn_tt_render.h` + * says in terms that they are refused by name against every live residency — which was + * not true of them. They sit at 85/86/87, inside the range sixteen matmul members reach, + * and any of the three core knobs can put one of them on top of another. The group half of + * the check covered them (it runs above, before this table), so the gap was render-vs- + * render, render-vs-gate and either-vs-a-family; a collision there is not a failure but a + * pair of residencies retiring each other with healthy "residency up" lines. */ + for (index = 0; index < (uint32_t)PN_TT_RENDER_DIRECTIONS; index++) { + uint32_t member; + + others[count++] = &device->render[index].residency; + /* AND THE STRIP BRIDGE'S OWN SPLIT MEMBERS. They default to 122 and upward, above + * every core any other family can reach at any legal knob — but that is an argument + * about defaults, and this table is what makes it a refusal. */ + for (member = 1u; member < device->render[index].members; member++) + others[count++] = &device->render[index].member[member - 1u]; + } + others[count++] = &device->gate.residency; + for (index = 0; index + 1u < device->gdn_members; index++) + others[count++] = &device->gdn_member[index]; + /* AND THE CHUNKWISE SPLIT'S OWN MEMBERS. They default to 107 and upward, above every core + * any other family reaches at its shipped default — but that is an argument about defaults, + * and this table is what makes it a refusal. Sixteen of them at the maximum, which is why + * the array above grew by `PN_TT_GDNC_MAX_MEMBERS - 1`. */ + for (index = 0; index + 1u < device->gdnc_members; index++) + others[count++] = &device->gdnc_member[index]; + /* THE ELEMENTWISE SPLIT'S EXTRA MEMBERS. They default to 106 and upward, above every core + * any other family can reach at any legal setting of any knob (pn_tt_eltwise_group.h) — + * but that is an argument about defaults, and this table is what makes it a refusal. */ + for (index = 0; index + 1u < device->eltwise_members; index++) + others[count++] = &device->eltwise_member[index]; + for (index = 0; index < device->head_norm_count; index++) { + uint32_t member; + + others[count++] = &device->head_norm[index].residency; + for (member = 1u; member < device->head_norm[index].members; member++) + others[count++] = &device->head_norm[index].member[member - 1u]; + } + /* AND THE ATTENTION CHUNK MIXER'S HEAD-SPLIT MEMBERS. */ + for (index = 0; index + 1u < device->attention_members; index++) + others[count++] = &device->attention_member[index]; + for (index = 0; index < count; index++) { + if (others[index] == except || !others[index]->core_booted) + continue; + if (others[index]->core_index == core) + return others[index]->name; + } + return NULL; +} + +bool pn_tt_prepare_family_on_core(pn_tt_device *device, pn_tt_residency *residency, + const char *program, const char *name, const char *core_env, + uint32_t core, uint32_t processor_mask) +{ + const char *root = pn_tt_program_override_root(); + const char *holder; + + if (pn_tt_residency_load(residency, &device->runtime, program, root, name, + processor_mask) != 0) + return false; + /* A RESIDENCY AND A MATMUL MEMBER ON ONE TENSIX RETIRE EACH OTHER FOREVER, and the group's + * members are not a fixed set: `PN_TT_MATMUL_CORES=4`, the shipped knee, puts member 1 on + * core 75. So a family's default cannot be checked against the other FAMILIES alone. + * MEASURED ON SILICON 2026-08-18: the GDN default of 75 collided with member 1 and the run + * logged 1560 mutual re-arms over 65 forwards, each one a correct-looking "residency up" + * line. Refusing NAMES it; the alternative is a run that works and is inexplicably slow. + * + * IT CHECKS EVERY LIVE RESIDENCY, NOT ONLY THE GROUP. It used to check the group alone, + * which was enough while the families' defaults were a hand-kept list of distinct + * numbers — but that list is now eleven cores over five knobs, and one of them + * (`PN_TT_GDN_HEAD_CORES`) grows a run of them. Booting a second program's firmware onto + * a core that already holds a resident triad is a WEDGED core, not a configuration. */ + holder = pn_tt_core_holder(device, core, residency); + if (holder) { + fprintf(stderr, "pn_tt: the %s residency was asked for core %u, which %s already " + "holds — the two would retire each other every token. Set %s to a " + "free core.\n", name, core, holder, core_env); + pn_tt_residency_release(residency); + return false; + } + /* AND THE OTHER HALF OF THE SAME QUESTION, WHICH ONLY THE CARD CAN ANSWER. The check + * above walks THIS PROCESS's residency table, and the failure that costs a card reset + * crosses a process boundary: a spinning compute thread left by an earlier `serve` + * outlives it, and this process's table is empty. `pn_tt_core_boot_guard.h` has the + * whole argument; `pn_tt_residency_bind_core()` below is the boot it sits in front of. */ + if (!pn_tt_core_boot_is_permitted(&device->runtime, core, name, core_env)) { + pn_tt_residency_release(residency); + return false; + } + if (pn_tt_residency_bind_core(residency, core) != 0) { + pn_tt_residency_release(residency); + return false; + } + return true; +} + +bool pn_tt_program_is_present(const char *program) +{ + ft_tt_runtime_images images; + + if (ft_tt_program_open_firmware(program, pn_tt_program_override_root(), &images) != FT_TT_OK) + return false; + ft_tt_runtime_images_release(&images); + return true; +} + +/** + * Refuse before arming anything if a family this run ASKED FOR has no program to arm. + * + * The same defect the resident matmul had, one file over: a family whose program is absent + * silently leaves its op on the host, prints `norm 0` in a line nobody reads, and + * produces a token that is correct, slower, and numerically different — with nothing saying + * the run was not the run that was configured. A missing program is a CONFIGURATION error + * and the answer to one is a refusal. + * + * Deliberately narrow: it checks only that the program RESOLVES. Arming can still fail + * for reasons that belong to the card and to this moment (a core in use, buffers that do not + * fit), and those stay non-fatal because the run can honestly continue without them. + * + * @param device open device, for its runtime + * @return 0 when every requested family has a program, -1 with the list on stderr otherwise + */ +static int pn_tt_refuse_missing_oncard_programs(pn_tt_device *device) +{ + /* ATTENTION is NULL here because it is THREE programs (rope, memory, attention) and they + * are checked where they are armed, in pn_tt_attention.c. A NULL is skipped rather than + * defaulted to a neighbour's name — a family checked against the wrong directory would + * report the wrong thing green. */ + static const char *const k_program[PN_TT_ONCARD_COUNT] = { + PN_TT_REDUCE_PROGRAM, PN_TT_ELTWISE_PROGRAM, PN_TT_ELTWISE_PROGRAM, + PN_TT_SAMPLE_PROGRAM, NULL, PN_TT_GDN_PROGRAM + }; + const char *root = pn_tt_program_override_root(); + unsigned family; + int missing = 0; + + for (family = 0; family < PN_TT_ONCARD_COUNT; family++) { + ft_tt_runtime_images images; + + if (!device->oncard_requested[family] || !k_program[family]) + continue; + if (ft_tt_program_open_firmware(k_program[family], root, &images) != FT_TT_OK) { + fprintf(stderr, "pn_tt: REFUSED — %s was asked for on the card and there is no " + "such program in %s\n", k_program[family], + pn_tt_program_where(root)); + missing++; + continue; + } + ft_tt_runtime_images_release(&images); + } + if (missing != 0) + fprintf(stderr, "pn_tt: %d on-card program%s missing. Build them into the blob, or " + "name only the families you have with %s.\n", + missing, missing == 1 ? " is" : "s are", PN_TT_ONCARD_ENV); + return missing == 0 ? 0 : -1; +} + +/* PM 8: which family a core holds, by NoC coordinate, for reports keyed by queue (the + * puller's AWAIT census). The last family started on a core wins. */ +#define PN_TT_CORE_LABELS 256u +static struct { uint16_t x, y; char name[32]; } g_core_labels[PN_TT_CORE_LABELS]; +static uint32_t g_core_label_count; + +static void pn_tt_core_label_note(uint16_t x, uint16_t y, const char *name) +{ + uint32_t i; + + for (i = 0; i < g_core_label_count; i++) + if (g_core_labels[i].x == x && g_core_labels[i].y == y) + break; + if (i == PN_TT_CORE_LABELS) + return; + g_core_labels[i].x = x; + g_core_labels[i].y = y; + snprintf(g_core_labels[i].name, sizeof(g_core_labels[i].name), "%s", name); + if (i == g_core_label_count) + g_core_label_count++; +} + +const char *pn_tt_core_label(uint16_t x, uint16_t y) +{ + uint32_t i; + + for (i = 0; i < g_core_label_count; i++) + if (g_core_labels[i].x == x && g_core_labels[i].y == y) + return g_core_labels[i].name; + return "?"; +} + +bool pn_tt_start_family(pn_tt_residency *residency) +{ + if (pn_tt_residency_start(residency) != 0) { + pn_tt_residency_release(residency); + return false; + } + pn_tt_core_label_note((uint16_t)residency->core_x, (uint16_t)residency->core_y, + residency->name); + fprintf(stderr, "pn_tt: %s residency up on core %u\n", residency->name, + residency->core_index); + return true; +} + +/** The reduce residency, sized for RMS norm at ONE width. */ +static bool pn_tt_arm_reduce(pn_tt_device *device, uint32_t norm_tile_cols) +{ + static const ft_tt_op_reduce_function members[] = { FT_TT_OP_REDUCE_FN_RMS_NORM }; + ft_tt_op_reduce_residency residency; + ft_tt_launch_cb_entry placed[FT_TT_OP_REDUCE_CB_COUNT]; + + if (ft_tt_op_reduce_residency_for(members, 1u, norm_tile_cols, PN_TT_TILE_BF16, + &residency) != FT_TT_OK) { + fprintf(stderr, "pn_tt: no reduce residency serves Wt=%u — norms stay on the host\n", + norm_tile_cols); + return false; + } + if (!pn_tt_prepare_family(device, &device->reduce, PN_TT_REDUCE_PROGRAM, "reduce", + PN_TT_CORE_REDUCE_ENV, PN_TT_CORE_REDUCE_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_reduce_place_cbs(&residency, placed, device->reduce.cb_buffer, + sizeof(device->reduce.cb_buffer), + &device->reduce.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the reduce circular buffers do not fit at Wt=%u\n", + norm_tile_cols); + pn_tt_residency_release(&device->reduce); + return false; + } + device->reduce_tile_cols = norm_tile_cols; + if (!pn_tt_start_family(&device->reduce)) + return false; + { + const uint32_t fill = pn_tt_env_u32("PN_TT_IDLE_FILL", 0u); + uint32_t f; + + for (f = device->idle_fill_count; f < fill && f < 64u; f++) { + pn_tt_residency *r = &device->idle_fill[f]; + + if (!pn_tt_prepare_family_on_core(device, r, PN_TT_REDUCE_PROGRAM, "idle-fill", + "PN_TT_IDLE_FILL_CORE", f, + PN_TT_ONCARD_TRIAD_MASK) || + ft_tt_op_reduce_place_cbs(&residency, placed, r->cb_buffer, + sizeof(r->cb_buffer), &r->cb_mask) != FT_TT_OK || + !pn_tt_start_family(r)) { + pn_tt_residency_release(r); + break; + } + device->idle_fill_count = f + 1u; + } + if (fill) + fprintf(stderr, "pn_tt: IDLE FILL — %u idle reduce residencies on cores 0..%u\n", + device->idle_fill_count, device->idle_fill_count - 1u); + } + /* L4: a second member over the same placement. Its failure is not fatal — the norm then + * runs whole on member 0. */ + if (device->reduce_members != 1u && pn_tt_env_u32("PN_TT_REDUCE_CORES", 2u) >= 2u /* L4 ON since 2026-10-05: decode 16.10 -> 16.28, prefill 335.3 -> 339.5, IDENTICAL */ && + (norm_tile_cols % 2u) == 0u && !device->reduce_member.running) { + device->reduce_members = 1u; + if (pn_tt_prepare_family(device, &device->reduce_member, PN_TT_REDUCE_PROGRAM, + "reduce-member", "PN_TT_CORE_REDUCE_MEMBER", 88u, + PN_TT_ONCARD_TRIAD_MASK)) { + if (ft_tt_op_reduce_place_cbs(&residency, placed, device->reduce_member.cb_buffer, + sizeof(device->reduce_member.cb_buffer), + &device->reduce_member.cb_mask) == FT_TT_OK && + pn_tt_start_family(&device->reduce_member)) + device->reduce_members = 2u; + else + pn_tt_residency_release(&device->reduce_member); + } + fprintf(stderr, "pn_tt: reduce COLUMN SPLIT — %u member(s)\n", device->reduce_members); + } + return true; +} + +/** L4: re-arm the reduce member alone (same placement as member 0). */ +static bool pn_tt_rearm_reduce_member(pn_tt_device *device) +{ + static const ft_tt_op_reduce_function members[] = { FT_TT_OP_REDUCE_FN_RMS_NORM }; + ft_tt_op_reduce_residency residency; + ft_tt_launch_cb_entry placed[FT_TT_OP_REDUCE_CB_COUNT]; + + pn_tt_residency_stop(&device->reduce_member); + pn_tt_residency_release(&device->reduce_member); + device->oncard_restarts++; + return ft_tt_op_reduce_residency_for(members, 1u, device->reduce_tile_cols, + PN_TT_TILE_BF16, &residency) == FT_TT_OK && + pn_tt_prepare_family(device, &device->reduce_member, PN_TT_REDUCE_PROGRAM, + "reduce-member", "PN_TT_CORE_REDUCE_MEMBER", 88u, + PN_TT_ONCARD_TRIAD_MASK) && + ft_tt_op_reduce_place_cbs(&residency, placed, device->reduce_member.cb_buffer, + sizeof(device->reduce_member.cb_buffer), + &device->reduce_member.cb_mask) == FT_TT_OK && + pn_tt_start_family(&device->reduce_member); +} + +/** + * The elementwise residency SET, sized for the widest SLICE and ONE depth for all. + * + * The split is planned BEFORE the placement, not after, because the placement is sized from + * the width a descriptor will actually carry — see `pn_tt_eltwise_group_plan()`. At the + * shipped `PN_TT_ELTWISE_CORES=1` the plan returns one member and the widest width, which is + * this function's pre-split behaviour word for word. + */ +static bool pn_tt_arm_eltwise(pn_tt_device *device, const uint32_t *tile_counts, uint32_t count) +{ + ft_tt_op_eltwise_request request; + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + uint32_t widest, depth, members; + + pn_tt_eltwise_group_plan(device, tile_counts, count, &members, &widest, &depth); + if (depth == 0u) { + fprintf(stderr, "pn_tt: no single block depth serves every elementwise shape — " + "SwiGLU and the residual adds stay on the host\n"); + return false; + } + + if (!pn_tt_prepare_family(device, &device->eltwise, PN_TT_ELTWISE_PROGRAM, "eltwise", + PN_TT_CORE_ELTWISE_ENV, PN_TT_CORE_ELTWISE_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + memset(&request, 0, sizeof(request)); + request.function = FT_TT_OP_VECTOR_FN_SILU_MUL; /* the widest of the six */ + request.tile_count = widest; + request.prefetch_tiles = depth; + request.input_a.tile_bytes = PN_TT_TILE_BF16; + request.input_b.tile_bytes = PN_TT_TILE_BF16; + request.output.tile_bytes = PN_TT_TILE_BF16; + if (ft_tt_op_eltwise_place_cbs(&request, placed, device->eltwise.cb_buffer, + sizeof(device->eltwise.cb_buffer), + &device->eltwise.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the elementwise circular buffers do not fit at depth %u\n", + depth); + pn_tt_residency_release(&device->eltwise); + return false; + } + device->eltwise_depth = depth; + if (!pn_tt_start_family(&device->eltwise)) + return false; + pn_tt_eltwise_group_arm(device, members, &request); + return true; +} + +/** L3: arm (or re-arm) top-k split member `m` (1-based) on `core` for a `slice`-tile row at + * `depth`, with the same greedy state block member 0 carries. */ +static bool pn_tt_arm_topk_member(pn_tt_device *device, uint32_t m, uint32_t core, + uint32_t slice, uint32_t depth) +{ + pn_tt_residency *r = &device->sample_member[m - 1u]; + ft_tt_op_topk_request request; + ft_tt_launch_cb_entry placed[FT_TT_OP_SAMPLE_CB_COUNT]; + ft_tt_op_sample_params params; + uint32_t state[FT_TT_OP_SAMPLE_STATE_WORDS]; + + memset(&request, 0, sizeof(request)); + request.tile_count = slice; + request.row = 0u; + request.select_count = PN_TT_TOPK_SELECT; + request.prefetch_tiles = depth; + request.logits.tile_bytes = PN_TT_TILE_BF16; + memset(¶ms, 0, sizeof(params)); + params.temperature = 0.0f; + params.top_p = 1.0f; + params.repeat_penalty = 1.0f; + params.rng_seed = PN_TT_TOPK_RNG_SEED; + if (ft_tt_op_sample_state_build(¶ms, state, FT_TT_OP_SAMPLE_STATE_WORDS) != FT_TT_OK || + !pn_tt_prepare_family_on_core(device, r, PN_TT_SAMPLE_PROGRAM, "topk-member", + "PN_TT_CORE_TOPK_MEMBER", core, PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_sample_place_cbs(&request, placed, r->cb_buffer, sizeof(r->cb_buffer), + &r->cb_mask) != FT_TT_OK || + !pn_tt_start_family(r)) { + pn_tt_residency_release(r); + return false; + } + device->sample_member_result[m - 1u] = placed[1].address; + device->sample_member_state[m - 1u] = placed[2].address; + if (device->runtime.noc.write(device->runtime.noc.user, r->core_x, r->core_y, + placed[2].address, state, sizeof(state)) != FT_TT_OK) { + pn_tt_residency_release(r); + return false; + } + return true; +} + +/** The sampling residency, plus the greedy state block it reads every descriptor. */ +static bool pn_tt_arm_sample(pn_tt_device *device, uint32_t vocab_tiles) +{ + ft_tt_op_topk_request request; + ft_tt_launch_cb_entry placed[FT_TT_OP_SAMPLE_CB_COUNT]; + ft_tt_op_sample_params params; + uint32_t state[FT_TT_OP_SAMPLE_STATE_WORDS]; + uint32_t depth; + int rc; + + { + /* L3: a split runs every member at ONE depth, so with `PN_TT_TOPK_CORES=N` the depth + * is the slice's (member 0 included); the whole row at that depth still divides. */ + const uint32_t split = pn_tt_env_u32("PN_TT_TOPK_CORES", 5u) /* L3 ON 2026-10-05: decode 16.43 -> 16.78, IDENTICAL */; + uint32_t basis = vocab_tiles; + + if (split >= 2u && split <= PN_TT_TOPK_MAX_MEMBERS && (vocab_tiles % split) == 0u) + basis = vocab_tiles / split; + depth = pn_tt_common_depth(&basis, 1u, PN_TT_ONCARD_MAX_DEPTH); + } + if (depth == 0u) { + fprintf(stderr, "pn_tt: no block depth serves a %u-tile logit row — top-N stays on " + "the host\n", vocab_tiles); + return false; + } + if (!pn_tt_prepare_family(device, &device->sample, PN_TT_SAMPLE_PROGRAM, "topk", + PN_TT_CORE_SAMPLE_ENV, PN_TT_CORE_SAMPLE_DEFAULT, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + memset(&request, 0, sizeof(request)); + request.tile_count = vocab_tiles; + request.row = 0u; + request.select_count = PN_TT_TOPK_SELECT; + request.prefetch_tiles = depth; + request.logits.tile_bytes = PN_TT_TILE_BF16; + if (ft_tt_op_sample_place_cbs(&request, placed, device->sample.cb_buffer, + sizeof(device->sample.cb_buffer), + &device->sample.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the sampling circular buffer does not fit at depth %u\n", depth); + pn_tt_residency_release(&device->sample); + return false; + } + device->sample_tile_count = vocab_tiles; + device->sample_depth = depth; + device->sample_result_address = placed[1].address; + device->sample_state_address = placed[2].address; + if (!pn_tt_start_family(&device->sample)) + return false; + + memset(¶ms, 0, sizeof(params)); + params.temperature = 0.0f; /* greedy: argmax on RAW values, no penalty */ + params.top_p = 1.0f; + params.repeat_penalty = 1.0f; + params.rng_seed = PN_TT_TOPK_RNG_SEED; + rc = ft_tt_op_sample_state_build(¶ms, state, FT_TT_OP_SAMPLE_STATE_WORDS); + if (rc == FT_TT_OK) + rc = device->runtime.noc.write(device->runtime.noc.user, device->sample.core_x, + device->sample.core_y, device->sample_state_address, + state, sizeof(state)); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: the sampler state block did not upload: %s\n", + ft_tt_error_string(rc)); + pn_tt_residency_release(&device->sample); + return false; + } + /* L3: the split members, each placed for its slice at member 0's depth. Not fatal. */ + { + static const uint32_t member_core[PN_TT_TOPK_MAX_MEMBERS - 1u] = { 92u, 93u, 94u, 113u, + 121u, 126u, 127u }; + const uint32_t asked = pn_tt_env_u32("PN_TT_TOPK_CORES", 5u) /* L3 ON 2026-10-05: decode 16.43 -> 16.78, IDENTICAL */; + uint32_t m; + + if (device->sample_members == 0u) + device->sample_members = 1u; + if (asked >= 2u && asked <= PN_TT_TOPK_MAX_MEMBERS && device->sample_members == 1u && + (vocab_tiles % asked) == 0u) { + uint32_t slice = vocab_tiles / asked; + uint32_t up = 1u; + + if (pn_tt_common_depth(&slice, 1u, PN_TT_ONCARD_MAX_DEPTH) < depth || + (slice % (2u * depth)) != 0u) { + fprintf(stderr, "pn_tt: top-k split over %u refused — a %u-tile slice does not " + "take depth %u\n", asked, slice, depth); + } else { + for (m = 1u; m < asked; m++) { + if (!pn_tt_arm_topk_member(device, m, member_core[m - 1u], slice, depth)) + break; + up++; + } + if (up == asked) { + device->sample_members = asked; + } else { + for (m = 1u; m < up; m++) + pn_tt_residency_release(&device->sample_member[m - 1u]); + } + fprintf(stderr, "pn_tt: top-k SPLIT — %u member(s), %u tiles each\n", + device->sample_members, vocab_tiles / device->sample_members); + } + } + } + return true; +} + +int pn_tt_device_arm_oncard(pn_tt_device *device, uint32_t norm_tile_cols, + const uint32_t *eltwise_tile_counts, uint32_t eltwise_count, + uint32_t vocab_tiles) +{ + bool eltwise_up; + + if (!device || norm_tile_cols == 0u || !eltwise_tile_counts || eltwise_count == 0u) + return -1; + if (eltwise_count > PN_TT_ONCARD_MAX_ELTWISE_SHAPES) { + fprintf(stderr, "pn_tt: %u elementwise shapes is more than the %u this device " + "records\n", eltwise_count, PN_TT_ONCARD_MAX_ELTWISE_SHAPES); + return -1; + } + pn_tt_read_oncard_selection(device); + if (pn_tt_refuse_missing_oncard_programs(device) != 0) + return -1; + /* KEPT, not consumed: pn_tt_oncard_rearm() arms a fresh residency from these. */ + memcpy(device->oncard_eltwise_tiles, eltwise_tile_counts, + (size_t)eltwise_count * sizeof(*eltwise_tile_counts)); + device->oncard_eltwise_count = eltwise_count; + + if (device->oncard_requested[PN_TT_ONCARD_NORM]) + device->oncard[PN_TT_ONCARD_NORM] = pn_tt_arm_reduce(device, norm_tile_cols); + /* ONE residency serves both elementwise members, so they arm together and are still + * selected apart: a run may move the SwiGLU and leave the residual adds on the host, + * which is what makes each a separately provable step. */ + if (device->oncard_requested[PN_TT_ONCARD_SWIGLU] || + device->oncard_requested[PN_TT_ONCARD_RESIDUAL]) { + eltwise_up = pn_tt_arm_eltwise(device, eltwise_tile_counts, eltwise_count); + device->oncard[PN_TT_ONCARD_SWIGLU] = + eltwise_up && device->oncard_requested[PN_TT_ONCARD_SWIGLU]; + device->oncard[PN_TT_ONCARD_RESIDUAL] = + eltwise_up && device->oncard_requested[PN_TT_ONCARD_RESIDUAL]; + } + if (device->oncard_requested[PN_TT_ONCARD_TOPK] && vocab_tiles != 0u) + device->oncard[PN_TT_ONCARD_TOPK] = pn_tt_arm_sample(device, vocab_tiles); + + fprintf(stderr, "pn_tt: on-card ops — norm %d, swiglu %d, residual %d, topk %d, gdn %d\n", + (int)device->oncard[PN_TT_ONCARD_NORM], (int)device->oncard[PN_TT_ONCARD_SWIGLU], + (int)device->oncard[PN_TT_ONCARD_RESIDUAL], (int)device->oncard[PN_TT_ONCARD_TOPK], + (int)device->oncard[PN_TT_ONCARD_GDN]); + return 0; +} + +/** + * Retire a residency that refused a descriptor, and arm a FRESH one on the same core from + * the shapes it was armed with. + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS, AND WHY ONLY THE MATMUL HAD IT + * --------------------------------------------------------------------------- + * A residency is BOUNDED on purpose (ft_tt_queue.h rule 3): `idle_spin_limit` retires a + * kernel whose host has stopped feeding it, because the alternative is a core held forever + * by a process that died. The matmul residency has re-armed on refusal since the resident + * queue landed (pn_tt_device.c); these three never did, because every caller so far was a + * one-shot harness that ran its tokens back to back and then exited. + * + * A `pantheon serve` is the first caller that IDLES — it arms at model load and then waits + * for a request. MEASURED on this card: the first request after a load succeeds, and one + * fired ~18 s later fails with `reduce descriptor 981 did not complete: error`, 981 being + * exactly one past the 980 reduces the first request issued. So the refusal is not a wrong + * descriptor, it is a correct descriptor arriving at a kernel that has already retired — + * and the recovery is the same ASK-and-re-arm the matmul already proved on silicon. + * + * The elementwise and reduce parameter words address DRAM only, so the caller may retry + * with the SAME words. The sampling words carry two L1 addresses on the sampling core and + * must be re-encoded after a re-arm; pn_tt_topk() does that rather than assume the fresh + * placement lands where the old one did. + * + * @return true when a fresh residency is live and the caller may retry + */ +static bool pn_tt_oncard_rearm(pn_tt_device *device, pn_tt_residency *residency) +{ + fprintf(stderr, "pn_tt: the %s residency refused a descriptor — retiring it by ASKING " + "and arming a fresh one on core %u\n", residency->name, + residency->core_index); + pn_tt_residency_stop(residency); + pn_tt_residency_release(residency); + device->oncard_restarts++; + + if (residency == &device->reduce) + return pn_tt_arm_reduce(device, device->reduce_tile_cols); + if (residency == &device->eltwise) + return pn_tt_arm_eltwise(device, device->oncard_eltwise_tiles, + device->oncard_eltwise_count); + if (residency == &device->sample) + return pn_tt_arm_sample(device, device->sample_tile_count); + return false; +} + +/* ================================================================ + * The ops + * ================================================================ */ + +int pn_tt_reduce_constants_upload(pn_tt_device *device, ft_tt_op_reduce_function function, + const float *gamma, uint32_t width, float eps, + uint32_t tile_bytes, pn_tt_norm_constants *out_constants) +{ + const bool wants_gamma = ft_tt_op_reduce_uses_gamma(function); + const ft_tt_noc_access *bulk; + ft_tt_buffer buffer; + unsigned char *constants; + size_t bytes; + size_t offset = 0; + int rc = FT_TT_OK; + + if (!device || !out_constants || width == 0u || (width % PN_TT_TILE_DIM) != 0u) + return -1; + if (wants_gamma && !gamma) + return -1; + if (pn_tt_red("norm_eps")) + eps *= 1000000.0f; /* RED PROOF: a wrong norm, everywhere, smoothly */ + bytes = ft_tt_op_reduce_constants_bytes(function, width / PN_TT_TILE_DIM, tile_bytes); + if (bytes == 0) + return -1; + constants = malloc(bytes); + if (!constants) + return -1; + /* The scaler tile, the eps tile, then gamma expanded to FULL 32x32 tiles — a norm weight + * is 1-D and the artifact stores 1-D tensors untiled, and expanding here turns the + * multiply into a plain mul_tiles instead of a broadcast op. */ + if (ft_tt_op_reduce_build_constants(function, width, eps, wants_gamma ? gamma : NULL, NULL, + tile_bytes, constants, bytes) != FT_TT_OK) { + free(constants); + return -1; + } + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, + device->strip_bank_cursor++ % device->dram_bank_count, bytes, + &buffer) != FT_TT_OK) { + free(constants); + fprintf(stderr, "pn_tt: DRAM full uploading norm constants\n"); + return -1; + } + bulk = &device->runtime.bulk_dram; + while (offset < bytes && rc == FT_TT_OK) { + size_t chunk = bytes - offset; + + if (chunk > PN_TT_ZERO_CHUNK_BYTES) + chunk = PN_TT_ZERO_CHUNK_BYTES; + rc = bulk->write(bulk->user, (uint16_t)device->dram[buffer.logical_bank].x, + (uint16_t)device->dram[buffer.logical_bank].y, + buffer.address + offset, constants + offset, chunk); + offset += chunk; + } + free(constants); + if (rc != FT_TT_OK) + return -1; + device->bytes_written += bytes; + out_constants->bank = buffer.logical_bank; + out_constants->address = (uint32_t)buffer.address; + out_constants->tile_cols = width / PN_TT_TILE_DIM; + return 0; +} + +int pn_tt_norm_constants_upload(pn_tt_device *device, const float *gamma, uint32_t width, + float eps, pn_tt_norm_constants *out_constants) +{ + return pn_tt_reduce_constants_upload(device, FT_TT_OP_REDUCE_FN_RMS_NORM, gamma, width, eps, + PN_TT_TILE_BF16, out_constants); +} + +int pn_tt_rms_norm(pn_tt_device *device, const pn_tt_norm_constants *constants, + const pn_tt_activation *input, const pn_tt_activation *output) +{ + ft_tt_op_reduce_request request; + uint32_t words[FT_TT_OP_VECTOR_WORDS]; + double started; + + if (!device || !constants || !input || !output || !device->reduce.running) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (constants->tile_cols != device->reduce_tile_cols || + input->tile_count < constants->tile_cols || output->tile_count < constants->tile_cols) { + fprintf(stderr, "pn_tt: rms_norm Wt=%u is not the Wt=%u this residency was placed " + "for\n", constants->tile_cols, device->reduce_tile_cols); + return -1; + } + + memset(&request, 0, sizeof(request)); + request.function = FT_TT_OP_REDUCE_FN_RMS_NORM; + /* ONE 32-ROW BLOCK PER BAND. A decode token is one block with 31 rows of zero padding and + * that is the shipped `1`; an Mt-band prefill strip is Mt of them, and the reduce family + * has carried the axis since it shipped — the resident kernels derive `row_blocks = + * tile_count / tile_cols` (resident_reduce_reader.c:192, _compute.c:459) and the encoder + * writes `TILE_COUNT = row_blocks * tile_cols` (ft_tt_op_vector.c:394). So the second band + * is a wider descriptor and NOT a second one, and each band is reduced on its own row — + * which is the whole correctness question, because a reduce that spanned bands would + * normalise 64 positions against one denominator. + * + * THE SPAN IS THE INPUT'S AND IT IS CLAMPED BY THE OUTPUT'S, because a norm that read two + * bands and wrote one walks off the end of the destination buffer. */ + request.row_blocks = input->active_rows ? input->active_rows : 1u; + if (output->active_rows && output->active_rows < request.row_blocks) + request.row_blocks = output->active_rows; + request.tile_cols = constants->tile_cols; + request.prefetch_tiles = constants->tile_cols; + request.input = pn_tt_operand(device, input); + request.constants.x = (uint16_t)device->dram[constants->bank].x; + request.constants.y = (uint16_t)device->dram[constants->bank].y; + request.constants.address = constants->address; + request.constants.tile_bytes = PN_TT_TILE_BF16; + request.output = pn_tt_operand(device, output); + if (ft_tt_op_reduce_parameters(&request, words) != FT_TT_OK) + return -1; + pn_tt_vec_probe_apply(words); + /* M3 HALF TILE (word 3 bit 23, resident_vector_protocol.h ftv_half_tile): a single-token + * forward's elementwise FPU passes run on faces 0-1. `PN_TT_REDUCE_HALF` 0 off, 1 (default) + * honour the single-row promise, 2 RED: every descriptor, multi-row prefill too. */ + { + static int half_mode = -1; + + if (half_mode < 0) + half_mode = (int)pn_tt_env_u32("PN_TT_REDUCE_HALF", 1u); + if (half_mode == 2 || (half_mode == 1 && (device->single_row || + (device->verify_rows && device->verify_rows <= 16u)))) + words[FT_TT_OP_VECTOR_DEPTH] |= 1u << 23; + } + + started = pn_tt_now_seconds(); + if (device->reduce_members == 2u && device->reduce_member.running && + (constants->tile_cols % 2u) == 0u) { + /* L4 COLUMN SPLIT: both members reduce the whole row; member m finishes half m. RED + * `reduce_slice_dup`: member 1 also writes half 0, so half 1 is never written. */ + uint32_t second[FT_TT_OP_VECTOR_WORDS]; + int rc0, rc1 = 0; + + memcpy(second, words, sizeof(second)); + words[FT_TT_OP_REDUCE_FINAL] = (words[FT_TT_OP_REDUCE_FINAL] & 0x00FFFFFFu) | + (0x20u << 24); + second[FT_TT_OP_REDUCE_FINAL] = + (second[FT_TT_OP_REDUCE_FINAL] & 0x00FFFFFFu) | + ((pn_tt_red("reduce_slice_dup") ? 0x20u : 0x21u) << 24); + rc0 = pn_tt_residency_enqueue(&device->reduce, FT_TT_QUEUE_OP_REDUCE, words, 1u); + if (pn_tt_residency_enqueue(&device->reduce_member, FT_TT_QUEUE_OP_REDUCE, second, 1u) != + 0) + rc1 = -1; + if (rc0 == 0 && pn_tt_residency_await(&device->reduce, PN_TT_OP_TIMEOUT_MS) != 0) + rc0 = -1; + if (rc1 == 0 && pn_tt_residency_await(&device->reduce_member, PN_TT_OP_TIMEOUT_MS) != 0) + rc1 = -1; + if (rc0 != 0 && (!pn_tt_oncard_rearm(device, &device->reduce) || + pn_tt_residency_submit(&device->reduce, FT_TT_QUEUE_OP_REDUCE, words, + PN_TT_OP_TIMEOUT_MS) != 0)) + return -1; + if (rc1 != 0 && (!pn_tt_rearm_reduce_member(device) || + pn_tt_residency_submit(&device->reduce_member, FT_TT_QUEUE_OP_REDUCE, + second, PN_TT_OP_TIMEOUT_MS) != 0)) + return -1; + pn_tt_oncard_account(device, "reduce", "rms_norm", constants->tile_cols, + pn_tt_now_seconds() - started); + return 0; + } + if (pn_tt_residency_submit(&device->reduce, FT_TT_QUEUE_OP_REDUCE, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + /* The words address DRAM only, so the same descriptor is valid for a fresh + * residency; nothing about it depended on the retired kernel. */ + if (!pn_tt_oncard_rearm(device, &device->reduce) || + pn_tt_residency_submit(&device->reduce, FT_TT_QUEUE_OP_REDUCE, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + pn_tt_oncard_account(device, "reduce", "rms_norm", constants->tile_cols, + pn_tt_now_seconds() - started); + return 0; +} + +/** One elementwise descriptor. `b` is NULL for a unary function. */ +static int pn_tt_eltwise(pn_tt_device *device, ft_tt_op_vector_function function, + const pn_tt_activation *a, const pn_tt_activation *b, + const pn_tt_activation *output) +{ + ft_tt_op_eltwise_request request; + uint32_t words[FT_TT_OP_VECTOR_WORDS]; + double started; + uint32_t tiles; + + if (!device || !a || !output || !device->eltwise.running) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (b && b->tile_count < a->tile_count) + return -1; + if (output->tile_count < a->tile_count) + return -1; + /* AN ELEMENTWISE OP HAS NO ROW AXIS AT ALL, so Mt bands are one descriptor of Mt times the + * tiles and the tile INDEX arithmetic is the same arithmetic. THE SPAN IS THE NARROWEST OF + * THE THREE OPERANDS: one of them is often a single-band scratch — the GDN block's + * `gdn_y_bridge` is written by a render inside one mixer pass — and widening the + * descriptor past it would read tiles nothing wrote. + * + * The CB placement does not move with this: the elementwise buffers are `depth * + * ELTWISE_CB_BLOCKS` pages (ft_tt_op_vector.c), sized from the block depth and never from + * the tile count, so a wider descriptor is more blocks through the same buffer. The depth + * condition below is preserved by a multiple: `n % 2d == 0` implies `k*n % 2d == 0`. */ + tiles = a->active_rows ? a->active_rows : 1u; + if (b && b->active_rows && b->active_rows < tiles) + tiles = b->active_rows; + if (output->active_rows && output->active_rows < tiles) + tiles = output->active_rows; + tiles *= a->tile_count; + /* The residency's ONE depth has to serve this descriptor too. Refusing here is a return + * value; discovering it on the card costs the residency. */ + if ((tiles % (2u * device->eltwise_depth)) != 0u || + ft_tt_op_block_depth(tiles, device->eltwise_depth) != device->eltwise_depth) { + fprintf(stderr, "pn_tt: %u tiles is not a whole number of depth-%u blocks\n", + tiles, device->eltwise_depth); + return -1; + } + + memset(&request, 0, sizeof(request)); + request.function = function; + request.tile_count = tiles; + request.prefetch_tiles = device->eltwise_depth; + { + static int row0_mode = -1; + + if (row0_mode < 0) + row0_mode = (int)pn_tt_env_u32("PN_TT_ELTWISE_ROW0", 1u); + request.row0 = row0_mode == 2 || + (row0_mode == 1 && + (device->single_row || + (device->verify_rows && device->verify_rows <= 4u))) + ? 1u : 0u; + } + request.input_a = pn_tt_operand(device, a); + if (b) + request.input_b = pn_tt_operand(device, b); + request.output = pn_tt_operand(device, output); + /* THE SPLIT TAKES EVERY DESCRIPTOR OR NONE. The residency was placed for the sliced + * width, so there is no per-descriptor choice to make here — one member is the whole + * pre-split path and N members is this same request over N disjoint tile ranges. */ + /* AND THE SPLIT TAKES A BAND-WIDE DESCRIPTOR WITHOUT KNOWING ABOUT BANDS, because the + * slice is `[member * tiles/members, ...)` address arithmetic over an operation with no + * row axis (pn_tt_eltwise_split.c). At `Mt` bands over `Mt` members each member gets + * EXACTLY the single-band width the residency was armed for, and the CBs were sized from + * the block DEPTH and never from the width, so a wider descriptor is more blocks through + * the same buffer. A width the set does not divide already runs whole on member 0 by its + * own rule. */ + if (pn_tt_eltwise_group_members(device) > 1u) + return pn_tt_eltwise_group_submit(device, &request); + /* PM 7: deferral is the group's; unsplit, every descriptor is awaited here. */ + device->eltwise_defer_next = false; + if (pn_tt_eltwise_group_settle(device) != 0 || + ft_tt_op_eltwise_parameters(&request, words) != FT_TT_OK) + return -1; + pn_tt_vec_probe_apply(words); + + started = pn_tt_now_seconds(); + if (pn_tt_residency_submit(&device->eltwise, FT_TT_QUEUE_OP_ELTWISE, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + if (!pn_tt_oncard_rearm(device, &device->eltwise) || + pn_tt_residency_submit(&device->eltwise, FT_TT_QUEUE_OP_ELTWISE, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + pn_tt_oncard_account(device, "eltwise", ft_tt_op_vector_function_name(function), tiles, + pn_tt_now_seconds() - started); + return 0; +} + +int pn_tt_add(pn_tt_device *device, const pn_tt_activation *a, const pn_tt_activation *b, + const pn_tt_activation *output) +{ + /* RED PROOF: the residual becomes a subtraction. */ + return pn_tt_eltwise(device, pn_tt_red("residual_sub") ? FT_TT_OP_VECTOR_FN_SUB + : FT_TT_OP_VECTOR_FN_ADD, + a, b, output); +} + +int pn_tt_silu_mul(pn_tt_device *device, const pn_tt_activation *a, const pn_tt_activation *b, + const pn_tt_activation *output) +{ + /* RED PROOF: the two operands of the gate swap, which is the mutation a SwiGLU is most + * likely to suffer for real and the one a shape check cannot see — both are n_ff. */ + if (pn_tt_red("swiglu_swap")) + return pn_tt_eltwise(device, FT_TT_OP_VECTOR_FN_SILU_MUL, b, a, output); + return pn_tt_eltwise(device, FT_TT_OP_VECTOR_FN_SILU_MUL, a, b, output); +} + +int pn_tt_sigmoid_mul(pn_tt_device *device, const pn_tt_activation *gate, + const pn_tt_activation *b, const pn_tt_activation *scratch, + const pn_tt_activation *output) +{ + if (!scratch) + return -1; + /* TWO DESCRIPTORS, NOT A SEVENTH TABLE ROW. `sigmoid(a)*b` is NOT `silu(a)*b` + * (`silu(a) = a*sigmoid(a)`), so the SwiGLU member cannot serve Ornith's attention gate — + * but SIGMOID and MUL are both already in the elementwise kernel's switch, so the pair + * costs one extra descriptor per attention layer (8 a token, ~2.4 ms) and NO kernel + * source change, no program rebuild and no blob regeneration. A fused + * `FT_TT_OP_VECTOR_FN_SIGMOID_MUL` would save that 2.4 ms and cost the whole provenance + * chain; it is worth doing when something else is already regenerating the blob. + * + * RED PROOF: run the gate as SILU instead of SIGMOID. Same shapes, same descriptor + * count, same operands — only the function differs, which is exactly the mistake the + * SwiGLU next door invites and which no shape check can see. */ + if (pn_tt_eltwise(device, pn_tt_red("attn_gate_silu") ? FT_TT_OP_VECTOR_FN_SILU + : FT_TT_OP_VECTOR_FN_SIGMOID, + gate, NULL, scratch) != 0) + return -1; + return pn_tt_eltwise(device, FT_TT_OP_VECTOR_FN_MUL, scratch, b, output); +} + +/** The kernel's order: key(bf16) descending, then index ascending (resident_topk_protocol.h). */ +static uint32_t pn_tt_topk_key(float value) +{ + uint32_t bits; + uint32_t u; + + memcpy(&bits, &value, sizeof(bits)); + u = bits >> 16; + return (u & 0x8000u) ? (uint16_t)~u : (uint16_t)(u | 0x8000u); +} + +/** L3: every member selects over its slice, the host merges exactly. */ +/* S2e (`PN_TT_TOPK_BULK`, default 1): the result block in ONE bulk L1 read (16 B-rounded) + * instead of the register accessor's word by word. */ +static int pn_tt_topk_read_block(pn_tt_device *device, uint32_t x, uint32_t y, uint32_t address, + uint32_t *block, size_t bytes) +{ + static int bulk = -1; + uint32_t staged[(FT_TT_OP_SAMPLE_RESULT_BYTES + 15u) / 4u]; + const size_t rounded = (bytes + 15u) & ~(size_t)15u; + int rc; + + if (bulk < 0) + bulk = (int)pn_tt_env_u32("PN_TT_TOPK_BULK", 1u); + if (!bulk || (address & 15u) != 0u || rounded > sizeof(staged) || !device->runtime.bulk_l1.read) + return device->runtime.noc.read(device->runtime.noc.user, (uint16_t)x, (uint16_t)y, + address, block, bytes); + rc = device->runtime.bulk_l1.read(device->runtime.bulk_l1.user, (uint16_t)x, (uint16_t)y, + address, staged, rounded); + if (rc == FT_TT_OK) + memcpy(block, staged, bytes); + return rc; +} + +static int pn_tt_topk_split(pn_tt_device *device, const pn_tt_activation *logits, + uint32_t count, uint32_t *out_indices, float *out_values, + uint32_t *out_count) +{ + const uint32_t members = device->sample_members; + const uint32_t slice = device->sample_tile_count / members; + uint32_t all_index[PN_TT_TOPK_MAX_MEMBERS * PN_TT_TOPK_SELECT_MAX]; + float all_value[PN_TT_TOPK_MAX_MEMBERS * PN_TT_TOPK_SELECT_MAX]; + uint32_t total = 0u, m, k; + const bool red = pn_tt_red("topk_split_no_offset"); + double started = pn_tt_now_seconds(); + int failed = 0; + + for (m = 0; m < members; m++) { + pn_tt_residency *r = m == 0u ? &device->sample : &device->sample_member[m - 1u]; + ft_tt_op_topk_request request; + uint32_t words[FT_TT_OP_SAMPLE_WORDS]; + + memset(&request, 0, sizeof(request)); + request.tile_count = slice; + request.row = device->topk_row; + request.select_count = count; + request.prefetch_tiles = device->sample_depth; + request.logits = pn_tt_operand(device, logits); + request.logits.address += m * slice * PN_TT_TILE_BF16; + request.result_address = + m == 0u ? device->sample_result_address : device->sample_member_result[m - 1u]; + request.state_address = + m == 0u ? device->sample_state_address : device->sample_member_state[m - 1u]; + if (ft_tt_op_sample_parameters(&request, words) != FT_TT_OK || + pn_tt_residency_enqueue(r, FT_TT_QUEUE_OP_TOPK, words, 1u) != 0) + failed = 1; + } + for (m = 0; m < members; m++) { + pn_tt_residency *r = m == 0u ? &device->sample : &device->sample_member[m - 1u]; + + if (pn_tt_residency_await(r, PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + if (failed) { + /* A member that idled out (a long prefill) or refused: re-arm the set and run the + * whole split once more — TOPK has no side effect but its result blocks. */ + static int retrying; + + if (retrying) + return -1; + if (!pn_tt_oncard_rearm(device, &device->sample)) + return -1; + for (m = 1u; m < members; m++) { + const uint32_t core = device->sample_member[m - 1u].core_index; + + pn_tt_residency_stop(&device->sample_member[m - 1u]); + pn_tt_residency_release(&device->sample_member[m - 1u]); + device->oncard_restarts++; + if (!pn_tt_arm_topk_member(device, m, core, slice, device->sample_depth)) + return -1; + } + retrying = 1; + failed = pn_tt_topk_split(device, logits, count, out_indices, out_values, out_count); + retrying = 0; + return failed; + } + for (m = 0; m < members; m++) { + const pn_tt_residency *r = m == 0u ? &device->sample : &device->sample_member[m - 1u]; + uint32_t block[FT_TT_OP_SAMPLE_RESULT_HEADER_WORDS + + PN_TT_TOPK_SELECT_MAX * FT_TT_OP_SAMPLE_PAIR_WORDS]; + uint32_t indices[PN_TT_TOPK_SELECT_MAX]; + float values[PN_TT_TOPK_SELECT_MAX]; + const size_t bytes = (FT_TT_OP_SAMPLE_RESULT_HEADER_WORDS + + count * FT_TT_OP_SAMPLE_PAIR_WORDS) * sizeof(uint32_t); + uint32_t produced = 0u; + + if (pn_tt_topk_read_block(device, r->core_x, r->core_y, + m == 0u ? device->sample_result_address + : device->sample_member_result[m - 1u], + block, bytes) != FT_TT_OK || + ft_tt_op_sample_result_decode(block, count, indices, values, &produced, + NULL) != FT_TT_OK) + return -1; + device->bytes_read += bytes; + for (k = 0; k < produced; k++) { + all_index[total] = indices[k] + (red ? 0u : m * slice * 32u); + all_value[total] = values[k]; + total++; + } + } + /* Insertion sort, the kernel's comparator: key desc, then index asc. */ + for (k = 1; k < total; k++) { + const uint32_t ix = all_index[k]; + const float v = all_value[k]; + const uint32_t key = pn_tt_topk_key(v); + uint32_t j = k; + + while (j > 0u && (pn_tt_topk_key(all_value[j - 1u]) < key || + (pn_tt_topk_key(all_value[j - 1u]) == key && all_index[j - 1u] > ix))) { + all_index[j] = all_index[j - 1u]; + all_value[j] = all_value[j - 1u]; + j--; + } + all_index[j] = ix; + all_value[j] = v; + } + if (total > count) + total = count; + for (k = 0; k < total; k++) { + out_indices[k] = all_index[k]; + out_values[k] = all_value[k]; + } + *out_count = total; + pn_tt_oncard_account(device, "topk", "select", device->sample_tile_count, + pn_tt_now_seconds() - started); + return 0; +} + +int pn_tt_topk(pn_tt_device *device, const pn_tt_activation *logits, uint32_t count, + uint32_t *out_indices, float *out_values, uint32_t *out_count) +{ + uint32_t block[FT_TT_OP_SAMPLE_RESULT_HEADER_WORDS + + PN_TT_TOPK_SELECT_MAX * FT_TT_OP_SAMPLE_PAIR_WORDS]; + const size_t bytes = (FT_TT_OP_SAMPLE_RESULT_HEADER_WORDS + + count * FT_TT_OP_SAMPLE_PAIR_WORDS) * sizeof(uint32_t); + ft_tt_op_topk_request request; + uint32_t words[FT_TT_OP_SAMPLE_WORDS]; + double started; + int rc; + + if (!device || !logits || !out_indices || !out_values || !out_count) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + if (count == 0u || count > PN_TT_TOPK_SELECT_MAX || !device->sample.running) + return -1; + if (logits->tile_count < device->sample_tile_count) + return -1; + if (device->sample_members >= 2u) + return pn_tt_topk_split(device, logits, count, out_indices, out_values, out_count); + + memset(&request, 0, sizeof(request)); + request.tile_count = device->sample_tile_count; + /* a matvec is Mt=1; the token's logits are logical row 0. RED PROOF: row 1 is the tile + * quantum's zero padding, so selecting over it returns an index of nothing. */ + request.row = pn_tt_red("topk_row") ? 1u : device->topk_row; + request.select_count = count; + request.prefetch_tiles = device->sample_depth; + request.logits = pn_tt_operand(device, logits); + request.result_address = device->sample_result_address; + request.state_address = device->sample_state_address; + if (ft_tt_op_sample_parameters(&request, words) != FT_TT_OK) + return -1; + + started = pn_tt_now_seconds(); + if (pn_tt_residency_submit(&device->sample, FT_TT_QUEUE_OP_TOPK, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + /* Unlike the other two families, these words carry L1 addresses on the sampling + * core, so they are RE-ENCODED against the fresh placement rather than reused. */ + if (!pn_tt_oncard_rearm(device, &device->sample)) + return -1; + request.prefetch_tiles = device->sample_depth; + request.result_address = device->sample_result_address; + request.state_address = device->sample_state_address; + if (ft_tt_op_sample_parameters(&request, words) != FT_TT_OK || + pn_tt_residency_submit(&device->sample, FT_TT_QUEUE_OP_TOPK, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + /* The result block is in L1 on the sampling core, so it takes the REGISTER accessor — + * the bulk one refuses a non-DRAM address by design. It is ~96 bytes against the + * 261,120 the logit row would have been. */ + rc = pn_tt_topk_read_block(device, device->sample.core_x, device->sample.core_y, + device->sample_result_address, block, bytes); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: the top-N result block did not read back: %s\n", + ft_tt_error_string(rc)); + return -1; + } + device->bytes_read += bytes; + { + uint32_t indices[PN_TT_TOPK_SELECT_MAX]; + float values[PN_TT_TOPK_SELECT_MAX]; + uint32_t produced = 0u, index; + + if (ft_tt_op_sample_result_decode(block, count, indices, values, &produced, + NULL) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the top-N result block is not one (bad magic)\n"); + return -1; + } + if (produced > count) + produced = count; + for (index = 0; index < produced; index++) { + out_indices[index] = indices[index]; + out_values[index] = values[index]; + } + *out_count = produced; + } + pn_tt_oncard_account(device, "topk", "select", device->sample_tile_count, + pn_tt_now_seconds() - started); + return 0; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_reader.c b/src/accel/tt/pn_tt_reader.c new file mode 100644 index 0000000..7a1a7f0 --- /dev/null +++ b/src/accel/tt/pn_tt_reader.c @@ -0,0 +1,362 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_reader.c + * @brief Implementation of the reader thread. See pn_tt_reader.h for why and what it may touch. + * + * @status Written for PHASE W (chain step). + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_reader.h" + +#include +#include +#include +#include +#include +#include + +#include "pn_tt_device_internal.h" + +#include "tt/convert/ft_float_convert.h" +#include "tt/ft_tt_runtime_udrv.h" +#include "tt/ft_tt_queue.h" +#include "tt/ft_tt_tilize.h" + +/** Jobs in flight: a layer posts two reads a band, 51 bands at 1,630 rows. */ +#define READER_RING 256u +/** One job's staged tiles; alpha/beta are one tile, this covers a band of up to 8. */ +#define READER_MAX_TILES 8u + +typedef struct reader_job { + uint16_t x, y; + uint32_t address, tiles, rows, count; + float *dst; + pn_tt_reader_gate gate; /**< target 0 = none */ +} reader_job; + +/** One reader thread on its own window bank, with its own ring: jobs are dealt round-robin, + * so a ticket names its worker (`(ticket - 1) % workers`) and its place in that ring. */ +typedef struct reader_worker { + pthread_t thread; + ft_tt_noc_access access; + ft_tt_noc_access l1; /**< the same bank, L1 addresses: the gate polls */ + bool has_l1; + reader_job ring[READER_RING]; + _Atomic uint64_t posted; /**< jobs written (producer) */ + _Atomic uint64_t done; /**< jobs finished (consumer) */ + _Atomic uint64_t failed_at; /**< local index of the last failed job, 0 = none */ + _Atomic int *stop; + _Atomic int *cancel; /**< gates fail at once (an abandon: their work may never run) */ + /** IDLE SLEEPS, never spins: a spinning reader took a core from decode on this 2-core + * host (dec48 38.7 -> 36.3 tok/s with two of them, run_w_stream2.sh). */ + pthread_mutex_t lock; + pthread_cond_t wake; + _Atomic int sleeping; + double read_seconds; + uint64_t reads; + unsigned char stage[READER_MAX_TILES * PN_TT_TILE_BF16]; +} reader_worker; + +/** `PN_TT_READER_THREADS` (default 1: two did not read faster — the link serialises PIO + * reads — and cost decode, run_w_stream2.sh; at most the udrv reader banks opened). */ +#define READER_MAX_WORKERS 4u + +struct pn_tt_reader { + reader_worker worker[READER_MAX_WORKERS]; + uint32_t workers; + uint64_t tickets; /**< posted over all workers (producer) */ + _Atomic int stop; + _Atomic int cancel; + double wait_seconds; /**< producer side */ +}; + +/** Poll the gate's completion word until it reaches its target (5 s bound). */ +static int reader_gate_wait(reader_worker *w, const pn_tt_reader_gate *gate) +{ + const double deadline = pn_tt_now_seconds() + 5.0; + uint32_t line[4]; /* fields 4..7, the 16-byte completion line ft_tt_queue reads */ + uint32_t count; + + if (gate->target == 0u) + return 0; + if (!w->has_l1) + return -1; + for (;;) { + if (w->l1.read(w->l1.user, gate->x, gate->y, + gate->control_address + FT_TT_QUEUE_FIELD_STOP_REQUEST * 4u, line, + sizeof(line)) != FT_TT_OK) + return -1; + count = line[FT_TT_QUEUE_FIELD_COMPLETED_COUNT - FT_TT_QUEUE_FIELD_STOP_REQUEST]; + /* Counts only grow within a run; the difference is wrap-safe. */ + if ((int32_t)(count - gate->target) >= 0) + return 0; + if (pn_tt_now_seconds() > deadline || + atomic_load_explicit(w->cancel, memory_order_relaxed)) + return -1; + } +} + +static int reader_run_job(reader_worker *w, const reader_job *job) +{ + uint32_t tile, row, column; + + if (reader_gate_wait(w, &job->gate) != 0) + return -1; + if (job->rows <= 16u) { + /* rows <= 16 live in faces 0-1: two 64 B-rounded runs a tile (PN_TT_ROWS_FACES's + * shape, pn_tt_activation_download_rows()) instead of the whole 2 KiB. */ + const size_t run = ((size_t)job->rows * 32u + 63u) & ~(size_t)63u; + + for (tile = 0; tile < job->tiles; tile++) + if (w->access.read(w->access.user, job->x, job->y, + job->address + tile * PN_TT_TILE_BF16, + w->stage + (size_t)tile * PN_TT_TILE_BF16, run) != FT_TT_OK || + w->access.read(w->access.user, job->x, job->y, + job->address + tile * PN_TT_TILE_BF16 + 512u, + w->stage + (size_t)tile * PN_TT_TILE_BF16 + 512u, run) != FT_TT_OK) + return -1; + } else if (w->access.read(w->access.user, job->x, job->y, job->address, w->stage, + (size_t)job->tiles * PN_TT_TILE_BF16) != FT_TT_OK) { + return -1; + } + for (tile = 0; tile < job->tiles; tile++) { + const uint16_t *base = (const uint16_t *)w->stage + (size_t)tile * (PN_TT_TILE_BF16 / 2u); + + for (row = 0; row < job->rows; row++) + for (column = 0; column < PN_TT_TILE_DIM; column++) + job->dst[(size_t)row * job->count + tile * PN_TT_TILE_DIM + column] = + ft_float_from_bf16(base[ft_tt_tile_face_index(row, column)]); + } + return 0; +} + +static void *reader_main(void *arg) +{ + reader_worker *w = (reader_worker *)arg; + + for (;;) { + const uint64_t done = atomic_load_explicit(&w->done, memory_order_relaxed); + + if (done == atomic_load_explicit(&w->posted, memory_order_acquire)) { + if (atomic_load_explicit(w->stop, memory_order_acquire)) + break; + pthread_mutex_lock(&w->lock); + atomic_store_explicit(&w->sleeping, 1, memory_order_seq_cst); + while (done == atomic_load_explicit(&w->posted, memory_order_seq_cst) && + !atomic_load_explicit(w->stop, memory_order_seq_cst)) + pthread_cond_wait(&w->wake, &w->lock); + atomic_store_explicit(&w->sleeping, 0, memory_order_seq_cst); + pthread_mutex_unlock(&w->lock); + continue; + } + { + const double at = pn_tt_now_seconds(); + + if (reader_run_job(w, &w->ring[done % READER_RING]) != 0) + atomic_store_explicit(&w->failed_at, done + 1u, memory_order_relaxed); + w->read_seconds += pn_tt_now_seconds() - at; + w->reads++; + } + atomic_store_explicit(&w->done, done + 1u, memory_order_release); + } + return NULL; +} + +/** Start up to `PN_TT_READER_THREADS` workers, one per reader bank that opened. */ +static struct pn_tt_reader *reader_start(pn_tt_device *device) +{ + struct pn_tt_reader *r = calloc(1, sizeof(*r)); + const uint32_t want = pn_tt_env_u32("PN_TT_READER_THREADS", 1u); + uint32_t i; + + if (!r || !device->runtime.udrv) + return (free(r), NULL); + for (i = 0; i < want && i < READER_MAX_WORKERS; i++) { + reader_worker *w = &r->worker[i]; + + if (ft_tt_runtime_udrv_reader_access(device->runtime.udrv, i, &w->access) != FT_TT_OK) + break; + w->has_l1 = ft_tt_runtime_udrv_reader_l1_access(device->runtime.udrv, i, &w->l1) == + FT_TT_OK; + w->stop = &r->stop; + w->cancel = &r->cancel; + pthread_mutex_init(&w->lock, NULL); + pthread_cond_init(&w->wake, NULL); + if (pthread_create(&w->thread, NULL, reader_main, w) != 0) + break; + r->workers++; + } + if (r->workers == 0u) + return (free(r), NULL); + return r; +} + +bool pn_tt_reader_available(pn_tt_device *device) +{ + static int enabled = -1; + + if (!device) + return false; + if (enabled < 0) { + const char *value = getenv("PN_TT_READER"); + + enabled = (value && *value) ? (strcmp(value, "0") != 0) : 1; + } + if (!enabled || device->reader_failed) + return false; + if (device->reader) + return true; + device->reader = reader_start(device); + if (!device->reader) { + device->reader_failed = true; + fprintf(stderr, "pn_tt: reader thread NOT started (no reader window bank) — strip reads " + "stay on the issuing thread\n"); + return false; + } + fprintf(stderr, "pn_tt: %u reader thread(s) started, each on its own window bank\n", + device->reader->workers); + return true; +} + +uint64_t pn_tt_reader_post_rows(pn_tt_device *device, uint32_t bank, uint32_t address, + uint32_t tiles, uint32_t rows, float *dst, uint32_t count) +{ + return pn_tt_reader_post_rows_gated(device, bank, address, tiles, rows, dst, count, NULL); +} + +uint64_t pn_tt_reader_post_rows_gated(pn_tt_device *device, uint32_t bank, uint32_t address, + uint32_t tiles, uint32_t rows, float *dst, + uint32_t count, const pn_tt_reader_gate *gate) +{ + struct pn_tt_reader *r; + reader_worker *w; + uint64_t posted; + reader_job *job; + + if (!pn_tt_reader_available(device) || !dst || tiles == 0u || tiles > READER_MAX_TILES || + rows == 0u || rows > PN_TT_TILE_DIM || count < tiles * PN_TT_TILE_DIM || + bank >= device->dram_bank_count) + return 0u; + r = device->reader; + w = &r->worker[r->tickets % r->workers]; + posted = atomic_load_explicit(&w->posted, memory_order_relaxed); + /* A full ring waits for a slot rather than overwriting one the thread has not read. */ + while (posted - atomic_load_explicit(&w->done, memory_order_acquire) >= READER_RING) + sched_yield(); + job = &w->ring[posted % READER_RING]; + job->x = (uint16_t)device->dram[bank].x; + job->y = (uint16_t)device->dram[bank].y; + job->address = address; + job->tiles = tiles; + job->rows = rows; + job->count = count; + job->dst = dst; + if (gate) + job->gate = *gate; + else + memset(&job->gate, 0, sizeof(job->gate)); + atomic_store_explicit(&w->posted, posted + 1u, memory_order_seq_cst); + if (atomic_load_explicit(&w->sleeping, memory_order_seq_cst)) { + pthread_mutex_lock(&w->lock); + pthread_cond_signal(&w->wake); + pthread_mutex_unlock(&w->lock); + } + return ++r->tickets; +} + +int pn_tt_reader_wait(pn_tt_device *device, uint64_t ticket) +{ + struct pn_tt_reader *r = device ? device->reader : NULL; + reader_worker *w; + uint64_t local; + double at; + + if (!r || ticket == 0u || ticket > r->tickets) + return -1; + w = &r->worker[(ticket - 1u) % r->workers]; + local = (ticket - 1u) / r->workers + 1u; + at = pn_tt_now_seconds(); + while (atomic_load_explicit(&w->done, memory_order_acquire) < local) + sched_yield(); + r->wait_seconds += pn_tt_now_seconds() - at; + /* failed_at only ever names the LAST failure; any failure at or after this job's place + * is treated as this one's, which errs toward re-reading inline. */ + return atomic_load_explicit(&w->failed_at, memory_order_relaxed) >= local ? -1 : 0; +} + +void pn_tt_reader_drain_cancelled(pn_tt_device *device) +{ + struct pn_tt_reader *r = device ? device->reader : NULL; + + if (!r) + return; + atomic_store_explicit(&r->cancel, 1, memory_order_seq_cst); + pn_tt_reader_drain(device); + atomic_store_explicit(&r->cancel, 0, memory_order_seq_cst); +} + +void pn_tt_reader_drain(pn_tt_device *device) +{ + struct pn_tt_reader *r = device ? device->reader : NULL; + uint64_t t; + + if (!r) + return; + for (t = r->tickets; t > 0u && t + r->workers > r->tickets; t--) + (void)pn_tt_reader_wait(device, t); +} + +void pn_tt_reader_close(pn_tt_device *device) +{ + struct pn_tt_reader *r = device ? device->reader : NULL; + double read_seconds = 0.0; + uint64_t reads = 0u; + uint32_t i; + + if (!r) + return; + atomic_store_explicit(&r->stop, 1, memory_order_seq_cst); + for (i = 0; i < r->workers; i++) { + pthread_mutex_lock(&r->worker[i].lock); + pthread_cond_signal(&r->worker[i].wake); + pthread_mutex_unlock(&r->worker[i].lock); + pthread_join(r->worker[i].thread, NULL); + pthread_cond_destroy(&r->worker[i].wake); + pthread_mutex_destroy(&r->worker[i].lock); + read_seconds += r->worker[i].read_seconds; + reads += r->worker[i].reads; + } + if (reads) + fprintf(stderr, "pn_tt: reader — %u thread(s), %llu reads, %.3f s reading (summed), " + "%.3f s waited on\n", + r->workers, (unsigned long long)reads, read_seconds, r->wait_seconds); + free(r); + device->reader = NULL; +} + +void pn_tt_reader_stats(const pn_tt_device *device, double *out_wait_seconds, + uint64_t *out_reads, double *out_read_seconds) +{ + const struct pn_tt_reader *r = device ? device->reader : NULL; + uint32_t i; + + if (out_wait_seconds) + *out_wait_seconds = r ? r->wait_seconds : 0.0; + if (out_reads) + *out_reads = 0u; + if (out_read_seconds) + *out_read_seconds = 0.0; + for (i = 0; r && i < r->workers; i++) { + if (out_reads) + *out_reads += r->worker[i].reads; + if (out_read_seconds) + *out_read_seconds += r->worker[i].read_seconds; + } +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_reader.h b/src/accel/tt/pn_tt_reader.h new file mode 100644 index 0000000..8fcf6c7 --- /dev/null +++ b/src/accel/tt/pn_tt_reader.h @@ -0,0 +1,86 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_reader.h + * @brief A READER THREAD on its own window bank: card->host strip reads (with the host + * de-tilize) taken off the thread that issues descriptors. + * + * WHY. The prefill's GDN chain step reads `ssm_alpha`/`ssm_beta` down a 32-row band at a time + * (the host owes softplus/exp/sigmoid): 2,448 tile reads, ~0.94 s of a 1,630-token prefill at + * the link's ~5.3 MB/s readback, and a PIO read stalls the CPU that issues it. Hidden under the + * gdnc windows (the alpha/beta prefetch) the reads still EXTEND those windows — the chain is + * 2.08 s, 1.0 s of it unclocked. The host has two cores and the second one sits idle. A read + * is the same bytes whichever core issues it, so the answer is bit-identical by construction. + * + * HOW IT STAYS SAFE. The thread owns a THIRD udrv window bank + * (`ft_tt_runtime_udrv_reader_access()`); it never touches the issuing thread's banks, the + * device's counters or a strip struct (addresses are resolved at post time on the caller's + * thread). One producer (the caller), one consumer (the thread), a fixed ring, C11 atomics. + * `PN_TT_READER=0` disables it; so does a transport without the reader bank (tt-kmd), and + * every caller then reads inline as before. `PN_TT_READER_THREADS=N` (default 1): N workers, + * each on its own bank, jobs dealt round-robin. Two read no faster (the link serialises PIO + * reads: 1.95 s summed vs 1.10 for one) — kept as the measured control. Idle workers sleep. + * + * @status Shipped (PHASE W); gated by run_w1a_eltwise_wide.sh, run_w_stream2.sh. + * @issues None + * @todo None + */ + +#ifndef PN_TT_READER_H +#define PN_TT_READER_H + +#ifdef USE_TT + +#include +#include + +typedef struct pn_tt_device pn_tt_device; + +/** True when posts will run on the reader thread (starting it on first use). */ +bool pn_tt_reader_available(pn_tt_device *device); + +/** + * Queue a read of `tiles` bf16 tiles at (`bank`, `address`) and the de-tilize of their first + * `rows` rows into `dst` (row stride `count` floats, tile t's columns at t*32) — exactly what + * `pn_tt_activation_download_rows()` computes for one band. + * + * @return a ticket > 0, or 0 when nothing was queued (the caller reads inline) + * + * Thread-safe: no (one producer) + */ +uint64_t pn_tt_reader_post_rows(pn_tt_device *device, uint32_t bank, uint32_t address, + uint32_t tiles, uint32_t rows, float *dst, uint32_t count); + +/** A card-side precondition: the queue at (x, y, control_address) has EXECUTED `target` + * descriptors. The reader polls it on its own bank before the read (PHASE W wavefront: a + * streamed host never learns when a projection finished, the card does). */ +typedef struct pn_tt_reader_gate { + uint16_t x, y; + uint32_t control_address; + uint32_t target; +} pn_tt_reader_gate; + +/** As pn_tt_reader_post_rows(), read only once `gate` holds (NULL = at once). A gate not met + * within 5 s fails the job (the caller re-reads inline). */ +uint64_t pn_tt_reader_post_rows_gated(pn_tt_device *device, uint32_t bank, uint32_t address, + uint32_t tiles, uint32_t rows, float *dst, + uint32_t count, const pn_tt_reader_gate *gate); + +/** Wait until `ticket` is done. @return 0, or -1 when that read failed (dst is unspecified). */ +int pn_tt_reader_wait(pn_tt_device *device, uint64_t ticket); + +/** Wait until everything posted is done (abort paths: nothing may still write a buffer). */ +void pn_tt_reader_drain(pn_tt_device *device); + +/** Drain with every pending GATE failing at once (an abort: the gated work may never run). */ +void pn_tt_reader_drain_cancelled(pn_tt_device *device); + +/** Stop and join the thread. Safe when it never started. Called by pn_tt_device_close(). */ +void pn_tt_reader_close(pn_tt_device *device); + +/** Seconds the WAITERS spent blocked, and reads done (for the prefill report). */ +void pn_tt_reader_stats(const pn_tt_device *device, double *out_wait_seconds, + uint64_t *out_reads, double *out_read_seconds); + +#endif /* USE_TT */ +#endif /* PN_TT_READER_H */ diff --git a/src/accel/tt/pn_tt_render.c b/src/accel/tt/pn_tt_render.c new file mode 100644 index 0000000..a0c2df7 --- /dev/null +++ b/src/accel/tt/pn_tt_render.c @@ -0,0 +1,738 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_render.c + * @brief Implementation of the activation strip bridge. See pn_tt_render.h — the argument for + * why this is two programs rather than one mixed CB table lives there. + * + * @status None + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_render.h" + +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_eltwise_split.h" +#include "pn_tt_replica_map.h" + +/** + * Which enabled Tensix each direction drives. + * + * Defaults, not constants, and each must be a core no other residency holds AND that the + * matmul group cannot grow into: members run upward from `PN_TT_MATMUL_CORE_FIRST` = 75, so + * even `PN_TT_MATMUL_CORES=16` stops at 90. The rest of the map, which is why these two are + * 85 and 86: + * + * 71 matmul member 0 72/73/74 reduce / eltwise / sample + * 75..90 matmul members 1..15 82/83/84 rope / memory / attention + * 85/86 render widen / narrow 90 gdn member 0 + * 91 head norm, slot 0 92..99 gdn head-split members + * 103 head norm, slot 1 100..102 used by the rectangle arm's control + * + * 85 and 86 are inside the range sixteen matmul members would reach, and that is not an + * oversight: `pn_tt_prepare_family()` refuses a collision BY NAME against every live + * residency, so a run that wants both would be told rather than silently sharing a core. The + * shipped setting is 8 members, which stops at 82. + */ +#define PN_TT_CORE_RENDER_WIDEN_ENV "PN_TT_CORE_RENDER_WIDEN" +#define PN_TT_CORE_RENDER_WIDEN_DEFAULT 85u +#define PN_TT_CORE_RENDER_NARROW_ENV "PN_TT_CORE_RENDER_NARROW" +#define PN_TT_CORE_RENDER_NARROW_DEFAULT 86u + +#define PN_TT_RENDER_WIDEN_PROGRAM "render_widen" +#define PN_TT_RENDER_NARROW_PROGRAM "render_narrow" + +/** + * THE SPLIT'S KNOBS, and where its members live. + * + * `PN_TT_RENDER_CORES=N` runs every render descriptor over N Tensix, member `m` taking a + * contiguous slice `[m*W/N, (m+1)*W/N)` of A, of the zero B and of the output. It is the + * SAME pattern the elementwise split runs and it needs no kernel change and no new opcode, + * because `render_widen`/`render_narrow` ARE the elementwise program at two page sizes: + * `resident_eltwise_reader.c` reads tile `t` at `base + t * TILE_BYTES`, unconditionally and + * in order. `pn_tt_eltwise_split.h` holds the arithmetic and is shared rather than copied. + * + * THE MEMBERS RUN ABOVE EVERY CORE ANY OTHER FAMILY CAN REACH AT ANY LEGAL KNOB. The + * elementwise split's map ends its own members at 121 and calls 122..139 free; the two + * directions are split INDEPENDENTLY, so they take one block each: + * + * 122..128 render-widen members 1..7 129..135 render-narrow members 1..7 + * + * That is a claim about the whole map and not about today's defaults, which is what + * `pn_tt_core_holder()` turns into a refusal BY NAME — a contested core does not fail, it + * computes badly (1.0e-2 against 6.9e-4 on the same descriptor). + * + * WHY THIS FAMILY AND NOT `reduce`: both `reduce` and `head_norm` are a row-wise reduction + * into DST column 0, so a tile slice there needs a cross-core partial-sum combine — a kernel + * and protocol change, not a host-side address offset. Render needs neither. + */ +#define PN_TT_RENDER_CORES_ENV "PN_TT_RENDER_CORES" +#define PN_TT_RENDER_MEMBER_CORE_ENV "PN_TT_RENDER_MEMBER_CORE_FIRST" +#define PN_TT_RENDER_MEMBER_CORE_DEFAULT 122u +/** CAP on the block depth, so a member-count ladder can be run at a FIXED depth. The depth + * MOVES WITH THE MEMBER COUNT — the slice shrinks and `2 * depth` must divide it — so a + * ladder walks two knobs at once and "it stopped at N" would not say which one stopped. */ +#define PN_TT_RENDER_DEPTH_ENV "PN_TT_RENDER_DEPTH" + +/** Everything that differs between the two directions, in one row each. */ +typedef struct render_direction { + const char *program; + const char *name; + const char *member_name; + const char *core_env; + uint32_t core_default; + uint32_t in_tile_bytes; + uint32_t out_tile_bytes; +} render_direction; + +static const render_direction g_render_directions[PN_TT_RENDER_DIRECTIONS] = { + { PN_TT_RENDER_WIDEN_PROGRAM, "render-widen", "render-widen-member", + PN_TT_CORE_RENDER_WIDEN_ENV, PN_TT_CORE_RENDER_WIDEN_DEFAULT, + PN_TT_TILE_BF16, PN_TT_TILE_FP32 }, + { PN_TT_RENDER_NARROW_PROGRAM, "render-narrow", "render-narrow-member", + PN_TT_CORE_RENDER_NARROW_ENV, PN_TT_CORE_RENDER_NARROW_DEFAULT, + PN_TT_TILE_FP32, PN_TT_TILE_BF16 }, +}; + +/** + * THE DEFAULT MEMBER COUNT IS PER DIRECTION, AND IT IS MEASURED, NOT CHOSEN. + * + * Silicon 2026-08-26, on the WINDOW await, five arms on one binary in one hour, every one + * `cmp` IDENTICAL: + * + * direction Wt N=1 N=2 N=4 K = T(1) - 2*(T(1)-T(2)) + * render-widen 256 125.4 105.1 152.7 84.6 us + * render-narrow 128 77.9 82.6 153.3 87.1 us + * + * The elementwise family's own break-even, measured the same day over five shapes, is + * **87.2 +- 1.3 us** — and render's two directions recover 84.6 and 87.1 from completely + * different programs at different page sizes. **The host's per-member cost does not care + * about the page size**, which is the falsifier for this lever not firing. At four members + * both sit on the host floor — 152.7 and 153.3 against 4 x 38.2 us — the same 38.1-38.4 us a + * member the elementwise ladder measured flat at N = 4, 8 and 16. + * + * **BUT K IS A PROPERTY OF THE AWAIT, AND THE SHIPPED AWAIT IS THE `line`.** On the line the + * same five shapes give **K = 44.3 +- 1.1 us** (render-widen 110.3 -> 77.1 recovers 43.9), + * and BOTH directions are above it: 110.3 and 62.8. So **{2, 2}**. Deciding this on the + * window numbers alone would have left the narrow direction unsplit for a bar that no longer + * applies — which is the same mistake as reading the width knee off a family average. + * + * `PN_TT_RENDER_CORES` overrides BOTH, which is what a ladder wants; a per-direction knob + * would make the two arms of a sweep disagree about what was being swept. + */ +static const uint32_t g_render_cores_default[PN_TT_RENDER_DIRECTIONS] = { 2u, 2u }; + +static void render_release_direction(pn_tt_device *device, uint32_t index); + +/** Member `m` of direction `d`, member 0 being the direction's own residency. */ +static pn_tt_residency *render_member(pn_tt_device *device, uint32_t index, uint32_t member) +{ + return member == 0u ? &device->render[index].residency + : &device->render[index].member[member - 1u]; +} + +/** The first core direction `index`'s members 1..N-1 run upward from. The two directions take + * disjoint blocks of `PN_TT_RENDER_SPLIT_MAX_MEMBERS - 1` cores so a ladder on one does not + * have to know what the other was set to. */ +static uint32_t render_member_core_first(uint32_t index) +{ + return pn_tt_env_u32(PN_TT_RENDER_MEMBER_CORE_ENV, PN_TT_RENDER_MEMBER_CORE_DEFAULT) + + index * (PN_TT_RENDER_SPLIT_MAX_MEMBERS - 1u); +} + +/** Fill `request` with the placement one member of `direction` runs. Placement reads the + * depth and the three page sizes and nothing else, so the tile count carried here is the + * SLICE and every member places identically. */ +static void render_placement(const render_direction *direction, uint32_t tiles, uint32_t depth, + ft_tt_op_eltwise_request *request) +{ + memset(request, 0, sizeof(*request)); + request->function = FT_TT_OP_VECTOR_FN_ADD; + request->tile_count = tiles; + request->prefetch_tiles = depth; + request->input_a.tile_bytes = direction->in_tile_bytes; + request->input_b.tile_bytes = direction->in_tile_bytes; + request->output.tile_bytes = direction->out_tile_bytes; +} + +/** + * How many members this direction may run, and the ONE block depth every one of them takes. + * + * IT RUNS BEFORE ANYTHING IS PLACED, for the reason `pn_tt_eltwise_group_plan()` gives: the + * placement is sized from the SLICED width, because the input CBs are two blocks of the + * effective depth and the depth has to divide what a descriptor actually carries. Deciding + * the member count afterwards places member 0 for a width no descriptor has. + * + * A member count whose slice admits no depth SHRINKS the split rather than refusing it — + * unsplit is always correct and always available. + * + * @param out_depth the depth every member is placed at; 0 when none serves even at one + * @return 1..PN_TT_RENDER_SPLIT_MAX_MEMBERS + */ +static uint32_t render_plan(uint32_t index, uint32_t tiles, uint32_t *out_depth) +{ + const render_direction *direction = &g_render_directions[index]; + const uint32_t cap = pn_tt_env_u32(PN_TT_RENDER_DEPTH_ENV, PN_TT_ONCARD_MAX_DEPTH); + uint32_t requested = pn_tt_env_u32(PN_TT_RENDER_CORES_ENV, g_render_cores_default[index]); + uint32_t members; + + *out_depth = 0u; + members = pn_tt_eltwise_split_member_limit(requested, &tiles, 1u, + PN_TT_RENDER_SPLIT_MAX_MEMBERS); + for (;;) { + const uint32_t slice = tiles / members; + uint32_t depth; + + /* THE DEEPEST BLOCK THAT BOTH DIVIDES THE RUN AND FITS THE ARENA, and the two are not + * the same question. `pn_tt_common_depth()` answers the first; `place_cbs` answers the + * second, and at 4096-byte pages it is the binding one — two input streams of + * `2*depth` pages is `4 * depth * tile_bytes` of a 1.29 MiB arena, so the narrow + * direction's ceiling is half the widen direction's. Walking down from the divisor + * rather than refusing is what stops a page size from silently costing a residency. */ + for (depth = pn_tt_common_depth(&slice, 1u, cap); depth > 0u; + depth = pn_tt_common_depth(&slice, 1u, depth - 1u)) { + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + ft_tt_op_eltwise_request request; + /* The depth search must not write into any residency's own CB buffer: + * `prepare_family` has not run yet and it zeroes the whole handle. */ + unsigned char scratch_cb[FT_TT_NUM_CIRCULAR_BUFFERS * + FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4]; + uint64_t scratch_mask; + + render_placement(direction, slice, depth, &request); + if (ft_tt_op_eltwise_place_cbs(&request, placed, scratch_cb, sizeof(scratch_cb), + &scratch_mask) == FT_TT_OK) { + *out_depth = depth; + if (requested > 1u && members != requested) + fprintf(stderr, "pn_tt: %s SPLIT — %u members was asked for; %u is the " + "largest that divides %u tiles into slices a block depth " + "serves\n", direction->name, requested, members, tiles); + return members; + } + } + if (members == 1u) + return 1u; + members = pn_tt_eltwise_split_member_limit(members - 1u, &tiles, 1u, + PN_TT_RENDER_SPLIT_MAX_MEMBERS); + } +} + +/** + * Arm one direction for `tiles`, ZERO OPERAND INCLUDED. + * + * The zero buffer is `tiles` INPUT-format tiles and it is never written after the allocation + * zeroes it — that is the whole of the "no kernel change" argument, and it is also why the + * buffer must be allocated by something that zeroes: a B operand full of whatever the + * allocator handed back would make a copy into a wrong answer with nothing refusing. + */ +static bool render_arm_direction(pn_tt_device *device, uint32_t index, uint32_t tiles) +{ + const render_direction *direction = &g_render_directions[index]; + pn_tt_render_residency *slot = &device->render[index]; + ft_tt_op_eltwise_request request; + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + ft_tt_buffer zeros; + uint32_t depth; + uint32_t members; + uint32_t member; + uint32_t first; + + if (!pn_tt_program_is_present(direction->program)) { + fprintf(stderr, "pn_tt: no `%s` program — that half of the activation strip bridge " + "stays on the host\n", direction->program); + return false; + } + members = render_plan(index, tiles, &depth); + if (depth == 0u) { + fprintf(stderr, "pn_tt: no block depth both divides a %u-tile %s and fits the L1 " + "arena at %u B -> %u B pages\n", tiles, direction->name, + direction->in_tile_bytes, direction->out_tile_bytes); + return false; + } + if (ft_tt_buffer_alloc(&device->allocator, FT_TT_MEM_SPACE_DRAM, + device->strip_bank_cursor++ % device->dram_bank_count, + (size_t)tiles * direction->in_tile_bytes, &zeros) != FT_TT_OK) { + fprintf(stderr, "pn_tt: DRAM full allocating the %s zero operand\n", direction->name); + return false; + } + if (pn_tt_zero_dram(device, zeros.logical_bank, (uint32_t)zeros.address, + (size_t)tiles * direction->in_tile_bytes) != 0) + return false; + + if (!pn_tt_prepare_family(device, &slot->residency, direction->program, direction->name, + direction->core_env, direction->core_default, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + render_placement(direction, tiles / members, depth, &request); + if (ft_tt_op_eltwise_place_cbs(&request, placed, slot->residency.cb_buffer, + sizeof(slot->residency.cb_buffer), + &slot->residency.cb_mask) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the %s circular buffers do not fit at %u tiles, depth %u\n", + direction->name, tiles / members, depth); + pn_tt_residency_release(&slot->residency); + return false; + } + if (!pn_tt_start_family(&slot->residency)) + return false; + slot->members = 1u; + memset(slot->member_seconds, 0, sizeof(slot->member_seconds)); + slot->in_tile_bytes = direction->in_tile_bytes; + slot->out_tile_bytes = direction->out_tile_bytes; + slot->tiles = tiles; + slot->depth = depth; + slot->zero_bank = zeros.logical_bank; + slot->zero_address = (uint32_t)zeros.address; + + /* MEMBERS 1..N-1, EACH ON ITS OWN TENSIX, at member 0's identical placement. + * A MEMBER THAT FAILS TO ARM COLLAPSES THE SPLIT TO ONE, loudly, rather than shrinking + * it: the width must divide the member count exactly, and 8 shrinking to 5 is a ragged + * slice the kernel refuses. Unsplit is always correct — member 0's placement serves the + * whole width too, since `2 * depth` divides a slice and the slice divides the width. */ + first = render_member_core_first(index); + for (member = 1u; member < members; member++) { + pn_tt_residency *handle = &slot->member[member - 1u]; + + if (!pn_tt_prepare_family_on_core(device, handle, direction->program, + direction->member_name, PN_TT_RENDER_MEMBER_CORE_ENV, + first + member - 1u, PN_TT_ONCARD_TRIAD_MASK)) + break; + if (ft_tt_op_eltwise_place_cbs(&request, placed, handle->cb_buffer, + sizeof(handle->cb_buffer), &handle->cb_mask) != + FT_TT_OK || + !pn_tt_start_family(handle)) { + pn_tt_residency_release(handle); + break; + } + slot->members++; + } + if (slot->members != members) { + fprintf(stderr, "pn_tt: %s SPLIT — asked for %u members and %u armed; COLLAPSING to " + "one core, because a partial split is a width no member divides\n", + direction->name, members, slot->members); + for (member = 1u; member < slot->members; member++) + pn_tt_residency_release(&slot->member[member - 1u]); + slot->members = 1u; + } + /* THE TABLE EVERY ONE OF THESE CORES READS MUST BE A WRITTEN FACT. They run + * `resident_vector_writer.c` and they arm on whichever side of + * `pn_tt_publish_replica_map()` the model's setup order puts them, and that function does + * not write to this family at all — so without this they read whatever is at the fixed L1 + * replica-table address, which is not "no mirroring" but "whatever a previous process + * left there". That is exactly the state that cost the elementwise split its `cmp`. + * Neither render output is a mirrored strip (`gdn_stream_in` is a GDN family buffer, + * `gdn_y_bridge` feeds the elementwise gate, and only `normed` and `ffn_swiglu` are + * replicated), so EMPTY is the CORRECT table here and not a stub. */ + for (member = 0u; member < slot->members; member++) { + if (pn_tt_publish_empty_replica_map(device, render_member(device, index, member), + direction->name) != 0) { + fprintf(stderr, "pn_tt: %s could not be given an empty replica table — refusing " + "the residency rather than running it on an unknown one\n", + direction->name); + render_release_direction(device, index); + return false; + } + } + if (slot->members > 1u) + fprintf(stderr, "pn_tt: %s SPLIT — every descriptor over %u members, core %u then " + "%u..%u, %u tiles each at block depth %u\n", + direction->name, slot->members, slot->residency.core_index, first, + first + slot->members - 2u, tiles / slot->members, depth); + fprintf(stderr, "pn_tt: %s is up on core %u — %u tiles, %u B -> %u B, depth %u\n", + direction->name, slot->residency.core_index, tiles, slot->in_tile_bytes, + slot->out_tile_bytes, depth); + return true; +} + +int pn_tt_device_arm_render(pn_tt_device *device, uint32_t widen_tiles, uint32_t narrow_tiles) +{ + if (!device) + return -1; + if (widen_tiles != 0u && !device->render[PN_TT_RENDER_WIDEN].residency.running) + (void)render_arm_direction(device, PN_TT_RENDER_WIDEN, widen_tiles); + if (narrow_tiles != 0u && !device->render[PN_TT_RENDER_NARROW].residency.running) + (void)render_arm_direction(device, PN_TT_RENDER_NARROW, narrow_tiles); + return 0; +} + +bool pn_tt_render_widen_is_armed(const pn_tt_device *device) +{ + return device && device->render[PN_TT_RENDER_WIDEN].residency.running; +} + +bool pn_tt_render_narrow_is_armed(const pn_tt_device *device) +{ + return device && device->render[PN_TT_RENDER_NARROW].residency.running; +} + +/** Advance one operand's base to `slice`, in ITS OWN page size — the two sides of a render + * are at different page sizes and an offset built from the wrong one is an address with + * nothing on either end of the link able to refuse it. An UNSET operand is left alone. */ +static void render_slice_operand(ft_tt_op_operand *operand, uint32_t first_tile) +{ + if (operand->tile_bytes == 0u) + return; + operand->address += first_tile * operand->tile_bytes; +} + +/** + * Retire ONE member and arm a fresh one ON THE CORE IT ALREADY HELD. + * + * The core is captured BEFORE the release, because re-reading `core_index` off a released + * handle is how a member gets armed on core 0. + */ +static bool render_member_rearm(pn_tt_device *device, uint32_t index, uint32_t member, + const ft_tt_op_eltwise_request *placement) +{ + const render_direction *direction = &g_render_directions[index]; + ft_tt_launch_cb_entry placed[FT_TT_OP_ELTWISE_CB_COUNT]; + pn_tt_residency *handle = render_member(device, index, member); + const char *core_env = member == 0u ? direction->core_env : PN_TT_RENDER_MEMBER_CORE_ENV; + const char *name = member == 0u ? direction->name : direction->member_name; + uint32_t core = handle->core_index; + + fprintf(stderr, "pn_tt: %s member %u refused a descriptor — retiring it by ASKING and " + "arming a fresh one on core %u\n", direction->name, member, core); + pn_tt_residency_stop(handle); + pn_tt_residency_release(handle); + device->oncard_restarts++; + /* A core whose compute thread is still spinning out its bound is a WEDGE, not a re-arm: + * booting firmware onto it leaves `instrn_buf_status = 0x66` and needs a rung-3 recovery + * and sudo. Every other family here refuses on the same reading. */ + if (!pn_tt_residency_retired(handle)) { + fprintf(stderr, "pn_tt: %s member %u did not retire — REFUSING to arm a fresh one on " + "core %u\n", direction->name, member, core); + return false; + } + if (!pn_tt_prepare_family_on_core(device, handle, direction->program, name, core_env, core, + PN_TT_ONCARD_TRIAD_MASK)) + return false; + if (ft_tt_op_eltwise_place_cbs(placement, placed, handle->cb_buffer, + sizeof(handle->cb_buffer), &handle->cb_mask) != FT_TT_OK) { + pn_tt_residency_release(handle); + return false; + } + if (!pn_tt_start_family(handle)) + return false; + /* Same argument as the arm: a fresh residency's replica table is whatever that L1 region + * held, and empty is a written fact. */ + return pn_tt_publish_empty_replica_map(device, handle, direction->name) == 0; +} + +/** + * Run ONE render descriptor across the whole set: ALL ENQUEUED BEFORE ANY IS AWAITED. + * + * The two loops being separate is the measurement and not a style. Enqueue-wait per member + * would run `members` SERIAL descriptors and print the flat line "it does not parallelise", + * produced by this function rather than by the card. + * + * Time is charged to the SET, once, at the WHOLE descriptor's tile count, so the census key + * `(render-widen, add, tiles)` is the same row at every member count and the ladder is + * directly comparable. + */ +static int render_submit_split(pn_tt_device *device, uint32_t index, + const ft_tt_op_eltwise_request *whole, uint32_t members) +{ + uint32_t words[PN_TT_RENDER_SPLIT_MAX_MEMBERS][FT_TT_OP_VECTOR_WORDS]; + bool bad[PN_TT_RENDER_SPLIT_MAX_MEMBERS]; + pn_tt_render_residency *slot = &device->render[index]; + double enqueue_started; + uint32_t member; + int failed = 0; + + memset(bad, 0, sizeof(bad)); + for (member = 0u; member < members; member++) { + ft_tt_op_eltwise_request sliced = *whole; + pn_tt_eltwise_slice slice; + + if (!pn_tt_eltwise_split_slice(whole->tile_count, members, member, + slot->in_tile_bytes, &slice)) { + fprintf(stderr, "pn_tt: %u %s tiles do not divide over %u members\n", + whole->tile_count, g_render_directions[index].name, members); + return -1; + } + sliced.tile_count = slice.tile_count; + sliced.prefetch_tiles = slot->depth; + render_slice_operand(&sliced.input_a, slice.first_tile); + render_slice_operand(&sliced.output, slice.first_tile); + /* THE ZERO OPERAND IS NOT OFFSET, AND THAT IS DELIBERATE. Every tile of it is the + * same tile — zeros, staged by the allocator and never written — so every member + * reading from the base reads exactly what an offset would have given it, and the + * read is provably inside the allocation even when a caller submits fewer tiles than + * the direction was armed for. Offsetting it would be arithmetic with no + * corresponding fact. */ + if (ft_tt_op_eltwise_parameters(&sliced, words[member]) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the %s slice [%u, %u) is one the family refuses\n", + g_render_directions[index].name, slice.first_tile, + slice.first_tile + slice.tile_count); + return -1; + } + pn_tt_vec_probe_apply(words[member]); + } + enqueue_started = pn_tt_now_seconds(); + for (member = 0u; member < members; member++) + if (pn_tt_residency_enqueue(render_member(device, index, member), + FT_TT_QUEUE_OP_ELTWISE, words[member], 1u) != 0) + failed = bad[member] = 1; + /* THE RESIDENCY CLOCKS RUN ON THIS PATH TOO (class 28). Enqueue on member 0, each serial + * await on its member, so the rows SUM to the split's wall. */ + render_member(device, index, 0u)->seconds += pn_tt_now_seconds() - enqueue_started; + /* EVERY member is awaited even after one has failed. An enqueue that got as far as the + * ring has a descriptor the card will run whatever this host does next, and leaving it + * un-awaited desynchronises that member's cursor for the rest of the run. */ + for (member = 0u; member < members; member++) { + const double member_started = pn_tt_now_seconds(); + double member_elapsed; + + if (pn_tt_residency_await(render_member(device, index, member), + PN_TT_OP_TIMEOUT_MS) != 0) + failed = bad[member] = 1; + member_elapsed = pn_tt_now_seconds() - member_started; + + slot->member_seconds[member] += member_elapsed; + render_member(device, index, member)->seconds += member_elapsed; + } + /* PER-MEMBER RE-ARM AND REPLAY. Replaying one member's slice is the SAME COMPUTATION and + * not merely a safe one: `a + 0` is a pure function of operands it does not modify and + * the slices are disjoint, so a member that succeeded is neither re-run nor read. */ + if (failed) { + ft_tt_op_eltwise_request placement; + + render_placement(&g_render_directions[index], whole->tile_count / members, slot->depth, + &placement); + failed = 0; + for (member = 0u; member < members; member++) { + if (!bad[member]) + continue; + if (!render_member_rearm(device, index, member, &placement) || + pn_tt_residency_submit(render_member(device, index, member), + FT_TT_QUEUE_OP_ELTWISE, words[member], + PN_TT_OP_TIMEOUT_MS) != 0) + failed = 1; + } + } + return failed ? -1 : 0; +} + +/** One render descriptor, in whichever direction the caller's operands name. */ +static int render_submit(pn_tt_device *device, uint32_t index, ft_tt_op_operand in, + ft_tt_op_operand out, uint32_t tiles) +{ + pn_tt_render_residency *slot; + ft_tt_op_eltwise_request request; + uint32_t words[FT_TT_OP_VECTOR_WORDS]; + uint32_t members; + double started; + + if (!device) + return -1; + slot = &device->render[index]; + if (!slot->residency.running) + return -1; + /* Chained projections are awaited HERE — pn_tt_device.h, pn_tt_matvec_enqueue. A render + * whose input a queued matvec is still writing reads the PREVIOUS token's activation and + * nothing anywhere refuses it. */ + if (pn_tt_matvec_flush(device) != 0) + return -1; + /* THE PAGE SIZE IS A THREE-SIDED CONTRACT WITH NO CARD-SIDE CHECK: the DRAM operand, the + * L1 circular buffer and the data-movement kernel's compile-time TILE_BYTES all have to + * agree, and the device can see none of it. Measured on the reduce family — constants + * built at 2048 under a 4096 program score 1.0e+00 with nothing refusing anywhere. */ + if (in.tile_bytes != slot->in_tile_bytes || out.tile_bytes != slot->out_tile_bytes) { + fprintf(stderr, "pn_tt: a %u B -> %u B render was handed %u B -> %u B operands\n", + slot->in_tile_bytes, slot->out_tile_bytes, in.tile_bytes, out.tile_bytes); + return -1; + } + if (tiles == 0u || tiles > slot->tiles) { + fprintf(stderr, "pn_tt: %u tiles is not a strip the %s residency was placed for " + "(%u)\n", tiles, g_render_directions[index].name, slot->tiles); + return -1; + } + /* THE DEPTH INVARIANT IS PER DESCRIPTOR AND THE DESCRIPTOR A MEMBER RUNS IS THE SLICE. + * A width the set does not divide, or one whose slice is off the `2 * depth` grid, RUNS + * WHOLE ON MEMBER 0 rather than failing: member 0's placement serves it (the slice + * divides the width and `2 * depth` divides the slice), so one core is slower rather + * than wrong. Failing instead would turn a new call site into a dropped descriptor, + * which on this family is a wrong answer with a return code nobody reads. Said ONCE, + * because 48 descriptors a token is a flood. */ + members = slot->members < 2u ? 1u : slot->members; + if (members > 1u && ((tiles % members) != 0u || + ((tiles / members) % (2u * slot->depth)) != 0u || + ft_tt_op_block_depth(tiles / members, slot->depth) != slot->depth)) { + static bool narrowed; + + if (!narrowed) { + narrowed = true; + fprintf(stderr, "pn_tt: %u %s tiles do not slice over %u members at depth %u — " + "this width runs WHOLE on member 0\n", tiles, + g_render_directions[index].name, members, slot->depth); + } + members = 1u; + } + if (ft_tt_op_block_depth(tiles / members, slot->depth) != slot->depth || + ((tiles / members) % (2u * slot->depth)) != 0u) { + fprintf(stderr, "pn_tt: %u tiles is not a whole number of depth-%u blocks the render " + "residency was placed for (%u)\n", tiles, slot->depth, slot->tiles); + return -1; + } + + memset(&request, 0, sizeof(request)); + /* `a + 0`. See pn_tt_render.h: there is no identity in the family and adding one would + * make every eltwise-family program stale by content provenance. */ + request.function = FT_TT_OP_VECTOR_FN_ADD; + request.tile_count = tiles; + request.prefetch_tiles = slot->depth; + request.input_a = in; + request.input_b.x = (uint16_t)device->dram[slot->zero_bank].x; + request.input_b.y = (uint16_t)device->dram[slot->zero_bank].y; + request.input_b.address = slot->zero_address; + request.input_b.tile_bytes = slot->in_tile_bytes; + request.output = out; + + if (members > 1u) { + int rc; + + started = pn_tt_now_seconds(); + rc = render_submit_split(device, index, &request, members); + pn_tt_oncard_account(device, g_render_directions[index].name, "add", tiles, + pn_tt_now_seconds() - started); + return rc; + } + if (ft_tt_op_eltwise_parameters(&request, words) != FT_TT_OK) + return -1; + pn_tt_vec_probe_apply(words); + + started = pn_tt_now_seconds(); + if (pn_tt_residency_submit(&slot->residency, FT_TT_QUEUE_OP_ELTWISE, words, + PN_TT_OP_TIMEOUT_MS) != 0) { + /* The words address DRAM only, so the same descriptor is valid for a fresh residency + * — the re-arm argument every other family here makes. */ + fprintf(stderr, "pn_tt: the %s residency refused a descriptor — retiring it by ASKING " + "and arming a fresh one\n", g_render_directions[index].name); + pn_tt_residency_stop(&slot->residency); + pn_tt_residency_release(&slot->residency); + device->oncard_restarts++; + if (!pn_tt_prepare_family(device, &slot->residency, g_render_directions[index].program, + g_render_directions[index].name, + g_render_directions[index].core_env, + g_render_directions[index].core_default, + PN_TT_ONCARD_TRIAD_MASK) || + !pn_tt_start_family(&slot->residency) || + pn_tt_residency_submit(&slot->residency, FT_TT_QUEUE_OP_ELTWISE, words, + PN_TT_OP_TIMEOUT_MS) != 0) + return -1; + } + pn_tt_oncard_account(device, g_render_directions[index].name, "add", tiles, + pn_tt_now_seconds() - started); + return 0; +} + +int pn_tt_render_widen(pn_tt_device *device, ft_tt_op_operand in, ft_tt_op_operand out, + uint32_t tiles) +{ + return render_submit(device, PN_TT_RENDER_WIDEN, in, out, tiles); +} + +int pn_tt_render_narrow(pn_tt_device *device, ft_tt_op_operand in, ft_tt_op_operand out, + uint32_t tiles) +{ + return render_submit(device, PN_TT_RENDER_NARROW, in, out, tiles); +} + +/** Retire one direction WHOLE — member 0 and every member. A LEAKED TRIAD IS A WEDGED TENSIX + * for the next process to ask for that core, which is what the head-norm slots cost when + * `pn_tt_device_close()` released named fields and missed a set. */ +static void render_release_direction(pn_tt_device *device, uint32_t index) +{ + pn_tt_render_residency *slot = &device->render[index]; + uint32_t member; + + for (member = slot->members; member > 0u; member--) { + pn_tt_residency *handle = render_member(device, index, member - 1u); + + if (handle->running) { + pn_tt_residency_stop(handle); + pn_tt_residency_release(handle); + } + } + if (slot->residency.running) { + pn_tt_residency_stop(&slot->residency); + pn_tt_residency_release(&slot->residency); + } + memset(slot, 0, sizeof(*slot)); +} + +void pn_tt_render_release(pn_tt_device *device) +{ + uint32_t index; + + if (!device) + return; + for (index = 0; index < (uint32_t)PN_TT_RENDER_DIRECTIONS; index++) + render_release_direction(device, index); +} + +uint32_t pn_tt_render_members(const pn_tt_device *device, uint32_t index) +{ + if (!device || index >= (uint32_t)PN_TT_RENDER_DIRECTIONS) + return 0u; + /* `members` is set by the arm, so a direction that never armed reads 0 and one that armed + * unsplit reads 1 — the caller has to be able to tell those two apart. */ + return device->render[index].members; +} + +bool pn_tt_render_member_stat(const pn_tt_device *device, uint32_t index, uint32_t member, + const char **out_name, uint32_t *out_core, uint64_t *out_ops, + double *out_seconds) +{ + const pn_tt_residency *handle; + + if (!device || index >= (uint32_t)PN_TT_RENDER_DIRECTIONS || + member >= pn_tt_render_members(device, index)) + return false; + handle = member == 0u ? &device->render[index].residency + : &device->render[index].member[member - 1u]; + if (out_name) + *out_name = g_render_directions[index].name; + if (out_core) + *out_core = handle->core_index; + if (out_ops) + *out_ops = handle->ops; + if (out_seconds) + *out_seconds = device->render[index].member_seconds[member]; + return true; +} + +void pn_tt_render_defer(pn_tt_device *device, bool narrow, bool on) +{ + pn_tt_render_residency *slot; + + if (!device) + return; + slot = &device->render[narrow ? PN_TT_RENDER_NARROW : PN_TT_RENDER_WIDEN]; + slot->residency.defer_await = on; + for (uint32_t member = 1u; member < slot->members; member++) + slot->member[member - 1u].defer_await = on; +} + +int pn_tt_render_settle(pn_tt_device *device, bool narrow) +{ + pn_tt_render_residency *slot; + int rc; + + if (!device) + return -1; + slot = &device->render[narrow ? PN_TT_RENDER_NARROW : PN_TT_RENDER_WIDEN]; + rc = pn_tt_residency_settle(&slot->residency, PN_TT_OP_TIMEOUT_MS); + for (uint32_t member = 1u; member < slot->members; member++) + if (pn_tt_residency_settle(&slot->member[member - 1u], PN_TT_OP_TIMEOUT_MS) != 0) + rc = -1; + return rc; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_render.h b/src/accel/tt/pn_tt_render.h new file mode 100644 index 0000000..eafa0b0 --- /dev/null +++ b/src/accel/tt/pn_tt_render.h @@ -0,0 +1,206 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_render.h + * @brief THE ACTIVATION STRIP BRIDGE: two resident cores that convert a strip of tiles from + * bf16 to fp32 and back, ON THE CARD, so the GDN block's activations stop crossing + * PCIe. + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS, IN ONE NUMBER + * --------------------------------------------------------------------------- + * A decode token is 0.390 s of HOST and 0.050 s of card, and 0.29 s of the host half is + * TRANSFER. 78% of that transfer is the Gated-Delta-Net block: `ssm_qkv` and `ssm_z` come + * down and the answer goes back up, **49,408 B a layer over 24 layers**. Nothing about the + * arithmetic needs the host — what needs it is a FORMAT SEAM. Every projection produces a + * bf16 strip; the GDN op and the per-head norms are Float32 programs, because the recurrence + * carries state across tokens and bf16 mantissas compound. So the activation crossed the link + * purely to change format. + * + * These two residencies are that conversion, in card DRAM. + * + * --------------------------------------------------------------------------- + * WHY IT IS A SEPARATE PROGRAM AND NOT A MIXED CB TABLE + * --------------------------------------------------------------------------- + * The obvious move — give the GDN op a Float16_b CB_IN and read `ssm_qkv` in place — is the + * one thing the hardware will not do. `emit_tensix_cb_descriptors.py`'s + * `conditional_destination_format()` is a WHOLE-PROGRAM decision: a Float32 CB unpacks into + * Tf32 source registers only when `dst_accum_mode` is on AND *every* CB of the program is + * Float32. One bf16 buffer anywhere in `gdn_float32.cbtable` demotes the recurrent state and + * the conv weights to Float16_b in SrcA. So the format change has to happen in a program + * whose arithmetic does not care, and a copy is exactly that program. + * + * --------------------------------------------------------------------------- + * THE OP IS `a + b` WITH b A ZERO STRIP, AND THAT IS WHY NO KERNEL CHANGED + * --------------------------------------------------------------------------- + * `ft_tt_op_vector_function` has no identity and adding one would edit + * `resident_eltwise_compute.c`, which by content provenance makes the shipped `eltwise` + * binary stale and costs a re-validation of an op that is already correct. `x + 0` is exact + * in every format involved, and the zero operand is free: every allocator here stages its + * DRAM zeroed, so the B buffer is allocated once and never written again. + * + * The two programs are `render_widen` (bf16 -> fp32) and `render_narrow` (fp32 -> bf16), + * built from the SAME three kernel sources as `eltwise` with a different `.cbtable` and one + * different `TILE_BYTES` on each of the two data-movement processors — the `reduce_fp32` + * precedent exactly. This is the first program in the tree whose reader and writer are built + * at DIFFERENT page sizes on purpose; each still agrees with its own circular buffer. + * + * --------------------------------------------------------------------------- + * WHAT THE NARROW DIRECTION COSTS, STATED RATHER THAN HIDDEN + * --------------------------------------------------------------------------- + * The narrowing happens at the UNPACK, and that unpack TRUNCATES toward zero where the host's + * `ft_bf16_from_float_array()` rounded to nearest — up to 2x the error on one value per + * element, on the last conversion before a consumer that would have rounded anyway. It is + * priced against the OUTPUT: the whole per-head-norm miss, of which this render is the + * largest term, costs 0.158% perplexity, and a 1e-3 relative bar on an intermediate tensor is + * refuted here — it fails an arm whose 48-token output is byte-identical to the oracle. + * + * --------------------------------------------------------------------------- + * SPLITTING IT: `PN_TT_RENDER_CORES=N`, AND WHY THE ARITHMETIC IS BORROWED + * --------------------------------------------------------------------------- + * These two programs ARE the elementwise program at two page sizes, so they take the + * elementwise split unchanged: `resident_eltwise_reader.c` reads tile `t` at + * `base + t * TILE_BYTES`, in order, so member `m` of N runs the SAME descriptor with + * `tile_count / N` and every operand base advanced by `m * (tile_count/N) * tile_bytes` — + * pairwise disjoint in A and in the output, with no shared operand, no reduction and no + * ordering. `pn_tt_eltwise_split.h` holds that arithmetic and is SHARED rather than copied, + * because two spellings of "which tiles does member m own" is the one thing here that fails + * with no error anywhere. The two directions are split INDEPENDENTLY: different programs at + * different page sizes have no reason to share a member count. + * + * `reduce` and `head_norm` do NOT take this pattern — both are a row-wise reduction into DST + * column 0, so a tile slice there needs a cross-core partial-sum combine, a kernel and + * protocol change rather than a host-side address offset. + * + * WHAT A MEMBER COSTS, MEASURED ON SILICON 2026-08-26. A descriptor is worth splitting in two + * iff its UNSPLIT TIME is above the break-even `K`, and "the knee is a width" was that + * statement seen through one family's pooled line. Render's two directions, on the two + * completion shapes: + * + * direction Wt window: N=1 N=2 N=4 K line: N=1 N=2 K + * render-widen 256 125.4 105.1 152.7 84.6 110.3 77.1 43.9 + * render-narrow 128 77.9 82.6 153.3 87.1 62.8 - - + * + * The elementwise family recovers **87.2 +- 1.3 us** on the window and **44.3 +- 1.1** on the + * line from five shapes; render's directions land on the same numbers from a different + * program at different page sizes, so **the page size is not in the host term**. At four + * members both sit on the host's per-member floor (152.7 and 153.3 against 4 x 38.2 us) — the + * same 38.1-38.4 us a member the elementwise ladder measured flat at N = 4, 8 and 16. + * + * On the shipped `line` await BOTH directions are above K, so **the default is {2, 2}**. On + * the window the narrow direction is below it and loses; the default would be {2, 1} there. + * K is a property of the AWAIT, not of the family. + * + * THE ZERO OPERAND IS NOT SLICED. Every tile of it is the same tile, so a member reading from + * the base reads exactly what an offset would have given it, and the read is provably inside + * the allocation even when a caller submits fewer tiles than the direction was armed for. + * + * AND EVERY MEMBER IS GIVEN AN EXPLICITLY EMPTY REPLICA TABLE. These cores run + * `resident_vector_writer.c`, `pn_tt_publish_replica_map()` does not write to this family at + * all, and the table lives at a FIXED L1 address — so an unpublished residency reads whatever + * a previous process left there, which is not "no mirroring" but "somebody else's addresses". + * That is the state that cost the elementwise split its `cmp`. Neither render output is a + * mirrored strip, so EMPTY is the correct table and not a stub. + * + * @status None + * @issues None + * @todo + * - [ ] An all-Float32 ELTWISE program would let `silu(z) *` run in fp32 so only the final + * `gated` is narrowed — modelled at 1.909e-03 against 1.023e-02, a 6.2x. It changes + * no bytes over PCIe, so it is an accuracy rung and not part of this bridge. + */ + +#ifndef PN_TT_RENDER_H +#define PN_TT_RENDER_H + +#ifdef USE_TT + +#include +#include + +#include "pn_tt_device.h" +#include "tt/ft_tt_op_dispatch.h" + +/** + * Arm both directions of the bridge, each placed for the WIDEST strip it will be asked for. + * + * Never fatal: a card without the two programs leaves the caller on whatever host path it + * had. The two cores are `PN_TT_CORE_RENDER_WIDEN` / `PN_TT_CORE_RENDER_NARROW`, and like + * every other residency here they are refused BY NAME against a live family or a matmul + * member rather than being allowed to contend — a contested core does not fail, it computes + * badly. + * + * @param widen_tiles the widest bf16 -> fp32 strip, in tiles; 0 skips that direction + * @param narrow_tiles the widest fp32 -> bf16 strip, in tiles; 0 skips that direction + * @return 0 (even when nothing armed), -1 on a bad argument + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_device_arm_render(pn_tt_device *device, uint32_t widen_tiles, uint32_t narrow_tiles); + +/** Is the bf16 -> fp32 direction live? */ +bool pn_tt_render_widen_is_armed(const pn_tt_device *device); + +/** Is the fp32 -> bf16 direction live? */ +bool pn_tt_render_narrow_is_armed(const pn_tt_device *device); + +/** + * `out = in`, bf16 tiles to fp32 tiles, `tiles` of them, in card DRAM. + * + * The operands are taken RAW rather than as a strip type because the three things this bridge + * connects are three different C types — a `pn_tt_activation`, a `pn_tt_fp32_strip` and a GDN + * family buffer — and they differ in the page size and in nothing else. The page each side + * must have is checked here, because a 2048-byte operand under a 4096-byte program is the + * failure with no card-side refusal anywhere. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_render_widen(pn_tt_device *device, ft_tt_op_operand in, ft_tt_op_operand out, + uint32_t tiles); + +/** `out = in`, fp32 tiles to bf16 tiles. See the header note on what the narrowing costs. */ +int pn_tt_render_narrow(pn_tt_device *device, ft_tt_op_operand in, ft_tt_op_operand out, + uint32_t tiles); + +/** Release both directions WHOLE — every member of each — and the two zero operands. + * + * A LEAKED TRIAD IS A WEDGED TENSIX for the next process to ask for that core, which is what + * the head-norm slots cost when `pn_tt_device_close()` released named fields and missed a + * set. This is now two sets. */ +void pn_tt_render_release(pn_tt_device *device); + +/** The two directions, in the order the device holds them — so a caller outside the backend + * can walk them without reaching into `pn_tt_device_internal.h`. */ +#define PN_TT_RENDER_WIDEN_INDEX 0u +#define PN_TT_RENDER_NARROW_INDEX 1u +#define PN_TT_RENDER_INDEX_COUNT 2u + +/** Live members of direction `index`, member 0 included. 1 when unsplit, **0 when that + * direction never armed** — and the caller has to be able to tell those two apart, because + * an arm that did not happen and a split that did not happen print the same silence. */ +uint32_t pn_tt_render_members(const pn_tt_device *device, uint32_t index); + +/** + * One member's core, descriptor count and AWAIT seconds — the shape of the set. + * + * Reported apart from the census because the census keys on the WHOLE descriptor's width by + * design, which keeps its `fixed + per_tile * tiles` fit comparable across a member ladder. + * What that therefore cannot show is whether the members are balanced, and "it stopped at N" + * is exactly the finding that needs it: a knee where every member costs the same is the + * cores; a knee where one member costs more than the others is that member being starved. + * + * @return false when the direction or the member does not exist + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +bool pn_tt_render_member_stat(const pn_tt_device *device, uint32_t index, uint32_t member, + const char **out_name, uint32_t *out_core, uint64_t *out_ops, + double *out_seconds); + +/** PM 7 ITEM 7: defer the awaits of one bridge direction's descriptors (`on`), and settle + * them. See pn_tt_residency_settle(). */ +void pn_tt_render_defer(pn_tt_device *device, bool narrow, bool on); +int pn_tt_render_settle(pn_tt_device *device, bool narrow); + +#endif /* USE_TT */ +#endif /* PN_TT_RENDER_H */ diff --git a/src/accel/tt/pn_tt_replica_map.c b/src/accel/tt/pn_tt_replica_map.c new file mode 100644 index 0000000..4a72588 --- /dev/null +++ b/src/accel/tt/pn_tt_replica_map.c @@ -0,0 +1,350 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_replica_map.c + * @brief Implementation of the activation-strip mirror map. + * + * @status Written 2026-08-19. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_replica_map.h" + +#include +#include + +#include "pn_tt_device_internal.h" +#include "pn_tt_eltwise_group.h" + +#include "tt/ft_tt_op_replicas.h" + +uint32_t pn_tt_activation_replica_count(const pn_tt_device *device) +{ + if (!device) + return 1u; + return device->activation_replicas ? device->activation_replicas : 1u; +} + +/** + * Add ONE SLICE of one strip's mirrors to the map, or say nothing if it has none. + * + * `byte_offset` is what makes a split addressable. The kernel's lookup is the exact triple + * `(x, y, base)`, so a descriptor writing the strip from tile `t` onward needs its OWN set, + * whose primary is the strip's base plus that offset and whose mirrors are each mirror's + * base plus the SAME offset — the mirrors are whole copies of the strip, so one offset + * addresses all of them. At offset 0 this is the unsliced set exactly. + */ +static int replica_map_add_slice(ft_tt_op_replica_table *table, const pn_tt_device *device, + const pn_tt_activation *strip, uint32_t byte_offset) +{ + ft_tt_op_replica_set *entry; + uint32_t index; + + if (!strip || strip->replica_count < 2u) + return 0; + if (table->set_count >= FT_TT_REPLICA_MAX_SETS) { + fprintf(stderr, "pn_tt: more mirrored strips than the replica table holds (%u)\n", + FT_TT_REPLICA_MAX_SETS); + return -1; + } + entry = &table->set[table->set_count]; + entry->count = strip->replica_count; + for (index = 0u; index < strip->replica_count; index++) { + const uint32_t bank = strip->replica_bank[index]; + + if (bank >= device->dram_bank_count) { + fprintf(stderr, "pn_tt: mirror %u names bank %u of %u\n", index, bank, + device->dram_bank_count); + return -1; + } + /* THE BANK'S OWN COORDINATE, not a door. A door is a read-side lever: the writer's + * cost is the barrier, not the port, and picking a port here would make the map + * depend on the matmul's member count. */ + entry->destination[index].x = (uint16_t)device->dram[bank].x; + entry->destination[index].y = (uint16_t)device->dram[bank].y; + entry->destination[index].base = strip->replica_address[index] + byte_offset; + } + table->set_count++; + return 0; +} + +/** Publish to one core and prove it landed. */ +static int replica_map_publish_to(pn_tt_device *device, const pn_tt_residency *residency, + const ft_tt_op_replica_table *table, const char *label) +{ + if (!residency->running) + return 0; + if (ft_tt_op_replica_table_write(&device->runtime.noc, residency->core_x, + residency->core_y, table) != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not write the replica map to %s (core %u)\n", label, + residency->core_index); + return -1; + } + /* READ IT BACK. A map that half-landed leaves one mirror stale, and a stale mirror is a + * plausible wrong answer with every kernel completing — there is nothing downstream + * that could refuse it. + * S2e (`PN_TT_REPLICA_VERIFY_ONCE`, default 1): a table this core has already read back + * byte for byte is not read back again — the verify alternates the decode map and its own + * twice a cycle, and each 808 B read-back costs ~0.56 ms on this link. */ + { + static struct { uint16_t x, y; uint64_t hash[4]; uint32_t n; } seen[64]; + static uint32_t seen_count; + static int once = -1; + const unsigned char *bytes = (const unsigned char *)table; + uint64_t hash = 1469598103934665603ull; + uint32_t i, h; + size_t b; + + if (once < 0) + once = (int)pn_tt_env_u32("PN_TT_REPLICA_VERIFY_ONCE", 1u); + for (b = 0; b < sizeof(*table); b++) + hash = (hash ^ bytes[b]) * 1099511628211ull; + for (i = 0; i < seen_count; i++) + if (seen[i].x == residency->core_x && seen[i].y == residency->core_y) + break; + if (once && i < seen_count) + for (h = 0; h < seen[i].n; h++) + if (seen[i].hash[h] == hash) + return 0; + if (ft_tt_op_replica_table_verify(&device->runtime.noc, residency->core_x, + residency->core_y, table) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the replica map did not read back from %s (core %u)\n", + label, residency->core_index); + return -1; + } + if (i == seen_count && seen_count < 64u) { + seen[i].x = (uint16_t)residency->core_x; + seen[i].y = (uint16_t)residency->core_y; + seen[i].n = 0u; + seen_count++; + } + if (i < seen_count) + seen[i].hash[seen[i].n++ % 4u] = hash, seen[i].n = seen[i].n > 4u ? 4u : seen[i].n; + } + return 0; +} + +static uint32_t g_replica_span; +static uint32_t g_replica_superband; + +void pn_tt_replica_map_set_span(uint32_t span) +{ + g_replica_span = span; +} + +void pn_tt_replica_map_set_superband(uint32_t bands) +{ + g_replica_superband = bands; +} + +/** Add the set keyed at `byte_offset` unless this table already holds that key. */ +static int replica_map_add_key(ft_tt_op_replica_table *table, const pn_tt_device *device, + const pn_tt_activation *strip, uint32_t byte_offset) +{ + uint32_t set; + + for (set = 0u; set < table->set_count; set++) + if (table->set[set].destination[0].base == strip->replica_address[0] + byte_offset && + table->set[set].destination[0].x == device->dram[strip->replica_bank[0]].x && + table->set[set].destination[0].y == device->dram[strip->replica_bank[0]].y) + return 0; + return replica_map_add_slice(table, device, strip, byte_offset); +} + +/** + * The keys of the SPLIT strip that ONE writer needs: eltwise member `member` (or, for + * `member < 0`, a core that never writes it sliced — it gets the base keys member 0 has). + * + * PER CORE SINCE PHASE W1a. The kernel's lookup is the exact `(x, y, base)` of the descriptor + * a core runs, and a core only ever runs ITS slice, so a key for another member's slice is a + * set no descriptor on that core can hit. One shared table held every member's keys and + * capped the split at the table's 8 sets; per core it holds that member's keys only — at most + * one a band (decode) or one a span (tall) — whatever the member count. + */ +static int replica_map_split_keys(pn_tt_device *device, ft_tt_op_replica_table *table, + const pn_tt_activation *strip, int member) +{ + const uint32_t m = member < 0 ? 0u : (uint32_t)member; + const uint32_t bands = strip->tile_rows ? strip->tile_rows : 1u; + const bool skip = pn_tt_red("eltwise_wide_key_skip"); + pn_tt_eltwise_slice slice; + uint32_t band, which, members; + + /* PHASE G: a TALL strip (more than 4 bands) is keyed by the producer's span — decode + * (span 1) and the prefill span `g_replica_span` — not a set per band. */ + if (bands > 4u) { + const uint32_t spans[2] = { 1u, g_replica_span ? g_replica_span : 1u }; + + for (which = 0u; which < 2u; which++) { + const uint32_t flat = spans[which] * strip->tile_count; + + if (which == 1u && spans[1] == 1u) + break; + members = pn_tt_eltwise_group_members_for(device, flat, FT_TT_OP_VECTOR_FN_SILU_MUL); + if (members > 1u && (flat % members) != 0u && + members <= device->eltwise_decode_members) + members = 1u; /* that width runs whole on member 0 */ + if (m >= members || + !pn_tt_eltwise_group_slice(device, flat, members, m, PN_TT_TILE_BF16, &slice)) + continue; + /* RED `eltwise_wide_key_skip`: a WIDE member is not told its prefill key — its + * slice's mirrors go stale and the grid's ffn_down reads them; must DIFFER. */ + if (skip && m >= device->eltwise_decode_members && which == 1u) + continue; + if (replica_map_add_key(table, device, strip, slice.byte_offset) != 0) + return -1; + } + /* PHASE W2: a SUPER-BAND producer writes `sb` bands at band j*sb (the last one + * shorter), so its member's slice of THAT width is a key of its own. */ + if (g_replica_superband && g_replica_span > g_replica_superband) { + uint32_t first; + + for (first = 0u; first < g_replica_span; first += g_replica_superband) { + const uint32_t n = g_replica_span - first < g_replica_superband + ? g_replica_span - first : g_replica_superband; + const uint32_t flat = n * strip->tile_count; + + members = pn_tt_eltwise_group_members_for(device, flat, FT_TT_OP_VECTOR_FN_SILU_MUL); + if (members > 1u && (flat % members) != 0u && + members <= device->eltwise_decode_members) + members = 1u; + if (m >= members || + !pn_tt_eltwise_group_slice(device, flat, members, m, PN_TT_TILE_BF16, + &slice)) + continue; + if (replica_map_add_key(table, device, strip, + first * strip->tile_count * PN_TT_TILE_BF16 + + slice.byte_offset) != 0) + return -1; + } + } + return 0; + } + members = pn_tt_eltwise_group_members_for(device, strip->tile_count, + FT_TT_OP_VECTOR_FN_SILU_MUL); + if (members > 1u && (strip->tile_count % members) != 0u) + members = 1u; + if (m >= members || + !pn_tt_eltwise_group_slice(device, strip->tile_count, members, m, PN_TT_TILE_BF16, + &slice)) + return 0; + for (band = 0u; band < bands; band++) + if (replica_map_add_key(table, device, strip, + band * strip->tile_count * PN_TT_TILE_BF16 + + slice.byte_offset) != 0) { + fprintf(stderr, "pn_tt: the replica table holds %u sets and %u band(s) need more " + "— lower %s, or the mirrors of the upper bands go STALE\n", + FT_TT_REPLICA_MAX_SETS, bands, PN_TT_STRIP_TILE_ROWS_ENV); + return -1; + } + return 0; +} + +/** The whole table ONE writer gets: every unsliced mirrored strip, and the split strip's keys + * for `member` (see replica_map_split_keys). */ +static int replica_map_build(pn_tt_device *device, const pn_tt_activation *const *strips, + uint32_t strip_count, const pn_tt_activation *split_output, + int member, ft_tt_op_replica_table *table) +{ + uint32_t index, band; + + memset(table, 0, sizeof(*table)); + for (index = 0u; index < strip_count; index++) { + const pn_tt_activation *strip = strips[index]; + const uint32_t bands = strip && strip->tile_rows ? strip->tile_rows : 1u; + + if (!strip || strip->replica_count < 2u) + continue; + /* BY PRODUCER (W2): the split strip (SwiGLU) is written by the eltwise members only, + * every other mirrored strip (`normed`) by the reduce/head-norm cores only — so each + * table holds only keys its own descriptors can hit, and the super-band keys fit. */ + if (strip == split_output) { + if (member >= 0 && replica_map_split_keys(device, table, strip, member) != 0) + return -1; + continue; + } + if (member >= 0 && split_output) + continue; + /* A BAND IS A SECOND SET OF KEYS: a producer writing band `m` writes `base + m * + * tile_count * 2048`, an address a one-set table never matches — measured, the + * plausible wrong answer at two bands (2026-08-22). A TALL strip's producers write + * the whole span in one descriptor, so its key is the base. */ + for (band = 0u; band < (bands > 4u ? 1u : bands); band++) + if (replica_map_add_key(table, device, strip, + band * strip->tile_count * PN_TT_TILE_BF16) != 0) + return -1; + if (bands > 4u && g_replica_superband && g_replica_span > g_replica_superband) + for (band = g_replica_superband; band < g_replica_span; band += g_replica_superband) + if (replica_map_add_key(table, device, strip, + band * strip->tile_count * PN_TT_TILE_BF16) != 0) + return -1; + } + return 0; +} + +int pn_tt_publish_replica_map(pn_tt_device *device, const pn_tt_activation *const *strips, + uint32_t strip_count, const pn_tt_activation *split_output) +{ + ft_tt_op_replica_table table; + uint32_t index; + + if (!device || (!strips && strip_count)) + return -1; + /* RED PROOF `PN_TT_RED=replica_stale`: allocate the mirrors and tell NOBODY about them. + * The producers then write only replica 0 and mirrors 1..R-1 stay at the zeros the + * allocation staged, so every matmul member except those on replica 0 multiplies a zero + * row. THE ANSWER IS GARBAGE, deliberately; it MUST differ. */ + if (pn_tt_red("replica_stale")) { + fprintf(stderr, "pn_tt: RED PROOF replica_stale — the mirrors are allocated and the " + "producers are never told, so every mirror but the first stays " + "ZERO. THE ANSWER IS GARBAGE unless nothing reads them.\n"); + strip_count = 0u; + } + /* EVEN AN EMPTY MAP IS PUBLISHED: the magic word with a zero set count is a different + * state from "this L1 region was never written". Every core running + * `resident_vector_writer.c` gets its OWN table (replica_map_split_keys). */ + if (replica_map_build(device, strips, strip_count, split_output, -1, &table) != 0 || + replica_map_publish_to(device, &device->reduce, &table, "the reduce residency") != 0 || + /* L4: the column-split member writes the same strips — without the table its half + * of every mirror goes stale (measured 2026-10-05: DIFFERS, deterministic). */ + replica_map_publish_to(device, &device->reduce_member, &table, + "the reduce member") != 0) + return -1; + for (index = 0u; index < device->head_norm_count; index++) + if (replica_map_publish_to(device, &device->head_norm[index].residency, &table, + "a head-norm residency") != 0) + return -1; + for (index = 0u; index < pn_tt_eltwise_group_members(device); index++) { + const pn_tt_residency *writer = + index == 0u ? &device->eltwise : &device->eltwise_member[index - 1u]; + + if (replica_map_build(device, strips, strip_count, split_output, (int)index, + &table) != 0 || + replica_map_publish_to(device, writer, &table, + index == 0u ? "the eltwise residency" + : "an eltwise split member") != 0) + return -1; + if (index + 1u == pn_tt_eltwise_group_members(device) && table.set_count) + fprintf(stderr, "pn_tt: replica map published — eltwise member %u holds %u " + "set(s), base (%u,%u)+0x%x\n", + index, table.set_count, table.set[0].destination[0].x, + table.set[0].destination[0].y, table.set[0].destination[0].base); + } + return 0; +} + +int pn_tt_publish_empty_replica_map(pn_tt_device *device, const pn_tt_residency *residency, + const char *label) +{ + ft_tt_op_replica_table table; + + if (!device || !residency) + return -1; + memset(&table, 0, sizeof(table)); + return replica_map_publish_to(device, residency, &table, label); +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_replica_map.h b/src/accel/tt/pn_tt_replica_map.h new file mode 100644 index 0000000..85dde5a --- /dev/null +++ b/src/accel/tt/pn_tt_replica_map.h @@ -0,0 +1,202 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_replica_map.h + * @brief Mirror the HOT activation strips across DRAM banks, and tell the producers. + * + * --------------------------------------------------------------------------- + * THE PROBLEM, MEASURED + * --------------------------------------------------------------------------- + * The Blackhole's 510.8 GB/s is EIGHT banks of ~59. Every member of a column-split matmul + * reads the WHOLE A strip, so N members are N readers of ONE bank. Per-bank during Ornith + * decode, counted from the descriptors: + * + * b0 5.8 b1 3.1 b2 43.1 b3 4.0 b4 4.9 b5 4.0 b6 3.1 b7 14.0 GB/s + * + * One bank at 73% of its own ceiling, seven nearly idle, the card at 16% of peak. That — + * and not a core-scaling knee — is why matmul card work is FLAT from 4 members to 16 and + * why the shipped setting is four cores on a 140-Tensix part. Adding readers to a saturated + * bank does nothing. + * + * The discriminating null is already recorded: the SAME bank 4 MiB apart reproduces the + * control to three decimals, while a DIFFERENT bank per member restores 1.90x / 1.53x a + * doubling. So it is the bank, not address locality and not a row buffer. + * + * --------------------------------------------------------------------------- + * WHY MIRRORS, AND NOT THE THREE CHEAPER THINGS + * --------------------------------------------------------------------------- + * 1. **More DOORS of the same bank** — already shipped (`PN_TT_ACTIVATION_DOORS`) and + * already exhausted. A door is a NoC port and lifts the ~45 GB/s ENDPOINT ceiling to the + * ~59 GB/s BANK ceiling. The bank ceiling is the one being hit. + * 2. **A different ALLOCATION POLICY** — there is none that helps. The traffic is N readers + * of one strip, so no placement of that one strip spreads it; only more than one copy of + * the bytes does, or a layout that puts different PARTS of the strip in different banks. + * 3. **INTERLEAVING the strip across banks by tile** — genuinely cheaper in bytes, and + * rejected. It moves the change into `resident_matmul_reader.c`, the one kernel the + * `cmp` gate rests on and the one with four program variants; it needs a bank table + * inside that reader plus a descriptor word to point at it; it breaks every OTHER + * consumer of the strip (the host uploads, the GDN op, attention, the elementwise + * reader), because the strip would no longer be contiguous anywhere; and it cuts A's + * request size from 16,384 B to 2,048, into a band this card is measurably worse in. + * Mirroring keeps replica 0 at the strip's own address, so every one of those consumers + * is untouched and the matmul reader does not change at all. + * 4. **COOPERATIVE multicast**, where a leader reads A once and broadcasts it — implemented, + * re-timed cleanly on 2026-08-19 with the residencies moved off the rectangle, and a + * NULL: control 0.5116, rect 0.5125, coop 0.5115 s/token. + * + * What mirroring costs is WRITES, which are the cheap direction on this card, and they are + * card-to-card. The strips are 256 KiB (`normed`) and 768 KiB (`ffn_swiglu`); at 8 mirrors + * that is ~112 MiB/token of extra NoC writes spread over eight banks. + * + * --------------------------------------------------------------------------- + * WHICH STRIPS, AND WHY ONLY THOSE + * --------------------------------------------------------------------------- + * Counted from Ornith's own shapes, A's bytes divide per layer as + * + * normed 70.6% ffn_swiglu 17.6% ssm_gated / attn_out 11.8% + * + * `normed` is the RMS-norm output that feeds all seven projections of every layer; + * `ffn_swiglu` is `silu(gate)*up` and feeds `ffn_down`. Their producers are the REDUCE and + * ELTWISE residencies — and both of those drain through the SAME kernel, + * `resident_vector_writer.c`, so ONE writer change covers 88% of A. The remaining 12% is + * produced by the host in the shipped forward, where `pn_tt_activation_upload()` fans out + * on its own. + * + * --------------------------------------------------------------------------- + * THE FAILURE MODE THIS FILE EXISTS TO PREVENT + * --------------------------------------------------------------------------- + * A mirror nobody refreshes is STALE, and a matmul member that reads it returns a plausible + * wrong answer — every kernel completes, every residency is healthy, no error word is set. + * So the map is published to every core that could produce a mirrored strip, and it is READ + * BACK and compared rather than trusted to the write's return code. + * + * @status Written 2026-08-19 and ON at 8 the same day, gated on the 48-token oracle across + * thirteen mirrored arms. `PN_TT_ACTIVATION_REPLICAS=1` is the control. + * @issues None + * @todo None + */ + +#ifndef PN_TT_REPLICA_MAP_H +#define PN_TT_REPLICA_MAP_H + +#ifdef USE_TT + +#include + +#include "pn_tt_device.h" +#include "pn_tt_residency.h" + +/** `PN_TT_ACTIVATION_REPLICAS=N` mirrors the hot strips into N distinct DRAM banks. */ +#define PN_TT_ACTIVATION_REPLICAS_ENV "PN_TT_ACTIVATION_REPLICAS" + +/** + * EIGHT — one per DRAM bank — SINCE 2026-08-19, and it is a measured default rather than a + * shape argument. + * + * Gated on the 48-token oracle and nothing softer: thirteen mirrored arms across member + * counts 1..32 and mirror counts 2/4/8 are all `cmp` IDENTICAL (189 B, md5 `80fc6259`), + * every one at `company=0` with zero residency re-arms, beside eleven unmirrored controls + * and a red arm (`PN_TT_RED=replica_stale`) that DIFFERS. + * + * What it buys, at 16 members: matmul card work **0.0959 -> 0.0595 s/token (1.61x)** and + * the per-bank table flat — b2 41.6 GB/s against seven banks at 11-19 becomes 25.5-29.0 + * across all eight. What it costs is the fan-out: the on-card families go 0.040 -> 0.042 + * s/token, i.e. **2 ms against the 36 ms it returns.** + * + * `PN_TT_ACTIVATION_REPLICAS=1` is the control arm and is the pre-replication forward word + * for word — the map is then empty and every producer's writer misses it. + */ +#define PN_TT_ACTIVATION_REPLICAS_DEFAULT 8u + +/** How many mirrors this run asked for, clamped to the bank count. 1 means off. */ +uint32_t pn_tt_activation_replica_count(const pn_tt_device *device); + +/** + * Publish the replica map to every residency that could produce one of these strips. + * + * The map is keyed on the strip's PRIMARY (bank coordinate, address), which is what the + * producing descriptor already carries in its output words — the REDUCE family has no free + * parameter word left, so the fan-out cannot be addressed per descriptor. + * + * Strips with one replica are skipped rather than refused: a caller passes its whole strip + * list and the ones that were not mirrored simply do not appear in the map. + * + * WRITTEN TO THE REDUCE, ELTWISE AND HEAD-NORM CORES, because those are the residencies + * running `resident_vector_writer.c`. A core that never produces a mirrored strip is + * harmless to tell: the lookup is keyed on the exact destination triple, so it misses. + * + * VERIFIED BY READ-BACK. A half-written map is a stale mirror and a stale mirror is a + * plausible wrong answer; there is no refusal anywhere downstream to catch it. + * + * --------------------------------------------------------------------------- + * `split_output` — AND WHY THE ELEMENTWISE SPLIT CANNOT BE TOLD FROM THE OTHERS + * --------------------------------------------------------------------------- + * **MEASURED ON SILICON 2026-08-24, AND IT COST THE LEVER ITS `cmp`.** The lookup is the + * EXACT triple, and the header above says what a miss does: "a miss is the shipped path word + * for word". `PN_TT_ELTWISE_CORES=N` hands member `m` the SAME descriptor with + * `C_BASE + m * slice_bytes` — so member 0 hits the table and mirrors its slice, and every + * member above it MISSES and leaves its slice stale in seven banks, which the matmul members + * then read. The completion is clean, every kernel retires, nothing anywhere refuses. The + * arms: `cmp` IDENTICAL at one member, 204 B at two, 8 B at four, 57 B at sixteen — wrong in + * proportion to `(N-1)/N` of the strip, and the discriminating null is + * `PN_TT_ACTIVATION_REPLICAS=1`, which is IDENTICAL at every member count because there is + * then no table to miss. + * + * So a strip a SPLIT family produces needs one set per slice, not one set. `split_output` is + * that strip — named by the caller, because nothing here can infer a producer, and the + * distinction is load-bearing: `normed` is the REDUCE's output and reduce is never split, so + * slicing it would spend the four-set table on entries no descriptor can hit. + * + * **AND THE MEMBERS ARE PUBLISHED TO.** They run `resident_vector_writer.c` like every other + * core in this list; before this they were armed with no table at all, which is not "no + * mirroring" but "whatever that L1 region held", and the file header is explicit that a map + * left by a previous process is the difference between missing and fanning out to somebody + * else's addresses. + * + * The table holds `FT_TT_REPLICA_MAX_SETS`. When a split needs more than that, the SPLIT is + * collapsed to one core here and said out loud — the alternative is a correct-looking map + * that silently covers some slices and not others. + * + * @param strips pointers to the strips that may be mirrored, in any order + * @param strip_count how many + * @param split_output the one of them the ELEMENTWISE family produces, or NULL + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_publish_replica_map(pn_tt_device *device, const pn_tt_activation *const *strips, + uint32_t strip_count, const pn_tt_activation *split_output); + +/** PHASE G: the prefill span (bands a pass's ops cover) the keys of a TALL strip (> 4 bands) + * are published for; republish after changing it. 0/1 = decode only. */ +void pn_tt_replica_map_set_span(uint32_t span); + +/** PHASE W2: producers also write SUPER-BANDS of `bands` bands at band j*bands of the prefill + * span; key those too (0 = off). Republish after changing it. */ +void pn_tt_replica_map_set_superband(uint32_t bands); + +/** + * Write an EMPTY table to one residency that runs `resident_vector_writer.c` and was armed + * AFTER `pn_tt_publish_replica_map()` had already run. + * + * "Empty" and "never written" are different states on this card, and only one of them is + * safe. The table lives at a FIXED L1 address inside a region every program owns, so a + * residency nobody published to reads whatever is at `FT_TT_REPLICA_TABLE_ADDRESS` — the + * magic word protects a zeroed or erased region, but it does not protect against a REAL + * table another process wrote on that core. An empty table with the magic word and a zero + * set count is a miss for every descriptor, which is the shipped path word for word, and it + * is a written fact rather than an assumption. + * + * The merged gate's output (`ssm_gated`) is genuinely not mirrored — A's bytes are `normed` + * 70.6% / `ffn_swiglu` 17.6% and only those two are replicated — so empty is the CORRECT + * table here and not a stub. + * + * @return 0, or -1 with the reason on stderr. A residency that is not running is a no-op. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_publish_empty_replica_map(pn_tt_device *device, const pn_tt_residency *residency, + const char *label); + +#endif /* USE_TT */ +#endif /* PN_TT_REPLICA_MAP_H */ diff --git a/src/accel/tt/pn_tt_residency.c b/src/accel/tt/pn_tt_residency.c new file mode 100644 index 0000000..efd5a1b --- /dev/null +++ b/src/accel/tt/pn_tt_residency.c @@ -0,0 +1,1234 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_residency.c + * @brief Implementation of one resident Tensix program on one core (see pn_tt_residency.h). + * + * @status Green: the reduce and elementwise residencies of the on-card forward run on it. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "pn_tt_residency.h" + +#include +#include +#include +#include + +#include "pn_tt_name_list.h" + +#include "tt/ft_tt_coord.h" +#include "tt/ft_tt_op_dispatch.h" +#include "tt/ft_tt_op_l1_layout.h" +#include "tt/ft_tt_program_resolve.h" + +/** Consecutive empty polls before a resident kernel returns on its own, and spins inside + * ONE circular-buffer wait before it abandons that wait. Neither may be 0: an unbounded + * wait is the one thing a resident kernel may not be configured into. */ +#define PN_TT_RESIDENCY_IDLE_SPINS FT_TT_QUEUE_DEFAULT_IDLE_SPINS +#define PN_TT_RESIDENCY_CB_WAIT_SPINS 100000000u + +/** Idle milliseconds after which an enqueue retires the residency first. See + * residency_idle_retire(). */ +#define PN_TT_RESIDENCY_IDLE_RETIRE_ENV "PN_TT_RESIDENCY_IDLE_RETIRE_MS" +/* 5000 ms: 1.6x under the fastest measured idle exit (~8 s), and past the 2.3-4.7 s load gap, + * so only the families idle through a prefill retire — 237.2 vs 234.0 tok/s at 2000. */ +#define PN_TT_RESIDENCY_IDLE_RETIRE_DEFAULT_MS 5000L +/** A keepalive NOP is the cheapest descriptor there is; a second is far past any healthy one. */ +#define PN_TT_RESIDENCY_KEEPALIVE_TIMEOUT_MS 1000u + +/** Polls of `pn_tt_residency_stop`'s exit wait. Each is a device read of ~3.5 µs, so this + * is comfortably longer than the longest descriptor and still bounded. */ +#define PN_TT_RESIDENCY_STOP_POLLS 20000u + +/** Every processor of a triad. */ +#define PN_TT_RESIDENCY_TRIAD_MASK \ + ((1u << FT_TT_PROC_DM0) | (1u << FT_TT_PROC_DM1) | (1u << FT_TT_PROC_MATH0) | \ + (1u << FT_TT_PROC_MATH1) | (1u << FT_TT_PROC_MATH2)) + +static double pn_tt_residency_now(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0.0; + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +/** + * THE AWAIT IS THE PER-DESCRIPTOR HOST COST OF EVERY FAMILY EXCEPT THE MATMUL, AND IT IS + * SEVENTEEN NON-POSTED READS. `PN_TT_RESIDENCY_AWAIT=control|window|line`, default `window`. + * + * --------------------------------------------------------------------------- + * WHAT THE SEVENTEEN ARE, AND WHY THE MATMUL'S 6.9 us IS A DIFFERENT NUMBER + * --------------------------------------------------------------------------- + * `residency->runtime->noc` is the UC / WORD32 accessor: **one 32-bit PCIe access per word** + * (ft_tt_runtime.h). Per descriptor per member this path issues + * + * ISSUE `write_op_parameters` 64 B -> 16 word writes, POSTED + * `write_ring_run` 32 B -> 8 word writes, POSTED + * `publish` 4 B -> 1 word write, POSTED + * AWAIT `read_field(COUNT)` 4 B -> 1 word read, NON-POSTED + * `read_control` 64 B -> 16 word reads, NON-POSTED + * + * The recorded matmul figures — a 64-byte parameter block at **2.08 us** and a 32-byte + * descriptor plus a 4-byte head bump at **4.84 us**, ~6.9 us together — are the ISSUE side, + * and issue is posted stores. **They were never a measurement of the await.** The matmul's + * own await never came through here: `pn_tt_group_await()` reads + * `read_completion_wide()` (two BLOCK16) and, with `PN_TT_FAST_COMPLETION` on, ONE + * `read_completion_line()`. So the two figures do not disagree; they are different halves. + * + * At the recorded ~3.4 us a non-posted read, seventeen is **57.8 us** — which is the + * ~58 us/descriptor the eltwise split's flat per-member await measured + * (0.0079-0.0080 s/token over 136 descriptors = 58.1 us), identical to three digits at + * N=2 and N=16 because it never touched the card's work in the first place. + * + * **SO THE PARAMETER CACHE IS 2.08 OF 58 us — 3.6%** — and the lever WAS the await. With the + * await at ONE access, the 25 posted writes are what is left: `PN_TT_ISSUE=word|wide` takes + * them to seven by moving the two 16-byte-aligned writes onto the BLOCK16 accessor. That knob, + * and the WC/UC store-ordering seam it opens, are documented in `pn_tt_issue.h`. + * + * --------------------------------------------------------------------------- + * THREE SHAPES, ONE KNOB, ONE BINARY — WHICH IS THE POINT OF THE KNOB + * --------------------------------------------------------------------------- + * `control` `read_field(COUNT)` + `read_control()` 1 + 16 = 17 what shipped + * `window` `read_field(COUNT)` + `read_completion()` 1 + 5 = 6 + * `line` `read_completion_line()` on BLOCK16 1 DEFAULT + * + * `control` is kept ALIVE rather than deleted so the ladder's control can be re-taken on the + * SAME BINARY in the same hour. A control measured on yesterday's build is how this program + * produced a 19% prediction that measured 3.4%. + * + * `window` gives up NOTHING. The completion window is words 5..9 and all four of rule 1's + * agreements are inside it — COUNT 5, TOKEN 6, STATE 7, ERROR 9 — on the same accessor, from + * the same L1, checked by the same function. `tests/test_queue.c` reads both and asserts the + * four words are equal, so this is a fact about the code and not a claim in a header. That is + * why it is the default with no silicon arm behind it: there is no state in which it can + * disagree with `control`, and one env var puts `control` back with no rebuild. + * + * --------------------------------------------------------------------------- + * `line` IS NOW THE DEFAULT, AND WHAT IT GIVES UP IS ZERO ON *THIS* PATH — BY CONSTRUCTION + * --------------------------------------------------------------------------- + * `line` does not read `error` (word 9) on the success side. That was why it stayed opt-in. + * The exposure is precisely: a descriptor whose COUNT, TOKEN and STATE all agree AND whose + * `error` word is set. `resident_queue_protocol.h` rule 5 — "ABANDONED IS NOT COMPLETED: a + * descriptor whose wait exited on stop or on its bound advances `tail` and does NOT advance + * `completed_count`" — makes every kernel error path leave the count SHORT, where the poll + * never succeeds and the 128th-poll error read names it. The protocol raises exactly ONE + * fault after completion has been published: `ftq_group_post()`'s refused inline write, and + * that function returns immediately when `ctx->group_address == 0`. + * + * **NO RESIDENCY ON THIS PATH ARMS THE GROUP POST.** `pn_tt_residency_start()` builds its + * kernel binding from one `memset` and sets three fields, none of them `group_address`; + * `pn_tt_matmul_group.c` is the only file in the tree that sets one, and the matmul's await + * never comes through here — it uses `pn_tt_group_await()`. That is not left as a comment: + * `residency->posts_group` is READ OFF the binding that was actually built, and a residency + * that ever armed the post FALLS BACK to the window, loudly. `PN_TT_RED=residency_posts_group` + * is the arm that proves the guard can fire, because nothing in the tree can make it. + * + * The remaining failure modes are all FAIL-SAFE rather than fail-silent. A stale BLOCK16 line + * can only show an OLD count, which spins one more iteration; the count and the token come + * out of the SAME 16 bytes of the SAME transaction, so there is no window between reading a + * count and reading the token it entitled you to — strictly better ordered than + * poll-then-check. And a refusal still NAMES `error`: the failure path re-reads the full + * window before printing. The matmul group has run this same read as its default since + * 2026-08-20 over 6.49 million poll reads in one run, byte-identical. + * + * `window` remains one env var away and is the shape to fall back to if a family ever does + * arm the group post. + */ +#define PN_TT_RESIDENCY_AWAIT_ENV "PN_TT_RESIDENCY_AWAIT" + +/** The knob, read ONCE. A `getenv` per descriptor is 136 a token on this family alone. + * An unrecognised value is REFUSED to the default and says so, rather than silently + * selecting one — a typo here would attribute a whole ladder to the wrong shape. */ +static pn_tt_await_shape residency_await_shape(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv(PN_TT_RESIDENCY_AWAIT_ENV); + + cached = (int)PN_TT_AWAIT_LINE; + if (value && *value) { + if (strcmp(value, "control") == 0) + cached = (int)PN_TT_AWAIT_CONTROL; + else if (strcmp(value, "window") == 0) + cached = (int)PN_TT_AWAIT_WINDOW; + else if (strcmp(value, "line") == 0) + cached = (int)PN_TT_AWAIT_LINE; + else + fprintf(stderr, "pn_tt: %s=%s is not control|window|line — using the default " + "(line)\n", PN_TT_RESIDENCY_AWAIT_ENV, value); + } + } + return (pn_tt_await_shape)cached; +} + +/** + * RED PROOF ONLY. `PN_TT_RED=residency_await_early` asks the four-agreement CHECK about one + * descriptor MORE than was issued, while leaving the POLL alone. + * + * It exists because a ladder over three completion shapes is only evidence if the check on + * each of them is live. A shape that would be green with no check at all is measuring the + * card's timing, not the protocol — and this backend has already caught twelve vacuous-gate + * classes. The wait still ends when the card really is finished, so the arm is bounded and + * the ring is untouched; only the verdict is under test. Every shape must REFUSE, on the + * first descriptor, naming `completed_count`. + */ +static bool residency_red_await_early(void) +{ + static int cached = -1; + + if (cached < 0) { + /* SELECTS, NOT CONTAINS. `pn_tt_name_list_contains()` answers TRUE for an UNSET list + * — it is a filter, "unset means every family" — so arming a red arm with it armed + * the arm on EVERY run and refused every descriptor of every family at token 0. + * Measured on silicon 2026-08-25; pn_tt_name_list.h carries the account. */ + cached = pn_tt_name_list_selects(getenv("PN_TT_RED"), "residency_await_early") ? 1 : 0; + } + return cached != 0; +} + +/** + * RED PROOF ONLY. `PN_TT_RED=residency_posts_group` claims this residency arms the group + * post, so the LINE shape's exclusivity check must fire and fall back to the window. + * + * The check it arms is the whole of the `line` argument turned into code, and a check that + * cannot fire is the thirteenth vacuous-gate class this backend has now paid for twice. + * `binding.group_address` is a literal zero out of ONE `memset` in ONE function, so nothing + * in the tree can make the guard fire on its own — which is exactly why it needs an arm that + * can. SELECTS, not CONTAINS: an unset list selects nothing. + */ +static bool residency_red_posts_group(void) +{ + static int cached = -1; + + if (cached < 0) + cached = pn_tt_name_list_selects(getenv("PN_TT_RED"), "residency_posts_group") ? 1 : 0; + return cached != 0; +} + +int pn_tt_residency_load(pn_tt_residency *residency, ft_tt_runtime *runtime, + const char *program_name, const char *override_root, const char *name, + uint32_t mask) +{ + const char *where = override_root ? override_root : "this binary's embedded program blob"; + + if (!residency || !runtime || !program_name || !name) + return -1; + memset(residency, 0, sizeof(*residency)); + /* A HANDLE THAT HAS NEVER RUN IS TRIVIALLY RETIRED. The memset above makes it false, and + * a caller asking `pn_tt_residency_retired()` before anything started would then refuse to + * arm anything at all. */ + residency->retired = true; + residency->runtime = runtime; + snprintf(residency->name, sizeof(residency->name), "%s", name); + + /* Firmware and kernels from the SAME program, always — and one call each, with the same + * name and the same override, so the pairing is structural. A kernel's text is linked at + * its own firmware's `__fw_export_text_end`, so a mixed pair is a wedged core and a + * card-level reset, not a configuration. */ + if (ft_tt_program_open_firmware(program_name, override_root, &residency->firmware) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: no %s firmware in %s — %s stays on the host\n", program_name, + where, name); + return -1; + } + if (ft_tt_program_open_kernels(program_name, override_root, mask, &residency->program) != + FT_TT_OK) { + fprintf(stderr, "pn_tt: no %s program in %s — that op stays on the host\n", name, where); + ft_tt_runtime_images_release(&residency->firmware); + return -1; + } + residency->images_loaded = true; + residency->processor_mask = mask; + return 0; +} + +static int residency_bind_core(pn_tt_residency *residency, uint32_t core_index) +{ + ft_tt_noc_coord core; + int rc; + + if (!residency || !residency->images_loaded) + return -1; + rc = ft_tt_coord_tensix_by_index(&residency->runtime->grid, core_index, + FT_TT_COORD_TRANSLATED, &core); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: Tensix index %u is not on this part (%s)\n", core_index, + residency->name); + return -1; + } + residency->core_index = core_index; + residency->core_x = (uint16_t)core.x; + residency->core_y = (uint16_t)core.y; + + rc = ft_tt_runtime_boot_core(&residency->runtime->noc, residency->core_x, residency->core_y, + &residency->firmware); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s core %u (%u,%u) firmware boot failed: %s\n", residency->name, + core_index, core.x, core.y, ft_tt_error_string(rc)); + return -1; + } + residency->core_booted = true; + return 0; +} + +uint32_t pn_tt_residency_queue_capacity(void) +{ + static uint32_t cached; + + if (cached == 0u) { + const char *value = getenv("PN_TT_QUEUE_CAPACITY"); + const long parsed = (value && *value) ? strtol(value, NULL, 10) : 0; + + cached = (parsed >= 2 && parsed <= (long)PN_TT_RESIDENCY_QUEUE_CAPACITY && + (parsed & (parsed - 1)) == 0) + ? (uint32_t)parsed + : PN_TT_RESIDENCY_QUEUE_CAPACITY_DEFAULT; + } + return cached; +} + +static int residency_start(pn_tt_residency *residency) +{ + static const unsigned processors[5] = { FT_TT_PROC_DM0, FT_TT_PROC_DM1, FT_TT_PROC_MATH0, + FT_TT_PROC_MATH1, FT_TT_PROC_MATH2 }; + uint32_t args[5][FT_TT_QUEUE_KERNEL_ARG_COUNT]; + ft_tt_op_args blocks[5]; + unsigned index; + int rc; + + if (!residency || !residency->core_booted || residency->running) + return -1; + + rc = ft_tt_queue_init(&residency->runtime->noc, residency->core_x, residency->core_y, + FT_TT_OP_QUEUE_BASE, pn_tt_residency_queue_capacity(), + ft_tt_queue_make_nonce(), &residency->queue); + /* ASKED, NOT ASSUMED. `FT_TT_OP_QUEUE_BASE` is 16-byte aligned today, so `line` is + * reachable today — but a queue placed on a word boundary is legal to + * `ft_tt_queue_init()`, works through the WORD32 accessor, and would make every BLOCK16 + * read fail. On this path that failure arrives as a completion that never completes: a + * retire-and-re-arm storm, not a diagnosable refusal. So the shape FALLS BACK loudly + * rather than arming something the accessor cannot reach. */ + if (rc == FT_TT_OK) { + residency->await_shape = residency_await_shape(); + if (residency->await_shape == PN_TT_AWAIT_LINE && + !ft_tt_queue_completion_line_is_addressable(&residency->queue)) { + fprintf(stderr, "pn_tt: %s control block at 0x%08x is not 16-byte aligned — the " + "completion LINE is unreachable; falling back to the window\n", + residency->name, residency->queue.control_address); + residency->await_shape = PN_TT_AWAIT_WINDOW; + } + /* THE SAME QUESTION IN THE WRITE DIRECTION, asked once here for the same reason. A + * demotion, not a refusal: `word` reaches any placement the queue is legal at. */ + residency->issue_shape = pn_tt_issue_shape_for(residency->runtime, &residency->queue); + if (residency->issue_shape != pn_tt_issue_shape_selected()) + fprintf(stderr, "pn_tt: %s control block at 0x%08x cannot carry the WIDE issue " + "shape — falling back to word\n", + residency->name, residency->queue.control_address); + } + if (rc == FT_TT_OK) + rc = ft_tt_op_program_load(&residency->runtime->noc, residency->core_x, + residency->core_y, &residency->program.program, NULL, + &residency->config); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s could not be staged: %s\n", residency->name, + ft_tt_error_string(rc)); + return -1; + } + residency->config.local_cb_mask = residency->cb_mask; + residency->config.local_cb_config_address = FT_TT_OP_CB_CONFIG_ADDRESS; + /* The reader is on BRISC and was built with NOC_INDEX=0; the firmware re-reads this + * from the launch message at run time, so the two must agree. */ + residency->config.brisc_noc_id = 0u; + + for (index = 0; index < 5u; index++) { + ft_tt_queue_kernel_binding binding; + + memset(&binding, 0, sizeof(binding)); + binding.idle_spin_limit = PN_TT_RESIDENCY_IDLE_SPINS; + binding.cb_wait_spin_limit = PN_TT_RESIDENCY_CB_WAIT_SPINS; + /* Per-processor status. "The triad is stuck" is not a diagnosis; "NCRISC is in + * CB_WAIT_FRONT at descriptor 12" is, and only these slots can say it. */ + binding.status_address = + FT_TT_QUEUE_STATUS_ADDRESS(residency->queue.control_address, processors[index]); + /* THE GROUP BINDING, AND IT IS NO LONGER A LITERAL ZERO. Everything the `line` + * shape's exclusivity argument says about this stays true — the reading below is + * still taken off the binding that was actually built, and a residency that set one + * still loses the line. What has changed is that a caller can now set it, which is + * the whole of the split's completion lever. */ + binding.group_x = residency->group_x; + binding.group_y = residency->group_y; + binding.group_address = residency->group_address; + binding.group_member = residency->group_member; + rc = ft_tt_queue_kernel_args(&residency->queue, &binding, args[index]); + if (rc != FT_TT_OK) + return -1; + /* READ OFF THE BINDING THAT WAS ACTUALLY BUILT, not off a comment. This is the ONE + * fact the `line` shape's whole argument rests on — see the guard below. */ + if (binding.group_address != 0u) + residency->posts_group = true; + blocks[index].processor = processors[index]; + blocks[index].values = args[index]; + blocks[index].count = FT_TT_QUEUE_KERNEL_ARG_COUNT; + } + if (residency_red_posts_group()) { + fprintf(stderr, "pn_tt: RED PROOF residency_posts_group — %s is claimed to arm the " + "group post, so the LINE completion shape must decline\n", + residency->name); + residency->posts_group = true; + } + /* THE ONE STATE THE LINE CANNOT SEE, MADE UNREACHABLE BY CONSTRUCTION RATHER THAN BY + * DISCIPLINE — the same move `pn_tt_matmul_group.c` makes for `PN_TT_FAST_COMPLETION`. + * + * `line` reads words 4..7 and does not read `error` (word 9) on the success side. Rule 5 + * of `resident_queue_protocol.h` is that an ABANDONED descriptor advances `tail` and does + * NOT advance `completed_count`, so every kernel error path leaves the count short, the + * poll never succeeds, and the 128th-poll error read names it. The protocol has EXACTLY + * ONE fault raised after completion is published: `ftq_group_post()`'s refused inline + * write, which returns immediately when `ctx->group_address == 0`. So a residency that + * never arms the group post has nothing to give up — and the loop above is where that + * stops being an argument and becomes a reading. */ + if (residency->await_shape == PN_TT_AWAIT_LINE && residency->posts_group) { + fprintf(stderr, "pn_tt: %s arms the group post, whose refusal is the one fault raised " + "AFTER completion is published — the completion LINE cannot see it; " + "falling back to the window\n", residency->name); + residency->await_shape = PN_TT_AWAIT_WINDOW; + } + + rc = ft_tt_op_stage(&residency->runtime->noc, residency->core_x, residency->core_y, + &residency->config, residency->cb_buffer, sizeof(residency->cb_buffer), + blocks, 5u); + if (rc == FT_TT_OK) + rc = ft_tt_launch_send_go(&residency->runtime->noc, residency->core_x, + residency->core_y); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s did not kick: %s\n", residency->name, + ft_tt_error_string(rc)); + return -1; + } + residency->running = true; + residency->retired = false; + residency->last_alive_seconds = pn_tt_residency_now(); + return 0; +} + +static void residency_stop(pn_tt_residency *residency) +{ + /* THE MASK THE PROGRAM WAS LOADED WITH, not every processor of a triad. + * + * The memory family (EMBEDDING / KV_APPEND) has NO compute thread and enables DM0|DM1 + * only, so its three TRISCs never run this program and never write EXIT into their status + * slots. Waiting on all five therefore spun the full 20,000 polls and then printed + * `still running: 0x1c` — which is the exact shape of the wedge signature this project + * has paid cores for, on a core that was perfectly healthy. MEASURED on 2026-08-15: the + * KV-append residency reported it on every clean shutdown. A false alarm that looks like + * the real alarm is worse than no alarm, because it teaches you to ignore the real one. */ + const uint32_t mask = residency ? residency->processor_mask : PN_TT_RESIDENCY_TRIAD_MASK; + uint32_t pending = mask; + unsigned attempt; + + /* THE EARLY RETURN MUST NOT TOUCH `retired`, AND THAT COST A SECOND WEDGED CORE. + * + * `pn_tt_residency_release()` calls this function AGAIN, and by then `running` is already + * false — so an early return that set `retired = true` overwrote the answer the real stop + * had just computed. MEASURED ON SILICON 2026-08-22: the stop printed `did not retire`, + * the release cleared the flag, the guard read `true` and armed anyway, and core 90 wedged + * a second time with the guard nominally in place. A fresh handle is `retired` from + * `pn_tt_residency_load()`; a started one is not; only a confirmed exit sets it back. */ + if (!residency || !residency->running || mask == 0u) + return; + residency->running = false; + residency->retired = false; + if (ft_tt_queue_request_stop(&residency->runtime->noc, &residency->queue) != FT_TT_OK) { + fprintf(stderr, "pn_tt: could not ask %s to stop\n", residency->name); + return; + } + for (attempt = 0; attempt < PN_TT_RESIDENCY_STOP_POLLS; attempt++) { + if (ft_tt_queue_all_processors_exited(&residency->runtime->noc, &residency->queue, mask, + &pending) == FT_TT_OK) { + residency->retired = true; + return; + } + } + + /* THE GRACE, AND IT IS THERE BECAUSE 20,000 POLLS IS 60 ms AND THE THING BEING WAITED FOR + * TAKES SECONDS. + * + * A REFUSED descriptor makes the reader exit and leaves the compute thread to spin out its + * whole 400,000,000-iteration bound — that is the family's design, not a fault — so a stop + * issued straight after one finds a processor still running for as long as that bound + * takes. The tight loop above cannot cover it: it is a NoC read a poll, ~3 us, so it gives + * up after 60 ms. What used to happen next is that the caller booted firmware onto that + * core, and MEASURED ON SILICON 2026-08-21 that is a WEDGE — core 90 came back + * `instrn_buf_status = 0x66` and needed `ft_tt_core recover --rung 3` with sudo. + * + * A millisecond of sleep between polls, up to the grace, costs nothing on the path where + * the core retires immediately (it never runs) and turns the other path from a wedge into + * a wait. If the grace expires the residency is left NOT RETIRED and says so, and + * `pn_tt_residency_retired()` is what a caller asks before it boots anything. */ + /* THE GRACE IS WALL TIME, not a count of naps: a signal (EINTR, e.g. the PN_PROFILE + * sampler's SIGPROF) cuts a nap short, and a counted loop would give up early. */ + { + struct timespec nap = { 0, 1000000L }; + const double grace_started = pn_tt_residency_now(); + unsigned millisecond; + + for (millisecond = 0; + (pn_tt_residency_now() - grace_started) * 1000.0 < (double)PN_TT_RESIDENCY_STOP_GRACE_MS; + millisecond++) { + (void)nanosleep(&nap, NULL); + if (ft_tt_queue_all_processors_exited(&residency->runtime->noc, &residency->queue, + mask, &pending) == FT_TT_OK) { + residency->retired = true; + if (millisecond > 100u) + fprintf(stderr, "pn_tt: %s retired after %u ms of grace — a refused " + "descriptor leaves the compute thread spinning out its " + "bound\n", residency->name, millisecond); + return; + } + } + } + fprintf(stderr, "pn_tt: %s did not retire; processors still running: 0x%x " + "(the core is left alone, NOT parked and NOT reset). NOTHING MAY BOOT " + "FIRMWARE ON IT — that is the wedge.\n", + residency->name, pending); +} + +bool pn_tt_residency_retired(const pn_tt_residency *residency) +{ + return residency && residency->retired; +} + +void pn_tt_residency_release(pn_tt_residency *residency) +{ + if (!residency) + return; + pn_tt_residency_stop(residency); + if (residency->images_loaded) { + ft_tt_runtime_images_release(&residency->program); + ft_tt_runtime_images_release(&residency->firmware); + residency->images_loaded = false; + } +} + +/** + * Wait for `expected` descriptors to be reported EXECUTED — or for the kernel to say it + * refused one, which is a different thing and must not be waited out. + * + * On the LINE shape the poll IS the check: `out_fields` comes back holding words 4..7 of the + * iteration that saw the count, so the caller has the count and the token out of ONE + * transaction with no window between them. On the slow path `out_fields` is untouched and + * the caller reads the completion window itself. + * + * @param out_fields filled on the LINE shape only; must not be NULL + * @return FT_TT_OK; FT_TT_ERROR if the kernel reported an error; FT_TT_EIO on timeout + */ +static int pn_tt_residency_wait(pn_tt_residency *residency, uint32_t expected, + uint32_t timeout_ms, + uint32_t out_fields[FT_TT_QUEUE_FIELD_COUNT]) +{ + const double deadline = pn_tt_residency_now() + (double)timeout_ms / 1000.0; + unsigned poll = 0; + + if (residency->await_shape == PN_TT_AWAIT_LINE) { + for (;;) { + int rc = ft_tt_queue_read_completion_line(&residency->runtime->bulk_l1, + &residency->queue, out_fields); + + residency->await_poll_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (out_fields[FT_TT_QUEUE_FIELD_COMPLETED_COUNT] >= expected) + return FT_TT_OK; + if ((++poll & 127u) == 0u) { + uint32_t error = 0; + + /* `error` is word 9 and deliberately outside the line. This is where a + * REFUSAL is found — the count it is waiting for will never arrive. */ + rc = ft_tt_queue_read_field(&residency->runtime->noc, &residency->queue, + FT_TT_QUEUE_FIELD_ERROR, &error); + residency->await_poll_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (error != FT_TT_QUEUE_KERR_NONE) + return FT_TT_ERROR; + if (pn_tt_residency_now() > deadline) + return FT_TT_EIO; + } + } + } + + for (;;) { + uint32_t value = 0; + int rc = ft_tt_queue_read_field(&residency->runtime->noc, &residency->queue, + FT_TT_QUEUE_FIELD_COMPLETED_COUNT, &value); + + residency->await_poll_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (value >= expected) + return FT_TT_OK; + if ((++poll & 127u) == 0u) { + uint32_t error = 0; + + rc = ft_tt_queue_read_field(&residency->runtime->noc, &residency->queue, + FT_TT_QUEUE_FIELD_ERROR, &error); + residency->await_poll_transactions += 1u; + if (rc != FT_TT_OK) + return rc; + if (error != FT_TT_QUEUE_KERR_NONE) + return FT_TT_ERROR; + if (pn_tt_residency_now() > deadline) + return FT_TT_EIO; + } + } +} + +/** + * `PN_TT_ONCARD_BATCH`, read once and remembered. + * + * THE SAME INSTRUMENT AS `PN_TT_GDN_BATCH`, aimed at every OTHER family — the reduce, the + * SwiGLU, the residual add, the top-N, the attention, the per-head norms. The spec prices + * those at "162 descriptors a token, 247 µs each, for ops whose Tensix work is microseconds" + * and concludes the token is round-trip-shaped. That is an inference from a number, not a + * measurement of one, and this is the measurement: if 247 µs is a PCIe round trip then K + * copies of a descriptor cost what one costs, and if it is the card's work they cost K times. + * + * A stopwatch, not a fast path — an in-place residual add applied K times is not the residual + * add the model wanted. The arm's timings are evidence; its text is not. + */ +static uint32_t oncard_batch(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_ONCARD_BATCH"); + long parsed = (value && *value) ? strtol(value, NULL, 10) : 0; + + cached = (parsed > 0 && parsed <= (long)pn_tt_residency_queue_capacity()) ? (int)parsed : 1; + } + return (uint32_t)cached; +} + +/** + * Is `name` in `PN_TT_ONCARD_BATCH_FAMILY`, a comma-separated list? + * + * WHY THE FILTER EXISTS: batching is a stopwatch that repeats a descriptor, so it is only + * ANSWER-PRESERVING for a family whose descriptor is idempotent. `add(a, b, out)` and + * `silu_mul(a, b, out)` write a pure function of two operands they do not touch, so K copies + * leave exactly what one leaves — the elementwise family and both renders can be batched with + * the completion still comparable to the oracle. The GDN recurrence reads and writes its + * carried state, and the per-head L2 norm runs IN PLACE; batching those computes a value the + * model does not want, and a `cmp` taken over them says nothing. + * + * Unset means EVERY family, which is what `PN_TT_ONCARD_BATCH` did before this filter existed + * and is the shape every recorded batch arm was taken in. + */ +static bool oncard_batch_wants(const char *name) +{ + return pn_tt_name_list_contains(getenv("PN_TT_ONCARD_BATCH_FAMILY"), name); +} + +/** + * Does `PN_TT_RED=issue_head_first` fire on THIS enqueue? ONCE per process, and only after the + * residency's ring has wrapped exactly once. + * + * BOTH CONDITIONS ARE THE ARM, not caution about it. Firing on the FIRST descriptor would + * publish a head over a ZEROED ring slot, which is opcode 0 — an unknown opcode the kernel + * refuses by name, so the arm would prove that a *malformed* descriptor is caught and say + * nothing about ordering. After one lap the slot holds a WELL-FORMED descriptor from the + * previous lap: the count matches, the kernel reports no error, and the only disagreement left + * is the per-index token. That is the failure a WC store overtaking a UC one would actually + * produce, and it is the one the arm has to be able to see. + * + * ONCE, because a refused descriptor strands the triad and the family re-arms: firing every + * enqueue is a refusal storm, and this backend has already paid for a wedged core. + */ +static bool residency_red_head_first_fires(const pn_tt_residency *residency) +{ + static bool fired = false; + + if (fired || !pn_tt_issue_red_head_first()) + return false; + if (residency->issues < residency->queue.capacity) + return false; + fired = true; + return true; +} + +int pn_tt_residency_submit(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t timeout_ms) +{ + uint32_t repeat = 1u; + + if (residency && oncard_batch_wants(residency->name)) + repeat = oncard_batch(); + return pn_tt_residency_submit_repeat(residency, opcode, parameters, repeat, timeout_ms); +} + +void pn_tt_residency_join_group(pn_tt_residency *residency, uint16_t x, uint16_t y, + uint32_t address, uint32_t member) +{ + if (!residency) + return; + /* REFUSED AFTER START rather than silently ignored: the binding is already in the + * kernel's runtime args by then, so a late call would set a host field that describes a + * card that is not doing it — which is the exact shape of the stale-runtime-arg defect + * the cooperative role left behind. */ + if (residency->running) { + fprintf(stderr, "pn_tt: %s is already running — a group binding set now would not " + "reach the kernel. IGNORED.\n", residency->name); + return; + } + residency->group_x = x; + residency->group_y = y; + residency->group_address = address; + residency->group_member = member; +} + +/** + * THE IDLE RETIRE: never send a descriptor into a triad that may be half gone. + * + * Every processor of a resident triad counts its OWN idle polls to `idle_spin_limit` and exits + * alone, so a family that sits idle long enough — `gdn` and `topk` through a whole batched + * prefill, every family in a `serve` between requests — passes through a window in which some + * of its processors have exited and the rest are still polling. A descriptor that lands in that + * window is fetched by the survivors, which block on circular buffers whose producer is gone; + * the stop that follows finds a compute thread mid-descriptor, `did not retire`, and the core is + * wedged (`instrn_buf_status = 0x66`, `recover --rung 3`). MEASURED: the first descriptor of + * `gdn`, its members and `topk` after EVERY batched prefill was refused, and 2 of 3 runs at 16 + * matmul members wedged core 90 this way. + * + * So past `PN_TT_RESIDENCY_IDLE_RETIRE_MS` of silence the residency is ASKED to stop BEFORE + * anything is written — every processor still alive is in its idle poll, which reads the ask, + * so the triad exits whole — and the enqueue refuses. The family's own refusal path (await + * fails on a stopped residency, `retired` reads true, re-arm, retry) does the rest, which is + * the path every one of these families already runs on a refusal. + * + * The default must sit well below the FASTEST processor's idle exit; see the spec for the + * measurement it is set from. `0` turns the retire off (the old behaviour, kept so a ladder's + * control is the same binary). + * + * @return true when the residency was retired here and the enqueue must refuse + */ +long pn_tt_residency_idle_retire_limit_ms(void) +{ + static long limit_ms = -1; + + if (limit_ms < 0) { + const char *value = getenv(PN_TT_RESIDENCY_IDLE_RETIRE_ENV); + char *end = NULL; + + limit_ms = PN_TT_RESIDENCY_IDLE_RETIRE_DEFAULT_MS; + if (value && *value) { + const long parsed = strtol(value, &end, 10); + + if (end && *end == '\0' && parsed >= 0) + limit_ms = parsed; + else + fprintf(stderr, "pn_tt: %s=%s is not a millisecond count — using %ld\n", + PN_TT_RESIDENCY_IDLE_RETIRE_ENV, value, limit_ms); + } + } + return limit_ms; +} + +/** DIAGNOSIS (`PN_TT_IDLE_RETIRE_ONLY=`): only the residency of that name (or "matmul" + * for the decode group, "none" for nobody) is retired for idleness. Unsafe past the card's own + * idle exit (~8 s) — for short-gap bisects only. */ +bool pn_tt_idle_retire_allowed(const char *name) +{ + const char *only = getenv("PN_TT_IDLE_RETIRE_ONLY"); + + return !only || !*only || (name && strcmp(only, name) == 0); +} + +static bool residency_idle_retire(pn_tt_residency *residency) +{ + const long limit_ms = pn_tt_residency_idle_retire_limit_ms(); + double idle; + + if (limit_ms == 0 || !residency || !residency->running || residency->last_alive_seconds <= 0.0 || + !pn_tt_idle_retire_allowed(residency->name)) + return false; + idle = pn_tt_residency_now() - residency->last_alive_seconds; + if (idle * 1000.0 < (double)limit_ms) + return false; + fprintf(stderr, "pn_tt: %s idle %.1f s (limit %ld ms) — retiring it by ASKING before a " + "descriptor can reach a half-exited triad\n", + residency->name, idle, limit_ms); + pn_tt_residency_stop(residency); + residency->idle_retires++; + return true; +} + +int pn_tt_residency_write_param_extension(pn_tt_residency *residency, uint32_t offset, + const uint32_t *words) +{ + const ft_tt_noc_access *payload; + int rc; + + if (!residency || !residency->running || !words || offset == 0u) + return -1; + payload = pn_tt_issue_access(residency->runtime, residency->issue_shape); + rc = ft_tt_queue_write_op_parameters(payload, &residency->queue, + PN_TT_RESIDENCY_PARAM_SLOT + residency->param_slot + + offset, + words, NULL); + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s parameter extension refused: %s\n", residency->name, + ft_tt_error_string(rc)); + return -1; + } + return 0; +} + +int pn_tt_residency_enqueue(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t repeat) +{ + return pn_tt_residency_enqueue_group(residency, opcode, parameters, repeat, 0u, NULL); +} + +int pn_tt_residency_enqueue_group(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t repeat, + uint32_t sequence, uint32_t *out_token) +{ + ft_tt_queue_descriptor descriptor[PN_TT_RESIDENCY_QUEUE_CAPACITY]; + const ft_tt_noc_access *payload; + uint32_t address = 0u; + uint32_t slot; + uint32_t index; + double at; + int rc; + + if (!residency || !residency->running || !parameters || repeat == 0u) + return -1; + if (repeat > residency->queue.capacity) + repeat = residency->queue.capacity; + /* PM 8: A DEFERRED RESIDENCY QUEUES SEVERAL DESCRIPTORS BEFORE ONE AWAIT. A full ring + * drains first — ft_tt_queue_stage() does not check occupancy — and every queued + * descriptor keeps its own parameter block until it runs (slot 0 would be overwritten by + * the next enqueue's). A caller that picked a slot itself (`param_slot`) keeps it. */ + slot = residency->param_slot; + if (residency->defer_await) { + const uint32_t depth = residency->queue.capacity < PN_TT_RESIDENCY_DEFER_SLOTS + ? residency->queue.capacity + : PN_TT_RESIDENCY_DEFER_SLOTS; + + if (residency->unawaited + repeat > depth && + pn_tt_residency_settle(residency, PN_TT_RESIDENCY_DEFER_TIMEOUT_MS) != 0) + return -1; + if (slot == 0u) + slot = 1u + (residency->queue.issued % PN_TT_RESIDENCY_DEFER_SLOTS); + } + + /* THE PAYLOAD AND THE HEAD BUMP GO THROUGH DIFFERENT ACCESSORS ON THE `wide` SHAPE, and + * the head bump is on `noc` on BOTH — a four-byte write at a word address cannot come + * through the 16-byte L1 accessor at all (pn_tt_issue.h). */ + payload = pn_tt_issue_access(residency->runtime, residency->issue_shape); + + at = pn_tt_residency_now(); + rc = ft_tt_queue_write_op_parameters(payload, &residency->queue, + PN_TT_RESIDENCY_PARAM_SLOT + slot, parameters, + &address); + residency->issue_param_seconds += pn_tt_residency_now() - at; + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s parameter block refused: %s\n", residency->name, + ft_tt_error_string(rc)); + return -1; + } + + if (residency_idle_retire(residency)) + return -1; + memset(descriptor, 0, sizeof(descriptor)); + for (index = 0; index < repeat; index++) { + descriptor[index].opcode = opcode; + descriptor[index].a0 = address; + /* `a5` = 0 is "post nothing", and `ftq_group_post()` returns on it before touching + * the NoC. So the descriptor a caller that never asked for a group sends is byte for + * byte the one this function has always sent. */ + descriptor[index].a5 = sequence; + } + /* READ BEFORE `ft_tt_queue_stage()` MOVES `issued`. The token the group check must match + * is the one stamped on the LAST descriptor of this enqueue, which is + * `token_at(issued + repeat - 1)` taken from the counter as it stands now. */ + if (out_token) + *out_token = ft_tt_queue_token_at(&residency->queue, + residency->queue.issued + repeat - 1u); + + if (residency_red_head_first_fires(residency)) { + fprintf(stderr, "pn_tt: RED PROOF issue_head_first — %s publishes head %u BEFORE the " + "descriptor, after exactly one lap of the ring. The card will run slot " + "%u's PREVIOUS occupant: a legal descriptor, a matching count and no " + "kernel error. Only the TOKEN can catch it.\n", + residency->name, residency->queue.head + repeat, + residency->queue.head & (residency->queue.capacity - 1u)); + (void)ft_tt_queue_write_head(&residency->runtime->noc, &residency->queue, + residency->queue.head + repeat); + /* AND THEN WAIT, WHICH IS WHAT MAKES THE ARM DETERMINISTIC RATHER THAN A COIN FLIP. + * + * MEASURED: without this, the head-first arm was caught on the `wide` shape and NOT on + * `word` — the ring write follows the head bump by ~130 ns through an already-aimed + * window, and the kernel's poll simply did not land in that gap. A red proof whose + * firing depends on a 130 ns race proves nothing on the run where it misses. Half a + * millisecond is longer than any poll period on the card and shorter than any timeout + * on the host, so the card is GUARANTEED to consume the stale slot. */ + { + struct timespec settle = { 0, 500000L }; + + (void)nanosleep(&settle, NULL); + } + } + + at = pn_tt_residency_now(); + rc = ft_tt_queue_stage(payload, &residency->queue, descriptor, repeat); + residency->issue_ring_seconds += pn_tt_residency_now() - at; + if (rc == FT_TT_OK) { + at = pn_tt_residency_now(); + rc = ft_tt_queue_publish(&residency->runtime->noc, &residency->queue); + residency->issue_head_seconds += pn_tt_residency_now() - at; + } + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s enqueue failed: %s\n", residency->name, + ft_tt_error_string(rc)); + return -1; + } + residency->issues++; + residency->unawaited += repeat; + residency->issue_accesses += pn_tt_issue_accesses( + residency->issue_shape, + FT_TT_QUEUE_PARAM_WORDS * 4u + repeat * FT_TT_QUEUE_DESCRIPTOR_BYTES); + residency->ops += repeat; + return 0; +} + +int pn_tt_residency_await(pn_tt_residency *residency, uint32_t timeout_ms) +{ + uint32_t fields[FT_TT_QUEUE_FIELD_COUNT]; + const char *reason = NULL; + uint32_t expected, checked; + int rc; + + if (!residency || !residency->running || timeout_ms == 0u) + return -1; + if (residency->defer_await) { + residency->await_pending = true; + return 0; + } + residency->await_pending = false; + + expected = residency->queue.issued; + /* The POLL waits for what was issued; the CHECK is what the red arm moves. */ + checked = residency_red_await_early() ? expected + 1u : expected; + residency->awaits++; + memset(fields, 0, sizeof(fields)); + rc = pn_tt_residency_wait(residency, expected, timeout_ms, fields); + /* THE COMPLETION WINDOW, NOT THE CONTROL BLOCK. Words 5..9 carry all four of rule 1's + * agreements — COUNT 5, TOKEN 6, STATE 7, ERROR 9 — so this is the same verdict on the + * same accessor for FIVE word reads instead of sixteen. `line` already has its answer out + * of the poll and reads nothing more; `control` is the shipped shape, kept so a ladder's + * control can be re-taken on this binary. */ + if (rc == FT_TT_OK && residency->await_shape == PN_TT_AWAIT_WINDOW) { + rc = ft_tt_queue_read_completion(&residency->runtime->noc, &residency->queue, fields); + residency->await_settle_transactions += FT_TT_QUEUE_COMPLETION_FIELD_COUNT; + } else if (rc == FT_TT_OK && residency->await_shape == PN_TT_AWAIT_CONTROL) { + rc = ft_tt_queue_read_control(&residency->runtime->noc, &residency->queue, fields); + residency->await_settle_transactions += FT_TT_QUEUE_FIELD_COUNT; + } + if (rc != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s descriptor %u did not complete: %s\n", residency->name, + expected, ft_tt_error_string(rc)); + return -1; + } + /* Completion is four independent agreements, not a count: the state is not the host's + * poison, the count is exact, the TOKEN is this run's nonce for this descriptor, and the + * kernel reports no error. Each of them fails a different lie about work that did not + * happen (ft_tt_queue.h rule 1). The fast path holds THREE of them and asks the check + * that does not look at the fourth — a separate function, so a line buffer whose `error` + * word is a memset zero cannot pass the four-agreement check as KERR_NONE. */ + if ((residency->await_shape == PN_TT_AWAIT_LINE + ? ft_tt_queue_check_completion_line(&residency->queue, fields, checked, &reason) + : ft_tt_queue_check_completion(&residency->queue, fields, checked, &reason)) != + FT_TT_OK) { + /* The FULL window on the way to a failure, and it is the only place the dropped + * assertion has to be recovered: a caller reporting a refusal must be able to NAME + * `error`, and five more reads cost nothing on a path that is already returning -1. + * The verdict is NOT re-taken from it — `reason` stands. */ + if (residency->await_shape == PN_TT_AWAIT_LINE) + (void)ft_tt_queue_read_completion(&residency->runtime->noc, &residency->queue, + fields); + fprintf(stderr, "pn_tt: %s completion refused: %s (state %s, error %s)\n", + residency->name, reason ? reason : "?", + ft_tt_queue_state_string(fields[FT_TT_QUEUE_FIELD_STATE]), + ft_tt_queue_kernel_error_string(fields[FT_TT_QUEUE_FIELD_ERROR])); + return -1; + } + residency->last_alive_seconds = pn_tt_residency_now(); + residency->unawaited = 0u; + return 0; +} + +/** + * RED PROOF ONLY. `PN_TT_RED=residency_group_early` asks the GROUP check about one sequence + * MORE than was issued, leaving the poll alone — the group-block twin of + * `residency_await_early`, and there for the same reason. A ladder that swaps N per-member + * awaits for one group read is only evidence if the group read's verdict is live; a group + * await that returned OK on any slot array would measure the card's timing and nothing else. + * Every member must be refused, naming the slot that did not reach the sequence. + */ +static bool residency_red_group_early(void) +{ + static int cached = -1; + + if (cached < 0) + cached = pn_tt_name_list_selects(getenv("PN_TT_RED"), "residency_group_early") ? 1 : 0; + return cached != 0; +} + +int pn_tt_residency_settle(pn_tt_residency *residency, uint32_t timeout_ms) +{ + bool deferring; + int rc; + + if (!residency || !residency->await_pending) + return 0; + deferring = residency->defer_await; + residency->defer_await = false; + rc = pn_tt_residency_await(residency, timeout_ms); + residency->defer_await = deferring; + return rc; +} + +int pn_tt_residency_await_group(pn_tt_residency *const *set, const uint32_t *token, + uint32_t members, uint32_t sequence, uint32_t timeout_ms) +{ + const double deadline = pn_tt_residency_now() + (double)timeout_ms / 1000.0; + ft_tt_queue_group_slot slots[FT_TT_QUEUE_GROUP_MAX_MEMBERS]; + const uint32_t want = sequence + (residency_red_group_early() ? 1u : 0u); + uint32_t expected; + uint32_t index; + + if (!set || !token || members == 0u || members > FT_TT_QUEUE_GROUP_MAX_MEMBERS || + sequence == 0u || timeout_ms == 0u) + return -1; + for (index = 0; index < members; index++) + if (!set[index] || !set[index]->running) + return -1; + if (set[0]->group_address == 0u) { + fprintf(stderr, "pn_tt: %s owns no group block — a group await here would poll an " + "address nothing posts to\n", set[0]->name); + return -1; + } + expected = set[0]->queue.issued; + + /* STAGE 1 — MEMBER 0's OWN COUNT, one four-byte read, exactly the single-core poll. It is + * the cheapest thing that can say "the set is probably done": member 0 is the slice this + * family gives the largest share of the work to, and the record has it at 113.1 us a + * descriptor against 3.3-10.6 for members 1..7. */ + for (;;) { + uint32_t value = 0; + + set[0]->awaits++; + set[0]->await_poll_transactions += 1u; + if (ft_tt_queue_read_field(&set[0]->runtime->noc, &set[0]->queue, + FT_TT_QUEUE_FIELD_COMPLETED_COUNT, &value) != FT_TT_OK) { + fprintf(stderr, "pn_tt: %s completion count is unreadable\n", set[0]->name); + return -1; + } + if (value >= expected) + break; + if (pn_tt_residency_now() > deadline) { + fprintf(stderr, "pn_tt: %s did not reach %u descriptors in %u ms\n", set[0]->name, + expected, timeout_ms); + return -1; + } + } + + /* STAGE 2 — THE WHOLE SLOT ARRAY IN ONE BLOCK16 TRANSACTION, which is the entire point: + * `ceil(8N/16)` 16-byte units for N members, against N independent poll loops. Two + * assertions per member, each failing a different lie — the SEQUENCE says how far that + * member got, the TOKEN says it ran THIS descriptor and not a previous occupant of the + * slot. */ + for (;;) { + uint32_t bad = members; + + if (ft_tt_queue_group_read_slots(&set[0]->runtime->bulk_l1, set[0]->queue.x, + set[0]->queue.y, set[0]->group_address, members, + slots) != FT_TT_OK) { + fprintf(stderr, "pn_tt: the %s group completion block did not read back\n", + set[0]->name); + return -1; + } + set[0]->await_settle_transactions += ((uint64_t)members * 8u + 15u) / 16u; + for (index = 0; index < members; index++) + if (slots[index].sequence < want || slots[index].token != token[index]) + break; + if (index == members) { + const double now = pn_tt_residency_now(); + + for (index = 0; index < members; index++) + set[index]->last_alive_seconds = now; + return 0; + } + bad = index; + if (pn_tt_residency_now() > deadline) { + fprintf(stderr, "pn_tt: the %s group did not reach sequence %u in %u ms — member " + "%u posted sequence %u token 0x%08x, wanted %u / 0x%08x\n", + set[0]->name, want, timeout_ms, bad, slots[bad].sequence, slots[bad].token, + want, token[bad]); + return -1; + } + } +} + +int pn_tt_residency_submit_repeat(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t repeat, + uint32_t timeout_ms) +{ + double started; + int rc; + + if (!residency) + return -1; + started = pn_tt_residency_now(); + rc = pn_tt_residency_enqueue(residency, opcode, parameters, repeat); + if (rc != 0) + return -1; + rc = pn_tt_residency_await(residency, timeout_ms); + if (rc != 0) + return -1; + residency->seconds += pn_tt_residency_now() - started; + return 0; +} + +/* ================================================================ + * LIFECYCLE CLOCK (PM 5): wall seconds spent booting, starting and stopping residencies — + * the idle retire + re-arm cost a request pays, which no op counter sees. + * ================================================================ */ + +static double g_lifecycle_seconds; + +int pn_tt_residency_bind_core(pn_tt_residency *residency, uint32_t core_index) +{ + const double started = pn_tt_residency_now(); + const int rc = residency_bind_core(residency, core_index); + + g_lifecycle_seconds += pn_tt_residency_now() - started; + return rc; +} + +static void residency_register(pn_tt_residency *residency); +static void residency_unregister(const pn_tt_residency *residency); + +int pn_tt_residency_start(pn_tt_residency *residency) +{ + const double started = pn_tt_residency_now(); + const int rc = residency_start(residency); + + g_lifecycle_seconds += pn_tt_residency_now() - started; + if (rc == 0) { + residency->unawaited = 0u; + residency_register(residency); + } + return rc; +} + +void pn_tt_residency_stop(pn_tt_residency *residency) +{ + const double started = pn_tt_residency_now(); + + residency_stop(residency); + g_lifecycle_seconds += pn_tt_residency_now() - started; + if (residency && !residency->running) + residency_unregister(residency); +} + +double pn_tt_residency_lifecycle_seconds(void) +{ + return g_lifecycle_seconds; +} + +void pn_tt_residency_lifecycle_add(double seconds) +{ + g_lifecycle_seconds += seconds; +} + +/* ================================================================ + * KEEPALIVE (PM 5) — the live-residency registry and the NOP sweep + * ================================================================ */ + +#define PN_TT_RESIDENCY_LIVE_MAX 256u +static pn_tt_residency *g_live[PN_TT_RESIDENCY_LIVE_MAX]; + +/** Remember a residency that just started (idempotent). Full registry = not kept alive. */ +static void residency_register(pn_tt_residency *residency) +{ + uint32_t index, empty = PN_TT_RESIDENCY_LIVE_MAX; + + for (index = 0; index < PN_TT_RESIDENCY_LIVE_MAX; index++) { + if (g_live[index] == residency) + return; + if (!g_live[index] && empty == PN_TT_RESIDENCY_LIVE_MAX) + empty = index; + } + if (empty < PN_TT_RESIDENCY_LIVE_MAX) + g_live[empty] = residency; +} + +static void residency_unregister(const pn_tt_residency *residency) +{ + uint32_t index; + + for (index = 0; index < PN_TT_RESIDENCY_LIVE_MAX; index++) + if (g_live[index] == residency) + g_live[index] = NULL; +} + +static bool residency_wants_keepalive(const pn_tt_residency *residency, double now, + double min_idle_seconds) +{ + const double limit = (double)pn_tt_residency_idle_retire_limit_ms() / 1000.0; + double idle; + + if (!residency->running || residency->unawaited != 0u || residency->last_alive_seconds <= 0.0) + return false; + idle = now - residency->last_alive_seconds; + /* Past the limit the next enqueue retires it anyway: leave that to the family's own path. */ + return idle >= min_idle_seconds && (limit <= 0.0 || idle < limit - 0.5); +} + +int pn_tt_residency_keepalive_all(double min_idle_seconds) +{ + static const uint32_t zeros[FT_TT_QUEUE_PARAM_WORDS]; + bool sent[PN_TT_RESIDENCY_LIVE_MAX]; + const double now = pn_tt_residency_now(); + uint32_t index; + int count = 0, failed = 0; + + for (index = 0; index < PN_TT_RESIDENCY_LIVE_MAX; index++) { + pn_tt_residency *residency = g_live[index]; + + sent[index] = false; + if (!residency || !residency_wants_keepalive(residency, now, min_idle_seconds)) + continue; + residency->param_slot = 0u; + if (pn_tt_residency_enqueue(residency, FT_TT_QUEUE_OP_NOP, zeros, 1u) != 0) { + failed = 1; + continue; + } + sent[index] = true; + count++; + } + for (index = 0; index < PN_TT_RESIDENCY_LIVE_MAX; index++) + if (sent[index] && pn_tt_residency_await(g_live[index], PN_TT_RESIDENCY_KEEPALIVE_TIMEOUT_MS) != 0) + failed = 1; + return failed ? -1 : count; +} + +#endif /* USE_TT */ diff --git a/src/accel/tt/pn_tt_residency.h b/src/accel/tt/pn_tt_residency.h new file mode 100644 index 0000000..27ef081 --- /dev/null +++ b/src/accel/tt/pn_tt_residency.h @@ -0,0 +1,450 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_residency.h + * @brief ONE resident Tensix program on ONE core: read its images, boot that core with the + * firmware they were linked against, place its circular buffers, kick it, and from + * then on submit descriptors to it. + * + * This is the part every resident op family does identically. What differs per + * family — which circular buffers, how many pages, what the sixteen parameter words + * mean — stays with the family's own driver in foundry/tt, and this file never looks + * at it: the caller hands over an already-encoded CB descriptor array and an already + * encoded parameter block. + * + * --------------------------------------------------------------------------- + * WHY ONE CORE PER FAMILY, RATHER THAN ONE CORE DOING EVERYTHING + * --------------------------------------------------------------------------- + * A resident kernel's text is linked at a fixed per-processor slab, so ONE program can be + * resident on a core at a time (foundry/tt/ft_tt_op_vector.h). A transformer block needs a + * matmul, a reduce (the RMS norms) and an elementwise (the SwiGLU gate, the residual adds), + * and swapping programs between them would re-upload five kernel images per switch — the + * 36.2 µs/kB cost the resident queue exists to remove. So each family gets its own core and + * they hand data to each other through card DRAM, which the host never touches. + * + * That is the whole mechanism by which the readback goes away. It is not a faster transfer; + * it is a transfer that does not happen, because the activation never leaves the card. + * + * --------------------------------------------------------------------------- + * THREE HAZARDS, ALL PAID FOR IN CORES ALREADY + * --------------------------------------------------------------------------- + * 1. **A kernel and its firmware are ONE build output.** A kernel's text is linked at its + * own firmware's `__fw_export_text_end`; booting one directory's firmware and loading + * another's kernels is a wedged core and a card-level reset. So this file boots from the + * SAME directory it loads the program from, always, with no way to say otherwise. + * 2. **Never park a resident triad, and never reset one out of a blocked wait.** Shutdown + * is a written word and a poll. There is deliberately no reset escalation here. + * 3. **A refused descriptor STRANDS the triad** — the reader sets an error and leaves its + * loop, `tail` never advances and the other four wait forever on work that will not + * arrive. So the wait polls the ERROR word as well as the count, which turns a 60 s + * timeout into a ~1 ms return, and the caller is told to re-arm rather than to wait. + * + * @status Used by the reduce (RMS norm) and elementwise (SwiGLU, residual add) residencies + * of the on-card forward. + * @issues None + * @todo None + */ + +#ifndef PN_TT_RESIDENCY_H +#define PN_TT_RESIDENCY_H + +#ifdef USE_TT + +#include +#include +#include + +#include "pn_tt_issue.h" + +#include "tt/ft_tt_queue.h" +#include "tt/ft_tt_runtime.h" + +/** Ring slots, the MAXIMUM a residency may arm (`PN_TT_QUEUE_CAPACITY`, a power of two + * 2..64; default PN_TT_RESIDENCY_QUEUE_CAPACITY_DEFAULT). 64 x 32 B ends at 0x900, far below + * the parameter slots at 0x9000. The kernel indexes with the mask it is given, so the depth + * is a host decision. A caller that enqueues without awaiting must stay within + * `queue.capacity` itself — ft_tt_queue_stage() does not check occupancy (the S2c deferred + * GDN rows overran an 8-slot ring at 10 rows and DIFFERED silently). */ +#define PN_TT_RESIDENCY_QUEUE_CAPACITY 64u +#define PN_TT_RESIDENCY_QUEUE_CAPACITY_DEFAULT 8u + +/** The ring depth residencies arm with: `PN_TT_QUEUE_CAPACITY`, else the default. */ +uint32_t pn_tt_residency_queue_capacity(void); + +/** + * Milliseconds a stop will WAIT, one poll at a time, after its tight loop gives up. + * + * The thing being waited for is a compute thread spinning out its CB-wait bound + * (100,000,000 iterations) after a refused descriptor — that is the family's design, not a + * fault — and it takes seconds. The tight loop covers 60 ms. + * + * **THE BOOT GUARD DELIBERATELY DOES NOT SHARE THIS NUMBER**, and the reason is measured: + * a stop is issued straight after a refusal, so the CB-wait bound is what it waits out, + * while `pn_tt_core_boot_guard.c` waits out whatever an EARLIER PROCESS left — which + * includes the 400,000,000-iteration IDLE bound, recorded at over 40 s. One number for both + * would be right for one of them. See PN_TT_BOOT_GUARD_WAIT_MS. + */ +#define PN_TT_RESIDENCY_STOP_GRACE_MS 20000u + +/** Which parameter-block slot every descriptor points at. One is enough while the host + * waits for each descriptor before issuing the next. */ +#define PN_TT_RESIDENCY_PARAM_SLOT 0u +/** PM 8: a deferred residency's queued descriptors rotate over parameter slots 1..this (more + * than any ring depth in use), and its flow-control settle waits this long. */ +#define PN_TT_RESIDENCY_DEFER_SLOTS 16u +#define PN_TT_RESIDENCY_DEFER_TIMEOUT_MS 60000u + +/** Longest program name held; for logs only. */ +#define PN_TT_RESIDENCY_NAME_BYTES 32u + +/** One resident program, its core, and its queue. Caller-owned storage. */ +/** + * How an await reads completion — the three shapes of `PN_TT_RESIDENCY_AWAIT`, and the whole + * per-descriptor host cost of every family except the matmul. See `pn_tt_residency.c` for the + * transaction arithmetic and why the matmul's recorded 6.9 µs is a different number. + */ +typedef enum pn_tt_await_shape { + PN_TT_AWAIT_CONTROL = 0, /**< poll + the whole 64-byte control block — 17 PCIe reads */ + PN_TT_AWAIT_WINDOW, /**< poll + words 5..9 — 6 reads, the same four agreements */ + PN_TT_AWAIT_LINE /**< ONE BLOCK16 read of words 4..7; the poll IS the check */ +} pn_tt_await_shape; + +typedef struct pn_tt_residency { + ft_tt_runtime *runtime; /**< NOT owned; the device's */ + ft_tt_runtime_images firmware; + ft_tt_runtime_images program; + ft_tt_launch_config config; + ft_tt_queue queue; + /** S2c: the parameter slot the next enqueue writes (0 = the shipped single slot). A + * caller queueing several descriptors before one await gives each its own slot. */ + uint32_t param_slot; + unsigned char cb_buffer[FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4]; + uint64_t cb_mask; + + char name[PN_TT_RESIDENCY_NAME_BYTES]; + /** The processors this program ENABLES, as passed to pn_tt_residency_load(). The + * retirement wait polls exactly these: a family with no compute thread (the memory + * family is DM0|DM1) leaves its TRISCs in the firmware GO wait, and they never write + * EXIT because they never ran this program. */ + uint32_t processor_mask; + uint32_t core_index; + uint16_t core_x, core_y; + + bool images_loaded; /**< the five firmware and five kernel ELFs are in memory */ + bool core_booted; /**< the core is running this program's firmware */ + bool running; /**< a residency is live on the core RIGHT NOW */ + /** Did the LAST `pn_tt_residency_stop()` see every enabled processor say EXIT? + * + * IT IS THE ONE THING THAT SEPARATES A RE-ARM FROM A WEDGE. Booting firmware onto a core + * whose compute thread is still spinning out its 400,000,000-iteration bound leaves + * `instrn_buf_status = 0x66` and needs `ft_tt_core recover --rung 3` and sudo — measured + * on core 90, silicon 2026-08-21, on exactly that sequence. True until a stop says + * otherwise. */ + bool retired; + + /** When this residency last proved itself alive: armed, or a descriptor completed. + * Read by `pn_tt_residency_enqueue_group()` to refuse a descriptor to a residency that + * may already be idling out — see `PN_TT_RESIDENCY_IDLE_RETIRE_MS`. */ + double last_alive_seconds; + uint64_t idle_retires; /**< times the idle retire fired on this handle */ + /** Descriptors enqueued since the last successful await (PM 5 keepalive: a residency + * with work outstanding is never sent a NOP — its parameter slot may be unread). */ + uint32_t unawaited; + /** PM 7 ITEM 7: while set, pn_tt_residency_await() records and returns 0; the caller owes + * a pn_tt_residency_settle() before anything consumes the output (streamed, that is where + * the puller's AWAIT lands). A refusal surfaces at the settle, without the family's + * replay. */ + bool defer_await; + bool await_pending; + + /** How this residency's awaits read completion. Decided ONCE, at arm time, from the env + * knob AND `ft_tt_queue_completion_line_is_addressable()` — not per descriptor, because + * a per-descriptor decision is a per-descriptor `getenv` on the hot path, and not from + * the knob alone, because a queue the BLOCK16 accessor cannot reach would turn every + * await into a completion that never completes. */ + pn_tt_await_shape await_shape; + + /** Did the kernel binding this residency was launched with arm the GROUP POST? + * + * It is a READING off the binding that was actually built, not a constant, and it is + * the single fact the `line` completion shape rests on. `ftq_group_post()`'s refused + * inline write is the ONLY fault `resident_queue_protocol.h` raises after + * `completed_count` has been published — every other error path abandons the descriptor + * and leaves the count short (rule 5), where the poll and the 128th-poll error read + * catch it. So a residency that never arms the post gives up nothing on the line, and + * one that does must keep the window. `pn_tt_matmul_group.c` makes the identical + * exclusion for `PN_TT_FAST_COMPLETION`; this is that argument on this path, as code. */ + bool posts_group; + + /** WHERE THIS RESIDENCY POSTS ITS GROUP COMPLETION, set by the caller BEFORE + * `pn_tt_residency_start()` and copied straight into the kernel binding there. + * + * Zero — the value one `memset` gives every residency — is "post nothing", which is + * what every family on this path did until the chunkwise GDN split needed the other + * shape. THE POINT IS THE AWAIT, NOT THE POST: a split of N members costs N independent + * blocking poll loops IN SERIES, and a group block collapses them into member 0's count + * plus ONE BLOCK16 read of the whole slot array. That is the shape + * `pn_tt_matmul_group.c` has run as its default since 2026-08-20. + * + * Arming it is not free on the completion side: `ftq_group_post()`'s refused inline + * write is the one fault raised AFTER `completed_count` is published, so + * `pn_tt_residency_start()` demotes the `line` shape to `window` for exactly these + * residencies. That demotion is why a group await has to be MEASURED against the + * per-member one rather than assumed to win. */ + uint16_t group_x, group_y; + uint32_t group_address; /**< 0 = post nothing */ + uint32_t group_member; /**< this residency's slot in the block */ + + uint64_t ops; /**< descriptors this residency has executed */ + double seconds; /**< wall time inside pn_tt_residency_submit() */ + + /** PCIe LOAD UNITS this residency's awaits have issued, SPLIT BY WHAT THEY ARE. + * + * **POOLING THESE TWO MAKES THE SHAPE INVISIBLE, AND THAT WAS MEASURED RATHER THAN + * REASONED.** One pooled counter read **49.0 and 54.5 reads an await** on two builds + * that were BOTH on the six-read `window` — because a residency waiting on real card + * work SPINS, and at ~44 spin polls an await the completion read is 5 of 49. A ladder + * whose acceptance criterion is "17.0 / 6.0 / 1.0" would have been unreadable against + * that, or read as a failed arm. So they are counted apart: + * + * `poll` every read inside the wait loop — the count poll and the 128th-poll error + * word. CARD-BOUND: it is how the host waits, and no shape removes it. + * `settle` the block read issued AFTER the wait returned — 16 on `control`, 5 on + * `window`, **0 on `line`, where the last poll IS the check**. This is the + * term the shapes move and the only one a ladder may be scored on. */ + uint64_t await_poll_transactions; + uint64_t await_settle_transactions; + uint64_t awaits; + + /** How this residency's enqueues write the parameter block and the ring slot. Decided + * ONCE at arm time from `PN_TT_ISSUE` AND the queue's placement — see pn_tt_issue.h. */ + pn_tt_issue_shape issue_shape; + + /** THE ISSUE HALF, in the same currency the await is counted in — PCIe ACCESSES. + * + * A byte counter could not price this link (64 bytes is sixteen accesses at WORD32 and + * four at BLOCK16) and a call counter could not either (three calls on both shapes). So + * the accesses are counted at the unit of the accessor that was ACTUALLY used, and the + * three calls are timed apart — which is what solves the per-call fixed cost against the + * per-access one instead of asserting a split between them. */ + uint64_t issues; /**< enqueues, i.e. descriptors ISSUED (not repeats) */ + uint64_t issue_accesses; /**< PCIe accesses those enqueues cost */ + double issue_param_seconds; /**< inside ft_tt_queue_write_op_parameters() */ + double issue_ring_seconds; /**< inside ft_tt_queue_stage() */ + double issue_head_seconds; /**< inside ft_tt_queue_publish() */ +} pn_tt_residency; + +/** + * Resolve `program_name`'s firmware and kernel ELFs. No device access. + * + * BY NAME, out of the binary's own program blob, because a program root is a property of one + * machine and a program name is a property of the model. `override_root` is the deliberate + * escape and is never a fallback: NULL means embedded, non-NULL means + * `/` off disk or a refusal (`ft_tt_program_resolve.h`). + * + * Failing here is not fatal to a serve: a card whose program is missing runs the host-side + * version of that op, which is what the caller does with the `false` return. + * + * @param runtime the open card; borrowed, must outlive the residency + * @param program_name a build_tensix_program.sh output directory's basename, e.g. "reduce" + * @param override_root NULL for the embedded blob; a directory to read instead + * @param name short label for logs + * @param mask processors the program enables, e.g. all five of a triad + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_tt_residency_load(pn_tt_residency *residency, ft_tt_runtime *runtime, + const char *program_name, const char *override_root, const char *name, + uint32_t mask); + +/** + * Resolve `core_index` to a TRANSLATED coordinate and boot THIS program's firmware on it. + * + * The coordinate comes from ft_tt_coord, never from a guess — a wrong NoC endpoint hangs + * the chip and costs a walk to the machine. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_residency_bind_core(pn_tt_residency *residency, uint32_t core_index); + +/** + * Load the kernel images, initialise the queue, plant the runtime args, write the caller's + * circular-buffer descriptor array and GO. + * + * The caller must have filled `residency->cb_buffer` and `residency->cb_mask` with its + * family's placement (ft_tt_op_*_place_cbs) before calling. That split is deliberate: CB + * geometry is the op family's business and the core is this file's. + * + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_residency_start(pn_tt_residency *residency); + +/** + * Retire the residency by ASKING, and wait for all five processors to say EXIT. + * + * Never a reset and never a park. The control block's own `state` cannot answer this — it + * has one writer — so every processor's own status slot is polled instead. Safe to call on + * a residency that is not running. + */ +void pn_tt_residency_stop(pn_tt_residency *residency); + +/** Did the last stop RETIRE it — every enabled processor saying EXIT? A caller about to boot + * firmware onto that core must ask: a `false` here is the wedge signature's precondition. */ +bool pn_tt_residency_retired(const pn_tt_residency *residency); + +/** Free the ELF images. Stops the residency first if it is still running. */ +void pn_tt_residency_release(pn_tt_residency *residency); + +/** + * Write sixteen parameter words, enqueue one descriptor, and wait for the card to report it + * EXECUTED — with the four independent agreements, not a count. + * + * The wait polls `completed_count` every iteration and the ERROR word every 128th, because + * a descriptor the kernel REFUSES never advances the count and would otherwise cost the + * whole timeout for something the card knew in microseconds. + * + * @param opcode an ft_tt_queue_opcode this program implements + * @param parameters FT_TT_QUEUE_PARAM_WORDS words, already encoded by the family's driver + * @param timeout_ms never 0 — there is no untimed path to the card + * `PN_TT_ONCARD_BATCH=K` turns this into the queue-depth instrument below, and + * `PN_TT_ONCARD_BATCH_FAMILY=a,b` restricts it to the named residencies. The filter is what + * makes the arm answer-preserving: `eltwise`, `render-widen` and `render-narrow` compute a + * pure function of operands they do not touch, so K copies leave what one leaves, while the + * GDN recurrence and the IN-PLACE per-head L2 norm do not. Unset means every family, which + * is the shape every recorded batch arm was taken in. + * + * @return 0, or -1 with the reason on stderr. A -1 leaves the residency + * STRANDED, and the caller must stop and re-arm it before reusing it + */ +int pn_tt_residency_submit(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t timeout_ms); + +/** + * `pn_tt_residency_submit()` with `repeat` COPIES of the same descriptor enqueued before the + * host waits once — the queue-depth instrument, and the only thing in this stack that can + * price a host round trip apart from the card's own work. + * + * The shipped path submits ONE descriptor and waits for it, so every descriptor pays a + * parameter write, a ring write, a head bump and the first poll's PCIe latency. Those are + * FIXED per host wait, not per descriptor, so `repeat` of them cost what one costs: + * + * seconds(repeat) = host_fixed + repeat * on_card_serial + * + * Two repeat counts therefore solve for both terms, which is exactly the question "does + * taking the host out of the dispatch loop help THIS op" — measured rather than argued. + * `repeat` is CLAMPED to the ring capacity; the ring is empty on entry because the previous + * call waited for it to drain. + * + * **THIS IS AN INSTRUMENT, NOT A FAST PATH.** Every copy runs the SAME parameter block, so + * for a stateful family — the GDN recurrence reads and writes its carried state — the second + * and later copies compute a value the model does not want. The timing is valid; the answer + * is not. Nothing calls this unless a measurement knob asks for it. + * + * @param repeat copies to enqueue, 1..PN_TT_RESIDENCY_QUEUE_CAPACITY; 0 is refused + * @return 0, or -1 with the reason on stderr, same stranding contract as submit() + */ +int pn_tt_residency_submit_repeat(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t repeat, + uint32_t timeout_ms); + +/** + * The two halves of `pn_tt_residency_submit_repeat()` apart, so N residencies can be started + * before ANY of them is waited on. + * + * That is the only way to ask whether an op scales across cores. Submitting to each core in + * turn and waiting each time measures N serial descriptors however many cores are involved, + * which is the same number the one-core arm prints and says nothing. + * + * `enqueue` counts the descriptors into `ops` and does NOT touch `seconds` — the caller owns + * the clock when it owns the overlap. A caller that enqueues must await before enqueuing + * again on the same residency: the ring holds one batch. + */ +int pn_tt_residency_enqueue(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t repeat); +int pn_tt_residency_await(pn_tt_residency *residency, uint32_t timeout_ms); + +/** The await a `defer_await` window skipped, now (0 when none is pending). */ +int pn_tt_residency_settle(pn_tt_residency *residency, uint32_t timeout_ms); + +/** + * Write sixteen words into the parameter slot `offset` slots AFTER this residency's current + * one — an EXTENSION block a family's descriptor reads beside its own (the gdn multi-row + * DELTA's per-row state addresses). Call it before the enqueue that uses it. Goes through the + * same accessor as the descriptor's own block, so the streamed puller captures it in order. + */ +int pn_tt_residency_write_param_extension(pn_tt_residency *residency, uint32_t offset, + const uint32_t *words); + +/** Wall seconds this process has spent in pn_tt_residency_bind_core / _start / _stop — the + * boots and retires behind every idle re-arm (PM 5 instrument). */ +double pn_tt_residency_lifecycle_seconds(void); +/** Add a re-arm that does not go through the three calls above (the decode matmul group). */ +void pn_tt_residency_lifecycle_add(double seconds); + +/** + * KEEPALIVE (PM 5): send one NOP descriptor to every LIVE residency that has been idle at + * least `min_idle_seconds` but not yet past the idle-retire limit, has nothing outstanding, + * and is not in `skip` (NULL-terminated, may be NULL); then await them all. The card's idle + * exit and the host's retire both restart, so a family a long prefill or a pause leaves idle + * is not retired and re-armed on its next use. The bounded-residency rule is unchanged: with + * no host, the card still exits on its own bound. + * @return NOPs sent, or -1 if one failed (that residency then retires on its next use) + */ +int pn_tt_residency_keepalive_all(double min_idle_seconds); + +/** + * The same enqueue, with a GROUP SEQUENCE stamped into every descriptor's `a5`. + * + * A non-zero sequence makes the kernel post `{sequence, token}` into this core's slot of the + * group block after it publishes its own completion — which is what lets ONE reader learn + * that N members finished, instead of N poll loops learning it one at a time. Zero is "post + * nothing" and is exactly `pn_tt_residency_enqueue()`, which is now this function. + * + * The sequence is the CALLER's counter, not the queue's: it is the group's notion of "item + * N", and every member of one issue must carry the same value or the slot array can never be + * satisfied at once. + * + * `out_token` receives the token this enqueue stamped on the LAST descriptor, because the + * group check needs it per member and `ft_tt_queue_token_at()` needs `issued` from BEFORE the + * next enqueue moves it. + */ +int pn_tt_residency_enqueue_group(pn_tt_residency *residency, uint32_t opcode, + const uint32_t *parameters, uint32_t repeat, + uint32_t sequence, uint32_t *out_token); + +/** + * Bind this residency to a group completion block. Call BEFORE `pn_tt_residency_start()`; + * after it, the binding is already in the kernel's runtime args and this does nothing. + * + * @param address the block's L1 address on core (x, y); 0 clears the binding + */ +void pn_tt_residency_join_group(pn_tt_residency *residency, uint16_t x, uint16_t y, + uint32_t address, uint32_t member); + +/** + * WAIT FOR EVERY MEMBER OF ONE GROUP ISSUE, in one place, the way the matmul group does. + * + * Stage 1 polls MEMBER 0's `completed_count` — one four-byte read, exactly the single-core + * poll, and the member that finishes last is the one that decides when this returns. Stage 2 + * reads the WHOLE slot array in one BLOCK16 transaction and requires, per member, that the + * posted sequence has reached `sequence` AND that the posted token is the one this run + * stamped on that member's own descriptor. Two assertions per member, each failing a + * different lie: the sequence says how far it got, the token says it ran THIS descriptor. + * + * @param set members[0] owns the block and is the one polled in stage 1 + * @param token per member, from `pn_tt_residency_enqueue_group()` + * @return 0, or -1 with the reason on stderr + */ +int pn_tt_residency_await_group(pn_tt_residency *const *set, const uint32_t *token, + uint32_t members, uint32_t sequence, uint32_t timeout_ms); + +/** + * The idle limit in milliseconds past which a residency (or the matmul group) is retired by + * ASKING before it is sent a descriptor — `PN_TT_RESIDENCY_IDLE_RETIRE_MS`, 0 = off. Shared so + * every family with its own queue applies the same bound. See residency_idle_retire(). + */ +long pn_tt_residency_idle_retire_limit_ms(void); + +#endif /* USE_TT */ +#endif /* PN_TT_RESIDENCY_H */ diff --git a/src/accel/tt/pn_tt_stage_pipeline.c b/src/accel/tt/pn_tt_stage_pipeline.c new file mode 100644 index 0000000..585bf1f --- /dev/null +++ b/src/accel/tt/pn_tt_stage_pipeline.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_stage_pipeline.c + * @brief The one-worker build pipeline. See the header for the contract. + * + * @status Used by the gdnc DELTA staging. + * @issues None + * @todo None + */ +#include "pn_tt_stage_pipeline.h" + +#include +#include + +/** The worker: wait for a job, build it batch by batch, publish progress, repeat. */ +static void *stage_pipeline_worker(void *argument) +{ + pn_tt_stage_pipeline *pipeline = (pn_tt_stage_pipeline *)argument; + + for (;;) { + pn_tt_stage_build_fn fn; + void *context; + uint32_t total, batch, done = 0u; + + pthread_mutex_lock(&pipeline->lock); + while (!pipeline->has_job && !pipeline->quit) + pthread_cond_wait(&pipeline->wake, &pipeline->lock); + if (pipeline->quit) { + pthread_mutex_unlock(&pipeline->lock); + return NULL; + } + pipeline->has_job = false; + fn = pipeline->fn; + context = pipeline->context; + total = pipeline->total; + batch = pipeline->batch; + pthread_mutex_unlock(&pipeline->lock); + + while (done < total) { + const uint32_t count = (total - done < batch) ? total - done : batch; + + if (fn(context, done, count) != 0) { + __atomic_store_n(&pipeline->failed, 1, __ATOMIC_RELEASE); + break; + } + done += count; + __atomic_store_n(&pipeline->progress, done, __ATOMIC_RELEASE); + } + } +} + +int pn_tt_stage_pipeline_start(pn_tt_stage_pipeline *pipeline, pn_tt_stage_build_fn fn, + void *context, uint32_t total, uint32_t batch) +{ + if (!pipeline || !fn || batch == 0u) + return -1; + if (!pipeline->started) { + memset(pipeline, 0, sizeof(*pipeline)); + if (pthread_mutex_init(&pipeline->lock, NULL) != 0) + return -1; + if (pthread_cond_init(&pipeline->wake, NULL) != 0) { + pthread_mutex_destroy(&pipeline->lock); + return -1; + } + if (pthread_create(&pipeline->thread, NULL, stage_pipeline_worker, pipeline) != 0) { + pthread_cond_destroy(&pipeline->wake); + pthread_mutex_destroy(&pipeline->lock); + return -1; + } + pipeline->started = true; + } + pthread_mutex_lock(&pipeline->lock); + __atomic_store_n(&pipeline->progress, 0u, __ATOMIC_RELEASE); + __atomic_store_n(&pipeline->failed, 0, __ATOMIC_RELEASE); + pipeline->fn = fn; + pipeline->context = context; + pipeline->total = total; + pipeline->batch = batch; + pipeline->has_job = true; + pthread_cond_signal(&pipeline->wake); + pthread_mutex_unlock(&pipeline->lock); + return 0; +} + +int pn_tt_stage_pipeline_wait(pn_tt_stage_pipeline *pipeline, uint32_t items) +{ + /* A SPIN, NOT A CONDITION WAIT: a batch is a few microseconds of work and the consumer is + * between MMIO uploads, so a sleep/wake round trip would cost more than the wait. The box + * has four CPUs and this runs on two of them. */ + for (;;) { + if (__atomic_load_n(&pipeline->failed, __ATOMIC_ACQUIRE) != 0) + return -1; + if (__atomic_load_n(&pipeline->progress, __ATOMIC_ACQUIRE) >= items) + return 0; + sched_yield(); + } +} + +void pn_tt_stage_pipeline_stop(pn_tt_stage_pipeline *pipeline) +{ + if (!pipeline || !pipeline->started) + return; + pthread_mutex_lock(&pipeline->lock); + pipeline->quit = true; + pthread_cond_signal(&pipeline->wake); + pthread_mutex_unlock(&pipeline->lock); + pthread_join(pipeline->thread, NULL); + pthread_cond_destroy(&pipeline->wake); + pthread_mutex_destroy(&pipeline->lock); + pipeline->started = false; +} diff --git a/src/accel/tt/pn_tt_stage_pipeline.h b/src/accel/tt/pn_tt_stage_pipeline.h new file mode 100644 index 0000000..4dc1f61 --- /dev/null +++ b/src/accel/tt/pn_tt_stage_pipeline.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_tt_stage_pipeline.h + * @brief A one-worker BUILD pipeline: a persistent thread builds items [0, total) in batches + * while the caller consumes the batches already built — so host-side construction of a + * staged block overlaps its own MMIO upload instead of preceding it. + * + * @status Used by the gdnc DELTA staging (`PN_TT_GDNC_STAGE_THREAD`). + * @issues None + * @todo None + * + * The builder must write only the bytes of the items it is handed, and the consumer must read + * only items `pn_tt_stage_pipeline_wait()` has released; that partition is the whole of the + * synchronisation contract. Progress is published with RELEASE and read with ACQUIRE, so a + * released item's bytes are visible to the consumer. + */ +#ifndef PN_TT_STAGE_PIPELINE_H +#define PN_TT_STAGE_PIPELINE_H + +#include +#include +#include + +/** Build items [first, first + count). Returns 0, or non-zero to fail the whole job. */ +typedef int (*pn_tt_stage_build_fn)(void *context, uint32_t first, uint32_t count); + +typedef struct pn_tt_stage_pipeline { + pthread_t thread; + pthread_mutex_t lock; + pthread_cond_t wake; + bool started; /**< the thread exists */ + bool quit; /**< asked to exit */ + bool has_job; /**< a job is posted and not yet picked up */ + pn_tt_stage_build_fn fn; + void *context; + uint32_t total, batch; + uint32_t progress; /**< items built so far; atomic, release/acquire */ + int failed; /**< non-zero once any batch failed; atomic */ +} pn_tt_stage_pipeline; + +/** Start a build of `total` items in batches of `batch`; spawns the worker on first use. + * @return 0, or -1 when the worker could not be started (the caller builds inline) */ +int pn_tt_stage_pipeline_start(pn_tt_stage_pipeline *pipeline, pn_tt_stage_build_fn fn, + void *context, uint32_t total, uint32_t batch); + +/** Block until at least `items` are built or the job failed. @return 0, or -1 on failure */ +int pn_tt_stage_pipeline_wait(pn_tt_stage_pipeline *pipeline, uint32_t items); + +/** Stop and join the worker, if any. Safe on a pipeline that never started. */ +void pn_tt_stage_pipeline_stop(pn_tt_stage_pipeline *pipeline); + +#endif /* PN_TT_STAGE_PIPELINE_H */ diff --git a/src/core/bench.c b/src/core/bench.c new file mode 100644 index 0000000..56c7d5b --- /dev/null +++ b/src/core/bench.c @@ -0,0 +1,61 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "bench.h" +#include +#include + +#ifdef __APPLE__ +#include +#elif defined(__linux__) +#include +#endif + +double pn_time_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1e6; +} + +size_t pn_memory_usage(void) { +#ifdef __APPLE__ + struct mach_task_basic_info info; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count) == KERN_SUCCESS) + return (size_t)info.resident_size; + return 0; +#elif defined(__linux__) + FILE *f = fopen("/proc/self/statm", "r"); + if (!f) return 0; + long pages = 0; + if (fscanf(f, "%*ld %ld", &pages) != 1) pages = 0; + fclose(f); + return (size_t)pages * (size_t)sysconf(_SC_PAGESIZE); +#else + return 0; +#endif +} + +void pn_bench_report(const pn_bench_result *result) { + if (!result) return; + + fprintf(stderr, "\n=== Benchmark ===\n"); + + if (result->n_prefill > 0 && result->prefill_ms > 0) { + double prefill_tps = (double)result->n_prefill / (result->prefill_ms / 1000.0); + fprintf(stderr, " prefill: %d tokens in %.1f ms (%.1f tok/s)\n", + result->n_prefill, result->prefill_ms, prefill_tps); + } + + if (result->n_decode > 0 && result->decode_ms > 0) { + double decode_tps = (double)result->n_decode / (result->decode_ms / 1000.0); + fprintf(stderr, " decode: %d tokens in %.1f ms (%.1f tok/s)\n", + result->n_decode, result->decode_ms, decode_tps); + } + + if (result->memory_bytes > 0) { + double mb = (double)result->memory_bytes / (1024.0 * 1024.0); + fprintf(stderr, " memory: %.1f MB RSS\n", mb); + } + + fprintf(stderr, "=================\n"); +} diff --git a/src/core/bench.h b/src/core/bench.h new file mode 100644 index 0000000..3ac05e2 --- /dev/null +++ b/src/core/bench.h @@ -0,0 +1,31 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_BENCH_H +#define PN_BENCH_H + +#include +#include + +/* + * Benchmark timing for Pantheon inference. + * Reports tok/s for prefill and decode phases. + */ + +typedef struct { + double prefill_ms; + double decode_ms; + int n_prefill; + int n_decode; + size_t memory_bytes; +} pn_bench_result; + +/* Get current time in milliseconds (monotonic clock) */ +double pn_time_ms(void); + +/* Print benchmark results to stderr */ +void pn_bench_report(const pn_bench_result *result); + +/* Get approximate RSS memory usage in bytes */ +size_t pn_memory_usage(void); + +#endif /* PN_BENCH_H */ diff --git a/src/core/bpe_pretokenize.c b/src/core/bpe_pretokenize.c new file mode 100644 index 0000000..5897688 --- /dev/null +++ b/src/core/bpe_pretokenize.c @@ -0,0 +1,374 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bpe_pretokenize.c + * @brief Pre-token splitter (qwen35/GPT-2 and llama-bpe modes) feeding ft_encode. + * + * Rewritten from a behaviour specification and measured behaviour; + * the implementer did not see the previous code. The split is a hand-written + * scanner over a fixed character-class table, tried rule by rule at each position: + * contraction, letter run, digits, symbol run, whitespace, single code point. + * + * @status None + * @issues - Decoder does not validate continuation bytes (a lead byte swallows whatever + * follows, e.g. C3 0A is one code point); stray high bytes classify as Latin-1. + * - Class table is coarse: e.g. 0x1E00-0x1EFF, Thai, fullwidth Latin are symbols; + * Arabic-Indic digits are both letters and numbers. + * - Contractions are case-insensitive in both modes, ASCII apostrophe only. + * - Unknown pre-type warning flag is a non-thread-safe static. + * @todo None + */ +#include "bpe_pretokenize.h" +#include "engine.h" + +#include +#include +#include +#include +#include +#include + +/* --------------------------------------------------------------------------- + * Name -> pre-type + * ------------------------------------------------------------------------- */ + +int pn_bpe_pre_from_name(const char *pre) { + if (pre == NULL) return PN_BPE_PRE_QWEN35; + static const char *const qwen_names[] = { "qwen35", "qwen2", "gpt-2", "default" }; + static const char *const llama_names[] = { "llama-bpe", "llama3", "llama-v3" }; + for (size_t i = 0; i < sizeof qwen_names / sizeof qwen_names[0]; i++) + if (strcmp(pre, qwen_names[i]) == 0) return PN_BPE_PRE_QWEN35; + for (size_t i = 0; i < sizeof llama_names / sizeof llama_names[0]; i++) + if (strcmp(pre, llama_names[i]) == 0) return PN_BPE_PRE_LLAMA3; + return PN_BPE_PRE_UNKNOWN; +} + +/* --------------------------------------------------------------------------- + * Lenient UTF-8 decode: width and code point at byte offset p. + * Continuation bytes are masked, never checked. + * ------------------------------------------------------------------------- */ + +static size_t decode_at(const unsigned char *s, size_t len, size_t p, uint32_t *cp) { + uint32_t c = s[p]; + if (c < 0x80) { + *cp = c; + return 1; + } + if ((c & 0xE0) == 0xC0 && p + 1 < len) { + *cp = ((c & 0x1F) << 6) | (s[p + 1] & 0x3Fu); + return 2; + } + if ((c & 0xF0) == 0xE0 && p + 2 < len) { + *cp = ((c & 0x0F) << 12) | ((s[p + 1] & 0x3Fu) << 6) | (s[p + 2] & 0x3Fu); + return 3; + } + if ((c & 0xF8) == 0xF0 && p + 3 < len) { + *cp = ((c & 0x07) << 18) | ((s[p + 1] & 0x3Fu) << 12) | + ((s[p + 2] & 0x3Fu) << 6) | (s[p + 3] & 0x3Fu); + return 4; + } + *cp = c; + return 1; +} + +/* --------------------------------------------------------------------------- + * Character classes. Each set is independent; they overlap on purpose. + * ------------------------------------------------------------------------- */ + +typedef struct { uint32_t lo, hi; } cp_range; + +static bool in_ranges(uint32_t cp, const cp_range *r, size_t n) { + for (size_t i = 0; i < n; i++) + if (cp >= r[i].lo && cp <= r[i].hi) return true; + return false; +} + +#define N_RANGES(a) (sizeof(a) / sizeof((a)[0])) + +static const cp_range letter_ranges[] = { + { 0x0041, 0x005A }, { 0x0061, 0x007A }, + { 0x00AA, 0x00AA }, { 0x00B5, 0x00B5 }, { 0x00BA, 0x00BA }, + { 0x00C0, 0x00D6 }, { 0x00D8, 0x00F6 }, { 0x00F8, 0x00FF }, + { 0x0100, 0x02AF }, + { 0x0370, 0x03FF }, + { 0x0400, 0x04FF }, + { 0x0530, 0x058F }, + { 0x0590, 0x05FF }, + { 0x0600, 0x06FF }, + { 0x0900, 0x097F }, + { 0x1100, 0x11FF }, + { 0x3040, 0x309F }, + { 0x30A0, 0x30FF }, + { 0x3400, 0x4DBF }, + { 0x4E00, 0x9FFF }, + { 0xAC00, 0xD7AF }, + { 0xF900, 0xFAFF }, + { 0x20000, 0x2FA1F }, +}; + +static const cp_range number_ranges[] = { + { 0x0030, 0x0039 }, + { 0x0660, 0x0669 }, + { 0x06F0, 0x06F9 }, + { 0xFF10, 0xFF19 }, +}; + +static const cp_range mark_ranges[] = { + { 0x0300, 0x036F }, + { 0x0483, 0x0489 }, + { 0x0591, 0x05BD }, + { 0x064B, 0x065F }, + { 0x0900, 0x0903 }, + { 0x093A, 0x094F }, + { 0x1AB0, 0x1AFF }, + { 0x1DC0, 0x1DFF }, + { 0x20D0, 0x20FF }, + { 0xFE20, 0xFE2F }, +}; + +static const cp_range space_ranges[] = { + { 0x0009, 0x000D }, /* tab, LF, VT, FF, CR */ + { 0x0020, 0x0020 }, + { 0x0085, 0x0085 }, + { 0x00A0, 0x00A0 }, + { 0x1680, 0x1680 }, + { 0x2000, 0x200A }, + { 0x2028, 0x2029 }, + { 0x202F, 0x202F }, + { 0x205F, 0x205F }, + { 0x3000, 0x3000 }, +}; + +static bool cls_letter(uint32_t cp) { return in_ranges(cp, letter_ranges, N_RANGES(letter_ranges)); } +static bool cls_number(uint32_t cp) { return in_ranges(cp, number_ranges, N_RANGES(number_ranges)); } +static bool cls_mark(uint32_t cp) { return in_ranges(cp, mark_ranges, N_RANGES(mark_ranges)); } +static bool cls_space(uint32_t cp) { return in_ranges(cp, space_ranges, N_RANGES(space_ranges)); } +static bool cls_newline(uint32_t cp) { return cp == 0x0A || cp == 0x0D; } + +/* Mode-dependent derived predicates. `llama` selects the llama-bpe rules. */ + +static bool is_run(uint32_t cp, bool llama) { + if (cls_letter(cp)) return true; + return !llama && cls_mark(cp); +} + +static bool is_sym(uint32_t cp, bool llama) { + if (cls_space(cp) || cls_letter(cp) || cls_number(cp)) return false; + if (!llama && cls_mark(cp)) return false; + return true; +} + +static bool is_prefix(uint32_t cp) { + return !cls_newline(cp) && !cls_letter(cp) && !cls_number(cp); +} + +/* --------------------------------------------------------------------------- + * One pre-token: returns the exclusive end offset of the pre-token at pos. + * ------------------------------------------------------------------------- */ + +static unsigned char ascii_lower(unsigned char c) { + return (c >= 'A' && c <= 'Z') ? (unsigned char)(c + ('a' - 'A')) : c; +} + +static size_t scan_contraction(const unsigned char *s, size_t len, size_t pos) { + if (s[pos] != '\'' || pos + 1 >= len) return 0; + unsigned char a = ascii_lower(s[pos + 1]); + if (a == 's' || a == 't' || a == 'm' || a == 'd') return pos + 2; + if (pos + 2 < len) { + unsigned char b = ascii_lower(s[pos + 2]); + if ((a == 'r' && b == 'e') || (a == 'v' && b == 'e') || (a == 'l' && b == 'l')) + return pos + 3; + } + return 0; +} + +static size_t scan_letters(const unsigned char *s, size_t len, size_t pos, + uint32_t cp0, size_t w0, bool llama) { + size_t start; + uint32_t next; + if (is_prefix(cp0) && pos + w0 < len && + (decode_at(s, len, pos + w0, &next), is_run(next, llama))) { + start = pos + w0; + } else if (is_run(cp0, llama)) { + start = pos; + } else { + return 0; + } + size_t p = start; + while (p < len) { + uint32_t cp; + size_t w = decode_at(s, len, p, &cp); + if (!is_run(cp, llama)) break; + p += w; + } + return p; +} + +static size_t scan_digits(const unsigned char *s, size_t len, size_t pos, + uint32_t cp0, size_t w0, bool llama) { + if (!cls_number(cp0)) return 0; + size_t p = pos + w0; + if (!llama) return p; + for (int extra = 0; extra < 2 && p < len; extra++) { + uint32_t cp; + size_t w = decode_at(s, len, p, &cp); + if (!cls_number(cp)) break; + p += w; + } + return p; +} + +static size_t scan_symbols(const unsigned char *s, size_t len, size_t pos, + uint32_t cp0, size_t w0, bool llama) { + size_t start = pos; + uint32_t cp = cp0; + if (cp0 == 0x20 && pos + w0 < len) { + uint32_t next; + decode_at(s, len, pos + w0, &next); + if (is_sym(next, llama)) { + start = pos + w0; + cp = next; + } + } + if (!is_sym(cp, llama)) return 0; + size_t p = start; + while (p < len) { + uint32_t c; + size_t w = decode_at(s, len, p, &c); + if (!is_sym(c, llama)) break; + p += w; + } + while (p < len) { + uint32_t c; + size_t w = decode_at(s, len, p, &c); + if (!cls_newline(c)) break; + p += w; + } + return p; +} + +static size_t scan_space(const unsigned char *s, size_t len, size_t pos, uint32_t cp0) { + if (!cls_space(cp0)) return 0; + size_t p = pos, last_start = pos, nl_end = 0, n = 0; + while (p < len) { + uint32_t c; + size_t w = decode_at(s, len, p, &c); + if (!cls_space(c)) break; + last_start = p; + p += w; + n++; + if (cls_newline(c)) nl_end = p; + } + if (nl_end != 0) return nl_end; /* stop after the last newline */ + if (p < len && n >= 2) return last_start; /* leave one space for the next word */ + return p; +} + +static size_t scan_one(const unsigned char *s, size_t len, size_t pos, bool llama) { + uint32_t cp0; + size_t w0 = decode_at(s, len, pos, &cp0); + size_t end; + if ((end = scan_contraction(s, len, pos)) != 0) return end; + if ((end = scan_letters(s, len, pos, cp0, w0, llama)) != 0) return end; + if ((end = scan_digits(s, len, pos, cp0, w0, llama)) != 0) return end; + if ((end = scan_symbols(s, len, pos, cp0, w0, llama)) != 0) return end; + if ((end = scan_space(s, len, pos, cp0)) != 0) return end; + return pos + w0; +} + +/* --------------------------------------------------------------------------- + * Driver + * ------------------------------------------------------------------------- */ + +static int *encode_with_mode(ft_tokenizer *tok, const char *text, int *length, bool llama) { + *length = 0; + if (tok == NULL || text == NULL) return calloc(1, sizeof(int)); + + const unsigned char *s = (const unsigned char *)text; + size_t len = strlen(text); + + int *ids = NULL; + size_t n_ids = 0, cap_ids = 0; + char *piece = NULL; + size_t cap_piece = 0; + + size_t pos = 0; + while (pos < len) { + size_t end = scan_one(s, len, pos, llama); + if (end <= pos) end = pos + 1; + size_t plen = end - pos; + + if (plen + 1 > cap_piece) { + size_t want = cap_piece ? cap_piece : 256; + while (want < plen + 1) want *= 2; + char *grown = realloc(piece, want); + if (grown == NULL) goto fail; + piece = grown; + cap_piece = want; + } + memcpy(piece, s + pos, plen); + piece[plen] = '\0'; + + char *enc = gpt2_bytes_encode(piece); + if (enc == NULL) goto fail; + int sub_n = 0; + int *sub = ft_encode(tok, enc, &sub_n); + free(enc); + if (sub == NULL) goto fail; + + if (sub_n > 0) { + size_t need = n_ids + (size_t)sub_n; + if (need > cap_ids) { + size_t want = cap_ids ? cap_ids : 256; + while (want < need) want *= 2; + int *grown = realloc(ids, want * sizeof(int)); + if (grown == NULL) { + free(sub); + goto fail; + } + ids = grown; + cap_ids = want; + } + memcpy(ids + n_ids, sub, (size_t)sub_n * sizeof(int)); + n_ids = need; + } + free(sub); + pos = end; + } + + free(piece); + *length = (int)n_ids; + if (n_ids == 0) { + free(ids); + return calloc(1, sizeof(int)); + } + return ids; + +fail: + free(piece); + free(ids); + return NULL; +} + +int *pn_bpe_encode_qwen35(ft_tokenizer *tok, const char *text, int *length) { + return encode_with_mode(tok, text, length, false); +} + +int *pn_bpe_encode_llama3(ft_tokenizer *tok, const char *text, int *length) { + return encode_with_mode(tok, text, length, true); +} + +int *pn_bpe_encode(ft_tokenizer *tok, const char *text, int *length) { + int pre = tok ? ft_get_pre_type(tok) : PN_BPE_PRE_QWEN35; + if (pre == PN_BPE_PRE_LLAMA3) return pn_bpe_encode_llama3(tok, text, length); + if (pre == PN_BPE_PRE_UNKNOWN) { + static bool warned = false; + if (!warned) { + warned = true; + fprintf(stderr, "bpe: tokenizer.ggml.pre is unsupported; falling back to the " + "qwen35/GPT-2 pre-tokenizer (token IDs may not match the " + "reference for this model)\n"); + } + } + return pn_bpe_encode_qwen35(tok, text, length); +} diff --git a/src/core/bpe_pretokenize.h b/src/core/bpe_pretokenize.h new file mode 100644 index 0000000..84c557e --- /dev/null +++ b/src/core/bpe_pretokenize.h @@ -0,0 +1,58 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bpe_pretokenize.h + * @brief Byte-level BPE encoding with a pre-token splitter (qwen35/GPT-2 and llama-bpe). + * + * Rewritten from a behaviour specification and measured behaviour; + * the implementer did not see the previous code. + * + * The splitter only decides pre-token boundaries. Each pre-token's raw bytes are + * GPT-2 byte-encoded and handed to ft_encode, and the per-piece IDs are concatenated. + * + * @status None + * @issues - Unknown pre-type warning uses a process-wide flag that is not thread-safe. + * - The UTF-8 decoder does not validate continuation bytes; invalid bytes + * classify by their Latin-1 value. + * - Character classes are a fixed coarse range table, not the Unicode database. + * - `length` is written before any argument check (NULL `length` crashes). + * @todo None + */ +#ifndef PN_BPE_PRETOKENIZE_H +#define PN_BPE_PRETOKENIZE_H + +#include "tokenizer/tokenizer.h" + +/** Pre-tokenizer variants. Values are stored via ft_set_pre_type and must not change. */ +typedef enum { + PN_BPE_PRE_QWEN35 = 0, /**< qwen35 / GPT-2 style split (single digits). */ + PN_BPE_PRE_LLAMA3 = 1, /**< llama-bpe split (digit groups of up to 3). */ + PN_BPE_PRE_UNKNOWN = 2, /**< Unrecognised name; encodes as qwen35 with a warning. */ +} pn_bpe_pre; + +/** + * Map a `tokenizer.ggml.pre` string to a pn_bpe_pre value (exact, case-sensitive). + * NULL, "qwen35", "qwen2", "gpt-2", "default" -> QWEN35; + * "llama-bpe", "llama3", "llama-v3" -> LLAMA3; anything else -> UNKNOWN. + */ +int pn_bpe_pre_from_name(const char *pre); + +/** + * Encode `text` with the pre-tokenizer selected by ft_get_pre_type(tok) + * (QWEN35 when tok is NULL). LLAMA3 -> pn_bpe_encode_llama3. UNKNOWN prints a + * one-line warning to stderr on the first such call in the process, then encodes + * as qwen35. Any other value encodes as qwen35 silently. + */ +int *pn_bpe_encode(ft_tokenizer *tok, const char *text, int *length); + +/** + * Encode `text` with the qwen35/GPT-2 split. `length` must be non-NULL; it is set to + * 0 first and to the ID count on success. Returns a malloc'd ID array (caller frees); + * a 1-int zeroed buffer when the result is empty or tok/text is NULL; NULL on failure. + */ +int *pn_bpe_encode_qwen35(ft_tokenizer *tok, const char *text, int *length); + +/** As pn_bpe_encode_qwen35, with the llama-bpe split. */ +int *pn_bpe_encode_llama3(ft_tokenizer *tok, const char *text, int *length); + +#endif /* PN_BPE_PRETOKENIZE_H */ diff --git a/src/core/chat.c b/src/core/chat.c new file mode 100644 index 0000000..4adafb4 --- /dev/null +++ b/src/core/chat.c @@ -0,0 +1,396 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "chat.h" +#include "engine.h" +#include "bpe_pretokenize.h" +#include +#include +#include + +/* ============================================================ + * Sequence builder — accumulates token IDs + * ============================================================ */ + +typedef struct { + uint32_t *tokens; + int len; + int cap; +} seq_builder; + +static void seq_init(seq_builder *s) { + s->cap = 256; + s->tokens = malloc(s->cap * sizeof(uint32_t)); + s->len = 0; +} + +static void seq_push_id(seq_builder *s, int id) { + if (s->len >= s->cap) { + s->cap *= 2; + s->tokens = realloc(s->tokens, s->cap * sizeof(uint32_t)); + } + s->tokens[s->len++] = (uint32_t)id; +} + +static void seq_push_ids(seq_builder *s, int *ids, int n) { + for (int i = 0; i < n; i++) { + seq_push_id(s, ids[i]); + } +} + +/* + * Push a special token by looking up its string in the vocab. + * Uses ft_token_to_id which does exact hash lookup. + */ +static void seq_push_special(seq_builder *s, ft_tokenizer *tok, const char *token_str) { + int id = ft_token_to_id(tok, token_str); + if (id < 0) { + fprintf(stderr, "chat: special token '%s' not found in vocab\n", token_str); + return; + } + seq_push_id(s, id); +} + +/* + * Push a text segment, encoding it properly for the tokenizer type. + * BPE models need GPT-2 byte encoding first. + * SentencePiece models encode directly (no leading space added here). + */ +static void seq_push_text(seq_builder *s, ft_tokenizer *tok, const char *text) { + if (!text || !*text) return; + + int is_bpe = (ft_get_model_type(tok) == 0); + int n = 0; + int *ids; + + if (is_bpe) { + /* qwen35 pre-tokenization + GPT-2 byte BPE (matches llama.cpp). */ + ids = pn_bpe_encode(tok, text, &n); + } else { + ids = ft_encode(tok, text, &n); + } + + if (ids) { + seq_push_ids(s, ids, n); + free(ids); + } +} + +/* + * Push text encoded via SentencePiece path — adds leading space for word boundary. + * Used for the initial text in Mistral format where SentencePiece BOS handling + * is done separately. + */ +static void seq_push_text_sp(seq_builder *s, ft_tokenizer *tok, const char *text, int add_bos) { + if (!text || !*text) return; + + int is_bpe = (ft_get_model_type(tok) == 0); + int n = 0; + int *ids; + + if (is_bpe) { + char *encoded = gpt2_bytes_encode(text); + if (!encoded) return; + ids = ft_encode_with_special(tok, encoded, &n, add_bos, false); + free(encoded); + } else { + ids = ft_encode_with_special(tok, text, &n, add_bos, false); + } + + if (ids) { + seq_push_ids(s, ids, n); + free(ids); + } +} + +static pn_token_seq seq_finish(seq_builder *s) { + pn_token_seq result; + result.tokens = s->tokens; + result.n_tokens = s->len; + return result; +} + +/* ============================================================ + * Style detection + * ============================================================ */ + +pn_chat_style pn_chat_detect_style(const char *jinja_template) { + if (!jinja_template) return PN_CHAT_NONE; + + if (strstr(jinja_template, "<|im_start|>")) { + /* Reasoning models (Nex-N2, Qwen3) prefill the assistant turn with + * via `add_generation_prompt`; match that for oracle parity. + * + * The prefill must match the MODEL'S OWN template exactly: + * Nex-N2: {{- '' }} -> prefill (_THINK) + * Bonsai: {{- '\n' }} -> prefill + "\n" (_THINK_NL) + * The distinguishing literal is the Jinja string '\n' with the + * escaped newline immediately before the closing quote — Nex-N2's + * non-thinking branch '\n\n\n\n' does NOT match it. */ + if (strstr(jinja_template, "")) { + if (strstr(jinja_template, "'\\n'")) + return PN_CHAT_CHATML_THINK_NL; + return PN_CHAT_CHATML_THINK; + } + return PN_CHAT_CHATML; + } + if (strstr(jinja_template, "[INST]")) return PN_CHAT_MISTRAL; + if (strstr(jinja_template, "<|system|>") || + strstr(jinja_template, "<|user|>")) return PN_CHAT_PHI3; + + return PN_CHAT_NONE; +} + +pn_chat_style pn_chat_style_from_arch(int arch) { + switch (arch) { + case 7: /* PN_ARCH_QWEN3 */ + case 1: /* PN_ARCH_QWEN2 */ + case 0: /* PN_ARCH_LLAMA (SmolLM2 uses llama arch) */ + return PN_CHAT_CHATML; + case 3: /* PN_ARCH_MISTRAL */ + case 4: /* PN_ARCH_MIXTRAL */ + return PN_CHAT_MISTRAL; + case 2: /* PN_ARCH_PHI */ + return PN_CHAT_PHI3; + default: + return PN_CHAT_NONE; + } +} + +const char* pn_chat_style_name(pn_chat_style style) { + switch (style) { + case PN_CHAT_CHATML: return "ChatML"; + case PN_CHAT_CHATML_THINK: return "ChatML+think"; + case PN_CHAT_CHATML_THINK_NL: return "ChatML+think\\n"; + case PN_CHAT_MISTRAL: return "Mistral"; + case PN_CHAT_PHI3: return "Phi-3"; + case PN_CHAT_NONE: return "none"; + } + return "unknown"; +} + +/* ============================================================ + * ChatML format + * + * Turn 0: <|im_start|>system\n{sys}<|im_end|>\n + * <|im_start|>user\n{user}<|im_end|>\n + * <|im_start|>assistant\n + * + * Turn 1+: <|im_end|>\n + * <|im_start|>user\n{user}<|im_end|>\n + * <|im_start|>assistant\n + * ============================================================ */ + +/* think: append the reasoning prefill after the assistant header, + * matching the model's `add_generation_prompt` template branch. + * think == 2 (_THINK_NL): the model's template prefills '\n' — the + * newline is part of the trained prefill and steers greedy decode INTO the + * reasoning branch (see PN_CHAT_CHATML_THINK_NL in chat.h). */ +static pn_token_seq format_chatml_first(ft_tokenizer *tok, + const char *system, const char *user, + int think) { + seq_builder s; + seq_init(&s); + + if (system && *system) { + seq_push_special(&s, tok, "<|im_start|>"); + seq_push_text(&s, tok, "system\n"); + seq_push_text(&s, tok, system); + seq_push_special(&s, tok, "<|im_end|>"); + seq_push_text(&s, tok, "\n"); + } + + seq_push_special(&s, tok, "<|im_start|>"); + seq_push_text(&s, tok, "user\n"); + seq_push_text(&s, tok, user); + seq_push_special(&s, tok, "<|im_end|>"); + seq_push_text(&s, tok, "\n"); + + seq_push_special(&s, tok, "<|im_start|>"); + seq_push_text(&s, tok, "assistant\n"); + if (think) { + seq_push_special(&s, tok, ""); + if (think == 2) + seq_push_text(&s, tok, "\n"); + } + + return seq_finish(&s); +} + +static pn_token_seq format_chatml_turn(ft_tokenizer *tok, const char *user, + int think) { + seq_builder s; + seq_init(&s); + + seq_push_special(&s, tok, "<|im_end|>"); + seq_push_text(&s, tok, "\n"); + + seq_push_special(&s, tok, "<|im_start|>"); + seq_push_text(&s, tok, "user\n"); + seq_push_text(&s, tok, user); + seq_push_special(&s, tok, "<|im_end|>"); + seq_push_text(&s, tok, "\n"); + + seq_push_special(&s, tok, "<|im_start|>"); + seq_push_text(&s, tok, "assistant\n"); + if (think) { + seq_push_special(&s, tok, ""); + if (think == 2) + seq_push_text(&s, tok, "\n"); + } + + return seq_finish(&s); +} + +/* ============================================================ + * Mistral format + * + * Turn 0: [BOS] [INST] {user} [/INST] + * Turn 1+: [INST] {user} [/INST] + * + * [INST] and [/INST] are plain text, not special tokens. + * BOS is added by SentencePiece, is the EOS special token. + * ============================================================ */ + +static pn_token_seq format_mistral_first(ft_tokenizer *tok, const char *user) { + seq_builder s; + seq_init(&s); + + /* Build the text: "[INST] {user} [/INST]" and let tokenizer add BOS */ + size_t ulen = strlen(user); + char *text = malloc(8 + ulen + 9 + 1); + sprintf(text, "[INST] %s [/INST]", user); + + seq_push_text_sp(&s, tok, text, 1); /* add_bos=1 for SentencePiece */ + free(text); + + return seq_finish(&s); +} + +static pn_token_seq format_mistral_turn(ft_tokenizer *tok, const char *user) { + seq_builder s; + seq_init(&s); + + /* EOS token to end previous assistant turn */ + int eos_id = ft_eos_id(tok); + if (eos_id >= 0) seq_push_id(&s, eos_id); + + /* " [INST] {user} [/INST]" — space before [INST] for SentencePiece */ + size_t ulen = strlen(user); + char *text = malloc(9 + ulen + 9 + 1); + sprintf(text, " [INST] %s [/INST]", user); + + seq_push_text(&s, tok, text); + free(text); + + return seq_finish(&s); +} + +/* ============================================================ + * Phi-3 format + * + * Turn 0: <|system|>\n{sys}<|end|>\n + * <|user|>\n{user}<|end|>\n + * <|assistant|>\n + * + * Turn 1+: <|end|>\n + * <|user|>\n{user}<|end|>\n + * <|assistant|>\n + * ============================================================ */ + +static pn_token_seq format_phi3_first(ft_tokenizer *tok, + const char *system, const char *user) { + seq_builder s; + seq_init(&s); + + if (system && *system) { + seq_push_special(&s, tok, "<|system|>"); + seq_push_text(&s, tok, "\n"); + seq_push_text(&s, tok, system); + seq_push_special(&s, tok, "<|end|>"); + seq_push_text(&s, tok, "\n"); + } + + seq_push_special(&s, tok, "<|user|>"); + seq_push_text(&s, tok, "\n"); + seq_push_text(&s, tok, user); + seq_push_special(&s, tok, "<|end|>"); + seq_push_text(&s, tok, "\n"); + + seq_push_special(&s, tok, "<|assistant|>"); + seq_push_text(&s, tok, "\n"); + + return seq_finish(&s); +} + +static pn_token_seq format_phi3_turn(ft_tokenizer *tok, const char *user) { + seq_builder s; + seq_init(&s); + + seq_push_special(&s, tok, "<|end|>"); + seq_push_text(&s, tok, "\n"); + + seq_push_special(&s, tok, "<|user|>"); + seq_push_text(&s, tok, "\n"); + seq_push_text(&s, tok, user); + seq_push_special(&s, tok, "<|end|>"); + seq_push_text(&s, tok, "\n"); + + seq_push_special(&s, tok, "<|assistant|>"); + seq_push_text(&s, tok, "\n"); + + return seq_finish(&s); +} + +/* ============================================================ + * Public API + * ============================================================ */ + +pn_token_seq pn_chat_format_first(pn_chat_style style, ft_tokenizer *tok, + const char *system, const char *user) { + switch (style) { + case PN_CHAT_CHATML: + return format_chatml_first(tok, system, user, 0); + case PN_CHAT_CHATML_THINK: + return format_chatml_first(tok, system, user, 1); + case PN_CHAT_CHATML_THINK_NL: + return format_chatml_first(tok, system, user, 2); + case PN_CHAT_MISTRAL: + return format_mistral_first(tok, user); + case PN_CHAT_PHI3: + return format_phi3_first(tok, system, user); + case PN_CHAT_NONE: { + /* Passthrough: just encode the user text with BOS */ + seq_builder s; + seq_init(&s); + seq_push_text_sp(&s, tok, user, 1); + return seq_finish(&s); + } + } + /* Unreachable, but satisfy compiler */ + pn_token_seq empty = {NULL, 0}; + return empty; +} + +pn_token_seq pn_chat_format_turn(pn_chat_style style, ft_tokenizer *tok, + const char *user) { + switch (style) { + case PN_CHAT_CHATML: + return format_chatml_turn(tok, user, 0); + case PN_CHAT_CHATML_THINK: + return format_chatml_turn(tok, user, 1); + case PN_CHAT_CHATML_THINK_NL: + return format_chatml_turn(tok, user, 2); + case PN_CHAT_MISTRAL: + return format_mistral_turn(tok, user); + case PN_CHAT_PHI3: + return format_phi3_turn(tok, user); + case PN_CHAT_NONE: { + seq_builder s; + seq_init(&s); + seq_push_text(&s, tok, user); + return seq_finish(&s); + } + } + pn_token_seq empty = {NULL, 0}; + return empty; +} diff --git a/src/core/chat.h b/src/core/chat.h new file mode 100644 index 0000000..2cb3dae --- /dev/null +++ b/src/core/chat.h @@ -0,0 +1,49 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_CHAT_H +#define PN_CHAT_H + +#include +#include "tokenizer/tokenizer.h" + +/* Supported chat template styles */ +typedef enum { + PN_CHAT_CHATML, /* Qwen2.5, SmolLM2 */ + PN_CHAT_CHATML_THINK, /* ChatML + reasoning prefill (Nex-N2, Qwen3): assistant turn opens with */ + PN_CHAT_MISTRAL, /* Mistral, Mixtral */ + PN_CHAT_PHI3, /* Phi-3 */ + PN_CHAT_NONE, /* Raw passthrough */ + PN_CHAT_CHATML_THINK_NL, /* ChatML + reasoning prefill "\n" (Bonsai-27B): + * the model's own template opens the assistant turn + * with {{- '\n' }} — the newline is part of the + * trained prefill. Prefilling bare (the _THINK + * form) leaves "\n\n\n\n" — the template's + * NON-thinking serialization — as the natural greedy + * continuation, so the model deterministically closes + * the think block empty and reasoning is forfeited. */ +} pn_chat_style; + +/* Token sequence (caller frees tokens) */ +typedef struct { + uint32_t *tokens; + int n_tokens; +} pn_token_seq; + +/* Detect chat style from Jinja2 template string (GGUF metadata) */ +pn_chat_style pn_chat_detect_style(const char *jinja_template); + +/* Fallback: detect from architecture enum value */ +pn_chat_style pn_chat_style_from_arch(int arch); + +/* Format first turn: system (optional) + user + assistant prefix → token IDs */ +pn_token_seq pn_chat_format_first(pn_chat_style style, ft_tokenizer *tok, + const char *system, const char *user); + +/* Format turn 1+: EOT + user + assistant prefix → token IDs */ +pn_token_seq pn_chat_format_turn(pn_chat_style style, ft_tokenizer *tok, + const char *user); + +/* Human-readable style name */ +const char* pn_chat_style_name(pn_chat_style style); + +#endif /* PN_CHAT_H */ diff --git a/src/core/engine.c b/src/core/engine.c new file mode 100644 index 0000000..5b687b0 --- /dev/null +++ b/src/core/engine.c @@ -0,0 +1,4366 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "engine.h" +#include "grammar_mask_shortlist.h" +#include "chat.h" +#include "bpe_pretokenize.h" +#include "../formats/gguf.h" +#include "../formats/gguf_dequant.h" +#include "../models/llama.h" +#include "../models/phi3.h" +#include "../models/rwkvx.h" +#include "../models/qwen35moe.h" +#include "ngram_draft.h" +#include "../models/bonsai.h" +#include "../models/pixtral_vision.h" +#include "../memory/arena.h" +#include "mem/pn_admission.h" +#include "mem/pn_kv_geometry.h" +#include "mem/pn_memwatch.h" +#include "mem/pn_ladder.h" +#include "qweight.h" +#include "sampling_modifiers.h" +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif +#ifdef USE_CUDA +#include "cuda/fc_cuda.h" +#endif +#ifdef USE_METAL +#include "metal/fm_metal.h" +#endif +#include +#include +#include +#include +#include +#include + +/* ============================================================ + * GPT-2 byte-level BPE decoding + * + * GPT-2 maps bytes to Unicode codepoints to avoid control chars: + * bytes 33-126 → same codepoint (printable ASCII) + * bytes 161-172 → same codepoint + * bytes 174-255 → same codepoint + * bytes 0-32, 127-160, 173 → Unicode 256+ (Ā, ā, Ă, ...) + * This function reverses that mapping. + * ============================================================ */ + +static int gpt2_unicode_to_byte(unsigned int cp) { + /* Direct-mapped ranges: printable ASCII and high Latin-1 */ + if (cp >= 33 && cp <= 126) return (int)cp; + if (cp >= 161 && cp <= 172) return (int)cp; + if (cp >= 174 && cp <= 255) return (int)cp; + /* Remapped bytes: 0-32, 127-160, 173 are mapped to 256+ */ + if (cp >= 256 && cp < 256 + 33 + 34 + 1) { + static int reverse_map[69]; + static int initialized = 0; + if (!initialized) { + int idx = 0; + for (int b = 0; b < 256; b++) { + if ((b >= 33 && b <= 126) || (b >= 161 && b <= 172) || (b >= 174 && b <= 255)) + continue; + reverse_map[idx++] = b; + } + initialized = 1; + } + int offset = (int)(cp - 256); + if (offset >= 0 && offset < 69) return reverse_map[offset]; + } + return -1; +} + +/* + * GPT-2 byte encoding: maps each raw byte to its GPT-2 Unicode codepoint. + * This is the inverse of gpt2_bytes_decode. + * Must be applied to input text before BPE tokenization. + */ +char* gpt2_bytes_encode(const char *input) { + if (!input) return NULL; + + /* Build forward map: byte -> codepoint */ + static unsigned int byte_to_cp[256]; + static int initialized = 0; + if (!initialized) { + int idx = 256; + for (int b = 0; b < 256; b++) { + if ((b >= 33 && b <= 126) || (b >= 161 && b <= 172) || (b >= 174 && b <= 255)) { + byte_to_cp[b] = (unsigned int)b; + } else { + byte_to_cp[b] = (unsigned int)idx++; + } + } + initialized = 1; + } + + size_t len = strlen(input); + /* Worst case: each byte becomes 2-byte UTF-8 (codepoints 128-2047) */ + char *out = malloc(len * 3 + 1); + if (!out) return NULL; + + size_t oi = 0; + const unsigned char *p = (const unsigned char *)input; + + for (size_t i = 0; i < len; i++) { + unsigned int cp = byte_to_cp[p[i]]; + if (cp < 0x80) { + out[oi++] = (char)cp; + } else if (cp < 0x800) { + out[oi++] = (char)(0xC0 | (cp >> 6)); + out[oi++] = (char)(0x80 | (cp & 0x3F)); + } else { + out[oi++] = (char)(0xE0 | (cp >> 12)); + out[oi++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + out[oi++] = (char)(0x80 | (cp & 0x3F)); + } + } + + out[oi] = '\0'; + return out; +} + +char* gpt2_bytes_decode(const char *input) { + if (!input) return NULL; + + size_t len = strlen(input); + char *out = malloc(len + 1); + if (!out) return NULL; + + size_t oi = 0; + const unsigned char *p = (const unsigned char *)input; + + while (*p) { + unsigned int cp; + int bytes; + + if (*p < 0x80) { + cp = *p; + bytes = 1; + } else if ((*p & 0xE0) == 0xC0) { + cp = (*p & 0x1F) << 6; + if (p[1]) { cp |= (p[1] & 0x3F); bytes = 2; } + else { out[oi++] = (char)*p; p++; continue; } + } else if ((*p & 0xF0) == 0xE0) { + cp = (*p & 0x0F) << 12; + if (p[1] && p[2]) { + cp |= (p[1] & 0x3F) << 6; + cp |= (p[2] & 0x3F); + bytes = 3; + } else { out[oi++] = (char)*p; p++; continue; } + } else if ((*p & 0xF8) == 0xF0) { + cp = (*p & 0x07) << 18; + if (p[1] && p[2] && p[3]) { + cp |= (p[1] & 0x3F) << 12; + cp |= (p[2] & 0x3F) << 6; + cp |= (p[3] & 0x3F); + bytes = 4; + } else { out[oi++] = (char)*p; p++; continue; } + } else { + out[oi++] = (char)*p; p++; continue; + } + + int byte_val = gpt2_unicode_to_byte(cp); + if (byte_val >= 0) { + out[oi++] = (char)byte_val; + } else { + for (int i = 0; i < bytes; i++) { + out[oi++] = (char)p[i]; + } + } + p += bytes; + } + + out[oi] = '\0'; + return out; +} + +/* ============================================================ + * Sampling defaults + * ============================================================ */ + +pn_sampling_params pn_sampling_defaults(void) { + return (pn_sampling_params){ + .temperature = 0.8f, + .top_p = 0.9f, + .top_k = 40, + .repeat_penalty = 1.1f, + .repeat_last_n = 64, + }; +} + +/* ============================================================ + * Format detection + * ============================================================ */ + +bool pn_is_gguf(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return false; + + char magic[4]; + size_t n = fread(magic, 1, 4, f); + fclose(f); + + return (n == 4 && memcmp(magic, "GGUF", 4) == 0); +} + +bool pn_is_safetensors(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return false; + + /* Safetensors: 8-byte LE header length, then '{' JSON */ + uint8_t buf[9]; + size_t n = fread(buf, 1, 9, f); + fclose(f); + + if (n < 9) return false; + uint64_t header_len; + memcpy(&header_len, buf, 8); + return (header_len < 10000000 && buf[8] == '{'); +} + +/* ============================================================ + * Architecture registry + * ============================================================ */ + +pn_forward_fn pn_get_forward_fn(pn_arch arch) { + switch (arch) { + case PN_ARCH_LLAMA: return pn_llama_forward; + case PN_ARCH_QWEN2: return pn_llama_forward; + case PN_ARCH_MISTRAL: return pn_llama_forward; + case PN_ARCH_MIXTRAL: return pn_llama_forward; + case PN_ARCH_PHI: return pn_phi3_forward; + case PN_ARCH_QWEN35MOE: return pn_qwen35moe_forward; + case PN_ARCH_BONSAI: return pn_bonsai_forward; + default: return NULL; + } +} + +static pn_arch detect_arch_from_gguf(pn_gguf_file *gguf) { + const char *arch = pn_gguf_get_str(gguf, "general.architecture"); + if (!arch) return PN_ARCH_UNKNOWN; + + if (strcmp(arch, "llama") == 0) { + /* Mixtral uses arch="llama" but has expert_count metadata */ + uint32_t n_experts = pn_gguf_get_u32(gguf, "llama.expert_count", 0); + if (n_experts > 0) return PN_ARCH_MIXTRAL; + return PN_ARCH_LLAMA; + } + if (strcmp(arch, "qwen2") == 0) return PN_ARCH_QWEN2; + if (strcmp(arch, "qwen3") == 0) return PN_ARCH_QWEN3; + if (strcmp(arch, "phi") == 0) return PN_ARCH_PHI; + if (strcmp(arch, "phi3") == 0) return PN_ARCH_PHI; + if (strcmp(arch, "mistral") == 0) return PN_ARCH_MISTRAL; + /* MiniCPM5-1B-SFT (our orchestrator) is a plain LlamaForCausalLM: RoPE + GQA + * (24L, 16Q/2KV, head_dim=128 decoupled from n_embd/n_head=96) + SwiGLU + + * RMSNorm, no MiniCPM-4 scale_depth/scale_emb. convert_hf_to_gguf.py emits + * general.architecture="llama" for it (that is why it runs on vanilla + * llama.cpp out of the box), so the OFFICIAL openbmb/MiniCPM5-1B-GGUF is + * already handled by the "llama" branch above with ZERO plumbing. + * + * This "minicpm" alias is only a defense-in-depth net for a community GGUF + * that mis-tags this plain-Llama model as "minicpm". CAVEAT: such a file may + * write its dims under the "minicpm.*" metadata prefix, which arch_prefix() + * does not read (it uses "llama" for PN_ARCH_LLAMA) — so if a "minicpm"- + * tagged file ever fails to load, that prefix mismatch is why. Prefer the + * official arch="llama" GGUF; warn so the operator knows to check. */ + if (strcmp(arch, "minicpm") == 0) { + fprintf(stderr, "engine: architecture='minicpm' -> routing to the dense " + "Llama-GQA path (PN_ARCH_LLAMA). If dims read wrong, the " + "GGUF likely uses a 'minicpm.*' metadata prefix; use the " + "official arch=\"llama\" MiniCPM5-1B-GGUF instead.\n"); + return PN_ARCH_LLAMA; + } + if (strcmp(arch, "qwen35moe") == 0) return PN_ARCH_QWEN35MOE; + /* PrismML Bonsai-27B declares general.architecture="qwen35": the SAME hybrid + * GDN + sparse-full-attention family as Nex-N2-mini, but a DENSE SwiGLU FFN + * (no MoE) and native-ternary weights. It gets its OWN arch (PN_ARCH_BONSAI) + * and its own forward entry (bonsai_forward.c), which reuses the shared + * qwen35moe blocks. A "qwen35" file with routed experts (should any exist) + * still falls to the qwen35moe MoE path. */ + if (strcmp(arch, "qwen35") == 0) + return pn_bonsai_is_bonsai_gguf(gguf) ? PN_ARCH_BONSAI + : PN_ARCH_QWEN35MOE; + + fprintf(stderr, "engine: unknown architecture '%s'\n", arch); + return PN_ARCH_UNKNOWN; +} + +/* ============================================================ + * BPE tokenizer extraction from GGUF + * ============================================================ */ + +static ft_tokenizer* build_tokenizer_from_gguf(pn_gguf_file *gguf) { + pn_gguf_arr *tokens_arr = pn_gguf_get_arr(gguf, "tokenizer.ggml.tokens"); + if (!tokens_arr) { + fprintf(stderr, "engine: no tokenizer.ggml.tokens in GGUF\n"); + return NULL; + } + + if (tokens_arr->elem_type != GGUF_TYPE_STRING) { + fprintf(stderr, "engine: tokenizer.ggml.tokens is not string array\n"); + free(tokens_arr); + return NULL; + } + + ft_tokenizer *tok = ft_new(); + if (!tok) { + free(tokens_arr); + return NULL; + } + + uint64_t n_vocab = tokens_arr->n; + const uint8_t *p = (const uint8_t *)tokens_arr->data; + + for (uint64_t i = 0; i < n_vocab; i++) { + uint64_t slen; + memcpy(&slen, p, 8); + p += 8; + + char *token_str = malloc(slen + 1); + if (!token_str) break; + memcpy(token_str, p, slen); + token_str[slen] = '\0'; + p += slen; + + ft_add_token(tok, token_str); + free(token_str); + } + + free(tokens_arr); + + fprintf(stderr, "engine: loaded %llu vocab tokens\n", (unsigned long long)n_vocab); + + /* Detect tokenizer model type */ + const char *tok_model = pn_gguf_get_str(gguf, "tokenizer.ggml.model"); + + if (tok_model && strcmp(tok_model, "llama") == 0) { + /* SentencePiece / Unigram tokenizer */ + ft_set_model_type(tok, 1); + + pn_gguf_arr *scores_arr = pn_gguf_get_arr(gguf, "tokenizer.ggml.scores"); + if (scores_arr && scores_arr->elem_type == GGUF_TYPE_FLOAT32) { + ft_set_scores(tok, (float *)scores_arr->data, (int)scores_arr->n); + fprintf(stderr, "engine: loaded %llu unigram scores (SentencePiece)\n", + (unsigned long long)scores_arr->n); + } + free(scores_arr); + } else { + /* BPE tokenizer — load merges */ + pn_gguf_arr *merges_arr = pn_gguf_get_arr(gguf, "tokenizer.ggml.merges"); + if (merges_arr && merges_arr->elem_type == GGUF_TYPE_STRING) { + const uint8_t *mp = (const uint8_t *)merges_arr->data; + uint64_t n_merges = merges_arr->n; + + for (uint64_t i = 0; i < n_merges; i++) { + uint64_t slen; + memcpy(&slen, mp, 8); + mp += 8; + + char *merge_str = malloc(slen + 1); + if (!merge_str) break; + memcpy(merge_str, mp, slen); + merge_str[slen] = '\0'; + mp += slen; + + char *space = strchr(merge_str, ' '); + if (space) { + *space = '\0'; + ft_add_merge(tok, merge_str, space + 1); + } + free(merge_str); + } + + fprintf(stderr, "engine: loaded %llu merges (BPE)\n", (unsigned long long)n_merges); + free(merges_arr); + } + + /* Select the BPE pre-tokenizer scanner from tokenizer.ggml.pre. */ + const char *pre = pn_gguf_get_str(gguf, "tokenizer.ggml.pre"); + ft_set_pre_type(tok, pn_bpe_pre_from_name(pre)); + fprintf(stderr, "engine: BPE pre-tokenizer = %s\n", pre ? pre : "(unset)"); + } + + uint32_t bos_id = pn_gguf_get_u32(gguf, "tokenizer.ggml.bos_token_id", 0); + uint32_t eos_id = pn_gguf_get_u32(gguf, "tokenizer.ggml.eos_token_id", 0); + + const char *bos_tok = ft_id_to_token(tok, (int)bos_id); + const char *eos_tok = ft_id_to_token(tok, (int)eos_id); + if (bos_tok) ft_set_bos_token(tok, bos_tok); + if (eos_tok) ft_set_eos_token(tok, eos_tok); + + return tok; +} + +/* KV Phase 1: the per-session context window the generic (dense-llama) KV cache + * and its scores buffer are sized against. Historically hard-clamped to 4096, + * which is what forced admission to CAP/drop-oldest on a longer chat. Now driven + * by the configured context, clamped to the per-session hard cap + * (PN_SESSION_MAX_CTX, default 40000 tokens) so a session can hold a real + * conversation without the drop-oldest — while still bounding the pre-allocated + * KV reservation. A sliding_window, if set, still wins (a genuine ring). The + * hybrid (qwen35moe/bonsai/ornith) path has its own window knob + * (QWEN35_MAX_SEQ) and does not use this. */ +#define PN_SESSION_MAX_CTX_DEFAULT 40000 + +static int pn_session_max_seq(const pn_config *cfg) { + int cap = PN_SESSION_MAX_CTX_DEFAULT; + const char *e = getenv("PN_SESSION_MAX_CTX"); + if (e && e[0]) { + int v = atoi(e); + if (v > 0) + cap = v; + } + int max_seq = cfg->n_ctx > 0 ? cfg->n_ctx : cap; + if (cfg->sliding_window > 0 && cfg->sliding_window < max_seq) + max_seq = cfg->sliding_window; + /* `--ctx-cap` is the operator's per-model window and it MUST bound the + * reservation, not just the advertised number. Without this the dense KV + * ring was sized from the GGUF's own context_length whatever the cap said + * — a 1.2 GB model reserving ~6 GB of host RSS at --ctx-cap 4096, on an + * 8 GB box. The hybrid ring has always honoured it (qwen35moe_max_seq_cfg); + * this is the dense path catching up. */ + if (cfg->ctx_cap > 0 && cfg->ctx_cap < max_seq) + max_seq = cfg->ctx_cap; + if (max_seq > cap) + max_seq = cap; + return max_seq; +} + +/* ============================================================ + * Arena sizing + * ============================================================ */ + +static size_t compute_arena_size(const pn_config *cfg) { + /* Arena must hold ALL per-layer buffers accumulated across layers, + * since the arena only resets between forward calls. + * + * Per-layer allocations: + * normed: n_embd, q: n_head*hd, k: n_kv_head*hd, v: n_kv_head*hd, + * attn_out: n_head*hd, attn_proj: n_embd, ffn_normed: n_embd, + * gate: n_ff, up: n_ff, ffn_hidden: n_ff, ffn_out: n_embd + * = 5*n_embd + 3*n_ff + 2*n_kv_head*head_dim floats per layer + * + * One-time allocations: + * hidden: n_embd, scores_buf: n_head*max_seq, + * final_normed: n_embd, logits: n_vocab + */ + int max_seq = pn_session_max_seq(cfg); + int head_dim = pn_head_dim(cfg); + + size_t per_layer = (size_t)(5 * cfg->n_embd) + + (size_t)(3 * cfg->n_ff) + + (size_t)(2 * cfg->n_kv_head * head_dim); + + /* MoE layers need extra buffers: gate_logits + expert intermediates */ + if (cfg->n_experts > 0) { + per_layer += (size_t)cfg->n_experts /* gate_logits */ + + (size_t)(2 * cfg->n_experts_used) /* top_idx/top_wt (int->float) */ + + (size_t)cfg->n_embd /* ffn_out accumulator */ + + (size_t)cfg->n_ff /* exp_gate */ + + (size_t)cfg->n_ff /* exp_up */ + + (size_t)cfg->n_ff /* exp_hid */ + + (size_t)cfg->n_embd; /* exp_out */ + } + + size_t one_time = (size_t)cfg->n_embd /* hidden */ + + (size_t)(cfg->n_head * max_seq) /* scores_buf */ + + (size_t)cfg->n_embd /* final_normed */ + + (size_t)cfg->n_vocab; /* logits */ + + /* Alignment overhead: 16 bytes per allocation */ + size_t n_allocs = (size_t)cfg->n_layer * 11 + 5; + if (cfg->n_experts > 0) n_allocs += (size_t)cfg->n_layer * 5; + size_t align_overhead = n_allocs * 16; + + size_t total_floats = per_layer * (size_t)cfg->n_layer + one_time; + size_t total = total_floats * sizeof(float) + align_overhead; + /* Add 25% safety margin for alignment rounding accumulation */ + return total + total / 4; +} + +/* ============================================================ + * Model loading + * ============================================================ */ + +pn_model* pn_load(const char *path) { + return pn_load_ex(path, NULL); +} + +/* ============================================================ + * Pre-flight admission control (MemoryAware §6, §11 Q7) + * + * Engine-level so the eval path (eval_ppl) — which crash-looped on the 32k + * OOM-reboot — is covered as well as the server. Reads the published + * pn_mem_state (host RAM + PSI + per-die VRAM) and the measured cost LUT; + * refuses/caps before any memory-growing work starts. Fail-open: if the + * memory-watch loop is not running there is no truth to check against, so it + * returns FIT (no behavior change). + * ============================================================ */ + +/* Is q4 KV in effect? B1: default ON; mirrors pn_qwen35moe_kv_q4_on() — + * ON unless QWEN35_KV_Q4=0. */ +static bool pn_admit_kv_q4(void) { + const char *e = getenv("QWEN35_KV_Q4"); + return !(e && e[0] == '0'); +} + +/* Path-aware cost model (DECIDED 2026-07-09): is this model GPU-resident — + * weights uploaded, the CUDA forward selected? Then the per-request prefill + * host cost is ~0 (the O(N^2) scores live on-die; measured on a K80 host) and admission + * admits the full context up to max_seq. Anything else is the CPU-forward / + * host-fallback path that the conservative LUT models. + * + * Recognizes ALL three CUDA forwards — the MoE forward AND the two dense + * forwards (bonsai/Ornith and plain-Llama), which are equally GPU-resident. + * Gating on the MoE pointer alone mislabeled Ornith path=cpu and charged it a + * phantom host_need from the CPU-fallback LUT, tripping the ladder into + * throttling a GPU-resident model (the 429 storm / idle-die split). */ +static bool pn_admit_gpu_resident(const pn_model *model) { +#ifdef USE_CUDA + if (model && (model->forward == pn_qwen35moe_forward_cuda || + model->forward == pn_bonsai_forward_cuda || + model->forward == pn_llama_forward_cuda)) + return true; +#endif +#ifdef USE_TT + /* The Tenstorrent forward is equally accelerator-resident: its weights live in the + * card's GDDR6 and never come back. Leaving it out here is the same defect the comment + * above records for Ornith — a resident model charged a phantom host_need from the + * CPU-fallback LUT, and then throttled. */ + if (model && (model->forward == pn_llama_forward_tt || + model->forward == pn_qwen35moe_forward_tt)) + return true; +#endif + (void)model; + return false; +} + +/* The per-session context window admission promises (the per-session hard cap). + * It MUST equal the window the model's KV ring is actually sized to, or admission + * either over-promises (admit a prompt the ring can't hold) or under-promises + * (reject one it could). Resolution, in order: + * 1. a per-model ctx_cap (set at load) — the KV ring is sized to exactly this + * (ornith's 40K), so admission promises exactly this. Wins over everything. + * 2. QWEN35_MAX_SEQ env — the hybrid ring's global override. + * 3. else: the HYBRID (qwen35moe/bonsai) ring defaults to PN_QWEN35MOE_MAX_SEQ + * (the ring's own default), so admission must match it — NOT the generic + * pn_session_max_seq, which governs the DENSE llama KV cache only. This is + * what keeps saber (no ctx_cap, unset env) reporting its true 4096 window + * instead of a phantom 40K the ring can't hold. Always clamped to the ring + * ceiling + trained ctx. + * A DENSE model short-circuits all of that and returns pn_session_max_seq() + * verbatim, because on the dense path that function *is* the ring: pn_state_new + * allocates the KV cache with the very same call. Same function, one number, + * no drift possible. */ +static int pn_admit_max_window(const pn_model *model) { + int cap = PN_QWEN35MOE_MAX_SEQ_CAP; + bool hybrid = model && (model->config.arch == PN_ARCH_QWEN35MOE || + model->config.arch == PN_ARCH_BONSAI); + /* Dense arches: pn_session_max_seq IS the ring (pn_state_new sizes the KV + * cache with it, and it now honours ctx_cap), so derive the promise from + * the identical call rather than re-deriving it. Re-deriving is how the + * promise could exceed the ring — a ctx_cap above PN_SESSION_MAX_CTX_DEFAULT + * was advertised in full while the ring stopped at 40000. */ + if (model && !hybrid) + return pn_session_max_seq(&model->config); + int w; + if (model && model->config.ctx_cap > 0) { + w = model->config.ctx_cap; + } else { + const char *e = getenv("QWEN35_MAX_SEQ"); + if (e && atoi(e) > 0) + w = atoi(e); + else if (hybrid) + w = PN_QWEN35MOE_MAX_SEQ; /* match the ring's own default */ + else + w = model ? pn_session_max_seq(&model->config) + : PN_SESSION_MAX_CTX_DEFAULT; + } + if (w <= 0) w = PN_SESSION_MAX_CTX_DEFAULT; + if (w > cap) w = cap; + /* Never promise more than the model's own trained context. */ + if (model && model->config.n_ctx > 0 && w > model->config.n_ctx) + w = model->config.n_ctx; + return w; +} + +int pn_session_ctx_cap(const pn_model *model) { + return pn_admit_max_window(model); +} + +/* One prompt through the TT card's BATCHED prefill, or a refusal. Tokens already in the + * card's own `uint32_t` form. + * + * The card multiplies 32 rows against every weight tile it reads whatever we put in them + * (a matvec is Mt=1 and the other 31 rows are the tile quantum's zero padding), so a + * 32-position batch costs the card exactly what one position costs. pn_llama_prefill_tt + * returns NULL WITHOUT having touched the state when it refuses, and the per-token loop at + * the call site is what runs then — so this is a pure accelerator, never a behaviour fork. + * + * EVERY PREFILL SITE IN THE PROCESS MUST COME THROUGH HERE. Measured 2026-08-26: the + * chunkwise Ornith prefill shipped at 24x and reached exactly ONE caller, because + * pn_stream_tokens() — which is what the CLI takes for any prompt that is NOT `--raw`, and + * what the chat REPL takes — had a per-token loop of its own and never asked. A 1,630-token + * prompt ran 1,632 forwards in 295 s with nothing on stderr, and the shipped rung was + * invisible rather than broken. + * + * Returns the LAST prompt position's logits (caller owns), or NULL to mean "not taken". */ +static tm_tensor *pn_prefill_tt_batched_tokens(pn_model *model, pn_state *state, + const uint32_t *tokens, int n_tokens) +{ +#ifdef USE_TT + if (!model || !tokens || n_tokens < 2) + return NULL; + /* TWO FAMILIES, TWO BATCHED PREFILLS, and they are not the same code because they are + * not the same trade. The llama one refuses on-card attention and batches everything + * position-dependent on the host; Ornith's mixers both run WHOLE on the card and are + * single-position there, so its batch reaches each row through the link. */ + if (model->forward == pn_llama_forward_tt) + return pn_llama_prefill_tt(model, tokens, NULL, n_tokens, state); + if (model->forward == pn_qwen35moe_forward_tt) + return pn_qwen35moe_prefill_tt(model, tokens, n_tokens, state); + return NULL; +#else + (void)model; (void)state; (void)tokens; (void)n_tokens; + return NULL; +#endif +} + +/* The same, for the tokenizer's signed ids. Negative ids are dropped, which is what the + * per-token loops at every call site do. */ +static tm_tensor *pn_prefill_tt_batched(pn_model *model, pn_state *state, + const int *prompt_tokens, int n_prompt_tokens) +{ +#ifdef USE_TT + uint32_t *toks; + tm_tensor *logits; + int n = 0; + + if (!model || !prompt_tokens || n_prompt_tokens < 2) + return NULL; + if (model->forward != pn_llama_forward_tt && model->forward != pn_qwen35moe_forward_tt) + return NULL; + toks = malloc((size_t)n_prompt_tokens * sizeof(*toks)); + if (!toks) + return NULL; + for (int i = 0; i < n_prompt_tokens; i++) + if (prompt_tokens[i] >= 0) + toks[n++] = (uint32_t)prompt_tokens[i]; + logits = pn_prefill_tt_batched_tokens(model, state, toks, n); + free(toks); + return logits; +#else + (void)model; (void)state; (void)prompt_tokens; (void)n_prompt_tokens; + return NULL; +#endif +} + +/* Apply the reactive ladder's rung-3 prefill clamp to a configured window. + * PURE: no logging, no state — pn_admit logs the transitions, every other + * caller (the /v1/models advertisement, the admin surface, the kv_cap a + * completion reports) just wants the number. Keeping the clamp in ONE place is + * what stops the advertised window from drifting away from the window + * admission actually enforces. + * + * The GPU-resident exemption is Phase 10 policy — see the long comment in + * pn_admit. */ +static int pn_apply_window_clamp(const pn_mem_state *st, int configured, + bool gpu_resident, bool kv_q4, + pn_ladder_plan *ladder_out) { + pn_ladder_plan ladder; + pn_ladder_plan_from_state(st, 1, configured, kv_q4, &ladder); + if (ladder_out) + *ladder_out = ladder; + if (!gpu_resident && ladder.prefill_window > 0 && + ladder.prefill_window < configured) + return ladder.prefill_window; + return configured; +} + +int pn_effective_ctx_window(const pn_model *model) { + int configured = pn_admit_max_window(model); + /* No memory truth to read ⇒ nothing can be clamping, so the configured + * window IS the effective one (same fail-open as pn_admit). */ + if (!pn_memwatch_running()) + return configured; + pn_mem_state st; + pn_memwatch_read(&st); + return pn_apply_window_clamp(&st, configured, pn_admit_gpu_resident(model), + pn_admit_kv_q4(), NULL); +} + +int pn_model_graph_residency_read(const pn_model *model, + pn_model_graph_residency *out) { + if (!out) + return -1; + memset(out, 0, sizeof(*out)); +#ifdef USE_CUDA + /* Only the hybrid qwen35 family captures graphs today. Bonsai shares that + * forward wholesale (bonsai_forward.c delegates), so it shares the gauge. */ + if (model && (model->config.arch == PN_ARCH_QWEN35MOE || + model->config.arch == PN_ARCH_BONSAI) && model->weights) + return pn_qwen35moe_gpu_graph_residency( + (const pn_qwen35moe_model *)model->weights, out); +#else + (void)model; +#endif + return -1; +} + +int pn_model_attn_residency_read(const pn_model *model, + pn_model_attn_residency *out) { + if (!out) + return -1; + memset(out, 0, sizeof(*out)); +#ifdef USE_CUDA + /* Same family as the graph gauge, for the same reason: the on-die + * attention path lives in the hybrid qwen35 forward, which bonsai shares + * wholesale. */ + if (model && (model->config.arch == PN_ARCH_QWEN35MOE || + model->config.arch == PN_ARCH_BONSAI) && model->weights) + return pn_qwen35moe_gpu_attn_residency( + (const pn_qwen35moe_model *)model->weights, out); +#else + (void)model; +#endif + return -1; +} + +/* Tightest reporting die (worst free VRAM headroom) — the conservative die to + * schedule against for the single-model server without threading a die id + * through the CUDA weight structs. Returns -1 if no die reports (CPU/host). */ +static int pn_admit_worst_die(const pn_mem_state *st) { + int worst = -1; + uint64_t worst_free = 0; + for (int i = 0; i < st->n_dies && i < PN_MEM_MAX_DIES; i++) { + if (st->per_die_budget[i] == 0) + continue; + if (worst < 0 || st->per_die_free[i] < worst_free) { + worst = i; + worst_free = st->per_die_free[i]; + } + } + return worst; +} + +/* ============================================================ + * Effective-context-window observability (Phase 10) + * + * Before this, nothing reported the window a model was actually serving: the + * ladder logged only when it clamped, never when it released, and no status + * surface exposed the number. A serve could silently halve a 131072-token + * window and neither the operator nor the client could tell. That made the + * clamp bug invisible AND unverifiable. + * + * Fix: one greppable `[window]` line per model whenever its effective window + * CHANGES — in both directions — plus one on the first admission so the + * steady-state number is always in the log. Change-only, so a healthy serve + * emits exactly one line per model for the life of the process. + * + * The same number is now also ADVERTISED: pn_effective_ctx_window feeds + * `context_window` on GET /v1/models and GET /admin/models, and it is what a + * completion reports as `kv_cap`. The log stays because it is the only surface + * that shows the TRANSITIONS (clamp/restore); the endpoint shows the current + * value. + * ============================================================ */ + +#define PN_WINDOW_LOG_SLOTS 16 + +static struct { + const pn_model *model; + int window; /* last-logged effective window, 0 = free slot */ +} g_window_log[PN_WINDOW_LOG_SLOTS]; +static pthread_mutex_t g_window_log_lock = PTHREAD_MUTEX_INITIALIZER; + +/* Short, stable, human-greppable tag for a model: its GGUF basename plus its + * die placement. The dies are what distinguish two instances of the SAME model + * file (e.g. nex-n2-mini run twice, on dies 2,3 and 4,5). */ +static void pn_model_tag(const pn_model *model, char *buf, size_t n) +{ + const char *base = "model"; + if (model && model->path) { + const char *slash = strrchr(model->path, '/'); + base = slash ? slash + 1 : model->path; + } + + char dies[64]; + dies[0] = '\0'; + if (model && model->n_gpu_dies > 0) { + size_t off = 0; + off += (size_t)snprintf(dies + off, sizeof(dies) - off, " dies="); + for (int i = 0; i < model->n_gpu_dies && off < sizeof(dies) - 1; i++) + off += (size_t)snprintf(dies + off, sizeof(dies) - off, "%s%d", + i ? "," : "", model->gpu_dies[i]); + } + snprintf(buf, n, "%s%s", base, dies); +} + +static void pn_log_effective_window(const pn_model *model, int configured, + int effective, pn_mem_level level, + const char *note) +{ + if (!model || effective <= 0) + return; + + pthread_mutex_lock(&g_window_log_lock); + + int slot = -1, free_slot = -1; + for (int i = 0; i < PN_WINDOW_LOG_SLOTS; i++) { + if (g_window_log[i].model == model) { slot = i; break; } + if (!g_window_log[i].model && free_slot < 0) free_slot = i; + } + + int prev = 0; + if (slot < 0) { + slot = free_slot; + if (slot < 0) { /* table full — log every time */ + pthread_mutex_unlock(&g_window_log_lock); + return; + } + g_window_log[slot].model = model; + } else { + prev = g_window_log[slot].window; + if (prev == effective) { /* unchanged — stay quiet */ + pthread_mutex_unlock(&g_window_log_lock); + return; + } + } + g_window_log[slot].window = effective; + + pthread_mutex_unlock(&g_window_log_lock); + + char tag[256]; + pn_model_tag(model, tag, sizeof(tag)); + + if (prev == 0) + fprintf(stderr, "[window] %s: effective context %d (configured %d) " + "[mem %s]\n", + tag, effective, configured, pn_mem_level_name(level)); + else if (effective < prev) + fprintf(stderr, "[window] %s: effective context CLAMPED %d -> %d " + "(configured %d) [mem %s: %s]\n", + tag, prev, effective, configured, pn_mem_level_name(level), + note ? note : ""); + else + fprintf(stderr, "[window] %s: effective context RESTORED %d -> %d " + "(configured %d) [mem %s]\n", + tag, prev, effective, configured, pn_mem_level_name(level)); +} + +pn_admit_status pn_admit(pn_model *model, int n_tokens, int *fit_tokens_out) { + if (fit_tokens_out) + *fit_tokens_out = n_tokens; + + /* Fail-open when there is no memory truth to check against. */ + if (!pn_memwatch_running() || n_tokens <= 0) + return PN_ADMIT_FIT; + + pn_mem_state st; + pn_memwatch_read(&st); + + bool kv_q4 = pn_admit_kv_q4(); + bool gpu_resident = pn_admit_gpu_resident(model); + int die_id = pn_admit_worst_die(&st); + int max_window = pn_admit_max_window(model); + int configured_window = max_window; /* pre-ladder, for the [window] log */ + /* Price against THIS model's real KV geometry, not a generic constant — + * the KV Phase 1 correctness fix (pn_kv_geometry.h). 0 falls back to the + * conservative constant inside admission. */ + uint64_t kv_per_token = pn_kv_bytes_per_token(model, kv_q4); + + /* Rung 3 of the reactive ladder (MemoryAware.md §7): under memory pressure, + * clamp the admitted prefill window below the configured max so an + * over-window prompt is CAPPED proactively. Only the plan's prefill_window + * is consumed here (slots/q4 apply elsewhere), so the concurrency arg is + * nominal. + * + * POLICY (Phase 10, DECIDED 2026-07-27) — the clamp does NOT apply to a + * GPU-resident model. Two independent reasons: + * + * 1. It buys nothing. The rung exists to bound the TRANSIENT HOST + * activation peak of a long prefill — pn_admission's O(N^2) LUT. On the + * GPU path that peak measured on a K80 host is ~0 (the scores live on-die in + * bounded flash tiles) and pn_admit_request hard-sets host_need_bytes + * = 0. Every clamp ever observed on the K80 test machine sat next to + * `host_need=0 MiB`. Halving the window frees ZERO host RAM. + * + * 2. The harm lands on a LIVE session. The window is re-evaluated on every + * turn of an existing conversation, and serve turns a CAP into a hard + * SG_CONTEXT_OVER_BUDGET rejection. So a transient pressure blip does + * not degrade a live session gracefully — it fails its next turn + * outright, against a cap the operator never configured. Gating NEW + * work is admission's job and admission still does it (the PSI stall + * guard and the reserve-line check below both still reject); silently + * re-pricing an in-flight session's context is not a memory lever, it + * is a correctness-of-service bug. + * + * On the host/CPU-fallback path the clamp is kept as-is: those prefills + * really do allocate lut_activation_peak(N) host bytes, and that is the + * path that OOM-rebooted the box. */ + pn_ladder_plan ladder; + int clamped = pn_apply_window_clamp(&st, max_window, gpu_resident, kv_q4, + &ladder); + if (clamped < max_window) { + fprintf(stderr, "[ladder] %s -> prefill window clamped %d -> %d (%s)\n", + pn_mem_level_name(st.level), max_window, clamped, ladder.note); + max_window = clamped; + } + + /* Observability (Phase 10): the window a model is ACTUALLY serving must be + * greppable from the serve log, in both directions, so an operator can tell + * a configured window from a clamped one. Logs on change only. */ + pn_log_effective_window(model, configured_window, max_window, st.level, + ladder.note); + + pn_admit_result res; + pn_admit_request(n_tokens, kv_q4, kv_per_token, gpu_resident, die_id, + max_window, &st, &res); + + if (res.verdict == PN_ADMIT_CAP && fit_tokens_out) + *fit_tokens_out = res.fit_tokens; + + fprintf(stderr, + "[admit] n=%d path=%s -> %s (%s) host_need=%llu MiB " + "host_avail=%llu MiB\n", + n_tokens, gpu_resident ? "gpu" : "cpu", + pn_admit_verdict_name(res.verdict), res.reason, + (unsigned long long)(res.host_need_bytes >> 20), + (unsigned long long)(res.host_avail_bytes >> 20)); + + switch (res.verdict) { + case PN_ADMIT_OK: return PN_ADMIT_FIT; + case PN_ADMIT_CAP: return PN_ADMIT_CAPPED; + case PN_ADMIT_REJECT: return PN_ADMIT_DENIED; + } + return PN_ADMIT_FIT; +} + +/* Record an explicit die placement on a freshly-allocated model (pn_load_on). + * Bookkeeping on every path; the GPU forwards read it where they support it. */ +static void model_record_dies(pn_model *model, const int *dies, int n_dies) +{ + if (!model || !dies || n_dies <= 0) + return; + if (n_dies > PN_MODEL_MAX_DIES) { + /* Truncating would record a placement NARROWER than the one the caller + * asked for and the die manager bound — the accelerator would then + * split fewer ways than it holds dies. Refuse the placement instead; + * the model still loads, with no placement recorded, and the load gate + * prices it as such. */ + fprintf(stderr, "pn_load_on: REFUSE placement on %d dies — the max is " + "%d (PN_MODEL_MAX_DIES); ignoring the placement rather than " + "silently narrowing it\n", n_dies, PN_MODEL_MAX_DIES); + return; + } + for (int i = 0; i < n_dies; i++) + model->gpu_dies[i] = dies[i]; + model->n_gpu_dies = n_dies; +} + +#ifdef USE_CUDA +/* The die this model's process-default CUDA context belongs on. fc_init_on() + * is first-call-wins, so this must be the placed die and not ordinal 0 — + * otherwise a serve pinned to `--dies 2,3` still builds a full context on + * die 0, which another serve owns. Unplaced models keep the legacy default. */ +static int model_primary_die(const pn_model *model) +{ + return (model && model->n_gpu_dies > 0) ? model->gpu_dies[0] : 0; +} +#endif + +pn_model* pn_load_ex(const char *path, const pn_load_opts *opts) { + return pn_load_on(path, opts, NULL, 0); +} + +pn_model* pn_load_on(const char *path, const pn_load_opts *opts, + const int *dies, int n_dies) { + if (!path) { + fprintf(stderr, "pn_load: path is NULL\n"); + return NULL; + } + + /* ---- Safetensors path ---- */ + if (pn_is_safetensors(path)) { + const char *config_path = opts ? opts->config_path : NULL; + + /* If config.json provided, use HF/ternary safetensors loader */ + if (config_path) { + pn_model *model = calloc(1, sizeof(pn_model)); + if (!model) return NULL; + model->path = strdup(path); + model_record_dies(model, dies, n_dies); + + /* Load weights via HF safetensors loader */ + pn_llama_qweights *qw = pn_llama_load_safetensors(path, config_path, &model->config); + if (!qw) { + free(model->path); + free(model); + return NULL; + } + model->weights = qw; + model->forward = pn_llama_forward; + + /* Load tokenizer from directory containing config.json */ + char tok_dir[1024]; + strncpy(tok_dir, config_path, sizeof(tok_dir) - 1); + tok_dir[sizeof(tok_dir) - 1] = '\0'; + char *last_slash = strrchr(tok_dir, '/'); + if (last_slash) *last_slash = '\0'; + else strcpy(tok_dir, "."); + + model->tokenizer = pn_llama_load_hf_tokenizer(tok_dir, &model->config); + if (!model->tokenizer) { + fprintf(stderr, "engine: warning: no tokenizer loaded (need vocab.json+merges.txt)\n"); + } + + model->chat_style = pn_chat_style_from_arch(model->config.arch); + +#ifdef USE_THREADS + model->pool = fw_pool_new(0); + if (model->pool) { + fprintf(stderr, "engine: thread pool created\n"); + } +#endif + + const char *fmt = (qw->layers && qw->layers[0].wq.type == PN_QUANT_TERNARY) + ? "ternary" : "F32"; + fprintf(stderr, "engine: loaded %s safetensors (%d layers, %d embd, %s weights)\n", + model->config.arch == PN_ARCH_QWEN2 ? "Qwen2" : "LLaMA", + model->config.n_layer, model->config.n_embd, fmt); + + return model; + } + + /* RWKV-X safetensors path (no config.json) */ + bool quantize = opts ? opts->quantize : false; + const char *vocab = opts ? opts->vocab_path : NULL; + + pn_model *model = calloc(1, sizeof(pn_model)); + if (!model) return NULL; + model->path = strdup(path); + model_record_dies(model, dies, n_dies); + model->config.arch = PN_ARCH_RWKVX; + + model->weights = pn_rwkvx_load(path, &model->config, quantize); + if (!model->weights) { + free(model->path); + free(model); + return NULL; + } + model->forward = pn_rwkvx_forward; + model->tokenizer = NULL; /* RWKV-X uses internal trie tokenizer */ + + if (vocab) { + if (pn_rwkvx_load_tokenizer(model->weights, vocab) != 0) { + fprintf(stderr, "engine: failed to load RWKV vocab: %s\n", vocab); + pn_rwkvx_free(model->weights); + free(model->path); + free(model); + return NULL; + } + } + + model->chat_style = PN_CHAT_NONE; + +#ifdef USE_THREADS + model->pool = fw_pool_new(0); + if (model->pool) { + fprintf(stderr, "engine: thread pool created\n"); + } +#endif + + const char *rwkv_type = pn_rwkvx_n_attn_layers(model->weights) > 0 ? "RWKV-X" : "RWKV-7"; + fprintf(stderr, "engine: loaded %s (%d layers, %d embd)\n", + rwkv_type, model->config.n_layer, model->config.n_embd); + + return model; + } + + /* ---- GGUF path (Llama, Phi, Qwen, Mistral, Mixtral) ---- */ + if (!pn_is_gguf(path)) { + fprintf(stderr, "pn_load: unrecognized file format: %s\n", path); + return NULL; + } + + pn_gguf_file *gguf = pn_gguf_open(path); + if (!gguf) return NULL; + + pn_arch arch = detect_arch_from_gguf(gguf); + if (arch == PN_ARCH_UNKNOWN) { + fprintf(stderr, "engine: no architecture detected, trying llama\n"); + arch = PN_ARCH_LLAMA; + } + + pn_model *model = calloc(1, sizeof(pn_model)); + if (!model) { + pn_gguf_close(gguf); + return NULL; + } + + model->path = strdup(path); + model_record_dies(model, dies, n_dies); + model->config.arch = arch; + /* KV Phase 1: per-model context cap must be in the config BEFORE the arch + * loader sizes the KV ring, so ornith can run a 40K window without moving + * saber's. The arch loaders copy it into their own config. */ + if (opts && opts->ctx_cap > 0) + model->config.ctx_cap = opts->ctx_cap; + + if (arch == PN_ARCH_PHI) { + /* Phi-3: fused QKV + fused gate+up layout */ + pn_phi3_qweights *qw = pn_phi3_load_gguf_quantized(gguf, &model->config); + if (qw) { + model->weights = qw; + model->forward = pn_phi3_forward; + fprintf(stderr, "engine: using phi3 quantized weights (zero-copy mmap)\n"); + } else { + pn_model_free(model); + pn_gguf_close(gguf); + return NULL; + } + } else if (arch == PN_ARCH_QWEN35MOE) { + /* Qwen3.5-MoE: hybrid SSM + full-attention + shared-expert MoE */ + pn_qwen35moe_model *qw = pn_qwen35moe_load(gguf, &model->config); + if (qw) { + model->weights = qw; + model->forward = pn_qwen35moe_forward; + fprintf(stderr, "engine: using qwen35moe weights (zero-copy mmap)\n"); + /* Explicit placement (pn_load_on) must reach the GPU upload's + * die selection — stored on the arch model before the upload. */ + if (model->n_gpu_dies > 0) + pn_qwen35moe_model_set_dies(qw, model->gpu_dies, + model->n_gpu_dies); +#ifdef USE_CUDA + /* Single-die GPU path: upload weights to persistent VRAM and select + * the CUDA forward when a GPU is present. Falls back to the CPU + * forward when there is no GPU (the Mac case) or the upload fails. + * fc_init_on() is idempotent: it dlopens libcuda + inits CUDA on the + * ACQUIRED die (not ordinal 0); a no-op (leaving fc_available() + * false) without a GPU. */ + fc_init_on(model_primary_die(model)); + if (fc_available()) { + if (pn_qwen35moe_gpu_upload(qw) == 0 && + pn_qwen35moe_model_gpu_loaded(qw)) { + model->forward = pn_qwen35moe_forward_cuda; + fprintf(stderr, "engine: qwen35moe on GPU (single die)\n"); + } else { + fprintf(stderr, "engine: qwen35moe GPU upload skipped/" + "failed; using CPU forward\n"); + } + } +#endif +#ifdef USE_METAL + /* Apple GPU path, mirroring the CUDA block above. fm_init() is + * idempotent and returns non-zero on a machine with no Metal + * device, so this whole block collapses to "keep the CPU forward" + * off-Mac. No MTLResidencySet is created — deliberate, see + * foundry/metal/qwen35moe_metal_spec.md §2. + * + * PHASE 0: the selected forward has no kernels yet; it reports + * NOT-IMPLEMENTED per token and delegates to the CPU forward, so + * selecting it is output-neutral. */ + if (fm_init() == 0 && fm_available() && + pn_qwen35moe_metal_upload(qw) == 0) { + model->forward = pn_qwen35moe_forward_metal; + fprintf(stderr, "engine: qwen35moe Metal backend selected " + "(Phase 0 scaffold — compute still on CPU)\n"); + } else { + fprintf(stderr, "engine: qwen35moe Metal upload skipped/" + "failed; using CPU forward\n"); + } +#endif + } else { + pn_model_free(model); + pn_gguf_close(gguf); + return NULL; + } + } else if (arch == PN_ARCH_BONSAI) { + /* PrismML Bonsai-27B: qwen35 hybrid family, dense SwiGLU FFN + native + * ternary. Own arch + own forward entry (bonsai_forward.c), reusing the + * shared qwen35moe loader/upload/lifecycle. */ + void *bw = pn_bonsai_load(gguf, &model->config); + if (bw) { + model->weights = bw; + model->forward = pn_bonsai_forward; + fprintf(stderr, "engine: using bonsai (dense-FFN ternary) weights (zero-copy mmap)\n"); + if (model->n_gpu_dies > 0) + pn_bonsai_model_set_dies(bw, model->gpu_dies, model->n_gpu_dies); +#ifdef USE_CUDA + fc_init_on(model_primary_die(model)); + if (fc_available()) { + if (pn_bonsai_gpu_upload(bw) == 0 && + pn_bonsai_model_gpu_loaded(bw)) { + model->forward = pn_bonsai_forward_cuda; + fprintf(stderr, "engine: bonsai on GPU (single die)\n"); + } else { + fprintf(stderr, "engine: bonsai GPU upload skipped/" + "failed; using CPU forward\n"); + } + } +#endif +#ifdef USE_TT + /* Tenstorrent: only when no other accelerator already claimed this model — + * one model, one residency, the same rule the llama branch below states. */ + if (model->forward == pn_bonsai_forward) { + if (pn_qwen35moe_tt_upload(model) == 0 && + pn_qwen35moe_model_tt_loaded(model)) { + model->forward = pn_qwen35moe_forward_tt; + fprintf(stderr, "engine: qwen35 (dense) on Tenstorrent " + "(matmuls on card)\n"); + } else { + fprintf(stderr, "engine: qwen35 Tenstorrent upload skipped/" + "failed; using CPU forward\n"); + } + } +#endif + } else { + pn_model_free(model); + pn_gguf_close(gguf); + return NULL; + } + } else if (arch == PN_ARCH_LLAMA || arch == PN_ARCH_QWEN2 || arch == PN_ARCH_QWEN3 || arch == PN_ARCH_MISTRAL || arch == PN_ARCH_MIXTRAL) { + /* Try quantized loader first */ + pn_llama_qweights *qw = pn_llama_load_gguf_quantized(gguf, &model->config); + if (qw) { + model->weights = qw; + model->forward = pn_llama_forward; + fprintf(stderr, "engine: using quantized weights (zero-copy mmap)\n"); +#ifdef USE_CUDA + /* Plain-Llama GPU forward: upload the (non-MoE) weights to a single + * die and select the CUDA forward when a GPU is present. Falls back + * to the CPU forward (the oracle) without a GPU or on upload + * failure. fc_init_on() is idempotent and binds the ACQUIRED die. */ + fc_init_on(model_primary_die(model)); + if (fc_available()) { + if (pn_llama_gpu_upload(model) == 0 && + pn_llama_model_gpu_loaded(model)) { + model->forward = pn_llama_forward_cuda; + fprintf(stderr, "engine: llama on GPU (single die)\n"); + } else { + fprintf(stderr, "engine: llama GPU upload skipped/" + "failed; using CPU forward\n"); + } + } +#endif +#ifdef USE_TT + /* Tenstorrent: only when no other accelerator already claimed this model. + * The residency handle is one slot (engine.h), and a model has one residency — + * so a build carrying both families binds whichever family answered first + * rather than uploading twice and leaking one. */ + if (model->forward == pn_llama_forward) { + if (pn_llama_tt_upload(model) == 0 && pn_llama_model_tt_loaded(model)) { + model->forward = pn_llama_forward_tt; + fprintf(stderr, "engine: llama on Tenstorrent (projections on card)\n"); + } else { + fprintf(stderr, "engine: llama Tenstorrent upload skipped/" + "failed; using CPU forward\n"); + } + } +#endif + } else { + /* Fallback to F32 dequant loader */ + pn_llama_weights *w = pn_llama_load_gguf(gguf, &model->config); + if (!w) { + pn_model_free(model); + pn_gguf_close(gguf); + return NULL; + } + model->weights = w; + model->forward = pn_llama_forward_f32; + } + } else { + fprintf(stderr, "pn_load: architecture not yet supported\n"); + pn_model_free(model); + pn_gguf_close(gguf); + return NULL; + } + + model->tokenizer = build_tokenizer_from_gguf(gguf); + if (!model->tokenizer) { + fprintf(stderr, "pn_load: failed to build tokenizer\n"); + pn_model_free(model); + pn_gguf_close(gguf); + return NULL; + } + + /* Detect chat template style */ + const char *chat_tmpl = pn_gguf_get_str(gguf, "tokenizer.chat_template"); + if (chat_tmpl) + model->chat_style = pn_chat_detect_style(chat_tmpl); + else + model->chat_style = pn_chat_style_from_arch(arch); + /* Some templates (e.g. MiniCPM5) begin with {{ bos_token }} — the model was + * trained with BOS at position 0 and degenerates (whitespace) without it. + * ChatML/Qwen/Nex templates do NOT lead with bos_token. Flag it when the + * template emits bos_token before the first <|im_start|> role marker so the + * prompt builder can prepend BOS for exactly those models. */ + if (chat_tmpl) { + const char *b = strstr(chat_tmpl, "bos_token"); + const char *im = strstr(chat_tmpl, "<|im_start|>"); + model->chat_add_bos = (b && (!im || b < im)) ? 1 : 0; + } + fprintf(stderr, "engine: chat style: %s%s\n", + pn_chat_style_name(model->chat_style), + model->chat_add_bos ? " +bos" : ""); + +#ifdef USE_THREADS + { + const char *nt = getenv("PN_THREADS"); + model->pool = fw_pool_new(nt ? atoi(nt) : 0); + } + if (model->pool) { + fprintf(stderr, "engine: thread pool created\n"); + pn_qmv_set_pool(model->pool); + } +#endif + + const char *name = pn_gguf_get_str(gguf, "general.name"); + fprintf(stderr, "engine: loaded %s (%d layers, %d params roughly)\n", + name ? name : path, + model->config.n_layer, + model->config.n_vocab * model->config.n_embd); + + return model; +} + +int pn_model_keepalive(pn_model *model) +{ +#ifdef USE_TT + return pn_qwen35moe_keepalive_tt(model); +#else + (void)model; + return 0; +#endif +} + +int pn_model_warm(pn_model *model) +{ +#ifdef USE_TT + return pn_qwen35moe_warm_tt(model); +#else + (void)model; + return 0; +#endif +} + +void pn_model_free(pn_model *model) { + if (!model) return; + + if (model->vision) pn_vision_free((pn_vision_model *)model->vision); + +#ifdef USE_TT + /* Tenstorrent residency shares the `gpu` slot, so it is claimed FIRST and by the + * forward pointer — the one thing that says which family put a handle there. It NULLs + * the slot, which is what keeps the CUDA free below from freeing a TT handle in a + * build that carries both. */ + if (model->gpu && model->forward == pn_llama_forward_tt) pn_llama_tt_free(model); + /* The qwen35 hybrid keeps its handle INSIDE its own weight struct, like the CUDA arms + * of that family do, so it is not in the `gpu` slot and is claimed by its own forward + * pointer. It must run BEFORE the weights are freed below: the residency holds pointers + * to the very `pn_qweight`s it is keyed on. */ + if (model->forward == pn_qwen35moe_forward_tt) pn_qwen35moe_tt_free(model); +#endif +#ifdef USE_CUDA + /* Plain-Llama GPU residency (pn_llama_gpu_upload); other arches keep their + * GPU handle inside their own weight struct and free it there. */ + if (model->gpu) pn_llama_gpu_free(model); +#endif + + if (model->weights) { + if (model->config.arch == PN_ARCH_RWKVX) { + pn_rwkvx_free(model->weights); + } else if (model->config.arch == PN_ARCH_QWEN35MOE) { + pn_qwen35moe_model *qw = (pn_qwen35moe_model *)model->weights; + pn_gguf_file *gguf = pn_qwen35moe_gguf(qw); + pn_qwen35moe_free(qw); + if (gguf) pn_gguf_close(gguf); + } else if (model->config.arch == PN_ARCH_BONSAI) { + void *bw = model->weights; + pn_gguf_file *gguf = pn_bonsai_gguf(bw); + pn_bonsai_free(bw); + if (gguf) pn_gguf_close(gguf); + } else if (model->config.arch == PN_ARCH_PHI) { + pn_phi3_qweights *qw = (pn_phi3_qweights *)model->weights; + pn_gguf_file *gguf = qw->gguf; + pn_phi3_qweights_free(qw); + if (gguf) pn_gguf_close(gguf); + } else if (model->config.arch == PN_ARCH_LLAMA || model->config.arch == PN_ARCH_QWEN2 || model->config.arch == PN_ARCH_QWEN3 || model->config.arch == PN_ARCH_MISTRAL || model->config.arch == PN_ARCH_MIXTRAL) { + /* Check if it's quantized or F32 weights by checking the tag */ + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + if (qw->is_quantized) { + pn_gguf_file *gguf = qw->gguf; + pn_llama_qweights_free(qw); + if (gguf) pn_gguf_close(gguf); + } else { + pn_llama_weights *w = (pn_llama_weights *)model->weights; + pn_gguf_file *gguf = w->gguf; + pn_llama_weights_free(w); + if (gguf) pn_gguf_close(gguf); + } + } + } + + if (model->tokenizer) ft_free(model->tokenizer); +#ifdef USE_THREADS + if (model->pool) { + pn_qmv_set_pool(NULL); + fw_pool_free(model->pool); + } +#endif + free(model->path); + free(model); +} + +/* ============================================================ + * State management + * ============================================================ */ + +pn_state* pn_state_new(pn_model *model) { + if (!model) return NULL; + + pn_state *state = calloc(1, sizeof(pn_state)); + if (!state) return NULL; + + state->pos = 0; + state->config = &model->config; + + /* RWKV-X uses its own state (recurrent + KV cache + workspace) */ + if (model->config.arch == PN_ARCH_RWKVX) { + state->kv_cache = pn_rwkvx_state_new(model->weights); + state->arena = NULL; + if (!state->kv_cache) { + free(state); + return NULL; + } + return state; + } + + /* Qwen3.5-MoE uses its own dual state (SSM conv/ssm + attention KV). */ + if (model->config.arch == PN_ARCH_QWEN35MOE) { + state->kv_cache = pn_qwen35moe_state_new(model->weights); + state->arena = NULL; + if (!state->kv_cache) { + free(state); + return NULL; + } + return state; + } + + /* Bonsai shares the qwen35moe dual-state (same hybrid GDN + attn-KV). */ + if (model->config.arch == PN_ARCH_BONSAI) { + state->kv_cache = pn_bonsai_state_new(model->weights); + state->arena = NULL; + if (!state->kv_cache) { + free(state); + return NULL; + } + return state; + } + + /* Allocate KV cache — sized to the per-session window (KV Phase 1: no longer + * a hard 4096 clamp; driven by config + ctx_cap + PN_SESSION_MAX_CTX). + * + * This call is the definition of the ring, and pn_admit_max_window() (hence + * pn_session_ctx_cap(), the scheduler's per-session gate, and + * pn_effective_ctx_window(), the advertised number) resolves to the SAME + * call on the dense path. The invariant that matters: the gate is never + * above the ring. pn_effective_ctx_window may sit *below* it under memory + * pressure — that is a transient prefill clamp, and clamping a permanent + * allocation to a transient signal would strand a slot at whatever the box + * happened to look like the instant it was created. + * + * The bytes here are eager (tm_zeros mallocs and memsets), all layers, fp32: + * 2 * n_layer * n_kv_head * max_seq * head_dim * 4 + * and one of these exists per decode slot AND per prefix-cache session, so + * an unbounded max_seq multiplies straight into host RSS. */ + int head_dim = pn_head_dim(&model->config); + int max_seq = pn_session_max_seq(&model->config); + + pn_llama_kv_cache *cache = pn_llama_kv_cache_new( + model->config.n_layer, + model->config.n_kv_head, + max_seq, + head_dim + ); + if (!cache) { + free(state); + return NULL; + } + state->kv_cache = cache; + + /* Create arena for scratch memory */ + size_t arena_size = compute_arena_size(&model->config); + state->arena = pn_arena_new(arena_size); + if (!state->arena) { + pn_llama_kv_cache_free(cache); + free(state); + return NULL; + } + fprintf(stderr, "engine: arena allocated (%.1f KB)\n", (double)arena_size / 1024.0); + + return state; +} + +void pn_state_free(pn_state *state) { + if (!state) return; + if (state->config && state->config->arch == PN_ARCH_RWKVX) { + pn_rwkvx_state_free(state->kv_cache); + free(state); + return; + } + if (state->config && state->config->arch == PN_ARCH_QWEN35MOE) { + pn_qwen35moe_state_free(state->kv_cache); + free(state); + return; + } + if (state->config && state->config->arch == PN_ARCH_BONSAI) { + pn_bonsai_state_free(state->kv_cache); + free(state); + return; + } + if (state->kv_cache) { + pn_llama_kv_cache_free((pn_llama_kv_cache *)state->kv_cache); + } + if (state->arena) { + pn_arena_free(state->arena); + } + free(state); +} + +void pn_state_reset(pn_state *state) { + if (!state) return; + if (state->config && state->config->arch == PN_ARCH_RWKVX) { + pn_rwkvx_state_reset(state->kv_cache); + state->pos = 0; + return; + } + if (state->config && state->config->arch == PN_ARCH_QWEN35MOE) { + pn_qwen35moe_state_reset(state->kv_cache); + state->pos = 0; + return; + } + if (state->config && state->config->arch == PN_ARCH_BONSAI) { + pn_bonsai_state_reset(state->kv_cache); + state->pos = 0; + return; + } + state->pos = 0; + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + if (kv) { + for (int i = 0; i < kv->n_layers; i++) { + if (kv->k_cache[i]) tm_zero_(kv->k_cache[i]); + if (kv->v_cache[i]) tm_zero_(kv->v_cache[i]); + } + } +} + +/* ============================================================ + * Sampling + * ============================================================ */ + +/* + * Uniform [0,1) draw for the final cumulative scan. + * + * Default: the process-global rand(), exactly as before. When the request + * carried an OpenAI `seed` (params->rng_seeded) the draw comes from that + * request's OWN stream instead, advanced in place — so two identical requests + * with the same seed replay the same tokens regardless of what other requests + * are sampling concurrently. See sampling_modifiers.h. + */ +static float rand_float(pn_sampling_params *params) { + if (params && params->rng_seeded) + return pn_seeded_rng_next_float(¶ms->rng_state); + return (float)rand() / ((float)RAND_MAX + 1.0f); +} + +/* ------------------------------------------------------------------ + * Optimized token sampler. + * + * This is a drop-in, LOSSLESS replacement for the original sampler: for any + * given logits / params / recent-token window and the same RNG state it + * returns the exact same token id, bit-for-bit. The correctness contract is + * enforced by tests/test_sampler_parity.c, which keeps a verbatim copy of the + * original implementation and diffs the two over thousands of random cases. + * + * What changed (all lossless): + * - Top-K threshold is found with an O(V) quickselect (order statistic on the + * probabilities) instead of the original O(top_k * V) selection sort. The + * threshold is the exact same value (the top_k-th largest probability), so + * the "keep probs >= threshold" set is bit-identical, including ties. + * - Top-P runs over only the <= top_k survivors (sorted once), instead of + * repeated full-vocab argmax passes. Survivor probabilities are accumulated + * in descending order, reproducing the same cumulative sum and the same + * top_p_threshold value. + * - The full-vocab softmax (temperature scaling + normalization over the full + * vocab) is preserved exactly, so every candidate's probability is + * identical to the original, and the final cumulative random draw scans the + * same probabilities in the same (ascending index) order. + * - Per-call malloc/free of three ~V-sized buffers is replaced by a + * thread-local scratch buffer that is reused across calls (and grown on + * demand). Thread-local makes it safe if multiple threads ever sample + * concurrently, without any locking. + * ------------------------------------------------------------------ */ + +typedef struct { + float *probs; /* softmax probabilities, indexed by token id */ + float *work; /* scratch for quickselect / top-p candidate sort */ + int cap; +} pn_sampler_scratch; + +static _Thread_local pn_sampler_scratch g_sampler_scratch = {0}; + +static pn_sampler_scratch *sampler_scratch_get(int n) { + pn_sampler_scratch *s = &g_sampler_scratch; + if (s->cap < n) { + free(s->probs); free(s->work); + s->probs = malloc((size_t)n * sizeof(float)); + s->work = malloc((size_t)n * sizeof(float)); + if (!s->probs || !s->work) { + free(s->probs); free(s->work); + s->probs = NULL; s->work = NULL; s->cap = 0; + return NULL; + } + s->cap = n; + } + return s; +} + +/* Quickselect: returns the k-th smallest element (0-indexed) of a[0..n-1], + * partitioning a[] in place. Uses a deterministic middle pivot (no rand(), so + * the global RNG that drives the final draw is untouched). The returned value + * is the exact order statistic, so it matches the original selection sort's + * threshold bit-for-bit. */ +static float sampler_select_kth(float *a, int n, int k) { + int lo = 0, hi = n - 1; + while (lo < hi) { + int i = lo, j = hi; + float pivot = a[lo + ((hi - lo) >> 1)]; + while (i <= j) { + while (a[i] < pivot) i++; + while (a[j] > pivot) j--; + if (i <= j) { + float t = a[i]; a[i] = a[j]; a[j] = t; + i++; j--; + } + } + if (k <= j) hi = j; + else if (k >= i) lo = i; + else break; + } + return a[k]; +} + +/* ------------------------------------------------------------------ + * OpenAI `logprobs` support. + * + * The reported numbers must be the MODEL's log-probabilities, so they come from + * a snapshot of the logits taken before the sampler touches them (bias, + * penalties, temperature, top-k/top-p and the grammar mask all mutate the array + * in place). The snapshot lives in a thread-local buffer that is only ever + * allocated for requests that actually asked for logprobs — a request without + * them pays nothing. The math itself is in sampling_modifiers.c. + * ------------------------------------------------------------------ */ + +static _Thread_local float *g_logprob_raw = NULL; +static _Thread_local int g_logprob_cap = 0; + +/* Copy the untouched logits aside. Returns NULL on OOM (logprobs are then + * simply not reported for this step — never a generation failure). */ +static const float *logprob_snapshot(const float *logits, int n_vocab) { + if (g_logprob_cap < n_vocab) { + float *grown = realloc(g_logprob_raw, (size_t)n_vocab * sizeof(float)); + if (!grown) return NULL; + g_logprob_raw = grown; + g_logprob_cap = n_vocab; + } + memcpy(g_logprob_raw, logits, (size_t)n_vocab * sizeof(float)); + return g_logprob_raw; +} + +/* Hand the sink the chosen token's logprob and the top alternatives. */ +static void emit_logprobs(const pn_logprob_sink *sink, const float *raw, + int n_vocab, uint32_t chosen) { + uint32_t top_ids[PN_LOGPROBS_MAX_TOP]; + float top_lp[PN_LOGPROBS_MAX_TOP]; + float chosen_lp = 0.0f; + int n_kept = pn_logprobs_extract(raw, n_vocab, chosen, sink->n_top, + &chosen_lp, top_ids, top_lp); + sink->emit(sink->user, chosen, chosen_lp, top_ids, top_lp, n_kept); +} + +static int sampler_cmp_desc(const void *pa, const void *pb) { + float x = *(const float *)pa, y = *(const float *)pb; + return (x < y) - (x > y); /* descending */ +} + +/* ------------------------------------------------------------------ + * Sparse-survivor sampler fast path (PN_SPARSE_SAMPLER). + * + * On the CUDA decode path the LM head's logits never leave the die whole: the + * device top-k reduces them to k (id, logit) pairs and gpu_present_topk lays + * those into the host row SPARSELY — every other entry is exactly -inf. Only + * k <= 40 tokens can possibly be sampled, but sample_token_core below still + * walks all 248320 of them EIGHT times and calls expf() on every one. On the + * K80 host's 2-core CPU that walk is the entire measured cost of serving at + * temperature 0.7 instead of 0. This path does the identical arithmetic over + * just the survivors. + * + * WHY THE DETECTION IS A BIT COMPARE. The obvious test, `logits[i] > + * -INFINITY`, is WRONG here and silently so: the engine builds with + * -ffast-math, which implies -ffinite-math-only, under which the compiler is + * entitled to assume no operand is infinite and folds the comparison to a + * constant TRUE. Measured on the release flags: over a row with 7 real + * survivors in 1000 entries, `> -INFINITY` counts 1000 and isfinite()/the bit + * compare both count 7. An earlier attempt at this fast path used `> + * -INFINITY`, so every row "overflowed" the survivor cap on entry, the fast + * path never once executed, and the A/B that concluded "the host sampler costs + * nothing" was measuring the old path against itself. Compare the bit pattern + * the producer actually writes; it is the one test -ffast-math cannot reason + * away. + * + * EXACTNESS. Bit-identical to the full-vocab path below, not merely close: + * - max: non-survivors are -inf and lose every comparison. + * - softmax sum: the full path accumulates ascending index and adds + * expf(-inf - max) = +0.0f for each non-survivor; adding +0.0f to a float + * never changes it, so gathering ascending and skipping those terms yields + * the same sum, in the same order, bit for bit. + * - top-k: the threshold is the top_k-th largest probability over the WHOLE + * row, i.e. over {S survivor probs} u {V-S zeros}. When top_k >= S the + * order statistic falls in the zero region and is 0.0f, so nothing is cut; + * otherwise it is the top_k-th largest survivor prob. Both reproduce the + * quickselect's value exactly, ties included. + * - top-p: the candidate set is `probs[i] > 0.0f`, which no non-survivor can + * join, so the descending sort and the cumulative threshold are identical. + * - draw: the cumulative scan crosses r at the same index, because the terms + * it skips are +0.0f and a zero term cannot be the crossing point (the + * scan would already have returned at the previous index). The exhausted + * fallbacks return the same ids, including the all-zero case's token 0. + * + * GREEDY TOO (added after the temp>0 path measured out). temp 0 is only ONE + * pass over n_vocab, so it looks like it has nothing to give — but that pass is + * a serially dependent float compare (`logits[i] > logits[best]`, best carried + * across the whole row) and the gather's pass is an independent compare against + * a constant. Measured on the same row and the same machine, the argmax pass + * costs 0.394 ms and the gather pass 0.057 ms: same O(n_vocab), 6.9x apart. + * Greedy therefore goes through the SAME sparse_gather and takes its argmax + * over the survivors. Bit-exact for the same reason the max above is: every + * non-survivor is -inf and loses a strict `>` to any survivor, and both scans + * run ascending so ties resolve to the same lowest id. + * + * Scope: only when the row really is sparse. A dense row — full_logits mode, + * which the serve forces for tool grammar, logit_bias and logprobs — trips the + * survivor cap in the first PN_SURV_MAX entries and falls through to the full + * path unchanged. + * + * NaN caveat (both paths, unchanged by the greedy addition): a NaN logit is not + * the -inf bit pattern, so it is gathered as a survivor. `>` is false in both + * directions against a NaN, so the full scan keeps whatever `best` held when it + * met one and the survivor scan keeps whatever ITS `best` held — those need not + * be the same index. A NaN in the LM head is a model-level failure that has + * never been observed here, and -ffast-math already declares them out of + * contract; recorded so the divergence is not a surprise if one ever appears. + * ------------------------------------------------------------------ */ + +/* The device top-k caps at FC_TOPK_MAX (256) survivors; anything past that is + * a dense row by definition. Kept local so engine.c still builds without CUDA. */ +#define PN_SURV_MAX 256 + +/* Bit pattern of -INFINITY as written by the sparse-logits producer. */ +#define PN_NEG_INF_BITS 0xff800000u + +_Static_assert(sizeof(float) == sizeof(uint32_t), + "sparse-survivor detection punnes float through uint32_t"); + +/* Runtime override for the tests (the token-identity gate flips arms in + * process). -1 = follow the env; 0/1 = force. */ +static int g_sparse_sampler_override = -1; +void pn_sampler_set_sparse(int on) { g_sparse_sampler_override = on ? 1 : 0; } +void pn_sampler_clear_sparse(void) { g_sparse_sampler_override = -1; } + +/* How many times the fast path actually RAN. The gate asserts this advances — + * an inert fast path is exactly how the previous attempt passed a vacuous + * bit-exactness test. */ +static unsigned long g_sparse_hits = 0; +/* Top-N rows the sampler swapped for the dense row (pn_sampler_topn_row_exact false). */ +static unsigned long g_topn_refills = 0; +unsigned long pn_sampler_topn_refills(void) { return g_topn_refills; } +unsigned long pn_sampler_sparse_hits(void) { return g_sparse_hits; } + +/* Per thread: the speculative-sampling chooser asks for the fast path on its own -INF rows + * (they are sparse by construction; gated byte-identical, run_empty_env_default.sh). */ +static _Thread_local int t_sparse_sampler_force = 0; + +static int sparse_sampler_enabled(void) { + if (t_sparse_sampler_force) return 1; + if (g_sparse_sampler_override >= 0) return g_sparse_sampler_override; + static int v = -1; + if (v < 0) v = getenv("PN_SPARSE_SAMPLER") ? 1 : 0; + return v; +} + +/* Set by the grammar wrapper when it APPLIED a mask THIS step. The masked row + * is dense anyway (the serve arms full_logits while the mask is live), so this + * is belt-and-braces: it keeps the fast path off even if that ever stops + * holding. Thread-local — the batched driver samples per slot on its own + * thread. + * + * IT IS A PER-STEP FLAG AND MUST BE CLEARED PER STEP (sample_token does it, at + * entry). It used to be set and never cleared, which on the batched path — one + * driver thread sampling EVERY slot — meant the first masked step of the first + * tool call latched the sparse-sampler fast path OFF for every slot, every + * session, for the life of the process. That is a whole-process loss of the + * largest measured win in the project, triggered by one grammar token. */ +static _Thread_local int g_grammar_masked = 0; + +/* Set while sample_token hands sample_token_core a row the grammar mask has + * already decided (refilled and/or rewritten): the core's top-N refill must + * not replace it. */ +static _Thread_local int t_grammar_row_final = 0; + +/* PN_GRAMMAR_SHORTLIST=0: always run the full-vocabulary mask (A/B, red arm). */ +static int grammar_shortlist_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("PN_GRAMMAR_SHORTLIST"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +typedef struct { + const pn_tool_grammar_hook *tg; +} grammar_allowed_ctx; + +/* gms_allowed_fn over the hook, with the full mask's rule for a token with no + * text (grammar_token_verdict). */ +static bool grammar_token_allowed(void *ctx, uint32_t id) +{ + const pn_tool_grammar_hook *tg = ((grammar_allowed_ctx *)ctx)->tg; + const char *txt = tg->token_text(tg->user, id); + if (!(txt && *txt)) return !tg->end_allowed || tg->end_allowed(tg->st); + return tg->allowed(tg->st, txt); +} + +/* Per-thread n_vocab byte scratch for the full mask's verdicts (NULL on OOM: + * the caller then asks allowed() again in pass 2). */ +static unsigned char *grammar_verdict_scratch(int n) +{ + static _Thread_local unsigned char *buf = NULL; + static _Thread_local int cap = 0; + if (n > cap) { + unsigned char *grown = realloc(buf, (size_t)n); + if (!grown) return NULL; + buf = grown; + cap = n; + } + return buf; +} + +/* Prefix verdicts for one masked step: does the walk survive this 1- or 2-byte + * prefix? Every grammar walk here is absorbing at DEAD, so a token whose + * prefix kills the walk is rejected without walking it. Rows of the 2-byte + * table are filled on first use (only for first bytes that survive). */ +typedef struct { + signed char one[256]; /* 1 alive, 0 dead */ + signed char two[256][256]; /* -1 unknown, 1 alive, 0 dead */ + bool two_ready[256]; +} grammar_prefix_verdicts; + +static void grammar_prefix_verdicts_init(const pn_tool_grammar_hook *tg, + grammar_prefix_verdicts *pv) +{ + char one[2] = { 0, 0 }; + pv->one[0] = 1; + for (int c = 1; c < 256; c++) { + one[0] = (char)c; + pv->one[c] = tg->allowed(tg->st, one) ? 1 : 0; + } + memset(pv->two_ready, 0, sizeof(pv->two_ready)); +} + +static bool grammar_prefix2_alive(const pn_tool_grammar_hook *tg, grammar_prefix_verdicts *pv, + unsigned char c0, unsigned char c1) +{ + if (!pv->two_ready[c0]) { + char two[3] = { (char)c0, 0, 0 }; + for (int c = 1; c < 256; c++) { + two[1] = (char)c; + pv->two[c0][c] = tg->allowed(tg->st, two) ? 1 : 0; + } + pv->two_ready[c0] = true; + } + return pv->two[c0][c1] != 0; +} + +/* The full mask's verdict for one token. A token with no text (an end token) + * is allowed unless the hook's end_allowed says otherwise. */ +static int grammar_token_verdict(const pn_tool_grammar_hook *tg, grammar_prefix_verdicts *pv, + const char *txt) +{ + if (!(txt && *txt)) return !tg->end_allowed || tg->end_allowed(tg->st); + const unsigned char c0 = (unsigned char)txt[0], c1 = (unsigned char)txt[1]; + if (!pv->one[c0]) return 0; + if (!c1) return 1; /* a 1-byte token: already judged */ + if (!grammar_prefix2_alive(tg, pv, c0, c1)) return 0; + return tg->allowed(tg->st, txt) ? 1 : 0; +} + +/* Per-thread chunk-need scratch (one byte per 16 ids). */ +static unsigned char *grammar_need_scratch(int n_chunks) +{ + static _Thread_local unsigned char *buf = NULL; + static _Thread_local int cap = 0; + if (n_chunks > cap) { + unsigned char *grown = realloc(buf, (size_t)n_chunks); + if (!grown) return NULL; + buf = grown; + cap = n_chunks; + } + return buf; +} + +/* The full mask on a TOP-N row without the whole dense row: judge every token + * (prefix-filtered), fetch only the 16-entry chunks that hold an allowed token + * (params->dense_chunks), and leave allowed tokens at their true logit and + * everything else at `mask` — the same row the full mask leaves on a refilled + * dense row. Returns 1 when applied, 0 when unavailable (no callback, no + * scratch, no allowed token, or the fetch failed — the row is then untouched + * apart from fetched chunks, which hold true logits). */ +static int grammar_mask_from_chunks(float *logits, int nv, const pn_sampling_params *params, + const pn_tool_grammar_hook *tg, float mask) +{ + static _Thread_local grammar_prefix_verdicts pv; + const int n_chunks = (nv + 15) / 16; + unsigned char *verdict = grammar_verdict_scratch(nv); + unsigned char *need = grammar_need_scratch(n_chunks); + int survivors = 0; + + if (!params->dense_chunks || !verdict || !need) + return 0; + grammar_prefix_verdicts_init(tg, &pv); + memset(need, 0, (size_t)n_chunks); + for (int i = 0; i < nv; i++) { + verdict[i] = (unsigned char)grammar_token_verdict(tg, &pv, tg->token_text(tg->user, + (uint32_t)i)); + if (verdict[i]) { need[i / 16] = 1; survivors++; } + } + if (survivors == 0 || params->dense_chunks(params->dense_ctx, need, logits, nv) != 0) + return 0; + for (int i = 0; i < nv; i++) + if (!verdict[i]) logits[i] = mask; + return 1; +} + +/* The shortlist decision for this row (grammar_mask_shortlist.h); true when the + * row was rewritten to the masked survivors. */ +static int grammar_mask_from_shortlist(float *logits, int n_vocab, + const pn_sampling_params *params, + const pn_tool_grammar_hook *tg, + const uint32_t *recent, int n_recent) +{ + uint32_t ids[GMS_MAX]; + float bound = 0.0f; + grammar_allowed_ctx ctx = { tg }; + gms_sampling s = { + .temperature = params->temperature, .top_k = params->top_k, + .repeat_penalty = params->repeat_penalty, + .presence_penalty = params->presence_penalty, + .frequency_penalty = params->frequency_penalty, + .has_logit_bias = params->logit_bias && params->n_logit_bias > 0, + .recent = recent, .n_recent = recent ? n_recent : 0, + }; + int nv = tg->n_vocab > 0 && tg->n_vocab < n_vocab ? tg->n_vocab : n_vocab; + + if (!grammar_shortlist_on()) + return 0; + int n = gms_select_shortlist(logits, nv, ids, &bound); + return n > 0 && gms_mask_from_shortlist(logits, n_vocab, ids, n, bound, &s, + grammar_token_allowed, &ctx); +} + +/* Gather the sparse row's survivors in ASCENDING id order. Returns the count, + * or -1 if the row holds more than PN_SURV_MAX finite entries (dense row → the + * caller must use the full path). */ +static int sparse_gather(const float *logits, int n_vocab, + int *ids, float *vals) { + int S = 0; + for (int i = 0; i < n_vocab; i++) { + uint32_t bits; + memcpy(&bits, &logits[i], sizeof bits); + if (bits == PN_NEG_INF_BITS) continue; + if (S >= PN_SURV_MAX) return -1; + ids[S] = i; vals[S] = logits[i]; S++; + } + return S; +} + +/* ------------------------------------------------------------------ + * Grammar-mask density tripwire (see engine.h). + * + * The serve arms the full-vocabulary row for the steps the grammar mask is + * live on. If it ever arms LATE, the mask lands on the device's sparse top-k + * presentation and can only choose among ~k tokens — which is not a slow step, + * it is the response_format defect: no legal token in the top-k means zero + * survivors, and the walk fails open (AUTO) or force-terminates (FORCED). + * + * So count it, in the sampler, where the two facts meet. The row test is a BIT + * compare against the -inf pattern the sparse producer writes, never + * `x > -INFINITY`: -ffast-math implies -ffinite-math-only, which folds the + * float form to a constant and is exactly what hid the sparse sampler for a + * month. The scan early-exits the instant the survivor count passes the device + * cap, so a correctly-armed DENSE row costs PN_SURV_MAX+1 iterations of a + * load-and-compare and nothing else. + * ------------------------------------------------------------------ */ +static unsigned long g_grammar_mask_steps = 0; +static unsigned long g_grammar_sparse_rows = 0; +unsigned long pn_sampler_grammar_mask_steps(void) { return g_grammar_mask_steps; } +/* Where masked steps go and what they cost: decided by the shortlist (no + * vocabulary scan), or scanned in full; wall time inside the mask (refill, + * shortlist, scan — not the draw). Process-wide, like the counters above. */ +static unsigned long g_grammar_shortlist_steps = 0; +static unsigned long g_grammar_fullscan_steps = 0; +static unsigned long g_grammar_chunk_steps = 0; +unsigned long pn_sampler_grammar_chunk_steps(void) { return g_grammar_chunk_steps; } +static double g_grammar_mask_seconds = 0.0; +unsigned long pn_sampler_grammar_shortlist_steps(void) { return g_grammar_shortlist_steps; } +unsigned long pn_sampler_grammar_fullscan_steps(void) { return g_grammar_fullscan_steps; } +double pn_sampler_grammar_mask_seconds(void) { return g_grammar_mask_seconds; } + +static double grammar_now_seconds(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} +unsigned long pn_sampler_grammar_sparse_rows(void) { return g_grammar_sparse_rows; } + +/* True when `logits` is still the device's sparse top-k presentation (at most + * PN_SURV_MAX finite entries) rather than the full vocabulary row. */ +static int grammar_row_is_sparse(const float *logits, int n_vocab) +{ + int finite = 0; + for (int i = 0; i < n_vocab; i++) { + uint32_t bits; + memcpy(&bits, &logits[i], sizeof bits); + if (bits == PN_NEG_INF_BITS) continue; + if (++finite > PN_SURV_MAX) return 0; + } + return 1; +} + +/* Record one mask application and shout if it landed on a sparse row. */ +static void grammar_note_mask_step(const float *logits, int n_vocab) +{ + g_grammar_mask_steps++; + if (!grammar_row_is_sparse(logits, n_vocab)) return; + g_grammar_sparse_rows++; + fprintf(stderr, "[tool_grammar] INVARIANT VIOLATED: mask applied to a " + "SPARSE top-k row (masked_steps=%lu sparse_rows=%lu) — the " + "full-logits row was armed LATE; the grammar can only see ~k " + "tokens this step and may fail open\n", + g_grammar_mask_steps, g_grammar_sparse_rows); +} + +/* Greedy over S survivors already in ascending-id order. Mirrors the full + * path's `for (i=1..n_vocab) if (logits[i] > logits[best]) best = i;` — same + * strict `>`, same ascending order, so the same lowest-id-wins tie-break. */ +static uint32_t sparse_argmax(const int *ids, const float *vals, int S) +{ + int best = 0; + for (int i = 1; i < S; i++) + if (vals[i] > vals[best]) best = i; + return (uint32_t)ids[best]; +} + +/* The sampler math over S survivors already in ascending-id order, with + * temperature > 0 and the repetition penalty already applied to vals[]. + * Mirrors sample_token_core's tail statement for statement. */ +static uint32_t sparse_sample(const int *ids, float *vals, int S, int n_vocab, + pn_sampling_params *params) { + float probs[PN_SURV_MAX]; + float work[PN_SURV_MAX]; + + float inv_temp = 1.0f / params->temperature; + for (int i = 0; i < S; i++) vals[i] *= inv_temp; + + int top_k = params->top_k; + if (top_k <= 0 || top_k > n_vocab) top_k = n_vocab; + + float max_logit = vals[0]; + for (int i = 1; i < S; i++) if (vals[i] > max_logit) max_logit = vals[i]; + + float sum = 0.0f; + for (int i = 0; i < S; i++) { probs[i] = expf(vals[i] - max_logit); sum += probs[i]; } + float inv_sum = 1.0f / sum; + for (int i = 0; i < S; i++) probs[i] *= inv_sum; + + /* Top-K over the full row: the order statistic lands in the zero region + * (threshold 0.0f, nothing cut) whenever top_k >= S. */ + if (top_k < n_vocab) { + float threshold = 0.0f; + if (top_k < S) { + memcpy(work, probs, (size_t)S * sizeof(float)); + qsort(work, (size_t)S, sizeof(float), sampler_cmp_desc); + threshold = work[top_k - 1]; + } + for (int i = 0; i < S; i++) if (probs[i] < threshold) probs[i] = 0.0f; + /* llama.cpp / HF order (2026-10-07): renormalise WITHIN the top-k set before + * top-p, so top-p measures mass among the candidates, not the full vocab. */ + sum = 0.0f; + for (int i = 0; i < S; i++) sum += probs[i]; + if (sum > 0.0f) { + inv_sum = 1.0f / sum; + for (int i = 0; i < S; i++) probs[i] *= inv_sum; + } + } + + if (params->top_p < 1.0f) { + int nc = 0; + for (int i = 0; i < S; i++) if (probs[i] > 0.0f) work[nc++] = probs[i]; + qsort(work, (size_t)nc, sizeof(float), sampler_cmp_desc); + float cum_prob = 0.0f, top_p_threshold = 0.0f; + for (int t = 0; t < nc; t++) { + if (!(cum_prob < params->top_p)) break; + if (work[t] <= 0.0f) break; + cum_prob += work[t]; + top_p_threshold = work[t]; + } + for (int i = 0; i < S; i++) if (probs[i] < top_p_threshold) probs[i] = 0.0f; + } + + sum = 0.0f; + for (int i = 0; i < S; i++) sum += probs[i]; + if (sum > 0.0f) { + inv_sum = 1.0f / sum; + for (int i = 0; i < S; i++) probs[i] *= inv_sum; + } + + float r = rand_float(params); + float cum = 0.0f; + for (int i = 0; i < S; i++) { + cum += probs[i]; + if (r < cum) return (uint32_t)ids[i]; + } + for (int i = S - 1; i >= 0; i--) { + if (probs[i] > 0.0f) return (uint32_t)ids[i]; + } + /* Every probability underflowed: the full path's scan falls off the end + * and returns token 0. Match it — NOT the lowest survivor id. */ + return 0; +} + +/* + * Can a TOP-N row (finite on N entries, -INFINITY elsewhere) decide this SAMPLED draw + * exactly as the dense row would? + * + * The sampler's top-k set, after the penalties, must lie inside the N: every token outside + * has a raw logit <= the smallest survivor's raw logit (v_min), and presence / frequency / + * repetition penalties only LOWER logits, so if the k-th largest PENALISED survivor is above + * v_min by a margin that survives temperature + expf rounding, no outside token can reach + * the top-k threshold and the renormalised top-k distribution is the dense one's. A logit + * bias can raise a token, so it is never exact (callers keep such requests dense). + * Not sparse at all (a dense row) also returns true: nothing to decide. + */ +bool pn_sampler_topn_row_exact(const float *logits, int n_vocab, const pn_sampling_params *params, + const uint32_t *recent, int n_recent) +{ + enum { MAX_N = 256 }; + int ids[MAX_N]; + float adj[MAX_N]; + float v_min = 0.0f, kth; + int S = 0, i, j; + uint32_t bits; + + if (!params || params->temperature <= 0.0f) + return true; + for (i = 0; i < n_vocab; i++) { + memcpy(&bits, &logits[i], sizeof(bits)); + if (bits == PN_NEG_INF_BITS) + continue; + if (S == MAX_N) + return true; /* not a top-N row */ + ids[S] = i; + adj[S] = logits[i]; + if (S == 0 || logits[i] < v_min) v_min = logits[i]; + S++; + } + if (S == n_vocab) + return true; + if (params->logit_bias && params->n_logit_bias > 0) + return false; + if (params->top_k < 1 || params->top_k > S) + return false; + for (j = 0; j < S; j++) { + int count = 0; + + for (i = 0; i < n_recent; i++) + if (recent && recent[i] == (uint32_t)ids[j]) + count++; + if (count > 0) { + adj[j] -= params->frequency_penalty * (float)count + params->presence_penalty; + if (params->repeat_penalty != 1.0f) + for (i = 0; i < count; i++) + adj[j] = adj[j] > 0.0f ? adj[j] / params->repeat_penalty + : adj[j] * params->repeat_penalty; + } + } + /* k-th largest adjusted value (S <= 256: partial selection). */ + for (i = 0; i < params->top_k; i++) { + int best = i; + + for (j = i + 1; j < S; j++) + if (adj[j] > adj[best]) best = j; + kth = adj[best]; + adj[best] = adj[i]; + adj[i] = kth; + } + kth = adj[params->top_k - 1]; + return (kth - v_min) / params->temperature > 1e-4f; +} + +/* Distribution-test red arm only: the pre-2026-10-07 order (top-p over full-vocab mass). */ +static int g_sampler_old_order = 0; + +/* The full path's sampling distribution: temperature -> full-vocab softmax -> top-k + * (renormalised) -> top-p -> renormalised, over the row as given (bias and penalties already + * applied). Returns the scratch probability row, or NULL. Shared by the draw and by the + * top-N distribution test (pn_sampler_distribution). */ +static float *sampler_full_probs(float *logits, int n_vocab, const pn_sampling_params *params) +{ + /* Apply temperature */ + float inv_temp = 1.0f / params->temperature; + for (int i = 0; i < n_vocab; i++) { + logits[i] *= inv_temp; + } + + int top_k = params->top_k; + if (top_k <= 0 || top_k > n_vocab) top_k = n_vocab; + + /* Full-vocab softmax (preserved exactly: same max, same normalization set) */ + float max_logit = logits[0]; + for (int i = 1; i < n_vocab; i++) { + if (logits[i] > max_logit) max_logit = logits[i]; + } + + pn_sampler_scratch *sc = sampler_scratch_get(n_vocab); + if (!sc) return NULL; + float *probs = sc->probs; + + float sum = 0.0f; + for (int i = 0; i < n_vocab; i++) { + probs[i] = expf(logits[i] - max_logit); + sum += probs[i]; + } + float inv_sum = 1.0f / sum; + for (int i = 0; i < n_vocab; i++) { + probs[i] *= inv_sum; + } + + /* Build the survivor set (token indices, ascending order). + * + * Top-K: find the top_k-th largest probability via quickselect and keep + * everything >= it (matches the original "if probs[i] < threshold -> 0", + * including ties at the threshold). When no top-k is requested, every token + * is a survivor, exactly as in the original. */ + /* Top-K filtering. The original found the top_k-th largest probability with + * an O(top_k * V) selection sort; we find that exact same value as an order + * statistic via O(V) quickselect. The zeroing pass below is byte-identical + * to the original ("keep probs >= threshold", ties included). */ + if (top_k < n_vocab) { + memcpy(sc->work, probs, (size_t)n_vocab * sizeof(float)); + /* top_k-th largest == (n_vocab - top_k)-th smallest (0-indexed) */ + float threshold = sampler_select_kth(sc->work, n_vocab, n_vocab - top_k); + for (int i = 0; i < n_vocab; i++) { + if (probs[i] < threshold) probs[i] = 0.0f; + } + /* llama.cpp / HF order (2026-10-07, David): renormalise WITHIN the top-k set + * before top-p (ascending index order — the sparse path sums the same nonzero + * terms in the same order, and adding 0.0f is exact). The distribution test's red + * arm alone switches it off (g_sampler_old_order). */ + sum = 0.0f; + if (!g_sampler_old_order) + for (int i = 0; i < n_vocab; i++) sum += probs[i]; + if (sum > 0.0f) { + inv_sum = 1.0f / sum; + for (int i = 0; i < n_vocab; i++) probs[i] *= inv_sum; + } + } + + /* Top-P filtering. The original repeatedly scanned the full vocab to pull + * out the next-largest probability, accumulating until the cumulative mass + * reached top_p and recording the last (smallest) probability it added as + * the threshold. Instead we gather the (at most top_k) surviving positive + * probabilities once and sort them descending, then accumulate in that same + * descending order. This yields the identical cumulative sum and identical + * top_p_threshold (ties are equal values, so their order is irrelevant); the + * final zeroing pass is byte-identical to the original. */ + if (params->top_p < 1.0f) { + float *cand = sc->work; + int nc = 0; + for (int i = 0; i < n_vocab; i++) { + if (probs[i] > 0.0f) cand[nc++] = probs[i]; + } + qsort(cand, (size_t)nc, sizeof(float), sampler_cmp_desc); + + float cum_prob = 0.0f; + float top_p_threshold = 0.0f; + for (int t = 0; t < nc; t++) { + if (!(cum_prob < params->top_p)) break; + if (cand[t] <= 0.0f) break; + cum_prob += cand[t]; + top_p_threshold = cand[t]; + } + for (int i = 0; i < n_vocab; i++) { + if (probs[i] < top_p_threshold) probs[i] = 0.0f; + } + } + + /* Re-normalize (full-vocab, ascending index order: byte-identical to the + * original so the final draw crosses at exactly the same token). */ + sum = 0.0f; + for (int i = 0; i < n_vocab; i++) sum += probs[i]; + if (sum > 0.0f) { + inv_sum = 1.0f / sum; + for (int i = 0; i < n_vocab; i++) probs[i] *= inv_sum; + } + + return probs; +} + +/* + * THE TOP-N DISTRIBUTION TEST (`PN_SAMPLER_DIST_CHECK=1`, a DENSE sampled run). + * + * For every sampled draw the dense row reaches the sampler with, build the top-N row the card + * would hand over (the kernel's comparator: value desc, index asc; N = PN_SAMPLER_DIST_N, + * default 64) and compute the production sampler's final distribution for BOTH (bias, + * penalties, then sampler_full_probs). Report the total variation distance on draws the + * exactness check accepts (where production would sample the top-N row) and count the + * refills. The draw itself still uses the dense row, so the run's text is the dense run's. + * Red arms (`PN_SAMPLER_DIST_RED`): `old_order` (the previous top-p) and `topk0` (top-k off, + * scored on every draw) must show a large distance. + */ +static float sampler_dist_probs(const float *row, int n_vocab, const pn_sampling_params *params, + const uint32_t *recent, int n_recent, float *out) +{ + float *work = malloc((size_t)n_vocab * sizeof(float)); + float *probs; + + if (!work) return -1.0f; + memcpy(work, row, (size_t)n_vocab * sizeof(float)); + pn_apply_logit_bias(work, n_vocab, params->logit_bias, params->n_logit_bias); + pn_apply_presence_frequency_penalties(work, n_vocab, params->presence_penalty, + params->frequency_penalty, recent, n_recent); + if (params->repeat_penalty != 1.0f && recent) + for (int i = 0; i < n_recent; i++) + if (recent[i] < (uint32_t)n_vocab) + work[recent[i]] = work[recent[i]] > 0.0f ? work[recent[i]] / params->repeat_penalty + : work[recent[i]] * params->repeat_penalty; + probs = sampler_full_probs(work, n_vocab, params); + if (probs) memcpy(out, probs, (size_t)n_vocab * sizeof(float)); + free(work); + return probs ? 0.0f : -1.0f; +} + +static struct { + unsigned long draws, scored, inexact; + double max_tvd, sum_tvd; +} g_dist; + +static void sampler_dist_report(void) +{ + if (g_dist.draws) + fprintf(stderr, "SAMPLER DIST — %lu draws, %lu scored, %lu inexact (refill), TVD max %.3e " + "mean %.3e\n", g_dist.draws, g_dist.scored, g_dist.inexact, + g_dist.max_tvd, g_dist.scored ? g_dist.sum_tvd / (double)g_dist.scored : 0.0); +} + +static void sampler_dist_check(const float *logits, int n_vocab, const pn_sampling_params *in, + const uint32_t *recent, int n_recent) +{ + static int registered; + const char *red = getenv("PN_SAMPLER_DIST_RED"); + const char *ns = getenv("PN_SAMPLER_DIST_N"); + const int N = ns ? atoi(ns) : 64; + pn_sampling_params p = *in; + float *sparse, *pd, *ps; + int *top, n = 0, i, j; + bool exact, score_all = false; + double tvd = 0.0; + + if (!registered) { atexit(sampler_dist_report); registered = 1; } + if (red && strcmp(red, "topk0") == 0) { p.top_k = 0; score_all = true; } + /* `score_all`: score the draws the exactness check REJECTS too (run with a small + * PN_SAMPLER_DIST_N, e.g. 8 at top_k 40, so the rejected ones carry real mass). */ + if (red && strcmp(red, "score_all") == 0) score_all = true; + sparse = malloc((size_t)n_vocab * sizeof(float)); + pd = malloc((size_t)n_vocab * sizeof(float)); + ps = malloc((size_t)n_vocab * sizeof(float)); + top = malloc((size_t)(N > 0 ? N : 1) * sizeof(int)); + if (!sparse || !pd || !ps || !top || N < 1) goto done; + /* top-N: value desc, index asc (the kernel's comparator). */ + for (i = 0; i < n_vocab; i++) { + if (n < N || logits[i] > logits[top[n - 1]]) { + int at = n < N ? n++ : N - 1; + top[at] = i; + for (j = at; j > 0 && logits[top[j]] > logits[top[j - 1]]; j--) { + int t = top[j]; top[j] = top[j - 1]; top[j - 1] = t; + } + } + } + for (i = 0; i < n_vocab; i++) sparse[i] = -INFINITY; + for (i = 0; i < n; i++) sparse[top[i]] = logits[top[i]]; + g_dist.draws++; + exact = pn_sampler_topn_row_exact(sparse, n_vocab, &p, recent, n_recent); + if (!exact) g_dist.inexact++; + if (!exact && !score_all) goto done; + if (red && strcmp(red, "old_order") == 0) g_sampler_old_order = 1; + if (sampler_dist_probs(logits, n_vocab, &p, recent, n_recent, pd) == 0.0f && + sampler_dist_probs(sparse, n_vocab, &p, recent, n_recent, ps) == 0.0f) { + for (i = 0; i < n_vocab; i++) tvd += fabs((double)pd[i] - (double)ps[i]); + tvd *= 0.5; + g_dist.scored++; + g_dist.sum_tvd += tvd; + if (tvd > g_dist.max_tvd) g_dist.max_tvd = tvd; + if ((g_dist.scored & 63u) == 0u) + sampler_dist_report(); + } + g_sampler_old_order = 0; +done: + free(sparse); free(pd); free(ps); free(top); +} + +/* Core sampler (repeat_penalty → temperature → top-k, renormalised → top-p → + * cumulative draw). The public sample_token wraps this with the OPTIONAL + * grammar mask; with no grammar hook the wrapper is a straight pass-through, so + * this path is unchanged from before Phase 5. */ +static uint32_t sample_token_core(float *logits, int n_vocab, pn_sampling_params *params, + uint32_t *recent_tokens, int n_recent) { + if (params->temperature > 0.0f && getenv("PN_SAMPLER_DIST_CHECK")) + sampler_dist_check(logits, n_vocab, params, recent_tokens, n_recent); + /* A top-N row that cannot decide this draw exactly is swapped for the dense row — + * never one the grammar mask already rewrote (that would drop the mask). */ + if (params->dense_row && params->temperature > 0.0f && !t_grammar_row_final && + !pn_sampler_topn_row_exact(logits, n_vocab, params, recent_tokens, n_recent)) { + g_topn_refills++; + if (params->dense_row(params->dense_ctx, logits, n_vocab) != 0) + fprintf(stderr, "sampler: dense refill failed; sampling the top-N row\n"); + } + /* Client-requested logit surgery, in OpenAI's order and BEFORE the greedy + * branch: both are explicit client intent (default 0 / empty = no-op), and + * both are deterministic functions of the prefix, so greedy stays + * reproducible. See the ordering note on + * apply_presence_frequency_penalties for why `repeat_penalty` is NOT here. */ + pn_apply_logit_bias(logits, n_vocab, params->logit_bias, + params->n_logit_bias); + pn_apply_presence_frequency_penalties(logits, n_vocab, + params->presence_penalty, + params->frequency_penalty, + recent_tokens, n_recent); + + /* Temperature == 0 means greedy: PURE argmax on the RAW logits, taken BEFORE + * any repetition penalty. Greedy decoding must return the model's top token + * exactly as the reference (llama.cpp --temp 0 applies no repeat penalty by + * default). Applying repeat_penalty first (the previous behaviour) could + * demote the true argmax below a near-tied runner-up whenever the top token + * had appeared in the last repeat_last_n tokens — for Bonsai-27B on the + * x²+5 prompt this flipped ':' (top, logit 25.41) to ' given' (25.08, Δ0.33) + * right after "Equation", steering the model into a LaTeX \frac attractor + * and losing the answer 30. The forward pass was faithful; the greedy + * decision was corrupted by the penalty. Sampling (temp>0) is unchanged. */ + if (params->temperature <= 0.0f) { + /* Same sparse fast path as the sampler below, same flag, same gather. + * Neither pn_apply_logit_bias (+=) nor the presence/frequency penalties + * (-=) can lift a -inf non-survivor back to finite, so the survivor set + * is the same set the full scan would have had to beat. */ + if (sparse_sampler_enabled() && !g_grammar_masked) { + int surv_ids[PN_SURV_MAX]; + float surv_vals[PN_SURV_MAX]; + int S = sparse_gather(logits, n_vocab, surv_ids, surv_vals); + if (S > 0) { + g_sparse_hits++; + return sparse_argmax(surv_ids, surv_vals, S); + } + /* S == 0 (nothing finite) or S == -1 (dense row): full scan. */ + } + int best = 0; + for (int i = 1; i < n_vocab; i++) { + if (logits[i] > logits[best]) best = i; + } + return (uint32_t)best; + } + + /* Apply repetition penalty (sampling path only) */ + if (params->repeat_penalty != 1.0f && recent_tokens && n_recent > 0) { + for (int i = 0; i < n_recent; i++) { + uint32_t tok = recent_tokens[i]; + if (tok < (uint32_t)n_vocab) { + if (logits[tok] > 0.0f) + logits[tok] /= params->repeat_penalty; + else + logits[tok] *= params->repeat_penalty; + } + } + } + + /* Sparse-survivor fast path. Placed HERE, after the repetition penalty and + * before the temperature scale, because the penalty writes through token + * ids that may not be survivors — and on a non-survivor it is a no-op + * either way (-inf/rp and -inf*rp are both -inf), so the gathered survivor + * logits are already penalised. See the block comment above sparse_gather + * for the bit-exactness argument and for why the row test is a bit + * compare and not `> -INFINITY`. */ + /* Also whenever a backend hands top-N rows (dense_row set): byte-identical to the full + * path by test_sparse_sampler_identity, ~10 ms a draw cheaper at 248k vocab. */ + if ((sparse_sampler_enabled() || params->dense_row) && !g_grammar_masked) { + int surv_ids[PN_SURV_MAX]; + float surv_vals[PN_SURV_MAX]; + int S = sparse_gather(logits, n_vocab, surv_ids, surv_vals); + if (S > 0) { + g_sparse_hits++; + return sparse_sample(surv_ids, surv_vals, S, n_vocab, params); + } + /* S == 0 (nothing finite) or S == -1 (dense row): full path below. */ + } + + float *probs = sampler_full_probs(logits, n_vocab, params); + if (!probs) return 0; + + /* Sample (identical cumulative scan and fallback as the original). */ + float r = rand_float(params); + float cum = 0.0f; + for (int i = 0; i < n_vocab; i++) { + cum += probs[i]; + if (r < cum) return (uint32_t)i; + } + for (int i = n_vocab - 1; i >= 0; i--) { + if (probs[i] > 0.0f) return (uint32_t)i; + } + return 0; +} + +/* + * Grammar-constrained wrapper around sample_token_core (n2 tool-calling Phase 5). + * + * OFF by default: when params->tool_grammar is NULL (the default) this is a + * straight pass-through to sample_token_core — logits are untouched and the + * sampled token is byte-identical to the pre-Phase-5 path. + * + * When a hook is present AND active, before sampling we MASK (logit set to a + * large-negative sentinel) every candidate token whose text would take the + * grammar off a valid tool-call prefix; after the draw we ADVANCE the grammar + * by the chosen token's text. The mask is a CPU-side logit mutation exactly + * like repeat_penalty, so it is device-agnostic and CPU-testable. A finite + * sentinel (not -INFINITY) is used so the core softmax's expf() underflows the + * masked token to ~0 reliably under -ffast-math (which assumes no infinities). + * + * Perf: the scan is O(n_vocab) token-text lookups + allowed() checks; but each + * allowed() short-circuits on the first char in a fixed-skeleton state, so the + * per-step cost is dominated by the n_vocab text lookups (a few hundred µs at + * 248k tokens) and is only paid while `active` (inside a tool call), which is a + * small fraction of a generation. A per-state allowed-token cache (see + * tool_grammar.c) would drop this to a single precomputed bitmask lookup. + */ +static uint32_t sample_token(float *logits, int n_vocab, pn_sampling_params *params, + uint32_t *recent_tokens, int n_recent) { + const pn_tool_grammar_hook *tg = params ? params->tool_grammar : NULL; + + /* g_grammar_masked describes THIS step only. Clearing it here is what + * makes it a per-step flag: it used to be set-once-never-cleared, so on the + * batched path (one driver thread samples every slot) the first masked step + * of the first tool call disabled the sparse-sampler fast path for every + * slot and every session until the process exited. */ + g_grammar_masked = 0; + t_grammar_row_final = 0; + + /* Snapshot the untouched logits FIRST when the request asked for logprobs — + * everything below (mask, bias, penalties, temperature, top-k/top-p) mutates + * `logits` in place, and the OpenAI field means the model's own + * distribution, not the post-filter one. */ + const pn_logprob_sink *lp_sink = + (params && params->logprobs && params->logprobs->emit) + ? params->logprobs : NULL; + const float *lp_raw = lp_sink ? logprob_snapshot(logits, n_vocab) : NULL; + + /* Self-refresh: when the hook exposes a refresh callback, recompute the + * mask-active flag from the live grammar state here (so the batched driver, + * which samples per-slot in its own thread and never touches `active`, gates + * correctly). Falls back to the caller-maintained `active` when NULL. */ + int tg_active = tg ? tg->active : 0; + if (tg && tg->refresh && tg->st) + tg_active = tg->refresh(tg->st); + + static const float TG_MASK = -1e30f; /* finite sentinel (fast-math safe) */ + if (tg && tg_active && tg->st && tg->token_text && tg->allowed) { + /* A top-N row (a backend handed `dense_row`) holds only N candidates. + * First try to decide the masked draw from those N alone + * (grammar_mask_shortlist.h: exact when enough of them are allowed); + * else fetch only the dense chunks holding allowed tokens + * (dense_chunks); else refill the whole dense row, try its top + * GMS_MAX, and run the full-vocabulary mask below. Scoped to top-N + * backends: a dense-row backend goes straight to the full mask, whose + * verdicts are the same — only faster (prefix filter, one allowed() + * per token). */ + const double mask_started = grammar_now_seconds(); + if (params->dense_row) { + if (grammar_mask_from_shortlist(logits, n_vocab, params, tg, recent_tokens, + n_recent)) { + g_grammar_mask_steps++; + g_grammar_shortlist_steps++; + g_grammar_mask_seconds += grammar_now_seconds() - mask_started; + t_grammar_row_final = 1; + goto masked; + } + if (grammar_mask_from_chunks(logits, n_vocab, params, tg, TG_MASK)) { + g_grammar_mask_steps++; + g_grammar_chunk_steps++; + g_grammar_mask_seconds += grammar_now_seconds() - mask_started; + g_grammar_masked = 1; /* a masked dense-equivalent row: full path */ + t_grammar_row_final = 1; + goto masked; + } + g_topn_refills++; + if (params->dense_row(params->dense_ctx, logits, n_vocab) != 0) + fprintf(stderr, "sampler: dense refill for the grammar mask failed\n"); + t_grammar_row_final = 1; + if (grammar_mask_from_shortlist(logits, n_vocab, params, tg, recent_tokens, + n_recent)) { + g_grammar_mask_steps++; + g_grammar_shortlist_steps++; + g_grammar_mask_seconds += grammar_now_seconds() - mask_started; + goto masked; + } + } + g_grammar_fullscan_steps++; + int nv = tg->n_vocab > 0 && tg->n_vocab < n_vocab ? tg->n_vocab : n_vocab; + /* Two-pass so the mask is only APPLIED when at least one token survives. + * Pass 1 counts survivors without mutating logits; if none survive (a + * name/key with no single whole-token continuation, or a tokenizer/ + * grammar mismatch) we DO NOT mask this step — masking all candidates + * would leave a degenerate all-(-1e30) distribution the sampler draws + * garbage from (the observed "prose slips through after " bug + * was this fail-open picking an arbitrary token). Fail-OPEN cleanly: + * emit the model's own top token unmasked and let the grammar re-sync on + * the next step. Pass 2 applies the mask only when survivors>0. */ + /* The mask is about to be evaluated against this row, so this is the + * one place that can see both facts at once: check the density + * invariant here, BEFORE pass 1 mutates anything. */ + grammar_note_mask_step(logits, nv); + /* One allowed() verdict per token, kept for pass 2, behind a 1- and + * 2-byte prefix filter (grammar_prefix_verdicts): at a structural + * position nearly every token dies on its first or second byte. */ + static _Thread_local grammar_prefix_verdicts pv; + unsigned char *verdict = grammar_verdict_scratch(nv); + grammar_prefix_verdicts_init(tg, &pv); + int survivors = 0; + for (int i = 0; i < nv; i++) { + if (logits[i] <= TG_MASK) continue; /* already excluded upstream */ + const char *txt = tg->token_text(tg->user, (uint32_t)i); + const int ok = grammar_token_verdict(tg, &pv, txt); + if (verdict) verdict[i] = (unsigned char)ok; + survivors += ok; + } + if (survivors > 0) { + for (int i = 0; i < nv; i++) { + if (logits[i] <= TG_MASK) continue; + const int ok = verdict ? verdict[i] + : grammar_token_verdict(tg, &pv, tg->token_text(tg->user, (uint32_t)i)); + if (!ok) + logits[i] = TG_MASK; + } + g_grammar_masked = 1; /* sparse fast path stands down this step */ + g_grammar_mask_seconds += grammar_now_seconds() - mask_started; + } else if (tg->fail_closed) { + /* Fail-CLOSED (forced-mode hardening): no legal token survives, so + * rather than fall open to the model's own out-of-grammar top token + * (the escape hatch that let garbage/empty calls through), force the + * caller-supplied EOS/end token — a clean, in-grammar termination that + * never emits garbage. Advance the grammar by it (so a driver-side + * hook that tracks completion sees the end) and return immediately; + * skip normal sampling entirely. */ + fprintf(stderr, "[tool_grammar] 0 survivors — fail-CLOSED (forcing EOS %u)\n", + tg->forced_token); + if (tg->advance && tg->st) { + const char *etxt = tg->token_text(tg->user, tg->forced_token); + if (etxt) tg->advance(tg->st, etxt); + } + if (lp_raw) emit_logprobs(lp_sink, lp_raw, n_vocab, tg->forced_token); + return tg->forced_token; + } else { + /* Diagnostic detail: a 0-survivor step means the grammar rejected + * EVERY candidate, which for a whole-answer constraint is normally + * impossible (whitespace and '{' are always in a BPE vocab). Report + * the scan bounds and how many entries had no text, so the cause is + * visible from the fleet log instead of needing a repro build. */ + int n_empty = 0, n_skipped = 0, n_rejected = 0; + const char *sample[3] = { NULL, NULL, NULL }; + int n_sample = 0; + for (int i = 0; i < nv; i++) { + const char *txt = tg->token_text(tg->user, (uint32_t)i); + if (!(txt && *txt)) { n_empty++; continue; } + if (logits[i] <= TG_MASK) { n_skipped++; continue; } + n_rejected++; + if (n_sample < 3) sample[n_sample++] = txt; + } + fprintf(stderr, "[tool_grammar] WARN: 0 survivors this step — " + "fail-open (mask skipped) [nv=%d n_vocab=%d empty=%d " + "logit_skipped=%d rejected=%d s0='%s' s1='%s' s2='%s']\n", + nv, n_vocab, n_empty, n_skipped, n_rejected, + sample[0] ? sample[0] : "", sample[1] ? sample[1] : "", + sample[2] ? sample[2] : ""); + } + } + +masked:; + uint32_t tok = sample_token_core(logits, n_vocab, params, recent_tokens, n_recent); + t_grammar_row_final = 0; + + /* Advance the grammar by the chosen token's text. This must run EVERY step + * (not only when the mask was active) so the FSM tracks the FREE reasoning + * region — it needs to see "" and the opener to engage the forced + * skeleton. The single-user serve loop advances externally (advance == NULL + * there); the batched driver relies on this in-thread advance. */ + if (tg && tg->st && tg->token_text && tg->advance) { + const char *txt = tg->token_text(tg->user, tok); + if (txt) tg->advance(tg->st, txt); + } + if (lp_raw) emit_logprobs(lp_sink, lp_raw, n_vocab, tok); + return tok; +} + +int pn_sampler_grammar_shortlist(float *logits, int n_vocab, const pn_sampling_params *params, + const uint32_t *recent_tokens, int n_recent) +{ + const pn_tool_grammar_hook *tg = params ? params->tool_grammar : NULL; + + if (!tg || !tg->st || !tg->token_text || !tg->allowed) + return 0; + return grammar_mask_from_shortlist(logits, n_vocab, params, tg, recent_tokens, n_recent); +} + +/* Public seam: reuse the internal sampler from external decode drivers (serve + * layer) without duplicating the sampling logic. See engine.h for contract. */ +uint32_t pn_sample_next_token(float *logits, int n_vocab, + pn_sampling_params *params, + uint32_t *recent_tokens, int n_recent) { + return sample_token(logits, n_vocab, params, recent_tokens, n_recent); +} + +/* ============================================================ + * Async forward-step seam (C2) — see engine.h. Dispatches to the qwen35moe + * CUDA async step when that forward is selected (the same discriminator + * pn_admit_gpu_resident uses); everything else is "unsupported" and the + * caller keeps the blocking model->forward path. + * ============================================================ */ + +/* True for the forwards backed by the shared Qwen3.5 CUDA path (pn_qwen35moe_model + * weights + qwen35moe kernels). pn_bonsai_forward_cuda (Bonsai/Ornith, dense) just + * delegates to pn_qwen35moe_forward_cuda, so its weights ARE a pn_qwen35moe_model — + * it can use the same non-blocking begin/poll. Gating on the exact MoE pointer + * alone left the dense family stuck on the blocking path (Ornith cap). */ +#ifdef USE_CUDA +static int uses_qwen35moe_cuda(const pn_model *model) { + return model && (model->forward == pn_qwen35moe_forward_cuda || + model->forward == pn_bonsai_forward_cuda); +} +#endif + +int pn_forward_step_begin(pn_model *model, uint32_t token, pn_state *state) { +#ifdef USE_CUDA + if (state && uses_qwen35moe_cuda(model)) + return pn_qwen35moe_forward_step_begin(model, token, state); +#endif + (void)model; (void)token; (void)state; + return 1; +} + +int pn_forward_step_poll(pn_model *model, pn_state *state, + tm_tensor **logits_out) { +#ifdef USE_CUDA + if (state && uses_qwen35moe_cuda(model)) + return pn_qwen35moe_forward_step_poll(model, state, logits_out); +#endif + (void)model; (void)state; (void)logits_out; + return -1; +} + +/* ============================================================ + * Generation + * ============================================================ */ + +char* pn_generate(pn_model *model, pn_state *state, + const char *prompt, int max_tokens, + pn_sampling_params *params) { + if (!model || !state || !prompt || !params) return NULL; + if (!model->forward) return NULL; +#ifdef USE_TT + if (pn_qwen35moe_tt_select_rows(model, params, false) > 0u) { + params->dense_row = pn_qwen35moe_tt_dense_row; + params->dense_ctx = model; + } +#endif + + /* RWKV-X path: use internal trie tokenizer */ + if (model->config.arch == PN_ARCH_RWKVX) { + uint32_t prompt_buf[8192]; + int n_prompt = pn_rwkvx_encode(model->weights, prompt, prompt_buf, 8192); + if (n_prompt == 0) return NULL; + + fprintf(stderr, "generate: prompt tokenized to %d tokens\n", n_prompt); + + int max_recent = params->repeat_last_n; + uint32_t *recent = calloc(max_recent, sizeof(uint32_t)); + int n_recent = 0; + + /* Prefill */ + tm_tensor *logits = NULL; + for (int i = 0; i < n_prompt; i++) { + if (logits) tm_free(logits); + logits = model->forward(model, prompt_buf[i], state); + if (!logits) { free(recent); return NULL; } + + if (n_recent < max_recent) + recent[n_recent++] = prompt_buf[i]; + else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = prompt_buf[i]; + } + } + + /* Decode loop */ + int *output_tokens = malloc(max_tokens * sizeof(int)); + int n_output = 0; + int eos_id = pn_rwkvx_eos_id(model->weights); + + for (int i = 0; i < max_tokens; i++) { + if (!logits) break; + float *logit_data = (float *)logits->data; + uint32_t next_token = sample_token(logit_data, model->config.n_vocab, + params, recent, n_recent); + tm_free(logits); + logits = NULL; + + if ((int)next_token == eos_id) break; + + output_tokens[n_output++] = (int)next_token; + + if (n_recent < max_recent) + recent[n_recent++] = next_token; + else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next_token; + } + + logits = model->forward(model, next_token, state); + } + + if (logits) tm_free(logits); + free(recent); + + /* Decode all output tokens at once */ + char *output = pn_rwkvx_decode(model->weights, (uint32_t*)output_tokens, n_output); + free(output_tokens); + return output; + } + + /* GGUF path: use ft_tokenizer */ + if (!model->tokenizer) return NULL; + + int is_bpe = (ft_get_model_type(model->tokenizer) == 0); + + /* BPE: qwen35 pre-tokenization + GPT-2 byte BPE (matches llama.cpp). + * SentencePiece: direct encode with BOS. */ + int n_prompt_tokens = 0; + int *prompt_tokens; + if (is_bpe) { + prompt_tokens = pn_bpe_encode(model->tokenizer, prompt, &n_prompt_tokens); + } else { + prompt_tokens = ft_encode_with_special(model->tokenizer, prompt, + &n_prompt_tokens, + true, /* add BOS for SentencePiece */ + false); /* no EOS */ + } + if (!prompt_tokens || n_prompt_tokens == 0) { + fprintf(stderr, "generate: tokenization failed\n"); + free(prompt_tokens); + return NULL; + } + + fprintf(stderr, "generate: prompt tokenized to %d tokens\n", n_prompt_tokens); + + /* The exact ids, for a harness that takes token ids rather than text. */ + if (getenv("PN_PROMPT_TOKEN_DUMP")) { + fprintf(stderr, "PROMPT_TOKENS %d:", n_prompt_tokens); + for (int t = 0; t < n_prompt_tokens; t++) + fprintf(stderr, " %d", prompt_tokens[t]); + fprintf(stderr, "\n"); + } + + int max_recent = params->repeat_last_n; + uint32_t *recent = calloc(max_recent, sizeof(uint32_t)); + int n_recent = 0; + + /* Prefill — the batched card path first; it refuses without touching the state and the + * per-token loop below is what runs then. `recent` is seeded from the prompt either way. */ + tm_tensor *logits = pn_prefill_tt_batched(model, state, prompt_tokens, n_prompt_tokens); + int prefill_batched = (logits != NULL); + /* The repeat-penalty window is PROMPT state, not forward state: seed it from the same + * tokens whichever prefill ran, or a batched turn would sample under a different penalty + * than a sequential one and the difference would look like a card defect. */ + for (int i = 0; prefill_batched && i < n_prompt_tokens; i++) { + if (prompt_tokens[i] < 0) continue; + if (n_recent < max_recent) { + recent[n_recent++] = (uint32_t)prompt_tokens[i]; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = (uint32_t)prompt_tokens[i]; + } + } + for (int i = 0; !prefill_batched && i < n_prompt_tokens; i++) { + if (prompt_tokens[i] < 0) continue; + + if (logits) tm_free(logits); + logits = model->forward(model, (uint32_t)prompt_tokens[i], state); + if (!logits) { + fprintf(stderr, "generate: forward pass failed at token %d\n", i); + free(prompt_tokens); + free(recent); + return NULL; + } + + if (n_recent < max_recent) { + recent[n_recent++] = (uint32_t)prompt_tokens[i]; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = (uint32_t)prompt_tokens[i]; + } + } + + free(prompt_tokens); + + /* Decode loop */ + int *output_tokens = malloc(max_tokens * sizeof(int)); + int n_output = 0; + + int eos_id = ft_eos_id(model->tokenizer); + + for (int i = 0; i < max_tokens; i++) { + if (!logits) break; + + float *logit_data = (float *)logits->data; + uint32_t next_token = sample_token(logit_data, model->config.n_vocab, + params, recent, n_recent); + tm_free(logits); + logits = NULL; + + if (eos_id >= 0 && (int)next_token == eos_id) break; + + output_tokens[n_output++] = (int)next_token; + + if (n_recent < max_recent) { + recent[n_recent++] = next_token; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next_token; + } + + logits = model->forward(model, next_token, state); + } + + if (logits) tm_free(logits); + free(recent); + + char *raw_output = ft_decode(model->tokenizer, output_tokens, n_output); + free(output_tokens); + + if (is_bpe) { + char *output = gpt2_bytes_decode(raw_output); + free(raw_output); + return output; + } + return raw_output; +} + +int pn_stream(pn_model *model, pn_state *state, + const char *prompt, int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data) { + if (!model || !state || !prompt || !params || !callback) return -1; + if (!model->forward) return -1; +#ifdef USE_TT + if (pn_qwen35moe_tt_select_rows(model, params, false) > 0u) { + params->dense_row = pn_qwen35moe_tt_dense_row; + params->dense_ctx = model; + } +#endif + + /* RWKV-X path: use internal trie tokenizer */ + if (model->config.arch == PN_ARCH_RWKVX) { + uint32_t prompt_buf[8192]; + int n_prompt = pn_rwkvx_encode(model->weights, prompt, prompt_buf, 8192); + if (n_prompt == 0) return -1; + + int max_recent = params->repeat_last_n; + uint32_t *recent = calloc(max_recent, sizeof(uint32_t)); + int n_recent = 0; + + /* Prefill */ + tm_tensor *logits = NULL; + for (int i = 0; i < n_prompt; i++) { + if (logits) tm_free(logits); + logits = model->forward(model, prompt_buf[i], state); + if (!logits) { free(recent); return -1; } + + if (n_recent < max_recent) + recent[n_recent++] = prompt_buf[i]; + else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = prompt_buf[i]; + } + } + + /* Decode with streaming */ + int n_generated = 0; + int eos_id = pn_rwkvx_eos_id(model->weights); + + for (int i = 0; i < max_tokens; i++) { + if (!logits) break; + float *logit_data = (float *)logits->data; + uint32_t next_token = sample_token(logit_data, model->config.n_vocab, + params, recent, n_recent); + tm_free(logits); + logits = NULL; + + if ((int)next_token == eos_id) break; + + n_generated++; + + char *str = pn_rwkvx_decode(model->weights, &next_token, 1); + if (str) { callback(str, user_data); free(str); } + + if (n_recent < max_recent) + recent[n_recent++] = next_token; + else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next_token; + } + + logits = model->forward(model, next_token, state); + } + + if (logits) tm_free(logits); + free(recent); + return n_generated; + } + + /* GGUF path */ + if (!model->tokenizer) return -1; + + int is_bpe = (ft_get_model_type(model->tokenizer) == 0); + + int n_prompt_tokens = 0; + int *prompt_tokens; + if (is_bpe) { + prompt_tokens = pn_bpe_encode(model->tokenizer, prompt, &n_prompt_tokens); + } else { + prompt_tokens = ft_encode_with_special(model->tokenizer, prompt, + &n_prompt_tokens, + true, /* add BOS for SentencePiece */ + false); /* no EOS */ + } + if (!prompt_tokens || n_prompt_tokens == 0) { + free(prompt_tokens); + return -1; + } + + int max_recent = params->repeat_last_n; + uint32_t *recent = calloc(max_recent, sizeof(uint32_t)); + int n_recent = 0; + + /* Prefill — the batched card path first; it refuses without touching the state and the + * per-token loop below is what runs then. `recent` is seeded from the prompt either way. */ + tm_tensor *logits = pn_prefill_tt_batched(model, state, prompt_tokens, n_prompt_tokens); + int prefill_batched = (logits != NULL); + /* The repeat-penalty window is PROMPT state, not forward state: seed it from the same + * tokens whichever prefill ran, or a batched turn would sample under a different penalty + * than a sequential one and the difference would look like a card defect. */ + for (int i = 0; prefill_batched && i < n_prompt_tokens; i++) { + if (prompt_tokens[i] < 0) continue; + if (n_recent < max_recent) { + recent[n_recent++] = (uint32_t)prompt_tokens[i]; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = (uint32_t)prompt_tokens[i]; + } + } + for (int i = 0; !prefill_batched && i < n_prompt_tokens; i++) { + if (prompt_tokens[i] < 0) continue; + + if (logits) tm_free(logits); + logits = model->forward(model, (uint32_t)prompt_tokens[i], state); + if (!logits) { + free(prompt_tokens); + free(recent); + return -1; + } + + if (n_recent < max_recent) { + recent[n_recent++] = (uint32_t)prompt_tokens[i]; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = (uint32_t)prompt_tokens[i]; + } + } + free(prompt_tokens); + + /* Decode with streaming */ + int n_generated = 0; + int eos_id = ft_eos_id(model->tokenizer); + + for (int i = 0; i < max_tokens; i++) { + if (!logits) break; + + float *logit_data = (float *)logits->data; + uint32_t next_token = sample_token(logit_data, model->config.n_vocab, + params, recent, n_recent); + tm_free(logits); + logits = NULL; + + if (eos_id >= 0 && (int)next_token == eos_id) break; + + n_generated++; + + int tok_id = (int)next_token; + char *raw_str = ft_decode(model->tokenizer, &tok_id, 1); + if (raw_str) { + if (is_bpe) { + char *tok_str = gpt2_bytes_decode(raw_str); + free(raw_str); + if (tok_str) { + callback(tok_str, user_data); + free(tok_str); + } + } else { + callback(raw_str, user_data); + free(raw_str); + } + } + + if (n_recent < max_recent) { + recent[n_recent++] = next_token; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next_token; + } + + logits = model->forward(model, next_token, state); + } + + if (logits) tm_free(logits); + free(recent); + + return n_generated; +} + +/* ============================================================ + * Multi-modal inference API (Phase 3.9) + * + * Infrastructure for audio, vision, and embedding models. + * Text models work immediately via pn_infer() → PN_OUT_LOGITS. + * Task-specific functions return "not supported" until + * model phases (3.10–3.21) implement them. + * ============================================================ */ + +pn_output* pn_infer(pn_model *model, pn_state *state, uint32_t token) { + if (!model || !state || !model->forward) return NULL; + + tm_tensor *logits = model->forward(model, token, state); + if (!logits) return NULL; + + pn_output *out = calloc(1, sizeof(pn_output)); + if (!out) { tm_free(logits); return NULL; } + + int n = logits->shape[0]; + out->type = PN_OUT_LOGITS; + out->data = malloc(n * sizeof(float)); + if (!out->data) { tm_free(logits); free(out); return NULL; } + memcpy(out->data, logits->data, n * sizeof(float)); + out->n = n; + + tm_free(logits); + return out; +} + +void pn_output_free(pn_output *out) { + if (!out) return; + free(out->data); + free(out); +} + +char* pn_transcribe(pn_model *model, pn_state *state, + const float *audio_samples, int n_samples, + int sample_rate) { + (void)model; (void)state; (void)audio_samples; (void)n_samples; (void)sample_rate; + fprintf(stderr, "pn_transcribe: not yet supported (requires Whisper model, Phase 3.12)\n"); + return NULL; +} + +int pn_synthesize(pn_model *model, pn_state *state, + const char *text, float **out_samples, + int *out_sample_rate) { + (void)model; (void)state; (void)text; (void)out_samples; (void)out_sample_rate; + fprintf(stderr, "pn_synthesize: not yet supported (requires TTS model, Phase 3.14/3.15)\n"); + return -1; +} + +int pn_attach_vision(pn_model *model, const char *mmproj_path) { + if (!model || !mmproj_path) return -1; + pn_gguf_file *g = pn_gguf_open(mmproj_path); + if (!g) { fprintf(stderr, "pn_attach_vision: cannot open %s\n", mmproj_path); return -1; } + pn_vision_model *v = pn_vision_load(g); + pn_gguf_close(g); /* vision weights are dequantized to owned F32 copies */ + if (!v) return -1; + model->vision = v; + return 0; +} + +/* Build the ChatML vision turn (im_start user \n vstart image_pad*n_img vend + * prompt im_end \n im_start assistant \n) with the supplied [n_img x n_embd] + * decoder-space embeds spliced at the <|image_pad|> positions, reset the state, + * batched-prefill its KV cache, and return the last-position logits (caller + * tm_free's). The embeds buffer is NOT freed (caller owns it). NULL on error. */ +static tm_tensor* vlm_prefill(pn_model *model, pn_state *state, + const float *img_embds, int n_img, const char *prompt) { + int im_start = ft_token_to_id(model->tokenizer, "<|im_start|>"); + int im_end = ft_token_to_id(model->tokenizer, "<|im_end|>"); + int vstart = ft_token_to_id(model->tokenizer, "<|vision_start|>"); + int vend = ft_token_to_id(model->tokenizer, "<|vision_end|>"); + int image_pad = ft_token_to_id(model->tokenizer, "<|image_pad|>"); + + int cap = n_img + 64, ns = 0; + int *seq = malloc((size_t)cap * sizeof(int)); + if (!seq) return NULL; + #define PUSH(id) do { if (ns >= cap) { cap *= 2; seq = realloc(seq, (size_t)cap * sizeof(int)); } seq[ns++] = (id); } while (0) + #define PUSH_TEXT(s) do { int L_ = 0; int *t_ = pn_bpe_encode(model->tokenizer, (s), &L_); for (int i_ = 0; i_ < L_; i_++) PUSH(t_[i_]); free(t_); } while (0) + + if (im_start >= 0) PUSH(im_start); + PUSH_TEXT("user\n"); + if (vstart >= 0) PUSH(vstart); + for (int i = 0; i < n_img; i++) PUSH(image_pad); + if (vend >= 0) PUSH(vend); + if (prompt && *prompt) PUSH_TEXT(prompt); + if (im_end >= 0) PUSH(im_end); + PUSH_TEXT("\n"); + if (im_start >= 0) PUSH(im_start); + PUSH_TEXT("assistant\n"); + #undef PUSH_TEXT + + pn_state_reset(state); + int n_embd = model->config.n_embd, img_k = 0; + tm_tensor *logits = NULL; + + uint32_t *ptoks = malloc((size_t)ns * sizeof(uint32_t)); + const float **prows = calloc((size_t)ns, sizeof(float *)); + if (ptoks && prows) { + for (int i = 0; i < ns; i++) { + if (seq[i] == image_pad && img_k < n_img) { + prows[i] = img_embds + (size_t)(img_k++) * n_embd; + ptoks[i] = 0; + } else { + ptoks[i] = (uint32_t)seq[i]; + } + } + logits = pn_llama_prefill(model, ptoks, prows, ns, state); + } + free(ptoks); + free(prows); + + if (!logits) { + /* Fallback: sequential single-token prefill. */ + img_k = 0; + for (int i = 0; i < ns; i++) { + if (logits) tm_free(logits); + if (seq[i] == image_pad && img_k < n_img) { + state->embd_override = img_embds + (size_t)(img_k++) * n_embd; + logits = model->forward(model, 0, state); + state->embd_override = NULL; + } else { + logits = model->forward(model, (uint32_t)seq[i], state); + } + if (!logits) { free(seq); return NULL; } + } + } + free(seq); + return logits; +} + +/* Vision-language decode from PRE-SUPPLIED image embeddings: this is exactly + * what pn_describe_stream does AFTER the vision encode. Splice the supplied + * [n_img x embd_dim] decoder-space embeds into a ChatML user turn at the + * <|image_pad|> positions, batched-prefill, and decode the assistant + * transcription. The embeds buffer is NOT freed here (the caller owns it), so a + * Core ML vision bridge can produce them and reuse the buffer. Returns newly + * allocated text (caller frees with free()); NULL on error. */ +char* pn_describe_embeds(pn_model *model, pn_state *state, + const float *img_embds, int n_img, int embd_dim, + const char *prompt, int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data, + pn_stop_callback should_stop, void *stop_data) { + if (!model || !state || !img_embds || n_img <= 0) return NULL; + if (embd_dim != model->config.n_embd) { + fprintf(stderr, "pn_describe_embeds: embd_dim %d != n_embd %d\n", + embd_dim, model->config.n_embd); + return NULL; + } + + tm_tensor *logits = vlm_prefill(model, state, img_embds, n_img, prompt); + if (!logits) return NULL; + + int im_end = ft_token_to_id(model->tokenizer, "<|im_end|>"); + int max_recent = params->repeat_last_n > 0 ? params->repeat_last_n : 64; + uint32_t *recent = calloc(max_recent, sizeof(uint32_t)); + int n_recent = 0; + int *out = malloc((size_t)max_tokens * sizeof(int)), no = 0; + int eos = ft_eos_id(model->tokenizer); + for (int i = 0; i < max_tokens && logits; i++) { + float *ld = (float *)logits->data; + uint32_t nt = sample_token(ld, model->config.n_vocab, params, recent, n_recent); + if (getenv("PN_VLM_DEBUG") && i < 6) { + /* show the top-3 logits + the sampled id to diagnose empty output */ + int a = 0, b = 0, cc = 0; + for (int v = 1; v < model->config.n_vocab; v++) { + if (ld[v] > ld[a]) { cc = b; b = a; a = v; } + else if (ld[v] > ld[b]) { cc = b; b = v; } + else if (ld[v] > ld[cc]) cc = v; + } + fprintf(stderr, " step %d: sampled=%u top=[%d %.2f, %d %.2f, %d %.2f] (im_end=%d eos=%d)\n", + i, nt, a, ld[a], b, ld[b], cc, ld[cc], im_end, eos); + } + tm_free(logits); + logits = NULL; + if ((eos >= 0 && (int)nt == eos) || (int)nt == im_end) break; + out[no++] = (int)nt; + if (n_recent < max_recent) recent[n_recent++] = nt; + else { memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); recent[max_recent - 1] = nt; } + if (callback) { + /* Emit the full decoded text so far. Decoding the whole accumulated + * sequence each step keeps qwen byte-level BPE valid (a partial + * multi-byte character simply is not shown until it completes). */ + char *raw_i = ft_decode(model->tokenizer, out, no); + char *txt_i = gpt2_bytes_decode(raw_i); + free(raw_i); + if (txt_i) { callback(txt_i, user_data); free(txt_i); } + } + if (should_stop && should_stop(stop_data)) break; // abort generation early + logits = model->forward(model, nt, state); + } + if (logits) tm_free(logits); + free(recent); + char *raw = ft_decode(model->tokenizer, out, no); + free(out); + char *text = gpt2_bytes_decode(raw); /* qwen byte-level BPE -> UTF-8 */ + free(raw); + return text; +} + +/* Prefill a state's KV cache from pre-supplied image embeds (ChatML vision turn) + * and sample the first generated token, WITHOUT running the decode loop. Leaves + * state ready to be driven by pn_batch_decoder / pn_llama_forward_batch. Returns + * 0 and writes *first_tok_out on success, -1 on error. */ +int pn_vlm_prefill_embeds(pn_model *model, pn_state *state, + const float *img_embds, int n_img, int embd_dim, + const char *prompt, pn_sampling_params *params, + uint32_t *first_tok_out) { + if (!model || !state || !img_embds || n_img <= 0 || !first_tok_out) return -1; + if (embd_dim != model->config.n_embd) return -1; + tm_tensor *logits = vlm_prefill(model, state, img_embds, n_img, prompt); + if (!logits) return -1; + pn_sampling_params sp = params ? *params : pn_sampling_defaults(); + *first_tok_out = sample_token((float *)logits->data, model->config.n_vocab, &sp, NULL, 0); + tm_free(logits); + return 0; +} + +/* ============================================================ + * Continuous-batch VLM decoder + * + * Drives pn_llama_forward_batch over up to max_batch prefilled sequences, + * sampling + EOS + detokenization per sequence, so a caller can keep the batch + * full (continuous batching): admit a prefilled page into any free slot, call + * step() repeatedly, and each finished page is returned (with its text and its + * recyclable state) so a fresh page can take the slot. + * ============================================================ */ + +typedef struct { + pn_state *state; + int page_id; + uint32_t pending; /* sampled-but-not-yet-processed token */ + int *out; /* generated token ids */ + int no, ocap; + uint32_t *recent; /* repeat-penalty ring */ + int n_recent; + int in_use; + /* Speculative-decode fields (used only by pn_batch_decoder_step_spec). */ + pn_draft_fn draft_fn; /* per-slot draft provider (NULL = plain K=1 slot) */ + void *draft_ud; + int spec_on; /* 1 while this slot still drafts (per-page fallback) */ + int spec_proved;/* 1 once any cycle accepted >= min_accept_frac */ + long passes; /* speculative cycles this slot has run */ + long drafted; /* draft tokens proposed for this slot */ + long accepted; /* draft tokens accepted for this slot */ +} pn_batch_slot; + +struct pn_batch_decoder { + pn_model *model; + int max_batch; + int max_tokens; + pn_sampling_params params; + int max_recent; + int eos, im_end; + pn_batch_slot *slots; + /* scratch reused across steps */ + pn_state **act_states; + uint32_t *act_toks; + int *act_slot; + float *logits; + /* Speculative-decode config + ragged verify scratch (allocated by + * pn_batch_decoder_spec_config; NULL until then). */ + int spec_configured; + int max_k; + double min_accept_frac; + int probe_cycles; + uint32_t *cat_toks; /* [PN_QMM_MAX_B] concatenated block tokens */ + pn_state **blk_states; /* [max_batch] one state per block */ + int *blk_len; /* [max_batch] rows per block (1 + drafts) */ + int *blk_slot; /* [max_batch] slot index per block */ + float *spec_logits; /* [PN_QMM_MAX_B * n_vocab] per-row logits */ + /* Aggregate speculative-decode accounting (observability). */ + long sp_passes; /* verify_batch passes actually run */ + long sp_drafted; /* draft tokens proposed */ + long sp_accepted; /* draft tokens accepted */ +}; + +pn_batch_decoder* pn_batch_decoder_new(pn_model *model, int max_batch, + int max_tokens, const pn_sampling_params *params) { + if (!model || max_batch <= 0) return NULL; + pn_batch_decoder *d = calloc(1, sizeof(*d)); + if (!d) return NULL; + d->model = model; + d->max_batch = max_batch; + d->max_tokens = max_tokens > 0 ? max_tokens : 4096; + d->params = params ? *params : pn_sampling_defaults(); + d->max_recent = d->params.repeat_last_n > 0 ? d->params.repeat_last_n : 64; + d->eos = ft_eos_id(model->tokenizer); + d->im_end = ft_token_to_id(model->tokenizer, "<|im_end|>"); + d->slots = calloc(max_batch, sizeof(pn_batch_slot)); + d->act_states = malloc((size_t)max_batch * sizeof(pn_state *)); + d->act_toks = malloc((size_t)max_batch * sizeof(uint32_t)); + d->act_slot = malloc((size_t)max_batch * sizeof(int)); + d->logits = malloc((size_t)max_batch * model->config.n_vocab * sizeof(float)); + if (!d->slots || !d->act_states || !d->act_toks || !d->act_slot || !d->logits) { + pn_batch_decoder_free(d); return NULL; + } + for (int i = 0; i < max_batch; i++) { + d->slots[i].ocap = d->max_tokens; + d->slots[i].out = malloc((size_t)d->max_tokens * sizeof(int)); + d->slots[i].recent = calloc(d->max_recent, sizeof(uint32_t)); + if (!d->slots[i].out || !d->slots[i].recent) { pn_batch_decoder_free(d); return NULL; } + } + return d; +} + +void pn_batch_decoder_free(pn_batch_decoder *d) { + if (!d) return; + if (d->slots) for (int i = 0; i < d->max_batch; i++) { + free(d->slots[i].out); free(d->slots[i].recent); + } + free(d->slots); free(d->act_states); free(d->act_toks); + free(d->act_slot); free(d->logits); + free(d->cat_toks); free(d->blk_states); free(d->blk_len); + free(d->blk_slot); free(d->spec_logits); + free(d); +} + +int pn_batch_decoder_capacity(const pn_batch_decoder *d) { return d ? d->max_batch : 0; } + +int pn_batch_decoder_active(const pn_batch_decoder *d) { + if (!d) return 0; + int n = 0; + for (int i = 0; i < d->max_batch; i++) if (d->slots[i].in_use) n++; + return n; +} + +int pn_batch_decoder_admit(pn_batch_decoder *d, pn_state *state, int page_id, + uint32_t first_tok) { + if (!d || !state) return -1; + for (int i = 0; i < d->max_batch; i++) { + pn_batch_slot *s = &d->slots[i]; + if (s->in_use) continue; + s->state = state; s->page_id = page_id; + s->pending = first_tok; s->no = 0; s->n_recent = 0; s->in_use = 1; + return 0; + } + return -1; /* full */ +} + +/* Finalize a slot: detokenize its output to UTF-8, hand back page_id/state/text, + * and free the slot for reuse. */ +static void batch_finalize(pn_batch_decoder *d, pn_batch_slot *s, pn_page_out *o) { + char *raw = ft_decode(d->model->tokenizer, s->out, s->no); + char *text = raw ? gpt2_bytes_decode(raw) : NULL; + free(raw); + o->page_id = s->page_id; + o->state = s->state; + o->text = text ? text : strdup(""); + s->in_use = 0; s->state = NULL; +} + +int pn_batch_decoder_step(pn_batch_decoder *d, pn_page_out *out, int max_out) { + if (!d) return 0; + int nf = 0; + + /* 1. Process each slot's pending token: EOS/max -> finish; else push it and + * enqueue the slot for the batched forward that produces its next token. */ + int na = 0; + for (int i = 0; i < d->max_batch; i++) { + pn_batch_slot *s = &d->slots[i]; + if (!s->in_use) continue; + int done = ((d->eos >= 0 && (int)s->pending == d->eos) || + (int)s->pending == d->im_end || s->no >= d->max_tokens); + if (done) { + if (nf < max_out) batch_finalize(d, s, &out[nf++]); + continue; + } + s->out[s->no++] = (int)s->pending; + if (s->n_recent < d->max_recent) s->recent[s->n_recent++] = s->pending; + else { memmove(s->recent, s->recent + 1, (d->max_recent - 1) * sizeof(uint32_t)); + s->recent[d->max_recent - 1] = s->pending; } + d->act_states[na] = s->state; + d->act_toks[na] = s->pending; + d->act_slot[na] = i; + na++; + } + + /* 2. One batched decode step over the continuing slots; sample each slot's + * next pending token. + * + * NOTE ON DETERMINISM: sampling is kept SERIAL on purpose. Each slot's + * logits / recent-token ring / candidate set are independent, but the + * random draw goes through the process-global rand()/srand() state (see + * rand_float()). Parallelizing the per-slot draws would make the order in + * which slots consume rand() nondeterministic and change the sampled + * tokens run-to-run, breaking reproducibility. Because the per-call + * sampler is now O(V) (no O(k*V) sort, no repeated full-vocab top-p + * passes, no per-call mallocs), the serial loop is already much cheaper. + * If a per-slot deterministic RNG is introduced, this loop can be + * parallelized over the thread pool safely. */ + if (na > 0) { + if (pn_llama_forward_batch(d->model, d->act_toks, NULL, d->act_states, na, d->logits) != 0) + return -1; /* unsupported: caller falls back */ + int nv = d->model->config.n_vocab; + for (int j = 0; j < na; j++) { + pn_batch_slot *s = &d->slots[d->act_slot[j]]; + s->pending = sample_token(d->logits + (size_t)j * nv, nv, &d->params, + s->recent, s->n_recent); + } + } + return nf; +} + +/* Finalize ALL active slots with whatever they have decoded so far (for early + * stop / drain). Returns the number written to out[]. */ +int pn_batch_decoder_flush(pn_batch_decoder *d, pn_page_out *out, int max_out) { + if (!d) return 0; + int nf = 0; + for (int i = 0; i < d->max_batch && nf < max_out; i++) { + pn_batch_slot *s = &d->slots[i]; + if (s->in_use) batch_finalize(d, s, &out[nf++]); + } + return nf; +} + +/* ============================================================ + * Batched lossless speculative decode (pn_batch_decoder_step_spec) + * + * One verify pass over the concatenation of every active slot's + * [pending, drafts...] block, then a per-slot greedy accept scan. A slot with + * no drafts (K=1 block) reduces EXACTLY to pn_batch_decoder_step's per-slot + * behavior: push the pending seed, verify one row, and set the next pending to + * the penalized argmax of that row over the same recent-token ring. So the + * emitted token stream is bit-identical to plain batched greedy decode for any + * draft; drafts only fold multiple plain steps into one verify pass. + * ============================================================ */ + +/* Slide one token into a slot's repeat-penalty ring, byte-for-byte as the plain + * step does (append until full, then shift). */ +static void spec_ring_push(pn_batch_decoder *d, pn_batch_slot *s, uint32_t tok) { + if (s->n_recent < d->max_recent) { + s->recent[s->n_recent++] = tok; + } else { + memmove(s->recent, s->recent + 1, (d->max_recent - 1) * sizeof(uint32_t)); + s->recent[d->max_recent - 1] = tok; + } +} + +/* Greedy argmax after applying the repeat penalty, a verbatim copy of + * sample_token's temperature<=0 path (penalty loop over the recent ring, then + * argmax). Mutates logits[] in place, exactly as sample_token would, so the + * accepted token matches plain greedy decode bit for bit. */ +static uint32_t penalized_argmax(float *logits, int n_vocab, pn_sampling_params *params, + uint32_t *recent_tokens, int n_recent) { + if (params->repeat_penalty != 1.0f && recent_tokens && n_recent > 0) { + for (int i = 0; i < n_recent; i++) { + uint32_t tok = recent_tokens[i]; + if (tok < (uint32_t)n_vocab) { + if (logits[tok] > 0.0f) + logits[tok] /= params->repeat_penalty; + else + logits[tok] *= params->repeat_penalty; + } + } + } + int best = 0; + for (int i = 1; i < n_vocab; i++) + if (logits[i] > logits[best]) best = i; + return (uint32_t)best; +} + +int pn_batch_decoder_spec_config(pn_batch_decoder *d, int max_k, + double min_accept_frac, int probe_cycles) { + if (!d) return -1; + /* Speculation is only lossless for greedy decode. */ + if (d->params.temperature > 0.0f) return -1; + if (max_k < 0) max_k = 0; + /* A block is 1 seed + up to max_k drafts and the whole step is capped at + * PN_QMM_MAX_B verify rows, so a single block can hold at most + * PN_QMM_MAX_B - 1 drafts. */ + if (max_k > PN_QMM_MAX_B - 1) max_k = PN_QMM_MAX_B - 1; + d->max_k = max_k; + d->min_accept_frac = min_accept_frac; + d->probe_cycles = probe_cycles; + + int nv = d->model->config.n_vocab; + free(d->cat_toks); free(d->blk_states); free(d->blk_len); + free(d->blk_slot); free(d->spec_logits); + d->cat_toks = malloc((size_t)PN_QMM_MAX_B * sizeof(uint32_t)); + d->blk_states = malloc((size_t)d->max_batch * sizeof(pn_state *)); + d->blk_len = malloc((size_t)d->max_batch * sizeof(int)); + d->blk_slot = malloc((size_t)d->max_batch * sizeof(int)); + /* Total verify rows are capped at PN_QMM_MAX_B, so PN_QMM_MAX_B rows of + * logits is always enough regardless of max_batch / max_k. */ + d->spec_logits = malloc((size_t)PN_QMM_MAX_B * (size_t)nv * sizeof(float)); + if (!d->cat_toks || !d->blk_states || !d->blk_len || !d->blk_slot || !d->spec_logits) { + free(d->cat_toks); free(d->blk_states); free(d->blk_len); + free(d->blk_slot); free(d->spec_logits); + d->cat_toks = NULL; d->blk_states = NULL; d->blk_len = NULL; + d->blk_slot = NULL; d->spec_logits = NULL; + return -1; + } + d->sp_passes = 0; d->sp_drafted = 0; d->sp_accepted = 0; + d->spec_configured = 1; + return 0; +} + +void pn_batch_decoder_spec_stats(const pn_batch_decoder *d, long *passes, + long *drafted, long *accepted) { + if (!d) return; + if (passes) *passes = d->sp_passes; + if (drafted) *drafted = d->sp_drafted; + if (accepted) *accepted = d->sp_accepted; +} + +int pn_batch_decoder_admit_spec(pn_batch_decoder *d, pn_state *state, int page_id, + uint32_t first_tok, pn_draft_fn draft_fn, void *draft_ud) { + if (!d || !state || !d->spec_configured) return -1; + for (int i = 0; i < d->max_batch; i++) { + pn_batch_slot *s = &d->slots[i]; + if (s->in_use) continue; + s->state = state; s->page_id = page_id; + s->pending = first_tok; s->no = 0; s->n_recent = 0; s->in_use = 1; + s->draft_fn = draft_fn; s->draft_ud = draft_ud; + s->spec_on = (draft_fn != NULL && d->max_k > 0); + s->spec_proved = 0; + s->passes = 0; s->drafted = 0; s->accepted = 0; + return 0; + } + return -1; /* full */ +} + +/* Append a committed (accepted) token to a slot's output + ring, honoring the + * SAME stop conditions as plain step: EOS / <|im_end|> / max_tokens are never + * appended — they are routed back through `pending` so the slot finalizes on the + * next step exactly like plain decode. Returns 1 if a stop was hit (caller must + * stop emitting this block), 0 otherwise. */ +static int spec_emit_committed(pn_batch_decoder *d, pn_batch_slot *s, uint32_t tok) { + if ((d->eos >= 0 && (int)tok == d->eos) || (int)tok == d->im_end || + s->no >= d->max_tokens) { + s->pending = tok; /* finalize next step; not appended (matches plain) */ + return 1; + } + s->out[s->no++] = (int)tok; + spec_ring_push(d, s, tok); + return 0; +} + +int pn_batch_decoder_step_spec(pn_batch_decoder *d, pn_page_out *out, int max_out) { + if (!d) return 0; + if (!d->spec_configured) return -1; + int nv = d->model->config.n_vocab; + int nf = 0; + + /* Phase 1: process each slot's pending token. Finish on EOS/max exactly like + * plain step; otherwise push the pending "seed", ask the slot's draft_fn for + * up to K drafts (clamped to max_k, the remaining verify-row budget, and the + * no-wraparound budget), and lay [seed, drafts...] into the concatenated + * block buffer. */ + int nblk = 0; /* number of blocks */ + int M = 0; /* total verify rows queued so far */ + for (int i = 0; i < d->max_batch; i++) { + pn_batch_slot *s = &d->slots[i]; + if (!s->in_use) continue; + + int done = ((d->eos >= 0 && (int)s->pending == d->eos) || + (int)s->pending == d->im_end || s->no >= d->max_tokens); + if (done) { + if (nf < max_out) batch_finalize(d, s, &out[nf++]); + continue; + } + + /* Guard the per-step verify-row cap BEFORE mutating the slot: each block + * needs at least its seed row. If the batch is already full (only when + * more than PN_QMM_MAX_B slots are active), leave this slot's pending in + * place untouched for a later step. */ + if (M + 1 > PN_QMM_MAX_B) continue; + + /* Push the pending seed into out + ring (position row 0 of the block). */ + s->out[s->no++] = (int)s->pending; + spec_ring_push(d, s, s->pending); + + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)s->state->kv_cache; + int budget_rows = PN_QMM_MAX_B - M - 1; /* draft rows left this step */ + int budget_wrap = kv->max_seq - s->state->pos - 1; /* no cache wraparound */ + int kmax = d->max_k; + if (kmax > budget_rows) kmax = budget_rows; + if (kmax > budget_wrap) kmax = budget_wrap; + if (kmax < 0) kmax = 0; + + int kk = 0; + if (s->spec_on && s->draft_fn && kmax > 0) { + /* draft_fn writes straight into the block, right after the seed. */ + kk = s->draft_fn((const uint32_t *)s->out, s->no, + d->cat_toks + M + 1, kmax, s->draft_ud); + if (kk < 0) kk = 0; + if (kk > kmax) kk = kmax; /* clamp to the requested budget */ + } + + d->cat_toks[M] = s->pending; /* seed = row 0 */ + d->blk_states[nblk] = s->state; + d->blk_len[nblk] = 1 + kk; + d->blk_slot[nblk] = i; + nblk++; + M += 1 + kk; + + s->passes++; + s->drafted += kk; + } + + if (nblk == 0) return nf; /* nothing to verify (all finished this step) */ + + /* Phase 2: one ragged verify pass over every block. */ + if (pn_llama_verify_batch(d->model, d->cat_toks, d->blk_states, d->blk_len, + nblk, d->spec_logits) != 0) + return -1; /* unsupported model: caller falls back to plain step */ + d->sp_passes++; + + /* Phase 3: per block, greedy accept scan. logits row (off+i) predicts the + * token AFTER block row i; row 0 predicts the token after the seed. Emit the + * penalized argmax per row, accept draft[i] iff it equals that argmax, stop + * at the first mismatch (the argmax is the correction). On full acceptance + * take the free bonus argmax. The committed tokens (all but the last emitted) + * go to out + ring now; the last emitted (correction or bonus) becomes the + * next pending. Commit pos over the seed + accepted drafts only. */ + int off = 0; + for (int b = 0; b < nblk; b++) { + pn_batch_slot *s = &d->slots[d->blk_slot[b]]; + int blk_len = d->blk_len[b]; + int kk = blk_len - 1; /* number of drafts in this block */ + int base_pos = s->state->pos; + const uint32_t *drafts = d->cat_toks + off + 1; + + int n_accept = 0, stopped = 0; + for (int i = 0; i < kk; i++) { + float *row = d->spec_logits + (size_t)(off + i) * nv; + uint32_t g = penalized_argmax(row, nv, &d->params, s->recent, s->n_recent); + if (g == drafts[i]) { + /* Accepted: g is a committed token; push to out + ring so the + * next row's penalty sees it, exactly as plain decode would. */ + n_accept++; + if (spec_emit_committed(d, s, g)) { stopped = 1; break; } + } else { + /* Mismatch: g is the correction and the last emitted token; it + * becomes the next pending (pushed to the ring next step). */ + s->pending = g; + stopped = 1; + break; + } + } + if (!stopped) { + /* Every draft accepted: bonus argmax after the last draft becomes the + * next pending. */ + float *row = d->spec_logits + (size_t)(off + kk) * nv; + s->pending = penalized_argmax(row, nv, &d->params, s->recent, s->n_recent); + } + + /* Commit the seed (base_pos) plus every accepted draft; their K/V is + * already in the cache from the verify pass. The correction/bonus stays + * uncommitted and is re-verified as the next seed. */ + s->state->pos = base_pos + 1 + n_accept; + s->accepted += n_accept; + d->sp_drafted += kk; + d->sp_accepted += n_accept; + + /* A page opens with a running header / page number that the draft source + * cannot match, so early cycles accept nothing; mark the slot "proved" + * once any single cycle clears min_accept_frac, which the body reliably + * does. Without this the cumulative rate stays low through the header and + * the probe below would drop a page that speculates well on its body. */ + if (kk > 0 && (double)n_accept / (double)kk >= d->min_accept_frac) + s->spec_proved = 1; + + /* Per-page fallback: after probe_cycles, drop a slot that has never once + * speculated well to plain single-token decode (it then rides as a K=1 + * block). A proved slot is never dropped. */ + if (s->spec_on && !s->spec_proved && s->passes >= d->probe_cycles && + s->drafted > 0 && + (double)s->accepted / (double)s->drafted < d->min_accept_frac) { + s->spec_on = 0; + } + + off += blk_len; + } + + return nf; +} + +/* Vision-language: encode the image (CPU flash-attention path), then delegate to + * pn_describe_embeds. Kept as the portable fallback when no external (Core ML) + * vision encoder is available. */ +char* pn_describe_stream(pn_model *model, pn_state *state, + const uint8_t *image_rgb, int width, int height, + const char *prompt, int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data, + pn_stop_callback should_stop, void *stop_data) { + if (!model || !state || !image_rgb) return NULL; + if (!model->vision) { fprintf(stderr, "pn_describe: no vision encoder attached\n"); return NULL; } + + float *img_embd = NULL; + int img_dim = 0; + int n_img = pn_vision_encode((pn_vision_model *)model->vision, image_rgb, width, height, + &img_embd, &img_dim); + if (n_img <= 0 || img_dim != model->config.n_embd) { + fprintf(stderr, "pn_describe: vision encode failed (n=%d dim=%d vs n_embd=%d)\n", + n_img, img_dim, model->config.n_embd); + free(img_embd); + return NULL; + } + char *text = pn_describe_embeds(model, state, img_embd, n_img, img_dim, + prompt, max_tokens, params, + callback, user_data, should_stop, stop_data); + free(img_embd); + return text; +} + +char* pn_describe(pn_model *model, pn_state *state, + const uint8_t *image_rgb, int width, int height, + const char *prompt, int max_tokens, + pn_sampling_params *params) { + return pn_describe_stream(model, state, image_rgb, width, height, + prompt, max_tokens, params, NULL, NULL, NULL, NULL); +} + +/* Run just the CPU vision encoder for one image, returning the decoder-space + * embeddings [n_img x n_embd] (caller frees *embeds_out with free()). This is + * the vision half of pn_describe_stream, split out so a bulk pipeline can stage + * embeds ahead of the batched decode (and mix with an external Core ML encoder). + * Returns 0 on success, -1 on error. */ +int pn_vision_encode_rgb(pn_model *model, const uint8_t *image_rgb, int width, int height, + float **embeds_out, int *n_img_out, int *embd_dim_out) { + if (!model || !image_rgb || !embeds_out || !n_img_out) return -1; + if (!model->vision) { fprintf(stderr, "pn_vision_encode_rgb: no vision encoder\n"); return -1; } + float *emb = NULL; + int dim = 0; + int n_img = pn_vision_encode((pn_vision_model *)model->vision, image_rgb, width, height, + &emb, &dim); + if (n_img <= 0 || dim != model->config.n_embd) { free(emb); return -1; } + *embeds_out = emb; + *n_img_out = n_img; + if (embd_dim_out) *embd_dim_out = dim; + return 0; +} + +float* pn_detect(pn_model *model, pn_state *state, + const uint8_t *image_rgb, int width, int height, + float confidence_threshold, int *n_boxes_out) { + (void)model; (void)state; (void)image_rgb; (void)width; (void)height; + (void)confidence_threshold; + if (n_boxes_out) *n_boxes_out = 0; + fprintf(stderr, "pn_detect: not yet supported (requires YOLO/RF-DETR model, Phase 3.18)\n"); + return NULL; +} + +float* pn_embed(pn_model *model, pn_state *state, + const char *text, int *n_dims_out) { + (void)model; (void)state; (void)text; + if (n_dims_out) *n_dims_out = 0; + fprintf(stderr, "pn_embed: not yet supported (requires CLIP/DINOv2 model, Phase 3.17)\n"); + return NULL; +} + +#ifdef USE_TT +/* SPECULATIVE SAMPLING's chooser (pn_spec_choose_fn): one verify row's sparse logits become the + * row a plain sparse decode step hands the sampler (-INFINITY everywhere the card did not + * name), and sample_token draws from it under the repeat window plain decode would have at + * that token — the cycle's own draws appended exactly as the emit loop appends them. */ +uint32_t pn_spec_sample_choose(void *opaque, const uint32_t *index, const float *value, + uint32_t count) +{ + pn_spec_sample_ctx *c = (pn_spec_sample_ctx *)opaque; + uint32_t i, token; + const int row = c->row_index++; + + for (i = 0; i < (uint32_t)c->n_vocab; i++) + c->row[i] = -INFINITY; + for (i = 0; i < count; i++) + if (index[i] < (uint32_t)c->n_vocab) + c->row[index[i]] = value[i]; + /* A row past 0 whose top-N cannot decide this draw: end the cycle there (the next + * cycle meets the same distribution as its row 0, which the dense refill covers). */ + if (row > 0 && !pn_sampler_topn_row_exact(c->row, c->n_vocab, c->params, c->recent, + c->n_recent)) + return PN_SPEC_CHOOSE_DEFER_TOKEN; + /* The sampler's sparse fast path (`PN_TT_SPEC_SAMPLE_SPARSE=0`: the full-row path). */ + t_sparse_sampler_force = !(getenv("PN_TT_SPEC_SAMPLE_SPARSE") && + atoi(getenv("PN_TT_SPEC_SAMPLE_SPARSE")) == 0); + token = sample_token(c->row, c->n_vocab, c->params, c->recent, c->n_recent); + t_sparse_sampler_force = 0; + if (c->max_recent > 0) { + if (c->n_recent < c->max_recent) { + c->recent[c->n_recent++] = token; + } else { + memmove(c->recent, c->recent + 1, (size_t)(c->max_recent - 1) * sizeof(uint32_t)); + c->recent[c->max_recent - 1] = token; + } + } + return token; +} +#endif + +int pn_stream_tokens(pn_model *model, pn_state *state, + const uint32_t *tokens, int n_tokens, + int max_tokens, + pn_sampling_params *params, + pn_token_callback callback, void *user_data) { + if (!model || !state || !tokens || n_tokens <= 0 || !params || !callback) return -1; + if (!model->forward) return -1; + if (!model->tokenizer && model->config.arch != PN_ARCH_RWKVX) return -1; + + int max_recent = params->repeat_last_n; + uint32_t *recent = calloc(max_recent, sizeof(uint32_t)); + int n_recent = 0; + + /* Prefill — the batched card path first, exactly as pn_generate() and pn_stream() do. + * THIS ENTRY POINT IS THE ONE THE CHAT CLI AND THE REPL TAKE, and it had a per-token + * loop of its own: the Ornith chunkwise prefill shipped at 24x on 2026-08-25 and never + * ran here once. A refusal returns NULL without touching the state and the loop below is + * what runs then, so this is an accelerator and not a behaviour fork. */ +#ifdef USE_TT + /* Top-N rows: the sampler swaps in the dense row whenever one cannot decide a draw. */ + if (pn_qwen35moe_tt_select_rows(model, params, false) > 0u) { + params->dense_row = pn_qwen35moe_tt_dense_row; + params->dense_ctx = model; + } +#endif + tm_tensor *logits = pn_prefill_tt_batched_tokens(model, state, tokens, n_tokens); + int prefill_batched = (logits != NULL); + /* The repeat-penalty window is PROMPT state, not forward state: seed it from the same + * tokens whichever prefill ran, or a batched turn would sample under a different penalty + * than a sequential one and the difference would look like a card defect. */ + for (int i = 0; prefill_batched && i < n_tokens; i++) { + if (n_recent < max_recent) { + recent[n_recent++] = tokens[i]; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = tokens[i]; + } + } + for (int i = 0; !prefill_batched && i < n_tokens; i++) { + if (logits) tm_free(logits); + logits = model->forward(model, tokens[i], state); + if (!logits) { + free(recent); + return -1; + } + + if (n_recent < max_recent) { + recent[n_recent++] = tokens[i]; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = tokens[i]; + } + } + + /* Decode with streaming */ + int n_generated = 0; + int eos_id; + int is_bpe = 0; + + if (model->config.arch == PN_ARCH_RWKVX) { + eos_id = pn_rwkvx_eos_id(model->weights); + } else { + eos_id = ft_eos_id(model->tokenizer); + is_bpe = (ft_get_model_type(model->tokenizer) == 0); + } + + /* STEP 4 S2: GREEDY SPECULATIVE DECODE on the Tenstorrent qwen35 forward + * (`PN_TT_SPEC=`, 0 = off). Lossless: only the model's own greedy tokens + * are ever emitted. Drafts: prompt-lookup n-grams over the whole history. */ +#ifdef USE_TT + int spec_k = 0; + /* SHIPPED DEFAULT 3 drafts (2026-10-06); `PN_TT_SPEC=0` off. Without an MTP head the + * qwen35 draft call returns -1 and the step decodes plainly (n-gram drafts only when + * PN_TT_SPEC is set explicitly). */ + /* Greedy speculation emits the verify rows' plain argmax: only when that IS the request's + * greedy pick (no logit bias, presence/frequency penalty or grammar hook). */ + if (pn_qwen35moe_tt_greedy_exact_on_topn(params) && + model->forward == pn_qwen35moe_forward_tt) + spec_k = getenv("PN_TT_SPEC") ? atoi(getenv("PN_TT_SPEC")) : 3; + /* SPECULATIVE SAMPLING (temperature > 0): the same MTP drafts, each row's token DRAWN by + * the engine's sampler (spec_sample_choose) — identical to plain sparse sampling, token + * for token and draw for draw. Only where plain decode's rows are the sparse top-N too + * (`PN_TT_SPARSE_LOGITS`); `PN_TT_SPEC_SAMPLE=0` off. No n-gram drafts here. */ + const bool spec_sampling = params->temperature > 0.0f && !params->tool_grammar && + model->forward == pn_qwen35moe_forward_tt && + !(getenv("PN_TT_SPEC_SAMPLE") && + atoi(getenv("PN_TT_SPEC_SAMPLE")) == 0) && + pn_qwen35moe_tt_spec_can_sample(model); + if (spec_sampling) + spec_k = getenv("PN_TT_SPEC") ? atoi(getenv("PN_TT_SPEC")) : 3; + const bool spec_ngram = getenv("PN_TT_SPEC") != NULL && !spec_sampling; + if (spec_k > 15) spec_k = 15; + pn_spec_sample_ctx spec_ctx = { params, NULL, model->config.n_vocab, NULL, 0, max_recent, 0 }; + if (spec_sampling && spec_k > 0) { + spec_ctx.row = malloc((size_t)model->config.n_vocab * sizeof(float)); + spec_ctx.recent = malloc((size_t)(max_recent > 0 ? max_recent : 1) * sizeof(uint32_t)); + if (!spec_ctx.row || !spec_ctx.recent) + spec_k = 0; + } +#else + int spec_k = 0; +#endif + uint32_t spec_pending[16]; + int spec_n_pending = 0, spec_at = 0; + int hist_cap = n_tokens + max_tokens + 1, n_hist = 0; + uint32_t *hist = spec_k > 0 ? malloc((size_t)hist_cap * sizeof(uint32_t)) : NULL; + if (hist) { memcpy(hist, tokens, (size_t)n_tokens * sizeof(uint32_t)); n_hist = n_tokens; } + else spec_k = 0; + + for (int i = 0; i < max_tokens; i++) { + uint32_t next_token; + if (spec_at < spec_n_pending) { + next_token = spec_pending[spec_at++]; + goto spec_emit; + } + if (!logits) break; + + float *logit_data = (float *)logits->data; + /* S0 (bit-exactness): `PN_LOGIT_HEX_DUMP=1` prints the top-8 (id, float bits) of every + * step, so two arms compare to the BIT, not to a printed digit. */ + if (getenv("PN_LOGIT_HEX_DUMP")) { + int nv = model->config.n_vocab, top[8], n = 0, v, t; + + for (v = 0; v < nv; v++) { + if (n < 8 || logit_data[v] > logit_data[top[n - 1]]) { + int at = n < 8 ? n++ : 7; + + top[at] = v; + for (t = at; t > 0 && logit_data[top[t]] > logit_data[top[t - 1]]; t--) { + int swap = top[t]; + + top[t] = top[t - 1]; + top[t - 1] = swap; + } + } + } + fprintf(stderr, "LOGITHEX step=%d", i); + for (t = 0; t < n; t++) { + uint32_t bits; + + memcpy(&bits, &logit_data[top[t]], sizeof(bits)); + fprintf(stderr, " %d:%08x", top[t], bits); + } + fprintf(stderr, "\n"); + } + /* Bonsai debug: BONSAI_LOGIT_DUMP prints top-3 (id,logit,margin) per + * decode step so the divergence-token margin vs the oracle is visible. */ + if (getenv("BONSAI_LOGIT_DUMP") && model->tokenizer) { + int nv = model->config.n_vocab; + int a = 0, b = 0, c = 0; + for (int v = 1; v < nv; v++) { + float x = logit_data[v]; + if (x > logit_data[a]) { c = b; b = a; a = v; } + else if (x > logit_data[b]) { c = b; b = v; } + else if (x > logit_data[c]) { c = v; } + } + int ta = a, tb = b, tc = c; + char *pa = ft_decode(model->tokenizer, &ta, 1); + char *pb = ft_decode(model->tokenizer, &tb, 1); + char *pc = ft_decode(model->tokenizer, &tc, 1); + fprintf(stderr, "LOGIT step=%d | [%d %.4f '%s'] [%d %.4f '%s'] " + "[%d %.4f '%s'] margin=%.4f\n", + i, a, logit_data[a], pa ? pa : "?", + b, logit_data[b], pb ? pb : "?", + c, logit_data[c], pc ? pc : "?", + logit_data[a] - logit_data[b]); + free(pa); free(pb); free(pc); + } + next_token = sample_token(logit_data, model->config.n_vocab, + params, recent, n_recent); + tm_free(logits); + logits = NULL; +spec_emit: + + if (eos_id >= 0 && (int)next_token == eos_id) break; + + n_generated++; + + if (model->config.arch == PN_ARCH_RWKVX) { + char *str = pn_rwkvx_decode(model->weights, &next_token, 1); + if (str) { callback(str, user_data); free(str); } + } else { + int tok_id = (int)next_token; + char *raw_str = ft_decode(model->tokenizer, &tok_id, 1); + if (raw_str) { + if (is_bpe) { + char *tok_str = gpt2_bytes_decode(raw_str); + free(raw_str); + if (tok_str) { + callback(tok_str, user_data); + free(tok_str); + } + } else { + callback(raw_str, user_data); + free(raw_str); + } + } + } + + if (n_recent < max_recent) { + recent[n_recent++] = next_token; + } else { + memmove(recent, recent + 1, (max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next_token; + } + + if (hist && n_hist < hist_cap) hist[n_hist++] = next_token; + if (spec_at < spec_n_pending) + continue; /* more of the last step's tokens to emit; no forward */ +#ifdef USE_TT + if (spec_k > 0) { + uint32_t drafts[16]; + int nd = pn_qwen35moe_tt_mtp_draft(model, state, next_token, spec_k, drafts); + if (nd < 1) + nd = spec_ngram ? pn_ngram_draft(hist, n_hist, 4, 1, spec_k, drafts) : 0; + /* No draft: a one-row verify costs more than the plain decode step. */ + int ne = 0; + if (nd > 0 && spec_ctx.row) { + memcpy(spec_ctx.recent, recent, (size_t)n_recent * sizeof(uint32_t)); + spec_ctx.n_recent = n_recent; + spec_ctx.row_index = 0; + ne = pn_qwen35moe_tt_spec_step_ex(model, state, next_token, drafts, nd, + spec_pending, pn_spec_sample_choose, &spec_ctx); + } else if (nd > 0) { + ne = pn_qwen35moe_tt_spec_step(model, state, next_token, drafts, nd, + spec_pending); + } + + if (nd == 0) { + /* fall through to the plain forward */ + } else if (ne > 0) { + spec_n_pending = ne; + spec_at = 0; + continue; + } + else { + fprintf(stderr, "engine: speculative step failed; plain decode from here\n"); + spec_k = 0; + spec_n_pending = spec_at = 0; + } + } +#endif + logits = model->forward(model, next_token, state); + } + + if (logits) tm_free(logits); + free(recent); + free(hist); +#ifdef USE_TT + free(spec_ctx.row); + free(spec_ctx.recent); + params->dense_row = NULL; + params->dense_ctx = NULL; +#endif + + return n_generated; +} + +/* ============================================================ + * Lossless speculative decode (single sequence, greedy) + * + * The whole scheme rests on pn_llama_verify: one batched forward over the seed + * plus K draft tokens returns, at each position i, the model's own logits for + * the prefix ending at candidate i (row i is causal, so it is independent of the + * candidates after it, and of K). The step below only ever emits argmax of those + * logits, and only "accepts" a draft token when it already equals that argmax. + * A draft can therefore never inject a token the real model would not have + * produced: it can only save forward passes when it guesses right. That is the + * lossless guarantee, and it holds for any draft (see test_spec_decode.c). + * ============================================================ */ + +/* Greedy argmax with the SAME tie-break as sample_token's temperature<=0 path + * (strict >, so the lowest index wins on a tie), keeping the emitted stream + * bit-identical to plain greedy decode. */ +static uint32_t spec_argmax(const float *logits, int n_vocab) { + int best = 0; + for (int i = 1; i < n_vocab; i++) + if (logits[i] > logits[best]) best = i; + return (uint32_t)best; +} + +int pn_spec_decode_greedy(pn_model *model, pn_state *state, uint32_t seed, + const uint32_t *draft, int K, uint32_t *out) { + if (!model || !state || !out || K < 0 || (K > 0 && !draft)) return -1; + + int n_vocab = model->config.n_vocab; + int M = K + 1; /* seed + K drafts fed to the verify pass */ + uint32_t *toks = malloc((size_t)M * sizeof(uint32_t)); + float *logits = malloc((size_t)M * (size_t)n_vocab * sizeof(float)); + if (!toks || !logits) { free(toks); free(logits); return -1; } + + toks[0] = seed; + for (int i = 0; i < K; i++) toks[1 + i] = draft[i]; + + int base_pos = state->pos; /* seed will occupy this position */ + if (pn_llama_verify(model, toks, NULL, M, state, logits) != 0) { + free(toks); free(logits); + return -1; /* unsupported model: caller falls back */ + } + + /* logits[i] is the prediction for the token AFTER candidate i, i.e. for the + * slot that draft[i] occupies. Accept while the draft matches that argmax. */ + int n_emit = 0; + int n_accept = 0; + int done = 0; + for (int i = 0; i < K && !done; i++) { + uint32_t g = spec_argmax(logits + (size_t)i * n_vocab, n_vocab); + out[n_emit++] = g; /* the true greedy token for this slot */ + if (g == draft[i]) n_accept++; /* draft matched: its K/V is valid */ + else done = 1; /* mismatch: g is the correction, stop */ + } + if (!done) { + /* Every draft token was accepted; take the free bonus after the last. */ + out[n_emit++] = spec_argmax(logits + (size_t)K * n_vocab, n_vocab); + } + + free(toks); free(logits); + + /* Commit the seed (position base_pos) plus every accepted draft token; the + * verify pass already wrote their K/V into the cache at these positions. The + * final emitted token (correction or bonus) stays uncommitted and becomes + * the next call's seed. */ + state->pos = base_pos + 1 + n_accept; + return n_emit; +} + +/* Lossless greedy speculative generation loop. See engine.h for the contract. + * + * The loop keeps the invariant that `seed` is the last generated token not yet + * written to the cache; pn_spec_decode_greedy consumes it, commits the seed plus + * accepted drafts, and returns the greedy continuation whose last element is the + * next seed. Plain decode is the K=0 special case (one token per pass), so the + * fallback path just stops calling draft_fn. */ +int pn_spec_generate(pn_model *model, pn_state *state, uint32_t seed, + int max_tokens, int K, int stop_id, + pn_draft_fn draft_fn, void *draft_ud, + double min_accept_frac, int probe_cycles, + uint32_t *out, pn_spec_stats *stats) { + if (!model || !state || !out || max_tokens <= 0) return -1; + + if (stats) { stats->verify_passes = 0; stats->tokens_emitted = 0; + stats->drafted = 0; stats->accepted = 0; stats->fell_back = 0; } + + /* Probe the verify path once (also the model-support gate). */ + if (K > 0) { + float *probe = malloc((size_t)model->config.n_vocab * sizeof(float)); + int ok = probe && pn_llama_verify(model, &seed, NULL, 1, state, probe) == 0; + free(probe); + if (!ok) return -1; /* unsupported: caller uses plain pn_describe path */ + } + + int max_k = (K > 0) ? K : 0; + uint32_t *draft = max_k ? malloc((size_t)max_k * sizeof(uint32_t)) : NULL; + uint32_t *emit = malloc((size_t)(max_k + 1) * sizeof(uint32_t)); + if ((max_k && !draft) || !emit) { free(draft); free(emit); return -1; } + + int n = 0; + long passes = 0, drafted = 0, accepted = 0; + int use_spec = (K > 0 && draft_fn != NULL); + int fell_back = 0; + + if ((int)seed == stop_id) { free(draft); free(emit); + if (stats) *stats = (pn_spec_stats){0,0,0,0,0}; + return 0; } + out[n++] = seed; + + while (n < max_tokens) { + int kk = 0; + if (use_spec) { + kk = draft_fn(out, n, draft, max_k, draft_ud); + if (kk < 0) kk = 0; + if (kk > max_k) kk = max_k; + } + int em = pn_spec_decode_greedy(model, state, seed, draft, kk, emit); + if (em < 0) { free(draft); free(emit); return -1; } + passes++; + drafted += kk; + for (int i = 0; i < em && i < kk; i++) { if (emit[i] == draft[i]) accepted++; else break; } + + int done = 0; + for (int j = 0; j < em; j++) { + if ((int)emit[j] == stop_id) { done = 1; break; } + out[n++] = emit[j]; + if (n >= max_tokens) { done = 1; break; } + } + seed = emit[em - 1]; + if (done) break; + + /* Per-page fallback: if early acceptance is poor, stop drafting. */ + if (use_spec && !fell_back && passes >= probe_cycles && drafted > 0 && + (double)accepted / (double)drafted < min_accept_frac) { + use_spec = 0; + fell_back = 1; + } + } + + free(draft); free(emit); + if (stats) { + stats->verify_passes = passes; + stats->tokens_emitted = n; + stats->drafted = drafted; + stats->accepted = accepted; + stats->fell_back = fell_back; + } + return n; +} diff --git a/src/core/engine.h b/src/core/engine.h new file mode 100644 index 0000000..900c420 --- /dev/null +++ b/src/core/engine.h @@ -0,0 +1,408 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_ENGINE_H +#define PN_ENGINE_H + +#include +#include +#include "../../include/pantheon.h" +#include "../models/gpu_attn_path_tracker.h" +#include "../models/gpu_graph_capture_tracker.h" +#include "tensor/tensor.h" +#include "tokenizer/tokenizer.h" + +/* + * Internal engine types — not part of public API. + */ + +/* Forward declarations */ +struct pn_arena; +#ifdef USE_THREADS +struct fw_pool; +#endif + +/* Supported model architectures */ +typedef enum { + PN_ARCH_LLAMA, + PN_ARCH_QWEN2, + PN_ARCH_PHI, + PN_ARCH_MISTRAL, + PN_ARCH_MIXTRAL, + PN_ARCH_RWKVX, + PN_ARCH_QWEN35MOE, + PN_ARCH_QWEN3, /* appended to keep existing enum values stable */ + PN_ARCH_BONSAI, /* PrismML Bonsai-27B: qwen35-family hybrid, dense SwiGLU FFN + native ternary (own forward entry, shares qwen35moe blocks) */ + PN_ARCH_UNKNOWN, +} pn_arch; + +/* Model modality — what kind of input/output the model handles */ +typedef enum { + PN_MODALITY_TEXT, + PN_MODALITY_AUDIO, + PN_MODALITY_VISION, + PN_MODALITY_MULTIMODAL, +} pn_modality; + +/* Model configuration (parsed from GGUF metadata or safetensors header) */ +typedef struct { + pn_arch arch; + pn_modality modality; + int n_vocab; + int n_embd; + int n_head; + int n_kv_head; + int head_dim; /* per-head dim; 0 = derive n_embd/n_head (Qwen3 decouples it) */ + int n_layer; + int n_ff; + int n_ctx; + float rope_theta; + float rms_norm_eps; + int sliding_window; /* 0 = full context, >0 = circular KV cache window */ + int rope_dim; /* RoPE rotation dim (0 = full head_dim) */ + int qk_norm; /* 1 = per-head RMSNorm on Q and K before RoPE (Qwen3) */ + int n_experts; /* 0 for non-MoE, 8 for Mixtral */ + int n_experts_used; /* top-k experts per token (2 for Mixtral) */ + int ctx_cap; /* KV Phase 1: per-model per-session context cap + * in tokens (the KV ring / admission window). + * 0 = unset → fall back to QWEN35_MAX_SEQ / + * PN_SESSION_MAX_CTX. Set per model at load so + * ornith can run a 40K window without changing + * saber's (they share the global env otherwise). */ +} pn_config; + +/* Effective per-head dimension (explicit head_dim, else derived). */ +static inline int pn_head_dim(const pn_config *c) { + return c->head_dim > 0 ? c->head_dim : (c->n_head ? c->n_embd / c->n_head : 0); +} + +/* Forward function pointer — each architecture implements one */ +typedef tm_tensor* (*pn_forward_fn)(pn_model *model, uint32_t token, pn_state *state); + +/* Max dies one model can be explicitly placed on (pn_load_on). + * + * 16, matching PN_DM_MAX_DIES / PN_MEM_MAX_DIES (the K80 test machine has 16 GK210 + * dies) and PN_DIE_LAYER_SPLIT_MAX_DIES (what the layer partitioner will + * plan). These four are the same number on purpose: a placement the die + * manager accepts must be one the split planner can plan and the memwatch + * state can account for, or a width gets bound in one layer and refused (or + * worse, silently narrowed) in another. It was 8 while the CUDA forward only + * ever split two ways. */ +#define PN_MODEL_MAX_DIES 16 + +/* Model struct (concrete definition, opaque to public API) */ +struct pn_model { + pn_config config; + void *weights; /* Architecture-specific weight struct */ + pn_forward_fn forward; /* Architecture's forward pass */ + ft_tokenizer *tokenizer; /* BPE tokenizer */ + char *path; /* Model file path */ + int chat_style; /* pn_chat_style enum */ + int chat_add_bos; /* template prepends bos_token (e.g. MiniCPM5) */ + void *vision; /* pn_vision_model* for VLMs (NULL otherwise) */ +#ifdef USE_THREADS + struct fw_pool *pool; /* Thread pool for parallel ops */ +#endif + int gpu_dies[PN_MODEL_MAX_DIES]; /* explicit die placement (pn_load_on) */ + int n_gpu_dies; /* 0 = none (env/defaults apply) */ +#if defined(USE_CUDA) || defined(USE_TT) + void *gpu; /* pn_llama_gpu* for the plain-Llama CUDA + forward, or pn_llama_tt* for the Tenstorrent + one (NULL until the matching _upload). + qwen35moe/bonsai keep their handle inside + their own model struct instead. + ONE slot on purpose: a model has ONE + accelerator residency, even in a build that + carries both families. */ +#endif +}; + +/* State struct (concrete definition, opaque to public API) */ +struct pn_state { + void *kv_cache; /* Architecture-specific KV cache */ + int pos; /* Current position in sequence */ + pn_config *config; /* Reference to model config */ + struct pn_arena *arena; /* Per-token scratch arena */ + const float *embd_override; /* if set, forward uses this embedding (image token) */ +}; + +/* Attach a vision encoder (clip mmproj GGUF) to a loaded model, for pn_describe. */ +int pn_attach_vision(pn_model *model, const char *mmproj_path); + +/* Get forward function for an architecture */ +pn_forward_fn pn_get_forward_fn(pn_arch arch); + +/** + * Snapshot `model`'s CUDA-graph residency — the graphs_live/graphs_total gauge. + * + * A capture that fails degrades to the direct kernel body with CORRECT output, + * so the only symptom of losing a layer's graph is that the serve is ~1.8 + * ms/step slower across 40 layers. This is the surface that makes that visible, + * and it separates the three states that look alike from the outside: + * captured (fine), failed (a real deficit), ineligible (correct behaviour — + * a non-Q3_K MoE never captures at all). + * + * @param model Any loaded model; architectures with no graph path report 0 + * @param out Zeroed and filled; `all.total == 0` means "no graph slots" + * @return 0 if the model has a graph path, -1 otherwise (`out` still + * zeroed, so a caller may ignore the return) + * + * Thread-safe: yes, lock-free (see pn_qwen35moe_gpu_graph_residency) + * Reentrant: yes + * Signal-safe: no + */ +int pn_model_graph_residency_read(const pn_model *model, + pn_model_graph_residency *out); + +/** + * Snapshot which attention path this model's attention layers are on. + * + * Same shape and same contract as pn_model_graph_residency_read, for the same + * reason: an on-die-attention loss returns correct output and reports nothing, + * so the counter is the only observable. + * + * @param model Any loaded model; architectures with no on-die attention path + * report 0 + * @param out Zeroed and filled; `all.total == 0` means "never evaluated" + * @return 0 if the model has an instrumented attention path, -1 otherwise + * (`out` still zeroed, so a caller may ignore the return) + * + * Thread-safe: yes, lock-free (see pn_qwen35moe_gpu_attn_residency) + * Reentrant: yes + * Signal-safe: no + */ +int pn_model_attn_residency_read(const pn_model *model, + pn_model_attn_residency *out); + +/* Detect model architecture from file */ +pn_arch pn_detect_arch(const char *path); + +/* Detect file format (GGUF vs safetensors) */ +bool pn_is_gguf(const char *path); +bool pn_is_safetensors(const char *path); + +/* Load options for pn_load_ex() */ +typedef struct { + const char *vocab_path; /* RWKV World tokenizer (required for safetensors) */ + const char *config_path; /* HuggingFace config.json (for Qwen/LLaMA safetensors) */ + bool quantize; /* INT8 quantize at load time */ + int ctx_cap; /* KV Phase 1: per-model per-session context cap in + * tokens (KV ring window). 0 = unset. Set BEFORE + * weight upload so the ring is sized per model. */ +} pn_load_opts; + +/* Extended model loading with options (safetensors + GGUF) */ +pn_model* pn_load_ex(const char *path, const pn_load_opts *opts); + +/* Placement-aware load (Phase 7 die-placement API): as pn_load_ex, but binds + * the model to an explicit die-set BEFORE any GPU weight upload, so the + * multi-model host can place disjoint die-sets per model. The placement is + * recorded on the model (gpu_dies/n_gpu_dies) on every path; today only the + * qwen35moe CUDA forward consumes it (N dies = an N-way layer split, layers + * divided as evenly as possible with the LM head on the LAST die; + * QWEN35_GPU_SPLIT_LAYER still tunes the single boundary of a 2-die split). + * Explicit placement wins over the QWEN35_GPU_DIE_A/B env knobs. Pass + * dies == NULL / n_dies == 0 for exact pn_load_ex behavior. Placement is + * bookkeeping-only without CUDA (the CPU forward ignores it). */ +pn_model* pn_load_on(const char *path, const pn_load_opts *opts, + const int *dies, int n_dies); + +/* GPT-2 byte encoding/decoding (used by chat module for BPE models) */ +char* gpt2_bytes_encode(const char *input); +char* gpt2_bytes_decode(const char *input); + +/* ============================================================ + * Lossless speculative decode (single sequence, greedy) + * ============================================================ */ + +/* Run ONE greedy speculative-decode step for a single sequence. + * + * Preconditions: state->pos = N, the KV cache holds committed positions 0..N-1, + * and `seed` is the last already-emitted token that has NOT yet been written to + * the cache (it occupies position N). draft[0..K-1] are the draft model's + * guesses for the K tokens that follow `seed` (K may be 0). + * + * Runs one pn_llama_verify pass over [seed, draft[0..K-1]] (M = K+1 tokens) and + * emits the EXACT greedy continuation of `seed`: with g_i = argmax(logits[i]), + * it appends g_0 (the true next token), accepts draft[i] while draft[i] == g_i, + * and at the first mismatch appends the correction g_i and stops; if all K draft + * tokens are accepted it appends the free bonus g_K. Every appended token is the + * argmax of the model's own logits for the true accepted prefix, so the emitted + * stream is bit-identical to plain greedy decoding no matter how good or bad the + * draft is (a bad draft just yields fewer tokens per pass, never a wrong token). + * + * Writes the emitted tokens into out[] (needs room for K+1) and advances + * state->pos to cover `seed` plus every accepted draft token (their K/V is + * already committed by the verify pass); the final emitted token (correction or + * bonus) is left uncommitted and becomes the next call's `seed`. Returns the + * number of emitted tokens (>= 1), or -1 if the model is unsupported. */ +int pn_spec_decode_greedy(pn_model *model, pn_state *state, uint32_t seed, + const uint32_t *draft, int K, uint32_t *out); + +/* pn_draft_fn (the external draft-provider seam) is declared in pantheon.h and + * available here via the include above; the batched speculative decoder and the + * single-sequence pn_spec_generate share that one typedef. */ + +/* Speedup accounting for a speculative generation run. */ +typedef struct { + long verify_passes; /* speculative forward passes actually run */ + long tokens_emitted; /* generated tokens (== plain-decode forward count) */ + long drafted; /* draft tokens proposed */ + long accepted; /* draft tokens accepted */ + int fell_back; /* 1 if the run dropped to plain decode (low acceptance) */ +} pn_spec_stats; + +/* Lossless greedy speculative generation for a single sequence. + * + * Preconditions: `state` is already prefilled (state->pos = context length) and + * `seed` is the first generated token, greedily sampled from the prefill logits + * (argmax), not yet written to the cache. This is exactly the state right after + * vlm_prefill in the OCR path. + * + * Each cycle asks draft_fn for up to K candidate tokens for the slots after the + * accepted prefix, runs one pn_spec_decode_greedy pass, and appends the exact + * greedy continuation. Because every emitted token is the model's own argmax and + * a draft is only accepted when it already equals that argmax, the produced + * stream is bit-identical to plain greedy decode for ANY draft (see the tests). + * The draft only changes how many tokens each verify pass yields, hence the + * speedup: verify_passes <= tokens_emitted, and plain decode would need exactly + * tokens_emitted forwards. + * + * Per-page fallback: after `probe_cycles` speculative cycles, if the running + * acceptance fraction is below `min_accept_frac` (a structure-heavy page where + * the draft rarely matches), the run drops to plain single-token decode for the + * remainder so speculation never costs more than it saves. Pass K<=0 to force + * plain decode from the start. + * + * Stops at `stop_id` (e.g. EOS / <|im_end|>; pass -1 for none) or max_tokens. + * The stop token is NOT emitted, matching the plain greedy decode loop. Writes + * generated ids into out[] (capacity >= max_tokens) and returns the count, or -1 + * if the model is not on the quantized verify path. Fills *stats if non-NULL. */ +int pn_spec_generate(pn_model *model, pn_state *state, uint32_t seed, + int max_tokens, int K, int stop_id, + pn_draft_fn draft_fn, void *draft_ud, + double min_accept_frac, int probe_cycles, + uint32_t *out, pn_spec_stats *stats); + +/* ============================================================ + * Sampler seam (for external decode drivers, e.g. the serve layer) + * ============================================================ */ + +/* The grammar mask's shortlist decision on its own (grammar_mask_shortlist.h): + * with params->tool_grammar as the grammar, rewrite `logits` to the masked + * survivors when the row's top entries provably decide the draw. Returns 1 + * when rewritten (sample it with the grammar hook removed and the same + * dense_row setting), 0 when undecidable (row untouched). Lets a speculative + * verify decide a masked row from its top-N exactly as plain decode would. + * + * Thread-safe: yes (no shared state) */ +int pn_sampler_grammar_shortlist(float *logits, int n_vocab, const pn_sampling_params *params, + const uint32_t *recent_tokens, int n_recent); + +/* Public wrapper around the internal token sampler so external decode drivers + * reuse the EXACT sampling path (repeat penalty -> temperature -> top-k -> + * top-p -> cumulative draw; temperature <= 0 is greedy argmax) instead of + * reimplementing it. Mutates logits[] in place exactly as the built-in generate + * loop does. recent_tokens[0..n_recent) is the repeat-penalty ring (may be NULL + * with n_recent == 0). Returns the sampled token id. + * + * Thread-safe: no (uses a shared per-vocab scratch buffer) + * Reentrant: no */ +uint32_t pn_sample_next_token(float *logits, int n_vocab, + pn_sampling_params *params, + uint32_t *recent_tokens, int n_recent); + +/* Sparse-survivor sampler fast path (PN_SPARSE_SAMPLER) — see engine.c. + * Covers BOTH decode modes: the temperature>0 sampler and the temperature<=0 + * greedy argmax, off the same gather and the same flag. + * + * set/clear override the env in process, so a gate can flip arms between two + * calls on the same logits row. + * + * pn_sampler_sparse_hits() counts how many times the fast path actually RAN, + * not how many times it was enabled. A gate MUST assert this advances: the + * previous attempt at this optimisation was silently inert (its row test + * `x > -INFINITY` is folded to a constant true by -ffast-math, so every row + * fell through to the slow path) and its bit-exactness gate still passed 18/18 + * — comparing the old path against itself. */ +void pn_sampler_set_sparse(int on); +void pn_sampler_clear_sparse(void); +unsigned long pn_sampler_sparse_hits(void); +/** Speculative sampling's chooser state (pn_spec_sample_choose, a pn_spec_choose_fn): the + * request's params, an n_vocab row buffer, a copy of the repeat window advanced per draw, + * and the verify row being chosen (reset to 0 before each cycle). */ +typedef struct pn_spec_sample_ctx { + pn_sampling_params *params; + float *row; + int n_vocab; + uint32_t *recent; + int n_recent, max_recent; + int row_index; +} pn_spec_sample_ctx; +/** Equal to qwen35moe.h's PN_SPEC_CHOOSE_DEFER. */ +#define PN_SPEC_CHOOSE_DEFER_TOKEN 0xFFFFFFFFu +/** One verify row's draw: the row as plain sparse decode would hand the sampler, drawn + * under the plain repeat window; PN_SPEC_CHOOSE_DEFER_TOKEN when a row past 0 cannot be + * decided exactly by its top-N. */ +uint32_t pn_spec_sample_choose(void *ctx, const uint32_t *index, const float *value, + uint32_t count); +/** Top-N rows the sampler swapped for the dense row (pn_sampling_params.dense_row). */ +unsigned long pn_sampler_topn_refills(void); +/** Can this top-N row (-INFINITY outside N entries) decide a sampled draw exactly as the + * dense row would (the top-k set after penalties lies inside the N)? Dense rows: true. */ +bool pn_sampler_topn_row_exact(const float *logits, int n_vocab, + const pn_sampling_params *params, + const uint32_t *recent, int n_recent); + +/* Grammar-mask density invariant — see the full-logits arming-window block in + * serve_generation.c. + * + * pn_sampler_grammar_mask_steps() counts steps that APPLIED a grammar mask. + * pn_sampler_grammar_sparse_rows() counts the subset of those whose row was + * still the device's SPARSE top-k presentation — steps where the mask could + * only choose among ~k tokens, so a grammar whose legal tokens were all pruned + * has zero survivors and fails open (the response_format defect). + * + * THE SPARSE COUNT MUST STAY ZERO. It is the load-bearing invariant of the + * arming window: arming the full row one token late drops the mask on exactly + * the step that engages the grammar. Both counters are process-wide across + * threads (relaxed increments — they are a tripwire, not an accounting record), + * so a whole serve run is checkable with one number, and a gate can prove its + * own detector fires by running the deliberately-late arm + * (PN_GRAMMAR_ARM_STALE=1). */ +unsigned long pn_sampler_grammar_mask_steps(void); +/** Masked steps decided from the shortlist / scanned in full, and the wall + * seconds spent inside the mask (refill + shortlist + scan, not the draw). */ +unsigned long pn_sampler_grammar_shortlist_steps(void); +unsigned long pn_sampler_grammar_fullscan_steps(void); +/** Masked steps applied by fetching only the dense chunks that hold an allowed + * token (pn_sampling_params.dense_chunks) instead of the whole row. */ +unsigned long pn_sampler_grammar_chunk_steps(void); +double pn_sampler_grammar_mask_seconds(void); +unsigned long pn_sampler_grammar_sparse_rows(void); + +/* ============================================================ + * Async forward-step seam (C2 — 2 cores drive 8 cards) + * + * Architecture-agnostic wrapper over an OPTIONAL non-blocking single-token + * step: begin launches the token's GPU work and returns immediately; poll + * reaps completion (CUDA event query — never blocks). While one card's + * step is in flight the calling thread sleeps, so concurrent generations + * on other cards keep the host's 2 cores instead of serializing behind + * blocking stream syncs. Today only the qwen35moe CUDA forward implements + * it; every other model (and any non-eligible token) reports "unsupported" + * and the caller uses model->forward exactly as before. + * ============================================================ */ + +/* Launch one token's forward without blocking. + * Returns 0 = in flight (caller MUST pn_forward_step_poll to completion + * before any other forward on this model), 1 = unsupported/not eligible + * (nothing launched — call model->forward instead), -1 = error. */ +int pn_forward_step_begin(pn_model *model, uint32_t token, pn_state *state); + +/* Reap the in-flight step. Returns 1 = still running (sleep briefly and + * poll again), 0 = done (*logits_out = owned logits tensor, state->pos + * advanced — same contract as model->forward), -1 = error. */ +int pn_forward_step_poll(pn_model *model, pn_state *state, + tm_tensor **logits_out); + +#endif /* PN_ENGINE_H */ diff --git a/src/core/grammar_mask_shortlist.c b/src/core/grammar_mask_shortlist.c new file mode 100644 index 0000000..9dde7ed --- /dev/null +++ b/src/core/grammar_mask_shortlist.c @@ -0,0 +1,137 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * Grammar mask from a shortlist — see grammar_mask_shortlist.h for why the + * decision is exact. Pure C, libc-only. + */ +#include "grammar_mask_shortlist.h" + +#include +#include + +#define GMS_NEG_INF_BITS 0xff800000u + +/* -inf built from its bits: under -ffast-math the INFINITY macro is not + * reliable, and the sampler recognises an excluded entry by these exact bits. */ +static float gms_neg_inf(void) +{ + const uint32_t bits = GMS_NEG_INF_BITS; + float v; + memcpy(&v, &bits, sizeof(v)); + return v; +} + +/* Reads the BITS through the pointer: a float value passed through fast-math + * code may be assumed finite, and the compare folded away. */ +static bool gms_is_neg_inf(const float *p) +{ + uint32_t bits; + memcpy(&bits, p, sizeof(bits)); + return bits == GMS_NEG_INF_BITS; +} + +/* The logit the sampler will see for `id` after the penalties, replicating + * pn_apply_presence_frequency_penalties and the repeat-penalty loop in their + * order (presence/frequency per occurrence, then — sampling only — the repeat + * penalty once per occurrence). */ +static float gms_penalised(float v, uint32_t id, const gms_sampling *s) +{ + int count = 0; + for (int i = 0; i < s->n_recent; i++) { + if (s->recent[i] != id) continue; + v -= s->frequency_penalty; + if (count == 0) v -= s->presence_penalty; + count++; + } + if (s->temperature > 0.0f && s->repeat_penalty != 1.0f) { + for (int i = 0; i < count; i++) + v = v > 0.0f ? v / s->repeat_penalty : v * s->repeat_penalty; + } + return v; +} + +/* Margin over `bound` that absorbs a rounding difference between this replica + * and the sampler's own penalty arithmetic. */ +static float gms_margin(float bound) +{ + return 1e-4f + 1e-4f * fabsf(bound); +} + +bool gms_mask_from_shortlist(float *logits, int n_vocab, const uint32_t *ids, int n_ids, + float bound, const gms_sampling *s, + gms_allowed_fn allowed, void *allowed_ctx) +{ + uint32_t kept_ids[GMS_MAX]; + float kept_vals[GMS_MAX]; + int n_kept = 0, n_clear = 0; + const int k = s->temperature <= 0.0f ? 1 : s->top_k; + + if (n_ids < 1 || n_ids > GMS_MAX || k < 1 || k > n_ids || s->has_logit_bias || + s->repeat_penalty < 1.0f || s->presence_penalty < 0.0f || s->frequency_penalty < 0.0f) + return false; + for (int i = 0; i < n_ids; i++) { + const uint32_t id = ids[i]; + if (gms_is_neg_inf(&logits[id]) || !allowed(allowed_ctx, id)) continue; + kept_ids[n_kept] = id; + kept_vals[n_kept++] = logits[id]; + if (gms_penalised(logits[id], id, s) > bound + gms_margin(bound)) n_clear++; + } + if (n_clear < k) + return false; + const float neg_inf = gms_neg_inf(); + for (int i = 0; i < n_vocab; i++) + logits[i] = neg_inf; + for (int i = 0; i < n_kept; i++) + logits[kept_ids[i]] = kept_vals[i]; + return true; +} + +/* Sift the min-heap (by value) down from `at`. */ +static void gms_heap_down(uint32_t *heap, const float *logits, int n, int at) +{ + for (;;) { + int least = at, l = 2 * at + 1, r = l + 1; + if (l < n && logits[heap[l]] < logits[heap[least]]) least = l; + if (r < n && logits[heap[r]] < logits[heap[least]]) least = r; + if (least == at) return; + uint32_t t = heap[at]; + heap[at] = heap[least]; + heap[least] = t; + at = least; + } +} + +int gms_select_shortlist(const float *logits, int n_vocab, uint32_t *ids, float *bound) +{ + int n = 0; + float left_out = gms_neg_inf(); + + for (int i = 0; i < n_vocab; i++) { + if (gms_is_neg_inf(&logits[i])) continue; + if (n < GMS_MAX) { + ids[n++] = (uint32_t)i; + if (n == GMS_MAX) + for (int j = GMS_MAX / 2 - 1; j >= 0; j--) gms_heap_down(ids, logits, n, j); + continue; + } + if (logits[i] <= logits[ids[0]]) { + if (logits[i] > left_out) left_out = logits[i]; + continue; + } + if (logits[ids[0]] > left_out) left_out = logits[ids[0]]; + ids[0] = (uint32_t)i; + gms_heap_down(ids, logits, n, 0); + } + if (n == 0) return 0; + if (n < GMS_MAX || gms_is_neg_inf(&left_out)) { + /* Every finite entry is listed: a top-N row, whose unlisted TRUE logits + * sit at or below its smallest entry. */ + float min = logits[ids[0]]; + for (int i = 1; i < n; i++) + if (logits[ids[i]] < min) min = logits[ids[i]]; + *bound = min; + } else { + *bound = left_out; + } + return n; +} diff --git a/src/core/grammar_mask_shortlist.h b/src/core/grammar_mask_shortlist.h new file mode 100644 index 0000000..0570707 --- /dev/null +++ b/src/core/grammar_mask_shortlist.h @@ -0,0 +1,83 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file grammar_mask_shortlist.h + * @brief Apply a grammar mask by judging a SHORTLIST of the highest logits + * instead of the whole vocabulary, when that provably decides the draw. + * + * The plain grammar mask asks `allowed()` of every one of the ~248k vocab + * entries, twice (count, then mask) — on a slow host that is the dominant cost + * of a constrained token. The draw, though, only ever looks at the top_k + * survivors (greedy: the argmax). Penalties only LOWER a logit (presence and + * frequency subtract, a repeat penalty >= 1 divides a positive and multiplies a + * negative), so a token outside the shortlist can never rise above `bound` — + * the largest raw value left out of it. Hence: if at least K shortlisted tokens + * are ALLOWED and keep a penalised value strictly above `bound` (with a small + * margin for the replica's rounding), the top-K of the masked row lies inside + * the shortlist, and masking everything else changes nothing the draw can see. + * + * When it decides, the row is rewritten in place: allowed shortlisted tokens + * keep their logit, every other entry becomes -INFINITY — a sparse row the + * sampler's survivor fast path takes. When it cannot decide (a logit_bias, a + * penalty that raises, top_k off or above the shortlist, too few allowed + * tokens) the row is left untouched and the caller runs the full mask. + * + * Shortlists: a backend's exact top-N row (every unlisted logit <= its smallest + * entry) or the top-GMS_MAX entries selected from a dense row. + * + * Pure C, libc-only; tests/test_grammar_mask_shortlist.c compares it with the + * full mask on random rows. + * + * @status Used by sample_token (engine.c) for every masked step. + * PN_GRAMMAR_SHORTLIST=0 turns it off (full mask every step). + * @issues None + * @todo None + */ +#ifndef PN_GRAMMAR_MASK_SHORTLIST_H +#define PN_GRAMMAR_MASK_SHORTLIST_H + +#include +#include + +/** Largest shortlist judged (also the sampler's sparse survivor cap). */ +#define GMS_MAX 256 + +/** The sampling parameters the decision depends on. */ +typedef struct { + float temperature; /**< <= 0: greedy (K = 1, no repeat penalty) */ + int top_k; /**< K when sampling; <= 0 or > shortlist: undecidable */ + float repeat_penalty; /**< sampling only; must be >= 1 */ + float presence_penalty; /**< must be >= 0 */ + float frequency_penalty; /**< must be >= 0 */ + bool has_logit_bias; /**< a bias can raise a token: undecidable */ + const uint32_t *recent; + int n_recent; +} gms_sampling; + +/** Is token `id` allowed by the grammar right now? */ +typedef bool (*gms_allowed_fn)(void *ctx, uint32_t id); + +/** + * Try to apply the mask from a shortlist. + * + * @param logits the row (n_vocab), rewritten only on success + * @param ids shortlisted token ids (distinct, < n_vocab) + * @param n_ids 1..GMS_MAX + * @param bound the largest raw logit NOT in the shortlist (or any upper bound) + * @return true when the row was rewritten (decided), false when untouched + * + * Thread-safe: yes (no shared state) + */ +bool gms_mask_from_shortlist(float *logits, int n_vocab, const uint32_t *ids, int n_ids, + float bound, const gms_sampling *sampling, + gms_allowed_fn allowed, void *allowed_ctx); + +/** + * Select the shortlist from a row: every finite entry when the row holds at + * most GMS_MAX of them (a backend top-N row: `*bound` = its smallest value), + * else the GMS_MAX largest (`*bound` = the largest value left out). + * @return the count written to `ids` (0 for an empty row) + */ +int gms_select_shortlist(const float *logits, int n_vocab, uint32_t *ids, float *bound); + +#endif /* PN_GRAMMAR_MASK_SHORTLIST_H */ diff --git a/src/core/mem/pn_admission.c b/src/core/mem/pn_admission.c new file mode 100644 index 0000000..5384434 --- /dev/null +++ b/src/core/mem/pn_admission.c @@ -0,0 +1,364 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_admission.c + * @brief Implementation of pre-flight admission control (MemoryAware.md §6). + * The cost of an N-token prefill is estimated from a measured + * per-prompt-length LUT (the transient activation peak — the O(N^2) term + * that OOM-rebooted the box) interpolated between anchor points, plus a + * linear per-token KV term. Refine the LUT with real K80 measurements + * (§11 Q1); the shape is intentionally conservative until then. + * + * @status Phase 2 — admission gates, path-aware (GPU-resident requests admit + * the full context up to max_window; the LUT gates the CPU-fallback + * path only) + the model-load host-spike budget (PN_LOAD_SPIKE_BYTES). + * @issues LUT anchors model the CPU-forward path (kept conservative on + * purpose); GPU-path peaks measured on a K80 host are ~0 — the gpu_resident + * branch of pn_admit_request encodes that finding. + * @todo None + */ + +#include "pn_admission.h" + +#include + +#include "pn_memwatch.h" + +/* Measured activation-peak LUT (MemoryAware.md §11 Q1). Each row is + * {prompt_tokens, peak_host_activation_bytes} — the transient, NON-KV host + * memory a full-attention prefill of that length peaks at. The curve is + * super-linear (the attention scores buffer is O(n_head * seq)); we interpolate + * linearly between anchors and extrapolate from the last segment's slope beyond + * the top anchor. Seeded conservatively from the spec's failure surface (32k + * full-attention prefill exhausts an 8 GB host); replace with K80 numbers + * captured at these exact lengths to tighten. */ +typedef struct { + int tokens; + uint64_t peak_bytes; +} admit_lut_row; + +/* MEASURED (K80) 2026-07-09 (K80, dies 0+1, Q3_K_M 2-die split @ layer 20, + * QWEN35_KV_Q4=1, QWEN35_MAX_SEQ=32768, serve prefill loop): on the GPU + * forward path the transient HOST activation peak is ~0 at every measured + * length — the O(N^2) scores live on-die, and serve RSS never moved: + * n=981 -> RSS delta 0 MiB (avail dip 1 MiB) + * n=1958 -> RSS delta 0 MiB (avail dip 100 MiB, page-cache noise) + * n=3933 -> RSS delta 246 MiB (avail dip 273 MiB, page-cache re-fault) + * n=8083 -> RSS delta 0 MiB (avail dip 36 MiB) + * n=20360 -> no avail dip over a 200 s capped-prefill sample + * The anchors below are therefore NOT tightened to the GPU-path numbers: they + * model the CPU-forward / host-fallback path — the path that actually + * OOM-rebooted the box and the one a GPU-refused load falls back to. Keeping + * them conservative keeps the reboot impossible on that path. GPU-resident + * requests no longer pay this LUT: pn_admit_request's gpu_resident branch + * admits the full context up to max_window (DECIDED 2026-07-09 — path-aware + * admission; the only length-triggered host fallback is n > QWEN35_MAX_SEQ). */ +static const admit_lut_row PN_ADMIT_LUT[] = { + { 1024, 64ULL * 1024 * 1024 }, /* ~64 MiB */ + { 2048, 160ULL * 1024 * 1024 }, /* ~160 MiB */ + { 4096, 440ULL * 1024 * 1024 }, /* ~440 MiB */ + { 8192, 1200ULL * 1024 * 1024 }, /* ~1.2 GiB */ + { 16384, 3400ULL * 1024 * 1024 }, /* ~3.4 GiB — already tips an 8 GB box */ + { 32768, 9800ULL * 1024 * 1024 }, /* ~9.6 GiB — the OOM-reboot point */ +}; +static const int PN_ADMIT_LUT_N = + (int)(sizeof(PN_ADMIT_LUT) / sizeof(PN_ADMIT_LUT[0])); + +/* Interpolate the activation peak for an arbitrary prompt length from the LUT. + * Below the first anchor: scale down linearly from it. Between anchors: linear + * interpolation. Above the top anchor: extrapolate along the last segment's + * slope (never clamp — the whole point is to keep escalating past 32k). */ +static uint64_t lut_activation_peak(int n_prompt) +{ + if (n_prompt <= 0) + return 0; + + const admit_lut_row *lo = &PN_ADMIT_LUT[0]; + if (n_prompt <= lo->tokens) + return lo->peak_bytes * (uint64_t)n_prompt / (uint64_t)lo->tokens; + + for (int i = 1; i < PN_ADMIT_LUT_N; i++) { + const admit_lut_row *a = &PN_ADMIT_LUT[i - 1]; + const admit_lut_row *b = &PN_ADMIT_LUT[i]; + if (n_prompt <= b->tokens) { + uint64_t span = (uint64_t)(b->tokens - a->tokens); + uint64_t off = (uint64_t)(n_prompt - a->tokens); + uint64_t rise = b->peak_bytes - a->peak_bytes; + return a->peak_bytes + rise * off / span; + } + } + + /* Extrapolate beyond the top anchor along the last segment's slope. */ + const admit_lut_row *a = &PN_ADMIT_LUT[PN_ADMIT_LUT_N - 2]; + const admit_lut_row *b = &PN_ADMIT_LUT[PN_ADMIT_LUT_N - 1]; + uint64_t span = (uint64_t)(b->tokens - a->tokens); + uint64_t rise = b->peak_bytes - a->peak_bytes; + uint64_t off = (uint64_t)(n_prompt - b->tokens); + return b->peak_bytes + rise * off / span; +} + +uint64_t pn_admit_kv_per_token(uint64_t kv_per_token, bool kv_q4) +{ + if (kv_per_token > 0) + return kv_per_token; + return kv_q4 ? PN_ADMIT_KV_BYTES_PER_TOKEN_Q4 + : PN_ADMIT_KV_BYTES_PER_TOKEN_FP32; +} + +uint64_t pn_admit_die_kv_bytes(uint64_t kv_per_token, bool kv_q4, int max_ctx, + int n_layers_here, int n_layer) +{ + if (max_ctx <= 0) + return 0; + uint64_t per_tok = pn_admit_kv_per_token(kv_per_token, kv_q4); + uint64_t whole = per_tok * (uint64_t)max_ctx; + if (n_layer <= 0 || n_layers_here <= 0) + return whole; /* layout unknown — charge all of it */ + if (n_layers_here >= n_layer) + return whole; /* this die holds them all */ + return whole * (uint64_t)n_layers_here / (uint64_t)n_layer; +} + +uint64_t pn_admit_host_cost(int n_prompt, bool kv_q4, uint64_t kv_per_token) +{ + if (n_prompt <= 0) + return 0; + uint64_t per_tok = pn_admit_kv_per_token(kv_per_token, kv_q4); + uint64_t kv = per_tok * (uint64_t)n_prompt; + return kv + lut_activation_peak(n_prompt); +} + +uint64_t pn_admit_die_cost(int n_prompt, bool kv_q4, uint64_t kv_per_token) +{ + if (n_prompt <= 0) + return 0; + /* On-die KV mirror grows the same per token; the flash-attention scratch is + * bounded (tiled), so the VRAM growth is dominated by the KV term. Use the + * same per-token KV term plus a modest activation-scratch slice. */ + uint64_t per_tok = pn_admit_kv_per_token(kv_per_token, kv_q4); + uint64_t kv = per_tok * (uint64_t)n_prompt; + return kv + lut_activation_peak(n_prompt) / 4; /* die scratch is smaller */ +} + +const char *pn_admit_verdict_name(pn_admit_verdict v) +{ + switch (v) { + case PN_ADMIT_OK: return "ADMIT"; + case PN_ADMIT_CAP: return "CAP"; + case PN_ADMIT_REJECT: return "REJECT"; + } + return "?"; +} + +/* Largest prompt length whose host cost fits under `budget`, by walking down + * from n_prompt in coarse steps then refining. Returns >=1 if any window fits, + * 0 if even a minimal window won't. */ +static int host_fit_window(int n_prompt, bool kv_q4, uint64_t kv_per_token, + uint64_t budget) +{ + if (pn_admit_host_cost(n_prompt, kv_q4, kv_per_token) <= budget) + return n_prompt; + + /* Binary search the largest fitting length in [1, n_prompt). */ + int lo = 1, hi = n_prompt, best = 0; + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + if (pn_admit_host_cost(mid, kv_q4, kv_per_token) <= budget) { + best = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + return best; +} + +void pn_admit_request(int n_prompt, bool kv_q4, uint64_t kv_per_token, + bool gpu_resident, int die_id, int max_window, + const pn_mem_state *state, pn_admit_result *out) +{ + out->verdict = PN_ADMIT_OK; + out->fit_tokens = n_prompt; + out->reason = "admitted"; + out->host_need_bytes = pn_admit_host_cost(n_prompt, kv_q4, kv_per_token); + out->host_avail_bytes = state ? state->host_avail_bytes : 0; + + if (n_prompt <= 0 || !state) + return; + + /* --- Host stall guard: if the box is already thrashing, do not pile on. + * Sustained, meaningful pressure only: avg10 decays over ~a minute, so any + * transient stall (e.g. page-cache churn right after a ~90 s model load) + * leaves it slightly > 0 long after the box recovered — rejecting on > 0 + * 503s every healthy request for that whole decay window. Threshold via + * PN_PSI_FULL_REJECT (default PN_PSI_FULL_REJECT_DEFAULT); sub-threshold + * pressure falls through to the byte math below, which still refuses any + * request that would not fit. --- */ + if (state->host_psi_full_avg10 >= pn_psi_full_reject_pct()) { + out->verdict = PN_ADMIT_REJECT; + out->reason = "host under sustained memory stall (PSI full) — retry shortly"; + return; + } + + /* --- Host headroom (§6.2, §6.3): MemAvailable minus the reserve. --- */ + uint64_t reserve = pn_host_reserve_bytes(state->host_total_bytes); + uint64_t host_budget = state->host_avail_bytes > reserve + ? state->host_avail_bytes - reserve : 0; + + /* --- GPU-resident path (DECIDED 2026-07-09, measured on a K80 host): per-request + * prefill host cost is ~0 — the O(N^2) scores live on-die in bounded flash + * tiles, and host + die KV are preallocated at max_seq by the load + * admission, so neither the host LUT nor the per-die growth math applies. + * Admit the FULL context; CAP only past max_window (the QWEN35_MAX_SEQ + * mirror — the one length that triggers the host-softmax fallback). Real + * host pressure still refuses: the PSI stall guard above, and MemAvailable + * already inside the reserve here. --- */ + if (gpu_resident) { + out->host_need_bytes = 0; + if (host_budget == 0) { + out->verdict = PN_ADMIT_REJECT; + out->reason = "host inside its memory reserve — retry shortly"; + return; + } + if (max_window > 0 && n_prompt > max_window) { + out->verdict = PN_ADMIT_CAP; + out->fit_tokens = max_window; + out->reason = "prompt exceeds the max sequence window"; + } + return; /* ADMIT full context */ + } + + /* --- Die headroom (§6.2): the model's die free VRAM, if reporting. --- */ + uint64_t die_need = pn_admit_die_cost(n_prompt, kv_q4, kv_per_token); + uint64_t die_free = 0; + bool have_die = (die_id >= 0 && die_id < state->n_dies && + state->per_die_budget[die_id] > 0); + if (have_die) + die_free = state->per_die_free[die_id]; + + bool host_fits = out->host_need_bytes <= host_budget; + bool die_fits = !have_die || die_need <= die_free; + + if (host_fits && die_fits) + return; /* ADMIT */ + + /* --- Doesn't fit at N. Can a smaller window fit host + die? --- */ + int host_win = host_fits ? n_prompt + : host_fit_window(n_prompt, kv_q4, kv_per_token, + host_budget); + int die_win = n_prompt; + if (have_die && !die_fits) { + /* Die cost is ~linear in the KV term; solve for the fitting length. */ + uint64_t per_tok = pn_admit_kv_per_token(kv_per_token, kv_q4); + die_win = per_tok ? (int)(die_free / per_tok) : 0; + } + int fit = host_win < die_win ? host_win : die_win; + if (max_window > 0 && fit > max_window) + fit = max_window; + + if (fit >= 1) { + out->verdict = PN_ADMIT_CAP; + out->fit_tokens = fit; + out->reason = "prompt capped to the fitting context window"; + } else { + out->verdict = PN_ADMIT_REJECT; + out->reason = "server memory-constrained — retry shortly"; + } +} + +bool pn_admit_model_load(uint64_t weights_bytes, int max_ctx, bool kv_q4, + uint64_t kv_per_token, uint64_t die_free_bytes, + uint64_t *need_out) +{ + uint64_t per_tok = pn_admit_kv_per_token(kv_per_token, kv_q4); + uint64_t kv = (max_ctx > 0) ? per_tok * (uint64_t)max_ctx : 0; + + /* Activation reserve: the transient peak at the max context we might serve, + * so a later long prefill on this die still has room. */ + uint64_t activation_reserve = lut_activation_peak(max_ctx > 0 ? max_ctx : 0); + + uint64_t die_reserve = pn_die_reserve_bytes(); + uint64_t need = weights_bytes + kv + activation_reserve; + if (need_out) + *need_out = need; + + /* Hold the per-die reserve back from the die's free VRAM. */ + uint64_t usable = die_free_bytes > die_reserve ? die_free_bytes - die_reserve : 0; + return need <= usable; +} + +uint64_t pn_admit_die_scratch_bytes(int max_ctx) +{ + uint64_t scratch = PN_ADMIT_DIE_SCRATCH_BASE_BYTES; + if (max_ctx > 0) + scratch += PN_ADMIT_DIE_SCRATCH_BYTES_PER_TOKEN * (uint64_t)max_ctx; + /* Never charge more than the flat constant this replaces: past the window + * it was calibrated at we have no measurement, and the slope is sized for + * the default session count. */ + if (scratch > PN_ADMIT_DIE_SCRATCH_MAX_BYTES) + scratch = PN_ADMIT_DIE_SCRATCH_MAX_BYTES; + return scratch; +} + +/* The projection both GPU load gates share: this die's weight slice + this + * die's KV at the capped window + the per-die GPU scratch. Deliberately NO host + * activation LUT — that models the CPU forward, and charging it to an + * accelerator load is what refused the deployed config at --ctx-cap 131072. Says nothing about + * the per-die reserve: that is the caller's question, and mixing the two up is + * what charged it twice. */ +static uint64_t gpu_die_load_need(uint64_t weights_bytes, uint64_t kv_bytes, + int max_ctx) +{ + return weights_bytes + kv_bytes + pn_admit_die_scratch_bytes(max_ctx); +} + +bool pn_admit_model_load_die(uint64_t weights_bytes, uint64_t kv_bytes, + int max_ctx, uint64_t die_free_bytes, + uint64_t *need_out) +{ + uint64_t need = gpu_die_load_need(weights_bytes, kv_bytes, max_ctx); + if (need_out) + *need_out = need; + + uint64_t die_reserve = pn_die_reserve_bytes(); + uint64_t usable = die_free_bytes > die_reserve ? die_free_bytes - die_reserve : 0; + return need <= usable; +} + +bool pn_admit_model_load_die_budgeted(uint64_t weights_bytes, uint64_t kv_bytes, + int max_ctx, + uint64_t die_budget_free_bytes, + uint64_t *need_out) +{ + uint64_t need = gpu_die_load_need(weights_bytes, kv_bytes, max_ctx); + if (need_out) + *need_out = need; + /* No reserve subtraction: the caller's figure is already net of it. */ + return need <= die_budget_free_bytes; +} + +uint64_t pn_load_spike_bytes(void) +{ + const char *env = getenv("PN_LOAD_SPIKE_BYTES"); + if (env && env[0]) { + char *end = NULL; + unsigned long long v = strtoull(env, &end, 10); + /* Garbage or zero falls back to the default — a zero budget would + * disarm the one gate that models the real 8 GB-box squeeze. */ + if (end != env && v > 0) + return (uint64_t)v; + } + return PN_ADMIT_LOAD_SPIKE_BYTES_DEFAULT; +} + +bool pn_admit_load_spike(uint64_t host_avail_bytes, uint64_t host_total_bytes, + uint64_t *spike_out) +{ + uint64_t spike = pn_load_spike_bytes(); + if (spike_out) + *spike_out = spike; + + uint64_t reserve = pn_host_reserve_bytes(host_total_bytes); + uint64_t budget = host_avail_bytes > reserve ? host_avail_bytes - reserve : 0; + return spike <= budget; +} diff --git a/src/core/mem/pn_admission.h b/src/core/mem/pn_admission.h new file mode 100644 index 0000000..c993d25 --- /dev/null +++ b/src/core/mem/pn_admission.h @@ -0,0 +1,311 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_admission.h + * @brief Pre-flight admission control — the load-bearing MemoryAware fix + * (MemoryAware.md §6). Before Pantheon starts any memory-growing work it + * *computes* what that work will need and refuses / caps it if it won't + * fit within reserved host + per-die headroom, instead of attempting the + * allocation and OOM-rebooting the box (the 32k full-attention prefill) + * or cascading into `cuMemAlloc failed: 2` (the model-load path). + * + * Engine-level by decision (§11 Q7): pn_admit_request lives next to + * pn_load and is called from the serve layer AND is reachable by the + * eval path (eval_ppl) — which is exactly what crash-looped. The cost of + * an N-token prompt is estimated from a measured per-prompt-length LUT + * (§11 Q1) plus a linear per-token KV term; refine to a formula later. + * + * @status Phase 2 — admission gates, now PATH-AWARE: a GPU-resident model + * admits the full context up to max_seq (per-request host cost is ~0, + * measured on a K80 host); the conservative LUT models the CPU-fallback path + * only. Model loads additionally budget the fixed GPU-upload host + * spike (PN_LOAD_SPIKE_BYTES) — the real 8 GB-box squeeze. + * @issues Cost LUT is seeded from the spec's measured points; re-measure on a + * K80 host at 1k/2k/4k/8k/16k/32k and update PN_ADMIT_LUT to tighten. + * mr_die_weight_share (model_registry.c) projects a die's weights from + * the GGUF tensor table, i.e. FILE bytes — but a quant that repacks on + * upload costs more resident VRAM than it costs on disk. Q4_K_M matches + * its file closely; Q3_K_M does not (K80: projected 7425 MiB/die, + * real residency ~11.8 GiB/die including scratch). Both this gate and + * gpu_admit_die_share inherit that under-projection. Fixing it needs + * upload-bytes accounting AND a rethink of the per-die reserve, since + * the real Q3_K_M residency legitimately exceeds total - reserve. + * @todo None + */ + +#ifndef PN_ADMISSION_H +#define PN_ADMISSION_H + +#include +#include + +#include "pn_mem_state.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* The verdict from an admission check. */ +typedef enum { + PN_ADMIT_OK = 0, /* fits — run it */ + PN_ADMIT_CAP = 1, /* won't fit at N, but a smaller window would — */ + /* run capped to `fit_tokens` and tell the client */ + PN_ADMIT_REJECT = 2, /* won't fit at all / host under stall — 503 + wait */ +} pn_admit_verdict; + +/* Result of pn_admit_request: the verdict, a client-facing reason, the largest + * prompt length that WOULD fit (for the CAP path), and the projected bytes. */ +typedef struct { + pn_admit_verdict verdict; + int fit_tokens; /* for CAP: the capped prompt length */ + uint64_t host_need_bytes; /* projected host cost of this request */ + uint64_t host_avail_bytes;/* host allocatable at decision time */ + const char *reason; /* short, client-safe explanation */ +} pn_admit_result; + +/* --- Cost model (measured LUT + linear KV; MemoryAware.md §6.2, §11 Q1) --- */ + +/* Per-token host KV cost — a GENERIC FALLBACK ONLY. The correct number is + * per-model, computed from the loaded model's real geometry by + * pn_kv_bytes_per_token() (pn_kv_geometry.h) and passed into the functions + * below as `kv_per_token`. These constants are used ONLY when the caller passes + * kv_per_token == 0 (geometry unavailable), so a mispriced model can never be + * priced at zero. They model a dense all-layers-KV shape and will mis-price a + * hybrid — always prefer the geometry value. */ +#define PN_ADMIT_KV_BYTES_PER_TOKEN_Q4 5898ULL /* ~5.76 KiB (fallback) */ +#define PN_ADMIT_KV_BYTES_PER_TOKEN_FP32 40960ULL /* ~40 KiB (fallback) */ + +/* Resolve the effective per-token KV bytes: the model-real value when non-zero, + * else the conservative generic constant for the KV precision. Pure. */ +uint64_t pn_admit_kv_per_token(uint64_t kv_per_token, bool kv_q4); + +/* The KV bytes ONE die of a split holds at `max_ctx`: the whole model's KV, + * scaled by the fraction of layers that die owns. The weights are sliced by + * layer range, so the KV projected on top of them has to be sliced the same + * way — charging every die the whole model's KV is a clean 2x over-projection + * of the KV term on a 2-die split, and it grows with the cap, which is where it + * does damage. + * + * `kv_per_token` is the model-real whole-model figure when the caller has a + * loaded model in hand, else 0 for the generic constant (pn_admit_kv_per_token). + * `n_layer <= 0` or `n_layers_here <= 0` means the layout could not be read: + * charge the WHOLE model's KV rather than guess a share, so an unfamiliar + * layout is gated conservatively rather than admitted blind. Pure. */ +uint64_t pn_admit_die_kv_bytes(uint64_t kv_per_token, bool kv_q4, int max_ctx, + int n_layers_here, int n_layer); + +/* GPU-path per-die scratch reserve for the load gates: everything a load puts + * on ONE die that is neither its weight slice nor the single-session KV the + * gate prices separately. The host activation LUT does not apply here — on the + * GPU path the O(N^2) scores live on-die in bounded tiles. + * + * This has two parts, and only one of them is fixed: + * + * BASE — batch prefill activations, flash-attention tiles, the batched + * [slots][n_vocab] logits buffer, captured-graph descriptors and per-die + * staging. These do NOT grow with the context: prefill runs in + * QWEN35_PREFILL_CHUNK-sized chunks, and the decode split partials are + * allocated to PN_QWEN35MOE_DEC_SPLIT_MAX splits, not to seq_len + * (qwen35moe_forward_cuda.c). + * + * PER-TOKEN — the batched serving path allocates the on-die KV as a PER-SLOT + * ring, [nslots][n_head_kv][max_seq][row] per layer plus q4 fp16 sink/window + * shadows (gpu_attn_slot_ring_ensure), while the gates' kv_bytes term prices + * ONE session. The slots the gate does not price are therefore proportional + * to the window the model is capped to: at the default 8 sessions and a + * half-model layer share that is ~24 KiB per token of cap. + * + * So a single fixed constant was a context measurement wearing a constant's + * clothes — it charged a 32k-window price to a model capped at 2048. The + * constants below are calibrated so the total lands EXACTLY on the previously + * measured value at a 32768 window (2-die Q3_K_M @ 32k q4-KV: ~2 GiB non-weight + * per die including the driver context, which the free-VRAM query has already + * paid for): + * + * 512 MiB + 32768 * 32 KiB = 1536 MiB — the old constant, unchanged + * + * and the old constant is also kept as a CEILING, so this repricing can only + * ever charge LESS than before, never more. That matters because the per-token + * slope is slot-dependent — it is calibrated at the default 8 sessions, and a + * serve pinned to `--max-sessions 2` allocates a quarter of that ring. Letting + * the line run past its calibration point would have charged a 131072-window + * production serve 4.6 GiB for a ring that costs it 768 MiB, and refused a + * split that runs today. Above 32768 we have no measurement, so the honest + * thing is to keep charging exactly what we charged before rather than + * extrapolate a number we never took. + * + * Behaviour at and above the one measured point is therefore identical; only + * windows BELOW it are repriced. A 2048-token cap now costs 576 MiB, against a + * measured ~400-480 MiB of real non-weight bytes actually added per die by + * a 2-die Qwen3.6-35B-A3B Q4_K_M load at that cap. */ +#define PN_ADMIT_DIE_SCRATCH_BASE_BYTES (512ULL * 1024 * 1024) /* 512 MiB */ +#define PN_ADMIT_DIE_SCRATCH_BYTES_PER_TOKEN (32ULL * 1024) /* 32 KiB */ +#define PN_ADMIT_DIE_SCRATCH_MAX_BYTES (1536ULL * 1024 * 1024) /* 1.5 GiB */ + +/* The per-die GPU scratch reserve for a model capped to `max_ctx` tokens: the + * fixed base plus the per-token ring term, clamped to + * PN_ADMIT_DIE_SCRATCH_MAX_BYTES so this never charges more than the flat + * constant it replaces. max_ctx <= 0 (no cap known) yields the base alone. + * Pure. */ +uint64_t pn_admit_die_scratch_bytes(int max_ctx); + +/* Model-LOAD host spike (measured on a K80 host 2026-07-09): uploading weights to the + * dies transiently costs ~2.4 GB of host anon RSS for ~55 s (dequant/staging + * buffers riding the slow host link) before releasing to ~1.1 GB steady-state. + * On the 8 GB box THIS transient — not per-request prefill, which is ~0 on the + * GPU path — is the real squeeze, so model-load admission budgets it as a + * fixed cost. Env override PN_LOAD_SPIKE_BYTES. */ +#define PN_ADMIT_LOAD_SPIKE_BYTES_DEFAULT (2400ULL * 1024 * 1024) /* ~2.4 GB */ + +/* Estimate the peak *host* bytes a prefill of n_prompt tokens will transiently + * need: the O(N^2)-ish activation peak (interpolated from the measured LUT) plus + * the linear KV growth. `kv_q4` selects the per-token KV term. Pure — no I/O. + * This is the number the OOM-reboot lacked a check against. */ +uint64_t pn_admit_host_cost(int n_prompt, bool kv_q4, uint64_t kv_per_token); + +/* Estimate the *VRAM* bytes a prefill of n_prompt tokens adds on the model's + * die: the on-die KV growth + flash-attention scratch. Pure. `kv_per_token` is + * the model-real per-token KV bytes (0 = use the generic constant). */ +uint64_t pn_admit_die_cost(int n_prompt, bool kv_q4, uint64_t kv_per_token); + +/* The effective model-load host-spike budget in bytes, honoring the + * PN_LOAD_SPIKE_BYTES env override (values <= 0 or garbage fall back to the + * default). Pure — safe to call anywhere. */ +uint64_t pn_load_spike_bytes(void); + +/* --- The two admission gates (MemoryAware.md §6.1, §6.2) --- */ + +/* Request admission (§6.2), PATH-AWARE. Given the prompt length and the live + * memory state, decide fit / cap / reject. + * + * `gpu_resident` selects the cost model: + * - true (weights uploaded, the CUDA forward is selected): the per-request + * prefill host cost is ~0 — the O(N^2) scores live on-die in bounded flash + * tiles and both host + die KV are preallocated at max_seq by the load + * admission (measured on a K80 host 2026-07-09: serve RSS never moved to ~12.8k). + * The only length-triggered flash->host-softmax fallback fires at + * n > QWEN35_MAX_SEQ (qwen35moe_forward_cuda.c), so the FULL context is + * admitted up to `max_window` and CAPPED only past it. Host can still + * REJECT, but only on genuine pressure (sustained PSI stall, or + * MemAvailable already inside the reserve). + * - false (CPU-forward / host-fallback path): the conservative LUT + linear + * KV cost is checked against host (MemAvailable - reserve) AND, if any die + * is reporting, that die's free VRAM — the path that OOM-rebooted the box. + * + * `die_id` is the model's die (-1 = no die / CPU path, host-only check). + * `max_window` caps the CAP fit_tokens to a runnable window. Fills *out. + * Never allocates, never blocks. + * + * Thread-safe: yes (reads a snapshot copy) + * Reentrant: yes */ +void pn_admit_request(int n_prompt, bool kv_q4, uint64_t kv_per_token, + bool gpu_resident, int die_id, int max_window, + const pn_mem_state *state, pn_admit_result *out); + +/* Model-load admission (§6.1). Before uploading weights to a die, check that + * weights + projected KV @ max ctx + an activation reserve fit within the die's + * free VRAM (from `die_free_bytes`) with the per-die reserve held back. Returns + * true to proceed, false to refuse cleanly (caller logs + picks another die or + * exits with a clear message instead of the failed:2 cascade). `need_out`, if + * non-NULL, receives the projected total need in bytes. + * + * This is the CPU-RESIDENT gate: its activation reserve is the host LUT, which + * models the CPU forward. A load that will run on an accelerator must use + * pn_admit_model_load_die / _die_budgeted instead — the LUT extrapolates to + * tens of GB at a wide window and refuses GPU loads that fit comfortably. + * + * `die_free_bytes` is RAW free VRAM as the hardware reports it — this function + * holds the per-die reserve back itself. Callers holding a die-manager budget + * (pn_dm_die.vram_budget, which is already total minus the reserve) must use a + * _budgeted entry point instead, or the reserve is charged twice. + * + * Thread-safe: yes + * Reentrant: yes */ +bool pn_admit_model_load(uint64_t weights_bytes, int max_ctx, bool kv_q4, + uint64_t kv_per_token, uint64_t die_free_bytes, + uint64_t *need_out); + +/* Per-die model-load admission for a GPU load (§6.1) — the split gate, and the + * single-die gate that mirrors it. Checks that ONE die can hold its share of + * the weights (`weights_bytes`, this die's slice, not the whole model) + its + * share of the projected KV (`kv_bytes`, this die's layers only) + the GPU-path + * scratch reserve for a `max_ctx`-token window, with the per-die reserve held + * back from `die_free_bytes`. Unlike pn_admit_model_load it does NOT add the + * host activation LUT — that models the CPU-forward path; a GPU load's on-die + * scratch is bounded (tiled flash attention). Returns true to proceed, false to + * refuse cleanly. `need_out`, if non-NULL, receives the projected need for this + * die in bytes. + * + * The scratch reserve and the per-die reserve are NOT the same quantity under + * two names, and summing them is not a double count: + * + * - the scratch reserve is an INCREMENT — bytes this load will ADD to the + * die, on top of its weights and single-session KV; + * - the per-die reserve is a FLOOR on free VRAM that must survive the peak, + * so the driver can still satisfy its own late allocations without the + * `cuMemAlloc failed: 2` cascade. + * + * Nor does subtracting the reserve from FREE charge already-resident driver + * overhead twice, which is how it reads at a glance. `die_free_bytes` is + * `total - already_allocated`, so `free - reserve` is identically + * `(total - reserve) - already_allocated`: the reserve is charged once, against + * total, exactly as pn_memwatch.h defines it and as die_manager's + * `vram_budget = total - reserve` applies it. K80 evidence that the sum is not + * over-tight: a 2-die Qwen3.6-35B-A3B Q4_K_M @ ctx-cap 2048 peaked at 10,570 / + * 10,879 MiB of 12,206, i.e. inside `total - reserve` (11,182) on both dies — + * the floor was genuinely respected by the real run, not merely projected. + * + * Thread-safe: yes + * Reentrant: yes */ +bool pn_admit_model_load_die(uint64_t weights_bytes, uint64_t kv_bytes, + int max_ctx, uint64_t die_free_bytes, + uint64_t *need_out); + +/* As pn_admit_model_load_die, but for a caller whose free figure is ALREADY net + * of the per-die reserve — the die manager subtracts it once when it sets + * vram_budget, so anything derived from vram_budget has paid it. + * + * The projection is identical; only the reserve handling differs. Getting that + * pairing wrong has bitten twice, in opposite directions: + * + * - mr_load passed a budget-derived free to the RAW entry point and so + * charged the 1 GiB reserve twice on every registry load, refusing a 2-die + * Qwen3.6-35B-A3B Q4_K_M whose own printed numbers said it fit; + * - and it reached for the CPU-resident projection, so once the gate started + * honouring --ctx-cap the host LUT it carries went with it. At the deployed config's + * --ctx-cap 131072 that charged 48.2 GiB of host activation to an + * accelerator load (K80, 2026-08-21: "die 4 has 11182 MiB free, needs + * 56363 MiB", of which weights were 7425 and KV 737), which would have + * refused the live qwen36-stock serve at startup. + * + * A GPU placement is a GPU load. This is the entry point it belongs at. + * + * Thread-safe: yes + * Reentrant: yes */ +bool pn_admit_model_load_die_budgeted(uint64_t weights_bytes, uint64_t kv_bytes, + int max_ctx, + uint64_t die_budget_free_bytes, + uint64_t *need_out); + +/* Model-load HOST-spike admission (§6.1 refinement). The GPU upload transiently + * costs ~PN_LOAD_SPIKE_BYTES of host anon RSS (see the constant above) — check + * that MemAvailable can absorb that fixed spike on top of the host reserve + * BEFORE the multi-minute upload starts, so a squeezed host refuses the load + * cleanly instead of tipping over mid-upload. Applies to every GPU load path + * (single-die and split) — call once per load attempt. Returns true to + * proceed, false to refuse. `spike_out`, if non-NULL, receives the budgeted + * spike in bytes. + * + * Thread-safe: yes + * Reentrant: yes */ +bool pn_admit_load_spike(uint64_t host_avail_bytes, uint64_t host_total_bytes, + uint64_t *spike_out); + +/* Human-readable verdict name, for logging. */ +const char *pn_admit_verdict_name(pn_admit_verdict v); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_ADMISSION_H */ diff --git a/src/core/mem/pn_host_signals.c b/src/core/mem/pn_host_signals.c new file mode 100644 index 0000000..4f4246e --- /dev/null +++ b/src/core/mem/pn_host_signals.c @@ -0,0 +1,225 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_host_signals.c + * @brief Implementation of the cheap host memory-pressure readers + * (MemoryAware.md §2.1). All parsing is defensive: a missing or + * malformed source leaves its output field 0 ("not observed") rather + * than failing the whole sample. + * + * @status Phase 1 — read-only. + * @issues None + * @todo None + */ + +#include "pn_host_signals.h" + +#include +#include +#include + +#if defined(__APPLE__) +#include +#include +#include +#include +#endif + +/* Read a whole small pseudo-file into buf (NUL-terminated). Returns the byte + * count, or -1 if the file could not be opened. Pseudo-files under /proc and + * the cgroup fs have no stable size, so we read until EOF into a fixed buffer. */ +static long read_small_file(const char *path, char *buf, size_t buf_len) +{ + FILE *f = fopen(path, "re"); + if (!f) + return -1; + + size_t n = fread(buf, 1, buf_len - 1, f); + fclose(f); + buf[n] = '\0'; + return (long)n; +} + +/* Find "key" at the start of a line in a /proc/meminfo-style buffer and return + * its value in kB (meminfo reports kB). Returns true if found. */ +static bool meminfo_field_kb(const char *buf, const char *key, uint64_t *out_kb) +{ + size_t key_len = strlen(key); + const char *p = buf; + + while (p && *p) { + if (strncmp(p, key, key_len) == 0 && p[key_len] == ':') { + const char *v = p + key_len + 1; + while (*v == ' ' || *v == '\t') + v++; + *out_kb = strtoull(v, NULL, 10); + return true; + } + p = strchr(p, '\n'); + if (p) + p++; + } + return false; +} + +bool pn_read_proc_meminfo(uint64_t *total_bytes, uint64_t *avail_bytes) +{ + char buf[4096]; + if (read_small_file("/proc/meminfo", buf, sizeof(buf)) < 0) + return false; + + uint64_t total_kb = 0, avail_kb = 0; + bool have_total = meminfo_field_kb(buf, "MemTotal", &total_kb); + bool have_avail = meminfo_field_kb(buf, "MemAvailable", &avail_kb); + + /* Older kernels lack MemAvailable — approximate with MemFree + Cached so + * the number is never wildly pessimistic. */ + if (!have_avail) { + uint64_t free_kb = 0, cached_kb = 0; + meminfo_field_kb(buf, "MemFree", &free_kb); + meminfo_field_kb(buf, "Cached", &cached_kb); + avail_kb = free_kb + cached_kb; + have_avail = (free_kb != 0 || cached_kb != 0); + } + + if (!have_total) + return false; + + *total_bytes = total_kb * 1024ULL; + *avail_bytes = avail_kb * 1024ULL; + return have_avail; +} + +void pn_read_portable_memory(uint64_t *total_bytes, uint64_t *avail_bytes) +{ + *total_bytes = 0; + *avail_bytes = 0; + +#if defined(__APPLE__) + int64_t memsize = 0; + size_t len = sizeof(memsize); + if (sysctlbyname("hw.memsize", &memsize, &len, NULL, 0) == 0 && memsize > 0) + *total_bytes = (uint64_t)memsize; + + /* Available ~= (free + inactive + speculative) pages: pages the VM can hand + * out without swapping. Mirrors what MemAvailable estimates on Linux. */ + mach_port_t host = mach_host_self(); + vm_size_t page_size = 0; + if (host_page_size(host, &page_size) != KERN_SUCCESS) + page_size = 4096; + + vm_statistics64_data_t vmstat; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + if (host_statistics64(host, HOST_VM_INFO64, + (host_info64_t)&vmstat, &count) == KERN_SUCCESS) { + uint64_t avail_pages = (uint64_t)vmstat.free_count + + (uint64_t)vmstat.inactive_count + + (uint64_t)vmstat.speculative_count; + *avail_bytes = avail_pages * (uint64_t)page_size; + } +#endif +} + +bool pn_read_proc_pressure(double *some_avg10, double *full_avg10) +{ + *some_avg10 = 0.0; + *full_avg10 = 0.0; + + char buf[512]; + if (read_small_file("/proc/pressure/memory", buf, sizeof(buf)) < 0) + return false; + + /* Lines look like: "some avg10=1.23 avg60=... total=...". Scan for the two + * prefixes and pull the avg10 field. */ + const char *some = strstr(buf, "some "); + const char *full = strstr(buf, "full "); + bool any = false; + + if (some) { + const char *a = strstr(some, "avg10="); + if (a) { *some_avg10 = strtod(a + 6, NULL); any = true; } + } + if (full) { + const char *a = strstr(full, "avg10="); + if (a) { *full_avg10 = strtod(a + 6, NULL); any = true; } + } + return any; +} + +/* Resolve this process's cgroup-v2 memory dir from /proc/self/cgroup. On pure + * cgroup-v2 the file has a single "0::" line; is relative to + * /sys/fs/cgroup. Writes the joined dir into out. Returns true on success. */ +static bool cgroup_v2_dir(char *out, size_t out_len) +{ + char buf[512]; + if (read_small_file("/proc/self/cgroup", buf, sizeof(buf)) < 0) + return false; + + const char *marker = strstr(buf, "0::"); + if (!marker) + return false; + const char *rel = marker + 3; + + /* Trim trailing newline. */ + char rel_path[256]; + size_t i = 0; + while (rel[i] && rel[i] != '\n' && i + 1 < sizeof(rel_path)) { + rel_path[i] = rel[i]; + i++; + } + rel_path[i] = '\0'; + + int n = snprintf(out, out_len, "/sys/fs/cgroup%s", rel_path); + return n > 0 && (size_t)n < out_len; +} + +/* Read one cgroup memory file as a u64; a "max" literal maps to 0 ("unset"). */ +static uint64_t read_cgroup_u64(const char *dir, const char *leaf) +{ + char path[512]; + if (snprintf(path, sizeof(path), "%s/%s", dir, leaf) >= (int)sizeof(path)) + return 0; + + char buf[64]; + if (read_small_file(path, buf, sizeof(buf)) < 0) + return 0; + if (strncmp(buf, "max", 3) == 0) + return 0; + return strtoull(buf, NULL, 10); +} + +bool pn_read_cgroup_v2_memory(uint64_t *current, uint64_t *high, uint64_t *max) +{ + *current = 0; + *high = 0; + *max = 0; + + char dir[512]; + if (!cgroup_v2_dir(dir, sizeof(dir))) + return false; + + /* memory.current is the tell that a v2 memory controller is present. */ + char probe[576]; + if (snprintf(probe, sizeof(probe), "%s/memory.current", dir) >= (int)sizeof(probe)) + return false; + char tmp[64]; + if (read_small_file(probe, tmp, sizeof(tmp)) < 0) + return false; + + *current = strtoull(tmp, NULL, 10); + *high = read_cgroup_u64(dir, "memory.high"); + *max = read_cgroup_u64(dir, "memory.max"); + return true; +} + +void pn_host_signals_sample(pn_host_sample *out) +{ + memset(out, 0, sizeof(*out)); + + if (!pn_read_proc_meminfo(&out->total_bytes, &out->avail_bytes)) + pn_read_portable_memory(&out->total_bytes, &out->avail_bytes); + + pn_read_proc_pressure(&out->psi_some_avg10, &out->psi_full_avg10); + pn_read_cgroup_v2_memory(&out->cgroup_current, &out->cgroup_high, + &out->cgroup_max); +} diff --git a/src/core/mem/pn_host_signals.h b/src/core/mem/pn_host_signals.h new file mode 100644 index 0000000..d67d4ea --- /dev/null +++ b/src/core/mem/pn_host_signals.h @@ -0,0 +1,70 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_host_signals.h + * @brief Cheap host memory-pressure readers (MemoryAware.md §2.1). Reads + * /proc/meminfo MemAvailable/MemTotal, /proc/pressure/memory PSI, and + * cgroup-v2 memory.{current,high,max} on Linux; on other platforms fills + * the portable total/available figure and reports the Linux-only signals + * as unobserved (0). Split out from pn_memwatch so the OS-specific /proc + * parsing is isolated and independently testable. + * + * @status Phase 1 — read-only. Mac-testable (portable total/avail path). + * @issues None + * @todo None + */ + +#ifndef PN_HOST_SIGNALS_H +#define PN_HOST_SIGNALS_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* One sample of host memory truth. Zeroed fields mean "not observed" — on + * macOS the PSI/cgroup fields are always 0 (no such interface). */ +typedef struct { + uint64_t total_bytes; /* physical RAM total */ + uint64_t avail_bytes; /* allocatable-without-swap estimate */ + double psi_some_avg10; /* memory PSI "some avg10" %, 0 if unobserved */ + double psi_full_avg10; /* memory PSI "full avg10" %, 0 if unobserved */ + uint64_t cgroup_current; /* cgroup-v2 memory.current, 0 if not in cgroup */ + uint64_t cgroup_high; /* cgroup-v2 memory.high (0 == "max"/unset) */ + uint64_t cgroup_max; /* cgroup-v2 memory.max (0 == "max"/unset) */ +} pn_host_sample; + +/* Take one full host sample (all sources). Never fails destructively: sources + * that can't be read leave their field 0. Cheap (a few small reads, ~1 ms). + * + * Thread-safe: yes (no shared state) + * Reentrant: yes + * Signal-safe: no (uses stdio/open) */ +void pn_host_signals_sample(pn_host_sample *out); + +/* --- Individually testable readers (exposed for unit tests) --- */ + +/* Parse total + available from /proc/meminfo. Returns true on success (Linux), + * false if /proc/meminfo is absent (then the caller uses the portable path). */ +bool pn_read_proc_meminfo(uint64_t *total_bytes, uint64_t *avail_bytes); + +/* Portable RAM total + available fallback for non-Linux (macOS sysctl + + * host_statistics64). Always fills something usable. */ +void pn_read_portable_memory(uint64_t *total_bytes, uint64_t *avail_bytes); + +/* Parse "some"/"full" avg10 from /proc/pressure/memory. Returns true on Linux + * with PSI enabled; leaves outputs 0 and returns false otherwise. */ +bool pn_read_proc_pressure(double *some_avg10, double *full_avg10); + +/* Read this process's cgroup-v2 memory.{current,high,max}. Returns true if the + * process is under a cgroup-v2 memory controller; 0-fills and returns false + * otherwise. A "max" literal maps to 0 (meaning "unset"). */ +bool pn_read_cgroup_v2_memory(uint64_t *current, uint64_t *high, uint64_t *max); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_HOST_SIGNALS_H */ diff --git a/src/core/mem/pn_kv_geometry.c b/src/core/mem/pn_kv_geometry.c new file mode 100644 index 0000000..4a16fdf --- /dev/null +++ b/src/core/mem/pn_kv_geometry.c @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_kv_geometry.c + * @brief Implementation of per-model KV-cache geometry for admission. + * Dispatches on the model's architecture: the qwen35moe/bonsai hybrid + * knows its own attention-layer count (delegated to + * pn_qwen35moe_kv_bytes_per_token); every other supported arch is dense + * and grows KV on all layers as fp32. + * + * @status Phase 1 — used by pn_admit to price requests against real geometry. + * @issues None + * @todo None + */ + +#include "pn_kv_geometry.h" + +#include "../../models/qwen35moe.h" + +/* Dense (all-layers-KV) per-token cost. Dense forwards (llama_forward_cuda and + * the CPU llama path) store the KV cache as fp32 [n_kv_head, max_seq, head_dim] + * per layer, so the fp32 term is the truth here regardless of a Q4 request. */ +static uint64_t dense_kv_bytes_per_token(const pn_config *cfg) +{ + int head_dim = pn_head_dim(cfg); + if (cfg->n_layer <= 0 || cfg->n_kv_head <= 0 || head_dim <= 0) + return 0; + return (uint64_t)cfg->n_layer * (uint64_t)cfg->n_kv_head * 2ULL + * (uint64_t)head_dim * sizeof(float); +} + +uint64_t pn_kv_bytes_per_token(const pn_model *model, bool kv_q4) +{ + if (!model) + return 0; + + /* Hybrid stack (Nex-N2-mini MoE, Bonsai, Ornith): KV grows only on the + * full-attention layers. Both archs store their weights as a + * pn_qwen35moe_model, so the same accessor serves both. */ + if (model->config.arch == PN_ARCH_QWEN35MOE || + model->config.arch == PN_ARCH_BONSAI) { + const pn_qwen35moe_model *qm = + (const pn_qwen35moe_model *)model->weights; + uint64_t per_tok = pn_qwen35moe_kv_bytes_per_token(qm, kv_q4); + if (per_tok > 0) + return per_tok; + } + + /* Every other supported arch is dense — all layers hold fp32 KV. */ + return dense_kv_bytes_per_token(&model->config); +} diff --git a/src/core/mem/pn_kv_geometry.h b/src/core/mem/pn_kv_geometry.h new file mode 100644 index 0000000..a10dafb --- /dev/null +++ b/src/core/mem/pn_kv_geometry.h @@ -0,0 +1,56 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_kv_geometry.h + * @brief Per-model KV-cache geometry for admission (KV Phase 1). + * + * Admission must price a request against the model's REAL KV growth, not + * a one-size constant. A dense model grows KV on every layer; a hybrid + * (qwen35moe / bonsai / ornith) grows it only on its full-attention + * layers, so a fixed PN_ADMIT_KV_BYTES_PER_TOKEN_* mis-prices it by + * several-fold and either over-reserves (fewer sessions) or under-guards + * (silent drop / OOM risk). This module derives the honest whole-model + * per-token KV byte cost from the loaded model's own geometry. + * + * @status Phase 1 — the correctness fix that makes every downstream KV budget + * real (session cap, reject-backstop, KV-usage reporting). + * @issues None + * @todo None + */ + +#ifndef PN_KV_GEOMETRY_H +#define PN_KV_GEOMETRY_H + +#include +#include + +#include "../engine.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Whole-model KV bytes added per admitted token, from THIS model's geometry. + * + * hybrid (qwen35moe / bonsai): n_attn * n_kv_head * 2(K+V) * bytes_per_vec + * — only the full-attention layers hold a growing KV cache; the SSM layers + * carry a fixed recurrent state that does not scale with sequence length. + * dense (llama / qwen / mistral / phi): n_layer * n_kv_head * 2 * bytes_per_vec + * + * `bytes_per_vec` is the packed Q4 row size under `kv_q4` (for the hybrid Q4 KV + * path), else head_dim * sizeof(float). Dense forwards store fp32 KV, so their + * cost is computed fp32 regardless of `kv_q4`. + * + * Returns 0 when the model / geometry is unavailable — callers fall back to the + * conservative generic constant so admission never silently prices at zero. + * + * Thread-safe: yes (reads immutable model config) + * Reentrant: yes + * Signal-safe: yes */ +uint64_t pn_kv_bytes_per_token(const pn_model *model, bool kv_q4); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_KV_GEOMETRY_H */ diff --git a/src/core/mem/pn_ladder.c b/src/core/mem/pn_ladder.c new file mode 100644 index 0000000..ec396be --- /dev/null +++ b/src/core/mem/pn_ladder.c @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_ladder.c + * @brief Implementation of the reactive degradation ladder (MemoryAware.md §7, + * §10 — Phase 4). A pure level->plan policy: given the pressure level + * pn_memwatch publishes and the operator config, decide how far to pull + * each mitigation rung. Monotonic (a higher level never relaxes a rung a + * lower one engaged) and reduce-only (a rung never grows a configured + * value), so the worst a mis-tuned ladder can do is serve conservatively. + * + * @status Phase 4 — reactive ladder rungs 2–4. + * @issues None + * @todo None + */ + +#include "pn_ladder.h" + +#include + +bool pn_ladder_disabled(void) +{ + const char *v = getenv("PN_LADDER_DISABLE"); + return v && v[0] && v[0] != '0'; +} + +/* Rung 2: the effective slot ceiling for a level. Reduce-only, floor of 1. */ +static int slots_for(pn_mem_level level, int cfg_max_slots) +{ + int cfg = cfg_max_slots > 0 ? cfg_max_slots : 1; + if (level >= PN_MEM_LEVEL_CRITICAL) + return PN_LADDER_CRITICAL_SLOTS; + if (level >= PN_LADDER_SHRINK_LEVEL) { + int s = cfg / PN_LADDER_TIGHT_SLOTS_DIV; + return s > 0 ? s : 1; + } + return cfg; +} + +/* Rung 3: the effective prefill window for a level. Reduce-only; 0 base means + * "no ladder clamp" and stays 0. */ +static int window_for(pn_mem_level level, int cfg_prefill_window) +{ + if (cfg_prefill_window <= 0) + return 0; + if (level >= PN_MEM_LEVEL_CRITICAL) + return cfg_prefill_window / PN_LADDER_CRITICAL_WINDOW_DIV; + if (level >= PN_LADDER_CLAMP_LEVEL) + return cfg_prefill_window / PN_LADDER_TIGHT_WINDOW_DIV; + return cfg_prefill_window; +} + +void pn_ladder_plan_for(pn_mem_level level, int cfg_max_slots, + int cfg_prefill_window, bool cfg_kv_q4, + pn_ladder_plan *out) +{ + if (!out) + return; + + int cfg_slots = cfg_max_slots > 0 ? cfg_max_slots : 1; + + /* Disabled, or ample headroom: mirror the config exactly. Keeping OK + * byte-identical to today is what makes wiring the rungs low-risk. */ + if (pn_ladder_disabled() || level <= PN_MEM_LEVEL_OK) { + out->level = level; + out->max_slots = cfg_slots; + out->force_kv_q4 = cfg_kv_q4; + out->kv_evict = false; + out->prefill_window = cfg_prefill_window > 0 ? cfg_prefill_window : 0; + out->degraded = false; + out->note = pn_ladder_disabled() ? "ladder disabled" + : "ample headroom"; + return; + } + + out->level = level; + out->max_slots = slots_for(level, cfg_slots); + out->force_kv_q4 = cfg_kv_q4 || (level >= PN_LADDER_FORCE_Q4_LEVEL); + out->kv_evict = (level >= PN_LADDER_EVICT_LEVEL); + out->prefill_window = window_for(level, cfg_prefill_window); + + out->degraded = out->max_slots < cfg_slots || + (out->force_kv_q4 && !cfg_kv_q4) || + out->kv_evict || + (cfg_prefill_window > 0 && + out->prefill_window < cfg_prefill_window); + + /* One client/log line per level. Ordered worst-first so the message names + * the strongest rung engaged. */ + switch (level) { + case PN_MEM_LEVEL_CRITICAL: + out->note = "server memory critical — single-slot, tightened KV, " + "context window reduced"; + break; + case PN_MEM_LEVEL_TIGHT: + out->note = "server under memory pressure — reduced concurrency, " + "tightened KV, context window may be reduced"; + break; + case PN_MEM_LEVEL_WATCH: + out->note = "server headroom shrinking — KV cache tightened"; + break; + default: + out->note = "ample headroom"; + break; + } +} + +void pn_ladder_plan_from_state(const pn_mem_state *state, int cfg_max_slots, + int cfg_prefill_window, bool cfg_kv_q4, + pn_ladder_plan *out) +{ + pn_mem_level level = state ? state->level : PN_MEM_LEVEL_OK; + pn_ladder_plan_for(level, cfg_max_slots, cfg_prefill_window, cfg_kv_q4, out); +} diff --git a/src/core/mem/pn_ladder.h b/src/core/mem/pn_ladder.h new file mode 100644 index 0000000..79e0491 --- /dev/null +++ b/src/core/mem/pn_ladder.h @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_ladder.h + * @brief The reactive degradation ladder (MemoryAware.md §7, §10 — Phase 4). + * Maps the pressure level pn_memwatch already publishes (pn_mem_level) + * onto concrete, monotonic mitigation "rungs" the engine engages BEFORE + * the box tips over: shrink concurrency, force a tighter KV cache, enable + * low-attention KV eviction, and clamp an over-window prefill. Admission + * (pn_admission) is the per-request fit gate; the ladder is the + * pressure-driven policy that keeps the whole box off the OOM-reboot line + * as headroom shrinks. + * + * Safe-by-construction: at PN_MEM_LEVEL_OK the plan is byte-identical to + * the operator config (full slots, no clamp, configured KV format) — the + * rungs only ever REDUCE from config, and only under real pressure. The + * pure planner has no I/O and no allocation, so it is trivially + * unit-testable off the K80 test machine; PN_LADDER_DISABLE bypasses it entirely for + * the portable / bare-Mac path (MemoryAware.md §11 Q2 — the layer must + * still work launched with no cgroup). + * + * @status Phase 4 — reactive ladder rungs 2–4. Pure planner + wiring hooks. + * @issues Rung-4b eviction exposes a policy flag + hook point; the SnapKV-style + * row selection itself lands with the KV path work. + * @todo None + */ + +#ifndef PN_LADDER_H +#define PN_LADDER_H + +#include + +#include "pn_mem_state.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Rung thresholds (which level first engages each rung). Monotonic and coarse + * on purpose — the levels are already the tuned signal (pn_memwatch host_level / + * die_level). Rationale for the ordering, least-harmful first: + * - Rung 4a force-q4 KV (near-zero quality cost, ~4–7x VRAM win) engages first, + * at WATCH. + * - Rung 2 shrink-concurrency (throughput only, no correctness hit) and Rung 3 + * clamp-prefill and Rung 4b evict engage together at TIGHT and harden at + * CRITICAL. */ +#define PN_LADDER_FORCE_Q4_LEVEL PN_MEM_LEVEL_WATCH /* rung 4a */ +#define PN_LADDER_SHRINK_LEVEL PN_MEM_LEVEL_TIGHT /* rung 2 */ +#define PN_LADDER_CLAMP_LEVEL PN_MEM_LEVEL_TIGHT /* rung 3 */ +#define PN_LADDER_EVICT_LEVEL PN_MEM_LEVEL_TIGHT /* rung 4b */ + +/* Rung 2 concurrency reduction: TIGHT halves the configured slot ceiling, + * CRITICAL floors it. Always at least one slot so a single request can still be + * served (shedding new *work* is admission's job, not the ladder's). */ +#define PN_LADDER_TIGHT_SLOTS_DIV 2 +#define PN_LADDER_CRITICAL_SLOTS 1 + +/* Rung 3 prefill-window clamp: TIGHT clamps an over-window prompt to half the + * configured window, CRITICAL to a quarter. Applied only when a base window is + * known (>0) — 0 means "no ladder clamp" (the caller's own max still applies). */ +#define PN_LADDER_TIGHT_WINDOW_DIV 2 +#define PN_LADDER_CRITICAL_WINDOW_DIV 4 + +/* The mitigation plan for one pressure level. Every field is an absolute value + * the caller applies directly (not a delta), so a caller wires one read without + * having to know the ladder's internal policy. */ +typedef struct { + pn_mem_level level; /* the level this plan was computed for */ + int max_slots; /* rung 2: effective concurrency ceiling (>=1) */ + bool force_kv_q4; /* rung 4a: force Q4 KV regardless of config */ + bool kv_evict; /* rung 4b: enable low-attention KV eviction */ + int prefill_window; /* rung 3: max prefill tokens (0 = no clamp) */ + bool degraded; /* true if any rung reduced a configured value */ + const char *note; /* short client/log note (never NULL) */ +} pn_ladder_plan; + +/* True when the ladder is disabled via PN_LADDER_DISABLE (the plan will mirror + * the operator config at every level). The portable / bare-Mac escape hatch. */ +bool pn_ladder_disabled(void); + +/* Compute the rung plan for a pressure `level` given the operator config: + * cfg_max_slots — configured concurrency ceiling (>=1; clamped up if <1) + * cfg_prefill_window — configured max prefill window in tokens (0 = unknown / + * no base to clamp against; the rung 3 clamp is then a + * no-op and prefill_window stays 0) + * cfg_kv_q4 — whether q4 KV is already the operator default + * Fills *out. Pure — no I/O, no allocation, always succeeds. At level OK (or + * when disabled) the plan equals the config with degraded=false. */ +void pn_ladder_plan_for(pn_mem_level level, int cfg_max_slots, + int cfg_prefill_window, bool cfg_kv_q4, + pn_ladder_plan *out); + +/* Convenience wrapper: derive the plan from a live pn_mem_state snapshot's + * level. `state` may be NULL (treated as level OK). Pure. */ +void pn_ladder_plan_from_state(const pn_mem_state *state, int cfg_max_slots, + int cfg_prefill_window, bool cfg_kv_q4, + pn_ladder_plan *out); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_LADDER_H */ diff --git a/src/core/mem/pn_lazy_zero.c b/src/core/mem/pn_lazy_zero.c new file mode 100644 index 0000000..18a43f2 --- /dev/null +++ b/src/core/mem/pn_lazy_zero.c @@ -0,0 +1,52 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* pn_lazy_zero — mmap-backed, lazily-committed, zero-filled host buffers. + * See pn_lazy_zero.h for the design rationale (Phase 10b / INV3). */ +#include "pn_lazy_zero.h" + +#include +#include + +#ifndef MAP_ANONYMOUS +#define MAP_ANONYMOUS MAP_ANON +#endif + +/* MAP_NORESERVE keeps a mostly-untouched ctx_cap-sized mirror from charging + * the overcommit accounting on Linux; harmless elsewhere. */ +#ifndef MAP_NORESERVE +#define MAP_NORESERVE 0 +#endif + +void *pn_lazy_zero_alloc(size_t bytes) +{ + if (bytes == 0) + return NULL; + void *p = mmap(NULL, bytes, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + return p == MAP_FAILED ? NULL : p; +} + +void pn_lazy_zero_free(void *ptr, size_t bytes) +{ + if (ptr && bytes) + munmap(ptr, bytes); +} + +void pn_lazy_zero_reset(void *ptr, size_t bytes) +{ + if (!ptr || !bytes) + return; +#ifdef __linux__ + /* On private anonymous mappings MADV_DONTNEED is not advisory on Linux: + * it drops the pages and guarantees zero-fill on the next access. */ + if (madvise(ptr, bytes, MADV_DONTNEED) == 0) + return; +#else + /* Portable equivalent: atomically replace the region with a fresh + * zero-fill mapping at the same address. */ + if (mmap(ptr, bytes, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0) != MAP_FAILED) + return; +#endif + memset(ptr, 0, bytes); /* last-resort correctness fallback */ +} diff --git a/src/core/mem/pn_lazy_zero.h b/src/core/mem/pn_lazy_zero.h new file mode 100644 index 0000000..d7fcb51 --- /dev/null +++ b/src/core/mem/pn_lazy_zero.h @@ -0,0 +1,51 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* pn_lazy_zero — mmap-backed, lazily-committed, zero-filled host buffers. + * + * Why this exists (Phase 10b / INV3): the per-session host KV mirrors and SSM + * states were calloc'd at full ctx_cap size and then fully COMMITTED by the + * first pn_state_reset (a full-buffer memset) — ~789 MiB/slot for saber-orch + * at ctx_cap 131072, ~245 MiB/slot for nex-n2-mini at 32000 — even for a + * session holding 200 tokens. glibc makes it worse: after a large free the + * sliding mmap threshold moves these buffers into 64 MiB arenas, where calloc + * memsets them (full commit at allocation) and free() never returns the pages + * to the OS. + * + * These helpers replace calloc/free/memset for such buffers with anonymous + * mmap / munmap / MADV_DONTNEED: + * - alloc: pages are committed only when WRITTEN, so RSS tracks the bytes a + * session actually uses, not its ctx_cap ceiling. + * - free: munmap returns the pages to the OS immediately (no arena + * retention). + * - reset: logically zero-fills the whole buffer AND returns its committed + * pages, in O(pages present): on Linux, MADV_DONTNEED on private + * anonymous memory guarantees the next read sees zeros; elsewhere the + * region is atomically replaced with a fresh zero mapping (MAP_FIXED). + * The pointer is unchanged in both cases. + * + * Use for large (>= ~1 MiB), zero-primed, size-fixed buffers whose committed + * footprint should track usage. Not a general allocator: the caller must + * remember the allocation size and pass it to free/reset. + * + * Thread-safe: yes (no shared state). + */ +#ifndef PN_LAZY_ZERO_H +#define PN_LAZY_ZERO_H + +#include + +/* Allocate `bytes` of zero-filled, lazily-committed memory (page-granular). + * Returns NULL on failure or if bytes == 0. */ +void *pn_lazy_zero_alloc(size_t bytes); + +/* Release a pn_lazy_zero_alloc'd buffer. `bytes` must be the allocation size. + * NULL is a no-op. */ +void pn_lazy_zero_free(void *ptr, size_t bytes); + +/* Zero the buffer's contents and return its committed pages to the OS. + * Equivalent to memset(ptr, 0, bytes) + "give the RAM back". The pointer + * stays valid and unchanged. Falls back to memset if the kernel call fails. + * NULL is a no-op. */ +void pn_lazy_zero_reset(void *ptr, size_t bytes); + +#endif /* PN_LAZY_ZERO_H */ diff --git a/src/core/mem/pn_mem_state.h b/src/core/mem/pn_mem_state.h new file mode 100644 index 0000000..b826a40 --- /dev/null +++ b/src/core/mem/pn_mem_state.h @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_mem_state.h + * @brief Published memory-pressure truth for the MemoryAware layer. A single + * lock-free snapshot (host RAM + PSI + per-die VRAM + a derived pressure + * level) written by pn_memwatch and read by admission / the degradation + * ladder. Transient runtime state only — never persisted (no-auto-save). + * + * @status Phase 1 — accounting + read-only signals. Publishes truth; no + * behavior change. Host signals are Mac/Metal-portable (Linux-only + * fields read 0 off-Linux); per-die VRAM is fed in from the GPU side. + * @issues None + * @todo None + */ + +#ifndef PN_MEM_STATE_H +#define PN_MEM_STATE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Hard ceiling on tracked dies (the K80 test machine has 16 GK210 dies). The state carries a + * fixed array so it stays a plain value type with no allocation. */ +#define PN_MEM_MAX_DIES 16 + +/* Derived pressure level. Ordered: each higher level engages the next rung of + * the degradation ladder (MemoryAware.md §7). Phase 1 only *computes* and logs + * it; later phases react to it. */ +typedef enum { + PN_MEM_LEVEL_OK = 0, /* ample headroom — normal operation */ + PN_MEM_LEVEL_WATCH = 1, /* headroom shrinking — pre-emptive caution */ + PN_MEM_LEVEL_TIGHT = 2, /* reserve nearly reached / PSI rising */ + PN_MEM_LEVEL_CRITICAL = 3, /* at the reserve line — refuse/shed */ +} pn_mem_level; + +/* One immutable snapshot of memory truth. Copied out by readers under a seqlock + * (pn_memwatch_read), so readers always see a self-consistent set of numbers. + * + * Host fields come from /proc on Linux; on macOS the sysctl-backed available + * figure is filled and the Linux-only PSI/cgroup fields read 0 (meaning "not + * observed", treated as no-pressure). Per-die fields are populated from the GPU + * side (pn_memwatch_set_die) and are 0 until a die reports in. */ +typedef struct { + /* --- Host RAM (system-wide) --- */ + uint64_t host_total_bytes; /* MemTotal (or sysctl hw.memsize on macOS) */ + uint64_t host_avail_bytes; /* MemAvailable — allocatable without swap */ + + /* --- Host memory stall (PSI, Linux only; 0 elsewhere) --- */ + double host_psi_some_avg10; /* /proc/pressure/memory "some avg10" (%) */ + double host_psi_full_avg10; /* "full avg10" (%) — whole-box thrash */ + + /* --- Our cgroup slice (Linux cgroup-v2 only; 0 if not under a cgroup) --- */ + uint64_t cgroup_current_bytes; /* memory.current */ + uint64_t cgroup_high_bytes; /* memory.high (0 == "max"/unset) */ + uint64_t cgroup_max_bytes; /* memory.max (0 == "max"/unset) */ + + /* --- Per-die VRAM (fed in from the GPU side) --- */ + int n_dies; /* number of dies with a reported number */ + uint64_t per_die_free[PN_MEM_MAX_DIES]; /* cuMemGetInfo free, bytes */ + uint64_t per_die_budget[PN_MEM_MAX_DIES]; /* usable budget (total - reserve)*/ + uint64_t per_die_used[PN_MEM_MAX_DIES]; /* our own alloc tally, bytes */ + + /* --- Derived --- */ + pn_mem_level level; /* worst-of host + any die */ + uint64_t seq; /* snapshot sequence number (monotonic) */ +} pn_mem_state; + +#ifdef __cplusplus +} +#endif + +#endif /* PN_MEM_STATE_H */ diff --git a/src/core/mem/pn_memwatch.c b/src/core/mem/pn_memwatch.c new file mode 100644 index 0000000..9c5710d --- /dev/null +++ b/src/core/mem/pn_memwatch.c @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_memwatch.c + * @brief Implementation of the MemoryAware poll loop + accounting hub + * (MemoryAware.md §2, §3, §11 Q3/Q6). Owns the single published + * pn_mem_state: the host-signal thread and the GPU-side die feed both + * write it through a seqlock so many readers get a self-consistent copy + * without ever blocking the writer. Derives the pressure level from the + * host reserve line + PSI + per-die budgets and logs on a level change. + * + * @status Phase 10 — level derivation split into HARD (host bytes, die VRAM; + * published immediately) and PSI (leading indicator; passed through a + * dwell filter and a noise floor before it can raise the level). Fixes + * the ladder tripping TIGHT on a healthy box off a 0.2% PSI blip. + * @issues None + * @todo None + */ + +#include "pn_memwatch.h" + +#include +#include +#include +#include +#include +#include + +#include "pn_host_signals.h" + +/* ================================================================ + * Published state (seqlock single-writer / many-reader) + * + * Writers hold g_write_lock so publishes are serialized (the host thread and + * the die feed both write). Readers spin the even/odd seq guard, never block a + * writer, and retry on a torn read. No persistence — transient runtime truth. + * ================================================================ */ + +static pn_mem_state g_state; /* the live snapshot */ +static atomic_uint_least64_t g_seq = 0; /* even = stable, odd = writing */ +static pthread_mutex_t g_write_lock = PTHREAD_MUTEX_INITIALIZER; + +static pthread_t g_thread; +static atomic_bool g_running = false; +static atomic_bool g_stop = false; + +/* ================================================================ + * Reserve policy (pure helpers, env-overridable) + * ================================================================ */ + +/* Parse a byte count from an env var; returns 0 (meaning "unset") if absent or + * unparseable. */ +static uint64_t env_bytes(const char *name) +{ + const char *v = getenv(name); + if (!v || !*v) + return 0; + char *end = NULL; + unsigned long long parsed = strtoull(v, &end, 10); + if (end == v) + return 0; + return (uint64_t)parsed; +} + +uint64_t pn_host_reserve_bytes(uint64_t host_total_bytes) +{ + uint64_t override = env_bytes("PN_HOST_RESERVE_BYTES"); + if (override) + return override; + + uint64_t frac = (uint64_t)(PN_HOST_RESERVE_FRACTION * (double)host_total_bytes); + return frac > PN_HOST_RESERVE_MIN_BYTES ? frac : PN_HOST_RESERVE_MIN_BYTES; +} + +uint64_t pn_die_reserve_bytes(void) +{ + uint64_t override = env_bytes("PN_DIE_RESERVE_BYTES"); + return override ? override : PN_DIE_RESERVE_BYTES; +} + +/* Read a positive percentage from an env var, falling back to `dflt`. A zero, + * negative or unparseable value falls back — a zero threshold would re-arm the + * any-non-zero-PSI-is-pressure behavior these thresholds exist to remove. */ +static double env_pct(const char *name, double dflt) +{ + const char *v = getenv(name); + if (v && *v) { + char *end = NULL; + double parsed = strtod(v, &end); + if (end != v && parsed > 0.0) + return parsed; + } + return dflt; +} + +double pn_psi_full_reject_pct(void) +{ + return env_pct("PN_PSI_FULL_REJECT", PN_PSI_FULL_REJECT_DEFAULT); +} + +double pn_psi_full_tight_pct(void) +{ + return env_pct("PN_PSI_FULL_TIGHT", PN_PSI_FULL_TIGHT_DEFAULT); +} + +double pn_psi_full_watch_pct(void) +{ + return env_pct("PN_PSI_FULL_WATCH", PN_PSI_FULL_WATCH_DEFAULT); +} + +double pn_psi_some_tight_pct(void) +{ + return env_pct("PN_PSI_SOME_TIGHT", PN_PSI_SOME_TIGHT_DEFAULT); +} + +double pn_psi_some_watch_pct(void) +{ + return env_pct("PN_PSI_SOME_WATCH", PN_PSI_SOME_WATCH_DEFAULT); +} + +int pn_psi_trip_polls(void) +{ + const char *v = getenv("PN_PSI_TRIP_POLLS"); + if (v && *v) { + char *end = NULL; + long parsed = strtol(v, &end, 10); + if (end != v && parsed >= 1) + return (int)parsed; + } + return PN_PSI_TRIP_POLLS_DEFAULT; +} + +const char *pn_mem_level_name(pn_mem_level level) +{ + switch (level) { + case PN_MEM_LEVEL_OK: return "OK"; + case PN_MEM_LEVEL_WATCH: return "WATCH"; + case PN_MEM_LEVEL_TIGHT: return "TIGHT"; + case PN_MEM_LEVEL_CRITICAL: return "CRITICAL"; + } + return "?"; +} + +/* ================================================================ + * Level derivation + * ================================================================ */ + +/* Host BYTE pressure from the reserve line alone. A fact, not a prediction: + * the reserve is off-limits, so escalate as avail approaches it. */ +static pn_mem_level host_byte_level(const pn_mem_state *s) +{ + uint64_t reserve = pn_host_reserve_bytes(s->host_total_bytes); + + if (s->host_avail_bytes <= reserve) + return PN_MEM_LEVEL_CRITICAL; + + uint64_t slack = s->host_avail_bytes - reserve; + /* WATCH when the slack above the reserve dips under one reserve's worth; + * TIGHT when it dips under a quarter. Coarse but monotonic. */ + if (slack < reserve / 4) + return PN_MEM_LEVEL_TIGHT; + if (slack < reserve) + return PN_MEM_LEVEL_WATCH; + return PN_MEM_LEVEL_OK; +} + +/* Worst per-die level: escalate as a die's free VRAM approaches exhaustion. A + * die with no reported budget (0) is skipped. */ +static pn_mem_level die_level(const pn_mem_state *s) +{ + pn_mem_level worst = PN_MEM_LEVEL_OK; + for (int i = 0; i < s->n_dies && i < PN_MEM_MAX_DIES; i++) { + uint64_t budget = s->per_die_budget[i]; + if (budget == 0) + continue; + uint64_t free = s->per_die_free[i]; + pn_mem_level lvl = PN_MEM_LEVEL_OK; + if (free == 0) + lvl = PN_MEM_LEVEL_CRITICAL; + else if (free < budget / 16) + lvl = PN_MEM_LEVEL_TIGHT; + else if (free < budget / 8) + lvl = PN_MEM_LEVEL_WATCH; + if (lvl > worst) + worst = lvl; + } + return worst; +} + +pn_mem_level pn_mem_hard_level(const pn_mem_state *s) +{ + if (!s) + return PN_MEM_LEVEL_OK; + pn_mem_level h = host_byte_level(s); + pn_mem_level d = die_level(s); + return h > d ? h : d; +} + +/* What the PSI stall counters predict, on their own. Proportionate: PSI avg10 + * is a decaying 10 s average that sits slightly above zero for ~a minute after + * any brief reclaim, so a reading has to clear a noise floor before it means + * anything. See the threshold block in pn_memwatch.h for the measurements. */ +pn_mem_level pn_mem_psi_level(const pn_mem_state *s) +{ + if (!s) + return PN_MEM_LEVEL_OK; + + pn_mem_level lvl = PN_MEM_LEVEL_OK; + + if (s->host_psi_full_avg10 >= pn_psi_full_reject_pct()) + lvl = PN_MEM_LEVEL_CRITICAL; + else if (s->host_psi_full_avg10 >= pn_psi_full_tight_pct()) + lvl = PN_MEM_LEVEL_TIGHT; + else if (s->host_psi_full_avg10 >= pn_psi_full_watch_pct()) + lvl = PN_MEM_LEVEL_WATCH; + + if (s->host_psi_some_avg10 >= pn_psi_some_tight_pct() && + lvl < PN_MEM_LEVEL_TIGHT) + lvl = PN_MEM_LEVEL_TIGHT; + else if (s->host_psi_some_avg10 >= pn_psi_some_watch_pct() && + lvl < PN_MEM_LEVEL_WATCH) + lvl = PN_MEM_LEVEL_WATCH; + + return lvl; +} + +void pn_psi_dwell_update(pn_psi_dwell *d, pn_mem_level raw) +{ + if (!d) + return; + + /* Genuine thrash: publish on the first sample, never delay protection. */ + if (raw >= PN_MEM_LEVEL_CRITICAL) { + d->trips = pn_psi_trip_polls(); + d->effective = raw; + return; + } + + /* Back to quiet: clear the counter and relax immediately (relax-fast is + * what lets a clamped window re-widen the moment the box is healthy). */ + if (raw <= PN_MEM_LEVEL_OK) { + d->trips = 0; + d->effective = PN_MEM_LEVEL_OK; + return; + } + + /* Elevated but sub-CRITICAL: it must HOLD before it engages a rung. */ + int need = pn_psi_trip_polls(); + if (d->trips < need) + d->trips++; + if (d->trips >= need) + d->effective = raw; /* sustained — and follows raw down too */ +} + +/* The dwell filter's state. Only the host poll advances it (PSI is a host + * signal sampled on that loop's cadence); the die feed republishes with + * whatever the filter last settled on. Guarded by g_write_lock. */ +static pn_psi_dwell g_psi_dwell = { 0, PN_MEM_LEVEL_OK }; + +pn_mem_level pn_mem_combine_level(pn_mem_level hard, pn_mem_level psi) +{ + /* PSI is a LEADING indicator of a squeeze — it is not evidence of one. + * Streaming 17 GB of GGUF off disk to fill eight dies drives full-PSI to + * ~6% for several seconds (measured on a K80 host during a 3-model load) while + * MemAvailable never leaves 6.3 GB of 7.8 GB — the box is dropping page + * cache it does not need, which is the kernel working correctly, not the + * box running out of memory. + * + * So below the whole-box-thrash line, PSI on its own may raise the level to + * WATCH — which engages only rung 4a (force Q4 KV, near-zero cost) — but no + * further unless the byte math corroborates. The moment host headroom or a + * die actually shows a squeeze (hard > OK), PSI is trusted in full and + * escalates on top of it, which is exactly what a leading indicator is for. + * + * A PSI-CRITICAL reading (>= the reject line) is NEVER capped: at that point + * the whole box is stalled on memory and MemAvailable's opinion is + * irrelevant. */ + if (psi > PN_MEM_LEVEL_WATCH && psi < PN_MEM_LEVEL_CRITICAL && + hard <= PN_MEM_LEVEL_OK) + psi = PN_MEM_LEVEL_WATCH; + + return hard > psi ? hard : psi; +} + +static pn_mem_level derive_level(const pn_mem_state *s) +{ + return pn_mem_combine_level(pn_mem_hard_level(s), g_psi_dwell.effective); +} + +/* ================================================================ + * Publish (seqlock write) + logging + * ================================================================ */ + +/* Publish an updated snapshot: bump seq odd, copy in, bump seq even. Callers + * hold g_write_lock. Logs when the derived level changes. */ +static void publish_locked(const pn_mem_state *next) +{ + pn_mem_level prev_level = g_state.level; + uint64_t next_seq = g_state.seq + 1; + + atomic_fetch_add_explicit(&g_seq, 1, memory_order_release); /* -> odd */ + + g_state = *next; + g_state.seq = next_seq; + + atomic_fetch_add_explicit(&g_seq, 1, memory_order_release); /* -> even */ + + if (g_state.level != prev_level) { + /* Name the DRIVING signal, not just the numbers. Phase 10 cost a + * diagnosis round-trip precisely because the old line printed host + * avail + PSI side by side and left the reader to infer which one + * escalated — and the answer (PSI) was the counter-intuitive one. */ + pn_mem_level hard = pn_mem_hard_level(&g_state); + pn_mem_level byte_lvl = host_byte_level(&g_state); + pn_mem_level die_lvl = die_level(&g_state); + const char *driver = "host-bytes"; + if (g_state.level <= PN_MEM_LEVEL_OK) + driver = "none"; /* de-escalated to OK: nothing drives */ + else if (g_state.level > hard) + driver = "psi"; + else if (die_lvl >= byte_lvl && die_lvl == hard) + driver = "die-vram"; + + fprintf(stderr, + "[memwatch] level %s -> %s by=%s | host avail %llu MiB / %llu MiB " + "(bytes=%s) psi(some %.1f full %.1f -> %s, dwell %d/%d) " + "dies=%d (vram=%s)\n", + pn_mem_level_name(prev_level), pn_mem_level_name(g_state.level), + driver, + (unsigned long long)(g_state.host_avail_bytes >> 20), + (unsigned long long)(g_state.host_total_bytes >> 20), + pn_mem_level_name(byte_lvl), + g_state.host_psi_some_avg10, g_state.host_psi_full_avg10, + pn_mem_level_name(g_psi_dwell.effective), + g_psi_dwell.trips, pn_psi_trip_polls(), + g_state.n_dies, pn_mem_level_name(die_lvl)); + } +} + +void pn_memwatch_read(pn_mem_state *out) +{ + for (;;) { + uint64_t s1 = atomic_load_explicit(&g_seq, memory_order_acquire); + if (s1 & 1u) + continue; /* a write is in progress — retry */ + *out = g_state; + uint64_t s2 = atomic_load_explicit(&g_seq, memory_order_acquire); + if (s1 == s2) + return; /* stable across the copy */ + } +} + +/* ================================================================ + * Host-signal poll: sample -> merge into state -> publish + * ================================================================ */ + +static void poll_host_once(void) +{ + pn_host_sample sample; + pn_host_signals_sample(&sample); + + pthread_mutex_lock(&g_write_lock); + + pn_mem_state next = g_state; /* keep die numbers, refresh host */ + next.host_total_bytes = sample.total_bytes; + next.host_avail_bytes = sample.avail_bytes; + next.host_psi_some_avg10 = sample.psi_some_avg10; + next.host_psi_full_avg10 = sample.psi_full_avg10; + next.cgroup_current_bytes = sample.cgroup_current; + next.cgroup_high_bytes = sample.cgroup_high; + next.cgroup_max_bytes = sample.cgroup_max; + + /* Advance the PSI dwell filter on this sample BEFORE deriving the level: + * a sub-CRITICAL PSI escalation only counts once it has held for + * pn_psi_trip_polls() consecutive polls. */ + pn_psi_dwell_update(&g_psi_dwell, pn_mem_psi_level(&next)); + + next.level = derive_level(&next); + + publish_locked(&next); + + pthread_mutex_unlock(&g_write_lock); +} + +void pn_memwatch_poll_now(void) +{ + poll_host_once(); +} + +void pn_memwatch_set_die(int die_id, uint64_t free_bytes, + uint64_t budget_bytes, uint64_t used_bytes) +{ + if (die_id < 0 || die_id >= PN_MEM_MAX_DIES) + return; + + pthread_mutex_lock(&g_write_lock); + + pn_mem_state next = g_state; + next.per_die_free[die_id] = free_bytes; + next.per_die_budget[die_id] = budget_bytes; + next.per_die_used[die_id] = used_bytes; + if (die_id + 1 > next.n_dies) + next.n_dies = die_id + 1; + next.level = derive_level(&next); + + publish_locked(&next); + + pthread_mutex_unlock(&g_write_lock); +} + +/* ================================================================ + * Poll thread + * ================================================================ */ + +static void *memwatch_thread(void *arg) +{ + (void)arg; + while (!atomic_load_explicit(&g_stop, memory_order_acquire)) { + poll_host_once(); + struct timespec ts = { + .tv_sec = PN_MEMWATCH_POLL_MS / 1000, + .tv_nsec = (long)(PN_MEMWATCH_POLL_MS % 1000) * 1000000L, + }; + nanosleep(&ts, NULL); + } + return NULL; +} + +int pn_memwatch_start(void) +{ + bool expected = false; + if (!atomic_compare_exchange_strong(&g_running, &expected, true)) + return 0; /* already running — idempotent */ + + atomic_store(&g_stop, false); + + /* Seed one synchronous poll so readers see real numbers immediately. */ + poll_host_once(); + + if (pthread_create(&g_thread, NULL, memwatch_thread, NULL) != 0) { + atomic_store(&g_running, false); + fprintf(stderr, "[memwatch] failed to start poll thread\n"); + return -1; + } + + fprintf(stderr, "[memwatch] poll thread started (%d ms)\n", PN_MEMWATCH_POLL_MS); + return 0; +} + +void pn_memwatch_stop(void) +{ + if (!atomic_load(&g_running)) + return; + atomic_store(&g_stop, true); + pthread_join(g_thread, NULL); + atomic_store(&g_running, false); +} + +bool pn_memwatch_running(void) +{ + return atomic_load(&g_running); +} diff --git a/src/core/mem/pn_memwatch.h b/src/core/mem/pn_memwatch.h new file mode 100644 index 0000000..a1bd5e3 --- /dev/null +++ b/src/core/mem/pn_memwatch.h @@ -0,0 +1,191 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pn_memwatch.h + * @brief The MemoryAware poll loop + accounting hub (MemoryAware.md §2, §3). + * One low-priority thread reads the cheap host signals (/proc/meminfo + * MemAvailable, /proc/pressure/memory PSI, cgroup-v2 memory.*) every + * ~250 ms and publishes a pn_mem_state snapshot. The GPU side feeds in + * per-die VRAM numbers; admission and the degradation ladder read the + * published truth. Unified in src/core/mem/ (§11 Q3) so host pressure is + * tracked on Mac/Metal too, not just a K80 host. + * + * Library-first: the reserve config and the die numbers are passed in, + * not hard-coded here. Boot-measured per-die budget (§11 Q6) is applied + * by the caller before feeding pn_memwatch_set_die. + * + * @status Phase 1 — publishes truth, logs on level change, no behavior change. + * @issues None + * @todo None + */ + +#ifndef PN_MEMWATCH_H +#define PN_MEMWATCH_H + +#include +#include + +#include "pn_mem_state.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/* Host-headroom reserve (MemoryAware.md §6.3). Admission treats this many bytes + * of host RAM as off-limits so the kernel, sshd, the WG tunnel and the serve + * loop always have room. Default policy: max(PN_HOST_RESERVE_MIN_BYTES, + * PN_HOST_RESERVE_FRACTION * MemTotal). Env override PN_HOST_RESERVE_BYTES. */ +#define PN_HOST_RESERVE_MIN_BYTES (1024ULL * 1024 * 1024) /* 1 GiB */ +#define PN_HOST_RESERVE_FRACTION 0.15 /* 15% of MemTotal */ + +/* Per-die VRAM reserve (MemoryAware.md §6.3): driver overhead + fragmentation + * slack held back from each die's measured total. Env override + * PN_DIE_RESERVE_BYTES. The budget the GPU side reports is total - reserve. */ +#define PN_DIE_RESERVE_BYTES (1024ULL * 1024 * 1024) /* ~1 GiB */ + +/* Poll cadence for the host-signal loop. */ +#define PN_MEMWATCH_POLL_MS 250 + +/* PSI-full REJECT threshold (percent of wall time the WHOLE box was stalled on + * memory, avg10). Below this, PSI-full is treated as a transient blip — e.g. + * the page-cache churn right after a ~90 s model load leaves avg10 slightly + * above 0 for a full decay minute — and requests are still admitted on the + * byte math. At or above it the box is genuinely thrashing and new work is + * rejected. Env override PN_PSI_FULL_REJECT (percent, float). */ +#define PN_PSI_FULL_REJECT_DEFAULT 15.0 + +/* --- PSI escalation thresholds (Phase 10) ----------------------------------- + * + * PSI avg10 is a *decaying 10-second average*, so it reads slightly above zero + * for ~a minute after any brief reclaim — a model load, page-cache churn, a + * log rotation. Treating "> 0" as pressure is what tripped the ladder to TIGHT + * on a healthy K80 host (log 2026-07-27: `OK -> TIGHT | host avail 5578 MiB / + * 7845 MiB psi(some 0.2 full 0.2)`, then `TIGHT -> OK | host avail 5071 MiB … + * psi(some 0.0 full 0.0)` — it relaxed at a LOWER avail, so PSI, not bytes, was + * the trigger). full avg10 = 0.2 means the whole box stalled for 20 ms out of + * the last 10 s. That is noise, not pressure. + * + * So PSI now escalates PROPORTIONATELY, with a noise floor, instead of snapping + * to TIGHT on any non-zero reading. All four are percentages, env-overridable. + * The REJECT threshold above is unchanged and still maps to CRITICAL. */ +#define PN_PSI_FULL_TIGHT_DEFAULT 5.0 /* PN_PSI_FULL_TIGHT -> TIGHT */ +#define PN_PSI_FULL_WATCH_DEFAULT 1.0 /* PN_PSI_FULL_WATCH -> WATCH */ +#define PN_PSI_SOME_TIGHT_DEFAULT 10.0 /* PN_PSI_SOME_TIGHT -> TIGHT */ +#define PN_PSI_SOME_WATCH_DEFAULT 5.0 /* PN_PSI_SOME_WATCH -> WATCH */ + +/* Consecutive polls a sub-CRITICAL PSI escalation must HOLD before it is + * allowed to raise the published level (~1 s at a 250 ms cadence). PSI is a + * leading indicator sampled every 250 ms; a single elevated sample must not be + * able to engage a degradation rung. De-escalation is immediate (relax fast) and + * a CRITICAL PSI reading bypasses the dwell entirely — real thrash is never + * delayed. Hard signals (byte headroom, per-die free VRAM) are never delayed: + * they are facts, not leading indicators. Env override PN_PSI_TRIP_POLLS. */ +#define PN_PSI_TRIP_POLLS_DEFAULT 4 + +/* Compute the effective host reserve in bytes for a given MemTotal, honoring + * the PN_HOST_RESERVE_BYTES env override. Pure — safe to call anywhere. */ +uint64_t pn_host_reserve_bytes(uint64_t host_total_bytes); + +/* Compute the effective per-die reserve in bytes, honoring the + * PN_DIE_RESERVE_BYTES env override. Pure. */ +uint64_t pn_die_reserve_bytes(void); + +/* The effective PSI-full avg10 percentage at/above which admission rejects new + * work, honoring the PN_PSI_FULL_REJECT env override (values <= 0 or garbage + * fall back to the default — a zero threshold would re-arm the transient-blip + * hypersensitivity). Pure — safe to call anywhere. */ +double pn_psi_full_reject_pct(void); + +/* The effective PSI escalation thresholds, honoring their env overrides + * (PN_PSI_FULL_TIGHT / PN_PSI_FULL_WATCH / PN_PSI_SOME_TIGHT / + * PN_PSI_SOME_WATCH; values <= 0 or garbage fall back to the default — a zero + * threshold would re-arm the any-non-zero-is-pressure hypersensitivity this + * replaced). Pure. */ +double pn_psi_full_tight_pct(void); +double pn_psi_full_watch_pct(void); +double pn_psi_some_tight_pct(void); +double pn_psi_some_watch_pct(void); + +/* Consecutive polls a sub-CRITICAL PSI escalation must hold before it counts, + * honoring PN_PSI_TRIP_POLLS (values < 1 fall back to the default). Pure. */ +int pn_psi_trip_polls(void); + +/* --- Level derivation, split by evidence class (pure; exposed for tests) --- + * + * HARD signals are facts about bytes we can measure right now: host + * MemAvailable against the reserve line, and each die's free VRAM against its + * budget. They escalate immediately. + * + * The PSI level is what the stall counters *predict*. It is noisy and decays + * slowly, so it is passed through the dwell filter below before it is allowed + * to raise the published level. */ +pn_mem_level pn_mem_hard_level(const pn_mem_state *s); +pn_mem_level pn_mem_psi_level(const pn_mem_state *s); + +/* Dwell filter for the PSI level: a sub-CRITICAL escalation must be observed on + * pn_psi_trip_polls() consecutive samples before `effective` follows it; any + * sample back at OK clears the counter and relaxes immediately. CRITICAL is + * published on the first sample. Caller-owned state so it is unit-testable + * without a poll loop; pn_memwatch owns one instance internally. */ +typedef struct { + int trips; /* consecutive elevated samples so far */ + pn_mem_level effective; /* the level the filter currently publishes */ +} pn_psi_dwell; + +void pn_psi_dwell_update(pn_psi_dwell *d, pn_mem_level raw); + +/* Combine the hard level with the (dwell-filtered) PSI level into the level + * that gets published. Sub-CRITICAL PSI is capped at WATCH while the hard + * signals say OK: a stall with ample measured headroom is reclaim churn, not + * exhaustion, so it may engage only the free rung. Once the byte math or a die + * corroborates, PSI escalates in full. PSI-CRITICAL is never capped. Pure. */ +pn_mem_level pn_mem_combine_level(pn_mem_level hard, pn_mem_level psi); + +/* Start the background poll thread. Idempotent — a second call is a no-op while + * one is already running. Returns 0 on success, negative on failure. The thread + * reads only host signals; die numbers arrive via pn_memwatch_set_die. + * + * Thread-safe: yes (idempotent start guard) + * Reentrant: no + * Signal-safe: no */ +int pn_memwatch_start(void); + +/* Stop and join the poll thread. Safe to call if never started. */ +void pn_memwatch_stop(void); + +/* True once pn_memwatch_start has succeeded and the loop is live. */ +bool pn_memwatch_running(void); + +/* Read a self-consistent copy of the current published state into *out. Always + * succeeds (returns a zeroed-but-valid snapshot before the first poll). Uses a + * seqlock, so it never blocks the writer. + * + * Thread-safe: yes (lock-free reader) + * Reentrant: yes + * Signal-safe: no (loops on a torn read) */ +void pn_memwatch_read(pn_mem_state *out); + +/* Feed one die's VRAM numbers in from the GPU side (MemoryAware.md §2.2, §11 + * Q3). `free_bytes` is the latest cuMemGetInfo free; `budget_bytes` is the + * boot-measured usable budget (total - reserve); `used_bytes` is our own + * per-die alloc tally. Republishes the level. die_id must be < PN_MEM_MAX_DIES. + * + * Thread-safe: yes (single-writer publish is serialized internally) + * Reentrant: no */ +void pn_memwatch_set_die(int die_id, uint64_t free_bytes, + uint64_t budget_bytes, uint64_t used_bytes); + +/* Force one immediate host-signal poll + publish, independent of the loop. Lets + * a caller get fresh truth at a decision point (e.g. just before admission) + * without waiting for the next tick. Safe to call whether or not the loop is + * running. */ +void pn_memwatch_poll_now(void); + +/* Human-readable level name, for logging. */ +const char *pn_mem_level_name(pn_mem_level level); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_MEMWATCH_H */ diff --git a/src/core/ngram_draft.c b/src/core/ngram_draft.c new file mode 100644 index 0000000..46b1540 --- /dev/null +++ b/src/core/ngram_draft.c @@ -0,0 +1,75 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file ngram_draft.c + * @brief Prompt-lookup n-gram drafter (see ngram_draft.h). A linear backward + * scan per n-gram length: history stays a plain array (a few thousand + * tokens at serving lengths), so a scan is ~microseconds against a + * ~50 ms K80 forward — not worth an index. + * + * @status Working; covered by tests/test_spec_qwen35.c on the K80 test machine. + * @issues None + * @todo None + */ + +/* 1. This module's own header */ +#include "ngram_draft.h" + +/* 2. Standard library */ +#include + +int pn_ngram_draft(const uint32_t *hist, int n_hist, int ngram_max, + int ngram_min, int max_k, uint32_t *out) +{ + if (!hist || !out || max_k <= 0 || ngram_min < 1 || + ngram_max < ngram_min) + return 0; + + for (int g = ngram_max; g >= ngram_min; g--) { + if (n_hist < g + 1) + continue; + const uint32_t *key = hist + n_hist - g; + /* Most recent earlier occurrence; start excludes the trailing + * self-match (start + g <= n_hist - 1 leaves >= 1 continuation). */ + for (int start = n_hist - g - 1; start >= 0; start--) { + if (memcmp(hist + start, key, (size_t)g * sizeof(*hist)) != 0) + continue; + int avail = n_hist - (start + g); + int k = avail < max_k ? avail : max_k; + memcpy(out, hist + start + g, (size_t)k * sizeof(*hist)); + return k; + } + } + return 0; +} + +void pn_spec_throttle_init(pn_spec_throttle *t, int min_accept, + int miss_limit, int cool_steps) +{ + t->min_accept = min_accept > 0 ? min_accept : 4; + t->miss_limit = miss_limit > 0 ? miss_limit : 2; + t->cool_steps = cool_steps > 0 ? cool_steps : 48; + t->miss = 0; + t->cool = 0; +} + +int pn_spec_throttle_gate(pn_spec_throttle *t) +{ + if (t->cool > 0) { + t->cool--; + return 0; + } + return 1; +} + +void pn_spec_throttle_report(pn_spec_throttle *t, int accepted_drafts) +{ + if (accepted_drafts >= t->min_accept) { + t->miss = 0; + return; + } + if (++t->miss >= t->miss_limit) { + t->miss = 0; + t->cool = t->cool_steps; + } +} diff --git a/src/core/ngram_draft.h b/src/core/ngram_draft.h new file mode 100644 index 0000000..8526914 --- /dev/null +++ b/src/core/ngram_draft.h @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file ngram_draft.h + * @brief Prompt-lookup n-gram drafter for speculative decoding: proposes the + * continuation of the most recent earlier occurrence of the current + * token-history suffix. Host-side and model-free — the "draft model" + * is the sequence's own repetition (code, quoted context, templated + * text), so drafting costs microseconds and every accepted draft is + * verified by the real model before it is emitted. + * + * @status Working; used by the qwen35moe speculative greedy decode drivers. + * @issues None + * @todo None + */ + +#ifndef PN_NGRAM_DRAFT_H +#define PN_NGRAM_DRAFT_H + +#include + +/** + * Propose up to max_k draft tokens from the token history (prompt + all + * committed output, oldest first). + * + * For g = ngram_max down to ngram_min, take the last g history tokens as the + * key and search BACKWARD (most recent match first) for an earlier occurrence + * of that g-gram; on a hit, propose the tokens that followed it. Longer + * n-grams are tried first (higher precision), the backward scan biases toward + * recent repetition, and the trailing self-match is excluded. + * + * @param hist Token history, oldest first. + * @param n_hist Number of history tokens. + * @param ngram_max Longest suffix length to try (e.g. 3). + * @param ngram_min Shortest suffix length to try (e.g. 2, >= 1). + * @param max_k Draft budget. + * @param out Receives the draft tokens (up to max_k). + * @return Number of draft tokens written (0 = no match; the caller + * should fall back to plain single-token decode). + * + * Thread-safe: yes (no state) + * Reentrant: yes + * Signal-safe: yes + */ +int pn_ngram_draft(const uint32_t *hist, int n_hist, int ngram_max, + int ngram_min, int max_k, uint32_t *out); + +/* ============================================================ + * Draft throttle — the economics guard. + * + * On the K80 a batched verify of K+1 positions costs several plain + * forwards, so a verify only pays when enough drafts are accepted + * (min_accept ~ verify_cost / forward_cost). Output phases that draft + * poorly (e.g. free-form reasoning text) must ride at plain-decode speed: + * after miss_limit consecutive verifies accepting < min_accept drafts, + * drafting pauses for cool_steps plain steps, then probes again. Purely a + * WHEN-to-draft policy — accepted tokens are always the model's exact + * greedy argmaxes, so output is identical under any throttle setting. + * ============================================================ */ + +typedef struct { + int min_accept; /* accepted drafts below this counts as a miss */ + int miss_limit; /* consecutive misses before cooling */ + int cool_steps; /* plain steps to ride before probing again */ + int miss; /* state: consecutive misses so far */ + int cool; /* state: plain steps left in the cooldown */ +} pn_spec_throttle; + +/* Initialize (also resets state). Non-positive args pick the defaults + * (min_accept 4, miss_limit 2, cool_steps 48). */ +void pn_spec_throttle_init(pn_spec_throttle *t, int min_accept, + int miss_limit, int cool_steps); + +/* 1 = drafting allowed this step; 0 = ride plain (consumes a cool step). */ +int pn_spec_throttle_gate(pn_spec_throttle *t); + +/* Report a verify's outcome (number of ACCEPTED DRAFTS, i.e. n_accept-1). */ +void pn_spec_throttle_report(pn_spec_throttle *t, int accepted_drafts); + +#endif /* PN_NGRAM_DRAFT_H */ diff --git a/src/core/prefix_cache.c b/src/core/prefix_cache.c new file mode 100644 index 0000000..c166e8d --- /dev/null +++ b/src/core/prefix_cache.c @@ -0,0 +1,899 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file prefix_cache.c + * @brief Session-based prefix cache (see prefix_cache.h). Keeps a cached + * pn_state per conversation of a session id (see find_conversation) + * so an agent turn prefills only the delta + * tokens appended to a fixed prefix instead of re-prefilling the whole + * context. The heavy lifting is already in the model's batched forward + * (it continues from the current state at an arbitrary pos0); this + * module is the session bookkeeping: which pn_state belongs to which + * session, the longest-common-prefix delta, LRU eviction, and — the one + * subtlety — preserving a session's small SSM state off the shared GPU + * buffers before another session reuses them (the KV cache already + * survives in each pn_state's host mirror). + * + * @status Increment 1: session-based; LRU eviction. SYS1: per-session + * system-prefix snapshot + restore (see prefix_cache.h). LCP-REWIND: + * plain-KV (llama-family) arches resume at the longest common prefix + * on history divergence (no snapshot needed — pure causal KV rows are + * position-invariant, so pos is simply truncated). MULTI-CONVO: one + * id holds an entry per conversation it carries; max_sessions still + * counts ids. Content dedup is follow-up. + * @issues None + * @todo Increment 2: content-addressed cross-session prefix dedup. + */ + +#include "prefix_cache.h" +#include "session_snapshot.h" +#include "../models/qwen35moe.h" + +#include +#include +#include + +#define PN_PREFIX_SID_MAX 64 + +/* One cached conversation of a session (an id may hold several, each its own + * entry). state == NULL marks a free slot. */ +typedef struct { + char sid[PN_PREFIX_SID_MAX]; + pn_state *state; /* owns the session's KV + SSM + pos */ + uint32_t *tokens; /* committed token history (prompt + generated) */ + int n_tok; + int cap_tok; + uint64_t stamp; /* LRU: last acquire/commit tick (monotonic counter) */ + uint64_t last_active_ms; /* wall-clock ms of last acquire/commit; 0 = never. + * Drives the Phase-5 idle-TTL reaper (the `stamp` + * counter is only an ordering, not a duration). */ + /* SYS1 — system-prefix boundary state. sys_snap is the dual-state image at + * pos == sys_len (pn_qwen35moe_state_snapshot); sys_tokens[0..sys_len) are + * the exact prefix tokens it covers (the reuse guard: a changed system + * prompt never matches). Survives history divergence resets; replaced when + * the system prompt changes; freed with the entry. */ + void *sys_snap; + uint32_t *sys_tokens; + int sys_len; +} pn_prefix_entry; + +struct pn_prefix_cache { + pn_model *model; /* not owned */ + pn_prefix_entry *ents; + int cap; /* == max_sessions */ + uint64_t clock; /* monotonic LRU tick */ + /* The session whose image currently occupies the shared GPU state + * buffers (or NULL). Snapshotted back to its host before another session + * overwrites those buffers. */ + pn_state *live; + /* Watch-liveness predicate (borrowed): the backstop reaper skips an idle slot + * whose base session is still watched. NULL = no watch protection. */ + pn_session_watched_fn watch_fn; + void *watch_ctx; +}; + +/* Length of the longest common prefix of two token runs. */ +static int lcp_len(const uint32_t *a, int na, const uint32_t *b, int nb) +{ + int n = na < nb ? na : nb; + int i = 0; + while (i < n && a[i] == b[i]) + i++; + return i; +} + +/* Snapshot the live session's SSM state off the shared GPU buffers into its + * host pn_state, so it survives another session reusing those buffers. */ +static void preserve_live(pn_prefix_cache *c) +{ + if (!c->live) + return; +#ifdef USE_CUDA + /* Bonsai shares the qwen35moe SSM/GPU state machinery, so it snapshots the + * same way. */ + if (c->model->config.arch == PN_ARCH_QWEN35MOE || + c->model->config.arch == PN_ARCH_BONSAI) + pn_qwen35moe_gpu_state_to_host(c->model, c->live); +#endif +} + +pn_prefix_cache *pn_prefix_cache_new(pn_model *model, int max_sessions) +{ + if (!model || max_sessions < 1) + return NULL; + pn_prefix_cache *c = calloc(1, sizeof(*c)); + if (!c) + return NULL; + c->ents = calloc((size_t)max_sessions, sizeof(*c->ents)); + if (!c->ents) { + free(c); + return NULL; + } + c->model = model; + c->cap = max_sessions; + return c; +} + +void pn_prefix_cache_set_watch_hook(pn_prefix_cache *c, pn_session_watched_fn fn, + void *ctx) +{ + if (!c) + return; + c->watch_fn = fn; + c->watch_ctx = ctx; +} + +/* Does this model's attention K/V live on an ACCELERATOR rather than in the host state? + * + * Every snapshot in this file serialises pn_state's host K/V. On the Tenstorrent on-card + * attention arm that mirror is deliberately never written — 32,768 B a position stops + * crossing the link, which is the entire point of the arm — so a capture there records ZEROS + * and a restore hands the forward an empty cache. The output stays fluent and is wrong, with + * nothing refusing anywhere. So ask before capturing, and decline the whole mechanism rather + * than capture something that is not the session. + * + * Declining costs a system-prefix re-prefill on a divergent history. Capturing the wrong + * bytes costs a wrong answer nobody can attribute, and the second is not a trade. + * + * A build with no accelerator, and an accelerator arm that left attention on the host, both + * answer 0 and keep the existing behaviour exactly. */ +static int kv_is_off_host(const pn_prefix_cache *c) +{ +#ifdef USE_TT + return pn_qwen35moe_model_tt_kv_on_card(c->model); +#else + (void)c; + return 0; +#endif +} + +/* Is this cache's model one whose dual state supports snapshot/restore? + * (Bonsai shares the qwen35moe dual-state machinery.) */ +static int sys_snapshot_supported(const pn_prefix_cache *c) +{ + if (kv_is_off_host(c)) + return 0; + return c->model->config.arch == PN_ARCH_QWEN35MOE || + c->model->config.arch == PN_ARCH_BONSAI; +} + +/* SESSION EXPORT/RESTORE — can this model's session be captured into a blob a + * client can hold and hand back? A strict SUPERSET of sys_snapshot_supported(), + * and deliberately a SEPARATE predicate. + * + * The SYS1 machinery above decides WHEN the serve snapshots on its own account + * (at the system-prompt boundary, to survive a divergent history) and is + * qwen35-only by design — the llama family rewinds instead, see + * lcp_rewind_supported() below. Export is a different question: any arch whose + * whole per-session state can be serialized can be exported, and every one of + * those two properties is owned by session_snapshot.h. Merging the predicates + * would have turned SYS1 on for the llama family as a side effect of making + * export work, i.e. changed the resume ladder on the hot path to fix an admin + * endpoint. */ +static int export_supported(const pn_prefix_cache *c) +{ + /* Same reason as above, and it is the LOUDER failure of the two: an exported blob leaves + * the box, and a client that hands back a session of zeroed K/V gets a coherent answer to + * a conversation the model cannot see. */ + if (kv_is_off_host(c)) + return 0; + return pn_session_snapshot_supported(c->model->config.arch); +} + +/* LCP-REWIND — is this model's per-session state a plain llama-family KV cache + * (pure causal attention, host-resident K/V rows, no recurrent component)? + * For those, row [i] of the KV cache depends only on tokens [0..i], so the + * state can be rewound to ANY l <= pos by truncating pos — the rows [0,l) + * stay exact and the next prefill simply overwrites [l..). This gives a + * divergent-history resume WITHOUT a snapshot: resume at the longest common + * prefix. Excluded: the recurrent (RWKVX) and hybrid-SSM (QWEN35MOE/BONSAI) + * arches, whose per-position state cannot be rewound — that is exactly what + * the SYS1 boundary snapshots above exist for. */ +static int lcp_rewind_supported(const pn_prefix_cache *c) +{ + switch (c->model->config.arch) { + case PN_ARCH_LLAMA: + case PN_ARCH_QWEN2: + case PN_ARCH_QWEN3: + case PN_ARCH_PHI: + case PN_ARCH_MISTRAL: + case PN_ARCH_MIXTRAL: + return 1; + default: + return 0; + } +} + +/* Replace an entry's committed history with tokens[0, n). 0, or -1 on OOM. */ +static int entry_set_tokens(pn_prefix_entry *e, const uint32_t *tokens, int n) +{ + if (e->cap_tok < n) { + uint32_t *t = realloc(e->tokens, (size_t)n * sizeof(uint32_t)); + if (!t) + return -1; + e->tokens = t; + e->cap_tok = n; + } + memcpy(e->tokens, tokens, (size_t)n * sizeof(uint32_t)); + e->n_tok = n; + return 0; +} + +#ifdef USE_TT +/* THE CARD TIER (Tenstorrent: K/V and the GDN state live on the card, one sequence at a time). + * First choice: the batched prefill's checkpoint (qwen35moe_prefill_checkpoint_tt.c) — the + * output of a resumed turn is the output of a cold prefill of the same prompt, and it covers the + * re-rendered history (lcp < n_tok) the append tier cannot. Then the append tier, but only when + * the card still holds this session's sequence: another session's forwards since then left its + * own K/V and state there. Returns 1 (resumed, *delta set), 0 (fall through to the append + * tier), -1 (full reset). */ +static int tt_card_tier(pn_prefix_cache *c, pn_prefix_entry *e, const uint32_t *tokens, + int n_ctx, int lcp, int *delta) +{ + int p; + + if (!kv_is_off_host(c)) + return 0; + p = pn_qwen35moe_tt_checkpoint_resume(c->model, e->state, tokens, n_ctx); + if (p > 0) { + fprintf(stderr, "[prefix_cache] card checkpoint sid=%s: resume at %d, prefill %d of %d " + "tokens [lcp=%d n_tok=%d]\n", e->sid, p, n_ctx - p, n_ctx, lcp, e->n_tok); + if (entry_set_tokens(e, tokens, p) != 0) + return -1; + *delta = p; + return 1; + } + if (p != 0) + return -1; + return pn_qwen35moe_tt_card_holds(c->model, e->state) ? 0 : -1; +} +#endif + +/* Drop an entry's system-prefix snapshot (keeps the tokens buffer for reuse). + * `sys_snap` is produced ONLY by pn_prefix_cache_capture_sys, which is gated on + * sys_snapshot_supported(), so it is always a qwen35-family snapshot and the + * typed free below is correct. If SYS1 is ever opened to another family this + * must go through session_snapshot.h like the export path does. */ +static void sys_snap_drop(pn_prefix_entry *e) +{ + pn_qwen35moe_state_snapshot_free(e->sys_snap); + e->sys_snap = NULL; + e->sys_len = 0; +} + +void pn_prefix_cache_free(pn_prefix_cache *c) +{ + if (!c) + return; + for (int i = 0; i < c->cap; i++) { + if (c->ents[i].state) + pn_state_free(c->ents[i].state); + free(c->ents[i].tokens); + pn_qwen35moe_state_snapshot_free(c->ents[i].sys_snap); + free(c->ents[i].sys_tokens); + } + free(c->ents); + free(c); +} + +static int entry_has_sid(const pn_prefix_entry *e, const char *sid) +{ + return e->state && strncmp(e->sid, sid, PN_PREFIX_SID_MAX) == 0; +} + +/* The entry `sid`'s last turn ran on: its most recently acquired, committed or + * imported conversation. Generation is serialized, so between an acquire and + * its commit the acquired entry is this one; capture, commit, reset, touch and + * export all address it. */ +static pn_prefix_entry *find_entry(pn_prefix_cache *c, const char *sid) +{ + pn_prefix_entry *best = NULL; + for (int i = 0; i < c->cap; i++) + if (entry_has_sid(&c->ents[i], sid) && + (!best || c->ents[i].stamp > best->stamp)) + best = &c->ents[i]; + return best; +} + +static int sid_entry_count(const pn_prefix_cache *c, const char *sid) +{ + int n = 0; + for (int i = 0; i < c->cap; i++) + n += entry_has_sid(&c->ents[i], sid); + return n; +} + +/* Distinct session ids holding at least one entry (what max_sessions counts). */ +static int live_sid_count(const pn_prefix_cache *c) +{ + int n = 0; + for (int i = 0; i < c->cap; i++) { + const pn_prefix_entry *e = &c->ents[i]; + int first = e->state != NULL; + for (int j = 0; j < i && first; j++) + if (entry_has_sid(&c->ents[j], e->sid)) + first = 0; + n += first; + } + return n; +} + +/* Free a single live slot in place (frees its pn_state + snapshot, keeps the + * tokens buffer for reuse), so a waiting session can take it. */ +static void free_slot(pn_prefix_cache *c, pn_prefix_entry *e) +{ + if (!e->state) + return; + if (c->live == e->state) + c->live = NULL; + pn_state_free(e->state); + e->state = NULL; + e->n_tok = 0; + e->sid[0] = '\0'; + e->last_active_ms = 0; + sys_snap_drop(e); +} + +pn_session_admit pn_prefix_cache_admit(pn_prefix_cache *c, const char *sid, + uint64_t now_ms, uint64_t idle_ttl_ms, + int *restart_tokens_out) +{ + if (restart_tokens_out) + *restart_tokens_out = 0; + /* No cache or no session id → uncached path: nothing to admit against. */ + if (!c || !sid || !sid[0]) + return PN_SESSION_ADMIT_OK; + + /* Step 1: reap idle slots (last activity older than the TTL). Skip the slot + * that currently owns the shared GPU buffers only if it is `sid`'s own — + * an idle OTHER session is fine to reap even if live-buffer-resident, since + * free_slot drops c->live for it. A slot whose base session still holds a + * liveness watch (this idle TTL is the 12 h backstop, which only touches + * UNwatched slots) is skipped — the watch, not the backstop, owns its + * release: the client watches the BASE id while the live slot is the mode-suffixed + * sid, so the watch protects the whole `-*` prefix. */ + if (idle_ttl_ms > 0) { + for (int i = 0; i < c->cap; i++) { + pn_prefix_entry *e = &c->ents[i]; + if (!e->state || strncmp(e->sid, sid, PN_PREFIX_SID_MAX) == 0) + continue; + if (c->watch_fn && c->watch_fn(e->sid, c->watch_ctx)) + continue; /* base is watched → the watch manager owns release */ + if (e->last_active_ms != 0 && + now_ms >= e->last_active_ms && + now_ms - e->last_active_ms >= idle_ttl_ms) { + fprintf(stderr, "[prefix_cache] reaped idle session sid=%s " + "(idle %llums >= %llums)\n", e->sid, + (unsigned long long)(now_ms - e->last_active_ms), + (unsigned long long)idle_ttl_ms); + free_slot(c, e); + } + } + } + + /* Step 2: an existing session always resumes. */ + pn_prefix_entry *mine = find_entry(c, sid); + if (mine) { + if (restart_tokens_out) + *restart_tokens_out = mine->n_tok; + return PN_SESSION_ADMIT_OK; + } + + /* Step 3: a free slot (fresh or just-reaped) admits the new session, and + * so does a full pool holding fewer than max_sessions ids: some id then + * holds a second conversation, which alloc_entry gives up for this one. */ + for (int i = 0; i < c->cap; i++) + if (!c->ents[i].state) + return PN_SESSION_ADMIT_OK; + if (live_sid_count(c) < c->cap) + return PN_SESSION_ADMIT_OK; + + /* Step 4: max_sessions live, non-idle ids → refuse the new one. */ + return PN_SESSION_ADMIT_AT_CAPACITY; +} + +void pn_prefix_cache_touch(pn_prefix_cache *c, const char *sid, uint64_t now_ms) +{ + if (!c || !sid || !sid[0]) + return; + pn_prefix_entry *e = find_entry(c, sid); + if (e) + e->last_active_ms = now_ms; +} + +/* The entry a full pool gives up for a new conversation of `sid`: the least + * recently used among `sid`'s own entries and every id's second-and-later + * conversations, so one id's extra conversation never costs another id its + * only one. Only when every entry is some other id's only conversation (a new + * id at max_sessions, which admission refuses before this) does it fall back to + * the plain LRU. */ +static pn_prefix_entry *eviction_victim(pn_prefix_cache *c, const char *sid) +{ + pn_prefix_entry *lru = NULL, *any = NULL; + for (int i = 0; i < c->cap; i++) { + pn_prefix_entry *e = &c->ents[i]; + if (!any || e->stamp < any->stamp) + any = e; + if ((entry_has_sid(e, sid) || sid_entry_count(c, e->sid) > 1) && + (!lru || e->stamp < lru->stamp)) + lru = e; + } + return lru ? lru : any; +} + +/* Return a free slot for a new conversation of `sid`, evicting per + * eviction_victim when the pool is full. NULL only on a pn_state_new failure. */ +static pn_prefix_entry *alloc_entry(pn_prefix_cache *c, const char *sid) +{ + pn_prefix_entry *slot = NULL; + for (int i = 0; i < c->cap; i++) + if (!c->ents[i].state) { slot = &c->ents[i]; break; } + + if (!slot) { + pn_prefix_entry *lru = eviction_victim(c, sid); + fprintf(stderr, "[prefix_cache] evict sid=%s (%d tokens) for a new " + "conversation of sid=%s\n", lru->sid, lru->n_tok, sid); + if (c->live == lru->state) + c->live = NULL; + pn_state_free(lru->state); + lru->state = NULL; + lru->n_tok = 0; /* keep tokens buffer for reuse */ + sys_snap_drop(lru); + slot = lru; + } + + slot->state = pn_state_new(c->model); + if (!slot->state) + return NULL; + pn_state_reset(slot->state); + snprintf(slot->sid, PN_PREFIX_SID_MAX, "%s", sid); + slot->n_tok = 0; + return slot; +} + +/* SYS1 — does the entry hold a snapshot covering EXACTLY this system prefix? */ +static int sys_snap_matches(const pn_prefix_entry *e, const uint32_t *tokens, + int sys_len) +{ + return e->sys_snap && e->sys_len == sys_len && + memcmp(e->sys_tokens, tokens, + (size_t)sys_len * sizeof(uint32_t)) == 0; +} + +/* What an entry's cached state is worth to a prompt (higher wins). */ +enum { PC_FIT_NONE, PC_FIT_SHARED, PC_FIT_SYS, PC_FIT_WARM }; + +/* Mirrors the acquire ladder's token checks, as bs_slot_fit does for the + * batched slots: the committed history is a prefix of the prompt (append), or + * the boundary snapshot covers its system prefix, or — only where the ladder + * can resume mid-history (the llama-family LCP rewind, the Tenstorrent card + * checkpoint) — the prompt shares at least its whole system prefix. */ +static int entry_fit(const pn_prefix_cache *c, const pn_prefix_entry *e, + const uint32_t *tokens, int n_ctx, int sys_len, int *lcp) +{ + *lcp = lcp_len(e->tokens, e->n_tok, tokens, n_ctx); + if (e->n_tok > 0 && *lcp == e->n_tok) + return PC_FIT_WARM; + if (sys_len > 0 && sys_len < n_ctx && sys_snap_matches(e, tokens, sys_len)) + return PC_FIT_SYS; + if (*lcp > 0 && *lcp >= sys_len && + (lcp_rewind_supported(c) || kv_is_off_host(c))) + return PC_FIT_SHARED; + return PC_FIT_NONE; +} + +/* Which of `sid`'s conversations this prompt continues (one session id can + * carry several: opencode sends a title request and the real turns under one + * id, each with its own system prompt). Best fit, then longest shared prefix, + * then most recent. NULL when none fits: the prompt is a new conversation and + * gets its own entry instead of resetting another's. */ +static pn_prefix_entry *find_conversation(pn_prefix_cache *c, const char *sid, + const uint32_t *tokens, int n_ctx, + int sys_len) +{ + pn_prefix_entry *best = NULL; + int best_fit = PC_FIT_NONE, best_lcp = 0; + for (int i = 0; i < c->cap; i++) { + pn_prefix_entry *e = &c->ents[i]; + int lcp = 0; + if (!entry_has_sid(e, sid)) + continue; + int fit = entry_fit(c, e, tokens, n_ctx, sys_len, &lcp); + if (fit == PC_FIT_NONE) + continue; + if (!best || fit > best_fit || (fit == best_fit && lcp > best_lcp) || + (fit == best_fit && lcp == best_lcp && e->stamp > best->stamp)) { + best = e; + best_fit = fit; + best_lcp = lcp; + } + } + return best; +} + +/* SYS1 — restore the session's state to its system-prefix boundary. On the GPU + * path the state's GPU shadows (SSM image, KV sync watermark, fp16 window + * ring) still hold the END of the previous turn; a restore to a non-zero pos + * never trips the pos==0 restart re-sync, so the bindings must be forgotten + * explicitly — the next forward then re-seeds everything from the restored + * host mirror. Returns 0 on success (state sits at pos == sys_len). */ +static int sys_snap_restore(pn_prefix_cache *c, pn_prefix_entry *e, int sys_len) +{ + if (!sys_snapshot_supported(c)) + return -1; + if (pn_qwen35moe_state_restore(e->state->kv_cache, e->sys_snap) != 0) + return -1; + e->state->pos = sys_len; +#ifdef USE_CUDA + pn_qwen35moe_gpu_forget_state(c->model, e->state); +#endif + return 0; +} + +pn_state *pn_prefix_cache_acquire_sys(pn_prefix_cache *c, const char *sid, + const uint32_t *tokens, int n_ctx, + int sys_len, int *delta_start, + int *capture_sys) +{ + if (!c || !sid || !tokens || n_ctx <= 0 || !delta_start || !capture_sys) + return NULL; + /* A system prefix is only usable when a conversation follows it. */ + int sys_ok = sys_len > 0 && sys_len < n_ctx; + *capture_sys = 0; + + pn_prefix_entry *e = find_conversation(c, sid, tokens, n_ctx, sys_len); + int delta = 0; + + if (e) { + int l = lcp_len(e->tokens, e->n_tok, tokens, n_ctx); + int card = 0; +#ifdef USE_TT + card = tt_card_tier(c, e, tokens, n_ctx, l, &delta); +#endif + if (card == 1) { + /* resumed from the card's checkpoint (delta set) */ + } else if (card == 0 && l == e->n_tok && e->state->pos == e->n_tok) { + /* Append case (the agentic turn): the whole cached history is a + * prefix of the new context and the state sits exactly at its + * end. Resume from there. */ + delta = l; + } else if (sys_ok && sys_snap_matches(e, tokens, sys_len) && + sys_snap_restore(c, e, sys_len) == 0) { + /* Divergent history but identical system prefix (the reasoning- + * model turn): resume at the system boundary, prefill only the + * conversation. The stale committed history is dropped; commit + * rebuilds it at end of turn. */ + delta = sys_len; + /* DIAG — why tier 1 (append) was missed, and what the collapse to + * the system boundary costs. `lcp` is how far the new context + * actually agrees with the committed history: if lcp == n_tok the + * histories match and it was `pos` that disqualified the append + * (state left mid-turn); if lcp < n_tok the client re-rendered the + * conversation and `lcp - sys_len` is the prefix we are re-prefilling + * for no reason, because this arch has no lcp-rewind to resume from. + * Read the FIRST of these after a fresh turn — later ones follow the + * n_tok = 0 below and only report the reset. */ + fprintf(stderr, "[prefix_cache] sys-prefix reuse sid=%s: resume at " + "%d, prefill %d of %d tokens " + "[diag lcp=%d n_tok=%d pos=%d wasted=%d]\n", + sid, sys_len, n_ctx - sys_len, n_ctx, + l, e->n_tok, e->state->pos, + l > sys_len ? l - sys_len : 0); + /* DIAG — the tokens straddling the divergence. `committed` is what + * we generated and cached; `incoming` is what the client sent back + * for the same span. Their first difference is at index `l`, so + * printing from there shows the re-render mismatch directly instead + * of inferring it from lengths. */ + if (l < e->n_tok) { + fprintf(stderr, "[prefix_cache] committed[%d..%d]:", l, e->n_tok - 1); + for (int i = l; i < e->n_tok && i < l + 32; i++) + fprintf(stderr, " %u", e->tokens[i]); + fprintf(stderr, "\n[prefix_cache] incoming [%d..%d]:", l, n_ctx - 1); + for (int i = l; i < n_ctx && i < l + 32; i++) + fprintf(stderr, " %u", tokens[i]); + fprintf(stderr, "\n"); + } + e->n_tok = 0; + } else if (lcp_rewind_supported(c)) { + /* Divergent history on a plain-KV arch: rewind to the longest + * common prefix and prefill only from there. This is what makes + * a warmed prefix pay off on these arches — the next turn's + * prompt shares the (large) system prefix but diverges right + * after it (assistant opener / No-Think think block / a fresh + * conversation), which the exact-prefix append check above can + * never hit. Clamps: never past the state's own position (rows + * beyond pos were never filled), and leave at least one prompt + * token to prefill so the next-token logits are recovered. */ + int l2 = l; + if (l2 > e->state->pos) + l2 = e->state->pos; + if (l2 >= n_ctx) + l2 = n_ctx - 1; + if (l2 > 0) { + e->state->pos = l2; + e->n_tok = l2; /* committed history truncated to the LCP */ + delta = l2; + fprintf(stderr, "[prefix_cache] lcp rewind sid=%s: resume at " + "%d, prefill %d of %d tokens\n", + sid, l2, n_ctx - l2, n_ctx); + } else { + pn_state_reset(e->state); + e->n_tok = 0; + delta = 0; + } + } else { + /* DIAG — a session that MISSED every tier re-prefills in full; say why (the + * same straddle print as the system-prefix tier), or a client re-rendering its + * history differently is invisible. */ + fprintf(stderr, "[prefix_cache] full reset sid=%s: lcp=%d n_tok=%d pos=%d " + "(no tier: append needs lcp == n_tok == pos)\n", + sid, l, e->n_tok, e->state->pos); + if (l < e->n_tok) { + fprintf(stderr, "[prefix_cache] committed[%d..]:", l); + for (int i = l; i < e->n_tok && i < l + 16; i++) + fprintf(stderr, " %u", e->tokens[i]); + fprintf(stderr, "\n[prefix_cache] incoming [%d..]:", l); + for (int i = l; i < n_ctx && i < l + 16; i++) + fprintf(stderr, " %u", tokens[i]); + fprintf(stderr, "\n"); + } + pn_state_reset(e->state); + e->n_tok = 0; + delta = 0; + } + } else { + /* A new session, or a new conversation of this one: its own entry, so + * the conversations it already holds keep their caches. */ + if (find_entry(c, sid)) + fprintf(stderr, "[prefix_cache] new conversation sid=%s (%d held)\n", + sid, sid_entry_count(c, sid)); + e = alloc_entry(c, sid); + if (!e) + return NULL; + delta = 0; + } + + /* Full prefill ahead + no snapshot covering these exact system tokens → + * ask the caller to capture the boundary state (replacing a stale snapshot + * from a changed system prompt). */ + if (delta == 0 && sys_ok && sys_snapshot_supported(c) && + !sys_snap_matches(e, tokens, sys_len)) + *capture_sys = 1; + + /* Any forward the caller now runs re-seeds/re-syncs this session's GPU + * state (host_state / KV host-id mismatch), overwriting whatever session + * last occupied the shared buffers — so preserve that one first. */ + if (c->live && c->live != e->state) + preserve_live(c); + c->live = e->state; + + e->stamp = ++c->clock; + *delta_start = delta; + return e->state; +} + +pn_state *pn_prefix_cache_acquire(pn_prefix_cache *c, const char *sid, + const uint32_t *tokens, int n_ctx, + int *delta_start) +{ + int capture_unused = 0; + return pn_prefix_cache_acquire_sys(c, sid, tokens, n_ctx, 0, delta_start, + &capture_unused); +} + +int pn_prefix_cache_capture_sys(pn_prefix_cache *c, const char *sid, + const uint32_t *tokens, int sys_len) +{ + if (!c || !sid || !tokens || sys_len <= 0) + return -1; + if (!sys_snapshot_supported(c)) + return -1; + pn_prefix_entry *e = find_entry(c, sid); + /* The capture protocol: acquire_sys set capture_sys, the caller prefilled + * exactly the system prefix — the state must sit at that boundary. */ + if (!e || e->state->pos != sys_len) + return -1; + +#ifdef USE_CUDA + /* Make the host mirror self-contained (GPU SSM image D2H + stale KV + * mirror refresh) before copying it. Continuing the prefill afterwards is + * unaffected: the D2H reads the GPU state without invalidating it. */ + if (pn_qwen35moe_gpu_state_to_host(c->model, e->state) != 0) + return -1; +#endif + + void *snap = pn_qwen35moe_state_snapshot(e->state->kv_cache, sys_len); + if (!snap) + return -1; + uint32_t *st = realloc(e->sys_tokens, (size_t)sys_len * sizeof(uint32_t)); + if (!st) { + pn_qwen35moe_state_snapshot_free(snap); + return -1; + } + memcpy(st, tokens, (size_t)sys_len * sizeof(uint32_t)); + pn_qwen35moe_state_snapshot_free(e->sys_snap); + e->sys_snap = snap; + e->sys_tokens = st; + e->sys_len = sys_len; + fprintf(stderr, "[prefix_cache] sys-prefix captured sid=%s: %d tokens\n", + sid, sys_len); + return 0; +} + +int pn_prefix_cache_commit(pn_prefix_cache *c, const char *sid, + const uint32_t *tokens, int n_ctx) +{ + if (!c || !sid || !tokens || n_ctx < 0) + return -1; + pn_prefix_entry *e = find_entry(c, sid); + if (!e) + return -1; + + if (entry_set_tokens(e, tokens, n_ctx) != 0) + return -1; + e->stamp = ++c->clock; + return 0; +} + +void *pn_prefix_cache_export_snapshot(pn_prefix_cache *c, const char *sid, + int *n_tok_out, const uint32_t **tokens_out) +{ + if (n_tok_out) *n_tok_out = 0; + if (tokens_out) *tokens_out = NULL; + if (!c || !sid || !export_supported(c)) + return NULL; + pn_prefix_entry *e = find_entry(c, sid); + /* Export the FULL committed history. The append path leaves the state at + * pos == n_tok; a mid-turn/diverged state (pos != n_tok) is not a clean + * export point — refuse rather than snapshot an inconsistent position. */ + if (!e || e->n_tok <= 0 || e->state->pos != e->n_tok) + return NULL; + +#ifdef USE_CUDA + /* Make the host mirror self-contained before copying (GPU SSM image D2H + + * stale-KV refresh). This session must own the shared GPU buffers for the + * D2H to read its live state — preserve any OTHER live session first, then + * claim `live` for this one (a read-only D2H does not disturb it). */ + if (c->live && c->live != e->state) + preserve_live(c); + c->live = e->state; + if (pn_qwen35moe_gpu_state_to_host(c->model, e->state) != 0) + return NULL; +#endif + + void *snap = pn_session_snapshot_capture(c->model->config.arch, + e->state->kv_cache, e->n_tok); + if (!snap) + return NULL; + if (n_tok_out) *n_tok_out = e->n_tok; + if (tokens_out) *tokens_out = e->tokens; + return snap; +} + +/* The conversation of `sid` an imported history replaces: one whose committed + * history it continues, or which continues it, or an empty one (most recent + * first). NULL when the history belongs to none of them. */ +static pn_prefix_entry *import_target(pn_prefix_cache *c, const char *sid, + const uint32_t *tokens, int n_tok) +{ + pn_prefix_entry *best = NULL; + for (int i = 0; i < c->cap; i++) { + pn_prefix_entry *e = &c->ents[i]; + if (!entry_has_sid(e, sid)) + continue; + int shorter = e->n_tok < n_tok ? e->n_tok : n_tok; + if (lcp_len(e->tokens, e->n_tok, tokens, n_tok) == shorter && + (!best || e->stamp > best->stamp)) + best = e; + } + return best; +} + +int pn_prefix_cache_import_snapshot(pn_prefix_cache *c, const char *sid, + const void *snap, const uint32_t *tokens, + int n_tok) +{ + if (!c || !sid || !snap || !tokens || n_tok <= 0) + return -1; + if (!export_supported(c)) + return -1; + if (pn_session_snapshot_pos(c->model->config.arch, snap) != n_tok) + return -1; + + /* Take (or reuse) a slot. The caller already ran admission, so a + * genuinely-new sid at capacity was refused upstream. A returning sid + * reuses the conversation this history continues (or is a prefix of); + * anything else is another conversation of that sid and gets its own entry + * (alloc_entry: a free slot, else per eviction_victim). */ + pn_prefix_entry *e = import_target(c, sid, tokens, n_tok); + int is_new = (e == NULL); + if (!e) { + e = alloc_entry(c, sid); + if (!e) + return -1; + } + + /* Restore the snapshot into the slot's live state. Validate BEFORE mutating + * pos/history so a rejected restore (dim mismatch — only reachable if the + * blob is from a different arch, which weights_version already guards) never + * corrupts the slot. A brand-new entry is freed on failure so it does not + * linger as an empty live conversation; a reused one keeps its prior state + * intact (we have not touched it yet). */ + if (pn_session_snapshot_restore(c->model->config.arch, e->state->kv_cache, + snap) != 0) { + if (is_new) + free_slot(c, e); + return -1; + } + e->state->pos = n_tok; +#ifdef USE_CUDA + /* A restore to a non-zero pos never trips the pos==0 restart re-sync, so + * forget the GPU bindings: the next forward re-seeds from the restored host + * mirror. This session now owns the shared GPU buffers. */ + pn_qwen35moe_gpu_forget_state(c->model, e->state); + if (c->live && c->live != e->state) + preserve_live(c); + c->live = e->state; +#endif + + /* Install the committed history (drives next turn's delta computation). On a + * token-buffer OOM here the state is already restored; free a brand-new slot + * (same contract as the restore-reject above) so it does not linger as an + * empty live session — a returning sid keeps its slot (it self-heals to a + * full prefill next turn, its old history being harmless). */ + if (e->cap_tok < n_tok) { + uint32_t *t = realloc(e->tokens, (size_t)n_tok * sizeof(uint32_t)); + if (!t) { + if (is_new) + free_slot(c, e); + return -1; + } + e->tokens = t; + e->cap_tok = n_tok; + } + memcpy(e->tokens, tokens, (size_t)n_tok * sizeof(uint32_t)); + e->n_tok = n_tok; + sys_snap_drop(e); /* the restored state is the new truth */ + e->stamp = ++c->clock; + return 0; +} + +void pn_prefix_cache_reset_session(pn_prefix_cache *c, const char *sid) +{ + if (!c || !sid) + return; + pn_prefix_entry *e = find_entry(c, sid); + if (!e) + return; + pn_state_reset(e->state); /* drop the (partial) KV/SSM state */ + e->n_tok = 0; /* no committed history → next acquire full-prefills */ + sys_snap_drop(e); /* invalidate any system-prefix snapshot */ +} + +void pn_prefix_cache_release_session(pn_prefix_cache *c, const char *sid) +{ + if (!c || !sid) + return; + /* Free the whole session (unlike reset_session, which keeps it occupied): + * every conversation's pn_state + snapshot, returning capacity immediately + * so a waiting session can take it. No-op for an unknown session. */ + for (int i = 0; i < c->cap; i++) + if (entry_has_sid(&c->ents[i], sid)) + free_slot(c, &c->ents[i]); +} + +void pn_prefix_cache_release_by_prefix(pn_prefix_cache *c, + const char *base_prefix) +{ + if (!c || !base_prefix || !base_prefix[0]) + return; + /* Free every live slot whose sid begins with base_prefix (the mode-suffixed + * `-orch`/`-act`/`-chat` slots a single base watch covers). A dead + * base's watch releases all of them at once. */ + size_t plen = strlen(base_prefix); + for (int i = 0; i < c->cap; i++) { + pn_prefix_entry *e = &c->ents[i]; + if (e->state && strncmp(e->sid, base_prefix, plen) == 0) + free_slot(c, e); + } +} diff --git a/src/core/prefix_cache.h b/src/core/prefix_cache.h new file mode 100644 index 0000000..7e19bc6 --- /dev/null +++ b/src/core/prefix_cache.h @@ -0,0 +1,390 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file prefix_cache.h + * @brief Session-based prefix cache — the agentic-turn latency win. Across + * agent turns the context is mostly unchanged (a tool result / message + * appended to a fixed prefix); instead of re-prefilling the WHOLE + * context every turn, a session's model state (attention KV + the small + * fixed SSM recurrent state + position) is kept keyed by a session id, + * so a follow-up prefills ONLY the new (delta) tokens. + * + * @status Increment 1: session-based caching (one cached pn_state per session + * id, LRU eviction). SYS1: system-prefix caching — the state at the + * system-prompt token boundary is snapshotted per session, so a turn + * whose history DIVERGES (the reasoning-model case: generated + * text is committed but never resent by the client) still resumes at + * the system boundary and re-prefills only the conversation. + * MULTI-CONVO: an id carrying several conversations (opencode's title + * request + real turns) keeps an entry per conversation; the acquire + * picks the one the prompt continues (single-user bs_slot_reroute). + * @issues Measured on a K80 host (2026-07-18, K80, Qwen3.6-35B Q3_K): a sys-prefix + * RESTORED turn is answer-correct but NOT bit-identical to a cold + * full prefill of the same context at temp 0 — greedy output matches + * for ~50 tokens then flips (reasoning cp ~180 chars), while two cold + * full prefills ARE byte-identical to each other. So the capture-time + * host mirror (pn_qwen35moe_gpu_state_to_host) is not byte-faithful + * to the live GPU state it snapshots (host-side snapshot/restore + * round-trip IS byte-exact — unit-proven). Applies equally to the + * single-user and batched paths (same primitives). Follow-up: audit + * the D2H mirror fidelity (fp16/q4 shadow round-trip, SSM precision). + * @todo + * - [ ] Increment 2: content-addressed prefix dedup (match arbitrary token + * prefixes across sessions; would let ONE system snapshot serve many + * sessions). Batched-path SYS1 is done — it lives per-slot in + * batch_sched.[ch] (bs_prefill_resume_sys / bs_prefill_capture_sys), + * not here: the scheduler's slots own their pn_states, so sharing this + * cache's per-session states would mix two ownership models. + * + * @warning On the BATCHED path (`mr_entry.sched` non-NULL) this cache holds + * ADMISSION BOOKKEEPING ONLY — nothing calls its acquire, so it has no + * entries and no session state at all; the sessions live in the + * scheduler's decode slots. Serve code must therefore never reach for + * `entry->prefix_cache` directly to find a session: go through the + * model_session.h facade, which has both arms. Session export, + * restore and release each shipped against this cache alone and each + * silently covered only the single-user path until that facade existed. + */ + +#ifndef PN_PREFIX_CACHE_H +#define PN_PREFIX_CACHE_H + +#include "engine.h" +#include + +/* Opaque session prefix cache. Concrete definition in prefix_cache.c. */ +typedef struct pn_prefix_cache pn_prefix_cache; + +/** + * Watch-liveness predicate: "is the base session that owns entry `sid` currently + * covered by a live (or within-grace) liveness watch?" The serve layer binds this + * to the session-watch manager (see pn_prefix_cache_set_watch_hook). It exists so + * the 12 h backstop reaper in pn_prefix_cache_admit does NOT reap a mode-suffixed + * slot (`-orch`/`-act`/`-chat`) while a watch on its base `` is alive: + * The client holds ONE watch per tab keyed on the tab's BASE id, but the live KV slots + * are the mode-suffixed sids, so the watch must protect the whole `-*` + * prefix. Returns nonzero when `sid`'s base is watched (skip the backstop). + * Called under the same lock as admit (a model lock); it may take the watch + * manager's own mutex — safe because the watch manager NEVER holds its mutex + * across a model lock (the reverse edge), so no lock cycle forms. + */ +typedef int (*pn_session_watched_fn)(const char *sid, void *ctx); + +/* Session-admission verdict (K80 serving-ops Phase 5). A "session" is a live + * prefix-cache slot. The cache holds up to max_sessions of them; a genuinely + * NEW session id over that cap is REFUSED (not queued, not LRU-evicting a live + * session) so "how many concurrent users this model holds" is a hard number. + * An idle slot (untouched past the TTL) is reaped first so a waiting user can + * take it. An EXISTING session id always resumes. */ +typedef enum { + PN_SESSION_ADMIT_OK = 0, /* existing sid resumes, or slot free */ + PN_SESSION_ADMIT_AT_CAPACITY = 1, /* new sid, all slots hold live sessions */ +} pn_session_admit; + +/** + * Create a session prefix cache over `model`, holding up to `max_sessions` + * live sessions (each owns a pn_state whose VRAM KV cache is the bulk of the + * cost). Least-recently-used sessions are evicted when the cache is full. + * + * Entries are per CONVERSATION: one id may hold several (one per distinct + * conversation it sends — see pn_prefix_cache_acquire_sys). The pool is still + * `max_sessions` entries in all, so memory stays at most `max_sessions` + * pn_states. A new conversation takes a free entry, else evicts the least + * recently used among its own id's entries and every id's second-and-later + * ones — never another id's only conversation (admission counts IDS, so a + * side conversation never makes a new id or the main one 429). + * + * @param model Loaded model (weights + forward). Not owned. + * @param max_sessions Max concurrent cached sessions (>= 1). + * @return Cache handle, or NULL on failure. + * + * Thread-safe: no + */ +pn_prefix_cache *pn_prefix_cache_new(pn_model *model, int max_sessions); + +/** + * Install the watch-liveness predicate the backstop reaper consults (see + * pn_session_watched_fn). `fn`/`ctx` are borrowed (must outlive the cache); pass + * `fn == NULL` to clear it (the default — no watch protection, backstop reaps any + * idle slot). Set once by the serve after the watch manager starts. Thread-safe: + * no (set before serving, then read-only under the cache's lock). + */ +void pn_prefix_cache_set_watch_hook(pn_prefix_cache *cache, + pn_session_watched_fn fn, void *ctx); + +/** Free the cache and every pn_state it still holds. */ +void pn_prefix_cache_free(pn_prefix_cache *cache); + +/** + * Session admission (K80 serving-ops Phase 5). Decide whether session `sid` may + * take (or already holds) one of the cache's live slots — WITHOUT allocating. + * Call this before pn_prefix_cache_acquire on the request path. + * + * Steps, in order: + * 1. Reap idle slots: any slot untouched for >= `idle_ttl_ms` (its last + * acquire/commit was that long ago) is freed here, so a returning-later + * user frees room for a waiting one. `idle_ttl_ms <= 0` disables reaping. + * 2. If `sid` already holds a slot → PN_ADMIT_OK (it always resumes). + * 3. Else if a slot is free (or one was just reaped), or fewer than + * max_sessions distinct ids hold the slots (one holds a spare + * conversation the acquire will evict) → PN_ADMIT_OK. + * 4. Else (max_sessions live, non-idle ids) → PN_ADMIT_AT_CAPACITY. + * Reaping is per conversation entry (each has its own last-activity time). + * + * On PN_SESSION_ADMIT_AT_CAPACITY the caller should REFUSE the new session + * (429/503) rather than let acquire LRU-evict a live one. `*restart_tokens_out` + * (may be NULL) receives the committed context length of the session that would + * need re-prefilling if this were an expired/returning session — 0 for a truly + * new sid — so the caller can quote a restart estimate. + * + * @param cache Cache handle (NULL → OK, no-op — caching off). + * @param sid Session id (NULL/empty → OK — uncached path). + * @param now_ms Caller's monotonic wall clock in ms (time-source + * agnostic: the serve layer passes its own now_ms()). + * @param idle_ttl_ms Idle-session TTL in ms (<= 0 disables the reaper). + * @param restart_tokens_out Out (nullable): committed tokens of `sid`'s slot. + * @return PN_SESSION_ADMIT_OK or PN_SESSION_ADMIT_AT_CAPACITY. + * + * Thread-safe: no (serialize with the same lock as acquire/commit). + */ +pn_session_admit pn_prefix_cache_admit(pn_prefix_cache *cache, const char *sid, + uint64_t now_ms, uint64_t idle_ttl_ms, + int *restart_tokens_out); + +/** + * Mark session `sid` as active at wall-clock `now_ms` (feeds the idle-TTL + * reaper in pn_prefix_cache_admit). Like capture, commit, reset and export it + * addresses the id's CURRENT conversation: the entry most recently acquired, + * committed or imported (generation is serialized, so between an acquire and + * its commit that is the acquired one). Call after a successful acquire/generation + * on the request path. No-op for an unknown session, a NULL cache, or an + * empty sid. Thread-safe: no. + */ +void pn_prefix_cache_touch(pn_prefix_cache *cache, const char *sid, + uint64_t now_ms); + +/** + * Acquire session `sid`'s cached model state for the full context + * `tokens[0..n_ctx)` and report where the caller must resume prefilling. + * + * Restores the session (first snapshotting any OTHER currently-live session's + * GPU state back to its host, so the shared GPU SSM buffers can be reused) and + * returns its pn_state positioned so that tokens[0..*delta_start) are already + * processed (state->pos == *delta_start). The caller then prefills + * tokens[*delta_start..n_ctx) through the model's batched forward (which + * advances state->pos) and generates, then calls pn_prefix_cache_commit. + * + * *delta_start == n_ctx means the whole context was already cached (nothing to + * prefill); *delta_start == 0 means a fresh or diverged session (full prefill). + * + * @param cache Cache handle. + * @param sid Session id (copied; NUL-terminated, up to 63 chars). + * @param tokens Full context token ids [0..n_ctx). + * @param n_ctx Context length (> 0). + * @param delta_start Out: first position the caller must prefill. + * @return The session's pn_state, or NULL on failure. + * + * Thread-safe: no + */ +pn_state *pn_prefix_cache_acquire(pn_prefix_cache *cache, const char *sid, + const uint32_t *tokens, int n_ctx, + int *delta_start); + +/** + * SYS1 — system-prefix-aware acquire. As pn_prefix_cache_acquire, but with the + * caller's system-prompt token boundary `sys_len` (tokens[0..sys_len) are the + * per-session-stable system prefix; 0 = no system prefix). Resume preference: + * + * 1. Full-history append resume (exact behavior of pn_prefix_cache_acquire): + * *delta_start = cached history length. + * 2. System-prefix resume: the session holds a state snapshot at its system + * boundary AND tokens[0..sys_len) match the snapshotted prefix exactly → + * the snapshot is restored and *delta_start = sys_len (only the + * conversation is prefilled). Fires on the divergent-history turns where + * (1) never can (reasoning models: text committed, never resent). + * 3. Fresh/diverged with no usable snapshot: clean reset, *delta_start = 0. + * + * Which of the id's conversations: the entry whose cached state the prompt + * continues — committed history a prefix of `tokens` (1), else a boundary + * snapshot over tokens[0..sys_len) (2), else, on arches that resume + * mid-history (llama LCP rewind, the Tenstorrent card checkpoint), the longest + * shared prefix covering the system prefix. None → a NEW entry for this + * conversation (case 3 on it), leaving the id's other conversations intact. + * + * *capture_sys is set to 1 when the caller should CAPTURE the system-prefix + * state this turn: a full prefill is about to run (case 3), a system prefix + * exists (0 < sys_len < n_ctx), and no valid snapshot covers these exact + * system tokens (none yet, or the system prompt changed — the stale snapshot + * is invalidated by the capture). The caller then prefills [0, sys_len), + * calls pn_prefix_cache_capture_sys, and prefills the rest. + * + * Correctness: any mismatch falls to case 3 (full reset + prefill) — never a + * partial/wrong reuse. Restore is exact: SSM state + KV rows + fp16 shadows + * are byte-restored to the boundary and GPU shadows are re-seeded from them. + * + * @param sys_len System-prefix token count (0 disables SYS1 for the call). + * @param capture_sys Out: 1 = capture the boundary state during this prefill. + * @return The session's pn_state, or NULL on failure. + * + * Thread-safe: no + */ +pn_state *pn_prefix_cache_acquire_sys(pn_prefix_cache *cache, const char *sid, + const uint32_t *tokens, int n_ctx, + int sys_len, int *delta_start, + int *capture_sys); + +/** + * How many of this turn's prompt tokens the cache let the caller SKIP — the + * prompt tokens NOT computed this turn. This is exactly OpenAI's + * `usage.prompt_tokens_details.cached_tokens`, and the only supported way to + * derive it: it is a restatement of the resume boundary the cache ITSELF chose + * (`delta_start`), never a second opinion recomputed from the prompt. A + * recomputed number would measure the reporter instead of the cache — the + * client's whole reason for wanting it is that `prompt_tokens / TTFT` silently + * inflates with cache hits, so the correction has to come from the mechanism. + * + * Every resume rung lands in `delta_start`, so all of them are covered by + * construction: full-history append, think-elided append (batched), system- + * prefix snapshot restore, and the llama-family LCP rewind. A full prefill + * (`delta_start == 0`) reports 0 — reported, not omitted, so a client can tell + * "the serve said nothing was reused" from "the serve does not report this". + * + * The clamp is what makes the wire invariant `0 <= cached_tokens <= n_ctx` + * unconditional, including for a caller that hands over a boundary from before + * its own fallback reset. + * + * @param delta_start The cache's chosen resume position for this turn. + * @param n_ctx This turn's prompt length in tokens. + * @return Reused prompt tokens, clamped to [0, n_ctx]. + * + * Thread-safe: yes (pure) + * Reentrant: yes + * Signal-safe: yes + */ +static inline int pn_prefix_cached_tokens(int delta_start, int n_ctx) +{ + if (delta_start <= 0 || n_ctx <= 0) + return 0; + return delta_start < n_ctx ? delta_start : n_ctx; +} + +/** + * SYS1 — snapshot session `sid`'s state at its system-prompt boundary. Call + * mid-prefill, when the session's state sits EXACTLY at pos == sys_len having + * just prefilled tokens[0..sys_len) (the acquire's *capture_sys == 1 protocol). + * Replaces any previous snapshot for the session. On the GPU path the live GPU + * state is synced to the host first, so the snapshot is self-contained. + * + * @param tokens The full prompt (tokens[0..sys_len) are copied as the guard). + * @param sys_len System-prefix token count (> 0). + * @return 0 on success, -1 on failure (unknown session / pos mismatch / + * unsupported architecture / allocation). Failure is benign: + * the caller just continues its full prefill uncached. + * + * Thread-safe: no + */ +int pn_prefix_cache_capture_sys(pn_prefix_cache *cache, const char *sid, + const uint32_t *tokens, int sys_len); + +/** + * Commit session `sid`'s new committed token history (prompt + everything + * generated this turn), tokens[0..n_ctx). The session's pn_state must already + * be at pos == n_ctx (the caller prefilled the delta and stepped decode). The + * history drives next turn's delta computation. + * + * @return 0 on success, -1 on failure (unknown session / allocation). + * + * Thread-safe: no + */ +int pn_prefix_cache_commit(pn_prefix_cache *cache, const char *sid, + const uint32_t *tokens, int n_ctx); + +/** + * Session warm-tier EXPORT (K80 serving-ops Phase 7). Snapshot session `sid`'s + * FULL current committed state (pos == its committed history length) as an + * opaque qwen35moe snapshot the caller then serializes to a portable blob. On + * the GPU path the live GPU state is synced to the host first, so the snapshot + * is self-contained (same discipline as pn_prefix_cache_capture_sys). The + * session is left untouched (read-only export). An id holding several + * conversations exports its most recently used one. + * + * @param cache Cache handle. + * @param sid Session id (must be a live slot). + * @param n_tok_out Out (nullable): the committed token count the snapshot + * covers (== snapshot pos). + * @param tokens_out Out (nullable): borrowed pointer to the session's committed + * token history (valid until the next cache op on this cache); + * the caller copies it if it needs to outlive that. + * @return Opaque snapshot (free with pn_qwen35moe_state_snapshot_free), + * or NULL (unknown session / empty history / unsupported arch + * / GPU sync or snapshot failure). + * + * Thread-safe: no (serialize with the cache's lock). + */ +void *pn_prefix_cache_export_snapshot(pn_prefix_cache *cache, const char *sid, + int *n_tok_out, const uint32_t **tokens_out); + +/** + * Session warm-tier IMPORT/RESTORE (K80 serving-ops Phase 7). Take (or reuse) a + * live slot for `sid` and restore `snap` (a deserialized qwen35moe snapshot) + * into its state, then install `tokens[0..n_tok)` as the committed history and + * position the state at pos == n_tok, so the very next acquire is a full-history + * append resume (delta prefill of just what the client sends beyond n_tok). The + * caller must have already admitted the session (capacity) and validated the + * blob's weights_version against this model. `snap`'s pos MUST equal n_tok. + * + * The restore replaces the id's conversation whose committed history `tokens` + * continues, or is a prefix of (or an empty one); otherwise it is another + * conversation of the id and takes its own entry, leaving the others intact. + * + * On the GPU path the state's GPU bindings are forgotten so the next forward + * re-seeds from the restored host mirror (same as the SYS1 restore path). + * + * @param cache Cache handle (must be non-NULL — restore requires caching). + * @param sid Session id (created if new; reused if already live). + * @param snap Deserialized snapshot to restore (pos must equal n_tok). + * @param tokens Committed token history to install [0..n_tok). + * @param n_tok Committed token count (> 0, == snap pos). + * @return 0 on success, -1 on failure (bad args / pos mismatch / dim + * mismatch / allocation / no free slot). + * + * Thread-safe: no (serialize with the cache's lock). + */ +int pn_prefix_cache_import_snapshot(pn_prefix_cache *cache, const char *sid, + const void *snap, const uint32_t *tokens, + int n_tok); + +/** + * Reset session `sid` to an empty state: clears its cached KV/SSM state, + * committed history, and system-prefix snapshot, so the NEXT acquire does a + * clean full prefill. Used to recover after a turn was ABORTED mid-prefill — + * the partial, uncommitted prefill left state->pos ahead of the committed + * history, which would break append-resume; this discards it. No-op for an + * unknown session. Thread-safe: no. + */ +void pn_prefix_cache_reset_session(pn_prefix_cache *cache, const char *sid); + +/** + * Release session `sid`: free its slots outright — every conversation it holds + * (state + snapshot freed, sid cleared), returning capacity to the pool IMMEDIATELY so a waiting session can be + * admitted. Unlike pn_prefix_cache_reset_session (which keeps the slot occupied), + * this drops the whole entry. Used by POST /session/release on a client's clean + * shutdown so a departing session doesn't hold a slot until the idle-TTL reaps it. + * No-op for an unknown session. Thread-safe: no. + */ +void pn_prefix_cache_release_session(pn_prefix_cache *cache, const char *sid); + +/** + * Release EVERY live session whose sid begins with `base_prefix` — the + * prefix-match complement to pn_prefix_cache_release_session. A liveness watch is + * held on a tab's BASE id `client--`, but the live KV slots are the + * mode-suffixed sids `-orch`/`-act`/`-chat`; when that base's watch dies + * past grace this frees all of its mode slots at once (pass `base_prefix` = + * "-"). Each freed slot returns capacity immediately. No-op for a NULL + * cache, a NULL/empty prefix, or no match. Thread-safe: no (serialize with the + * same lock as acquire/release). + */ +void pn_prefix_cache_release_by_prefix(pn_prefix_cache *cache, + const char *base_prefix); + +#endif /* PN_PREFIX_CACHE_H */ diff --git a/src/core/qweight.c b/src/core/qweight.c new file mode 100644 index 0000000..0de6726 --- /dev/null +++ b/src/core/qweight.c @@ -0,0 +1,887 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qweight.c + * @brief Quantized weight matvec / matmul / dot kernels, threading and GPU dispatch. + * + * Written from a behaviour specification (plans/cleanroom_r2_qweight_spec.md); no ggml or + * llama.cpp source was consulted. + * + * Release results depend on clang fast-math codegen for the exact expression shapes below + * (statement order, accumulator variables, helper boundaries). Keep the shapes. + * + * @status None + * @issues - Block kernels ignore a partial trailing block; Q2_0_g128 processes it. + * - Ternary is unsupported through pn_qdot but works through pn_qmv. + * - Ternary 4-wide loop mis-decodes when group_size % 4 != 0; group_size <= 0 + * divides by zero. + * - Q2_0 release math is amax*RN(1/127) and 127/amax; debug computes the literal + * source, so release and debug may differ at ties. + * - pn_qmm / pn_qmm_prefill output is undefined for types the row dequant does + * not cover and for non-block-multiple cols; dequant return codes are ignored. + * - pn_qmv_group under USE_METAL dereferences a NULL entry; its fallback path + * reports each weight to the observer twice. + * - pn_qweight_from_gguf ignores middle dims, casts dims to int, floors the row + * stride, leaves partial fields written on the silent -1 path. + * - Non-Metal pn_qmv_gpu_upload returns 0 and pn_qmv_gpu_free leaves gpu set. + * - The four Metal staging capacity globals have external linkage. + * - pn_qmv_set_pool is ignored without USE_THREADS. + * @todo None + */ +#include "qweight.h" + +#include +#include +#include +#include +#include + +#if defined(__ARM_NEON) || defined(__ARM_NEON__) +#include +#define PN_QWEIGHT_NEON 1 +#endif + +#ifdef USE_BLAS + #if defined(__APPLE__) + #include + #else + #include + #endif +#endif + +#include "../accel/simd.h" + +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +#ifdef USE_METAL +#include "metal/fm_metal.h" +#endif + +/* Rows below this run on the calling thread; the dispatch costs more than it saves. */ +#define QWEIGHT_THREAD_MIN_ROWS 128 +/* pn_qmv_group coalesces at most this many weights into one dispatch. */ +#define QWEIGHT_GROUP_MAX 8 +/* BLAS panel target: 131072 floats (~512 KB) per dequantized panel. */ +#define QWEIGHT_PANEL_FLOATS 131072 +#define QWEIGHT_PANEL_MAX_ROWS 128 +/* Prefill panels never drop below this so sgemm's N stays AMX-friendly. */ +#define QWEIGHT_PREFILL_MIN_ROWS 64 + +/* ------------------------------------------------------------------------------------------ + * Process-wide state + * ---------------------------------------------------------------------------------------- */ + +#ifdef USE_THREADS +static fw_pool *g_pool = NULL; +#endif + +static int g_gpu_decode = 0; +static pn_qweight_dispatch_observer_fn g_observer_fn = NULL; +static void *g_observer_user = NULL; + +#ifdef USE_METAL +static fm_buffer *g_gpu_in = NULL; +static fm_buffer *g_gpu_out = NULL; +static fm_buffer *g_qmm_in = NULL; +static fm_buffer *g_qmm_out = NULL; +size_t g_gpu_in_cap = 0; +size_t g_gpu_out_cap = 0; +size_t g_qmm_in_cap = 0; +size_t g_qmm_out_cap = 0; +#endif + +/* ------------------------------------------------------------------------------------------ + * Per-type row dot kernels + * ---------------------------------------------------------------------------------------- */ + +static float dot_row_f16(const void *row, const float *vec, int n) { + const uint16_t *src16 = (const uint16_t *)row; + float sum = 0.0f; + for (int i = 0; i < n; i++) + sum += pn_half_to_float(src16[i]) * vec[i]; + return sum; +} + +static float dot_row_q4_0(const uint8_t *row, const float *vec, int n) { + int nb = n / 32; + float sum = 0.0f; + for (int i = 0; i < nb; i++) + sum += pn_dot_q4_0_f32(row + 18 * i, vec + 32 * i); + return sum; +} + +static float dot_row_q8_0(const uint8_t *row, const float *vec, int n) { + int nb = n / 32; + float sum = 0.0f; + for (int i = 0; i < nb; i++) + sum += pn_dot_q8_0_f32(row + 34 * i, vec + 32 * i); + return sum; +} + +typedef void (*dequant_block_fn)(const void *block, float *out); + +/* Dequantize each 256-element block to a stack scratch, then dot it. */ +static float dot_row_kquant_scratch(const uint8_t *row, const float *vec, int n, + size_t block_bytes, dequant_block_fn dequant) { + int nb = n / 256; + float sum = 0.0f; + float tmp[256]; + for (int i = 0; i < nb; i++) { + dequant(row + (size_t)i * block_bytes, tmp); + sum += pn_dot_f32(tmp, vec + 256 * i, 256); + } + return sum; +} + +#ifdef PN_QWEIGHT_NEON + +/* Widen 16 quant bytes to floats, accumulate q*v into *qd and the plain sum of v into *vs. */ +static inline void acc16(uint8x16_t q, const float *v, float32x4_t *qd, float32x4_t *vs) { + uint16x8_t q_lo = vmovl_u8(vget_low_u8(q)); + uint16x8_t q_hi = vmovl_high_u8(q); + float32x4_t q0 = vcvtq_f32_u32(vmovl_u16(vget_low_u16(q_lo))); + float32x4_t q1 = vcvtq_f32_u32(vmovl_high_u16(q_lo)); + float32x4_t q2 = vcvtq_f32_u32(vmovl_u16(vget_low_u16(q_hi))); + float32x4_t q3 = vcvtq_f32_u32(vmovl_high_u16(q_hi)); + float32x4_t v0 = vld1q_f32(v); + float32x4_t v1 = vld1q_f32(v + 4); + float32x4_t v2 = vld1q_f32(v + 8); + float32x4_t v3 = vld1q_f32(v + 12); + *qd = vfmaq_f32(*qd, q0, v0); + *qd = vfmaq_f32(*qd, q1, v1); + *qd = vfmaq_f32(*qd, q2, v2); + *qd = vfmaq_f32(*qd, q3, v3); + *vs = vaddq_f32(*vs, vaddq_f32(vaddq_f32(v0, v1), vaddq_f32(v2, v3))); +} + +static void unpack_q4_k_scales(const uint8_t *s, uint8_t *sc, uint8_t *mn) { + for (int j = 0; j < 4; j++) { + sc[j] = s[j] & 63; + mn[j] = s[j + 4] & 63; + } + for (int j = 4; j < 8; j++) { + sc[j] = (uint8_t)((s[j + 4] & 0x0F) | ((s[j - 4] >> 6) << 4)); + mn[j] = (uint8_t)((s[j + 4] >> 4) | ((s[j] >> 6) << 4)); + } +} + +/* One Q4_K block (256 elements) folded into the row accumulator acc. */ +static inline float32x4_t q4_k_block_acc(const uint8_t *blk, const float *vec, + float32x4_t acc) { + uint16_t d_bits = 0; + uint16_t dmin_bits = 0; + memcpy(&d_bits, blk, sizeof(d_bits)); + memcpy(&dmin_bits, blk + 2, sizeof(dmin_bits)); + float d = pn_half_to_float(d_bits); + float dmin = pn_half_to_float(dmin_bits); + uint8_t sc[8]; + uint8_t mn[8]; + unpack_q4_k_scales(blk + 4, sc, mn); + const uint8_t *qs = blk + 16; + const uint8x16_t mask_lo = vdupq_n_u8(0x0F); + + for (int k = 0; k < 4; k++) { + uint8x16_t p0 = vld1q_u8(qs + 32 * k); + uint8x16_t p1 = vld1q_u8(qs + 32 * k + 16); + float32x4_t qd = vdupq_n_f32(0.0f); + float32x4_t vs = vdupq_n_f32(0.0f); + acc16(vandq_u8(p0, mask_lo), vec, &qd, &vs); + acc16(vandq_u8(p1, mask_lo), vec + 16, &qd, &vs); + acc = vfmaq_n_f32(acc, qd, d * (float)sc[2 * k]); + acc = vfmaq_n_f32(acc, vs, -dmin * (float)mn[2 * k]); + + qd = vdupq_n_f32(0.0f); + vs = vdupq_n_f32(0.0f); + acc16(vshrq_n_u8(p0, 4), vec + 32, &qd, &vs); + acc16(vshrq_n_u8(p1, 4), vec + 48, &qd, &vs); + acc = vfmaq_n_f32(acc, qd, d * (float)sc[2 * k + 1]); + acc = vfmaq_n_f32(acc, vs, -dmin * (float)mn[2 * k + 1]); + vec += 64; + } + return acc; +} + +static float dot_row_q4_k(const uint8_t *row, const float *vec, int n) { + int nb = n / 256; + float32x4_t acc = vdupq_n_f32(0.0f); + for (int i = 0; i < nb; i++) + acc = q4_k_block_acc(row + (size_t)i * 144, vec + 256 * i, acc); + float sum = 0.0f; + sum += vaddvq_f32(acc); + return sum; +} + +/* One quant group of a Q6_K block: q against 16 inputs, scale s, bias -32 folded in. */ +static inline float32x4_t q6_k_group_acc(uint8x16_t q, const float *input, float d, + int8_t s, float32x4_t acc) { + float32x4_t qd = vdupq_n_f32(0.0f); + float32x4_t vs = vdupq_n_f32(0.0f); + acc16(q, input, &qd, &vs); + float coef = d * (float)s; + acc = vfmaq_n_f32(acc, qd, coef); + acc = vfmaq_n_f32(acc, vs, -32.0f * coef); + return acc; +} + +/* One Q6_K block (256 elements) folded into the row accumulator acc. */ +static inline float32x4_t q6_k_block_acc(const uint8_t *blk, const float *vec, + float32x4_t acc) { + uint16_t d_bits = 0; + memcpy(&d_bits, blk + 208, sizeof(d_bits)); + float d = pn_half_to_float(d_bits); + const uint8_t *ql = blk; + const uint8_t *qh = blk + 128; + const int8_t *sc = (const int8_t *)(blk + 192); + const uint8x16_t mask_lo = vdupq_n_u8(0x0F); + const uint8x16_t mask_2 = vdupq_n_u8(3); + + for (int h = 0; h < 2; h++) { + const float *vo = vec + 128 * h; + for (int g = 0; g < 2; g++) { + int o = 16 * g; + uint8x16_t la = vld1q_u8(ql + o); + uint8x16_t lb = vld1q_u8(ql + 32 + o); + uint8x16_t hb = vld1q_u8(qh + o); + uint8x16_t q1 = vorrq_u8(vandq_u8(la, mask_lo), + vshlq_n_u8(vandq_u8(hb, mask_2), 4)); + uint8x16_t q2 = vorrq_u8(vandq_u8(lb, mask_lo), + vshlq_n_u8(vandq_u8(vshrq_n_u8(hb, 2), mask_2), 4)); + uint8x16_t q3 = vorrq_u8(vshrq_n_u8(la, 4), + vshlq_n_u8(vandq_u8(vshrq_n_u8(hb, 4), mask_2), 4)); + uint8x16_t q4 = vorrq_u8(vshrq_n_u8(lb, 4), vshlq_n_u8(vshrq_n_u8(hb, 6), 4)); + acc = q6_k_group_acc(q1, vo + o, d, sc[0 + g], acc); + acc = q6_k_group_acc(q2, vo + 32 + o, d, sc[2 + g], acc); + acc = q6_k_group_acc(q3, vo + 64 + o, d, sc[4 + g], acc); + acc = q6_k_group_acc(q4, vo + 96 + o, d, sc[6 + g], acc); + } + ql += 64; + qh += 32; + sc += 8; + } + return acc; +} + +static float dot_row_q6_k(const uint8_t *row, const float *vec, int n) { + int nb = n / 256; + float32x4_t acc = vdupq_n_f32(0.0f); + for (int i = 0; i < nb; i++) + acc = q6_k_block_acc(row + (size_t)i * 210, vec + 256 * i, acc); + float sum = 0.0f; + sum += vaddvq_f32(acc); + return sum; +} + +#else /* !PN_QWEIGHT_NEON */ + +static float dot_row_q4_k(const uint8_t *row, const float *vec, int n) { + return dot_row_kquant_scratch(row, vec, n, 144, pn_dequantize_block_q4_k); +} + +static float dot_row_q6_k(const uint8_t *row, const float *vec, int n) { + return dot_row_kquant_scratch(row, vec, n, 210, pn_dequantize_block_q6_k); +} + +#endif /* PN_QWEIGHT_NEON */ + +/* Q2_0_g128 with activations quantized to int8 per 32-element sub-block. */ +static float dot_row_q2_0_g128(const uint8_t *row, const float *vec, int n) { + float sum = 0.0f; + int nblk = (n + 127) / 128; + for (int blk = 0; blk < nblk; blk++) { + const uint8_t *b = row + 34 * blk; + uint16_t d_bits = 0; + memcpy(&d_bits, b, sizeof(d_bits)); + float d_w = pn_half_to_float(d_bits); + const uint8_t *qs = b + 2; + int start = 128 * blk; + int cnt = n - start; + if (cnt > 128) + cnt = 128; + float blk_acc = 0.0f; + int sb = 0; + while (sb + 32 <= cnt) { + float amax = 0.0f; + for (int j = 0; j < 32; j++) { + float a = fabsf(vec[start + sb + j]); + if (a > amax) + amax = a; + } + float d_pre = amax / 127.0f; + float id = (d_pre != 0.0f) ? 1.0f / d_pre : 0.0f; + float d_act = pn_half_to_float(pn_float_to_half(d_pre)); + int isum = 0; + for (int j = 0; j < 32; j++) { + int k = sb + j; + int code = (qs[k >> 2] >> (2 * (k & 3))) & 3; + isum += (code - 1) * (int)roundf(vec[start + sb + j] * id); + } + blk_acc += d_act * (float)isum; + sb += 32; + } + for (int j = sb; j < cnt; j++) { + int code = (qs[j >> 2] >> (2 * (j & 3))) & 3; + blk_acc += (float)(code - 1) * vec[start + j]; + } + sum += d_w * blk_acc; + } + return sum; +} + +/* Packed 2-bit ternary row (00=-1, 01=0, 10=+1, 11=nothing) with per-group scales. */ +static float dot_row_ternary(const uint8_t *packed, const float *vec, int n, + const float *scales, int gs) { + int n_groups = (n + gs - 1) / gs; + float sum = 0.0f; + for (int g = 0; g < n_groups; g++) { + float scale = scales[g]; + int start = g * gs; + int end = start + gs; + if (end > n) + end = n; + float gs_acc = 0.0f; + int i = start; + /* Always decodes byte i>>2 from bit pair 0, even when i % 4 != 0 [AS-IS]. */ + while (i + 3 < end) { + uint8_t byte = packed[i >> 2]; + for (int k = 0; k < 4; k++) { + int c = (byte >> (2 * k)) & 3; + if (c == 0) + gs_acc -= vec[i + k]; + else if (c == 2) + gs_acc += vec[i + k]; + } + i += 4; + } + while (i < end) { + int c = (packed[i >> 2] >> (2 * (i & 3))) & 3; + if (c == 0) + gs_acc -= vec[i]; + else if (c == 2) + gs_acc += vec[i]; + i++; + } + sum += scale * gs_acc; + } + return sum; +} + +float pn_qdot(const void *row_data, const float *vec, int n, pn_gguf_quant type) { + const uint8_t *row = (const uint8_t *)row_data; + switch (type) { + case GGUF_QUANT_F32: + return pn_dot_f32((const float *)row_data, vec, n); + case GGUF_QUANT_F16: + return dot_row_f16(row_data, vec, n); + case GGUF_QUANT_Q4_0: + return dot_row_q4_0(row, vec, n); + case GGUF_QUANT_Q8_0: + return dot_row_q8_0(row, vec, n); + case GGUF_QUANT_Q3_K: + return dot_row_kquant_scratch(row, vec, n, 110, pn_dequantize_block_q3_k); + case GGUF_QUANT_Q4_K: + return dot_row_q4_k(row, vec, n); + case GGUF_QUANT_Q5_K: + return dot_row_kquant_scratch(row, vec, n, 176, pn_dequantize_block_q5_k); + case GGUF_QUANT_Q6_K: + return dot_row_q6_k(row, vec, n); + case GGUF_QUANT_Q2_0_G128: + return dot_row_q2_0_g128(row, vec, n); + default: + fprintf(stderr, "pn_qdot: unsupported type %d\n", (int)type); + return 0.0f; + } +} + +/* ------------------------------------------------------------------------------------------ + * Observer and process-wide setters + * ---------------------------------------------------------------------------------------- */ + +void pn_qweight_set_dispatch_observer(pn_qweight_dispatch_observer_fn fn, void *user) { + g_observer_fn = fn; + g_observer_user = user; +} + +void pn_qweight_note_dispatch(const pn_qweight *w, pn_qweight_op op) { + if (g_observer_fn && w) + g_observer_fn(w, op, g_observer_user); +} + +void pn_qmv_set_pool(struct fw_pool *pool) { +#ifdef USE_THREADS + g_pool = pool; +#else + (void)pool; +#endif +} + +void pn_qmm_set_gpu_decode(int on) { + g_gpu_decode = on; +} + +/* ------------------------------------------------------------------------------------------ + * GPU (Metal) staging, matvec and residency + * ---------------------------------------------------------------------------------------- */ + +#ifdef USE_METAL + +/* Reuse a staging buffer if big enough, else regrow it with 25% + 4 KB slack. */ +static fm_buffer *gpu_stage(fm_buffer **buf, size_t *cap, size_t bytes) { + if (*buf && *cap >= bytes) + return *buf; + if (*buf) { + fm_buffer_free(*buf); + *buf = NULL; + } + size_t want = bytes + (bytes >> 2) + 4096; + *buf = fm_buffer_alloc(want); + *cap = *buf ? want : 0; + return *buf; +} + +/* Returns 1 if the GPU computed out, 0 if the caller must run the CPU path. */ +static int gpu_matvec(const pn_qweight *w, const float *vec, float *out) { + if (!w->gpu || w->type != GGUF_QUANT_Q4_K || !fm_available()) + return 0; + size_t in_bytes = (size_t)w->cols * sizeof(float); + size_t out_bytes = (size_t)w->rows * sizeof(float); + fm_buffer *in = gpu_stage(&g_gpu_in, &g_gpu_in_cap, in_bytes); + fm_buffer *outbuf = gpu_stage(&g_gpu_out, &g_gpu_out_cap, out_bytes); + if (!in || !outbuf) + return 0; + memcpy(fm_buffer_data(in), vec, in_bytes); + fm_matvec_q4_k((fm_buffer *)w->gpu, in, outbuf, w->rows, w->cols, (int)w->row_stride); + fm_sync(); + memcpy(out, fm_buffer_data(outbuf), out_bytes); + return 1; +} + +/* Returns 1 if the GPU computed out, 0 if the caller must run the CPU path. */ +static int gpu_qmm(const pn_qweight *w, const float *acts, int B, float *out) { + if (!w->gpu || !g_gpu_decode || w->type != GGUF_QUANT_Q4_K || B > 32 || !fm_available()) + return 0; + size_t in_bytes = (size_t)B * (size_t)w->cols * sizeof(float); + size_t out_bytes = (size_t)B * (size_t)w->rows * sizeof(float); + fm_buffer *in = gpu_stage(&g_qmm_in, &g_qmm_in_cap, in_bytes); + fm_buffer *outbuf = gpu_stage(&g_qmm_out, &g_qmm_out_cap, out_bytes); + if (!in || !outbuf) + return 0; + memcpy(fm_buffer_data(in), acts, in_bytes); + fm_sgemm_q4_k((fm_buffer *)w->gpu, in, outbuf, w->rows, w->cols, (int)w->row_stride, B); + fm_sync(); + memcpy(out, fm_buffer_data(outbuf), out_bytes); + return 1; +} + +#endif /* USE_METAL */ + +int pn_qmv_gpu_available(void) { +#ifdef USE_METAL + return fm_available() ? 1 : 0; +#else + return 0; +#endif +} + +int pn_qmv_gpu_upload(pn_qweight *w) { +#ifdef USE_METAL + if (!w) + return 0; + if (w->gpu) + return 1; + if (w->type != GGUF_QUANT_Q4_K) + return 0; + if (fm_init() != 0 || !fm_available()) + return 0; + size_t bytes = (size_t)w->rows * w->row_stride; + fm_buffer *buf = fm_buffer_alloc(bytes); + if (!buf) + return 0; + memcpy(fm_buffer_data(buf), w->data, bytes); + w->gpu = buf; + return 1; +#else + (void)w; + return 0; +#endif +} + +void pn_qmv_gpu_free(pn_qweight *w) { +#ifdef USE_METAL + if (w && w->gpu) { + fm_buffer_free((fm_buffer *)w->gpu); + w->gpu = NULL; + } +#else + (void)w; +#endif +} + +/* ------------------------------------------------------------------------------------------ + * pn_qmv + * ---------------------------------------------------------------------------------------- */ + +typedef struct { + const pn_qweight *w; + const float *vec; + float *out; + int n_groups; +} qmv_task; + +static void qmv_ternary_range(int start, int end, void *arg) { + const qmv_task *t = (const qmv_task *)arg; + const pn_qweight *w = t->w; + for (int r = start; r < end; r++) { + const uint8_t *row = (const uint8_t *)w->data + (size_t)r * w->row_stride; + const float *row_scales = w->scales + (size_t)r * (size_t)t->n_groups; + t->out[r] = dot_row_ternary(row, t->vec, w->cols, row_scales, w->group_size); + } +} + +static void qmv_range(int start, int end, void *arg) { + const qmv_task *t = (const qmv_task *)arg; + const pn_qweight *w = t->w; + for (int r = start; r < end; r++) { + const uint8_t *row = (const uint8_t *)w->data + (size_t)r * w->row_stride; + t->out[r] = pn_qdot(row, t->vec, w->cols, w->type); + } +} + +/* Run fn over [0, rows), split over the pool when one is set and rows clear the threshold. */ +static void run_rows(int rows, void (*fn)(int, int, void *), void *task) { +#ifdef USE_THREADS + if (g_pool && rows >= QWEIGHT_THREAD_MIN_ROWS) { + fw_for(g_pool, rows, fn, task); + return; + } +#endif + fn(0, rows, task); +} + +void pn_qmv(const pn_qweight *w, const float *vec, float *out) { + if (!w || !vec || !out) + return; + pn_qweight_note_dispatch(w, PN_QWEIGHT_OP_MATVEC); + + qmv_task task = { w, vec, out, 0 }; + if (w->type == PN_QUANT_TERNARY) { + task.n_groups = (w->cols + w->group_size - 1) / w->group_size; + run_rows(w->rows, qmv_ternary_range, &task); + return; + } +#ifdef USE_METAL + if (w->gpu && gpu_matvec(w, vec, out)) + return; +#endif + run_rows(w->rows, qmv_range, &task); +} + +/* ------------------------------------------------------------------------------------------ + * pn_qmv_group + * ---------------------------------------------------------------------------------------- */ + +#ifdef USE_THREADS + +typedef struct { + const pn_qweight *const *w; + const float *const *vec; + float *const *out; + int off[QWEIGHT_GROUP_MAX + 1]; + int n; +} qmv_group_task; + +/* Global rows [start, end) across the concatenated weights. */ +static void qmv_group_range(int start, int end, void *arg) { + const qmv_group_task *t = (const qmv_group_task *)arg; + int s = 0; + while (s < t->n && start >= t->off[s + 1]) + s++; + for (int r = start; r < end; r++) { + while (r >= t->off[s + 1]) + s++; + const pn_qweight *w = t->w[s]; + int lr = r - t->off[s]; + const uint8_t *row = (const uint8_t *)w->data + (size_t)lr * w->row_stride; + t->out[s][lr] = pn_qdot(row, t->vec[s], w->cols, w->type); + } +} + +/* Returns 1 if all n weights were computed in one pool dispatch. */ +static int qmv_group_coalesced(const pn_qweight *const *w, const float *const *vec, + float *const *out, int n) { + int coalesce = g_pool != NULL; + for (int i = 0; i < n && coalesce; i++) { + if (!w[i] || w[i]->type == PN_QUANT_TERNARY) + coalesce = 0; +#ifdef USE_METAL + /* Dereferences a NULL entry [AS-IS]. */ + if (w[i]->gpu != NULL && w[i]->type == GGUF_QUANT_Q4_K) + coalesce = 0; +#endif + } + if (!coalesce) + return 0; + + qmv_group_task task = { w, vec, out, {0}, n }; + task.off[0] = 0; + for (int i = 0; i < n; i++) + task.off[i + 1] = task.off[i] + w[i]->rows; + if (task.off[n] < QWEIGHT_THREAD_MIN_ROWS) + return 0; + fw_for(g_pool, task.off[n], qmv_group_range, &task); + return 1; +} + +#endif /* USE_THREADS */ + +void pn_qmv_group(const pn_qweight *const *w, const float *const *vec, + float *const *out, int n) { + if (n <= 0 || !w) + return; + if (n > QWEIGHT_GROUP_MAX) + n = QWEIGHT_GROUP_MAX; + for (int i = 0; i < n; i++) + pn_qweight_note_dispatch(w[i], PN_QWEIGHT_OP_MATVEC); +#ifdef USE_THREADS + if (qmv_group_coalesced(w, vec, out, n)) + return; +#endif + for (int i = 0; i < n; i++) + pn_qmv(w[i], vec[i], out[i]); +} + +/* ------------------------------------------------------------------------------------------ + * pn_qmm / pn_qmm_prefill + * ---------------------------------------------------------------------------------------- */ + +/* Dequantize one full row; -1 (nothing written) for types it does not cover. */ +static int dequant_row(const uint8_t *row, pn_gguf_quant type, int cols, float *out) { + switch (type) { + case GGUF_QUANT_F32: + memcpy(out, row, (size_t)cols * sizeof(float)); + return 0; + case GGUF_QUANT_F16: { + const uint16_t *src16 = (const uint16_t *)row; + for (int i = 0; i < cols; i++) + out[i] = pn_half_to_float(src16[i]); + return 0; + } + case GGUF_QUANT_Q4_0: + for (int i = 0; i < cols / 32; i++) + pn_dequantize_block_q4_0(row + (size_t)i * 18, out + 32 * i); + return 0; + case GGUF_QUANT_Q8_0: + for (int i = 0; i < cols / 32; i++) + pn_dequantize_block_q8_0(row + (size_t)i * 34, out + 32 * i); + return 0; + case GGUF_QUANT_Q3_K: + for (int i = 0; i < cols / 256; i++) + pn_dequantize_block_q3_k(row + (size_t)i * 110, out + 256 * i); + return 0; + case GGUF_QUANT_Q4_K: + for (int i = 0; i < cols / 256; i++) + pn_dequantize_block_q4_k(row + (size_t)i * 144, out + 256 * i); + return 0; + case GGUF_QUANT_Q5_K: + for (int i = 0; i < cols / 256; i++) + pn_dequantize_block_q5_k(row + (size_t)i * 176, out + 256 * i); + return 0; + case GGUF_QUANT_Q6_K: + for (int i = 0; i < cols / 256; i++) + pn_dequantize_block_q6_k(row + (size_t)i * 210, out + 256 * i); + return 0; + default: + return -1; + } +} + +/* Rows per ~512 KB dequantized panel, clamped to [1, 128]. */ +static int panel_rows(int cols) { + int c = cols > 1 ? cols : 1; + int rows = QWEIGHT_PANEL_FLOATS / c; + if (rows < 1) + rows = 1; + if (rows > QWEIGHT_PANEL_MAX_ROWS) + rows = QWEIGHT_PANEL_MAX_ROWS; + return rows; +} + +typedef struct { + const pn_qweight *w; + const float *acts; + int B; + float *out; + int panel; +} qmm_task; + +static const uint8_t *qweight_row(const pn_qweight *w, int r) { + return (const uint8_t *)w->data + (size_t)r * w->row_stride; +} + +/* out[b*rows + r0 + i] for rows r0 .. r0+rb-1, from rb dequantized rows in panel. */ +static void qmm_panel_product(const qmm_task *t, const float *panel, int r0, int rb) { + const pn_qweight *w = t->w; + int cols = w->cols; +#ifdef USE_BLAS + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, t->B, rb, cols, + 1.0f, t->acts, cols, panel, cols, 0.0f, t->out + r0, w->rows); +#else + for (int i = 0; i < rb; i++) { + for (int b = 0; b < t->B; b++) + t->out[(size_t)b * w->rows + r0 + i] = + pn_dot_f32(panel + (size_t)i * cols, t->acts + (size_t)b * cols, cols); + } +#endif +} + +/* Direct quantized dots for rows [r0, r1): the allocation-failure fallback. */ +static void qmm_rows_direct(const qmm_task *t, int r0, int r1) { + const pn_qweight *w = t->w; + for (int r = r0; r < r1; r++) { + for (int b = 0; b < t->B; b++) + t->out[(size_t)b * w->rows + r] = + pn_qdot(qweight_row(w, r), t->acts + (size_t)b * w->cols, w->cols, w->type); + } +} + +#ifdef USE_BLAS +/* Panelled sgemm over rows [start, end). Returns -1 if the panel cannot be allocated. */ +static int qmm_range_blas(const qmm_task *t, int start, int end) { + const pn_qweight *w = t->w; + int cols = w->cols; + int panel_cap = panel_rows(cols); + float *panel = malloc((size_t)panel_cap * (size_t)cols * sizeof(float)); + if (!panel) + return -1; + for (int r0 = start; r0 < end; r0 += panel_cap) { + int rb = end - r0 < panel_cap ? end - r0 : panel_cap; + for (int i = 0; i < rb; i++) + dequant_row(qweight_row(w, r0 + i), w->type, cols, panel + (size_t)i * cols); + qmm_panel_product(t, panel, r0, rb); + } + free(panel); + return 0; +} +#endif + +static void qmm_range(int start, int end, void *arg) { + const qmm_task *t = (const qmm_task *)arg; + const pn_qweight *w = t->w; +#ifdef USE_BLAS + if (qmm_range_blas(t, start, end) == 0) + return; +#endif + float *scratch = malloc((size_t)w->cols * sizeof(float)); + if (!scratch) { + qmm_rows_direct(t, start, end); + return; + } + for (int r = start; r < end; r++) { + dequant_row(qweight_row(w, r), w->type, w->cols, scratch); + for (int b = 0; b < t->B; b++) + t->out[(size_t)b * w->rows + r] = + pn_dot_f32(scratch, t->acts + (size_t)b * w->cols, w->cols); + } + free(scratch); +} + +/* Per-batch pn_qmv loop: ternary weights and batches above the shared-dequant cap. */ +static void qmm_per_batch(const pn_qweight *w, const float *acts, int B, float *out) { + for (int b = 0; b < B; b++) + pn_qmv(w, acts + (size_t)b * w->cols, out + (size_t)b * w->rows); +} + +void pn_qmm(const pn_qweight *w, const float *acts, int B, float *out) { + if (!w || !acts || !out || B <= 0) + return; + pn_qweight_note_dispatch(w, PN_QWEIGHT_OP_MATMUL); +#ifdef USE_METAL + if (gpu_qmm(w, acts, B, out)) + return; +#endif + if (w->type == PN_QUANT_TERNARY || B > PN_QMM_MAX_B) { + qmm_per_batch(w, acts, B, out); + return; + } + qmm_task task = { w, acts, B, out, 0 }; + run_rows(w->rows, qmm_range, &task); +} + +/* Panel blocks [bs, be) of task->panel rows each. */ +static void prefill_range(int bs, int be, void *arg) { + const qmm_task *t = (const qmm_task *)arg; + const pn_qweight *w = t->w; + int panel = t->panel; + float *pbuf = malloc((size_t)panel * (size_t)w->cols * sizeof(float)); + if (!pbuf) { + for (int blk = bs; blk < be; blk++) { + int r0 = blk * panel; + int rb = w->rows - r0 < panel ? w->rows - r0 : panel; + qmm_rows_direct(t, r0, r0 + rb); + } + return; + } + for (int blk = bs; blk < be; blk++) { + int r0 = blk * panel; + int rb = w->rows - r0 < panel ? w->rows - r0 : panel; + for (int i = 0; i < rb; i++) + dequant_row(qweight_row(w, r0 + i), w->type, w->cols, pbuf + (size_t)i * w->cols); + qmm_panel_product(t, pbuf, r0, rb); + } + free(pbuf); +} + +void pn_qmm_prefill(const pn_qweight *w, const float *acts, int B, float *out) { + if (!w || !acts || !out || B <= 0) + return; + pn_qweight_note_dispatch(w, PN_QWEIGHT_OP_MATMUL); + if (w->type == PN_QUANT_TERNARY) { + qmm_per_batch(w, acts, B, out); + return; + } + int panel = panel_rows(w->cols); + if (panel < QWEIGHT_PREFILL_MIN_ROWS) + panel = QWEIGHT_PREFILL_MIN_ROWS; + int nblk = (w->rows + panel - 1) / panel; + qmm_task task = { w, acts, B, out, panel }; +#ifdef USE_THREADS + if (g_pool && nblk > 1) { + fw_for(g_pool, nblk, prefill_range, &task); + return; + } +#endif + prefill_range(0, nblk, &task); +} + +/* ------------------------------------------------------------------------------------------ + * GGUF loading + * ---------------------------------------------------------------------------------------- */ + +int pn_qweight_from_gguf(pn_qweight *qw, pn_gguf_file *gguf, const char *name) { + if (!qw || !gguf || !name) + return -1; + pn_gguf_tensor_info *info = pn_gguf_find_tensor(gguf, name); + if (!info) { + fprintf(stderr, "pn_qweight_from_gguf: tensor '%s' not found\n", name); + return -1; + } + qw->data = (const char *)gguf->data + info->offset; + qw->type = info->type; + qw->gpu = NULL; + if (info->n_dims >= 2) { + qw->rows = (int)info->dims[info->n_dims - 1]; + qw->cols = (int)info->dims[0]; + } else { + qw->rows = (int)info->dims[0]; + qw->cols = 1; + } + size_t bpb = pn_gguf_bytes_per_block(qw->type); + int bs = pn_gguf_block_size(qw->type); + if (bs > 0 && bpb > 0) { + qw->row_stride = ((size_t)qw->cols / (size_t)bs) * bpb; + return 0; + } + qw->row_stride = 0; + return -1; +} diff --git a/src/core/qweight.h b/src/core/qweight.h new file mode 100644 index 0000000..541b863 --- /dev/null +++ b/src/core/qweight.h @@ -0,0 +1,241 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qweight.h + * @brief Quantized weight handle and its matvec / matmul / dot kernels. + * + * Written from a behaviour specification (plans/cleanroom_r2_qweight_spec.md); no ggml or + * llama.cpp source was consulted. + * + * @status None + * @issues - Block kernels ignore a partial trailing block; Q2_0_g128 processes it. + * - Ternary is unsupported through pn_qdot (stderr + 0) but works via pn_qmv. + * - pn_qmm / pn_qmm_prefill output is undefined for types the row dequant does + * not cover and for non-block-multiple cols. + * - pn_qweight_from_gguf ignores middle dims, never writes scales/group_size. + * @todo None + */ +#ifndef PN_QWEIGHT_H +#define PN_QWEIGHT_H + +#include + +#include "../formats/gguf.h" +#include "../formats/gguf_dequant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** Largest batch pn_qmm runs through the shared-dequant path; larger B loops pn_qmv. */ +#define PN_QMM_MAX_B 64 + +/** + * A quantized weight matrix [rows, cols]. Plain public struct: callers build it by hand. + * + * data row 0 of the quantized bytes; row r starts at data + r*row_stride + * type quant type of every row + * rows output dimension + * cols input dimension + * row_stride bytes between consecutive rows + * scales PN_QUANT_TERNARY only: per-group scales, scales[r*n_groups + g] + * group_size PN_QUANT_TERNARY only: elements per scale group + * gpu fm_buffer * holding the weight bytes on a GPU device, or NULL + * (unconditional field: the struct layout never depends on build flags) + */ +typedef struct { + const void *data; + pn_gguf_quant type; + int rows; + int cols; + size_t row_stride; + const float *scales; + int group_size; + void *gpu; +} pn_qweight; + +/** Kind of dispatch reported to the dispatch observer. */ +typedef enum { + PN_QWEIGHT_OP_MATVEC, + PN_QWEIGHT_OP_MATMUL, + PN_QWEIGHT_OP_DOT +} pn_qweight_op; + +/** Observer called once per reported dispatch, on the dispatching thread. */ +typedef void (*pn_qweight_dispatch_observer_fn)(const pn_qweight *w, + pn_qweight_op op, void *user); + +struct fw_pool; + +/** + * Fill a pn_qweight from a named GGUF tensor (data points into the mmap). + * + * @param qw Output handle; scales and group_size are never written + * @param gguf Open GGUF file + * @param name Tensor name (exact match) + * @return 0 on success; -1 on NULL args, a missing tensor (stderr line), or a type + * that cannot be sized (silent; data/type/gpu/rows/cols already written) + * + * Thread-safe: yes (writes only *qw) + * Reentrant: yes + * Signal-safe: no + */ +int pn_qweight_from_gguf(pn_qweight *qw, pn_gguf_file *gguf, const char *name); + +/** + * out[r] = row r of w dotted with vec, for r in [0, rows). + * + * @param w Weight (NULL: no-op) + * @param vec Input [cols] (NULL: no-op) + * @param out Output [rows] (NULL: no-op) + * + * Thread-safe: no (uses the registered pool and GPU staging buffers) + * Reentrant: no + * Signal-safe: no + */ +void pn_qmv(const pn_qweight *w, const float *vec, float *out); + +/** + * out[i] = w[i] @ vec[i] for i < min(n, 8), in one parallel dispatch when possible. + * Results equal per-weight pn_qmv exactly. + * + * @param w Array of n weight pointers (NULL array: no-op) + * @param vec Array of n input vectors + * @param out Array of n output vectors + * @param n Number of weights (<= 0: no-op; > 8 is clamped to 8) + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +void pn_qmv_group(const pn_qweight *const *w, const float *const *vec, + float *const *out, int n); + +/** + * Batched matvec: out[b*rows + r] = row r of w dotted with acts[b*cols ...]. + * + * @param w Weight + * @param acts Activations [B, cols] row-major + * @param B Batch size (<= 0: no-op; > PN_QMM_MAX_B loops pn_qmv) + * @param out Output [B, rows] row-major + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +void pn_qmm(const pn_qweight *w, const float *acts, int B, float *out); + +/** + * Prefill matmul: as pn_qmm with no B cap and panels of at least 64 rows. + * + * @param w Weight + * @param acts Activations [B, cols] row-major + * @param B Batch size (<= 0: no-op) + * @param out Output [B, rows] row-major + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +void pn_qmm_prefill(const pn_qweight *w, const float *acts, int B, float *out); + +/** + * Install (or clear, fn = NULL) the process-wide dispatch observer. + * + * @param fn Observer, or NULL to disable + * @param user Passed back to fn on every call (stored regardless of fn) + * + * Thread-safe: no (call before the forward) + * Reentrant: no + * Signal-safe: no + */ +void pn_qweight_set_dispatch_observer(pn_qweight_dispatch_observer_fn fn, void *user); + +/** + * Report a dispatch to the observer (if one is installed and w is non-NULL). + * + * @param w Weight being dispatched + * @param op Kind of dispatch + * + * Thread-safe: yes, if the observer is + * Reentrant: yes, if the observer is + * Signal-safe: no + */ +void pn_qweight_note_dispatch(const pn_qweight *w, pn_qweight_op op); + +/** + * Register the thread pool the matvec / matmul entry points split rows over. + * Ignored in builds without USE_THREADS. + * + * @param pool Pool, or NULL to run serially + * + * Thread-safe: no (call before the forward) + * Reentrant: no + * Signal-safe: no + */ +void pn_qmv_set_pool(struct fw_pool *pool); + +/** + * Dot one quantized row with a float vector. + * + * @param row_data Row bytes (any alignment) + * @param vec Input [n] + * @param n Elements; block types use whole blocks only (Q2_0_g128 rounds up) + * @param type Quant type; unsupported types print one stderr line and return 0 + * @return The dot product + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: no + */ +float pn_qdot(const void *row_data, const float *vec, int n, pn_gguf_quant type); + +/** + * @return 1 if the Metal GPU matvec is available (USE_METAL builds), else 0 + * + * Thread-safe: TBD (follows fm_available) + * Reentrant: TBD + * Signal-safe: no + */ +int pn_qmv_gpu_available(void); + +/** + * Upload a Q4_K weight's bytes to the GPU and attach the buffer to w->gpu. + * + * @param w Weight (NULL: returns 0) + * @return 1 if w->gpu is (already or now) set; 0 otherwise. Always 0 without USE_METAL. + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +int pn_qmv_gpu_upload(pn_qweight *w); + +/** + * Free any buffer attached to w->gpu and clear it (USE_METAL builds; else no-op). + * + * @param w Weight (NULL: no-op) + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +void pn_qmv_gpu_free(pn_qweight *w); + +/** + * Enable (non-zero) or disable GPU decode for pn_qmm on GPU-resident Q4_K weights. + * Off by default: GPU decode is slower than the CPU path; weights upload for prefill. + * + * @param on Flag, stored as-is + * + * Thread-safe: no (call before the forward) + * Reentrant: no + * Signal-safe: no + */ +void pn_qmm_set_gpu_decode(int on); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_QWEIGHT_H */ diff --git a/src/core/sample_profiler.c b/src/core/sample_profiler.c new file mode 100644 index 0000000..14bb6b7 --- /dev/null +++ b/src/core/sample_profiler.c @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file sample_profiler.c + * @brief Implementation of the SIGPROF sampling profiler. See sample_profiler.h. + * + * @status Instrument only. + * @issues Linux/x86-64 only. + * @todo None + */ + +#define _GNU_SOURCE +#include "sample_profiler.h" + +#if defined(__linux__) && defined(__x86_64__) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** Samples kept: a 60 s run at 1 kHz on two cores. */ +#define PROF_MAX_SAMPLES (1u << 18) + +/** Frames kept a sample: the interrupted IP, then up to PROF_CALLERS callers (glibc + * backtrace through the signal frame; .eh_frame, no frame pointers needed). */ +#define PROF_CALLERS 4 + +typedef struct prof_sample { + uintptr_t ip; + uintptr_t caller[PROF_CALLERS]; + int32_t tid; +} prof_sample; + +static prof_sample g_samples[PROF_MAX_SAMPLES]; +static _Atomic uint32_t g_count; +static const char *g_path; +static uintptr_t g_base; +static _Atomic int g_window = -1; /**< -1 always record; 0 outside / 1 inside a marked window */ + +static void prof_on_signal(int sig, siginfo_t *info, void *context) +{ + const ucontext_t *uc = (const ucontext_t *)context; + uint32_t slot; + + (void)sig; + (void)info; + if (atomic_load_explicit(&g_window, memory_order_relaxed) == 0) + return; + slot = atomic_fetch_add_explicit(&g_count, 1u, memory_order_relaxed); + if (slot >= PROF_MAX_SAMPLES) + return; + g_samples[slot].ip = (uintptr_t)uc->uc_mcontext.gregs[REG_RIP]; + g_samples[slot].tid = (int32_t)syscall(SYS_gettid); + { + void *frames[PROF_CALLERS + 4]; + const int n = backtrace(frames, PROF_CALLERS + 4); + int i, at = -1, k; + + /* Find the interrupted IP in the walk; its successors are the callers. */ + for (i = 0; i < n; i++) + if ((uintptr_t)frames[i] == g_samples[slot].ip) { + at = i; + break; + } + for (k = 0; k < PROF_CALLERS; k++) + g_samples[slot].caller[k] = + at >= 0 && at + 1 + k < n ? (uintptr_t)frames[at + 1 + k] - 1u : 0u; + } +} + +/** The executable's load base, from the first r-xp line naming it (PIE). */ +static uintptr_t prof_image_base(void) +{ + char self[512], line[1024]; + ssize_t n = readlink("/proc/self/exe", self, sizeof(self) - 1u); + FILE *maps; + uintptr_t base = 0u; + + if (n <= 0) + return 0u; + self[n] = '\0'; + maps = fopen("/proc/self/maps", "r"); + if (!maps) + return 0u; + while (fgets(line, sizeof(line), maps)) + if (strstr(line, self)) { + base = (uintptr_t)strtoull(line, NULL, 16); + break; + } + fclose(maps); + return base; +} + +static int prof_compare(const void *a, const void *b) +{ + const prof_sample *x = a, *y = b; + int k; + + if (x->tid != y->tid) + return x->tid < y->tid ? -1 : 1; + if (x->ip != y->ip) + return x->ip < y->ip ? -1 : 1; + for (k = 0; k < PROF_CALLERS; k++) + if (x->caller[k] != y->caller[k]) + return x->caller[k] < y->caller[k] ? -1 : 1; + return 0; +} + +static uintptr_t prof_rel(uintptr_t address) +{ + return address >= g_base ? address - g_base : 0u; +} + +static void prof_dump(void) +{ + struct itimerval off; + uint32_t count = atomic_load(&g_count), i, run; + FILE *out; + + memset(&off, 0, sizeof(off)); + setitimer(ITIMER_PROF, &off, NULL); + if (count > PROF_MAX_SAMPLES) + count = PROF_MAX_SAMPLES; + out = fopen(g_path, "w"); + if (!out) + return; + qsort(g_samples, count, sizeof(g_samples[0]), prof_compare); + fprintf(out, "# base 0x%lx samples %u (1 ms of process CPU each)\n", (unsigned long)g_base, + count); + for (i = 0; i < count; i = run) { + for (run = i; run < count && prof_compare(&g_samples[run], &g_samples[i]) == 0; run++) + ; + fprintf(out, "0x%lx %d %u 0x%lx 0x%lx 0x%lx 0x%lx\n", + (unsigned long)prof_rel(g_samples[i].ip), g_samples[i].tid, run - i, + (unsigned long)prof_rel(g_samples[i].caller[0]), + (unsigned long)prof_rel(g_samples[i].caller[1]), + (unsigned long)prof_rel(g_samples[i].caller[2]), + (unsigned long)prof_rel(g_samples[i].caller[3])); + } + fclose(out); +} + +void pn_sample_profiler_start(void) +{ + static int started; + struct sigaction action; + struct itimerval every; + + g_path = getenv("PN_PROFILE"); + if (started || !g_path || !*g_path) + return; + started = 1; + g_base = prof_image_base(); + { + void *warm[4]; + + (void)backtrace(warm, 4); /* loads libgcc's unwinder now, not inside a handler */ + } + /* `PN_PROFILE_ALL=1` records the whole process; otherwise only marked windows. */ + atomic_store(&g_window, getenv("PN_PROFILE_ALL") ? -1 : 0); + memset(&action, 0, sizeof(action)); + action.sa_sigaction = prof_on_signal; + action.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&action.sa_mask); + sigaction(SIGPROF, &action, NULL); + memset(&every, 0, sizeof(every)); + every.it_interval.tv_usec = 1000; + every.it_value.tv_usec = 1000; + /* Windowed: the timer runs only inside a window, so nothing else is ever interrupted + * (round 1 sampled the whole process and its text differed — an EINTR somewhere). */ + if (atomic_load(&g_window) == -1) + setitimer(ITIMER_PROF, &every, NULL); + atexit(prof_dump); + fprintf(stderr, "profile: sampling every 1 ms of CPU into %s\n", g_path); +} + +void pn_sample_profiler_window(int inside) +{ + struct itimerval every; + + if (!g_path || atomic_load(&g_window) == -1) + return; + memset(&every, 0, sizeof(every)); + if (inside) { + every.it_interval.tv_usec = 1000; + every.it_value.tv_usec = 1000; + } + atomic_store(&g_window, inside ? 1 : 0); + setitimer(ITIMER_PROF, &every, NULL); +} + +#else + +void pn_sample_profiler_start(void) +{ +} + +void pn_sample_profiler_window(int inside) +{ + (void)inside; +} + +#endif diff --git a/src/core/sample_profiler.h b/src/core/sample_profiler.h new file mode 100644 index 0000000..178eaf0 --- /dev/null +++ b/src/core/sample_profiler.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file sample_profiler.h + * @brief A self-contained SAMPLING profiler: SIGPROF every millisecond of process CPU time, + * the interrupted instruction pointer and thread counted, dumped at exit. + * + * WHY. The TT host's kernel refuses unprivileged `perf` (perf_event_paranoid 4), and a root + * run of the engine leaves the udrv claim files root-owned. This needs neither: + * `PN_PROFILE=` arms it, and the dump is ` ` + * lines plus the image base, resolved afterwards with `addr2line -f -e ` + * (tools/sample_profile_report.sh). + * + * @status Instrument only; off unless PN_PROFILE is set. + * @issues Linux/x86-64 only (reads RIP from the ucontext); a no-op elsewhere. + * @todo None + */ +#ifndef PN_SAMPLE_PROFILER_H +#define PN_SAMPLE_PROFILER_H + +/** Arm the profiler when `PN_PROFILE` names an output file. Safe to call more than once. */ +void pn_sample_profiler_start(void); + +/** Record only inside marked windows from the first call on (1 = enter, 0 = leave): the + * prefill, not the 38 s weight upload around it. */ +void pn_sample_profiler_window(int inside); + +#endif /* PN_SAMPLE_PROFILER_H */ diff --git a/src/core/sampling_modifiers.c b/src/core/sampling_modifiers.c new file mode 100644 index 0000000..d7b20c2 --- /dev/null +++ b/src/core/sampling_modifiers.c @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * OpenAI sampling controls that act on a logits row — see sampling_modifiers.h + * for the contract and the ordering rationale. + * + * Pure C, libc + libm only. No engine/model/CUDA deps, so the whole file is + * unit-tested against real logit rows without a forward pass. + */ + +#include "sampling_modifiers.h" + +#include + +void pn_apply_logit_bias(float *logits, int n_vocab, + const pn_logit_bias_entry *bias, int n) +{ + if (!logits || !bias || n <= 0) return; + for (int i = 0; i < n; i++) { + uint32_t id = bias[i].token_id; + if (id < (uint32_t)n_vocab) + logits[id] += bias[i].bias; + } +} + +void pn_apply_presence_frequency_penalties(float *logits, int n_vocab, + float presence_penalty, + float frequency_penalty, + const uint32_t *recent_tokens, + int n_recent) +{ + if (!logits) return; + if (presence_penalty == 0.0f && frequency_penalty == 0.0f) return; + if (!recent_tokens || n_recent <= 0) return; + + for (int i = 0; i < n_recent; i++) { + uint32_t tok = recent_tokens[i]; + if (tok >= (uint32_t)n_vocab) continue; + /* Every occurrence pays the frequency penalty; the FIRST occurrence in + * the window also pays the presence penalty. Scanning backwards for an + * earlier copy keeps this allocation-free — the window is repeat_last_n + * entries (64 by default), so the O(n²) is a few thousand compares, + * nothing beside a full-vocab softmax. */ + int seen_before = 0; + for (int j = 0; j < i; j++) { + if (recent_tokens[j] == tok) { seen_before = 1; break; } + } + logits[tok] -= frequency_penalty; + if (!seen_before) logits[tok] -= presence_penalty; + } +} + +uint64_t pn_seed_to_rng_state(uint64_t seed) +{ + uint64_t z = seed + 0x9E3779B97F4A7C15ULL; + z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL; + z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL; + z ^= z >> 31; + return z ? z : 0x9E3779B97F4A7C15ULL; +} + +float pn_seeded_rng_next_float(uint64_t *state) +{ + if (!state) return 0.0f; + uint64_t x = *state; + x ^= x >> 12; x ^= x << 25; x ^= x >> 27; + *state = x; + /* Top 24 bits of the scrambled word → a float in [0,1) with no modulo bias + * and no rounding to exactly 1.0f. */ + return (float)((x * 0x2545F4914F6CDD1DULL) >> 40) / 16777216.0f; +} + +int pn_logprobs_extract(const float *logits, int n_vocab, uint32_t chosen, + int n_top, float *chosen_out, + uint32_t *top_ids_out, float *top_lp_out) +{ + if (!logits || n_vocab <= 0) return 0; + if (n_top < 0) n_top = 0; + if (n_top > PN_LOGPROBS_MAX_TOP) n_top = PN_LOGPROBS_MAX_TOP; + + float max_logit = logits[0]; + for (int i = 1; i < n_vocab; i++) + if (logits[i] > max_logit) max_logit = logits[i]; + float sum = 0.0f; + for (int i = 0; i < n_vocab; i++) sum += expf(logits[i] - max_logit); + /* log Z, so logprob(t) == logits[t] - log_z. */ + float log_z = max_logit + logf(sum); + + if (chosen_out) + *chosen_out = ((int)chosen < n_vocab) ? logits[chosen] - log_z : 0.0f; + + if (n_top == 0 || !top_ids_out || !top_lp_out) return 0; + + /* Bounded insertion sort. The `<= worst` early-out makes this ~O(n_vocab) + * in practice and n_top is capped at 20, so a full 150k-vocab row costs a + * single linear scan plus a handful of shifts. */ + int n_kept = 0; + float worst = 0.0f; + for (int i = 0; i < n_vocab; i++) { + if (n_kept == n_top && logits[i] <= worst) continue; + int pos = (n_kept < n_top) ? n_kept : n_top - 1; + while (pos > 0 && top_lp_out[pos - 1] < logits[i]) { + top_lp_out[pos] = top_lp_out[pos - 1]; + top_ids_out[pos] = top_ids_out[pos - 1]; + pos--; + } + top_lp_out[pos] = logits[i]; + top_ids_out[pos] = (uint32_t)i; + if (n_kept < n_top) n_kept++; + worst = top_lp_out[n_kept - 1]; + } + for (int i = 0; i < n_kept; i++) top_lp_out[i] -= log_z; + return n_kept; +} diff --git a/src/core/sampling_modifiers.h b/src/core/sampling_modifiers.h new file mode 100644 index 0000000..9d589c4 --- /dev/null +++ b/src/core/sampling_modifiers.h @@ -0,0 +1,123 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file sampling_modifiers.h + * @brief The OpenAI sampling controls that act on a logits row: `logit_bias`, + * `presence_penalty` / `frequency_penalty`, the per-request `seed` RNG, + * and `logprobs`. + * + * These are pure functions over a float array plus a recent-token window — no + * model, no tokenizer, no engine state — so they live apart from the sampler + * that calls them (engine.c) and are unit-tested directly + * (tests/test_sampling_modifiers.c) instead of through a full forward pass. + * That separation is deliberate: the last bug in this area (a repetition penalty + * applied on the wrong side of the greedy branch) was an ORDERING bug that a + * test over the real logits row would have caught immediately. + * + * @status Complete: bias, presence/frequency penalties, seeded RNG and logprob + * extraction all implemented and tested. + * @issues None + * @todo None + */ + +#ifndef PN_SAMPLING_MODIFIERS_H +#define PN_SAMPLING_MODIFIERS_H + +#include + +#include "../../include/pantheon.h" /* pn_logit_bias_entry, PN_LOGPROBS_MAX_TOP */ + +/** + * Add each entry's bias to its token's logit, in place. + * + * Runs FIRST in the sampling pipeline — before penalties, temperature and + * top-k/top-p — because OpenAI defines `logit_bias` as a modification of the + * model's own logits, and because a -100 bias must be able to veto a token + * regardless of how the rest of the pipeline would have scored it. Entries + * whose id is out of range are skipped. No-op when `bias` is NULL or `n` <= 0. + * + * @param logits [n_vocab] logits row, modified in place + * @param n_vocab vocabulary size + * @param bias borrowed id→bias array (may be NULL) + * @param n number of entries + * + * Thread-safe: yes (operates only on the caller's buffers) + */ +void pn_apply_logit_bias(float *logits, int n_vocab, + const pn_logit_bias_entry *bias, int n); + +/** + * Apply OpenAI's presence and frequency penalties over a recent-token window: + * + * logit[t] -= frequency_penalty * occurrences(t in window) + * logit[t] -= presence_penalty * (occurrences(t in window) > 0) + * + * No-op when both penalties are 0 (the default) or the window is empty, so a + * request that does not ask for them leaves the logits untouched. + * + * ORDERING CONTRACT: unlike the server's default `repeat_penalty`, these are + * only ever non-zero because a client asked, and they are a deterministic + * function of the already-generated prefix — so the caller applies them on the + * greedy (temperature<=0) path as well, and greedy stays reproducible. + * + * @param logits [n_vocab] logits row, modified in place + * @param n_vocab vocabulary size + * @param presence_penalty OpenAI `presence_penalty` + * @param frequency_penalty OpenAI `frequency_penalty` + * @param recent_tokens borrowed window of recently emitted ids (may be NULL) + * @param n_recent window length + * + * Thread-safe: yes + */ +void pn_apply_presence_frequency_penalties(float *logits, int n_vocab, + float presence_penalty, + float frequency_penalty, + const uint32_t *recent_tokens, + int n_recent); + +/** + * Diffuse a client-supplied `seed` into a usable RNG state (splitmix64). + * + * The common seeds are tiny (0, 1, 42) and the xorshift64* stream mixes poorly + * out of a low-entropy state, which would make neighbouring seeds produce + * visibly correlated first tokens. Never returns 0 (xorshift's fixed point). + * + * Thread-safe: yes (pure) + */ +uint64_t pn_seed_to_rng_state(uint64_t seed); + +/** + * Draw the next uniform value in [0, 1) from a per-request xorshift64* stream, + * advancing `*state` in place. A per-request stream is what makes `seed` + * meaningful at all: the process-global rand() is shared by every connection + * thread and by the batched driver's per-slot loop, so it can never be + * deterministic for one request. + * + * Thread-safe: yes, provided each caller owns its own `state`. + */ +float pn_seeded_rng_next_float(uint64_t *state); + +/** + * Log-softmax a logits row and extract the OpenAI `logprobs` view of it. + * + * The caller must pass the RAW logits — the model's own distribution — not a row + * that temperature, penalties, top-k/top-p or a grammar mask have already + * rewritten; that is what the OpenAI field means. + * + * @param logits [n_vocab] RAW logits (NOT modified) + * @param n_vocab vocabulary size + * @param chosen the token that was actually sampled + * @param n_top how many alternatives to report (clamped to + * [0, PN_LOGPROBS_MAX_TOP]) + * @param chosen_out receives logprob(chosen) (may be NULL) + * @param top_ids_out receives the top ids, descending (>= n_top capacity) + * @param top_lp_out receives their logprobs, descending + * @return how many alternatives were written + * + * Thread-safe: yes + */ +int pn_logprobs_extract(const float *logits, int n_vocab, uint32_t chosen, + int n_top, float *chosen_out, + uint32_t *top_ids_out, float *top_lp_out); + +#endif /* PN_SAMPLING_MODIFIERS_H */ diff --git a/src/core/session_snapshot.c b/src/core/session_snapshot.c new file mode 100644 index 0000000..5ae1456 --- /dev/null +++ b/src/core/session_snapshot.c @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_snapshot.c + * @brief Family selection for session snapshot/restore — see session_snapshot.h + * for the contract and for why the two families are not interchangeable. + * + * Every function here is a two-way switch and nothing else. The rule the switch + * enforces: an architecture is routed BY NAME, and an architecture nobody has + * written a family for is a refusal. Falling through to "probably llama" would + * hand an RWKVX recurrent state to a KV-cache reader. + * + * @status Complete + * @issues None + * @todo None + */ + +#include "session_snapshot.h" + +#include "../models/llama.h" +#include "../models/qwen35moe.h" + +/* Which snapshot implementation owns this architecture. */ +typedef enum { + SNAP_FAMILY_NONE = 0, + SNAP_FAMILY_LLAMA_KV, /* pure causal attention, fp32 KV only */ + SNAP_FAMILY_QWEN35_DUAL, /* SSM recurrent state + attention KV */ +} snap_family; + +/* The llama-family list is deliberately ENUMERATED rather than expressed as + * "everything except the three hybrids". pn_state_new() allocates a + * pn_llama_kv_cache for every arch outside RWKVX/QWEN35MOE/BONSAI today, so a + * default-allow would be correct today and wrong the first time a fourth state + * shape lands — and it would be wrong by writing a KV reader over a state that + * is not one. This is the same list lcp_rewind_supported() keeps in + * prefix_cache.c, for the same reason: both statements are "this arch's whole + * per-session state is a plain causal KV cache". */ +static snap_family family_for(pn_arch arch) +{ + switch (arch) { + case PN_ARCH_LLAMA: + case PN_ARCH_QWEN2: + case PN_ARCH_QWEN3: + case PN_ARCH_PHI: + case PN_ARCH_MISTRAL: + case PN_ARCH_MIXTRAL: + return SNAP_FAMILY_LLAMA_KV; + case PN_ARCH_QWEN35MOE: + case PN_ARCH_BONSAI: + return SNAP_FAMILY_QWEN35_DUAL; + default: + return SNAP_FAMILY_NONE; /* RWKVX and anything unknown */ + } +} + +int pn_session_snapshot_supported(pn_arch arch) +{ + return family_for(arch) != SNAP_FAMILY_NONE; +} + +void *pn_session_snapshot_capture(pn_arch arch, const void *kv_cache, int pos) +{ + switch (family_for(arch)) { + case SNAP_FAMILY_LLAMA_KV: + return pn_llama_kv_snapshot(kv_cache, pos); + case SNAP_FAMILY_QWEN35_DUAL: + return pn_qwen35moe_state_snapshot(kv_cache, pos); + default: + return NULL; + } +} + +int pn_session_snapshot_restore(pn_arch arch, void *kv_cache, const void *snap) +{ + switch (family_for(arch)) { + case SNAP_FAMILY_LLAMA_KV: + return pn_llama_kv_restore(kv_cache, snap); + case SNAP_FAMILY_QWEN35_DUAL: + return pn_qwen35moe_state_restore(kv_cache, snap); + default: + return -1; + } +} + +int pn_session_snapshot_pos(pn_arch arch, const void *snap) +{ + switch (family_for(arch)) { + case SNAP_FAMILY_LLAMA_KV: + return pn_llama_kv_snapshot_pos(snap); + case SNAP_FAMILY_QWEN35_DUAL: + return pn_qwen35moe_state_snapshot_pos(snap); + default: + return -1; + } +} + +void pn_session_snapshot_free(pn_arch arch, void *snap) +{ + switch (family_for(arch)) { + case SNAP_FAMILY_LLAMA_KV: + pn_llama_kv_snapshot_free(snap); + return; + case SNAP_FAMILY_QWEN35_DUAL: + pn_qwen35moe_state_snapshot_free(snap); + return; + default: + return; + } +} + +int pn_session_snapshot_serialize(pn_arch arch, const void *snap, + uint64_t weights_version, + unsigned char **bytes_out, size_t *len_out) +{ + switch (family_for(arch)) { + case SNAP_FAMILY_LLAMA_KV: + return pn_llama_kv_snap_serialize(snap, weights_version, bytes_out, + len_out); + case SNAP_FAMILY_QWEN35_DUAL: + return pn_qwen35moe_snap_serialize(snap, weights_version, bytes_out, + len_out); + default: + return -1; + } +} + +void *pn_session_snapshot_deserialize(pn_arch arch, const unsigned char *bytes, + size_t len, uint64_t *weights_version_out) +{ + switch (family_for(arch)) { + case SNAP_FAMILY_LLAMA_KV: + return pn_llama_kv_snap_deserialize(bytes, len, weights_version_out); + case SNAP_FAMILY_QWEN35_DUAL: + return pn_qwen35moe_snap_deserialize(bytes, len, weights_version_out); + default: + return NULL; + } +} diff --git a/src/core/session_snapshot.h b/src/core/session_snapshot.h new file mode 100644 index 0000000..3bf995f --- /dev/null +++ b/src/core/session_snapshot.h @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_snapshot.h + * @brief The ONE seam between "a session's state" and the family of snapshot + * code that can capture it. Keyed on `pn_arch` and nothing else, so a + * caller (the prefix cache, the session facade, the export/restore HTTP + * handlers) never names a model family. + * + * Two families exist today and they are not interchangeable: + * + * - QWEN35MOE / BONSAI — a hybrid DUAL state: per-layer SSM recurrent + * buffers plus attention KV (fp32 or q4 with fp16 sink/window + * shadows). qwen35moe_state_snapshot.c, blob magic "PNS1". + * - LLAMA / QWEN2 / QWEN3 / PHI / MISTRAL / MIXTRAL — pure causal + * attention, so the whole per-session state is an fp32 KV cache. + * llama_state_snapshot.c, blob magic "PNLK". + * + * The blobs carry different magics, so a blob of one family fed to the + * other is refused at parse, not misread. `pn_session_snapshot_*` never + * guesses: the arch selects the family, and an unsupported arch (RWKVX) + * is a refusal. + * + * @status Complete. The llama-family arm is what makes session export/restore + * reachable on the Tenstorrent backend, which serves exactly that + * family; before it, `/admin/session/export` answered 404 + * `session_not_found` for a live, generating session because the only + * predicate in the path asked "is this qwen35moe?". + * @issues None + * @todo None + */ + +#ifndef PN_SESSION_SNAPSHOT_H +#define PN_SESSION_SNAPSHOT_H + +#include +#include + +#include "engine.h" /* pn_arch */ + +/** + * Can a session on this architecture be captured at all? + * + * @param arch the model's architecture + * @return 1 if capture/restore/serialize are implemented, 0 otherwise + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +int pn_session_snapshot_supported(pn_arch arch); + +/** + * Capture a session's state at `pos` into an opaque, family-tagged snapshot. + * + * @param arch the model's architecture (selects the family) + * @param kv_cache pn_state::kv_cache for that model + * @param pos committed token count to capture + * @return Opaque snapshot (free with pn_session_snapshot_free), or NULL + * + * Thread-safe: no — the caller must hold whatever lock stops a turn mutating + * the state (gen_lock on the single-user path, the scheduler's on the batched) + * Reentrant: yes Signal-safe: no + */ +void *pn_session_snapshot_capture(pn_arch arch, const void *kv_cache, int pos); + +/** + * Write a snapshot back into a live state. Dims are validated against the + * destination; a mismatch is a refusal, never a partial write. + * + * @return 0 on success, -1 if the snapshot does not fit + * + * Thread-safe: no (same discipline as capture) Reentrant: yes Signal-safe: no + */ +int pn_session_snapshot_restore(pn_arch arch, void *kv_cache, const void *snap); + +/** The position a snapshot was taken at, or -1. */ +int pn_session_snapshot_pos(pn_arch arch, const void *snap); + +/** Free a snapshot of this family. NULL-safe. */ +void pn_session_snapshot_free(pn_arch arch, void *snap); + +/** + * Serialize a snapshot into one contiguous little-endian blob. + * + * @param weights_version opaque model-identity tag, echoed in the header and + * validated by the restore handler before any state is + * touched + * @return 0 on success, -1 on failure (`*bytes_out` untouched on failure) + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +int pn_session_snapshot_serialize(pn_arch arch, const void *snap, + uint64_t weights_version, + unsigned char **bytes_out, size_t *len_out); + +/** + * Parse a blob back into an opaque snapshot of this family. + * + * @return Opaque snapshot, or NULL on any validation failure (including a blob + * whose magic belongs to the OTHER family). + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +void *pn_session_snapshot_deserialize(pn_arch arch, const unsigned char *bytes, + size_t len, uint64_t *weights_version_out); + +#endif /* PN_SESSION_SNAPSHOT_H */ diff --git a/src/formats/gguf.c b/src/formats/gguf.c new file mode 100644 index 0000000..3962063 --- /dev/null +++ b/src/formats/gguf.c @@ -0,0 +1,471 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "gguf.h" +#include +#include +#include +#include +#include +#include +#include + +/* ============================================================ + * Cursor-based reader helpers + * All reads are little-endian (GGUF spec). + * ============================================================ */ + +typedef struct { + const uint8_t *base; + const uint8_t *cur; + const uint8_t *end; +} cursor_t; + +#define CHECK_BOUNDS(c, n) do { \ + if ((c)->cur + (n) > (c)->end) { \ + fprintf(stderr, "gguf: unexpected end of file at offset %zu (need %zu bytes)\n", \ + (size_t)((c)->cur - (c)->base), (size_t)(n)); \ + goto fail; \ + } \ +} while(0) + +static inline uint8_t read_u8(cursor_t *c) { + uint8_t v = c->cur[0]; + c->cur += 1; + return v; +} + +static inline uint16_t read_u16(cursor_t *c) { + uint16_t v; + memcpy(&v, c->cur, 2); + c->cur += 2; + return v; +} + +static inline uint32_t read_u32(cursor_t *c) { + uint32_t v; + memcpy(&v, c->cur, 4); + c->cur += 4; + return v; +} + +static inline uint64_t read_u64(cursor_t *c) { + uint64_t v; + memcpy(&v, c->cur, 8); + c->cur += 8; + return v; +} + +static inline int8_t read_i8(cursor_t *c) { return (int8_t)read_u8(c); } +static inline int16_t read_i16(cursor_t *c) { return (int16_t)read_u16(c); } +static inline int32_t read_i32(cursor_t *c) { return (int32_t)read_u32(c); } +static inline int64_t read_i64(cursor_t *c) { return (int64_t)read_u64(c); } + +static inline float read_f32(cursor_t *c) { + float v; + memcpy(&v, c->cur, 4); + c->cur += 4; + return v; +} + +static inline double read_f64(cursor_t *c) { + double v; + memcpy(&v, c->cur, 8); + c->cur += 8; + return v; +} + +/* Read a GGUF string: 8-byte length + data. Returns malloc'd null-terminated copy. */ +static char* read_string(cursor_t *c, uint64_t *len_out) { + uint64_t len = read_u64(c); + if (len_out) *len_out = len; + if (c->cur + len > c->end) return NULL; + char *s = malloc(len + 1); + if (!s) return NULL; + memcpy(s, c->cur, len); + s[len] = '\0'; + c->cur += len; + return s; +} + +/* Size of a single GGUF metadata type value (excluding arrays/strings) */ +static size_t gguf_type_size(pn_gguf_type type) { + switch (type) { + case GGUF_TYPE_UINT8: return 1; + case GGUF_TYPE_INT8: return 1; + case GGUF_TYPE_UINT16: return 2; + case GGUF_TYPE_INT16: return 2; + case GGUF_TYPE_UINT32: return 4; + case GGUF_TYPE_INT32: return 4; + case GGUF_TYPE_FLOAT32: return 4; + case GGUF_TYPE_BOOL: return 1; + case GGUF_TYPE_UINT64: return 8; + case GGUF_TYPE_INT64: return 8; + case GGUF_TYPE_FLOAT64: return 8; + default: return 0; + } +} + +/* Skip over an array value in the cursor (used during metadata parsing) */ +static int skip_array(cursor_t *c) { + if (c->cur + 12 > c->end) return -1; /* type(4) + count(8) */ + pn_gguf_type elem_type = (pn_gguf_type)read_u32(c); + uint64_t count = read_u64(c); + + if (elem_type == GGUF_TYPE_STRING) { + for (uint64_t i = 0; i < count; i++) { + if (c->cur + 8 > c->end) return -1; + uint64_t slen = read_u64(c); + if (c->cur + slen > c->end) return -1; + c->cur += slen; + } + } else { + size_t esz = gguf_type_size(elem_type); + if (esz == 0) return -1; + size_t total = esz * count; + if (c->cur + total > c->end) return -1; + c->cur += total; + } + return 0; +} + +/* ============================================================ + * pn_gguf_open — parse GGUF header, metadata, tensor info + * ============================================================ */ + +pn_gguf_file* pn_gguf_open(const char *path) { + if (!path) return NULL; + + int fd = open(path, O_RDONLY); + if (fd < 0) { + fprintf(stderr, "gguf: cannot open %s\n", path); + return NULL; + } + + struct stat st; + if (fstat(fd, &st) < 0) { + close(fd); + return NULL; + } + size_t file_size = (size_t)st.st_size; + + void *mmap_base = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (mmap_base == MAP_FAILED) { + fprintf(stderr, "gguf: mmap failed for %s\n", path); + close(fd); + return NULL; + } + + cursor_t c = { + .base = (const uint8_t *)mmap_base, + .cur = (const uint8_t *)mmap_base, + .end = (const uint8_t *)mmap_base + file_size, + }; + + /* 1. Magic "GGUF" */ + CHECK_BOUNDS(&c, 4); + if (memcmp(c.cur, "GGUF", 4) != 0) { + fprintf(stderr, "gguf: bad magic in %s\n", path); + goto fail; + } + c.cur += 4; + + /* 2. Version (expect v3) */ + CHECK_BOUNDS(&c, 4); + uint32_t version = read_u32(&c); + if (version < 2 || version > 3) { + fprintf(stderr, "gguf: unsupported version %u in %s\n", version, path); + goto fail; + } + + /* 3. Tensor count and metadata KV count */ + CHECK_BOUNDS(&c, 16); + uint64_t n_tensors = read_u64(&c); + uint64_t n_kv = read_u64(&c); + + /* Allocate file handle */ + pn_gguf_file *file = calloc(1, sizeof(pn_gguf_file)); + if (!file) goto fail; + + file->version = version; + file->n_tensors = n_tensors; + file->n_kv = n_kv; + file->mmap_base = mmap_base; + file->mmap_size = file_size; + file->fd = fd; + + /* 4. Parse metadata KV pairs */ + file->kv = calloc(n_kv, sizeof(pn_gguf_kv)); + if (!file->kv && n_kv > 0) goto fail_file; + + for (uint64_t i = 0; i < n_kv; i++) { + /* Key string */ + CHECK_BOUNDS(&c, 8); + char *key = read_string(&c, &file->kv[i].key.len); + if (!key) goto fail_file; + file->kv[i].key.data = key; + + /* Value type */ + CHECK_BOUNDS(&c, 4); + file->kv[i].type = (pn_gguf_type)read_u32(&c); + + /* Value — read or skip based on type */ + switch (file->kv[i].type) { + case GGUF_TYPE_UINT8: CHECK_BOUNDS(&c, 1); file->kv[i].value.u8 = read_u8(&c); break; + case GGUF_TYPE_INT8: CHECK_BOUNDS(&c, 1); file->kv[i].value.i8 = read_i8(&c); break; + case GGUF_TYPE_UINT16: CHECK_BOUNDS(&c, 2); file->kv[i].value.u16 = read_u16(&c); break; + case GGUF_TYPE_INT16: CHECK_BOUNDS(&c, 2); file->kv[i].value.i16 = read_i16(&c); break; + case GGUF_TYPE_UINT32: CHECK_BOUNDS(&c, 4); file->kv[i].value.u32 = read_u32(&c); break; + case GGUF_TYPE_INT32: CHECK_BOUNDS(&c, 4); file->kv[i].value.i32 = read_i32(&c); break; + case GGUF_TYPE_FLOAT32: CHECK_BOUNDS(&c, 4); file->kv[i].value.f32 = read_f32(&c); break; + case GGUF_TYPE_BOOL: CHECK_BOUNDS(&c, 1); file->kv[i].value.b = read_u8(&c) != 0; break; + case GGUF_TYPE_UINT64: CHECK_BOUNDS(&c, 8); file->kv[i].value.u64 = read_u64(&c); break; + case GGUF_TYPE_INT64: CHECK_BOUNDS(&c, 8); file->kv[i].value.i64 = read_i64(&c); break; + case GGUF_TYPE_FLOAT64: CHECK_BOUNDS(&c, 8); file->kv[i].value.f64 = read_f64(&c); break; + case GGUF_TYPE_STRING: { + CHECK_BOUNDS(&c, 8); + char *val = read_string(&c, &file->kv[i].value.str.len); + if (!val) goto fail_file; + file->kv[i].value.str.data = val; + break; + } + case GGUF_TYPE_ARRAY: { + /* Store pointer to array data for later parsing */ + const uint8_t *arr_start = c.cur; + if (skip_array(&c) < 0) goto fail_file; + /* Store raw array pointer in the string field (reinterpreted later) */ + file->kv[i].value.str.data = (char *)arr_start; + file->kv[i].value.str.len = (uint64_t)(c.cur - arr_start); + break; + } + default: + fprintf(stderr, "gguf: unknown metadata type %d for key '%s'\n", + file->kv[i].type, key); + goto fail_file; + } + } + + /* 5. Parse tensor info entries */ + file->tensors = calloc(n_tensors, sizeof(pn_gguf_tensor_info)); + if (!file->tensors && n_tensors > 0) goto fail_file; + + for (uint64_t i = 0; i < n_tensors; i++) { + /* Name */ + CHECK_BOUNDS(&c, 8); + char *name = read_string(&c, &file->tensors[i].name.len); + if (!name) goto fail_file; + file->tensors[i].name.data = name; + + /* Number of dimensions */ + CHECK_BOUNDS(&c, 4); + file->tensors[i].n_dims = read_u32(&c); + if (file->tensors[i].n_dims > 4) { + fprintf(stderr, "gguf: tensor '%s' has %u dims (max 4)\n", + name, file->tensors[i].n_dims); + goto fail_file; + } + + /* Dimension sizes */ + CHECK_BOUNDS(&c, file->tensors[i].n_dims * 8); + for (uint32_t d = 0; d < file->tensors[i].n_dims; d++) { + file->tensors[i].dims[d] = read_u64(&c); + } + + /* Quantization type */ + CHECK_BOUNDS(&c, 4); + file->tensors[i].type = (pn_gguf_quant)read_u32(&c); + + /* Offset within tensor data section */ + CHECK_BOUNDS(&c, 8); + file->tensors[i].offset = read_u64(&c); + } + + /* 6. Calculate data section start (align to GGUF_DEFAULT_ALIGNMENT from file start) */ + size_t header_end = (size_t)(c.cur - c.base); + size_t alignment = 32; /* GGUF default alignment (GGUF_DEFAULT_ALIGNMENT = 32) */ + + /* Check for custom alignment in metadata */ + pn_gguf_kv *align_kv = pn_gguf_find_kv(file, "general.alignment"); + if (align_kv && align_kv->type == GGUF_TYPE_UINT32) { + alignment = align_kv->value.u32; + } + + size_t data_offset = (header_end + alignment - 1) & ~(alignment - 1); + file->data = (uint8_t *)mmap_base + data_offset; + file->data_size = file_size - data_offset; + + fprintf(stderr, "gguf: opened %s (v%u, %llu tensors, %llu metadata)\n", + path, version, (unsigned long long)n_tensors, (unsigned long long)n_kv); + + return file; + +fail_file: + pn_gguf_close(file); + return NULL; + +fail: + munmap(mmap_base, file_size); + close(fd); + return NULL; +} + +/* ============================================================ + * Close + * ============================================================ */ + +void pn_gguf_close(pn_gguf_file *file) { + if (!file) return; + + /* Free metadata key strings and string values */ + if (file->kv) { + for (uint64_t i = 0; i < file->n_kv; i++) { + free(file->kv[i].key.data); + if (file->kv[i].type == GGUF_TYPE_STRING) { + free(file->kv[i].value.str.data); + } + /* Array values point into mmap — don't free */ + } + free(file->kv); + } + + /* Free tensor name strings */ + if (file->tensors) { + for (uint64_t i = 0; i < file->n_tensors; i++) { + free(file->tensors[i].name.data); + } + free(file->tensors); + } + + /* Unmap file */ + if (file->mmap_base) { + munmap(file->mmap_base, file->mmap_size); + } + if (file->fd >= 0) { + close(file->fd); + } + + free(file); +} + +/* ============================================================ + * Metadata lookup helpers + * ============================================================ */ + +pn_gguf_kv* pn_gguf_find_kv(pn_gguf_file *file, const char *key) { + if (!file || !key) return NULL; + for (uint64_t i = 0; i < file->n_kv; i++) { + if (file->kv[i].key.data && strcmp(file->kv[i].key.data, key) == 0) { + return &file->kv[i]; + } + } + return NULL; +} + +uint32_t pn_gguf_get_u32(pn_gguf_file *file, const char *key, uint32_t default_val) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv) return default_val; + if (kv->type == GGUF_TYPE_UINT32) return kv->value.u32; + if (kv->type == GGUF_TYPE_INT32 && kv->value.i32 >= 0) return (uint32_t)kv->value.i32; + return default_val; +} + +uint64_t pn_gguf_get_u64(pn_gguf_file *file, const char *key, uint64_t default_val) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv) return default_val; + if (kv->type == GGUF_TYPE_UINT64) return kv->value.u64; + if (kv->type == GGUF_TYPE_UINT32) return kv->value.u32; + if (kv->type == GGUF_TYPE_INT32 && kv->value.i32 >= 0) return (uint64_t)kv->value.i32; + return default_val; +} + +int32_t pn_gguf_get_i32(pn_gguf_file *file, const char *key, int32_t default_val) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv) return default_val; + if (kv->type == GGUF_TYPE_INT32) return kv->value.i32; + if (kv->type == GGUF_TYPE_UINT32 && kv->value.u32 <= INT32_MAX) return (int32_t)kv->value.u32; + return default_val; +} + +float pn_gguf_get_f32(pn_gguf_file *file, const char *key, float default_val) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv || kv->type != GGUF_TYPE_FLOAT32) return default_val; + return kv->value.f32; +} + +const char* pn_gguf_get_str(pn_gguf_file *file, const char *key) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv || kv->type != GGUF_TYPE_STRING) return NULL; + return kv->value.str.data; +} + +/* ============================================================ + * Array metadata access + * ============================================================ */ + +uint64_t pn_gguf_get_arr_n(pn_gguf_file *file, const char *key) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv || kv->type != GGUF_TYPE_ARRAY) return 0; + /* Array data pointer stored in str.data, parse element count */ + const uint8_t *p = (const uint8_t *)kv->value.str.data; + uint64_t count; + memcpy(&count, p + 4, 8); /* skip type(4), read count(8) */ + return count; +} + +pn_gguf_arr* pn_gguf_get_arr(pn_gguf_file *file, const char *key) { + pn_gguf_kv *kv = pn_gguf_find_kv(file, key); + if (!kv || kv->type != GGUF_TYPE_ARRAY) return NULL; + + const uint8_t *p = (const uint8_t *)kv->value.str.data; + pn_gguf_arr *arr = malloc(sizeof(pn_gguf_arr)); + if (!arr) return NULL; + + uint32_t elem_type; + memcpy(&elem_type, p, 4); + memcpy(&arr->n, p + 4, 8); + arr->elem_type = (pn_gguf_type)elem_type; + arr->data = (void *)(p + 12); /* past type(4) + count(8) */ + + return arr; +} + +const char* pn_gguf_arr_str(pn_gguf_arr *arr, uint64_t idx) { + if (!arr || arr->elem_type != GGUF_TYPE_STRING || idx >= arr->n) return NULL; + + /* Walk string array from start to find idx-th entry */ + const uint8_t *p = (const uint8_t *)arr->data; + for (uint64_t i = 0; i < idx; i++) { + uint64_t slen; + memcpy(&slen, p, 8); + p += 8 + slen; + } + + uint64_t slen; + memcpy(&slen, p, 8); + /* Strings in GGUF arrays are NOT null-terminated, but we return + * a pointer into the mmap'd data. The caller must handle length + * or we need to copy. For now return pointer (length available via slen). */ + /* Actually for BPE token extraction we need null-terminated copies. + * The caller should use the length. For convenience, check if next byte is \0. */ + return (const char *)(p + 8); +} + +/* ============================================================ + * Tensor lookup + * ============================================================ */ + +pn_gguf_tensor_info* pn_gguf_find_tensor(pn_gguf_file *file, const char *name) { + if (!file || !name) return NULL; + for (uint64_t i = 0; i < file->n_tensors; i++) { + if (file->tensors[i].name.data && strcmp(file->tensors[i].name.data, name) == 0) { + return &file->tensors[i]; + } + } + return NULL; +} + +void* pn_gguf_tensor_data(pn_gguf_file *file, const char *name) { + if (!file || !file->data || !name) return NULL; + pn_gguf_tensor_info *info = pn_gguf_find_tensor(file, name); + if (!info) return NULL; + return (char *)file->data + info->offset; +} diff --git a/src/formats/gguf.h b/src/formats/gguf.h new file mode 100644 index 0000000..639f664 --- /dev/null +++ b/src/formats/gguf.h @@ -0,0 +1,157 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_GGUF_H +#define PN_GGUF_H + +#include +#include +#include + +/* + * GGUF file format parser. + * + * GGUF layout: + * 1. Magic: "GGUF" (4 bytes) + * 2. Version: uint32 (v2 or v3) + * 3. Tensor count: uint64 + * 4. Metadata count: uint64 + * 5. Metadata entries: typed key-value pairs + * 6. Tensor info: name, dims, quant type, offset + * 7. Padding (32-byte aligned, overridable via general.alignment) + * 8. Tensor data (binary weights) + */ + +/* GGUF metadata value types */ +typedef enum { + GGUF_TYPE_UINT8 = 0, + GGUF_TYPE_INT8 = 1, + GGUF_TYPE_UINT16 = 2, + GGUF_TYPE_INT16 = 3, + GGUF_TYPE_UINT32 = 4, + GGUF_TYPE_INT32 = 5, + GGUF_TYPE_FLOAT32 = 6, + GGUF_TYPE_BOOL = 7, + GGUF_TYPE_STRING = 8, + GGUF_TYPE_ARRAY = 9, + GGUF_TYPE_UINT64 = 10, + GGUF_TYPE_INT64 = 11, + GGUF_TYPE_FLOAT64 = 12, +} pn_gguf_type; + +/* GGUF quantization types (subset — expand as needed) */ +typedef enum { + GGUF_QUANT_F32 = 0, + GGUF_QUANT_F16 = 1, + GGUF_QUANT_Q4_0 = 2, + GGUF_QUANT_Q4_1 = 3, + GGUF_QUANT_Q5_0 = 6, + GGUF_QUANT_Q5_1 = 7, + GGUF_QUANT_Q8_0 = 8, + GGUF_QUANT_Q8_1 = 9, + GGUF_QUANT_Q2_K = 10, + GGUF_QUANT_Q3_K = 11, + GGUF_QUANT_Q4_K = 12, + GGUF_QUANT_Q5_K = 13, + GGUF_QUANT_Q6_K = 14, + GGUF_QUANT_Q8_K = 15, + GGUF_QUANT_BF16 = 30, /* 1|8|7. The format the checkpoints actually ship in: same 2 bytes/weight as F16 but the fp32 exponent range, so widening is the top-16-bits reinterpret and no weight is re-rounded. */ + GGUF_QUANT_Q2_0_G128 = 142, /* PrismML Bonsai native ternary (g128 release): 128-elem blocks, fp16 group scale + 2-bit codes. NOTE: the shipped Q2_0_g128 pack tags tensors type 142 / general.file_type 141; the older g64 pack used id 42. The 34-byte block layout ([fp16 d][32 codes], 128 elems) is identical — only the type id differs. */ + PN_QUANT_TERNARY = 100, /* Packed 2-bit ternary with per-group float32 scales */ +} pn_gguf_quant; + +/* GGUF string (length-prefixed) */ +typedef struct { + uint64_t len; + char *data; +} pn_gguf_string; + +/* GGUF metadata key-value pair */ +typedef struct { + pn_gguf_string key; + pn_gguf_type type; + union { + uint8_t u8; + int8_t i8; + uint16_t u16; + int16_t i16; + uint32_t u32; + int32_t i32; + uint64_t u64; + int64_t i64; + float f32; + double f64; + bool b; + pn_gguf_string str; + } value; +} pn_gguf_kv; + +/* GGUF tensor info entry */ +typedef struct { + pn_gguf_string name; + uint32_t n_dims; + uint64_t dims[4]; + pn_gguf_quant type; + uint64_t offset; /* Offset from start of tensor data section */ +} pn_gguf_tensor_info; + +/* GGUF file handle */ +typedef struct { + uint32_t version; + uint64_t n_tensors; + uint64_t n_kv; + pn_gguf_kv *kv; /* Metadata array [n_kv] */ + pn_gguf_tensor_info *tensors; /* Tensor info array [n_tensors] */ + void *data; /* Pointer to tensor data section */ + size_t data_size; + void *mmap_base; /* mmap base pointer (for munmap) */ + size_t mmap_size; /* mmap size */ + int fd; /* File descriptor */ +} pn_gguf_file; + +/* Get metadata uint64 value. Returns default_val if not found. */ +uint64_t pn_gguf_get_u64(pn_gguf_file *file, const char *key, uint64_t default_val); + +/* Get metadata int32 value. Returns default_val if not found. */ +int32_t pn_gguf_get_i32(pn_gguf_file *file, const char *key, int32_t default_val); + +/* Get metadata array length. Returns 0 if not found or not an array. */ +uint64_t pn_gguf_get_arr_n(pn_gguf_file *file, const char *key); + +/* GGUF array metadata entry */ +typedef struct { + pn_gguf_type elem_type; + uint64_t n; + void *data; /* Raw pointer into mmap'd array data */ +} pn_gguf_arr; + +/* Get metadata array. Returns NULL if not found or not an array. */ +pn_gguf_arr* pn_gguf_get_arr(pn_gguf_file *file, const char *key); + +/* Get string from array at index. Returns NULL on error. */ +const char* pn_gguf_arr_str(pn_gguf_arr *arr, uint64_t idx); + +/* Open and parse a GGUF file (header + metadata + tensor info, mmap data) */ +pn_gguf_file* pn_gguf_open(const char *path); + +/* Close and free a GGUF file */ +void pn_gguf_close(pn_gguf_file *file); + +/* Look up a metadata value by key. Returns NULL if not found. */ +pn_gguf_kv* pn_gguf_find_kv(pn_gguf_file *file, const char *key); + +/* Get a metadata uint32 value. Returns default_val if not found. */ +uint32_t pn_gguf_get_u32(pn_gguf_file *file, const char *key, uint32_t default_val); + +/* Get a metadata float value. Returns default_val if not found. */ +float pn_gguf_get_f32(pn_gguf_file *file, const char *key, float default_val); + +/* Get a metadata string value. Returns NULL if not found. */ +const char* pn_gguf_get_str(pn_gguf_file *file, const char *key); + +/* Get pointer to tensor data by name. Returns NULL if not found. */ +void* pn_gguf_tensor_data(pn_gguf_file *file, const char *name); + +/* Get tensor info by name. Returns NULL if not found. */ +pn_gguf_tensor_info* pn_gguf_find_tensor(pn_gguf_file *file, const char *name); + +#endif /* PN_GGUF_H */ diff --git a/src/formats/gguf_dequant.c b/src/formats/gguf_dequant.c new file mode 100644 index 0000000..c71dc50 --- /dev/null +++ b/src/formats/gguf_dequant.c @@ -0,0 +1,443 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gguf_dequant.c + * @brief Dequantization of GGUF tensor blocks to float32, half conversions, size helpers. + * + * Rewritten from a behaviour specification and public GGUF format documentation; + * the implementer did not see the previous code. + * + * Bit-exactness under -ffast-math: every scale x quant product here is exactly + * representable in binary32, so grouping does not change results; Q4_K/Q5_K round once + * at the min subtraction. All math is float (no double, no explicit fma), and pure bit + * moves (F32/BF16 copy, half<->float) are integer-only so FTZ/DAZ cannot touch them. + * + * @status None + * @issues - Partial trailing blocks are skipped silently (tail of dst left untouched). + * - numel == 0 makes pn_dequantize fail; unsupported types via + * pn_gguf_dequantize_tensor print two lines. + * - Q4_1/Q5_0/Q5_1/Q8_1/Q2_K/Q8_K: block size known, bytes per block 0. + * - pn_float_to_half canonicalises NaNs (not a round trip with pn_half_to_float). + * - pn_gguf_dequantize_row: no bounds check, offset multiply can wrap. + * @todo None + */ +#include "gguf_dequant.h" + +#include +#include +#include +#include +#include +#include + +/* --------------------------------------------------------------------------- + * Bit helpers + * ------------------------------------------------------------------------- */ + +static inline float bits_to_float(uint32_t u) { + float f; + memcpy(&f, &u, sizeof f); + return f; +} + +static inline uint32_t float_to_bits(float f) { + uint32_t u; + memcpy(&u, &f, sizeof u); + return u; +} + +/* Little-endian 16-bit load from an arbitrarily aligned address. */ +static inline uint16_t load_u16(const uint8_t *p) { + uint16_t v; + memcpy(&v, p, sizeof v); + return v; +} + +/* --------------------------------------------------------------------------- + * Half precision + * ------------------------------------------------------------------------- */ + +float pn_half_to_float(uint16_t h) { + uint32_t sign = (uint32_t)(h & 0x8000u) << 16; + uint32_t exp = (h >> 10) & 0x1Fu; + uint32_t man = h & 0x3FFu; + + if (exp == 0) { + if (man == 0) return bits_to_float(sign); + /* Subnormal: value = man * 2^-24. Find the leading one at bit b (0..9). */ + uint32_t b = 9; + while (!(man & (1u << b))) b--; + uint32_t frac = (man & ((1u << b) - 1u)) << (23 - b); + return bits_to_float(sign | ((b + 103u) << 23) | frac); + } + if (exp == 31) return bits_to_float(sign | 0x7F800000u | (man << 13)); + return bits_to_float(sign | ((exp + 112u) << 23) | (man << 13)); +} + +uint16_t pn_float_to_half(float f) { + uint32_t x = float_to_bits(f); + uint16_t s16 = (uint16_t)((x >> 16) & 0x8000u); + uint32_t E = (x >> 23) & 0xFFu; + uint32_t M = x & 0x7FFFFFu; + int e = (int)E - 112; + + if (E == 0xFF) return (uint16_t)(s16 | 0x7C00u | (M ? 0x0200u : 0u)); + if (e >= 31) return (uint16_t)(s16 | 0x7C00u); + + if (e <= 0) { + if (e < -10) return s16; + uint32_t mm = M | 0x800000u; + uint32_t shift = (uint32_t)(14 - e); /* 14..24 */ + uint32_t q = mm >> shift; + uint32_t r = mm & ((1u << shift) - 1u); + uint32_t halfway = 1u << (shift - 1); + if (r > halfway || (r == halfway && (q & 1u))) q++; + return (uint16_t)(s16 | q); /* carry into 0x400 = min normal */ + } + + uint32_t q = M >> 13; + uint32_t r = M & 0x1FFFu; + if (r > 0x1000u || (r == 0x1000u && (q & 1u))) q++; + if (q == 0x400u) { + q = 0; + e += 1; + if (e >= 31) return (uint16_t)(s16 | 0x7C00u); + } + return (uint16_t)(s16 | ((uint32_t)e << 10) | q); +} + +/* --------------------------------------------------------------------------- + * Block decoders. Field offsets come from the public structs so the layouts live in + * one place; reads go through byte pointers so any alignment is fine. + * ------------------------------------------------------------------------- */ + +#define FIELD(blk, type, field) ((const uint8_t *)(blk) + offsetof(type, field)) + +void pn_dequantize_block_q4_0(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q4_0, d))); + const uint8_t *qs = FIELD(block, pn_block_q4_0, qs); + for (int j = 0; j < 16; j++) { + int lo = (qs[j] & 0x0F) - 8; + int hi = (qs[j] >> 4) - 8; + out[j] = d * (float)lo; + out[j + 16] = d * (float)hi; + } +} + +void pn_dequantize_block_q8_0(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q8_0, d))); + const int8_t *qs = (const int8_t *)FIELD(block, pn_block_q8_0, qs); + for (int j = 0; j < 32; j++) + out[j] = d * (float)qs[j]; +} + +void pn_dequantize_block_q2_0_g128(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q2_0_g128, d))); + const uint8_t *qs = FIELD(block, pn_block_q2_0_g128, qs); + for (int j = 0; j < 128; j++) { + int code = (qs[j / 4] >> (2 * (j % 4))) & 3; + out[j] = (float)(code - 1) * d; + } +} + +void pn_dequantize_block_q3_k(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q3_k, d))); + const uint8_t *hmask = FIELD(block, pn_block_q3_k, hmask); + const uint8_t *qs = FIELD(block, pn_block_q3_k, qs); + const uint8_t *S = FIELD(block, pn_block_q3_k, scales); + + /* 16 six-bit scales: low nibbles from S[0..7], top two bits from S[8..11]. */ + int8_t sc[16]; + for (int i = 0; i < 4; i++) { + uint8_t hi = S[8 + i]; + sc[i] = (int8_t)(((S[i] & 0x0F) | (((hi >> 0) & 3) << 4)) - 32); + sc[i + 4] = (int8_t)(((S[i + 4] & 0x0F) | (((hi >> 2) & 3) << 4)) - 32); + sc[i + 8] = (int8_t)(((S[i] >> 4) | (((hi >> 4) & 3) << 4)) - 32); + sc[i + 12] = (int8_t)(((S[i + 4] >> 4) | (((hi >> 6) & 3) << 4)) - 32); + } + + for (int h = 0; h < 2; h++) { + for (int j = 0; j < 4; j++) { + for (int g = 0; g < 2; g++) { /* 16-element group within l */ + const int k = 8 * h + 2 * j + g; + const float dl = d * (float)sc[k]; + for (int l = 16 * g; l < 16 * g + 16; l++) { + int low2 = (qs[32 * h + l] >> (2 * j)) & 3; + int hbit = (hmask[l] >> (4 * h + j)) & 1; + int q = low2 - (hbit ? 0 : 4); + out[128 * h + 32 * j + l] = dl * (float)q; + } + } + } + } +} + +/* 6-bit scale/min unpack shared by Q4_K and Q5_K. */ +static void unpack_scale_min(const uint8_t *S, uint8_t sc[8], uint8_t mn[8]) { + for (int k = 0; k < 4; k++) { + sc[k] = S[k] & 63; + mn[k] = S[k + 4] & 63; + } + for (int k = 4; k < 8; k++) { + sc[k] = (uint8_t)((S[k + 4] & 0x0F) | ((S[k - 4] >> 6) << 4)); + mn[k] = (uint8_t)((S[k + 4] >> 4) | ((S[k] >> 6) << 4)); + } +} + +void pn_dequantize_block_q4_k(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q4_k, d))); + const float dm = pn_half_to_float(load_u16(FIELD(block, pn_block_q4_k, dmin))); + const uint8_t *qs = FIELD(block, pn_block_q4_k, qs); + uint8_t sc[8], mn[8]; + unpack_scale_min(FIELD(block, pn_block_q4_k, scales), sc, mn); + + for (int p = 0; p < 4; p++) { + const float d0 = d * (float)sc[2 * p], m0 = dm * (float)mn[2 * p]; + const float d1 = d * (float)sc[2 * p + 1], m1 = dm * (float)mn[2 * p + 1]; + const uint8_t *c = qs + 32 * p; + float *o = out + 64 * p; + for (int l = 0; l < 32; l++) { + o[l] = d0 * (float)(c[l] & 0xF) - m0; + o[l + 32] = d1 * (float)(c[l] >> 4) - m1; + } + } +} + +void pn_dequantize_block_q5_k(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q5_k, d))); + const float dm = pn_half_to_float(load_u16(FIELD(block, pn_block_q5_k, dmin))); + const uint8_t *qh = FIELD(block, pn_block_q5_k, qh); + const uint8_t *qs = FIELD(block, pn_block_q5_k, qs); + uint8_t sc[8], mn[8]; + unpack_scale_min(FIELD(block, pn_block_q5_k, scales), sc, mn); + + for (int p = 0; p < 4; p++) { + const float d0 = d * (float)sc[2 * p], m0 = dm * (float)mn[2 * p]; + const float d1 = d * (float)sc[2 * p + 1], m1 = dm * (float)mn[2 * p + 1]; + const uint8_t *c = qs + 32 * p; + const uint8_t bit_lo = (uint8_t)(1u << (2 * p)); + const uint8_t bit_hi = (uint8_t)(1u << (2 * p + 1)); + float *o = out + 64 * p; + for (int l = 0; l < 32; l++) { + int qlo = (c[l] & 0xF) + ((qh[l] & bit_lo) ? 16 : 0); + int qhi = (c[l] >> 4) + ((qh[l] & bit_hi) ? 16 : 0); + o[l] = d0 * (float)qlo - m0; + o[l + 32] = d1 * (float)qhi - m1; + } + } +} + +void pn_dequantize_block_q6_k(const void *block, float *out) { + const float d = pn_half_to_float(load_u16(FIELD(block, pn_block_q6_k, d))); + const uint8_t *ql_all = FIELD(block, pn_block_q6_k, ql); + const uint8_t *qh_all = FIELD(block, pn_block_q6_k, qh); + const int8_t *sc_all = (const int8_t *)FIELD(block, pn_block_q6_k, scales); + + for (int h = 0; h < 2; h++) { + const uint8_t *QL = ql_all + 64 * h; + const uint8_t *QH = qh_all + 32 * h; + const int8_t *SC = sc_all + 8 * h; + float *o = out + 128 * h; + for (int l = 0; l < 32; l++) { + const int t = l / 16; + int q1 = (QL[l] & 0xF) | (((QH[l] >> 0) & 3) << 4); + int q2 = (QL[l + 32] & 0xF) | (((QH[l] >> 2) & 3) << 4); + int q3 = (QL[l] >> 4) | (((QH[l] >> 4) & 3) << 4); + int q4 = (QL[l + 32] >> 4) | (((QH[l] >> 6) & 3) << 4); + o[l] = d * (float)SC[0 + t] * (float)(q1 - 32); + o[l + 32] = d * (float)SC[2 + t] * (float)(q2 - 32); + o[l + 64] = d * (float)SC[4 + t] * (float)(q3 - 32); + o[l + 96] = d * (float)SC[6 + t] * (float)(q4 - 32); + } + } +} + +/* --------------------------------------------------------------------------- + * Bulk dispatch + * ------------------------------------------------------------------------- */ + +typedef void (*block_fn)(const void *block, float *out); + +static void run_blocks(const void *src, float *dst, size_t numel, + size_t elems, size_t bytes, block_fn fn) { + const uint8_t *p = (const uint8_t *)src; + size_t nb = numel / elems; + for (size_t i = 0; i < nb; i++) + fn(p + i * bytes, dst + i * elems); +} + +int pn_dequantize(const void *src, float *dst, size_t numel, pn_gguf_quant type) { + if (src == NULL || dst == NULL || numel == 0) return -1; + const uint8_t *bytes = (const uint8_t *)src; + + switch (type) { + case GGUF_QUANT_F32: + memcpy(dst, src, numel * sizeof(float)); + return 0; + case GGUF_QUANT_F16: + for (size_t i = 0; i < numel; i++) + dst[i] = pn_half_to_float(load_u16(bytes + 2 * i)); + return 0; + case GGUF_QUANT_BF16: + for (size_t i = 0; i < numel; i++) { + uint32_t w = (uint32_t)load_u16(bytes + 2 * i) << 16; + memcpy(&dst[i], &w, sizeof w); + } + return 0; + case GGUF_QUANT_Q4_0: + run_blocks(src, dst, numel, 32, sizeof(pn_block_q4_0), pn_dequantize_block_q4_0); + return 0; + case GGUF_QUANT_Q8_0: + run_blocks(src, dst, numel, 32, sizeof(pn_block_q8_0), pn_dequantize_block_q8_0); + return 0; + case GGUF_QUANT_Q2_0_G128: + run_blocks(src, dst, numel, 128, sizeof(pn_block_q2_0_g128), + pn_dequantize_block_q2_0_g128); + return 0; + case GGUF_QUANT_Q3_K: + run_blocks(src, dst, numel, 256, sizeof(pn_block_q3_k), pn_dequantize_block_q3_k); + return 0; + case GGUF_QUANT_Q4_K: + run_blocks(src, dst, numel, 256, sizeof(pn_block_q4_k), pn_dequantize_block_q4_k); + return 0; + case GGUF_QUANT_Q5_K: + run_blocks(src, dst, numel, 256, sizeof(pn_block_q5_k), pn_dequantize_block_q5_k); + return 0; + case GGUF_QUANT_Q6_K: + run_blocks(src, dst, numel, 256, sizeof(pn_block_q6_k), pn_dequantize_block_q6_k); + return 0; + default: + fprintf(stderr, "pn_dequantize: unsupported quant type %d\n", (int)type); + return -1; + } +} + +/* --------------------------------------------------------------------------- + * Size helpers and names + * ------------------------------------------------------------------------- */ + +int pn_gguf_block_size(pn_gguf_quant type) { + switch (type) { + case GGUF_QUANT_F32: + case GGUF_QUANT_F16: + case GGUF_QUANT_BF16: + return 1; + case GGUF_QUANT_Q4_0: + case GGUF_QUANT_Q4_1: + case GGUF_QUANT_Q5_0: + case GGUF_QUANT_Q5_1: + case GGUF_QUANT_Q8_0: + case GGUF_QUANT_Q8_1: + return 32; + case GGUF_QUANT_Q2_K: + case GGUF_QUANT_Q3_K: + case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: + case GGUF_QUANT_Q6_K: + case GGUF_QUANT_Q8_K: + return 256; + case GGUF_QUANT_Q2_0_G128: + return 128; + default: + return 0; + } +} + +size_t pn_gguf_bytes_per_block(pn_gguf_quant type) { + switch (type) { + case GGUF_QUANT_F32: return 4; + case GGUF_QUANT_F16: + case GGUF_QUANT_BF16: return 2; + case GGUF_QUANT_Q4_0: return sizeof(pn_block_q4_0); + case GGUF_QUANT_Q8_0: return sizeof(pn_block_q8_0); + case GGUF_QUANT_Q2_0_G128: return sizeof(pn_block_q2_0_g128); + case GGUF_QUANT_Q3_K: return sizeof(pn_block_q3_k); + case GGUF_QUANT_Q4_K: return sizeof(pn_block_q4_k); + case GGUF_QUANT_Q5_K: return sizeof(pn_block_q5_k); + case GGUF_QUANT_Q6_K: return sizeof(pn_block_q6_k); + default: return 0; /* includes the deliberately unsizeable types */ + } +} + +size_t pn_gguf_type_size(pn_gguf_quant type, size_t numel) { + int bs = pn_gguf_block_size(type); + if (bs <= 0) return 0; + size_t bpb = pn_gguf_bytes_per_block(type); + if (bpb == 0) return 0; + return (numel / (size_t)bs) * bpb; +} + +const char *pn_gguf_quant_name(pn_gguf_quant type, char *buf, size_t buf_len) { + if (buf == NULL || buf_len == 0) return ""; + const char *name = NULL; + switch (type) { + case GGUF_QUANT_F32: name = "F32"; break; + case GGUF_QUANT_F16: name = "F16"; break; + case GGUF_QUANT_BF16: name = "BF16"; break; + case GGUF_QUANT_Q4_0: name = "Q4_0"; break; + case GGUF_QUANT_Q4_1: name = "Q4_1"; break; + case GGUF_QUANT_Q5_0: name = "Q5_0"; break; + case GGUF_QUANT_Q5_1: name = "Q5_1"; break; + case GGUF_QUANT_Q8_0: name = "Q8_0"; break; + case GGUF_QUANT_Q8_1: name = "Q8_1"; break; + case GGUF_QUANT_Q2_K: name = "Q2_K"; break; + case GGUF_QUANT_Q3_K: name = "Q3_K"; break; + case GGUF_QUANT_Q4_K: name = "Q4_K"; break; + case GGUF_QUANT_Q5_K: name = "Q5_K"; break; + case GGUF_QUANT_Q6_K: name = "Q6_K"; break; + case GGUF_QUANT_Q8_K: name = "Q8_K"; break; + case GGUF_QUANT_Q2_0_G128: name = "Q2_0_G128"; break; + default: break; + } + if (name != NULL) + snprintf(buf, buf_len, "%s", name); + else + snprintf(buf, buf_len, "type%d", (int)type); + return buf; +} + +/* --------------------------------------------------------------------------- + * Tensor-level helpers + * ------------------------------------------------------------------------- */ + +float *pn_gguf_dequantize_tensor(pn_gguf_file *file, const char *name, size_t *numel_out) { + if (file == NULL || name == NULL) return NULL; + pn_gguf_tensor_info *info = pn_gguf_find_tensor(file, name); + if (info == NULL) { + fprintf(stderr, "pn_gguf_dequantize_tensor: tensor '%s' not found\n", name); + return NULL; + } + + size_t numel = 1; + for (uint32_t i = 0; i < info->n_dims; i++) + numel *= (size_t)info->dims[i]; + + const char *raw = (const char *)file->data + info->offset; + float *out = malloc(numel * sizeof(float)); + if (out == NULL) return NULL; + + if (pn_dequantize(raw, out, numel, info->type) < 0) { + fprintf(stderr, "pn_gguf_dequantize_tensor: unsupported type %d for '%s'\n", + (int)info->type, name); + free(out); + return NULL; + } + if (numel_out != NULL) *numel_out = numel; + return out; +} + +int pn_gguf_dequantize_row(pn_gguf_file *file, const char *name, + size_t row, size_t row_numel, float *dst) { + if (file == NULL || name == NULL || dst == NULL) return -1; + pn_gguf_tensor_info *info = pn_gguf_find_tensor(file, name); + if (info == NULL) return -1; + + int bs = pn_gguf_block_size(info->type); + if (bs <= 0) return -1; + size_t off = row * row_numel; + if (off % (size_t)bs != 0 || row_numel % (size_t)bs != 0) return -1; + + size_t byte_off = pn_gguf_type_size(info->type, off); + const char *src = (const char *)file->data + info->offset + byte_off; + return pn_dequantize(src, dst, row_numel, info->type); +} diff --git a/src/formats/gguf_dequant.h b/src/formats/gguf_dequant.h new file mode 100644 index 0000000..9c8c5d0 --- /dev/null +++ b/src/formats/gguf_dequant.h @@ -0,0 +1,150 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gguf_dequant.h + * @brief GGUF tensor block layouts, dequantization to float32, and size helpers. + * + * Rewritten from a behaviour specification and public GGUF format documentation; + * the implementer did not see the previous code. + * + * @status None + * @issues - pn_dequantize returns -1 for numel == 0 and silently skips a partial trailing + * block (returns 0, tail untouched); pn_gguf_dequantize_tensor then hands that + * tail back uninitialised. + * - An unsupported type through pn_gguf_dequantize_tensor prints two lines. + * - Q4_1/Q5_0/Q5_1/Q8_1/Q2_K/Q8_K have a block size but 0 bytes per block, so + * they size as 0 (deliberately unsizeable). + * - pn_float_to_half canonicalises NaNs; pn_half_to_float keeps NaN payloads. + * - pn_gguf_quant_name returns a literal "" (not buf) for a NULL/empty buffer. + * - pn_gguf_dequantize_row has no bounds check; its offset multiply can wrap. + * @todo None + */ +#ifndef PN_GGUF_DEQUANT_H +#define PN_GGUF_DEQUANT_H + +#include +#include +#include "gguf.h" + +/* --------------------------------------------------------------------------- + * Block layouts (ABI: field order and sizes are relied on across the tree). + * `d` / `dmin` are IEEE binary16 bit patterns. + * ------------------------------------------------------------------------- */ + +/** Q4_0: 32 elements, 4-bit codes offset by 8. */ +typedef struct { + uint16_t d; + uint8_t qs[16]; +} pn_block_q4_0; + +/** Q8_0: 32 elements, signed 8-bit codes. */ +typedef struct { + uint16_t d; + int8_t qs[32]; +} pn_block_q8_0; + +/** Q2_0_g128: 128 elements, 2-bit codes (0..3 -> -1..+2), low bits first. */ +typedef struct { + uint16_t d; + uint8_t qs[32]; +} pn_block_q2_0_g128; + +/** Q3_K: 256 elements, 2 low bits + 1 high bit per element, 16 six-bit scales. */ +typedef struct { + uint8_t hmask[32]; + uint8_t qs[64]; + uint8_t scales[12]; + uint16_t d; +} pn_block_q3_k; + +/** Q4_K: 256 elements, 4-bit codes, 8 sub-blocks with 6-bit scale and min. */ +typedef struct { + uint16_t d; + uint16_t dmin; + uint8_t scales[12]; + uint8_t qs[128]; +} pn_block_q4_k; + +/** Q5_K: as Q4_K plus one high bit per element in qh. */ +typedef struct { + uint16_t d; + uint16_t dmin; + uint8_t scales[12]; + uint8_t qh[32]; + uint8_t qs[128]; +} pn_block_q5_k; + +/** Q6_K: 256 elements, 4 low + 2 high bits per element, 16 signed 8-bit scales. */ +typedef struct { + uint8_t ql[128]; + uint8_t qh[64]; + int8_t scales[16]; + uint16_t d; +} pn_block_q6_k; + +_Static_assert(sizeof(pn_block_q4_0) == 18, "pn_block_q4_0 must be 18 bytes"); +_Static_assert(sizeof(pn_block_q8_0) == 34, "pn_block_q8_0 must be 34 bytes"); +_Static_assert(sizeof(pn_block_q2_0_g128) == 34, "pn_block_q2_0_g128 must be 34 bytes"); +_Static_assert(sizeof(pn_block_q3_k) == 110, "pn_block_q3_k must be 110 bytes"); +_Static_assert(sizeof(pn_block_q4_k) == 144, "pn_block_q4_k must be 144 bytes"); +_Static_assert(sizeof(pn_block_q5_k) == 176, "pn_block_q5_k must be 176 bytes"); +_Static_assert(sizeof(pn_block_q6_k) == 210, "pn_block_q6_k must be 210 bytes"); + +/** Buffer size that holds any pn_gguf_quant_name result. */ +#define PN_GGUF_QUANT_NAME_MAX 16 + +/** + * Dequantize `numel` elements of `type` from `src` into `dst`. + * F32/F16/BF16 convert every element. Block types convert floor(numel / block) whole + * blocks; a partial trailing block is left untouched in `dst`. + * Returns 0 on success; -1 (silently) if src/dst is NULL or numel is 0; -1 with a + * "pn_dequantize: unsupported quant type N" line on stderr for any other type. + */ +int pn_dequantize(const void *src, float *dst, size_t numel, pn_gguf_quant type); + +/** Dequantize one block at `block` (any alignment) into `out`. No checks. */ +void pn_dequantize_block_q4_0(const void *block, float *out); /* 32 floats */ +void pn_dequantize_block_q8_0(const void *block, float *out); /* 32 floats */ +void pn_dequantize_block_q2_0_g128(const void *block, float *out); /* 128 floats */ +void pn_dequantize_block_q3_k(const void *block, float *out); /* 256 floats */ +void pn_dequantize_block_q4_k(const void *block, float *out); /* 256 floats */ +void pn_dequantize_block_q5_k(const void *block, float *out); /* 256 floats */ +void pn_dequantize_block_q6_k(const void *block, float *out); /* 256 floats */ + +/** Exact binary16 -> binary32 widening (subnormals normalised, NaN payload kept). */ +float pn_half_to_float(uint16_t h); + +/** binary32 -> binary16, round to nearest even; overflow -> Inf; NaN -> 0x7E00 | sign. */ +uint16_t pn_float_to_half(float f); + +/** + * Dequantize a whole named tensor into a new malloc'd buffer (caller frees). + * Sets *numel_out (if non-NULL) on success. Returns NULL if file/name is NULL, the + * tensor is missing (stderr line), malloc fails, or the type is unsupported (stderr). + */ +float *pn_gguf_dequantize_tensor(pn_gguf_file *file, const char *name, size_t *numel_out); + +/** + * Dequantize row `row` (row_numel elements, block-aligned) of a named tensor into dst. + * Returns the pn_dequantize result, or -1 on NULL args, missing tensor, a type with no + * block size, or a misaligned row. No bounds check against the tensor dims. + */ +int pn_gguf_dequantize_row(pn_gguf_file *file, const char *name, + size_t row, size_t row_numel, float *dst); + +/** Elements per block (1 for F32/F16/BF16); 0 for unknown types. */ +int pn_gguf_block_size(pn_gguf_quant type); + +/** Bytes per block; 0 for types this code cannot size (incl. Q4_1, Q2_K, ...). */ +size_t pn_gguf_bytes_per_block(pn_gguf_quant type); + +/** Bytes for `numel` elements (whole blocks only); 0 if the type cannot be sized. */ +size_t pn_gguf_type_size(pn_gguf_quant type, size_t numel); + +/** + * Write the type's name ("Q4_K", or "type" if unknown) into buf with snprintf + * truncation and return buf. Returns the literal "" when buf is NULL or buf_len is 0. + */ +const char *pn_gguf_quant_name(pn_gguf_quant type, char *buf, size_t buf_len); + +#endif /* PN_GGUF_DEQUANT_H */ diff --git a/src/formats/gguf_layer_bytes.c b/src/formats/gguf_layer_bytes.c new file mode 100644 index 0000000..ebef32c --- /dev/null +++ b/src/formats/gguf_layer_bytes.c @@ -0,0 +1,142 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gguf_layer_bytes.c + * @brief What a range of transformer blocks actually weighs in a GGUF. + * + * Used by the multi-die admission gate, which places layers [lo,hi) on one die + * and must project that die's resident weight bytes. See gguf_layer_bytes.h for + * why a prorated share of mmap_size is the wrong number. + */ + +#include "gguf_layer_bytes.h" +#include "gguf_dequant.h" + +#include +#include + +/* Element count of a tensor. 0 if the info is malformed (n_dims out of range, + * or a zero dimension), which propagates to a 0 byte count. */ +static uint64_t tensor_numel(const pn_gguf_tensor_info *t) +{ + if (!t || t->n_dims == 0 || t->n_dims > 4) + return 0; + uint64_t n = 1; + for (uint32_t i = 0; i < t->n_dims; i++) { + if (t->dims[i] == 0) + return 0; + n *= t->dims[i]; + } + return n; +} + +/* Layer index of a `blk..` tensor, or -1 for anything else + * (token_embd.weight, output.weight, output_norm.weight, …). The trailing dot + * is required so a hypothetical `blk.12suffix` cannot pass as layer 12. */ +static int blk_layer_index(const char *name) +{ + if (!name || strncmp(name, "blk.", 4) != 0) + return -1; + const char *p = name + 4; + if (*p < '0' || *p > '9') + return -1; + long idx = 0; + while (*p >= '0' && *p <= '9') { + idx = idx * 10 + (*p - '0'); + if (idx > 1000000) /* not a layer index; refuse rather than wrap */ + return -1; + p++; + } + if (*p != '.') + return -1; + return (int)idx; +} + +uint64_t pn_gguf_tensor_file_bytes(pn_gguf_file *gf, const char *name) +{ + if (!gf || !name) + return 0; + pn_gguf_tensor_info *t = pn_gguf_find_tensor(gf, name); + if (!t) + return 0; + uint64_t numel = tensor_numel(t); + if (numel == 0) + return 0; + return (uint64_t)pn_gguf_type_size(t->type, (size_t)numel); +} + +uint64_t pn_gguf_tensor_numel(pn_gguf_file *gf, const char *name) +{ + if (!gf || !name) + return 0; + return tensor_numel(pn_gguf_find_tensor(gf, name)); +} + +int pn_gguf_layer_count(pn_gguf_file *gf) +{ + if (!gf || !gf->tensors) + return 0; + int max_idx = -1; + for (uint64_t i = 0; i < gf->n_tensors; i++) { + if (!gf->tensors[i].name.data) + continue; + int layer = blk_layer_index(gf->tensors[i].name.data); + if (layer > max_idx) + max_idx = layer; + } + int n = max_idx + 1; + + /* Drop a trailing MTP / NextN head. Qwen3.6-35B-A3B ships block_count=41: + * 40 transformer layers plus a speculative-draft block the autoregressive + * forward never runs and never uploads. Same rule the loader applies + * (qwen35moe.c), so a placement sized from this count splits the same + * layers the forward will. Counting it put ~230 MiB of weight that is + * never resident onto the last die's projection — enough, on its own, to + * refuse a 2-die Q4_K_M load with 100 MiB to spare. */ + if (n >= 2) { + char key[64]; + snprintf(key, sizeof(key), "blk.%d.nextn.eh_proj.weight", n - 1); + if (pn_gguf_find_tensor(gf, key) != NULL) + n -= 1; + } + return n; +} + +uint64_t pn_gguf_layer_range_bytes(pn_gguf_file *gf, int lo, int hi) +{ + if (lo < 0) + lo = 0; + if (!gf || !gf->tensors || hi <= lo) + return 0; + uint64_t total = 0; + for (uint64_t i = 0; i < gf->n_tensors; i++) { + const pn_gguf_tensor_info *t = &gf->tensors[i]; + if (!t->name.data) + continue; + int layer = blk_layer_index(t->name.data); + if (layer < lo || layer >= hi) + continue; + uint64_t numel = tensor_numel(t); + if (numel == 0) + continue; + uint64_t bytes = (uint64_t)pn_gguf_type_size(t->type, (size_t)numel); + /* An UNRECOGNISED quant must not size as FREE. This sum is a safety + * projection: a tensor we cannot measure contributes 0, which reads + * downstream as "costs nothing" and admits a load that cannot fit. + * Measured 2026-08-25: BF16 (type 30) was in the enum but not in + * pn_gguf_block_size, so every expert tensor sized as zero and the gate + * projected 10 MiB per die for a model that needs ~8,300. Returning 0 + * for the WHOLE range makes the caller fall back to its prorated + * estimate instead, which is wrong but never optimistic. */ + if (bytes == 0) { + char qn[32]; + fprintf(stderr, "[gguf] layer range: no size for quant %s (%d) on " + "'%s' — refusing to project this range\n", + pn_gguf_quant_name(t->type, qn, sizeof(qn)), (int)t->type, + t->name.data); + return 0; + } + total += bytes; + } + return total; +} diff --git a/src/formats/gguf_layer_bytes.h b/src/formats/gguf_layer_bytes.h new file mode 100644 index 0000000..866de3d --- /dev/null +++ b/src/formats/gguf_layer_bytes.h @@ -0,0 +1,40 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_GGUF_LAYER_BYTES_H +#define PN_GGUF_LAYER_BYTES_H + +#include "gguf.h" +#include + +/* + * Per-layer weight accounting straight off the GGUF tensor table. + * + * A multi-die split gives each die a *range* of layers, so the admission gate + * needs "what do layers [lo,hi) actually weigh", not "what fraction of the file + * is hi-lo layers". The two are not the same number: the file also carries + * token_embd and output.weight, which belong to no layer, so prorating + * mmap_size across n_layer charges every die for tensors it does not hold. On + * Qwen3.6-35B-A3B that is ~700 MiB of phantom weight per die — enough to refuse + * a load that fits with 1.3 GiB to spare. + */ + +/* File bytes of one named tensor. 0 if absent, malformed, or an unsupported + * quant (callers treat 0 as "don't know", not "free"). */ +uint64_t pn_gguf_tensor_file_bytes(pn_gguf_file *gf, const char *name); + +/* Summed file bytes of every `blk..*` tensor with lo <= i < hi. Returns 0 + * when the table holds no matching tensor, which callers use to fall back to + * an older estimate rather than admit blind. */ +uint64_t pn_gguf_layer_range_bytes(pn_gguf_file *gf, int lo, int hi); + +/* Highest `blk..` index in the table, + 1. Read off the tensors rather than + * `.block_count` so it needs no architecture prefix and cannot disagree + * with the tensors a layer range will actually sum. 0 if there are none. */ +int pn_gguf_layer_count(pn_gguf_file *gf); + +/* Element count of one named tensor, 0 if absent or malformed. Callers that + * must size a DEQUANTIZED copy need this rather than the file bytes — a tied + * model's LM head is token_embd materialized to F32, four bytes an element. */ +uint64_t pn_gguf_tensor_numel(pn_gguf_file *gf, const char *name); + +#endif /* PN_GGUF_LAYER_BYTES_H */ diff --git a/src/memory/arena.c b/src/memory/arena.c new file mode 100644 index 0000000..6d7ae5f --- /dev/null +++ b/src/memory/arena.c @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "arena.h" +#include +#include + +#define ARENA_ALIGN 16 + +pn_arena* pn_arena_new(size_t capacity) { + pn_arena *a = malloc(sizeof(pn_arena)); + if (!a) return NULL; + + /* Allocate aligned backing buffer */ + a->base = NULL; + if (posix_memalign((void **)&a->base, ARENA_ALIGN, capacity) != 0) { + free(a); + return NULL; + } + + a->capacity = capacity; + a->used = 0; + return a; +} + +void pn_arena_free(pn_arena *arena) { + if (!arena) return; + free(arena->base); + free(arena); +} + +void* pn_arena_alloc(pn_arena *arena, size_t n) { + if (!arena || n == 0) return NULL; + + /* Align up to 16 bytes */ + size_t aligned = (arena->used + (ARENA_ALIGN - 1)) & ~(size_t)(ARENA_ALIGN - 1); + if (aligned + n > arena->capacity) return NULL; + + void *ptr = arena->base + aligned; + arena->used = aligned + n; + return ptr; +} + +float* pn_arena_alloc_f32(pn_arena *arena, int n) { + return (float *)pn_arena_alloc(arena, (size_t)n * sizeof(float)); +} + +void pn_arena_reset(pn_arena *arena) { + if (arena) arena->used = 0; +} + +size_t pn_arena_used(const pn_arena *arena) { + return arena ? arena->used : 0; +} diff --git a/src/memory/arena.h b/src/memory/arena.h new file mode 100644 index 0000000..2906c64 --- /dev/null +++ b/src/memory/arena.h @@ -0,0 +1,37 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_ARENA_H +#define PN_ARENA_H + +#include + +/* + * Bump allocator for per-token scratch memory. + * O(1) alloc, O(1) reset. 16-byte aligned. + */ + +typedef struct pn_arena { + char *base; + size_t capacity; + size_t used; +} pn_arena; + +/* Create arena with given capacity (bytes). Returns NULL on failure. */ +pn_arena* pn_arena_new(size_t capacity); + +/* Free arena and its backing buffer. */ +void pn_arena_free(pn_arena *arena); + +/* Allocate n bytes from arena (16-byte aligned). Returns NULL if full. */ +void* pn_arena_alloc(pn_arena *arena, size_t n); + +/* Allocate n floats from arena. Returns NULL if full. */ +float* pn_arena_alloc_f32(pn_arena *arena, int n); + +/* Reset arena to empty (O(1), no deallocation). */ +void pn_arena_reset(pn_arena *arena); + +/* Current bytes used. */ +size_t pn_arena_used(const pn_arena *arena); + +#endif /* PN_ARENA_H */ diff --git a/src/models/bonsai.h b/src/models/bonsai.h new file mode 100644 index 0000000..5aee708 --- /dev/null +++ b/src/models/bonsai.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bonsai.h + * @brief PrismML Bonsai-27B (native ternary) arch entry — its OWN forward/loader + * surface (PN_ARCH_BONSAI). Bonsai declares general.architecture="qwen35": + * the SAME hybrid Gated-DeltaNet + sparse-full-attention family as + * Nex-N2-mini (qwen35moe), but with a DENSE SwiGLU FFN (no MoE) and native + * ternary (Q2_0_g128 / type-142) weights. + * + * Isolation model: Bonsai gets its own arch enum and its own top-level + * forward/loader entry points (this file + bonsai_forward.c), kept + * physically separate from the shipped Nex-N2 forward. The heavy, + * already-shared machinery (GDN block, full-attention block, RoPE, + * RMSNorm, Q4 KV, the ternary fc_matvec_q2_0, GPU upload, dual-state + * lifecycle) is REUSED from the qwen35moe module rather than cloned — + * the only Bonsai-specific behavior (dense SwiGLU FFN) is driven by a + * config property (cfg->dense_ffn) the shared block reads. The shipped Nex-N2 + * (qwen35moe, dense_ffn=0) is byte-identical either way. + * + * @status Phase 2 refactor: own-files isolation of the Bonsai bring-up that was + * first brought up as a dense_ffn flag on the qwen35moe path. + */ + +#ifndef PN_BONSAI_H +#define PN_BONSAI_H + +#include "../core/engine.h" +#include "../formats/gguf.h" + +/** + * True if the open GGUF is a Bonsai-family model: general.architecture="qwen35" + * AND a dense FFN (no routed experts / merged expert tensors — a plain + * blk.0.ffn_gate is present). Nex-N2-mini (qwen35moe, routed MoE) is NOT Bonsai. + * engine.c uses this to route to PN_ARCH_BONSAI vs PN_ARCH_QWEN35MOE. + */ +int pn_bonsai_is_bonsai_gguf(pn_gguf_file *gguf); + +/** + * Load a Bonsai GGUF. Reuses the qwen35moe loader (which detects the dense-FFN + * variant from the metadata and loads ffn_gate/up/down into the shared-expert + * slots). Returns the shared opaque qwen35moe model handle (as void*) so the + * engine can drive it through the qwen35moe lifecycle. NULL on failure. + */ +void *pn_bonsai_load(pn_gguf_file *gguf, pn_config *config); + +/** + * Bonsai CPU forward (pn_forward_fn). Runs the hybrid stack for one token + * (48 GDN + 16 full-attn + dense SwiGLU FFN, ternary matvecs on the CPU + * oracle). Delegates to the shared qwen35moe CPU forward. + */ +tm_tensor *pn_bonsai_forward(pn_model *model, uint32_t token, pn_state *state); + +#ifdef USE_CUDA +/** + * Bonsai single-die GPU forward (pn_forward_fn). Delegates to the shared + * qwen35moe CUDA forward; the dense-FFN GPU block + fc_matvec_q2_0 ternary + * matvec run inside the shared forward, selected by cfg->dense_ffn. + */ +tm_tensor *pn_bonsai_forward_cuda(pn_model *model, uint32_t token, + pn_state *state); + +/** Upload Bonsai weights to persistent VRAM (delegates to qwen35moe). */ +int pn_bonsai_gpu_upload(void *model); +/** True if the Bonsai model's weights are resident on a GPU die. */ +int pn_bonsai_model_gpu_loaded(const void *model); +#endif + +/** Bonsai state lifecycle (dual SSM + attn-KV) — delegates to qwen35moe. */ +void *pn_bonsai_state_new(void *weights); +void pn_bonsai_state_free(void *state); +void pn_bonsai_state_reset(void *state); + +/** Free the Bonsai model handle (delegates to qwen35moe). */ +void pn_bonsai_free(void *model); +/** GGUF backing the Bonsai model (delegates to qwen35moe). */ +pn_gguf_file *pn_bonsai_gguf(const void *model); +/** Explicit die placement for the GPU upload (delegates to qwen35moe). */ +void pn_bonsai_model_set_dies(void *model, const int *dies, int n); + +#endif /* PN_BONSAI_H */ diff --git a/src/models/bonsai_forward.c b/src/models/bonsai_forward.c new file mode 100644 index 0000000..fe6c228 --- /dev/null +++ b/src/models/bonsai_forward.c @@ -0,0 +1,104 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bonsai_forward.c + * @brief PrismML Bonsai-27B top-level forward/loader entry (PN_ARCH_BONSAI). + * + * This is Bonsai's OWN entry file, kept physically separate from the shipped + * Nex-N2 forward (qwen35moe_forward*.c). It owns the pn_forward_fn/loader/state + * seam for PN_ARCH_BONSAI and DELEGATES the heavy lifting to the shared + * qwen35moe machinery: Bonsai is the SAME hybrid GDN + sparse-full-attention + * family (general.architecture="qwen35"), differing only in a DENSE SwiGLU FFN + * (no MoE) and native-ternary (Q2_0_g128 / type-142) weights. + * + * Why delegate instead of clone: the hybrid forward is ~7.4k lines of shared, + * die-scratch-heavy CUDA orchestration (GDN block, flash-attn, Q4 KV, RoPE, + * RMSNorm, the ternary fc_matvec_q2_0). Cloning it to strip MoE -> dense would + * be a large correctness/drift risk (the exact hazard the bring-up flagged). + * Instead, the ONE Bonsai-specific behavior — the dense SwiGLU FFN block — is a + * config-driven branch inside the shared per-layer FFN helper, selected by + * cfg->dense_ffn (set at load from the GGUF metadata). The shipped Nex-N2 (qwen35moe, + * dense_ffn=0) never takes that branch and is byte-identical. Bonsai therefore + * has its own arch identity + its own forward entry file, without a risky clone. + */ + +#include "bonsai.h" +#include "qwen35moe.h" + +#include + +int pn_bonsai_is_bonsai_gguf(pn_gguf_file *gguf) +{ + if (!gguf) + return 0; + const char *arch = pn_gguf_get_str(gguf, "general.architecture"); + if (!arch || strcmp(arch, "qwen35") != 0) + return 0; + /* Dense FFN (Bonsai) vs routed MoE (Nex-N2): a plain per-layer ffn_gate is + * present and the merged expert tensor is absent. */ + if (pn_gguf_find_tensor(gguf, "blk.0.ffn_gate_exps.weight") != NULL) + return 0; + if (pn_gguf_find_tensor(gguf, "blk.0.ffn_gate.weight") == NULL) + return 0; + return 1; +} + +void *pn_bonsai_load(pn_gguf_file *gguf, pn_config *config) +{ + /* The shared loader detects the dense-FFN variant from the metadata + * (expert_count absent + ffn_gate present) and sets cfg->dense_ffn. */ + return pn_qwen35moe_load(gguf, config); +} + +tm_tensor *pn_bonsai_forward(pn_model *model, uint32_t token, pn_state *state) +{ + return pn_qwen35moe_forward(model, token, state); +} + +#ifdef USE_CUDA +tm_tensor *pn_bonsai_forward_cuda(pn_model *model, uint32_t token, + pn_state *state) +{ + return pn_qwen35moe_forward_cuda(model, token, state); +} + +int pn_bonsai_gpu_upload(void *model) +{ + return pn_qwen35moe_gpu_upload((pn_qwen35moe_model *)model); +} + +int pn_bonsai_model_gpu_loaded(const void *model) +{ + return pn_qwen35moe_model_gpu_loaded((const pn_qwen35moe_model *)model); +} +#endif + +void *pn_bonsai_state_new(void *weights) +{ + return pn_qwen35moe_state_new(weights); +} + +void pn_bonsai_state_free(void *state) +{ + pn_qwen35moe_state_free(state); +} + +void pn_bonsai_state_reset(void *state) +{ + pn_qwen35moe_state_reset(state); +} + +void pn_bonsai_free(void *model) +{ + pn_qwen35moe_free((pn_qwen35moe_model *)model); +} + +pn_gguf_file *pn_bonsai_gguf(const void *model) +{ + return pn_qwen35moe_gguf((const pn_qwen35moe_model *)model); +} + +void pn_bonsai_model_set_dies(void *model, const int *dies, int n) +{ + pn_qwen35moe_model_set_dies((pn_qwen35moe_model *)model, dies, n); +} diff --git a/src/models/gpu_attn_path_tracker.c b/src/models/gpu_attn_path_tracker.c new file mode 100644 index 0000000..4747f88 --- /dev/null +++ b/src/models/gpu_attn_path_tracker.c @@ -0,0 +1,210 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gpu_attn_path_tracker.c + * @brief Implementation of the per-attention-layer path tracker — which + * attention path ran, why the on-die one was refused, and the bounded + * retry for a failed per-slot KV-ring allocation. + * + * Pure host state machine: no CUDA, no allocation, no locks. Deliberate — the + * retry policy has to be provable by a unit test over this file alone, on any + * machine, with no GPU and no model. + * + * @status None + * @issues None + * @todo None + */ + +#include "gpu_attn_path_tracker.h" + +#include +#include + +/* Eligible-step gap before retry attempt number N (1-based on + * alloc_failures). The last entry covers any further failure, but the budget + * gives up before it can be reached twice. */ +static const unsigned short PN_ATTN_RING_BACKOFF_STEPS[] = { 8, 32, 128, 512 }; + +#define PN_ATTN_RING_BACKOFF_COUNT \ + ((int)(sizeof(PN_ATTN_RING_BACKOFF_STEPS) / \ + sizeof(PN_ATTN_RING_BACKOFF_STEPS[0]))) + +void pn_attn_path_ondie(pn_attn_path_tracker *t) +{ + if (!t) + return; + t->state = PN_ATTN_PATH_ONDIE; + t->reason = PN_ATTN_HOST_NONE; +} + +void pn_attn_path_host(pn_attn_path_tracker *t, pn_attn_host_reason reason) +{ + if (!t) + return; + /* Count the ENGAGEMENT, not the step: a layer already parked on the host + * path would otherwise add one per token and the counter would read as a + * token rate rather than an incident count. */ + if (t->state != PN_ATTN_PATH_HOST) + t->lifetime_host_engagements++; + t->state = PN_ATTN_PATH_HOST; + t->reason = (unsigned char)reason; +} + +int pn_attn_ring_should_attempt(pn_attn_path_tracker *t) +{ + if (!t) + return 1; /* no tracker: never block the allocation */ + + /* Budget spent (transiently exhausted, or the geometry proven impossible on + * this die) — this is the bound, and the only permanent no. A strictly + * smaller geometry clears it via pn_attn_ring_geometry_shrank before this + * function is reached, which is what keeps "give up" from meaning "this + * layer is off until a reload". */ + if (t->alloc_gave_up) + return 0; + + if (t->retry_cooldown_steps > 0) { + t->retry_cooldown_steps--; + return 0; + } + return 1; +} + +void pn_attn_ring_alloc_ok(pn_attn_path_tracker *t) +{ + /* Called on every steady-state step (the geometry is already satisfied), so + * the common case must touch nothing. */ + if (!t || (!t->alloc_failures && !t->alloc_gave_up && + !t->retry_cooldown_steps)) + return; + t->alloc_failures = 0; + t->alloc_gave_up = 0; + t->alloc_impossible = 0; + t->retry_cooldown_steps = 0; +} + +void pn_attn_ring_alloc_failed(pn_attn_path_tracker *t, int impossible) +{ + if (!t) + return; + t->lifetime_alloc_failures++; + if (t->alloc_failures < 255) + t->alloc_failures++; + + /* Measured, not guessed: the caller compared the request against this die's + * TOTAL usable VRAM. A request that would fail on an empty die has nothing + * transient to wait for, so waiting out four backoffs would be four rounds + * of pointless multi-hundred-MiB cuMemAllocs. Stop now. */ + if (impossible) { + t->alloc_impossible = 1; + t->alloc_gave_up = 1; + t->retry_cooldown_steps = 0; + return; + } + + if (t->alloc_failures >= PN_ATTN_RING_MAX_FAILURES) { + t->alloc_gave_up = 1; + t->retry_cooldown_steps = 0; + return; + } + + int idx = t->alloc_failures - 1; + if (idx >= PN_ATTN_RING_BACKOFF_COUNT) + idx = PN_ATTN_RING_BACKOFF_COUNT - 1; + t->retry_cooldown_steps = PN_ATTN_RING_BACKOFF_STEPS[idx]; +} + +void pn_attn_ring_geometry_shrank(pn_attn_path_tracker *t) +{ + if (!t) + return; + t->alloc_failures = 0; + t->alloc_gave_up = 0; + t->alloc_impossible = 0; + t->retry_cooldown_steps = 0; +} + +void pn_attn_residency_add(pn_attn_residency *out, + const pn_attn_path_tracker *t) +{ + if (!out || !t || t->state == PN_ATTN_PATH_UNKNOWN) + return; + out->total++; + out->lifetime_alloc_failures += t->lifetime_alloc_failures; + out->lifetime_host_engagements += t->lifetime_host_engagements; + if (t->state == PN_ATTN_PATH_HOST) { + out->host++; + if (t->reason < PN_ATTN_HOST_REASON_COUNT) + out->by_reason[t->reason]++; + if (t->alloc_gave_up) + out->gave_up++; + if (t->alloc_impossible) + out->impossible++; + } else { + out->ondie++; + } +} + +const char *pn_attn_path_state_name(unsigned char state) +{ + switch (state) { + case PN_ATTN_PATH_ONDIE: return "ondie"; + case PN_ATTN_PATH_HOST: return "host"; + default: return "unknown"; + } +} + +const char *pn_attn_host_reason_name(unsigned char reason) +{ + switch (reason) { + case PN_ATTN_HOST_FLASH_OFF: return "flash_slots_off"; + case PN_ATTN_HOST_NO_KERNELS: return "no_flash_kernels"; + case PN_ATTN_HOST_NO_SCRATCH: return "no_batch_scratch"; + case PN_ATTN_HOST_NO_SLOT_KERNEL: return "no_slot_decode_kernel"; + case PN_ATTN_HOST_RING_WRAP: return "kv_geometry_or_ring_wrap"; + case PN_ATTN_HOST_RING_ALLOC: return "ring_alloc_failed"; + case PN_ATTN_HOST_DEC_SCRATCH: return "dec_scratch_alloc_failed"; + default: return "none"; + } +} + +/* QWEN35_RING_ALLOC_FAIL_INJECT=N or N:impossible. Parsed once. */ +static void ring_inject_parse(int *n_out, int *imp_out) +{ + static int cached_n = -1; + static int cached_imp = 0; + if (cached_n < 0) { + cached_n = 0; + const char *env = getenv("QWEN35_RING_ALLOC_FAIL_INJECT"); + if (env && env[0]) { + cached_n = atoi(env); + if (cached_n < 0) + cached_n = 0; + cached_imp = (strstr(env, "impossible") != NULL); + } + } + *n_out = cached_n; + *imp_out = cached_imp; +} + +int pn_attn_ring_inject_failures(void) +{ + int n = 0, imp = 0; + ring_inject_parse(&n, &imp); + return n; +} + +int pn_attn_ring_inject_impossible(void) +{ + int n = 0, imp = 0; + ring_inject_parse(&n, &imp); + return imp; +} + +int pn_attn_ring_inject_now(const pn_attn_path_tracker *t) +{ + int n = pn_attn_ring_inject_failures(); + if (n <= 0 || !t) + return 0; + return (int)t->lifetime_alloc_failures < n; +} diff --git a/src/models/gpu_attn_path_tracker.h b/src/models/gpu_attn_path_tracker.h new file mode 100644 index 0000000..c35f390 --- /dev/null +++ b/src/models/gpu_attn_path_tracker.h @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gpu_attn_path_tracker.h + * @brief Per-attention-layer record of WHICH attention path is running — the + * on-die flash kernels or the host-softmax fallback — plus the bounded + * retry policy for a per-slot KV-ring allocation that failed. + * + * WHY THIS EXISTS + * Host-side causal GQA softmax on a 2-core host is the wall this project + * spent a phase escaping: it was 87.5% of an 8k prefill, and moving attention + * on-die took that prefill from 26 minutes to 3.82 minutes. Falling back to it + * is not graceful degradation. A measured cliff on the depth bench is + * 13.13 / 13.02 tok/s at depth 1024 / 2048 and 1.22 tok/s at 4096 — 10.7x, + * K=2, two dies of one K80, 2026-08-08 — with correct output, no error, and nothing on + * any status surface saying which path the layer took. (A widely-quoted 158x + * for the same cliff came off an ASAN build; ASAN inflates the host path by + * roughly another order of magnitude. 10.7x is the real number, and it grows + * with context because host softmax is O(context) per layer per token.) + * + * Worse, one of the ways in LATCHED. afcad12 made a failed + * gpu_attn_slot_ring_ensure sticky, for a good reason: without it the layer + * re-freed and re-attempted a 144 MiB cuMemAlloc on EVERY decode step and + * bumped slots_ring_gen each time, which invalidates every layer's captured + * CUDA graph every step — a 62% collapse, far worse than the ~10-15% the + * fallback intends. But the cure made ONE transient allocation failure pin + * that layer to the slow path for the life of the process. That is the same + * one-way ratchet the CUDA-graph capture latch had + * (gpu_graph_capture_tracker.h), and it gets the same treatment. + * + * THE TWO CASES, KEPT APART + * TRANSIENT — the rings do not fit RIGHT NOW. Another model is loading, a + * sibling instance holds the die, a bigger geometry has not been + * released yet. Retried on a backoff of 8/32/128/512 eligible + * steps and then given up on. A success resets the budget. + * IMPOSSIBLE — the request exceeds the die's whole usable VRAM + * (total - reserve), so it would fail on an empty die. Nothing + * to wait for: give up immediately at this geometry. The caller + * decides this from fc_mem_info_ctx, so it is a measurement, not + * a heuristic. + * + * Either way a STRICTLY SMALLER geometry (fewer slots, shorter window, a + * cheaper cache mode) resets the budget at once — dropping K or ctx_cap must + * recover the on-die path without waiting out a backoff and without a reload. + * + * WHY THE RETRY IS CHEAP NOW + * The 62% collapse afcad12 measured was not the failed cuMemAlloc; it was the + * unconditional slots_ring_gen bump beside it. A retry that frees nothing and + * allocates nothing must not claim the ring addresses moved. With that fixed + * and the backoff above, a permanently-unfittable layer spends at most + * PN_ATTN_RING_MAX_FAILURES rounds of a handful of failed cuMemAllocs over + * ~680 steps and then costs nothing at all. + * + * Thread-safe: no — one tracker belongs to one attention layer on one die, + * touched only by that die's driver thread. The residency roll-up reads them + * without a lock and may observe a step-boundary-torn snapshot; every field is + * a single word and the counts are advisory. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_GPU_ATTN_PATH_TRACKER_H +#define PN_GPU_ATTN_PATH_TRACKER_H + +/* Which attention path this layer ran on its most recent batched-decode step. */ +typedef enum { + PN_ATTN_PATH_UNKNOWN = 0, /* the forward has not reached this layer yet */ + PN_ATTN_PATH_ONDIE, /* on-die flash attention */ + PN_ATTN_PATH_HOST /* host-softmax fallback */ +} pn_attn_path_state; + +/* Why the on-die path was refused. Mirrors the guards in + * attn_slots_flash_prep, in the order they are evaluated. */ +typedef enum { + PN_ATTN_HOST_NONE = 0, + PN_ATTN_HOST_FLASH_OFF, /* QWEN35_FLASH_SLOTS=0 */ + PN_ATTN_HOST_NO_KERNELS, /* no flash kernels on this ctx */ + PN_ATTN_HOST_NO_SCRATCH, /* batch scratch a_z / pos_g missing */ + PN_ATTN_HOST_NO_SLOT_KERNEL, /* no fp32 / q4 slot-decode kernel */ + PN_ATTN_HOST_RING_WRAP, /* non-uniform KV geometry, or pos+1>max_seq */ + PN_ATTN_HOST_RING_ALLOC, /* per-slot KV ring allocation failed */ + PN_ATTN_HOST_DEC_SCRATCH, /* decode partial-scratch allocation failed */ + PN_ATTN_HOST_REASON_COUNT +} pn_attn_host_reason; + +/* Retry budget for a failed ring allocation: 4 retries after the first + * failure, then stop. Counted in ELIGIBLE STEPS (steps where this layer ran + * and a geometry change asked for an allocation), so the cost is fixed + * regardless of token rate. */ +#define PN_ATTN_RING_MAX_FAILURES 5 + +/* Per-attention-layer path + ring-allocation tracker. Zero-initialised = + * UNKNOWN, which is what calloc of the layer array already gives. */ +typedef struct { + unsigned char state; /* pn_attn_path_state */ + unsigned char reason; /* pn_attn_host_reason (HOST only) */ + unsigned char alloc_failures; /* consecutive ring-alloc failures */ + unsigned char alloc_gave_up; /* budget spent, or proven impossible */ + unsigned char alloc_impossible; /* need > this die's whole usable VRAM */ + unsigned short retry_cooldown_steps; /* eligible steps left before retry */ + unsigned int lifetime_alloc_failures; + unsigned int lifetime_host_engagements; /* on-die -> host transitions */ +} pn_attn_path_tracker; + +/* Roll-up over a set of trackers. `total` counts only layers the forward has + * actually REACHED — a model that has never decoded batched reports total 0 + * rather than a fabricated deficit. ondie + host == total. */ +typedef struct { + int total; + int ondie; + int host; + int gave_up; /* subset of host: ring-alloc retry budget spent */ + int impossible; /* subset of gave_up: cannot fit an empty die */ + int by_reason[PN_ATTN_HOST_REASON_COUNT]; + unsigned int lifetime_alloc_failures; + unsigned int lifetime_host_engagements; +} pn_attn_residency; + +/* One model's attention-path residency, split by code path. `all` is the sum. + * Only the batched-decode slots path is instrumented today — it is the path + * the serve actually takes (QWEN35_BATCH_SLOTS=1 routes even a lone request + * through the batch scheduler) and the only one carrying the ring latch. */ +typedef struct { + pn_attn_residency all; + pn_attn_residency batched_attn; +} pn_model_attn_residency; + +/* This layer ran on-die this step. Clears any host reason; does NOT clear the + * ring-allocation budget (that is pn_attn_ring_alloc_ok's job) so a layer that + * is on-die for an unrelated reason cannot silently refund a failing ring. */ +void pn_attn_path_ondie(pn_attn_path_tracker *t); + +/* This layer took the host-softmax fallback this step, for `reason`. Counts a + * lifetime engagement only on an on-die -> host transition, so a layer parked + * on the host path does not inflate the counter every token. */ +void pn_attn_path_host(pn_attn_path_tracker *t, pn_attn_host_reason reason); + +/** + * Should a per-slot KV-ring allocation be attempted on this step? + * + * Call ONCE per step on a layer whose ring geometry changed and therefore needs + * an allocation. It owns the whole backoff: on a cooldown step it decrements + * and answers no. + * + * @param t Tracker for this attention layer + * @return 1 to attempt the allocation now, 0 to take the host path this step + * + * Thread-safe: no (single owning driver thread) + * Reentrant: no (mutates the tracker) + */ +int pn_attn_ring_should_attempt(pn_attn_path_tracker *t); + +/* The rings allocated. Clears the failure budget: a geometry that has ever + * fitted is proven fittable, so a later failure starts fresh. */ +void pn_attn_ring_alloc_ok(pn_attn_path_tracker *t); + +/* A ring allocation attempt failed. `impossible` != 0 means the request + * exceeds the die's entire usable VRAM and would fail on an empty die, so + * there is nothing transient to wait for and the budget is spent at once. */ +void pn_attn_ring_alloc_failed(pn_attn_path_tracker *t, int impossible); + +/* The requested geometry is strictly SMALLER than the one that failed, so the + * old verdict does not describe it. Clears the budget and any cooldown, which + * is what makes dropping K or ctx_cap recover the on-die path immediately. */ +void pn_attn_ring_geometry_shrank(pn_attn_path_tracker *t); + +/* Fold ONE tracker into a residency roll-up. Zero the roll-up first. */ +void pn_attn_residency_add(pn_attn_residency *out, + const pn_attn_path_tracker *t); + +/* Short stable names for logs and JSON. */ +const char *pn_attn_path_state_name(unsigned char state); +const char *pn_attn_host_reason_name(unsigned char reason); + +/** + * Test-only fault injection: force the first N ring-allocation attempts of + * EVERY attention layer to be treated as failures, so the retry path can be + * exercised end to end on real hardware without having to genuinely exhaust a + * die. Read once from QWEN35_RING_ALLOC_FAIL_INJECT; 0 (unset) is the + * production path and costs one cached compare. + * + * A value of the form "N:impossible" injects PERMANENT failures instead of + * transient ones, which is how the give-up-at-once path is exercised. + * + * @return How many attempts per layer to fail, 0 = never inject + */ +int pn_attn_ring_inject_failures(void); + +/* Is the injected failure a permanent (impossible) one? */ +int pn_attn_ring_inject_impossible(void); + +/* Should THIS layer's current allocation attempt be forced to fail? Keyed off + * the layer's own lifetime failure count, so each layer fails exactly N times + * and then behaves normally. Injecting N < PN_ATTN_RING_MAX_FAILURES exercises + * recovery; injecting more exercises the give-up path. */ +int pn_attn_ring_inject_now(const pn_attn_path_tracker *t); + +#endif /* PN_GPU_ATTN_PATH_TRACKER_H */ diff --git a/src/models/gpu_graph_capture_tracker.c b/src/models/gpu_graph_capture_tracker.c new file mode 100644 index 0000000..e8f2030 --- /dev/null +++ b/src/models/gpu_graph_capture_tracker.c @@ -0,0 +1,198 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gpu_graph_capture_tracker.c + * @brief Implementation of the per-layer CUDA-graph capture tracker — bounded + * retry, permanent give-up, and the three-state residency roll-up. + * + * Pure host state machine: no CUDA, no allocation, no locks. That is deliberate + * — it is the piece that has to be testable without a GPU, and the gate that + * proves the retry policy is a unit test over this file alone. + * + * @status None + * @issues None + * @todo None + */ + +#include "gpu_graph_capture_tracker.h" + +#include + +/* Eligible-step gap before retry attempt number N (1-based on + * consecutive_failures). The last entry is used for any further failure, but + * the budget gives up before it can be reached twice. */ +static const unsigned short PN_GRAPH_RETRY_BACKOFF_STEPS[] = { 8, 32, 128, 512 }; + +#define PN_GRAPH_RETRY_BACKOFF_COUNT \ + ((int)(sizeof(PN_GRAPH_RETRY_BACKOFF_STEPS) / \ + sizeof(PN_GRAPH_RETRY_BACKOFF_STEPS[0]))) + +int pn_graph_capture_should_attempt(pn_graph_capture_tracker *t) +{ + if (!t) + return 0; + + /* A slot that spent its budget never attempts again — this is the bound. + * It is also the ONLY permanent no: a gated-out slot never reaches this + * function at all (the caller tests eligibility first), which is what keeps + * INELIGIBLE structurally incapable of becoming a retry loop. */ + if (t->state == PN_GRAPH_CAPTURE_GAVE_UP) + return 0; + + /* Reaching here means the gate is open, so a stale INELIGIBLE from a + * previously-closed gate is out of date. */ + if (t->state == PN_GRAPH_CAPTURE_INELIGIBLE) { + t->state = PN_GRAPH_CAPTURE_PENDING; + t->ineligible_reason = PN_GRAPH_INELIGIBLE_NONE; + } + + t->evaluated = 1; + if (t->retry_cooldown_steps > 0) { + t->retry_cooldown_steps--; + return 0; + } + return 1; +} + +void pn_graph_capture_succeeded(pn_graph_capture_tracker *t) +{ + if (!t) + return; + t->evaluated = 1; + t->state = PN_GRAPH_CAPTURE_LIVE; + t->ineligible_reason = PN_GRAPH_INELIGIBLE_NONE; + t->consecutive_failures = 0; + t->retry_cooldown_steps = 0; + t->lifetime_captures++; +} + +void pn_graph_capture_failed(pn_graph_capture_tracker *t) +{ + if (!t) + return; + t->evaluated = 1; + t->lifetime_failures++; + if (t->consecutive_failures < 255) + t->consecutive_failures++; + + if (t->consecutive_failures >= PN_GRAPH_CAPTURE_MAX_FAILURES) { + t->state = PN_GRAPH_CAPTURE_GAVE_UP; + t->retry_cooldown_steps = 0; + return; + } + + int idx = t->consecutive_failures - 1; + if (idx >= PN_GRAPH_RETRY_BACKOFF_COUNT) + idx = PN_GRAPH_RETRY_BACKOFF_COUNT - 1; + t->state = PN_GRAPH_CAPTURE_RETRYING; + t->retry_cooldown_steps = PN_GRAPH_RETRY_BACKOFF_STEPS[idx]; +} + +void pn_graph_capture_dropped(pn_graph_capture_tracker *t) +{ + if (!t) + return; + t->evaluated = 1; + t->state = PN_GRAPH_CAPTURE_PENDING; + t->ineligible_reason = PN_GRAPH_INELIGIBLE_NONE; + t->consecutive_failures = 0; + t->retry_cooldown_steps = 0; +} + +void pn_graph_capture_superseded(pn_graph_capture_tracker *t) +{ + if (!t) + return; + t->evaluated = 0; + t->state = PN_GRAPH_CAPTURE_PENDING; + t->ineligible_reason = PN_GRAPH_INELIGIBLE_NONE; + t->consecutive_failures = 0; + t->retry_cooldown_steps = 0; +} + +void pn_graph_capture_ineligible(pn_graph_capture_tracker *t, + pn_graph_ineligible_reason reason) +{ + if (!t) + return; + /* A live graph is a live graph regardless of this step's gate, and a slot + * that exhausted its retry budget really did FAIL — neither should be + * relabelled as "this layer never captures". */ + if (t->state == PN_GRAPH_CAPTURE_LIVE || t->state == PN_GRAPH_CAPTURE_GAVE_UP) + return; + t->evaluated = 1; + t->state = PN_GRAPH_CAPTURE_INELIGIBLE; + t->ineligible_reason = (unsigned char)reason; +} + +void pn_graph_residency_add(pn_graph_residency *out, + const pn_graph_capture_tracker *t) +{ + if (!out || !t || !t->evaluated) + return; + out->total++; + out->lifetime_captures += t->lifetime_captures; + out->lifetime_failures += t->lifetime_failures; + switch (t->state) { + case PN_GRAPH_CAPTURE_LIVE: + out->live++; + break; + case PN_GRAPH_CAPTURE_RETRYING: + out->failed++; + break; + case PN_GRAPH_CAPTURE_GAVE_UP: + out->failed++; + out->gave_up++; + break; + case PN_GRAPH_CAPTURE_INELIGIBLE: + out->ineligible++; + break; + default: + out->pending++; + break; + } +} + +const char *pn_graph_capture_state_name(unsigned char state) +{ + switch (state) { + case PN_GRAPH_CAPTURE_LIVE: return "live"; + case PN_GRAPH_CAPTURE_RETRYING: return "retrying"; + case PN_GRAPH_CAPTURE_GAVE_UP: return "gave_up"; + case PN_GRAPH_CAPTURE_INELIGIBLE: return "ineligible"; + default: return "pending"; + } +} + +const char *pn_graph_ineligible_reason_name(unsigned char reason) +{ + switch (reason) { + case PN_GRAPH_INELIGIBLE_GRAPH_OFF: return "QWEN35_GRAPH=0"; + case PN_GRAPH_INELIGIBLE_SLOTS_OFF: return "QWEN35_GRAPH_SLOTS=0"; + case PN_GRAPH_INELIGIBLE_ATTN_OFF: return "QWEN35_GRAPH_SLOTS_ATTN=0"; + case PN_GRAPH_INELIGIBLE_PROFILING: return "profiling active"; + case PN_GRAPH_INELIGIBLE_MOE_NOT_GROUPED: return "MoE not grouped"; + case PN_GRAPH_INELIGIBLE_SLOT_BASE: return "slot_base>0"; + default: return "none"; + } +} + +int pn_graph_capture_inject_failures(void) +{ + static int cached = -1; + if (cached < 0) { + const char *env = getenv("QWEN35_GRAPH_CAPTURE_FAIL_INJECT"); + cached = env ? atoi(env) : 0; + if (cached < 0) + cached = 0; + } + return cached; +} + +int pn_graph_capture_inject_now(const pn_graph_capture_tracker *t) +{ + int n = pn_graph_capture_inject_failures(); + if (n <= 0 || !t) + return 0; + return (int)t->lifetime_failures < n; +} diff --git a/src/models/gpu_graph_capture_tracker.h b/src/models/gpu_graph_capture_tracker.h new file mode 100644 index 0000000..d6cd54a --- /dev/null +++ b/src/models/gpu_graph_capture_tracker.h @@ -0,0 +1,197 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gpu_graph_capture_tracker.h + * @brief Per-layer CUDA-graph capture state: bounded retry after a FAILED + * capture, a hard stop for a capture that will never work, and the + * three-state residency counters that make graph loss observable. + * + * WHY THIS EXISTS + * A captured CUDA graph collapses a layer's ~13-30 kernel launches into one + * replay and is worth ~1.8 ms/step across the 40 layers (+3.8% at K=1, + * +2.4% at K=2 aggregate). Capture can fail for reasons that have nothing to + * do with this layer: until foundry e6af3d4 any other host thread's + * cuMemAlloc/cuStreamSynchronize could invalidate an in-flight capture + * process-wide, and an /admin/models/unload during another model's decode + * still legitimately can. + * + * The forward used to latch a single `graph_tried` flag, cleared only in the + * drop branches — every one of which requires an EXISTING graph. So a capture + * that FAILED could never re-arm: one transient collision cost that layer its + * graph for the life of the process. A long-lived serve ratcheted downward + * and never recovered, with correct output and no metric saying so. + * + * THE TWO CASES, KEPT APART + * FAILED — a capture was attempted and did not produce a live graph. + * Might be a transient collision, so it is retried on an + * exponential backoff (8, 32, 128, 512 eligible steps) and then + * given up on permanently. A capture that succeeds resets the + * budget, so recurring collisions never exhaust it. + * INELIGIBLE — a gate says this layer does not capture at all (MoE not + * grouped, QWEN35_GRAPH=0, sprof active, slot_base>0, ...). + * NO capture is ever attempted, so there is nothing to retry + * and nothing to back off. A Q5_K_M model reporting 40 + * ineligible layers is correct behaviour, not a fault. + * + * Thread-safe: no — one tracker belongs to one layer on one die, touched only + * by that die's driver thread. The residency roll-up reads them without a lock + * and may therefore observe a step-boundary-torn snapshot; every field is a + * single word and the counts are advisory, so a torn read costs an off-by-one + * gauge reading and nothing else. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_GPU_GRAPH_CAPTURE_TRACKER_H +#define PN_GPU_GRAPH_CAPTURE_TRACKER_H + +/* Lifecycle state of ONE graph slot (one layer, one code path). */ +typedef enum { + PN_GRAPH_CAPTURE_PENDING = 0, /* eligible; no capture attempted yet */ + PN_GRAPH_CAPTURE_LIVE, /* a captured graph is replaying */ + PN_GRAPH_CAPTURE_RETRYING, /* capture failed; backing off, will retry */ + PN_GRAPH_CAPTURE_GAVE_UP, /* capture failed the retry budget; stopped */ + PN_GRAPH_CAPTURE_INELIGIBLE /* gated out; no capture is ever attempted */ +} pn_graph_capture_state; + +/* Why a slot is INELIGIBLE. Ordered so the FIRST failing gate wins, which is + * how the caller evaluates them. */ +typedef enum { + PN_GRAPH_INELIGIBLE_NONE = 0, + PN_GRAPH_INELIGIBLE_GRAPH_OFF, /* QWEN35_GRAPH=0 */ + PN_GRAPH_INELIGIBLE_SLOTS_OFF, /* QWEN35_GRAPH_SLOTS=0 */ + PN_GRAPH_INELIGIBLE_ATTN_OFF, /* QWEN35_GRAPH_SLOTS_ATTN=0 */ + PN_GRAPH_INELIGIBLE_PROFILING, /* sprof/prof/debug syncs mid-body */ + PN_GRAPH_INELIGIBLE_MOE_NOT_GROUPED,/* expert quant is not Q3_K */ + PN_GRAPH_INELIGIBLE_SLOT_BASE /* slot_base>0 group runs direct */ +} pn_graph_ineligible_reason; + +/* Warm-up (pos<1, body not yet run directly) is deliberately NOT an + * ineligibility reason: it clears by itself within a step or two, and counting + * it as "will never capture" would make a healthy serve's first readings lie. + * A warming slot simply stays PENDING. */ + +/* Retry budget: 4 retries after the first failure, then GAVE_UP. The backoff + * is counted in ELIGIBLE STEPS (steps where this layer ran and could have + * captured), so it is a fixed amortised cost regardless of token rate: a + * genuinely uncapturable layer spends 5 wasted begin/end pairs over ~680 steps + * and then costs nothing at all. */ +#define PN_GRAPH_CAPTURE_MAX_FAILURES 5 + +/* Per-layer, per-path capture tracker. Zero-initialised = PENDING, which is + * what calloc of the layer array already gives. */ +typedef struct { + unsigned char state; /* pn_graph_capture_state */ + unsigned char ineligible_reason; /* pn_graph_ineligible_reason */ + unsigned char consecutive_failures; + unsigned char evaluated; /* the forward has reached this slot */ + unsigned short retry_cooldown_steps; /* eligible steps left before retry */ + unsigned int lifetime_captures; /* successful captures */ + unsigned int lifetime_failures; /* failed capture attempts */ +} pn_graph_capture_tracker; + +/* Three-state residency roll-up over a set of trackers. `total` counts only + * slots the forward has actually REACHED (`evaluated`) — a model that decodes + * batched never touches its single-token slots, and reporting those as pending + * graphs would be an invented deficit. live + failed + ineligible + pending == + * total. */ +typedef struct { + int total; + int live; /* holding a captured graph right now */ + int failed; /* attempted, no graph (RETRYING or GAVE_UP) */ + int gave_up; /* subset of `failed`: retry budget exhausted */ + int ineligible; /* gated out; will never attempt a capture */ + int pending; /* eligible, not yet attempted */ + unsigned int lifetime_captures; + unsigned int lifetime_failures; +} pn_graph_residency; + +/* One model's graph residency, split by the code path each slot belongs to. + * `all` is the sum; a class the model has never exercised reports total 0 + * rather than a fabricated deficit (see `evaluated` above). */ +typedef struct { + pn_graph_residency all; + pn_graph_residency batched_attn; /* per-layer graph, batched decode */ + pn_graph_residency batched_ssm; /* per-layer graph, batched decode */ + pn_graph_residency single_ssm; /* per-layer graph, single-token decode */ + pn_graph_residency segment; /* per-die full-step graph (GRAPH_FULL) */ +} pn_model_graph_residency; + +/** + * Should a capture be attempted on this eligible step? + * + * Call ONCE per step on a slot that is eligible and holds no live graph. It + * owns the whole backoff: on a cooldown step it decrements and answers no. + * + * @param t Tracker for this layer's graph slot + * @return 1 to attempt a capture now, 0 to run the body directly this step + * + * Thread-safe: no (single owning driver thread) + * Reentrant: no (mutates the tracker) + */ +int pn_graph_capture_should_attempt(pn_graph_capture_tracker *t); + +/* A capture succeeded and the graph is live. Clears the failure budget: a slot + * that has ever captured is proven capturable, so a later drop starts fresh. */ +void pn_graph_capture_succeeded(pn_graph_capture_tracker *t); + +/* A capture attempt did not produce a live, replayable graph. Arms the next + * backoff, or gives up once the budget is spent. */ +void pn_graph_capture_failed(pn_graph_capture_tracker *t); + +/* A LIVE graph was dropped for a legitimate reason (a capture key changed, or a + * replay failed). Re-arms an immediate attempt with a fresh budget — this is + * not a capture failure. */ +void pn_graph_capture_dropped(pn_graph_capture_tracker *t); + +/* This slot's eligibility gate failed. Records the reason and attempts + * nothing; safe to call every step. A LIVE graph or a spent retry budget is + * left alone — both are a truer description of the slot than "ineligible" — + * and the failure budget is preserved, so a slot that is only TEMPORARILY + * gated out (sprof active, this step's slot_base>0) resumes where it left off + * when the gate reopens. */ +void pn_graph_capture_ineligible(pn_graph_capture_tracker *t, + pn_graph_ineligible_reason reason); + +/** + * This slot has been SUPERSEDED by a graph at a coarser granularity — the + * per-die full-step segment graph swallowed the per-layer path, so those layer + * slots are no longer part of any decode step. + * + * Without this a slot frozen mid-retry keeps reporting a deficit it can never + * work off, because the code that would retry it is never reached again. The + * measured symptom: a serve whose segment graphs were all live still read + * `live 2 / total 32 / failed 30`. The lifetime capture/failure counters are + * deliberately KEPT — what happened, happened — but the slot drops out of the + * live/total headline until the coarser graph is dropped and the per-layer path + * runs again, which re-evaluates it. + */ +void pn_graph_capture_superseded(pn_graph_capture_tracker *t); + +/* Fold ONE tracker into a residency roll-up. Zero the roll-up first. */ +void pn_graph_residency_add(pn_graph_residency *out, + const pn_graph_capture_tracker *t); + +/* Short stable names for logs and JSON. */ +const char *pn_graph_capture_state_name(unsigned char state); +const char *pn_graph_ineligible_reason_name(unsigned char reason); + +/** + * Test-only fault injection: force the first N capture attempts of EVERY slot + * to be treated as failures, so the retry path can be exercised end to end on + * real hardware. Read once from QWEN35_GRAPH_CAPTURE_FAIL_INJECT; 0 (unset) is + * the production path and costs one cached compare. + * + * @return How many attempts per slot to fail, 0 = never inject + */ +int pn_graph_capture_inject_failures(void); + +/* Should THIS slot's current capture attempt be forced to fail? Keyed off the + * slot's own lifetime failure count, so each slot fails exactly N times and + * then behaves normally. Injecting N < PN_GRAPH_CAPTURE_MAX_FAILURES exercises + * recovery; injecting more exercises the give-up path. */ +int pn_graph_capture_inject_now(const pn_graph_capture_tracker *t); + +#endif /* PN_GPU_GRAPH_CAPTURE_TRACKER_H */ diff --git a/src/models/llama.c b/src/models/llama.c new file mode 100644 index 0000000..239184c --- /dev/null +++ b/src/models/llama.c @@ -0,0 +1,3066 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "llama.h" +#include "llama_attention_trace.h" +#include "llama_residual_trace.h" +#include "../core/engine.h" +#include "../core/qweight.h" +#include "../memory/arena.h" +#include "../accel/simd.h" +#include "../formats/gguf_dequant.h" +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif +#include +#include +#include +#include + +#ifdef USE_BLAS + #if defined(__APPLE__) + #include + #else + #include + #endif +#endif +#ifdef USE_METAL + #include "metal/fm_metal.h" +#endif + +/* ============================================================ + * Config reading from GGUF (shared by both loaders) + * ============================================================ */ + +static int read_config(pn_gguf_file *gguf, pn_config *config, const char *prefix) { + char key[128]; + + snprintf(key, sizeof(key), "%s.vocab_size", prefix); + config->n_vocab = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.embedding_length", prefix); + config->n_embd = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.head_count", prefix); + config->n_head = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.head_count_kv", prefix); + config->n_kv_head = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.block_count", prefix); + config->n_layer = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.feed_forward_length", prefix); + config->n_ff = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.context_length", prefix); + config->n_ctx = (int)pn_gguf_get_u32(gguf, key, 2048); + snprintf(key, sizeof(key), "%s.rope.freq_base", prefix); + config->rope_theta = pn_gguf_get_f32(gguf, key, 10000.0f); + snprintf(key, sizeof(key), "%s.attention.layer_norm_rms_epsilon", prefix); + config->rms_norm_eps = pn_gguf_get_f32(gguf, key, 1e-5f); + snprintf(key, sizeof(key), "%s.attention.sliding_window", prefix); + config->sliding_window = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.rope.dimension_count", prefix); + config->rope_dim = (int)pn_gguf_get_u32(gguf, key, 0); + + /* Qwen3 decouples head_dim from n_embd/n_head (stored as attention.key_length). */ + snprintf(key, sizeof(key), "%s.attention.key_length", prefix); + config->head_dim = (int)pn_gguf_get_u32(gguf, key, 0); + + snprintf(key, sizeof(key), "%s.expert_count", prefix); + config->n_experts = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.expert_used_count", prefix); + config->n_experts_used = (int)pn_gguf_get_u32(gguf, key, 0); + + if (config->n_vocab == 0) { + pn_gguf_tensor_info *embd = pn_gguf_find_tensor(gguf, "token_embd.weight"); + if (embd && embd->n_dims >= 2) { + config->n_vocab = (int)embd->dims[embd->n_dims - 1]; + } + } + + if (config->n_embd == 0 || config->n_head == 0 || config->n_layer == 0) { + fprintf(stderr, "%s: invalid config (n_embd=%d, n_head=%d, n_layer=%d)\n", + prefix, config->n_embd, config->n_head, config->n_layer); + return -1; + } + + if (config->n_kv_head == 0) config->n_kv_head = config->n_head; + + /* Qwen3 adds per-head RMSNorm on Q and K (detected by the norm tensors). */ + config->qk_norm = pn_gguf_find_tensor(gguf, "blk.0.attn_q_norm.weight") != NULL; + + fprintf(stderr, "%s: n_vocab=%d n_embd=%d n_head=%d n_kv_head=%d head_dim=%d qk_norm=%d rope_dim=%d theta=%.0f n_layer=%d n_ff=%d sw=%d", + prefix, config->n_vocab, config->n_embd, config->n_head, + config->n_kv_head, pn_head_dim(config), config->qk_norm, config->rope_dim, + config->rope_theta, config->n_layer, config->n_ff, config->sliding_window); + if (config->n_experts > 0) + fprintf(stderr, " n_experts=%d n_experts_used=%d", config->n_experts, config->n_experts_used); + fprintf(stderr, "\n"); + return 0; +} + +/* ============================================================ + * Helper: dequantize a GGUF tensor to F32 float array (owned) + * ============================================================ */ + +static float* dequant_to_f32(pn_gguf_file *gguf, const char *name, size_t *numel_out) { + return pn_gguf_dequantize_tensor(gguf, name, numel_out); +} + +/* ============================================================ + * Helper: dequantize a GGUF tensor into a Foundry tm_tensor + * (legacy, used by F32 loader) + * ============================================================ */ + +static tm_tensor* gguf_to_tensor(pn_gguf_file *gguf, const char *name) { + pn_gguf_tensor_info *info = pn_gguf_find_tensor(gguf, name); + if (!info) { + fprintf(stderr, "llama: tensor '%s' not found in GGUF\n", name); + return NULL; + } + + int64_t shape[4]; + for (uint32_t d = 0; d < info->n_dims; d++) { + shape[d] = (int64_t)info->dims[info->n_dims - 1 - d]; + } + + if (info->type == GGUF_QUANT_F32) { + void *raw = (char *)gguf->data + info->offset; + tm_tensor *t = tm_from_data(raw, shape, (int)info->n_dims, TM_F32); + if (!t) return NULL; + tm_tensor *owned = tm_clone(t); + tm_free(t); + return owned; + } + + size_t out_numel = 0; + float *data = pn_gguf_dequantize_tensor(gguf, name, &out_numel); + if (!data) return NULL; + + tm_tensor *t = tm_from_data(data, shape, (int)info->n_dims, TM_F32); + if (!t) { + free(data); + return NULL; + } + tm_tensor *owned = tm_clone(t); + tm_free(t); + free(data); + return owned; +} + +/* ============================================================ + * Quantized weight loading from GGUF (Phase 2) + * ============================================================ */ + +/* Check if a quant type is supported for direct qmv */ +static int is_supported_quant(pn_gguf_quant type) { + switch (type) { + case GGUF_QUANT_F32: + case GGUF_QUANT_F16: + case GGUF_QUANT_Q4_0: + case GGUF_QUANT_Q8_0: + case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: + case GGUF_QUANT_Q6_K: + return 1; + default: + return 0; + } +} + +static const char* arch_prefix(pn_arch arch) { + switch (arch) { + case PN_ARCH_QWEN2: return "qwen2"; + case PN_ARCH_QWEN3: return "qwen3"; + case PN_ARCH_PHI: return "phi3"; + case PN_ARCH_MIXTRAL: return "llama"; + default: return "llama"; + } +} + +pn_llama_qweights* pn_llama_load_gguf_quantized(pn_gguf_file *gguf, pn_config *config) { + if (!gguf || !config) return NULL; + + if (read_config(gguf, config, arch_prefix(config->arch)) < 0) return NULL; + + /* Check that the first layer's weight type is supported */ + pn_gguf_tensor_info *test_info = pn_gguf_find_tensor(gguf, "blk.0.attn_q.weight"); + if (!test_info || !is_supported_quant(test_info->type)) { + fprintf(stderr, "llama: quant type %d not supported for quantized path\n", + test_info ? test_info->type : -1); + return NULL; + } + + pn_llama_qweights *qw = calloc(1, sizeof(pn_llama_qweights)); + if (!qw) return NULL; + qw->is_quantized = 1; + qw->n_layers = config->n_layer; + qw->n_embd = config->n_embd; + qw->n_vocab = config->n_vocab; + qw->gguf = gguf; + + /* Token embeddings: always dequant to F32 (needed for embedding lookup) */ + size_t embd_numel = 0; + qw->tok_embd = dequant_to_f32(gguf, "token_embd.weight", &embd_numel); + if (!qw->tok_embd) { + fprintf(stderr, "llama: failed to load token_embd.weight\n"); + pn_llama_qweights_free(qw); + return NULL; + } + + /* Detect actual vocab size from embedding */ + if (config->n_vocab == 0 && embd_numel > 0 && config->n_embd > 0) { + config->n_vocab = (int)(embd_numel / (size_t)config->n_embd); + qw->n_vocab = config->n_vocab; + } + + /* Output norm: always F32 */ + size_t norm_numel = 0; + qw->output_norm = dequant_to_f32(gguf, "output_norm.weight", &norm_numel); + if (!qw->output_norm) { + fprintf(stderr, "llama: failed to load output_norm.weight\n"); + pn_llama_qweights_free(qw); + return NULL; + } + + /* Output head: try as qweight (quantized mmap), fall back to tied embeddings */ + pn_gguf_tensor_info *out_info = pn_gguf_find_tensor(gguf, "output.weight"); + if (out_info && is_supported_quant(out_info->type)) { + if (pn_qweight_from_gguf(&qw->output, gguf, "output.weight") < 0) { + pn_llama_qweights_free(qw); + return NULL; + } + } else { + /* Tied embeddings: reuse the quantized token_embd tensor directly as the + * LM head instead of materializing a ~622 MB F32 copy. Its GGUF dims + * reverse to rows=n_vocab, cols=n_embd (matching the manual setup this + * replaces), and pn_qweight_from_gguf points qw->output at the mmapped + * Q6_K blocks. The decode paths take the quantized branch: single-stream + * pn_qmv(&qw->output, ...) and batched pn_qmm(&qw->output, ...) (the F32 + * GEMM head is only taken when qw->output.type == GGUF_QUANT_F32). + * qw->output_f32 stays NULL, so no F32 head buffer is allocated. */ + if (pn_qweight_from_gguf(&qw->output, gguf, "token_embd.weight") < 0) { + pn_llama_qweights_free(qw); + return NULL; + } + } + + /* Per-layer weights */ + qw->layers = calloc(config->n_layer, sizeof(pn_llama_qlayer)); + if (!qw->layers) { + pn_llama_qweights_free(qw); + return NULL; + } + + for (int i = 0; i < config->n_layer; i++) { + char name[128]; + pn_llama_qlayer *l = &qw->layers[i]; + + /* Norms: always dequant to F32 */ + snprintf(name, sizeof(name), "blk.%d.attn_norm.weight", i); + l->attn_norm = dequant_to_f32(gguf, name, NULL); + if (!l->attn_norm) { + fprintf(stderr, "llama: failed to load %s\n", name); + pn_llama_qweights_free(qw); + return NULL; + } + + snprintf(name, sizeof(name), "blk.%d.ffn_norm.weight", i); + l->ffn_norm = dequant_to_f32(gguf, name, NULL); + if (!l->ffn_norm) { + fprintf(stderr, "llama: failed to load %s\n", name); + pn_llama_qweights_free(qw); + return NULL; + } + + /* Projection weights: keep quantized in mmap */ + #define LOAD_QW(field, fmt) do { \ + snprintf(name, sizeof(name), fmt, i); \ + if (pn_qweight_from_gguf(&l->field, gguf, name) < 0) { \ + fprintf(stderr, "llama: failed to load %s\n", name); \ + pn_llama_qweights_free(qw); \ + return NULL; \ + } \ + } while(0) + + /* Attention weights (shared by all architectures) */ + LOAD_QW(wq, "blk.%d.attn_q.weight"); + LOAD_QW(wk, "blk.%d.attn_k.weight"); + LOAD_QW(wv, "blk.%d.attn_v.weight"); + LOAD_QW(wo, "blk.%d.attn_output.weight"); + + if (config->n_experts > 0) { + /* MoE: load router + expert weights */ + LOAD_QW(ffn_gate_inp, "blk.%d.ffn_gate_inp.weight"); + + int n_exp = config->n_experts; + l->experts = calloc(n_exp, sizeof(pn_moe_expert)); + if (!l->experts) { + pn_llama_qweights_free(qw); + return NULL; + } + + /* Try merged format first: blk.N.ffn_gate_exps.weight */ + snprintf(name, sizeof(name), "blk.%d.ffn_gate_exps.weight", i); + pn_gguf_tensor_info *gate_exps_info = pn_gguf_find_tensor(gguf, name); + + if (gate_exps_info) { + /* Merged format: 3D tensors [n_experts, n_ff/n_embd, cols] */ + pn_gguf_tensor_info *up_exps_info, *down_exps_info; + snprintf(name, sizeof(name), "blk.%d.ffn_up_exps.weight", i); + up_exps_info = pn_gguf_find_tensor(gguf, name); + snprintf(name, sizeof(name), "blk.%d.ffn_down_exps.weight", i); + down_exps_info = pn_gguf_find_tensor(gguf, name); + + if (!up_exps_info || !down_exps_info) { + fprintf(stderr, "llama: layer %d missing merged expert tensors\n", i); + pn_llama_qweights_free(qw); + return NULL; + } + + /* Compute row strides for slicing */ + size_t gate_bpb = pn_gguf_bytes_per_block(gate_exps_info->type); + int gate_bs = pn_gguf_block_size(gate_exps_info->type); + size_t down_bpb = pn_gguf_bytes_per_block(down_exps_info->type); + int down_bs = pn_gguf_block_size(down_exps_info->type); + + /* gate_exps/up_exps: GGUF dims [n_embd, n_ff, n_experts] innermost-first + * Per expert: rows=n_ff, cols=n_embd, row_stride based on n_embd */ + int gate_cols = config->n_embd; + int gate_rows = config->n_ff; + size_t gate_row_stride = ((size_t)gate_cols / (size_t)gate_bs) * gate_bpb; + size_t gate_expert_stride = (size_t)gate_rows * gate_row_stride; + + /* down_exps: GGUF dims [n_ff, n_embd, n_experts] innermost-first + * Per expert: rows=n_embd, cols=n_ff, row_stride based on n_ff */ + int down_cols = config->n_ff; + int down_rows = config->n_embd; + size_t down_row_stride = ((size_t)down_cols / (size_t)down_bs) * down_bpb; + size_t down_expert_stride = (size_t)down_rows * down_row_stride; + + const char *gate_base = (const char *)gguf->data + gate_exps_info->offset; + const char *up_base = (const char *)gguf->data + up_exps_info->offset; + const char *down_base = (const char *)gguf->data + down_exps_info->offset; + + for (int e = 0; e < n_exp; e++) { + pn_moe_expert *exp = &l->experts[e]; + + exp->w_gate.data = gate_base + e * gate_expert_stride; + exp->w_gate.type = gate_exps_info->type; + exp->w_gate.rows = gate_rows; + exp->w_gate.cols = gate_cols; + exp->w_gate.row_stride = gate_row_stride; + + exp->w_up.data = up_base + e * gate_expert_stride; + exp->w_up.type = up_exps_info->type; + exp->w_up.rows = gate_rows; + exp->w_up.cols = gate_cols; + exp->w_up.row_stride = gate_row_stride; + + exp->w_down.data = down_base + e * down_expert_stride; + exp->w_down.type = down_exps_info->type; + exp->w_down.rows = down_rows; + exp->w_down.cols = down_cols; + exp->w_down.row_stride = down_row_stride; + } + } else { + /* Split format: blk.N.ffn_gate.E.weight per expert */ + for (int e = 0; e < n_exp; e++) { + pn_moe_expert *exp = &l->experts[e]; + + snprintf(name, sizeof(name), "blk.%d.ffn_gate.%d.weight", i, e); + if (pn_qweight_from_gguf(&exp->w_gate, gguf, name) < 0) { + fprintf(stderr, "llama: failed to load %s\n", name); + pn_llama_qweights_free(qw); + return NULL; + } + snprintf(name, sizeof(name), "blk.%d.ffn_up.%d.weight", i, e); + if (pn_qweight_from_gguf(&exp->w_up, gguf, name) < 0) { + fprintf(stderr, "llama: failed to load %s\n", name); + pn_llama_qweights_free(qw); + return NULL; + } + snprintf(name, sizeof(name), "blk.%d.ffn_down.%d.weight", i, e); + if (pn_qweight_from_gguf(&exp->w_down, gguf, name) < 0) { + fprintf(stderr, "llama: failed to load %s\n", name); + pn_llama_qweights_free(qw); + return NULL; + } + } + } + } else { + /* Standard FFN weights (non-MoE) */ + LOAD_QW(w_gate, "blk.%d.ffn_gate.weight"); + LOAD_QW(w_up, "blk.%d.ffn_up.weight"); + LOAD_QW(w_down, "blk.%d.ffn_down.weight"); + } + + #undef LOAD_QW + + /* Optional attention biases (Qwen2 has them, Llama doesn't) */ + snprintf(name, sizeof(name), "blk.%d.attn_q.bias", i); + if (pn_gguf_find_tensor(gguf, name)) + l->bq = dequant_to_f32(gguf, name, NULL); + + snprintf(name, sizeof(name), "blk.%d.attn_k.bias", i); + if (pn_gguf_find_tensor(gguf, name)) + l->bk = dequant_to_f32(gguf, name, NULL); + + snprintf(name, sizeof(name), "blk.%d.attn_v.bias", i); + if (pn_gguf_find_tensor(gguf, name)) + l->bv = dequant_to_f32(gguf, name, NULL); + + snprintf(name, sizeof(name), "blk.%d.attn_output.bias", i); + if (pn_gguf_find_tensor(gguf, name)) + l->bo = dequant_to_f32(gguf, name, NULL); + + /* Optional per-head Q/K RMSNorm weights (Qwen3) */ + snprintf(name, sizeof(name), "blk.%d.attn_q_norm.weight", i); + if (pn_gguf_find_tensor(gguf, name)) + l->attn_q_norm = dequant_to_f32(gguf, name, NULL); + snprintf(name, sizeof(name), "blk.%d.attn_k_norm.weight", i); + if (pn_gguf_find_tensor(gguf, name)) + l->attn_k_norm = dequant_to_f32(gguf, name, NULL); + + fprintf(stderr, "\r loaded layer %d/%d", i + 1, config->n_layer); + fflush(stderr); + } + fprintf(stderr, "\n"); + + return qw; +} + +/* ============================================================ + * F32 weight loading from GGUF (Phase 1 legacy) + * ============================================================ */ + +pn_llama_weights* pn_llama_load_gguf(pn_gguf_file *gguf, pn_config *config) { + if (!gguf || !config) return NULL; + + if (read_config(gguf, config, arch_prefix(config->arch)) < 0) return NULL; + + pn_llama_weights *w = calloc(1, sizeof(pn_llama_weights)); + if (!w) return NULL; + w->n_layers = config->n_layer; + w->gguf = gguf; + + w->tok_embd = gguf_to_tensor(gguf, "token_embd.weight"); + w->output_norm = gguf_to_tensor(gguf, "output_norm.weight"); + + w->output = gguf_to_tensor(gguf, "output.weight"); + if (!w->output && w->tok_embd) { + w->output = tm_clone(w->tok_embd); + } + + if (!w->tok_embd || !w->output_norm || !w->output) { + fprintf(stderr, "llama: failed to load global tensors\n"); + pn_llama_weights_free(w); + return NULL; + } + + if (config->n_vocab == 0) { + config->n_vocab = (int)w->tok_embd->shape[0]; + } + + w->layers = calloc(config->n_layer, sizeof(pn_llama_layer)); + if (!w->layers) { + pn_llama_weights_free(w); + return NULL; + } + + for (int i = 0; i < config->n_layer; i++) { + char name[128]; + pn_llama_layer *l = &w->layers[i]; + + #define LOAD_LAYER(field, fmt) do { \ + snprintf(name, sizeof(name), fmt, i); \ + l->field = gguf_to_tensor(gguf, name); \ + if (!l->field) { \ + fprintf(stderr, "llama: failed to load %s\n", name); \ + pn_llama_weights_free(w); \ + return NULL; \ + } \ + } while(0) + + LOAD_LAYER(attn_norm, "blk.%d.attn_norm.weight"); + LOAD_LAYER(wq, "blk.%d.attn_q.weight"); + LOAD_LAYER(wk, "blk.%d.attn_k.weight"); + LOAD_LAYER(wv, "blk.%d.attn_v.weight"); + LOAD_LAYER(wo, "blk.%d.attn_output.weight"); + LOAD_LAYER(ffn_norm, "blk.%d.ffn_norm.weight"); + LOAD_LAYER(w_gate, "blk.%d.ffn_gate.weight"); + LOAD_LAYER(w_up, "blk.%d.ffn_up.weight"); + LOAD_LAYER(w_down, "blk.%d.ffn_down.weight"); + + #undef LOAD_LAYER + + fprintf(stderr, "\r loaded layer %d/%d", i + 1, config->n_layer); + fflush(stderr); + } + fprintf(stderr, "\n"); + + return w; +} + +/* ============================================================ + * HuggingFace safetensors loading (BF16 + ternary) + * ============================================================ */ + +#include "safetensors/safetensors.h" +#include "tokenizer/tokenizer.h" + +/* Read entire file into malloc'd buffer, NUL-terminated */ +static char* read_file_text(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) return NULL; + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + char *buf = malloc(len + 1); + if (!buf) { fclose(f); return NULL; } + fread(buf, 1, len, f); + buf[len] = '\0'; + fclose(f); + return buf; +} + +/* Simple JSON helpers for config.json parsing */ +static const char* json_find(const char *json, const char *key) { + char search[256]; + snprintf(search, sizeof(search), "\"%s\"", key); + const char *p = strstr(json, search); + if (!p) return NULL; + p += strlen(search); + while (*p && (*p == ' ' || *p == ':' || *p == '\t' || *p == '\n' || *p == '\r')) p++; + return p; +} + +static int json_int(const char *json, const char *key, int def) { + const char *p = json_find(json, key); + if (!p) return def; + return atoi(p); +} + +static double json_float(const char *json, const char *key, double def) { + const char *p = json_find(json, key); + if (!p) return def; + return atof(p); +} + +static int json_bool(const char *json, const char *key, int def) { + const char *p = json_find(json, key); + if (!p) return def; + return strncmp(p, "true", 4) == 0; +} + +static pn_arch detect_arch_from_json(const char *json) { + const char *p = json_find(json, "model_type"); + if (!p || *p != '"') return PN_ARCH_LLAMA; + p++; + if (strncmp(p, "qwen2", 5) == 0) return PN_ARCH_QWEN2; + if (strncmp(p, "llama", 5) == 0) return PN_ARCH_LLAMA; + if (strncmp(p, "mistral", 7) == 0) return PN_ARCH_MISTRAL; + if (strncmp(p, "phi3", 4) == 0 || strncmp(p, "phi", 3) == 0) return PN_ARCH_PHI; + return PN_ARCH_LLAMA; +} + +static int read_config_json(const char *path, pn_config *config) { + char *json = read_file_text(path); + if (!json) { + fprintf(stderr, "llama_sf: failed to read config: %s\n", path); + return -1; + } + + config->arch = detect_arch_from_json(json); + config->n_embd = json_int(json, "hidden_size", 0); + config->n_head = json_int(json, "num_attention_heads", 0); + config->n_kv_head = json_int(json, "num_key_value_heads", config->n_head); + config->n_layer = json_int(json, "num_hidden_layers", 0); + config->n_ff = json_int(json, "intermediate_size", 0); + config->n_vocab = json_int(json, "vocab_size", 0); + config->n_ctx = json_int(json, "max_position_embeddings", 32768); + config->rope_theta = (float)json_float(json, "rope_theta", 10000.0); + config->rms_norm_eps = (float)json_float(json, "rms_norm_eps", 1e-6); + config->sliding_window = json_int(json, "sliding_window", 0); + config->rope_dim = 0; + config->n_experts = 0; + config->n_experts_used = 0; + + int tied = json_bool(json, "tie_word_embeddings", 0); + free(json); + + if (config->n_embd == 0 || config->n_head == 0 || config->n_layer == 0) { + fprintf(stderr, "llama_sf: invalid config (n_embd=%d, n_head=%d, n_layer=%d)\n", + config->n_embd, config->n_head, config->n_layer); + return -1; + } + + fprintf(stderr, "llama_sf: n_vocab=%d n_embd=%d n_head=%d n_kv=%d n_layer=%d n_ff=%d tied=%d\n", + config->n_vocab, config->n_embd, config->n_head, config->n_kv_head, + config->n_layer, config->n_ff, tied); + return tied ? 1 : 0; /* return tied flag */ +} + +/* BF16 -> F32 conversion */ +static inline float bf16_to_f32(uint16_t h) { + uint32_t bits = (uint32_t)h << 16; + float f; + memcpy(&f, &bits, sizeof(float)); + return f; +} + +/* Dequantize a safetensors BF16 tensor to F32 (owned malloc) */ +static float* sf_load_bf16(fs_file *sf, const char *name, int expected_numel) { + fs_tensor_info *info = fs_get_info(sf, name); + if (!info) return NULL; + + const void *raw = fs_get_data(sf, name); + if (!raw) { fs_free_info(info); return NULL; } + + size_t numel = 1; + for (int d = 0; d < info->ndim; d++) numel *= (size_t)info->shape[d]; + + if (expected_numel > 0 && (int)numel != expected_numel) { + fprintf(stderr, "llama_sf: %s: expected %d elements, got %zu\n", name, expected_numel, numel); + fs_free_info(info); + return NULL; + } + + float *out = malloc(numel * sizeof(float)); + if (!out) { fs_free_info(info); return NULL; } + + if (info->dtype == FS_DTYPE_BF16) { + const uint16_t *src = (const uint16_t *)raw; + for (size_t i = 0; i < numel; i++) out[i] = bf16_to_f32(src[i]); + } else if (info->dtype == FS_DTYPE_F16) { + const uint16_t *src = (const uint16_t *)raw; + for (size_t i = 0; i < numel; i++) out[i] = pn_half_to_float(src[i]); + } else if (info->dtype == FS_DTYPE_F32) { + memcpy(out, raw, numel * sizeof(float)); + } else { + fprintf(stderr, "llama_sf: %s: unsupported dtype %s\n", name, fs_dtype_name(info->dtype)); + free(out); + fs_free_info(info); + return NULL; + } + + fs_free_info(info); + return out; +} + +/* Load a BF16 weight matrix into a pn_qweight as F32 (owned) */ +static int sf_load_weight_f32(fs_file *sf, const char *name, pn_qweight *qw, int rows, int cols) { + float *data = sf_load_bf16(sf, name, rows * cols); + if (!data) return -1; + + qw->data = data; + qw->type = GGUF_QUANT_F32; + qw->rows = rows; + qw->cols = cols; + qw->row_stride = (size_t)cols * sizeof(float); + qw->scales = NULL; + qw->group_size = 0; + return 0; +} + +/* Load a ternary weight from packed U8 + scales + shape tensors */ +static int sf_load_weight_ternary(fs_file *sf, const char *base_name, + pn_qweight *qw, int group_size) { + char scale_name[256], shape_name[256]; + snprintf(scale_name, sizeof(scale_name), "%s.__scale", base_name); + snprintf(shape_name, sizeof(shape_name), "%s.__shape", base_name); + + /* Read shape */ + const int32_t *shape_data = fs_get_i32(sf, shape_name); + if (!shape_data) { + fprintf(stderr, "llama_sf: missing %s\n", shape_name); + return -1; + } + int rows = shape_data[0]; + int cols = shape_data[1]; + + /* Read packed ternary data (U8) — copy to owned memory */ + const void *packed_raw = fs_get_data(sf, base_name); + if (!packed_raw) { + fprintf(stderr, "llama_sf: missing %s\n", base_name); + return -1; + } + size_t packed_bytes_per_row = ((size_t)cols + 3) / 4; + size_t total_packed = (size_t)rows * packed_bytes_per_row; + uint8_t *packed = malloc(total_packed); + if (!packed) return -1; + memcpy(packed, packed_raw, total_packed); + + /* Read scales (F32) — copy to owned memory */ + int n_groups = (cols + group_size - 1) / group_size; + size_t total_scales = (size_t)rows * n_groups; + + fs_tensor_info *scale_info = fs_get_info(sf, scale_name); + if (!scale_info) { + fprintf(stderr, "llama_sf: missing %s\n", scale_name); + free(packed); + return -1; + } + const void *scale_raw = fs_get_data(sf, scale_name); + float *scales = malloc(total_scales * sizeof(float)); + if (!scales) { free(packed); fs_free_info(scale_info); return -1; } + memcpy(scales, scale_raw, total_scales * sizeof(float)); + fs_free_info(scale_info); + + qw->data = packed; + qw->type = PN_QUANT_TERNARY; + qw->rows = rows; + qw->cols = cols; + qw->row_stride = packed_bytes_per_row; + qw->scales = scales; + qw->group_size = group_size; + return 0; +} + +/* Permute Q/K weight rows from HF natural order to GGUF interleaved order. + * GGUF conversion interleaves first/second half of each head's dimensions + * so that RoPE can rotate consecutive pairs (i, i+1). + * Input layout per head: [elem0, elem1, ..., elemH/2-1, elemH/2, ..., elemH-1] + * Output layout per head: [elem0, elemH/2, elem1, elemH/2+1, ...] */ +static void permute_qk_for_rope(pn_qweight *qw, int n_heads) { + if (!qw->data || qw->type != GGUF_QUANT_F32) return; + int head_dim = qw->rows / n_heads; + int half = head_dim / 2; + int cols = qw->cols; + float *data = (float *)qw->data; + float *temp = malloc((size_t)head_dim * cols * sizeof(float)); + if (!temp) return; + + for (int h = 0; h < n_heads; h++) { + float *head_start = data + (size_t)h * head_dim * cols; + memcpy(temp, head_start, (size_t)head_dim * cols * sizeof(float)); + for (int i = 0; i < half; i++) { + memcpy(head_start + (size_t)(2 * i) * cols, + temp + (size_t)i * cols, (size_t)cols * sizeof(float)); + memcpy(head_start + (size_t)(2 * i + 1) * cols, + temp + (size_t)(i + half) * cols, (size_t)cols * sizeof(float)); + } + } + free(temp); +} + +/* Permute Q/K bias from HF to GGUF interleaved order (same as weight row permutation). */ +static void permute_qk_bias(float *bias, int n_heads, int head_dim) { + if (!bias) return; + int half = head_dim / 2; + float *temp = malloc((size_t)head_dim * sizeof(float)); + if (!temp) return; + for (int h = 0; h < n_heads; h++) { + float *head_start = bias + h * head_dim; + memcpy(temp, head_start, (size_t)head_dim * sizeof(float)); + for (int i = 0; i < half; i++) { + head_start[2 * i] = temp[i]; + head_start[2 * i + 1] = temp[i + half]; + } + } + free(temp); +} + +/* Permute Q/K weight rows for ternary (generic: works with any row_stride). + * Same interleaving as permute_qk_for_rope but operates on packed byte rows + * and also permutes per-row scale arrays. */ +static void permute_qk_ternary(pn_qweight *qw, int n_heads) { + if (!qw->data) return; + int head_dim = qw->rows / n_heads; + int half = head_dim / 2; + size_t rs = qw->row_stride; + uint8_t *data = (uint8_t *)qw->data; + int n_groups = (qw->cols + qw->group_size - 1) / qw->group_size; + + uint8_t *temp = malloc((size_t)head_dim * rs); + float *stemp = qw->scales ? malloc((size_t)head_dim * n_groups * sizeof(float)) : NULL; + if (!temp) return; + + for (int h = 0; h < n_heads; h++) { + uint8_t *hd = data + (size_t)h * head_dim * rs; + memcpy(temp, hd, (size_t)head_dim * rs); + for (int i = 0; i < half; i++) { + memcpy(hd + (size_t)(2 * i) * rs, temp + (size_t)i * rs, rs); + memcpy(hd + (size_t)(2 * i + 1) * rs, temp + (size_t)(i + half) * rs, rs); + } + if (qw->scales && stemp) { + float *hs = (float *)qw->scales + (size_t)h * head_dim * n_groups; + memcpy(stemp, hs, (size_t)head_dim * n_groups * sizeof(float)); + for (int i = 0; i < half; i++) { + memcpy(hs + (size_t)(2 * i) * n_groups, + stemp + (size_t)i * n_groups, (size_t)n_groups * sizeof(float)); + memcpy(hs + (size_t)(2 * i + 1) * n_groups, + stemp + (size_t)(i + half) * n_groups, (size_t)n_groups * sizeof(float)); + } + } + } + free(temp); + free(stemp); +} + +/* Free owned data in a pn_qweight (for safetensors path) */ +static void free_owned_qweight(pn_qweight *qw) { + if (qw->data) free((void *)qw->data); + if (qw->scales) free((void *)qw->scales); + memset(qw, 0, sizeof(*qw)); +} + +pn_llama_qweights* pn_llama_load_safetensors(const char *model_path, + const char *config_path, + pn_config *config) { + if (!model_path || !config_path || !config) return NULL; + + /* Parse config.json */ + int tied = read_config_json(config_path, config); + if (tied < 0) return NULL; + + /* Open safetensors */ + fs_file *sf = fs_open(model_path); + if (!sf) { + fprintf(stderr, "llama_sf: failed to open %s\n", model_path); + return NULL; + } + + /* Detect format: HuggingFace BF16 or ternarized */ + int is_ternary = fs_has_tensor(sf, "layers.0.self_attn.q_proj.weight.__scale"); + int is_hf = fs_has_tensor(sf, "model.layers.0.self_attn.q_proj.weight"); + + if (!is_ternary && !is_hf) { + fprintf(stderr, "llama_sf: unrecognized safetensors format\n"); + fs_close(sf); + return NULL; + } + + fprintf(stderr, "llama_sf: detected %s format\n", is_ternary ? "ternary" : "HF BF16"); + + pn_llama_qweights *qw = calloc(1, sizeof(pn_llama_qweights)); + if (!qw) { fs_close(sf); return NULL; } + qw->is_quantized = 1; + qw->n_layers = config->n_layer; + qw->n_embd = config->n_embd; + qw->n_vocab = config->n_vocab; + qw->gguf = NULL; /* signals owned data */ + + int n_embd = config->n_embd; + int n_head = config->n_head; + int n_kv = config->n_kv_head; + int head_dim = n_embd / n_head; + int n_ff = config->n_ff; + + /* Token embeddings (always F32/BF16, never ternary) */ + if (is_hf) { + qw->tok_embd = sf_load_bf16(sf, "model.embed_tokens.weight", config->n_vocab * n_embd); + } else { + qw->tok_embd = sf_load_bf16(sf, "embed_tokens.weight", config->n_vocab * n_embd); + } + if (!qw->tok_embd) { + fprintf(stderr, "llama_sf: failed to load embeddings\n"); + pn_llama_qweights_free(qw); + fs_close(sf); + return NULL; + } + + /* Final norm */ + if (is_hf) { + qw->output_norm = sf_load_bf16(sf, "model.norm.weight", n_embd); + } else { + qw->output_norm = sf_load_bf16(sf, "norm.weight", n_embd); + } + if (!qw->output_norm) { + fprintf(stderr, "llama_sf: failed to load output norm\n"); + pn_llama_qweights_free(qw); + fs_close(sf); + return NULL; + } + + /* LM head: check for lm_head.weight, else tie to embeddings */ + const char *head_name = is_hf ? "lm_head.weight" : "lm_head.weight"; + if (fs_has_tensor(sf, head_name) && !tied) { + float *head = sf_load_bf16(sf, head_name, config->n_vocab * n_embd); + if (head) { + qw->output_f32 = head; + qw->output.data = head; + qw->output.type = GGUF_QUANT_F32; + qw->output.rows = config->n_vocab; + qw->output.cols = n_embd; + qw->output.row_stride = (size_t)n_embd * sizeof(float); + qw->output.scales = NULL; + qw->output.group_size = 0; + } + } + if (!qw->output.data) { + /* Tied embeddings */ + size_t sz = (size_t)config->n_vocab * n_embd * sizeof(float); + qw->output_f32 = malloc(sz); + if (!qw->output_f32) { + pn_llama_qweights_free(qw); + fs_close(sf); + return NULL; + } + memcpy(qw->output_f32, qw->tok_embd, sz); + qw->output.data = qw->output_f32; + qw->output.type = GGUF_QUANT_F32; + qw->output.rows = config->n_vocab; + qw->output.cols = n_embd; + qw->output.row_stride = (size_t)n_embd * sizeof(float); + qw->output.scales = NULL; + qw->output.group_size = 0; + } + + /* Per-layer weights */ + qw->layers = calloc(config->n_layer, sizeof(pn_llama_qlayer)); + if (!qw->layers) { + pn_llama_qweights_free(qw); + fs_close(sf); + return NULL; + } + + for (int i = 0; i < config->n_layer; i++) { + pn_llama_qlayer *l = &qw->layers[i]; + char name[256]; + int ok = 1; + + if (is_hf) { + /* HuggingFace BF16 format */ + + /* Norms */ + snprintf(name, sizeof(name), "model.layers.%d.input_layernorm.weight", i); + l->attn_norm = sf_load_bf16(sf, name, n_embd); + snprintf(name, sizeof(name), "model.layers.%d.post_attention_layernorm.weight", i); + l->ffn_norm = sf_load_bf16(sf, name, n_embd); + if (!l->attn_norm || !l->ffn_norm) { ok = 0; goto layer_fail; } + + /* Attention projections */ + snprintf(name, sizeof(name), "model.layers.%d.self_attn.q_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->wq, n_head * head_dim, n_embd) == 0; + snprintf(name, sizeof(name), "model.layers.%d.self_attn.k_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->wk, n_kv * head_dim, n_embd) == 0; + snprintf(name, sizeof(name), "model.layers.%d.self_attn.v_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->wv, n_kv * head_dim, n_embd) == 0; + snprintf(name, sizeof(name), "model.layers.%d.self_attn.o_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->wo, n_embd, n_head * head_dim) == 0; + + /* FFN projections */ + snprintf(name, sizeof(name), "model.layers.%d.mlp.gate_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->w_gate, n_ff, n_embd) == 0; + snprintf(name, sizeof(name), "model.layers.%d.mlp.up_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->w_up, n_ff, n_embd) == 0; + snprintf(name, sizeof(name), "model.layers.%d.mlp.down_proj.weight", i); + ok = ok && sf_load_weight_f32(sf, name, &l->w_down, n_embd, n_ff) == 0; + + if (!ok) goto layer_fail; + + /* Permute Q/K weights from HF to GGUF interleaved layout for RoPE */ + permute_qk_for_rope(&l->wq, n_head); + permute_qk_for_rope(&l->wk, n_kv); + + /* Optional biases (Qwen2) */ + snprintf(name, sizeof(name), "model.layers.%d.self_attn.q_proj.bias", i); + if (fs_has_tensor(sf, name)) l->bq = sf_load_bf16(sf, name, n_head * head_dim); + snprintf(name, sizeof(name), "model.layers.%d.self_attn.k_proj.bias", i); + if (fs_has_tensor(sf, name)) l->bk = sf_load_bf16(sf, name, n_kv * head_dim); + snprintf(name, sizeof(name), "model.layers.%d.self_attn.v_proj.bias", i); + if (fs_has_tensor(sf, name)) l->bv = sf_load_bf16(sf, name, n_kv * head_dim); + + /* Permute Q/K biases to match permuted weights */ + if (l->bq) permute_qk_bias(l->bq, n_head, head_dim); + if (l->bk) permute_qk_bias(l->bk, n_kv, head_dim); + + } else { + /* Ternarized format */ + int gs = 128; /* default group_size from ternarizer */ + + /* Norms (always F32) */ + snprintf(name, sizeof(name), "layers.%d.input_layernorm.weight", i); + l->attn_norm = sf_load_bf16(sf, name, n_embd); + snprintf(name, sizeof(name), "layers.%d.post_attention_layernorm.weight", i); + l->ffn_norm = sf_load_bf16(sf, name, n_embd); + if (!l->attn_norm || !l->ffn_norm) { ok = 0; goto layer_fail; } + + /* Ternary weights (HF-style names from merge output) */ + snprintf(name, sizeof(name), "layers.%d.self_attn.q_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->wq, gs) == 0; + snprintf(name, sizeof(name), "layers.%d.self_attn.k_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->wk, gs) == 0; + snprintf(name, sizeof(name), "layers.%d.self_attn.v_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->wv, gs) == 0; + snprintf(name, sizeof(name), "layers.%d.self_attn.o_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->wo, gs) == 0; + snprintf(name, sizeof(name), "layers.%d.mlp.gate_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->w_gate, gs) == 0; + snprintf(name, sizeof(name), "layers.%d.mlp.up_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->w_up, gs) == 0; + snprintf(name, sizeof(name), "layers.%d.mlp.down_proj.weight", i); + ok = ok && sf_load_weight_ternary(sf, name, &l->w_down, gs) == 0; + + if (!ok) goto layer_fail; + + /* Permute ternary Q/K weights for interleaved RoPE */ + permute_qk_ternary(&l->wq, n_head); + permute_qk_ternary(&l->wk, n_kv); + + /* Load biases from original model if available */ + snprintf(name, sizeof(name), "layers.%d.self_attn.q_proj.bias", i); + if (fs_has_tensor(sf, name)) { + l->bq = sf_load_bf16(sf, name, n_head * head_dim); + if (l->bq) permute_qk_bias(l->bq, n_head, head_dim); + } + snprintf(name, sizeof(name), "layers.%d.self_attn.k_proj.bias", i); + if (fs_has_tensor(sf, name)) { + l->bk = sf_load_bf16(sf, name, n_kv * head_dim); + if (l->bk) permute_qk_bias(l->bk, n_kv, head_dim); + } + snprintf(name, sizeof(name), "layers.%d.self_attn.v_proj.bias", i); + if (fs_has_tensor(sf, name)) l->bv = sf_load_bf16(sf, name, n_kv * head_dim); + } + + fprintf(stderr, "\r loaded layer %d/%d", i + 1, config->n_layer); + fflush(stderr); + continue; + +layer_fail: + fprintf(stderr, "\nllama_sf: failed to load layer %d\n", i); + pn_llama_qweights_free(qw); + fs_close(sf); + return NULL; + } + fprintf(stderr, "\n"); + + fs_close(sf); + return qw; +} + +/* ============================================================ + * HuggingFace tokenizer loader (vocab.json + merges.txt) + * ============================================================ */ + +/* Find closing quote of a JSON string, handling escape sequences. + * p must point to the first char after the opening quote. + * Returns pointer to the closing quote, or NULL if not found. */ +static const char* json_skip_string(const char *p) { + while (*p && *p != '"') { + if (*p == '\\') { + p++; + if (!*p) return NULL; + if (*p == 'u') { + /* \uXXXX — skip 4 hex digits */ + for (int i = 0; i < 4 && p[1]; i++) p++; + } + } + p++; + } + return *p == '"' ? p : NULL; +} + +/* Unescape JSON string [start, end) into malloc'd buffer. + * Handles: \" \\ \/ \n \t \r \b \f \uXXXX (including surrogate pairs → UTF-8). + * Returns owned string, caller frees. */ +static char* json_unescape(const char *start, const char *end) { + size_t max_len = (size_t)(end - start); + char *out = malloc(max_len * 4 + 1); /* \uXXXX → up to 4 UTF-8 bytes */ + if (!out) return NULL; + + char *w = out; + const char *p = start; + while (p < end) { + if (*p == '\\' && p + 1 < end) { + p++; + switch (*p) { + case '"': *w++ = '"'; break; + case '\\': *w++ = '\\'; break; + case '/': *w++ = '/'; break; + case 'n': *w++ = '\n'; break; + case 't': *w++ = '\t'; break; + case 'r': *w++ = '\r'; break; + case 'b': *w++ = '\b'; break; + case 'f': *w++ = '\f'; break; + case 'u': { + if (p + 4 >= end) { p++; continue; } + unsigned cp = 0; + for (int i = 1; i <= 4; i++) { + char c = p[i]; + cp <<= 4; + if (c >= '0' && c <= '9') cp |= (unsigned)(c - '0'); + else if (c >= 'a' && c <= 'f') cp |= 10u + (unsigned)(c - 'a'); + else if (c >= 'A' && c <= 'F') cp |= 10u + (unsigned)(c - 'A'); + } + p += 4; /* p now at last hex digit */ + /* Handle UTF-16 surrogate pairs: \uD800-DBFF + \uDC00-DFFF */ + if (cp >= 0xD800 && cp <= 0xDBFF && + p + 6 < end && p[1] == '\\' && p[2] == 'u') { + unsigned lo = 0; + for (int i = 3; i <= 6; i++) { + char c = p[i]; + lo <<= 4; + if (c >= '0' && c <= '9') lo |= (unsigned)(c - '0'); + else if (c >= 'a' && c <= 'f') lo |= 10u + (unsigned)(c - 'a'); + else if (c >= 'A' && c <= 'F') lo |= 10u + (unsigned)(c - 'A'); + } + cp = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00); + p += 6; /* skip \uXXXX of low surrogate */ + } + /* Encode as UTF-8 */ + if (cp < 0x80) { + *w++ = (char)cp; + } else if (cp < 0x800) { + *w++ = (char)(0xC0 | (cp >> 6)); + *w++ = (char)(0x80 | (cp & 0x3F)); + } else if (cp < 0x10000) { + *w++ = (char)(0xE0 | (cp >> 12)); + *w++ = (char)(0x80 | ((cp >> 6) & 0x3F)); + *w++ = (char)(0x80 | (cp & 0x3F)); + } else { + *w++ = (char)(0xF0 | (cp >> 18)); + *w++ = (char)(0x80 | ((cp >> 12) & 0x3F)); + *w++ = (char)(0x80 | ((cp >> 6) & 0x3F)); + *w++ = (char)(0x80 | (cp & 0x3F)); + } + break; + } + default: *w++ = *p; break; + } + p++; + } else { + *w++ = *p++; + } + } + *w = '\0'; + return out; +} + +ft_tokenizer* pn_llama_load_hf_tokenizer(const char *dir_path, const pn_config *config) { + char vocab_path[1024], merges_path[1024]; + snprintf(vocab_path, sizeof(vocab_path), "%s/vocab.json", dir_path); + snprintf(merges_path, sizeof(merges_path), "%s/merges.txt", dir_path); + + /* Read vocab.json */ + char *vocab_json = read_file_text(vocab_path); + if (!vocab_json) { + fprintf(stderr, "llama_sf: failed to read %s\n", vocab_path); + return NULL; + } + + /* Count vocab size and allocate sorted array */ + int n_vocab = config->n_vocab; + if (n_vocab <= 0) n_vocab = 151936; /* Qwen2 default */ + + char **tokens = calloc(n_vocab, sizeof(char *)); + if (!tokens) { free(vocab_json); return NULL; } + + /* Parse vocab.json: find "token": id pairs. + * Properly handles JSON escape sequences (\", \\, \uXXXX, etc.) */ + const char *p = vocab_json; + int n_parsed = 0; + + /* Skip to opening brace */ + while (*p && *p != '{') p++; + if (*p == '{') p++; + + while (*p) { + /* Skip whitespace and commas */ + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r' || *p == ',') p++; + if (*p == '}' || *p == '\0') break; + + /* Expect opening quote of key */ + if (*p != '"') { p++; continue; } + p++; /* skip opening quote */ + + /* Find end of key string (handles \" and \\ correctly) */ + const char *key_start = p; + const char *key_end = json_skip_string(p); + if (!key_end) break; + p = key_end + 1; /* past closing quote */ + + /* Skip colon + whitespace */ + while (*p == ' ' || *p == '\t' || *p == ':') p++; + + /* Parse integer ID */ + if (*p >= '0' && *p <= '9') { + int id = atoi(p); + while (*p >= '0' && *p <= '9') p++; + if (id >= 0 && id < n_vocab) { + tokens[id] = json_unescape(key_start, key_end); + if (tokens[id]) n_parsed++; + } + } + } + fprintf(stderr, "llama_sf: parsed %d/%d tokens from vocab.json\n", n_parsed, n_vocab); + free(vocab_json); + + /* Create tokenizer and add tokens in order */ + ft_tokenizer *tok = ft_new(); + if (!tok) { + for (int i = 0; i < n_vocab; i++) free(tokens[i]); + free(tokens); + return NULL; + } + + for (int i = 0; i < n_vocab; i++) { + if (tokens[i]) { + ft_add_token(tok, tokens[i]); + free(tokens[i]); + } else { + /* Fill gaps with placeholder */ + char placeholder[32]; + snprintf(placeholder, sizeof(placeholder), "<|reserved_%d|>", i); + ft_add_token(tok, placeholder); + } + } + free(tokens); + + /* Load merges.txt */ + char *merges = read_file_text(merges_path); + if (merges) { + char *line = merges; + int n_merges = 0; + while (*line) { + char *eol = strchr(line, '\n'); + if (!eol) eol = line + strlen(line); + size_t line_len = (size_t)(eol - line); + + /* Skip comment lines and empty lines */ + if (line_len > 0 && line[0] != '#') { + /* Find space separator */ + char *space = NULL; + for (char *s = line; s < eol; s++) { + if (*s == ' ') { space = s; break; } + } + if (space && space > line && space < eol - 1) { + *space = '\0'; + char saved = *eol; + *eol = '\0'; + ft_add_merge(tok, line, space + 1); + *eol = saved; + *space = ' '; + n_merges++; + } + } + + if (*eol) line = eol + 1; + else break; + } + fprintf(stderr, "llama_sf: loaded %d merges\n", n_merges); + free(merges); + } else { + fprintf(stderr, "llama_sf: no merges.txt found, BPE disabled\n"); + } + + /* Set BOS/EOS from config */ + int bos_id = json_int("{\"bos_token_id\": 151643}", "bos_token_id", 151643); + int eos_id = json_int("{\"eos_token_id\": 151643}", "eos_token_id", 151643); + (void)bos_id; + + const char *eos_tok = ft_id_to_token(tok, eos_id); + if (eos_tok) ft_set_eos_token(tok, eos_tok); + + fprintf(stderr, "llama_sf: tokenizer loaded (%d vocab)\n", n_vocab); + return tok; +} + +/* ============================================================ + * Free + * ============================================================ */ + +void pn_llama_weights_free(pn_llama_weights *w) { + if (!w) return; + if (w->tok_embd) tm_free(w->tok_embd); + if (w->output_norm) tm_free(w->output_norm); + if (w->output) tm_free(w->output); + + if (w->layers) { + for (int i = 0; i < w->n_layers; i++) { + pn_llama_layer *l = &w->layers[i]; + if (l->attn_norm) tm_free(l->attn_norm); + if (l->wq) tm_free(l->wq); + if (l->wk) tm_free(l->wk); + if (l->wv) tm_free(l->wv); + if (l->wo) tm_free(l->wo); + if (l->ffn_norm) tm_free(l->ffn_norm); + if (l->w_gate) tm_free(l->w_gate); + if (l->w_up) tm_free(l->w_up); + if (l->w_down) tm_free(l->w_down); + } + free(w->layers); + } + free(w); +} + +void pn_llama_qweights_free(pn_llama_qweights *qw) { + if (!qw) return; + free(qw->tok_embd); + free(qw->output_norm); + free(qw->output_f32); /* NULL-safe if tied embeddings */ + + if (qw->layers) { + int owned = (qw->gguf == NULL); /* safetensors path: data is owned */ + for (int i = 0; i < qw->n_layers; i++) { + free(qw->layers[i].attn_norm); + free(qw->layers[i].ffn_norm); + free(qw->layers[i].bq); + free(qw->layers[i].bk); + free(qw->layers[i].bv); + free(qw->layers[i].bo); + free(qw->layers[i].attn_q_norm); /* NULL-safe (Qwen3) */ + free(qw->layers[i].attn_k_norm); + free(qw->layers[i].experts); /* NULL-safe */ + + if (owned) { + free_owned_qweight(&qw->layers[i].wq); + free_owned_qweight(&qw->layers[i].wk); + free_owned_qweight(&qw->layers[i].wv); + free_owned_qweight(&qw->layers[i].wo); + free_owned_qweight(&qw->layers[i].w_gate); + free_owned_qweight(&qw->layers[i].w_up); + free_owned_qweight(&qw->layers[i].w_down); + } + } + free(qw->layers); + } + free(qw); +} + +/* ============================================================ + * KV Cache + * ============================================================ */ + +pn_llama_kv_cache* pn_llama_kv_cache_new(int n_layers, int n_kv_head, int max_seq, int head_dim) { + pn_llama_kv_cache *cache = calloc(1, sizeof(pn_llama_kv_cache)); + if (!cache) return NULL; + + cache->n_layers = n_layers; + cache->max_seq = max_seq; + cache->k_cache = calloc(n_layers, sizeof(tm_tensor *)); + cache->v_cache = calloc(n_layers, sizeof(tm_tensor *)); + if (!cache->k_cache || !cache->v_cache) { + pn_llama_kv_cache_free(cache); + return NULL; + } + + int64_t shape[] = { n_kv_head, max_seq, head_dim }; + for (int i = 0; i < n_layers; i++) { + cache->k_cache[i] = tm_zeros(shape, 3, TM_F32); + cache->v_cache[i] = tm_zeros(shape, 3, TM_F32); + if (!cache->k_cache[i] || !cache->v_cache[i]) { + pn_llama_kv_cache_free(cache); + return NULL; + } + } + + return cache; +} + +void pn_llama_kv_cache_free(pn_llama_kv_cache *cache) { + if (!cache) return; + if (cache->k_cache) { + for (int i = 0; i < cache->n_layers; i++) { + if (cache->k_cache[i]) tm_free(cache->k_cache[i]); + } + free(cache->k_cache); + } + if (cache->v_cache) { + for (int i = 0; i < cache->n_layers; i++) { + if (cache->v_cache[i]) tm_free(cache->v_cache[i]); + } + free(cache->v_cache); + } + free(cache); +} + +/* ============================================================ + * RoPE (Rotary Position Embeddings) — in-place on float* + * ============================================================ */ + +static void apply_rope_raw(float *data, int n_heads, int head_dim, + int rope_dim, int pos, float theta_base) { + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < rot; i += 2) { + float freq = 1.0f / powf(theta_base, (float)i / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle); + float sin_a = sinf(angle); + + float x0 = head[i]; + float x1 = head[i + 1]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + 1] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* NEOX-style RoPE: pairs dim i with i+half (half = rot/2). Qwen3 GGUF weights + * are stored unpermuted, so the rotation is NEOX (matching HF), unlike the + * interleaved layout the permuted Llama/Qwen2 GGUFs above expect. */ +static void apply_rope_neox(float *data, int n_heads, int head_dim, + int rope_dim, int pos, float theta_base) { + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + float freq = 1.0f / powf(theta_base, (float)(2 * i) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i]; + float x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* ============================================================ + * Attention head computation — for threading + * ============================================================ */ + +typedef struct { + const float *q_data; /* [n_head, head_dim] */ + const float *k_cache_data; /* [n_kv_head, max_seq, head_dim] */ + const float *v_cache_data; /* [n_kv_head, max_seq, head_dim] */ + float *attn_out; /* [n_head, head_dim] */ + float *scores_buf; /* [n_head, max_seq] pre-allocated */ + int head_dim; + int n_kv_groups; + int max_seq; + int seq_len; + float scale; +} attn_args; + +static void compute_attn_heads(int start, int end, void *arg) { + attn_args *a = (attn_args *)arg; + + for (int qh = start; qh < end; qh++) { + int kv_h = qh / a->n_kv_groups; + const float *q_vec = a->q_data + qh * a->head_dim; + const float *k_base = a->k_cache_data + kv_h * a->max_seq * a->head_dim; + const float *v_base = a->v_cache_data + kv_h * a->max_seq * a->head_dim; + float *scores = a->scores_buf + qh * a->max_seq; + float *out_vec = a->attn_out + qh * a->head_dim; + + /* Q @ K^T for each cached position */ + for (int s = 0; s < a->seq_len; s++) { + scores[s] = pn_dot_f32(q_vec, k_base + s * a->head_dim, a->head_dim) * a->scale; + } + + /* Softmax */ + float max_score = scores[0]; + for (int s = 1; s < a->seq_len; s++) { + if (scores[s] > max_score) max_score = scores[s]; + } + float sum_exp = 0.0f; + for (int s = 0; s < a->seq_len; s++) { + scores[s] = expf(scores[s] - max_score); + sum_exp += scores[s]; + } + float inv_sum = 1.0f / sum_exp; + for (int s = 0; s < a->seq_len; s++) { + scores[s] *= inv_sum; + } + + /* Weighted sum of V */ + memset(out_vec, 0, a->head_dim * sizeof(float)); + for (int s = 0; s < a->seq_len; s++) { + float w_s = scores[s]; + const float *v_vec = v_base + s * a->head_dim; + for (int d = 0; d < a->head_dim; d++) { + out_vec[d] += w_s * v_vec[d]; + } + } + } +} + +/* Blocked ("flash") prefill attention. The per-query path (compute_attn_heads + * called once per position) re-reads the whole K/V cache for every query, so + * prefill's attention is O(P^2) DRAM traffic and dominates prefill. This + * processes a BLOCK of queries together, streaming K and V in blocks that stay + * cache-resident and are reused across the query block, cutting K/V reads from + * P times to ~P/BQ times. Same math as compute_attn_heads (full-precision + * softmax; scores tile is L2-resident). Parallelized over query heads. + * Assumes contiguous keys at positions 0..base_pos+P-1 (no sliding-window + * wraparound), which holds for a fresh prefill. */ +typedef struct { + const float *q; /* [P, n_head*head_dim] */ + const float *k_cache; /* [n_kv_head, max_seq, head_dim] */ + const float *v_cache; + float *out; /* [P, n_head*head_dim] */ + int P, base_pos, head_dim, n_head, n_kv_groups, max_seq; + float scale; +} pf_attn_args; + +/* Query block sized so the QK^T / A@V sgemm M dimension is AMX-friendly. At + * PF_BQ=128 the scores tile is 128 x (base+P) x 4 bytes (~700 KB for ~1400 + * keys), which stays L2-resident per worker. BLAS tiles the K dimension + * internally, so no explicit key-blocking is needed. */ +#define PF_BQ 128 + +static void prefill_flash_range(int start, int end, void *arg) { + const pf_attn_args *a = (const pf_attn_args *)arg; + int hd = a->head_dim, P = a->P, base = a->base_pos, maxs = a->max_seq; + int qdim = a->n_head * hd; + int nkey_max = base + P; /* keys 0..base+P-1 */ + float *scores = malloc((size_t)PF_BQ * nkey_max * sizeof(float)); + if (!scores) return; + + for (int qh = start; qh < end; qh++) { + int kv_h = qh / a->n_kv_groups; + const float *k_base = a->k_cache + (size_t)kv_h * maxs * hd; + const float *v_base = a->v_cache + (size_t)kv_h * maxs * hd; + + for (int qb0 = 0; qb0 < P; qb0 += PF_BQ) { + int bq = (qb0 + PF_BQ < P) ? PF_BQ : (P - qb0); + int kmax = base + qb0 + bq; /* exclusive causal max in block */ + /* This head's query and output slabs: rows strided by qdim (heads + * are interleaved), each hd wide, starting at column qh*hd. */ + const float *qblk = a->q + (size_t)qb0 * qdim + (size_t)qh * hd; + float *oblk = a->out + (size_t)qb0 * qdim + (size_t)qh * hd; + +#ifdef USE_BLAS + /* 1. Full score rectangle S[bq, kmax] = scale * Qblk @ K[0:kmax]^T. + * NoTrans A (bq x hd, lda=qdim) times Trans B (K stored kmax x hd, + * ldb=hd) yields Qblk @ K^T; the softmax scale folds into alpha. + * Columns past each row's causal limit are computed, then masked. */ + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, bq, kmax, hd, + a->scale, qblk, qdim, k_base, hd, 0.0f, scores, nkey_max); + + /* 2. Exact per-row causal softmax over [0, lim) (unchanged math), + * then zero the non-causal tail [lim, kmax) so it contributes + * exactly 0.0 to the A@V GEMM below. */ + for (int i = 0; i < bq; i++) { + int lim = base + qb0 + i + 1; + float *sc = scores + (size_t)i * nkey_max; + float mx = sc[0]; + for (int s = 1; s < lim; s++) if (sc[s] > mx) mx = sc[s]; + float sum = 0.0f; + for (int s = 0; s < lim; s++) { sc[s] = expf(sc[s] - mx); sum += sc[s]; } + float inv = 1.0f / sum; + for (int s = 0; s < lim; s++) sc[s] *= inv; + for (int s = lim; s < kmax; s++) sc[s] = 0.0f; + } + + /* 3. out[bq, hd] = S[bq, kmax] @ V[0:kmax]. The zeroed tail makes + * this exactly the masked weighted sum of V. */ + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, bq, hd, kmax, + 1.0f, scores, nkey_max, v_base, hd, 0.0f, oblk, qdim); +#else + (void)qblk; (void)oblk; + /* Portable fallback: same per-row causal softmax + weighted V sum. */ + for (int i = 0; i < bq; i++) { + int lim = base + qb0 + i + 1; + const float *qv = a->q + (size_t)(qb0 + i) * qdim + (size_t)qh * hd; + float *sc = scores + (size_t)i * nkey_max; + for (int s = 0; s < lim; s++) + sc[s] = pn_dot_f32(qv, k_base + (size_t)s * hd, hd) * a->scale; + float mx = sc[0]; + for (int s = 1; s < lim; s++) if (sc[s] > mx) mx = sc[s]; + float sum = 0.0f; + for (int s = 0; s < lim; s++) { sc[s] = expf(sc[s] - mx); sum += sc[s]; } + float inv = 1.0f / sum; + float *out = a->out + (size_t)(qb0 + i) * qdim + (size_t)qh * hd; + memset(out, 0, hd * sizeof(float)); + for (int s = 0; s < lim; s++) { + float w = sc[s] * inv; + const float *vv = v_base + (size_t)s * hd; + for (int d = 0; d < hd; d++) out[d] += w * vv[d]; + } + } +#endif + } + } + free(scores); +} + +/* ============================================================ + * Forward pass — quantized weights + arena (Phase 2) + * + * Uses raw float* buffers from arena instead of tm_tensor. + * Only the final logits output is a tm_tensor (returned to caller). + * ============================================================ */ + +tm_tensor* pn_llama_forward(pn_model *model, uint32_t token, pn_state *state) { + if (!model || !state || !model->weights) return NULL; + + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + if (!qw->is_quantized) { + return pn_llama_forward_f32(model, token, state); + } + + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + pn_arena *arena = state->arena; + int pos = state->pos; + int n_embd = cfg->n_embd; + int head_dim = pn_head_dim(cfg); + int n_kv_groups = cfg->n_head / cfg->n_kv_head; + int max_seq = kv->max_seq; + +#ifdef USE_THREADS + fw_pool *pool = model->pool; + /* Register this model's pool so pn_qmv parallelizes its row loop across + * cores for every Q/K/V/O and gate/up/down projection in this token. */ + pn_qmv_set_pool(pool); +#endif + + if (!arena) return NULL; + + /* Reset arena for this forward call */ + pn_arena_reset(arena); + + /* 1. Token embedding lookup (or injected image embedding for VLM tokens) */ + float *hidden = pn_arena_alloc_f32(arena, n_embd); + if (!hidden) return NULL; + if (state->embd_override) + memcpy(hidden, state->embd_override, n_embd * sizeof(float)); + else + memcpy(hidden, qw->tok_embd + (size_t)token * n_embd, n_embd * sizeof(float)); + + /* Pre-allocate attention scores buffer: [n_head, max_seq] */ + float *scores_buf = pn_arena_alloc_f32(arena, cfg->n_head * max_seq); + if (!scores_buf) return NULL; + + /* 2. Transformer layers */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_qlayer *l = &qw->layers[layer]; + + /* === Attention sublayer === */ + + /* RMSNorm */ + float *normed = pn_arena_alloc_f32(arena, n_embd); + if (!normed) return NULL; + pn_vrms_norm_f32(hidden, l->attn_norm, normed, n_embd, cfg->rms_norm_eps); + + /* Q/K/V projections via quantized matvec */ + float *q = pn_arena_alloc_f32(arena, l->wq.rows); + float *k = pn_arena_alloc_f32(arena, l->wk.rows); + float *v = pn_arena_alloc_f32(arena, l->wv.rows); + if (!q || !k || !v) return NULL; + /* Q/K/V all read `normed`: fuse into one parallel dispatch. */ + { + const pn_qweight *qkv_w[3] = { &l->wq, &l->wk, &l->wv }; + const float *qkv_vec[3] = { normed, normed, normed }; + float *qkv_out[3] = { q, k, v }; + pn_qmv_group(qkv_w, qkv_vec, qkv_out, 3); + } + + /* Optional attention biases (Qwen2) */ + if (l->bq) pn_vadd_f32(q, l->bq, q, l->wq.rows); + if (l->bk) pn_vadd_f32(k, l->bk, k, l->wk.rows); + if (l->bv) pn_vadd_f32(v, l->bv, v, l->wv.rows); + + /* Per-head Q/K RMSNorm before RoPE (Qwen3) */ + if (l->attn_q_norm) { + for (int h = 0; h < cfg->n_head; h++) + pn_vrms_norm_f32(q + (size_t)h * head_dim, l->attn_q_norm, + q + (size_t)h * head_dim, head_dim, cfg->rms_norm_eps); + } + if (l->attn_k_norm) { + for (int h = 0; h < cfg->n_kv_head; h++) + pn_vrms_norm_f32(k + (size_t)h * head_dim, l->attn_k_norm, + k + (size_t)h * head_dim, head_dim, cfg->rms_norm_eps); + } + + /* Apply RoPE to Q and K (NEOX for Qwen3's unpermuted GGUF weights) */ + if (cfg->arch == PN_ARCH_QWEN3) { + apply_rope_neox(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + apply_rope_neox(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } else { + apply_rope_raw(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + apply_rope_raw(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } + + /* Update KV cache (circular for sliding window) */ + float *k_cache_data = (float *)kv->k_cache[layer]->data; + float *v_cache_data = (float *)kv->v_cache[layer]->data; + int cache_pos = pos % max_seq; + + for (int h = 0; h < cfg->n_kv_head; h++) { + memcpy(k_cache_data + h * max_seq * head_dim + cache_pos * head_dim, + k + h * head_dim, + head_dim * sizeof(float)); + memcpy(v_cache_data + h * max_seq * head_dim + cache_pos * head_dim, + v + h * head_dim, + head_dim * sizeof(float)); + } + + /* Compute attention */ + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + float *attn_out = pn_arena_alloc_f32(arena, cfg->n_head * head_dim); + if (!attn_out) return NULL; + + attn_args args = { + .q_data = q, + .k_cache_data = k_cache_data, + .v_cache_data = v_cache_data, + .attn_out = attn_out, + .scores_buf = scores_buf, + .head_dim = head_dim, + .n_kv_groups = n_kv_groups, + .max_seq = max_seq, + .seq_len = seq_len, + .scale = 1.0f / sqrtf((float)head_dim), + }; + +#ifdef USE_THREADS + if (pool && cfg->n_head >= 4) { + fw_for(pool, cfg->n_head, compute_attn_heads, &args); + } else { + compute_attn_heads(0, cfg->n_head, &args); + } +#else + compute_attn_heads(0, cfg->n_head, &args); +#endif + + /* Attention SHAPE capture — the post-softmax weights are still in scores_buf and + * attn_out is not yet consumed, so this reads the arithmetic that just ran rather + * than recomputing anything. One branch on a global when nothing is armed. */ + if (pn_attention_trace_armed()) { + pn_attention_trace_layer cap = { + .scores = scores_buf, + .attn_out = attn_out, + .v_cache = v_cache_data, + .normed = normed, + .n_head = cfg->n_head, + .n_kv_groups = n_kv_groups, + .head_dim = head_dim, + .max_seq = max_seq, + .seq_len = seq_len, + .n_embd = n_embd, + }; + + pn_attention_trace_record(layer, &cap); + } + + /* Output projection */ + float *attn_proj = pn_arena_alloc_f32(arena, n_embd); + if (!attn_proj) return NULL; + pn_qmv(&l->wo, attn_out, attn_proj); + if (l->bo) pn_vadd_f32(attn_proj, l->bo, attn_proj, n_embd); + + /* Residual connection */ + pn_vadd_f32(hidden, attn_proj, hidden, n_embd); + + /* === FFN sublayer === */ + + /* RMSNorm */ + float *ffn_normed = pn_arena_alloc_f32(arena, n_embd); + if (!ffn_normed) return NULL; + pn_vrms_norm_f32(hidden, l->ffn_norm, ffn_normed, n_embd, cfg->rms_norm_eps); + + if (cfg->n_experts > 0) { + /* MoE routing */ + float *gate_logits = pn_arena_alloc_f32(arena, cfg->n_experts); + if (!gate_logits) return NULL; + pn_qmv(&l->ffn_gate_inp, ffn_normed, gate_logits); + + /* Softmax over gate logits */ + float max_gl = gate_logits[0]; + for (int e = 1; e < cfg->n_experts; e++) + if (gate_logits[e] > max_gl) max_gl = gate_logits[e]; + float sum_exp_gl = 0; + for (int e = 0; e < cfg->n_experts; e++) { + gate_logits[e] = expf(gate_logits[e] - max_gl); + sum_exp_gl += gate_logits[e]; + } + for (int e = 0; e < cfg->n_experts; e++) + gate_logits[e] /= sum_exp_gl; + + /* Top-k selection */ + int top_idx[8]; + float top_wt[8]; + for (int k = 0; k < cfg->n_experts_used; k++) { + int best = -1; + float best_val = -1; + for (int e = 0; e < cfg->n_experts; e++) { + int skip = 0; + for (int j = 0; j < k; j++) + if (top_idx[j] == e) skip = 1; + if (!skip && gate_logits[e] > best_val) { + best = e; + best_val = gate_logits[e]; + } + } + top_idx[k] = best; + top_wt[k] = best_val; + } + + /* Renormalize selected weights */ + float wt_sum = 0; + for (int k = 0; k < cfg->n_experts_used; k++) wt_sum += top_wt[k]; + float inv_wt = 1.0f / wt_sum; + for (int k = 0; k < cfg->n_experts_used; k++) top_wt[k] *= inv_wt; + + /* Run selected experts and accumulate */ + float *ffn_out = pn_arena_alloc_f32(arena, n_embd); + if (!ffn_out) return NULL; + memset(ffn_out, 0, n_embd * sizeof(float)); + float *exp_gate = pn_arena_alloc_f32(arena, cfg->n_ff); + float *exp_up = pn_arena_alloc_f32(arena, cfg->n_ff); + float *exp_hid = pn_arena_alloc_f32(arena, cfg->n_ff); + float *exp_out = pn_arena_alloc_f32(arena, n_embd); + if (!exp_gate || !exp_up || !exp_hid || !exp_out) return NULL; + + for (int k = 0; k < cfg->n_experts_used; k++) { + pn_moe_expert *expert = &l->experts[top_idx[k]]; + pn_qmv(&expert->w_gate, ffn_normed, exp_gate); + pn_qmv(&expert->w_up, ffn_normed, exp_up); + pn_vsilu_mul_f32(exp_gate, exp_up, exp_hid, cfg->n_ff); + pn_qmv(&expert->w_down, exp_hid, exp_out); + for (int j = 0; j < n_embd; j++) + ffn_out[j] += top_wt[k] * exp_out[j]; + } + + pn_vadd_f32(hidden, ffn_out, hidden, n_embd); + } else { + /* Standard FFN (non-MoE) */ + float *gate = pn_arena_alloc_f32(arena, cfg->n_ff); + float *up = pn_arena_alloc_f32(arena, cfg->n_ff); + if (!gate || !up) return NULL; + /* gate and up both read `ffn_normed`: fuse into one dispatch. */ + { + const pn_qweight *gu_w[2] = { &l->w_gate, &l->w_up }; + const float *gu_vec[2] = { ffn_normed, ffn_normed }; + float *gu_out[2] = { gate, up }; + pn_qmv_group(gu_w, gu_vec, gu_out, 2); + } + + float *ffn_hidden = pn_arena_alloc_f32(arena, cfg->n_ff); + if (!ffn_hidden) return NULL; + pn_vsilu_mul_f32(gate, up, ffn_hidden, cfg->n_ff); + + float *ffn_out = pn_arena_alloc_f32(arena, n_embd); + if (!ffn_out) return NULL; + pn_qmv(&l->w_down, ffn_hidden, ffn_out); + + pn_vadd_f32(hidden, ffn_out, hidden, n_embd); + } + + /* The residual stream as this layer leaves it — the state an accelerator arm has to + * be compared against, which a per-op score never sees. Off unless armed. */ + pn_residual_trace_record(layer, hidden, n_embd); + } + + /* 3. Final RMSNorm */ + float *final_normed = pn_arena_alloc_f32(arena, n_embd); + if (!final_normed) return NULL; + pn_vrms_norm_f32(hidden, qw->output_norm, final_normed, n_embd, cfg->rms_norm_eps); + pn_residual_trace_record(cfg->n_layer, final_normed, n_embd); + + /* 4. LM head: output @ normed -> logits */ + float *logits_raw = pn_arena_alloc_f32(arena, cfg->n_vocab); + if (!logits_raw) return NULL; + pn_qmv(&qw->output, final_normed, logits_raw); + + /* Create output tm_tensor (only allocation that escapes the arena) */ + int64_t logits_shape[] = { cfg->n_vocab }; + tm_tensor *logits_t = tm_from_data(logits_raw, logits_shape, 1, TM_F32); + tm_tensor *logits = logits_t ? tm_clone(logits_t) : NULL; + if (logits_t) tm_free(logits_t); + + state->pos++; + return logits; +} + +/* ============================================================ + * Batched multi-token prefill (Phase 3) + * + * Processes P tokens through each layer with GEMM projections. Each quantized + * weight is dequantized once per layer in L2-resident panels (via pn_qmm_prefill) + * and multiplied against all P rows on AMX, instead of once per token. + * Numerically equivalent to P sequential single-token forwards up to BLAS + * summation order (the same reassociation the projection GEMMs already use). + * ============================================================ */ + +#ifdef USE_METAL +/* GPU F16 prefill projection: out[P,rows] = normed[P,cols] @ dequant(w)^T using + * the GPU-resident Q4_K weight (fm_gemm_q4_k_f16). Returns 1 if it ran on the + * GPU, 0 if the caller must use the CPU dequant+GEMM path. */ +static fm_buffer *g_prefill_x = NULL; static size_t g_prefill_x_cap = 0; +static fm_buffer *g_prefill_o = NULL; static size_t g_prefill_o_cap = 0; +static fm_buffer *prefill_stage(fm_buffer **b, size_t *cap, size_t bytes) { + if (*b && *cap >= bytes) return *b; + if (*b) fm_buffer_free(*b); + *b = fm_buffer_alloc(bytes); *cap = *b ? bytes : 0; return *b; +} +static int prefill_gemm_gpu(const pn_qweight *w, const float *normed, float *out, + int P, int rows, int cols) { + if (!w->gpu || w->type != GGUF_QUANT_Q4_K || !fm_available()) return 0; + fm_buffer *x = prefill_stage(&g_prefill_x, &g_prefill_x_cap, (size_t)P * cols * sizeof(float)); + fm_buffer *o = prefill_stage(&g_prefill_o, &g_prefill_o_cap, (size_t)P * rows * sizeof(float)); + if (!x || !o) return 0; + memcpy(fm_buffer_data(x), normed, (size_t)P * cols * sizeof(float)); + fm_gemm_q4_k_f16((fm_buffer *)w->gpu, x, o, P, rows, cols, (int)w->row_stride); + fm_sync(); + memcpy(out, fm_buffer_data(o), (size_t)P * rows * sizeof(float)); + return 1; +} +#endif + +/* A prefill projection GEMM. GPU F16 path when the weight is GPU-resident (opt-in + * PN_GPU_PREFILL upload), else the CPU panel path: pn_qmm_prefill dequantizes the + * weight in L2-resident panels and runs a per-panel cblas_sgemm on AMX, with no + * full-weight F32 DRAM scratch. Output layout is [P, rows] (out[p*rows + r]), + * matching the [B, rows] contract of pn_qmm. */ +static void prefill_project(const pn_qweight *w, const float *normed, float *out, + int P, int rows, int cols) { +#ifdef USE_METAL + if (prefill_gemm_gpu(w, normed, out, P, rows, cols)) return; +#else + (void)rows; (void)cols; +#endif + pn_qmm_prefill(w, normed, P, out); +} + +/* ============================================================ + * Batched prefill helpers: per-row work parallelized over the row range. + * Each range_fn performs the identical per-element arithmetic on a disjoint + * subset of rows, so the parallel result is bit-for-bit identical to the + * serial loop it replaces. + * ============================================================ */ + +/* NEOX RoPE applied from a precomputed [P, rot/2] cos/sin table. This computes + * the SAME rotation as apply_rope_neox: cos_row[i] == cosf(pos*freq_i) and + * sin_row[i] == sinf(pos*freq_i) with freq_i == 1/powf(theta,(2i)/rot), and the + * same pairing (dim i with i+half). Reusing the cached cosf/sinf float bits + * makes the output bit-for-bit identical to the per-row recompute. */ +static void apply_rope_neox_tab(float *data, int n_heads, int head_dim, + int rot, const float *cos_row, const float *sin_row) { + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + float cos_a = cos_row[i], sin_a = sin_row[i]; + float x0 = head[i]; + float x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* Row-parallel RMSNorm: dst[p] = rmsnorm(src[p], weight). */ +typedef struct { + const float *src; const float *weight; float *dst; + int n_embd; float eps; +} prefill_rmsnorm_args; +static void prefill_rmsnorm_range(int start, int end, void *arg) { + const prefill_rmsnorm_args *a = (const prefill_rmsnorm_args *)arg; + for (int p = start; p < end; p++) + pn_vrms_norm_f32(a->src + (size_t)p * a->n_embd, a->weight, + a->dst + (size_t)p * a->n_embd, a->n_embd, a->eps); +} + +/* Row-parallel SiLU-multiply: out[p] = silu(gate[p]) * up[p]. */ +typedef struct { + const float *gate; const float *up; float *out; int n_ff; +} prefill_silu_args; +static void prefill_silu_range(int start, int end, void *arg) { + const prefill_silu_args *a = (const prefill_silu_args *)arg; + for (int p = start; p < end; p++) + pn_vsilu_mul_f32(a->gate + (size_t)p * a->n_ff, a->up + (size_t)p * a->n_ff, + a->out + (size_t)p * a->n_ff, a->n_ff); +} + +/* Row-parallel residual add: hidden[p] += delta[p], applying an optional bias + * to delta first (the attention-output projection bias). */ +typedef struct { + float *hidden; float *delta; const float *bias; int n_embd; +} prefill_residual_args; +static void prefill_residual_range(int start, int end, void *arg) { + const prefill_residual_args *a = (const prefill_residual_args *)arg; + for (int p = start; p < end; p++) { + float *d = a->delta + (size_t)p * a->n_embd; + if (a->bias) pn_vadd_f32(d, a->bias, d, a->n_embd); + pn_vadd_f32(a->hidden + (size_t)p * a->n_embd, d, + a->hidden + (size_t)p * a->n_embd, a->n_embd); + } +} + +/* Row-parallel attention pre-processing: QKV bias, optional QK-norm, RoPE + * (table-driven NEOX for Qwen3, raw fallback otherwise), then KV-cache fill. + * Rows are independent, so per-row identical arithmetic is bitwise identical. */ +typedef struct { + float *q_buf, *k_buf, *v_buf; + const float *bq, *bk, *bv; + const float *attn_q_norm, *attn_k_norm; + const float *cos_tab, *sin_tab; /* [P, rot/2]; NULL when not NEOX */ + float *k_cache_data, *v_cache_data; + int qdim, kdim, vdim, head_dim, n_head, n_kv_head, rot, half; + int arch, rope_dim, base_pos, max_seq; + float rms_eps, rope_theta; +} prefill_attn_pre_args; +static void prefill_attn_pre_range(int start, int end, void *arg) { + const prefill_attn_pre_args *a = (const prefill_attn_pre_args *)arg; + for (int p = start; p < end; p++) { + int pos = a->base_pos + p; + float *q = a->q_buf + (size_t)p * a->qdim; + float *k = a->k_buf + (size_t)p * a->kdim; + float *v = a->v_buf + (size_t)p * a->vdim; + + if (a->bq) pn_vadd_f32(q, a->bq, q, a->qdim); + if (a->bk) pn_vadd_f32(k, a->bk, k, a->kdim); + if (a->bv) pn_vadd_f32(v, a->bv, v, a->vdim); + + if (a->attn_q_norm) { + for (int h = 0; h < a->n_head; h++) + pn_vrms_norm_f32(q + (size_t)h * a->head_dim, a->attn_q_norm, + q + (size_t)h * a->head_dim, a->head_dim, a->rms_eps); + } + if (a->attn_k_norm) { + for (int h = 0; h < a->n_kv_head; h++) + pn_vrms_norm_f32(k + (size_t)h * a->head_dim, a->attn_k_norm, + k + (size_t)h * a->head_dim, a->head_dim, a->rms_eps); + } + + if (a->arch == PN_ARCH_QWEN3) { + const float *cos_row = a->cos_tab + (size_t)p * a->half; + const float *sin_row = a->sin_tab + (size_t)p * a->half; + apply_rope_neox_tab(q, a->n_head, a->head_dim, a->rot, cos_row, sin_row); + apply_rope_neox_tab(k, a->n_kv_head, a->head_dim, a->rot, cos_row, sin_row); + } else { + apply_rope_raw(q, a->n_head, a->head_dim, a->rope_dim, pos, a->rope_theta); + apply_rope_raw(k, a->n_kv_head, a->head_dim, a->rope_dim, pos, a->rope_theta); + } + + int cache_pos = pos % a->max_seq; + for (int h = 0; h < a->n_kv_head; h++) { + memcpy(a->k_cache_data + (size_t)h * a->max_seq * a->head_dim + (size_t)cache_pos * a->head_dim, + k + (size_t)h * a->head_dim, a->head_dim * sizeof(float)); + memcpy(a->v_cache_data + (size_t)h * a->max_seq * a->head_dim + (size_t)cache_pos * a->head_dim, + v + (size_t)h * a->head_dim, a->head_dim * sizeof(float)); + } + } +} + +/* Dispatch a per-row range fn over all P rows: use the pool when one exists and + * P clears a small threshold, otherwise run it inline. */ +#ifdef USE_THREADS +#define PN_PREFILL_ROW_PAR_MIN 8 +#define PREFILL_FOR_ROWS(fn, arg) \ + do { if (pool && P >= PN_PREFILL_ROW_PAR_MIN) fw_for(pool, P, (fn), (arg)); \ + else (fn)(0, P, (arg)); } while (0) +#else +#define PREFILL_FOR_ROWS(fn, arg) (fn)(0, P, (arg)) +#endif + +/* Shared batched forward over P tokens of ONE sequence starting at state->pos. + * This is the common engine behind pn_llama_prefill (prompt ingest) and + * pn_llama_verify (speculative-decode verify): identical layer stack, identical + * kernels, so the numerics are the same. The tail differs only in what logits + * are produced and whether the position is committed: + * + * all_logits != NULL -> compute the final RMSNorm + LM head for ALL P rows + * into all_logits[P*n_vocab] (row-major). Used by verify. + * out_last != NULL -> also (or instead) return the LAST row's logits as an + * owned tm_tensor via *out_last. Used by prefill. + * commit_pos != 0 -> advance state->pos by P (prefill). Verify passes 0 so + * the K/V just written stays uncommitted (free rollback). + * + * Returns 0 on success, -1 on unsupported model / allocation failure (state left + * untouched in that case). */ +static int llama_batch_forward_seq(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, + pn_state *state, tm_tensor **out_last, + float *all_logits, int commit_pos) { + if (out_last) *out_last = NULL; + if (!model || !state || !model->weights || n_tokens <= 0) return -1; + + /* Only the quantized, non-MoE Llama/Qwen forward is supported here. */ + if (model->forward != pn_llama_forward) return -1; + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + if (!qw->is_quantized) return -1; + + pn_config *cfg = &model->config; + if (cfg->n_experts > 0) return -1; /* MoE: fall back to per-token */ + + /* Reject ternary / unsupported quant up front (no state mutation). */ + pn_qweight *probe[] = { &qw->layers[0].wq, &qw->layers[0].wk, + &qw->layers[0].wv, &qw->layers[0].wo, + &qw->layers[0].w_gate, &qw->layers[0].w_up, + &qw->layers[0].w_down }; + for (size_t i = 0; i < sizeof(probe) / sizeof(probe[0]); i++) { + switch (probe[i]->type) { + case GGUF_QUANT_F32: case GGUF_QUANT_F16: + case GGUF_QUANT_Q4_0: case GGUF_QUANT_Q8_0: + case GGUF_QUANT_Q3_K: case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: case GGUF_QUANT_Q6_K: + break; + default: + return -1; + } + } + +#ifdef USE_METAL + fm_init(); /* idempotent; enables the Metal GEMM path if a device exists */ + /* Opt-in (PN_GPU_PREFILL): upload Q4_K decoder weights and run prefill's + * projection GEMMs as F16 MPS matmuls on the GPU. OFF by default: measured + * a net LOSS for bulk OCR, because (a) prefill is attention-bound (the O(P^2) + * decoder attention over ~1369 image tokens dominates, not the GEMMs, so the + * GEMM speedup is only ~1.3x) and (b) it contends with Core ML vision for the + * single GPU, breaking the CPU-compute / GPU-vision overlap that makes bulk + * fast. Useful only when vision runs on the CPU (GPU otherwise idle). */ + static int gpu_prefill_uploaded = 0; + if (!gpu_prefill_uploaded && fm_available() && getenv("PN_GPU_PREFILL")) { + for (int layer = 0; layer < model->config.n_layer; layer++) { + pn_llama_qlayer *l = &qw->layers[layer]; + pn_qmv_gpu_upload(&l->wq); pn_qmv_gpu_upload(&l->wk); pn_qmv_gpu_upload(&l->wv); + pn_qmv_gpu_upload(&l->wo); pn_qmv_gpu_upload(&l->w_gate); + pn_qmv_gpu_upload(&l->w_up); pn_qmv_gpu_upload(&l->w_down); + } + gpu_prefill_uploaded = 1; + } +#endif + + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_arena *arena = state->arena; + int P = n_tokens; + int base_pos = state->pos; + int n_embd = cfg->n_embd; + int head_dim = pn_head_dim(cfg); + int n_head = cfg->n_head; + int n_kv_head = cfg->n_kv_head; + int n_kv_groups = n_head / n_kv_head; + int n_ff = cfg->n_ff; + int max_seq = kv->max_seq; + int qdim = qw->layers[0].wq.rows; /* n_head * head_dim */ + int kdim = qw->layers[0].wk.rows; /* n_kv_head * head_dim */ + int vdim = qw->layers[0].wv.rows; + + /* Batched activation buffers (malloc'd; the per-token arena is sized for + * a single token and may be too small for P rows). */ + float *hidden = malloc((size_t)P * n_embd * sizeof(float)); + float *normed = malloc((size_t)P * n_embd * sizeof(float)); + float *q_buf = malloc((size_t)P * qdim * sizeof(float)); + float *k_buf = malloc((size_t)P * kdim * sizeof(float)); + float *v_buf = malloc((size_t)P * vdim * sizeof(float)); + float *attn_out = malloc((size_t)P * qdim * sizeof(float)); + float *attn_proj = malloc((size_t)P * n_embd * sizeof(float)); + float *gate_buf = malloc((size_t)P * n_ff * sizeof(float)); + float *up_buf = malloc((size_t)P * n_ff * sizeof(float)); + float *ffn_hidden = malloc((size_t)P * n_ff * sizeof(float)); + float *ffn_out = malloc((size_t)P * n_embd * sizeof(float)); + float *scores_buf = malloc((size_t)n_head * max_seq * sizeof(float)); + + if (!hidden || !normed || !q_buf || !k_buf || !v_buf || !attn_out || + !attn_proj || !gate_buf || !up_buf || !ffn_hidden || !ffn_out || + !scores_buf) { + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + return -1; + } + + /* Precompute the NEOX RoPE cos/sin table once for this prefill (Change 1): + * cos/sin of pos*freq_i for every row and every rotary pair. freq_i depends + * only on i and the angle only on (pos, i), so this [P, rot/2] table is + * reused across all layers and all heads instead of recomputing powf/cosf/ + * sinf per (layer, row, head, i). The values are computed exactly as + * apply_rope_neox does, so the cached float bits keep the result identical. */ + int rope_rot = (cfg->rope_dim > 0 && cfg->rope_dim <= head_dim) ? cfg->rope_dim : head_dim; + int rope_half = rope_rot / 2; + float *rope_cos = NULL, *rope_sin = NULL; + if (cfg->arch == PN_ARCH_QWEN3) { + rope_cos = malloc((size_t)P * rope_half * sizeof(float)); + rope_sin = malloc((size_t)P * rope_half * sizeof(float)); + float *rope_freq = malloc((size_t)rope_half * sizeof(float)); + if (!rope_cos || !rope_sin || !rope_freq) { + free(rope_cos); free(rope_sin); free(rope_freq); + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + return -1; + } + for (int i = 0; i < rope_half; i++) + rope_freq[i] = 1.0f / powf(cfg->rope_theta, (float)(2 * i) / (float)rope_rot); + for (int p = 0; p < P; p++) { + int pos = base_pos + p; + float *cr = rope_cos + (size_t)p * rope_half; + float *sr = rope_sin + (size_t)p * rope_half; + for (int i = 0; i < rope_half; i++) { + float angle = (float)pos * rope_freq[i]; + cr[i] = cosf(angle); + sr[i] = sinf(angle); + } + } + free(rope_freq); + } + +#ifdef USE_THREADS + fw_pool *pool = model->pool; +#endif + + /* 1. Build the [P, n_embd] input embedding matrix. */ + for (int p = 0; p < P; p++) { + float *dst = hidden + (size_t)p * n_embd; + if (embd_rows && embd_rows[p]) + memcpy(dst, embd_rows[p], n_embd * sizeof(float)); + else + memcpy(dst, qw->tok_embd + (size_t)tokens[p] * n_embd, + n_embd * sizeof(float)); + } + + float scale = 1.0f / sqrtf((float)head_dim); + + /* 2. Transformer layers. */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_qlayer *l = &qw->layers[layer]; + + /* --- Attention --- */ + + /* RMSNorm each row. */ + prefill_rmsnorm_args attn_rn = { + .src = hidden, .weight = l->attn_norm, .dst = normed, + .n_embd = n_embd, .eps = cfg->rms_norm_eps, + }; + PREFILL_FOR_ROWS(prefill_rmsnorm_range, &attn_rn); + + /* Q/K/V projections (dequant once, then GEMM). */ + prefill_project(&l->wq, normed, q_buf, P, qdim, n_embd); + prefill_project(&l->wk, normed, k_buf, P, kdim, n_embd); + prefill_project(&l->wv, normed, v_buf, P, vdim, n_embd); + + /* Per-row post-processing: biases, QK-norm, RoPE, KV-cache fill. */ + float *k_cache_data = (float *)kv->k_cache[layer]->data; + float *v_cache_data = (float *)kv->v_cache[layer]->data; + + prefill_attn_pre_args pre = { + .q_buf = q_buf, .k_buf = k_buf, .v_buf = v_buf, + .bq = l->bq, .bk = l->bk, .bv = l->bv, + .attn_q_norm = l->attn_q_norm, .attn_k_norm = l->attn_k_norm, + .cos_tab = rope_cos, .sin_tab = rope_sin, + .k_cache_data = k_cache_data, .v_cache_data = v_cache_data, + .qdim = qdim, .kdim = kdim, .vdim = vdim, .head_dim = head_dim, + .n_head = n_head, .n_kv_head = n_kv_head, .rot = rope_rot, .half = rope_half, + .arch = cfg->arch, .rope_dim = cfg->rope_dim, + .base_pos = base_pos, .max_seq = max_seq, + .rms_eps = cfg->rms_norm_eps, .rope_theta = cfg->rope_theta, + }; + PREFILL_FOR_ROWS(prefill_attn_pre_range, &pre); + + /* Causal attention (KV cache now holds all P keys). Blocked "flash" path + * when keys are contiguous (no sliding-window wraparound) — reads K/V + * once per query block instead of once per query; else per-query. */ + if (base_pos + P <= max_seq) { + pf_attn_args fa = { + .q = q_buf, .k_cache = k_cache_data, .v_cache = v_cache_data, + .out = attn_out, .P = P, .base_pos = base_pos, .head_dim = head_dim, + .n_head = n_head, .n_kv_groups = n_kv_groups, .max_seq = max_seq, + .scale = scale, + }; +#ifdef USE_THREADS + if (pool && n_head >= 4) fw_for(pool, n_head, prefill_flash_range, &fa); + else prefill_flash_range(0, n_head, &fa); +#else + prefill_flash_range(0, n_head, &fa); +#endif + } else { + for (int p = 0; p < P; p++) { + int pos = base_pos + p; + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + attn_args args = { + .q_data = q_buf + (size_t)p * qdim, + .k_cache_data = k_cache_data, + .v_cache_data = v_cache_data, + .attn_out = attn_out + (size_t)p * qdim, + .scores_buf = scores_buf, + .head_dim = head_dim, + .n_kv_groups = n_kv_groups, + .max_seq = max_seq, + .seq_len = seq_len, + .scale = scale, + }; +#ifdef USE_THREADS + if (pool && n_head >= 4) fw_for(pool, n_head, compute_attn_heads, &args); + else compute_attn_heads(0, n_head, &args); +#else + compute_attn_heads(0, n_head, &args); +#endif + } + } + + /* Output projection + residual. */ + prefill_project(&l->wo, attn_out, attn_proj, P, n_embd, qdim); + prefill_residual_args attn_res = { + .hidden = hidden, .delta = attn_proj, .bias = l->bo, .n_embd = n_embd, + }; + PREFILL_FOR_ROWS(prefill_residual_range, &attn_res); + + /* --- FFN --- */ + prefill_rmsnorm_args ffn_rn = { + .src = hidden, .weight = l->ffn_norm, .dst = normed, + .n_embd = n_embd, .eps = cfg->rms_norm_eps, + }; + PREFILL_FOR_ROWS(prefill_rmsnorm_range, &ffn_rn); + + prefill_project(&l->w_gate, normed, gate_buf, P, n_ff, n_embd); + prefill_project(&l->w_up, normed, up_buf, P, n_ff, n_embd); + + prefill_silu_args silu = { + .gate = gate_buf, .up = up_buf, .out = ffn_hidden, .n_ff = n_ff, + }; + PREFILL_FOR_ROWS(prefill_silu_range, &silu); + + prefill_project(&l->w_down, ffn_hidden, ffn_out, P, n_embd, n_ff); + + prefill_residual_args ffn_res = { + .hidden = hidden, .delta = ffn_out, .bias = NULL, .n_embd = n_embd, + }; + PREFILL_FOR_ROWS(prefill_residual_range, &ffn_res); + } + + /* 3. Final RMSNorm + LM head. Two shapes share the same normed hidden state: + * - all_logits: every row (verify needs a prediction per candidate). The + * final RMSNorm reuses the [P, n_embd] `normed` scratch (free after the + * layer loop), then ONE M=P LM-head GEMM, exactly as forward_batch does. + * - out_last: only the last row (prefill's returned logits are for the + * last token), matching the sequential path via a single pn_qmv. */ + int rc = 0; + if (all_logits) { + for (int p = 0; p < P; p++) + pn_vrms_norm_f32(hidden + (size_t)p * n_embd, qw->output_norm, + normed + (size_t)p * n_embd, n_embd, cfg->rms_norm_eps); +#ifdef USE_BLAS + if (qw->output.type == GGUF_QUANT_F32) { + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, P, cfg->n_vocab, n_embd, + 1.0f, normed, n_embd, (const float *)qw->output.data, n_embd, + 0.0f, all_logits, cfg->n_vocab); + } else +#endif + { + pn_qmm(&qw->output, normed, P, all_logits); + } + } + if (out_last) { + float *last_hidden = hidden + (size_t)(P - 1) * n_embd; + float *final_normed = arena ? pn_arena_alloc_f32(arena, n_embd) : NULL; + float *fn = final_normed ? final_normed : normed; /* reuse normed if no arena */ + pn_vrms_norm_f32(last_hidden, qw->output_norm, fn, n_embd, cfg->rms_norm_eps); + + float *logits_raw = malloc((size_t)cfg->n_vocab * sizeof(float)); + if (!logits_raw) { + rc = -1; + } else { + pn_qmv(&qw->output, fn, logits_raw); + int64_t logits_shape[] = { cfg->n_vocab }; + tm_tensor *logits_t = tm_from_data(logits_raw, logits_shape, 1, TM_F32); + *out_last = logits_t ? tm_clone(logits_t) : NULL; + if (logits_t) tm_free(logits_t); + free(logits_raw); + } + } + + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + free(rope_cos); free(rope_sin); + + /* Commit the position only for the prefill path. Verify leaves pos untouched + * so the K/V just written into slots pos..pos+P-1 stays uncommitted, giving + * free rollback of rejected drafts. */ + if (commit_pos) state->pos = base_pos + P; + return rc; +} + +#undef PREFILL_FOR_ROWS +#undef PN_PREFILL_ROW_PAR_MIN + +/* Batched multi-token prefill: thin wrapper over the shared core that returns + * the last token's logits and commits state->pos (see header contract). */ +tm_tensor* pn_llama_prefill(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, + pn_state *state) { + tm_tensor *last = NULL; + if (llama_batch_forward_seq(model, tokens, embd_rows, n_tokens, state, + &last, NULL, /*commit_pos=*/1) != 0) + return NULL; + return last; +} + +/* Speculative-decode verify pass: run the same batched forward over the P + * candidate tokens, emit per-row logits into out_logits, and DO NOT advance pos + * (see header contract). out_logits must hold n_tokens * n_vocab floats. */ +int pn_llama_verify(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, + pn_state *state, float *out_logits) { + if (!out_logits) return -1; + return llama_batch_forward_seq(model, tokens, embd_rows, n_tokens, state, + NULL, out_logits, /*commit_pos=*/0); +} + +int pn_llama_forward_batch(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, pn_state **states, + int batch, float *out_logits) { + if (!model || !states || !out_logits || batch <= 0) return -1; + if (!model->weights) return -1; + + /* Same support envelope as pn_llama_prefill. */ + if (model->forward != pn_llama_forward) return -1; + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + if (!qw->is_quantized) return -1; + pn_config *cfg = &model->config; + if (cfg->n_experts > 0) return -1; + + pn_qweight *probe[] = { &qw->layers[0].wq, &qw->layers[0].wk, + &qw->layers[0].wv, &qw->layers[0].wo, + &qw->layers[0].w_gate, &qw->layers[0].w_up, + &qw->layers[0].w_down }; + for (size_t i = 0; i < sizeof(probe) / sizeof(probe[0]); i++) { + switch (probe[i]->type) { + case GGUF_QUANT_F32: case GGUF_QUANT_F16: + case GGUF_QUANT_Q4_0: case GGUF_QUANT_Q8_0: + case GGUF_QUANT_Q3_K: case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: case GGUF_QUANT_Q6_K: + break; + default: + return -1; + } + } + + /* Every sequence must carry its own KV cache. */ + int max_seq = 0; + for (int b = 0; b < batch; b++) { + if (!states[b] || !states[b]->kv_cache) return -1; + pn_llama_kv_cache *kvb = (pn_llama_kv_cache *)states[b]->kv_cache; + if (kvb->max_seq > max_seq) max_seq = kvb->max_seq; + } + +#ifdef USE_METAL + fm_init(); + /* Opt-in (PN_BATCH_GPU): upload the Q4_K decoder weights to the GPU once so + * pn_qmm runs the batched GEMM on the GPU. Non-Q4_K weights (e.g. Q6_K + * output) stay on the CPU AMX path — pn_qmm dispatches per weight. */ + static int gpu_batch_uploaded = 0; + if (!gpu_batch_uploaded && batch <= 32 && getenv("PN_BATCH_GPU") && fm_available()) { + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_qlayer *l = &qw->layers[layer]; + pn_qmv_gpu_upload(&l->wq); pn_qmv_gpu_upload(&l->wk); + pn_qmv_gpu_upload(&l->wv); pn_qmv_gpu_upload(&l->wo); + pn_qmv_gpu_upload(&l->w_gate); pn_qmv_gpu_upload(&l->w_up); + pn_qmv_gpu_upload(&l->w_down); + } + pn_qmm_set_gpu_decode(1); /* PN_BATCH_GPU experiment: decode on GPU */ + gpu_batch_uploaded = 1; + } +#endif + + int P = batch; /* one row per sequence */ + int n_embd = cfg->n_embd; + int head_dim = pn_head_dim(cfg); + int n_head = cfg->n_head; + int n_kv_head = cfg->n_kv_head; + int n_kv_groups = n_head / n_kv_head; + int n_ff = cfg->n_ff; + int qdim = qw->layers[0].wq.rows; + int kdim = qw->layers[0].wk.rows; + int vdim = qw->layers[0].wv.rows; + + float *hidden = malloc((size_t)P * n_embd * sizeof(float)); + float *normed = malloc((size_t)P * n_embd * sizeof(float)); + float *q_buf = malloc((size_t)P * qdim * sizeof(float)); + float *k_buf = malloc((size_t)P * kdim * sizeof(float)); + float *v_buf = malloc((size_t)P * vdim * sizeof(float)); + float *attn_out = malloc((size_t)P * qdim * sizeof(float)); + float *attn_proj = malloc((size_t)P * n_embd * sizeof(float)); + float *gate_buf = malloc((size_t)P * n_ff * sizeof(float)); + float *up_buf = malloc((size_t)P * n_ff * sizeof(float)); + float *ffn_hidden = malloc((size_t)P * n_ff * sizeof(float)); + float *ffn_out = malloc((size_t)P * n_embd * sizeof(float)); + float *scores_buf = malloc((size_t)n_head * max_seq * sizeof(float)); + float *final_normed = malloc((size_t)P * n_embd * sizeof(float)); + + if (!hidden || !normed || !q_buf || !k_buf || !v_buf || !attn_out || + !attn_proj || !gate_buf || !up_buf || !ffn_hidden || !ffn_out || + !scores_buf || !final_normed) { + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + free(final_normed); + return -1; + } + +#ifdef USE_THREADS + fw_pool *pool = model->pool; +#endif + + /* 1. Input embedding matrix [P, n_embd], one row per sequence. */ + for (int b = 0; b < P; b++) { + float *dst = hidden + (size_t)b * n_embd; + if (embd_rows && embd_rows[b]) + memcpy(dst, embd_rows[b], n_embd * sizeof(float)); + else + memcpy(dst, qw->tok_embd + (size_t)tokens[b] * n_embd, + n_embd * sizeof(float)); + } + + float scale = 1.0f / sqrtf((float)head_dim); + + /* 2. Transformer layers. Projections/FFN are M=P GEMMs (weight dequant'd + * once, applied to all P sequences); only the KV write + attention are + * per-sequence, since each sequence owns its cache and position. */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_qlayer *l = &qw->layers[layer]; + + for (int b = 0; b < P; b++) + pn_vrms_norm_f32(hidden + (size_t)b * n_embd, l->attn_norm, + normed + (size_t)b * n_embd, n_embd, cfg->rms_norm_eps); + + /* Fused M=P projections: dequant each weight row once, dot against all + * P sequences (quantized weight read once, no F32 DRAM copy). */ + pn_qmm(&l->wq, normed, P, q_buf); + pn_qmm(&l->wk, normed, P, k_buf); + pn_qmm(&l->wv, normed, P, v_buf); + + /* Per-sequence post-proc: bias, QK-norm, RoPE, KV write into that + * sequence's own cache at its own position. */ + for (int b = 0; b < P; b++) { + pn_llama_kv_cache *kvb = (pn_llama_kv_cache *)states[b]->kv_cache; + int pos = states[b]->pos; + int b_max_seq = kvb->max_seq; + float *k_cache_data = (float *)kvb->k_cache[layer]->data; + float *v_cache_data = (float *)kvb->v_cache[layer]->data; + float *q = q_buf + (size_t)b * qdim; + float *k = k_buf + (size_t)b * kdim; + float *v = v_buf + (size_t)b * vdim; + + if (l->bq) pn_vadd_f32(q, l->bq, q, qdim); + if (l->bk) pn_vadd_f32(k, l->bk, k, kdim); + if (l->bv) pn_vadd_f32(v, l->bv, v, vdim); + + if (l->attn_q_norm) { + for (int h = 0; h < n_head; h++) + pn_vrms_norm_f32(q + (size_t)h * head_dim, l->attn_q_norm, + q + (size_t)h * head_dim, head_dim, cfg->rms_norm_eps); + } + if (l->attn_k_norm) { + for (int h = 0; h < n_kv_head; h++) + pn_vrms_norm_f32(k + (size_t)h * head_dim, l->attn_k_norm, + k + (size_t)h * head_dim, head_dim, cfg->rms_norm_eps); + } + + if (cfg->arch == PN_ARCH_QWEN3) { + apply_rope_neox(q, n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + apply_rope_neox(k, n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } else { + apply_rope_raw(q, n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + apply_rope_raw(k, n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } + + int cache_pos = pos % b_max_seq; + for (int h = 0; h < n_kv_head; h++) { + memcpy(k_cache_data + (size_t)h * b_max_seq * head_dim + (size_t)cache_pos * head_dim, + k + (size_t)h * head_dim, head_dim * sizeof(float)); + memcpy(v_cache_data + (size_t)h * b_max_seq * head_dim + (size_t)cache_pos * head_dim, + v + (size_t)h * head_dim, head_dim * sizeof(float)); + } + } + + /* Attention: each sequence attends only its own cache. */ + for (int b = 0; b < P; b++) { + pn_llama_kv_cache *kvb = (pn_llama_kv_cache *)states[b]->kv_cache; + int pos = states[b]->pos; + int b_max_seq = kvb->max_seq; + int seq_len = (pos + 1 < b_max_seq) ? pos + 1 : b_max_seq; + attn_args args = { + .q_data = q_buf + (size_t)b * qdim, + .k_cache_data = (float *)kvb->k_cache[layer]->data, + .v_cache_data = (float *)kvb->v_cache[layer]->data, + .attn_out = attn_out + (size_t)b * qdim, + .scores_buf = scores_buf, + .head_dim = head_dim, + .n_kv_groups = n_kv_groups, + .max_seq = b_max_seq, + .seq_len = seq_len, + .scale = scale, + }; +#ifdef USE_THREADS + if (pool && n_head >= 4) fw_for(pool, n_head, compute_attn_heads, &args); + else compute_attn_heads(0, n_head, &args); +#else + compute_attn_heads(0, n_head, &args); +#endif + } + + pn_qmm(&l->wo, attn_out, P, attn_proj); + for (int b = 0; b < P; b++) { + float *ap = attn_proj + (size_t)b * n_embd; + if (l->bo) pn_vadd_f32(ap, l->bo, ap, n_embd); + pn_vadd_f32(hidden + (size_t)b * n_embd, ap, hidden + (size_t)b * n_embd, n_embd); + } + + for (int b = 0; b < P; b++) + pn_vrms_norm_f32(hidden + (size_t)b * n_embd, l->ffn_norm, + normed + (size_t)b * n_embd, n_embd, cfg->rms_norm_eps); + + pn_qmm(&l->w_gate, normed, P, gate_buf); + pn_qmm(&l->w_up, normed, P, up_buf); + + for (int b = 0; b < P; b++) + pn_vsilu_mul_f32(gate_buf + (size_t)b * n_ff, up_buf + (size_t)b * n_ff, + ffn_hidden + (size_t)b * n_ff, n_ff); + + pn_qmm(&l->w_down, ffn_hidden, P, ffn_out); + + for (int b = 0; b < P; b++) + pn_vadd_f32(hidden + (size_t)b * n_embd, ffn_out + (size_t)b * n_embd, + hidden + (size_t)b * n_embd, n_embd); + } + + /* 3. Final RMSNorm (all rows), then the LM head as ONE M=P GEMM so the large + * output/embedding weight is read once instead of once per row (for tied + * embeddings it is a ~600 MB F32 matrix, so per-row was B x that traffic). */ + for (int b = 0; b < P; b++) + pn_vrms_norm_f32(hidden + (size_t)b * n_embd, qw->output_norm, + final_normed + (size_t)b * n_embd, n_embd, cfg->rms_norm_eps); +#ifdef USE_BLAS + if (qw->output.type == GGUF_QUANT_F32) { + /* F32 tied-embedding head: GEMM straight off the weight, no dequant. */ + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, P, cfg->n_vocab, n_embd, + 1.0f, final_normed, n_embd, (const float *)qw->output.data, n_embd, + 0.0f, out_logits, cfg->n_vocab); + } else +#endif + { + pn_qmm(&qw->output, final_normed, P, out_logits); + } + + for (int b = 0; b < P; b++) states[b]->pos += 1; + + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + free(final_normed); + return 0; +} + +/* Ragged batched speculative-decode verify. This is pn_llama_forward_batch's + * body with P = M (M = sum of the per-sequence block lengths), specialized so a + * row's logits are bit-identical to running that one token through + * forward_batch on its own sequence: + * + * - Projections / FFN / LM head are M=P pn_qmm calls, exactly as forward_batch. + * pn_qmm dequantizes each weight row once and dots it against every + * activation row independently, so an output row does not depend on M or the + * other rows (this is what makes it bitwise-equal to forward_batch, which + * uses the same pn_qmm). NOT pn_qmm_prefill and NOT the F32 BLAS LM head, + * whose reassociated summation order would diverge. + * - Attention pre-processing (bias, QK-norm, RoPE at each row's true position, + * KV write into that row's OWN cache) and per-query causal attention are the + * one ragged part, driven by row via row_seq[]/row_pos[]. Uses + * apply_rope_neox/apply_rope_raw directly (the same calls forward_batch + * uses) and per-query compute_attn_heads (NOT prefill_flash_range, whose + * cblas summation order differs from forward_batch). Row r's attention + * seq_len = states[b]->pos + (r-off[b]) + 1 IS the causal mask: it sees its + * committed prefix plus its own block's earlier rows, nothing after and + * nothing from another sequence. + * + * No state->pos is advanced: rejected drafts roll back for free. */ +int pn_llama_verify_batch(pn_model *model, const uint32_t *tokens, + pn_state **states, const int *n_tok, int n_seqs, + float *out_logits) { + if (!model || !tokens || !states || !n_tok || !out_logits || n_seqs <= 0) + return -1; + if (!model->weights) return -1; + + /* Same support envelope as pn_llama_forward_batch. */ + if (model->forward != pn_llama_forward) return -1; + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + if (!qw->is_quantized) return -1; + pn_config *cfg = &model->config; + if (cfg->n_experts > 0) return -1; + + pn_qweight *probe[] = { &qw->layers[0].wq, &qw->layers[0].wk, + &qw->layers[0].wv, &qw->layers[0].wo, + &qw->layers[0].w_gate, &qw->layers[0].w_up, + &qw->layers[0].w_down }; + for (size_t i = 0; i < sizeof(probe) / sizeof(probe[0]); i++) { + switch (probe[i]->type) { + case GGUF_QUANT_F32: case GGUF_QUANT_F16: + case GGUF_QUANT_Q4_0: case GGUF_QUANT_Q8_0: + case GGUF_QUANT_Q3_K: case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: case GGUF_QUANT_Q6_K: + break; + default: + return -1; + } + } + + /* Ragged layout: off[b] is block b's first row, M the total rows. Reject any + * block with a non-positive length, a missing cache, or a cache wraparound + * (states[b]->pos + n_tok[b] > that state's max_seq) — the no-wrap guarantee + * is what lets cache_pos == row_pos and keeps the committed prefix contiguous + * below the block. All checks run before any state mutation. */ + int M = 0, max_seq = 0; + for (int b = 0; b < n_seqs; b++) { + if (n_tok[b] <= 0) return -1; + if (!states[b] || !states[b]->kv_cache) return -1; + pn_llama_kv_cache *kvb = (pn_llama_kv_cache *)states[b]->kv_cache; + if (states[b]->pos + n_tok[b] > kvb->max_seq) return -1; + if (kvb->max_seq > max_seq) max_seq = kvb->max_seq; + M += n_tok[b]; + } + if (M > PN_QMM_MAX_B) return -1; /* the LM-head pn_qmm caps at PN_QMM_MAX_B */ + + int *off = malloc((size_t)n_seqs * sizeof(int)); + int *row_seq = malloc((size_t)M * sizeof(int)); + int *row_pos = malloc((size_t)M * sizeof(int)); + if (!off || !row_seq || !row_pos) { free(off); free(row_seq); free(row_pos); return -1; } + { + int r = 0; + for (int b = 0; b < n_seqs; b++) { + off[b] = r; + for (int j = 0; j < n_tok[b]; j++) { + row_seq[r] = b; + row_pos[r] = states[b]->pos + j; /* this row's true position */ + r++; + } + } + } + + int P = M; /* one row per candidate token */ + int n_embd = cfg->n_embd; + int head_dim = pn_head_dim(cfg); + int n_head = cfg->n_head; + int n_kv_head = cfg->n_kv_head; + int n_kv_groups = n_head / n_kv_head; + int n_ff = cfg->n_ff; + int qdim = qw->layers[0].wq.rows; + int kdim = qw->layers[0].wk.rows; + int vdim = qw->layers[0].wv.rows; + + float *hidden = malloc((size_t)P * n_embd * sizeof(float)); + float *normed = malloc((size_t)P * n_embd * sizeof(float)); + float *q_buf = malloc((size_t)P * qdim * sizeof(float)); + float *k_buf = malloc((size_t)P * kdim * sizeof(float)); + float *v_buf = malloc((size_t)P * vdim * sizeof(float)); + float *attn_out = malloc((size_t)P * qdim * sizeof(float)); + float *attn_proj = malloc((size_t)P * n_embd * sizeof(float)); + float *gate_buf = malloc((size_t)P * n_ff * sizeof(float)); + float *up_buf = malloc((size_t)P * n_ff * sizeof(float)); + float *ffn_hidden = malloc((size_t)P * n_ff * sizeof(float)); + float *ffn_out = malloc((size_t)P * n_embd * sizeof(float)); + float *scores_buf = malloc((size_t)n_head * max_seq * sizeof(float)); + float *final_normed = malloc((size_t)P * n_embd * sizeof(float)); + + if (!hidden || !normed || !q_buf || !k_buf || !v_buf || !attn_out || + !attn_proj || !gate_buf || !up_buf || !ffn_hidden || !ffn_out || + !scores_buf || !final_normed) { + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + free(final_normed); free(off); free(row_seq); free(row_pos); + return -1; + } + +#ifdef USE_THREADS + fw_pool *pool = model->pool; +#endif + + /* 1. Input embedding matrix [M, n_embd], one row per candidate token. */ + for (int r = 0; r < P; r++) + memcpy(hidden + (size_t)r * n_embd, + qw->tok_embd + (size_t)tokens[r] * n_embd, n_embd * sizeof(float)); + + float scale = 1.0f / sqrtf((float)head_dim); + + /* 2. Transformer layers. Projections/FFN are M=P pn_qmm GEMMs (identical to + * forward_batch); only the KV write + attention are ragged per row. */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_qlayer *l = &qw->layers[layer]; + + for (int r = 0; r < P; r++) + pn_vrms_norm_f32(hidden + (size_t)r * n_embd, l->attn_norm, + normed + (size_t)r * n_embd, n_embd, cfg->rms_norm_eps); + + pn_qmm(&l->wq, normed, P, q_buf); + pn_qmm(&l->wk, normed, P, k_buf); + pn_qmm(&l->wv, normed, P, v_buf); + + /* Per-row post-proc: bias, QK-norm, RoPE at row_pos[r], KV write into + * row_seq[r]'s own cache at cache_pos = row_pos[r] % that cache max_seq. */ + for (int r = 0; r < P; r++) { + pn_llama_kv_cache *kvb = (pn_llama_kv_cache *)states[row_seq[r]]->kv_cache; + int pos = row_pos[r]; + int b_max_seq = kvb->max_seq; + float *k_cache_data = (float *)kvb->k_cache[layer]->data; + float *v_cache_data = (float *)kvb->v_cache[layer]->data; + float *q = q_buf + (size_t)r * qdim; + float *k = k_buf + (size_t)r * kdim; + float *v = v_buf + (size_t)r * vdim; + + if (l->bq) pn_vadd_f32(q, l->bq, q, qdim); + if (l->bk) pn_vadd_f32(k, l->bk, k, kdim); + if (l->bv) pn_vadd_f32(v, l->bv, v, vdim); + + if (l->attn_q_norm) { + for (int h = 0; h < n_head; h++) + pn_vrms_norm_f32(q + (size_t)h * head_dim, l->attn_q_norm, + q + (size_t)h * head_dim, head_dim, cfg->rms_norm_eps); + } + if (l->attn_k_norm) { + for (int h = 0; h < n_kv_head; h++) + pn_vrms_norm_f32(k + (size_t)h * head_dim, l->attn_k_norm, + k + (size_t)h * head_dim, head_dim, cfg->rms_norm_eps); + } + + if (cfg->arch == PN_ARCH_QWEN3) { + apply_rope_neox(q, n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + apply_rope_neox(k, n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } else { + apply_rope_raw(q, n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + apply_rope_raw(k, n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } + + int cache_pos = pos % b_max_seq; + for (int h = 0; h < n_kv_head; h++) { + memcpy(k_cache_data + (size_t)h * b_max_seq * head_dim + (size_t)cache_pos * head_dim, + k + (size_t)h * head_dim, head_dim * sizeof(float)); + memcpy(v_cache_data + (size_t)h * b_max_seq * head_dim + (size_t)cache_pos * head_dim, + v + (size_t)h * head_dim, head_dim * sizeof(float)); + } + } + + /* Attention: row r attends its own cache with seq_len = row_pos[r] + 1 + * (committed prefix + this block's rows up to and including r). */ + for (int r = 0; r < P; r++) { + pn_llama_kv_cache *kvb = (pn_llama_kv_cache *)states[row_seq[r]]->kv_cache; + int b_max_seq = kvb->max_seq; + int seq_len = row_pos[r] + 1; /* no-wrap guaranteed: <= b_max_seq */ + attn_args args = { + .q_data = q_buf + (size_t)r * qdim, + .k_cache_data = (float *)kvb->k_cache[layer]->data, + .v_cache_data = (float *)kvb->v_cache[layer]->data, + .attn_out = attn_out + (size_t)r * qdim, + .scores_buf = scores_buf, + .head_dim = head_dim, + .n_kv_groups = n_kv_groups, + .max_seq = b_max_seq, + .seq_len = seq_len, + .scale = scale, + }; +#ifdef USE_THREADS + if (pool && n_head >= 4) fw_for(pool, n_head, compute_attn_heads, &args); + else compute_attn_heads(0, n_head, &args); +#else + compute_attn_heads(0, n_head, &args); +#endif + } + + pn_qmm(&l->wo, attn_out, P, attn_proj); + for (int r = 0; r < P; r++) { + float *ap = attn_proj + (size_t)r * n_embd; + if (l->bo) pn_vadd_f32(ap, l->bo, ap, n_embd); + pn_vadd_f32(hidden + (size_t)r * n_embd, ap, hidden + (size_t)r * n_embd, n_embd); + } + + for (int r = 0; r < P; r++) + pn_vrms_norm_f32(hidden + (size_t)r * n_embd, l->ffn_norm, + normed + (size_t)r * n_embd, n_embd, cfg->rms_norm_eps); + + pn_qmm(&l->w_gate, normed, P, gate_buf); + pn_qmm(&l->w_up, normed, P, up_buf); + + for (int r = 0; r < P; r++) + pn_vsilu_mul_f32(gate_buf + (size_t)r * n_ff, up_buf + (size_t)r * n_ff, + ffn_hidden + (size_t)r * n_ff, n_ff); + + pn_qmm(&l->w_down, ffn_hidden, P, ffn_out); + + for (int r = 0; r < P; r++) + pn_vadd_f32(hidden + (size_t)r * n_embd, ffn_out + (size_t)r * n_embd, + hidden + (size_t)r * n_embd, n_embd); + } + + /* 3. Final RMSNorm (all rows) + LM head as ONE M=P pn_qmm, so row r's logits + * equal forward_batch's for that token. Always pn_qmm (never the F32 BLAS + * branch): row-independence is the bit-identity guarantee. */ + for (int r = 0; r < P; r++) + pn_vrms_norm_f32(hidden + (size_t)r * n_embd, qw->output_norm, + final_normed + (size_t)r * n_embd, n_embd, cfg->rms_norm_eps); + pn_qmm(&qw->output, final_normed, P, out_logits); + + /* No pos commit: the caller advances states[b]->pos over accepted rows. */ + + free(hidden); free(normed); free(q_buf); free(k_buf); free(v_buf); + free(attn_out); free(attn_proj); free(gate_buf); free(up_buf); + free(ffn_hidden); free(ffn_out); free(scores_buf); + free(final_normed); free(off); free(row_seq); free(row_pos); + return 0; +} + +/* ============================================================ + * Forward pass — F32 weights (Phase 1 legacy) + * ============================================================ */ + +static void apply_rope(tm_tensor *t, int pos, int head_dim, int rope_dim, float theta_base) { + if (!t) return; + + int n_heads = (int)t->shape[0]; + float *data = (float *)t->data; + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < rot; i += 2) { + float freq = 1.0f / powf(theta_base, (float)i / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle); + float sin_a = sinf(angle); + + float x0 = head[i]; + float x1 = head[i + 1]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + 1] = x0 * sin_a + x1 * cos_a; + } + } +} + +tm_tensor* pn_llama_forward_f32(pn_model *model, uint32_t token, pn_state *state) { + if (!model || !state || !model->weights) return NULL; + + pn_llama_weights *w = (pn_llama_weights *)model->weights; + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + int pos = state->pos; + int head_dim = cfg->n_embd / cfg->n_head; + int n_kv_groups = cfg->n_head / cfg->n_kv_head; + + /* 1. Token embedding lookup */ + int64_t tok_shape[] = { 1 }; + int32_t tok_val = (int32_t)token; + tm_tensor *tok_idx = tm_from_data(&tok_val, tok_shape, 1, TM_I32); + tm_tensor *hidden = tm_embedding(w->tok_embd, tok_idx); + tm_free(tok_idx); + + tm_tensor *h = tm_squeeze(hidden, 0); + tm_free(hidden); + hidden = h; + + /* 2. Transformer layers */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_layer *l = &w->layers[layer]; + + /* Attention */ + tm_tensor *normed = tm_rms_norm(hidden, l->attn_norm, cfg->rms_norm_eps); + + tm_tensor *q = tm_mv(l->wq, normed); + tm_tensor *k = tm_mv(l->wk, normed); + tm_tensor *v = tm_mv(l->wv, normed); + tm_free(normed); + + int64_t q_shape[] = { cfg->n_head, head_dim }; + int64_t kv_shape[] = { cfg->n_kv_head, head_dim }; + + tm_tensor *q_heads = tm_reshape(q, q_shape, 2); + tm_tensor *k_heads = tm_reshape(k, kv_shape, 2); + tm_tensor *v_heads = tm_reshape(v, kv_shape, 2); + tm_free(q); tm_free(k); tm_free(v); + + apply_rope(q_heads, pos, head_dim, cfg->rope_dim, cfg->rope_theta); + apply_rope(k_heads, pos, head_dim, cfg->rope_dim, cfg->rope_theta); + + float *k_cache_data = (float *)kv->k_cache[layer]->data; + float *v_cache_data = (float *)kv->v_cache[layer]->data; + float *k_data = (float *)k_heads->data; + float *v_data = (float *)v_heads->data; + int max_seq_f32 = kv->max_seq; + int cache_pos = pos % max_seq_f32; + + for (int hh = 0; hh < cfg->n_kv_head; hh++) { + memcpy(k_cache_data + hh * max_seq_f32 * head_dim + cache_pos * head_dim, + k_data + hh * head_dim, + head_dim * sizeof(float)); + memcpy(v_cache_data + hh * max_seq_f32 * head_dim + cache_pos * head_dim, + v_data + hh * head_dim, + head_dim * sizeof(float)); + } + tm_free(k_heads); + tm_free(v_heads); + + int seq_len = (pos + 1 < max_seq_f32) ? pos + 1 : max_seq_f32; + float *attn_out = calloc(cfg->n_head * head_dim, sizeof(float)); + float *q_data_ptr = (float *)q_heads->data; + + for (int qh = 0; qh < cfg->n_head; qh++) { + int kv_h = qh / n_kv_groups; + float *q_vec = q_data_ptr + qh * head_dim; + float *k_base = k_cache_data + kv_h * kv->max_seq * head_dim; + float *v_base = v_cache_data + kv_h * kv->max_seq * head_dim; + + float *scores = calloc(seq_len, sizeof(float)); + float scale = 1.0f / sqrtf((float)head_dim); + + for (int s = 0; s < seq_len; s++) { + float dot = 0.0f; + float *k_vec = k_base + s * head_dim; + for (int d = 0; d < head_dim; d++) { + dot += q_vec[d] * k_vec[d]; + } + scores[s] = dot * scale; + } + + float max_score = scores[0]; + for (int s = 1; s < seq_len; s++) { + if (scores[s] > max_score) max_score = scores[s]; + } + float sum_exp = 0.0f; + for (int s = 0; s < seq_len; s++) { + scores[s] = expf(scores[s] - max_score); + sum_exp += scores[s]; + } + float inv_sum = 1.0f / sum_exp; + for (int s = 0; s < seq_len; s++) { + scores[s] *= inv_sum; + } + + float *out_vec = attn_out + qh * head_dim; + for (int s = 0; s < seq_len; s++) { + float w_s = scores[s]; + float *v_vec = v_base + s * head_dim; + for (int d = 0; d < head_dim; d++) { + out_vec[d] += w_s * v_vec[d]; + } + } + + free(scores); + } + tm_free(q_heads); + + int64_t attn_shape[] = { cfg->n_head * head_dim }; + tm_tensor *attn_vec = tm_from_data(attn_out, attn_shape, 1, TM_F32); + tm_tensor *attn_proj = tm_mv(l->wo, attn_vec); + tm_free(attn_vec); + free(attn_out); + + tm_tensor *h2 = tm_add(hidden, attn_proj); + tm_free(hidden); + tm_free(attn_proj); + hidden = h2; + + /* FFN */ + normed = tm_rms_norm(hidden, l->ffn_norm, cfg->rms_norm_eps); + + tm_tensor *gate = tm_mv(l->w_gate, normed); + tm_tensor *up = tm_mv(l->w_up, normed); + tm_free(normed); + + tm_tensor *gate_act = tm_silu(gate); + tm_free(gate); + + tm_tensor *ffn_hidden = tm_mul(gate_act, up); + tm_free(gate_act); + tm_free(up); + + tm_tensor *ffn_out = tm_mv(l->w_down, ffn_hidden); + tm_free(ffn_hidden); + + h2 = tm_add(hidden, ffn_out); + tm_free(hidden); + tm_free(ffn_out); + hidden = h2; + } + + /* 3. Final RMSNorm */ + tm_tensor *normed = tm_rms_norm(hidden, w->output_norm, cfg->rms_norm_eps); + tm_free(hidden); + + /* 4. LM head */ + tm_tensor *logits = tm_mv(w->output, normed); + tm_free(normed); + + state->pos++; + return logits; +} diff --git a/src/models/llama.h b/src/models/llama.h new file mode 100644 index 0000000..c7cbb89 --- /dev/null +++ b/src/models/llama.h @@ -0,0 +1,465 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_LLAMA_H +#define PN_LLAMA_H + +#include "../../include/pantheon.h" +#include "../core/engine.h" +#include "../core/qweight.h" +#include "../formats/gguf.h" + +/* + * Llama model architecture. + * + * Covers: Llama 2, Llama 3, Llama 3.1/3.2, CodeLlama, Yi, Solar. + * + * Architecture: + * embed -> for each layer: + * RMSNorm -> Q/K/V proj -> RoPE -> GQA -> O proj -> residual -> + * RMSNorm -> SwiGLU FFN (gate*silu(up), down) -> residual + * -> RMSNorm -> LM head -> logits + * + * Two weight representations: + * 1. F32 (legacy): all weights dequantized to tm_tensor (pn_llama_weights) + * 2. Quantized: weights stay in mmap'd quant form (pn_llama_qweights) + */ + +/* ============================================================ + * F32 weight structs (legacy, Phase 1) + * ============================================================ */ + +/* Llama layer weights (F32) */ +typedef struct { + tm_tensor *attn_norm; /* RMSNorm weight [n_embd] */ + tm_tensor *wq; /* Q projection [n_head * head_dim, n_embd] */ + tm_tensor *wk; /* K projection [n_kv_head * head_dim, n_embd] */ + tm_tensor *wv; /* V projection [n_kv_head * head_dim, n_embd] */ + tm_tensor *wo; /* Output projection [n_embd, n_head * head_dim] */ + tm_tensor *ffn_norm; /* RMSNorm weight [n_embd] */ + tm_tensor *w_gate; /* Gate projection [n_ff, n_embd] */ + tm_tensor *w_up; /* Up projection [n_ff, n_embd] */ + tm_tensor *w_down; /* Down projection [n_embd, n_ff] */ +} pn_llama_layer; + +/* Llama model weights (F32) */ +typedef struct { + tm_tensor *tok_embd; /* Token embeddings [n_vocab, n_embd] */ + tm_tensor *output_norm; /* Final RMSNorm [n_embd] */ + tm_tensor *output; /* LM head [n_vocab, n_embd] (may share with tok_embd) */ + pn_llama_layer *layers; /* Per-layer weights [n_layer] */ + int n_layers; + pn_gguf_file *gguf; /* Kept open for on-the-fly dequant */ +} pn_llama_weights; + +/* ============================================================ + * Quantized weight structs (Phase 2) + * ============================================================ */ + +/* MoE expert weights (one expert's FFN) */ +typedef struct { + pn_qweight w_gate; /* Gate projection [n_ff, n_embd] */ + pn_qweight w_up; /* Up projection [n_ff, n_embd] */ + pn_qweight w_down; /* Down projection [n_embd, n_ff] */ +} pn_moe_expert; + +/* Llama layer with quantized projections + F32 norms */ +typedef struct { + float *attn_norm; /* RMSNorm weight [n_embd] (F32, owned) */ + pn_qweight wq; /* Q projection (quantized, mmap) */ + pn_qweight wk; /* K projection (quantized, mmap) */ + pn_qweight wv; /* V projection (quantized, mmap) */ + pn_qweight wo; /* Output projection (quantized, mmap) */ + float *bq, *bk, *bv, *bo; /* Attention biases (NULL for Llama, F32 owned) */ + float *attn_q_norm; /* Qwen3 per-head Q RMSNorm [head_dim] (F32 owned, NULL otherwise) */ + float *attn_k_norm; /* Qwen3 per-head K RMSNorm [head_dim] (F32 owned, NULL otherwise) */ + float *ffn_norm; /* RMSNorm weight [n_embd] (F32, owned) */ + pn_qweight w_gate; /* Gate projection (quantized, mmap) — zeroed for MoE */ + pn_qweight w_up; /* Up projection (quantized, mmap) — zeroed for MoE */ + pn_qweight w_down; /* Down projection (quantized, mmap) — zeroed for MoE */ + /* MoE fields (NULL/zeroed for non-MoE models) */ + pn_qweight ffn_gate_inp; /* Router [n_experts, n_embd] */ + pn_moe_expert *experts; /* Array of n_experts (NULL for non-MoE) */ +} pn_llama_qlayer; + +/* Llama model with quantized weights */ +typedef struct { + int is_quantized; /* Always 1, used to distinguish from pn_llama_weights */ + float *tok_embd; /* Token embeddings [n_vocab * n_embd] (F32, owned) */ + float *output_norm; /* Final RMSNorm [n_embd] (F32, owned) */ + pn_qweight output; /* LM head (quantized or F32, mmap or owned) */ + float *output_f32; /* LM head F32 if needed for tied embeddings (owned) */ + pn_llama_qlayer *layers; /* Per-layer quantized weights [n_layer] */ + int n_layers; + int n_embd; + int n_vocab; + pn_gguf_file *gguf; /* Kept open (mmap backing) */ +} pn_llama_qweights; + +/* ============================================================ + * Llama KV cache + * ============================================================ */ + +typedef struct { + tm_tensor **k_cache; /* Key cache per layer [n_layer][n_kv_head, max_seq, head_dim] */ + tm_tensor **v_cache; /* Value cache per layer [n_layer][n_kv_head, max_seq, head_dim] */ + int n_layers; + int max_seq; + /* WHERE THE LIVE KV ACTUALLY IS. + * + * NULL — and it stays NULL on every path but one — means the tensors above hold it and a + * snapshot is a host memcpy. NON-NULL means the cache lives on an accelerator: the + * Tenstorrent backend running ON-CARD ATTENTION keeps it in card DRAM as bf16, the + * tensors above are never written, and a snapshot that read them anyway would export a + * page of zeros and call it a session. + * + * It is a FUNCTION POINTER rather than a device handle for two reasons that both matter. + * llama_state_snapshot.c then needs no accelerator header and links into every build, + * including the ones with no card support compiled in; and the off-hardware suite can + * install a FAKE mover, so the card branch is covered by a test that can go red instead + * of by an `#ifdef` nothing exercises. + * + * Set by the FORWARD that owns the cache, not by whoever allocated it — the forward is + * the only code that knows which arm is live. */ + void *kv_device; + /* Move one layer's K (`values` 0) or V (`values` 1) between the device and a packed + * [head][position][dim] fp32 array of `position_count` positions. `capture` non-zero is + * device -> host; zero is host -> device. Returns 0, or non-zero on a failed transfer — + * which the caller must NOT absorb. */ + int (*kv_move)(void *device, int layer, int values, int position_count, float *rows, + int capture); +} pn_llama_kv_cache; + +/* ============================================================ + * Forward passes + * ============================================================ */ + +/* Forward pass — quantized weights + arena (Phase 2, preferred) */ +tm_tensor* pn_llama_forward(pn_model *model, uint32_t token, pn_state *state); + +/* Forward pass — F32 weights (Phase 1 legacy) */ +tm_tensor* pn_llama_forward_f32(pn_model *model, uint32_t token, pn_state *state); + +#ifdef USE_CUDA +/* ============================================================ + * Single-die GPU forward (plain-Llama arch: MiniCPM5, Llama-3, etc.) + * + * Mirrors pn_llama_forward but runs the compute-heavy projections, norms, + * SwiGLU FFN and LM head on the GPU via Foundry's fc_* kernels with + * GPU-resident (repacked) weights, keeping RoPE + GQA-softmax attention on the + * host (bit-parity with the CPU oracle; the KV cache stays fp32 host-side). + * Reuses the SAME quant matvec / RMSNorm / SwiGLU kernels the Qwen3.5 CUDA + * forward uses — no new attention/FFN math. Non-MoE softmax layers only. + * ============================================================ */ + +/** + * Repack + upload every quantized projection (q/k/v/o + gate/up/down + LM head) + * and the F32 norms/embeddings of a loaded pn_llama_qweights to persistent + * device buffers, attaching the GPU handle to the model. Idempotent; a no-op + * (leaving the model un-uploaded) when fc_available() is false or a weight's + * quant type has no GPU kernel. The CPU weights are kept live (host fallback). + * + * @param model Loaded pn_model whose weights are pn_llama_qweights. + * @return 0 on success (or graceful skip when no GPU), -1 on failure. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_llama_gpu_upload(pn_model *model); + +/* 1 if the model has a live GPU-resident weight handle attached. */ +int pn_llama_model_gpu_loaded(const pn_model *model); + +/* Free the GPU handle + all its device buffers (detaches from the model). */ +void pn_llama_gpu_free(pn_model *model); + +/** + * Single-die GPU forward for one token. Requires pn_llama_gpu_upload() to have + * attached a GPU handle. Returns a freshly-allocated logits tm_tensor [n_vocab] + * (caller owns it), or NULL on failure / no GPU handle. Advances state->pos. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +tm_tensor* pn_llama_forward_cuda(pn_model *model, uint32_t token, + pn_state *state); +#endif /* USE_CUDA */ + +#ifdef USE_TT +/* ============================================================ + * Tenstorrent Blackhole forward (plain-Llama arch) + * + * Same shape as the CUDA path — the device does the matrices, the host keeps + * RoPE, KV append and GQA-softmax for bit-parity with the CPU oracle — with two + * differences. Weights come from a converted FTTW artifact resident in the + * card's GDDR6 and NOT from the GGUF directly; and the norms, residual adds and + * SwiGLU gate stay on the host as well. src/accel/tt/pn_tt_device.h states which + * capability gaps force that and what closes them. + * + * THE ARTIFACT IS PRODUCED AND CACHED FROM THE `.gguf`, NOT ASKED FOR. Tensix + * eats 32x32 tiles in four 16x16 faces and the matmul kernels read a block-float + * the unpacker decodes natively, so a GGUF can never be uploaded as it stands — + * but that conversion is a shader cache, not a question. `--model x.gguf` on its + * own converts once, caches beside the checkpoint, and is instant thereafter. + * PN_TT_ARTIFACT still names an artifact to use INSTEAD, as an override. + * ============================================================ */ + +/** + * Open the card, make the checkpoint's matmul weights resident, and attach the + * handle to the model. + * + * Converts the model's `.gguf` into a card-ready artifact if no valid cached one + * is beside it, choosing the precision rung from what fits — never asking. A + * first load of a large model prints a progress bar; later loads print one line + * naming the rung and the fit. + * + * Refuses — leaving the model CPU-resident — when the weights are F32 or MoE, + * the card is absent, or the model does not fit at any rung the card can load. + * A refusal for not fitting carries the arithmetic; it is never a silent + * fallback to a cheaper rung than the one it reports. + * + * @param model Loaded pn_model whose weights are pn_llama_qweights and whose + * `path` is the source checkpoint. + * @return 0 on success, -1 on refusal or failure. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_llama_tt_upload(pn_model *model); + +/* 1 if the model has a live Tenstorrent residency attached. */ +int pn_llama_model_tt_loaded(const pn_model *model); + +/* Release the residency and detach from the model. Never parks the core. */ +void pn_llama_tt_free(pn_model *model); + +/** + * One decode step with every projection on the card. Returns a freshly + * allocated logits tm_tensor [n_vocab] (caller owns it), or NULL on failure — a + * failed matmul is never absorbed into a plausible answer. Advances state->pos. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +tm_tensor* pn_llama_forward_tt(pn_model *model, uint32_t token, pn_state *state); + +/** + * BATCHED multi-token prefill on the card — the same contract as pn_llama_prefill, on the + * TT route. + * + * Ingests n_tokens positions starting at state->pos, advances state->pos by n_tokens and + * returns the LAST position's logits (caller owns the tm_tensor), or NULL — WITHOUT having + * mutated state — when the batched path refuses, in which case the caller runs its ordinary + * per-token loop. Refusals: fewer than 2 tokens, PN_TT_PREFILL=0, on-card attention (its + * rotary, KV append and flash decode each take ONE position), any of the diagnostic arms + * (PN_TT_TRACE / ATTN_CHECK / ROPE_CHECK / ROW_CENSUS, all of which read row 0 as if the + * other 31 were padding), or a prompt that would wrap the KV ring. + * + * A tile is 32 rows and a matvec is Mt=1, so the card already multiplies 32 rows against + * every weight tile it reads and 31 of them are zero padding. This fills them with prompt + * positions: same descriptor, same weight traffic, same reader floor, 32 tokens. The LAST + * token is deliberately NOT batched — it goes through pn_llama_forward_tt, so the LM head, + * the on-card top-N and the sparse-logit presentation are the shipping code unmodified. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +tm_tensor* pn_llama_prefill_tt(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, pn_state *state); +#endif /* USE_TT */ + +/* Batched multi-token prefill (quantized, non-MoE Llama/Qwen path). + * + * Processes n_tokens positions in one shot using GEMM projections, producing + * exactly the same KV-cache state and final-token logits as running n_tokens + * sequential pn_llama_forward() calls. Advances state->pos by n_tokens and + * returns the logits for the LAST position (caller owns the tm_tensor). + * + * For each position i: if embd_rows && embd_rows[i] != NULL, that [n_embd] + * vector is used as the input embedding (image-token override); otherwise + * tokens[i] is looked up in the token-embedding table. + * + * Returns NULL (without mutating state) if the model is not supported by the + * batched path (e.g. MoE, F32 legacy weights, ternary). Callers should fall + * back to the per-token loop in that case. */ +tm_tensor* pn_llama_prefill(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, + pn_state *state); + +/* Batched single-token decode across B independent sequences (quantized, + * non-MoE Llama/Qwen path). + * + * Decodes ONE token for each of `batch` sequences in a single M=batch GEMM + * pass, so every weight is dequantized once and applied to all B rows — the + * same amortization prefill uses, now for autoregressive decode. Each + * states[b] supplies sequence b's own KV cache and position (the sequences are + * fully independent: different lengths and positions are fine, and each + * attends only its own cache). For row b: if embd_rows && embd_rows[b], that + * [n_embd] vector is the input embedding; otherwise tokens[b] is looked up. + * + * Writes [batch, n_vocab] row-major logits into out_logits (caller-allocated) + * and advances each states[b]->pos by one. Returns 0 on success, or -1 without + * mutating any state if the model/quant is unsupported (caller falls back to + * per-sequence pn_llama_forward). */ +int pn_llama_forward_batch(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, pn_state **states, + int batch, float *out_logits); + +/* Ragged batched speculative-decode verify (multi-sequence, quantized non-MoE + * path). Verifies n_seqs INDEPENDENT sequences in one forward pass: block b is + * n_tok[b] candidate tokens for sequence states[b], starting at states[b]->pos. + * `tokens` is the concatenation of all blocks (M = sum n_tok[b] rows, grouped by + * sequence, block b occupying rows off[b]..off[b]+n_tok[b]-1). + * + * Structured as pn_llama_forward_batch's body with P = M: every projection, FFN + * and the LM head is a single M=P pn_qmm (each weight row dequantized once and + * dotted against every activation row independently), so a given output row's + * logits are bit-identical whether it is verified alone or alongside any other + * rows or sequences. Only the attention pre-processing (bias, per-head QK-norm, + * RoPE at each row's true position, KV write into that row's own cache) and the + * per-query causal attention are ragged: row r uses sequence row_seq[r]'s cache, + * position row_pos[r] = states[b]->pos + (r - off[b]), and attends exactly its + * committed prefix 0..states[b]->pos-1 plus its block's rows 0..(r-off[b]) via a + * seq_len = states[b]->pos + (r-off[b]) + 1 mask. It therefore never sees any + * other sequence's cache or any row after it. K/V for every row IS written into + * its sequence's cache at its position, but NO states[b]->pos is advanced (free + * per-slot rollback: the caller commits accepted rows by advancing pos, and the + * uncommitted slots are overwritten by the next real write and never read). + * + * Writes [M, n_vocab] row-major logits into out_logits (caller-owned); row r + * predicts the token AFTER candidate r. Returns 0 on success, or -1 (without + * mutating any state) if the model/quant is unsupported, M > PN_QMM_MAX_B, or any + * block would wrap its cache (states[b]->pos + n_tok[b] > that state's kv + * max_seq). Callers fall back to per-sequence pn_llama_verify in that case. */ +int pn_llama_verify_batch(pn_model *model, const uint32_t *tokens, + pn_state **states, const int *n_tok, int n_seqs, + float *out_logits); + +/* Speculative-decode verify pass (single sequence, quantized non-MoE path). + * + * Runs ONE batched forward over n_tokens candidate tokens starting at the + * sequence's current position state->pos, using the exact same M=P machinery as + * pn_llama_prefill (panel-path projections + FFN GEMMs, causal flash attention + * for the P queries over the existing cache, and the batched LM head), but: + * + * 1. it computes the LM head for EVERY one of the P positions and writes the + * full [n_tokens, n_vocab] row-major logits into out_logits (caller-owned), + * so the caller can verify each drafted token against its own prediction; + * 2. it does NOT advance state->pos. The K/V for the P positions ARE written + * into the cache at slots pos..pos+P-1 (position-indexed), but because pos + * is left untouched those slots are uncommitted: rejected drafts are undone + * for free by simply not advancing pos, and the stale slots are overwritten + * by the next real write and never read (seq_len derives from pos). + * + * Row i's logits depend only on tokens 0..i (causal), so the prediction used to + * accept/reject token i is exactly the logits the model produces for the prefix + * ending at candidate i, independent of the candidates after it. This is what + * makes greedy speculative decode lossless: the accepted stream is always the + * plain-greedy stream, whatever the draft quality. + * + * Returns 0 on success, or -1 (without mutating state) if the model/quant is + * unsupported (caller falls back to plain decode). */ +int pn_llama_verify(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, + pn_state *state, float *out_logits); + +/* ============================================================ + * Weight loading + * ============================================================ */ + +/* Load quantized weights from GGUF (zero-copy mmap). + * Only dequants tok_embd and norms to F32. Returns NULL if unsupported quant type. */ +pn_llama_qweights* pn_llama_load_gguf_quantized(pn_gguf_file *gguf, pn_config *config); + +/* Load F32 weights from GGUF (dequants everything). Legacy Phase 1 loader. */ +pn_llama_weights* pn_llama_load_gguf(pn_gguf_file *gguf, pn_config *config); + +/* Load from HuggingFace safetensors format (BF16 or ternarized). + * Detects format automatically. Loads tokenizer from vocab.json+merges.txt + * in the same directory as config_path. */ +pn_llama_qweights* pn_llama_load_safetensors(const char *model_path, + const char *config_path, + pn_config *config); + +/* Load BPE tokenizer from HuggingFace directory (vocab.json + merges.txt). + * dir_path: directory containing vocab.json and merges.txt. + * config: parsed config (used for bos/eos token IDs). */ +ft_tokenizer* pn_llama_load_hf_tokenizer(const char *dir_path, const pn_config *config); + +/* Free */ +void pn_llama_weights_free(pn_llama_weights *w); +void pn_llama_qweights_free(pn_llama_qweights *qw); + +/* KV cache */ +pn_llama_kv_cache* pn_llama_kv_cache_new(int n_layers, int n_kv_head, int max_seq, int head_dim); +void pn_llama_kv_cache_free(pn_llama_kv_cache *cache); + +/* ============================================================ + * Session snapshot — llama-family KV cache at a token position + * (llama_state_snapshot.c). Pure host memory: this family's whole per-session + * state IS the KV cache, and on every backend Pantheon has today — CPU, CUDA + * and Tenstorrent — those rows live in host RAM, so there is no device readback + * to arrange. Reached through core/session_snapshot.h, which picks this family + * or the qwen35moe dual-state one from the model's arch; call that, not these. + * ============================================================ */ + +/** + * Deep-copy rows [0, pos) of every layer out of a live KV cache. + * + * @param cache_v pn_llama_kv_cache * (pn_state::kv_cache on this family) + * @param pos committed length; must be > 0 and <= the cache's max_seq, + * because the live cache is a RING and a wrapped one no longer + * holds rows [0, pos) + * @return Opaque snapshot (free with pn_llama_kv_snapshot_free), or NULL + * + * Thread-safe: no (caller serializes against the generation that owns `cache`) + * Reentrant: yes + * Signal-safe: no + */ +void *pn_llama_kv_snapshot(const void *cache_v, int pos); + +/** + * Copy a snapshot back into a live KV cache. Validates n_layers / n_kv_head / + * head_dim exactly and bounds pos against THIS cache's ring; max_seq itself + * need not match, since the rows are repacked on the way in. The caller sets + * pn_state::pos to pn_llama_kv_snapshot_pos() afterwards. + * + * @return 0 on success, -1 if the snapshot does not fit this cache + * + * Thread-safe: no (same discipline as the capture) + * Reentrant: yes + * Signal-safe: no + */ +int pn_llama_kv_restore(void *cache_v, const void *snap); + +/** The position a snapshot was taken at, or -1. */ +int pn_llama_kv_snapshot_pos(const void *snap); + +/** Free a snapshot. NULL-safe. */ +void pn_llama_kv_snapshot_free(void *snap); + +/** + * Serialize a snapshot into ONE contiguous little-endian blob (magic "PNLK"). + * + * @param snap Snapshot from pn_llama_kv_snapshot() + * @param weights_version Opaque model-identity tag, echoed in the header + * @param bytes_out Receives a malloc'd buffer (caller frees) + * @param len_out Receives its length + * @return 0 on success, -1 on failure + * + * Thread-safe: yes (reads the snapshot only) + * Reentrant: yes + * Signal-safe: no + */ +int pn_llama_kv_snap_serialize(const void *snap, uint64_t weights_version, + unsigned char **bytes_out, size_t *len_out); + +/** + * Parse a blob back into an opaque snapshot (free with + * pn_llama_kv_snapshot_free). Validates magic, version, framing, every derived + * size, and that the blob is consumed EXACTLY — trailing bytes are a refusal. + * + * @return Opaque snapshot, or NULL on any validation failure. + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: no + */ +void *pn_llama_kv_snap_deserialize(const unsigned char *bytes, size_t len, + uint64_t *weights_version_out); + +#endif /* PN_LLAMA_H */ diff --git a/src/models/llama_attention_trace.c b/src/models/llama_attention_trace.c new file mode 100644 index 0000000..109e7c2 --- /dev/null +++ b/src/models/llama_attention_trace.c @@ -0,0 +1,152 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_attention_trace.c + * @brief Implementation of the per-head attention-shape capture declared in + * llama_attention_trace.h. + * + * @status Working + * @issues None + * @todo None + */ + +#include "llama_attention_trace.h" + +#include +#include +#include + +/** The armed capture, or NULL. One slot: the only caller is a single-threaded harness. */ +static pn_attention_trace *g_armed_attn_trace = NULL; + +int pn_attention_trace_alloc(pn_attention_trace *trace, int n_layer, int n_head) +{ + if (!trace || n_layer <= 0 || n_head <= 0) + return -1; + memset(trace, 0, sizeof(*trace)); + trace->heads = calloc((size_t)n_layer * (size_t)n_head, sizeof(*trace->heads)); + trace->normed_rms = calloc((size_t)n_layer, sizeof(*trace->normed_rms)); + if (!trace->heads || !trace->normed_rms) { + pn_attention_trace_free(trace); + return -1; + } + trace->n_layer = n_layer; + trace->n_head = n_head; + return 0; +} + +void pn_attention_trace_free(pn_attention_trace *trace) +{ + if (!trace) + return; + if (g_armed_attn_trace == trace) + g_armed_attn_trace = NULL; + free(trace->heads); + free(trace->normed_rms); + memset(trace, 0, sizeof(*trace)); +} + +void pn_attention_trace_capture_into(pn_attention_trace *trace) +{ + g_armed_attn_trace = trace; + if (trace) { + trace->recorded = 0; + trace->seq_len = 0; + } +} + +int pn_attention_trace_armed(void) +{ + return g_armed_attn_trace != NULL; +} + +/** Mean of the squares of `v`. */ +static double mean_square(const float *v, int n) +{ + double sum = 0.0; + + for (int i = 0; i < n; i++) + sum += (double)v[i] * (double)v[i]; + return n > 0 ? sum / (double)n : 0.0; +} + +/** + * Per-component mean square of each cached V row of ONE kv head, summed against the + * attention weights of one query head. Kept separate from the shape statistics because the + * kv head is shared across a group of query heads and this is the expensive term. + */ +static double weighted_v_mean_square(const float *v_base, const float *weights, int seq_len, + int head_dim) +{ + double acc = 0.0; + + for (int s = 0; s < seq_len; s++) { + double p = (double)weights[s]; + + if (p > 0.0) + acc += p * mean_square(v_base + (size_t)s * (size_t)head_dim, head_dim); + } + return acc; +} + +/** Entropy, collision and peak of one already-normalised weight vector. */ +static void weight_shape(const float *weights, int seq_len, pn_attn_head_record *rec) +{ + double entropy = 0.0, collision = 0.0, peak = 0.0; + int peak_at = 0; + + for (int s = 0; s < seq_len; s++) { + double p = (double)weights[s]; + + if (p > 0.0) + entropy -= p * log(p); + collision += p * p; + if (p > peak) { + peak = p; + peak_at = s; + } + } + rec->top_pos_rel = seq_len > 1 ? (double)peak_at / (double)(seq_len - 1) : 0.0; + rec->self_weight = (double)weights[seq_len - 1]; + rec->entropy = entropy; + rec->collision = collision; + rec->max_weight = peak; + rec->sink_weight = (double)weights[0]; +} + +void pn_attention_trace_record(int layer, const pn_attention_trace_layer *in) +{ + pn_attention_trace *trace = g_armed_attn_trace; + + if (!trace || !in || layer < 0 || layer >= trace->n_layer || in->n_head != trace->n_head) + return; + if (!in->scores || !in->attn_out || !in->v_cache || in->seq_len <= 0) + return; + + for (int h = 0; h < in->n_head; h++) { + pn_attn_head_record *rec = &trace->heads[(size_t)layer * (size_t)trace->n_head + h]; + const float *weights = in->scores + (size_t)h * (size_t)in->max_seq; + const float *v_base = in->v_cache + (size_t)(h / in->n_kv_groups) * + (size_t)in->max_seq * (size_t)in->head_dim; + + weight_shape(weights, in->seq_len, rec); + rec->out_rms = sqrt(mean_square(in->attn_out + (size_t)h * (size_t)in->head_dim, + in->head_dim)); + rec->vbar_rms = sqrt(weighted_v_mean_square(v_base, weights, in->seq_len, in->head_dim)); + rec->v0_rms = sqrt(mean_square(v_base, in->head_dim)); + } + if (in->normed) + trace->normed_rms[layer] = sqrt(mean_square(in->normed, in->n_embd)); + trace->seq_len = in->seq_len; + if (layer + 1 > trace->recorded) + trace->recorded = layer + 1; +} + +const pn_attn_head_record *pn_attention_trace_head(const pn_attention_trace *trace, int layer, + int head) +{ + if (!trace || !trace->heads || layer < 0 || layer >= trace->recorded || head < 0 || + head >= trace->n_head) + return NULL; + return &trace->heads[(size_t)layer * (size_t)trace->n_head + head]; +} diff --git a/src/models/llama_attention_trace.h b/src/models/llama_attention_trace.h new file mode 100644 index 0000000..ebb9f26 --- /dev/null +++ b/src/models/llama_attention_trace.h @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_attention_trace.h + * @brief Capture the GQA softmax's SHAPE — per head, per layer — out of the llama forward, + * together with the magnitude that shape is supposed to explain. + * + * The ChatML defect was traced to `attn_output`'s INPUT being 1.2-3.8x larger under a + * chat framing at unchanged PCC, while `attn_v`'s input is identical between framings. + * The stated mechanism — less cancellation in the weighted average of V, i.e. more + * CONCENTRATED attention weights — was inferred from those magnitudes and never + * measured. This records both halves at once so the inference can be tested rather + * than repeated: + * + * - the SHAPE of each head's weight vector (entropy, collision, max weight), + * - WHERE its mass sits (weight on the position-0 sink, on the current token, and + * the relative position of the peak), and + * - that head's contribution to `attn_output`'s input RMS, plus the + * no-cancellation reference the weights average over. + * + * Shape and location are independent, and only the pair identifies the cause. A head + * that parks 60% of its mass on a near-zero-magnitude prefix sink and a head that puts + * 60% on a full-magnitude content token have the SAME entropy and the same collision, + * and outputs that differ by a factor of several — so entropy alone cannot answer the + * question this exists to answer, and recording it alone would look like a null result. + * + * `cancellation` is the quantity that closes (or refutes) the link. For a head whose + * output is o = sum_s p_s v_s, define + * + * vbar_rms = sqrt( sum_s p_s * ||v_s||^2 / head_dim ) + * + * — the per-component RMS of the V rows the head averaged, weighted by the attention + * itself, so a head that attends to unusually large V rows is not mistaken for a head + * that cancels less. Then `out_rms / vbar_rms` is exactly how much of that magnitude + * SURVIVED the weighted average. If the V rows were mutually uncorrelated, that ratio + * would be sqrt(sum_s p_s^2) — the square root of the collision probability — so + * `collision` gives the concentration-only PREDICTION of the same number, and the two + * can be compared directly. Concentration explaining the magnitude means those two + * move together; a measured cancellation that does not track sqrt(collision) means the + * magnitude comes from WHICH vectors are averaged, not from how many. + * + * The recorded weights are the post-softmax ones the forward actually used, read out of the + * same scores buffer `compute_attn_heads` left them in — nothing is recomputed, so this + * cannot drift from the arithmetic it claims to describe. + * + * ASSUMES A CONTIGUOUS CACHE (no sliding-window wraparound): positions 0..seq_len-1. The + * only caller is a debug harness stepping a fresh sequence, which is exactly that case. + * + * @status Working + * @issues None + * @todo None + */ +#ifndef PN_LLAMA_ATTENTION_TRACE_H +#define PN_LLAMA_ATTENTION_TRACE_H + +/** One query head's attention at one position of one layer. */ +typedef struct { + double entropy; /**< -sum p ln p, in nats. ln(seq_len) is the uniform maximum */ + double collision; /**< sum p^2; its reciprocal is the effective number of positions */ + double max_weight; /**< the largest single weight */ + double sink_weight;/**< the weight on position 0 — the attention sink every framing has, + and the first token a chat template differs in */ + double out_rms; /**< RMS over head_dim of this head's slice of `attn_out` */ + double vbar_rms; /**< sqrt(sum_s p_s ||v_s||^2 / head_dim) — no-cancellation reference */ + double v0_rms; /**< RMS of the position-0 V row this head averages over. A sink whose + V row is small drags vbar_rms down in proportion to sink_weight */ + double top_pos_rel;/**< argmax position / (seq_len-1) — WHERE the peak sits. Near 0 is a + prefix sink, near 1 is recency. Entropy cannot tell these apart */ + double self_weight;/**< the weight on the current token, the other slot a head can park on */ +} pn_attn_head_record; + +/** One forward's attention shape: n_layer * n_head head records plus per-layer scalars. */ +typedef struct { + pn_attn_head_record *heads; /**< n_layer * n_head, row-major by layer, owned */ + double *normed_rms; /**< n_layer — RMS of `attn_v`'s input, owned */ + int n_layer; + int n_head; + int seq_len; /**< keys attended at the position just captured */ + int recorded; /**< layers the forward now running has written */ +} pn_attention_trace; + +/** Everything one layer's capture reads. Pointers are the forward's own live buffers. */ +typedef struct { + const float *scores; /**< [n_head, max_seq] post-softmax weights */ + const float *attn_out; /**< [n_head, head_dim] the weighted V sums */ + const float *v_cache; /**< [n_kv_head, max_seq, head_dim] */ + const float *normed; /**< [n_embd] the vector the Q/K/V projections read */ + int n_head; + int n_kv_groups; + int head_dim; + int max_seq; + int seq_len; + int n_embd; +} pn_attention_trace_layer; + +/** + * Allocate a capture buffer. + * + * @return 0 on success, -1 on allocation failure + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +int pn_attention_trace_alloc(pn_attention_trace *trace, int n_layer, int n_head); + +void pn_attention_trace_free(pn_attention_trace *trace); + +/** + * Direct the next forward's per-layer attention records into `trace`, or NULL to stop. + * + * One process-wide slot, for the same reason the residual trace has one: the forward + * signature is the engine's public contract and a debug instrument does not get to change + * it. The only caller is a harness running one forward at a time. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_attention_trace_capture_into(pn_attention_trace *trace); + +/** @return whether a capture is armed, so the forward can skip work it only does to record. */ +int pn_attention_trace_armed(void); + +/** + * Record one layer's attention shape. Called by a forward implementation immediately after + * the heads are computed and BEFORE the output projection consumes `attn_out`. + */ +void pn_attention_trace_record(int layer, const pn_attention_trace_layer *in); + +/** @return the record for (layer, head) of a completed capture, or NULL if never written. */ +const pn_attn_head_record *pn_attention_trace_head(const pn_attention_trace *trace, int layer, + int head); + +#endif /* PN_LLAMA_ATTENTION_TRACE_H */ diff --git a/src/models/llama_forward_cuda.c b/src/models/llama_forward_cuda.c new file mode 100644 index 0000000..588c001 --- /dev/null +++ b/src/models/llama_forward_cuda.c @@ -0,0 +1,590 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_forward_cuda.c + * @brief Single-die GPU forward for the plain-Llama arch (MiniCPM5, Llama-3). + * + * Runs the compute-heavy parts of pn_llama_forward on the GPU — token embedding + * lookup, per-layer RMSNorm, Q/K/V/O projections, SwiGLU dense FFN and the LM + * head — via Foundry's fc_* CUDA kernels with GPU-resident (repacked) weights. + * RoPE and the GQA-softmax attention stay on the host, bit-identical to the CPU + * oracle (pn_llama_forward): the small Q/K/V vectors are read back, rotated and + * attended on the host (fp32 KV cache), and the attention output is uploaded for + * the output projection. This is the same reuse pattern the Qwen3.5 dense path + * uses (gpu_matvec + fc_rms_norm_ctx + fc_moe_silu_mul_ctx + fc_add_ctx) — no + * new attention/FFN math is written here. + * + * Scope: non-MoE softmax-attention Llama-family models on ONE die. MoE and the + * F32-legacy path fall back to the CPU forward (the loader never selects this + * forward for them). + * + * @status Correctness-first GPU forward, validated against the CPU oracle by + * temp-0 logit parity. + * @issues None + * @todo None + */ + +#include "llama.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include "../accel/simd.h" +#include "../core/engine.h" +#include "../core/qweight.h" +/* Foundry headers resolve through -I$(FOUNDRY), NOT a hardcoded ../../foundry: + * relative to this file that path is Pantheon/foundry, which exists only where + * someone has made the in-tree symlink. The K80 test machine builds against the sibling + * checkout (../foundry) and this was the one file that could not build there. */ +#include "cuda/fc_cuda.h" +#include "tensor/tensor.h" +#include "../formats/gguf.h" + +#ifdef USE_CUDA + +/* One GPU-resident quantized weight: repacked device bytes + shape + the CPU + * fallback pointer (used only for exotic quant types with no kernel). */ +typedef struct { + fc_buffer *bytes; /* repacked device bytes (NULL => host fallback) */ + pn_gguf_quant type; + int rows; + int cols; + int row_stride; /* repacked stride when bytes != NULL */ + const pn_qweight *cpu; /* host weight (fallback) */ +} llama_gpu_weight; + +/* Per-layer GPU weights. Norms live on the host (tiny F32 vectors) but are + * uploaded once so RMSNorm can run entirely on the GPU. */ +typedef struct { + fc_buffer *attn_norm; /* [n_embd] */ + llama_gpu_weight wq, wk, wv, wo; + fc_buffer *ffn_norm; /* [n_embd] */ + llama_gpu_weight w_gate, w_up, w_down; +} llama_gpu_layer; + +/* Model-level GPU residency handle (stored on pn_model->gpu). */ +typedef struct { + fc_cuda_ctx *ctx; + int n_layers; + llama_gpu_layer *layers; + fc_buffer *output_norm; /* [n_embd] */ + llama_gpu_weight output;/* LM head */ + int n_embd; + int n_vocab; + int loaded; /* 1 once every required weight is device-resident */ + /* Persistent scratch device buffers (reused every token). */ + fc_buffer *d_hidden; /* [n_embd] running residual stream */ + fc_buffer *d_normed; /* [n_embd] */ + fc_buffer *d_q; /* [n_head*head_dim] */ + fc_buffer *d_k; /* [n_kv_head*head_dim] */ + fc_buffer *d_v; /* [n_kv_head*head_dim] */ + fc_buffer *d_attn; /* [n_head*head_dim] attention output */ + fc_buffer *d_attn_proj; /* [n_embd] */ + fc_buffer *d_gate; /* [n_ff] */ + fc_buffer *d_up; /* [n_ff] */ + fc_buffer *d_ffn_out; /* [n_embd] */ + fc_buffer *d_logits; /* [n_vocab] */ + /* Host staging for the RoPE + attention round-trip. */ + float *h_q, *h_k, *h_v, *h_attn; + int q_rows, k_rows, v_rows; +} pn_llama_gpu; + +/* ============================================================ + * Upload helpers (mirror gpu_weight_upload / gpu_vec_upload) + * ============================================================ */ + +/* Repack + upload one quantized weight to a persistent device buffer. Leaves + * bytes NULL (host fallback) when the quant type has no repacked GPU form. */ +static int llama_gpu_weight_upload(fc_cuda_ctx *ctx, llama_gpu_weight *g, + const pn_qweight *w) +{ + g->cpu = w; + g->type = w->type; + g->rows = w->rows; + g->cols = w->cols; + g->row_stride = (int)w->row_stride; + g->bytes = NULL; + if (!w->data || w->rows <= 0) + return 0; + size_t rstride = fc_repack_row_bytes((int)w->type, w->cols); + if (rstride) { + size_t rtotal = (size_t)w->rows * rstride; + unsigned char *tmp = malloc(rtotal); + if (!tmp) + return -1; + if (fc_repack_weight((int)w->type, w->data, tmp, w->rows, w->cols) != 0) { + free(tmp); + return -1; + } + g->bytes = fc_buffer_from_ptr_ctx(ctx, tmp, rtotal); + free(tmp); + g->row_stride = (int)rstride; + return g->bytes ? 0 : -1; + } + if ((int)w->type == GGUF_QUANT_F32) { + size_t total = (size_t)w->rows * w->row_stride; + g->bytes = fc_buffer_from_ptr_ctx(ctx, w->data, total); + return g->bytes ? 0 : -1; + } + /* Repackable quant type with an unsupported cols, or an exotic type: keep on + * the host (llama_gpu_matvec falls back to pn_qmv). */ + return 0; +} + +/* Upload one F32 vector (norm) to a persistent device buffer. */ +static fc_buffer *llama_gpu_vec_upload(fc_cuda_ctx *ctx, const float *src, int n) +{ + if (!src || n <= 0) + return NULL; + return fc_buffer_from_ptr_ctx(ctx, src, (size_t)n * sizeof(float)); +} + +/* GPU quantized matvec y = W @ x. Dispatches by quant type; falls back to the + * host pn_qmv (via x_host/y_host staging) for types with no device kernel. */ +static void llama_gpu_matvec(fc_cuda_ctx *ctx, const llama_gpu_weight *w, + fc_buffer *x, fc_buffer *y, + float *x_host, float *y_host) +{ + if (w->bytes) { + switch ((int)w->type) { + case GGUF_QUANT_Q3_K: + fc_matvec_q3k_ctx(ctx, w->bytes, x, y, w->rows, w->cols, w->row_stride); + return; + case GGUF_QUANT_Q4_K: + fc_matvec_q4k_ctx(ctx, w->bytes, x, y, w->rows, w->cols, w->row_stride); + return; + case GGUF_QUANT_Q5_K: + fc_matvec_q5k_ctx(ctx, w->bytes, x, y, w->rows, w->cols, w->row_stride); + return; + case GGUF_QUANT_Q6_K: + fc_matvec_q6k_ctx(ctx, w->bytes, x, y, w->rows, w->cols, w->row_stride); + return; + case GGUF_QUANT_Q8_0: + fc_matvec_q80_ctx(ctx, w->bytes, x, y, w->rows, w->cols, w->row_stride); + return; + case GGUF_QUANT_F32: + fc_matvec_f32_ctx(ctx, w->bytes, x, y, w->rows, w->cols, w->row_stride); + return; + default: + break; + } + } + /* Host fallback (exotic quant only). */ + fc_sync_ctx(ctx); + fc_buffer_to_cpu(x, x_host); + pn_qmv(w->cpu, x_host, y_host); + fc_buffer_from_cpu(y, y_host); +} + +/* True if every quantized weight of a layer became device-resident (so the + * layer runs fully on the GPU with no per-op host round-trip). */ +static int layer_weight_ok(const llama_gpu_weight *w) +{ + /* A zero-row weight (absent, e.g. MoE gate/up/down on a dense model) is fine + * — it is simply never used. A present weight must have device bytes. */ + return w->rows == 0 || w->bytes != NULL; +} + +/* ============================================================ + * Upload / teardown + * ============================================================ */ + +/* Drop the quantized-weight page cache after the GPU upload. Once every weight + * is device-resident the host mmap of the quant bytes is dead for the GPU path, + * but on the 8 GB host that page cache is exactly what drains MemAvailable and + * trips admission. Advise the kernel to drop it (mirror of qwen35moe's + * gpu_drop_weight_pages so the dense path gets the same reclaim). Safe: the + * mapping is MAP_PRIVATE + PROT_READ, so a rare host-fallback matvec (exotic + * quant with no kernel) re-faults the clean pages straight from the file — + * correct, just slow. The F32 tok_embd/norms are separate heap copies (NOT in + * this mmap), so they are untouched. Default-ON (the GPU-resident design) since + * the host-RAM win + decode-neutrality were validated on a K80 host 2026-07-24 on the + * MoE path; opt OUT with QWEN35_DROP_WEIGHTS=0 for host-fallback debugging. */ +static void llama_gpu_drop_weight_pages(const pn_model *model) +{ + const char *e = getenv("QWEN35_DROP_WEIGHTS"); + if (e && e[0] == '0') + return; + const pn_llama_qweights *qw = (const pn_llama_qweights *)model->weights; + if (!qw) + return; + pn_gguf_file *gf = qw->gguf; + if (!gf || !gf->data || gf->data_size == 0) + return; + long pg = sysconf(_SC_PAGESIZE); + if (pg <= 0) + return; + uintptr_t ds = (uintptr_t)gf->data; + uintptr_t start = (ds + (uintptr_t)pg - 1) & ~((uintptr_t)pg - 1); /* page-align up */ + uintptr_t end = ds + gf->data_size; + if (end <= start) + return; + size_t len = (size_t)(end - start) & ~((size_t)pg - 1); /* whole pages only */ + if (len == 0) + return; + if (madvise((void *)start, len, MADV_DONTNEED) == 0) + fprintf(stderr, "llama: dropped %llu MiB host weight page cache " + "(MADV_DONTNEED; weights resident in VRAM)\n", + (unsigned long long)(len >> 20)); + else + fprintf(stderr, "llama: MADV_DONTNEED on weight pages failed " + "(errno %d); host cache not dropped\n", errno); +} + +int pn_llama_gpu_upload(pn_model *model) +{ + if (!model || !model->weights) + return -1; + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + if (!qw->is_quantized) + return -1; /* F32 legacy path stays on the CPU */ + pn_config *cfg = &model->config; + if (cfg->n_experts > 0) + return -1; /* MoE not handled here */ + if (!fc_available()) + return 0; /* graceful skip (no GPU) */ + if (model->gpu) + return 0; /* idempotent */ + + pn_llama_gpu *g = calloc(1, sizeof(*g)); + if (!g) + return -1; + /* Honour the explicit placement (`--dies N`). Hard-coding ordinal 0 here + * only worked when CUDA_VISIBLE_DEVICES happened to pin the die to 0; with + * the full device list visible it put a model asked for die 6 onto die 0 — + * a die another serve owns. Unplaced models keep the ordinal-0 default. */ + int die = (model->n_gpu_dies > 0) ? model->gpu_dies[0] : 0; + g->ctx = fc_cuda_init_device(die); + if (!g->ctx || !fc_cuda_ctx_available(g->ctx)) { + fprintf(stderr, "llama: fc_cuda_init_device(%d) failed\n", die); + free(g); + return -1; + } + fprintf(stderr, "llama: GPU upload -> die %d\n", die); + g->n_layers = qw->n_layers; + g->n_embd = cfg->n_embd; + g->n_vocab = cfg->n_vocab; + g->layers = calloc(qw->n_layers, sizeof(*g->layers)); + if (!g->layers) + goto fail; + + int head_dim = pn_head_dim(cfg); + int q_rows = cfg->n_head * head_dim; + int kv_rows = cfg->n_kv_head * head_dim; + + for (int i = 0; i < qw->n_layers; i++) { + pn_llama_qlayer *l = &qw->layers[i]; + llama_gpu_layer *d = &g->layers[i]; + d->attn_norm = llama_gpu_vec_upload(g->ctx, l->attn_norm, cfg->n_embd); + d->ffn_norm = llama_gpu_vec_upload(g->ctx, l->ffn_norm, cfg->n_embd); + if (llama_gpu_weight_upload(g->ctx, &d->wq, &l->wq) != 0 || + llama_gpu_weight_upload(g->ctx, &d->wk, &l->wk) != 0 || + llama_gpu_weight_upload(g->ctx, &d->wv, &l->wv) != 0 || + llama_gpu_weight_upload(g->ctx, &d->wo, &l->wo) != 0 || + llama_gpu_weight_upload(g->ctx, &d->w_gate, &l->w_gate) != 0 || + llama_gpu_weight_upload(g->ctx, &d->w_up, &l->w_up) != 0 || + llama_gpu_weight_upload(g->ctx, &d->w_down, &l->w_down) != 0) + goto fail; + if (!d->attn_norm || !d->ffn_norm || + !layer_weight_ok(&d->wq) || !layer_weight_ok(&d->wk) || + !layer_weight_ok(&d->wv) || !layer_weight_ok(&d->wo) || + !layer_weight_ok(&d->w_gate) || !layer_weight_ok(&d->w_up) || + !layer_weight_ok(&d->w_down)) { + fprintf(stderr, "llama: layer %d has a weight the GPU can't hold; " + "aborting GPU upload\n", i); + goto fail; + } + } + + g->output_norm = llama_gpu_vec_upload(g->ctx, qw->output_norm, cfg->n_embd); + if (llama_gpu_weight_upload(g->ctx, &g->output, &qw->output) != 0) + goto fail; + if (!g->output_norm || !layer_weight_ok(&g->output)) { + fprintf(stderr, "llama: LM head not GPU-resident; aborting GPU upload\n"); + goto fail; + } + + /* Persistent per-token scratch. */ + g->q_rows = q_rows; + g->k_rows = kv_rows; + g->v_rows = kv_rows; + g->d_hidden = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_embd * sizeof(float)); + g->d_normed = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_embd * sizeof(float)); + g->d_q = fc_buffer_alloc_ctx(g->ctx, (size_t)q_rows * sizeof(float)); + g->d_k = fc_buffer_alloc_ctx(g->ctx, (size_t)kv_rows * sizeof(float)); + g->d_v = fc_buffer_alloc_ctx(g->ctx, (size_t)kv_rows * sizeof(float)); + g->d_attn = fc_buffer_alloc_ctx(g->ctx, (size_t)q_rows * sizeof(float)); + g->d_attn_proj = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_embd * sizeof(float)); + g->d_gate = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_ff * sizeof(float)); + g->d_up = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_ff * sizeof(float)); + g->d_ffn_out = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_embd * sizeof(float)); + g->d_logits = fc_buffer_alloc_ctx(g->ctx, (size_t)cfg->n_vocab * sizeof(float)); + g->h_q = malloc((size_t)q_rows * sizeof(float)); + g->h_k = malloc((size_t)kv_rows * sizeof(float)); + g->h_v = malloc((size_t)kv_rows * sizeof(float)); + g->h_attn = malloc((size_t)q_rows * sizeof(float)); + if (!g->d_hidden || !g->d_normed || !g->d_q || !g->d_k || !g->d_v || + !g->d_attn || !g->d_attn_proj || !g->d_gate || !g->d_up || + !g->d_ffn_out || !g->d_logits || !g->h_q || !g->h_k || !g->h_v || + !g->h_attn) + goto fail; + + g->loaded = 1; + model->gpu = g; + /* Weights are fully device-resident now — drop the host mmap page cache + * (gated on QWEN35_DROP_WEIGHTS until validated on a K80 host). */ + llama_gpu_drop_weight_pages(model); + fprintf(stderr, "llama: on GPU (single die, %d layers)\n", g->n_layers); + return 0; + +fail: + model->gpu = g; /* let pn_llama_gpu_free reclaim partial state */ + pn_llama_gpu_free(model); + return -1; +} + +int pn_llama_model_gpu_loaded(const pn_model *model) +{ + if (!model || !model->gpu) + return 0; + return ((const pn_llama_gpu *)model->gpu)->loaded; +} + +void pn_llama_gpu_free(pn_model *model) +{ + if (!model || !model->gpu) + return; + pn_llama_gpu *g = (pn_llama_gpu *)model->gpu; + if (g->layers) { + for (int i = 0; i < g->n_layers; i++) { + llama_gpu_layer *d = &g->layers[i]; + fc_buffer_free(d->attn_norm); + fc_buffer_free(d->ffn_norm); + fc_buffer_free(d->wq.bytes); + fc_buffer_free(d->wk.bytes); + fc_buffer_free(d->wv.bytes); + fc_buffer_free(d->wo.bytes); + fc_buffer_free(d->w_gate.bytes); + fc_buffer_free(d->w_up.bytes); + fc_buffer_free(d->w_down.bytes); + } + free(g->layers); + } + fc_buffer_free(g->output_norm); + fc_buffer_free(g->output.bytes); + fc_buffer_free(g->d_hidden); + fc_buffer_free(g->d_normed); + fc_buffer_free(g->d_q); + fc_buffer_free(g->d_k); + fc_buffer_free(g->d_v); + fc_buffer_free(g->d_attn); + fc_buffer_free(g->d_attn_proj); + fc_buffer_free(g->d_gate); + fc_buffer_free(g->d_up); + fc_buffer_free(g->d_ffn_out); + fc_buffer_free(g->d_logits); + free(g->h_q); + free(g->h_k); + free(g->h_v); + free(g->h_attn); + if (g->ctx) + fc_cuda_ctx_free(g->ctx); + free(g); + model->gpu = NULL; +} + +/* ============================================================ + * Host RoPE + GQA-softmax attention (bit-parity with the CPU oracle) + * ============================================================ */ + +/* Interleaved RoPE (Llama/Qwen2 permuted GGUF layout) — identical math to + * llama.c apply_rope_raw. MiniCPM5 (PN_ARCH_LLAMA) takes this path. */ +static void llama_rope_raw(float *data, int n_heads, int head_dim, + int rope_dim, int pos, float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < rot; i += 2) { + float freq = 1.0f / powf(theta_base, (float)i / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i], x1 = head[i + 1]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + 1] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* NEOX RoPE (Qwen3 unpermuted GGUF layout) — identical to llama.c + * apply_rope_neox. */ +static void llama_rope_neox(float *data, int n_heads, int head_dim, + int rope_dim, int pos, float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + float freq = 1.0f / powf(theta_base, (float)(2 * i) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i], x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* GQA softmax attention for all heads — identical math to llama.c + * compute_attn_heads (full-precision softmax, weighted V sum). Serial: attention + * is tiny relative to the projections and this runs on 2 host cores. */ +static void llama_attn_heads(const float *q, const float *k_cache, + const float *v_cache, float *attn_out, + float *scores, int n_head, int n_kv_groups, + int head_dim, int max_seq, int seq_len, float scale) +{ + for (int qh = 0; qh < n_head; qh++) { + int kv_h = qh / n_kv_groups; + const float *q_vec = q + (size_t)qh * head_dim; + const float *k_base = k_cache + (size_t)kv_h * max_seq * head_dim; + const float *v_base = v_cache + (size_t)kv_h * max_seq * head_dim; + float *out_vec = attn_out + (size_t)qh * head_dim; + for (int s = 0; s < seq_len; s++) + scores[s] = pn_dot_f32(q_vec, k_base + (size_t)s * head_dim, head_dim) * scale; + float max_score = scores[0]; + for (int s = 1; s < seq_len; s++) + if (scores[s] > max_score) max_score = scores[s]; + float sum_exp = 0.0f; + for (int s = 0; s < seq_len; s++) { + scores[s] = expf(scores[s] - max_score); + sum_exp += scores[s]; + } + float inv_sum = 1.0f / sum_exp; + memset(out_vec, 0, head_dim * sizeof(float)); + for (int s = 0; s < seq_len; s++) { + float w_s = scores[s] * inv_sum; + const float *v_vec = v_base + (size_t)s * head_dim; + for (int d = 0; d < head_dim; d++) + out_vec[d] += w_s * v_vec[d]; + } + } +} + +/* ============================================================ + * Forward + * ============================================================ */ + +tm_tensor *pn_llama_forward_cuda(pn_model *model, uint32_t token, pn_state *state) +{ + if (!model || !state || !model->weights || !model->gpu) + return NULL; + pn_llama_gpu *g = (pn_llama_gpu *)model->gpu; + if (!g->loaded) + return NULL; + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + fc_cuda_ctx *ctx = g->ctx; + + int n_embd = cfg->n_embd; + int head_dim = pn_head_dim(cfg); + int n_kv_groups = cfg->n_head / cfg->n_kv_head; + int max_seq = kv->max_seq; + int pos = state->pos; + float eps = cfg->rms_norm_eps; + + /* 1. Token embedding -> device hidden. */ + if (state->embd_override) + fc_buffer_from_cpu(g->d_hidden, state->embd_override); + else + fc_buffer_from_cpu(g->d_hidden, + qw->tok_embd + (size_t)token * n_embd); + + float *scores = malloc((size_t)max_seq * sizeof(float)); + if (!scores) + return NULL; + + for (int layer = 0; layer < cfg->n_layer; layer++) { + llama_gpu_layer *d = &g->layers[layer]; + + /* === Attention === */ + fc_rms_norm_ctx(ctx, g->d_hidden, d->attn_norm, g->d_normed, n_embd, eps); + llama_gpu_matvec(ctx, &d->wq, g->d_normed, g->d_q, g->h_q, g->h_q); + llama_gpu_matvec(ctx, &d->wk, g->d_normed, g->d_k, g->h_k, g->h_k); + llama_gpu_matvec(ctx, &d->wv, g->d_normed, g->d_v, g->h_v, g->h_v); + + /* RoPE + GQA softmax on the host (parity with the CPU oracle). */ + fc_sync_ctx(ctx); + fc_buffer_to_cpu(g->d_q, g->h_q); + fc_buffer_to_cpu(g->d_k, g->h_k); + fc_buffer_to_cpu(g->d_v, g->h_v); + + if (cfg->arch == PN_ARCH_QWEN3) { + llama_rope_neox(g->h_q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + llama_rope_neox(g->h_k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } else { + llama_rope_raw(g->h_q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + llama_rope_raw(g->h_k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } + + float *k_cache_data = (float *)kv->k_cache[layer]->data; + float *v_cache_data = (float *)kv->v_cache[layer]->data; + int cache_pos = pos % max_seq; + for (int h = 0; h < cfg->n_kv_head; h++) { + memcpy(k_cache_data + ((size_t)h * max_seq + cache_pos) * head_dim, + g->h_k + (size_t)h * head_dim, head_dim * sizeof(float)); + memcpy(v_cache_data + ((size_t)h * max_seq + cache_pos) * head_dim, + g->h_v + (size_t)h * head_dim, head_dim * sizeof(float)); + } + + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + llama_attn_heads(g->h_q, k_cache_data, v_cache_data, g->h_attn, scores, + cfg->n_head, n_kv_groups, head_dim, max_seq, seq_len, + 1.0f / sqrtf((float)head_dim)); + + /* Output projection + residual, back on the GPU. */ + fc_buffer_from_cpu(g->d_attn, g->h_attn); + llama_gpu_matvec(ctx, &d->wo, g->d_attn, g->d_attn_proj, + g->h_attn, g->h_attn); + fc_add_ctx(ctx, g->d_hidden, g->d_attn_proj, g->d_hidden, n_embd); + + /* === SwiGLU FFN === */ + fc_rms_norm_ctx(ctx, g->d_hidden, d->ffn_norm, g->d_normed, n_embd, eps); + llama_gpu_matvec(ctx, &d->w_gate, g->d_normed, g->d_gate, g->h_q, g->h_q); + llama_gpu_matvec(ctx, &d->w_up, g->d_normed, g->d_up, g->h_q, g->h_q); + fc_moe_silu_mul_ctx(ctx, g->d_gate, g->d_up, g->d_gate, cfg->n_ff); + llama_gpu_matvec(ctx, &d->w_down, g->d_gate, g->d_ffn_out, g->h_q, g->h_q); + fc_add_ctx(ctx, g->d_hidden, g->d_ffn_out, g->d_hidden, n_embd); + } + + /* Final norm + LM head. */ + fc_rms_norm_ctx(ctx, g->d_hidden, g->output_norm, g->d_normed, n_embd, eps); + llama_gpu_matvec(ctx, &g->output, g->d_normed, g->d_logits, g->h_q, g->h_q); + + float *logits_raw = malloc((size_t)cfg->n_vocab * sizeof(float)); + if (!logits_raw) { + free(scores); + return NULL; + } + fc_sync_ctx(ctx); + fc_buffer_to_cpu(g->d_logits, logits_raw); + free(scores); + + int64_t logits_shape[] = { cfg->n_vocab }; + tm_tensor *logits_t = tm_from_data(logits_raw, logits_shape, 1, TM_F32); + tm_tensor *logits = logits_t ? tm_clone(logits_t) : NULL; + if (logits_t) tm_free(logits_t); + free(logits_raw); + + state->pos++; + return logits; +} + +#endif /* USE_CUDA */ diff --git a/src/models/llama_forward_tt.c b/src/models/llama_forward_tt.c new file mode 100644 index 0000000..b6cec33 --- /dev/null +++ b/src/models/llama_forward_tt.c @@ -0,0 +1,2611 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_forward_tt.c + * @brief The `llama` decode forward on a Tenstorrent Blackhole card — projections, norms, + * the SwiGLU gate, the residual adds and the LM head's top-N all on the card, with + * the activation kept RESIDENT IN CARD DRAM between them. + * + * Structurally this is still llama_forward_cuda.c: the same ordered layer and the + * same host RoPE / KV append / GQA softmax kept for bit-parity with the CPU oracle. + * What differs is that the hidden state does not come back to the host between ops. + * + * --------------------------------------------------------------------------- + * WHY THE ACTIVATION STAYS ON THE CARD, AS A NUMBER + * --------------------------------------------------------------------------- + * Card->host is ~4.7 MB/s and there is no device->host DMA on Blackhole, so a forward that + * returns every projection's result to the host spends the token on the link: 0.412 s of a + * 0.568 s token, 73%, after two rounds of making that transfer as small as it can be. It + * cannot be made smaller — a matvec's result is already only logical row 0 of each tile. + * The only move left is not to make it, which means the ops BETWEEN the matmuls have to run + * on the card too. They do, each on its own resident core (pn_tt_device.h), and they pass + * activations to each other through DRAM. + * + * WHAT STILL CROSSES. `q`, `k` and `v` come back every layer and `attn` goes out. It USED + * to be forced: the on-card RoPE kernel refuses the INTERLEAVED rotary pairing this model + * family takes and the converter did not permute wq/wk. **The converter does now** + * (`--rope-pairing split-half`, recorded in the artifact header), so the kernel's refusal + * is no longer what holds the readback here — the host-side KV append and GQA softmax are. + * Those need q/k/v on the host whatever the pairing is, so moving RoPE on-card ALONE + * removes no transfer; RoPE, KV-append and attention have to move together. + * + * --------------------------------------------------------------------------- + * THE INSTRUMENT — build it before you need it + * --------------------------------------------------------------------------- + * With 24 layers and 7 projections each, "the token is wrong" is 169 candidate ops. So + * every matvec can be scored against Pantheon's own CPU `pn_qmv` on the SAME input, in the + * same call, under `PN_TT_TRACE`: + * + * PN_TT_TRACE=1 one line per op: shape, max|Δ|, PCC, and the worst element's index + * PN_TT_TRACE=2 the above, plus a per-layer summary of the worst op in that layer + * + * Tracing now DOWNLOADS each op's input and output, because they no longer pass through + * host memory. That is a debug cost by construction and it is the reason the trace is a + * knob rather than a build. + * + * Expect a NON-ZERO Δ everywhere and do not read it as a bug: the card's activation format + * is bf16 and its weights are Bfp8_b, against an fp32 host reference over Q8_0. What the + * trace is for is finding the op where Δ stops looking like rounding. + * + * --------------------------------------------------------------------------- + * MOVING AN OP ON-CARD MOVES ITS NUMERICS + * --------------------------------------------------------------------------- + * The host computes norms, the SwiGLU gate and the residual adds in fp32 over an fp32 + * residual stream. The card computes them in bf16 with an fp32 destination accumulator and + * writes bf16 back, so every stage moved is one more bf16 rounding of the residual stream. + * `PN_TT_ONCARD` selects which ops move, so the A/B is one binary — and the difference it + * shows is a RESULT, not a regression to tune away. + * + * @status Norms, SwiGLU, residual adds and top-N on the card behind PN_TT_ONCARD. The + * generation loop runs: token N+1 decodes from token N's KV state, token for token + * against the CPU oracle, through `pantheon --chat` and through `pantheon serve`. + * The artifact is produced from the `.gguf` and cached on first load — a serve gets + * a checkpoint path and nothing else (llama_tt_resolve_artifact). + * @issues None + * @todo + * - [ ] Move RoPE, the KV append and the GQA softmax on-card TOGETHER — separately none + * of them removes a byte, because q/k/v cross for whichever of the three is left. + */ + +#ifdef USE_TT + +#include +#include +#include +#include + +#include "llama.h" +#include "llama_residual_trace.h" + +#include "../accel/simd.h" +#include "../accel/tt/pn_tt_device.h" +#include "../accel/tt/pn_tt_matmul_group.h" +#include "../core/engine.h" +#include "../core/qweight.h" +#include "tt/convert/ft_float_convert.h" +#include "tt/convert/ft_tt_artifact_cache.h" +#include "tt/ft_tt_memmap.h" + +/** + * An artifact to use INSTEAD of the cache — an override, not the normal path. + * + * The normal path is `--model something.gguf` and nothing else: the artifact is produced + * from the checkpoint on first load and cached beside it. This variable exists for the case + * the cache cannot serve — an artifact converted with an experimental option, or two + * artifacts under comparison — and it is an OVERRIDE in the strict sense: it is used + * verbatim, its provenance is NOT checked against the checkpoint, and if it does not load + * the run refuses rather than quietly converting a fresh one. That asymmetry is deliberate + * and is the one `ft_tt_program_open_kernels()` already uses for a program-directory + * override: an explicit choice wins or fails, and the default is the cache. + */ +#define PN_TT_ARTIFACT_ENV "PN_TT_ARTIFACT" + +/** Where cached artifacts live. Unset keeps them beside the checkpoint. */ +#define PN_TT_ARTIFACT_CACHE_DIR_ENV "PN_TT_ARTIFACT_CACHE_DIR" + +/** Force a rung by name (`bfp8`/`bfp4`) instead of letting the fit choose. An INSTRUMENT: + * it still has to fit, and a forced rung that does not is refused with the arithmetic. */ +#define PN_TT_RUNG_ENV "PN_TT_RUNG" + +/** Force a rotary pairing by name. An instrument; the default is read off the checkpoint. */ +#define PN_TT_ROPE_PAIRING_ENV "PN_TT_ROPE_PAIRING" + +/** Context the rung choice must leave KV room for. Defaults to the model's own cap. */ +#define PN_TT_CONTEXT_TOKENS_ENV "PN_TT_CONTEXT_TOKENS" + +/** + * Card DRAM to hold back from the weights, in MiB. + * + * NOT A MEASUREMENT. It is a reserve for everything the forward allocates that is neither a + * weight nor the KV cache — the rotary table, the activation scratch, the reduce and + * elementwise residencies' buffers — and it is stated as a round number rather than dressed + * up as one, because nobody has instrumented the allocator's high-water mark. Raise it if a + * model the planner says fits runs the allocator out. + */ +#define PN_TT_CARD_RESERVE_MIB_ENV "PN_TT_CARD_RESERVE_MIB" +#define PN_TT_CARD_RESERVE_MIB_DEFAULT 512u + +/** Per-op divergence reporting. 0 off, 1 per op, 2 per op plus a per-layer worst. */ +#define PN_TT_TRACE_ENV "PN_TT_TRACE" + +/** Present the LM head as a SPARSE row built from the on-card top-N instead of reading the + * whole vocabulary back — which is the shape the K80 path already ships (`gpu_present_topk`) + * and the only arm in which the logit row genuinely never crosses the link. Off by default, + * because the parity harness scores a full row. */ +#define PN_TT_SPARSE_LOGITS_ENV "PN_TT_SPARSE_LOGITS" + +/** + * Run BOTH attention arms every layer and compare them element for element. + * + * The crossed control for on-card attention: the host arm keeps its own fp32 KV cache and + * computes the GQA softmax as it always did, the card arm keeps its bf16 cache and runs + * flash decode, and the two answers are scored against each other. It costs the readback + * on-card attention exists to remove, so it is a knob and never a build. + */ +#define PN_TT_ATTN_CHECK_ENV "PN_TT_ATTN_CHECK" + +/** + * Recover, from the card itself, WHICH rotary frequency it applied to WHICH feature slot. + * + * The standing hypothesis for the on-card attention defect is that the card's rotary re-pairs + * what `--rope-pairing split-half` un-paired. An attention-output check cannot answer it: + * `q . k` is invariant under any pairing applied to BOTH q and k, so a consistently wrong + * pairing costs the attention output only its rounding — which is exactly the regime the + * shipping attn-check already reads (max|d| 0.039). So this asks the question directly. + * + * A rotation is invertible from its own operands. With `x` the pre-RoPE strip and `y` the + * post-RoPE strip, both read off the card, slot `j` and its partner `j+half` satisfy + * + * y[j] = x[j] cos t - x[j+half] sin t + * y[j+half] = x[j] sin t + x[j+half] cos t + * + * so `cos t` and `sin t` fall out of two dot products and `t = atan2(sin t, cos t)` is the + * angle the card ACTUALLY used, to bf16 precision, with no assumption about the table. + * Comparing it against `position * theta^(-2p/rot)` for every candidate pair index `p` names + * the frequency the card put on that slot. + */ +#define PN_TT_ROPE_CHECK_ENV "PN_TT_ROPE_CHECK" + +/** + * Measure what rows 1-31 of the strips hold on the on-card attention arm. + * + * pn_tt_oncard.c invariant 1 records them as INFERRED harmless and NOT measured: flash decode + * writes whole output tiles, so `attn` — and through the residual add, `hidden` — are no + * longer known to be zero there. An inferred-harmless assumption underneath a live defect + * investigation is worth one measurement. + */ +#define PN_TT_ROW_CENSUS_ENV "PN_TT_ROW_CENSUS" + +/** + * Emulate ONE of the card's attention roundings inside the HOST arm, so the gate can price it. + * + * Phase 7.7 records that on-card attention fails the gate where the host arm passes, and that + * "which of the two on-card changes supplies that rounding is NOT separated" — separating them + * on the card needs an fp32 KV program that does not exist. It does NOT need one HERE: the + * host arm can be given each of the card's roundings one at a time, and the gate then prices + * each in the only currency that matters, per-document perplexity. + * + * A comma-separated list of: + * `bf16table` round the host rotary's cos/sin to bf16 — the card's table format. This is + * the one rounding that is COHERENT across a whole head: it perturbs the ANGLE + * at a position, so every score against that position moves together. + * `bf16rot` round the host's rotated q to bf16 — the card's q strip is bf16. + * `bf16kv` round k and v to bf16 on the way into the host KV cache — the card's cache. + * `bf16softmax` the flash-decode kernel's own INTERMEDIATE precision. `attention_float16b` + * makes EVERY CB Float16_b, so `cb_qs` (Q pre-scaled by 1/sqrt(head_dim)), + * `cb_s` (the scores, then the exponentials in place), the running max and the + * row sums all round to bf16 between passes, while the host holds every one of + * them in fp32. This is the only one of the four that is not a storage format + * anybody chose — it is what running the same arithmetic on the card costs. + * `bf16qscale` / `bf16scores` — the two HALVES of `bf16softmax`, so the half with a cheap + * fix can be priced alone. `bf16qscale` is only the pre-scale into `cb_qs`; + * `bf16scores` is everything downstream of it. `scale_query()` already records + * that the pre-scale could be fused into `exp_tile`'s `scale_en` argument for + * one fewer pass, so if that half carries the damage the fix is cheaper AND + * faster than what ships. + * + * They compose, so naming them all is the whole emulation and each name alone is its share. + * This changes the HOST arm only; it is an instrument and never a default. + */ +#define PN_TT_HOST_EMULATE_ENV "PN_TT_HOST_EMULATE" + +/** The widest head this file's on-stack per-head scratch serves. MiniCPM5-1B is 128 and + * Qwen3.8 is 256; a model past this is a refusal rather than a stack overrun. */ +#define LLAMA_TT_MAX_HEAD_DIM 512 + +/** A recovered rotation is only readable where the pair carries signal: below this the + * denominator `x[j]^2 + x[j+half]^2` is bf16 noise and the angle is arbitrary. */ +#define LLAMA_TT_ROPE_MIN_PAIR_ENERGY 1e-4 + +/** `-std=c11` sets `__STRICT_ANSI__` and glibc then does NOT define `M_PI`. Spelling it here + * is one line; discovering it on a Linux host's GCC after a Mac build succeeded is a round. */ +#define LLAMA_TT_PI 3.14159265358979323846 + +/** Two candidate pair indices are only DISTINGUISHABLE when their angles differ by more than + * the bf16 table's own error. Slots below this are counted separately rather than voted. */ +#define LLAMA_TT_ROPE_MIN_SEPARATION 0.05 + +/** How far apart two attention outputs may be before an element is called DIVERGED. + * + * Not a pass/fail threshold — the report prints max|Δ| and the count on both sides of it. + * It is set at the scale a bf16 KV cache against an fp32 one is EXPECTED to differ by, so + * the count separates "rounding, everywhere" from "one element is wrong", which is the + * distinction a permutation or an off-by-one position makes and a norm does not. */ +#define LLAMA_TT_ATTN_DIVERGENCE 0.05 + +/** Pairs asked of the on-card top-N. */ +#define PN_TT_TOPK_PAIRS 8u + +/** One layer's resident projections and the two norms in front of them. */ +typedef struct { + pn_tt_weight wq, wk, wv, wo; + pn_tt_weight w_gate, w_up, w_down; + pn_tt_norm_constants attn_norm, ffn_norm; +} pn_llama_tt_layer; + +/** + * Every activation of one token, resident in card DRAM. + * + * `hidden` is TWO strips and not one because the residual add is a stream op: the reader is + * ahead of the writer, so writing the result back over an input would be safe today and + * would be an unstated dependency on the reader's lead. Two strips and a slot index cost a + * few KiB of a 31 GiB card and depend on nothing. + */ +typedef struct { + pn_tt_activation hidden[2]; + pn_tt_activation normed; + pn_tt_activation q, k, v; + /** RoPE is out of place: the reader runs ahead of the writer, so rotating over the input + * would be an unstated dependency on that lead — the same argument `hidden` makes. */ + pn_tt_activation q_rot, k_rot; + pn_tt_activation attn; /**< the GQA output going back out for wo */ + pn_tt_activation project; /**< wo's and ffn_down's output, before the residual add */ + pn_tt_activation gate, up, swiglu; + pn_tt_activation logits; + int slot; /**< which `hidden` holds the live residual stream */ +} pn_llama_tt_strips; + +/** The model-level handle, stored on pn_model::gpu. */ +typedef struct { + pn_tt_device *device; + pn_llama_tt_layer *layers; + pn_tt_weight output; + pn_tt_norm_constants output_norm; + pn_llama_tt_strips card; + int n_layers; + bool loaded; + bool sparse_logits; + bool attn_check; /**< run both attention arms and score them against each other */ + bool rope_check; /**< recover the card's own rotary angles and name the pairing */ + bool row_census; /**< measure rows 1-31 of the strips on the on-card arm */ + /* PN_TT_HOST_EMULATE: give the HOST arm one of the card's roundings so the gate can + * price it. Three independent bits, because the point is to separate them. */ + bool emulate_bf16_table, emulate_bf16_rot, emulate_bf16_kv; + /** `bf16softmax` is BOTH of these; `bf16qscale` and `bf16scores` are its two halves, so + * the one with a cheap fix can be priced on its own. */ + bool emulate_bf16_q_scale, emulate_bf16_scores; + bool attn_armed; /**< arming has been ATTEMPTED; see attn_refused for the verdict */ + /** Arming was asked for and FAILED. It latches, so every later token refuses too: a + * refusal that becomes a silent host-side fallback on token 2 is the exact defect the + * matmul residency's missing-program incident cost a round to. */ + bool attn_refused; + int trace; + + /* Where the residual stream currently is. Both may be true after a sync; neither is + * true only before the first token's embedding lands. */ + bool hidden_on_host; + bool hidden_on_card; + + /* Per-BATCH host scratch — [rows x width] each, allocated the first time a batched + * prefill runs and NULL until then, so a decode-only run pays nothing for them. Kept + * SEPARATE from the per-token buffers below rather than resizing those: the single-token + * forward is the shipping path and every recorded figure is its, so the batched path is + * not allowed to change a line of it. */ + float *b_hidden, *b_normed, *b_q, *b_k, *b_v, *b_attn, *b_gate, *b_up, *b_scratch; + int b_rows; /**< rows the batch buffers are sized for; 0 = not allocated */ + + /* Per-token host scratch, allocated once at upload. */ + float *hidden, *normed, *q, *k, *v, *attn, *gate, *up, *ffn_out, *scores, *reference; + float *trace_in, *trace_out; + float *attn_card; /**< the card's attention output, under PN_TT_ATTN_CHECK only */ + /** The pre- and post-RoPE strips read straight off the card, under PN_TT_ROPE_CHECK, and + * one strip's rows 1-31 under PN_TT_ROW_CENSUS. Widest strip either needs. */ + float *probe_in, *probe_out; + int probe_width; + int scores_len; + + /* The crossed control's running verdict over the whole run. */ + double attn_check_worst; + int attn_check_worst_layer; + long attn_check_diverged; + double kv_export_worst; + int kv_export_worst_layer; + + /* The rotary probe's running verdict over the whole run. */ + long rope_slots_identity, rope_slots_reflected, rope_slots_other, rope_slots_degenerate; + double rope_worst_identity_residual, rope_worst_norm_error; + /** How far from its own index the winning candidate ever sat, and the angular gap between + * the two at that slot. A winner one index away whose angles differ by less than the bf16 + * table's own error is the ladder being finer than the instrument, not a mis-pairing. */ + int rope_worst_other_distance; + double rope_worst_other_separation; + + /* The row census's running verdict: the largest magnitude seen in rows 1-31, and how many + * of those elements were not finite. */ + double row_census_worst; + long row_census_nonfinite; + + /* The on-card top-N of the last token, and the host argmax it is gated against. */ + uint32_t topk_index[PN_TT_TOPK_PAIRS]; + float topk_value[PN_TT_TOPK_PAIRS]; + uint32_t topk_count; + + /* The device's phase counters as of the PREVIOUS token, so the report can print a + * per-token cost. The counters themselves are monotonic and never reset — and a running + * total printed under the words "token done" was read as a per-token figure and turned a + * 1.9x dispatch regression into a reported 73x. Print both, label both. */ + double last_upload, last_dispatch, last_readback, last_oncard; + uint64_t last_matvecs; +} pn_llama_tt; + +/* ============================================================ + * Host RoPE + GQA-softmax attention + * + * Copied from llama_forward_cuda.c, which copied them from llama.c's oracle. They are + * duplicated rather than shared because bit-parity with the CPU forward is the whole + * acceptance criterion here: if these drift, an argmax mismatch stops being evidence about + * the card. + * ============================================================ */ + +/* Interleaved RoPE (Llama/Qwen2 permuted GGUF layout) — identical math to llama.c + * apply_rope_raw. MiniCPM5 (PN_ARCH_LLAMA) takes this path, and it is also the reason the + * on-card RoPE kernel cannot be used (pn_tt_device.h). */ +static void llama_tt_rope_raw(float *data, int n_heads, int head_dim, int rope_dim, int pos, + float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < rot; i += 2) { + float freq = 1.0f / powf(theta_base, (float)i / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i], x1 = head[i + 1]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + 1] = x0 * sin_a + x1 * cos_a; + } + } +} + +/** One value put through the card's storage format and back. */ +static float llama_tt_to_bf16_and_back(float value) +{ + return ft_float_from_bf16(ft_bf16_from_float(value)); +} + +/** `count` values through the card's storage format, in place. */ +static void llama_tt_round_to_bf16(float *values, int count) +{ + for (int i = 0; i < count; i++) + values[i] = llama_tt_to_bf16_and_back(values[i]); +} + +/** + * NEOX RoPE with the cos/sin rounded to bf16 — the CARD's table, on the HOST's arm. + * + * Everything else is `llama_tt_rope_neox` character for character, so the gate reading is + * attributable to the table format and to nothing else. The angle is still reduced in float + * exactly as the shipping path reduces it; only the two table values are narrowed, which is + * what `ft_tt_op_rope_build_table()` does on the card (`ft_bf16_from_float((float)cos(angle))`). + * + * This is an instrument reached only through PN_TT_HOST_EMULATE. + */ +static void llama_tt_rope_neox_bf16_table(float *data, int n_heads, int head_dim, int rope_dim, + int pos, float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + float freq = 1.0f / powf(theta_base, (float)(2 * i) / (float)rot); + float angle = (float)pos * freq; + float cos_a = llama_tt_to_bf16_and_back(cosf(angle)); + float sin_a = llama_tt_to_bf16_and_back(sinf(angle)); + float x0 = head[i], x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* NEOX RoPE (Qwen3 unpermuted GGUF layout) — identical to llama.c apply_rope_neox. */ +static void llama_tt_rope_neox(float *data, int n_heads, int head_dim, int rope_dim, int pos, + float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + float freq = 1.0f / powf(theta_base, (float)(2 * i) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i], x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/** + * NEOX RoPE for a `--rope-pairing split-half-reversed` artifact: slot `i` carries the + * model's rotary pair `half - 1 - i`, so it takes that pair's frequency. + * + * **This is exact, not an approximation.** The attention score is a sum of one term per + * rotary pair, `q . k = sum_p (rotated q_p . rotated k_p)`; reflecting the slot index applies + * the SAME reflection to q and to k, so the set of (frequency, feature-pair) terms is + * identical and only the order they are summed in moves. That reordering is the whole point + * of the experiment — it is also what re-draws which 16 output features share a block-float + * exponent in `attn_q`/`attn_k`. + * + * Note the frequency is computed with exactly the expression `llama_tt_rope_neox` uses, on a + * reflected index rather than a reflected result: `(float)(2*j)/(float)rot` for the same + * integer `j` produces the same float, so slot `i` here gets bit-identical cos/sin to slot + * `half-1-i` there and the arms differ only where they are meant to. + */ +/** + * Interleaved RoPE for a `--rope-pairing interleaved-reversed` artifact: the partner is still + * adjacent (GGUF's own grouping), but slot `p` carries the model's rotary pair `half - 1 - p`. + * + * This is the CROSSED CONTROL for the split-half result. `split-half` changed two things at + * once — it moved a partner from adjacent to `+half`, AND it thereby stopped a pair's two + * components sharing a block-float exponent. This arm reflects the pair index without moving + * the partner, so it re-draws the exponent blocks while KEEPING each pair's two components in + * one block. Exact in exact arithmetic, for the same reason as the split-half reflection. + */ +static void llama_tt_rope_raw_reversed(float *data, int n_heads, int head_dim, int rope_dim, int pos, + float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < rot; i += 2) { + int j = half - 1 - (i / 2); /* the model's pair index this slot holds */ + float freq = 1.0f / powf(theta_base, (float)(2 * j) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i], x1 = head[i + 1]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + 1] = x0 * sin_a + x1 * cos_a; + } + } +} + +static void llama_tt_rope_neox_reversed(float *data, int n_heads, int head_dim, int rope_dim, int pos, + float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + int j = half - 1 - i; /* the model's pair index this slot holds */ + float freq = 1.0f / powf(theta_base, (float)(2 * j) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle), sin_a = sinf(angle); + float x0 = head[i], x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* GQA softmax attention for all heads — identical math to llama.c compute_attn_heads. */ +static void llama_tt_attn_heads(const float *q, const float *k_cache, const float *v_cache, + float *attn_out, float *scores, int n_head, int n_kv_groups, + int head_dim, int max_seq, int seq_len, float scale) +{ + for (int qh = 0; qh < n_head; qh++) { + int kv_h = qh / n_kv_groups; + const float *q_vec = q + (size_t)qh * head_dim; + const float *k_base = k_cache + (size_t)kv_h * max_seq * head_dim; + const float *v_base = v_cache + (size_t)kv_h * max_seq * head_dim; + float *out_vec = attn_out + (size_t)qh * head_dim; + for (int s = 0; s < seq_len; s++) + scores[s] = pn_dot_f32(q_vec, k_base + (size_t)s * head_dim, head_dim) * scale; + float max_score = scores[0]; + for (int s = 1; s < seq_len; s++) + if (scores[s] > max_score) max_score = scores[s]; + float sum_exp = 0.0f; + for (int s = 0; s < seq_len; s++) { + scores[s] = expf(scores[s] - max_score); + sum_exp += scores[s]; + } + float inv_sum = 1.0f / sum_exp; + memset(out_vec, 0, head_dim * sizeof(float)); + for (int s = 0; s < seq_len; s++) { + float w_s = scores[s] * inv_sum; + const float *v_vec = v_base + (size_t)s * head_dim; + for (int d = 0; d < head_dim; d++) + out_vec[d] += w_s * v_vec[d]; + } + } +} + +/** + * The GQA softmax with the flash-decode kernel's own INTERMEDIATE precision — the host arm + * doing the card's arithmetic, so the gate can price what the card's circular buffers cost. + * + * `attention_float16b.cbtable` makes every CB Float16_b, and the kernel both produces and + * consumes `cb_s` in place, so each of these lands in bf16 between passes where + * `llama_tt_attn_heads` keeps fp32 throughout: + * + * - **Q pre-scaled by 1/sqrt(head_dim) into `cb_qs`** — a rounding the host does not have at + * all, because it multiplies the SCORE by the scale instead (`scale_query()` explains why + * the card scales the smaller operand: `dim_tiles` tile multiplies against + * `blocks * block_tiles` score tiles). + * - the scores, written to `cb_s` out of an fp32 DST; + * - the row max out of `running_max()`; + * - the exponentials, written back over `cb_s` in place by `exponentiate_block()`; + * - the row sum and its reciprocal. + * + * The two accumulations stay fp32 because DST_ACCUM_MODE is 1 on that kernel: what is + * emulated is where a value is STORED, which is exactly where the card is narrower. + * + * Not a claim to be bit-equal to the card — the SFPU exponential and the online block + * rescaling are not reproduced here. It is a PRICE for the format path, and its value is that + * it is the only one of the four candidates that is not a storage format anyone chose. + */ +static void llama_tt_attn_heads_bf16_softmax(const float *q, const float *k_cache, + const float *v_cache, float *attn_out, + float *scores, int n_head, int n_kv_groups, + int head_dim, int max_seq, int seq_len, float scale, + bool round_q_scale, bool round_scores) +{ + float q_scaled[LLAMA_TT_MAX_HEAD_DIM]; + + for (int qh = 0; qh < n_head; qh++) { + int kv_h = qh / n_kv_groups; + const float *q_vec = q + (size_t)qh * head_dim; + const float *k_base = k_cache + (size_t)kv_h * max_seq * head_dim; + const float *v_base = v_cache + (size_t)kv_h * max_seq * head_dim; + float *out_vec = attn_out + (size_t)qh * head_dim; + float max_score, sum_exp = 0.0f, inv_sum; + + for (int d = 0; d < head_dim; d++) { + float scaled = q_vec[d] * scale; + + q_scaled[d] = round_q_scale ? llama_tt_to_bf16_and_back(scaled) : scaled; + } + for (int s = 0; s < seq_len; s++) { + float raw = pn_dot_f32(q_scaled, k_base + (size_t)s * head_dim, head_dim); + + scores[s] = round_scores ? llama_tt_to_bf16_and_back(raw) : raw; + } + max_score = scores[0]; + for (int s = 1; s < seq_len; s++) + if (scores[s] > max_score) max_score = scores[s]; + if (round_scores) + max_score = llama_tt_to_bf16_and_back(max_score); + for (int s = 0; s < seq_len; s++) { + float p = expf(scores[s] - max_score); + + scores[s] = round_scores ? llama_tt_to_bf16_and_back(p) : p; + sum_exp += scores[s]; + } + if (round_scores) { + sum_exp = llama_tt_to_bf16_and_back(sum_exp); + inv_sum = llama_tt_to_bf16_and_back(1.0f / sum_exp); + } else { + inv_sum = 1.0f / sum_exp; + } + memset(out_vec, 0, head_dim * sizeof(float)); + for (int s = 0; s < seq_len; s++) { + float w_s = round_scores ? llama_tt_to_bf16_and_back(scores[s] * inv_sum) + : scores[s] * inv_sum; + const float *v_vec = v_base + (size_t)s * head_dim; + + for (int d = 0; d < head_dim; d++) + out_vec[d] += w_s * v_vec[d]; + } + } +} + +/* ============================================================ + * The traced matvec — one op, and its divergence from the oracle + * ============================================================ */ + +/** max|a-b| and PCC over n elements, plus the index of the worst element. Both metrics, + * always: a per-element scale error moves max-abs 35x while PCC barely notices, and a + * permutation moves neither — so these are the floor of the evidence, not the whole of it. */ +static void llama_tt_score(const float *card, const float *host, int n, double *out_max_abs, + double *out_pcc, int *out_worst) +{ + double sum_a = 0.0, sum_b = 0.0, sum_aa = 0.0, sum_bb = 0.0, sum_ab = 0.0; + double worst = -1.0, denom; + int worst_index = 0; + + for (int i = 0; i < n; i++) { + double a = (double)card[i], b = (double)host[i]; + double delta = fabs(a - b); + + if (delta > worst) { worst = delta; worst_index = i; } + sum_a += a; sum_b += b; sum_aa += a * a; sum_bb += b * b; sum_ab += a * b; + } + *out_max_abs = worst; + *out_worst = worst_index; + denom = sqrt(((double)n * sum_aa - sum_a * sum_a) * ((double)n * sum_bb - sum_b * sum_b)); + *out_pcc = denom > 0.0 ? ((double)n * sum_ab - sum_a * sum_b) / denom : 1.0; +} + +/** A block-float exponent is SHARED by 16 consecutive values, so what it costs is the spread + * INSIDE a block, not the tensor's overall scale. */ +#define LLAMA_TT_BLOCKFLOAT_SPAN 16 + +/** + * Dynamic range of one activation vector — the INPUT side of the loss, which the per-op PCC + * does not see. Three numbers, because they fail differently: + * + * rms the scale, so a range figure can be read relative to it + * crest max|a| / rms — a single outlier in a vector moves this and nothing else + * spread mean over 16-value blocks of log2(block max|a| / block rms), in BITS. This is the + * quantity a shared exponent has to cover, so it is the direct test of the + * "structured prompts widen the range and block float pays for it" hypothesis. + */ +static void llama_tt_activation_range(const float *a, int n, double *out_rms, double *out_crest, + double *out_spread) +{ + double sum_sq = 0.0, amax = 0.0, spread = 0.0; + int blocks = 0; + + for (int i = 0; i < n; i += LLAMA_TT_BLOCKFLOAT_SPAN) { + int span = n - i < LLAMA_TT_BLOCKFLOAT_SPAN ? n - i : LLAMA_TT_BLOCKFLOAT_SPAN; + double block_sq = 0.0, block_max = 0.0; + + for (int j = i; j < i + span; j++) { + double v = fabs((double)a[j]); + + block_sq += v * v; + if (v > block_max) block_max = v; + } + sum_sq += block_sq; + if (block_max > amax) amax = block_max; + block_sq = sqrt(block_sq / span); + if (block_sq > 0.0 && block_max > 0.0) { + spread += log2(block_max / block_sq); + blocks++; + } + } + *out_rms = sqrt(sum_sq / n); + *out_crest = *out_rms > 0.0 ? amax / *out_rms : 0.0; + *out_spread = blocks > 0 ? spread / blocks : 0.0; +} + +/** + * One projection on the card, both operands resident, optionally scored against the CPU + * oracle on the same input. + * + * The comparison is deliberately made on the OP and not on the token, because a wrong + * answer that only shows up in the argmax has already been through 169 chances to cancel. + * Under trace this DOWNLOADS both operands — they no longer pass through host memory at + * all, which is the point of the file and the cost of the instrument. + * + * @return 0, or -1 (the caller aborts the forward — a failed matmul must not be absorbed) + */ +static int llama_tt_matvec(pn_llama_tt *g, const pn_tt_weight *weight, const pn_qweight *cpu, + const pn_tt_activation *in, const pn_tt_activation *out, int layer, + const char *label) +{ + if (pn_tt_matvec_resident(g->device, weight, in, out) != 0) { + fprintf(stderr, "llama_tt: layer %d %s failed on the card\n", layer, label); + return -1; + } + if (g->trace > 0 && cpu && g->reference && g->trace_in && g->trace_out) { + double max_abs = 0.0, pcc = 1.0, rms = 0.0, crest = 0.0, spread = 0.0; + int worst = 0; + + if (pn_tt_activation_download(g->device, in, g->trace_in, weight->in_features) != 0 || + pn_tt_activation_download(g->device, out, g->trace_out, weight->out_features) != 0) + return -1; + pn_qmv(cpu, g->trace_in, g->reference); + llama_tt_score(g->trace_out, g->reference, (int)weight->out_features, &max_abs, &pcc, + &worst); + llama_tt_activation_range(g->trace_in, (int)weight->in_features, &rms, &crest, &spread); + fprintf(stderr, "llama_tt trace: L%02d %-12s Kt=%-4u Nt=%-5u max|d|=%.6f pcc=%.9f " + "worst[%d] card=%.6f cpu=%.6f in_rms=%.6f in_crest=%.4f " + "in_spread_bits=%.4f\n", + layer, label, weight->tile_rows, weight->tile_cols, max_abs, pcc, worst, + (double)g->trace_out[worst], (double)g->reference[worst], rms, crest, spread); + } + return 0; +} + +/** + * The same projection, ISSUED and not awaited — for the ones a layer can run together. + * + * Within a layer q/k/v read one `normed` and write three disjoint strips, and gate/up read + * one `normed` and write two: nothing in either set reads what the set writes, so their + * descriptors may sit in the members' rings at once. The card runs them back to back at its + * own 0.39-0.45 us op-to-op instead of one host round trip apiece, and the host waits FOUR + * times a layer instead of seven. + * + * UNDER TRACE IT DEGRADES TO THE WAITING FORM, deliberately: the trace scores the output + * against the CPU oracle, and reading an output IS waiting for it. An instrument that + * silently changed the thing it measures would be worse than a slower one. + */ +static int llama_tt_matvec_begin(pn_llama_tt *g, const pn_tt_weight *weight, + const pn_qweight *cpu, const pn_tt_activation *in, + const pn_tt_activation *out, int layer, const char *label) +{ + if (g->trace > 0 && cpu && g->reference && g->trace_in && g->trace_out) + return llama_tt_matvec(g, weight, cpu, in, out, layer, label); + if (pn_tt_matvec_enqueue(g->device, weight, in, out) != 0) { + fprintf(stderr, "llama_tt: layer %d %s failed on the card\n", layer, label); + return -1; + } + return 0; +} + +/* ============================================================ + * Where the residual stream lives + * ============================================================ */ + +/** The strip currently holding the residual stream, and the other one. */ +static const pn_tt_activation *llama_tt_hidden(pn_llama_tt *g) +{ + return &g->card.hidden[g->card.slot]; +} + +static const pn_tt_activation *llama_tt_hidden_spare(pn_llama_tt *g) +{ + return &g->card.hidden[g->card.slot ^ 1]; +} + +/** Make the HOST copy current, reading it back only if it is not already. Every call site + * is a place where an op did NOT move on-card. */ +static int llama_tt_hidden_to_host(pn_llama_tt *g, int n_embd) +{ + if (g->hidden_on_host) + return 0; + if (pn_tt_activation_download(g->device, llama_tt_hidden(g), g->hidden, (uint32_t)n_embd) != 0) + return -1; + g->hidden_on_host = true; + return 0; +} + +/** Make the CARD copy current, writing it only if it is not already. */ +static int llama_tt_hidden_to_card(pn_llama_tt *g, int n_embd) +{ + if (g->hidden_on_card) + return 0; + if (pn_tt_activation_upload(g->device, llama_tt_hidden(g), g->hidden, (uint32_t)n_embd) != 0) + return -1; + g->hidden_on_card = true; + return 0; +} + +/** One RMS norm into `g->card.normed`, on the card or on the host, leaving the result + * where the next matmul reads it either way. */ +static int llama_tt_norm(pn_llama_tt *g, const pn_tt_norm_constants *constants, + const float *host_gamma, const pn_config *cfg) +{ + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_NORM)) { + if (llama_tt_hidden_to_card(g, cfg->n_embd) != 0) + return -1; + return pn_tt_rms_norm(g->device, constants, llama_tt_hidden(g), &g->card.normed); + } + if (llama_tt_hidden_to_host(g, cfg->n_embd) != 0) + return -1; + pn_vrms_norm_f32(g->hidden, host_gamma, g->normed, cfg->n_embd, cfg->rms_norm_eps); + return pn_tt_activation_upload(g->device, &g->card.normed, g->normed, (uint32_t)cfg->n_embd); +} + +/** hidden += `g->card.project`, on the card or on the host. */ +static int llama_tt_residual(pn_llama_tt *g, const pn_config *cfg) +{ + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_RESIDUAL)) { + if (llama_tt_hidden_to_card(g, cfg->n_embd) != 0) + return -1; + if (pn_tt_add(g->device, llama_tt_hidden(g), &g->card.project, + llama_tt_hidden_spare(g)) != 0) + return -1; + g->card.slot ^= 1; + g->hidden_on_card = true; + g->hidden_on_host = false; + return 0; + } + if (pn_tt_activation_download(g->device, &g->card.project, g->ffn_out, + (uint32_t)cfg->n_embd) != 0) + return -1; + if (llama_tt_hidden_to_host(g, cfg->n_embd) != 0) + return -1; + pn_vadd_f32(g->hidden, g->ffn_out, g->hidden, cfg->n_embd); + g->hidden_on_host = true; + g->hidden_on_card = false; + return 0; +} + +/* ============================================================ + * Residency + * ============================================================ */ + +/** Bind one artifact tensor name to a resident weight, reporting the miss by name. */ +static int llama_tt_bind(pn_llama_tt *g, const char *name, pn_tt_weight *out) +{ + if (pn_tt_device_find_weight(g->device, name, out) != 0) { + fprintf(stderr, "llama_tt: artifact has no %s\n", name); + return -1; + } + return 0; +} + +/** Every projection of one layer, by the GGUF names the converter preserved. */ +static int llama_tt_bind_layer(pn_llama_tt *g, int layer) +{ + pn_llama_tt_layer *d = &g->layers[layer]; + char name[128]; + struct { const char *suffix; pn_tt_weight *slot; } bindings[] = { + { "attn_q", &d->wq }, { "attn_k", &d->wk }, + { "attn_v", &d->wv }, { "attn_output", &d->wo }, + { "ffn_gate", &d->w_gate }, { "ffn_up", &d->w_up }, + { "ffn_down", &d->w_down }, + }; + + for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) { + if (snprintf(name, sizeof(name), "blk.%d.%s.weight", layer, bindings[i].suffix) < 0) + return -1; + if (llama_tt_bind(g, name, bindings[i].slot) != 0) + return -1; + } + return 0; +} + +/** Allocate the per-token host scratch. One allocation per vector, once, at load. + * + * `scores` is NOT here: its length is the KV ring's max_seq, which belongs to the session + * and not to the model, so it is sized on the first forward from the live cache. */ +static int llama_tt_alloc_scratch(pn_llama_tt *g, const pn_config *cfg) +{ + int n_embd = cfg->n_embd; + int q_width = cfg->n_head * pn_head_dim(cfg); + int kv_width = cfg->n_kv_head * pn_head_dim(cfg); + int widest = cfg->n_vocab > cfg->n_ff ? cfg->n_vocab : cfg->n_ff; + + g->hidden = calloc((size_t)n_embd, sizeof(float)); + g->normed = calloc((size_t)n_embd, sizeof(float)); + g->q = calloc((size_t)q_width, sizeof(float)); + g->k = calloc((size_t)kv_width, sizeof(float)); + g->v = calloc((size_t)kv_width, sizeof(float)); + g->attn = calloc((size_t)q_width, sizeof(float)); + g->gate = calloc((size_t)cfg->n_ff, sizeof(float)); + g->up = calloc((size_t)cfg->n_ff, sizeof(float)); + g->ffn_out = calloc((size_t)n_embd, sizeof(float)); + g->reference = g->trace > 0 ? calloc((size_t)widest, sizeof(float)) : NULL; + g->trace_in = g->trace > 0 ? calloc((size_t)widest, sizeof(float)) : NULL; + g->trace_out = g->trace > 0 ? calloc((size_t)widest, sizeof(float)) : NULL; + g->attn_card = g->attn_check ? calloc((size_t)q_width, sizeof(float)) : NULL; + /* One width serves both probes: the rotary check reads q strips (q_width) and the row + * census reads `attn` (q_width) and `hidden` (n_embd), so the widest of the three. */ + g->probe_width = q_width > n_embd ? q_width : n_embd; + g->probe_in = (g->rope_check || g->row_census) + ? calloc((size_t)g->probe_width, sizeof(float)) : NULL; + g->probe_out = (g->rope_check || g->row_census) + ? calloc((size_t)g->probe_width, sizeof(float)) : NULL; + return (g->hidden && g->normed && g->q && g->k && g->v && g->attn && g->gate && g->up && + g->ffn_out && (!g->attn_check || g->attn_card) && + (!(g->rope_check || g->row_census) || (g->probe_in && g->probe_out)) && + (g->trace == 0 || (g->reference && g->trace_in && g->trace_out))) ? 0 : -1; +} + +/** + * Every activation of one token, allocated ONCE in card DRAM and zeroed. + * + * The zeroing is load-bearing rather than tidy: a matvec writes only logical row 0 of each + * tile and the other 31 rows are fed to the FPU anyway, so they have to be zero and have to + * stay zero. Every on-card op here preserves that for free. + */ +static int llama_tt_alloc_strips(pn_llama_tt *g, const pn_config *cfg) +{ + pn_llama_tt_strips *s = &g->card; + const pn_llama_tt_layer *l0 = &g->layers[0]; + uint32_t embd_tiles = (uint32_t)cfg->n_embd / 32u; + uint32_t ff_tiles = (uint32_t)cfg->n_ff / 32u; + + if (pn_tt_activation_alloc(g->device, embd_tiles, &s->hidden[0]) != 0 || + pn_tt_activation_alloc(g->device, embd_tiles, &s->hidden[1]) != 0 || + pn_tt_activation_alloc(g->device, embd_tiles, &s->normed) != 0 || + pn_tt_activation_alloc(g->device, l0->wq.tile_cols, &s->q) != 0 || + pn_tt_activation_alloc(g->device, l0->wk.tile_cols, &s->k) != 0 || + pn_tt_activation_alloc(g->device, l0->wv.tile_cols, &s->v) != 0 || + pn_tt_activation_alloc(g->device, l0->wq.tile_cols, &s->q_rot) != 0 || + pn_tt_activation_alloc(g->device, l0->wk.tile_cols, &s->k_rot) != 0 || + pn_tt_activation_alloc(g->device, l0->wo.tile_rows, &s->attn) != 0 || + pn_tt_activation_alloc(g->device, embd_tiles, &s->project) != 0 || + pn_tt_activation_alloc(g->device, ff_tiles, &s->gate) != 0 || + pn_tt_activation_alloc(g->device, ff_tiles, &s->up) != 0 || + pn_tt_activation_alloc(g->device, ff_tiles, &s->swiglu) != 0 || + pn_tt_activation_alloc(g->device, g->output.tile_cols, &s->logits) != 0) + return -1; + s->slot = 0; + return 0; +} + +/** Expand every norm weight into the card-side constants the reduce op reads. Once, at + * load: a per-token upload of gamma would put back a transfer this file exists to remove. */ +static int llama_tt_upload_norms(pn_llama_tt *g, pn_llama_qweights *qw, const pn_config *cfg) +{ + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_tt_norm_constants_upload(g->device, qw->layers[layer].attn_norm, + (uint32_t)cfg->n_embd, cfg->rms_norm_eps, + &g->layers[layer].attn_norm) != 0) + return -1; + if (pn_tt_norm_constants_upload(g->device, qw->layers[layer].ffn_norm, + (uint32_t)cfg->n_embd, cfg->rms_norm_eps, + &g->layers[layer].ffn_norm) != 0) + return -1; + } + return pn_tt_norm_constants_upload(g->device, qw->output_norm, (uint32_t)cfg->n_embd, + cfg->rms_norm_eps, &g->output_norm); +} + +/** + * Arm the three attention residencies and the card-resident KV cache, once, on the first + * forward. + * + * It is LAZY rather than done at load because the cache's capacity is the SESSION's + * `max_seq`, which does not exist at model load — sizing it from the model instead would + * allocate the checkpoint's 131,072-position context (3 GiB) for a serve that asked for 2k. + * + * **A non-split-half artifact is a REFUSAL.** foundry's RoPE kernel implements NeoX pairing + * and refuses interleaved (2i, 2i+1) pairing by shape; a llama-family artifact converted + * WITHOUT `--rope-pairing split-half` holds q and k in interleaved order, and rotating those + * as NeoX is a wrong answer with no error anywhere. The host arm reads the same flag to pick + * its own rotation, so the two arms cannot disagree about it either. + */ +static int llama_tt_ensure_attention(pn_llama_tt *g, pn_model *model, int max_seq) +{ + pn_config *cfg = &model->config; + + if (g->attn_refused) + return -1; + if (g->attn_armed) + return 0; + g->attn_armed = true; + if (!pn_tt_device_oncard_requested(g->device, PN_TT_ONCARD_ATTENTION)) + return 0; + /* TWO refusals for TWO independent artifact bits, and the reflected one is tested FIRST + * because a `split-half-reversed` artifact is split-half SHAPED and would otherwise pass + * the second check and run silently wrong: the on-card RoPE table is built with the + * frequency in SLOT order, so a reflected artifact gets every frequency on the wrong + * feature. `interleaved-reversed` fails both, and it should say the reflection first + * because that is the reason a fixed converter flag could not rescue it. */ + if (pn_tt_device_rope_pairs_reversed(g->device)) { + fprintf(stderr, "llama_tt: REFUSED — this artifact reflects the rotary pair index " + "(slot p carries pair half-1-p). The on-card RoPE table is built in " + "SLOT order, so the card would apply every frequency to the wrong " + "feature. It is an instrument: run it with host attention.\n"); + g->attn_refused = true; + return -1; + } + if (cfg->arch != PN_ARCH_QWEN3 && !pn_tt_device_rope_is_split_half(g->device)) { + fprintf(stderr, "llama_tt: REFUSED — on-card attention needs a `--rope-pairing " + "split-half` artifact; this one is interleaved and the on-card RoPE " + "kernel cannot express that pairing\n"); + g->attn_refused = true; + return -1; + } + if (pn_tt_device_arm_attention(g->device, (uint32_t)cfg->n_head, (uint32_t)cfg->n_kv_head, + (uint32_t)pn_head_dim(cfg), (uint32_t)cfg->rope_dim, + (double)cfg->rope_theta, (uint32_t)max_seq, + (uint32_t)cfg->n_layer) != 0) { + g->attn_refused = true; + return -1; + } + return 0; +} + +/** + * The session-export hook: move one layer's K or V between the card and a packed fp32 array. + * + * Installed on the KV cache when — and only when — attention is running on the card, because + * that is the only arm on which the host tensors are stale. It is the ONLY place the card's + * KV cache crosses the link, and it runs at export/restore time rather than per token; the + * arithmetic for that choice is in pn_tt_device.h and llama_state_snapshot.c. + */ +static int llama_tt_kv_move(void *device_v, int layer, int values, int position_count, + float *rows, int capture) +{ + pn_tt_device *device = (pn_tt_device *)device_v; + + if (!device || layer < 0 || position_count <= 0 || !rows) + return -1; + if (capture) + return pn_tt_kv_download(device, (uint32_t)layer, values != 0, + (uint32_t)position_count, rows); + return pn_tt_kv_upload(device, (uint32_t)layer, values != 0, (uint32_t)position_count, + rows); +} + +/** Size the attention score row to the live KV ring, once. */ +static int llama_tt_ensure_scores(pn_llama_tt *g, int max_seq) +{ + if (g->scores && g->scores_len >= max_seq) + return 0; + free(g->scores); + g->scores = calloc((size_t)max_seq, sizeof(float)); + g->scores_len = g->scores ? max_seq : 0; + return g->scores ? 0 : -1; +} + +/** + * KV bytes one token costs in card DRAM, across every layer. + * + * The card's KV cache is bf16 and holds K and V for every KV head at full head_dim + * (ft_tt_kv_cache_layout_init), so this is 2 regions x heads x dim x 2 bytes per layer. It + * is the model's geometry, not a guess, and it is what makes "how much context does this + * rung leave" answerable. + */ +static uint64_t llama_tt_kv_bytes_per_token(const pn_config *cfg) +{ + uint64_t heads = (uint64_t)(cfg->n_kv_head > 0 ? cfg->n_kv_head : cfg->n_head); + uint64_t dim = (uint64_t)pn_head_dim(cfg); + + return 2ull * heads * dim * 2ull * (uint64_t)cfg->n_layer; +} + +/** The context a rung choice has to leave room for. */ +static uint32_t llama_tt_context_tokens(const pn_config *cfg) +{ + const char *override_value = getenv(PN_TT_CONTEXT_TOKENS_ENV); + + if (override_value && *override_value) { + unsigned long parsed = strtoul(override_value, NULL, 10); + + if (parsed > 0ul) + return (uint32_t)parsed; + } + if (cfg->ctx_cap > 0) + return (uint32_t)cfg->ctx_cap; + return cfg->n_ctx > 0 ? (uint32_t)cfg->n_ctx : 4096u; +} + +/** An env override that named something this build does not know. Refuse, never ignore. */ +#define LLAMA_TT_ENV_BAD (-2) + +/** A rung forced by the environment: an ft_tt_ledger_mode, -1 for "choose", or _ENV_BAD. */ +static int llama_tt_forced_rung(void) +{ + const char *name = getenv(PN_TT_RUNG_ENV); + int mode; + + if (!name || !*name) + return -1; + mode = ft_tt_ledger_mode_from_name(name); + if (mode < 0) { + fprintf(stderr, "llama_tt: REFUSED — %s='%s' is not a rung name\n", PN_TT_RUNG_ENV, name); + return LLAMA_TT_ENV_BAD; + } + return mode; +} + +/** A pairing forced by the environment: an ft_tt_rope_pairing, -1 for "read it off the + * checkpoint", or _ENV_BAD. A misspelling that silently fell back to the default would read + * as "this pairing costs nothing", which is the most expensive way to be wrong here. */ +static int llama_tt_forced_rope_pairing(void) +{ + static const struct { const char *name; ft_tt_rope_pairing pairing; } NAMES[] = { + {"source", FT_TT_ROPE_PAIRING_SOURCE}, + {"split-half", FT_TT_ROPE_PAIRING_SPLIT_HALF}, + {"split-half-reversed", FT_TT_ROPE_PAIRING_SPLIT_HALF_REVERSED}, + {"interleaved-reversed", FT_TT_ROPE_PAIRING_INTERLEAVED_REVERSED} + }; + const char *name = getenv(PN_TT_ROPE_PAIRING_ENV); + size_t i; + + if (!name || !*name) + return -1; + for (i = 0; i < sizeof(NAMES) / sizeof(NAMES[0]); i++) { + if (strcmp(name, NAMES[i].name) == 0) + return (int)NAMES[i].pairing; + } + fprintf(stderr, "llama_tt: REFUSED — %s='%s' names none of source, split-half, " + "split-half-reversed, interleaved-reversed\n", + PN_TT_ROPE_PAIRING_ENV, name); + return LLAMA_TT_ENV_BAD; +} + +/** + * Turn a `.gguf` path into a card-ready artifact, converting and caching it if need be. + * + * THIS IS THE WHOLE PRODUCT REQUIREMENT IN ONE FUNCTION: a user downloads a GGUF and our + * software makes it run. They are never asked which rung, never told their file is the + * wrong format, and never run a converter by hand. What they see on a first load is a + * progress bar, and on every load after that, nothing. + * + * @param model a loaded model whose `path` is the checkpoint + * @param out receives the artifact path to load + * @param out_bytes its size + * @return 0 and a usable path, or -1 with the reason already printed + */ +static int llama_tt_resolve_artifact(const pn_model *model, char *out, size_t out_bytes) +{ + const char *override_path = getenv(PN_TT_ARTIFACT_ENV); + const char *reserve_mib = getenv(PN_TT_CARD_RESERVE_MIB_ENV); + const pn_config *cfg = &model->config; + ft_tt_artifact_cache_request request; + ft_tt_artifact_cache_result result; + char line[FT_TT_CAPACITY_REASON_BYTES]; + unsigned long reserve; + int rc; + + if (override_path && *override_path) { + if (snprintf(out, out_bytes, "%s", override_path) >= (int)out_bytes) { + fprintf(stderr, "llama_tt: REFUSED — %s is longer than %zu bytes\n", + PN_TT_ARTIFACT_ENV, out_bytes); + return -1; + } + fprintf(stderr, "llama_tt: artifact %s (%s override — its provenance is NOT checked " + "against the checkpoint)\n", out, PN_TT_ARTIFACT_ENV); + return 0; + } + + if (!model->path || !*model->path) { + fprintf(stderr, "llama_tt: REFUSED — the model has no checkpoint path to convert " + "from, and %s is not set\n", PN_TT_ARTIFACT_ENV); + return -1; + } + + reserve = PN_TT_CARD_RESERVE_MIB_DEFAULT; + if (reserve_mib && *reserve_mib) + reserve = strtoul(reserve_mib, NULL, 10); + + memset(&request, 0, sizeof(request)); + request.gguf_path = model->path; + request.cache_dir = getenv(PN_TT_ARTIFACT_CACHE_DIR_ENV); + request.budget.card_bytes = (uint64_t)FT_TT_MEM_DRAM_SIZE * 8ull; + request.budget.runtime_state_bytes = (uint64_t)reserve * 1024ull * 1024ull; + request.budget.kv_bytes_per_token = llama_tt_kv_bytes_per_token(cfg); + request.budget.context_tokens = llama_tt_context_tokens(cfg); + request.progress_style = FT_TT_CACHE_PROGRESS_STDERR; + + request.forced_mode = llama_tt_forced_rung(); + request.rope_pairing = llama_tt_forced_rope_pairing(); + if (request.forced_mode == LLAMA_TT_ENV_BAD || request.rope_pairing == LLAMA_TT_ENV_BAD) + return -1; + + rc = ft_tt_artifact_cache_ensure(&request, &result); + if (rc != FT_TT_ARTIFACT_CACHE_OK) { + /* A refusal carries the arithmetic. It is not a fallback: the caller stays on the + * CPU and says so, rather than converting something that will not fit. */ + fprintf(stderr, "llama_tt: REFUSED — %s\n", result.reason); + return -1; + } + + /* THE ONE LINE. Which rung, why, what it left, and whether anything was converted. */ + fprintf(stderr, "llama_tt: %s — %s\n", result.produced ? "converted" : "cached", + ft_tt_capacity_selection_line(&result.selection, &request.budget, line, + sizeof(line))); + if (result.produced && result.had_artifact && result.note[0]) + fprintf(stderr, "llama_tt: the cached artifact was NOT reusable and was not served — " + "%s\n", result.note); + fprintf(stderr, "llama_tt: rotary pairing %s (%s); KV %llu B/token, %u tokens reserved\n", + result.rope_pairing == FT_TT_ROPE_PAIRING_SPLIT_HALF ? "split-half" : "source", + result.rope_reason ? result.rope_reason : "", + (unsigned long long)request.budget.kv_bytes_per_token, + request.budget.context_tokens); + + if (snprintf(out, out_bytes, "%s", result.artifact_path) >= (int)out_bytes) { + fprintf(stderr, "llama_tt: REFUSED — the cached artifact's path does not fit\n"); + return -1; + } + return 0; +} + +int pn_llama_tt_upload(pn_model *model) +{ + char artifact[FT_TT_ARTIFACT_CACHE_PATH_BYTES]; + uint32_t eltwise_tiles[2]; + pn_llama_qweights *qw; + pn_llama_tt *g; + pn_config *cfg; + const char *trace; + const char *sparse; + const char *check; + + if (!model || !model->weights) + return -1; + qw = (pn_llama_qweights *)model->weights; + cfg = &model->config; + if (!qw->is_quantized) { + fprintf(stderr, "llama_tt: F32 weights are not a converted artifact; staying on CPU\n"); + return -1; + } + if (cfg->n_experts > 0) { + fprintf(stderr, "llama_tt: MoE is not built on this backend; staying on CPU\n"); + return -1; + } + g = calloc(1, sizeof(*g)); + if (!g) + return -1; + trace = getenv(PN_TT_TRACE_ENV); + g->trace = trace ? atoi(trace) : 0; + sparse = getenv(PN_TT_SPARSE_LOGITS_ENV); + g->sparse_logits = sparse && atoi(sparse) != 0; + check = getenv(PN_TT_ATTN_CHECK_ENV); + g->attn_check = check && atoi(check) != 0; + check = getenv(PN_TT_ROPE_CHECK_ENV); + g->rope_check = check && atoi(check) != 0; + check = getenv(PN_TT_ROW_CENSUS_ENV); + g->row_census = check && atoi(check) != 0; + check = getenv(PN_TT_HOST_EMULATE_ENV); + if (check && *check) { + g->emulate_bf16_table = strstr(check, "bf16table") != NULL; + g->emulate_bf16_rot = strstr(check, "bf16rot") != NULL; + g->emulate_bf16_kv = strstr(check, "bf16kv") != NULL; + g->emulate_bf16_q_scale = + strstr(check, "bf16softmax") != NULL || strstr(check, "bf16qscale") != NULL; + g->emulate_bf16_scores = + strstr(check, "bf16softmax") != NULL || strstr(check, "bf16scores") != NULL; + /* A misspelled name that silently emulates nothing would read as "this rounding costs + * nothing", which is the most expensive possible way to be wrong here. */ + if (!g->emulate_bf16_table && !g->emulate_bf16_rot && !g->emulate_bf16_kv && + !g->emulate_bf16_q_scale && !g->emulate_bf16_scores) { + fprintf(stderr, "llama_tt: REFUSED — %s='%s' names none of bf16table, bf16rot, " + "bf16kv, bf16softmax, bf16qscale, bf16scores\n", + PN_TT_HOST_EMULATE_ENV, check); + free(g); + return -1; + } + if ((g->emulate_bf16_q_scale || g->emulate_bf16_scores) && + pn_head_dim(cfg) > LLAMA_TT_MAX_HEAD_DIM) { + fprintf(stderr, "llama_tt: REFUSED — bf16softmax holds one head on the stack and " + "head_dim %d exceeds %d\n", pn_head_dim(cfg), LLAMA_TT_MAX_HEAD_DIM); + free(g); + return -1; + } + fprintf(stderr, "llama_tt: HOST ARM EMULATING the card — bf16table %d bf16rot %d " + "bf16kv %d bf16qscale %d bf16scores %d. This is an instrument; the " + "card arm is unaffected.\n", + (int)g->emulate_bf16_table, (int)g->emulate_bf16_rot, (int)g->emulate_bf16_kv, + (int)g->emulate_bf16_q_scale, (int)g->emulate_bf16_scores); + } + g->n_layers = cfg->n_layer; + + /* The card is opened BEFORE the artifact is resolved, and the order is load-bearing: + * resolving can mean a twenty-minute conversion, and doing that on a machine whose card + * then fails to open would burn the twenty minutes to end up on the CPU anyway. */ + if (pn_tt_device_open(0, &g->device) != 0) + goto fail; + if (llama_tt_resolve_artifact(model, artifact, sizeof(artifact)) != 0) + goto fail; + if (pn_tt_device_load_artifact(g->device, artifact) != 0) + goto fail; + + g->layers = calloc((size_t)cfg->n_layer, sizeof(*g->layers)); + if (!g->layers) + goto fail; + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (llama_tt_bind_layer(g, layer) != 0) + goto fail; + } + if (llama_tt_bind(g, "output.weight", &g->output) != 0) + goto fail; + + /* The shape the artifact says must be the shape the engine says. A silent disagreement + * here is a matmul with the right tile counts and the wrong meaning. */ + if ((int)g->output.out_features != cfg->n_vocab || + (int)g->layers[0].wq.in_features != cfg->n_embd) { + fprintf(stderr, "llama_tt: artifact shape (vocab %u, n_embd %u) disagrees with the " + "checkpoint (vocab %d, n_embd %d)\n", + g->output.out_features, g->layers[0].wq.in_features, cfg->n_vocab, cfg->n_embd); + goto fail; + } + if (llama_tt_alloc_scratch(g, cfg) != 0) + goto fail; + if (llama_tt_alloc_strips(g, cfg) != 0) + goto fail; + + /* The shapes are the MODEL's, not the artifact's: the reduce residency's buffers are + * placed for ONE width and the elementwise residency runs ONE block depth for every + * descriptor it will ever see, so both have to be decided against the widths this + * forward will actually ask for. */ + eltwise_tiles[0] = (uint32_t)cfg->n_embd / 32u; + eltwise_tiles[1] = (uint32_t)cfg->n_ff / 32u; + if (pn_tt_device_arm_oncard(g->device, (uint32_t)cfg->n_embd / 32u, eltwise_tiles, 2u, + g->output.tile_cols) != 0) + goto fail; + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_NORM) && llama_tt_upload_norms(g, qw, cfg) != 0) + goto fail; + + g->loaded = true; + model->gpu = g; + fprintf(stderr, "llama_tt: %u weights resident (%.1f MiB), %d layers, trace=%d\n", + pn_tt_device_weight_count(g->device), + (double)pn_tt_device_weight_bytes(g->device) / (1024.0 * 1024.0), cfg->n_layer, + g->trace); + return 0; + +fail: + model->gpu = g; + pn_llama_tt_free(model); + return -1; +} + +int pn_llama_model_tt_loaded(const pn_model *model) +{ + const pn_llama_tt *g = model ? (const pn_llama_tt *)model->gpu : NULL; + + return (g && g->loaded) ? 1 : 0; +} + +void pn_llama_tt_free(pn_model *model) +{ + pn_llama_tt *g = model ? (pn_llama_tt *)model->gpu : NULL; + + if (!g) + return; + pn_tt_device_close(g->device); + free(g->layers); + free(g->hidden); free(g->normed); free(g->q); free(g->k); free(g->v); + free(g->attn); free(g->gate); free(g->up); free(g->ffn_out); free(g->scores); + free(g->reference); free(g->trace_in); free(g->trace_out); free(g->attn_card); + free(g->probe_in); free(g->probe_out); + free(g->b_hidden); free(g->b_normed); free(g->b_q); free(g->b_k); free(g->b_v); + free(g->b_attn); free(g->b_gate); free(g->b_up); free(g->b_scratch); + free(g); + model->gpu = NULL; +} + +/* ============================================================ + * Forward + * ============================================================ */ + +/** + * ONE position's rotary, KV append and GQA softmax, over q/k/v ALREADY in host memory. + * + * This is the body `llama_tt_attention_host` used to hold between its download and its + * return, factored out with the buffers as arguments and NOTHING else changed. The batched + * prefill calls it once per row, so the two paths run the SAME arithmetic per position and a + * divergence between them cannot be a second copy of this having drifted. `q` and `k` are + * rotated IN PLACE, exactly as before. + */ +static int llama_tt_attend_one(pn_llama_tt *g, pn_model *model, pn_state *state, int layer, + int pos, float *q, float *k, float *v, float *out) +{ + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + int head_dim = pn_head_dim(cfg); + int max_seq = kv->max_seq; + int n_kv_groups = cfg->n_head / cfg->n_kv_head; + int q_width = cfg->n_head * head_dim; + int kv_width = cfg->n_kv_head * head_dim; + + /* THE PAIRING COMES FROM THE ARTIFACT, NOT THE ARCHITECTURE. A `--rope-pairing + * split-half` artifact holds wq/wk with llama.cpp's HF->GGUF permutation undone, so + * its q and k arrive in NeoX order whatever `arch` says — and reading that pairing off + * the architecture instead would rotate the wrong feature pairs with no symptom but a + * wrong answer. Q.K is invariant under a permutation applied to both, so the two + * artifacts agree to the last digit that block float allows. */ + if (pn_tt_device_rope_pairs_reversed(g->device)) { + /* The two reflected INSTRUMENTS. The two artifact bits are independent — SPLIT_HALF + * says where a partner is, PAIRS_REVERSED says which pair a slot holds — so the + * reflected arm still has to ask the first question, and it is tested before the + * plain split-half branch because that predicate is true here too. */ + if (pn_tt_device_rope_is_split_half(g->device)) { + llama_tt_rope_neox_reversed(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + llama_tt_rope_neox_reversed(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, + cfg->rope_theta); + } else { + llama_tt_rope_raw_reversed(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + llama_tt_rope_raw_reversed(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, + cfg->rope_theta); + } + } else if (cfg->arch == PN_ARCH_QWEN3 || pn_tt_device_rope_is_split_half(g->device)) { + if (g->emulate_bf16_table) { + llama_tt_rope_neox_bf16_table(q, cfg->n_head, head_dim, cfg->rope_dim, pos, + cfg->rope_theta); + llama_tt_rope_neox_bf16_table(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, + cfg->rope_theta); + } else { + llama_tt_rope_neox(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + llama_tt_rope_neox(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, + cfg->rope_theta); + } + } else { + llama_tt_rope_raw(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + llama_tt_rope_raw(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } + + /* The card's q strip is bf16 and its KV cache is bf16. Each is applied on its own so the + * gate prices Q's rounding and the cache's rounding separately — they are different + * quantities: Q's moves this token's query, the cache's moves EVERY position it will ever + * be read at. */ + if (g->emulate_bf16_rot) + llama_tt_round_to_bf16(q, q_width); + if (g->emulate_bf16_kv) { + llama_tt_round_to_bf16(k, kv_width); + llama_tt_round_to_bf16(v, kv_width); + } + + float *k_cache = (float *)kv->k_cache[layer]->data; + float *v_cache = (float *)kv->v_cache[layer]->data; + int cache_pos = pos % max_seq; + for (int h = 0; h < cfg->n_kv_head; h++) { + memcpy(k_cache + ((size_t)h * max_seq + cache_pos) * head_dim, + k + (size_t)h * head_dim, head_dim * sizeof(float)); + memcpy(v_cache + ((size_t)h * max_seq + cache_pos) * head_dim, + v + (size_t)h * head_dim, head_dim * sizeof(float)); + } + + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + if (g->emulate_bf16_q_scale || g->emulate_bf16_scores) + llama_tt_attn_heads_bf16_softmax(q, k_cache, v_cache, out, g->scores, + cfg->n_head, n_kv_groups, head_dim, max_seq, seq_len, + 1.0f / sqrtf((float)head_dim), + g->emulate_bf16_q_scale, g->emulate_bf16_scores); + else + llama_tt_attn_heads(q, k_cache, v_cache, out, g->scores, cfg->n_head, + n_kv_groups, head_dim, max_seq, seq_len, + 1.0f / sqrtf((float)head_dim)); + return 0; +} + +/** + * RoPE, the KV append and the GQA softmax ON THE HOST — the shipping path, unchanged. + * + * It leaves `g->attn` holding the attention output and does NOT upload it, so the crossed + * control can run this and the card arm over the same q/k/v and compare. + */ +static int llama_tt_attention_host(pn_llama_tt *g, pn_model *model, pn_state *state, int layer, + int pos) +{ + pn_llama_tt_strips *s = &g->card; + pn_config *cfg = &model->config; + int q_width = cfg->n_head * pn_head_dim(cfg); + int kv_width = cfg->n_kv_head * pn_head_dim(cfg); + + /* THE READBACK THAT IS LEFT when attention is on the host. q, k and v come back because + * the KV append and the GQA softmax are host-side; the rotary pairing no longer decides + * it. 10,240 B a layer, 245,760 a token, 52 ms at 4.7 MB/s — the whole readback phase. */ + if (pn_tt_activation_download(g->device, &s->q, g->q, (uint32_t)q_width) != 0 || + pn_tt_activation_download(g->device, &s->k, g->k, (uint32_t)kv_width) != 0 || + pn_tt_activation_download(g->device, &s->v, g->v, (uint32_t)kv_width) != 0) + return -1; + return llama_tt_attend_one(g, model, state, layer, pos, g->q, g->k, g->v, g->attn); +} + +/** + * The same three ops ON THE CARD, and NOTHING crosses the link. + * + * q, k and v stay in DRAM strips; RoPE rotates q and k into their own strips; the append + * places k and v into this layer's card-resident cache; flash decode reads that cache and + * writes `attn` — which is where `wo` already reads from. + */ +static int llama_tt_attention_card(pn_llama_tt *g, pn_model *model, pn_state *state, int layer, + int pos) +{ + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_llama_tt_strips *s = &g->card; + pn_config *cfg = &model->config; + int max_seq = kv->max_seq; + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + + if (pn_tt_rope(g->device, &s->q, &s->q_rot, (uint32_t)cfg->n_head, (uint32_t)pos) != 0 || + pn_tt_rope(g->device, &s->k, &s->k_rot, (uint32_t)cfg->n_kv_head, (uint32_t)pos) != 0) + return -1; + if (pn_tt_kv_append(g->device, (uint32_t)layer, false, &s->k_rot, (uint32_t)pos) != 0 || + pn_tt_kv_append(g->device, (uint32_t)layer, true, &s->v, (uint32_t)pos) != 0) + return -1; + return pn_tt_attention(g->device, (uint32_t)layer, &s->q_rot, &s->attn, + (uint32_t)seq_len); +} + +/** + * THE CROSSED CONTROL. Run BOTH arms over the same q/k/v and compare the two attention + * outputs element for element. + * + * This is not a PCC check and deliberately not one: the failures this op can have are a + * wrong KV position, a group's query reading another group's head, a mask tail off by one and + * a face-order mistake — every one of which is a PERMUTATION or a localised wrong element, + * and PCC is blind to the first and saturates on the second (foundry/tt/DEV_NOTES.md §3). So + * it reports max|Δ|, WHERE, and how many elements are past a threshold, which is the + * resolution "one element of one head is wrong" needs. + * + * It costs the readback it exists to remove — both arms, plus a download of the card's + * answer — so it is a knob, exactly like PN_TT_TRACE. + */ +static void llama_tt_attention_compare(pn_llama_tt *g, const pn_config *cfg, int layer) +{ + int q_width = cfg->n_head * pn_head_dim(cfg); + double worst = 0.0; + int worst_index = 0, diverged = 0; + + if (pn_tt_activation_download(g->device, &g->card.attn, g->attn_card, + (uint32_t)q_width) != 0) { + fprintf(stderr, "llama_tt attn-check: L%02d the card's attention output did not read " + "back\n", layer); + return; + } + for (int i = 0; i < q_width; i++) { + double delta = fabs((double)g->attn_card[i] - (double)g->attn[i]); + + if (delta > worst) { worst = delta; worst_index = i; } + if (delta > LLAMA_TT_ATTN_DIVERGENCE) diverged++; + } + if (worst > g->attn_check_worst) { + g->attn_check_worst = worst; + g->attn_check_worst_layer = layer; + } + g->attn_check_diverged += diverged; + fprintf(stderr, "llama_tt attn-check: L%02d max|d|=%.6f at %d (head %d dim %d) card=%.6f " + "host=%.6f, %d of %d past %.3f\n", + layer, worst, worst_index, worst_index / pn_head_dim(cfg), + worst_index % pn_head_dim(cfg), (double)g->attn_card[worst_index], + (double)g->attn[worst_index], diverged, q_width, LLAMA_TT_ATTN_DIVERGENCE); +} + +/** + * THE EXPORT PATH, CHECKED ON SILICON — the card's KV cache read back and compared against + * the host's, position for position. + * + * Session export downloads the card cache (llama_state_snapshot.c) and nothing else in the + * forward ever reads it back, so without this the only proof that `pn_tt_kv_download()` + * addresses the right tiles is off-hardware arithmetic. Under `PN_TT_ATTN_CHECK` BOTH caches + * are live and current — the host arm appends its fp32 rows and the card arm appends its bf16 + * ones from the same k/v — so the host cache is a real reference for the card's. + * + * It is bf16 against fp32, so the bar is a bf16 rounding and not equality; what it actually + * catches is a wrong tile, a wrong head stride, a transposed face map or an off-by-one + * position, every one of which is a whole-value error rather than a rounding one. + */ +static void llama_tt_kv_export_check(pn_llama_tt *g, pn_model *model, pn_state *state, + int layer, int pos) +{ + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + int head_dim = pn_head_dim(cfg); + int count = pos + 1; + size_t elems = (size_t)cfg->n_kv_head * (size_t)count * (size_t)head_dim; + float *card_k = malloc(elems * sizeof(float)); + float *card_v = malloc(elems * sizeof(float)); + double worst = 0.0; + int worst_index = 0, which = 0; + + if (!card_k || !card_v) { free(card_k); free(card_v); return; } + if (pn_tt_kv_download(g->device, (uint32_t)layer, false, (uint32_t)count, card_k) != 0 || + pn_tt_kv_download(g->device, (uint32_t)layer, true, (uint32_t)count, card_v) != 0) { + fprintf(stderr, "llama_tt kv-export: L%02d the card KV did not read back\n", layer); + free(card_k); free(card_v); + return; + } + for (int kvside = 0; kvside < 2; kvside++) { + const float *card = kvside ? card_v : card_k; + const float *live = (const float *)(kvside ? kv->v_cache[layer]->data + : kv->k_cache[layer]->data); + + for (int h = 0; h < cfg->n_kv_head; h++) { + for (int p = 0; p < count; p++) { + for (int e = 0; e < head_dim; e++) { + size_t ci = ((size_t)h * count + p) * head_dim + e; + size_t li = ((size_t)h * kv->max_seq + p) * head_dim + e; + double delta = fabs((double)card[ci] - (double)live[li]); + + if (delta > worst) { worst = delta; worst_index = (int)ci; which = kvside; } + } + } + } + } + if (worst > g->kv_export_worst) { + g->kv_export_worst = worst; + g->kv_export_worst_layer = layer; + } + fprintf(stderr, "llama_tt kv-export: L%02d %d positions, max|d|=%.6f in %s at %d\n", layer, + count, worst, which ? "V" : "K", worst_index); + free(card_k); + free(card_v); +} + +/** An angle folded into (-pi, pi], so two angles can be compared without a branch per wrap. */ +static double llama_tt_wrap_angle(double angle) +{ + double wrapped = fmod(angle + LLAMA_TT_PI, 2.0 * LLAMA_TT_PI); + + if (wrapped < 0.0) + wrapped += 2.0 * LLAMA_TT_PI; + return wrapped - LLAMA_TT_PI; +} + +/** + * Is this float an infinity or a NaN, read off its BITS? + * + * `isfinite()` cannot be used here: the build carries `-ffast-math`, which implies + * `-ffinite-math-only`, under which the compiler is entitled to fold `isfinite(x)` to 1 — so + * a census built on it would report zero non-finite elements whatever the card wrote. That is + * exactly the vacuous-check shape this project keeps a catalogue of. + */ +static int llama_tt_float_is_non_finite(float value) +{ + uint32_t bits; + + memcpy(&bits, &value, sizeof(bits)); + return ((bits >> 23) & 0xFFu) == 0xFFu; +} + +/** The angle a NeoX rotary applies to pair index `p` at `pos`, in the model's own terms. */ +static double llama_tt_expected_rope_angle(int p, int rot, int pos, double theta_base) +{ + return llama_tt_wrap_angle((double)pos * pow(theta_base, -2.0 * (double)p / (double)rot)); +} + +/** + * Recover the card's rotary angle per feature slot and report which pair index it belongs to. + * + * Reads the pre-RoPE strip and the post-RoPE strip off the card — neither is the host's, so + * this measures the card alone and does not need the host arm to be running. See the + * PN_TT_ROPE_CHECK block at the top of this file for the inversion. + * + * For every slot it scores the recovered angle against EVERY candidate pair index, so the + * output is not "does it match what we expect" but "which frequency did it actually apply". + * Slots whose identity and reflected candidates are closer together than the bf16 table's own + * error are counted as degenerate rather than voted, because at those frequencies no + * measurement could tell the two apart. + */ +static void llama_tt_rope_recover_check(pn_llama_tt *g, const pn_config *cfg, int layer, int pos) +{ + const pn_llama_tt_strips *s = &g->card; + int head_dim = pn_head_dim(cfg); + int q_width = cfg->n_head * head_dim; + int rot = (cfg->rope_dim > 0 && cfg->rope_dim <= head_dim) ? cfg->rope_dim : head_dim; + int half = rot / 2; + long identity = 0, reflected = 0, other = 0, degenerate = 0, readable = 0; + double worst_identity = 0.0, worst_norm_error = 0.0; + + if (pn_tt_activation_download(g->device, &s->q, g->probe_in, (uint32_t)q_width) != 0 || + pn_tt_activation_download(g->device, &s->q_rot, g->probe_out, (uint32_t)q_width) != 0) { + fprintf(stderr, "llama_tt rope-check: L%02d the rotary strips did not read back\n", + layer); + return; + } + for (int h = 0; h < cfg->n_head; h++) { + const float *x = g->probe_in + (size_t)h * head_dim; + const float *y = g->probe_out + (size_t)h * head_dim; + + for (int j = 0; j < half; j++) { + double x0 = x[j], x1 = x[j + half], y0 = y[j], y1 = y[j + half]; + double energy = x0 * x0 + x1 * x1; + double cos_t, sin_t, recovered, best_residual, identity_residual, separation; + int best_p = 0; + + if (energy < LLAMA_TT_ROPE_MIN_PAIR_ENERGY) + continue; + readable++; + cos_t = (x0 * y0 + x1 * y1) / energy; + sin_t = (x0 * y1 - x1 * y0) / energy; + /* A rotation preserves the norm. Reported separately because a table that is not + * a (cos, sin) pair at all fails HERE and not on the angle. */ + if (fabs(sqrt(cos_t * cos_t + sin_t * sin_t) - 1.0) > worst_norm_error) + worst_norm_error = fabs(sqrt(cos_t * cos_t + sin_t * sin_t) - 1.0); + recovered = atan2(sin_t, cos_t); + + identity_residual = fabs(llama_tt_wrap_angle( + recovered - llama_tt_expected_rope_angle(j, rot, pos, cfg->rope_theta))); + if (identity_residual > worst_identity) + worst_identity = identity_residual; + separation = fabs(llama_tt_wrap_angle( + llama_tt_expected_rope_angle(j, rot, pos, cfg->rope_theta) - + llama_tt_expected_rope_angle(half - 1 - j, rot, pos, cfg->rope_theta))); + if (separation < LLAMA_TT_ROPE_MIN_SEPARATION) { + degenerate++; + continue; + } + best_residual = -1.0; + for (int p = 0; p < half; p++) { + double residual = fabs(llama_tt_wrap_angle( + recovered - llama_tt_expected_rope_angle(p, rot, pos, cfg->rope_theta))); + + if (best_residual < 0.0 || residual < best_residual) { + best_residual = residual; + best_p = p; + } + } + if (best_p == j) { + identity++; + } else if (best_p == half - 1 - j) { + reflected++; + } else { + int distance = best_p > j ? best_p - j : j - best_p; + double gap = fabs(llama_tt_wrap_angle( + llama_tt_expected_rope_angle(j, rot, pos, cfg->rope_theta) - + llama_tt_expected_rope_angle(best_p, rot, pos, cfg->rope_theta))); + + other++; + if (distance > g->rope_worst_other_distance) + g->rope_worst_other_distance = distance; + if (gap > g->rope_worst_other_separation) + g->rope_worst_other_separation = gap; + } + } + } + g->rope_slots_identity += identity; + g->rope_slots_reflected += reflected; + g->rope_slots_other += other; + g->rope_slots_degenerate += degenerate; + if (worst_identity > g->rope_worst_identity_residual) + g->rope_worst_identity_residual = worst_identity; + if (worst_norm_error > g->rope_worst_norm_error) + g->rope_worst_norm_error = worst_norm_error; + fprintf(stderr, "llama_tt rope-check: L%02d pos=%d readable=%ld best_match identity=%ld " + "reflected=%ld other=%ld degenerate=%ld worst_identity_residual=%.6f rad " + "worst_norm_error=%.6f\n", + layer, pos, readable, identity, reflected, other, degenerate, worst_identity, + worst_norm_error); +} + +/** + * Read rows 1-31 of one strip off the card and report the largest magnitude in them. + * + * The strip invariant says they are zero. On the on-card attention arm that is INFERRED, not + * measured (pn_tt_oncard.c invariant 1), and this is the measurement. Non-finite elements are + * counted separately because an inf or a NaN in a row nothing reads is still an inf that a + * future row-crossing op would consume. + */ +static void llama_tt_row_census_strip(pn_llama_tt *g, const pn_tt_activation *strip, + const char *name, int width, int layer) +{ + double worst = 0.0, row0_worst = 0.0, row0_disagreement = 0.0; + long nonfinite = 0; + int worst_row = 0, worst_index = 0; + + /* THE CONTROL, and without it a reader that returned zeros would report the invariant + * HOLDING. Row 0 is the row the forward actually uses, so it must come back non-zero, and + * it must AGREE with the shipping row-0 reader — which reads different bytes by a + * different path. A census that cannot go red is the vacuous-gate shape this project has + * caught four times. */ + if (pn_tt_activation_download_tile_row(g->device, strip, 0u, g->probe_in, + (uint32_t)width) == 0 && + pn_tt_activation_download(g->device, strip, g->probe_out, (uint32_t)width) == 0) { + for (int i = 0; i < width; i++) { + double delta = fabs((double)g->probe_in[i] - (double)g->probe_out[i]); + + if (fabs((double)g->probe_in[i]) > row0_worst) + row0_worst = fabs((double)g->probe_in[i]); + if (delta > row0_disagreement) + row0_disagreement = delta; + } + } + for (uint32_t row = 1; row < 32u; row++) { + if (pn_tt_activation_download_tile_row(g->device, strip, row, g->probe_in, + (uint32_t)width) != 0) { + fprintf(stderr, "llama_tt row-census: L%02d %s row %u did not read back\n", layer, + name, row); + return; + } + for (int i = 0; i < width; i++) { + double value = (double)g->probe_in[i]; + + if (llama_tt_float_is_non_finite(g->probe_in[i])) { nonfinite++; continue; } + if (fabs(value) > worst) { + worst = fabs(value); + worst_row = (int)row; + worst_index = i; + } + } + } + if (worst > g->row_census_worst) + g->row_census_worst = worst; + g->row_census_nonfinite += nonfinite; + fprintf(stderr, "llama_tt row-census: L%02d %-7s rows 1-31 max|v|=%.6g at row %d column %d, " + "%ld non-finite of %d; CONTROL row 0 max|v|=%.6g, disagrees with the row-0 " + "reader by %.6g\n", + layer, name, worst, worst_row, worst_index, nonfinite, 31 * width, row0_worst, + row0_disagreement); +} + +/** The attention half of one layer. Split out to keep the forward readable and to keep + * each half under a screen — the ordering inside it is the CUDA path's, unchanged. */ +static int llama_tt_attention(pn_llama_tt *g, pn_model *model, pn_state *state, int layer, + int pos) +{ + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + pn_llama_qlayer *cpu = &qw->layers[layer]; + pn_llama_tt_layer *d = &g->layers[layer]; + pn_llama_tt_strips *s = &g->card; + pn_config *cfg = &model->config; + bool on_card = pn_tt_device_oncard(g->device, PN_TT_ONCARD_ATTENTION); + int q_width = cfg->n_head * pn_head_dim(cfg); + + if (llama_tt_norm(g, &d->attn_norm, cpu->attn_norm, cfg) != 0) + return -1; + /* ONE WAIT FOR THREE PROJECTIONS. They share `normed` and write q, k and v, which are + * three disjoint strips — so nothing here reads what this set writes and the host has no + * reason to stop between them. The wait is the next thing that touches q/k/v, and every + * device operation flushes on entry, so it happens whether or not anyone remembers. */ + if (llama_tt_matvec_begin(g, &d->wq, &cpu->wq, &s->normed, &s->q, layer, "attn_q") != 0 || + llama_tt_matvec_begin(g, &d->wk, &cpu->wk, &s->normed, &s->k, layer, "attn_k") != 0 || + llama_tt_matvec_begin(g, &d->wv, &cpu->wv, &s->normed, &s->v, layer, "attn_v") != 0) + return -1; + if (pn_tt_matvec_flush(g->device) != 0) + return -1; + + /* The host arm runs FIRST under the check, because it is the one that consumes q/k/v as + * they are — the card arm rotates q and k into their own strips and leaves the originals + * untouched, so the order does not change either answer. */ + if (!on_card || g->attn_check) { + if (llama_tt_attention_host(g, model, state, layer, pos) != 0) + return -1; + if (!on_card && + pn_tt_activation_upload(g->device, &s->attn, g->attn, (uint32_t)q_width) != 0) + return -1; + } + if (on_card) { + if (llama_tt_attention_card(g, model, state, layer, pos) != 0) + return -1; + /* Layer 0 only, and only past position 0. At position 0 every rotary angle is exactly + * zero, so the recovery would be reading the identity out of the identity — a probe + * that cannot distinguish anything is worse than no probe. */ + if (g->rope_check && layer == 0 && pos > 0) + llama_tt_rope_recover_check(g, cfg, layer, pos); + if (g->row_census && layer == (pos % cfg->n_layer)) + llama_tt_row_census_strip(g, &s->attn, "attn", q_width, layer); + if (g->attn_check) { + llama_tt_attention_compare(g, cfg, layer); + /* ONE layer, not all 24: this reads whole tiles off the card and its cost grows + * with the sequence, so checking every layer every token would dominate the run + * and prove nothing the rotation does not. The layer rotates with the position so + * a long enough run covers all of them. */ + if (layer == (pos % cfg->n_layer)) + llama_tt_kv_export_check(g, model, state, layer, pos); + } + } + + if (llama_tt_matvec(g, &d->wo, &cpu->wo, &s->attn, &s->project, layer, "attn_output") != 0) + return -1; + if (llama_tt_residual(g, cfg) != 0) + return -1; + /* AFTER the residual add, because the question is not what flash decode wrote but what + * propagates: `hidden` is the strip every later layer and the LM head read. */ + if (on_card && g->row_census && layer == (pos % cfg->n_layer)) + llama_tt_row_census_strip(g, llama_tt_hidden(g), "hidden", cfg->n_embd, layer); + return 0; +} + +/** The SwiGLU half of one layer. Nothing here crosses the link when the gate and the + * residual add are on-card: normed, gate, up and the product are all DRAM strips. */ +static int llama_tt_ffn(pn_llama_tt *g, pn_model *model, int layer) +{ + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + pn_llama_qlayer *cpu = &qw->layers[layer]; + pn_llama_tt_layer *d = &g->layers[layer]; + pn_llama_tt_strips *s = &g->card; + pn_config *cfg = &model->config; + + if (llama_tt_norm(g, &d->ffn_norm, cpu->ffn_norm, cfg) != 0) + return -1; + /* ONE WAIT FOR BOTH HALVES OF THE GATE — same argument as q/k/v: one input, two + * disjoint outputs, and the SwiGLU that consumes them flushes on entry. */ + if (llama_tt_matvec_begin(g, &d->w_gate, &cpu->w_gate, &s->normed, &s->gate, layer, + "ffn_gate") != 0 || + llama_tt_matvec_begin(g, &d->w_up, &cpu->w_up, &s->normed, &s->up, layer, "ffn_up") != 0) + return -1; + if (pn_tt_matvec_flush(g->device) != 0) + return -1; + + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_SWIGLU)) { + if (pn_tt_silu_mul(g->device, &s->gate, &s->up, &s->swiglu) != 0) + return -1; + } else { + if (pn_tt_activation_download(g->device, &s->gate, g->gate, (uint32_t)cfg->n_ff) != 0 || + pn_tt_activation_download(g->device, &s->up, g->up, (uint32_t)cfg->n_ff) != 0) + return -1; + pn_vsilu_mul_f32(g->gate, g->up, g->gate, cfg->n_ff); + if (pn_tt_activation_upload(g->device, &s->swiglu, g->gate, (uint32_t)cfg->n_ff) != 0) + return -1; + } + + if (llama_tt_matvec(g, &d->w_down, &cpu->w_down, &s->swiglu, &s->project, layer, + "ffn_down") != 0) + return -1; + return llama_tt_residual(g, cfg); +} + +/** + * The LM head, and the only place the vocabulary is involved. + * + * Two arms, and the difference is 261,120 bytes a token. With the on-card top-N the card + * hands back eight (index, value) pairs and nothing else; `PN_TT_SPARSE_LOGITS` presents + * exactly those, scattered into an all -INFINITY row, which is the shape the K80 path + * already ships. Without it the whole row is read so the parity harness can score it — and + * when both are true, the card's index is checked against the host's serial argmax over the + * row it did NOT see, which is the gate this op is accepted on. + */ +static int llama_tt_head(pn_llama_tt *g, pn_model *model, float *logits_raw) +{ + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + pn_config *cfg = &model->config; + bool have_topk = false; + + if (llama_tt_norm(g, &g->output_norm, qw->output_norm, cfg) != 0) + return -1; + /* The vector the LM head actually reads. It is the last row of the residual trace + * because a divergence here is the one that reaches the logits directly. */ + if (pn_residual_trace_armed()) { + if (pn_tt_activation_download(g->device, &g->card.normed, g->normed, + (uint32_t)cfg->n_embd) != 0) + return -1; + pn_residual_trace_record(cfg->n_layer, g->normed, cfg->n_embd); + } + if (llama_tt_matvec(g, &g->output, &qw->output, &g->card.normed, &g->card.logits, -1, + "lm_head") != 0) + return -1; + + g->topk_count = 0u; + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_TOPK)) { + if (pn_tt_topk(g->device, &g->card.logits, PN_TT_TOPK_PAIRS, g->topk_index, + g->topk_value, &g->topk_count) != 0) + return -1; + have_topk = g->topk_count > 0u; + } + + if (g->sparse_logits && have_topk) { + for (int i = 0; i < cfg->n_vocab; i++) + logits_raw[i] = -INFINITY; + for (uint32_t i = 0; i < g->topk_count; i++) { + if (g->topk_index[i] < (uint32_t)cfg->n_vocab) + logits_raw[g->topk_index[i]] = g->topk_value[i]; + } + return 0; + } + + if (pn_tt_activation_download(g->device, &g->card.logits, logits_raw, + (uint32_t)cfg->n_vocab) != 0) + return -1; + if (have_topk) { + int argmax = 0; + + for (int i = 1; i < cfg->n_vocab; i++) + if (logits_raw[i] > logits_raw[argmax]) argmax = i; + fprintf(stderr, "llama_tt: on-card top-1 index %u vs host argmax %d — %s\n", + g->topk_index[0], argmax, + g->topk_index[0] == (uint32_t)argmax ? "MATCH" : "MISMATCH"); + } + return 0; +} + +/** The per-token cost and the run total, split by the machine that paid each. */ +static void llama_tt_report(pn_llama_tt *g) +{ + double upload = 0.0, dispatch = 0.0, readback = 0.0, oncard = 0.0; + uint64_t reg_r = 0, reg_w = 0, bulk_r = 0, bulk_w = 0; + uint64_t resident = 0, oneshot = 0, restarts = 0, oncard_ops = 0, oncard_restarts = 0; + uint64_t matvecs = pn_tt_device_matvec_count(g->device); + + pn_tt_device_phase_seconds(g->device, &upload, &dispatch, &readback); + pn_tt_device_oncard_stats(g->device, &oncard, &oncard_ops, &oncard_restarts); + /* The TOTAL is the sum of the four phases, not `matvec_seconds`: an activation that + * crosses the link now does so OUTSIDE a matvec — a strip is uploaded once and read back + * once, not per projection — so a total taken from the matvec timer would omit exactly + * the transfers this file exists to remove and would improve on its own. */ + /* THIS TOKEN, then the run so far. The device's counters are cumulative and monotonic; + * printing only those under "token done" is what let a 0.437 s token be read as a + * 16.789 s one. The per-token line is first because it is the one anyone quotes. */ + fprintf(stderr, "llama_tt: THIS TOKEN — %llu matmuls, %.3f s " + "(upload %.3f + dispatch/compute %.3f + readback %.3f + on-card %.3f)\n", + (unsigned long long)(matvecs - g->last_matvecs), + (upload - g->last_upload) + (dispatch - g->last_dispatch) + + (readback - g->last_readback) + (oncard - g->last_oncard), + upload - g->last_upload, dispatch - g->last_dispatch, readback - g->last_readback, + oncard - g->last_oncard); + fprintf(stderr, "llama_tt: RUN TOTAL — %llu matmuls on the card, %.3f s total " + "(upload %.3f + dispatch/compute %.3f + readback %.3f + on-card %.3f " + "over %llu ops), %.2f MiB read back, %.2f MiB written\n", + (unsigned long long)matvecs, + upload + dispatch + readback + oncard, upload, dispatch, readback, oncard, + (unsigned long long)oncard_ops, + (double)pn_tt_device_bytes_read(g->device) / (1024.0 * 1024.0), + (double)pn_tt_device_bytes_written(g->device) / (1024.0 * 1024.0)); + g->last_upload = upload; + g->last_dispatch = dispatch; + g->last_readback = readback; + g->last_oncard = oncard; + g->last_matvecs = matvecs; + /* Which WINDOW carried those bytes. A register access on the bulk path, or bulk data + * still on the register path, both look like a working token from here and only these + * counters tell them apart. */ + pn_tt_device_window_traffic(g->device, ®_r, ®_w, &bulk_r, &bulk_w); + { + uint64_t reg_aims = 0, bulk_aims = 0, aim_failures = 0; + + /* The aim counts ride the same line because they answer the question the byte + * counts cannot: the two transports move IDENTICAL bytes and differ only in what + * an aim costs, so a wall-clock gap between them is aims x per-aim cost and there + * is no way to price it without this number. */ + pn_tt_device_window_aims(g->device, ®_aims, &bulk_aims, &aim_failures); + fprintf(stderr, "llama_tt: windows — register(UC/word32) read %llu B write %llu B " + "%llu aims, bulk(WC/block16) read %llu B write %llu B %llu aims, " + "%llu aim failures\n", + (unsigned long long)reg_r, (unsigned long long)reg_w, + (unsigned long long)reg_aims, (unsigned long long)bulk_r, + (unsigned long long)bulk_w, (unsigned long long)bulk_aims, + (unsigned long long)aim_failures); + } + /* WHICH dispatch path carried the ops. Both produce identical numbers, so a silent + * fallback to one-shot looks exactly like a resident run from the outside. */ + pn_tt_device_dispatch_counts(g->device, &resident, &oneshot, &restarts); + /* HOW MANY PROJECTIONS ACTUALLY WENT WIDE. A group whose member count does not divide a + * weight's Nt leaves that weight on one core, so a run can be configured for N cores and + * still spend most of its seconds on one — which reads from outside as "multi-core did + * not help" and is a different statement entirely. */ + { + uint64_t split_ops = 0, solo_ops = 0; + + pn_tt_matmul_group_op_counts(g->device, &split_ops, &solo_ops); + if (split_ops + solo_ops > 0) + fprintf(stderr, "llama_tt: group — %llu projections split across the group, " + "%llu on one core\n", + (unsigned long long)split_ops, (unsigned long long)solo_ops); + } + /* WHAT THE DISPATCH PHASE IS MADE OF. `dispatch/compute` above is one number for two + * very different things, and which of them grows with the member count is the whole + * question a multi-core round has to answer. */ + pn_tt_matmul_group_time_report(g->device); + fprintf(stderr, "llama_tt: dispatch — %llu resident, %llu one-shot, %llu matmul " + "residency restarts, %llu on-card residency restarts; " + "on-card norm %d swiglu %d residual %d topk %d attention %d\n", + (unsigned long long)resident, (unsigned long long)oneshot, + (unsigned long long)restarts, (unsigned long long)oncard_restarts, + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_NORM), + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_SWIGLU), + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_RESIDUAL), + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_TOPK), + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_ATTENTION)); + /* The crossed control's verdict for the WHOLE run, not per layer: a single worst element + * over every layer and every token is the number that says whether the card's attention + * ever left the rounding band, and the count says how wide the excursion was. */ + if (g->attn_check) + fprintf(stderr, "llama_tt attn-check: RUN WORST max|d|=%.6f at layer %d, %ld elements " + "past %.3f; KV EXPORT worst max|d|=%.6f at layer %d\n", + g->attn_check_worst, g->attn_check_worst_layer, g->attn_check_diverged, + LLAMA_TT_ATTN_DIVERGENCE, g->kv_export_worst, g->kv_export_worst_layer); + /* The rotary verdict for the WHOLE run. `identity` is the artifact's own pairing; every + * count in `reflected` or `other` is a feature the card rotated at another frequency. */ + if (g->rope_check) + fprintf(stderr, "llama_tt rope-check: RUN TOTAL best_match identity=%ld reflected=%ld " + "other=%ld (degenerate, not voted=%ld); worst identity residual %.6f " + "rad, worst norm error %.6f; worst 'other' was %d index away with the " + "two candidates %.6f rad apart\n", + g->rope_slots_identity, g->rope_slots_reflected, g->rope_slots_other, + g->rope_slots_degenerate, g->rope_worst_identity_residual, + g->rope_worst_norm_error, g->rope_worst_other_distance, + g->rope_worst_other_separation); + if (g->row_census) + fprintf(stderr, "llama_tt row-census: RUN WORST |v| in rows 1-31 = %.6g, %ld non-finite " + "elements\n", g->row_census_worst, g->row_census_nonfinite); +} + +tm_tensor *pn_llama_forward_tt(pn_model *model, uint32_t token, pn_state *state) +{ + if (!model || !state || !model->weights || !model->gpu) + return NULL; + pn_llama_tt *g = (pn_llama_tt *)model->gpu; + if (!g->loaded) + return NULL; + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + int pos = state->pos; + + if (!kv || llama_tt_ensure_scores(g, kv->max_seq) != 0) + return NULL; + if (llama_tt_ensure_attention(g, model, kv->max_seq) != 0) + return NULL; + /* The card holds ONE KV region set. The host cache is the sequence's identity, and a + * cache that comes back after being displaced is two sequences alternating on it. */ + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_ATTENTION)) { + if (pn_tt_kv_bind(g->device, kv) != 0) + return NULL; + /* SAY WHERE THE STATE IS, so session export cannot read the wrong place. On this arm + * the host tensors are never written; llama_state_snapshot.c calls this hook and + * moves the rows to and from the card instead. Set HERE and not at allocation, + * because the forward is the only code that knows which arm is live — and under + * PN_TT_ATTN_CHECK both caches ARE current, so the card is still the one to trust: + * it is what the next token will actually attend to. */ + kv->kv_device = g->device; + kv->kv_move = llama_tt_kv_move; + } + + /* Token embedding, on the host: the artifact stores the table tiled and block-float, + * which the on-card gather cannot read (pn_tt_device.h). The engine's own fp32 copy is + * the same values and is already resident. It costs one 64-byte-a-tile UPLOAD, in the + * direction the link is ~60x faster. */ + if (state->embd_override) + memcpy(g->hidden, state->embd_override, (size_t)cfg->n_embd * sizeof(float)); + else + memcpy(g->hidden, qw->tok_embd + (size_t)token * cfg->n_embd, + (size_t)cfg->n_embd * sizeof(float)); + g->card.slot = 0; + g->hidden_on_host = true; + g->hidden_on_card = false; + + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (llama_tt_attention(g, model, state, layer, pos) != 0) + return NULL; + if (llama_tt_ffn(g, model, layer) != 0) + return NULL; + /* The card's residual stream at the same point the CPU oracle records its own. The + * download is the instrument's cost and changes no arithmetic: the card copy stays + * authoritative, so the next layer's norm still reads what it would have. */ + if (pn_residual_trace_armed()) { + if (llama_tt_hidden_to_host(g, cfg->n_embd) != 0) + return NULL; + pn_residual_trace_record(layer, g->hidden, cfg->n_embd); + } + if (g->trace > 1) { + double norm = 0.0; + + if (llama_tt_hidden_to_host(g, cfg->n_embd) != 0) + return NULL; + for (int i = 0; i < cfg->n_embd; i++) + norm += (double)g->hidden[i] * (double)g->hidden[i]; + fprintf(stderr, "llama_tt trace: L%02d hidden |x|=%.6f\n", layer, sqrt(norm)); + } + } + + float *logits_raw = malloc((size_t)cfg->n_vocab * sizeof(float)); + if (!logits_raw) + return NULL; + if (llama_tt_head(g, model, logits_raw) != 0) { + free(logits_raw); + return NULL; + } + + int64_t logits_shape[] = { cfg->n_vocab }; + tm_tensor *logits_t = tm_from_data(logits_raw, logits_shape, 1, TM_F32); + tm_tensor *logits = logits_t ? tm_clone(logits_t) : NULL; + if (logits_t) tm_free(logits_t); + free(logits_raw); + + llama_tt_report(g); + state->pos++; + return logits; +} + + +/* ============================================================ + * BATCHED PREFILL — the tile quantum IS the batch dimension + * + * --------------------------------------------------------------------------- + * WHAT WAS WRONG, AND IT WAS NOT THE MATMUL + * --------------------------------------------------------------------------- + * A prompt has every one of its tokens in hand at once, so every weight tile read can serve + * ALL of them. We were reading the model once per prompt token: a 166-token prompt read a + * 1.15 GB artifact 166 times, ~190 GB, and measured TTFT 39.25 s. The matmul counter proved + * it rather than the timing implying it — a 25-token prompt plus 24 decode tokens moved the + * RUN TOTAL by exactly 8,281 = 49 x 169 forwards. + * + * --------------------------------------------------------------------------- + * THE CARD WAS ALREADY DOING IT AND WE WERE THROWING 31 OF 32 ROWS AWAY + * --------------------------------------------------------------------------- + * A tile is 32x32 and a matvec is Mt=1, so the FPU multiplies 32 rows against every weight + * tile it reads and 31 of them are the zero padding pn_tt_activation_alloc staged. The + * reader floor — 81 core cycles per tile-pair, the AI clock — is a cost of READING a weight + * tile and is paid per tile, not per row. So M prompt positions in one tile cost the card + * EXACTLY what one position costs: same descriptor, same Mt, same Kt, same Nt, same bytes. + * + * Everything downstream was already 32-row shaped and nobody had to be told: + * - `pn_tt_rms_norm` submits `row_blocks = 1`, and one row block IS 32 rows, each reduced + * over its own row (resident_reduce_compute.c); + * - the elementwise family (SwiGLU, the residual adds) is per-TILE, so all 32 rows already; + * - the matmul is per-row in M by construction: C[m][n] sums over k and reads only row m + * of A, so a row cannot see another row. + * The ONLY thing standing between the card and a batch was that nothing could put more than + * one row across the link. pn_tt_activation_{upload,download}_rows is that, and this is its + * caller. + * + * --------------------------------------------------------------------------- + * WHY 32 AND NOT 32 x SOMETHING — THE L1 QUESTION, ANSWERED RATHER THAN ASSUMED + * --------------------------------------------------------------------------- + * 32 is the TILE QUANTUM, not a budget: it costs zero extra L1 (Mt does not appear in the + * matmul's CB sizing, `2*depth*(in0_tile + in1_tile) + 2*out_tile <= 1,351,680`) and zero + * extra DRAM (the strips are already 32 rows tall — that is what was being wasted). + * + * Going PAST 32 means Mt >= 2, and with the resident reader as it stands that buys no + * bandwidth at all. Its loop is `for mt { for nt { for kt } }`, so B is re-read once per + * tile-row and A once per output column: both traffics scale linearly in Mt and the bytes + * PER TOKEN are flat. Mt > 1 would amortise the descriptor and nothing else. A blocked + * reader that held Mt A tile-rows in L1 and streamed B past all of them WOULD, and that is + * where L1 becomes the binding constraint: 2*depth*(Mt*2048 + 1088) + 2*Mt*2048 <= 1,351,680 + * gives Mt <= 6 at this model's depth=48 (192 rows) and Mt <= 164 at depth=1. That is a + * kernel round, not this one. + * ============================================================ */ + +/** Rows of one batched pass. 32 is the tile and the default; the env exists so a sweep can + * A/B the batch against itself, and 1 turns the batched path into the sequential one + * through the same code — which is what makes the byte comparison below a control. */ +#define PN_TT_PREFILL_ROWS_ENV "PN_TT_PREFILL_ROWS" + +/** `PN_TT_PREFILL=0` puts the whole prompt back on the one-at-a-time path, in ONE binary. */ +#define PN_TT_PREFILL_ENV "PN_TT_PREFILL" + +/** + * THE BREAK-EVEN, AND IT IS A DIVISION RATHER THAN A TASTE. + * + * A batched pass reads WHOLE tiles: 2048 B per tile however many of the 32 rows carry a + * token. The row-0 reader costs 128 B per tile per row (2 x 64 B, the card->host alignment). + * So a pass of M rows costs 2048/M bytes a row against 128, and the two are equal at + * **M = 16**. Below that the batch moves MORE bytes per token than the path it replaces, and + * since prefill is now readback-bound that is a straight loss — measured, not feared: a pass + * of 8 rows reads 0.94x of the sequential rate and a pass of 3 reads 0.43x. + * + * So a prompt shorter than this, and the ragged tail of any prompt, go through the ordinary + * one-at-a-time forward. `PN_TT_PREFILL_MIN_ROWS` is the A/B knob; 0 disables the guard so + * the loss is reproducible on demand. + */ +#define PN_TT_PREFILL_MIN_ROWS_ENV "PN_TT_PREFILL_MIN_ROWS" + +static int llama_tt_prefill_min_rows(void) +{ + const char *env = getenv(PN_TT_PREFILL_MIN_ROWS_ENV); + int min_rows = env ? atoi(env) : 16; + + if (min_rows < 1) min_rows = 1; + if (min_rows > 32) min_rows = 32; + return min_rows; +} + +static int llama_tt_prefill_rows(void) +{ + const char *env = getenv(PN_TT_PREFILL_ROWS_ENV); + int rows = env ? atoi(env) : 32; + + if (rows < 1) rows = 1; + if (rows > 32) rows = 32; + return rows; +} + +static bool llama_tt_prefill_enabled(void) +{ + const char *env = getenv(PN_TT_PREFILL_ENV); + + return !(env && env[0] == '0'); +} + +/** + * THE RED PROOFS, and the gate is not believed until each of them goes red. + * + * Same knob and same shape as `pn_tt_red` in the device layer, spelled here so the model + * file does not have to reach into pn_tt_device_internal.h. Each mutation is HOST-SIDE + * arithmetic only — no transfer changes length, so none of them can reach the zero-length + * NoC hazard a shortening mutation would (DEV_NOTES.md §1). + * + * prefill_pos every row rotates and appends at the FIRST position of the pass, so a + * batch of 32 writes 32 K/V entries all built for base_pos. This is what + * "the batch forgot which row is which token" looks like. + * prefill_causal the per-row loop runs in REVERSE, so row r reads cache entries later + * rows have already written. The mask in this design IS the loop order, + * so this is the mutation that proves the order is load-bearing. + * prefill_rowfill q/k/v come back and rows 1..M-1 are overwritten with row 0 — exactly + * the bug of a transfer that still only moves logical row 0, which is + * the state this whole path replaces. + */ +static bool llama_tt_red(const char *name) +{ + const char *value = getenv("PN_TT_RED"); + + return value && *value && strcmp(value, name) == 0; +} + +/** The batch host scratch, allocated the first time a batched prefill runs. */ +static int llama_tt_alloc_batch(pn_llama_tt *g, const pn_config *cfg, int rows) +{ + int n_embd = cfg->n_embd; + int q_width = cfg->n_head * pn_head_dim(cfg); + int kv_width = cfg->n_kv_head * pn_head_dim(cfg); + int widest = cfg->n_ff > n_embd ? cfg->n_ff : n_embd; + + if (g->b_rows >= rows) + return 0; + free(g->b_hidden); free(g->b_normed); free(g->b_q); free(g->b_k); free(g->b_v); + free(g->b_attn); free(g->b_gate); free(g->b_up); free(g->b_scratch); + g->b_rows = 0; + g->b_hidden = calloc((size_t)rows * n_embd, sizeof(float)); + g->b_normed = calloc((size_t)rows * n_embd, sizeof(float)); + g->b_q = calloc((size_t)rows * q_width, sizeof(float)); + g->b_k = calloc((size_t)rows * kv_width, sizeof(float)); + g->b_v = calloc((size_t)rows * kv_width, sizeof(float)); + g->b_attn = calloc((size_t)rows * q_width, sizeof(float)); + g->b_gate = calloc((size_t)rows * cfg->n_ff, sizeof(float)); + g->b_up = calloc((size_t)rows * cfg->n_ff, sizeof(float)); + g->b_scratch = calloc((size_t)rows * widest, sizeof(float)); + if (!g->b_hidden || !g->b_normed || !g->b_q || !g->b_k || !g->b_v || !g->b_attn || + !g->b_gate || !g->b_up || !g->b_scratch) + return -1; + g->b_rows = rows; + return 0; +} + +/** One RMS norm over `rows` rows. On card it is the SAME descriptor the single-token path + * submits — `row_blocks = 1` already means 32 rows — so this branch is not a batched + * variant of the op, it is the op with its rows used. */ +static int llama_tt_norm_rows(pn_llama_tt *g, const pn_tt_norm_constants *constants, + const float *host_gamma, const pn_config *cfg, + const pn_tt_activation *in, const pn_tt_activation *out, + int rows) +{ + int n_embd = cfg->n_embd; + + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_NORM)) + return pn_tt_rms_norm(g->device, constants, in, out); + if (pn_tt_activation_download_rows(g->device, in, g->b_scratch, (uint32_t)n_embd, + (uint32_t)rows) != 0) + return -1; + for (int r = 0; r < rows; r++) + pn_vrms_norm_f32(g->b_scratch + (size_t)r * n_embd, host_gamma, + g->b_normed + (size_t)r * n_embd, n_embd, cfg->rms_norm_eps); + return pn_tt_activation_upload_rows(g->device, out, g->b_normed, (uint32_t)n_embd, + (uint32_t)rows); +} + +/** hidden += project, over `rows` rows, flipping the live slot exactly as the single-token + * residual does. The card arm is per-TILE, so it needed nothing. */ +static int llama_tt_residual_rows(pn_llama_tt *g, const pn_config *cfg, int rows) +{ + pn_llama_tt_strips *s = &g->card; + int n_embd = cfg->n_embd; + + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_RESIDUAL)) { + if (pn_tt_add(g->device, llama_tt_hidden(g), &s->project, llama_tt_hidden_spare(g)) != 0) + return -1; + s->slot ^= 1; + return 0; + } + if (pn_tt_activation_download_rows(g->device, &s->project, g->b_scratch, + (uint32_t)n_embd, (uint32_t)rows) != 0) + return -1; + if (pn_tt_activation_download_rows(g->device, llama_tt_hidden(g), g->b_hidden, + (uint32_t)n_embd, (uint32_t)rows) != 0) + return -1; + for (int r = 0; r < rows; r++) + pn_vadd_f32(g->b_hidden + (size_t)r * n_embd, g->b_scratch + (size_t)r * n_embd, + g->b_hidden + (size_t)r * n_embd, n_embd); + return pn_tt_activation_upload_rows(g->device, llama_tt_hidden(g), g->b_hidden, + (uint32_t)n_embd, (uint32_t)rows); +} + +/** + * ONE batched pass: `rows` consecutive prompt positions starting at `base_pos`, through the + * whole layer stack, leaving their K/V in the cache and NOT computing any logits. + * + * No LM head runs here at all. The caller hands the LAST prompt token to the ordinary + * single-token forward, which is what produces the logits — so the head, the top-N and the + * sparse-logit presentation are literally the shipping code and cannot diverge. It also + * makes the acceptance argument short: if the K/V this writes is byte-identical to what the + * sequential path writes, the logits are byte-identical because they come out of the same + * function reading the same cache. + */ +static int llama_tt_prefill_pass(pn_llama_tt *g, pn_model *model, pn_state *state, + const uint32_t *tokens, const float *const *embd_rows, + int rows, int base_pos) +{ + pn_llama_qweights *qw = (pn_llama_qweights *)model->weights; + pn_llama_tt_strips *s = &g->card; + pn_config *cfg = &model->config; + int n_embd = cfg->n_embd; + int q_width = cfg->n_head * pn_head_dim(cfg); + int kv_width = cfg->n_kv_head * pn_head_dim(cfg); + + for (int r = 0; r < rows; r++) { + if (embd_rows && embd_rows[r]) + memcpy(g->b_hidden + (size_t)r * n_embd, embd_rows[r], + (size_t)n_embd * sizeof(float)); + else + memcpy(g->b_hidden + (size_t)r * n_embd, + qw->tok_embd + (size_t)tokens[r] * n_embd, + (size_t)n_embd * sizeof(float)); + } + s->slot = 0; + g->hidden_on_host = false; + g->hidden_on_card = true; + if (pn_tt_activation_upload_rows(g->device, llama_tt_hidden(g), g->b_hidden, + (uint32_t)n_embd, (uint32_t)rows) != 0) + return -1; + + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_llama_qlayer *cpu = &qw->layers[layer]; + pn_llama_tt_layer *d = &g->layers[layer]; + + if (llama_tt_norm_rows(g, &d->attn_norm, cpu->attn_norm, cfg, llama_tt_hidden(g), + &s->normed, rows) != 0) + return -1; + /* The same chain the single-token path takes: one wait for three projections. */ + if (llama_tt_matvec_begin(g, &d->wq, NULL, &s->normed, &s->q, layer, "attn_q") != 0 || + llama_tt_matvec_begin(g, &d->wk, NULL, &s->normed, &s->k, layer, "attn_k") != 0 || + llama_tt_matvec_begin(g, &d->wv, NULL, &s->normed, &s->v, layer, "attn_v") != 0) + return -1; + if (pn_tt_matvec_flush(g->device) != 0) + return -1; + if (pn_tt_activation_download_rows(g->device, &s->q, g->b_q, (uint32_t)q_width, + (uint32_t)rows) != 0 || + pn_tt_activation_download_rows(g->device, &s->k, g->b_k, (uint32_t)kv_width, + (uint32_t)rows) != 0 || + pn_tt_activation_download_rows(g->device, &s->v, g->b_v, (uint32_t)kv_width, + (uint32_t)rows) != 0) + return -1; + /* STRICTLY IN POSITION ORDER, and that is a correctness requirement rather than a + * convenience: row r attends its own K/V, so row r's append has to have happened + * before row r reads the cache, and rows r+1.. must NOT have. A row loop in order + * gives exactly the causal mask the sequential path produces, with no mask code. */ + if (llama_tt_red("prefill_rowfill")) + for (int r = 1; r < rows; r++) { + memcpy(g->b_q + (size_t)r * q_width, g->b_q, + (size_t)q_width * sizeof(float)); + memcpy(g->b_k + (size_t)r * kv_width, g->b_k, + (size_t)kv_width * sizeof(float)); + memcpy(g->b_v + (size_t)r * kv_width, g->b_v, + (size_t)kv_width * sizeof(float)); + } + for (int i = 0; i < rows; i++) { + int r = llama_tt_red("prefill_causal") ? rows - 1 - i : i; + int row_pos = llama_tt_red("prefill_pos") ? base_pos : base_pos + r; + + if (llama_tt_attend_one(g, model, state, layer, row_pos, + g->b_q + (size_t)r * q_width, + g->b_k + (size_t)r * kv_width, + g->b_v + (size_t)r * kv_width, + g->b_attn + (size_t)r * q_width) != 0) + return -1; + } + if (pn_tt_activation_upload_rows(g->device, &s->attn, g->b_attn, (uint32_t)q_width, + (uint32_t)rows) != 0) + return -1; + if (llama_tt_matvec(g, &d->wo, NULL, &s->attn, &s->project, layer, "attn_output") != 0) + return -1; + if (llama_tt_residual_rows(g, cfg, rows) != 0) + return -1; + + if (llama_tt_norm_rows(g, &d->ffn_norm, cpu->ffn_norm, cfg, llama_tt_hidden(g), + &s->normed, rows) != 0) + return -1; + if (llama_tt_matvec_begin(g, &d->w_gate, NULL, &s->normed, &s->gate, layer, + "ffn_gate") != 0 || + llama_tt_matvec_begin(g, &d->w_up, NULL, &s->normed, &s->up, layer, "ffn_up") != 0) + return -1; + if (pn_tt_matvec_flush(g->device) != 0) + return -1; + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_SWIGLU)) { + if (pn_tt_silu_mul(g->device, &s->gate, &s->up, &s->swiglu) != 0) + return -1; + } else { + if (pn_tt_activation_download_rows(g->device, &s->gate, g->b_gate, + (uint32_t)cfg->n_ff, (uint32_t)rows) != 0 || + pn_tt_activation_download_rows(g->device, &s->up, g->b_up, + (uint32_t)cfg->n_ff, (uint32_t)rows) != 0) + return -1; + for (int r = 0; r < rows; r++) + pn_vsilu_mul_f32(g->b_gate + (size_t)r * cfg->n_ff, + g->b_up + (size_t)r * cfg->n_ff, + g->b_gate + (size_t)r * cfg->n_ff, cfg->n_ff); + if (pn_tt_activation_upload_rows(g->device, &s->swiglu, g->b_gate, + (uint32_t)cfg->n_ff, (uint32_t)rows) != 0) + return -1; + } + if (llama_tt_matvec(g, &d->w_down, NULL, &s->swiglu, &s->project, layer, + "ffn_down") != 0) + return -1; + if (llama_tt_residual_rows(g, cfg, rows) != 0) + return -1; + } + state->pos = base_pos + rows; + return 0; +} + +/** Return every strip this file owns to the zeroed state a Mt=1 matvec's row 0 depends on. + * Nothing in the decode path reads rows 1-31 — `pn_tt_topk` names row 0 in its descriptor + * and every other op is per-row — so this is not load-bearing today. It is what keeps the + * strip invariant a property of the strip rather than a claim about who reads what. */ +static int llama_tt_restage_strips(pn_llama_tt *g) +{ + pn_llama_tt_strips *s = &g->card; + const pn_tt_activation *all[] = { &s->hidden[0], &s->hidden[1], &s->normed, &s->q, &s->k, + &s->v, &s->q_rot, &s->k_rot, &s->attn, &s->project, + &s->gate, &s->up, &s->swiglu, &s->logits }; + + for (size_t i = 0; i < sizeof(all) / sizeof(all[0]); i++) + if (pn_tt_activation_zero(g->device, all[i]) != 0) + return -1; + return 0; +} + +tm_tensor *pn_llama_prefill_tt(pn_model *model, const uint32_t *tokens, + const float *const *embd_rows, int n_tokens, pn_state *state) +{ + pn_llama_tt *g; + pn_llama_kv_cache *kv; + tm_tensor *last = NULL; + int rows, min_rows, done; + + if (!model || !state || !model->weights || !model->gpu || n_tokens <= 0 || !tokens) + return NULL; + if (model->forward != pn_llama_forward_tt) + return NULL; + g = (pn_llama_tt *)model->gpu; + if (!g->loaded) + return NULL; + kv = (pn_llama_kv_cache *)state->kv_cache; + if (!kv) + return NULL; + /* REFUSALS, each of which falls back to the caller's per-token loop rather than being + * absorbed. A batch of one is the sequential path and buys nothing. */ + if (n_tokens < 2 || !llama_tt_prefill_enabled()) + return NULL; + /* ON-CARD ATTENTION IS SINGLE-POSITION BY CONSTRUCTION: pn_tt_rope, pn_tt_kv_append and + * the flash decode each take ONE position, so a 32-row pass has no way to express 32 of + * them. Refusing is the honest answer; silently rotating every row to base_pos would be + * a wrong answer with no symptom. */ + if (pn_tt_device_oncard(g->device, PN_TT_ONCARD_ATTENTION)) + return NULL; + /* The trace scores an op against the CPU oracle at Mt=1 and would read row 0 of a + * 32-row strip as if the other 31 were padding. An instrument that silently measured + * something else is worse than not having it here. */ + if (g->trace > 0 || g->attn_check || g->rope_check || g->row_census) + return NULL; + /* SAME REASON, one layer down. PN_TT_MATVEC_DUMP appends logical ROW 0 of every + * projection's output, which under a 32-row pass is the first token of the pass and + * not the op — and the op COUNT changes too, so two dumps taken at different core + * counts would no longer be comparable files. The multi-core byte gate is the thing + * that instrument exists for; batching under it would quietly break that gate. */ + if (getenv("PN_TT_MATVEC_DUMP")) + return NULL; + /* A pass must not wrap the ring: every row of it is live at once, so a wrap would have + * a later row overwrite an earlier row's K/V inside the same pass. */ + if (state->pos + n_tokens > kv->max_seq) + return NULL; + + rows = llama_tt_prefill_rows(); + min_rows = llama_tt_prefill_min_rows(); + if (min_rows > rows) + min_rows = rows; + /* REFUSED BEFORE ANYTHING IS TOUCHED. A prompt with no full-width pass in it would be + * SLOWER batched, so the honest answer is to hand it back rather than to run it. */ + if (n_tokens - 1 < min_rows) + return NULL; + if (llama_tt_ensure_scores(g, kv->max_seq) != 0) + return NULL; + if (llama_tt_alloc_batch(g, &model->config, rows) != 0) + return NULL; + + /* Batch while a pass is still worth its whole-tile transfer; then the TAIL — the last + * token always, plus whatever remainder is under the break-even — goes through the + * ordinary forward. That is also what keeps the head, the top-N and the sparse-logit + * presentation as the shipping code unmodified: no LM head runs in a batched pass at all. */ + done = 0; + while (n_tokens - 1 - done >= min_rows) { + int take = n_tokens - 1 - done; + + if (take > rows) take = rows; + if (llama_tt_prefill_pass(g, model, state, tokens + done, + embd_rows ? embd_rows + done : NULL, take, + state->pos) != 0) + return NULL; + done += take; + } + if (llama_tt_restage_strips(g) != 0) + return NULL; + + for (int i = done; i < n_tokens; i++) { + if (last) tm_free(last); + if (embd_rows && embd_rows[i]) { + state->embd_override = embd_rows[i]; + last = pn_llama_forward_tt(model, 0, state); + state->embd_override = NULL; + } else { + last = pn_llama_forward_tt(model, tokens[i], state); + } + if (!last) + return NULL; + } + return last; +} + +#endif /* USE_TT */ diff --git a/src/models/llama_residual_trace.c b/src/models/llama_residual_trace.c new file mode 100644 index 0000000..6b2b495 --- /dev/null +++ b/src/models/llama_residual_trace.c @@ -0,0 +1,76 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_residual_trace.c + * @brief Implementation of the per-layer residual-stream capture declared in + * llama_residual_trace.h. + * + * @status Working + * @issues None + * @todo None + */ + +#include "llama_residual_trace.h" + +#include +#include + +/** The armed capture, or NULL. One slot: the only caller is a single-threaded harness. */ +static pn_residual_trace *g_armed_trace = NULL; + +int pn_residual_trace_alloc(pn_residual_trace *trace, int n_rows, int width) +{ + if (!trace || n_rows <= 0 || width <= 0) + return -1; + trace->rows = calloc((size_t)n_rows * (size_t)width, sizeof(float)); + if (!trace->rows) + return -1; + trace->n_rows = n_rows; + trace->width = width; + trace->recorded = 0; + return 0; +} + +void pn_residual_trace_free(pn_residual_trace *trace) +{ + if (!trace) + return; + if (g_armed_trace == trace) + g_armed_trace = NULL; + free(trace->rows); + trace->rows = NULL; + trace->n_rows = 0; + trace->width = 0; + trace->recorded = 0; +} + +void pn_residual_trace_capture_into(pn_residual_trace *trace) +{ + g_armed_trace = trace; + if (trace) + trace->recorded = 0; +} + +void pn_residual_trace_record(int row, const float *hidden, int width) +{ + pn_residual_trace *trace = g_armed_trace; + + if (!trace || !hidden || row < 0 || row >= trace->n_rows || width != trace->width) + return; + memcpy(trace->rows + (size_t)row * (size_t)trace->width, hidden, + (size_t)trace->width * sizeof(float)); + if (row + 1 > trace->recorded) + trace->recorded = row + 1; +} + +int pn_residual_trace_armed(void) +{ + return g_armed_trace != NULL; +} + +const float *pn_residual_trace_row(const pn_residual_trace *trace, int row) +{ + if (!trace || !trace->rows || row < 0 || row >= trace->recorded) + return NULL; + return trace->rows + (size_t)row * (size_t)trace->width; +} diff --git a/src/models/llama_residual_trace.h b/src/models/llama_residual_trace.h new file mode 100644 index 0000000..f34300b --- /dev/null +++ b/src/models/llama_residual_trace.h @@ -0,0 +1,73 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_residual_trace.h + * @brief Capture the residual stream layer by layer out of ANY llama forward, so two + * implementations can be compared on their ACCUMULATED STATE rather than on the + * local error of each op. + * + * The per-op trace in llama_forward_tt.c feeds the card's own input to the CPU + * reference and scores each op in isolation. That measures per-op error and is blind + * to the divergence of the stream those ops write into: the ChatML defect shows the + * same per-op error as plain prose (1.06x) with five times the end-to-end logit + * divergence, which is only visible here. + * + * @status Working + * @issues None + * @todo None + */ +#ifndef PN_LLAMA_RESIDUAL_TRACE_H +#define PN_LLAMA_RESIDUAL_TRACE_H + +/** + * One forward's residual stream. Row L (0 <= L < n_layer) is the hidden state AFTER layer + * L's FFN residual add; row n_layer is the final normed vector the LM head reads. + */ +typedef struct { + float *rows; /**< n_rows * width, owned */ + int n_rows; + int width; + int recorded; /**< how many rows the forward now running has written */ +} pn_residual_trace; + +/** + * Allocate a capture buffer. + * + * @param n_rows layers + 1 — the extra row is the final normed vector + * @param width n_embd + * @return 0 on success, -1 on allocation failure + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +int pn_residual_trace_alloc(pn_residual_trace *trace, int n_rows, int width); + +void pn_residual_trace_free(pn_residual_trace *trace); + +/** + * Direct the next forward's per-layer records into `trace`, or NULL to stop capturing. + * Resets `trace->recorded` so a capture is always one forward's worth. + * + * Deliberately a single process-wide slot rather than a parameter on the forward: the + * forward signature is the engine's public contract and a debug instrument does not get to + * change it. The one caller is a debug harness that runs one forward at a time. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_residual_trace_capture_into(pn_residual_trace *trace); + +/** + * Record one row. Called by a forward implementation; a no-op — one predictable + * branch on a global — when no capture is armed, which is every production call. + * + * @param row row index; ignored if outside the armed buffer + * @param hidden the vector to copy, `width` floats + */ +void pn_residual_trace_record(int row, const float *hidden, int width); + +/** @return whether a capture is armed, so a forward can skip work it only does to record. */ +int pn_residual_trace_armed(void); + +/** @return row `row` of a completed capture, or NULL if it was never written. */ +const float *pn_residual_trace_row(const pn_residual_trace *trace, int row); + +#endif /* PN_LLAMA_RESIDUAL_TRACE_H */ diff --git a/src/models/llama_state_snapshot.c b/src/models/llama_state_snapshot.c new file mode 100644 index 0000000..95a282a --- /dev/null +++ b/src/models/llama_state_snapshot.c @@ -0,0 +1,462 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file llama_state_snapshot.c + * @brief Host-side snapshot/restore of a LLAMA-FAMILY KV cache at a token + * position — the session export/restore primitive for every architecture + * whose entire per-session state is pure causal attention (llama, qwen2, + * qwen3, phi, mistral, mixtral). It is the counterpart of + * qwen35moe_state_snapshot.c, which carries a recurrent SSM state as + * well; this family has none, so a snapshot is exactly the KV rows + * [0, pos) of every layer plus the dims needed to validate a restore. + * + * This is the family the Tenstorrent Blackhole backend serves, and THERE + * THE CACHE MAY BE IN ONE OF TWO PLACES. With attention on the host it is + * host memory (`tm_zeros(..., TM_F32)`, see llama.c) and a snapshot is a + * memcpy that touches no device at all. With `PN_TT_ONCARD=...,attention` + * it is CARD DRAM in bf16, the host tensors are never written, and a + * snapshot that read them would export a page of zeros and call it a + * session. `pn_llama_kv_cache::tt_device` is which, and `kv_layer_move()` + * is the one place that asks. + * + * Exact restore is still reachable here and still is not on CUDA (see + * prefix_cache.h @issues, where the D2H mirror is not byte-faithful to + * the live ring) — because on the card the ROUND TRIP is exact: bf16 is a + * strict subset of fp32, so widening out and narrowing back loses nothing. + * + * @status Complete: capture/restore and serialize/deserialize, unit-tested + * off-hardware (tests/test_llama_state_snapshot.c) and end-to-end + * through `pantheon serve` on the card, on both the host-cache and the + * card-cache arm. + * @issues None + * @todo None + */ + +#include "llama.h" + +#include +#include +#include + +/* One layer's captured KV: rows [0, pos) per head, packed [n_kv_head][pos] + * [head_dim] (the live cache strides by max_seq). fp32 throughout — this family + * has no quantized-KV mode. */ +typedef struct { + float *k_rows; + float *v_rows; +} llama_snap_layer; + +/* Whole-cache snapshot at one position (opaque to callers). */ +typedef struct { + int pos; + int n_layers; + int n_kv_head; + int head_dim; + int max_seq; /* the SOURCE cache's ring length (diagnostic) */ + llama_snap_layer *lay; +} llama_kv_snap; + +/* Elements per layer in the packed representation. */ +static size_t snap_layer_elems(const llama_kv_snap *sn) +{ + return (size_t)sn->n_kv_head * (size_t)sn->pos * (size_t)sn->head_dim; +} + +/* Copy `pos` rows per head between packed ([pos] stride) and live ([max_seq] + * stride) layouts. dir_capture != 0: live -> packed; else packed -> live. */ +static void kv_rows_copy(float *packed, float *live, int n_kv_head, int max_seq, + int pos, int head_dim, int dir_capture) +{ + for (int h = 0; h < n_kv_head; h++) { + float *p = packed + (size_t)h * (size_t)pos * (size_t)head_dim; + float *l = live + (size_t)h * (size_t)max_seq * (size_t)head_dim; + size_t n = (size_t)pos * (size_t)head_dim * sizeof(float); + if (dir_capture) + memcpy(p, l, n); + else + memcpy(l, p, n); + } +} + +/* ============================================================ + * WHERE THE ROWS COME FROM — host memory, or the card + * ============================================================ + * + * The trade was decided in pn_tt_device.h and the arithmetic is not close. A host MIRROR — + * downloading k and v every layer to keep the fp32 tensors current — costs 49,152 B a token + * of ROW-0 reads EVERY token, because a 64-byte card->host read alignment makes row 0 cost + * twice its payload. Downloading the whole cache once at export reads WHOLE tiles with no + * waste: 24,576 B a position. **The mirror therefore moves exactly 2x the bytes of the export + * it would make free, and it charges every session including the ones nobody exports.** + * Export is an event; decode is a loop; the cost goes on the event. + * + * Measured consequence at 4.7 MB/s card->host: ~10.2 s to export a 2048-token session, and + * ~0.18 s to restore one (host->card is 287 MB/s, ~61x faster). + * + * **THE BLOB FORMAT DOES NOT CHANGE, deliberately.** The card holds bf16, so an fp32 payload + * stores twice the bytes the state actually has — but bf16 is a strict subset of fp32, so + * widening out and narrowing back is EXACT, and holding PN_LSNAP_FORMAT_VERSION at 1 means + * every blob a client already holds still restores. Halving a file that lives on the user's + * machine is not worth forking the format for. + */ + +/* Move one layer's K and V between wherever they live and the packed [head][pos][dim] + * arrays. `dir_capture` non-zero is live -> packed; zero is packed -> live. + * + * Returns 0, or -1 if a device transfer failed — which must NOT be absorbed. A snapshot that + * quietly keeps whatever the buffer held is the zero-page export in a different disguise. */ +static int kv_layer_move(const pn_llama_kv_cache *cache, int layer, float *k_packed, + float *v_packed, int n_kv_head, int max_seq, int pos, int head_dim, + int dir_capture) +{ + if (cache->kv_move) { + if (cache->kv_move(cache->kv_device, layer, 0, pos, k_packed, dir_capture) != 0) + return -1; + return cache->kv_move(cache->kv_device, layer, 1, pos, v_packed, dir_capture) == 0 + ? 0 : -1; + } + kv_rows_copy(k_packed, (float *)cache->k_cache[layer]->data, n_kv_head, max_seq, pos, + head_dim, dir_capture); + kv_rows_copy(v_packed, (float *)cache->v_cache[layer]->data, n_kv_head, max_seq, pos, + head_dim, dir_capture); + return 0; +} + +/* Read the cache geometry out of layer 0's tensors and check every other layer + * agrees. Returns 0 on success. A cache whose layers disagree is not a shape + * this format can describe, and silently snapshotting layer 0's dims for all of + * them would corrupt the restore. */ +static int kv_cache_geometry(const pn_llama_kv_cache *cache, int *n_kv_head, + int *max_seq, int *head_dim) +{ + if (!cache || cache->n_layers <= 0 || !cache->k_cache || !cache->v_cache) + return -1; + const tm_tensor *k0 = cache->k_cache[0]; + if (!k0 || k0->ndim != 3) + return -1; + *n_kv_head = (int)k0->shape[0]; + *max_seq = (int)k0->shape[1]; + *head_dim = (int)k0->shape[2]; + if (*n_kv_head <= 0 || *max_seq <= 0 || *head_dim <= 0) + return -1; + for (int i = 0; i < cache->n_layers; i++) { + const tm_tensor *k = cache->k_cache[i]; + const tm_tensor *v = cache->v_cache[i]; + if (!k || !v || !k->data || !v->data || k->ndim != 3 || v->ndim != 3) + return -1; + if (k->shape[0] != *n_kv_head || k->shape[1] != *max_seq || + k->shape[2] != *head_dim || + v->shape[0] != *n_kv_head || v->shape[1] != *max_seq || + v->shape[2] != *head_dim) + return -1; + } + return 0; +} + +void pn_llama_kv_snapshot_free(void *snap_v) +{ + llama_kv_snap *sn = (llama_kv_snap *)snap_v; + if (!sn) + return; + if (sn->lay) { + for (int i = 0; i < sn->n_layers; i++) { + free(sn->lay[i].k_rows); + free(sn->lay[i].v_rows); + } + free(sn->lay); + } + free(sn); +} + +void *pn_llama_kv_snapshot(const void *cache_v, int pos) +{ + const pn_llama_kv_cache *cache = (const pn_llama_kv_cache *)cache_v; + int n_kv_head = 0, max_seq = 0, head_dim = 0; + + if (pos <= 0) + return NULL; + if (kv_cache_geometry(cache, &n_kv_head, &max_seq, &head_dim) != 0) + return NULL; + /* The live cache is a RING (`cache_pos = pos % max_seq` in every forward), + * so once pos exceeds max_seq the rows [0, pos) no longer exist anywhere — + * they were overwritten. Refuse rather than export a wrapped cache as if it + * were linear. */ + if (pos > max_seq) + return NULL; + + llama_kv_snap *sn = calloc(1, sizeof(*sn)); + if (!sn) + return NULL; + sn->pos = pos; + sn->n_layers = cache->n_layers; + sn->n_kv_head = n_kv_head; + sn->head_dim = head_dim; + sn->max_seq = max_seq; + sn->lay = calloc((size_t)cache->n_layers, sizeof(*sn->lay)); + if (!sn->lay) + goto fail; + + size_t elems = snap_layer_elems(sn); + for (int i = 0; i < cache->n_layers; i++) { + sn->lay[i].k_rows = malloc(elems * sizeof(float)); + sn->lay[i].v_rows = malloc(elems * sizeof(float)); + if (!sn->lay[i].k_rows || !sn->lay[i].v_rows) + goto fail; + if (kv_layer_move(cache, i, sn->lay[i].k_rows, sn->lay[i].v_rows, n_kv_head, + max_seq, pos, head_dim, 1) != 0) + goto fail; + } + return sn; + +fail: + pn_llama_kv_snapshot_free(sn); + return NULL; +} + +int pn_llama_kv_restore(void *cache_v, const void *snap_v) +{ + pn_llama_kv_cache *cache = (pn_llama_kv_cache *)cache_v; + const llama_kv_snap *sn = (const llama_kv_snap *)snap_v; + int n_kv_head = 0, max_seq = 0, head_dim = 0; + + if (!sn) + return -1; + if (kv_cache_geometry(cache, &n_kv_head, &max_seq, &head_dim) != 0) + return -1; + if (sn->n_layers != cache->n_layers || sn->n_kv_head != n_kv_head || + sn->head_dim != head_dim) + return -1; + /* pos is attacker-controlled (it arrives inside a client-held blob), so + * bound it against THIS cache's ring rather than the one it was captured + * on. max_seq itself is deliberately NOT required to match: the rows are + * repacked on the way in, so a cache configured with a longer ring accepts + * a shorter capture. */ + if (sn->pos <= 0 || sn->pos > max_seq) + return -1; + + for (int i = 0; i < cache->n_layers; i++) { + if (kv_layer_move(cache, i, sn->lay[i].k_rows, sn->lay[i].v_rows, n_kv_head, max_seq, + sn->pos, head_dim, 0) != 0) + return -1; + } + return 0; +} + +int pn_llama_kv_snapshot_pos(const void *snap_v) +{ + const llama_kv_snap *sn = (const llama_kv_snap *)snap_v; + return sn ? sn->pos : -1; +} + +/* ============================================================ + * Serialize / deserialize — the session warm-tier blob + * + * One contiguous little-endian blob, framed exactly like the qwen35moe one + * (put_u32/put_u64 write LE regardless of host endianness) so the two are + * distinguishable by MAGIC alone and never by length or by luck. Layout: + * + * [ header, 36 bytes ] + * u32 magic = PN_LSNAP_MAGIC ("PNLK" as LE bytes) + * u32 format_version = PN_LSNAP_FORMAT_VERSION + * u64 weights_version (opaque model-identity tag; validated by caller) + * i32 pos + * i32 n_layers + * i32 n_kv_head + * i32 head_dim + * i32 max_seq (source ring length; diagnostic, see restore) + * [ n_layers × ] + * f32 k_rows[n_kv_head * pos * head_dim] + * f32 v_rows[...] + * + * f32 payloads are copied as raw bytes: this fleet and every client on it are + * little-endian IEEE-754, so no per-element swap is done. The framing fields + * ARE explicitly LE, and format_version gates any future change. + * ============================================================ */ + +#define PN_LSNAP_MAGIC 0x4B4C4E50u /* 'P''N''L''K' little-endian */ +#define PN_LSNAP_FORMAT_VERSION 1u +#define PN_LSNAP_HEADER_BYTES 36u + +/* Bounded cursor over a byte buffer (write or read). `err` latches on the first + * overflow so callers check it once at the end instead of per-field. */ +typedef struct { + unsigned char *buf; /* NULL in the sizing pass (writer) */ + size_t cap; + size_t off; + int err; +} lsnap_cursor; + +static void put_bytes(lsnap_cursor *c, const void *src, size_t n) +{ + if (c->err) + return; + if (c->off + n > c->cap) { c->err = 1; return; } + if (c->buf && n) + memcpy(c->buf + c->off, src, n); + c->off += n; +} + +static void put_u32(lsnap_cursor *c, uint32_t v) +{ + unsigned char b[4] = { (unsigned char)(v), (unsigned char)(v >> 8), + (unsigned char)(v >> 16), (unsigned char)(v >> 24) }; + put_bytes(c, b, 4); +} + +static void put_i32(lsnap_cursor *c, int32_t v) { put_u32(c, (uint32_t)v); } + +static void put_u64(lsnap_cursor *c, uint64_t v) +{ + unsigned char b[8]; + for (int i = 0; i < 8; i++) b[i] = (unsigned char)(v >> (8 * i)); + put_bytes(c, b, 8); +} + +static void get_bytes(lsnap_cursor *c, void *dst, size_t n) +{ + if (c->err) + return; + if (c->off + n > c->cap) { c->err = 1; return; } + if (n) + memcpy(dst, c->buf + c->off, n); + c->off += n; +} + +static uint32_t get_u32(lsnap_cursor *c) +{ + unsigned char b[4] = {0}; + get_bytes(c, b, 4); + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static int32_t get_i32(lsnap_cursor *c) { return (int32_t)get_u32(c); } + +static uint64_t get_u64(lsnap_cursor *c) +{ + unsigned char b[8] = {0}; + get_bytes(c, b, 8); + uint64_t v = 0; + for (int i = 0; i < 8; i++) v |= (uint64_t)b[i] << (8 * i); + return v; +} + +/* Walk the whole snapshot through a cursor: write (buf != NULL) or size-only + * (buf == NULL, cap = SIZE_MAX). ONE walk shared by both passes so the sizing + * pass and the write pass can never disagree on the layout. */ +static void lsnap_walk_write(lsnap_cursor *c, const llama_kv_snap *sn, + uint64_t weights_version) +{ + put_u32(c, PN_LSNAP_MAGIC); + put_u32(c, PN_LSNAP_FORMAT_VERSION); + put_u64(c, weights_version); + put_i32(c, sn->pos); + put_i32(c, sn->n_layers); + put_i32(c, sn->n_kv_head); + put_i32(c, sn->head_dim); + put_i32(c, sn->max_seq); + + size_t rows_b = snap_layer_elems(sn) * sizeof(float); + for (int i = 0; i < sn->n_layers; i++) { + put_bytes(c, sn->lay[i].k_rows, rows_b); + put_bytes(c, sn->lay[i].v_rows, rows_b); + } +} + +int pn_llama_kv_snap_serialize(const void *snap_v, uint64_t weights_version, + unsigned char **bytes_out, size_t *len_out) +{ + const llama_kv_snap *sn = (const llama_kv_snap *)snap_v; + if (!sn || !bytes_out || !len_out) + return -1; + *bytes_out = NULL; + *len_out = 0; + + /* Pass 1: size (buf == NULL, unbounded cap). */ + lsnap_cursor sz = { .buf = NULL, .cap = SIZE_MAX, .off = 0, .err = 0 }; + lsnap_walk_write(&sz, sn, weights_version); + if (sz.err) + return -1; + + unsigned char *buf = malloc(sz.off ? sz.off : 1); + if (!buf) + return -1; + + /* Pass 2: write into the exact-size buffer. */ + lsnap_cursor wr = { .buf = buf, .cap = sz.off, .off = 0, .err = 0 }; + lsnap_walk_write(&wr, sn, weights_version); + if (wr.err || wr.off != sz.off) { + free(buf); + return -1; + } + *bytes_out = buf; + *len_out = sz.off; + return 0; +} + +void *pn_llama_kv_snap_deserialize(const unsigned char *bytes, size_t len, + uint64_t *weights_version_out) +{ + if (weights_version_out) + *weights_version_out = 0; + if (!bytes || len < PN_LSNAP_HEADER_BYTES) + return NULL; + + lsnap_cursor c = { .buf = (unsigned char *)bytes, .cap = len, + .off = 0, .err = 0 }; + if (get_u32(&c) != PN_LSNAP_MAGIC) + return NULL; + if (get_u32(&c) != PN_LSNAP_FORMAT_VERSION) + return NULL; + uint64_t wv = get_u64(&c); + if (weights_version_out) + *weights_version_out = wv; + int pos = get_i32(&c); + int n_layers = get_i32(&c); + int n_kv_head = get_i32(&c); + int head_dim = get_i32(&c); + int max_seq = get_i32(&c); + if (c.err || pos <= 0 || n_layers <= 0 || n_kv_head <= 0 || head_dim <= 0 || + max_seq <= 0 || pos > max_seq) + return NULL; + + llama_kv_snap *sn = calloc(1, sizeof(*sn)); + if (!sn) + return NULL; + sn->pos = pos; + sn->n_layers = n_layers; + sn->n_kv_head = n_kv_head; + sn->head_dim = head_dim; + sn->max_seq = max_seq; + sn->lay = calloc((size_t)n_layers, sizeof(*sn->lay)); + if (!sn->lay) + goto fail; + + /* Guard the element product against size_t overflow before it becomes a + * malloc size: a wrap to a small value would under-allocate the rows. */ + size_t elems = snap_layer_elems(sn); + if ((size_t)pos > SIZE_MAX / (size_t)n_kv_head / (size_t)head_dim || + elems > SIZE_MAX / sizeof(float)) + goto fail; + size_t rows_b = elems * sizeof(float); + + for (int i = 0; i < n_layers; i++) { + sn->lay[i].k_rows = malloc(rows_b); + sn->lay[i].v_rows = malloc(rows_b); + if (!sn->lay[i].k_rows || !sn->lay[i].v_rows) + goto fail; + get_bytes(&c, sn->lay[i].k_rows, rows_b); + get_bytes(&c, sn->lay[i].v_rows, rows_b); + if (c.err) + goto fail; + } + + /* Reject trailing garbage: a valid blob is consumed exactly. */ + if (c.off != len) + goto fail; + return sn; + +fail: + pn_llama_kv_snapshot_free(sn); + return NULL; +} diff --git a/src/models/phi3.c b/src/models/phi3.c new file mode 100644 index 0000000..c8a10e7 --- /dev/null +++ b/src/models/phi3.c @@ -0,0 +1,486 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#include "phi3.h" +#include "../core/engine.h" +#include "../core/qweight.h" +#include "../memory/arena.h" +#include "../accel/simd.h" +#include "../formats/gguf_dequant.h" +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif +#include +#include +#include +#include + +/* ============================================================ + * Config reading from GGUF (self-contained, uses "phi3" prefix) + * ============================================================ */ + +static int phi3_read_config(pn_gguf_file *gguf, pn_config *config) { + const char *prefix = "phi3"; + char key[128]; + + snprintf(key, sizeof(key), "%s.vocab_size", prefix); + config->n_vocab = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.embedding_length", prefix); + config->n_embd = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.head_count", prefix); + config->n_head = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.head_count_kv", prefix); + config->n_kv_head = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.block_count", prefix); + config->n_layer = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.feed_forward_length", prefix); + config->n_ff = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.context_length", prefix); + config->n_ctx = (int)pn_gguf_get_u32(gguf, key, 2048); + snprintf(key, sizeof(key), "%s.rope.freq_base", prefix); + config->rope_theta = pn_gguf_get_f32(gguf, key, 10000.0f); + snprintf(key, sizeof(key), "%s.attention.layer_norm_rms_epsilon", prefix); + config->rms_norm_eps = pn_gguf_get_f32(gguf, key, 1e-5f); + snprintf(key, sizeof(key), "%s.attention.sliding_window", prefix); + config->sliding_window = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.rope.dimension_count", prefix); + config->rope_dim = (int)pn_gguf_get_u32(gguf, key, 0); + + if (config->n_vocab == 0) { + pn_gguf_tensor_info *embd = pn_gguf_find_tensor(gguf, "token_embd.weight"); + if (embd && embd->n_dims >= 2) { + config->n_vocab = (int)embd->dims[embd->n_dims - 1]; + } + } + + if (config->n_embd == 0 || config->n_head == 0 || config->n_layer == 0) { + fprintf(stderr, "phi3: invalid config (n_embd=%d, n_head=%d, n_layer=%d)\n", + config->n_embd, config->n_head, config->n_layer); + return -1; + } + + if (config->n_kv_head == 0) config->n_kv_head = config->n_head; + + fprintf(stderr, "phi3: n_vocab=%d n_embd=%d n_head=%d n_kv_head=%d n_layer=%d n_ff=%d rope_dim=%d\n", + config->n_vocab, config->n_embd, config->n_head, + config->n_kv_head, config->n_layer, config->n_ff, config->rope_dim); + return 0; +} + +/* ============================================================ + * Helper: dequantize a GGUF tensor to F32 float array (owned) + * ============================================================ */ + +static float* phi3_dequant_to_f32(pn_gguf_file *gguf, const char *name, size_t *numel_out) { + return pn_gguf_dequantize_tensor(gguf, name, numel_out); +} + +/* ============================================================ + * Check if a quant type is supported for direct qmv + * ============================================================ */ + +static int phi3_is_supported_quant(pn_gguf_quant type) { + switch (type) { + case GGUF_QUANT_F32: + case GGUF_QUANT_F16: + case GGUF_QUANT_Q4_0: + case GGUF_QUANT_Q8_0: + case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: + case GGUF_QUANT_Q6_K: + return 1; + default: + return 0; + } +} + +/* ============================================================ + * Quantized weight loading from GGUF + * ============================================================ */ + +pn_phi3_qweights* pn_phi3_load_gguf_quantized(pn_gguf_file *gguf, pn_config *config) { + if (!gguf || !config) return NULL; + + if (phi3_read_config(gguf, config) < 0) return NULL; + + /* Check that the first layer's fused QKV weight type is supported */ + pn_gguf_tensor_info *test_info = pn_gguf_find_tensor(gguf, "blk.0.attn_qkv.weight"); + if (!test_info || !phi3_is_supported_quant(test_info->type)) { + fprintf(stderr, "phi3: quant type %d not supported for quantized path\n", + test_info ? test_info->type : -1); + return NULL; + } + + pn_phi3_qweights *qw = calloc(1, sizeof(pn_phi3_qweights)); + if (!qw) return NULL; + qw->is_quantized = 1; + qw->n_layers = config->n_layer; + qw->n_embd = config->n_embd; + qw->n_vocab = config->n_vocab; + qw->gguf = gguf; + + /* Token embeddings: always dequant to F32 */ + size_t embd_numel = 0; + qw->tok_embd = phi3_dequant_to_f32(gguf, "token_embd.weight", &embd_numel); + if (!qw->tok_embd) { + fprintf(stderr, "phi3: failed to load token_embd.weight\n"); + pn_phi3_qweights_free(qw); + return NULL; + } + + /* Detect actual vocab size from embedding */ + if (config->n_vocab == 0 && embd_numel > 0 && config->n_embd > 0) { + config->n_vocab = (int)(embd_numel / (size_t)config->n_embd); + qw->n_vocab = config->n_vocab; + } + + /* Output norm: always F32 */ + size_t norm_numel = 0; + qw->output_norm = phi3_dequant_to_f32(gguf, "output_norm.weight", &norm_numel); + if (!qw->output_norm) { + fprintf(stderr, "phi3: failed to load output_norm.weight\n"); + pn_phi3_qweights_free(qw); + return NULL; + } + + /* Output head: try as qweight, fall back to tied embeddings */ + pn_gguf_tensor_info *out_info = pn_gguf_find_tensor(gguf, "output.weight"); + if (out_info && phi3_is_supported_quant(out_info->type)) { + if (pn_qweight_from_gguf(&qw->output, gguf, "output.weight") < 0) { + pn_phi3_qweights_free(qw); + return NULL; + } + } else { + /* Tied embeddings: use tok_embd as F32 output head */ + qw->output_f32 = malloc(embd_numel * sizeof(float)); + if (!qw->output_f32) { + pn_phi3_qweights_free(qw); + return NULL; + } + memcpy(qw->output_f32, qw->tok_embd, embd_numel * sizeof(float)); + qw->output.data = qw->output_f32; + qw->output.type = GGUF_QUANT_F32; + qw->output.rows = config->n_vocab; + qw->output.cols = config->n_embd; + qw->output.row_stride = (size_t)config->n_embd * sizeof(float); + } + + /* Per-layer weights */ + qw->layers = calloc(config->n_layer, sizeof(pn_phi3_qlayer)); + if (!qw->layers) { + pn_phi3_qweights_free(qw); + return NULL; + } + + for (int i = 0; i < config->n_layer; i++) { + char name[128]; + pn_phi3_qlayer *l = &qw->layers[i]; + + /* Norms: always dequant to F32 */ + snprintf(name, sizeof(name), "blk.%d.attn_norm.weight", i); + l->attn_norm = phi3_dequant_to_f32(gguf, name, NULL); + if (!l->attn_norm) { + fprintf(stderr, "phi3: failed to load %s\n", name); + pn_phi3_qweights_free(qw); + return NULL; + } + + snprintf(name, sizeof(name), "blk.%d.ffn_norm.weight", i); + l->ffn_norm = phi3_dequant_to_f32(gguf, name, NULL); + if (!l->ffn_norm) { + fprintf(stderr, "phi3: failed to load %s\n", name); + pn_phi3_qweights_free(qw); + return NULL; + } + + /* Projection weights: keep quantized in mmap */ + #define LOAD_QW(field, fmt) do { \ + snprintf(name, sizeof(name), fmt, i); \ + if (pn_qweight_from_gguf(&l->field, gguf, name) < 0) { \ + fprintf(stderr, "phi3: failed to load %s\n", name); \ + pn_phi3_qweights_free(qw); \ + return NULL; \ + } \ + } while(0) + + LOAD_QW(attn_qkv, "blk.%d.attn_qkv.weight"); + LOAD_QW(attn_output, "blk.%d.attn_output.weight"); + LOAD_QW(ffn_gate_up, "blk.%d.ffn_up.weight"); + LOAD_QW(ffn_down, "blk.%d.ffn_down.weight"); + + #undef LOAD_QW + + fprintf(stderr, "\r loaded layer %d/%d", i + 1, config->n_layer); + fflush(stderr); + } + fprintf(stderr, "\n"); + + return qw; +} + +/* ============================================================ + * Free + * ============================================================ */ + +void pn_phi3_qweights_free(pn_phi3_qweights *qw) { + if (!qw) return; + free(qw->tok_embd); + free(qw->output_norm); + free(qw->output_f32); + + if (qw->layers) { + for (int i = 0; i < qw->n_layers; i++) { + free(qw->layers[i].attn_norm); + free(qw->layers[i].ffn_norm); + } + free(qw->layers); + } + free(qw); +} + +/* ============================================================ + * RoPE (Rotary Position Embeddings) — NeoX style, in-place + * + * NeoX-style RoPE pairs elements offset by half the rotation + * dimension: pair i = (head[i], head[i + rot/2]). + * This matches HuggingFace's rotate_half() convention used by + * Phi-3 (and Qwen2, Falcon, etc.), where the GGUF weights are + * stored WITHOUT the interleaving permutation. + * ============================================================ */ + +static void phi3_apply_rope(float *data, int n_heads, int head_dim, + int rope_dim, int pos, float theta_base) { + int rot = (rope_dim > 0 && rope_dim <= head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + h * head_dim; + for (int i = 0; i < half; i++) { + float freq = 1.0f / powf(theta_base, (float)(2 * i) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle); + float sin_a = sinf(angle); + + float x0 = head[i]; + float x1 = head[i + half]; + head[i] = x0 * cos_a - x1 * sin_a; + head[i + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* ============================================================ + * Attention head computation — for threading + * ============================================================ */ + +typedef struct { + const float *q_data; + const float *k_cache_data; + const float *v_cache_data; + float *attn_out; + float *scores_buf; + int head_dim; + int n_kv_groups; + int max_seq; + int seq_len; + float scale; +} phi3_attn_args; + +static void phi3_compute_attn_heads(int start, int end, void *arg) { + phi3_attn_args *a = (phi3_attn_args *)arg; + + for (int qh = start; qh < end; qh++) { + int kv_h = qh / a->n_kv_groups; + const float *q_vec = a->q_data + qh * a->head_dim; + const float *k_base = a->k_cache_data + kv_h * a->max_seq * a->head_dim; + const float *v_base = a->v_cache_data + kv_h * a->max_seq * a->head_dim; + float *scores = a->scores_buf + qh * a->max_seq; + float *out_vec = a->attn_out + qh * a->head_dim; + + for (int s = 0; s < a->seq_len; s++) { + scores[s] = pn_dot_f32(q_vec, k_base + s * a->head_dim, a->head_dim) * a->scale; + } + + float max_score = scores[0]; + for (int s = 1; s < a->seq_len; s++) { + if (scores[s] > max_score) max_score = scores[s]; + } + float sum_exp = 0.0f; + for (int s = 0; s < a->seq_len; s++) { + scores[s] = expf(scores[s] - max_score); + sum_exp += scores[s]; + } + float inv_sum = 1.0f / sum_exp; + for (int s = 0; s < a->seq_len; s++) { + scores[s] *= inv_sum; + } + + memset(out_vec, 0, a->head_dim * sizeof(float)); + for (int s = 0; s < a->seq_len; s++) { + float w_s = scores[s]; + const float *v_vec = v_base + s * a->head_dim; + for (int d = 0; d < a->head_dim; d++) { + out_vec[d] += w_s * v_vec[d]; + } + } + } +} + +/* ============================================================ + * Forward pass — quantized weights + arena + * + * Key differences from Llama: + * 1. Single fused QKV matvec, then pointer split + * 2. Single fused gate+up matvec, then pointer split + * ============================================================ */ + +tm_tensor* pn_phi3_forward(pn_model *model, uint32_t token, pn_state *state) { + if (!model || !state || !model->weights) return NULL; + + pn_phi3_qweights *qw = (pn_phi3_qweights *)model->weights; + pn_llama_kv_cache *kv = (pn_llama_kv_cache *)state->kv_cache; + pn_config *cfg = &model->config; + pn_arena *arena = state->arena; + int pos = state->pos; + int n_embd = cfg->n_embd; + int head_dim = n_embd / cfg->n_head; + int n_kv_groups = cfg->n_head / cfg->n_kv_head; + int max_seq = kv->max_seq; + +#ifdef USE_THREADS + fw_pool *pool = model->pool; +#endif + + if (!arena) return NULL; + + pn_arena_reset(arena); + + /* 1. Token embedding lookup */ + float *hidden = pn_arena_alloc_f32(arena, n_embd); + if (!hidden) return NULL; + memcpy(hidden, qw->tok_embd + (size_t)token * n_embd, n_embd * sizeof(float)); + + /* Pre-allocate attention scores buffer: [n_head, max_seq] */ + float *scores_buf = pn_arena_alloc_f32(arena, cfg->n_head * max_seq); + if (!scores_buf) return NULL; + + /* 2. Transformer layers */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + pn_phi3_qlayer *l = &qw->layers[layer]; + + /* === Attention sublayer === */ + + /* RMSNorm */ + float *normed = pn_arena_alloc_f32(arena, n_embd); + if (!normed) return NULL; + pn_vrms_norm_f32(hidden, l->attn_norm, normed, n_embd, cfg->rms_norm_eps); + + /* Fused QKV projection: single matvec, then pointer split */ + float *qkv = pn_arena_alloc_f32(arena, l->attn_qkv.rows); /* 3*n_embd */ + if (!qkv) return NULL; + pn_qmv(&l->attn_qkv, normed, qkv); + + float *q = qkv; /* [0 : n_embd] */ + float *k = qkv + n_embd; /* [n_embd : 2*n_embd] */ + float *v = qkv + 2 * n_embd; /* [2*n_embd : 3*n_embd] */ + + /* Apply RoPE to Q and K */ + phi3_apply_rope(q, cfg->n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + phi3_apply_rope(k, cfg->n_kv_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + + /* Update KV cache */ + float *k_cache_data = (float *)kv->k_cache[layer]->data; + float *v_cache_data = (float *)kv->v_cache[layer]->data; + int cache_pos = pos % max_seq; + + for (int h = 0; h < cfg->n_kv_head; h++) { + memcpy(k_cache_data + h * max_seq * head_dim + cache_pos * head_dim, + k + h * head_dim, + head_dim * sizeof(float)); + memcpy(v_cache_data + h * max_seq * head_dim + cache_pos * head_dim, + v + h * head_dim, + head_dim * sizeof(float)); + } + + /* Compute attention */ + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + float *attn_out = pn_arena_alloc_f32(arena, cfg->n_head * head_dim); + if (!attn_out) return NULL; + + phi3_attn_args args = { + .q_data = q, + .k_cache_data = k_cache_data, + .v_cache_data = v_cache_data, + .attn_out = attn_out, + .scores_buf = scores_buf, + .head_dim = head_dim, + .n_kv_groups = n_kv_groups, + .max_seq = max_seq, + .seq_len = seq_len, + .scale = 1.0f / sqrtf((float)head_dim), + }; + +#ifdef USE_THREADS + if (pool && cfg->n_head >= 4) { + fw_for(pool, cfg->n_head, phi3_compute_attn_heads, &args); + } else { + phi3_compute_attn_heads(0, cfg->n_head, &args); + } +#else + phi3_compute_attn_heads(0, cfg->n_head, &args); +#endif + + /* Output projection */ + float *attn_proj = pn_arena_alloc_f32(arena, n_embd); + if (!attn_proj) return NULL; + pn_qmv(&l->attn_output, attn_out, attn_proj); + + /* Residual connection */ + pn_vadd_f32(hidden, attn_proj, hidden, n_embd); + + /* === FFN sublayer === */ + + /* RMSNorm */ + float *ffn_normed = pn_arena_alloc_f32(arena, n_embd); + if (!ffn_normed) return NULL; + pn_vrms_norm_f32(hidden, l->ffn_norm, ffn_normed, n_embd, cfg->rms_norm_eps); + + /* Fused gate+up projection: single matvec, then pointer split */ + float *gate_up = pn_arena_alloc_f32(arena, l->ffn_gate_up.rows); /* 2*n_ff */ + if (!gate_up) return NULL; + pn_qmv(&l->ffn_gate_up, ffn_normed, gate_up); + + float *gate = gate_up; /* [0 : n_ff] */ + float *up = gate_up + cfg->n_ff; /* [n_ff : 2*n_ff] */ + + /* SiLU(gate) * up */ + float *ffn_hidden = pn_arena_alloc_f32(arena, cfg->n_ff); + if (!ffn_hidden) return NULL; + pn_vsilu_mul_f32(gate, up, ffn_hidden, cfg->n_ff); + + /* Down projection */ + float *ffn_out = pn_arena_alloc_f32(arena, n_embd); + if (!ffn_out) return NULL; + pn_qmv(&l->ffn_down, ffn_hidden, ffn_out); + + /* Residual */ + pn_vadd_f32(hidden, ffn_out, hidden, n_embd); + } + + /* 3. Final RMSNorm */ + float *final_normed = pn_arena_alloc_f32(arena, n_embd); + if (!final_normed) return NULL; + pn_vrms_norm_f32(hidden, qw->output_norm, final_normed, n_embd, cfg->rms_norm_eps); + + /* 4. LM head: output @ normed -> logits */ + float *logits_raw = pn_arena_alloc_f32(arena, cfg->n_vocab); + if (!logits_raw) return NULL; + pn_qmv(&qw->output, final_normed, logits_raw); + + /* Create output tm_tensor (only allocation that escapes the arena) */ + int64_t logits_shape[] = { cfg->n_vocab }; + tm_tensor *logits_t = tm_from_data(logits_raw, logits_shape, 1, TM_F32); + tm_tensor *logits = logits_t ? tm_clone(logits_t) : NULL; + if (logits_t) tm_free(logits_t); + + state->pos++; + return logits; +} diff --git a/src/models/phi3.h b/src/models/phi3.h new file mode 100644 index 0000000..104caf2 --- /dev/null +++ b/src/models/phi3.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +#ifndef PN_PHI3_H +#define PN_PHI3_H + +#include "../../include/pantheon.h" +#include "../core/engine.h" +#include "../core/qweight.h" +#include "../formats/gguf.h" +#include "llama.h" /* Reuses pn_llama_kv_cache */ + +/* + * Phi-3 model architecture. + * + * Covers: Phi-3-mini (3.8B), Phi-3-small, Phi-3-medium. + * + * Same compute as Llama (RMSNorm, RoPE, MHA/GQA, SwiGLU) but different + * GGUF weight layout: + * - Fused QKV: blk.N.attn_qkv.weight [3*n_embd, n_embd] + * - Fused gate+up: blk.N.ffn_up.weight [2*n_ff, n_embd] + * - Partial RoPE: rope.dimension_count may be < head_dim + */ + +/* ============================================================ + * Quantized weight structs + * ============================================================ */ + +typedef struct { + float *attn_norm; /* RMSNorm weight [n_embd] (F32, owned) */ + pn_qweight attn_qkv; /* FUSED Q+K+V [3*n_embd, n_embd] (quantized, mmap) */ + pn_qweight attn_output; /* Output projection [n_embd, n_embd] (quantized, mmap) */ + float *ffn_norm; /* RMSNorm weight [n_embd] (F32, owned) */ + pn_qweight ffn_gate_up; /* FUSED gate+up [2*n_ff, n_embd] (quantized, mmap) */ + pn_qweight ffn_down; /* Down projection [n_embd, n_ff] (quantized, mmap) */ +} pn_phi3_qlayer; + +typedef struct { + int is_quantized; /* Always 1, used to distinguish weight type */ + float *tok_embd; /* Token embeddings [n_vocab * n_embd] (F32, owned) */ + float *output_norm; /* Final RMSNorm [n_embd] (F32, owned) */ + pn_qweight output; /* LM head (quantized or F32) */ + float *output_f32; /* LM head F32 if tied embeddings (owned) */ + pn_phi3_qlayer *layers; /* Per-layer quantized weights [n_layer] */ + int n_layers; + int n_embd; + int n_vocab; + pn_gguf_file *gguf; /* Kept open (mmap backing) */ +} pn_phi3_qweights; + +/* ============================================================ + * Forward pass + * ============================================================ */ + +tm_tensor* pn_phi3_forward(pn_model *model, uint32_t token, pn_state *state); + +/* ============================================================ + * Weight loading + * ============================================================ */ + +pn_phi3_qweights* pn_phi3_load_gguf_quantized(pn_gguf_file *gguf, pn_config *config); + +/* Free */ +void pn_phi3_qweights_free(pn_phi3_qweights *qw); + +#endif /* PN_PHI3_H */ diff --git a/src/models/pixtral_vision.c b/src/models/pixtral_vision.c new file mode 100644 index 0000000..a5b6178 --- /dev/null +++ b/src/models/pixtral_vision.c @@ -0,0 +1,674 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pixtral_vision.c + * @brief Pixtral-style vision tower and LightOnOCR-style projector on F32 weights. + * + * Written from a behaviour specification (plans/cleanroom_r2_pixtral_spec.md); no ggml or + * llama.cpp source was consulted. + * + * @status None + * @issues - Kept as-is pending a separate fix: use_silu and image_size are loaded but + * unused (long edge fixed at 1024); image_mean/std arrays are not type- or + * length-checked and their descriptors leak; tensor shapes are not validated; + * allocations are unchecked; out_embd == NULL crashes; n_head == 0 divides by + * zero; head_dim % 4 != 0 leaves RoPE slots uninitialised; non-flash paths + * allocate N*N scores; on macOS the projector sgemms run with the caller's + * BLAS threading. + * - pn_vision_encode is far past ~100 lines: the attention fallbacks are inline + * on purpose, because release output bits depend on where loops live. + * @todo None + */ +#include "pixtral_vision.h" + +#include +#include +#include +#include +#include + +#ifdef USE_BLAS + #ifdef __APPLE__ + #include + #else + #include + #endif +#endif + +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +#ifdef USE_BLAS +#define PN_VISION_HAVE_CBLAS 1 +#endif + +/* Tiled online-softmax attention: never materialises the N x N score matrix. */ +#if defined(USE_THREADS) && defined(PN_VISION_HAVE_CBLAS) && !defined(PN_NO_VISION_FLASH) +#define PN_VISION_FLASH 1 +#define PN_VISION_FLASH_QUERY_TILE 128 +#define PN_VISION_FLASH_KEY_TILE 512 +#endif + +/* Runtime long edge of the resized image (independent of cfg.image_size). */ +#define PN_VISION_TARGET_LONG_EDGE 1024 + +/* ------------------------------------------------------------------------- */ +/* Elementwise / linear helpers */ +/* ------------------------------------------------------------------------- */ + +/* Y[M,N] = X[M,K] * W[N,K]^T, all row-major. */ +static void vision_linear_forward(const float *X, const float *W, float *Y, int M, int N, int K) { +#ifdef PN_VISION_HAVE_CBLAS + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, M, N, K, 1.0f, X, K, W, K, 0.0f, Y, N); +#else + for (int row = 0; row < M; row++) { + const float *x = X + (size_t)row * K; + for (int col = 0; col < N; col++) { + const float *w = W + (size_t)col * K; + float acc = 0; + for (int k = 0; k < K; k++) + acc += x[k] * w[k]; + Y[(size_t)row * N + col] = acc; + } + } +#endif +} + +/* In-place RMSNorm of one row; the sum of squares is accumulated in double. */ +static void vision_rms_norm_row(float *x, const float *w, int n, float eps) { + double ss = 0; + for (int i = 0; i < n; i++) + ss += (double)x[i] * x[i]; + float inv = 1.0f / sqrtf((float)(ss / n) + eps); + for (int i = 0; i < n; i++) + x[i] = x[i] * inv * w[i]; +} + +static inline float vision_gelu_tanh(float x) { + return 0.5f * x * (1.0f + tanhf(0.79788456f * (x + 0.044715f * x * x * x))); +} + +static inline float vision_silu(float x) { + return x / (1.0f + expf(-x)); +} + +/* Bilinear RGB8 resize with half-pixel centres and edge clamping. */ +static void vision_resize_bilinear_rgb8(const uint8_t *src, int sw, int sh, + uint8_t *dst, int dw, int dh) { + for (int y = 0; y < dh; y++) { + float sy = (y + 0.5f) * sh / dh - 0.5f; + int y0 = (int)floorf(sy); + float fy = sy - y0; + int y1 = y0 + 1; + if (y0 < 0) y0 = 0; + if (y1 > sh - 1) y1 = sh - 1; + if (y0 > sh - 1) y0 = sh - 1; + for (int x = 0; x < dw; x++) { + float sx = (x + 0.5f) * sw / dw - 0.5f; + int x0 = (int)floorf(sx); + float fx = sx - x0; + int x1 = x0 + 1; + if (x0 < 0) x0 = 0; + if (x1 > sw - 1) x1 = sw - 1; + if (x0 > sw - 1) x0 = sw - 1; + for (int ch = 0; ch < 3; ch++) { + float a = src[(y0 * sw + x0) * 3 + ch]; + float b = src[(y0 * sw + x1) * 3 + ch]; + float cc = src[(y1 * sw + x0) * 3 + ch]; + float d = src[(y1 * sw + x1) * 3 + ch]; + float top = a * (1 - fx) + b * fx; + float bot = cc * (1 - fx) + d * fx; + dst[(y * dw + x) * 3 + ch] = (uint8_t)lroundf(top * (1 - fy) + bot * fy); + } + } + } +} + +/* + * Per-patch 2D RoPE tables [gh*gw, head_dim]. The first half of each head rotates by + * the patch row, the second half by the patch column; each pair's two slots hold the + * same angle. + */ +static void vision_rope_2d_tables(float *cosb, float *sinb, int gh, int gw, int head_dim, + float theta) { + int half = head_dim / 2; + int npairs = half / 2; + for (int r = 0; r < gh; r++) { + for (int c = 0; c < gw; c++) { + float *cs = cosb + (size_t)(r * gw + c) * head_dim; + float *sn = sinb + (size_t)(r * gw + c) * head_dim; + for (int j = 0; j < npairs; j++) { + float fe = powf(theta, -(float)(2 * j) / (float)half); + float fo = powf(theta, -(float)(2 * j + 1) / (float)half); + float ar = r * fe; + float ac = c * fo; + cs[2 * j] = cs[2 * j + 1] = cosf(ar); + sn[2 * j] = sn[2 * j + 1] = sinf(ar); + cs[half + 2 * j] = cs[half + 2 * j + 1] = cosf(ac); + sn[half + 2 * j] = sn[half + 2 * j + 1] = sinf(ac); + } + } + } +} + +/* Rotate adjacent element pairs of one head; even slot uses table[2k], odd table[2k+1]. */ +static void vision_rope_rotate_head(float *v, const float *cp, const float *sp, int hd) { + for (int k = 0; k < hd / 2; k++) { + float x0 = v[2 * k]; + float x1 = v[2 * k + 1]; + v[2 * k] = x0 * cp[2 * k] - x1 * sp[2 * k]; + v[2 * k + 1] = x0 * sp[2 * k + 1] + x1 * cp[2 * k + 1]; + } +} + +/* ------------------------------------------------------------------------- */ +/* Attention workers (one per build path) */ +/* ------------------------------------------------------------------------- */ + +#ifdef PN_VISION_FLASH +typedef struct { + const float *Q, *K, *V; + float *AO; + int N, D, off, hd; + float scaleq; +} vision_flash_attention_job; + +/* + * Query tiles [start, end) of one head. Each tile is computed whole by one thread and + * written to its own rows, so results do not depend on the worker count. + */ +static void vision_flash_attention_tiles(int start, int end, void *arg) { + const vision_flash_attention_job *job = (const vision_flash_attention_job *)arg; + const int BQ = PN_VISION_FLASH_QUERY_TILE; + const int BK = PN_VISION_FLASH_KEY_TILE; + const int N = job->N, D = job->D, off = job->off, hd = job->hd; +#ifdef __APPLE__ + /* Tile sgemms are small; threading them inside a worker would oversubscribe. */ + BLASSetThreading(BLAS_THREADING_SINGLE_THREADED); +#endif + float *S = malloc((size_t)BQ * BK * sizeof(float)); + float *O = malloc((size_t)BQ * hd * sizeof(float)); + float *mrun = malloc((size_t)BQ * sizeof(float)); + float *lrun = malloc((size_t)BQ * sizeof(float)); + + for (int qt = start; qt < end; qt++) { + int q0 = qt * BQ; + int qn = N - q0 < BQ ? N - q0 : BQ; + const float *Qp = job->Q + (size_t)q0 * D + off; + for (int i = 0; i < qn; i++) { + mrun[i] = -1e30f; + lrun[i] = 0.0f; + } + memset(O, 0, (size_t)qn * hd * sizeof(float)); + + for (int k0 = 0; k0 < N; k0 += BK) { + int kn = N - k0 < BK ? N - k0 : BK; + const float *Kp = job->K + (size_t)k0 * D + off; + const float *Vp = job->V + (size_t)k0 * D + off; + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, qn, kn, hd, job->scaleq, + Qp, D, Kp, D, 0.0f, S, kn); + for (int i = 0; i < qn; i++) { + float *srow = S + (size_t)i * kn; + float mprev = mrun[i]; + float mcur = mprev; + for (int j = 0; j < kn; j++) + if (srow[j] > mcur) mcur = srow[j]; + float corr = expf(mprev - mcur); + float sum = 0.0f; + for (int j = 0; j < kn; j++) { + float p = expf(srow[j] - mcur); + srow[j] = p; + sum += p; + } + lrun[i] = lrun[i] * corr + sum; + mrun[i] = mcur; + for (int d = 0; d < hd; d++) + O[i * hd + d] *= corr; + } + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, qn, hd, kn, 1.0f, + S, kn, Vp, D, 1.0f, O, hd); + } + + for (int i = 0; i < qn; i++) { + float inv = 1.0f / lrun[i]; + for (int d = 0; d < hd; d++) + job->AO[(size_t)(q0 + i) * D + off + d] = O[i * hd + d] * inv; + } + } + + free(S); + free(O); + free(mrun); + free(lrun); +} +#endif /* PN_VISION_FLASH */ + +#if defined(USE_THREADS) && defined(PN_VISION_HAVE_CBLAS) && !defined(PN_VISION_FLASH) +typedef struct { + float *scores; + int N; +} vision_softmax_job; + +/* In-place softmax of score rows [start, end). */ +static void vision_softmax_rows(int start, int end, void *arg) { + const vision_softmax_job *job = (const vision_softmax_job *)arg; + int N = job->N; + for (int qi = start; qi < end; qi++) { + float *row = job->scores + (size_t)qi * N; + float mx = -1e30f; + for (int j = 0; j < N; j++) + if (row[j] > mx) mx = row[j]; + float sum = 0; + for (int j = 0; j < N; j++) { + row[j] = expf(row[j] - mx); + sum += row[j]; + } + float inv = 1.0f / sum; + for (int j = 0; j < N; j++) + row[j] *= inv; + } +} +#endif + +#if defined(USE_THREADS) && !defined(PN_VISION_HAVE_CBLAS) +typedef struct { + const float *Q, *K, *V; + float *AO, *scores; + int N, D, off, hd; + float scaleq; +} vision_attention_job; + +/* Scalar attention for query rows [start, end) of one head; row qi uses scores + qi*N. */ +static void vision_attention_rows(int start, int end, void *arg) { + const vision_attention_job *job = (const vision_attention_job *)arg; + int N = job->N, D = job->D, off = job->off, hd = job->hd; + float scaleq = job->scaleq; + for (int qi = start; qi < end; qi++) { + float *scores = job->scores + (size_t)qi * N; + const float *q = job->Q + (size_t)qi * D + off; + float maxs = -1e30f; + for (int ki = 0; ki < N; ki++) { + const float *k = job->K + (size_t)ki * D + off; + float s = 0; + for (int d = 0; d < hd; d++) + s += q[d] * k[d]; + s *= scaleq; + scores[ki] = s; + if (s > maxs) maxs = s; + } + float sum = 0; + for (int ki = 0; ki < N; ki++) { + scores[ki] = expf(scores[ki] - maxs); + sum += scores[ki]; + } + float inv = 1.0f / sum; + float *o = job->AO + (size_t)qi * D + off; + for (int d = 0; d < hd; d++) + o[d] = 0; + for (int ki = 0; ki < N; ki++) { + float wgt = scores[ki] * inv; + const float *v = job->V + (size_t)ki * D + off; + for (int d = 0; d < hd; d++) + o[d] += wgt * v[d]; + } + } +} +#endif + +/* ------------------------------------------------------------------------- */ +/* Load / free */ +/* ------------------------------------------------------------------------- */ + +pn_vision_model* pn_vision_load(pn_gguf_file *gguf) { + if (!gguf) + return NULL; + const char *arch = pn_gguf_get_str(gguf, "general.architecture"); + if (!arch || strcmp(arch, "clip") != 0) { + fprintf(stderr, "vision: not a clip mmproj GGUF\n"); + return NULL; + } + + pn_vision_model *m = calloc(1, sizeof(*m)); + pn_vision_config *c = &m->cfg; + c->image_size = (int)pn_gguf_get_u32(gguf, "clip.vision.image_size", 1540); + c->patch_size = (int)pn_gguf_get_u32(gguf, "clip.vision.patch_size", 14); + c->hidden = (int)pn_gguf_get_u32(gguf, "clip.vision.embedding_length", 1024); + c->n_layer = (int)pn_gguf_get_u32(gguf, "clip.vision.block_count", 24); + c->n_head = (int)pn_gguf_get_u32(gguf, "clip.vision.attention.head_count", 16); + c->n_ff = (int)pn_gguf_get_u32(gguf, "clip.vision.feed_forward_length", 4096); + c->merge = (int)pn_gguf_get_u32(gguf, "clip.vision.spatial_merge_size", 2); + c->proj_dim = (int)pn_gguf_get_u32(gguf, "clip.vision.projection_dim", 1024); + c->eps = pn_gguf_get_f32(gguf, "clip.vision.attention.layer_norm_epsilon", 1e-5f); + c->rope_theta = pn_gguf_get_f32(gguf, "clip.vision.rope_theta", 10000.0f); + c->use_silu = (int)pn_gguf_get_u32(gguf, "clip.use_silu", 1); + c->head_dim = c->hidden / c->n_head; + + /* Array type and length are trusted as-is (see @issues). */ + pn_gguf_arr *mean = pn_gguf_get_arr(gguf, "clip.vision.image_mean"); + pn_gguf_arr *std = pn_gguf_get_arr(gguf, "clip.vision.image_std"); + for (int ch = 0; ch < 3; ch++) { + c->image_mean[ch] = mean ? ((float *)mean->data)[ch] : 0.5f; + c->image_std[ch] = std ? ((float *)std->data)[ch] : 0.5f; + } + + /* Every top-level tensor is attempted so each missing one is reported. */ + m->patch_embd = pn_gguf_dequantize_tensor(gguf, "v.patch_embd.weight", NULL); + m->pre_ln = pn_gguf_dequantize_tensor(gguf, "v.pre_ln.weight", NULL); + m->mm_input_norm = pn_gguf_dequantize_tensor(gguf, "mm.input_norm.weight", NULL); + m->mm_merger = pn_gguf_dequantize_tensor(gguf, "mm.patch_merger.weight", NULL); + m->mm1 = pn_gguf_dequantize_tensor(gguf, "mm.1.weight", NULL); + m->mm2 = pn_gguf_dequantize_tensor(gguf, "mm.2.weight", NULL); + + m->layers = calloc(c->n_layer, sizeof(*m->layers)); + int ok = m->patch_embd && m->pre_ln && m->mm_input_norm && m->mm_merger && m->mm1 && m->mm2; + static const char *const layer_tensor_names[9] = { + "ln1", "attn_q", "attn_k", "attn_v", "attn_out", "ln2", "ffn_gate", "ffn_up", "ffn_down" + }; + for (int i = 0; ok && i < c->n_layer; i++) { + pn_vision_layer *L = &m->layers[i]; + float **slots[9] = { &L->ln1, &L->wq, &L->wk, &L->wv, &L->wo, + &L->ln2, &L->w_gate, &L->w_up, &L->w_down }; + for (int t = 0; t < 9; t++) { + char name[64]; + snprintf(name, sizeof(name), "v.blk.%d.%s.weight", i, layer_tensor_names[t]); + *slots[t] = pn_gguf_dequantize_tensor(gguf, name, NULL); + } + for (int t = 0; t < 9; t++) + if (!*slots[t]) ok = 0; + } + + if (!ok) { + fprintf(stderr, "vision: missing tensors\n"); + pn_vision_free(m); + return NULL; + } + fprintf(stderr, "vision: clip loaded (hidden=%d layers=%d heads=%d ff=%d patch=%d merge=%d)\n", + c->hidden, c->n_layer, c->n_head, c->n_ff, c->patch_size, c->merge); + return m; +} + +void pn_vision_free(pn_vision_model *m) { + if (!m) + return; + free(m->patch_embd); + free(m->pre_ln); + free(m->mm_input_norm); + free(m->mm_merger); + free(m->mm1); + free(m->mm2); + if (m->layers) { + for (int i = 0; i < m->cfg.n_layer; i++) { + pn_vision_layer *L = &m->layers[i]; + free(L->ln1); + free(L->wq); + free(L->wk); + free(L->wv); + free(L->wo); + free(L->ln2); + free(L->w_gate); + free(L->w_up); + free(L->w_down); + } + free(m->layers); + } + free(m); +} + +/* ------------------------------------------------------------------------- */ +/* Encode */ +/* ------------------------------------------------------------------------- */ + +/* + * Kept as one function on purpose: under -ffast-math the release bits depend on loop + * shapes and where they live, so the serial attention fallbacks stay inline here. + */ +int pn_vision_encode(pn_vision_model *m, const uint8_t *rgb, int w, int h, + float **out_embd, int *out_dim) { + if (!m || !rgb || w <= 0 || h <= 0) + return -1; + const pn_vision_config *c = &m->cfg; + const int ps = c->patch_size, mg = c->merge, D = c->hidden, hd = c->head_dim; + const int nh = c->n_head, F = c->n_ff, PD = c->proj_dim; + const float eps = c->eps; + + /* Target size: long edge scaled to 1024, rounded up to whole patches, at least one + * merge block per side. */ + int step = ps * mg; + int long_edge = w > h ? w : h; + float scale = (float)PN_VISION_TARGET_LONG_EDGE / (float)long_edge; + int tw = (int)((float)w * scale); + int th = (int)((float)h * scale); + tw = ((tw + ps - 1) / ps) * ps; + if (tw < step) tw = step; + th = ((th + ps - 1) / ps) * ps; + if (th < step) th = step; + int gw = tw / ps, gh = th / ps, N = gw * gh; + + uint8_t *img = malloc((size_t)tw * th * 3); + vision_resize_bilinear_rgb8(rgb, w, h, img, tw, th); + + /* Patch matrix [N, 3*ps*ps], each row ordered (channel, ky, kx). */ + int pk = 3 * ps * ps; + float *P = malloc((size_t)N * pk * sizeof(float)); + for (int gr = 0; gr < gh; gr++) { + for (int gc = 0; gc < gw; gc++) { + float *prow = P + (size_t)(gr * gw + gc) * pk; + int idx = 0; + for (int ch = 0; ch < 3; ch++) { + for (int ky = 0; ky < ps; ky++) { + for (int kx = 0; kx < ps; kx++) { + int px = gc * ps + kx; + int py = gr * ps + ky; + float v = img[(py * tw + px) * 3 + ch] / 255.0f; + prow[idx++] = (v - c->image_mean[ch]) / c->image_std[ch]; + } + } + } + } + } + free(img); + + float *X = malloc((size_t)N * D * sizeof(float)); + vision_linear_forward(P, m->patch_embd, X, N, D, pk); + free(P); + for (int i = 0; i < N; i++) + vision_rms_norm_row(X + (size_t)i * D, m->pre_ln, D, eps); + + float *cosb = malloc((size_t)N * hd * sizeof(float)); + float *sinb = malloc((size_t)N * hd * sizeof(float)); + vision_rope_2d_tables(cosb, sinb, gh, gw, hd, c->rope_theta); + + size_t nd = (size_t)N * D; + size_t nf = (size_t)N * F; + float *normed = malloc(nd * sizeof(float)); + float *Q = malloc(nd * sizeof(float)); + float *K = malloc(nd * sizeof(float)); + float *V = malloc(nd * sizeof(float)); + float *AO = malloc(nd * sizeof(float)); + float *tmpD = malloc(nd * sizeof(float)); + float *gate = malloc(nf * sizeof(float)); + float *up = malloc(nf * sizeof(float)); +#ifdef PN_VISION_FLASH + float *scores = NULL; +#else + float *scores = malloc((size_t)N * N * sizeof(float)); +#endif + float scaleq = 1.0f / sqrtf((float)hd); + +#ifdef USE_THREADS + fw_pool *vpool = fw_pool_new(0); +#endif +#if defined(PN_VISION_FLASH) && defined(__APPLE__) + /* One thread already saturates the matrix unit for the big matmuls; worker-level + * threading on top would oversubscribe. Restored before the projector. */ + int saved_blas_threading = (int)BLASGetThreading(); + BLASSetThreading(BLAS_THREADING_SINGLE_THREADED); +#endif + + for (int l = 0; l < c->n_layer; l++) { + const pn_vision_layer *L = &m->layers[l]; + + memcpy(normed, X, nd * sizeof(float)); + for (int i = 0; i < N; i++) + vision_rms_norm_row(normed + (size_t)i * D, L->ln1, D, eps); + vision_linear_forward(normed, L->wq, Q, N, D, D); + vision_linear_forward(normed, L->wk, K, N, D, D); + vision_linear_forward(normed, L->wv, V, N, D, D); + + for (int i = 0; i < N; i++) { + for (int hh = 0; hh < nh; hh++) { + vision_rope_rotate_head(Q + (size_t)i * D + hh * hd, cosb + (size_t)i * hd, + sinb + (size_t)i * hd, hd); + vision_rope_rotate_head(K + (size_t)i * D + hh * hd, cosb + (size_t)i * hd, + sinb + (size_t)i * hd, hd); + } + } + + for (int hh = 0; hh < nh; hh++) { + int off = hh * hd; +#if defined(PN_VISION_FLASH) + int ntiles = (N + PN_VISION_FLASH_QUERY_TILE - 1) / PN_VISION_FLASH_QUERY_TILE; + vision_flash_attention_job job = { Q, K, V, AO, N, D, off, hd, scaleq }; + if (vpool) + fw_for(vpool, ntiles, vision_flash_attention_tiles, &job); + else + vision_flash_attention_tiles(0, ntiles, &job); +#elif defined(PN_VISION_HAVE_CBLAS) + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasTrans, N, N, hd, scaleq, + Q + off, D, K + off, D, 0.0f, scores, N); +#ifdef USE_THREADS + vision_softmax_job sjob = { scores, N }; + if (vpool) { + fw_for(vpool, N, vision_softmax_rows, &sjob); + } else +#endif + { + for (int qi = 0; qi < N; qi++) { + float *row = scores + (size_t)qi * N; + float mx = -1e30f; + for (int j = 0; j < N; j++) + if (row[j] > mx) mx = row[j]; + float sum = 0; + for (int j = 0; j < N; j++) { + row[j] = expf(row[j] - mx); + sum += row[j]; + } + float inv = 1.0f / sum; + for (int j = 0; j < N; j++) + row[j] *= inv; + } + } + cblas_sgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, N, hd, N, 1.0f, + scores, N, V + off, D, 0.0f, AO + off, D); +#else +#ifdef USE_THREADS + vision_attention_job ajob = { Q, K, V, AO, scores, N, D, off, hd, scaleq }; + if (vpool) { + fw_for(vpool, N, vision_attention_rows, &ajob); + } else +#endif + { + for (int qi = 0; qi < N; qi++) { + const float *q = Q + (size_t)qi * D + off; + float maxs = -1e30f; + for (int ki = 0; ki < N; ki++) { + const float *k = K + (size_t)ki * D + off; + float s = 0; + for (int d = 0; d < hd; d++) + s += q[d] * k[d]; + s *= scaleq; + scores[ki] = s; + if (s > maxs) maxs = s; + } + float sum = 0; + for (int ki = 0; ki < N; ki++) { + scores[ki] = expf(scores[ki] - maxs); + sum += scores[ki]; + } + float inv = 1.0f / sum; + float *o = AO + (size_t)qi * D + off; + for (int d = 0; d < hd; d++) + o[d] = 0; + for (int ki = 0; ki < N; ki++) { + float wgt = scores[ki] * inv; + const float *v = V + (size_t)ki * D + off; + for (int d = 0; d < hd; d++) + o[d] += wgt * v[d]; + } + } + } +#endif + } + + vision_linear_forward(AO, L->wo, tmpD, N, D, D); + for (size_t i = 0; i < nd; i++) + X[i] += tmpD[i]; + + memcpy(normed, X, nd * sizeof(float)); + for (int i = 0; i < N; i++) + vision_rms_norm_row(normed + (size_t)i * D, L->ln2, D, eps); + vision_linear_forward(normed, L->w_gate, gate, N, F, D); + vision_linear_forward(normed, L->w_up, up, N, F, D); + for (size_t i = 0; i < nf; i++) + gate[i] = vision_silu(gate[i]) * up[i]; + vision_linear_forward(gate, L->w_down, tmpD, N, D, F); + for (size_t i = 0; i < nd; i++) + X[i] += tmpD[i]; + } + +#ifdef USE_THREADS + if (vpool) + fw_pool_free(vpool); +#endif +#if defined(PN_VISION_FLASH) && defined(__APPLE__) + BLASSetThreading((enum BLAS_THREADING)saved_blas_threading); +#endif + free(normed); + free(Q); + free(K); + free(V); + free(AO); + free(gate); + free(up); + free(scores); + free(cosb); + free(sinb); + + /* Projector: RMSNorm, mg x mg spatial merge (channel-major), linear, GELU, linear. + * An odd trailing patch row/column is dropped by the merge. */ + for (int i = 0; i < N; i++) + vision_rms_norm_row(X + (size_t)i * D, m->mm_input_norm, D, eps); + int mh = gh / mg, mw = gw / mg, M = mh * mw, mk = D * mg * mg; + float *merged = malloc((size_t)M * mk * sizeof(float)); + for (int br = 0; br < mh; br++) { + for (int bc = 0; bc < mw; bc++) { + float *dst = merged + (size_t)(br * mw + bc) * mk; + for (int ch = 0; ch < D; ch++) + for (int ky = 0; ky < mg; ky++) + for (int kx = 0; kx < mg; kx++) + dst[ch * mg * mg + ky * mg + kx] = + X[((size_t)(br * mg + ky) * gw + (bc * mg + kx)) * D + ch]; + } + } + free(X); + free(tmpD); + + size_t mpd = (size_t)M * PD; + float *mo = malloc(mpd * sizeof(float)); + vision_linear_forward(merged, m->mm_merger, mo, M, PD, mk); + free(merged); + float *h1 = malloc(mpd * sizeof(float)); + vision_linear_forward(mo, m->mm1, h1, M, PD, PD); + for (size_t i = 0; i < mpd; i++) + h1[i] = vision_gelu_tanh(h1[i]); + float *out = malloc(mpd * sizeof(float)); + vision_linear_forward(h1, m->mm2, out, M, PD, PD); + free(mo); + free(h1); + + *out_embd = out; + if (out_dim) + *out_dim = PD; + return M; +} diff --git a/src/models/pixtral_vision.h b/src/models/pixtral_vision.h new file mode 100644 index 0000000..9128987 --- /dev/null +++ b/src/models/pixtral_vision.h @@ -0,0 +1,126 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file pixtral_vision.h + * @brief Pixtral-style vision encoder with a LightOnOCR-style projector (clip mmproj GGUF). + * + * Written from a behaviour specification (plans/cleanroom_r2_pixtral_spec.md); no ggml or + * llama.cpp source was consulted. + * + * @status None + * @issues None + * @todo None + */ +#ifndef PN_PIXTRAL_VISION_H +#define PN_PIXTRAL_VISION_H + +#include +#include "../formats/gguf.h" +#include "../formats/gguf_dequant.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Vision tower + projector hyperparameters, read from `clip.*` GGUF metadata. + * Layout is ABI (callers and tests rely on sizeof/offsetof). + */ +typedef struct { + int image_size; /* advertised image size; stored, not used by encode */ + int patch_size; /* square patch edge in pixels */ + int hidden; /* tower width D */ + int n_layer; /* transformer blocks */ + int n_head; /* attention heads */ + int head_dim; /* hidden / n_head (integer division) */ + int n_ff; /* MLP inner width */ + int merge; /* projector spatial merge factor */ + int proj_dim; /* output embedding width (decoder n_embd) */ + float eps; /* RMSNorm epsilon, used by every norm */ + float rope_theta; /* 2D RoPE base */ + int use_silu; /* stored, not used: the MLP is always SiLU-gated */ + float image_mean[3]; /* per-channel pixel normalisation mean */ + float image_std[3]; /* per-channel pixel normalisation std */ +} pn_vision_config; + +/** + * One transformer block's F32 weights, row-major [out, in]: + * norms [hidden]; wq/wk/wv/wo [hidden, hidden]; w_gate/w_up [n_ff, hidden]; + * w_down [hidden, n_ff]. No biases. + */ +typedef struct { + float *ln1; + float *wq, *wk, *wv, *wo; + float *ln2; + float *w_gate, *w_up; + float *w_down; +} pn_vision_layer; + +/** + * A loaded vision encoder. Owns F32 copies of every weight: + * patch_embd [hidden, 3*ps*ps] (input ordered channel, ky, kx); pre_ln and + * mm_input_norm [hidden]; mm_merger [proj_dim, hidden*merge*merge]; + * mm1/mm2 [proj_dim, proj_dim]; layers[cfg.n_layer]. + */ +typedef struct { + pn_vision_config cfg; + float *patch_embd; + float *pre_ln; + pn_vision_layer *layers; + float *mm_input_norm; + float *mm_merger; + float *mm1; + float *mm2; +} pn_vision_model; + +/** + * Load a vision encoder from a GGUF whose `general.architecture` is "clip" (a vision + * mmproj file). Weights are dequantized to owned F32 copies, so the caller may close + * the GGUF right after this returns. + * + * @param gguf Open GGUF file (not modified beyond reads) + * @return New model (free with pn_vision_free), or NULL on a NULL argument, a + * non-clip file, or missing/undecodable tensors (messages on stderr) + * + * Thread-safe: yes (no shared state) + * Reentrant: yes + * Signal-safe: no + */ +pn_vision_model* pn_vision_load(pn_gguf_file *gguf); + +/** + * Free a model returned by pn_vision_load. NULL is a no-op. + * + * Thread-safe: yes (for distinct models) + * Reentrant: yes + * Signal-safe: no + */ +void pn_vision_free(pn_vision_model *m); + +/** + * Encode an RGB8 image into decoder-space embeddings. Runs a Pixtral-style tower + * (patch conv, RMSNorm, 2D rotary attention, SiLU-gated MLP) on the image resized so + * its long edge is about 1024 px, then a LightOnOCR-style projector (RMSNorm, spatial + * merge, two linears with GELU between). + * + * @param m Loaded model + * @param rgb Interleaved RGB8 pixels, w*h*3 bytes + * @param w, h Image size in pixels (> 0) + * @param out_embd Receives a malloc'd [rows * proj_dim] float buffer (caller frees) + * @param out_dim Receives proj_dim (may be NULL) + * @return Number of rows (image tokens, row-major over the merged grid), or -1 + * on a NULL model/image or non-positive size (outputs untouched) + * + * Thread-safe: TBD (reads the model only; on macOS changes the calling thread's + * Accelerate threading during the tower and restores it afterwards) + * Reentrant: TBD + * Signal-safe: no + */ +int pn_vision_encode(pn_vision_model *m, const uint8_t *rgb, int w, int h, + float **out_embd, int *out_dim); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_PIXTRAL_VISION_H */ diff --git a/src/models/qwen35moe.c b/src/models/qwen35moe.c new file mode 100644 index 0000000..f88e991 --- /dev/null +++ b/src/models/qwen35moe.c @@ -0,0 +1,1014 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe.c + * @brief Qwen3.5-MoE (Qwen3-Next lineage) loader for Pantheon. Implements A2: + * config parsing, per-layer type map, block-struct definitions, GGUF + * weight loading, and a stub forward. Hybrid Gated-Delta-Net SSM + + * sparse full-attention + shared-expert MoE. + * + * @status A6 complete: the forward orchestration + dual-state lifecycle live in + * qwen35moe_forward.c; this file keeps the model struct + GGUF load and + * exposes internal accessors (config, per-type counts, index maps, + * per-type weight arrays, embed/norm/head) for that file. A2-A5 kernels + * load via the GGUF/pn_qweight path; F32 norms/biases dequantized+owned. + * @issues None + * @todo + * - [ ] A8: validate logits vs the llama.cpp oracle (numeric correctness). + */ + +/* 1. This module's own header */ +#include "qwen35moe.h" + +/* 2. Standard library */ +#include +#include +#include + +/* 4. Project headers */ +#include "../core/qweight.h" +#include "../formats/gguf_dequant.h" +#include "qwen35moe_internal.h" +#include "qwen35moe_kv_q4.h" + +/* The config, per-layer-type enum, and block structs (pn_qwen35moe_config, + * pn_qwen35moe_moe, pn_qwen35moe_ssm_layer, pn_qwen35moe_attn_layer) live in + * qwen35moe_internal.h so the per-block compute files can share them. */ + +/* ============================================================ + * Model (opaque) + * + * Mirrors rwkvx.c's dual per-type arrays: ssm_layers[] and attn_layers[] are + * indexed by per-type index; block_type[] + block_index[] map a global layer + * index to its type and per-type slot. + * ============================================================ */ + +struct pn_qwen35moe_model { + pn_qwen35moe_config cfg; + + /* Model-level tensors */ + float *token_embd; /* [n_vocab * n_embd] (F32, owned) */ + float *output_norm; /* final RMSNorm [n_embd] (F32, owned) */ + pn_qweight output; /* LM head */ + float *output_f32; /* LM head F32 if tied embeddings (owned) */ + + /* Per-type block arrays */ + pn_qwen35moe_ssm_layer *ssm_layers; + pn_qwen35moe_attn_layer *attn_layers; + int n_ssm; + int n_attn; + + /* Global layer index -> (type, per-type slot) */ + pn_qwen35moe_layer_type *block_type; /* [n_layer] */ + int *block_index; /* [n_layer] per-type index */ + + pn_gguf_file *gguf; /* kept open (mmap backing) */ + + /* GPU residency handle (opaque pn_qwen35moe_gpu, defined in + * qwen35moe_forward_cuda.c). NULL when CUDA is unavailable or the model + * was not uploaded. Held here so the single-die GPU forward can find its + * persistent fc_buffer weights without re-uploading per token. The CPU + * weights above stay live so the CPU path is never broken. */ + void *gpu; + + /* Explicit die placement (pn_load_on, Phase 7). n_gpu_dies == 0 means "no + * placement given" — the GPU upload falls back to the QWEN35_GPU_DIE_A/B + * env knobs / defaults. When set, placement wins over env. */ + int gpu_dies[8]; + int n_gpu_dies; + + /* Where a DETACHED accelerator matvec seam is parked. This family's accelerator is a + * hook on cfg.mv rather than a separate forward, so a parity harness needs somewhere to + * put it while it runs the CPU oracle in the same process. + * See pn_qwen35moe_model_accel_mv_enable(). */ + pn_qwen35moe_mv mv_detached; +}; + +/* ============================================================ + * Config reading + * ============================================================ */ + +static int qwen35moe_read_config(pn_gguf_file *gguf, pn_qwen35moe_config *qc, + pn_config *config) +{ + /* Two GGUF families share this loader: Nex-N2-mini tags + * general.architecture="qwen35moe" (routed MoE), PrismML Bonsai-27B tags + * "qwen35" (dense SwiGLU FFN). The metadata keys are prefixed by the arch + * string, so pick the prefix from what the file declares. */ + const char *arch = pn_gguf_get_str(gguf, "general.architecture"); + const char *p = (arch && strcmp(arch, "qwen35") == 0) ? "qwen35" : "qwen35moe"; + char key[128]; + + snprintf(key, sizeof(key), "%s.block_count", p); + qc->n_layer = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.full_attention_interval", p); + qc->full_attention_interval = (int)pn_gguf_get_u32(gguf, key, 4); + snprintf(key, sizeof(key), "%s.embedding_length", p); + qc->n_embd = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.head_count", p); + qc->n_head = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.head_count_kv", p); + qc->n_head_kv = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.attention.key_length", p); + qc->head_dim = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.rope.dimension_count", p); + qc->rope_dim = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.rope.freq_base", p); + qc->rope_theta = pn_gguf_get_f32(gguf, key, 10000.0f); + + /* Multi-section (IMRoPE) split. A2 didn't parse this — A3 does. For a + * text-only model every section shares the single linear token position, + * so the sections only matter for image/video position ids; the attention + * kernel collapses multi-section RoPE to standard NEOX. We still parse and + * log the array so the field is populated for A6 / multimodal variants. + * Stored innermost-first as INT32; default split [rope_dim,0,0,0]. */ + qc->rope_sections[0] = qc->rope_dim; + qc->rope_sections[1] = 0; + qc->rope_sections[2] = 0; + qc->rope_sections[3] = 0; + snprintf(key, sizeof(key), "%s.rope.dimension_sections", p); + pn_gguf_arr *sections = pn_gguf_get_arr(gguf, key); + if (sections && sections->n > 0 && sections->elem_type == GGUF_TYPE_INT32) { + const int32_t *vals = (const int32_t *)sections->data; + int n = sections->n < 4 ? (int)sections->n : 4; + int sum = 0; + for (int s = 0; s < n; s++) { + qc->rope_sections[s] = (int)vals[s]; + sum += qc->rope_sections[s]; + } + fprintf(stderr, + "qwen35moe: rope.dimension_sections = [%d %d %d %d] (sum=%d, " + "rope_dim=%d) — text path uses standard NEOX\n", + qc->rope_sections[0], qc->rope_sections[1], + qc->rope_sections[2], qc->rope_sections[3], sum, qc->rope_dim); + } + snprintf(key, sizeof(key), "%s.attention.layer_norm_rms_epsilon", p); + qc->rms_norm_eps = pn_gguf_get_f32(gguf, key, 1e-6f); + snprintf(key, sizeof(key), "%s.expert_count", p); + qc->n_expert = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.expert_used_count", p); + qc->n_expert_used = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.expert_feed_forward_length", p); + qc->expert_ff = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.expert_shared_feed_forward_length", p); + qc->shared_ff = (int)pn_gguf_get_u32(gguf, key, 0); + /* Routed-expert weight scale. Absent in the Qwen3.5-MoE GGUFs seen so far + * (gating is softmax + norm_topk_prob, no extra scale), so it defaults to + * 1.0; honored if a future conversion emits the key. */ + snprintf(key, sizeof(key), "%s.expert_weights_scale", p); + qc->expert_weights_scale = pn_gguf_get_f32(gguf, key, 1.0f); + + /* Dense-FFN (Bonsai) detection: no routed experts + merged expert tensors + * absent + a plain blk.0.ffn_gate present. When dense, feed_forward_length + * gives the SwiGLU inner dim and we drive the FFN through the shared-expert + * slots (shared_ff := ffn_length). */ + qc->dense_ffn = 0; + qc->ffn_length = 0; + if (qc->n_expert == 0 && + pn_gguf_find_tensor(gguf, "blk.0.ffn_gate_exps.weight") == NULL && + pn_gguf_find_tensor(gguf, "blk.0.ffn_gate.weight") != NULL) { + qc->dense_ffn = 1; + snprintf(key, sizeof(key), "%s.feed_forward_length", p); + qc->ffn_length = (int)pn_gguf_get_u32(gguf, key, 0); + if (qc->ffn_length == 0) { + /* Fall back to the actual tensor shape (rows = ffn dim). */ + pn_gguf_tensor_info *g0 = pn_gguf_find_tensor(gguf, "blk.0.ffn_gate.weight"); + if (g0 && g0->n_dims >= 2) + qc->ffn_length = (int)g0->dims[g0->n_dims - 1]; + } + qc->shared_ff = qc->ffn_length; /* dense FFN reuses the shared-expert path */ + } + + snprintf(key, sizeof(key), "%s.ssm.state_size", p); + qc->ssm_state = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.ssm.group_count", p); + qc->ssm_n_group = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.ssm.time_step_rank", p); + qc->ssm_dt_rank = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.ssm.inner_size", p); + qc->ssm_d_inner = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.ssm.conv_kernel", p); + qc->ssm_conv = (int)pn_gguf_get_u32(gguf, key, 0); + snprintf(key, sizeof(key), "%s.context_length", p); + qc->n_ctx = (int)pn_gguf_get_u32(gguf, key, 2048); + + /* Derived: conv runs over the full q|k|v stream = + * key_dim*n_head_kv*2 (q,k) + value_dim*... — actually key*2 + value where + * key = head_dim*n_head_kv*... ; per the design doc this is the attn_qkv + * row count. Prefer the actual tensor shape over arithmetic. */ + pn_gguf_tensor_info *qkv0 = pn_gguf_find_tensor(gguf, "blk.0.attn_qkv.weight"); + if (qkv0 && qkv0->n_dims >= 2) + qc->conv_dim = (int)qkv0->dims[qkv0->n_dims - 1]; + else + qc->conv_dim = qc->n_embd * 2 + qc->ssm_d_inner; + + /* Vocab from the token embedding shape (outermost dim). */ + pn_gguf_tensor_info *embd = pn_gguf_find_tensor(gguf, "token_embd.weight"); + if (embd && embd->n_dims >= 2) + qc->n_vocab = (int)embd->dims[embd->n_dims - 1]; + + /* Multi-Token-Prediction (MTP) head skip. Qwen3.6-35B-A3B ships an extra + * trailing block (block_count=41: 40 transformer layers + 1 MTP head). The + * MTP block is only used for speculative draft during training and lacks the + * full hybrid-layer tensor set (no blk.N.ssm_conv1d) - the standard + * autoregressive forward never touches it. If the last block is missing its + * SSM conv while an earlier block has one, treat it as an MTP head and drop + * it from the layer count. Additive: Nex-N2 (40 layers, every block a real + * hybrid layer) never trips this, so it is a no-op there. */ + if (qc->n_layer >= 2) { + char mtp_key[64]; + snprintf(mtp_key, sizeof(mtp_key), "blk.%d.nextn.eh_proj.weight", qc->n_layer - 1); + if (pn_gguf_find_tensor(gguf, mtp_key) != NULL) { + fprintf(stderr, "qwen35moe: dropping trailing MTP/NextN head (block %d, has " + "nextn.eh_proj); loading %d transformer layers\n", + qc->n_layer - 1, qc->n_layer - 1); + qc->n_layer -= 1; + } + } + + if (qc->n_embd == 0 || qc->n_head == 0 || qc->n_layer == 0) { + fprintf(stderr, "qwen35moe: invalid config (n_embd=%d n_head=%d n_layer=%d)\n", + qc->n_embd, qc->n_head, qc->n_layer); + return -1; + } + if (qc->n_head_kv == 0) + qc->n_head_kv = qc->n_head; + + /* KV Phase 1: carry the per-model context cap set by the engine (pn_load_on + * from the load opts) INTO the internal config, so the KV ring is sized per + * model. config->ctx_cap is set before this call; 0 = unset (env fallback). */ + qc->ctx_cap = config->ctx_cap; + + /* Mirror the shared subset into the engine config. */ + config->arch = PN_ARCH_QWEN35MOE; + config->modality = PN_MODALITY_TEXT; + config->n_vocab = qc->n_vocab; + config->n_embd = qc->n_embd; + config->n_head = qc->n_head; + config->n_kv_head = qc->n_head_kv; + config->n_layer = qc->n_layer; + config->n_ff = qc->dense_ffn ? qc->ffn_length : qc->expert_ff; + config->n_ctx = qc->n_ctx; + config->rope_theta = qc->rope_theta; + config->rms_norm_eps = qc->rms_norm_eps; + config->rope_dim = qc->rope_dim; + config->n_experts = qc->n_expert; + config->n_experts_used = qc->n_expert_used; + + fprintf(stderr, + "qwen35moe: n_layer=%d interval=%d n_embd=%d n_head=%d/%d head_dim=%d " + "rope_dim=%d\n", + qc->n_layer, qc->full_attention_interval, qc->n_embd, qc->n_head, + qc->n_head_kv, qc->head_dim, qc->rope_dim); + if (qc->dense_ffn) + fprintf(stderr, + "qwen35moe: DENSE FFN (Bonsai, arch=%s) ffn_length=%d conv_dim=%d " + "n_vocab=%d\n", + p, qc->ffn_length, qc->conv_dim, qc->n_vocab); + else + fprintf(stderr, + "qwen35moe: experts=%d top-%d expert_ff=%d shared_ff=%d conv_dim=%d " + "n_vocab=%d\n", + qc->n_expert, qc->n_expert_used, qc->expert_ff, qc->shared_ff, + qc->conv_dim, qc->n_vocab); + fprintf(stderr, + "qwen35moe: ssm state=%d n_group=%d dt_rank=%d d_inner=%d conv=%d\n", + qc->ssm_state, qc->ssm_n_group, qc->ssm_dt_rank, qc->ssm_d_inner, + qc->ssm_conv); + return 0; +} + +/* True when global layer i is a full-attention layer. */ +static int qwen35moe_is_attn_layer(const pn_qwen35moe_config *qc, int i) +{ + int interval = qc->full_attention_interval > 0 ? qc->full_attention_interval : 4; + return ((i + 1) % interval) == 0; +} + +/* ============================================================ + * Loading helpers + * ============================================================ */ + +/* Dequantize a named tensor to an owned F32 buffer. Logs + returns NULL on miss. */ +static float *qwen35moe_load_f32(pn_gguf_file *gguf, const char *name) +{ + float *out = pn_gguf_dequantize_tensor(gguf, name, NULL); + if (!out) + fprintf(stderr, "qwen35moe: failed to load %s\n", name); + return out; +} + +/* Load the shared-expert MoE block for one layer. Routed experts are sliced + * from the merged 3D *_exps tensors (zero-copy). Returns 0 on success. */ +static int qwen35moe_load_moe(pn_gguf_file *gguf, const pn_qwen35moe_config *qc, + int i, pn_qwen35moe_moe *moe) +{ + char name[128]; + + /* Dense-FFN (Bonsai): a single SwiGLU per layer. Load ffn_gate/up/down into + * the shared-expert slots (the shared-expert code path IS a dense SwiGLU + * minus the routed experts + scalar gate) and leave the router/expert + * pointers NULL so the FFN block skips routing entirely. */ + if (qc->dense_ffn) { + moe->ffn_gate_inp = (pn_qweight){0}; + moe->gate_exps = NULL; + moe->up_exps = NULL; + moe->down_exps = NULL; + moe->ffn_gate_inp_shexp = (pn_qweight){0}; + snprintf(name, sizeof(name), "blk.%d.ffn_gate.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_gate_shexp, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ffn_up.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_up_shexp, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ffn_down.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_down_shexp, gguf, name) < 0) + return -1; + return 0; + } + + snprintf(name, sizeof(name), "blk.%d.ffn_gate_inp.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_gate_inp, gguf, name) < 0) + return -1; + + snprintf(name, sizeof(name), "blk.%d.ffn_gate_exps.weight", i); + pn_gguf_tensor_info *gate = pn_gguf_find_tensor(gguf, name); + snprintf(name, sizeof(name), "blk.%d.ffn_up_exps.weight", i); + pn_gguf_tensor_info *up = pn_gguf_find_tensor(gguf, name); + snprintf(name, sizeof(name), "blk.%d.ffn_down_exps.weight", i); + pn_gguf_tensor_info *down = pn_gguf_find_tensor(gguf, name); + if (!gate || !up || !down) { + fprintf(stderr, "qwen35moe: layer %d missing merged expert tensors\n", i); + return -1; + } + + int n_exp = qc->n_expert; + moe->gate_exps = calloc((size_t)n_exp, sizeof(*moe->gate_exps)); + moe->up_exps = calloc((size_t)n_exp, sizeof(*moe->up_exps)); + moe->down_exps = calloc((size_t)n_exp, sizeof(*moe->down_exps)); + if (!moe->gate_exps || !moe->up_exps || !moe->down_exps) + return -1; + + /* gate/up: GGUF dims [n_embd, expert_ff, n_expert]; per expert + * rows=expert_ff cols=n_embd. down: [expert_ff, n_embd, n_expert]; + * per expert rows=n_embd cols=expert_ff. (mirrors llama.c MoE slicing) */ + size_t gate_bpb = pn_gguf_bytes_per_block(gate->type); + int gate_bs = pn_gguf_block_size(gate->type); + size_t down_bpb = pn_gguf_bytes_per_block(down->type); + int down_bs = pn_gguf_block_size(down->type); + + int gate_rows = qc->expert_ff, gate_cols = qc->n_embd; + size_t gate_row_stride = ((size_t)gate_cols / (size_t)gate_bs) * gate_bpb; + size_t gate_expert_stride = (size_t)gate_rows * gate_row_stride; + int down_rows = qc->n_embd, down_cols = qc->expert_ff; + size_t down_row_stride = ((size_t)down_cols / (size_t)down_bs) * down_bpb; + size_t down_expert_stride = (size_t)down_rows * down_row_stride; + + const char *gate_base = (const char *)gguf->data + gate->offset; + const char *up_base = (const char *)gguf->data + up->offset; + const char *down_base = (const char *)gguf->data + down->offset; + + for (int e = 0; e < n_exp; e++) { + moe->gate_exps[e] = (pn_qweight){ .data = gate_base + e * gate_expert_stride, + .type = gate->type, .rows = gate_rows, .cols = gate_cols, + .row_stride = gate_row_stride }; + moe->up_exps[e] = (pn_qweight){ .data = up_base + e * gate_expert_stride, + .type = up->type, .rows = gate_rows, .cols = gate_cols, + .row_stride = gate_row_stride }; + moe->down_exps[e] = (pn_qweight){ .data = down_base + e * down_expert_stride, + .type = down->type, .rows = down_rows, .cols = down_cols, + .row_stride = down_row_stride }; + } + + snprintf(name, sizeof(name), "blk.%d.ffn_gate_shexp.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_gate_shexp, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ffn_up_shexp.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_up_shexp, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ffn_down_shexp.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_down_shexp, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ffn_gate_inp_shexp.weight", i); + if (pn_qweight_from_gguf(&moe->ffn_gate_inp_shexp, gguf, name) < 0) + return -1; + + return 0; +} + +/* Load one SSM (Gated-Delta-Net) layer. Returns 0 on success. */ +static int qwen35moe_load_ssm_layer(pn_gguf_file *gguf, const pn_qwen35moe_config *qc, + int i, pn_qwen35moe_ssm_layer *l) +{ + char name[128]; + + snprintf(name, sizeof(name), "blk.%d.attn_norm.weight", i); + if (!(l->attn_norm = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.post_attention_norm.weight", i); + if (!(l->post_attention_norm = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_conv1d.weight", i); + if (!(l->ssm_conv1d = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_a", i); + if (!(l->ssm_a = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_dt.bias", i); + if (!(l->ssm_dt = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_norm.weight", i); + if (!(l->ssm_norm = qwen35moe_load_f32(gguf, name))) + return -1; + + snprintf(name, sizeof(name), "blk.%d.attn_qkv.weight", i); + if (pn_qweight_from_gguf(&l->attn_qkv, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.attn_gate.weight", i); + if (pn_qweight_from_gguf(&l->attn_gate, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_alpha.weight", i); + if (pn_qweight_from_gguf(&l->ssm_alpha, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_beta.weight", i); + if (pn_qweight_from_gguf(&l->ssm_beta, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.ssm_out.weight", i); + if (pn_qweight_from_gguf(&l->ssm_out, gguf, name) < 0) + return -1; + + return qwen35moe_load_moe(gguf, qc, i, &l->moe); +} + +/* Load one full-attention layer. Returns 0 on success. */ +static int qwen35moe_load_attn_layer(pn_gguf_file *gguf, const pn_qwen35moe_config *qc, + int i, pn_qwen35moe_attn_layer *l) +{ + char name[128]; + + snprintf(name, sizeof(name), "blk.%d.attn_norm.weight", i); + if (!(l->attn_norm = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.post_attention_norm.weight", i); + if (!(l->post_attention_norm = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.attn_q_norm.weight", i); + if (!(l->attn_q_norm = qwen35moe_load_f32(gguf, name))) + return -1; + snprintf(name, sizeof(name), "blk.%d.attn_k_norm.weight", i); + if (!(l->attn_k_norm = qwen35moe_load_f32(gguf, name))) + return -1; + + snprintf(name, sizeof(name), "blk.%d.attn_q.weight", i); + if (pn_qweight_from_gguf(&l->attn_q, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.attn_k.weight", i); + if (pn_qweight_from_gguf(&l->attn_k, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.attn_v.weight", i); + if (pn_qweight_from_gguf(&l->attn_v, gguf, name) < 0) + return -1; + snprintf(name, sizeof(name), "blk.%d.attn_output.weight", i); + if (pn_qweight_from_gguf(&l->attn_output, gguf, name) < 0) + return -1; + + return qwen35moe_load_moe(gguf, qc, i, &l->moe); +} + +/* ============================================================ + * Public: load + * ============================================================ */ + +pn_qwen35moe_model *pn_qwen35moe_load(pn_gguf_file *gguf, pn_config *config) +{ + if (!gguf || !config) + return NULL; + + pn_qwen35moe_model *m = calloc(1, sizeof(*m)); + if (!m) + return NULL; + m->gguf = gguf; + + if (qwen35moe_read_config(gguf, &m->cfg, config) < 0) + goto fail; + + const pn_qwen35moe_config *qc = &m->cfg; + + /* Final RMSNorm. */ + if (!(m->output_norm = qwen35moe_load_f32(gguf, "output_norm.weight"))) + goto fail; + + /* Embedding + LM head. A2c host-RAM reclaim (eight_card_serving_plan): for + * UNTIED models (a separate output.weight exists) the ~2 GB F32 token_embd + * heap is only needed for the per-token embedding lookup — do that on + * demand from the mmap (pn_qwen35moe_model_embed_row) and skip the heap. + * For TIED models the embedding also IS the LM head, so it's materialized. */ + pn_gguf_tensor_info *out_info = pn_gguf_find_tensor(gguf, "output.weight"); + if (out_info) { + if (pn_qweight_from_gguf(&m->output, gguf, "output.weight") < 0) + goto fail; + /* Per-row embedding needs the tensor present + row-aligned; otherwise + * fall back to materializing so no model is ever broken. */ + pn_gguf_tensor_info *ei = pn_gguf_find_tensor(gguf, "token_embd.weight"); + int bsz = ei ? pn_gguf_block_size(ei->type) : 0; + if (ei && bsz > 0 && (qc->n_embd % bsz) == 0) { + m->token_embd = NULL; /* on-demand per-row dequant from the mmap */ + } else { + size_t embd_numel = 0; + m->token_embd = pn_gguf_dequantize_tensor(gguf, "token_embd.weight", + &embd_numel); + if (!m->token_embd) { + fprintf(stderr, "qwen35moe: failed to load token_embd.weight\n"); + goto fail; + } + } + } else { + /* Tied: materialize the embedding (also serves as the F32 LM head). */ + size_t embd_numel = 0; + m->token_embd = pn_gguf_dequantize_tensor(gguf, "token_embd.weight", + &embd_numel); + if (!m->token_embd) { + fprintf(stderr, "qwen35moe: failed to load token_embd.weight\n"); + goto fail; + } + m->output_f32 = malloc(embd_numel * sizeof(float)); + if (!m->output_f32) + goto fail; + memcpy(m->output_f32, m->token_embd, embd_numel * sizeof(float)); + m->output = (pn_qweight){ .data = m->output_f32, .type = GGUF_QUANT_F32, + .rows = qc->n_vocab, .cols = qc->n_embd, + .row_stride = (size_t)qc->n_embd * sizeof(float) }; + } + + /* Count layers per type, build the index maps. */ + m->block_type = calloc((size_t)qc->n_layer, sizeof(*m->block_type)); + m->block_index = calloc((size_t)qc->n_layer, sizeof(*m->block_index)); + if (!m->block_type || !m->block_index) + goto fail; + for (int i = 0; i < qc->n_layer; i++) { + if (qwen35moe_is_attn_layer(qc, i)) { + m->block_type[i] = PN_QWEN35MOE_LAYER_ATTN; + m->block_index[i] = m->n_attn++; + } else { + m->block_type[i] = PN_QWEN35MOE_LAYER_SSM; + m->block_index[i] = m->n_ssm++; + } + } + + m->ssm_layers = calloc((size_t)m->n_ssm, sizeof(*m->ssm_layers)); + m->attn_layers = calloc((size_t)m->n_attn, sizeof(*m->attn_layers)); + if ((m->n_ssm && !m->ssm_layers) || (m->n_attn && !m->attn_layers)) + goto fail; + + /* Honor the MTP trap: nextn_predict_layers=0 — never touch blk.N.nextn.* . */ + for (int i = 0; i < qc->n_layer; i++) { + int idx = m->block_index[i]; + int rc; + if (m->block_type[i] == PN_QWEN35MOE_LAYER_ATTN) + rc = qwen35moe_load_attn_layer(gguf, qc, i, &m->attn_layers[idx]); + else + rc = qwen35moe_load_ssm_layer(gguf, qc, i, &m->ssm_layers[idx]); + if (rc < 0) { + fprintf(stderr, "qwen35moe: failed to load layer %d\n", i); + goto fail; + } + fprintf(stderr, "\r loaded layer %d/%d", i + 1, qc->n_layer); + fflush(stderr); + } + fprintf(stderr, "\n"); + fprintf(stderr, "qwen35moe: loaded %d SSM + %d attention layers\n", + m->n_ssm, m->n_attn); + + return m; + +fail: + pn_qwen35moe_free(m); + return NULL; +} + +/* ============================================================ + * Public: free + * ============================================================ */ + +static void qwen35moe_free_moe(pn_qwen35moe_moe *moe) +{ + free(moe->gate_exps); + moe->gate_exps = NULL; + free(moe->up_exps); + moe->up_exps = NULL; + free(moe->down_exps); + moe->down_exps = NULL; +} + +void pn_qwen35moe_free(pn_qwen35moe_model *model) +{ + if (!model) + return; + +#ifdef USE_CUDA + /* Release any GPU-resident weight buffers before the CPU weights. */ + pn_qwen35moe_gpu_free(model); +#endif +#ifdef USE_METAL + /* Metal buffers wrap the mmap'd weights; drop the wrappers before the + * layer arrays that hold them go away. */ + pn_qwen35moe_metal_free(model); +#endif + + free(model->token_embd); + free(model->output_norm); + free(model->output_f32); + + if (model->ssm_layers) { + for (int i = 0; i < model->n_ssm; i++) { + pn_qwen35moe_ssm_layer *l = &model->ssm_layers[i]; + free(l->attn_norm); + free(l->post_attention_norm); + free(l->ssm_conv1d); + free(l->ssm_a); + free(l->ssm_dt); + free(l->ssm_norm); + qwen35moe_free_moe(&l->moe); + } + free(model->ssm_layers); + } + if (model->attn_layers) { + for (int i = 0; i < model->n_attn; i++) { + pn_qwen35moe_attn_layer *l = &model->attn_layers[i]; + free(l->attn_norm); + free(l->post_attention_norm); + free(l->attn_q_norm); + free(l->attn_k_norm); + qwen35moe_free_moe(&l->moe); + } + free(model->attn_layers); + } + + free(model->block_type); + free(model->block_index); + free(model); +} + +pn_gguf_file *pn_qwen35moe_gguf(const pn_qwen35moe_model *model) +{ + return model ? model->gguf : NULL; +} + +/* ============================================================ + * Internal accessors for the forward/state file (qwen35moe_forward.c) + * + * The model struct is opaque outside this file; the forward orchestration + * needs the config + per-type layer counts to size the dual state. It also + * reaches the per-type block arrays + index maps via the helpers below. + * ============================================================ */ + +const pn_qwen35moe_config *pn_qwen35moe_model_config(const pn_qwen35moe_model *m) +{ + return m ? &m->cfg : NULL; +} + +pn_qwen35moe_config *pn_qwen35moe_model_config_mut(pn_qwen35moe_model *m) +{ + return m ? &m->cfg : NULL; +} + +int pn_qwen35moe_model_accel_mv_enable(pn_qwen35moe_model *m, int enable) +{ + int was_attached; + + if (!m) + return -1; + was_attached = m->cfg.mv.fn != NULL; + if (enable) { + if (!was_attached && m->mv_detached.fn) { + m->cfg.mv = m->mv_detached; + m->mv_detached.fn = NULL; + m->mv_detached.ctx = NULL; + } + } else if (was_attached) { + m->mv_detached = m->cfg.mv; + m->cfg.mv.fn = NULL; + m->cfg.mv.ctx = NULL; + } + return was_attached; +} + +void pn_qwen35moe_model_accel_mv_forget(pn_qwen35moe_model *m) +{ + if (!m) + return; + /* BOTH copies. The shadow is the whole difference between PAUSING the seam and RETIRING + * it, and a teardown that left the shadow set would hand the freed context back the next + * time anything re-enabled the seam. */ + m->cfg.mv.fn = NULL; + m->cfg.mv.ctx = NULL; + m->mv_detached.fn = NULL; + m->mv_detached.ctx = NULL; +} + +int pn_qwen35moe_model_n_ssm(const pn_qwen35moe_model *m) +{ + return m ? m->n_ssm : 0; +} + +int pn_qwen35moe_model_n_attn(const pn_qwen35moe_model *m) +{ + return m ? m->n_attn : 0; +} + +uint64_t pn_qwen35moe_kv_bytes_per_token(const pn_qwen35moe_model *m, bool kv_q4) +{ + if (!m) + return 0; + const pn_qwen35moe_config *qc = &m->cfg; + /* Only the full-attention layers grow a per-token KV cache; the SSM layers + * hold a fixed-size recurrent state that does NOT scale with sequence + * length, so it is not part of the per-token admission cost. */ + uint64_t bytes_per_vec = kv_q4 + ? (uint64_t)pn_kvq4_row_bytes(qc->head_dim) + : (uint64_t)qc->head_dim * sizeof(float); + /* K and V, one packed vector each per KV head, per attention layer. */ + return (uint64_t)m->n_attn * (uint64_t)qc->n_head_kv * 2ULL * bytes_per_vec; +} + +/* The forward loop walks layers in global order; these expose the index maps + * and per-type weight arrays without leaking the struct layout into the header. */ +pn_qwen35moe_layer_type pn_qwen35moe_model_layer_type(const pn_qwen35moe_model *m, + int layer) +{ + return m->block_type[layer]; +} + +int pn_qwen35moe_model_layer_index(const pn_qwen35moe_model *m, int layer) +{ + return m->block_index[layer]; +} + +const pn_qwen35moe_ssm_layer *pn_qwen35moe_model_ssm_layer( + const pn_qwen35moe_model *m, int idx) +{ + return &m->ssm_layers[idx]; +} + +const pn_qwen35moe_attn_layer *pn_qwen35moe_model_attn_layer( + const pn_qwen35moe_model *m, int idx) +{ + return &m->attn_layers[idx]; +} + +const float *pn_qwen35moe_model_token_embd(const pn_qwen35moe_model *m) +{ + return m->token_embd; +} + +/* Write embedding row `token` into dst[n_embd]. Materialized F32 heap (tied + * models / fallback) → row copy; otherwise (A2c untied path) dequantize the row + * on demand straight from the mmap, so no ~2 GB F32 heap is held. */ +void pn_qwen35moe_model_embed_row(const pn_qwen35moe_model *m, uint32_t token, + float *dst) +{ + size_t c = (size_t)m->cfg.n_embd; + /* Bound the token id against the vocab. The forward-path guards test + * `(int)token >= n_vocab`, which a token in [INT_MAX+1, UINT32_MAX] slips + * past (it casts negative), so this — the single embedding chokepoint — + * must reject out-of-range ids itself: an unbounded `token * c` offset + * would read past the embedding table (OOB read into the residual stream). + * Out-of-range → emit a zero row rather than garbage. */ + if (token >= (uint32_t)m->cfg.n_vocab) { + memset(dst, 0, c * sizeof(float)); + return; + } + if (m->token_embd) { + memcpy(dst, m->token_embd + (size_t)token * c, c * sizeof(float)); + return; + } + if (pn_gguf_dequantize_row(m->gguf, "token_embd.weight", token, c, dst) != 0) { + /* NOT defensive -- this used to memset and carry on, which is how a + * BF16 model ran to completion emitting noise: every token embedded as + * a zero vector and the forward dutifully generated from nothing. A + * zero embedding is not a safe fallback, it is a silent wrong answer. + * Say so once, loudly, so the cause is in the log instead of being + * inferred from the output. */ + static int warned = 0; + if (!warned) { + warned = 1; + fprintf(stderr, "qwen35moe: token_embd.weight row %d could not be " + "dequantized — the embedding is UNAVAILABLE and generation " + "from here is meaningless. Check the tensor's quant is " + "supported by pn_dequantize.\n", token); + } + memset(dst, 0, c * sizeof(float)); + } +} + +const float *pn_qwen35moe_model_output_norm(const pn_qwen35moe_model *m) +{ + return m->output_norm; +} + +const pn_qweight *pn_qwen35moe_model_output(const pn_qwen35moe_model *m) +{ + return &m->output; +} + +/* Mutable views of the same three objects, for a GPU backend that must attach + * a device-buffer handle to every weight (pn_qweight.gpu). Kept as separate + * entry points rather than dropping const from the readers above so the + * compute path stays unable to write a weight by accident. */ +pn_qwen35moe_ssm_layer *pn_qwen35moe_model_ssm_layer_mut(pn_qwen35moe_model *m, + int idx) +{ + return &m->ssm_layers[idx]; +} + +pn_qwen35moe_attn_layer *pn_qwen35moe_model_attn_layer_mut(pn_qwen35moe_model *m, + int idx) +{ + return &m->attn_layers[idx]; +} + +pn_qweight *pn_qwen35moe_model_output_mut(pn_qwen35moe_model *m) +{ + return &m->output; +} + +pn_gguf_file *pn_qwen35moe_model_gguf(const pn_qwen35moe_model *m) +{ + return m ? m->gguf : NULL; +} + +/* ============================================================ + * Weight enumeration (see qwen35moe_internal.h) + * ============================================================ */ + +/* Emit one weight, skipping the empty slots a variant does not carry (the + * dense-FFN Bonsai family has no router and no expert arrays). An empty slot is + * not a weight, and emitting it would put a phantom in every consumer's census. */ +static void qwen35moe_emit_weight(pn_qwen35moe_weight_fn fn, void *user, + pn_qweight *w, const char *name, int layer, + int expert) +{ + if (!w || !w->data || w->rows <= 0) + return; + pn_qwen35moe_weight_ref ref = { w, name, layer, expert }; + fn(&ref, user); +} + +/* Router + routed expert slices + shared-expert SwiGLU + its scalar gate. + * n_expert is 0 for the dense-FFN variant, where only the shared slots exist. */ +static void qwen35moe_for_each_moe_weight(pn_qwen35moe_moe *moe, int layer, + int n_expert, + pn_qwen35moe_weight_fn fn, void *user) +{ + char name[96]; + + snprintf(name, sizeof(name), "blk.%d.ffn_gate_inp", layer); + qwen35moe_emit_weight(fn, user, &moe->ffn_gate_inp, name, layer, -1); + + for (int e = 0; e < n_expert; e++) { + if (moe->gate_exps) { + snprintf(name, sizeof(name), "blk.%d.ffn_gate_exps[%d]", layer, e); + qwen35moe_emit_weight(fn, user, &moe->gate_exps[e], name, layer, e); + } + if (moe->up_exps) { + snprintf(name, sizeof(name), "blk.%d.ffn_up_exps[%d]", layer, e); + qwen35moe_emit_weight(fn, user, &moe->up_exps[e], name, layer, e); + } + if (moe->down_exps) { + snprintf(name, sizeof(name), "blk.%d.ffn_down_exps[%d]", layer, e); + qwen35moe_emit_weight(fn, user, &moe->down_exps[e], name, layer, e); + } + } + + snprintf(name, sizeof(name), "blk.%d.ffn_gate_shexp", layer); + qwen35moe_emit_weight(fn, user, &moe->ffn_gate_shexp, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.ffn_up_shexp", layer); + qwen35moe_emit_weight(fn, user, &moe->ffn_up_shexp, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.ffn_down_shexp", layer); + qwen35moe_emit_weight(fn, user, &moe->ffn_down_shexp, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.ffn_gate_inp_shexp", layer); + qwen35moe_emit_weight(fn, user, &moe->ffn_gate_inp_shexp, name, layer, -1); +} + +/* One Gated-Delta-Net SSM layer's projections. The conv/decay/norm weights are + * F32 arrays owned by the model, not pn_qweights, so they are not emitted. */ +static void qwen35moe_for_each_ssm_weight(pn_qwen35moe_ssm_layer *l, int layer, + int n_expert, + pn_qwen35moe_weight_fn fn, void *user) +{ + char name[96]; + + snprintf(name, sizeof(name), "blk.%d.attn_qkv", layer); + qwen35moe_emit_weight(fn, user, &l->attn_qkv, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.attn_gate", layer); + qwen35moe_emit_weight(fn, user, &l->attn_gate, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.ssm_alpha", layer); + qwen35moe_emit_weight(fn, user, &l->ssm_alpha, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.ssm_beta", layer); + qwen35moe_emit_weight(fn, user, &l->ssm_beta, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.ssm_out", layer); + qwen35moe_emit_weight(fn, user, &l->ssm_out, name, layer, -1); + + qwen35moe_for_each_moe_weight(&l->moe, layer, n_expert, fn, user); +} + +/* One full-attention layer's projections. */ +static void qwen35moe_for_each_attn_weight(pn_qwen35moe_attn_layer *l, int layer, + int n_expert, + pn_qwen35moe_weight_fn fn, void *user) +{ + char name[96]; + + snprintf(name, sizeof(name), "blk.%d.attn_q", layer); + qwen35moe_emit_weight(fn, user, &l->attn_q, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.attn_k", layer); + qwen35moe_emit_weight(fn, user, &l->attn_k, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.attn_v", layer); + qwen35moe_emit_weight(fn, user, &l->attn_v, name, layer, -1); + snprintf(name, sizeof(name), "blk.%d.attn_output", layer); + qwen35moe_emit_weight(fn, user, &l->attn_output, name, layer, -1); + + qwen35moe_for_each_moe_weight(&l->moe, layer, n_expert, fn, user); +} + +void pn_qwen35moe_for_each_weight(pn_qwen35moe_model *m, + pn_qwen35moe_weight_fn fn, void *user) +{ + if (!m || !fn) + return; + + for (int i = 0; i < m->cfg.n_layer; i++) { + int idx = m->block_index[i]; + if (m->block_type[i] == PN_QWEN35MOE_LAYER_SSM) + qwen35moe_for_each_ssm_weight(&m->ssm_layers[idx], i, + m->cfg.n_expert, fn, user); + else + qwen35moe_for_each_attn_weight(&m->attn_layers[idx], i, + m->cfg.n_expert, fn, user); + } + qwen35moe_emit_weight(fn, user, &m->output, "output", -1, -1); +} + +/* GPU-residency handle accessors. The handle is an opaque pn_qwen35moe_gpu + * (defined in qwen35moe_forward_cuda.c); this file only stores the pointer so + * the model carries its persistent fc_buffer weights across forward calls. */ +void *pn_qwen35moe_model_gpu(const pn_qwen35moe_model *m) +{ + return m ? m->gpu : NULL; +} + +void pn_qwen35moe_model_set_gpu(pn_qwen35moe_model *m, void *gpu) +{ + if (m) + m->gpu = gpu; +} + +/* Explicit die placement (pn_load_on). Stored before the GPU upload so + * gpu_die_ids()/gpu_split_layer() in qwen35moe_forward_cuda.c can prefer it + * over the QWEN35_GPU_DIE_A/B env knobs. Capped at the struct's 8 entries + * (the forward consumes at most 2 today). */ +void pn_qwen35moe_model_set_dies(pn_qwen35moe_model *m, const int *dies, int n) +{ + if (!m) + return; + if (!dies || n <= 0) { + m->n_gpu_dies = 0; + return; + } + if (n > (int)(sizeof(m->gpu_dies) / sizeof(m->gpu_dies[0]))) + n = (int)(sizeof(m->gpu_dies) / sizeof(m->gpu_dies[0])); + for (int i = 0; i < n; i++) + m->gpu_dies[i] = dies[i]; + m->n_gpu_dies = n; +} + +int pn_qwen35moe_model_dies(const pn_qwen35moe_model *m, const int **dies_out) +{ + if (dies_out) + *dies_out = m ? m->gpu_dies : NULL; + return m ? m->n_gpu_dies : 0; +} + +#ifdef USE_CUDA +int pn_qwen35moe_model_gpu_loaded(const pn_qwen35moe_model *m) +{ + return (m && m->gpu) ? 1 : 0; +} +#endif + +/* See qwen35moe.h. Lives here (not in the CUDA file) so a non-CUDA build still + * links it; the flag is simply never read when there is no device top-k. */ +/* True when `model`'s pn_state carries a pn_qwen35moe_state — the MoE arch and + * the dense Bonsai/Ornith family, which share this state and forward. */ +int pn_qwen35moe_owns_state(const pn_model *model) +{ + if (!model) return 0; + return model->config.arch == PN_ARCH_QWEN35MOE || + model->config.arch == PN_ARCH_BONSAI; +} + +void pn_qwen35moe_state_set_full_logits(pn_state *state, int on) +{ + if (!state || !state->kv_cache) return; + ((pn_qwen35moe_state *)state->kv_cache)->full_logits = on ? 1 : 0; +} diff --git a/src/models/qwen35moe.h b/src/models/qwen35moe.h new file mode 100644 index 0000000..e406fe3 --- /dev/null +++ b/src/models/qwen35moe.h @@ -0,0 +1,824 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe.h + * @brief Qwen3.5-MoE (Qwen3-Next lineage) adapter for Pantheon — opaque model + * handle plus the pn_model/pn_state adapter API. Hybrid Gated-Delta-Net + * SSM + sparse full-attention + shared-expert MoE. + * + * @status A6: full hybrid forward + dual-state lifecycle assembled (forward + * orchestration in qwen35moe_forward.c). Runs end-to-end producing + * finite logits; numeric correctness vs the oracle is A8. + * @issues None + * @todo + * - [ ] A8: validate logits vs the llama.cpp oracle (numeric correctness) + */ + +#ifndef PN_QWEN35MOE_H +#define PN_QWEN35MOE_H + +#include +#include +#include + +#include "../core/engine.h" +#include "../formats/gguf.h" +#include "gpu_attn_path_tracker.h" +#include "gpu_graph_capture_tracker.h" + +/* Single source of truth for the env-overridable KV-window ceiling + * (QWEN35_MAX_SEQ). The KV ring is allocated up to this cap (qwen35moe_max_seq), + * so admission MUST promise the same ceiling — otherwise a QWEN35_MAX_SEQ above + * the admission cap would let the ring allocate larger than admission ever + * checked. Kept here (not in qwen35moe_forward.c) so the engine admission path + * and the CUDA admit gate share the exact number. Raised to 256K for + * long-context serving; the default window (PN_QWEN35MOE_MAX_SEQ) is unchanged, + * so a plain run reserves no more memory than before. */ +#define PN_QWEN35MOE_MAX_SEQ_CAP 262144 + +/* The hybrid KV ring's DEFAULT window (tokens) when neither a per-model ctx_cap + * nor QWEN35_MAX_SEQ is set. In the header (shared with the engine admission + * path) so admission promises exactly the window the ring is sized to. */ +#define PN_QWEN35MOE_MAX_SEQ 4096 + +/* Opaque weight/model handle. Concrete definition lives in qwen35moe.c. */ +typedef struct pn_qwen35moe_model pn_qwen35moe_model; + +/** + * Load a Qwen3.5-MoE GGUF model: parses all qwen35moe.* config keys, derives + * the per-layer type map (full-attention when (i+1) % full_attention_interval + * == 0, else SSM), and loads every per-layer + model-level tensor. + * Keeps the GGUF file open (mmap backing for quantized weights). + * + * @param gguf Open GGUF file handle (ownership stays with the caller). + * @param config Engine config to populate from GGUF metadata. + * @return Opaque model handle, or NULL on failure. + * + * Thread-safe: no (single load call) + * Reentrant: no + * Signal-safe: no + */ +pn_qwen35moe_model *pn_qwen35moe_load(pn_gguf_file *gguf, pn_config *config); + +/** + * Free a Qwen3.5-MoE model handle. Does NOT close the GGUF file — the engine + * owns that and closes it after this returns. + */ +void pn_qwen35moe_free(pn_qwen35moe_model *model); + +/** + * Return the GGUF file backing this model (mmap source for quantized weights). + * The engine uses this to close the file after pn_qwen35moe_free(). Does not + * transfer ownership. + */ +pn_gguf_file *pn_qwen35moe_gguf(const pn_qwen35moe_model *model); + +/** + * Real per-token KV growth in bytes for THIS model, across its full-attention + * layers only (the hybrid stack's SSM layers hold recurrent state, not a + * per-token-growing KV cache). This is the honest number admission must use for + * this arch — the generic PN_ADMIT_KV_BYTES_PER_TOKEN_* constants model a dense + * all-layers-KV model and badly misprice a hybrid. + * + * per_token = n_attn * n_head_kv * 2(K+V) * bytes_per_head_vec + * where bytes_per_head_vec is the packed Q4 row size (pn_kvq4_row_bytes) under + * `kv_q4`, else head_dim * sizeof(float). + * + * @param model Loaded qwen35moe/bonsai model handle. + * @param kv_q4 true if the KV cache is stored Q4 (the serving default). + * @return Whole-model KV bytes added per admitted token, or 0 if model + * is NULL / geometry is not yet available. + * + * Thread-safe: yes (reads immutable config) + * Reentrant: yes + * Signal-safe: yes + */ +uint64_t pn_qwen35moe_kv_bytes_per_token(const pn_qwen35moe_model *model, + bool kv_q4); + +/** + * Forward pass conforming to pn_forward_fn (implemented in qwen35moe_forward.c). + * Runs the full hybrid stack for one token and returns owned logits [n_vocab]. + */ +tm_tensor *pn_qwen35moe_forward(pn_model *model, uint32_t token, pn_state *state); + +#ifdef USE_CUDA +/** + * Single-die GPU forward conforming to pn_forward_fn (implemented in + * qwen35moe_forward_cuda.c). Selected by the engine when fc_available() and the + * model's weights have been uploaded via pn_qwen35moe_gpu_upload(); otherwise + * the engine keeps pn_qwen35moe_forward (CPU). Single die only; the 2-die swap + * is a stubbed seam in qwen35moe_forward_cuda.c. + */ +tm_tensor *pn_qwen35moe_forward_cuda(pn_model *model, uint32_t token, + pn_state *state); + +/** + * BATCHED prefill forward (implemented in qwen35moe_forward_cuda.c): runs n + * tokens of ONE sequence through the whole stack in a single pass — batched + * matvecs (fc_matvec_*_nx), SSM state scans, causal attention over all n + * query positions, per-token MoE routing — and returns the LAST position's + * logits [n_vocab] (owned tensor), advancing state->pos by n. The KV cache, + * SSM state and per-position logits are bit-identical to n sequential + * single-token forwards; per_pos_logits (optional, [n * n_vocab]) receives + * every position's full logits — the correctness-gate hook. Falls back + * internally to sequential single-token forwards if the batched path cannot + * run (exotic quant mix), so callers never lose correctness. + */ +tm_tensor *pn_qwen35moe_forward_batch_cuda(pn_model *model, + const uint32_t *tokens, int n, + pn_state *state, + float *per_pos_logits); + +/** + * Phase 9b — prefill chunk pipelining (QWEN35_PREFILL_PIPELINE=1, default OFF). + * + * Under a 2-die layer split the dies run strictly sequentially per chunk: the + * boundary hand-off syncs die A, the LM head syncs die B, so each idles for + * the other's half. Chunk N+1's die-A work has no dependency on chunk N's + * die-B work — the SSM recurrence and the KV rings are per-layer state and a + * layer lives on exactly one die — so the two CAN overlap. The only thing + * preventing it is the host blocking on die B for logits nobody reads. + * + * pn_qwen35moe_prefill_chunk_cuda() runs an intermediate chunk with the head + * phase and the die-B sync omitted, advancing state->pos by n and leaving die + * B's segment IN FLIGHT. Returns 0 on success, -1 if the batched path could + * not run (nothing advanced; fall back to pn_qwen35moe_forward_batch_cuda, + * which is always correct). The LAST chunk of a range must use the ordinary + * call, so the caller gets real logits and both dies end up drained. + * + * pn_qwen35moe_prefill_drain_cuda() syncs both dies. Call it before any early + * exit that reads or resets state an in-flight segment is still writing. + * + * Bit-exactness: identical kernels in an identical per-die order on identical + * inputs — only the issue timing and the discarded intermediate LM heads + * change. Nothing reassociates. + */ +int pn_qwen35moe_prefill_pipeline_enabled(void); +int pn_qwen35moe_prefill_chunk_cuda(pn_model *model, const uint32_t *tokens, + int n, pn_state *state); +void pn_qwen35moe_prefill_drain_cuda(pn_model *model); + +/** + * Phase 9c — WIDE prefill pipelining + * (QWEN35_PREFILL_PIPELINE_WIDE=1, default OFF). + * + * 9b above overlaps exactly two dies: chunk N+1's first against chunk N's last. + * That is the whole machine on a 2-die split, but on an 8-die layer split it + * leaves six dies idle at any instant, because the boundary hand-off syncs the + * sending die at every hop inside a chunk. + * + * This entry runs a whole prefill range as an n_die-stage software pipeline: at + * step T die d computes chunk T-d, so every die is working on a different chunk + * at the same time and the steady-state period is ONE die-segment instead of + * n_die of them. It owns the chunk loop, which 9b could not — a pipeline has to + * see all the chunks to keep several in flight. + * + * Legal for the same reason 9b is: die d's chunk c needs die d-1's chunk c + * (produced a step earlier) and die d's own carried KV/SSM state, which is + * per-layer and therefore per-die, and which die d advances in chunk order. + * There is no dependency from die d's chunk c to die d-1's chunk c+1. + * + * `should_abort` is polled once per step. On abort NO new chunk is injected but + * the pipeline still drains what is in flight, so the session's layers all end + * at the same position rather than half-walked; *`state->pos`* then advances by + * the tokens that fully cleared, and the call reports 1. + * + * Bit-exact against the sequential path: identical kernels in an identical + * per-die order on identical inputs, only the issue timing differs. Gate it + * with PN_PREFILL_LOGITS_HASH=1. + * + * @param chunk tokens per pipeline stage; clamped down to the q4 ring cap. + * @param logits_out receives an OWNED [n_vocab] tensor of the last position's + * logits (caller frees), NULL on abort/refusal. + * @return 0 = whole range prefilled; 1 = aborted after draining; -1 = refused + * or failed. On -1 nothing was advanced and the caller must fall back + * to the ordinary chunked path. + * + * Thread-safe: no (serialized by the caller's generation lock) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_prefill_pipeline_wide_enabled(void); +int pn_qwen35moe_prefill_range_pipelined_cuda(pn_model *model, + const uint32_t *tokens, + int n_total, pn_state *state, + int chunk, + int (*should_abort)(void *), + void *ud, + tm_tensor **logits_out); + +/** + * Speculative greedy decoding, increment 1 (temp 0, bit-exact by + * construction). verify runs [pending, draft_1..draft_{n-1}] at positions + * state->pos.. through the batched forward with per-position SSM state + * snapshots and fills argmax_out[t] with each position's exact greedy + * argmax — WITHOUT advancing state->pos or committing state. The caller + * accepts the longest prefix where draft_t == argmax_out[t-1] (plus the + * free correction/bonus token) and calls accept with n_accept in [1, n]: + * every SSM layer's dual state is rolled to its position-(n_accept-1) + * snapshot (the KV cache needs no rollback) and state->pos += n_accept. + * Accepted tokens are exactly the model's greedy argmaxes, so the emitted + * stream is IDENTICAL to plain greedy decode. verify returns -1 (no state + * touched; fall back to plain decode) if the batched path cannot run. + */ + + +int pn_qwen35moe_spec_verify(pn_model *model, const uint32_t *tokens, int n, + pn_state *state, int32_t *argmax_out); +int pn_qwen35moe_spec_accept(pn_model *model, pn_state *state, int n_accept); + +/** + * MULTI-USER DECODE, increment 2 of the batched-forward work: K users each + * decoding one token/step, processed in ONE forward pass — the weights + * stream once for all K (the serving-throughput lever). Each user keeps its + * OWN state (pn_state per user): per-slot SSM images on the GPU, per-user + * host KV cache, per-user RoPE position. BIT-EXACT per user: a user's token + * stream when batched is identical to that user decoding alone (run the + * single-user reference under QWEN35_FUSE=0x37 — the fused qknorm+rope + * kernel's known ~1-ulp contraction, same caveat as the prefill gate). + * + * Protocol (fixed K): + * 1. prefill each user's pn_state (single-token or batched prefill); + * 2. IMMEDIATELY after each user's prefill, bind it: + * pn_qwen35moe_decode_slot_bind(model, state_u, u, K) — this + * snapshots the live GPU SSM state into slot u (the host mirror is + * stale after a GPU prefill, so bind must precede any other user's + * forward); + * 3. step all users: pn_qwen35moe_decode_slots(model, tokens, states, K, + * logits) runs ONE pass, fills logits[u] (each user's own logits + * buffer, device top-k presentation like single-user decode) and + * advances each states[u]->pos by 1. + * Re-binding (or changing K) re-seeds slots; continuous batching + * (join/leave mid-flight, prefill+decode mixing) is the follow-up. + * Returns 0 on success, -1 on failure (no state corrupted on the + * validation failures; a mid-pass kernel failure is fatal to the batch). + */ +int pn_qwen35moe_decode_slot_bind(pn_model *model, pn_state *state, int slot, + int n_slots); +/** + * Move a DECODE slot's live SSM state from GPU index `src` to `dst` via D2D + * copy (conv_slots[src] → conv_slots[dst], ssm_slots likewise), then update + * the GPU's slot_state[dst] bookkeeping. Used by the dynamic-width batch + * scheduler (Phase D2) to compact the active user set to [0, n_active) when a + * user joins or leaves, without going through the stale host mirror. + * + * Must be called under the scheduler's gpu_mu. Returns 0 on success, -1 if + * src/dst are out of range or the slot buffers are not allocated. If src == + * dst, a no-op (returns 0). + */ +int pn_qwen35moe_decode_slot_move(pn_model *model, int src, int dst, + int n_slots); +int pn_qwen35moe_decode_slots(pn_model *model, const uint32_t *tokens, + pn_state **states, int n_users, + float **logits_out); + +/** + * The memory-pressure decode-concurrency ceiling (rung 2 of the reactive + * ladder): the max n_slots decode_slot_bind will accept right now. Full + * PN_QWEN35MOE_GPU_MAX_SLOTS at level OK / when memwatch is off. The batch + * scheduler sizes its slot count with this up front so the per-bind guard + * never refuses a lazily-initialized lane. + */ +int pn_qwen35moe_gpu_ladder_max_slots(void); + +/** + * PREFIX CACHING support: snapshot the live GPU-resident SSM dual state of + * `state` back into its host pn_state, so the host state is fully + * self-contained (host KV mirror + host SSM + pos). The prefix cache calls + * this on a session BEFORE another session's forward overwrites the shared + * per-layer GPU SSM buffers; the session is then restored later purely by the + * ordinary seed-on-host-change path (H2D SSM re-seed + KV re-sync from the host + * mirror) that the batched/single-token forward already runs when it sees a + * different host state. No-op (returns 0) without a GPU handle — the CPU path + * keeps the host state authoritative already. Only layers whose GPU buffer + * currently holds THIS state's image are copied. Returns 0 on success, -1 on + * failure. + */ +int pn_qwen35moe_gpu_state_to_host(pn_model *model, pn_state *state); + + + +/** + * Phase 10b (INV3): SSM-only variant of gpu_state_to_host — captures the + * SHARED single-user SSM buffers into the host mirror but leaves the KV + * mirror alone. The batch scheduler uses it at slot bind / turn finalize in + * lazy-mirror mode, where the KV rows live in the GPU slot ring and are + * seeded device-to-device on rebind (materializing them to host RAM there + * would recreate exactly the per-session host footprint the lazy mode + * removes). No-op (returns 0) without a GPU handle. + */ +int pn_qwen35moe_gpu_state_ssm_to_host(pn_model *model, pn_state *state); + +/** + * Phase 10b (INV3): QWEN35_HOST_KV_MIRROR gate. 1 = legacy EAGER host KV + * mirror (every decode token D2H-appended to host RAM); 0 (default) = LAZY — + * the GPU slot ring is authoritative while a session is bound and the host + * mirror is materialized on demand (export, sys-prefix capture, prefill + * yield, host-softmax fallback). Cached on first read. + */ +int pn_qwen35moe_host_kv_mirror_eager(void); + +/** + * PREFIX CACHING (batched path): snapshot a DECODE SLOT's live SSM state back + * into that slot's pn_state host mirror. gpu_state_to_host captures only the + * SHARED single-user SSM buffers; batched decode advances the per-slot images + * in the slot-major conv_slots/ssm_slots arrays at row `gpu_idx`, so after a + * batched turn a slot's SSM host mirror is stale. This D2H-copies + * conv_slots[gpu_idx] → st->conv_state and ssm_slots[gpu_idx] → st->ssm_state + * for every SSM layer (the inverse of decode_slot_bind's host→slot seed) and + * clears any shared-buffer host_state binding for this state, leaving the + * slot's pn_state warm + self-contained (host KV mirror + host SSM + pos) and + * delta-reusable next turn. `gpu_idx` is the slot's CURRENT GPU index + * (bs_slot.gpu_idx). Must be called owning the GPU turn (no decode step may + * advance the slots mid-copy). No-op (returns 0) without a GPU handle. + * Returns 0 on success, -1 on failure. + */ +int pn_qwen35moe_decode_slot_ssm_to_host(pn_model *model, int gpu_idx, + pn_state *state); + +/** + * Forget every GPU-resident "this state is live here" binding for `state` + * (per-layer SSM d->host_state + single-user KV kv_host_id/kv_synced_pos), so + * the next forward re-seeds all GPU shadows from the host mirror as if the + * state were fresh. A serve slot's pn_state is reused across requests and + * pn_state_reset clears only the host mirror; without this a long re-prefill + * on a reused slot reads stale GPU residue and diverges from a fresh state. + * Host bookkeeping only; call under the GPU-owner lock right after + * pn_state_reset. No-op on the CPU path (host state already authoritative). + */ +void pn_qwen35moe_gpu_forget_state(pn_model *model, pn_state *state); + +/** + * ASYNC single-token decode step (C2, QWEN35_ASYNC_SCHED): begin launches + * one token's GPU step (segment-graph replay + LM head) and returns WITHOUT + * blocking; poll reaps completion via a CUDA event and advances the 2-die + * boundary hand-off, so one 2-core host keeps up to 8 cards' generations in + * flight simultaneously instead of serializing on fc_sync barriers. + * + * begin: 0 = step in flight (caller MUST poll to completion before any + * other forward on this model), 1 = token not async-eligible (nothing + * launched — run the ordinary blocking forward, bit-identical), -1 = error. + * poll: 1 = still running (sleep briefly, poll again — never blocks), + * 0 = done (*logits_out = owned logits tensor, state->pos advanced), + * -1 = error (machine reset; abort the generation). + * + * Serialized per model by the serve layer's gen_lock; different models + * (own die contexts) interleave freely from any thread. + */ +int pn_qwen35moe_forward_step_begin(pn_model *model, uint32_t token, + pn_state *state); +int pn_qwen35moe_forward_step_poll(pn_model *model, pn_state *state, + tm_tensor **logits_out); + +/* Upload the model's weights to persistent GPU buffers (no-op without a GPU). + * Defined in qwen35moe_forward_cuda.c; declared here so the engine loader can + * call it after a successful pn_qwen35moe_load(). */ +int pn_qwen35moe_gpu_upload(pn_qwen35moe_model *model); +void pn_qwen35moe_gpu_free(pn_qwen35moe_model *model); + +/* True once GPU weights are resident on the model (upload succeeded). */ +int pn_qwen35moe_model_gpu_loaded(const pn_qwen35moe_model *model); + +/** + * Snapshot this model's CUDA-graph residency — how many of its per-layer / + * per-segment graph slots hold a live captured graph right now, how many + * FAILED to capture, and how many are gated out as INELIGIBLE. Only the third + * of those is normal-and-fine; only the second is a problem. + * + * A capture failure degrades to the direct kernel body with correct output, so + * without this counter a serve can silently shed graph coverage layer by layer + * (~1.8 ms/step for all 40 layers) and look completely healthy. + * + * @param model Loaded qwen35moe model + * @param out Filled with the per-class + summed residency + * @return 0 on success, -1 if the model has no GPU handle (CPU forward) + * + * Thread-safe: yes, lock-free — it reads per-die counters the driver threads + * write. A concurrent decode step can make the snapshot off by one slot; the + * numbers are a gauge, not an invariant. + * Reentrant: yes + * Signal-safe: no + */ +int pn_qwen35moe_gpu_graph_residency(const pn_qwen35moe_model *model, + pn_model_graph_residency *out); + +/** + * Snapshot which attention path every attention layer is on RIGHT NOW — + * on-die flash, or the host-softmax fallback — and why. + * + * The fallback is correctness-faithful and silent: the same tokens come back, + * an order of magnitude slower — 13.02 -> 1.22 tok/s measured on the depth + * bench at K=2, and worse the deeper the context. This is the only thing that can + * tell an operator it is happening. + * + * @param model Loaded qwen35moe model + * @param out Filled with the per-class + summed residency + * @return 0 on success, -1 if the model has no GPU handle (CPU forward) + * + * Thread-safe: yes, lock-free — same contract as the graph residency above. + * Reentrant: yes + * Signal-safe: no + */ +int pn_qwen35moe_gpu_attn_residency(const pn_qwen35moe_model *model, + pn_model_attn_residency *out); +#endif /* USE_CUDA */ + +#ifdef USE_METAL +/** + * Make every quantized weight of the model addressable by the Apple GPU + * (implemented in qwen35moe_forward_metal.c). Unified memory means there is no + * device copy to make: a page-aligned mmap'd weight is wrapped in place with + * fm_buffer_from_ptr and the handle stored on pn_qweight.gpu. Idempotent per + * weight — a weight that already has a buffer is left alone — so calling this + * twice is a no-op, and the CPU weights stay live either way. + * + * Deliberately does NOT create an MTLResidencySet: wiring the working set was + * measured worth ~0.7% and costs the file-backed/evictable memory posture that + * is the point of this backend (foundry/metal/qwen35moe_metal_spec.md §2). + * + * A weight whose pointer is not host-page aligned cannot take the zero-copy + * path. Those are COUNTED AND LOGGED (census on stderr) and left un-uploaded + * rather than copied, because on a real GGUF that is nearly every tensor and + * the copy would duplicate the whole model in host RAM. + * + * @param model Loaded qwen35moe/bonsai model (CPU weights resident). + * @return 0 when the walk ran (even if every weight missed zero-copy), + * -1 when there is no Metal device or the handle is unusable. + * + * Thread-safe: no (mutates every weight's gpu field) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_metal_upload(pn_qwen35moe_model *model); + +/** + * Release every Metal buffer attached by pn_qwen35moe_metal_upload. NULL-safe + * and safe on a model that was never uploaded. Frees only the wrappers — the + * mmap'd weight bytes belong to the GGUF file. + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +void pn_qwen35moe_metal_free(pn_qwen35moe_model *model); + +/** + * Metal forward conforming to pn_forward_fn (implemented in + * qwen35moe_forward_metal.c). Selected by the engine when fm_available() and + * pn_qwen35moe_metal_upload() succeeded. + * + * PHASE 0: no kernels exist yet. The token forward reports NOT-IMPLEMENTED and + * this adapter delegates the token to pn_qwen35moe_forward (CPU), so the + * emitted logits are bit-identical to a build without USE_METAL. It is a seam, + * not a GPU forward — do not read a Metal selection in the log as GPU compute. + * + * Thread-safe: no (mutates state) + * Reentrant: no + * Signal-safe: no + */ +tm_tensor *pn_qwen35moe_forward_metal(pn_model *model, uint32_t token, + pn_state *state); +#endif /* USE_METAL */ + +/** + * Require the FULL logits row for this state (constrained decoding). + * + * The die normally reduces the LM head to the top-k (id, logit) pairs so only k + * pairs cross the 0.4 GB/s host link; the host row is -inf everywhere else. A + * grammar mask must be able to force a token the model did not rank in its + * top-k, so a turn decoding under a grammar sets this for its lifetime. + * + * Costs the full n_vocab D2H per token, so set it ONLY while a grammar is live. + * No-op for a NULL state. + * + * Thread-safe: no (per-state flag; set before the turn's first forward) + */ +void pn_qwen35moe_state_set_full_logits(pn_state *state, int on); + +/** True when `model`'s pn_state is a pn_qwen35moe_state (MoE + dense Bonsai). */ +int pn_qwen35moe_owns_state(const pn_model *model); + + +/* Explicit die placement (pn_load_on, Phase 7). Stored on the model BEFORE the + * GPU upload so its die selection prefers the placement over the + * QWEN35_GPU_DIE_A/B env knobs. Unconditional (bookkeeping without CUDA); + * declared here so the engine loader can call it. n == 0 clears. */ +void pn_qwen35moe_model_set_dies(pn_qwen35moe_model *m, const int *dies, int n); +int pn_qwen35moe_model_dies(const pn_qwen35moe_model *m, const int **dies_out); + +/* Dual-state lifecycle: per-SSM-layer conv/ssm state + per-attention-layer KV + * cache + per-token scratch arena (implemented in qwen35moe_forward.c). */ +void *pn_qwen35moe_state_new(void *weights); +void pn_qwen35moe_state_free(void *state); +void pn_qwen35moe_state_reset(void *state); + +/* ============================================================ + * Host-side state snapshot at a token position (system-prefix caching) — + * implemented in qwen35moe_state_snapshot.c. Pure host memory: on the GPU path + * the caller must make the host mirror authoritative FIRST (snapshot after + * pn_qwen35moe_gpu_state_to_host) and, after a restore, forget the state's GPU + * bindings (pn_qwen35moe_gpu_forget_state) so the next forward re-seeds from + * the restored mirror — a restore to a NON-ZERO pos does not trigger the + * pos==0 restart re-sync on its own. + * ============================================================ */ + +/** + * Deep-copy the dual state `state` (a pn_qwen35moe_state*) as it stands at + * position `pos`: every SSM layer's conv/recurrent state, every attention + * layer's KV rows [0, pos), and the full fp16 sink/window shadows (q4 mode — + * the window ring is later overwritten, so it must be captured). + * + * @param state Dual state (host mirror must be current). + * @param pos Position the state sits at (> 0, <= every layer's max_seq). + * @return Opaque snapshot (free with pn_qwen35moe_state_snapshot_free), + * or NULL on failure. + * + * Thread-safe: no + */ +void *pn_qwen35moe_state_snapshot(const void *state, int pos); + +/** + * Restore a snapshot into `state`'s host buffers (dims must match exactly — + * same model, KV mode and window) and mark the host mirror authoritative + * (kv_mirror_stale cleared). The caller sets pn_state->pos to the snapshot + * position and forgets the GPU bindings (see above). + * + * @return 0 on success, -1 on dimension mismatch / bad args. + * + * Thread-safe: no + */ +int pn_qwen35moe_state_restore(void *state, const void *snap); + +/** The position a snapshot was taken at, or -1. */ +int pn_qwen35moe_state_snapshot_pos(const void *snap); + +/** Free a snapshot. NULL-safe. */ +void pn_qwen35moe_state_snapshot_free(void *snap); + +/** + * Serialize a snapshot (the scattered per-layer buffers produced by + * pn_qwen35moe_state_snapshot) into ONE contiguous little-endian blob for the + * session warm-tier (K80 serving-ops Phase 7): a fixed header + * {magic, format_version, weights_version, pos, n_ssm, n_attn} followed by + * every SSM layer's dims+conv+ssm bytes and every attention layer's + * dims+KV-rows (+q4 sink/window shadows) in layer order. The blob is + * self-describing (all dims are in it) so pn_qwen35moe_snap_deserialize is the + * exact inverse — the round-trip snap→bytes→snap is byte-identical. + * + * @param snap Snapshot to serialize (from pn_qwen35moe_state_snapshot). + * @param weights_version Model-identity tag stamped in the header (see + * mr_weights_version); the restore side rejects a blob + * whose tag does not match the target model. + * @param bytes_out Out: malloc'd blob (caller frees). NULL on failure. + * @param len_out Out: blob length in bytes. + * @return 0 on success, -1 on bad args / allocation failure. + * + * Thread-safe: no + */ +int pn_qwen35moe_snap_serialize(const void *snap, uint64_t weights_version, + unsigned char **bytes_out, size_t *len_out); + +/** + * Deserialize a blob produced by pn_qwen35moe_snap_serialize back into an + * opaque snapshot (free with pn_qwen35moe_state_snapshot_free). Validates the + * magic, format version, and internal length consistency; a truncated, + * corrupt, or wrong-format blob is rejected (NULL). Does NOT check + * weights_version — the caller compares *weights_version_out against the target + * model's tag and refuses a mismatch (so it can quote a clean error). + * + * @param bytes Blob bytes. + * @param len Blob length. + * @param weights_version_out Out (nullable): the header's weights_version tag. + * @return Opaque snapshot, or NULL on any validation failure. + * + * Thread-safe: no + */ +void *pn_qwen35moe_snap_deserialize(const unsigned char *bytes, size_t len, + uint64_t *weights_version_out); + + +#ifdef USE_TT +/** + * STEP 4 S2 (Tenstorrent): one greedy speculative step — verify [seed, drafts...] at + * state->pos.., accept the longest draft prefix equal to the model's greedy tokens, commit the + * seed + accepted drafts (state->pos advances by that many) and roll the carried state back + * to the last committed row. `emit` receives the accepted drafts then the model's next token + * (uncommitted: the next call's seed). @return the number of tokens in `emit`, or -1 + * (unsupported / failed: the caller falls back to plain decode). + */ +int pn_qwen35moe_tt_spec_step(pn_model *model, pn_state *state, uint32_t seed, + const uint32_t *drafts, int n_drafts, uint32_t *emit); +/** + * A verify row's chooser for SPECULATIVE SAMPLING: given row r's sparse logits (the same + * top-N (index, value) pairs a plain decode step's sparse row carries), return the token the + * engine's sampler picks — called once per EMITTED token, in order, so the sampler's RNG and + * repeat window advance exactly as plain decode's do (shared randomness: a draft is accepted + * iff plain sampling would have drawn it). + */ +typedef uint32_t (*pn_spec_choose_fn)(void *ctx, const uint32_t *index, const float *value, + uint32_t count); +/** A chooser's answer for "this row's top-N cannot decide the draw exactly" (rows >= 1 only: + * the cycle then ends with the previous row's accepted draft as the next, uncommitted + * token, and the next cycle meets this distribution again as its row 0, whose dense row + * can be refilled). */ +#define PN_SPEC_CHOOSE_DEFER 0xFFFFFFFFu +/** pn_qwen35moe_tt_spec_step with a chooser (NULL = greedy). */ +int pn_qwen35moe_tt_spec_step_ex(pn_model *model, pn_state *state, uint32_t seed, + const uint32_t *drafts, int n_drafts, uint32_t *emit, + pn_spec_choose_fn choose, void *choose_ctx); +/** True when a greedy pick over the exact top-N row equals the dense argmax for these params: + * temperature <= 0 and no logit surgery that can promote a token from outside the top-N + * (logit_bias, presence/frequency penalties, a grammar hook). */ +bool pn_qwen35moe_tt_greedy_exact_on_topn(const pn_sampling_params *params); +/** Per request, before the prompt's forwards (the prefill's last row is token 1): the logit + * rows. PN_TT_SPARSE_LOGITS unset: top-8 for exact greedy, top-64 for sampled requests with + * 1 <= top_k <= 64 (no bias / grammar / logprobs), dense otherwise; =1 forces top-N, =0 + * dense. @return the N chosen, 0 = dense. */ +uint32_t pn_qwen35moe_tt_select_rows(pn_model *model, const pn_sampling_params *params, + bool needs_full_logits); +/** pn_sampling_params.dense_row for this forward: the last head's logit row 0, dense + * (`model_ctx` is the pn_model). */ +int pn_qwen35moe_tt_dense_row(void *model_ctx, float *out, int n_vocab); +/** pn_sampling_params.dense_chunks for this forward: only the 16-entry chunks of the + * dense row 0 marked in `need` (`model_ctx` is the pn_model). */ +int pn_qwen35moe_tt_dense_chunks(void *model_ctx, const unsigned char *need, float *out, + int n_vocab); +/** True when the verify can hand a chooser each row's sparse logits AND plain decode's rows + * are the same sparse presentation (`PN_TT_SPARSE_LOGITS`): speculative sampling is then + * identical to plain sampling. */ +bool pn_qwen35moe_tt_spec_can_sample(pn_model *model); +/** PM 5: keep the model's resident card families armed while idle (serve tick). */ +int pn_qwen35moe_keepalive_tt(pn_model *model); +/** PM 6: a short internal prefill + 2 tokens at serve start, so the families armed during the + * load (and idle past the retire limit by the time the serve listens) are re-armed before the + * first request instead of inside it. Leaves no session state behind. `PN_TT_WARM=0` off. */ +int pn_qwen35moe_warm_tt(pn_model *model); +/** RED `spec_lookup_unverified`: arm the next verify cycle's unverified (wrong) accept. */ +void pn_qwen35moe_tt_red_accept_next_cycle(void); + +/** STEP 4 S2b: up to `k` drafts for the token after `seed` (at state->pos) from the MTP head + * (`PN_TT_MTP`), after its owed catch-up. @return the count, or -1 (no MTP: draft elsewhere) */ +int pn_qwen35moe_tt_mtp_draft(pn_model *model, pn_state *state, uint32_t seed, int k, + uint32_t *drafts); + +/** + * Put this model's MATMULS on a Tenstorrent Blackhole card (qwen35moe_forward_tt.c). + * + * Opens the card, turns the checkpoint into a cached FTTW artifact if one is not already + * beside it, binds every matmul weight by name, and installs the `qwen35moe_mv` seam so the + * forward's matvecs leave for the card. Nothing else moves: the Gated-Delta-Net recurrence, + * the conv1d, the attention softmax and every norm stay on the host, running the CPU + * oracle's own code on its own fp32 state — so the two arms differ in the matmuls alone. + * + * Refuses rather than half-working. The MoE branch, a geometry that does not divide by the + * 32-wide tile, and a binding that did not resolve are three DIFFERENT refusals, because + * each sends a different person to a different fix. + * + * @return 0 on success, -1 on refusal or failure (the model stays on the CPU). + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35moe_tt_upload(pn_model *model); + +/* 1 if the model has a live Tenstorrent residency attached. */ +int pn_qwen35moe_model_tt_loaded(const pn_model *model); + +/** + * 1 if this model's attention K/V lives in CARD DRAM rather than in the host state. + * + * **THE HOST MIRROR IS NOT WRITTEN ON THAT ARM.** `qwen35moe_resident_tt.c` fills + * `st->kv[idx]` only under `PN_TT_ATTN_COMPARE`, because the whole point of on-card attention + * is that 32,768 B a position stops crossing the link — a 256k session would be 16 GiB of + * host mirror on a 7.1 GB box. Anything that reads the host K/V as if it were the session + * must therefore ASK first: the system-prefix snapshot and the session export both serialise + * `st->kv`, and on the card arm they would capture zeros, restore them, and produce a fluent + * wrong answer with nothing refusing anywhere. + * + * @return 1 when the K/V is on the card, 0 otherwise (no residency, or host attention) + * + * Thread-safe: yes Reentrant: yes Signal-safe: no + */ +int pn_qwen35moe_model_tt_kv_on_card(const pn_model *model); + +/** + * MULTI-TURN: resume `state` from the card's prefill checkpoint for a prompt `tokens[0, n)`. + * + * The checkpoint is the carried GDN state at the last full pass boundary of this sequence's + * last batched prefill, in card DRAM (never on the host's disk). When it belongs to `state`, + * the card still holds `state`'s sequence, it lies below `n` and its tokens are a prefix of + * `tokens`, the carried state is copied back and `state->pos` set to it; a batched prefill + * from there runs the passes a cold prefill of `tokens` would, so the output is the cold one. + * + * @return the resumed position (> 0); 0 when nothing applies (nothing changed); -1 when the + * restore copy failed (the card state is undefined: start the sequence over) + */ +int pn_qwen35moe_tt_checkpoint_resume(pn_model *model, pn_state *state, const uint32_t *tokens, + int n); + +/** + * MULTI-TURN: the next prefill of `state` keeps its checkpoint at or below `ceiling` — the end + * of the prompt's history, i.e. where the chat template's forced generation prompt (the + * "\n" after the assistant header) begins. A client resends that turn WITHOUT those + * tokens, so a checkpoint above `ceiling` could never be resumed and the next turn would + * re-prefill the whole conversation. Applies to that one prefill; <= 0 sets no ceiling. + */ +void pn_qwen35moe_tt_checkpoint_ceiling(pn_model *model, const pn_state *state, int ceiling); + +/** 1 when the card's K/V and carried state hold `state`'s sequence (the last forward on the + * card was for it), else 0. An append that continues a sequence needs it. */ +int pn_qwen35moe_tt_card_holds(const pn_model *model, const pn_state *state); + +/* Release the residency and detach. Never parks the core. */ +void pn_qwen35moe_tt_free(pn_model *model); + +/** + * One decode step with the matmuls on the card, conforming to pn_forward_fn. + * + * A DISTINCT function pointer from the CPU forward even though it delegates to it: the + * engine classifies a residency by that pointer, and byte-identical output has never been + * evidence that a card ran anything. + */ +tm_tensor *pn_qwen35moe_forward_tt(pn_model *model, uint32_t token, pn_state *state); + +/** + * BATCHED PREFILL: up to 32 prompt positions through one set of projections. + * + * DEFAULT OFF (`PN_TT_PREFILL=1` arms it) and it REFUSES rather than falls back — a NULL + * return means the prompt was not touched and the caller's per-token loop is what runs. + * Ornith's two mixers are both single-position on the card, so a batch buys the projections, + * the norms, the SwiGLU and the residual adds, and pays a per-layer per-position link round + * trip to reach each row. `qwen35moe_prefill_tt.c` states that trade and measures it. + * + * @return the LAST prompt position's logit row (caller owns), or NULL for NOT TAKEN + */ +tm_tensor *pn_qwen35moe_prefill_tt(pn_model *model, const uint32_t *tokens, int n_tokens, + pn_state *state); + +/** + * What the LAST `pn_qwen35moe_prefill_tt()` call actually batched. It returns logits even + * when its short tail pass aborted and the prompt ran per token, so a caller that must know + * the batched path ran — a causality gate — reads this, not the return value. + * + * @param model the loaded model + * @return positions that call ran through batched passes; -1 if it declined, aborted + * a pass, never ran, or `model` is not on the TT forward + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_prefill_tt_last_batched(const pn_model *model); + +/** + * The matmul split for the run so far: how many went to the card, how many the seam + * declined (a weight it does not hold), and how many the card refused. + * + * An acceptance run reads `on_host == 0 && failed == 0` alongside a non-zero `on_card`. + * This exists because the card and a silent CPU fallback have returned the same bytes. + */ +void pn_qwen35moe_tt_matmul_counts(const pn_model *model, uint64_t *on_card, + uint64_t *on_host, uint64_t *failed); +#endif /* USE_TT */ + +/** + * Detach or reattach the accelerator's matvec seam, and report what it was. + * + * **THIS FAMILY'S ACCELERATOR IS NOT A SEPARATE FORWARD — IT IS A HOOK ON THE SHARED + * CONFIG.** `qwen35moe_mv()` consults `cfg->mv.fn` on every projection, and the upload + * installs the card's there. So a process whose card is armed has NO CPU forward: calling + * `pn_qwen35moe_forward` or `pn_bonsai_forward` still routes all 249 projections to the + * card. A parity harness that scores "the CPU forward" against "the TT forward" in one + * process is therefore scoring the card against the card unless it detaches this first — + * and against the RESIDENT-ACTIVATION arm it does not merely read as parity, it segfaults, + * because that arm keeps the residual stream in card DRAM and the host path hands the seam + * host buffers. + * + * `pn_qwen35moe_forward_tt` does not use the seam on the resident arm (it drives the card + * directly through `pn_qwen35_tt_resident_forward`), so detaching the seam gives a genuine + * CPU oracle without disturbing the accelerator arm. + * + * @param enable 0 detaches the seam, non-zero restores whatever was detached + * @return 1 if the seam was attached before this call, 0 if not, -1 on a bad model + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35moe_model_accel_mv_enable(pn_qwen35moe_model *m, int enable); + +/** + * FORGET the accelerator seam entirely — both the live hook and the detached shadow. + * + * `..._mv_enable(m, 0)` is a PAUSE: it moves the hook into a shadow copy so a later + * `enable(m, 1)` can put it back, which is exactly what a parity harness wants around an + * oracle call. An accelerator being TORN DOWN wants the opposite, and the difference is a + * use-after-free: the hook's context is the accelerator object, so freeing that object while + * either copy still names it leaves the next projection reading freed memory. A teardown that + * only paused the seam would still be holding the pointer in the shadow. + * + * Call this before freeing whatever `mv.ctx` points at. Idempotent. + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +void pn_qwen35moe_model_accel_mv_forget(pn_qwen35moe_model *m); + +#endif /* PN_QWEN35MOE_H */ diff --git a/src/models/qwen35moe_attn.c b/src/models/qwen35moe_attn.c new file mode 100644 index 0000000..6b87b26 --- /dev/null +++ b/src/models/qwen35moe_attn.c @@ -0,0 +1,240 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_attn.c + * @brief Full-attention block for Qwen3.5-MoE, one token at a time. + * + * Written from a behaviour specification (plans/cleanroom_r2_qwen35_spec.md); no ggml or llama.cpp source was consulted. + * + * @status None + * @issues A failure at the scores allocation leaves the KV appended and k mutated. Negative + * pos is undefined. Q4 mode with head_dim > 256 overflows the worker's stack scratch. + * @todo None + */ + +#include "qwen35moe_internal.h" +#include "qwen35moe_kv_q4.h" + +#include +#include +#include + +#include "../accel/simd.h" +#include "../core/qweight.h" +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +/* Per-head RMSNorm in place; the weight is shared by every head. */ +static void attn_qk_norm(float *data, int n_heads, int head_dim, const float *weight, + float eps) { + for (int h = 0; h < n_heads; h++) { + float *head = data + (size_t)h * head_dim; + pn_vrms_norm_f32(head, weight, head, head_dim, eps); + } +} + +/* Partial rotary embedding over the first rot dims of each head, pairing dim j with + * dim j + rot/2. rope_dim outside (0, head_dim) means full rotation. */ +static void attn_rope(float *data, int n_heads, int head_dim, int rope_dim, int pos, + float theta) { + int rot = (rope_dim > 0 && rope_dim < head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + (size_t)h * head_dim; + for (int j = 0; j < half; j++) { + float freq = 1.0f / powf(theta, (float)(2 * j) / (float)rot); + float angle = (float)pos * freq; + float c = cosf(angle); + float s = sinf(angle); + float x0 = head[j]; + float x1 = head[j + half]; + head[j] = x0 * c - x1 * s; + head[j + half] = x0 * s + x1 * c; + } + } +} + +/* Inputs of the per-query-head attention worker. */ +typedef struct { + const float *q; + const float *k_cache; + const float *v_cache; + const uint8_t *kq; + const uint8_t *vq; + const uint16_t *k_sink; + const uint16_t *v_sink; + const uint16_t *k_win; + const uint16_t *v_win; + int q4; + int sink; + int win; + int cur_pos; + float *attn_out; + float *scores_buf; + int hd; + int n_kv_groups; + int max_seq; + int seq_len; + float scale; +} attn_heads_args; + +/* Scores, softmax and weighted V sum for query heads [start, end). */ +static void attn_heads(int start, int end, void *arg) { + const attn_heads_args *a = (const attn_heads_args *)arg; + int hd = a->hd; + int max_seq = a->max_seq; + int seq_len = a->seq_len; + float kbuf[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + float vbuf[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + for (int qh = start; qh < end; qh++) { + int kv_h = qh / a->n_kv_groups; + const float *q_vec = a->q + (size_t)qh * hd; + float *scores = a->scores_buf + (size_t)qh * max_seq; + float *out = a->attn_out + (size_t)qh * hd; + const float *k_base = a->q4 ? NULL : a->k_cache + (size_t)kv_h * max_seq * hd; + const float *v_base = a->q4 ? NULL : a->v_cache + (size_t)kv_h * max_seq * hd; + + for (int s = 0; s < seq_len; s++) { + const float *k_row; + if (a->q4) { + pn_kvq4_load_row(a->kq, a->k_sink, a->k_win, kv_h, max_seq, s, a->cur_pos, + kbuf, hd, a->sink, a->win); + k_row = kbuf; + } else { + k_row = k_base + (size_t)s * hd; + } + scores[s] = pn_dot_f32(q_vec, k_row, hd) * a->scale; + } + + float max = scores[0]; + for (int s = 1; s < seq_len; s++) + if (scores[s] > max) + max = scores[s]; + double sum = 0.0; + for (int s = 0; s < seq_len; s++) { + scores[s] = expf(scores[s] - max); + sum += (double)scores[s]; + } + float inv = (float)(1.0 / sum); + + memset(out, 0, (size_t)hd * sizeof(*out)); + for (int s = 0; s < seq_len; s++) { + float w = scores[s] * inv; + const float *v_row; + if (a->q4) { + pn_kvq4_load_row(a->vq, a->v_sink, a->v_win, kv_h, max_seq, s, a->cur_pos, + vbuf, hd, a->sink, a->win); + v_row = vbuf; + } else { + v_row = v_base + (size_t)s * hd; + } + for (int d = 0; d < hd; d++) + out[d] += w * v_row[d]; + } + } +} + +/* Append this token's K and V (per KV head) at the cache slot. */ +static void attn_kv_append(pn_qwen35moe_kv *kv, const float *k, const float *v, int nkv, + int hd, int max_seq, int cache_pos, int sink, int win) { + for (int h = 0; h < nkv; h++) { + if (kv->q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, h, max_seq, cache_pos, + k + (size_t)h * hd, hd, sink, win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, h, max_seq, cache_pos, + v + (size_t)h * hd, hd, sink, win); + } else { + size_t slot = ((size_t)h * max_seq + cache_pos) * hd; + memcpy(kv->k + slot, k + (size_t)h * hd, (size_t)hd * sizeof(*kv->k)); + memcpy(kv->v + slot, v + (size_t)h * hd, (size_t)hd * sizeof(*kv->v)); + } + } +} + +int pn_qwen35moe_attn_core(const pn_qwen35moe_attn_layer *layer, + const pn_qwen35moe_config *cfg, + const float *qg, float *k, float *v, + pn_qwen35moe_kv *kv, int pos, pn_arena *arena, + float *attn_out, struct fw_pool *pool) { + if (!layer || !cfg || !qg || !k || !v || !kv || !arena || !attn_out) + return -1; + int nh = cfg->n_head; + int nkv = cfg->n_head_kv; + int hd = cfg->head_dim; + int groups = nh / nkv; + int max_seq = kv->max_seq; + int qg_stride = 2 * hd; + + float *q = pn_arena_alloc_f32(arena, nh * hd); + float *z = pn_arena_alloc_f32(arena, nh * hd); + if (!q || !z) + return -1; + for (int h = 0; h < nh; h++) { + const float *src = qg + (size_t)h * qg_stride; + memcpy(q + (size_t)h * hd, src, (size_t)hd * sizeof(*q)); + memcpy(z + (size_t)h * hd, src + hd, (size_t)hd * sizeof(*z)); + } + + attn_qk_norm(q, nh, hd, layer->attn_q_norm, cfg->rms_norm_eps); + attn_qk_norm(k, nkv, hd, layer->attn_k_norm, cfg->rms_norm_eps); + attn_rope(q, nh, hd, cfg->rope_dim, pos, cfg->rope_theta); + attn_rope(k, nkv, hd, cfg->rope_dim, pos, cfg->rope_theta); + + int cache_pos = pos % max_seq; + int sink = kv->q4 ? pn_kvq4_sink() : 0; + int win = kv->q4 ? pn_kvq4_win() : 0; + attn_kv_append(kv, k, v, nkv, hd, max_seq, cache_pos, sink, win); + + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + float *scores_buf = pn_arena_alloc_f32(arena, nh * max_seq); + if (!scores_buf) + return -1; + + attn_heads_args args = { + .q = q, .k_cache = kv->k, .v_cache = kv->v, + .kq = kv->kq, .vq = kv->vq, + .k_sink = kv->k_sink, .v_sink = kv->v_sink, + .k_win = kv->k_win, .v_win = kv->v_win, + .q4 = kv->q4, .sink = sink, .win = win, .cur_pos = pos, + .attn_out = attn_out, .scores_buf = scores_buf, + .hd = hd, .n_kv_groups = groups, .max_seq = max_seq, .seq_len = seq_len, + .scale = 1.0f / sqrtf((float)hd), + }; +#ifdef USE_THREADS + if (pool && nh >= 4) + fw_for(pool, nh, attn_heads, &args); + else + attn_heads(0, nh, &args); +#else + (void)pool; + attn_heads(0, nh, &args); +#endif + + for (int i = 0; i < nh * hd; i++) { + float g = 1.0f / (1.0f + expf(-z[i])); + attn_out[i] *= g; + } + return 0; +} + +int pn_qwen35moe_attn_block(const pn_qwen35moe_attn_layer *layer, + const pn_qwen35moe_config *cfg, const float *x, + pn_qwen35moe_kv *kv, int pos, pn_arena *arena, + float *out, struct fw_pool *pool) { + if (!layer || !cfg || !x || !kv || !arena || !out) + return -1; + float *qg = pn_arena_alloc_f32(arena, layer->attn_q.rows); + float *k = pn_arena_alloc_f32(arena, layer->attn_k.rows); + float *v = pn_arena_alloc_f32(arena, layer->attn_v.rows); + float *attn_out = pn_arena_alloc_f32(arena, cfg->n_head * cfg->head_dim); + if (!qg || !k || !v || !attn_out) + return -1; + qwen35moe_mv(cfg, &layer->attn_q, x, qg); + qwen35moe_mv(cfg, &layer->attn_k, x, k); + qwen35moe_mv(cfg, &layer->attn_v, x, v); + if (pn_qwen35moe_attn_core(layer, cfg, qg, k, v, kv, pos, arena, attn_out, pool) < 0) + return -1; + qwen35moe_mv(cfg, &layer->attn_output, attn_out, out); + return 0; +} diff --git a/src/models/qwen35moe_attn_tt.c b/src/models/qwen35moe_attn_tt.c new file mode 100644 index 0000000..22c894d --- /dev/null +++ b/src/models/qwen35moe_attn_tt.c @@ -0,0 +1,490 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_attn_tt.c + * @brief Ornith's FULL-ATTENTION block on the card: the de-interleaved Q/gate projections, + * the per-head QK-RMSNorms, partial NeoX RoPE, the KV append, flash decode and the + * SIGMOID output gate — with the KV cache itself living in card DRAM. + * + * The op family is `src/accel/tt/pn_tt_attention.c` and it already existed, green on + * silicon, for MiniCPM5-1B. This file is what Ornith's SHAPE needs on top of it. + * + * --------------------------------------------------------------------------- + * WHY THIS IS THE PHASE WHOSE VALUE DOES NOT EVAPORATE + * --------------------------------------------------------------------------- + * Host attention is **O(context) at 0.53 ms per position** — measured as a straight line + * (14.80 ms/token at position ~25, 19.80 at ~35, 26.27 at ~46.5), 8 layers re-reading + * 262,144 B of fp32 KV per position. Every card-work win this project has made is worth + * ~1.04x at 8k context, because at 8k the host attention core alone is 4.4 s of a 4.8 s + * token. This block is the only one whose gain GROWS with the context the product runs at. + * + * --------------------------------------------------------------------------- + * THE FOUR THINGS ORNITH'S SHAPE NEEDS, AND WHERE EACH LANDED + * --------------------------------------------------------------------------- + * 1. **The joint Q+gate projection, de-interleaved.** `blk.N.attn_q.weight` emits + * `[h0 Q(256) | h0 gate(256) | h1 Q | ...]`, and NO op downstream can read that in + * place: the QK-norm reads head `h` as row block `h` at tiles `h*8` consecutive, and + * flash decode reads a KV group as one contiguous run of `q_rows*dim_tiles = 16` tiles. + * Over the joint strip a group's run is head `2g`'s Q followed by head `2g`'s GATE. + * `foundry/tt/tests/test_attention_ops.c` asserts exactly that. + * **Solved in the WEIGHT**, at load: two column-permuted copies via + * `pn_tt_device_stage_weight_columns()`, so the projection emits Q and the gate already + * apart. Zero per-token cost, no kernel change. `pn_tt_column_runs.h` prices the three + * candidates against each other. + * 2. **Per-head QK-RMSNorm on both Q and K**, at `head_dim/32 = 8` tile columns. The + * existing per-head reduce residency is placed for the GDN's Wt=4 and REFUSES any other + * width, so this is a SECOND slot on its own core. It is **bf16**, deliberately — see + * `pn_tt_head_norm.h`: flash decode is bf16 end to end, so an fp32 norm would need a + * render either side of it that the card does not have and would be re-rounded by the + * very next op. + * 3. **The SIGMOID output gate.** `sigmoid(z) * attn` is NOT `silu(z) * attn`, so the + * SwiGLU member cannot serve it; it is SIGMOID then MUL, two descriptors on the existing + * elementwise residency, no kernel source change and no blob regeneration. + * 4. The all-Float32 program question is deliberately NOT reopened here. It is the same + * strip-bridge item the GDN norms are blocked on, and (2) is the argument for why + * attention is on the other side of it. + * + * **A FIFTH THING WAS SCOPED AND IS NOT REAL.** The spec's "GQA repeat — interleaved, + * `kh = vh % 16`" describes the GDN block's head repeat, NOT the attention layers'. + * Pantheon's own attention oracle is `kv_h = qh / n_kv_groups` (`qwen35moe_attn.c`), which is + * CONTIGUOUS and is exactly what `pn_tt_attention()` issues. Checked head by head in the + * foundry gate rather than read off the spec. + * + * --------------------------------------------------------------------------- + * WHAT IT IS OPT-IN, AND IT IS NOT A HEDGE + * --------------------------------------------------------------------------- + * `PN_TT_ONCARD=all` does not select attention and must not. This arm moves WHERE THE KV + * CACHE LIVES (card DRAM, bf16) and changes the order the attention sum accumulates in + * (blocked online softmax against a two-pass host softmax whose denominator is a double). + * The host cache is fp32 and may be Q4. It is a DIFFERENT FORWARD, and on top of that the + * QK-norm's RMS member measured 3.165e-03 at Wt=4 against a 1e-3 gate. So the `cmp` bar for + * this arm is not the shipped path's: the discriminating instrument is the CROSSED CONTROL + * below, which runs both arms over the same q/k/v and reports max|delta| PER HEAD. + * + * @status WRITTEN AND BUILDS; NOT RUN ON SILICON. Authored in a wave with exclusive card + * access held by another agent, so every figure here is DERIVED or inherited from a + * prior measurement and none of it is a measurement of this code. + * @issues The card holds ONE K/V region set (`pn_tt_attention.c`), so two sequences + * alternating is detected and refused rather than served. + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include +#include + +#include "../accel/tt/pn_tt_column_runs.h" +#include "../accel/tt/pn_tt_head_norm.h" + +/** Elements per tile row, and the tile count of a width. */ +#define QWEN35_ATTN_TILE 32u + +/** A crossed control against the host arm, per head. `PN_TT_ATTN_COMPARE=1`. It costs the + * readback it exists to remove, so it is a knob exactly like `PN_TT_TRACE`. */ +#define QWEN35_ATTN_COMPARE_ENV "PN_TT_ATTN_COMPARE" + +static uint32_t qwen35_attn_tiles(uint32_t width) +{ + return (width + QWEN35_ATTN_TILE - 1u) / QWEN35_ATTN_TILE; +} + +/* ================================================================ + * Arming + * ================================================================ */ + +/** + * Stage the two de-interleaved halves of one layer's joint Q+gate projection. + * + * The run lists are built by `pn_tt_column_runs_field()` and checked injective by the + * placement path, so a shape this function got wrong is a refusal at load rather than a + * projection that is quietly missing one head. + */ +int pn_qwen35_tt_attn_split_q(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, const char *name, + bool extra, pn_qwen35_tt_layer *slot) +{ + pn_tt_column_run runs[PN_TT_COLUMN_RUNS_MAX]; + const uint32_t head_tiles = qwen35_attn_tiles((uint32_t)cfg->head_dim); + const uint32_t joint_tiles = 2u * head_tiles; + const uint32_t heads = (uint32_t)cfg->n_head; + int (*stage)(pn_tt_device *, const char *, const pn_tt_column_run *, uint32_t, + pn_tt_weight *) = + extra ? pn_tt_device_stage_extra_columns : pn_tt_device_stage_weight_columns; + + if (pn_tt_column_runs_field(heads, joint_tiles, 0u, head_tiles, runs, + PN_TT_COLUMN_RUNS_MAX) != heads || + stage(g->device, name, runs, heads, &slot->attn_q_only) != 0) { + fprintf(stderr, "qwen35_tt: could not stage the Q half of %s\n", name); + return -1; + } + if (pn_tt_column_runs_field(heads, joint_tiles, head_tiles, head_tiles, runs, + PN_TT_COLUMN_RUNS_MAX) != heads || + stage(g->device, name, runs, heads, &slot->attn_gate_only) != 0) { + fprintf(stderr, "qwen35_tt: could not stage the gate half of %s\n", name); + return -1; + } + /* The two halves must reconstruct the joint weight's width exactly. A `head_dim` that + * does not divide the projection is the one shape mistake the run list cannot see. + * (An extra-artifact layer has no joint weight placed: its width is the two halves'.) */ + if (extra) + slot->attn_q.tile_cols = slot->attn_q_only.tile_cols + slot->attn_gate_only.tile_cols; + if (slot->attn_q_only.tile_cols + slot->attn_gate_only.tile_cols != + slot->attn_q.tile_cols) { + fprintf(stderr, "qwen35_tt: %s is %u tile columns but 2 x %u heads x %u is %u — the " + "Q+gate interleave is not what this layer's weight holds\n", name, + slot->attn_q.tile_cols, heads, head_tiles, + slot->attn_q_only.tile_cols + slot->attn_gate_only.tile_cols); + return -1; + } + return 0; +} + +/** The extra strips the card arm needs beside the ones the resident forward already holds. */ +static int qwen35_attn_alloc_strips(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg) +{ + pn_qwen35_tt_attn_strips *a = &g->attn_card; + const uint32_t q_tiles = qwen35_attn_tiles((uint32_t)(cfg->n_head * cfg->head_dim)); + const uint32_t kv_tiles = qwen35_attn_tiles((uint32_t)(cfg->n_head_kv * cfg->head_dim)); + + /* SEPARATE ROTATED STRIPS rather than RoPE in place: `pn_tt_rope` is a stream op whose + * reader runs ahead of its writer, so an in-place rotation would be safe today and an + * unstated dependency on that lead. The same argument the residual pair is written on. */ + return (pn_tt_activation_alloc(g->device, q_tiles, &a->q) == 0 && + pn_tt_activation_alloc(g->device, q_tiles, &a->q_rot) == 0 && + pn_tt_activation_alloc(g->device, q_tiles, &a->z) == 0 && + pn_tt_activation_alloc(g->device, q_tiles, &a->gate) == 0 && + pn_tt_activation_alloc(g->device, q_tiles, &a->gated) == 0 && + pn_tt_activation_alloc(g->device, kv_tiles, &a->k_rot) == 0) + ? 0 + : -1; +} + +/** + * Is this a shape the on-card RoPE kernel will execute rather than approximate? + * + * `ft_tt_op_rope.h` refuses `half % 32 == 16` (the partner lands in a different FACE COLUMN) + * and a `rot` that is not a multiple of 32 at all. Ornith's own RoPE is + * `qwen35moe_rope_partial()`, which is the kernel's split-half NeoX form expression for + * expression — the two agree on the pairing, so the only question left is the width, and it + * is asked HERE because the alternative is finding out on the card. + */ +static bool qwen35_attn_rope_is_expressible(const pn_qwen35moe_config *cfg) +{ + const int rot = (cfg->rope_dim > 0 && cfg->rope_dim < cfg->head_dim) ? cfg->rope_dim + : cfg->head_dim; + + if ((rot % 64) != 0) { + fprintf(stderr, "qwen35_tt: REFUSED — on-card attention needs a rotary width that is " + "a multiple of 64 so `half` is a whole number of tiles; this model " + "rotates %d of %d\n", rot, cfg->head_dim); + return false; + } + return true; +} + +int pn_qwen35_tt_attn_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg) +{ + const uint32_t head_width = (uint32_t)cfg->head_dim; + int layer; + + if (!g || !m || !cfg) + return -1; + g->attn_on_card = false; + if (!pn_tt_device_oncard_requested(g->device, PN_TT_ONCARD_ATTENTION)) + return 0; + if (!qwen35_attn_rope_is_expressible(cfg)) + return -1; + + /* THE QK-NORM RESIDENCY FIRST, because it is the one that can decline for a reason the + * caller can act on — a second head width when the GDN already took the last slot. */ + if (pn_tt_device_arm_head_norm_at(g->device, head_width, FT_TT_OP_TILE_BYTES_BF16) != 0) + return -1; + if (!pn_tt_head_norm_is_armed(g->device, head_width)) { + fprintf(stderr, "qwen35_tt: REFUSED — on-card attention needs a per-head reduce " + "residency at Wt=%u for the QK-RMSNorms and none armed\n", + head_width / QWEN35_ATTN_TILE); + return -1; + } + + if (pn_tt_device_arm_attention(g->device, (uint32_t)cfg->n_head, + (uint32_t)cfg->n_head_kv, head_width, + (uint32_t)cfg->rope_dim, (double)cfg->rope_theta, + (uint32_t)pn_qwen35moe_max_seq(cfg), + (uint32_t)pn_qwen35moe_model_n_attn(m) + + (g->mtp.path[0] ? 1u : 0u)) != + 0) + return -1; + /* STEP 4 S2b: the MTP block's KV region is the one after the trunk's. */ + g->mtp.attn_index = pn_qwen35moe_model_n_attn(m); + if (!pn_tt_device_oncard(g->device, PN_TT_ONCARD_ATTENTION)) + return -1; + if (qwen35_attn_alloc_strips(g, cfg) != 0) { + fprintf(stderr, "qwen35_tt: REFUSED — the attention strips did not allocate\n"); + return -1; + } + + for (layer = 0; layer < cfg->n_layer; layer++) { + pn_qwen35_tt_layer *slot = &g->layers[layer]; + int index; + const pn_qwen35moe_attn_layer *l; + + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_ATTN) + continue; + index = pn_qwen35moe_model_layer_index(m, layer); + l = pn_qwen35moe_model_attn_layer(m, index); + { + char name[128]; + + snprintf(name, sizeof(name), "blk.%d.attn_q.weight", layer); + if (!l || pn_qwen35_tt_attn_split_q(g, cfg, name, false, slot) != 0) + return -1; + } + /* GAMMA IS SHARED ACROSS HEADS, which is what the row-block layout gives for free + * and what `qwen35moe_qk_norm_heads()` does: one `attn_q_norm` applied identically + * to all 32 Q heads, one `attn_k_norm` to all 16 KV heads. */ + if (pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, + l->attn_q_norm, head_width, cfg->rms_norm_eps, + &slot->attn_q_norm) != 0 || + pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, + l->attn_k_norm, head_width, cfg->rms_norm_eps, + &slot->attn_k_norm) != 0) { + fprintf(stderr, "qwen35_tt: REFUSED — layer %d's QK-norm constants did not " + "upload\n", layer); + return -1; + } + } + + g->attn_on_card = true; + g->attn_compare = getenv(QWEN35_ATTN_COMPARE_ENV) != NULL; + fprintf(stderr, "qwen35_tt: attention on the card — %d Q heads / %d KV groups, head_dim " + "%d, QK-norm on the Wt=%u per-head residency, sigmoid gate on the " + "elementwise one\n", cfg->n_head, cfg->n_head_kv, cfg->head_dim, + head_width / QWEN35_ATTN_TILE); + return 0; +} + +bool pn_qwen35_tt_attn_is_on_card(const pn_qwen35_tt *g) +{ + return g && g->attn_on_card; +} + +/* ================================================================ + * The block + * ================================================================ */ + +int pn_qwen35_tt_attn_project_card(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d) +{ + pn_qwen35_tt_strips *s = &g->card; + pn_qwen35_tt_attn_strips *a = &g->attn_card; + + if (!g || !d) + return -1; + /* FOUR PROJECTIONS, NOT THREE, and the fourth is free. The joint weight is staged as two + * halves, so Q and the gate come out of two matmuls over the same input instead of one + * matmul and a per-token de-interleave. Same FLOPs, same weight bytes, one more + * descriptor: 8 a token against 249. They are independent, so they chain. */ + if (pn_qwen35_tt_project(g, &d->attn_q_only, &s->normed, &a->q, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_gate_only, &s->normed, &a->z, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_k, &s->normed, &s->attn_k, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_v, &s->normed, &s->attn_v, false) != 0) + return -1; + return 0; +} + +int pn_qwen35_tt_attn_core_card(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int pos, + int seq_len) +{ + pn_qwen35_tt_strips *s = &g->card; + pn_qwen35_tt_attn_strips *a = &g->attn_card; + + if (!g || !d || !cfg || seq_len <= 0) + return -1; + + /* IN PLACE IS CORRECT HERE and not elsewhere: the reduce reader has read a whole row + * block before the writer can emit it, and the row blocks do not alias + * (`pn_tt_head_norm.h`). V is NOT normed — that is the model, not an omission. */ + if (pn_tt_head_norm_bf16(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &d->attn_q_norm, + (uint32_t)cfg->n_head, &a->q, &a->q) != 0 || + pn_tt_head_norm_bf16(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &d->attn_k_norm, + (uint32_t)cfg->n_head_kv, &s->attn_k, &s->attn_k) != 0) + return -1; + + if (pn_tt_rope(g->device, &a->q, &a->q_rot, (uint32_t)cfg->n_head, (uint32_t)pos) != 0 || + pn_tt_rope(g->device, &s->attn_k, &a->k_rot, (uint32_t)cfg->n_head_kv, + (uint32_t)pos) != 0) + return -1; + if (pn_tt_kv_append(g->device, (uint32_t)attn_index, false, &a->k_rot, + (uint32_t)pos) != 0 || + pn_tt_kv_append(g->device, (uint32_t)attn_index, true, &s->attn_v, + (uint32_t)pos) != 0) + return -1; + if (pn_tt_attention(g->device, (uint32_t)attn_index, &a->q_rot, &s->attn_out, + (uint32_t)seq_len) != 0) + return -1; + + /* SIGMOID, NOT SILU. `silu(x) = x*sigmoid(x)`, so reusing the SwiGLU member here is off + * by a factor of the gate itself — and both operands are `n_head*head_dim`, so no shape + * check anywhere would see it. `PN_TT_RED=attn_gate_silu` is that mutation. */ + return pn_tt_sigmoid_mul(g->device, &a->z, &s->attn_out, &a->gate, &a->gated); +} + +int pn_qwen35_tt_attn_core_chunk(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int base_pos, + int rows) +{ + pn_qwen35_tt_strips *s = &g->card; + pn_qwen35_tt_attn_strips *a = &g->attn_card; + + if (!g || !d || !cfg || rows <= 0 || rows > 32 || base_pos < 0 || (base_pos % 32) != 0) + return -1; + + /* UNCHANGED, AND THAT IS THE FINDING. A per-head norm is `row_blocks = heads` over a + * `[1, heads*S]` strip; a row reduce reduces ALONG the row, so row t of row block h is + * position t of head h and 32 positions cost one descriptor exactly as one position + * does. V is still not normed. */ + if (pn_tt_head_norm_bf16(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &d->attn_q_norm, + (uint32_t)cfg->n_head, &a->q, &a->q) != 0 || + pn_tt_head_norm_bf16(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &d->attn_k_norm, + (uint32_t)cfg->n_head_kv, &s->attn_k, &s->attn_k) != 0) + return -1; + + if (pn_tt_rope_chunk(g->device, &a->q, &a->q_rot, (uint32_t)cfg->n_head, + (uint32_t)base_pos, (uint32_t)rows) != 0 || + pn_tt_rope_chunk(g->device, &s->attn_k, &a->k_rot, (uint32_t)cfg->n_head_kv, + (uint32_t)base_pos, (uint32_t)rows) != 0) + return -1; + if (pn_tt_kv_append_chunk(g->device, (uint32_t)attn_index, false, &a->k_rot, + (uint32_t)base_pos, (uint32_t)rows) != 0 || + pn_tt_kv_append_chunk(g->device, (uint32_t)attn_index, true, &s->attn_v, + (uint32_t)base_pos, (uint32_t)rows) != 0) + return -1; + if (pn_tt_attention_chunk(g->device, (uint32_t)attn_index, &a->q_rot, &s->attn_out, + (uint32_t)base_pos, (uint32_t)rows) != 0) + return -1; + + /* SIGMOID, NOT SILU — the same note as the per-position arm, and per element, so the + * chunk costs one descriptor. */ + return pn_tt_sigmoid_mul(g->device, &a->z, &s->attn_out, &a->gate, &a->gated); +} + +int pn_qwen35_tt_attn_block_card(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int pos, + int seq_len) +{ + if (pn_qwen35_tt_attn_project_card(g, d) != 0 || + pn_qwen35_tt_attn_core_card(g, d, cfg, attn_index, pos, seq_len) != 0) + return -1; + return pn_qwen35_tt_project(g, &d->attn_output, &g->attn_card.gated, &g->card.project, + true); +} + +/* ================================================================ + * The crossed control + * ================================================================ */ + +/** + * Run BOTH arms over the same projections and diff the attention outputs PER HEAD. + * + * NOT a PCC check, and deliberately not one. The failures this path can have are a wrong KV + * position, a group's query reading another group's head, a mask tail off by one, a + * de-interleave that swapped Q and the gate, and a QK-norm at the wrong width — every one of + * which is a PERMUTATION or a localised wrong element. PCC is blind to the first and + * saturates on the second. So this reports max|delta| and WHICH HEAD it was on, because + * "one head of thirty-two is wrong" is a per-tensor figure of 1.0e+00 and a per-head figure + * that names the head. A per-tensor max-norm hid a per-head fault in the GDN family for + * three rounds. + * + * It costs a full download of both answers AND a second whole attention block, so it is off + * unless `PN_TT_ATTN_COMPARE` is set. When it IS set, `host_attention_seconds` counts the + * shadow arm and no timing from that run is comparable with one from a run without it. + * + * The caller runs the HOST block first and the CARD block second, so `s->project` — the one + * the forward carries — is the CARD's. Reversed, this would be a host forward with a card + * shadow, which is a measurement of the thing not under test. + */ +void pn_qwen35_tt_attn_compare(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int layer, + const float *host_attn) +{ + const int head_dim = cfg->head_dim; + const uint32_t width = (uint32_t)(cfg->n_head * head_dim); + double worst = 0.0; + double worst_relative = 0.0; + int worst_relative_head = -1; + int worst_head = -1; + int worst_lane = -1; + int heads_out = 0; + int head; + + if (!g->attn_compare || !host_attn || !g->h_attn_card) + return; + if (pn_tt_activation_download(g->device, &g->attn_card.gated, g->h_attn_card, width) != 0) { + fprintf(stderr, "qwen35_tt: [attn compare] layer %d — the card answer did not come " + "back; NOTHING is scored for this layer\n", layer); + return; + } + for (head = 0; head < cfg->n_head; head++) { + double head_worst = 0.0; + double scale = 0.0; + int lane; + + for (lane = 0; lane < head_dim; lane++) { + const size_t index = (size_t)head * (size_t)head_dim + (size_t)lane; + const double delta = fabs((double)g->h_attn_card[index] - + (double)host_attn[index]); + + if (fabs((double)host_attn[index]) > scale) + scale = fabs((double)host_attn[index]); + if (delta > head_worst) { + head_worst = delta; + if (delta > worst) { + worst = delta; + worst_head = head; + worst_lane = lane; + } + } + } + /* MAX-NORM RELATIVE, per head — the same figure the GDN parity gate reports, so the + * two are readable against each other and against the 1e-3 bar. + * + * **THE COUNT ALONE DOES NOT DECIDE ANYTHING.** "16 of 16 heads past 1e-3" is the + * same sentence for an arm 1.1x over the bar and for one 800x over it, and those are + * a bf16 cache and a permutation respectively. The WORST relative figure is what + * separates them, so it is carried out beside the count rather than thrown away + * inside the comparison. */ + if (scale > 0.0) { + const double relative = head_worst / scale; + + if (relative > 1.0e-3) + heads_out++; + if (relative > worst_relative) { + worst_relative = relative; + worst_relative_head = head; + } + } + } + fprintf(stderr, "qwen35_tt: [attn compare] layer %d — max|delta| %.3e at head %d lane %d, " + "worst RELATIVE %.3e at head %d, %d of %d heads past 1e-3 relative\n", + layer, worst, worst_head, worst_lane, worst_relative, worst_relative_head, + heads_out, cfg->n_head); +} + +/* ================================================================ + * Release + * ================================================================ */ + +void pn_qwen35_tt_attn_release(pn_qwen35_tt *g) +{ + if (!g) + return; + free(g->h_attn_card); + g->h_attn_card = NULL; + g->attn_on_card = false; +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_checkpoint_band.c b/src/models/qwen35moe_checkpoint_band.c new file mode 100644 index 0000000..e174711 --- /dev/null +++ b/src/models/qwen35moe_checkpoint_band.c @@ -0,0 +1,25 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_checkpoint_band.c + * @brief Checkpoint position arithmetic (see qwen35moe_checkpoint_band.h). + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "qwen35moe_checkpoint_band.h" + +int pn_qwen35_ckpt_band_at(int base, int batch_end, int ceiling) +{ + int limit = batch_end; + int at; + + if (ceiling > 0 && ceiling < limit) + limit = ceiling; + if (limit <= base) + return 0; + at = base + ((limit - base) / PN_QWEN35_CKPT_BAND_ROWS) * PN_QWEN35_CKPT_BAND_ROWS; + return at > base ? at : 0; +} diff --git a/src/models/qwen35moe_checkpoint_band.h b/src/models/qwen35moe_checkpoint_band.h new file mode 100644 index 0000000..47cf8fb --- /dev/null +++ b/src/models/qwen35moe_checkpoint_band.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_checkpoint_band.h + * @brief Where a multi-turn prefill checkpoint is taken: the position arithmetic of + * qwen35moe_prefill_checkpoint_tt.c, host-only so it is unit-testable without a card. + * + * The checkpoint is the carried GDN state at a 32-row band boundary of a batched prefill + * (see qwen35moe_prefill_checkpoint_tt.c for why only a band boundary is bit-exact). A later + * turn can resume from it only when its prompt starts with every token below it. + * + * THE CEILING. A chat prompt ends with tokens the template FORCED after the assistant header + * — the generation prompt, "\n" on Ornith. When the client resends that turn as + * history the template renders it without them, so the next prompt agrees with this one only + * up to the header's end. A checkpoint above that point can never be resumed: the next turn + * re-prefills the whole conversation (35k tokens: 79 s instead of ~2 s). The ceiling is the + * header's end; the checkpoint is the last band boundary at or below it. + * + * @status Complete. + * @issues None + * @todo None + */ + +#ifndef PN_QWEN35MOE_CHECKPOINT_BAND_H +#define PN_QWEN35MOE_CHECKPOINT_BAND_H + +/** Rows of a chunk-mixer band: the only positions a checkpoint can sit at. */ +#define PN_QWEN35_CKPT_BAND_ROWS 32 + +/** + * The position to checkpoint in a batched prefill that runs rows [base, batch_end). + * + * @param base where the passes start (0, or a restored checkpoint) — a band multiple + * @param batch_end one past the last batched row + * @param ceiling no checkpoint above this position (the end of the history the next turn + * will resend); <= 0 means none + * @return the last band boundary in (base, min(batch_end, ceiling)], or 0 when there is none + * (the checkpoint at `base`, if any, still stands) + * + * Thread-safe: yes (pure) + */ +int pn_qwen35_ckpt_band_at(int base, int batch_end, int ceiling); + +#endif /* PN_QWEN35MOE_CHECKPOINT_BAND_H */ diff --git a/src/models/qwen35moe_forward.c b/src/models/qwen35moe_forward.c new file mode 100644 index 0000000..0536dd9 --- /dev/null +++ b/src/models/qwen35moe_forward.c @@ -0,0 +1,548 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_forward.c + * @brief Qwen3.5-MoE hybrid forward-pass assembly (A6). Wires the four + * isolation-verified kernels (loader A2, attention A3, SSM A4, MoE A5) + * into an end-to-end single-token forward with dual state, plus the + * dual-state lifecycle (new/free/reset) the engine hooks call. + * + * @status A6 complete: embed -> per-layer (attn_norm -> SSM|attn block -> + * +residual -> post_attention_norm -> shared-MoE -> +residual) -> + * output_norm -> LM head. Runs end-to-end; numeric correctness vs the + * oracle is A8. + * @issues None + * @todo + * - [ ] A8: validate logits vs the llama.cpp oracle (the (1+w) norm watch- + * list items in docs/architecture/qwen35moe.md may surface here). + */ + +/* 1. This module's own header */ +#include "qwen35moe.h" + +/* 2. Standard library */ +#include +#include +#include + +/* 3. System / platform headers */ +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +/* 4. Project headers */ +#include "../accel/simd.h" +#include "../core/qweight.h" +#include "../memory/arena.h" +#include "qwen35moe_internal.h" +#include "qwen35moe_kv_q4.h" + +#include "../core/mem/pn_lazy_zero.h" +#include "../core/mem/pn_memwatch.h" +#include "../core/mem/pn_ladder.h" + +/* QWEN35_KV_Q4 gate: controls Q4_0 KV-cache quantization (~4-7x smaller + * VRAM/host, 256K-context lever). Quality validated — PPL delta 0.024%. + * + * Default (B1): ON. Opt-out with QWEN35_KV_Q4=0 to restore fp32 KV for + * diagnostic or A/B work. The old opt-in (QWEN35_KV_Q4=1) still forces ON + * explicitly and is unchanged. Read once. */ +int pn_qwen35moe_kv_q4_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_KV_Q4"); + /* Default ON; only explicit "0" disables. */ + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* QWEN35_HOST_KV_MIRROR gate (Phase 10b / INV3). Default LAZY (0): the + * batched slots path stops maintaining the per-token host KV mirror — the + * GPU slot ring is authoritative while a session is bound, rebinds are + * seeded device-to-device, and the mirror is materialized on demand + * (export, sys-prefix capture, prefill yield, host-softmax fallback). Set + * QWEN35_HOST_KV_MIRROR=1 to restore the legacy eager mirror (every decode + * token D2H-appended to host RAM). Read once. */ +int pn_qwen35moe_host_kv_mirror_eager(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_HOST_KV_MIRROR"); + v = (env && env[0] == '1') ? 1 : 0; + } + return v; +} + +/* Phase 10b: global content-generation counter for pn_qwen35moe_kv (see + * content_gen in qwen35moe_internal.h). Seeding each kv from a process-wide + * counter guarantees a recycled buffer address can never alias a stale GPU + * ring watermark. */ +unsigned pn_qwen35moe_kv_next_content_gen(void) +{ + static unsigned counter; + return __atomic_add_fetch(&counter, 1u, __ATOMIC_RELAXED); +} + +/* Per-token scratch arena size. The hot consumers are the attention block + * (an [n_head * max_seq] scores buffer) and the SSM block (several conv_dim / + * val_dim buffers). 16 MB leaves generous headroom for every block's working + * set with the mini's dims, and is reset to empty before each block. */ +#define PN_QWEN35MOE_ARENA_BYTES (16u * 1024u * 1024u) + +/* KV-cache window per attention layer (default PN_QWEN35MOE_MAX_SEQ, matching + * rwkvx's decode budget). QWEN35_MAX_SEQ overrides for long-context runs, and a + * per-model ctx_cap overrides that. Memory scales linearly: n_head_kv * max_seq + * * head_dim * 8 bytes per attention layer on the host, the same again on the + * die for the flash-attention cache. Both PN_QWEN35MOE_MAX_SEQ (default) and + * PN_QWEN35MOE_MAX_SEQ_CAP (ceiling) live in qwen35moe.h so the engine admission + * path and the CUDA admit gate share the exact numbers the ring is sized to. */ + +/* The configured KV window (env-overridable, parsed once). */ +static int qwen35moe_clamp_max_seq(int v) +{ + if (v < 16) + return 16; + if (v > PN_QWEN35MOE_MAX_SEQ_CAP) + return PN_QWEN35MOE_MAX_SEQ_CAP; + return v; +} + +static int qwen35moe_max_seq(void) +{ + static int v = -1; + if (v < 0) { + v = PN_QWEN35MOE_MAX_SEQ; + const char *env = getenv("QWEN35_MAX_SEQ"); + if (env) { + int e = atoi(env); + if (e > 0) + v = qwen35moe_clamp_max_seq(e); + } + } + return v; +} + +/* KV Phase 1: the KV window this MODEL's ring is sized to. A per-model + * cfg->ctx_cap (set at load) wins so ornith can run 40K without the global + * QWEN35_MAX_SEQ moving saber's window; 0 falls back to the global/default. */ +static int qwen35moe_max_seq_cfg(const pn_qwen35moe_config *cfg) +{ + if (cfg && cfg->ctx_cap > 0) + return qwen35moe_clamp_max_seq(cfg->ctx_cap); + return qwen35moe_max_seq(); +} + +int pn_qwen35moe_max_seq(const pn_qwen35moe_config *cfg) +{ + return qwen35moe_max_seq_cfg(cfg); +} + +/* Add src into dst, element-wise: dst[i] += src[i]. */ +static void qwen35moe_residual_add(float *dst, const float *src, int n) +{ + for (int i = 0; i < n; i++) + dst[i] += src[i]; +} + +/* Debug dump: first-3, last-3 + sum, matching llama-eval-callback's format so + * activations can be bisected against the oracle. Gated by QWEN35_DEBUG. */ +static int qwen35moe_dbg_enabled(void) +{ + static int v = -1; + if (v < 0) + v = getenv("QWEN35_DEBUG") ? 1 : 0; + return v; +} + +static void qwen35moe_dbg(const char *tag, int layer, const float *x, int n) +{ + if (!qwen35moe_dbg_enabled()) + return; + double sum = 0.0; + for (int i = 0; i < n; i++) + sum += x[i]; + fprintf(stderr, "PN %-22s L%-2d [%12.6f %12.6f %12.6f ... %12.6f %12.6f " + "%12.6f] sum=%.6f\n", + tag, layer, x[0], x[1], x[2], x[n - 3], x[n - 2], x[n - 1], sum); +} + +/* ============================================================ + * Dual-state lifecycle + * ============================================================ */ + +/* ============================================================ + * Lazily-committed host-state buffers (Phase 10b / INV3) + * + * The KV mirror ring and the SSM recurrent state are the two per-session + * allocations that made host RAM scale with ctx_cap instead of usage: they + * were calloc'd at full max_seq and fully committed by the first + * pn_state_reset memset (~789 MiB/slot at ctx_cap 131072). They now come + * from pn_lazy_zero_alloc (anonymous mmap), so committed pages track the + * rows actually written, reset returns the pages to the OS, and free + * bypasses glibc arena retention. The byte-size helpers below are shared by + * init/free/reset so allocator and deallocator can never disagree. + * ============================================================ */ + +/* Bytes of ONE of the kq/vq (q4) or k/v (fp32) host mirror rings. */ +static size_t qwen35moe_kv_ring_bytes(const pn_qwen35moe_kv *kv) +{ + if (kv->q4) + return (size_t)kv->n_head_kv * kv->max_seq * + (size_t)pn_kvq4_row_bytes(kv->head_dim); + return (size_t)kv->n_head_kv * kv->max_seq * kv->head_dim * + sizeof(float); +} + +static size_t qwen35moe_ssm_conv_bytes(const pn_qwen35moe_ssm_state *st) +{ + return (size_t)st->conv_minus_1 * st->conv_dim * sizeof(float); +} + +static size_t qwen35moe_ssm_state_bytes(const pn_qwen35moe_ssm_state *st) +{ + return (size_t)st->state_size * st->state_size * st->n_v_heads * + sizeof(float); +} + +/* Allocate one attention layer's KV cache, zero-primed. Returns 0 on success. + * Under QWEN35_KV_Q4 the cache is stored as ggml-Q4_0 rows (~4-7x smaller); + * otherwise fp32 (the A/B baseline). */ +static int qwen35moe_kv_init(pn_qwen35moe_kv *kv, const pn_qwen35moe_config *cfg) +{ + kv->n_head_kv = cfg->n_head_kv; + kv->head_dim = cfg->head_dim; + kv->max_seq = qwen35moe_max_seq_cfg(cfg); + kv->q4 = pn_qwen35moe_kv_q4_on(); + + /* Rung 4a of the reactive ladder (MemoryAware.md §7): a KV cache allocated + * while the box is under memory pressure defaults to the ~4-7x-smaller Q4 + * format even if the operator did not set QWEN35_KV_Q4 — the cheapest VRAM + * win, engaged from WATCH up. KV format is fixed at allocation, so this acts + * at init time (not retroactively on a live fp32 cache). At level OK the + * plan mirrors the config, so an unpressured init is unchanged. */ + if (!kv->q4 && pn_memwatch_running()) { + pn_mem_state st; + pn_memwatch_read(&st); + pn_ladder_plan lp; + pn_ladder_plan_from_state(&st, 1, 0, false, &lp); + if (lp.force_kv_q4) { + kv->q4 = 1; + fprintf(stderr, "[ladder] %s -> forcing Q4 KV cache at init (%s)\n", + pn_mem_level_name(st.level), lp.note); + } + } + kv->k = kv->v = NULL; + kv->kq = kv->vq = NULL; + kv->k_sink = kv->v_sink = NULL; + kv->k_win = kv->v_win = NULL; + kv->content_gen = pn_qwen35moe_kv_next_content_gen(); + size_t ring_bytes = qwen35moe_kv_ring_bytes(kv); + if (kv->q4) { + kv->kq = pn_lazy_zero_alloc(ring_bytes); + kv->vq = pn_lazy_zero_alloc(ring_bytes); + /* fp16 sink + recent-window shadow (kept at fp16 to stop argmax flips) */ + size_t sink_n = (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * kv->head_dim; + size_t win_n = (size_t)kv->n_head_kv * PN_KVQ4_WMAX * kv->head_dim; + kv->k_sink = calloc(sink_n, sizeof(uint16_t)); + kv->v_sink = calloc(sink_n, sizeof(uint16_t)); + kv->k_win = calloc(win_n, sizeof(uint16_t)); + kv->v_win = calloc(win_n, sizeof(uint16_t)); + return (kv->kq && kv->vq && kv->k_sink && kv->v_sink && + kv->k_win && kv->v_win) ? 0 : -1; + } + kv->k = pn_lazy_zero_alloc(ring_bytes); + kv->v = pn_lazy_zero_alloc(ring_bytes); + return (kv->k && kv->v) ? 0 : -1; +} + +/* Allocate one SSM layer's recurrent state, zero-primed (the SSM kernel assumes + * a zero conv_state + ssm_state at sequence start). Returns 0 on success. */ +static int qwen35moe_ssm_init(pn_qwen35moe_ssm_state *st, + const pn_qwen35moe_config *cfg) +{ + st->conv_dim = cfg->conv_dim; + st->conv_minus_1 = cfg->ssm_conv - 1; + st->state_size = cfg->ssm_state; + st->n_v_heads = cfg->ssm_dt_rank; + st->conv_state = pn_lazy_zero_alloc(qwen35moe_ssm_conv_bytes(st)); + st->ssm_state = pn_lazy_zero_alloc(qwen35moe_ssm_state_bytes(st)); + return (st->conv_state && st->ssm_state) ? 0 : -1; +} + +void *pn_qwen35moe_state_new(void *weights) +{ + pn_qwen35moe_model *m = (pn_qwen35moe_model *)weights; + if (!m) + return NULL; + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + pn_qwen35moe_state *s = calloc(1, sizeof(*s)); + if (!s) + return NULL; + + s->n_ssm = pn_qwen35moe_model_n_ssm(m); + s->n_attn = pn_qwen35moe_model_n_attn(m); + s->n_embd = cfg->n_embd; + s->n_vocab = cfg->n_vocab; + + s->ssm = s->n_ssm ? calloc((size_t)s->n_ssm, sizeof(*s->ssm)) : NULL; + s->kv = s->n_attn ? calloc((size_t)s->n_attn, sizeof(*s->kv)) : NULL; + if ((s->n_ssm && !s->ssm) || (s->n_attn && !s->kv)) + goto fail; + + for (int i = 0; i < s->n_ssm; i++) + if (qwen35moe_ssm_init(&s->ssm[i], cfg) < 0) + goto fail; + for (int i = 0; i < s->n_attn; i++) + if (qwen35moe_kv_init(&s->kv[i], cfg) < 0) + goto fail; + + s->arena = pn_arena_new(PN_QWEN35MOE_ARENA_BYTES); + s->x_norm = calloc((size_t)s->n_embd, sizeof(float)); + s->block_out = calloc((size_t)s->n_embd, sizeof(float)); + s->logits = calloc((size_t)s->n_vocab, sizeof(float)); + if (!s->arena || !s->x_norm || !s->block_out || !s->logits) + goto fail; + + fprintf(stderr, + "qwen35moe: dual state allocated (%d SSM + %d attn layers, " + "arena %.1f MB)\n", + s->n_ssm, s->n_attn, + (double)PN_QWEN35MOE_ARENA_BYTES / (1024.0 * 1024.0)); + return s; + +fail: + pn_qwen35moe_state_free(s); + return NULL; +} + +void pn_qwen35moe_state_free(void *state) +{ + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state; + if (!s) + return; + +#ifdef USE_CUDA + /* FIRST, while every buffer below is still allocated: make the GPU layers + * forget this session. They keep raw per-slot back-pointers into `s` + * (kv_slot_kv, kv_slot_host_id, host_state) and nothing else clears them, + * so without this the next pn_qwen35moe_decode_slot_bind_cuda into that + * slot dereferences freed memory in its lazy-mirror park copy. The serve + * does exactly that on every session release and idle reap. */ + pn_qwen35moe_gpu_forget_state_cuda(s); +#endif + + if (s->ssm) { + for (int i = 0; i < s->n_ssm; i++) { + pn_lazy_zero_free(s->ssm[i].conv_state, + qwen35moe_ssm_conv_bytes(&s->ssm[i])); + pn_lazy_zero_free(s->ssm[i].ssm_state, + qwen35moe_ssm_state_bytes(&s->ssm[i])); + } + free(s->ssm); + } + if (s->kv) { + for (int i = 0; i < s->n_attn; i++) { + size_t ring_bytes = qwen35moe_kv_ring_bytes(&s->kv[i]); + pn_lazy_zero_free(s->kv[i].k, ring_bytes); + pn_lazy_zero_free(s->kv[i].v, ring_bytes); + pn_lazy_zero_free(s->kv[i].kq, ring_bytes); + pn_lazy_zero_free(s->kv[i].vq, ring_bytes); + free(s->kv[i].k_sink); + free(s->kv[i].v_sink); + free(s->kv[i].k_win); + free(s->kv[i].v_win); + } + free(s->kv); + } + pn_arena_free(s->arena); + free(s->x_norm); + free(s->block_out); + free(s->logits); + free(s); +} + +void pn_qwen35moe_state_reset(void *state) +{ + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state; + if (!s) + return; + + for (int i = 0; i < s->n_ssm; i++) { + pn_qwen35moe_ssm_state *st = &s->ssm[i]; + pn_lazy_zero_reset(st->conv_state, qwen35moe_ssm_conv_bytes(st)); + pn_lazy_zero_reset(st->ssm_state, qwen35moe_ssm_state_bytes(st)); + } + for (int i = 0; i < s->n_attn; i++) { + pn_qwen35moe_kv *kv = &s->kv[i]; + size_t ring_bytes = qwen35moe_kv_ring_bytes(kv); + /* Phase 10b: lazy reset zeroes the mirror AND returns its committed + * pages — a slot reused for a new request stops carrying the previous + * session's ctx_cap-sized host high-water mark. */ + if (kv->q4) { + pn_lazy_zero_reset(kv->kq, ring_bytes); + pn_lazy_zero_reset(kv->vq, ring_bytes); + size_t sink_n = (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * + kv->head_dim; + size_t win_n = (size_t)kv->n_head_kv * PN_KVQ4_WMAX * kv->head_dim; + memset(kv->k_sink, 0, sink_n * sizeof(uint16_t)); + memset(kv->v_sink, 0, sink_n * sizeof(uint16_t)); + memset(kv->k_win, 0, win_n * sizeof(uint16_t)); + memset(kv->v_win, 0, win_n * sizeof(uint16_t)); + } else { + pn_lazy_zero_reset(kv->k, ring_bytes); + pn_lazy_zero_reset(kv->v, ring_bytes); + } + /* B2: host mirror is now zeroed (empty context) — mark current so + * preserve_live doesn't D2H stale GPU ring content over it. */ + kv->kv_mirror_stale = 0; + /* Phase 10b: the buffer pointer survives the reset but its content + * identity does not — invalidate every GPU ring watermark keyed on + * (pointer, generation). */ + kv->content_gen = pn_qwen35moe_kv_next_content_gen(); + } +} + +/* ============================================================ + * Forward (single token) + * ============================================================ */ + +/* Run one layer's hybrid block: attn_norm -> (SSM|attn) -> +residual -> + * post_attention_norm -> shared-MoE -> +residual. Mutates x in place. */ +static int qwen35moe_layer_forward(pn_qwen35moe_model *m, int layer, float *x, + pn_qwen35moe_state *s, int pos, + struct fw_pool *pool) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + int idx = pn_qwen35moe_model_layer_index(m, layer); + const float *attn_norm_w, *post_norm_w; + const pn_qwen35moe_moe *moe; + int rc; + + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + const pn_qwen35moe_attn_layer *l = pn_qwen35moe_model_attn_layer(m, idx); + attn_norm_w = l->attn_norm; + post_norm_w = l->post_attention_norm; + moe = &l->moe; + pn_vrms_norm_f32(x, attn_norm_w, s->x_norm, c, cfg->rms_norm_eps); + qwen35moe_dbg("attn_norm", layer, s->x_norm, c); + pn_arena_reset(s->arena); + rc = pn_qwen35moe_attn_block(l, cfg, s->x_norm, &s->kv[idx], pos, + s->arena, s->block_out, pool); + } else { + const pn_qwen35moe_ssm_layer *l = pn_qwen35moe_model_ssm_layer(m, idx); + attn_norm_w = l->attn_norm; + post_norm_w = l->post_attention_norm; + moe = &l->moe; + pn_vrms_norm_f32(x, attn_norm_w, s->x_norm, c, cfg->rms_norm_eps); + qwen35moe_dbg("attn_norm", layer, s->x_norm, c); + pn_arena_reset(s->arena); + rc = pn_qwen35moe_ssm_block(l, cfg, s->x_norm, &s->ssm[idx], + s->arena, s->block_out, pool); + } + if (rc < 0) + return rc; + + qwen35moe_dbg("block_out", layer, s->block_out, c); + /* Residual after the SSM/attention block (block kernels skip it). */ + qwen35moe_residual_add(x, s->block_out, c); + qwen35moe_dbg("attn_residual", layer, x, c); + + /* Shared-expert MoE FFN on the post_attention_norm'd hidden state. */ + pn_vrms_norm_f32(x, post_norm_w, s->x_norm, c, cfg->rms_norm_eps); + qwen35moe_dbg("attn_post_norm", layer, s->x_norm, c); + pn_arena_reset(s->arena); + rc = pn_qwen35moe_moe_block(moe, cfg, s->x_norm, s->arena, s->block_out, + pool); + if (rc < 0) + return rc; + qwen35moe_dbg("ffn_out", layer, s->block_out, c); + qwen35moe_residual_add(x, s->block_out, c); + qwen35moe_dbg("l_out", layer, x, c); + return 0; +} + +/* Internal forward with an explicit thread pool (the public entry point's pool + * comes from the model; the per-block kernels accept it for parallel sections). */ +static float *qwen35moe_forward_with_pool(pn_qwen35moe_model *m, uint32_t token, + pn_qwen35moe_state *s, int pos, + struct fw_pool *pool) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + int n_layer = cfg->n_layer; + + if ((int)token >= cfg->n_vocab) + return NULL; + + /* Embed: copy the token's row from the dequantized embedding table into a + * working hidden state that carries the residual stream through the stack. */ + float *hidden = calloc((size_t)c, sizeof(float)); + if (!hidden) + return NULL; + pn_qwen35moe_model_embed_row(m, token, hidden); + qwen35moe_dbg("input_embed", -1, hidden, c); + + for (int layer = 0; layer < n_layer; layer++) { + if (qwen35moe_layer_forward(m, layer, hidden, s, pos, pool) < 0) { + free(hidden); + return NULL; + } + } + + /* Final norm + LM head. */ + pn_vrms_norm_f32(hidden, pn_qwen35moe_model_output_norm(m), s->x_norm, c, + cfg->rms_norm_eps); + qwen35moe_dbg("result_norm", -1, s->x_norm, c); + qwen35moe_mv(cfg, pn_qwen35moe_model_output(m), s->x_norm, s->logits); + qwen35moe_dbg("result_output", -1, s->logits, cfg->n_vocab); + + free(hidden); + return s->logits; +} + +float *pn_qwen35moe_forward_token(pn_qwen35moe_model *m, uint32_t token, + pn_qwen35moe_state *s, int pos) +{ + if (!m || !s) + return NULL; + return qwen35moe_forward_with_pool(m, token, s, pos, NULL); +} + +/* ============================================================ + * pn_forward_fn adapter + * ============================================================ */ + +tm_tensor *pn_qwen35moe_forward(pn_model *model, uint32_t token, pn_state *state) +{ + if (!model || !state) + return NULL; + + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return NULL; + + struct fw_pool *pool = NULL; +#ifdef USE_THREADS + pool = model->pool; +#endif + + /* Thread pool lives on the model; thread it through to the per-block + * kernels for their parallel sections. pos drives RoPE + the KV caches. */ + float *logits = qwen35moe_forward_with_pool(m, token, s, state->pos, pool); + if (!logits) + return NULL; + + int64_t shape[1] = { s->n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + if (!out) + return NULL; + memcpy(tm_data_f32(out), logits, (size_t)s->n_vocab * sizeof(float)); + + state->pos++; + return out; +} diff --git a/src/models/qwen35moe_forward_cuda.c b/src/models/qwen35moe_forward_cuda.c new file mode 100644 index 0000000..3e13232 --- /dev/null +++ b/src/models/qwen35moe_forward_cuda.c @@ -0,0 +1,10933 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_forward_cuda.c + * @brief GPU forward path for Qwen3.5-MoE, single-die or N-die layer-split. + * Mirrors the CPU orchestration in qwen35moe_forward.c op-for-op, but + * uploads every weight to a persistent GPU fc_buffer at load and runs the + * 40-layer sandwich (attn_norm -> SSM|attn block -> +residual -> + * post_attn_norm -> shared-MoE -> +residual) through Foundry's fc_*_ctx + * CUDA kernels, with GPU-resident dual state (SSM conv+recurrent state + * and KV cache) and per-layer scratch. Embed -> per-layer loop -> + * output_norm -> LM head. The LM-head result is reduced ON the die to + * the top-k (id, logit) pairs by default (gpu_fetch_logits / + * QWEN35_GPU_TOPK) — only k pairs cross the 0.4 GB/s host link, not + * the 248k-vocab buffer; QWEN35_GPU_FULL_LOGITS=1 restores the full + * logits D2H copy. + * + * N-die split: an explicit placement of N dies (pn_load_on / serve's + * --dies) splits the layers N ways via pn_plan_die_layer_split — the + * SAME partitioner admission prices the load with, so the projection and + * the upload cannot disagree. Die i holds layers [layer_lo[i], + * layer_hi[i]) in its own VRAM with its own fc_cuda_ctx; token_embd is + * never uploaded (host row copy at embed time) and the final norm + LM + * head ride on the LAST die. The forward walks the dies in order, + * handing the residual stream across each of the N-1 boundaries — one + * contiguous [positions][n_embd] f32 copy, device-to-device over the + * peer link where the pair has it, else staged through the host + * (QWEN35_HANDOFF_HOST=1 forces the host path). The SSM/attention dual + * state never crosses dies. + * + * More dies buys CAPACITY, not speed: every die's layer maths costs + * exactly what it did, and each extra boundary adds a sync plus a + * transfer per step. Split a model across the FEWEST dies that hold it + * at the context you want; go wider only to buy KV headroom. + * + * Legacy barswap (QWEN35_GPU_BARSWAP) time-multiplexes one MMIO slot and + * supports at most a 2-die split; wider placements are refused there. + * The 2-die-only optimizations (T3 vocab-split head, L5 die pipeline, + * QWEN35_TWO_THREAD, the C2 async phase machine) stay off above two + * dies — each is a two-stage construction, and deepening them would add + * boundary hops to a trade that is already paying them. + * + * @status HOST C only. Compiles on the Mac with -DUSE_CUDA via dlopen (no GPU + * present here). The operator validates it layer-by-layer against the + * CPU reference on a K80 host (the A8 per-layer-dump bisection): the + * per-layer dump tags are byte-identical to the CPU path + * (qwen35moe_forward.c qwen35moe_dbg) and fire for EVERY layer on both + * dies, so all 40 GPU-vs-CPU diffs line up. The 2-die context-swap + * mechanism is gated by tests/test_2die_swap (run that FIRST). + * + * @issues Several ops have no dedicated GPU kernel yet and are done host-side on + * a read-back buffer (then re-uploaded). Each is marked `TODO kernel:` + * inline and listed in the file's final report. They are correctness- + * neutral (they call the same CPU helpers the reference uses) but cost a + * D2H/H2D round trip; closing them is follow-up kernel work. + * + * The boundary hand-off's peer copy MUST be followed by a sync of the + * RECEIVING die (gpu_handoff_import). cuMemcpyPeer lands on its own + * stream, so without it the next die reads a stale residual stream and + * the model emits fluent-shaped garbage instead of failing — the failure + * mode is silent, which is why it is written down here rather than + * trusted to memory. Reproduced on K80 2026-08-21 at both 2 and 3 dies. + * + * g->bhost[] is ONE host staging set shared by every die. The + * sequential paths are safe because only one die is ever live, but the + * 9c wide prefill pipeline runs all of them at once, so any layer path + * that stages through bhost would have two dies writing the same host + * bytes — silent corruption again, not a failure. The default batched + * path never does (flash attention is on-die, the lazy KV mirror skips + * the host store, MoE and SSM never leave the die), and + * pn_qwen35moe_prefill_range_pipelined_cuda REFUSES the two settings + * that reintroduce it (QWEN35_HOST_KV_MIRROR=1, and flash unavailable). + * Per-die staging buffers would lift that restriction. + * + * QWEN35_GPU_MAX_LAYERS (single-die debug cap) now prices the KV for + * the layers it actually uploads rather than for the whole model, so + * its gate is looser than before. It is a validation knob, unset in + * production. + * @todo + * - [ ] Close the TODO-kernel gaps (joint Q+gate split, GQA head broadcast + + * L2-norm, decay gate / softplus, sigmoid gates, SwiGLU expert matmuls + * for non-Q3_K experts, attention softmax over the KV window). + * - [ ] K80: per-layer GPU-vs-CPU validation against the oracle dump, and + * confirm tests/test_2die_swap PASSes before trusting the split. + * - [ ] K80 (C1): regenerate fc_cuda_ptx.h (cd foundry/cuda_kernels && + * make), then gate the segment graphs — argmax-exact generation vs + * QWEN35_GRAPH_FULL=0 and decode tok/s A/B (see gpu_segment_forward). + */ + +/* 1. This module's own header */ +#include "qwen35moe.h" + +/* 2. Standard library */ +#include +#include +#include +#include +#include +#include +#include +#include +/* Unconditional: the live-GPU-context registry below is guarded by a mutex in + * every build, USE_THREADS or not — a model unload can race a session free. */ +#include + +/* 3. System / platform headers */ +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#include +#endif + +/* 4. Project headers */ +#include "../accel/die_layer_split.h" +#include "../accel/simd.h" +#include "../core/mem/pn_admission.h" +#include "../core/mem/pn_memwatch.h" +#include "../core/mem/pn_ladder.h" +#include "../core/qweight.h" +#include "../formats/gguf_layer_bytes.h" +#include "gpu_attn_path_tracker.h" +#include "gpu_graph_capture_tracker.h" +#include "qwen35moe_internal.h" +#include "qwen35moe_kv_q4.h" + +#ifdef USE_CUDA + +#include "cuda/fc_cuda.h" +#include "barswap/barswap.h" + +/* The no-placement boundary default (the legacy single-model serve path only — + * an explicit placement supersedes it entirely). With no QWEN35_GPU_SPLIT_LAYER + * override the split is disabled for a model of this depth or shallower, so + * every layer runs on one die (the validated single-die path). A boundary below + * n_layer implies two dies, named by QWEN35_GPU_DIE_A/B. See gpu_plan_dies(). */ +#define GPU_DIE_SPLIT_LAYER 40 + +/* Default die ids for an unplaced 2-die split (override via QWEN35_GPU_DIE_A / + * QWEN35_GPU_DIE_B). Two dies of one K80 card. */ +#define GPU_DIE_A_DEFAULT 0 +#define GPU_DIE_B_DEFAULT 1 + +/* Cap for the host-side attention-softmax scratch and the top-k expert + * selection read-back. Matches the QWEN35_MAX_SEQ ceiling + * (PN_QWEN35MOE_MAX_SEQ_CAP, see qwen35moe_forward.c). At 256K the per-head + * scores array is 1 MB, which is too large for the stack — the three + * host-softmax sites therefore malloc a `max_seq`-sized scratch on the heap + * (freed at end of block) rather than stack-allocating this cap. Top-k <= 32. */ +#define PN_QWEN35MOE_GPU_MAX_SEQ_SCORES 262144 +#define PN_QWEN35MOE_GPU_MAX_TOPK 32 + +/* Decode-mode flash attention (QWEN35_FLASH_DECODE): the single decode query + * splits the cached context across blocks so the die is not near-idle (npos=1 + * prefill launches only n_head_kv=2 blocks). Aim for ~KEYS_PER_SPLIT keys per + * block, capped at SPLIT_MAX splits (partial scratch scales with the cap). */ +#define PN_QWEN35MOE_DEC_KEYS_PER_SPLIT 256 +#define PN_QWEN35MOE_DEC_SPLIT_MAX 32 + +/* Multi-user decode: max concurrent slots (per-user SSM state images are + * ~2.1 MB per SSM layer per slot; 16 slots is ~1 GB across the model — + * within a die pair's headroom next to the split weights). */ +#define PN_QWEN35MOE_GPU_MAX_SLOTS 16 + +/* ============================================================ + * GPU-resident weight handle + * + * One fc_buffer per weight matrix (raw quant bytes, uploaded once) plus the + * F32 norm/router vectors. Held on the model via pn_qwen35moe_model_set_gpu so + * the per-token forward never re-uploads. Mirrors rwkvx.c's persistent + * q->cuda_data pattern, generalized to the qwen35moe layer zoo. + * ============================================================ */ + +/* A single weight matrix resident on the GPU: its raw quant bytes plus the dims + * and quant type the matvec kernel needs. row_stride is the qweight's + * bytes-per-row (for Q3_K, (cols/256)*110). */ +typedef struct { + fc_buffer *bytes; /* raw quant bytes uploaded H2D (NULL if absent) */ + pn_gguf_quant type; /* quant type (drives kernel dispatch) */ + int rows; + int cols; + int row_stride; /* bytes per row */ + const pn_qweight *cpu; /* back-pointer to the CPU qweight (host fallback) */ + fc_cuda_ctx *ctx; /* owning die context (which die holds this weight) */ +} gpu_weight; + +/* GPU copy of one F32 vector (norms, router bias, etc.). */ +typedef struct { + fc_buffer *buf; + int n; + fc_cuda_ctx *ctx; /* owning die context */ +} gpu_vec; + +/* GPU MoE block weights (router + per-expert SwiGLU + shared expert). + * + * Grouped mode (grouped == 1, the fast path): the merged 3D GGUF expert + * tensors are uploaded as ONE contiguous buffer per matrix (gate_all/up_all/ + * down_all) and the whole routed-expert block runs in two launches + * (fc_moe_gateup_ctx + fc_moe_down_accum_ctx) that index experts on device + * from the router's top_idx — no per-expert buffers, no host read-back of the + * selection. Both launches dispatch on the tensor's gguf quant: requires + * gate/up Q3_K|Q4_K (gate and up the same) and down Q3_K|Q4_K|Q6_K, covering both + * the Q3_K_M mini's mixed layout and a pure Q4_K_M, plus contiguous expert + * slices; anything else falls back to the per-expert path + * (gate_exps/up_exps/down_exps + the old dispatch loop). */ +/* One cached fused-launch descriptor (fc_fused_multi): built lazily on first + * use from persistent weight/scratch buffers, then reused every token. desc + * == NULL after tried != 0 means the segment mix is unsupported and the + * caller keeps its unfused launch sequence. */ +typedef struct { + fc_buffer *desc; + int nseg; + int blocks; + int sh_bytes; + int tried; +} gpu_fused; + +typedef struct { + gpu_weight ffn_gate_inp; /* router logits */ + int grouped; /* 1 = grouped kernels, 0 = per-expert path */ + gpu_weight gate_all; /* merged [n_expert*ff, n_embd] (grouped) */ + gpu_weight up_all; /* merged [n_expert*ff, n_embd] (grouped) */ + gpu_weight down_all; /* merged [n_expert*n_embd, ff] (grouped) */ + gpu_weight *gate_exps; /* [n_expert] */ + gpu_weight *up_exps; /* [n_expert] */ + gpu_weight *down_exps; /* [n_expert] */ + gpu_weight ffn_gate_shexp; + gpu_weight ffn_up_shexp; + gpu_weight ffn_down_shexp; + gpu_vec ffn_gate_inp_shexp; /* shared-expert scalar gate [n_embd] */ + const float *gate_inp_shexp_host; /* host mmap pointer to the same vector + * (constant weights — saves a per-token + * D2H in the shared-gate dot) */ + int n_expert; + /* Fused launches: router + shexp scalar-gate dot + shexp gate/up SwiGLU + * in one kernel (fx_pre), and shexp down + sigmoid gate + combine + + * residual in one kernel (fx_post). */ + gpu_fused fx_pre; + gpu_fused fx_post; + int sdiag_logged; /* QWEN35_SDIAG: this layer's structure reported once */ +} gpu_moe; + +typedef struct { + gpu_vec attn_norm; + gpu_weight attn_qkv; + gpu_weight attn_gate; + gpu_vec ssm_conv1d; /* [conv_dim * ssm_conv] */ + gpu_vec ssm_a; /* [n_v_heads] */ + gpu_weight ssm_alpha; + gpu_weight ssm_beta; + gpu_vec ssm_dt; /* [n_v_heads] */ + gpu_vec ssm_norm; /* [ssm_state] */ + gpu_weight ssm_out; + gpu_vec post_attention_norm; + gpu_moe moe; + fc_cuda_ctx *ctx; /* die context this layer's weights live on */ + /* Persistent GPU-resident SSM dual state (conv + recurrent), allocated + * ONCE on first use and then living entirely on this layer's die — no + * per-token H2D/D2H round trip. The CONTENTS are (re)seeded from the host + * state by an H2D copy when host_state changes or a sequence restarts at + * pos 0; the buffer ADDRESSES never change after the first allocation, so + * the layer's captured CUDA graph (below) stays valid across re-seeds. */ + fc_buffer *conv_state_g; + fc_buffer *ssm_state_g; + const void *host_state; + /* Fused launches: the four from-x_norm projections (+ sigmoid/decay + * epilogues) in one kernel, and ssm_out + residual in one kernel. */ + gpu_fused fx_proj; + gpu_fused fx_out; + /* Captured CUDA graph of this layer's ENTIRE position-invariant launch + * sequence (attn_norm -> SSM block -> post_attention_norm -> MoE, ~13 + * launches), replayed with one cuGraphLaunch per token. Captured lazily + * at pos >= 1 (after the fused descriptors were built at pos 0). + * graph_capture owns the retry policy for a FAILED capture (bounded + * backoff, then a permanent stop) and the residency state the + * graphs_live/graphs_total gauge reads — see + * gpu_graph_capture_tracker.h. */ + fc_graph *graph; + pn_graph_capture_tracker graph_capture; + /* Set once this layer's body has run DIRECTLY (uncaptured) in this + * process — capture is only attempted after that, so the fused + * descriptors/scratch it builds on first use are never allocated + * mid-capture (cuMemAlloc during capture invalidates it; the trap + * fires when a BATCHED prefill precedes the first single-token + * forward, since the batch path never builds the fused descs). */ + int body_ran; + /* Speculative-verify snapshots (see pn_qwen35moe_spec_verify_tokens_cuda): + * per-position dual-state images written by the seq scans during a spec + * verify batch — conv_snaps[t] / ssm_snaps[t] = this layer's state AFTER + * position t — so a partial accept of M < n positions restores + * state-after-M with one D2D copy. Lazily allocated on the first spec + * verify, sized snaps_cap positions. */ + fc_buffer *conv_snaps; + fc_buffer *ssm_snaps; + int snaps_cap; + /* Multi-user decode slot state (see pn_qwen35moe_decode_slots_cuda): + * slot-major per-user dual-state images — conv_slots [slots_cap][conv_n], + * ssm_slots [slots_cap][ssm_n] — each slot advancing independently one + * token per batched decode step. Seeded per slot by the bind call + * (D2D from the live conv_state_g/ssm_state_g right after that user's + * prefill); sized by the bind's n_slots. */ + fc_buffer *conv_slots; + fc_buffer *ssm_slots; + int slots_cap; + /* L9 — captured CUDA graph of this SSM layer's BATCHED-decode body + * (attn_norm -> slot-SSM block -> post_attention_norm -> batched MoE, + * ~30 launches -> 1), replayed once per batched decode step. The slots + * path is position-invariant (per-slot device state at stable addresses, + * batch scratch persistent per die) so the same graph replays every + * step — the only per-step variance (the token embed + boundary handoff + * + logits fetch) stays OUTSIDE, exactly like the single-token per-layer + * SSM graph above. Keyed by the captured slot count: a change in nslots + * changes the launch grids, so the graph is dropped and recaptured. + * The 10 attention layers are NOT graphed (their per-layer host KV-mirror + * D2H sync breaks capture); this covers the 30 SSM layers = ~75% of the + * per-step launches. */ + fc_graph *slots_graph; + pn_graph_capture_tracker slots_graph_capture; + int slots_graph_n; /* nslots the graph was captured against (0 = none) */ + int slots_graph_base; /* L5 — slot_base baked into the graph's ring-offset + * views; a different group (die-interleave) reuses + * this same layer with a different base → the graph + * is dropped + recaptured on a base change. */ + const void *slots_graph_scratch; /* b->hidden addr baked into the graph: + * a batch-scratch realloc (a wider + * prefill) moves it → drop + recapture */ +} gpu_ssm_layer; + +typedef struct { + gpu_vec attn_norm; + gpu_weight attn_q; + gpu_weight attn_k; + gpu_weight attn_v; + gpu_vec attn_q_norm; /* [head_dim] */ + gpu_vec attn_k_norm; /* [head_dim] */ + gpu_weight attn_output; + gpu_vec post_attention_norm; + gpu_moe moe; + fc_cuda_ctx *ctx; /* die context this layer's weights live on */ + /* Fused launches: q/k/v projections in one kernel, and attn_output + + * residual in one kernel. */ + gpu_fused fx_qkv; + gpu_fused fx_out; + /* On-die flash-attention KV ring cache (batched prefill): K/V rows in + * the host cache's exact [n_head_kv][max_seq][head_dim] layout, on this + * layer's die. kv_host_id names the state whose cache the GPU copy + * mirrors (its host k pointer); kv_synced_pos is how many leading rows + * are valid — gpu_attn_kv_cache_sync re-uploads missing rows from the + * host mirror after a state switch or host-side (single-token decode) + * appends. */ + fc_buffer *k_cache_g; + fc_buffer *v_cache_g; + /* fp16 sink + recent-window shadow (q4 mode): the flash kernel reads these + * for the attention sink and the last W tokens instead of q4, so greedy + * argmax stops flipping. Sized to PN_KVQ4_SINKMAX / PN_KVQ4_WMAX rows/head; + * filled by the append kernel and re-uploaded from the host shadow on a + * non-contiguous re-sync. */ + fc_buffer *k_sink_g, *v_sink_g; + fc_buffer *k_win_g, *v_win_g; + /* Phase 9.2 — undo buffer for the recent-window ring during a speculative + * verify. The ring is keyed `pos % W`, so appending a row at position p + * overwrites the fp16 shadow of position p - W, which is STILL inside the + * window and still read. A rejected draft therefore has to put that slot + * back, and the only lossless copy is the one taken before the append (the + * q4 body is coarser, so restoring from it would not be bit-exact). + * [n_head_kv][spec_win_cap][head_dim] fp16, mirroring the ring's layout; + * spec_win_pos0/n record which verify the snapshot belongs to. */ + fc_buffer *k_win_snap_g, *v_win_snap_g; + int spec_win_cap; + int spec_win_pos0, spec_win_n; + int cache_cap; /* allocated max_seq rows (0 = none) */ + const void *kv_host_id; + int kv_synced_pos; + /* Decode split-attention partials (QWEN35_FLASH_DECODE): per (head, split) + * running max / sum and the unnormalized accumulator, on this die. Sized to + * PN_QWEN35MOE_DEC_SPLIT_MAX splits and merged by the combine kernel. */ + fc_buffer *dec_m_g, *dec_l_g, *dec_acc_g; + int dec_splits_cap; + /* Segment-graph (C1) capture gates, mirroring gpu_ssm_layer.body_ran: + * body_ran = this layer's full sandwich ran DIRECTLY once (fused descs + + * per-die scratch built, so nothing allocates mid-capture); flash_ran = + * the on-die decode-flash path (not the host-softmax fallback) ran, so + * the GPU KV ring + dec_* partial scratch exist. */ + int body_ran; + int flash_ran; + /* L7 — multi-user decode slot KV rings (QWEN35_FLASH_SLOTS): one ring + * per slot, [nslots][n_head_kv][max_seq][row], each slot slice + * byte-identical to the single-user ring above, on this layer's die, + * plus the per-slot fp16 sink/window shadows (q4 mode) and the per-slot + * decode-split partial scratch. kv_slot_host_id / kv_slot_synced are the + * per-slot watermarks (same convention as kv_host_id / kv_synced_pos); + * gpu_attn_block_slots_flash re-uploads missing rows from the host + * mirror, which the slots path keeps CURRENT every step (unlike the + * single-token B2 deferral), so bind/move/fallback stay trivially + * correct. */ + fc_buffer *k_slots_g, *v_slots_g; + fc_buffer *k_sink_slots_g, *v_sink_slots_g; + fc_buffer *k_win_slots_g, *v_win_slots_g; + fc_buffer *dec_slots_m_g, *dec_slots_l_g, *dec_slots_acc_g; + int kv_slots_cap_seq; /* allocated max_seq rows per slot (0 = none) */ + int kv_slots_cap_n; /* allocated slot count */ + int kv_slots_q4; /* cache mode the rings were allocated for */ + int dec_slots_cap; /* slots the dec partial scratch covers */ + /* Ring-allocation failure geometry (Phase 7). A layer whose per-slot KV + * rings do not fit records the geometry that failed. It used to STOP + * retrying that geometry forever, because without any brake the layer + * re-freed and re-attempted a multi-hundred-MiB cuMemAlloc on EVERY decode + * step (Phase 6 measured 3504 failed 144 MiB allocations over ~1750 steps + * at ctx_cap 131072 x K=4 — a 62% collapse instead of the ~10-15% + * host-softmax degradation the fallback intends). But a permanent latch + * means ONE transient failure pins the layer to the host path for the life + * of the process, which is a measured 10.7x cliff (13.02 -> 1.22 tok/s at + * depth 4096, K=2) that nothing reports. + * + * So the geometry is still recorded — it is what tells a SMALLER request + * apart from a same-or-larger one — but the retry policy now lives in + * `path` (gpu_attn_path_tracker.h): a bounded 8/32/128/512-eligible-step + * backoff for a transient failure, an immediate stop for one that could not + * fit an empty die, and an instant budget reset when the geometry shrinks. + * The per-step cost the latch was protecting against is gone independently: + * slots_ring_gen no longer bumps on an attempt that neither freed nor + * allocated anything, so a retry cannot invalidate every layer's captured + * graph. 0 = no failure recorded. */ + int kv_slots_fail_seq; /* max_seq of the failed ring allocation */ + int kv_slots_fail_n; /* slot count of the failed ring allocation */ + int kv_slots_fail_q4; /* cache mode of the failed ring allocation */ + int dec_slots_fail_n; /* slot count of the failed dec-scratch alloc */ + /* Which attention path this layer is on right now, why, and the ring + * allocation's retry budget. Read by pn_qwen35moe_gpu_attn_residency and + * published on /v1/models + /admin/models, so a host-softmax fallback is + * something an operator can SEE rather than infer from throughput. */ + pn_attn_path_tracker path; + const void *kv_slot_host_id[PN_QWEN35MOE_GPU_MAX_SLOTS]; + int kv_slot_synced[PN_QWEN35MOE_GPU_MAX_SLOTS]; + /* Phase 10b (lazy host mirror) — parallel per-slot bookkeeping. + * kv_slot_gen pairs with kv_slot_host_id: a watermark is valid only when + * the pointer AND the content generation match (a reset or snapshot- + * restored state keeps its pointer but bumps its generation). + * kv_slot_kv points at the bound kv struct so a ring realloc can rescue + * live rows into the host mirror before freeing the only copy. kv_gen is + * the single-user-ring counterpart, paired with kv_host_id. */ + unsigned kv_slot_gen[PN_QWEN35MOE_GPU_MAX_SLOTS]; + pn_qwen35moe_kv *kv_slot_kv[PN_QWEN35MOE_GPU_MAX_SLOTS]; + unsigned kv_gen; + int sdiag_flash_logged; /* QWEN35_SDIAG: flash/host decision reported */ + /* Bumped whenever the slot rings / decode partial scratch are (re)allocated + * — a captured graph holds their old device addresses. */ + unsigned slots_ring_gen; + /* L10 (Phase 3) — captured CUDA graph of this ATTENTION layer's batched- + * decode body: attn_norm -> on-die slots flash -> out-proj + residual -> + * post_attention_norm -> batched MoE. + * + * These layers used to report `graph=never` because the host KV mirror sat + * MID-body (a blocking fc_sync_ctx + D2H at step 6), and a mid-body host + * sync invalidates capture. It does not have to sit there: b->a_k / b->a_v + * still hold this layer's roped K/V after the whole sandwich runs (nothing + * downstream touches the attention scratch), so the mirror is now issued + * AFTER the body. The ring re-sync H2D — a no-op in steady state, live only + * right after a bind/move — is likewise hoisted AHEAD of the body. What is + * left inside is pure device work over persistent buffers. + * + * Position-invariance comes free: the slot kernels already read every + * user's position from the device pos buffer and recompute each user's + * split plan on-device. The one host-computed launch argument is + * `launch_splits` (the flash grid width = max over users of their split + * plan), which steps up once every `kps` tokens of context — so it is part + * of the key and costs at most PN_QWEN35MOE_DEC_SPLIT_MAX recaptures over + * a session's whole life. */ + fc_graph *slots_graph; + pn_graph_capture_tracker slots_graph_capture; + int slots_graph_n; /* nslots at capture (0 = none) */ + int slots_graph_base; /* slot_base at capture */ + int slots_graph_splits; /* launch_splits (flash grid) at capture */ + int slots_graph_q4; /* KV cache mode at capture */ + int slots_graph_max_seq; /* ring max_seq at capture */ + unsigned slots_graph_gen; /* slots_ring_gen at capture */ + const void *slots_graph_scratch; /* b->hidden addr baked into the graph */ +} gpu_attn_layer; + +/* Persistent per-die GPU scratch for the per-layer blocks. Every buffer is + * fully overwritten by its producer before use each call, so reuse across + * layers/tokens is safe. Killing the ~800 cuMemAlloc/cuMemFree pairs per token + * that the per-call allocations cost was worth ~30% of decode time. */ +typedef struct { + fc_cuda_ctx *ctx; /* die this scratch belongs to (NULL = unset) */ + /* SSM block */ + fc_buffer *s_qkv, *s_z, *s_beta, *s_alpha, *s_conv_out; + fc_buffer *s_qg, *s_kg, *s_vg, *s_gbuf, *s_y, *s_gated; + /* attention block */ + fc_buffer *a_qg, *a_q, *a_z, *a_k, *a_v, *a_out; + /* MoE block */ + fc_buffer *m_logits, *m_tidx, *m_twt, *m_gate, *m_up, *m_hid; + fc_buffer *m_exp, *m_out, *m_shexp; + fc_buffer *m_sgate; /* [1] sigmoid'd shared-expert gate (fused path) */ + /* residual stream + norm/block scratch + LM-head logits */ + fc_buffer *r_hidden, *r_xnorm, *r_blockout, *r_logits; + /* [1] int32 decode position for the segment graph (C1): the ONLY + * per-token input the captured attention kernels read. The host writes + * pos here (4-byte H2D, outside the graph) then replays the graph; the + * device-pos kernels (rope_pos, kv_append_dev, flash_decode_dev) derive + * everything position-dependent from it. */ + fc_buffer *dec_pos; +} gpu_die_scratch; + +static void gpu_die_scratch_free(gpu_die_scratch *sc); +static void gpu_fused_free(gpu_fused *f); + +/* One die segment's captured full-step graph (C1) + the KV geometry baked + * into its recorded kernels. */ +typedef struct { + fc_graph *graph; + pn_graph_capture_tracker capture; /* bounded retry + residency state */ + int cap_max_seq; /* kv->max_seq at capture (0 = no attn) */ + int cap_q4; /* kv->q4 at capture (-1 = no attn layers) */ +} gpu_seg_graph; + +/* Persistent per-die GPU scratch for the BATCHED (prefill) forward: the + * per-token buffers above, times the batch capacity n_cap, token-major + * ([n][dim]; position t's vector at offset t*dim). Allocated lazily on the + * first batch call and re-allocated only if a later batch is bigger. Every + * buffer is fully overwritten by its producer for the live n positions. */ +typedef struct { + fc_cuda_ctx *ctx; /* die this scratch belongs to (NULL = unset) */ + int n_cap; /* allocated batch capacity */ + /* residual stream + per-block scratch */ + fc_buffer *hidden, *x_norm, *block_out; /* [n][n_embd] */ + /* SSM block */ + fc_buffer *s_qkv, *s_conv; /* [n][conv_dim] */ + fc_buffer *s_z, *s_qg, *s_kg, *s_vg, *s_y, *s_gated;/* [n][val_dim] */ + fc_buffer *s_beta, *s_alpha, *s_g; /* [n][n_v_heads] */ + /* attention block */ + fc_buffer *a_qg; /* [n][2*attn_dim] */ + fc_buffer *a_q, *a_z, *a_out; /* [n][attn_dim] */ + fc_buffer *a_k, *a_v; /* [n][kv_dim] */ + /* MoE block */ + fc_buffer *m_logits; /* [n][n_expert] */ + fc_buffer *m_tidx, *m_twt; /* [n][k] */ + fc_buffer *m_gate, *m_up; /* [n][shared_ff] */ + fc_buffer *m_hid; /* [n][k*expert_ff] */ + fc_buffer *m_out, *m_shexp; /* [n][n_embd] */ + /* Expert-union dedup request lists — allocated ONLY when + * gpu_moe_dedup_enabled(), so the default-off build carries no extra + * VRAM and the decode path is untouched. */ + fc_buffer *m_ecount; /* [n_expert] int32 */ + fc_buffer *m_ereq; /* [n_expert][n] int32 */ + /* Compacted (expert, request-tile) work list, for the compacted-grid + * gate+up (FC_MOE_DEDUP_COMPACT, default off). [fc_moe_dedup_wmax()] int2, + * i.e. 2.8 KB at the prefill shape. */ + fc_buffer *m_wlist; +} gpu_batch_scratch; + +static void gpu_batch_scratch_free(gpu_batch_scratch *b); + +/* Host-side scratch carried across the GPU forward (see the section header + * further down). Owned by pn_qwen35moe_gpu so the buffers are allocated once, + * not per token (3 x n_vocab-float callocs per token was measurable mmap/ + * page-fault churn on the 2-core K80 host). */ +typedef struct { + float *a; /* general scratch, >= max(conv_dim, n_head*head_dim*2, val_dim) */ + float *b; + float *c; + int cap; +} gpu_host_scratch; + +static void gpu_host_scratch_free(gpu_host_scratch *h); + +/* QWEN35_ASYNC_KVMIRROR — deferred host-KV-mirror store (per die). + * + * The flash-attn slots path (gpu_attn_block_slots_flash step 6) normally does a + * BLOCKING fc_sync_ctx + cuMemcpyDtoH of the roped K/V into the host mirror at + * EVERY attention layer. On the single-threaded 2-die driver that per-layer + * stream drain defeats the die-pipeline's overlap (a die can't stay busy while + * the host blocks reaping the other's mirror copy). + * + * The host mirror is only READ at step BOUNDARIES (slot bind / move / session + * preserve) or to re-sync the device ring right after such a boundary — never + * mid-step in steady-state flash decode (the device ring is authoritative). So + * the mirror store can be safely deferred: per attention layer we (1) queue an + * ASYNC DtoH of that layer's roped K/V into a per-layer host staging slot on + * the die's own stream (no drain), and (2) record a pending-store descriptor. + * At the END of the decode step, once per die, we fc_sync_ctx ONCE (draining + * the already-queued async copies) and replay every pending host store. The + * mirror is thus identical to the per-layer path by the time any boundary + * consumer runs, but zero per-layer host stalls happen in between. + * + * Each descriptor carries everything the store loop needs; the staged K/V bytes + * live in a per-die growable arena (kv_stage) so overlapping layers never + * collide. Gated behind QWEN35_ASYNC_KVMIRROR (default OFF). */ +typedef struct { + pn_qwen35moe_kv *kv[PN_QWEN35MOE_GPU_MAX_SLOTS]; /* per-user host cache */ + int pos[PN_QWEN35MOE_GPU_MAX_SLOTS]; /* this slot's position */ + int nslots; + int kv_q4; + int kv_sink, kv_win; + int n_head_kv, head_dim, max_seq, kv_dim; + size_t stage_off; /* byte offset into the die arena of this layer's K||V */ +} qw_kv_store; + +typedef struct { + qw_kv_store *stores; /* [n_cap] pending descriptors this step */ + int n; /* live descriptor count */ + int n_cap; + float *stage; /* host staging arena: 2*kv_dim floats per descriptor */ + size_t stage_bytes; /* allocated arena size */ + size_t stage_used; /* bytes handed out this step */ +} qw_kv_defer; + +/* Top-level GPU residency handle, stored on the model. + * + * The model's layers are split across `n_die` dies (1 .. the placement width + * the die manager bound). Die i owns global layers [layer_lo[i], layer_hi[i]), + * planned by pn_plan_die_layer_split — the SAME partitioner admission prices + * the load with, so what is projected and what is uploaded cannot disagree. + * token_embd is never uploaded (the embedding is a host row copy at embed + * time); the final norm + LM head ride on the LAST die. + * + * Single-die (n_die == 1): head_ctx is NULL, every layer's .ctx == ctx[0], bs + * is NULL (no barswap needed — the operator acquires one die externally). + * + * Split (n_die >= 2): each context is bound to its own die's CUDA ordinal in + * small-BAR mode, or to ordinal 0 while that die sits in the barswap MMIO slot + * in the legacy regime, where the forward bs_acquire()s the die before touching + * its context. Between consecutive dies the forward hands the residual stream + * across — device-to-device when the pair has peer access (peer_ok[i]), else + * staged through the host. */ +typedef struct pn_qwen35moe_gpu { + gpu_ssm_layer *ssm; /* [n_ssm] */ + gpu_attn_layer *attn; /* [n_attn] */ + int n_ssm; + int n_attn; + gpu_vec output_norm; /* final RMSNorm [n_embd] (on the LAST die) */ + gpu_weight output; /* LM head (on the LAST die) */ + /* token_embd is read host-side at embed time (just a row copy), so it is + * not uploaded; the embedded row is uploaded fresh each token to ctx[0]. */ + + /* T3 — vocab-split LM head (QWEN35_VOCAB_SPLIT, EXACTLY 2 dies). It pairs + * the head die with die 0 specifically; at n_die > 2 those are not adjacent + * in the pipeline and the head's partner would be a die the residual stream + * left several hops ago, so the path stays off (see vocab_split_eligible). + * When ON the LM head is split by vocab rows: die A holds the TOP half + * [0, vocab_half) + * in output_a + a copy of the final norm (output_norm_a), die B holds the + * BOTTOM half [vocab_half, n_vocab) in `output` (re-uploaded to the slice). + * Memory-neutral: die B's head shrinks to a half exactly as die A grows by + * a half. After the final norm on die B, x_norm [K][n_embd] is P2P-copied + * to die A (xnorm_a); each die computes its half's logits + local top-k and + * the two per-slot top-k lists are merged order-stably on the host. Head- + * phase only (once per step, outside the layer loop) → no per-layer sync + * hazard. vocab_split_ready gates the whole path; falls back to the single- + * die `output` head if setup failed / peer unavailable. */ + gpu_weight output_a; /* LM head TOP half [0,vocab_half) (on ctx_a) */ + gpu_weight output_b; /* LM head BOTTOM half [vocab_half,nv) (ctx_b) */ + gpu_vec output_norm_a; /* final RMSNorm copy (on ctx_a) */ + fc_buffer *xnorm_a; /* [cap][n_embd] x_norm mirror on die A */ + int xnorm_a_cap; + fc_buffer *spec_logits_a; /* [cap][vocab_half] logits on die A */ + int spec_logits_a_cap; + fc_buffer *spec_logits_b; /* [cap][n_vocab-vocab_half] logits on die B */ + int spec_logits_b_cap; + int vocab_half; /* rows in the die-A (top) half */ + int vocab_split_ready; /* 1 = split head uploaded + peer enabled */ + + /* N-die residency / multiplexing. Die i holds layers [layer_lo[i], + * layer_hi[i]); the ranges come from pn_plan_die_layer_split. */ + int n_die; /* 1 .. PN_DIE_LAYER_SPLIT_MAX_DIES */ + fc_cuda_ctx *ctx[PN_DIE_LAYER_SPLIT_MAX_DIES]; + int die[PN_DIE_LAYER_SPLIT_MAX_DIES]; /* CUDA ordinal / barswap die id */ + int layer_lo[PN_DIE_LAYER_SPLIT_MAX_DIES]; + int layer_hi[PN_DIE_LAYER_SPLIT_MAX_DIES]; + /* peer_ok[i] = the die i -> die i+1 hand-off can go device-to-device. */ + int peer_ok[PN_DIE_LAYER_SPLIT_MAX_DIES]; + /* The die the final norm + LM head live on: the LAST one. NULL/-1 mirrors + * are kept for the head die rather than recomputed at every use, because + * "the head's context" is read far more often than the split changes. */ + fc_cuda_ctx *head_ctx; /* ctx[n_die-1], or NULL when n_die == 1 */ + int head_die; /* die[n_die-1] */ + int split_layer; /* layer_hi[0]; == n_layer when n_die == 1 */ + bs_context *bs; /* barswap handle (NULL in single-die mode) */ + int active_die; /* die currently in the MMIO slot (-1 = none) */ + + /* Lazily-created persistent scratch, one per die (see gpu_die_scratch). */ + gpu_die_scratch scratch[PN_DIE_LAYER_SPLIT_MAX_DIES]; + + /* Batched-forward scratch, one per die, plus its host staging regions + * (sized for bhost_cap positions; see gpu_get_batch_host). */ + gpu_batch_scratch bscratch[PN_DIE_LAYER_SPLIT_MAX_DIES]; + float *bhost[4]; + /* L5 — a SECOND host-staging set so the die-pipeline can run group0's + * die-B host mirror concurrently with group1's die-A host mirror without + * the two colliding on shared bhost regions. Lazily allocated (only when + * QWEN35_DIE_PIPELINE engages) at the same per_pos*cap size as bhost. */ + float *bhost2[4]; + int bhost_cap; + + /* Persistent host-side scratch (allocated on first forward). */ + gpu_host_scratch host; + int host_ok; + + /* Device top-k logits fetch (QWEN35_GPU_TOPK, see gpu_fetch_logits): the + * bookkeeping for the sparse host presentation lives on the pn_qwen35moe_state + * that OWNS the host logits buffer (s->topk_filled / s->topk_prev_ids / + * s->topk_prev_n), not here — see the comment on gpu_fetch_logits. */ + + /* Speculative-verify bookkeeping: [n][n_vocab] LM-head buffer on the + * head's die (lazily sized spec_logits_cap positions) and the position + * count of the LAST verify (0 = none pending) — spec_commit validates + * n_accept against it. */ + fc_buffer *spec_logits; + int spec_logits_cap; + int spec_n; + /* Phase 9.2 — the state and base position of that pending verify, so + * spec_commit can fold the ACCEPTED KV rows into each attention layer's + * ring watermark (the verify itself leaves it at the committed frontier; + * see gpu_attn_block_batch_flash). NULL/0 when no verify is pending. */ + pn_qwen35moe_state *spec_state; + int spec_pos0; + + /* Multi-user decode bookkeeping (see the decode-slots section): + * which host state each slot is bound to and the per-die int32 RoPE + * position buffers (indexed by die). The sparse top-k presentation state + * is NOT per slot — it belongs to the per-user pn_qwen35moe_state whose + * logits buffer it describes. */ + const void *slot_state[PN_QWEN35MOE_GPU_MAX_SLOTS]; + int n_slots; /* bound slot count (0 = none) */ + fc_buffer *pos_buf[PN_DIE_LAYER_SPLIT_MAX_DIES]; + int pos_buf_cap[PN_DIE_LAYER_SPLIT_MAX_DIES]; + + /* Full-step segment graphs (C1, QWEN35_GRAPH_FULL): ONE captured CUDA + * graph per die segment covering EVERY layer body in that segment (SSM + * and attention interleaved) — the whole per-die decode step replays + * with a single cuGraphLaunch. Indexed by die: seg[i] covers layers + * [layer_lo[i], layer_hi[i]); single-die uses seg[0] for all layers. The + * embed upload, the boundary hand-offs and the end-of-step logits fetch + * stay OUTSIDE (they host-sync). cap_max_seq / cap_q4 are the KV geometry + * baked into the captured kernels — replay is refused (and the graph + * dropped for recapture if the ring reallocates) when the live state + * disagrees. */ + gpu_seg_graph seg[PN_DIE_LAYER_SPLIT_MAX_DIES]; + + /* C2 (QWEN35_ASYNC_SCHED): the in-flight ASYNC decode step's phase + * machine — begin() launches a die segment and returns without blocking; + * poll() reaps completion via a CUDA event and advances to the next + * phase, so a 2-core host keeps many cards' steps in flight at once. + * One slot per model suffices: the serve layer's per-model gen_lock + * already serializes generations on this model, and begin/poll are + * called from that single generation thread. as_ev[i] belongs to ctx[i] + * (lazily created; a failed create latches via as_ev_tried and async stays + * off — the sync path is unaffected). The phase machine itself is a + * two-segment one, so async engages only at n_die <= 2 (see + * pn_qwen35moe_decode_begin_cuda). */ + int as_phase; /* qw_async_phase */ + int as_pos; /* position of the in-flight token */ + fc_event *as_ev[PN_DIE_LAYER_SPLIT_MAX_DIES]; + int as_ev_tried[PN_DIE_LAYER_SPLIT_MAX_DIES]; + + /* QWEN35_ASYNC_KVMIRROR — per-die deferred host-KV-mirror store state, + * indexed by die (see qw_kv_defer above). */ + qw_kv_defer defer[PN_DIE_LAYER_SPLIT_MAX_DIES]; + + /* QWEN35_TWO_THREAD — persistent per-die worker threads (index 0 = die A, + * 1 = die B). Each thread owns its die's CUDA context launch queue, so the + * two dies' kernel launches are issued in parallel instead of serialized on + * one host thread. Lazily spun up on first threaded decode; torn down at + * model free. See qw_tt_* below. */ + struct qw_tt *tt; + + /* Intrusive link in the process-wide registry of live GPU contexts (see + * qw_gpu_register below). Typed void* only because this struct is an + * anonymous typedef; it always holds a pn_qwen35moe_gpu *. */ + void *reg_next; +} pn_qwen35moe_gpu; + +static void qw_defer_free(pn_qwen35moe_gpu *g); /* teardown (impl below) */ +static void qw_tt_free(pn_qwen35moe_gpu *g); /* TT teardown (impl below) */ + +/* ============================================================ + * Live-GPU-context registry — the session-state lifetime hook + * + * gpu_attn_layer keeps raw per-slot back-pointers into a session's + * pn_qwen35moe_state (kv_slot_kv, kv_slot_host_id) and gpu_ssm_layer keeps one + * (host_state). pn_qwen35moe_state_free frees that state and has no model + * handle, so it cannot reach the GPU layers directly — which is how a freed + * kv survived as a dangling back-pointer and got dereferenced by the next + * bind's lazy-mirror park copy (heap-use-after-free). + * + * The lifetime is broken at the free site instead of guarded at the use site: + * every live GPU context is registered here, and pn_qwen35moe_state_free calls + * pn_qwen35moe_gpu_forget_state_cuda, which drops every reference to the dying + * state before its memory goes back to the allocator. A stale back-pointer + * therefore cannot exist, so no consumer needs to (and none can) test one. + * + * The list is touched only at model load/free and session free, so a plain + * mutex costs nothing measurable. + * ============================================================ */ +static pn_qwen35moe_gpu *qw_gpu_reg_head = NULL; +static pthread_mutex_t qw_gpu_reg_mtx = PTHREAD_MUTEX_INITIALIZER; + +static void qw_gpu_register(pn_qwen35moe_gpu *g) +{ + pthread_mutex_lock(&qw_gpu_reg_mtx); + g->reg_next = qw_gpu_reg_head; + qw_gpu_reg_head = g; + pthread_mutex_unlock(&qw_gpu_reg_mtx); +} + +static void qw_gpu_unregister(pn_qwen35moe_gpu *g) +{ + pthread_mutex_lock(&qw_gpu_reg_mtx); + pn_qwen35moe_gpu **pp = &qw_gpu_reg_head; + while (*pp) { + if (*pp == g) { + *pp = (pn_qwen35moe_gpu *)g->reg_next; + g->reg_next = NULL; + break; + } + pp = (pn_qwen35moe_gpu **)&(*pp)->reg_next; + } + pthread_mutex_unlock(&qw_gpu_reg_mtx); +} + + +/* MemoryAware Phase 1: publish one die's post-upload VRAM truth (cuMemGetInfo + * free + our own per-ctx alloc tally) into the live pn_mem_state, so admission + * schedules against real numbers from the moment the model is resident. */ +static void gpu_feed_die_mem_state(fc_cuda_ctx *ctx, int die_id) +{ + if (!ctx || !pn_memwatch_running()) + return; + size_t die_free = 0, die_total = 0; + if (fc_mem_info_ctx(ctx, &die_free, &die_total) != 0 || die_total == 0) + return; + uint64_t reserve = pn_die_reserve_bytes(); + uint64_t budget = die_total > reserve ? die_total - reserve : 0; + pn_memwatch_set_die(die_id, die_free, budget, fc_ctx_alloc_bytes(ctx)); +} + +/* The KV window admission schedules against (mirrors qwen35moe_max_seq's + * QWEN35_MAX_SEQ clamp). KV Phase 1: a per-model ctx_cap (set at load) wins so + * the model-load fit gate budgets the SAME window the ring is sized to. Pass + * the model to honor its cap; NULL keeps the global env behavior. */ +static int gpu_admit_max_ctx_model(const pn_qwen35moe_model *model) +{ + const pn_qwen35moe_config *cfg = + model ? pn_qwen35moe_model_config(model) : NULL; + int max_ctx; + if (cfg && cfg->ctx_cap > 0) { + max_ctx = cfg->ctx_cap; + } else { + const char *seq_env = getenv("QWEN35_MAX_SEQ"); + max_ctx = seq_env ? atoi(seq_env) : 4096; + if (max_ctx <= 0) max_ctx = 4096; + } + /* Clamp to the SAME ceiling the KV ring is allocated against + * (qwen35moe_max_seq), so admission never promises less than the ring can + * hold. */ + if (max_ctx > PN_QWEN35MOE_MAX_SEQ_CAP) max_ctx = PN_QWEN35MOE_MAX_SEQ_CAP; + return max_ctx; +} + +/* VRAM a pn_qweight costs once uploaded, mirroring gpu_weight_upload's own + * arithmetic: the repacked row stride when the type repacks, the file stride + * otherwise. Needed for the LM head because a TIED model has no output.weight + * in the file at all — its head is the F32 materialization of token_embd + * (qwen35moe.c), i.e. ~4x what the tensor table would suggest. */ +static uint64_t gpu_qweight_upload_bytes(const pn_qweight *w) +{ + if (!w || !w->data || w->rows <= 0 || w->cols <= 0) + return 0; + size_t rstride = fc_repack_row_bytes((int)w->type, w->cols); + if (rstride == 0) + rstride = w->row_stride; + return (uint64_t)w->rows * (uint64_t)rstride; +} + +/* Bytes the LM head + final norm cost on whichever die holds them (die B in a + * 2-die split, die A single-die). */ +static uint64_t gpu_head_weight_bytes(pn_qwen35moe_model *model) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(model); + uint64_t head = gpu_qweight_upload_bytes(pn_qwen35moe_model_output(model)); + if (cfg && cfg->n_embd > 0) + head += (uint64_t)cfg->n_embd * sizeof(float); /* output_norm */ + return head; +} + +/* MemoryAware per-die fit gate (§6.1): verify ONE die of an N-die split — or + * the single die of an unsplit load, which is the same question at N=1 — can + * hold its slice of the weights (layers [layer_lo,layer_hi) plus whatever + * non-layer weight that die owns) + its layers' share of the KV @ max_ctx + the + * GPU scratch reserve. Feeds the die's measured truth into memwatch and warns + * on stranded VRAM (§8.2) on the way, so the check and the accounting always + * agree. Returns 0 to proceed, -1 to refuse — the caller fails the load cleanly + * instead of letting a doomed multi-minute upload cascade into `cuMemAlloc + * failed: 2`. Fail-open when the die's free/total cannot be read (no truth to + * gate against). */ +static int gpu_admit_die_share(pn_qwen35moe_model *model, fc_cuda_ctx *ctx, + int die_id, int layer_lo, int layer_hi, + uint64_t extra_weight_bytes, int n_layer, + int max_ctx, int kv_q4) +{ + int n_layers_here = layer_hi - layer_lo; + size_t die_free = 0, die_total = 0; + if (fc_mem_info_ctx(ctx, &die_free, &die_total) != 0 || die_total == 0) + return 0; + + uint64_t reserve = pn_die_reserve_bytes(); + uint64_t budget = die_total > reserve ? die_total - reserve : 0; + if (pn_memwatch_running()) + pn_memwatch_set_die(die_id, die_free, budget, fc_ctx_alloc_bytes(ctx)); + + if (die_free < die_total / 2) + fprintf(stderr, "[admit] WARNING die ordinal %d shows only " + "%llu / %llu MiB free — possible stranded VRAM from a " + "prior crash (see startup reclaim, MemoryAware §8.2)\n", + die_id, + (unsigned long long)(die_free >> 20), + (unsigned long long)(die_total >> 20)); + + /* This die's resident weight bytes = what its OWN layers weigh in the + * tensor table, plus any non-layer weight it holds (the LM head on die B). + * Not a prorated share of mmap_size: the file also carries token_embd — + * never uploaded, the embed is a host row copy — and output.weight, which + * belongs to one die. Prorating charged every die for both. On + * Qwen3.6-35B-A3B Q4_K_M that over-projected a die by ~1.6 GiB and refused + * a split that in fact fits with ~1.3 GiB of headroom per die. + * Falls back to the old proration when the table yields no blk.* tensors, + * so an unfamiliar layout is gated conservatively rather than blind. */ + pn_gguf_file *gf = pn_qwen35moe_gguf(model); + uint64_t weights_bytes = pn_gguf_layer_range_bytes(gf, layer_lo, layer_hi); + if (weights_bytes > 0) { + weights_bytes += extra_weight_bytes; + } else { + uint64_t file_bytes = gf ? (uint64_t)gf->mmap_size : 0; + weights_bytes = (n_layer > 0) + ? file_bytes * (uint64_t)n_layers_here / (uint64_t)n_layer + : file_bytes; + } + /* Real whole-model KV/token from THIS model's attention-layer geometry + * (KV Phase 1), scaled by this die's layer share for the split-load check. */ + uint64_t per_tok = pn_qwen35moe_kv_bytes_per_token(model, kv_q4 != 0); + uint64_t kv_bytes = pn_admit_die_kv_bytes(per_tok, kv_q4 != 0, max_ctx, + n_layers_here, n_layer); + + uint64_t need = 0; + if (!pn_admit_model_load_die(weights_bytes, kv_bytes, max_ctx, die_free, + &need)) { + fprintf(stderr, "[admit] REFUSE model load: die ordinal %d has " + "%llu MiB free but its share needs ~%llu MiB " + "(%d/%d layers + KV@%d + scratch) — refusing before the " + "cuMemAlloc cascade\n", + die_id, + (unsigned long long)(die_free >> 20), + (unsigned long long)(need >> 20), + n_layers_here, n_layer, max_ctx); + return -1; + } + return 0; +} + +/* The boundary knob, or 0 when unset. QWEN35_GPU_SPLIT_LAYER predates + * placement and tunes ONE boundary, which describes a 2-die split completely + * and nothing wider — pn_plan_die_layer_split honours it at that width and + * ignores it above, and this reports the ignore rather than letting a knob + * quietly stop working. */ +static int gpu_split_layer_override(int n_die) +{ + const char *env = getenv("QWEN35_GPU_SPLIT_LAYER"); + if (!env) + return 0; + int v = atoi(env); + if (v <= 0) + return 0; + if (n_die > 2) { + fprintf(stderr, "qwen35moe: QWEN35_GPU_SPLIT_LAYER=%d ignored — it " + "names ONE boundary and this is a %d-die split (%d " + "boundaries); layers divide evenly instead\n", + v, n_die, n_die - 1); + return 0; + } + return v; +} + +/* Plan the layer split and the die ids it runs on, onto `g`. + * + * Explicit placement (pn_load_on) is authoritative: N placed dies mean an + * N-way split across exactly those dies, with the final norm + LM head on the + * last. That is the whole point — a placement of three dies used to bind three + * and upload a two-way split, so the third sat empty while the first two took + * half the model each and the second one OOM'd. + * + * With no placement at all the legacy single-model serve path applies + * unchanged: QWEN35_GPU_SPLIT_LAYER (default GPU_DIE_SPLIT_LAYER) picks the + * boundary, a boundary below n_layer implies two dies, and QWEN35_GPU_DIE_A/B + * name them. That is what a multi-die service unit would use. + * + * Returns 0 on success, -1 to refuse. A width this build cannot run is a + * REFUSAL, never a narrower split: silently splitting a 3-die placement two + * ways is the bug being fixed, and doing it "safely" would be the same bug. */ +static int gpu_plan_dies(const pn_qwen35moe_model *model, int n_layer, + pn_qwen35moe_gpu *g) +{ + const int *placed = NULL; + int n_placed = pn_qwen35moe_model_dies(model, &placed); + + if (n_placed > PN_DIE_LAYER_SPLIT_MAX_DIES) { + fprintf(stderr, "qwen35moe: REFUSE — placed on %d dies but this build " + "splits at most %d ways (PN_DIE_LAYER_SPLIT_MAX_DIES)\n", + n_placed, PN_DIE_LAYER_SPLIT_MAX_DIES); + return -1; + } + if (n_placed > n_layer) { + fprintf(stderr, "qwen35moe: REFUSE — placed on %d dies but the model " + "has only %d layers; a die with no layers is a mis-split, not " + "a narrower one\n", n_placed, n_layer); + return -1; + } + + int n_die; + if (n_placed >= 1 && placed) { + n_die = n_placed; + for (int i = 0; i < n_die; i++) + g->die[i] = placed[i]; + if (getenv("QWEN35_GPU_DIE_A") || getenv("QWEN35_GPU_DIE_B")) + fprintf(stderr, "qwen35moe: explicit die placement (%d dies) " + "supersedes QWEN35_GPU_DIE_A/B\n", n_die); + } else { + /* No placement: the legacy env/compile-time regime. */ + int boundary = GPU_DIE_SPLIT_LAYER; + const char *env = getenv("QWEN35_GPU_SPLIT_LAYER"); + if (env) { int v = atoi(env); if (v > 0) boundary = v; } + if (boundary > n_layer) + boundary = n_layer; + n_die = (boundary < n_layer) ? 2 : 1; + g->die[0] = GPU_DIE_A_DEFAULT; + g->die[1] = GPU_DIE_B_DEFAULT; + const char *ea = getenv("QWEN35_GPU_DIE_A"); + const char *eb = getenv("QWEN35_GPU_DIE_B"); + if (ea) g->die[0] = atoi(ea); + if (eb) g->die[1] = atoi(eb); + } + + /* A 1-die placement is authoritative — the boundary knob cannot carve it + * up — which pn_plan_die_layer_split enforces by ignoring the override at + * every width but 2. */ + pn_die_layer_split plan; + if (pn_plan_die_layer_split(n_layer, n_die, + gpu_split_layer_override(n_die), &plan) != 0) { + fprintf(stderr, "qwen35moe: REFUSE — cannot split %d layers across %d " + "dies\n", n_layer, n_die); + return -1; + } + for (int i = 0; i < n_die; i++) { + g->layer_lo[i] = plan.layer_lo[i]; + g->layer_hi[i] = plan.layer_hi[i]; + } + g->n_die = n_die; + g->head_die = g->die[n_die - 1]; + g->split_layer = g->layer_hi[0]; + return 0; +} + +/* Hand the residual stream across the boundary between die `i` and die `i+1`: + * `bytes` of token-major [positions][n_embd] f32 out of `src` (on die i) into + * `dst` (on die i+1). + * + * This is the whole cost of splitting a model: ONE contiguous activation copy + * per boundary per step. An N-die split pays it N-1 times, which is why more + * dies buys CAPACITY and not speed — nothing about the layer maths gets + * cheaper, and each hop adds a host sync plus a transfer. + * + * Device-to-device across the peer link where the pair has it (on a K80 that + * is the on-card PLX switch), else staged through the host buffer the caller + * supplies. Both cost the same bytes; the peer path saves a round trip through + * host memory and the PCIe hop in each direction. `src`'s die is synced first + * either way — the copy reads what the layer loop just wrote. + * + * Split in two because the legacy barswap regime holds ONE die in its MMIO + * slot at a time: the D2H must happen while the SENDING die is still selected + * and the H2D after the RECEIVING one is, and collapsing them into a single + * call put a sync + a read of the sending die's memory on the wrong side of + * the swap. Phase 1 is everything that must happen before the switch. */ +static void gpu_handoff_export(pn_qwen35moe_gpu *g, int from, fc_buffer *src, + void *host_stage, size_t bytes) +{ + fc_sync_ctx(g->ctx[from]); + if (!g->peer_ok[from]) + fc_buffer_to_cpu_n(src, host_stage, bytes); +} + +/* Phase 2: run this once die `from + 1` is the selected one and its scratch + * exists, so `dst` is a real buffer on the receiving die. A peer copy that + * fails here still has the host route available — peer_ok is only ever set + * when both dies are live (never under barswap), so reading `src` from the + * host at this point is legal. */ +static void gpu_handoff_import(pn_qwen35moe_gpu *g, int from, fc_buffer *src, + fc_buffer *dst, void *host_stage, size_t bytes) +{ + if (g->peer_ok[from]) { + if (fc_buffer_copy_peer(dst, src, bytes) == 0) { + /* Sync the RECEIVING die, not just the sending one. cuMemcpyPeer + * lands on its own context's stream, so without this the next + * die's first layer can read `dst` before the copy has committed — + * the residual stream is then whatever was in the buffer last + * step, and the model emits fluent-looking garbage rather than + * failing. This is the same sync the T3 vocab-split copy takes + * after its x_norm peer copy, for the same reason. */ + fc_sync_ctx(g->ctx[from + 1]); + return; + } + fc_buffer_to_cpu_n(src, host_stage, bytes); + } + fc_buffer_from_cpu_n(dst, host_stage, bytes); +} + +/* The index of the die whose context is `ctx`, or -1 if it is not one of ours. + * The per-die side tables (scratch, pos buffers, deferred KV mirrors, segment + * graphs, async events) are all keyed by this. It used to be the expression + * `ctx == g->ctx_b ? 1 : 0`, which silently answered "die 0" for every middle + * die of a wider split. */ +static int gpu_die_index_of_ctx(const pn_qwen35moe_gpu *g, + const fc_cuda_ctx *ctx) +{ + for (int i = 0; i < g->n_die; i++) + if (g->ctx[i] == ctx) + return i; + return -1; +} + +/* The CUDA ordinal / barswap die id behind `ctx`. Falls back to die 0 for an + * unknown context, matching the old expression's behaviour for the only case + * it could face (a single-die model's sole context). */ +static int gpu_die_of_ctx(const pn_qwen35moe_gpu *g, const fc_cuda_ctx *ctx) +{ + int i = gpu_die_index_of_ctx(g, ctx); + return g->die[i >= 0 ? i : 0]; +} + +/* Bring nvidia up after a die is acquired (blacklisted; each submodule needs + * --ignore-install). Mirrors compute_persist / the test harnesses. Returns 0 on + * success; harmless to call once per acquired die. */ +static int gpu_load_nvidia(void) +{ + if (system("modprobe --ignore-install nvidia 2>/dev/null") != 0) + return -1; + if (system("modprobe --ignore-install nvidia_uvm 2>/dev/null") != 0) + return -1; + return 0; +} + +/* Swap which die is in the barswap MMIO slot. No-op in single-die mode (bs + * NULL) or when the requested die is already active. Returns 0 on success. */ +static int gpu_select_die(pn_qwen35moe_gpu *g, int die) +{ + if (!g->bs) + return 0; /* single-die: caller acquired the die externally */ + if (g->active_die == die) + return 0; + if (!bs_acquire(g->bs, die)) { + fprintf(stderr, "qwen35moe: bs_acquire(die %d) failed\n", die); + return -1; + } + g->active_die = die; + return 0; +} + +/* ============================================================ + * Per-layer debug dump — IDENTICAL tags/format to the CPU path + * (qwen35moe_forward.c qwen35moe_dbg) so GPU output diffs against the CPU dump + * line-for-line. Gated by QWEN35_DEBUG. Takes a host buffer; GPU callers read + * back the relevant fc_buffer first (the dump is a validation aid, not a hot + * path, so the extra D2H copy is acceptable). + * ============================================================ */ + +/* ============================================================ + * Lightweight wall-clock profiler (QWEN35_PROF=1). Accumulates per-token time + * by matvec quant type (kernel vs host-fallback) and by forward phase, printed + * once per token. Adds fc_sync around timed ops for attribution, so enable + * only when profiling. + * ============================================================ */ + +#include + +static double prof_now(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int prof_on(void) +{ + static int v = -1; + if (v < 0) + v = getenv("QWEN35_PROF") ? 1 : 0; + return v; +} + +#define PROF_MAX_TYPE 40 +static double prof_mv_time[PROF_MAX_TYPE]; /* by quant type */ +static long prof_mv_cnt[PROF_MAX_TYPE]; +static double prof_mv_fallback; /* total host-fallback matvec time */ +/* phases: 0=ssm_state_rt 1=attn_host 2=moe_total 3=lm_head 4=logits_d2h + * 5=ssm_block 6=attn_block 7=moe_gateup 8=moe_down 9=moe_shexp */ +static double prof_phase[10]; + +static void prof_reset(void) +{ + memset(prof_mv_time, 0, sizeof(prof_mv_time)); + memset(prof_mv_cnt, 0, sizeof(prof_mv_cnt)); + memset(prof_phase, 0, sizeof(prof_phase)); + prof_mv_fallback = 0.0; +} + +static void prof_report(double total) +{ + fprintf(stderr, "PROF token total=%.3fs | matvec by type:", total); + for (int t = 0; t < PROF_MAX_TYPE; t++) + if (prof_mv_cnt[t]) + fprintf(stderr, " t%d=%.3fs/%ld", t, prof_mv_time[t], prof_mv_cnt[t]); + fprintf(stderr, " | fallback=%.3fs\n", prof_mv_fallback); + fprintf(stderr, + "PROF phases: ssm_state_rt=%.3f attn_host=%.3f moe=%.3f head=%.3f " + "logits_d2h=%.3f ssm_blk=%.3f attn_blk=%.3f moe_gateup=%.3f " + "moe_down=%.3f moe_shexp=%.3f\n", + prof_phase[0], prof_phase[1], prof_phase[2], prof_phase[3], + prof_phase[4], prof_phase[5], prof_phase[6], prof_phase[7], + prof_phase[8], prof_phase[9]); +} + +/* Batch-path (prefill) phase profiler, gated by QWEN35_PROF like the + * single-token profiler above: per-call values reset each batch call, + * cumulative values span a whole chunked prefill. bprof_lap syncs the die + * so each span's wall time is attributed to the phase that issued it. */ +enum { + BPROF_ATTN_MV = 0, /* attn q/k/v + out projections (GPU) */ + BPROF_ATTN_SPLIT, /* host q/gate split + its D2H/H2D (host path) */ + BPROF_ATTN_ROPE, /* qk-rmsnorm + partial rope (+ device q/z split) */ + BPROF_ATTN_FLASH, /* on-die kv sync/append + flash kernel + out gate */ + BPROF_ATTN_XFER, /* attention D2H/H2D (kv mirror / host-softmax IO) */ + BPROF_ATTN_HOST, /* HOST causal GQA softmax + kv append + out gate */ + BPROF_SSM, /* whole SSM block (projections + scans) */ + BPROF_MOE, /* whole MoE block (router + experts + shexp) */ + BPROF_COUNT +}; +static const char *bprof_name[BPROF_COUNT] = { + "attn_mv", "attn_split", "attn_rope", "attn_flash", "attn_xfer", + "attn_host", "ssm", "moe" +}; +static double bprof_call[BPROF_COUNT]; +static double bprof_cum[BPROF_COUNT]; +static double bprof_cum_total; +static long bprof_cum_ntok; + +static void bprof_add(int phase, double dt) +{ + bprof_call[phase] += dt; + bprof_cum[phase] += dt; +} + +/* Close the span started at *t0: drain ctx, book the time, restart. */ +static void bprof_lap(int phase, fc_cuda_ctx *ctx, double *t0) +{ + if (!prof_on()) + return; + fc_sync_ctx(ctx); + double t = prof_now(); + bprof_add(phase, t - *t0); + *t0 = t; +} + +static void bprof_report(int n, int pos0, double call_total) +{ + double call_sum = 0.0, cum_sum = 0.0; + bprof_cum_total += call_total; + bprof_cum_ntok += n; + fprintf(stderr, "BPROF call n=%d pos0=%d total=%.3fs |", n, pos0, + call_total); + for (int ph = 0; ph < BPROF_COUNT; ph++) { + fprintf(stderr, " %s=%.3f", bprof_name[ph], bprof_call[ph]); + call_sum += bprof_call[ph]; + cum_sum += bprof_cum[ph]; + } + fprintf(stderr, " other=%.3f\n", call_total - call_sum); + fprintf(stderr, "BPROF cum ntok=%ld total=%.3fs |", bprof_cum_ntok, + bprof_cum_total); + for (int ph = 0; ph < BPROF_COUNT; ph++) + fprintf(stderr, " %s=%.3f(%.1f%%)", bprof_name[ph], bprof_cum[ph], + 100.0 * bprof_cum[ph] / + (bprof_cum_total > 0 ? bprof_cum_total : 1.0)); + fprintf(stderr, " other=%.3f\n", bprof_cum_total - cum_sum); +} + +/* ============================================================ + * Slots-path (batched decode) phase profiler — QWEN35_SPROF=1 (M2). + * + * Sync-bracketed like bprof: each lap drains the issuing die so wall time is + * attributed to the phase that queued the work. Instrumentation only — the + * launch sequence and math are untouched; with the env unset every hook is a + * single predictable branch. The extra syncs inflate the absolute step time, + * so read the %% split from SPROF and the true step time from an unprofiled + * run. sprof_active is set only inside pn_qwen35moe_decode_slots_tokens_cuda, + * so the shared gpu_moe_block_batch laps stay dead on the prefill path. + * ============================================================ */ +enum { + SPROF_DENSE = 0, /* dense/shared matvecs (gpu_matvec_nx) in ssm/attn */ + SPROF_MOE_GATEUP, /* routed experts: grouped gate+up SwiGLU */ + SPROF_MOE_DOWN, /* routed experts: down proj + weighted accumulate */ + SPROF_MOE_SHEXP, /* router (+topk) + shared expert + combine */ + SPROF_MOE_ROWFB, /* M2b: per-row (non-grouped) MoE fallback loop */ + SPROF_ATTN_HOST, /* host GQA softmax + q/gate split + PCIe + syncs */ + SPROF_ATTN_FLASH, /* M2b: on-die slot flash (qk-norm/rope/append/kernel) */ + SPROF_ATTN_MIRROR, /* M2b: host KV-mirror D2H + per-slot store */ + SPROF_SSM, /* slot conv1d + recurrence + gated norm + glue */ + SPROF_LM_HEAD, /* final norm + ONE batched LM-head GEMM */ + SPROF_LOGITS, /* per-slot D2D + sync + top-k fetch loop */ + SPROF_XFER, /* embed H2D + die-boundary D2H/H2D hand-off */ + SPROF_NCOUNT +}; +static const char *sprof_pname[SPROF_NCOUNT] = { + "dense_nx", "moe_gateup", "moe_down", "shexp_router", "moe_rowfb", + "attn_host", "attn_flash", "attn_mirror", + "ssm_slots", "lm_head", "logits_loop", "embed_xfer" +}; +static double sprof_step_ph[SPROF_NCOUNT]; /* current step */ +static double sprof_cum_ph[SPROF_NCOUNT]; /* across steps */ +static double sprof_cum_total; +static long sprof_nsteps; +static int sprof_active; + +static int sprof_on(void) +{ + static int v = -1; + if (v < 0) + v = getenv("QWEN35_SPROF") ? 1 : 0; + return v; +} + +/* QWEN35_PROF_SUB (with QWEN35_PROF): sub-split BPROF's two big buckets on the + * batched PREFILL path — `moe=` into router+shared vs the two routed-expert + * kernels (by borrowing the sprof spans below, since gpu_moe_block_batch is + * shared with the slots path), and `ssm=` into projections / conv1d scan / + * qk-norm / recurrence scan / out. Costs the same extra syncs bprof does, and + * nests INSIDE the bprof spans, so it inflates the totals further still: read + * the split, never the absolute time. */ +static int prof_sub_on(void) +{ + static int v = -1; + if (v < 0) + v = getenv("QWEN35_PROF_SUB") ? 1 : 0; + return v; +} + +/* SSM-block sub-phases (QWEN35_PROF_SUB). Kept out of BPROF_COUNT so the + * existing BPROF line and its `other=` residual are unchanged; ssm_lap does + * not touch the enclosing BPROF_SSM span's clock, so ssm= stays the whole + * block and these five partition it. */ +enum { + SSMP_PROJ = 0, /* 4 projections + sigmoid + decay gate */ + SSMP_CONV, /* fc_ssm_conv1d_silu_seq — sequential scan over n */ + SSMP_QKNORM, /* fc_ssm_qknorm_gqa_nx */ + SSMP_RECUR, /* fc_ssm_recurrence_seq — sequential scan over n */ + SSMP_OUT, /* gated norm + out proj + residual add */ + SSMP_COUNT +}; +static const char *ssmp_name[SSMP_COUNT] = { + "proj", "conv_seq", "qknorm", "recur_seq", "out" +}; +static double ssmp_call[SSMP_COUNT]; +static double ssmp_cum[SSMP_COUNT]; + +static void ssm_lap(int phase, fc_cuda_ctx *ctx, double *t0) +{ + if (!prof_sub_on()) + return; + fc_sync_ctx(ctx); + double t = prof_now(); + ssmp_call[phase] += t - *t0; + ssmp_cum[phase] += t - *t0; + *t0 = t; +} + +/* ============================================================ + * DECODE-slots sub-spans — QWEN35_SPROF=1 *and* QWEN35_PROF_SUB=1. + * + * `ssm_slots` and `attn_flash` are each ~6.7 ms of the batched decode step — + * individually larger than the LM head — and neither had ever been split + * internally: ssm_lap above only ever fired on the batched PREFILL path. These + * two tables split them on the path the fleet actually decodes on, so the + * fusion question ("is the chain's cost the GDN recurrence itself, or the glue + * between the launches?") is answered with a measurement instead of a guess. + * + * Gating is deliberately the CONJUNCTION of both envs. The laps sync, and a + * sync mid-layer breaks CUDA-graph capture — sprof already stands the slots + * path down from graphs when it is active, so riding on sprof_active keeps + * every new sync inside a configuration that had already given graphs up. + * With QWEN35_PROF_SUB alone (the prefill split) the decode path is untouched. + * + * READ THE SPLIT, NOT THE ABSOLUTE. Two distortions run the same direction: + * the extra syncs serialise work the real path overlaps, and graphs are off, + * so per-launch overhead is charged at its un-amortised price. That makes the + * glue share an UPPER BOUND on what fusion could recover under graphs. + * ============================================================ */ +enum { + SSMD_DECAY = 0, /* fc_sigmoid + fc_ssm_decay_gate_nx */ + SSMD_CONV, /* fc_ssm_conv1d_silu_slots */ + SSMD_QKNORM, /* fc_ssm_qknorm_gqa_nx */ + SSMD_RECUR, /* fc_ssm_recurrence_slots — the GDN recurrence proper */ + SSMD_GNORM, /* fc_ssm_gated_norm */ + SSMD_COUNT +}; +static const char *ssmd_name[SSMD_COUNT] = { + "decay_gate", "conv1d", "qknorm", "recur", "gated_norm" +}; +/* Launches per lap, so a ms/launch figure separates "this kernel is slow" from + * "this lap holds two launches". Kept beside the names so they cannot drift. */ +static const int ssmd_launches[SSMD_COUNT] = { 2, 1, 1, 1, 1 }; + +enum { + ATTD_QZSPLIT = 0, /* fc_attn_qz_split */ + ATTD_QKNORM, /* fc_qk_rms_norm x2 (q, k) */ + ATTD_ROPE, /* fc_rope_neox_partial_pos x2 (q, k) */ + ATTD_KVAPPEND, /* fc_attn_kv_append_slots[_q4] */ + ATTD_FLASH, /* fc_attn_flash_decode_slots[_q4] — split-K + combine */ + ATTD_OUTGATE, /* fc_sigmoid + fc_mul */ + ATTD_COUNT +}; +static const char *attd_name[ATTD_COUNT] = { + "qz_split", "qknorm", "rope", "kv_append", "flash", "out_gate" +}; +/* -1 = not a fixed count: the flash lap dispatches launch_splits split-K + * launches plus a combine, so it varies with KV depth. */ +static const int attd_launches[ATTD_COUNT] = { 1, 2, 2, 1, -1, 2 }; + +static double ssmd_cum[SSMD_COUNT], attd_cum[ATTD_COUNT]; +static long ssmd_laps[SSMD_COUNT], attd_laps[ATTD_COUNT]; + +/* Both decode tables ride on sprof_active so they can never fire on a + * graph-captured step. */ +static int sub_decode_on(void) +{ + return sprof_active && prof_sub_on(); +} + +static void sub_begin(fc_cuda_ctx *ctx, double *t0) +{ + if (!sub_decode_on()) + return; + fc_sync_ctx(ctx); + *t0 = prof_now(); +} + +static void sub_lap(double *cum, long *laps, fc_cuda_ctx *ctx, double *t0) +{ + if (!sub_decode_on()) + return; + fc_sync_ctx(ctx); + double t = prof_now(); + *cum += t - *t0; + (*laps)++; + *t0 = t; +} + +/* One table per phase: total ms/step, share of the phase, laps per step (= the + * layer count the phase runs over) and ms per launch. `laps/step` is the number + * that turns a share into a fusion argument: it says how many times the chain + * pays whatever per-launch cost the glue rows are measuring. */ +static void sub_decode_table(const char *tag, int n, const char *const *name, + const double *cum, const long *laps, + const int *launches, long nsteps) +{ + double tot = 0.0; + for (int i = 0; i < n; i++) + tot += cum[i]; + if (tot <= 0.0 || nsteps <= 0) + return; + fprintf(stderr, "SUBDEC %s steps=%ld phase=%.2fms/step |", tag, nsteps, + 1e3 * tot / (double)nsteps); + for (int i = 0; i < n; i++) { + double per_step_ms = 1e3 * cum[i] / (double)nsteps; + double lps = (double)laps[i] / (double)nsteps; + fprintf(stderr, " %s=%.2fms(%.0f%%,laps/step=%.0f", name[i], + per_step_ms, 100.0 * cum[i] / tot, lps); + if (launches[i] > 0 && laps[i] > 0) + fprintf(stderr, ",%.3fms/launch", + per_step_ms / (lps * (double)launches[i])); + fprintf(stderr, ")"); + } + fprintf(stderr, "\n"); +} + +static void sub_decode_report(void) +{ + if (!sub_decode_on()) + return; + sub_decode_table("ssm_slots", SSMD_COUNT, ssmd_name, ssmd_cum, ssmd_laps, + ssmd_launches, sprof_nsteps); + sub_decode_table("attn_flash", ATTD_COUNT, attd_name, attd_cum, attd_laps, + attd_launches, sprof_nsteps); +} + +/* Open a span: drain the die (so prior phases don't leak in) + timestamp. */ +static void sprof_begin(fc_cuda_ctx *ctx, double *t0) +{ + if (!sprof_active) + return; + if (ctx) + fc_sync_ctx(ctx); + *t0 = prof_now(); +} + +/* Close the span started at *t0 into `phase`; restart the clock. */ +static void sprof_lap(int phase, fc_cuda_ctx *ctx, double *t0) +{ + if (!sprof_active) + return; + if (ctx) + fc_sync_ctx(ctx); + double t = prof_now(); + sprof_step_ph[phase] += t - *t0; + *t0 = t; +} + +static void sprof_step_report(int nslots, double step_total) +{ + double sum = 0.0; + sprof_nsteps++; + sprof_cum_total += step_total; + fprintf(stderr, "SPROF step K=%d total=%.1fms |", nslots, + 1e3 * step_total); + for (int ph = 0; ph < SPROF_NCOUNT; ph++) { + sprof_cum_ph[ph] += sprof_step_ph[ph]; + sum += sprof_step_ph[ph]; + fprintf(stderr, " %s=%.1f(%.0f%%)", sprof_pname[ph], + 1e3 * sprof_step_ph[ph], + 100.0 * sprof_step_ph[ph] / (step_total > 0 ? step_total : 1)); + } + fprintf(stderr, " other=%.1f\n", 1e3 * (step_total - sum)); + if (sprof_nsteps % 10 == 0) { + double csum = 0.0; + fprintf(stderr, "SPROF cum steps=%ld avg=%.1fms |", sprof_nsteps, + 1e3 * sprof_cum_total / sprof_nsteps); + for (int ph = 0; ph < SPROF_NCOUNT; ph++) { + csum += sprof_cum_ph[ph]; + fprintf(stderr, " %s=%.1f%%", sprof_pname[ph], + 100.0 * sprof_cum_ph[ph] / + (sprof_cum_total > 0 ? sprof_cum_total : 1)); + } + fprintf(stderr, " other=%.1f%%\n", + 100.0 * (sprof_cum_total - csum) / + (sprof_cum_total > 0 ? sprof_cum_total : 1)); + sub_decode_report(); + } +} + +/* ============================================================ + * Slots-path structural diagnostics — QWEN35_SDIAG=1 (M2b, Phase 0). + * + * ONE-SHOT facts about the batched decode step that a timing profile cannot + * show: does the on-die flash slots path engage (and if not, which guard + * rejected it), what did the per-slot KV ring allocation ask for and did it + * succeed, which layers are `grouped` vs on the per-row MoE fallback, and + * whether each SSM layer's slots graph actually captured. Each site latches + * after its first report, so this is a handful of stderr lines per process, + * not a per-step cost. Unlike sprof it inserts NO syncs and does NOT disable + * graph capture or the die pipeline — so the facts it reports are the facts + * of the REAL (unprofiled) path. Default off => production untouched. + * ============================================================ */ +static int sdiag_on(void) +{ + static int v = -1; + if (v < 0) + v = getenv("QWEN35_SDIAG") ? 1 : 0; + return v; +} + +static int qwen35moe_dbg_enabled(void) +{ + static int v = -1; + if (v < 0) + v = getenv("QWEN35_DEBUG") ? 1 : 0; + return v; +} + +static void qwen35moe_dbg(const char *tag, int layer, const float *x, int n) +{ + if (!qwen35moe_dbg_enabled()) + return; + double sum = 0.0; + for (int i = 0; i < n; i++) + sum += x[i]; + fprintf(stderr, "PN %-22s L%-2d [%12.6f %12.6f %12.6f ... %12.6f %12.6f " + "%12.6f] sum=%.6f\n", + tag, layer, x[0], x[1], x[2], x[n - 3], x[n - 2], x[n - 1], sum); +} + +/* Read an fc_buffer's first n floats back to host and dump with the CPU tag. + * ctx is the buffer's owning die context (so the sync hits the right stream). */ +static void qwen35moe_dbg_gpu(fc_cuda_ctx *ctx, const char *tag, int layer, + fc_buffer *buf, int n, float *scratch) +{ + if (!qwen35moe_dbg_enabled()) + return; + fc_sync_ctx(ctx); + fc_buffer_to_cpu(buf, scratch); + qwen35moe_dbg(tag, layer, scratch, n); +} + +/* ============================================================ + * Upload helpers + * ============================================================ */ + +/* Upload one quantized weight matrix to a persistent fc_buffer. + * Quant types with a repacked device layout (Q3_K/Q4_K/Q5_K/Q6_K/Q8_0 — see + * fc_repack_weight in foundry fc_cuda.h) are converted ONCE here into the + * 4-byte-aligned, scale-pre-unpacked form the matvec/MoE kernels read; + * g->row_stride becomes the repacked stride. Values are bit-identical, only + * the layout changes. Other types (F32 rows for the router/alpha/beta) upload + * raw. The CPU qweight keeps pointing at the mmap'd GGUF blocks for the host + * fallback. Returns 0 on success. */ +static int vocab_split_on(void); /* fwd decl (defined near the decode path) */ + +static int gpu_weight_upload(fc_cuda_ctx *ctx, gpu_weight *g, const pn_qweight *w) +{ + g->cpu = w; + g->type = w->type; + g->rows = w->rows; + g->cols = w->cols; + g->row_stride = (int)w->row_stride; + g->bytes = NULL; + g->ctx = ctx; + if (!w->data || w->rows <= 0) + return 0; /* absent/empty tensor — host fallback will handle it */ + size_t rstride = fc_repack_row_bytes((int)w->type, w->cols); + if (rstride) { + size_t rtotal = (size_t)w->rows * rstride; + unsigned char *tmp = malloc(rtotal); + if (!tmp) + return -1; + if (fc_repack_weight((int)w->type, w->data, tmp, w->rows, w->cols) != 0) { + free(tmp); + return -1; + } + g->bytes = fc_buffer_from_ptr_ctx(ctx, tmp, rtotal); + free(tmp); + g->row_stride = (int)rstride; + return g->bytes ? 0 : -1; + } + switch ((int)w->type) { + case GGUF_QUANT_Q3_K: + case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: + case GGUF_QUANT_Q6_K: + case GGUF_QUANT_Q8_0: + /* Repackable type with an odd cols the kernels couldn't handle + * anyway — leave on the host (gpu_matvec falls back to pn_qmv). */ + return 0; + default: + break; + } + size_t total = (size_t)w->rows * w->row_stride; + g->bytes = fc_buffer_from_ptr_ctx(ctx, w->data, total); + return g->bytes ? 0 : -1; +} + +/* T3 — upload a ROW SLICE [row_start, row_start+row_count) of a weight to ctx's + * die. Same repack/copy path as gpu_weight_upload but over a contiguous sub- + * range of the (row-major) source rows: row r of the source lives at + * w->data + r*w->row_stride, and repack is per-row, so a slice is just a + * shifted base pointer + a reduced row count. gw->rows becomes row_count and + * gw->cpu stays the FULL weight (host fallback re-derives the slice from the + * row offset when it runs — but the split path never uses the host fallback). + * Used to give each die half the LM-head vocab rows (memory-neutral). */ +static int gpu_weight_upload_rows(fc_cuda_ctx *ctx, gpu_weight *gw, + const pn_qweight *w, int row_start, + int row_count) +{ + if (!w || row_start < 0 || row_count <= 0 || + row_start + row_count > w->rows) + return -1; + gw->cpu = w; + gw->type = w->type; + gw->rows = row_count; + gw->cols = w->cols; + gw->row_stride = (int)w->row_stride; + gw->bytes = NULL; + gw->ctx = ctx; + if (!w->data) + return -1; + const unsigned char *base = + (const unsigned char *)w->data + (size_t)row_start * w->row_stride; + size_t rstride = fc_repack_row_bytes((int)w->type, w->cols); + if (rstride) { + size_t rtotal = (size_t)row_count * rstride; + unsigned char *tmp = malloc(rtotal); + if (!tmp) + return -1; + if (fc_repack_weight((int)w->type, base, tmp, row_count, w->cols) != 0) { + free(tmp); + return -1; + } + gw->bytes = fc_buffer_from_ptr_ctx(ctx, tmp, rtotal); + free(tmp); + gw->row_stride = (int)rstride; + return gw->bytes ? 0 : -1; + } + /* Non-repacked types: only F32 (and raw quants with an odd cols) reach + * here. Odd-cols quants have no GPU kernel — the split path requires a + * GPU-resident head, so refuse rather than silently host-fall-back. */ + switch ((int)w->type) { + case GGUF_QUANT_Q3_K: + case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: + case GGUF_QUANT_Q6_K: + case GGUF_QUANT_Q8_0: + return -1; + default: + break; + } + size_t total = (size_t)row_count * w->row_stride; + gw->bytes = fc_buffer_from_ptr_ctx(ctx, base, total); + return gw->bytes ? 0 : -1; +} + +/* Upload one F32 vector to a persistent fc_buffer on ctx's die. NULL src -> + * empty (allowed for optional norms). Returns 0 on success. */ +static int gpu_vec_upload(fc_cuda_ctx *ctx, gpu_vec *g, const float *src, int n) +{ + g->n = n; + g->buf = NULL; + g->ctx = ctx; + if (!src || n <= 0) + return 0; + g->buf = fc_buffer_from_ptr_ctx(ctx, src, (size_t)n * sizeof(float)); + return g->buf ? 0 : -1; +} + +static void gpu_weight_free(gpu_weight *g) +{ + if (g && g->bytes) + fc_buffer_free(g->bytes); +} + +static void gpu_vec_free(gpu_vec *g) +{ + if (g && g->buf) + fc_buffer_free(g->buf); +} + +/* Upload one layer's shared-expert MoE weights. When dense (Bonsai), only the + * three FFN projections (loaded into the shexp slots) exist — no router, no + * routed experts, no scalar gate — so skip all of those and upload just the + * SwiGLU triplet. */ +static int gpu_moe_upload(fc_cuda_ctx *ctx, gpu_moe *g, + const pn_qwen35moe_moe *moe, int n_expert, int n_embd, + int dense) +{ + g->n_expert = n_expert; + if (dense) { + g->grouped = 0; + g->gate_exps = g->up_exps = g->down_exps = NULL; + if (gpu_weight_upload(ctx, &g->ffn_gate_shexp, &moe->ffn_gate_shexp) < 0) + return -1; + if (gpu_weight_upload(ctx, &g->ffn_up_shexp, &moe->ffn_up_shexp) < 0) + return -1; + if (gpu_weight_upload(ctx, &g->ffn_down_shexp, &moe->ffn_down_shexp) < 0) + return -1; + g->gate_inp_shexp_host = NULL; + return 0; + } + if (gpu_weight_upload(ctx, &g->ffn_gate_inp, &moe->ffn_gate_inp) < 0) + return -1; + + /* Grouped fast path: gate/up Q3_K|Q4_K + down Q3_K|Q4_K, and every expert a + * contiguous slice of the merged 3D tensor (the GGUF loader slices them + * that way — verify anyway, the kernels index e * rows * row_stride). + * + * gate and up must be the SAME type: one kernel decodes both matrices in + * one warp body, so there is no per-matrix dispatch to give them. down is + * dispatched separately and may differ (Q3_K|Q4_K|Q6_K) — a Q4_K_M mixes + * Q4_K and Q6_K down across layers just as the Q3_K_M mini mixes Q3_K and + * Q4_K. + * + * The *_quant_available_ctx probes are the second half of the gate — the + * Q4_K gate+up and Q6_K down kernels are optional PTX symbols, and a + * foundry fc_cuda_ptx.h regenerated before they existed must drop the model + * to the per-expert fallback HERE rather than fail on every layer at run + * time. That fallback is what makes a big Q4_K_M unloadable: it replaces 3 + * merged buffers a layer with 3*n_expert (768) separate ones. */ + int gu_type = (int)moe->gate_exps[0].type; + int dn_type = (int)moe->down_exps[0].type; + g->grouped = ((gu_type == GGUF_QUANT_Q3_K || gu_type == GGUF_QUANT_Q4_K || + gu_type == GGUF_QUANT_F16 || gu_type == GGUF_QUANT_BF16) && + (int)moe->up_exps[0].type == gu_type && + fc_moe_gateup_quant_available_ctx(ctx, gu_type) && + fc_moe_down_quant_available_ctx(ctx, dn_type)); + if (g->grouped) { + const pn_qweight *g0 = &moe->gate_exps[0]; + const pn_qweight *u0 = &moe->up_exps[0]; + const pn_qweight *d0 = &moe->down_exps[0]; + size_t g_stride = (size_t)g0->rows * g0->row_stride; + size_t d_stride = (size_t)d0->rows * d0->row_stride; + for (int e = 0; e < n_expert && g->grouped; e++) { + if (moe->gate_exps[e].data != (const uint8_t *)g0->data + e * g_stride || + moe->up_exps[e].data != (const uint8_t *)u0->data + e * g_stride || + moe->down_exps[e].data != (const uint8_t *)d0->data + e * d_stride || + moe->gate_exps[e].type != g0->type || + moe->up_exps[e].type != u0->type || + moe->down_exps[e].type != d0->type) + g->grouped = 0; + } + } + if (g->grouped) { + /* Upload each merged tensor as one buffer; describe it as a single + * tall matrix (rows = n_expert * per-expert rows) so the row_stride/ + * type plumbing stays uniform. */ + pn_qweight merged; + merged = *(&moe->gate_exps[0]); + merged.rows = n_expert * moe->gate_exps[0].rows; + if (gpu_weight_upload(ctx, &g->gate_all, &merged) < 0) + return -1; + merged = *(&moe->up_exps[0]); + merged.rows = n_expert * moe->up_exps[0].rows; + if (gpu_weight_upload(ctx, &g->up_all, &merged) < 0) + return -1; + merged = *(&moe->down_exps[0]); + merged.rows = n_expert * moe->down_exps[0].rows; + if (gpu_weight_upload(ctx, &g->down_all, &merged) < 0) + return -1; + } else { + g->gate_exps = calloc((size_t)n_expert, sizeof(gpu_weight)); + g->up_exps = calloc((size_t)n_expert, sizeof(gpu_weight)); + g->down_exps = calloc((size_t)n_expert, sizeof(gpu_weight)); + if (!g->gate_exps || !g->up_exps || !g->down_exps) + return -1; + for (int e = 0; e < n_expert; e++) { + if (gpu_weight_upload(ctx, &g->gate_exps[e], &moe->gate_exps[e]) < 0) + return -1; + if (gpu_weight_upload(ctx, &g->up_exps[e], &moe->up_exps[e]) < 0) + return -1; + if (gpu_weight_upload(ctx, &g->down_exps[e], &moe->down_exps[e]) < 0) + return -1; + } + } + if (gpu_weight_upload(ctx, &g->ffn_gate_shexp, &moe->ffn_gate_shexp) < 0) + return -1; + if (gpu_weight_upload(ctx, &g->ffn_up_shexp, &moe->ffn_up_shexp) < 0) + return -1; + if (gpu_weight_upload(ctx, &g->ffn_down_shexp, &moe->ffn_down_shexp) < 0) + return -1; + /* The shared-expert scalar gate is a 1-D [n_embd] F32 vector (see the CPU + * qwen35moe_shared note); upload it as a vec, not a matvec weight. */ + g->gate_inp_shexp_host = (const float *)moe->ffn_gate_inp_shexp.data; + return gpu_vec_upload(ctx, &g->ffn_gate_inp_shexp, + (const float *)moe->ffn_gate_inp_shexp.data, n_embd); +} + +static void gpu_moe_free(gpu_moe *g) +{ + gpu_weight_free(&g->ffn_gate_inp); + gpu_weight_free(&g->gate_all); + gpu_weight_free(&g->up_all); + gpu_weight_free(&g->down_all); + if (g->gate_exps) { + for (int e = 0; e < g->n_expert; e++) { + gpu_weight_free(&g->gate_exps[e]); + gpu_weight_free(&g->up_exps[e]); + gpu_weight_free(&g->down_exps[e]); + } + } + free(g->gate_exps); + free(g->up_exps); + free(g->down_exps); + gpu_weight_free(&g->ffn_gate_shexp); + gpu_weight_free(&g->ffn_up_shexp); + gpu_weight_free(&g->ffn_down_shexp); + gpu_vec_free(&g->ffn_gate_inp_shexp); +} + +/* Upload one SSM layer's weights into slot d, tagged to ctx (its die). */ +static int gpu_upload_ssm_layer(fc_cuda_ctx *ctx, gpu_ssm_layer *d, + const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg) +{ + int n_embd = cfg->n_embd; + int conv_dim = cfg->conv_dim; + int n_v_heads = cfg->ssm_dt_rank; + d->ctx = ctx; + if (gpu_vec_upload(ctx, &d->attn_norm, l->attn_norm, n_embd) < 0 || + gpu_weight_upload(ctx, &d->attn_qkv, &l->attn_qkv) < 0 || + gpu_weight_upload(ctx, &d->attn_gate, &l->attn_gate) < 0 || + gpu_vec_upload(ctx, &d->ssm_conv1d, l->ssm_conv1d, conv_dim * cfg->ssm_conv) < 0 || + gpu_vec_upload(ctx, &d->ssm_a, l->ssm_a, n_v_heads) < 0 || + gpu_weight_upload(ctx, &d->ssm_alpha, &l->ssm_alpha) < 0 || + gpu_weight_upload(ctx, &d->ssm_beta, &l->ssm_beta) < 0 || + gpu_vec_upload(ctx, &d->ssm_dt, l->ssm_dt, n_v_heads) < 0 || + gpu_vec_upload(ctx, &d->ssm_norm, l->ssm_norm, cfg->ssm_state) < 0 || + gpu_weight_upload(ctx, &d->ssm_out, &l->ssm_out) < 0 || + gpu_vec_upload(ctx, &d->post_attention_norm, l->post_attention_norm, n_embd) < 0 || + gpu_moe_upload(ctx, &d->moe, &l->moe, cfg->n_expert, n_embd, cfg->dense_ffn) < 0) + return -1; + return 0; +} + +/* Upload one attention layer's weights into slot d, tagged to ctx (its die). */ +static int gpu_upload_attn_layer(fc_cuda_ctx *ctx, gpu_attn_layer *d, + const pn_qwen35moe_attn_layer *l, + const pn_qwen35moe_config *cfg) +{ + int n_embd = cfg->n_embd; + d->ctx = ctx; + if (gpu_vec_upload(ctx, &d->attn_norm, l->attn_norm, n_embd) < 0 || + gpu_weight_upload(ctx, &d->attn_q, &l->attn_q) < 0 || + gpu_weight_upload(ctx, &d->attn_k, &l->attn_k) < 0 || + gpu_weight_upload(ctx, &d->attn_v, &l->attn_v) < 0 || + gpu_vec_upload(ctx, &d->attn_q_norm, l->attn_q_norm, cfg->head_dim) < 0 || + gpu_vec_upload(ctx, &d->attn_k_norm, l->attn_k_norm, cfg->head_dim) < 0 || + gpu_weight_upload(ctx, &d->attn_output, &l->attn_output) < 0 || + gpu_vec_upload(ctx, &d->post_attention_norm, l->post_attention_norm, n_embd) < 0 || + gpu_moe_upload(ctx, &d->moe, &l->moe, cfg->n_expert, n_embd, cfg->dense_ffn) < 0) + return -1; + return 0; +} + +/* Phase A.1 (eight_card_serving_plan): once weights are resident in VRAM, the + * host mmap of the quantized weight bytes is dead for the GPU path — but on the + * 8 GB host that ~15 GB of page cache is exactly what drains MemAvailable and + * trips admission. Advise the kernel to drop it. Safe: the mapping is + * MAP_PRIVATE + PROT_READ, so a rare host-fallback matvec (missing kernel) + * re-faults the clean pages straight from the file — correct, just slow. The + * F32 token_embd and per-layer norms are separate heap copies (NOT in this + * mmap), so they are untouched. Default-ON (the GPU-resident design) since the + * host-RAM win + decode-neutrality were validated on a K80 host 2026-07-24 (ornith's + * ~10 GB GGUF Rss collapsed to ~162 MiB, decode bit-correct); opt OUT with + * QWEN35_DROP_WEIGHTS=0 for the rare host-fallback debug case. */ +static void gpu_drop_weight_pages(pn_qwen35moe_model *model) +{ + const char *e = getenv("QWEN35_DROP_WEIGHTS"); + if (e && e[0] == '0') + return; + pn_gguf_file *gf = pn_qwen35moe_gguf(model); + if (!gf || !gf->data || gf->data_size == 0) + return; + long pg = sysconf(_SC_PAGESIZE); + if (pg <= 0) + return; + uintptr_t ds = (uintptr_t)gf->data; + uintptr_t start = (ds + (uintptr_t)pg - 1) & ~((uintptr_t)pg - 1); /* page-align up */ + uintptr_t end = ds + gf->data_size; + if (end <= start) + return; + size_t len = (size_t)(end - start) & ~((size_t)pg - 1); /* whole pages only */ + if (len == 0) + return; + if (madvise((void *)start, len, MADV_DONTNEED) == 0) + fprintf(stderr, "qwen35moe: dropped %llu MiB host weight page cache " + "(MADV_DONTNEED; weights resident in VRAM)\n", + (unsigned long long)(len >> 20)); + else + fprintf(stderr, "qwen35moe: MADV_DONTNEED on weight pages failed " + "(errno %d); host cache not dropped\n", errno); +} + +/* Upload the weights for global layers [layer_lo, layer_hi) onto `ctx`. Each + * global layer maps to an SSM or attention local slot via the model's index + * map; the slot is tagged with the context it now lives on, which is what lets + * every later path resolve a layer's die from the layer itself. */ +static int gpu_upload_layer_range(pn_qwen35moe_model *model, + pn_qwen35moe_gpu *g, fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + int layer_lo, int layer_hi) +{ + for (int layer = layer_lo; layer < layer_hi; layer++) { + int idx = pn_qwen35moe_model_layer_index(model, layer); + if (pn_qwen35moe_model_layer_type(model, layer) == + PN_QWEN35MOE_LAYER_ATTN) { + const pn_qwen35moe_attn_layer *l = + pn_qwen35moe_model_attn_layer(model, idx); + if (gpu_upload_attn_layer(ctx, &g->attn[idx], l, cfg) < 0) + return -1; + } else { + const pn_qwen35moe_ssm_layer *l = + pn_qwen35moe_model_ssm_layer(model, idx); + if (gpu_upload_ssm_layer(ctx, &g->ssm[idx], l, cfg) < 0) + return -1; + } + } + return 0; +} + +/* Whether the T3 vocab-split head can run on this placement: it pairs the head + * die with die 0 over a peer link, which is only the adjacent pair when there + * are EXACTLY two dies. At three or more, die 0 is several hops upstream of + * the head and its x_norm mirror would be a copy across the whole pipeline for + * no locality — so the path stays off rather than quietly costing a hop. */ +static int vocab_split_eligible(const pn_qwen35moe_gpu *g, int small_bar) +{ + return vocab_split_on() && small_bar && g->n_die == 2; +} + +/* Admission for ONE die of the split (§6.1). `ordinal` is what the die is + * called in the memwatch/report space (its CUDA ordinal in small-BAR mode, its + * index under barswap). `head_share_bytes` is any non-layer weight this die + * holds beyond the head convention — used to charge die 0 for the vocab-split + * half it may take. */ +static int gpu_admit_one_die(pn_qwen35moe_model *model, pn_qwen35moe_gpu *g, + int i, int ordinal, uint64_t head_share_bytes) +{ + uint64_t extra = head_share_bytes; + if (i == g->n_die - 1) + extra += gpu_head_weight_bytes(model); + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(model); + return gpu_admit_die_share(model, g->ctx[i], ordinal, + g->layer_lo[i], g->layer_hi[i], extra, + cfg->n_layer, gpu_admit_max_ctx_model(model), + pn_qwen35moe_kv_q4_on()); +} + +/* Bind a CUDA context to every die of the split and gate each die on its own + * share, before any weight moves. In small-BAR mode all dies are live at once, + * so the whole placement is proved to fit up front — a doomed split is refused + * in milliseconds rather than after a multi-minute upload. Legacy barswap can + * only see the die currently in its MMIO slot, so only die 0 is bound here and + * the rest are bound + gated inside the upload loop. + * + * Die 0's context always binds here, split or not. */ +static int gpu_bind_and_gate_dies(pn_qwen35moe_model *model, + pn_qwen35moe_gpu *g, int small_bar) +{ + /* With the vocab split ON, die 0 also holds the head's top half. Charge it + * for that AND still charge the head die the whole head: the split is + * best-effort and falls back to the head die holding all of it, so both + * dies stay upper-bounded either way. */ + uint64_t head_a = vocab_split_eligible(g, small_bar) + ? gpu_head_weight_bytes(model) / 2 : 0; + + int last = small_bar ? g->n_die : 1; + for (int i = 0; i < last; i++) { + int ordinal = small_bar ? g->die[i] : 0; + g->ctx[i] = fc_cuda_init_device(ordinal); + if (!g->ctx[i] || !fc_cuda_ctx_available(g->ctx[i])) { + fprintf(stderr, "qwen35moe: fc_cuda_init_device for die %d " + "(ordinal %d) failed\n", i, ordinal); + return -1; + } + if (gpu_admit_one_die(model, g, i, ordinal, i == 0 ? head_a : 0) < 0) + return -1; + } + return 0; +} + +/* Does the peer link between two contexts actually MOVE BYTES? + * + * Asking the driver is not enough, and this is not a theoretical worry. On the + * K80 host, for two dies on DIFFERENT cards, cuDeviceCanAccessPeer answers yes, + * cuCtxEnablePeerAccess succeeds, and cuMemcpyPeer returns CUDA_SUCCESS — + * while copying nothing at all. The destination keeps whatever it held. Every + * layer of the API reports success; only the data disagrees. + * + * Fed into a layer split that is silent, not loud: the receiving die runs its + * layers on the previous step's residual stream and the model emits + * fluent-shaped garbage. Measured 2026-08-21 on dies 4/6 and 5/6 (cross-card: + * every one of 2048 floats unmoved) against dies 4/5 and 6/7 (same card, behind + * the on-card PLX switch: all 2048 correct). + * + * So the link is PROVEN before it is trusted: poison the destination, copy a + * known pattern across, read it back and compare. Once per pair at load, on 64 + * floats. Returns 1 if the bytes arrived, 0 otherwise — and 0 means the caller + * stages that hop through the host, which always works. + * + * Thread-safe: no (touches both contexts; call at load, single-threaded). */ +static int gpu_peer_link_moves_bytes(fc_cuda_ctx *from, fc_cuda_ctx *to) +{ + enum { PROBE_N = 64 }; + float probe[PROBE_N], back[PROBE_N]; + for (int i = 0; i < PROBE_N; i++) { + probe[i] = (float)(i + 1) * 1.5f; + back[i] = -1.0f; /* poison: a no-op copy leaves this */ + } + size_t bytes = sizeof probe; + fc_buffer *src = fc_buffer_alloc_ctx(from, bytes); + fc_buffer *dst = fc_buffer_alloc_ctx(to, bytes); + int ok = 0; + if (src && dst) { + fc_buffer_from_cpu_n(dst, back, bytes); + fc_sync_ctx(to); + fc_buffer_from_cpu_n(src, probe, bytes); + fc_sync_ctx(from); + if (fc_buffer_copy_peer(dst, src, bytes) == 0) { + fc_sync_ctx(to); + fc_buffer_to_cpu_n(dst, back, bytes); + fc_sync_ctx(to); + ok = memcmp(back, probe, bytes) == 0; + } + } + fc_buffer_free(src); + fc_buffer_free(dst); + return ok; +} + +/* T3 — vocab-split the LM head across the two dies (QWEN35_VOCAB_SPLIT). + * `g->output` (the FULL head, already resident on the head die) is left + * untouched so the single-token, prefill and pipeline head paths keep working; + * the split uses SEPARATE half buffers only in the decode-slots head: die 0 + * gets output_a (top half [0,half)) + a norm copy, the head die gets output_b + * (bottom half [half,nv)) as a zero-copy offset VIEW into the resident full + * head, so it costs no new memory there. Best-effort — any failure leaves + * vocab_split_ready = 0 and the decode-slots head runs the full head, so the + * serve is never regressed. Returns 0 always: this cannot fail a load. */ +static int gpu_setup_vocab_split(pn_qwen35moe_model *model, + pn_qwen35moe_gpu *g, int small_bar, + int n_embd) +{ + g->vocab_split_ready = 0; + if (!vocab_split_eligible(g, small_bar)) + return 0; + + const pn_qweight *ow = pn_qwen35moe_model_output(model); + int nv = ow ? ow->rows : 0; + int half = nv / 2; /* die 0 = [0,half), head die = [half,nv) */ + /* Same proof the boundary hand-off takes: this path P2Ps x_norm from the + * head die back to die 0 every step, and a peer link that reports success + * while moving nothing would feed die 0's half of the LM head a stale + * x_norm — wrong top-k for half the vocab, silently. */ + if (nv < 2 || half < 1 || !g->output.bytes || + fc_cuda_enable_peer(g->ctx[0], g->head_ctx) != 0 || + !gpu_peer_link_moves_bytes(g->head_ctx, g->ctx[0])) { + if (nv >= 2 && half >= 1 && g->output.bytes) + fprintf(stderr, "qwen35moe: vocab-split OFF — no verified peer " + "link from die %d to die %d\n", g->head_die, g->die[0]); + return 0; + } + + g->output_b = g->output; /* copy type/cols/rows/stride/ctx */ + g->output_b.bytes = fc_buffer_offset_view(g->output.bytes, + (size_t)half * g->output.row_stride); + g->output_b.rows = nv - half; + int ok = g->output_b.bytes != NULL && + (gpu_weight_upload_rows(g->ctx[0], &g->output_a, ow, 0, half) == 0) && + (gpu_vec_upload(g->ctx[0], &g->output_norm_a, + pn_qwen35moe_model_output_norm(model), + n_embd) == 0); + if (!ok) { + fc_buffer_free_view(g->output_b.bytes); g->output_b.bytes = NULL; + fc_buffer_free(g->output_a.bytes); g->output_a.bytes = NULL; + fc_buffer_free(g->output_norm_a.buf); g->output_norm_a.buf = NULL; + fprintf(stderr, "qwen35moe: vocab-split setup failed — decode-slots " + "head stays on the full head-die head\n"); + return 0; + } + g->vocab_half = half; + g->vocab_split_ready = 1; + fc_sync_ctx(g->ctx[0]); + fprintf(stderr, "qwen35moe: LM head vocab-split ON [die %d rows [0,%d) " + "copy, die %d rows [%d,%d) view] (T3)\n", + g->die[0], half, g->head_die, half, nv); + return 0; +} + +/* Enable — and then PROVE — peer access for every ADJACENT die pair, so the + * per-step residual hand-off can go device-to-device instead of D2H + H2D + * through the host. Only adjacent pairs matter: the residual stream walks the + * dies in order, so die i only ever hands to die i+1. + * + * Per pair, and proven per pair (gpu_peer_link_moves_bytes): on a K80 host the + * pairs that really carry bytes are the two dies of ONE card, behind its PLX + * switch. A cross-card pair claims the capability and drops the data, so it + * stages through the host — and the pairs that do work still do not. + * QWEN35_HANDOFF_HOST=1 forces every hop through the host, which is how the two + * routes are measured against each other. */ +static void gpu_enable_handoff_peers(pn_qwen35moe_gpu *g) +{ + const char *force_host = getenv("QWEN35_HANDOFF_HOST"); + if (force_host && force_host[0] && force_host[0] != '0') { + fprintf(stderr, "qwen35moe: QWEN35_HANDOFF_HOST — every boundary " + "hand-off staged through the host\n"); + return; + } + if (g->bs) + return; /* barswap time-multiplexes one slot; no two dies are live */ + + int n_peer = 0; + for (int i = 0; i + 1 < g->n_die; i++) { + g->peer_ok[i] = (fc_cuda_enable_peer(g->ctx[i], g->ctx[i + 1]) == 0) && + gpu_peer_link_moves_bytes(g->ctx[i], g->ctx[i + 1]); + if (!g->peer_ok[i]) + fprintf(stderr, "qwen35moe: die %d -> %d hand-off staged through " + "the host (no verified peer link)\n", + g->die[i], g->die[i + 1]); + n_peer += g->peer_ok[i]; + } + if (g->n_die > 1) + fprintf(stderr, "qwen35moe: boundary hand-off — %d of %d hops " + "device-to-device (verified), the rest staged through the " + "host\n", n_peer, g->n_die - 1); +} + +int pn_qwen35moe_gpu_upload(pn_qwen35moe_model *model) +{ + if (!model) + return -1; + if (!fc_available()) { + /* No GPU at runtime (the Mac case): graceful skip — the CPU forward + * stays selected. Not an error. */ + return 0; + } + if (pn_qwen35moe_model_gpu(model)) + return 0; /* already uploaded */ + + /* MemoryAware load-spike budget (§6.1 refinement, measured on a K80 host + * 2026-07-09): the GPU upload transiently costs ~2.4 GB of host anon RSS + * for ~55 s (dequant/staging buffers) before releasing to ~1.1 GB. On the + * 8 GB box THIS transient — not per-request prefill, which is ~0 on the + * GPU path — is the real squeeze. Check it ONCE per load attempt, before + * any die context spins up, so a squeezed host refuses cleanly instead of + * tipping over mid-upload. Fail-open when host truth is unavailable. */ + { + pn_mem_state mst; + pn_memwatch_poll_now(); + pn_memwatch_read(&mst); + uint64_t spike = 0; + if (mst.host_total_bytes > 0 && + !pn_admit_load_spike(mst.host_avail_bytes, mst.host_total_bytes, + &spike)) { + fprintf(stderr, "[admit] REFUSE model load: host has %llu MiB " + "available but the GPU-upload transient needs ~%llu MiB " + "on top of the %llu MiB host reserve " + "(PN_LOAD_SPIKE_BYTES) — refusing before the upload\n", + (unsigned long long)(mst.host_avail_bytes >> 20), + (unsigned long long)(spike >> 20), + (unsigned long long) + (pn_host_reserve_bytes(mst.host_total_bytes) >> 20)); + return -1; + } + } + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(model); + int n_ssm = pn_qwen35moe_model_n_ssm(model); + int n_attn = pn_qwen35moe_model_n_attn(model); + int n_embd = cfg->n_embd; + int n_layer = cfg->n_layer; + + pn_qwen35moe_gpu *g = calloc(1, sizeof(*g)); + if (!g) + return -1; + /* Registered before anything can bind a slot on it, and unregistered in + * pn_qwen35moe_gpu_free (which the `fail:` path below also runs), so the + * session-free hook always sees exactly the live contexts. */ + qw_gpu_register(g); + g->n_ssm = n_ssm; + g->n_attn = n_attn; + g->active_die = -1; + g->ssm = n_ssm ? calloc((size_t)n_ssm, sizeof(gpu_ssm_layer)) : NULL; + g->attn = n_attn ? calloc((size_t)n_attn, sizeof(gpu_attn_layer)) : NULL; + if ((n_ssm && !g->ssm) || (n_attn && !g->attn)) + goto fail; + + /* Plan the split first: it decides how many contexts to bind, what each + * one uploads, and what admission prices. A width this build cannot run + * refuses here, before a die is touched. */ + if (gpu_plan_dies(model, n_layer, g) < 0) + goto fail; + int n_die = g->n_die; + + /* Single-die prefix cap (single-die mode only): the full model (~16 GB) + * exceeds one 12 GB die, so QWEN35_GPU_MAX_LAYERS uploads + validates only + * the first N global layers on one die. Ignored once a real split is + * configured (the split itself is what makes the full model fit). */ + int max_layers = n_layer; + if (n_die == 1) { + const char *cap_env = getenv("QWEN35_GPU_MAX_LAYERS"); + if (cap_env) { int v = atoi(cap_env); if (v > 0 && v < max_layers) max_layers = v; } + g->layer_hi[0] = max_layers; + } + + /* Mode: small-BAR (default) maps all dies at once, so each context binds + * directly to its die's real CUDA ordinal and no barswap multiplexing is + * needed. Opt into the legacy barswap single-slot regime (>16 GB BARs) with + * QWEN35_GPU_BARSWAP=1. In barswap mode every context inits device 0 (the + * one MMIO slot) and the forward bs_acquire()s each die before touching it. */ + int small_bar = (getenv("QWEN35_GPU_BARSWAP") == NULL); + if (!small_bar && n_die > 2) { + /* Barswap can only bind a context to a die that is currently in its + * single MMIO slot, so the contexts have to be created one at a time, + * interleaved with the uploads — which means a doomed wide split is + * only discovered several minutes in. Refuse instead of building a + * second, untested binding order for a regime the fleet no longer runs + * (the BARs are resized; small-BAR is the default). */ + fprintf(stderr, "qwen35moe: REFUSE — QWEN35_GPU_BARSWAP supports at " + "most a 2-die split, not %d\n", n_die); + goto fail; + } + + if (n_die >= 2 && !small_bar) { + g->bs = bs_init(); + if (!g->bs) { + fprintf(stderr, "qwen35moe: bs_init failed (a layer split needs " + "bar_swap.ko)\n"); + goto fail; + } + if (gpu_select_die(g, g->die[0]) < 0) + goto fail; + gpu_load_nvidia(); /* harmless if already up */ + } + + /* Bind every die's context and gate every die's share BEFORE uploading a + * single weight, so a split that cannot fit is refused instantly instead + * of after a multi-minute upload. (Legacy barswap can only see one die at + * a time, so its second context + gate happen after die 0's upload.) */ + if (gpu_bind_and_gate_dies(model, g, small_bar) < 0) + goto fail; + + for (int i = 0; i < n_die; i++) { + /* Legacy barswap: bind + gate die i now that it can be swapped in. */ + if (i > 0 && !small_bar) { + if (gpu_select_die(g, g->die[i]) < 0) + goto fail; + gpu_load_nvidia(); + g->ctx[i] = fc_cuda_init_device(0); + if (!g->ctx[i] || !fc_cuda_ctx_available(g->ctx[i])) { + fprintf(stderr, "qwen35moe: fc_cuda_init_device for die %d " + "(ordinal 0) failed\n", i); + goto fail; + } + if (gpu_admit_one_die(model, g, i, i /* barswap slot ordinal */, + 0) < 0) + goto fail; + } + if (gpu_upload_layer_range(model, g, g->ctx[i], cfg, + g->layer_lo[i], g->layer_hi[i]) < 0) + goto fail; + /* The final norm + LM head ride on the LAST die (which is also die 0 + * when the model is not split). */ + if (i == n_die - 1) { + if (gpu_vec_upload(g->ctx[i], &g->output_norm, + pn_qwen35moe_model_output_norm(model), + n_embd) < 0 || + gpu_weight_upload(g->ctx[i], &g->output, + pn_qwen35moe_model_output(model)) < 0) + goto fail; + } + fc_sync_ctx(g->ctx[i]); + } + g->head_ctx = (n_die > 1) ? g->ctx[n_die - 1] : NULL; + + if (gpu_setup_vocab_split(model, g, small_bar, n_embd) < 0) + goto fail; + gpu_enable_handoff_peers(g); + + for (int i = 0; i < n_die; i++) + gpu_feed_die_mem_state(g->ctx[i], small_bar ? g->die[i] : i); + + pn_qwen35moe_model_set_gpu(model, g); + if (n_die == 1) { + fprintf(stderr, + "qwen35moe: GPU weights uploaded (%d SSM + %d attn, first %d " + "global) [single die %d]\n", + n_ssm, n_attn, max_layers, g->die[0]); + } else { + fprintf(stderr, "qwen35moe: GPU weights uploaded [%d-die layer split:", + n_die); + for (int i = 0; i < n_die; i++) + fprintf(stderr, " die %d = [%d,%d)%s", g->die[i], g->layer_lo[i], + g->layer_hi[i], i == n_die - 1 ? " + head" : ""); + fprintf(stderr, "]\n"); + } + gpu_drop_weight_pages(model); + return 0; + +fail: + /* Reuse the public free on a temporarily-attached handle so every uploaded + * buffer is released. */ + pn_qwen35moe_model_set_gpu(model, g); + pn_qwen35moe_gpu_free(model); + return -1; +} + +/* Fold one residency class into the `all` total. */ +static void gpu_graph_residency_merge(pn_graph_residency *all, + const pn_graph_residency *part) +{ + all->total += part->total; + all->live += part->live; + all->failed += part->failed; + all->gave_up += part->gave_up; + all->ineligible += part->ineligible; + all->pending += part->pending; + all->lifetime_captures += part->lifetime_captures; + all->lifetime_failures += part->lifetime_failures; +} + +int pn_qwen35moe_gpu_graph_residency(const pn_qwen35moe_model *model, + pn_model_graph_residency *out) +{ + if (!out) + return -1; + memset(out, 0, sizeof(*out)); + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(model); + if (!g) + return -1; + + for (int i = 0; i < g->n_attn; i++) + pn_graph_residency_add(&out->batched_attn, + &g->attn[i].slots_graph_capture); + for (int i = 0; i < g->n_ssm; i++) { + pn_graph_residency_add(&out->batched_ssm, &g->ssm[i].slots_graph_capture); + pn_graph_residency_add(&out->single_ssm, &g->ssm[i].graph_capture); + } + for (int i = 0; i < (int)(sizeof(g->seg) / sizeof(g->seg[0])); i++) + pn_graph_residency_add(&out->segment, &g->seg[i].capture); + + gpu_graph_residency_merge(&out->all, &out->batched_attn); + gpu_graph_residency_merge(&out->all, &out->batched_ssm); + gpu_graph_residency_merge(&out->all, &out->single_ssm); + gpu_graph_residency_merge(&out->all, &out->segment); + return 0; +} + +int pn_qwen35moe_gpu_attn_residency(const pn_qwen35moe_model *model, + pn_model_attn_residency *out) +{ + if (!out) + return -1; + memset(out, 0, sizeof(*out)); + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(model); + if (!g) + return -1; + + for (int i = 0; i < g->n_attn; i++) + pn_attn_residency_add(&out->batched_attn, &g->attn[i].path); + /* Only one class today, so `all` is a copy rather than a merge — the + * single-token attention path is not instrumented, because the serve + * routes every request through the batch scheduler. */ + out->all = out->batched_attn; + return 0; +} + +void pn_qwen35moe_gpu_free(pn_qwen35moe_model *model) +{ + if (!model) + return; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(model); + if (!g) + return; + qw_gpu_unregister(g); + for (int i = 0; i < g->n_ssm; i++) { + gpu_ssm_layer *d = &g->ssm[i]; + gpu_vec_free(&d->attn_norm); + gpu_weight_free(&d->attn_qkv); + gpu_weight_free(&d->attn_gate); + gpu_vec_free(&d->ssm_conv1d); + gpu_vec_free(&d->ssm_a); + gpu_weight_free(&d->ssm_alpha); + gpu_weight_free(&d->ssm_beta); + gpu_vec_free(&d->ssm_dt); + gpu_vec_free(&d->ssm_norm); + gpu_weight_free(&d->ssm_out); + gpu_vec_free(&d->post_attention_norm); + gpu_moe_free(&d->moe); + fc_buffer_free(d->conv_state_g); + fc_buffer_free(d->ssm_state_g); + fc_buffer_free(d->conv_snaps); + fc_buffer_free(d->ssm_snaps); + fc_buffer_free(d->conv_slots); + fc_buffer_free(d->ssm_slots); + gpu_fused_free(&d->fx_proj); + gpu_fused_free(&d->fx_out); + gpu_fused_free(&d->moe.fx_pre); + gpu_fused_free(&d->moe.fx_post); + fc_graph_free(d->graph); + fc_graph_free(d->slots_graph); + } + for (int i = 0; i < g->n_attn; i++) { + gpu_attn_layer *d = &g->attn[i]; + gpu_vec_free(&d->attn_norm); + gpu_weight_free(&d->attn_q); + gpu_weight_free(&d->attn_k); + gpu_weight_free(&d->attn_v); + gpu_vec_free(&d->attn_q_norm); + gpu_vec_free(&d->attn_k_norm); + gpu_weight_free(&d->attn_output); + gpu_vec_free(&d->post_attention_norm); + gpu_moe_free(&d->moe); + fc_buffer_free(d->k_cache_g); + fc_buffer_free(d->v_cache_g); + fc_buffer_free(d->k_sink_g); + fc_buffer_free(d->v_sink_g); + fc_buffer_free(d->k_win_g); + fc_buffer_free(d->v_win_g); + fc_buffer_free(d->k_win_snap_g); + fc_buffer_free(d->v_win_snap_g); + fc_buffer_free(d->dec_m_g); + fc_buffer_free(d->dec_l_g); + fc_buffer_free(d->dec_acc_g); + fc_buffer_free(d->k_slots_g); + fc_buffer_free(d->v_slots_g); + fc_buffer_free(d->k_sink_slots_g); + fc_buffer_free(d->v_sink_slots_g); + fc_buffer_free(d->k_win_slots_g); + fc_buffer_free(d->v_win_slots_g); + fc_buffer_free(d->dec_slots_m_g); + fc_buffer_free(d->dec_slots_l_g); + fc_buffer_free(d->dec_slots_acc_g); + gpu_fused_free(&d->fx_qkv); + gpu_fused_free(&d->fx_out); + gpu_fused_free(&d->moe.fx_pre); + gpu_fused_free(&d->moe.fx_post); + fc_graph_free(d->slots_graph); + } + for (int i = 0; i < PN_DIE_LAYER_SPLIT_MAX_DIES; i++) { + fc_graph_free(g->seg[i].graph); + fc_event_free(g->as_ev[i]); + } + free(g->ssm); + free(g->attn); + gpu_vec_free(&g->output_norm); + gpu_weight_free(&g->output); + /* T3 vocab-split head buffers (no-ops when the split was never enabled). + * output_b.bytes is a non-owning offset VIEW into g->output — free only the + * view wrapper, never the aliased device memory (that is g->output's). */ + gpu_weight_free(&g->output_a); + fc_buffer_free_view(g->output_b.bytes); + g->output_b.bytes = NULL; + gpu_vec_free(&g->output_norm_a); + fc_buffer_free(g->xnorm_a); + fc_buffer_free(g->spec_logits_a); + fc_buffer_free(g->spec_logits_b); + for (int i = 0; i < PN_DIE_LAYER_SPLIT_MAX_DIES; i++) { + gpu_die_scratch_free(&g->scratch[i]); + gpu_batch_scratch_free(&g->bscratch[i]); + fc_buffer_free(g->pos_buf[i]); + } + fc_buffer_free(g->spec_logits); + for (int i = 0; i < 4; i++) { + free(g->bhost[i]); + free(g->bhost2[i]); + } + qw_tt_free(g); + qw_defer_free(g); + if (g->host_ok) + gpu_host_scratch_free(&g->host); + + /* Tear down the per-die contexts and the barswap handle. NOTE: on the K80 + * barswap rig the runtime path uses _Exit() and never reaches this (CUDA's + * context teardown wedges a thread); this free is for the upload-failure + * path and non-CUDA hosts. Each fc_buffer_free above self-enters its owning + * context, so a buffer's die must be the one currently in the slot for its + * cuMemFree to land on mapped BARs — in split mode the operator should + * prefer process-death reclamation over calling this mid-run. */ + for (int i = 0; i < g->n_die; i++) + fc_cuda_ctx_free(g->ctx[i]); + if (g->bs) + bs_free(g->bs); + free(g); + pn_qwen35moe_model_set_gpu(model, NULL); +} + +void pn_qwen35moe_gpu_forget_state_cuda(const pn_qwen35moe_state *s) +{ + if (!s) + return; + pthread_mutex_lock(&qw_gpu_reg_mtx); + for (pn_qwen35moe_gpu *g = qw_gpu_reg_head; g; + g = (pn_qwen35moe_gpu *)g->reg_next) { + /* Attention: the per-slot bind bookkeeping. kv_slot_kv is the pointer + * the lazy-mirror park copy dereferences; kv_slot_host_id / kv_gen / + * kv_synced_pos name the same session's ring buffers by ADDRESS, so + * leaving them behind would let a recycled malloc alias a new + * session's cache onto this one's stale watermark. Both go together. */ + for (int i = 0; i < g->n_attn; i++) { + gpu_attn_layer *d = &g->attn[i]; + for (int j = 0; j < s->n_attn; j++) { + const pn_qwen35moe_kv *kv = &s->kv[j]; + const void *id_k = (const void *)kv->k; + const void *id_q = (const void *)kv->kq; + for (int sl = 0; sl < PN_QWEN35MOE_GPU_MAX_SLOTS; sl++) { + const void *hid = d->kv_slot_host_id[sl]; + if (d->kv_slot_kv[sl] != kv && + !(hid && (hid == id_k || hid == id_q))) + continue; + d->kv_slot_kv[sl] = NULL; + d->kv_slot_host_id[sl] = NULL; + d->kv_slot_gen[sl] = 0; + d->kv_slot_synced[sl] = 0; + } + if (d->kv_host_id && + (d->kv_host_id == id_k || d->kv_host_id == id_q)) { + d->kv_host_id = NULL; + d->kv_synced_pos = 0; + } + } + } + /* SSM: host_state names the session whose recurrent image the live + * GPU buffers hold. Clearing it costs one H2D reseed on the next bind + * and removes the same recycled-address aliasing. */ + for (int i = 0; i < g->n_ssm; i++) { + gpu_ssm_layer *d = &g->ssm[i]; + for (int j = 0; j < s->n_ssm; j++) + if (d->host_state == (const void *)&s->ssm[j]) + d->host_state = NULL; + } + } + pthread_mutex_unlock(&qw_gpu_reg_mtx); +} + +int pn_qwen35moe_gpu_slot_kv_refs_cuda(const void *base, size_t bytes) +{ + if (!base || !bytes) + return 0; + uintptr_t lo = (uintptr_t)base, hi = lo + (uintptr_t)bytes; + int n = 0; + pthread_mutex_lock(&qw_gpu_reg_mtx); + for (pn_qwen35moe_gpu *g = qw_gpu_reg_head; g; + g = (pn_qwen35moe_gpu *)g->reg_next) + for (int i = 0; i < g->n_attn; i++) + for (int sl = 0; sl < PN_QWEN35MOE_GPU_MAX_SLOTS; sl++) { + uintptr_t p = (uintptr_t)g->attn[i].kv_slot_kv[sl]; + if (p >= lo && p < hi) + n++; + } + pthread_mutex_unlock(&qw_gpu_reg_mtx); + return n; +} + +/* ============================================================ + * GPU matvec dispatch + * + * fc_matvec_q3k consumes raw Q3_K bytes directly. For any other quant type + * (the GGUF mix can carry Q6_K output, etc.) there is no dedicated kernel yet, + * so we fall back to a host-side pn_qmv on a read-back of x and write the result + * back to the GPU out buffer. That fallback is correctness-faithful (same path + * the CPU reference uses) but costs a D2H/H2D round trip. + * + * Q3_K/Q4_K/Q5_K/Q6_K/Q8_0/Q2_0/F32/F16/BF16 all have a kernel now; the host + * fallback below only remains for types outside that set. + * ============================================================ */ + +/* y[rows] = dequant(W) . x[cols]. x and y are GPU buffers; x_host/y_host are + * caller-provided host scratch sized >= cols / rows for the fallback path. The + * matvec runs on the weight's owning die context (w->ctx), so the same call + * works for layers on die A or die B. x and y MUST belong to that same ctx. */ +static void gpu_matvec(const gpu_weight *w, fc_buffer *x, fc_buffer *y, + float *x_host, float *y_host) +{ + double t0 = 0.0; + int prof = prof_on(); + if (prof) { + fc_sync_ctx(w->ctx); + t0 = prof_now(); + } + /* On-device dispatch by quant type. Every type the Q3_K_M mini actually + * mixes in has a kernel now (matvec_q3k.cu + matvec_kquants.cu); the host + * fallback below only remains for exotic types outside that set. */ + int on_gpu = 1; + if (w->bytes) { + switch ((int)w->type) { + case GGUF_QUANT_Q3_K: + fc_matvec_q3k_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_Q4_K: + fc_matvec_q4k_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_Q5_K: + fc_matvec_q5k_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_Q6_K: + fc_matvec_q6k_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_Q8_0: + fc_matvec_q80_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_Q2_0_G128: + /* Bonsai native ternary: warp-per-row conditional add/sub, no + * dequant. Weights repacked to [f32 d][32 codes] at upload (B2). */ + fc_matvec_q2_0_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_F32: + fc_matvec_f32_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_F16: + /* The DENSE half of an F16 model. The launcher is silent when the + * kernel is absent (a pre-F16 fc_cuda_ptx.h), so probe first and + * let the host fallback below take it — a no-op launch would leave + * y holding the previous token's values. */ + if (!fc_matvec_f16_available_ctx(w->ctx)) { + on_gpu = 0; + break; + } + fc_matvec_f16_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + case GGUF_QUANT_BF16: + /* The same dense tensors unrounded — the checkpoints ship bf16, so + * this is the arm an F16 conversion has to be measured AGAINST. + * Same probe-first rule as F16 above. */ + if (!fc_matvec_bf16_available_ctx(w->ctx)) { + on_gpu = 0; + break; + } + fc_matvec_bf16_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride); + break; + default: + on_gpu = 0; + break; + } + } else { + on_gpu = 0; + } + if (on_gpu) { + if (prof) { + fc_sync_ctx(w->ctx); + double dt = prof_now() - t0; + int t = (int)w->type; + if (t >= 0 && t < PROF_MAX_TYPE) { + prof_mv_time[t] += dt; + prof_mv_cnt[t]++; + } + } + return; + } + /* Host fallback via the CPU qweight (exotic quant types only). */ + fc_sync_ctx(w->ctx); + fc_buffer_to_cpu(x, x_host); + pn_qmv(w->cpu, x_host, y_host); + fc_buffer_from_cpu(y, y_host); + if (prof) { + fc_sync_ctx(w->ctx); + double dt = prof_now() - t0; + int t = (int)w->type; + if (t >= 0 && t < PROF_MAX_TYPE) { + prof_mv_time[t] += dt; + prof_mv_cnt[t]++; + } + prof_mv_fallback += dt; + } +} + +/* ============================================================ + * Fused per-layer launches + * + * Decode is launch-bound on the K80 (~80 us/launch), so the per-layer + * launch storms are collapsed with fc_fused_multi (see fused_layer.cu): + * each fused launch batches several independent matvecs that read the same + * x, with the tiny glue ops (sigmoid, decay gate, silu*mul, scalar gate, + * combine/residual adds) folded into their epilogues. Every segment's math + * is bit-identical to the stand-alone kernels it replaces. Descriptors are + * built ONCE per layer (all referenced buffers are persistent) and cached + * in gpu_fused; a build failure (exotic quant mix) leaves desc NULL and the + * caller keeps the original unfused sequence. + * ============================================================ */ + +/* Fusion-site bitmask (QWEN35_FUSE, default all on) — bisection aid: + * bit0 ssm proj4, bit1 ssm out+res, bit2 attn qkv, bit3 attn norm+rope, + * bit4 moe pre, bit5 moe post. */ +static int gpu_fuse_mask(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_FUSE"); + v = env ? (int)strtol(env, NULL, 0) : 0x3F; + } + return v; +} + +/* CUDA-graph replay of the SSM layers (QWEN35_GRAPH, default on; set 0 to + * disable — bisection aid). */ +static int gpu_graph_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_GRAPH"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* Full-step segment graphs (C1): capture EVERY layer of a die segment — + * SSM and attention interleaved — into one graph and replay the whole + * per-die decode step with a single cuGraphLaunch (QWEN35_GRAPH_FULL, + * default on; set 0 for the previous behavior: per-layer SSM graphs + + * direct attention — the A/B baseline for the K80 hardware gate). Also requires + * QWEN35_GRAPH itself on, and self-disables when the device-pos attention + * kernels are missing from the PTX blob. */ +static int gpu_graph_full_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_GRAPH_FULL"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* L9 — per-layer graphs on the BATCHED-decode (slots) path (QWEN35_GRAPH_SLOTS, + * default on; set 0 for the ungraphed A/B baseline). A sub-gate of QWEN35_GRAPH, + * the same way QWEN35_GRAPH_FULL is: it only engages when the master graph gate + * is on. */ +static int gpu_graph_slots_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_GRAPH_SLOTS"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* L10 — the ATTENTION half of the slots capture (QWEN35_GRAPH_SLOTS_ATTN, + * default on; set 0 for the Phase-2 baseline: SSM layers captured, attention + * layers direct). A sub-gate of QWEN35_GRAPH_SLOTS, so that the one thing + * Phase 3 changed can be A/B'd on a single binary and a single deployed fleet + * — which matters here more than anywhere else, since QWEN35_SPROF disables + * capture outright and cannot be used to price it. */ +static int gpu_graph_slots_attn_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_GRAPH_SLOTS_ATTN"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* Expert-union dedup for the batched routed gate+up (QWEN35_MOE_DEDUP, + * default OFF). + * + * PREFILL ONLY, by construction — see gpu_moe_block_batch's allow_dedup + * argument. The dedup kernel's win is n*k / |union of routed experts|, which + * a 128-token prefill chunk makes ~4x and an 8-slot decode step makes ~1.1x; + * at decode that does not cover the wider grid and it measured 7-9% SLOWER. + * Enabling this flag must therefore never reach the decode path. + * + * Kept default-off until the prefill A/B is settled; when off, not one + * device buffer or launch on either path changes. */ +static int gpu_moe_dedup_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_MOE_DEDUP"); + v = (env && env[0] == '1') ? 1 : 0; + } + return v; +} + +/* QWEN35_MOE_DEDUP_DECODE=1 — MEASUREMENT ESCAPE HATCH, never a serving knob. + * + * The reason decode is excluded above is a regression figure that had no + * measurement behind it in this repo. This lets gpu_moe_block_slots opt in so + * the number can be reproduced on demand instead of taken on faith. It is a + * SECOND flag on top of QWEN35_MOE_DEDUP: with only QWEN35_MOE_DEDUP=1 set, + * decode still cannot reach the dedup kernel. */ +static int gpu_moe_dedup_decode_override(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_MOE_DEDUP_DECODE"); + v = (env && env[0] == '1') ? 1 : 0; + } + return v; +} + +/* Fill one plain matvec segment from a resident weight. */ +static fc_fused_seg gpu_seg_mv(const gpu_weight *w, fc_buffer *out, int ep) +{ + fc_fused_seg s; + memset(&s, 0, sizeof(s)); + s.w = w->bytes; + s.out = out; + s.rows = w->rows; + s.cols = w->cols; + s.row_stride = w->row_stride; + s.kind = ((int)w->type == GGUF_QUANT_F32) ? 1 : 0; + s.qtype = (int)w->type; + s.ep = ep; + return s; +} + +/* Build (once) and launch a cached fused descriptor. Returns 0 if the fused + * launch ran, -1 if unsupported (caller must run its unfused sequence). */ +static int gpu_fused_run(fc_cuda_ctx *ctx, gpu_fused *f, + const fc_fused_seg *segs, int nseg, fc_buffer *x) +{ + if (!f->tried) { + f->tried = 1; + f->nseg = nseg; + f->desc = fc_fused_build_ctx(ctx, segs, nseg, &f->blocks, &f->sh_bytes); + } + if (!f->desc) + return -1; + fc_fused_multi_ctx(ctx, f->desc, f->nseg, f->blocks, f->sh_bytes, x); + return 0; +} + +static void gpu_fused_free(gpu_fused *f) +{ + if (f && f->desc) { + fc_buffer_free(f->desc); + f->desc = NULL; + } +} + +/* ============================================================ + * Host-side scratch carried across the GPU forward + * + * The fc_* kernels handle the dense, regular ops (rms_norm, matvec_q3k, rope, + * qk_rms_norm, ssm_recurrence, conv1d, gated_norm, moe router/gate, silu/add/ + * mul). The irregular glue ops that lack a kernel are done on these host + * buffers between kernel calls (each marked TODO kernel inline). One allocation + * sized to the largest intermediate (val_dim / conv_dim / n_head*head_dim). + * The typedef lives up top (pn_qwen35moe_gpu owns one persistent copy). + * ============================================================ */ + +static int gpu_host_scratch_init(gpu_host_scratch *h, const pn_qwen35moe_config *cfg) +{ + int val_dim = cfg->ssm_state * cfg->ssm_dt_rank; /* 4096 */ + int qg = cfg->head_dim * cfg->n_head * 2; /* 8192 */ + int cap = cfg->conv_dim; + if (val_dim > cap) cap = val_dim; + if (qg > cap) cap = qg; + if (cfg->n_vocab > cap) cap = cfg->n_vocab; + h->cap = cap; + h->a = calloc((size_t)cap, sizeof(float)); + h->b = calloc((size_t)cap, sizeof(float)); + h->c = calloc((size_t)cap, sizeof(float)); + return (h->a && h->b && h->c) ? 0 : -1; +} + +static void gpu_host_scratch_free(gpu_host_scratch *h) +{ + free(h->a); + free(h->b); + free(h->c); +} + +/* Free every buffer in one die's persistent scratch. */ +static void gpu_die_scratch_free(gpu_die_scratch *sc) +{ + fc_buffer **bufs[] = { + &sc->s_qkv, &sc->s_z, &sc->s_beta, &sc->s_alpha, &sc->s_conv_out, + &sc->s_qg, &sc->s_kg, &sc->s_vg, &sc->s_gbuf, &sc->s_y, &sc->s_gated, + &sc->a_qg, &sc->a_q, &sc->a_z, &sc->a_k, &sc->a_v, &sc->a_out, + &sc->m_logits, &sc->m_tidx, &sc->m_twt, &sc->m_gate, &sc->m_up, + &sc->m_hid, &sc->m_exp, &sc->m_out, &sc->m_shexp, &sc->m_sgate, + &sc->r_hidden, &sc->r_xnorm, &sc->r_blockout, &sc->r_logits, + &sc->dec_pos, + }; + for (size_t i = 0; i < sizeof(bufs) / sizeof(bufs[0]); i++) { + fc_buffer_free(*bufs[i]); + *bufs[i] = NULL; + } + sc->ctx = NULL; +} + +/* Get (lazily creating) the persistent scratch for `ctx`'s die. Returns NULL + * for a context that is not one of the split's dies, or on allocation failure. + * + * Keyed by DIE INDEX rather than by scanning a two-entry free list: the list + * had exactly as many slots as the old split had dies, so a third context + * would have found none free and silently returned NULL mid-forward. */ +static gpu_die_scratch *gpu_get_scratch(pn_qwen35moe_gpu *g, fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg) +{ + int die_idx = gpu_die_index_of_ctx(g, ctx); + if (die_idx < 0) + return NULL; + gpu_die_scratch *sc = &g->scratch[die_idx]; + if (sc->ctx == ctx) + return sc; + + const int n_embd = cfg->n_embd; + const int conv_dim = cfg->conv_dim; + const int val_dim = cfg->ssm_state * cfg->ssm_dt_rank; + const int n_v_heads = cfg->ssm_dt_rank; + const int qg_dim = cfg->head_dim * cfg->n_head * 2; + const int kv_dim = cfg->head_dim * cfg->n_head_kv; + const int attn_dim = cfg->head_dim * cfg->n_head; + int ff = cfg->expert_ff > cfg->shared_ff ? cfg->expert_ff : cfg->shared_ff; + + sc->s_qkv = fc_buffer_alloc_ctx(ctx, (size_t)conv_dim * sizeof(float)); + sc->s_z = fc_buffer_alloc_ctx(ctx, (size_t)val_dim * sizeof(float)); + sc->s_beta = fc_buffer_alloc_ctx(ctx, (size_t)n_v_heads * sizeof(float)); + sc->s_alpha = fc_buffer_alloc_ctx(ctx, (size_t)n_v_heads * sizeof(float)); + sc->s_conv_out = fc_buffer_alloc_ctx(ctx, (size_t)conv_dim * sizeof(float)); + sc->s_qg = fc_buffer_alloc_ctx(ctx, (size_t)val_dim * sizeof(float)); + sc->s_kg = fc_buffer_alloc_ctx(ctx, (size_t)val_dim * sizeof(float)); + sc->s_vg = fc_buffer_alloc_ctx(ctx, (size_t)val_dim * sizeof(float)); + sc->s_gbuf = fc_buffer_alloc_ctx(ctx, (size_t)n_v_heads * sizeof(float)); + sc->s_y = fc_buffer_alloc_ctx(ctx, (size_t)val_dim * sizeof(float)); + sc->s_gated = fc_buffer_alloc_ctx(ctx, (size_t)val_dim * sizeof(float)); + sc->a_qg = fc_buffer_alloc_ctx(ctx, (size_t)qg_dim * sizeof(float)); + sc->a_q = fc_buffer_alloc_ctx(ctx, (size_t)attn_dim * sizeof(float)); + sc->a_z = fc_buffer_alloc_ctx(ctx, (size_t)attn_dim * sizeof(float)); + sc->a_k = fc_buffer_alloc_ctx(ctx, (size_t)kv_dim * sizeof(float)); + sc->a_v = fc_buffer_alloc_ctx(ctx, (size_t)kv_dim * sizeof(float)); + sc->a_out = fc_buffer_alloc_ctx(ctx, (size_t)attn_dim * sizeof(float)); + /* Dense-FFN (Bonsai) has no router / routed experts, so n_expert and + * n_expert_used are 0 — but a 0-byte alloc returns NULL and would trip the + * validity check below. Floor these MoE-only buffers to 1 element; the + * dense FFN path never reads them. */ + int n_expert_alloc = cfg->n_expert > 0 ? cfg->n_expert : 1; + int k_alloc = cfg->n_expert_used > 0 ? cfg->n_expert_used : 1; + sc->m_logits = fc_buffer_alloc_ctx(ctx, (size_t)n_expert_alloc * sizeof(float)); + sc->m_tidx = fc_buffer_alloc_ctx(ctx, (size_t)PN_QWEN35MOE_GPU_MAX_TOPK * sizeof(int32_t)); + sc->m_twt = fc_buffer_alloc_ctx(ctx, (size_t)PN_QWEN35MOE_GPU_MAX_TOPK * sizeof(float)); + sc->m_gate = fc_buffer_alloc_ctx(ctx, (size_t)ff * sizeof(float)); + sc->m_up = fc_buffer_alloc_ctx(ctx, (size_t)ff * sizeof(float)); + /* m_hid holds all k selected experts' SwiGLU activations at once for the + * grouped kernels (hid[s*ff + row]); k*ff also covers the old per-expert + * path's single-ff use. */ + sc->m_hid = fc_buffer_alloc_ctx(ctx, (size_t)k_alloc * + (size_t)ff * sizeof(float)); + sc->m_exp = fc_buffer_alloc_ctx(ctx, (size_t)n_embd * sizeof(float)); + sc->m_out = fc_buffer_alloc_ctx(ctx, (size_t)n_embd * sizeof(float)); + sc->m_shexp = fc_buffer_alloc_ctx(ctx, (size_t)n_embd * sizeof(float)); + sc->m_sgate = fc_buffer_alloc_ctx(ctx, sizeof(float)); + sc->r_hidden = fc_buffer_alloc_ctx(ctx, (size_t)n_embd * sizeof(float)); + sc->r_xnorm = fc_buffer_alloc_ctx(ctx, (size_t)n_embd * sizeof(float)); + sc->r_blockout = fc_buffer_alloc_ctx(ctx, (size_t)n_embd * sizeof(float)); + sc->r_logits = fc_buffer_alloc_ctx(ctx, (size_t)cfg->n_vocab * sizeof(float)); + sc->dec_pos = fc_buffer_alloc_ctx(ctx, sizeof(int32_t)); + + if (!sc->s_qkv || !sc->s_z || !sc->s_beta || !sc->s_alpha || + !sc->s_conv_out || !sc->s_qg || !sc->s_kg || !sc->s_vg || !sc->s_gbuf || + !sc->s_y || !sc->s_gated || !sc->a_qg || !sc->a_q || !sc->a_z || + !sc->a_k || !sc->a_v || !sc->a_out || !sc->m_logits || !sc->m_tidx || + !sc->m_twt || + !sc->m_gate || !sc->m_up || !sc->m_hid || !sc->m_exp || !sc->m_out || + !sc->m_shexp || !sc->m_sgate || !sc->r_hidden || !sc->r_xnorm || + !sc->r_blockout || !sc->r_logits || !sc->dec_pos) { + gpu_die_scratch_free(sc); + return NULL; + } + sc->ctx = ctx; + return sc; +} + +/* ============================================================ + * SSM (Gated-Delta-Net) block on the GPU + * + * Mirrors pn_qwen35moe_ssm_block. Projections + conv1d + recurrence + gated + * norm run on-device; the per-head L2-norm + interleaved GQA broadcast and the + * decay-gate softplus are host-side glue (TODO kernels). + * ============================================================ */ + +/* GPU SSM block. x_norm is the attn_norm'd input (GPU buffer). Writes the block + * output (pre-residual) to out_buf (GPU buffer) AND adds it into the residual + * stream `hidden` (fused into the ssm_out epilogue, or an explicit fc_add on + * the fallback path). dual state buffers (conv_state, ssm_state) are + * GPU-resident and updated in place. */ +static int gpu_ssm_block(gpu_ssm_layer *d, const pn_qwen35moe_config *cfg, + fc_buffer *x_norm, fc_buffer *conv_state_g, + fc_buffer *ssm_state_g, fc_buffer *out_buf, + fc_buffer *hidden, gpu_die_scratch *sc, + gpu_host_scratch *h) +{ + const int conv_dim = cfg->conv_dim; + const int S = cfg->ssm_state; + const int n_k_heads = cfg->ssm_n_group; + const int n_v_heads = cfg->ssm_dt_rank; + const int kv_groups = n_v_heads / n_k_heads; + const int key_dim = S * n_k_heads; + const int val_dim = S * n_v_heads; + const float eps = cfg->rms_norm_eps; + fc_cuda_ctx *ctx = d->ctx; /* this layer's die context */ + + /* Persistent per-die scratch (every buffer fully overwritten below). */ + fc_buffer *qkv = sc->s_qkv, *z = sc->s_z, *beta = sc->s_beta; + fc_buffer *alpha = sc->s_alpha, *conv_out = sc->s_conv_out; + fc_buffer *qg = sc->s_qg, *kg = sc->s_kg, *vg = sc->s_vg; + fc_buffer *gbuf = sc->s_gbuf, *y = sc->s_y, *gated = sc->s_gated; + + /* 1.+2. The four projections from x_norm — qkv, gate z, beta, alpha — + * batched into ONE fused launch, with beta's sigmoid and alpha's decay + * gate (g = ssm_a * softplus(alpha + ssm_dt), written straight to gbuf) + * folded into the F32 segments' epilogues. Falls back to the original + * 6-launch sequence if the quant mix is unsupported. */ + { + fc_fused_seg segs[4]; + segs[0] = gpu_seg_mv(&d->attn_qkv, qkv, 0); + segs[1] = gpu_seg_mv(&d->attn_gate, z, 0); + segs[2] = gpu_seg_mv(&d->ssm_beta, beta, 1); /* sigmoid */ + segs[3] = gpu_seg_mv(&d->ssm_alpha, gbuf, 2); /* decay gate */ + segs[3].aux0 = d->ssm_dt.buf; + segs[3].aux1 = d->ssm_a.buf; + if (!(gpu_fuse_mask() & 1) || + gpu_fused_run(ctx, &d->fx_proj, segs, 4, x_norm) != 0) { + gpu_matvec(&d->attn_qkv, x_norm, qkv, h->a, h->b); + gpu_matvec(&d->attn_gate, x_norm, z, h->a, h->b); + gpu_matvec(&d->ssm_beta, x_norm, beta, h->a, h->b); + gpu_matvec(&d->ssm_alpha, x_norm, alpha, h->a, h->b); + /* beta = sigmoid(beta), on device. */ + fc_sigmoid_ctx(ctx, beta, n_v_heads); + /* Decay gate g = ssm_a * softplus(alpha + ssm_dt), on device. */ + fc_ssm_decay_gate_ctx(ctx, alpha, d->ssm_dt.buf, d->ssm_a.buf, + gbuf, n_v_heads); + } + } + + /* 3. Causal conv1d + SiLU over the conv_dim stream; rolls conv_state. */ + fc_ssm_conv1d_silu_ctx(ctx, qkv, d->ssm_conv1d.buf, conv_state_g, conv_out, + conv_dim, cfg->ssm_conv); + + /* 3b. q/k per-head L2-norm + GQA interleaved-repeat (vh = r*n_k_heads + kh), + * v straight through — on device. Mirrors the CPU ssm / ggml_repeat. */ + fc_ssm_qknorm_gqa_ctx(ctx, conv_out, qg, kg, vg, n_k_heads, kv_groups, S, + key_dim, val_dim, eps); + + /* 4. Per-token delta-rule recurrence (scale = 1/sqrt(S)). On device. */ + fc_ssm_recurrence_ctx(ctx, ssm_state_g, qg, kg, vg, gbuf, beta, y, n_v_heads, + S, 1.0f / sqrtf((float)S)); + + /* 5. gated = RMSNorm(y, ssm_norm) * silu(z), then output projection with + * the +residual folded into the epilogue (out_buf keeps the pre-residual + * block output for the debug dumps; hidden += out_buf). */ + fc_ssm_gated_norm_ctx(ctx, y, d->ssm_norm.buf, z, gated, n_v_heads, S, eps); + { + fc_fused_seg seg = gpu_seg_mv(&d->ssm_out, out_buf, 3); + seg.aux1 = hidden; + if (!(gpu_fuse_mask() & 2) || + gpu_fused_run(ctx, &d->fx_out, &seg, 1, gated) != 0) { + gpu_matvec(&d->ssm_out, gated, out_buf, h->a, h->b); + fc_add_ctx(ctx, hidden, out_buf, hidden, cfg->n_embd); + } + } + + return 0; +} + +/* On-die decode flash attention — defined after gpu_attn_kv_cache_sync (which + * it reuses). Returns 0 = ran, 1 = unavailable, <0 = error. */ +static int gpu_attn_block_decode_flash(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + fc_buffer *x_norm, pn_qwen35moe_kv *kv, + int pos, gpu_die_scratch *sc, + gpu_host_scratch *h); + +/* Full-step segment graph (C1) — defined after the flash-decode block whose + * machinery it reuses. Runs die segment [lo, hi) as ONE captured CUDA graph + * (replaying, or lazily capturing at pos >= 1). Returns 0 = segment ran via + * the graph, 1 = not eligible (caller runs the per-layer loop), -1 = error. */ +static int gpu_segment_forward(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + fc_cuda_ctx *ctx, int seg_idx, int lo, int hi, + fc_buffer *hidden, fc_buffer *x_norm, + fc_buffer *block_out, pn_qwen35moe_state *s, + int pos, gpu_host_scratch *h); + +/* Phase 10b (lazy host KV mirror) — forward decls; definitions live with + * gpu_attn_kv_cache_sync below. */ +static int qw_kv_mirror_lazy(void); +static int qw_kv_layer_materialize(gpu_attn_layer *d, pn_qwen35moe_kv *kv, + int rows); +static size_t qw_kv_row_bytes(const pn_qwen35moe_kv *kv); +static size_t qw_kv_sink_bytes(const pn_qwen35moe_kv *kv); +static size_t qw_kv_win_bytes(const pn_qwen35moe_kv *kv); +static int gpu_attn_slot_ring_ensure(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + int q4, int max_seq, int nslots, + int kv_idx); + +/* ============================================================ + * Full-attention block on the GPU + * + * Mirrors pn_qwen35moe_attn_block: joint Q+gate proj + split, QK-RMSNorm, + * partial NEOX RoPE, GQA softmax over the KV window, sigmoid output gate, + * output proj. Projections/RoPE/QK-norm run on-device; the Q/gate split, the + * GQA softmax attention, and the sigmoid gate are host-side glue (TODO kernels). + * ============================================================ */ + +static int gpu_attn_block(gpu_attn_layer *d, const pn_qwen35moe_config *cfg, + fc_buffer *x_norm, pn_qwen35moe_kv *kv, + int pos, fc_buffer *out_buf, + fc_buffer *hidden, gpu_die_scratch *sc, + gpu_host_scratch *h) +{ + /* Phase 10b (lazy mirror): this host-softmax path reads the host KV + * mirror [0, pos). If earlier steps ran on-die with the lazy mirror the + * rows live only in a GPU ring — pull them down first. Once this path + * runs it keeps the mirror current itself (it appends every token). */ + if (kv->kv_mirror_stale && + qw_kv_layer_materialize(d, kv, + (pos < kv->max_seq) ? pos : kv->max_seq) != 0) { + fprintf(stderr, "[attn] host attn: mirror stale with no ring source " + "(pos=%d) — failing step\n", pos); + return -1; + } + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int n_kv_groups = n_head / n_head_kv; + const int q_stride = head_dim * 2; + const float eps = cfg->rms_norm_eps; + const int max_seq = kv->max_seq; + float *k_cache = kv->k; /* fp32 mode; NULL under q4 */ + float *v_cache = kv->v; + const int kv_q4 = kv->q4; + const int kv_sink = kv_q4 ? pn_kvq4_sink() : 0; + const int kv_win = kv_q4 ? pn_kvq4_win() : 0; + fc_cuda_ctx *ctx = d->ctx; /* this layer's die context */ + + /* Persistent per-die scratch (every buffer fully overwritten below). */ + fc_buffer *qg = sc->a_qg, *qbuf = sc->a_q, *kbuf = sc->a_k; + fc_buffer *vbuf = sc->a_v, *attn_out = sc->a_out; + + /* On-die decode flash attention (QWEN35_FLASH_DECODE): the whole block runs + * on the die (device split -> QK-norm/RoPE -> KV append -> split flash -> + * gate), leaving attn_out gated in sc->a_out. Returns 0 = ran (skip to the + * output projection), 1 = unavailable (fall through to the host-softmax + * path below), <0 = error. */ + int df = gpu_attn_block_decode_flash(d, cfg, x_norm, kv, pos, sc, h); + if (df < 0) + return -1; + if (df == 0) + goto out_proj; + + /* 1. Joint Q+gate, K and V projections — ONE fused launch (all three read + * x_norm; K/V moving ahead of the Q split touches disjoint buffers). */ + { + fc_fused_seg segs[3]; + segs[0] = gpu_seg_mv(&d->attn_q, qg, 0); + segs[1] = gpu_seg_mv(&d->attn_k, kbuf, 0); + segs[2] = gpu_seg_mv(&d->attn_v, vbuf, 0); + if (!(gpu_fuse_mask() & 4) || + gpu_fused_run(ctx, &d->fx_qkv, segs, 3, x_norm) != 0) { + gpu_matvec(&d->attn_q, x_norm, qg, h->a, h->b); + gpu_matvec(&d->attn_k, x_norm, kbuf, h->a, h->b); + gpu_matvec(&d->attn_v, x_norm, vbuf, h->a, h->b); + } + } + + /* Split per head: first head_dim = Q, second head_dim = gate z. TODO kernel: + * the strided Q/gate split. Host-side; q -> qbuf on device, z kept in h->c. */ + fc_sync_ctx(ctx); + fc_buffer_to_cpu(qg, h->a); + { + float *qonly = h->b; /* [n_head*head_dim] */ + float *zgate = h->c; /* [n_head*head_dim] */ + for (int hd = 0; hd < n_head; hd++) { + const float *src = h->a + (size_t)hd * q_stride; + memcpy(qonly + (size_t)hd * head_dim, src, (size_t)head_dim * sizeof(float)); + memcpy(zgate + (size_t)hd * head_dim, src + head_dim, + (size_t)head_dim * sizeof(float)); + } + fc_buffer_from_cpu(qbuf, qonly); + /* zgate stays in h->c for the sigmoid output gate below. */ + } + + /* 2.-4. Per-head QK-RMSNorm + partial NEOX RoPE on Q and K — ONE fused + * launch (was 4; per-head math identical). V is not normed/roped. */ + if (!(gpu_fuse_mask() & 8) || + fc_attn_qknorm_rope_ctx(ctx, qbuf, kbuf, d->attn_q_norm.buf, + d->attn_k_norm.buf, n_head, n_head_kv, + head_dim, eps, cfg->rope_dim, pos, + cfg->rope_theta) != 0) { + fc_qk_rms_norm_ctx(ctx, qbuf, d->attn_q_norm.buf, n_head, head_dim, eps); + fc_qk_rms_norm_ctx(ctx, kbuf, d->attn_k_norm.buf, n_head_kv, head_dim, eps); + fc_rope_neox_partial_ctx(ctx, qbuf, n_head, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + fc_rope_neox_partial_ctx(ctx, kbuf, n_head_kv, head_dim, cfg->rope_dim, pos, cfg->rope_theta); + } + + /* 5. Append this token's K/V to the host-side cache, then GQA-attend. + * TODO kernel: the KV-cache append + GQA softmax attention over the window + * (no fc kernel for windowed attention; fc_softmax is per-row only). Done + * host-side on read-backs of qbuf/kbuf/vbuf. The KV cache mirrors the CPU + * pn_qwen35moe_kv layout [n_head_kv][max_seq][head_dim]. */ + { + double tah0 = 0.0; + if (prof_on()) { fc_sync_ctx(ctx); tah0 = prof_now(); } + int cache_pos = pos % max_seq; + float *kvec = h->a; /* [n_head_kv*head_dim] */ + float *vvec = h->b; + fc_sync_ctx(ctx); + fc_buffer_to_cpu(kbuf, kvec); + fc_buffer_to_cpu(vbuf, vvec); + for (int hd = 0; hd < n_head_kv; hd++) { + if (kv_q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, hd, max_seq, + cache_pos, kvec + (size_t)hd * head_dim, + head_dim, kv_sink, kv_win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, hd, max_seq, + cache_pos, vvec + (size_t)hd * head_dim, + head_dim, kv_sink, kv_win); + } else { + memcpy(k_cache + ((size_t)hd * max_seq + cache_pos) * head_dim, + kvec + (size_t)hd * head_dim, (size_t)head_dim * sizeof(float)); + memcpy(v_cache + ((size_t)hd * max_seq + cache_pos) * head_dim, + vvec + (size_t)hd * head_dim, (size_t)head_dim * sizeof(float)); + } + } + + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + float *qhost = h->b; /* reuse: read q back */ + fc_buffer_to_cpu(qbuf, qhost); + float *aout = h->a; /* [n_head*head_dim] */ + float scale = 1.0f / sqrtf((float)head_dim); + /* per-head GQA softmax attention (mirrors qwen35moe_compute_heads); + * under q4 the K/V rows are dequantized on the fly. scores is heap + * scratch (seq_len can reach the 256K cap — too large for the stack). */ + float *scores = (float *)malloc((size_t)seq_len * sizeof(float)); + float krow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + float vrow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + if (!scores) { if (prof_on()) prof_phase[1] += prof_now() - tah0; return -1; } + for (int qh = 0; qh < n_head; qh++) { + int kv_h = qh / n_kv_groups; + const float *q_vec = qhost + (size_t)qh * head_dim; + const float *k_base = kv_q4 ? NULL : + k_cache + (size_t)kv_h * max_seq * head_dim; + const float *v_base = kv_q4 ? NULL : + v_cache + (size_t)kv_h * max_seq * head_dim; + float *out_vec = aout + (size_t)qh * head_dim; + for (int s = 0; s < seq_len; s++) { + const float *k_vec; + if (kv_q4) { + pn_kvq4_load_row(kv->kq, kv->k_sink, kv->k_win, kv_h, + max_seq, s, pos, krow, head_dim, + kv_sink, kv_win); + k_vec = krow; + } else { + k_vec = k_base + (size_t)s * head_dim; + } + scores[s] = pn_dot_f32(q_vec, k_vec, head_dim) * scale; + } + float mx = scores[0]; + for (int s = 1; s < seq_len; s++) + if (scores[s] > mx) mx = scores[s]; + float sum_exp = 0.0f; + for (int s = 0; s < seq_len; s++) { + scores[s] = expf(scores[s] - mx); + sum_exp += scores[s]; + } + float inv_sum = 1.0f / sum_exp; + memset(out_vec, 0, (size_t)head_dim * sizeof(float)); + for (int s = 0; s < seq_len; s++) { + float w_s = scores[s] * inv_sum; + const float *v_vec; + if (kv_q4) { + pn_kvq4_load_row(kv->vq, kv->v_sink, kv->v_win, kv_h, + max_seq, s, pos, vrow, head_dim, + kv_sink, kv_win); + v_vec = vrow; + } else { + v_vec = v_base + (size_t)s * head_dim; + } + for (int dd = 0; dd < head_dim; dd++) + out_vec[dd] += w_s * v_vec[dd]; + } + } + free(scores); + + /* 6. Output gate: attn_out *= sigmoid(z). TODO kernel: sigmoid gate. + * z is in h->c (set above). */ + int attn_dim = n_head * head_dim; + for (int i = 0; i < attn_dim; i++) { + float gv = 1.0f / (1.0f + expf(-h->c[i])); + aout[i] *= gv; + } + fc_buffer_from_cpu(attn_out, aout); + if (prof_on()) prof_phase[1] += prof_now() - tah0; + } + + /* 7. Output projection -> [n_embd] with the +residual folded into the + * epilogue (out_buf keeps the pre-residual output for the debug dumps). */ +out_proj: + { + fc_fused_seg seg = gpu_seg_mv(&d->attn_output, out_buf, 3); + seg.aux1 = hidden; + if (gpu_fused_run(ctx, &d->fx_out, &seg, 1, attn_out) != 0) { + gpu_matvec(&d->attn_output, attn_out, out_buf, h->a, h->b); + fc_add_ctx(ctx, hidden, out_buf, hidden, cfg->n_embd); + } + } + + return 0; +} + +/* ============================================================ + * Shared-expert MoE FFN on the GPU + * + * Mirrors pn_qwen35moe_moe_block. Router matvec + softmax/top-k + per-expert + * SwiGLU + shared expert run via fc_* kernels; the gate logit dot is host-side. + * ============================================================ */ + +/* SwiGLU for one expert on device: out = (silu(gate.x) * (up.x)) . down. All + * buffers + weights belong to ctx (this layer's die). */ +static void gpu_swiglu_expert(fc_cuda_ctx *ctx, const gpu_weight *gate, + const gpu_weight *up, const gpu_weight *down, + fc_buffer *x, int ff, fc_buffer *t_gate, + fc_buffer *t_up, fc_buffer *t_hid, fc_buffer *out, + gpu_host_scratch *h) +{ + gpu_matvec(gate, x, t_gate, h->a, h->b); + gpu_matvec(up, x, t_up, h->a, h->b); + fc_silu_ctx(ctx, t_gate, t_gate, ff); /* silu(gate) */ + fc_mul_ctx(ctx, t_gate, t_up, t_hid, ff); /* * up */ + gpu_matvec(down, t_hid, out, h->a, h->b); +} + +static int gpu_moe_block(fc_cuda_ctx *ctx, gpu_moe *d, + const pn_qwen35moe_config *cfg, fc_buffer *x_norm, + fc_buffer *out_buf, fc_buffer *hidden, + gpu_die_scratch *sc, gpu_host_scratch *h) +{ + const int n_embd = cfg->n_embd; + const int n_expert = cfg->n_expert; + const int k = cfg->n_expert_used; + const int expert_ff = cfg->expert_ff; + const int shared_ff = cfg->shared_ff; + + /* Persistent per-die scratch (every buffer fully overwritten below; + * moe_out is explicitly zeroed before accumulation). */ + fc_buffer *logits = sc->m_logits, *top_idx = sc->m_tidx, *top_wt = sc->m_twt; + fc_buffer *t_gate = sc->m_gate, *t_up = sc->m_up, *t_hid = sc->m_hid; + fc_buffer *exp_out = sc->m_exp, *moe_out = sc->m_out, *shexp = sc->m_shexp; + + /* Dense-FFN (Bonsai): a single plain SwiGLU, no router/experts/scalar-gate. + * gate/up/down are in the shexp slots; ffn dim = ffn_length. Mirrors the + * CPU dense path (qwen35moe_dense_ffn) and the shared-expert unfused + * sequence minus the sigmoid gate, then adds the residual. */ + if (cfg->dense_ffn) { + const int ff = cfg->ffn_length; + gpu_matvec(&d->ffn_gate_shexp, x_norm, t_gate, h->a, h->b); + gpu_matvec(&d->ffn_up_shexp, x_norm, t_up, h->a, h->b); + fc_moe_silu_mul_ctx(ctx, t_gate, t_up, t_gate, ff); /* silu(gate)*up */ + gpu_matvec(&d->ffn_down_shexp, t_gate, out_buf, h->a, h->b); + fc_add_ctx(ctx, hidden, out_buf, hidden, n_embd); /* residual */ + return 0; + } + + /* 1. Fused MoE prologue — ONE launch for everything that reads x_norm: + * the router logits, the shared-expert scalar-gate dot (sigmoid'd into + * m_sgate), and the shared expert's gate+up SwiGLU (silu(g.x)*(u.x) + * straight into t_gate; one warp computes both dots, replacing gate + * matvec + up matvec + silu_mul). Only on the grouped path — the + * per-expert fallback reuses t_gate for the routed experts. */ + int pre_fused = 0; + if (d->grouped && (gpu_fuse_mask() & 16) && + d->ffn_gate_shexp.type == d->ffn_up_shexp.type && + d->ffn_gate_shexp.rows == d->ffn_up_shexp.rows && + d->ffn_gate_shexp.row_stride == d->ffn_up_shexp.row_stride) { + double tp0 = 0.0; + if (prof_on()) { fc_sync_ctx(ctx); tp0 = prof_now(); } + fc_fused_seg segs[3]; + segs[0] = gpu_seg_mv(&d->ffn_gate_inp, logits, 0); + memset(&segs[1], 0, sizeof(segs[1])); /* scalar-gate dot */ + segs[1].w = d->ffn_gate_inp_shexp.buf; + segs[1].out = sc->m_sgate; + segs[1].rows = 1; + segs[1].cols = n_embd; + segs[1].row_stride = n_embd * (int)sizeof(float); + segs[1].kind = 1; + segs[1].ep = 1; /* sigmoid */ + segs[2] = gpu_seg_mv(&d->ffn_gate_shexp, t_gate, 0); + segs[2].w2 = d->ffn_up_shexp.bytes; + segs[2].kind = 2; /* gate+up SwiGLU pair */ + pre_fused = (gpu_fused_run(ctx, &d->fx_pre, segs, 3, x_norm) == 0); + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[9] += prof_now() - tp0; } + } + + /* Router matvec (unfused path) + softmax/top-k/norm_topk_prob/ + * expert_weights_scale on device. */ + if (!pre_fused) + gpu_matvec(&d->ffn_gate_inp, x_norm, logits, h->a, h->b); + fc_moe_router_topk_ctx(ctx, logits, top_idx, top_wt, n_expert, k, + cfg->expert_weights_scale); + + if (d->grouped) { + /* Grouped fast path: the whole routed-expert block in two launches + * that read top_idx/top_wt on device — no sync, no read-back, no + * zero pass. hid[s*ff+row] then out[row] = sum_s wt_s * down_e_s.hid_s + * (bit-identical reduction/accumulate order to the per-expert path). */ + double tg0 = 0.0; + if (prof_on()) { fc_sync_ctx(ctx); tg0 = prof_now(); } + fc_moe_gateup_ctx(ctx, (int)d->gate_all.type, d->gate_all.bytes, + d->up_all.bytes, x_norm, top_idx, t_hid, k, + expert_ff, n_embd, d->gate_all.row_stride); + if (prof_on()) { + fc_sync_ctx(ctx); + double tn = prof_now(); + prof_phase[7] += tn - tg0; + tg0 = tn; + } + fc_moe_down_accum_ctx(ctx, (int)d->down_all.type, d->down_all.bytes, + t_hid, top_idx, top_wt, moe_out, k, n_embd, + expert_ff, d->down_all.row_stride); + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[8] += prof_now() - tg0; } + } else { + /* Per-expert fallback (off-menu expert layouts): read the selection + * back and launch each expert's SwiGLU separately. */ + memset(h->a, 0, (size_t)n_embd * sizeof(float)); + fc_buffer_from_cpu(moe_out, h->a); + + int32_t idx_host[PN_QWEN35MOE_GPU_MAX_TOPK]; + float wt_host[PN_QWEN35MOE_GPU_MAX_TOPK]; + fc_sync_ctx(ctx); + fc_buffer_to_cpu(top_idx, idx_host); + fc_buffer_to_cpu(top_wt, wt_host); + + for (int s = 0; s < k; s++) { + int e = idx_host[s]; + if (e < 0 || e >= n_expert) + continue; + gpu_swiglu_expert(ctx, &d->gate_exps[e], &d->up_exps[e], + &d->down_exps[e], x_norm, expert_ff, t_gate, + t_up, t_hid, exp_out, h); + fc_moe_weighted_add_ctx(ctx, exp_out, moe_out, wt_host[s], n_embd); + } + } + + /* 2.+3. Shared-expert epilogue + combine. Fused path: ONE launch — the + * shexp down projection with epilogue out_buf[r] = moe_out[r] + + * dot*sigmoid_gate, hidden[r] += out_buf[r] (replaces down matvec + + * dot_gate + combine add + the caller's residual add). Fallback: the + * original sequence plus the residual add moved in from the caller. */ + double tsx0 = 0.0; + if (prof_on()) { fc_sync_ctx(ctx); tsx0 = prof_now(); } + int post_fused = 0; + if (pre_fused && (gpu_fuse_mask() & 32)) { + fc_fused_seg seg = gpu_seg_mv(&d->ffn_down_shexp, out_buf, 4); + seg.aux0 = moe_out; + seg.aux1 = hidden; + seg.aux2 = sc->m_sgate; + post_fused = (gpu_fused_run(ctx, &d->fx_post, &seg, 1, t_gate) == 0); + } + if (!post_fused) { + if (!pre_fused) { + gpu_matvec(&d->ffn_gate_shexp, x_norm, t_gate, h->a, h->b); + gpu_matvec(&d->ffn_up_shexp, x_norm, t_up, h->a, h->b); + fc_moe_silu_mul_ctx(ctx, t_gate, t_up, t_gate, shared_ff); + } + gpu_matvec(&d->ffn_down_shexp, t_gate, shexp, h->a, h->b); + fc_moe_shexp_dot_gate_ctx(ctx, d->ffn_gate_inp_shexp.buf, x_norm, + shexp, n_embd); + fc_add_ctx(ctx, moe_out, shexp, out_buf, n_embd); + fc_add_ctx(ctx, hidden, out_buf, hidden, n_embd); + } + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[9] += prof_now() - tsx0; } + + return 0; +} + +/* ============================================================ + * Per-layer forward (the sandwich) + the full token forward + * ============================================================ */ + +/* One SSM layer's COMPLETE launch sequence: attn_norm -> SSM block (+residual + * fused into its output epilogue) -> post_attention_norm -> shared-MoE + * (+residual fused). Every op is an async launch on the layer's stream over + * persistent buffers with position-invariant grids/args, so the whole body is + * CUDA-graph-capturable — one cuGraphLaunch replays it exactly (decode is + * launch-bound). The dbg dumps and prof timers inside are no-ops unless their + * env knobs are set, and those knobs disable graph capture (they sync + * mid-body). */ +static int gpu_ssm_layer_body(gpu_ssm_layer *d, const pn_qwen35moe_config *cfg, + int layer, fc_buffer *hidden, fc_buffer *x_norm, + fc_buffer *block_out, gpu_die_scratch *sc, + gpu_host_scratch *h) +{ + fc_cuda_ctx *ctx = d->ctx; + int c = cfg->n_embd; + int rc; + + fc_rms_norm_ctx(ctx, hidden, d->attn_norm.buf, x_norm, c, cfg->rms_norm_eps); + qwen35moe_dbg_gpu(ctx, "attn_norm", layer, x_norm, c, h->a); + + double tb0 = prof_on() ? prof_now() : 0.0; + rc = gpu_ssm_block(d, cfg, x_norm, d->conv_state_g, d->ssm_state_g, + block_out, hidden, sc, h); + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[5] += prof_now() - tb0; } + if (rc < 0) + return rc; + + /* block_out keeps the pre-residual output so the debug dumps still line + * up with the CPU reference. */ + qwen35moe_dbg_gpu(ctx, "block_out", layer, block_out, c, h->a); + qwen35moe_dbg_gpu(ctx, "attn_residual", layer, hidden, c, h->a); + + /* Shared-expert MoE on the post_attention_norm'd hidden state. */ + fc_rms_norm_ctx(ctx, hidden, d->post_attention_norm.buf, x_norm, c, + cfg->rms_norm_eps); + qwen35moe_dbg_gpu(ctx, "attn_post_norm", layer, x_norm, c, h->a); + double tm0 = prof_on() ? prof_now() : 0.0; + rc = gpu_moe_block(ctx, &d->moe, cfg, x_norm, block_out, hidden, sc, h); + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[2] += prof_now() - tm0; } + if (rc < 0) + return rc; + qwen35moe_dbg_gpu(ctx, "ffn_out", layer, block_out, c, h->a); + qwen35moe_dbg_gpu(ctx, "l_out", layer, hidden, c, h->a); + + return 0; +} + +/* Run one layer on the GPU: attn_norm -> (SSM|attn) -> +residual -> + * post_attention_norm -> shared-MoE -> +residual. hidden is the GPU residual + * stream (updated in place). x_norm/block_out are reusable GPU scratch. All of + * hidden/x_norm/block_out MUST belong to `ctx` — this layer's die context, the + * caller swaps the barswap slot to that die before calling. + * + * SSM layers replay a captured CUDA graph of their whole launch sequence + * (captured lazily at pos >= 1, after token 0 built the fused descriptors); + * attention layers keep the direct path HERE (fallback). When the segment + * graph is live (gpu_segment_forward, QWEN35_GRAPH_FULL) this function is + * bypassed entirely — the whole die segment, attention layers included, + * replays as one graph via the device-pos flash-decode kernels. */ +static int gpu_layer_forward(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + fc_cuda_ctx *ctx, int layer, fc_buffer *hidden, + fc_buffer *x_norm, fc_buffer *block_out, + pn_qwen35moe_state *s, int pos, gpu_host_scratch *h) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + int idx = pn_qwen35moe_model_layer_index(m, layer); + int rc; + + gpu_die_scratch *sc = gpu_get_scratch(g, ctx, cfg); + if (!sc) + return -1; + + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + gpu_attn_layer *d = &g->attn[idx]; + fc_rms_norm_ctx(ctx, hidden, d->attn_norm.buf, x_norm, c, + cfg->rms_norm_eps); + qwen35moe_dbg_gpu(ctx, "attn_norm", layer, x_norm, c, h->a); + pn_qwen35moe_kv *kv = &s->kv[idx]; + double tb0 = prof_on() ? prof_now() : 0.0; + rc = gpu_attn_block(d, cfg, x_norm, kv, pos, + block_out, hidden, sc, h); + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[6] += prof_now() - tb0; } + if (rc < 0) + return rc; + qwen35moe_dbg_gpu(ctx, "block_out", layer, block_out, c, h->a); + qwen35moe_dbg_gpu(ctx, "attn_residual", layer, hidden, c, h->a); + + fc_rms_norm_ctx(ctx, hidden, d->post_attention_norm.buf, x_norm, c, + cfg->rms_norm_eps); + qwen35moe_dbg_gpu(ctx, "attn_post_norm", layer, x_norm, c, h->a); + double tm0 = prof_on() ? prof_now() : 0.0; + rc = gpu_moe_block(ctx, &d->moe, cfg, x_norm, block_out, hidden, sc, h); + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[2] += prof_now() - tm0; } + if (rc < 0) + return rc; + qwen35moe_dbg_gpu(ctx, "ffn_out", layer, block_out, c, h->a); + qwen35moe_dbg_gpu(ctx, "l_out", layer, hidden, c, h->a); + d->body_ran = 1; /* segment-graph gate: fused descs/scratch built */ + return 0; + } + + /* ---- SSM layer. ---- */ + gpu_ssm_layer *d = &g->ssm[idx]; + pn_qwen35moe_ssm_state *st = &s->ssm[idx]; + + /* Persistent GPU-resident dual state (shadows the host copies in + * s->ssm[idx]; the host mirror is NOT read back per token — enable + * QWEN35_DEBUG bisection only with the CPU path or accept that the host + * copy goes stale). Allocated ONCE on this layer's die; a state swap or + * a sequence restart at pos 0 re-seeds the CONTENTS with an H2D copy so + * the buffer addresses stay stable for the captured graph. */ + size_t conv_n = (size_t)st->conv_minus_1 * st->conv_dim; + size_t ssm_n = (size_t)st->state_size * st->state_size * st->n_v_heads; + double ts0 = prof_on() ? prof_now() : 0.0; + if (!d->conv_state_g) + d->conv_state_g = fc_buffer_alloc_ctx(ctx, conv_n * sizeof(float)); + if (!d->ssm_state_g) + d->ssm_state_g = fc_buffer_alloc_ctx(ctx, ssm_n * sizeof(float)); + if (!d->conv_state_g || !d->ssm_state_g) + return -1; + if (d->host_state != (const void *)st || pos == 0) { + fc_buffer_from_cpu(d->conv_state_g, st->conv_state); + fc_buffer_from_cpu(d->ssm_state_g, st->ssm_state); + d->host_state = (const void *)st; + } + if (prof_on()) { fc_sync_ctx(ctx); prof_phase[0] += prof_now() - ts0; } + + /* Fast path: replay the layer's captured graph (~13 launches -> 1). */ + if (d->graph) { + if (fc_graph_launch(d->graph) == 0) + return 0; + /* Replay failed — the graph is no use to anyone. Drop it and re-arm a + * fresh capture rather than re-launching a dead graph every token. */ + fc_graph_free(d->graph); + d->graph = NULL; + pn_graph_capture_dropped(&d->graph_capture); + } + + /* Capture lazily: only after a DIRECT body run (body_ran) built the fused + * descriptors and per-die scratch (those creations alloc/copy and are not + * capturable — see body_ran). prof/dbg sync mid-body — direct path only. + * The non-grouped MoE fallback reads the expert selection back to the host + * mid-block, so it is not capturable either. */ + int graph_gate_ok = gpu_graph_enabled() && !prof_on() && + !qwen35moe_dbg_enabled() && + (d->moe.grouped || cfg->dense_ffn); + if (!graph_gate_ok) { + pn_graph_capture_ineligible(&d->graph_capture, + !gpu_graph_enabled() ? PN_GRAPH_INELIGIBLE_GRAPH_OFF + : (prof_on() || qwen35moe_dbg_enabled()) + ? PN_GRAPH_INELIGIBLE_PROFILING + : PN_GRAPH_INELIGIBLE_MOE_NOT_GROUPED); + } + + /* pos>=1 && body_ran are warm-up, not ineligibility — they clear by + * themselves, so the tracker stays PENDING through them rather than + * reporting a healthy layer as "will never capture". */ + if (graph_gate_ok && pos >= 1 && d->body_ran && + pn_graph_capture_should_attempt(&d->graph_capture)) { + int captured = 0; + if (fc_graph_begin_ctx(ctx) == 0) { + rc = gpu_ssm_layer_body(d, cfg, layer, hidden, x_norm, block_out, + sc, h); + d->graph = fc_graph_end_ctx(ctx); + if (pn_graph_capture_inject_now(&d->graph_capture) && d->graph) { + fc_graph_free(d->graph); /* QWEN35_GRAPH_CAPTURE_FAIL_INJECT */ + d->graph = NULL; + } + /* The recorded launches did NOT execute — replay the graph now, + * or fall through and run the body directly. */ + if (rc == 0 && d->graph && fc_graph_launch(d->graph) == 0) + captured = 1; + else if (d->graph) { + fc_graph_free(d->graph); + d->graph = NULL; + } + } + if (captured) { + pn_graph_capture_succeeded(&d->graph_capture); + return 0; + } + pn_graph_capture_failed(&d->graph_capture); + } + + rc = gpu_ssm_layer_body(d, cfg, layer, hidden, x_norm, block_out, sc, h); + if (rc == 0) + d->body_ran = 1; + return rc; +} + +/* Decode-time logits fetch mode. Default is device top-k (k = 40, enough for + * both greedy argmax and the serving sampler's top_k=40): the 248k-vocab + * logits never cross the 0.4 GB/s host link — the die reduces them to k + * (id, logit) pairs (~4 ms/token -> microseconds). + * QWEN35_GPU_TOPK= override k (1 = pure argmax), clamped to + * [1, FC_TOPK_MAX] + * QWEN35_GPU_FULL_LOGITS=1 restore the full n_vocab D2H copy (anything + * that needs ALL logits) + * QWEN35_DEBUG also forces the full path so the result_output dump stays + * real. Returns 0 for full mode. */ +/* The per-state survivor-id array must be able to hold a full k. */ +_Static_assert(PN_QWEN35MOE_TOPK_MAX >= FC_TOPK_MAX, + "PN_QWEN35MOE_TOPK_MAX must cover FC_TOPK_MAX survivors"); + +/* Does any state in this batch need the full logits row (constrained decoding)? + * The die reduces every slot in one kernel, so the batch is all-or-nothing; full + * logits are a superset of top-k, so answering "yes" for the whole step is + * correct for the unconstrained slots too — they just get more than they need, + * and only while a constrained request is actually in flight. */ +static int batch_needs_full_logits(pn_qwen35moe_state *const *sts, int n) +{ + for (int i = 0; i < n; i++) + if (sts[i] && sts[i]->full_logits) return 1; + return 0; +} + +static int gpu_topk_k(void) +{ + static int v = -1; + if (v < 0) { + if (getenv("QWEN35_GPU_FULL_LOGITS") || qwen35moe_dbg_enabled()) { + v = 0; + } else { + v = 40; + const char *env = getenv("QWEN35_GPU_TOPK"); + if (env) { + v = atoi(env); + if (v < 0) v = 0; + if (v > FC_TOPK_MAX) v = FC_TOPK_MAX; + } + } + } + return v; +} + +/* Present k (id, val) survivors sparsely in s->logits. + * + * The sparse contract: s->logits is -inf everywhere except this step's k + * survivors. To avoid an n_vocab -inf memset per token it is filled in full + * ONCE and thereafter patched incrementally — clear the ids the PREVIOUS fetch + * wrote, write this fetch's. That is only sound if the bookkeeping describes + * every write the buffer has seen, which is why it lives on the state that + * OWNS the buffer (s->topk_*) and not on the shared per-model GPU context. + * + * It used to live on the GPU context, in two independent copies: g->topk_* + * (used by the prefill / single-user fetch) and g->slot_topk_*[slot] (used by + * the batched decode fetch). Both maintained the SAME s->logits buffer while + * each cleared only its OWN previous ids, so whatever the other last wrote + * lingered as phantom finite entries — forever. A turn that ended on EOS left + * EOS behind at its real logit; the next turn's prefill did not clear it (the + * prefill tracker's "already filled" guard still matched the buffer address), + * EOS won at step 0, and the turn emitted ZERO tokens with finish_reason + * "stop" — which, generating nothing, left the phantom in place and latched + * the model until reload. One tracker per buffer, owned by the buffer, is the + * fix: every writer now clears exactly what the previous writer left. */ +static void gpu_present_topk(pn_qwen35moe_state *s, const int *ids, + const float *vals, int k, int n_vocab) +{ + float *dst = s->logits; + + if (!s->topk_filled) { /* first fetch into this state's buffer */ + for (int i = 0; i < n_vocab; i++) + dst[i] = -INFINITY; + s->topk_filled = 1; + s->topk_prev_n = 0; + } + for (int i = 0; i < s->topk_prev_n; i++) + dst[s->topk_prev_ids[i]] = -INFINITY; + for (int i = 0; i < k; i++) + dst[ids[i]] = vals[i]; + memcpy(s->topk_prev_ids, ids, (size_t)k * sizeof(int)); + s->topk_prev_n = k; +} + +/* Read the LM-head result off the die into s->logits[n_vocab]. + * + * Top-k mode (default): fc_topk_logits_ctx reduces the logits ON the die + * (bit-exact iterative argmax, host tie-break) and copies back only k + * (id, logit) pairs, presented SPARSELY by gpu_present_topk above — so + * downstream consumers are untouched: the host argmax scan finds the identical + * token, and any sampler with top_k <= k sees exactly the survivors it would + * have kept. + * + * Full mode (k == 0, or any top-k failure): the original full n_vocab D2H + * copy. */ +static void gpu_fetch_logits(fc_cuda_ctx *ctx, fc_buffer *logits_g, + pn_qwen35moe_state *s, int n_vocab) +{ + int k = gpu_topk_k(); + if (s && s->full_logits) k = 0; /* constrained decode needs every logit */ + int ids[FC_TOPK_MAX]; + float vals[FC_TOPK_MAX]; + + if (k > 0 && + fc_topk_logits_ctx(ctx, logits_g, n_vocab, k, ids, vals) == 0) { + gpu_present_topk(s, ids, vals, k, n_vocab); + return; + } + + fc_buffer_to_cpu(logits_g, s->logits); + s->topk_filled = 0; /* logits holds real values everywhere */ +} + +/* QWEN35_PERSLOT_FULL_LOGITS=1 — decide top-k vs full logits PER SLOT instead + * of for the whole batch. Default OFF: this is a decode-path change that has + * not been through the agent-client gate on the K80 test machine yet. */ +static int perslot_full_logits_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_PERSLOT_FULL_LOGITS"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* Deliver each slot's LM-head result out of the batched [nslots][n_vocab] + * `logits_all` into that slot's own s->logits. + * + * WHY THIS IS NOT JUST THE OLD LOOP. A slot that asked for constrained + * decoding (tool grammar, logit_bias or logprobs — see sg_needs_full_logits) + * sets s->full_logits for the WHOLE TURN, and the old code answered that + * question for the entire batch: one such slot and every slot dropped onto the + * per-slot fallback, which costs, per slot, a 993 KB device-to-device stage + * into sc->r_logits, a full stream sync, and its own 2k+1 top-k launches over + * its own row. At k=40 that is 81 launches and ~40 MB of device reads PER + * SLOT where the batched reduction does all K rows in the SAME 81 launches + * (blockIdx.y is the slot). The innocent slots paid for the constrained one. + * + * Per-slot instead: run the batched reduction once over all K rows exactly as + * an unconstrained step does, hand the unconstrained slots their sparse + * presentation from it, and give ONLY the constrained slots a full row — + * copied straight out of `logits_all` at that slot's row offset, so even they + * lose the D2D stage and the extra sync. When EVERY slot wants full logits + * (K=1 with a grammar, the live single-user tool-calling shape) the reduction + * is skipped entirely rather than run and thrown away. + * + * Bit-exact, per slot, either way. fc_topk_logits_slots_ctx is documented and + * built to be per-row identical to fc_topk_logits_ctx — same nparts, scan + * order, comparator and reduction shapes — and it restores the row it clobbers + * during its rounds before it syncs, so the full-row copy taken afterwards is + * the same bytes fc_buffer_to_cpu would have taken. + * + * @param ctx the die the head ran on; `logits_all` and `sc` belong to it + * @param logits_all [nslots][n_vocab] batched head output + * @param sc per-die scratch, for the legacy fallback's single-row stage + * @param sts [nslots] states, already offset to this group's base + * @param nslots rows in `logits_all` + * @param n_vocab vocabulary size + * @return 0 on success, -1 if a state is missing + * + * Thread-safe: no (per-die, called inside the model's GPU turn) + */ +static int gpu_present_slots_logits(fc_cuda_ctx *ctx, fc_buffer *logits_all, + gpu_die_scratch *sc, + pn_qwen35moe_state *const *sts, int nslots, + int n_vocab) +{ + int k = gpu_topk_k(); + size_t row_bytes = (size_t)n_vocab * sizeof(float); + int ids_all[PN_QWEN35MOE_GPU_MAX_SLOTS * FC_TOPK_MAX]; + float vals_all[PN_QWEN35MOE_GPU_MAX_SLOTS * FC_TOPK_MAX]; + + for (int u = 0; u < nslots; u++) + if (!sts[u]) return -1; + + if (k > 0 && perslot_full_logits_on()) { + int n_topk = 0; + for (int u = 0; u < nslots; u++) + if (!sts[u]->full_logits) n_topk++; + + int reduced = 0; + if (n_topk > 0) { + reduced = (fc_topk_logits_slots_ctx(ctx, logits_all, nslots, + n_vocab, k, ids_all, + vals_all) == 0); + } else { + /* No slot wants a sparse row, so nothing reduces the head — but + * the copies below are on the legacy NULL stream and the head + * matvec is on ctx->stream, so commit it explicitly. */ + fc_sync_ctx(ctx); + } + if (reduced || n_topk == 0) { + for (int u = 0; u < nslots; u++) { + if (sts[u]->full_logits) { + fc_buffer_to_cpu_at(logits_all, sts[u]->logits, + (size_t)u * row_bytes, row_bytes); + sts[u]->topk_filled = 0; + } else { + gpu_present_topk(sts[u], ids_all + (size_t)u * k, + vals_all + (size_t)u * k, k, n_vocab); + } + } + return 0; + } + /* The batched reduction failed: fall through to the legacy path, which + * is the same fallback the batch-wide code takes. */ + } + + if (batch_needs_full_logits(sts, nslots)) k = 0; + if (k > 0 && fc_topk_logits_slots_ctx(ctx, logits_all, nslots, n_vocab, k, + ids_all, vals_all) == 0) { + for (int u = 0; u < nslots; u++) + gpu_present_topk(sts[u], ids_all + (size_t)u * k, + vals_all + (size_t)u * k, k, n_vocab); + return 0; + } + for (int u = 0; u < nslots; u++) { + fc_buffer_copy_d2d_ctx(ctx, sc->r_logits, 0, logits_all, + (size_t)u * row_bytes, row_bytes); + fc_sync_ctx(ctx); + gpu_fetch_logits(ctx, sc->r_logits, sts[u], n_vocab); + } + return 0; +} + +float *pn_qwen35moe_forward_token_cuda(pn_qwen35moe_model *m, uint32_t token, + pn_qwen35moe_state *s, int pos) +{ + if (!m || !s) + return NULL; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return NULL; /* not uploaded — caller should fall back to the CPU path */ + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + if ((int)token >= cfg->n_vocab) + return NULL; + + /* NOTE: no sparse-top-k reset is needed here any more. The tracking now + * lives on the state that owns s->logits (see gpu_present_topk), so a + * fresh state starts un-filled by construction (calloc) and a reused one + * carries bookkeeping that still describes its own buffer. The old + * pos==0 reset existed only to paper over the tracking living on the + * shared GPU context, keyed by the buffer's address. */ + + /* Persistent host scratch: allocated on the first forward and reused — + * the previous per-token calloc/free of ~3 x n_vocab floats was pure + * mmap/page-fault churn on the 2-core K80 host. */ + if (!g->host_ok) { + if (gpu_host_scratch_init(&g->host, cfg) < 0) + return NULL; + g->host_ok = 1; + } + gpu_host_scratch h = g->host; /* pointer copies — the buffers persist */ + + double prof_t0 = 0.0; + if (prof_on()) { + prof_reset(); + prof_t0 = prof_now(); + } + + /* The residual stream + scratch + the LM-head output buffer all come from + * the per-die persistent scratch (gpu_die_scratch), so each die works in + * its OWN memory. The boundary `hidden` is handed to the next die between + * segments (gpu_handoff_hidden); the SSM/attention dual state never + * crosses. */ + fc_buffer *logits_g = NULL; + gpu_die_scratch *sc = NULL; + + /* Embed: fetch the token's embedding row (per-row from the mmap when the + * F32 heap was dropped, A2c), then upload it as die A's residual stream. */ + pn_qwen35moe_model_embed_row(m, token, h.a); + qwen35moe_dbg("input_embed", -1, h.a, c); + + /* ---- Walk the dies in order. Die i runs its layer range against its own + * scratch, then hands the residual stream to die i+1. The last die also + * runs the final norm + LM head. ---- */ + for (int d = 0; d < g->n_die; d++) { + if (gpu_select_die(g, g->die[d]) < 0) + goto fail; + sc = gpu_get_scratch(g, g->ctx[d], cfg); + if (!sc) + goto fail; + if (d == 0) + fc_buffer_from_cpu(sc->r_hidden, h.a); /* seed the stream */ + + int layer_lo = g->layer_lo[d]; + int layer_hi = g->layer_hi[d]; + /* Single-die prefix cap (same env as upload, single-die mode only): + * run only the first N global layers so the per-layer l_out dumps can + * be diffed against the CPU on one die. The final norm/head are + * meaningless when capped (partial hidden) — only the per-layer dumps + * matter in that mode. */ + if (g->n_die == 1) { + const char *cap_env = getenv("QWEN35_GPU_MAX_LAYERS"); + if (cap_env) { int v = atoi(cap_env); + if (v > 0 && v < layer_hi) layer_hi = v; } + } + + /* Segment graph first (C1): this die's whole segment as ONE + * cuGraphLaunch. Falls back to the per-layer loop (with its per-layer + * SSM graphs) when not eligible — pos 0, capture failed, ring wrap, + * prof/dbg, etc. */ + int seg_rc = gpu_segment_forward(m, g, g->ctx[d], d, layer_lo, layer_hi, + sc->r_hidden, sc->r_xnorm, + sc->r_blockout, s, pos, &h); + if (seg_rc < 0) + goto fail; + if (seg_rc != 0) { + for (int layer = layer_lo; layer < layer_hi; layer++) { + if (gpu_layer_forward(m, g, g->ctx[d], layer, sc->r_hidden, + sc->r_xnorm, sc->r_blockout, s, pos, + &h) < 0) + goto fail; + } + } + + if (d + 1 < g->n_die) { + /* Boundary hand-off, then move onto the next die. Export runs + * while THIS die is still selected; import after the switch, once + * the receiving die's scratch exists. */ + size_t hidden_bytes = (size_t)c * sizeof(float); + gpu_handoff_export(g, d, sc->r_hidden, h.a, hidden_bytes); + if (gpu_select_die(g, g->die[d + 1]) < 0) + goto fail; + gpu_die_scratch *next = gpu_get_scratch(g, g->ctx[d + 1], cfg); + if (!next) + goto fail; + gpu_handoff_import(g, d, sc->r_hidden, next->r_hidden, h.a, + hidden_bytes); + continue; + } + + /* ---- Last die: final norm + LM head. ---- */ + logits_g = sc->r_logits; + fc_rms_norm_ctx(g->ctx[d], sc->r_hidden, g->output_norm.buf, + sc->r_xnorm, c, cfg->rms_norm_eps); + qwen35moe_dbg_gpu(g->ctx[d], "result_norm", -1, sc->r_xnorm, c, h.a); + double th0 = prof_on() ? prof_now() : 0.0; + gpu_matvec(&g->output, sc->r_xnorm, logits_g, h.a, h.b); + if (prof_on()) { fc_sync_ctx(g->ctx[d]); prof_phase[3] += prof_now() - th0; } + double tl0 = prof_on() ? prof_now() : 0.0; + fc_sync_ctx(g->ctx[d]); + gpu_fetch_logits(g->ctx[d], logits_g, s, cfg->n_vocab); + if (prof_on()) prof_phase[4] += prof_now() - tl0; + qwen35moe_dbg("result_output", -1, s->logits, cfg->n_vocab); + } + + /* All buffers (device gpu_die_scratch + host g->host) are persistent — + * nothing to free per token. */ + if (prof_on()) + prof_report(prof_now() - prof_t0); + return s->logits; + +fail: + return NULL; +} + +/* ============================================================ + * BATCHED PREFILL forward — n tokens of ONE sequence in one pass + * + * The serving-foundation path: all n positions' activations live in + * token-major [n][dim] GPU buffers and every per-token matvec becomes ONE + * fc_matvec_*_nx launch (the weight is streamed once per 8-column tile — + * FC_NX_COLS, L6-widened — instead of once per token). The SSM conv/recurrence are scans — one + * launch walks the n positions inside the kernel, carrying state exactly + * as n sequential tokens would. Attention appends all n K/V rows to the + * host cache and runs the same host GQA softmax per position (causal by + * construction: position t attends to cache entries [0, pos0+t]). MoE + * routes each position to its own top-8 experts in batched launches. + * + * BIT-EXACT: every kernel used here is per-position bit-identical to the + * single-token path's kernel (see the fc_*_nx headers), the host glue is + * the same code per position, and the accumulation orders are unchanged — + * so the per-position logits, the final KV cache, and the carried SSM + * state are bit-identical to running the n tokens one at a time through + * pn_qwen35moe_forward_token_cuda. + * + * The single-token fused/graph fast paths are NOT used here (the batch is + * compute-dense enough that launch count no longer dominates); the batch + * path is additive and leaves the single-token path untouched. + * ============================================================ */ + +static void gpu_batch_scratch_free(gpu_batch_scratch *b) +{ + fc_buffer **bufs[] = { + &b->hidden, &b->x_norm, &b->block_out, + &b->s_qkv, &b->s_conv, &b->s_z, &b->s_qg, &b->s_kg, &b->s_vg, + &b->s_y, &b->s_gated, &b->s_beta, &b->s_alpha, &b->s_g, + &b->a_qg, &b->a_q, &b->a_z, &b->a_out, &b->a_k, &b->a_v, + &b->m_logits, &b->m_tidx, &b->m_twt, &b->m_gate, &b->m_up, + &b->m_hid, &b->m_out, &b->m_shexp, &b->m_ecount, &b->m_ereq, + &b->m_wlist, + }; + for (size_t i = 0; i < sizeof(bufs) / sizeof(bufs[0]); i++) { + fc_buffer_free(*bufs[i]); + *bufs[i] = NULL; + } + b->ctx = NULL; + b->n_cap = 0; +} + +/* Get (lazily creating / growing) the batch scratch for ctx's die, sized for + * at least n positions. Returns NULL for a context that is not one of the + * split's dies, or on allocation failure. Keyed by die index — see + * gpu_get_scratch for why the old two-slot scan could not survive a wider + * split. */ +static gpu_batch_scratch *gpu_get_batch_scratch(pn_qwen35moe_gpu *g, + fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + int n) +{ + int die_idx = gpu_die_index_of_ctx(g, ctx); + if (die_idx < 0) + return NULL; + gpu_batch_scratch *b = &g->bscratch[die_idx]; + if (b->ctx == ctx && b->n_cap >= n) + return b; + if (b->ctx) + gpu_batch_scratch_free(b); + + const size_t c = (size_t)cfg->n_embd; + const size_t conv_dim = (size_t)cfg->conv_dim; + const size_t val_dim = (size_t)cfg->ssm_state * cfg->ssm_dt_rank; + const size_t nvh = (size_t)cfg->ssm_dt_rank; + const size_t attn_dim = (size_t)cfg->head_dim * cfg->n_head; + const size_t kv_dim = (size_t)cfg->head_dim * cfg->n_head_kv; + const size_t nn = (size_t)n; + size_t ff = (size_t)(cfg->expert_ff > cfg->shared_ff ? cfg->expert_ff + : cfg->shared_ff); +#define BALLOC(field, elems) \ + b->field = fc_buffer_alloc_ctx(ctx, nn * (elems) * sizeof(float)) + BALLOC(hidden, c); BALLOC(x_norm, c); BALLOC(block_out, c); + BALLOC(s_qkv, conv_dim); BALLOC(s_conv, conv_dim); + BALLOC(s_z, val_dim); BALLOC(s_qg, val_dim); BALLOC(s_kg, val_dim); + BALLOC(s_vg, val_dim); BALLOC(s_y, val_dim); BALLOC(s_gated, val_dim); + BALLOC(s_beta, nvh); BALLOC(s_alpha, nvh); BALLOC(s_g, nvh); + BALLOC(a_qg, 2 * attn_dim); BALLOC(a_q, attn_dim); BALLOC(a_z, attn_dim); + BALLOC(a_out, attn_dim); + BALLOC(a_k, kv_dim); BALLOC(a_v, kv_dim); + BALLOC(m_logits, (size_t)cfg->n_expert); + b->m_tidx = fc_buffer_alloc_ctx(ctx, nn * cfg->n_expert_used * + sizeof(int32_t)); + BALLOC(m_twt, (size_t)cfg->n_expert_used); + BALLOC(m_gate, ff); BALLOC(m_up, ff); + BALLOC(m_hid, (size_t)cfg->n_expert_used * ff); + BALLOC(m_out, c); BALLOC(m_shexp, c); +#undef BALLOC + /* Expert-union dedup request lists (flag-gated, so the default build + * allocates nothing here). ereq is capped at n requests per expert: a + * token's k slots are distinct, so it can appear in an expert's list at + * most once and R <= n. */ + if (gpu_moe_dedup_enabled() && cfg->n_expert > 0) { + b->m_ecount = fc_buffer_alloc_ctx(ctx, (size_t)cfg->n_expert * + sizeof(int32_t)); + b->m_ereq = fc_buffer_alloc_ctx(ctx, (size_t)cfg->n_expert * nn * + sizeof(int32_t)); + if (!b->m_ecount || !b->m_ereq) { + gpu_batch_scratch_free(b); + return NULL; + } + /* Compacted work list for the compacted-grid gate+up. Sized at the + * EXACT host-side bound on the (expert, request-tile) count, which is + * also the grid.y that launch uses — 352 int2 = 2.8 KB at the prefill + * shape. Allocated whenever the dedup path is on: it is negligible, and + * a NULL here is what disables the compacted arm. wmax is monotone in + * ntok, so sizing it at n_cap covers every chunk this scratch serves. */ + int wmax = fc_moe_dedup_wmax(cfg->n_expert, cfg->n_expert_used, n, n); + if (wmax > 0) + b->m_wlist = fc_buffer_alloc_ctx(ctx, (size_t)wmax * 2 * + sizeof(int32_t)); + } + + if (!b->hidden || !b->x_norm || !b->block_out || !b->s_qkv || !b->s_conv || + !b->s_z || !b->s_qg || !b->s_kg || !b->s_vg || !b->s_y || !b->s_gated || + !b->s_beta || !b->s_alpha || !b->s_g || !b->a_qg || !b->a_q || + !b->a_z || !b->a_out || !b->a_k || !b->a_v || !b->m_logits || + !b->m_tidx || + !b->m_twt || !b->m_gate || !b->m_up || !b->m_hid || !b->m_out || + !b->m_shexp) { + gpu_batch_scratch_free(b); + return NULL; + } + b->ctx = ctx; + b->n_cap = n; + return b; +} + +/* Host staging for the batch path: 4 float regions sized against the + * largest batched read-back (the joint Q+gate projection, 2*attn_dim wide). + * r0: aqg read-back / embed + boundary staging / attn-out staging + * r1: q-only staging + roped-q read-back; r2: the persisted gate z + * r3: k|v read-back (2*kv_dim per position). */ +static int gpu_get_batch_host(pn_qwen35moe_gpu *g, + const pn_qwen35moe_config *cfg, int n) +{ + size_t per_pos = 2 * (size_t)cfg->head_dim * cfg->n_head; /* 2*attn_dim */ + if (per_pos < (size_t)cfg->conv_dim) + per_pos = (size_t)cfg->conv_dim; + if (g->bhost_cap >= n && g->bhost[0]) + return 0; + for (int i = 0; i < 4; i++) { + free(g->bhost[i]); + g->bhost[i] = malloc((size_t)n * per_pos * sizeof(float)); + if (!g->bhost[i]) + return -1; + } + g->bhost_cap = n; + return 0; +} + +/* Batched matvec dispatch: y[t][rows] = dequant(w) . x[t], t in [0, n). + * Returns -1 if the weight has no batched kernel (caller must abort the + * batch and fall back to the single-token path). */ +static int gpu_matvec_nx(const gpu_weight *w, fc_buffer *x, fc_buffer *y, + int n) +{ + if (!w->bytes) + return -1; + switch ((int)w->type) { + case GGUF_QUANT_Q3_K: + fc_matvec_q3k_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_Q4_K: + fc_matvec_q4k_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_Q5_K: + fc_matvec_q5k_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_Q6_K: + fc_matvec_q6k_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_Q8_0: + fc_matvec_q80_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_F32: + fc_matvec_f32_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_F16: + /* No batched host fallback exists (a -1 here fails the whole batched + * MoE block), so an absent kernel must refuse, not launch nothing. */ + if (!fc_matvec_f16_available_ctx(w->ctx)) + return -1; + fc_matvec_f16_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + case GGUF_QUANT_BF16: + if (!fc_matvec_bf16_available_ctx(w->ctx)) + return -1; + fc_matvec_bf16_nx_ctx(w->ctx, w->bytes, x, y, w->rows, w->cols, + w->row_stride, n); + return 0; + default: + return -1; + } +} + +/* Should this batched MoE call take the expert-union dedup gate+up? + * + * Five conditions, all required: + * + * 1. `allow_dedup` — the caller is a path where dedup is allowed at all + * (prefill; see gpu_moe_block_batch). This is the policy gate. + * 2. The flag is on. Default off. + * 3. The batch is wide enough to break even. The dedup kernel launches + * n_expert blocks per FF row-group no matter how few positions there are, + * while _nx launches n*k; below n*k == n_expert the dedup grid is strictly + * bigger AND the expert union is nearly as large as the application count, + * so it can only lose. n_expert/k is 32 positions for nex-n2-mini. This is + * a block-count derivation, not a measured optimum — the measured point is + * n = 128, the prefill chunk. + * 4. The scratch exists and the kernels are actually in the loaded PTX. The + * last check matters because a stale fc_cuda_ptx.h leaves the two dedup + * kernels NULL while the rest of the grouped path loads fine; without it + * the dedup wrapper would silently no-op and hand the down kernel an + * UNWRITTEN hid. + * 5. The gate/up experts are Q3_K. The dedup kernels decode Q3_K and ONLY + * Q3_K (fc_q3k_decode_sb) — there is no Q4_K twin, because dedup is an + * opt-in prefill speed lever, not a format the engine must support. A + * Q4_K_M model therefore keeps the batched gate+up on fc_moe_gateup_nx_ctx, + * which does dispatch on quant. Without this the flag would hand Q4_K bytes + * to a Q3_K decode — silently wrong output, the exact failure this whole + * change exists to remove. */ +static int gpu_moe_batch_use_dedup(fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int n, + int allow_dedup, int gateup_quant) +{ + if (!gpu_moe_dedup_enabled()) + return 0; + /* The decode override exists to measure the LOSING configuration, so it + * bypasses the width floor as well — otherwise an 8-slot step would fail + * condition 3 and the override would measure nothing. */ + int wide = n * cfg->n_expert_used >= cfg->n_expert || + gpu_moe_dedup_decode_override(); + int ready = b->m_ecount && b->m_ereq && fc_moe_dedup_available_ctx(ctx) && + gateup_quant == GGUF_QUANT_Q3_K; + int use = allow_dedup && wide && ready; + + /* Announce each DISTINCT verdict once. QWEN35_MOE_DEDUP=1 asked for a + * different kernel; silently running the old one would make an A/B measure + * nothing, and a missing-kernel fallback is exactly the case a caller needs + * told about. + * + * Keyed on (allow_dedup, use) rather than announced-once, because a single + * process reaches here from BOTH the prefill and the decode call sites and + * a once-only line reports whichever ran first. Verifying "the flag is on + * and decode still declined" needs to see the decode verdict, so all four + * combinations get one line each — four lines, worst case, per process. */ + static int announced[2][2]; + if (!announced[allow_dedup ? 1 : 0][use ? 1 : 0]) { + announced[allow_dedup ? 1 : 0][use ? 1 : 0] = 1; + fprintf(stderr, "[qwen35moe] MoE dedup gate+up: %s (n=%d k=%d " + "n_expert=%d, prefill_path=%d wide=%d kernels=%d " + "gateup_quant=%d)\n", + use ? "ON" : "OFF", n, cfg->n_expert_used, cfg->n_expert, + allow_dedup, wide, ready, gateup_quant); + } + return use; +} + +/* QWEN35_MOE_DEDUP_DIAG=1: read the per-expert request counts back and report + * the expert union for the first few layers, then stop. + * + * The dedup's whole saving is n*k / |union|, and that ratio has only ever been + * a binomial estimate. This measures it. Diagnostic only — it forces a host + * sync per call, so it is off by default and self-limits to LAYERS layers of + * the first chunk. */ +static void gpu_moe_dedup_report_union(fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int n) +{ + enum { LAYERS = 4 }; + static int diag = -1; + static int reported = 0; + if (diag < 0) { + const char *env = getenv("QWEN35_MOE_DEDUP_DIAG"); + diag = (env && env[0] == '1') ? 1 : 0; + } + if (!diag || reported >= LAYERS) + return; + reported++; + + int32_t *counts = malloc((size_t)cfg->n_expert * sizeof(int32_t)); + if (!counts) + return; + fc_sync_ctx(ctx); + fc_buffer_to_cpu_n(b->m_ecount, counts, + (size_t)cfg->n_expert * sizeof(int32_t)); + int unique = 0, maxr = 0, total = 0; + for (int e = 0; e < cfg->n_expert; e++) { + int r = counts[e]; + if (r <= 0) + continue; + unique++; + total += r; + if (r > maxr) + maxr = r; + } + /* tiles = the z-tiles the kernel actually does work in, summed over + * experts: this is how many times a weight stream is re-read, so + * applications/tiles is the REALISED decode saving, not the ideal one. */ + int tiles = 0; + for (int e = 0; e < cfg->n_expert; e++) + if (counts[e] > 0) + tiles += (counts[e] + FC_MOE_DEDUP_RT - 1) / FC_MOE_DEDUP_RT; + fprintf(stderr, "[qwen35moe] dedup union: n=%d apps=%d unique=%d/%d " + "maxR=%d meanR=%.2f tiles=%d ideal=%.2fx realised=%.2fx\n", + n, total, unique, cfg->n_expert, maxr, + unique ? (double)total / unique : 0.0, tiles, + unique ? (double)total / unique : 0.0, + tiles ? (double)total / tiles : 0.0); + free(counts); +} + +/* Batched shared-expert MoE: mirrors gpu_moe_block's UNFUSED sequence per + * position (the fused fx_pre/fx_post launches are bit-identical to that + * sequence, so this matches the single-token path either way). Requires + * the grouped expert layout. + * + * `allow_dedup` is the PREFILL DISCRIMINANT, and it is a parameter rather + * than an internal env read on purpose. This function has two callers with + * opposite economics: the prefill chunk loop (n = 128 positions, ~4x expert + * reuse — dedup wins) and gpu_moe_block_slots, the multi-user DECODE step + * (n = live slots, ~1.1x reuse — dedup measured 7-9% SLOWER and is what got + * the whole idea shelved). Reading QWEN35_MOE_DEDUP in here would flip both + * at once. The callers decide; only prefill passes 1. */ +static int gpu_moe_block_batch(fc_cuda_ctx *ctx, gpu_moe *d, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int n, int allow_dedup) +{ + const int n_embd = cfg->n_embd; + const int k = cfg->n_expert_used; + + if (!d->grouped) + return -1; + + double sp_t0 = 0.0; + sprof_begin(ctx, &sp_t0); + + /* Router logits + per-position softmax/top-k. */ + if (gpu_matvec_nx(&d->ffn_gate_inp, b->x_norm, b->m_logits, n) < 0) + return -1; + fc_moe_router_topk_nx_ctx(ctx, b->m_logits, b->m_tidx, b->m_twt, + cfg->n_expert, k, cfg->expert_weights_scale, n); + sprof_lap(SPROF_MOE_SHEXP, ctx, &sp_t0); + + /* Routed experts: grouped gate+up SwiGLU + down/accumulate, all + * positions in two launches — or three, when the gate+up runs over the + * expert union instead of over every (token, slot) pair. The dedup hid is + * bit-exact with the _nx hid, so the down kernel below is unchanged + * either way and the two paths are a clean A/B. */ + if (gpu_moe_batch_use_dedup(ctx, cfg, b, n, allow_dedup, + (int)d->gate_all.type)) { + fc_moe_dedup_build_ctx(ctx, b->m_tidx, b->m_ecount, b->m_ereq, + cfg->n_expert, k, n, n); + gpu_moe_dedup_report_union(ctx, cfg, b, n); + /* FC_MOE_DEDUP_COMPACT=1 (default off, and inert unless the PTX was + * built with -DFC_MOE_DEDUP_COMPACT=1) folds the (expert, request-tile) + * grid axes into one over a device-built work list: 22,528 blocks + * instead of 262,144, bit-exact. One extra tiny launch to compact + * ecount; ereq, hid and the down kernel below are unchanged. */ + int wmax = b->m_wlist + ? fc_moe_dedup_wmax(cfg->n_expert, k, n, n) : 0; + if (wmax > 0 && fc_moe_dedup_compact_enabled_ctx(ctx)) { + fc_moe_dedup_build_wl_ctx(ctx, b->m_ecount, b->m_wlist, + cfg->n_expert, wmax); + fc_moe_gateup_q3k_dedup_compact_ctx( + ctx, d->gate_all.bytes, d->up_all.bytes, b->x_norm, b->m_ereq, + b->m_wlist, b->m_hid, k, cfg->expert_ff, n_embd, + d->gate_all.row_stride, wmax, n); + } else { + fc_moe_gateup_q3k_dedup_ctx(ctx, d->gate_all.bytes, + d->up_all.bytes, b->x_norm, + b->m_ecount, b->m_ereq, b->m_hid, k, + cfg->expert_ff, n_embd, + d->gate_all.row_stride, cfg->n_expert, + n); + } + } else { + fc_moe_gateup_nx_ctx(ctx, (int)d->gate_all.type, d->gate_all.bytes, + d->up_all.bytes, b->x_norm, b->m_tidx, b->m_hid, + k, cfg->expert_ff, n_embd, + d->gate_all.row_stride, n); + } + sprof_lap(SPROF_MOE_GATEUP, ctx, &sp_t0); + fc_moe_down_accum_nx_ctx(ctx, (int)d->down_all.type, d->down_all.bytes, + b->m_hid, b->m_tidx, b->m_twt, b->m_out, k, + n_embd, cfg->expert_ff, + d->down_all.row_stride, n); + sprof_lap(SPROF_MOE_DOWN, ctx, &sp_t0); + + /* Shared expert + scalar gate + combine + residual. */ + if (gpu_matvec_nx(&d->ffn_gate_shexp, b->x_norm, b->m_gate, n) < 0 || + gpu_matvec_nx(&d->ffn_up_shexp, b->x_norm, b->m_up, n) < 0) + return -1; + fc_moe_silu_mul_ctx(ctx, b->m_gate, b->m_up, b->m_gate, + n * cfg->shared_ff); + if (gpu_matvec_nx(&d->ffn_down_shexp, b->m_gate, b->m_shexp, n) < 0) + return -1; + fc_moe_shexp_dot_gate_nx_ctx(ctx, d->ffn_gate_inp_shexp.buf, b->x_norm, + b->m_shexp, n_embd, n); + fc_add_ctx(ctx, b->m_out, b->m_shexp, b->block_out, n * n_embd); + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, n * n_embd); + sprof_lap(SPROF_MOE_SHEXP, ctx, &sp_t0); + return 0; +} + +/* MULTI-USER DECODE MoE: the batched grouped block when this layer HAS a + * grouped expert layout, else the single-token MoE run once per slot row. + * + * gpu_moe_block_batch is grouped-only: the merged-expert kernels exist for + * gate/up Q3_K|Q4_K with down Q3_K|Q4_K|Q6_K, and gpu_moe_setup clears `grouped` for any + * other expert quant (or a dense FFN). The single-token forward has always + * carried a per-expert fallback for exactly that case, so a model with an + * off-menu expert quant still decodes — it just loses the grouped kernels on + * those layers. The slots path had no such fallback: one non-grouped layer + * made gpu_moe_block_batch return -1, which aborted the WHOLE batched step at + * any slot count. (Qwen3.6-35B-A3B "saber": blk.0/blk.1 ffn_down_exps are + * Q5_K, so every batched decode step died on layer 0 even with one user.) + * + * The fallback re-runs the validated single-token gpu_moe_block over 1-row + * views of the batch scratch — identical math to what that user would get + * decoding alone, at the cost of the grouped kernels on those layers only. + * Slot rows are independent here (the MoE is position-invariant and carries no + * state), so row-by-row is exact, not an approximation. + * + * `sc` (per-die scratch) and `h` (host scratch) are only touched on the + * fallback; callers may pass NULL when the layer is grouped. + * + * Passes allow_dedup = 0: at decode-step slot counts the expert union is + * nearly as large as the application count, so the dedup gate+up costs more + * than it saves. Decode stays on the _nx path regardless of QWEN35_MOE_DEDUP; + * only the explicit QWEN35_MOE_DEDUP_DECODE measurement override lifts it. */ +static int gpu_moe_block_slots(fc_cuda_ctx *ctx, gpu_moe *d, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int n, + gpu_die_scratch *sc, gpu_host_scratch *h) +{ + if (d->grouped) + return gpu_moe_block_batch(ctx, d, cfg, b, n, + gpu_moe_dedup_decode_override()); + if (!sc || !h) { + fprintf(stderr, "[qwen35moe] slots MoE fallback needs die/host " + "scratch (non-grouped experts)\n"); + return -1; + } + + const size_t row_bytes = (size_t)cfg->n_embd * sizeof(float); + double sp_t0 = 0.0; + sprof_begin(ctx, &sp_t0); + for (int u = 0; u < n; u++) { + fc_buffer *x_norm = b->x_norm, *out_buf = b->block_out, + *hidden = b->hidden; + int rc; + if (u > 0) { + x_norm = fc_buffer_offset_view(b->x_norm, (size_t)u * row_bytes); + out_buf = fc_buffer_offset_view(b->block_out, + (size_t)u * row_bytes); + hidden = fc_buffer_offset_view(b->hidden, (size_t)u * row_bytes); + if (!x_norm || !out_buf || !hidden) { + fc_buffer_free_view(x_norm); + fc_buffer_free_view(out_buf); + fc_buffer_free_view(hidden); + return -1; + } + } + rc = gpu_moe_block(ctx, d, cfg, x_norm, out_buf, hidden, sc, h); + if (u > 0) { + fc_buffer_free_view(x_norm); + fc_buffer_free_view(out_buf); + fc_buffer_free_view(hidden); + } + if (rc < 0) + return rc; + } + sprof_lap(SPROF_MOE_ROWFB, ctx, &sp_t0); + return 0; +} + +/* Batched SSM (Gated-Delta-Net) block: projections/glue batched, conv1d + + * recurrence as position scans over the GPU-resident dual state. When spec + * is set, the scans additionally write per-position state snapshots into + * d->conv_snaps / d->ssm_snaps (allocated by the caller) for the partial- + * accept rollback; the math is unchanged either way. */ +static int gpu_ssm_block_batch(gpu_ssm_layer *d, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int n, int spec) +{ + const int conv_dim = cfg->conv_dim; + const int S = cfg->ssm_state; + const int n_k_heads = cfg->ssm_n_group; + const int n_v_heads = cfg->ssm_dt_rank; + const int kv_groups = n_v_heads / n_k_heads; + const int key_dim = S * n_k_heads; + const int val_dim = S * n_v_heads; + const float eps = cfg->rms_norm_eps; + fc_cuda_ctx *ctx = d->ctx; + + double sst0 = 0.0; + if (prof_sub_on()) { fc_sync_ctx(ctx); sst0 = prof_now(); } + + /* Four projections from x_norm (same unfused order as gpu_ssm_block). */ + if (gpu_matvec_nx(&d->attn_qkv, b->x_norm, b->s_qkv, n) < 0 || + gpu_matvec_nx(&d->attn_gate, b->x_norm, b->s_z, n) < 0 || + gpu_matvec_nx(&d->ssm_beta, b->x_norm, b->s_beta, n) < 0 || + gpu_matvec_nx(&d->ssm_alpha, b->x_norm, b->s_alpha, n) < 0) + return -1; + fc_sigmoid_ctx(ctx, b->s_beta, n * n_v_heads); + fc_ssm_decay_gate_nx_ctx(ctx, b->s_alpha, d->ssm_dt.buf, d->ssm_a.buf, + b->s_g, n_v_heads, n); + ssm_lap(SSMP_PROJ, ctx, &sst0); + + /* Conv1d + recurrence: scans over the n positions (state in place), + * with per-position snapshots in spec mode. */ + fc_ssm_conv1d_silu_seq_ctx(ctx, b->s_qkv, d->ssm_conv1d.buf, + d->conv_state_g, b->s_conv, conv_dim, + cfg->ssm_conv, n, + spec ? d->conv_snaps : NULL); + ssm_lap(SSMP_CONV, ctx, &sst0); + fc_ssm_qknorm_gqa_nx_ctx(ctx, b->s_conv, b->s_qg, b->s_kg, b->s_vg, + n_k_heads, kv_groups, S, key_dim, val_dim, + eps, n); + ssm_lap(SSMP_QKNORM, ctx, &sst0); + fc_ssm_recurrence_seq_ctx(ctx, d->ssm_state_g, b->s_qg, b->s_kg, b->s_vg, + b->s_g, b->s_beta, b->s_y, n_v_heads, S, n, + 1.0f / sqrtf((float)S), + spec ? d->ssm_snaps : NULL); + ssm_lap(SSMP_RECUR, ctx, &sst0); + + /* Gated norm (heads are uniform across positions, so the single-token + * kernel covers the batch with n*n_v_heads head-blocks) + out proj. */ + fc_ssm_gated_norm_ctx(ctx, b->s_y, d->ssm_norm.buf, b->s_z, b->s_gated, + n * n_v_heads, S, eps); + if (gpu_matvec_nx(&d->ssm_out, b->s_gated, b->block_out, n) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, n * cfg->n_embd); + ssm_lap(SSMP_OUT, ctx, &sst0); + return 0; +} + +/* ============================================================ + * ON-DIE FLASH ATTENTION — batched prefill + * + * The long-context lever: the host causal GQA softmax is O(N^2) on the + * 2-core K80 host (87.5% of an 8k prefill), so the batched path keeps a + * GPU-resident copy of each attention layer's KV ring cache and runs the + * whole attention block on the die (device q/z split -> QK-norm/RoPE -> + * KV append -> fc_attn_flash_prefill -> sigmoid gate). The host cache is + * still maintained as the mirror (a small D2H per call) so the single- + * token decode path and cross-path re-syncs stay correct. + * + * NOT bit-exact vs the host softmax: the flash kernel's online softmax + * and 16-lane dots reduce in a different order (~1e-6 relative drift, + * far below Q3_K noise). Gate: argmax-exact on the reference sequence + + * eval-harness A/B (QWEN35_FLASH=0 vs 1). QWEN35_FLASH=0 disables. + * ============================================================ */ + +/* QWEN35_FLASH gate (default ON; =0 keeps the host-softmax path). */ +static int flash_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_FLASH"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* StreamingLLM sliding-window attention gate. QWEN35_ATTN_WINDOW= is the + * recent-window size; 0/unset => full attention (the A/B baseline, a true + * no-op on both the flash and host-softmax paths). QWEN35_ATTN_SINK= is the + * attention-sink prefix (default 4). Parsed once, cached. Every attention + * layer (the 10 global i%4==3 layers) uses the same window. */ +static int attn_window(void) +{ + static int w = -1; + if (w < 0) { + const char *env = getenv("QWEN35_ATTN_WINDOW"); + long v = env ? strtol(env, NULL, 10) : 0; + w = (v > 0) ? (int)v : 0; + } + return w; +} + +static int attn_sink(void) +{ + static int s = -1; + if (s < 0) { + const char *env = getenv("QWEN35_ATTN_SINK"); + long v = env ? strtol(env, NULL, 10) : 4; + s = (v >= 0) ? (int)v : 4; + } + return s; +} + +/* ============================================================ + * Phase 10b (INV3) — lazy host KV mirror helpers + * + * In lazy mode (QWEN35_HOST_KV_MIRROR unset/0) the batched slots path stops + * writing the per-token host KV mirror: the GPU rings are authoritative for + * a bound session, rebinds are seeded device-to-device, and the mirror is + * materialized from a ring only when a real consumer needs host bytes + * (session export, sys-prefix capture, prefill yield, host-softmax + * fallback, ring realloc). kv_mirror_stale = 1 marks "the mirror is missing + * rows that live in a GPU ring". + * ============================================================ */ + +static int qw_kv_mirror_lazy(void) +{ + return !pn_qwen35moe_host_kv_mirror_eager(); +} + +static size_t qw_kv_row_bytes(const pn_qwen35moe_kv *kv) +{ + return kv->q4 ? (size_t)pn_kvq4_row_bytes(kv->head_dim) + : (size_t)kv->head_dim * sizeof(float); +} + +/* Per-slot fp16 sink / window shadow byte sizes (q4 mode). */ +static size_t qw_kv_sink_bytes(const pn_qwen35moe_kv *kv) +{ + return (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * kv->head_dim * + sizeof(uint16_t); +} + +static size_t qw_kv_win_bytes(const pn_qwen35moe_kv *kv) +{ + return (size_t)kv->n_head_kv * PN_KVQ4_WMAX * kv->head_dim * + sizeof(uint16_t); +} + +/* Materialize one layer's host KV mirror rows [0, rows) (plus the fp16 + * sink/window shadows) from whichever GPU ring currently holds this kv's + * content — the single-user ring or a decode slot ring, identified by the + * (pointer, generation) watermark. Blocking D2H on the layer's ctx; caller + * holds the GPU turn. Returns 0 and clears kv_mirror_stale on success, -1 + * if no ring holds the content (mirror left stale — the caller must treat + * the mirror as unusable, NOT read it). */ +static int qw_kv_layer_materialize(gpu_attn_layer *d, pn_qwen35moe_kv *kv, + int rows) +{ + const void *host_id = kv->q4 ? (const void *)kv->kq + : (const void *)kv->k; + size_t row_b = qw_kv_row_bytes(kv); + unsigned char *kdst = kv->q4 ? kv->kq : (unsigned char *)kv->k; + unsigned char *vdst = kv->q4 ? kv->vq : (unsigned char *)kv->v; + if (rows < 0) { + /* "As many rows as the source holds": the caller has no pos at hand + * (slot bind); take the matching ring's own watermark. */ + rows = 0; + if (d->k_cache_g && d->kv_host_id == host_id && + d->kv_gen == kv->content_gen && d->kv_synced_pos > rows) + rows = d->kv_synced_pos; + if (d->k_slots_g && d->kv_slots_cap_seq == kv->max_seq) + for (int su = 0; su < d->kv_slots_cap_n; su++) + if (d->kv_slot_host_id[su] == host_id && + d->kv_slot_gen[su] == kv->content_gen && + d->kv_slot_synced[su] > rows) + rows = d->kv_slot_synced[su]; + if (rows == 0) + return -1; /* no ring holds this content */ + } + if (rows > kv->max_seq) + rows = kv->max_seq; + if (rows <= 0) { + kv->kv_mirror_stale = 0; + return 0; + } + + /* Single-user ring (post-prefill / single-user decode). */ + if (d->k_cache_g && d->v_cache_g && d->cache_cap == kv->max_seq && + d->kv_host_id == host_id && d->kv_gen == kv->content_gen && + d->kv_synced_pos >= rows) { + for (int hd = 0; hd < kv->n_head_kv; hd++) { + size_t off = (size_t)hd * kv->max_seq * row_b; + fc_buffer_to_cpu_at(d->k_cache_g, kdst + off, off, + (size_t)rows * row_b); + fc_buffer_to_cpu_at(d->v_cache_g, vdst + off, off, + (size_t)rows * row_b); + } + if (kv->q4 && d->k_sink_g) { + fc_buffer_to_cpu(d->k_sink_g, kv->k_sink); + fc_buffer_to_cpu(d->v_sink_g, kv->v_sink); + fc_buffer_to_cpu(d->k_win_g, kv->k_win); + fc_buffer_to_cpu(d->v_win_g, kv->v_win); + } + kv->kv_mirror_stale = 0; + return 0; + } + + /* Decode slot ring (batched sessions). */ + if (d->k_slots_g && d->v_slots_g && d->kv_slots_cap_seq == kv->max_seq && + d->kv_slots_q4 == kv->q4) { + for (int su = 0; su < d->kv_slots_cap_n; su++) { + if (d->kv_slot_host_id[su] != host_id || + d->kv_slot_gen[su] != kv->content_gen || + d->kv_slot_synced[su] < rows) + continue; + for (int hd = 0; hd < kv->n_head_kv; hd++) { + size_t dst_off = (size_t)hd * kv->max_seq * row_b; + size_t src_off = ((size_t)su * kv->n_head_kv + hd) * + kv->max_seq * row_b; + fc_buffer_to_cpu_at(d->k_slots_g, kdst + dst_off, src_off, + (size_t)rows * row_b); + fc_buffer_to_cpu_at(d->v_slots_g, vdst + dst_off, src_off, + (size_t)rows * row_b); + } + if (kv->q4 && d->k_sink_slots_g) { + size_t sink_b = qw_kv_sink_bytes(kv); + size_t win_b = qw_kv_win_bytes(kv); + fc_buffer_to_cpu_at(d->k_sink_slots_g, kv->k_sink, + (size_t)su * sink_b, sink_b); + fc_buffer_to_cpu_at(d->v_sink_slots_g, kv->v_sink, + (size_t)su * sink_b, sink_b); + fc_buffer_to_cpu_at(d->k_win_slots_g, kv->k_win, + (size_t)su * win_b, win_b); + fc_buffer_to_cpu_at(d->v_win_slots_g, kv->v_win, + (size_t)su * win_b, win_b); + } + kv->kv_mirror_stale = 0; + return 0; + } + } + return -1; +} + +/* Ensure this layer's GPU KV ring cache exists and holds rows [0, pos0) + * of `kv`, uploading missing rows from the host mirror (contiguous per + * head) when the bound state changed or host-side appends ran past the + * GPU copy. Rows >= pos0 need no sync: this batch call appends them + * before any query reads them. Returns 0 on success. */ +static int gpu_attn_kv_cache_sync(gpu_attn_layer *d, + const pn_qwen35moe_kv *kv, int pos0) +{ + /* Per-position row size: q4 -> ggml-Q4_0 bytes, else fp32. The GPU cache + * mirrors the host cache byte-for-byte (host is quantized too under q4), so + * a re-upload is a plain byte copy in either mode. */ + const int q4 = kv->q4; + size_t row_b = q4 ? (size_t)pn_kvq4_row_bytes(kv->head_dim) + : (size_t)kv->head_dim * sizeof(float); + size_t head_b = (size_t)kv->max_seq * row_b; + const void *host_id = q4 ? (const void *)kv->kq : (const void *)kv->k; + + if (!d->k_cache_g || d->cache_cap < kv->max_seq) { + fc_buffer_free(d->k_cache_g); + fc_buffer_free(d->v_cache_g); + fc_buffer_free(d->k_sink_g); + fc_buffer_free(d->v_sink_g); + fc_buffer_free(d->k_win_g); + fc_buffer_free(d->v_win_g); + fc_buffer_free(d->k_win_snap_g); + fc_buffer_free(d->v_win_snap_g); + d->k_sink_g = d->v_sink_g = d->k_win_g = d->v_win_g = NULL; + d->k_win_snap_g = d->v_win_snap_g = NULL; + d->spec_win_cap = d->spec_win_n = 0; + size_t bytes = (size_t)kv->n_head_kv * head_b; + d->k_cache_g = fc_buffer_alloc_ctx(d->ctx, bytes); + d->v_cache_g = fc_buffer_alloc_ctx(d->ctx, bytes); + d->cache_cap = (d->k_cache_g && d->v_cache_g) ? kv->max_seq : 0; + if (q4 && d->cache_cap) { + /* fp16 sink + recent-window shadow, sized to the maxima (host and + * device rings are indexed identically). */ + size_t sink_b = (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * + kv->head_dim * sizeof(uint16_t); + size_t win_b = (size_t)kv->n_head_kv * PN_KVQ4_WMAX * + kv->head_dim * sizeof(uint16_t); + d->k_sink_g = fc_buffer_alloc_ctx(d->ctx, sink_b); + d->v_sink_g = fc_buffer_alloc_ctx(d->ctx, sink_b); + d->k_win_g = fc_buffer_alloc_ctx(d->ctx, win_b); + d->v_win_g = fc_buffer_alloc_ctx(d->ctx, win_b); + if (!d->k_sink_g || !d->v_sink_g || !d->k_win_g || !d->v_win_g) + d->cache_cap = 0; + } + d->kv_host_id = NULL; + if (!d->cache_cap) + return -1; + } + + /* A sequence RESTART (pos0 == 0) invalidates the whole GPU cache for this + * layer: on a REUSED state (same host_id — same kq buffer — but reset + + * re-prefilled) the incremental watermark and, critically, the fp16 + * q4 sink/window SHADOW still hold the PREVIOUS sequence's residue. The + * append below only (re)writes [pos0, pos0+n); the shadow re-sync only + * runs when synced < pos0, which is vacuously false at pos0 == 0 — so + * without this the restarted sequence attends stale sink/window K/V and + * its prefill logits diverge (observed: a reused serve slot's long-prompt + * completion differed from a fresh state). Drop the watermark so the + * block below force-re-syncs the shadow from the (reset-zeroed, then + * re-prefilled) host mirror. */ + if (pos0 == 0) { + d->kv_host_id = NULL; + d->kv_synced_pos = 0; + } + + int synced = (d->kv_host_id == host_id && + d->kv_gen == kv->content_gen) ? d->kv_synced_pos : 0; + if (synced > pos0) + synced = pos0; /* spec re-run: stale tail rows get re-appended */ + /* Phase 9.2: a speculative verify used to force `synced = 0` here and + * re-upload the whole [0, pos0) window from the host KV mirror on every + * call — 48 us per context position, ~90 ms of the verify's 110 ms at + * depth 1024, and outright BROKEN since Phase 10b/INV3 made that mirror + * lazy (it is stale, so the refusal below fired and verify returned -1). + * + * It is not needed. The committed frontier [0, pos0) in this ring is + * exactly as trustworthy for a verify as it is for the prefill and + * decode appends that wrote it: the watermark tracks it, the clamp above + * re-appends any tail past pos0, and rejected draft rows only ever sit at + * ring slots >= pos0 (attention at position p reads only [0, p], so they + * are never attended, and the next append overwrites them). What the old + * comment called "holes" is the one case the watermark really cannot see + * — rows committed into a DIFFERENT ring (a decode slot) — and that is + * handled by the D2D branch below, which now runs for spec too. + * + * The accepted rows a verify appends are folded into the watermark by + * pn_qwen35moe_spec_commit_cuda, which is what closes the loop. */ + int host_uploaded = 0; + if (synced < pos0) { + /* Phase 10b: prefer sourcing the missing rows on-die from a decode + * slot ring that already holds this content (the multi-turn resume + * case: the previous turn decoded in a slot ring; the delta prefill + * runs here in the single-user ring). D2D, no host round-trip. + * Slot rings only ever hold committed content — the batched decode + * path never speculates — so this is a valid source for a verify. */ + int d2d_done = 0; + if (d->k_slots_g && d->v_slots_g && + d->kv_slots_cap_seq == kv->max_seq && d->kv_slots_q4 == q4) { + for (int su = 0; su < d->kv_slots_cap_n; su++) { + if (d->kv_slot_host_id[su] != host_id || + d->kv_slot_gen[su] != kv->content_gen || + d->kv_slot_synced[su] < pos0) + continue; + for (int hd = 0; hd < kv->n_head_kv; hd++) { + size_t dst_off = (size_t)hd * head_b + + (size_t)synced * row_b; + size_t src_off = ((size_t)su * kv->n_head_kv + hd) * + head_b + (size_t)synced * row_b; + size_t bytes = (size_t)(pos0 - synced) * row_b; + fc_buffer_copy_d2d_ctx(d->ctx, d->k_cache_g, dst_off, + d->k_slots_g, src_off, bytes); + fc_buffer_copy_d2d_ctx(d->ctx, d->v_cache_g, dst_off, + d->v_slots_g, src_off, bytes); + } + if (q4 && d->k_sink_slots_g && d->k_sink_g) { + size_t sink_b = qw_kv_sink_bytes(kv); + size_t win_b = qw_kv_win_bytes(kv); + fc_buffer_copy_d2d_ctx(d->ctx, d->k_sink_g, 0, + d->k_sink_slots_g, + (size_t)su * sink_b, sink_b); + fc_buffer_copy_d2d_ctx(d->ctx, d->v_sink_g, 0, + d->v_sink_slots_g, + (size_t)su * sink_b, sink_b); + fc_buffer_copy_d2d_ctx(d->ctx, d->k_win_g, 0, + d->k_win_slots_g, + (size_t)su * win_b, win_b); + fc_buffer_copy_d2d_ctx(d->ctx, d->v_win_g, 0, + d->v_win_slots_g, + (size_t)su * win_b, win_b); + } + d2d_done = 1; + break; + } + } + if (!d2d_done) { + if (qw_kv_mirror_lazy() && kv->kv_mirror_stale) { + /* The mirror is missing rows and no ring holds them — every + * lazy-mode flow materializes before reaching here, so this + * is an invariant breach. Refuse to upload garbage. */ + fprintf(stderr, "[attn] kv_cache_sync: rows [%d,%d) needed, " + "host mirror STALE and no GPU ring source — refusing " + "stale upload\n", synced, pos0); + return -1; + } + const unsigned char *ksrc = q4 ? kv->kq + : (const unsigned char *)kv->k; + const unsigned char *vsrc = q4 ? kv->vq + : (const unsigned char *)kv->v; + for (int hd = 0; hd < kv->n_head_kv; hd++) { + size_t off_b = (size_t)hd * head_b + (size_t)synced * row_b; + size_t bytes = (size_t)(pos0 - synced) * row_b; + fc_buffer_from_cpu_at(d->k_cache_g, off_b, ksrc + off_b, bytes); + fc_buffer_from_cpu_at(d->v_cache_g, off_b, vsrc + off_b, bytes); + } + host_uploaded = 1; + } + } + /* Re-sync the fp16 q4 sink/window shadow from the host mirror on any + * non-incremental HOST entry: a host re-upload (the D2D branch above + * refreshed the shadow on-die already) OR a sequence RESTART (pos0 == 0 + * — a reused state whose shadow still holds the previous sequence's + * residue; the append below only rewrites the ring rows, never the + * shadow, and at pos0 == 0 the row-upload branch above is skipped). + * The incremental append path keeps the shadow live so this is cheap + * and rare (a few hundred KB/layer, once per sequence start). */ + if (q4 && (host_uploaded || pos0 == 0)) { + size_t sink_b = (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * + kv->head_dim * sizeof(uint16_t); + size_t win_b = (size_t)kv->n_head_kv * PN_KVQ4_WMAX * + kv->head_dim * sizeof(uint16_t); + fc_buffer_from_cpu_at(d->k_sink_g, 0, (const unsigned char *)kv->k_sink, sink_b); + fc_buffer_from_cpu_at(d->v_sink_g, 0, (const unsigned char *)kv->v_sink, sink_b); + fc_buffer_from_cpu_at(d->k_win_g, 0, (const unsigned char *)kv->k_win, win_b); + fc_buffer_from_cpu_at(d->v_win_g, 0, (const unsigned char *)kv->v_win, win_b); + } + d->kv_host_id = host_id; + d->kv_gen = kv->content_gen; + d->kv_synced_pos = pos0; + return 0; +} + +/* QWEN35_FLASH_DECODE gate (default ON; =0 keeps the host-softmax decode path; + * A/B baseline). Set to any value except "0" to route single-token decode + * attention through the on-die split flash kernel. */ +static int flash_decode_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_FLASH_DECODE"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* Target keys per decode split. QWEN35_FLASH_DECODE_KPS overrides the default + * for tuning / forcing the multi-split combine path in tests; clamped to the + * tile width so split_len rounding stays sane. */ +static int dec_keys_per_split(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_FLASH_DECODE_KPS"); + v = env ? atoi(env) : PN_QWEN35MOE_DEC_KEYS_PER_SPLIT; + if (v < 16) /* FC_ATTN_TILE_KEYS: split_len is rounded to this */ + v = 16; + } + return v; +} + +/* On-die decode flash attention: a single query (the new token at `pos`) + * attends the cached context [0, pos] entirely on the die. Mirrors + * gpu_attn_block_batch_flash with npos=1 but keeps everything in the per-die + * scratch and splits the KV sequence across blocks (fc_attn_flash_decode) so + * the die is not left near-idle. On success attn_out (sc->a_out) holds the + * sigmoid-gated attention output, ready for the shared output projection. + * Returns 0 = ran, 1 = unavailable (caller uses the host path), <0 = error. */ +static int gpu_attn_block_decode_flash(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + fc_buffer *x_norm, pn_qwen35moe_kv *kv, + int pos, gpu_die_scratch *sc, + gpu_host_scratch *h) +{ + fc_cuda_ctx *ctx = d->ctx; + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int attn_dim = n_head * head_dim; + const int max_seq = kv->max_seq; + const int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + + /* Availability: flag on, kernels present, no ring wrap in the window, and + * the q4 decode kernel present when the cache is quantized. */ + if (!flash_decode_on() || !fc_attn_flash_available_ctx(ctx) || + !fc_attn_flash_decode_available_ctx(ctx) || !sc->a_z || + pos + 1 > max_seq || + (kv->q4 && !fc_attn_flash_decode_q4_available_ctx(ctx))) + return 1; + + /* Split count from context length: ~KEYS_PER_SPLIT keys/block, capped so + * the partial scratch stays bounded. split_len is a multiple of the tile + * width so tile boundaries line up. */ + int kps = dec_keys_per_split(); + int n_splits = (seq_len + kps - 1) / kps; + if (n_splits < 1) + n_splits = 1; + if (n_splits > PN_QWEN35MOE_DEC_SPLIT_MAX) + n_splits = PN_QWEN35MOE_DEC_SPLIT_MAX; + int split_len = (seq_len + n_splits - 1) / n_splits; + split_len = (split_len + 15) & ~15; + /* split_len rounding can leave the tail splits empty; drop them so no + * fully-masked block is launched (combine ignores -inf partials anyway). */ + n_splits = (seq_len + split_len - 1) / split_len; + + /* Partial scratch (m, l, acc) on this die, allocated once to the cap. */ + if (!d->dec_acc_g || d->dec_splits_cap < PN_QWEN35MOE_DEC_SPLIT_MAX) { + fc_buffer_free(d->dec_m_g); + fc_buffer_free(d->dec_l_g); + fc_buffer_free(d->dec_acc_g); + size_t np = (size_t)n_head * PN_QWEN35MOE_DEC_SPLIT_MAX; + d->dec_m_g = fc_buffer_alloc_ctx(ctx, np * sizeof(float)); + d->dec_l_g = fc_buffer_alloc_ctx(ctx, np * sizeof(float)); + d->dec_acc_g = fc_buffer_alloc_ctx(ctx, + np * (size_t)head_dim * sizeof(float)); + d->dec_splits_cap = (d->dec_m_g && d->dec_l_g && d->dec_acc_g) + ? PN_QWEN35MOE_DEC_SPLIT_MAX : 0; + if (!d->dec_splits_cap) + return -1; + } + + /* 1. Q+gate / K / V projections (single position). */ + gpu_matvec(&d->attn_q, x_norm, sc->a_qg, h->a, h->b); + gpu_matvec(&d->attn_k, x_norm, sc->a_k, h->a, h->b); + gpu_matvec(&d->attn_v, x_norm, sc->a_v, h->a, h->b); + + /* 2.-4. Device q/gate split (z persists in a_z for the output gate), + * QK-RMSNorm, partial NEOX RoPE — all on the die. */ + fc_attn_qz_split_ctx(ctx, sc->a_qg, sc->a_q, sc->a_z, n_head, head_dim, 1); + fc_qk_rms_norm_ctx(ctx, sc->a_q, d->attn_q_norm.buf, n_head, head_dim, + cfg->rms_norm_eps); + fc_qk_rms_norm_ctx(ctx, sc->a_k, d->attn_k_norm.buf, n_head_kv, head_dim, + cfg->rms_norm_eps); + fc_rope_neox_partial_ctx(ctx, sc->a_q, n_head, head_dim, cfg->rope_dim, pos, + cfg->rope_theta); + fc_rope_neox_partial_ctx(ctx, sc->a_k, n_head_kv, head_dim, cfg->rope_dim, + pos, cfg->rope_theta); + + /* 5. Sync the GPU KV ring to rows [0, pos) (re-uploads any host-only + * appends), append this token's row, split flash over [0, pos]. */ + if (gpu_attn_kv_cache_sync(d, kv, pos) < 0) + return -1; + float scale = 1.0f / sqrtf((float)head_dim); + if (kv->q4) { + fc_attn_kv_append_q4_ctx(ctx, sc->a_k, sc->a_v, d->k_cache_g, + d->v_cache_g, d->k_sink_g, d->v_sink_g, + d->k_win_g, d->v_win_g, n_head_kv, head_dim, + max_seq, pos, 1, pn_kvq4_sink(), + pn_kvq4_win()); + d->kv_synced_pos = pos + 1; + if (fc_attn_flash_decode_q4_ctx(ctx, sc->a_q, d->k_cache_g, + d->v_cache_g, d->k_sink_g, d->v_sink_g, + d->k_win_g, d->v_win_g, d->dec_m_g, + d->dec_l_g, d->dec_acc_g, sc->a_out, + n_head, n_head_kv, head_dim, max_seq, + seq_len, n_splits, split_len, + pn_kvq4_sink(), pn_kvq4_win(), + attn_sink(), attn_window(), + scale) < 0) + return -1; + } else { + fc_attn_kv_append_ctx(ctx, sc->a_k, sc->a_v, d->k_cache_g, d->v_cache_g, + n_head_kv, head_dim, max_seq, pos, 1); + d->kv_synced_pos = pos + 1; + if (fc_attn_flash_decode_ctx(ctx, sc->a_q, d->k_cache_g, d->v_cache_g, + d->dec_m_g, d->dec_l_g, d->dec_acc_g, + sc->a_out, n_head, n_head_kv, head_dim, + max_seq, seq_len, n_splits, split_len, + attn_sink(), attn_window(), scale) < 0) + return -1; + } + + /* 6. Sigmoid output gate on the die: attn_out *= sigmoid(z). */ + fc_sigmoid_ctx(ctx, sc->a_z, attn_dim); + fc_mul_ctx(ctx, sc->a_out, sc->a_z, sc->a_out, attn_dim); + + /* 7. B2 — deferred KV host mirror: mark the host mirror stale instead of + * paying ~2 D2H/attn-layer per decode token. The GPU ring is the live + * source of truth; pn_qwen35moe_gpu_state_to_host() refreshes the host + * mirror on-demand when this session is preserved (session switch / LRU + * eviction), before the GPU KV buffers are reused for another session. */ + kv->kv_mirror_stale = 1; + d->flash_ran = 1; /* segment-graph gate: ring + dec_* scratch exist */ + return 0; +} + +/* ============================================================ + * Full-step segment graph (C1, QWEN35_GRAPH_FULL) + * + * One captured CUDA graph per die segment replays EVERY layer body — + * SSM and attention interleaved — with a single cuGraphLaunch per + * token (~300 launches -> 1 on a 20-layer segment). Made possible by + * B2 (no per-token KV-mirror D2H) plus the device-position kernels: + * the only per-token variance inside an attention body is the token + * position, so it is moved into a device int (sc->dec_pos, one 4-byte + * H2D per die per token, OUTSIDE the graph) and the RoPE / KV-append / + * split-flash / combine kernels read it from device memory, recomputing + * the split plan on-device with the exact host formula (bit-exact). + * + * What stays OUTSIDE the graph (host-synchronous or per-token-variant): + * - the token-embedding row fetch + residual-stream H2D seed + * - the 2-die boundary hand-off (sync + D2H + barswap + H2D) + * - the end-of-step LM head top-k logits fetch (D2H) + * - SSM state re-seed H2D on session switch; KV ring re-sync H2D on + * session resume (both no-ops in steady-state decode) + * - host bookkeeping (kv_synced_pos watermark, kv_mirror_stale) + * ============================================================ */ + +/* Graph-capturable attention-layer body: the exact launch sequence of + * gpu_attn_block's flash-decode path + out-proj + post-norm + MoE, with the + * three position-dependent host-arg kernels swapped for their device-pos + * variants (rope_pos, kv_append*_dev, flash_decode*_dev reading sc->dec_pos). + * No debug dumps / prof syncs — the segment graph only engages with both off. + * Caller has done the per-token host work (KV ring sync, dec_pos upload) and + * guarantees flash eligibility (no ring wrap, dec scratch + ring allocated, + * grouped MoE). */ +static int gpu_attn_layer_body_graph(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + fc_buffer *hidden, fc_buffer *x_norm, + fc_buffer *block_out, + pn_qwen35moe_kv *kv, + gpu_die_scratch *sc, gpu_host_scratch *h) +{ + fc_cuda_ctx *ctx = d->ctx; + const int c = cfg->n_embd; + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int attn_dim = n_head * head_dim; + const int max_seq = kv->max_seq; + const float eps = cfg->rms_norm_eps; + float scale = 1.0f / sqrtf((float)head_dim); + int rc; + + fc_rms_norm_ctx(ctx, hidden, d->attn_norm.buf, x_norm, c, eps); + + /* 1. Q+gate / K / V projections — same three launches as the direct + * flash path (NOT the fused fx_qkv: keep the launch sequence identical + * to the path the K80 hardware gate diffs against). */ + gpu_matvec(&d->attn_q, x_norm, sc->a_qg, h->a, h->b); + gpu_matvec(&d->attn_k, x_norm, sc->a_k, h->a, h->b); + gpu_matvec(&d->attn_v, x_norm, sc->a_v, h->a, h->b); + + /* 2.-4. Device q/gate split, QK-RMSNorm, RoPE at the DEVICE position. */ + fc_attn_qz_split_ctx(ctx, sc->a_qg, sc->a_q, sc->a_z, n_head, head_dim, 1); + fc_qk_rms_norm_ctx(ctx, sc->a_q, d->attn_q_norm.buf, n_head, head_dim, + eps); + fc_qk_rms_norm_ctx(ctx, sc->a_k, d->attn_k_norm.buf, n_head_kv, head_dim, + eps); + fc_rope_neox_partial_pos_ctx(ctx, sc->a_q, n_head, head_dim, cfg->rope_dim, + sc->dec_pos, cfg->rope_theta, 1); + fc_rope_neox_partial_pos_ctx(ctx, sc->a_k, n_head_kv, head_dim, + cfg->rope_dim, sc->dec_pos, cfg->rope_theta, + 1); + + /* 5. Ring append + split flash + combine, all at the device position. */ + if (kv->q4) { + fc_attn_kv_append_q4_dev_ctx(ctx, sc->a_k, sc->a_v, d->k_cache_g, + d->v_cache_g, d->k_sink_g, d->v_sink_g, + d->k_win_g, d->v_win_g, n_head_kv, + head_dim, max_seq, sc->dec_pos, + pn_kvq4_sink(), pn_kvq4_win()); + rc = fc_attn_flash_decode_q4_dev_ctx(ctx, sc->a_q, d->k_cache_g, + d->v_cache_g, d->k_sink_g, + d->v_sink_g, d->k_win_g, + d->v_win_g, d->dec_m_g, + d->dec_l_g, d->dec_acc_g, + sc->a_out, n_head, n_head_kv, + head_dim, max_seq, sc->dec_pos, + dec_keys_per_split(), + PN_QWEN35MOE_DEC_SPLIT_MAX, + pn_kvq4_sink(), pn_kvq4_win(), + attn_sink(), attn_window(), + scale); + } else { + fc_attn_kv_append_dev_ctx(ctx, sc->a_k, sc->a_v, d->k_cache_g, + d->v_cache_g, n_head_kv, head_dim, max_seq, + sc->dec_pos); + rc = fc_attn_flash_decode_dev_ctx(ctx, sc->a_q, d->k_cache_g, + d->v_cache_g, d->dec_m_g, + d->dec_l_g, d->dec_acc_g, sc->a_out, + n_head, n_head_kv, head_dim, + max_seq, sc->dec_pos, + dec_keys_per_split(), + PN_QWEN35MOE_DEC_SPLIT_MAX, + attn_sink(), attn_window(), scale); + } + if (rc < 0) + return -1; + + /* 6. Sigmoid output gate. */ + fc_sigmoid_ctx(ctx, sc->a_z, attn_dim); + fc_mul_ctx(ctx, sc->a_out, sc->a_z, sc->a_out, attn_dim); + + /* 7. Output projection (+residual), same fused/unfused choice as the + * direct path. */ + { + fc_fused_seg seg = gpu_seg_mv(&d->attn_output, block_out, 3); + seg.aux1 = hidden; + if (gpu_fused_run(ctx, &d->fx_out, &seg, 1, sc->a_out) != 0) { + gpu_matvec(&d->attn_output, sc->a_out, block_out, h->a, h->b); + fc_add_ctx(ctx, hidden, block_out, hidden, c); + } + } + + /* post_attention_norm + shared-MoE (+residual). */ + fc_rms_norm_ctx(ctx, hidden, d->post_attention_norm.buf, x_norm, c, eps); + return gpu_moe_block(ctx, &d->moe, cfg, x_norm, block_out, hidden, sc, h); +} + +static int gpu_segment_forward(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + fc_cuda_ctx *ctx, int seg_idx, int lo, int hi, + fc_buffer *hidden, fc_buffer *x_norm, + fc_buffer *block_out, pn_qwen35moe_state *s, + int pos, gpu_host_scratch *h) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + gpu_seg_graph *seg = &g->seg[seg_idx]; + int layer, rc; + + /* Global gates (all env-latched — constant across the process). */ + if (!gpu_graph_enabled() || !gpu_graph_full_enabled() || + !flash_decode_on() || prof_on() || qwen35moe_dbg_enabled()) { + pn_graph_capture_ineligible(&seg->capture, + !gpu_graph_enabled() ? PN_GRAPH_INELIGIBLE_GRAPH_OFF + : (prof_on() || qwen35moe_dbg_enabled()) + ? PN_GRAPH_INELIGIBLE_PROFILING + : PN_GRAPH_INELIGIBLE_SLOTS_OFF); + return 1; + } + if (pos < 1) + return 1; /* pos 0 runs direct: builds descriptors + scratch */ + /* A capture that FAILED gets bounded retries, not a permanent latch: the + * cooldown is consumed here, the one point this segment reaches once per + * token. The per-layer eligibility passes below can still refuse the step + * afterwards; that costs at most one cooldown tick and no state. */ + if (!seg->graph && !pn_graph_capture_should_attempt(&seg->capture)) + return 1; + + gpu_die_scratch *sc = gpu_get_scratch(g, ctx, cfg); + if (!sc) + return -1; + + /* ---- Pass 1: eligibility. Pure host checks, no device work, so a + * mid-loop refusal leaves nothing half-done. ---- */ + int q4 = -1, max_seq = 0; + for (layer = lo; layer < hi; layer++) { + int idx = pn_qwen35moe_model_layer_index(m, layer); + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + gpu_attn_layer *d = &g->attn[idx]; + pn_qwen35moe_kv *kv = &s->kv[idx]; + if (!d->moe.grouped && !cfg->dense_ffn) { + /* Permanent: expert quant decides `grouped` at upload, so this + * segment can never be captured on this model. */ + pn_graph_capture_ineligible(&seg->capture, + PN_GRAPH_INELIGIBLE_MOE_NOT_GROUPED); + return 1; + } + if (!d->body_ran || !d->flash_ran) + return 1; + if (pos + 1 > kv->max_seq) + return 1; /* ring wrap: the host-softmax path owns this */ + if (q4 < 0) { + q4 = kv->q4; + max_seq = kv->max_seq; + if (!fc_attn_decode_graph_available_ctx(ctx, q4)) + return 1; + } else if (kv->q4 != q4 || kv->max_seq != max_seq) { + return 1; /* mixed KV geometry — refuse, run direct */ + } + if (d->cache_cap < kv->max_seq) { + /* The direct path is about to REALLOCATE this layer's GPU + * ring — any captured graph holds the old addresses. Drop + * it and recapture once the new ring exists. */ + if (seg->graph) { + fc_graph_free(seg->graph); + seg->graph = NULL; + } + pn_graph_capture_dropped(&seg->capture); + return 1; + } + } else { + gpu_ssm_layer *d = &g->ssm[idx]; + if (!d->moe.grouped && !cfg->dense_ffn) { + pn_graph_capture_ineligible(&seg->capture, + PN_GRAPH_INELIGIBLE_MOE_NOT_GROUPED); + return 1; + } + if (!d->body_ran || !d->conv_state_g || !d->ssm_state_g) + return 1; + } + } + /* A replayed graph bakes in the KV geometry (max_seq strides, q4 kernel + * choice). Refuse replay when the live state disagrees; the graph stays + * for the geometry it was captured against. */ + if (seg->graph && q4 >= 0 && + (q4 != seg->cap_q4 || max_seq != seg->cap_max_seq)) + return 1; + + /* ---- Pass 2: per-token host pre-work (all OUTSIDE the graph). H2D + * copies are synchronous and the stream is idle between tokens (the + * logits fetch synced it), so nothing races the previous replay. ---- */ + for (layer = lo; layer < hi; layer++) { + int idx = pn_qwen35moe_model_layer_index(m, layer); + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + gpu_attn_layer *d = &g->attn[idx]; + /* Session resume / host-side appends: re-upload missing ring + * rows [synced, pos). Steady-state decode: watermark == pos, + * no copy. */ + if (gpu_attn_kv_cache_sync(d, &s->kv[idx], pos) < 0) + return 1; + } else { + gpu_ssm_layer *d = &g->ssm[idx]; + pn_qwen35moe_ssm_state *st = &s->ssm[idx]; + /* Session switch: re-seed the GPU dual state CONTENTS (the + * addresses the graph recorded never change). */ + if (d->host_state != (const void *)st) { + fc_buffer_from_cpu(d->conv_state_g, st->conv_state); + fc_buffer_from_cpu(d->ssm_state_g, st->ssm_state); + d->host_state = (const void *)st; + } + } + } + + /* The per-token device position — the ONLY per-token input the captured + * kernels read. */ + int32_t p32 = (int32_t)pos; + fc_buffer_from_cpu(sc->dec_pos, &p32); + + if (seg->graph) { + if (getenv("BONSAI_GRAPH_DBG")) { static int rc_[2]={0,0}; if(seg_idx<2 && (++rc_[seg_idx])%64==1) fprintf(stderr, "[graphdbg] REPLAY seg %d (count %d)\n", seg_idx, rc_[seg_idx]); } + if (fc_graph_launch(seg->graph) != 0) + return 1; /* direct path takes this token; retry next */ + goto post; + } + + /* ---- Capture lazily (mirrors the per-layer SSM capture): the recorded + * launches do NOT execute; on any failure the direct path takes this token + * and nothing was half-applied. ---- */ + if (fc_graph_begin_ctx(ctx) != 0) { + pn_graph_capture_failed(&seg->capture); + return 1; + } + rc = 0; + for (layer = lo; layer < hi && rc == 0; layer++) { + int idx = pn_qwen35moe_model_layer_index(m, layer); + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) + rc = gpu_attn_layer_body_graph(&g->attn[idx], cfg, hidden, x_norm, + block_out, &s->kv[idx], sc, h); + else + rc = gpu_ssm_layer_body(&g->ssm[idx], cfg, layer, hidden, x_norm, + block_out, sc, h); + } + seg->graph = fc_graph_end_ctx(ctx); + if (pn_graph_capture_inject_now(&seg->capture) && seg->graph) { + fc_graph_free(seg->graph); /* fault injection, test only */ + seg->graph = NULL; + } + if (rc != 0 || !seg->graph) { + if (seg->graph) { + fc_graph_free(seg->graph); + seg->graph = NULL; + } + pn_graph_capture_failed(&seg->capture); + return 1; + } + seg->cap_q4 = q4; + seg->cap_max_seq = max_seq; + if (getenv("BONSAI_GRAPH_DBG")) + fprintf(stderr, "[graphdbg] CAPTURED seg %d layers [%d,%d) dense=%d\n", seg_idx, lo, hi, cfg->dense_ffn); + if (fc_graph_launch(seg->graph) != 0) { + fc_graph_free(seg->graph); + seg->graph = NULL; + pn_graph_capture_failed(&seg->capture); + return 1; + } + pn_graph_capture_succeeded(&seg->capture); + /* This segment graph now owns every step in [lo,hi), so the per-layer SSM + * graph slots underneath it are out of play — drop them from the residency + * gauge until the segment graph is dropped and the per-layer path runs + * again. Without this a per-layer slot frozen mid-retry keeps reporting a + * deficit that nothing will ever work off: a healthy serve read + * `live 2 / total 32 / failed 30`. */ + for (layer = lo; layer < hi; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_ATTN) + pn_graph_capture_superseded( + &g->ssm[pn_qwen35moe_model_layer_index(m, layer)].graph_capture); + } + +post: + /* Host bookkeeping the graphed appends imply (the direct path does this + * inside gpu_attn_block_decode_flash). */ + for (layer = lo; layer < hi; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_ATTN) + continue; + int idx = pn_qwen35moe_model_layer_index(m, layer); + g->attn[idx].kv_synced_pos = pos + 1; + s->kv[idx].kv_mirror_stale = 1; /* B2: GPU ring is source of truth */ + } + return 0; +} + +/* ---- Phase 9.2: speculative-verify undo for the q4 recent-window ring ---- + * + * The fp16 window shadow is a ring of PN_KVQ4_WMAX slots addressed + * `pos % PN_KVQ4_WMAX`, and the flash kernel reads a key at position s through + * it whenever `s > cur_pos - W` for `W = pn_kvq4_win()`. Appending a row at + * position p therefore destroys the shadow of `p - WMAX`. For prefill and + * decode that is right (p - WMAX aged out of the W-wide window long ago). For + * a speculative verify a rejected draft row is never re-processed, so a slot it + * clobbered is never rewritten — if that slot were still inside somebody's + * window, every later query wanting it would read the wrong K/V, resyncing a + * token or two later and looking like a sampling wobble rather than a cache + * bug. + * + * WHEN THAT CAN HAPPEN — and at the depths we run, it cannot. A verify block + * spans [pos0, pos0+n); the oldest position anything can still want from the + * shadow is > pos0 - W, and the newest eviction the block causes is + * pos0 + n - 1 - WMAX. So the block is harmless iff + * + * n <= WMAX - W + 1 (129 rows at the default W = 128) + * + * and verify depths are D <= 16. This machinery was written when the ring + * lapped at W instead of WMAX, where the bound was `n <= 1` — i.e. always + * unsafe. Lapping at WMAX (foundry 9353d0b) + * removed the hazard; qw_spec_win_needed now returns 0 in that regime and the + * snapshot is skipped entirely. It is kept, correct, for the out-of-envelope + * case rather than deleted, because the bound is a property of two tunables + * (QWEN35_KV_Q4_WIN and the caller's D) and not of the code. + * + * Restoring from the q4 body is not an option — that is the coarser copy the + * shadow exists to avoid, so it would break bit-exactness. Instead the verify + * snapshots the slots it is about to touch, and spec_commit copies back the + * ones belonging to rejected rows. + * + * A block that sits entirely inside the first ring lap has no prior occupant, + * so it is skipped outright. + */ +static int qw_spec_win_needed(const pn_qwen35moe_kv *kv, int pos0, int n) +{ + int win = pn_kvq4_win(); + if (!kv->q4 || win <= 0 || pos0 + n <= PN_KVQ4_WMAX) + return 0; /* no q4 shadow, or nothing to evict */ + if (n <= PN_KVQ4_WMAX - win + 1) + return 0; /* every eviction is outside every + * live W-wide window — see above */ + return 1; +} + +/* Copy rows [t0, t1) of the verify block between the live ring and the undo + * buffer. The slots are consecutive modulo the ring, so this is one or two + * runs per KV head, not one copy per row. */ +static void qw_spec_win_copy(gpu_attn_layer *d, const pn_qwen35moe_kv *kv, + int pos0, int t0, int t1, int to_snap) +{ + const size_t row_b = (size_t)kv->head_dim * sizeof(uint16_t); + for (int h = 0; h < kv->n_head_kv; h++) { + int t = t0; + while (t < t1) { + int slot = (pos0 + t) % PN_KVQ4_WMAX; + int run = PN_KVQ4_WMAX - slot; + if (run > t1 - t) run = t1 - t; + size_t ring_off = ((size_t)h * PN_KVQ4_WMAX + (size_t)slot) * row_b; + size_t snap_off = ((size_t)h * (size_t)d->spec_win_cap + + (size_t)t) * row_b; + size_t bytes = (size_t)run * row_b; + if (to_snap) { + fc_buffer_copy_d2d_ctx(d->ctx, d->k_win_snap_g, snap_off, + d->k_win_g, ring_off, bytes); + fc_buffer_copy_d2d_ctx(d->ctx, d->v_win_snap_g, snap_off, + d->v_win_g, ring_off, bytes); + } else { + fc_buffer_copy_d2d_ctx(d->ctx, d->k_win_g, ring_off, + d->k_win_snap_g, snap_off, bytes); + fc_buffer_copy_d2d_ctx(d->ctx, d->v_win_g, ring_off, + d->v_win_snap_g, snap_off, bytes); + } + t += run; + } + } +} + +/* Called from the batched flash path before a SPECULATIVE append. */ +static int qw_spec_win_snapshot(gpu_attn_layer *d, const pn_qwen35moe_kv *kv, + int pos0, int n) +{ + d->spec_win_n = 0; + if (!qw_spec_win_needed(kv, pos0, n) || !d->k_win_g || !d->v_win_g) + return 0; + if (d->spec_win_cap < n) { + fc_buffer_free(d->k_win_snap_g); + fc_buffer_free(d->v_win_snap_g); + size_t bytes = (size_t)kv->n_head_kv * (size_t)n * + (size_t)kv->head_dim * sizeof(uint16_t); + d->k_win_snap_g = fc_buffer_alloc_ctx(d->ctx, bytes); + d->v_win_snap_g = fc_buffer_alloc_ctx(d->ctx, bytes); + d->spec_win_cap = (d->k_win_snap_g && d->v_win_snap_g) ? n : 0; + if (!d->spec_win_cap) + return -1; + } + qw_spec_win_copy(d, kv, pos0, 0, n, 1); + d->spec_win_pos0 = pos0; + d->spec_win_n = n; + return 0; +} + +/* Called from spec_commit for a PARTIAL accept: put back the window slots the + * rejected rows [n_accept, n) evicted. */ +static void qw_spec_win_restore(gpu_attn_layer *d, const pn_qwen35moe_kv *kv, + int pos0, int n, int n_accept) +{ + if (d->spec_win_n != n || d->spec_win_pos0 != pos0 || n_accept >= n) + return; + if (!qw_spec_win_needed(kv, pos0, n)) + return; + qw_spec_win_copy(d, kv, pos0, n_accept, n, 0); + d->spec_win_n = 0; +} + +/* Batched full-attention block, ON-DIE flash path (see section header). + * Returns 0 on success, 1 if flash cannot run for this call (kernels + * missing / disabled / ring wrap — caller uses the host path), -1 on + * error. */ +static int gpu_attn_block_batch_flash(pn_qwen35moe_gpu *g, gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + pn_qwen35moe_kv *kv, int pos0, + gpu_batch_scratch *b, int n, int spec) +{ + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int attn_dim = n_head * head_dim; + const int kv_dim = n_head_kv * head_dim; + fc_cuda_ctx *ctx = d->ctx; + + if (!flash_on() || !fc_attn_flash_available_ctx(ctx) || !b->a_z || + pos0 + n > kv->max_seq || + (kv->q4 && !fc_attn_flash_q4_available_ctx(ctx))) + return 1; + + double bt0 = prof_on() ? prof_now() : 0.0; + + /* 1. Joint Q+gate, K, V projections for all positions. */ + if (gpu_matvec_nx(&d->attn_q, b->x_norm, b->a_qg, n) < 0 || + gpu_matvec_nx(&d->attn_k, b->x_norm, b->a_k, n) < 0 || + gpu_matvec_nx(&d->attn_v, b->x_norm, b->a_v, n) < 0) + return -1; + bprof_lap(BPROF_ATTN_MV, ctx, &bt0); + + /* 2.-4. Device q/gate split (z persists in a_z for the output gate), + * QK-RMSNorm, batched partial NEOX RoPE — same math as the host path, + * minus the split's D2H/H2D round trip. */ + fc_attn_qz_split_ctx(ctx, b->a_qg, b->a_q, b->a_z, n_head, head_dim, n); + fc_qk_rms_norm_ctx(ctx, b->a_q, d->attn_q_norm.buf, n * n_head, head_dim, + cfg->rms_norm_eps); + fc_qk_rms_norm_ctx(ctx, b->a_k, d->attn_k_norm.buf, n * n_head_kv, + head_dim, cfg->rms_norm_eps); + fc_rope_neox_partial_nx_ctx(ctx, b->a_q, n_head, head_dim, cfg->rope_dim, + pos0, cfg->rope_theta, n); + fc_rope_neox_partial_nx_ctx(ctx, b->a_k, n_head_kv, head_dim, + cfg->rope_dim, pos0, cfg->rope_theta, n); + bprof_lap(BPROF_ATTN_ROPE, ctx, &bt0); + + /* 5. On-die attention: sync the GPU KV ring to pos0, append the n new + * rows, causal GQA flash kernel, sigmoid output gate. */ + if (gpu_attn_kv_cache_sync(d, kv, pos0) < 0) + return -1; + /* A verify's rows may be rejected, and the q4 window append is destructive + * one ring-lap back (see k_win_snap_g) — snapshot what it is about to + * overwrite so spec_commit can undo the rejected tail. */ + if (spec && qw_spec_win_snapshot(d, kv, pos0, n) < 0) + return -1; + if (kv->q4) + fc_attn_kv_append_q4_ctx(ctx, b->a_k, b->a_v, d->k_cache_g, + d->v_cache_g, d->k_sink_g, d->v_sink_g, + d->k_win_g, d->v_win_g, n_head_kv, head_dim, + kv->max_seq, pos0, n, pn_kvq4_sink(), + pn_kvq4_win()); + else + fc_attn_kv_append_ctx(ctx, b->a_k, b->a_v, d->k_cache_g, d->v_cache_g, + n_head_kv, head_dim, kv->max_seq, pos0, n); + /* A speculative verify appends n DRAFT rows to the GPU ring, but a partial + * accept keeps only n_accept of them, so the watermark stays at the + * committed frontier until the caller says how many landed — + * pn_qwen35moe_spec_commit_cuda advances it by n_accept. A prefill append + * is durable, so it advances by n immediately. */ + d->kv_synced_pos = spec ? pos0 : pos0 + n; + if (kv->q4) { + if (fc_attn_flash_prefill_q4_ctx(ctx, b->a_q, d->k_cache_g, + d->v_cache_g, d->k_sink_g, d->v_sink_g, + d->k_win_g, d->v_win_g, b->a_out, n_head, + n_head_kv, head_dim, kv->max_seq, + pos0, n, pn_kvq4_sink(), pn_kvq4_win(), + attn_sink(), attn_window(), + 1.0f / sqrtf((float)head_dim)) < 0) + return -1; + } else if (fc_attn_flash_prefill_ctx(ctx, b->a_q, d->k_cache_g, + d->v_cache_g, b->a_out, n_head, + n_head_kv, head_dim, kv->max_seq, + pos0, n, attn_sink(), attn_window(), + 1.0f / sqrtf((float)head_dim)) < 0) + return -1; + fc_sigmoid_ctx(ctx, b->a_z, n * attn_dim); + fc_mul_ctx(ctx, b->a_out, b->a_z, b->a_out, n * attn_dim); + bprof_lap(BPROF_ATTN_FLASH, ctx, &bt0); + + /* 6. Host KV mirror (session-resume source of truth): D2H the roped K/V + * rows and append them to the host ring cache. This is batched prefill — + * once per context, not per decode token. After this write the mirror is + * current; decode tokens deferred by B2 will set kv_mirror_stale again. + * + * Phase 10b (lazy mirror): skip the host store entirely — the ring this + * call just appended to is authoritative, downstream consumers (slot + * bind, delta prefill) seed device-to-device, and anything that truly + * needs host bytes materializes them via qw_kv_layer_materialize. Mark + * the mirror stale so no one trusts it. Also skips the fc_sync_ctx: + * step 7 is device work on the same stream. + * + * Phase 9.2: this also covers the speculative verify, which used to be + * excluded (`&& !spec`) because the old verify path treated the host + * mirror as authoritative. It no longer does — the ring is — and the + * exclusion was actively wrong under the lazy mirror: it made a verify + * D2H its DRAFT rows into the mirror and then clear kv_mirror_stale, + * claiming a mirror whose [0, pos0) rows had never been written. + */ + if (qw_kv_mirror_lazy()) { + kv->kv_mirror_stale = 1; + bprof_lap(BPROF_ATTN_XFER, ctx, &bt0); + if (gpu_matvec_nx(&d->attn_output, b->a_out, b->block_out, n) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, n * cfg->n_embd); + bprof_lap(BPROF_ATTN_MV, ctx, &bt0); + return 0; + } + fc_sync_ctx(ctx); + float *hb_kv = g->bhost[3]; + fc_buffer_to_cpu_n(b->a_k, hb_kv, (size_t)n * kv_dim * sizeof(float)); + fc_buffer_to_cpu_n(b->a_v, hb_kv + (size_t)n * kv_dim, + (size_t)n * kv_dim * sizeof(float)); + const int kv_sink = kv->q4 ? pn_kvq4_sink() : 0; + const int kv_win = kv->q4 ? pn_kvq4_win() : 0; + for (int t = 0; t < n; t++) { + int cache_pos = (pos0 + t) % kv->max_seq; + const float *kvec = hb_kv + (size_t)t * kv_dim; + const float *vvec = hb_kv + (size_t)(n + t) * kv_dim; + for (int hd = 0; hd < n_head_kv; hd++) { + if (kv->q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, hd, + kv->max_seq, cache_pos, + kvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, hd, + kv->max_seq, cache_pos, + vvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + } else { + memcpy(kv->k + ((size_t)hd * kv->max_seq + cache_pos) * head_dim, + kvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + memcpy(kv->v + ((size_t)hd * kv->max_seq + cache_pos) * head_dim, + vvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + } + } + } + /* B2 — host mirror is now current through pos0+n-1; clear the stale flag. */ + kv->kv_mirror_stale = 0; + bprof_lap(BPROF_ATTN_XFER, ctx, &bt0); + + /* 7. Output projection + residual. */ + if (gpu_matvec_nx(&d->attn_output, b->a_out, b->block_out, n) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, n * cfg->n_embd); + bprof_lap(BPROF_ATTN_MV, ctx, &bt0); + return 0; +} + +/* Batched full-attention block, HOST-softmax fallback: batched + * projections/norm/rope, then the host-side KV append + causal GQA softmax + * per position (the same host code as gpu_attn_block, position by + * position). Used when flash is disabled/unavailable or the window wraps + * the ring. */ +static int gpu_attn_block_batch(pn_qwen35moe_gpu *g, gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + pn_qwen35moe_kv *kv, + int pos0, gpu_batch_scratch *b, int n) +{ + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int n_kv_groups = n_head / n_head_kv; + const int q_stride = head_dim * 2; + const int attn_dim = n_head * head_dim; + const int kv_dim = n_head_kv * head_dim; + const float eps = cfg->rms_norm_eps; + const int max_seq = kv->max_seq; + float *k_cache = kv->k; /* fp32 mode; NULL under q4 */ + float *v_cache = kv->v; + const int kv_q4 = kv->q4; + const int kv_sink = kv_q4 ? pn_kvq4_sink() : 0; + const int kv_win = kv_q4 ? pn_kvq4_win() : 0; + fc_cuda_ctx *ctx = d->ctx; + /* Phase 10b (lazy mirror): this host-softmax prefill fallback reads the + * host KV mirror [0, pos0+t). If earlier chunks went through the flash + * path with the lazy mirror, pull the rows down from the GPU ring + * first; this path keeps the mirror current itself from here on. */ + if (kv->kv_mirror_stale && + qw_kv_layer_materialize(d, kv, + (pos0 < max_seq) ? pos0 : max_seq) != 0) { + fprintf(stderr, "[attn] host batch attn: mirror stale with no ring " + "source (pos0=%d) — failing chunk\n", pos0); + return -1; + } + float *hb_qg = g->bhost[0]; + float *hb_q = g->bhost[1]; + float *hb_z = g->bhost[2]; + float *hb_kv = g->bhost[3]; + double bt0 = prof_on() ? prof_now() : 0.0; + + /* 1. Joint Q+gate, K, V projections for all positions. */ + if (gpu_matvec_nx(&d->attn_q, b->x_norm, b->a_qg, n) < 0 || + gpu_matvec_nx(&d->attn_k, b->x_norm, b->a_k, n) < 0 || + gpu_matvec_nx(&d->attn_v, b->x_norm, b->a_v, n) < 0) + return -1; + bprof_lap(BPROF_ATTN_MV, ctx, &bt0); + + /* Q/gate split, host-side per position (same glue as the single-token + * path; zgate persists in hb_z for the output gate below). */ + fc_sync_ctx(ctx); + fc_buffer_to_cpu_n(b->a_qg, hb_qg, + (size_t)n * 2 * attn_dim * sizeof(float)); + for (int t = 0; t < n; t++) { + const float *src_t = hb_qg + (size_t)t * 2 * attn_dim; + float *q_t = hb_q + (size_t)t * attn_dim; + float *z_t = hb_z + (size_t)t * attn_dim; + for (int hd = 0; hd < n_head; hd++) { + memcpy(q_t + (size_t)hd * head_dim, + src_t + (size_t)hd * q_stride, + (size_t)head_dim * sizeof(float)); + memcpy(z_t + (size_t)hd * head_dim, + src_t + (size_t)hd * q_stride + head_dim, + (size_t)head_dim * sizeof(float)); + } + } + fc_buffer_from_cpu_n(b->a_q, hb_q, (size_t)n * attn_dim * sizeof(float)); + bprof_lap(BPROF_ATTN_SPLIT, ctx, &bt0); + + /* 2.-4. QK-RMSNorm (heads uniform across positions -> single-token + * kernel with n x the head count) + batched partial NEOX RoPE. */ + fc_qk_rms_norm_ctx(ctx, b->a_q, d->attn_q_norm.buf, n * n_head, head_dim, + eps); + fc_qk_rms_norm_ctx(ctx, b->a_k, d->attn_k_norm.buf, n * n_head_kv, + head_dim, eps); + fc_rope_neox_partial_nx_ctx(ctx, b->a_q, n_head, head_dim, cfg->rope_dim, + pos0, cfg->rope_theta, n); + fc_rope_neox_partial_nx_ctx(ctx, b->a_k, n_head_kv, head_dim, + cfg->rope_dim, pos0, cfg->rope_theta, n); + + /* 5.+6. Host: append every position's K/V to the cache, then the causal + * GQA softmax + sigmoid output gate per position (identical code to + * gpu_attn_block, looped). aout accumulates in hb_qg (done with it). */ + bprof_lap(BPROF_ATTN_ROPE, ctx, &bt0); + fc_sync_ctx(ctx); + fc_buffer_to_cpu_n(b->a_q, hb_q, (size_t)n * attn_dim * sizeof(float)); + fc_buffer_to_cpu_n(b->a_k, hb_kv, (size_t)n * kv_dim * sizeof(float)); + fc_buffer_to_cpu_n(b->a_v, hb_kv + (size_t)n * kv_dim, + (size_t)n * kv_dim * sizeof(float)); + bprof_lap(BPROF_ATTN_XFER, ctx, &bt0); + { + float *aout_all = hb_qg; + float scale = 1.0f / sqrtf((float)head_dim); + /* heap scratch: max_seq can reach the 256K cap (too large for stack). */ + float *scores = (float *)malloc((size_t)max_seq * sizeof(float)); + float krow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + float vrow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + if (!scores) return -1; + for (int t = 0; t < n; t++) { + int pos = pos0 + t; + int cache_pos = pos % max_seq; + const float *kvec = hb_kv + (size_t)t * kv_dim; + const float *vvec = hb_kv + (size_t)(n + t) * kv_dim; + for (int hd = 0; hd < n_head_kv; hd++) { + if (kv_q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, hd, + max_seq, cache_pos, + kvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, hd, + max_seq, cache_pos, + vvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + } else { + memcpy(k_cache + ((size_t)hd * max_seq + cache_pos) * head_dim, + kvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + memcpy(v_cache + ((size_t)hd * max_seq + cache_pos) * head_dim, + vvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + } + } + + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + const float *qhost = hb_q + (size_t)t * attn_dim; + float *aout = aout_all + (size_t)t * attn_dim; + for (int qh = 0; qh < n_head; qh++) { + int kv_h = qh / n_kv_groups; + const float *q_vec = qhost + (size_t)qh * head_dim; + const float *k_base = kv_q4 ? NULL : + k_cache + (size_t)kv_h * max_seq * head_dim; + const float *v_base = kv_q4 ? NULL : + v_cache + (size_t)kv_h * max_seq * head_dim; + float *out_vec = aout + (size_t)qh * head_dim; + /* Sliding window (QWEN35_ATTN_WINDOW): attend key sp iff + * sp < aw_sink OR sp > pos - aw_win. Masked keys get score + * -inf (exp -> 0), so the softmax is over the windowed set. + * aw_win <= 0 attends all (full-attention baseline). */ + const int aw_win = attn_window(); + const int aw_sink = attn_sink(); + for (int sp = 0; sp < seq_len; sp++) { + if (aw_win > 0 && sp >= aw_sink && sp <= pos - aw_win) { + scores[sp] = -INFINITY; + continue; + } + const float *k_vec; + if (kv_q4) { + pn_kvq4_load_row(kv->kq, kv->k_sink, kv->k_win, kv_h, + max_seq, sp, pos, krow, head_dim, + kv_sink, kv_win); + k_vec = krow; + } else { + k_vec = k_base + (size_t)sp * head_dim; + } + scores[sp] = pn_dot_f32(q_vec, k_vec, head_dim) * scale; + } + float mx = -INFINITY; + for (int sp = 0; sp < seq_len; sp++) + if (scores[sp] > mx) mx = scores[sp]; + float sum_exp = 0.0f; + for (int sp = 0; sp < seq_len; sp++) { + scores[sp] = expf(scores[sp] - mx); + sum_exp += scores[sp]; + } + float inv_sum = 1.0f / sum_exp; + memset(out_vec, 0, (size_t)head_dim * sizeof(float)); + for (int sp = 0; sp < seq_len; sp++) { + /* Masked keys contributed exp()=0 above; skip their V. */ + if (aw_win > 0 && sp >= aw_sink && sp <= pos - aw_win) + continue; + float w_s = scores[sp] * inv_sum; + const float *v_vec; + if (kv_q4) { + pn_kvq4_load_row(kv->vq, kv->v_sink, kv->v_win, kv_h, + max_seq, sp, pos, vrow, head_dim, + kv_sink, kv_win); + v_vec = vrow; + } else { + v_vec = v_base + (size_t)sp * head_dim; + } + for (int dd = 0; dd < head_dim; dd++) + out_vec[dd] += w_s * v_vec[dd]; + } + } + + const float *z_t = hb_z + (size_t)t * attn_dim; + for (int i = 0; i < attn_dim; i++) { + float gv = 1.0f / (1.0f + expf(-z_t[i])); + aout[i] *= gv; + } + } + free(scores); + bprof_lap(BPROF_ATTN_HOST, ctx, &bt0); + fc_buffer_from_cpu_n(b->a_out, aout_all, + (size_t)n * attn_dim * sizeof(float)); + bprof_lap(BPROF_ATTN_XFER, ctx, &bt0); + } + + /* 7. Output projection + residual. */ + if (gpu_matvec_nx(&d->attn_output, b->a_out, b->block_out, n) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, n * cfg->n_embd); + bprof_lap(BPROF_ATTN_MV, ctx, &bt0); + return 0; +} + +/* One layer of the batched forward: attn_norm -> (SSM scan | attn) -> + * +residual -> post_attention_norm -> shared-MoE -> +residual, all over the + * [n][n_embd] residual stream in b->hidden. */ +static int gpu_layer_forward_batch(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + fc_cuda_ctx *ctx, int layer, + gpu_batch_scratch *b, + pn_qwen35moe_state *s, int pos0, int n, + int spec) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + int idx = pn_qwen35moe_model_layer_index(m, layer); + gpu_moe *moe; + int rc; + + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + gpu_attn_layer *d = &g->attn[idx]; + pn_qwen35moe_kv *kv = &s->kv[idx]; + fc_rms_norm_nx_ctx(ctx, b->hidden, d->attn_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, n); + rc = gpu_attn_block_batch_flash(g, d, cfg, kv, pos0, b, n, spec); + if (rc > 0) { /* flash unavailable for this call — host softmax */ + /* ...except under a speculative verify. The host fallback appends + * into the HOST q4 window ring, which has the same one-lap-back + * eviction problem (see qw_spec_win_snapshot) and no undo buffer, + * so a rejected draft would silently corrupt a live shadow slot. + * Fail the verify instead; every caller falls back to a plain + * forward, which is correct and only marginally slower. */ + if (spec) { + fprintf(stderr, "[attn] spec verify: flash unavailable, " + "refusing the host-softmax fallback\n"); + return -1; + } + rc = gpu_attn_block_batch(g, d, cfg, kv, pos0, b, n); + } + if (rc < 0) + return rc; + fc_rms_norm_nx_ctx(ctx, b->hidden, d->post_attention_norm.buf, + b->x_norm, c, cfg->rms_norm_eps, n); + moe = &d->moe; + } else { + gpu_ssm_layer *d = &g->ssm[idx]; + pn_qwen35moe_ssm_state *st = &s->ssm[idx]; + + /* GPU-resident dual state, seeded exactly like the single-token + * path (fresh buffers, a state swap, or a restart at pos 0). */ + size_t conv_n = (size_t)st->conv_minus_1 * st->conv_dim; + size_t ssm_n = (size_t)st->state_size * st->state_size * st->n_v_heads; + if (!d->conv_state_g) + d->conv_state_g = fc_buffer_alloc_ctx(ctx, conv_n * sizeof(float)); + if (!d->ssm_state_g) + d->ssm_state_g = fc_buffer_alloc_ctx(ctx, ssm_n * sizeof(float)); + if (!d->conv_state_g || !d->ssm_state_g) + return -1; + if (d->host_state != (const void *)st || pos0 == 0) { + fc_buffer_from_cpu(d->conv_state_g, st->conv_state); + fc_buffer_from_cpu(d->ssm_state_g, st->ssm_state); + d->host_state = (const void *)st; + } + + /* Spec verify: per-position snapshot buffers, grown to n. */ + if (spec && d->snaps_cap < n) { + fc_buffer_free(d->conv_snaps); + fc_buffer_free(d->ssm_snaps); + d->conv_snaps = fc_buffer_alloc_ctx(ctx, (size_t)n * conv_n * + sizeof(float)); + d->ssm_snaps = fc_buffer_alloc_ctx(ctx, (size_t)n * ssm_n * + sizeof(float)); + d->snaps_cap = (d->conv_snaps && d->ssm_snaps) ? n : 0; + if (!d->snaps_cap) + return -1; + } + + fc_rms_norm_nx_ctx(ctx, b->hidden, d->attn_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, n); + double st0 = prof_on() ? prof_now() : 0.0; + rc = gpu_ssm_block_batch(d, cfg, b, n, spec); + if (rc < 0) + return rc; + bprof_lap(BPROF_SSM, ctx, &st0); + fc_rms_norm_nx_ctx(ctx, b->hidden, d->post_attention_norm.buf, + b->x_norm, c, cfg->rms_norm_eps, n); + moe = &d->moe; + } + + /* allow_dedup = !spec: this function serves both the PREFILL chunk loop + * (n = QWEN35_PREFILL_CHUNK positions, wide enough for the expert union + * to pay) and the speculative VERIFY (n = draft length, a handful of + * positions — decode-shaped economics, so it stays on _nx). */ + double mt0 = prof_on() ? prof_now() : 0.0; + rc = gpu_moe_block_batch(ctx, moe, cfg, b, n, !spec); + bprof_lap(BPROF_MOE, ctx, &mt0); + return rc; +} + +/* Phase 9b — prefill chunk pipelining (QWEN35_PREFILL_PIPELINE, default OFF). + * + * The two dies hold disjoint layer ranges (A = [0, split), B = [split, + * n_layer)) and today every prefill chunk drains BOTH of them before the + * serve's chunk loop advances: the die-A sync at the boundary hand-off, then + * the die-B sync in front of the LM-head fetch. So die A idles for the whole + * back half of every chunk and die B for the whole front half. + * + * The dependency graph does not require that. Chunk N+1's die-A work needs + * only state die A itself wrote (its own KV rings and its own SSM recurrence + * for layers [0, split)); it has NO dependency on chunk N's die-B work. The + * SSM recurrence IS sequential across chunks, but it is sequential *per die*: + * layer L's carry is consumed by layer L, and layer L lives on exactly one + * die. Chunk N+1's die-A layers therefore serialize behind chunk N's die-A + * layers — which is the same stream, so CUDA already orders it — and overlap + * chunk N's die-B layers, which is the win. + * + * All this needs is for the host not to block on die B. Dropping the tail + * (final norm + LM head + fc_sync_ctx + logits fetch) on every chunk but the + * last does exactly that: the host issues die B's segment asynchronously and + * walks straight into chunk N+1's die-A segment. The rendezvous that keeps it + * correct is already in the code — the hand-off's `fc_buffer_from_cpu_n` into + * bb->hidden is a legacy-null-stream HtoD in ctx_b, so it implicitly waits for + * every outstanding blocking-stream op in that context, i.e. for die B to + * finish chunk N before its input buffer is overwritten. Steady-state period + * becomes max(t_A, t_B) instead of t_A + t_B. + * + * NOTHING about the arithmetic changes: same kernels, same order, same bytes + * across the link, same per-die stream. Only the LM head of the discarded + * intermediate chunks stops being computed — and the serve already threw those + * logits away (sg_prefill_range tm_free()s every chunk but the last). + * + * Callers MUST drain (pn_qwen35moe_prefill_drain_cuda) before touching state + * that an in-flight die-B segment is still writing — the range helpers do this + * on every early exit. */ +static int qw_prefill_pipeline_on(void) +{ + static int v = -1; + if (v < 0) { + const char *e = getenv("QWEN35_PREFILL_PIPELINE"); + v = (e && e[0] == '1') ? 1 : 0; + } + return v; +} + +/* ★ QWEN35_PROF makes the pipeline INERT, and the inertness is invisible. + * + * bprof_lap() / ssm_lap() / sprof_lap() each call fc_sync_ctx(ctx) on the die + * that issued the phase, so with profiling on every layer of die B's segment is + * drained the moment it is issued — the last thing a die-B layer does is + * bprof_lap(BPROF_MOE, ctx_b, ...). By the time gpu_batch_tokens_core reaches + * `if (no_tail) return`, there is nothing left in flight for the next chunk's + * die-A segment to overlap. A profiled run is a SERIALISED run, on both entry + * points, whatever QWEN35_PREFILL_PIPELINE says. + * + * The serve's `[prefill_pipeline] ON: ...` sentinel does NOT catch this: it + * counts the code path the chunk loop chose, not work actually left in flight, + * so it prints duty=100% serialised=0 for a fully serialised profiled run. + * Neither does comparing BPROF's bucket sum to the same run's own wall clock — + * both quantities come from the same serialised run. + * + * This cost the campaign a 1.85x error in its baseline: a profiled harness + * measured 31.37 tok/s at 14 080 while the unprofiled serve does ~58-68 at the + * same depth. Read absolute seconds and tok/s from an UNPROFILED run; read + * shares and A/B ratios from a profiled one (they survive, because both dies + * carry the same layer mix). */ +static void qw_warn_prof_serialises_pipeline(void) +{ + static int warned = 0; + if (warned || !prof_on()) + return; + warned = 1; + fprintf(stderr, "[qwen35moe] WARNING: QWEN35_PROF=1 with " + "QWEN35_PREFILL_PIPELINE=1 — the profiler's per-phase " + "fc_sync_ctx drains die B inside every chunk, so the two " + "dies run SERIALISED. Shares and A/B ratios are valid; " + "absolute seconds and tok/s are pipeline-OFF numbers and " + "are ~1.6-2.0x slower than the unprofiled serve.\n"); +} + +int pn_qwen35moe_prefill_pipeline_enabled(void) +{ + int on = qw_prefill_pipeline_on(); + if (on) + qw_warn_prof_serialises_pipeline(); + return on; +} + +/** + * LM head for the LAST position of a batched pass, into s->logits. + * + * `b->x_norm` must already hold the final norm for all n positions; this slices + * row n-1 out of it and runs the same single-column head matvec + fetch the + * single-token path uses. + * + * Shared by the sequential batched tail and the pipelined prefill so both turn + * the same activations into the same logit bytes — the pipeline's bit-exactness + * argument depends on there being exactly one implementation of this. + * + * @return s->logits (borrowed; owned by the state), or NULL on failure. + * + * Thread-safe: no (mutates per-die scratch and s->logits) + * Reentrant: no + * Signal-safe: no + */ +static float *gpu_batch_head_last_row(pn_qwen35moe_gpu *g, + const pn_qwen35moe_config *cfg, + fc_cuda_ctx *ctx, gpu_batch_scratch *b, + gpu_die_scratch *sc, gpu_host_scratch h, + pn_qwen35moe_state *s, int n) +{ + int c = cfg->n_embd; + fc_buffer_copy_d2d_ctx(ctx, sc->r_xnorm, 0, b->x_norm, + (size_t)(n - 1) * c * sizeof(float), + (size_t)c * sizeof(float)); + gpu_matvec(&g->output, sc->r_xnorm, sc->r_logits, h.a, h.b); + fc_sync_ctx(ctx); + gpu_fetch_logits(ctx, sc->r_logits, s, cfg->n_vocab); + return s->logits; +} + +/* Shared core of the batched prefill and the speculative verify. spec = 0: + * the original prefill semantics (all_logits / last-position fetch). spec = + * 1: every SSM layer's seq scans write per-position state snapshots, and the + * tail computes each position's greedy argmax ON the die (batched LM head + + * device top-k(1) per row) into argmax_out — s->logits is not updated. + * no_tail = 1 (9b, prefill-only, spec=0 and all_logits=NULL): skip the whole + * head phase and return WITHOUT syncing die B, leaving its segment in flight. */ +static float *gpu_batch_tokens_core(pn_qwen35moe_model *m, + const uint32_t *tokens, int n, + pn_qwen35moe_state *s, int pos0, + float *all_logits, int spec, + int32_t *argmax_out, int no_tail) +{ + if (!m || !s || !tokens || n <= 0) + return NULL; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return NULL; + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + for (int t = 0; t < n; t++) + if ((int)tokens[t] >= cfg->n_vocab) + return NULL; + + if (!g->host_ok) { + if (gpu_host_scratch_init(&g->host, cfg) < 0) + return NULL; + g->host_ok = 1; + } + gpu_host_scratch h = g->host; + if (gpu_get_batch_host(g, cfg, n) < 0) + return NULL; + + /* ---- Walk the dies in order: die 0 embeds all n tokens, each die runs + * its layer range, and each boundary hands the [n][c] activations to the + * next die. ---- */ + fc_cuda_ctx *ctx = NULL; + gpu_batch_scratch *b = NULL; + gpu_die_scratch *sc = NULL; + size_t hidden_bytes = (size_t)n * c * sizeof(float); + + for (int d = 0; d < g->n_die; d++) { + if (d > 0) + gpu_handoff_export(g, d - 1, b->hidden, g->bhost[0], hidden_bytes); + if (gpu_select_die(g, g->die[d]) < 0) + return NULL; + gpu_batch_scratch *bd = gpu_get_batch_scratch(g, g->ctx[d], cfg, n); + gpu_die_scratch *scd = gpu_get_scratch(g, g->ctx[d], cfg); + if (!bd || !scd) + return NULL; + if (d == 0) { + for (int t = 0; t < n; t++) + pn_qwen35moe_model_embed_row(m, tokens[t], + g->bhost[0] + (size_t)t * c); + fc_buffer_from_cpu_n(bd->hidden, g->bhost[0], hidden_bytes); + } else { + gpu_handoff_import(g, d - 1, b->hidden, bd->hidden, g->bhost[0], + hidden_bytes); + } + + for (int layer = g->layer_lo[d]; layer < g->layer_hi[d]; layer++) { + if (gpu_layer_forward_batch(m, g, g->ctx[d], layer, bd, s, pos0, n, + spec) < 0) + return NULL; + } + ctx = g->ctx[d]; + b = bd; + sc = scd; + } + + /* 9b: an intermediate prefill chunk. Its logits are discarded by the + * caller, so skip the head phase entirely and — the point of the whole + * exercise — return without syncing the last die, leaving its segment in + * flight so the next chunk's die-0 segment overlaps it. Returns the + * success sentinel; s->logits is deliberately left stale (no caller in + * this mode reads it, and the final chunk always refreshes it). */ + if (no_tail) + return s->logits; + + /* ---- Final norm over all positions + LM head. ---- */ + fc_rms_norm_nx_ctx(ctx, b->hidden, g->output_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, n); + + if (spec) { + /* Per-position greedy argmax without leaving the die: ONE batched + * LM-head launch into the [n][n_vocab] spec buffer, then a device + * top-k(1) per row (bit-exact iterative argmax, same kernel the + * serving fetch uses) — only n (id, logit) pairs cross the 0.4 GB/s + * host link instead of n full vocab vectors. */ + if (!argmax_out) + return NULL; + if (g->spec_logits_cap < n) { + fc_buffer_free(g->spec_logits); + g->spec_logits = fc_buffer_alloc_ctx(ctx, (size_t)n * + cfg->n_vocab * sizeof(float)); + g->spec_logits_cap = g->spec_logits ? n : 0; + if (!g->spec_logits_cap) + return NULL; + } + if (gpu_matvec_nx(&g->output, b->x_norm, g->spec_logits, n) < 0) + return NULL; + for (int t = 0; t < n; t++) { + int id; + float val; + fc_buffer_copy_d2d_ctx(ctx, sc->r_logits, 0, g->spec_logits, + (size_t)t * cfg->n_vocab * sizeof(float), + (size_t)cfg->n_vocab * sizeof(float)); + if (fc_topk_logits_ctx(ctx, sc->r_logits, cfg->n_vocab, 1, + &id, &val) != 0) + return NULL; + argmax_out[t] = id; + /* The first draft mismatch ends the acceptable prefix — rows + * past it are unreachable for the caller's accept scan, so + * skip their top-k fetches (marked -1). */ + if (t + 1 < n && tokens[t + 1] != (uint32_t)id) { + for (int r = t + 1; r < n; r++) + argmax_out[r] = -1; + break; + } + } + return s->logits; /* success sentinel — contents NOT updated */ + } + + if (all_logits) { + /* Per-position logits (the bit-exact gate): slice each position's + * x_norm and run the SAME single-column LM-head matvec + full D2H + * the single-token path uses. */ + for (int t = 0; t < n; t++) { + fc_buffer_copy_d2d_ctx(ctx, sc->r_xnorm, 0, b->x_norm, + (size_t)t * c * sizeof(float), + (size_t)c * sizeof(float)); + gpu_matvec(&g->output, sc->r_xnorm, sc->r_logits, h.a, h.b); + fc_sync_ctx(ctx); + fc_buffer_to_cpu(sc->r_logits, + all_logits + (size_t)t * cfg->n_vocab); + } + memcpy(s->logits, all_logits + (size_t)(n - 1) * cfg->n_vocab, + (size_t)cfg->n_vocab * sizeof(float)); + s->topk_filled = 0; /* s->logits holds real values */ + return s->logits; + } + + /* Serving path: only the LAST position's logits leave the die. */ + return gpu_batch_head_last_row(g, cfg, ctx, b, sc, h, s, n); +} + +/* Largest row count one batched call may span before the q4 fp16 recent-window + * ring aliases against itself. + * + * The ring laps at PN_KVQ4_WMAX (256) and a query row at cur_pos reads the last + * `win` positions from it. A batched call appends ALL of its rows before ANY + * row runs its attention, so the union of positions the call still needs is + * (pos0 - win, pos0 + n - 1] — win + n - 1 rows. Keep that inside the ring and + * every row reads its own true window: n <= WMAX - win + 1. We cap at + * WMAX - win (one row of slack, so the batch never touches the row it would + * lap) — 128 rows at the default win=128, which is exactly serve's default + * prefill chunk, so production never splits. Returns 0 for "no cap". + * + * Above the cap the kernel's win_live_lo guard keeps the result CORRECT (those + * rows read the q4 body instead of the shadow) but no longer batch-invariant; + * splitting here keeps a long chunk bit-identical to the same tokens prefilled + * in serve-sized chunks. Non-q4 caches and win == 0 have no ring, no cap. */ +static int qw_batch_row_cap(const pn_qwen35moe_state *s) +{ + if (!s || s->n_attn <= 0 || !s->kv || !s->kv[0].q4) + return 0; + int win = pn_kvq4_win(); + if (win <= 0) + return 0; + int cap = PN_KVQ4_WMAX - win; + return cap < 1 ? 1 : cap; +} + +static float *qw_batch_tokens_entry(pn_qwen35moe_model *m, + const uint32_t *tokens, int n, + pn_qwen35moe_state *s, int pos0, + float *all_logits, int no_tail) +{ + /* Split an oversized batch into ring-safe sub-batches. Chunk boundaries + * carry the KV/SSM state, so this is exactly the caller's own chunking — + * the per-position logits land at the same offsets either way. */ + int cap = qw_batch_row_cap(s); + if (cap > 0 && m && s && tokens && n > cap) { + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + float *r = NULL; + for (int off = 0; off < n; off += cap) { + int nb = n - off < cap ? n - off : cap; + r = qw_batch_tokens_entry( + m, tokens + off, nb, s, pos0 + off, + all_logits ? all_logits + (size_t)off * cfg->n_vocab : NULL, + no_tail); + if (!r) + return NULL; + } + return r; + } + if (!prof_on()) + return gpu_batch_tokens_core(m, tokens, n, s, pos0, all_logits, 0, + NULL, no_tail); + memset(bprof_call, 0, sizeof(bprof_call)); + /* QWEN35_PROF_SUB additionally splits BPROF's two big buckets. The MoE + * side needs no new instrumentation: gpu_moe_block_batch already laps + * router+shared / routed gate+up / routed down for the slots profiler and + * is shared with this path, so turning sprof_active on for the duration is + * all it takes (sprof_active otherwise gates only decode-slots code, which + * cannot run from here). The SSM side uses ssm_lap inside + * gpu_ssm_block_batch. Both nest inside the BPROF spans. */ + int sub = prof_sub_on(); + if (sub) { + memset(sprof_step_ph, 0, sizeof(sprof_step_ph)); + memset(ssmp_call, 0, sizeof(ssmp_call)); + sprof_active = 1; + } + double t0 = prof_now(); + float *r = gpu_batch_tokens_core(m, tokens, n, s, pos0, all_logits, 0, + NULL, no_tail); + if (sub) { + sprof_active = 0; + for (int ph = 0; ph < SPROF_NCOUNT; ph++) + sprof_cum_ph[ph] += sprof_step_ph[ph]; + } + if (r) { + bprof_report(n, pos0, prof_now() - t0); + if (sub) { + double moe_sum = sprof_cum_ph[SPROF_MOE_SHEXP] + + sprof_cum_ph[SPROF_MOE_GATEUP] + + sprof_cum_ph[SPROF_MOE_DOWN]; + double ssm_sum = 0.0; + for (int ph = 0; ph < SSMP_COUNT; ph++) + ssm_sum += ssmp_cum[ph]; + fprintf(stderr, "MPROF cum n=%d moe |", n); + fprintf(stderr, " router_shexp=%.3f(%.1f%%) gateup=%.3f(%.1f%%)" + " down=%.3f(%.1f%%)", + sprof_cum_ph[SPROF_MOE_SHEXP], + 100.0 * sprof_cum_ph[SPROF_MOE_SHEXP] / + (moe_sum > 0 ? moe_sum : 1.0), + sprof_cum_ph[SPROF_MOE_GATEUP], + 100.0 * sprof_cum_ph[SPROF_MOE_GATEUP] / + (moe_sum > 0 ? moe_sum : 1.0), + sprof_cum_ph[SPROF_MOE_DOWN], + 100.0 * sprof_cum_ph[SPROF_MOE_DOWN] / + (moe_sum > 0 ? moe_sum : 1.0)); + fprintf(stderr, " | ssm"); + for (int ph = 0; ph < SSMP_COUNT; ph++) + fprintf(stderr, " %s=%.3f(%.1f%%)", ssmp_name[ph], ssmp_cum[ph], + 100.0 * ssmp_cum[ph] / (ssm_sum > 0 ? ssm_sum : 1.0)); + fprintf(stderr, "\n"); + } + } + return r; +} + +float *pn_qwen35moe_forward_batch_tokens_cuda(pn_qwen35moe_model *m, + const uint32_t *tokens, int n, + pn_qwen35moe_state *s, int pos0, + float *all_logits) +{ + return qw_batch_tokens_entry(m, tokens, n, s, pos0, all_logits, 0); +} + +/* ============================================================ + * Phase 9b — prefill chunk pipelining, public entry points + * ============================================================ */ + +static inline int qw_model_uses_qwen35moe_state(const pn_model *model); + +/* Run ONE intermediate prefill chunk: everything the ordinary batched prefill + * does except the head phase, and WITHOUT draining die B (see the long comment + * on qw_prefill_pipeline_on). Advances state->pos by n. Returns 0, or -1 if + * the batched path could not run — in which case NOTHING was advanced and the + * caller must fall back to the ordinary chunk call, which is always correct. + * + * Single-die placement is a legal no-op: there is no die B to leave in flight, + * so this just skips a discarded LM head. */ +int pn_qwen35moe_prefill_chunk_cuda(pn_model *model, const uint32_t *tokens, + int n, pn_state *state) +{ + if (!model || !state || !tokens || n <= 0) + return -1; + if (!qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + if (!pn_qwen35moe_model_gpu(m)) + return -1; + if (!qw_batch_tokens_entry(m, tokens, n, s, state->pos, NULL, 1)) + return -1; + state->pos += n; + return 0; +} + +/* ============================================================ + * Phase 9c — WIDE prefill pipelining: every die busy at once + * + * WHAT 9b LEAVES ON THE TABLE. 9b overlaps chunk N+1's FIRST die with chunk N's + * LAST die, and nothing else: gpu_handoff_export() does a blocking + * fc_sync_ctx() on the sending die at every boundary, so inside a chunk the + * dies still run strictly one after another. On a 2-die split that is the whole + * machine and 9b measured 1.96x of its 1.984x ceiling. On the 8-die split it + * overlaps 2 of 8 dies: the period falls from 8t to 7t, worth 1.14x. Seven + * eighths of the dies are still idle at any instant. + * + * WHAT THIS DOES. It runs the chunks as a proper software pipeline: at step T, + * die d computes chunk T-d. Every die works on a DIFFERENT chunk at the same + * time, so the steady-state period is one die-segment instead of n_die of them + * and the ceiling is n_die rather than 2. + * + * WHY IT IS LEGAL — the same dependency argument 9b made, carried further. Die + * d's work on chunk c needs (a) die d-1's output for chunk c, which die d-1 + * produced one step earlier, and (b) die d's OWN carried state — its KV rings + * and its SSM recurrence for layers [lo[d], hi[d]). A layer lives on exactly + * one die, so (b) is a per-die sequential dependency and die d visits chunks in + * strictly increasing order, which honours it. There is no dependency from die + * d's chunk c to die d-1's chunk c+1, and that is the one the pipeline breaks. + * + * ORDER OF A STEP, and why each part is where it is: + * 1. Barrier every die that computed at step T-1. A die's output buffer is + * about to be read and its input buffer overwritten. + * 2. Hand the residual stream forward, from the HIGHEST die down. Descending + * is not cosmetic: die d's hidden must be copied into die d+1 before die + * d-1's is copied into die d, or a chunk is overwritten before it moves. + * 3. Die 0 embeds the newly-injected chunk T. + * 4. Issue every active die's layer segment, with NO sync between them. This + * is the only step that costs GPU time, and all n_die of them overlap. + * + * BIT-EXACTNESS. Same kernels, same per-die order, same inputs, same bytes over + * the same links; only the issue timing changes. Nothing reassociates and no + * arithmetic is skipped. Verified with PN_PREFILL_LOGITS_HASH=1 against the + * sequential path. + * + * WHAT IT COSTS. n_die - 1 boundary copies per chunk, exactly as today - the + * pipeline does not add hops, it only stops idling between them. The barrier is + * n_die fc_sync_ctx calls per step against one die-segment of compute. + * + * THE REAL CEILING IS POWER, NOT THIS CODE. A K80 is two GK210 dies behind one + * board budget; driving both drags them off 875 MHz. An 8-die pipeline puts two + * busy dies on each of the four boards, so the win is n_die x (throttled clock / + * 875), not n_die. That is a property of the hardware, measured separately. + * ============================================================ */ + +static int qw_prefill_pipeline_wide_on(void) +{ + /* DEFAULT ON. A layer split ran the dies in sequence, so prefill used 1.22 + * of 8 dies -- 16% occupancy -- with the host thread spinning in + * fc_sync_ctx rather than working. Measured on the 8-die BF16 35B: 2.42x at + * 475 tokens, 5.12x at 1,850, 6.40x at 4,637 (95% of the structural + * 8n/(n+7) ceiling), taking TTFT at 4,637 tokens from 171 s to 26.7 s. + * Bit-exact: an FNV-1a hash over the prefill's final logits is identical + * with the pipeline off and on, at four seeded prompts. + * + * It falls off at depth -- 4.58x at 9k, 2.75x at 16.7k -- and the cause is + * thermal, not algorithmic: die 2 reaches 91 C and the driver clocks it to + * 478 then 256 MHz while the other seven hold 832-875, and a pipeline's + * period is its slowest stage. Sequentially that never appears because the + * neighbours are idle. The software answer is a cost-weighted split (die 2 + * sustains ~0.30 of the others' rate at depth and should hold ~1.5 layers, + * not 5); until that lands, deep prefill is still faster than sequential, + * just not 8x. + * + * QWEN35_PREFILL_PIPELINE_WIDE=0 restores the sequential path. The wide + * path already refuses cleanly under QWEN35_PROF, a single die, barswap, + * QWEN35_HOST_KV_MIRROR=1, or no flash. */ + static int v = -1; + if (v < 0) { + const char *e = getenv("QWEN35_PREFILL_PIPELINE_WIDE"); + v = (e && e[0] == '0') ? 0 : 1; + } + return v; +} + +/* One chunk of the prefill range: where it starts, how many tokens, and the + * absolute position its first token occupies. */ +typedef struct { + const uint32_t *tokens; + int n; + int pos0; +} qw_pipe_chunk; + +static inline qw_pipe_chunk qw_pipe_chunk_at(const uint32_t *tokens, + int n_total, int pos0, + int chunk, int c) +{ + qw_pipe_chunk k; + int off = c * chunk; + k.tokens = tokens + off; + k.n = n_total - off < chunk ? n_total - off : chunk; + k.pos0 = pos0 + off; + return k; +} + +/** + * Run the whole prefill range as an n_die-stage software pipeline. + * + * @param inject_probe optional; called once per step with the number of chunks + * already injected. Returning non-zero stops NEW chunks + * being injected — the pipeline then drains what is in + * flight so the KV/SSM state still lands on a chunk + * boundary — and the range reports an abort. + * @param retired_out receives the number of tokens that fully cleared the + * pipeline (== n_total unless aborted). + * @return 0 on success, 1 if aborted after draining, -1 on failure. + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no + */ +static int gpu_prefill_pipeline_run(pn_qwen35moe_model *m, + const uint32_t *tokens, int n_total, + pn_qwen35moe_state *s, int pos0, int chunk, + int (*inject_probe)(void *), void *ud, + int *retired_out) +{ + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + const int n_die = g->n_die; + const int c = cfg->n_embd; + int aborted = 0; + + *retired_out = 0; + + for (int t = 0; t < n_total; t++) + if ((int)tokens[t] >= cfg->n_vocab) + return -1; + + if (!g->host_ok) { + if (gpu_host_scratch_init(&g->host, cfg) < 0) + return -1; + g->host_ok = 1; + } + gpu_host_scratch h = g->host; + + /* Size every per-die scratch and the host staging set for the WIDEST chunk + * up front. gpu_get_batch_scratch frees and reallocates when it is asked to + * grow, and doing that mid-pipeline would pull buffers out from under a + * segment still in flight. After this, every call is a cache hit. */ + if (gpu_get_batch_host(g, cfg, chunk) < 0) + return -1; + gpu_batch_scratch *bs[PN_DIE_LAYER_SPLIT_MAX_DIES]; + gpu_die_scratch *ds[PN_DIE_LAYER_SPLIT_MAX_DIES]; + for (int d = 0; d < n_die; d++) { + if (gpu_select_die(g, g->die[d]) < 0) + return -1; + bs[d] = gpu_get_batch_scratch(g, g->ctx[d], cfg, chunk); + ds[d] = gpu_get_scratch(g, g->ctx[d], cfg); + if (!bs[d] || !ds[d]) + return -1; + } + + const int n_chunks = (n_total + chunk - 1) / chunk; + int injected = n_chunks; /* lowered to stop injecting on abort */ + + for (int step = 0; step < injected + n_die - 1; step++) { + /* 1. Drain step T-1 before touching any buffer it wrote. */ + for (int d = 0; d < n_die; d++) { + int prev = step - 1 - d; + if (prev >= 0 && prev < injected) + fc_sync_ctx(g->ctx[d]); + } + + /* Chunk c is issued to the last die at step c + n_die - 1, so it has + * landed once the barrier at step c + n_die has run. At the top of step + * T that makes chunks 0 .. T - n_die retired, i.e. T - n_die + 1 of + * them. Only the abort path reads this, but it has to be exact: it is + * what state->pos advances by. */ + int retired_chunks = step - n_die + 1; + if (retired_chunks > injected) + retired_chunks = injected; + if (retired_chunks > 0) + *retired_out = (retired_chunks * chunk < n_total) + ? retired_chunks * chunk : n_total; + + if (!aborted && inject_probe && inject_probe(ud)) { + /* Stop feeding the pipeline, but keep stepping until what is + * already in flight has cleared the last die: a half-walked chunk + * would leave this session's layers at different positions. */ + aborted = 1; + int in_flight = step; + injected = in_flight < n_chunks ? in_flight : n_chunks; + if (step >= injected + n_die - 1) + break; + } + + /* 2. Boundary hand-offs, highest die first. */ + for (int d = n_die - 1; d >= 1; d--) { + int ck = step - d; + if (ck < 0 || ck >= injected) + continue; + qw_pipe_chunk k = qw_pipe_chunk_at(tokens, n_total, pos0, chunk, ck); + size_t bytes = (size_t)k.n * c * sizeof(float); + gpu_handoff_export(g, d - 1, bs[d - 1]->hidden, g->bhost[0], bytes); + if (gpu_select_die(g, g->die[d]) < 0) + return -1; + gpu_handoff_import(g, d - 1, bs[d - 1]->hidden, bs[d]->hidden, + g->bhost[0], bytes); + } + + /* 3. Inject chunk `step` at die 0. */ + if (step < injected) { + qw_pipe_chunk k = qw_pipe_chunk_at(tokens, n_total, pos0, chunk, + step); + if (gpu_select_die(g, g->die[0]) < 0) + return -1; + for (int t = 0; t < k.n; t++) + pn_qwen35moe_model_embed_row(m, k.tokens[t], + g->bhost[0] + (size_t)t * c); + fc_buffer_from_cpu_n(bs[0]->hidden, g->bhost[0], + (size_t)k.n * c * sizeof(float)); + } + + /* 4. Issue every active die's segment. No syncs — this is the overlap. */ + for (int d = 0; d < n_die; d++) { + int ck = step - d; + if (ck < 0 || ck >= injected) + continue; + qw_pipe_chunk k = qw_pipe_chunk_at(tokens, n_total, pos0, chunk, ck); + if (gpu_select_die(g, g->die[d]) < 0) + return -1; + for (int layer = g->layer_lo[d]; layer < g->layer_hi[d]; layer++) + if (gpu_layer_forward_batch(m, g, g->ctx[d], layer, bs[d], s, + k.pos0, k.n, 0) < 0) + return -1; + + /* The final chunk's last die also runs the head, so the caller gets + * real logits from the same activations the sequential path would + * have produced. Intermediate chunks' heads are discarded by the + * serve and are never computed, exactly as in 9b. */ + if (d == n_die - 1 && !aborted && ck == n_chunks - 1) { + fc_rms_norm_nx_ctx(g->ctx[d], bs[d]->hidden, + g->output_norm.buf, bs[d]->x_norm, c, + cfg->rms_norm_eps, k.n); + if (!gpu_batch_head_last_row(g, cfg, g->ctx[d], bs[d], ds[d], h, + s, k.n)) + return -1; + } + } + } + + for (int d = 0; d < n_die; d++) + fc_sync_ctx(g->ctx[d]); + + if (!aborted) + *retired_out = n_total; + return aborted ? 1 : 0; +} + +/* Drain both dies. Every early exit from a pipelined chunk loop (abort, error, + * end of range) must call this before anything reads or resets the state an + * in-flight die-B segment is still writing. Idempotent and cheap when the + * pipeline is off or already drained. */ +void pn_qwen35moe_prefill_drain_cuda(pn_model *model) +{ + if (!model || !qw_model_uses_qwen35moe_state(model)) + return; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + if (!m) + return; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return; + for (int i = 0; i < g->n_die; i++) + if (g->ctx[i]) + fc_sync_ctx(g->ctx[i]); +} + +int pn_qwen35moe_prefill_pipeline_wide_enabled(void) +{ + return qw_prefill_pipeline_wide_on(); +} + +int pn_qwen35moe_prefill_range_pipelined_cuda(pn_model *model, + const uint32_t *tokens, + int n_total, pn_state *state, + int chunk, + int (*should_abort)(void *), + void *ud, + tm_tensor **logits_out) +{ + if (logits_out) + *logits_out = NULL; + if (!qw_prefill_pipeline_wide_on()) + return -1; + if (!model || !state || !tokens || n_total <= 0 || chunk <= 0) + return -1; + if (!qw_model_uses_qwen35moe_state(model)) + return -1; + + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return -1; + + /* A one-die placement has no stages to overlap, and barswap holds a single + * die in its MMIO slot at a time, so no two dies are ever live. */ + if (g->n_die < 2 || g->bs) + return -1; + + /* QWEN35_PROF drains the issuing die inside every phase lap, which + * serialises the pipeline while still printing a full profile — the exact + * failure that cost the 9b campaign a 1.85x baseline error. Refuse rather + * than report pipeline-shaped numbers from a serialised run. */ + if (prof_on()) { + static int warned = 0; + if (!warned) { + warned = 1; + fprintf(stderr, "[qwen35moe] QWEN35_PREFILL_PIPELINE_WIDE ignored: " + "QWEN35_PROF=1 drains each die inside every phase lap, so " + "the pipeline cannot overlap. Profile the sequential path " + "for shares; measure the pipeline unprofiled.\n"); + } + return -1; + } + + /* ★ The shared-host-buffer hazard. Several dies are live at once here, so + * ANY layer path that stages through the shared g->bhost[] set would have + * two dies writing the same host bytes — which would not fail, it would + * produce fluent-looking garbage. The default batched layer path is clean: + * flash attention does its q/gate split and softmax on the die, the lazy KV + * mirror skips the host store, and the MoE and SSM blocks never leave the + * die at all. Two configurations reintroduce the hazard, and both are + * refused rather than raced: + * - QWEN35_HOST_KV_MIRROR=1 D2Hs every attention layer's K/V through + * bhost[3]; + * - without flash, gpu_attn_block_batch runs the q/gate split and the + * causal softmax on the host through bhost[1..3]. + * Refusing costs nothing: the caller runs the sequential path, which is + * correct under every configuration. */ + if (pn_qwen35moe_host_kv_mirror_eager() || !flash_on()) + return -1; + for (int d = 0; d < g->n_die; d++) + if (!fc_attn_flash_available_ctx(g->ctx[d])) + return -1; + + /* Keep the pipeline's chunk inside the q4 recent-window ring cap. Above it + * qw_batch_tokens_entry would silently re-split a chunk into sub-batches, + * which would break the one-chunk-per-die-per-step invariant the schedule + * is built on. */ + int cap = qw_batch_row_cap(s); + if (cap > 0 && chunk > cap) + chunk = cap; + + int retired = 0; + int rc = gpu_prefill_pipeline_run(m, tokens, n_total, s, state->pos, chunk, + should_abort, ud, &retired); + if (rc < 0) + return -1; /* state is undefined; caller resets */ + state->pos += retired; + if (rc == 1) + return 1; /* aborted after draining */ + + /* Same owned [n_vocab] tensor the sequential batched entry hands back, so + * the serve's chunk loop and this one are interchangeable to the caller. */ + if (logits_out) { + int64_t shape[1] = { s->n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + if (!out) + return -1; + memcpy(tm_data_f32(out), s->logits, + (size_t)s->n_vocab * sizeof(float)); + *logits_out = out; + } + return 0; +} + +/* ============================================================ + * Speculative greedy decoding — batched verify + partial-accept commit + * + * verify runs [tokens[0..n)] (the committed pending token + the drafts) + * through the batched forward at positions pos0..pos0+n-1 with per-position + * SSM state snapshots, and reports each position's greedy argmax. It does + * NOT commit anything: the caller scans the argmaxes against the drafts, + * decides how many positions to accept (n_accept in [1, n]), and calls + * commit. + * + * commit makes the dual state reflect exactly n_accept processed tokens: + * - SSM (conv + recurrent) state: restored from snapshot n_accept-1 with + * one D2D copy per layer (a no-op when n_accept == n — the in-place + * scan already left state-after-n). + * - KV cache: nothing to do. Entries the rejected positions wrote sit at + * ring slots > pos0+n_accept-1; attention for a position p only reads + * slots [0, p], so they are never read and are overwritten when those + * positions are re-processed with the corrected tokens. (verify refuses + * pos0 + n > max_seq, so the ring never wraps mid-verify.) + * Every accepted position's argmax is the model's exact greedy token given + * the same prefix — bit-exact vs plain single-token greedy decode. + * ============================================================ */ + +int pn_qwen35moe_spec_verify_tokens_cuda(pn_qwen35moe_model *m, + const uint32_t *tokens, int n, + pn_qwen35moe_state *s, int pos0, + int32_t *argmax_out) +{ + if (!m || !s || !tokens || n <= 0 || !argmax_out) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return -1; + /* No KV-ring wraparound inside a verify block: rejected slots must stay + * in the never-read tail of the ring. */ + if (s->n_attn > 0 && pos0 + n > s->kv[0].max_seq) + return -1; + + g->spec_n = 0; + g->spec_state = NULL; + if (!gpu_batch_tokens_core(m, tokens, n, s, pos0, NULL, 1, argmax_out, 0)) + return -1; + g->spec_n = n; + g->spec_state = s; + g->spec_pos0 = pos0; + return 0; +} + +int pn_qwen35moe_spec_commit_cuda(pn_qwen35moe_model *m, int n_accept) +{ + if (!m) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g || g->spec_n <= 0 || n_accept < 1 || n_accept > g->spec_n) + return -1; + + int n = g->spec_n; + pn_qwen35moe_state *s = g->spec_state; + int pos0 = g->spec_pos0; + g->spec_n = 0; + g->spec_state = NULL; + + /* Phase 9.2 — KV ring watermark. The verify appended rows + * [pos0, pos0+n) to the single-user GPU ring of every attention layer + * but left the watermark at pos0, because only the caller knows how many + * drafts landed. The first n_accept of those rows now ARE committed + * content, so hand them to the watermark; the rejected tail stays above + * the new frontier, where attention never reads it and the next append + * overwrites it. + * + * This is what makes the next verify free. Without it the watermark + * trails the frontier and gpu_attn_kv_cache_sync tries to close the gap + * from the host KV mirror — which under the lazy mirror (Phase 10b/INV3) + * does not hold those rows, so the verify fails outright, and under the + * eager mirror costs a full-window H2D. That was the Phase 9.1 defect. + * + * Only layers still bound to this state's ring are advanced; anything + * else has been rebound since the verify and must resync normally. */ + if (s) { + for (int layer = 0; layer < pn_qwen35moe_model_config(m)->n_layer; + layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != + PN_QWEN35MOE_LAYER_ATTN) + continue; + int idx = pn_qwen35moe_model_layer_index(m, layer); + gpu_attn_layer *da = &g->attn[idx]; + pn_qwen35moe_kv *kv = &s->kv[idx]; + const void *host_id = kv->q4 ? (const void *)kv->kq + : (const void *)kv->k; + if (da->kv_host_id == host_id && da->kv_gen == kv->content_gen && + da->kv_synced_pos == pos0) + da->kv_synced_pos = pos0 + n_accept; + } + } + + if (n_accept == n) + return 0; /* full accept: the in-place scans already hold state-after-n */ + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + size_t conv_bytes = (size_t)(cfg->ssm_conv - 1) * cfg->conv_dim * + sizeof(float); + size_t ssm_bytes = (size_t)cfg->ssm_state * cfg->ssm_state * + cfg->ssm_dt_rank * sizeof(float); + size_t snap = (size_t)(n_accept - 1); + + /* Walk the layers in die order (front then back), syncing each die's + * copies before leaving it, so barswap rigs swap at most once and never + * touch a context whose die is out of the slot. Attention layers ride + * along to put back the recent-window shadow slots the rejected rows + * evicted (see qw_spec_win_restore). */ + fc_cuda_ctx *cur = NULL; + for (int layer = 0; layer < cfg->n_layer; layer++) { + int lidx = pn_qwen35moe_model_layer_index(m, layer); + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_SSM) { + gpu_attn_layer *da = &g->attn[lidx]; + if (!s || da->spec_win_n != n) + continue; + if (da->ctx != cur) { + if (cur) + fc_sync_ctx(cur); + if (gpu_select_die(g, gpu_die_of_ctx(g, da->ctx)) < 0) + return -1; + cur = da->ctx; + } + qw_spec_win_restore(da, &s->kv[lidx], pos0, n, n_accept); + continue; + } + gpu_ssm_layer *d = &g->ssm[lidx]; + if (!d->conv_snaps || !d->ssm_snaps || d->snaps_cap < n) + return -1; + if (d->ctx != cur) { + if (cur) + fc_sync_ctx(cur); + if (gpu_select_die(g, gpu_die_of_ctx(g, d->ctx)) < 0) + return -1; + cur = d->ctx; + } + fc_buffer_copy_d2d_ctx(d->ctx, d->conv_state_g, 0, d->conv_snaps, + snap * conv_bytes, conv_bytes); + fc_buffer_copy_d2d_ctx(d->ctx, d->ssm_state_g, 0, d->ssm_snaps, + snap * ssm_bytes, ssm_bytes); + } + if (cur) + fc_sync_ctx(cur); + return 0; +} + +/* ============================================================ + * MULTI-USER DECODE — K users, ONE token each, in one forward pass + * + * THE serving-throughput lever: decode is weight-bandwidth-bound, so + * streaming every weight once for K users' tokens (the fc_matvec_*_nx / + * grouped-MoE nx kernels, exactly as batched prefill uses them) raises + * aggregate tok/s toward Kx. The difference from prefill: the K rows are + * INDEPENDENT sequences, so + * - each slot owns its OWN SSM dual state (conv_slots/ssm_slots on the + * layer, slot-major) advanced by single-step slot kernels — no scan, + * no cross-slot dependency; + * - each slot attends to its OWN host KV cache at its OWN position + * (per-row RoPE positions via fc_rope_neox_partial_pos); + * - MoE routes each slot's token to its own top-8 (the nx kernels + * already index experts per row). + * + * BIT-EXACT PER USER: every kernel is per-row/per-slot bit-identical to + * the single-token path's kernel and the host attention glue is the same + * code per user, so each user's token stream when batched is IDENTICAL + * to that user decoding alone. Gated by tests/test_users_qwen35 (run the + * single-user reference under QWEN35_FUSE=0x37 — the fused qknorm+rope + * contraction differs by ~1 ulp from the standalone kernels this path + * mirrors, same caveat as the prefill/spec gates). + * + * Protocol: prefill each user (any path), then IMMEDIATELY bind it to a + * slot (pn_qwen35moe_decode_slot_bind_cuda snapshots the live GPU SSM + * state D2D into the slot — the host mirror is stale after GPU prefill), + * then step all users together with pn_qwen35moe_decode_slots_tokens_cuda. + * Fixed K for this increment; continuous batching (join/leave mid-flight, + * prefill+decode mixing) is the follow-up. + * ============================================================ */ + +/* NOTE: the batched decode path fetches each user's logits with plain + * gpu_fetch_logits — there is no per-slot variant any more. The sparse + * bookkeeping hangs off the per-user pn_qwen35moe_state that owns the buffer + * (gpu_present_topk), so K users stay independently maintained for free, and + * the prefill fetch and the decode fetch share ONE tracker for the buffer they + * both write into (they did not, which is what leaked EOS across turns). */ + +/* Upload the K users' positions to the die's int32 RoPE buffer (die_idx: + * 0 = ctx_a, 1 = ctx_b). Returns the buffer or NULL on failure. */ +static fc_buffer *gpu_pos_buf_upload(pn_qwen35moe_gpu *g, fc_cuda_ctx *ctx, + int die_idx, const int *pos, int nslots) +{ + if (g->pos_buf[die_idx] && g->pos_buf_cap[die_idx] < nslots) { + fc_buffer_free(g->pos_buf[die_idx]); + g->pos_buf[die_idx] = NULL; + g->pos_buf_cap[die_idx] = 0; + } + if (!g->pos_buf[die_idx]) { + g->pos_buf[die_idx] = + fc_buffer_alloc_ctx(ctx, (size_t)nslots * sizeof(int32_t)); + if (!g->pos_buf[die_idx]) + return NULL; + g->pos_buf_cap[die_idx] = nslots; + } + fc_buffer_from_cpu_n(g->pos_buf[die_idx], pos, + (size_t)nslots * sizeof(int32_t)); + return g->pos_buf[die_idx]; +} + +/* Bind user state `st` to decode slot `slot` (of n_slots total): allocate + * (or re-allocate on capacity change — which invalidates every slot, so + * bind ALL users after choosing K) the per-layer slot-state buffers, then + * seed the slot's conv/ssm images. Source: the LIVE GPU state when this + * user's forward was the model's most recent (d->host_state == st — the + * bind-right-after-prefill protocol), else the host mirror (valid only for + * a state that has not been through a GPU forward, e.g. fresh at pos 0). */ +/* Rung 2 of the reactive ladder (MemoryAware.md §7): the pressure-capped decode + * concurrency ceiling. At level OK this is the full compile-time + * PN_QWEN35MOE_GPU_MAX_SLOTS; TIGHT halves it and CRITICAL floors it to 1. + * Reads the live pn_mem_state; when memwatch is off (bare Mac) it returns the + * full ceiling. Exported so the batched-decode scheduler can size its slot + * count up-front — the bind guard below is the last-line enforcement. */ +int pn_qwen35moe_gpu_ladder_max_slots(void) +{ + if (!pn_memwatch_running()) + return PN_QWEN35MOE_GPU_MAX_SLOTS; + pn_mem_state st; + pn_memwatch_read(&st); + pn_ladder_plan lp; + pn_ladder_plan_from_state(&st, PN_QWEN35MOE_GPU_MAX_SLOTS, 0, false, &lp); + return lp.max_slots > 0 ? lp.max_slots : 1; +} + +int pn_qwen35moe_decode_slot_bind_cuda(pn_qwen35moe_model *m, + pn_qwen35moe_state *s, int slot, + int n_slots) +{ + if (!m || !s || slot < 0 || n_slots <= slot || + n_slots > PN_QWEN35MOE_GPU_MAX_SLOTS) + return -1; + + /* Rung 2 enforcement: refuse a bind above the current memory-pressure + * concurrency ceiling (the scheduler should have sized to it via + * pn_qwen35moe_gpu_ladder_max_slots; admission sheds the excess). At level + * OK the ceiling is the full max, so this never fires on an unpressured + * box. */ + int slot_ceiling = pn_qwen35moe_gpu_ladder_max_slots(); + if (n_slots > slot_ceiling) { + fprintf(stderr, "[ladder] refusing %d decode slots: memory-pressure " + "ceiling is %d\n", n_slots, slot_ceiling); + return -1; + } + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return -1; + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + size_t conv_bytes = (size_t)(cfg->ssm_conv - 1) * cfg->conv_dim * + sizeof(float); + size_t ssm_bytes = (size_t)cfg->ssm_state * cfg->ssm_state * + cfg->ssm_dt_rank * sizeof(float); + + if (g->n_slots != n_slots) { + /* Slot count changed: every previously bound slot is void. */ + memset(g->slot_state, 0, sizeof(g->slot_state)); + g->n_slots = n_slots; + } + + /* Phase 10b (lazy mirror): this bind may hand the slot to a DIFFERENT + * session. If the outgoing session's only KV copy is this slot's rings, + * materialize it into its host mirror now — once the incoming session's + * first step re-seeds the ring, those rows are gone. This is the "park" + * copy: it is the one lazy-mode host materialization that is not driven + * by an explicit consumer, it is bounded by the rows actually decoded, + * and it only fires when sessions outnumber ring slots. */ + if (qw_kv_mirror_lazy()) { + for (int i = 0; i < g->n_attn; i++) { + gpu_attn_layer *da = &g->attn[i]; + pn_qwen35moe_kv *okv = da->kv_slot_kv[slot]; + if (!okv || !da->kv_slot_host_id[slot] || !okv->kv_mirror_stale) + continue; + if (okv == &s->kv[i]) + continue; /* same session re-binding: rows stay valid */ + const void *oid = okv->q4 ? (const void *)okv->kq + : (const void *)okv->k; + if (oid != da->kv_slot_host_id[slot] || + okv->content_gen != da->kv_slot_gen[slot]) + continue; /* bookkeeping no longer describes this kv */ + qw_kv_layer_materialize(da, okv, da->kv_slot_synced[slot]); + } + } + + /* Walk the layers in die order (front then back) to sync across the + * die boundary. No gpu_select_die / bs_acquire here: the K80 strap-quirk + * maps all dies simultaneously (each die has its own persistent fc_cuda_ctx) + * so there is no MMIO slot to acquire. Every fc_buffer_*_ctx call below + * already targets d->ctx directly — barswap is never needed on this path. + * (Mirrors the async forward: "No gpu_select_die needed — async requires + * small-BAR mode, where every die is live behind its own context.") */ + fc_cuda_ctx *cur = NULL; + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_SSM) + continue; + gpu_ssm_layer *d = &g->ssm[pn_qwen35moe_model_layer_index(m, layer)]; + pn_qwen35moe_ssm_state *st = &s->ssm[pn_qwen35moe_model_layer_index(m, layer)]; + if (d->ctx != cur) { + if (cur) + fc_sync_ctx(cur); /* flush previous die before switching */ + cur = d->ctx; + } + if (d->slots_cap < n_slots) { + fc_buffer_free(d->conv_slots); + fc_buffer_free(d->ssm_slots); + d->conv_slots = fc_buffer_alloc_ctx(d->ctx, + (size_t)n_slots * conv_bytes); + d->ssm_slots = fc_buffer_alloc_ctx(d->ctx, + (size_t)n_slots * ssm_bytes); + d->slots_cap = (d->conv_slots && d->ssm_slots) ? n_slots : 0; + if (!d->slots_cap) + return -1; + /* L9 — the slot state buffers just moved: any captured slots + * graph holds their old addresses. Drop it so the next decode + * step recaptures against the new allocation. */ + if (d->slots_graph) { + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + } + pn_graph_capture_dropped(&d->slots_graph_capture); + d->slots_graph_n = 0; + } + if (d->conv_state_g && d->host_state == (const void *)st) { + /* Live GPU state belongs to this user — D2D snapshot. */ + fc_buffer_copy_d2d_ctx(d->ctx, d->conv_slots, + (size_t)slot * conv_bytes, + d->conv_state_g, 0, conv_bytes); + fc_buffer_copy_d2d_ctx(d->ctx, d->ssm_slots, + (size_t)slot * ssm_bytes, + d->ssm_state_g, 0, ssm_bytes); + } else { + /* Host mirror (fresh state / CPU-prefilled state only). The fc + * API has no offset H2D, so bounce through the live state_g + * buffers (allocating them if this model never decoded). */ + if (!d->conv_state_g) + d->conv_state_g = fc_buffer_alloc_ctx(d->ctx, conv_bytes); + if (!d->ssm_state_g) + d->ssm_state_g = fc_buffer_alloc_ctx(d->ctx, ssm_bytes); + if (!d->conv_state_g || !d->ssm_state_g) + return -1; + fc_buffer_from_cpu(d->conv_state_g, st->conv_state); + fc_buffer_from_cpu(d->ssm_state_g, st->ssm_state); + d->host_state = (const void *)st; + fc_buffer_copy_d2d_ctx(d->ctx, d->conv_slots, + (size_t)slot * conv_bytes, + d->conv_state_g, 0, conv_bytes); + fc_buffer_copy_d2d_ctx(d->ctx, d->ssm_slots, + (size_t)slot * ssm_bytes, + d->ssm_state_g, 0, ssm_bytes); + } + } + if (cur) + fc_sync_ctx(cur); + + /* L7 — EAGER mode: invalidate this slot's KV-ring watermark on every + * attention layer; a bind can put a RESET+re-prefilled state (same + * pointer, new rows) into the slot, and the pointer alone cannot see + * that. The host mirror is authoritative at bind time (bs_slot_bind + * runs state_to_host), so the next flash step re-uploads [0, pos). */ + if (pn_qwen35moe_host_kv_mirror_eager()) { + for (int i = 0; i < g->n_attn; i++) + g->attn[i].kv_slot_host_id[slot] = NULL; + } else { + /* Phase 10b LAZY mode: hand the incoming state's KV rows to the + * slot ring NOW, while the single-user ring still holds its + * prefill — this bind runs under the same GPU turn that produced + * it. Deferring the seed to the first group step is unsafe under + * concurrency: another session's prefill reuses the single-user + * ring in between (measured on a K80 host: a 16-way blast erred out every + * slot with "no ring source"). A warm rebind of content the slot + * already holds keeps its rows; anything unseedable falls back to + * materializing the host mirror so the first step's legacy + * mirror-upload path works. */ + for (int i = 0; i < g->n_attn; i++) { + gpu_attn_layer *da = &g->attn[i]; + pn_qwen35moe_kv *kv = &s->kv[i]; + const void *id = kv->q4 ? (const void *)kv->kq + : (const void *)kv->k; + if (da->kv_slot_host_id[slot] == id && + da->kv_slot_gen[slot] == kv->content_gen) + continue; /* warm rebind: the slot already holds it */ + int rows = (da->k_cache_g && da->v_cache_g && + da->cache_cap == kv->max_seq && + da->kv_host_id == id && + da->kv_gen == kv->content_gen) + ? da->kv_synced_pos : 0; + int seeded = 0; + if (rows > 0 && + gpu_attn_slot_ring_ensure(da, cfg, kv->q4, kv->max_seq, + n_slots, i) == 0 && + da->kv_slots_cap_n > slot && + da->kv_slots_cap_seq == kv->max_seq) { + size_t row_b = qw_kv_row_bytes(kv); + size_t head_b = (size_t)kv->max_seq * row_b; + for (int hd = 0; hd < kv->n_head_kv; hd++) { + size_t src_off = (size_t)hd * head_b; + size_t dst_off = ((size_t)slot * kv->n_head_kv + hd) * + head_b; + fc_buffer_copy_d2d_ctx(da->ctx, da->k_slots_g, dst_off, + da->k_cache_g, src_off, + (size_t)rows * row_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->v_slots_g, dst_off, + da->v_cache_g, src_off, + (size_t)rows * row_b); + } + if (kv->q4 && da->k_sink_g && da->k_sink_slots_g) { + size_t sink_b = qw_kv_sink_bytes(kv); + size_t win_b = qw_kv_win_bytes(kv); + fc_buffer_copy_d2d_ctx(da->ctx, da->k_sink_slots_g, + (size_t)slot * sink_b, + da->k_sink_g, 0, sink_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->v_sink_slots_g, + (size_t)slot * sink_b, + da->v_sink_g, 0, sink_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->k_win_slots_g, + (size_t)slot * win_b, + da->k_win_g, 0, win_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->v_win_slots_g, + (size_t)slot * win_b, + da->v_win_g, 0, win_b); + } + da->kv_slot_host_id[slot] = id; + da->kv_slot_gen[slot] = kv->content_gen; + da->kv_slot_synced[slot] = rows; + da->kv_slot_kv[slot] = kv; + seeded = 1; + } + if (!seeded) { + if (kv->kv_mirror_stale && + qw_kv_layer_materialize(da, kv, -1) != 0 && + kv->kv_mirror_stale) { + fprintf(stderr, "[attn] slot_bind kv_idx=%d slot=%d: no " + "ring source to seed or materialize — first " + "step will refuse if rows are needed\n", + i, slot); + } + da->kv_slot_host_id[slot] = NULL; + da->kv_slot_kv[slot] = NULL; + } + } + for (int i = 0; i < g->n_die; i++) + fc_sync_ctx(g->ctx[i]); + } + + g->slot_state[slot] = (const void *)s; + return 0; +} + +/* Move a DECODE slot's live SSM state from GPU index `src` to `dst` without + * going through the host mirror (which is stale for in-flight decode users). + * Used by the dynamic-width batch scheduler (Phase D2) to compactly remap + * the active user set to [0, n_active) when a user joins or leaves. + * + * Copies conv_slots[src] → conv_slots[dst] and ssm_slots[src] → ssm_slots[dst] + * (D2D) for every SSM layer, then updates slot_state[dst] = slot_state[src] + * and clears slot_state[src]. The dst slot's top-k bookkeeping is reset. + * If src == dst, only updates slot_state bookkeeping (no-op copy). + * + * Must be called under gpu_mu (no decode step or bind can interleave). + * The source state continues to live at slot_state[src] until the caller + * updates the GPU map; the caller must clear slot_state[src] afterwards + * (done here: src entry is set to NULL). + * + * Returns 0 on success, -1 if the slots are out of range or not allocated. */ +int pn_qwen35moe_decode_slot_move_cuda(pn_qwen35moe_model *m, + int src, int dst, int n_slots) +{ + if (!m || src < 0 || dst < 0 || src >= n_slots || dst >= n_slots || + n_slots > PN_QWEN35MOE_GPU_MAX_SLOTS) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g || g->n_slots < n_slots) + return -1; + if (!g->slot_state[src]) + return -1; /* source slot not bound */ + + if (src == dst) { + /* No data move needed; slot_state bookkeeping is already correct. */ + return 0; + } + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + size_t conv_bytes = (size_t)(cfg->ssm_conv - 1) * cfg->conv_dim * + sizeof(float); + size_t ssm_bytes = (size_t)cfg->ssm_state * cfg->ssm_state * + cfg->ssm_dt_rank * sizeof(float); + + fc_cuda_ctx *cur = NULL; + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_SSM) + continue; + int idx = pn_qwen35moe_model_layer_index(m, layer); + gpu_ssm_layer *d = &g->ssm[idx]; + if (!d->conv_slots || !d->ssm_slots || d->slots_cap < n_slots) + return -1; /* slots not allocated */ + if (d->ctx != cur) { + if (cur) + fc_sync_ctx(cur); /* flush previous die */ + cur = d->ctx; + } + fc_buffer_copy_d2d_ctx(d->ctx, + d->conv_slots, (size_t)dst * conv_bytes, + d->conv_slots, (size_t)src * conv_bytes, + conv_bytes); + fc_buffer_copy_d2d_ctx(d->ctx, + d->ssm_slots, (size_t)dst * ssm_bytes, + d->ssm_slots, (size_t)src * ssm_bytes, + ssm_bytes); + } + if (cur) + fc_sync_ctx(cur); + + /* L7 — attn slot KV rings. + * + * EAGER mode: invalidate the DST watermark on every attention layer + * instead of moving ring bytes; the host mirror is current every step, + * so the next flash step re-uploads the moved user's rows [0, pos) from + * it — a one-off H2D per join-compaction, not a per-step cost. + * + * Phase 10b LAZY mode: the mirror does not hold the rows, the ring + * does — move them D2D (src slot slice -> dst slot slice, same die) and + * carry the (pointer, generation, watermark) bookkeeping, so the moved + * user's next step re-uploads nothing at all. */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_ATTN) + continue; + gpu_attn_layer *da = &g->attn[pn_qwen35moe_model_layer_index(m, layer)]; + int carried = 0; + if (qw_kv_mirror_lazy() && da->k_slots_g && da->v_slots_g && + da->kv_slot_host_id[src] && da->kv_slot_kv[src] && + da->kv_slots_cap_n > src && da->kv_slots_cap_n > dst) { + const pn_qwen35moe_kv *mkv = da->kv_slot_kv[src]; + size_t row_b = qw_kv_row_bytes(mkv); + size_t head_b = (size_t)da->kv_slots_cap_seq * row_b; + int rows = da->kv_slot_synced[src]; + if (rows > da->kv_slots_cap_seq) + rows = da->kv_slots_cap_seq; + for (int hd = 0; rows > 0 && hd < mkv->n_head_kv; hd++) { + size_t src_off = ((size_t)src * mkv->n_head_kv + hd) * head_b; + size_t dst_off = ((size_t)dst * mkv->n_head_kv + hd) * head_b; + fc_buffer_copy_d2d_ctx(da->ctx, da->k_slots_g, dst_off, + da->k_slots_g, src_off, + (size_t)rows * row_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->v_slots_g, dst_off, + da->v_slots_g, src_off, + (size_t)rows * row_b); + } + if (mkv->q4 && da->k_sink_slots_g) { + size_t sink_b = qw_kv_sink_bytes(mkv); + size_t win_b = qw_kv_win_bytes(mkv); + fc_buffer_copy_d2d_ctx(da->ctx, da->k_sink_slots_g, + (size_t)dst * sink_b, + da->k_sink_slots_g, + (size_t)src * sink_b, sink_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->v_sink_slots_g, + (size_t)dst * sink_b, + da->v_sink_slots_g, + (size_t)src * sink_b, sink_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->k_win_slots_g, + (size_t)dst * win_b, + da->k_win_slots_g, + (size_t)src * win_b, win_b); + fc_buffer_copy_d2d_ctx(da->ctx, da->v_win_slots_g, + (size_t)dst * win_b, + da->v_win_slots_g, + (size_t)src * win_b, win_b); + } + fc_sync_ctx(da->ctx); + da->kv_slot_host_id[dst] = da->kv_slot_host_id[src]; + da->kv_slot_gen[dst] = da->kv_slot_gen[src]; + da->kv_slot_kv[dst] = da->kv_slot_kv[src]; + da->kv_slot_synced[dst] = da->kv_slot_synced[src]; + carried = 1; + } + if (!carried) + da->kv_slot_host_id[dst] = NULL; + da->kv_slot_host_id[src] = NULL; + da->kv_slot_kv[src] = NULL; + } + + /* Update slot_state bookkeeping. */ + g->slot_state[dst] = g->slot_state[src]; + g->slot_state[src] = NULL; + /* No sparse-top-k bookkeeping to move: it rides on the user's state, not + * on the slot index, so a slot move cannot desynchronise it. */ + return 0; +} + +/* Batched-decode SSM block: identical launch sequence to the prefill + * gpu_ssm_block_batch except the conv1d and recurrence are SLOT kernels — + * K independent single-step updates against the layer's slot-major state + * (no scan; each user advances one position). */ +static int gpu_ssm_block_slots(gpu_ssm_layer *d, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int slot_base, int nslots) +{ + const int conv_dim = cfg->conv_dim; + const int S = cfg->ssm_state; + const int n_k_heads = cfg->ssm_n_group; + const int n_v_heads = cfg->ssm_dt_rank; + const int kv_groups = n_v_heads / n_k_heads; + const int key_dim = S * n_k_heads; + const int val_dim = S * n_v_heads; + const float eps = cfg->rms_norm_eps; + fc_cuda_ctx *ctx = d->ctx; + + /* L5 — offset the persistent per-slot SSM rings to this group's base so + * the slot kernels (which index conv/ssm state by their block slot idx + * 0..nslots) land on rows [slot_base, slot_base+nslots). The batch + * scratch (b->*) stays group-local [0,nslots). slot_base==0 => the ring + * view is byte-identical to the ring itself (today's behavior). */ + const size_t conv_stride = (size_t)(cfg->ssm_conv - 1) * conv_dim * + sizeof(float); + const size_t ssm_stride = (size_t)S * S * n_v_heads * sizeof(float); + fc_buffer *conv_ring = d->conv_slots; + fc_buffer *ssm_ring = d->ssm_slots; + fc_buffer *conv_view = NULL, *ssm_view = NULL; + if (slot_base > 0) { + conv_view = fc_buffer_offset_view(d->conv_slots, + (size_t)slot_base * conv_stride); + ssm_view = fc_buffer_offset_view(d->ssm_slots, + (size_t)slot_base * ssm_stride); + if (!conv_view || !ssm_view) { + fc_buffer_free_view(conv_view); + fc_buffer_free_view(ssm_view); + return -1; + } + conv_ring = conv_view; + ssm_ring = ssm_view; + } + + double sp_t0 = 0.0; + sprof_begin(ctx, &sp_t0); + + if (gpu_matvec_nx(&d->attn_qkv, b->x_norm, b->s_qkv, nslots) < 0 || + gpu_matvec_nx(&d->attn_gate, b->x_norm, b->s_z, nslots) < 0 || + gpu_matvec_nx(&d->ssm_beta, b->x_norm, b->s_beta, nslots) < 0 || + gpu_matvec_nx(&d->ssm_alpha, b->x_norm, b->s_alpha, nslots) < 0) { + fc_buffer_free_view(conv_view); + fc_buffer_free_view(ssm_view); + return -1; + } + sprof_lap(SPROF_DENSE, ctx, &sp_t0); + + /* Sub-span clock is SEPARATE from sp_t0 on purpose: the five laps below + * partition SPROF_SSM without shortening it, exactly as ssm_lap does for + * BPROF_SSM on the prefill path. */ + double sub_t0 = 0.0; + sub_begin(ctx, &sub_t0); + + fc_sigmoid_ctx(ctx, b->s_beta, nslots * n_v_heads); + fc_ssm_decay_gate_nx_ctx(ctx, b->s_alpha, d->ssm_dt.buf, d->ssm_a.buf, + b->s_g, n_v_heads, nslots); + sub_lap(&ssmd_cum[SSMD_DECAY], &ssmd_laps[SSMD_DECAY], ctx, &sub_t0); + + /* Per-slot single-step conv1d + recurrence over the slot-major state. */ + fc_ssm_conv1d_silu_slots_ctx(ctx, b->s_qkv, d->ssm_conv1d.buf, + conv_ring, b->s_conv, conv_dim, + cfg->ssm_conv, nslots); + sub_lap(&ssmd_cum[SSMD_CONV], &ssmd_laps[SSMD_CONV], ctx, &sub_t0); + fc_ssm_qknorm_gqa_nx_ctx(ctx, b->s_conv, b->s_qg, b->s_kg, b->s_vg, + n_k_heads, kv_groups, S, key_dim, val_dim, + eps, nslots); + sub_lap(&ssmd_cum[SSMD_QKNORM], &ssmd_laps[SSMD_QKNORM], ctx, &sub_t0); + fc_ssm_recurrence_slots_ctx(ctx, ssm_ring, b->s_qg, b->s_kg, + b->s_vg, b->s_g, b->s_beta, b->s_y, + n_v_heads, S, nslots, + 1.0f / sqrtf((float)S)); + sub_lap(&ssmd_cum[SSMD_RECUR], &ssmd_laps[SSMD_RECUR], ctx, &sub_t0); + + fc_ssm_gated_norm_ctx(ctx, b->s_y, d->ssm_norm.buf, b->s_z, b->s_gated, + nslots * n_v_heads, S, eps); + sub_lap(&ssmd_cum[SSMD_GNORM], &ssmd_laps[SSMD_GNORM], ctx, &sub_t0); + sprof_lap(SPROF_SSM, ctx, &sp_t0); + fc_buffer_free_view(conv_view); + fc_buffer_free_view(ssm_view); + if (gpu_matvec_nx(&d->ssm_out, b->s_gated, b->block_out, nslots) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, + nslots * cfg->n_embd); + sprof_lap(SPROF_DENSE, ctx, &sp_t0); + return 0; +} + +/* QWEN35_FLASH_SLOTS gate (default ON; =0 keeps the host-softmax slots path; + * A/B baseline). Routes the batched-decode attention through the on-die + * per-slot split flash kernels (L7). */ +static int flash_slots_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_FLASH_SLOTS"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* Host mirror of the decode split plan (fc_attn_dec_plan / the + * gpu_attn_block_decode_flash formula): n_splits for one user's seq_len. + * The slot kernels recompute each user's plan on-device; the host only + * needs the MAX across users to size the launch. */ +static int gpu_attn_slot_plan_splits(int seq_len, int kps) +{ + int n_splits = (seq_len + kps - 1) / kps; + if (n_splits < 1) + n_splits = 1; + if (n_splits > PN_QWEN35MOE_DEC_SPLIT_MAX) + n_splits = PN_QWEN35MOE_DEC_SPLIT_MAX; + int split_len = (seq_len + n_splits - 1) / n_splits; + split_len = (split_len + 15) & ~15; + return (seq_len + split_len - 1) / split_len; +} + +/* Ensure this layer's per-slot KV rings ([nslots][n_head_kv][max_seq][row] + + * q4 fp16 shadows) and the per-slot decode partial scratch exist for + * (nslots, max_seq, q4). A geometry change reallocates and invalidates every + * slot watermark — safe, because the slots path keeps the host mirror + * current, so the next step re-uploads each user's rows from it. Returns + * 0 = ready, 1 = unavailable (allocation failed; caller uses the host + * path). */ +static int gpu_attn_slot_ring_ensure(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + int q4, int max_seq, int nslots, + int kv_idx) +{ + fc_cuda_ctx *ctx = d->ctx; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + size_t row_b = q4 ? (size_t)pn_kvq4_row_bytes(head_dim) + : (size_t)head_dim * sizeof(float); + + if (!d->k_slots_g || d->kv_slots_cap_seq != max_seq || + d->kv_slots_cap_n < nslots || d->kv_slots_q4 != q4) { + size_t sink_b = q4 ? (size_t)nslots * n_head_kv * PN_KVQ4_SINKMAX * + head_dim * sizeof(uint16_t) : 0; + size_t win_b = q4 ? (size_t)nslots * n_head_kv * PN_KVQ4_WMAX * + head_dim * sizeof(uint16_t) : 0; + size_t bytes = (size_t)nslots * n_head_kv * max_seq * row_b; + size_t need = 2 * bytes + 2 * sink_b + 2 * win_b; + + /* A geometry STRICTLY SMALLER than the one that failed is a different + * question, so the recorded verdict does not describe it: clear the + * budget at once. This is what makes dropping K or ctx_cap recover the + * on-die path immediately — no backoff to wait out, no reload. */ + int same_or_larger = (d->kv_slots_fail_n && q4 == d->kv_slots_fail_q4 && + nslots >= d->kv_slots_fail_n && + max_seq >= d->kv_slots_fail_seq); + if (d->kv_slots_fail_n && !same_or_larger) { + d->kv_slots_fail_seq = d->kv_slots_fail_n = d->kv_slots_fail_q4 = 0; + pn_attn_ring_geometry_shrank(&d->path); + } + + /* BOUNDED retry, not a latch (gpu_attn_path_tracker.h). A failure that + * is merely contention gets 4 more chances on an 8/32/128/512 + * eligible-step backoff; one that cannot fit an empty die stops at + * once. On a step where the answer is "not now" nothing is freed and + * nothing is allocated, so we return BEFORE the free below and + * slots_ring_gen does not move — claiming the ring addresses shifted on + * a step where they did not is what turned the un-latched retry into a + * 62% collapse (afcad12): it invalidates every layer's captured graph, + * every step. */ + if (!pn_attn_ring_should_attempt(&d->path)) { + pn_attn_path_host(&d->path, PN_ATTN_HOST_RING_ALLOC); + return 1; + } + /* Captured BEFORE the frees below: whether this attempt actually moves + * any ring address, and so whether slots_ring_gen may bump. */ + const int had_rings = (d->k_slots_g != NULL); + /* Phase 10b (lazy mirror): the rings about to be freed may hold the + * ONLY copy of live sessions' KV rows (the mirror is not maintained + * per token). Rescue every bound slot's rows into its host mirror + * before the free — bounded by the rows actually decoded, and rare + * (a geometry change, not a per-step event). */ + if (qw_kv_mirror_lazy() && d->k_slots_g) { + for (int su = 0; su < d->kv_slots_cap_n; su++) { + pn_qwen35moe_kv *okv = d->kv_slot_kv[su]; + if (!okv || !d->kv_slot_host_id[su] || !okv->kv_mirror_stale) + continue; + const void *oid = okv->q4 ? (const void *)okv->kq + : (const void *)okv->k; + if (oid != d->kv_slot_host_id[su] || + okv->content_gen != d->kv_slot_gen[su] || + okv->max_seq != d->kv_slots_cap_seq) + continue; /* bookkeeping no longer describes this kv */ + qw_kv_layer_materialize(d, okv, d->kv_slot_synced[su]); + } + } + fc_buffer_free(d->k_slots_g); + fc_buffer_free(d->v_slots_g); + fc_buffer_free(d->k_sink_slots_g); + fc_buffer_free(d->v_sink_slots_g); + fc_buffer_free(d->k_win_slots_g); + fc_buffer_free(d->v_win_slots_g); + d->k_slots_g = d->v_slots_g = NULL; + d->k_sink_slots_g = d->v_sink_slots_g = NULL; + d->k_win_slots_g = d->v_win_slots_g = NULL; + memset(d->kv_slot_host_id, 0, sizeof(d->kv_slot_host_id)); + memset(d->kv_slot_gen, 0, sizeof(d->kv_slot_gen)); + memset(d->kv_slot_kv, 0, sizeof(d->kv_slot_kv)); + + d->k_slots_g = fc_buffer_alloc_ctx(ctx, bytes); + d->v_slots_g = fc_buffer_alloc_ctx(ctx, bytes); + int ok = (d->k_slots_g && d->v_slots_g); + if (ok && q4) { + d->k_sink_slots_g = fc_buffer_alloc_ctx(ctx, sink_b); + d->v_sink_slots_g = fc_buffer_alloc_ctx(ctx, sink_b); + d->k_win_slots_g = fc_buffer_alloc_ctx(ctx, win_b); + d->v_win_slots_g = fc_buffer_alloc_ctx(ctx, win_b); + ok = (d->k_sink_slots_g && d->v_sink_slots_g && + d->k_win_slots_g && d->v_win_slots_g); + } + /* Test-only fault injection: hand back the memory and report failure, + * so the retry path is exercisable on real hardware without having to + * genuinely exhaust a die. Default off, one cached compare. */ + if (ok && pn_attn_ring_inject_now(&d->path)) { + fc_buffer_free(d->k_slots_g); + fc_buffer_free(d->v_slots_g); + fc_buffer_free(d->k_sink_slots_g); + fc_buffer_free(d->v_sink_slots_g); + fc_buffer_free(d->k_win_slots_g); + fc_buffer_free(d->v_win_slots_g); + d->k_slots_g = d->v_slots_g = NULL; + d->k_sink_slots_g = d->v_sink_slots_g = NULL; + d->k_win_slots_g = d->v_win_slots_g = NULL; + ok = 0; + } + d->kv_slots_cap_seq = ok ? max_seq : 0; + d->kv_slots_cap_n = ok ? nslots : 0; + d->kv_slots_q4 = q4; + /* Only claim the addresses moved when they actually did. A retry that + * found NULL rings and failed again moved nothing, and bumping the + * generation anyway would drop every layer's captured graph. */ + if (ok || had_rings) + d->slots_ring_gen++; + /* QWEN35_SDIAG: report what the ring allocation ASKED for and whether + * it landed. This branch only runs on a geometry change, so it is at + * most one line per attention layer per bind — the direct evidence for + * "do the flash slot rings fit at this ctx_cap". */ + if (sdiag_on()) + fprintf(stderr, "[sdiag] ring_ensure kv_idx=%d nslots=%d " + "max_seq=%d q4=%d row_b=%zu k+v=%.1f MiB/slot " + "total=%.1f MiB -> %s\n", + kv_idx, nslots, max_seq, q4, row_b, + 2.0 * (double)(bytes / nslots) / (1024.0 * 1024.0), + 2.0 * (double)bytes / (1024.0 * 1024.0), + ok ? "OK" : "ALLOC FAILED (host softmax fallback)"); + if (!ok) { + /* Transient or impossible? ASK THE DIE rather than guessing. A + * request that exceeds this die's whole usable VRAM would fail on + * an EMPTY die, so there is nothing to wait for and retrying it + * four times is four rounds of pointless multi-hundred-MiB + * cuMemAllocs. Anything smaller is contention — another model + * loading, a sibling instance, a bigger geometry not yet released — + * and contention is exactly what a bounded retry is for. */ + size_t die_free = 0, die_total = 0; + int impossible = 0; + if (pn_attn_ring_inject_now(&d->path)) { + /* Injected fault: the die is fine, so there is nothing to + * measure and the log line below honestly reports 0 / 0 MiB. + * Test-only path — QWEN35_RING_ALLOC_FAIL_INJECT is unset in + * production, where the real reading is always taken. */ + impossible = pn_attn_ring_inject_impossible(); + } else if (fc_mem_info_ctx(ctx, &die_free, &die_total) == 0 && + die_total > 0) { + uint64_t reserve = pn_die_reserve_bytes(); + uint64_t usable = (uint64_t)die_total > reserve + ? (uint64_t)die_total - reserve : 0; + impossible = ((uint64_t)need > usable); + } + d->kv_slots_fail_seq = max_seq; + d->kv_slots_fail_n = nslots; + d->kv_slots_fail_q4 = q4; + pn_attn_ring_alloc_failed(&d->path, impossible); + pn_attn_path_host(&d->path, PN_ATTN_HOST_RING_ALLOC); + /* Say so ONCE per failure, unconditionally — silence is what let + * the 62% collapse hide behind healthy-looking throughput logs. + * The retry budget bounds this to PN_ATTN_RING_MAX_FAILURES lines + * per layer, and naming the verdict makes a transient squeeze + * visibly different from a geometry that can never fit. */ + fprintf(stderr, "[attn] kv_idx=%d: on-die KV ring allocation " + "FAILED (%d slots x %d ctx = %.0f MiB, die free %.0f / " + "total %.0f MiB); this layer falls back to HOST softmax. " + "%s (attempt %d/%d). Lower ctx_cap or max_sessions to keep " + "attention on-die.\n", + kv_idx, nslots, max_seq, + (double)need / (1024.0 * 1024.0), + (double)die_free / (1024.0 * 1024.0), + (double)die_total / (1024.0 * 1024.0), + impossible ? "IMPOSSIBLE on this die — not retrying" + : (d->path.alloc_gave_up + ? "retry budget spent — not retrying" + : "will retry on a backoff"), + (int)d->path.alloc_failures, PN_ATTN_RING_MAX_FAILURES); + return 1; /* OOM: the host path still works */ + } + /* Allocation succeeded at this geometry — clear the failure record. */ + d->kv_slots_fail_seq = d->kv_slots_fail_n = d->kv_slots_fail_q4 = 0; + } + + if (!d->dec_slots_acc_g || d->dec_slots_cap < nslots) { + /* Same bounded-retry rule as the rings above, sharing this layer's + * budget: both allocations have to land for the layer to be on-die, so + * one budget describes the layer. A smaller slot count is a different + * question and resets it. */ + if (d->dec_slots_fail_n && nslots < d->dec_slots_fail_n) { + d->dec_slots_fail_n = 0; + pn_attn_ring_geometry_shrank(&d->path); + } + if (!pn_attn_ring_should_attempt(&d->path)) { + pn_attn_path_host(&d->path, PN_ATTN_HOST_DEC_SCRATCH); + return 1; + } + const int had_dec = (d->dec_slots_acc_g != NULL); + fc_buffer_free(d->dec_slots_m_g); + fc_buffer_free(d->dec_slots_l_g); + fc_buffer_free(d->dec_slots_acc_g); + size_t np = (size_t)nslots * cfg->n_head * PN_QWEN35MOE_DEC_SPLIT_MAX; + d->dec_slots_m_g = fc_buffer_alloc_ctx(ctx, np * sizeof(float)); + d->dec_slots_l_g = fc_buffer_alloc_ctx(ctx, np * sizeof(float)); + d->dec_slots_acc_g = fc_buffer_alloc_ctx(ctx, + np * (size_t)head_dim * + sizeof(float)); + d->dec_slots_cap = (d->dec_slots_m_g && d->dec_slots_l_g && + d->dec_slots_acc_g) ? nslots : 0; + /* Same rule as the rings: an attempt that moved nothing must not claim + * the scratch address changed. */ + if (d->dec_slots_cap || had_dec) + d->slots_ring_gen++; + if (!d->dec_slots_cap) { + size_t dec_need = np * (2 * sizeof(float) + + (size_t)head_dim * sizeof(float)); + size_t die_free = 0, die_total = 0; + int impossible = 0; + if (fc_mem_info_ctx(ctx, &die_free, &die_total) == 0 && + die_total > 0) { + uint64_t reserve = pn_die_reserve_bytes(); + uint64_t usable = (uint64_t)die_total > reserve + ? (uint64_t)die_total - reserve : 0; + impossible = ((uint64_t)dec_need > usable); + } + d->dec_slots_fail_n = nslots; + pn_attn_ring_alloc_failed(&d->path, impossible); + pn_attn_path_host(&d->path, PN_ATTN_HOST_DEC_SCRATCH); + fprintf(stderr, "[attn] kv_idx=%d: decode partial-scratch " + "allocation FAILED at %d slots (%.0f MiB, die free %.0f " + "MiB); this layer falls back to HOST softmax. %s " + "(attempt %d/%d).\n", kv_idx, nslots, + (double)dec_need / (1024.0 * 1024.0), + (double)die_free / (1024.0 * 1024.0), + impossible ? "IMPOSSIBLE on this die — not retrying" + : (d->path.alloc_gave_up + ? "retry budget spent — not retrying" + : "will retry on a backoff"), + (int)d->path.alloc_failures, PN_ATTN_RING_MAX_FAILURES); + if (sdiag_on()) + fprintf(stderr, "[sdiag] ring_ensure kv_idx=%d nslots=%d " + "decode partial scratch ALLOC FAILED\n", + kv_idx, nslots); + return 1; + } + d->dec_slots_fail_n = 0; + } + /* Both allocations are in place at this geometry: the layer is provably + * fittable, so a LATER failure starts with a fresh budget. */ + pn_attn_ring_alloc_ok(&d->path); + return 0; +} + +/* QWEN35_ASYNC_KVMIRROR gate (default OFF). When on, gpu_attn_block_slots_flash + * step 6 defers its host-KV-mirror store instead of blocking per attention + * layer (see qw_kv_defer). Process-constant. */ +static int async_kvmirror_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_ASYNC_KVMIRROR"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* Map a ctx to its per-die deferred-mirror slot. An unrecognised context + * falls back to slot 0 the way the old two-way expression did. */ +static int qw_defer_idx(pn_qwen35moe_gpu *g, fc_cuda_ctx *ctx) +{ + int i = gpu_die_index_of_ctx(g, ctx); + return i >= 0 ? i : 0; +} + +/* Queue one attention layer's host-KV-mirror store for deferred replay. Stages + * the roped K/V (already async-D2H'd into the die arena at stage_off by the + * caller) plus all the metadata the store loop needs. Grows the descriptor + * array as needed. Returns the descriptor, or NULL on OOM. */ +static qw_kv_store *qw_defer_push(qw_kv_defer *df) +{ + if (df->n >= df->n_cap) { + int nc = df->n_cap ? df->n_cap * 2 : 64; + qw_kv_store *ns = realloc(df->stores, (size_t)nc * sizeof(*ns)); + if (!ns) + return NULL; + df->stores = ns; + df->n_cap = nc; + } + return &df->stores[df->n++]; +} + +/* Pre-size a die's staging arena for a whole decode step and reset its + * watermarks. Called at step start for each active die so the arena is NEVER + * realloc'd mid-step (an in-flight async DtoH writes into it — moving it would + * corrupt earlier layers' queued copies). `n_attn` attention layers, each + * staging 2*nslots*kv_dim floats. Returns 0 on success, -1 on OOM. */ +static int qw_defer_reset(qw_kv_defer *df, int n_attn, int nslots, int kv_dim) +{ + df->n = 0; + df->stage_used = 0; + size_t need = (size_t)n_attn * 2 * nslots * (size_t)kv_dim * sizeof(float); + if (need > df->stage_bytes) { + float *ns = realloc(df->stage, need); + if (!ns) + return -1; + df->stage = ns; + df->stage_bytes = need; + } + return 0; +} + +/* Hand out `bytes` of pre-sized per-die host staging arena for one layer's + * K||V. The arena is reserved up front by qw_defer_reset, so this never + * reallocs (which would invalidate in-flight async DtoH destinations). Returns + * the byte offset, or (size_t)-1 if the pre-sized arena is exhausted. */ +static size_t qw_defer_stage(qw_kv_defer *df, size_t bytes) +{ + if (df->stage_used + bytes > df->stage_bytes) + return (size_t)-1; /* pre-sizing was insufficient — caller falls back */ + size_t off = df->stage_used; + df->stage_used += bytes; + return off; +} + +/* Flush the deferred host-KV-mirror stores for one die: ONE fc_sync_ctx drains + * all the async D2H copies queued this step, then replay every pending store + * into the host mirror (byte-identical to the per-layer store loop). Clears the + * pending list + arena watermark for the next step. */ +static void qw_defer_flush(pn_qwen35moe_gpu *g, int idx, fc_cuda_ctx *ctx) +{ + qw_kv_defer *df = &g->defer[idx]; + if (df->n == 0) { + df->stage_used = 0; + return; + } + fc_sync_ctx(ctx); /* drain every async DtoH queued this step */ + for (int s = 0; s < df->n; s++) { + qw_kv_store *st = &df->stores[s]; + const float *hb_kv = df->stage + st->stage_off / sizeof(float); + for (int u = 0; u < st->nslots; u++) { + pn_qwen35moe_kv *kv = st->kv[u]; + int cache_pos = st->pos[u] % st->max_seq; + const float *kvec = hb_kv + (size_t)u * st->kv_dim; + const float *vvec = hb_kv + (size_t)(st->nslots + u) * st->kv_dim; + for (int hd = 0; hd < st->n_head_kv; hd++) { + if (st->kv_q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, hd, + st->max_seq, cache_pos, + kvec + (size_t)hd * st->head_dim, + st->head_dim, st->kv_sink, st->kv_win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, hd, + st->max_seq, cache_pos, + vvec + (size_t)hd * st->head_dim, + st->head_dim, st->kv_sink, st->kv_win); + } else { + memcpy(kv->k + ((size_t)hd * st->max_seq + cache_pos) * + st->head_dim, + kvec + (size_t)hd * st->head_dim, + (size_t)st->head_dim * sizeof(float)); + memcpy(kv->v + ((size_t)hd * st->max_seq + cache_pos) * + st->head_dim, + vvec + (size_t)hd * st->head_dim, + (size_t)st->head_dim * sizeof(float)); + } + } + } + } + df->n = 0; + df->stage_used = 0; +} + +/* Free the per-die deferred-mirror arenas (teardown). */ +static void qw_defer_free(pn_qwen35moe_gpu *g) +{ + for (int i = 0; i < PN_DIE_LAYER_SPLIT_MAX_DIES; i++) { + free(g->defer[i].stores); + free(g->defer[i].stage); + memset(&g->defer[i], 0, sizeof(g->defer[i])); + } +} + +/* Batched-decode attention block, ON-DIE flash path (L7): one launch runs + * every user's decode attention against its own GPU-resident KV ring — + * replacing the host per-user softmax + its 2 syncs + PCIe round-trips per + * layer (the K=8 steady-state ceiling; multi-tenant-speed-opt.md §3.2). + * Each user's split plan is recomputed on-device from its own position, so + * per-user outputs are bit-identical to the single-token decode-flash path + * at the same position (the test_users_qwen35 gate). + * + * The host KV mirror stays CURRENT (step 6): the roped K/V rows are D2H'd + * and stored per user exactly as the host path does — ~32 KB/layer, vs the + * O(K*seq) softmax + ~0.5 MB it replaces — so bind (state_to_host), + * slot-move, host-path fallback and session preserve all keep today's + * semantics with no B2-style deferral bookkeeping. + * + * Returns 0 = ran, 1 = unavailable (caller uses the host path), <0 = error. */ +/* L10 — host-computed plan for ONE attention layer's on-die slots flash path. + * + * Everything the device launch sequence needs that only the host can work out + * (ring views, cache geometry, the flash grid width) is resolved up front into + * this struct. That is what leaves the launch sequence itself pure device work + * — and therefore CUDA-graph capturable. `views` are OWNED (slot_base > 0 + * only); release with attn_slots_plan_free. */ +typedef struct { + fc_buffer *k_ring, *v_ring; + fc_buffer *k_sink, *v_sink, *k_win, *v_win; + fc_buffer *views[6]; /* owned offset views, or all NULL */ + int kv_q4, max_seq; + int sink_n, win_n; /* q4 sink / recent-window row counts */ + int kv_dim; + int launch_splits, kps; /* flash grid width + keys per split */ + float scale; +} qw_attn_slots_plan; + +static void attn_slots_plan_free(qw_attn_slots_plan *pl) +{ + for (int i = 0; i < 6; i++) { + fc_buffer_free_view(pl->views[i]); + pl->views[i] = NULL; + } +} + +/* Phase 1 of the slots attention block: pure HOST work — the availability + * checks, the ring allocation, the slot_base ring views, the [synced, pos) + * re-sync H2D from the host mirror (a no-op in steady-state decode), and the + * flash grid width. No device compute is issued, so a refusal leaves nothing + * half-done and the caller can take the host-softmax path. + * + * Returns 0 = ready (plan filled), 1 = flash unavailable (use host softmax, + * plan untouched), <0 = hard error. */ +static int attn_slots_flash_prep(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, int kv_idx, + pn_qwen35moe_state *const *sts, + const int *pos, fc_buffer *pos_g, + gpu_batch_scratch *b, int slot_base, + int nslots, int total_slots, + qw_attn_slots_plan *pl) +{ + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + fc_cuda_ctx *ctx = d->ctx; + + memset(pl, 0, sizeof(*pl)); + + /* Availability: flag on, kernels present, uniform KV geometry across + * the slots, no ring wrap for any user. Env gates are process-constant, + * so flash/host never alternate per step (only a ring wrap could mix + * them — and the mirror-current design keeps that correct too). */ + /* QWEN35_SDIAG: name the guard that rejects the on-die path. Latches per + * layer (first decision only), so a steady-state step logs nothing and the + * default (env unset) path is a single predictable branch. */ + /* Every refusal also RECORDS itself on the layer's path tracker, which is + * what /v1/models and /admin/models publish. The sdiag line is a debug + * aid that latches after the first decision; the tracker is the live gauge + * and is written every step, because "which path is this layer on RIGHT + * NOW" is the question an operator staring at 1.22 tok/s is asking. One + * byte store on a path that is already refusing — no hot-path cost. */ +#define FLASH_NA(why, reason) do { \ + pn_attn_path_host(&d->path, (why)); \ + if (sdiag_on() && !d->sdiag_flash_logged) { \ + d->sdiag_flash_logged = 1; \ + fprintf(stderr, "[sdiag] flash_slots kv_idx=%d nslots=%d " \ + "UNAVAILABLE: %s -> HOST softmax\n", \ + kv_idx, nslots, (reason)); \ + } \ + return 1; \ + } while (0) + + if (!flash_slots_on()) + FLASH_NA(PN_ATTN_HOST_FLASH_OFF, "QWEN35_FLASH_SLOTS=0"); + if (!fc_attn_flash_available_ctx(ctx)) + FLASH_NA(PN_ATTN_HOST_NO_KERNELS, "no flash kernels on this ctx"); + if (!b->a_z || !pos_g) + FLASH_NA(PN_ATTN_HOST_NO_SCRATCH, "batch scratch a_z/pos_g missing"); + const int kv_q4 = sts[0]->kv[kv_idx].q4; + const int max_seq = sts[0]->kv[kv_idx].max_seq; + if (kv_q4 ? !fc_attn_flash_decode_slots_q4_available_ctx(ctx) + : !fc_attn_flash_decode_slots_available_ctx(ctx)) + FLASH_NA(PN_ATTN_HOST_NO_SLOT_KERNEL, + kv_q4 ? "no q4 slot-decode kernel" + : "no fp32 slot-decode kernel"); + for (int u = 0; u < nslots; u++) { + const pn_qwen35moe_kv *kv = &sts[u]->kv[kv_idx]; + if (kv->q4 != kv_q4 || kv->max_seq != max_seq || + pos[u] + 1 > max_seq) + FLASH_NA(PN_ATTN_HOST_RING_WRAP, + "non-uniform KV geometry / ring wrap across slots"); + } + int rr = gpu_attn_slot_ring_ensure(d, cfg, kv_q4, max_seq, total_slots, + kv_idx); + if (rr > 0) { + /* ring_ensure already recorded the precise reason (ring vs decode + * scratch) and the retry verdict; do not overwrite it. */ + if (sdiag_on() && !d->sdiag_flash_logged) { + d->sdiag_flash_logged = 1; + fprintf(stderr, "[sdiag] flash_slots kv_idx=%d nslots=%d " + "UNAVAILABLE: slot KV ring allocation failed -> HOST " + "softmax\n", kv_idx, nslots); + } + return 1; + } + if (rr != 0) + return rr; + pn_attn_path_ondie(&d->path); + if (sdiag_on() && !d->sdiag_flash_logged) { + d->sdiag_flash_logged = 1; + fprintf(stderr, "[sdiag] flash_slots kv_idx=%d nslots=%d max_seq=%d " + "q4=%d ENGAGED (on-die attention)\n", + kv_idx, nslots, max_seq, kv_q4); + } +#undef FLASH_NA + + const int kv_sink = kv_q4 ? pn_kvq4_sink() : 0; + const int kv_win = kv_q4 ? pn_kvq4_win() : 0; + size_t row_b = kv_q4 ? (size_t)pn_kvq4_row_bytes(head_dim) + : (size_t)head_dim * sizeof(float); + + /* L5 — offset views of the persistent per-slot KV rings so the slot + * kernels (indexed by block slot idx 0..nslots) land on ring rows + * [slot_base, slot_base+nslots); the batch scratch (b->a_k etc.) stays + * group-local [0,nslots). slot_base==0 => views equal the rings (today). + * The per-slot host bookkeeping (kv_slot_host_id/synced) is indexed at + * slot_base+u below. */ + const size_t ring_row_stride = (size_t)n_head_kv * max_seq * row_b; + const size_t sink_row_stride = (size_t)n_head_kv * PN_KVQ4_SINKMAX * + head_dim * sizeof(uint16_t); + const size_t win_row_stride = (size_t)n_head_kv * PN_KVQ4_WMAX * + head_dim * sizeof(uint16_t); + fc_buffer *k_ring = d->k_slots_g, *v_ring = d->v_slots_g; + fc_buffer *k_sink = d->k_sink_slots_g, *v_sink = d->v_sink_slots_g; + fc_buffer *k_win = d->k_win_slots_g, *v_win = d->v_win_slots_g; + fc_buffer **views = pl->views; + if (slot_base > 0) { + views[0] = k_ring = fc_buffer_offset_view(d->k_slots_g, + (size_t)slot_base * ring_row_stride); + views[1] = v_ring = fc_buffer_offset_view(d->v_slots_g, + (size_t)slot_base * ring_row_stride); + int ok = (k_ring && v_ring); + if (kv_q4) { + views[2] = k_sink = fc_buffer_offset_view(d->k_sink_slots_g, + (size_t)slot_base * sink_row_stride); + views[3] = v_sink = fc_buffer_offset_view(d->v_sink_slots_g, + (size_t)slot_base * sink_row_stride); + views[4] = k_win = fc_buffer_offset_view(d->k_win_slots_g, + (size_t)slot_base * win_row_stride); + views[5] = v_win = fc_buffer_offset_view(d->v_win_slots_g, + (size_t)slot_base * win_row_stride); + ok = ok && k_sink && v_sink && k_win && v_win; + } + if (!ok) { + attn_slots_plan_free(pl); + return -1; + } + } + + /* Sync each slot's ring rows [synced, pos[u]) — a no-op in steady-state + * decode (watermark == pos); a full [0, pos) seed on the first step + * after a bind / slot move / realloc. Watermarks are valid only when + * both the buffer pointer AND the content generation match (Phase 10b). + * Seed source preference: (1) the single-user ring right after prefill + * (the bind hand-off) or (2) another slot holding this content (slot + * move) — both on-die D2D; (3) the host mirror, only when it is + * authoritative (eager mode, or a snapshot-restored session). */ + for (int u = 0; u < nslots; u++) { + int su = slot_base + u; /* absolute slot for host bookkeeping */ + const pn_qwen35moe_kv *kv = &sts[u]->kv[kv_idx]; + const void *host_id = kv_q4 ? (const void *)kv->kq + : (const void *)kv->k; + int synced = (d->kv_slot_host_id[su] == host_id && + d->kv_slot_gen[su] == kv->content_gen) + ? d->kv_slot_synced[su] : 0; + if (synced > pos[u]) + synced = pos[u]; + if (synced < pos[u]) { + int seeded = 0; + size_t seed_bytes = (size_t)(pos[u] - synced) * row_b; + if (d->k_cache_g && d->v_cache_g && d->cache_cap == max_seq && + d->kv_host_id == host_id && + d->kv_gen == kv->content_gen && + d->kv_synced_pos >= pos[u]) { + /* (1) D2D from the single-user ring (post-prefill bind). */ + for (int hd = 0; hd < n_head_kv; hd++) { + size_t src_off = ((size_t)hd * max_seq + synced) * row_b; + size_t dst_off = (((size_t)su * n_head_kv + hd) * + max_seq + synced) * row_b; + fc_buffer_copy_d2d_ctx(ctx, d->k_slots_g, dst_off, + d->k_cache_g, src_off, + seed_bytes); + fc_buffer_copy_d2d_ctx(ctx, d->v_slots_g, dst_off, + d->v_cache_g, src_off, + seed_bytes); + } + if (kv_q4 && d->k_sink_g && d->k_sink_slots_g) { + fc_buffer_copy_d2d_ctx(ctx, d->k_sink_slots_g, + (size_t)su * sink_row_stride, + d->k_sink_g, 0, sink_row_stride); + fc_buffer_copy_d2d_ctx(ctx, d->v_sink_slots_g, + (size_t)su * sink_row_stride, + d->v_sink_g, 0, sink_row_stride); + fc_buffer_copy_d2d_ctx(ctx, d->k_win_slots_g, + (size_t)su * win_row_stride, + d->k_win_g, 0, win_row_stride); + fc_buffer_copy_d2d_ctx(ctx, d->v_win_slots_g, + (size_t)su * win_row_stride, + d->v_win_g, 0, win_row_stride); + } + seeded = 1; + } + for (int s2 = 0; !seeded && s2 < d->kv_slots_cap_n; s2++) { + /* (2) D2D from another slot that holds this content. */ + if (s2 == su || d->kv_slot_host_id[s2] != host_id || + d->kv_slot_gen[s2] != kv->content_gen || + d->kv_slot_synced[s2] < pos[u]) + continue; + for (int hd = 0; hd < n_head_kv; hd++) { + size_t src_off = (((size_t)s2 * n_head_kv + hd) * + max_seq + synced) * row_b; + size_t dst_off = (((size_t)su * n_head_kv + hd) * + max_seq + synced) * row_b; + fc_buffer_copy_d2d_ctx(ctx, d->k_slots_g, dst_off, + d->k_slots_g, src_off, + seed_bytes); + fc_buffer_copy_d2d_ctx(ctx, d->v_slots_g, dst_off, + d->v_slots_g, src_off, + seed_bytes); + } + if (kv_q4 && d->k_sink_slots_g) { + fc_buffer_copy_d2d_ctx(ctx, d->k_sink_slots_g, + (size_t)su * sink_row_stride, + d->k_sink_slots_g, + (size_t)s2 * sink_row_stride, + sink_row_stride); + fc_buffer_copy_d2d_ctx(ctx, d->v_sink_slots_g, + (size_t)su * sink_row_stride, + d->v_sink_slots_g, + (size_t)s2 * sink_row_stride, + sink_row_stride); + fc_buffer_copy_d2d_ctx(ctx, d->k_win_slots_g, + (size_t)su * win_row_stride, + d->k_win_slots_g, + (size_t)s2 * win_row_stride, + win_row_stride); + fc_buffer_copy_d2d_ctx(ctx, d->v_win_slots_g, + (size_t)su * win_row_stride, + d->v_win_slots_g, + (size_t)s2 * win_row_stride, + win_row_stride); + } + seeded = 1; + } + if (!seeded && qw_kv_mirror_lazy() && kv->kv_mirror_stale) { + fprintf(stderr, "[attn] slots_prep kv_idx=%d slot=%d: rows " + "[%d,%d) have no ring source and the host mirror is " + "stale — refusing stale attention\n", + kv_idx, su, synced, pos[u]); + attn_slots_plan_free(pl); + return -1; + } + if (!seeded) { + /* (3) Host mirror upload (authoritative here). */ + const unsigned char *ksrc = kv_q4 + ? kv->kq : (const unsigned char *)kv->k; + const unsigned char *vsrc = kv_q4 + ? kv->vq : (const unsigned char *)kv->v; + for (int hd = 0; hd < n_head_kv; hd++) { + size_t src_off = ((size_t)hd * max_seq + synced) * row_b; + /* group-relative u into the slot_base-offset ring view */ + size_t dst_off = (((size_t)u * n_head_kv + hd) * max_seq + + synced) * row_b; + fc_buffer_from_cpu_at(k_ring, dst_off, ksrc + src_off, + seed_bytes); + fc_buffer_from_cpu_at(v_ring, dst_off, vsrc + src_off, + seed_bytes); + } + if (kv_q4) { + /* Re-seed this slot's fp16 sink/window shadow from the + * host shadow (cheap; only on a non-contiguous re-sync). */ + size_t sink_b = (size_t)n_head_kv * PN_KVQ4_SINKMAX * + head_dim * sizeof(uint16_t); + size_t win_b = (size_t)n_head_kv * PN_KVQ4_WMAX * + head_dim * sizeof(uint16_t); + fc_buffer_from_cpu_at(k_sink, (size_t)u * sink_b, + kv->k_sink, sink_b); + fc_buffer_from_cpu_at(v_sink, (size_t)u * sink_b, + kv->v_sink, sink_b); + fc_buffer_from_cpu_at(k_win, (size_t)u * win_b, + kv->k_win, win_b); + fc_buffer_from_cpu_at(v_win, (size_t)u * win_b, + kv->v_win, win_b); + } + } + } + d->kv_slot_host_id[su] = host_id; + d->kv_slot_gen[su] = kv->content_gen; + d->kv_slot_kv[su] = (pn_qwen35moe_kv *)kv; + d->kv_slot_synced[su] = pos[u] + 1; /* the append below lands it */ + } + + /* The flash grid width: the max of the per-user split plans. Every OTHER + * launch argument below is position-invariant (the slot kernels read each + * user's position from pos_g and recompute its own plan on-device), so + * this is the single value a captured graph has to be keyed on. */ + int kps = dec_keys_per_split(); + int launch_splits = 1; + for (int u = 0; u < nslots; u++) { + int ns = gpu_attn_slot_plan_splits(pos[u] + 1, kps); + if (ns > launch_splits) + launch_splits = ns; + } + + pl->k_ring = k_ring; pl->v_ring = v_ring; + pl->k_sink = k_sink; pl->v_sink = v_sink; + pl->k_win = k_win; pl->v_win = v_win; + pl->kv_q4 = kv_q4; + pl->max_seq = max_seq; + pl->sink_n = kv_sink; + pl->win_n = kv_win; + pl->kv_dim = n_head_kv * head_dim; + pl->launch_splits = launch_splits; + pl->kps = kps; + pl->scale = 1.0f / sqrtf((float)head_dim); + return 0; +} + +/* Phase 2 of the slots attention block: steps 1-5 + the sigmoid output gate, + * as PURE DEVICE WORK over persistent buffers and the prep'd plan. No host + * sync, no blocking copy, no allocation — this is the part that goes inside a + * captured CUDA graph, and it must stay that way. */ +static int attn_slots_flash_core(gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, fc_buffer *pos_g, + int nslots, const qw_attn_slots_plan *pl) +{ + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int attn_dim = n_head * head_dim; + const int kv_q4 = pl->kv_q4; + const int max_seq = pl->max_seq; + const int kv_sink = pl->sink_n, kv_win = pl->win_n; + const int launch_splits = pl->launch_splits, kps = pl->kps; + const float scale = pl->scale; + fc_buffer *k_ring = pl->k_ring, *v_ring = pl->v_ring; + fc_buffer *k_sink = pl->k_sink, *v_sink = pl->v_sink; + fc_buffer *k_win = pl->k_win, *v_win = pl->v_win; + fc_cuda_ctx *ctx = d->ctx; + double sp_t0 = 0.0; + sprof_begin(ctx, &sp_t0); + + /* 1. Joint Q+gate, K, V projections for all users. */ + if (gpu_matvec_nx(&d->attn_q, b->x_norm, b->a_qg, nslots) < 0 || + gpu_matvec_nx(&d->attn_k, b->x_norm, b->a_k, nslots) < 0 || + gpu_matvec_nx(&d->attn_v, b->x_norm, b->a_v, nslots) < 0) + return -1; + sprof_lap(SPROF_DENSE, ctx, &sp_t0); + + /* Separate clock: these laps partition SPROF_ATTN_FLASH, they do not + * shorten it (same contract as the SSM sub-spans). */ + double sub_t0 = 0.0; + sub_begin(ctx, &sub_t0); + + /* 2.-4. Device q/gate split (z persists in a_z for the output gate), + * QK-RMSNorm, per-row-position partial NEOX RoPE — same math as the + * host path, minus the split's D2H/H2D round trip. */ + fc_attn_qz_split_ctx(ctx, b->a_qg, b->a_q, b->a_z, n_head, head_dim, + nslots); + sub_lap(&attd_cum[ATTD_QZSPLIT], &attd_laps[ATTD_QZSPLIT], ctx, &sub_t0); + fc_qk_rms_norm_ctx(ctx, b->a_q, d->attn_q_norm.buf, nslots * n_head, + head_dim, cfg->rms_norm_eps); + fc_qk_rms_norm_ctx(ctx, b->a_k, d->attn_k_norm.buf, nslots * n_head_kv, + head_dim, cfg->rms_norm_eps); + sub_lap(&attd_cum[ATTD_QKNORM], &attd_laps[ATTD_QKNORM], ctx, &sub_t0); + fc_rope_neox_partial_pos_ctx(ctx, b->a_q, n_head, head_dim, + cfg->rope_dim, pos_g, cfg->rope_theta, + nslots); + fc_rope_neox_partial_pos_ctx(ctx, b->a_k, n_head_kv, head_dim, + cfg->rope_dim, pos_g, cfg->rope_theta, + nslots); + sub_lap(&attd_cum[ATTD_ROPE], &attd_laps[ATTD_ROPE], ctx, &sub_t0); + + /* 5. Ring append (all K rows, one launch) + per-user split flash + + * combine into a_out. */ + if (kv_q4) { + fc_attn_kv_append_slots_q4_ctx(ctx, b->a_k, b->a_v, k_ring, + v_ring, k_sink, + v_sink, k_win, + v_win, n_head_kv, head_dim, + max_seq, pos_g, nslots, kv_sink, + kv_win); + sub_lap(&attd_cum[ATTD_KVAPPEND], &attd_laps[ATTD_KVAPPEND], ctx, + &sub_t0); + if (fc_attn_flash_decode_slots_q4_ctx(ctx, b->a_q, k_ring, + v_ring, k_sink, + v_sink, + k_win, + v_win, + d->dec_slots_m_g, + d->dec_slots_l_g, + d->dec_slots_acc_g, b->a_out, + n_head, n_head_kv, head_dim, + max_seq, pos_g, nslots, + launch_splits, kps, + PN_QWEN35MOE_DEC_SPLIT_MAX, + kv_sink, kv_win, attn_sink(), + attn_window(), scale) < 0) + return -1; + } else { + fc_attn_kv_append_slots_ctx(ctx, b->a_k, b->a_v, k_ring, + v_ring, n_head_kv, head_dim, + max_seq, pos_g, nslots); + sub_lap(&attd_cum[ATTD_KVAPPEND], &attd_laps[ATTD_KVAPPEND], ctx, + &sub_t0); + if (fc_attn_flash_decode_slots_ctx(ctx, b->a_q, k_ring, + v_ring, d->dec_slots_m_g, + d->dec_slots_l_g, + d->dec_slots_acc_g, b->a_out, + n_head, n_head_kv, head_dim, + max_seq, pos_g, nslots, + launch_splits, kps, + PN_QWEN35MOE_DEC_SPLIT_MAX, + attn_sink(), attn_window(), + scale) < 0) + return -1; + } + + sub_lap(&attd_cum[ATTD_FLASH], &attd_laps[ATTD_FLASH], ctx, &sub_t0); + + /* Sigmoid output gate on the die: a_out *= sigmoid(z). */ + fc_sigmoid_ctx(ctx, b->a_z, nslots * attn_dim); + fc_mul_ctx(ctx, b->a_out, b->a_z, b->a_out, nslots * attn_dim); + sub_lap(&attd_cum[ATTD_OUTGATE], &attd_laps[ATTD_OUTGATE], ctx, &sub_t0); + sprof_lap(SPROF_ATTN_FLASH, ctx, &sp_t0); + return 0; +} + +/* Phase 3 of the slots attention block — step 6, the host KV mirror append: + * D2H the roped K/V rows and store them into each user's host cache (identical + * store code to the host path) so the mirror stays authoritative for + * bind/move/fallback/preserve. + * + * L10: this used to sit MID-body, between the output gate and the out + * projection, where its blocking fc_sync_ctx made the whole attention layer + * un-capturable. It runs AFTER the layer body now. That is safe because + * b->a_k / b->a_v are dedicated attention scratch: the out projection, the + * post-attention norm and the batched MoE all write elsewhere (a_out / + * block_out / hidden / x_norm / m_*), so the roped K/V is still intact when + * this runs, and the NEXT attention layer's projections are the first thing + * that overwrites it. + * + * QWEN35_ASYNC_KVMIRROR: instead of the per-layer blocking sync + D2H + + * store, queue an ASYNC D2H of this layer's K||V into a per-die host arena + * on the die's own stream and record the store for deferred replay at + * end-of-step (qw_defer_flush). No per-layer host stall => the die stays + * busy and the 2-die pipeline can actually overlap. Byte-identical result: + * the mirror is fully replayed before any boundary consumer (bind/move/ + * preserve) runs. Falls back to the blocking path if async D2H or arena + * allocation is unavailable. */ +static void attn_slots_mirror(pn_qwen35moe_gpu *g, gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, int kv_idx, + pn_qwen35moe_state *const *sts, const int *pos, + gpu_batch_scratch *b, int nslots, + const qw_attn_slots_plan *pl, float *hb_kv) +{ + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int kv_dim = pl->kv_dim; + const int kv_q4 = pl->kv_q4; + const int max_seq = pl->max_seq; + const int kv_sink = pl->sink_n, kv_win = pl->win_n; + fc_cuda_ctx *ctx = d->ctx; + double sp_t0 = 0.0; + + /* Phase 10b (lazy mirror): no per-token host append at all — the slot + * ring this step appended to is authoritative, and the prep watermark + * (kv_slot_synced = pos+1) records that the ring, not the mirror, holds + * the rows. This removes a per-layer fc_sync + 2 D2H copies from every + * decode step AND stops host RSS from growing with decode length. */ + if (qw_kv_mirror_lazy()) { + for (int u = 0; u < nslots; u++) + sts[u]->kv[kv_idx].kv_mirror_stale = 1; + return; + } + sprof_begin(ctx, &sp_t0); + + if (async_kvmirror_on()) { + int idx = qw_defer_idx(g, ctx); + qw_kv_defer *df = &g->defer[idx]; + size_t layer_bytes = (size_t)2 * nslots * kv_dim * sizeof(float); + size_t off = qw_defer_stage(df, layer_bytes); + qw_kv_store *st = (off != (size_t)-1) ? qw_defer_push(df) : NULL; + if (st) { + float *stage = df->stage + off / sizeof(float); + int ok = (fc_buffer_to_cpu_n_async(b->a_k, stage, + (size_t)nslots * kv_dim * sizeof(float)) == 0) && + (fc_buffer_to_cpu_n_async(b->a_v, + stage + (size_t)nslots * kv_dim, + (size_t)nslots * kv_dim * sizeof(float)) == 0); + if (ok) { + st->nslots = nslots; + st->kv_q4 = kv_q4; + st->kv_sink = kv_sink; + st->kv_win = kv_win; + st->n_head_kv = n_head_kv; + st->head_dim = head_dim; + st->max_seq = max_seq; + st->kv_dim = kv_dim; + st->stage_off = off; + for (int u = 0; u < nslots; u++) { + st->kv[u] = &sts[u]->kv[kv_idx]; + st->pos[u] = pos[u]; + } + sprof_lap(SPROF_ATTN_MIRROR, ctx, &sp_t0); + return; + } + /* async D2H failed — un-push the descriptor + arena, fall back. */ + df->n--; + df->stage_used = off; + } + /* fall through to the blocking path below (correctness preserved). */ + } + fc_sync_ctx(ctx); + fc_buffer_to_cpu_n(b->a_k, hb_kv, (size_t)nslots * kv_dim * sizeof(float)); + fc_buffer_to_cpu_n(b->a_v, hb_kv + (size_t)nslots * kv_dim, + (size_t)nslots * kv_dim * sizeof(float)); + for (int u = 0; u < nslots; u++) { + pn_qwen35moe_kv *kv = &sts[u]->kv[kv_idx]; + int cache_pos = pos[u] % max_seq; + const float *kvec = hb_kv + (size_t)u * kv_dim; + const float *vvec = hb_kv + (size_t)(nslots + u) * kv_dim; + for (int hd = 0; hd < n_head_kv; hd++) { + if (kv_q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, hd, + max_seq, cache_pos, + kvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, hd, + max_seq, cache_pos, + vvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + } else { + memcpy(kv->k + ((size_t)hd * max_seq + cache_pos) * head_dim, + kvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + memcpy(kv->v + ((size_t)hd * max_seq + cache_pos) * head_dim, + vvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + } + } + } + sprof_lap(SPROF_ATTN_MIRROR, ctx, &sp_t0); +} + +/* The on-die flash path for one batched-decode attention block, as the direct + * (non-graph-captured) caller sees it: prep -> device core -> host KV mirror -> + * out projection + residual. Same contract as before the L10 split — 0 = done, + * 1 = flash unavailable (caller runs host softmax), <0 = error. */ +static int gpu_attn_block_slots_flash(pn_qwen35moe_gpu *g, gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, + int kv_idx, + pn_qwen35moe_state *const *sts, + const int *pos, fc_buffer *pos_g, + gpu_batch_scratch *b, int slot_base, + int nslots, int total_slots, + float *hb_kv) +{ + fc_cuda_ctx *ctx = d->ctx; + qw_attn_slots_plan pl; + int rc = attn_slots_flash_prep(d, cfg, kv_idx, sts, pos, pos_g, b, + slot_base, nslots, total_slots, &pl); + if (rc != 0) + return rc; /* 1 = host softmax path, <0 = error (plan freed) */ + + rc = attn_slots_flash_core(d, cfg, b, pos_g, nslots, &pl); + if (rc == 0) { + attn_slots_mirror(g, d, cfg, kv_idx, sts, pos, b, nslots, &pl, hb_kv); + /* 7. Output projection + residual. */ + double sp_t0 = 0.0; + sprof_begin(ctx, &sp_t0); + if (gpu_matvec_nx(&d->attn_output, b->a_out, b->block_out, nslots) < 0) + rc = -1; + else + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, + nslots * cfg->n_embd); + sprof_lap(SPROF_DENSE, ctx, &sp_t0); + } + attn_slots_plan_free(&pl); + return rc; +} + +/* Batched-decode attention block: identical launch/glue sequence to the + * prefill gpu_attn_block_batch except (a) RoPE takes each row's OWN + * position from the die's pos buffer, and (b) the host KV append + causal + * GQA softmax runs against each USER's own cache at its own position (the + * same host code as gpu_attn_block, per user). */ +static int gpu_attn_block_slots(pn_qwen35moe_gpu *g, gpu_attn_layer *d, + const pn_qwen35moe_config *cfg, int kv_idx, + pn_qwen35moe_state *const *sts, + const int *pos, fc_buffer *pos_g, + gpu_batch_scratch *b, int slot_base, + int nslots, int total_slots, + float *const hb[4]) +{ + const int n_head = cfg->n_head; + const int n_head_kv = cfg->n_head_kv; + const int head_dim = cfg->head_dim; + const int n_kv_groups = n_head / n_head_kv; + const int q_stride = head_dim * 2; + const int attn_dim = n_head * head_dim; + const int kv_dim = n_head_kv * head_dim; + const float eps = cfg->rms_norm_eps; + fc_cuda_ctx *ctx = d->ctx; + /* Group-local host staging: hb[] = this group's 4 staging regions. For a + * single group these are g->bhost[0..3]; the pipeline hands each group a + * distinct set so overlapping die-A/die-B host mirrors never collide. */ + float *hb_qg = hb[0]; + float *hb_q = hb[1]; + float *hb_z = hb[2]; + float *hb_kv = hb[3]; + + /* L7 — on-die per-slot flash attention (QWEN35_FLASH_SLOTS, default + * on). Falls through to the host-softmax path below only when the slot + * kernels are unavailable / disabled / a user's ring would wrap. */ + { + int fr = gpu_attn_block_slots_flash(g, d, cfg, kv_idx, sts, pos, + pos_g, b, slot_base, nslots, + total_slots, hb_kv); + if (fr <= 0) + return fr; + } + + double sp_t0 = 0.0; + sprof_begin(ctx, &sp_t0); + + /* 1. Joint Q+gate, K, V projections for all users. */ + if (gpu_matvec_nx(&d->attn_q, b->x_norm, b->a_qg, nslots) < 0 || + gpu_matvec_nx(&d->attn_k, b->x_norm, b->a_k, nslots) < 0 || + gpu_matvec_nx(&d->attn_v, b->x_norm, b->a_v, nslots) < 0) + return -1; + sprof_lap(SPROF_DENSE, ctx, &sp_t0); + + /* Q/gate split, host-side per user (same glue as the single-token + * path; zgate persists in hb_z for the output gate below). */ + fc_sync_ctx(ctx); + fc_buffer_to_cpu_n(b->a_qg, hb_qg, + (size_t)nslots * 2 * attn_dim * sizeof(float)); + for (int u = 0; u < nslots; u++) { + const float *src_u = hb_qg + (size_t)u * 2 * attn_dim; + float *q_u = hb_q + (size_t)u * attn_dim; + float *z_u = hb_z + (size_t)u * attn_dim; + for (int hd = 0; hd < n_head; hd++) { + memcpy(q_u + (size_t)hd * head_dim, + src_u + (size_t)hd * q_stride, + (size_t)head_dim * sizeof(float)); + memcpy(z_u + (size_t)hd * head_dim, + src_u + (size_t)hd * q_stride + head_dim, + (size_t)head_dim * sizeof(float)); + } + } + fc_buffer_from_cpu_n(b->a_q, hb_q, + (size_t)nslots * attn_dim * sizeof(float)); + + /* 2.-4. QK-RMSNorm (heads uniform across rows) + per-row-position + * partial NEOX RoPE (each user at its own context length). */ + fc_qk_rms_norm_ctx(ctx, b->a_q, d->attn_q_norm.buf, nslots * n_head, + head_dim, eps); + fc_qk_rms_norm_ctx(ctx, b->a_k, d->attn_k_norm.buf, nslots * n_head_kv, + head_dim, eps); + fc_rope_neox_partial_pos_ctx(ctx, b->a_q, n_head, head_dim, + cfg->rope_dim, pos_g, cfg->rope_theta, + nslots); + fc_rope_neox_partial_pos_ctx(ctx, b->a_k, n_head_kv, head_dim, + cfg->rope_dim, pos_g, cfg->rope_theta, + nslots); + + /* 5.+6. Host: per USER, append K/V to that user's cache, causal GQA + * softmax over that user's window, sigmoid output gate — identical + * code to gpu_attn_block per user. */ + fc_sync_ctx(ctx); + fc_buffer_to_cpu_n(b->a_q, hb_q, (size_t)nslots * attn_dim * sizeof(float)); + fc_buffer_to_cpu_n(b->a_k, hb_kv, (size_t)nslots * kv_dim * sizeof(float)); + fc_buffer_to_cpu_n(b->a_v, hb_kv + (size_t)nslots * kv_dim, + (size_t)nslots * kv_dim * sizeof(float)); + { + float *aout_all = hb_qg; + float scale = 1.0f / sqrtf((float)head_dim); + /* heap scratch: max_seq (same for all slots) can reach the 256K cap — + * too large for the stack. */ + int scores_cap = (nslots > 0) ? sts[0]->kv[kv_idx].max_seq : 4096; + float *scores = (float *)malloc((size_t)scores_cap * sizeof(float)); + float krow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + float vrow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + if (!scores) return -1; + for (int u = 0; u < nslots; u++) { + pn_qwen35moe_kv *kv = &sts[u]->kv[kv_idx]; + float *k_cache = kv->k; + float *v_cache = kv->v; + const int kv_q4 = kv->q4; + const int kv_sink = kv_q4 ? pn_kvq4_sink() : 0; + const int kv_win = kv_q4 ? pn_kvq4_win() : 0; + int max_seq = kv->max_seq; + int p = pos[u]; + int cache_pos = p % max_seq; + /* Phase 10b (lazy mirror): this host path READS the mirror + * [0, seq_len) below. If decode ran on-die since the last + * materialization the mirror is missing rows — pull them from + * the GPU ring first. From here on this layer keeps the mirror + * current itself (it appends every step), so this fires once + * per fallback engagement, not per token. */ + if (kv->kv_mirror_stale && + qw_kv_layer_materialize(d, kv, + (p < max_seq) ? p : max_seq) != 0) { + fprintf(stderr, "[attn] slots host fallback kv_idx=%d " + "slot=%d: host mirror stale with no ring source — " + "failing step rather than attending garbage\n", + kv_idx, u); + free(scores); + return -1; + } + const float *kvec = hb_kv + (size_t)u * kv_dim; + const float *vvec = hb_kv + (size_t)(nslots + u) * kv_dim; + for (int hd = 0; hd < n_head_kv; hd++) { + if (kv_q4) { + pn_kvq4_store_row(kv->kq, kv->k_sink, kv->k_win, hd, + max_seq, cache_pos, + kvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + pn_kvq4_store_row(kv->vq, kv->v_sink, kv->v_win, hd, + max_seq, cache_pos, + vvec + (size_t)hd * head_dim, head_dim, + kv_sink, kv_win); + } else { + memcpy(k_cache + ((size_t)hd * max_seq + cache_pos) * head_dim, + kvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + memcpy(v_cache + ((size_t)hd * max_seq + cache_pos) * head_dim, + vvec + (size_t)hd * head_dim, + (size_t)head_dim * sizeof(float)); + } + } + + int seq_len = (p + 1 < max_seq) ? p + 1 : max_seq; + const float *qhost = hb_q + (size_t)u * attn_dim; + float *aout = aout_all + (size_t)u * attn_dim; + for (int qh = 0; qh < n_head; qh++) { + int kv_h = qh / n_kv_groups; + const float *q_vec = qhost + (size_t)qh * head_dim; + const float *k_base = kv_q4 ? NULL : + k_cache + (size_t)kv_h * max_seq * head_dim; + const float *v_base = kv_q4 ? NULL : + v_cache + (size_t)kv_h * max_seq * head_dim; + float *out_vec = aout + (size_t)qh * head_dim; + for (int sp = 0; sp < seq_len; sp++) { + const float *k_vec; + if (kv_q4) { + pn_kvq4_load_row(kv->kq, kv->k_sink, kv->k_win, kv_h, + max_seq, sp, p, krow, head_dim, + kv_sink, kv_win); + k_vec = krow; + } else { + k_vec = k_base + (size_t)sp * head_dim; + } + scores[sp] = pn_dot_f32(q_vec, k_vec, head_dim) * scale; + } + float mx = scores[0]; + for (int sp = 1; sp < seq_len; sp++) + if (scores[sp] > mx) mx = scores[sp]; + float sum_exp = 0.0f; + for (int sp = 0; sp < seq_len; sp++) { + scores[sp] = expf(scores[sp] - mx); + sum_exp += scores[sp]; + } + float inv_sum = 1.0f / sum_exp; + memset(out_vec, 0, (size_t)head_dim * sizeof(float)); + for (int sp = 0; sp < seq_len; sp++) { + float w_s = scores[sp] * inv_sum; + const float *v_vec; + if (kv_q4) { + pn_kvq4_load_row(kv->vq, kv->v_sink, kv->v_win, kv_h, + max_seq, sp, p, vrow, head_dim, + kv_sink, kv_win); + v_vec = vrow; + } else { + v_vec = v_base + (size_t)sp * head_dim; + } + for (int dd = 0; dd < head_dim; dd++) + out_vec[dd] += w_s * v_vec[dd]; + } + } + + const float *z_u = hb_z + (size_t)u * attn_dim; + for (int i = 0; i < attn_dim; i++) { + float gv = 1.0f / (1.0f + expf(-z_u[i])); + aout[i] *= gv; + } + } + free(scores); + fc_buffer_from_cpu_n(b->a_out, aout_all, + (size_t)nslots * attn_dim * sizeof(float)); + } + sprof_lap(SPROF_ATTN_HOST, ctx, &sp_t0); + + /* 7. Output projection + residual. */ + if (gpu_matvec_nx(&d->attn_output, b->a_out, b->block_out, nslots) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, nslots * cfg->n_embd); + sprof_lap(SPROF_DENSE, ctx, &sp_t0); + return 0; +} + +/* L9 — the position-invariant SSM slot layer body (attn_norm -> slot-SSM + * block -> post_attention_norm -> slots MoE -> +residual). On a GROUPED layer + * this is pure stream-ordered: no host sync, no D2H, no allocation — every + * buffer is persistent (the per-slot device state + the per-die batch + * scratch), so the exact same launch sequence replays every step, and that is + * the sequence captured into the per-layer slots graph. A non-grouped layer + * takes the per-row MoE fallback (which does sync + read back the routing) and + * is never graph-captured — the capture gate already requires moe.grouped. + * Returns 0 on success, <0 on error. */ +static int gpu_ssm_slots_layer_body(gpu_ssm_layer *d, fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, int slot_base, + int nslots, gpu_die_scratch *sc, + gpu_host_scratch *h) +{ + int c = cfg->n_embd; + int rc; + fc_rms_norm_nx_ctx(ctx, b->hidden, d->attn_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, nslots); + rc = gpu_ssm_block_slots(d, cfg, b, slot_base, nslots); + if (rc < 0) + return rc; + fc_rms_norm_nx_ctx(ctx, b->hidden, d->post_attention_norm.buf, + b->x_norm, c, cfg->rms_norm_eps, nslots); + return gpu_moe_block_slots(ctx, &d->moe, cfg, b, nslots, sc, h); +} + +/* L10 — graph-capturable body of ONE batched-decode ATTENTION layer: + * attn_norm -> on-die slots flash core -> out projection + residual -> + * post_attention_norm -> batched MoE. + * + * Pure device work by construction. The two host-touching pieces of the + * attention block live outside it: the ring re-sync + plan (attn_slots_flash_ + * prep, before) and the host KV mirror (attn_slots_mirror, after). Everything + * here reads persistent buffers at stable addresses, and the only launch + * argument derived from the token positions is pl->launch_splits, which the + * caller keys the graph on. */ +static int gpu_attn_slots_layer_body(gpu_attn_layer *d, fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *b, fc_buffer *pos_g, + int nslots, + const qw_attn_slots_plan *pl, + gpu_die_scratch *sc, gpu_host_scratch *h) +{ + int c = cfg->n_embd; + fc_rms_norm_nx_ctx(ctx, b->hidden, d->attn_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, nslots); + if (attn_slots_flash_core(d, cfg, b, pos_g, nslots, pl) < 0) + return -1; + if (gpu_matvec_nx(&d->attn_output, b->a_out, b->block_out, nslots) < 0) + return -1; + fc_add_ctx(ctx, b->hidden, b->block_out, b->hidden, nslots * c); + fc_rms_norm_nx_ctx(ctx, b->hidden, d->post_attention_norm.buf, + b->x_norm, c, cfg->rms_norm_eps, nslots); + return gpu_moe_block_slots(ctx, &d->moe, cfg, b, nslots, sc, h); +} + +/* L10 — run one batched-decode ATTENTION layer through its captured CUDA + * graph, capturing it first if needed. + * + * Returns 0 = layer complete, <0 = error, 1 = "not mine, run the direct path" + * (only when the on-die flash core is unavailable and the host-softmax + * fallback — which is capture-hostile — has to own the layer). In that case + * prep did no device work and returned before the ring re-sync, so the direct + * path re-running prep is a no-op. */ +static int gpu_attn_slots_layer_graph(pn_qwen35moe_gpu *g, gpu_attn_layer *d, + fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, + int kv_idx, + pn_qwen35moe_state *const *sts, + const int *pos, fc_buffer *pos_g, + gpu_batch_scratch *b, int layer, + int slot_base, int nslots, + int total_slots, float *const hb[4], + gpu_die_scratch *sc, gpu_host_scratch *h) +{ + qw_attn_slots_plan pl; + int rc = attn_slots_flash_prep(d, cfg, kv_idx, sts, pos, pos_g, b, + slot_base, nslots, total_slots, &pl); + if (rc != 0) + return rc > 0 ? 1 : rc; + + /* A captured graph bakes in the slot count and the ring-offset base (launch + * grids), the batch-scratch addresses, the KV geometry, the ring + * allocation, and the flash grid width. A change in any invalidates it — + * drop + recapture. launch_splits is the only one that moves during a + * session, and it steps up at most PN_QWEN35MOE_DEC_SPLIT_MAX times as the + * context grows (once per `kps` tokens), so this is not a per-step + * recapture. */ + if (d->slots_graph && + (d->slots_graph_n != nslots || + d->slots_graph_base != slot_base || + d->slots_graph_splits != pl.launch_splits || + d->slots_graph_q4 != pl.kv_q4 || + d->slots_graph_max_seq != pl.max_seq || + d->slots_graph_gen != d->slots_ring_gen || + d->slots_graph_scratch != (const void *)b->hidden)) { + /* QWEN35_SDIAG: log EVERY drop, not just the first. A captured graph + * that is silently invalidated and recaptured each step is worse than + * no graph at all, and a per-step drop is indistinguishable from a + * healthy one in a first-step-only report. One line per drop makes the + * rate directly countable. */ + if (sdiag_on()) + fprintf(stderr, "[sdiag] layer %2d ATTN graph=DROPPED " + "n %d->%d base %d->%d splits %d->%d q4 %d->%d " + "max_seq %d->%d gen %u->%u scratch %s\n", + layer, d->slots_graph_n, nslots, + d->slots_graph_base, slot_base, + d->slots_graph_splits, pl.launch_splits, + d->slots_graph_q4, pl.kv_q4, + d->slots_graph_max_seq, pl.max_seq, + d->slots_graph_gen, d->slots_ring_gen, + d->slots_graph_scratch == (const void *)b->hidden + ? "same" : "MOVED"); + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + pn_graph_capture_dropped(&d->slots_graph_capture); + } + + int done = 0; + if (d->slots_graph) { + if (fc_graph_launch(d->slots_graph) == 0) { + done = 1; + } else { + /* Replay failed unexpectedly — drop it and run direct this step. */ + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + pn_graph_capture_dropped(&d->slots_graph_capture); + } + } + + /* Capture lazily, and RETRY a failed capture on a bounded backoff — a + * capture lost to a transient collision must not cost this layer its graph + * for the life of the process. The recorded launches do NOT execute, so on + * any failure the direct body runs this step and nothing was + * half-applied. */ + if (!done && pn_graph_capture_should_attempt(&d->slots_graph_capture)) { + int brc = 0; + int begin_ok = fc_graph_begin_ctx(ctx) == 0; + if (begin_ok) { + brc = gpu_attn_slots_layer_body(d, ctx, cfg, b, pos_g, nslots, + &pl, sc, h); + d->slots_graph = fc_graph_end_ctx(ctx); + if (pn_graph_capture_inject_now(&d->slots_graph_capture) && + d->slots_graph) { + fc_graph_free(d->slots_graph); /* fault injection, test only */ + d->slots_graph = NULL; + } + if (brc == 0 && d->slots_graph && + fc_graph_launch(d->slots_graph) == 0) { + d->slots_graph_n = nslots; + d->slots_graph_base = slot_base; + d->slots_graph_splits = pl.launch_splits; + d->slots_graph_q4 = pl.kv_q4; + d->slots_graph_max_seq = pl.max_seq; + d->slots_graph_gen = d->slots_ring_gen; + d->slots_graph_scratch = (const void *)b->hidden; + done = 1; + } else if (d->slots_graph) { + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + } + } + + if (done) { + pn_graph_capture_succeeded(&d->slots_graph_capture); + if (sdiag_on() && !d->moe.sdiag_logged) { + d->moe.sdiag_logged = 1; + fprintf(stderr, "[sdiag] layer %2d ATTN moe=grouped " + "graph=CAPTURED n=%d base=%d splits=%d\n", + layer, nslots, slot_base, pl.launch_splits); + } + } else { + pn_graph_capture_failed(&d->slots_graph_capture); + /* Log EVERY failure, not just the first: the retry budget bounds + * this to PN_GRAPH_CAPTURE_MAX_FAILURES lines per layer, and the + * attempt/cooldown make a permanent give-up visibly different from + * a transient collision that recovers. */ + if (sdiag_on()) + fprintf(stderr, "[sdiag] layer %2d ATTN moe=grouped " + "graph=%s (body rc=%d) attempt %d/%d -> %s\n", layer, + begin_ok ? "CAPTURE FAILED" : "BEGIN FAILED", brc, + d->slots_graph_capture.consecutive_failures, + PN_GRAPH_CAPTURE_MAX_FAILURES, + pn_graph_capture_state_name( + d->slots_graph_capture.state)); + } + } + + if (!done && + gpu_attn_slots_layer_body(d, ctx, cfg, b, pos_g, nslots, &pl, + sc, h) < 0) { + attn_slots_plan_free(&pl); + return -1; + } + + /* Step 6, outside the graph: b->a_k / b->a_v still hold this layer's roped + * K/V (nothing in the body after the flash core writes them). */ + attn_slots_mirror(g, d, cfg, kv_idx, sts, pos, b, nslots, &pl, hb[3]); + attn_slots_plan_free(&pl); + return 0; +} + +/* One layer of the batched decode: attn_norm -> (slot-SSM | per-user attn) + * -> +residual -> post_attention_norm -> shared-MoE -> +residual over the + * [K][n_embd] residual stream (the MoE block is the prefill one — routing + * is already per-row). */ +static int gpu_layer_forward_slots(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + fc_cuda_ctx *ctx, int layer, + gpu_batch_scratch *b, + pn_qwen35moe_state *const *sts, + const int *pos, fc_buffer *pos_g, + int slot_base, int nslots, int total_slots, + float *const hb[4]) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + int idx = pn_qwen35moe_model_layer_index(m, layer); + int rc; + /* Only a NON-grouped layer needs the per-row MoE fallback's scratch; + * resolving it lazily keeps the grouped (graph-captured) path allocation- + * free, exactly as before. */ + gpu_host_scratch *host_scr = g->host_ok ? &g->host : NULL; + + /* Which gate closed the graph path for this layer this step? Evaluated in + * the same order the graph_ok expressions below test them, so the FIRST + * failing gate is the one reported. */ + pn_graph_ineligible_reason gate_reason = PN_GRAPH_INELIGIBLE_NONE; + int is_attn = pn_qwen35moe_model_layer_type(m, layer) == + PN_QWEN35MOE_LAYER_ATTN; + int layer_grouped = is_attn ? g->attn[idx].moe.grouped + : g->ssm[idx].moe.grouped; + if (!gpu_graph_enabled()) + gate_reason = PN_GRAPH_INELIGIBLE_GRAPH_OFF; + else if (!gpu_graph_slots_enabled()) + gate_reason = PN_GRAPH_INELIGIBLE_SLOTS_OFF; + else if (is_attn && !gpu_graph_slots_attn_enabled()) + gate_reason = PN_GRAPH_INELIGIBLE_ATTN_OFF; + else if (sprof_active) + gate_reason = PN_GRAPH_INELIGIBLE_PROFILING; + else if (!layer_grouped) + gate_reason = PN_GRAPH_INELIGIBLE_MOE_NOT_GROUPED; + else if (slot_base != 0) + gate_reason = PN_GRAPH_INELIGIBLE_SLOT_BASE; + + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + gpu_attn_layer *d = &g->attn[idx]; + gpu_die_scratch *die_scr = d->moe.grouped ? NULL + : gpu_get_scratch(g, ctx, cfg); + /* L10 — captured-graph path for the attention layer. Same gates as the + * SSM layers (graph env on, sprof off — its mid-body syncs would break + * capture — grouped MoE, base-0 group only), plus the flash core being + * available: the host-softmax fallback is unconditionally + * capture-hostile and owns its layer end to end. */ + int graph_ok = gpu_graph_enabled() && gpu_graph_slots_enabled() && + gpu_graph_slots_attn_enabled() && !sprof_active && + d->moe.grouped && slot_base == 0; + if (graph_ok) { + rc = gpu_attn_slots_layer_graph(g, d, ctx, cfg, idx, sts, pos, + pos_g, b, layer, slot_base, nslots, + total_slots, hb, die_scr, + host_scr); + if (rc <= 0) + return rc; /* 0 = layer done, <0 = error; >0 = run direct */ + } else { + pn_graph_capture_ineligible(&d->slots_graph_capture, gate_reason); + } + if (!graph_ok && sdiag_on() && !d->moe.sdiag_logged) { + d->moe.sdiag_logged = 1; + fprintf(stderr, "[sdiag] layer %2d ATTN moe=%s graph=%s\n", layer, + d->moe.grouped ? "grouped" : "PER-ROW FALLBACK", + !gpu_graph_enabled() ? "off (QWEN35_GRAPH=0)" + : !gpu_graph_slots_enabled() ? "off (QWEN35_GRAPH_SLOTS=0)" + : !gpu_graph_slots_attn_enabled() + ? "never (QWEN35_GRAPH_SLOTS_ATTN=0)" + : sprof_active ? "disabled (sprof active)" + : !d->moe.grouped ? "INELIGIBLE (MoE not grouped)" + : "skipped (slot_base>0)"); + } + fc_rms_norm_nx_ctx(ctx, b->hidden, d->attn_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, nslots); + rc = gpu_attn_block_slots(g, d, cfg, idx, sts, pos, pos_g, b, + slot_base, nslots, total_slots, hb); + if (rc < 0) + return rc; + fc_rms_norm_nx_ctx(ctx, b->hidden, d->post_attention_norm.buf, + b->x_norm, c, cfg->rms_norm_eps, nslots); + return gpu_moe_block_slots(ctx, &d->moe, cfg, b, nslots, die_scr, + host_scr); + } + + /* ---- SSM layer. ---- */ + gpu_ssm_layer *d = &g->ssm[idx]; + gpu_die_scratch *die_scr = d->moe.grouped ? NULL + : gpu_get_scratch(g, ctx, cfg); + if (d->slots_cap < slot_base + nslots || !d->conv_slots || !d->ssm_slots) + return -1; /* slots not bound */ + + /* L9 — replay the captured slots graph (~30 launches -> 1) when the graph + * gate is on, sprof is off (its mid-body syncs would break capture), MoE + * is grouped, and the graph matches this step's slot count. + * L5 — only the base-0 group uses the graph: a per-layer graph slot bakes + * in one slot_base, and alternating two groups' bases per step would drop + + * recapture every layer every step (thrash). The base-0 group keeps a + * stable graph across steps; the other group (slot_base>0) runs direct. */ + int graph_ok = gpu_graph_enabled() && gpu_graph_slots_enabled() && + !sprof_active && d->moe.grouped && slot_base == 0; + + /* A captured graph bakes in the slot count (launch grids), the batch- + * scratch buffer addresses AND (L5) the ring-offset baked from slot_base; + * a change in any invalidates it — drop + recapture. */ + if (d->slots_graph && (d->slots_graph_n != nslots || + d->slots_graph_base != slot_base || + d->slots_graph_scratch != (const void *)b->hidden)) { + /* QWEN35_SDIAG: see the ATTN drop log — one line per drop so a + * per-step recapture cannot hide behind a first-step-only report. */ + if (sdiag_on()) + fprintf(stderr, "[sdiag] layer %2d SSM graph=DROPPED " + "n %d->%d base %d->%d scratch %s\n", + layer, d->slots_graph_n, nslots, + d->slots_graph_base, slot_base, + d->slots_graph_scratch == (const void *)b->hidden + ? "same" : "MOVED"); + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + pn_graph_capture_dropped(&d->slots_graph_capture); + } + + if (graph_ok && d->slots_graph) { + if (fc_graph_launch(d->slots_graph) == 0) + return 0; + /* Replay failed unexpectedly — drop it and run direct this step. */ + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + pn_graph_capture_dropped(&d->slots_graph_capture); + } + + if (!graph_ok) + pn_graph_capture_ineligible(&d->slots_graph_capture, gate_reason); + + /* Capture lazily, and RETRY a failed capture on a bounded backoff (see + * gpu_graph_capture_tracker.h): the recorded launches do NOT execute, so on + * any failure the direct body runs this step and nothing was + * half-applied. */ + if (graph_ok && !d->slots_graph && + pn_graph_capture_should_attempt(&d->slots_graph_capture)) { + int captured = 0; + int begin_ok = fc_graph_begin_ctx(ctx) == 0; + rc = 0; + if (begin_ok) { + rc = gpu_ssm_slots_layer_body(d, ctx, cfg, b, slot_base, nslots, + die_scr, host_scr); + d->slots_graph = fc_graph_end_ctx(ctx); + if (pn_graph_capture_inject_now(&d->slots_graph_capture) && + d->slots_graph) { + fc_graph_free(d->slots_graph); /* fault injection, test only */ + d->slots_graph = NULL; + } + if (rc == 0 && d->slots_graph && + fc_graph_launch(d->slots_graph) == 0) { + d->slots_graph_n = nslots; + d->slots_graph_base = slot_base; + d->slots_graph_scratch = (const void *)b->hidden; + captured = 1; + } else if (d->slots_graph) { + fc_graph_free(d->slots_graph); + d->slots_graph = NULL; + } + } + + if (captured) { + pn_graph_capture_succeeded(&d->slots_graph_capture); + if (sdiag_on() && !d->moe.sdiag_logged) { + d->moe.sdiag_logged = 1; + fprintf(stderr, "[sdiag] layer %2d SSM moe=grouped " + "graph=CAPTURED n=%d base=%d\n", + layer, nslots, slot_base); + } + return 0; + } + pn_graph_capture_failed(&d->slots_graph_capture); + /* Every failure, bounded by the retry budget — see the ATTN log. */ + if (sdiag_on()) + fprintf(stderr, "[sdiag] layer %2d SSM moe=grouped " + "graph=%s (body rc=%d) attempt %d/%d -> %s\n", layer, + begin_ok ? "CAPTURE FAILED" : "BEGIN FAILED", rc, + d->slots_graph_capture.consecutive_failures, + PN_GRAPH_CAPTURE_MAX_FAILURES, + pn_graph_capture_state_name(d->slots_graph_capture.state)); + } + + /* Not graph-eligible at all: say which precondition failed. */ + if (!graph_ok && sdiag_on() && !d->moe.sdiag_logged) { + d->moe.sdiag_logged = 1; + fprintf(stderr, "[sdiag] layer %2d SSM moe=%s graph=%s\n", layer, + d->moe.grouped ? "grouped" : "PER-ROW FALLBACK", + !gpu_graph_enabled() ? "off (QWEN35_GRAPH=0)" + : !gpu_graph_slots_enabled() ? "off (QWEN35_GRAPH_SLOTS=0)" + : sprof_active ? "disabled (sprof active)" + : !d->moe.grouped ? "INELIGIBLE (MoE not grouped)" + : slot_base != 0 ? "skipped (slot_base>0)" + : "direct"); + } + + return gpu_ssm_slots_layer_body(d, ctx, cfg, b, slot_base, nslots, + die_scr, host_scr); +} + +/* ============================================================ + * QWEN35_TWO_THREAD — 2-host-thread die driver (TT) + * + * One persistent worker thread per die, each owning its die's CUDA context + * launch queue. On a K80 in small-BAR mode both dies are live behind their own + * persistent fc_cuda_ctx (no MMIO-slot barswap — g->bs == NULL), and CUDA's + * context stack is thread-local, so two host threads issuing launches to two + * different contexts run their dies' kernel queues in parallel. This removes + * the single-host-thread launch-starvation term (one thread could not enqueue + * both dies' hundreds of small launches fast enough — each die sat ~50% SM even + * when "overlapping"). Reuses the async KV mirror (a per-die thread no longer + * stalls on a blocking per-layer D2H). + * + * Coordination: the calling (control) thread posts a task to each worker and + * blocks on the worker's completion via a per-worker mutex+condvar. Correct + * synchronization only where a real data dependency exists — the die-A→die-B + * boundary hidden-state handoff — expressed as: control waits for worker A's + * done, does the [K][n_embd] handoff, then posts worker B. Everything each + * worker touches (its die's ctx/stream/scratch/rings/defer arena) is per-die; + * the shared host boundary staging (g->bhost) is written by A and read into B + * only under the control thread's ordering, so no two threads touch it at once. + * + * NOTE (measured, honest): under a pure die-A-front / die-B-back LAYER split the + * two dies CANNOT overlap within one decode step — die B's first layer depends + * on die A's boundary output, and the next step's die-A front depends on this + * step's sampled token (autoregressive). So TT alone does not create + * concurrency for a single-batch step; it is the launch-parallel infrastructure + * that a row-split TP scheme (both dies every layer) would ride on. Default OFF; + * bit-exact to the sequential path. Process-constant gate. */ +struct qw_tt { + pthread_t th[2]; + pthread_mutex_t mtx[2]; + pthread_cond_t cv_go[2]; /* control -> worker: task ready */ + pthread_cond_t cv_done[2]; /* worker -> control: task complete */ + int go[2]; /* task pending for worker i */ + int done[2]; /* worker i finished its task */ + int rc[2]; /* worker i return code */ + int quit; /* teardown flag */ + pthread_mutex_t claim_mtx; /* serializes worker index self-assignment */ + int claim_next; /* next free worker index (0 then 1) */ + /* Task payload (front/back layer range for the current step). */ + pn_qwen35moe_model *m; + pn_qwen35moe_gpu *g; + const pn_qwen35moe_config *cfg; + pn_qwen35moe_state *const *sts; + const int *pos; + fc_buffer *pos_g[2]; /* per-die uploaded positions */ + gpu_batch_scratch *bscr[2]; /* per-die batch scratch */ + int layer_lo[2]; + int layer_hi[2]; + int nslots; +}; + +static void *qw_tt_worker(void *arg) +{ + struct qw_tt *tt = (struct qw_tt *)arg; + /* Each worker claims its die index once, race-free, under claim_mtx: the + * first thread to reach here takes 0 (die A / ctx_a), the second takes 1 + * (die B / ctx_b). This avoids depending on tt->th[] being published. */ + pthread_mutex_lock(&tt->claim_mtx); + int which = tt->claim_next++; + pthread_mutex_unlock(&tt->claim_mtx); + for (;;) { + pthread_mutex_lock(&tt->mtx[which]); + while (!tt->go[which] && !tt->quit) + pthread_cond_wait(&tt->cv_go[which], &tt->mtx[which]); + if (tt->quit) { + pthread_mutex_unlock(&tt->mtx[which]); + return NULL; + } + tt->go[which] = 0; + pthread_mutex_unlock(&tt->mtx[which]); + + /* Run this die's assigned layer range on the full batch. Each + * gpu_layer_forward_slots targets tt->g->ctx[0]/ctx_b directly (per-die + * ctx); the fc_* calls push/pop that context on THIS thread's stack. */ + fc_cuda_ctx *ctx = (which == 0) ? tt->g->ctx[0] : tt->g->head_ctx; + int rc = 0; + for (int layer = tt->layer_lo[which]; + layer < tt->layer_hi[which] && rc == 0; layer++) { + rc = gpu_layer_forward_slots(tt->m, tt->g, ctx, layer, + tt->bscr[which], tt->sts, tt->pos, + tt->pos_g[which], 0, tt->nslots, + tt->nslots, tt->g->bhost); + } + /* Drain this die's stream so the control thread's post-work (the + * boundary handoff read, or the head) sees committed results. */ + if (rc == 0) + fc_sync_ctx(ctx); + + pthread_mutex_lock(&tt->mtx[which]); + tt->rc[which] = rc; + tt->done[which] = 1; + pthread_cond_signal(&tt->cv_done[which]); + pthread_mutex_unlock(&tt->mtx[which]); + } +} + +static void qw_tt_post(struct qw_tt *tt, int which) +{ + pthread_mutex_lock(&tt->mtx[which]); + tt->done[which] = 0; + tt->go[which] = 1; + pthread_cond_signal(&tt->cv_go[which]); + pthread_mutex_unlock(&tt->mtx[which]); +} + +static int qw_tt_wait(struct qw_tt *tt, int which) +{ + pthread_mutex_lock(&tt->mtx[which]); + while (!tt->done[which]) + pthread_cond_wait(&tt->cv_done[which], &tt->mtx[which]); + int rc = tt->rc[which]; + pthread_mutex_unlock(&tt->mtx[which]); + return rc; +} + +static void qw_tt_free(pn_qwen35moe_gpu *g) +{ + struct qw_tt *tt = g->tt; + if (!tt) + return; + tt->quit = 1; + for (int i = 0; i < 2; i++) { + pthread_mutex_lock(&tt->mtx[i]); + pthread_cond_signal(&tt->cv_go[i]); + pthread_mutex_unlock(&tt->mtx[i]); + } + for (int i = 0; i < 2; i++) + pthread_join(tt->th[i], NULL); + for (int i = 0; i < 2; i++) { + pthread_mutex_destroy(&tt->mtx[i]); + pthread_cond_destroy(&tt->cv_go[i]); + pthread_cond_destroy(&tt->cv_done[i]); + } + pthread_mutex_destroy(&tt->claim_mtx); + free(tt); + g->tt = NULL; +} + +/* Lazily spin up the two worker threads. Returns the handle or NULL (caller + * falls back to the sequential path). The workers claim indices 0/1 by the + * order they are created: we create th[0] first and let it settle before th[1] + * so pthread_equal against th[0] is deterministic. */ +static struct qw_tt *qw_tt_ensure(pn_qwen35moe_gpu *g) +{ + if (g->tt) + return g->tt; + struct qw_tt *tt = calloc(1, sizeof(*tt)); + if (!tt) + return NULL; + tt->g = g; + pthread_mutex_init(&tt->claim_mtx, NULL); + tt->claim_next = 0; + for (int i = 0; i < 2; i++) { + pthread_mutex_init(&tt->mtx[i], NULL); + pthread_cond_init(&tt->cv_go[i], NULL); + pthread_cond_init(&tt->cv_done[i], NULL); + } + /* Create worker 0 first; it will observe th[1] == 0 and claim which==0. + * Then create worker 1. To make the pthread_equal test robust we set th[i] + * before creating the thread is impossible; instead each worker computes + * `which` from th[0], so worker 1 (created after th[0] is set) sees + * !equal(self, th[0]) => 1, and worker 0 sees equal => 0. Serialize the + * two creations so th[0] is fully published before worker 1 starts. */ + if (pthread_create(&tt->th[0], NULL, qw_tt_worker, tt) != 0) { + free(tt); + return NULL; + } + if (pthread_create(&tt->th[1], NULL, qw_tt_worker, tt) != 0) { + tt->quit = 1; + pthread_mutex_lock(&tt->mtx[0]); + pthread_cond_signal(&tt->cv_go[0]); + pthread_mutex_unlock(&tt->mtx[0]); + pthread_join(tt->th[0], NULL); + free(tt); + return NULL; + } + g->tt = tt; + return tt; +} + +/* QWEN35_TWO_THREAD gate (default OFF). Two dies only; small-BAR mode (no + * barswap: g->bs == NULL); nslots>=1. Process-constant. */ +static int two_thread_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_TWO_THREAD"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* QWEN35_DIE_PIPELINE gate (default OFF). When on (and two dies, grouped MoE, + * nslots>=2) the batched-decode step software-pipelines two batch groups + * across the die pair so both physical dies compute at once (L5). Off = the + * proven single-group sequential path runs untouched. Process-constant. */ +static int die_pipeline_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_DIE_PIPELINE"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* T3 — vocab-split LM head across the 2 dies (QWEN35_VOCAB_SPLIT). Default + * OFF; only meaningful in 2-die non-barswap mode with peer access. */ +static int vocab_split_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_VOCAB_SPLIT"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* QWEN35_VOCAB_SPLIT_FULL_LOGITS=1 — serve a constrained slot its FULL row FROM + * the split head (two offset D2Hs, one per die half) instead of abandoning the + * split for the whole batch. Default OFF: it is a decode-path change on the + * live tool-calling shape and has not been through the agent-client gate. */ +static int vocab_split_full_logits_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_VOCAB_SPLIT_FULL_LOGITS"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* ------------------------------------------------------------------ + * T3PROF — where does the vocab-split head's predicted saving actually go? + * + * The per-kernel model says a 50/50 row split of the 248320x2048 Q6_K head + * (microbenchmark eta 1.993) should return ~2.4 ms of the phase table's 4.8 ms + * lm_head. The serve measures ~1.01 ms. That ~1.4 ms gap is the single largest + * uncertainty in the two-die campaign, because it is ALSO the calibration point + * for whether per-kernel models over-predict on this engine generally (in which + * case tensor parallelism is 1.09-1.15x, not 1.33-1.44x) or whether T3 just has + * its own overhead that nothing else would inherit. + * + * So: time every stage of both head paths on the host, with an explicit drain + * at each boundary so the attribution is real rather than "whatever had not + * been enqueued yet". The drains INFLATE the totals -- read the split, not the + * absolutes, exactly as QWEN35_PROF_SUB is read. Enable with + * QWEN35_T3PROF= (any non-numeric value means 32). + * ------------------------------------------------------------------ */ +enum { + T3P_NORM = 0, /* final RMS norm (both paths) */ + T3P_SYNCB, /* split: fc_sync_ctx(ctx_b) before the x_norm peer copy */ + T3P_COPY, /* split: fc_buffer_copy_peer of x_norm, 8 KB/slot */ + T3P_SYNCA, /* split: fc_sync_ctx(ctx_a) after it */ + T3P_MATVEC, /* split: BOTH half-head matvecs, drained (concurrent) */ + T3P_TOPKA, /* split: die A's top-k over its half */ + T3P_TOPKB, /* split: die B's top-k over its half */ + T3P_MERGE, /* split: host 2-way merge + present */ + T3P_FULL_MV, /* single-die: the whole 248320-row head, drained */ + T3P_FULL_PRES, /* single-die: gpu_present_slots_logits (top-k + D2H) */ + T3P_COUNT +}; +static const char *t3p_name[T3P_COUNT] = { + "rms_norm", "sync(B)", "copy_peer", "sync(A)", "matvec_both", + "topk_A", "topk_B", "host_merge", "matvec_full", "present_full" +}; +static double t3p_cum[T3P_COUNT]; +static long t3p_nsplit, t3p_nfull; + +static int t3prof_every(void) +{ + static int v = -1; + if (v < 0) { + const char *e = getenv("QWEN35_T3PROF"); + if (!e || !e[0]) v = 0; + else { v = atoi(e); if (v <= 0) v = 32; } + } + return v; +} + +static void t3p_lap(int slot, double *t0) +{ + double now = prof_now(); + t3p_cum[slot] += now - *t0; + *t0 = now; +} + +/* Report + reset. Called from whichever head path ran. */ +static void t3p_report(void) +{ + int i; + long n = t3p_nsplit ? t3p_nsplit : t3p_nfull; + double tot = 0.0; + if (n <= 0) + return; + for (i = 0; i < T3P_COUNT; i++) + tot += t3p_cum[i]; + fprintf(stderr, "[T3PROF] %s head, %ld steps, %.3f ms/step total:", + t3p_nsplit ? "SPLIT" : "single-die", n, tot * 1e3 / n); + for (i = 0; i < T3P_COUNT; i++) + if (t3p_cum[i] > 0.0) + fprintf(stderr, " %s=%.3f", t3p_name[i], t3p_cum[i] * 1e3 / n); + fprintf(stderr, "\n"); + fflush(stderr); + memset(t3p_cum, 0, sizeof(t3p_cum)); + t3p_nsplit = t3p_nfull = 0; +} + +/* Run one group's FRONT layers [0, front_end) on die A's ctx: embed the + * group's tokens into hb[0], H2D into b->hidden, run the front layers. The + * ring/scratch offsetting is handled inside gpu_layer_forward_slots via + * slot_base. Leaves work queued on ctx_a's stream (no sync). Returns 0/-1. */ +static int gpu_group_front(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + const pn_qwen35moe_config *cfg, + const uint32_t *tokens, + pn_qwen35moe_state *const *sts, const int *pos, + gpu_batch_scratch *ba, fc_buffer *pos_a, + int slot_base, int count, int total_slots, + float *const hb[4], int front_end) +{ + int c = cfg->n_embd; + for (int u = 0; u < count; u++) + pn_qwen35moe_model_embed_row(m, tokens[slot_base + u], + hb[0] + (size_t)u * c); + fc_buffer_from_cpu_n(ba->hidden, hb[0], (size_t)count * c * sizeof(float)); + for (int layer = 0; layer < front_end; layer++) { + if (gpu_layer_forward_slots(m, g, g->ctx[0], layer, ba, sts + slot_base, + pos + slot_base, pos_a, slot_base, count, + total_slots, hb) < 0) + return -1; + } + return 0; +} + +/* Run one group's BACK layers [split, n_layer) on die B's ctx from the handed- + * off hidden already in hb[0] (H2D into bb->hidden), then leave work queued on + * ctx_b (no sync). Returns 0/-1. */ +static int gpu_group_back(pn_qwen35moe_model *m, pn_qwen35moe_gpu *g, + const pn_qwen35moe_config *cfg, + pn_qwen35moe_state *const *sts, const int *pos, + gpu_batch_scratch *bb, fc_buffer *pos_b, + int slot_base, int count, int total_slots, + float *const hb[4], int split, int n_layer) +{ + int c = cfg->n_embd; + fc_buffer_from_cpu_n(bb->hidden, hb[0], (size_t)count * c * sizeof(float)); + for (int layer = split; layer < n_layer; layer++) { + if (gpu_layer_forward_slots(m, g, g->head_ctx, layer, bb, sts + slot_base, + pos + slot_base, pos_b, slot_base, count, + total_slots, hb) < 0) + return -1; + } + return 0; +} + +/* Final norm + LM head + per-user top-k for one group's rows [slot_base, + * slot_base+count), reusing g->spec_logits [count][n_vocab]. Runs on die B's + * ctx (b = bb). Must be called after that group's back layers have synced. + * Returns 0/-1. */ +static int gpu_group_head(pn_qwen35moe_gpu *g, fc_cuda_ctx *ctx, + const pn_qwen35moe_config *cfg, gpu_batch_scratch *b, + gpu_die_scratch *sc, pn_qwen35moe_state *const *sts, + int slot_base, int count) +{ + int c = cfg->n_embd; + fc_rms_norm_nx_ctx(ctx, b->hidden, g->output_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, count); + if (g->spec_logits_cap < count) { + fc_buffer_free(g->spec_logits); + g->spec_logits = fc_buffer_alloc_ctx(ctx, (size_t)count * + cfg->n_vocab * sizeof(float)); + g->spec_logits_cap = g->spec_logits ? count : 0; + if (!g->spec_logits_cap) + return -1; + } + if (gpu_matvec_nx(&g->output, b->x_norm, g->spec_logits, count) < 0) + return -1; + return gpu_present_slots_logits(ctx, g->spec_logits, sc, sts + slot_base, + count, cfg->n_vocab); +} + +/* T3 — vocab-split LM head. `b->x_norm` [nslots][n_embd] is already computed + * on die B (ctx_b). Split the head: die B computes the BOTTOM half + * [vocab_half, n_vocab) local logits + top-k, die A computes the TOP half + * [0, vocab_half) local logits + top-k (from an x_norm mirror P2P-copied from + * die B), then the two descending per-slot top-k lists are merged order-stably + * on the host and presented per slot. Bit-exact to the single-die head: each + * die's per-row top-k is the exact iterative argmax over ITS vocab range, and + * the 2-way merge (larger value first; ties → the die-A element, whose global + * index is < vocab_half ≤ every die-B index) reproduces the single argmax's + * value-desc / lowest-index tiebreak over the full vocab. Runs ONCE per step, + * outside the layer loop, so there is no per-layer sync hazard. Returns 0 on + * success, -1 on any error (caller falls back to the single-die head). */ +static int gpu_split_head_slots(pn_qwen35moe_gpu *g, + const pn_qwen35moe_config *cfg, + gpu_batch_scratch *bb, + pn_qwen35moe_state *const *sts, int nslots) +{ + int c = cfg->n_embd; + int nv = cfg->n_vocab; + int half = g->vocab_half; /* die A rows [0,half) */ + int nb = nv - half; /* die B rows [half,nv) */ + int k = gpu_topk_k(); + /* How many slots need the whole row this step (grammar mask live, + * logit_bias, logprobs). The split head keeps each half's logits on its + * OWN die — spec_logits_a [K][half] on A, spec_logits_b [K][nb] on B — so + * serving one of those slots means two offset D2H copies off two dies + * instead of one contiguous copy off one. That is the entire difference, + * and it is why the whole batch used to be dropped off the split head the + * moment any single slot asked: worth +1.9% K=1 / +3.0% K=2, given back + * because one user sent `tools`. QWEN35_VOCAB_SPLIT_FULL_LOGITS=1 does the + * two copies instead. */ + int n_full = 0; + for (int u = 0; u < nslots; u++) { + if (!sts[u]) return -1; /* same guard gpu_present_slots_logits makes */ + if (sts[u]->full_logits) n_full++; + } + if (n_full > 0 && !vocab_split_full_logits_on()) + k = 0; /* legacy: no split for this batch */ + if (k <= 0 || half <= 0 || nb <= 0) + return -1; /* full-logits mode: no split */ + + /* die-A x_norm mirror + die-A logits buffer (lazily sized). */ + if (g->xnorm_a_cap < nslots) { + fc_buffer_free(g->xnorm_a); + g->xnorm_a = fc_buffer_alloc_ctx(g->ctx[0], + (size_t)nslots * c * sizeof(float)); + g->xnorm_a_cap = g->xnorm_a ? nslots : 0; + if (!g->xnorm_a_cap) + return -1; + } + if (g->spec_logits_a_cap < nslots) { + fc_buffer_free(g->spec_logits_a); + g->spec_logits_a = fc_buffer_alloc_ctx(g->ctx[0], + (size_t)nslots * half * sizeof(float)); + g->spec_logits_a_cap = g->spec_logits_a ? nslots : 0; + if (!g->spec_logits_a_cap) + return -1; + } + /* die-B logits buffer (bottom half) — a DEDICATED [K][nb] buffer. */ + if (g->spec_logits_b_cap < nslots) { + fc_buffer_free(g->spec_logits_b); + g->spec_logits_b = fc_buffer_alloc_ctx(g->head_ctx, + (size_t)nslots * nb * sizeof(float)); + g->spec_logits_b_cap = g->spec_logits_b ? nslots : 0; + if (!g->spec_logits_b_cap) + return -1; + } + + /* P2P the [K][n_embd] x_norm from die B -> die A. bb->x_norm is on ctx_b; + * xnorm_a on ctx_a. Sync die B so x_norm is fully written, then copy, then + * sync die A so the copied data is committed and stream-ordered before the + * die-A matvec reads it. */ + double t3t = 0.0; + int t3p = t3prof_every(); + if (t3p) t3t = prof_now(); + + fc_sync_ctx(g->head_ctx); + if (t3p) t3p_lap(T3P_SYNCB, &t3t); + if (fc_buffer_copy_peer(g->xnorm_a, bb->x_norm, + (size_t)nslots * c * sizeof(float)) != 0) + return -1; + if (t3p) t3p_lap(T3P_COPY, &t3t); + fc_sync_ctx(g->ctx[0]); + if (t3p) t3p_lap(T3P_SYNCA, &t3t); + + /* die-A top-half matvec + local top-k, die-B bottom-half matvec + local + * top-k. The two run on separate real devices. */ + if (gpu_matvec_nx(&g->output_a, g->xnorm_a, g->spec_logits_a, nslots) < 0) + return -1; + if (gpu_matvec_nx(&g->output_b, bb->x_norm, g->spec_logits_b, nslots) < 0) + return -1; + if (t3p) { + /* Drain BOTH so the concurrent matvec pair is attributed to itself and + * not smeared into whichever top-k call happens to block first. */ + fc_sync_ctx(g->ctx[0]); + fc_sync_ctx(g->head_ctx); + t3p_lap(T3P_MATVEC, &t3t); + } + + /* Per-die per-slot top-k. Die A indices are already global [0,half); die B + * local indices [0,nb) are offset by +half to global. Both lists come back + * value-descending (iterative argmax). + * + * Skipped entirely when EVERY slot wants the full row (K=1 with a live + * grammar — the single-user tool-calling shape): there is nothing to reduce + * for, and running the reduction to throw it away would cost 2 x 81 + * launches. The two halves then still have to be COMMITTED before the + * copies below, which run on the legacy NULL stream while the matvecs are on + * each ctx's own stream — the same explicit-sync rule + * gpu_present_slots_logits documents. When the reduction does run it syncs + * both dies itself. */ + int ids_a[PN_QWEN35MOE_GPU_MAX_SLOTS * FC_TOPK_MAX]; + float vals_a[PN_QWEN35MOE_GPU_MAX_SLOTS * FC_TOPK_MAX]; + int ids_b[PN_QWEN35MOE_GPU_MAX_SLOTS * FC_TOPK_MAX]; + float vals_b[PN_QWEN35MOE_GPU_MAX_SLOTS * FC_TOPK_MAX]; + int reduced = (n_full < nslots); + if (reduced) { + if (fc_topk_logits_slots_ctx(g->ctx[0], g->spec_logits_a, nslots, half, k, + ids_a, vals_a) != 0) + return -1; + if (t3p) t3p_lap(T3P_TOPKA, &t3t); + if (fc_topk_logits_slots_ctx(g->head_ctx, g->spec_logits_b, nslots, nb, k, + ids_b, vals_b) != 0) + return -1; + if (t3p) t3p_lap(T3P_TOPKB, &t3t); + } else { + fc_sync_ctx(g->ctx[0]); + fc_sync_ctx(g->head_ctx); + } + + /* Present each slot: a constrained slot gets its FULL row assembled from + * the two halves, everything else the order-stable 2-way merge of the two + * top-k lists, truncated to k. + * + * The full row is bit-identical to the single-die head's. Each output row + * is an independent dot product of the same x_norm (die A reads a byte-copy + * of die B's) against the same Q6_K weight row, the split is at a ROW + * boundary (half = n_vocab/2, no K-block is cut), and fc_matvec_nx_launch + * dispatches on the BATCH width, which both halves share — so row i of + * spec_logits_a/b holds exactly the float the full head would have written. + * The top-k above clobbers rows during its rounds and restores them before + * it syncs, which is why the copies are taken after it and not before. */ + for (int u = 0; u < nslots; u++) { + if (sts[u]->full_logits) { + fc_buffer_to_cpu_at(g->spec_logits_a, sts[u]->logits, + (size_t)u * half * sizeof(float), + (size_t)half * sizeof(float)); + fc_buffer_to_cpu_at(g->spec_logits_b, sts[u]->logits + half, + (size_t)u * nb * sizeof(float), + (size_t)nb * sizeof(float)); + sts[u]->topk_filled = 0; /* the row holds real values everywhere */ + continue; + } + const int *ia = ids_a + (size_t)u * k; + const float *va = vals_a + (size_t)u * k; + const int *ib = ids_b + (size_t)u * k; + const float *vb = vals_b + (size_t)u * k; + int mids[FC_TOPK_MAX]; + float mvals[FC_TOPK_MAX]; + int ai = 0, bi = 0; + for (int i = 0; i < k; i++) { + /* Take the larger value; on a tie prefer die A (lower global + * index) to match the single argmax's lowest-index tiebreak. */ + if (bi >= k || (ai < k && va[ai] >= vb[bi])) { + mvals[i] = va[ai]; + mids[i] = ia[ai]; /* already global [0,half) */ + ai++; + } else { + mvals[i] = vb[bi]; + mids[i] = ib[bi] + half; /* local -> global */ + bi++; + } + } + gpu_present_topk(sts[u], mids, mvals, k, nv); + } + if (t3p) { + t3p_lap(T3P_MERGE, &t3t); + if (++t3p_nsplit >= t3p) + t3p_report(); + } + return 0; +} + +/* Shared final-norm + LM head + per-slot top-k presentation, run on `ctx` + * (die B in two-die mode, the sole die otherwise). Factored out so both the + * sequential and the QWEN35_TWO_THREAD drivers use byte-identical head math. + * Mirrors the tail of the sequential decode: final RMS norm over all K rows, + * then either the T3 vocab-split head (2-die, peer-enabled, ctx==ctx_b) or the + * single batched LM head + L8 batched top-k, presenting each slot's sparse + * top-k into sts[u]->logits. `sp_step0` is the profiler step-start stamp (only + * read when sprof is active). Returns 0 on success, -1 on error. */ +static int gpu_head_from_slots(pn_qwen35moe_gpu *g, + const pn_qwen35moe_config *cfg, fc_cuda_ctx *ctx, + gpu_batch_scratch *b, gpu_die_scratch *sc, + pn_qwen35moe_state *const *sts, int nslots, + int two_die, double sp_step0) +{ + int c = cfg->n_embd; + double sp_t0 = 0.0; + double t3t = 0.0; + int t3p = t3prof_every(); + sprof_begin(ctx, &sp_t0); + if (t3p) t3t = prof_now(); + fc_rms_norm_nx_ctx(ctx, b->hidden, g->output_norm.buf, b->x_norm, c, + cfg->rms_norm_eps, nslots); + if (t3p) { fc_sync_ctx(ctx); t3p_lap(T3P_NORM, &t3t); } + + if (g->vocab_split_ready && vocab_split_on() && two_die && + ctx == g->head_ctx && + gpu_split_head_slots(g, cfg, b, sts, nslots) == 0) { + sprof_lap(SPROF_LM_HEAD, ctx, &sp_t0); + sprof_lap(SPROF_LOGITS, ctx, &sp_t0); + if (sprof_active) { + sprof_step_report(nslots, prof_now() - sp_step0); + sprof_active = 0; + } + return 0; + } + + if (g->spec_logits_cap < nslots) { + fc_buffer_free(g->spec_logits); + g->spec_logits = fc_buffer_alloc_ctx(ctx, (size_t)nslots * + cfg->n_vocab * sizeof(float)); + g->spec_logits_cap = g->spec_logits ? nslots : 0; + if (!g->spec_logits_cap) + return -1; + } + if (gpu_matvec_nx(&g->output, b->x_norm, g->spec_logits, nslots) < 0) + return -1; + if (t3p) { fc_sync_ctx(ctx); t3p_lap(T3P_FULL_MV, &t3t); } + sprof_lap(SPROF_LM_HEAD, ctx, &sp_t0); + if (gpu_present_slots_logits(ctx, g->spec_logits, sc, sts, nslots, + cfg->n_vocab) < 0) + return -1; + if (t3p) { + t3p_lap(T3P_FULL_PRES, &t3t); + if (++t3p_nfull >= t3p) + t3p_report(); + } + sprof_lap(SPROF_LOGITS, ctx, &sp_t0); + if (sprof_active) { + sprof_step_report(nslots, prof_now() - sp_step0); + sprof_active = 0; + } + return 0; +} + +/* One batched decode step: K users, tokens[u] at position pos[u] of user + * u's own sequence, in ONE pass. Each user's logits land in its own + * sts[u]->logits (device top-k sparse presentation, same as single-user + * decode). Slot u must be bound to sts[u]. Does NOT advance any position + * bookkeeping (the pn_model adapter does). Returns 0 on success. */ +int pn_qwen35moe_decode_slots_tokens_cuda(pn_qwen35moe_model *m, + const uint32_t *tokens, + pn_qwen35moe_state *const *sts, + const int *pos, int nslots) +{ + if (!m || !tokens || !sts || !pos || nslots <= 0 || + nslots > PN_QWEN35MOE_GPU_MAX_SLOTS) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g || g->n_slots < nslots) + return -1; + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int c = cfg->n_embd; + int n_layer = cfg->n_layer; + for (int u = 0; u < nslots; u++) { + if ((int)tokens[u] >= cfg->n_vocab || !sts[u] || + g->slot_state[u] != (const void *)sts[u]) + return -1; + } + + int two_die = (g->head_ctx != NULL); + int split = g->split_layer; + + if (!g->host_ok) { + if (gpu_host_scratch_init(&g->host, cfg) < 0) + return -1; + g->host_ok = 1; + } + if (gpu_get_batch_host(g, cfg, nslots) < 0) + return -1; + + /* QWEN35_ASYNC_KVMIRROR: pre-size + reset each active die's deferred-mirror + * arena for this step (see qw_kv_defer). Pre-sizing to the full step up + * front means the arena never reallocs mid-step, so in-flight async DtoH + * destinations stay valid. On OOM, disable the deferral for this step by + * leaving the arenas at n=0 (the flash path then falls back to blocking). */ + if (async_kvmirror_on()) { + int kv_dim0 = cfg->n_head_kv * cfg->head_dim; + for (int d = 0; d < g->n_die; d++) + if (qw_defer_reset(&g->defer[d], g->n_attn, nslots, kv_dim0) < 0) + g->defer[d].stage_bytes = 0; /* used>bytes => stage() fails */ + } + + /* M2 slots-path profiler: arm the phase brackets for this step. */ + double sp_step0 = 0.0, sp_t0 = 0.0; + if (sprof_on()) { + sprof_active = 1; + memset(sprof_step_ph, 0, sizeof(sprof_step_ph)); + sp_step0 = prof_now(); + } + + /* ============================================================ + * L5 — two-die-pair interleave (QWEN35_DIE_PIPELINE). Software-pipeline + * two batch groups so both physical dies compute at once: group0-die-B + * overlaps group1-die-A (separate contexts = the two dies run in + * parallel). Bit-exact — independent users, unchanged per-user math, only + * the launch ordering + per-group slot_base ring offsetting differ. + * Off / not exactly two dies / <2 slots / sprof-on falls to the sequential + * path. The interleave is a TWO-stage software pipeline by construction — + * one front group overlapping one back group — so it runs at exactly two + * dies; a wider split would need a deeper pipeline with as many groups as + * stages, which is separate work and would not pay on this hardware (each + * extra stage adds a boundary hop, and the measured 2-die overlap is only + * 1.33-1.44x to begin with). + * Requires small-BAR mode (g->bs == NULL): the two contexts are separate + * real CUDA devices that run concurrently. Under legacy barswap the dies + * time-share one MMIO slot (no true overlap), so the pipeline is skipped. + * ============================================================ */ + if (g->n_die == 2 && !g->bs && die_pipeline_on() && nslots >= 2 && + !sprof_active) { + int h = nslots / 2; /* group0 = [0,h), group1 = [h,K) */ + int n0 = h, n1 = nslots - h; + /* Ensure the second host-staging set (group1) exists. */ + if (!g->bhost2[0]) { + size_t per_pos = 2 * (size_t)cfg->head_dim * cfg->n_head; + if (per_pos < (size_t)cfg->conv_dim) + per_pos = (size_t)cfg->conv_dim; + for (int i = 0; i < 4; i++) { + g->bhost2[i] = malloc((size_t)g->bhost_cap * per_pos * + sizeof(float)); + if (!g->bhost2[i]) + return -1; + } + } + float *const *hb0 = g->bhost; /* group0 staging */ + float *const *hb1 = g->bhost2; /* group1 staging */ + + gpu_batch_scratch *ba = gpu_get_batch_scratch(g, g->ctx[0], cfg, nslots); + gpu_batch_scratch *bb = gpu_get_batch_scratch(g, g->head_ctx, cfg, nslots); + gpu_die_scratch *sc_b = gpu_get_scratch(g, g->head_ctx, cfg); + if (!ba || !bb || !sc_b) + return -1; + + /* --- group0 front on die A --- */ + if (gpu_select_die(g, g->die[0]) < 0) + return -1; + fc_buffer *pos_a = gpu_pos_buf_upload(g, g->ctx[0], 0, pos, n0); + if (!pos_a || + gpu_group_front(m, g, cfg, tokens, sts, pos, ba, pos_a, + 0, n0, nslots, hb0, split) < 0) + return -1; + /* handoff group0 A->B: sync die A, D2H group0 hidden into hb0[0]. */ + fc_sync_ctx(g->ctx[0]); + fc_buffer_to_cpu_n(ba->hidden, hb0[0], (size_t)n0 * c * sizeof(float)); + + /* --- launch group1 front on die A (async, NO sync) --- */ + pos_a = gpu_pos_buf_upload(g, g->ctx[0], 0, pos + h, n1); + if (!pos_a || + gpu_group_front(m, g, cfg, tokens, sts, pos, ba, pos_a, + h, n1, nslots, hb1, split) < 0) + return -1; + + /* --- group0 back on die B (async) — runs on the OTHER physical die + * concurrently with group1's die-A work just launched above --- */ + if (gpu_select_die(g, g->head_die) < 0) + return -1; + fc_buffer *pos_b = gpu_pos_buf_upload(g, g->head_ctx, 1, pos, n0); + if (!pos_b || + gpu_group_back(m, g, cfg, sts, pos, bb, pos_b, + 0, n0, nslots, hb0, split, n_layer) < 0) + return -1; + + /* group0 head (die B) — sync die B first, then norm+head+topk. */ + fc_sync_ctx(g->head_ctx); + if (gpu_group_head(g, g->head_ctx, cfg, bb, sc_b, sts, 0, n0) < 0) + return -1; + + /* --- handoff group1 A->B: sync die A (group1 front done), D2H --- */ + if (gpu_select_die(g, g->die[0]) < 0) + return -1; + fc_sync_ctx(g->ctx[0]); + fc_buffer_to_cpu_n(ba->hidden, hb1[0], (size_t)n1 * c * sizeof(float)); + + /* --- group1 back on die B, then group1 head --- */ + if (gpu_select_die(g, g->head_die) < 0) + return -1; + pos_b = gpu_pos_buf_upload(g, g->head_ctx, 1, pos + h, n1); + if (!pos_b || + gpu_group_back(m, g, cfg, sts, pos, bb, pos_b, + h, n1, nslots, hb1, split, n_layer) < 0) + return -1; + fc_sync_ctx(g->head_ctx); + if (gpu_group_head(g, g->head_ctx, cfg, bb, sc_b, sts, h, n1) < 0) + return -1; + /* Replay both dies' deferred host-KV mirror stores (both dies are drained + * by their group syncs above) before returning, so the mirror is current + * for the next step's bind/move/ring re-sync. */ + if (async_kvmirror_on()) { + qw_defer_flush(g, 0, g->ctx[0]); + qw_defer_flush(g, 1, g->head_ctx); + } + return 0; + } + + /* ============================================================ + * QWEN35_TWO_THREAD — 2-host-thread layer-split driver. Runs the SAME + * front(die A)/back(die B) layer split as the sequential path below, but + * each die's layer range is issued from its own persistent worker thread so + * the two dies' launch queues are fed in parallel (removes the single-thread + * launch-starvation term). The die-A->die-B boundary hidden handoff is a + * real data dependency: worker A must finish before the handoff, which must + * finish before worker B starts — so for a single layer-split step the two + * dies do NOT overlap (see the honest note at struct qw_tt). Bit-exact to + * the sequential path (identical op sequence per die; only the host-side + * issuer differs). Only EXACTLY two dies + small-BAR (g->bs==NULL) — the + * worker pool is a pair, one thread per die. After the back layers this + * sets ctx/b/sc to die B and FALLS THROUGH to the shared head. + * ============================================================ */ + if (two_thread_on() && g->n_die == 2 && !g->bs) { + struct qw_tt *tt = qw_tt_ensure(g); + if (tt) { + gpu_batch_scratch *ba = gpu_get_batch_scratch(g, g->ctx[0], cfg, + nslots); + gpu_die_scratch *sc_a = gpu_get_scratch(g, g->ctx[0], cfg); + fc_buffer *pos_a = gpu_pos_buf_upload(g, g->ctx[0], 0, pos, nslots); + if (!ba || !sc_a || !pos_a) + return -1; + + /* Embed + seed die A's residual (control thread; die A stream idle). */ + for (int u = 0; u < nslots; u++) + pn_qwen35moe_model_embed_row(m, tokens[u], + g->bhost[0] + (size_t)u * c); + fc_buffer_from_cpu_n(ba->hidden, g->bhost[0], + (size_t)nslots * c * sizeof(float)); + + /* Worker A: front layers [0,split) on die A, full K. */ + tt->m = m; tt->g = g; tt->cfg = cfg; tt->sts = sts; tt->pos = pos; + tt->nslots = nslots; + tt->bscr[0] = ba; tt->pos_g[0] = pos_a; + tt->layer_lo[0] = 0; tt->layer_hi[0] = split; + qw_tt_post(tt, 0); + if (qw_tt_wait(tt, 0) != 0) + return -1; + + /* Boundary hand-off (control thread): die A drained by its worker's + * fc_sync_ctx. D2H the boundary hidden, flush die A's KV mirror, then + * seed die B and post worker B for the back layers. */ + fc_buffer_to_cpu_n(ba->hidden, g->bhost[0], + (size_t)nslots * c * sizeof(float)); + if (async_kvmirror_on()) + qw_defer_flush(g, 0, g->ctx[0]); + + gpu_batch_scratch *bb = gpu_get_batch_scratch(g, g->head_ctx, cfg, + nslots); + gpu_die_scratch *sc_b = gpu_get_scratch(g, g->head_ctx, cfg); + fc_buffer *pos_b = gpu_pos_buf_upload(g, g->head_ctx, 1, pos, nslots); + if (!bb || !sc_b || !pos_b) + return -1; + fc_buffer_from_cpu_n(bb->hidden, g->bhost[0], + (size_t)nslots * c * sizeof(float)); + + tt->bscr[1] = bb; tt->pos_g[1] = pos_b; + tt->layer_lo[1] = split; tt->layer_hi[1] = n_layer; + qw_tt_post(tt, 1); + if (qw_tt_wait(tt, 1) != 0) + return -1; + + if (async_kvmirror_on()) + qw_defer_flush(g, 1, g->head_ctx); + + /* Fall through to the shared final-norm + LM head on die B. */ + gpu_head_from_slots(g, cfg, g->head_ctx, bb, sc_b, sts, nslots, two_die, + sp_step0); + return 0; + } + /* qw_tt_ensure failed: fall through to the sequential path. */ + } + + /* ---- Sequential N-die walk: die 0 embeds all K tokens, each die runs + * its layer range against its own scratch, and each boundary hands the + * [K][c] activations to the next die. ---- */ + fc_cuda_ctx *ctx = NULL; + gpu_batch_scratch *b = NULL; + gpu_die_scratch *sc = NULL; + size_t hidden_bytes = (size_t)nslots * c * sizeof(float); + + for (int d = 0; d < g->n_die; d++) { + if (d > 0) { + sprof_begin(g->ctx[d - 1], &sp_t0); + gpu_handoff_export(g, d - 1, b->hidden, g->bhost[0], hidden_bytes); + /* The die we are leaving is drained by the export's sync, and is + * still the selected one: replay its deferred host-KV mirror + * stores here, so the mirror is current before any boundary + * consumer reads it. */ + if (async_kvmirror_on()) + qw_defer_flush(g, d - 1, g->ctx[d - 1]); + } + if (gpu_select_die(g, g->die[d]) < 0) + return -1; + gpu_batch_scratch *bd = gpu_get_batch_scratch(g, g->ctx[d], cfg, nslots); + gpu_die_scratch *scd = gpu_get_scratch(g, g->ctx[d], cfg); + fc_buffer *pos_d = gpu_pos_buf_upload(g, g->ctx[d], d, pos, nslots); + if (!bd || !scd || !pos_d) + return -1; + + if (d == 0) { + sprof_begin(g->ctx[d], &sp_t0); + for (int u = 0; u < nslots; u++) + pn_qwen35moe_model_embed_row(m, tokens[u], + g->bhost[0] + (size_t)u * c); + fc_buffer_from_cpu_n(bd->hidden, g->bhost[0], hidden_bytes); + } else { + gpu_handoff_import(g, d - 1, b->hidden, bd->hidden, g->bhost[0], + hidden_bytes); + } + sprof_lap(SPROF_XFER, g->ctx[d], &sp_t0); + + for (int layer = g->layer_lo[d]; layer < g->layer_hi[d]; layer++) { + if (gpu_layer_forward_slots(m, g, g->ctx[d], layer, bd, sts, pos, + pos_d, 0, nslots, nslots, + g->bhost) < 0) { + fprintf(stderr, "[qwen35moe] batched decode step failed on " + "die %d layer %d (n=%d)\n", g->die[d], layer, nslots); + return -1; + } + } + ctx = g->ctx[d]; + b = bd; + sc = scd; + } + + /* QWEN35_ASYNC_KVMIRROR: replay the LAST die's deferred host-KV mirror + * stores (every earlier die was flushed at its boundary hand-off above). + * Done before the head/return so the mirror is current for the next step's + * bind/move/re-sync. */ + if (async_kvmirror_on()) + qw_defer_flush(g, g->n_die - 1, ctx); + + /* ---- Final norm over all K rows + ONE batched LM head, then each user's + * row reduced on-die to top-k pairs. Shared with the TWO_THREAD path. ---- */ + return gpu_head_from_slots(g, cfg, ctx, b, sc, sts, nslots, two_die, + sp_step0); +} + +/* ============================================================ + * pn_forward_fn adapter (CUDA) + * + * Mirrors pn_qwen35moe_forward (the CPU adapter) but dispatches to the single- + * die GPU token forward. The engine selects this when fc_available() and the + * GPU weights are uploaded; otherwise it keeps the CPU adapter. + * ============================================================ */ + +tm_tensor *pn_qwen35moe_forward_cuda(pn_model *model, uint32_t token, + pn_state *state) +{ + if (!model || !state) + return NULL; + + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return NULL; + + float *logits = pn_qwen35moe_forward_token_cuda(m, token, s, state->pos); + if (!logits) + return NULL; + + int64_t shape[1] = { s->n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + if (!out) + return NULL; + memcpy(tm_data_f32(out), logits, (size_t)s->n_vocab * sizeof(float)); + + state->pos++; + return out; +} + +/* Batched prefill adapter: runs the n tokens through the batched forward in + * ONE pass and returns the LAST position's logits (owned tensor), advancing + * state->pos by n. per_pos_logits (optional, [n * n_vocab]) additionally + * receives every position's full logits — the bit-exact gate hook. + * + * If the batched path cannot run (exotic quant / non-grouped experts), it + * falls back to n sequential single-token forwards — same results, no + * speedup — so callers never lose correctness. */ +tm_tensor *pn_qwen35moe_forward_batch_cuda(pn_model *model, + const uint32_t *tokens, int n, + pn_state *state, + float *per_pos_logits) +{ + if (!model || !state || !tokens || n <= 0) + return NULL; + + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return NULL; + + float *logits = pn_qwen35moe_forward_batch_tokens_cuda(m, tokens, n, s, + state->pos, + per_pos_logits); + if (logits) { + state->pos += n; + } else { + /* Fallback: sequential single-token forwards (bit-identical by + * definition; the batch path is purely an optimization). */ + for (int t = 0; t < n; t++) { + logits = pn_qwen35moe_forward_token_cuda(m, tokens[t], s, + state->pos); + if (!logits) + return NULL; + state->pos++; + if (per_pos_logits) + memcpy(per_pos_logits + (size_t)t * s->n_vocab, logits, + (size_t)s->n_vocab * sizeof(float)); + } + } + + int64_t shape[1] = { s->n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + if (!out) + return NULL; + memcpy(tm_data_f32(out), logits, (size_t)s->n_vocab * sizeof(float)); + return out; +} + +/* Every adapter below casts model->weights to pn_qwen35moe_model* and + * state->kv_cache to pn_qwen35moe_state*. The batch scheduler calls several of + * them (forget_state, state_to_host, decode_slot_bind) on WHATEVER model owns + * the slot — it is generic — so a GPU-placed llama model reached them and the + * casts read another struct's fields: n_layer came out of garbage and the layer + * loop walked off into a SIGSEGV that took the whole serve down. Observed on + * the K80 test machine with MiniCPM5-1B (PN_ARCH_LLAMA) under QWEN35_BATCH_SLOTS=1, + * intermittent because the garbage was sometimes benign. + * + * Only the two archs that actually allocate the qwen35moe dual state + * (pn_state_new: QWEN35MOE and BONSAI) may pass. Everything else gets the + * ordinary failure return, which every caller already treats as "no GPU fast + * path for this state". */ +static inline int qw_model_uses_qwen35moe_state(const pn_model *model) +{ + return model && (model->config.arch == PN_ARCH_QWEN35MOE || + model->config.arch == PN_ARCH_BONSAI); +} + +/* Speculative-decode adapters (see the verify/commit section above). + * verify runs at state->pos WITHOUT advancing it; accept commits n_accept + * positions and advances state->pos by n_accept. */ +int pn_qwen35moe_spec_verify(pn_model *model, const uint32_t *tokens, int n, + pn_state *state, int32_t *argmax_out) +{ + if (!model || !state || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + return pn_qwen35moe_spec_verify_tokens_cuda(m, tokens, n, s, state->pos, + argmax_out); +} + +int pn_qwen35moe_spec_accept(pn_model *model, pn_state *state, int n_accept) +{ + if (!model || !state || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + if (!m) + return -1; + if (pn_qwen35moe_spec_commit_cuda(m, n_accept) != 0) + return -1; + state->pos += n_accept; + return 0; +} + +/* Multi-user decode adapters (see the decode-slots section above). bind + * snapshots `state`'s live SSM images into `slot`; step runs ONE batched + * forward for all n_users (slot u == states[u]), fills each user's logits + * (logits_out[u], owned by that user's state, valid until its next + * forward) and advances each states[u]->pos by 1. */ +int pn_qwen35moe_decode_slot_bind(pn_model *model, pn_state *state, int slot, + int n_slots) +{ + if (!model || !state || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + return pn_qwen35moe_decode_slot_bind_cuda(m, s, slot, n_slots); +} + +int pn_qwen35moe_decode_slot_move(pn_model *model, int src, int dst, + int n_slots) +{ + if (!model || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + if (!m) + return -1; + return pn_qwen35moe_decode_slot_move_cuda(m, src, dst, n_slots); +} + +int pn_qwen35moe_decode_slots(pn_model *model, const uint32_t *tokens, + pn_state **states, int n_users, + float **logits_out) +{ + if (!model || !tokens || !states || n_users <= 0 || + n_users > PN_QWEN35MOE_GPU_MAX_SLOTS || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + if (!m) + return -1; + + pn_qwen35moe_state *sts[PN_QWEN35MOE_GPU_MAX_SLOTS]; + int pos[PN_QWEN35MOE_GPU_MAX_SLOTS]; + for (int u = 0; u < n_users; u++) { + if (!states[u]) + return -1; + sts[u] = (pn_qwen35moe_state *)states[u]->kv_cache; + pos[u] = states[u]->pos; + if (!sts[u]) + return -1; + } + + if (pn_qwen35moe_decode_slots_tokens_cuda(m, tokens, sts, pos, + n_users) != 0) + return -1; + + for (int u = 0; u < n_users; u++) { + states[u]->pos++; + if (logits_out) + logits_out[u] = sts[u]->logits; + } + return 0; +} + +/* ============================================================ + * PREFIX CACHING — snapshot the live GPU state back to the host (on-evict) + * + * B2 (KV mirror): single-token decode no longer writes D2H per token; instead + * kv->kv_mirror_stale is set. This function refreshes any stale attn KV + * mirrors by D2H-ing the full GPU KV ring back to kv->k/v (or kq/vq). The GPU + * ring layout is byte-identical to the host ring, so a full fc_buffer_to_cpu of + * k_cache_g/v_cache_g is sufficient. After the copy the mirror is authoritative + * and gpu_attn_kv_cache_sync can H2D from it on session resume. + * + * B3 (SSM state): the 30 SSM layers' fixed-size recurrent state lives only in + * the shared per-layer GPU buffers (conv_state_g / ssm_state_g) after a GPU + * decode. The host copy goes stale when d->host_state matches this session. + * This D2H copies it back so a session's pn_state is self-contained before the + * GPU buffers are reused for another session. + * + * INVARIANT: called by preserve_live (prefix_cache.c) before c->live is + * switched to a different session. At that point the GPU KV ring and SSM state + * still hold this session's data. After this call: + * - kv->k/v (or kq/vq + k_sink/v_sink/k_win/v_win) are current → next resume + * can H2D via gpu_attn_kv_cache_sync. + * - st->conv_state/ssm_state are current → re-seeds on next forward. + * A session is NEVER left with stale host state after its GPU buffers are + * claimed by another session, so no KV is lost. ============================================================ */ +/* SSM half of gpu_state_to_host: capture the SHARED single-user SSM buffers + * into the host mirror for every layer whose GPU buffer holds THIS state's + * image. Die-ordered walk so at most one die-select is needed. */ +static int qw_state_ssm_shared_to_host(pn_qwen35moe_model *m, + pn_qwen35moe_gpu *g, + pn_qwen35moe_state *s) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int rc = 0; + fc_cuda_ctx *cur = NULL; + + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_SSM) + continue; + int idx = pn_qwen35moe_model_layer_index(m, layer); + gpu_ssm_layer *d = &g->ssm[idx]; + pn_qwen35moe_ssm_state *st = &s->ssm[idx]; + /* Only copy layers whose GPU buffer currently holds THIS state's + * image — otherwise the host copy is already the source of truth. */ + if (!d->conv_state_g || !d->ssm_state_g || + d->host_state != (const void *)st) + continue; + if (d->ctx != cur) { + if (cur) + fc_sync_ctx(cur); + if (gpu_select_die(g, gpu_die_of_ctx(g, d->ctx)) < 0) { + rc = -1; + break; + } + cur = d->ctx; + } + fc_buffer_to_cpu(d->conv_state_g, st->conv_state); + fc_buffer_to_cpu(d->ssm_state_g, st->ssm_state); + } + if (cur) + fc_sync_ctx(cur); + return rc; +} + +int pn_qwen35moe_gpu_state_to_host(pn_model *model, pn_state *state) +{ + if (!model || !state || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return 0; /* CPU path: the host state is already authoritative. */ + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int rc = 0; + fc_cuda_ctx *cur = NULL; + + /* B2 / Phase 10b — KV mirror materialization (attn layers). Walk + * die-ordered so at most one die-select is needed. Only layers whose + * kv_mirror_stale flag is set need work; the rows come from whichever + * GPU ring holds this state's content (single-user ring on the B2 + * single-user deferral, decode slot ring in lazy-mirror batched mode), + * bounded to [0, min(pos, max_seq)) so the host commit is proportional + * to the session's real context. */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_ATTN) + continue; + int idx = pn_qwen35moe_model_layer_index(m, layer); + gpu_attn_layer *d = &g->attn[idx]; + pn_qwen35moe_kv *kv = &s->kv[idx]; + if (!kv->kv_mirror_stale) + continue; + if (d->ctx != cur) { + if (cur) + fc_sync_ctx(cur); + if (gpu_select_die(g, gpu_die_of_ctx(g, d->ctx)) < 0) { + rc = -1; + break; + } + cur = d->ctx; + } + int rows = state->pos < kv->max_seq ? state->pos : kv->max_seq; + if (qw_kv_layer_materialize(d, kv, rows) != 0) { + fprintf(stderr, "[attn] state_to_host kv_idx=%d: host mirror " + "stale with no GPU ring source (rows=%d) — mirror left " + "stale\n", idx, rows); + rc = -1; + } + } + if (cur) + fc_sync_ctx(cur); + if (rc < 0) + return rc; + + /* B3 — SSM state snapshot (shared single-user buffers). */ + return qw_state_ssm_shared_to_host(m, g, s); +} + +int pn_qwen35moe_gpu_state_ssm_to_host(pn_model *model, pn_state *state) +{ + if (!model || !state || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return 0; /* CPU path: the host state is already authoritative. */ + return qw_state_ssm_shared_to_host(m, g, s); +} + +/* ============================================================ + * PREFIX CACHING (batched path) — snapshot a DECODE SLOT's live SSM state + * back into that slot's pn_state host mirror. + * + * pn_qwen35moe_gpu_state_to_host (above) captures only the SHARED single-user + * SSM buffers (conv_state_g / ssm_state_g, guarded by d->host_state == st). + * Batched decode instead advances the per-slot SSM images in the slot-major + * arrays d->conv_slots / d->ssm_slots at row `gpu_idx` — those shared buffers + * are NEVER touched — so after a batched turn a slot's pn_state SSM mirror is + * stale even though its pos + KV mirror are current. That leaves the slot's + * state not delta-reusable by the prefix cache. + * + * This D2H-copies conv_slots[gpu_idx] → st->conv_state and + * ssm_slots[gpu_idx] → st->ssm_state for every SSM layer, the exact inverse of + * the host→slot seed in pn_qwen35moe_decode_slot_bind_cuda (same conv_bytes / + * ssm_bytes stride). After it the slot's pn_state SSM mirror is authoritative + * — combined with the already-current KV mirror (bs_slot_bind / the L2 yield + * keep it live) and pos == n_committed, the state is warm + self-contained and + * a next turn can delta-prefill only the appended tokens. + * + * Also clears d->host_state so any layer whose SHARED buffer happens to point + * at this st is re-seeded on its next single-user forward — the slot images + * are the source of truth here, not the shared buffers. Host mirror + GPU + * read only; no slot compute. Caller (the batched commit) must hold the GPU + * turn so no decode step advances the slots mid-copy. gpu_idx is the slot's + * CURRENT GPU index (bs_slot.gpu_idx), which decode_slot_move keeps current. + * ============================================================ */ +int pn_qwen35moe_decode_slot_ssm_to_host(pn_model *model, int gpu_idx, + pn_state *state) +{ + if (!model || !state || gpu_idx < 0 || + gpu_idx >= PN_QWEN35MOE_GPU_MAX_SLOTS || !qw_model_uses_qwen35moe_state(model)) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return 0; /* CPU path: the host state is already authoritative. */ + if (g->n_slots <= gpu_idx) + return -1; + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + size_t conv_bytes = (size_t)(cfg->ssm_conv - 1) * cfg->conv_dim * + sizeof(float); + size_t ssm_bytes = (size_t)cfg->ssm_state * cfg->ssm_state * + cfg->ssm_dt_rank * sizeof(float); + + int rc = 0; + fc_cuda_ctx *cur = NULL; + /* Die-ordered walk (front die then back) so at most one die-select per die, + * mirroring the B3 loop in pn_qwen35moe_gpu_state_to_host. */ + for (int layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_SSM) + continue; + int idx = pn_qwen35moe_model_layer_index(m, layer); + gpu_ssm_layer *d = &g->ssm[idx]; + pn_qwen35moe_ssm_state *st = &s->ssm[idx]; + if (!d->conv_slots || !d->ssm_slots || d->slots_cap <= gpu_idx) + return -1; /* slots not allocated for this width */ + if (d->ctx != cur) { + if (cur) + fc_sync_ctx(cur); + if (gpu_select_die(g, gpu_die_of_ctx(g, d->ctx)) < 0) { + rc = -1; + break; + } + cur = d->ctx; + } + fc_buffer_to_cpu_at(d->conv_slots, st->conv_state, + (size_t)gpu_idx * conv_bytes, conv_bytes); + fc_buffer_to_cpu_at(d->ssm_slots, st->ssm_state, + (size_t)gpu_idx * ssm_bytes, ssm_bytes); + /* The slot images are now the source of truth for this state; drop any + * shared-buffer "live here" binding so a later single-user forward + * re-seeds from the (now-current) host mirror rather than trusting a + * stale shared buffer. */ + if (d->host_state == (const void *)st) + d->host_state = NULL; + } + if (cur) + fc_sync_ctx(cur); + return rc; +} + +/* Forget every GPU-resident "this state is live here" binding for `state`, so + * the NEXT forward re-seeds all per-layer GPU shadows from the host mirror as + * if the state were fresh. A serve slot's pn_state is REUSED across requests; + * pn_qwen35moe_state_reset zeros only the HOST mirror, leaving d->host_state + * (SSM) and kv_host_id/kv_synced_pos (single-user KV) still pointing at this + * state with stale GPU contents from its previous life. A short re-prefill + * only re-touches low positions so the residue is never read (the reused slot + * looked fine), but a LONG prefill attends/carries positions the reset never + * cleared → its logits diverge from a fresh state. Clearing the bindings + * here forces gpu_attn_kv_cache_sync / the SSM re-seed gate to treat the + * reused state as new (host_state != st → full re-seed from the zeroed + + * re-prefilled host mirror). Host bookkeeping only (no GPU work); the + * scheduler calls it under the GPU turn right after pn_state_reset, so no + * forward can race it. No-op without a GPU handle (CPU path). */ +void pn_qwen35moe_gpu_forget_state(pn_model *model, pn_state *state) +{ + if (!model || !state || !qw_model_uses_qwen35moe_state(model)) + return; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g) + return; + + /* Any verify pending against this state is void once its bindings are + * dropped — and spec_commit must not dereference a state the caller is + * about to reset or free. */ + if (g->spec_state == s) { + g->spec_state = NULL; + g->spec_n = 0; + } + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + for (int layer = 0; layer < cfg->n_layer; layer++) { + int idx = pn_qwen35moe_model_layer_index(m, layer); + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_SSM) { + gpu_ssm_layer *d = &g->ssm[idx]; + if (d->host_state == (const void *)&s->ssm[idx]) + d->host_state = NULL; /* force SSM re-seed from host */ + } else { + gpu_attn_layer *d = &g->attn[idx]; + pn_qwen35moe_kv *kv = &s->kv[idx]; + const void *host_id = kv->q4 ? (const void *)kv->kq + : (const void *)kv->k; + if (d->kv_host_id == host_id) { + d->kv_host_id = NULL; /* force full KV re-sync + shadow */ + d->kv_synced_pos = 0; + } + } + } +} + +/* ============================================================ + * C2 — ASYNC decode step (launch / event-poll) + * + * The synchronous forward above ends every die segment with a blocking + * fc_sync_ctx. That is fine for one card, but the K80 test machine's host has 2 cores + * driving up to 8 cards: 8 connection threads blocked in + * cuStreamSynchronize spin/yield for the two cores, so a card whose step + * FINISHED waits a scheduler quantum for its thread to get CPU again and + * launch the next step — the cards serialize at the sync points instead + * of overlapping. + * + * The async step splits one token's forward at its natural host-sync + * seams into launch + reap: + * + * begin(): embed seed (H2D) -> segment-A graph replay (one + * cuGraphLaunch) -> [single-die: + final norm + LM head + * launches] -> fc_event_record -> RETURN (no blocking). + * poll(): fc_event_query (never blocks). + * WAIT_A done -> boundary hidden D2H/H2D bounce -> + * segment-B graph replay -> final norm + + * LM head -> record -> WAIT_FINAL. + * WAIT_FINAL done -> top-k logits fetch (syncs only THIS + * card's stream) -> s->logits ready. + * + * The caller (serve's generation loop) sleeps between polls, so a + * waiting card costs ~zero host CPU and every active card's stream is + * in flight simultaneously. Between begin/poll phases each ctx's stream + * is provably idle at the points where the sync path relied on that + * (gpu_segment_forward's pre-work H2Ds, the boundary bounce, dec_pos), + * because the phase only advances once the event for ALL prior work on + * that stream has completed — the same invariant the blocking fc_sync + * gave, established without occupying a core. + * + * ELIGIBILITY mirrors the segment-graph gate (gpu_segment_forward): any + * refusal — pos 0, capture failure, ring wrap, prof/dbg, KV-geometry + * change — returns 1 from begin() and the caller runs the ordinary + * blocking forward for that token, bit-identically. Legacy barswap mode + * (g->bs != NULL, one global MMIO slot) and host-fallback LM heads are + * also refused. With QWEN35_ASYNC_SCHED=0 begin() always returns 1 and + * serving behaves exactly as before this change. + * + * Correctness under interleaving: every buffer begin/poll touches + * (gpu_die_scratch, gpu_host_scratch, the KV/SSM state, the events, the + * phase machine itself) hangs off THIS model's pn_qwen35moe_gpu / its + * own per-die fc_cuda_ctx, and the per-model gen_lock guarantees one + * generation (hence one in-flight step) per model. Different models + * share no CUDA context, stream, or scratch, and every fc_* op + * push/pops its own context — so N cards' steps interleave freely. + * ============================================================ */ + +enum { + QW_ASYNC_IDLE = 0, + QW_ASYNC_WAIT_A = 1, /* 2-die: front segment in flight on ctx_a */ + QW_ASYNC_WAIT_FINAL = 2, /* back segment + LM head in flight */ +}; + +/* Async dispatch (QWEN35_ASYNC_SCHED, default on; set 0 to restore the + * fully synchronous serve path — bisection aid). */ +static int gpu_async_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("QWEN35_ASYNC_SCHED"); + v = (env && env[0] == '0') ? 0 : 1; + } + return v; +} + +/* Does the LM head have an on-device matvec kernel? The exotic-quant host + * fallback in gpu_matvec does a blocking D2H of x (drains the whole + * segment inside begin()), defeating the overlap — refuse async then. */ +static int gpu_head_on_device(const pn_qwen35moe_gpu *g) +{ + if (!g->output.bytes) + return 0; + switch ((int)g->output.type) { + case GGUF_QUANT_Q3_K: + case GGUF_QUANT_Q4_K: + case GGUF_QUANT_Q5_K: + case GGUF_QUANT_Q6_K: + case GGUF_QUANT_Q8_0: + case GGUF_QUANT_F32: + return 1; + default: + return 0; + } +} + +/* Lazily create the completion event for die slot `which` (0 = ctx_a, + * 1 = ctx_b). A failed create latches so async stays off cheaply. */ +static fc_event *gpu_async_event(pn_qwen35moe_gpu *g, int which, + fc_cuda_ctx *ctx) +{ + if (!g->as_ev[which] && !g->as_ev_tried[which]) { + g->as_ev_tried[which] = 1; + g->as_ev[which] = fc_event_create_ctx(ctx); + } + return g->as_ev[which]; +} + +/* Arm `ev` behind everything queued on ctx's stream. If the record fails + * we fall back to a blocking sync RIGHT HERE, so a later query on the + * (possibly stale) event can never report "done" before the work is — + * the stream is already drained. Degraded to sync, never wrong. */ +static void gpu_async_arm(fc_cuda_ctx *ctx, fc_event *ev) +{ + if (fc_event_record_ctx(ctx, ev) != 0) + fc_sync_ctx(ctx); +} + +/* Reap one phase's event: 0 = complete, 1 = still running. An error from + * the query degrades to a blocking sync (correct, just not async). */ +static int gpu_async_reap(fc_cuda_ctx *ctx, fc_event *ev) +{ + int q = fc_event_query(ev); + if (q > 0) + return 1; + if (q < 0) + fc_sync_ctx(ctx); + return 0; +} + +/** + * Launch one single-token decode step WITHOUT blocking. Returns 0 when the + * step is in flight (the caller MUST then call .._poll until it returns + * 0 or -1 before any other forward on this model), 1 when this token is + * not async-eligible (caller runs the ordinary blocking forward — nothing + * was launched), -1 on error. + */ +int pn_qwen35moe_forward_token_cuda_begin(pn_qwen35moe_model *m, + uint32_t token, + pn_qwen35moe_state *s, int pos) +{ + if (!m || !s) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g || !gpu_async_enabled()) + return 1; + if (g->bs) + return 1; /* legacy barswap: one global MMIO slot — stay sync */ + if (g->as_phase != QW_ASYNC_IDLE) + return -1; /* protocol misuse: a step is already in flight */ + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int n_layer = cfg->n_layer; + if ((int)token >= cfg->n_vocab) + return 1; + /* The phase machine has exactly two segment phases (front, then back), so + * it drives a 1- or 2-die split. A wider split has more boundaries than + * phases; rather than deepen the machine for a shape that trades tok/s for + * capacity anyway, hand those steps to the sync N-die walk, which is the + * only correctness-critical path. */ + if (g->n_die > 2) + return 1; + int two_die = (g->n_die == 2); + int split = g->split_layer; + + /* Debug layer cap runs the (partial) sync path; exotic LM-head quant + * would host-fallback inside begin (see gpu_head_on_device). */ + if (!two_die && getenv("QWEN35_GPU_MAX_LAYERS")) + return 1; + if (!gpu_head_on_device(g)) + return 1; + + /* The completion events must exist BEFORE anything is launched. */ + fc_event *ev_a = gpu_async_event(g, 0, g->ctx[0]); + fc_event *ev_b = two_die ? gpu_async_event(g, 1, g->head_ctx) : NULL; + if (!ev_a || (two_die && !ev_b)) + return 1; + + if (!g->host_ok) { + if (gpu_host_scratch_init(&g->host, cfg) < 0) + return -1; + g->host_ok = 1; + } + gpu_host_scratch h = g->host; + + /* Embed + seed die A's residual stream. The H2D is synchronous but the + * stream is idle between tokens (poll drained it via the event before + * the previous step completed), so it is a pure ~n_embd-float copy. */ + gpu_die_scratch *sc_a = gpu_get_scratch(g, g->ctx[0], cfg); + if (!sc_a) + return -1; + pn_qwen35moe_model_embed_row(m, token, h.a); + fc_buffer_from_cpu(sc_a->r_hidden, h.a); + + /* Front segment as ONE graph replay (C1). Any refusal — pos 0, capture + * failed, ring wrap, prof/dbg — means the sync path takes this token; + * the seed above is harmlessly redone there and the segment's pass-2 + * pre-work H2Ds are idempotent. */ + int seg_rc = gpu_segment_forward(m, g, g->ctx[0], 0, 0, + two_die ? split : n_layer, + sc_a->r_hidden, sc_a->r_xnorm, + sc_a->r_blockout, s, pos, &g->host); + if (seg_rc < 0) + return -1; + if (seg_rc != 0) + return 1; + + if (two_die) { + gpu_async_arm(g->ctx[0], ev_a); + g->as_phase = QW_ASYNC_WAIT_A; + g->as_pos = pos; + return 0; + } + + /* Single die: final norm + LM head are stream-ordered launches behind + * the segment graph — enqueue them now, then arm the event. (The + * QWEN35_DEBUG result dumps are skipped: dbg refuses the segment path, + * so async never engages while dumping.) */ + fc_rms_norm_ctx(g->ctx[0], sc_a->r_hidden, g->output_norm.buf, + sc_a->r_xnorm, cfg->n_embd, cfg->rms_norm_eps); + gpu_matvec(&g->output, sc_a->r_xnorm, sc_a->r_logits, h.a, h.b); + gpu_async_arm(g->ctx[0], ev_a); + g->as_phase = QW_ASYNC_WAIT_FINAL; + g->as_pos = pos; + return 0; +} + +/** + * Reap / advance the in-flight step started by .._begin. Returns 1 while + * the current phase's GPU work is still running (call again after a short + * sleep — nothing blocks), 0 when the step is complete and s->logits + * holds the token's logits (same presentation as the sync forward), -1 on + * error (the machine resets; the generation should abort, exactly as a + * NULL return from the sync forward). + */ +int pn_qwen35moe_forward_token_cuda_poll(pn_qwen35moe_model *m, + pn_qwen35moe_state *s) +{ + if (!m || !s) + return -1; + pn_qwen35moe_gpu *g = (pn_qwen35moe_gpu *)pn_qwen35moe_model_gpu(m); + if (!g || g->as_phase == QW_ASYNC_IDLE) + return -1; + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int n_layer = cfg->n_layer; + int split = g->split_layer; + int pos = g->as_pos; + gpu_host_scratch h = g->host; + + if (g->as_phase == QW_ASYNC_WAIT_A) { + if (gpu_async_reap(g->ctx[0], g->as_ev[0]) != 0) + return 1; + + /* 2-die boundary hand-off (die A's stream is now idle): copy the + * boundary hidden D2H, re-seed it H2D as die B's residual stream. + * No gpu_select_die needed — async requires small-BAR mode, where + * every die is live behind its own context. */ + gpu_die_scratch *sc_a = gpu_get_scratch(g, g->ctx[0], cfg); + gpu_die_scratch *sc_b = gpu_get_scratch(g, g->head_ctx, cfg); + if (!sc_a || !sc_b) { + g->as_phase = QW_ASYNC_IDLE; + return -1; + } + fc_buffer_to_cpu(sc_a->r_hidden, h.a); + fc_buffer_from_cpu(sc_b->r_hidden, h.a); + + int seg_rc = gpu_segment_forward(m, g, g->head_ctx, 1, split, n_layer, + sc_b->r_hidden, sc_b->r_xnorm, + sc_b->r_blockout, s, pos, &g->host); + if (seg_rc < 0) { + g->as_phase = QW_ASYNC_IDLE; + return -1; + } + if (seg_rc != 0) { + /* Back segment can't replay this token (capture failure / + * geometry change): finish it on the DIRECT per-layer path — + * blocking host round-trips, rare, bit-identical. */ + for (int layer = split; layer < n_layer; layer++) { + if (gpu_layer_forward(m, g, g->head_ctx, layer, sc_b->r_hidden, + sc_b->r_xnorm, sc_b->r_blockout, s, + pos, &g->host) < 0) { + g->as_phase = QW_ASYNC_IDLE; + return -1; + } + } + } + fc_rms_norm_ctx(g->head_ctx, sc_b->r_hidden, g->output_norm.buf, + sc_b->r_xnorm, cfg->n_embd, cfg->rms_norm_eps); + gpu_matvec(&g->output, sc_b->r_xnorm, sc_b->r_logits, h.a, h.b); + gpu_async_arm(g->head_ctx, g->as_ev[1]); + g->as_phase = QW_ASYNC_WAIT_FINAL; + return 1; + } + + /* QW_ASYNC_WAIT_FINAL — the LM head result sits on the last die. */ + fc_cuda_ctx *ctx = g->head_ctx ? g->head_ctx : g->ctx[0]; + fc_event *ev = g->head_ctx ? g->as_ev[1] : g->as_ev[0]; + if (gpu_async_reap(ctx, ev) != 0) + return 1; + gpu_die_scratch *sc = gpu_get_scratch(g, ctx, cfg); + if (!sc) { + g->as_phase = QW_ASYNC_IDLE; + return -1; + } + /* Top-k fetch syncs only THIS card's stream (already drained by the + * event) — no global barrier across cards. */ + gpu_fetch_logits(ctx, sc->r_logits, s, cfg->n_vocab); + g->as_phase = QW_ASYNC_IDLE; + return 0; +} + +/* pn_forward_step adapters (engine seam): pn_model/pn_state signatures + * mirroring pn_qwen35moe_forward_cuda, advancing state->pos on completion + * exactly as the sync adapter does. */ +int pn_qwen35moe_forward_step_begin(pn_model *model, uint32_t token, + pn_state *state) +{ + if (!model || !state) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + return pn_qwen35moe_forward_token_cuda_begin(m, token, s, state->pos); +} + +int pn_qwen35moe_forward_step_poll(pn_model *model, pn_state *state, + tm_tensor **logits_out) +{ + if (!model || !state || !logits_out) + return -1; + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return -1; + + int rc = pn_qwen35moe_forward_token_cuda_poll(m, s); + if (rc != 0) + return rc; + + int64_t shape[1] = { s->n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + if (!out) + return -1; + memcpy(tm_data_f32(out), s->logits, (size_t)s->n_vocab * sizeof(float)); + state->pos++; + *logits_out = out; + return 0; +} + +#endif /* USE_CUDA */ diff --git a/src/models/qwen35moe_forward_metal.c b/src/models/qwen35moe_forward_metal.c new file mode 100644 index 0000000..5db7a8e --- /dev/null +++ b/src/models/qwen35moe_forward_metal.c @@ -0,0 +1,394 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_forward_metal.c + * @brief Metal (Apple GPU) backend seam for the Qwen3.5-MoE forward. Phase 0 + * scaffold: weight residency + the engine-selectable forward entry. + * No kernels yet — the forward reports NOT-IMPLEMENTED and the adapter + * routes the token to the CPU forward, so selecting this backend is + * output-neutral until Phase 1..5 fill the seam. + * + * Structure mirrors qwen35moe_forward_cuda.c: an upload pass that makes + * every quantized weight GPU-addressable and attaches the handle to the + * weight (pn_qweight.gpu), plus a pn_forward_fn adapter selected by + * src/core/engine.c. What it does NOT mirror is the CUDA path's + * machinery: unified memory means there is no device allocation, no + * H2D/D2H of weights or state, and deliberately no MTLResidencySet + * (foundry/metal/qwen35moe_metal_spec.md §2 — wiring the working set was + * measured worth ~0.7% and costs the whole memory posture). + * + * @status Phase 0 scaffold. Upload + census implemented and measured; forward + * is a NOT-IMPLEMENTED seam that falls back to the CPU forward. + * + * The upload and release walks now go through the model's own + * pn_qwen35moe_for_each_weight, shared with the kernel-coverage gate, + * so all three traverse one list. PN_METAL_COVERAGE=1 runs that gate at + * load (qwen35moe_metal_coverage.h) — off by default because it costs a + * CPU forward token. + * + * Two compile-time switches exist ONLY to prove the acceptance checks + * can fail (never define them in a real build): + * PN_METAL_HARNESS_NO_FALLBACK — the forward claims success and + * returns zero logits, so "Metal build output == CPU build output" + * is shown to be a real gate. Measured: output becomes "!!!!..." + * instead of the CPU answer. + * PN_METAL_HARNESS_COPY_LIMIT=N — bounds the unaligned-copy path to + * N weights so it can be exercised on a 15 GiB model, proving the + * census counters are measurements and not constants. + * @issues **Zero-copy does not currently reach the weights.** fm_buffer_from_ptr + * only takes newBufferWithBytesNoCopy when the pointer is host-page + * aligned (16 KiB on Apple Silicon), but GGUF aligns tensor data to + * general.alignment (32 bytes) and the expert slices are interior + * offsets into merged tensors. Measured on + * nex-agi_Nex-N2-mini-Q3_K_M.gguf: 0 of 31,111 weights land page + * aligned. Copying instead would duplicate ~15 GiB of weights in a + * 24 GiB machine, so unaligned weights are counted and SKIPPED, not + * copied (see PN_METAL_COPY_UNALIGNED_ENV to opt in for measurement). + * The fix is a backend design change, not a bug here: wrap ONE buffer + * over the page-aligned mmap region and bind per-tensor byte offsets. + * @todo + * - [ ] Phase 1-5: fill pn_qwen35moe_forward_token_metal with the real + * kernel sequence; drop the CPU fallback in the adapter once it lands. + * - [ ] Re-do the upload against a single whole-mmap buffer + per-tensor + * offsets once fm_metal exposes an offset-bearing buffer view. + */ + +/* 1. This module's own header */ +#include "qwen35moe.h" + +/* 2. Standard library */ +#include +#include +#include +#include + +/* 3. System / platform headers */ +#ifdef USE_METAL +#include +#endif + +/* 4. Project headers */ +#include "../core/qweight.h" +#include "qwen35moe_internal.h" +#include "qwen35moe_metal_coverage.h" + +#ifdef USE_METAL + +#include "metal/fm_metal.h" + +/* Env knob: copy weights that are not host-page aligned into fresh Metal + * buffers instead of skipping them. OFF by default and it must stay that way + * for real models — on this file 15 GiB of weights miss page alignment, so the + * "fallback copy" is not a fallback, it is a second copy of the whole model in + * a 24 GiB machine. Exists so the copy path can be exercised deliberately on a + * small model. */ +#define PN_METAL_COPY_UNALIGNED_ENV "PN_METAL_COPY_UNALIGNED" + +/* Env knob: run the kernel-coverage gate at model load. Off by default because + * it costs one CPU forward token to trace the dispatch set. */ +#define PN_METAL_COVERAGE_ENV "PN_METAL_COVERAGE" + +/* How many distinct unaligned tensor names to name in the census log. The + * count is the finding; the names are there to show WHICH kinds of tensors + * miss, and 31k of them would just be noise. */ +#define PN_METAL_CENSUS_SAMPLE_NAMES 8 + +/* ============================================================ + * Page-alignment census + * + * fm_buffer_from_ptr silently degrades from newBufferWithBytesNoCopy to a + * copy when the pointer is not page aligned (foundry/metal/fm_metal.m:172-186). + * Silently is the problem: the whole memory design of this backend is that the + * weights stay the mmap'd file, so a degraded upload has to be visible in the + * log, per tensor, or it looks exactly like a working one. + * ============================================================ */ + +typedef struct { + long page_size; /* host page size the alignment test uses */ + unsigned long total; /* weights walked */ + unsigned long zero_copy; /* page aligned -> newBufferWithBytesNoCopy */ + unsigned long copied; /* unaligned, copied because the env knob is on */ + unsigned long skipped; /* unaligned, left un-uploaded (the default) */ + unsigned long already; /* already had a buffer (idempotent re-run) */ + unsigned long failed; /* fm_buffer_from_ptr returned NULL */ + uint64_t zero_copy_bytes; + uint64_t copied_bytes; + uint64_t skipped_bytes; + int copy_unaligned; /* PN_METAL_COPY_UNALIGNED */ + int n_sample; + char sample[PN_METAL_CENSUS_SAMPLE_NAMES][96]; +} pn_metal_upload_census; + +/** + * Record one unaligned weight's name in the bounded sample list. + * Silently ignored once the sample is full — the counters carry the totals. + */ +static void census_sample_name(pn_metal_upload_census *c, const char *name) +{ + if (c->n_sample >= PN_METAL_CENSUS_SAMPLE_NAMES) + return; + snprintf(c->sample[c->n_sample], sizeof(c->sample[0]), "%s", name); + c->n_sample++; +} + +/** + * Make one quantized weight GPU-addressable, idempotently. + * + * Mirrors pn_qmv_gpu_upload's contract (already-resident weights are a no-op, + * a weight that cannot be uploaded is left alone rather than failing the whole + * model), but takes the zero-copy path: the weight's bytes ARE the mmap'd GGUF, + * so a page-aligned pointer is wrapped in place and no memory is duplicated. + * + * @param w Weight to wrap; w->gpu receives the fm_buffer on success. + * @param name Diagnostic tensor name, used only for the census log. + * @param c Census accumulator (updated for every outcome). + * + * Thread-safe: no (mutates w and c) + * Reentrant: no + * Signal-safe: no + */ +static void metal_wrap_weight(pn_qweight *w, const char *name, + pn_metal_upload_census *c) +{ + if (!w || !w->data || w->rows <= 0 || w->row_stride == 0) + return; + + c->total++; + if (w->gpu) { + c->already++; + return; + } + + size_t bytes = (size_t)w->rows * w->row_stride; + int aligned = ((uintptr_t)w->data % (uintptr_t)c->page_size) == 0; + + if (!aligned) { + census_sample_name(c, name); + if (!c->copy_unaligned) { + c->skipped++; + c->skipped_bytes += bytes; + return; + } +#ifdef PN_METAL_HARNESS_COPY_LIMIT + /* Harness proof only: bound the copy path so it can be exercised on a + * 15 GiB model without allocating 15 GiB. Never defined in a real + * build. */ + if (c->copied >= (unsigned long)PN_METAL_HARNESS_COPY_LIMIT) { + c->skipped++; + c->skipped_bytes += bytes; + return; + } +#endif + } + + /* Cast away const: the buffer wraps the mmap'd bytes read-only from our + * side, but Metal's API takes a mutable pointer. The mapping itself is + * MAP_PRIVATE and nothing here writes through it. */ + fm_buffer *b = fm_buffer_from_ptr((void *)(uintptr_t)w->data, bytes); + if (!b) { + c->failed++; + return; + } + w->gpu = b; + if (aligned) { + c->zero_copy++; + c->zero_copy_bytes += bytes; + } else { + c->copied++; + c->copied_bytes += bytes; + } +} + +/** + * Release a weight's Metal buffer, if it has one. Symmetric with + * metal_wrap_weight; safe on a weight that was never uploaded. + */ +static void metal_release_weight(pn_qweight *w) +{ + if (w && w->gpu) { + fm_buffer_free((fm_buffer *)w->gpu); + w->gpu = NULL; + } +} + +/* ============================================================ + * Weight walk + * + * The walk itself lives in the model (pn_qwen35moe_for_each_weight) so the + * upload, the release and the kernel-coverage gate all traverse the SAME list. + * A private copy here is how a newly added weight gets uploaded but not + * released, or checked but not uploaded. + * ============================================================ */ + +/** pn_qwen35moe_weight_fn adapter for the upload pass. */ +static void metal_upload_one(const pn_qwen35moe_weight_ref *ref, void *user) +{ + metal_wrap_weight(ref->w, ref->name, (pn_metal_upload_census *)user); +} + +/** pn_qwen35moe_weight_fn adapter for the release pass. */ +static void metal_release_one(const pn_qwen35moe_weight_ref *ref, void *user) +{ + (void)user; + metal_release_weight(ref->w); +} + +/** + * Print the census. Always at INFO volume (stderr, one block) — a degraded + * upload is indistinguishable from a healthy one at runtime, so this is the + * only place the difference is visible. + */ +static void metal_log_census(const pn_metal_upload_census *c) +{ + fprintf(stderr, + "metal: qwen35moe weight upload — %lu weights, page=%ld B\n" + "metal: zero-copy (page aligned) : %lu (%.2f GiB)\n" + "metal: copied (unaligned) : %lu (%.2f GiB)\n" + "metal: skipped (unaligned) : %lu (%.2f GiB)\n" + "metal: already resident : %lu\n" + "metal: buffer creation failed : %lu\n", + c->total, c->page_size, + c->zero_copy, (double)c->zero_copy_bytes / (1024.0 * 1024.0 * 1024.0), + c->copied, (double)c->copied_bytes / (1024.0 * 1024.0 * 1024.0), + c->skipped, (double)c->skipped_bytes / (1024.0 * 1024.0 * 1024.0), + c->already, c->failed); + + if (c->skipped == 0 && c->copied == 0) + return; + + for (int i = 0; i < c->n_sample; i++) + fprintf(stderr, "metal: unaligned e.g. %s\n", c->sample[i]); + fprintf(stderr, + "metal: NOTE — GGUF aligns tensor data to general.alignment " + "(32 B by default) and expert slices are interior offsets, so " + "page alignment is accidental. Until the backend binds one buffer " + "over the whole mmap with per-tensor offsets, the zero-copy path " + "in the spec is not reachable for this file.\n"); + if (!c->copy_unaligned) + fprintf(stderr, + "metal: unaligned weights were SKIPPED, not copied — copying " + "would duplicate the model in host RAM. Set %s=1 to copy " + "anyway (measurement only).\n", PN_METAL_COPY_UNALIGNED_ENV); +} + +/* ============================================================ + * Public API + * ============================================================ */ + +int pn_qwen35moe_metal_upload(pn_qwen35moe_model *model) +{ + if (!model) + return -1; + if (fm_init() != 0 || !fm_available()) { + /* No Metal device at runtime: graceful skip, exactly like the CUDA + * upload's no-GPU case. The engine keeps the CPU forward. */ + return -1; + } + + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(model); + if (!cfg) + return -1; + + const char *copy_env = getenv(PN_METAL_COPY_UNALIGNED_ENV); + pn_metal_upload_census census = {0}; + census.page_size = sysconf(_SC_PAGESIZE); + if (census.page_size <= 0) + census.page_size = 4096; + census.copy_unaligned = (copy_env && atoi(copy_env) != 0) ? 1 : 0; + + pn_qwen35moe_for_each_weight(model, metal_upload_one, &census); + metal_log_census(&census); + + /* Debug knob: prove, at load, that a Metal matvec kernel exists for every + * quant type this model will dispatch. Off by default because it runs a + * full CPU forward token to trace the dispatch set. */ + const char *cov_env = getenv(PN_METAL_COVERAGE_ENV); + if (cov_env && atoi(cov_env) != 0) + pn_qwen35moe_metal_coverage_check(model, stderr); + + /* A degraded (copy / skip) upload is a reportable finding, not a failure: + * the weights are still correct and the CPU path is untouched. Only a + * missing device or a broken model handle is an error. */ + return 0; +} + +void pn_qwen35moe_metal_free(pn_qwen35moe_model *model) +{ + pn_qwen35moe_for_each_weight(model, metal_release_one, NULL); +} + +int pn_qwen35moe_forward_token_metal(pn_qwen35moe_model *model, uint32_t token, + pn_qwen35moe_state *state, int pos, + float **logits_out) +{ + (void)model; (void)token; (void)state; (void)pos; + + if (logits_out) + *logits_out = NULL; + +#ifdef PN_METAL_HARNESS_NO_FALLBACK + /* Harness proof only: claim the forward ran and hand back a zero logits + * row, so the A/B that asserts "Metal output == CPU output" is shown to + * FAIL when the fallback is removed. Never defined in a real build. */ + if (state && state->logits) { + memset(state->logits, 0, (size_t)state->n_vocab * sizeof(float)); + if (logits_out) + *logits_out = state->logits; + return PN_QWEN35MOE_METAL_OK; + } +#endif + + /* Phase 0: no kernels exist. Reporting NOT-IMPLEMENTED (rather than + * producing something) is what keeps the backend honest — the adapter + * turns this into a CPU token, so selecting Metal today changes nothing + * about the output. */ + return PN_QWEN35MOE_METAL_ENOTIMPL; +} + +/* ============================================================ + * pn_forward_fn adapter (Metal) + * + * Mirrors pn_qwen35moe_forward_cuda's adapter shape. The one difference is the + * NOT-IMPLEMENTED branch: the CUDA adapter has a real kernel path to dispatch + * to, this one does not yet, so it delegates the whole token to the CPU + * adapter. Delegating (rather than duplicating the pos/logits bookkeeping) is + * what makes the fallback bit-identical to a non-Metal build by construction. + * ============================================================ */ + +tm_tensor *pn_qwen35moe_forward_metal(pn_model *model, uint32_t token, + pn_state *state) +{ + if (!model || !state) + return NULL; + + pn_qwen35moe_model *m = (pn_qwen35moe_model *)model->weights; + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state->kv_cache; + if (!m || !s) + return NULL; + + float *logits = NULL; + int rc = pn_qwen35moe_forward_token_metal(m, token, s, state->pos, &logits); + + if (rc == PN_QWEN35MOE_METAL_ENOTIMPL) { + static int warned = 0; + if (!warned) { + warned = 1; + fprintf(stderr, "metal: qwen35moe forward not implemented yet " + "(Phase 0 scaffold) — running the CPU forward\n"); + } + return pn_qwen35moe_forward(model, token, state); + } + if (rc != PN_QWEN35MOE_METAL_OK || !logits) + return NULL; + + int64_t shape[1] = { s->n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + if (!out) + return NULL; + memcpy(tm_data_f32(out), logits, (size_t)s->n_vocab * sizeof(float)); + + state->pos++; + return out; +} + +#endif /* USE_METAL */ diff --git a/src/models/qwen35moe_forward_tt.c b/src/models/qwen35moe_forward_tt.c new file mode 100644 index 0000000..0155781 --- /dev/null +++ b/src/models/qwen35moe_forward_tt.c @@ -0,0 +1,1626 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_forward_tt.c + * @brief The `qwen35` hybrid stack — 24 Gated-Delta-Net layers and 8 full-attention + * layers — with its MATMULS on a Tenstorrent Blackhole card. + * + * This is the first rung of the ladder, and the rung is chosen deliberately. + * + * --------------------------------------------------------------------------- + * WHAT THIS MOVES, AND WHY IT IS EXACTLY THE MATMULS AND NOTHING ELSE + * --------------------------------------------------------------------------- + * `llama_forward_tt.c` moved a whole family at a time — norms, then SwiGLU, then the + * attention triple — against a model whose every layer is the same shape. This model is + * a HYBRID: layer `i` is full attention when `(i+1) % full_attention_interval == 0` and a + * Gated-Delta-Net recurrence otherwise, and the recurrence carries 2 MiB of fp32 state per + * layer from one token to the next. Moving the recurrence and the matmuls in one step + * would give a wrong first answer several independent candidates at once, which is the + * situation this project's own method exists to avoid: **one new thing at a time, against + * a working oracle.** + * + * So the seam is `qwen35moe_mv()` (qwen35moe_internal.h) and nothing else changes. The + * conv1d, the L2 norms, the GQA head repeat, the delta-rule recurrence, the gated norm, + * the attention softmax and every RMSNorm run the SAME HOST CODE as the CPU oracle, on the + * same fp32 state, in the same order. The two arms therefore differ in the matmuls alone, + * and the only expected divergence is the one the weights carry: Q8_0 dequantised to fp32 + * on the host, against Bfp8_b tiles and a bf16 activation row on the card. + * + * --------------------------------------------------------------------------- + * THE COST OF THIS RUNG, STATED BEFORE IT IS MEASURED + * --------------------------------------------------------------------------- + * `pn_tt_matvec()` takes a host row in and hands a host row back, so every one of this + * model's **249 matmuls a token** crosses the link twice. Card->host is the slow direction + * (~4.7 MB/s, no device->host DMA on Blackhole) and it is what will bind here — exactly as + * it bound `llama_forward_tt.c` at the same rung, where 0.412 s of a 0.568 s token was the + * link. That is not a defect of this file; it is the measurement that prices every step + * after it, and it is why the next rung keeps the activation resident and the one after + * that moves the recurrence on-card. + * + * 249 = 24 SSM layers x 5 (attn_qkv, attn_gate, ssm_alpha, ssm_beta, ssm_out) + * + 8 attention layers x 4 (attn_q, attn_k, attn_v, attn_output) + * + 32 layers x 3 (the dense SwiGLU: ffn_gate, ffn_up, ffn_down) + * + 1 LM head. + * The count is checked at upload, because a binding that silently did not happen is a + * matmul that silently ran on the CPU, and this project has twice had a card arm and a CPU + * arm produce identical bytes. + * + * --------------------------------------------------------------------------- + * WHAT IS DELIBERATELY NOT HERE + * --------------------------------------------------------------------------- + * - **`ssm_conv1d` is not bound and must not be.** It is a depthwise causal convolution, + * not a matvec: the artifact holds it as an 8192x4 matmul tile grid because the converter + * classifies every 2-D tensor, but multiplying an activation by it computes nothing this + * model wants. It stays on the host until there is a conv op. + * - **MoE is refused, not attempted.** `n_expert > 0` is SABER, and its router and expert + * gather are a different phase. This file serves the DENSE branch (`dense_ffn`). + * - **The embedding table is not on the card** for the reason `llama_forward_tt.c` gives: + * it is tiled block-float and no on-card op reads it. + * + * --------------------------------------------------------------------------- + * AND THE RUNG ABOVE IT, WHICH IS NOW THE DEFAULT + * --------------------------------------------------------------------------- + * The projection-only arm above is still built, still selectable (`PN_TT_RESIDENT=0`) and + * still the reference every measurement on this model was taken against. It is no longer + * what runs: `qwen35moe_resident_tt.c` keeps the residual stream, both RMSNorms, the dense + * SwiGLU and the LM head in card DRAM, which is 74.5% of that arm's readback. This file + * owns the residency, the bindings, the counters and the choice between the two arms; that + * one owns the forward. + * + * @status Two arms. The RESIDENT one (default) keeps activations in card DRAM and crosses + * the link only for the Gated-Delta-Net's and the attention core's operands. The + * PROJECTION-ONLY one (`PN_TT_RESIDENT=0`) round-trips every matvec and is the + * reference. Both refuse rather than falling back when the geometry, the + * architecture branch or a binding is not what they expect. + * @issues None + * @todo + * - [ ] The WHOLE Gated-Delta-Net block on the card — conv1d + SiLU, the L2 norms, the + * GQA repeat, the delta rule against a state resident in card DRAM, and the gated + * norm. It has to move as ONE piece: the conv1d is width-preserving so moving it + * alone changes nothing, and moving the L2 norm and the GQA repeat with it is + * WORSE than doing nothing (the repeat doubles q and k). This is the floor the + * resident arm cannot reach past. + */ + +#ifdef USE_TT + +#include +#include +#include +#include + +#include "qwen35moe.h" +#include "qwen35moe_internal.h" +#include "qwen35moe_tt_internal.h" + +#include "../accel/tt/pn_tt_dispatch.h" +#include "../accel/tt/pn_tt_device.h" +#include "../accel/tt/pn_tt_matmul_group.h" +#include "../accel/tt/pn_tt_render.h" +#include "../core/bench.h" +#include "../core/engine.h" +#include "../core/qweight.h" +#include "tt/convert/ft_tt_artifact_cache.h" +#include "tt/ft_tt_memmap.h" + +/* RED `spec_lookup_unverified` (serve): the next verify cycle accepts its first draft unverified + * and wrong — armed only on a cycle whose drafts came from prompt lookup, so it DIFFERS only if + * lookup drafts reach the output. */ +static bool g_red_accept_next_cycle; + +void pn_qwen35moe_tt_red_accept_next_cycle(void) +{ + g_red_accept_next_cycle = true; +} + +/** An artifact to use INSTEAD of the cache. Same override semantics, and deliberately the + * same spelling, as the llama backend's: used verbatim, provenance unchecked, refuses + * rather than quietly reconverting. */ +#define PN_QWEN35_TT_ARTIFACT_ENV "PN_TT_ARTIFACT" + +/** Where cached artifacts live. Unset keeps them beside the checkpoint. */ +#define PN_QWEN35_TT_CACHE_DIR_ENV "PN_TT_ARTIFACT_CACHE_DIR" + +/** Force a rung by name. An instrument: a forced rung still has to fit. */ +#define PN_QWEN35_TT_RUNG_ENV "PN_TT_RUNG" + +/** Card DRAM held back from the weight budget, in MiB. */ +#define PN_QWEN35_TT_RESERVE_MIB_ENV "PN_TT_CARD_RESERVE_MIB" +#define PN_QWEN35_TT_RESERVE_MIB_DEFAULT 512u + +/** Report the per-token matmul split on stderr every N tokens. 0 (default) is silent. */ +#define PN_QWEN35_TT_REPORT_ENV "PN_TT_REPORT" + +/** A rung name that names nothing, kept distinct from "not set". */ +#define PN_QWEN35_TT_ENV_BAD (-2) + +/* ============================================================ + * The seam + * ============================================================ */ + +/** + * `qwen35moe_mv`'s backend: run this matvec on the card, or decline. + * + * Declining is CORRECT — the seam runs the host matvec — and that is precisely why every + * decline is counted. A backend that quietly declined every weight would produce the right + * answer at CPU speed and look like a working card. + */ +static int qwen35_tt_mv(void *ctx, const pn_qweight *w, const float *x, float *out) +{ + pn_qwen35_tt *g = (pn_qwen35_tt *)ctx; + int i; + + if (!g || !g->device || !w) + return -1; + for (i = 0; i < g->n_bindings; i++) { + if (g->bindings[i].key != w) + continue; + if (pn_tt_matvec(g->device, &g->bindings[i].card, x, out) != 0) { + g->mv_failed++; + if (!g->failure_reported) { + g->failure_reported = 1; + fprintf(stderr, "qwen35_tt: a card matvec FAILED and the host ran it " + "instead. The answer is still correct; the run is no " + "longer a measurement of the card.\n"); + } + return -1; + } + g->mv_on_card++; + return 0; + } + g->mv_on_host++; + return -1; +} + +/* ============================================================ + * Residency + * ============================================================ */ + +/** + * Bind one artifact tensor to one host weight, reporting a miss by name. + * + * ONE walk fills BOTH tables: the seam's `pn_qweight *`-keyed table for the projection-only + * arm, and `card_out` — the per-layer handle the resident arm indexes by layer number. A + * second walk would be a second place for a tensor name to be wrong, and only one of the two + * would be checked against the count. + */ +static int qwen35_tt_bind(pn_qwen35_tt *g, const char *name, const pn_qweight *key, + pn_tt_weight *card_out) +{ + pn_qwen35_tt_binding *slot; + + if (g->n_bindings >= g->n_expected) { + fprintf(stderr, "qwen35_tt: more bindings than the %d this geometry predicts — " + "the layer walk and the count disagree\n", g->n_expected); + return -1; + } + slot = &g->bindings[g->n_bindings]; + if (pn_tt_device_find_weight(g->device, name, &slot->card) != 0) { + fprintf(stderr, "qwen35_tt: the artifact has no %s\n", name); + return -1; + } + slot->key = key; + g->n_bindings++; + if (card_out) + *card_out = slot->card; + return 0; +} + +/** + * How many matmul weights this geometry has, derived and not assumed. + * + * Checked against the walk afterwards. The two are computed from different things — this + * from the layer counts, the walk from the tensors that actually bound — so a layer whose + * type was misread cannot satisfy both. + */ +static int qwen35_tt_expected_bindings(const pn_qwen35moe_config *cfg, int n_ssm, int n_attn) +{ + return n_ssm * 5 /* attn_qkv, attn_gate, ssm_alpha, ssm_beta, ssm_out */ + + n_attn * 4 /* attn_q (Q and its gate, joint), attn_k, attn_v, attn_output */ + + cfg->n_layer * 3 /* the dense SwiGLU on every layer */ + + 1; /* the LM head */ +} + +/** Every matmul weight of one layer, by the GGUF names the converter preserved. */ +static int qwen35_tt_bind_layer(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, int layer) +{ + pn_qwen35_tt_layer *d = &g->layers[layer]; + const pn_qwen35moe_moe *moe; + char name[128]; + int idx = pn_qwen35moe_model_layer_index(m, layer); + int i; + + d->is_attention = pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN; + if (d->is_attention) { + pn_qwen35moe_attn_layer *l = pn_qwen35moe_model_attn_layer_mut(m, idx); + struct { const char *suffix; const pn_qweight *key; pn_tt_weight *card; } bind[] = { + { "attn_q", &l->attn_q, &d->attn_q }, + { "attn_k", &l->attn_k, &d->attn_k }, + { "attn_v", &l->attn_v, &d->attn_v }, + { "attn_output", &l->attn_output, &d->attn_output }, + }; + if (!l) + return -1; + for (i = 0; i < (int)(sizeof(bind) / sizeof(bind[0])); i++) { + snprintf(name, sizeof(name), "blk.%d.%s.weight", layer, bind[i].suffix); + if (qwen35_tt_bind(g, name, bind[i].key, bind[i].card) != 0) + return -1; + } + moe = &l->moe; + } else { + pn_qwen35moe_ssm_layer *l = pn_qwen35moe_model_ssm_layer_mut(m, idx); + struct { const char *suffix; const pn_qweight *key; pn_tt_weight *card; } bind[] = { + { "attn_qkv", &l->attn_qkv, &d->attn_qkv }, + { "attn_gate", &l->attn_gate, &d->attn_gate }, + { "ssm_alpha", &l->ssm_alpha, &d->ssm_alpha }, + { "ssm_beta", &l->ssm_beta, &d->ssm_beta }, + { "ssm_out", &l->ssm_out, &d->ssm_out }, + }; + if (!l) + return -1; + for (i = 0; i < (int)(sizeof(bind) / sizeof(bind[0])); i++) { + snprintf(name, sizeof(name), "blk.%d.%s.weight", layer, bind[i].suffix); + if (qwen35_tt_bind(g, name, bind[i].key, bind[i].card) != 0) + return -1; + } + /* ssm_conv1d is NOT bound, on purpose — see the file note. A depthwise conv is not + * a matvec, and the artifact holding it as one is a property of the converter's + * classifier, not a claim that multiplying by it means anything. */ + moe = &l->moe; + } + + (void)cfg; + { + struct { const char *suffix; const pn_qweight *key; pn_tt_weight *card; } ffn[] = { + { "ffn_gate", &moe->ffn_gate_shexp, &d->ffn_gate }, + { "ffn_up", &moe->ffn_up_shexp, &d->ffn_up }, + { "ffn_down", &moe->ffn_down_shexp, &d->ffn_down }, + }; + for (i = 0; i < (int)(sizeof(ffn) / sizeof(ffn[0])); i++) { + snprintf(name, sizeof(name), "blk.%d.%s.weight", layer, ffn[i].suffix); + if (qwen35_tt_bind(g, name, ffn[i].key, ffn[i].card) != 0) + return -1; + } + } + return 0; +} + +/** A forced rung by name, or -1 for "the fit decides", or ENV_BAD. */ +static int qwen35_tt_forced_rung(void) +{ + static const struct { const char *name; ft_tt_ledger_mode mode; } NAMES[] = { + {"bfp8", FT_TT_LEDGER_BFP8}, {"bfp4", FT_TT_LEDGER_BFP4}, + }; + const char *name = getenv(PN_QWEN35_TT_RUNG_ENV); + size_t i; + + if (!name || !*name) + return -1; + for (i = 0; i < sizeof(NAMES) / sizeof(NAMES[0]); i++) { + if (strcmp(name, NAMES[i].name) == 0) + return (int)NAMES[i].mode; + } + fprintf(stderr, "qwen35_tt: REFUSED — %s='%s' names neither bfp8 nor bfp4\n", + PN_QWEN35_TT_RUNG_ENV, name); + return PN_QWEN35_TT_ENV_BAD; +} + +/** + * The recurrent state this model needs in addition to its KV cache. + * + * It is a FIXED cost per sequence and does not grow with context — that is the whole point + * of a linear-attention layer — but it is not small, and leaving it out of the capacity + * plan would let the selector choose a rung whose weights fit and whose run does not. + * Per SSM layer: `(ssm_conv - 1) * conv_dim` conv floats + `ssm_state^2 * ssm_dt_rank` + * recurrence floats. + */ +static uint64_t qwen35_tt_recurrent_state_bytes(const pn_qwen35moe_config *cfg, int n_ssm) +{ + uint64_t conv = (uint64_t)(cfg->ssm_conv - 1) * (uint64_t)cfg->conv_dim; + uint64_t recur = (uint64_t)cfg->ssm_state * (uint64_t)cfg->ssm_state * + (uint64_t)cfg->ssm_dt_rank; + return (conv + recur) * sizeof(float) * (uint64_t)n_ssm; +} + +/** + * Turn the `.gguf` path into a card-ready artifact, converting and caching if need be. + * + * The same one function the llama backend uses, with this model's own KV arithmetic and + * with the recurrent state charged to the run-time reserve rather than forgotten. + */ +static int qwen35_tt_resolve_artifact(pn_model *model, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, int n_ssm, + char *out, size_t out_bytes) +{ + const char *override_path = getenv(PN_QWEN35_TT_ARTIFACT_ENV); + const char *reserve_mib = getenv(PN_QWEN35_TT_RESERVE_MIB_ENV); + ft_tt_artifact_cache_request request; + ft_tt_artifact_cache_result result; + char line[FT_TT_CAPACITY_REASON_BYTES]; + unsigned long reserve; + int forced; + int rc; + + if (override_path && *override_path) { + if (snprintf(out, out_bytes, "%s", override_path) >= (int)out_bytes) { + fprintf(stderr, "qwen35_tt: REFUSED — %s is longer than %zu bytes\n", + PN_QWEN35_TT_ARTIFACT_ENV, out_bytes); + return -1; + } + fprintf(stderr, "qwen35_tt: artifact %s (%s override — its provenance is NOT " + "checked against the checkpoint)\n", out, PN_QWEN35_TT_ARTIFACT_ENV); + return 0; + } + if (!model->path || !*model->path) { + fprintf(stderr, "qwen35_tt: REFUSED — the model has no checkpoint path to convert " + "from, and %s is not set\n", PN_QWEN35_TT_ARTIFACT_ENV); + return -1; + } + + forced = qwen35_tt_forced_rung(); + if (forced == PN_QWEN35_TT_ENV_BAD) + return -1; + + reserve = PN_QWEN35_TT_RESERVE_MIB_DEFAULT; + if (reserve_mib && *reserve_mib) + reserve = strtoul(reserve_mib, NULL, 10); + + memset(&request, 0, sizeof(request)); + request.gguf_path = model->path; + request.cache_dir = getenv(PN_QWEN35_TT_CACHE_DIR_ENV); + request.budget.card_bytes = (uint64_t)FT_TT_MEM_DRAM_SIZE * 8ull; + request.budget.runtime_state_bytes = (uint64_t)reserve * 1024ull * 1024ull + + qwen35_tt_recurrent_state_bytes(cfg, n_ssm); + request.budget.kv_bytes_per_token = pn_qwen35moe_kv_bytes_per_token(m, false); + request.budget.context_tokens = + (uint32_t)(cfg->ctx_cap > 0 ? cfg->ctx_cap : model->config.n_ctx); + request.progress_style = FT_TT_CACHE_PROGRESS_STDERR; + request.forced_mode = forced; + /* The pairing is a property of the CHECKPOINT and the selector reads it off the file. + * For this family it answers `source`, and for this model that is not merely right but + * load-bearing: `attn_q` holds Q and its output gate INTERLEAVED PER HEAD + * ([h0 Q, h0 gate, h1 Q, ...], qwen35moe_attn.c), so a split-half row permutation over + * that tensor would permute the gate as well and there would be no error anywhere. */ + request.rope_pairing = -1; + + rc = ft_tt_artifact_cache_ensure(&request, &result); + if (rc != FT_TT_ARTIFACT_CACHE_OK) { + fprintf(stderr, "qwen35_tt: REFUSED — %s\n", result.reason); + return -1; + } + fprintf(stderr, "qwen35_tt: %s — %s\n", result.produced ? "converted" : "cached", + ft_tt_capacity_selection_line(&result.selection, &request.budget, line, + sizeof(line))); + if (result.produced && result.had_artifact && result.note[0]) + fprintf(stderr, "qwen35_tt: the cached artifact was NOT reusable and was not " + "served — %s\n", result.note); + fprintf(stderr, "qwen35_tt: rotary pairing %s (%s); KV %llu B/token, %u tokens " + "reserved, recurrent state %.2f MiB\n", + result.rope_pairing == FT_TT_ROPE_PAIRING_SPLIT_HALF ? "split-half" : "source", + result.rope_reason ? result.rope_reason : "", + (unsigned long long)request.budget.kv_bytes_per_token, + request.budget.context_tokens, + (double)qwen35_tt_recurrent_state_bytes(cfg, n_ssm) / (1024.0 * 1024.0)); + + if (snprintf(out, out_bytes, "%s", result.artifact_path) >= (int)out_bytes) { + fprintf(stderr, "qwen35_tt: REFUSED — the cached artifact's path does not fit\n"); + return -1; + } + return 0; +} + +/* ============================================================ + * Public entry points + * ============================================================ */ + +int pn_qwen35moe_tt_upload(pn_model *model) +{ + char artifact[FT_TT_ARTIFACT_CACHE_PATH_BYTES]; + const pn_qwen35moe_config *cfg; + pn_qwen35moe_config *cfg_mut; + pn_qwen35moe_model *m; + pn_qwen35_tt *g = NULL; + const char *report; + int n_ssm, n_attn; + int layer; + + if (!model || !model->weights) + return -1; + m = (pn_qwen35moe_model *)model->weights; + cfg = pn_qwen35moe_model_config(m); + if (!cfg) + return -1; + + /* Two refusals, and they are different sentences on purpose: telling the SABER user + * their model does not fit would send them to fix the wrong thing. */ + if (!cfg->dense_ffn || cfg->n_expert > 0) { + fprintf(stderr, "qwen35_tt: this backend serves the DENSE qwen35 branch; this " + "checkpoint is the MoE one (%d experts) and its router and expert " + "gather are not built here. Staying on CPU.\n", cfg->n_expert); + return -1; + } + if (cfg->n_embd % 32 != 0 || cfg->ffn_length % 32 != 0) { + fprintf(stderr, "qwen35_tt: REFUSED — a card tile is 32 wide and this geometry " + "(n_embd %d, ffn %d) does not divide by it\n", + cfg->n_embd, cfg->ffn_length); + return -1; + } + + n_ssm = pn_qwen35moe_model_n_ssm(m); + n_attn = pn_qwen35moe_model_n_attn(m); + if (n_ssm + n_attn != cfg->n_layer) { + fprintf(stderr, "qwen35_tt: REFUSED — %d SSM + %d attention layers is not the %d " + "this model declares\n", n_ssm, n_attn, cfg->n_layer); + return -1; + } + + g = calloc(1, sizeof(*g)); + if (!g) + return -1; + report = getenv(PN_QWEN35_TT_REPORT_ENV); + g->report_every = report ? atoi(report) : 0; + g->n_expected = qwen35_tt_expected_bindings(cfg, n_ssm, n_attn); + g->n_layers = cfg->n_layer; + { + const char *sparse = getenv(PN_QWEN35_TT_SPARSE_ENV); + g->sparse_env = (sparse && *sparse) ? (atoi(sparse) != 0) : -1; + g->sparse_logits = g->sparse_env == 1; + } + + /* The card is opened BEFORE the artifact is resolved, and the order is load-bearing: + * resolving can mean a ten-minute conversion, and doing that on a machine whose card + * then fails to open would burn the ten minutes to end up on the CPU anyway. */ + if (pn_tt_device_open(0, &g->device) != 0) + goto fail; + /* M1: depth 4 on this model (+0.6-1.0% decode, +0.6% prefill, three rounds). Before the + * artifact loads, because the matmul residency arms there. */ + pn_tt_device_prefer_prefetch_tiles(g->device, 4u); + /* M4: pipelined matmul reader + 3-chunk CBs (+1.7% decode, +1.0% prefill). */ + pn_tt_device_prefer_matmul_pipe(g->device, true); + if (qwen35_tt_resolve_artifact(model, m, cfg, n_ssm, artifact, sizeof(artifact)) != 0) + goto fail; + if (pn_tt_device_load_artifact(g->device, artifact) != 0) + goto fail; + pn_qwen35_tt_mtp_resolve(g, artifact); + + g->bindings = calloc((size_t)g->n_expected, sizeof(*g->bindings)); + g->layers = calloc((size_t)cfg->n_layer, sizeof(*g->layers)); + if (!g->bindings || !g->layers) + goto fail; + for (layer = 0; layer < cfg->n_layer; layer++) { + if (qwen35_tt_bind_layer(g, m, cfg, layer) != 0) + goto fail; + } + if (qwen35_tt_bind(g, "output.weight", pn_qwen35moe_model_output(m), &g->output) != 0) + goto fail; + + if (g->n_bindings != g->n_expected) { + fprintf(stderr, "qwen35_tt: REFUSED — bound %d weights where the geometry predicts " + "%d. A missing binding is a matmul that would run on the CPU while " + "the run reported the card.\n", g->n_bindings, g->n_expected); + goto fail; + } + + /* The shape the artifact says must be the shape the engine says. A silent disagreement + * is a matmul with the right tile counts and the wrong meaning. */ + { + pn_tt_weight head = g->bindings[g->n_bindings - 1].card; + if ((int)head.out_features != cfg->n_vocab || (int)head.in_features != cfg->n_embd) { + fprintf(stderr, "qwen35_tt: REFUSED — the artifact's LM head is %u x %u and the " + "engine says %d x %d\n", head.in_features, head.out_features, + cfg->n_embd, cfg->n_vocab); + goto fail; + } + } + + cfg_mut = pn_qwen35moe_model_config_mut(m); + cfg_mut->mv.fn = qwen35_tt_mv; + cfg_mut->mv.ctx = g; + pn_qwen35moe_model_set_gpu(m, g); + + /* Never fatal: a card that cannot arm the on-card families still serves this model on + * the projection-only arm, slower and not wrong. */ + if (pn_qwen35_tt_resident_arm(g, m, cfg) != 0) + goto fail; + + fprintf(stderr, "qwen35_tt: %d matmul weights resident (%.2f GiB), %d SSM + %d " + "attention layers, %d matmuls a token, %s arm\n", + (int)pn_tt_device_weight_count(g->device), + (double)pn_tt_device_weight_bytes(g->device) / (1024.0 * 1024.0 * 1024.0), + n_ssm, n_attn, g->n_bindings, + g->resident ? "RESIDENT-ACTIVATION" : "projection-only"); + /* AFTER every family has armed, so the line covers the KV regions, the GDN carried state + * and the activation mirrors as well as the weights — which is what a capacity question + * has to be answered from, and what the weight figure alone cannot say. */ + pn_tt_device_report_dram(g->device, "after arm"); + return 0; + +fail: + /* DETACH THE SEAM BEFORE FREEING WHAT IT POINTS AT. The three lines above this label + * install `qwen35_tt_mv` on the SHARED CONFIG with `g` as its context, and the arm below + * them is the one that can fail — so a refusal there used to free `g` and leave + * `cfg->mv.fn` pointing into it. The caller then falls back to the CPU forward, which + * calls `qwen35moe_mv()` on the very first projection, which reads the freed context. + * + * MEASURED 2026-08-21: on-card attention asked for BY NAME with a head-norm slot it could + * not have refuses correctly, the engine prints "using CPU forward" — and the process + * dies with SIGSEGV before the first token. The refusal is meant to cost ~0.05 tok/s and + * it cost a crash, which is not a price anyone chose. This is the same "an accelerator is + * not always a forward" seam that made a parity harness score the card against itself: + * on this family the accelerator is a HOOK, so tearing it down means tearing down the + * hook and not only the object. + * + * FORGET rather than detach: detaching only moves the hook into a shadow copy that a + * later `parity_accel_seam_enable(m, 1)` would put straight back. */ + pn_qwen35moe_model_accel_mv_forget(m); + pn_qwen35moe_model_set_gpu(m, NULL); + if (g) { + pn_qwen35_tt_resident_release(g); + free(g->layers); + free(g->bindings); + if (g->device) + pn_tt_device_close(g->device); + free(g); + } + return -1; +} + +int pn_qwen35moe_model_tt_loaded(const pn_model *model) +{ + const pn_qwen35moe_model *m; + + if (!model || !model->weights) + return 0; + m = (const pn_qwen35moe_model *)model->weights; + return pn_qwen35moe_model_gpu(m) != NULL; +} + +int pn_qwen35moe_model_tt_kv_on_card(const pn_model *model) +{ + const pn_qwen35moe_model *m; + const pn_qwen35_tt *g; + + if (!model || !model->weights) + return 0; + m = (const pn_qwen35moe_model *)model->weights; + g = (const pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + return g && pn_qwen35_tt_attn_is_on_card(g) ? 1 : 0; +} + +void pn_qwen35moe_tt_free(pn_model *model) +{ + pn_qwen35moe_model *m; + pn_qwen35_tt *g; + + if (!model || !model->weights) + return; + m = (pn_qwen35moe_model *)model->weights; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + if (!g) + return; + /* FORGET, not detach — the shadow copy `..._mv_enable(m, 0)` keeps would still name `g` + * after the free below, and a parity harness re-enabling the seam would restore it. */ + pn_qwen35moe_model_accel_mv_forget(m); + pn_qwen35moe_model_set_gpu(m, NULL); + pn_qwen35_tt_resident_release(g); + pn_qwen35_tt_prefill_release(g); + pn_qwen35_tt_ckpt_release(g); + free(g->layers); + free(g->bindings); + /* Closes the residency. It never parks the core — parking a resident triad breaks its + * next boot, whether or not every processor stopped cleanly (DEV_NOTES.md §1). */ + if (g->device) + pn_tt_device_close(g->device); + free(g); +} + +void pn_qwen35moe_tt_matmul_counts(const pn_model *model, uint64_t *on_card, + uint64_t *on_host, uint64_t *failed) +{ + const pn_qwen35moe_model *m; + const pn_qwen35_tt *g = NULL; + + if (model && model->weights) { + m = (const pn_qwen35moe_model *)model->weights; + g = (const pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + } + if (on_card) + *on_card = g ? g->mv_on_card : 0; + if (on_host) + *on_host = g ? g->mv_on_host : 0; + if (failed) + *failed = g ? g->mv_failed : 0; +} + +/** + * Where the time went, per token, split by machine rather than summed. + * + * "A token takes N seconds" is not a finding; the attribution is. `upload` is a host tilize + * plus a ~287 MB/s write, `dispatch` is the kernel launch and the card's own compute, and + * `readback` is the ~5 MB/s return. At this rung every matvec pays all three, so the split + * is the argument for the next rung and the number that prices it. + * + * `resident` and `oneshot` are EVIDENCE and not telemetry: both dispatch paths produce + * identical results from identical kernels, so a run that silently fell back to one-shot + * looks exactly like a resident one from its output alone. + * + * The last line is the one the resident arm is read from: `oncard` is the seconds and the + * descriptor count of the norms, the SwiGLUs, the residual adds and the top-N, which are + * work that used to be host arithmetic OUTSIDE all four phases. Moving an op onto the card + * moves its cost INTO `oncard`, so a rung that halves the readback and grows `oncard` has + * not lost the difference — it has started reporting work it was always doing. + */ +/** + * The wall against the phase sum, with the difference broken out by name. + * + * The four device counters time the SEAMS and only the seams. Host arithmetic between two + * seams — the Gated-Delta-Net recurrence, the attention core, the embedding gather, the + * sparse logit row — is in none of them, so "dispatch + readback + on-card" is a lower bound + * on the token and quoting it AS the token prices the next rung against the wrong + * denominator. `unattributed` is what neither the counters nor the four named regions claim; + * it is malloc, the arena resets, the per-descriptor host bookkeeping and the timer calls + * themselves, and if it ever grows past a few percent something else has moved into it. + * + * `forward_wall` still excludes sampling and de-tokenisation, which happen in the caller's + * generate loop. Those are read by subtracting the cumulative figure printed here from the + * CLI's own decode wall. + */ +static void qwen35_tt_report_wall(const pn_qwen35_tt *g, double upload, double dispatch, + double readback, double oncard) +{ + double tokens = (double)(g->tokens ? g->tokens : 1); + double phases = upload + dispatch + readback + oncard; + double named = g->host_recurrence_seconds + g->host_attention_seconds + + g->host_embed_seconds + g->host_logit_seconds; + double wall = g->forward_wall_seconds; + double unattributed = wall - phases - named; + + fprintf(stderr, "qwen35_tt: WALL %.4f s/token (%.2f tok/s) = phases %.4f + host GDN " + "%.4f + host attn %.4f + embed %.4f + logit row %.4f + unattributed " + "%.4f\n", + wall / tokens, wall > 0.0 ? tokens / wall : 0.0, phases / tokens, + g->host_recurrence_seconds / tokens, g->host_attention_seconds / tokens, + g->host_embed_seconds / tokens, g->host_logit_seconds / tokens, + unattributed / tokens); + fprintf(stderr, "qwen35_tt: the phase sum is %.1f%% of the wall; %.4f s/token " + "(%.1f%%) is host work OUTSIDE all four counters. Cumulative forward " + "wall %.3f s over %llu forwards\n", + wall > 0.0 ? 100.0 * phases / wall : 0.0, (wall - phases) / tokens, + wall > 0.0 ? 100.0 * (wall - phases) / wall : 0.0, wall, + (unsigned long long)g->tokens); +} + +/** + * The on-card phase PER FAMILY, and the GDN's two phases apart. + * + * "162 descriptors a token, 247 µs each" is an average over families whose work differs by + * three orders of magnitude — a 4,096-element RMS norm and a 32-head delta rule are both one + * descriptor. Every re-pricing of the on-card term has so far been a SUBTRACTION between two + * runs, which cannot tell one family from another and cannot tell CONV from DELTA at all. + * These counters are per residency and were always being kept; this only prints them. + */ +static void qwen35_tt_report_residencies(const pn_qwen35_tt *g) +{ + static const char *const phase_name[3] = { "?", "conv", "delta" }; + const double tokens = (double)(g->tokens ? g->tokens : 1); + unsigned index; + unsigned phase; + + for (index = 0;; index++) { + const char *name = NULL; + uint64_t ops = 0; + double seconds = 0.0; + uint32_t core = 0; + + if (!pn_tt_device_residency_stat(g->device, index, &name, &ops, &seconds, &core)) + break; + if (ops == 0u) + continue; + fprintf(stderr, "qwen35_tt: residency %-10s core %3u — %.4f s/token over %.1f " + "descriptors/token, %.1f us each\n", + name ? name : "?", core, seconds / tokens, (double)ops / tokens, + 1e6 * seconds / (double)ops); + } + { + double issue = 0.0, wait = 0.0; + uint64_t issues = 0, waits = 0, wait_descriptors = 0; + + pn_tt_device_dispatch_split(g->device, &issue, &issues, &wait, &waits, + &wait_descriptors); + if (issues != 0u) + fprintf(stderr, "qwen35_tt: dispatch ISSUE %.4f s/token over %.1f issues/token, " + "%.1f us each; WAIT %.4f s/token over %.1f waits/token, %.1f us " + "each covering %.2f descriptors\n", + issue / tokens, (double)issues / tokens, 1e6 * issue / (double)issues, + wait / tokens, (double)waits / tokens, + waits ? 1e6 * wait / (double)waits : 0.0, + waits ? (double)wait_descriptors / (double)waits : 0.0); + /* The device-level split is only fed by the CHAINED form (`pn_tt_matvec_enqueue`), + * and this model dispatches through `pn_tt_matvec_dispatch`, which enqueues and + * flushes in one call and touches neither counter — so on this arm the line above + * is silent and the group's own clocks are the only ones that see the matmul. They + * are printed unconditionally for that reason. */ + pn_tt_matmul_group_time_report(g->device); + pn_tt_matmul_group_read_report(g->device, tokens); + if (g->tokens > g->idle_clock_from && g->idle_clock_from > 0u) + pn_tt_device_idle_clock(g->device, 1, (double)(g->tokens - g->idle_clock_from)); + pn_tt_dispatch_report(); + pn_tt_device_wait_report(g->device, tokens); + } + { + /* THE AWAIT IN PCIe ACCESSES, SPIN APART FROM SETTLE. `settle` is the term the + * completion shape moves — **16.0 control / 5.0 window / 0.0 line** — and it is the + * only one an arm may be scored on. `poll` is the wait loop and is CARD-BOUND: it is + * how the host waits, it runs ~44 reads an await on this model, and it swamps the + * settle term in any pooled ratio. Printed unconditionally, because a knob that did + * not arm has to be visible as a number and not as an absence. */ + uint64_t awaits = 0, poll = 0, settle = 0; + unsigned fast = 0; + + pn_tt_device_await_transactions(g->device, &awaits, &poll, &settle, &fast); + if (awaits != 0u) + fprintf(stderr, "qwen35_tt: residency AWAIT %.1f awaits/token — SETTLE %.2f " + "reads each (%.1f/token), POLL %.1f reads each (%.1f/token); " + "%u residencies on the one-transaction path\n", + (double)awaits / tokens, (double)settle / (double)awaits, + (double)settle / tokens, (double)poll / (double)awaits, + (double)poll / tokens, fast); + } + { + /* THE ISSUE IN PCIe ACCESSES, AND ITS THREE CALLS TIMED APART. The whole point of the + * per-call split is that a per-ACCESS model and a per-CALL model predict opposite + * things about the wide shape, and the recorded 2.08/4.84 µs pair fits neither. Three + * calls at 16, 8 and 1 accesses over-determine it. Printed unconditionally so a knob + * that did not arm is visible as a number and not as an absence. */ + uint64_t issues = 0, accesses = 0; + double param = 0.0, ring = 0.0, head = 0.0; + unsigned wide = 0; + + pn_tt_device_issue_transactions(g->device, &issues, &accesses, ¶m, &ring, &head, + &wide); + if (issues != 0u) + fprintf(stderr, "qwen35_tt: residency ISSUE %.1f issues/token — %.2f accesses " + "each (%.1f/token), %.4f s/token = param %.2f + ring %.2f + head " + "%.2f us each; %u residencies on the wide shape\n", + (double)issues / tokens, (double)accesses / (double)issues, + (double)accesses / tokens, (param + ring + head) / tokens, + 1e6 * param / (double)issues, 1e6 * ring / (double)issues, + 1e6 * head / (double)issues, wide); + } + { + /* WHICH WINDOW ACTUALLY CARRIED IT — the one witness of the issue shape that is not + * this file's own bookkeeping. `wide` moves the parameter block and the ring slot from + * the UC window to the WC one, so the write bytes must MOVE BETWEEN THESE TWO NUMBERS + * by the amount the issue counters claim. A knob that armed nothing leaves them where + * they were, and no amount of derived counting could tell you that. */ + uint64_t reg_r = 0, reg_w = 0, bulk_r = 0, bulk_w = 0; + uint64_t reg_aims = 0, bulk_aims = 0, aim_failures = 0; + double aim_seconds = 0.0, aim_verify_seconds = 0.0; + + pn_tt_device_window_traffic(g->device, ®_r, ®_w, &bulk_r, &bulk_w); + pn_tt_device_window_aims(g->device, ®_aims, &bulk_aims, &aim_failures); + pn_tt_device_window_aim_seconds(g->device, &aim_seconds, &aim_verify_seconds); + fprintf(stderr, "qwen35_tt: windows — register(UC/word32) read %.1f write %.1f B/token " + "%llu aims, bulk(WC/block16) read %.1f write %.1f B/token %llu aims, " + "%llu aim failures\n", + (double)reg_r / tokens, (double)reg_w / tokens, + (unsigned long long)reg_aims, (double)bulk_r / tokens, + (double)bulk_w / tokens, (unsigned long long)bulk_aims, + (unsigned long long)aim_failures); + /* WHAT THE AIMS COST. The issue path re-aims a window ONCE PER DESCRIPTOR, so this is + * a per-token term and not setup — and the readback's share of it is what decides + * whether the fix is fewer readbacks or more windows. */ + if (reg_aims + bulk_aims != 0u) + fprintf(stderr, "qwen35_tt: window AIMS %.1f/token — %.4f s/token, %.2f us each, " + "of which the READBACK is %.4f s/token (%.1f%%, %.2f us each)\n", + (double)(reg_aims + bulk_aims) / tokens, aim_seconds / tokens, + 1e6 * aim_seconds / (double)(reg_aims + bulk_aims), + aim_verify_seconds / tokens, + aim_seconds > 0.0 ? 100.0 * aim_verify_seconds / aim_seconds : 0.0, + 1e6 * aim_verify_seconds / (double)(reg_aims + bulk_aims)); + } + { + /* WHETHER THE BANK IS ABSORBING THE ROTATION, measured rather than inferred from the + * aim count falling. The window COUNTS are what the knob achieved — a bank that could + * not claim what it asked for runs short and says so — and `misaims` is the red arm's + * own counter, printed unconditionally so an arm that never fired reads as a 0 and + * not as an absence. `audit` must be 0 on every arm but the red one. */ + uint32_t reg_windows = 0u, bulk_windows = 0u; + uint64_t rhit = 0, rmiss = 0, bhit = 0, bmiss = 0, misaims = 0, audit = 0; + + pn_tt_device_window_bank(g->device, ®_windows, &bulk_windows, &rhit, &rmiss, &bhit, + &bmiss, &misaims, &audit); + if (reg_windows + bulk_windows != 0u) + fprintf(stderr, "qwen35_tt: window BANK — register %u windows %.1f hit + %.1f " + "miss/token (%.2f%%), bulk %u windows %.1f hit + %.1f miss/token " + "(%.2f%%), %llu misaims injected, %llu audit failures\n", + reg_windows, (double)rhit / tokens, (double)rmiss / tokens, + (rhit + rmiss) ? 100.0 * (double)rhit / (double)(rhit + rmiss) : 0.0, + bulk_windows, (double)bhit / tokens, (double)bmiss / tokens, + (bhit + bmiss) ? 100.0 * (double)bhit / (double)(bhit + bmiss) : 0.0, + (unsigned long long)misaims, (unsigned long long)audit); + } + for (phase = 1u; phase < 3u; phase++) { + double seconds = 0.0, recovered_seconds = 0.0; + uint64_t ops = 0, recovered_ops = 0, rearms = 0; + + pn_tt_device_gdn_phase_stats(g->device, phase, &seconds, &ops); + pn_tt_device_gdn_phase_recovery(g->device, phase, &recovered_seconds, &recovered_ops, + &rearms); + if (ops == 0u && recovered_ops == 0u) + continue; + /* THE RE-ARM COUNT PRINTS EVEN AT ZERO. The `us each` beside it is the CLEAN figure, + * and a contaminated one that reads as clean is the whole defect: PHASE CONV read + * 4,502 us a descriptor against 738.2 with nothing behind it but a circular-buffer + * write pointer that made every CONV after a DELTA refuse, and no line in this report + * could have said so. */ + fprintf(stderr, "qwen35_tt: gdn phase %-5s — %.4f s/token over %.1f " + "descriptors/token, %.1f us each; %llu replayed over %llu re-arms " + "(%.4f s)\n", + phase_name[phase], seconds / tokens, (double)ops / tokens, + ops ? 1e6 * seconds / (double)ops : 0.0, + (unsigned long long)recovered_ops, (unsigned long long)rearms, + recovered_seconds); + } +} + +/** + * The on-card phase PER SHAPE, and each family's fixed/per-tile split where it can be solved. + * + * The line above this one reports the elementwise residency as 136 descriptors at one mean. + * Those 136 are six functions over three widths — 64 residual adds at 128 tiles, 32 SwiGLUs + * at 384, 24 GDN output gates at 128, and the attention gate's SIGMOID and MUL at 256 apiece. + * A mean over that cannot price any lever against any of them, which is the same defect the + * per-family report was written to fix, one level down. + * + * The FIT is the second thing this buys. A family running several widths through one program + * solves `per_descriptor = fixed + per_tile * tiles` from its own ordinary descriptors — and + * the intercept is exactly what merging or removing descriptors recovers. It is NOT a host + * round trip: it is everything that does not scale with the strip, host and card together. + * A family that ran one width is refused rather than divided. + */ +static void qwen35_tt_report_census(const pn_qwen35_tt *g) +{ + static const char *const families[] = { "eltwise", "render-widen", "render-narrow", + "reduce", "head-norm", "topk" }; + const pn_tt_descriptor_census *census = pn_tt_device_census(g->device); + const double tokens = (double)(g->tokens ? g->tokens : 1); + uint32_t index; + size_t family; + + if (!census || census->row_count == 0u) + return; + for (index = 0; index < census->row_count; index++) { + const pn_tt_descriptor_census_row *row = NULL; + + if (!pn_tt_descriptor_census_row_at(census, index, &row) || !row || row->ops == 0u) + continue; + fprintf(stderr, "qwen35_tt: census %-13s %-9s Wt=%-5u — %.4f s/token over %.1f " + "descriptors/token, %.1f us each, %.3f us/tile\n", + row->family, row->function, row->tile_count, row->seconds / tokens, + (double)row->ops / tokens, 1e6 * row->seconds / (double)row->ops, + 1e6 * row->seconds / ((double)row->ops * (double)row->tile_count)); + } + for (family = 0; family < sizeof(families) / sizeof(families[0]); family++) { + double fixed = 0.0, per_tile = 0.0; + uint32_t shapes = 0; + + if (pn_tt_descriptor_census_fit(census, families[family], &fixed, &per_tile, + &shapes) != 0) + continue; + fprintf(stderr, "qwen35_tt: census FIT %-13s over %u widths — fixed %.1f us/descriptor " + "+ %.3f us/tile\n", families[family], shapes, 1e6 * fixed, + 1e6 * per_tile); + } + /* Printed even at zero. A census that quietly dropped rows reads exactly like a census of + * a cheaper model, and the reader has to be able to see that it did not. The re-arm count + * is here for the same reason: every `us each` above is the CLEAN figure, and a run with + * refusals in it has to be readable AS one. */ + { + uint64_t recovered_ops = 0, rearms = 0; + double recovered_seconds = 0.0; + + (void)pn_tt_descriptor_census_family_recovery(census, NULL, &recovered_ops, + &recovered_seconds, &rearms); + fprintf(stderr, "qwen35_tt: census %u keys, %llu dropped (%.4f s), %llu refused, " + "%llu replayed over %llu re-arms (%.4f s)\n", + census->row_count, (unsigned long long)census->dropped, + census->dropped_seconds, (unsigned long long)census->refused, + (unsigned long long)recovered_ops, (unsigned long long)rearms, + recovered_seconds); + } +} + +/** + * The elementwise split PER MEMBER — silent when the family runs on one core. + * + * Reported apart from the census because the census keys on the WHOLE descriptor's width, by + * design: that keeps the `fixed + per_tile * tiles` fit comparable across a member ladder. + * What it therefore cannot show is the shape of the set, and "it stopped at N" is exactly the + * finding that needs it — a knee where every member costs the same is the cores; a knee where + * one member costs more than the others is that member being starved. + */ +static void qwen35_tt_report_eltwise_split(const pn_qwen35_tt *g) +{ + const double tokens = (double)(g->tokens ? g->tokens : 1); + unsigned index; + + for (index = 1u; index < 64u; index++) { + uint32_t core = 0u; + uint64_t ops = 0; + double seconds = 0.0; + + /* Member 1 existing is what makes this a SPLIT; a lone member 0 is the pre-split + * path and has nothing to say here. */ + if (!pn_tt_device_eltwise_member_stat(g->device, index, NULL, NULL, NULL)) + break; + if (index == 1u) { + pn_tt_device_eltwise_member_stat(g->device, 0u, &core, &ops, &seconds); + fprintf(stderr, "qwen35_tt: eltwise split member 0 core %-3u — %.4f s/token " + "await over %.1f descriptors/token\n", + core, seconds / tokens, (double)ops / tokens); + } + pn_tt_device_eltwise_member_stat(g->device, index, &core, &ops, &seconds); + fprintf(stderr, "qwen35_tt: eltwise split member %-1u core %-3u — %.4f s/token " + "await over %.1f descriptors/token\n", + index, core, seconds / tokens, (double)ops / tokens); + } +} + +/** + * The strip bridge's split PER MEMBER — silent when a direction runs on one core. + * + * Same argument as the elementwise one above, and printed for the same reason: a SPLIT line + * at arming time is a claim about what was asked for, and a later stage can still take the + * members away. Measured 2026-08-26 on the elementwise family, where `pn_tt_publish_replica_ + * map()` COLLAPSED every split above two members and the arming line still said 16 — three + * census ladders read as a core-scaling knee that was a table size. A per-member line that + * is ABSENT is the only thing that catches that. + */ +static void qwen35_tt_report_render_split(const pn_qwen35_tt *g) +{ + const double tokens = (double)(g->tokens ? g->tokens : 1); + uint32_t direction; + + for (direction = 0; direction < PN_TT_RENDER_INDEX_COUNT; direction++) { + uint32_t members = pn_tt_render_members(g->device, direction); + uint32_t member; + + if (members < 2u) + continue; + for (member = 0; member < members; member++) { + const char *name = "?"; + uint32_t core = 0u; + uint64_t ops = 0; + double seconds = 0.0; + + if (!pn_tt_render_member_stat(g->device, direction, member, &name, &core, &ops, + &seconds)) + break; + fprintf(stderr, "qwen35_tt: %s split member %-1u core %-3u — %.4f s/token " + "await over %.1f descriptors/token\n", + name, member, core, seconds / tokens, (double)ops / tokens); + } + } +} + +static void qwen35_tt_report(const pn_qwen35_tt *g) +{ + double upload = 0.0, dispatch = 0.0, readback = 0.0; + uint64_t resident = 0, oneshot = 0, restarts = 0; + double per_token; + + pn_tt_device_phase_seconds(g->device, &upload, &dispatch, &readback); + pn_tt_device_dispatch_counts(g->device, &resident, &oneshot, &restarts); + per_token = g->tokens ? (upload + dispatch + readback) / (double)g->tokens : 0.0; + + fprintf(stderr, "qwen35_tt: token %llu — %llu matmuls on the card, %llu on the host, " + "%llu refused\n", (unsigned long long)g->tokens, + (unsigned long long)g->mv_on_card, (unsigned long long)g->mv_on_host, + (unsigned long long)g->mv_failed); + fprintf(stderr, "qwen35_tt: phases %.3f s upload + %.3f s dispatch + %.3f s readback " + "= %.3f s/token on the card path (%.2f tok/s if it were all)\n", + upload / (double)(g->tokens ? g->tokens : 1), + dispatch / (double)(g->tokens ? g->tokens : 1), + readback / (double)(g->tokens ? g->tokens : 1), per_token, + per_token > 0.0 ? 1.0 / per_token : 0.0); + fprintf(stderr, "qwen35_tt: %llu resident dispatches, %llu one-shot, %llu restarts; " + "%.2f MiB read back, %.2f MiB written\n", + (unsigned long long)resident, (unsigned long long)oneshot, + (unsigned long long)restarts, + (double)pn_tt_device_bytes_read(g->device) / (1024.0 * 1024.0), + (double)pn_tt_device_bytes_written(g->device) / (1024.0 * 1024.0)); + { + double oncard = 0.0; + uint64_t ops = 0, oncard_restarts = 0; + + pn_tt_device_oncard_stats(g->device, &oncard, &ops, &oncard_restarts); + fprintf(stderr, "qwen35_tt: on-card ops %.3f s/token over %llu descriptors " + "(%llu restarts); readback %.2f KiB/token\n", + oncard / (double)(g->tokens ? g->tokens : 1), (unsigned long long)ops, + (unsigned long long)oncard_restarts, + (double)pn_tt_device_bytes_read(g->device) / + (1024.0 * (double)(g->tokens ? g->tokens : 1))); + qwen35_tt_report_wall(g, upload, dispatch, readback, oncard); + } + qwen35_tt_report_residencies(g); + qwen35_tt_report_eltwise_split(g); + qwen35_tt_report_render_split(g); + qwen35_tt_report_census(g); + /* The on-card GDN verdict rides the same cadence, so a long run shows the gate moving + * rather than only reporting it at teardown — a run killed on the link never reaches the + * release path, and a gate whose only output is at exit measures nothing then. */ + pn_qwen35_tt_gdn_parity_report(g); + pn_qwen35_tt_gdn_normcheck_report(g); +} + +/** + * SAY IT WHEN A PROMPT IS BEING PREFILLED ONE POSITION AT A TIME. + * + * The chunkwise prefill ships ON and is worth ~24x, and it is reached by the caller ASKING + * for it (`pn_qwen35moe_prefill_tt`). A caller that does not ask gets a correct answer at + * ~20x the wall, and until 2026-08-26 two of the three prefill entry points in this tree did + * not ask — with nothing anywhere saying so. This is the counter that says so, and it lives + * in the forward rather than at a call site precisely because the defect WAS a call site. + * + * `run` is consecutive single-position forwards. Generation is also single-position, so the + * threshold is what separates a prompt from a completion; a chat turn of 64 generated tokens + * on a model that HAS prefilled once will not trip it, because a batched pass clears the run + * and sets `prefill_passes`. + * + * @param g the residency, for its counters + * @param pos the position this forward is about to compute + */ +static void qwen35_tt_note_sequential_prefill(pn_qwen35_tt *g, int pos) +{ + if (pos == 0) + g->sequential_prefill_run = 0; + g->sequential_prefill_run++; + if (g->sequential_prefill_said || g->prefill_passes != 0u || + g->sequential_prefill_run < PN_QWEN35_TT_SEQUENTIAL_PREFILL_ALARM) + return; + g->sequential_prefill_said = true; + fprintf(stderr, + "qwen35_tt: PREFILL IS RUNNING PER TOKEN — %llu consecutive single-position " + "forwards and NOT ONE batched pass. The chunkwise prefill is armed and nobody " + "asked it; this prompt costs roughly 20x what it should. The caller must go " + "through pn_qwen35moe_prefill_tt(), or set PN_TT_PREFILL=0 to say the " + "per-token path is what was wanted.\n", + (unsigned long long)g->sequential_prefill_run); +} + +/** Wrap the logit row the chosen arm produced in the tensor the engine expects. */ +static tm_tensor *qwen35_tt_wrap_logits(const float *logits, int n_vocab) +{ + int64_t shape[1] = { n_vocab }; + tm_tensor *out = tm_empty(shape, 1, TM_F32); + + if (!out) + return NULL; + memcpy(tm_data_f32(out), logits, (size_t)n_vocab * sizeof(float)); + return out; +} + +/** + * A MEASUREMENT KNOB, NOT A FEATURE: `PN_TT_IDLE_PROBE=:` sleeps `ms` before the + * ``-th TT forward of this process (0-based), so every residency on the card sits idle + * across the gap exactly as it does in a `serve` between requests. It exists to prove the idle + * retire: with the retire ON the token after the gap must be byte-identical; with it OFF + * (`PN_TT_RESIDENCY_IDLE_RETIRE_MS=0`) the first descriptors after a gap past the idle bound + * must be refused. Read once; said out loud when armed. + */ +static void qwen35_tt_idle_probe(void) +{ + static long at = -2, ms = 0, calls = 0; + + if (at == -2) { + const char *value = getenv("PN_TT_IDLE_PROBE"); + + at = -1; + if (value && sscanf(value, "%ld:%ld", &at, &ms) == 2 && at >= 0 && ms > 0) + fprintf(stderr, "qwen35_tt: IDLE PROBE armed — %ld ms of card idle before TT " + "forward %ld\n", ms, at); + else + at = -1; + } + if (at >= 0 && calls == at) { + struct timespec nap = { ms / 1000, (ms % 1000) * 1000000L }; + + fprintf(stderr, "qwen35_tt: IDLE PROBE — sleeping %ld ms before forward %ld\n", ms, at); + (void)nanosleep(&nap, NULL); + } + calls++; +} + +tm_tensor *pn_qwen35moe_forward_tt(pn_model *model, uint32_t token, pn_state *state) +{ + pn_qwen35moe_model *m; + pn_qwen35moe_state *s; + pn_qwen35_tt *g; + struct fw_pool *pool = NULL; + tm_tensor *out; + double forward_started_ms; + + if (!model || !model->weights || !state) + return NULL; + m = (pn_qwen35moe_model *)model->weights; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + if (!g) { + fprintf(stderr, "qwen35_tt: the TT forward was selected with no residency " + "attached\n"); + return NULL; + } +#ifdef USE_THREADS + pool = model->pool; +#endif + qwen35_tt_idle_probe(); + forward_started_ms = pn_time_ms(); + + if (g->resident) { + const float *logits; + + s = (pn_qwen35moe_state *)state->kv_cache; + if (!s) + return NULL; + /* The residual stream, both RMSNorms, the dense SwiGLU and the LM head stay in card + * DRAM; the Gated-Delta-Net recurrence and the attention core run on the host, + * through the same two functions the CPU oracle calls. */ + qwen35_tt_note_sequential_prefill(g, state->pos); + pn_qwen35_tt_ckpt_note(g, state, state->pos); + logits = pn_qwen35_tt_resident_forward(g, m, token, s, state->pos, pool); + if (!logits) + return NULL; + { + double wrap_started_ms = pn_time_ms(); + out = qwen35_tt_wrap_logits(logits, s->n_vocab); + g->host_logit_seconds += (pn_time_ms() - wrap_started_ms) / 1000.0; + } + /* STEP 4 S2b: this position's hidden is the MTP catch-up owed before the next draft. */ + if (out && g->mtp.on) { + if (pn_tt_row_copy(g->device, &g->card.hidden[g->card.slot], 0u, &g->mtp.h, 0u) == 0) { + g->mtp.pend_base = state->pos; + g->mtp.pend_n = 1; + g->mtp.pend_tall = false; + } else { + g->mtp.pend_n = 0; + } + } + if (out) + state->pos++; + } else { + /* The whole hybrid stack, unchanged. The matmuls leave through the seam installed at + * upload; everything else is the CPU oracle's own arithmetic on its own fp32 state, + * which is what makes this arm diagnosable against it. */ + out = pn_qwen35moe_forward(model, token, state); + } + + g->forward_wall_seconds += (pn_time_ms() - forward_started_ms) / 1000.0; + g->tokens++; + if (g->tokens == 2u && g->idle_clock_from == 0u) { + pn_tt_device_idle_clock(g->device, 0, 0.0); + g->idle_clock_from = 2u; + } + if (g->tokens == 2u || g->tokens == 24u || g->tokens == 47u) { + char label[32]; + + snprintf(label, sizeof(label), "decode forward %llu", (unsigned long long)g->tokens); + pn_tt_device_telemetry_log(g->device, label); + } + if (g->report_every > 0 && g->tokens % (uint64_t)g->report_every == 0) + qwen35_tt_report(g); + return out; +} + +/* ======================================================================================= + * STEP 4 S2 — one speculative step (greedy, lossless) + * ======================================================================================= */ + +int pn_qwen35moe_tt_spec_step(pn_model *model, pn_state *state, uint32_t seed, + const uint32_t *drafts, int n_drafts, uint32_t *emit) +{ + return pn_qwen35moe_tt_spec_step_ex(model, state, seed, drafts, n_drafts, emit, NULL, NULL); +} + +/* This request's logit rows: `pairs` = 0 dense, else the card's exact top-`pairs`. */ +static void qwen35_tt_set_rows(pn_model *model, uint32_t pairs) +{ + pn_qwen35_tt *g; + + if (!model || !model->weights || model->forward != pn_qwen35moe_forward_tt) + return; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + if (!g || !g->resident) + return; + g->sparse_logits = g->sparse_capable && pairs > 0u; + g->topk_pairs = pairs > 0u ? pairs : PN_QWEN35_TT_TOPK_GREEDY; +} + +int pn_qwen35moe_tt_dense_row(void *model_ctx, float *out, int n_vocab) +{ + pn_model *model = (pn_model *)model_ctx; + pn_qwen35_tt *g; + + if (!model || !model->weights || !out) + return -1; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + if (!g || !g->resident) + return -1; + return pn_tt_activation_download(g->device, &g->card.logits, out, (uint32_t)n_vocab); +} + +int pn_qwen35moe_tt_dense_chunks(void *model_ctx, const unsigned char *need, float *out, + int n_vocab) +{ + pn_model *model = (pn_model *)model_ctx; + pn_qwen35_tt *g; + + if (!model || !model->weights || !out || !need) + return -1; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + if (!g || !g->resident) + return -1; + return pn_tt_activation_download_chunks(g->device, &g->card.logits, need, out, + (uint32_t)n_vocab); +} + +bool pn_qwen35moe_tt_greedy_exact_on_topn(const pn_sampling_params *params) +{ + return params && params->temperature <= 0.0f && !params->tool_grammar && + !(params->logit_bias && params->n_logit_bias > 0) && + params->presence_penalty == 0.0f && params->frequency_penalty == 0.0f; +} + +uint32_t pn_qwen35moe_tt_select_rows(pn_model *model, const pn_sampling_params *params, + bool needs_full_logits) +{ + pn_qwen35_tt *g; + uint32_t pairs = 0u; + const bool greedy = params && params->temperature <= 0.0f; + + if (!model || !model->weights || model->forward != pn_qwen35moe_forward_tt || !params) + return 0u; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + if (!g) + return 0u; + if (g->sparse_env == 1) { + pairs = greedy ? PN_QWEN35_TT_TOPK_GREEDY : PN_QWEN35_TT_TOPK_PAIRS; + } else if (g->sparse_env == -1 && !needs_full_logits) { + if (pn_qwen35moe_tt_greedy_exact_on_topn(params)) + pairs = PN_QWEN35_TT_TOPK_GREEDY; + /* Sampled (llama.cpp/HF order): the renormalised top-k set lies inside the top-64 + * unless the penalties reach past it — the sampler checks that per draw and refills + * the dense row (pn_sampler_topn_row_exact). `PN_TT_SAMPLED_TOPN=0`: dense. */ + else if (!greedy && !params->tool_grammar && !params->logprobs && + !(params->logit_bias && params->n_logit_bias > 0) && params->top_k >= 1 && + params->top_k <= (int)PN_QWEN35_TT_TOPK_PAIRS && + !(getenv("PN_TT_SAMPLED_TOPN") && atoi(getenv("PN_TT_SAMPLED_TOPN")) == 0)) + pairs = PN_QWEN35_TT_TOPK_PAIRS; + } + qwen35_tt_set_rows(model, pairs); + return g->sparse_logits ? pairs : 0u; +} + +bool pn_qwen35moe_tt_spec_can_sample(pn_model *model) +{ + pn_qwen35_tt *g; + + if (!model || !model->weights) + return false; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + return g && g->resident && g->sparse_logits && + !(getenv("PN_TT_SPEC_HEADS") && atoi(getenv("PN_TT_SPEC_HEADS")) == 0); +} + +int pn_qwen35moe_tt_spec_step_ex(pn_model *model, pn_state *state, uint32_t seed, + const uint32_t *drafts, int n_drafts, uint32_t *emit, + pn_spec_choose_fn choose, void *choose_ctx) +{ + pn_qwen35moe_model *m; + const pn_qwen35moe_config *cfg; + pn_qwen35moe_state *s; + pn_qwen35_tt *g; + uint32_t tokens[16], argmax[16], forced[16], next = 0u; + int rows, n_accept = 0, i, committed; + double started_ms; + + if (!model || !model->weights || !state || !emit || n_drafts < 0 || n_drafts > 15 || + (choose && !pn_qwen35moe_tt_spec_can_sample(model))) + return -1; + m = (pn_qwen35moe_model *)model->weights; + cfg = pn_qwen35moe_model_config(m); + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + s = (pn_qwen35moe_state *)state->kv_cache; + if (!g || !g->resident || !s || !cfg) + return -1; + pn_qwen35_tt_ckpt_note(g, state, state->pos); + /* The verify's heads need the top-N rows: refuse BEFORE any card work — a verify that + * ran and then failed would leave the carried state advanced past the committed token + * (the empty-env run: spec failed after the pass, greedy DIFFERED). */ + if (!g->sparse_logits && !(getenv("PN_TT_SPEC_HEADS") && atoi(getenv("PN_TT_SPEC_HEADS")) == 0)) + return -1; + /* The idle clock's window starts at the first speculative cycle when decode is spec. */ + if (g->idle_clock_from == 0u) { + pn_tt_device_idle_clock(g->device, 0, 0.0); + g->idle_clock_from = g->tokens > 0u ? g->tokens : 1u; + } + started_ms = pn_time_ms(); + tokens[0] = seed; + for (i = 0; i < n_drafts; i++) + tokens[1 + i] = drafts[i]; + rows = n_drafts + 1; + /* S2d MEASURE: `PN_TT_DISPATCH_OBSERVE_SPEC=1` + `PN_TT_DISPATCH_RECORD=1000000+c` records + * verify cycle c in the dispatch recorder's observe mode (2000000+c: its MTP drafting). */ + if (getenv("PN_TT_DISPATCH_OBSERVE_SPEC")) + pn_tt_dispatch_token_begin(g->device, 1000000u + g->spec_cycles); + { + /* S2d: the verify runs as STREAMED puller segments (`PN_TT_SPEC_STREAM=0` off). */ + const bool streamed = !getenv("PN_TT_DISPATCH_OBSERVE_SPEC") && + pn_tt_dispatch_stream_begin(g->device); + const int vrc = pn_qwen35_tt_spec_verify(g, m, cfg, s, tokens, rows, state->pos, argmax); + + if ((streamed && !pn_tt_dispatch_stream_end()) || vrc != 0) + return -1; + } + if (getenv("PN_TT_DISPATCH_OBSERVE_SPEC")) + pn_tt_dispatch_token_end(g->device, 1000000u + g->spec_cycles); + /* argmax[r] is the greedy token AFTER tokens[r]; draft i sits at row i+1, so it is + * accepted while it equals argmax[i]. RED `spec_accept_unverified`: the first draft is + * accepted whatever the verify said, and on the first cycle it is a token the verify + * certainly rejected (argmax + 1) — so the red cannot be vacuous at high acceptance. */ + { + const char *red = getenv("PN_TT_RED"); + const bool red_lookup = g_red_accept_next_cycle; + + g_red_accept_next_cycle = false; + if ((red && strcmp(red, "spec_accept_unverified") == 0) || red_lookup) { + for (i = 0; i < n_drafts; i++) + forced[i] = drafts[i]; + if ((g->spec_cycles == 0u || red_lookup) && n_drafts > 0) + forced[0] = (argmax[0] + 1u) % (uint32_t)s->n_vocab; + drafts = forced; + n_accept = n_drafts > 0 ? 1 : 0; + } + /* SAMPLING (`choose`): row r's token is the chooser's draw over row r's sparse + * logits, drawn once per EMITTED token in order; a draft is accepted iff the draw + * equals it — plain sampling's tokens, RNG stream and repeat window exactly. */ + for (;;) { + const uint32_t token = + choose ? choose(choose_ctx, g->spec_topk_index[n_accept], + g->spec_topk_value[n_accept], g->spec_topk_count[n_accept]) + : argmax[n_accept]; + + if (token == PN_SPEC_CHOOSE_DEFER) { + /* Row n_accept's top-N cannot decide it: the draft row n_accept-1 accepted + * becomes the next (uncommitted) token. Only rows >= 1 may defer. */ + if (n_accept == 0) + return -1; + n_accept--; + next = drafts[n_accept]; + g->spec_deferred++; + break; + } + if (n_accept < n_drafts && token == drafts[n_accept]) { + n_accept++; + continue; + } + next = token; + break; + } + for (i = 0; i < n_accept; i++) + emit[i] = drafts[i]; + } + if (getenv("PN_TT_SPEC_TRACE")) { + fprintf(stderr, "SPEC pos %d seed %u |", state->pos, seed); + for (i = 0; i < n_drafts; i++) + fprintf(stderr, " d%u", drafts[i]); + fprintf(stderr, " |"); + for (i = 0; i < rows; i++) + fprintf(stderr, " a%u", argmax[i]); + fprintf(stderr, " | accept %d\n", n_accept); + } + emit[n_accept] = next; + committed = n_accept + 1; /* the seed + the accepted drafts */ + { + const double t = pn_time_ms(); + + /* RED `spec_rollback_skip`: keep the state after the LAST verify row — must DIFFER. */ + const char *red = getenv("PN_TT_RED"); + + if (committed < rows && !(red && strcmp(red, "spec_rollback_skip") == 0) && + pn_qwen35_tt_spec_rollback(g, cfg, committed - 1) != 0) + return -1; + g->spec_rollback_ms += pn_time_ms() - t; + } + /* STEP 4 S2b: the committed rows' hiddens (the verify set, rows 0..committed-1) are the + * MTP catch-up owed before the next draft; each is followed by an accepted draft, the last + * by the next seed. */ + if (g->mtp.on) { + g->mtp.pend_base = state->pos; + g->mtp.pend_n = committed; + g->mtp.pend_tall = true; + for (i = 0; i < n_accept; i++) + g->mtp.pend_tok[i] = drafts[i]; + } + state->pos += committed; + /* The verify issued outside the dispatch model's per-token shape: re-learn it before any + * plain forward replays again. */ + pn_tt_dispatch_sequence_reset(); + g->spec_cycles++; + g->spec_drafted += (uint64_t)n_drafts; + g->spec_accepted += (uint64_t)n_accept; + g->forward_wall_seconds += (pn_time_ms() - started_ms) / 1000.0; + g->spec_total_ms += pn_time_ms() - started_ms; + g->tokens += (uint64_t)committed; + if (g->report_every > 0 && g->tokens >= (uint64_t)g->report_every && !g->spec_reported) { + uint64_t mr; + double mr_ms, cyc = (double)(g->spec_cycles ? g->spec_cycles : 1); + + g->spec_reported = true; + pn_tt_move_stats(&mr, &mr_ms); + fprintf(stderr, "qwen35_tt: SPEC — %llu cycles, %llu drafted, %llu accepted (%.2f), " + "%.2f tokens a cycle | ms a cycle: total %.1f = pass %.1f (row copies " + "%.1f x%.0f) + heads %.1f + mtp %.1f (%.1f steps, %.1f heads) + rollback " + "%.1f\n", + (unsigned long long)g->spec_cycles, (unsigned long long)g->spec_drafted, + (unsigned long long)g->spec_accepted, + g->spec_drafted ? (double)g->spec_accepted / (double)g->spec_drafted : 0.0, + (double)(g->spec_cycles + g->spec_accepted) / cyc, + (g->spec_total_ms + g->mtp.ms) / cyc, + g->spec_pass_ms / cyc, mr_ms / cyc, (double)mr / cyc, g->spec_head_ms / cyc, + g->mtp.ms / cyc, (double)g->mtp.steps / cyc, (double)g->mtp.heads / cyc, + g->spec_rollback_ms / cyc); + { + unsigned c; + double clocked = 0.0; + + fprintf(stderr, "qwen35_tt: SPEC verify pass by card clock, ms a cycle:"); + for (c = 0; c < g->spec_clock_count; c++) { + clocked += g->spec_clock_seconds[c]; + if (g->spec_clock_seconds[c] * 1000.0 / cyc >= 0.5) + fprintf(stderr, " %s %.1f", g->spec_clock_names[c], + g->spec_clock_seconds[c] * 1000.0 / cyc); + } + fprintf(stderr, " | clocked %.1f of pass %.1f = gdn rows %.1f + attention rows %.1f " + "+ batched %.1f\n", clocked * 1000.0 / cyc, g->spec_pass_ms / cyc, + g->spec_ssm_rows_ms / cyc, g->spec_attn_rows_ms / cyc, + (g->spec_pass_ms - g->spec_ssm_rows_ms - g->spec_attn_rows_ms) / cyc); + fprintf(stderr, "qwen35_tt: SPEC gdn rows by section, ms a cycle: pulls %.1f conv %.1f " + "l2 %.1f delta %.1f tail %.1f\n", + g->spec_gdn_part_ms[0] / cyc, g->spec_gdn_part_ms[1] / cyc, + g->spec_gdn_part_ms[2] / cyc, g->spec_gdn_part_ms[3] / cyc, + g->spec_gdn_part_ms[4] / cyc); + pn_tt_dispatch_stream_report(cyc); + } + qwen35_tt_report(g); + } + return n_accept + 1; +} + +/* ======================================================================================= + * STEP 4 S2b — MTP drafts + * ======================================================================================= */ + +static int qwen35_tt_mtp_draft_steps(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *s, + pn_state *state, uint32_t seed, int k, uint32_t *drafts, + int n); + +/** `PN_TT_SPEC_ADAPT`: the confidence below which drafting stops. DEFAULT 0.6 + * (run_serve_spec_adapt.sh round 2: g256 +5.5%, m256 +2.6%, t2 +2.0%, code/g48 flat, all + * IDENTICAL); `=0` off. */ +static float qwen35_tt_spec_adapt_tau(void) +{ + static float tau = -1.0f; + + if (tau < 0.0f) { + const char *env = getenv("PN_TT_SPEC_ADAPT"); + + tau = env && *env ? (float)atof(env) : 0.6f; + if (tau < 0.0f) + tau = 0.0f; + } + return tau; +} + +/** Drafts `from..k-1`, each one MTP step from the previous draft (the catch-up made draft 0). + * @return k, or the count of drafts that are still good when a step failed */ +static int qwen35_tt_mtp_draft_tail(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *s, + pn_state *state, uint32_t *drafts, int from, int k) +{ + const float tau = qwen35_tt_spec_adapt_tau(); + int i; + + for (i = from; i < k; i++) { + /* ADAPTIVE COUNT (`PN_TT_SPEC_ADAPT=tau`): a draft the head is unsure of is the last + * one worth a verify row; drafting past it spends an MTP step and a row on tokens the + * verify will almost surely refuse. The drafts are the same tokens either way. */ + if (tau > 0.0f && g->mtp.last_conf < tau) { + g->mtp.adapt_stops++; + return i; + } + if (pn_qwen35_tt_mtp_step(g, m, cfg, s, drafts[i - 1], NULL, 0u, state->pos - 1 + i, + true, &drafts[i]) != 0) + return i; + } + return k; +} + +int pn_qwen35moe_tt_mtp_draft(pn_model *model, pn_state *state, uint32_t seed, int k, + uint32_t *drafts) +{ + pn_qwen35moe_model *m; + const pn_qwen35moe_config *cfg; + pn_qwen35moe_state *s; + pn_qwen35_tt *g; + int n; + + if (!model || !model->weights || !state || !drafts || k < 1 || k > 15) + return -1; + m = (pn_qwen35moe_model *)model->weights; + cfg = pn_qwen35moe_model_config(m); + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + s = (pn_qwen35moe_state *)state->kv_cache; + if (!g || !g->mtp.on || !s || !cfg) + return -1; + n = g->mtp.pend_n; + /* The catch-up must end exactly where the seed sits, or the drafts are for elsewhere. */ + if (n < 1 || g->mtp.pend_base + n != state->pos) + return -1; + g->mtp.pend_n = 0; + { + /* The drafts are decode time: booked into the forward wall the reports divide. */ + const double before = g->mtp.ms; + int got; + + bool streamed = false; + + if (getenv("PN_TT_DISPATCH_OBSERVE_SPEC")) + pn_tt_dispatch_token_begin(g->device, 2000000u + g->spec_cycles); + else + streamed = pn_tt_dispatch_stream_begin(g->device); + got = qwen35_tt_mtp_draft_steps(g, m, cfg, s, state, seed, k, drafts, n); + if (streamed && !pn_tt_dispatch_stream_end()) + got = -1; + if (getenv("PN_TT_DISPATCH_OBSERVE_SPEC")) + pn_tt_dispatch_token_end(g->device, 2000000u + g->spec_cycles); + + g->forward_wall_seconds += (g->mtp.ms - before) / 1000.0; + return got; + } +} + +static int qwen35_tt_mtp_draft_steps(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *s, + pn_state *state, uint32_t seed, int k, uint32_t *drafts, + int n) +{ + int i; + + /* S2f: the committed rows' catch-up as ONE pass (`PN_TT_MTP_BATCH=0`: one step a row). */ + if (g->mtp.pend_tall && n >= 2 && + !(getenv("PN_TT_MTP_BATCH") && atoi(getenv("PN_TT_MTP_BATCH")) == 0)) { + uint32_t next[16]; + + for (i = 0; i < n - 1; i++) + next[i] = g->mtp.pend_tok[i]; + next[n - 1] = seed; + if (pn_qwen35_tt_mtp_catchup_rows(g, m, cfg, s, next, n, g->mtp.pend_base, + &drafts[0]) != 0) + return -1; + n = 0; + } + for (i = 0; i < n; i++) { + const uint32_t tok = i < n - 1 ? g->mtp.pend_tok[i] : seed; + const pn_tt_activation *src = + g->mtp.pend_tall ? &g->card_tall.hidden[g->card_tall.slot] : NULL; + + if (pn_qwen35_tt_mtp_step(g, m, cfg, s, tok, src, (uint32_t)i, g->mtp.pend_base + i, + i == n - 1, &drafts[0]) != 0) + return -1; + } + return qwen35_tt_mtp_draft_tail(g, m, cfg, s, state, drafts, 1, k); +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_gdn_normcheck_tt.c b/src/models/qwen35moe_gdn_normcheck_tt.c new file mode 100644 index 0000000..0f08786 --- /dev/null +++ b/src/models/qwen35moe_gdn_normcheck_tt.c @@ -0,0 +1,477 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_gdn_normcheck_tt.c + * @brief SCORE THE TWO PER-HEAD NORMS ON THE CARD AGAINST THE ORACLE'S OWN FUNCTIONS, on + * real layer data, without moving them yet. + * + * --------------------------------------------------------------------------- + * WHAT THIS MEASURES AND WHY IT IS A SEPARATE STEP + * --------------------------------------------------------------------------- + * The claim this project has restated several times without running it is that the GDN's + * two per-head norms are "ordinary narrow reduces on a second residency". Two halves of + * that claim were never tested: + * + * 1. **The LAYOUT.** A reduce residency reduces along a ROW, and the norms are per head. + * Every activation this backend has is a `[1, N]` strip. The resolution is that + * `resident_reduce_reader.c` reads row block `b` at tiles `b*tile_cols + wt`, so a + * descriptor with `tile_cols = S/32` and `row_blocks = heads` gives ONE reduce per head + * with no reshape at all — pn_tt_head_norm.h carries it. This runs it. + * 2. **The PRECISION.** The host norms are fp64 (the L2's accumulator is `double` on + * purpose — `pn_qwen35moe_l2_norm()`'s own comment names a token-60-90 divergence onset + * for doing it in float32). The q/k norm feeds the delta rule's recurrent state, so + * whether the card's format is affordable there is the question the 48-token `cmp` will + * otherwise answer expensively. The FIRST run of this probe answered NO for the bf16 + * `reduce` program — 8.599e-03 and 1.283e-02 against a 1e-3 gate — which is what put the + * residency on `reduce_fp32`. + * + * It is a PROBE and not a path: it stages the host's own `conv_out` and `y` into card strips, + * runs the card, and scores. Nothing it does is on the shipped route and nothing downstream + * reads its output — which is exactly why it can run beside a `cmp` that must stay identical. + * + * **THREE numbers, not two.** The q/k arm is one Float32 descriptor and scores one thing. The + * gated arm is TWO descriptors on TWO residencies at TWO formats — a Float32 reduce and then + * a bf16 elementwise gate — so it is scored before and after the crossing. Quoting one number + * for it is what would hide which half costs what. + * + * **`PN_TT_GDN_NORMCHECK=1`**, with `PN_TT_ONCARD=norm,swiglu,residual,topk,gdn` — the + * gated arm needs the ELEMENTWISE residency for its `silu(z) *` half, and that is the + * shipped `pn_tt_silu_mul()`. + * + * @status Layout CONFIRMED 2026-08-19 on a p150a — the row-block-per-head strip is right and + * costs no reshape. On the bf16 `reduce` program the precision answer was NO: q/k L2 + * 8.599e-03 worst head (SSM layer 7), gated 1.283e-02 (layer 16), 146,844 of 196,608 + * q/k elements past 1e-3. + * + * ON `reduce_fp32`, 2026-08-19, 48 SSM layers, run twice with identical figures and + * 0 refusals: q/k L2 **1.737e-03** (layer 19), RMS alone **3.165e-03** (layer 8), + * gated **1.023e-02** (layer 12); 25,718 of 196,608 q/k elements past 1e-3. So the + * Float32 reduce is a real 4.95x on the q/k norm and 5.7x on the element count, and + * NEITHER NORM CLEARS THE 1e-3 GATE. pn_tt_head_norm.h decomposes what is left. + * + * Both red arms fire on the card. `PN_TT_RED=head_norm_bf16_constants` (constants at + * the 2048 page under a 4096 program) drives every figure to ~1.0e+00 with nothing + * refusing anywhere, and `PN_TT_RED=norm_eps` drives them to 8.6e-01/9.99e-01. The + * first leaves `cmp` IDENTICAL, which is the proof that this probe really is off the + * shipped path. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include +#include + +#include "../accel/simd.h" +#include "../accel/tt/pn_tt_fp32_strip.h" +#include "../accel/tt/pn_tt_head_norm.h" + +/** How many SSM layers are scored before the probe goes quiet. Two tokens' worth at + * Ornith's 24, so a per-layer figure is available for a position that carries state and + * for one that does not. */ +#define GDN_NORMCHECK_LAYER_BUDGET 48 + +/* --------------------------------------------------------------------------- + * `PN_TT_RED=head_norm_synthetic` — THE ARM THAT SETTLES CARD-VERSUS-MODEL + * + * The off-hardware model of this exact program at this exact shape + * (foundry `tests/test_vector_ops.c`, "the three walls under the Float32 head norms") + * scores the L2 member 8.406e-04 and the RMS member 1.847e-03. The card scores 1.737e-03 + * and 3.165e-03 — **2.07x and 2.02x above it, the SAME factor on both members**, which is a + * shared term rather than a per-member one. The three named candidates have now been priced + * off-hardware and NONE of them is 2x: the SFPU rsqrt polynomial is 7.2e-08 (four orders + * below the residue), reordering the chain is a null, and the card's 48x32 sample costs the + * RMS member ~1.3x and the L2 member nothing. + * + * What is left unpriced is the DATA. The model runs uniform values in [-1, 1); the card runs + * Ornith's real `conv_out` and `y`. This arm removes that difference in the only way that + * settles it — it feeds the card the model's OWN stream and scores it against the same fp64 + * oracle: + * + * card lands on ~8.4e-04 / ~1.8e-03 -> the 2x is the DATA and the model is sound + * card stays at ~1.7e-03 / ~3.2e-03 -> the 2x is the CARD and the model is missing a + * mechanism that is neither format nor chain + * + * The stream is xorshift64* at seed 0x1220E8, which is `ft_tt_vector_random_value()`. This + * file cannot link foundry's test tree, so it spells the generator itself — and the two + * spellings are held together by the eight words below, which `test_vector_ops.c` pins as + * the same literals. A disagreement is a REFUSAL, not an incomparable number. + * --------------------------------------------------------------------------- */ +/** `pn_tt_red()` spelled here, because a model file does not reach into + * `pn_tt_device_internal.h` (llama_forward_tt.c makes the same argument for its own + * mutations). One `strcmp` against `PN_TT_RED`, exactly as the accel layer's does. */ +static bool normcheck_red(const char *name) +{ + const char *value = getenv("PN_TT_RED"); + + return value && *value && strcmp(value, name) == 0; +} + +static uint64_t g_normcheck_random_state = 0x1220E8ULL; + +static float normcheck_random_value(void) +{ + uint32_t bits; + + g_normcheck_random_state ^= g_normcheck_random_state >> 12; + g_normcheck_random_state ^= g_normcheck_random_state << 25; + g_normcheck_random_state ^= g_normcheck_random_state >> 27; + bits = (uint32_t)((g_normcheck_random_state * 0x2545F4914F6CDD1Dull) >> 40); + return ((float)bits / 8388608.0f) - 1.0f; +} + +/** The generator against the eight words test_vector_ops.c pins. Resets the stream. */ +static bool normcheck_random_matches_the_model(void) +{ + static const uint32_t pinned[8] = { 0xBF508D2Au, 0x3EF4CC04u, 0x3EE46400u, 0xBDF7F450u, + 0xBE494EE8u, 0xBF4FF826u, 0x3E88AB9Cu, 0xBF4CD48Cu }; + unsigned index; + + g_normcheck_random_state = 0x1220E8ULL; + for (index = 0; index < 8u; index++) { + const float value = normcheck_random_value(); + uint32_t bits; + + memcpy(&bits, &value, sizeof(bits)); + if (bits != pinned[index]) + return false; + } + g_normcheck_random_state = 0x1220E8ULL; + return true; +} + +struct pn_qwen35_gdn_normcheck { + /* THE REDUCE'S OWN OPERANDS ARE Float32 — `reduce_fp32` is placed by + * reduce_float32.cbtable and its reader issues 4096-byte reads. */ + pn_tt_fp32_strip qk; /**< q then k, 2*key_dim as one [1, N] strip */ + pn_tt_fp32_strip qk_out; /**< the card's L2 answer. SEPARATE from the input on purpose: + * in-place would be a second question in the same run. */ + pn_tt_fp32_strip y; /**< PHASE DELTA's answer, v_heads*S */ + pn_tt_fp32_strip y_out; /**< the card's RMS answer */ + /* THE GATE'S ARE bf16 — `pn_tt_silu_mul()` runs on the ELEMENTWISE residency, which is + * `eltwise_float16b.cbtable`. So the gated arm crosses fp32 -> bf16 between the two + * descriptors, and scoring the RMS answer BEFORE that crossing is what separates the + * reduce's error from the bridge's. */ + pn_tt_activation z; /**< the attn_gate projection, v_heads*S */ + pn_tt_activation y_bridge; /**< the card's RMS answer, rendered back into a bf16 strip */ + pn_tt_activation gated; /**< silu(z) * rmsnorm(y) */ + pn_tt_norm_constants l2; + pn_tt_norm_constants *rms; /**< one per SSM layer — gamma is `ssm_norm`, per layer */ + bool *rms_ready; + float *host; /**< download scratch, the wider of the two shapes */ + float *want; /**< the oracle's answer for the same input */ + float *synth; /**< `PN_TT_RED=head_norm_synthetic`: the model's own stream */ + bool synthetic; + int ssm_layers; + int scored; + double worst_l2; + double worst_rms; + double worst_gated; + int worst_l2_layer; + int worst_rms_layer; + int worst_gated_layer; + uint64_t l2_over_gate; /**< elements past 1e-3 relative, across every scored layer */ + uint64_t rms_over_gate; + uint64_t gated_over_gate; + uint64_t elements; +}; + +/** + * Max-norm relative error of `got` against `want`, PER HEAD, returning the worst head. + * + * Per head and not per tensor because a per-head fault is invisible in a per-tensor figure + * — PHASE DELTA scored 1.000e+00 for three rounds on exactly that mistake. + */ +static double normcheck_score(const float *want, const float *got, int heads, int width, + uint64_t *out_over_gate) +{ + double worst = 0.0; + int head; + + for (head = 0; head < heads; head++) { + const float *w = want + (size_t)head * width; + const float *g = got + (size_t)head * width; + double max_abs = 0.0; + double max_want = 0.0; + int index; + + for (index = 0; index < width; index++) { + const double diff = fabs((double)g[index] - (double)w[index]); + const double magnitude = fabs((double)w[index]); + + if (diff > max_abs) + max_abs = diff; + if (magnitude > max_want) + max_want = magnitude; + if (magnitude > 0.0 && diff / magnitude > 1e-3) + (*out_over_gate)++; + } + if (max_want > 0.0 && max_abs / max_want > worst) + worst = max_abs / max_want; + } + return worst; +} + +int pn_qwen35_tt_gdn_normcheck_arm(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg) +{ + const uint32_t state = (uint32_t)cfg->ssm_state; + const uint32_t key_tiles = (uint32_t)(2 * cfg->ssm_state * cfg->ssm_n_group) / 32u; + const uint32_t value_tiles = (uint32_t)(cfg->ssm_state * cfg->ssm_dt_rank) / 32u; + struct pn_qwen35_gdn_normcheck *check; + size_t widest; + + if (!g || !cfg || !getenv("PN_TT_GDN_NORMCHECK")) + return 0; + if (pn_tt_device_arm_head_norm(g->device, state) != 0) + return 0; + if (!pn_tt_head_norm_is_armed(g->device, state)) { + fprintf(stderr, "qwen35_tt: PN_TT_GDN_NORMCHECK asked for the per-head norms and the " + "residency did not arm — nothing is scored\n"); + return 0; + } + + check = calloc(1, sizeof(*check)); + if (!check) + return -1; + check->ssm_layers = cfg->n_layer; + check->rms = calloc((size_t)cfg->n_layer, sizeof(*check->rms)); + check->rms_ready = calloc((size_t)cfg->n_layer, sizeof(*check->rms_ready)); + widest = (key_tiles > value_tiles ? key_tiles : value_tiles) * 32u; + check->host = calloc(widest, sizeof(*check->host)); + check->want = calloc(widest, sizeof(*check->want)); + check->synth = calloc(widest, sizeof(*check->synth)); + if (!check->rms || !check->rms_ready || !check->host || !check->want || !check->synth) + goto fail; + if (normcheck_red("head_norm_synthetic")) { + if (!normcheck_random_matches_the_model()) { + fprintf(stderr, "qwen35_tt: PN_TT_RED=head_norm_synthetic — this file's xorshift " + "does NOT reproduce the model's pinned stream, so the two figures " + "would not be comparable. Refusing the arm.\n"); + goto fail; + } + check->synthetic = true; + } + + /* The L2 member takes eps*eps: the oracle clamps the MAGNITUDE at eps and the eps tile + * folds into the SQUARE. foundry ft_tt_op_vector.h carries the derivation. */ + if (pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, NULL, state, + cfg->rms_norm_eps * cfg->rms_norm_eps, + &check->l2) != 0) + goto fail; + if (pn_tt_fp32_strip_alloc(g->device, key_tiles, &check->qk) != 0 || + pn_tt_fp32_strip_alloc(g->device, key_tiles, &check->qk_out) != 0 || + pn_tt_fp32_strip_alloc(g->device, value_tiles, &check->y) != 0 || + pn_tt_fp32_strip_alloc(g->device, value_tiles, &check->y_out) != 0 || + pn_tt_activation_alloc(g->device, value_tiles, &check->z) != 0 || + pn_tt_activation_alloc(g->device, value_tiles, &check->y_bridge) != 0 || + pn_tt_activation_alloc(g->device, value_tiles, &check->gated) != 0) + goto fail; + + g->gdn_normcheck = check; + fprintf(stderr, "qwen35_tt: PN_TT_GDN_NORMCHECK is on — the two per-head norms run on " + "the card beside the host's and are scored per head over the first %d " + "SSM layers. This is a PROBE: nothing downstream reads it.%s\n", + GDN_NORMCHECK_LAYER_BUDGET, + check->synthetic ? " RED: head_norm_synthetic — the card is fed the OFF-HARDWARE " + "MODEL'S OWN uniform stream instead of Ornith's activations, " + "so l2/rms are directly comparable with 8.406e-04 / 1.847e-03; " + "`gated` is NOT scored, its oracle is the real layer's." + : ""); + return 0; + +fail: + free(check->rms); + free(check->rms_ready); + free(check->host); + free(check->want); + free(check->synth); + free(check); + return -1; +} + +void pn_qwen35_tt_gdn_normcheck_release(pn_qwen35_tt *g) +{ + if (!g || !g->gdn_normcheck) + return; + free(g->gdn_normcheck->rms); + free(g->gdn_normcheck->rms_ready); + free(g->gdn_normcheck->host); + free(g->gdn_normcheck->want); + free(g->gdn_normcheck->synth); + free(g->gdn_normcheck); + g->gdn_normcheck = NULL; +} + +/** The q/k arm: ONE descriptor over q|k, which the row-block-per-head layout makes 32 + * independent norms of 16 q-heads followed by 16 k-heads. */ +static int normcheck_l2(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index) +{ + struct pn_qwen35_gdn_normcheck *check = g->gdn_normcheck; + const int state = cfg->ssm_state; + const int k_heads = cfg->ssm_n_group; + const uint32_t count = (uint32_t)(2 * state * k_heads); + const float *source = g->gdn_conv_out; + double worst; + int head; + + if (check->synthetic) { + uint32_t element; + + for (element = 0; element < count; element++) + check->synth[element] = normcheck_random_value(); + source = check->synth; + } + if (pn_tt_fp32_strip_upload(g->device, &check->qk, source, count) != 0 || + pn_tt_head_norm(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &check->l2, + (uint32_t)(2 * k_heads), &check->qk, &check->qk_out) != 0 || + pn_tt_fp32_strip_download(g->device, &check->qk_out, check->host, count) != 0) + return -1; + + if (check->synthetic) { + /* The same oracle over the synthetic stream — `pn_qwen35moe_l2_norm()` in place, + * which is what `gdn_split_and_normalise()` calls on the real one. */ + memcpy(check->want, source, (size_t)count * sizeof(float)); + for (head = 0; head < 2 * k_heads; head++) + pn_qwen35moe_l2_norm(check->want + (size_t)head * state, state, + cfg->rms_norm_eps); + } else { + /* The oracle's answer is already computed: `gdn_split_and_normalise()` wrote it. */ + memcpy(check->want, g->gdn_qn, (size_t)state * k_heads * sizeof(float)); + memcpy(check->want + (size_t)state * k_heads, g->gdn_kn, + (size_t)state * k_heads * sizeof(float)); + } + worst = normcheck_score(check->want, check->host, 2 * k_heads, state, + &check->l2_over_gate); + if (worst > check->worst_l2) { + check->worst_l2 = worst; + check->worst_l2_layer = ssm_index; + } + check->elements += (uint64_t)count; + return 0; +} + +/** + * The gated arm, SCORED TWICE, because it is two descriptors on two residencies at two + * formats and one number cannot say which of them costs what. + * + * `worst_rms` the Float32 reduce's own answer against `pn_vrms_norm_f32()`, read back + * straight off the fp32 output strip. This is the number the Float32 + * program moves. + * `worst_gated` the whole of `pn_qwen35moe_gated_norm()`: that answer rendered into a + * **bf16** strip, multiplied by `silu(z)` on the bf16 ELEMENTWISE + * residency, and read back. The extra term is the fp32 -> bf16 BRIDGE + * between the two families, and it is a different item's to remove. + */ +static int normcheck_gated(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, const float *z, + const float *gated) +{ + struct pn_qwen35_gdn_normcheck *check = g->gdn_normcheck; + const int state = cfg->ssm_state; + const int v_heads = cfg->ssm_dt_rank; + const uint32_t count = (uint32_t)(state * v_heads); + const float *source = g->gdn_y; + double worst; + int head; + + if (check->synthetic) { + uint32_t element; + + for (element = 0; element < count; element++) + check->synth[element] = normcheck_random_value(); + source = check->synth; + } + if (!check->rms_ready[ssm_index]) { + if (pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, + l->ssm_norm, (uint32_t)state, cfg->rms_norm_eps, + &check->rms[ssm_index]) != 0) + return -1; + check->rms_ready[ssm_index] = true; + } + if (pn_tt_fp32_strip_upload(g->device, &check->y, source, count) != 0 || + pn_tt_head_norm(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &check->rms[ssm_index], + (uint32_t)v_heads, &check->y, &check->y_out) != 0 || + pn_tt_fp32_strip_download(g->device, &check->y_out, check->host, count) != 0) + return -1; + + /* The reduce alone. `pn_qwen35moe_gated_norm()`'s own first half, per head. */ + for (head = 0; head < v_heads; head++) + pn_vrms_norm_f32(source + (size_t)head * state, l->ssm_norm, + check->want + (size_t)head * state, state, cfg->rms_norm_eps); + worst = normcheck_score(check->want, check->host, v_heads, state, &check->rms_over_gate); + if (worst > check->worst_rms) { + check->worst_rms = worst; + check->worst_rms_layer = ssm_index; + } + + /* The gate's oracle is the REAL layer's `gated`, which a synthetic `y` does not produce. + * Scoring it under this arm would be a number against the wrong answer, so it is skipped + * by name rather than reported as a large one. */ + if (check->synthetic) + return 0; + + /* Then the bridge and the gate. */ + if (pn_tt_activation_upload(g->device, &check->y_bridge, check->host, count) != 0 || + pn_tt_activation_upload(g->device, &check->z, z, count) != 0 || + pn_tt_silu_mul(g->device, &check->z, &check->y_bridge, &check->gated) != 0 || + pn_tt_activation_download(g->device, &check->gated, check->host, count) != 0) + return -1; + + worst = normcheck_score(gated, check->host, v_heads, state, &check->gated_over_gate); + if (worst > check->worst_gated) { + check->worst_gated = worst; + check->worst_gated_layer = ssm_index; + } + return 0; +} + +int pn_qwen35_tt_gdn_normcheck_layer(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const float *z, const float *gated) +{ + struct pn_qwen35_gdn_normcheck *check; + + if (!g || !g->gdn_normcheck || !l || !cfg || !z || !gated) + return 0; + check = g->gdn_normcheck; + if (check->scored >= GDN_NORMCHECK_LAYER_BUDGET || ssm_index >= check->ssm_layers) + return 0; + if (normcheck_l2(g, cfg, ssm_index) != 0 || + normcheck_gated(g, l, cfg, ssm_index, z, gated) != 0) { + fprintf(stderr, "qwen35_tt: the per-head norm probe failed at SSM layer %d — no " + "score from here on\n", ssm_index); + check->scored = GDN_NORMCHECK_LAYER_BUDGET; + return -1; + } + fprintf(stderr, "qwen35_tt: [normcheck] ssm %2d l2 %.3e rms %.3e gated %.3e\n", + ssm_index, check->worst_l2, check->worst_rms, check->worst_gated); + check->scored++; + return 0; +} + +void pn_qwen35_tt_gdn_normcheck_report(const pn_qwen35_tt *g) +{ + const struct pn_qwen35_gdn_normcheck *check; + + if (!g || !g->gdn_normcheck) + return; + check = g->gdn_normcheck; + fprintf(stderr, + "qwen35_tt: per-head norms ON CARD vs the oracle over %d SSM layers — " + "q/k L2 worst %.3e (layer %d), RMS worst %.3e (layer %d), gated worst %.3e " + "(layer %d); past 1e-3 relative: %llu of %llu q/k, %llu RMS, %llu gated. The RMS " + "figure is the Float32 reduce alone; gated adds the fp32->bf16 bridge into the " + "bf16 elementwise gate.\n", + check->scored, check->worst_l2, check->worst_l2_layer, check->worst_rms, + check->worst_rms_layer, check->worst_gated, check->worst_gated_layer, + (unsigned long long)check->l2_over_gate, (unsigned long long)check->elements, + (unsigned long long)check->rms_over_gate, + (unsigned long long)check->gated_over_gate); +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_gdn_parity_tt.c b/src/models/qwen35moe_gdn_parity_tt.c new file mode 100644 index 0000000..3181f14 --- /dev/null +++ b/src/models/qwen35moe_gdn_parity_tt.c @@ -0,0 +1,382 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_gdn_parity_tt.c + * @brief The ON-CARD GATE for the Gated-Delta-Net arm: a SHADOW host recurrence run beside + * the card's, on the same inputs, from the same zero initial condition, so a card + * answer can be priced against the oracle's every token of every SSM layer. + * + * --------------------------------------------------------------------------- + * WHAT IT COMPARES, AND WHY THOSE TWO + * --------------------------------------------------------------------------- + * Two numbers a layer, and they SPLIT THE OP: + * + * conv `g->gdn_conv_out` against `pn_qwen35moe_conv1d_silu()` — PHASE CONV alone, + * including the rolling window, so a wrong tap plane or a wrong live row shows + * here and nowhere else. + * gated `g->h_gated` against the whole of `pn_qwen35moe_ssm_recurrence()` — the block's + * actual output. With `conv` clean, a `gated` miss is PHASE DELTA and the two + * host norms around it, and the norms are the oracle's own code on both arms. + * + * Both call the ORACLE'S OWN functions. A parity arm that reimplemented the conv1d would be + * comparing the card against a third answer, which measures nothing. + * + * --------------------------------------------------------------------------- + * THE SHADOW STATE IS THE HOST ARM'S OWN, AND IT EVOLVES INDEPENDENTLY + * --------------------------------------------------------------------------- + * `pn_qwen35moe_state_new()` allocates a zeroed `pn_qwen35moe_ssm_state` per SSM layer + * whatever arm runs, and the on-card arm leaves it untouched. So the shadow costs no + * allocation and starts where the card's DRAM state starts. It is NEVER resynced from the + * card: the two recurrences run side by side for the whole sequence, which is the only way a + * carried-state error is visible at all. A single-token compare passes on a recurrence that + * is right once and wrong on the second token — the reason the acceptance is 48 tokens. + * + * **The consequence is that a per-token number is drift, not op error.** Position 0 is the + * one place both states are zero, so the position-0 figures are the op's own error with no + * compounding, and the trajectory after it is how that error propagates. Both are reported. + * + * --------------------------------------------------------------------------- + * THE METRIC IS MAX-NORM RELATIVE, NOT PER-ELEMENT RELATIVE + * --------------------------------------------------------------------------- + * `max|card - host| / max|host|` over the tensor. Per-element relative error is unusable + * here: a delta-rule output has elements many orders below its own scale and dividing by one + * of those reports 10^6 for a difference that is numerical noise. Max-norm relative is + * scale-invariant, has no arbitrary floor, and is what "1e-3 relative" means for a tensor. + * + * **THE BAR IS 1e-3 AND IT IS THE CUDA ARM'S, NOT THE CPU'S.** The oracle accumulates its + * dots in fp64 deliberately; a Tensix cannot, so bit-equality was never available. That was + * decided in `foundry/tt/tests/Makefile.gdn` before this file existed. + * + * @status Ran the first on-card GDN gate, 2026-08-18. It found two silent defects (a shared + * NoC coordinate word and a core collision with the matmul group) and then localised the + * remainder: at position 0 layer 0 `conv` is 6.872e-04 and `gated` is 1.000e+00, so PHASE + * CONV is inside the bar and PHASE DELTA is not. + * @issues The per-token figures after position 0 are DRIFT, not op error — the two states + * evolve independently and are never resynced, which is deliberate (it is the only way a + * carried-state error is visible at all) but means only the position-0 column is a + * statement about one descriptor. + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include +#include +#include + +#include "../memory/arena.h" + +/** The gate. Fixed, not an environment variable: a tolerance a run can widen is not a gate. */ +#define PN_GDN_PARITY_TOLERANCE 1.0e-3f + +/** `PN_TT_GDN_PARITY=1` arms it. Off by default — it runs a second full host recurrence per + * SSM layer, which is the cost the on-card arm exists to remove. */ +#define PN_GDN_PARITY_ENV "PN_TT_GDN_PARITY" + +/** One tensor's worst disagreement, and where it was. */ +typedef struct { + float worst; /**< max-norm relative error */ + int worst_pos; + int worst_layer; + float at_zero; /**< the same figure at position 0, where neither state has drifted */ +} pn_gdn_parity_track; + +struct pn_qwen35_gdn_parity { + float *host_conv_out; /**< conv_dim */ + float *host_gated; /**< ssm_state * ssm_dt_rank */ + float *saved_window; /**< (ssm_conv-1) * conv_dim — the shadow window across the probe */ + size_t window_floats; + pn_gdn_parity_track conv; + pn_gdn_parity_track gated; + float token_conv_this_layer; /**< the layer being compared right now, for the pos-0 trace */ + float token_conv; /**< worst of either tensor within the token being built */ + float token_gated; + int token_conv_layer; + int token_gated_layer; + int tokens; + int layers_compared; +}; + +/* ================================================================ + * The metric + * ================================================================ */ + +/** + * `max|card - host| / max|host|` over `count` elements. + * + * @param worst_index written with the element that produced it, or -1 if the reference is + * identically zero (in which case the answer is 0 or +INFINITY, and the + * caller wants to know which element) + * @return the max-norm relative error; 0 when both tensors are identically zero + */ +static float gdn_parity_relative(const float *card, const float *host, size_t count, + size_t *worst_index) +{ + float worst_diff = 0.0f; + float reference = 0.0f; + size_t index; + size_t at = 0; + + for (index = 0; index < count; index++) { + const float diff = fabsf(card[index] - host[index]); + const float magnitude = fabsf(host[index]); + + if (diff > worst_diff) { + worst_diff = diff; + at = index; + } + if (magnitude > reference) + reference = magnitude; + } + *worst_index = at; + if (reference <= FLT_MIN) + return worst_diff > 0.0f ? INFINITY : 0.0f; + return worst_diff / reference; +} + +/** + * WHERE the disagreeing channels are, not just how big the worst one is. + * + * A tilized one-row strip has two coordinates a bug can follow — the TILE (`c / 32`) and the + * COLUMN INSIDE it (`c % 32`) — and Tf32 rounding follows neither. Printing the histogram of + * `c % 32` separates "a few channels round differently" from "one column of every tile is + * wrong", which are a tolerance question and a layout bug respectively. + */ +static void gdn_parity_locate(const float *card, const float *host, size_t count) +{ + unsigned per_column[32] = { 0 }; + size_t first[6]; + size_t found = 0; + size_t index; + size_t bad = 0; + + for (index = 0; index < count; index++) { + const float magnitude = fabsf(host[index]); + + if (fabsf(card[index] - host[index]) <= 1.0e-3f * magnitude + 1.0e-6f) + continue; + bad++; + per_column[index % 32u]++; + if (found < 6u) + first[found++] = index; + } + fprintf(stderr, "gdn-parity: %zu of %zu channels off by more than 1e-3 relative\n", bad, + count); + fprintf(stderr, "gdn-parity: by column %% 32:"); + for (index = 0; index < 32u; index++) + fprintf(stderr, " %u", per_column[index]); + fprintf(stderr, "\n"); + fprintf(stderr, "gdn-parity: first bad channels:"); + for (index = 0; index < found; index++) + fprintf(stderr, " %zu(tile %zu col %zu)", first[index], first[index] / 32u, + first[index] % 32u); + fprintf(stderr, "\n"); +} + +/** Fold one comparison into a tensor's running worst. */ +static void gdn_parity_record(pn_gdn_parity_track *track, float relative, int position, + int layer) +{ + if (relative > track->worst) { + track->worst = relative; + track->worst_pos = position; + track->worst_layer = layer; + } + if (position == 0 && relative > track->at_zero) + track->at_zero = relative; +} + +/* ================================================================ + * Lifecycle + * ================================================================ */ + +int pn_qwen35_tt_gdn_parity_arm(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg) +{ + const char *requested = getenv(PN_GDN_PARITY_ENV); + struct pn_qwen35_gdn_parity *parity; + + if (!g || !cfg) + return -1; + if (!g->gdn_on_card || !requested || !*requested || strcmp(requested, "0") == 0) + return 0; + + parity = calloc(1, sizeof(*parity)); + if (!parity) + return -1; + parity->window_floats = (size_t)(cfg->ssm_conv - 1) * (size_t)cfg->conv_dim; + parity->host_conv_out = calloc((size_t)cfg->conv_dim, sizeof(float)); + parity->host_gated = + calloc((size_t)cfg->ssm_state * (size_t)cfg->ssm_dt_rank, sizeof(float)); + parity->saved_window = calloc(parity->window_floats, sizeof(float)); + if (!parity->host_conv_out || !parity->host_gated || !parity->saved_window) { + free(parity->host_conv_out); + free(parity->host_gated); + free(parity->saved_window); + free(parity); + return -1; + } + g->gdn_parity = parity; + fprintf(stderr, "qwen35_tt: gdn PARITY armed — a shadow host recurrence runs beside every " + "SSM layer and the gate is %.1e max-norm relative. This roughly TRIPLES " + "the token; it is a gate, not a measurement arm.\n", + (double)PN_GDN_PARITY_TOLERANCE); + return 0; +} + +void pn_qwen35_tt_gdn_parity_release(pn_qwen35_tt *g) +{ + if (!g || !g->gdn_parity) + return; + free(g->gdn_parity->host_conv_out); + free(g->gdn_parity->host_gated); + free(g->gdn_parity->saved_window); + free(g->gdn_parity); + g->gdn_parity = NULL; +} + +/* ================================================================ + * The comparison + * ================================================================ */ + +int pn_qwen35_tt_gdn_parity_layer(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int layer, + const float *qkv, const float *z, const float *beta_raw, + const float *alpha, pn_qwen35moe_ssm_state *state, + pn_arena *arena, struct fw_pool *pool) +{ + struct pn_qwen35_gdn_parity *parity = g ? g->gdn_parity : NULL; + const size_t conv_count = (size_t)cfg->conv_dim; + const size_t gated_count = (size_t)cfg->ssm_state * (size_t)cfg->ssm_dt_rank; + size_t where; + float relative; + + if (!parity) + return 0; + if (!l || !qkv || !z || !beta_raw || !alpha || !state || !arena) + return -1; + + /* PHASE CONV alone. The oracle's conv1d ROLLS the window, and the whole recurrence below + * has to roll it exactly once for this token, so the window is saved and put back. */ + memcpy(parity->saved_window, state->conv_state, parity->window_floats * sizeof(float)); + pn_qwen35moe_conv1d_silu(l, cfg, qkv, state, parity->host_conv_out); + memcpy(state->conv_state, parity->saved_window, parity->window_floats * sizeof(float)); + + relative = gdn_parity_relative(g->gdn_conv_out, parity->host_conv_out, conv_count, &where); + parity->token_conv_this_layer = relative; + if (relative > parity->token_conv) { + parity->token_conv = relative; + parity->token_conv_layer = layer; + } + /* The first layer of the first token, once, AS BIT PATTERNS. At position 0 the window is + * zero, so the card's answer is SiLU(qkv * w[.][taps-1]) and nothing else — the simplest + * arithmetic this op ever does. A ratio cannot say which arm is wrong and a decimal + * cannot say HOW: a swapped half-word, a shifted exponent and a wrong tile all read as + * "garbage" in %g and are three different bugs in %08x. */ + if (parity->tokens == 0 && parity->layers_compared == 0) { + union { float f; uint32_t u; } card, host; + size_t index; + + for (index = 0; index < 8u; index++) { + card.f = g->gdn_conv_out[index]; + host.f = parity->host_conv_out[index]; + fprintf(stderr, "gdn-parity: conv_out[%zu] card %08x (%g) host %08x (%g) " + "qkv %g w[.][taps-1] %g\n", + index, card.u, (double)card.f, host.u, (double)host.f, + (double)qkv[index], + (double)l->ssm_conv1d[index * (size_t)cfg->ssm_conv + + (size_t)(cfg->ssm_conv - 1)]); + } + card.f = g->gdn_conv_out[where]; + host.f = parity->host_conv_out[where]; + fprintf(stderr, "gdn-parity: worst element %zu card %08x (%g) host %08x (%g)\n", + where, card.u, (double)card.f, host.u, (double)host.f); + gdn_parity_locate(g->gdn_conv_out, parity->host_conv_out, conv_count); + } + + /* The whole block, on the shadow state, from the same four projections. The arena was + * reset at the top of this layer and the on-card arm allocates nothing from it. */ + pn_arena_reset(arena); + if (pn_qwen35moe_ssm_recurrence(l, cfg, qkv, z, beta_raw, alpha, state, arena, + parity->host_gated, pool) < 0) + return -1; + + relative = gdn_parity_relative(g->h_gated, parity->host_gated, gated_count, &where); + /* THE FIRST TOKEN, LAYER BY LAYER. A per-token maximum says how bad the block is; it + * cannot say WHERE it starts, and at position 0 every carried state is zero, so a layer's + * error is that layer's own plus whatever the residual stream carried into it. The layer + * at which this stops being small is the layer the search belongs in. + * + * POSITION 1 IS PRINTED TOO, and it is a different statement: `conv` cannot drift, because + * BOTH arms are handed the SAME `qkv` at every layer and the only other input is the + * rolling window, which both roll from that same qkv. So a `conv` figure that is 7e-4 at + * position 0 and 2.3 at position 1 is not drift — it is the ROLL. */ + if (parity->tokens < 2) { + /* `gated` landing on exactly 1.000 means `max|card - host| == max|host|`, which for a + * ratio is the signature of a card answer that is identically ZERO — and the block's + * output is zero exactly when PHASE DELTA's `y` is. So the scale of `y` itself is + * printed beside the ratio; a ratio cannot distinguish "wrong" from "absent". */ + float y_scale = 0.0f; + size_t index; + + for (index = 0; index < gated_count; index++) + if (fabsf(g->gdn_y[index]) > y_scale) + y_scale = fabsf(g->gdn_y[index]); + fprintf(stderr, "gdn-parity: pos %d layer %2d conv %.3e gated %.3e max|card y| %.4g " + "(y[0..3] %g %g %g %g)\n", + parity->tokens, layer, (double)parity->token_conv_this_layer, (double)relative, + (double)y_scale, (double)g->gdn_y[0], (double)g->gdn_y[1], + (double)g->gdn_y[2], (double)g->gdn_y[3]); + } + if (relative > parity->token_gated) { + parity->token_gated = relative; + parity->token_gated_layer = layer; + } + parity->layers_compared++; + return 0; +} + +void pn_qwen35_tt_gdn_parity_token(pn_qwen35_tt *g, int position) +{ + struct pn_qwen35_gdn_parity *parity = g ? g->gdn_parity : NULL; + + if (!parity) + return; + gdn_parity_record(&parity->conv, parity->token_conv, position, parity->token_conv_layer); + gdn_parity_record(&parity->gated, parity->token_gated, position, + parity->token_gated_layer); + fprintf(stderr, "gdn-parity: pos=%-4d conv %.3e (layer %2d) gated %.3e (layer %2d)\n", + position, (double)parity->token_conv, parity->token_conv_layer, + (double)parity->token_gated, parity->token_gated_layer); + parity->token_conv = 0.0f; + parity->token_gated = 0.0f; + parity->token_conv_layer = -1; + parity->token_gated_layer = -1; + parity->tokens++; +} + +void pn_qwen35_tt_gdn_parity_report(const pn_qwen35_tt *g) +{ + const struct pn_qwen35_gdn_parity *parity = g ? g->gdn_parity : NULL; + bool passed; + + if (!parity) + return; + passed = parity->conv.worst <= PN_GDN_PARITY_TOLERANCE && + parity->gated.worst <= PN_GDN_PARITY_TOLERANCE; + fprintf(stderr, + "gdn-parity: %d tokens, %d layer comparisons\n" + "gdn-parity: conv worst %.3e at pos %d layer %d | pos-0 %.3e\n" + "gdn-parity: gated worst %.3e at pos %d layer %d | pos-0 %.3e\n" + "gdn-parity: GATE %.1e max-norm relative — %s\n", + parity->tokens, parity->layers_compared, (double)parity->conv.worst, + parity->conv.worst_pos, parity->conv.worst_layer, (double)parity->conv.at_zero, + (double)parity->gated.worst, parity->gated.worst_pos, parity->gated.worst_layer, + (double)parity->gated.at_zero, (double)PN_GDN_PARITY_TOLERANCE, + passed ? "PASS" : "FAIL"); +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_gdn_strip_tt.c b/src/models/qwen35moe_gdn_strip_tt.c new file mode 100644 index 0000000..37aa212 --- /dev/null +++ b/src/models/qwen35moe_gdn_strip_tt.c @@ -0,0 +1,1398 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_gdn_strip_tt.c + * @brief THE STRIP BRIDGE: the whole Gated-Delta-Net block on the card, with no activation + * crossing PCIe. This is the file that removes 78% of a decode token's readback. + * + * --------------------------------------------------------------------------- + * THE NUMBER THIS FILE EXISTS FOR + * --------------------------------------------------------------------------- + * A decode token was 0.390 s of HOST against 0.050 s of card, and 0.29 s of the host half was + * TRANSFER. The Gated-Delta-Net block owned **49,408 B a layer over 24 SSM layers** of it: + * `ssm_qkv` and `ssm_z` came down, the recurrence ran, `gated` went back up. `qwen35moe_gdn_tt.c` + * moved the two heavy PHASES onto the card and made the transfer WORSE, because the per-head + * L2 norm sits between them and forced `conv_out` and `y` across as well — 1.32 tok/s armed + * against 1.95 unarmed, `cmp`-identical and correct, and a wall regression for one reason + * only. + * + * Nothing about that seam was arithmetic. It was a FORMAT and a LAYOUT: + * + * FORMAT every projection writes a bf16 strip; the GDN op and the per-head reduces are + * Float32 programs, because the recurrence carries state across tokens. Fixed by + * `pn_tt_render.h` — two resident cores that convert a strip in card DRAM. + * LAYOUT PHASE DELTA used to take three tilized `[heads, S]` matrices, one row per head, + * which is not the geometry a projection or a reduce produces. It takes HEAD-MAJOR + * ROW-0 STRIPS now, which is exactly PHASE CONV's output geometry — so the delta + * rule reads the convolution's answer IN PLACE and the L2 norm between them writes + * back over its own input. + * + * With both, the block is six descriptors and two 256-byte host transfers a layer: + * + * widen(ssm_qkv) bf16 strip -> the GDN stream buffer, fp32 + * PHASE CONV -> conv_out, which IS the delta input + * L2 head norm, IN PLACE, over its first `2*k_heads*state_tiles` tiles + * PHASE DELTA -> y, an fp32 row-0 strip + * RMS head norm -> y_norm, fp32 + * narrow(y_norm) fp32 -> a bf16 strip + * silu(z) * that, on the shipped elementwise residency -> `ssm_gated` + * + * `alpha` and `beta` still come DOWN (two tiles, 256 B) and the gate block goes UP (256 B), + * because `beta = sigmoid(.)` and `decay = exp(ssm_a * softplus(.))` need a `pow` and so a + * `log`, and the SFPU has neither. That is the whole of the block's link traffic: + * **readback 1478.09 -> 326.09 KiB/token.** + * + * --------------------------------------------------------------------------- + * WHY THE L2 NORM IN PLACE IS A DECISION AND NOT A HOPE + * --------------------------------------------------------------------------- + * Its input and its output are the same 256-tile buffer: `conv_out`'s q and k slices, normed + * back over themselves, with v left alone beside them. That is safe by PIPELINE ORDER — the + * reduce reader prefetches ONE row block, so it has read block `b` before the writer can have + * written block `b` — and the alternative is a whole extra fp32 copy descriptor a layer to + * move `v` into a second buffer. + * + * `PN_TT_GDN_STRIP_CHECK=1` is the arm that makes that a measurement rather than a claim: it + * reads the conv output's q|k PREIMAGE off the card, runs the descriptor, reads the ANSWER + * back, and scores it per head against `pn_qwen35moe_l2_norm()` on the preimage. A reduce + * racing its own input cannot pass that — it would be normalising values it had already + * overwritten. It also checks that `v`, which this descriptor must not touch, is byte for + * byte what PHASE CONV left there. + * + * @status SHIPPED ON by default, MEASURED ON SILICON (p150a, 2026-08-21), against a control + * re-taken on the same binary and the same blob with `PN_TT_PROGRAM_DIR` unset: + * + * arm readback KiB/token wall tok/s cmp vs the 48-token oracle + * control 1478.09 2.20 IDENTICAL, 189 B, md5 80fc6259 + * gdn, no bridge 2630.09 1.43 IDENTICAL + * THE BRIDGE 326.09 3.79 IDENTICAL + * + * **4.53x the readback and 1.72x the wall, with the output BYTE-IDENTICAL** — so the + * narrowing render and both per-head norms moving on-card cost nothing the 48-token + * completion can see, and no parity ratio is needed to justify the default. + * Decomposed: readback 0.287 -> 0.065 s/token, wall 0.4543 -> 0.2639, and the bridge's + * own card time is ~0.022 s/token over 48 render descriptors — 22 ms spent to remove + * 222 ms. 0 refusals, 0 restarts, company=0 on every arm. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include +#include + +#include "../accel/tt/pn_tt_fp32_strip.h" +#include "../core/bench.h" +#include "../accel/tt/pn_tt_eltwise_group.h" +#include "../accel/tt/pn_tt_gate.h" +#include "../accel/tt/pn_tt_gdn.h" +#include "../accel/tt/pn_tt_gdnc.h" +#include "../accel/tt/pn_tt_gdngate.h" +#include "tt/kernels/resident_gdngate_protocol.h" +#include "tt/kernels/ft_det_math.h" +#include "../accel/tt/pn_tt_head_norm.h" +#include "../accel/tt/pn_tt_render.h" + +/** + * ON BY DEFAULT; `PN_TT_GDN_STRIP=0` turns it off. + * + * It flipped on the two numbers this file exists for, measured on a p150a against a control + * re-taken on the same binary and the same blob: readback **1478.09 -> 326.09 KiB/token** and + * wall **2.20 -> 3.79 tok/s**, with the 48-token completion `cmp` IDENTICAL to the oracle — + * 189 B, md5 80fc6259 — so the output gate is satisfied by BYTE IDENTITY rather than by a + * parity ratio. `0` reproduces the host-mediated arm at 1.43 tok/s, which is what makes the + * gain attributable. + */ +#define PN_QWEN35_TT_GDN_STRIP_ENV "PN_TT_GDN_STRIP" + +/** The in-place proof below. Budgeted, off by default, nothing downstream reads it. */ +#define PN_QWEN35_TT_GDN_STRIP_CHECK_ENV "PN_TT_GDN_STRIP_CHECK" +#define PN_QWEN35_TT_GDN_STRIP_CHECK_BUDGET 24 + +/** + * WHICH OF THE TWO CHUNK KERNELS RUNS. `both` is the arm as it was measured and the default. + * + * A CPU oracle scored the chunkwise prefill on 2026-08-24 and it FAILED: the mean is unbiased + * (t = +0.433) and the SPREAD doubles, `dnll_sd` 0.1943 -> 0.3776. `PN_TT_PREFILL=1` alone — + * batched projections, batched attention, both GDN mixers still the decode family — scores + * BIT-IDENTICALLY to the per-token path, so all of that variance belongs to PHASE CONV, to + * PHASE DELTA, or to their interaction. Nothing else could separate the two: they share the + * conv window, the recurrent state and all three buffers, and `PN_TT_PREFILL_GDNC` moves them + * together. + * + * both the arm as it stands, both mixers chunkwise + * conv PHASE CONV chunkwise, PHASE DELTA per position over the chunk's rows + * delta PHASE CONV per position, PHASE DELTA chunkwise + * none BOTH per position, through the SAME row shuttle — **the control**, and the only + * thing that makes the other two readable. It must score bit-identically to the + * sequential arm; if it does not, the shuttle is the divergence and no attribution + * it produces means anything. + * + * The three non-default modes are SLOW — a row of the stream crosses the link per position + * per layer, which is the traffic the chunk exists to remove. They are a quality instrument. + */ +#define PN_QWEN35_TT_GDNC_PHASE_ENV "PN_TT_PREFILL_GDNC_PHASE" + +typedef enum { + PN_GDNC_PHASE_BOTH = 0, + PN_GDNC_PHASE_CONV, + PN_GDNC_PHASE_DELTA, + PN_GDNC_PHASE_NONE +} pn_gdnc_phase; + +/** + * THE TWO MIXERS' CHUNK LENGTH, INSIDE A 32-ROW PASS. 0 (the default) is the whole pass. + * + * `PN_TT_PREFILL_ROWS` cannot answer the chunk-length question. It shortens the PASS, which + * moves the attention chunk with it, and — measured 2026-08-24 — it WEDGES core 90: with the + * SSM chunked the decode `gdn` family goes unused for a whole prefill, its first descriptor + * after it is refused, and TRISC0 spins out its bound. The boot guard refuses rather than + * wedging worse and rung 3 clears it, but the arm produces no dump. At the default 32 the + * same arm runs clean. + * + * So the sub-chunk runs inside a 32-row pass: the pass, the attention chunk, the projections + * and the decode family all stay exactly where the passing `both` arm leaves them, and the + * ONLY variable is C. It uses the same row shuttle, which the `none` control proved + * byte-exact — and `PN_TT_PREFILL_GDNC_ROWS=32` reproduces the `both` arm, which is this + * knob's own control. + */ +#define PN_QWEN35_TT_GDNC_ROWS_ENV "PN_TT_PREFILL_GDNC_ROWS" + +/** 0 when unset or out of range; said out loud once when it is not. */ +static int gdnc_sub_rows(void) +{ + static int rows = -1; + const char *env; + + if (rows >= 0) + return rows; + env = getenv(PN_QWEN35_TT_GDNC_ROWS_ENV); + rows = env && *env ? atoi(env) : 0; + if (rows < 0 || rows > 32) + rows = 0; + if (rows != 0) + fprintf(stderr, "qwen35_tt: gdnc SUB-CHUNK ARMED — the two GDN mixers run at C = %d " + "inside a 32-row pass, through the row shuttle. An attribution " + "instrument, and SLOW.\n", rows); + return rows; +} + +/** + * PHASE DELTA'S CHUNK LENGTH, IN PLACE. **16 by default — this is the shipped rung.** + * + * --------------------------------------------------------------------------- + * WHAT IT IS, AND WHY IT IS NOT `PN_TT_PREFILL_GDNC_ROWS` + * --------------------------------------------------------------------------- + * `PN_TT_PREFILL_GDNC_ROWS` above answers the chunk-length QUESTION through a row shuttle: a + * row of the stream crosses the link per position per layer, which is the traffic the chunk + * exists to remove. It is an instrument and it is slow. This knob is the ANSWER: PHASE DELTA + * runs `ceil(rows/C)` descriptors over the SAME 32-row strip, each owning rows + * `[first_row, first_row + C)` IN PLACE, and nothing crosses the link at all. + * + * --------------------------------------------------------------------------- + * WHY 16, MEASURED + * --------------------------------------------------------------------------- + * PHASE DELTA owns 100% of the chunkwise arm's quality variance and the chunk length is the + * whole of it. `A = I + A_off` is WELL CONDITIONED (`max|A^-1| = 1.0`, `cond ~ 30`) but + * `A_off`'s powers are not monotone: at C = 32, `max|A_off^8| = 3.6e+04` and the Newton + * iterates reach 1.7e+06 before collapsing to ~1, so the answer is a cancellation of + * 1e5-sized intermediates and a 4.9e-4 Tf32 unpack truncation leaves a residue it cannot + * remove. At C = 16 the intermediates never get large enough for that to bite. The ladder, + * scored against one cached oracle at 339 positions: C = 4 ratio 0.993285 / sd 0.1747; + * C = 8 1.000834 / 0.1705; **C = 16 0.995699 / t -0.477 / 4.13% / KL 0.00460 / sd 0.1664 / + * worst-doc 1.0289 — 6/6 and the best figures of the whole ladder**; C = 32 FAILS + * worst-document. + * + * --------------------------------------------------------------------------- + * PHASE CONV STAYS AT THE WHOLE PASS, AND THAT IS NOT A COMPROMISE + * --------------------------------------------------------------------------- + * The convolution is a fixed `taps` FIR, not a length-sensitive recurrence: output row t reads + * input rows `t-(taps-1)..t`, which for `t >= taps-1` are inside the strip whatever the + * chunking, and the band tiles already select them. A sub-chunked CONV and a whole-pass CONV + * accumulate the same `2*taps` terms in the same tap order, differing only in which of each + * pair carries the value and which carries an exact zero. So CONV runs ONCE — one descriptor + * instead of two — and the measured `chunkwise CONV alone PASSES 6/6 and beats what ships` is + * the arm that says the whole-pass CONV is fine. The L2 norm between them runs once for the + * same reason: a ROW reduce reduces ALONG the row, so every position is normalised + * independently and 32 rows at once is 32 rows one at a time. + * + * `0` or anything `>= rows` is the whole pass, which is the C = 32 arm and this knob's own + * control: it must be BYTE-IDENTICAL to the dump the sub-chunk path did not exist for. + * Anything not a multiple of 16 is refused, because `first_row` must land on a tile FACE + * boundary for the writer's `y` write to stay a contiguous suffix. + */ +#define PN_QWEN35_TT_DELTA_ROWS_ENV "PN_TT_PREFILL_DELTA_ROWS" +#define PN_QWEN35_TT_DELTA_ROWS_DEFAULT 16 + +/** Read once, and said out loud whenever it is not the default. */ +static int delta_sub_rows(void) +{ + static int rows = -1; + const char *env; + + if (rows >= 0) + return rows; + env = getenv(PN_QWEN35_TT_DELTA_ROWS_ENV); + rows = (env && *env) ? atoi(env) : PN_QWEN35_TT_DELTA_ROWS_DEFAULT; + if (rows < 0 || rows > 32 || (rows % 16) != 0) { + fprintf(stderr, "qwen35_tt: %s=%s is not 0, 16 or 32 — `first_row` must land on a " + "tile face boundary. Staying at %d.\n", + PN_QWEN35_TT_DELTA_ROWS_ENV, env ? env : "", PN_QWEN35_TT_DELTA_ROWS_DEFAULT); + rows = PN_QWEN35_TT_DELTA_ROWS_DEFAULT; + } + if (rows != PN_QWEN35_TT_DELTA_ROWS_DEFAULT) + fprintf(stderr, "qwen35_tt: PHASE DELTA chunk length is C = %d, not the shipped %d\n", + rows, PN_QWEN35_TT_DELTA_ROWS_DEFAULT); + return rows; +} + +/** `PN_TT_GDNC_STAGE_OVERLAP`: stage each DELTA sub-chunk's slot under the card time of the + * descriptor before it (CONV for sub-chunk 0, sub-chunk 0's DELTA for sub-chunk 1). ON by + * default: 234.0 -> 244.4 tok/s, 2,448 of 2,448 stagings hidden, IDENTICAL, read-back 0. */ +static bool gdnc_stage_overlap(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_GDNC_STAGE_OVERLAP"); + + cached = (value && *value) ? (strcmp(value, "0") != 0) : 1; + fprintf(stderr, "qwen35_tt: PHASE DELTA staging %s\n", + cached ? "runs UNDER the previous gdnc descriptor's card time (overlap)" + : "runs between descriptors (serial)"); + } + return cached != 0; +} + +/** One sub-chunk to prestage from the overlap hook. */ +typedef struct { + pn_tt_device *device; + uint32_t layer, first_row, chunk_rows; + const float *decay, *beta; +} gdn_prestage_job; + +static int gdn_prestage_run(void *context) +{ + const gdn_prestage_job *job = (const gdn_prestage_job *)context; + + return pn_tt_gdnc_delta_prestage_at(job->device, job->layer, job->first_row, + job->chunk_rows, job->decay, job->beta); +} + +/** The sub-chunk starting at `first` as a prestage job. */ +static gdn_prestage_job gdn_prestage_job_at(pn_qwen35_tt *g, int ssm_index, int v_heads, + int rows, int sub, int first) +{ + gdn_prestage_job job; + + job.device = g->device; + job.layer = (uint32_t)ssm_index; + job.first_row = (uint32_t)first; + job.chunk_rows = (uint32_t)((rows - first < sub) ? rows - first : sub); + job.decay = g->gdn_chunk_decay + (size_t)first * v_heads; + job.beta = g->gdn_chunk_beta + (size_t)first * v_heads; + return job; +} + +/** + * PHASE DELTA over the pass, at the shipped chunk length, IN PLACE. + * + * The recurrent state carries across sub-chunks exactly as it carries across passes — neither + * this file nor the kernels touch it in between — and the strip is untouched, so `decay` and + * `beta` are just sliced. One iteration when C covers the pass, and then this is byte for + * byte the descriptor that shipped. + */ +uint32_t pn_qwen35_tt_gdn_gate_sub_rows(void) +{ + const int sub = delta_sub_rows(); + + return sub == 0 ? 32u : (uint32_t)sub; +} + +/** PM 7 ITEM 5: the band's DELTA descriptors on the card-built gate blocks — the same split + * as below (`ftgg_sub_live_rows()` is that split), nothing staged. */ +static int gdn_delta_card_gate(pn_qwen35_tt *g, int ssm_index, int rows) +{ + const uint32_t sub_rows = pn_qwen35_tt_gdn_gate_sub_rows(); + + for (uint32_t sub = 0; sub < 2u; sub++) { + const uint32_t live = ftgg_sub_live_rows((uint32_t)rows, sub_rows, sub); + + if (live == 0u) + continue; + if (pn_tt_gdnc_delta_oncard_card_gate( + g->device, (uint32_t)ssm_index, sub * sub_rows, live, + pn_tt_gdngate_block_address(g->device, (uint32_t)ssm_index, + g->gdn_card_gate_band, sub)) != 0) + return -1; + } + return 0; +} + +static int gdn_delta_in_place(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index, + int rows) +{ + const int v_heads = cfg->ssm_dt_rank; + const int sub = delta_sub_rows(); + int first; + + if (g->gdn_card_gate) + return gdn_delta_card_gate(g, ssm_index, rows); + + if (sub == 0 || sub >= rows) + return pn_tt_gdnc_delta_oncard(g->device, (uint32_t)ssm_index, (uint32_t)rows, + g->gdn_chunk_decay, g->gdn_chunk_beta); + for (first = 0; first < rows; first += sub) { + const int live = (rows - first < sub) ? rows - first : sub; + gdn_prestage_job next; + + /* The NEXT sub-chunk stages under this one's card time, into the other slot. */ + if (gdnc_stage_overlap() && first + sub < rows) { + next = gdn_prestage_job_at(g, ssm_index, v_heads, rows, sub, first + sub); + pn_tt_gdnc_set_overlap(g->device, gdn_prestage_run, &next); + } + if (pn_tt_gdnc_delta_oncard_at(g->device, (uint32_t)ssm_index, (uint32_t)first, + (uint32_t)live, + g->gdn_chunk_decay + (size_t)first * v_heads, + g->gdn_chunk_beta + (size_t)first * v_heads) != 0) + return -1; + } + return 0; +} + +/** Read once and SAID OUT LOUD once — an isolation arm that silently did not fire is the + * failure mode this whole measurement is exposed to. */ +static pn_gdnc_phase gdnc_phase(void) +{ + static pn_gdnc_phase phase = PN_GDNC_PHASE_BOTH; + static bool read = false; + const char *env; + + if (read) + return phase; + read = true; + env = getenv(PN_QWEN35_TT_GDNC_PHASE_ENV); + if (!env || !*env || strcmp(env, "both") == 0) + return phase; + if (strcmp(env, "conv") == 0) + phase = PN_GDNC_PHASE_CONV; + else if (strcmp(env, "delta") == 0) + phase = PN_GDNC_PHASE_DELTA; + else if (strcmp(env, "none") == 0) + phase = PN_GDNC_PHASE_NONE; + else + fprintf(stderr, "qwen35_tt: %s=%s is not one of both|conv|delta|none — staying on " + "`both`\n", PN_QWEN35_TT_GDNC_PHASE_ENV, env); + if (phase != PN_GDNC_PHASE_BOTH) + fprintf(stderr, "qwen35_tt: gdnc phase split ARMED — %s. PHASE %s is chunkwise, the " + "other runs PER POSITION through the row shuttle. This is an " + "attribution instrument and it is SLOW.\n", + env, phase == PN_GDNC_PHASE_CONV ? "CONV" + : phase == PN_GDNC_PHASE_DELTA ? "DELTA" + : "NEITHER"); + return phase; +} + +bool pn_qwen35_tt_gdn_card_gate_possible(void) +{ + return gdnc_phase() == PN_GDNC_PHASE_BOTH && gdnc_sub_rows() == 0; +} + +int pn_qwen35_tt_gdn_strip_arm(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg) +{ + const uint32_t state = (uint32_t)cfg->ssm_state; + uint32_t stream_tiles; + uint32_t value_tiles; + const char *want; + + if (!g || !g->device || !cfg) + return -1; + /* Default ON: only an explicit `0` turns it off. */ + want = getenv(PN_QWEN35_TT_GDN_STRIP_ENV); + if (want && *want && atoi(want) == 0) { + fprintf(stderr, "qwen35_tt: %s=0 — the strip bridge is OFF by request; the block's " + "activations cross the link again\n", PN_QWEN35_TT_GDN_STRIP_ENV); + return 0; + } + if (!g->gdn_on_card) { + fprintf(stderr, "qwen35_tt: %s needs the GDN family armed — add `gdn` to " + "PN_TT_ONCARD. The block stays on the host.\n", + PN_QWEN35_TT_GDN_STRIP_ENV); + return 0; + } + /* THE TWO EXISTING GDN PROBES SCORE THE HOST-MEDIATED ARM AND CANNOT SEE THIS ONE. + * `PN_TT_GDN_PARITY` runs a shadow host recurrence beside `pn_qwen35_tt_gdn_recurrence()` + * and `PN_TT_GDN_NORMCHECK` scores the two per-head norms against `gdn_conv_out`/`gdn_y` + * — neither function is on this path, so with the bridge on they would run ZERO + * comparisons and report a clean sheet. That is a gate passing because it never fired, + * which this project has now hit seven times, so the bridge STANDS DOWN and says why + * rather than letting a probe read as green. */ + if (getenv("PN_TT_GDN_PARITY") || getenv("PN_TT_GDN_NORMCHECK")) { + fprintf(stderr, "qwen35_tt: PN_TT_GDN_PARITY / PN_TT_GDN_NORMCHECK score the " + "HOST-MEDIATED arm and would compare nothing at all against the strip " + "bridge — standing the bridge down so the probe measures what it " + "claims to. Unset it to run the bridge.\n"); + return 0; + } + stream_tiles = pn_tt_gdn_stream_tiles(g->device); + value_tiles = pn_tt_gdn_value_tiles(g->device); + if (stream_tiles == 0u || value_tiles == 0u) + return 0; + + /* The widest strip each direction sees: the qkv stream one way, `y_norm` the other. */ + if (pn_tt_device_arm_render(g->device, stream_tiles, value_tiles) != 0) + return 0; + if (pn_tt_device_arm_head_norm(g->device, state) != 0) + return 0; + /* THE MERGED GATE, OPT-IN. It takes `render_narrow` out of the DECODE token; the render + * stays armed because the chunkwise prefill path still narrows through it, and a residency + * that armed is a residency the stats table reports at zero ops rather than one that + * silently was not there. */ + if (pn_tt_device_arm_gate_narrow(g->device, value_tiles) != 0) + return 0; + if (!pn_tt_render_widen_is_armed(g->device) || !pn_tt_render_narrow_is_armed(g->device) || + !pn_tt_head_norm_is_armed(g->device, state)) { + fprintf(stderr, "qwen35_tt: %s asked for the strip bridge and one of the three " + "residencies it needs did not arm — the block stays on the " + "host-mediated path\n", PN_QWEN35_TT_GDN_STRIP_ENV); + return 0; + } + + g->gdn_rms = calloc((size_t)cfg->n_layer, sizeof(*g->gdn_rms)); + g->gdn_rms_ready = calloc((size_t)cfg->n_layer, sizeof(*g->gdn_rms_ready)); + if (!g->gdn_rms || !g->gdn_rms_ready) + return -1; + /* The L2 member takes eps*eps: the oracle clamps the MAGNITUDE at eps and the eps tile + * folds into the SQUARE. foundry `ft_tt_op_vector.h` carries the derivation. It is ONE + * buffer for the model — the L2 norm carries no gamma, so nothing in it is per layer. */ + if (pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, NULL, state, + cfg->rms_norm_eps * cfg->rms_norm_eps, + &g->gdn_l2) != 0) + return -1; + if (pn_tt_fp32_strip_alloc(g->device, value_tiles, &g->gdn_y_norm) != 0 || + pn_tt_activation_alloc(g->device, value_tiles, &g->gdn_y_bridge) != 0) + return -1; + /* The in-place probe's scratch — three q|k widths and two v widths, allocated only when + * the probe is asked for so the shipped arm pays nothing for it. */ + if (getenv(PN_QWEN35_TT_GDN_STRIP_CHECK_ENV)) { + g->gdn_probe = calloc((size_t)3u * pn_tt_gdn_key_tiles(g->device) * 32u + + (size_t)2u * value_tiles * 32u, + sizeof(*g->gdn_probe)); + if (!g->gdn_probe) + return -1; + } + + /* THE CHUNK's gate scratch, one tile-row of positions wide. Allocated only when the + * chunkwise family armed, so a decode-only process pays nothing for it. */ + if (pn_tt_gdnc_is_armed(g->device)) { + g->gdn_chunk_decay = calloc(32u * (size_t)cfg->ssm_dt_rank, sizeof(float)); + g->gdn_chunk_beta = calloc(32u * (size_t)cfg->ssm_dt_rank, sizeof(float)); + if (!g->gdn_chunk_decay || !g->gdn_chunk_beta) + return -1; + } + + /* THE PHASE-ISOLATION ARM's row shuttle, allocated only when it is asked for. */ + if (pn_tt_gdnc_is_armed(g->device) && + (gdnc_phase() != PN_GDNC_PHASE_BOTH || gdnc_sub_rows() != 0)) { + g->gdn_split_stream = calloc(32u * (size_t)stream_tiles * 32u, sizeof(float)); + g->gdn_split_y = calloc(32u * (size_t)value_tiles * 32u, sizeof(float)); + if (!g->gdn_split_stream || !g->gdn_split_y) + return -1; + } + + g->gdn_strip = true; + fprintf(stderr, "qwen35_tt: THE STRIP BRIDGE IS ON — the Gated-Delta-Net block runs whole " + "on the card. Per SSM layer the link carries alpha+beta DOWN (256 B) and " + "the gate block UP (256 B); the activations do not cross at all.\n"); + return 0; +} + +void pn_qwen35_tt_gdn_strip_release(pn_qwen35_tt *g) +{ + if (!g) + return; + free(g->gdn_rms); + free(g->gdn_rms_ready); + free(g->gdn_probe); + free(g->gdn_chunk_decay); + free(g->gdn_chunk_beta); + free(g->gdn_split_stream); + free(g->gdn_split_y); + g->gdn_rms = NULL; + g->gdn_rms_ready = NULL; + g->gdn_probe = NULL; + g->gdn_chunk_decay = NULL; + g->gdn_chunk_beta = NULL; + g->gdn_split_stream = NULL; + g->gdn_split_y = NULL; + g->gdn_strip = false; +} + +/** + * THE IN-PLACE PROOF, and it is the only thing in this file that reads the card. + * + * The q/k L2 norm writes over its own input. That is safe by pipeline order, and this is what + * turns "by pipeline order" into a number: score the card's answer, per head, against the host + * oracle applied to the PREIMAGE this function read a moment earlier. A reduce that raced its + * own input would be normalising values it had already overwritten, and the two would not + * agree to Tf32 truncation. + * + * `v` is checked too, and it is the cheaper half of the same question: this descriptor covers + * the first `2*k_heads*state_tiles` tiles of a 256-tile buffer, so if its write pointer walked + * past its row blocks, `v` is where it would land — and `v` is PHASE DELTA's operand. + * + * A PROBE: budgeted, off by default, and nothing downstream reads it. + */ + +/** Max-norm relative error of `got` against `want`, PER HEAD. A per-head fault is invisible + * in a per-tensor figure — this family scored 1.000e+00 for three rounds on exactly that. */ +static double gdn_strip_worst_head(const float *want, const float *got, int heads, int width) +{ + double worst = 0.0; + int head; + + for (head = 0; head < heads; head++) { + double max_abs = 0.0, max_want = 0.0; + int index; + + for (index = 0; index < width; index++) { + const double diff = fabs((double)got[(size_t)head * width + index] - + (double)want[(size_t)head * width + index]); + const double magnitude = fabs((double)want[(size_t)head * width + index]); + + if (diff > max_abs) + max_abs = diff; + if (magnitude > max_want) + max_want = magnitude; + } + if (max_want > 0.0 && max_abs / max_want > worst) + worst = max_abs / max_want; + } + return worst; +} + +/** + * Read the conv-output q|k PREIMAGE, run the descriptor, score the answer. Returns the + * descriptor's own result so the caller cannot accidentally skip a failure. + */ +static int gdn_strip_l2_checked(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index, + ft_tt_op_operand stream, uint32_t stream_tiles) +{ + const int state = cfg->ssm_state; + const int k_heads = cfg->ssm_n_group; + const uint32_t key_tiles = pn_tt_gdn_key_tiles(g->device); + const uint32_t value_tiles = pn_tt_gdn_value_tiles(g->device); + const size_t key_count = (size_t)key_tiles * 32u; + const size_t value_count = (size_t)value_tiles * 32u; + /* ITS OWN SCRATCH, sized from the two counts it actually reads. The first spelling of + * this probe borrowed `gdn_qn`, which is `k_heads * state` — HALF the q|k strip — and + * overran it: the arm aborted and printed a worst head of 1.7e+00 against a path whose + * 48-token completion is byte-identical to the oracle. A probe that reports a failure it + * caused itself is worse than no probe. */ + float *preimage = g->gdn_probe; + float *want = preimage + key_count; + float *got = want + key_count; + float *v_before = got + key_count; + float *v_after = v_before + value_count; + double worst; + int head; + int rc; + + if (!g->gdn_probe) + return -1; + if (pn_tt_gdn_stream_read(g->device, 0u, key_tiles, preimage) != 0 || + pn_tt_gdn_stream_read(g->device, key_tiles, value_tiles, v_before) != 0) + return -1; + /* The oracle's own function on the preimage, per k-head: q's heads then k's. */ + memcpy(want, preimage, key_count * sizeof(float)); + for (head = 0; head < 2 * k_heads; head++) + pn_qwen35moe_l2_norm(want + (size_t)head * state, state, cfg->rms_norm_eps); + + rc = pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &g->gdn_l2, + (uint32_t)(2 * k_heads), stream, stream, stream_tiles, + stream_tiles); + if (rc != 0) + return rc; + if (pn_tt_gdn_stream_read(g->device, 0u, key_tiles, got) != 0 || + pn_tt_gdn_stream_read(g->device, key_tiles, value_tiles, v_after) != 0) + return -1; + + worst = gdn_strip_worst_head(want, got, 2 * k_heads, state); + fprintf(stderr, "qwen35_tt: [strip] ssm %2d L2 IN PLACE worst head %.3e v untouched %s\n", + ssm_index, worst, + memcmp(v_before, v_after, value_count * sizeof(float)) == 0 ? "YES" : "NO"); + return 0; +} + +/** This layer's gated-RMS constants, built on first use. `gamma` is `ssm_norm`, per layer. */ +static int gdn_strip_rms_constants(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index) +{ + if (g->gdn_rms_ready[ssm_index]) + return 0; + if (pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, l->ssm_norm, + (uint32_t)cfg->ssm_state, cfg->rms_norm_eps, + &g->gdn_rms[ssm_index]) != 0) + return -1; + g->gdn_rms_ready[ssm_index] = true; + return 0; +} + +/** One position's gates on the HOST: the engine's libm arithmetic, or (det gates) the same + * integer ft_det_math the card runs — on float bits that are bf16 << 16, as the card sees. */ +static void gdn_gate_host(const pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const float *alpha, const float *beta_raw, int v_heads, float *decay, + float *beta) +{ + int head; + + if (!g->gdn_det_gates) { + pn_qwen35moe_beta_sigmoid(beta_raw, v_heads, beta); + pn_qwen35moe_decay_gate(l, alpha, v_heads, decay); + for (head = 0; head < v_heads; head++) + decay[head] = expf(decay[head]); + return; + } + for (head = 0; head < v_heads; head++) { + uint32_t a, dt, ssm_a, b, out; + + memcpy(&a, &alpha[head], sizeof(a)); + memcpy(&dt, &l->ssm_dt[head], sizeof(dt)); + memcpy(&ssm_a, &l->ssm_a[head], sizeof(ssm_a)); + memcpy(&b, &beta_raw[head], sizeof(b)); + out = ft_det_gdn_gate(a, dt, ssm_a); + memcpy(&decay[head], &out, sizeof(out)); + out = ft_det_gdn_beta(b); + memcpy(&beta[head], &out, sizeof(out)); + } +} + +/** ITEM 6: this many rows' gates straight into the gate buffer the next DELTA reads, computed + * on the card from band 0 of the alpha/beta strips. */ +static int gdn_gate_card(pn_qwen35_tt *g, int ssm_index, uint32_t rows) +{ + uint32_t bank, address, stride; + + if (pn_tt_gdn_gate_target(g->device, rows, &bank, &address, &stride) != 0) + return -1; + return pn_tt_gdngate_flat(g->device, (uint32_t)ssm_index, &g->card.ssm_alpha, + &g->card.ssm_beta, rows, bank, address, stride); +} + +int pn_qwen35_tt_gdn_strip_layer(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const pn_tt_activation *qkv, const pn_tt_activation *z, + const float *beta_raw, const float *alpha, + const pn_tt_activation *gated) +{ + const int v_heads = cfg->ssm_dt_rank; + const uint32_t stream_tiles = pn_tt_gdn_stream_tiles(g->device); + const uint32_t value_tiles = pn_tt_gdn_value_tiles(g->device); + ft_tt_op_operand stream; + + /* ITEM 6: `alpha == beta_raw == NULL` asks for the gates ON THE CARD (det gates only). */ + const bool card = !alpha && !beta_raw && g && g->gdn_det_gates; + + if (!g || !l || !cfg || !qkv || !z || (!card && (!beta_raw || !alpha)) || !gated) + return -1; + if (!g->gdn_strip || ssm_index < 0) + return -1; + if (gdn_strip_rms_constants(g, l, cfg, ssm_index) != 0) + return -1; + + /* 1 and 2, the oracle's own, on the two operands that still cross. `decay = exp(g)` is + * applied HERE because the card has no `log` and therefore no `pow`. */ + if (!card) + gdn_gate_host(g, l, alpha, beta_raw, v_heads, g->gdn_decay, g->gdn_beta); + + /* 3. The bf16 projection becomes the fp32 stream PHASE CONV reads, in card DRAM. */ + if (pn_tt_render_widen(g->device, pn_tt_operand(g->device, qkv), + pn_tt_gdn_stream_in_operand(g->device), stream_tiles) != 0) + return -1; + if (pn_tt_gdn_conv_oncard(g->device, (uint32_t)ssm_index) != 0) + return -1; + + /* 4. The q/k L2 norm, IN PLACE over the first `2*k_heads*state_tiles` tiles of the conv + * output — one row block per head, `state_tiles` tile columns wide, exactly the + * `[1, heads*S]` reduce `pn_tt_head_norm.h` describes. `v` is the rest of the same buffer + * and this descriptor never reaches it. The GQA repeat is NOT materialised: the DELTA + * reader's `vh % k_heads` is the repeat, and staging the 32-head form is the 1.33x-WORSE + * row of the split-point table. */ + stream = pn_tt_gdn_stream_operand(g->device); + if (g->gdn_strip_checks < PN_QWEN35_TT_GDN_STRIP_CHECK_BUDGET && + getenv(PN_QWEN35_TT_GDN_STRIP_CHECK_ENV)) { + g->gdn_strip_checks++; + if (gdn_strip_l2_checked(g, cfg, ssm_index, stream, stream_tiles) != 0) + return -1; + } else if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &g->gdn_l2, + (uint32_t)(2 * cfg->ssm_n_group), stream, stream, + stream_tiles, stream_tiles) != 0) { + return -1; + } + + /* 5. PHASE DELTA over that same buffer, plus 256 B of gate block (ITEM 6: written by the + * card, not uploaded). */ + if (card ? (gdn_gate_card(g, ssm_index, 1u) != 0 || + pn_tt_gdn_delta_descriptor_only(g->device, (uint32_t)ssm_index) != 0) + : pn_tt_gdn_delta_oncard(g->device, (uint32_t)ssm_index, g->gdn_decay, + g->gdn_beta) != 0) + return -1; + + /* 6. The gated norm, in two halves at two formats: the RMS in the Float32 reduce family + * where the recurrence's precision lives, then the render, then `silu(z) *` on the SHIPPED + * bf16 elementwise residency. The render is the term `gated` is bound by — see + * pn_tt_render.h for what it costs and pn_tt_gate.h for what removes it. */ + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &g->gdn_rms[ssm_index], + (uint32_t)v_heads, pn_tt_gdn_y_operand(g->device), + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + value_tiles, g->gdn_y_norm.tile_count) != 0) + return -1; + /* THE MERGED GATE, WHEN IT IS ARMED: one descriptor instead of two, reading `y_norm` as + * fp32 straight into SrcB. The unpack that narrows it is the SAME truncating fp32->bf16 + * conversion `render_narrow` performs one descriptor earlier — verified off-hardware to be + * the same `unpack_src / unpack_dst / tile_size` triple — so this is expected to be `cmp` + * IDENTICAL and a DIFFERS is a finding, not a cost. */ + if (pn_tt_gate_narrow_is_armed(g->device)) + return pn_tt_gate_narrow(g->device, + pn_tt_operand(g->device, z), + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + pn_tt_operand(g->device, gated), value_tiles); + if (pn_tt_render_narrow(g->device, pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + pn_tt_operand(g->device, &g->gdn_y_bridge), value_tiles) != 0) + return -1; + return pn_tt_silu_mul(g->device, z, &g->gdn_y_bridge, gated); +} + +/** PM 5: the multi-row DELTA verify mode, ON unless `PN_TT_SPEC_DELTA_ROWS=0`. */ +static bool delta_multi_row_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_SPEC_DELTA_ROWS"); + + cached = (value && *value) ? (strcmp(value, "0") != 0) : 1; + fprintf(stderr, "qwen35_tt: verify DELTA rows %s\n", + cached ? "in ONE multi-row descriptor (PN_TT_SPEC_DELTA_ROWS=0 off)" + : "one descriptor a row"); + } + return cached != 0; +} + +/** PM 6: the multi-row CONV verify mode, ON unless `PN_TT_SPEC_CONV_ROWS=0`. */ +static bool conv_multi_row_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *value = getenv("PN_TT_SPEC_CONV_ROWS"); + + cached = (value && *value) ? (strcmp(value, "0") != 0) : 1; + fprintf(stderr, "qwen35_tt: verify CONV rows %s\n", + cached ? "in ONE multi-row descriptor (PN_TT_SPEC_CONV_ROWS=0 off)" + : "one descriptor a row"); + } + return cached != 0; +} + +/** + * STEP 4 S2c: the same six steps over the first `rows` rows of a strip (the speculative + * verify), with the DECODE recurrences run row by row through the kernels' row select + * (`FT_TT_OP_GDN_ROW_SHIFT`) and everything row-wise run ONCE over all rows: the widen, the + * q/k L2 norm, the gated RMS norm and the gate. Every CONV runs before the L2 norm and every + * DELTA after it, exactly the per-row order of each row's own data. Row r's new window and + * state go to snapshot slot r (STATE_OUT) and trade places with the live ones, so slot r keeps + * the state before row r (the rollback). Bit-exactness per row is the gate's to show. + */ +int pn_qwen35_tt_gdn_strip_rows(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, int rows, + const pn_tt_activation *qkv, const pn_tt_activation *z, + const float *beta_raw, size_t beta_stride, + const float *alpha, size_t alpha_stride, + const pn_tt_activation *gated) +{ + const int v_heads = cfg->ssm_dt_rank; + const uint32_t stream_tiles = pn_tt_gdn_stream_tiles(g->device); + const uint32_t value_tiles = pn_tt_gdn_value_tiles(g->device); + ft_tt_op_operand stream; + int r, rc = 0; + /* S2c: the rows' CONVs (then DELTAs) queue behind each other and are awaited once + * (`PN_TT_SPEC_DEFER=0`: one await a row). */ + const bool defer = !getenv("PN_TT_SPEC_DEFER") || atoi(getenv("PN_TT_SPEC_DEFER")) != 0; + /* PRICING PROBE, TIMING ONLY (the text it produces is not evidence): bit 0 runs the DELTA + * of row 0 alone, bit 1 the CONV of row 0 alone — the ceiling of a multi-row mode. */ + const uint32_t probe = getenv("PN_TT_PROBE_GDN_ROWS") ? (uint32_t)atoi(getenv("PN_TT_PROBE_GDN_ROWS")) : 0u; + const int conv_rows = (probe & 2u) ? 1 : rows; + int delta_rows = (probe & 1u) ? 1 : rows; + double mark = pn_time_ms(); + + if (!g->gdn_strip || ssm_index < 0 || rows < 1 || rows > 16) + return -1; + if (gdn_strip_rms_constants(g, l, cfg, ssm_index) != 0) + return -1; + if (pn_tt_render_widen(g->device, pn_tt_operand(g->device, qkv), + pn_tt_gdn_stream_in_operand(g->device), stream_tiles) != 0) + return -1; + /* PM 6 MULTI-ROW CONV: every row in ONE descriptor per member (not deferred: one await). */ + r = 0; + if (conv_rows > 1 && conv_multi_row_enabled()) { + const int multi = pn_tt_gdn_conv_rows_oncard(g->device, (uint32_t)ssm_index, + (uint32_t)conv_rows); + + if (multi == -1) + return -1; + if (multi == 0) + r = conv_rows; /* done; a refusal (-2) leaves r at 0 and the per-row walk runs */ + } + (void)pn_tt_gdn_defer(g->device, defer); + for (; rc == 0 && r < conv_rows; r++) + rc = (pn_tt_gdn_set_row(g->device, (uint32_t)r) != 0 || + pn_tt_gdn_state_out(g->device, r) != 0 || + pn_tt_gdn_conv_oncard(g->device, (uint32_t)ssm_index) != 0 || + pn_tt_gdn_layer_swap_part(g->device, (uint32_t)ssm_index, (uint32_t)r, true, + false) != 0) + ? -1 : 0; + (void)pn_tt_gdn_defer(g->device, false); + if (pn_tt_gdn_await_deferred(g->device) != 0) + rc = -1; + (void)pn_tt_gdn_state_out(g->device, -1); + (void)pn_tt_gdn_set_row(g->device, 0u); + if (rc != 0) + return -1; + g->spec_gdn_part_ms[1] += pn_time_ms() - mark; + mark = pn_time_ms(); + stream = pn_tt_gdn_stream_operand(g->device); + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &g->gdn_l2, + (uint32_t)(2 * cfg->ssm_n_group), stream, stream, + stream_tiles, stream_tiles) != 0) + return -1; + g->spec_gdn_part_ms[2] += pn_time_ms() - mark; + mark = pn_time_ms(); + /* S2d: every row's gates computed first and staged in ONE write (deferred mode), then the + * DELTAs queue on the staged blocks. */ + /* PM 6: the alpha/beta reads (reader thread) land before the host math needs them. + * ITEM 6: with det gates on a deferred verify there are no reads — the card writes every + * row's gate block itself. */ + if (defer && g->gdn_det_gates && !alpha && !beta_raw) { + (void)pn_tt_gdn_defer(g->device, true); + rc = gdn_gate_card(g, ssm_index, (uint32_t)rows); + if (rc == 0 && rows > 1 && delta_rows == rows && delta_multi_row_enabled()) { + (void)pn_tt_gdn_defer(g->device, false); + rc = pn_tt_gdn_delta_rows_oncard(g->device, (uint32_t)ssm_index, (uint32_t)rows); + delta_rows = 0; + } + for (r = 0; rc == 0 && r < delta_rows; r++) + rc = (pn_tt_gdn_set_row(g->device, (uint32_t)r) != 0 || + pn_tt_gdn_state_out(g->device, r) != 0 || + pn_tt_gdn_delta_descriptor_only(g->device, (uint32_t)ssm_index) != 0 || + pn_tt_gdn_layer_swap_part(g->device, (uint32_t)ssm_index, (uint32_t)r, false, + true) != 0) + ? -1 : 0; + } else if (pn_qwen35_tt_ab_verify_wait(g) != 0) { + return -1; + } else if (defer) { + float decay[16 * 64], beta[16 * 64]; + + if (v_heads > 64) + return -1; + for (r = 0; r < rows; r++) + gdn_gate_host(g, l, alpha + (size_t)r * alpha_stride, + beta_raw + (size_t)r * beta_stride, v_heads, decay + r * v_heads, + beta + r * v_heads); + (void)pn_tt_gdn_defer(g->device, true); + rc = pn_tt_gdn_gate_upload_rows(g->device, (uint32_t)rows, decay, beta); + /* PM 5 MULTI-ROW DELTA (`PN_TT_SPEC_DELTA_ROWS=0` off): every row in ONE descriptor + * per member — q/k/v and the state read once a head, the rows carried in L1. */ + if (rc == 0 && rows > 1 && delta_rows == rows && delta_multi_row_enabled()) { + (void)pn_tt_gdn_defer(g->device, false); + rc = pn_tt_gdn_delta_rows_oncard(g->device, (uint32_t)ssm_index, (uint32_t)rows); + delta_rows = 0; + } + for (r = 0; rc == 0 && r < delta_rows; r++) + rc = (pn_tt_gdn_set_row(g->device, (uint32_t)r) != 0 || + pn_tt_gdn_state_out(g->device, r) != 0 || + pn_tt_gdn_delta_descriptor_only(g->device, (uint32_t)ssm_index) != 0 || + pn_tt_gdn_layer_swap_part(g->device, (uint32_t)ssm_index, (uint32_t)r, false, + true) != 0) + ? -1 : 0; + } else + for (r = 0; rc == 0 && r < rows; r++) { + gdn_gate_host(g, l, alpha + (size_t)r * alpha_stride, beta_raw + (size_t)r * beta_stride, + v_heads, g->gdn_decay, g->gdn_beta); + rc = (pn_tt_gdn_set_row(g->device, (uint32_t)r) != 0 || + pn_tt_gdn_state_out(g->device, r) != 0 || + pn_tt_gdn_delta_oncard(g->device, (uint32_t)ssm_index, g->gdn_decay, + g->gdn_beta) != 0 || + pn_tt_gdn_layer_swap_part(g->device, (uint32_t)ssm_index, (uint32_t)r, false, + true) != 0) + ? -1 : 0; + } + (void)pn_tt_gdn_defer(g->device, false); + if (pn_tt_gdn_await_deferred(g->device) != 0) + rc = -1; + (void)pn_tt_gdn_state_out(g->device, -1); + (void)pn_tt_gdn_set_row(g->device, 0u); + if (rc != 0) + return -1; + g->spec_gdn_part_ms[3] += pn_time_ms() - mark; + g->spec_gdn_mark_ms = pn_time_ms(); + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &g->gdn_rms[ssm_index], + (uint32_t)v_heads, pn_tt_gdn_y_operand(g->device), + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + value_tiles, g->gdn_y_norm.tile_count) != 0) + return -1; + if (pn_tt_gate_narrow_is_armed(g->device)) + return pn_tt_gate_narrow(g->device, pn_tt_operand(g->device, z), + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + pn_tt_operand(g->device, gated), value_tiles); + if (pn_tt_render_narrow(g->device, pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + pn_tt_operand(g->device, &g->gdn_y_bridge), value_tiles) != 0) + return -1; + return pn_tt_silu_mul(g->device, z, &g->gdn_y_bridge, gated); +} + +/* ================================================================ + * THE CHUNK — the same six steps, with the two recurrences over 32 positions at once + * ================================================================ */ + +/** + * PHASE CONV per position across the chunk's rows, leaving the buffer a chunk descriptor's + * shape. The `conv` half of the phase-isolation arm — see `PN_TT_PREFILL_GDNC_PHASE`. + * + * THE ZERO IN THE LOOP IS THE POINT, not defensive coding. A decode descriptor computes WHOLE + * TILES over an input whose rows 1..31 are padding, so it writes `SiLU(0) = 0` into rows 1..31 + * of its output and it expects rows 1..31 of its input to be zero. Handing it a chunk's buffer + * unmodified would both feed it 31 other positions and destroy the rows already answered. + */ +static int gdn_split_conv(pn_qwen35_tt *g, int ssm_index, int rows, uint32_t stream_tiles) +{ + const size_t width = (size_t)stream_tiles * 32u; + int t; + + /* The chunk's whole input comes off the card FIRST, because the loop below wipes it. */ + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_IN, (uint32_t)t, + stream_tiles, g->gdn_split_stream + (size_t)t * width, + true) != 0) + return -1; + + for (t = 0; t < rows; t++) { + float *row = g->gdn_split_stream + (size_t)t * width; + + if (pn_tt_gdn_strip_zero(g->device, PN_TT_GDN_STRIP_STREAM_IN) != 0 || + pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_IN, 0u, stream_tiles, + row, false) != 0 || + pn_tt_gdn_conv_oncard(g->device, (uint32_t)ssm_index) != 0 || + pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_OUT, 0u, stream_tiles, + row, true) != 0) + return -1; + } + + /* Reassembled the way a chunk descriptor leaves it: rows 0..C-1 answered, the rest zero. */ + if (pn_tt_gdn_strip_zero(g->device, PN_TT_GDN_STRIP_STREAM_OUT) != 0) + return -1; + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_OUT, (uint32_t)t, + stream_tiles, g->gdn_split_stream + (size_t)t * width, + false) != 0) + return -1; + return 0; +} + +/** PHASE DELTA per position across the chunk's rows. The `delta` half, and the same argument + * about the zero — `y` is whole-tile written too, so every position would wipe the last. */ +static int gdn_split_delta(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index, + int rows, uint32_t stream_tiles, uint32_t value_tiles) +{ + const size_t width = (size_t)stream_tiles * 32u; + const size_t y_width = (size_t)value_tiles * 32u; + const int v_heads = cfg->ssm_dt_rank; + int t; + + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_OUT, (uint32_t)t, + stream_tiles, g->gdn_split_stream + (size_t)t * width, + true) != 0) + return -1; + + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_zero(g->device, PN_TT_GDN_STRIP_STREAM_OUT) != 0 || + pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_OUT, 0u, stream_tiles, + g->gdn_split_stream + (size_t)t * width, false) != 0 || + pn_tt_gdn_delta_oncard(g->device, (uint32_t)ssm_index, + g->gdn_chunk_decay + (size_t)t * v_heads, + g->gdn_chunk_beta + (size_t)t * v_heads) != 0 || + pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_Y, 0u, value_tiles, + g->gdn_split_y + (size_t)t * y_width, true) != 0) + return -1; + + if (pn_tt_gdn_strip_zero(g->device, PN_TT_GDN_STRIP_Y) != 0) + return -1; + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_Y, (uint32_t)t, value_tiles, + g->gdn_split_y + (size_t)t * y_width, false) != 0) + return -1; + return 0; +} + +/** + * The two mixers at a SHORTER chunk length than the pass, `sub` rows at a time. + * + * Each sub-chunk is put at rows 0..sub-1 with the rest of the buffer zero, because that is + * where both chunk kernels read; the window and the recurrent state carry across sub-chunks + * exactly as they carry across passes, since neither this file nor the kernels touch them in + * between. `sub == rows` is one iteration and reproduces the whole-pass arm. + */ +static int gdn_chunk_subchunked(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index, + int rows, int sub, uint32_t stream_tiles, uint32_t value_tiles) +{ + const size_t width = (size_t)stream_tiles * 32u; + const size_t y_width = (size_t)value_tiles * 32u; + const int v_heads = cfg->ssm_dt_rank; + int first; + int t; + + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_IN, (uint32_t)t, + stream_tiles, g->gdn_split_stream + (size_t)t * width, + true) != 0) + return -1; + + for (first = 0; first < rows; first += sub) { + const int live = (rows - first < sub) ? rows - first : sub; + + if (pn_tt_gdn_strip_zero(g->device, PN_TT_GDN_STRIP_STREAM_IN) != 0) + return -1; + for (t = 0; t < live; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_STREAM_IN, (uint32_t)t, + stream_tiles, + g->gdn_split_stream + (size_t)(first + t) * width, + false) != 0) + return -1; + if (pn_tt_gdnc_conv_oncard(g->device, (uint32_t)ssm_index, (uint32_t)live) != 0) + return -1; + /* The same L2 descriptor as every other arm: rows at and above `live` are zero and a + * zero row normalises to a zero row. */ + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &g->gdn_l2, + (uint32_t)(2 * cfg->ssm_n_group), + pn_tt_gdn_stream_operand(g->device), + pn_tt_gdn_stream_operand(g->device), stream_tiles, + stream_tiles) != 0) + return -1; + if (pn_tt_gdnc_delta_oncard(g->device, (uint32_t)ssm_index, (uint32_t)live, + g->gdn_chunk_decay + (size_t)first * v_heads, + g->gdn_chunk_beta + (size_t)first * v_heads) != 0) + return -1; + for (t = 0; t < live; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_Y, (uint32_t)t, value_tiles, + g->gdn_split_y + (size_t)(first + t) * y_width, + true) != 0) + return -1; + } + + if (pn_tt_gdn_strip_zero(g->device, PN_TT_GDN_STRIP_Y) != 0) + return -1; + for (t = 0; t < rows; t++) + if (pn_tt_gdn_strip_row_move(g->device, PN_TT_GDN_STRIP_Y, (uint32_t)t, value_tiles, + g->gdn_split_y + (size_t)t * y_width, false) != 0) + return -1; + return 0; +} + +/** + * Steps 3-5 of the chunk: PHASE CONV, the q/k L2 norm, PHASE DELTA. + * + * The L2 norm between them is the SAME descriptor in every arm and that is deliberate — a row + * reduce reduces ALONG the row, so row t of row block h is position t of head h and one + * position and 32 are the same arithmetic per row. It is not a candidate for the divergence + * and running it batched in all four arms keeps the comparison to the two mixers. + */ +static int gdn_chunk_mixers(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index, + int rows, uint32_t stream_tiles, uint32_t value_tiles) +{ + const pn_gdnc_phase phase = gdnc_phase(); + const int sub = gdnc_sub_rows(); + + if ((phase != PN_GDNC_PHASE_BOTH || sub != 0) && + (!g->gdn_split_stream || !g->gdn_split_y)) + return -1; + /* The sub-chunk answers a different question and the two do not compose: it needs BOTH + * mixers chunkwise, at a shorter C. */ + if (sub != 0 && phase == PN_GDNC_PHASE_BOTH) + return gdn_chunk_subchunked(g, cfg, ssm_index, rows, sub, stream_tiles, value_tiles); + + if (phase == PN_GDNC_PHASE_BOTH || phase == PN_GDNC_PHASE_CONV) { + gdn_prestage_job first; + const int delta_sub = delta_sub_rows(); + + /* Sub-chunk 0's staging hides under CONV, which reads neither slot. */ + if (phase == PN_GDNC_PHASE_BOTH && gdnc_stage_overlap() && !g->gdn_card_gate) { + first = gdn_prestage_job_at(g, ssm_index, cfg->ssm_dt_rank, rows, + (delta_sub == 0 || delta_sub >= rows) ? rows : delta_sub, + 0); + pn_tt_gdnc_set_overlap(g->device, gdn_prestage_run, &first); + } + if (pn_tt_gdnc_conv_oncard(g->device, (uint32_t)ssm_index, (uint32_t)rows) != 0) { + pn_tt_gdnc_set_overlap(g->device, NULL, NULL); + return -1; + } + } else if (gdn_split_conv(g, ssm_index, rows, stream_tiles) != 0) { + return -1; + } + + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &g->gdn_l2, + (uint32_t)(2 * cfg->ssm_n_group), + pn_tt_gdn_stream_operand(g->device), + pn_tt_gdn_stream_operand(g->device), stream_tiles, + stream_tiles) != 0) + return -1; + + if (phase == PN_GDNC_PHASE_BOTH) + return gdn_delta_in_place(g, cfg, ssm_index, rows); + /* THE PHASE-ISOLATION ARMS KEEP THE WHOLE-PASS DESCRIPTOR. They exist to attribute the + * variance between the two mixers at ONE chunk length; sub-chunking one of them inside an + * arm whose other half is per-position would answer neither question. */ + if (phase == PN_GDNC_PHASE_DELTA) + return pn_tt_gdnc_delta_oncard(g->device, (uint32_t)ssm_index, (uint32_t)rows, + g->gdn_chunk_decay, g->gdn_chunk_beta); + return gdn_split_delta(g, cfg, ssm_index, rows, stream_tiles, value_tiles); +} + +bool pn_qwen35_tt_gdn_chunk_available(const pn_qwen35_tt *g, int rows) +{ + uint32_t floor; + + if (!g || !g->gdn_strip || !g->gdn_chunk_decay || !g->gdn_chunk_beta) + return false; + /* An isolation arm that quietly fell back to the chunk path would be a measurement of the + * thing not under test — the one way this attribution can lie and read plausible. */ + if ((gdnc_phase() != PN_GDNC_PHASE_BOTH || gdnc_sub_rows() != 0) && + (!g->gdn_split_stream || !g->gdn_split_y)) + return false; + floor = pn_tt_gdnc_min_chunk_rows(g->device); + /* A ZERO FLOOR MEANS THE FAMILY IS NOT ARMED, not that any chunk will do — the floor is + * `conv_taps - 1` and a family that armed has at least two taps. */ + if (floor == 0u) + return false; + return rows > 0 && rows <= 32 && (uint32_t)rows >= floor; +} + +/* ======================================================================================= + * PM 7 ITEM 7 step 1 — THE PIPELINED BAND (streamed, card gates) + * + * Per band the chain was widen, CONV, L2, DELTA, gated RMS, narrow, gate — each awaited before + * the next, so the puller stood still through all seven. Only CONV (window) and DELTA (state) + * carry across bands; RMS / narrow / gate of band b depend on band b's DELTA alone. So band b's + * tail runs under band b+1's head, its awaits moved to just before the op that rewrites what it + * reads: + * band b+1: widen | CONV | settle RMS(b), settle gate(b-1), narrow(b) | L2 | DELTA | + * settle narrow(b), gate(b) | RMS(b+1) + * WAR hazards, each closed by the settle before it: DELTA(b+1) rewrites y (RMS(b) settled), + * RMS(b+1) rewrites y_norm (narrow(b) settled), narrow(b) rewrites the bridge (gate(b-1) + * settled). Every op is the one the serial chain ran, on the same bytes: bit-exact. + * ======================================================================================= */ + +/** One `strcmp` against `PN_TT_RED`. */ +static bool gdn_strip_env_red(const char *name) +{ + const char *value = getenv("PN_TT_RED"); + + return value && *value && strcmp(value, name) == 0; +} + +/** The pending band's narrow (deferred). Needs its RMS done and the gate before it done. */ +static int gdn_pipe_narrow(pn_qwen35_tt *g, uint32_t value_tiles) +{ + if (pn_tt_head_norm_settle(g->device, g->gdn_rms[g->gdn_pipe_layer].tile_cols) != 0 || + pn_tt_eltwise_group_settle(g->device) != 0) + return -1; + pn_tt_render_defer(g->device, true, true); + { + const int rc = pn_tt_render_narrow(g->device, + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + pn_tt_operand(g->device, &g->gdn_y_bridge), + value_tiles); + + pn_tt_render_defer(g->device, true, false); + return rc; + } +} + +/** The pending band's gate (deferred), once its narrow is done. */ +static int gdn_pipe_gate(pn_qwen35_tt *g) +{ + if (pn_tt_render_settle(g->device, true) != 0) + return -1; + pn_tt_eltwise_group_defer_next(g->device); + return pn_tt_silu_mul(g->device, &g->gdn_pipe_z, &g->gdn_y_bridge, &g->gdn_pipe_gated); +} + +/** Band b's RMS (deferred), its tail then pending with z / gated kept for the next band. */ +static int gdn_pipe_rms(pn_qwen35_tt *g, int ssm_index, int v_heads, uint32_t value_tiles, + const pn_tt_activation *z, const pn_tt_activation *gated) +{ + int rc; + + pn_tt_head_norm_defer(g->device, g->gdn_rms[ssm_index].tile_cols, true); + rc = pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, + &g->gdn_rms[ssm_index], (uint32_t)v_heads, + pn_tt_gdn_y_operand(g->device), + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + value_tiles, g->gdn_y_norm.tile_count); + pn_tt_head_norm_defer(g->device, g->gdn_rms[ssm_index].tile_cols, false); + g->gdn_pipe_z = *z; + g->gdn_pipe_gated = *gated; + g->gdn_pipe_layer = ssm_index; + g->gdn_pipe_stage = rc == 0 ? 1 : 0; + return rc; +} + +int pn_qwen35_tt_gdn_pipe_drain(pn_qwen35_tt *g) +{ + int rc = 0; + + if (!g || g->gdn_pipe_stage == 0) + return 0; + g->gdn_pipe_stage = 0; + if (gdn_pipe_narrow(g, pn_tt_gdn_value_tiles(g->device)) != 0 || gdn_pipe_gate(g) != 0) + rc = -1; + /* Everything awaited: whatever reads `gated` next sees it written. */ + if (pn_tt_render_settle(g->device, true) != 0 || pn_tt_eltwise_group_settle(g->device) != 0 || + pn_tt_head_norm_settle(g->device, g->gdn_rms[g->gdn_pipe_layer].tile_cols) != 0) + rc = -1; + return rc; +} + +/** PM 8 TIMING PROBE `PN_TT_PROBE_CHAIN=conv|delta|gdnc`: the piped band skips that gdnc + * phase's descriptors — the chain's floor without them. The text is garbage by design. */ +static bool gdn_probe_skips(const char *phase) +{ + static const char *probe = (const char *)1; + + if (probe == (const char *)1) + probe = getenv("PN_TT_PROBE_CHAIN"); + return probe && *probe && + (strcmp(probe, phase) == 0 || strcmp(probe, "gdnc") == 0); +} + +/** The pipelined band: widen, CONV, [narrow(prev)], L2, DELTA, [gate(prev)], RMS(b). */ +static int gdn_strip_chunk_piped(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int ssm_index, + const pn_tt_activation *qkv, const pn_tt_activation *z, + int rows, const pn_tt_activation *gated) +{ + const uint32_t stream_tiles = pn_tt_gdn_stream_tiles(g->device); + const uint32_t value_tiles = pn_tt_gdn_value_tiles(g->device); + const bool pending = g->gdn_pipe_stage == 1; + ft_tt_op_operand stream; + + /* A band of another layer must not inherit this one's tail. */ + if (pending && g->gdn_pipe_layer != ssm_index && pn_qwen35_tt_gdn_pipe_drain(g) != 0) + return -1; + if (pn_tt_render_widen(g->device, pn_tt_operand(g->device, qkv), + pn_tt_gdn_stream_in_operand(g->device), stream_tiles) != 0 || + (!gdn_probe_skips("conv") && + pn_tt_gdnc_conv_oncard(g->device, (uint32_t)ssm_index, (uint32_t)rows) != 0)) + return -1; + if (g->gdn_pipe_stage == 1 && gdn_pipe_narrow(g, value_tiles) != 0) + return -1; + stream = pn_tt_gdn_stream_operand(g->device); + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_L2_NORM, &g->gdn_l2, + (uint32_t)(2 * cfg->ssm_n_group), stream, stream, + stream_tiles, stream_tiles) != 0 || + (!gdn_probe_skips("delta") && gdn_delta_card_gate(g, ssm_index, rows) != 0)) + return -1; + /* RED `gdn_pipe_gate_skip`: the pending band's gate is never issued (its `gated` rows stay + * whatever they were) — must DIFFER: proves the pipelined gate is what writes them. */ + if (g->gdn_pipe_stage == 1 && !gdn_strip_env_red("gdn_pipe_gate_skip") && + gdn_pipe_gate(g) != 0) + return -1; + return gdn_pipe_rms(g, ssm_index, cfg->ssm_dt_rank, value_tiles, z, gated); +} + +int pn_qwen35_tt_gdn_strip_chunk(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const pn_tt_activation *qkv, const pn_tt_activation *z, + const float *beta_raw, uint32_t beta_stride, + const float *alpha, uint32_t alpha_stride, int rows, + const pn_tt_activation *gated) +{ + const int v_heads = cfg->ssm_dt_rank; + const uint32_t stream_tiles = pn_tt_gdn_stream_tiles(g->device); + const uint32_t value_tiles = pn_tt_gdn_value_tiles(g->device); + int position; + + if (!g || !l || !cfg || !qkv || !z || !beta_raw || !alpha || !gated) + return -1; + if (ssm_index < 0 || !pn_qwen35_tt_gdn_chunk_available(g, rows)) + return -1; + { + const double at = pn_time_ms() / 1000.0; + const int rc = gdn_strip_rms_constants(g, l, cfg, ssm_index); + + pn_tt_device_host_clock_add(g->device, PN_TT_HOST_CLOCK_GDN_CONSTANTS, + pn_time_ms() / 1000.0 - at); + if (rc != 0) + return -1; + } + + /* 1 and 2, the oracle's own, now for every position of the chunk. The result is ROW-MAJOR + * BY POSITION, which is the layout `pn_tt_gdnc_delta_oncard()` transposes per head into a + * decay-ratio tile and a gate run. `decay = exp(g)` is applied HERE because the card has + * no `log` and therefore no `pow`. (PM 7 ITEM 5: or not at all — the card computed them.) */ + if (!g->gdn_card_gate) { + const double at = pn_time_ms() / 1000.0; + + for (position = 0; position < rows; position++) { + float *decay = g->gdn_chunk_decay + (size_t)position * v_heads; + float *beta = g->gdn_chunk_beta + (size_t)position * v_heads; + + gdn_gate_host(g, l, alpha + (size_t)position * alpha_stride, + beta_raw + (size_t)position * beta_stride, v_heads, decay, beta); + } + pn_tt_device_host_clock_add(g->device, PN_TT_HOST_CLOCK_GDN_MATH, + pn_time_ms() / 1000.0 - at); + } + + if (g->gdn_pipe && g->gdn_card_gate) + return gdn_strip_chunk_piped(g, cfg, ssm_index, qkv, z, rows, gated); + + /* 3. The bf16 projection becomes the fp32 stream, WHOLE TILES — the render is per tile, so + * all 32 rows cross in the same descriptor one row used to. Rows at and above the chunk are + * zero and the host owes that: `pn_tt_activation_upload_rows()` tilizes the pass's hidden + * strip at `valid_rows = rows`, an RMS norm of a zero row is zero and an `Mt=1` matvec of a + * zero row is zero, so every projection is already zero above the chunk. */ + if (pn_tt_render_widen(g->device, pn_tt_operand(g->device, qkv), + pn_tt_gdn_stream_in_operand(g->device), stream_tiles) != 0) + return -1; + + /* 4 and 5. PHASE CONV, the q/k L2 norm IN PLACE, PHASE DELTA — the three steps + * `PN_TT_PREFILL_GDNC_PHASE` can split between the chunkwise and the decode families. The + * L2 norm is the same descriptor in every arm; the norm's own reason is in + * `gdn_chunk_mixers()`. */ + if (gdn_chunk_mixers(g, cfg, ssm_index, rows, stream_tiles, value_tiles) != 0) + return -1; + + /* 6. The gated norm and the output gate, both unchanged for the same reasons as 4. */ + if (pn_tt_head_norm_operands(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &g->gdn_rms[ssm_index], + (uint32_t)v_heads, pn_tt_gdn_y_operand(g->device), + pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + value_tiles, g->gdn_y_norm.tile_count) != 0) + return -1; + /* PM 7: the previous chunk's gate (deferred) still reads `gdn_y_bridge`: settled before + * the narrow rewrites it. Streamed, this is where its puller AWAIT now lands — the widen, + * CONV, L2, DELTA and gated norm above ran under it. */ + if (pn_tt_eltwise_group_settle(g->device) != 0) + return -1; + if (pn_tt_render_narrow(g->device, pn_tt_fp32_strip_operand(g->device, &g->gdn_y_norm), + pn_tt_operand(g->device, &g->gdn_y_bridge), value_tiles) != 0) + return -1; + if (g->gdn_chunk_defer_gate) + pn_tt_eltwise_group_defer_next(g->device); + return pn_tt_silu_mul(g->device, z, &g->gdn_y_bridge, gated); +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_gdn_tt.c b/src/models/qwen35moe_gdn_tt.c new file mode 100644 index 0000000..e98ace7 --- /dev/null +++ b/src/models/qwen35moe_gdn_tt.c @@ -0,0 +1,279 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_gdn_tt.c + * @brief The Gated-Delta-Net recurrence with its two heavy phases ON THE CARD — the caller + * `foundry/tt/ft_tt_op_gdn.h` was written for, and the only thing that stands between + * the triad and its first instruction on silicon. + * + * --------------------------------------------------------------------------- + * THE SAME FIVE STEPS AS THE ORACLE, IN THE SAME ORDER, WITH TWO OF THEM MOVED + * --------------------------------------------------------------------------- + * `pn_qwen35moe_ssm_recurrence()` in qwen35moe_ssm.c IS the definition of this block, value + * for value, and it does five things between the input projections and the output one: + * + * 1. beta = sigmoid(beta_raw) host, and stays host + * 2. g = ssm_a * softplus(alpha + ssm_dt) host, and CANNOT move: `decay = + * exp(g)` needs a `pow` and so a `log`, + * and the SFPU has neither + * 3. conv1d + SiLU over conv_dim, window rolled -> PHASE CONV + * 4. split q|k|v, L2-norm each q/k head, GQA repeat host for now (see below); the repeat + * is never materialised — it is the + * reader's `vh % k_heads` + * 5. the delta rule over every v-head -> PHASE DELTA + * 6. gated norm: RMSNorm(y, ssm_norm) * silu(z) host for now + * + * Steps 1, 2, 4 and 6 call the ORACLE'S OWN functions rather than reimplementing them — + * `pn_qwen35moe_beta_sigmoid`, `pn_qwen35moe_decay_gate`, `pn_qwen35moe_l2_norm`, + * `pn_qwen35moe_gated_norm`. That is what makes a disagreement between this arm and the CPU + * one attributable to the two descriptors and to nothing else, which is the whole point of + * the gate this file exists to enable. + * + * --------------------------------------------------------------------------- + * WHY 4 AND 6 ARE STILL ON THE HOST, AND WHAT THAT COSTS + * --------------------------------------------------------------------------- + * Both are per-head norms at `S/32` = 4 tile columns against the model's 128, and a reduce + * residency is placed for exactly ONE width. `pn_tt_head_norm.c` is that second residency + * and it is ARMED — but they still cannot move, and the reason is not the width. + * + * **MEASURED 2026-08-19, `PN_TT_GDN_NORMCHECK=1`, and it settles two things the scoping got + * wrong in opposite directions.** The LAYOUT is free: a reduce with `row_blocks = heads` + * over a `[1, heads*S]` strip is one norm per head with no reshape at all, because the + * reader indexes row block `b` at tiles `b*tile_cols`. The PRECISION is not: the shipped + * reduce family is bf16, and on Ornith's real per-layer data the card scores 8.599e-03 on + * the q/k L2 norm and 1.283e-02 on the gated norm against these very functions, 9x and 13x + * past the 1e-3 gate. The lever is the CB's format — the reduce alone is 6.1e-03 of it — + * so the norms move when there is a **Float32 `reduce` program**, and not before. + * + * --------------------------------------------------------------------------- + * AND THE NORMS DID NOT MOVE ALONE — THE PLAN ITEM WAS NOT SEPARABLE + * --------------------------------------------------------------------------- + * Moving them removes `conv_out` (32,768 B a layer) and `y` (16,384 B) from the readback, + * which takes this arm from 2630.09 KiB/token back to the control's 1478.09 — PARITY, not a + * gain. The gain is the OTHER 49,408 B a layer, and that needs `ssm_qkv` and `ssm_z` to stop + * being downloaded too. **That is `qwen35moe_gdn_strip_tt.c`**, and it took a FORMAT bridge + * (two render residencies, `pn_tt_render.h`) and a LAYOUT change (PHASE DELTA reads head-major + * row-0 strips, so conv_out IS the delta input) rather than the bf16 GDN operand this comment + * used to predict — a mixed CB table demotes the whole program's fp32 unpack, so the GDN op + * cannot read a bf16 strip at any price. + * + * **THIS FILE IS THE HOST-MEDIATED ARM AND IT IS STILL SLOWER, ON PURPOSE.** Because the L2 + * norm sits between the two phases, `conv_out` has to come back (32,768 B a layer) and `y` + * has to come back (16,384 B), on top of the 49,408 B of projections. So it roughly DOUBLES + * the readback — ~1.18 MB a token more — to remove 0.0421 s a token of host delta-rule + * arithmetic, and at the measured 5.26 MB/s that is a net LOSS of ~0.18 s a token. It is the + * "conv1d alone is 1.00x" row of the split-point table from the other side, and it is why + * this path is opt-in: it is the CORRECTNESS vehicle for the on-card gate — the arm that + * calls the ORACLE'S OWN functions around the two descriptors, so a disagreement is + * attributable to them and to nothing else. The speed rung is the strip bridge beside it. + * + * @status RUN ON SILICON 2026-08-18 and NOT YET CORRECT. Opt-in with + * `PN_TT_ONCARD=norm,swiglu,residual,topk,gdn`; `PN_TT_GDN_PARITY=1` runs the oracle + * beside it and prices the disagreement (`qwen35moe_gdn_parity_tt.c`). + * @issues The six host steps around the two descriptors are the oracle's own and are not in + * question. + * + * AT POSITION 0 BOTH PHASES ARE NOW INSIDE OR NEAR THE GATE. PHASE CONV is 1.6e-04 to + * 8.7e-04 across all 24 SSM layers; PHASE DELTA agrees with its closed form on all 32 + * heads at 2.7e-03 to 7.0e-03, uniformly, with a systematic `card/want` of 0.9955 to + * 0.9977 that is Tf32 truncation over the delta rule's six unpack stages and NOT a + * layout fault. `gated` at layer 0 is 6.636e-04. + * + * THE 48-TOKEN `cmp` PASSES AS OF 2026-08-19 — 189 B, md5 80fc6259, IDENTICAL to the + * oracle and to the host-recurrence control, at 0.99 tok/s against the control's 1.44 + * (expected: the two per-head norms have not moved yet). The defect that had been + * failing it was the conv window's NEWEST plane, wrong on every CONV descriptor but + * the first; it was CB_IN carrying two producers across the two phases and it is + * fixed in foundry. `conv` is 8.677e-04 at position 0 and 1.098e-03 to 1.790e-03 + * after it, and what is left in both phases is Tf32 truncation over the unpack + * stages, not layout. + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include +#include + +#include "../accel/tt/pn_tt_gdn.h" +#include "../accel/tt/pn_tt_gdnc.h" + +int pn_qwen35_tt_gdn_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg) +{ + pn_tt_gdn_shape shape; + int layer; + int ssm_layers = 0; + + if (!g || !g->device || !m || !cfg) + return -1; + if (!pn_tt_device_oncard_requested(g->device, PN_TT_ONCARD_GDN)) + return 0; + + for (layer = 0; layer < cfg->n_layer; layer++) { + if (pn_qwen35moe_model_layer_type(m, layer) != PN_QWEN35MOE_LAYER_ATTN) + ssm_layers++; + } + if (ssm_layers == 0) + return 0; + + shape.state_size = (uint32_t)cfg->ssm_state; + shape.v_heads = (uint32_t)cfg->ssm_dt_rank; + shape.k_heads = (uint32_t)cfg->ssm_n_group; + shape.conv_dim = (uint32_t)cfg->conv_dim; + shape.conv_taps = (uint32_t)cfg->ssm_conv; + shape.layer_count = (uint32_t)ssm_layers; + if (pn_tt_device_arm_gdn(g->device, &shape) != 0) + return -1; + if (!pn_tt_gdn_is_armed(g->device)) + return 0; + /* THE CHUNKWISE FAMILY ARMS BEHIND THE DECODE ONE AND ONLY BEHIND IT — it advances the + * SAME conv window and the SAME recurrent state, so there is nothing for it to carry + * without them. Default OFF; `PN_TT_PREFILL_GDNC=1` asks for it. It is never fatal: an + * unarmed chunkwise family leaves the prefill on the per-position path. */ + if (pn_tt_device_arm_gdnc(g->device) != 0) + return -1; + + /* The conv weight is a per-layer CONSTANT and the transpose into tap-major planes is a + * load-time cost, not a per-token one. Doing it per token would put a 32 KiB gather and a + * 4 MiB write inside a 24-layer loop. */ + for (layer = 0; layer < cfg->n_layer; layer++) { + const pn_qwen35moe_ssm_layer *l; + int idx; + + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) + continue; + idx = pn_qwen35moe_model_layer_index(m, layer); + l = pn_qwen35moe_model_ssm_layer(m, idx); + if (!l || !l->ssm_conv1d || + pn_tt_gdn_conv_weight_upload(g->device, (uint32_t)idx, l->ssm_conv1d) != 0) { + fprintf(stderr, "qwen35_tt: SSM layer %d's conv weight did not stage — the GDN " + "block stays on the host\n", layer); + return -1; + } + /* THE SAME TRANSPOSE AT `valid_rows` 32 INSTEAD OF 1, into the chunkwise family's own + * constant, plus the band tiles behind it. Two copies of one weight, and the second + * exists so the SHIPPED decode plane is not touched by prefill work at all. */ + if (pn_tt_gdnc_is_armed(g->device) && + pn_tt_gdnc_conv_weight_upload(g->device, (uint32_t)idx, l->ssm_conv1d) != 0) { + fprintf(stderr, "qwen35_tt: SSM layer %d's ROW-REPLICATED conv weight did not " + "stage — the chunkwise mixers are unusable and prefill stays " + "per-position\n", layer); + pn_tt_gdnc_release(g->device); + } + } + + g->gdn_conv_out = calloc((size_t)cfg->conv_dim, sizeof(float)); + g->gdn_qn = calloc((size_t)cfg->ssm_state * (size_t)cfg->ssm_n_group, sizeof(float)); + g->gdn_kn = calloc((size_t)cfg->ssm_state * (size_t)cfg->ssm_n_group, sizeof(float)); + g->gdn_y = calloc((size_t)cfg->ssm_state * (size_t)cfg->ssm_dt_rank, sizeof(float)); + g->gdn_decay = calloc((size_t)cfg->ssm_dt_rank, sizeof(float)); + g->gdn_beta = calloc((size_t)cfg->ssm_dt_rank, sizeof(float)); + if (!g->gdn_conv_out || !g->gdn_qn || !g->gdn_kn || !g->gdn_y || !g->gdn_decay || + !g->gdn_beta) + return -1; + + g->gdn_on_card = true; + /* The PROBE, not the path: it needs `gdn_conv_out`/`gdn_y` allocated above, and it is + * armed last so a failure there leaves the arm itself intact. */ + if (pn_qwen35_tt_gdn_normcheck_arm(g, cfg) != 0) + fprintf(stderr, "qwen35_tt: the per-head norm probe did not arm; the recurrence is " + "unaffected\n"); + fprintf(stderr, "qwen35_tt: the Gated-Delta-Net block's two heavy phases run ON THE CARD " + "for %d SSM layers — two descriptors a layer, state resident, `cmp` " + "IDENTICAL to the oracle. WITHOUT PN_TT_GDN_STRIP the two per-head norms " + "stay on the host, so `conv_out` and `y` cross as well and this arm is " + "SLOWER than the host recurrence: it is the correctness vehicle, not a " + "speed rung.\n", ssm_layers); + return 0; +} + +void pn_qwen35_tt_gdn_release(pn_qwen35_tt *g) +{ + if (!g) + return; + pn_qwen35_tt_gdn_normcheck_release(g); + free(g->gdn_conv_out); + free(g->gdn_qn); + free(g->gdn_kn); + free(g->gdn_y); + free(g->gdn_decay); + free(g->gdn_beta); + g->gdn_conv_out = g->gdn_qn = g->gdn_kn = g->gdn_y = NULL; + g->gdn_decay = g->gdn_beta = NULL; + g->gdn_on_card = false; +} + +/** + * The host half of step 4: split the convolved stream and L2-normalise each q and k HEAD. + * + * The GQA repeat the oracle does here is deliberately NOT done: `q` and `k` leave this + * function with ONE row per k-head, which is what the DELTA reader takes, and the repeat is + * its `vh % k_heads`. Materialising it would double both streams — the 1.33x-worse row of + * the split-point table. + */ +static void gdn_split_and_normalise(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg) +{ + const int state = cfg->ssm_state; + const int k_heads = cfg->ssm_n_group; + const size_t head_bytes = (size_t)state * sizeof(float); + const float *q_src = g->gdn_conv_out; + const float *k_src = g->gdn_conv_out + (size_t)state * (size_t)k_heads; + int head; + + for (head = 0; head < k_heads; head++) { + memcpy(g->gdn_qn + (size_t)head * state, q_src + (size_t)head * state, head_bytes); + memcpy(g->gdn_kn + (size_t)head * state, k_src + (size_t)head * state, head_bytes); + pn_qwen35moe_l2_norm(g->gdn_qn + (size_t)head * state, state, cfg->rms_norm_eps); + pn_qwen35moe_l2_norm(g->gdn_kn + (size_t)head * state, state, cfg->rms_norm_eps); + } +} + +int pn_qwen35_tt_gdn_recurrence(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const float *qkv, const float *z, const float *beta_raw, + const float *alpha, float *gated) +{ + const int v_heads = cfg->ssm_dt_rank; + const int key_dim = cfg->ssm_state * cfg->ssm_n_group; + int head; + + if (!g || !l || !cfg || !qkv || !z || !beta_raw || !alpha || !gated) + return -1; + if (!g->gdn_on_card || ssm_index < 0) + return -1; + + /* 1 and 2, the oracle's own, on the oracle's own inputs. `decay = exp(g)` is applied + * HERE rather than on the card because the card has no `log` and therefore no `pow`; the + * op takes the answer as 32 fp32 scalars, 128 B a layer. */ + pn_qwen35moe_beta_sigmoid(beta_raw, v_heads, g->gdn_beta); + pn_qwen35moe_decay_gate(l, alpha, v_heads, g->gdn_decay); + for (head = 0; head < v_heads; head++) + g->gdn_decay[head] = expf(g->gdn_decay[head]); + + /* 3. PHASE CONV — conv1d + SiLU over the whole stream, and the window rolls on the card. */ + if (pn_tt_gdn_conv(g->device, (uint32_t)ssm_index, qkv, g->gdn_conv_out) != 0) + return -1; + + /* 4. split and L2-normalise, WITHOUT the repeat. */ + gdn_split_and_normalise(g, cfg); + + /* 5. PHASE DELTA — v is the third slice of the convolved stream and crosses untouched. */ + if (pn_tt_gdn_delta(g->device, (uint32_t)ssm_index, g->gdn_qn, g->gdn_kn, + g->gdn_conv_out + 2 * key_dim, g->gdn_decay, g->gdn_beta, + g->gdn_y) != 0) + return -1; + + /* 6. the gated norm, the oracle's own. */ + pn_qwen35moe_gated_norm(l, cfg, g->gdn_y, z, gated); + + /* AFTER 4 and 6, because it scores the card's answer against what they just wrote. */ + (void)pn_qwen35_tt_gdn_normcheck_layer(g, l, cfg, ssm_index, z, gated); + return 0; +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_internal.h b/src/models/qwen35moe_internal.h new file mode 100644 index 0000000..81cfb08 --- /dev/null +++ b/src/models/qwen35moe_internal.h @@ -0,0 +1,809 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_internal.h + * @brief Internal (cross-file) definitions for the Qwen3.5-MoE port: config, + * per-layer type map, block structs, and the per-block forward kernels + * shared between qwen35moe.c (load/orchestrate) and the compute files + * (qwen35moe_attn.c, later qwen35moe_ssm.c / qwen35moe_moe.c). + * + * @status A6: dual-state struct (per-layer SSM state + KV cache + per-token + * scratch arena) declared here; forward orchestration in + * qwen35moe_forward.c. A5: shared-expert MoE FFN kernel (qwen35moe_moe.c). + * A4: SSM (Gated-Delta-Net) block kernel + per-layer SSM state declared + * here; A3 full-attention block kernel declared here. Structs shared so + * each compute block lives in its own file (whale prevention). + * @issues None + * @todo None + */ + +#ifndef PN_QWEN35MOE_INTERNAL_H +#define PN_QWEN35MOE_INTERNAL_H + +#include "../core/qweight.h" +#include "../formats/gguf.h" +#include "../memory/arena.h" + +/* Forward declaration (full type from threadpool/threadpool.h under USE_THREADS; + * NULL-able so non-threaded builds never need the definition). */ +struct fw_pool; + +/* ============================================================ + * The matvec seam + * ============================================================ + * + * EVERY matmul this architecture does goes through qwen35moe_mv(), and an + * accelerator backend replaces exactly that and nothing else. There are only + * thirteen call sites in the whole family, so this seam is small enough to + * read in one sitting — which is the point: an accelerator arm and the CPU arm + * then differ in the matmuls ALONE, and the SSM recurrence, the conv1d, the + * attention softmax and every norm are literally the same code executing the + * same arithmetic in both. A wrong answer therefore has one candidate family + * rather than two, which is what makes the first card run of a hybrid model + * diagnosable at all. + * + * `fn` returns 0 when it did the work. Anything else means it did NOT, and the + * host matvec runs instead. A backend that means to serve a weight must never + * use that path silently: it is there so an UNBOUND weight is still computed + * correctly, and every backend here counts its uses, because "the card was + * quietly not used" and "the card was used" have produced identical bytes in + * this project before. + */ +typedef struct pn_qwen35moe_mv { + int (*fn)(void *ctx, const pn_qweight *w, const float *x, float *out); + void *ctx; +} pn_qwen35moe_mv; + +/* ============================================================ + * Config + * ============================================================ */ + +/* Hybrid Qwen3.5-MoE dimensions parsed from qwen35moe.* GGUF keys. The engine + * pn_config carries the shared subset; this struct carries the SSM/MoE/hybrid + * dims that pn_config has no field for. */ +typedef struct { + int n_layer; /* block_count (40) */ + int full_attention_interval; /* attn layer when (i+1) % interval == 0 (4) */ + int n_embd; /* embedding_length (2048) */ + int n_head; /* attention.head_count (16) */ + int n_head_kv; /* attention.head_count_kv (2) */ + int head_dim; /* attention.key_length / value_length (256) */ + int rope_dim; /* rope.dimension_count — partial RoPE (64) */ + int rope_sections[4]; /* rope.dimension_sections (multi-section RoPE) */ + float rope_theta; /* rope.freq_base (1e7) */ + float rms_norm_eps; /* attention.layer_norm_rms_epsilon (1e-6) */ + int n_expert; /* expert_count (256; 0 for dense Bonsai) */ + int n_expert_used; /* expert_used_count (8; 0 for dense Bonsai) */ + int expert_ff; /* expert_feed_forward_length (512) */ + int shared_ff; /* expert_shared_feed_forward_length (512) */ + float expert_weights_scale; /* expert_weights_scale (routed weight scale, default 1.0) */ + /* Dense-FFN variant (PrismML Bonsai-27B): general.architecture=qwen35, no + * MoE. When dense_ffn is set the per-layer FFN is a single SwiGLU + * (ffn_gate/up/down, dim = ffn_length) loaded into the shared-expert slots; + * the router / routed experts / shared scalar gate are skipped. Nex-N2-mini + * (qwen35moe) leaves dense_ffn=0 and runs the routed shared-expert MoE. */ + int dense_ffn; /* 1 = dense SwiGLU FFN (Bonsai), 0 = MoE (Nex-N2) */ + int ffn_length; /* feed_forward_length (17408) when dense_ffn */ + int ssm_state; /* ssm.state_size (128) */ + int ssm_n_group; /* ssm.group_count (16) */ + int ssm_dt_rank; /* ssm.time_step_rank (32) */ + int ssm_d_inner; /* ssm.inner_size (4096) */ + int ssm_conv; /* ssm.conv_kernel (4) */ + int conv_dim; /* derived: key*2 + value = 8192 */ + int n_vocab; /* from token_embd shape */ + int n_ctx; /* context_length */ + int ctx_cap; /* KV Phase 1: per-model per-session context cap + * in tokens (KV ring window). 0 = unset → fall + * back to QWEN35_MAX_SEQ / PN_QWEN35MOE_MAX_SEQ. + * Mirrors engine pn_config.ctx_cap; lets ornith + * run 40K without moving saber's window. */ + /* Where this model's matmuls run. Zeroed by the loader's calloc, which is + * exactly the CPU arm; a backend installs itself at upload time. It lives + * on the config rather than on the model because the config is the one + * thing already threaded through every block function. */ + pn_qwen35moe_mv mv; +} pn_qwen35moe_config; + +/* The one matvec every block in this family calls. See the seam note above. */ +static inline void qwen35moe_mv(const pn_qwen35moe_config *cfg, const pn_qweight *w, + const float *x, float *out) +{ + if (cfg && cfg->mv.fn && cfg->mv.fn(cfg->mv.ctx, w, x, out) == 0) + return; + pn_qmv(w, x, out); +} + +/* Per-layer type: which compute block this layer runs. */ +typedef enum { + PN_QWEN35MOE_LAYER_SSM = 0, /* Gated-Delta-Net SSM */ + PN_QWEN35MOE_LAYER_ATTN = 1, /* sparse full-attention */ +} pn_qwen35moe_layer_type; + +/* ============================================================ + * Block structs + * ============================================================ */ + +/* Shared-expert MoE block — present on every layer. Routed experts are sliced + * from merged 3D tensors (zero-copy mmap); the shared expert is its own SwiGLU + * with a sigmoid scalar gate. */ +typedef struct { + pn_qweight ffn_gate_inp; /* router logits [n_expert, n_embd] */ + pn_qweight *gate_exps; /* per-expert gate [n_expert] (sliced) */ + pn_qweight *up_exps; /* per-expert up [n_expert] (sliced) */ + pn_qweight *down_exps; /* per-expert down [n_expert] (sliced) */ + pn_qweight ffn_gate_shexp; /* shared-expert gate */ + pn_qweight ffn_up_shexp; /* shared-expert up */ + pn_qweight ffn_down_shexp; /* shared-expert down */ + pn_qweight ffn_gate_inp_shexp; /* shared-expert scalar gate [n_embd] */ +} pn_qwen35moe_moe; + +/* SSM (Gated-Delta-Net) layer: norms + 4 projections + conv + recurrence params + * + gated-norm + output proj, plus the shared MoE FFN. */ +typedef struct { + float *attn_norm; /* pre-block RMSNorm [n_embd] (F32, owned) */ + pn_qweight attn_qkv; /* fused q|k|v stream [conv_dim, n_embd] */ + pn_qweight attn_gate; /* output gate z [ssm_d_inner, n_embd] */ + float *ssm_conv1d; /* causal conv1d weight [conv, conv_dim] (F32, owned) */ + float *ssm_a; /* decay A (= -exp(A_log)) [ssm_dt_rank] (F32, owned) */ + pn_qweight ssm_alpha; /* alpha proj [ssm_dt_rank, n_embd] */ + pn_qweight ssm_beta; /* beta proj [ssm_dt_rank, n_embd] */ + float *ssm_dt; /* dt bias [ssm_dt_rank] (F32, owned) */ + float *ssm_norm; /* gated RMSNorm weight [ssm_state] (F32, owned) */ + pn_qweight ssm_out; /* output proj [n_embd, ssm_d_inner] */ + float *post_attention_norm; /* pre-MoE RMSNorm [n_embd] (F32, owned) */ + pn_qwen35moe_moe moe; +} pn_qwen35moe_ssm_layer; + +/* Full-attention layer: norms + Q/K/V projections + QK-RMSNorm + output proj, + * plus the shared MoE FFN. */ +typedef struct { + float *attn_norm; /* pre-block RMSNorm [n_embd] (F32, owned) */ + pn_qweight attn_q; /* joint Q+gate [2*head_dim*n_head, n_embd] */ + pn_qweight attn_k; /* K proj [head_dim*n_head_kv, n_embd] */ + pn_qweight attn_v; /* V proj [head_dim*n_head_kv, n_embd] */ + float *attn_q_norm; /* per-head QK-RMSNorm on Q [head_dim] (F32, owned) */ + float *attn_k_norm; /* per-head QK-RMSNorm on K [head_dim] (F32, owned) */ + pn_qweight attn_output; /* output proj [n_embd, head_dim*n_head] */ + float *post_attention_norm; /* pre-MoE RMSNorm [n_embd] (F32, owned) */ + pn_qwen35moe_moe moe; +} pn_qwen35moe_attn_layer; + +/* ============================================================ + * Full-attention block (A3) — implemented in qwen35moe_attn.c + * + * Per-layer KV cache (the only growing state for attention layers). One of + * these exists per attention layer in the model. Layout is per-head-major, + * matching llama.c's pn_llama_kv_cache: [n_head_kv][max_seq][head_dim]. + * ============================================================ */ + +typedef struct { + float *k; /* [n_head_kv * max_seq * head_dim] (owned) — fp32 mode */ + float *v; /* [n_head_kv * max_seq * head_dim] (owned) — fp32 mode */ + /* TurboQuant Q4 KV (QWEN35_KV_Q4): 4-bit cache, ~4-7x smaller. When q4 is + * set these hold the cache as [n_head_kv][max_seq][pn_kvq4_row_bytes] + * ggml-Q4_0 rows and k/v above are NULL. See qwen35moe_kv_q4.h. */ + unsigned char *kq; + unsigned char *vq; + /* fp16 sink + recent-window shadow (q4 mode only): the attention sink and + * the last W tokens are kept at fp16 so greedy argmax stops flipping on + * near-ties. Sized to PN_KVQ4_SINKMAX / PN_KVQ4_WMAX; runtime W/sink pick + * how much is live. See qwen35moe_kv_q4.h. */ + uint16_t *k_sink, *v_sink; /* [n_head_kv][PN_KVQ4_SINKMAX][head_dim] */ + uint16_t *k_win, *v_win; /* [n_head_kv][PN_KVQ4_WMAX][head_dim] ring */ + int q4; /* 1 = Q4 cache active (kq/vq), 0 = fp32 (k/v) */ + int n_head_kv; + int head_dim; + int max_seq; + /* B2 — deferred KV host mirror: set when the GPU ring has rows that have + * not been written back to this struct's k/v (or kq/vq) host buffers. + * The per-decode-token D2H is deferred; the mirror is refreshed lazily by + * pn_qwen35moe_gpu_state_to_host() before the GPU buffers are reused for a + * different session (preserve_live in prefix_cache.c). + * 0 = host mirror current (post-prefill, post-evict-sync, or no GPU path). + * 1 = GPU ring has new rows not yet in the host mirror. */ + int kv_mirror_stale; + /* Phase 10b — content identity generation. GPU ring watermarks + * (kv_host_id / kv_slot_host_id) key on this struct's buffer POINTER, + * which survives pn_state_reset and snapshot-restore on a reused serve + * slot: same pointer, different rows. In lazy-mirror mode the bind path + * no longer blanket-invalidates those watermarks, so the pointer alone + * is not enough. content_gen is seeded from a global counter at init + * (so a freed state's recycled buffer address can never alias) and + * bumped by state_reset and snapshot-restore; a watermark is valid only + * when BOTH the pointer and the generation match. */ + unsigned content_gen; +} pn_qwen35moe_kv; + +/* Upper bound on head_dim for stack scratch (mini 256, Pro 128). */ +#define PN_QWEN35MOE_ATTN_HEAD_DIM_MAX 256 + +/* Runtime QWEN35_KV_Q4 gate (default OFF; keeps the fp32 KV baseline for A/B). + * Cached on first read. Defined in qwen35moe_forward.c. */ +int pn_qwen35moe_kv_q4_on(void); + +/* Phase 10b: next value of the global kv content-generation counter (see + * pn_qwen35moe_kv.content_gen). Defined in qwen35moe_forward.c. */ +unsigned pn_qwen35moe_kv_next_content_gen(void); + +/** + * Run one full-attention block (layers where (i+1)%4==0). + * + * Op order (matches the Qwen3-Next reference + llama.cpp build_layer_attn): + * 1. joint Q+gate projection (attn_q) -> split per head: Q = first head_dim, + * gate z = second head_dim. + * 2. QK-RMSNorm: Q via attn_q_norm, K via attn_k_norm (per-head, pre-RoPE). + * 3. K/V projections; K/V are n_head_kv heads. + * 4. partial NEOX RoPE on the first rope_dim dims of every head (Q and K), + * remaining dims pass through. Multi-section RoPE collapses to standard + * NEOX for text (all sections share the linear token position). + * 5. GQA attention: each KV head broadcast across n_head/n_head_kv Q heads, + * scale 1/sqrt(head_dim). Current token's K/V appended to the cache first. + * 6. output gate: attn_out *= sigmoid(z). + * 7. output projection (attn_output). + * + * @param layer Attention-layer weights for this block. + * @param cfg Model config (dims, rope params, eps). + * @param x attn_norm'd input hidden state [n_embd] (read-only). + * @param kv Per-layer KV cache for this attention layer (updated in place). + * @param pos Absolute token position (0-based) for RoPE + cache slot. + * @param arena Per-token scratch arena (reset by the caller, not here). + * @param out Output buffer [n_embd]; residual add is the caller's job. + * @param pool Optional thread pool for per-head parallel attention (or NULL). + * @return 0 on success, negative on failure (allocation). + * + * Thread-safe: no (mutates kv and arena) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_attn_block(const pn_qwen35moe_attn_layer *layer, + const pn_qwen35moe_config *cfg, + const float *x, + pn_qwen35moe_kv *kv, + int pos, + pn_arena *arena, + float *out, + struct fw_pool *pool); + +/** + * Steps 2-6 of the block above, with the projections already done by the caller. + * + * The seam an accelerator backend uses: it runs attn_q / attn_k / attn_v and attn_output on + * its own device and calls this for the arithmetic between them, so the device arm and the + * CPU oracle share ONE implementation of the Q/gate de-interleave, the QK-RMSNorm, the + * partial NEOX RoPE, the KV append, the GQA softmax and the sigmoid output gate. A wrong + * answer from the device arm therefore has the matmuls as its only candidate. + * + * @param qg attn_q's output, `layer->attn_q.rows` floats. Q and its output gate are + * INTERLEAVED PER HEAD — [h0 Q(head_dim), h0 gate(head_dim), h1 Q, ...]. + * @param k attn_k's output, `n_head_kv * head_dim` floats. MUTATED in place: normed, + * rotated, then appended to the cache. + * @param v attn_v's output, `n_head_kv * head_dim` floats. Appended to the cache. + * @param attn_out receives `n_head * head_dim` floats — attn_output's input. + * @return 0 on success, negative on failure (allocation). + * + * Thread-safe: no (mutates kv and arena) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_attn_core(const pn_qwen35moe_attn_layer *layer, + const pn_qwen35moe_config *cfg, + const float *qg, float *k, float *v, + pn_qwen35moe_kv *kv, + int pos, + pn_arena *arena, + float *attn_out, + struct fw_pool *pool); + +/* ============================================================ + * SSM (Gated-Delta-Net) block (A4) — implemented in qwen35moe_ssm.c + * + * Per-layer recurrent state. Unlike the attention KV cache, this is a fixed-size + * state that is updated in place every token (no growth with sequence length). + * One of these exists per SSM layer in the model. + * + * conv_state — the last (ssm_conv - 1) timesteps of the full conv_dim-channel + * q|k|v stream, used to prime the causal conv1d for the next token. Layout is + * channel-major: conv_state[c * (ssm_conv-1) + t], t=0 oldest .. (conv-2) + * newest. Total (ssm_conv-1) * conv_dim floats (e.g. 3 * 8192 = 24576). + * + * ssm_state — the recurrent matrix state S, one [ssm_state x ssm_state] matrix + * per V-head (ssm_dt_rank heads). Stored TRANSPOSED to match the recurrence's + * access pattern (and llama.cpp's gated_delta_net layout): for head h, key dim + * i, value dim j, + * ssm_state[ h*ssm_state*ssm_state + j*ssm_state + i ] == S_h[i][j]. + * So row j (contiguous, length ssm_state) holds column j of S, indexed by the + * key dim i — making the per-token dot products over i unit-stride. Total + * ssm_state * ssm_state * ssm_dt_rank floats (e.g. 128*128*32 = 524288). + * ============================================================ */ + +typedef struct { + float *conv_state; /* [(ssm_conv-1) * conv_dim] (owned) */ + float *ssm_state; /* [ssm_state * ssm_state * ssm_dt_rank] (owned, transposed) */ + int conv_dim; + int conv_minus_1; /* ssm_conv - 1 (conv_state timesteps) */ + int state_size; /* ssm_state (per-head matrix is state_size x state_size) */ + int n_v_heads; /* ssm_dt_rank */ +} pn_qwen35moe_ssm_state; + +/** + * Run one Gated-Delta-Net SSM block (layers where (i+1)%4 != 0). + * + * Op order (matches docs/architecture/qwen35moe.md SSM section + llama.cpp + * build_layer_attn_linear / gated_delta_net autoregressive kernel): + * 1. Projections from the attn_norm'd input: attn_qkv -> fused q|k|v + * [conv_dim]; attn_gate -> z [ssm_d_inner]; ssm_beta -> beta[n_v_heads] + * then beta = sigmoid(beta); ssm_alpha -> alpha[n_v_heads]. + * 2. Decay gate (scalar per V-head): g = ssm_a * softplus(alpha + ssm_dt). + * ssm_a is pre-stored as -exp(A_log) (negative) -> plain multiply. + * 3. Causal conv1d (k=ssm_conv) over the full conv_dim q|k|v stream primed + * with conv_state -> SiLU. Split q/k [state*n_k_heads], v [state*n_v_heads]; + * L2-normalize q,k over head dim; GQA-repeat q,k from n_k_heads->n_v_heads. + * 3b. conv_state is updated in place to the newest (ssm_conv-1) timesteps. + * 4. Per-token recurrence (per V-head, read-AFTER-write, 1/sqrt(state) scale + * applied once); ssm_state updated in place. + * 5. Output: gated = RMSNorm(y, ssm_norm) * silu(z); cur = ssm_out * gated. + * + * @param layer SSM-layer weights for this block. + * @param cfg Model config (dims, eps). + * @param x attn_norm'd input hidden state [n_embd] (read-only). + * @param state Per-layer recurrent SSM state (conv + ssm; updated in place). + * @param arena Per-token scratch arena (reset by the caller, not here). + * @param out Output buffer [n_embd]; residual add is the caller's job. + * @param pool Optional thread pool for per-head parallel recurrence (or NULL). + * @return 0 on success, negative on failure (allocation/validation). + * + * Thread-safe: no (mutates state and arena) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_ssm_block(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, + const float *x, + pn_qwen35moe_ssm_state *state, + pn_arena *arena, + float *out, + struct fw_pool *pool); + +/** + * Steps 2-5 of the block above, with the four input projections already done by the caller + * and the output projection left to it. + * + * The accelerator seam, and the reason it exists is a control rather than tidiness: the + * conv1d, the L2 norms, the interleaved GQA repeat, the delta-rule recurrence and the gated + * norm are ONE implementation shared by the CPU oracle and every device arm, so a device + * arm that disagrees with the oracle disagrees about the MATMULS and about nothing else. + * + * `state` is mutated in place — both the conv window and the transposed recurrent matrix — + * so a device arm that calls this is carrying the same state the oracle carries, and a + * comparison at token 48 is a statement about the STATE rather than about one step. + * + * @param qkv attn_qkv's output, `conv_dim` floats (the fused q|k|v stream) + * @param z attn_gate's output, `ssm_state * ssm_dt_rank` floats + * @param beta_raw ssm_beta's output, `ssm_dt_rank` floats, BEFORE the sigmoid + * @param alpha ssm_alpha's output, `ssm_dt_rank` floats + * @param gated receives `ssm_state * ssm_dt_rank` floats — ssm_out's input + * @return 0 on success, negative on failure (allocation/validation). + * + * Thread-safe: no (mutates state and arena) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_ssm_recurrence(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, + const float *qkv, const float *z, + const float *beta_raw, const float *alpha, + pn_qwen35moe_ssm_state *state, + pn_arena *arena, + float *gated, + struct fw_pool *pool); + +/* ------------------------------------------------------------ + * The three pieces of the recurrence that are NOT the two card phases + * ------------------------------------------------------------ + * The Gated-Delta-Net op on the card owns `conv1d + SiLU` and the delta rule. The decay gate + * (a `softplus`, which the SFPU has no `log` for), the q/k L2 norm and the output gated norm + * stay on the host, and `qwen35moe_gdn_tt.c` runs exactly these three around the two + * descriptors. They are exported rather than copied for the same reason + * `pn_qwen35moe_ssm_recurrence` exists: a second implementation of an L2 norm is a second + * answer, and the oracle is the one that wins. + */ + +/** `g[h] = ssm_a[h] * softplus(alpha[h] + ssm_dt[h])`, the pre-exp decay gate per V-head. */ +void pn_qwen35moe_decay_gate(const pn_qwen35moe_ssm_layer *layer, const float *alpha, + int n_v_heads, float *g_out); + +/** `x[d] /= max(sqrt(sum x^2), eps)`, in place over one head. L2, NOT RMS: no mean factor, + * and the clamp is `max(.,eps)` rather than `+eps` under the root, matching ggml_l2_norm. + * The sum accumulates in double on purpose — this feeds the recurrence directly. */ +void pn_qwen35moe_l2_norm(float *data, int head_dim, float eps); + +/** `gated = RMSNorm(y, ssm_norm) * silu(z)`, per V-head over `ssm_state` elements. */ +void pn_qwen35moe_gated_norm(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, const float *y, const float *z, + float *gated); + +/** `beta = sigmoid(beta_raw)` per V-head — the one line of the recurrence that is neither a + * card phase nor a norm, exported so the two arms cannot spell it differently. */ +void pn_qwen35moe_beta_sigmoid(const float *beta_raw, int n_v_heads, float *beta_out); + +/** + * `conv_out = SiLU(conv1d(qkv))` over the whole `conv_dim` stream, and ROLLS `state->conv_state`. + * + * PHASE CONV's oracle, exported for one reason: the on-card GDN parity arm needs the host's + * answer to the SAME sub-step the card ran, and computing it any other way would compare the + * card against a second implementation instead of against the oracle. + * + * **It MUTATES the window.** A caller running it for comparison and then running the whole + * recurrence over the same state must restore `state->conv_state` in between, or the window + * rolls twice for one token. + * + * @param qkv `conv_dim` floats, the fused projection + * @param state the layer's carried state; `conv_state` is read and rolled + * @param conv_out `conv_dim` floats, written + */ +void pn_qwen35moe_conv1d_silu(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, const float *qkv, + pn_qwen35moe_ssm_state *state, float *conv_out); + +/* ============================================================ + * Shared-expert MoE FFN (A5) — implemented in qwen35moe_moe.c + * + * Runs on EVERY layer (both SSM and attention) on the post_attention_norm'd + * hidden state. Extends the Mixtral-style routed-MoE pattern with two + * qwen35moe specifics: softmax + norm_topk_prob gating, and an always-on + * sigmoid-gated shared expert. + * ============================================================ */ + +/** + * Run one shared-expert MoE FFN block (every layer). + * + * Op order (matches docs/architecture/qwen35moe.md "Shared-expert MoE FFN"): + * 1. Routed: logits = ffn_gate_inp . x [n_expert] -> softmax -> top-k + * (n_expert_used) -> norm_topk_prob (renormalize the selected weights to + * sum to 1) -> scale by expert_weights_scale. For each selected expert e, + * SwiGLU_e(x) = (silu(gate_e . x) * (up_e . x)) . down_e. + * moe_out = sum_e weight_e * SwiGLU_e(x). + * 2. Shared (always on): shexp = (silu(gate_shexp . x) * (up_shexp . x)) . + * down_shexp; sgate = sigmoid(ffn_gate_inp_shexp . x) [1 scalar]; + * shexp *= sgate. + * 3. Combine: out = moe_out + shexp. (Caller adds the residual.) + * + * @param moe MoE-block weights for this layer (router + experts + shared). + * @param cfg Model config (n_expert, n_expert_used, ffs, scale). + * @param x post_attention_norm'd hidden state [n_embd] (read-only). + * @param arena Per-token scratch arena (reset by the caller, not here). + * @param out Output buffer [n_embd]; residual add is the caller's job. + * @param pool Optional thread pool (unused for now; accepted for symmetry). + * @return 0 on success, negative on failure (allocation/validation). + * + * Thread-safe: no (mutates arena) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_moe_block(const pn_qwen35moe_moe *moe, + const pn_qwen35moe_config *cfg, + const float *x, + pn_arena *arena, + float *out, + struct fw_pool *pool); + +/* ============================================================ + * Dual state + forward orchestration (A6) — qwen35moe_forward.c + * + * Mirrors rwkvx.c's dual top-level state: parallel per-type arrays (one + * pn_qwen35moe_ssm_state per SSM layer, one pn_qwen35moe_kv per attention + * layer), indexed by the model's per-type block_index, plus a per-token + * scratch arena and two ping-pong hidden-state buffers. + * ============================================================ */ + +/* Upper bound on the sparse top-k survivor count tracked per state below. + * Must be >= foundry's FC_TOPK_MAX (checked with a static assert in + * qwen35moe_forward_cuda.c); kept independent so this header stays free of + * the CUDA backend headers. */ +#define PN_QWEN35MOE_TOPK_MAX 256 + +typedef struct { + pn_qwen35moe_ssm_state *ssm; /* [n_ssm] per-SSM-layer recurrent state */ + pn_qwen35moe_kv *kv; /* [n_attn] per-attention-layer KV cache */ + int n_ssm; + int n_attn; + pn_arena *arena; /* per-token scratch (reset each token) */ + float *x_norm; /* [n_embd] pre-block normed hidden state */ + float *block_out; /* [n_embd] block/MoE output before residual */ + float *logits; /* [n_vocab] owned output buffer */ + int n_embd; + int n_vocab; + + /* Sparse top-k logits presentation bookkeeping (GPU path, see + * gpu_fetch_logits in qwen35moe_forward_cuda.c). `logits` is maintained + * SPARSELY: -inf everywhere except this step's k survivors, refilled in + * full only once and thereafter patched incrementally. This tracking + * describes THIS state's `logits` buffer, so it must live with it: an + * earlier version kept it on the shared per-model GPU context keyed by the + * buffer's ADDRESS, which silently aliased whenever a freed state's + * address was handed back to a new state — the full refill was skipped and + * the previous sequence's survivors (EOS included) leaked into the new + * sequence's step-0 logits. Zeroed by the calloc in + * pn_qwen35moe_state_new, which is exactly right: a fresh state's `logits` + * has not been -inf-filled yet. */ + int topk_filled; /* 1 = `logits` is -inf-filled and the + * prev-ids below describe its finite entries */ + int topk_prev_n; /* survivors written by the previous fetch */ + int topk_prev_ids[PN_QWEN35MOE_TOPK_MAX]; + + /* Constrained decoding needs the WHOLE logits row. The die normally + * reduces the LM head to the top-k (id, logit) pairs so only k pairs cross + * the 0.4 GB/s host link; everything else arrives as -inf. A grammar mask + * has to be able to force a token the model did NOT rank in its top-k — the + * whole point of constraining — so a request decoding under a grammar sets + * this and the fetch sends the full row for as long as it is live. */ + int full_logits; +} pn_qwen35moe_state; + +/* Opaque model handle — full definition in qwen35moe.c, also typedef'd in + * qwen35moe.h (C11 permits the identical re-typedef when both are included). */ +typedef struct pn_qwen35moe_model pn_qwen35moe_model; + +/** + * Run the full hybrid forward for one token (single-token decode/prefill). + * embed -> per-layer (attn_norm -> SSM|attn block -> +residual -> + * post_attention_norm -> shared-MoE -> +residual) -> output_norm -> LM head. + * + * @param model Loaded qwen35moe model (weights + config + index maps). + * @param token Input token id. + * @param state Dual state (updated in place); pos drives RoPE + caches. + * @param pos Absolute token position (0-based). + * @return Logits buffer [n_vocab] owned by state, or NULL on failure. + * + * Thread-safe: no (mutates state) + * Reentrant: no + * Signal-safe: no + */ +float *pn_qwen35moe_forward_token(pn_qwen35moe_model *model, uint32_t token, + pn_qwen35moe_state *state, int pos); + +#ifdef USE_CUDA +/* Batched GPU prefill (qwen35moe_forward_cuda.c): n tokens of one sequence, + * positions pos0..pos0+n-1, in one pass. Returns state->logits holding the + * LAST position's logits, or NULL if the batched path cannot run (caller + * falls back to per-token forwards). all_logits (optional, [n * n_vocab]) + * receives every position's full logits. Bit-exact vs n sequential + * pn_qwen35moe_forward_token_cuda calls (logits, KV cache, SSM state). */ +float *pn_qwen35moe_forward_batch_tokens_cuda(pn_qwen35moe_model *m, + const uint32_t *tokens, int n, + pn_qwen35moe_state *s, int pos0, + float *all_logits); + +/* Speculative greedy verify (qwen35moe_forward_cuda.c): run tokens[0..n) + * (committed pending token + drafts) at positions pos0..pos0+n-1 through the + * batched forward with per-position SSM state snapshots, filling + * argmax_out[t] with each position's exact greedy argmax (device top-k(1) — + * identical tokens to the single-token path). Commits NOTHING; refuses + * pos0 + n > the KV ring size. Returns 0 on success, -1 if the batched path + * cannot run (caller falls back to plain single-token decode). */ +int pn_qwen35moe_spec_verify_tokens_cuda(pn_qwen35moe_model *m, + const uint32_t *tokens, int n, + pn_qwen35moe_state *s, int pos0, + int32_t *argmax_out); + +/* Commit the first n_accept (in [1, n]) positions of the LAST verify: + * restores every SSM layer's conv + recurrent state from its snapshot + * n_accept-1 (no-op when n_accept == n). The KV cache needs no rollback — + * rejected entries sit past the committed position and are never read. + * The caller advances its position bookkeeping by n_accept. */ +int pn_qwen35moe_spec_commit_cuda(pn_qwen35moe_model *m, int n_accept); + +/* Multi-user decode (qwen35moe_forward_cuda.c): bind user state s to decode + * slot `slot` of n_slots (snapshots the live GPU SSM state D2D into the + * slot — call immediately after that user's prefill), then step all K users + * one token each in ONE batched pass. tokens[u]/pos[u] are user u's pending + * token and position; each user's logits land in sts[u]->logits. Bit-exact + * per user vs decoding alone. Returns 0 on success. */ +int pn_qwen35moe_decode_slot_bind_cuda(pn_qwen35moe_model *m, + pn_qwen35moe_state *s, int slot, + int n_slots); +/* Rung 2 (reactive ladder): the current memory-pressure-capped decode + * concurrency ceiling (full PN_QWEN35MOE_GPU_MAX_SLOTS at level OK, halved at + * TIGHT, 1 at CRITICAL). A batched-decode scheduler should size its slot count + * to this; bind refuses anything above it. */ +int pn_qwen35moe_gpu_ladder_max_slots(void); +int pn_qwen35moe_decode_slots_tokens_cuda(pn_qwen35moe_model *m, + const uint32_t *tokens, + pn_qwen35moe_state *const *sts, + const int *pos, int nslots); + +/* Session-state lifetime hook. The GPU layers hold raw back-pointers into a + * session's state (per-slot kv_slot_kv / kv_slot_host_id, per-SSM-layer + * host_state). Call this while `s` is still fully alive — pn_qwen35moe_state_free + * does, as its first act — and every live GPU context drops its references, so + * no dangling back-pointer can outlive the state. Safe with no GPU loaded. + * + * This is why the bind path needs no "is my back-pointer still valid?" test: + * an invalid one cannot exist. A test that answered by reading the suspect + * object would be reading freed memory to decide whether it was freed. */ +void pn_qwen35moe_gpu_forget_state_cuda(const pn_qwen35moe_state *s); + +/* Instrument for the lifetime regression test: how many per-slot kv + * back-pointers currently land inside [base, base+bytes). Compares addresses + * only and never dereferences, so it is valid to pass the (already freed) + * range of a state's kv array. Must be 0 after that state is freed. */ +int pn_qwen35moe_gpu_slot_kv_refs_cuda(const void *base, size_t bytes); +#endif + +/* Accessors into the opaque model, used by qwen35moe_forward.c (it needs the + * config, per-type layer counts, index maps, per-type weight arrays, and the + * model-level embed/norm/head tensors without seeing the struct layout). */ +const pn_qwen35moe_config *pn_qwen35moe_model_config(const pn_qwen35moe_model *m); +int pn_qwen35moe_model_n_ssm(const pn_qwen35moe_model *m); +int pn_qwen35moe_model_n_attn(const pn_qwen35moe_model *m); +/* The KV window this MODEL's ring is sized to: cfg->ctx_cap when set, else the + * global QWEN35_MAX_SEQ. Exported because the TT backend has to size the CARD's + * KV regions to the same number at arm time, and a second copy of that rule + * would be a cache the card and the host disagree about the length of. */ +int pn_qwen35moe_max_seq(const pn_qwen35moe_config *cfg); +pn_qwen35moe_layer_type pn_qwen35moe_model_layer_type(const pn_qwen35moe_model *m, + int layer); +int pn_qwen35moe_model_layer_index(const pn_qwen35moe_model *m, int layer); +const pn_qwen35moe_ssm_layer *pn_qwen35moe_model_ssm_layer( + const pn_qwen35moe_model *m, int idx); +const pn_qwen35moe_attn_layer *pn_qwen35moe_model_attn_layer( + const pn_qwen35moe_model *m, int idx); +const float *pn_qwen35moe_model_token_embd(const pn_qwen35moe_model *m); +/* Write embedding row `token` into dst[n_embd]. Uses the F32 heap if present + * (tied/fallback), else dequantizes the row on demand from the mmap (A2c). */ +void pn_qwen35moe_model_embed_row(const pn_qwen35moe_model *m, uint32_t token, + float *dst); +const float *pn_qwen35moe_model_output_norm(const pn_qwen35moe_model *m); +const pn_qweight *pn_qwen35moe_model_output(const pn_qwen35moe_model *m); + +/* MUTABLE weight accessors. A GPU backend has to WRITE one field of every + * weight (pn_qweight.gpu, its device-buffer handle), which the const accessors + * above cannot express — and const-casting their result is a lie about + * ownership that the compiler can no longer check. Same objects, same + * lifetime; only the constness differs. */ +pn_qwen35moe_ssm_layer *pn_qwen35moe_model_ssm_layer_mut(pn_qwen35moe_model *m, + int idx); +pn_qwen35moe_attn_layer *pn_qwen35moe_model_attn_layer_mut(pn_qwen35moe_model *m, + int idx); +pn_qweight *pn_qwen35moe_model_output_mut(pn_qwen35moe_model *m); + +/* The config, mutable — a backend installs its matvec seam through this, and + * the seam is a field of the config for the reason given at its definition. */ +pn_qwen35moe_config *pn_qwen35moe_model_config_mut(pn_qwen35moe_model *m); + +/* The still-open GGUF the model's weights point into (mmap backing). Lets a + * backend resolve a weight's bytes back to the tensor that contains them — + * expert slices are interior offsets into one merged tensor, so pointer + * containment is the only way to recover tensor identity. NULL if the model + * was not built from a GGUF. */ +pn_gguf_file *pn_qwen35moe_model_gguf(const pn_qwen35moe_model *m); + +/* ============================================================ + * Weight enumeration + * + * ONE walk over every pn_qweight the model owns, so the "set of weights this + * model has" is stated in a single place. Every backend that must touch all of + * them (Metal upload, Metal release, the kernel-coverage gate) goes through + * this; a private copy of the walk in each of them is how a newly added weight + * gets uploaded by one and forgotten by another. + * ============================================================ */ + +typedef struct { + pn_qweight *w; /* the weight itself (mutable: backends attach .gpu) */ + const char *name; /* e.g. "blk.12.ffn_gate_exps[7]"; valid for this call */ + int layer; /* global layer index, or -1 for a model-level weight */ + int expert; /* expert slice index, or -1 when not an expert slice */ +} pn_qwen35moe_weight_ref; + +typedef void (*pn_qwen35moe_weight_fn)(const pn_qwen35moe_weight_ref *ref, + void *user); + +/** + * Invoke fn once per pn_qweight the model owns, in layer order. + * + * @param m Loaded model (no-op if NULL) + * @param fn Callback (no-op if NULL) + * @param user Opaque pointer handed back to the callback + * + * Thread-safe: no (the callback may mutate the weights) + * Reentrant: no + * Signal-safe: no + */ +void pn_qwen35moe_for_each_weight(pn_qwen35moe_model *m, + pn_qwen35moe_weight_fn fn, void *user); + +/* Opaque GPU-residency handle accessors (qwen35moe.c stores the pointer; the + * concrete pn_qwen35moe_gpu struct lives in qwen35moe_forward_cuda.c). */ +void *pn_qwen35moe_model_gpu(const pn_qwen35moe_model *m); +void pn_qwen35moe_model_set_gpu(pn_qwen35moe_model *m, void *gpu); + +/* Explicit die placement from pn_load_on (Phase 7): stored on the model before + * the GPU upload; gpu_die_ids()/gpu_split_layer() prefer it over the + * QWEN35_GPU_DIE_A/B env knobs. n == 0 clears (env/defaults apply). */ +void pn_qwen35moe_model_set_dies(pn_qwen35moe_model *m, const int *dies, int n); +int pn_qwen35moe_model_dies(const pn_qwen35moe_model *m, const int **dies_out); + +#ifdef USE_CUDA +/* ============================================================ + * Single-die GPU forward path (A7 scaffold) — qwen35moe_forward_cuda.c + * + * Mirrors the CPU forward (qwen35moe_forward.c) but dispatches the per-layer + * sandwich to Foundry's fc_* CUDA kernels with GPU-resident weights, dual + * state, and scratch. Single die only; the 2-die layer-split swap is a stubbed + * seam (see qwen35moe_forward_cuda.c). Compile-checks on the Mac via dlopen + * (no GPU); validated layer-by-layer against the CPU reference on a K80 host. + * ============================================================ */ + +/** + * Upload every layer's Q3_K weight bytes + F32 norms/router params to + * persistent fc_buffers and attach the resulting pn_qwen35moe_gpu handle to the + * model (via pn_qwen35moe_model_set_gpu). Idempotent: a no-op if already + * uploaded or if fc_available() is false. The CPU weights are kept live. + * + * @param model Loaded qwen35moe model (CPU weights resident). + * @return 0 on success (or graceful skip when no GPU), -1 on failure. + */ +int pn_qwen35moe_gpu_upload(pn_qwen35moe_model *model); + +/* Free the GPU-residency handle and all its fc_buffers (detaches it from the + * model). Safe to call with a model that was never uploaded. */ +void pn_qwen35moe_gpu_free(pn_qwen35moe_model *model); + +/** + * Single-die GPU forward for one token. Mirrors pn_qwen35moe_forward_token: + * embed -> per-layer (attn_norm -> SSM|attn block -> +residual -> + * post_attention_norm -> shared-MoE -> +residual) -> output_norm -> LM head, + * dispatched to fc_* kernels with GPU-resident weights + dual state. Reads the + * final logits back to host (state->logits) and returns them. + * + * Requires pn_qwen35moe_gpu_upload() to have attached a GPU handle to the model. + * Reuses the host-side dual state (pn_qwen35moe_state) for KV/SSM bookkeeping + * and shadows the GPU-resident copies inside the GPU handle. + * + * @return state->logits [n_vocab], or NULL on failure / no GPU handle. + */ +float *pn_qwen35moe_forward_token_cuda(pn_qwen35moe_model *model, uint32_t token, + pn_qwen35moe_state *state, int pos); +#endif /* USE_CUDA */ + +#ifdef USE_METAL +/* ============================================================ + * Metal (Apple GPU) forward path — qwen35moe_forward_metal.c + * + * Same seam as the CUDA path above: a token-level entry the pn_forward_fn + * adapter (pn_qwen35moe_forward_metal, declared in qwen35moe.h) wraps. Unlike + * the CUDA one it returns a STATUS, because in Phase 0 "I did not run" is a + * distinct outcome from "I ran and failed" — the adapter turns the former into + * a CPU token and the latter into an error. + * ============================================================ */ + +enum { + PN_QWEN35MOE_METAL_OK = 0, + PN_QWEN35MOE_METAL_ERROR = -1, + PN_QWEN35MOE_METAL_ENOTIMPL = -2, /* no kernel path yet — use the CPU forward */ +}; + +/** + * Run one token through the Metal forward. + * + * PHASE 0: unimplemented on purpose. Returns PN_QWEN35MOE_METAL_ENOTIMPL + * without touching model or state, so no half-built GPU path can corrupt the + * dual state or produce plausible-looking wrong logits. Phases 1-5 fill it in. + * + * @param model Loaded model with Metal weight residency attached. + * @param token Input token id. + * @param state Dual state (SSM/KV), updated in place once implemented. + * @param pos Absolute token position (0-based). + * @param logits_out Out: logits [n_vocab] owned by state, NULL on non-OK. + * @return PN_QWEN35MOE_METAL_OK / _ENOTIMPL / _ERROR. + * + * Thread-safe: no (mutates state) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_forward_token_metal(pn_qwen35moe_model *model, uint32_t token, + pn_qwen35moe_state *state, int pos, + float **logits_out); +#endif /* USE_METAL */ + +#endif /* PN_QWEN35MOE_INTERNAL_H */ diff --git a/src/models/qwen35moe_kv_q4.h b/src/models/qwen35moe_kv_q4.h new file mode 100644 index 0000000..a41989d --- /dev/null +++ b/src/models/qwen35moe_kv_q4.h @@ -0,0 +1,165 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_kv_q4.h + * @brief Q4 KV-cache row helpers with an fp16 sink and recent-window shadow. + * + * Written from a behaviour specification (plans/cleanroom_r2_qwen35_spec.md); no ggml or llama.cpp source was consulted. + * + * @status None + * @issues Env getters cache per translation unit and are not thread-safe. The window + * predicate compares a cache slot with the absolute position, so after the ring + * wraps almost every slot reads Q4. The fp16 window ring laps at PN_KVQ4_WMAX, not + * at the runtime window. head_dim % 32 tail elements are neither quantised nor + * dequantised. + * @todo None + */ + +#ifndef QWEN35MOE_KV_Q4_H +#define QWEN35MOE_KV_Q4_H + +#include +#include +#include +#include + +#include "../formats/gguf_dequant.h" + +/* Layout constants are ABI: device kernels, snapshot files and allocators use them. */ +#define PN_KVQ4_BLK 32 +#define PN_KVQ4_BLK_BYTES ((int)sizeof(pn_block_q4_0)) +#define PN_KVQ4_WMAX 256 +#define PN_KVQ4_SINKMAX 8 + +/** Runtime fp16 window length (QWEN35_KV_Q4_WIN, default 128, clamped to [0, WMAX]). */ +static inline int pn_kvq4_win(void) { + static int cached_win = -1; + if (cached_win < 0) { + const char *env_value = getenv("QWEN35_KV_Q4_WIN"); + int win = env_value ? atoi(env_value) : 128; + if (win < 0) + win = 0; + if (win > PN_KVQ4_WMAX) + win = PN_KVQ4_WMAX; + cached_win = win; + } + return cached_win; +} + +/** Runtime fp16 sink length (QWEN35_KV_Q4_SINK, default 4, clamped to [0, SINKMAX]). */ +static inline int pn_kvq4_sink(void) { + static int cached_sink = -1; + if (cached_sink < 0) { + const char *env_value = getenv("QWEN35_KV_Q4_SINK"); + int sink = env_value ? atoi(env_value) : 4; + if (sink < 0) + sink = 0; + if (sink > PN_KVQ4_SINKMAX) + sink = PN_KVQ4_SINKMAX; + cached_sink = sink; + } + return cached_sink; +} + +/** 1 when slot s is read from an fp16 shadow (sink or recent window), else 0. */ +static inline int pn_kvq4_is_fp16(int s, int cur_pos, int sink, int win) { + return (s < sink) || (win > 0 && s > cur_pos - win); +} + +static inline void pn_kvq4_f32_to_half_row(const float *src, uint16_t *dst, int head_dim) { + for (int d = 0; d < head_dim; d++) + dst[d] = pn_float_to_half(src[d]); +} + +static inline void pn_kvq4_half_to_f32_row(const uint16_t *src, float *dst, int head_dim) { + for (int d = 0; d < head_dim; d++) + dst[d] = pn_half_to_float(src[d]); +} + +/** Bytes of one quantised row: whole 32-element blocks only. */ +static inline int pn_kvq4_row_bytes(int head_dim) { + return (head_dim / PN_KVQ4_BLK) * PN_KVQ4_BLK_BYTES; +} + +/** Quantise the whole 32-element blocks of src into dst; the tail is ignored. */ +static inline void pn_kvq4_quant_row(const float *src, uint8_t *dst, int head_dim) { + int n_blocks = head_dim / PN_KVQ4_BLK; + for (int b = 0; b < n_blocks; b++) { + const float *x = src + (size_t)b * PN_KVQ4_BLK; + pn_block_q4_0 *blk = (pn_block_q4_0 *)(dst + (size_t)b * PN_KVQ4_BLK_BYTES); + float amax = 0.0f; + for (int j = 0; j < PN_KVQ4_BLK; j++) { + float a = fabsf(x[j]); + if (a > amax) + amax = a; + } + float d = amax / -8.0f; + float id = d != 0.0f ? 1.0f / d : 0.0f; + blk->d = pn_float_to_half(d); + for (int j = 0; j < PN_KVQ4_BLK / 2; j++) { + int q0 = (int)lrintf(x[j] * id) + 8; + int q1 = (int)lrintf(x[j + PN_KVQ4_BLK / 2] * id) + 8; + if (q0 < 0) + q0 = 0; + else if (q0 > 15) + q0 = 15; + if (q1 < 0) + q1 = 0; + else if (q1 > 15) + q1 = 15; + blk->qs[j] = (uint8_t)(q0 | (q1 << 4)); + } + } +} + +/** Dequantise the whole blocks of a row; tail floats of dst are left untouched. */ +static inline void pn_kvq4_dequant_row(const uint8_t *src, float *dst, int head_dim) { + int n_blocks = head_dim / PN_KVQ4_BLK; + for (int b = 0; b < n_blocks; b++) + pn_dequantize_block_q4_0(src + (size_t)b * PN_KVQ4_BLK_BYTES, + dst + (size_t)b * PN_KVQ4_BLK); +} + +/** + * Append one row at cache slot cache_pos: always Q4, plus the fp16 sink shadow when the + * slot is a sink slot, plus the fp16 window ring (indexed by slot % WMAX) when win > 0. + */ +static inline void pn_kvq4_store_row(uint8_t *q4_buf, uint16_t *sink_buf, + uint16_t *win_buf, int h, int max_seq, + int cache_pos, const float *src, + int head_dim, int sink, int win) { + int rb = pn_kvq4_row_bytes(head_dim); + pn_kvq4_quant_row(src, q4_buf + ((size_t)h * max_seq + cache_pos) * rb, head_dim); + if (cache_pos < sink) + pn_kvq4_f32_to_half_row(src, + sink_buf + ((size_t)h * PN_KVQ4_SINKMAX + cache_pos) * head_dim, + head_dim); + if (win > 0) + pn_kvq4_f32_to_half_row(src, + win_buf + ((size_t)h * PN_KVQ4_WMAX + + cache_pos % PN_KVQ4_WMAX) * head_dim, + head_dim); +} + +/** Reconstruct slot s: sink shadow, else window ring, else the Q4 row. */ +static inline void pn_kvq4_load_row(const uint8_t *q4_buf, + const uint16_t *sink_buf, + const uint16_t *win_buf, int h, int max_seq, + int s, int cur_pos, float *dst, + int head_dim, int sink, int win) { + if (s < sink) { + pn_kvq4_half_to_f32_row(sink_buf + ((size_t)h * PN_KVQ4_SINKMAX + s) * head_dim, + dst, head_dim); + return; + } + if (win > 0 && s > cur_pos - win) { + pn_kvq4_half_to_f32_row(win_buf + ((size_t)h * PN_KVQ4_WMAX + s % PN_KVQ4_WMAX) + * head_dim, + dst, head_dim); + return; + } + int rb = pn_kvq4_row_bytes(head_dim); + pn_kvq4_dequant_row(q4_buf + ((size_t)h * max_seq + s) * rb, dst, head_dim); +} + +#endif /* QWEN35MOE_KV_Q4_H */ diff --git a/src/models/qwen35moe_metal_coverage.c b/src/models/qwen35moe_metal_coverage.c new file mode 100644 index 0000000..d730ae9 --- /dev/null +++ b/src/models/qwen35moe_metal_coverage.c @@ -0,0 +1,486 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_metal_coverage.c + * @brief Implementation of the qwen35moe Metal kernel-coverage gate. + * + * Three passes, in this order: + * 1. index every GGUF tensor by its byte range, so a weight pointer + * can be resolved back to the tensor that contains it (expert + * slices are interior offsets into one merged tensor, so pointer + * containment is the only way to recover tensor identity); + * 2. enumerate the model's pn_qweights (pn_qwen35moe_for_each_weight) + * and mark the tensors they bind; + * 3. install the dispatch observer, run ONE real forward token, and + * mark the tensors the forward actually dispatched. + * + * A tensor then falls into exactly one class: dispatched (needs a + * kernel), bound-but-never-dispatched (unaccounted — a failure), or + * bound by nothing (consumed elementwise / dequantized at load — + * correctly excluded, and the false alarm a file-contents census + * would raise). + * + * @status Phase 1 gate. Expected to fail on the stock Q3_K_M file until the + * four new matvec kernels land. + * @issues None + * @todo None + */ + +/* 1. This module's own header */ +#include "qwen35moe_metal_coverage.h" + +/* 2. Standard library */ +#include +#include + +/* 4. Project headers */ +#include "../core/qweight.h" +#include "qwen35moe.h" +#include "qwen35moe_metal_kernel_registry.h" + +#define GIB (1024.0 * 1024.0 * 1024.0) + +/* One GGUF tensor's byte range plus what the model does with it. */ +typedef struct { + const uint8_t *start; + uint64_t bytes; + const char *name; /* borrowed from the GGUF (NUL-terminated) */ + pn_gguf_quant type; + int n_bound; /* pn_qweights whose bytes lie inside */ + int n_dispatched;/* of those, ones the forward dispatched */ + unsigned ops; /* 1u << pn_qweight_op, OR'd */ +} cov_tensor; + +/* Working state shared by the enumeration walk and the forward trace. */ +typedef struct { + cov_tensor *tensors; /* sorted by start address */ + int n_tensors; + int unresolved; /* weights whose bytes are in no tensor */ + int dispatched_weights; + int drop_type; /* harness: quant type to hide, or -1 */ +} cov_ctx; + +/* + * Harness knob: drop one quant type from the dispatch derivation, so the + * "a dispatched tensor went unaccounted for" branch can be exercised on the + * real model instead of being taken on trust (see + * `harnesses-here-measure-nothing`). + * + * It can only ever make the gate fail harder. The dropped tensors are still + * bound by the enumeration pass, so hiding their dispatches moves them from + * "dispatched" to "unaccounted", which is a failure either way. There is no + * setting of this variable that turns a red gate green. + */ +#define PN_METAL_COVERAGE_DROP_TYPE_ENV "PN_METAL_COVERAGE_DROP_TYPE" + +static int cov_drop_type_from_env(void) +{ + const char *env = getenv(PN_METAL_COVERAGE_DROP_TYPE_ENV); + return (env && *env) ? atoi(env) : -1; +} + +/* ============================================================ + * Tensor index + * ============================================================ */ + +static int cov_tensor_cmp(const void *a, const void *b) +{ + const uint8_t *pa = ((const cov_tensor *)a)->start; + const uint8_t *pb = ((const cov_tensor *)b)->start; + if (pa < pb) return -1; + if (pa > pb) return 1; + return 0; +} + +/** + * Build the address-sorted tensor index from the still-open GGUF. + * Returns 0 on success, -1 on allocation failure or an empty file. + */ +static int cov_build_index(pn_gguf_file *gguf, cov_ctx *ctx) +{ + if (!gguf || !gguf->tensors || gguf->n_tensors == 0 || !gguf->data) + return -1; + + ctx->n_tensors = (int)gguf->n_tensors; + ctx->tensors = calloc((size_t)ctx->n_tensors, sizeof(*ctx->tensors)); + if (!ctx->tensors) + return -1; + + for (int i = 0; i < ctx->n_tensors; i++) { + const pn_gguf_tensor_info *ti = &gguf->tensors[i]; + size_t numel = 1; + for (uint32_t d = 0; d < ti->n_dims && d < 4; d++) + numel *= (size_t)ti->dims[d]; + + cov_tensor *ct = &ctx->tensors[i]; + ct->start = (const uint8_t *)gguf->data + ti->offset; + ct->bytes = (uint64_t)pn_gguf_type_size(ti->type, numel); + ct->name = ti->name.data ? ti->name.data : ""; + ct->type = ti->type; + } + + qsort(ctx->tensors, (size_t)ctx->n_tensors, sizeof(*ctx->tensors), + cov_tensor_cmp); + return 0; +} + +/** + * Resolve a weight's data pointer to the tensor containing it. + * Binary search for the last tensor starting at or before `p`, then a + * containment test — an interior expert slice lands on its merged parent. + * + * @return the tensor, or NULL if `p` is outside every tensor + */ +static cov_tensor *cov_tensor_for_ptr(cov_ctx *ctx, const void *p) +{ + const uint8_t *q = (const uint8_t *)p; + int lo = 0, hi = ctx->n_tensors - 1, found = -1; + + while (lo <= hi) { + int mid = lo + (hi - lo) / 2; + if (ctx->tensors[mid].start <= q) { + found = mid; + lo = mid + 1; + } else { + hi = mid - 1; + } + } + if (found < 0) + return NULL; + + cov_tensor *ct = &ctx->tensors[found]; + if (q < ct->start + ct->bytes) + return ct; + return NULL; +} + +/* ============================================================ + * Pass 2 — enumerate the model's weights + * ============================================================ */ + +static void cov_bind_weight(const pn_qwen35moe_weight_ref *ref, void *user) +{ + cov_ctx *ctx = (cov_ctx *)user; + cov_tensor *ct = cov_tensor_for_ptr(ctx, ref->w->data); + if (!ct) { + ctx->unresolved++; + return; + } + ct->n_bound++; +} + +/* ============================================================ + * Pass 3 — trace one forward + * ============================================================ */ + +static void cov_observe_dispatch(const pn_qweight *w, pn_qweight_op op, + void *user) +{ + cov_ctx *ctx = (cov_ctx *)user; + cov_tensor *ct = cov_tensor_for_ptr(ctx, w->data); + if (!ct) + return; /* counted as unresolved by the enumeration pass */ + if (ctx->drop_type >= 0 && (int)ct->type == ctx->drop_type) + return; /* harness: pretend this type is never dispatched */ + ct->n_dispatched++; + ct->ops |= 1u << (unsigned)op; + ctx->dispatched_weights++; +} + +/** + * Run one forward token at position 0 with the observer installed. + * + * One token is sufficient and this is load-bearing: every layer runs on every + * token, and a layer's routed experts are slices of that layer's own merged + * tensors, so whichever 8 of 256 the router picks, the tensor is marked. What + * one token does NOT cover is a code path gated on position or batch size — + * see the header's note on prefill. + * + * @return 0 on success, -1 if the state could not be built or the forward + * returned no logits (either is a gate failure, never a pass) + */ +static int cov_trace_forward(pn_qwen35moe_model *model, cov_ctx *ctx) +{ + void *state = pn_qwen35moe_state_new(model); + if (!state) + return -1; + + pn_qweight_set_dispatch_observer(cov_observe_dispatch, ctx); + float *logits = pn_qwen35moe_forward_token(model, 0, + (pn_qwen35moe_state *)state, 0); + pn_qweight_set_dispatch_observer(NULL, NULL); + + pn_qwen35moe_state_free(state); + return logits ? 0 : -1; +} + +/* ============================================================ + * Classification + per-type rows + * ============================================================ */ + +/** Find (or append) the row for a quant type. NULL when the table is full. */ +static pn_qwen35moe_metal_coverage_row *cov_row_for_type( + pn_qwen35moe_metal_coverage *out, pn_gguf_quant type) +{ + for (int i = 0; i < out->n_rows; i++) + if (out->rows[i].type == type) + return &out->rows[i]; + + if (out->n_rows >= PN_METAL_COVERAGE_MAX_TYPES) + return NULL; + + pn_qwen35moe_metal_coverage_row *row = &out->rows[out->n_rows++]; + memset(row, 0, sizeof(*row)); + row->type = type; + pn_gguf_quant_name(type, row->type_name, sizeof(row->type_name)); + row->covered = pn_qwen35moe_metal_kernel_for_type(type, &row->kernel_entry); + return row; +} + +/** Record an unaccounted tensor's name, up to the sample bound. */ +static void cov_name_unaccounted(pn_qwen35moe_metal_coverage *out, + const cov_tensor *ct) +{ + if (out->n_unaccounted_named >= PN_METAL_COVERAGE_MAX_SAMPLES) + return; + snprintf(out->unaccounted[out->n_unaccounted_named], + PN_METAL_COVERAGE_NAME_MAX, "%s", ct->name); + out->n_unaccounted_named++; +} + +/** Fold one tensor into its class and, when dispatched, into its type row. */ +static void cov_classify_tensor(const cov_tensor *ct, + pn_qwen35moe_metal_coverage *out) +{ + out->file_tensors++; + out->file_bytes += ct->bytes; + + if (ct->n_dispatched == 0) { + if (ct->n_bound > 0) { + out->unaccounted_tensors++; + out->unaccounted_bytes += ct->bytes; + cov_name_unaccounted(out, ct); + } else { + out->excluded_tensors++; + out->excluded_bytes += ct->bytes; + } + return; + } + + out->dispatched_tensors++; + out->dispatched_bytes += ct->bytes; + + pn_qwen35moe_metal_coverage_row *row = cov_row_for_type(out, ct->type); + if (!row) { + /* More distinct types than the table holds. Count it uncovered rather + * than drop it: an unexpected type is exactly what must not pass. */ + out->uncovered_tensors++; + out->uncovered_bytes += ct->bytes; + return; + } + if (row->tensors == 0) + snprintf(row->example, sizeof(row->example), "%s", ct->name); + row->tensors++; + row->bytes += ct->bytes; + row->weights += ct->n_dispatched; + row->ops |= ct->ops; + + if (!row->covered) { + out->uncovered_tensors++; + out->uncovered_bytes += ct->bytes; + } +} + +/* Heaviest type first; ties broken by type id so the order is deterministic. */ +static int cov_row_cmp(const void *a, const void *b) +{ + const pn_qwen35moe_metal_coverage_row *ra = a, *rb = b; + if (ra->bytes > rb->bytes) return -1; + if (ra->bytes < rb->bytes) return 1; + return (int)ra->type - (int)rb->type; +} + +/* ============================================================ + * Public API + * ============================================================ */ + +int pn_qwen35moe_metal_coverage_run(pn_qwen35moe_model *model, + pn_qwen35moe_metal_coverage *out) +{ + if (!model || !out) + return -1; + + memset(out, 0, sizeof(*out)); + + cov_ctx ctx = {0}; + ctx.drop_type = cov_drop_type_from_env(); + if (cov_build_index(pn_qwen35moe_model_gguf(model), &ctx) != 0) + return -1; + + pn_qwen35moe_for_each_weight(model, cov_bind_weight, &ctx); + + int rc = cov_trace_forward(model, &ctx); + out->traced = (rc == 0); + + for (int i = 0; i < ctx.n_tensors; i++) + cov_classify_tensor(&ctx.tensors[i], out); + + /* Rank by bytes so the table reads as a progress meter: the type worth the + * most of the model is the one to build first. */ + qsort(out->rows, (size_t)out->n_rows, sizeof(out->rows[0]), cov_row_cmp); + + out->unresolved_weights = ctx.unresolved; + out->dispatched_weights = ctx.dispatched_weights; + + free(ctx.tensors); + return rc; +} + +int pn_qwen35moe_metal_coverage_passed(const pn_qwen35moe_metal_coverage *cov) +{ + if (!cov || !cov->traced) + return 0; + return cov->uncovered_tensors == 0 && + cov->unaccounted_tensors == 0 && + cov->unresolved_weights == 0 && + cov->dispatched_tensors > 0; +} + +/** + * Human label for the ops a type was dispatched under, e.g. "matvec+dot". + * Every op present is named: a type reached through more than one op shape + * needs a kernel for each, so collapsing them would understate the work. + */ +static void cov_ops_label(unsigned ops, char *buf, size_t len) +{ + static const struct { pn_qweight_op op; const char *name; } NAMES[] = { + { PN_QWEIGHT_OP_MATVEC, "matvec" }, + { PN_QWEIGHT_OP_MATMUL, "matmul" }, + { PN_QWEIGHT_OP_DOT, "dot" }, + }; + size_t used = 0; + + buf[0] = '\0'; + for (size_t i = 0; i < sizeof(NAMES) / sizeof(NAMES[0]); i++) { + if (!(ops & (1u << (unsigned)NAMES[i].op))) + continue; + int n = snprintf(buf + used, len - used, "%s%s", + used ? "+" : "", NAMES[i].name); + if (n < 0 || (size_t)n >= len - used) + return; + used += (size_t)n; + } + if (used == 0) + snprintf(buf, len, "?"); +} + +/** The per-type table — the Phase-1 progress meter. */ +static void cov_report_rows(const pn_qwen35moe_metal_coverage *cov, FILE *f) +{ + fprintf(f, "metal-coverage: %-6s %8s %8s %11s %-13s %s\n", + "type", "tensors", "covered", "bytes", "op", "kernel"); + for (int i = 0; i < cov->n_rows; i++) { + const pn_qwen35moe_metal_coverage_row *r = &cov->rows[i]; + char frac[24], ops[32]; + snprintf(frac, sizeof(frac), "%d/%d", r->covered ? r->tensors : 0, + r->tensors); + cov_ops_label(r->ops, ops, sizeof(ops)); + fprintf(f, "metal-coverage: %-6s %8d %8s %8.2f GiB %-13s %s\n", + r->type_name, r->tensors, frac, (double)r->bytes / GIB, ops, + r->covered ? r->kernel_entry : "MISSING"); + } +} + +/** Every finding that makes the verdict what it is, named. */ +static void cov_report_findings(const pn_qwen35moe_metal_coverage *cov, FILE *f) +{ + for (int i = 0; i < cov->n_rows; i++) { + const pn_qwen35moe_metal_coverage_row *r = &cov->rows[i]; + if (r->covered) + continue; + fprintf(f, "metal-coverage: MISSING KERNEL %-6s %s" + " (+%d more tensor(s), %d dispatch site(s), %.2f GiB)\n", + r->type_name, r->example, r->tensors - 1, r->weights, + (double)r->bytes / GIB); + } + + /* Registration is per quant type, which is the matvec question. A type that + * is ALSO reached through a different op shape needs a kernel for that + * shape too, and this gate cannot see whether the registered entry point + * serves it — so say so out loud rather than let the `op` column carry it. */ + for (int i = 0; i < cov->n_rows; i++) { + const pn_qwen35moe_metal_coverage_row *r = &cov->rows[i]; + if (!r->covered || !(r->ops & (1u << PN_QWEIGHT_OP_DOT))) + continue; + fprintf(f, "metal-coverage: NOTE %s is also dispatched as a scalar dot; " + "%s covers the matvec shape only — the dot needs its own " + "kernel and this gate does not check it\n", + r->type_name, r->kernel_entry); + } + + if (cov->unaccounted_tensors > 0) { + fprintf(f, "metal-coverage: UNACCOUNTED %d tensor(s) (%.2f GiB) are " + "bound to a pn_qweight but were never dispatched — either a " + "dispatch site the observer cannot see, or dead weight\n", + cov->unaccounted_tensors, + (double)cov->unaccounted_bytes / GIB); + for (int i = 0; i < cov->n_unaccounted_named; i++) + fprintf(f, "metal-coverage: unaccounted e.g. %s\n", + cov->unaccounted[i]); + } + + if (cov->unresolved_weights > 0) + fprintf(f, "metal-coverage: UNRESOLVED %d weight(s) point outside " + "every GGUF tensor — the trace cannot vouch for them\n", + cov->unresolved_weights); + + if (!cov->traced) + fprintf(f, "metal-coverage: TRACE FAILED — the dispatch set is unknown, " + "so this is a failure, not an empty pass\n"); +} + +void pn_qwen35moe_metal_coverage_report(const pn_qwen35moe_metal_coverage *cov, + FILE *f) +{ + if (!cov || !f) + return; + + uint64_t covered_bytes = cov->dispatched_bytes - cov->uncovered_bytes; + int covered_tensors = cov->dispatched_tensors - cov->uncovered_tensors; + double pct = cov->dispatched_bytes + ? 100.0 * (double)covered_bytes / (double)cov->dispatched_bytes + : 0.0; + + fprintf(f, "metal-coverage: qwen35moe Metal matvec coverage " + "(dispatch set traced from one forward)\n"); + cov_report_rows(cov, f); + fprintf(f, "metal-coverage: TOTAL %d/%d dispatched tensors covered, " + "%.2f/%.2f GiB (%.1f%%)\n", + covered_tensors, cov->dispatched_tensors, + (double)covered_bytes / GIB, (double)cov->dispatched_bytes / GIB, + pct); + fprintf(f, "metal-coverage: dispatch sites: %d pn_qweight(s) across %d " + "tensor(s)\n", cov->dispatched_weights, cov->dispatched_tensors); + fprintf(f, "metal-coverage: excluded: %d of %d file tensors (%.2f GiB) " + "are never fed to a kernel — norms, embeddings; not a gap\n", + cov->excluded_tensors, cov->file_tensors, + (double)cov->excluded_bytes / GIB); + + cov_report_findings(cov, f); + + fprintf(f, "metal-coverage: %s\n", + pn_qwen35moe_metal_coverage_passed(cov) ? "PASS" : "FAIL"); +} + +int pn_qwen35moe_metal_coverage_check(pn_qwen35moe_model *model, FILE *f) +{ + pn_qwen35moe_metal_coverage cov = {0}; + int rc = pn_qwen35moe_metal_coverage_run(model, &cov); + if (rc != 0 && cov.file_tensors == 0) { + fprintf(f ? f : stderr, + "metal-coverage: could not run (no model or no GGUF backing)\n"); + return 0; + } + pn_qwen35moe_metal_coverage_report(&cov, f ? f : stderr); + return pn_qwen35moe_metal_coverage_passed(&cov); +} diff --git a/src/models/qwen35moe_metal_coverage.h b/src/models/qwen35moe_metal_coverage.h new file mode 100644 index 0000000..724951b --- /dev/null +++ b/src/models/qwen35moe_metal_coverage.h @@ -0,0 +1,148 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_metal_coverage.h + * @brief Kernel-coverage gate for the qwen35moe Metal backend: asserts that + * every weight the forward actually dispatches a kernel at has a Metal + * matvec kernel registered for its quant type, and reports the shortfall + * as tensors and bytes per type. + * + * The MVP decodes the stock mixed-quant GGUF (Q3_K/Q4_K/Q5_K/Q6_K/Q8_0/ + * F32 in one file — foundry/metal/qwen35moe_metal_spec.md §5). A missing + * or mis-registered kernel for any one of those does not fail loudly: it + * produces garbage logits at end-to-end bring-up, where it is easy to + * misattribute to a hard kernel like the SSM or attention. This gate + * turns that into an immediate, named failure. + * + * WHY IT GATES ON DISPATCH, NOT ON FILE CONTENTS. A census of the GGUF's + * tensor types is wrong in both directions: + * - false alarm: 361 F32 tensors in the file are RMSNorm weights the + * forward consumes elementwise, and token_embd.weight (Q3_K, 218 MB) + * is dequantized wholesale at load. None is ever fed to a matvec. + * - false pass: the 256 experts of a layer are slices of ONE merged + * tensor, so per-tensor counting hides how the forward reaches them. + * So the dispatch set is TRACED: the observer in src/core/qweight.h is + * installed at the matvec entry points and one real forward is run. What + * the forward dispatches is what the gate checks, by construction, and + * it keeps up when the forward changes. + * + * @status Expected to FAIL on the stock Q3_K_M file until the four Phase-1 + * matvec kernels land (plan Phase 1). Failing is the point. + * @issues None + * @todo None + */ + +#ifndef PN_QWEN35MOE_METAL_COVERAGE_H +#define PN_QWEN35MOE_METAL_COVERAGE_H + +#include +#include + +#include "../formats/gguf.h" +#include "../formats/gguf_dequant.h" +#include "qwen35moe_internal.h" + +/* Distinct quant types a single model file can dispatch. The stock Q3_K_M file + * uses 6; the cap is generous so an unexpected type lands in the table (and + * fails the gate) rather than being dropped. */ +#define PN_METAL_COVERAGE_MAX_TYPES 16 + +/* Names kept per finding, so a failure report can point at real tensors + * without printing thousands of them. */ +#define PN_METAL_COVERAGE_MAX_SAMPLES 8 +#define PN_METAL_COVERAGE_NAME_MAX 128 + +/* One quant type's dispatch footprint, at GGUF-tensor granularity so the + * numbers line up with the measured table in the spec. */ +typedef struct { + pn_gguf_quant type; + char type_name[PN_GGUF_QUANT_NAME_MAX]; + const char *kernel_entry; /* resolved foundry symbol, NULL if none */ + int covered; /* 1 when a kernel is registered */ + int tensors; /* dispatched GGUF tensors of this type */ + uint64_t bytes; /* their total bytes */ + int weights; /* dispatched pn_qweight slices */ + unsigned ops; /* 1u << pn_qweight_op, OR'd */ + char example[PN_METAL_COVERAGE_NAME_MAX]; /* first such tensor */ +} pn_qwen35moe_metal_coverage_row; + +/* Full result of one gate run. */ +typedef struct { + pn_qwen35moe_metal_coverage_row rows[PN_METAL_COVERAGE_MAX_TYPES]; + int n_rows; + + int traced; /* the forward trace ran */ + int file_tensors; /* tensors in the GGUF */ + uint64_t file_bytes; + + int dispatched_tensors; /* a pn_qweight in them was dispatched */ + uint64_t dispatched_bytes; + int dispatched_weights; + + /* Tensors no pn_qweight points into: consumed elementwise or dequantized + * at load. Reported, never a failure — this is the false alarm a + * file-contents census would raise. */ + int excluded_tensors; + uint64_t excluded_bytes; + + /* Tensors a pn_qweight DOES point into that the trace never dispatched. + * A failure: either a live dispatch site the observer cannot see, or dead + * weight. Either way it must not pass silently. */ + int unaccounted_tensors; + uint64_t unaccounted_bytes; + char unaccounted[PN_METAL_COVERAGE_MAX_SAMPLES][PN_METAL_COVERAGE_NAME_MAX]; + int n_unaccounted_named; + + /* pn_qweights whose bytes lie outside every GGUF tensor (an owned + * dequantized copy, or a walk/loader disagreement). Reported so the trace + * can never be quietly incomplete. */ + int unresolved_weights; + + int uncovered_tensors; /* dispatched, no kernel for the type */ + uint64_t uncovered_bytes; +} pn_qwen35moe_metal_coverage; + +/** + * Run the gate: enumerate the model's weights, trace one real forward to learn + * the dispatch set, and resolve each dispatched type against the Metal kernel + * registry. + * + * Runs exactly one forward token at position 0 on a scratch state, which is + * enough: every layer runs every token, and the experts a layer routes to are + * slices of that layer's own merged tensors, so one token reaches every tensor + * the forward can dispatch. + * + * @param model Loaded qwen35moe model + * @param out Receives the result (caller-owned, plain struct) + * @return 0 on success, -1 if the model/GGUF is unusable or the trace + * could not run (which is itself a gate failure, not a pass) + * + * Thread-safe: no (installs the process-wide dispatch observer, runs a forward) + * Reentrant: no + * Signal-safe: no + */ +int pn_qwen35moe_metal_coverage_run(pn_qwen35moe_model *model, + pn_qwen35moe_metal_coverage *out); + +/** + * Print the per-type coverage table plus every finding. Written to be readable + * as a Phase-1 progress meter, not only as a pass/fail. + */ +void pn_qwen35moe_metal_coverage_report(const pn_qwen35moe_metal_coverage *cov, + FILE *f); + +/** + * The verdict: 1 when every dispatched tensor's type has a registered kernel + * and nothing is unaccounted for, else 0. + */ +int pn_qwen35moe_metal_coverage_passed(const pn_qwen35moe_metal_coverage *cov); + +/** + * Convenience: run + report + verdict. Used by the test and by the debug hook + * in pn_qwen35moe_metal_upload (PN_METAL_COVERAGE=1). + * + * @return 1 if the gate passes, 0 if it fails or could not run + */ +int pn_qwen35moe_metal_coverage_check(pn_qwen35moe_model *model, FILE *f); + +#endif /* PN_QWEN35MOE_METAL_COVERAGE_H */ diff --git a/src/models/qwen35moe_metal_kernel_registry.c b/src/models/qwen35moe_metal_kernel_registry.c new file mode 100644 index 0000000..0720d3f --- /dev/null +++ b/src/models/qwen35moe_metal_kernel_registry.c @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_metal_kernel_registry.c + * @brief Implementation of the quant-type -> Metal matvec entry point lookup. + * + * @status Phase 1. Q4_K (fm_matvec_q4_k_at) and F32 (fm_sgemv) resolve today; + * the four Phase-1 kernels do not exist yet, so their types report + * uncovered. + * @issues None + * @todo None + */ + +/* 1. This module's own header */ +#include "qwen35moe_metal_kernel_registry.h" + +/* 2. Standard library */ +#include + +/* 3. System / platform headers */ +#ifdef __APPLE__ +#include +#endif + +/* + * Candidate entry-point names per quant type, most specific first. Several are + * listed per type because a kernel may land in either the plain or the + * offset-bearing (`_at`, fm_bufref) form, and because the codebase is not + * consistent about `q3k` vs `q3_k`. The names come from + * foundry/metal/qwen35moe_metal_plan.md Phase 1 and from the entry points + * fm_metal.h already exports. + * + * Deliberately NOT listed: fm_matvec_q8 as the Q8_0 kernel. It is a + * single-scale int8 dot, not the block-scaled Q8_0 layout (plan Phase 1: + * "Write a new one; do not extend it"), so accepting it here would mark 94 + * tensors covered by a kernel that would read their bytes wrong — precisely + * the silent-garbage failure this gate exists to prevent. + */ +typedef struct { + pn_gguf_quant type; + const char *candidates[4]; +} pn_metal_kernel_entry; + +static const pn_metal_kernel_entry KERNEL_TABLE[] = { + { GGUF_QUANT_F32, { "fm_sgemv", NULL, NULL, NULL } }, + { GGUF_QUANT_Q3_K, { "fm_matvec_q3k_at", "fm_matvec_q3k", + "fm_matvec_q3_k_at", "fm_matvec_q3_k" } }, + { GGUF_QUANT_Q4_K, { "fm_matvec_q4_k_at", "fm_matvec_q4_k", NULL, NULL } }, + { GGUF_QUANT_Q5_K, { "fm_matvec_q5k_at", "fm_matvec_q5k", + "fm_matvec_q5_k_at", "fm_matvec_q5_k" } }, + { GGUF_QUANT_Q6_K, { "fm_matvec_q6k_at", "fm_matvec_q6k", + "fm_matvec_q6_k_at", "fm_matvec_q6_k" } }, + { GGUF_QUANT_Q8_0, { "fm_matvec_q8_0_at", "fm_matvec_q8_0", NULL, NULL } }, +}; + +#define KERNEL_TABLE_LEN (sizeof(KERNEL_TABLE) / sizeof(KERNEL_TABLE[0])) + +/* + * Is `symbol` present in the running image? RTLD_DEFAULT searches the main + * executable first, which is where the Makefile's whole-program link puts + * foundry's Metal objects. + * + * Off Apple there is no Metal at all, so nothing is registered by definition — + * and this also keeps a Linux build (a K80 host) from needing -ldl. + */ +static int symbol_is_linked(const char *symbol) +{ +#ifdef __APPLE__ + return dlsym(RTLD_DEFAULT, symbol) != NULL; +#else + (void)symbol; + return 0; +#endif +} + +int pn_qwen35moe_metal_kernel_for_type(pn_gguf_quant type, + const char **name_out) +{ + if (name_out) + *name_out = NULL; + + for (size_t i = 0; i < KERNEL_TABLE_LEN; i++) { + if (KERNEL_TABLE[i].type != type) + continue; + for (size_t c = 0; c < 4; c++) { + const char *cand = KERNEL_TABLE[i].candidates[c]; + if (!cand) + break; + if (symbol_is_linked(cand)) { + if (name_out) + *name_out = cand; + return 1; + } + } + return 0; + } + return 0; /* type not in the table at all: no kernel is even planned */ +} diff --git a/src/models/qwen35moe_metal_kernel_registry.h b/src/models/qwen35moe_metal_kernel_registry.h new file mode 100644 index 0000000..cad0e43 --- /dev/null +++ b/src/models/qwen35moe_metal_kernel_registry.h @@ -0,0 +1,47 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_metal_kernel_registry.h + * @brief Which quant types the Metal backend currently has a matvec kernel for. + * Answers the question by looking the foundry entry point up in the + * running binary's symbol table, so the answer is linked evidence rather + * than a claim maintained by hand. + * + * @status Phase 1 of foundry/metal/qwen35moe_metal_plan.md. Q4_K and F32 have + * entry points today; Q3_K / Q5_K / Q6_K / Q8_0 do not. + * @issues None + * @todo None + */ + +#ifndef PN_QWEN35MOE_METAL_KERNEL_REGISTRY_H +#define PN_QWEN35MOE_METAL_KERNEL_REGISTRY_H + +#include "../formats/gguf.h" + +/** + * Resolve the Metal matvec entry point for a quant type. + * + * A type is "covered" when foundry exports an entry point that dispatches a + * matvec for it. That is tested by symbol lookup in the running image, not by a + * flag in a table here, for three reasons: + * + * 1. It cannot claim a kernel that is not linked. A hand-maintained boolean + * can, and the failure it hides — a weight fed to the wrong kernel — is + * silent garbage logits, which is the whole reason this gate exists. + * 2. It flips on its own the day a kernel lands, with no edit here. + * 3. In a build without USE_METAL nothing resolves, so coverage reports a + * truthful 0% instead of crashing. + * + * @param type GGUF quant type to look up + * @param name_out Receives the resolved symbol name (NULL when uncovered), + * or NULL if the caller does not want it. Static storage. + * @return 1 if a kernel is registered for `type`, else 0 + * + * Thread-safe: yes (read-only lookup) + * Reentrant: yes + * Signal-safe: no (dlsym) + */ +int pn_qwen35moe_metal_kernel_for_type(pn_gguf_quant type, + const char **name_out); + +#endif /* PN_QWEN35MOE_METAL_KERNEL_REGISTRY_H */ diff --git a/src/models/qwen35moe_moe.c b/src/models/qwen35moe_moe.c new file mode 100644 index 0000000..4cf8f56 --- /dev/null +++ b/src/models/qwen35moe_moe.c @@ -0,0 +1,249 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_moe.c + * @brief Qwen3.5-MoE shared-expert MoE FFN (A5). Runs on EVERY layer (both SSM + * and attention) on the post_attention_norm'd hidden state. Extends + * Pantheon's Mixtral-style routed MoE with softmax + norm_topk_prob + * gating and an always-on sigmoid-gated shared expert. + * + * @status A5 complete: routed softmax + top-k + norm_topk_prob + expert_weights + * _scale SwiGLU experts, plus the sigmoid-gated shared expert and add. + * Verified in isolation against a NumPy reference (< 1e-4 max abs diff). + * @issues None + * @todo None + */ + +/* 1. This module's own header */ +#include "qwen35moe_internal.h" + +/* 2. Standard library */ +#include +#include +#include + +/* 3. System / platform headers */ +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +/* 4. Project headers */ +#include "../accel/simd.h" +#include "../core/qweight.h" + +/* Hard cap on top-k experts — sized for the on-stack selection scratch. The + * Qwen3.5-MoE configs use 8; Mixtral uses 2. */ +#define PN_QWEN35MOE_MAX_TOPK 32 + +/* sigmoid(x) = 1 / (1 + exp(-x)). */ +static inline float qwen35moe_sigmoid(float x) +{ + return 1.0f / (1.0f + expf(-x)); +} + +/* ============================================================ + * Routed experts: softmax router -> top-k -> norm_topk_prob -> SwiGLU sum + * ============================================================ */ + +/* Softmax over the n_expert router logits, in place. */ +static void qwen35moe_router_softmax(float *logits, int n_expert) +{ + float max_gl = logits[0]; + for (int e = 1; e < n_expert; e++) + if (logits[e] > max_gl) + max_gl = logits[e]; + float sum_exp = 0.0f; + for (int e = 0; e < n_expert; e++) { + logits[e] = expf(logits[e] - max_gl); + sum_exp += logits[e]; + } + float inv = 1.0f / sum_exp; + for (int e = 0; e < n_expert; e++) + logits[e] *= inv; +} + +/* Select the top-k experts by probability (selection sort, k is small). + * Fills top_idx/top_wt and returns the sum of the selected weights. */ +static float qwen35moe_select_topk(const float *probs, int n_expert, int k, + int *top_idx, float *top_wt) +{ + float wt_sum = 0.0f; + for (int s = 0; s < k; s++) { + int best = -1; + float best_val = -1.0f; + for (int e = 0; e < n_expert; e++) { + int skip = 0; + for (int j = 0; j < s; j++) + if (top_idx[j] == e) { + skip = 1; + break; + } + if (!skip && probs[e] > best_val) { + best = e; + best_val = probs[e]; + } + } + top_idx[s] = best; + top_wt[s] = best_val; + wt_sum += best_val; + } + return wt_sum; +} + +/* SwiGLU for one expert: out = (silu(gate . x) * (up . x)) . down. + * scratch_gate/up/hid are [expert_ff], out is [n_embd]. */ +static void qwen35moe_swiglu_expert(const pn_qwen35moe_config *cfg, + const pn_qweight *gate, const pn_qweight *up, + const pn_qweight *down, const float *x, + int expert_ff, float *scratch_gate, + float *scratch_up, float *scratch_hid, + float *out) +{ + qwen35moe_mv(cfg, gate, x, scratch_gate); + qwen35moe_mv(cfg, up, x, scratch_up); + pn_vsilu_mul_f32(scratch_gate, scratch_up, scratch_hid, expert_ff); + qwen35moe_mv(cfg, down, scratch_hid, out); +} + +/* Routed-MoE branch: softmax router, top-k, norm_topk_prob renormalization, + * expert_weights_scale, then the weighted SwiGLU expert sum into moe_out. */ +static int qwen35moe_routed(const pn_qwen35moe_moe *moe, + const pn_qwen35moe_config *cfg, const float *x, + pn_arena *arena, float *moe_out) +{ + const int n_embd = cfg->n_embd; + const int n_expert = cfg->n_expert; + const int k = cfg->n_expert_used; + const int expert_ff = cfg->expert_ff; + + float *logits = pn_arena_alloc_f32(arena, n_expert); + float *exp_gate = pn_arena_alloc_f32(arena, expert_ff); + float *exp_up = pn_arena_alloc_f32(arena, expert_ff); + float *exp_hid = pn_arena_alloc_f32(arena, expert_ff); + float *exp_out = pn_arena_alloc_f32(arena, n_embd); + if (!logits || !exp_gate || !exp_up || !exp_hid || !exp_out) + return -1; + + qwen35moe_mv(cfg, &moe->ffn_gate_inp, x, logits); + qwen35moe_router_softmax(logits, n_expert); + + int top_idx[PN_QWEN35MOE_MAX_TOPK]; + float top_wt[PN_QWEN35MOE_MAX_TOPK]; + float wt_sum = qwen35moe_select_topk(logits, n_expert, k, top_idx, top_wt); + + /* norm_topk_prob: renormalize the selected weights to sum to 1, then apply + * the routed-weight scale (1.0 unless the GGUF carries the key). */ + float inv_wt = (wt_sum > 0.0f) ? (1.0f / wt_sum) : 0.0f; + float scale = cfg->expert_weights_scale * inv_wt; + for (int s = 0; s < k; s++) + top_wt[s] *= scale; + + memset(moe_out, 0, (size_t)n_embd * sizeof(float)); + for (int s = 0; s < k; s++) { + int e = top_idx[s]; + qwen35moe_swiglu_expert(cfg, &moe->gate_exps[e], &moe->up_exps[e], + &moe->down_exps[e], x, expert_ff, exp_gate, + exp_up, exp_hid, exp_out); + float w = top_wt[s]; + for (int j = 0; j < n_embd; j++) + moe_out[j] += w * exp_out[j]; + } + return 0; +} + +/* ============================================================ + * Shared expert (always on, sigmoid-gated) + * ============================================================ */ + +/* Shared-expert branch: shexp = sigmoid(ffn_gate_inp_shexp . x) * SwiGLU_shexp(x). + * The scalar sigmoid gate is applied ONLY to this branch. */ +static int qwen35moe_shared(const pn_qwen35moe_moe *moe, + const pn_qwen35moe_config *cfg, const float *x, + pn_arena *arena, float *shexp_out) +{ + const int n_embd = cfg->n_embd; + const int shared_ff = cfg->shared_ff; + + float *sh_gate = pn_arena_alloc_f32(arena, shared_ff); + float *sh_up = pn_arena_alloc_f32(arena, shared_ff); + float *sh_hid = pn_arena_alloc_f32(arena, shared_ff); + if (!sh_gate || !sh_up || !sh_hid) + return -1; + + qwen35moe_swiglu_expert(cfg, &moe->ffn_gate_shexp, &moe->ffn_up_shexp, + &moe->ffn_down_shexp, x, shared_ff, sh_gate, sh_up, + sh_hid, shexp_out); + + /* 1 scalar/token: project x -> single logit, sigmoid, scale the branch. + * ffn_gate_inp_shexp is a 1-D [n_embd] vector; the GGUF loader stores 1-D + * tensors as rows=n_embd/cols=1, so pn_qmv would emit n_embd outputs (and + * overflow a scalar). Compute the dot product directly instead — a single + * weight . x reduction over the embedding dim. + * + * Reported to the dispatch observer by hand precisely BECAUSE it bypasses + * pn_qmv: a GPU backend still owes this site a kernel, so leaving it + * unreported would let the coverage gate mistake a real dispatch for an + * unused weight. */ + pn_qweight_note_dispatch(&moe->ffn_gate_inp_shexp, PN_QWEIGHT_OP_DOT); + const float *gate_w = (const float *)moe->ffn_gate_inp_shexp.data; + float gate_logit = 0.0f; + for (int j = 0; j < n_embd; j++) + gate_logit += gate_w[j] * x[j]; + float sgate = qwen35moe_sigmoid(gate_logit); + for (int j = 0; j < n_embd; j++) + shexp_out[j] *= sgate; + return 0; +} + +/* ============================================================ + * Block entry point + * ============================================================ */ + +/* Dense-FFN branch (Bonsai): a single plain SwiGLU, no router, no scalar gate. + * gate/up/down live in the shared-expert slots; dim = ffn_length (= shared_ff). + * out = (silu(gate . x) * (up . x)) . down. Caller adds the residual. */ +static int qwen35moe_dense_ffn(const pn_qwen35moe_moe *moe, + const pn_qwen35moe_config *cfg, const float *x, + pn_arena *arena, float *out) +{ + const int ff = cfg->ffn_length; + float *g = pn_arena_alloc_f32(arena, ff); + float *u = pn_arena_alloc_f32(arena, ff); + float *h = pn_arena_alloc_f32(arena, ff); + if (!g || !u || !h) + return -1; + qwen35moe_swiglu_expert(cfg, &moe->ffn_gate_shexp, &moe->ffn_up_shexp, + &moe->ffn_down_shexp, x, ff, g, u, h, out); + return 0; +} + +int pn_qwen35moe_moe_block(const pn_qwen35moe_moe *moe, + const pn_qwen35moe_config *cfg, const float *x, + pn_arena *arena, float *out, struct fw_pool *pool) +{ + (void)pool; /* per-token MoE is cheap; threading reserved for A6. */ + if (!moe || !cfg || !x || !arena || !out) + return -1; + + /* Bonsai dense SwiGLU FFN — no MoE routing. */ + if (cfg->dense_ffn) + return qwen35moe_dense_ffn(moe, cfg, x, arena, out); + + if (cfg->n_expert_used > PN_QWEN35MOE_MAX_TOPK) + return -1; + + const int n_embd = cfg->n_embd; + + /* Routed experts -> out. */ + if (qwen35moe_routed(moe, cfg, x, arena, out) < 0) + return -1; + + /* Shared expert (sigmoid-gated) -> shexp, then out += shexp. */ + float *shexp = pn_arena_alloc_f32(arena, n_embd); + if (!shexp) + return -1; + if (qwen35moe_shared(moe, cfg, x, arena, shexp) < 0) + return -1; + pn_vadd_f32(out, shexp, out, n_embd); + return 0; +} diff --git a/src/models/qwen35moe_prefill_checkpoint_tt.c b/src/models/qwen35moe_prefill_checkpoint_tt.c new file mode 100644 index 0000000..cf7379a --- /dev/null +++ b/src/models/qwen35moe_prefill_checkpoint_tt.c @@ -0,0 +1,287 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_prefill_checkpoint_tt.c + * @brief MULTI-TURN PREFIX/STATE CACHING for Ornith on the card: the batched prefill's + * checkpoint, and the resume a later turn takes from it. + * + * --------------------------------------------------------------------------- + * WHY A CHECKPOINT AND NOT THE TURN'S END + * --------------------------------------------------------------------------- + * A hybrid model cannot rewind by longest common prefix: every GDN layer's state is a fold of + * the whole prefix. And the batched prefill is not bit-identical to the per-token path (the + * chunk mixers run 32 rows at a time), so the state at a turn's end (decode, per token) is + * not the state a cold prefill of the next prompt passes through. The one state that is: the + * state at a 32-row BAND boundary of a batched prefill. + * + * The passes are 2,048 rows (the tall strips), so a chat prompt is ONE pass, and inside it each + * SSM layer runs its chunk mixer one 32-row band at a time. Every op of a pass is row-local or + * runs per 32-row band at an absolute band position, so the state after band j is the same + * whatever pass it ran in. `qwen35moe_prefill_tt.c` therefore copies each SSM layer's state + * (conv window + recurrent state, card side, `pn_tt_gdn_checkpoint.c`) when its band loop + * reaches the LAST 32-row boundary of the batched range, and a later turn whose prompt starts + * with those tokens restores it and prefills from there: the same bands at the same positions, + * so a restored-then-tail run is the cold run. + * + * The boundary is also kept at or below the end of the history the next turn will resend + * (the serve's ceiling, qwen35moe_checkpoint_band.h): the template's generation prompt + * ("\n") is not in the re-rendered history, and a checkpoint that covers it can never + * be resumed — 1 turn in 32 re-prefilled its whole conversation before the ceiling. + * + * --------------------------------------------------------------------------- + * WHAT KEEPS IT VALID + * --------------------------------------------------------------------------- + * The attention K/V is not copied: its rows below the checkpoint are the card's own and stay + * right while the SAME sequence keeps running AT OR ABOVE the checkpoint. Every card forward, + * verify and prefill calls `pn_qwen35_tt_ckpt_note()`, and a forward for another pn_state, or + * at a position below the checkpoint, drops it. + * + * `pn_qwen35moe_tt_card_holds()` answers the same question for the prefix cache's APPEND tier, + * which had no card check at all: session A, then session B, then A's exact-history append ran + * per token over B's K/V and state. RED `card_holds_skip` restores that (must DIFFER). + * + * The checkpoint lives in card DRAM only. Nothing is written to the host's disk: user data + * never rests on the serving host (a blob for the user's machine would be a separate export). + * + * @status Per-layer capture at the last band boundary; resume through the serve's prefix cache. + * @issues One checkpoint a model (one sequence on the card). + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include + +#include "../accel/tt/pn_tt_gdn_checkpoint.h" +#include "qwen35moe.h" +#include "qwen35moe_checkpoint_band.h" +#include "../core/bench.h" + +/** `PN_TT_RED=`, spelled here: a model file does not reach into the device layer. */ +static bool ckpt_red(const char *name) +{ + const char *value = getenv("PN_TT_RED"); + + return value && *value && strcmp(value, name) == 0; +} + +bool pn_qwen35_tt_ckpt_enabled(void) +{ + const char *env = getenv("PN_TT_PREFILL_CKPT"); + + return !(env && *env && atoi(env) == 0); +} + +static void ckpt_drop(pn_qwen35_tt *g, const char *why) +{ + if (!g->ckpt.valid) + return; + fprintf(stderr, "qwen35_tt: prefill checkpoint at %d dropped — %s\n", g->ckpt.pos, why); + g->ckpt.valid = false; + g->ckpt.restored = false; +} + +void pn_qwen35_tt_ckpt_forget(pn_qwen35_tt *g) +{ + if (!g) + return; + if (g->ckpt.valid) + ckpt_drop(g, "a warm-up ran on the card"); + g->card_owner = NULL; +} + +void pn_qwen35_tt_ckpt_note(pn_qwen35_tt *g, const pn_state *state, int pos) +{ + if (!g) + return; + if (g->ckpt.valid && state != g->ckpt.owner) + ckpt_drop(g, "the card ran another sequence"); + else if (g->ckpt.valid && pos < g->ckpt.pos) + ckpt_drop(g, "a forward below it rewrites its K/V"); + if (g->ckpt.restored && pos != g->ckpt.pos) + g->ckpt.restored = false; + g->card_owner = state; +} + +bool pn_qwen35_tt_ckpt_restored_at(const pn_qwen35_tt *g, const pn_state *state, int pos) +{ + return g && g->ckpt.valid && g->ckpt.restored && g->ckpt.owner == state && + g->ckpt.pos == pos && pos > 0; +} + +/** Make room for `n` checkpoint tokens. */ +static int ckpt_tokens_reserve(pn_qwen35_tt *g, int n) +{ + uint32_t *grown; + + if (n <= g->ckpt.capacity) + return 0; + grown = realloc(g->ckpt.tokens, (size_t)n * sizeof(*grown)); + if (!grown) + return -1; + g->ckpt.tokens = grown; + g->ckpt.capacity = n; + return 0; +} + +void pn_qwen35_tt_ckpt_plan(pn_qwen35_tt *g, const pn_state *state, int base, int batch_end) +{ + int at, ceiling; + + if (!g) + return; + /* The ceiling is for THIS prefill of its state only: a later prefill (another request, + * a warm-up) must not inherit it. */ + ceiling = (state && g->ckpt.ceiling_owner == state) ? g->ckpt.ceiling : 0; + g->ckpt.ceiling = 0; + g->ckpt.ceiling_owner = NULL; + g->ckpt.pending_at = 0; + g->ckpt.pending_layers = 0; + g->ckpt.capture_ms = 0.0; + g->ckpt.verify_words = 0u; + g->ckpt.verify_differ = 0; + if (!state || !pn_qwen35_tt_ckpt_enabled() || !g->gdn_on_card || base % 32 != 0) + return; + at = pn_qwen35_ckpt_band_at(base, batch_end, ceiling); + if (at == 0) + return; /* no new full band: the checkpoint at `base`, if any, still stands */ + /* The tokens below `base` are the restored checkpoint's, which the resume matched against + * this prompt. A prefill from anywhere else has a prefix this file does not know. */ + if (base != 0 && !(g->ckpt.valid && g->ckpt.owner == state && g->ckpt.pos == base)) { + ckpt_drop(g, "a prefill from an unknown base"); + return; + } + if (ckpt_tokens_reserve(g, at) != 0) { + ckpt_drop(g, "no host memory for its tokens"); + return; + } + /* The copies overwrite the stored state layer by layer: never a half-taken checkpoint. */ + g->ckpt.valid = false; + g->ckpt.restored = false; + g->ckpt.pending_at = at; +} + +void pn_qwen35_tt_ckpt_band_done(pn_qwen35_tt *g, int ssm_index, int end) +{ + const double started = pn_time_ms(); + long long differ; + + if (!g || g->ckpt.pending_at == 0 || end != g->ckpt.pending_at) + return; + if (pn_tt_gdn_checkpoint_capture_layer(g->device, (uint32_t)ssm_index) != 0) { + g->ckpt.pending_at = 0; /* abandoned: the commit finds nothing to commit */ + return; + } + g->ckpt.capture_ms += pn_time_ms() - started; + g->ckpt.pending_layers++; + differ = pn_tt_gdn_checkpoint_verify_layer(g->device, (uint32_t)ssm_index, + &g->ckpt.verify_words); + if (differ < 0 || g->ckpt.verify_differ < 0) + g->ckpt.verify_differ = -1; + else + g->ckpt.verify_differ += differ; +} + +void pn_qwen35_tt_ckpt_commit(pn_qwen35_tt *g, const pn_state *state, const uint32_t *tokens, + int base, int n_ssm) +{ + const int at = g ? g->ckpt.pending_at : 0; + + if (!g || at == 0) + return; + g->ckpt.pending_at = 0; + if (g->ckpt.pending_layers != n_ssm) { + fprintf(stderr, "qwen35_tt: prefill checkpoint at %d NOT taken — %d of %d layers " + "captured\n", at, g->ckpt.pending_layers, n_ssm); + return; + } + memcpy(g->ckpt.tokens + base, tokens, (size_t)(at - base) * sizeof(*tokens)); + g->ckpt.pos = at; + g->ckpt.owner = state; + g->ckpt.valid = true; + fprintf(stderr, "qwen35_tt: prefill checkpoint at %d — %d layers copied in %.1f ms\n", at, + n_ssm, g->ckpt.capture_ms); + if (pn_tt_gdn_checkpoint_verify_wanted()) + fprintf(stderr, "pn_tt: gdn checkpoint — VERIFY after capture: %lld of %llu sampled " + "words DIFFER\n", g->ckpt.verify_differ, g->ckpt.verify_words); +} + +void pn_qwen35_tt_ckpt_release(pn_qwen35_tt *g) +{ + if (!g) + return; + free(g->ckpt.tokens); + memset(&g->ckpt, 0, sizeof(g->ckpt)); +} + +/** The resident model behind `model`, or NULL. */ +static pn_qwen35_tt *ckpt_model(const pn_model *model) +{ + const pn_qwen35moe_model *m; + + if (!model || !model->weights || model->forward != pn_qwen35moe_forward_tt) + return NULL; + m = (const pn_qwen35moe_model *)model->weights; + return (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)m); +} + +int pn_qwen35moe_tt_checkpoint_resume(pn_model *model, pn_state *state, const uint32_t *tokens, + int n) +{ + pn_qwen35_tt *g = ckpt_model(model); + const int pos = g ? g->ckpt.pos : 0; + + if (!g || !state || !tokens || !g->resident || !g->gdn_on_card || + !pn_qwen35_tt_ckpt_enabled() || !g->ckpt.valid) + return 0; + /* The card must hold this sequence (its K/V below `pos`), and a prompt token must be left + * over: the last one is the ordinary forward that produces the logits. */ + if (g->ckpt.owner != state || g->card_owner != state || pos >= n || + memcmp(tokens, g->ckpt.tokens, (size_t)pos * sizeof(*tokens)) != 0) + return 0; + if (pn_tt_gdn_checkpoint_restore(g->device) != 0) { + ckpt_drop(g, "the restore copy failed"); + return -1; + } + if (pn_tt_gdn_checkpoint_verify_wanted()) { + const int n_ssm = pn_qwen35moe_model_n_ssm((const pn_qwen35moe_model *)model->weights); + unsigned long long words = 0u; + long long differ = 0, one; + uint32_t layer; + + for (layer = 0; layer < (uint32_t)n_ssm; layer++) { + one = pn_tt_gdn_checkpoint_verify_layer(g->device, layer, &words); + differ = one < 0 || differ < 0 ? -1 : differ + one; + } + fprintf(stderr, "pn_tt: gdn checkpoint — VERIFY after restore: %lld of %llu sampled " + "words DIFFER\n", differ, words); + } + state->pos = pos; + g->ckpt.restored = true; + return pos; +} + +void pn_qwen35moe_tt_checkpoint_ceiling(pn_model *model, const pn_state *state, int ceiling) +{ + pn_qwen35_tt *g = ckpt_model(model); + + if (!g) + return; + g->ckpt.ceiling = ceiling; + g->ckpt.ceiling_owner = state; +} + +int pn_qwen35moe_tt_card_holds(const pn_model *model, const pn_state *state) +{ + const pn_qwen35_tt *g = ckpt_model(model); + + if (g && ckpt_red("card_holds_skip")) + return 1; + return g && state && g->card_owner == state ? 1 : 0; +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_prefill_overlap_tt.c b/src/models/qwen35moe_prefill_overlap_tt.c new file mode 100644 index 0000000..322e165 --- /dev/null +++ b/src/models/qwen35moe_prefill_overlap_tt.c @@ -0,0 +1,339 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_prefill_overlap_tt.c + * @brief PHASE W2 (gate W3a): an SSM layer's FFN runs on the prefill GRID, one SUPER-BAND of + * rows at a time, while the GDN chain mixes the next super-band. + * + * WHY. A tall prefill runs each layer as projections over every row, then the GDN chain band by + * band, then the out-projection and the FFN over every row — so while the chain runs (2.08 s + * of a 1,630-token prompt) the 48-core grid idles, and while the FFN runs (1.0 s) the chain's + * cores idle. The FFN is position-independent: rows [a, b) of it need only rows [a, b) of the + * mixer's output. So once the chain has finished super-band j (`PN_TT_PREFILL_OVERLAP` bands), + * that super-band's out-projection, residual, post-norm, FFN and residual can run while the + * chain moves on. + * + * WHAT OVERLAPS. Only the grid's projections (gate, up, down) run DETACHED + * (pn_tt_matmul_grid_issue_detached): the chain's ops flush the matvec chain on entry, and a + * detached projection is not on it. The short ops between them — the out-projection (decode + * group), the residual adds, the post-norm, the SwiGLU (wide eltwise) — run synchronously + * inside the chain at the moment their input is ready, polled between bands. + * + * WHY BIT-EXACT. Every op is today's op over a row sub-range of the same strips: each output + * row's arithmetic (its K reduction, its norm row, its elementwise tile) never depended on the + * other rows, and the M split is over rows. The mirrors of `normed` and `ffn_swiglu` written at + * a super-band base are keyed by `pn_tt_replica_map_set_superband()`. + * + * Knobs: `PN_TT_PREFILL_OVERLAP=S` bands a super-band (default 13, 0 off), `PN_TT_PREFILL_OVERLAP_LAYERS=N` + * only the first N SSM layers (the W3a gate). RED `prefill_overlap_shift`: the FFN of each + * super-band reads the mixer rows one band late — must DIFFER. + * + * @status Gate W3a. + * @issues Attention layers are not overlapped. + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include + +#include "../accel/tt/pn_tt_matmul_group.h" +#include "../accel/tt/pn_tt_gdngate.h" +#include "../accel/tt/pn_tt_reader.h" +#include "../core/bench.h" + +enum { OV_IDLE = 0, OV_GATE_UP, OV_DOWN }; + +static struct { + pn_qwen35_tt *g; + const pn_qwen35_tt_layer *d; + pn_qwen35_tt_overlap_next next; /**< the next SSM layer the job also prepares, or none */ + int prepared_ssm; /**< that layer's index once a super-band was prepared */ + uint32_t prepared_bands; /**< bands of it prepared so far */ + bool card_gates; /**< PM 7: prepare gate blocks on the card, not reads */ + bool prepared_card_gates; /**< the prepared layer's gates were issued on the card */ + int state; + int s0; /**< the hidden slot holding the layer's INPUT */ + uint32_t band, span; /**< the super-band's first band and band count */ + double sync_seconds; /**< time the chain spent on the job's synchronous steps */ + unsigned long long superbands; +} g_ov; + +bool pn_qwen35_tt_wavefront_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *env = getenv("PN_TT_PREFILL_WAVEFRONT"); + + /* DEFAULT ON (run_w_wavefront.sh 2026-10-08: serve TTFT 2.73 -> 2.69 s, CLI wall 2.973 + * -> 2.924, IDENTICAL, reds DIFFER); `PN_TT_PREFILL_WAVEFRONT=0` off. */ + cached = env && *env ? atoi(env) != 0 : 1; + } + return cached != 0; +} + +uint32_t pn_qwen35_tt_overlap_bands(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *env = getenv("PN_TT_PREFILL_OVERLAP"); + + /* DEFAULT 13 inside the streamed loop (run_w_stream3.sh 2026-10-08: CLI wall 3.22 -> + * 2.95 s; S 17 serve TTFT 2.98 -> 2.77 s); `PN_TT_PREFILL_OVERLAP=0` off. */ + cached = env && *env ? atoi(env) : 13; + if (cached < 0) + cached = 0; + } + return (uint32_t)cached; +} + +bool pn_qwen35_tt_overlap_layer(pn_qwen35_tt *g, int ssm_index, uint32_t bands) +{ + static int limit = -2; + const uint32_t sb = pn_qwen35_tt_overlap_bands(); + + if (limit == -2) { + const char *env = getenv("PN_TT_PREFILL_OVERLAP_LAYERS"); + + limit = env && *env ? atoi(env) : -1; + } + return g && sb && bands > sb && pn_tt_matmul_grid_is_running(g->device) && + (limit < 0 || ssm_index < limit); +} + +/** Strip `strip` narrowed to bands [band, band + span) — a COPY, so the chain's own strips, + * which the band loop keeps seeking, are never touched. */ +static int ov_view(const pn_tt_activation *strip, uint32_t band, uint32_t span, + pn_tt_activation *out) +{ + *out = *strip; + return pn_tt_activation_seek(out, band) == 0 && pn_tt_activation_span(out, span) == 0 ? 0 + : -1; +} + +/** Step 1: out-projection, residual, post-norm (all synchronous), gate + up (detached). */ +static int ov_start(void) +{ + pn_qwen35_tt *g = g_ov.g; + pn_qwen35_tt_strips *s = &g->card; + const bool red = getenv("PN_TT_RED") && strcmp(getenv("PN_TT_RED"), "prefill_overlap_shift") == 0; + const uint32_t in_band = red && g_ov.band + g_ov.span < s->ssm_gated.tile_rows + ? g_ov.band + 1u : g_ov.band; + pn_tt_activation gated, project, h_in, h_mid, normed, gate, up; + + if (ov_view(&s->ssm_gated, in_band, g_ov.span, &gated) != 0 || + ov_view(&s->project, g_ov.band, g_ov.span, &project) != 0 || + ov_view(&s->hidden[g_ov.s0], g_ov.band, g_ov.span, &h_in) != 0 || + ov_view(&s->hidden[g_ov.s0 ^ 1], g_ov.band, g_ov.span, &h_mid) != 0 || + ov_view(&s->normed, g_ov.band, g_ov.span, &normed) != 0 || + ov_view(&s->ffn_gate, g_ov.band, g_ov.span, &gate) != 0 || + ov_view(&s->ffn_up, g_ov.band, g_ov.span, &up) != 0) + return -1; + if (pn_qwen35_tt_project(g, &g_ov.d->ssm_out, &gated, &project, true) != 0 || + pn_tt_add(g->device, &h_in, &project, &h_mid) != 0 || + pn_tt_rms_norm(g->device, &g_ov.d->post_norm, &h_mid, &normed) != 0 || + pn_tt_matmul_grid_issue_detached(g->device, &g_ov.d->ffn_gate, &normed, &gate) != 0 || + pn_tt_matmul_grid_issue_detached(g->device, &g_ov.d->ffn_up, &normed, &up) != 0) + return -1; + g_ov.state = OV_GATE_UP; + return 0; +} + +/** Step 2: SwiGLU (synchronous) and down (detached). */ +static int ov_swiglu_down(void) +{ + pn_qwen35_tt *g = g_ov.g; + pn_qwen35_tt_strips *s = &g->card; + pn_tt_activation gate, up, swiglu, project; + + if (ov_view(&s->ffn_gate, g_ov.band, g_ov.span, &gate) != 0 || + ov_view(&s->ffn_up, g_ov.band, g_ov.span, &up) != 0 || + ov_view(&s->ffn_swiglu, g_ov.band, g_ov.span, &swiglu) != 0 || + ov_view(&s->project, g_ov.band, g_ov.span, &project) != 0) + return -1; + if (pn_tt_silu_mul(g->device, &gate, &up, &swiglu) != 0 || + pn_tt_matmul_grid_issue_detached(g->device, &g_ov.d->ffn_down, &swiglu, &project) != 0) + return -1; + g_ov.state = OV_DOWN; + return 0; +} + +/** + * Step 4 (the WAVEFRONT): this super-band of the NEXT SSM layer — its attn-norm, its q/k/v and z + * projections (grid, detached), its alpha/beta (decode group) and their reads, gated on the card + * having run them. So the reader starts the next layer while this one still mixes. Every op is + * the one the next layer's start would have run, over these rows. + */ +static int ov_prepare_next(void) +{ + pn_qwen35_tt *g = g_ov.g; + pn_qwen35_tt_strips *s = &g->card; + const pn_qwen35_tt_layer *n = g_ov.next.d; + pn_tt_activation h, normed, qkv, z, alpha, beta; + pn_tt_reader_gate gate; + + if (ov_view(&s->hidden[g_ov.s0], g_ov.band, g_ov.span, &h) != 0 || + ov_view(&s->normed, g_ov.band, g_ov.span, &normed) != 0 || + ov_view(&s->ssm_qkv, g_ov.band, g_ov.span, &qkv) != 0 || + ov_view(&s->ssm_z, g_ov.band, g_ov.span, &z) != 0 || + ov_view(&s->ssm_alpha, g_ov.band, g_ov.span, &alpha) != 0 || + ov_view(&s->ssm_beta, g_ov.band, g_ov.span, &beta) != 0) + return -1; + if (pn_tt_rms_norm(g->device, &n->attn_norm, &h, &normed) != 0 || + pn_tt_matmul_grid_issue_detached(g->device, &n->attn_qkv, &normed, &qkv) != 0 || + pn_tt_matmul_grid_issue_detached(g->device, &n->attn_gate, &normed, &z) != 0 || + pn_qwen35_tt_project(g, &n->ssm_alpha, &normed, &alpha, false) != 0 || + pn_qwen35_tt_project(g, &n->ssm_beta, &normed, &beta, false) != 0 || + pn_tt_matvec_flush(g->device) != 0) + return -1; + /* PM 7 ITEM 5: with the gates on the card these rows' gate blocks are computed there, + * after the flush above (the projections that wrote alpha/beta are done) — no reads. */ + if (g_ov.card_gates) { + if (pn_tt_gdngate_issue_bands(g->device, (uint32_t)g_ov.next.ssm_index, &s->ssm_alpha, + &s->ssm_beta, (uint32_t)g_ov.next.rows, + pn_qwen35_tt_gdn_gate_sub_rows(), g_ov.band, + g_ov.span) != 0) + return -1; + } else if (!pn_tt_matmul_group_member0_completion(g->device, &gate.x, &gate.y, + &gate.control_address, &gate.target) || + !pn_qwen35_tt_ab_reader_post_next(g, g_ov.next.ssm_index, (int)g_ov.band, + (int)g_ov.span, g_ov.next.rows, + g_ov.next.alpha, g_ov.next.alpha_width, + g_ov.next.beta, g_ov.next.beta_width, &gate)) { + return -1; + } + if (g_ov.band == 0u) { + g_ov.prepared_ssm = g_ov.next.ssm_index; + g_ov.prepared_bands = 0u; + g_ov.prepared_card_gates = g_ov.card_gates; + } + if (g_ov.prepared_ssm == g_ov.next.ssm_index && g_ov.prepared_bands == g_ov.band) + g_ov.prepared_bands += g_ov.span; + return 0; +} + +/** Step 3: the FFN residual (synchronous): hidden[s0] = hidden[s0^1] + project; then step 4. */ +static int ov_finish(void) +{ + pn_qwen35_tt *g = g_ov.g; + pn_qwen35_tt_strips *s = &g->card; + pn_tt_activation h_mid, project, h_out; + + if (ov_view(&s->hidden[g_ov.s0 ^ 1], g_ov.band, g_ov.span, &h_mid) != 0 || + ov_view(&s->project, g_ov.band, g_ov.span, &project) != 0 || + ov_view(&s->hidden[g_ov.s0], g_ov.band, g_ov.span, &h_out) != 0 || + pn_tt_add(g->device, &h_mid, &project, &h_out) != 0) + return -1; + g_ov.state = OV_IDLE; + if (g_ov.next.d && ov_prepare_next() != 0) { + g_ov.prepared_ssm = -1; /* the next layer runs its own start */ + return -1; + } + return 0; +} + +bool pn_qwen35_tt_overlap_prepared(int ssm_index, uint32_t bands) +{ + return g_ov.prepared_ssm == ssm_index && g_ov.prepared_bands >= bands; +} + +bool pn_qwen35_tt_overlap_gates_prepared(int ssm_index, uint32_t bands) +{ + return pn_qwen35_tt_overlap_prepared(ssm_index, bands) && g_ov.prepared_card_gates; +} + +void pn_qwen35_tt_overlap_set_card_gates(bool on) +{ + g_ov.card_gates = on; +} + +void pn_qwen35_tt_overlap_forget(void) +{ + g_ov.prepared_ssm = -1; + g_ov.prepared_bands = 0u; +} + +/** Run the job's next step if its detached projections are done; `block` waits for them. */ +static int ov_step(bool block) +{ + const double at = pn_time_ms(); + int rc = 0, done; + + if (g_ov.state == OV_IDLE) + return 0; + done = block ? (pn_tt_matmul_grid_detached_wait(g_ov.g->device) == FT_TT_OK ? 1 : -1) + : pn_tt_matmul_grid_detached_poll(g_ov.g->device); + if (done < 0) + rc = -1; + else if (done == 1) + rc = g_ov.state == OV_GATE_UP ? ov_swiglu_down() : ov_finish(); + g_ov.sync_seconds += (pn_time_ms() - at) / 1000.0; + return rc; +} + +int pn_qwen35_tt_overlap_advance(pn_qwen35_tt *g) +{ + (void)g; + return ov_step(false); +} + +int pn_qwen35_tt_overlap_drain(pn_qwen35_tt *g) +{ + (void)g; + while (g_ov.state != OV_IDLE) + if (ov_step(true) != 0) { + g_ov.state = OV_IDLE; + return -1; + } + return 0; +} + +int pn_qwen35_tt_overlap_superband(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, uint32_t band, + uint32_t span, const pn_qwen35_tt_overlap_next *next) +{ + double at; + int rc; + + if (pn_qwen35_tt_overlap_drain(g) != 0) + return -1; + at = pn_time_ms(); + g_ov.g = g; + g_ov.d = d; + g_ov.s0 = g->card.slot; + g_ov.band = band; + g_ov.span = span; + if (next && next->d && pn_qwen35_tt_wavefront_enabled()) + g_ov.next = *next; + else + memset(&g_ov.next, 0, sizeof(g_ov.next)); + g_ov.superbands++; + rc = ov_start(); + g_ov.sync_seconds += (pn_time_ms() - at) / 1000.0; + if (rc != 0) + g_ov.state = OV_IDLE; + return rc; +} + +double pn_qwen35_tt_overlap_take_seconds(void) +{ + const double seconds = g_ov.sync_seconds; + + g_ov.sync_seconds = 0.0; + return seconds; +} + +void pn_qwen35_tt_overlap_report(void) +{ + if (g_ov.superbands) + fprintf(stderr, "qwen35_tt: prefill OVERLAP — %llu super-bands of %u bands, FFN on the " + "grid under the GDN chain\n", + g_ov.superbands, pn_qwen35_tt_overlap_bands()); +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_prefill_prefetch_tt.c b/src/models/qwen35moe_prefill_prefetch_tt.c new file mode 100644 index 0000000..e95189c --- /dev/null +++ b/src/models/qwen35moe_prefill_prefetch_tt.c @@ -0,0 +1,393 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_prefill_prefetch_tt.c + * @brief The batched prefill's alpha/beta READS for the next 32-row band, hidden under the + * current band's GDN chunk descriptors. + * + * Every SSM layer of a pass reads `ssm_alpha` and `ssm_beta` down a band at a time (the host + * owes `softplus`/`exp`/`sigmoid`, which the SFPU cannot do) — 1,224 band-layers and ~1.05 s of + * a 1,630-token prefill at the link's ~5.3 MB/s readback, every one of them with the card idle. + * All of a layer's bands are projected before its band loop starts, so band b+1's rows are + * already on the card while band b's mixers run. This reads them then: alpha inside the CONV + * descriptor's enqueue/await window (the gdnc AUX overlap slot — the first slot is the DELTA + * staging's), beta inside the DELTA's (the alpha hook arms the beta one). Same bytes, same host buffers, earlier: bit-identical by + * construction. A read that did not happen (no window opened, a failure) is just done by the + * band itself, as before. + * + * `PN_TT_PREFILL_AB_PREFETCH=0` off. RED `prefill_ab_prefetch_stale`: the prefetch reads the + * CURRENT band's rows into the next band's place (must DIFFER). + * + * @status Written; gated by run_prefill_ab_prefetch.sh. + * @issues The first band of each layer is still read inline (its rows exist only after that + * layer's projections). + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include + +#include "../accel/tt/pn_tt_gdnc.h" +#include "../accel/tt/pn_tt_reader.h" +#include "../accel/tt/pn_tt_dispatch.h" +#include "../accel/tt/pn_tt_matmul_group.h" +#include "tt/ft_tt_op_l1_layout.h" +#include "../core/bench.h" + +/** One band's pending reads. Static: one prefill runs at a time on a model's card. */ +static struct { + pn_qwen35_tt *g; + int ssm_index, band, rows; + uint32_t alpha_width, beta_width; + float *alpha, *beta; /**< host rows of that band */ + bool alpha_done, beta_done; + bool armed; + double seconds; /**< reads done inside a window (cumulative, reported) */ + unsigned long long reads; +} g_ab; + +static bool ab_red(const char *name) +{ + const char *value = getenv("PN_TT_RED"); + + return value && *value && strcmp(value, name) == 0; +} + +bool pn_qwen35_tt_ab_prefetch_enabled(void) +{ + const char *env = getenv("PN_TT_PREFILL_AB_PREFETCH"); + + return !(env && *env && atoi(env) == 0); +} + +/** Read `rows` of band `band` of `strip` (only that strip is seeked; the band loop re-seeks + * every strip at its next band). */ +static int ab_read(pn_tt_activation *strip, int band, float *host, uint32_t width, int rows) +{ + const double at = pn_time_ms(); + const int read_band = ab_red("prefill_ab_prefetch_stale") ? band - 1 : band; + int rc; + + if (pn_tt_activation_seek(strip, (uint32_t)read_band) != 0) + return -1; + rc = pn_tt_activation_download_rows(g_ab.g->device, strip, host, width, (uint32_t)rows); + g_ab.seconds += (pn_time_ms() - at) / 1000.0; + g_ab.reads++; + return rc; +} + +static int ab_hook_beta(void *context) +{ + (void)context; + if (!g_ab.armed || g_ab.beta_done) + return 0; + g_ab.beta_done = ab_read(&g_ab.g->card.ssm_beta, g_ab.band, g_ab.beta, g_ab.beta_width, + g_ab.rows) == 0; + return g_ab.beta_done ? 0 : -1; +} + +static int ab_hook_alpha(void *context) +{ + (void)context; + if (!g_ab.armed || g_ab.alpha_done) + return 0; + g_ab.alpha_done = ab_read(&g_ab.g->card.ssm_alpha, g_ab.band, g_ab.alpha, g_ab.alpha_width, + g_ab.rows) == 0; + /* The next gdnc window (the DELTA) takes the beta read. */ + pn_tt_gdnc_set_overlap_aux(g_ab.g->device, ab_hook_beta, NULL); + return g_ab.alpha_done ? 0 : -1; +} + +void pn_qwen35_tt_ab_prefetch_arm(pn_qwen35_tt *g, int ssm_index, int band, int rows, + float *alpha, uint32_t alpha_width, float *beta, + uint32_t beta_width) +{ + if (!g || !pn_qwen35_tt_ab_prefetch_enabled() || rows <= 0) + return; + g_ab.g = g; + g_ab.ssm_index = ssm_index; + g_ab.band = band; + g_ab.rows = rows; + g_ab.alpha = alpha; + g_ab.beta = beta; + g_ab.alpha_width = alpha_width; + g_ab.beta_width = beta_width; + g_ab.alpha_done = false; + g_ab.beta_done = false; + g_ab.armed = true; + pn_tt_gdnc_set_overlap_aux(g->device, ab_hook_alpha, NULL); +} + +void pn_qwen35_tt_ab_prefetch_settle(pn_qwen35_tt *g) +{ + if (!g || !g_ab.armed) + return; + /* A window that never opened leaves its hook set: it must not fire on some later + * descriptor (the next layer has overwritten these strips by then). */ + pn_tt_gdnc_set_overlap_aux(g->device, NULL, NULL); +} + +bool pn_qwen35_tt_ab_prefetched(const pn_qwen35_tt *g, int ssm_index, int band, bool beta) +{ + if (!g || !g_ab.armed || g_ab.g != g || g_ab.ssm_index != ssm_index || g_ab.band != band) + return false; + return beta ? g_ab.beta_done : g_ab.alpha_done; +} + +void pn_qwen35_tt_ab_prefetch_reset(void) +{ + g_ab.armed = false; +} + +/* ---- PHASE W: the reader-thread mode ---------------------------------------------------- */ + +/** A tall pass is at most 64 bands (QWEN35_TT_TALL_MAX_ROWS). */ +#define AB_READER_MAX_BANDS 64 + +/** One layer's tickets: [0] alpha, [1] beta, per band; 0 = not posted. */ +typedef struct ab_reader_slot { + uint64_t ticket[2][AB_READER_MAX_BANDS]; + int ssm_index; + int bands; /**< bands the layer has */ + int posted; /**< bands posted so far */ + bool armed; +} ab_reader_slot; + +/** `cur` is the layer whose band loop is running; `nxt` the next SSM layer, whose bands the + * W2 wavefront posts super-band by super-band while `cur` still mixes. */ +static struct { + ab_reader_slot cur, nxt; + double wait_seconds; +} g_abr; + +/** The DRAM address of band `band` of `strip`, whatever band it is seeked to — computed, so the + * strip struct the issuing thread keeps moving is never touched by the post. */ +static uint32_t ab_band_address(const pn_tt_activation *strip, int band) +{ + const uint32_t band_bytes = strip->tile_count * FT_TT_OP_TILE_BYTES_BF16; + + return strip->address - strip->row_offset * band_bytes + (uint32_t)band * band_bytes; +} + +/** Post bands [first, first + count) of the alpha/beta strips into `slot`, gated or not. */ +static bool ab_post_bands(pn_qwen35_tt *g, ab_reader_slot *slot, int first, int count, int rows, + float *alpha, uint32_t alpha_width, float *beta, uint32_t beta_width, + const pn_tt_reader_gate *gate) +{ + const pn_tt_activation *a = &g->card.ssm_alpha, *b = &g->card.ssm_beta; + /* RED `prefill_reader_stale`: band b is read from band b-1's rows — must DIFFER. */ + const bool stale = ab_red("prefill_reader_stale"); + int band; + + for (band = first; band < first + count; band++) { + const int done = band * 32; + const int take = rows - done < 32 ? rows - done : 32; + const int src = stale && band > 0 ? band - 1 : band; + + if (take <= 0 || band >= AB_READER_MAX_BANDS) + return false; + slot->ticket[0][band] = pn_tt_reader_post_rows_gated( + g->device, a->bank, ab_band_address(a, src), a->tile_count, (uint32_t)take, + alpha + (size_t)done * alpha_width, alpha_width, gate); + slot->ticket[1][band] = pn_tt_reader_post_rows_gated( + g->device, b->bank, ab_band_address(b, src), b->tile_count, (uint32_t)take, + beta + (size_t)done * beta_width, beta_width, gate); + slot->posted = band + 1; + } + return true; +} + +bool pn_qwen35_tt_ab_reader_post(pn_qwen35_tt *g, int rows, float *alpha, uint32_t alpha_width, + float *beta, uint32_t beta_width) +{ + const pn_tt_activation *a = &g->card.ssm_alpha, *b = &g->card.ssm_beta; + const int bands = (rows + 31) / 32; + + g_abr.cur.armed = false; + if (!g || !pn_tt_reader_available(g->device) || rows <= 0 || bands > AB_READER_MAX_BANDS || + a->replica_count > 1u || b->replica_count > 1u) + return false; + g_abr.cur.bands = bands; + g_abr.cur.posted = 0; + if (!ab_post_bands(g, &g_abr.cur, 0, bands, rows, alpha, alpha_width, beta, beta_width, + NULL)) + return false; + g_abr.cur.armed = true; + return true; +} + +bool pn_qwen35_tt_ab_reader_post_next(pn_qwen35_tt *g, int ssm_index, int first, int count, + int rows, float *alpha, uint32_t alpha_width, float *beta, + uint32_t beta_width, const pn_tt_reader_gate *gate) +{ + const int bands = (rows + 31) / 32; + + if (!g || !pn_tt_reader_available(g->device) || bands > AB_READER_MAX_BANDS) + return false; + if (first == 0) { + memset(&g_abr.nxt, 0, sizeof(g_abr.nxt)); + g_abr.nxt.ssm_index = ssm_index; + g_abr.nxt.bands = bands; + } + if (g_abr.nxt.ssm_index != ssm_index || first != g_abr.nxt.posted || + !ab_post_bands(g, &g_abr.nxt, first, count, rows, alpha, alpha_width, beta, beta_width, + gate)) { + g_abr.nxt.armed = false; + return false; + } + g_abr.nxt.armed = g_abr.nxt.posted == g_abr.nxt.bands; + return true; +} + +bool pn_qwen35_tt_ab_reader_promote(int ssm_index) +{ + if (!g_abr.nxt.armed || g_abr.nxt.ssm_index != ssm_index) + return false; + g_abr.cur = g_abr.nxt; + memset(&g_abr.nxt, 0, sizeof(g_abr.nxt)); + return true; +} + +int pn_qwen35_tt_ab_reader_take(pn_qwen35_tt *g, int band) +{ + const double at = pn_time_ms(); + int rc; + + if (!g_abr.cur.armed || band < 0 || band >= g_abr.cur.bands || + g_abr.cur.ticket[0][band] == 0u || g_abr.cur.ticket[1][band] == 0u) + return -1; + rc = (pn_tt_reader_wait(g->device, g_abr.cur.ticket[0][band]) == 0 && + pn_tt_reader_wait(g->device, g_abr.cur.ticket[1][band]) == 0) ? 0 : -1; + g_abr.wait_seconds += (pn_time_ms() - at) / 1000.0; + return rc; +} + +void pn_qwen35_tt_ab_reader_end(pn_qwen35_tt *g) +{ + int band; + + /* Only THIS layer's reads: the next layer's are the wavefront's and must keep running. */ + if (g_abr.cur.armed && g) + for (band = g_abr.cur.bands - 1; band >= 0; band--) + if (g_abr.cur.ticket[1][band]) { + (void)pn_tt_reader_wait(g->device, g_abr.cur.ticket[1][band]); + break; + } + g_abr.cur.armed = false; +} + +void pn_qwen35_tt_ab_reader_abandon(pn_qwen35_tt *g) +{ + pn_qwen35_tt_ab_reader_end(g); + if (g) + pn_tt_reader_drain_cancelled(g->device); + memset(&g_abr.nxt, 0, sizeof(g_abr.nxt)); +} + +/* ---- PM 6: the VERIFY's alpha/beta on the reader (no stream boundary) ------------------- */ + +/** One verify layer's two reads, posted after its projections and taken before the host math. */ +static struct { + uint64_t ticket[2]; + pn_qwen35_tt *g; + float *alpha, *beta; + uint32_t alpha_width, beta_width; + int rows; + bool armed; + double wait_seconds; + unsigned long long posts; +} g_abv; + +static bool ab_verify_enabled(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *env = getenv("PN_TT_SPEC_AB_READER"); + + cached = env && *env ? atoi(env) != 0 : 1; + } + return cached != 0; +} + +bool pn_qwen35_tt_ab_verify_post(pn_qwen35_tt *g, int rows, float *alpha, uint32_t alpha_width, + float *beta, uint32_t beta_width) +{ + const pn_tt_activation *a = &g->card.ssm_alpha, *b = &g->card.ssm_beta; + pn_tt_reader_gate gate; + /* RED `spec_ab_ungated`: the reads go out with no gate — before the card has run the + * projections they read; must DIFFER. */ + const bool ungated = ab_red("spec_ab_ungated"); + + g_abv.armed = false; + if (!g || !ab_verify_enabled() || rows <= 0 || rows > 32 || a->replica_count > 1u || + b->replica_count > 1u || !pn_tt_reader_available(g->device) || + !pn_tt_matmul_group_member0_completion(g->device, &gate.x, &gate.y, + &gate.control_address, &gate.target)) + return false; + g_abv.ticket[0] = pn_tt_reader_post_rows_gated(g->device, a->bank, ab_band_address(a, 0), + a->tile_count, (uint32_t)rows, alpha, + alpha_width, ungated ? NULL : &gate); + g_abv.ticket[1] = pn_tt_reader_post_rows_gated(g->device, b->bank, ab_band_address(b, 0), + b->tile_count, (uint32_t)rows, beta, + beta_width, ungated ? NULL : &gate); + if (g_abv.ticket[0] == 0u || g_abv.ticket[1] == 0u) { + pn_tt_reader_drain_cancelled(g->device); + return false; + } + g_abv.g = g; + g_abv.alpha = alpha; + g_abv.beta = beta; + g_abv.alpha_width = alpha_width; + g_abv.beta_width = beta_width; + g_abv.rows = rows; + g_abv.armed = true; + g_abv.posts++; + /* The projections the gate waits on are still in the host's capture: launch them. */ + (void)pn_tt_dispatch_stream_kick(); + return true; +} + +int pn_qwen35_tt_ab_verify_wait(pn_qwen35_tt *g) +{ + const double at = pn_time_ms(); + int rc = 0; + + if (!g_abv.armed || g_abv.g != g) + return 0; + g_abv.armed = false; + /* What the host captured since (widen, CONV, L2) runs while the reads land. */ + (void)pn_tt_dispatch_stream_kick(); + if (pn_tt_reader_wait(g->device, g_abv.ticket[0]) != 0 || + pn_tt_reader_wait(g->device, g_abv.ticket[1]) != 0) { + /* A failed read is re-read through the stream (a boundary: slower, the same bytes). */ + rc = (pn_tt_activation_download_rows(g->device, &g->card.ssm_alpha, g_abv.alpha, + g_abv.alpha_width, (uint32_t)g_abv.rows) == 0 && + pn_tt_activation_download_rows(g->device, &g->card.ssm_beta, g_abv.beta, + g_abv.beta_width, (uint32_t)g_abv.rows) == 0) + ? 0 : -1; + } + g_abv.wait_seconds += (pn_time_ms() - at) / 1000.0; + return rc; +} + +void pn_qwen35_tt_ab_prefetch_report(void) +{ + if (g_abv.posts) + fprintf(stderr, "qwen35_tt: verify alpha/beta READER — %llu layers, %.3f s waited " + "(cumulative)\n", g_abv.posts, g_abv.wait_seconds); + if (g_abr.wait_seconds > 0.0) + fprintf(stderr, "qwen35_tt: prefill alpha/beta READER — %.3f s waited on the reader " + "thread (cumulative)\n", g_abr.wait_seconds); + if (g_ab.reads == 0u) + return; + fprintf(stderr, "qwen35_tt: prefill alpha/beta PREFETCH — %llu reads under gdnc windows " + "(%.3f s, cumulative)\n", g_ab.reads, g_ab.seconds); +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_prefill_tt.c b/src/models/qwen35moe_prefill_tt.c new file mode 100644 index 0000000..8f49a06 --- /dev/null +++ b/src/models/qwen35moe_prefill_tt.c @@ -0,0 +1,2200 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_prefill_tt.c + * @brief BATCHED PREFILL for Ornith: up to 32 prompt positions through one set of + * projections, with the two mixers still one position at a time. + * + * --------------------------------------------------------------------------- + * WHY THIS IS NOT THE LLAMA BATCHED PREFILL + * --------------------------------------------------------------------------- + * `llama_forward_tt.c` batches a prompt by putting 32 positions in rows 0..31 of every strip + * and letting the card multiply the rows it was multiplying anyway. It can do that because it + * REFUSES on-card attention: with attention on the host, every position-dependent step is + * host arithmetic over host rows, and nothing on the card ever needs to know which row is + * which token. + * + * Ornith is the opposite arm. Both of its mixers run WHOLE ON THE CARD, and both are + * single-position by construction: + * + * - the Gated-Delta-Net op advances a carried state in card DRAM, one position per pair of + * descriptors, and the recurrence is sequential in time by definition; + * - `pn_tt_rope`, `pn_tt_kv_append` and flash decode each take ONE `pos`. + * + * And every one of those ops reads LOGICAL ROW 0 of its input strip. **There is no on-card + * row gather** — the op set has BROADCAST_COL and nothing that moves row `r` of a tile to row + * 0 — so a 32-row pass can batch the norms, the nine projections a layer, the SwiGLU and the + * residual adds, and it must then reach each of its 32 rows THROUGH THE LINK: pull the + * mixer's input down as whole tiles once a layer, put row `r` back as row 0, run the mixer, + * pull its row-0 answer back, and re-upload all 32 rows for the output projection. + * + * That trade is the whole of this file and it is not obviously a win, which is why it is + * measured rather than argued and why it defaults OFF: + * + * WHAT IT SAVES the projections, the model RMS norms, the SwiGLU and the residual adds + * stop being paid per position. A matvec is `Mt=1` and the FPU already + * multiplies 32 rows against every weight tile it reads; 31 of them were + * zero padding. + * WHAT IT COSTS one n_embd-wide row-0 read and one strip-wide row-0 write per LAYER per + * POSITION — the traffic Phase A spent the whole program removing + * (readback 1478.09 -> 6.09 KiB/token). + * + * `PN_TT_PREFILL_REPORT=1` prints the split, so the arm answers its own question. + * + * --------------------------------------------------------------------------- + * WHAT IS AND IS NOT BATCHED, AND WHY EACH + * --------------------------------------------------------------------------- + * BATCHED `pn_tt_rms_norm` submits `row_blocks = 1`, and one row block IS 32 rows, + * each reduced over its own row. + * every projection `C[m][n]` sums over k and reads only row m of A. + * `pn_tt_add`, per element, so per row already. + * `pn_tt_silu_mul` + * PER ROW the GDN block a recurrence; row r's answer depends on row r-1's state. + * the attention core rotary, K/V append and the flash decode each name a + * position, and row r must attend rows <= r. + * + * The per-row loops run STRICTLY IN POSITION ORDER, and that is a correctness requirement + * rather than a convenience: the loop order IS the causal mask and IS the recurrence's time + * axis. `PN_TT_RED=prefill_causal` runs it backwards, which is the mutation that proves so. + * + * --------------------------------------------------------------------------- + * NO LM HEAD RUNS IN A BATCHED PASS + * --------------------------------------------------------------------------- + * The caller hands the LAST prompt token to the ordinary single-token forward, which is what + * produces the logits — so the head, the top-N and the sparse-logit presentation are literally + * the shipping code and cannot diverge. It also makes the acceptance argument short: if the + * card state this writes is what the sequential path writes, the logits are what the + * sequential path produces, because they come out of the same function reading the same card. + * + * @status Written, default OFF. `PN_TT_PREFILL=1` arms it. + * @issues The gather is host-mediated because the op set has no row gather. If the link makes + * that a net loss, the answer is an on-card row-select descriptor (or the chunkwise + * parallel delta rule, which subsumes it) and not a tuning of this file. + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include + +#include "../accel/tt/pn_tt_dispatch.h" +#include "../accel/tt/pn_tt_eltwise_group.h" +#include "../accel/tt/pn_tt_gdn.h" +#include "../accel/tt/pn_tt_gdngate.h" +#include "../accel/tt/pn_tt_head_norm.h" +#include "../accel/tt/pn_tt_matmul_group.h" +#include "../accel/tt/pn_tt_replica_map.h" +#include "../accel/tt/pn_tt_residency.h" +#include "../core/bench.h" +#include "qwen35moe.h" +#include "qwen35moe_prefill_tt_wall.h" +#include "../core/sample_profiler.h" + +/** + * **ON BY DEFAULT as of 2026-08-25.** Only an explicit `0` puts prefill back on the per-token + * path. + * + * IT WAS OFF UNTIL THE MEASUREMENT SAID WHICH WAY IT GOES, and it now has. The batched + * plumbing on its own is NUMERICALLY EXACT — `PN_TT_PREFILL=1` with both GDN mixers still the + * sequential decode family scores BIT-IDENTICALLY to the per-token path over 339 positions, + * every figure to every printed digit — so the batched projections, the model norms, the + * SwiGLU, the residual adds and the host row select contribute EXACTLY ZERO. What it buys is + * the batched attention and the projections; what makes it worth having is + * `PN_TT_PREFILL_GDNC`, which is on with it. + */ +#define PN_QWEN35_TT_PREFILL_ENV "PN_TT_PREFILL" + +/** Rows of one batched pass. 32 is the tile and the default; 1 turns the batched path into + * the sequential computation THROUGH THE BATCHED CODE, which is the control that separates a + * plumbing defect from a batching defect. */ +#define PN_QWEN35_TT_PREFILL_ROWS_ENV "PN_TT_PREFILL_ROWS" + +/** Print the transfer / sequential / batched split at the end of the prefill. */ +#define PN_QWEN35_TT_PREFILL_REPORT_ENV "PN_TT_PREFILL_REPORT" + +/** + * `PN_TT_PREFILL_ATTN_CHUNK=0` puts the ATTENTION layers back on the per-position path while + * the SSM layers stay chunked. ON by default, because the chunk is bit-exact there. + * + * IT IS AN ISOLATION KNOB AND IT EXISTS BECAUSE THE TWO CHUNK PATHS CANNOT OTHERWISE BE + * SEPARATED. `PN_TT_PREFILL_ROWS` moves both at once and it moves them differently: attention + * needs a chunk whose first position is a multiple of 32, so at ROWS=4 only the FIRST pass + * takes the attention chunk while every pass takes the SSM one. A chunk-size sweep read + * through that confound cannot say which of the two a divergence belongs to. + */ +#define PN_QWEN35_TT_PREFILL_ATTN_CHUNK_ENV "PN_TT_PREFILL_ATTN_CHUNK" + +/** + * The host scratch a batched pass row-selects through. One `[rows x width]` fp32 buffer per + * strip that has to cross, allocated on the first batched prefill and kept for the process. + */ +struct pn_qwen35_tt_batch { + int rows; + + float *hidden; /**< n_embd, the embedding rows going up */ + float *ssm_qkv; /**< the fused q|k|v stream, conv_dim */ + float *ssm_z; + float *ssm_alpha; + float *ssm_beta; + float *ssm_gated; + float *attn_q; + float *attn_z; + float *attn_k; + float *attn_v; + float *attn_gated; + + /* The arm's own accounting. Seconds, cumulative over the prefill. */ + double transfer_seconds; + double sequential_seconds; + double batched_seconds; + /* G0: `sequential` ATTRIBUTED. Each sequential bracket snapshots every card clock the + * device keeps (`pn_tt_device_card_clocks()`) on entry and exit and adds the difference + * here, entry by entry, so the report can say which clocks the bracket's wall contains + * and how much of it NO clock contains. */ + const char *sequential_clock_names[PN_TT_CARD_CLOCKS_MAX]; + double sequential_clock_seconds[PN_TT_CARD_CLOCKS_MAX]; + unsigned sequential_clock_count; + /** MEASURE (2026-10-05): `batched` split by bracket site — see kPrefillBatchedNames. */ + double batched_site_seconds[7]; + /** PHASE G: the widest span any pass of this prefill wrote — what the restage zeroes. */ + uint32_t max_span; + double sequential_unmatched_seconds; /**< brackets a residency armed inside */ + uint64_t passes; + uint64_t rows_done; +}; + +/** Same knob and same shape as the device layer's `pn_tt_red`. Every mutation here is HOST + * ordering only — no transfer changes length, so none can reach a zero-length NoC hazard. + * + * prefill_causal the per-row loop runs in REVERSE, so row r's mixer runs after rows + * later than it. For attention that breaks the mask; for the GDN block it + * feeds the recurrence its positions out of order. The loop order is the + * only thing enforcing either, which is what this proves. + * prefill_rowfill every row is served row 0's projection output — exactly the bug of a + * batch whose transfer still only moves logical row 0. + * prefill_pos every row runs the mixer at the pass's FIRST position, so 32 K/V + * entries land on one slot and the rotary is wrong for 31 of them. + */ +static bool prefill_red(const char *name) +{ + const char *value = getenv("PN_TT_RED"); + + return value && *value && strcmp(value, name) == 0; +} + +/** Default ON: only an explicit `0` puts the attention layers back per position. */ +static bool prefill_attn_chunk_enabled(void) +{ + const char *env = getenv(PN_QWEN35_TT_PREFILL_ATTN_CHUNK_ENV); + + return !(env && *env && atoi(env) == 0); +} + +/** How many 32-row bands a pass has. Every strip is allocated the same height, so hidden[0] + * speaks for all of them; a model whose strips did not arm at all reports one. */ +static uint32_t prefill_bands(pn_qwen35_tt *g) +{ + const uint32_t rows = g->card.hidden[0].tile_rows; + + return rows ? rows : 1u; +} + +/** + * How many bands a pass of `rows` positions actually OCCUPIES, which is what the widened ops + * must span — not how many the strips have. + * + * THE TWO DIFFER ON THE TAIL PASS AND IT IS NOT FREE. A 28-row tail at two bands spans one; + * telling the matmul to span two would sweep the weights for a whole band nobody wrote, which + * is measured: `PN_TT_PREFILL_ROWS=32` at two bands ran the shipped answer at 126.576 GB/token + * against 68.510 — the byte figures DOUBLED for the same 1,629 positions. + */ +static uint32_t prefill_span_for(pn_qwen35_tt *g, int rows) +{ + const uint32_t bands = prefill_bands(g); + const uint32_t want = (uint32_t)((rows + 31) / 32); + + if (rows <= 0) + return 1u; + return want < bands ? want : bands; +} + +/** The GDN chunk mixer, asked about a whole PASS rather than a chunk: every band of it has to + * be a legal chunk, because every band is a separate descriptor. At one band this is the + * single call it replaced. */ +static bool prefill_chunk_available(pn_qwen35_tt *g, int rows) +{ + const uint32_t bands = prefill_bands(g); + int done = 0; + + for (uint32_t band = 0; band < bands && done < rows; band++) { + const int take = (rows - done) < 32 ? (rows - done) : 32; + + if (!pn_qwen35_tt_gdn_chunk_available(g, take)) + return false; + done += take; + } + return done == rows; +} + +/** + * How many of `n_tokens` (from absolute position `base`) the batched passes take; the rest, + * at least the last token, run the ordinary forward. + * + * THE CHUNK FLOOR CUT. The GDN chunk mixer refuses a band shorter than `conv_taps - 1` (3), and + * a pass whose LAST band is that short drops the WHOLE pass onto the per-position fallback, + * which cannot address a second band: the pass fails, the positions it half wrote run per token + * over it, and the answer is wrong as well as 20x slow — every prompt whose batched length is + * 1 or 2 past a multiple of 32 (2 in 32 lengths; found 2026-10-07 at 1,795 tokens). Those one + * or two positions go to the ordinary forward instead. Decided on ABSOLUTE positions, so a + * resumed prefill (base a multiple of 32) cuts exactly where a cold one of the same prompt does. + */ +static int prefill_batched_rows(pn_qwen35_tt *g, int base, int n_tokens) +{ + int end = base + n_tokens - 1; + const int residue = end % 32; + + if (end > 32 && residue > 0 && !pn_qwen35_tt_gdn_chunk_available(g, residue) && + !prefill_red("prefill_floor_cut_off")) + end -= residue; + return end > base ? end - base : 0; +} + +static bool prefill_enabled(void) +{ + const char *env = getenv(PN_QWEN35_TT_PREFILL_ENV); + + return !(env && *env && atoi(env) == 0); +} + +/** + * Every strip a batched pass writes into, in ONE list. + * + * It was already written out twice — once in `prefill_restage()` and once, in pieces, in the + * two `prefill_zero()` calls — and the band seek needs a third reader. A strip missing from + * this list is a strip the seek leaves pointing at band 0 while the rest of the pass has moved + * to band 1, which is the projections of one chunk mixed with the positions of another and + * nothing on the card that would say so. So there is one list and three callers. + */ +#define PN_QWEN35_TT_MAX_STRIPS 32u + +static size_t prefill_strips_of(pn_qwen35_tt_strips *s, pn_qwen35_tt_attn_strips *a, + pn_tt_activation **out, size_t max, bool keep_empty) +{ + pn_tt_activation *all[] = { + &s->hidden[0], &s->hidden[1], &s->normed, &s->project, &s->ssm_qkv, &s->ssm_z, + &s->ssm_alpha, &s->ssm_beta, &s->ssm_gated, &s->attn_qg, &s->attn_k, &s->attn_v, + &s->attn_out, &s->ffn_gate, &s->ffn_up, &s->ffn_swiglu, &s->logits, + &a->q, &a->q_rot, &a->k_rot, &a->z, &a->gate, &a->gated, + }; + const size_t count = sizeof(all) / sizeof(all[0]); + size_t n = 0; + + for (size_t i = 0; i < count && n < max; i++) + if (keep_empty || all[i]->tile_count) + out[n++] = all[i]; + return n; +} + +static size_t prefill_strips(pn_qwen35_tt *g, pn_tt_activation **out, size_t max) +{ + return prefill_strips_of(&g->card, &g->attn_card, out, max, false); +} + +/* pn_tt_device_internal.h PN_TT_STRIP_MAX_TILE_ROWS; pn_tt_activation_alloc_rows refuses more. */ +#define QWEN35_TT_TALL_MAX_ROWS 64u + +/* The span the tall strips' mirrors were last keyed for (prefill_pass); 0 = re-key. */ +static uint32_t g_prefill_published_span; + +/** + * PHASE G option 3 — PREFILL-ONLY TALL STRIPS (`PN_TT_PREFILL_TILE_ROWS`, a power of two + * above the decode height, at most QWEN35_TT_TALL_MAX_ROWS; 0/unset = off). + * + * G1 made EVERY strip tall, and decode paid -1.4% for it (matmul host ISSUE +3 us, window + * aims +20 a token) with the cause unidentified. This keeps the decode strips at their own + * height and gives the batched passes a SECOND set of the same widths and mirror counts at + * the tall height, swapped into `card` / `attn_card` for the passes and swapped back before + * the tail forward. The logit strip is not copied: no batched pass runs the LM head. + */ +int pn_qwen35_tt_prefill_tall_alloc(pn_qwen35_tt *g) +{ + pn_tt_activation *small[PN_QWEN35_TT_MAX_STRIPS], *tall[PN_QWEN35_TT_MAX_STRIPS]; + /* DEFAULT 64 (G option 3, shipped with on-card dispatch: run_d_ceilings.sh). */ + const char *env = getenv("PN_TT_PREFILL_TILE_ROWS"); + const uint32_t rows = env ? (uint32_t)strtoul(env, NULL, 10) : 64u; + size_t n, i; + + g->tall_rows = 0u; + if (rows == 0u) + return 0; + if ((rows & (rows - 1u)) != 0u || rows > QWEN35_TT_TALL_MAX_ROWS || + rows <= g->card.hidden[0].tile_rows) { + fprintf(stderr, "qwen35_tt: PN_TT_PREFILL_TILE_ROWS=%u must be a power of two above " + "the decode height %u and at most %u — prefill shares the decode " + "strips\n", rows, g->card.hidden[0].tile_rows, QWEN35_TT_TALL_MAX_ROWS); + return 0; + } + memset(&g->card_tall, 0, sizeof(g->card_tall)); + memset(&g->attn_card_tall, 0, sizeof(g->attn_card_tall)); + n = prefill_strips_of(&g->card, &g->attn_card, small, PN_QWEN35_TT_MAX_STRIPS, true); + (void)prefill_strips_of(&g->card_tall, &g->attn_card_tall, tall, PN_QWEN35_TT_MAX_STRIPS, + true); + for (i = 0; i < n; i++) { + if (small[i]->tile_count == 0u || small[i] == &g->card.logits) + continue; + if (pn_tt_activation_alloc_replicated_rows(g->device, small[i]->tile_count, + small[i]->replica_count, rows, + tall[i]) != 0) { + fprintf(stderr, "qwen35_tt: the prefill-only tall strips did not allocate\n"); + return -1; + } + } + g->tall_rows = rows; + pn_tt_device_set_prefill_tile_rows(g->device, rows); + fprintf(stderr, "qwen35_tt: prefill-only TALL strips — %u bands for the batched passes, " + "decode keeps %u\n", rows, g->card.hidden[0].tile_rows); + return 0; +} + +static void prefill_tall_swap(pn_qwen35_tt *g) +{ + pn_qwen35_tt_strips s = g->card; + pn_qwen35_tt_attn_strips a = g->attn_card; + + g->card = g->card_tall; + g->card_tall = s; + g->attn_card = g->attn_card_tall; + g->attn_card_tall = a; + g->tall_active = !g->tall_active; +} + +/* STEP 4 S2: a batched pass whose mixers run the DECODE kernels row by row (the verify). */ +static bool g_verify_mode; + +static void prefill_tall_enter(pn_qwen35_tt *g) +{ + if (g->tall_rows == 0u || g->tall_active) + return; + prefill_tall_swap(g); + g_prefill_published_span = 0u; /* the first pass keys the tall mirrors */ +} + +/** Swap the decode strips back and re-key the producers' mirror map for them. RED + * `prefill_tall_map_stale`: skip the re-key, so decode's mirrors go stale — must DIFFER. */ +static int prefill_tall_exit(pn_qwen35_tt *g) +{ + const pn_tt_activation *mirrored[2]; + + if (!g->tall_active) + return 0; + prefill_tall_swap(g); + if (prefill_red("prefill_tall_map_stale")) + return 0; + mirrored[0] = &g->card.normed; + mirrored[1] = &g->card.ffn_swiglu; + pn_tt_replica_map_set_span(0u); + return pn_tt_publish_replica_map(g->device, mirrored, 2u, &g->card.ffn_swiglu); +} + + +/** + * Point every strip at band `band` and tell every op to span `span` bands. + * + * TWO SETTINGS AND NOT ONE, because the ops divide in two. The matmul, the RMS norms and the + * elementwise family are tile-row-general and take `span`; the per-head norms, RoPE, the KV + * append, the attention chunk and both GDN mixers have already spent their row axis and can + * only be run once per band, so they take `band` and a span of one. See + * `pn_tt_activation_seek()`. + */ +static int prefill_seek(pn_qwen35_tt *g, uint32_t band, uint32_t span) +{ + pn_tt_activation *strips[PN_QWEN35_TT_MAX_STRIPS]; + const size_t n = prefill_strips(g, strips, PN_QWEN35_TT_MAX_STRIPS); + + /* RED PROOF `PN_TT_RED=prefill_band0`: every MIXER pass runs on band 0, so positions + * 32..63 are mixed from band 0's projections and written over band 0's answer. THE + * PROJECTIONS ARE UNTOUCHED — the span-2 calls keep their band-0 seek either way — so + * this arm mutates the seek and nothing else, and it is the ONLY evidence that band 1 + * carries the second half of the pass rather than being computed and discarded. It MUST + * DIFFER; an IDENTICAL here means a two-band pass is a one-band pass with waste. */ + if (span == 1u && prefill_red("prefill_band0")) + band = 0u; + + for (size_t i = 0; i < n; i++) + if (pn_tt_activation_seek(strips[i], band) != 0 || + pn_tt_activation_span(strips[i], span) != 0) + return -1; + return 0; +} + +/** Rows of one batched pass: the tall strips' height while they are swapped in or about to + * be, the decode strips' otherwise, narrowed by `PN_TT_PREFILL_ROWS`. */ +static int prefill_rows_wanted(pn_qwen35_tt *g) +{ + const char *env = getenv(PN_QWEN35_TT_PREFILL_ROWS_ENV); + const uint32_t bands = (g->tall_rows && !g->tall_active) ? g->tall_rows : prefill_bands(g); + const int ceiling = (int)(bands * 32u); + int rows = env && *env ? atoi(env) : ceiling; + + if (rows < 1) rows = 1; + /* THE CEILING IS THE ALLOCATION, not the tile. A pass may carry as many positions as the + * strips have bands; asking for more would run the mixers over a band nothing wrote. */ + if (rows > ceiling) rows = ceiling; + return rows; +} + +/** The strip's full width in floats. Transfers move whole tiles, so the padding rides along + * and the round trip is exact rather than nearly so. */ +static uint32_t prefill_width(const pn_tt_activation *strip) +{ + return strip->tile_count * 32u; +} + +static double prefill_now(void) +{ + return pn_time_ms() / 1000.0; +} + +/** batched + sequential + transfer so far (cumulative over the process); 0 before the first + * pass allocated the scratch. The wall regions report their movement. */ +static double prefill_accounted(const pn_qwen35_tt *g) +{ + const struct pn_qwen35_tt_batch *b = g->batch; + + return b ? b->batched_seconds + b->sequential_seconds + b->transfer_seconds : 0.0; +} + +#define PREFILL_WALL_OPEN(g, region) pn_prefill_wall_open((region), prefill_accounted(g)) +#define PREFILL_WALL_CLOSE(g, region) pn_prefill_wall_close((region), prefill_accounted(g)) + +/** The `batched` bracket sites, in kPrefillBatchedNames order. */ +static const char *const kPrefillBatchedNames[7] = { "ssm-in-proj", "ssm-out-proj", + "attn-proj", "attn-out-proj", "ffn", + "attn-norm", "post-norm" }; + +static void prefill_batched_add(struct pn_qwen35_tt_batch *b, unsigned site, double seconds) +{ + b->batched_seconds += seconds; + if (site < 7u) + b->batched_site_seconds[site] += seconds; +} + +/** Every card clock at the open of a `sequential` bracket. */ +typedef struct { + const char *names[PN_TT_CARD_CLOCKS_MAX]; + double seconds[PN_TT_CARD_CLOCKS_MAX]; + unsigned count; + double mark; +} prefill_sequential_bracket; + +/** Open a `sequential` bracket: every card clock, then the wall mark. */ +/** + * PHASE W chain step — `PN_TT_PREFILL_STREAM` (default on, `=0` off): a layer's band loop runs as STREAMED puller + * segments (pn_tt_dispatch_stream_begin, the S2d machinery): the host's queue writes become + * puller entries and its completion polls are answered at once while the puller awaits the + * same counts on the card, so the chain stops paying a host round trip per op. The alpha/beta + * reads are on the reader thread's own window bank and are not boundaries. While streaming, + * the sequential bracket does not snapshot card clocks (each would be a boundary). + */ +static bool g_prefill_streaming; + +static bool prefill_stream_wanted(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *env = getenv("PN_TT_PREFILL_STREAM"); + + /* DEFAULT ON (run_w_stream.sh 2026-10-08: serve 1,630 TTFT 3.24 -> 2.98 s, IDENTICAL). */ + cached = env && *env ? atoi(env) != 0 : 1; + } + return cached != 0; +} + +static void prefill_stream_open(pn_qwen35_tt *g) +{ + g_prefill_streaming = prefill_stream_wanted() && !g_verify_mode && + pn_tt_dispatch_stream_begin(g->device); +} + +/** @return 0, or -1 when a streamed segment failed on the card (the work is then untrusted). */ +static int prefill_stream_close(void) +{ + const bool was = g_prefill_streaming; + + g_prefill_streaming = false; + return (!was || pn_tt_dispatch_stream_end()) ? 0 : -1; +} + +static void prefill_sequential_open(const pn_qwen35_tt *g, prefill_sequential_bracket *open) +{ + open->count = g_prefill_streaming ? 0u + : pn_tt_device_card_clocks(g->device, open->names, + open->seconds, + PN_TT_CARD_CLOCKS_MAX); + open->mark = prefill_now(); +} + +/** Add `seconds` to the entry called `name`, BY NAME — split members repeat member 0's name + * and sum into one family row, and an index would not survive the table changing shape + * between two brackets. */ +static void prefill_sequential_book(struct pn_qwen35_tt_batch *b, const char *name, + double seconds) +{ + unsigned index; + + for (index = 0; index < b->sequential_clock_count; index++) + if (strcmp(b->sequential_clock_names[index], name) == 0) + break; + if (index == b->sequential_clock_count) { + if (index >= PN_TT_CARD_CLOCKS_MAX) { + b->sequential_unmatched_seconds += seconds; + return; + } + b->sequential_clock_names[index] = name; + b->sequential_clock_seconds[index] = 0.0; + b->sequential_clock_count++; + } + b->sequential_clock_seconds[index] += seconds; +} + +/** + * Close it: the wall into `sequential_seconds`, each clock's movement into its entry. + * + * A residency that ARMS inside the bracket changes the table's shape, and a clock matched by + * position would then be subtracted from its neighbour. So the snapshots must agree entry by + * entry on the name; when they do not, the whole bracket goes to `unmatched` — counted, never + * guessed at. + */ +static void prefill_sequential_close(pn_qwen35_tt *g, const prefill_sequential_bracket *open) +{ + struct pn_qwen35_tt_batch *b = g->batch; + const double wall = prefill_now() - open->mark; + const char *names[PN_TT_CARD_CLOCKS_MAX]; + double after[PN_TT_CARD_CLOCKS_MAX]; + unsigned count, index; + + b->sequential_seconds += wall; + if (g_prefill_streaming) + return; + count = pn_tt_device_card_clocks(g->device, names, after, PN_TT_CARD_CLOCKS_MAX); + for (index = 0; index < count && count == open->count; index++) + if (names[index] != open->names[index]) + break; + if (count != open->count || index != count) { + b->sequential_unmatched_seconds += wall; + return; + } + for (index = 0; index < count; index++) + prefill_sequential_book(b, names[index], after[index] - open->seconds[index]); +} + +void pn_qwen35_tt_prefill_release(pn_qwen35_tt *g) +{ + struct pn_qwen35_tt_batch *b; + + if (!g || !g->batch) + return; + b = g->batch; + free(b->hidden); free(b->ssm_qkv); free(b->ssm_z); free(b->ssm_alpha); free(b->ssm_beta); + free(b->ssm_gated); free(b->attn_q); free(b->attn_z); free(b->attn_k); free(b->attn_v); + free(b->attn_gated); + free(b); + g->batch = NULL; +} + +static int prefill_alloc(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int rows) +{ + const pn_qwen35_tt_strips *s = &g->card; + const pn_qwen35_tt_attn_strips *a = &g->attn_card; + struct pn_qwen35_tt_batch *b; + + if (g->batch && g->batch->rows >= rows) + return 0; + pn_qwen35_tt_prefill_release(g); + b = calloc(1, sizeof(*b)); + if (!b) + return -1; + b->hidden = calloc((size_t)rows * (size_t)cfg->n_embd, sizeof(float)); + b->ssm_qkv = calloc((size_t)rows * prefill_width(&s->ssm_qkv), sizeof(float)); + b->ssm_z = calloc((size_t)rows * prefill_width(&s->ssm_z), sizeof(float)); + b->ssm_alpha = calloc((size_t)rows * prefill_width(&s->ssm_alpha), sizeof(float)); + b->ssm_beta = calloc((size_t)rows * prefill_width(&s->ssm_beta), sizeof(float)); + b->ssm_gated = calloc((size_t)rows * prefill_width(&s->ssm_gated), sizeof(float)); + b->attn_q = calloc((size_t)rows * prefill_width(&a->q), sizeof(float)); + b->attn_z = calloc((size_t)rows * prefill_width(&a->z), sizeof(float)); + b->attn_k = calloc((size_t)rows * prefill_width(&s->attn_k), sizeof(float)); + b->attn_v = calloc((size_t)rows * prefill_width(&s->attn_v), sizeof(float)); + b->attn_gated = calloc((size_t)rows * prefill_width(&a->gated), sizeof(float)); + if (!b->hidden || !b->ssm_qkv || !b->ssm_z || !b->ssm_alpha || !b->ssm_beta || + !b->ssm_gated || !b->attn_q || !b->attn_z || !b->attn_k || !b->attn_v || + !b->attn_gated) { + g->batch = b; + pn_qwen35_tt_prefill_release(g); + return -1; + } + b->rows = rows; + g->batch = b; + return 0; +} + +/** Download `rows` whole tile-rows of a strip into a host buffer, timed as TRANSFER. */ +static int prefill_pull(pn_qwen35_tt *g, const pn_tt_activation *strip, float *host, int rows) +{ + struct pn_qwen35_tt_batch *b = g->batch; + double mark = prefill_now(); + int rc = pn_tt_activation_download_rows(g->device, strip, host, prefill_width(strip), + (uint32_t)rows); + + b->transfer_seconds += prefill_now() - mark; + return rc; +} + +/** Put ONE host row back as the strip's logical row 0 — the row select, done through the + * link because the card has no op that can do it. */ +static int prefill_push_row(pn_qwen35_tt *g, const pn_tt_activation *strip, const float *host, + int row) +{ + struct pn_qwen35_tt_batch *b = g->batch; + uint32_t width = prefill_width(strip); + int take = prefill_red("prefill_rowfill") ? 0 : row; + double mark = prefill_now(); + int rc = pn_tt_activation_upload(g->device, strip, host + (size_t)take * width, width); + + b->transfer_seconds += prefill_now() - mark; + return rc; +} + +/** Read a mixer's row-0 answer into row `row` of a host buffer. */ +static int prefill_pull_row(pn_qwen35_tt *g, const pn_tt_activation *strip, float *host, + int row) +{ + struct pn_qwen35_tt_batch *b = g->batch; + uint32_t width = prefill_width(strip); + double mark = prefill_now(); + int rc = pn_tt_activation_download(g->device, strip, host + (size_t)row * width, width); + + b->transfer_seconds += prefill_now() - mark; + return rc; +} + +/** Put all `rows` host rows up as a batch, so the next projection serves every one of them. */ +static int prefill_push(pn_qwen35_tt *g, const pn_tt_activation *strip, const float *host, + int rows) +{ + struct pn_qwen35_tt_batch *b = g->batch; + double mark = prefill_now(); + int rc = pn_tt_activation_upload_rows(g->device, strip, host, prefill_width(strip), + (uint32_t)rows); + + b->transfer_seconds += prefill_now() - mark; + return rc; +} + +/** + * ZERO A MIXER'S WORKING SET, and this is a CORRECTNESS step rather than hygiene. + * + * `pn_tt_activation_upload()` writes logical row 0 and nothing else, so after a 32-row pull + * the row select leaves rows 1..31 carrying the OTHER 31 positions' projection outputs. In + * decode those rows are the tile quantum's zeros, and every claim that makes a mixer's row-0 + * answer independent of them rests on that: an op that mixes across rows anywhere — the delta + * rule's outer product, a broadcast, the K/V append — would read live neighbours instead of + * zeros and there is nothing on the card that would say so. + * + * A whole-strip zero costs one write of the strip, in the 200-390 MB/s direction, ONCE a + * layer rather than once a row. That is ~0.08 ms a position against the 3+ ms a position the + * row select already pays, so restoring the invariant is free at this rung and the arm does + * not have to depend on an unproven independence. + */ +static int prefill_zero(pn_qwen35_tt *g, const pn_tt_activation *const *strips, size_t n) +{ + struct pn_qwen35_tt_batch *b = g->batch; + double mark = prefill_now(); + + for (size_t i = 0; i < n; i++) + if (strips[i]->tile_count && + pn_tt_activation_zero_bands(g->device, strips[i], + prefill_bands(g) > 4u ? b->max_span : 0u) != 0) + return -1; + b->transfer_seconds += prefill_now() - mark; + return 0; +} + +/** hidden = hidden + project, on the card, into the spare strip. Per tile, so all 32 rows. */ +static int prefill_residual(pn_qwen35_tt *g) +{ + int rc; + + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_RESIDUAL); + rc = pn_tt_add(g->device, &g->card.hidden[g->card.slot], &g->card.project, + &g->card.hidden[g->card.slot ^ 1]); + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_RESIDUAL); + if (rc != 0) + return -1; + g->card.slot ^= 1; + return 0; +} + +/** + * One SSM layer over `rows` positions: four projections ONCE, then the recurrence `rows` + * times through the link, then the output projection ONCE. + */ +/** Every exit from the chunk band loop: nothing the reader thread or a detached grid + * projection still owes may land after the loop has returned. */ +static void prefill_chain_settle(pn_qwen35_tt *g) +{ + g->gdn_chunk_defer_gate = false; + g->gdn_card_gate = false; + (void)pn_qwen35_tt_gdn_pipe_drain(g); + g->gdn_pipe = false; + (void)pn_tt_eltwise_group_settle(g->device); + (void)prefill_stream_close(); + (void)pn_qwen35_tt_overlap_drain(g); + pn_qwen35_tt_ab_reader_abandon(g); + pn_qwen35_tt_overlap_forget(); +} + +/** Close the loop's stream, its wait booked as SEQUENTIAL (the card running the streamed + * chain). @return 0, or -1 when a segment failed. */ +static int prefill_stream_finish(pn_qwen35_tt *g) +{ + const double at = prefill_now(); + const int rc = prefill_stream_close(); + + g->batch->sequential_seconds += prefill_now() - at; + return rc; +} + +static bool prefill_env_on(const char *name); + +/** PM 7 ITEM 7 step 1: `PN_TT_GDN_PIPE=0` runs the band chain serially. */ +static bool prefill_gdn_pipe_wanted(void) +{ + const char *env = getenv("PN_TT_GDN_PIPE"); + + return !(env && *env && atoi(env) == 0); +} + +/** The verify's deferred GDN rows (`PN_TT_SPEC_DEFER=0` off) — gdn_strip_rows' own reading. */ +static bool prefill_spec_defer(void) +{ + const char *env = getenv("PN_TT_SPEC_DEFER"); + + return !env || atoi(env) != 0; +} + +/** ITEM 6: `PN_TT_GDNGATE_DECODE=0` — det gates on the host from read alpha/beta. */ +static bool prefill_card_decode_gates(void) +{ + const char *env = getenv("PN_TT_GDNGATE_DECODE"); + + return !(env && *env && atoi(env) == 0); +} + +/** + * PM 7 ITEM 5: put layer `ssm_index`'s gate blocks on the card — already issued by the W2 + * wavefront for a prepared layer, else issued here for every band (not awaited; the band loop + * settles them once). @return true when the layer runs on card gates, false = the host path. + */ +static bool prefill_card_gates_begin(pn_qwen35_tt *g, int ssm_index, int rows, + bool gates_prepared) +{ + if (g_verify_mode || !pn_tt_gdngate_enabled(g->device) || + !pn_qwen35_tt_gdn_card_gate_possible()) + return false; + if (!gates_prepared && + pn_tt_gdngate_issue(g->device, (uint32_t)ssm_index, &g->card.ssm_alpha, + &g->card.ssm_beta, (uint32_t)rows, + pn_qwen35_tt_gdn_gate_sub_rows()) != 0) + return false; + /* `PN_TT_GDNGATE_VERIFY=1` (gates only): every block read back and compared word for word + * with the host build of it from host-read alpha/beta — the card == host proof on the + * model's own data. Reads the link: never in a timing arm. */ + if (prefill_env_on("PN_TT_GDNGATE_VERIFY")) { + const long bad = pn_tt_gdngate_settle(g->device) == 0 + ? pn_tt_gdngate_verify_layer(g->device, (uint32_t)ssm_index, + &g->card.ssm_alpha, &g->card.ssm_beta, + (uint32_t)rows, + pn_qwen35_tt_gdn_gate_sub_rows()) + : -1; + + fprintf(stderr, "qwen35_tt: gdngate VERIFY layer %d — %ld word(s) differ over %d rows\n", + ssm_index, bad, rows); + } + return true; +} + +/** An env knob that is on when set to anything but 0. */ +static bool prefill_env_on(const char *name) +{ + const char *value = getenv(name); + + return value && *value && atoi(value) != 0; +} + +/** + * Returns 0, -1, or 1 when the layer's out-projection, residuals, post-norm and FFN were + * already run inside the band loop (PHASE W2 overlap) — the caller then skips them. + */ +static int prefill_ssm_layer(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35_tt_layer *d, const pn_qwen35moe_config *cfg, + int ssm_index, int base_pos, int rows, + const pn_qwen35_tt_layer *next_d, int next_ssm_index) +{ + struct pn_qwen35_tt_batch *b = g->batch; + pn_qwen35_tt_strips *s = &g->card; + const pn_qwen35moe_ssm_layer *l = pn_qwen35moe_model_ssm_layer(m, ssm_index); + uint32_t alpha_w = prefill_width(&s->ssm_alpha); + uint32_t beta_w = prefill_width(&s->ssm_beta); + /* PHASE W WAVEFRONT: the previous layer's super-band jobs already ran this layer's + * attn-norm, in-proj and read posts — skip them. */ + const bool prepared = !g_verify_mode && + pn_qwen35_tt_overlap_prepared(ssm_index, prefill_span_for(g, rows)); + /* Read BEFORE pn_qwen35_tt_overlap_forget() below: the wavefront's own record of having + * issued this layer's gate blocks (run_pm7_item6_diag.sh: read after it, every layer + * re-issued at its start — 24 of 88 issues, 0.146 s of settles). */ + const bool gates_prepared = prepared && + pn_qwen35_tt_overlap_gates_prepared(ssm_index, + prefill_span_for(g, rows)); + const pn_qwen35_tt_overlap_next next = { + next_d, next_ssm_index, rows, b->ssm_alpha, b->ssm_beta, alpha_w, beta_w, + }; + double mark; + + if (!l) + return -1; + pn_qwen35_tt_overlap_forget(); + mark = prefill_now(); + if (!prepared && + (pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false) != 0 || + pn_tt_matvec_flush(g->device) != 0)) + return -1; + prefill_batched_add(b, 0, prefill_now() - mark); /* ssm-in-proj */ + + /* STEP 4 S2: THE VERIFY. Row r of the pass goes through the DECODE GDN kernels exactly as + * a single-position forward would: its qkv/z rows are copied on the card into row 0 of the + * decode strips (rows 1-31 there are the decode invariant's zeros), the decode strip layer + * runs, and its gated row 0 is copied back into row r. S0 proved this arithmetic + * bit-identical to per-token. Row r writes its new carried state into snapshot slot r and + * the two trade places (pn_tt_gdn_layer_swap): slot r keeps the state BEFORE row r for a + * rollback, with no copy. */ + if (g_verify_mode) { + /* The projections land first, so the row timer holds only the row loop. */ + const double rows_started = pn_tt_matvec_flush(g->device) == 0 ? pn_time_ms() : -1.0; + + if (rows_started < 0.0) + return -1; + /* PM 6: the reads go to the reader (gated on the card running these projections) and + * the stream keeps flowing; gdn_strip_rows waits for them before the host math. The + * S2 row-copy path still reads inline. */ + /* ITEM 6: det gates computed on the card for a deferred verify — no reads at all. */ + const bool card = g->gdn_det_gates && !getenv("PN_TT_SPEC_ROWCOPY") && + prefill_spec_defer() && prefill_card_decode_gates(); + + if (!card && + (getenv("PN_TT_SPEC_ROWCOPY") || + !pn_qwen35_tt_ab_verify_post(g, rows, b->ssm_alpha, alpha_w, b->ssm_beta, + beta_w)) && + (prefill_pull(g, &s->ssm_alpha, b->ssm_alpha, rows) != 0 || + prefill_pull(g, &s->ssm_beta, b->ssm_beta, rows) != 0)) + return -1; + g->spec_gdn_part_ms[0] += pn_time_ms() - rows_started; + /* S2c (`PN_TT_SPEC_ROWCOPY=1` keeps the S2 form: decode strips + puller row copies): + * the decode recurrences with the kernels' ROW SELECT on the verify strips, the + * row-wise ops once over all rows — no row copies, no per-row flushes. */ + if (!getenv("PN_TT_SPEC_ROWCOPY")) { + if (pn_qwen35_tt_gdn_strip_rows(g, l, cfg, ssm_index, rows, &s->ssm_qkv, &s->ssm_z, + card ? NULL : b->ssm_beta, beta_w, + card ? NULL : b->ssm_alpha, alpha_w, + &s->ssm_gated) != 0 || + pn_tt_matvec_flush(g->device) != 0) + return -1; + g->spec_gdn_part_ms[4] += pn_time_ms() - g->spec_gdn_mark_ms; + } else + for (int r = 0; r < rows; r++) { + pn_qwen35_tt_strips *v = &g->card_tall; /* the VERIFY rows: swapped out below */ + int rc; + + prefill_tall_swap(g); /* g->card = decode strips, g->card_tall = verify rows */ + v = &g->card_tall; + rc = (pn_tt_row_copy(g->device, &v->ssm_qkv, (uint32_t)r, &g->card.ssm_qkv, 0u) != 0 || + pn_tt_row_copy(g->device, &v->ssm_z, (uint32_t)r, &g->card.ssm_z, 0u) != 0) + ? -1 : 0; + /* Row r's new state goes to slot r, then trades places with the live one. */ + if (rc == 0) + rc = pn_tt_gdn_state_out(g->device, r); + if (rc == 0) { + pn_tt_device_set_single_row(g->device, true); + rc = pn_qwen35_tt_gdn_strip_layer(g, l, cfg, ssm_index, &g->card.ssm_qkv, + &g->card.ssm_z, + b->ssm_beta + (size_t)r * beta_w, + b->ssm_alpha + (size_t)r * alpha_w, + &g->card.ssm_gated); + pn_tt_device_set_single_row(g->device, false); + } + if (rc == 0 && pn_tt_matvec_flush(g->device) != 0) + rc = -1; + if (pn_tt_gdn_state_out(g->device, -1) != 0 || + (rc == 0 && pn_tt_gdn_layer_swap(g->device, (uint32_t)ssm_index, (uint32_t)r) != 0)) + rc = -1; + if (rc == 0) + rc = pn_tt_row_copy(g->device, &g->card.ssm_gated, 0u, &v->ssm_gated, + (uint32_t)r); + prefill_tall_swap(g); + if (rc != 0) + return -1; + } + g->spec_ssm_rows_ms += pn_time_ms() - rows_started; + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->ssm_out, &s->ssm_gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 1, prefill_now() - mark); /* ssm-out-proj */ + return 0; + } + + /* THE CHUNK PATH: THE 24 SSM LAYERS STOP ROW-SELECTING THROUGH THE LINK. + * + * `ssm_qkv` and `ssm_z` never come down and never go back up — the two chunkwise mixers + * read the strip the projections already wrote, with row t carrying position `base+t`, and + * advance the SAME conv window and the SAME recurrent state the decode family owns. What + * still crosses is `alpha` and `beta` DOWN (two tiles) and the per-head decay tiles and + * gate block UP (128 + 20 KiB a layer a chunk), because `beta = sigmoid(.)` and + * `decay = exp(.)` need a `log` the SFPU does not have, and because the decay-ratio matrix + * is built on the host as a product recurrence ON PURPOSE — a card-built one costs the + * whole precision advantage of going chunkwise. That was 0.2063 s/row of transfer. + * + * THE FLOOR IS THE CONV WINDOW'S. A chunk shorter than `taps-1` cannot REPLACE the window, + * only shift it, and both the host encoder and the device refuse — so those positions go + * through the per-position path below, which is the same arithmetic on the same bytes and + * leaves the window and the state exactly where a chunk would have. That is the ONE seam + * in this file and it costs nothing, because the two families share the buffers. + * + * The two red arms about the per-position LOOP select the fallback path, because a chunk + * has no such loop; the chunk path's own carry arms are `gdnc_window_wipe` and + * `gdnc_state_wipe` in pn_tt_gdnc.c, and both are INVISIBLE IN CHUNK 0. */ + if (prefill_chunk_available(g, rows) && !prefill_red("prefill_causal") && + !prefill_red("prefill_rowfill")) { + const uint32_t bands = prefill_span_for(g, rows); + /* PHASE W: every band's alpha/beta read posted to the READER THREAD now (all of them + * are projected); the band loop only waits on its own. Off: the prefetch below. */ + /* PM 7 ITEM 5: the gates on the card — no alpha/beta crosses the link at all. */ + const bool card_gates = prefill_card_gates_begin(g, ssm_index, rows, gates_prepared); + const bool reader = !card_gates && + ((prepared && pn_qwen35_tt_ab_reader_promote(ssm_index)) || + pn_qwen35_tt_ab_reader_post(g, rows, b->ssm_alpha, alpha_w, + b->ssm_beta, beta_w)); + const uint32_t overlap_bands = pn_qwen35_tt_overlap_bands(); + const bool overlap = pn_qwen35_tt_overlap_layer(g, ssm_index, bands); + + /* The layer's gate blocks, settled ONCE before the first band, by the HOST and before + * the stream opens: as a puller AWAIT inside the stream it stalled ~8.5 s a layer + * (run_pm7_item5_gates.sh round 2: TTFT 204 s), while a host await is the gate time + * itself — and a prepared layer's blocks are long done. */ + if (card_gates && pn_tt_gdngate_settle(g->device) != 0) + return -1; + prefill_stream_open(g); + g->gdn_card_gate = card_gates; + /* PM 7 ITEM 7 step 1: the pipelined band (`PN_TT_GDN_PIPE=0` off). */ + g->gdn_pipe = card_gates && g_prefill_streaming && prefill_gdn_pipe_wanted(); + pn_qwen35_tt_overlap_set_card_gates(card_gates); + /* PM 7: streamed, each band's output gate is left pending (its AWAIT moves to the next + * band's narrow, or to the settles below); `PN_TT_ELTWISE_DEFER=0` off. */ + g->gdn_chunk_defer_gate = g_prefill_streaming; + int done = 0; + + /* ONE BAND AT A TIME, IN POSITION ORDER, and the order is the correctness claim: the + * conv window and the `[S,S]` recurrent state are card-resident and carry from one + * descriptor to the next, so band 1's mixer must see band 0's already run. That is + * also why the GDN mixers cannot simply be widened the way the projections were — + * `chunk_rows > 32` is refused by name in five places because the intra-chunk decay + * matrix is ONE 32x32 tile, and above 32 the DELTA term is O(C^2). The band loop runs + * the mixer the shape it already has, over different bytes. */ + for (uint32_t band = 0; band < bands && done < rows; band++) { + const int take = (rows - done) < 32 ? (rows - done) : 32; + int rc; + + if (prefill_seek(g, band, 1u) != 0) { + prefill_chain_settle(g); + return -1; + } + /* Band b's reads: the reader thread's, or done under band b-1's mixers (the + * prefetch), or inline. A failed reader read is re-read inline. None at all when + * the card computed the gates. */ + g->gdn_card_gate_band = band; + if (card_gates) { + /* nothing to read */ + } else if (reader) { + const double waited = prefill_now(); + const int took = pn_qwen35_tt_ab_reader_take(g, (int)band); + + b->transfer_seconds += prefill_now() - waited; + if (took != 0 && + (prefill_pull(g, &s->ssm_alpha, b->ssm_alpha + (size_t)done * alpha_w, + take) != 0 || + prefill_pull(g, &s->ssm_beta, b->ssm_beta + (size_t)done * beta_w, + take) != 0)) { + prefill_chain_settle(g); + return -1; + } + } else if ((!pn_qwen35_tt_ab_prefetched(g, ssm_index, (int)band, false) && + prefill_pull(g, &s->ssm_alpha, b->ssm_alpha + (size_t)done * alpha_w, take) != 0) || + (!pn_qwen35_tt_ab_prefetched(g, ssm_index, (int)band, true) && + prefill_pull(g, &s->ssm_beta, b->ssm_beta + (size_t)done * beta_w, take) != 0)) + return -1; + pn_qwen35_tt_ab_prefetch_reset(); + if (!reader && !card_gates && band + 1u < bands && done + take < rows) { + const int next = (rows - done - take) < 32 ? (rows - done - take) : 32; + + pn_qwen35_tt_ab_prefetch_arm(g, ssm_index, (int)band + 1, next, + b->ssm_alpha + (size_t)(done + take) * alpha_w, + alpha_w, + b->ssm_beta + (size_t)(done + take) * beta_w, + beta_w); + } + prefill_sequential_bracket bracket; + prefill_sequential_open(g, &bracket); + rc = pn_qwen35_tt_gdn_strip_chunk(g, l, cfg, ssm_index, &s->ssm_qkv, &s->ssm_z, + b->ssm_beta + (size_t)done * beta_w, beta_w, + b->ssm_alpha + (size_t)done * alpha_w, alpha_w, + take, &s->ssm_gated); + prefill_sequential_close(g, &bracket); + pn_qwen35_tt_ab_prefetch_settle(g); + if (rc != 0) { + prefill_chain_settle(g); + return -1; + } + done += take; + /* MULTI-TURN: this layer's state is the state at `base_pos + done`. The capture is + * a puller COPY, which refuses mid-stream: the stream closes around it. */ + if (g_prefill_streaming && g->ckpt.pending_at == base_pos + done) { + if (pn_qwen35_tt_gdn_pipe_drain(g) != 0 || + pn_tt_eltwise_group_settle(g->device) != 0 || prefill_stream_finish(g) != 0) { + prefill_chain_settle(g); + return -1; + } + pn_qwen35_tt_ckpt_band_done(g, ssm_index, base_pos + done); + prefill_stream_open(g); + } else { + pn_qwen35_tt_ckpt_band_done(g, ssm_index, base_pos + done); + } + /* PHASE W2: a finished super-band's FFN goes to the grid; otherwise the job in + * flight takes its next step if its projections are done. */ + /* STREAMED, a poll is answered at once and its AWAIT stalls the puller there, so + * the job's next step is SCHEDULED instead: mid-super-band (the SwiGLU + down) and + * at its close (the residual) — the grid runs under the chain ops issued between. */ + if (overlap) { + const bool boundary = (band + 1u) % overlap_bands == 0u && band + 1u < bands && + done < rows; + const bool step = !g_prefill_streaming || + (band + 1u) % overlap_bands == overlap_bands / 2u; + + /* The job reads `gated` (out-proj) and the grid rewrites z: gate settled. */ + if ((boundary || step) && (pn_qwen35_tt_gdn_pipe_drain(g) != 0 || + pn_tt_eltwise_group_settle(g->device) != 0)) { + prefill_chain_settle(g); + return -1; + } + rc = boundary ? pn_qwen35_tt_overlap_superband(g, d, band + 1u - overlap_bands, + overlap_bands, &next) + : step ? pn_qwen35_tt_overlap_advance(g) : 0; + prefill_batched_add(b, 4, pn_qwen35_tt_overlap_take_seconds()); + if (rc != 0) { + prefill_chain_settle(g); + return -1; + } + } + } + pn_qwen35_tt_ab_prefetch_reset(); + g->gdn_chunk_defer_gate = false; + g->gdn_card_gate = false; + if (pn_qwen35_tt_gdn_pipe_drain(g) != 0 || pn_tt_eltwise_group_settle(g->device) != 0 || + prefill_stream_finish(g) != 0) { + g->gdn_pipe = false; + prefill_chain_settle(g); + return -1; + } + g->gdn_pipe = false; + pn_qwen35_tt_ab_reader_end(g); + if (overlap) { + const uint32_t last = ((bands - 1u) / overlap_bands) * overlap_bands; + int orc = pn_qwen35_tt_overlap_superband(g, d, last, bands - last, &next); + + if (orc == 0) + orc = pn_qwen35_tt_overlap_drain(g); + prefill_batched_add(b, 4, pn_qwen35_tt_overlap_take_seconds()); + if (orc != 0 || prefill_seek(g, 0u, bands) != 0) + return -1; + return 1; + } + if (prefill_seek(g, 0u, bands) != 0) + return -1; + + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->ssm_out, &s->ssm_gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 1, prefill_now() - mark); /* ssm-out-proj */ + return 0; + } + + if (prefill_pull(g, &s->ssm_qkv, b->ssm_qkv, rows) != 0 || + prefill_pull(g, &s->ssm_z, b->ssm_z, rows) != 0 || + prefill_pull(g, &s->ssm_alpha, b->ssm_alpha, rows) != 0 || + prefill_pull(g, &s->ssm_beta, b->ssm_beta, rows) != 0) + return -1; + { + const pn_tt_activation *live[] = { &s->ssm_qkv, &s->ssm_z, &s->ssm_gated }; + + if (prefill_zero(g, live, sizeof(live) / sizeof(live[0])) != 0) + return -1; + } + + for (int i = 0; i < rows; i++) { + int r = prefill_red("prefill_causal") ? rows - 1 - i : i; + int rc; + + if (prefill_push_row(g, &s->ssm_qkv, b->ssm_qkv, r) != 0 || + prefill_push_row(g, &s->ssm_z, b->ssm_z, r) != 0) + return -1; + prefill_sequential_bracket bracket; + prefill_sequential_open(g, &bracket); + rc = pn_qwen35_tt_gdn_strip_layer(g, l, cfg, ssm_index, &s->ssm_qkv, &s->ssm_z, + b->ssm_beta + (size_t)r * beta_w, + b->ssm_alpha + (size_t)r * alpha_w, + &s->ssm_gated); + prefill_sequential_close(g, &bracket); + if (rc != 0) + return -1; + if (prefill_pull_row(g, &s->ssm_gated, b->ssm_gated, r) != 0) + return -1; + } + if (prefill_push(g, &s->ssm_gated, b->ssm_gated, rows) != 0) + return -1; + + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->ssm_out, &s->ssm_gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 1, prefill_now() - mark); /* ssm-out-proj */ + return 0; +} + +/** One attention layer over `rows` positions. Same shape as the SSM one: project once, + * rotate/append/attend `rows` times, project out once. */ +static int prefill_attn_layer(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int base_pos, + int rows) +{ + struct pn_qwen35_tt_batch *b = g->batch; + pn_qwen35_tt_strips *s = &g->card; + pn_qwen35_tt_attn_strips *a = &g->attn_card; + const int max_seq = pn_qwen35moe_max_seq(cfg); + double mark = prefill_now(); + + if (pn_qwen35_tt_attn_project_card(g, d) != 0 || pn_tt_matvec_flush(g->device) != 0) + return -1; + prefill_batched_add(b, 2, prefill_now() - mark); /* attn-proj */ + + /* STEP 4 S2: THE VERIFY — the decode attention core, row by row, at position base+r. */ + if (g_verify_mode && !getenv("PN_TT_SPEC_ROWCOPY") && + !(getenv("PN_TT_SPEC_ATTN_ROWS") && atoi(getenv("PN_TT_SPEC_ATTN_ROWS")) == 0)) { + /* S2c: the row-wise steps once over every row (QK norms, rotary at each row's own + * position by the decode table's angles, the gate), the K/V appends straight from row + * r of the verify strips, and only flash decode per row through the decode strips — + * one row copy each way. Row r attends to positions <= base+r, so appending every + * row first changes nothing it reads. */ + const double rows_started = pn_time_ms(); + pn_qwen35_tt_attn_strips *da = &g->attn_card_tall; /* the decode strips, swapped out */ + pn_qwen35_tt_strips *ds = &g->card_tall; + int rc = 0, rows_done = 0; + + if (pn_tt_head_norm_bf16(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &d->attn_q_norm, + (uint32_t)cfg->n_head, &a->q, &a->q) != 0 || + pn_tt_head_norm_bf16(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, &d->attn_k_norm, + (uint32_t)cfg->n_head_kv, &s->attn_k, &s->attn_k) != 0 || + pn_tt_rope_rows(g->device, &a->q, &a->q_rot, (uint32_t)cfg->n_head, + (uint32_t)base_pos, (uint32_t)rows) != 0 || + pn_tt_rope_rows(g->device, &s->attn_k, &a->k_rot, (uint32_t)cfg->n_head_kv, + (uint32_t)base_pos, (uint32_t)rows) != 0) + return -1; + for (int r = 0; rc == 0 && r < rows; r++) + rc = (pn_tt_kv_append_row(g->device, (uint32_t)attn_index, false, &a->k_rot, + (uint32_t)r, (uint32_t)(base_pos + r)) != 0 || + pn_tt_kv_append_row(g->device, (uint32_t)attn_index, true, &s->attn_v, + (uint32_t)r, (uint32_t)(base_pos + r)) != 0) + ? -1 : 0; + /* S2f: flash decode for every row in ONE descriptor a group (ROWS mode) when every + * row ends in the same position tile (`PN_TT_SPEC_ATTN_MULTI=0`: one a row). */ + if (rc == 0 && base_pos / 32 == (base_pos + rows - 1) / 32 && base_pos + rows < max_seq && + !(getenv("PN_TT_SPEC_ATTN_MULTI") && atoi(getenv("PN_TT_SPEC_ATTN_MULTI")) == 0)) { + rc = pn_tt_attention_rows(g->device, (uint32_t)attn_index, &a->q_rot, &s->attn_out, + (uint32_t)base_pos, (uint32_t)rows); + rows_done = rows; + } + for (int r = rows_done; rc == 0 && r < rows; r++) { + const int pos = base_pos + r; + const int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + + rc = (pn_tt_row_copy(g->device, &a->q_rot, (uint32_t)r, &da->q_rot, 0u) != 0 || + pn_tt_attention(g->device, (uint32_t)attn_index, &da->q_rot, &ds->attn_out, + (uint32_t)seq_len) != 0 || + pn_tt_row_copy(g->device, &ds->attn_out, 0u, &s->attn_out, (uint32_t)r) != 0) + ? -1 : 0; + } + if (rc != 0 || pn_tt_sigmoid_mul(g->device, &a->z, &s->attn_out, &a->gate, + &a->gated) != 0) + return -1; + g->spec_attn_rows_ms += pn_time_ms() - rows_started; + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->attn_output, &a->gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 3, prefill_now() - mark); /* attn-out-proj */ + return 0; + } + if (g_verify_mode) { + const double rows_started = pn_time_ms(); + + for (int r = 0; r < rows; r++) { + const int pos = base_pos + r; + const int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + pn_qwen35_tt_strips *vs; + pn_qwen35_tt_attn_strips *va; + int rc; + + prefill_tall_swap(g); + vs = &g->card_tall; + va = &g->attn_card_tall; + rc = (pn_tt_row_copy(g->device, &va->q, (uint32_t)r, &g->attn_card.q, 0u) != 0 || + pn_tt_row_copy(g->device, &va->z, (uint32_t)r, &g->attn_card.z, 0u) != 0 || + pn_tt_row_copy(g->device, &vs->attn_k, (uint32_t)r, &g->card.attn_k, 0u) != 0 || + pn_tt_row_copy(g->device, &vs->attn_v, (uint32_t)r, &g->card.attn_v, 0u) != 0) + ? -1 : 0; + if (rc == 0) { + pn_tt_device_set_single_row(g->device, true); + rc = pn_qwen35_tt_attn_core_card(g, d, cfg, attn_index, pos, seq_len); + pn_tt_device_set_single_row(g->device, false); + } + if (rc == 0 && pn_tt_matvec_flush(g->device) != 0) + rc = -1; + if (rc == 0) + rc = pn_tt_row_copy(g->device, &g->attn_card.gated, 0u, &va->gated, (uint32_t)r); + prefill_tall_swap(g); + if (rc != 0) + return -1; + } + g->spec_attn_rows_ms += pn_time_ms() - rows_started; + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->attn_output, &a->gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 3, prefill_now() - mark); /* attn-out-proj */ + return 0; + } + + /* THE CHUNK PATH: NO ROW SELECT AT ALL. + * + * Every strip the attention block needs is already on the card with row t carrying + * position `base_pos + t` — the projections put it there — so the four whole-tile pulls, + * the nine-strip re-zero, the 4 x rows row-0 pushes and the rows row-0 pulls all go + * away. That is ~48 KiB up and ~32 KiB down PER POSITION removed from a 5.3 MB/s link, + * and it is the whole reason batching cost 3.4x what it saved. + * + * The alignment condition is the one the three chunk ops share (RoPE's row-phase + * contract, a whole KV tile, one causal mask tile), and a prefill pass always starts on + * a multiple of 32. The red arms still have to be able to reorder and mis-position the + * work, so a named one falls back to the per-position path rather than being ignored. */ + if ((base_pos % 32) == 0 && rows > 0 && rows <= (int)(prefill_bands(g) * 32u) && + prefill_attn_chunk_enabled() && + !prefill_red("prefill_causal") && !prefill_red("prefill_rowfill")) { + /* RED PROOF: the whole chunk claims to start at position 0, so RoPE rotates by the + * wrong angle and the chunk attends to the wrong span of cache. INVISIBLE ON THE + * FIRST PASS, where base_pos IS 0 — the same structure both GDN mixers recorded, and + * the reason the gate runs more than one pass. + * + * `prefill_causal` and `prefill_rowfill` are about the per-position LOOP, which a + * chunk does not have, so they select the fallback path above and still fire there. + * The chunk path's own causality arm is on the card, in + * foundry/tt/tests/test_phase6_attention.c: `causal_off` points the descriptor at a + * constants operand whose causal tiles are all live. */ + const uint32_t bands = prefill_span_for(g, rows); + int done = 0; + + prefill_stream_open(g); + /* ONE BAND AT A TIME, for a reason the GDN loop does not have: the chunk-mode + * attention descriptor REFUSES a non-zero `query_row` (ft_tt_op_attention.c) and there + * is no tile-row field in the protocol at all, so the second band is reached the only + * way it can be — by sliding the operand address, which is exactly what the seek did. + * RoPE's table is one 32-position block and the KV append's `source_row0` caps at 32 + * for the same reason. Each band carries its own first position, so the mask, the + * rotary angle and the K/V slots are the ones the per-32 path would have used. */ + for (uint32_t band = 0; band < bands && done < rows; band++) { + const int take = (rows - done) < 32 ? (rows - done) : 32; + const int chunk_base = prefill_red("prefill_pos") ? 0 : base_pos + done; + int rc; + + if (prefill_seek(g, band, 1u) != 0) { + (void)prefill_stream_close(); + return -1; + } + prefill_sequential_bracket bracket; + prefill_sequential_open(g, &bracket); + rc = pn_qwen35_tt_attn_core_chunk(g, d, cfg, attn_index, chunk_base, take); + prefill_sequential_close(g, &bracket); + if (rc != 0) { + (void)prefill_stream_close(); + return -1; + } + done += take; + } + if (prefill_stream_finish(g) != 0 || prefill_seek(g, 0u, bands) != 0) + return -1; + + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->attn_output, &a->gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 3, prefill_now() - mark); /* attn-out-proj */ + return 0; + } + + if (prefill_pull(g, &a->q, b->attn_q, rows) != 0 || + prefill_pull(g, &a->z, b->attn_z, rows) != 0 || + prefill_pull(g, &s->attn_k, b->attn_k, rows) != 0 || + prefill_pull(g, &s->attn_v, b->attn_v, rows) != 0) + return -1; + { + const pn_tt_activation *live[] = { &a->q, &a->z, &s->attn_k, &s->attn_v, &a->q_rot, + &a->k_rot, &s->attn_out, &a->gate, &a->gated }; + + if (prefill_zero(g, live, sizeof(live) / sizeof(live[0])) != 0) + return -1; + } + + for (int i = 0; i < rows; i++) { + int r = prefill_red("prefill_causal") ? rows - 1 - i : i; + int pos = prefill_red("prefill_pos") ? base_pos : base_pos + r; + int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + int rc; + + if (prefill_push_row(g, &a->q, b->attn_q, r) != 0 || + prefill_push_row(g, &a->z, b->attn_z, r) != 0 || + prefill_push_row(g, &s->attn_k, b->attn_k, r) != 0 || + prefill_push_row(g, &s->attn_v, b->attn_v, r) != 0) + return -1; + prefill_sequential_bracket bracket; + prefill_sequential_open(g, &bracket); + rc = pn_qwen35_tt_attn_core_card(g, d, cfg, attn_index, pos, seq_len); + prefill_sequential_close(g, &bracket); + if (rc != 0) + return -1; + if (prefill_pull_row(g, &a->gated, b->attn_gated, r) != 0) + return -1; + } + if (prefill_push(g, &a->gated, b->attn_gated, rows) != 0) + return -1; + + mark = prefill_now(); + if (pn_qwen35_tt_project(g, &d->attn_output, &a->gated, &s->project, true) != 0) + return -1; + prefill_batched_add(b, 3, prefill_now() - mark); /* attn-out-proj */ + return 0; +} + +/** The dense SwiGLU FFN, entirely batched: nothing in it is position-dependent. */ +static int prefill_ffn(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d) +{ + struct pn_qwen35_tt_batch *b = g->batch; + pn_qwen35_tt_strips *s = &g->card; + double mark = prefill_now(); + int rc = -1; + + if (pn_qwen35_tt_project(g, &d->ffn_gate, &s->normed, &s->ffn_gate, false) == 0 && + pn_qwen35_tt_project(g, &d->ffn_up, &s->normed, &s->ffn_up, false) == 0 && + pn_tt_matvec_flush(g->device) == 0 && + pn_tt_silu_mul(g->device, &s->ffn_gate, &s->ffn_up, &s->ffn_swiglu) == 0 && + pn_qwen35_tt_project(g, &d->ffn_down, &s->ffn_swiglu, &s->project, true) == 0) + rc = 0; + prefill_batched_add(b, 4, prefill_now() - mark); /* ffn */ + return rc; +} + +/** ONE batched pass: `rows` consecutive positions from `base_pos` through the whole stack, + * leaving the card's carried state and K/V where the sequential path would leave them, and + * computing no logits at all. */ +/** + * STEP 4 S2b: the MTP block over a pass's rows — its KV for the prompt, so a draft attends to + * the whole context. Row r is (trunk hidden r, embedding of `next[r]`) at base_pos + r; the + * trunk's hidden is consumed (the logits come from the tail forward, not from this pass). + * Chunked attention: a draft only proposes, so the parity-gated prefill path is fine here. + */ +static int prefill_mtp_layer(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, const uint32_t *next, int rows, + int base_pos) +{ + struct pn_qwen35_tt_batch *b = g->batch; + pn_qwen35_tt_strips *s = &g->card; + const pn_qwen35_tt_layer *d = &g->mtp.layer; + const uint32_t bands = prefill_span_for(g, rows); + pn_tt_activation *e = &s->hidden[s->slot ^ 1]; + int staged = 0; + + for (int r = 0; r < rows; r++) + pn_qwen35moe_model_embed_row(m, next[r], b->hidden + (size_t)r * (size_t)cfg->n_embd); + if (prefill_seek(g, 0u, bands) != 0 || + (!(g_verify_mode && bands == 1u) && + pn_tt_activation_zero_bands(g->device, e, prefill_bands(g) > 4u ? b->max_span : 0u) != 0)) + return -1; + for (uint32_t band = 0; band < bands && staged < rows; band++) { + const int take = (rows - staged) < 32 ? (rows - staged) : 32; + + if (prefill_seek(g, band, 1u) != 0 || + prefill_push(g, e, b->hidden + (size_t)staged * (size_t)cfg->n_embd, take) != 0) + return -1; + staged += take; + } + if (prefill_seek(g, 0u, bands) != 0) + return -1; + if (pn_tt_rms_norm(g->device, &g->mtp.hnorm, &s->hidden[s->slot], &s->normed) != 0 || + pn_qwen35_tt_project(g, &g->mtp.eh_h, &s->normed, &s->project, true) != 0 || + pn_tt_rms_norm(g->device, &g->mtp.enorm, e, &s->normed) != 0 || + pn_qwen35_tt_project(g, &g->mtp.eh_e, &s->normed, &s->hidden[s->slot], true) != 0 || + prefill_residual(g) != 0) + return -1; + if (pn_tt_rms_norm(g->device, &d->attn_norm, &s->hidden[s->slot], &s->normed) != 0 || + prefill_attn_layer(g, d, cfg, g->mtp.attn_index, base_pos, rows) != 0 || + prefill_residual(g) != 0 || + pn_tt_rms_norm(g->device, &d->post_norm, &s->hidden[s->slot], &s->normed) != 0 || + prefill_ffn(g, d) != 0 || prefill_residual(g) != 0) + return -1; + return 0; +} + +static int prefill_pass(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, const uint32_t *tokens, int rows, + int base_pos) +{ + struct pn_qwen35_tt_batch *b = g->batch; + pn_qwen35_tt_strips *s = &g->card; + double mark; + + const uint32_t bands = prefill_span_for(g, rows); + int staged = 0; + + /* PHASE G: TALL strips (a whole prompt a pass) key their mirrors by the span the pass's + * producers write (pn_tt_replica_map.c), so a pass whose span differs republishes. */ + if (prefill_bands(g) > 4u && bands != g_prefill_published_span) { + const pn_tt_activation *mirrored[2] = { &s->normed, &s->ffn_swiglu }; + + pn_tt_replica_map_set_span(bands); + pn_tt_replica_map_set_superband(pn_qwen35_tt_overlap_bands()); + if (pn_tt_publish_replica_map(g->device, mirrored, 2u, &s->ffn_swiglu) != 0) + return -1; + g_prefill_published_span = bands; + } + + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_EMBED); + for (int r = 0; r < rows; r++) + pn_qwen35moe_model_embed_row(m, tokens[r], + b->hidden + (size_t)r * (size_t)cfg->n_embd); + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_EMBED); + s->slot = 0; + /* A SHORT PASS LEAVES BANDS NOBODY WRITES, and an Mt-band matmul reads all of them. The + * tilizer zeroes the rows above `take` INSIDE the last band it writes; the bands PAST it + * would still carry the previous pass's positions, so the whole strip is re-staged zero + * first. An Mt=2 matmul of a zero band is a zero band and every op after it preserves + * that, exactly as the 31 padding rows have always been preserved. Once a pass, not once + * a layer: only the pass's FIRST strip can carry a previous pass's residue. */ + /* PHASE G: tall strips are zeroed over the bands written (this pass's, or a previous + * pass's if wider), never the whole allocation. */ + if (bands > b->max_span) + b->max_span = bands; + /* S2f: a verify (<= 32 rows, one band) does not zero: the band's upload rewrites every + * tile (rows above the count zero) and no op reads a band it does not span — the zeroing + * was the prompt's whole span (51 bands after a 1,629-row prompt) every cycle. */ + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_STAGE); + if (prefill_seek(g, 0u, bands) != 0 || + (!(g_verify_mode && bands == 1u) && !prefill_red("prefill_stage_skip_band1") && + pn_tt_activation_zero_bands(g->device, &s->hidden[0], + prefill_bands(g) > 4u ? b->max_span : 0u) != 0)) + return -1; + /* THE UPLOAD IS PER BAND AND THE TILIZER IS WHY. `ft_tt_tilize_strip()` takes `valid_rows` + * 1..32 and writes ONE tile-row, zeroing the rows above the count — which is the strip + * invariant, restored per band. A 64-row upload would need `ft_tt_tilize_matrix()` and + * would buy one fewer host call; the invariant is worth more than the call. */ + for (uint32_t band = 0; band < bands && staged < rows; band++) { + const int take = (rows - staged) < 32 ? (rows - staged) : 32; + + /* RED `prefill_stage_skip_band1`: a prompt pass reads band 1 of hidden[0] without + * writing it (no zero, no upload) — the residue-read class the restage gate exists + * to see. Must DIFFER (NaN under PN_TT_PREFILL_RESIDUE_POISON). */ + if (band == 1u && !g_verify_mode && prefill_red("prefill_stage_skip_band1")) { + staged += take; + continue; + } + if (prefill_seek(g, band, 1u) != 0 || + prefill_push(g, &s->hidden[0], + b->hidden + (size_t)staged * (size_t)cfg->n_embd, take) != 0) + return -1; + staged += take; + } + if (prefill_seek(g, 0u, bands) != 0) + return -1; + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_STAGE); + + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_LAYERS); + for (int layer = 0; layer < cfg->n_layer; layer++) { + const pn_qwen35_tt_layer *d = &g->layers[layer]; + int idx = pn_qwen35moe_model_layer_index(m, layer); + + mark = prefill_now(); + /* (W wavefront: a prepared SSM layer's attn-norm already ran, super-band by super-band.) */ + if (!(!d->is_attention && !g_verify_mode && + pn_qwen35_tt_overlap_prepared(idx, prefill_span_for(g, rows))) && + pn_tt_rms_norm(g->device, &d->attn_norm, &s->hidden[s->slot], &s->normed) != 0) + return -1; + prefill_batched_add(b, 5, prefill_now() - mark); /* attn-norm */ + + if (d->is_attention) { + pn_qwen35_tt_overlap_forget(); + if (prefill_attn_layer(g, d, cfg, idx, base_pos, rows) != 0) + return -1; + } else { + const bool next_ssm = layer + 1 < cfg->n_layer && !g->layers[layer + 1].is_attention; + const int src = prefill_ssm_layer( + g, m, d, cfg, idx, base_pos, rows, next_ssm ? &g->layers[layer + 1] : NULL, + next_ssm ? pn_qwen35moe_model_layer_index(m, layer + 1) : -1); + + if (src < 0) + return -1; + if (src == 1) { /* PHASE W2: the band loop already ran the rest of the layer */ + if (!g_verify_mode) + (void)pn_tt_device_keepalive(g->device, PN_QWEN35_TT_KEEPALIVE_IDLE_S); + continue; + } + } + if (prefill_residual(g) != 0) + return -1; + + mark = prefill_now(); + if (pn_tt_rms_norm(g->device, &d->post_norm, &s->hidden[s->slot], &s->normed) != 0) + return -1; + prefill_batched_add(b, 6, prefill_now() - mark); /* post-norm */ + + if (prefill_ffn(g, d) != 0 || prefill_residual(g) != 0) + return -1; + /* PM 5 KEEPALIVE, per layer: one tall pass is seconds (tens at 8k) during which the + * decode-only families (gdn, topk, ...) would idle past the retire limit. */ + if (!g_verify_mode) + (void)pn_tt_device_keepalive(g->device, PN_QWEN35_TT_KEEPALIVE_IDLE_S); + } + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_LAYERS); + /* A prompt pass never holds the prompt's last token, so every row has a next one. */ + /* `PN_TT_MTP_PROMPT=0`: no MTP KV for the prompt (prices what the prefill cost buys). */ + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_MTP); + if (g->mtp.on && !g_verify_mode && + !(getenv("PN_TT_MTP_PROMPT") && atoi(getenv("PN_TT_MTP_PROMPT")) == 0) && + prefill_mtp_layer(g, m, cfg, tokens + 1, rows, base_pos) != 0) + return -1; + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_MTP); + b->passes++; + b->rows_done += (uint64_t)rows; + /* The forward's own alarm reads this: a process that has run a batched pass is a process + * whose callers ask, so its single-position forwards are generation and not a prefill. */ + g->prefill_passes++; + g->sequential_prefill_run = 0; + return 0; +} + +/** + * The strips' state after the last pass. `PN_TT_PREFILL_RESTAGE=1` zeroes every band the + * prompt wrote (the pre-2026-10-07 default: 2.38 s of a 1,630-token prefill, ~700 MB of zeros + * over the link); `PN_TT_PREFILL_RESIDUE_POISON=1` fills the WHOLE tall allocation with 0xFF + * (bf16 NaN) instead, the test that nothing reads the residue; default: neither. + * + * WHY THE ZERO IS NOT NEEDED ON THE TALL SET. With prefill-only tall strips (`tall_active`) + * the decode strips are a different allocation the prompt never touched, so the residue is + * read by nothing but the NEXT batched pass (a prompt, a verify, an MTP catch-up), and every + * one of those writes each band it spans before reading it: hidden[0] is zeroed over the + * pass's span and uploaded band by band (the tilizer zeroes rows above the count), the mixer + * working sets are zeroed in `prefill_zero()`, every other strip is a producer's output over + * the span. The S2f verify already relies on it (it never zeroes). Strips SHARED with decode + * (tall strips off) keep the zero: there the next reader is a decode token. + */ +static bool prefill_restage_env(const char *name) +{ + const char *env = getenv(name); + + return env && *env && atoi(env) != 0; +} + +static int prefill_restage(pn_qwen35_tt *g) +{ + pn_tt_activation *strips[PN_QWEN35_TT_MAX_STRIPS]; + const bool poison = g->tall_active && prefill_restage_env("PN_TT_PREFILL_RESIDUE_POISON"); + const bool zero = !g->tall_active || prefill_restage_env("PN_TT_PREFILL_RESTAGE"); + size_t n; + + /* BAND 0 AND A SPAN OF ONE FIRST, because everything after the prefill is a decode token + * and a decode token is band 0. A strip left pointing at band 1 would send every later + * projection into the wrong half of its own buffer, with every kernel retiring clean — + * and `pn_tt_activation_zero()` zeroes the WHOLE allocation from band 0 whatever the seek + * is, so the restage itself is correct either way. This line is about what comes after. */ + if (prefill_seek(g, 0u, 1u) != 0) + return -1; + n = prefill_strips(g, strips, PN_QWEN35_TT_MAX_STRIPS); + for (size_t i = 0; (zero || poison) && i < n; i++) + if (pn_tt_activation_fill_bands(g->device, strips[i], + poison ? 0u : g->batch->max_span, + poison ? 0xFFu : 0u) != 0) + return -1; + g->batch->max_span = 0u; + return 0; +} + +/** + * A REFUSAL THAT SAYS ITS NAME, because a silent one is a shipped default degrading 20x. + * + * `pn_qwen35moe_prefill_tt()` returns NULL to mean "not taken" and the engine's per-token + * loop then runs the whole prompt. That fallback is CORRECT and it is also ~20x slower than + * the path that ships on by default, so a NULL nobody can see is indistinguishable from the + * batched prefill having run. Measured 2026-08-26 on a clean checkout of main: a 1,630-token + * prompt took 295 s and ~1,632 forwards, and the only line on stderr was the carried-state + * reset — which is printed by the batched path on its way IN. + * + * @param reason what stopped it, in the caller's own terms + * @return NULL always, so a refusal site is one `return` + */ +static tm_tensor *prefill_declined(const char *reason) +{ + fprintf(stderr, "qwen35_tt: BATCHED PREFILL DECLINED — %s. Nothing was touched and the " + "caller's per-token loop will produce the same answer, at roughly 20x " + "the wall.\n", reason); + return NULL; +} + +/** + * THE OTHER CLASS, AND IT IS NOT THE SAME EVENT. A pass that failed part way has already + * written the card's conv window, recurrent state and K/V for the positions it did complete, + * and `state->pos` has already advanced for every pass that succeeded before it. The caller + * cannot tell this from a refusal — it sees the same NULL — so its per-token replay runs the + * prompt again over state that is neither empty nor complete. + * + * @param reason what failed + * @param positions_done positions whose state is already on the card + */ +static tm_tensor *prefill_aborted(const char *reason, int positions_done) +{ + fprintf(stderr, "qwen35_tt: BATCHED PREFILL ABORTED after %d positions — %s. The card's " + "carried state and K/V are PARTLY WRITTEN; the caller's per-token replay " + "cannot undo them, so this answer is WRONG and not merely slow.\n", + positions_done, reason); + return NULL; +} + +/** + * G0: `sequential` ATTRIBUTED, with its denominator. Every card clock that moved inside a + * sequential bracket, as seconds and as a share of `sequential`, then what NO clock contains + * (host work between descriptors, transfers inside the mixers, anything unclocked) and the + * brackets a residency armed inside, which are counted rather than split. + */ +static void prefill_report_sequential(const struct pn_qwen35_tt_batch *b) +{ + const double total = b->sequential_seconds > 0.0 ? b->sequential_seconds : 1.0; + double clocked = 0.0; + unsigned index; + + for (index = 0; index < b->sequential_clock_count; index++) + if (b->sequential_clock_seconds[index] > 0.0) + clocked += b->sequential_clock_seconds[index]; + fprintf(stderr, "qwen35_tt: sequential ATTRIBUTED — %.4f s of %.4f s clocked (%.1f%%), " + "%.4f s unclocked (%.1f%%), %.4f s in brackets a residency armed inside\n", + clocked, b->sequential_seconds, 100.0 * clocked / total, + b->sequential_seconds - clocked - b->sequential_unmatched_seconds, + 100.0 * (b->sequential_seconds - clocked - b->sequential_unmatched_seconds) / total, + b->sequential_unmatched_seconds); + for (index = 0; index < b->sequential_clock_count; index++) + if (b->sequential_clock_seconds[index] > 0.0) + fprintf(stderr, "qwen35_tt: sequential clock %-16s %.4f s (%.1f%%)\n", + b->sequential_clock_names[index], b->sequential_clock_seconds[index], + 100.0 * b->sequential_clock_seconds[index] / total); +} + +/** MEASURE: `batched` by bracket site (each = its projections + flush, or its op). */ +static void prefill_report_batched(const struct pn_qwen35_tt_batch *b) +{ + const double total = b->batched_seconds > 0.0 ? b->batched_seconds : 1.0; + unsigned site; + + for (site = 0; site < 7u; site++) + fprintf(stderr, "qwen35_tt: batched site %-14s %.4f s (%.1f%%)\n", + kPrefillBatchedNames[site], b->batched_site_seconds[site], + 100.0 * b->batched_site_seconds[site] / total); +} + +static void prefill_report(const pn_qwen35_tt *g) +{ + const struct pn_qwen35_tt_batch *b = g->batch; + double rows = (double)(b->rows_done ? b->rows_done : 1); + + fprintf(stderr, "qwen35_tt: batched prefill — %llu rows over %llu passes; " + "batched %.4f s (%.4f s/row) + sequential %.4f s (%.4f s/row) + " + "transfer %.4f s (%.4f s/row)\n", + (unsigned long long)b->rows_done, (unsigned long long)b->passes, + b->batched_seconds, b->batched_seconds / rows, + b->sequential_seconds, b->sequential_seconds / rows, + b->transfer_seconds, b->transfer_seconds / rows); + prefill_report_sequential(b); + prefill_report_batched(b); +} + +tm_tensor *pn_qwen35moe_prefill_tt(pn_model *model, const uint32_t *tokens, int n_tokens, + pn_state *state) +{ + pn_qwen35moe_model *m; + const pn_qwen35moe_config *cfg; + pn_qwen35_tt *g; + tm_tensor *last = NULL; + int rows, done, base, batched; + bool resumed, tail_aborted = false; + + /* NOT A DECLINE THIS FILE OWNS: the caller asks every model, and a prompt of one token + * has no batch in it. Saying so on every llama turn would be noise. */ + if (!model || !state || !model->weights || !tokens || n_tokens < 2) + return NULL; + if (model->forward != pn_qwen35moe_forward_tt) + return NULL; + m = (pn_qwen35moe_model *)model->weights; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + cfg = pn_qwen35moe_model_config(m); + if (g) + g->prefill_last_batched = -1; + if (!prefill_enabled()) + return prefill_declined(PN_QWEN35_TT_PREFILL_ENV "=0 was asked for"); + if (!g || !cfg || !g->resident) + return prefill_declined("the model is not resident on the card"); + /* REFUSALS, each of which hands the prompt back UNTOUCHED so the caller's per-token loop + * runs. A refusal that half-ran a pass would be a wrong answer with no symptom. + * + * Both mixers must be the ON-CARD ones. With the host recurrence or host attention the + * batch would have to carry `pn_qwen35moe_state`'s per-layer host state through a row + * loop it was never written for, and the arm would be measuring a third forward. */ + if (!g->gdn_strip) + return prefill_declined("the GDN strip bridge did not arm"); + if (!g->attn_on_card) + return prefill_declined("attention is not on the card"); + /* Every instrument below assumes ONE position per forward: the crossed control runs a + * whole second attention block, and the GDN gate and normcheck run shadow recurrences off + * host state a batched pass does not maintain. */ + if (g->attn_compare || g->gdn_parity || g->gdn_normcheck) + return prefill_declined("a per-forward instrument is armed " + "(PN_TT_ATTN_COMPARE / PN_TT_GDN_PARITY / PN_TT_GDN_NORMCHECK)"); + /* A continuation is batched only from a restored checkpoint: its passes are a cold run's + * (qwen35moe_prefill_checkpoint_tt.c). Any other position runs per token. */ + resumed = pn_qwen35_tt_ckpt_restored_at(g, state, state->pos); + if (state->pos != 0 && !resumed) + return prefill_declined("the sequence does not start at position 0"); + base = state->pos; + pn_qwen35_tt_ckpt_note(g, state, state->pos); + /* PHASE G0 MEASURE: one synthetic projection on layer 0's ffn_up, then the prefill as + * usual (G0 uses its own buffers). Run several times for a best-of. */ + if (getenv("PN_TT_G0")) { + int rep; + + for (rep = 0; rep < 3; rep++) + (void)pn_tt_matmul_group_g0(g->device, &g->layers[0].ffn_up); + } + /* ON RECORD AND IT COSTS 4,096 SEQUENTIAL POSITIONS BEFORE IT BITES: `max_seq` defaults to + * 4096, so a longer prompt used to run the per-token path to position 4096 and then fail + * at `the KV append at position 4096 of layer 0 is not encodable`, with nothing having + * said the batch declined. QWEN35_MAX_SEQ is the knob and it is named here. */ + if (state->pos + n_tokens > pn_qwen35moe_max_seq(cfg)) { + char why[192]; + + snprintf(why, sizeof(why), "the prompt is %d tokens and max_seq is %d — raise " + "QWEN35_MAX_SEQ, or the per-token loop will run to %d and " + "then fail at the KV append", + n_tokens, pn_qwen35moe_max_seq(cfg), pn_qwen35moe_max_seq(cfg)); + return prefill_declined(why); + } + + /* A SECOND BAND NEEDS BOTH CHUNK PATHS, and the arms that select the per-position + * fallback are named here rather than discovered inside a layer. The fallback moves ONE + * host row at a time through `pn_tt_activation_upload()`, which writes logical row 0 of + * whichever band the strip is pointed at and can address nothing else — so at more than + * one band it would run every band's positions through band 0. A decline hands the prompt + * back untouched; an abort would not. + * + * Decided on the PASS height, which is the TALL strips' when they exist: deciding on the + * decode strips let `PN_TT_STRIP_TILE_ROWS=1` and `PN_TT_PREFILL_GDNC=0` through to a + * 64-band pass that failed, ran the prompt per token and still returned logits — the + * causality gate scored those arms green while no batched pass had run. */ + if (prefill_rows_wanted(g) > 32 && + (prefill_red("prefill_causal") || prefill_red("prefill_rowfill") || + !prefill_attn_chunk_enabled() || !pn_qwen35_tt_gdn_chunk_available(g, 32))) + return prefill_declined("a pass wider than 32 rows needs both chunk paths — the " + "per-position fallback moves one host row at a time and " + "cannot address a second band (" PN_QWEN35_TT_PREFILL_ROWS_ENV + "=32 runs it)"); + + pn_prefill_wall_set_rearm_clock(pn_tt_residency_lifecycle_seconds); + pn_prefill_wall_begin(prefill_accounted(g)); + pn_sample_profiler_window(1); + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_SETUP); + prefill_tall_enter(g); + rows = prefill_rows_wanted(g); + if (prefill_alloc(g, cfg, rows) != 0) { + (void)prefill_tall_exit(g); + return prefill_declined("the host row scratch could not be allocated"); + } + + /* The card's carried state is per sequence and there is one region set per model, so a + * new sequence zeroes it exactly where the host arm would get a fresh calloc. This is the + * same seam `pn_qwen35_tt_resident_forward()` takes at pos 0, and the refusal above keeps + * the batched arm to sequences that start there. */ + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_STATE_RESET); + if (!resumed && pn_tt_gdn_state_reset(g->device) != 0) { + (void)prefill_tall_exit(g); + return prefill_declined("the carried state could not be zeroed"); + } + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_STATE_RESET); + /* PHASE G: the prefill grid (`PN_TT_PREFILL_GRID`), started for this prefill only. */ + pn_tt_device_telemetry_log(g->device, "prefill start"); + /* The grid pays only when a projection has rows to share: booting 48 cores for a short + * prompt cost ~40 ms and served 0 projections (run_d_defaults.sh). `PN_TT_PREFILL_GRID_MIN_ROWS` + * (default 64) batched rows or more. */ + /* By THIS call's rows: grid and matmul group are bit-identical (pf64 with the grid off + * IDENTICAL, run_prefill_ab_prefetch.sh), so a resumed tail may skip the boot. */ + /* DEFAULT 768 (run_prefill_grid_min.sh): at 508 rows the grid LOST 40 ms, at 1,629 it wins + * 0.32 s; a resumed 113-row tail without it: TTFT 618 -> 453 ms, IDENTICAL. */ + if (n_tokens - 1 >= (getenv("PN_TT_PREFILL_GRID_MIN_ROWS") + ? atoi(getenv("PN_TT_PREFILL_GRID_MIN_ROWS")) : 768)) + { + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_GRID_BEGIN); + (void)pn_tt_matmul_grid_begin(g->device); + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_GRID_BEGIN); + } + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_SETUP); + + /* Batch everything but the LAST token; that one goes through the ordinary forward, which + * is what produces the logits and what keeps the head unmodified. */ + batched = prefill_batched_rows(g, base, n_tokens); + pn_qwen35_tt_ckpt_plan(g, state, base, base + batched); + done = 0; + while (batched - done >= rows) { + if (prefill_pass(g, m, cfg, tokens + done, rows, state->pos) != 0) { + pn_tt_matmul_grid_end(g->device); + (void)prefill_tall_exit(g); + return prefill_aborted("a batched pass failed on the card", done); + } + done += rows; + state->pos += rows; + /* PM 5 KEEPALIVE: a long prompt leaves the decode-only families idle for seconds. */ + (void)pn_tt_device_keepalive(g->device, PN_QWEN35_TT_KEEPALIVE_IDLE_S); + } + /* THE SHORT TAIL PASS IS THE ONE FAILURE THIS FUNCTION ABSORBS, and absorbing it silently + * was wrong: a pass that failed part way has already written some of those positions, and + * the tail loop below then runs them AGAIN on top of that state. Loud, and it keeps the + * old behaviour rather than turning a working prompt into a refusal. */ + if (batched - done > 0) { + if (prefill_pass(g, m, cfg, tokens + done, batched - done, state->pos) == 0) { + state->pos += batched - done; + done = batched; + } else { + tail_aborted = true; + (void)prefill_aborted("the short tail pass failed; the remaining positions run " + "per token OVER the state it half wrote", done); + } + } + pn_qwen35_tt_ckpt_commit(g, state, tokens, base, pn_qwen35moe_model_n_ssm(m)); + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_FLUSH); + if (pn_tt_matvec_flush(g->device) != 0) { + pn_tt_matmul_grid_end(g->device); + (void)prefill_tall_exit(g); + return prefill_aborted("the last pass did not complete", done); + } + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_FLUSH); + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_GRID_END); + pn_tt_matmul_grid_end(g->device); + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_GRID_END); + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_RESTAGE); + if (prefill_restage(g) != 0) { + (void)prefill_tall_exit(g); + return prefill_aborted("the strips could not be re-zeroed after the last pass", done); + } + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_RESTAGE); + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_TALL_EXIT); + if (prefill_tall_exit(g) != 0) + return prefill_aborted("the decode strips' mirror map did not republish", done); + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_TALL_EXIT); + /* STEP 3a: everything is flushed (restage zeroes through the flush); give decode a clean + * parameter cache rather than one full of this prefill's blocks. */ + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_PARAM_RESET); + pn_tt_matmul_group_param_cache_reset(g->device); + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_PARAM_RESET); + + PREFILL_WALL_OPEN(g, PN_PREFILL_WALL_TAIL); + for (int i = done; i < n_tokens; i++) { + if (last) tm_free(last); + last = pn_qwen35moe_forward_tt(model, tokens[i], state); + if (!last) + return prefill_aborted("the tail forward that produces the logits failed", + state->pos); + } + PREFILL_WALL_CLOSE(g, PN_PREFILL_WALL_TAIL); + g->prefill_last_batched = tail_aborted ? -1 : done; + pn_sample_profiler_window(0); + if (getenv(PN_QWEN35_TT_PREFILL_REPORT_ENV)) { + prefill_report(g); + pn_qwen35_tt_ab_prefetch_report(); + pn_qwen35_tt_overlap_report(); + fprintf(stderr, "qwen35_tt: prefill gate DEFER — %llu chain gate descriptor(s) deferred " + "(cumulative)\n", + (unsigned long long)pn_tt_eltwise_group_deferred(g->device)); + pn_tt_gdngate_report(g->device); + pn_prefill_wall_report(n_tokens, prefill_accounted(g)); + } + return last; +} + +int pn_qwen35moe_prefill_tt_last_batched(const pn_model *model) +{ + const pn_qwen35_tt *g = NULL; + + if (model && model->weights && model->forward == pn_qwen35moe_forward_tt) + g = (const pn_qwen35_tt *)pn_qwen35moe_model_gpu( + (const pn_qwen35moe_model *)model->weights); + return g ? g->prefill_last_batched : -1; +} + +/* ======================================================================================= + * STEP 4 S2 — THE SPECULATIVE VERIFY + * ======================================================================================= */ + +/** + * Run `rows` tokens (the seed, then the drafts) at positions `base_pos..` through ONE batched + * pass whose projections, norms, SwiGLU and residuals serve every row and whose GDN and + * attention mixers are the DECODE kernels run row by row (S0: bit-identical to per-token), + * then each row's head through the DECODE head path (its hidden row copied to row 0), giving + * each row's exact greedy token in `argmax_out[r]`. Leaves the carried state after the LAST + * row; snapshots of every earlier row are in slots 0..rows-2 for pn_qwen35_tt_spec_rollback(). + * K/V for every row is written at its position (a rejected row's is overwritten later). + * @return 0, or -1 (nothing is reliable; the caller must stop speculating) + */ +int pn_qwen35_tt_spec_verify(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *st, + const uint32_t *tokens, int rows, int base_pos, + uint32_t *argmax_out) +{ + int r, rc = 0; + double t0 = pn_time_ms(), t1; + + if (!g || !m || !cfg || !st || !tokens || rows < 1 || rows > 16 || !argmax_out || + g->tall_rows == 0u) + return -1; + if (pn_tt_gdn_snapshot_reserve(g->device, (uint32_t)rows) != 0) + return -1; + prefill_tall_enter(g); + if (prefill_alloc(g, cfg, rows) != 0) { + (void)prefill_tall_exit(g); + return -1; + } + { + const char *names0[PN_TT_CARD_CLOCKS_MAX], *names1[PN_TT_CARD_CLOCKS_MAX]; + double sec0[PN_TT_CARD_CLOCKS_MAX], sec1[PN_TT_CARD_CLOCKS_MAX]; + unsigned n0 = pn_tt_device_card_clocks(g->device, names0, sec0, PN_TT_CARD_CLOCKS_MAX); + unsigned n1, i, j; + + g_verify_mode = true; + /* S2c: the live-row promise — half tiles at <= 16 rows, eltwise E1 at <= 4 + * (`PN_TT_SPEC_HALF=0` off). */ + pn_tt_device_set_verify_rows(g->device, + (!getenv("PN_TT_SPEC_HALF") || + atoi(getenv("PN_TT_SPEC_HALF")) != 0) ? (uint32_t)rows : 0u); + rc = prefill_pass(g, m, cfg, tokens, rows, base_pos); + pn_tt_device_set_verify_rows(g->device, 0u); + g_verify_mode = false; + n1 = pn_tt_device_card_clocks(g->device, names1, sec1, PN_TT_CARD_CLOCKS_MAX); + for (i = 0; i < n1 && i < n0; i++) { + for (j = 0; j < g->spec_clock_count; j++) + if (strcmp(g->spec_clock_names[j], names1[i]) == 0) + break; + if (j == g->spec_clock_count) { + if (j == PN_TT_CARD_CLOCKS_MAX) + continue; + g->spec_clock_names[j] = names1[i]; + g->spec_clock_seconds[j] = 0.0; + g->spec_clock_count++; + } + g->spec_clock_seconds[j] += sec1[i] - sec0[i]; + } + } + if (rc == 0 && pn_tt_matvec_flush(g->device) != 0) + rc = -1; + /* Back on the decode strips with the decode mirror map republished FIRST: the decode + * head's norm and LM head read replicated activations, and under the tall map they would + * read the tall mirrors (the first S2a round emitted one token forever). */ + if (prefill_tall_exit(g) != 0) + rc = -1; + t1 = pn_time_ms(); + g->spec_pass_ms += t1 - t0; + /* S2c: every row's head in one LM-head pass (`PN_TT_SPEC_HEADS=0`: the S2 form below). */ + if (rc == 0 && !(getenv("PN_TT_SPEC_HEADS") && atoi(getenv("PN_TT_SPEC_HEADS")) == 0)) { + rc = pn_qwen35_tt_head_rows(g, st, &g->card_tall.hidden[g->card_tall.slot], rows, + argmax_out); + g->spec_head_ms += pn_time_ms() - t1; + return rc; + } + /* THE HEADS: row r's hidden (still in the tall set) into the decode hidden's row 0, then + * the DECODE head (norm, LM head, top-k) exactly as a single-position forward runs it. */ + for (r = 0; rc == 0 && r < rows; r++) { + int v, best = 0; + + if (pn_tt_row_copy(g->device, &g->card_tall.hidden[g->card_tall.slot], (uint32_t)r, + &g->card.hidden[g->card.slot], 0u) != 0 || + pn_qwen35_tt_head_row(g, st) != 0) { + rc = -1; + break; + } + /* The engine's greedy rule: strict >, the lowest index wins a tie. */ + for (v = 1; v < st->n_vocab; v++) + if (st->logits[v] > st->logits[best]) + best = v; + argmax_out[r] = (uint32_t)best; + } + g->spec_head_ms += pn_time_ms() - t1; + return rc; +} + +/** Restore the carried state after row `last_kept` (0-based) of the last verify, every layer: + * slot last_kept+1 holds the state before row last_kept+1, i.e. after row last_kept. */ +int pn_qwen35_tt_spec_rollback(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int last_kept) +{ + int layer, ssm = 0; + + for (layer = 0; layer < cfg->n_layer; layer++) { + if (g->layers[layer].is_attention) + continue; + if (pn_tt_gdn_layer_swap(g->device, (uint32_t)ssm, (uint32_t)last_kept + 1u) != 0) + return -1; + ssm++; + } + return 0; +} + +int pn_qwen35_tt_mtp_catchup_rows(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *st, + const uint32_t *next, int rows, int base_pos, + uint32_t *argmax_out) +{ + const double started = pn_time_ms(); + int rc; + + if (!g->mtp.on || rows < 1 || rows > 16 || g->tall_rows == 0u) + return -1; + /* The verify set holds the committed rows' hiddens: enter it as it was left. */ + prefill_tall_enter(g); + if (prefill_alloc(g, cfg, rows) != 0) { + (void)prefill_tall_exit(g); + return -1; + } + { + const uint32_t bands = prefill_span_for(g, rows); + pn_qwen35_tt_strips *s = &g->card; + + rc = 0; + if (prefill_bands(g) > 4u && bands != g_prefill_published_span) { + const pn_tt_activation *mirrored[2] = { &s->normed, &s->ffn_swiglu }; + + pn_tt_replica_map_set_span(bands); + rc = pn_tt_publish_replica_map(g->device, mirrored, 2u, &s->ffn_swiglu); + g_prefill_published_span = bands; + } + } + /* Exact per row is not required of a draft, but the verify's row paths make it so. */ + g_verify_mode = true; + pn_tt_device_set_verify_rows(g->device, (uint32_t)rows); + if (rc == 0) + rc = prefill_mtp_layer(g, m, cfg, next, rows, base_pos); + pn_tt_device_set_verify_rows(g->device, 0u); + g_verify_mode = false; + if (rc == 0 && pn_tt_matvec_flush(g->device) != 0) + rc = -1; + if (prefill_tall_exit(g) != 0) + rc = -1; + g->mtp.steps++; + g->mtp.ms += pn_time_ms() - started; + if (rc != 0) + return -1; + return pn_qwen35_tt_mtp_head_row(g, st, &g->card_tall.hidden[g->card_tall.slot], + (uint32_t)rows - 1u, argmax_out); +} + +/* PM 7: 192 rows, not 48. The prompt's SwiGLU must reach the WIDE eltwise members (W1a: a + * descriptor of >= 2,048 tiles; 192 rows = 6 tile rows x 384 = 2,304): 48 rows (768 tiles) + * left all 13 idle since the load, so the first long request retired and re-armed them + * (run_serve_warm.sh: first 1,630 TTFT 2.73 s vs 2.65 after). `PN_TT_WARM_ROWS` (2..192) + * overrides, for the A/B. */ +#define PN_QWEN35_TT_WARM_ROWS 192 + +static void warm_discard_token(const char *token, void *user_data) +{ + (void)token; + (void)user_data; +} + +int pn_qwen35moe_warm_tt(pn_model *model) +{ + const char *env = getenv("PN_TT_WARM"); + const char *rows_env = getenv("PN_TT_WARM_ROWS"); + const int rows = rows_env && *rows_env ? atoi(rows_env) : PN_QWEN35_TT_WARM_ROWS; + pn_sampling_params params; + pn_qwen35_tt *g; + pn_state *state; + uint32_t tokens[PN_QWEN35_TT_WARM_ROWS]; + double started; + int i, n; + + if (!model || !model->weights || model->forward != pn_qwen35moe_forward_tt || + (env && *env && atoi(env) == 0)) + return 0; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + if (!g || !g->device) + return 0; + /* A batched pass (every chunk family) and then two decode tokens (every decode family). + * The ids are arbitrary vocabulary; nothing is kept. */ + if (rows < 2 || rows > PN_QWEN35_TT_WARM_ROWS) + return 0; + for (i = 0; i < rows; i++) + tokens[i] = 1000u + (uint32_t)i * 7u; + state = pn_state_new(model); + if (!state) + return -1; + params = pn_sampling_defaults(); + params.temperature = 0.0f; + started = pn_time_ms(); + n = pn_stream_tokens(model, state, tokens, rows, 2, ¶ms, warm_discard_token, NULL); + pn_state_free(state); + /* The card held the warm-up's sequence: no session may take it for its own. */ + pn_qwen35_tt_ckpt_forget(g); + fprintf(stderr, "qwen35_tt: warm-up — %d-row prefill + %d token(s) in %.0f ms; every " + "family re-armed before the first request\n", rows, n, + pn_time_ms() - started); + return n < 0 ? -1 : 0; +} + +/** PM 5: the serve's idle tick — keep this model's resident families armed (see + * pn_tt_device_keepalive). @return NOPs sent, 0 if none were due or not a TT model, -1 */ +int pn_qwen35moe_keepalive_tt(pn_model *model) +{ + pn_qwen35_tt *g; + + if (!model || !model->weights || model->forward != pn_qwen35moe_forward_tt) + return 0; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu((pn_qwen35moe_model *)model->weights); + return g && g->device ? pn_tt_device_keepalive(g->device, PN_QWEN35_TT_KEEPALIVE_IDLE_S) : 0; +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_prefill_tt_wall.c b/src/models/qwen35moe_prefill_tt_wall.c new file mode 100644 index 0000000..f30078c --- /dev/null +++ b/src/models/qwen35moe_prefill_tt_wall.c @@ -0,0 +1,89 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_prefill_tt_wall.c + * @brief Region wall clocks for the TT batched prefill (see the header). + * + * status: measurement only. + * issues: process-global; the prefill is single-threaded and one call at a time. + * todo: none. + */ +#include "qwen35moe_prefill_tt_wall.h" + +#include + +#include "../core/bench.h" + +static const char *const kPrefillWallNames[PN_PREFILL_WALL_COUNT] = { + "setup", "state-reset", "grid-begin", "embed", "stage", "layers", "residual", "mtp", + "flush", "grid-end", "restage", "tall-exit", "param-reset", "tail-forward" +}; + +static double g_wall_seconds[PN_PREFILL_WALL_COUNT]; +static double g_wall_accounted[PN_PREFILL_WALL_COUNT]; +static double g_open_wall[PN_PREFILL_WALL_COUNT]; +static double g_open_accounted[PN_PREFILL_WALL_COUNT]; +static double g_total_mark; +static double g_total_accounted_mark; +static double g_wall_rearm[PN_PREFILL_WALL_COUNT]; +static double g_open_rearm[PN_PREFILL_WALL_COUNT]; +static double g_total_rearm_mark; +static double (*g_rearm_clock)(void); + +static double prefill_rearm_now(void) +{ + return g_rearm_clock ? g_rearm_clock() : 0.0; +} + +void pn_prefill_wall_set_rearm_clock(double (*clock)(void)) +{ + g_rearm_clock = clock; +} + +static double prefill_wall_now(void) +{ + return pn_time_ms() / 1000.0; +} + +void pn_prefill_wall_begin(double accounted_seconds) +{ + for (int i = 0; i < PN_PREFILL_WALL_COUNT; i++) { + g_wall_seconds[i] = 0.0; + g_wall_accounted[i] = 0.0; + g_wall_rearm[i] = 0.0; + } + g_total_rearm_mark = prefill_rearm_now(); + g_total_accounted_mark = accounted_seconds; + g_total_mark = prefill_wall_now(); +} + +void pn_prefill_wall_open(pn_prefill_wall_region region, double accounted_seconds) +{ + g_open_accounted[region] = accounted_seconds; + g_open_rearm[region] = prefill_rearm_now(); + g_open_wall[region] = prefill_wall_now(); +} + +void pn_prefill_wall_close(pn_prefill_wall_region region, double accounted_seconds) +{ + g_wall_seconds[region] += prefill_wall_now() - g_open_wall[region]; + g_wall_accounted[region] += accounted_seconds - g_open_accounted[region]; + g_wall_rearm[region] += prefill_rearm_now() - g_open_rearm[region]; +} + +void pn_prefill_wall_report(int n_tokens, double accounted_seconds) +{ + const double total = prefill_wall_now() - g_total_mark; + const double accounted = accounted_seconds - g_total_accounted_mark; + + fprintf(stderr, "qwen35_tt: prefill WALL — %d tokens in %.4f s (%.1f tok/s); " + "batched+sequential+transfer %.4f s, OUTSIDE them %.4f s; re-arms %.4f s\n", + n_tokens, total, total > 0.0 ? n_tokens / total : 0.0, accounted, + total - accounted, prefill_rearm_now() - g_total_rearm_mark); + for (int i = 0; i < PN_PREFILL_WALL_COUNT; i++) + if (g_wall_seconds[i] > 0.0) + fprintf(stderr, "qwen35_tt: wall %-12s %.4f s, of it in the lines %.4f, " + "outside %.4f, re-arms %.4f\n", + kPrefillWallNames[i], g_wall_seconds[i], g_wall_accounted[i], + g_wall_seconds[i] - g_wall_accounted[i], g_wall_rearm[i]); +} diff --git a/src/models/qwen35moe_prefill_tt_wall.h b/src/models/qwen35moe_prefill_tt_wall.h new file mode 100644 index 0000000..213208c --- /dev/null +++ b/src/models/qwen35moe_prefill_tt_wall.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_prefill_tt_wall.h + * @brief The batched prefill's WALL, by region, against the batched/sequential/transfer lines. + * + * The `batched prefill —` line sums three brackets that live INSIDE the layer loop. A serve + * request of 1,630 tokens took 7.07 s where that line said 4.02 s, so ~3 s lived outside it. + * This file clocks every region of `pn_qwen35moe_prefill_tt()` and reports, per region, its + * wall and how much of that wall the three lines already contain — the remainder is what + * no line names. Printed under `PN_TT_PREFILL_REPORT`, once per call (not cumulative). + * + * status: measurement only; changes no arithmetic and no ordering. + * issues: regions nest (residual and mtp inside a pass, state-reset and grid-begin inside setup), + * so the rows do not sum to total. + * todo: none. + */ +#ifndef PN_QWEN35MOE_PREFILL_TT_WALL_H +#define PN_QWEN35MOE_PREFILL_TT_WALL_H + +typedef enum { + PN_PREFILL_WALL_SETUP, /**< tall enter, row scratch, state zero, grid begin */ + PN_PREFILL_WALL_STATE_RESET, /**< the carried GDN state + conv window zeroed (in SETUP) */ + PN_PREFILL_WALL_GRID_BEGIN, /**< the prefill grid booted (in SETUP) */ + PN_PREFILL_WALL_EMBED, /**< host embedding rows */ + PN_PREFILL_WALL_STAGE, /**< strip zero + band uploads of the embeddings */ + PN_PREFILL_WALL_LAYERS, /**< the whole layer loop */ + PN_PREFILL_WALL_RESIDUAL, /**< residual adds (inside LAYERS) */ + PN_PREFILL_WALL_MTP, /**< the MTP block over the prompt */ + PN_PREFILL_WALL_FLUSH, /**< the final matvec flush */ + PN_PREFILL_WALL_GRID_END, + PN_PREFILL_WALL_RESTAGE, /**< re-zero every strip the prompt wrote */ + PN_PREFILL_WALL_TALL_EXIT, /**< republish the decode mirror map */ + PN_PREFILL_WALL_PARAM_RESET, + PN_PREFILL_WALL_TAIL, /**< the last token's ordinary forward (the logits) */ + PN_PREFILL_WALL_COUNT +} pn_prefill_wall_region; + +/** Start a call: zero every region, mark the total. */ +void pn_prefill_wall_begin(double accounted_seconds); + +/** Open / close a region. `accounted_seconds` = batched + sequential + transfer right now. */ +void pn_prefill_wall_open(pn_prefill_wall_region region, double accounted_seconds); +void pn_prefill_wall_close(pn_prefill_wall_region region, double accounted_seconds); + +/** The re-arm clock (seconds spent booting / starting / stopping residencies so far): each + * region then also reports how much of its wall was idle-retire re-arms. NULL = none. */ +void pn_prefill_wall_set_rearm_clock(double (*clock)(void)); + +/** Print `prefill WALL —` and one row per region that moved. */ +void pn_prefill_wall_report(int n_tokens, double accounted_seconds); + +#endif /* PN_QWEN35MOE_PREFILL_TT_WALL_H */ diff --git a/src/models/qwen35moe_resident_tt.c b/src/models/qwen35moe_resident_tt.c new file mode 100644 index 0000000..92f5169 --- /dev/null +++ b/src/models/qwen35moe_resident_tt.c @@ -0,0 +1,1274 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_resident_tt.c + * @brief The `qwen35` hybrid forward with its ACTIVATIONS resident in card DRAM. + * + * --------------------------------------------------------------------------- + * WHAT THIS RUNG REMOVES, AND WHY IT IS EXACTLY THIS MUCH + * --------------------------------------------------------------------------- + * The rung below this one (`qwen35moe_forward_tt.c`'s projection-only arm) put all 249 + * matmuls on the card and left every activation on the host, so each projection's result + * came back over a link measured at 4.97 MB/s. That is 6.39 MiB and 1.286 s a token — 56% + * of it. The bytes decompose exactly, and the decomposition is what decides this file: + * + * | consumer | B/token | share | + * |-------------------------------------|-----------|-------| + * | the dense SwiGLU (gate, up, down) | 3,670,016 | 54.8% | + * | the LM head's logit row | 993,280 | 14.8% | + * | the Gated-Delta-Net's operands | 1,185,792 | 17.7% | + * | ssm_out + attn_output | 327,680 | 4.9% | + * | the attention block's q/k/v | 524,288 | 7.8% | + * + * The first two, and the fourth, need NO arithmetic the card cannot already do: an RMSNorm + * (the reduce residency), silu(gate)*up (the elementwise residency), a residual add (the + * same residency) and an exact top-N (the sampling residency) are all shipped ops with red + * proofs. Moving them is this file. That is 74.5% of the readback, and it does not depend on + * the recurrence at all. + * + * --------------------------------------------------------------------------- + * WHAT IT DELIBERATELY DOES NOT REMOVE, AND THE WARNING THAT APPLIES + * --------------------------------------------------------------------------- + * The Gated-Delta-Net recurrence and the attention core still run on the HOST, so their + * operands still cross: `attn_qkv`, `attn_gate`, `ssm_alpha` and `ssm_beta` come down and + * `gated` goes back up; `attn_q`, `attn_k` and `attn_v` come down and `attn_out` goes back + * up. **Each value crosses the link exactly once in each direction — nothing here is + * downloaded and re-uploaded unchanged.** + * + * The standing warning is that on-card GDN and resident activations "must land together or + * the second cancels the first". That is a statement about the FLOOR, and this file makes it + * measurable rather than argued: the host recurrence goes from 17.7% of the readback to + * ~78% of what is left, but the readback itself falls ~4.4x. So this rung is not cancelled — + * what the host recurrence sets is the floor beneath it, and that floor cannot be lowered by + * anything in this file. + * + * Lowering it means moving the WHOLE Gated-Delta-Net block, not a piece of it, and that is + * arithmetic rather than preference. Per SSM layer the host takes over after four + * projections and 49,408 B come down. After an on-card conv1d it is still 49,408 — a + * depthwise convolution is width-preserving. After an on-card conv1d + L2 norm + GQA repeat + * it is 65,536, because the interleaved repeat DOUBLES q and k. Only with the delta rule and + * the gated norm on the card too does it collapse, to 256 B: `alpha` and `beta` down for the + * host `softplus` (there is no SFPU `log`) and `decay` and `beta` back up. + * + * **THAT IS `PN_TT_GDN_STRIP` AND IT IS IMPLEMENTED** — `qwen35moe_gdn_strip_tt.c`. This file + * carries both routes and the flag chooses: the block above the `gdn_strip` branch downloads + * nothing but `alpha` and `beta`. + * + * @status The residual stream, both RMSNorms, the dense SwiGLU, the LM head and the exact + * top-N run on the card. The attention core runs on the host. The GDN recurrence runs + * on the host too UNLESS `PN_TT_ONCARD` names `gdn`, in which case its two heavy + * phases are card descriptors over state resident in card DRAM + * (`qwen35moe_gdn_tt.c`) — an arm that has never executed on silicon. + * @issues None + * @todo None + */ + +#ifdef USE_TT + +#include "qwen35moe_tt_internal.h" + +#include +#include +#include +#include +#include + +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +#include "../accel/tt/pn_tt_dispatch.h" +#include "../accel/tt/pn_tt_gdn.h" +#include "../accel/tt/pn_tt_gdnc.h" +#include "../accel/tt/pn_tt_gdngate.h" +#include "../accel/tt/pn_tt_head_norm.h" +#include "../accel/tt/pn_tt_replica_map.h" +#include "../core/bench.h" +#include "../memory/arena.h" + +/** A strip is a whole number of 32x32 tiles. */ +#define PN_QWEN35_TT_TILE 32u + +/** Force the projection-only arm even when everything armed — the discriminator. */ +#define PN_QWEN35_TT_RESIDENT_ENV "PN_TT_RESIDENT" + +/* ============================================================ + * Bring-up + * ============================================================ */ + +/** Tiles wide enough to hold `features`, which the geometry gate has already made a + * multiple of the tile width for every strip this file allocates. */ +static uint32_t qwen35_tt_tiles(uint32_t features) +{ + return (features + PN_QWEN35_TT_TILE - 1u) / PN_QWEN35_TT_TILE; +} + +/** + * Allocate every strip ONCE, zeroed. + * + * The zeroing is load-bearing rather than tidy: a matvec writes only logical row 0 of each + * tile and the FPU is fed all 32 rows anyway, so rows 1-31 have to be zero and have to stay + * zero. Every op used here preserves that for free — a matmul of a zero row is zero, + * silu(0)*0 is zero, 0+0 is zero, and an RMS norm of a zero row is 0 * gamma * rsqrt(eps). + * + * Widths come from the WEIGHTS rather than from the config wherever a weight defines them, + * so a strip is exactly what its producer writes and its consumer reads. A disagreement + * between the artifact and the engine is then a refusal at `pn_tt_matvec_*` rather than a + * silently truncated projection. + */ +static int qwen35_tt_alloc_strips(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, + const pn_qwen35_tt_layer *ssm, + const pn_qwen35_tt_layer *attn) +{ + pn_qwen35_tt_strips *s = &g->card; + uint32_t embd_tiles = qwen35_tt_tiles((uint32_t)cfg->n_embd); + /* THE TWO HOT STRIPS, AND ONLY THOSE. Counted from this model's own shapes, A's DRAM + * bytes are `normed` 70.6%, `ffn_swiglu` 17.6% and `ssm_gated`/`attn_out` 11.8% — and + * these two are exactly the pair whose producers (the REDUCE and ELTWISE residencies) + * drain through the one shared writer that can mirror them. `normed` is the RMS-norm + * output that feeds all seven projections of every layer; `ffn_swiglu` feeds + * `ffn_down`. Mirroring anything else would cost writes and buy a bank nobody is + * queueing on. pn_tt_replica_map.h has the arithmetic. */ + uint32_t replicas; + const pn_tt_activation *mirrored[2]; + + /* TWO BANDS FOR THIS MODEL: a batched prefill pass carries 64 positions, halving the pass + * count — 282.3 -> ~300 tok/s with the 8-set replica table keeping the eltwise split, every + * arm IDENTICAL. Decode reads band 0 and is unchanged. This model runs both chunk paths, + * which a second band requires; any other model keeps 1. `PN_TT_STRIP_TILE_ROWS` wins. */ + (void)pn_tt_device_prefer_strip_tile_rows(g->device, 2u); + replicas = pn_tt_activation_replica_count(g->device); + + if (pn_tt_activation_alloc(g->device, embd_tiles, &s->hidden[0]) != 0 || + pn_tt_activation_alloc(g->device, embd_tiles, &s->hidden[1]) != 0 || + pn_tt_activation_alloc_replicated(g->device, embd_tiles, replicas, &s->normed) != 0 || + pn_tt_activation_alloc(g->device, embd_tiles, &s->project) != 0 || + pn_tt_activation_alloc(g->device, ssm->attn_qkv.tile_cols, &s->ssm_qkv) != 0 || + pn_tt_activation_alloc(g->device, ssm->attn_gate.tile_cols, &s->ssm_z) != 0 || + pn_tt_activation_alloc(g->device, ssm->ssm_alpha.tile_cols, &s->ssm_alpha) != 0 || + pn_tt_activation_alloc(g->device, ssm->ssm_beta.tile_cols, &s->ssm_beta) != 0 || + pn_tt_activation_alloc(g->device, ssm->ssm_out.tile_rows, &s->ssm_gated) != 0 || + pn_tt_activation_alloc(g->device, attn->attn_q.tile_cols, &s->attn_qg) != 0 || + pn_tt_activation_alloc(g->device, attn->attn_k.tile_cols, &s->attn_k) != 0 || + pn_tt_activation_alloc(g->device, attn->attn_v.tile_cols, &s->attn_v) != 0 || + pn_tt_activation_alloc(g->device, attn->attn_output.tile_rows, &s->attn_out) != 0 || + pn_tt_activation_alloc(g->device, ssm->ffn_gate.tile_cols, &s->ffn_gate) != 0 || + pn_tt_activation_alloc(g->device, ssm->ffn_up.tile_cols, &s->ffn_up) != 0 || + pn_tt_activation_alloc_replicated(g->device, ssm->ffn_down.tile_rows, replicas, + &s->ffn_swiglu) != 0 || + pn_tt_activation_alloc(g->device, g->output.tile_cols, &s->logits) != 0) + return -1; + /* TELL THE PRODUCERS. Allocating mirrors is half the mechanism; without this the copies + * are allocated, zeroed and never written again, and a matmul member reading one gets a + * strip of zeros with every kernel completing and no error word set. The map is + * published even when it is EMPTY, so a core the previous process left a map on is + * provably cleared rather than assumed to be. */ + mirrored[0] = &s->normed; + mirrored[1] = &s->ffn_swiglu; + if (pn_tt_publish_replica_map(g->device, mirrored, 2u, &s->ffn_swiglu) != 0) + return -1; + s->slot = 0; + return 0; +} + +/** Expand every norm weight into the card-side constants the reduce op reads. Once, at + * load: a per-token upload of gamma would put back a transfer this file exists to remove. */ +static int qwen35_tt_upload_norms(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg) +{ + uint32_t width = (uint32_t)cfg->n_embd; + int layer; + + for (layer = 0; layer < cfg->n_layer; layer++) { + int idx = pn_qwen35moe_model_layer_index(m, layer); + const float *pre; + const float *post; + + if (pn_qwen35moe_model_layer_type(m, layer) == PN_QWEN35MOE_LAYER_ATTN) { + const pn_qwen35moe_attn_layer *l = pn_qwen35moe_model_attn_layer(m, idx); + if (!l) + return -1; + pre = l->attn_norm; + post = l->post_attention_norm; + } else { + const pn_qwen35moe_ssm_layer *l = pn_qwen35moe_model_ssm_layer(m, idx); + if (!l) + return -1; + pre = l->attn_norm; + post = l->post_attention_norm; + } + if (pn_tt_norm_constants_upload(g->device, pre, width, cfg->rms_norm_eps, + &g->layers[layer].attn_norm) != 0 || + pn_tt_norm_constants_upload(g->device, post, width, cfg->rms_norm_eps, + &g->layers[layer].post_norm) != 0) + return -1; + } + return pn_tt_norm_constants_upload(g->device, pn_qwen35moe_model_output_norm(m), width, + cfg->rms_norm_eps, &g->output_norm); +} + +/** The host buffers the two host-side blocks read their projections into. Allocated once — + * a per-token malloc of 32 KiB inside a 32-layer loop is not free on a 2-core host. */ +static int qwen35_tt_alloc_host_scratch(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, + const pn_qwen35_tt_layer *ssm, + const pn_qwen35_tt_layer *attn) +{ + g->h_qkv = calloc((size_t)ssm->attn_qkv.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_z = calloc((size_t)ssm->attn_gate.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_alpha = calloc((size_t)ssm->ssm_alpha.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_beta = calloc((size_t)ssm->ssm_beta.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_gated = calloc((size_t)ssm->ssm_out.tile_rows * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_qg = calloc((size_t)attn->attn_q.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_k = calloc((size_t)attn->attn_k.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_v = calloc((size_t)attn->attn_v.tile_cols * PN_QWEN35_TT_TILE, sizeof(float)); + g->h_attn = calloc((size_t)attn->attn_output.tile_rows * PN_QWEN35_TT_TILE, + sizeof(float)); + g->h_embed = calloc((size_t)cfg->n_embd, sizeof(float)); + return (g->h_qkv && g->h_z && g->h_alpha && g->h_beta && g->h_gated && g->h_qg && + g->h_k && g->h_v && g->h_attn && g->h_embed) ? 0 : -1; +} + +/** + * Find the first layer of each kind, so the strip widths can come from a real weight. + * + * A hybrid stack has both, and this model's own load-time gate has already refused a + * geometry where either count is zero. Returning a refusal rather than assuming layer 0 is + * an SSM layer is what keeps this file honest about a family it has not seen. + */ +static int qwen35_tt_representative_layers(const pn_qwen35_tt *g, + const pn_qwen35_tt_layer **ssm, + const pn_qwen35_tt_layer **attn) +{ + int layer; + + *ssm = NULL; + *attn = NULL; + for (layer = 0; layer < g->n_layers; layer++) { + if (g->layers[layer].is_attention) { + if (!*attn) + *attn = &g->layers[layer]; + } else if (!*ssm) { + *ssm = &g->layers[layer]; + } + } + return (*ssm && *attn) ? 0 : -1; +} + +/** + * PM 7 ITEM 5: arm the GDN GATE family (the gates computed on the card) when the chunkwise + * prefill and its tall strips are up. A failure is SAID and leaves the host gate path, which is + * the pre-item-5 arithmetic (libm) — so a run that did not arm is not the new reference. + */ +static void qwen35_tt_gdngate_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg) +{ + const int n_ssm = pn_qwen35moe_model_n_ssm(m); + const float **ssm_a, **ssm_dt; + int i; + + if (!pn_tt_gdnc_is_armed(g->device) || g->tall_rows == 0u || n_ssm <= 0) + return; + ssm_a = calloc((size_t)n_ssm, sizeof(*ssm_a)); + ssm_dt = calloc((size_t)n_ssm, sizeof(*ssm_dt)); + for (i = 0; ssm_a && ssm_dt && i < n_ssm; i++) { + const pn_qwen35moe_ssm_layer *l = pn_qwen35moe_model_ssm_layer(m, i); + + ssm_a[i] = l ? l->ssm_a : NULL; + ssm_dt[i] = l ? l->ssm_dt : NULL; + if (!ssm_a[i] || !ssm_dt[i]) + break; + } + if (!ssm_a || !ssm_dt || i < n_ssm || + pn_tt_gdngate_arm(g->device, (uint32_t)n_ssm, (uint32_t)cfg->ssm_dt_rank, ssm_a, ssm_dt, + g->tall_rows * 32u, pn_tt_gdnc_compact_scale_bits(g->device)) != 0) + fprintf(stderr, "qwen35_tt: the GDN gates stay on the HOST (gdngate did not arm) — " + "alpha/beta read back as before, libm arithmetic\n"); + else { + const char *sweep = getenv("PN_TT_GDNGATE_SWEEP"); + + g->gdn_det_gates = true; + fprintf(stderr, "qwen35_tt: GDN gates ON THE CARD (ft_det_math) — prefill, verify and " + "decode read no alpha/beta back\n"); + if (sweep && atoi(sweep) > 0) + (void)pn_tt_gdngate_sweep(g->device, (uint32_t)atoi(sweep)); + } + free(ssm_a); + free(ssm_dt); +} + +int pn_qwen35_tt_resident_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg) +{ + const pn_qwen35_tt_layer *ssm; + const pn_qwen35_tt_layer *attn; + uint32_t eltwise_tiles[3]; + uint32_t eltwise_count = 2u; + const char *want; + + if (!g || !g->device || !m || !cfg || !g->layers) + return -1; + + want = getenv(PN_QWEN35_TT_RESIDENT_ENV); + if (want && *want && atoi(want) == 0) { + fprintf(stderr, "qwen35_tt: %s=0 — the resident-activation arm is OFF by request; " + "every projection round-trips through the host\n", + PN_QWEN35_TT_RESIDENT_ENV); + return 0; + } + if (qwen35_tt_representative_layers(g, &ssm, &attn) != 0) { + fprintf(stderr, "qwen35_tt: the resident arm needs one layer of each kind and this " + "stack has only one — staying on the projection-only arm\n"); + return 0; + } + + /* Every width an elementwise DESCRIPTOR will carry. Both residual adds are n_embd wide + * and the SwiGLU is ffn wide; one residency serves both, at one block depth that has to + * divide each of them (and twice itself into each) — pn_tt_device.h states the + * invariant and the core it cost. */ + eltwise_tiles[0] = qwen35_tt_tiles((uint32_t)cfg->n_embd); + eltwise_tiles[1] = ssm->ffn_gate.tile_cols; + /* ON-CARD ATTENTION IS ON BY DEFAULT FOR THIS FAMILY AND FOR NO OTHER, and it is declared + * HERE because a default belongs to a model rather than to the card. It passes a + * calibrated chat-parity gate on this hybrid (0.986080, disagree 2.76%, +0.4% perplexity) + * and is faster at every length measured; the same op family through `llama_forward_tt.c` + * is recorded as FAILING that gate on MiniCPM5-1B, so the default cannot be global. + * An explicit `PN_TT_ONCARD` list still wins over this. */ + pn_tt_device_oncard_default(g->device, PN_TT_ONCARD_ATTENTION, true); + + /* ON-CARD ATTENTION'S SIGMOID GATE IS A THIRD WIDTH, and it has to be declared HERE + * rather than where it is used: the residency runs ONE block depth for every descriptor + * it will ever see, so a width discovered after `place_cbs` is a descriptor the depth + * cannot serve. `n_head*head_dim` is the gate's width and it is not `n_embd` on this + * model — Ornith is 32 x 256 = 8,192 against 4,096. + * + * The question is only answerable because the call above re-read the selection: this ran + * BEFORE anything populated `oncard_requested`, so it used to read a calloc'd false and + * the third width was never declared at all. */ + if (pn_tt_device_oncard_requested(g->device, PN_TT_ONCARD_ATTENTION)) + eltwise_tiles[eltwise_count++] = + qwen35_tt_tiles((uint32_t)(cfg->n_head * cfg->head_dim)); + if (pn_tt_device_arm_oncard(g->device, qwen35_tt_tiles((uint32_t)cfg->n_embd), + eltwise_tiles, eltwise_count, g->output.tile_cols) != 0) + return 0; + + /* All three or none. A mixed arm would mean a per-op host fallback that downloads and + * re-uploads the very strip this file exists to keep resident, which is slower than the + * arm below it AND harder to attribute. A clean refusal is worth more than a fast-looking + * hybrid. */ + if (!pn_tt_device_oncard(g->device, PN_TT_ONCARD_NORM) || + !pn_tt_device_oncard(g->device, PN_TT_ONCARD_SWIGLU) || + !pn_tt_device_oncard(g->device, PN_TT_ONCARD_RESIDUAL)) { + fprintf(stderr, "qwen35_tt: the norm / SwiGLU / residual families did not all arm " + "(norm %d, swiglu %d, residual %d) — staying on the projection-only " + "arm\n", + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_NORM), + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_SWIGLU), + (int)pn_tt_device_oncard(g->device, PN_TT_ONCARD_RESIDUAL)); + return 0; + } + + if (qwen35_tt_alloc_strips(g, cfg, ssm, attn) != 0 || + qwen35_tt_upload_norms(g, m, cfg) != 0 || + qwen35_tt_alloc_host_scratch(g, cfg, ssm, attn) != 0) { + fprintf(stderr, "qwen35_tt: the resident arm could not be staged — staying on the " + "projection-only arm\n"); + return 0; + } + + /* A family that was ASKED for and did not arm is a refusal, not a configuration — say so + * once rather than quietly reading the whole logit row back and reporting a slow token + * nobody can attribute. */ + g->sparse_capable = pn_tt_device_oncard(g->device, PN_TT_ONCARD_TOPK); + if (g->sparse_logits && !g->sparse_capable) { + fprintf(stderr, "qwen35_tt: %s was asked for and the sampling residency did NOT arm " + "— the whole %u-tile logit row comes back instead\n", + PN_QWEN35_TT_SPARSE_ENV, g->output.tile_cols); + g->sparse_logits = false; + } + /* The Gated-Delta-Net block is the LAST family to arm and the only one that may fail + * FATALLY: everything above it degrades to a host op, but a half-staged GDN residency + * would leave some layers' carried state on the card and some on the host. */ + if (pn_qwen35_tt_gdn_arm(g, m, cfg) != 0) { + fprintf(stderr, "qwen35_tt: the on-card Gated-Delta-Net arm was asked for and could " + "not be staged — the recurrence stays on the host\n"); + pn_qwen35_tt_gdn_release(g); + } + /* THE STRIP BRIDGE, on top of that arm and never instead of it. A refusal leaves the + * host-mediated recurrence running, which is slower and not wrong. */ + if (pn_qwen35_tt_gdn_strip_arm(g, cfg) != 0) { + fprintf(stderr, "qwen35_tt: the strip bridge was asked for and could not be staged — " + "the block stays on the host-mediated path\n"); + pn_qwen35_tt_gdn_strip_release(g); + } + /* THE DEFAULT-VERSUS-NAMED SPLIT, AND HERE IT IS NOT THEORETICAL. The GDN block WITHOUT + * the bridge is a measured wall REGRESSION — 1.43 tok/s against the control's 2.20 — + * because the per-head norms on the host force `conv_out` and `y` across the link as well. + * So a DEFAULT arm that loses the bridge gives the recurrence back to the host rather than + * shipping the slow half of the block; an arm the operator NAMED keeps it, because that + * arm is the correctness vehicle they asked for and substituting it quietly would be a + * different forward. The card-side residency stays armed and simply goes unused. */ + if (g->gdn_on_card && !g->gdn_strip && + !pn_tt_device_oncard_named(g->device, PN_TT_ONCARD_GDN)) { + fprintf(stderr, "qwen35_tt: the Gated-Delta-Net block is on by DEFAULT and the strip " + "bridge did not arm. Without it the block is 1.43 tok/s against 2.20, " + "so the recurrence goes back to the host. Name `gdn` in PN_TT_ONCARD " + "to run it anyway.\n"); + pn_qwen35_tt_gdn_release(g); + } + if (pn_qwen35_tt_gdn_parity_arm(g, cfg) != 0) + return -1; + /* ON-CARD ATTENTION IS A REFUSAL WHEN IT WAS NAMED, AND A DEGRADATION WHEN IT IS ONLY THE + * DEFAULT. Every family above it falls back to a host op with the same arithmetic; this + * one moves the KV cache into card DRAM at bf16 and changes the accumulation order, so a + * half-armed attention asked for BY NAME is a forward that is partly one model and partly + * another and must not be substituted. + * + * But `return -1` here reaches `goto fail` in pn_qwen35moe_tt_upload, and that drops the + * WHOLE model to the CPU forward — on Ornith, ~0.05 tok/s instead of the host-attention + * arm's 2.2. That is the right price for a request the operator made explicitly and the + * wrong one for a default, so the two are split rather than sharing a flag. The split is + * inert while attention is opt-in; it is the prerequisite for defaulting it on. */ + if (pn_qwen35_tt_attn_arm(g, m, cfg) != 0) { + pn_qwen35_tt_attn_release(g); + if (pn_tt_device_oncard_named(g->device, PN_TT_ONCARD_ATTENTION)) { + fprintf(stderr, "qwen35_tt: on-card attention was asked for BY NAME and did not " + "arm — refusing rather than silently running the host block with " + "a different KV cache\n"); + return -1; + } + fprintf(stderr, "qwen35_tt: on-card attention is on by DEFAULT and did not arm — " + "running the HOST attention block instead. The answer is correct and " + "O(context) on the host; it is not the card arm.\n"); + } + /* STEP 4 S2b: the MTP draft head, after the attention arm reserved its KV region. A + * failure leaves speculation to the n-gram drafter. */ + if (pn_qwen35_tt_attn_is_on_card(g) && pn_qwen35_tt_mtp_arm(g, m, cfg) != 0) { + fprintf(stderr, "qwen35_tt: the MTP draft head did not arm — no MTP drafts\n"); + g->mtp.on = false; + } + if (pn_qwen35_tt_attn_is_on_card(g)) { + /* Only the CROSSED CONTROL needs a host copy of the card's answer, and only when it + * is asked for — the whole point of the arm is that nothing comes back. */ + if (getenv("PN_TT_ATTN_COMPARE")) { + g->h_attn_card = calloc((size_t)cfg->n_head * (size_t)cfg->head_dim, + sizeof(float)); + if (!g->h_attn_card) + return -1; + } + } + if (pn_qwen35_tt_prefill_tall_alloc(g) != 0) + return -1; + qwen35_tt_gdngate_arm(g, m, cfg); + g->resident = true; + fprintf(stderr, "qwen35_tt: resident activations ARMED — the residual stream, both " + "RMSNorms, the dense SwiGLU and the LM head stay in card DRAM; the " + "logit row %s\n", + g->sparse_logits ? "stays there too (exact top-N crosses instead)" + : "still comes back in full"); + return 0; +} + +void pn_qwen35_tt_resident_release(pn_qwen35_tt *g) +{ + if (!g) + return; + pn_qwen35_tt_gdn_parity_report(g); + pn_qwen35_tt_gdn_normcheck_report(g); + pn_qwen35_tt_gdn_parity_release(g); + pn_qwen35_tt_gdn_strip_release(g); + pn_qwen35_tt_gdn_release(g); + pn_qwen35_tt_attn_release(g); + free(g->h_qkv); + free(g->h_z); + free(g->h_alpha); + free(g->h_beta); + free(g->h_gated); + free(g->h_qg); + free(g->h_k); + free(g->h_v); + free(g->h_attn); + free(g->h_embed); + g->h_qkv = g->h_z = g->h_alpha = g->h_beta = g->h_gated = NULL; + g->h_qg = g->h_k = g->h_v = g->h_attn = g->h_embed = NULL; +} + +/* ============================================================ + * The forward + * ============================================================ */ + +/** The strip currently carrying the residual stream, and the one the next add writes. */ +static pn_tt_activation *qwen35_tt_hidden(pn_qwen35_tt *g) +{ + return &g->card.hidden[g->card.slot]; +} + +static pn_tt_activation *qwen35_tt_hidden_spare(pn_qwen35_tt *g) +{ + return &g->card.hidden[g->card.slot ^ 1]; +} + +/** hidden = hidden + project, on the card, into the spare strip. */ +static int qwen35_tt_residual(pn_qwen35_tt *g) +{ + if (pn_tt_add(g->device, qwen35_tt_hidden(g), &g->card.project, + qwen35_tt_hidden_spare(g)) != 0) + return -1; + g->card.slot ^= 1; + return 0; +} + +/** One projection, ISSUED. Counted here because this is the only place the resident arm + * asks the card for a matmul, and the count is the evidence that it did. */ +int pn_qwen35_tt_project(pn_qwen35_tt *g, const pn_tt_weight *w, + const pn_tt_activation *in, const pn_tt_activation *out, bool wait) +{ + int rc = wait ? pn_tt_matvec_resident(g->device, w, in, out) + : pn_tt_matvec_enqueue(g->device, w, in, out); + + if (rc != 0) { + g->mv_failed++; + if (!g->failure_reported) { + g->failure_reported = 1; + fprintf(stderr, "qwen35_tt: a card projection FAILED inside the resident " + "forward. There is no host arm for it at this rung, so the " + "token is refused rather than quietly computed elsewhere.\n"); + } + return -1; + } + g->mv_on_card++; + return 0; +} + +/** ITEM 6: `PN_TT_GDNGATE_DECODE=0` keeps det gates but computes them on the host from read + * alpha/beta (same bits; the A/B for the card path's speed). */ +static bool qwen35_tt_card_decode_gates(void) +{ + static int cached = -1; + + if (cached < 0) { + const char *env = getenv("PN_TT_GDNGATE_DECODE"); + + cached = (env && *env) ? (atoi(env) != 0) : 1; + } + return cached != 0; +} + +/** + * The Gated-Delta-Net block: four projections out, the recurrence on the host, one back. + * + * The four are INDEPENDENT — one input strip, four disjoint output strips — so they go in + * the ring together and the host waits once. Four is exactly `PN_TT_MATVEC_MAX_CHAIN`. + */ +static int qwen35_tt_ssm_block(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int ssm_index, + pn_qwen35moe_ssm_state *state, pn_arena *arena, + struct fw_pool *pool) +{ + pn_qwen35_tt_strips *s = &g->card; + uint32_t gated_n = (uint32_t)(cfg->ssm_state * cfg->ssm_dt_rank); + + if (pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false) != 0) + return -1; + + /* THE STRIP BRIDGE: `ssm_qkv` and `ssm_z` DO NOT COME DOWN. The block runs whole on the + * card and the only two things that cross are `alpha` and `beta` — one tile each, 256 B — + * because `sigmoid` and `exp(a * softplus(.))` need a `pow` the SFPU has no `log` for. + * That is 49,408 B a layer removed; see qwen35moe_gdn_strip_tt.c. */ + if (g->gdn_strip) { + /* ITEM 6: with det gates the card computes them — nothing is read back. */ + const bool card = g->gdn_det_gates && qwen35_tt_card_decode_gates(); + double started_ms; + int rc; + + if (!card && + (pn_tt_activation_download(g->device, &s->ssm_alpha, g->h_alpha, + s->ssm_alpha.tile_count * PN_QWEN35_TT_TILE) != 0 || + pn_tt_activation_download(g->device, &s->ssm_beta, g->h_beta, + s->ssm_beta.tile_count * PN_QWEN35_TT_TILE) != 0)) + return -1; + started_ms = pn_time_ms(); + rc = pn_qwen35_tt_gdn_strip_layer(g, l, cfg, ssm_index, &s->ssm_qkv, &s->ssm_z, + card ? NULL : g->h_beta, card ? NULL : g->h_alpha, + &s->ssm_gated); + g->host_recurrence_seconds += (pn_time_ms() - started_ms) / 1000.0; + if (rc != 0) + return -1; + (void)gated_n; + (void)state; + (void)arena; + (void)pool; + return pn_qwen35_tt_project(g, &d->ssm_out, &s->ssm_gated, &s->project, true); + } + + /* The four downloads flush the chain themselves — pn_tt_device.h's one-seam rule. */ + if (pn_tt_activation_download(g->device, &s->ssm_qkv, g->h_qkv, + s->ssm_qkv.tile_count * PN_QWEN35_TT_TILE) != 0 || + pn_tt_activation_download(g->device, &s->ssm_z, g->h_z, + s->ssm_z.tile_count * PN_QWEN35_TT_TILE) != 0 || + pn_tt_activation_download(g->device, &s->ssm_alpha, g->h_alpha, + s->ssm_alpha.tile_count * PN_QWEN35_TT_TILE) != 0 || + pn_tt_activation_download(g->device, &s->ssm_beta, g->h_beta, + s->ssm_beta.tile_count * PN_QWEN35_TT_TILE) != 0) + return -1; + + /* The CPU oracle's own conv1d, L2 norms, interleaved GQA repeat, delta rule and gated + * norm, on the oracle's own fp32 state. Shared code, not a reimplementation — which is + * what makes a disagreement with the oracle attributable to the matmuls. */ + { + double started_ms = pn_time_ms(); + /* The on-card arm carries the state in card DRAM, so it takes neither `state` nor the + * arena; the two device descriptors it issues are timed by the on-card counter and + * what lands HERE is only the host arithmetic left around them. */ + int recurrence_rc = + g->gdn_on_card + ? pn_qwen35_tt_gdn_recurrence(g, l, cfg, ssm_index, g->h_qkv, g->h_z, + g->h_beta, g->h_alpha, g->h_gated) + : pn_qwen35moe_ssm_recurrence(l, cfg, g->h_qkv, g->h_z, g->h_beta, g->h_alpha, + state, arena, g->h_gated, pool); + g->host_recurrence_seconds += (pn_time_ms() - started_ms) / 1000.0; + if (recurrence_rc < 0) + return -1; + } + + /* The on-card GATE, and it runs OUTSIDE the recurrence timer on purpose: it is a second + * full host recurrence, so timing it inside would report the arm as three times its own + * cost. A no-op unless PN_TT_GDN_PARITY armed it. */ + if (g->gdn_on_card && pn_qwen35_tt_gdn_parity_layer(g, l, cfg, ssm_index, g->h_qkv, + g->h_z, g->h_beta, g->h_alpha, state, + arena, pool) != 0) + return -1; + + if (pn_tt_activation_upload(g->device, &s->ssm_gated, g->h_gated, gated_n) != 0) + return -1; + return pn_qwen35_tt_project(g, &d->ssm_out, &s->ssm_gated, &s->project, true); +} + +/** + * The full-attention block: three projections out, the attention core on the host, one back. + * Q/K/V are independent, so they chain. + * + * THIS IS THE SHIPPED ARM AND THE ONE THE ORACLE WAS TAKEN ON. It is also the reference the + * crossed control diffs the card arm against, which is why it is left whole and reachable + * rather than replaced: `qwen35moe_attn_tt.c` runs BESIDE it under `PN_TT_ATTN_COMPARE`, not + * instead of it. + */ +static int qwen35_tt_attn_block(pn_qwen35_tt *g, const pn_qwen35moe_attn_layer *l, + const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, pn_qwen35moe_kv *kv, + int pos, pn_arena *arena, struct fw_pool *pool) +{ + pn_qwen35_tt_strips *s = &g->card; + uint32_t attn_n = (uint32_t)(cfg->n_head * cfg->head_dim); + + if (pn_qwen35_tt_project(g, &d->attn_q, &s->normed, &s->attn_qg, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_k, &s->normed, &s->attn_k, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_v, &s->normed, &s->attn_v, false) != 0) + return -1; + + if (pn_tt_activation_download(g->device, &s->attn_qg, g->h_qg, + s->attn_qg.tile_count * PN_QWEN35_TT_TILE) != 0 || + pn_tt_activation_download(g->device, &s->attn_k, g->h_k, + s->attn_k.tile_count * PN_QWEN35_TT_TILE) != 0 || + pn_tt_activation_download(g->device, &s->attn_v, g->h_v, + s->attn_v.tile_count * PN_QWEN35_TT_TILE) != 0) + return -1; + + { + double started_ms = pn_time_ms(); + int attention_rc = pn_qwen35moe_attn_core(l, cfg, g->h_qg, g->h_k, g->h_v, kv, pos, + arena, g->h_attn, pool); + g->host_attention_seconds += (pn_time_ms() - started_ms) / 1000.0; + if (attention_rc < 0) + return -1; + } + + if (pn_tt_activation_upload(g->device, &s->attn_out, g->h_attn, attn_n) != 0) + return -1; + return pn_qwen35_tt_project(g, &d->attn_output, &s->attn_out, &s->project, true); +} + +/** The dense SwiGLU FFN, entirely on the card: gate and up chain, silu(gate)*up is one + * descriptor rather than two, and down writes straight into the residual's operand. */ +static int qwen35_tt_ffn(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d) +{ + pn_qwen35_tt_strips *s = &g->card; + + if (pn_qwen35_tt_project(g, &d->ffn_gate, &s->normed, &s->ffn_gate, false) != 0 || + pn_qwen35_tt_project(g, &d->ffn_up, &s->normed, &s->ffn_up, false) != 0) + return -1; + if (pn_tt_matvec_flush(g->device) != 0) + return -1; + if (pn_tt_silu_mul(g->device, &s->ffn_gate, &s->ffn_up, &s->ffn_swiglu) != 0) + return -1; + return pn_qwen35_tt_project(g, &d->ffn_down, &s->ffn_swiglu, &s->project, true); +} + +/** One layer: attn_norm -> (SSM | attention) -> +residual -> post_norm -> FFN -> +residual. + * The same order `qwen35moe_forward.c` runs, with the norms and the adds on the card. */ +static int qwen35_tt_layer(pn_qwen35_tt *g, pn_qwen35moe_model *m, int layer, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *st, int pos, + struct fw_pool *pool) +{ + const pn_qwen35_tt_layer *d = &g->layers[layer]; + int idx = pn_qwen35moe_model_layer_index(m, layer); + int rc; + + if (pn_tt_rms_norm(g->device, &d->attn_norm, qwen35_tt_hidden(g), &g->card.normed) != 0) + return -1; + pn_arena_reset(st->arena); + if (d->is_attention) { + const pn_qwen35moe_attn_layer *l = pn_qwen35moe_model_attn_layer(m, idx); + if (!l) + return -1; + if (pn_qwen35_tt_attn_is_on_card(g)) { + const int max_seq = pn_qwen35moe_max_seq(cfg); + const int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; + + /* THE CROSSED CONTROL, and the ORDER is the whole of its correctness. The host + * block runs FIRST and leaves its answer in `g->h_attn`; the card block then + * runs over freshly re-projected q/k/v and OVERWRITES `s->project`, so the + * forward carries the CARD's answer and the host's is the shadow. Running it the + * other way round would silently make the compare arm a host forward — a + * measurement of the thing not under test. + * + * Both arms maintain their own KV cache: the host's `st->kv[idx]` and the card's + * region. Neither is ever resynced from the other, which is what makes a + * divergence at position N still visible at position N+1. */ + if (g->attn_compare && + qwen35_tt_attn_block(g, l, d, cfg, &st->kv[idx], pos, st->arena, pool) != 0) + return -1; + rc = pn_qwen35_tt_attn_block_card(g, d, cfg, idx, pos, seq_len); + if (rc == 0 && g->attn_compare) + pn_qwen35_tt_attn_compare(g, cfg, layer, g->h_attn); + } else { + rc = qwen35_tt_attn_block(g, l, d, cfg, &st->kv[idx], pos, st->arena, pool); + } + } else { + const pn_qwen35moe_ssm_layer *l = pn_qwen35moe_model_ssm_layer(m, idx); + if (!l) + return -1; + rc = qwen35_tt_ssm_block(g, l, d, cfg, idx, &st->ssm[idx], st->arena, pool); + } + if (rc != 0 || qwen35_tt_residual(g) != 0) + return -1; + + if (pn_tt_rms_norm(g->device, &d->post_norm, qwen35_tt_hidden(g), &g->card.normed) != 0) + return -1; + if (qwen35_tt_ffn(g, d) != 0) + return -1; + return qwen35_tt_residual(g); +} + +/** + * The LM head, and the one place the logit row's 993,280 B a token is decided. + * + * With the sampling residency armed, the card selects the exact top-N and ~96 bytes of + * (index, value) pairs cross instead of 248,320 floats; the row handed back is the same + * sparse presentation the K80 path uses, `-INFINITY` everywhere the card did not name. The + * comparator is exact — highest value wins, lowest index breaks a tie — so a greedy argmax + * over this row is the argmax over the dense one, not an approximation of it. + */ +static int qwen35_tt_head_with(pn_qwen35_tt *g, pn_qwen35moe_state *st, + const pn_tt_norm_constants *norm) +{ + pn_qwen35_tt_strips *s = &g->card; + uint32_t i; + double started_ms; + + if (pn_tt_rms_norm(g->device, norm, qwen35_tt_hidden(g), &s->normed) != 0) + return -1; + if (pn_qwen35_tt_project(g, &g->output, &s->normed, &s->logits, true) != 0) + return -1; + + if (!g->sparse_logits) + return pn_tt_activation_download(g->device, &s->logits, st->logits, + (uint32_t)st->n_vocab); + + g->topk_count = 0; + if (pn_tt_topk(g->device, &s->logits, g->topk_pairs ? g->topk_pairs : PN_QWEN35_TT_TOPK_GREEDY, + g->topk_index, g->topk_value, &g->topk_count) != 0 || g->topk_count == 0u) + return -1; + + /* `PN_TT_TOPN_CROSSCHECK=1`: the card's top-N against a host selection over the dense row + * (value desc, index asc) — same ids, same values, same order. Instrumentation only. */ + if (getenv("PN_TT_TOPN_CROSSCHECK")) { + static unsigned long checked, bad; + float *dense = malloc((size_t)st->n_vocab * sizeof(float)); + + if (dense && pn_tt_activation_download(g->device, &s->logits, dense, + (uint32_t)st->n_vocab) == 0) { + uint32_t k; + int ok = 1; + + for (k = 0; k < g->topk_count && ok; k++) { + uint32_t v, rank = 0; + const uint32_t id = g->topk_index[k]; + + if (id >= (uint32_t)st->n_vocab || dense[id] != g->topk_value[k]) { ok = 0; break; } + for (v = 0; v < (uint32_t)st->n_vocab; v++) + if (dense[v] > dense[id] || (dense[v] == dense[id] && v < id)) + rank++; + if (rank != k) ok = 0; + } + checked++; + if (!ok) bad++; + if ((checked & 63u) == 0u || !ok) + fprintf(stderr, "qwen35_tt: TOPN CROSSCHECK — %lu rows, %lu mismatched (N %u)\n", + checked, bad, g->topk_count); + } + free(dense); + } + /* 248,320 floats a token written twice — once to -INFINITY here, once more when the + * caller copies the row into the engine's tensor. The card sent 96 bytes; the host still + * touches 993,280. Timed because it is host work between two seams and therefore in + * NONE of the device counters. */ + started_ms = pn_time_ms(); + for (i = 0; i < (uint32_t)st->n_vocab; i++) + st->logits[i] = -INFINITY; + for (i = 0; i < g->topk_count; i++) { + if (g->topk_index[i] < (uint32_t)st->n_vocab) + st->logits[g->topk_index[i]] = g->topk_value[i]; + } + g->host_logit_seconds += (pn_time_ms() - started_ms) / 1000.0; + return 0; +} + +static int qwen35_tt_head(pn_qwen35_tt *g, pn_qwen35moe_state *st) +{ + return qwen35_tt_head_with(g, st, &g->output_norm); +} + +/* S2c: the verify's heads in ONE pass — rows 0..rows-1 of `hidden` through the final norm and + * the LM head together (one weight stream; the half-tile levers hold for <= 16 rows), then a + * top-k per row (the kernel's row select) and the engine's greedy rule on each. */ +int pn_qwen35_tt_head_rows(pn_qwen35_tt *g, pn_qwen35moe_state *st, + const pn_tt_activation *hidden, int rows, uint32_t *argmax_out) +{ + pn_qwen35_tt_strips *s = &g->card; + int r, rc = 0; + + if (rows < 1 || rows > 16 || !g->sparse_logits) + return -1; + pn_tt_device_set_verify_rows(g->device, (uint32_t)rows); + if (pn_tt_rms_norm(g->device, &g->output_norm, hidden, &s->normed) != 0 || + pn_qwen35_tt_project(g, &g->output, &s->normed, &s->logits, true) != 0) + rc = -1; + pn_tt_device_set_verify_rows(g->device, 0u); + for (r = 0; rc == 0 && r < rows; r++) { + uint32_t i; + int best = -1; + + pn_tt_device_set_topk_row(g->device, (uint32_t)r); + g->topk_count = 0; + if (pn_tt_topk(g->device, &s->logits, + g->topk_pairs ? g->topk_pairs : PN_QWEN35_TT_TOPK_GREEDY, g->topk_index, + g->topk_value, &g->topk_count) != 0 || g->topk_count == 0u) { + rc = -1; + break; + } + /* The greedy rule over the sparse row: largest value, lowest index on a tie. */ + { + float bv = -INFINITY; + + for (i = 0; i < g->topk_count; i++) { + const int idx = (int)g->topk_index[i]; + const float v = g->topk_value[i]; + + if (idx >= st->n_vocab) + continue; + if (best < 0 || v > bv || (v == bv && idx < best)) { + best = idx; + bv = v; + } + } + } + if (best < 0) { + rc = -1; + break; + } + argmax_out[r] = (uint32_t)best; + memcpy(g->spec_topk_index[r], g->topk_index, sizeof(g->spec_topk_index[r])); + memcpy(g->spec_topk_value[r], g->topk_value, sizeof(g->spec_topk_value[r])); + g->spec_topk_count[r] = g->topk_count; + } + pn_tt_device_set_topk_row(g->device, 0u); + return rc; +} + +int pn_qwen35_tt_head_row(pn_qwen35_tt *g, pn_qwen35moe_state *st) +{ + int rc; + + if (!getenv("PN_TT_HEAD_ROW0") || atoi(getenv("PN_TT_HEAD_ROW0")) != 0) + pn_tt_device_set_single_row(g->device, true); + rc = qwen35_tt_head(g, st); + pn_tt_device_set_single_row(g->device, false); + return rc; +} + +/** + * DFlash acceptance measurement (`PN_TT_DUMP_HIDDEN=`): after layers 1,5,...,29 (the + * drafter's `target_layer_ids`, HF `hidden_states[l+1]`) append this position's residual + * row, plus layer 31 (the last: final norm + LM head must reproduce the next token, the dump's + * own check). Record: int32 pos, int32 token, then 9 x n_embd float32. Instrumentation only — every + * record is a card read; run it with PN_TT_SPEC=0 PN_TT_PREFILL=0 PN_TT_DISPATCH=0. + */ +static void qwen35_tt_dump_hidden(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int layer, + uint32_t token, int pos) +{ + static FILE *out; + static int armed = -1; + static float *row; + int32_t head[2]; + + if (armed < 0) { + const char *path = getenv("PN_TT_DUMP_HIDDEN"); + + armed = 0; + if (path && *path) { + out = fopen(path, "wb"); + row = calloc((size_t)cfg->n_embd, sizeof(float)); + armed = (out && row) ? 1 : 0; + if (!armed) + fprintf(stderr, "qwen35_tt: PN_TT_DUMP_HIDDEN %s could not be opened\n", path); + } + } + if (!armed || layer < 1 || ((layer - 1) % 4 != 0 && layer != 31) || layer > 31) + return; + if (layer == 1) { + head[0] = pos; + head[1] = (int32_t)token; + fwrite(head, sizeof(head), 1, out); + } + if (pn_tt_activation_download(g->device, &g->card.hidden[g->card.slot], row, + (uint32_t)cfg->n_embd) != 0) + memset(row, 0, (size_t)cfg->n_embd * sizeof(float)); + fwrite(row, sizeof(float), (size_t)cfg->n_embd, out); + if (layer == 31) + fflush(out); +} + +float *pn_qwen35_tt_resident_forward(pn_qwen35_tt *g, pn_qwen35moe_model *m, uint32_t token, + pn_qwen35moe_state *st, int pos, struct fw_pool *pool) +{ + const pn_qwen35moe_config *cfg = pn_qwen35moe_model_config(m); + int layer; + + if (!cfg || (int)token >= cfg->n_vocab) + return NULL; + + /* THE CARD'S CARRIED STATE IS PER SEQUENCE and there is one region set per model, so a + * new sequence has to zero it exactly where the host arm gets a freshly calloc'd + * `pn_qwen35moe_ssm_state`. Position 0 is that seam. A caller resuming mid-sequence from + * a prefix cache would not pass through it — stated because nothing here can detect it. */ + if (g->gdn_on_card && pos == 0 && pn_tt_gdn_state_reset(g->device) != 0) + return NULL; + /* STEP 3 (A): the dispatch recorder sees this whole forward, embedding to top-k. */ + pn_tt_dispatch_token_begin(g->device, g->tokens + 1u); + + /* The embedding table is not on the card — it is tiled block-float and no on-card op + * reads it — so the residual stream starts with one upload a token. 4,096 bf16 at the + * fast direction of the link is under 20 microseconds. */ + { + double started_ms = pn_time_ms(); + pn_qwen35moe_model_embed_row(m, token, g->h_embed); + g->host_embed_seconds += (pn_time_ms() - started_ms) / 1000.0; + } + g->card.slot = 0; + if (pn_tt_activation_upload(g->device, &g->card.hidden[0], g->h_embed, + (uint32_t)cfg->n_embd) != 0) + return NULL; + + /* ONE TOKEN: every strip's live row is row 0 (PHASE E row-0 path). Cleared on every exit + * so a batched prefill after this never inherits the promise. */ + pn_tt_device_set_single_row(g->device, true); + for (layer = 0; layer < cfg->n_layer; layer++) { + if (qwen35_tt_layer(g, m, layer, cfg, st, pos, pool) != 0) { + pn_tt_device_set_single_row(g->device, false); + return NULL; + } + qwen35_tt_dump_hidden(g, cfg, layer, token, pos); + } + pn_tt_device_set_single_row(g->device, false); + pn_qwen35_tt_gdn_parity_token(g, pos); + /* STEP 3a: `PN_TT_HEAD_ROW0` (default 1; 0 = off) keeps the single-row promise over the head too (final + * norm, LM head, top-k) — the LM head then reads row-0 half tiles like every projection. */ + if (!getenv("PN_TT_HEAD_ROW0") || atoi(getenv("PN_TT_HEAD_ROW0")) != 0) + pn_tt_device_set_single_row(g->device, true); + if (qwen35_tt_head(g, st) != 0) { + pn_tt_device_set_single_row(g->device, false); + pn_tt_dispatch_token_end(g->device, g->tokens + 1u); + return NULL; + } + pn_tt_device_set_single_row(g->device, false); + pn_tt_dispatch_token_end(g->device, g->tokens + 1u); + return st->logits; +} + +/* ======================================================================================= + * STEP 4 S2b — THE MTP DRAFT HEAD + * + * Ornith's KL-distilled nextn block (protoLabsAI/Ornith-1.0-9B-MTP-GGUF), converted by + * tools/build_mtp_gguf.py + ft_tt_convert_gguf into a second artifact. One position: + * x = eh_proj_e @ enorm(emb(t_{p+1})) + eh_proj_h @ hnorm(h_p) + * x = x + attn(attn_norm(x)) at p (its own KV region); x = x + ffn(post_norm(x)) + * draft = argmax(lm_head(shared_head_norm(x))) — the trunk's LM head (byte-identical) + * and x chains as the next position's h. It only proposes: the verify decides every token. + * ======================================================================================= */ + +static int qwen35_tt_mtp_norm(pn_qwen35_tt *g, const char *name, uint32_t n, float eps, + pn_tt_norm_constants *out) +{ + const float *v = pn_tt_device_extra_vector(g->device, name, n); + + return v ? pn_tt_norm_constants_upload(g->device, v, n, eps, out) : -1; +} + +void pn_qwen35_tt_mtp_resolve(pn_qwen35_tt *g, const char *trunk_artifact) +{ + const char *env = getenv("PN_TT_MTP"); + const char *spec = getenv("PN_TT_SPEC"); + size_t n; + + g->mtp.path[0] = '\0'; + /* SHIPPED DEFAULT (2026-10-06, David): MTP speculative decoding on. Off with + * `PN_TT_SPEC=0` (no MTP arms either) or `PN_TT_MTP=` (empty: no drafts from the head). */ + if (spec && atoi(spec) == 0) + return; + if (env) { + snprintf(g->mtp.path, sizeof(g->mtp.path), "%s", env); + return; + } + if (!trunk_artifact || (n = strlen(trunk_artifact)) < 5u || + strcmp(trunk_artifact + n - 5u, ".fttw") != 0 || n + 5u >= sizeof(g->mtp.path)) + return; + snprintf(g->mtp.path, sizeof(g->mtp.path), "%.*s.mtp.fttw", (int)(n - 5u), trunk_artifact); + if (access(g->mtp.path, R_OK) != 0) { + fprintf(stderr, "qwen35_tt: no MTP artifact at %s — plain decode (no speculative " + "drafts from the head)\n", g->mtp.path); + g->mtp.path[0] = '\0'; + } +} + +int pn_qwen35_tt_mtp_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, const pn_qwen35moe_config *cfg) +{ + const char *path = g->mtp.path; + pn_qwen35_tt_layer *d = &g->mtp.layer; + const uint32_t n_embd = (uint32_t)cfg->n_embd, head_dim = (uint32_t)cfg->head_dim; + const float *qn, *kn; + struct { const char *name; pn_tt_weight *w; } place[] = { + { "mtp.eh_proj_e.weight", &g->mtp.eh_e }, { "mtp.eh_proj_h.weight", &g->mtp.eh_h }, + { "mtp.attn_k.weight", &d->attn_k }, { "mtp.attn_v.weight", &d->attn_v }, + { "mtp.attn_output.weight", &d->attn_output }, + { "mtp.ffn_gate.weight", &d->ffn_gate }, { "mtp.ffn_up.weight", &d->ffn_up }, + { "mtp.ffn_down.weight", &d->ffn_down }, + }; + size_t i; + + (void)m; + g->mtp.on = false; + if (!path || !*path) + return 0; + if (!g->attn_on_card) { + fprintf(stderr, "qwen35_tt: MTP refused — it needs on-card attention\n"); + return -1; + } + memset(d, 0, sizeof(*d)); + d->is_attention = true; + if (pn_tt_device_extra_open(g->device, path) != 0) + return -1; + for (i = 0; i < sizeof(place) / sizeof(place[0]); i++) + if (pn_tt_device_place_extra_weight(g->device, place[i].name, place[i].w) != 0) + return -1; + if (pn_qwen35_tt_attn_split_q(g, cfg, "mtp.attn_q.weight", true, d) != 0) + return -1; + qn = pn_tt_device_extra_vector(g->device, "mtp.attn_q_norm.weight", head_dim); + kn = pn_tt_device_extra_vector(g->device, "mtp.attn_k_norm.weight", head_dim); + if (!qn || !kn || + pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, qn, head_dim, + cfg->rms_norm_eps, &d->attn_q_norm) != 0 || + pn_tt_head_norm_constants_upload(g->device, FT_TT_OP_REDUCE_FN_RMS_NORM, kn, head_dim, + cfg->rms_norm_eps, &d->attn_k_norm) != 0 || + qwen35_tt_mtp_norm(g, "mtp.attn_norm.weight", n_embd, cfg->rms_norm_eps, + &d->attn_norm) != 0 || + qwen35_tt_mtp_norm(g, "mtp.post_attention_norm.weight", n_embd, cfg->rms_norm_eps, + &d->post_norm) != 0 || + qwen35_tt_mtp_norm(g, "mtp.enorm.weight", n_embd, cfg->rms_norm_eps, + &g->mtp.enorm) != 0 || + qwen35_tt_mtp_norm(g, "mtp.hnorm.weight", n_embd, cfg->rms_norm_eps, + &g->mtp.hnorm) != 0 || + qwen35_tt_mtp_norm(g, "mtp.shared_head_norm.weight", n_embd, cfg->rms_norm_eps, + &g->mtp.head_norm) != 0) + return -1; + if (pn_tt_activation_alloc(g->device, qwen35_tt_tiles(n_embd), &g->mtp.h) != 0 || + pn_tt_activation_alloc(g->device, qwen35_tt_tiles(n_embd), &g->mtp.e) != 0) + return -1; + g->mtp.sub = false; + /* DEFAULT 2048 tiles (ids < 65,536): run_s2c5.sh — acceptance unchanged (0.81 short / + * 0.75 long), MTP 27.3 -> 18.3 ms a cycle; 1024 lost acceptance (0.73). 0 = whole head. */ + if (!getenv("PN_TT_MTP_VOCAB_TILES") || atoi(getenv("PN_TT_MTP_VOCAB_TILES")) > 0) { + const uint32_t total = g->output.tile_cols; + const uint32_t n = getenv("PN_TT_MTP_VOCAB_TILES") + ? (uint32_t)atoi(getenv("PN_TT_MTP_VOCAB_TILES")) : 2048u; + const uint32_t tail = 248044u / 32u; /* Ornith's special tokens start at 248044 */ + pn_tt_column_run runs[2] = { { 0u, n }, { tail, total - tail } }; + float *ninf = malloc((size_t)total * 32u * sizeof(float)); + uint32_t i; + + if (!ninf || n == 0u || n >= tail || tail >= total || + pn_tt_device_stage_weight_columns(g->device, "output.weight", runs, 2u, + &g->mtp.head_sub) != 0 || + pn_tt_activation_alloc(g->device, total, &g->mtp.logits_sub) != 0) { + free(ninf); + return -1; + } + for (i = 0; i < total * 32u; i++) + ninf[i] = -INFINITY; + if (pn_tt_activation_upload(g->device, &g->mtp.logits_sub, ninf, total * 32u) != 0) { + free(ninf); + return -1; + } + free(ninf); + g->mtp.sub_tiles = n; + g->mtp.sub_tail_first = tail; + g->mtp.sub = true; + fprintf(stderr, "qwen35_tt: MTP draft vocabulary — ids < %u + the %u-tile special tail " + "(%u of %u LM-head columns)\n", n * 32u, total - tail, + n + total - tail, total); + } + g->mtp.pend_n = 0; + g->mtp.on = true; + fprintf(stderr, "qwen35_tt: MTP draft head ARMED from %s (KV region %d)\n", path, + g->mtp.attn_index); + return 0; +} + +/** The draft head on the decode hidden (row 0): shared_head_norm, the (draft-vocabulary) LM + * head, top-k, the greedy rule. */ +/** The best value's softmax over the top-k values: an upper bound on its full-vocabulary + * probability, enough to say a draft is unlikely to be accepted. */ +static float qwen35_tt_topk_confidence(const float *values, uint32_t count, float best) +{ + double sum = 0.0; + uint32_t i; + + for (i = 0; i < count; i++) + sum += exp((double)(values[i] - best)); + return sum > 0.0 ? (float)(1.0 / sum) : 1.0f; +} + +static int qwen35_tt_mtp_head(pn_qwen35_tt *g, pn_qwen35moe_state *st, uint32_t *argmax_out) +{ + pn_qwen35_tt_strips *s = &g->card; + int rc = 0; + + if (g->mtp.sub) { + /* The draft vocabulary: subset logits into the -inf strip (the subset's columns are + * its first tiles), top-k, the best pair mapped back to its vocabulary id. */ + uint32_t i; + int best = -1; + float bv = -INFINITY; + + if (pn_tt_rms_norm(g->device, &g->mtp.head_norm, qwen35_tt_hidden(g), &s->normed) != 0 || + pn_qwen35_tt_project(g, &g->mtp.head_sub, &s->normed, &g->mtp.logits_sub, true) != 0) + rc = -1; + g->topk_count = 0; + if (rc == 0 && (pn_tt_topk(g->device, &g->mtp.logits_sub, PN_QWEN35_TT_TOPK_GREEDY, + g->topk_index, g->topk_value, &g->topk_count) != 0 || + g->topk_count == 0u)) + rc = -1; + for (i = 0; rc == 0 && i < g->topk_count; i++) { + const uint32_t sub = g->topk_index[i]; + const int id = (int)(sub < g->mtp.sub_tiles * 32u + ? sub + : g->mtp.sub_tail_first * 32u + (sub - g->mtp.sub_tiles * 32u)); + + if (id >= st->n_vocab) + continue; + if (best < 0 || g->topk_value[i] > bv || (g->topk_value[i] == bv && id < best)) { + best = id; + bv = g->topk_value[i]; + } + } + if (rc == 0 && best < 0) + rc = -1; + if (rc == 0 && argmax_out) + *argmax_out = (uint32_t)best; + if (rc == 0) + g->mtp.last_conf = qwen35_tt_topk_confidence(g->topk_value, g->topk_count, bv); + g->mtp.heads++; + } else { + int v, best = 0; + + rc = qwen35_tt_head_with(g, st, &g->mtp.head_norm); + for (v = 1; rc == 0 && v < st->n_vocab; v++) + if (st->logits[v] > st->logits[best]) + best = v; + if (rc == 0 && argmax_out) + *argmax_out = (uint32_t)best; + g->mtp.last_conf = 1.0f; + g->mtp.heads++; + } + return rc; +} + +int pn_qwen35_tt_mtp_head_row(pn_qwen35_tt *g, pn_qwen35moe_state *st, + const pn_tt_activation *src, uint32_t row, uint32_t *argmax_out) +{ + const double started = pn_time_ms(); + int rc; + + if (!g->mtp.on || pn_tt_row_copy(g->device, src, row, &g->card.hidden[g->card.slot], 0u) != 0 || + pn_tt_row_copy(g->device, src, row, &g->mtp.h, 0u) != 0) + return -1; + pn_tt_device_set_single_row(g->device, true); + rc = qwen35_tt_mtp_head(g, st, argmax_out); + pn_tt_device_set_single_row(g->device, false); + g->mtp.ms += pn_time_ms() - started; + return rc; +} + +int pn_qwen35_tt_mtp_step(pn_qwen35_tt *g, pn_qwen35moe_model *m, const pn_qwen35moe_config *cfg, + pn_qwen35moe_state *st, uint32_t token, const pn_tt_activation *h_src, + uint32_t h_row, int pos, bool want_head, uint32_t *argmax_out) +{ + pn_qwen35_tt_strips *s = &g->card; + const pn_qwen35_tt_layer *d = &g->mtp.layer; + const int max_seq = pn_qwen35moe_max_seq(cfg); + const double started = pn_time_ms(); + int rc = -1; + + if (!g->mtp.on || pos < 0 || pos >= max_seq) + return -1; + if (h_src && pn_tt_row_copy(g->device, h_src, h_row, &g->mtp.h, 0u) != 0) + return -1; + pn_qwen35moe_model_embed_row(m, token, g->h_embed); + if (pn_tt_activation_upload(g->device, &g->mtp.e, g->h_embed, (uint32_t)cfg->n_embd) != 0) + return -1; + pn_tt_device_set_single_row(g->device, true); + s->slot = 0; + if (pn_tt_rms_norm(g->device, &g->mtp.hnorm, &g->mtp.h, &s->normed) == 0 && + pn_qwen35_tt_project(g, &g->mtp.eh_h, &s->normed, &s->project, true) == 0 && + pn_tt_rms_norm(g->device, &g->mtp.enorm, &g->mtp.e, &s->normed) == 0 && + pn_qwen35_tt_project(g, &g->mtp.eh_e, &s->normed, &s->hidden[0], true) == 0 && + qwen35_tt_residual(g) == 0 && + pn_tt_rms_norm(g->device, &d->attn_norm, qwen35_tt_hidden(g), &s->normed) == 0 && + pn_qwen35_tt_attn_block_card(g, d, cfg, g->mtp.attn_index, pos, pos + 1) == 0 && + qwen35_tt_residual(g) == 0 && + pn_tt_rms_norm(g->device, &d->post_norm, qwen35_tt_hidden(g), &s->normed) == 0 && + qwen35_tt_ffn(g, d) == 0 && qwen35_tt_residual(g) == 0 && + pn_tt_row_copy(g->device, qwen35_tt_hidden(g), 0u, &g->mtp.h, 0u) == 0) + rc = 0; + if (rc == 0 && want_head) + rc = qwen35_tt_mtp_head(g, st, argmax_out); + pn_tt_device_set_single_row(g->device, false); + g->mtp.steps++; + g->mtp.ms += pn_time_ms() - started; + return rc; +} + +#endif /* USE_TT */ diff --git a/src/models/qwen35moe_ssm.c b/src/models/qwen35moe_ssm.c new file mode 100644 index 0000000..a3cdc59 --- /dev/null +++ b/src/models/qwen35moe_ssm.c @@ -0,0 +1,294 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_ssm.c + * @brief Gated-Delta-Net SSM block for Qwen3.5-MoE, one token at a time. + * + * Written from a behaviour specification (plans/cleanroom_r2_qwen35_spec.md); no ggml or llama.cpp source was consulted. + * + * @status None + * @issues The state struct's own dims are ignored (the config's are used). ssm_conv == 1 + * writes one float before conv_state. groups = nv / nk is computed before the + * asserts. A failure after the conv step leaves conv_state rolled. QWEN35_DEBUG is + * honoured only on the first recurrence call of the process; the trace statics are + * not thread-safe. + * @todo None + */ + +#include "qwen35moe_internal.h" + +#include +#include +#include +#include +#include + +#include "../accel/simd.h" +#include "../core/qweight.h" +#ifdef USE_THREADS +#include "threadpool/threadpool.h" +#endif + +/* Stack scratch per K-head in the q/k normalise step; bounds ssm_state. */ +#define SSM_HEAD_SCRATCH 256 + +static float ssm_softplus(float x) { + if (x > 20.0f) + return x; + return log1pf(expf(x)); +} + +static float ssm_silu(float x) { + return x / (1.0f + expf(-x)); +} + +/* QWEN35_DEBUG trace line: first three, last three, and the double sum of x[0..n). */ +static void ssm_dbg(const char *tag, const float *x, int n) { + static int debug_enabled = -1; + if (debug_enabled < 0) + debug_enabled = getenv("QWEN35_DEBUG") != NULL ? 1 : 0; + if (!debug_enabled) + return; + double sum = 0.0; + for (int i = 0; i < n; i++) + sum += x[i]; + fprintf(stderr, "SSM %-18s [%12.6f %12.6f %12.6f ... %12.6f %12.6f %12.6f] sum=%.6f\n", + tag, x[0], x[1], x[2], x[n - 3], x[n - 2], x[n - 1], sum); +} + +void pn_qwen35moe_l2_norm(float *data, int head_dim, float eps) { + double sumsq = 0.0; + for (int d = 0; d < head_dim; d++) + sumsq += (double)data[d] * (double)data[d]; + float inv = 1.0f / fmaxf((float)sqrt(sumsq), eps); + for (int d = 0; d < head_dim; d++) + data[d] *= inv; +} + +void pn_qwen35moe_beta_sigmoid(const float *beta_raw, int n_v_heads, float *beta_out) { + for (int h = 0; h < n_v_heads; h++) + beta_out[h] = 1.0f / (1.0f + expf(-beta_raw[h])); +} + +void pn_qwen35moe_decay_gate(const pn_qwen35moe_ssm_layer *layer, + const float *alpha, int n_v_heads, float *g_out) { + for (int h = 0; h < n_v_heads; h++) { + float sp = ssm_softplus(alpha[h] + layer->ssm_dt[h]); + g_out[h] = layer->ssm_a[h] * sp; + } +} + +void pn_qwen35moe_conv1d_silu(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, const float *qkv, + pn_qwen35moe_ssm_state *state, float *conv_out) { + int channels = cfg->conv_dim; + int taps = cfg->ssm_conv; + int m = taps - 1; + const float *w = layer->ssm_conv1d; + for (int c = 0; c < channels; c++) { + const float *cw = w + (size_t)c * taps; + float *cs = state->conv_state + (size_t)c * m; + float acc = qkv[c] * cw[m]; + for (int t = 0; t < m; t++) + acc += cs[t] * cw[t]; + conv_out[c] = ssm_silu(acc); + for (int t = 0; t < m - 1; t++) + cs[t] = cs[t + 1]; + cs[m - 1] = qkv[c]; + } +} + +void pn_qwen35moe_gated_norm(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, const float *y, + const float *z, float *gated) { + int S = cfg->ssm_state; + int n_heads = cfg->ssm_dt_rank; + float eps = cfg->rms_norm_eps; + for (int h = 0; h < n_heads; h++) { + const float *yh = y + (size_t)h * S; + const float *zh = z + (size_t)h * S; + float *gh = gated + (size_t)h * S; + pn_vrms_norm_f32(yh, layer->ssm_norm, gh, S, eps); + for (int d = 0; d < S; d++) + gh[d] *= ssm_silu(zh[d]); + } +} + +/* Inputs of the per-V-head delta-rule worker. */ +typedef struct { + const float *q; + const float *k; + const float *v; + const float *g; + const float *beta; + float *state; + float *y; + int S; + float scale; +} ssm_recurrence_args; + +/* Delta rule for V-heads [start, end): decay, predict, write, then read per value row. */ +static void ssm_recurrence_heads(int start, int end, void *arg) { + const ssm_recurrence_args *a = (const ssm_recurrence_args *)arg; + int S = a->S; + for (int h = start; h < end; h++) { + float *sh = a->state + (size_t)h * S * S; + const float *kh = a->k + (size_t)h * S; + const float *qh = a->q + (size_t)h * S; + const float *vh = a->v + (size_t)h * S; + float *yh = a->y + (size_t)h * S; + float decay = expf(a->g[h]); + float beta = a->beta[h]; + for (int j = 0; j < S; j++) { + float *row = sh + (size_t)j * S; + for (int i = 0; i < S; i++) + row[i] *= decay; + double predd = 0.0; + for (int i = 0; i < S; i++) + predd += (double)row[i] * (double)kh[i]; + float delta = (vh[j] - (float)predd) * beta; + for (int i = 0; i < S; i++) + row[i] += kh[i] * delta; + double yd = 0.0; + for (int i = 0; i < S; i++) + yd += (double)row[i] * (double)qh[i]; + yh[j] = a->scale * (float)yd; + } + } +} + +/* L2-normalise each K-head of the q and k sources and repeat them across V-heads, tiled: + * V-head r*nk + kh takes K-head kh. */ +static void ssm_split_normalise_repeat(const float *conv_out, int S, int nk, int groups, + int key_dim, int val_dim, float eps, + float *q, float *k, float *v) { + const float *q_src = conv_out; + const float *k_src = conv_out + key_dim; + const float *v_src = conv_out + 2 * (size_t)key_dim; + for (int kh = 0; kh < nk; kh++) { + float q_head[SSM_HEAD_SCRATCH]; + float k_head[SSM_HEAD_SCRATCH]; + memcpy(q_head, q_src + (size_t)kh * S, (size_t)S * sizeof(*q_head)); + memcpy(k_head, k_src + (size_t)kh * S, (size_t)S * sizeof(*k_head)); + pn_qwen35moe_l2_norm(q_head, S, eps); + pn_qwen35moe_l2_norm(k_head, S, eps); + for (int r = 0; r < groups; r++) { + int vh = r * nk + kh; + memcpy(q + (size_t)vh * S, q_head, (size_t)S * sizeof(*q)); + memcpy(k + (size_t)vh * S, k_head, (size_t)S * sizeof(*k)); + } + } + memcpy(v, v_src, (size_t)val_dim * sizeof(*v)); +} + +int pn_qwen35moe_ssm_recurrence(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, + const float *qkv, const float *z, + const float *beta_raw, const float *alpha, + pn_qwen35moe_ssm_state *state, pn_arena *arena, + float *gated, struct fw_pool *pool) { + if (!layer || !cfg || !qkv || !z || !beta_raw || !alpha || !state || !arena || !gated) + return -1; + int C = cfg->conv_dim; + int S = cfg->ssm_state; + int nk = cfg->ssm_n_group; + int nv = cfg->ssm_dt_rank; + int groups = nv / nk; + int key_dim = S * nk; + int val_dim = S * nv; + assert(S <= SSM_HEAD_SCRATCH && "ssm_state exceeds the per-head stack scratch"); + assert(nv % nk == 0 && "V-heads must be a multiple of K-heads"); + + float *beta = pn_arena_alloc_f32(arena, nv); + if (!beta) + return -1; + + /* Trace only the first recurrence call of the process. */ + static int traced_before = 0; + int trace = !traced_before; + traced_before = 1; + + if (trace) { + ssm_dbg("qkv_mixed", qkv, C); + ssm_dbg("z", z, val_dim); + } + pn_qwen35moe_beta_sigmoid(beta_raw, nv, beta); + if (trace) + ssm_dbg("beta_sigmoid", beta, nv); + + float *g = pn_arena_alloc_f32(arena, nv); + if (!g) + return -1; + pn_qwen35moe_decay_gate(layer, alpha, nv, g); + if (trace) + ssm_dbg("gate", g, nv); + + float *conv_out = pn_arena_alloc_f32(arena, C); + if (!conv_out) + return -1; + pn_qwen35moe_conv1d_silu(layer, cfg, qkv, state, conv_out); + if (trace) + ssm_dbg("conv_silu", conv_out, C); + + float *q = pn_arena_alloc_f32(arena, val_dim); + float *k = pn_arena_alloc_f32(arena, val_dim); + float *v = pn_arena_alloc_f32(arena, val_dim); + if (!q || !k || !v) + return -1; + ssm_split_normalise_repeat(conv_out, S, nk, groups, key_dim, val_dim, cfg->rms_norm_eps, + q, k, v); + if (trace) { + ssm_dbg("q_predelta", q, val_dim); + ssm_dbg("k_predelta", k, val_dim); + ssm_dbg("v_predelta", v, val_dim); + } + + float *y = pn_arena_alloc_f32(arena, val_dim); + if (!y) + return -1; + ssm_recurrence_args args = { + .q = q, .k = k, .v = v, .g = g, .beta = beta, + .state = state->ssm_state, .y = y, .S = S, + .scale = 1.0f / sqrtf((float)S), + }; +#ifdef USE_THREADS + if (pool && nv >= 4) + fw_for(pool, nv, ssm_recurrence_heads, &args); + else + ssm_recurrence_heads(0, nv, &args); +#else + (void)pool; + ssm_recurrence_heads(0, nv, &args); +#endif + if (trace) + ssm_dbg("recur_y", y, val_dim); + pn_qwen35moe_gated_norm(layer, cfg, y, z, gated); + if (trace) + ssm_dbg("final_output", gated, val_dim); + return 0; +} + +int pn_qwen35moe_ssm_block(const pn_qwen35moe_ssm_layer *layer, + const pn_qwen35moe_config *cfg, const float *x, + pn_qwen35moe_ssm_state *state, pn_arena *arena, + float *out, struct fw_pool *pool) { + if (!layer || !cfg || !x || !state || !arena || !out) + return -1; + int val_dim = cfg->ssm_state * cfg->ssm_dt_rank; + float *qkv = pn_arena_alloc_f32(arena, cfg->conv_dim); + float *z = pn_arena_alloc_f32(arena, val_dim); + float *beta = pn_arena_alloc_f32(arena, cfg->ssm_dt_rank); + float *alpha = pn_arena_alloc_f32(arena, cfg->ssm_dt_rank); + float *gated = pn_arena_alloc_f32(arena, val_dim); + if (!qkv || !z || !beta || !alpha || !gated) + return -1; + qwen35moe_mv(cfg, &layer->attn_qkv, x, qkv); + qwen35moe_mv(cfg, &layer->attn_gate, x, z); + qwen35moe_mv(cfg, &layer->ssm_beta, x, beta); + qwen35moe_mv(cfg, &layer->ssm_alpha, x, alpha); + if (pn_qwen35moe_ssm_recurrence(layer, cfg, qkv, z, beta, alpha, state, arena, gated, + pool) < 0) + return -1; + qwen35moe_mv(cfg, &layer->ssm_out, gated, out); + return 0; +} diff --git a/src/models/qwen35moe_state_snapshot.c b/src/models/qwen35moe_state_snapshot.c new file mode 100644 index 0000000..bcadeda --- /dev/null +++ b/src/models/qwen35moe_state_snapshot.c @@ -0,0 +1,587 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_state_snapshot.c + * @brief Host-side snapshot/restore of the qwen35moe dual state at a token + * position — the primitive behind SYSTEM-PREFIX caching. A snapshot + * deep-copies everything the forward needs to continue from position + * `pos`: every SSM layer's conv + recurrent state (fixed size) and every + * attention layer's KV rows [0, pos) plus the full fp16 sink/window + * shadows (q4 mode). Restore copies it back into a live state, making + * the host mirror authoritative again (kv_mirror_stale cleared); the + * caller must then reset pn_state->pos and (on the GPU path) call + * pn_qwen35moe_gpu_forget_state so the next forward re-seeds the GPU + * shadows from the restored host mirror. + * + * @status Complete: capture/restore round-trip is unit-tested host-side + * (tests/test_prefix_cache_sys.c) for both fp32 and q4 KV modes. Adds + * (K80 serving-ops Phase 7) pn_qwen35moe_snap_serialize/_deserialize — + * a contiguous portable blob for the session warm-tier; the snap→bytes + * →snap round-trip is byte-identical (unit-tested, same suite). + * @issues None + * @todo None + */ + +#include "qwen35moe_internal.h" +#include "qwen35moe_kv_q4.h" + +#include +#include +#include +#include + +/* One SSM layer's captured recurrent state (full copy — fixed size). */ +typedef struct { + size_t conv_n; /* floats in conv_state */ + size_t ssm_n; /* floats in ssm_state */ + float *conv; + float *ssm; +} snap_ssm_layer; + +/* One attention layer's captured KV: rows [0, pos) per head, packed + * [n_head_kv][pos][row_b] (the live cache strides by max_seq), plus the full + * fp16 sink/window shadows in q4 mode (the window is a RING — later positions + * overwrite earlier slots, so it cannot be reconstructed from the q4 body). */ +typedef struct { + int q4; + int n_head_kv; + int head_dim; + int max_seq; + size_t row_b; /* bytes per row: q4 packed or head_dim * 4 */ + unsigned char *k_rows; + unsigned char *v_rows; + uint16_t *k_sink; /* q4 only, else NULL */ + uint16_t *v_sink; + uint16_t *k_win; + uint16_t *v_win; +} snap_kv_layer; + +/* Whole-state snapshot at one position (opaque to callers). */ +typedef struct { + int pos; + int n_ssm; + int n_attn; + snap_ssm_layer *ssm; + snap_kv_layer *kv; +} qwen35moe_state_snap; + +/* Copy `pos` rows per head between packed ([pos] stride) and live ([max_seq] + * stride) layouts. dir_capture != 0: live -> packed; else packed -> live. */ +static void kv_rows_copy(unsigned char *packed, unsigned char *live, + int n_head_kv, int max_seq, int pos, size_t row_b, + int dir_capture) +{ + for (int h = 0; h < n_head_kv; h++) { + unsigned char *p = packed + (size_t)h * pos * row_b; + unsigned char *l = live + (size_t)h * max_seq * row_b; + if (dir_capture) + memcpy(p, l, (size_t)pos * row_b); + else + memcpy(l, p, (size_t)pos * row_b); + } +} + +/* Capture one attention layer's KV into sk. Returns 0 on success. */ +static int snap_kv_capture(const pn_qwen35moe_kv *kv, snap_kv_layer *sk, + int pos) +{ + sk->q4 = kv->q4; + sk->n_head_kv = kv->n_head_kv; + sk->head_dim = kv->head_dim; + sk->max_seq = kv->max_seq; + sk->row_b = kv->q4 ? (size_t)pn_kvq4_row_bytes(kv->head_dim) + : (size_t)kv->head_dim * sizeof(float); + + size_t rows_b = (size_t)kv->n_head_kv * pos * sk->row_b; + sk->k_rows = malloc(rows_b ? rows_b : 1); + sk->v_rows = malloc(rows_b ? rows_b : 1); + if (!sk->k_rows || !sk->v_rows) + return -1; + unsigned char *ksrc = kv->q4 ? kv->kq : (unsigned char *)kv->k; + unsigned char *vsrc = kv->q4 ? kv->vq : (unsigned char *)kv->v; + kv_rows_copy(sk->k_rows, ksrc, kv->n_head_kv, kv->max_seq, pos, sk->row_b, 1); + kv_rows_copy(sk->v_rows, vsrc, kv->n_head_kv, kv->max_seq, pos, sk->row_b, 1); + + if (kv->q4) { + size_t sink_n = (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * kv->head_dim; + size_t win_n = (size_t)kv->n_head_kv * PN_KVQ4_WMAX * kv->head_dim; + sk->k_sink = malloc(sink_n * sizeof(uint16_t)); + sk->v_sink = malloc(sink_n * sizeof(uint16_t)); + sk->k_win = malloc(win_n * sizeof(uint16_t)); + sk->v_win = malloc(win_n * sizeof(uint16_t)); + if (!sk->k_sink || !sk->v_sink || !sk->k_win || !sk->v_win) + return -1; + memcpy(sk->k_sink, kv->k_sink, sink_n * sizeof(uint16_t)); + memcpy(sk->v_sink, kv->v_sink, sink_n * sizeof(uint16_t)); + memcpy(sk->k_win, kv->k_win, win_n * sizeof(uint16_t)); + memcpy(sk->v_win, kv->v_win, win_n * sizeof(uint16_t)); + } + return 0; +} + +/* Restore one attention layer's KV from sk (dims already validated). */ +static void snap_kv_restore(const snap_kv_layer *sk, pn_qwen35moe_kv *kv, + int pos) +{ + unsigned char *kdst = kv->q4 ? kv->kq : (unsigned char *)kv->k; + unsigned char *vdst = kv->q4 ? kv->vq : (unsigned char *)kv->v; + kv_rows_copy(sk->k_rows, kdst, kv->n_head_kv, kv->max_seq, pos, sk->row_b, 0); + kv_rows_copy(sk->v_rows, vdst, kv->n_head_kv, kv->max_seq, pos, sk->row_b, 0); + if (kv->q4) { + size_t sink_n = (size_t)kv->n_head_kv * PN_KVQ4_SINKMAX * kv->head_dim; + size_t win_n = (size_t)kv->n_head_kv * PN_KVQ4_WMAX * kv->head_dim; + memcpy(kv->k_sink, sk->k_sink, sink_n * sizeof(uint16_t)); + memcpy(kv->v_sink, sk->v_sink, sink_n * sizeof(uint16_t)); + memcpy(kv->k_win, sk->k_win, win_n * sizeof(uint16_t)); + memcpy(kv->v_win, sk->v_win, win_n * sizeof(uint16_t)); + } + /* The restored host mirror is now the single source of truth. */ + kv->kv_mirror_stale = 0; + /* Phase 10b: content replaced under the same pointer — invalidate every + * GPU ring watermark keyed on (pointer, generation) so the next forward + * re-uploads the restored rows instead of trusting stale ring content. */ + kv->content_gen = pn_qwen35moe_kv_next_content_gen(); +} + +void pn_qwen35moe_state_snapshot_free(void *snap_v) +{ + qwen35moe_state_snap *sn = (qwen35moe_state_snap *)snap_v; + if (!sn) + return; + if (sn->ssm) { + for (int i = 0; i < sn->n_ssm; i++) { + free(sn->ssm[i].conv); + free(sn->ssm[i].ssm); + } + free(sn->ssm); + } + if (sn->kv) { + for (int i = 0; i < sn->n_attn; i++) { + free(sn->kv[i].k_rows); + free(sn->kv[i].v_rows); + free(sn->kv[i].k_sink); + free(sn->kv[i].v_sink); + free(sn->kv[i].k_win); + free(sn->kv[i].v_win); + } + free(sn->kv); + } + free(sn); +} + +void *pn_qwen35moe_state_snapshot(const void *state_v, int pos) +{ + const pn_qwen35moe_state *s = (const pn_qwen35moe_state *)state_v; + if (!s || pos <= 0) + return NULL; + for (int i = 0; i < s->n_attn; i++) + if (pos > s->kv[i].max_seq) + return NULL; + + qwen35moe_state_snap *sn = calloc(1, sizeof(*sn)); + if (!sn) + return NULL; + sn->pos = pos; + sn->n_ssm = s->n_ssm; + sn->n_attn = s->n_attn; + sn->ssm = s->n_ssm ? calloc((size_t)s->n_ssm, sizeof(*sn->ssm)) : NULL; + sn->kv = s->n_attn ? calloc((size_t)s->n_attn, sizeof(*sn->kv)) : NULL; + if ((s->n_ssm && !sn->ssm) || (s->n_attn && !sn->kv)) + goto fail; + + for (int i = 0; i < s->n_ssm; i++) { + const pn_qwen35moe_ssm_state *st = &s->ssm[i]; + snap_ssm_layer *sl = &sn->ssm[i]; + sl->conv_n = (size_t)st->conv_minus_1 * st->conv_dim; + sl->ssm_n = (size_t)st->state_size * st->state_size * st->n_v_heads; + sl->conv = malloc(sl->conv_n * sizeof(float)); + sl->ssm = malloc(sl->ssm_n * sizeof(float)); + if (!sl->conv || !sl->ssm) + goto fail; + memcpy(sl->conv, st->conv_state, sl->conv_n * sizeof(float)); + memcpy(sl->ssm, st->ssm_state, sl->ssm_n * sizeof(float)); + } + for (int i = 0; i < s->n_attn; i++) + if (snap_kv_capture(&s->kv[i], &sn->kv[i], pos) < 0) + goto fail; + return sn; + +fail: + pn_qwen35moe_state_snapshot_free(sn); + return NULL; +} + +/* Do the snapshot's dimensions match the target state exactly? A mismatch + * (different model / KV mode / window) makes a restore meaningless. */ +static int snap_matches_state(const qwen35moe_state_snap *sn, + const pn_qwen35moe_state *s) +{ + if (sn->n_ssm != s->n_ssm || sn->n_attn != s->n_attn) + return 0; + /* pos strides the live KV buffer on restore: snap_kv_restore copies + * pos*row_b bytes per head into a buffer whose per-head span is + * max_seq*row_b (kv_rows_copy). pos is an attacker-controlled header field + * (deserialize only checks pos>0), so a blob with pos>max_seq — all other + * dims matching this live model — drives an out-of-bounds write past the + * live cache. The capture path guards pos<=max_seq (pn_qwen35moe_state_ + * snapshot); the restore path MUST re-check it here since the blob is + * untrusted. Every attention layer shares the snapshot's single pos. */ + for (int i = 0; i < s->n_attn; i++) + if (sn->pos > s->kv[i].max_seq) + return 0; + for (int i = 0; i < s->n_ssm; i++) { + const pn_qwen35moe_ssm_state *st = &s->ssm[i]; + if (sn->ssm[i].conv_n != (size_t)st->conv_minus_1 * st->conv_dim || + sn->ssm[i].ssm_n != + (size_t)st->state_size * st->state_size * st->n_v_heads) + return 0; + } + for (int i = 0; i < s->n_attn; i++) { + const snap_kv_layer *sk = &sn->kv[i]; + const pn_qwen35moe_kv *kv = &s->kv[i]; + if (sk->q4 != kv->q4 || sk->n_head_kv != kv->n_head_kv || + sk->head_dim != kv->head_dim || sk->max_seq != kv->max_seq) + return 0; + } + return 1; +} + +int pn_qwen35moe_state_restore(void *state_v, const void *snap_v) +{ + pn_qwen35moe_state *s = (pn_qwen35moe_state *)state_v; + const qwen35moe_state_snap *sn = (const qwen35moe_state_snap *)snap_v; + if (!s || !sn) + return -1; + assert(sn->pos > 0); + if (!snap_matches_state(sn, s)) + return -1; + + for (int i = 0; i < s->n_ssm; i++) { + pn_qwen35moe_ssm_state *st = &s->ssm[i]; + memcpy(st->conv_state, sn->ssm[i].conv, + sn->ssm[i].conv_n * sizeof(float)); + memcpy(st->ssm_state, sn->ssm[i].ssm, + sn->ssm[i].ssm_n * sizeof(float)); + } + for (int i = 0; i < s->n_attn; i++) + snap_kv_restore(&sn->kv[i], &s->kv[i], sn->pos); + return 0; +} + +int pn_qwen35moe_state_snapshot_pos(const void *snap_v) +{ + const qwen35moe_state_snap *sn = (const qwen35moe_state_snap *)snap_v; + return sn ? sn->pos : -1; +} + +/* ============================================================ + * Serialize / deserialize — the session warm-tier blob (Phase 7) + * + * One contiguous little-endian blob. All integers are fixed-width and written + * LE regardless of host endianness (put_u32/64), so a blob is portable across + * machines. Layout: + * + * [ header ] + * u32 magic = PN_SNAP_MAGIC ("PNS1" as LE bytes) + * u32 format_version = PN_SNAP_FORMAT_VERSION + * u64 weights_version (opaque model-identity tag; validated by caller) + * i32 pos + * i32 n_ssm + * i32 n_attn + * [ n_ssm × SSM layer ] + * u64 conv_n (floats) + * u64 ssm_n (floats) + * f32 conv[conv_n] (raw little-endian float32) + * f32 ssm [ssm_n] + * [ n_attn × attention layer ] + * i32 q4 (0 = fp32 KV, 1 = q4 packed + fp16 shadows) + * i32 n_head_kv + * i32 head_dim + * i32 max_seq + * u64 row_b (bytes per row) + * bytes k_rows[n_head_kv * pos * row_b] + * bytes v_rows[...] + * (q4 only:) + * u16 k_sink[n_head_kv * PN_KVQ4_SINKMAX * head_dim] + * u16 v_sink[...] + * u16 k_win [n_head_kv * PN_KVQ4_WMAX * head_dim] + * u16 v_win [...] + * + * f32/u16 payloads are copied as raw bytes: the K80 fleet and any client are + * little-endian IEEE-754, so no per-element byte-swap is done (documented). The + * header/count fields ARE explicitly LE so the framing is unambiguous. On a + * hypothetical big-endian host the float payload would need swapping — out of + * scope for this fleet; the format_version gates a future change. + * ============================================================ */ + +#define PN_SNAP_MAGIC 0x31534E50u /* 'P''N''S''1' little-endian */ +#define PN_SNAP_FORMAT_VERSION 1u + +/* Bounded cursor over a byte buffer (write or read). `err` latches on the first + * overflow so callers check it once at the end instead of per-field. */ +typedef struct { + unsigned char *buf; /* NULL in sizing pass (writer) */ + size_t cap; + size_t off; + int err; +} snap_cursor; + +static void put_bytes(snap_cursor *c, const void *src, size_t n) +{ + if (c->err) + return; + if (c->off + n > c->cap) { c->err = 1; return; } + if (c->buf && n) + memcpy(c->buf + c->off, src, n); + c->off += n; +} + +static void put_u32(snap_cursor *c, uint32_t v) +{ + unsigned char b[4] = { (unsigned char)(v), (unsigned char)(v >> 8), + (unsigned char)(v >> 16), (unsigned char)(v >> 24) }; + put_bytes(c, b, 4); +} + +static void put_i32(snap_cursor *c, int32_t v) { put_u32(c, (uint32_t)v); } + +static void put_u64(snap_cursor *c, uint64_t v) +{ + unsigned char b[8]; + for (int i = 0; i < 8; i++) b[i] = (unsigned char)(v >> (8 * i)); + put_bytes(c, b, 8); +} + +static void get_bytes(snap_cursor *c, void *dst, size_t n) +{ + if (c->err) + return; + if (c->off + n > c->cap) { c->err = 1; return; } + if (n) + memcpy(dst, c->buf + c->off, n); + c->off += n; +} + +static uint32_t get_u32(snap_cursor *c) +{ + unsigned char b[4] = {0}; + get_bytes(c, b, 4); + return (uint32_t)b[0] | ((uint32_t)b[1] << 8) | + ((uint32_t)b[2] << 16) | ((uint32_t)b[3] << 24); +} + +static int32_t get_i32(snap_cursor *c) { return (int32_t)get_u32(c); } + +static uint64_t get_u64(snap_cursor *c) +{ + unsigned char b[8] = {0}; + get_bytes(c, b, 8); + uint64_t v = 0; + for (int i = 0; i < 8; i++) v |= (uint64_t)b[i] << (8 * i); + return v; +} + +/* Total q4 sink/window element counts for one attention layer. */ +static void snap_kv_shadow_counts(const snap_kv_layer *sk, size_t *sink_n, + size_t *win_n) +{ + *sink_n = (size_t)sk->n_head_kv * PN_KVQ4_SINKMAX * sk->head_dim; + *win_n = (size_t)sk->n_head_kv * PN_KVQ4_WMAX * sk->head_dim; +} + +/* Walk the whole snapshot through a cursor: write (buf != NULL) or size-only + * (buf == NULL, cursor->cap = SIZE_MAX). One walk shared by both so the sizing + * pass and the write pass can never disagree on the layout. */ +static void snap_walk_write(snap_cursor *c, const qwen35moe_state_snap *sn, + uint64_t weights_version) +{ + put_u32(c, PN_SNAP_MAGIC); + put_u32(c, PN_SNAP_FORMAT_VERSION); + put_u64(c, weights_version); + put_i32(c, sn->pos); + put_i32(c, sn->n_ssm); + put_i32(c, sn->n_attn); + + for (int i = 0; i < sn->n_ssm; i++) { + const snap_ssm_layer *sl = &sn->ssm[i]; + put_u64(c, (uint64_t)sl->conv_n); + put_u64(c, (uint64_t)sl->ssm_n); + put_bytes(c, sl->conv, sl->conv_n * sizeof(float)); + put_bytes(c, sl->ssm, sl->ssm_n * sizeof(float)); + } + for (int i = 0; i < sn->n_attn; i++) { + const snap_kv_layer *sk = &sn->kv[i]; + put_i32(c, sk->q4); + put_i32(c, sk->n_head_kv); + put_i32(c, sk->head_dim); + put_i32(c, sk->max_seq); + put_u64(c, (uint64_t)sk->row_b); + size_t rows_b = (size_t)sk->n_head_kv * sn->pos * sk->row_b; + put_bytes(c, sk->k_rows, rows_b); + put_bytes(c, sk->v_rows, rows_b); + if (sk->q4) { + size_t sink_n, win_n; + snap_kv_shadow_counts(sk, &sink_n, &win_n); + put_bytes(c, sk->k_sink, sink_n * sizeof(uint16_t)); + put_bytes(c, sk->v_sink, sink_n * sizeof(uint16_t)); + put_bytes(c, sk->k_win, win_n * sizeof(uint16_t)); + put_bytes(c, sk->v_win, win_n * sizeof(uint16_t)); + } + } +} + +int pn_qwen35moe_snap_serialize(const void *snap_v, uint64_t weights_version, + unsigned char **bytes_out, size_t *len_out) +{ + const qwen35moe_state_snap *sn = (const qwen35moe_state_snap *)snap_v; + if (!sn || !bytes_out || !len_out) + return -1; + *bytes_out = NULL; + *len_out = 0; + + /* Pass 1: size (buf == NULL, unbounded cap). */ + snap_cursor sz = { .buf = NULL, .cap = SIZE_MAX, .off = 0, .err = 0 }; + snap_walk_write(&sz, sn, weights_version); + if (sz.err) + return -1; + + unsigned char *buf = malloc(sz.off ? sz.off : 1); + if (!buf) + return -1; + + /* Pass 2: write into the exact-size buffer. */ + snap_cursor wr = { .buf = buf, .cap = sz.off, .off = 0, .err = 0 }; + snap_walk_write(&wr, sn, weights_version); + if (wr.err || wr.off != sz.off) { + free(buf); + return -1; + } + *bytes_out = buf; + *len_out = sz.off; + return 0; +} + +/* Read one SSM layer from the cursor into sl (allocates conv/ssm). */ +static int snap_ssm_read(snap_cursor *c, snap_ssm_layer *sl) +{ + sl->conv_n = (size_t)get_u64(c); + sl->ssm_n = (size_t)get_u64(c); + if (c->err) + return -1; + /* Guard against an absurd count that would overflow the malloc. */ + if (sl->conv_n > (SIZE_MAX / sizeof(float)) || + sl->ssm_n > (SIZE_MAX / sizeof(float))) + return -1; + sl->conv = malloc(sl->conv_n * sizeof(float) + 1); + sl->ssm = malloc(sl->ssm_n * sizeof(float) + 1); + if (!sl->conv || !sl->ssm) + return -1; + get_bytes(c, sl->conv, sl->conv_n * sizeof(float)); + get_bytes(c, sl->ssm, sl->ssm_n * sizeof(float)); + return c->err ? -1 : 0; +} + +/* Read one attention layer from the cursor into sk (allocates rows + shadows). */ +static int snap_kv_read(snap_cursor *c, snap_kv_layer *sk, int pos) +{ + sk->q4 = get_i32(c); + sk->n_head_kv = get_i32(c); + sk->head_dim = get_i32(c); + sk->max_seq = get_i32(c); + sk->row_b = (size_t)get_u64(c); + if (c->err || sk->n_head_kv < 0 || sk->head_dim < 0 || sk->max_seq < 0 || + (sk->q4 != 0 && sk->q4 != 1)) + return -1; + + /* row_b MUST be exactly what the arch derives from (q4, head_dim). A hostile + * blob that keeps the validated dims (q4/n_head_kv/head_dim/max_seq — the only + * fields snap_matches_state checks) but inflates row_b would drive an + * out-of-bounds write on restore: snap_kv_restore strides the live KV buffer by + * this row_b (kv_rows_copy: live + h*max_seq*row_b), so a larger-than-real row_b + * writes past the buffer. Pin row_b to the derived value here so it can never + * diverge from head_dim. */ + size_t expect_row_b = sk->q4 ? (size_t)pn_kvq4_row_bytes(sk->head_dim) + : (size_t)sk->head_dim * sizeof(float); + if (sk->row_b != expect_row_b) + return -1; + /* Guard the rows_b product against size_t overflow (a wrap to a small value + * would under-allocate k_rows). */ + if (sk->row_b != 0 && sk->n_head_kv != 0 && + (size_t)pos > SIZE_MAX / sk->row_b / (size_t)sk->n_head_kv) + return -1; + + size_t rows_b = (size_t)sk->n_head_kv * (size_t)pos * sk->row_b; + sk->k_rows = malloc(rows_b ? rows_b : 1); + sk->v_rows = malloc(rows_b ? rows_b : 1); + if (!sk->k_rows || !sk->v_rows) + return -1; + get_bytes(c, sk->k_rows, rows_b); + get_bytes(c, sk->v_rows, rows_b); + + if (sk->q4) { + size_t sink_n, win_n; + snap_kv_shadow_counts(sk, &sink_n, &win_n); + sk->k_sink = malloc(sink_n * sizeof(uint16_t)); + sk->v_sink = malloc(sink_n * sizeof(uint16_t)); + sk->k_win = malloc(win_n * sizeof(uint16_t)); + sk->v_win = malloc(win_n * sizeof(uint16_t)); + if (!sk->k_sink || !sk->v_sink || !sk->k_win || !sk->v_win) + return -1; + get_bytes(c, sk->k_sink, sink_n * sizeof(uint16_t)); + get_bytes(c, sk->v_sink, sink_n * sizeof(uint16_t)); + get_bytes(c, sk->k_win, win_n * sizeof(uint16_t)); + get_bytes(c, sk->v_win, win_n * sizeof(uint16_t)); + } + return c->err ? -1 : 0; +} + +void *pn_qwen35moe_snap_deserialize(const unsigned char *bytes, size_t len, + uint64_t *weights_version_out) +{ + if (weights_version_out) + *weights_version_out = 0; + if (!bytes || len < 24) /* header is 24 bytes minimum */ + return NULL; + + snap_cursor c = { .buf = (unsigned char *)bytes, .cap = len, + .off = 0, .err = 0 }; + if (get_u32(&c) != PN_SNAP_MAGIC) + return NULL; + if (get_u32(&c) != PN_SNAP_FORMAT_VERSION) + return NULL; + uint64_t wv = get_u64(&c); + if (weights_version_out) + *weights_version_out = wv; + int pos = get_i32(&c); + int n_ssm = get_i32(&c); + int n_attn = get_i32(&c); + if (c.err || pos <= 0 || n_ssm < 0 || n_attn < 0) + return NULL; + + qwen35moe_state_snap *sn = calloc(1, sizeof(*sn)); + if (!sn) + return NULL; + sn->pos = pos; + sn->n_ssm = n_ssm; + sn->n_attn = n_attn; + sn->ssm = n_ssm ? calloc((size_t)n_ssm, sizeof(*sn->ssm)) : NULL; + sn->kv = n_attn ? calloc((size_t)n_attn, sizeof(*sn->kv)) : NULL; + if ((n_ssm && !sn->ssm) || (n_attn && !sn->kv)) + goto fail; + + for (int i = 0; i < n_ssm; i++) + if (snap_ssm_read(&c, &sn->ssm[i]) != 0) + goto fail; + for (int i = 0; i < n_attn; i++) + if (snap_kv_read(&c, &sn->kv[i], pos) != 0) + goto fail; + + /* Reject trailing garbage: a valid blob is consumed exactly. */ + if (c.off != len) + goto fail; + return sn; + +fail: + pn_qwen35moe_state_snapshot_free(sn); + return NULL; +} diff --git a/src/models/qwen35moe_tt_internal.h b/src/models/qwen35moe_tt_internal.h new file mode 100644 index 0000000..c2a3db7 --- /dev/null +++ b/src/models/qwen35moe_tt_internal.h @@ -0,0 +1,934 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file qwen35moe_tt_internal.h + * @brief The state the two halves of the `qwen35` Tenstorrent backend share: + * `qwen35moe_forward_tt.c` (residency, bindings, counters) and + * `qwen35moe_resident_tt.c` (the resident-activation forward). + * + * Private to those two translation units. Nothing outside `src/models/` includes it. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_QWEN35MOE_TT_INTERNAL_H +#define PN_QWEN35MOE_TT_INTERNAL_H + +/** PM 5: a resident family idle this long gets a keepalive NOP (the retire limit is 5 s). */ +#define PN_QWEN35_TT_KEEPALIVE_IDLE_S 2.0 + + +#ifdef USE_TT + +#include +#include + +#include "qwen35moe_internal.h" + +#include "../accel/tt/pn_tt_device.h" +#include "../accel/tt/pn_tt_fp32_strip.h" +#include "../core/engine.h" + +/** How many (index, value) pairs the card's exact top-N returns when the logit row stays + * resident. Matches the llama backend's, and eight is well past what a greedy decode or a + * temperature sampler over a survivor set needs. */ +/** Top-N array capacity (the kernel's ceiling); the per-request N is `topk_pairs`. */ +#define PN_QWEN35_TT_TOPK_PAIRS 64u +/** N for greedy rows and MTP drafts. */ +#define PN_QWEN35_TT_TOPK_GREEDY 8u + +/** Present the LM head as the card's exact top-N instead of reading the whole logit row. + * Deliberately the same spelling as the llama backend's. */ +#define PN_QWEN35_TT_SPARSE_ENV "PN_TT_SPARSE_LOGITS" + +/** + * One host weight and the card weight that serves it. + * + * Keyed by the ADDRESS of the `pn_qweight` the model already holds, because that is the only + * thing the `qwen35moe_mv` seam is handed. The alternative — re-deriving a tensor name at + * every matvec — would put a string compare on the hot path and, worse, would make the seam + * depend on a naming convention rather than on the binding that was actually checked at load. + */ +typedef struct { + const pn_qweight *key; + pn_tt_weight card; +} pn_qwen35_tt_binding; + +/** + * Every card-side handle one layer needs, resolved once at load. + * + * A layer is EITHER an SSM layer or an attention layer, so the two projection sets alias in + * meaning rather than in storage: `is_attention` says which five (or four) of the first nine + * fields are live. Keeping both sets in one struct costs 5 words a layer and lets the + * resident forward index by absolute layer number, which is what every other structure here + * is keyed by. + */ +typedef struct { + bool is_attention; + + /* SSM layers */ + pn_tt_weight attn_qkv; /**< x -> the fused q|k|v stream, conv_dim wide */ + pn_tt_weight attn_gate; /**< x -> z, the gated norm's multiplier */ + pn_tt_weight ssm_alpha; + pn_tt_weight ssm_beta; + pn_tt_weight ssm_out; /**< gated -> the block output */ + + /* Attention layers */ + pn_tt_weight attn_q; /**< x -> Q and its output gate, INTERLEAVED per head */ + pn_tt_weight attn_k; + pn_tt_weight attn_v; + pn_tt_weight attn_output; + /** The two DE-INTERLEAVED halves of `attn_q`, staged only when attention is on the card. + * A joint projection cannot be read in place by either the QK-norm or flash decode, and + * permuting the WEIGHT costs one upload at load and nothing per token — + * `src/accel/tt/pn_tt_column_runs.h` prices that against the alternatives. */ + pn_tt_weight attn_q_only; + pn_tt_weight attn_gate_only; + /** The per-head QK-RMSNorm constants, at the ATTENTION head width. One gamma each, + * shared across heads, which is what `qwen35moe_qk_norm_heads()` does. */ + pn_tt_norm_constants attn_q_norm; + pn_tt_norm_constants attn_k_norm; + + /* Every layer */ + pn_tt_weight ffn_gate; + pn_tt_weight ffn_up; + pn_tt_weight ffn_down; + + pn_tt_norm_constants attn_norm; /**< pre-block RMSNorm */ + pn_tt_norm_constants post_norm; /**< pre-FFN RMSNorm */ +} pn_qwen35_tt_layer; + +/** + * Every activation of one token, resident in card DRAM. + * + * `hidden` is TWO strips and not one because the residual add is a stream op: the reader + * runs ahead of the writer, so writing the result back over an input would be safe today and + * would be an unstated dependency on that lead. Two strips and a slot index cost a few KiB + * of a 31 GiB card and depend on nothing. Same argument as the llama backend's. + */ +typedef struct { + pn_tt_activation hidden[2]; + pn_tt_activation normed; /**< every RMSNorm's output; every projection's input */ + pn_tt_activation project; /**< ssm_out / attn_output / ffn_down output, pre-residual */ + + /* The SSM block's four projection outputs and its one projection input. */ + pn_tt_activation ssm_qkv; + pn_tt_activation ssm_z; + pn_tt_activation ssm_alpha; + pn_tt_activation ssm_beta; + pn_tt_activation ssm_gated; + + /* The attention block's three projection outputs and its one projection input. */ + pn_tt_activation attn_qg; + pn_tt_activation attn_k; + pn_tt_activation attn_v; + pn_tt_activation attn_out; + + pn_tt_activation ffn_gate; + pn_tt_activation ffn_up; + pn_tt_activation ffn_swiglu; + + pn_tt_activation logits; + + int slot; /**< which `hidden` holds the live residual stream */ +} pn_qwen35_tt_strips; + +/** + * The extra strips the ON-CARD attention block needs, allocated only when it arms. + * + * `q_rot` and `k_rot` are separate from `q` and `k` rather than rotated in place because + * RoPE is a stream op whose reader runs ahead of its writer: an in-place rotation would be + * safe today and an unstated dependency on that lead. The same argument `hidden[2]` is + * written on. + */ +typedef struct { + pn_tt_activation q; /**< the de-interleaved Q half, then QK-normed in place */ + pn_tt_activation q_rot; /**< after partial NeoX RoPE — what flash decode reads */ + pn_tt_activation k_rot; + pn_tt_activation z; /**< the de-interleaved output-gate half, raw */ + pn_tt_activation gate; /**< sigmoid(z); the elementwise pair's intermediate */ + pn_tt_activation gated; /**< sigmoid(z) * attn — what `attn_output` reads */ +} pn_qwen35_tt_attn_strips; + +/** The model-level handle, stored on the model's `gpu` slot. */ +typedef struct { + pn_tt_device *device; + + /* The `qwen35moe_mv` seam's table — the PROJECTION-ONLY arm. Still built, because it is + * the arm the resident forward falls back to when the on-card families do not arm, and + * because it is the reference this project's ladder was measured on. */ + pn_qwen35_tt_binding *bindings; + int n_bindings; + int n_expected; + + /* The RESIDENT arm. `resident` is false when any of norm / SwiGLU / residual failed to + * arm; the forward then runs the seam arm and says so once. */ + bool resident; + /** The logit row as the card's exact top-N (effective, per request). `sparse_env`: + * PN_TT_SPARSE_LOGITS as set (1/0), or -1 unset = sparse for GREEDY requests only (the + * greedy argmax over the top-N is the dense argmax; a sampled request keeps the dense + * row). `sparse_capable`: the sampling residency armed. */ + bool sparse_logits; + int sparse_env; + bool sparse_capable; + uint32_t topk_pairs; /**< this request's N: 8 greedy, 64 sampled (top_k <= 64) */ + uint64_t spec_deferred; /**< spec-sampling cycles cut short by an inexact top-N row */ + pn_qwen35_tt_layer *layers; + int n_layers; + pn_tt_weight output; + pn_tt_norm_constants output_norm; + pn_qwen35_tt_strips card; + + /* Host scratch the resident forward reads projections into. Allocated once. */ + float *h_qkv; /**< conv_dim */ + float *h_z; /**< ssm_d_inner */ + float *h_alpha; /**< ssm_dt_rank, rounded up to a tile */ + float *h_beta; /**< ssm_dt_rank, rounded up to a tile */ + float *h_gated; /**< ssm_d_inner */ + float *h_qg; /**< 2 * head_dim * n_head */ + float *h_k; /**< head_dim * n_head_kv */ + float *h_v; /**< head_dim * n_head_kv */ + float *h_attn; /**< head_dim * n_head */ + float *h_embed; /**< n_embd */ + float *h_logits; /**< n_vocab */ + + /* The ON-CARD Gated-Delta-Net arm's host scratch. Allocated only when it arms, so a run + * without `PN_TT_ONCARD=...,gdn` pays nothing for it. `gdn_qn`/`gdn_kn` are per K-HEAD + * and NOT GQA-repeated — the repeat is the card reader's modulo. */ + bool gdn_on_card; /**< the two card phases replace the host recurrence */ + float *gdn_conv_out; /**< conv_dim, PHASE CONV's answer */ + float *gdn_qn; /**< ssm_n_group * ssm_state, L2-normalised */ + float *gdn_kn; /**< likewise */ + float *gdn_y; /**< ssm_dt_rank * ssm_state, PHASE DELTA's answer */ + float *gdn_decay; /**< ssm_dt_rank, already through softplus and exp */ + float *gdn_beta; /**< ssm_dt_rank, already through the sigmoid */ + + /* THE STRIP BRIDGE — the whole Gated-Delta-Net block on the card, with no activation + * crossing PCIe. `PN_TT_GDN_STRIP`, and it needs `gdn_on_card`, both render residencies + * and the Wt = ssm_state/32 head-norm slot; any of those missing leaves the arm off and + * the host-mediated recurrence above running unchanged. + * + * What still crosses per layer is `alpha` and `beta` DOWN (two tiles, 256 B) and the gate + * block UP (256 B), against 49,408 B. Readback 1478.09 -> 326.09 KiB/token. */ + bool gdn_strip; + pn_tt_norm_constants gdn_l2; /**< the q/k L2 norm's constants, one for the model */ + pn_tt_norm_constants *gdn_rms; /**< the gated RMS norm's, ONE PER LAYER: gamma is + * `ssm_norm` and that is a per-layer weight */ + bool *gdn_rms_ready; + pn_tt_fp32_strip gdn_y_norm; /**< RMSNorm(y), fp32, before the narrow render */ + uint32_t gdn_strip_checks; /**< PN_TT_GDN_STRIP_CHECK layers scored so far */ + float *gdn_probe; /**< the in-place proof's own scratch; NULL unless asked */ + pn_tt_activation gdn_y_bridge; /**< the same values as a bf16 strip, for the gate */ + /** THE CHUNK's gate scratch: `32 * ssm_dt_rank` each, ROW-MAJOR BY POSITION, because + * `beta = sigmoid(.)` and `decay = exp(ssm_a * softplus(.))` need a `pow` and so a `log` + * and the SFPU has neither — so they are host arithmetic for 32 positions at once now + * rather than for one. NULL unless the chunkwise family armed. */ + float *gdn_chunk_decay; + /** PM 7: the streamed band loop sets this — a chunk's output gate (its last op) is left + * pending and the NEXT chunk settles it before its narrow rewrites the gate's operand. */ + bool gdn_chunk_defer_gate; + /** PM 7 ITEM 5: this layer's gates were computed ON THE CARD (the GDN GATE family): the + * band loop reads nothing back, the chunk skips the host gate math, and each DELTA + * descriptor points at its card-built block of band `gdn_card_gate_band`. */ + bool gdn_card_gate; + uint32_t gdn_card_gate_band; + /** PM 7 ITEMS 5+6: the gates are ft_det_math everywhere (set when gdngate arms): the hot + * paths compute them on the card (no alpha/beta read back), every other TT path computes + * the SAME integer function on the host — so prefill, verify and plain decode agree. */ + bool gdn_det_gates; + /** PM 7 ITEM 7 step 1: the streamed band loop's PIPELINE — band b's gated RMS norm, narrow + * and output gate run (deferred) under band b+1's CONV / L2 / DELTA instead of before them. + * Same ops, same operands, same order per buffer: bit-exact. `gdn_pipe_stage` 1 = the + * previous band's RMS is in flight; its z / gated views are kept for the narrow + gate. */ + bool gdn_pipe; + int gdn_pipe_stage; + int gdn_pipe_layer; + pn_tt_activation gdn_pipe_z, gdn_pipe_gated; + float *gdn_chunk_beta; + /** THE PHASE-ISOLATION ARM's row shuttle: `32 * conv_dim` and `32 * v_heads * state` + * floats. NULL unless `PN_TT_PREFILL_GDNC_PHASE` names something other than `both`, so + * neither a shipped process nor the measured chunkwise arm pays anything for it. It + * exists to say WHICH of the two chunk kernels owns a divergence — + * `qwen35moe_gdn_strip_tt.c`. */ + float *gdn_split_stream; + float *gdn_split_y; + + /* The ON-CARD GATE: a shadow host recurrence run beside the card's, on the same inputs + * and from the same zero initial condition. NULL unless `PN_TT_GDN_PARITY` is set, + * because it costs a second full recurrence a layer — the very thing the on-card arm + * exists to remove. `qwen35moe_gdn_parity_tt.c`. */ + struct pn_qwen35_gdn_parity *gdn_parity; + + /* The PER-HEAD NORM PROBE: the two norms this block still runs on the host, run on the + * card BESIDE them and scored per head. NULL unless `PN_TT_GDN_NORMCHECK` is set. + * `qwen35moe_gdn_normcheck_tt.c`. */ + struct pn_qwen35_gdn_normcheck *gdn_normcheck; + + /* ON-CARD ATTENTION — `qwen35moe_attn_tt.c`. OPT-IN, and `PN_TT_ONCARD=all` does NOT + * select it: this arm moves the KV cache into card DRAM at bf16 and changes the order + * the attention sum accumulates in, so it is a different forward rather than a faster + * one. `attn_compare` runs the host arm beside it and diffs PER HEAD. */ + bool attn_on_card; + bool attn_compare; + pn_qwen35_tt_attn_strips attn_card; + /* PHASE G option 3: the PREFILL-ONLY tall set (`PN_TT_PREFILL_TILE_ROWS`). Swapped with + * `card` / `attn_card` for the batched passes and back before the tail forward, so decode + * never sees a tall strip. `tall_rows` 0 = prefill shares the decode strips. */ + pn_qwen35_tt_strips card_tall; + pn_qwen35_tt_attn_strips attn_card_tall; + uint32_t tall_rows; + bool tall_active; + /** Positions the last `pn_qwen35moe_prefill_tt()` call ran through batched passes; -1 + * when it declined or a pass aborted (the prompt then ran per token). */ + int prefill_last_batched; + /* STEP 4 S2 accounting */ + uint64_t spec_cycles, spec_drafted, spec_accepted; + double spec_pass_ms, spec_head_ms, spec_rollback_ms, spec_total_ms; + double spec_ssm_rows_ms, spec_attn_rows_ms; /**< the per-row mixer loops inside the pass */ + /** The gdn rows by section (host wall, each section ends on an await): alpha/beta pulls, + * widen + CONV rows, the q/k L2 norm, gates + DELTA rows, gated norm + gate + flush. */ + double spec_gdn_part_ms[5]; + double spec_gdn_mark_ms; /**< where the running section of spec_gdn_part_ms began */ + /* The verify pass's wall attributed to card clocks (pn_tt_device_card_clocks, by name). */ + const char *spec_clock_names[PN_TT_CARD_CLOCKS_MAX]; + double spec_clock_seconds[PN_TT_CARD_CLOCKS_MAX]; + unsigned spec_clock_count; + bool spec_reported; + uint64_t idle_clock_from; /**< tokens when the idle-clock window opened (0 = not yet) */ + /* STEP 4 S2b: the MTP draft head (`PN_TT_MTP=`, tools/build_mtp_gguf.py). */ + struct { + bool on; + /** The MTP artifact this run will arm, or "" (none): `PN_TT_MTP`, else the trunk + * artifact's sibling `.mtp.fttw` when that file exists + * (pn_qwen35_tt_mtp_resolve, before the attention arm reserves its KV region). */ + char path[1024]; + pn_qwen35_tt_layer layer; /**< the nextn block: attention + dense FFN */ + pn_tt_weight eh_e, eh_h; /**< eh_proj's embedding half and hidden half */ + pn_tt_norm_constants enorm, hnorm, head_norm; + pn_tt_activation h, e; /**< decode strips: the hidden it chains on; the embedding */ + int attn_index; /**< its KV region, after the trunk's */ + /* The catch-up owed before the next draft: positions pend_base.., one a row, the + * hidden of row i from the verify set (`pend_tall`) or `h` (one row), followed by + * token pend_tok[i] (the last one is the next seed, supplied by the draft call). */ + int pend_base, pend_n; + bool pend_tall; + uint32_t pend_tok[16]; + uint64_t steps, heads; + double ms; + /** The last draft's confidence: its softmax over the head's top-k values (1 on the + * dense path). Read by the adaptive draft count (`PN_TT_SPEC_ADAPT`). */ + float last_conf; + uint64_t adapt_stops; /**< cycles the adaptive count ended early */ + /* S2c: the DRAFT VOCABULARY (`PN_TT_MTP_VOCAB_TILES=N`): the LM head's first N tile + * columns (ids < 32N, BPE rank ~ frequency) + the special-token tail, staged as its own + * weight; its logits strip is pre-filled -inf so top-k never picks an unwritten tile. + * Drafts only — the verify still uses the whole head. */ + bool sub; + pn_tt_weight head_sub; + pn_tt_activation logits_sub; + uint32_t sub_tiles, sub_tail_first; + } mtp; + float *h_attn_card; /**< the card answer, for the crossed control only */ + + /* THE BATCHED PREFILL's host scratch — one [32 x width] buffer per strip it has to row + * select through. NULL until a batched prefill runs, so a decode-only process pays + * nothing for it. `qwen35moe_prefill_tt.c` owns the definition. */ + struct pn_qwen35_tt_batch *batch; + + uint32_t topk_index[PN_QWEN35_TT_TOPK_PAIRS]; + float topk_value[PN_QWEN35_TT_TOPK_PAIRS]; + uint32_t topk_count; + /** Speculative sampling: every verify row's top-N, kept by pn_qwen35_tt_head_rows(). */ + uint32_t spec_topk_index[16][PN_QWEN35_TT_TOPK_PAIRS]; + float spec_topk_value[16][PN_QWEN35_TT_TOPK_PAIRS]; + uint32_t spec_topk_count[16]; + + /* The evidence that the card was used. Byte-identical output is NOT evidence — the card + * and a silent CPU fallback have returned the same md5 in this project — so the split is + * counted and reported, and an acceptance run reads `on_host == 0`. */ + uint64_t mv_on_card; + uint64_t mv_on_host; /**< a weight the table does not hold; should be 0 */ + uint64_t mv_failed; /**< the card was asked and refused; should be 0 */ + int failure_reported; /**< the first failure prints; the rest only count */ + + /* The wall-clock reconciliation. The four device counters time the SEAMS — a tilize and + * a link write, a dispatch, a link read and a de-tilize, an on-card descriptor — and + * nothing else. Everything the host computes BETWEEN two seams is invisible to all four, + * so a phase sum quoted as a token understates it by however much host arithmetic the + * rung still owns. These accumulate the named host regions, so whatever is left over is a + * real unknown rather than a region nobody thought to time. + * + * All seconds, cumulative over `tokens`. `forward_wall_seconds` brackets the whole + * forward, so `forward_wall - (upload+dispatch+readback+oncard) - the four below` is the + * unattributed slice. Sampling and de-tokenisation are OUTSIDE the forward and therefore + * outside `forward_wall_seconds` too — they are read by subtracting it from the CLI's + * own generate-loop wall. */ + double forward_wall_seconds; /**< entry to exit of pn_qwen35moe_forward_tt */ + double host_recurrence_seconds; /**< pn_qwen35moe_ssm_recurrence, 24 layers a token */ + double host_attention_seconds; /**< pn_qwen35moe_attn_core, 8 layers a token */ + double host_embed_seconds; /**< the embedding-row gather and dequant */ + double host_logit_seconds; /**< the sparse -INFINITY fill and the logit-row copy */ + + int report_every; + uint64_t tokens; + + /* ------------------------------------------------------------------ + * DID ANY CALLER ACTUALLY ASK FOR THE BATCHED PREFILL? + * + * The chunkwise prefill shipped ON by default on 2026-08-25 at ~24x, and on 2026-08-26 it + * was found to reach exactly ONE of the three entry points that prefill a prompt: the + * chat CLI (`pn_stream_tokens`) and the serve (`sg_prompt_chunk`) both had per-token + * loops that never called `pn_qwen35moe_prefill_tt()` at all. Nothing refused, nothing + * errored, nothing printed — a 1,630-token prompt simply took 295 s and 1,632 forwards, + * and two sessions read that as the rung having regressed. + * + * A COUNTER IN THE MODEL CATCHES IT WHATEVER THE CALLER IS. `prefill_passes` is bumped by + * the batched path; `sequential_prefill_run` counts consecutive single-position forwards + * from position 0. When the run crosses `PN_QWEN35_TT_SEQUENTIAL_PREFILL_ALARM` with no + * batched pass ever having run, the forward says so ONCE. A wall figure has no + * denominator; this does. + * ------------------------------------------------------------------ */ + uint64_t prefill_passes; /**< batched passes this process has run */ + uint64_t sequential_prefill_run; /**< consecutive one-position forwards */ + bool sequential_prefill_said; /**< the alarm prints once, then only counts */ + + /* ------------------------------------------------------------------ + * MULTI-TURN: the batched prefill's checkpoint (qwen35moe_prefill_checkpoint_tt.c). + * The carried GDN state at the last full PASS boundary of the last prefill, copied on the + * card, with the token ids it covers. The attention K/V rows below `pos` are the card's + * own and stay valid while the same sequence keeps running at or above `pos`. + * ------------------------------------------------------------------ */ + struct pn_qwen35_tt_checkpoint { + bool valid; + bool restored; /**< restored for `owner` at `pos`, the prefill not yet run */ + int pos; /**< positions [0, pos), a multiple of the pass rows */ + const pn_state *owner; + uint32_t *tokens; /**< tokens[0, pos) */ + int capacity; + /* A capture under way in this prefill: every SSM layer's state is copied when its + * band loop reaches `pending_at`; the checkpoint is valid once all of them were. */ + int pending_at; /**< 0 = no capture in this prefill */ + int pending_layers; + double capture_ms; + unsigned long long verify_words; + long long verify_differ; /**< PN_TT_CKPT_VERIFY tally; -1 once a read failed */ + /* The next prefill of `ceiling_owner` checkpoints at or below `ceiling` (the end of + * the history its next turn resends; pn_qwen35moe_tt_checkpoint_ceiling). Used once. */ + int ceiling; + const pn_state *ceiling_owner; + } ckpt; + /** The pn_state whose sequence the card's K/V and carried state hold now (the last one a + * forward, a verify or a prefill ran for). */ + const pn_state *card_owner; +} pn_qwen35_tt; + +/* ============================================================ + * MULTI-TURN CHECKPOINT — qwen35moe_prefill_checkpoint_tt.c + * ============================================================ */ + +/** `PN_TT_PREFILL_CKPT=0` turns capture and resume off (the control). Default on. */ +bool pn_qwen35_tt_ckpt_enabled(void); + +/** Every card forward, verify and prefill: a forward for another sequence, or below the + * checkpoint, invalidates it (the K/V below it is about to be rewritten). */ +void pn_qwen35_tt_ckpt_note(pn_qwen35_tt *g, const pn_state *state, int pos); +/** Forget the card's sequence (a warm-up ran): no state owns it, no checkpoint is valid. */ +void pn_qwen35_tt_ckpt_forget(pn_qwen35_tt *g); + +/** May a batched prefill start at `pos` != 0 for `state`? Only right after a resume. */ +bool pn_qwen35_tt_ckpt_restored_at(const pn_qwen35_tt *g, const pn_state *state, int pos); + +/** + * Before a prefill's passes: the batched rows run from `base` (0, or the restored checkpoint) + * to `batch_end`; the capture position is the last 32-row band boundary in that range, and at + * or below the ceiling the serve set for this state (qwen35moe_checkpoint_band.h). Each + * SSM layer's state is copied there by pn_qwen35_tt_ckpt_band_done(). A capture overwrites the + * previous checkpoint, so that one is dropped first. + */ +void pn_qwen35_tt_ckpt_plan(pn_qwen35_tt *g, const pn_state *state, int base, int batch_end); + +/** The chunk band loop finished a band of SSM layer `ssm_index` ending at absolute position + * `end`: at the capture position, copy that layer's state. */ +void pn_qwen35_tt_ckpt_band_done(pn_qwen35_tt *g, int ssm_index, int end); + +/** After the passes: valid when every layer was captured; `tokens` are this prefill's, from + * `base`. */ +void pn_qwen35_tt_ckpt_commit(pn_qwen35_tt *g, const pn_state *state, const uint32_t *tokens, + int base, int n_ssm); + +/** Free the host side. */ +void pn_qwen35_tt_ckpt_release(pn_qwen35_tt *g); + +/* ============================================================ + * PREFILL alpha/beta PREFETCH — qwen35moe_prefill_prefetch_tt.c + * ============================================================ */ + +/** `PN_TT_PREFILL_AB_PREFETCH=0` turns it off. Default on. */ +bool pn_qwen35_tt_ab_prefetch_enabled(void); +/** Before band b's chunk mixers: read band `band` (= b+1) of `ssm_alpha` / `ssm_beta` (`rows` + * rows) into the host rows given, inside the CONV and DELTA windows. */ +void pn_qwen35_tt_ab_prefetch_arm(pn_qwen35_tt *g, int ssm_index, int band, int rows, + float *alpha, uint32_t alpha_width, float *beta, + uint32_t beta_width); +/** After band b's mixers: clear a window hook that never ran. */ +void pn_qwen35_tt_ab_prefetch_settle(pn_qwen35_tt *g); +/** Were band `band`'s alpha (`beta` false) or beta rows of this layer already read? */ +bool pn_qwen35_tt_ab_prefetched(const pn_qwen35_tt *g, int ssm_index, int band, bool beta); +/** Forget any pending read (a new pass). */ +void pn_qwen35_tt_ab_prefetch_reset(void); +/** One line under PN_TT_PREFILL_REPORT. */ +void pn_qwen35_tt_ab_prefetch_report(void); + +/* PHASE W — the alpha/beta reads on the READER THREAD (pn_tt_reader.h): every band of a layer + * posted once its projections land, taken band by band. `post` returns false when the reader + * is not available (the prefetch above then runs); `take` returns -1 when a band's read + * failed (the caller reads it inline); `end` drains — call it on EVERY exit. */ +bool pn_qwen35_tt_ab_reader_post(pn_qwen35_tt *g, int rows, float *alpha, uint32_t alpha_width, + float *beta, uint32_t beta_width); +int pn_qwen35_tt_ab_reader_take(pn_qwen35_tt *g, int band); +void pn_qwen35_tt_ab_reader_end(pn_qwen35_tt *g); +/* PHASE W wavefront: the NEXT SSM layer's bands [first, first + count), gated on the card + * having executed its alpha/beta projections; `promote` makes them the current layer's once + * every band is posted; `abandon` waits out everything (abort / end of pass). */ +struct pn_tt_reader_gate; +bool pn_qwen35_tt_ab_reader_post_next(pn_qwen35_tt *g, int ssm_index, int first, int count, + int rows, float *alpha, uint32_t alpha_width, float *beta, + uint32_t beta_width, const struct pn_tt_reader_gate *gate); +bool pn_qwen35_tt_ab_reader_promote(int ssm_index); +void pn_qwen35_tt_ab_reader_abandon(pn_qwen35_tt *g); +/* PM 6: the speculative VERIFY's alpha/beta through the reader (gated on member 0, the stream + * kicked) instead of a stream boundary; `wait` before the host math (`PN_TT_SPEC_AB_READER=0` + * off). post false = read them the old way. */ +bool pn_qwen35_tt_ab_verify_post(pn_qwen35_tt *g, int rows, float *alpha, uint32_t alpha_width, + float *beta, uint32_t beta_width); +int pn_qwen35_tt_ab_verify_wait(pn_qwen35_tt *g); + +/* PHASE W2 (qwen35moe_prefill_overlap_tt.c): an SSM layer's FFN on the grid, a super-band at a + * time, under the GDN chain. */ +/** The next SSM layer a super-band job also prepares (`PN_TT_PREFILL_WAVEFRONT=1`): its weights, + * index, and the host rows its alpha/beta reads land in. `d` NULL = none. */ +typedef struct pn_qwen35_tt_overlap_next { + const pn_qwen35_tt_layer *d; + int ssm_index; + int rows; + float *alpha, *beta; + uint32_t alpha_width, beta_width; +} pn_qwen35_tt_overlap_next; + +uint32_t pn_qwen35_tt_overlap_bands(void); +bool pn_qwen35_tt_wavefront_enabled(void); +bool pn_qwen35_tt_overlap_layer(pn_qwen35_tt *g, int ssm_index, uint32_t bands); +int pn_qwen35_tt_overlap_superband(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, uint32_t band, + uint32_t span, const pn_qwen35_tt_overlap_next *next); +/** Has the wavefront already run SSM layer `ssm_index`'s attn-norm, in-proj and read posts for + * all `bands`? `forget` drops it (a new pass, an abort). */ +bool pn_qwen35_tt_overlap_prepared(int ssm_index, uint32_t bands); + +/** PM 7 ITEM 5: the prepared layer's gate blocks were issued on the card (all `bands`). */ +bool pn_qwen35_tt_overlap_gates_prepared(int ssm_index, uint32_t bands); + +/** PM 7 ITEM 5: the wavefront prepares the next layer's gates on the card (not reads). */ +void pn_qwen35_tt_overlap_set_card_gates(bool on); +void pn_qwen35_tt_overlap_forget(void); +int pn_qwen35_tt_overlap_advance(pn_qwen35_tt *g); +int pn_qwen35_tt_overlap_drain(pn_qwen35_tt *g); +double pn_qwen35_tt_overlap_take_seconds(void); +void pn_qwen35_tt_overlap_report(void); + +/** Consecutive single-position forwards that mean "this is a prefill, and it is not batched". + * 64 is well past any chat turn's generation and far short of a real prompt. */ +#define PN_QWEN35_TT_SEQUENTIAL_PREFILL_ALARM 64u + +/** + * Bring up the resident-activation arm: the on-card op residencies, the strips and the norm + * constants. + * + * Never fatal. On any refusal it leaves `g->resident` false and the caller runs the + * projection-only arm, which is slower and not wrong. Implemented in + * `qwen35moe_resident_tt.c`. + * + * @return 0 whether or not the arm came up; -1 only on a bad argument + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_resident_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg); + +/** Release whatever `pn_qwen35_tt_resident_arm` allocated on the host side. */ +void pn_qwen35_tt_resident_release(pn_qwen35_tt *g); + +/** + * One token through the resident-activation forward. + * + * The residual stream, both RMSNorms, the dense SwiGLU and the LM head stay in card DRAM; + * only the values the host still computes — the Gated-Delta-Net recurrence's operands and + * the attention block's — cross the link, and each crosses it exactly once. + * + * @return the logit row, owned by `g`, or NULL on failure + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +float *pn_qwen35_tt_resident_forward(pn_qwen35_tt *g, pn_qwen35moe_model *m, + uint32_t token, pn_qwen35moe_state *s, int pos, + struct fw_pool *pool); + +/* ============================================================ + * The ON-CARD Gated-Delta-Net arm — qwen35moe_gdn_tt.c + * + * OPT-IN, and it has never executed on silicon. It replaces `pn_qwen35moe_ssm_recurrence()` + * with the same six steps, two of them run as card descriptors over state that lives in card + * DRAM across tokens. Until the two per-head norms move onto a second narrow reduce residency + * it is SLOWER than the host recurrence, for the reason `pn_tt_gdn.h` states as a number. + * ============================================================ */ + +/** + * Arm the GDN residency, allocate every SSM layer's carried state, and TRANSPOSE each layer's + * conv weight into the tap-major planes the card reads. + * + * Reads `PN_TT_ONCARD` through `pn_tt_device_oncard_requested()`, so an unset variable leaves + * `g->gdn_on_card` false and the host recurrence in place. + * + * @return 0 whether or not it armed; -1 on a bad argument or a staging failure + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_gdn_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg); + +/** Release the host scratch `pn_qwen35_tt_gdn_arm` allocated. Safe on NULL and on an arm + * that never came up. */ +void pn_qwen35_tt_gdn_release(pn_qwen35_tt *g); + +/* ============================================================ + * ON-CARD ATTENTION — implemented in qwen35moe_attn_tt.c + * ------------------------------------------------------------ + * The op family (`src/accel/tt/pn_tt_attention.c`) already existed and is green on silicon + * for MiniCPM5-1B. What this file adds is Ornith's SHAPE: the de-interleaved Q/gate + * projections, the per-head QK-RMSNorms at head_dim/32 tile columns, and the SIGMOID output + * gate. See the file note for why each landed where it did. + * ============================================================ */ + +/** + * Bring up on-card attention if `PN_TT_ONCARD` names it. + * + * Stages the two de-interleaved halves of every attention layer's joint Q+gate projection, + * uploads the QK-norm constants, arms the per-head reduce residency at the ATTENTION head + * width and the RoPE/memory/attention triple, and allocates the extra strips. + * + * A REFUSAL, not a fallback: attention was asked for by name, so a missing program, an + * unexpressible rotary width or a residency that will not arm returns -1 rather than leaving + * the block on the host with different numerics and nothing saying so. + * + * @return 0 (armed, or never asked for), -1 on a refusal + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_attn_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg); + +/** Did the on-card attention arm come up for this run? */ +bool pn_qwen35_tt_attn_is_on_card(const pn_qwen35_tt *g); + +/** + * One attention layer entirely on the card, leaving the block output in `g->card.project`. + * + * NOTHING crosses the link: q, k, v, the gate and the answer all stay in DRAM strips, and + * the KV cache is the card's. + * + * @param attn_index the ATTENTION-layer ordinal (`pn_qwen35moe_model_layer_index()`), which + * is what indexes the card's per-layer K/V regions + * @param seq_len LIVE cached positions, i.e. `min(pos + 1, max_seq)` + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_attn_block_card(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int pos, + int seq_len); + +/** + * THE TWO HALVES OF THAT BLOCK, SPLIT AT THE POSITION SEAM. + * + * `..._project_card` is the part a batched prefill runs ONCE for up to 32 rows: four + * projections over `g->card.normed`, per-row by construction. `..._core_card` is the part + * that cannot be batched — the QK-norms, the rotary, the K/V append and flash decode each + * take ONE position — so it runs once per row over a strip whose logical row 0 the caller + * has just rewritten. The output projection stays with the caller, because the batched arm + * defers it until all the rows' gates are back in one strip. + * + * `pn_qwen35_tt_attn_block_card()` is exactly these two plus that projection, so the shipped + * decode path is byte-for-byte the same sequence it was. + * + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_attn_project_card(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d); + +/** @copydoc pn_qwen35_tt_attn_project_card */ +int pn_qwen35_tt_attn_core_card(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int pos, + int seq_len); + +/** + * The same block over a CHUNK of `rows` consecutive positions, in rows 0..rows-1 of the + * strips the projections already wrote. + * + * EVERY STEP EXCEPT ATTENTION ALREADY BATCHED AND NONE OF THEM CHANGES HERE. The two + * per-head QK-RMSNorms are `row_blocks = heads` over a `[1, heads*S]` strip and a row reduce + * reduces ALONG the row, so row t of row block h is position t of head h; the sigmoid gate + * is per element. RoPE, the KV append and attention take the chunk forms, and that is the + * whole difference — which is why this is a sibling of `_core_card` rather than a mode + * inside it: the shipped decode sequence stays byte for byte what it was. + * + * `base_pos` MUST be a multiple of 32 and `rows` 1..32. + * + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_attn_core_chunk(pn_qwen35_tt *g, const pn_qwen35_tt_layer *d, + const pn_qwen35moe_config *cfg, int attn_index, int base_pos, + int rows); + +/** Free the batched prefill's host scratch. Safe on NULL and on a run that never batched. + * The entry point itself is declared in `qwen35moe.h`, because the engine calls it. */ +void pn_qwen35_tt_prefill_release(pn_qwen35_tt *g); +/** PHASE G option 3: allocate the prefill-only tall strip set (`PN_TT_PREFILL_TILE_ROWS`), + * after the decode strips and the attention strips exist. 0 also when the knob is off. */ +int pn_qwen35_tt_prefill_tall_alloc(pn_qwen35_tt *g); +/** STEP 4 S2 — the speculative verify (qwen35moe_prefill_tt.c). */ +int pn_qwen35_tt_spec_verify(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *st, + const uint32_t *tokens, int rows, int base_pos, + uint32_t *argmax_out); +int pn_qwen35_tt_spec_rollback(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int last_kept); +/** STEP 4 S2b — the MTP draft head (qwen35moe_resident_tt.c). arm: place the second + * artifact's weights (after the attention arm, which reserved its KV region). step: one MTP + * position — hidden row `h_row` of `h_src` (NULL: the chained `mtp.h`) and the embedding of + * `token` through eh_proj, the nextn block at `pos`, and with `want_head` its head's argmax. + * prefill_layer (qwen35moe_prefill_tt.c, inside a batched pass): the same over a pass's rows, + * writing the MTP KV for the prompt. */ +void pn_qwen35_tt_mtp_resolve(pn_qwen35_tt *g, const char *trunk_artifact); +int pn_qwen35_tt_mtp_arm(pn_qwen35_tt *g, pn_qwen35moe_model *m, const pn_qwen35moe_config *cfg); +int pn_qwen35_tt_mtp_step(pn_qwen35_tt *g, pn_qwen35moe_model *m, const pn_qwen35moe_config *cfg, + pn_qwen35moe_state *st, uint32_t token, const pn_tt_activation *h_src, + uint32_t h_row, int pos, bool want_head, uint32_t *argmax_out); +int pn_qwen35_tt_mtp_head_row(pn_qwen35_tt *g, pn_qwen35moe_state *st, + const pn_tt_activation *src, uint32_t row, uint32_t *argmax_out); +/** S2f: the MTP catch-up over `rows` committed verify rows in ONE pass (qwen35moe_prefill_tt.c): + * the verify set's hiddens + the embeddings of `next[]`, at base_pos.., then the draft head on + * the last row. @return 0, or -1 */ +int pn_qwen35_tt_mtp_catchup_rows(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, pn_qwen35moe_state *st, + const uint32_t *next, int rows, int base_pos, + uint32_t *argmax_out); +int pn_qwen35_tt_attn_split_q(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, const char *name, + bool extra, pn_qwen35_tt_layer *slot); +/** The decode head (final norm, LM head, top-k into st->logits) on the decode hidden's row 0, + * under the single-row promise — exactly what a single-position forward runs. */ +int pn_qwen35_tt_head_row(pn_qwen35_tt *g, pn_qwen35moe_state *st); +/** S2c: rows 0..rows-1 of `hidden` through the head together; each row's greedy token. */ +int pn_qwen35_tt_head_rows(pn_qwen35_tt *g, pn_qwen35moe_state *st, + const pn_tt_activation *hidden, int rows, uint32_t *argmax_out); + +/** + * THE CROSSED CONTROL: diff the card's attention answer against the host arm's, PER HEAD. + * + * Off unless `PN_TT_ATTN_COMPARE` is set, because it costs the readback the block exists to + * remove. It reports max|delta|, WHICH head and lane, and how many heads are past 1e-3 + * relative — not a PCC, because every failure this path can have is a permutation or a + * localised wrong element and PCC is blind to the first and saturates on the second. + */ +void pn_qwen35_tt_attn_compare(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int layer, + const float *host_attn); + +/** Free what `pn_qwen35_tt_attn_arm` allocated. Safe on NULL and on an arm that never came + * up. The card strips and the KV regions belong to the device and go with it. */ +void pn_qwen35_tt_attn_release(pn_qwen35_tt *g); + +/** One projection, ISSUED (or issued and awaited when `wait`). Shared with + * `qwen35moe_attn_tt.c` so both blocks count the same way — the on-card/on-host split is + * the evidence that the card ran the forward, and two counters would be two answers. */ +int pn_qwen35_tt_project(pn_qwen35_tt *g, const pn_tt_weight *w, const pn_tt_activation *in, + const pn_tt_activation *out, bool wait); + +/** + * One SSM layer's recurrence, with `conv1d + SiLU` and the delta rule on the card. + * + * Argument for argument the same as `pn_qwen35moe_ssm_recurrence()` minus the state, the + * arena and the thread pool — the state is the card's, and there is no per-head host loop + * left to spread over threads. + * + * @param ssm_index the SSM-layer ordinal, i.e. `pn_qwen35moe_model_layer_index()` + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_gdn_recurrence(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const float *qkv, const float *z, const float *beta_raw, + const float *alpha, float *gated); + +/** + * Arm THE STRIP BRIDGE — the same block with no activation crossing PCIe. + * + * Needs the GDN family armed, both `render_*` residencies and a Wt = `ssm_state/32` head-norm + * slot. Any of them missing leaves `g->gdn_strip` false and the host-mediated recurrence + * above running; it is never fatal, because a slower correct forward is the right failure. + * + * @return 0 whether or not the arm came up; -1 only on a bad argument or an allocation + */ +int pn_qwen35_tt_gdn_strip_arm(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg); + +/** Free what `pn_qwen35_tt_gdn_strip_arm` allocated. */ +void pn_qwen35_tt_gdn_strip_release(pn_qwen35_tt *g); + +/** + * ONE SSM LAYER, END TO END ON THE CARD. + * + * `qkv`, `z` and `gated` are CARD STRIPS, not host arrays — that is the whole difference and + * the whole of the 49,408 B a layer. `beta_raw` and `alpha` are still host floats, because + * `beta = sigmoid(.)` and `decay = exp(ssm_a * softplus(.))` need a `pow` and so a `log`, and + * the SFPU has neither; they are two tiles down and 256 B up. + * + * @param ssm_index the SSM-layer ordinal, i.e. `pn_qwen35moe_model_layer_index()` + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_gdn_strip_rows(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, int rows, + const pn_tt_activation *qkv, const pn_tt_activation *z, + const float *beta_raw, size_t beta_stride, + const float *alpha, size_t alpha_stride, + const pn_tt_activation *gated); +int pn_qwen35_tt_gdn_strip_layer(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const pn_tt_activation *qkv, const pn_tt_activation *z, + const float *beta_raw, const float *alpha, + const pn_tt_activation *gated); + +/** + * THE SAME SSM LAYER OVER A CHUNK of `rows` consecutive positions, in rows 0..rows-1 of the + * strips the projections already wrote. + * + * EVERY STEP EXCEPT THE TWO RECURRENCES ALREADY BATCHED AND NONE OF THEM CHANGES HERE. The + * render is per tile, the two per-head norms are `row_blocks = heads` over a `[1, heads*S]` + * strip and a row reduce reduces ALONG the row, and the `silu(z) *` gate is per element — so + * row t of row block h is position t of head h and 32 positions cost one descriptor exactly as + * one position does. Only PHASE CONV and PHASE DELTA change, and they change to the CHUNKWISE + * family, which advances the same window and the same recurrent state. + * + * `rows` must be at least `pn_tt_gdnc_min_chunk_rows()` — a chunk shorter than the conv window + * cannot REPLACE it, only shift it, and both the host encoder and the device refuse. The + * caller routes a shorter tail through `pn_qwen35_tt_gdn_strip_layer()` position by position, + * which is the same arithmetic on the same bytes. + * + * `beta_raw` and `alpha` are `rows` ROWS of host floats at strides `beta_stride`/`alpha_stride` + * — the shape `pn_tt_activation_download_rows()` produces. + * + * @return 0, or -1 with the reason on stderr + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +/** PM 7 ITEM 5: the DELTA sub-chunk length the card gate blocks are cut at (the band loop's + * own split; 32 when the split is off). */ +uint32_t pn_qwen35_tt_gdn_gate_sub_rows(void); + +/** PM 7 ITEM 5: the shipped chunk arm (both mixers chunkwise, no isolation split) — the only + * one the card gate blocks serve. */ +bool pn_qwen35_tt_gdn_card_gate_possible(void); + +/** PM 7 ITEM 7 step 1: finish the pipeline's pending band (its RMS, narrow and gate), awaited. + * @return 0 or -1. Safe when nothing is pending. */ +int pn_qwen35_tt_gdn_pipe_drain(pn_qwen35_tt *g); + +int pn_qwen35_tt_gdn_strip_chunk(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const pn_tt_activation *qkv, const pn_tt_activation *z, + const float *beta_raw, uint32_t beta_stride, + const float *alpha, uint32_t alpha_stride, int rows, + const pn_tt_activation *gated); + +/** Is the chunkwise SSM path usable for a chunk of `rows` positions this run? False when the + * `gdnc` family did not arm, when the strip bridge is off, or when `rows` is below the conv + * window's floor — every one of which is a reason to run the positions one at a time. */ +bool pn_qwen35_tt_gdn_chunk_available(const pn_qwen35_tt *g, int rows); + +/* ============================================================ + * THE ON-CARD GATE — implemented in qwen35moe_gdn_parity_tt.c + * ------------------------------------------------------------ + * A shadow host recurrence run beside the card's, on the same inputs, from the same zero + * initial condition, never resynced. Two numbers a layer: `conv` isolates PHASE CONV and + * `gated` prices the whole block. The bar is 1e-3 MAX-NORM RELATIVE and it is the CUDA arm's + * — the oracle accumulates its dots in fp64 on purpose, so bit-equality was never available. + * ============================================================ */ + +/** Opaque; the parity file owns every byte of it. */ +struct pn_qwen35_gdn_parity; + +/** + * Arm the shadow recurrence if `PN_TT_GDN_PARITY` names it and the GDN arm actually came up. + * + * @return 0 whether or not it armed; -1 only on a bad argument or an allocation failure + * + * Thread-safe: no Reentrant: no Signal-safe: no + */ +int pn_qwen35_tt_gdn_parity_arm(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg); + +/** Free it. Safe on NULL and on a run that never armed. */ +void pn_qwen35_tt_gdn_parity_release(pn_qwen35_tt *g); + +/** + * Compare ONE SSM layer, after the card arm has written `g->gdn_conv_out` and `g->h_gated`. + * + * Advances the shadow state by exactly one token. A no-op returning 0 when parity is not + * armed, so the call site needs no guard. + * + * @param layer the SSM-layer ordinal, for the report only + * @param state the HOST arm's own per-layer state, which the on-card arm leaves untouched + * @return 0, or -1 if the shadow recurrence itself failed + */ +int pn_qwen35_tt_gdn_parity_layer(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int layer, + const float *qkv, const float *z, const float *beta_raw, + const float *alpha, pn_qwen35moe_ssm_state *state, + pn_arena *arena, struct fw_pool *pool); + +/** Close one token: fold its layers into the running worst and print one line. */ +void pn_qwen35_tt_gdn_parity_token(pn_qwen35_tt *g, int position); + +/** The verdict, once, at teardown. */ +void pn_qwen35_tt_gdn_parity_report(const pn_qwen35_tt *g); + +/* ================================================================ + * THE PER-HEAD NORM PROBE — implemented in qwen35moe_gdn_normcheck_tt.c + * ================================================================ + * The two norms of this block that are NOT on the card, run on the card anyway and scored + * against the oracle's own `pn_qwen35moe_l2_norm()` and `pn_qwen35moe_gated_norm()`. A + * probe, not a path: nothing downstream reads what it computes. + */ +struct pn_qwen35_gdn_normcheck; + +/** Arm the per-head reduce residency and the probe's strips. `PN_TT_GDN_NORMCHECK=1`, and + * a card that cannot arm it leaves `g->gdn_normcheck` NULL rather than failing the run. */ +int pn_qwen35_tt_gdn_normcheck_arm(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg); + +/** Score ONE SSM layer's two norms. `z` and `gated` are the block's own, after the host + * has written `gated` — so the comparison is against the value the forward actually used. */ +int pn_qwen35_tt_gdn_normcheck_layer(pn_qwen35_tt *g, const pn_qwen35moe_ssm_layer *l, + const pn_qwen35moe_config *cfg, int ssm_index, + const float *z, const float *gated); + +/** One line: the worst head of each norm, and how many elements passed 1e-3 relative. */ +void pn_qwen35_tt_gdn_normcheck_report(const pn_qwen35_tt *g); + +/** Free the probe's strips and constants. Safe on a run that never armed it. */ +void pn_qwen35_tt_gdn_normcheck_release(pn_qwen35_tt *g); + +#endif /* USE_TT */ + +#endif /* PN_QWEN35MOE_TT_INTERNAL_H */ diff --git a/src/models/rwkvx.c b/src/models/rwkvx.c new file mode 100644 index 0000000..3c93a12 --- /dev/null +++ b/src/models/rwkvx.c @@ -0,0 +1,2200 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * rwkvx.c — RWKV-X inference engine for Pantheon + * + * Copied from foundry/rwkvx/rwkvx.c with Metal, CUDA, HybridMem, Training, + * Batch, Instrumentation, and OpenMP support stripped. + * Wrapped with Pantheon's pn_model/pn_state adapter interface. + */ + +#include "rwkvx.h" +#include "tensor/tensor.h" +#include "safetensors/safetensors.h" + +#include +#include +#include +#include +#include + +#ifdef __ARM_NEON + #include +#endif + +#ifdef USE_THREADS + #include "threadpool/threadpool.h" +#endif + +#ifdef USE_BLAS + #if defined(__APPLE__) + #include + #else + #include + #endif +#endif + +#ifdef USE_CUDA + #include "cuda/fc_cuda.h" +#endif + +#ifdef USE_THREADS +/* File-scope pool pointer — set during forward, used by internal + * parallel functions. Avoids changing all static function signatures. */ +static _Thread_local fw_pool *__rx_pool = NULL; +#endif + +/* Aligned malloc helper — 64-byte alignment for NEON/AVX cache line ops */ +static void* aligned_malloc(size_t size) { + void *ptr = NULL; + if (posix_memalign(&ptr, 64, size) != 0) return NULL; + return ptr; +} + +/* ================================================================ + * Internal: INT8 quantized weight matrix (per-row symmetric) + * ================================================================ */ + +typedef struct { + int8_t *data; /* [rows * cols] quantized data */ + float *scales; /* [rows] per-row scale: float = int8 * scale */ + int rows, cols; +#ifdef USE_CUDA + fc_buffer *cuda_data; /* Persistent GPU copy of int8 weights */ + fc_buffer *cuda_scales; /* Persistent GPU copy of per-row scales */ +#endif +} rx_q8_weight; + +static rx_q8_weight* rx_q8_from_f32(const float *src, int rows, int cols) { + rx_q8_weight *q = (rx_q8_weight*)calloc(1, sizeof(rx_q8_weight)); + q->rows = rows; + q->cols = cols; + q->data = (int8_t*)aligned_malloc(rows * cols * sizeof(int8_t)); + q->scales = (float*)aligned_malloc(rows * sizeof(float)); + + for (int i = 0; i < rows; i++) { + const float *row = &src[i * cols]; + float amax = 0.0f; + for (int j = 0; j < cols; j++) { + float a = row[j] < 0 ? -row[j] : row[j]; + if (a > amax) amax = a; + } + float scale = amax / 127.0f; + q->scales[i] = scale; + float inv_scale = (scale > 0.0f) ? 127.0f / amax : 0.0f; + int8_t *dst = &q->data[i * cols]; + for (int j = 0; j < cols; j++) { + float v = row[j] * inv_scale; + int vi = (int)(v + (v >= 0 ? 0.5f : -0.5f)); + if (vi > 127) vi = 127; + if (vi < -127) vi = -127; + dst[j] = (int8_t)vi; + } + } +#ifdef USE_CUDA + if (fc_available()) { + q->cuda_data = fc_buffer_from_ptr(q->data, rows * cols * sizeof(int8_t)); + q->cuda_scales = fc_buffer_from_ptr(q->scales, rows * sizeof(float)); + } else { + q->cuda_data = NULL; + q->cuda_scales = NULL; + } +#endif + return q; +} + +static void rx_q8_free(rx_q8_weight *q) { + if (!q) return; +#ifdef USE_CUDA + if (q->cuda_data) fc_buffer_free(q->cuda_data); + if (q->cuda_scales) fc_buffer_free(q->cuda_scales); +#endif + free(q->data); + free(q->scales); + free(q); +} + +/* Quantize a float vector to int8 in-place into a pre-allocated buffer. + * Returns the scale factor (amax / 127). */ +static float fast_quantize_input(const float *x, int8_t *x_q, int n) { + float x_amax = 0.0f; + for (int j = 0; j < n; j++) { + float a = x[j] < 0 ? -x[j] : x[j]; + if (a > x_amax) x_amax = a; + } + float x_scale = x_amax / 127.0f; + float x_inv_scale = (x_amax > 0.0f) ? 127.0f / x_amax : 0.0f; + for (int j = 0; j < n; j++) { + float v = x[j] * x_inv_scale; + int vi = (int)(v + (v >= 0 ? 0.5f : -0.5f)); + if (vi > 127) vi = 127; + if (vi < -127) vi = -127; + x_q[j] = (int8_t)vi; + } + return x_scale; +} + +#ifdef USE_THREADS +typedef struct { + const int8_t *x_q; + float x_scale; + const rx_q8_weight *q; + float *out; + int cols; +} q8_row_ctx; + +static void q8_row_worker(int start, int end, void *arg); +#endif + +/* Quantized matvec with pre-quantized input. */ +static void fast_matvec_q8_preq(const int8_t *x_q, float x_scale, + const rx_q8_weight *q, float *out, + int rows, int cols) { +#ifdef USE_CUDA + if (fc_available() && q->cuda_data && (int64_t)rows * cols >= FC_Q8_THRESHOLD) { + fc_buffer *bxq = fc_buffer_from_ptr(x_q, cols * sizeof(int8_t)); + fc_buffer *bout = fc_buffer_alloc(rows * sizeof(float)); + fc_matvec_q8(bxq, q->cuda_data, q->cuda_scales, bout, + x_scale, rows, cols); + fc_sync(); + fc_buffer_to_cpu(bout, out); + fc_buffer_free(bxq); + fc_buffer_free(bout); + return; + } +#endif + +#ifdef __ARM_NEON + int cols64 = cols & ~63; + int cols16 = cols & ~15; +#endif + +#ifdef USE_THREADS + if (__rx_pool && rows >= 512) { + q8_row_ctx ctx = { x_q, x_scale, q, out, cols }; + fw_for(__rx_pool, rows, q8_row_worker, &ctx); + return; + } +#endif + for (int i = 0; i < rows; i++) { + const int8_t *row = &q->data[i * cols]; + float w_scale = q->scales[i]; + + if (i + 1 < rows) + __builtin_prefetch(&q->data[(i + 1) * cols], 0, 1); + +#ifdef __ARM_NEON + int32x4_t acc0 = vdupq_n_s32(0); + int32x4_t acc1 = vdupq_n_s32(0); + int32x4_t acc2 = vdupq_n_s32(0); + int32x4_t acc3 = vdupq_n_s32(0); + for (int j = 0; j < cols64; j += 64) { + acc0 = vdotq_s32(acc0, vld1q_s8(&x_q[j]), vld1q_s8(&row[j])); + acc1 = vdotq_s32(acc1, vld1q_s8(&x_q[j + 16]), vld1q_s8(&row[j + 16])); + acc2 = vdotq_s32(acc2, vld1q_s8(&x_q[j + 32]), vld1q_s8(&row[j + 32])); + acc3 = vdotq_s32(acc3, vld1q_s8(&x_q[j + 48]), vld1q_s8(&row[j + 48])); + } + int32x4_t acc = vaddq_s32(vaddq_s32(acc0, acc1), vaddq_s32(acc2, acc3)); + for (int j = cols64; j < cols16; j += 16) + acc = vdotq_s32(acc, vld1q_s8(&x_q[j]), vld1q_s8(&row[j])); + int32_t dot = vaddvq_s32(acc); + for (int j = cols16; j < cols; j++) + dot += (int32_t)x_q[j] * (int32_t)row[j]; +#else + int32_t dot = 0; + for (int j = 0; j < cols; j++) + dot += (int32_t)x_q[j] * (int32_t)row[j]; +#endif + out[i] = (float)dot * w_scale * x_scale; + } +} + +#ifdef USE_THREADS +static void q8_row_worker(int start, int end, void *arg) { + q8_row_ctx *ctx = (q8_row_ctx *)arg; + const int8_t *x_q = ctx->x_q; + float x_scale = ctx->x_scale; + const rx_q8_weight *q = ctx->q; + float *out = ctx->out; + int cols = ctx->cols; + int rows = q->rows; +#ifdef __ARM_NEON + int cols64 = cols & ~63; + int cols16 = cols & ~15; +#endif + for (int i = start; i < end; i++) { + const int8_t *row = &q->data[i * cols]; + float w_scale = q->scales[i]; + if (i + 1 < rows) + __builtin_prefetch(&q->data[(i + 1) * cols], 0, 1); +#ifdef __ARM_NEON + int32x4_t acc0 = vdupq_n_s32(0); + int32x4_t acc1 = vdupq_n_s32(0); + int32x4_t acc2 = vdupq_n_s32(0); + int32x4_t acc3 = vdupq_n_s32(0); + for (int j = 0; j < cols64; j += 64) { + acc0 = vdotq_s32(acc0, vld1q_s8(&x_q[j]), vld1q_s8(&row[j])); + acc1 = vdotq_s32(acc1, vld1q_s8(&x_q[j + 16]), vld1q_s8(&row[j + 16])); + acc2 = vdotq_s32(acc2, vld1q_s8(&x_q[j + 32]), vld1q_s8(&row[j + 32])); + acc3 = vdotq_s32(acc3, vld1q_s8(&x_q[j + 48]), vld1q_s8(&row[j + 48])); + } + int32x4_t acc = vaddq_s32(vaddq_s32(acc0, acc1), vaddq_s32(acc2, acc3)); + for (int j = cols64; j < cols16; j += 16) + acc = vdotq_s32(acc, vld1q_s8(&x_q[j]), vld1q_s8(&row[j])); + int32_t dot = vaddvq_s32(acc); + for (int j = cols16; j < cols; j++) + dot += (int32_t)x_q[j] * (int32_t)row[j]; +#else + int32_t dot = 0; + for (int j = 0; j < cols; j++) + dot += (int32_t)x_q[j] * (int32_t)row[j]; +#endif + out[i] = (float)dot * w_scale * x_scale; + } +} +#endif + +/* Quantized matvec: convenience wrapper that quantizes x on the fly. */ +static void fast_matvec_q8(const float *x, const rx_q8_weight *q, float *out, + int rows, int cols, int8_t *q8_buf) { + float x_scale = fast_quantize_input(x, q8_buf, cols); + fast_matvec_q8_preq(q8_buf, x_scale, q, out, rows, cols); +} + +/* ================================================================ + * Internal: RWKV-7 block weights + * ================================================================ */ + +typedef struct { + tm_tensor *ln1_w, *ln1_b, *ln2_w, *ln2_b; + tm_tensor *x_r, *x_w, *x_k, *x_v, *x_a, *x_g; + tm_tensor *w0, *w1, *w2; + tm_tensor *a0, *a1, *a2; + tm_tensor *v0, *v1, *v2; + tm_tensor *g1, *g2; + tm_tensor *k_k, *k_a, *r_k; + tm_tensor *receptance_w, *key_w, *value_w, *output_w; + tm_tensor *ln_x_w, *ln_x_b; + tm_tensor *ffn_x_k, *ffn_key_w, *ffn_value_w; + + rx_q8_weight *receptance_q8, *key_q8, *value_q8, *output_q8; + rx_q8_weight *ffn_key_q8, *ffn_value_q8; +} rx_rwkv_block; + +/* ================================================================ + * Internal: Sparse attention block weights + * ================================================================ */ + +typedef struct { + tm_tensor *ln1_w, *ln1_b, *ln2_w, *ln2_b; + tm_tensor *receptance_w, *key_w, *value_w, *output_w; + tm_tensor *ffn_x_k, *ffn_key_w, *ffn_value_w; + + rx_q8_weight *receptance_q8, *key_q8, *value_q8, *output_q8; + rx_q8_weight *ffn_key_q8, *ffn_value_q8; +} rx_attn_block; + +/* ================================================================ + * Internal: Model configuration + * ================================================================ */ + +typedef struct { + int n_embd; + int n_head; + int head_size; + int n_rwkv_layer; + int n_attn_layer; + int vocab_size; +} rx_config; + +/* ================================================================ + * Internal: Model structure + * ================================================================ */ + +typedef struct rx_model_internal rx_model_internal; + +struct rx_model_internal { + rx_config config; + + tm_tensor *emb_weight; + tm_tensor *ln_out_w; + tm_tensor *ln_out_b; + tm_tensor *head_weight; + rx_q8_weight *head_q8; + + rx_rwkv_block *rwkv_blocks; + rx_attn_block *attn_blocks; + + int *block_order; + int *block_index; + int n_blocks; + + bool quantized; + +#ifdef USE_THREADS + fw_pool *pool; +#endif + + /* Tokenizer (loaded separately) */ + void *tokenizer; /* rx_tokenizer* */ +}; + +/* ================================================================ + * Internal: State structure + * ================================================================ */ + +typedef struct { + tm_tensor *att_x_prev; + tm_tensor *att_kv; + tm_tensor *ffn_x_prev; +} rx_rwkv_state; + +typedef struct { + tm_tensor *k_cache; + tm_tensor *v_cache; + int cache_len; + int max_cache_len; + tm_tensor *ffn_x_prev; +} rx_attn_state; + +/* ================================================================ + * Internal: Scratch workspace + * ================================================================ */ + +#define RX_NUM_SCRATCH 16 + +typedef struct { + float **buf; + float *lora_buf; + float *ffn_buf; + float *x_buf[2]; + int8_t *q8_buf; + int c; + int max_lora_dim; + int ffn_dim; +} rx_workspace; + +typedef struct { + rx_rwkv_state *rwkv; + rx_attn_state *attn; + tm_tensor *v_first; + float *v_first_buf; + rx_config config; + rx_workspace *workspace; +} rx_state_internal; + +static rx_workspace* rx_workspace_new(int c, int max_lora_dim, int ffn_dim) { + rx_workspace *ws = (rx_workspace*)calloc(1, sizeof(rx_workspace)); + ws->c = c; + ws->max_lora_dim = max_lora_dim > 0 ? max_lora_dim : 256; + ws->ffn_dim = ffn_dim > 0 ? ffn_dim : c * 4; + ws->buf = (float**)malloc(RX_NUM_SCRATCH * sizeof(float*)); + for (int i = 0; i < RX_NUM_SCRATCH; i++) + ws->buf[i] = (float*)aligned_malloc(c * sizeof(float)); + ws->lora_buf = (float*)aligned_malloc(ws->max_lora_dim * sizeof(float)); + ws->ffn_buf = (float*)aligned_malloc(ws->ffn_dim * sizeof(float)); + ws->x_buf[0] = (float*)aligned_malloc(c * sizeof(float)); + ws->x_buf[1] = (float*)aligned_malloc(c * sizeof(float)); + int q8_size = ws->ffn_dim > c ? ws->ffn_dim : c; + ws->q8_buf = (int8_t*)aligned_malloc(q8_size * sizeof(int8_t)); + return ws; +} + +static void rx_workspace_free(rx_workspace *ws) { + if (!ws) return; + for (int i = 0; i < RX_NUM_SCRATCH; i++) + free(ws->buf[i]); + free(ws->buf); + free(ws->lora_buf); + free(ws->ffn_buf); + free(ws->x_buf[0]); + free(ws->x_buf[1]); + free(ws->q8_buf); + free(ws); +} + +/* ================================================================ + * Internal: Tokenizer trie + * ================================================================ */ + +#define RX_VOCAB_SIZE 65536 +#define RX_MAX_TOKEN_LEN 128 + +typedef struct rx_trie_node { + struct rx_trie_node *children[256]; + int32_t token_id; +} rx_trie_node; + +typedef struct { + rx_trie_node *root; + uint8_t *token_bytes[RX_VOCAB_SIZE]; + int token_lens[RX_VOCAB_SIZE]; +} rx_tokenizer; + +/* ================================================================ + * Zero-allocation helpers + * ================================================================ */ + +/* Matrix-vector: out[D] = x[C] @ w[C, D] */ +static void fast_matvec(const float *x, const float *w, float *out, + int rows, int cols) { +#ifdef USE_BLAS + cblas_sgemv(CblasRowMajor, CblasTrans, + rows, cols, + 1.0f, w, cols, x, 1, 0.0f, out, 1); +#else + memset(out, 0, cols * sizeof(float)); + for (int i = 0; i < rows; i++) { + float xi = x[i]; + const float *wi = &w[i * cols]; + for (int j = 0; j < cols; j++) + out[j] += xi * wi[j]; + } +#endif +} + +/* Layer norm: out = (x - mean) / sqrt(var + eps) * weight + bias */ +static void fast_layer_norm(const float *x, const float *w, const float *b, + float *out, int c) { +#ifdef __ARM_NEON + int c4 = c & ~3; + float32x4_t sum_v = vdupq_n_f32(0.0f); + for (int i = 0; i < c4; i += 4) + sum_v = vaddq_f32(sum_v, vld1q_f32(&x[i])); + float mean = vaddvq_f32(sum_v); + for (int i = c4; i < c; i++) mean += x[i]; + mean /= c; + + float32x4_t mean_v = vdupq_n_f32(mean); + float32x4_t var_v = vdupq_n_f32(0.0f); + for (int i = 0; i < c4; i += 4) { + float32x4_t d = vsubq_f32(vld1q_f32(&x[i]), mean_v); + var_v = vfmaq_f32(var_v, d, d); + } + float var = vaddvq_f32(var_v); + for (int i = c4; i < c; i++) { + float d = x[i] - mean; + var += d * d; + } + var /= c; + + float inv_std = 1.0f / sqrtf(var + 1e-5f); + float32x4_t inv_std_v = vdupq_n_f32(inv_std); + for (int i = 0; i < c4; i += 4) { + float32x4_t xv = vsubq_f32(vld1q_f32(&x[i]), mean_v); + float32x4_t normed = vmulq_f32(xv, inv_std_v); + vst1q_f32(&out[i], vfmaq_f32(vld1q_f32(&b[i]), + normed, vld1q_f32(&w[i]))); + } + for (int i = c4; i < c; i++) + out[i] = (x[i] - mean) * inv_std * w[i] + b[i]; +#else + float mean = 0.0f; + for (int i = 0; i < c; i++) mean += x[i]; + mean /= c; + + float var = 0.0f; + for (int i = 0; i < c; i++) { + float d = x[i] - mean; + var += d * d; + } + var /= c; + + float inv_std = 1.0f / sqrtf(var + 1e-5f); + for (int i = 0; i < c; i++) + out[i] = (x[i] - mean) * inv_std * w[i] + b[i]; +#endif +} + +/* Group norm: C elements in num_groups groups, eps=64e-5 per RWKV convention */ +static void fast_group_norm(const float *x, int num_groups, int group_size, + const float *weight, const float *bias, + float *out) { + for (int g = 0; g < num_groups; g++) { + const float *gx = &x[g * group_size]; + float *go = &out[g * group_size]; + int off = g * group_size; + +#ifdef __ARM_NEON + int gs4 = group_size & ~3; + float32x4_t sum_v = vdupq_n_f32(0.0f); + for (int i = 0; i < gs4; i += 4) + sum_v = vaddq_f32(sum_v, vld1q_f32(&gx[i])); + float mean = vaddvq_f32(sum_v); + for (int i = gs4; i < group_size; i++) mean += gx[i]; + mean /= group_size; + + float32x4_t mean_v = vdupq_n_f32(mean); + float32x4_t var_v = vdupq_n_f32(0.0f); + for (int i = 0; i < gs4; i += 4) { + float32x4_t d = vsubq_f32(vld1q_f32(&gx[i]), mean_v); + var_v = vfmaq_f32(var_v, d, d); + } + float var = vaddvq_f32(var_v); + for (int i = gs4; i < group_size; i++) { + float d = gx[i] - mean; + var += d * d; + } + var /= group_size; + + float inv_std = 1.0f / sqrtf(var + 64e-5f); + float32x4_t inv_std_v = vdupq_n_f32(inv_std); + for (int i = 0; i < gs4; i += 4) { + float32x4_t xv = vsubq_f32(vld1q_f32(&gx[i]), mean_v); + float32x4_t normed = vmulq_f32(xv, inv_std_v); + vst1q_f32(&go[i], vfmaq_f32(vld1q_f32(&bias[off + i]), + normed, vld1q_f32(&weight[off + i]))); + } + for (int i = gs4; i < group_size; i++) + go[i] = (gx[i] - mean) * inv_std * weight[off + i] + bias[off + i]; +#else + float mean = 0.0f; + for (int i = 0; i < group_size; i++) mean += gx[i]; + mean /= group_size; + + float var = 0.0f; + for (int i = 0; i < group_size; i++) { + float d = gx[i] - mean; + var += d * d; + } + var /= group_size; + + float inv_std = 1.0f / sqrtf(var + 64e-5f); + for (int i = 0; i < group_size; i++) + go[i] = (gx[i] - mean) * inv_std * weight[off + i] + bias[off + i]; +#endif + } +} + +/* L2 normalize per head */ +static void fast_l2_normalize_per_head(const float *x, float *out, + int n_head, int head_size) { + for (int h = 0; h < n_head; h++) { + const float *hx = &x[h * head_size]; + float *ho = &out[h * head_size]; +#ifdef __ARM_NEON + int hs4 = head_size & ~3; + float32x4_t sq_v = vdupq_n_f32(0.0f); + for (int i = 0; i < hs4; i += 4) { + float32x4_t v = vld1q_f32(&hx[i]); + sq_v = vfmaq_f32(sq_v, v, v); + } + float sq_sum = vaddvq_f32(sq_v); + for (int i = hs4; i < head_size; i++) + sq_sum += hx[i] * hx[i]; + + float inv_norm = 1.0f / (sqrtf(sq_sum) + 1e-12f); + float32x4_t inv_v = vdupq_n_f32(inv_norm); + for (int i = 0; i < hs4; i += 4) + vst1q_f32(&ho[i], vmulq_f32(vld1q_f32(&hx[i]), inv_v)); + for (int i = hs4; i < head_size; i++) + ho[i] = hx[i] * inv_norm; +#else + float sq_sum = 0.0f; + for (int i = 0; i < head_size; i++) + sq_sum += hx[i] * hx[i]; + float inv_norm = 1.0f / (sqrtf(sq_sum) + 1e-12f); + for (int i = 0; i < head_size; i++) + ho[i] = hx[i] * inv_norm; +#endif + } +} + +/* Token shift + lerp: out = xx + (prev - xx) * lerp_coeff */ +static void fast_token_shift(const float *xx, const float *prev, + const float *lerp, float *out, int c) { +#ifdef __ARM_NEON + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) { + float32x4_t xx_v = vld1q_f32(&xx[i]); + float32x4_t diff = vsubq_f32(vld1q_f32(&prev[i]), xx_v); + vst1q_f32(&out[i], vfmaq_f32(xx_v, diff, vld1q_f32(&lerp[i]))); + } + for (int i = c4; i < c; i++) + out[i] = xx[i] + (prev[i] - xx[i]) * lerp[i]; +#else + for (int i = 0; i < c; i++) + out[i] = xx[i] + (prev[i] - xx[i]) * lerp[i]; +#endif +} + +/* NEON residual add helper: out[i] += src[i] */ +static inline void fast_residual_add(float *out, const float *src, int c) { +#ifdef __ARM_NEON + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) + vst1q_f32(&out[i], vaddq_f32(vld1q_f32(&out[i]), vld1q_f32(&src[i]))); + for (int i = c4; i < c; i++) + out[i] += src[i]; +#else + for (int i = 0; i < c; i++) + out[i] += src[i]; +#endif +} + +/* ================================================================ + * WKV-7 kernel (single timestep, zero-allocation) + * ================================================================ */ + +#ifdef USE_THREADS +typedef struct { + const float *r_d, *w_d, *k_d, *v_d, *nkk_d, *kka_d; + float *s_d, *y_out; + int n_head, head_size; +} wkv7_ctx; + +static void wkv7_head_worker(int start, int end, void *arg); +#endif + +static void rx_wkv7_one( + const float *r_d, const float *w_d, const float *k_d, const float *v_d, + const float *nkk_d, const float *kka_d, + float *s_d, float *y_out, + int n_head, int head_size) +{ + int n = head_size; + +#ifdef USE_THREADS + if (__rx_pool) { + wkv7_ctx ctx = { r_d, w_d, k_d, v_d, nkk_d, kka_d, + s_d, y_out, n_head, head_size }; + fw_for(__rx_pool, n_head, wkv7_head_worker, &ctx); + return; + } +#endif + for (int h = 0; h < n_head; h++) { + int off = h * n; + for (int i = 0; i < n; i++) { + int sbase = (h * n + i) * n; + if (i + 1 < n) + __builtin_prefetch(&s_d[(h * n + (i + 1)) * n], 1, 3); + +#ifdef __ARM_NEON + int n4 = n & ~3; + float32x4_t sa_v = vdupq_n_f32(0.0f); + for (int j = 0; j < n4; j += 4) + sa_v = vfmaq_f32(sa_v, vld1q_f32(&nkk_d[off + j]), + vld1q_f32(&s_d[sbase + j])); + float sa = vaddvq_f32(sa_v); + for (int j = n4; j < n; j++) + sa += nkk_d[off + j] * s_d[sbase + j]; + + float vv = v_d[off + i]; + float32x4_t vv_v = vdupq_n_f32(vv); + float32x4_t sa_bcast = vdupq_n_f32(sa); + float32x4_t yy_v = vdupq_n_f32(0.0f); + for (int j = 0; j < n4; j += 4) { + float32x4_t s = vld1q_f32(&s_d[sbase + j]); + float32x4_t w = vld1q_f32(&w_d[off + j]); + float32x4_t k = vld1q_f32(&k_d[off + j]); + float32x4_t kka = vld1q_f32(&kka_d[off + j]); + s = vfmaq_f32(vfmaq_f32(vmulq_f32(s, w), k, vv_v), kka, sa_bcast); + vst1q_f32(&s_d[sbase + j], s); + yy_v = vfmaq_f32(yy_v, s, vld1q_f32(&r_d[off + j])); + } + float yy = vaddvq_f32(yy_v); + for (int j = n4; j < n; j++) { + s_d[sbase + j] = s_d[sbase + j] * w_d[off + j] + + k_d[off + j] * vv + + sa * kka_d[off + j]; + yy += s_d[sbase + j] * r_d[off + j]; + } +#else + float sa = 0.0f; + for (int j = 0; j < n; j++) + sa += nkk_d[off + j] * s_d[sbase + j]; + + float vv = v_d[off + i]; + float yy = 0.0f; + for (int j = 0; j < n; j++) { + s_d[sbase + j] = s_d[sbase + j] * w_d[off + j] + + k_d[off + j] * vv + + sa * kka_d[off + j]; + yy += s_d[sbase + j] * r_d[off + j]; + } +#endif + y_out[off + i] = yy; + } + } +} + +#ifdef USE_THREADS +static void wkv7_head_worker(int start, int end, void *arg) { + wkv7_ctx *ctx = (wkv7_ctx *)arg; + int n = ctx->head_size; + for (int h = start; h < end; h++) { + int off = h * n; + for (int i = 0; i < n; i++) { + int sbase = (h * n + i) * n; + if (i + 1 < n) + __builtin_prefetch(&ctx->s_d[(h * n + (i + 1)) * n], 1, 3); +#ifdef __ARM_NEON + int n4 = n & ~3; + float32x4_t sa_v = vdupq_n_f32(0.0f); + for (int j = 0; j < n4; j += 4) + sa_v = vfmaq_f32(sa_v, vld1q_f32(&ctx->nkk_d[off + j]), + vld1q_f32(&ctx->s_d[sbase + j])); + float sa = vaddvq_f32(sa_v); + for (int j = n4; j < n; j++) + sa += ctx->nkk_d[off + j] * ctx->s_d[sbase + j]; + + float vv = ctx->v_d[off + i]; + float32x4_t vv_v = vdupq_n_f32(vv); + float32x4_t sa_bcast = vdupq_n_f32(sa); + float32x4_t yy_v = vdupq_n_f32(0.0f); + for (int j = 0; j < n4; j += 4) { + float32x4_t s = vld1q_f32(&ctx->s_d[sbase + j]); + float32x4_t w = vld1q_f32(&ctx->w_d[off + j]); + float32x4_t k = vld1q_f32(&ctx->k_d[off + j]); + float32x4_t kka = vld1q_f32(&ctx->kka_d[off + j]); + s = vfmaq_f32(vfmaq_f32(vmulq_f32(s, w), k, vv_v), kka, sa_bcast); + vst1q_f32(&ctx->s_d[sbase + j], s); + yy_v = vfmaq_f32(yy_v, s, vld1q_f32(&ctx->r_d[off + j])); + } + float yy = vaddvq_f32(yy_v); + for (int j = n4; j < n; j++) { + ctx->s_d[sbase + j] = ctx->s_d[sbase + j] * ctx->w_d[off + j] + + ctx->k_d[off + j] * vv + + sa * ctx->kka_d[off + j]; + yy += ctx->s_d[sbase + j] * ctx->r_d[off + j]; + } +#else + float sa = 0.0f; + for (int j = 0; j < n; j++) + sa += ctx->nkk_d[off + j] * ctx->s_d[sbase + j]; + float vv = ctx->v_d[off + i]; + float yy = 0.0f; + for (int j = 0; j < n; j++) { + ctx->s_d[sbase + j] = ctx->s_d[sbase + j] * ctx->w_d[off + j] + + ctx->k_d[off + j] * vv + + sa * ctx->kka_d[off + j]; + yy += ctx->s_d[sbase + j] * ctx->r_d[off + j]; + } +#endif + ctx->y_out[off + i] = yy; + } + } +} +#endif + +/* ================================================================ + * RWKV-7 block forward (single token) + * ================================================================ */ + +static void rwkv7_time_mix_fast( + rx_rwkv_block *blk, int layer_id, + const float *xx, rx_rwkv_state *st, float *v_first, + int n_head, int head_size, rx_workspace *ws, + float *output_buf) +{ + int c = n_head * head_size; + float *prev = tm_data_f32(st->att_x_prev); + + float *shifted = ws->buf[0]; + float *r_vec = ws->buf[1]; + float *k_vec = ws->buf[2]; + float *v_vec = ws->buf[3]; + float *w_vec = ws->buf[4]; + float *a_vec = ws->buf[5]; + float *g_vec = ws->buf[6]; + float *kk = ws->buf[7]; + float *k_mod = ws->buf[8]; + float *v_final = ws->buf[9]; + float *y = ws->buf[10]; + float *y_gn = ws->buf[11]; + float *tmp = ws->buf[12]; + float *tmp2 = ws->buf[13]; + float *xv_save = ws->buf[14]; + float *lora = ws->lora_buf; + + int8_t *q8_buf = ws->q8_buf; + #define MATVEC(x_arg, w_tensor, q8_arg, out_arg, r_arg) do { \ + if (q8_arg) fast_matvec_q8((x_arg), (q8_arg), (out_arg), (q8_arg)->rows, (q8_arg)->cols, q8_buf); \ + else fast_matvec((x_arg), tm_data_f32(w_tensor), (out_arg), (r_arg), (int)tm_size((w_tensor), 1)); \ + } while(0) + + fast_token_shift(xx, prev, tm_data_f32(blk->x_r), shifted, c); + MATVEC(shifted, blk->receptance_w, blk->receptance_q8, r_vec, c); + + fast_token_shift(xx, prev, tm_data_f32(blk->x_k), shifted, c); + MATVEC(shifted, blk->key_w, blk->key_q8, k_vec, c); + + fast_token_shift(xx, prev, tm_data_f32(blk->x_v), shifted, c); + memcpy(xv_save, shifted, c * sizeof(float)); + MATVEC(shifted, blk->value_w, blk->value_q8, v_vec, c); + + /* Decay gate: w = exp(-0.606531 * sigmoid(w0 + tanh(xw @ w1) @ w2)) */ + { + int lora_dim = (int)tm_size(blk->w1, 1); + float *w0_d = tm_data_f32(blk->w0); + + fast_token_shift(xx, prev, tm_data_f32(blk->x_w), shifted, c); + fast_matvec(shifted, tm_data_f32(blk->w1), lora, c, lora_dim); + +#if defined(USE_BLAS) && defined(__APPLE__) + vvtanhf(lora, lora, &lora_dim); +#else + for (int i = 0; i < lora_dim; i++) + lora[i] = tanhf(lora[i]); +#endif + + fast_matvec(lora, tm_data_f32(blk->w2), w_vec, lora_dim, c); + +#if defined(USE_BLAS) && defined(__APPLE__) + vDSP_vadd(w0_d, 1, w_vec, 1, w_vec, 1, c); + vDSP_vneg(w_vec, 1, w_vec, 1, c); + vvexpf(w_vec, w_vec, &c); + float one = 1.0f; + vDSP_vsadd(w_vec, 1, &one, w_vec, 1, c); + vvrecf(w_vec, w_vec, &c); + float neg_coeff = -0.606531f; + vDSP_vsmul(w_vec, 1, &neg_coeff, w_vec, 1, c); + vvexpf(w_vec, w_vec, &c); +#else + for (int i = 0; i < c; i++) { + float sig = 1.0f / (1.0f + expf(-(w0_d[i] + w_vec[i]))); + w_vec[i] = expf(-0.606531f * sig); + } +#endif + } + + /* Gate a: a = sigmoid(a0 + (xa @ a1) @ a2) */ + { + int lora_dim = (int)tm_size(blk->a1, 1); + float *a0_d = tm_data_f32(blk->a0); + + fast_token_shift(xx, prev, tm_data_f32(blk->x_a), shifted, c); + fast_matvec(shifted, tm_data_f32(blk->a1), lora, c, lora_dim); + fast_matvec(lora, tm_data_f32(blk->a2), a_vec, lora_dim, c); + +#if defined(USE_BLAS) && defined(__APPLE__) + vDSP_vadd(a0_d, 1, a_vec, 1, a_vec, 1, c); + vDSP_vneg(a_vec, 1, a_vec, 1, c); + vvexpf(a_vec, a_vec, &c); + float one_a = 1.0f; + vDSP_vsadd(a_vec, 1, &one_a, a_vec, 1, c); + vvrecf(a_vec, a_vec, &c); +#else + for (int i = 0; i < c; i++) + a_vec[i] = 1.0f / (1.0f + expf(-(a0_d[i] + a_vec[i]))); +#endif + } + + /* Gate g: g = sigmoid(xg @ g1) @ g2 */ + { + int lora_dim = (int)tm_size(blk->g1, 1); + + fast_token_shift(xx, prev, tm_data_f32(blk->x_g), shifted, c); + fast_matvec(shifted, tm_data_f32(blk->g1), lora, c, lora_dim); + +#if defined(USE_BLAS) && defined(__APPLE__) + vDSP_vneg(lora, 1, lora, 1, lora_dim); + vvexpf(lora, lora, &lora_dim); + float one_g = 1.0f; + vDSP_vsadd(lora, 1, &one_g, lora, 1, lora_dim); + vvrecf(lora, lora, &lora_dim); +#else + for (int i = 0; i < lora_dim; i++) + lora[i] = 1.0f / (1.0f + expf(-lora[i])); +#endif + + fast_matvec(lora, tm_data_f32(blk->g2), g_vec, lora_dim, c); + } + + /* Update prev for next token */ + memcpy(prev, xx, c * sizeof(float)); + + /* Key normalization: kk = L2_normalize(k * k_k), per head */ + { + float *kk_d = tm_data_f32(blk->k_k); +#ifdef __ARM_NEON + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) + vst1q_f32(&tmp[i], vmulq_f32(vld1q_f32(&k_vec[i]), + vld1q_f32(&kk_d[i]))); + for (int i = c4; i < c; i++) + tmp[i] = k_vec[i] * kk_d[i]; +#else + for (int i = 0; i < c; i++) + tmp[i] = k_vec[i] * kk_d[i]; +#endif + fast_l2_normalize_per_head(tmp, kk, n_head, head_size); + } + + /* Modify key: k_mod = k * (1 + (a-1) * k_a) */ + { + float *ka_d = tm_data_f32(blk->k_a); +#ifdef __ARM_NEON + int c4 = c & ~3; + float32x4_t one_v = vdupq_n_f32(1.0f); + for (int i = 0; i < c4; i += 4) { + float32x4_t am1 = vsubq_f32(vld1q_f32(&a_vec[i]), one_v); + float32x4_t factor = vfmaq_f32(one_v, am1, vld1q_f32(&ka_d[i])); + vst1q_f32(&k_mod[i], vmulq_f32(vld1q_f32(&k_vec[i]), factor)); + } + for (int i = c4; i < c; i++) + k_mod[i] = k_vec[i] * (1.0f + (a_vec[i] - 1.0f) * ka_d[i]); +#else + for (int i = 0; i < c; i++) + k_mod[i] = k_vec[i] * (1.0f + (a_vec[i] - 1.0f) * ka_d[i]); +#endif + } + + /* Value first-token gating */ + if (layer_id == 0) { + memcpy(v_final, v_vec, c * sizeof(float)); + memcpy(v_first, v_vec, c * sizeof(float)); + } else { + int lora_dim = (int)tm_size(blk->v1, 1); + float *v0_d = tm_data_f32(blk->v0); + + fast_matvec(xv_save, tm_data_f32(blk->v1), lora, c, lora_dim); + fast_matvec(lora, tm_data_f32(blk->v2), tmp, lora_dim, c); + +#if defined(USE_BLAS) && defined(__APPLE__) + vDSP_vadd(v0_d, 1, tmp, 1, tmp, 1, c); + vDSP_vneg(tmp, 1, tmp, 1, c); + vvexpf(tmp, tmp, &c); + float one_v = 1.0f; + vDSP_vsadd(tmp, 1, &one_v, tmp, 1, c); + vvrecf(tmp, tmp, &c); + #ifdef __ARM_NEON + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) { + float32x4_t vv = vld1q_f32(&v_vec[i]); + float32x4_t diff = vsubq_f32(vld1q_f32(&v_first[i]), vv); + vst1q_f32(&v_final[i], vfmaq_f32(vv, diff, vld1q_f32(&tmp[i]))); + } + for (int i = c4; i < c; i++) + v_final[i] = v_vec[i] + (v_first[i] - v_vec[i]) * tmp[i]; + #else + for (int i = 0; i < c; i++) + v_final[i] = v_vec[i] + (v_first[i] - v_vec[i]) * tmp[i]; + #endif +#else + for (int i = 0; i < c; i++) { + float blend = 1.0f / (1.0f + expf(-(v0_d[i] + tmp[i]))); + v_final[i] = v_vec[i] + (v_first[i] - v_vec[i]) * blend; + } +#endif + } + + /* WKV-7 kernel */ + { +#ifdef __ARM_NEON + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) { + float32x4_t kk_v = vld1q_f32(&kk[i]); + vst1q_f32(&tmp[i], vnegq_f32(kk_v)); + vst1q_f32(&tmp2[i], vmulq_f32(kk_v, vld1q_f32(&a_vec[i]))); + } + for (int i = c4; i < c; i++) { + tmp[i] = -kk[i]; + tmp2[i] = kk[i] * a_vec[i]; + } +#else + for (int i = 0; i < c; i++) { + tmp[i] = -kk[i]; + tmp2[i] = kk[i] * a_vec[i]; + } +#endif + rx_wkv7_one(r_vec, w_vec, k_mod, v_final, + tmp, tmp2, tm_data_f32(st->att_kv), y, + n_head, head_size); + } + + /* Group norm */ + fast_group_norm(y, n_head, head_size, + tm_data_f32(blk->ln_x_w), tm_data_f32(blk->ln_x_b), + y_gn); + + /* Bonus: per-head (r * k_mod * r_k).sum() * v_final */ + { + float *rk_d = tm_data_f32(blk->r_k); + for (int h = 0; h < n_head; h++) { + int off = h * head_size; +#ifdef __ARM_NEON + int hs4 = head_size & ~3; + float32x4_t sum_v = vdupq_n_f32(0.0f); + for (int j = 0; j < hs4; j += 4) { + float32x4_t rv = vld1q_f32(&r_vec[off + j]); + float32x4_t km = vld1q_f32(&k_mod[off + j]); + float32x4_t rk = vld1q_f32(&rk_d[off + j]); + sum_v = vfmaq_f32(sum_v, vmulq_f32(rv, km), rk); + } + float sum = vaddvq_f32(sum_v); + for (int j = hs4; j < head_size; j++) + sum += r_vec[off + j] * k_mod[off + j] * rk_d[off + j]; + + float32x4_t sum_bcast = vdupq_n_f32(sum); + for (int j = 0; j < hs4; j += 4) { + float32x4_t cur = vld1q_f32(&y_gn[off + j]); + vst1q_f32(&y_gn[off + j], + vfmaq_f32(cur, sum_bcast, vld1q_f32(&v_final[off + j]))); + } + for (int j = hs4; j < head_size; j++) + y_gn[off + j] += sum * v_final[off + j]; +#else + float sum = 0.0f; + for (int j = 0; j < head_size; j++) + sum += r_vec[off + j] * k_mod[off + j] * rk_d[off + j]; + for (int j = 0; j < head_size; j++) + y_gn[off + j] += sum * v_final[off + j]; +#endif + } + } + + /* Output: (y_bonus * g) @ output_w */ + { +#ifdef __ARM_NEON + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) + vst1q_f32(&tmp[i], vmulq_f32(vld1q_f32(&y_gn[i]), + vld1q_f32(&g_vec[i]))); + for (int i = c4; i < c; i++) + tmp[i] = y_gn[i] * g_vec[i]; +#else + for (int i = 0; i < c; i++) + tmp[i] = y_gn[i] * g_vec[i]; +#endif + MATVEC(tmp, blk->output_w, blk->output_q8, output_buf, c); + } + + #undef MATVEC +} + +/* Channel mix (ReLU^2 FFN) for a single token */ +static void rwkv7_channel_mix_fast( + rx_rwkv_block *blk, const float *xx, rx_rwkv_state *st, + rx_workspace *ws, float *output_buf) +{ + int c = ws->c; + float *prev = tm_data_f32(st->ffn_x_prev); + float *shifted = ws->buf[0]; + int ffn_dim = blk->ffn_key_q8 ? blk->ffn_key_q8->rows + : (int)tm_size(blk->ffn_key_w, 1); + + fast_token_shift(xx, prev, tm_data_f32(blk->ffn_x_k), shifted, c); + memcpy(prev, xx, c * sizeof(float)); + + float *fk_buf = ws->ffn_buf; + int8_t *q8_buf = ws->q8_buf; + + if (blk->ffn_key_q8) + fast_matvec_q8(shifted, blk->ffn_key_q8, fk_buf, + blk->ffn_key_q8->rows, blk->ffn_key_q8->cols, q8_buf); + else + fast_matvec(shifted, tm_data_f32(blk->ffn_key_w), fk_buf, c, ffn_dim); + + /* relu^2 */ +#ifdef __ARM_NEON + { + int fd4 = ffn_dim & ~3; + float32x4_t zero = vdupq_n_f32(0.0f); + for (int i = 0; i < fd4; i += 4) { + float32x4_t v = vmaxq_f32(vld1q_f32(&fk_buf[i]), zero); + vst1q_f32(&fk_buf[i], vmulq_f32(v, v)); + } + for (int i = fd4; i < ffn_dim; i++) { + float v = fk_buf[i] > 0.0f ? fk_buf[i] : 0.0f; + fk_buf[i] = v * v; + } + } +#else + for (int i = 0; i < ffn_dim; i++) { + float v = fk_buf[i] > 0.0f ? fk_buf[i] : 0.0f; + fk_buf[i] = v * v; + } +#endif + + if (blk->ffn_value_q8) + fast_matvec_q8(fk_buf, blk->ffn_value_q8, output_buf, + blk->ffn_value_q8->rows, blk->ffn_value_q8->cols, q8_buf); + else + fast_matvec(fk_buf, tm_data_f32(blk->ffn_value_w), output_buf, ffn_dim, c); +} + +/* Full RWKV-7 block forward */ +static void rwkv7_block_forward_fast( + rx_rwkv_block *blk, int layer_id, + float *x_data, rx_rwkv_state *st, float *v_first, + int n_head, int head_size, rx_workspace *ws, + float *x_out) +{ + int c = n_head * head_size; + float *ln_out = ws->buf[15]; + + fast_layer_norm(x_data, tm_data_f32(blk->ln1_w), tm_data_f32(blk->ln1_b), + ln_out, c); + + rwkv7_time_mix_fast(blk, layer_id, ln_out, st, v_first, + n_head, head_size, ws, x_out); + fast_residual_add(x_out, x_data, c); + + fast_layer_norm(x_out, tm_data_f32(blk->ln2_w), tm_data_f32(blk->ln2_b), + ln_out, c); + + float *cmix_out = ws->buf[0]; + rwkv7_channel_mix_fast(blk, ln_out, st, ws, cmix_out); + fast_residual_add(x_out, cmix_out, c); +} + +/* ================================================================ + * Sparse attention block forward (single token, decode mode) + * Stripped: HybridMem, instrumentation, embedding capture + * ================================================================ */ + +static void attn_block_forward_fast( + rx_attn_block *blk, float *x_data, rx_attn_state *st, + int n_head, int head_size, rx_workspace *ws, + float *x_out) +{ + int c = n_head * head_size; + float *ln_out = ws->buf[15]; + float *q_vec = ws->buf[1]; + float *k_vec = ws->buf[2]; + float *v_vec = ws->buf[3]; + float *y_data = ws->buf[10]; + float *att_out = ws->buf[11]; + + fast_layer_norm(x_data, tm_data_f32(blk->ln1_w), tm_data_f32(blk->ln1_b), + ln_out, c); + + /* Q/K/V projections */ + int8_t *q8_buf = ws->q8_buf; + if (blk->receptance_q8) { + float ln_q8_scale = fast_quantize_input(ln_out, q8_buf, c); + fast_matvec_q8_preq(q8_buf, ln_q8_scale, blk->receptance_q8, q_vec, + blk->receptance_q8->rows, blk->receptance_q8->cols); + fast_matvec_q8_preq(q8_buf, ln_q8_scale, blk->key_q8, k_vec, + blk->key_q8->rows, blk->key_q8->cols); + fast_matvec_q8_preq(q8_buf, ln_q8_scale, blk->value_q8, v_vec, + blk->value_q8->rows, blk->value_q8->cols); + } else { + fast_matvec(ln_out, tm_data_f32(blk->receptance_w), q_vec, c, c); + fast_matvec(ln_out, tm_data_f32(blk->key_w), k_vec, c, c); + fast_matvec(ln_out, tm_data_f32(blk->value_w), v_vec, c, c); + } + + /* Append K/V to cache — simple eviction (discard oldest half) */ + float *k_cache = tm_data_f32(st->k_cache); + float *v_cache = tm_data_f32(st->v_cache); + + if (st->cache_len >= st->max_cache_len) { + int evict_count = st->max_cache_len / 2; + if (evict_count < 1) evict_count = 1; + int remain = st->cache_len - evict_count; + if (remain > 0) { + memmove(k_cache, &k_cache[evict_count * c], remain * c * sizeof(float)); + memmove(v_cache, &v_cache[evict_count * c], remain * c * sizeof(float)); + } + st->cache_len = remain; + } + + memcpy(&k_cache[st->cache_len * c], k_vec, c * sizeof(float)); + memcpy(&v_cache[st->cache_len * c], v_vec, c * sizeof(float)); + st->cache_len++; + + int ct = st->cache_len; + float scale = 1.0f / sqrtf((float)head_size); + + memset(y_data, 0, c * sizeof(float)); + + /* Per-head attention */ + for (int h = 0; h < n_head; h++) { + int off = h * head_size; + float attn_scores_local[ct]; + + /* Q @ K^T */ + for (int t = 0; t < ct; t++) { +#ifdef __ARM_NEON + int hs4 = head_size & ~3; + float32x4_t dot_v = vdupq_n_f32(0.0f); + for (int j = 0; j < hs4; j += 4) + dot_v = vfmaq_f32(dot_v, vld1q_f32(&q_vec[off + j]), + vld1q_f32(&k_cache[t * c + off + j])); + float dot = vaddvq_f32(dot_v); + for (int j = hs4; j < head_size; j++) + dot += q_vec[off + j] * k_cache[t * c + off + j]; +#else + float dot = 0.0f; + for (int j = 0; j < head_size; j++) + dot += q_vec[off + j] * k_cache[t * c + off + j]; +#endif + if (t + 1 < ct) + __builtin_prefetch(&k_cache[(t + 1) * c + off], 0, 1); + attn_scores_local[t] = dot * scale; + } + + /* Softmax */ + float max_s = attn_scores_local[0]; + for (int t = 1; t < ct; t++) + if (attn_scores_local[t] > max_s) max_s = attn_scores_local[t]; + + float sum_e = 0.0f; + for (int t = 0; t < ct; t++) { + attn_scores_local[t] = expf(attn_scores_local[t] - max_s); + sum_e += attn_scores_local[t]; + } + for (int t = 0; t < ct; t++) + attn_scores_local[t] /= sum_e; + + /* Value aggregation */ + for (int j = 0; j < head_size; j++) { + float val = 0.0f; + for (int t = 0; t < ct; t++) + val += attn_scores_local[t] * v_cache[t * c + off + j]; + y_data[off + j] = val; + } + } + + /* Output projection */ + if (blk->output_q8) + fast_matvec_q8(y_data, blk->output_q8, att_out, + blk->output_q8->rows, blk->output_q8->cols, q8_buf); + else + fast_matvec(y_data, tm_data_f32(blk->output_w), att_out, c, c); + + /* Residual: x1 = x + att_out */ +#ifdef __ARM_NEON + { + int c4 = c & ~3; + for (int i = 0; i < c4; i += 4) + vst1q_f32(&x_out[i], vaddq_f32(vld1q_f32(&x_data[i]), + vld1q_f32(&att_out[i]))); + for (int i = c4; i < c; i++) + x_out[i] = x_data[i] + att_out[i]; + } +#else + for (int i = 0; i < c; i++) + x_out[i] = x_data[i] + att_out[i]; +#endif + + /* Pre-FFN LayerNorm */ + fast_layer_norm(x_out, tm_data_f32(blk->ln2_w), tm_data_f32(blk->ln2_b), + ln_out, c); + + /* Channel mix: token shift + relu^2 FFN */ + { + float *ffn_prev = tm_data_f32(st->ffn_x_prev); + float *shifted = ws->buf[0]; + int ffn_dim = blk->ffn_key_q8 ? blk->ffn_key_q8->rows + : (int)tm_size(blk->ffn_key_w, 1); + float *fk_buf = ws->ffn_buf; + + fast_token_shift(ln_out, ffn_prev, tm_data_f32(blk->ffn_x_k), shifted, c); + memcpy(ffn_prev, ln_out, c * sizeof(float)); + + if (blk->ffn_key_q8) + fast_matvec_q8(shifted, blk->ffn_key_q8, fk_buf, + blk->ffn_key_q8->rows, blk->ffn_key_q8->cols, q8_buf); + else + fast_matvec(shifted, tm_data_f32(blk->ffn_key_w), fk_buf, c, ffn_dim); + +#ifdef __ARM_NEON + { + int fd4 = ffn_dim & ~3; + float32x4_t zero = vdupq_n_f32(0.0f); + for (int i = 0; i < fd4; i += 4) { + float32x4_t v = vmaxq_f32(vld1q_f32(&fk_buf[i]), zero); + vst1q_f32(&fk_buf[i], vmulq_f32(v, v)); + } + for (int i = fd4; i < ffn_dim; i++) { + float v = fk_buf[i] > 0.0f ? fk_buf[i] : 0.0f; + fk_buf[i] = v * v; + } + } +#else + for (int i = 0; i < ffn_dim; i++) { + float v = fk_buf[i] > 0.0f ? fk_buf[i] : 0.0f; + fk_buf[i] = v * v; + } +#endif + + if (blk->ffn_value_q8) + fast_matvec_q8(fk_buf, blk->ffn_value_q8, att_out, + blk->ffn_value_q8->rows, blk->ffn_value_q8->cols, q8_buf); + else + fast_matvec(fk_buf, tm_data_f32(blk->ffn_value_w), att_out, ffn_dim, c); + + fast_residual_add(x_out, att_out, c); + } +} + +/* ================================================================ + * Weight loading helpers + * ================================================================ */ + +static float* bf16_to_f32(const void *raw, int64_t numel) { + const uint16_t *src = (const uint16_t*)raw; + float *dst = (float*)malloc(numel * sizeof(float)); + for (int64_t i = 0; i < numel; i++) { + uint32_t bits = (uint32_t)src[i] << 16; + memcpy(&dst[i], &bits, sizeof(float)); + } + return dst; +} + +static tm_tensor* load_weight(fs_file *f, const char *name) { + if (!fs_has_tensor(f, name)) + return NULL; + + fs_tensor_info *info = fs_get_info(f, name); + if (!info) return NULL; + + tm_tensor *t = NULL; + + if (info->dtype == FS_DTYPE_F32) { + const float *data = fs_get_f32(f, name); + if (!data) { fs_free_info(info); return NULL; } + tm_tensor *t_view = tm_from_data((void*)data, info->shape, info->ndim, TM_F32); + t = tm_clone(t_view); + tm_free(t_view); + } else if (info->dtype == FS_DTYPE_BF16) { + const void *raw = fs_get_data(f, name); + if (!raw) { fs_free_info(info); return NULL; } + int64_t numel = 1; + for (int d = 0; d < info->ndim; d++) numel *= info->shape[d]; + float *f32_data = bf16_to_f32(raw, numel); + tm_tensor *t_view = tm_from_data(f32_data, info->shape, info->ndim, TM_F32); + t = tm_clone(t_view); + tm_free(t_view); + free(f32_data); + } else { + fprintf(stderr, "rwkvx: unsupported dtype %s for weight %s\n", + fs_dtype_name(info->dtype), name); + fs_free_info(info); + return NULL; + } + + bool is_linear = strstr(name, "key.weight") || strstr(name, "value.weight") || + strstr(name, "receptance.weight") || strstr(name, "output.weight") || + strstr(name, "head.weight"); + if (is_linear && info->ndim == 2) { + tm_tensor *tt = tm_t(t); + tm_tensor *tc = tm_contiguous(tt); + tm_free(t); tm_free(tt); + t = tc; + } + + for (int d = tm_ndim(t) - 1; d >= 0; d--) { + if (tm_size(t, d) == 1 && tm_ndim(t) > 1) { + tm_tensor *sq = tm_squeeze(t, d); + tm_free(t); + t = sq; + } + } + + if (strstr(name, "att.r_k") && tm_ndim(t) == 2) { + int64_t flat_shape[] = {tm_numel(t)}; + tm_tensor *flat = tm_reshape(t, flat_shape, 1); + tm_free(t); + t = flat; + } + + fs_free_info(info); + return t; +} + +static tm_tensor* require_weight(fs_file *f, const char *name) { + tm_tensor *t = load_weight(f, name); + if (!t) { + fprintf(stderr, "rwkvx: missing required weight: %s\n", name); + } + return t; +} + +static void pre_normalize_embeddings(tm_tensor **emb, + tm_tensor *ln0_w, tm_tensor *ln0_b, + int vocab_size, int n_embd) +{ + if (!ln0_w || !ln0_b) return; + float *e = tm_data_f32(*emb); + float *w = tm_data_f32(ln0_w); + float *b = tm_data_f32(ln0_b); + float eps = 1e-5f; + + for (int v = 0; v < vocab_size; v++) { + float *row = &e[v * n_embd]; + float mean = 0.0f; + for (int j = 0; j < n_embd; j++) mean += row[j]; + mean /= n_embd; + float var = 0.0f; + for (int j = 0; j < n_embd; j++) { + float d = row[j] - mean; + var += d * d; + } + var /= n_embd; + float inv_std = 1.0f / sqrtf(var + eps); + for (int j = 0; j < n_embd; j++) + row[j] = (row[j] - mean) * inv_std * w[j] + b[j]; + } +} + +/* ================================================================ + * Internal model loading + * ================================================================ */ + +static rx_model_internal* rx_model_load_internal(const char *path) { +#ifdef USE_CUDA + fc_init(); /* Initialize CUDA if available (no-op if already init) */ +#endif + + fs_file *f = fs_open(path); + if (!f) { + fprintf(stderr, "rwkvx: failed to open %s\n", path); + return NULL; + } + + /* Auto-detect weight prefix: RWKV-X uses "rwkv." prefix, pure RWKV-7 uses none */ + const char *prefix = "rwkv."; + fs_tensor_info *emb_info = fs_get_info(f, "rwkv.emb.weight"); + if (!emb_info) { + emb_info = fs_get_info(f, "emb.weight"); + if (!emb_info) { + fprintf(stderr, "rwkvx: missing emb.weight (tried rwkv.emb.weight and emb.weight)\n"); + fs_close(f); + return NULL; + } + prefix = ""; + } + int vocab_size = (int)emb_info->shape[0]; + int n_embd = (int)emb_info->shape[1]; + int head_size = 64; + int n_head = n_embd / head_size; + fs_free_info(emb_info); + + int count; + const char **names = fs_tensor_names(f, &count); + int max_rwkv = -1, max_attn = -1; + char rwkv_scan_fmt[64]; + snprintf(rwkv_scan_fmt, sizeof(rwkv_scan_fmt), "%sblocks.%%d.", prefix); + for (int i = 0; i < count; i++) { + int idx; + if (sscanf(names[i], rwkv_scan_fmt, &idx) == 1) { + if (idx > max_rwkv) max_rwkv = idx; + } else if (sscanf(names[i], "moba.%d.", &idx) == 1) { + if (idx > max_attn) max_attn = idx; + } + } + int n_rwkv_layer = max_rwkv + 1; + int n_attn_layer = max_attn >= 0 ? max_attn + 1 : 0; + + const char *model_type = n_attn_layer > 0 ? "RWKV-X hybrid" : "RWKV-7 pure"; + fprintf(stderr, "rwkvx: %s — n_embd=%d n_head=%d n_rwkv=%d n_attn=%d vocab=%d\n", + model_type, n_embd, n_head, n_rwkv_layer, n_attn_layer, vocab_size); + + rx_model_internal *m = (rx_model_internal*)calloc(1, sizeof(rx_model_internal)); + m->config.n_embd = n_embd; + m->config.n_head = n_head; + m->config.head_size = head_size; + m->config.n_rwkv_layer = n_rwkv_layer; + m->config.n_attn_layer = n_attn_layer; + m->config.vocab_size = vocab_size; + + char name_buf[256]; + snprintf(name_buf, sizeof(name_buf), "%semb.weight", prefix); + m->emb_weight = require_weight(f, name_buf); + snprintf(name_buf, sizeof(name_buf), "%sln_out.weight", prefix); + m->ln_out_w = require_weight(f, name_buf); + snprintf(name_buf, sizeof(name_buf), "%sln_out.bias", prefix); + m->ln_out_b = require_weight(f, name_buf); + snprintf(name_buf, sizeof(name_buf), "%shead.weight", prefix); + m->head_weight = require_weight(f, name_buf); + + if (!m->emb_weight || !m->ln_out_w || !m->ln_out_b || !m->head_weight) { + /* Let pn_rwkvx_free handle cleanup */ + fs_close(f); + return NULL; + } + + snprintf(name_buf, sizeof(name_buf), "%sblocks.0.ln0.weight", prefix); + tm_tensor *ln0_w = load_weight(f, name_buf); + snprintf(name_buf, sizeof(name_buf), "%sblocks.0.ln0.bias", prefix); + tm_tensor *ln0_b = load_weight(f, name_buf); + pre_normalize_embeddings(&m->emb_weight, ln0_w, ln0_b, vocab_size, n_embd); + if (ln0_w) tm_free(ln0_w); + if (ln0_b) tm_free(ln0_b); + + /* Load RWKV-7 blocks */ + m->rwkv_blocks = (rx_rwkv_block*)calloc(n_rwkv_layer, sizeof(rx_rwkv_block)); + for (int i = 0; i < n_rwkv_layer; i++) { + char buf[256], block_pfx[128]; + snprintf(block_pfx, sizeof(block_pfx), "%sblocks.%d", prefix, i); + rx_rwkv_block *b = &m->rwkv_blocks[i]; + + #define LOAD_RWKV(field, suffix) do { \ + snprintf(buf, sizeof(buf), "%s.%s", block_pfx, suffix); \ + b->field = require_weight(f, buf); \ + } while(0) + + LOAD_RWKV(ln1_w, "ln1.weight"); + LOAD_RWKV(ln1_b, "ln1.bias"); + LOAD_RWKV(ln2_w, "ln2.weight"); + LOAD_RWKV(ln2_b, "ln2.bias"); + LOAD_RWKV(x_r, "att.x_r"); + LOAD_RWKV(x_w, "att.x_w"); + LOAD_RWKV(x_k, "att.x_k"); + LOAD_RWKV(x_v, "att.x_v"); + LOAD_RWKV(x_a, "att.x_a"); + LOAD_RWKV(x_g, "att.x_g"); + LOAD_RWKV(w0, "att.w0"); + LOAD_RWKV(w1, "att.w1"); + LOAD_RWKV(w2, "att.w2"); + LOAD_RWKV(a0, "att.a0"); + LOAD_RWKV(a1, "att.a1"); + LOAD_RWKV(a2, "att.a2"); + LOAD_RWKV(g1, "att.g1"); + LOAD_RWKV(g2, "att.g2"); + LOAD_RWKV(k_k, "att.k_k"); + LOAD_RWKV(k_a, "att.k_a"); + LOAD_RWKV(r_k, "att.r_k"); + LOAD_RWKV(receptance_w, "att.receptance.weight"); + LOAD_RWKV(key_w, "att.key.weight"); + LOAD_RWKV(value_w, "att.value.weight"); + LOAD_RWKV(output_w, "att.output.weight"); + LOAD_RWKV(ln_x_w, "att.ln_x.weight"); + LOAD_RWKV(ln_x_b, "att.ln_x.bias"); + LOAD_RWKV(ffn_x_k, "ffn.x_k"); + LOAD_RWKV(ffn_key_w, "ffn.key.weight"); + LOAD_RWKV(ffn_value_w, "ffn.value.weight"); + if (i > 0) { + LOAD_RWKV(v0, "att.v0"); + LOAD_RWKV(v1, "att.v1"); + LOAD_RWKV(v2, "att.v2"); + } + #undef LOAD_RWKV + fprintf(stderr, "rwkvx: loaded RWKV-7 block %d\n", i); + } + + /* Load sparse attention blocks */ + m->attn_blocks = (rx_attn_block*)calloc(n_attn_layer > 0 ? n_attn_layer : 1, + sizeof(rx_attn_block)); + for (int i = 0; i < n_attn_layer; i++) { + char buf[256]; + rx_attn_block *b = &m->attn_blocks[i]; + + #define LOAD_ATTN(field, fmt, ...) do { \ + snprintf(buf, sizeof(buf), fmt, __VA_ARGS__); \ + b->field = require_weight(f, buf); \ + } while(0) + + LOAD_ATTN(ln1_w, "moba.%d.ln1.weight", i); + LOAD_ATTN(ln1_b, "moba.%d.ln1.bias", i); + LOAD_ATTN(ln2_w, "moba.%d.ln2.weight", i); + LOAD_ATTN(ln2_b, "moba.%d.ln2.bias", i); + LOAD_ATTN(receptance_w, "moba.%d.att.receptance.weight", i); + LOAD_ATTN(key_w, "moba.%d.att.key.weight", i); + LOAD_ATTN(value_w, "moba.%d.att.value.weight", i); + LOAD_ATTN(output_w, "moba.%d.att.output.weight", i); + LOAD_ATTN(ffn_x_k, "moba.%d.ffn.x_k", i); + LOAD_ATTN(ffn_key_w, "moba.%d.ffn.key.weight", i); + LOAD_ATTN(ffn_value_w, "moba.%d.ffn.value.weight", i); + #undef LOAD_ATTN + fprintf(stderr, "rwkvx: loaded sparse attention block %d\n", i); + } + + /* Build block execution order */ + int n_blocks = n_rwkv_layer + n_attn_layer; + m->n_blocks = n_blocks; + m->block_order = (int*)calloc(n_blocks, sizeof(int)); + m->block_index = (int*)calloc(n_blocks, sizeof(int)); + + if (n_attn_layer == 0) { + for (int i = 0; i < n_rwkv_layer; i++) { + m->block_order[i] = 0; + m->block_index[i] = i; + } + } else { + int interval = n_rwkv_layer / n_attn_layer; + int pos = 0; + for (int a = 0; a < n_attn_layer; a++) { + int start = a * interval; + int end = (a + 1) * interval; + for (int j = start; j < end; j++) { + m->block_order[pos] = 0; + m->block_index[pos] = j; + pos++; + } + m->block_order[pos] = 1; + m->block_index[pos] = a; + pos++; + } + } + +#ifdef USE_THREADS + m->pool = fw_pool_new(0); + if (m->pool) + fprintf(stderr, "rwkvx: pthread thread pool created\n"); +#endif + + fs_close(f); + fprintf(stderr, "rwkvx: model loaded successfully (%d blocks)\n", n_blocks); + return m; +} + +/* Quantize a 2D weight tensor to INT8 */ +static rx_q8_weight* rx_quantize_weight(tm_tensor *t) { + if (!t || tm_ndim(t) != 2) return NULL; + int in_dim = (int)tm_size(t, 0); + int out_dim = (int)tm_size(t, 1); + const float *src = tm_data_f32(t); + + float *transposed = (float*)malloc(out_dim * in_dim * sizeof(float)); + for (int i = 0; i < in_dim; i++) + for (int j = 0; j < out_dim; j++) + transposed[j * in_dim + i] = src[i * out_dim + j]; + + rx_q8_weight *q = rx_q8_from_f32(transposed, out_dim, in_dim); + free(transposed); + return q; +} + +static void rx_model_quantize_weights(rx_model_internal *m) { + if (!m || m->quantized) return; + + for (int i = 0; i < m->config.n_rwkv_layer; i++) { + rx_rwkv_block *b = &m->rwkv_blocks[i]; + b->receptance_q8 = rx_quantize_weight(b->receptance_w); + b->key_q8 = rx_quantize_weight(b->key_w); + b->value_q8 = rx_quantize_weight(b->value_w); + b->output_q8 = rx_quantize_weight(b->output_w); + b->ffn_key_q8 = rx_quantize_weight(b->ffn_key_w); + b->ffn_value_q8 = rx_quantize_weight(b->ffn_value_w); + tm_free(b->receptance_w); b->receptance_w = NULL; + tm_free(b->key_w); b->key_w = NULL; + tm_free(b->value_w); b->value_w = NULL; + tm_free(b->output_w); b->output_w = NULL; + tm_free(b->ffn_key_w); b->ffn_key_w = NULL; + tm_free(b->ffn_value_w); b->ffn_value_w = NULL; + } + + for (int i = 0; i < m->config.n_attn_layer; i++) { + rx_attn_block *b = &m->attn_blocks[i]; + b->receptance_q8 = rx_quantize_weight(b->receptance_w); + b->key_q8 = rx_quantize_weight(b->key_w); + b->value_q8 = rx_quantize_weight(b->value_w); + b->output_q8 = rx_quantize_weight(b->output_w); + b->ffn_key_q8 = rx_quantize_weight(b->ffn_key_w); + b->ffn_value_q8 = rx_quantize_weight(b->ffn_value_w); + tm_free(b->receptance_w); b->receptance_w = NULL; + tm_free(b->key_w); b->key_w = NULL; + tm_free(b->value_w); b->value_w = NULL; + tm_free(b->output_w); b->output_w = NULL; + tm_free(b->ffn_key_w); b->ffn_key_w = NULL; + tm_free(b->ffn_value_w); b->ffn_value_w = NULL; + } + + m->head_q8 = rx_quantize_weight(m->head_weight); + tm_free(m->head_weight); m->head_weight = NULL; + + m->quantized = true; + fprintf(stderr, "rwkvx: weights quantized to INT8 (per-row symmetric)\n"); +} + +/* Internal model free */ +static void rx_model_free_internal(rx_model_internal *m) { + if (!m) return; + if (m->emb_weight) tm_free(m->emb_weight); + if (m->ln_out_w) tm_free(m->ln_out_w); + if (m->ln_out_b) tm_free(m->ln_out_b); + if (m->head_weight) tm_free(m->head_weight); + rx_q8_free(m->head_q8); + + for (int i = 0; i < m->config.n_rwkv_layer; i++) { + rx_rwkv_block *b = &m->rwkv_blocks[i]; + if (b->ln1_w) tm_free(b->ln1_w); if (b->ln1_b) tm_free(b->ln1_b); + if (b->ln2_w) tm_free(b->ln2_w); if (b->ln2_b) tm_free(b->ln2_b); + if (b->x_r) tm_free(b->x_r); if (b->x_w) tm_free(b->x_w); + if (b->x_k) tm_free(b->x_k); if (b->x_v) tm_free(b->x_v); + if (b->x_a) tm_free(b->x_a); if (b->x_g) tm_free(b->x_g); + if (b->w0) tm_free(b->w0); if (b->w1) tm_free(b->w1); if (b->w2) tm_free(b->w2); + if (b->a0) tm_free(b->a0); if (b->a1) tm_free(b->a1); if (b->a2) tm_free(b->a2); + if (b->v0) tm_free(b->v0); if (b->v1) tm_free(b->v1); if (b->v2) tm_free(b->v2); + if (b->g1) tm_free(b->g1); if (b->g2) tm_free(b->g2); + if (b->k_k) tm_free(b->k_k); if (b->k_a) tm_free(b->k_a); if (b->r_k) tm_free(b->r_k); + if (b->receptance_w) tm_free(b->receptance_w); if (b->key_w) tm_free(b->key_w); + if (b->value_w) tm_free(b->value_w); if (b->output_w) tm_free(b->output_w); + if (b->ln_x_w) tm_free(b->ln_x_w); if (b->ln_x_b) tm_free(b->ln_x_b); + if (b->ffn_x_k) tm_free(b->ffn_x_k); + if (b->ffn_key_w) tm_free(b->ffn_key_w); if (b->ffn_value_w) tm_free(b->ffn_value_w); + rx_q8_free(b->receptance_q8); rx_q8_free(b->key_q8); + rx_q8_free(b->value_q8); rx_q8_free(b->output_q8); + rx_q8_free(b->ffn_key_q8); rx_q8_free(b->ffn_value_q8); + } + free(m->rwkv_blocks); + + for (int i = 0; i < m->config.n_attn_layer; i++) { + rx_attn_block *b = &m->attn_blocks[i]; + if (b->ln1_w) tm_free(b->ln1_w); if (b->ln1_b) tm_free(b->ln1_b); + if (b->ln2_w) tm_free(b->ln2_w); if (b->ln2_b) tm_free(b->ln2_b); + if (b->receptance_w) tm_free(b->receptance_w); if (b->key_w) tm_free(b->key_w); + if (b->value_w) tm_free(b->value_w); if (b->output_w) tm_free(b->output_w); + if (b->ffn_x_k) tm_free(b->ffn_x_k); + if (b->ffn_key_w) tm_free(b->ffn_key_w); if (b->ffn_value_w) tm_free(b->ffn_value_w); + rx_q8_free(b->receptance_q8); rx_q8_free(b->key_q8); + rx_q8_free(b->value_q8); rx_q8_free(b->output_q8); + rx_q8_free(b->ffn_key_q8); rx_q8_free(b->ffn_value_q8); + } + free(m->attn_blocks); + + free(m->block_order); + free(m->block_index); + + /* Tokenizer is freed by pn_rwkvx_free() before calling this function */ + +#ifdef USE_THREADS + if (m->pool) fw_pool_free(m->pool); +#endif + +#ifdef USE_CUDA + fc_free(); +#endif + + free(m); +} + +/* ================================================================ + * Internal: Forward pass + * ================================================================ */ + +#define DEFAULT_MAX_CACHE_LEN 2048 + +static tm_tensor* rx_forward_internal(rx_model_internal *m, uint32_t token, + rx_state_internal *s) { + if (!m || !s) return NULL; +#ifdef USE_THREADS + __rx_pool = m->pool; +#endif + rx_config cfg = m->config; + int c = cfg.n_embd; + int vocab = cfg.vocab_size; + rx_workspace *ws = s->workspace; + + float *emb_data = tm_data_f32(m->emb_weight); + float *x_cur = ws->x_buf[0]; + float *x_next = ws->x_buf[1]; + memcpy(x_cur, &emb_data[token * c], c * sizeof(float)); + + for (int pos = 0; pos < m->n_blocks; pos++) { + int type = m->block_order[pos]; + int idx = m->block_index[pos]; + + if (type == 0) { + rwkv7_block_forward_fast( + &m->rwkv_blocks[idx], idx, x_cur, + &s->rwkv[idx], s->v_first_buf, + cfg.n_head, cfg.head_size, ws, x_next); + } else { + attn_block_forward_fast( + &m->attn_blocks[idx], x_cur, &s->attn[idx], + cfg.n_head, cfg.head_size, ws, x_next); + } + float *tmp = x_cur; + x_cur = x_next; + x_next = tmp; + } + + fast_layer_norm(x_cur, tm_data_f32(m->ln_out_w), tm_data_f32(m->ln_out_b), + x_next, c); + + int64_t logits_shape[] = {vocab}; + tm_tensor *logits = tm_empty(logits_shape, 1, TM_F32); + if (m->head_q8) + fast_matvec_q8(x_next, m->head_q8, tm_data_f32(logits), + m->head_q8->rows, m->head_q8->cols, ws->q8_buf); + else + fast_matvec(x_next, tm_data_f32(m->head_weight), + tm_data_f32(logits), c, vocab); + + return logits; +} + +/* ================================================================ + * Internal: State management + * ================================================================ */ + +static rx_state_internal* rx_state_new_internal(const rx_model_internal *m) { + if (!m) return NULL; + rx_config cfg = m->config; + int c = cfg.n_embd; + + rx_state_internal *s = (rx_state_internal*)calloc(1, sizeof(rx_state_internal)); + s->config = cfg; + + s->rwkv = (rx_rwkv_state*)calloc(cfg.n_rwkv_layer, sizeof(rx_rwkv_state)); + int64_t shape_c[] = {c}; + int state_size = cfg.n_head * cfg.head_size * cfg.head_size; + int64_t shape_s[] = {state_size}; + for (int i = 0; i < cfg.n_rwkv_layer; i++) { + s->rwkv[i].att_x_prev = tm_zeros(shape_c, 1, TM_F32); + s->rwkv[i].att_kv = tm_zeros(shape_s, 1, TM_F32); + s->rwkv[i].ffn_x_prev = tm_zeros(shape_c, 1, TM_F32); + } + + s->attn = (rx_attn_state*)calloc(cfg.n_attn_layer > 0 ? cfg.n_attn_layer : 1, + sizeof(rx_attn_state)); + for (int i = 0; i < cfg.n_attn_layer; i++) { + int64_t cache_shape[] = {DEFAULT_MAX_CACHE_LEN * c}; + s->attn[i].k_cache = tm_zeros(cache_shape, 1, TM_F32); + s->attn[i].v_cache = tm_zeros(cache_shape, 1, TM_F32); + s->attn[i].cache_len = 0; + s->attn[i].max_cache_len = DEFAULT_MAX_CACHE_LEN; + s->attn[i].ffn_x_prev = tm_zeros(shape_c, 1, TM_F32); + } + + s->v_first = tm_zeros(shape_c, 1, TM_F32); + s->v_first_buf = tm_data_f32(s->v_first); + + int max_lora = 0, max_ffn = 0; + for (int i = 0; i < cfg.n_rwkv_layer; i++) { + rx_rwkv_block *blk = &m->rwkv_blocks[i]; + int d; + d = (int)tm_size(blk->w1, 1); if (d > max_lora) max_lora = d; + d = (int)tm_size(blk->a1, 1); if (d > max_lora) max_lora = d; + d = (int)tm_size(blk->g1, 1); if (d > max_lora) max_lora = d; + if (blk->v1) { d = (int)tm_size(blk->v1, 1); if (d > max_lora) max_lora = d; } + d = blk->ffn_key_q8 ? blk->ffn_key_q8->rows : (int)tm_size(blk->ffn_key_w, 1); + if (d > max_ffn) max_ffn = d; + } + for (int i = 0; i < cfg.n_attn_layer; i++) { + rx_attn_block *blk = &m->attn_blocks[i]; + int d = blk->ffn_key_q8 ? blk->ffn_key_q8->rows : (int)tm_size(blk->ffn_key_w, 1); + if (d > max_ffn) max_ffn = d; + } + s->workspace = rx_workspace_new(c, max_lora, max_ffn); + + return s; +} + +static void rx_state_free_internal(rx_state_internal *s) { + if (!s) return; + for (int i = 0; i < s->config.n_rwkv_layer; i++) { + tm_free(s->rwkv[i].att_x_prev); + tm_free(s->rwkv[i].att_kv); + tm_free(s->rwkv[i].ffn_x_prev); + } + free(s->rwkv); + for (int i = 0; i < s->config.n_attn_layer; i++) { + tm_free(s->attn[i].k_cache); + tm_free(s->attn[i].v_cache); + tm_free(s->attn[i].ffn_x_prev); + } + free(s->attn); + tm_free(s->v_first); + rx_workspace_free(s->workspace); + free(s); +} + +static void rx_state_reset_internal(rx_state_internal *s) { + if (!s) return; + rx_config cfg = s->config; + for (int i = 0; i < cfg.n_rwkv_layer; i++) { + tm_fill_(s->rwkv[i].att_x_prev, 0.0f); + tm_fill_(s->rwkv[i].att_kv, 0.0f); + tm_fill_(s->rwkv[i].ffn_x_prev, 0.0f); + } + for (int i = 0; i < cfg.n_attn_layer; i++) { + tm_fill_(s->attn[i].k_cache, 0.0f); + tm_fill_(s->attn[i].v_cache, 0.0f); + s->attn[i].cache_len = 0; + tm_fill_(s->attn[i].ffn_x_prev, 0.0f); + } + tm_fill_(s->v_first, 0.0f); +} + +/* ================================================================ + * Tokenizer + * ================================================================ */ + +static rx_trie_node* trie_node_new(void) { + rx_trie_node *n = (rx_trie_node*)calloc(1, sizeof(rx_trie_node)); + n->token_id = -1; + return n; +} + +static void trie_node_free(rx_trie_node *n) { + if (!n) return; + for (int i = 0; i < 256; i++) + trie_node_free(n->children[i]); + free(n); +} + +static void trie_insert(rx_trie_node *root, const uint8_t *key, int len, int32_t token_id) { + rx_trie_node *node = root; + for (int i = 0; i < len; i++) { + int idx = key[i]; + if (!node->children[idx]) + node->children[idx] = trie_node_new(); + node = node->children[idx]; + } + node->token_id = token_id; +} + +static int parse_python_literal(const char *s, uint8_t *out, int max_out) { + const char *p = s; + bool is_bytes = false; + int out_len = 0; + + while (*p == ' ' || *p == '\t') p++; + if (*p == 'b') { is_bytes = true; p++; } + + char quote = *p; + if (quote != '\'' && quote != '"') return -1; + p++; + + const char *end = p; + while (*end && *end != quote) { + if (*end == '\\') end++; + if (*end) end++; + } + if (*end != quote) return -1; + + while (p < end && out_len < max_out) { + if (*p == '\\' && p + 1 < end) { + switch (p[1]) { + case 'x': + if (p + 3 < end) { + char hex[3] = {p[2], p[3], 0}; + out[out_len++] = (uint8_t)strtol(hex, NULL, 16); + p += 4; + } else { + return -1; + } + break; + case 'n': out[out_len++] = '\n'; p += 2; break; + case 'r': out[out_len++] = '\r'; p += 2; break; + case 't': out[out_len++] = '\t'; p += 2; break; + case '\\': out[out_len++] = '\\'; p += 2; break; + case '\'': out[out_len++] = '\''; p += 2; break; + case '"': out[out_len++] = '"'; p += 2; break; + case '0': out[out_len++] = 0; p += 2; break; + default: + out[out_len++] = '\\'; + p++; + break; + } + } else { + if (is_bytes) { + out[out_len++] = (uint8_t)*p; + } else { + out[out_len++] = (unsigned char)*p; + } + p++; + } + } + return out_len; +} + +static rx_tokenizer* rx_tokenizer_new(const char *vocab_path) { + FILE *fp = fopen(vocab_path, "r"); + if (!fp) { + fprintf(stderr, "rwkvx: failed to open vocab: %s\n", vocab_path); + return NULL; + } + + rx_tokenizer *tok = (rx_tokenizer*)calloc(1, sizeof(rx_tokenizer)); + tok->root = trie_node_new(); + + const char *eot = "<|endoftext|>"; + int eot_len = (int)strlen(eot); + tok->token_bytes[0] = (uint8_t*)malloc(eot_len); + memcpy(tok->token_bytes[0], eot, eot_len); + tok->token_lens[0] = eot_len; + + char line[4096]; + while (fgets(line, sizeof(line), fp)) { + int ll = (int)strlen(line); + while (ll > 0 && (line[ll-1] == '\n' || line[ll-1] == '\r')) line[--ll] = 0; + if (ll == 0) continue; + + char *first_space = strchr(line, ' '); + if (!first_space) continue; + char *last_space = strrchr(line, ' '); + if (last_space == first_space) continue; + + int idx = atoi(line); + if (idx < 0 || idx >= RX_VOCAB_SIZE) continue; + + *last_space = 0; + const char *literal = first_space + 1; + + uint8_t bytes[RX_MAX_TOKEN_LEN]; + int blen = parse_python_literal(literal, bytes, RX_MAX_TOKEN_LEN); + if (blen < 0) continue; + + trie_insert(tok->root, bytes, blen, idx); + tok->token_bytes[idx] = (uint8_t*)malloc(blen); + memcpy(tok->token_bytes[idx], bytes, blen); + tok->token_lens[idx] = blen; + } + + fclose(fp); + return tok; +} + +static void rx_tokenizer_free(rx_tokenizer *tok) { + if (!tok) return; + trie_node_free(tok->root); + for (int i = 0; i < RX_VOCAB_SIZE; i++) + free(tok->token_bytes[i]); + free(tok); +} + +static int rx_tokenizer_encode(const rx_tokenizer *tok, const char *text, + uint32_t *tokens_out, int max_tokens) { + if (!tok || !text) return 0; + const uint8_t *src = (const uint8_t*)text; + int src_len = (int)strlen(text); + int n = 0, i = 0; + + while (i < src_len && n < max_tokens) { + rx_trie_node *node = tok->root; + int last_match_pos = i; + int32_t last_match_id = -1; + int j = i; + + while (j < src_len) { + int byte = src[j]; + if (!node->children[byte]) break; + node = node->children[byte]; + j++; + if (node->token_id >= 0) { + last_match_pos = j; + last_match_id = node->token_id; + } + } + + if (last_match_id >= 0) { + tokens_out[n++] = (uint32_t)last_match_id; + i = last_match_pos; + } else { + tokens_out[n++] = (uint32_t)(src[i] + 1); + i++; + } + } + return n; +} + +static char* rx_tokenizer_decode(const rx_tokenizer *tok, const uint32_t *tokens, + int n_tokens) { + if (!tok || !tokens) return NULL; + + int total = 0; + for (int i = 0; i < n_tokens; i++) { + uint32_t tid = tokens[i]; + if (tid < RX_VOCAB_SIZE) + total += tok->token_lens[tid]; + } + + char *out = (char*)malloc(total + 1); + int pos = 0; + for (int i = 0; i < n_tokens; i++) { + uint32_t tid = tokens[i]; + if (tid < RX_VOCAB_SIZE && tok->token_bytes[tid]) { + memcpy(&out[pos], tok->token_bytes[tid], tok->token_lens[tid]); + pos += tok->token_lens[tid]; + } + } + out[pos] = 0; + return out; +} + +/* ================================================================ + * Pantheon adapter layer + * ================================================================ */ + +void* pn_rwkvx_load(const char *safetensors_path, pn_config *config, bool quantize) { + rx_model_internal *m = rx_model_load_internal(safetensors_path); + if (!m) return NULL; + + if (quantize) + rx_model_quantize_weights(m); + + /* Fill Pantheon config */ + config->arch = PN_ARCH_RWKVX; + config->n_vocab = m->config.vocab_size; + config->n_embd = m->config.n_embd; + config->n_head = m->config.n_head; + config->n_kv_head = m->config.n_head; + config->n_layer = m->config.n_rwkv_layer + m->config.n_attn_layer; + config->n_ff = 0; + config->n_ctx = DEFAULT_MAX_CACHE_LEN; + config->rope_theta = 0.0f; + config->rms_norm_eps = 1e-5f; + config->sliding_window = 0; + config->rope_dim = 0; + config->n_experts = 0; + config->n_experts_used = 0; + + return m; +} + +void pn_rwkvx_free(void *weights) { + rx_model_internal *m = (rx_model_internal*)weights; + if (!m) return; + + /* Free tokenizer if loaded */ + if (m->tokenizer) + rx_tokenizer_free((rx_tokenizer*)m->tokenizer); + m->tokenizer = NULL; + + rx_model_free_internal(m); +} + +tm_tensor* pn_rwkvx_forward(pn_model *model, uint32_t token, pn_state *state) { + rx_model_internal *m = (rx_model_internal*)model->weights; + rx_state_internal *s = (rx_state_internal*)state->kv_cache; + return rx_forward_internal(m, token, s); +} + +void* pn_rwkvx_state_new(void *weights) { + rx_model_internal *m = (rx_model_internal*)weights; + return rx_state_new_internal(m); +} + +void pn_rwkvx_state_free(void *state) { + rx_state_free_internal((rx_state_internal*)state); +} + +void pn_rwkvx_state_reset(void *state) { + rx_state_reset_internal((rx_state_internal*)state); +} + +int pn_rwkvx_encode(void *weights, const char *text, uint32_t *out, int max) { + rx_model_internal *m = (rx_model_internal*)weights; + if (!m || !m->tokenizer) return 0; + return rx_tokenizer_encode((rx_tokenizer*)m->tokenizer, text, out, max); +} + +char* pn_rwkvx_decode(void *weights, const uint32_t *tokens, int n) { + rx_model_internal *m = (rx_model_internal*)weights; + if (!m || !m->tokenizer) return NULL; + return rx_tokenizer_decode((rx_tokenizer*)m->tokenizer, tokens, n); +} + +int pn_rwkvx_eos_id(void *weights) { + (void)weights; + return 0; /* <|endoftext|> */ +} + +int pn_rwkvx_vocab_size(void *weights) { + rx_model_internal *m = (rx_model_internal*)weights; + if (!m) return 0; + return m->config.vocab_size; +} + +int pn_rwkvx_n_attn_layers(void *weights) { + rx_model_internal *m = (rx_model_internal*)weights; + if (!m) return 0; + return m->config.n_attn_layer; +} + +int pn_rwkvx_load_tokenizer(void *weights, const char *vocab_path) { + rx_model_internal *m = (rx_model_internal*)weights; + if (!m || !vocab_path) return -1; + rx_tokenizer *tok = rx_tokenizer_new(vocab_path); + if (!tok) return -1; + m->tokenizer = tok; + return 0; +} diff --git a/src/models/rwkvx.h b/src/models/rwkvx.h new file mode 100644 index 0000000..94977db --- /dev/null +++ b/src/models/rwkvx.h @@ -0,0 +1,43 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * rwkvx.h — RWKV-X adapter for Pantheon + * + * Wraps the RWKV-X inference engine (copied from foundry/rwkvx/rwkvx.c) + * into Pantheon's pn_model/pn_state/pn_forward_fn interface. + */ + +#ifndef PN_RWKVX_H +#define PN_RWKVX_H + +#include "../core/engine.h" + +/* Load RWKV-X safetensors model. Populates config from weight shapes. + * If quantize=true, quantizes linear weights to INT8 at load time. + * Returns opaque weights pointer (rx_model*), or NULL on failure. */ +void* pn_rwkvx_load(const char *safetensors_path, pn_config *config, bool quantize); + +/* Free RWKV-X weights. */ +void pn_rwkvx_free(void *weights); + +/* Forward pass conforming to pn_forward_fn signature. + * Extracts internal rx_model/rx_state from pn_model/pn_state. */ +tm_tensor* pn_rwkvx_forward(pn_model *model, uint32_t token, pn_state *state); + +/* State lifecycle — creates/frees RWKV-X dual state (recurrent + KV cache + workspace). */ +void* pn_rwkvx_state_new(void *weights); +void pn_rwkvx_state_free(void *state); +void pn_rwkvx_state_reset(void *state); + +/* Tokenizer access (for engine.c encode/decode routing) */ +int pn_rwkvx_encode(void *weights, const char *text, uint32_t *out, int max); +char* pn_rwkvx_decode(void *weights, const uint32_t *tokens, int n); +int pn_rwkvx_eos_id(void *weights); /* returns 0 (<|endoftext|>) */ +int pn_rwkvx_vocab_size(void *weights); /* returns 65536 */ +int pn_rwkvx_n_attn_layers(void *weights); /* 0 for pure RWKV-7, >0 for RWKV-X hybrid */ + +/* Load RWKV World tokenizer into the model (separate from weight loading). + * Must be called after pn_rwkvx_load(). Returns 0 on success, -1 on failure. */ +int pn_rwkvx_load_tokenizer(void *weights, const char *vocab_path); + +#endif /* PN_RWKVX_H */ diff --git a/src/serve/attn_residency_json.c b/src/serve/attn_residency_json.c new file mode 100644 index 0000000..4058204 --- /dev/null +++ b/src/serve/attn_residency_json.c @@ -0,0 +1,69 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file attn_residency_json.c + * @brief Implementation of the attention-path residency JSON writers + * (see attn_residency_json.h). + * + * @status None + * @issues None + * @todo None + */ + +#include "attn_residency_json.h" + +/* One class object: {"ondie":..,"host":..,"total":..}. `gave_up` and + * `impossible` are emitted only when non-zero — they are the loud cases and a + * zero would be noise on every healthy read. */ +static void ar_write_counts(json_writer *jw, const pn_attn_residency *c) +{ + jw_printf(jw, "{\"ondie\":%d,\"host\":%d,\"total\":%d", + c->ondie, c->host, c->total); + if (c->gave_up > 0) + jw_printf(jw, ",\"gave_up\":%d", c->gave_up); + if (c->impossible > 0) + jw_printf(jw, ",\"impossible\":%d", c->impossible); + jw_raw(jw, "}"); +} + +/* `,"reasons":{"":n,...}` over the reasons any layer is CURRENTLY on. + * Omitted entirely when nothing is on the host path, so a healthy model does + * not carry an object full of zeros. */ +static void ar_write_reasons(json_writer *jw, const pn_attn_residency *c) +{ + if (c->host == 0) + return; + jw_raw(jw, ",\"reasons\":{"); + int first = 1; + for (int i = 1; i < PN_ATTN_HOST_REASON_COUNT; i++) { + if (c->by_reason[i] == 0) + continue; + jw_printf(jw, "%s\"%s\":%d", first ? "" : ",", + pn_attn_host_reason_name((unsigned char)i), c->by_reason[i]); + first = 0; + } + jw_raw(jw, "}"); +} + +void ar_write_summary(json_writer *jw, const pn_model_attn_residency *r) +{ + if (!jw || !r || r->all.total == 0) + return; + jw_raw(jw, ",\"attention\":"); + ar_write_counts(jw, &r->all); +} + +void ar_write_detail(json_writer *jw, const pn_model_attn_residency *r) +{ + if (!jw || !r || r->all.total == 0) + return; + jw_printf(jw, ",\"attention\":{\"ondie\":%d,\"host\":%d,\"total\":%d," + "\"gave_up\":%d,\"impossible\":%d," + "\"host_engagements\":%u,\"ring_alloc_failures\":%u", + r->all.ondie, r->all.host, r->all.total, + r->all.gave_up, r->all.impossible, + r->all.lifetime_host_engagements, + r->all.lifetime_alloc_failures); + ar_write_reasons(jw, &r->all); + jw_raw(jw, "}"); +} diff --git a/src/serve/attn_residency_json.h b/src/serve/attn_residency_json.h new file mode 100644 index 0000000..a90ff8b --- /dev/null +++ b/src/serve/attn_residency_json.h @@ -0,0 +1,63 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file attn_residency_json.h + * @brief Serialises a model's ATTENTION-PATH residency into the JSON both the + * client surface (GET /v1/models) and the operator surface + * (GET /admin/models) emit. + * + * WHY IT IS ON THE WIRE AT ALL + * Same argument as graph_residency_json.h, only sharper. When the on-die + * flash-attention path is unavailable the layer falls back to host-side + * causal GQA softmax on a 2-core host — the exact wall this project spent a + * phase escaping (87.5% of an 8k prefill; fixing it took that prefill from + * 26 minutes to 3.82). The fallback returns the SAME tokens, logs nothing per + * step, and on the depth bench costs 13.02 -> 1.22 tok/s (K=2, depth 4096). + * A user gets correct answers an order of magnitude slower, with no error + * anywhere, and it gets worse the deeper the context runs. + * + * Two states, and `host` is the fault: + * ondie — the flash kernels are running. Good. + * host — this layer is on the host-softmax fallback. `reasons` names why. + * + * `gave_up` is the number that should page someone: a layer whose KV-ring + * allocation spent its retry budget will not try again at this geometry. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_ATTN_RESIDENCY_JSON_H +#define PN_SERVE_ATTN_RESIDENCY_JSON_H + +#include "../models/gpu_attn_path_tracker.h" +#include "json_writer.h" + +/** + * Append `,"attention":{...}` — the client-facing summary. Writes NOTHING when + * the forward has never reached an attention layer (CPU-resident, an + * architecture with no on-die attention path, or a model that has not served a + * request yet), the same omit-when-unknown rule `context_window` follows. + * + * @param jw Open json_writer positioned inside a model object + * @param r Residency snapshot (mr_info.attn) + * + * Thread-safe: yes (writes only to `jw`) + * Reentrant: yes + * Signal-safe: no + */ +void ar_write_summary(json_writer *jw, const pn_model_attn_residency *r); + +/** + * Append `,"attention":{...}` with the per-reason breakdown and the lifetime + * counters — the operator view. A reason no layer is currently sitting on is + * omitted rather than reported as a zero. + * + * Thread-safe: yes (writes only to `jw`) + * Reentrant: yes + * Signal-safe: no + */ +void ar_write_detail(json_writer *jw, const pn_model_attn_residency *r); + +#endif /* PN_SERVE_ATTN_RESIDENCY_JSON_H */ diff --git a/src/serve/base64.c b/src/serve/base64.c new file mode 100644 index 0000000..8f3ac71 --- /dev/null +++ b/src/serve/base64.c @@ -0,0 +1,110 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file base64.c + * @brief Implementation of the Base64 encode/decode in base64.h. + * + * @status Complete; unit-tested (tests/test_base64.c). + * @issues None + * @todo None + */ + +#include "base64.h" + +#include +#include + +static const char B64_ALPHABET[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +char *pn_base64_encode(const unsigned char *src, size_t len) +{ + size_t out_len = ((len + 2) / 3) * 4; + char *out = malloc(out_len + 1); + if (!out) + return NULL; + + size_t o = 0; + size_t i = 0; + while (i + 3 <= len) { + unsigned v = ((unsigned)src[i] << 16) | ((unsigned)src[i + 1] << 8) | + (unsigned)src[i + 2]; + out[o++] = B64_ALPHABET[(v >> 18) & 0x3F]; + out[o++] = B64_ALPHABET[(v >> 12) & 0x3F]; + out[o++] = B64_ALPHABET[(v >> 6) & 0x3F]; + out[o++] = B64_ALPHABET[v & 0x3F]; + i += 3; + } + size_t rem = len - i; + if (rem == 1) { + unsigned v = (unsigned)src[i] << 16; + out[o++] = B64_ALPHABET[(v >> 18) & 0x3F]; + out[o++] = B64_ALPHABET[(v >> 12) & 0x3F]; + out[o++] = '='; + out[o++] = '='; + } else if (rem == 2) { + unsigned v = ((unsigned)src[i] << 16) | ((unsigned)src[i + 1] << 8); + out[o++] = B64_ALPHABET[(v >> 18) & 0x3F]; + out[o++] = B64_ALPHABET[(v >> 12) & 0x3F]; + out[o++] = B64_ALPHABET[(v >> 6) & 0x3F]; + out[o++] = '='; + } + out[o] = '\0'; + return out; +} + +/* Reverse map: alphabet char -> 6-bit value, 0xFF for invalid. Built once. */ +static int b64_value(unsigned char ch) +{ + if (ch >= 'A' && ch <= 'Z') return ch - 'A'; + if (ch >= 'a' && ch <= 'z') return ch - 'a' + 26; + if (ch >= '0' && ch <= '9') return ch - '0' + 52; + if (ch == '+') return 62; + if (ch == '/') return 63; + return -1; +} + +unsigned char *pn_base64_decode(const char *src, size_t *out_len) +{ + if (out_len) + *out_len = 0; + if (!src) + return NULL; + size_t n = strlen(src); + /* A valid base64 body length is a multiple of 4 (padding included). */ + if (n % 4 != 0) + return NULL; + + size_t pad = 0; + if (n >= 1 && src[n - 1] == '=') pad++; + if (n >= 2 && src[n - 2] == '=') pad++; + size_t dec_len = n / 4 * 3 - pad; + + unsigned char *out = malloc(dec_len ? dec_len : 1); + if (!out) + return NULL; + + size_t o = 0; + for (size_t i = 0; i < n; i += 4) { + int q[4]; + for (int k = 0; k < 4; k++) { + char ch = src[i + k]; + if (ch == '=') { + /* Padding is only legal in the final quad's last two slots. */ + if (i + 4 != n || k < 2) { free(out); return NULL; } + q[k] = 0; + } else { + q[k] = b64_value((unsigned char)ch); + if (q[k] < 0) { free(out); return NULL; } + } + } + unsigned v = ((unsigned)q[0] << 18) | ((unsigned)q[1] << 12) | + ((unsigned)q[2] << 6) | (unsigned)q[3]; + if (o < dec_len) out[o++] = (unsigned char)((v >> 16) & 0xFF); + if (o < dec_len) out[o++] = (unsigned char)((v >> 8) & 0xFF); + if (o < dec_len) out[o++] = (unsigned char)(v & 0xFF); + } + if (out_len) + *out_len = dec_len; + return out; +} diff --git a/src/serve/base64.h b/src/serve/base64.h new file mode 100644 index 0000000..d035438 --- /dev/null +++ b/src/serve/base64.h @@ -0,0 +1,42 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file base64.h + * @brief Standard Base64 (RFC 4648, '+' '/' alphabet, '=' padding) encode / + * decode over byte buffers. Used by the session warm-tier (K80 + * serving-ops Phase 7) to carry a binary session snapshot blob inside a + * JSON body: /admin/session/export returns base64, /session/restore + * accepts it. Kept a tiny standalone module (no deps) so both the serve + * and any test can link it. + * + * @status Complete; unit-tested (tests/test_base64.c). + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_BASE64_H +#define PN_SERVE_BASE64_H + +#include + +/** + * Base64-encode `len` bytes of `src`. Returns a malloc'd NUL-terminated string + * (caller frees), or NULL on allocation failure. A zero-length input encodes to + * an empty string. + */ +char *pn_base64_encode(const unsigned char *src, size_t len); + +/** + * Base64-decode NUL-terminated `src` into a freshly malloc'd byte buffer. + * Rejects any non-alphabet character (whitespace included) and malformed + * padding — a corrupt/tampered blob is a clean failure, not silent truncation. + * + * @param src NUL-terminated base64 text. + * @param out_len Out: decoded byte count. + * @return malloc'd bytes (caller frees), or NULL on invalid input / + * allocation failure. A zero-length input decodes to a 1-byte + * allocation with *out_len == 0. + */ +unsigned char *pn_base64_decode(const char *src, size_t *out_len); + +#endif /* PN_SERVE_BASE64_H */ diff --git a/src/serve/batch_sched.c b/src/serve/batch_sched.c new file mode 100644 index 0000000..1233771 --- /dev/null +++ b/src/serve/batch_sched.c @@ -0,0 +1,3138 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file batch_sched.c + * @brief Per-model batch decode scheduler (C3). + * See batch_sched.h for the full design + concurrency model. + * + * Locking recap: + * GPU turnstile (gpu_ticket_next/serving + cv_gpu, under mu) — one GPU + * owner at a time, FIFO: a prefill chunk (+ the final chunk's + * bind), or one driver decode step. Restores the + * one-in-flight-forward guarantee the per-model gen_lock used to + * give (the C2 async phase machine, the shared live SSM buffers / + * d->host_state, the shared GPU KV ring and sc->r_logits all + * assume it). FIFO tickets — not a mutex — so the free-running + * L3 driver and chunk-yielding prefillers alternate fairly + * (chunk → step → chunk); a turn owner never cond-waits and every + * ticket is followed by a release, so no deadlock is possible. + * mu — slot statuses, the token/logits/ring handshakes, the turnstile + * counters. The only mutex; all condvars wait on it. + * + * @status Complete. The resume ladder (exact-prefix append, think-elision, + * system-prefix snapshot, full reset) plus the PN_REQPROF divergence + * dumps that make a miss readable straight out of the serve log, and + * the session-substitution report (bs_slot_session_conflict), and + * per-conversation routing under one session id (bs_slot_reroute). + * @issues A warm resume is only valid while the session's KV rows are still in + * a GPU ring (the host mirror is lazy — Phase 10b/INV3), so + * bs_warm_kv_reusable has to prove it before every warm rung. How many + * sessions can hold a WARM rung at once is therefore a property of the + * decode-slot ring, and it is NOT one — it is however many are + * CONCURRENTLY ACTIVE. rebuild_gpu_map compacts the active slots into + * GPU ring rows [0, n_active), so: + * - sessions whose turns OVERLAP occupy distinct rows and all keep + * their warm KV (measured on a K80 host 2026-08-02, saber-orch, two live + * sessions, both resuming at ~1.05 s TTFT); + * - sessions that run STRICTLY SEQUENTIALLY each compact alone to row + * 0 and therefore overwrite each other's rows — every turn, so both + * fall to the system-prefix rung and their TTFT goes linear in + * conversation depth again (measured 2.2 -> 3.4 -> 4.3 s). + * The system-prefix rung is genuinely multi-session (its snapshots are + * per-slot host deep copies), so N sessions stay warm AT THAT TIER + * regardless of interleaving. + * @todo None + */ + +#include "batch_sched.h" + +#include "request_profile.h" + +#include +#include +#include +#include +#include + +#include "../core/engine.h" /* gpt2_bytes_decode (diagnostic dumps) */ +#include "../core/mem/pn_mem_state.h" +#include "../core/mem/pn_memwatch.h" +#include "tokenizer/tokenizer.h" + +/* The qwen35moe header is needed on ALL builds now: the SYS1-batched + * system-prefix snapshot/restore primitives (pn_qwen35moe_state_snapshot & + * co.) are pure host code, unit-tested on the Mac. The GPU slot API it also + * declares stays behind USE_CUDA at the call sites: on non-CUDA builds the + * scheduler is disabled (bs_enabled() false, bs_create NULL, no driver + * thread), so those calls never compile in. */ +#include "../models/qwen35moe.h" + +/* Phase 10b: forward decl — defined with the SYS1 snapshot helpers below. */ +static void bs_sys_snap_unref(bs_slot *sl); + +/* ============================================================ + * Feature gate + * ============================================================ */ + +bool bs_enabled(void) +{ +#ifndef USE_CUDA + return false; /* batch decode requires the CUDA backend */ +#else + /* DEFAULT ON. This is the path the fleet deploys, and the only one that + * commits the CLIENT RE-RENDER of a turn (sg_project_client_rerender) rather + * than the raw generated ids. The single-user path commits what the model + * emitted, so a tool call GENERATED as ordinary text ("<","tool","_call",">") + * but RE-RENDERED as the special id diverges from the committed + * prefix — and with no lcp-rewind on this arch that collapses the resume + * point to the system boundary on EVERY turn. + * + * Measured on the K80 test machine 2026-08-28, same probe both ways: single-user + * collapsed 9 of 9 turns, wasting 1 -> 839 tokens and growing; batched + * collapsed only turn 1 (no history yet) and appended the other 5. Defaulting + * this OFF meant every measurement taken through it described a path we do + * not deploy. QWEN35_BATCH_SLOTS=0 still forces the old behaviour. */ + static int cached = -1; + if (cached < 0) { + const char *env = getenv("QWEN35_BATCH_SLOTS"); + cached = (env && env[0] == '0') ? 0 : 1; + fprintf(stderr, "[batch_sched] multi-user batched decode %s%s\n", + cached ? "enabled" : "DISABLED", + env ? " (QWEN35_BATCH_SLOTS)" : " (default)"); + } + return (bool)cached; +#endif +} + +/* ============================================================ + * GPU-ownership turnstile (see batch_sched.h) — FIFO tickets under mu. + * + * acquire: take a ticket, wait until served. The wait is UNCONDITIONAL on + * shutdown by design: a taken ticket must always be served-then-released or + * the turnstile would skip/starve later tickets. This is safe — every turn + * owner (a prefill chunk, a bind, a decode step) completes in bounded time + * and releases, so the turn always arrives; shutdown is checked by callers + * after they own (and release) the turn. + * ============================================================ */ + +static void bs_gpu_acquire_locked(pn_batch_sched *sched) +{ + uint64_t my = sched->gpu_ticket_next++; + while (sched->gpu_ticket_serving != my) + pthread_cond_wait(&sched->cv_gpu, &sched->mu); +} + +static void bs_gpu_release_locked(pn_batch_sched *sched) +{ + sched->gpu_ticket_serving++; + pthread_cond_broadcast(&sched->cv_gpu); +} + +static void bs_gpu_acquire(pn_batch_sched *sched) +{ + pthread_mutex_lock(&sched->mu); + bs_gpu_acquire_locked(sched); + pthread_mutex_unlock(&sched->mu); +} + +static void bs_gpu_release(pn_batch_sched *sched) +{ + pthread_mutex_lock(&sched->mu); + bs_gpu_release_locked(sched); + pthread_mutex_unlock(&sched->mu); +} + +/* ============================================================ + * Driver thread — the heart of C3 + * ============================================================ */ + +#ifdef USE_CUDA + +/* Per-step driver timing to stderr (M6 instrumentation), QWEN35_BS_STATS=1. */ +static int bs_stats_on(void) +{ + static int v = -1; + if (v < 0) { + const char *e = getenv("QWEN35_BS_STATS"); + v = (e && e[0] == '1') ? 1 : 0; + } + return v; +} + +static double bs_now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1e3 + (double)ts.tv_nsec / 1e6; +} + +/* + * Driver design — "dynamic-width step" (Phase D2): + * + * The driver compacts the active (BOUND/DECODE) lanes into a contiguous + * GPU index range [0, n_active) so `pn_qwen35moe_decode_slots` runs over + * only the live users. Idle lanes pay zero GPU compute. + * + * gpu_map[gpu_idx] = scheduler slot index. The mapping is built/rebuilt + * whenever active_set_changed is set (a user joined or left); rebuild_gpu_map + * realizes the (arbitrary) permutation that compacts the active lanes to + * [0, n_active), using the reserved scratch slot at index `cap` to break + * cycles (the engine is bound with cap+1 slots). Steady state (no + * join/leave) has zero overhead. + * + * Correctness: + * - BOUND/DECODE lanes: carry their real token; BOUND → DECODE on first + * step. + * - PREFILL lanes: excluded from the compacted set (never stepped); the + * prefill's own GPU work is serialized by the turnstile. + * - IDLE lanes: excluded from the compacted set; never passed to the GPU. + * - A slot released mid-step has left DECODE before the driver reads mu + * again; its GPU slot's logits are discarded at deposit time. + * - A newly-BOUND slot (continuous batching join) is included in the + * NEXT step after its bind completes (bind sets active_set_changed and + * signals cv_work). The bind itself owns the GPU turn so no + * interleaving with a step is possible. + * - A slot that turns IDLE during the step (released by its owner before + * the driver re-acquires mu for deposit): the deposit loop checks + * status == BS_SLOT_DECODE before writing logits, so it is skipped + * harmlessly — the slot's next owner will find it IDLE and its state + * reset on the next bs_prefill_begin. + * + * The step runs owning the GPU turn (no prefill can overlap) with mu + * dropped while the GPU is busy (request threads keep posting/releasing). + */ + +/* Gate: may the driver run a step right now? Under sched->mu. + * Returns the number of active (BOUND/DECODE) slots if a step is allowed, + * 0 if blocked. The caller uses this count as n_active for the compacted + * step so it does not need to scan again. */ +static int gate_ok(pn_batch_sched *sched) +{ + int n_active = 0; + if (!sched->slots_gpu_init) + return 0; /* no lane registered with the GPU yet */ + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + switch (sl->status) { + case BS_SLOT_PREFILL: + /* PREFILL no longer closes the gate (pre-L2 it did): since D2 + * the lane is never in the compacted gpu_map (never stepped), + * and GPU exclusivity against the prefill's own forwards is the + * turnstile's job — the prefiller owns (or is FIFO-queued for) + * the GPU turn for each chunk. Blocking here would defeat the + * L2 chunk-yield windows. */ + break; + case BS_SLOT_DECODE: + if (sl->finished) + break; /* driver stopped it (EOS / budget); + * owner is draining — not stepped */ + if (!sl->token_ready) + return 0; /* lockstep (legacy mode): wait for the + * owner. In driver-sampling mode the + * driver re-arms this itself, so it + * never blocks the gate. */ + n_active++; + break; + case BS_SLOT_BOUND: + if (sl->finished) + break; /* bound with a zero budget + * (max_tokens == 1): nothing to step */ + n_active++; /* first step: no token needed yet */ + break; + default: + break; /* IDLE: excluded from active set */ + } + } + return n_active; /* 0 ↔ nothing to step */ +} + +/* Rebuild the GPU-index → scheduler-slot compacted mapping (GPU turn owned, + * mu held). Called when active_set_changed is set (a user joined, left, or + * finished). + * + * Compacts all active (BOUND/DECODE) scheduler slots to GPU indices [0, n_active). + * The new GPU-index order mirrors the scheduler slot order (stable, lowest first). + * + * For each active slot: + * - Its CURRENT GPU index = the old gpu_map entry (for DECODE slots), + * or the scheduler slot index (for newly-BOUND slots, whose bind call + * in bs_slot_bind set g->slot_state[sched_slot_idx]). + * - Its TARGET GPU index = its rank in the compacted list (0, 1, 2, ...). + * - If current == target: no move needed (slot_state already correct). + * - If current != target: call pn_qwen35moe_decode_slot_move(current, target, + * cap) to D2D copy the SSM state from conv_slots[current] to conv_slots[target] + * and update g->slot_state[target] — without touching the stale host mirror. + * + * Moves realize an ARBITRARY permutation (both downward compaction and the + * upward moves / cycles that concurrent multi-join produces): see the body + * for the greedy place-into-free + cycle-break algorithm. A move that is + * genuinely upward is not a corruption — the old "src >= dst only" refusal + * was the K>=5 empty-output bug. + * + * Returns n_active (≥ 1; the gate already ensured this), or -1 on failure. + * Precondition: GPU turn owned, mu held. + * + * A slot the driver marked `finished` (EOS / budget, L3) is inactive here — + * it drops out of the map exactly like a released slot. */ +static int slot_is_active(const bs_slot *sl) +{ + return (sl->status == BS_SLOT_BOUND || sl->status == BS_SLOT_DECODE) && + !sl->finished; +} + +static int rebuild_gpu_map(pn_batch_sched *sched) +{ + /* Build the target compacted list: all active sched slots in order. */ + int target_sched[BS_MAX_SLOTS]; /* target_sched[gpu_rank] = sched_slot */ + int n_active = 0; + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + if (slot_is_active(sl)) + target_sched[n_active++] = i; + } + + /* Realize the (arbitrary) permutation that maps each active slot from its + * CURRENT GPU index (sl->gpu_idx — where its live decode state resides, + * set at bind to a free GPU index and maintained by every move below) to + * its TARGET GPU index (its rank in the compacted list). + * + * The old loop assumed every move packs DOWN (src >= dst) and refused any + * upward move. That invariant does NOT hold: >=5 joiners binding + * concurrently at the highest free GPU indices (7,6,5,4,3) before the + * driver rebuilds get compacted to ranks (0,1,2,3,4), so the slot at + * gpu 3 must move UP to rank 4 (a gpu 3<->4 swap) — the refusal returned + * -1 and decode_slots then produced nothing for every user (the K>=5 + * "all users emit empty content" bug). A correct compaction must handle + * upward moves and cycles. + * + * occupied[p] = the active sched slot currently living at GPU index p, or + * -1 if free. Realize the permutation greedily: repeatedly move any slot + * whose target rank is currently free into place (that frees its old + * index, unblocking others); when no such slot exists the remaining + * unplaced slots form a pure cycle, so break it by parking one slot at a + * free GPU index and continue. Every move D2D-copies the SSM state and + * re-seeds the KV ring from the (current) host mirror, so a park + final + * move is correct. + * + * A free GPU index is ALWAYS available for the cycle break: users occupy + * only [0, cap), and the engine is bound with cap+1 slots, so index `cap` + * is a permanent scratch slot the decode step never touches (it runs over + * [0, n_active) with n_active <= cap). This is essential at n_active == + * cap: 8 joiners binding concurrently at the highest free indices produce + * a full reversal (gpu 7..0 -> ranks 0..7 = four 2-cycles) with no free + * index in [0, cap) — the scratch at `cap` breaks it. Placed slots + * (gpu_idx == rank) are never disturbed again. */ + const int scratch = sched->cap; /* reserved cycle-break slot */ + int occupied[BS_MAX_SLOTS]; + for (int p = 0; p <= scratch; p++) + occupied[p] = -1; + for (int rank = 0; rank < n_active; rank++) { + int i = target_sched[rank]; + int g = sched->slots[i].gpu_idx; + if (g < 0 || g >= sched->cap) { + fprintf(stderr, "[batch_sched] rebuild_gpu_map: sched slot %d has " + "bad GPU index %d\n", i, g); + sched->active_set_changed = false; + return -1; + } + occupied[g] = i; + } + + for (;;) { + int made_progress = 0; + int all_placed = 1; + for (int rank = 0; rank < n_active; rank++) { + int i = target_sched[rank]; + int src = sched->slots[i].gpu_idx; + if (src == rank) + continue; /* already at its target */ + all_placed = 0; + if (occupied[rank] >= 0) + continue; /* target still held; try later */ + if (pn_qwen35moe_decode_slot_move(sched->model, src, rank, + sched->cap + 1) != 0) { + fprintf(stderr, "[batch_sched] rebuild_gpu_map: slot_move " + "failed (sched=%d gpu %d→%d cap=%d)\n", i, src, rank, + sched->cap); + sched->active_set_changed = false; + return -1; + } + occupied[src] = -1; + occupied[rank] = i; + sched->slots[i].gpu_idx = rank; + made_progress = 1; + } + if (all_placed) + break; + if (!made_progress) { + /* Pure cycle: every unplaced slot's target is held by another + * unplaced slot. Park the first unplaced slot at a free GPU + * index (the reserved scratch at `cap` is always free) to break + * the cycle. */ + int tmp = -1; + for (int p = 0; p <= scratch; p++) + if (occupied[p] < 0) { tmp = p; break; } + if (tmp < 0) { + fprintf(stderr, "[batch_sched] rebuild_gpu_map: permutation " + "cycle with no free GPU slot (n_active=%d cap=%d)\n", + n_active, sched->cap); + sched->active_set_changed = false; + return -1; + } + for (int rank = 0; rank < n_active; rank++) { + int i = target_sched[rank]; + int src = sched->slots[i].gpu_idx; + if (src == rank) + continue; + if (pn_qwen35moe_decode_slot_move(sched->model, src, tmp, + sched->cap + 1) != 0) { + fprintf(stderr, "[batch_sched] rebuild_gpu_map: park move " + "failed (sched=%d gpu %d→%d cap=%d)\n", i, src, tmp, + sched->cap); + sched->active_set_changed = false; + return -1; + } + occupied[src] = -1; + occupied[tmp] = i; + sched->slots[i].gpu_idx = tmp; + break; + } + } + } + + /* Commit the new map: gpu rank -> sched slot (each slot's gpu_idx is now + * exactly its rank, set by the moves above). */ + for (int rank = 0; rank < n_active; rank++) { + sched->gpu_map[rank] = target_sched[rank]; + sched->slots[target_sched[rank]].gpu_idx = rank; + } + sched->n_active_gpu = n_active; + sched->active_set_changed = false; + return n_active; +} + +/* Deposit one slot's step failure and wake its owner (under mu). Serves + * both modes: legacy owners see logits==NULL + logits_ready; L3 owners see + * step_error after draining the ring. */ +static void driver_error_slot(pn_batch_sched *sched, bs_slot *sl) +{ + sl->logits = NULL; + sl->logits_ready = true; + sl->step_error = true; + sl->finished = true; + sched->active_set_changed = true; + pthread_cond_signal(&sl->cv); +} + +/* Advance this slot's tool-grammar (if any) over a FORCE-INJECTED token id — + * the driver-side twin of the single-user injection's sg_tg_step. A normally + * SAMPLED token is advanced inside pn_sample_next_token; an injected "" + * close is not sampled, so its text must be fed to the grammar here so the FSM + * walk stays aligned (and a live forced walk sees reasoning end). No-op when + * the slot carries no grammar hook. */ +static void driver_grammar_advance_injected(bs_slot *sl, uint32_t tok) +{ + const pn_tool_grammar_hook *tgh = sl->sp.tool_grammar; + if (tgh && tgh->advance && tgh->st && tgh->token_text) { + const char *txt = tgh->token_text(tgh->user, tok); + if (txt) tgh->advance(tgh->st, txt); + } + /* An injected token is not sampled, so the logprob sink never sees it — and + * the consumer pairs records with tokens BY POSITION, so a missing record + * would shift every later token's logprobs onto the wrong text. Record the + * documented NaN placeholder to keep the two sequences aligned (see the + * injected-token contract on pn_logprob_sink). No-op when the request did + * not ask for logprobs. */ + const pn_logprob_sink *lp = sl->sp.logprobs; + if (lp && lp->emit) + lp->emit(lp->user, tok, NAN, NULL, NULL, 0); +} + +/* ============================================================ + * Full-logits arming window, driver side + * + * The design, the invariant it maintains and why arming one token early or one + * token late are both wrong are documented once, in serve_generation.c above + * sg_grammar_arm_window_on(). Read that first. This is the batched half. + * + * The batched path is the STRAIGHTFORWARD half, and it is worth saying why: the + * driver samples a slot, the sampler advances that slot's grammar over the token + * it drew, the reasoning force-close injects and advances in the same breath — + * and only after all of that does the driver leave the deposit loop and issue + * the next decode_slots. One thread, one critical section, so arming at the end + * of the deposit is provably before the read and provably after every mutation. + * There is no hole here and no latch is needed. + * ============================================================ */ + +/* PN_GRAMMAR_ARM_WINDOW=1 — the driver maintains the window (default OFF: the + * owner armed the row for the whole turn and the driver must not touch it). */ +static int bs_grammar_arm_window_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("PN_GRAMMAR_ARM_WINDOW"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* PN_GRAMMAR_ARM_STALE=1 — FALSIFIER (see serve_generation.c). Arms from the + * walk state as it stood BEFORE this step's advance, reproducing the one-token + * -late ordering, so a gate can watch pn_sampler_grammar_sparse_rows go + * non-zero and know its detector is not vacuous. Never set in production. */ +static int bs_grammar_arm_stale_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("PN_GRAMMAR_ARM_STALE"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/** + * Arm this slot's full-vocabulary logits row for the next decode step. + * + * Call at the END of the slot's deposit, after the sampler's advance and after + * any injected-token advance, and before the driver issues the next step. + * + * @param sched owning scheduler (for the model's state discriminator) + * @param sl the slot just sampled + * @param active_before the walk's mask-active value read BEFORE this step's + * advance; used only by the PN_GRAMMAR_ARM_STALE falsifier + * + * Thread-safe: no — driver thread only, under sched->mu + */ +static void driver_arm_full_logits(pn_batch_sched *sched, bs_slot *sl, + int active_before) +{ + if (!bs_grammar_arm_window_on()) return; + if (!sl->state || !pn_qwen35moe_owns_state(sched->model)) return; + const pn_tool_grammar_hook *tgh = sl->sp.tool_grammar; + int need = ((sl->sp.logit_bias && sl->sp.n_logit_bias > 0) || + sl->sp.logprobs != NULL) ? 1 : 0; + if (!need && tgh && tgh->st) + need = bs_grammar_arm_stale_on() + ? active_before + : (tgh->refresh ? tgh->refresh(tgh->st) : 1); + pn_qwen35moe_state_set_full_logits(sl->state, need); +} + +/* AUTO reasoning-budget force-close, PER SLOT, in the driver's sampler — the + * batched twin of sg_run_completion's cap. Returns the token this step must + * push to the ring + step into the state: + * + * - FORCE-CLOSE injection pending (reason_close_i < reason_close_n): return + * the next queued "" id (do NOT sample; the caller already skips + * sampling when *inject_out is set), advance the grammar over it, and when + * the queue drains leave and stamp the grammar reasoning-end. + * - otherwise: `sampled` is the token pn_sample_next_token just produced. + * Count it toward the budget iff the slot is inside ; latch the trip + * off the PRE-count think state (mirrors 8680b89's fix so a self-close on + * the trip token can't starve the cap), and if the budget is reached while + * still open — with no live tool-call skeleton — queue the close for the + * next step(s). If `sampled` is itself the "" close, mark the block + * self-closed instead (no double-close). + * + * *inject_out is set true when the returned token is an injected close (the + * caller then must NOT count it against the grammar again — it was fed here). + * budget<=0 / not-think slots never enter here (guarded by the caller). */ +static uint32_t driver_reason_step(bs_slot *sl, uint32_t sampled, + const pn_tool_grammar_hook *tgh, + bool *inject_out) +{ + *inject_out = false; + /* Drain a pending force-close injection first. */ + if (sl->reason_close_i < sl->reason_close_n) { + uint32_t close_tok = sl->reason_close[sl->reason_close_i++]; + driver_grammar_advance_injected(sl, close_tok); + *inject_out = true; + if (sl->reason_close_i >= sl->reason_close_n) { + /* Whole close emitted — reasoning region is over. The grammar's own + * advance over "" (driver_grammar_advance_injected above) + * self-detects reasoning-end, so a live forced walk would compel the + * tool-call opener on content from the very next char. */ + sl->reason_in_think = false; + /* …and that is exactly the frame-capture defect: THIS close was + * INJECTED by the budget, not chosen by the model, so the model is + * still mid-sentence. Tell the grammar so it grants a bounded + * landing window instead of forcing a frame around the unfinished + * thought. Runs AFTER the advance above, whose self-detection has + * already set reasoning_ended — this adds the window on top. NULL + * hook (or a dialect without a window) → pre-fix behavior. */ + if (tgh && tgh->reasoning_end_injected && tgh->st) + tgh->reasoning_end_injected(tgh->st); + } + return close_tok; + } + + if (!sl->reason_in_think) + return sampled; + + /* A token sampled while inside counts against the budget. Latch + * the trip HERE, off the pre-count in-think state, so a budget reached on + * the very token that also self-closes still fires the cap (the 8680b89 + * starve bug on the single-user path). A tool-call skeleton being emitted + * must NOT be force-closed mid-call — the same guard sg_tg_in_call gives the + * single-user path. */ + sl->reason_toks++; + bool in_call = (tgh && tgh->in_call && tgh->st && tgh->in_call(tgh->st)); + bool self_closed = (sl->reason_close_n == 1 && + sampled == sl->reason_close[0]); + if (self_closed) { + sl->reason_in_think = false; /* model concluded on its own */ + return sampled; + } + if (sl->reason_budget > 0 && !sl->reason_forced && + sl->reason_toks >= sl->reason_budget && !in_call) { + sl->reason_forced = true; + sl->reason_close_i = 0; /* arm the injection for the next step(s) */ + } + return sampled; +} + +static void *driver_thread(void *arg) +{ + pn_batch_sched *sched = arg; + + /* Compacted active-set arrays — sized to sched->cap. */ + uint32_t tokens[BS_MAX_SLOTS]; + pn_state *states[BS_MAX_SLOTS]; + float *lo[BS_MAX_SLOTS]; + /* Maps compacted GPU index → scheduler slot index (snapshot copy). */ + int gpu_map_snap[BS_MAX_SLOTS]; + + const int stats = bs_stats_on(); + uint64_t step_no = 0; + double t_prev_done = 0.0; /* end of the previous deposit (M6) */ + + pthread_mutex_lock(&sched->mu); + for (;;) { + /* ── Wait for the gate (predicate re-checked under mu). ── */ + while (!sched->shutdown && !gate_ok(sched)) + pthread_cond_wait(&sched->cv_work, &sched->mu); + if (sched->shutdown) + break; + double t_gate = stats ? bs_now_ms() : 0.0; + + /* ── Take the GPU turn — FIFO behind any prefiller already queued + * (initial join or L2 chunk re-acquire), so the driver can never + * barge a joiner out and a joiner can never be starved by the + * free-running L3 step loop. cv_gpu waits release mu, so owners + * keep posting/releasing meanwhile. ── */ + bs_gpu_acquire_locked(sched); + + /* Re-check the gate: it may have closed while we waited for the + * turn (a release, an error). Back off by releasing the turn. */ + int n_active = gate_ok(sched); + if (sched->shutdown || !n_active) { + bs_gpu_release_locked(sched); + if (sched->shutdown) + break; + continue; + } + + /* ── Rebuild the compacted GPU map if the active set changed (a + * user joined, left, or finished since the last step). GPU turn + * owned + mu held. On failure, error out all active slots. ── */ + if (sched->active_set_changed) { + int rn = rebuild_gpu_map(sched); + if (rn < 0) { + /* Hard GPU failure: error every active slot (BOUND included + * — its owner is parked in await_first/pop_token) so owners + * unblock and release; a later joiner triggers a fresh + * rebuild attempt. */ + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + if (sl->status == BS_SLOT_DECODE || + sl->status == BS_SLOT_BOUND) + driver_error_slot(sched, sl); + } + bs_gpu_release_locked(sched); + continue; + } + n_active = rn; + } + + /* ── Snapshot the compacted step (mu held, turn owned). + * Build tokens[]/states[] over GPU indices [0, n_active). ── */ + int snap_n = 0; + for (int g = 0; g < sched->n_active_gpu; g++) { + int i = sched->gpu_map[g]; + if (i < 0 || i >= sched->cap) + continue; /* stale map entry (should not happen) */ + bs_slot *sl = &sched->slots[i]; + states[snap_n] = sl->state; + if (sl->status == BS_SLOT_BOUND && !sl->finished) { + sl->status = BS_SLOT_DECODE; /* first step */ + sl->token_ready = false; + tokens[snap_n] = sl->next_token; + } else if (sl->status == BS_SLOT_DECODE && !sl->finished) { + sl->token_ready = false; + tokens[snap_n] = sl->next_token; + } else { + /* Slot went IDLE/finished between gate_ok and the snapshot + * (owner released it); skip by not advancing snap_n. */ + continue; + } + gpu_map_snap[snap_n] = i; + snap_n++; + } + /* snap_n may be < n_active if a release raced us; that's fine. */ + if (snap_n == 0) { + /* Nothing left to step (all active slots were released). */ + bs_gpu_release_locked(sched); + continue; + } + + /* ── GPU step: turn owned (no prefill can overlap), mu dropped + * (owners keep draining rings / releasing meanwhile). ── */ + pthread_mutex_unlock(&sched->mu); + double t_launch = stats ? bs_now_ms() : 0.0; + memset(lo, 0, sizeof(lo)); + int rc = pn_qwen35moe_decode_slots(sched->model, tokens, states, + snap_n, lo); + /* Phase 0: split the step into the GPU forward and the host-side + * per-slot deposit+sampling that follows it, so `samp_ms` can be read + * against K directly (the per-slot sampler is sequential, so it is + * expected to scale with snap_n). Stamped before re-taking mu so the + * lock wait is not charged to either. */ + double t_dec = stats ? bs_now_ms() : 0.0; + pthread_mutex_lock(&sched->mu); + + if (rc != 0) + fprintf(stderr, "[batch_sched] decode_slots failed (n=%d); " + "erroring out active slots\n", snap_n); + + /* ── Deposit and wake each owner. Use gpu_map_snap to reverse the + * GPU-index → sched-slot mapping. A slot released mid-step is + * no longer DECODE and is skipped. + * + * L3 (driver_sample): append the just-stepped token to the + * slot's repeat window, sample the next token from its + * (host-side, sparse top-k) logits — sequentially per slot, the + * documented deterministic order — push it to the output ring + * and re-arm token_ready, so the next step needs NOTHING from + * the owners. EOS/<|im_end|>/budget ends the slot's stepping + * (finished; the owner drains and releases). + * + * Legacy: deposit the logits pointer; the owner samples. ── */ + for (int g = 0; g < snap_n; g++) { + int i = gpu_map_snap[g]; + bs_slot *sl = &sched->slots[i]; + if (sl->status != BS_SLOT_DECODE) + continue; + if (rc != 0) { + driver_error_slot(sched, sl); + continue; + } + /* PC-batched: tokens[g] was just stepped INTO sl->state (pos + * advanced by 1). If this slot is armed for caching, append it to + * the committed-token log so ct_n == state->pos stays invariant. + * ct is pre-sized to prompt + budget at bind, so this never + * overflows; on the impossible overflow, disarm (finalize will + * then no-op → this session re-full-prefills next turn). */ + if (sl->armed_sid[0]) { + if (sl->ct && sl->ct_n < sl->ct_cap) + sl->ct[sl->ct_n++] = tokens[g]; + else + sl->armed_sid[0] = '\0'; + } + if (!sched->driver_sample) { + sl->logits = lo[g]; + sl->logits_ready = true; + pthread_cond_signal(&sl->cv); + continue; + } + /* L3 in-driver sampling. */ + if (sl->max_recent > 0 && sl->recent) { + if (sl->n_recent < sl->max_recent) { + sl->recent[sl->n_recent++] = tokens[g]; + } else { + memmove(sl->recent, sl->recent + 1, + (size_t)(sl->max_recent - 1) * sizeof(uint32_t)); + sl->recent[sl->max_recent - 1] = tokens[g]; + } + } + const pn_tool_grammar_hook *tgh = sl->sp.tool_grammar; + /* AUTO reasoning-budget force-close (per slot). When a "" + * close is queued (the budget tripped on a prior step) inject it + * WITHOUT sampling — advancing the grammar over it — so the KV/SSM + * steps over the close exactly as if the model had emitted it. + * Otherwise sample normally, then account the token toward the + * budget (arming the injection when it trips). Slots with no budget + * are byte-identical: driver_reason_step returns `sampled` untouched + * (reason_in_think stays false). */ + bool injected = false; + uint32_t next; + /* PN_GRAMMAR_ARM_STALE falsifier only: the walk's mask-active value + * BEFORE this step's advance — the stale reading the window exists + * to avoid. Not read on the shipping path. */ + int active_before = (bs_grammar_arm_stale_on() && tgh && + tgh->refresh && tgh->st) + ? tgh->refresh(tgh->st) : 0; + if (sl->reason_close_i < sl->reason_close_n) { + next = driver_reason_step(sl, 0, tgh, &injected); + } else { + next = pn_sample_next_token(lo[g], sched->n_vocab, + &sl->sp, sl->recent, + sl->n_recent); + next = driver_reason_step(sl, next, tgh, &injected); + } + /* Every mutation this step can make to the grammar walk has now + * happened (the sampler advanced over a drawn token; the force-close + * advanced over an injected one). Arm the next step's row from the + * walk as it now stands — the driver issues that step itself, still + * holding mu, so this write provably precedes the read. */ + driver_arm_full_logits(sched, sl, active_before); + if (!sl->ring || sl->ring_w >= sl->ring_cap) { + /* Cannot happen (ring_cap == bind budget); defensive. */ + driver_error_slot(sched, sl); + continue; + } + sl->ring[sl->ring_w++] = next; + sl->budget--; + /* Grammar completion-stop (batched): the sampler (or the injection + * above) just advanced this slot's grammar by `next`; if that closed + * a whole tool call, finish the slot NOW so a forced call terminates + * right after ""/"" instead of running to + * budget (the batched twin of the single-user break-on-complete). + * No-op when the slot carries no grammar hook. */ + bool grammar_done = (tgh && tgh->complete && tgh->st && + tgh->complete(tgh->st)); + /* Record the OUTCOME of a force-closed turn while the grammar hook + * is still live and held under mu: bs_slot_release reports it, and + * by then the request may have freed the hook. */ + if (grammar_done && sl->reason_forced) + sl->reason_forced_call = true; + if ((int)next == sched->eos_id || + (int)next == sched->im_end_id || sl->budget <= 0 || + grammar_done) { + sl->finished = true; + sched->active_set_changed = true; + } else { + sl->next_token = next; + sl->token_ready = true; + } + pthread_cond_signal(&sl->cv); + } + bs_gpu_release_locked(sched); + + if (stats) { + double t_done = bs_now_ms(); + fprintf(stderr, "[bs] step=%llu n=%d dead_ms=%.1f " + "turn_wait_ms=%.1f step_ms=%.1f dec_ms=%.1f " + "samp_ms=%.1f\n", + (unsigned long long)step_no++, snap_n, + t_prev_done > 0.0 ? t_gate - t_prev_done : 0.0, + t_launch - t_gate, t_done - t_launch, + t_dec - t_launch, t_done - t_dec); + t_prev_done = t_done; + } + } + + /* Shutdown: wake every parked owner (their wait predicate checks + * sched->shutdown). bs_destroy broadcasts too — belt and braces. */ + for (int i = 0; i < sched->cap; i++) + pthread_cond_broadcast(&sched->slots[i].cv); + pthread_mutex_unlock(&sched->mu); + return NULL; +} + +/* ============================================================ + * Lazy GPU lane registration + * + * Called from bs_slot_bind with the GPU turn owned (so no other prefill, + * bind or decode step can interleave — the double-entry race that wedged the + * K80 is structurally impossible: the second binder queues on the turnstile + * for its whole prefill and sees slots_gpu_init == true). Registers every lane's + * pn_state* with the GPU (allocates the slot-major conv/ssm buffers once and + * seeds each lane) so decode_slots finds a valid slot_state[i] for every i. + * + * Fail-soft: a bind error propagates -1 immediately; nothing waits on it. + * ============================================================ */ +static int bs_lazy_init_gpu(pn_batch_sched *sched) +{ + /* Bind cap+1 engine slots: the extra top index is the rebuild_gpu_map + * cycle-break scratch (users occupy [0, cap); decode never steps it). */ + for (int i = 0; i < sched->cap; i++) { + if (pn_qwen35moe_decode_slot_bind(sched->model, + sched->slots[i].state, + i, sched->cap + 1) != 0) { + fprintf(stderr, "[batch_sched] lazy gpu init: bind failed " + "(slot=%d cap=%d)\n", i, sched->cap); + return -1; + } + } + pthread_mutex_lock(&sched->mu); + sched->slots_gpu_init = true; /* driver reads this under mu */ + pthread_mutex_unlock(&sched->mu); + return 0; +} +#endif /* USE_CUDA — driver_thread and helpers */ + +/* ============================================================ + * Lifecycle + * ============================================================ */ + +pn_batch_sched *bs_create(pn_model *model, int cap, const char *model_name) +{ +#ifndef USE_CUDA + (void)model; (void)cap; (void)model_name; + return NULL; /* batch decode requires USE_CUDA */ +#else + if (!model || cap <= 0 || cap > BS_MAX_USERS) + return NULL; /* BS_MAX_SLOTS reserves the top index as scratch */ + + /* Clamp to the memory-pressure concurrency ceiling up front so the + * per-bind ladder guard can never refuse the lazy init later (which + * would have turned every request into a 500). We bind cap+1 engine + * slots (the extra one is the rebuild scratch), so leave room for it + * under the ceiling: cap ≤ ceiling-1. */ + int ceiling = pn_qwen35moe_gpu_ladder_max_slots(); + if (ceiling > 1 && cap > ceiling - 1) { + fprintf(stderr, "[batch_sched] capping slots %d -> %d " + "(memory-pressure ladder ceiling %d, incl. 1 scratch)\n", + cap, ceiling - 1, ceiling); + cap = ceiling - 1; + } + + pn_batch_sched *sched = calloc(1, sizeof(*sched)); + /* Stamp the log name BEFORE the driver thread starts, so a force-close + * logged by an early step already has it (calloc left it empty). */ + if (sched && model_name && model_name[0]) + snprintf(sched->model_name, sizeof(sched->model_name), "%s", model_name); + if (!sched) + return NULL; + + sched->model = model; + sched->cap = cap; + + /* GPU turnstile: ticket 0 is the first served. */ + sched->gpu_ticket_next = 0; + sched->gpu_ticket_serving = 0; + + /* L3 — in-driver sampling (default ON; QWEN35_BS_DRIVER_SAMPLE=0 + * restores the legacy owner-side lockstep). */ + { + const char *env = getenv("QWEN35_BS_DRIVER_SAMPLE"); + sched->driver_sample = !(env && env[0] == '0'); + } + sched->n_vocab = model->config.n_vocab; + sched->eos_id = model->tokenizer ? ft_eos_id(model->tokenizer) : -1; + sched->im_end_id = model->tokenizer + ? ft_token_to_id(model->tokenizer, "<|im_end|>") + : -1; + + if (pthread_cond_init(&sched->cv_gpu, NULL) != 0) { + free(sched); return NULL; + } + if (pthread_mutex_init(&sched->mu, NULL) != 0) { + pthread_cond_destroy(&sched->cv_gpu); free(sched); return NULL; + } + if (pthread_cond_init(&sched->cv_work, NULL) != 0) { + pthread_mutex_destroy(&sched->mu); + pthread_cond_destroy(&sched->cv_gpu); free(sched); return NULL; + } + if (pthread_cond_init(&sched->cv_idle, NULL) != 0) { + pthread_cond_destroy(&sched->cv_work); + pthread_mutex_destroy(&sched->mu); + pthread_cond_destroy(&sched->cv_gpu); free(sched); return NULL; + } + + for (int i = 0; i < cap; i++) { + bs_slot *sl = &sched->slots[i]; + sl->slot_idx = i; + sl->status = BS_SLOT_IDLE; + sl->gpu_idx = -1; + if (pthread_cond_init(&sl->cv, NULL) != 0) { + /* Roll back already-initialised condvars. */ + for (int j = 0; j < i; j++) + pthread_cond_destroy(&sched->slots[j].cv); + pthread_cond_destroy(&sched->cv_idle); + pthread_cond_destroy(&sched->cv_work); + pthread_mutex_destroy(&sched->mu); + pthread_cond_destroy(&sched->cv_gpu); + free(sched); + return NULL; + } + sl->state = pn_state_new(model); + if (!sl->state) { + /* Free everything allocated so far. */ + for (int j = 0; j <= i; j++) { + if (sched->slots[j].state) + pn_state_free(sched->slots[j].state); + if (j < i) + pthread_cond_destroy(&sched->slots[j].cv); + } + pthread_cond_destroy(&sched->slots[i].cv); + pthread_cond_destroy(&sched->cv_idle); + pthread_cond_destroy(&sched->cv_work); + pthread_mutex_destroy(&sched->mu); + pthread_cond_destroy(&sched->cv_gpu); + free(sched); + return NULL; + } + } + + /* GPU lane registration is LAZY: no GPU work here. Binding all lanes + * (cuMemAlloc for the slot-major buffers) immediately after the model + * upload hangs on K80 because the CUDA context has not settled after a + * multi-minute weight upload. Deferred to the first bs_slot_bind. */ + sched->slots_gpu_init = false; + sched->n_active_gpu = 0; + sched->active_set_changed = false; + for (int i = 0; i < BS_MAX_SLOTS; i++) + sched->gpu_map[i] = -1; + + /* D1: per-model overflow queue. Read queue depth from env. */ + sched->n_queued = 0; + sched->queue_cap = BS_QUEUE_DEPTH_DEFAULT; + { + const char *env = getenv("BS_QUEUE_DEPTH"); + if (env) { + int v = atoi(env); + if (v > 0) sched->queue_cap = v; + } + } + + /* D3: pressure level cache (OK at startup until first poll). */ + sched->pressure_level = 0; /* PN_MEM_LEVEL_OK */ + + sched->driver_running = true; + sched->shutdown = false; + if (pthread_create(&sched->driver, NULL, driver_thread, sched) != 0) { + sched->driver_running = false; + for (int i = 0; i < cap; i++) { + pn_state_free(sched->slots[i].state); + pthread_cond_destroy(&sched->slots[i].cv); + } + pthread_cond_destroy(&sched->cv_idle); + pthread_cond_destroy(&sched->cv_work); + pthread_mutex_destroy(&sched->mu); + pthread_cond_destroy(&sched->cv_gpu); + free(sched); + return NULL; + } + + fprintf(stderr, "[batch_sched] created scheduler (cap=%d) for model '%s'\n", + cap, sched->model_name[0] ? sched->model_name : "?"); + return sched; +#endif /* USE_CUDA */ +} + +/* Both projections describe the SAME committed history, so they are always + * invalidated together: a survivor would claim to be a prefix of a conversation + * this slot no longer holds, and a resume off it would splice a request onto + * somebody else's KV. One function so that a future invalidation site cannot + * clear one and forget the other. */ +static void bs_slot_clear_projections(bs_slot *sl) +{ + sl->vis_n = 0; + sl->vis_think_n = 0; +} + +void bs_destroy(pn_batch_sched *sched) +{ + if (!sched) + return; + + /* Signal the driver to stop and wake anything parked. */ + pthread_mutex_lock(&sched->mu); + sched->shutdown = true; + pthread_cond_broadcast(&sched->cv_work); + pthread_cond_broadcast(&sched->cv_gpu); + for (int i = 0; i < sched->cap; i++) + pthread_cond_broadcast(&sched->slots[i].cv); + pthread_mutex_unlock(&sched->mu); + + if (sched->driver_running) + pthread_join(sched->driver, NULL); + + for (int i = 0; i < sched->cap; i++) { + pn_state_free(sched->slots[i].state); + pthread_cond_destroy(&sched->slots[i].cv); + /* L3 buffers normally freed by bs_slot_release; defensive sweep. */ + free(sched->slots[i].recent); + free(sched->slots[i].ring); + free(sched->slots[i].hist); /* PC-batched warm-state history */ + free(sched->slots[i].vis); /* think-elided projection */ + free(sched->slots[i].vis_think); /* reasoning-bearing projection */ + free(sched->slots[i].pend_vis); + free(sched->slots[i].pend_vis_think); + free(sched->slots[i].ct); /* PC-batched committed-token log */ + bs_sys_snap_unref(&sched->slots[i]); + free(sched->slots[i].sys_tokens); /* SYS1-batched boundary snapshot */ + } + pthread_cond_destroy(&sched->cv_idle); + pthread_cond_destroy(&sched->cv_work); + pthread_mutex_destroy(&sched->mu); + pthread_cond_destroy(&sched->cv_gpu); + free(sched); +} + +/* ============================================================ + * Per-request API + * ============================================================ */ + +/* Mark an IDLE slot PREFILL and clear its handshake state. Under mu. + * Does NOT touch sl->state (a dummy lane may still be in flight) nor the + * PC-batched warm-state fields (sid/hist — preserved so a same-session + * re-acquire can resume; bs_prefill_resume decides reset vs resume). */ +static void bs_slot_mark_prefill(bs_slot *sl) +{ + sl->status = BS_SLOT_PREFILL; + sl->logits_ready = false; + sl->token_ready = false; + sl->logits = NULL; + sl->next_token = 0; + sl->finished = false; + sl->step_error = false; + sl->sid_conflict = false; + sl->gpu_idx = -1; /* no GPU decode state until bind */ +} + +/* Claim one IDLE slot (already under mu) and mark it PREFILL. With a + * non-empty `sid`, preference order (cheapest resume first): + * 1. THIS session's warm IDLE slot (committed history → append delta); + * 2. the IDLE slot holding THIS session's system-prefix snapshot + * (SYS1-batched: a divergent turn still resumes at the boundary — + * matters when the warm history was invalidated but the snapshot, + * which survives resets, was not); + * 3. any free slot (this turn full-prefills). + * Returns the slot index >= 0, or -1 if none free. */ +static int try_claim_idle(pn_batch_sched *sched, const char *sid) +{ + if (sid && sid[0]) { + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + if (sl->status == BS_SLOT_IDLE && sl->sid[0] && + strncmp(sl->sid, sid, sizeof(sl->sid)) == 0) { + bs_slot_mark_prefill(sl); + return i; + } + } + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + if (sl->status == BS_SLOT_IDLE && sl->sys_sid[0] && + strncmp(sl->sys_sid, sid, sizeof(sl->sys_sid)) == 0) { + bs_slot_mark_prefill(sl); + return i; + } + } + } + /* No slot holds anything for THIS session. Prefer one that holds nothing for + * ANYONE: taking the first idle slot evicts its warm history + system-prefix + * snapshot, and it always picks slot 0, so a second session (a tab's reflect + * pass, a second mode, a second tab) destroyed the first session's cache + * while the model still had idle cold slots. */ + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + if (sl->status == BS_SLOT_IDLE && !sl->sid[0] && !sl->sys_sid[0]) { + bs_slot_mark_prefill(sl); + return i; + } + } + /* Every idle slot is warm for someone else — one of them has to lose. */ + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + if (sl->status == BS_SLOT_IDLE) { + bs_slot_mark_prefill(sl); + return i; + } + } + return -1; +} + +int bs_slot_acquire(pn_batch_sched *sched) +{ + if (!sched) + return -1; + pthread_mutex_lock(&sched->mu); + int found = try_claim_idle(sched, NULL); + pthread_mutex_unlock(&sched->mu); + return found; +} + +int bs_slot_acquire_biased(pn_batch_sched *sched, const char *sid) +{ + if (!sched) + return -1; + pthread_mutex_lock(&sched->mu); + int found = try_claim_idle(sched, sid); + pthread_mutex_unlock(&sched->mu); + return found; +} + +/* Reset a slot's pn_state to fresh (host mirror zeroed + GPU bindings + * forgotten) and drop any PC-batched warm-state binding. Must be called + * owning the GPU turn (pn_qwen35moe_gpu_forget_state is host bookkeeping the + * turn protects from a racing forward). */ +static void bs_slot_reset_state(pn_batch_sched *sched, int idx) +{ + pn_state_reset(sched->slots[idx].state); +#ifdef USE_CUDA + /* This slot's pn_state is REUSED across requests. pn_state_reset cleared + * only the host mirror; the GPU still thinks this state is "live" on some + * layers (SSM d->host_state, single-user KV kv_host_id) with stale + * contents from its previous request. A long prefill would read that + * residue and diverge from a fresh state (short prefills never reach the + * stale positions, which is why the bug only showed on long prompts). + * Forget the bindings so the upcoming prefill re-seeds every GPU shadow + * from the freshly-zeroed host mirror. Safe under the GPU turn: no + * forward can race this host-side bookkeeping. */ + pn_qwen35moe_gpu_forget_state(sched->model, sched->slots[idx].state); +#endif + /* The warm state is gone — this slot no longer holds any session. */ + sched->slots[idx].sid[0] = '\0'; + sched->slots[idx].hist_n = 0; + bs_slot_clear_projections(&sched->slots[idx]); + sched->slots[idx].ct_keep = 0; + sched->slots[idx].ct_prompt_from = 0; +} + +void bs_prefill_begin(pn_batch_sched *sched, int idx) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return; + /* From here to bs_slot_bind / bs_prefill_abort (modulo bs_prefill_yield + * windows) this thread is the model's single GPU owner: no other + * prefill chunk, no decode step. FIFO ticket: a joiner waits at most + * one step / one chunk of every earlier waiter — the free-running L3 + * driver cannot starve it. */ + bs_gpu_acquire(sched); + bs_slot_reset_state(sched, idx); +} + +void bs_prefill_resume(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int n_ctx, int *delta_start) +{ + int capture_unused = 0; + bs_prefill_resume_sys(sched, idx, sid, tokens, n_ctx, 0, delta_start, + &capture_unused); +} + +/* ── SYS1-batched helpers (per-slot system-prefix boundary snapshot). All + * owner-only: the slot's owner holds the slot (PREFILL) and the GPU turn, so + * no other thread reads or writes the sys_* fields concurrently (the affinity + * scans read sys_sid under mu on IDLE slots only). ── */ + +/* Is this model one whose dual state supports snapshot/restore? (Bonsai + * shares the qwen35moe dual-state machinery — same rule as prefix_cache.c.) */ +static int bs_sys_snapshot_supported(const pn_batch_sched *sched) +{ + return sched->model->config.arch == PN_ARCH_QWEN35MOE || + sched->model->config.arch == PN_ARCH_BONSAI; +} + +/* Does this slot hold a boundary snapshot for EXACTLY this session + system + * prefix? Both guards are required: the sid pins the snapshot to its session + * and the byte-match catches a changed system prompt. */ +static int bs_sys_snap_matches(const bs_slot *sl, const char *sid, + const uint32_t *tokens, int sys_len) +{ + return sl->sys_snap && sl->sys_len == sys_len && + strncmp(sl->sys_sid, sid, sizeof(sl->sys_sid)) == 0 && + memcmp(sl->sys_tokens, tokens, + (size_t)sys_len * sizeof(uint32_t)) == 0; +} + +/* ── Reroute: several conversations under one session id (bs_slot_reroute). + * All under mu, reading IDLE slots only — their warm fields (sid, hist, the + * projections, the boundary snapshot) change only under mu or while owned. ── */ + +/* What an IDLE slot's cached state is worth to this prompt. */ +enum { BS_FIT_NONE, BS_FIT_SYS, BS_FIT_WARM }; + +/* Is p[0..n) a strict prefix of tokens[0..n_ctx)? */ +static int bs_is_strict_prefix(const uint32_t *p, int n, + const uint32_t *tokens, int n_ctx) +{ + return p && n > 0 && n < n_ctx && + memcmp(p, tokens, (size_t)n * sizeof(uint32_t)) == 0; +} + +/* The warm-history test mirrors the resume ladder's token checks (rungs 1 and + * 1b); the GPU-side checks it cannot make here are re-made by the ladder, + * which falls back to this slot's own snapshot on a miss. */ +static int bs_slot_fit(const bs_slot *sl, const char *sid, + const uint32_t *tokens, int n_ctx, int sys_len) +{ + if (sl->sid[0] && strncmp(sl->sid, sid, sizeof(sl->sid)) == 0 && + (bs_is_strict_prefix(sl->hist, sl->hist_n, tokens, n_ctx) || + bs_is_strict_prefix(sl->vis, sl->vis_n, tokens, n_ctx) || + bs_is_strict_prefix(sl->vis_think, sl->vis_think_n, tokens, n_ctx))) + return BS_FIT_WARM; + if (sys_len > 0 && sys_len < n_ctx && + bs_sys_snap_matches(sl, sid, tokens, sys_len)) + return BS_FIT_SYS; + return BS_FIT_NONE; +} + +static int bs_slot_holds_sid(const bs_slot *sl, const char *sid) +{ + return (sl->sid[0] && strncmp(sl->sid, sid, sizeof(sl->sid)) == 0) || + (sl->sys_sid[0] && strncmp(sl->sys_sid, sid, sizeof(sl->sys_sid)) == 0); +} + +/* The slot this prompt should run on (see bs_slot_reroute). Under mu. */ +static int bs_reroute_target_locked(const pn_batch_sched *sched, int idx, + const char *sid, const uint32_t *tokens, + int n_ctx, int sys_len) +{ + int best = idx; + int best_fit = bs_slot_fit(&sched->slots[idx], sid, tokens, n_ctx, sys_len); + for (int i = 0; i < sched->cap; i++) { + const bs_slot *sl = &sched->slots[i]; + if (i == idx || sl->status != BS_SLOT_IDLE) + continue; + int fit = bs_slot_fit(sl, sid, tokens, n_ctx, sys_len); + if (fit > best_fit) { + best = i; + best_fit = fit; + } + } + if (best_fit != BS_FIT_NONE || !bs_slot_holds_sid(&sched->slots[idx], sid)) + return best; + /* A new conversation under this id: keep the other one's cache if a cold + * slot can take this one instead. */ + for (int i = 0; i < sched->cap; i++) { + const bs_slot *sl = &sched->slots[i]; + if (sl->status == BS_SLOT_IDLE && !sl->sid[0] && !sl->sys_sid[0]) + return i; + } + return idx; +} + +int bs_slot_reroute(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int n_ctx, int sys_len) +{ + if (!sched || idx < 0 || idx >= sched->cap || !sid || !sid[0] || + !tokens || n_ctx <= 0) + return idx; + pthread_mutex_lock(&sched->mu); + int to = bs_reroute_target_locked(sched, idx, sid, tokens, n_ctx, sys_len); + int fit = BS_FIT_NONE; + if (to != idx) { + fit = bs_slot_fit(&sched->slots[to], sid, tokens, n_ctx, sys_len); + bs_slot_mark_prefill(&sched->slots[to]); + /* Claimed but never touched (no GPU turn, no state, no bind): handing + * it back is just the status flip; its warm fields stay as they were. */ + sched->slots[idx].status = BS_SLOT_IDLE; + pthread_cond_broadcast(&sched->cv_idle); + } + pthread_mutex_unlock(&sched->mu); + if (to != idx) + fprintf(stderr, "[batch_sched] session reroute slot=%d -> %d sid=%s: " + "%s\n", idx, to, sid, + fit == BS_FIT_WARM ? "warm history continues there" + : fit == BS_FIT_SYS ? "system-prefix snapshot there" + : "new conversation, cold slot keeps the other one's cache"); + return to; +} + +/* Phase 10b: release a slot's reference on its boundary snapshot. A shared + * snapshot (sys_snap_refs non-NULL) is freed only when the last sharer lets + * go; a private one is freed immediately. */ +static void bs_sys_snap_unref(bs_slot *sl) +{ + if (sl->sys_snap && sl->sys_snap_refs && --(*sl->sys_snap_refs) > 0) { + sl->sys_snap = NULL; + sl->sys_snap_refs = NULL; + return; + } + pn_qwen35moe_state_snapshot_free(sl->sys_snap); + free(sl->sys_snap_refs); + sl->sys_snap = NULL; + sl->sys_snap_refs = NULL; +} + +/* Drop a slot's boundary snapshot (keeps the tokens buffer for realloc reuse). + * sys_sid is cleared under mu — the affinity scans read it. */ +static void bs_sys_snap_drop(pn_batch_sched *sched, bs_slot *sl) +{ + bs_sys_snap_unref(sl); + sl->sys_len = 0; + pthread_mutex_lock(&sched->mu); + sl->sys_sid[0] = '\0'; + pthread_mutex_unlock(&sched->mu); +} + +/* Restore the slot's boundary snapshot into its own pn_state, leaving the + * state at pos == sys_len. Host-side write into THIS slot's own mirrors (the + * snapshot is a self-contained deep copy — no other slot's state is touched). + * The GPU shadows still hold the end of some previous turn; a restore to a + * non-zero pos never trips the pos==0 restart re-sync, so the bindings are + * forgotten explicitly — the delta prefill then re-seeds [0, sys_len) SSM + + * KV from the restored host mirror via the ordinary seed-on-host-change path + * (the exact mechanism the warm-history resume validated on a K80 host). GPU turn + * must be held. Returns 0 on success. */ +static int bs_sys_snap_restore(pn_batch_sched *sched, bs_slot *sl, int sys_len) +{ + if (!bs_sys_snapshot_supported(sched)) + return -1; + if (pn_qwen35moe_state_restore(sl->state->kv_cache, sl->sys_snap) != 0) + return -1; + sl->state->pos = sys_len; +#ifdef USE_CUDA + pn_qwen35moe_gpu_forget_state(sched->model, sl->state); +#endif + return 0; +} + +/* How many token ids either side of the divergence the window dump prints. */ +#define BS_DIVERGENCE_WINDOW 24 + +/* Longest decoded run the window prints per side (keeps one log line bounded). */ +#define BS_DIVERGENCE_TEXT_MAX 1024 + +/* Decode [from, to) of `ids` into printable text, escaping newlines/tabs so the + * whole window stays on ONE greppable line. Owned result, caller frees. + * Diagnostic-only (PN_REQPROF=1), so a NULL return just degrades the dump. */ +static char *bs_decode_run(pn_model *model, const uint32_t *ids, int from, int to) +{ + if (!model || !model->tokenizer || to <= from) + return NULL; + char *out = calloc(BS_DIVERGENCE_TEXT_MAX + 8, 1); + if (!out) + return NULL; + size_t used = 0; + for (int i = from; i < to && used < BS_DIVERGENCE_TEXT_MAX; i++) { + int id = (int)ids[i]; + char *raw = ft_decode(model->tokenizer, &id, 1); + if (!raw) + continue; + char *piece = raw; + char *decoded = NULL; + if (ft_get_model_type(model->tokenizer) == 0) { + decoded = gpt2_bytes_decode(raw); + if (decoded) + piece = decoded; + } + for (const char *p = piece; *p && used < BS_DIVERGENCE_TEXT_MAX; p++) { + if (*p == '\n') { out[used++] = '\\'; if (used < BS_DIVERGENCE_TEXT_MAX) out[used++] = 'n'; } + else if (*p == '\t') { out[used++] = '\\'; if (used < BS_DIVERGENCE_TEXT_MAX) out[used++] = 't'; } + else if (*p == '\r') { out[used++] = '\\'; if (used < BS_DIVERGENCE_TEXT_MAX) out[used++] = 'r'; } + else { out[used++] = *p; } + } + free(decoded); + free(raw); + } + out[used] = '\0'; + return out; +} + +/* Print two token runs around the point where they diverge, as ids AND as + * decoded text. `first_diff` alone names WHICH token broke the prefix match; + * the window names WHAT the client failed to reproduce after it, which is the + * difference between "one unreproducible opener" (recoverable by resending it) + * and "an entire span that was never streamed". PN_REQPROF=1 only, so no + * hot-path cost. `a_label`/`b_label` name the two sides. */ +static void bs_log_token_window(pn_model *model, + const char *a_label, const uint32_t *a, int a_n, + const char *b_label, const uint32_t *b, int b_n, + int first_diff) +{ + int from = first_diff - BS_DIVERGENCE_WINDOW; + if (from < 0) + from = 0; + int a_to = first_diff + BS_DIVERGENCE_WINDOW; + if (a_to > a_n) a_to = a_n; + int b_to = first_diff + BS_DIVERGENCE_WINDOW; + if (b_to > b_n) b_to = b_n; + + fprintf(stderr, "[reqprof] %s[%d..] =", a_label, from); + for (int i = from; i < a_to; i++) + fprintf(stderr, " %u", a[i]); + fprintf(stderr, "\n[reqprof] %s[%d..] =", b_label, from); + for (int i = from; i < b_to; i++) + fprintf(stderr, " %u", b[i]); + fprintf(stderr, "\n"); + + char *a_txt = bs_decode_run(model, a, from, a_to); + char *b_txt = bs_decode_run(model, b, from, b_to); + fprintf(stderr, "[reqprof] %s TEXT: |%s|\n", a_label, a_txt ? a_txt : "?"); + fprintf(stderr, "[reqprof] %s TEXT: |%s|\n", b_label, b_txt ? b_txt : "?"); + free(a_txt); + free(b_txt); +} + +/* Phase 8 diagnostic: report WHY append-resume missed and fell back to the + * system-prefix path. Every real multi-turn conversation measured in Phase 8 + * fell through here, and the per-turn cost is the re-prefill that fallback + * forces — so the reason is the finding. PN_REQPROF=1 only. */ +static void bs_log_append_resume_miss(pn_model *model, int idx, const char *sid, + const bs_slot *sl, + const uint32_t *tokens, int n_ctx) +{ + if (!pn_reqprof_enabled()) + return; + const char *why; + int first_diff = -1; + if (!sid || !sid[0]) + why = "request carried no session id"; + else if (!sl->sid[0]) + why = "slot holds no committed history"; + else if (strncmp(sl->sid, sid, sizeof(sl->sid)) != 0) + why = "slot history belongs to a different session"; + else if (sl->hist_n <= 0) + why = "committed history empty"; + else if (sl->hist_n >= n_ctx) + why = "committed history not shorter than the new context"; + else if ((int)sl->state->pos != sl->hist_n) + why = "state pos != committed history length"; + else { + why = "committed history is not a prefix of the new context"; + for (int i = 0; i < sl->hist_n; i++) { + if (sl->hist[i] != tokens[i]) { + first_diff = i; + break; + } + } + } + fprintf(stderr, "[reqprof] append-resume MISS slot=%d sid=%s: %s " + "(hist_n=%d pos=%d n_ctx=%d first_diff=%d hist_tok=%u new_tok=%u)\n", + idx, sid ? sid : "-", why, sl->hist_n, (int)sl->state->pos, n_ctx, + first_diff, + first_diff >= 0 ? sl->hist[first_diff] : 0u, + first_diff >= 0 ? tokens[first_diff] : 0u); + if (first_diff >= 0) + bs_log_token_window(model, "hist", sl->hist, sl->hist_n, + "new ", tokens, n_ctx, first_diff); + fflush(stderr); +} + +/* PROJECTION diagnostic: the projection is what actually decides the rung, and + * it is NOT `hist` — so bs_log_append_resume_miss's first_diff (which is + * measured against hist) points at the first think block the two sides disagree + * about, never at whatever the client really failed to reproduce. This dump + * answers the only question that matters on a miss: at which index does the + * SERVE's prediction of the client's re-render stop matching what the client + * actually sent, and what is on each side. `label` names WHICH projection + * (think-elided / think-included) this run is about, so a session that misses + * both can be told apart from one that is simply on the other convention. + * PN_REQPROF=1 only. */ +static void bs_log_vis_divergence(pn_model *model, int idx, const char *sid, + const bs_slot *sl, const char *label, + const uint32_t *proj, int proj_n, + const uint32_t *tokens, int n_ctx) +{ + if (!pn_reqprof_enabled()) + return; + int n = proj_n < n_ctx ? proj_n : n_ctx; + int first_diff = -1; + for (int i = 0; i < n; i++) { + if (proj[i] != tokens[i]) { + first_diff = i; + break; + } + } + fprintf(stderr, "[reqprof] %s MISS slot=%d sid=%s: projection is " + "not a prefix of the new context (proj_n=%d hist_n=%d n_ctx=%d " + "first_diff=%d proj_tok=%u new_tok=%u)\n", label, idx, + sid ? sid : "-", proj_n, sl->hist_n, n_ctx, first_diff, + first_diff >= 0 ? proj[first_diff] : 0u, + first_diff >= 0 ? tokens[first_diff] : 0u); + if (first_diff >= 0) + bs_log_token_window(model, "proj", proj, proj_n, + "new ", tokens, n_ctx, first_diff); + fflush(stderr); +} + +/* ── SESSION-SUBSTITUTION detection ──────────────────────────────────────── + * + * A session id names ONE conversation. When a client posts a second, unrelated + * conversation under the same id, every rung of the resume ladder misses, the + * slot's boundary snapshot is replaced by the intruder's, and BOTH conversations + * re-prefill in full from then on — silently. Detect it and say so. + * + * The discriminator is the SYSTEM BLOCK: every legitimate history change (an + * append, a think-elided re-render, a client-side compaction) keeps + * tokens[0..sys_len) byte-identical, so a change there is the one thing that + * cannot be a continuation. See bs_slot_session_conflict in the header for the + * full argument and the deliberate false-negative list. + * ────────────────────────────────────────────────────────────────────────── */ + +/* The system-prefix record this slot holds for `sid`, if any. Prefers the + * boundary snapshot (which stores the exact prefix tokens it covers) and falls + * back to the committed history, whose head IS the system block. Returns 1 and + * fills `out` and `out_n` on a hit, 0 when this slot holds nothing for `sid`. */ +static int bs_recorded_sys_prefix(const bs_slot *sl, const char *sid, + const uint32_t **out, int *out_n) +{ + if (sl->sys_sid[0] && strncmp(sl->sys_sid, sid, sizeof(sl->sys_sid)) == 0 && + sl->sys_tokens && sl->sys_len > 0) { + *out = sl->sys_tokens; + *out_n = sl->sys_len; + return 1; + } + if (sl->sid[0] && strncmp(sl->sid, sid, sizeof(sl->sid)) == 0 && + sl->hist && sl->hist_n > 0) { + *out = sl->hist; + *out_n = sl->hist_n; + return 1; + } + return 0; +} + +/* Set sl->sid_conflict and report (unconditionally — this is a client defect the + * client can only learn about if the serve says it) when this turn's system + * block differs from the one this session id last committed on this slot. + * Called only after rungs 1 and 1b have missed: a hit on either proves the + * system block matched, since both memcmp over a span that contains it. */ +static void bs_note_session_substitution(pn_batch_sched *sched, int idx, + const char *sid, + const uint32_t *tokens, int n_ctx, + int sys_len) +{ + bs_slot *sl = &sched->slots[idx]; + sl->sid_conflict = false; + if (!sid || !sid[0] || sys_len <= 0 || sys_len > n_ctx) + return; /* no session id, or no system block to compare */ + + const uint32_t *rec = NULL; + int rec_n = 0; + if (!bs_recorded_sys_prefix(sl, sid, &rec, &rec_n)) + return; /* nothing recorded for this session here — cannot tell */ + + /* A record shorter than this request's system block cannot contain it, so + * the block changed; otherwise compare the block byte for byte. */ + if (rec_n >= sys_len && + memcmp(rec, tokens, (size_t)sys_len * sizeof(uint32_t)) == 0) + return; /* same system block → an ordinary history divergence */ + + sl->sid_conflict = true; + fprintf(stderr, "[batch_sched] WARN session-substitution slot=%d sid=%s: " + "this session id already identified a conversation with a %d-token " + "system prompt; this request carries a DIFFERENT %d-token one. Both " + "conversations now re-prefill in full every turn. Use one session id " + "per conversation.\n", idx, sid, rec_n, sys_len); + fflush(stderr); +} + +bool bs_slot_session_conflict(const pn_batch_sched *sched, int idx) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return false; + return sched->slots[idx].sid_conflict; +} + +/* Is this slot's warm KV actually RE-USABLE, and not merely bookkept? + * + * Phase 10b (INV3) made the host KV mirror LAZY: between turns a session's rows + * live in a GPU ring, not in host RAM, and bs_slot_finalize deliberately does not + * materialize them (that is the per-session host footprint lazy mode removed). + * A warm resume then forgets the GPU bindings and re-uploads from the host + * mirror — which only works while a ring still holds the content. When another + * session's forward has taken the ring in between, nothing does, and the + * resumed forward FAILS ITS STEP ("host mirror STALE and no GPU ring source"): + * the turn returns with zero tokens generated. Measured on the K80 test machine + * 2026-08-02, four consecutive turns produced an EMPTY response this way. + * + * So the warm rungs must prove the rows are reachable before committing to + * them. Materializing the mirror here does that and makes the re-upload sound; + * it is bounded by the session's own context (a few MiB over PCIe, once per + * resumed turn) and it costs no host RAM the state had not already allocated. + * A failure is not an error — it means another session won the ring, so the + * caller falls through to a rung that re-prefills. + * + * GPU turn must be held. */ +static bool bs_warm_kv_reusable(pn_batch_sched *sched, bs_slot *sl) +{ +#ifdef USE_CUDA + if (pn_qwen35moe_gpu_state_to_host(sched->model, sl->state) != 0) { + if (pn_reqprof_enabled()) + fprintf(stderr, "[reqprof] warm resume refused slot=%d sid=%s: this " + "session's KV rows are no longer in any GPU ring\n", + sl->slot_idx, sl->sid); + return false; + } + return true; +#else + (void)sched; (void)sl; + return true; +#endif +} + +/* One candidate re-render of the committed history, as the serve predicted the + * client would send it back. Two exist because two conventions exist. */ +typedef struct { + const uint32_t *tok; + int n; + const char *label; +} bs_projection; + +/* PROJECTION rung — resume a session whose committed history differs from the + * client's re-render only in the think blocks, whichever way round. + * + * A projection is that re-render as the serve predicted it (see + * bs_slot_note_visible). When one is a strict prefix of the new context, every + * KV row in [0, hist_n) is a correct continuation of the conversation the client + * is describing, so the state resumes AS IS at pos == hist_n and only the + * genuinely new tail is prefilled. The two candidates differ in what they claim + * about this turn's think block: + * + * think-elided — the client DROPPED it. The block stays in the model's + * context, which is where it was generated and where it + * cannot be removed from (30 of 40 layers are a sequential + * SSM recurrence; there is no rewind). The KV then holds + * strictly more than the client described. + * think-included — the client SENT IT BACK. The KV holds the block as the + * model SAMPLED it, the client describes it as re-tokenized + * TEXT; same content, and the ids need not agree because the + * KV is authoritative for a span both sides agree is there. + * This is the strictly weaker claim of the two, and it is the + * rung that lets a client send reasoning back without paying a + * full re-prefill every turn. + * + * Neither candidate is trusted past its own memcmp: a divergence at ANY index + * drops through to the re-prefilling rungs, which is the only safe answer — + * reusing KV that does not correspond to the request's prefix is silently wrong + * output, strictly worse than a slow turn. + * + * *delta is set in the CLIENT's index space (where the prefill reads from + * `tokens`), which is hist_n - proj_n behind the state's position — that is the + * whole point, and sg_prefill_range_sched positions tokens from state->pos, not + * from the array index. The committed log is then hist[0, hist_n) followed by + * tokens[*delta, n_ctx), which is what ct_keep / ct_prompt_from carry to bind. + * + * Returns 1 on a hit (state untouched, ready to continue), 0 on a miss. + * GPU turn must be held. */ +static int bs_projection_resume(pn_batch_sched *sched, int idx, + const char *sid, const uint32_t *tokens, + int n_ctx, int *delta) +{ + bs_slot *sl = &sched->slots[idx]; + if (!sid || !sid[0] || !sl->sid[0]) + return 0; + if (strncmp(sl->sid, sid, sizeof(sl->sid)) != 0) + return 0; + if (sl->hist_n <= 0 || (int)sl->state->pos != sl->hist_n) + return 0; + + /* Reasoning-bearing first: it is the convention that keeps the model + * reasoning, so on a session that uses it the elided candidate would only + * log a misleading divergence before this one hit. */ + const bs_projection cands[] = { + { sl->vis_think, sl->vis_think_n, "think-included" }, + { sl->vis, sl->vis_n, "think-elided" }, + }; + const bs_projection *hit = NULL; + for (size_t i = 0; i < sizeof(cands) / sizeof(cands[0]) && !hit; i++) { + if (!cands[i].tok || cands[i].n <= 0) + continue; + /* Strictly shorter, so at least one new token remains to prefill and the + * next-token logits are recovered (same rule as the exact-prefix rung). */ + if (cands[i].n >= n_ctx) { + if (pn_reqprof_enabled()) + fprintf(stderr, "[reqprof] %s MISS slot=%d sid=%s: projection " + "not shorter than the new context (proj_n=%d hist_n=%d " + "n_ctx=%d)\n", cands[i].label, idx, sid, cands[i].n, + sl->hist_n, n_ctx); + continue; + } + if (memcmp(cands[i].tok, tokens, + (size_t)cands[i].n * sizeof(uint32_t)) == 0) + hit = &cands[i]; + } + if (!hit) { + for (size_t i = 0; i < sizeof(cands) / sizeof(cands[0]); i++) + if (cands[i].tok && cands[i].n > 0 && cands[i].n < n_ctx) + bs_log_vis_divergence(sched->model, idx, sid, sl, + cands[i].label, cands[i].tok, cands[i].n, + tokens, n_ctx); + return 0; + } + + /* Keeping the model's own think tokens costs context: the KV ends this turn + * at hist_n + (n_ctx - proj_n), which is hist_n - proj_n AHEAD of the n_ctx + * that admission already cleared. Refuse the rung rather than overrun the + * session's window — the fallback rungs re-prefill and land exactly at + * n_ctx. */ + int cap = pn_session_ctx_cap(sched->model); + if (cap > 0 && sl->hist_n + (n_ctx - hit->n) > cap) + return 0; + if (!bs_warm_kv_reusable(sched, sl)) + return 0; + +#ifdef USE_CUDA + /* Same re-seed contract as the exact-prefix rung: another session may have + * claimed the shared SSM/KV buffers between this session's turns, so forget + * the GPU bindings and let the delta prefill re-upload [0, hist_n) from the + * warm host mirror finalize left authoritative. */ + pn_qwen35moe_gpu_forget_state(sched->model, sl->state); +#endif + sl->ct_keep = sl->hist_n; + sl->ct_prompt_from = hit->n; + *delta = hit->n; + if (pn_reqprof_enabled()) { + /* hist_n - proj_n is NOT printed as a count: it is negative whenever the + * reasoning-bearing projection runs past the KV, which is routine. */ + fprintf(stderr, "[reqprof] append-resume HIT (%s) slot=%d sid=%s: " + "projection %d over a %d-token KV, resume at pos %d, " + "prefill %d of %d\n", hit->label, idx, sid, hit->n, + sl->hist_n, sl->hist_n, n_ctx - hit->n, n_ctx); + fflush(stderr); + } + return 1; +} + +/* Copy a pending projection into the slot's published slot, refusing one that + * slot. Returns the published length, or 0 when nothing was published (OOM is + * benign: the session just loses that rung for a turn and re-prefills). + * + * No length relation to the committed history is imposed here, and the + * reasoning-bearing projection is why. Its length is + * hist_n +/- a couple of tokens by construction: the client's rendering carries + * the template's own separators ("\n" ... "\n\n") where the KV + * carries whatever the model sampled around the same text, and the emitter + * trimmed the leading newline out of the reasoning string. A + * "must not exceed hist_n" tripwire therefore lands right on the boundary and + * would silently disable the rung on a coin flip. It is not a correctness gate + * either way: the gates are the full memcmp against the request and the strict + * proj_n < n_ctx in bs_projection_resume, and a LONGER projection is the + * conservative direction — more tokens must match before any KV is reused. */ +static int bs_publish_projection(uint32_t **dst, int *cap, + const uint32_t *pend, int pend_n) +{ + if (pend_n <= 0) + return 0; + if (*cap < pend_n) { + uint32_t *v = realloc(*dst, (size_t)pend_n * sizeof(uint32_t)); + if (!v) + return 0; + *dst = v; + *cap = pend_n; + } + memcpy(*dst, pend, (size_t)pend_n * sizeof(uint32_t)); + return pend_n; +} + +void bs_slot_publish_projections(bs_slot *sl, int hist_n) +{ + if (!sl) + return; + bs_slot_clear_projections(sl); + /* The think-ELIDED projection drops every think block, so one longer than + * the history it projects is nonsense — refuse it (the tripwire this rung + * shipped with). The reasoning-bearing one puts the block back and is + * legitimately hist_n-sized either way, so it carries no such bound; see + * bs_publish_projection for why a bound there would be a coin flip. */ + if (sl->pend_vis_n <= hist_n) + sl->vis_n = bs_publish_projection(&sl->vis, &sl->vis_cap, + sl->pend_vis, sl->pend_vis_n); + sl->vis_think_n = bs_publish_projection(&sl->vis_think, &sl->vis_think_cap, + sl->pend_vis_think, + sl->pend_vis_think_n); +} + +/* Build one pending projection into `dst` as head ++ mid ++ tail. `mid` may be + * empty (the think-elided candidate). Returns the length, or 0 on OOM — benign, + * the turn just loses that candidate. */ +static int bs_build_projection(uint32_t **dst, int *cap, + const uint32_t *head, int head_n, + const uint32_t *mid, int mid_n, + const uint32_t *tail, int tail_n) +{ + int want = head_n + mid_n + tail_n; + if (*cap < want) { + uint32_t *v = realloc(*dst, (size_t)want * sizeof(uint32_t)); + if (!v) + return 0; + *dst = v; + *cap = want; + } + memcpy(*dst, head, (size_t)head_n * sizeof(uint32_t)); + if (mid_n > 0) + memcpy(*dst + head_n, mid, (size_t)mid_n * sizeof(uint32_t)); + memcpy(*dst + head_n + mid_n, tail, (size_t)tail_n * sizeof(uint32_t)); + return want; +} + +void bs_slot_note_visible(pn_batch_sched *sched, int idx, + const uint32_t *prompt, int n_prompt, + int think_prefill_len, + const uint32_t *think_block, int think_block_n, + const uint32_t *rerender, int rerender_n) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return; + bs_slot *sl = &sched->slots[idx]; + sl->pend_vis_n = 0; + sl->pend_vis_think_n = 0; + /* The prompt minus its forced generation prefix is the client's own + * rendering of every earlier turn — whichever think-block convention it + * follows — so it is exactly what it will resend. `rerender` is this turn's + * reply put back through that same rendering, so the two concatenated ARE + * next turn's context up to the point the client appends to it. */ + int head_n = n_prompt - think_prefill_len; + if (!prompt || n_prompt <= 0 || think_prefill_len < 0 || head_n <= 0 || + !rerender || rerender_n <= 0) { + if (pn_reqprof_enabled()) + fprintf(stderr, "[reqprof] note_visible slot=%d: NO projection " + "(n_prompt=%d think_prefill=%d rerender_n=%d)\n", + idx, n_prompt, think_prefill_len, rerender_n); + return; + } + if (!think_block || think_block_n <= 0) + think_block_n = 0; + + sl->pend_vis_n = bs_build_projection(&sl->pend_vis, &sl->pend_vis_cap, + prompt, head_n, NULL, 0, + rerender, rerender_n); + if (think_block_n > 0) + sl->pend_vis_think_n = + bs_build_projection(&sl->pend_vis_think, &sl->pend_vis_think_cap, + prompt, head_n, think_block, think_block_n, + rerender, rerender_n); + if (pn_reqprof_enabled()) { + char *tail_txt = bs_decode_run(sched->model, rerender, 0, rerender_n); + fprintf(stderr, "[reqprof] note_visible slot=%d: projections elided=%d " + "included=%d (head %d + think %d + re-render %d, n_prompt=%d " + "think_prefill=%d) RENDER: |%s|\n", idx, sl->pend_vis_n, + sl->pend_vis_think_n, head_n, think_block_n, rerender_n, + n_prompt, think_prefill_len, tail_txt ? tail_txt : "?"); + free(tail_txt); + fflush(stderr); + } +} + +void bs_prefill_resume_sys(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int n_ctx, int sys_len, + int *delta_start, int *capture_sys) +{ + if (delta_start) + *delta_start = 0; + if (capture_sys) + *capture_sys = 0; + if (!sched || idx < 0 || idx >= sched->cap || !tokens || n_ctx <= 0) + return; + bs_gpu_acquire(sched); /* same GPU-owner contract as bs_prefill_begin */ + + bs_slot *sl = &sched->slots[idx]; + /* A system prefix is only usable when a conversation follows it. */ + int sys_ok = sid && sid[0] && sys_len > 0 && sys_len < n_ctx; + int delta = 0; + /* Default committed-log seeding: the log IS the prompt (rungs 1, 2 and 3). + * Only the think-elision rung overrides it. */ + sl->ct_keep = 0; + sl->ct_prompt_from = 0; + sl->sid_conflict = false; + /* Resume ladder — mirrors pn_prefix_cache_acquire_sys. + * + * 1. Append resume: THIS slot's warm state belongs to `sid`, its committed + * history is an exact prefix of the new context, and the state sits + * exactly at that history's end (pos == hist_n). The agentic append + * case: prefill only [hist_n, n_ctx). hist_n < n_ctx is strict so at + * least one delta token remains to recover the next-token logits (the + * degenerate full-hit never happens in a real chat turn — an assistant + * opener is always appended — and falls through to a full reset). */ + if (sid && sid[0] && sl->sid[0] && + strncmp(sl->sid, sid, sizeof(sl->sid)) == 0 && + sl->hist_n > 0 && sl->hist_n < n_ctx && + (int)sl->state->pos == sl->hist_n && + memcmp(sl->hist, tokens, + (size_t)sl->hist_n * sizeof(uint32_t)) == 0 && + bs_warm_kv_reusable(sched, sl)) { + delta = sl->hist_n; /* warm: resume, do NOT reset */ +#ifdef USE_CUDA + /* Force every GPU shadow of this warm state to re-seed from its HOST + * mirror on the delta-prefill forward. Between this session's turns + * another session's forward may have claimed the shared SSM buffers / + * the shared KV ring, and this state's own live decode advanced its + * per-slot buffers (not the shared ones the prefill forward re-seeds). + * finalize left the host mirror authoritative at pos==hist_n; forgetting + * the GPU bindings (host-bookkeeping only — the host mirror is NOT + * touched) makes the resumed forward re-upload [0,hist_n) SSM + KV from + * that warm mirror, then continue the delta. Safe under the GPU turn + * just acquired. */ + pn_qwen35moe_gpu_forget_state(sched->model, sl->state); +#endif + } else if (bs_projection_resume(sched, idx, sid, tokens, n_ctx, &delta)) { + /* 1b. Projection append resume — the client's re-render of the history + * differs from the committed KV only in the think blocks, either + * because it dropped them or because it sent the reasoning back as + * text. See bs_projection_resume. */ + } else { + /* 2. System-prefix resume: divergent history but this slot holds THIS + * session's boundary snapshot over these exact system tokens (the + * reasoning-model turn). Restore it and prefill only the + * conversation. The stale committed history is dropped — + * bs_slot_finalize rebuilds it at end of turn. */ + /* Both warm rungs missed. Before the fallbacks mutate this slot, + * decide whether the miss is an ordinary history divergence or a second + * conversation wearing this session's id — and say so out loud. */ + bs_note_session_substitution(sched, idx, sid, tokens, n_ctx, sys_len); + bs_log_append_resume_miss(sched->model, idx, sid, sl, tokens, n_ctx); + int resumed = 0; + if (sys_ok && bs_sys_snap_matches(sl, sid, tokens, sys_len)) { + if (bs_sys_snap_restore(sched, sl, sys_len) == 0) { + resumed = 1; + delta = sys_len; + pthread_mutex_lock(&sched->mu); + sl->sid[0] = '\0'; /* history no longer matches the state */ + sl->hist_n = 0; + bs_slot_clear_projections(sl); + pthread_mutex_unlock(&sched->mu); + fprintf(stderr, "[batch_sched] sys-prefix reuse slot=%d " + "sid=%s: resume at %d, prefill %d of %d tokens\n", + idx, sid, sys_len, n_ctx - sys_len, n_ctx); + } else { + /* Restore refused (dim/KV-mode change): the snapshot can never + * apply again — drop it so a capture can replace it. Nothing + * was written (the restore validates dims before any copy). */ + bs_sys_snap_drop(sched, sl); + } + } + /* 3. Fresh/diverged with no usable snapshot: full reset + prefill. */ + if (!resumed) { + bs_slot_reset_state(sched, idx); + delta = 0; + } + } + + /* Full prefill ahead + no snapshot covering these exact system tokens → + * ask the caller to capture the boundary state mid-prefill (replacing a + * stale snapshot from a changed system prompt or another session). */ + if (capture_sys && delta == 0 && sys_ok && bs_sys_snapshot_supported(sched) + && !bs_sys_snap_matches(sl, sid, tokens, sys_len)) + *capture_sys = 1; + if (delta_start) + *delta_start = delta; +} + +int bs_prefill_capture_sys(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int sys_len) +{ + if (!sched || idx < 0 || idx >= sched->cap || !sid || !sid[0] || !tokens || + sys_len <= 0) + return -1; + if (!bs_sys_snapshot_supported(sched)) + return -1; + bs_slot *sl = &sched->slots[idx]; + /* The capture protocol: resume_sys set capture_sys, the caller prefilled + * exactly the system prefix — the state must sit at that boundary. */ + if ((int)sl->state->pos != sys_len) + return -1; + + /* Phase 10b: if another slot already holds a snapshot of this EXACT + * system prefix, share it (refcounted) instead of materializing + + * deep-copying another ~68 MiB host image — the content depends only on + * the weights + the prefix tokens, so any slot's snapshot of a byte- + * identical prefix is interchangeable. The sid pinning that guards + * RESUME lives in bs_sys_snap_matches (per-slot sys_sid), which is + * per-slot state and unaffected by sharing the snapshot bytes. Safe to + * scan: sys_snap/sys_len/sys_tokens are only mutated under the GPU + * turn, which this caller holds. */ + for (int i = 0; i < sched->cap; i++) { + bs_slot *o = &sched->slots[i]; + if (o == sl || !o->sys_snap || o->sys_len != sys_len || + memcmp(o->sys_tokens, tokens, + (size_t)sys_len * sizeof(uint32_t)) != 0) + continue; + if (!o->sys_snap_refs) { + o->sys_snap_refs = malloc(sizeof(int)); + if (!o->sys_snap_refs) + break; /* OOM: fall through to a private capture */ + *o->sys_snap_refs = 1; + } + uint32_t *st_tok = realloc(sl->sys_tokens, + (size_t)sys_len * sizeof(uint32_t)); + if (!st_tok) + break; /* fall through to a private capture */ + memcpy(st_tok, tokens, (size_t)sys_len * sizeof(uint32_t)); + bs_sys_snap_unref(sl); + sl->sys_snap = o->sys_snap; + sl->sys_snap_refs = o->sys_snap_refs; + ++(*sl->sys_snap_refs); + sl->sys_tokens = st_tok; + sl->sys_len = sys_len; + pthread_mutex_lock(&sched->mu); + snprintf(sl->sys_sid, sizeof(sl->sys_sid), "%s", sid); + pthread_mutex_unlock(&sched->mu); + fprintf(stderr, "[batch_sched] sys-prefix SHARED slot=%d <- slot=%d " + "sid=%s: %d tokens (refs=%d)\n", idx, i, sid, sys_len, + *sl->sys_snap_refs); + return 0; + } + +#ifdef USE_CUDA + /* Make the host mirror self-contained (GPU SSM image D2H + stale KV + * mirror refresh) before copying it — the same D2H the L2 yield protocol + * performs at every chunk boundary, so continuing the prefill afterwards + * is unaffected (the GPU state is read, not invalidated, and no other + * forward can interleave: the GPU turn is held). */ + if (pn_qwen35moe_gpu_state_to_host(sched->model, sl->state) != 0) + return -1; +#endif + + void *snap = pn_qwen35moe_state_snapshot(sl->state->kv_cache, sys_len); + if (!snap) + return -1; + uint32_t *st = realloc(sl->sys_tokens, (size_t)sys_len * sizeof(uint32_t)); + if (!st) { + pn_qwen35moe_state_snapshot_free(snap); + return -1; + } + memcpy(st, tokens, (size_t)sys_len * sizeof(uint32_t)); + bs_sys_snap_unref(sl); + sl->sys_snap = snap; + sl->sys_snap_refs = NULL; /* private until another slot shares it */ + sl->sys_tokens = st; + sl->sys_len = sys_len; + pthread_mutex_lock(&sched->mu); /* sys_sid is read by the affinity scans */ + snprintf(sl->sys_sid, sizeof(sl->sys_sid), "%s", sid); + pthread_mutex_unlock(&sched->mu); + fprintf(stderr, "[batch_sched] sys-prefix captured slot=%d sid=%s: " + "%d tokens\n", idx, sid, sys_len); + return 0; +} + +/* PC-batched — snapshot the slot's live SSM + KV into its pn_state host mirror + * and publish the driver-maintained committed-token log as this slot's warm + * history for next-turn delta reuse. Owner-called after decode (before + * release); takes + releases the GPU turn itself. No-op if the slot was not + * armed for caching (no session id at bind). */ +int bs_slot_finalize(pn_batch_sched *sched, int idx) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return -1; + + bs_slot *sl = &sched->slots[idx]; + + /* Is this turn armed for caching? (armed_sid set at bind; driver appends to + * ct under mu. The owner is the sole toucher of this slot post-decode.) */ + pthread_mutex_lock(&sched->mu); + int armed = (sl->armed_sid[0] != '\0'); + pthread_mutex_unlock(&sched->mu); + if (!armed) + return 0; /* not caching this turn */ + + bs_gpu_acquire(sched); /* exclusive: no decode step / rebuild can run */ + /* Read gpu_idx UNDER the GPU turn: rebuild_gpu_map (which relocates a + * slot's gpu_idx via decode_slot_move) only runs inside the driver's turn, + * so while we hold the turn gpu_idx cannot move out from under us. */ + pthread_mutex_lock(&sched->mu); + int gpu_idx = sl->gpu_idx; + pthread_mutex_unlock(&sched->mu); + int rc = 0; +#ifdef USE_CUDA + if (gpu_idx < 0) { + rc = -1; /* slot never bound a GPU decode state (e.g. EOS at token 1) */ + } else { + /* Refresh the host mirror and pull this slot's per-slot SSM images + * back into the pn_state host mirror. Phase 10b lazy-mirror mode: + * KV stays in the GPU slot ring between turns (the warm resume path + * re-uses it in place; a rebind seeds it D2D), so only the SSM half + * is captured — materializing the KV here would recreate the exact + * per-session host footprint lazy mode removes. Eager mode keeps + * the full legacy capture. */ + if (pn_qwen35moe_host_kv_mirror_eager()) { + if (pn_qwen35moe_gpu_state_to_host(sched->model, sl->state) != 0) + rc = -1; + } else if (pn_qwen35moe_gpu_state_ssm_to_host(sched->model, + sl->state) != 0) { + rc = -1; + } + if (rc == 0 && + pn_qwen35moe_decode_slot_ssm_to_host(sched->model, gpu_idx, + sl->state) != 0) + rc = -1; + } +#else + (void)gpu_idx; +#endif + bs_gpu_release(sched); + + pthread_mutex_lock(&sched->mu); + if (rc != 0) { + /* Warm state is untrustworthy — disarm + invalidate so a future + * affinity hit re-full-prefills rather than reusing a stale snapshot. */ + sl->armed_sid[0] = '\0'; + sl->sid[0] = '\0'; + sl->hist_n = 0; + bs_slot_clear_projections(sl); + pthread_mutex_unlock(&sched->mu); + return -1; + } + + /* Sanity: the committed log length must equal the state position — that is + * the invariant the whole delta-reuse rests on. If it ever diverges, do + * not publish a mismatched history. */ + if ((int)sl->state->pos != sl->ct_n) { + sl->armed_sid[0] = '\0'; + sl->sid[0] = '\0'; + sl->hist_n = 0; + bs_slot_clear_projections(sl); + pthread_mutex_unlock(&sched->mu); + return -1; + } + + /* Publish the committed log (ct → hist) + session as this slot's warm + * history (read by bs_slot_acquire_biased / bs_prefill_resume). */ + int n_ctx = sl->ct_n; + if (sl->hist_cap < n_ctx) { + uint32_t *h = realloc(sl->hist, (size_t)n_ctx * sizeof(uint32_t)); + if (!h) { + sl->armed_sid[0] = '\0'; + sl->sid[0] = '\0'; + sl->hist_n = 0; + bs_slot_clear_projections(sl); + pthread_mutex_unlock(&sched->mu); + return -1; + } + sl->hist = h; + sl->hist_cap = n_ctx; + } + memcpy(sl->hist, sl->ct, (size_t)n_ctx * sizeof(uint32_t)); + sl->hist_n = n_ctx; + bs_slot_publish_projections(sl, n_ctx); + snprintf(sl->sid, sizeof(sl->sid), "%s", sl->armed_sid); + sl->armed_sid[0] = '\0'; /* consumed */ + pthread_mutex_unlock(&sched->mu); + return 0; +} + +/* ============================================================ + * Session warm-tier EXPORT / RESTORE / RELEASE — batched arm + * (see the block comment in batch_sched.h) + * ============================================================ */ + +/* Reserve an IDLE slot for an out-of-band (non-request) operation by marking + * it PREFILL: try_claim_idle then skips it, the driver excludes PREFILL lanes + * from its active set, and the owner-only warm-state and boundary-snapshot + * fields become ours. Under mu. Paired with bs_slot_unreserve_locked. */ +static void bs_slot_reserve_locked(bs_slot *sl) +{ + sl->status = BS_SLOT_PREFILL; +} + +/* Release an out-of-band reservation: back to IDLE and wake anyone queued on + * a free slot (the D1 waiters). Under mu. */ +static void bs_slot_unreserve_locked(pn_batch_sched *sched, bs_slot *sl) +{ + sl->status = BS_SLOT_IDLE; + pthread_cond_broadcast(&sched->cv_idle); +} + +bs_session_result bs_session_export(pn_batch_sched *sched, const char *sid, + void **snap_out, uint32_t **tokens_out, + int *n_tok_out) +{ + if (snap_out) *snap_out = NULL; + if (tokens_out) *tokens_out = NULL; + if (n_tok_out) *n_tok_out = 0; + if (!sched || !sid || !sid[0] || !snap_out) + return BS_SESSION_ERROR; + if (!bs_sys_snapshot_supported(sched)) + return BS_SESSION_ERROR; + + /* Locate this session's warm slot. A slot that is not IDLE is mid-turn: + * its pos runs ahead of the committed history and the driver is still + * appending, so it is not a clean export point (the single-user path + * refuses the same shape via `state->pos != n_tok`). Report BUSY so the + * caller can tell "retry after the turn" from "no such session". */ + pthread_mutex_lock(&sched->mu); + int idx = -1, busy = 0; + for (int i = 0; i < sched->cap; i++) { + bs_slot *s = &sched->slots[i]; + if (!s->sid[0] || strncmp(s->sid, sid, sizeof(s->sid)) != 0) + continue; + if (s->status != BS_SLOT_IDLE) { busy = 1; continue; } + if (s->hist_n <= 0) + continue; + idx = i; + break; + } + if (idx < 0) { + pthread_mutex_unlock(&sched->mu); + return busy ? BS_SESSION_BUSY : BS_SESSION_NOT_FOUND; + } + bs_slot *sl = &sched->slots[idx]; + bs_slot_reserve_locked(sl); + int n_tok = sl->hist_n; + pthread_mutex_unlock(&sched->mu); + + /* The committed log and the state position must agree — that invariant is + * what makes the snapshot a coherent resume point (bs_slot_finalize + * enforces it at publish time; re-check because we own the slot now). */ + bs_session_result rc = BS_SESSION_OK; + if ((int)sl->state->pos != n_tok) + rc = BS_SESSION_ERROR; + + void *snap = NULL; + if (rc == BS_SESSION_OK) { + bs_gpu_acquire(sched); /* exclusive: no decode step can move the ring */ +#ifdef USE_CUDA + /* INV3: the KV rows live in the GPU decode-slot ring, not in a host + * mirror. gpu_state_to_host drives qw_kv_layer_materialize for every + * layer still flagged stale, bounded to [0, min(pos, max_seq)), and + * fails loudly if no ring holds the content rather than serialising a + * stale mirror. The SSM half of the slot's mirror is already current + * (bs_slot_finalize pulled the per-slot images down at commit time). */ + if (pn_qwen35moe_gpu_state_to_host(sched->model, sl->state) != 0) { + /* No GPU ring still holds this session's rows — another session + * took the ring after this slot was released. The state is gone, + * not broken: report NOT_FOUND so the client falls back to replay, + * and drop the warm binding so no later turn tries to append-resume + * onto content that no longer exists. */ + fprintf(stderr, "[batch_sched] session export slot=%d sid=%s: " + "KV materialize failed (no ring source) — warm state " + "invalidated\n", idx, sid); + pthread_mutex_lock(&sched->mu); + sl->sid[0] = '\0'; + sl->hist_n = 0; + bs_slot_clear_projections(sl); + pthread_mutex_unlock(&sched->mu); + rc = BS_SESSION_NOT_FOUND; + } +#endif + if (rc == BS_SESSION_OK) { + snap = pn_qwen35moe_state_snapshot(sl->state->kv_cache, n_tok); + if (!snap) + rc = BS_SESSION_ERROR; + } + bs_gpu_release(sched); + } + + /* Copy the committed history out while we still hold the slot. */ + uint32_t *toks = NULL; + if (rc == BS_SESSION_OK) { + toks = (uint32_t *)malloc((size_t)n_tok * sizeof(uint32_t)); + if (!toks) { + pn_qwen35moe_state_snapshot_free(snap); + snap = NULL; + rc = BS_SESSION_ERROR; + } else { + memcpy(toks, sl->hist, (size_t)n_tok * sizeof(uint32_t)); + } + } + + pthread_mutex_lock(&sched->mu); + bs_slot_unreserve_locked(sched, sl); + pthread_mutex_unlock(&sched->mu); + + if (rc != BS_SESSION_OK) + return rc; + *snap_out = snap; + if (tokens_out) *tokens_out = toks; else free(toks); + if (n_tok_out) *n_tok_out = n_tok; + fprintf(stderr, "[batch_sched] session exported slot=%d sid=%s: %d tokens\n", + idx, sid, n_tok); + return BS_SESSION_OK; +} + +bs_session_result bs_session_restore(pn_batch_sched *sched, const char *sid, + const void *snap, const uint32_t *tokens, + int n_tok) +{ + if (!sched || !sid || !sid[0] || !snap || !tokens || n_tok <= 0) + return BS_SESSION_ERROR; + if (!bs_sys_snapshot_supported(sched)) + return BS_SESSION_ERROR; + if (pn_qwen35moe_state_snapshot_pos(snap) != n_tok) + return BS_SESSION_ERROR; + + /* Prefer this session's own slot (a re-restore replaces its warm state), + * then any IDLE slot. Never displace an active one. */ + pthread_mutex_lock(&sched->mu); + int idx = -1; + for (int i = 0; i < sched->cap && idx < 0; i++) { + bs_slot *s = &sched->slots[i]; + if (s->status == BS_SLOT_IDLE && s->sid[0] && + strncmp(s->sid, sid, sizeof(s->sid)) == 0) + idx = i; + } + for (int i = 0; i < sched->cap && idx < 0; i++) + if (sched->slots[i].status == BS_SLOT_IDLE) + idx = i; + if (idx < 0) { + pthread_mutex_unlock(&sched->mu); + return BS_SESSION_BUSY; + } + bs_slot *sl = &sched->slots[idx]; + bs_slot_reserve_locked(sl); + pthread_mutex_unlock(&sched->mu); + + /* Grow the history buffer BEFORE mutating the state, so an OOM leaves the + * slot's previous warm state intact rather than half-replaced. */ + bs_session_result rc = BS_SESSION_OK; + if (sl->hist_cap < n_tok) { + uint32_t *h = realloc(sl->hist, (size_t)n_tok * sizeof(uint32_t)); + if (!h) + rc = BS_SESSION_ERROR; + else { sl->hist = h; sl->hist_cap = n_tok; } + } + + if (rc == BS_SESSION_OK) { + bs_gpu_acquire(sched); + /* Host write into THIS slot's own mirrors; state_restore clears + * kv_mirror_stale (the mirror is now authoritative — this is the one + * case INV3 keeps host-sourced) and bumps the KV content generation, so + * every ring watermark still carrying the old generation stops matching + * and the next prefill re-uploads [0, n_tok) from the restored mirror. + * The explicit forget covers the SSM shadows the same way — exactly the + * SYS1-batched boundary-restore protocol (bs_sys_snap_restore). */ + if (pn_qwen35moe_state_restore(sl->state->kv_cache, snap) != 0) { + rc = BS_SESSION_ERROR; + } else { + sl->state->pos = n_tok; +#ifdef USE_CUDA + pn_qwen35moe_gpu_forget_state(sched->model, sl->state); +#endif + } + bs_gpu_release(sched); + } + + pthread_mutex_lock(&sched->mu); + if (rc == BS_SESSION_OK) { + memcpy(sl->hist, tokens, (size_t)n_tok * sizeof(uint32_t)); + sl->hist_n = n_tok; + /* An imported history carries no think-elision span (export does not + * ship one), so the restored session's next turn falls back to the + * exact-prefix rung and establishes a span of its own. */ + bs_slot_clear_projections(sl); + snprintf(sl->sid, sizeof(sl->sid), "%s", sid); + sl->armed_sid[0] = '\0'; + } else { + /* The state may have been partially written — do not advertise it. */ + sl->sid[0] = '\0'; + sl->hist_n = 0; + bs_slot_clear_projections(sl); + } + bs_slot_unreserve_locked(sched, sl); + pthread_mutex_unlock(&sched->mu); + + /* The restored image is the new truth for this session; a boundary + * snapshot captured under its old history must not out-rank it. */ + if (rc == BS_SESSION_OK && sl->sys_sid[0] && + strncmp(sl->sys_sid, sid, sizeof(sl->sys_sid)) == 0) + bs_sys_snap_drop(sched, sl); + + if (rc == BS_SESSION_OK) + fprintf(stderr, "[batch_sched] session restored slot=%d sid=%s: " + "%d tokens\n", idx, sid, n_tok); + return rc; +} + +/* Shared body of bs_session_release / _by_prefix. `n` == 0 means "exact + * match on the whole id"; n > 0 means "match the first n characters". */ +static void bs_session_release_match(pn_batch_sched *sched, const char *key, + size_t n) +{ + for (int i = 0; i < sched->cap; i++) { + bs_slot *sl = &sched->slots[i]; + pthread_mutex_lock(&sched->mu); + int mine = (sl->status == BS_SLOT_IDLE && sl->sid[0] && + (n ? strncmp(sl->sid, key, n) == 0 + : strncmp(sl->sid, key, sizeof(sl->sid)) == 0)); + int snap_mine = (sl->status == BS_SLOT_IDLE && sl->sys_sid[0] && + (n ? strncmp(sl->sys_sid, key, n) == 0 + : strncmp(sl->sys_sid, key, + sizeof(sl->sys_sid)) == 0)); + if (mine) { + sl->sid[0] = '\0'; + sl->hist_n = 0; + bs_slot_clear_projections(sl); + sl->armed_sid[0] = '\0'; + } + pthread_mutex_unlock(&sched->mu); + /* bs_sys_snap_drop takes mu itself for the sys_sid clear. */ + if (snap_mine) + bs_sys_snap_drop(sched, sl); + if (mine || snap_mine) + fprintf(stderr, "[batch_sched] session released slot=%d key=%s\n", + i, key); + } +} + +void bs_session_release(pn_batch_sched *sched, const char *sid) +{ + if (!sched || !sid || !sid[0]) + return; + bs_session_release_match(sched, sid, 0); +} + +void bs_session_release_by_prefix(pn_batch_sched *sched, + const char *base_prefix) +{ + if (!sched || !base_prefix || !base_prefix[0]) + return; + bs_session_release_match(sched, base_prefix, strlen(base_prefix)); +} + +void bs_prefill_yield(pn_batch_sched *sched, int idx) +{ +#ifdef USE_CUDA + if (!sched || idx < 0 || idx >= sched->cap) + return; + + /* Only worth a handoff when the driver actually has a step to run + * (gate open: >=1 active slot, tokens ready). Racing a token post / + * release here is harmless — worst case one missed (or one empty) + * interleave window. */ + pthread_mutex_lock(&sched->mu); + int runnable = gate_ok(sched); + pthread_mutex_unlock(&sched->mu); + if (!runnable) + return; /* keep the turn; nothing to interleave */ + + /* Phase 9b: a pipelined prefill chunk deliberately returns with die B's + * segment still in flight. This is the point where the GPU turn is handed + * to somebody else, so drain both dies FIRST — explicitly, rather than + * leaning on state_to_host's per-layer syncs, which skip any layer whose + * GPU buffer is not currently this state's image and are therefore not a + * guaranteed full drain. ~1 µs on an idle stream, only on a real + * handover, and a no-op when the pipeline is off. */ + pn_qwen35moe_prefill_drain_cuda(sched->model); + + /* Make this slot's host mirror authoritative BEFORE letting any other + * forward at the GPU (the prefix-cache eviction protocol): the next + * chunk then re-seeds from the mirror via the ordinary + * seed-on-host-change path even if an interleaved forward claimed the + * live SSM buffers / shared KV ring. On failure keep the turn — + * resuming without a valid mirror would be unsafe, and keeping it is + * always safe (just no interleave). */ + if (pn_qwen35moe_gpu_state_to_host(sched->model, + sched->slots[idx].state) != 0) { + fprintf(stderr, "[batch_sched] prefill_yield: state_to_host failed " + "(slot=%d); continuing without yield\n", idx); + return; + } + + pthread_mutex_lock(&sched->mu); + bs_gpu_release_locked(sched); + pthread_cond_signal(&sched->cv_work); /* the driver may be parked */ + /* Re-acquire FIFO: the driver (and any earlier waiter) goes first — + * this is the chunk → step → chunk alternation. */ + bs_gpu_acquire_locked(sched); + pthread_mutex_unlock(&sched->mu); +#else + (void)sched; (void)idx; +#endif +} + +void bs_prefill_abort(pn_batch_sched *sched, int idx) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return; + bs_gpu_release(sched); + bs_slot_release(sched, idx); +} + +void bs_prefill_abort_reset(pn_batch_sched *sched, int idx) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return; + /* Still owning the GPU turn here (the range helper re-acquires after every + * yield), which is bs_slot_reset_state's precondition. */ + bs_slot_reset_state(sched, idx); + bs_gpu_release(sched); + bs_slot_release(sched, idx); +} + +/* Shared bind body. `sp == NULL` = legacy (owner-side sampling via the + * logits handshake); `sp != NULL` = L3 (arm the driver's per-slot sampler: + * copy the params + repeat window, allocate the output ring sized to the + * full budget so it can never overflow). */ +static int bs_slot_bind_common(pn_batch_sched *sched, int idx, + uint32_t first_token, + const pn_sampling_params *sp, + const uint32_t *recent, int n_recent, + int max_new, + const char *sid, const uint32_t *prompt, + int n_prompt) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return -1; + +#ifdef USE_CUDA + uint32_t *rec_copy = NULL; + uint32_t *ring = NULL; + int max_recent = 0; + + /* PC-batched: cache this turn only when a session id AND the full prompt + * are supplied AND driver-sampling is on (the committed log is driver-fed; + * legacy lockstep is left non-caching). Pre-size the committed-token log + * to prompt + every possibly-stepped decode token so the driver never + * reallocs it mid-step under mu. */ + int cache_turn = (sp && sid && sid[0] && prompt && n_prompt > 0); + uint32_t *ct_buf = NULL; + int ct_cap = 0; + + if (sp) { + max_recent = sp->repeat_last_n > 0 ? sp->repeat_last_n : 0; + if (n_recent > max_recent) + n_recent = max_recent; /* defensive; caller passes the window */ + if (max_recent > 0) { + rec_copy = malloc((size_t)max_recent * sizeof(uint32_t)); + if (!rec_copy) + goto fail; + if (n_recent > 0) + memcpy(rec_copy, recent, (size_t)n_recent * sizeof(uint32_t)); + } + if (max_new > 0) { + ring = malloc((size_t)max_new * sizeof(uint32_t)); + if (!ring) + goto fail; + } + } + /* THINK-ELISION: a turn that resumed by eliding the previous think block + * committed hist[0, ct_keep) followed by prompt[ct_prompt_from ..) — NOT + * the prompt as given. Seed the log to match the KV, or the ct_n == + * state->pos invariant finalize checks fails and the turn goes uncached. + * Both fields are 0 on every other path, which reduces to the plain copy. */ + int ct_keep = sched->slots[idx].ct_keep; + int ct_from = sched->slots[idx].ct_prompt_from; + int ct_seed_n = ct_keep + (n_prompt - ct_from); + if (cache_turn) { + ct_cap = ct_seed_n + (max_new > 0 ? max_new : 0); + ct_buf = malloc((size_t)ct_cap * sizeof(uint32_t)); + if (!ct_buf) + goto fail; /* rec_copy/ring freed at fail */ + if (ct_keep > 0) + memcpy(ct_buf, sched->slots[idx].hist, + (size_t)ct_keep * sizeof(uint32_t)); + memcpy(ct_buf + ct_keep, prompt + ct_from, + (size_t)(n_prompt - ct_from) * sizeof(uint32_t)); + } + + /* The GPU turn is owned (bs_prefill_begin) — this whole section is + * atomic w.r.t. every other forward on this model. */ + + /* The batched decode seeds each slot from this state, and a GPU prefill + * leaves the host mirror stale (B2). Refresh the B3 SSM host copy now, + * while the live GPU state is still this request's (the slot SSM seed + * in decode_slot_bind reads the host mirror). Phase 10b lazy-mirror + * mode skips the KV half: the prefill rows are in the single-user GPU + * ring and the first slots step D2D-seeds the slot ring from it — no + * host round-trip. Eager mode keeps the full legacy capture. */ + if (pn_qwen35moe_host_kv_mirror_eager()) { + if (pn_qwen35moe_gpu_state_to_host(sched->model, + sched->slots[idx].state) != 0) { + fprintf(stderr, "[batch_sched] state_to_host failed (slot=%d)\n", + idx); + goto fail; + } + } else if (pn_qwen35moe_gpu_state_ssm_to_host(sched->model, + sched->slots[idx].state) + != 0) { + fprintf(stderr, "[batch_sched] ssm_to_host failed (slot=%d)\n", idx); + goto fail; + } + + /* Lazy lane registration on the first bind ever (serialized by the GPU + * turn; runs at most once). Registering lane idx here uses this state's + * now-authoritative host mirror, and re-binding it below is harmless. */ + if (!sched->slots_gpu_init) { + if (bs_lazy_init_gpu(sched) != 0) + goto fail; + } + + /* Pick a FREE GPU slot index for this joiner: the lowest GPU index not + * currently claimed by ANY active slot's gpu_idx. This is the + * continuous-batching fix — binding at the SCHEDULER slot index (the old + * code) could, after a compaction diverged gpu-index from sched-index, + * land on top of an in-flight decoder and corrupt it (decode_slots then + * failed its slot_state validation). + * + * Scanning claimed gpu_idx (not n_active_gpu) is required for the + * CONCURRENT-START case: several joiners bind back-to-back, each under + * the GPU turn, BEFORE the driver rebuilds — so n_active_gpu is still + * stale and would hand them all the same index. Every active slot's + * gpu_idx is already set at its own bind, so the scan sees them. Read + * under mu (the driver and other binds write gpu_idx under mu). */ + pthread_mutex_lock(&sched->mu); + bool claimed[BS_MAX_SLOTS] = { false }; + for (int i = 0; i < sched->cap; i++) { + bs_slot *o = &sched->slots[i]; + if (i != idx && slot_is_active(o) && o->gpu_idx >= 0 && + o->gpu_idx < sched->cap) + claimed[o->gpu_idx] = true; + } + /* Scan from the TOP down for the highest free GPU index. Binding high + * tends to keep a joiner's gpu_idx >= its compacted rank, so the common + * case is a downward compaction; but this is only a hint now, not a + * correctness requirement — rebuild_gpu_map realizes an arbitrary + * permutation, so the >=5-concurrent-joiners layout that forces an + * upward move / cycle is handled correctly rather than refused. */ + int gpu_slot = -1; + for (int g = sched->cap - 1; g >= 0; g--) + if (!claimed[g]) { gpu_slot = g; break; } + pthread_mutex_unlock(&sched->mu); + if (gpu_slot < 0) { + /* Every GPU index claimed — cannot happen (at most `cap` slots exist + * and this one is not yet active), but fail cleanly if it does. */ + fprintf(stderr, "[batch_sched] bind: no free GPU slot (slot=%d)\n", + idx); + goto fail; + } + + /* Snapshot this request's post-prefill SSM state into GPU slot gpu_slot. + * cap+1 slots are bound (top index reserved as rebuild scratch). */ + if (pn_qwen35moe_decode_slot_bind(sched->model, + sched->slots[idx].state, + gpu_slot, sched->cap + 1) != 0) { + fprintf(stderr, "[batch_sched] slot_bind failed (slot=%d gpu=%d)\n", + idx, gpu_slot); + goto fail; + } + + pthread_mutex_lock(&sched->mu); + { + bs_slot *sl = &sched->slots[idx]; + sl->gpu_idx = gpu_slot; + sl->next_token = first_token; + sl->token_ready = false; /* driver reads next_token directly on BOUND */ + sl->logits_ready = false; + sl->logits = NULL; + sl->step_error = false; + if (sp) { + sl->sp = *sp; + sl->recent = rec_copy; + sl->n_recent = n_recent; + sl->max_recent = max_recent; + sl->ring = ring; + sl->ring_cap = max_new > 0 ? max_new : 0; + sl->ring_r = 0; + sl->ring_w = 0; + sl->budget = max_new; + /* max_tokens == 1: nothing for the driver to step. */ + sl->finished = (max_new <= 0); + } else { + sl->recent = NULL; + sl->n_recent = 0; + sl->max_recent = 0; + sl->ring = NULL; + sl->ring_cap = 0; + sl->ring_r = 0; + sl->ring_w = 0; + sl->budget = 0; + sl->finished = false; + } + /* PC-batched: arm the committed-token log so the driver records every + * stepped token. ct is seeded with what the KV actually holds (== the + * prompt, except on a think-elided resume — see ct_keep above), so the + * invariant ct_n == state->pos holds from here. The old ct buffer + * (from this slot's previous request) is replaced. */ + if (cache_turn) { + free(sl->ct); + sl->ct = ct_buf; + sl->ct_cap = ct_cap; + sl->ct_n = ct_seed_n; + snprintf(sl->armed_sid, sizeof(sl->armed_sid), "%s", sid); + } else { + sl->armed_sid[0] = '\0'; /* not caching this turn */ + sl->ct_n = 0; + } + sl->pend_vis_n = 0; + sl->pend_vis_think_n = 0; + sl->status = BS_SLOT_BOUND; + } + /* A new user is joining the active set — the driver must rebuild the + * compacted GPU map before the next step to include this slot. */ + sched->active_set_changed = true; + pthread_cond_signal(&sched->cv_work); /* gate may open */ + bs_gpu_release_locked(sched); + pthread_mutex_unlock(&sched->mu); + return 0; + +fail: + free(rec_copy); + free(ring); + free(ct_buf); + bs_gpu_release(sched); + fprintf(stderr, "[batch_sched] bind failed; slot %d released, " + "request degrades to an error\n", idx); + bs_slot_release(sched, idx); + return -1; +#else + /* Non-CUDA build: sched is always NULL so this is never reached. */ + (void)first_token; (void)sp; (void)recent; (void)n_recent; (void)max_new; + (void)sid; (void)prompt; (void)n_prompt; + return -1; +#endif +} + +int bs_slot_bind(pn_batch_sched *sched, int idx, uint32_t first_token) +{ + return bs_slot_bind_common(sched, idx, first_token, NULL, NULL, 0, 0, + NULL, NULL, 0); +} + +int bs_slot_bind_ex(pn_batch_sched *sched, int idx, uint32_t first_token, + const pn_sampling_params *sp, + const uint32_t *recent, int n_recent, int max_new, + const char *sid, const uint32_t *prompt, int n_prompt) +{ + if (!sp) + return -1; + return bs_slot_bind_common(sched, idx, first_token, sp, recent, n_recent, + max_new, sid, prompt, n_prompt); +} + +void bs_slot_arm_reasoning_budget(pn_batch_sched *sched, int idx, + int reason_budget, bool in_think, + const uint32_t *close_ids, int n_close, + uint32_t first_token) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return; + if (!sched->driver_sample) + return; /* legacy lockstep path caps in the owner loop, not here */ + int cap = (int)(sizeof(((bs_slot *)0)->reason_close) / + sizeof(((bs_slot *)0)->reason_close[0])); + if (n_close > cap) n_close = cap; + + pthread_mutex_lock(&sched->mu); + bs_slot *sl = &sched->slots[idx]; + sl->reason_budget = reason_budget > 0 ? reason_budget : 0; + sl->reason_in_think = (sl->reason_budget > 0) && in_think && n_close > 0; + sl->reason_toks = 0; + sl->reason_forced = false; + sl->reason_forced_call = false; + sl->reason_close_n = (sl->reason_budget > 0) ? n_close : 0; + sl->reason_close_i = sl->reason_close_n; /* nothing queued yet */ + for (int i = 0; i < sl->reason_close_n && close_ids; i++) + sl->reason_close[i] = close_ids[i]; + /* The owner holds token #1 (first_token) and will em_feed it itself; count + * it here iff it was sampled inside , so the driver's per-step count + * stays aligned with the single-user loop (which counts every token + * including the first). A first_token that is itself the "" close + * means the model concluded immediately — leave . */ + if (sl->reason_in_think) { + sl->reason_toks = 1; + if (sl->reason_close_n == 1 && first_token == sl->reason_close[0]) + sl->reason_in_think = false; + else if (sl->reason_budget > 0 && sl->reason_toks >= sl->reason_budget) { + /* Degenerate budget of 1: trip on the first token. */ + sl->reason_forced = true; + sl->reason_close_i = 0; + } + } + pthread_mutex_unlock(&sched->mu); +} + +/* Wait for logits from the driver (shared between await_first and step_wait). + * Called with sched->mu held; returns with it released. */ +static int slot_wait_logits(pn_batch_sched *sched, bs_slot *sl, + float **logits_out) +{ + while (!sl->logits_ready && !sched->shutdown) + pthread_cond_wait(&sl->cv, &sched->mu); + + if (sched->shutdown || !sl->logits) { + pthread_mutex_unlock(&sched->mu); + return -1; + } + + *logits_out = sl->logits; + sl->logits_ready = false; + pthread_mutex_unlock(&sched->mu); + return 0; +} + +int bs_slot_await_first(pn_batch_sched *sched, int idx, float **logits_out) +{ + if (!sched || idx < 0 || idx >= sched->cap || !logits_out) + return -1; + pthread_mutex_lock(&sched->mu); + /* The first token was already deposited via bs_slot_bind; just wait. */ + return slot_wait_logits(sched, &sched->slots[idx], logits_out); +} + +int bs_slot_step_wait(pn_batch_sched *sched, int idx, + uint32_t next_token, float **logits_out) +{ + if (!sched || idx < 0 || idx >= sched->cap || !logits_out) + return -1; + + pthread_mutex_lock(&sched->mu); + + bs_slot *sl = &sched->slots[idx]; + + /* Deposit this step's token for the driver, then wait for its logits. */ + sl->next_token = next_token; + sl->token_ready = true; + pthread_cond_signal(&sched->cv_work); + + return slot_wait_logits(sched, sl, logits_out); +} + +int bs_slot_pop_token(pn_batch_sched *sched, int idx, uint32_t *tok) +{ + if (!sched || idx < 0 || idx >= sched->cap || !tok) + return -1; + pthread_mutex_lock(&sched->mu); + bs_slot *sl = &sched->slots[idx]; + for (;;) { + /* Drain order matters: queued tokens first (an EOS the driver + * pushed must reach the owner), then error, then clean finish. */ + if (sl->ring && sl->ring_r < sl->ring_w) { + *tok = sl->ring[sl->ring_r++]; + pthread_mutex_unlock(&sched->mu); + return 1; + } + if (sched->shutdown || sl->step_error) { + pthread_mutex_unlock(&sched->mu); + return -1; + } + if (sl->finished) { + pthread_mutex_unlock(&sched->mu); + return 0; + } + pthread_cond_wait(&sl->cv, &sched->mu); + } +} + +bool bs_driver_sampling(const pn_batch_sched *sched) +{ + return sched && sched->driver_sample; +} + +void bs_slot_release(pn_batch_sched *sched, int idx) +{ + if (!sched || idx < 0 || idx >= sched->cap) + return; + pthread_mutex_lock(&sched->mu); + bs_slot *sl = &sched->slots[idx]; + /* Only the owner calls release, and it does so exactly once, so any + * non-IDLE status here belongs to this owner: free the slot directly. + * The driver never retires slots on its own — a mid-step release just + * means this lane's logits are discarded at deposit time. (The driver + * only touches the L3 ring/recent under mu with status == DECODE, so + * freeing them here cannot race a deposit.) */ + bool was_active = (sl->status == BS_SLOT_BOUND || + sl->status == BS_SLOT_DECODE); + if (sl->status != BS_SLOT_IDLE) { + sl->status = BS_SLOT_IDLE; + sl->logits = NULL; + sl->logits_ready = false; + sl->token_ready = false; + free(sl->recent); + sl->recent = NULL; + sl->n_recent = 0; + sl->max_recent = 0; + free(sl->ring); + sl->ring = NULL; + sl->ring_cap = 0; + sl->ring_r = 0; + sl->ring_w = 0; + sl->budget = 0; + sl->finished = false; + sl->step_error = false; + /* OBSERVABILITY — a reasoning force-close is an ABNORMAL event, so it + * logs unconditionally (not behind a verbose flag). This is the BATCHED + * path, which is what the live fleet runs, so this is the line that + * actually answers "how often is this happening, and to which model". + * It is the one condition under which the grammar compels the tool-call + * opener while the model is still mid-thought — how a half-written call + * ends up inside a parameter VALUE (tool_calling.c FRAME_MARKERS). + * Logged from release (not from the injection point) so the OUTCOME — + * whether a tool call actually closed afterwards — is on the same line. + * Held under sched->mu, like every other slot-field read here. */ + if (sl->reason_forced) + fprintf(stderr, "[reason_budget] FORCED close model=%s path=batched " + "slot=%d sid=%s reason_toks=%d/%d tool_call=%s\n", + sched->model_name[0] ? sched->model_name : "?", + idx, sl->sid[0] ? sl->sid : "-", + sl->reason_toks, sl->reason_budget, + sl->reason_forced_call ? "yes" : "no"); + /* Clear the AUTO reasoning-budget arm so a reused slot starts clean. */ + sl->reason_budget = 0; + sl->reason_toks = 0; + sl->reason_in_think = false; + sl->reason_forced = false; + sl->reason_forced_call = false; + sl->reason_close_n = 0; + sl->reason_close_i = 0; + /* PC-batched: clear the per-turn caching arm. A successful + * bs_slot_finalize already consumed armed_sid (and published sid/hist); + * clearing here covers the error/no-finalize path so a stale arm can't + * mislead the next request that reuses this slot. sid/hist (the warm + * state) are intentionally preserved for session-affinity reuse. */ + sl->armed_sid[0] = '\0'; + /* Drop our GPU index. The GPU slot_state[gpu_idx] entry is left as-is + * (harmless): the released slot is excluded from every future + * compacted step (target_sched only lists active slots), and the + * rebuild's compaction overwrites this vacated GPU index when a + * higher-ranked active slot moves down onto it. Clearing GPU state + * here would race the driver's turn-held decode_slots read. */ + sl->gpu_idx = -1; + } + /* A user left the active set — the driver must rebuild the compacted GPU + * map before the next step so the vacated GPU slot is not stepped. */ + if (was_active) + sched->active_set_changed = true; + pthread_cond_signal(&sched->cv_work); /* gate may open (or reshape) */ + pthread_cond_broadcast(&sched->cv_idle); /* a slot is free — wake D1 waiters */ + pthread_mutex_unlock(&sched->mu); +} + +/* ============================================================ + * D1 + D3: bounded overflow queue + admission-as-backpressure + * ============================================================ */ + +/* Compute absolute deadline for pthread_cond_timedwait from a relative + * wait_ms value. Fills *ts with CLOCK_REALTIME deadline. */ +static void make_deadline(struct timespec *ts, int wait_ms) +{ + clock_gettime(CLOCK_REALTIME, ts); + long ns = ts->tv_nsec + (long)wait_ms * 1000000L; + ts->tv_sec += ns / 1000000000L; + ts->tv_nsec = ns % 1000000000L; +} + +bs_acquire_result bs_slot_acquire_or_wait(pn_batch_sched *sched, + int wait_ms, + int *slot_out, + int *retry_after_s, + const char *sid) +{ + if (slot_out) *slot_out = -1; + if (retry_after_s) *retry_after_s = 0; + if (!sched) return BS_ACQUIRE_429; + + pthread_mutex_lock(&sched->mu); + + if (sched->shutdown) { + pthread_mutex_unlock(&sched->mu); + return BS_ACQUIRE_SHUTDOWN; + } + + /* Fast path: slot free and pressure OK — claim immediately. */ + if (sched->pressure_level < BS_PRESSURE_HOLD_LEVEL) { + int s = try_claim_idle(sched, sid); + if (s >= 0) { + pthread_mutex_unlock(&sched->mu); + if (slot_out) *slot_out = s; + return BS_ACQUIRE_OK; + } + } + + /* Slow path: must queue. + * + * Admission decision: + * - If queue is full + pressure CRITICAL → 503 (last resort, no wait). + * - If queue is full (but pressure not CRITICAL) → 429. + * - Otherwise enqueue and wait. */ + if (sched->n_queued >= sched->queue_cap) { + int level = sched->pressure_level; + pthread_mutex_unlock(&sched->mu); + if (retry_after_s) *retry_after_s = 5; + /* 3 = PN_MEM_LEVEL_CRITICAL */ + return (level >= 3) ? BS_ACQUIRE_503 : BS_ACQUIRE_429; + } + + /* Enqueue this waiter. */ + sched->n_queued++; + + /* Compute absolute deadline. */ + struct timespec deadline; + make_deadline(&deadline, wait_ms > 0 ? wait_ms : 0); + + bs_acquire_result result = BS_ACQUIRE_TIMEOUT; + + for (;;) { + if (sched->shutdown) { + result = BS_ACQUIRE_SHUTDOWN; + break; + } + + /* D3: if pressure is TIGHT/CRITICAL, stay queued — don't admit a new + * prefill that could OOM. But if CRITICAL and queue is now full + * (more callers arrived while we waited), fail with 503. */ + if (sched->pressure_level >= BS_PRESSURE_HOLD_LEVEL) { + /* If queue is now at its cap and we're still stuck at the back + * due to pressure, we've been holding a queue slot ourselves — + * count that as fair (we're already in); just keep waiting until + * pressure clears or deadline. */ + if (wait_ms <= 0) { + /* Non-blocking mode: don't wait. */ + result = (sched->pressure_level >= 3) ? BS_ACQUIRE_503 + : BS_ACQUIRE_429; + break; + } + /* Timed wait for pressure to clear or a slot to open. */ + int rc = pthread_cond_timedwait(&sched->cv_idle, &sched->mu, + &deadline); + if (rc != 0) { + /* Timeout. */ + if (retry_after_s) *retry_after_s = 2; + result = BS_ACQUIRE_TIMEOUT; + break; + } + continue; /* re-check all conditions */ + } + + /* Pressure OK: try to claim a slot. */ + int s = try_claim_idle(sched, sid); + if (s >= 0) { + result = BS_ACQUIRE_OK; + sched->n_queued--; + pthread_mutex_unlock(&sched->mu); + if (slot_out) *slot_out = s; + return BS_ACQUIRE_OK; + } + + /* No slot free yet. */ + if (wait_ms <= 0) { + result = BS_ACQUIRE_429; + break; + } + + int rc = pthread_cond_timedwait(&sched->cv_idle, &sched->mu, + &deadline); + if (rc != 0) { + if (retry_after_s) *retry_after_s = 2; + result = BS_ACQUIRE_TIMEOUT; + break; + } + /* Woke up — re-check all conditions from the top. */ + } + + /* Leaving the queue without a slot. */ + sched->n_queued--; + pthread_mutex_unlock(&sched->mu); + return result; +} + +void bs_notify_pressure(pn_batch_sched *sched, int level) +{ + if (!sched) return; + pthread_mutex_lock(&sched->mu); + int old = sched->pressure_level; + sched->pressure_level = level; + /* Wake D1 waiters whenever pressure drops (they might now be able to + * proceed) or whenever it hits CRITICAL (so they can fail fast). */ + if (level < old || level >= 3) + pthread_cond_broadcast(&sched->cv_idle); + pthread_mutex_unlock(&sched->mu); +} + +void bs_get_load(pn_batch_sched *sched, bs_load *out) +{ + if (!out) return; + + /* Single-user (non-batched) path: no scheduler. That path serializes on the + * per-model gen_lock and 429s a second concurrent chat, so it presents as a + * single always-free slot for dispatch. Exact instantaneous busyness there + * is minor (a second request is refused, not queued). */ + if (!sched) { + out->capacity = 1; + out->active_slots = 0; + out->queue_depth = 0; + out->free_slots = 1; + return; + } + + pthread_mutex_lock(&sched->mu); + int busy = 0; + for (int i = 0; i < sched->cap; i++) + if (sched->slots[i].status != BS_SLOT_IDLE) busy++; /* PREFILL/BOUND/DECODE */ + out->capacity = sched->cap; + out->active_slots = busy; + out->free_slots = sched->cap - busy; + out->queue_depth = sched->n_queued; /* D1 overflow-queue waiters */ + pthread_mutex_unlock(&sched->mu); +} diff --git a/src/serve/batch_sched.h b/src/serve/batch_sched.h new file mode 100644 index 0000000..3c568c6 --- /dev/null +++ b/src/serve/batch_sched.h @@ -0,0 +1,1220 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file batch_sched.h + * @brief Per-model batch decode scheduler (C3: one card serves K users in one + * batched GPU forward, removing the same-model 429). + * + * Architecture + * ============ + * Each `mr_entry` that carries a GPU-capable model gets a `pn_batch_sched` + * (gated by QWEN35_BATCH_SLOTS=1; disabled → old trylock→429 behaviour is + * unchanged). The scheduler owns K `pn_state*` instances — one per slot — + * allocated at model-load time (host RAM only; GPU slot state is lazily bound + * on the first real request to avoid hanging on cuMemAlloc right after a long + * weight upload). A single background driver thread drives the + * per-step `pn_qwen35moe_decode_slots` calls; request threads each prefill + * their own prompt, bind their slot, then enter the slot's decode loop, + * exchange tokens with the driver, and stream their own SSE. + * + * Concurrency model (the part three bugs came from — read this first) + * =================================================================== + * ONE mutex (`mu`) + a FIFO GPU-ownership turnstile built on it: + * + * - GPU turnstile (`gpu_ticket_next` / `gpu_ticket_serving` / `cv_gpu`, + * all guarded by `mu`) — the model's single-GPU-owner "lock". The + * model's single-user forward machinery (C2 async phase machine + * `g->as_phase`, per-die scratch, the shared live SSM buffers keyed by + * `d->host_state`, the shared GPU KV ring) supports exactly ONE + * in-flight generation; pre-C3 that was guaranteed by the per-model + * gen_lock, which the batched path does not take. The turnstile + * restores the guarantee: a request thread owns the turn across a + * prefill CHUNK *and* the slot bind (so no other forward can run + * between the last prefill token and the bind / the chunk-boundary + * `state_to_host` — the "bind right after prefill" protocol the SSM + * D2D snapshot requires), and the driver owns it across each + * `decode_slots` step (batched decode shares `sc->r_logits` and the + * CUDA streams with the single-user path, so a step must never overlap + * a prefill token in flight). + * + * Why a ticketed turnstile instead of a plain mutex (the pre-L2/L3 + * `gpu_mu`): pthread mutexes allow barging. With the driver + * free-running (L3: it no longer waits for owners between steps) a + * plain mutex lets it re-acquire the GPU ahead of a waiting or + * chunk-yielding prefiller indefinitely; and two alternating + * prefillers could likewise starve the driver. FIFO tickets make the + * interleave provably fair: chunk → step → chunk → step. A turn + * holder never cond-waits (chunks and steps are bounded GPU work), and + * every ticket is always followed by a release, so the turnstile is + * deadlock-free by construction. + * + * - `mu` — guards slot status words, the token/logits handshake flags, + * the L3 per-slot output rings and the turnstile counters. Every + * pthread_cond_wait predicate is re-checked in a while loop under + * `mu`; every transition another thread waits on is made (and + * signalled) under `mu`, so wakeups cannot be lost. + * + * Slot lifecycle per request (statuses under `mu`): + * IDLE -(bs_slot_acquire)-> PREFILL -(bs_slot_bind)-> BOUND + * -(driver step)----> DECODE -(bs_slot_release)-> IDLE + * Only the OWNER (the request thread) ever moves a slot back to IDLE — the + * driver never frees a slot on its own, so a slot can never be re-acquired + * while its previous owner might still touch it. + * + * Driver step: + * The driver compacts the active lanes (BOUND, or DECODE and not yet + * `finished`) into GPU indices [0, n_active) and steps only those (D2). + * PREFILL lanes are simply excluded from the compacted set — they no + * longer close the gate (pre-L2 they did): their pn_state is never + * passed to decode_slots, and GPU exclusivity against the prefill's own + * forwards is the turnstile's job. This is what lets decode interleave + * a chunked prefill (L2). The driver's gate is: + * + * >=1 slot BOUND/DECODE (not finished) + * AND every unfinished DECODE slot has token_ready. + * + * With in-driver sampling (L3, default) token_ready is set by the driver + * itself right after sampling, so the gate is open continuously while + * any slot decodes — owners are OFF the step critical path entirely. + * + * Liveness: every PREFILL leaves the state in finite time — prefill + * chunks are serialized by the turnstile and complete (bind → BOUND) or + * fail (release → IDLE); at most `cap` requests are admitted, and the + * FIFO tickets interleave decode steps between prefill chunks (L2), so + * decoders make progress even during a long join. Every transition + * that can open the gate signals `cv_work`. + * + * Isolation guarantees: + * - Each slot has its own `pn_state*` (own KV ring, own SSM images via the + * slot-major conv_slots/ssm_slots arrays, own RoPE position). + * - Sampling: with L3 (default) the driver samples every slot itself, + * sequentially, with a per-slot copy of the request's sampling params + + * repeat window — per-slot streams are independent and, at temp 0, + * bit-identical to owner-side sampling (same sample_token, same recent + * window contents at every step). With QWEN35_BS_DRIVER_SAMPLE=0 the + * legacy owner-side sampling (pn_sample_next_token, _Thread_local + * scratch) is restored. + * - The driver READS the next-token word and WRITES the logits pointer / + * output ring; owners read the ring (L3) or logits (legacy); all + * guarded by `mu` + the token_ready/logits_ready flags. + * - No request thread touches another slot's state, logits, or emitter. + * + * 1-user no-regression: + * With one active user the driver compacts to n_active=1 and calls + * decode_slots with K=1 (not the old fixed-cap-8 step); the turnstile is + * uncontended; pn_qwen35moe_decode_slots is documented bit-exact for + * K=1 vs. the single-user forward. The SSE streaming path is unchanged. + * Measured goal: K=1 tok/s ≈ single-user non-batched (~13 tok/s), NOT + * the old 3.4 tok/s from 8-wide batching with 7 dummy lanes. + * + * Fail-soft: + * Any GPU failure (lazy init, slot bind, decode_slots) propagates as an + * error to the owning request thread(s) — logits==NULL wakeups, -1 + * returns — which release their slots; nothing is ever left waiting on a + * condition nobody will signal. All-slots-busy stays a clean 429. + * + * Dynamic-width / continuous batching (Phase D2): + * The driver compacts the active (BOUND/DECODE) slots into GPU indices + * [0, n_active) each step and calls decode_slots with n_active instead of + * cap. IDLE slots pay zero GPU compute. When the active set changes (a + * user joins via bs_slot_bind, leaves via bs_slot_release, or the driver + * marks a slot `finished` — EOS / budget), active_set_changed is set; the + * driver rebuilds the gpu_map[] before the next step with + * pn_qwen35moe_decode_slot_move calls (while owning the GPU turn, so no + * interleaving). Join/leave cost = n_active move calls once per event; + * steady-state = zero. K=1 runs a 1-wide step (single-user speed). + * + * L3 — in-driver sampling + per-slot output ring (default on; + * QWEN35_BS_DRIVER_SAMPLE=0 restores the legacy lockstep): + * bs_slot_bind_ex hands the driver the request's sampling params, the + * repeat window seeded with the prompt tail + first token, and a token + * budget (max_tokens - 1). After each step the driver appends the + * just-stepped token to the slot's repeat window, samples the next token + * from the slot's (host-side, sparse top-k) logits, pushes it to the + * slot's output ring, and immediately re-arms token_ready — the step + * loop never waits on an owner again. Owners drain the ring with + * bs_slot_pop_token and do detokenize + emitter + SSE entirely off the + * critical path: ONE STALLED CLIENT NO LONGER STALLS THE BATCH (the §3.5 + * reliability hazard). The driver stops stepping a slot (finished=true) + * on EOS/<|im_end|> or budget exhaustion; stop-STRING detection stays + * owner-side (text-level), so a slot may over-generate past a stop + * string until its owner releases — bounded by the budget, never + * affecting emitted output (surplus ring tokens are discarded). The + * ring is sized to the full budget up front, so it can never overflow + * and the driver never blocks on a slow consumer. + * + * L2 — chunked prefill (bs_prefill_yield): + * The owner prefills in chunks (serve_generation), releasing the GPU + * turn between chunks so decode steps interleave. At each yield the + * slot's host mirror is made authoritative first + * (pn_qwen35moe_gpu_state_to_host — the prefix-cache eviction protocol), + * so ANY forward that interleaves (a decode step, or even another + * prefill claiming the live SSM buffers / shared KV ring) is safe: the + * resumed chunk re-seeds from the mirror via the ordinary + * seed-on-host-change path. A joiner's decode stall is bounded by one + * chunk instead of the whole prompt. + * + * SYS1-batched — per-slot system-prefix caching (bs_prefill_resume_sys / + * bs_prefill_capture_sys): + * The batched twin of the single-user SYS1 (prefix_cache.h): each slot may + * hold ONE host-side boundary snapshot (state at pos == sys_len), keyed by + * session id + the exact prefix tokens. A divergent-history turn (the + * reasoning-model case, where the warm-history append resume can never + * fire) restores the snapshot into the slot's own pn_state and prefills + * only the conversation. DESIGN: snapshots are per-SLOT (not a shared + * per-session store) — a deliberate conservative bound. Each snapshot is + * a self-contained deep copy of the slot's own host mirrors, captured and + * restored only by the slot's owner under the GPU turnstile, so concurrent + * sessions in different slots can never read or clobber each other's + * state; the shared-GPU hazards are handled by the exact protocols the + * warm-history path already validated on a K80 host (capture = the L2-yield D2H + * that makes the host mirror authoritative; restore = host write + GPU + * forget + seed-on-host-change re-seed). Cost of the conservatism: a + * session that lands on a foreign slot (its own busy) full-prefills and + * its capture replaces that slot's snapshot; the affinity scans (warm sid + * first, then snapshot sys_sid) make that rare. + * + * @status Validated on a K80 host (QWEN35_BATCH_SLOTS=1 to enable; default off). + * L1/L2/L3 gated by tests/test_serve_sched_qwen35 (serve-path + * parity vs the single-user reference, join-during-decode, + * slow-client isolation) + tests/test_users_qwen35 (engine gate). + * SYS1-batched host logic gated by tests/test_prefix_cache_sys; + * proven on a K80 host 2026-07-18 (K80 dies 2,3, Qwen3.6-35B Q3_K, + * QWEN35_BATCH_SLOTS=1, 2 concurrent sessions): per-session + * sys-prefix reuse on divergent turn 2 (TTFT 84.5s -> 3.5s at a + * ~3.1k-token system prefix), zero cross-session leakage. Restored + * turns match the single-user SYS1's fidelity exactly (see the + * prefix_cache.h @issues note: answer-correct, not bit-identical to + * a cold prefill — pre-existing at 85a130e, same primitives). + * THINK-ELISION (Phase 11) measured on a K80 host on nex-n2-mini + saber-orch: + * append-resume 0/6 -> 5/6 turns, re-prefilled tokens 1848 -> 117, + * client TTFT flat with conversation depth instead of linear. + * REASONING-BEARING RESUME: the same rung now also matches a client + * that SENDS `reasoning_content` BACK — the convention that keeps the + * model reasoning at all and + * which, on the exact-history rung alone, re-prefilled every turn + * because a client cannot reproduce the ids sampled inside a think + * block. Host logic gated by tests/test_prefix_cache_sys + * (test_reasoning_bearing_resume) + tests/test_prefix_projection; NOT + * yet measured on a K80 host end to end. + * @issues A turn that reasons and emits NO visible answer cannot be + * projected, so it falls back to the SYS1 boundary (measured 3/6 + * turns on saber-orch at max_tokens 200). Tool-call turns are + * attempted but their client re-render was never exercised. + * + * WARM-RUNG CONCURRENCY (measured on a K80 host + * 2026-08-02). The warm rungs (1 append, 1b + * think-elision) need the session's KV rows still in a GPU decode-slot + * ring row, and rebuild_gpu_map compacts ACTIVE slots into rows + * [0, n_active). So concurrently-active sessions keep distinct rows + * and all resume warm, while sessions that alternate STRICTLY + * SEQUENTIALLY each compact alone onto row 0 and clobber each other + * every turn — dropping both to the SYS1 rung, whose prefill grows + * with conversation depth. The SYS1 rung itself is per-slot host + * state and is unaffected. A model therefore holds N warm SYS1 + * sessions and as many warm-KV sessions as are concurrently in flight. + * @todo None + */ + +#ifndef PN_SERVE_BATCH_SCHED_H +#define PN_SERVE_BATCH_SCHED_H + +#include +#include +#include + +#include "../../include/pantheon.h" +#include "../core/engine.h" + +/* Maximum concurrent USER slots this scheduler will manage. 8 to match the + * K80 host die count and keep per-slot SSM images well within headroom. */ +#define BS_MAX_USERS 8 + +/* Physical slot-array / engine-binding size: the user slots PLUS one reserved + * scratch slot (the top index) that rebuild_gpu_map uses to break permutation + * cycles when all user slots are active (e.g. the K=8 full-reversal from a + * concurrent burst). Decode never runs over the scratch index — it steps + * [0, n_active) with n_active ≤ cap ≤ BS_MAX_USERS. Must stay ≤ + * PN_QWEN35MOE_GPU_MAX_SLOTS (16). */ +#define BS_MAX_SLOTS (BS_MAX_USERS + 1) + +/* Per-slot state machine values (bs_slot.status). */ +typedef enum { + BS_SLOT_IDLE = 0, /* free, pn_state available (dummy decode lane) */ + BS_SLOT_PREFILL = 1, /* request thread owns the slot; prefill runs + * owning the GPU turnstile turn */ + BS_SLOT_BOUND = 2, /* prefill done, GPU SSM bound, waiting for driver */ + BS_SLOT_DECODE = 3, /* driver is stepping this slot */ +} bs_slot_status; + +/* One decode slot. */ +typedef struct { + pn_state *state; /* owned by sched; reset by the owner in + * bs_prefill_begin (owning the GPU turn) */ + bs_slot_status status; /* guarded by sched->mu */ + uint32_t next_token; /* → driver: token for the next step (written + * by the owner in legacy mode, by the driver + * itself in L3 driver-sampling mode) */ + float *logits; /* driver → request thread (LEGACY mode only): + * logits for sampling (pointer into state's + * logits buffer; valid until the next step). + * NULL with logits_ready set = step failed. */ + bool logits_ready; /* driver has deposited logits for this step */ + bool token_ready; /* next_token is valid for the next step */ + pthread_cond_t cv; /* driver → request wakeup (logits_ready / + * ring push / finished / step_error) */ + int slot_idx; /* index in sched->slots[] */ + int gpu_idx; /* GPU slot index where THIS slot's live + * decode state currently resides (set at + * bind to the first-free GPU index, + * updated by every decode_slot_move; -1 + * when the slot holds no GPU state). The + * authoritative "current GPU position" + * for rebuild_gpu_map — replaces the old + * gpu==sched-index assumption that broke + * once continuous-batching compaction + * diverged the two (a fresh joiner reusing + * a low sched index would overwrite a + * compacted decoder's GPU slot). */ + + /* ── L3 driver-sampling state (all guarded by sched->mu; allocated by + * bs_slot_bind_ex, freed by bs_slot_release). ── */ + pn_sampling_params sp; /* per-request sampling params (copy) */ + uint32_t *recent; /* repeat-penalty window (driver-owned copy; + * NULL when repeat_last_n == 0) */ + int n_recent; /* live entries in recent */ + int max_recent; /* capacity of recent (sp.repeat_last_n) */ + uint32_t *ring; /* output ring, sized to the full bind-time + * budget → can never overflow */ + int ring_cap; /* == bind-time budget */ + int ring_r; /* owner read index (absolute) */ + int ring_w; /* driver write index (absolute) */ + int budget; /* driver samples remaining (max_tokens-1 + * at bind; slot finishes at 0) */ + bool finished; /* driver stopped stepping this slot (EOS / + * <|im_end|> / budget) — excluded from the + * active set; owner drains + releases */ + bool step_error; /* a decode step failed; owner sees -1 from + * bs_slot_pop_token after draining */ + + /* ── L3 AUTO reasoning-budget force-close (guarded by sched->mu; armed at + * bs_slot_bind_ex, driven per-step by the driver's sampler; mirrors the + * single-user sg_run_completion cap PER SLOT). A think-style model is + * prefilled with "" OPEN, so `reason_in_think` starts true; every + * token the driver samples while still inside counts toward + * `reason_budget`. Once the count reaches the budget without a self-emitted + * "" (and no tool-call skeleton is live), the driver FORCE-CLOSES: + * it queues this slot's "" token id(s) in `reason_close[0..n)` and + * steps them (KV/SSM advances over the close) before resuming normal + * sampling, so the model concludes and answers. `reason_budget <= 0` (a + * model with budget 0, or a non-think model) disables all of this — the + * slot decodes byte-identically to the pre-fix batched path. ── */ + int reason_budget; /* per-model AUTO reasoning cap (0 = off) */ + int reason_toks; /* tokens sampled while inside */ + bool reason_in_think; /* slot is currently inside */ + bool reason_forced; /* force-close already fired this turn */ + bool reason_forced_call; /* a COMPLETE tool call closed after that + * force-close. Recorded by the driver (it + * already queries the grammar's `complete` + * for the completion-stop) purely so + * bs_slot_release can report the OUTCOME of + * an abnormal turn without dereferencing the + * request-owned grammar hook, which may be + * freed by then. */ + uint32_t reason_close[8]; /* precomputed "" token id(s) */ + int reason_close_n; /* count of close ids (0 = none) */ + int reason_close_i; /* next queued close id to inject + * (== reason_close_n → nothing pending) */ + + /* ── PC-batched: session affinity + warm-state history (guarded by + * sched->mu; set by bs_slot_finalize, read by bs_slot_acquire_biased / + * bs_prefill_resume). When `sid[0]` is set this slot's pn_state holds + * session `sid`'s committed history in `hist[0..hist_n)` at pos==hist_n, + * so the same session routed back here delta-prefills only the appended + * tokens. Cleared on any reset (a divergence / different session claiming + * the slot). No session id → sid stays empty and the slot behaves + * byte-identically to the pre-PC batched path. ── */ + char sid[64]; /* session bound to this warm state ("" = none) */ + uint32_t *hist; /* committed token history in this pn_state (owned) */ + int hist_n; /* committed length (== state->pos) */ + int hist_cap; /* allocation of hist */ + + /* ── THINK-ELISION (the append-resume fix). A reasoning model's assistant + * turn opens with a forced "" prefill and then generates a think + * block the client is NEVER shown as text: the serve streams it on the + * separate `reasoning_content` channel, and streams NOTHING at all when the + * block is empty — the common case, since at temperature 0 nex-n2-mini + * deterministically answers the prefill with "\n\n\n\n" and goes + * straight to the answer. The client's re-render of that turn therefore + * cannot reproduce those tokens, exact-prefix append-resume fails at the + * "" opener, and the WHOLE conversation is re-prefilled every turn + * (Phase 8). + * + * `vis[0..vis_n)` is the CLIENT-VISIBLE PROJECTION of `hist`: the same + * conversation with every think block this session has ever generated + * removed — i.e. what the client is expected to resend as the history so + * far. When it is a strict prefix of the new context, every KV row in + * [0, hist_n) is a correct continuation of the conversation the client is + * describing, so the state resumes AS IS at pos == hist_n and only the new + * tail is prefilled. The think blocks stay in the model's context, which + * is where they were generated and where they cannot be removed from (30 of + * 40 layers are a sequential SSM recurrence — there is no rewind). + * + * A projection, not a span list, because the blocks ACCUMULATE: after two + * elided resumes the client omits two blocks, and matching around only the + * most recent one misses on every other turn (measured). vis_n == 0 + * disables the rung (non-think model, or a turn with no reproducible tail). + * ── */ + uint32_t *vis; /* client-visible projection of hist (owned) */ + int vis_n; /* its length (0 = rung disabled) */ + int vis_cap; /* allocation of vis */ + + /* ── REASONING-BEARING projection (the collapse fix). Eliding reasoning + * from the history is what a client does by DEFAULT, and it is also what + * makes the model stop reasoning: shown a history of assistant turns with + * no visible deliberation, it copies that shape and closes its own think + * block at token 0 — 18/18 collapse with reasoning stripped, 0/18 with it + * present, Fisher p = 2.2e-10. + * So a client SHOULD resend `reasoning_content`, and then `vis` — which has + * every think block removed — matches nothing and the whole conversation + * re-prefills every turn. + * + * `vis_think[0..vis_think_n)` is the same projection with this turn's think + * block PUT BACK, rendered from the reasoning TEXT the serve streamed + * through the builder's own sg_encode_history_think_block. The client + * cannot reproduce the ids the model sampled inside the block, and does not + * have to: it echoes the string, the serve re-derives the ids. + * + * Both projections are kept because the two are the same length only by + * accident and a client may switch (the client's send_reasoning is a flag). The + * head of each is taken from THIS turn's prompt, so a switch costs at most + * one missed turn. vis_think_n == 0 disables the rung — a non-think model, + * an empty think block, or a No-Think turn, all of which `vis` covers. ── */ + uint32_t *vis_think; /* reasoning-bearing projection (owned) */ + int vis_think_n; /* its length (0 = rung disabled) */ + int vis_think_cap; /* allocation of vis_think */ + + /* The projections for the turn currently in flight, built by + * bs_slot_note_visible and published into vis / vis_think by + * bs_slot_finalize. Owner-only, like `ct`. */ + uint32_t *pend_vis; + int pend_vis_n; + int pend_vis_cap; + uint32_t *pend_vis_think; + int pend_vis_think_n; + int pend_vis_think_cap; + + /* ── Committed-log seeding for a turn that resumed via think-elision. The + * KV then holds hist[0, hist_n) followed by prompt[ct_prompt_from ..), which + * is NOT the prompt the caller passed — so bs_slot_bind cannot simply copy + * the prompt into `ct`. ct_keep is how many leading tokens of this slot's + * own `hist` the log starts with; ct_prompt_from is where the prompt joins + * it. Both 0 = the ordinary case (log == prompt). Set by + * bs_prefill_resume_sys, consumed by the next bs_slot_bind. ── */ + int ct_keep; + int ct_prompt_from; + /* ── SYS1-batched: per-slot SYSTEM-PREFIX boundary snapshot (owner-only; + * see bs_prefill_resume_sys / bs_prefill_capture_sys). `sys_snap` is a + * self-contained host deep-copy of this slot's pn_state at pos == sys_len + * (pn_qwen35moe_state_snapshot: SSM conv+recurrent, KV rows [0, sys_len), + * fp16 sink/window shadows), captured for session `sys_sid` over the exact + * prefix `sys_tokens[0..sys_len)` (the reuse guard). Because it is a deep + * copy off THIS slot's own host mirrors it references no live state: it + * survives slot resets, other sessions using the slot, and warm-history + * (`sid`/`hist`) invalidation — restore only ever fires on an exact + * session-id + prefix byte-match and fully overwrites [0, sys_len). + * Replaced when a different session (or a changed system prompt) captures + * on this slot; freed by bs_destroy. Written only by the slot's owner + * while it holds the slot; `sys_sid` is read by the affinity scans under + * sched->mu (IDLE slots only, so no owner write can race a read). ── */ + char sys_sid[64]; /* session the snapshot belongs to ("" = none) */ + void *sys_snap; /* boundary snapshot (NULL = none) */ + int *sys_snap_refs; /* Phase 10b: shared-snapshot refcount. + NULL = sys_snap privately owned (the + common case); non-NULL = sys_snap is + SHARED between slots whose system + prefixes are byte-identical (each + ~68 MiB host image depends only on + weights + prefix tokens), and *refs + counts the sharers. */ + uint32_t *sys_tokens; /* exact prefix tokens covered (owned) */ + int sys_len; /* snapshot boundary (tokens covered) */ + + /* ── PC-batched committed-token log (guarded by sched->mu). When caching + * is armed for this turn (armed_sid[0] set at bs_slot_bind_ex), `ct` holds + * the EXACT tokens whose K/V+SSM are committed in `state` — i.e. the ids + * at positions [0, state->pos). Seeded with the prompt at bind + * (ct_n == n_prompt == state->pos); the driver appends each token it steps + * into the state, so ct_n == state->pos is an invariant the driver + * maintains at the source of truth. bs_slot_finalize copies ct → hist as + * the committed history for next-turn delta reuse (no owner-side token + * reconstruction). Empty armed_sid → the log is not maintained (no + * caching this turn). ── */ + char armed_sid[64]; /* session this turn caches for ("" = off) */ + uint32_t *ct; /* committed-token log (owned) */ + int ct_n; /* committed length (== state->pos when armed) */ + int ct_cap; /* allocation of ct */ + + /* ── SESSION-SUBSTITUTION detection (owner-only; set by + * bs_prefill_resume_sys, read by bs_slot_session_conflict). True when THIS + * turn arrived under a session id that this slot already held a record for, + * but carrying a DIFFERENT system prompt — i.e. the client is running a + * second, unrelated conversation under one session id. See + * bs_slot_session_conflict for why the system block is the discriminator + * and what it deliberately does NOT flag. ── */ + bool sid_conflict; +} bs_slot; + +/* Maximum number of requests that may wait in the per-model overflow queue + * (D1). When the queue is also full, callers receive 429 + Retry-After. + * Env override BS_QUEUE_DEPTH (integer > 0). */ +#define BS_QUEUE_DEPTH_DEFAULT 32 + +/* D3: memory-pressure levels at/above which new prefills are held in the + * queue rather than immediately starting a prefill that could OOM. + * Corresponds to pn_mem_level values (see pn_mem_state.h): + * PN_MEM_LEVEL_OK=0 WATCH=1 TIGHT=2 CRITICAL=3 + * + * Threshold is CRITICAL (3), NOT TIGHT (2), by design: + * - TIGHT is the *normal resting state* when a 16GB model is resident on the + * 8GB host ("reserve nearly reached" is baseline, not an emergency). It is + * already handled where it belongs: the memory ladder degrades at TIGHT + * (Q4/shrink/evict, see pn_ladder.h) and pn_admit() fits every request to + * live headroom per-prefill. Holding all intake at TIGHT would stall the + * server at its baseline (every request queues → times out → 429). + * - CRITICAL means genuinely at the reserve line — only then does D3 hold new + * prefills as a last-resort backpressure, and 503 if the queue is also full. + * The D1 queue still absorbs pure slot-contention bursts independently of this. */ +#define BS_PRESSURE_HOLD_LEVEL 3 /* PN_MEM_LEVEL_CRITICAL */ + +/* Batch scheduler — one per resident model. */ +typedef struct { + pn_model *model; /* borrowed (registry keeps it alive) */ + char model_name[64]; /* registry name, for LOGGING only — never a + * routing input. The reasoning force-close + * is an abnormal event and its log line has + * to name the model it happened to. */ + + /* ── GPU-ownership turnstile (guarded by mu) — the single-GPU-owner + * "lock": one prefill chunk (+ the final chunk's bind), or one driver + * decode step, at a time. FIFO by ticket: acquire = take + * gpu_ticket_next++ and wait on cv_gpu until gpu_ticket_serving reaches + * it; release = gpu_ticket_serving++ + broadcast. FIFO (not a plain + * mutex) so the free-running L3 driver cannot barge ahead of a waiting + * or chunk-yielding prefiller, and prefillers cannot starve the driver: + * the interleave is provably chunk → step → chunk. A turn owner never + * cond-waits; every ticket is followed by a release (chunks and steps + * are bounded) — deadlock-free by construction. ── */ + uint64_t gpu_ticket_next; /* next ticket to hand out */ + uint64_t gpu_ticket_serving; /* ticket currently owning the GPU */ + pthread_cond_t cv_gpu; /* turnstile wakeup (serving advanced) */ + + pthread_mutex_t mu; /* guards all slot status + token exchanges */ + pthread_cond_t cv_work; /* request → driver: gate may have opened + * (bind / token posted / slot released) */ + pthread_cond_t cv_idle; /* → callers: a slot went IDLE (space free) + * OR pressure level dropped (D3) */ + + bs_slot slots[BS_MAX_SLOTS]; + int cap; /* active capacity (≤ BS_MAX_SLOTS, clamped + * to pn_qwen35moe_gpu_ladder_max_slots at + * create time) */ + + pthread_t driver; /* background driver thread */ + bool driver_running; + bool shutdown; /* set to true to stop driver */ + + /* Lazy GPU slot init: true once pn_qwen35moe_decode_slot_bind has been + * called for EVERY lane so that decode_slots can validate + * g->slot_state[i] for all i. False at bs_create time (no GPU work at + * create — avoids the startup hang where an eager pre-bind races an + * unsettled CUDA context right after a multi-minute weight upload); set + * true inside the first successful bs_slot_bind. Written under mu + * with the GPU turn owned, read by the driver under mu. */ + bool slots_gpu_init; + + /* Dynamic-width / continuous-batching state (Phase D2). + * + * The driver compacts the active (BOUND/DECODE) slots into a contiguous + * GPU index range [0, n_active_gpu) each step so decode_slots runs over + * only the live lanes — idle lanes pay zero compute. + * + * gpu_map[gpu_idx] = scheduler slot index (sched_slot_idx). The inverse + * (sched → GPU) is recovered by scanning gpu_map[0..n_active_gpu). + * active_set_changed: set to true by bs_slot_bind (a new user joined), + * bs_slot_release (a user left) and the driver (a slot finished — L3) to + * tell the driver the GPU mapping needs to be rebuilt before the next + * step. Written under mu; the driver reads it under mu with the GPU + * turn owned and clears it after rebuilding. + * + * The GPU always sees exactly `cap` bound slots (the lazy-init binding + * registers all cap lanes once); decode_slots is then called with + * n_active_gpu ≤ cap, using indices [0, n_active_gpu). When a user joins + * or leaves we call pn_qwen35moe_decode_slot_bind for each affected lane + * (still with n_slots = cap) so the GPU slot state matches the compacted + * mapping — these per-join/leave rebinds are the only overhead. + * + * Invariant: gpu_map[0..n_active_gpu) holds the current compacted mapping. + * Written only by the driver under mu with the GPU turn owned; read by + * the driver likewise and for diagnostics only. */ + int n_active_gpu; /* current compacted count */ + int gpu_map[BS_MAX_SLOTS]; /* GPU idx → sched slot idx */ + bool active_set_changed; /* mapping needs rebuild */ + + /* ── D1: per-model overflow queue ────────────────────────────────────── + * When all K slots are busy, incoming requests increment n_queued and + * block on cv_idle (already signaled by bs_slot_release). Only 429 + * when n_queued == queue_cap (the queue itself is full). FIFO fairness + * is guaranteed by the cond broadcast: every waiter re-races for the + * one free slot; the first to succeed decrements n_queued and continues. + * This is the standard bounded-queue / cond-broadcast pattern — safe + * and no starvation in the common burst case because prefills are + * serialized by the GPU turnstile anyway so throughput is identical to + * N separate FIFO queues draining one at a time. + * + * Guarded by mu. */ + int n_queued; /* current waiters (D1 queue depth) */ + int queue_cap; /* max waiters before 429 (D1 limit) */ + + /* ── D3: admission pressure level (snapshot) ────────────────────────── + * A waiter in the D1 queue also holds off starting its prefill while + * the host pressure level is >= BS_PRESSURE_HOLD_LEVEL. The pressure + * poller thread (or any path that calls pn_memwatch_poll_now) signals + * cv_idle to wake waiters so they can re-check and proceed once the + * pressure clears. Waiters use a timed wait (abs-time deadline) so + * they never wait longer than bs_queue_wait_ms milliseconds total. + * Only 503 when BOTH the queue is full AND pressure is CRITICAL — + * the last-resort hard refusal that keeps the box from OOMing. */ + int pressure_level; /* cached pn_mem_level, written under mu */ + + /* ── L3: in-driver sampling config (immutable after bs_create) ──────── */ + bool driver_sample; /* QWEN35_BS_DRIVER_SAMPLE != "0" (default + * true): driver samples + fills per-slot + * output rings; false = legacy lockstep + * (owner-side sampling via the logits + * handshake) */ + int n_vocab; /* model->config.n_vocab (sampling width) */ + int eos_id; /* tokenizer EOS id (-1 if unknown) */ + int im_end_id; /* <|im_end|> id (-1 if unknown) */ +} pn_batch_sched; + +/* ============================================================ + * Scheduler lifecycle (called from model_registry.c) + * ============================================================ */ + +/** + * Allocate and start a batch scheduler for `model`. Allocates `cap` + * pn_state instances (cap ≤ BS_MAX_SLOTS, clamped to the memory-pressure + * ladder ceiling so lazy GPU init cannot be refused later). Returns NULL + * on failure. + * + * `model_name` is the registry name, copied for LOGGING only (may be NULL). + */ +pn_batch_sched *bs_create(pn_model *model, int cap, const char *model_name); + +/** + * Stop the driver thread, free all slot states, free the scheduler. + * Must be called after all slot threads have finished (no in-flight requests). + */ +void bs_destroy(pn_batch_sched *sched); + +/* ============================================================ + * Per-request slot API (called from openai_chat / serve_generation) + * ============================================================ */ + +/** + * Reserve a free slot. Returns the slot index (0..cap-1) if a slot is free, + * or -1 if all slots are occupied (caller should return 429). + * Sets slot->status = BS_SLOT_PREFILL; does NOT touch the slot's pn_state + * (that happens in bs_prefill_begin, under the GPU lock). Non-blocking. + */ +int bs_slot_acquire(pn_batch_sched *sched); + +/* Return codes for bs_slot_acquire_or_wait. */ +typedef enum { + BS_ACQUIRE_OK = 0, /* slot acquired; *slot_out is the slot index */ + BS_ACQUIRE_429 = -1, /* queue full — 429 + Retry-After to client */ + BS_ACQUIRE_503 = -2, /* memory CRITICAL + queue full — hard 503 */ + BS_ACQUIRE_TIMEOUT = -3, /* waited too long without a slot freeing */ + BS_ACQUIRE_SHUTDOWN = -4, /* scheduler is shutting down */ +} bs_acquire_result; + +/** + * D1 + D3: Reserve a slot, waiting in a bounded FIFO queue if all slots are + * busy or if host memory pressure is TIGHT/CRITICAL (D3 backpressure gate). + * + * Behaviour: + * - If a slot is free AND pressure < BS_PRESSURE_HOLD_LEVEL: acquire + * immediately (identical to bs_slot_acquire) and return BS_ACQUIRE_OK. + * - Otherwise: enqueue this caller (increment n_queued) and block on + * cv_idle (signaled by bs_slot_release and the pressure poller). + * - 429 (BS_ACQUIRE_429) immediately if n_queued would exceed queue_cap + * AND pressure < CRITICAL. + * - 503 (BS_ACQUIRE_503) immediately if n_queued > queue_cap AND + * pressure level is CRITICAL — last-resort refusal. + * - When a slot frees AND pressure < hold level: claim the slot and + * return BS_ACQUIRE_OK. + * - If wait_ms elapses without a slot: BS_ACQUIRE_TIMEOUT. + * - On scheduler shutdown: BS_ACQUIRE_SHUTDOWN. + * + * On BS_ACQUIRE_OK, *slot_out holds the acquired slot index. + * On any error, *slot_out is -1 and no slot was acquired. + * + * `retry_after_s` (if non-NULL): filled with a suggested Retry-After value + * in seconds on BS_ACQUIRE_429/503/TIMEOUT; 0 on success. + * + * wait_ms: max milliseconds to queue-wait (0 → non-blocking, equivalent to + * bs_slot_acquire with pressure check). Typical: 30 000 (30 s). + * + * `sid` (PC-batched, may be NULL): session-affinity hint — when a free slot is + * claimed, THIS session's warm slot is preferred (delta-prefill next turn), + * falling back to any free slot. A soft hint only; NULL → no affinity. + * + * Composes with D2: a dequeued request acquires a slot set to BS_SLOT_PREFILL, + * then proceeds through the normal bs_prefill_begin → bs_slot_bind → decode + * path, joining the batch at the next driver step exactly as a non-queued + * request would. + * + * Thread-safe: yes (guards via sched->mu; does NOT touch the GPU turn). + */ +bs_acquire_result bs_slot_acquire_or_wait(pn_batch_sched *sched, + int wait_ms, + int *slot_out, + int *retry_after_s, + const char *sid); + +/** + * PC-batched — session-affinity acquire. Like bs_slot_acquire, but when + * `sid` is non-NULL/non-empty it FIRST tries to reclaim the IDLE slot whose + * warm pn_state already holds this session's committed history (so its next + * prefill deltas rather than resets), then the IDLE slot holding this + * session's SYSTEM-PREFIX snapshot (SYS1-batched: a divergent turn still + * resumes at the boundary), falling back to any free slot. This is + * a SOFT hint layered on continuous batching — it never blocks and never + * displaces an active slot; if the session's warm slot is currently busy it + * simply takes another free slot (that turn full-prefills). `sid == NULL` + * (or empty) is identical to bs_slot_acquire. Returns the slot index or -1 + * if all slots are busy. Non-blocking. + */ +int bs_slot_acquire_biased(pn_batch_sched *sched, const char *sid); + +/** + * Move a just-claimed slot to the IDLE slot whose cache this prompt actually + * continues. The acquire runs before tokenization, so with one session id + * carrying several conversations (opencode: a title request plus the real + * turns, each with its own system prompt) it can only pick the FIRST slot of + * that id. Called once the prompt is known; preference: + * 1. a slot of `sid` whose warm history (exact or a think projection) is a + * strict prefix of `tokens`; + * 2. a slot holding `sid`'s boundary snapshot over tokens[0..sys_len); + * 3. nothing reusable anywhere: when slot `idx` holds ANOTHER conversation + * of `sid`, a cold IDLE slot instead, so the new conversation does not + * evict it. + * Ties keep `idx`. On a move the new slot is claimed (PREFILL) and `idx` + * returns to IDLE with its warm state intact. Call only between the acquire + * and bs_prefill_resume_sys (`idx` is PREFILL, no GPU turn held). Empty + * `sid` is a no-op. Returns the slot the caller now owns. + */ +int bs_slot_reroute(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int n_ctx, int sys_len); + +/** + * PC-batched — resume-aware prefill begin. Like bs_prefill_begin (takes the + * GPU turn), but if this slot's warm pn_state still belongs to session `sid` + * and its committed history is an exact prefix of `tokens[0..n_ctx)` with + * state->pos == that history length, it RESUMES: does NOT reset, and sets + * *delta_start to the history length so the caller prefills only the appended + * tokens. Otherwise it resets + forgets exactly like bs_prefill_begin and + * sets *delta_start = 0 (full prefill). `sid == NULL`/empty always resets + * (*delta_start = 0) — byte-identical to bs_prefill_begin. Same GPU-turn + * pairing contract as bs_prefill_begin (→ bind / abort). + */ +void bs_prefill_resume(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int n_ctx, int *delta_start); + +/** + * SYS1-batched — system-prefix-aware resume (the batched twin of + * pn_prefix_cache_acquire_sys). As bs_prefill_resume, plus the caller's + * system-prompt token boundary `sys_len` (tokens[0..sys_len) are the + * per-session-stable system prefix; 0 disables SYS1 for the call). Resume + * preference, mirroring the single-user ladder: + * + * 1. Warm-history append resume (exact bs_prefill_resume behavior): + * *delta_start = this slot's committed history length. + * 2. System-prefix resume: this slot holds a boundary snapshot for THIS + * session AND tokens[0..sys_len) byte-match it → the snapshot is restored + * into the slot's pn_state (host-side; GPU shadows forgotten so the delta + * prefill re-seeds from the restored mirror) and *delta_start = sys_len. + * Fires on the divergent-history turns (reasoning models: text + * committed, never resent) where (1) never can. + * 3. Fresh/diverged with no usable snapshot: full reset, *delta_start = 0. + * + * *capture_sys is set to 1 when the caller should CAPTURE the boundary this + * turn: a full prefill is about to run (case 3), a system prefix exists + * (0 < sys_len < n_ctx), a session id was supplied, and no valid snapshot on + * this slot covers these exact system tokens. The caller then prefills + * [0, sys_len), calls bs_prefill_capture_sys (still owning the GPU turn), and + * prefills the rest. + * + * Correctness: any mismatch (session, prefix bytes, dims) falls to case 3 — + * never a partial/wrong reuse. sys_len == 0 or an empty sid is byte-identical + * to bs_prefill_resume. Same GPU-turn pairing contract as bs_prefill_begin + * (→ bind / abort). + */ +void bs_prefill_resume_sys(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int n_ctx, int sys_len, + int *delta_start, int *capture_sys); + +/** + * Did the turn just resolved by bs_prefill_resume_sys arrive under a session id + * that already identified a DIFFERENT conversation on this slot? + * + * A session id is meant to name ONE conversation. When a client reuses one id + * for two conversations (the measured case: an agent's reflect pass POSTing its + * own system prompt under the main chat's id) the serve cannot resume either of + * them: each turn evicts the other's warm prefix and re-prefills in full, so the + * session silently goes quadratic. Nothing about that is visible to the client + * today, which is why it hid for weeks. + * + * THE DISCRIMINATOR IS THE SYSTEM BLOCK, and it is chosen because it is the one + * thing every LEGITIMATE history change keeps byte-identical: + * - append — the system block is the unchanged head of the prompt; + * - think-elision — the client's re-render differs only after the header; + * - compaction — the client rewrites the log body only, and asserts the wire + * prefix ahead of it is byte-identical (its own strcmp + * test, tests/test_context_summary.c). + * So "same session id, different system block" is substitution, and a plain + * divergence of the conversation body is NOT flagged. + * + * Deliberate limits (false negatives only — this never flags a legitimate turn): + * - a request with NO system message (sys_len == 0) carries no signal; + * - a session whose slot was taken by someone else has no record left to + * compare against, so its next substitution is missed; + * - a client that legitimately varies its system prompt per turn (injected + * timestamps, a changing tool list) is reported every turn. That is why + * this is a REPORT and not a refusal. + * + * Valid from bs_prefill_resume_sys until the slot's next resume/reset. + * Thread-safe: no (owner-only, like the rest of the resume state). + */ +bool bs_slot_session_conflict(const pn_batch_sched *sched, int idx); + +/** + * SYS1-batched — snapshot slot `idx`'s state at its system-prompt boundary + * for session `sid`. Call mid-prefill, OWNING the GPU turn (between + * bs_prefill_resume_sys and bs_slot_bind, never inside a yield window), when + * the slot's state sits EXACTLY at pos == sys_len having just prefilled + * tokens[0..sys_len) (the *capture_sys == 1 protocol). On the GPU path the + * live GPU state is synced to the slot's host mirror first (the same D2H the + * L2 yield protocol performs), so the snapshot is self-contained and the + * prefill continues unaffected. Replaces any previous snapshot on this slot + * (possibly another session's — per-slot capacity is the documented + * conservative bound; session affinity routes a session back to its slot, so + * replacement is rare in practice). + * + * @return 0 on success, -1 on failure (pos mismatch / unsupported arch / + * allocation / D2H). Failure is benign: the caller just continues + * its full prefill uncached. + * + * Thread-safe: no (owner-only, GPU turn held) + */ +int bs_prefill_capture_sys(pn_batch_sched *sched, int idx, const char *sid, + const uint32_t *tokens, int sys_len); + +/** + * Record what the client is expected to resend for this turn — BOTH ways. + * + * Call once per turn from the decode loop, after generation ends and BEFORE + * bs_slot_finalize. Builds the slot's two projections: + * + * vis = prompt[0, n_prompt - think_prefill_len) ++ rerender + * vis_think = prompt[0, n_prompt - think_prefill_len) ++ think_block + * ++ rerender + * + * The shared head is the history the client rendered ITSELF this turn (the + * prompt minus the forced generation prefill), so whichever convention the + * client follows for earlier turns is already baked in and the projections stay + * correct as think blocks accumulate in the KV. The two differ only in this + * turn's reply: `vis` is the client that DROPS reasoning, `vis_think` the + * client that sends it back. Recording both means neither client pays a + * re-prefill, and a client that switches mid-session pays at most one turn. + * + * @param prompt the request's full prompt token array + * @param n_prompt its length + * @param think_prefill_len prompt tokens the chat template forced after the + * assistant header ("", "\n", or the + * closed No-Think block) — see + * sg_build_prompt_tokens(). 0 for a non-think model, + * whose whole prompt is then the head. + * @param think_block this turn's think block as the builder will render + * it from the `reasoning_content` the client echoes + * (sg_encode_history_think_block). NULL / 0 leaves + * vis_think empty, disabling the reasoning-bearing + * rung for this turn — which is right for a No-Think + * turn, an empty block, or a non-think model. + * @param think_block_n how many tokens. + * @param rerender this turn's reply AS THE CLIENT WILL RESEND IT, put + * back through the serve's own render + tokenize path + * (sg_project_client_rerender). NULL / 0 disables + * BOTH rungs for this turn. + * @param rerender_n how many tokens. + * + * The projection is what the client will SEND, not what the model produced. + * Those differ more than they look: a model's sampled ids are not a function of + * the text it produced — "" has both a special id and a four-token + * byte encoding and the char-level tool grammar accepts either — so a projection + * built from the GENERATED ids misses on whichever turns sampling picked the + * other one, which is what made every turn re-prefill its whole conversation. + * Building it from the re-render + * removes the choice. A turn whose re-render still does not match is safe to + * record anyway — the resume path memcmps the whole projection and simply + * misses. + * + * Thread-safe: no (owner-only, between decode and finalize) + */ +void bs_slot_note_visible(pn_batch_sched *sched, int idx, + const uint32_t *prompt, int n_prompt, + int think_prefill_len, + const uint32_t *think_block, int think_block_n, + const uint32_t *rerender, int rerender_n); + +/** + * Publish this turn's pending projections as the slot's live ones, against the + * committed history `hist_n` they map onto. Called by bs_slot_finalize once + * the history is published; exposed so the CPU test drives the REAL publish + * rule rather than a stand-in that is free to drift from it. + * + * The two candidates are NOT bounded the same way, and that asymmetry is the + * rule: the think-elided projection must not exceed `hist_n` (it strictly drops + * tokens, so a longer one is nonsense), while the reasoning-bearing one may — + * The client's rendering carries the template's own think separators where the + * KV carries what the model sampled, which lands the two lengths within a token + * or two of each other. Bounding it would disable the rung on a coin flip; it + * is not a correctness gate either way (see bs_projection_resume). + * + * @param sl the slot (caller holds sched->mu) + * @param hist_n the committed history length the projections map onto + * + * Thread-safe: no (call under sched->mu) + */ +void bs_slot_publish_projections(bs_slot *sl, int hist_n); + +/** + * PC-batched — commit a session's warm state to its slot. Called by the + * owner AFTER decode completes and BEFORE bs_slot_release, while the slot's + * GPU decode state (gpu_idx) is still valid. Takes the GPU turn, snapshots + * the slot's live per-slot SSM images back into its pn_state host mirror + * (pn_qwen35moe_decode_slot_ssm_to_host) and refreshes the KV host mirror, so + * the pn_state is warm + self-contained at pos == committed-length, then + * publishes the driver-maintained committed-token log (ct → hist) + armed_sid + * as this slot's warm history for next-turn delta reuse. Releases the GPU + * turn before returning. Does NOT release the slot — the owner still calls + * bs_slot_release. No-op if the slot was never armed for caching (no session + * id at bind). Returns 0 on success, -1 on failure (the slot's warm state is + * invalidated so a future affinity hit re-full-prefills rather than reusing + * stale state). + */ +int bs_slot_finalize(pn_batch_sched *sched, int idx); + +/* ============================================================ + * Session warm-tier EXPORT / RESTORE / RELEASE — batched arm + * + * The batched twin of prefix_cache.h's export/import. A batched session's + * authoritative state is NOT in the model's pn_prefix_cache (that cache holds + * only admission bookkeeping on this path — nothing ever calls its acquire, so + * it has no entries and no pn_states): it is the warm `bs_slot` its last turn + * finalized on — `sl->sid` + `sl->hist[0..hist_n)` over `sl->state` at + * pos == hist_n, with the KV rows themselves living in the GPU decode-slot + * ring (Phase 10b / INV3: the host mirror is NOT maintained). + * + * Export therefore materializes [0, pos) from the ring through + * pn_qwen35moe_gpu_state_to_host (which calls the INV3 materializer and + * refuses loudly rather than reading a stale mirror), snapshots the now- + * self-contained host image, and leaves the session untouched. Restore is + * the exact inverse of the SYS1-batched boundary restore that the warm path + * already validated on a K80 host: host write + GPU forget + content-generation bump, + * so the next delta prefill re-seeds from the restored mirror. + * + * Both reserve the target slot (IDLE → PREFILL) under `mu` so no request can + * claim it mid-operation, take the GPU turn for the D2H/H2D bookkeeping, and + * return the slot to IDLE. A slot that is mid-turn is never exported from — + * the single-user path refuses a `pos != n_tok` state for the same reason. + * ============================================================ */ + +/* Result codes shared by bs_session_export / bs_session_restore. */ +typedef enum { + BS_SESSION_OK = 0, /* done */ + BS_SESSION_NOT_FOUND = -1, /* no warm slot holds this session (export) */ + BS_SESSION_BUSY = -2, /* the session's slot is mid-turn, or (restore) + * every slot is busy → 429 */ + BS_SESSION_ERROR = -3, /* materialize / snapshot / restore failed */ +} bs_session_result; + +/** + * Export session `sid`'s warm slot state as an opaque qwen35moe snapshot the + * caller serializes to a portable blob (same snapshot type as + * pn_prefix_cache_export_snapshot, so ONE serializer serves both paths). + * + * Materializes the slot's KV rows [0, pos) from the GPU decode-slot ring via + * the INV3 materializer — no host mirror is maintained or resurrected; the + * host bytes are transient (they live in the slot's pn_state mirror until its + * next reset) and bounded by the session's real context, exactly the + * occasional-consumer contract INV3 defines. Read-only w.r.t. the session: + * the slot's warm history, sid and position are unchanged. + * + * @param n_tok_out Out: committed token count the snapshot covers. + * @param tokens_out Out: malloc'd COPY of the committed history (caller frees). + * @param snap_out Out: snapshot (free with pn_qwen35moe_state_snapshot_free). + * @return BS_SESSION_OK / _NOT_FOUND / _BUSY / _ERROR. + * + * Thread-safe: yes (takes mu + the GPU turn internally). + */ +bs_session_result bs_session_export(pn_batch_sched *sched, const char *sid, + void **snap_out, uint32_t **tokens_out, + int *n_tok_out); + +/** + * Restore a deserialized snapshot into a slot as session `sid`'s warm state, + * so the NEXT chat turn on `sid` routes to that slot (session affinity) and + * delta-prefills only what the client appends beyond `tokens[0..n_tok)`. + * + * Prefers the slot already holding `sid` (a re-restore overwrites it), else any + * IDLE slot; BS_SESSION_BUSY when every slot is occupied. The caller must have + * validated the blob's weights_version first. `snap`'s pos MUST equal n_tok. + * + * Thread-safe: yes (takes mu + the GPU turn internally). + */ +bs_session_result bs_session_restore(pn_batch_sched *sched, const char *sid, + const void *snap, const uint32_t *tokens, + int n_tok); + +/** + * Drop session `sid`'s warm state from whatever IDLE slot holds it: clears the + * session binding + committed history and releases the slot's system-prefix + * boundary snapshot, so the slot full-prefills for whoever takes it next and + * no departed session's context lingers. The slot's pn_state buffers are left + * alone — bs_prefill_begin resets + forgets them under the GPU turn on the next + * acquire, which is the only safe place to do it. A slot mid-turn is skipped + * (its owner still needs it). Idempotent; no-op for an unknown session. + * + * Thread-safe: yes (takes mu). + */ +void bs_session_release(pn_batch_sched *sched, const char *sid); + +/** + * Prefix-match complement of bs_session_release: drop the warm state of every + * IDLE slot whose session id begins with `base_prefix`. A dead liveness watch + * covers a tab's BASE id while the live slots are the mode-suffixed + * `-orch` / `-act` / `-chat` sessions, so it frees them all at once. + * + * Thread-safe: yes. + */ +void bs_session_release_by_prefix(pn_batch_sched *sched, + const char *base_prefix); + +/** + * Take exclusive ownership of the model's GPU (a FIFO turnstile ticket) for + * this slot's prefill and reset the slot's pn_state. Blocks until any + * in-flight decode step or other prefill chunk finishes (finite; FIFO). + * MUST be paired with exactly one of bs_slot_bind / bs_slot_bind_ex (normal + * path) or bs_prefill_abort (any prefill failure) — both release the GPU + * turn. bs_prefill_yield may release + re-acquire it in between (L2). + */ +void bs_prefill_begin(pn_batch_sched *sched, int idx); + +/** + * L2 — chunk-boundary yield: give the driver a decode step in the middle of + * a long prefill. If there is a step to run (gate open), makes the slot's + * host mirror authoritative (pn_qwen35moe_gpu_state_to_host — the + * prefix-cache eviction protocol, so any interleaved forward can safely + * claim the live GPU buffers; the next chunk re-seeds via the ordinary + * seed-on-host-change path), releases the GPU turn, and re-acquires it FIFO + * behind the driver (and any earlier waiter). No-op when there is nothing + * to interleave, or if the mirror refresh fails (keeping the turn is always + * safe). Call ONLY between bs_prefill_begin and bs_slot_bind/abort, never + * after the final chunk (the bind must directly follow the last prefill + * token under the same turn). + */ +void bs_prefill_yield(pn_batch_sched *sched, int idx); + +/** + * Abort a prefill started with bs_prefill_begin: releases the GPU turn and + * frees the slot (→ IDLE). Do NOT also call bs_slot_release. + */ +void bs_prefill_abort(pn_batch_sched *sched, int idx); + +/** + * bs_prefill_abort for a prefill that was cut off PART-WAY THROUGH (client + * disconnect, or a chunk that failed): the slot's pn_state now holds KV/SSM + * for a prompt prefix that no committed history describes. Resets the state + * and drops the slot's warm session binding under the still-held GPU turn, + * then releases turn + slot exactly like bs_prefill_abort. + * + * The resume ladder already refuses a half-written state (rungs 1/1b require + * state->pos == hist_n), so this is not what makes the next turn correct — it + * makes it CHEAP and quiet: without it the slot keeps advertising the session + * id it can no longer serve, so try_claim_idle steers that session's next turn + * straight back onto the poisoned slot to discover the miss, logging an + * append-resume miss and evicting nothing useful. Mirror of the single-user + * path's pn_prefix_cache_reset_session on abort. + */ +void bs_prefill_abort_reset(pn_batch_sched *sched, int idx); + +/** + * Finish the prefill critical section: refresh the slot state's host mirrors + * (the batched decode's per-user attention runs against the HOST KV cache, + * which is stale after a GPU prefill), lazily register all lanes with the + * GPU on the first call, snapshot this slot's live SSM state into lane + * `idx`, mark the slot BOUND and wake the driver. `first_token` is the + * first decode token (sampled from prefill logits by the caller). + * + * Must be called owning the GPU turn (from bs_prefill_begin); ALWAYS + * releases it before returning. Returns 0 on success; -1 on failure, in + * which case the slot has already been released (do not call + * bs_slot_release or bs_prefill_abort). + */ +int bs_slot_bind(pn_batch_sched *sched, int idx, uint32_t first_token); + +/** + * L3 variant of bs_slot_bind for driver-sampling mode (bs_driver_sampling() + * true). Same GPU-turn contract and failure semantics as bs_slot_bind, + * plus it arms the driver's per-slot sampler: + * - `sp` the request's sampling params (copied); + * - `recent`/`n_recent` the repeat-penalty window as of the FIRST decode + * token — the prompt tail; the driver appends each stepped token + * (starting with `first_token`) before sampling, exactly mirroring the + * legacy owner loop, so streams are bit-identical at temp 0; + * - `max_new` the driver's sampling budget (max_tokens - 1; the owner + * already holds token #1 = `first_token`). max_new <= 0 binds the slot + * already `finished` (nothing to step — max_tokens == 1). + * The owner then consumes tokens via bs_slot_pop_token. + * + * PC-batched: when `sid` is non-NULL/non-empty the slot is ARMED for caching — + * its committed-token log is seeded with `prompt[0..n_prompt)` (== state->pos + * after prefill) and the driver appends every token it steps, so at + * bs_slot_finalize the log is the exact committed history at state->pos. + * `sid == NULL`/empty leaves caching off for this turn (byte-identical path). + */ +int bs_slot_bind_ex(pn_batch_sched *sched, int idx, uint32_t first_token, + const pn_sampling_params *sp, + const uint32_t *recent, int n_recent, int max_new, + const char *sid, const uint32_t *prompt, int n_prompt); + +/** + * Arm the AUTO reasoning-budget force-close for a slot the driver is sampling + * (L3). Mirrors sg_run_completion's single-user cap PER SLOT: the driver + * counts tokens sampled while the slot is inside and, once the count + * reaches `reason_budget` without a self-emitted "", force-decodes the + * "" close (`close_ids[0..n_close)`) so the model concludes and answers. + * + * Call AFTER bs_slot_bind_ex and BEFORE the owner drains any token — the slot + * is BOUND (the driver rebuilds its GPU map before the first step), so this + * write races nothing. `first_token` is the owner-held token #1; it is counted + * here iff the slot starts inside (`in_think`), so the driver's per-step + * count stays aligned with the single-user loop (which counts every token + * emitted while em.in_think, first token included). + * + * @param reason_budget AUTO reasoning cap in tokens (<= 0 → no-op / uncapped) + * @param in_think true if the assistant turn was prefilled with + * still OPEN (a think-style model) + * @param close_ids the model's "" token id(s) (from the caller's + * tokenizer encode); copied into the slot + * @param n_close count of close ids (<= 8; 0 → force-close disabled) + * @param first_token the owner-held first decode token (counted iff in_think) + * + * Thread-safe: yes (takes sched->mu). No-op when driver-sampling is off. + */ +void bs_slot_arm_reasoning_budget(pn_batch_sched *sched, int idx, + int reason_budget, bool in_think, + const uint32_t *close_ids, int n_close, + uint32_t first_token); + +/** + * L3 — pop the next driver-sampled token for this slot (blocks while the + * ring is empty and the driver is still stepping). Returns: + * 1 = *tok holds the next token (EOS/<|im_end|> ARE delivered — the + * owner performs the finish check, mirroring the legacy loop); + * 0 = clean end of stream (budget exhausted; ring fully drained); + * -1 = a decode step failed or the scheduler is shutting down (after the + * ring is drained) — the caller must still bs_slot_release. + */ +int bs_slot_pop_token(pn_batch_sched *sched, int idx, uint32_t *tok); + +/* True when this scheduler runs L3 in-driver sampling (default; disable + * with QWEN35_BS_DRIVER_SAMPLE=0). */ +bool bs_driver_sampling(const pn_batch_sched *sched); + +/** + * Wait for the driver to complete the FIRST decode step after bs_slot_bind. + * No token is deposited here — the first token was passed to bs_slot_bind. + * + * On return, `*logits_out` points to this slot's logits from the first decode + * step (valid until the next call to bs_slot_step_wait or bs_slot_release). + * + * Returns 0 on success, -1 on error (driver failed or shutdown) — the caller + * must still bs_slot_release. + */ +int bs_slot_await_first(pn_batch_sched *sched, int idx, float **logits_out); + +/** + * Per-step decode exchange from the request-thread side (steps 2+). + * + * Deposits `next_token` (sampled from previous step's logits) for the driver. + * Then waits for the driver to complete the next decode step and deposit new + * logits. + * + * On return, `*logits_out` points to this slot's logits (valid until the + * next call to bs_slot_step_wait or bs_slot_release). + * + * Returns 0 on success, -1 if the scheduler is shutting down or the step + * failed — the caller must still bs_slot_release. + */ +int bs_slot_step_wait(pn_batch_sched *sched, int idx, + uint32_t next_token, float **logits_out); + +/** + * Free this slot (EOS / stop / length limit / error). Must be called + * exactly once by the slot's owner after bs_slot_bind succeeded (on the + * pre-bind paths use bs_prefill_abort / plain release for a slot still in + * PREFILL that never took the GPU lock). Marks the slot IDLE for reuse. + */ +void bs_slot_release(pn_batch_sched *sched, int idx); + +/* Returns true when QWEN35_BATCH_SLOTS=1 (env check, cached). */ +bool bs_enabled(void); + +/** + * D3: Notify the scheduler that the host memory pressure level has changed. + * Wakes any waiters in the D1 queue so they can re-check whether they should + * proceed (pressure dropped) or keep waiting (still under pressure). Callers: + * the serve layer or any path that polls pn_memwatch after a level change. + * + * `level` is the new pn_mem_level value (0=OK … 3=CRITICAL). + * Thread-safe: yes. + */ +void bs_notify_pressure(pn_batch_sched *sched, int level); + +/* ============================================================ + * Dispatch layer (Phase G0): live load snapshot for self-registration + * ============================================================ */ + +/* Read-only load snapshot reported to the dispatch registry on each heartbeat. + * `capacity` is the served concurrency (batch-slot count); `active_slots` the + * currently-busy slots (not IDLE); `queue_depth` the D1 overflow waiters. */ +typedef struct { + int capacity; + int active_slots; + int queue_depth; + int free_slots; +} bs_load; + +/** + * Snapshot the scheduler's current load under `sched->mu` (a pure read — no + * bookkeeping added). When `sched == NULL` (the single-user, non-batched + * QWEN35_BATCH_SLOTS-off path) this reports capacity 1 / free 1: that path + * 429s a second concurrent chat, so it is treated as a single always-available + * slot for dispatch purposes. Thread-safe. + */ +void bs_get_load(pn_batch_sched *sched, bs_load *out); + +#endif /* PN_SERVE_BATCH_SCHED_H */ diff --git a/src/serve/grammar_spec_chooser.c b/src/serve/grammar_spec_chooser.c new file mode 100644 index 0000000..45786d4 --- /dev/null +++ b/src/serve/grammar_spec_chooser.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * Speculative decoding under a tool-call grammar — see grammar_spec_chooser.h + * for the exactness argument behind each branch. Pure C, libc-only. + */ +#include "grammar_spec_chooser.h" + +#include + +/* Text of a token, or NULL when it has none (an end/special token). */ +static const char *gsc_token_text(const gsc_chooser *c, uint32_t id) +{ + const char *text = c->text ? c->text(c->text_ctx, id) : NULL; + return (text && *text) ? text : NULL; +} + +/* The sampler's own rule: a token with no text is always allowed. */ +static bool gsc_token_allowed(const gsc_chooser *c, const void *walk, uint32_t id) +{ + const char *text = gsc_token_text(c, id); + return !text || c->ops->allowed(walk, text); +} + +static void gsc_advance_walk(gsc_chooser *c, uint32_t id) +{ + const char *text = gsc_token_text(c, id); + if (text) c->ops->advance(c->walk, text); + if (c->ops->complete(c->walk)) c->completed = true; +} + +int gsc_truncate_drafts(gsc_chooser *c, const uint32_t *drafts, int n) +{ + c->ops->copy(c->walk, c->live); + if (c->ops->complete(c->walk)) { + c->drafts_cut += (unsigned long)n; + return 0; + } + for (int i = 0; i < n; i++) { + if (c->ops->mask_active(c->walk) && !gsc_token_allowed(c, c->walk, drafts[i])) { + c->drafts_cut += (unsigned long)(n - i); + return i; + } + const char *text = gsc_token_text(c, drafts[i]); + if (text) c->ops->advance(c->walk, text); + if (c->ops->complete(c->walk)) { + c->drafts_cut += (unsigned long)(n - i - 1); + return i + 1; + } + } + return n; +} + +void gsc_begin_cycle(gsc_chooser *c) +{ + c->ops->copy(c->walk, c->live); + c->row = 0; + c->completed = false; +} + +/* The verify's greedy rule over a sparse row: largest value, lowest id on a tie + * (qwen35moe_resident_tt.c spec head). */ +static uint32_t gsc_greedy_pick(const uint32_t *index, const float *value, uint32_t count) +{ + uint32_t best = index[0]; + float best_value = value[0]; + for (uint32_t i = 1; i < count; i++) { + if (value[i] > best_value || (value[i] == best_value && index[i] < best)) { + best = index[i]; + best_value = value[i]; + } + } + return best; +} + +uint32_t gsc_choose(void *ctx, const uint32_t *index, const float *value, uint32_t count) +{ + gsc_chooser *c = (gsc_chooser *)ctx; + const int row = c->row++; + uint32_t token; + + if (c->completed) { + c->rows_deferred++; + return GSC_DEFER; + } + if (!c->ops->mask_active(c->walk)) { + if (c->base) + token = c->base(c->base_ctx, row, index, value, count); + else + token = count ? gsc_greedy_pick(index, value, count) : (row > 0 ? GSC_DEFER : 0u); + } else { + c->rows_masked++; + token = c->masked(c->masked_ctx, c->walk, row, index, value, count); + } + if (token == GSC_DEFER) { + c->rows_deferred++; + return GSC_DEFER; + } + gsc_advance_walk(c, token); + return token; +} diff --git a/src/serve/grammar_spec_chooser.h b/src/serve/grammar_spec_chooser.h new file mode 100644 index 0000000..02bd2ce --- /dev/null +++ b/src/serve/grammar_spec_chooser.h @@ -0,0 +1,106 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file grammar_spec_chooser.h + * @brief Speculative decoding under a tool-call grammar: draft truncation and a + * verify-row chooser whose output equals plain constrained decoding. + * + * A speculative cycle verifies [seed, d0, d1, ...] in one pass and gets, per row + * r, the card's exact top-N (index, value) pairs for the position after row r's + * input. Plain decoding would pick each token with the grammar mask applied to + * the DENSE row. This chooser makes the same pick, row by row, from a private + * copy of the grammar walk advanced over the tokens it has already chosen: + * + * - mask inactive at row r (reasoning, prose) → the unconstrained pick (the + * base chooser / the verify's greedy rule) — the mask is a no-op there. + * - mask active → the caller's masked pick + * with this private walk. It must return exactly the plain constrained + * step's token: the serve decides from the row's top-N when the shortlist + * rule proves that exact (grammar_mask_shortlist.h), else row 0 downloads + * the dense row and masks it as plain decode does, and a later row DEFERS — + * the cycle ends at the previous row and the next cycle meets this position + * as its row 0. + * - the row after the one that COMPLETED the call → DEFER, so nothing past the + * call is committed to the state. + * + * A draft is accepted iff it equals the chosen token, so a draft that breaks the + * grammar can never be accepted. gsc_truncate_drafts additionally cuts drafts + * at the first one the grammar rejects (it would cost a verify row for nothing) + * and right after the one that completes the call. + * + * Pure C, libc-only: the grammar, the token text, the base pick and the masked + * pick all arrive as callbacks, so tests/test_grammar_spec_chooser.c drives it + * with the real JSON grammar and synthetic rows, no device. + * + * @status Used by the Tenstorrent single-user serve loop (serve_generation.c, + * sg_spec_cycle) for tool_choice "required". + * @issues None + * @todo None + */ +#ifndef PN_GRAMMAR_SPEC_CHOOSER_H +#define PN_GRAMMAR_SPEC_CHOOSER_H + +#include +#include + +/** Same value as qwen35moe.h's PN_SPEC_CHOOSE_DEFER. */ +#define GSC_DEFER 0xFFFFFFFFu + +/** Grammar walk operations (one dialect's tg_ or tgj_ functions behind void pointers). */ +typedef struct { + void (*copy)(void *dst, const void *src); + bool (*allowed)(const void *st, const char *text); + void (*advance)(void *st, const char *text); + bool (*mask_active)(const void *st); + bool (*complete)(const void *st); +} gsc_grammar_ops; + +/** A token's decoded text (NULL/"" = no text: always allowed, no advance). */ +typedef const char *(*gsc_token_text_fn)(void *ctx, uint32_t id); + +/** Unconstrained pick for verify row `row` (sampling mode). May return + * GSC_DEFER for row > 0. */ +typedef uint32_t (*gsc_base_choose_fn)(void *ctx, int row, const uint32_t *index, + const float *value, uint32_t count); + +/** A row under an active mask: the plain constrained step's token for this row + * with `walk` as the grammar state, or GSC_DEFER when the row's top-N cannot + * decide it exactly (only for row > 0 — row 0 must decide). */ +typedef uint32_t (*gsc_masked_choose_fn)(void *ctx, void *walk, int row, const uint32_t *index, + const float *value, uint32_t count); + +typedef struct { + const gsc_grammar_ops *ops; + const void *live; /**< the serve's walk, already past the seed */ + void *walk; /**< caller-owned scratch of the same type */ + gsc_token_text_fn text; + void *text_ctx; + gsc_base_choose_fn base; /**< NULL = greedy (largest value, lowest id) */ + void *base_ctx; + gsc_masked_choose_fn masked; + void *masked_ctx; + int row; /**< next verify row this cycle */ + bool completed; /**< a chosen token completed the call */ + unsigned long rows_masked; /**< rows chosen under an active mask */ + unsigned long rows_deferred; /**< rows deferred (undecidable, or past the call) */ + unsigned long drafts_cut; /**< drafts removed by gsc_truncate_drafts */ +} gsc_chooser; + +/** + * Cut `drafts` to the prefix worth verifying from the live walk: stop before the + * first draft the grammar rejects while its mask is active, and right after the + * draft that completes the call. Uses (and clobbers) c->walk. + * @return the number of drafts kept (0..n). + */ +int gsc_truncate_drafts(gsc_chooser *c, const uint32_t *drafts, int n); + +/** Start a verify cycle: c->walk = c->live, row 0. */ +void gsc_begin_cycle(gsc_chooser *c); + +/** + * pn_spec_choose_fn-compatible chooser (ctx = gsc_chooser*). Called once per + * emitted row, in order. Returns the chosen token or GSC_DEFER (never at row 0). + */ +uint32_t gsc_choose(void *ctx, const uint32_t *index, const float *value, uint32_t count); + +#endif /* PN_GRAMMAR_SPEC_CHOOSER_H */ diff --git a/src/serve/graph_residency_json.c b/src/serve/graph_residency_json.c new file mode 100644 index 0000000..762609c --- /dev/null +++ b/src/serve/graph_residency_json.c @@ -0,0 +1,62 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file graph_residency_json.c + * @brief Implementation of the graphs_live/graphs_total JSON writers + * (see graph_residency_json.h). + * + * @status None + * @issues None + * @todo None + */ + +#include "graph_residency_json.h" + +/* One class object: {"live":..,"total":..,"failed":..,"ineligible":.., + * "pending":..}. `gave_up` is emitted only when non-zero — it is the loud + * case (a slot that spent its retry budget and stopped trying) and a zero + * would just be noise on every healthy read. */ +static void gr_write_counts(json_writer *jw, const pn_graph_residency *c) +{ + jw_printf(jw, "{\"live\":%d,\"total\":%d,\"failed\":%d," + "\"ineligible\":%d,\"pending\":%d", + c->live, c->total, c->failed, c->ineligible, c->pending); + if (c->gave_up > 0) + jw_printf(jw, ",\"gave_up\":%d", c->gave_up); + jw_raw(jw, "}"); +} + +/* Emit `,"":{...}` for a class the model has actually exercised. */ +static void gr_write_class(json_writer *jw, const char *name, + const pn_graph_residency *c) +{ + if (c->total == 0) + return; + jw_printf(jw, ",\"%s\":", name); + gr_write_counts(jw, c); +} + +void gr_write_summary(json_writer *jw, const pn_model_graph_residency *r) +{ + if (!jw || !r || r->all.total == 0) + return; + jw_raw(jw, ",\"graphs\":"); + gr_write_counts(jw, &r->all); +} + +void gr_write_detail(json_writer *jw, const pn_model_graph_residency *r) +{ + if (!jw || !r || r->all.total == 0) + return; + jw_printf(jw, ",\"graphs\":{\"live\":%d,\"total\":%d,\"failed\":%d," + "\"gave_up\":%d,\"ineligible\":%d,\"pending\":%d," + "\"captures\":%u,\"capture_failures\":%u", + r->all.live, r->all.total, r->all.failed, r->all.gave_up, + r->all.ineligible, r->all.pending, + r->all.lifetime_captures, r->all.lifetime_failures); + gr_write_class(jw, "batched_attn", &r->batched_attn); + gr_write_class(jw, "batched_ssm", &r->batched_ssm); + gr_write_class(jw, "single_ssm", &r->single_ssm); + gr_write_class(jw, "segment", &r->segment); + jw_raw(jw, "}"); +} diff --git a/src/serve/graph_residency_json.h b/src/serve/graph_residency_json.h new file mode 100644 index 0000000..ce2753e --- /dev/null +++ b/src/serve/graph_residency_json.h @@ -0,0 +1,59 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file graph_residency_json.h + * @brief Serialises a model's CUDA-graph residency into the JSON both the + * client surface (GET /v1/models) and the operator surface + * (GET /admin/models) emit. + * + * WHY IT IS ON THE WIRE AT ALL + * A CUDA-graph capture that fails degrades to the direct kernel body and + * produces CORRECT output — the only trace is that the layer costs its share + * of ~1.8 ms/step forever. Nothing in the logs, the completion `usage`, or + * any metric said so, which is how a serve could silently shed graph coverage + * layer by layer and still look healthy. This is that missing gauge. + * + * Three states, and only ONE of them is a fault: + * live — a captured graph is replaying. Good. + * failed — a capture was attempted and did not stick. THE fault. + * ineligible — this model never captures (its MoE experts are not Q3_K, so + * the grouped path is off). A Q5_K_M model reporting 40 + * ineligible layers is correct behaviour, not a problem. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_GRAPH_RESIDENCY_JSON_H +#define PN_SERVE_GRAPH_RESIDENCY_JSON_H + +#include "../models/gpu_graph_capture_tracker.h" +#include "json_writer.h" + +/** + * Append `,"graphs":{...}` — the client-facing summary. Writes NOTHING when + * the model has no graph slots at all (CPU-resident, or an architecture with no + * graph path), the same omit-when-unknown rule `context_window` follows. + * + * @param jw Open json_writer positioned inside a model object + * @param r Residency snapshot (mr_info.graphs) + * + * Thread-safe: yes (writes only to `jw`) + * Reentrant: yes + * Signal-safe: no + */ +void gr_write_summary(json_writer *jw, const pn_model_graph_residency *r); + +/** + * Append `,"graphs":{...}` with the per-class breakdown and the lifetime + * capture/failure counters — the operator view. A class the model has never + * exercised is omitted rather than reported as an empty deficit. + * + * Thread-safe: yes (writes only to `jw`) + * Reentrant: yes + * Signal-safe: no + */ +void gr_write_detail(json_writer *jw, const pn_model_graph_residency *r); + +#endif /* PN_SERVE_GRAPH_RESIDENCY_JSON_H */ diff --git a/src/serve/http_client.c b/src/serve/http_client.c new file mode 100644 index 0000000..a0aaff8 --- /dev/null +++ b/src/serve/http_client.c @@ -0,0 +1,273 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file http_client.c + * @brief Implementation of the minimal proxy HTTP client (see http_client.h). + * Blocking sockets with a non-blocking connect + select() timeout, then + * a straight relay of the response body to a sink. Headers are parsed + * only far enough to find the status code and the end of the header + * block; everything after CRLFCRLF is streamed through untouched, so SSE + * chunks (and chunked-transfer bytes) pass through unbuffered. + * + * The router hands the upstream the same body it received and forwards + * raw bytes back, so it does not need to understand chunked framing — it + * relays exactly what serve emits (serve uses Connection: close, so EOF + * marks the end). + * + * @status G2. + * @issues None + * @todo None + */ + +#include "http_client.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Portable memmem (avoid relying on the GNU/BSD extension across platforms). */ +static void *memmem_local(const void *hay, size_t hlen, const void *needle, + size_t nlen) +{ + if (nlen == 0) return (void *)hay; + if (hlen < nlen) return NULL; + const char *h = hay; + for (size_t i = 0; i + nlen <= hlen; i++) + if (memcmp(h + i, needle, nlen) == 0) + return (void *)(h + i); + return NULL; +} + +/* Parse "http://host:port" into host + port. Returns 0 on success. Port + * defaults to 80 if absent. A trailing path on the endpoint is ignored. */ +static int parse_endpoint(const char *endpoint, char *host, size_t host_cap, + int *port) +{ + const char *p = endpoint; + if (strncmp(p, "http://", 7) == 0) p += 7; + else if (strncmp(p, "https://", 8) == 0) return -1; /* TLS not handled here */ + + /* host is up to ':' or '/' or end. */ + size_t i = 0; + while (*p && *p != ':' && *p != '/') { + if (i + 1 >= host_cap) return -1; + host[i++] = *p++; + } + host[i] = '\0'; + if (i == 0) return -1; + + *port = 80; + if (*p == ':') { + p++; + int v = 0; + if (*p < '0' || *p > '9') return -1; + while (*p >= '0' && *p <= '9') { v = v * 10 + (*p - '0'); p++; } + *port = v; + } + return 0; +} + +/* Connect with a timeout. Returns fd (>=0) or -1. */ +static int connect_timeout(const char *host, int port, int connect_ms) +{ + if (connect_ms <= 0) connect_ms = 2000; + + char portstr[16]; + snprintf(portstr, sizeof(portstr), "%d", port); + struct addrinfo hints, *res = NULL; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + if (getaddrinfo(host, portstr, &hints, &res) != 0 || !res) + return -1; + + int fd = -1; + for (struct addrinfo *ai = res; ai; ai = ai->ai_next) { + fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); + if (fd < 0) continue; + + int flags = fcntl(fd, F_GETFL, 0); + fcntl(fd, F_SETFL, flags | O_NONBLOCK); + + int rc = connect(fd, ai->ai_addr, ai->ai_addrlen); + if (rc == 0) { + fcntl(fd, F_SETFL, flags); /* back to blocking */ + break; + } + if (errno == EINPROGRESS) { + fd_set wf; + FD_ZERO(&wf); + FD_SET(fd, &wf); + struct timeval tv = { connect_ms / 1000, (connect_ms % 1000) * 1000 }; + rc = select(fd + 1, NULL, &wf, NULL, &tv); + if (rc > 0) { + int soerr = 0; + socklen_t slen = sizeof(soerr); + getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &slen); + if (soerr == 0) { + fcntl(fd, F_SETFL, flags); /* connected — blocking */ + break; + } + } + } + close(fd); + fd = -1; + } + freeaddrinfo(res); + return fd; +} + +static int write_all(int fd, const char *buf, size_t len) +{ + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { if (errno == EINTR) continue; return -1; } + off += (size_t)n; + } + return 0; +} + +/* Core: send `method path` + body to endpoint, then relay the response body to + * `sink`. All the public entry points funnel through this. */ +static hc_result hc_do(const char *method, const char *endpoint, + const char *path, const char *body, size_t body_len, + const char *extra_hdrs, int connect_ms, + hc_sink_fn sink, void *user, int *status_out) +{ + if (status_out) *status_out = 0; + signal(SIGPIPE, SIG_IGN); + + char host[256]; + int port = 0; + if (parse_endpoint(endpoint, host, sizeof(host), &port) != 0) + return HC_CONNECT_FAILED; + + int fd = connect_timeout(host, port, connect_ms); + if (fd < 0) return HC_CONNECT_FAILED; + + /* Build + send the request. Connection: close so the response ends at EOF + * (no chunked-decode needed on our side; we relay raw bytes). */ + char header[1024]; + int hn = snprintf(header, sizeof(header), + "%s %s HTTP/1.1\r\n" + "Host: %s:%d\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %zu\r\n" + "Accept: text/event-stream\r\n" + "Connection: close\r\n" + "%s" + "\r\n", + method, path, host, port, body_len, + (extra_hdrs && extra_hdrs[0]) ? extra_hdrs : ""); + if (hn < 0 || (size_t)hn >= sizeof(header)) { close(fd); return HC_UPSTREAM_ERROR; } + if (write_all(fd, header, (size_t)hn) != 0) { close(fd); return HC_UPSTREAM_ERROR; } + if (body_len && write_all(fd, body, body_len) != 0) { close(fd); return HC_UPSTREAM_ERROR; } + + /* Read the response. Accumulate until we have the header block, parse the + * status line, then relay everything past CRLFCRLF to the sink and keep + * relaying subsequent reads until EOF. */ + char buf[8192]; + char *acc = NULL; + size_t acc_len = 0, acc_cap = 0; + int header_done = 0; + int status = 0; + int relayed_any = 0; + hc_result result = HC_UPSTREAM_ERROR; + + for (;;) { + ssize_t n = read(fd, buf, sizeof(buf)); + if (n < 0) { + if (errno == EINTR) continue; + result = relayed_any ? HC_MIDSTREAM_FAILED : HC_UPSTREAM_ERROR; + break; + } + if (n == 0) { + /* EOF. If we never finished headers, upstream error. Otherwise the + * body ended cleanly (Connection: close). */ + result = header_done ? HC_OK : HC_UPSTREAM_ERROR; + break; + } + + if (!header_done) { + /* Append to the header accumulator and look for CRLFCRLF. */ + if (acc_len + (size_t)n + 1 > acc_cap) { + size_t ncap = (acc_cap ? acc_cap * 2 : 8192); + while (ncap < acc_len + (size_t)n + 1) ncap *= 2; + char *g = realloc(acc, ncap); + if (!g) { free(acc); close(fd); return HC_UPSTREAM_ERROR; } + acc = g; + acc_cap = ncap; + } + memcpy(acc + acc_len, buf, (size_t)n); + acc_len += (size_t)n; + acc[acc_len] = '\0'; + + char *hend = memmem_local(acc, acc_len, "\r\n\r\n", 4); + if (!hend) continue; /* need more header bytes */ + + /* Parse status line: "HTTP/1.1 ...". */ + if (acc_len >= 12 && strncmp(acc, "HTTP/", 5) == 0) { + const char *sp = memchr(acc, ' ', acc_len); + if (sp) status = atoi(sp + 1); + } + if (status_out) *status_out = status; + header_done = 1; + + /* Relay any body bytes already read past the header terminator. */ + size_t body_off = (size_t)(hend - acc) + 4; + size_t have = acc_len - body_off; + if (have) { + relayed_any = 1; + if (sink(acc + body_off, have, user) != 0) { + result = HC_RELAY_ABORTED; + break; + } + } + free(acc); + acc = NULL; + continue; + } + + /* Header already done — straight relay. */ + relayed_any = 1; + if (sink(buf, (size_t)n, user) != 0) { result = HC_RELAY_ABORTED; break; } + } + + free(acc); + close(fd); + return result; +} + +hc_result hc_post_stream(const char *endpoint, const char *path, + const char *body, size_t body_len, + const char *extra_hdrs, int connect_ms, + hc_sink_fn sink, void *user, int *status_out) +{ + return hc_do("POST", endpoint, path, body, body_len, extra_hdrs, + connect_ms, sink, user, status_out); +} + +/* Discarding sink for hc_request_simple — we only care about the status code. */ +static int discard_sink(const char *bytes, size_t len, void *user) +{ + (void)bytes; (void)len; (void)user; + return 0; +} + +hc_result hc_request_simple(const char *method, const char *endpoint, + const char *path, const char *body, size_t body_len, + int connect_ms, int *status_out) +{ + return hc_do(method, endpoint, path, body, body_len, NULL, + connect_ms, discard_sink, NULL, status_out); +} diff --git a/src/serve/http_client.h b/src/serve/http_client.h new file mode 100644 index 0000000..a7f5b5f --- /dev/null +++ b/src/serve/http_client.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file http_client.h + * @brief Minimal blocking HTTP/1.1 client for the dispatch router's reverse + * proxy (Phase G2). Just enough to open a TCP connection to an upstream + * serve, send a POST with a JSON body, and stream the response back + * chunk-by-chunk through a sink callback so SSE passes through UNBUFFERED. + * + * Only `http://host:port` endpoints are supported (the local + WG + * positions are plain HTTP; TLS is terminated at Caddy in the VPS + * position, not here). No external deps — POSIX sockets only. + * + * @status G2: connect + POST + streamed response relay with a connect-timeout + * and a first-byte distinction for failover. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_HTTP_CLIENT_H +#define PN_SERVE_HTTP_CLIENT_H + +#include + +/* Sink for streamed response bytes. Called repeatedly with raw body bytes as + * they arrive from the upstream (headers already stripped). Return 0 to keep + * going, non-zero to abort the relay (e.g. the downstream client went away). */ +typedef int (*hc_sink_fn)(const char *bytes, size_t len, void *user); + +/* Outcome of an hc_post_stream call — lets the router decide whether to fail + * over (safe only if NOTHING was relayed to the client yet). */ +typedef enum { + HC_OK = 0, /* completed; body fully relayed */ + HC_CONNECT_FAILED, /* could not connect / DNS / bad endpoint — pre-relay */ + HC_UPSTREAM_ERROR, /* connected but no valid response before any body — pre-relay */ + HC_RELAY_ABORTED, /* the sink returned non-zero (downstream closed) — post-relay */ + HC_MIDSTREAM_FAILED, /* upstream dropped AFTER we began relaying body — NOT safe to retry */ +} hc_result; + +/** + * POST `body` to `endpoint`+`path` and stream the response body to `sink`. + * + * @param endpoint upstream base, e.g. "http://127.0.0.1:8201" (no trailing /) + * @param path request target, e.g. "/v1/chat/completions" + * @param body request body bytes (JSON), or NULL + * @param body_len length of body + * @param extra_hdrs NULL, or additional "Name: value\r\n" header lines to send + * @param connect_ms connect timeout in milliseconds (<=0 ⇒ a sane default) + * @param sink called with response body bytes as they arrive + * @param user opaque passed to sink + * @param status_out if non-NULL, receives the upstream HTTP status code (or 0) + * + * @return an hc_result. HC_CONNECT_FAILED / HC_UPSTREAM_ERROR mean nothing was + * relayed yet (safe to fail over to another instance). + */ +hc_result hc_post_stream(const char *endpoint, const char *path, + const char *body, size_t body_len, + const char *extra_hdrs, int connect_ms, + hc_sink_fn sink, void *user, int *status_out); + +/** + * Simple request/response helper (no streaming) used by the serve-side + * registry client for register/heartbeat/deregister. Sends `method` + * (e.g. "POST"/"DELETE") with a JSON body and discards the response body, + * returning only the HTTP status via *status_out. + * + * @return HC_OK if a full response arrived (check *status_out for 2xx); + * HC_CONNECT_FAILED / HC_UPSTREAM_ERROR on transport failure. + */ +hc_result hc_request_simple(const char *method, const char *endpoint, + const char *path, const char *body, size_t body_len, + int connect_ms, int *status_out); + +#endif /* PN_SERVE_HTTP_CLIENT_H */ diff --git a/src/serve/http_server.c b/src/serve/http_server.c new file mode 100644 index 0000000..e7025e8 --- /dev/null +++ b/src/serve/http_server.c @@ -0,0 +1,336 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file http_server.c + * @brief Implementation of the minimal HTTP/1.1 server (see http_server.h). + * Uses BSD sockets; SIGPIPE is ignored so a client disconnect mid-stream + * cannot kill the server. Each request/response is per-connection and + * carries no shared state, so the accept loop in serve_command.c drives + * one of these handlers per connection thread concurrently. + * + * @status Complete for the serve layer. + * @issues None + * @todo None + */ + +#include "http_server.h" + +#include +/* Hard ceiling on a request body (Content-Length). The largest legitimate body + * is a /session/restore blob: the biggest session snapshot on the fleet is + * ~130 MB (ornith), ~173 MB base64'd, plus the JSON envelope — comfortably + * under this. The cap exists so a hostile/broken client cannot declare a + * multi-GB Content-Length and drive the connection thread to malloc it and OOM + * the 7.8 GB host (a DoS with no per-request memory admission on this path). + * Anything over the cap is refused before a single body byte is allocated. */ +#define HS_MAX_BODY_BYTES ((size_t)512 * 1024 * 1024) +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Write all `len` bytes, retrying short/interrupted writes. Returns 0 on + * success, -1 if the peer went away. */ +static int write_all(int fd, const char *buf, size_t len) +{ + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { + if (errno == EINTR) continue; + return -1; + } + off += (size_t)n; + } + return 0; +} + +int hs_listen(const char *host, int port) +{ + /* A broken client connection must not raise SIGPIPE and kill the daemon. */ + signal(SIGPIPE, SIG_IGN); + + int fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + fprintf(stderr, "serve: socket() failed: %s\n", strerror(errno)); + return -1; + } + + int one = 1; + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + struct sockaddr_in addr = {0}; + addr.sin_family = AF_INET; + addr.sin_port = htons((uint16_t)port); + if (inet_pton(AF_INET, host, &addr.sin_addr) != 1) { + fprintf(stderr, "serve: bad host address '%s'\n", host); + close(fd); + return -1; + } + + if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { + fprintf(stderr, "serve: bind(%s:%d) failed: %s\n", host, port, + strerror(errno)); + close(fd); + return -1; + } + if (listen(fd, 16) < 0) { + fprintf(stderr, "serve: listen() failed: %s\n", strerror(errno)); + close(fd); + return -1; + } + return fd; +} + +int hs_accept(int listen_fd) +{ + int cfd = accept(listen_fd, NULL, NULL); + if (cfd < 0 && errno != EINTR) + fprintf(stderr, "serve: accept() failed: %s\n", strerror(errno)); + return cfd; +} + +/* Case-insensitive search for a header value; returns a pointer into `headers` + * at the start of the value, or NULL. Value ends at CRLF. */ +static const char *find_header(const char *headers, const char *name) +{ + size_t nlen = strlen(name); + const char *p = headers; + while (*p) { + const char *line_end = strstr(p, "\r\n"); + if (!line_end) break; + if ((size_t)(line_end - p) > nlen && strncasecmp(p, name, nlen) == 0 && + p[nlen] == ':') { + const char *v = p + nlen + 1; + while (*v == ' ' || *v == '\t') v++; + return v; + } + p = line_end + 2; + } + return NULL; +} + +int hs_read_request(int client_fd, hs_request *req) +{ + memset(req, 0, sizeof(*req)); + + size_t cap = 8192, len = 0; + char *buf = malloc(cap); + if (!buf) return -1; + + /* Read until we have the full header block (terminated by CRLFCRLF). */ + char *header_end = NULL; + while (!header_end) { + if (len + 1 >= cap) { + cap *= 2; + char *grown = realloc(buf, cap); + if (!grown) { free(buf); return -1; } + buf = grown; + } + ssize_t n = read(client_fd, buf + len, cap - len - 1); + if (n <= 0) { free(buf); return -1; } + len += (size_t)n; + buf[len] = '\0'; + header_end = strstr(buf, "\r\n\r\n"); + } + + /* Parse the request line: METHOD SP TARGET SP VERSION. */ + char *sp1 = strchr(buf, ' '); + if (!sp1) { free(buf); return -1; } + char *sp2 = strchr(sp1 + 1, ' '); + if (!sp2) { free(buf); return -1; } + req->method = strndup(buf, (size_t)(sp1 - buf)); + req->path = strndup(sp1 + 1, (size_t)(sp2 - sp1 - 1)); + + /* Optional prefix-cache session key (PC0.1): X-Session-Id header. Copied + * out before `buf` is freed. Absent → NULL (anonymous / full prefill). */ + const char *sid = find_header(buf, "X-Session-Id"); + if (sid) { + const char *e = strstr(sid, "\r\n"); + size_t slen = e ? (size_t)(e - sid) : strlen(sid); + req->session_id = strndup(sid, slen); + } + + /* Body: read Content-Length bytes past the header terminator. */ + size_t body_offset = (size_t)(header_end - buf) + 4; + size_t content_length = 0; + const char *cl = find_header(buf, "Content-Length"); + if (cl) content_length = (size_t)strtoul(cl, NULL, 10); + + /* Refuse an oversized body BEFORE allocating it: a hostile Content-Length + * would otherwise malloc gigabytes on this connection thread and OOM the + * host. Answer a clean 413 so a legit-but-too-large client gets a reason. */ + if (content_length > HS_MAX_BODY_BYTES) { + hs_send_error(client_fd, 413, "payload_too_large", + "request body exceeds the server limit"); + free(buf); + return -1; + } + + if (content_length > 0) { + req->body = malloc(content_length + 1); + if (!req->body) { hs_request_free(req); free(buf); return -1; } + size_t have = len - body_offset; + if (have > content_length) have = content_length; + memcpy(req->body, buf + body_offset, have); + size_t got = have; + while (got < content_length) { + ssize_t n = read(client_fd, req->body + got, content_length - got); + if (n <= 0) { hs_request_free(req); free(buf); return -1; } + got += (size_t)n; + } + req->body[content_length] = '\0'; + req->body_len = content_length; + } + + free(buf); + return (req->method && req->path) ? 0 : -1; +} + +void hs_request_free(hs_request *req) +{ + if (!req) return; + free(req->method); + free(req->path); + free(req->body); + free(req->session_id); + memset(req, 0, sizeof(*req)); +} + +/* Human-readable reason phrase for the status codes the serve layer emits. */ +static const char *reason_phrase(int status) +{ + switch (status) { + case 200: return "OK"; + case 400: return "Bad Request"; + case 404: return "Not Found"; + case 405: return "Method Not Allowed"; + case 413: return "Payload Too Large"; + case 429: return "Too Many Requests"; + case 500: return "Internal Server Error"; + case 503: return "Service Unavailable"; + default: return "OK"; + } +} + +void hs_send_json(int client_fd, int status, const char *json_body) +{ + size_t blen = json_body ? strlen(json_body) : 0; + char header[256]; + int hn = snprintf(header, sizeof(header), + "HTTP/1.1 %d %s\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %zu\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n\r\n", + status, reason_phrase(status), blen); + if (hn < 0) return; + if (write_all(client_fd, header, (size_t)hn) != 0) return; + if (blen) write_all(client_fd, json_body, blen); +} + +void hs_send_error(int client_fd, int status, const char *type, + const char *message) +{ + hs_send_error_ex(client_fd, status, type, message, NULL); +} + +void hs_send_error_ex(int client_fd, int status, const char *type, + const char *message, const char *extra_headers) +{ + /* Small, hand-built to avoid a json_writer dependency in the socket layer. */ + char body[1024]; + /* Escape the message minimally (quotes/backslashes) into a temp buffer. */ + char esc[512]; + size_t j = 0; + for (const char *s = message; *s && j < sizeof(esc) - 2; s++) { + if (*s == '"' || *s == '\\') esc[j++] = '\\'; + esc[j++] = *s; + } + esc[j] = '\0'; + int bn = snprintf(body, sizeof(body), + "{\"error\":{\"message\":\"%s\",\"type\":\"%s\",\"code\":null}}", + esc, type); + if (bn < 0) return; + + /* When extra headers are provided (e.g. "Retry-After: 5\r\n"), inject + * them before the blank line that ends the response headers. */ + if (!extra_headers || !extra_headers[0]) { + hs_send_json(client_fd, status, body); + return; + } + + size_t blen = (size_t)bn; + char header[384]; + int hn = snprintf(header, sizeof(header), + "HTTP/1.1 %d %s\r\n" + "Content-Type: application/json\r\n" + "Content-Length: %zu\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "%s\r\n", /* extra_headers + blank line */ + status, reason_phrase(status), blen, extra_headers); + if (hn < 0) return; + if (write_all(client_fd, header, (size_t)hn) != 0) return; + write_all(client_fd, body, blen); +} + +void hs_begin_sse(int client_fd) +{ + static const char header[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n\r\n"; + write_all(client_fd, header, sizeof(header) - 1); +} + +void hs_send_sse(int client_fd, const char *json_data) +{ + write_all(client_fd, "data: ", 6); + write_all(client_fd, json_data, strlen(json_data)); + write_all(client_fd, "\n\n", 2); +} + +void hs_send_sse_comment(int client_fd, const char *text) +{ + write_all(client_fd, ": ", 2); + write_all(client_fd, text, strlen(text)); + write_all(client_fd, "\n\n", 2); +} + +void hs_end_sse(int client_fd) +{ + /* sizeof-1, not a hand-counted 13 — "data: [DONE]\n\n" is 14 bytes; writing 13 + * dropped the terminating blank line of the final SSE event. */ + static const char done[] = "data: [DONE]\n\n"; + write_all(client_fd, done, sizeof(done) - 1); +} + +void hs_close(int client_fd) +{ + close(client_fd); +} + +int hs_client_alive(int client_fd) +{ + if (client_fd < 0) + return 0; + /* Peek one byte without blocking: recv()==0 is an orderly peer close (the + * client aborted / disconnected); EAGAIN is open-but-idle (the normal case + * while we stream); >0 is buffered data (still open). */ + char probe; + ssize_t r = recv(client_fd, &probe, 1, MSG_PEEK | MSG_DONTWAIT); + if (r == 0) + return 0; + if (r < 0) + return (errno == EAGAIN || errno == EWOULDBLOCK) ? 1 : 0; + return 1; +} diff --git a/src/serve/http_server.h b/src/serve/http_server.h new file mode 100644 index 0000000..cf640d2 --- /dev/null +++ b/src/serve/http_server.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file http_server.h + * @brief Minimal HTTP/1.1 server over POSIX sockets — just enough to host the + * OpenAI-compatible endpoints. Supports a plain JSON response path and a + * chunked Server-Sent-Events path. Each request/response is + * per-connection with no shared state, so serve_command.c drives one + * handler per connection thread concurrently. + * + * JACLibc was evaluated first (global instruction / plan hint) but its socket + * surface (include/core/jsio.h) is a Node-style JS runtime abstraction, not a + * drop-in POSIX helper, and pulling in a whole alternate libc under Pantheon's + * Accelerate build is a heavy dep. Per the plan, minimal POSIX sockets are used. + * + * @status Serves GET/POST with Content-Length bodies; SSE streaming works. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_HTTP_SERVER_H +#define PN_SERVE_HTTP_SERVER_H + +#include + +/* A parsed HTTP request. Buffers are owned; release with hs_request_free(). */ +typedef struct { + char *method; /* "GET", "POST", ... (owned) */ + char *path; /* request target, e.g. "/v1/chat/completions" (owned) */ + char *body; /* request body bytes (owned, NUL-terminated), or NULL */ + size_t body_len; /* body length in bytes */ + char *session_id; /* optional X-Session-Id header value (owned), or NULL — + * the prefix-cache session key (PC0.1). */ +} hs_request; + +/** + * Bind and listen on host:port (host is a dotted-quad, e.g. "127.0.0.1"). + * @return listening socket fd, or -1 on error (message logged to stderr). + */ +int hs_listen(const char *host, int port); + +/** Accept one client connection. Returns the client fd, or -1 on error. */ +int hs_accept(int listen_fd); + +/** + * Read and parse one request from a client fd. Reads headers then the body per + * Content-Length. Returns 0 on success (fills *req), -1 on error/EOF. + * On success the caller must hs_request_free(req). + */ +int hs_read_request(int client_fd, hs_request *req); + +/** Free the owned buffers of a request. NULL-safe. */ +void hs_request_free(hs_request *req); + +/** Send a complete JSON response with the given HTTP status code. */ +void hs_send_json(int client_fd, int status, const char *json_body); + +/** + * Send an OpenAI-shaped error response: + * {"error":{"message":..,"type":..,"code":null}} + */ +void hs_send_error(int client_fd, int status, const char *type, + const char *message); + +/** + * Like hs_send_error but injects `extra_headers` (may be NULL) into the + * response before the terminal blank line. Each header must be in the form + * "Name: value\r\n". Used for Retry-After on 429/503 responses. + */ +void hs_send_error_ex(int client_fd, int status, const char *type, + const char *message, const char *extra_headers); + +/** Begin a Server-Sent-Events stream (writes 200 + SSE headers). */ +void hs_begin_sse(int client_fd); + +/** Send one SSE event: "data: \n\n". */ +void hs_send_sse(int client_fd, const char *json_data); + +/** Send an SSE comment line (": \n\n"). Clients ignore it as an event but + * it is body bytes, so a stall watchdog that counts bytes sees the stream alive. + * `text` must not contain a newline. */ +void hs_send_sse_comment(int client_fd, const char *text); + +/** Send the terminal "data: [DONE]\n\n" SSE event. */ +void hs_end_sse(int client_fd); + +/** Close a client connection. */ +void hs_close(int client_fd); + +/** + * Non-blocking check that the client is still connected: 1 if open, 0 if the + * peer has closed (an orderly shutdown seen via a zero-length MSG_PEEK) or the + * fd is dead. Used to abort a long prefill the instant the client disconnects, + * releasing the model slot instead of finishing a response nobody will read. + */ +int hs_client_alive(int client_fd); + +#endif /* PN_SERVE_HTTP_SERVER_H */ diff --git a/src/serve/json_canonical_stream.c b/src/serve/json_canonical_stream.c new file mode 100644 index 0000000..71da13e --- /dev/null +++ b/src/serve/json_canonical_stream.c @@ -0,0 +1,375 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_canonical_stream.c + * @brief Implementation of the incremental JSON re-serializer + * (see json_canonical_stream.h). A pushdown state machine fed one byte + * at a time (plain string bytes in bulk), mirroring json_reader.c's + * acceptance rule for rule. + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "json_canonical_stream.h" + +#include +#include +#include + +#include "json_reader.h" + +typedef enum { + ST_VALUE = 0, /* a value is owed (whitespace skipped) */ + ST_OBJ_FIRST, /* just after '{': a key or '}' */ + ST_OBJ_KEY, /* just after ',' in an object: a key */ + ST_COLON, /* after a key: ':' */ + ST_ARR_FIRST, /* just after '[': a value or ']' */ + ST_AFTER, /* after a value inside a container: ',' or the closer */ + ST_STRING, /* inside a string (key or value) */ + ST_TOKEN, /* inside a number or literal */ + ST_END, /* DONE or FAILED */ +} jcs_state; + +typedef enum { + ESC_NONE = 0, + ESC_BACKSLASH, /* read '\' */ + ESC_HEX, /* reading the 4 digits of \uXXXX */ + ESC_HIGH, /* a high surrogate was read: is "\u" next? */ + ESC_HIGH_BACKSLASH, /* high surrogate, then '\' */ + ESC_LOW, /* reading the 4 digits of the low half */ +} jcs_escape; + +static bool is_json_ws(char c) +{ + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +/* Bytes that end a number/literal token. jr_parse reads a scalar and then + * requires whitespace, ',' or the closer; any other structural byte right after + * a scalar is a rejection, which the state after the token reports. */ +static bool ends_token(char c) +{ + return is_json_ws(c) || c == ',' || c == '}' || c == ']' || c == '{' || + c == '[' || c == '"' || c == ':'; +} + +static int hex_nibble(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +static void fail(jcs_parser *p) +{ + p->status = JCS_FAILED; + p->state = ST_END; +} + +static bool stack_push(jcs_parser *p, char kind) +{ + jcs_stack *s = &p->stack; + if (s->depth >= s->cap) { + int cap = s->cap ? s->cap * 2 : 16; + char *grown = realloc(s->kinds, (size_t)cap); + if (!grown) return false; + s->kinds = grown; + s->cap = cap; + } + s->kinds[s->depth++] = kind; + return true; +} + +static char stack_top(const jcs_parser *p) +{ + return p->stack.depth ? p->stack.kinds[p->stack.depth - 1] : 0; +} + +/* Inside the root object, at member level (where the events fire). */ +static bool at_root_member(const jcs_parser *p) +{ + return p->stack.depth == 1 && p->stack.kinds[0] == 'o'; +} + +bool jcs_init(jcs_parser *p, const jcs_events *ev) +{ + memset(p, 0, sizeof(*p)); + if (ev) p->ev = *ev; + if (!jw_init(&p->out) || !jw_init(&p->token) || !jw_init(&p->decoded)) { + jcs_free(p); + return false; + } + p->state = ST_VALUE; + return true; +} + +void jcs_free(jcs_parser *p) +{ + jw_free(&p->out); + jw_free(&p->token); + jw_free(&p->decoded); + free(p->stack.kinds); + p->stack.kinds = NULL; + p->stack.depth = p->stack.cap = 0; +} + +/* A value just finished at the current depth. */ +static void value_complete(jcs_parser *p, const char *decoded) +{ + if (p->stack.depth == 0) { + p->status = JCS_DONE; + p->state = ST_END; + return; + } + if (at_root_member(p) && p->ev.root_value_end) + p->ev.root_value_end(p->ev.ud, p->out.len, decoded); + p->state = ST_AFTER; +} + +/* Append decoded string bytes: to the decoded copy, and (for a value) escaped + * to the output. Nothing after a decoded NUL survives — the DOM keeps strings + * as C strings, so jr_to_json stops there. */ +static void emit_decoded(jcs_parser *p, const char *bytes, size_t n) +{ + if (p->str_nul || n == 0) return; + jw_raw_n(&p->decoded, bytes, n); + if (!p->str_is_key) jw_escaped_n(&p->out, bytes, n); +} + +/* One code point, encoded exactly as json_reader.c's append_utf8 does (the + * same truncating char casts, so even out-of-range surrogate arithmetic matches + * byte for byte). */ +static void emit_code_point(jcs_parser *p, uint32_t cp) +{ + char buf[4]; + size_t len = 0; + if (cp == 0) { p->str_nul = true; return; } + if (cp <= 0x7F) { + buf[len++] = (char)cp; + } else if (cp <= 0x7FF) { + buf[len++] = (char)(0xC0 | (cp >> 6)); + buf[len++] = (char)(0x80 | (cp & 0x3F)); + } else if (cp <= 0xFFFF) { + buf[len++] = (char)(0xE0 | (cp >> 12)); + buf[len++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[len++] = (char)(0x80 | (cp & 0x3F)); + } else { + buf[len++] = (char)(0xF0 | (cp >> 18)); + buf[len++] = (char)(0x80 | ((cp >> 12) & 0x3F)); + buf[len++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[len++] = (char)(0x80 | (cp & 0x3F)); + } + emit_decoded(p, buf, len); +} + +static void begin_string(jcs_parser *p, bool is_key) +{ + p->str_is_key = is_key; + p->str_nul = false; + p->esc = ESC_NONE; + p->decoded.len = 0; + p->decoded.data[0] = '\0'; + if (!is_key) jw_raw_n(&p->out, "\"", 1); + p->state = ST_STRING; +} + +static void end_string(jcs_parser *p) +{ + if (p->str_is_key) { + jw_string_n(&p->out, p->decoded.data, p->decoded.len); + if (at_root_member(p) && p->ev.root_key) + p->ev.root_key(p->ev.ud, p->decoded.data); + p->state = ST_COLON; + return; + } + jw_raw_n(&p->out, "\"", 1); + value_complete(p, p->decoded.data); +} + +/* Write a finished number/literal, or fail if jr_parse would reject it. */ +static void finish_token(jcs_parser *p) +{ + const char *t = p->token.data; + size_t len = p->token.len; + if (t[0] == 't' || t[0] == 'f' || t[0] == 'n') { + if (strcmp(t, "true") != 0 && strcmp(t, "false") != 0 && + strcmp(t, "null") != 0) { fail(p); return; } + jw_raw_n(&p->out, t, len); + } else { + char *end = NULL; + strtod(t, &end); + if (end == t || end != t + len) { fail(p); return; } + jr_value *v = jr_parse(t); /* the DOM's own number → text path */ + if (!v) { fail(p); return; } + jr_to_json(v, &p->out); + jr_free(v); + } + value_complete(p, NULL); +} + +/* The first byte of a value (whitespace already skipped). */ +static void begin_value(jcs_parser *p, char c) +{ + if (c == ',' || c == '}' || c == ']' || c == ':') { fail(p); return; } + if (at_root_member(p) && p->ev.root_value_begin) + p->ev.root_value_begin(p->ev.ud, p->out.len, c); + if (c == '"') { begin_string(p, false); return; } + if (c == '{' || c == '[') { + if (!stack_push(p, c == '{' ? 'o' : 'a')) { fail(p); return; } + jw_raw_n(&p->out, &c, 1); + p->state = (c == '{') ? ST_OBJ_FIRST : ST_ARR_FIRST; + return; + } + p->token.len = 0; + jw_raw_n(&p->token, &c, 1); + p->state = ST_TOKEN; +} + +static void close_container(jcs_parser *p, char closer) +{ + jw_raw_n(&p->out, &closer, 1); + p->stack.depth--; + value_complete(p, NULL); +} + +/* One escape-sequence byte. Returns true when `c` must be re-read in the + * (new) state — the byte after a high surrogate that turned out not to start a + * low one. */ +static bool step_escape(jcs_parser *p, char c) +{ + switch (p->esc) { + case ESC_BACKSLASH: { + char out; + switch (c) { + case '"': case '\\': case '/': out = c; break; + case 'b': out = '\b'; break; + case 'f': out = '\f'; break; + case 'n': out = '\n'; break; + case 'r': out = '\r'; break; + case 't': out = '\t'; break; + case 'u': p->esc = ESC_HEX; p->hex_n = 0; p->hex_val = 0; return false; + default: fail(p); return false; + } + emit_decoded(p, &out, 1); + p->esc = ESC_NONE; + return false; + } + case ESC_HEX: + case ESC_LOW: { + int nib = hex_nibble(c); + if (nib < 0) { fail(p); return false; } + p->hex_val = (p->hex_val << 4) | nib; + if (++p->hex_n < 4) return false; + if (p->esc == ESC_LOW) { + /* json_reader.c's arithmetic verbatim (int, unchecked low half). */ + int cp = 0x10000 + (((p->high - 0xD800) << 10) | (p->hex_val - 0xDC00)); + emit_code_point(p, (uint32_t)cp); + p->esc = ESC_NONE; + } else if (p->hex_val >= 0xD800 && p->hex_val <= 0xDBFF) { + p->high = p->hex_val; + p->esc = ESC_HIGH; + } else { + emit_code_point(p, (uint32_t)p->hex_val); + p->esc = ESC_NONE; + } + return false; + } + case ESC_HIGH: + if (c == '\\') { p->esc = ESC_HIGH_BACKSLASH; return false; } + emit_code_point(p, (uint32_t)p->high); + p->esc = ESC_NONE; + return true; + case ESC_HIGH_BACKSLASH: + if (c == 'u') { p->esc = ESC_LOW; p->hex_n = 0; p->hex_val = 0; return false; } + emit_code_point(p, (uint32_t)p->high); + p->esc = ESC_BACKSLASH; /* the '\' starts an ordinary escape */ + return true; + default: + return false; + } +} + +/* One byte in a structural (non-string, non-token) state. */ +static void step_structural(jcs_parser *p, char c) +{ + if (is_json_ws(c)) return; + switch (p->state) { + case ST_VALUE: + begin_value(p, c); + return; + case ST_OBJ_FIRST: + if (c == '}') { close_container(p, '}'); return; } + if (c == '"') { begin_string(p, true); return; } + fail(p); + return; + case ST_OBJ_KEY: + if (c == '"') { begin_string(p, true); return; } + fail(p); + return; + case ST_COLON: + if (c == ':') { jw_raw_n(&p->out, ":", 1); p->state = ST_VALUE; return; } + fail(p); + return; + case ST_ARR_FIRST: + if (c == ']') { close_container(p, ']'); return; } + p->state = ST_VALUE; + begin_value(p, c); + return; + case ST_AFTER: { + char top = stack_top(p); + if (c == ',') { + jw_raw_n(&p->out, ",", 1); + p->state = (top == 'o') ? ST_OBJ_KEY : ST_VALUE; + return; + } + if ((top == 'o' && c == '}') || (top == 'a' && c == ']')) { + close_container(p, c); + return; + } + fail(p); + return; + } + default: + return; + } +} + +/* Run of plain string bytes starting at bytes[i]: neither '"' nor '\'. */ +static size_t plain_run(const char *bytes, size_t i, size_t n) +{ + size_t j = i; + while (j < n && bytes[j] != '"' && bytes[j] != '\\') j++; + return j - i; +} + +size_t jcs_feed(jcs_parser *p, const char *bytes, size_t n) +{ + size_t i = 0; + while (i < n && p->status == JCS_RUNNING) { + char c = bytes[i]; + if (p->state == ST_STRING) { + if (p->esc == ESC_NONE) { + size_t run = plain_run(bytes, i, n); + if (run) { emit_decoded(p, bytes + i, run); i += run; continue; } + if (c == '"') end_string(p); + else p->esc = ESC_BACKSLASH; + i++; + continue; + } + if (!step_escape(p, c)) i++; + continue; + } + if (p->state == ST_TOKEN) { + if (!ends_token(c)) { jw_raw_n(&p->token, &c, 1); i++; continue; } + finish_token(p); /* then re-read c after the value */ + continue; + } + step_structural(p, c); + i++; + } + if (p->out.oom || p->token.oom || p->decoded.oom) fail(p); + return i; +} diff --git a/src/serve/json_canonical_stream.h b/src/serve/json_canonical_stream.h new file mode 100644 index 0000000..568e926 --- /dev/null +++ b/src/serve/json_canonical_stream.h @@ -0,0 +1,108 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_canonical_stream.h + * @brief Incremental JSON re-serializer: fed raw JSON text in arbitrary + * pieces, it writes — as early as each byte is DECIDED — exactly the + * bytes `jr_to_json(jr_parse(text))` would produce for the whole text. + * + * Why it exists. The serve's tool-call parser builds a call's `arguments` + * string by parsing the model's JSON into a DOM (json_reader) and + * re-serializing it compactly (jr_to_json). Streaming those arguments while the + * model is still writing them is only correct if the bytes streamed so far are + * a PREFIX of that final re-serialization. This module is the incremental half + * of that contract: everything it writes to `out` is final, and when the input + * is complete `out` is byte-identical to jr_to_json's output. + * + * Acceptance mirrors jr_parse, not RFC 8259: the same whitespace set + * (space/tab/LF/CR), the same string escapes (including the unpaired-surrogate + * and "\u0000 truncates the C string" behaviour), numbers read by strtod and + * written by jr_to_json, literals true/false/null. A text jr_parse rejects puts + * the parser in JCS_FAILED at the first byte that decides the rejection. + * + * What is held back (not yet in `out`): a number or literal until the byte that + * ends it (its value — and so its re-serialized form — is unknown until then), + * an object key until its closing quote, a partial escape sequence, and a high + * surrogate until the next bytes show whether a low surrogate follows. String + * CONTENT is written as it arrives, which is what makes a long string value + * stream. + * + * Root-object member events (jcs_events) let a caller find one member's value + * inside `out` — the tool-call streamer uses them to slice out "arguments". + * + * Depends only on libc + json_writer/json_reader. + * + * @status Complete; byte-equivalence with jr_parse+jr_to_json is property-tested + * over generated documents fed in random splits + * (tests/test_tool_call_stream.c). + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_JSON_CANONICAL_STREAM_H +#define PN_SERVE_JSON_CANONICAL_STREAM_H + +#include +#include + +#include "json_writer.h" + +typedef enum { + JCS_RUNNING = 0, /* more input wanted */ + JCS_DONE, /* the root value is complete; later bytes are ignored */ + JCS_FAILED, /* jr_parse would reject this text */ +} jcs_status; + +/* Callbacks for the members of a ROOT OBJECT (depth 1). Every pointer may be + * NULL. Offsets are into the parser's `out`. */ +typedef struct { + /* A member key completed (decoded; cut at an embedded NUL as the DOM does). */ + void (*root_key)(void *ud, const char *key); + /* That member's value begins; its first canonical byte will be at `at`. + * `first` is the value's first raw char ('{', '[', '"', or a scalar's). */ + void (*root_value_begin)(void *ud, size_t at, char first); + /* That member's value is complete; its canonical bytes end at `end`. + * `decoded` is the decoded string for a string value, else NULL. */ + void (*root_value_end)(void *ud, size_t end, const char *decoded); + void *ud; +} jcs_events; + +/* One frame of the container stack: 'o' object, 'a' array. */ +typedef struct { + char *kinds; + int depth; + int cap; +} jcs_stack; + +typedef struct { + jcs_status status; + json_writer out; /* canonical bytes so far (all final) */ + + /* -- private -- */ + jcs_events ev; + jcs_stack stack; + int state; /* jcs_state (json_canonical_stream.c) */ + json_writer token; /* pending number/literal text */ + json_writer decoded; /* decoded bytes of the string being read */ + bool str_is_key; + bool str_nul; /* a \u0000 ended the C string: drop the rest */ + int esc; /* escape sub-state */ + int hex_n; /* hex digits read in the current \u */ + int hex_val; + int high; /* pending high surrogate */ +} jcs_parser; + +/** Initialize. `ev` may be NULL. Returns false on allocation failure. */ +bool jcs_init(jcs_parser *p, const jcs_events *ev); + +/** + * Feed `n` bytes. Returns how many were consumed: all of them while RUNNING; + * fewer once the root value completes (the byte that completed it is consumed, + * the rest are not). A FAILED or DONE parser consumes nothing. + */ +size_t jcs_feed(jcs_parser *p, const char *bytes, size_t n); + +/** Release owned buffers. */ +void jcs_free(jcs_parser *p); + +#endif /* PN_SERVE_JSON_CANONICAL_STREAM_H */ diff --git a/src/serve/json_reader.c b/src/serve/json_reader.c new file mode 100644 index 0000000..c7bc0ff --- /dev/null +++ b/src/serve/json_reader.c @@ -0,0 +1,418 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_reader.c + * @brief Implementation of the minimal JSON DOM parser (see json_reader.h). + * + * @status Handles the OpenAI request grammar; UTF-16 surrogate pairs in \u + * escapes are decoded to UTF-8. + * @issues None + * @todo None + */ + +#include "json_reader.h" + +#include +#include +#include +#include +#include + +/* One object member: key + value. */ +typedef struct { + char *key; + jr_value *value; +} jr_member; + +struct jr_value { + jr_type type; + union { + bool boolean; + double number; + char *string; /* JR_STRING (owned) */ + struct { /* JR_ARRAY */ + jr_value **items; + int count; + } array; + struct { /* JR_OBJECT */ + jr_member *members; + int count; + } object; + } as; +}; + +/* Cursor over the source text. */ +typedef struct { + const char *p; + bool error; +} jr_cursor; + +static jr_value *parse_value(jr_cursor *cur); + +static void skip_whitespace(jr_cursor *cur) +{ + while (*cur->p == ' ' || *cur->p == '\t' || *cur->p == '\n' || *cur->p == '\r') + cur->p++; +} + +/* Append one Unicode code point as UTF-8 to buf[*len], growing via caller cap. */ +static void append_utf8(char *buf, size_t *len, uint32_t cp) +{ + if (cp <= 0x7F) { + buf[(*len)++] = (char)cp; + } else if (cp <= 0x7FF) { + buf[(*len)++] = (char)(0xC0 | (cp >> 6)); + buf[(*len)++] = (char)(0x80 | (cp & 0x3F)); + } else if (cp <= 0xFFFF) { + buf[(*len)++] = (char)(0xE0 | (cp >> 12)); + buf[(*len)++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[(*len)++] = (char)(0x80 | (cp & 0x3F)); + } else { + buf[(*len)++] = (char)(0xF0 | (cp >> 18)); + buf[(*len)++] = (char)(0x80 | ((cp >> 12) & 0x3F)); + buf[(*len)++] = (char)(0x80 | ((cp >> 6) & 0x3F)); + buf[(*len)++] = (char)(0x80 | (cp & 0x3F)); + } +} + +static int hex_nibble(char c) +{ + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +/* Read 4 hex digits at cur->p (which points just past the 'u'); -1 on error. */ +static int read_hex4(jr_cursor *cur) +{ + int v = 0; + for (int i = 0; i < 4; i++) { + int n = hex_nibble(cur->p[i]); + if (n < 0) return -1; + v = (v << 4) | n; + } + cur->p += 4; + return v; +} + +/* Parse a JSON string literal (cur->p at the opening quote). Returns a malloc'd + * unescaped NUL-terminated string, or NULL on error. */ +static char *parse_string_raw(jr_cursor *cur) +{ + cur->p++; /* opening quote */ + const char *start = cur->p; + size_t max = 1; + for (const char *s = start; *s && *s != '"'; s++) { + max += (*s == '\\') ? 4 : 1; /* worst case: \uXXXX -> up to 4 UTF-8 bytes */ + if (*s == '\\' && s[1]) s++; + } + char *out = malloc(max + 1); + if (!out) { cur->error = true; return NULL; } + + size_t len = 0; + while (*cur->p && *cur->p != '"') { + char c = *cur->p; + if (c != '\\') { + out[len++] = c; + cur->p++; + continue; + } + cur->p++; /* consume backslash */ + char e = *cur->p++; + switch (e) { + case '"': out[len++] = '"'; break; + case '\\': out[len++] = '\\'; break; + case '/': out[len++] = '/'; break; + case 'b': out[len++] = '\b'; break; + case 'f': out[len++] = '\f'; break; + case 'n': out[len++] = '\n'; break; + case 'r': out[len++] = '\r'; break; + case 't': out[len++] = '\t'; break; + case 'u': { + int cp = read_hex4(cur); + if (cp < 0) { free(out); cur->error = true; return NULL; } + if (cp >= 0xD800 && cp <= 0xDBFF && + cur->p[0] == '\\' && cur->p[1] == 'u') { + cur->p += 2; + int lo = read_hex4(cur); + if (lo < 0) { free(out); cur->error = true; return NULL; } + cp = 0x10000 + (((cp - 0xD800) << 10) | (lo - 0xDC00)); + } + append_utf8(out, &len, (uint32_t)cp); + break; + } + default: + free(out); + cur->error = true; + return NULL; + } + } + if (*cur->p != '"') { free(out); cur->error = true; return NULL; } + cur->p++; /* closing quote */ + out[len] = '\0'; + return out; +} + +static jr_value *make_value(jr_type type) +{ + jr_value *v = calloc(1, sizeof(*v)); + if (v) v->type = type; + return v; +} + +static jr_value *parse_string(jr_cursor *cur) +{ + char *s = parse_string_raw(cur); + if (!s) return NULL; + jr_value *v = make_value(JR_STRING); + if (!v) { free(s); cur->error = true; return NULL; } + v->as.string = s; + return v; +} + +static jr_value *parse_number(jr_cursor *cur) +{ + char *end = NULL; + double d = strtod(cur->p, &end); + if (end == cur->p) { cur->error = true; return NULL; } + cur->p = end; + jr_value *v = make_value(JR_NUMBER); + if (!v) { cur->error = true; return NULL; } + v->as.number = d; + return v; +} + +static jr_value *parse_literal(jr_cursor *cur, const char *word, jr_type type, + bool bool_value) +{ + size_t n = strlen(word); + if (strncmp(cur->p, word, n) != 0) { cur->error = true; return NULL; } + cur->p += n; + jr_value *v = make_value(type); + if (!v) { cur->error = true; return NULL; } + if (type == JR_BOOL) v->as.boolean = bool_value; + return v; +} + +static jr_value *parse_array(jr_cursor *cur) +{ + cur->p++; /* '[' */ + jr_value *v = make_value(JR_ARRAY); + if (!v) { cur->error = true; return NULL; } + skip_whitespace(cur); + if (*cur->p == ']') { cur->p++; return v; } + + int cap = 4; + v->as.array.items = malloc((size_t)cap * sizeof(jr_value *)); + if (!v->as.array.items) { jr_free(v); cur->error = true; return NULL; } + + while (1) { + jr_value *item = parse_value(cur); + if (cur->error) { jr_free(v); return NULL; } + if (v->as.array.count >= cap) { + cap *= 2; + jr_value **grown = realloc(v->as.array.items, + (size_t)cap * sizeof(jr_value *)); + if (!grown) { jr_free(item); jr_free(v); cur->error = true; return NULL; } + v->as.array.items = grown; + } + v->as.array.items[v->as.array.count++] = item; + skip_whitespace(cur); + if (*cur->p == ',') { cur->p++; skip_whitespace(cur); continue; } + if (*cur->p == ']') { cur->p++; break; } + jr_free(v); + cur->error = true; + return NULL; + } + return v; +} + +static jr_value *parse_object(jr_cursor *cur) +{ + cur->p++; /* '{' */ + jr_value *v = make_value(JR_OBJECT); + if (!v) { cur->error = true; return NULL; } + skip_whitespace(cur); + if (*cur->p == '}') { cur->p++; return v; } + + int cap = 4; + v->as.object.members = malloc((size_t)cap * sizeof(jr_member)); + if (!v->as.object.members) { jr_free(v); cur->error = true; return NULL; } + + while (1) { + skip_whitespace(cur); + if (*cur->p != '"') { jr_free(v); cur->error = true; return NULL; } + char *key = parse_string_raw(cur); + if (!key) { jr_free(v); return NULL; } + skip_whitespace(cur); + if (*cur->p != ':') { free(key); jr_free(v); cur->error = true; return NULL; } + cur->p++; + jr_value *val = parse_value(cur); + if (cur->error) { free(key); jr_free(v); return NULL; } + if (v->as.object.count >= cap) { + cap *= 2; + jr_member *grown = realloc(v->as.object.members, + (size_t)cap * sizeof(jr_member)); + if (!grown) { free(key); jr_free(val); jr_free(v); cur->error = true; return NULL; } + v->as.object.members = grown; + } + v->as.object.members[v->as.object.count].key = key; + v->as.object.members[v->as.object.count].value = val; + v->as.object.count++; + skip_whitespace(cur); + if (*cur->p == ',') { cur->p++; continue; } + if (*cur->p == '}') { cur->p++; break; } + jr_free(v); + cur->error = true; + return NULL; + } + return v; +} + +static jr_value *parse_value(jr_cursor *cur) +{ + skip_whitespace(cur); + switch (*cur->p) { + case '"': return parse_string(cur); + case '{': return parse_object(cur); + case '[': return parse_array(cur); + case 't': return parse_literal(cur, "true", JR_BOOL, true); + case 'f': return parse_literal(cur, "false", JR_BOOL, false); + case 'n': return parse_literal(cur, "null", JR_NULL, false); + default: return parse_number(cur); + } +} + +jr_value *jr_parse(const char *text) +{ + if (!text) return NULL; + jr_cursor cur = { .p = text, .error = false }; + jr_value *root = parse_value(&cur); + if (cur.error) { jr_free(root); return NULL; } + return root; +} + +void jr_free(jr_value *value) +{ + if (!value) return; + switch (value->type) { + case JR_STRING: + free(value->as.string); + break; + case JR_ARRAY: + for (int i = 0; i < value->as.array.count; i++) + jr_free(value->as.array.items[i]); + free(value->as.array.items); + break; + case JR_OBJECT: + for (int i = 0; i < value->as.object.count; i++) { + free(value->as.object.members[i].key); + jr_free(value->as.object.members[i].value); + } + free(value->as.object.members); + break; + default: + break; + } + free(value); +} + +jr_type jr_kind(const jr_value *value) +{ + return value ? value->type : JR_NULL; +} + +const jr_value *jr_object_get(const jr_value *value, const char *key) +{ + if (!value || value->type != JR_OBJECT || !key) return NULL; + for (int i = 0; i < value->as.object.count; i++) + if (strcmp(value->as.object.members[i].key, key) == 0) + return value->as.object.members[i].value; + return NULL; +} + +int jr_array_len(const jr_value *value) +{ + return (value && value->type == JR_ARRAY) ? value->as.array.count : 0; +} + +int jr_object_member_count(const jr_value *value) +{ + return (value && value->type == JR_OBJECT) ? value->as.object.count : 0; +} + +const char *jr_object_key_at(const jr_value *value, int index) +{ + if (!value || value->type != JR_OBJECT) return NULL; + if (index < 0 || index >= value->as.object.count) return NULL; + return value->as.object.members[index].key; +} + +const jr_value *jr_object_value_at(const jr_value *value, int index) +{ + if (!value || value->type != JR_OBJECT) return NULL; + if (index < 0 || index >= value->as.object.count) return NULL; + return value->as.object.members[index].value; +} + +const jr_value *jr_array_get(const jr_value *value, int index) +{ + if (!value || value->type != JR_ARRAY) return NULL; + if (index < 0 || index >= value->as.array.count) return NULL; + return value->as.array.items[index]; +} + +const char *jr_as_string(const jr_value *value) +{ + return (value && value->type == JR_STRING) ? value->as.string : NULL; +} + +double jr_as_number(const jr_value *value, double fallback) +{ + return (value && value->type == JR_NUMBER) ? value->as.number : fallback; +} + +bool jr_as_bool(const jr_value *value, bool fallback) +{ + return (value && value->type == JR_BOOL) ? value->as.boolean : fallback; +} + +/* Render a number without a trailing ".0" when it is integral, so a schema + * like {"minimum": 0} round-trips as 0 rather than 0.0. */ +static void jr_write_number(json_writer *jw, double n) +{ + if (isfinite(n) && n == floor(n) && fabs(n) < 1e15) + jw_printf(jw, "%lld", (long long)n); + else + jw_printf(jw, "%.17g", n); +} + +void jr_to_json(const jr_value *value, json_writer *jw) +{ + if (!value) { jw_raw(jw, "null"); return; } + switch (value->type) { + case JR_NULL: jw_raw(jw, "null"); break; + case JR_BOOL: jw_raw(jw, value->as.boolean ? "true" : "false"); break; + case JR_NUMBER: jr_write_number(jw, value->as.number); break; + case JR_STRING: jw_string(jw, value->as.string); break; + case JR_ARRAY: + jw_raw(jw, "["); + for (int i = 0; i < value->as.array.count; i++) { + if (i) jw_raw(jw, ","); + jr_to_json(value->as.array.items[i], jw); + } + jw_raw(jw, "]"); + break; + case JR_OBJECT: + jw_raw(jw, "{"); + for (int i = 0; i < value->as.object.count; i++) { + if (i) jw_raw(jw, ","); + jw_string(jw, value->as.object.members[i].key); + jw_raw(jw, ":"); + jr_to_json(value->as.object.members[i].value, jw); + } + jw_raw(jw, "}"); + break; + } +} diff --git a/src/serve/json_reader.h b/src/serve/json_reader.h new file mode 100644 index 0000000..e4ae105 --- /dev/null +++ b/src/serve/json_reader.h @@ -0,0 +1,94 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_reader.h + * @brief Minimal recursive-descent JSON parser to a small in-memory DOM. + * Just enough to read OpenAI chat-completion requests (objects, arrays, + * strings with escapes, numbers, booleans, null). No external deps. + * + * @status Parses the request shapes the serve layer needs; not a general-purpose + * validator (duplicate keys keep the first, trailing garbage ignored). + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_JSON_READER_H +#define PN_SERVE_JSON_READER_H + +#include +#include + +#include "json_writer.h" + +/* JSON value kinds. */ +typedef enum { + JR_NULL, + JR_BOOL, + JR_NUMBER, + JR_STRING, + JR_ARRAY, + JR_OBJECT, +} jr_type; + +/* Opaque JSON value node. */ +typedef struct jr_value jr_value; + +/** + * Parse a NUL-terminated JSON document into a DOM tree. + * + * @param text UTF-8 JSON document (not modified, not retained). + * @return Root value (free with jr_free), or NULL on parse error / OOM. + * + * Ownership: caller frees the returned tree via jr_free(). + * Thread-safe: yes (no shared state) + */ +jr_value *jr_parse(const char *text); + +/** Recursively free a value tree returned by jr_parse(). NULL-safe. */ +void jr_free(jr_value *value); + +/** Kind of a value. Returns JR_NULL for a NULL pointer. */ +jr_type jr_kind(const jr_value *value); + +/** + * Look up a key in an object. Returns the child value (borrowed, owned by the + * tree) or NULL if `value` is not an object or the key is absent. + */ +const jr_value *jr_object_get(const jr_value *value, const char *key); + +/** Number of elements in an array (0 if not an array). */ +int jr_array_len(const jr_value *value); + +/** Number of members in an object (0 if not an object). */ +int jr_object_member_count(const jr_value *value); + +/** Key of member `index` (borrowed), preserving source order, or NULL. */ +const char *jr_object_key_at(const jr_value *value, int index); + +/** Value of member `index` (borrowed), preserving source order, or NULL. */ +const jr_value *jr_object_value_at(const jr_value *value, int index); + +/** Element `index` of an array (borrowed), or NULL if out of range. */ +const jr_value *jr_array_get(const jr_value *value, int index); + +/** + * String payload of a JR_STRING value (unescaped, NUL-terminated, borrowed), + * or NULL if `value` is not a string. + */ +const char *jr_as_string(const jr_value *value); + +/** Numeric payload; returns `fallback` if `value` is not a number. */ +double jr_as_number(const jr_value *value, double fallback); + +/** Boolean payload; returns `fallback` if `value` is not a bool. */ +bool jr_as_bool(const jr_value *value, bool fallback); + +/** + * Serialize a value tree back to compact JSON, appending to `jw`. Integral + * numbers render without a decimal point (so a tool schema round-trips cleanly). + * This is the C equivalent of Jinja's `value | tojson`, used to render tool + * definitions into the prompt. + */ +void jr_to_json(const jr_value *value, json_writer *jw); + +#endif /* PN_SERVE_JSON_READER_H */ diff --git a/src/serve/json_value_grammar.c b/src/serve/json_value_grammar.c new file mode 100644 index 0000000..1523295 --- /dev/null +++ b/src/serve/json_value_grammar.c @@ -0,0 +1,312 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * Incremental JSON-value well-formedness walk. See json_value_grammar.h for + * the contract, the size argument, and the named duplication with + * response_format_grammar.c. + * + * Pure C, libc-only. No engine/CUDA deps → unit-tested standalone. + * + * ──────────────────────────────────────────────────────────────────────────── + * The one non-obvious mechanic: RECONSUME + * ──────────────────────────────────────────────────────────────────────────── + * Every JSON token is self-delimiting except numbers and the bare literals: `1` + * is finished only by the char AFTER it, and that char is itself structural + * ("1,", "1]", "1 "). So step_char reports `reconsume` when it ended a number + * or a literal without consuming the char, and the caller re-feeds it into the + * new phase. It can happen at most once per char — the reconsumed char is + * always structural and structural phases never reconsume — which is what the + * guard in jvg_step asserts rather than assumes. + */ + +#include "json_value_grammar.h" + +#include + +/* ── small helpers ── */ + +static bool is_ws(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +static bool is_digit(char c) { + return c >= '0' && c <= '9'; +} + +static bool is_hex(char c) { + return is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static void die(jvg_walk *w) { + w->phase = JVG_P_DEAD; +} + +/* The literal a `lit_which` names, so the walk can match it char by char + * without carrying the string itself in the (copied-per-candidate) state. */ +static const char *literal_text(unsigned char which) { + switch (which) { + case 1: return "true"; + case 2: return "false"; + case 3: return "null"; + default: return ""; + } +} + +/* Is the innermost open container an array? Undefined at depth 0 — every + * caller checks depth first. */ +static bool innermost_is_array(const jvg_walk *w) { + return (w->container_is_array >> (unsigned)(w->depth - 1)) & 1u; +} + +/* Open a container and sit the walk in its FIRST slot. The first slot is the + * only one where the matching close is legal, which is what makes `{}` / `[]` + * valid while `{"a":1,}` / `[1,]` are structurally unreachable. */ +static void open_container(jvg_walk *w, bool is_array) { + if (w->depth >= JVG_MAX_DEPTH) { die(w); return; } + if (is_array) w->container_is_array |= ((uint64_t)1 << w->depth); + else w->container_is_array &= ~((uint64_t)1 << w->depth); + w->depth++; + w->phase = is_array ? JVG_P_FIRST_ELEM : JVG_P_FIRST_KEY; +} + +/* Close the innermost container. Closing the LAST one completes the walk. */ +static void close_container(jvg_walk *w) { + if (w->depth == 0) { die(w); return; } + w->depth--; + w->phase = (w->depth == 0) ? JVG_P_COMPLETE : JVG_P_AFTER; +} + +/* A value just finished: the innermost container decides what may follow, and + * at depth 0 the whole walk is done. */ +static void value_finished(jvg_walk *w) { + w->phase = (w->depth == 0) ? JVG_P_COMPLETE : JVG_P_AFTER; +} + +/* Consume the first char of a value. Kills the walk when `c` cannot begin one. */ +static void begin_value(jvg_walk *w, char c) { + if (c == '{') { open_container(w, false); return; } + if (c == '[') { open_container(w, true); return; } + if (c == '"') { w->phase = JVG_P_STRING; w->str_is_key = false; return; } + if (c == '-' || is_digit(c)) { + w->phase = JVG_P_NUMBER; + w->num_can_end = is_digit(c); + return; + } + if (c == 't' || c == 'f' || c == 'n') { + w->lit_which = (c == 't') ? 1 : (c == 'f') ? 2 : 3; + w->lit_pos = 1; + w->phase = JVG_P_LITERAL; + return; + } + die(w); +} + +/* ── the char walk ── */ + +static void step_char(jvg_walk *w, char c, bool *reconsume) { + *reconsume = false; + + switch (w->phase) { + + case JVG_P_START: + if (is_ws(c)) return; + /* A schema-typed parameter names the container its value must be, so + * anything else — a bare string, a number, the prose the model writes + * when it forgets the format — is off-grammar at the very first char + * rather than at the tool. */ + if (w->required_container && c != (char)w->required_container) { + die(w); + return; + } + begin_value(w, c); + return; + + case JVG_P_VALUE_REQ: + if (is_ws(c)) return; + begin_value(w, c); + return; + + case JVG_P_FIRST_ELEM: + if (is_ws(c)) return; + if (c == ']' && w->depth > 0 && innermost_is_array(w)) { + close_container(w); /* "[]" */ + return; + } + begin_value(w, c); + return; + + case JVG_P_FIRST_KEY: + if (is_ws(c)) return; + if (c == '"') { w->phase = JVG_P_STRING; w->str_is_key = true; return; } + if (c == '}' && w->depth > 0 && !innermost_is_array(w)) { + close_container(w); /* "{}" */ + return; + } + die(w); + return; + + case JVG_P_KEY_REQ: + /* After a ',' inside an object the next thing MUST be a key. This is + * the state that makes the live failure unreachable: the model wrote + * `{"content":"",""}` — a second, bare STRING where a key/value pair + * was owed. The string parses as a key, and then the ':' this phase's + * successor requires never arrives, so '}' dies. */ + if (is_ws(c)) return; + if (c == '"') { w->phase = JVG_P_STRING; w->str_is_key = true; return; } + die(w); /* a trailing comma cannot be followed by '}' either */ + return; + + case JVG_P_COLON: + if (is_ws(c)) return; + if (c == ':') { w->phase = JVG_P_VALUE_REQ; return; } + die(w); + return; + + case JVG_P_STRING: + if (c == '\\') { w->phase = JVG_P_STR_ESC; return; } + if (c == '"') { + if (w->str_is_key) w->phase = JVG_P_COLON; + else value_finished(w); + return; + } + if ((unsigned char)c < 0x20) { die(w); return; } /* raw control char */ + return; + + case JVG_P_STR_ESC: + if (c == 'u') { w->phase = JVG_P_STR_UNI; w->uni_left = 4; return; } + if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || + c == 'n' || c == 'r' || c == 't') { + w->phase = JVG_P_STRING; + return; + } + die(w); + return; + + case JVG_P_STR_UNI: + if (!is_hex(c)) { die(w); return; } + if (--w->uni_left == 0) w->phase = JVG_P_STRING; + return; + + case JVG_P_NUMBER: + if (is_digit(c)) { w->num_can_end = true; return; } + if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') { + w->num_can_end = false; + return; + } + /* Anything else ends the number — but a bare "-" or a trailing + * '.'/'e'/'+'/'-' is not a number, so it may only end on a digit. */ + if (!w->num_can_end) { die(w); return; } + value_finished(w); + *reconsume = true; + return; + + case JVG_P_LITERAL: { + const char *lit = literal_text(w->lit_which); + if (lit[w->lit_pos] != c) { die(w); return; } + w->lit_pos++; + if (lit[w->lit_pos] == '\0') value_finished(w); + return; + } + + case JVG_P_AFTER: + if (is_ws(c)) return; + if (w->depth == 0) { die(w); return; } + if (c == ',') { + w->phase = innermost_is_array(w) ? JVG_P_VALUE_REQ : JVG_P_KEY_REQ; + return; + } + if (c == ']' && innermost_is_array(w)) { close_container(w); return; } + if (c == '}' && !innermost_is_array(w)) { close_container(w); return; } + die(w); + return; + + case JVG_P_COMPLETE: + /* Trailing whitespace is padding, not a second value. Tolerating it + * here is what lets a scalar at depth 0 be ENDED by the space after it + * ("3 ") rather than dying on its own terminator. */ + if (is_ws(c)) return; + die(w); + return; + + case JVG_P_DEAD: + default: + die(w); + return; + } +} + +/* ── public API ── */ + +void jvg_begin(jvg_walk *w, char required_container) { + if (!w) return; + memset(w, 0, sizeof(*w)); + w->phase = JVG_P_START; + w->required_container = + (required_container == '[' || required_container == '{') + ? (unsigned char)required_container : 0; +} + +void jvg_step(jvg_walk *w, char c) { + if (!w || w->phase == JVG_P_DEAD) return; + bool reconsume = true; + /* At most two passes — see the RECONSUME note at the top. The guard is the + * assertion, not an allowance: a third pass would be a bug in step_char. */ + for (int pass = 0; reconsume && w->phase != JVG_P_DEAD && pass < 2; pass++) + step_char(w, c, &reconsume); +} + +bool jvg_is_dead(const jvg_walk *w) { + return w && w->phase == JVG_P_DEAD; +} + +bool jvg_is_complete(const jvg_walk *w) { + return w && w->phase == JVG_P_COMPLETE; +} + +int jvg_depth(const jvg_walk *w) { + return w ? (int)w->depth : 0; +} + +const char *jvg_phase_name(const jvg_walk *w) { + if (!w) return "none"; + switch (w->phase) { + case JVG_P_START: return "start"; + case JVG_P_VALUE_REQ: return "value_req"; + case JVG_P_FIRST_ELEM: return "first_elem"; + case JVG_P_FIRST_KEY: return "first_key"; + case JVG_P_KEY_REQ: return "key_req"; + case JVG_P_COLON: return "colon"; + case JVG_P_STRING: return "string"; + case JVG_P_STR_ESC: return "str_esc"; + case JVG_P_STR_UNI: return "str_uni"; + case JVG_P_NUMBER: return "number"; + case JVG_P_LITERAL: return "literal"; + case JVG_P_AFTER: return "after"; + case JVG_P_COMPLETE: return "complete"; + case JVG_P_DEAD: return "dead"; + default: return "?"; + } +} + +bool jvg_text_is_json_value(const char *text, char required_container) { + if (!text) return false; + jvg_walk w; + jvg_begin(&w, required_container); + for (const char *p = text; *p; p++) { + jvg_step(&w, *p); + if (w.phase == JVG_P_DEAD) return false; + /* Past the closing brace/bracket only whitespace may follow — a second + * value, or any trailing junk, is not "one well-formed value". */ + if (w.phase == JVG_P_COMPLETE) { + for (const char *q = p + 1; *q; q++) + if (!is_ws(*q)) return false; + return true; + } + } + /* END OF TEXT. A top-level NUMBER is the one value with no terminator of + * its own — "3" is finished only by the char after it, and here there is no + * char after it. Running out of text IS that boundary, so settle it. Every + * other phase means the value is genuinely unfinished, and a number still + * inside a container is unfinished too (its ']' never arrived). */ + return w.phase == JVG_P_NUMBER && w.num_can_end && w.depth == 0; +} diff --git a/src/serve/json_value_grammar.h b/src/serve/json_value_grammar.h new file mode 100644 index 0000000..7f4982f --- /dev/null +++ b/src/serve/json_value_grammar.h @@ -0,0 +1,153 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_value_grammar.h + * @brief Char-level, incremental "is this text still a valid JSON VALUE prefix?" + * walk, small enough to embed BY VALUE inside another grammar's state. + * + * WHAT THIS IS FOR. The tool grammars constrain a tool call's FRAME — the + * `` / `` skeleton, the NAME and KEY sets, the + * `required` mask, a string `enum`. Inside `…` the + * bytes were free text, so a parameter whose schema says `"type":"array"` or + * `"type":"object"` — whose value the parser SPLICES RAW into the call's + * `arguments` JSON (tool_calling.c write_typed_value) — was completely + * unconstrained. A model that writes one malformed nested object there does not + * merely get that argument wrong: the raw splice makes the WHOLE `arguments` + * string un-parseable, so every other argument of the call is lost with it. + * Live, full-weight qwen36-bf16, grammar armed and masking the frame correctly: + * three consecutive `todowrite` calls died on + * + * [{"content":"…","status":"in_progress"},{"content":"",""}] + * ^ a bare value, + * no key + * + * This walk is what makes those bytes off-grammar. + * + * WHY A SEPARATE, EMBEDDABLE MODULE. The constrained sampler asks + * "does appending this token's text keep a valid prefix?" once per vocabulary + * entry per step (~248k times), and it answers by COPYING the walk state onto + * the stack and walking the candidate on the copy. So the state must be + * (a) a POD with no pointers to own and (b) small. This one is: the container + * nesting stack is ONE BIT per open level in a `uint64_t` rather than a byte + * array, which is the whole reason it costs 16 bytes instead of 80 and can be + * embedded in `tg_state` without measurably changing the per-step mask cost. + * + * NOT A PARSER. It answers only "could this still become a valid JSON value", + * never "what value is it" — there is no tree, no allocation, and it never sees + * the whole text at once. Use json_reader.h (`jr_parse`) when the bytes are all + * in hand and the answer needs to be a value; use this when the bytes arrive one + * token at a time and the answer has to come BEFORE the next token is drawn. + * + * WHAT IS VALIDATED. Objects, arrays, strings (with `\\` escapes, `\\uXXXX`, and + * raw control characters rejected), numbers (at least one digit; a number may not + * end on `.`, `e`, `+` or `-`), the literals true/false/null, and whitespace + * between tokens. Trailing commas are structurally unreachable, and so is a + * bare value in an object slot — the two shapes the live model actually emitted. + * Nesting deeper than JVG_MAX_DEPTH is simply not an allowed continuation. + * + * WHAT IS NOT. Nothing about the SCHEMA: `items`, nested `properties`, + * `required`, per-member `type` and `enum` are all still dropped. A well-formed + * `[{"wrong_key":1}]` is accepted here and rejected later by the tool. See the + * spec note in tool_grammar.h for why well-formedness was taken first. + * + * @note DUPLICATION, NAMED. response_format_grammar.c carries its own copy of + * this FSM (it predates this file) with a top-level-object rule and a + * reasoning grace wrapped around it. This module was written to be that + * core — same phases, same `reconsume` protocol — so the migration is + * mechanical, but it is NOT done here: `response_format` is a live feature + * and refactoring it inside a tool-grammar fix would widen the blast + * radius for no functional gain. Follow-up, not yet filed: migrate + * response_format_grammar.c onto this walk and delete its copy. + * + * Pure C, libc-only — no engine/CUDA deps, unit-tested standalone + * (tests/test_json_value_grammar.c). + */ + +#ifndef PN_JSON_VALUE_GRAMMAR_H +#define PN_JSON_VALUE_GRAMMAR_H + +#include +#include + +/* Deepest container nesting the walk will allow to be opened. One BIT of + * `container_is_array` per level, so this is exactly the width of that word. */ +#define JVG_MAX_DEPTH 64 + +/* Walk phases. Exposed only so `jvg_walk` can be a POD the embedder copies by + * value — treat the enum as opaque and use the predicates below. */ +typedef enum { + JVG_P_START = 0, /* whitespace, then the value begins */ + JVG_P_VALUE_REQ, /* a value is REQUIRED here (after ':' or ',') */ + JVG_P_FIRST_ELEM, /* an array's first slot: a value, or ']' ("[]") */ + JVG_P_FIRST_KEY, /* an object's first slot: a key, or '}' ("{}") */ + JVG_P_KEY_REQ, /* a key is REQUIRED here (after ',') */ + JVG_P_COLON, /* expect ':' after a key (whitespace ok) */ + JVG_P_STRING, /* inside a "…" */ + JVG_P_STR_ESC, /* the previous char was a backslash */ + JVG_P_STR_UNI, /* consuming the 4 hex digits of a \uXXXX escape */ + JVG_P_NUMBER, /* inside a numeric literal */ + JVG_P_LITERAL, /* inside true / false / null */ + JVG_P_AFTER, /* after a value: ',' or this container's close */ + JVG_P_COMPLETE, /* the TOP-LEVEL value closed — nothing more is legal */ + JVG_P_DEAD, /* off-grammar */ +} jvg_phase; + +/** + * A walk position. A POD by construction: no pointers, no owned memory, safe to + * copy with `=` and to throw the copy away (which is exactly what the mask's + * per-candidate check does). 16 bytes. + */ +typedef struct { + uint64_t container_is_array; /* bit i: level i is an array, else object */ + unsigned char phase; /* jvg_phase */ + unsigned char depth; /* open containers (0 before the first) */ + unsigned char uni_left; /* \uXXXX hex digits still required */ + unsigned char lit_pos; /* chars of true/false/null matched */ + unsigned char lit_which; /* 0 none, 1 true, 2 false, 3 null */ + bool str_is_key; /* the string being read is an object key */ + bool num_can_end; /* the number so far may legally end here */ + unsigned char required_container; /* '[' , '{' , or 0 = any JSON value */ +} jvg_walk; + +/** + * Start a fresh walk. + * + * @param required_container '[' to require the value be an ARRAY, '{' to + * require an OBJECT, 0 to accept any JSON value. + * The schema-typed callers pass the container their + * `"type"` names, which is what lets the consumer + * splice the bytes in raw and know they will parse. + * Leading whitespace is allowed either way. + */ +void jvg_begin(jvg_walk *w, char required_container); + +/** Consume one char, moving the walk (to JVG_P_DEAD on any violation). */ +void jvg_step(jvg_walk *w, char c); + +/** Has the walk left the grammar? Nothing can revive it. */ +bool jvg_is_dead(const jvg_walk *w); + +/** + * Has a COMPLETE top-level JSON value been emitted? True the instant the value + * closes; from that point every further char is off-grammar, so the embedder + * takes over (a tool grammar goes looking for its ``). + */ +bool jvg_is_complete(const jvg_walk *w); + +/** Open container nesting depth (0 before the first '{'/'[' and after the last). */ +int jvg_depth(const jvg_walk *w); + +/** Phase name, for diagnostics only ("string", "after", "dead", …). */ +const char *jvg_phase_name(const jvg_walk *w); + +/** + * Whole-text convenience: is `text` (NUL-terminated) exactly one well-formed + * JSON value, with nothing but whitespace after it? + * + * For callers that hold all the bytes and want the same verdict the incremental + * walk would give — so the decoding constraint and any post-parse re-check + * cannot disagree about what "well-formed" means. + */ +bool jvg_text_is_json_value(const char *text, char required_container); + +#endif /* PN_JSON_VALUE_GRAMMAR_H */ diff --git a/src/serve/json_writer.c b/src/serve/json_writer.c new file mode 100644 index 0000000..42b5956 --- /dev/null +++ b/src/serve/json_writer.c @@ -0,0 +1,114 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_writer.c + * @brief Implementation of the growable JSON string builder (see json_writer.h). + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "json_writer.h" + +#include +#include +#include +#include + +/* Ensure at least `extra` more bytes (plus the trailing NUL) fit. */ +static bool jw_reserve(json_writer *jw, size_t extra) +{ + if (jw->oom) return false; + size_t need = jw->len + extra + 1; + if (need <= jw->cap) return true; + size_t cap = jw->cap ? jw->cap : 256; + while (cap < need) cap *= 2; + char *grown = realloc(jw->data, cap); + if (!grown) { jw->oom = true; return false; } + jw->data = grown; + jw->cap = cap; + return true; +} + +bool jw_init(json_writer *jw) +{ + jw->data = malloc(256); + if (!jw->data) { jw->oom = true; jw->len = jw->cap = 0; return false; } + jw->data[0] = '\0'; + jw->len = 0; + jw->cap = 256; + jw->oom = false; + return true; +} + +void jw_free(json_writer *jw) +{ + if (!jw) return; + free(jw->data); + jw->data = NULL; + jw->len = jw->cap = 0; + jw->oom = false; +} + +void jw_raw_n(json_writer *jw, const char *bytes, size_t len) +{ + if (!jw_reserve(jw, len)) return; + memcpy(jw->data + jw->len, bytes, len); + jw->len += len; + jw->data[jw->len] = '\0'; +} + +void jw_raw(json_writer *jw, const char *text) +{ + if (text) jw_raw_n(jw, text, strlen(text)); +} + +void jw_printf(json_writer *jw, const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + va_list ap2; + va_copy(ap2, ap); + int n = vsnprintf(NULL, 0, fmt, ap); + va_end(ap); + if (n < 0 || !jw_reserve(jw, (size_t)n)) { va_end(ap2); return; } + vsnprintf(jw->data + jw->len, (size_t)n + 1, fmt, ap2); + va_end(ap2); + jw->len += (size_t)n; +} + +void jw_escaped_n(json_writer *jw, const char *text, size_t len) +{ + for (size_t i = 0; i < len; i++) { + unsigned char c = (unsigned char)text[i]; + switch (c) { + case '"': jw_raw_n(jw, "\\\"", 2); break; + case '\\': jw_raw_n(jw, "\\\\", 2); break; + case '\b': jw_raw_n(jw, "\\b", 2); break; + case '\f': jw_raw_n(jw, "\\f", 2); break; + case '\n': jw_raw_n(jw, "\\n", 2); break; + case '\r': jw_raw_n(jw, "\\r", 2); break; + case '\t': jw_raw_n(jw, "\\t", 2); break; + default: + if (c < 0x20) { + jw_printf(jw, "\\u%04x", c); + } else { + jw_raw_n(jw, (const char *)&c, 1); + } + break; + } + } +} + +void jw_string_n(json_writer *jw, const char *text, size_t len) +{ + jw_raw_n(jw, "\"", 1); + jw_escaped_n(jw, text, len); + jw_raw_n(jw, "\"", 1); +} + +void jw_string(json_writer *jw, const char *text) +{ + jw_string_n(jw, text ? text : "", text ? strlen(text) : 0); +} diff --git a/src/serve/json_writer.h b/src/serve/json_writer.h new file mode 100644 index 0000000..8547907 --- /dev/null +++ b/src/serve/json_writer.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file json_writer.h + * @brief Growable string builder with JSON string escaping. Used to shape + * OpenAI response bodies and SSE chunks without a JSON library. + * + * @status Complete for the serve layer's output needs. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_JSON_WRITER_H +#define PN_SERVE_JSON_WRITER_H + +#include +#include + +/* Growable byte buffer. Zero-initialize with jw_init(). */ +typedef struct { + char *data; /* NUL-terminated buffer (owned) */ + size_t len; /* bytes used, excluding the NUL */ + size_t cap; /* allocated capacity */ + bool oom; /* set if any append failed (allocation) */ +} json_writer; + +/** Initialize an empty writer. Returns false on allocation failure. */ +bool jw_init(json_writer *jw); + +/** Release the writer's buffer. NULL-safe; leaves the struct zeroed. */ +void jw_free(json_writer *jw); + +/** Append a raw (already-escaped / structural) NUL-terminated fragment. */ +void jw_raw(json_writer *jw, const char *text); + +/** Append `len` raw bytes. */ +void jw_raw_n(json_writer *jw, const char *bytes, size_t len); + +/** Append a formatted fragment (printf-style). */ +void jw_printf(json_writer *jw, const char *fmt, ...) + __attribute__((format(printf, 2, 3))); + +/** + * Append a JSON string literal: a leading '"', the escaped contents of `text`, + * and a trailing '"'. Escapes control chars, quotes and backslashes per RFC + * 8259. `text` may be NULL (emits an empty string literal). + */ +void jw_string(json_writer *jw, const char *text); + +/** + * Append the ESCAPED contents of `len` bytes with no surrounding quotes — the + * body jw_string_n writes between its two '"'. Escaping is per byte, so a + * string written in pieces produces the same bytes as one jw_string_n call. + */ +void jw_escaped_n(json_writer *jw, const char *text, size_t len); + +/** As jw_string but for a bounded (not necessarily NUL-terminated) buffer. */ +void jw_string_n(json_writer *jw, const char *text, size_t len); + +#endif /* PN_SERVE_JSON_WRITER_H */ diff --git a/src/serve/model_admin.c b/src/serve/model_admin.c new file mode 100644 index 0000000..6258b9d --- /dev/null +++ b/src/serve/model_admin.c @@ -0,0 +1,419 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file model_admin.c + * @brief Implementation of the runtime load/unload admin endpoints (see + * model_admin.h). Thin JSON shims over model_registry + die_manager — + * all policy (duplicates, drain, placement, VRAM admission) lives + * there. + * + * @status Live on the CPU host; die numbers become real on the K80 test machine. + * @issues Registry refusals surface as one reason string; the HTTP status is + * picked by matching it (see status_for_reason) — coarse but enough + * for an operator surface. + * @todo None + */ + +#include "model_admin.h" + +#include "attn_residency_json.h" +#include "graph_residency_json.h" + +#include +#include +#include + +#include "../accel/die_manager.h" +#include "../core/prefix_cache.h" +#include "../core/session_snapshot.h" +#include "base64.h" +#include "http_server.h" +#include "json_reader.h" +#include "json_writer.h" +#include "model_session.h" +#include "serve_registry.h" + +/* Map a registry refusal reason to an HTTP status: duplicates conflict (409), + * capacity refusals are 507 (insufficient storage), a missing file is 404, + * a `dies` array this binary cannot honour is the CALLER's mistake (400), + * anything else a 500. */ +static int status_for_reason(const char *reason) +{ + /* serve_backend_guard's short reasons. 400, not 500: nothing is broken — + * the request named accelerator dies on a process that has no accelerator + * backend compiled in, or none visible. Retrying is pointless; the fix is a + * different binary or an omitted `dies`. */ + if (strstr(reason, "no accelerator")) + return 400; + /* 409, not 500: a busy model is a CONFLICTING STATE, not a broken server. + * An unload that could not drain refuses rather than reporting success and + * stranding the dies, and the operator's move is to retry — same family as + * "already unloading". */ + if (strstr(reason, "already loaded") || strstr(reason, "already unloading") || + strstr(reason, "model is busy")) + return 409; + if (strstr(reason, "admission") || strstr(reason, "binding") || + strstr(reason, "table full")) + return 507; + if (strstr(reason, "not found") || strstr(reason, "no such")) + return 404; + return 500; +} + +/* Write one model row {"id":..,"object":"model","dies":[..],"vram_bytes":..}. + * + * The admin surface carries BOTH context numbers because an operator's question + * is different from a client's: `context_window` is what sessions actually get + * right now, `context_window_configured` is what --ctx-cap asked for. They are + * equal unless the memory ladder is clamping this instant, and seeing them + * differ is the whole point — that is the signal that pressure is costing + * context. (/v1/models advertises only the effective one; a client has no use + * for a window it cannot have.) */ +static void write_model_row(json_writer *jw, const mr_info *m) +{ + jw_raw(jw, "{\"id\":"); + jw_string(jw, m->name); + jw_raw(jw, ",\"object\":\"model\",\"path\":"); + jw_string(jw, m->path); + jw_raw(jw, ",\"dies\":["); + for (int i = 0; i < m->n_dies; i++) + jw_printf(jw, "%s%d", i ? "," : "", m->dies[i]); + /* Both concurrency numbers, for exactly the reason the row already carries + * both context numbers: `max_concurrent` is what this model SERVES right now + * (bs_get_load's capacity), `max_concurrent_configured` is what + * --max-concurrent asked for. They differ whenever there is no scheduler at + * all (no CUDA compiled in, QWEN35_BATCH_SLOTS off, bs_create failed) or the + * memory-pressure ladder clamped the slot count — and seeing them differ IS + * the signal. The incident serve reported max_concurrent 2 with a NULL + * scheduler and an actual served concurrency of 1. */ + jw_printf(jw, "],\"vram_bytes\":%llu,\"max_sessions\":%d," + "\"max_concurrent\":%d,\"max_concurrent_configured\":%d", + (unsigned long long)m->vram_bytes, m->max_sessions, + m->served_concurrency, m->max_concurrent); + if (m->context_window > 0) + jw_printf(jw, ",\"context_window\":%d", m->context_window); + if (m->context_window_configured > 0) + jw_printf(jw, ",\"context_window_configured\":%d", + m->context_window_configured); + /* The operator gets the per-class breakdown and the lifetime counters: a + * model at live 38/40 with capture_failures 2 lost two layers' graphs to + * something transient, which is a completely different story from a model + * reporting 40 ineligible layers because its experts are not Q3_K. */ + gr_write_detail(jw, &m->graphs); + /* And which attention path each layer is actually on. `host` > 0 is the + * fault; `reasons` names it, and `gave_up` says the layer has stopped + * trying to get back on-die at this geometry. */ + ar_write_detail(jw, &m->attn); + jw_raw(jw, "}"); +} + +void ma_handle_load(int client_fd, mr_registry *reg, const char *body) +{ + if (!body) { + hs_send_error(client_fd, 400, "invalid_request_error", "empty request body"); + return; + } + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { + hs_send_error(client_fd, 400, "invalid_request_error", "malformed JSON"); + jr_free(root); + return; + } + const char *path = jr_as_string(jr_object_get(root, "path")); + const char *name = jr_as_string(jr_object_get(root, "name")); + if (!path || !*path) { + hs_send_error(client_fd, 400, "invalid_request_error", "'path' is required"); + jr_free(root); + return; + } + + int dies[PN_MODEL_MAX_DIES]; + int n_dies = 0; + const jr_value *jd = jr_object_get(root, "dies"); + if (jr_kind(jd) == JR_ARRAY) { + int len = jr_array_len(jd); + if (len > PN_MODEL_MAX_DIES) { + hs_send_error(client_fd, 400, "invalid_request_error", "too many dies"); + jr_free(root); + return; + } + for (int i = 0; i < len; i++) + dies[n_dies++] = (int)jr_as_number(jr_array_get(jd, i), -1); + } + + pn_load_opts opts = { + .quantize = jr_as_bool(jr_object_get(root, "quantize"), false), + }; + + /* Per-model context cap (KV Phase 1): the KV ring is sized to this window at + * load, so it must be in the load opts. Absent/<=0 = engine default + * (QWEN35_MAX_SEQ / PN_SESSION_MAX_CTX). This is how ornith gets a 40K + * window without moving saber's. */ + int ctx_cap = (int)jr_as_number(jr_object_get(root, "ctx_cap"), 0); + if (ctx_cap < 0) ctx_cap = 0; + opts.ctx_cap = ctx_cap; + + /* Per-model AUTO-path reasoning cap (tokens); absent/<=0 = disabled. */ + int reasoning_budget = (int)jr_as_number( + jr_object_get(root, "reasoning_budget"), 0); + if (reasoning_budget < 0) reasoning_budget = 0; + + /* Per-model live-session (RESIDENCY) cap; absent/<=0 = the engine default + * (BS_MAX_USERS). mr_load clamps into [1, BS_MAX_USERS]. */ + int max_sessions = (int)jr_as_number( + jr_object_get(root, "max_sessions"), 0); + if (max_sessions < 0) max_sessions = 0; + + /* Per-model CONCURRENCY cap = the batch scheduler's decode slots; + * absent/<=0 = max_sessions. mr_load clamps into [1, BS_MAX_USERS]. */ + int max_concurrent = (int)jr_as_number( + jr_object_get(root, "max_concurrent"), 0); + if (max_concurrent < 0) max_concurrent = 0; + + char reason[160] = ""; + if (mr_load(reg, path, name, &opts, n_dies ? dies : NULL, n_dies, + reasoning_budget, max_sessions, max_concurrent, + reason, sizeof(reason)) != 0) { + hs_send_error(client_fd, status_for_reason(reason), "load_failed", reason); + jr_free(root); + return; + } + + /* Answer with the freshly-registered row (resolve the served name — it + * may have been derived from the basename). */ + mr_info info[MR_MAX_MODELS]; + int n = mr_snapshot(reg, info, MR_MAX_MODELS); + const mr_info *loaded = NULL; + for (int i = 0; i < n; i++) + if (strcmp(info[i].path, path) == 0 && + (!name || strcmp(info[i].name, name) == 0)) + loaded = &info[i]; + + /* Register the newly-loaded model with the dispatch router so it becomes + * routable — startup registration only covers the launch model, so an + * /admin-loaded model would otherwise be invisible to the router (503 on + * dispatch). Uses the resolved served name (may be derived from the path). */ + { + const char *reg_name = loaded ? loaded->name : name; + if (reg_name && *reg_name) + serve_registry_add(reg_name); + } + + json_writer jw; + if (!jw_init(&jw)) { hs_send_error(client_fd, 500, "server_error", "oom"); jr_free(root); return; } + if (loaded) { + write_model_row(&jw, loaded); + hs_send_json(client_fd, 200, jw.data); + } else { + hs_send_json(client_fd, 200, "{\"status\":\"loaded\"}"); + } + jw_free(&jw); + jr_free(root); +} + +void ma_handle_unload(int client_fd, mr_registry *reg, const char *body) +{ + if (!body) { + hs_send_error(client_fd, 400, "invalid_request_error", "empty request body"); + return; + } + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { + hs_send_error(client_fd, 400, "invalid_request_error", "malformed JSON"); + jr_free(root); + return; + } + const char *name = jr_as_string(jr_object_get(root, "name")); + if (!name || !*name) { + hs_send_error(client_fd, 400, "invalid_request_error", "'name' is required"); + jr_free(root); + return; + } + + char reason[160] = ""; + if (mr_unload(reg, name, reason, sizeof(reason)) != 0) { + hs_send_error(client_fd, status_for_reason(reason), "unload_failed", reason); + jr_free(root); + return; + } + + /* Deregister from the router (only if no replica of this name remains). */ + serve_registry_remove(name); + + json_writer jw; + if (!jw_init(&jw)) { hs_send_error(client_fd, 500, "server_error", "oom"); jr_free(root); return; } + jw_raw(&jw, "{\"unloaded\":"); + jw_string(&jw, name); + jw_raw(&jw, "}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); + jr_free(root); +} + +/* Serialize the exported snapshot to a blob, base64 it, and answer with the + * session-export JSON. Owns `snap` (frees it). Returns after answering. */ +static void session_export_reply(int client_fd, pn_arch arch, void *snap, + uint64_t weights_version, const char *sid, + int n_tok, uint32_t *tokens) +{ + unsigned char *blob = NULL; + size_t blob_len = 0; + if (pn_session_snapshot_serialize(arch, snap, weights_version, &blob, + &blob_len) != 0) { + pn_session_snapshot_free(arch, snap); + free(tokens); + hs_send_error(client_fd, 500, "server_error", "snapshot serialize failed"); + return; + } + pn_session_snapshot_free(arch, snap); + + char *b64 = pn_base64_encode(blob, blob_len); + free(blob); + if (!b64) { + free(tokens); + hs_send_error(client_fd, 500, "server_error", "oom"); + return; + } + + json_writer jw; + if (!jw_init(&jw)) { free(b64); free(tokens); hs_send_error(client_fd, 500, "server_error", "oom"); return; } + jw_raw(&jw, "{\"session_id\":"); + jw_string(&jw, sid); + jw_printf(&jw, ",\"weights_version\":%llu,\"n_tok\":%d,\"blob_bytes\":%zu" + ",\"blob_b64\":", (unsigned long long)weights_version, n_tok, blob_len); + jw_string(&jw, b64); + /* Committed token ids the blob covers. The client MUST send these back on + * /session/restore (len == the snapshot's pos), so export has to provide + * them — a blob-only client has no other way to get them. */ + jw_raw(&jw, ",\"tokens\":["); + for (int i = 0; i < n_tok; i++) { + if (i) jw_raw(&jw, ","); + jw_printf(&jw, "%u", tokens ? tokens[i] : 0u); + } + jw_raw(&jw, "]}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); + free(b64); + free(tokens); +} + +void ma_handle_session_export(int client_fd, mr_registry *reg, const char *body) +{ + if (!body) { + hs_send_error(client_fd, 400, "invalid_request_error", "empty request body"); + return; + } + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { + hs_send_error(client_fd, 400, "invalid_request_error", "malformed JSON"); + jr_free(root); + return; + } + const char *name = jr_as_string(jr_object_get(root, "name")); + const char *sid = jr_as_string(jr_object_get(root, "session_id")); + if (!sid || !*sid) { + hs_send_error(client_fd, 400, "invalid_request_error", "'session_id' is required"); + jr_free(root); + return; + } + + mr_entry *entry = mr_acquire(reg, name); + if (!entry) { + hs_send_error(client_fd, 404, "model_not_found", + "the requested model is not loaded (see GET /v1/models)"); + jr_free(root); + return; + } + /* An architecture with no snapshot family is a REFUSAL WITH ITS OWN REASON, + * not a lookup miss. Before this existed the only predicate in the path was + * "is this qwen35moe?", so a live, generating session on any other arch — + * every model the Tenstorrent backend serves, and every dense Llama/Qwen on + * CPU or CUDA — came back 404 session_not_found. The operator was told the + * session was gone; it was sitting in the prefix cache resuming turns. */ + pn_arch arch = entry->model->config.arch; + if (!pn_session_snapshot_supported(arch)) { + mr_release(reg, entry); + hs_send_error(client_fd, 400, "session_export_unsupported", + "this model's architecture has no session snapshot " + "(its per-session state cannot be serialized); " + "replay the conversation instead"); + jr_free(root); + return; + } + uint64_t weights_version = mr_weights_version(entry); + int n_tok = 0; + uint32_t *tokens = NULL; + void *snap = NULL; + + /* Route through the session facade. A BATCHED model's session lives in the + * scheduler's warm decode slot, NOT in the prefix cache (which holds only + * admission bookkeeping on that path) — looking it up in the prefix cache, + * what this used to do, made export answer session_not_found for every + * session on every batched model, i.e. everything the K80 test machine actually runs. + * The facade materializes the KV rows from the GPU decode-slot ring on that + * path (Phase 10b / INV3) and takes the model's session lock either way, so + * an in-flight turn cannot mutate the state mid-export. */ + ms_result msr = ms_session_export(entry, sid, &snap, &tokens, &n_tok); + if (msr != MS_OK) { + mr_release(reg, entry); + if (msr == MS_NO_CACHE) + hs_send_error(client_fd, 400, "session_caching_disabled", + "this model has no session cache to export from"); + else if (msr == MS_BUSY) + hs_send_error(client_fd, 429, "session_busy", + "a turn is in flight on that session; " + "export again once it completes"); + else if (msr == MS_NOT_FOUND) + hs_send_error(client_fd, 404, "session_not_found", + "no live session with that id (or nothing to export)"); + else + hs_send_error(client_fd, 500, "server_error", + "session export failed"); + jr_free(root); + return; + } + + session_export_reply(client_fd, arch, snap, weights_version, sid, n_tok, + tokens); + mr_release(reg, entry); + jr_free(root); +} + +void ma_handle_models(int client_fd, mr_registry *reg) +{ + mr_info info[MR_MAX_MODELS]; + int n = mr_snapshot(reg, info, MR_MAX_MODELS); + + json_writer jw; + if (!jw_init(&jw)) { hs_send_error(client_fd, 500, "server_error", "oom"); return; } + jw_raw(&jw, "{\"object\":\"list\",\"data\":["); + for (int i = 0; i < n; i++) { + if (i) jw_raw(&jw, ","); + write_model_row(&jw, &info[i]); + } + jw_raw(&jw, "]}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); +} + +void ma_handle_dies(int client_fd) +{ + json_writer jw; + if (!jw_init(&jw)) { hs_send_error(client_fd, 500, "server_error", "oom"); return; } + int n = pn_dm_count(); + jw_printf(&jw, "{\"object\":\"list\",\"n_dies\":%d,\"data\":[", n); + for (int i = 0; i < n; i++) { + pn_dm_die d; + if (pn_dm_get(i, &d) != 0) + continue; + if (i) jw_raw(&jw, ","); + jw_printf(&jw, "{\"die\":%d,\"card\":%d,\"model\":", i, pn_dm_card_of(i)); + jw_string(&jw, d.model_name); + jw_printf(&jw, ",\"n_models\":%d,\"vram_budget\":%llu,\"vram_used\":%llu}", + d.n_models, (unsigned long long)d.vram_budget, + (unsigned long long)d.vram_used); + } + jw_raw(&jw, "]}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); +} diff --git a/src/serve/model_admin.h b/src/serve/model_admin.h new file mode 100644 index 0000000..2d5c1c5 --- /dev/null +++ b/src/serve/model_admin.h @@ -0,0 +1,77 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file model_admin.h + * @brief Runtime load/unload admin endpoints for multi-model serving: + * models add/drop without restarting serve, so + * GET /v1/models is a live list. + * + * Routes (wired in serve_command.c): + * POST /admin/models/load {"path":"...", "name":"...", + * "dies":[0,1], "quantize":false} + * POST /admin/models/unload {"name":"..."} + * GET /admin/dies die-manager occupancy snapshot + * + * These share the serve port, so keep that port private (localhost or + * a private network) and put a gateway in front that forwards only + * /v1. Loads run on the caller's connection thread — a big model load + * takes a while and the client waits; serving of resident models + * continues meanwhile. + * + * @status Live on the CPU host; real die placement numbers are gated on GPU hardware. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_MODEL_ADMIN_H +#define PN_SERVE_MODEL_ADMIN_H + +#include "model_registry.h" + +/** POST /admin/models/load — load + register a model at runtime. */ +void ma_handle_load(int client_fd, mr_registry *reg, const char *body); + +/** POST /admin/models/unload — drain and unload a resident model. */ +void ma_handle_unload(int client_fd, mr_registry *reg, const char *body); + +/** GET /admin/dies — die-manager occupancy snapshot (empty list on CPU). */ +/** + * GET /admin/models — every LIVE instance, one row each (NOT deduped by name, + * unlike /v1/models: two instances of the same model on different dies are two + * rows, because an operator's question is per-instance). + * + * The row is the same one /admin/models/load returns, so until now its + * operator-only fields — context_window_configured, and the CUDA-graph + * residency `graphs` — could be read only as a side effect of LOADING a model. + * That is why task #20 had to infer the memory ladder's runtime clamp instead + * of reading it. This makes the row readable at any time. + * + * Thread-safe: yes (mr_snapshot takes the registry lock) + * Reentrant: no (writes to a client fd) + * Signal-safe: no + */ +void ma_handle_models(int client_fd, mr_registry *reg); + +void ma_handle_dies(int client_fd); + +/** + * POST /admin/session/export {"name":"...","session_id":"..."} — session + * warm-tier export (K80 serving-ops Phase 7). Snapshot + serialize the named + * model's live session state for `session_id` into a portable blob and return + * it base64 in JSON alongside its weights_version tag and the committed token + * ids the blob covers, so the CLIENT can store it and later POST + * /session/restore to warm-restart the session. Read-only w.r.t. the session. + * + * Covers BOTH session models via the model_session.h facade — the batched + * scheduler's warm decode slot as well as the single-user prefix cache. Since + * the host KV mirror was removed (Phase 10b / INV3) this is the only path by + * which a session's state can leave the GPU, so it materializes the KV rows + * from the decode-slot ring on demand rather than reading any mirror. + * + * 404 if the model or session is unknown (including "the session's rows are no + * longer in any GPU ring" — the client falls back to message replay); 429 if a + * turn is in flight on that session; 400 if the model has no session store. + */ +void ma_handle_session_export(int client_fd, mr_registry *reg, const char *body); + +#endif /* PN_SERVE_MODEL_ADMIN_H */ diff --git a/src/serve/model_registry.c b/src/serve/model_registry.c new file mode 100644 index 0000000..362b7c6 --- /dev/null +++ b/src/serve/model_registry.c @@ -0,0 +1,871 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file model_registry.c + * @brief Implementation of the multi-model registry (see model_registry.h). + * + * Locking rules: + * - reg->lock guards the slot table (used/unloading/refs/n). It is NEVER + * held across pn_load_on / pn_model_free — a slot is reserved (used, no + * model yet) so the name is taken, then the load runs unlocked and the + * result is published (or the slot returned) under the lock. + * - entry->gen_lock is the per-model generation serializer; the registry + * never takes it (the serve chat path does, via trylock). + * - reg->drained is signaled by mr_release; mr_unload waits on it until the + * entry's refs hit zero, then frees outside the lock. The wait is BOUNDED + * (see MR_UNLOAD_DRAIN_MS_DEFAULT) and rolls the entry back on timeout — + * an unload that cannot free must refuse, never report success and strand + * the dies. + * + * @status CPU-validated (tests/test_model_registry.c, + * tests/test_model_unload_reclaim.c); die placement numbers become + * real on the K80 test machine (die-manager CUDA probe). + * @issues None + * @todo None + */ + +#include "model_registry.h" + +#include +#include +#include +#include +#include +#include + +#include "../accel/backend_identity.h" +#include "../accel/die_layer_split.h" +#include "../accel/die_manager.h" +#include "../core/mem/pn_admission.h" +#include "../formats/gguf_layer_bytes.h" +#include "batch_sched.h" +#include "serve_backend_guard.h" + +/* Pre-load admission gate context, used ONLY when the model was launched + * without --ctx-cap. With a cap set, the KV ring is sized to that cap and this + * guess is simply wrong: at 8k it demands ~1.1 GiB of KV that a --ctx-cap 2048 + * model will never allocate, which is enough on its own to refuse a 2-die + * Qwen3.6-35B-A3B Q4_K_M that fits with 1.3 GiB per die to spare. */ +#define MR_ADMIT_CTX_GUESS 8192 + +/* Weight bytes die `idx` of `n_placed` will actually hold. + * + * NOT file_bytes / n_placed. A GGUF also carries token_embd — never uploaded, + * the embedding is a host row copy — and output.weight, which belongs to ONE + * die. Prorating the file charges every die for both: on Qwen3.6-35B-A3B + * Q4_K_M that is ~294 MiB of phantom weight per die, and it lands on a gate + * whose margin is smaller than that. + * + * Mirrors the placement the CUDA forward makes (layers split evenly, LM head + * on the LAST die), read straight off the tensor table. Opening the GGUF here + * is an mmap + header parse, not a load. Falls back to the prorated share if + * the file will not open or has no blk.* tensors, so an unfamiliar layout is + * gated conservatively rather than admitted blind. + * + * "Mirrors" is now literal: the layer range comes from pn_die_layer_range, the + * same partitioner qwen35moe_forward_cuda.c uploads by. It used to be a second + * copy of the arithmetic, and the copies disagreed — the forward split at + * n_layer/2 for ANY width while this projected n_layer*idx/n_placed, so a + * 3-die placement was priced at a third per die and loaded at a half. + * + * `n_layers_here_out` / `n_layer_out`, when non-NULL, receive this die's layer + * count and the model's total — the KV projection needs the same layer share + * the weights were sliced by, and deriving it twice is how the two drift apart. + * Both are set to 0 when the layout could not be read, which callers must treat + * as "no share known" rather than as a zero share. */ +static uint64_t mr_die_weight_share(const char *path, int idx, int n_placed, + uint64_t file_bytes, + int *n_layers_here_out, int *n_layer_out) +{ + uint64_t fallback = (n_placed > 0) ? file_bytes / (uint64_t)n_placed + : file_bytes; + if (n_layers_here_out) + *n_layers_here_out = 0; + if (n_layer_out) + *n_layer_out = 0; + if (!path || n_placed < 1 || idx < 0 || idx >= n_placed) + return fallback; + + pn_gguf_file *gf = pn_gguf_open(path); + if (!gf) + return fallback; + + uint64_t share = fallback; + int n_layer = pn_gguf_layer_count(gf); + int lo = 0, hi = 0; + if (pn_die_layer_range(n_layer, n_placed, idx, &lo, &hi) == 0) { + uint64_t layers = pn_gguf_layer_range_bytes(gf, lo, hi); + if (layers > 0) { + share = layers; + if (n_layers_here_out) + *n_layers_here_out = hi - lo; + if (n_layer_out) + *n_layer_out = n_layer; + if (idx == n_placed - 1) { + /* The LM head rides with the last die's layers. A TIED model + * has no output.weight: its head is token_embd materialized to + * F32, so size it as 4 bytes an element, not as the quantized + * tensor it was packed as. */ + uint64_t head = pn_gguf_tensor_file_bytes(gf, "output.weight"); + if (head == 0) + head = pn_gguf_tensor_numel(gf, "token_embd.weight") * + sizeof(float); + share += head; + } + } + } + pn_gguf_close(gf); + return share; +} + +/* How long mr_unload waits for in-flight references to drain before refusing. + * + * The wait has to be BOUNDED because not every reference drains on its own. + * A generation turn releases in milliseconds, but pn_session_watch_add takes a + * reference for the whole lifetime of an open /session/watch connection — a + * pinned model would park the unload forever. That is not hypothetical: it is + * what the shutdown path already works around by stopping the watch manager + * BEFORE mr_free (serve_command.c, serve_run_shutdown), and it is exactly what + * /admin/models/unload hit in the field — the entry was marked `unloading` + * (gone from /v1/models and from mr_acquire) while the drain never returned, so + * pn_dm_release and the model free below it never ran and the dies stayed + * budgeted AND resident until the whole serve was restarted. + * + * 15 s is above a normal turn's release latency and below an operator's + * patience. Override with PN_UNLOAD_DRAIN_MS (0 = refuse immediately if + * anything is in flight — what the regression test uses). */ +#define MR_UNLOAD_DRAIN_MS_DEFAULT 15000 + +/* Sentinel for the shutdown path: wait for the drain without a deadline. + * mr_free is only reached after serve_run_shutdown has quiesced generations and + * stopped the watch manager, so there is nothing left that cannot drain, and an + * unbounded wait there keeps shutdown semantics byte-identical to before. */ +#define MR_UNLOAD_DRAIN_FOREVER (-1) + +static void set_err(char *err, size_t errlen, const char *msg) +{ + if (err && errlen) { + strncpy(err, msg, errlen - 1); + err[errlen - 1] = '\0'; + } +} + +/* Configured unload drain budget in milliseconds (PN_UNLOAD_DRAIN_MS, else the + * default). A garbage value falls back; an explicit 0 is honoured. */ +static int mr_unload_drain_ms(void) +{ + const char *env = getenv("PN_UNLOAD_DRAIN_MS"); + if (env && *env) { + char *end = NULL; + long v = strtol(env, &end, 10); + if (end != env && v >= 0 && v <= 3600000L) + return (int)v; + } + return MR_UNLOAD_DRAIN_MS_DEFAULT; +} + +/* Basename of a path (pointer into the same string). */ +static const char *path_basename(const char *path) +{ + const char *slash = strrchr(path, '/'); + return slash ? slash + 1 : path; +} + +int mr_init(mr_registry *reg) +{ + if (!reg) + return -1; + memset(reg, 0, sizeof(*reg)); + if (pthread_mutex_init(®->lock, NULL) != 0) + return -1; + if (pthread_cond_init(®->drained, NULL) != 0) { + pthread_mutex_destroy(®->lock); + return -1; + } + /* A3: serialize GPU weight uploads so the ~2.4 GB transient never stacks. */ + if (pthread_mutex_init(®->upload_lock, NULL) != 0) { + pthread_cond_destroy(®->drained); + pthread_mutex_destroy(®->lock); + return -1; + } + return 0; +} + +/* Locked lookup: the slot holding `name`, or NULL. Ready-only when + * require_ready (skips reserved-but-still-loading slots). */ +static mr_entry *find_locked(mr_registry *reg, const char *name, + bool require_ready) +{ + for (int i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *e = ®->entries[i]; + if (!e->used) + continue; + if (require_ready && (!e->model || e->unloading)) + continue; + if (strncmp(e->name, name, MR_MAX_NAME) == 0) + return e; + } + return NULL; +} + +int mr_load(mr_registry *reg, const char *path, const char *name, + const pn_load_opts *opts, const int *dies, int n_dies, + int reasoning_budget, int max_sessions, int max_concurrent, + char *err, size_t errlen) +{ + if (!reg || !path) { + set_err(err, errlen, "registry/path required"); + return -1; + } + + /* Resolve the per-model session cap: <=0 → the BS_MAX_USERS default + * (byte-identical to the pre-cap behavior); otherwise clamp into + * [1, BS_MAX_USERS] — the batch scheduler's user cap is the hard ceiling, + * so a request for more sessions than the batcher can serve is capped, not + * refused (over-request is an operator sizing hint, not an error). */ + int resolved_max_sessions = BS_MAX_USERS; + if (max_sessions > 0) + resolved_max_sessions = max_sessions < BS_MAX_USERS + ? max_sessions : BS_MAX_USERS; + + /* Resolve the per-model CONCURRENCY cap — a different ceiling from the + * residency cap above, and the one the batch scheduler enforces. Residency + * = how many named conversations may hold KV; concurrency = how many + * requests may decode in the same batched step. Before Phase 7 the + * scheduler was created with the compile-time BS_MAX_USERS regardless of + * either, so a model configured for 2 users would happily admit 8 — which + * is how a burst could drive saber-orch off the ctx_cap-131072 x K=4 ring + * cliff. Absent/<=0 defaults to the + * resolved max_sessions, so a single manifest knob still sizes both. */ + int resolved_max_concurrent = resolved_max_sessions; + if (max_concurrent > 0) + resolved_max_concurrent = max_concurrent < BS_MAX_USERS + ? max_concurrent : BS_MAX_USERS; + if (!name || !*name) + name = path_basename(path); + if (strlen(name) >= MR_MAX_NAME) { + set_err(err, errlen, "model name too long"); + return -1; + } + if (n_dies < 0 || n_dies > PN_MODEL_MAX_DIES || (n_dies > 0 && !dies)) { + set_err(err, errlen, "bad die list"); + return -1; + } + + /* A die request this process cannot honour is a REFUSAL, not a quiet CPU + * placement. The check lives here as well as in pn_serve_main's pre-flight + * because POST /admin/models/load reaches this function directly with its + * own `dies` array, and a serve that refused the wrong manifest at startup + * must not accept the same mistake an hour later over HTTP. Same predicate, + * one definition (serve_backend_guard.h). n_dies == 0 is always allowed — + * that is how a CPU serve is asked for. */ + { + pn_backend_guard_verdict v = + pn_backend_guard_check(n_dies, pn_backend_supports_die_placement(), + pn_dm_count()); + if (v != PN_BACKEND_GUARD_OK) { + set_err(err, errlen, pn_backend_guard_short_reason(v)); + return -1; + } + } + + struct stat st; + if (stat(path, &st) != 0 || st.st_size <= 0) { + set_err(err, errlen, "model file not found"); + return -1; + } + uint64_t weights_bytes = (uint64_t)st.st_size; /* GGUF mmaps ~1:1 */ + /* Model-identity tag for the session warm-tier (Phase 7): FNV-1a over the + * path, then mixed with the file size and mtime. Two different files, or the + * same path after a re-quantize/rebuild (size or mtime changes), get + * distinct tags — so a stale session blob is cleanly rejected on restore. */ + uint64_t weights_version = 1469598103934665603ull; /* FNV offset basis */ + for (const char *p = path; *p; p++) { + weights_version ^= (unsigned char)*p; + weights_version *= 1099511628211ull; /* FNV prime */ + } + weights_version ^= weights_bytes + 0x9E3779B97F4A7C15ull + + (weights_version << 6) + (weights_version >> 2); + weights_version ^= (uint64_t)st.st_mtime + 0x9E3779B97F4A7C15ull + + (weights_version << 6) + (weights_version >> 2); + + /* --- reserve the name/slot ------------------------------------------ */ + /* Same-name placements are ALLOWED and intentional: an operator places one + * model id on several cards (e.g. --dies 0,1 and --dies 2,3 under one + * --name) and mr_acquire load-balances across them. Clients see a single id + * (oai_handle_models dedupes by name). Load/unload is operator-only (never + * exposed to clients), so name collisions are an operator concern, not a + * safety hole — hence no duplicate-name refusal here. */ + pthread_mutex_lock(®->lock); + mr_entry *e = NULL; + for (int i = 0; i < MR_MAX_MODELS; i++) { + if (!reg->entries[i].used) { + e = ®->entries[i]; + break; + } + } + if (!e) { + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "model table full"); + return -1; + } + memset(e, 0, sizeof(*e)); + e->used = true; /* reserved: name taken, not ready */ + strncpy(e->name, name, MR_MAX_NAME - 1); + pthread_mutex_unlock(®->lock); + + /* --- placement + cross-model VRAM budgeting (no registry lock) ------- */ + int placed[PN_MODEL_MAX_DIES]; + int n_placed = 0; + if (n_dies > 0) { + memcpy(placed, dies, (size_t)n_dies * sizeof(int)); + n_placed = n_dies; + } else if (pn_dm_count() > 0) { + /* Auto-place: one die when the weights fit a die's budget, else a + * same-card pair (the qwen35moe 2-die split). Ask what ONE die would + * really hold, not what the file weighs — a tied model's F32 LM head + * makes its residency exceed its file, so the file size can say "fits + * one die" about a model that does not. */ + pn_dm_die d0; + int want = 1; + uint64_t one_die = mr_die_weight_share(path, 0, 1, weights_bytes, + NULL, NULL); + if (pn_dm_get(0, &d0) == 0 && one_die > d0.vram_budget) + want = 2; + if (pn_dm_place(one_die, want, placed) == 0) + n_placed = want; + /* No fit → fall through CPU-resident rather than refusing: the CPU + * forward still serves (the K80 host does this for GGUF models the + * GPU path doesn't cover). */ + } + + uint64_t vram_bytes = 0; + if (n_placed > 0 && pn_dm_count() > 0) { + /* Admission per candidate die (MemoryAware §6.1): weights share + KV + * projection must fit the die's remaining budget. B1: Q4 KV is now + * the default; mirror pn_qwen35moe_kv_q4_on()'s logic (ON unless =0). */ + const char *kv_q4_env = getenv("QWEN35_KV_Q4"); + bool kv_q4 = !(kv_q4_env && kv_q4_env[0] == '0'); + /* The KV ring is sized to --ctx-cap when one is set, so gate against + * that and fall back to the guess only when it is absent. */ + int admit_ctx = (opts && opts->ctx_cap > 0) ? opts->ctx_cap + : MR_ADMIT_CTX_GUESS; + uint64_t need_total = 0; + for (int i = 0; i < n_placed; i++) { + pn_dm_die d; + if (pn_dm_get(placed[i], &d) != 0) { + pthread_mutex_lock(®->lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "unknown die in placement"); + return -1; + } + uint64_t die_free = d.vram_budget > d.vram_used + ? d.vram_budget - d.vram_used : 0; + uint64_t need = 0; + int n_layers_here = 0, n_layer = 0; + uint64_t per_die = mr_die_weight_share(path, i, n_placed, + weights_bytes, + &n_layers_here, &n_layer); + /* Pre-load placement guess: the model is not resident yet, so its + * real KV geometry is unknown — use the conservative generic + * per-token constant (KV Phase 1 threads real geometry in only where + * the loaded model is in hand). Measured against the real thing on + * the one model that matters here, the generic Q4 constant is close: + * 5898 B/token vs the 6144 B/token Qwen3.6-35B-A3B actually reports + * from its own geometry, a 4% under-price that is ~15 MiB per die at + * a 131072 cap — noise beside the scratch reserve. Not worth + * teaching this path to parse a hybrid's attention layout. + * + * Scale by THIS die's layer share, the same way the weights were + * sliced. Charging every die the whole model's KV double-counted it + * across a 2-die split. */ + uint64_t kv_bytes = pn_admit_die_kv_bytes(0, kv_q4, admit_ctx, + n_layers_here, n_layer); + /* A GPU placement is a GPU load: project weights + KV + on-die + * scratch, NOT the host activation LUT. die_free comes off + * vram_budget, which the die manager already set to total minus the + * per-die reserve — so use the _budgeted entry point, which does not + * hold it back a second time. */ + if (!pn_admit_model_load_die_budgeted(per_die, kv_bytes, admit_ctx, + die_free, &need)) { + pthread_mutex_lock(®->lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + /* Say WHICH die and BY HOW MUCH. A bare "(admission)" sent an + * operator hunting the wrong gate for a whole session: the + * numbers are the difference between "buy VRAM" and "the + * projection is wrong". */ + snprintf(err, errlen, + "die VRAM budget refused the model (admission): " + "die %d has %llu MiB free, needs %llu MiB " + "(weights %llu + KV@%d + scratch)", + placed[i], + (unsigned long long)(die_free >> 20), + (unsigned long long)(need >> 20), + (unsigned long long)(per_die >> 20), admit_ctx); + return -1; + } + need_total += need; + } + vram_bytes = need_total; + if (pn_dm_bind(placed, n_placed, name, vram_bytes) != 0) { + pthread_mutex_lock(®->lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "die binding refused (placement raced or " + "no longer fits)"); + return -1; + } + } + + /* --- the load itself (slow; registry stays serviceable) -------------- */ + /* A3: serialize GPU weight uploads — at most one in-flight at a time so + * the ~2.4 GB host transient never stacks. The startup loop is sequential + * already; this guard covers a runtime /admin/models/load that arrives + * while a startup upload is still in progress. The mutex is held only + * across pn_load_on (and released immediately after): everything else + * (admission, slot reservation, publish) stays outside it so a waiting + * caller sees the live registry and can be refused promptly on its slot + * attempt once the prior load finishes. */ + pthread_mutex_lock(®->upload_lock); + pn_model *model = pn_load_on(path, opts, n_placed ? placed : NULL, + n_placed); + pthread_mutex_unlock(®->upload_lock); + pn_state *state = model ? pn_state_new(model) : NULL; + if (!model || !state) { + if (model) + pn_model_free(model); + if (n_placed > 0) + pn_dm_release(name); + pthread_mutex_lock(®->lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + set_err(err, errlen, model ? "failed to create inference state" + : "failed to load model"); + return -1; + } + + /* --- publish ---------------------------------------------------------- */ + if (pthread_mutex_init(&e->gen_lock, NULL) != 0) { + pn_state_free(state); + pn_model_free(model); + if (n_placed > 0) + pn_dm_release(name); + pthread_mutex_lock(®->lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "failed to init generation lock"); + return -1; + } + if (pthread_mutex_init(&e->pc_lock, NULL) != 0) { + pthread_mutex_destroy(&e->gen_lock); + pn_state_free(state); + pn_model_free(model); + if (n_placed > 0) + pn_dm_release(name); + pthread_mutex_lock(®->lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "failed to init prefix-cache lock"); + return -1; + } + pthread_mutex_lock(®->lock); + e->path = strdup(path); + e->model = model; + e->state = state; + e->sched = NULL; /* C3: initialised below after unlock */ + e->prefix_cache = NULL; /* PC1: initialised below after unlock */ + memcpy(e->dies, placed, (size_t)n_placed * sizeof(int)); + e->n_dies = n_placed; + e->reasoning_budget = reasoning_budget > 0 ? reasoning_budget : 0; + e->max_sessions = resolved_max_sessions; + e->max_concurrent = resolved_max_concurrent; + e->vram_bytes = vram_bytes; + e->weights_version = weights_version; + e->refs = 0; + e->unloading = false; + reg->n++; + pthread_mutex_unlock(®->lock); + + /* C3: start the batch scheduler for GPU-resident models when enabled. + * Done after publish so the model is acquirable while the scheduler + * starts its driver thread (scheduler creation is fast — no GPU work). */ + if (bs_enabled() && n_placed > 0) { + /* The scheduler's slot count is the per-model CONCURRENCY cap, not the + * compile-time BS_MAX_USERS: a model sized for N users must not admit + * more than N into a batched step. bs_create reserves an extra engine + * slot as the rebuild scratch on top of this, so the cap is the user + * slot count exactly. */ + pn_batch_sched *sched = bs_create(model, resolved_max_concurrent, e->name); + if (sched) { + fprintf(stderr, "registry: batch scheduler for '%s': " + "%d concurrent decode slots (max_concurrent), " + "%d resident sessions (max_sessions)\n", + e->name, resolved_max_concurrent, resolved_max_sessions); + /* Publish sched under the registry lock so concurrent acquires + * see a consistent view (sched==NULL means old path). */ + pthread_mutex_lock(®->lock); + e->sched = sched; + pthread_mutex_unlock(®->lock); + } else { + fprintf(stderr, "registry: batch scheduler creation failed for " + "'%s'; falling back to single-user mode\n", e->name); + } + } + + /* PC1: session prefix cache for GPU-resident models (the SSM + * snapshot/restore primitive is CUDA-only). max_sessions = the resolved + * per-model cap (default BS_MAX_USERS = 8) — bounds both the live-session + * count and the host footprint (~240 MB/session). A big model with little + * die headroom (e.g. saber, ~18 GB across 2 dies) is capped BELOW 8 so 8 + * snapshots can't OOM the card; the default stays byte-identical. A + * creation failure is non-fatal: prefix_cache stays NULL and serving falls + * back to full prefill (byte-identical to today). */ + if (n_placed > 0) { + pn_prefix_cache *pc = pn_prefix_cache_new(model, resolved_max_sessions); + if (pc) { + pthread_mutex_lock(®->lock); + e->prefix_cache = pc; + pthread_mutex_unlock(®->lock); + fprintf(stderr, "registry: prefix cache enabled for '%s' " + "(max_sessions=%d)\n", e->name, resolved_max_sessions); + } else { + fprintf(stderr, "registry: prefix cache creation failed for " + "'%s'; falling back to full prefill\n", e->name); + } + } + + fprintf(stderr, "registry: loaded '%s' (%s)%s\n", e->name, path, + n_placed ? "" : " [CPU-resident]"); + if (n_placed) { + fprintf(stderr, "registry: '%s' placed on die%s", e->name, + n_placed > 1 ? "s" : ""); + for (int i = 0; i < n_placed; i++) + fprintf(stderr, "%s%d", i ? "," : " ", placed[i]); + fprintf(stderr, " (%.1f GiB projected)\n", + (double)vram_bytes / (1024.0 * 1024.0 * 1024.0)); + } + return 0; +} + +mr_entry *mr_acquire(mr_registry *reg, const char *name) +{ + if (!reg) + return NULL; + pthread_mutex_lock(®->lock); + mr_entry *e = NULL; + if (name && *name) { + /* Multi-card load-balance: among the ready placements that share this + * name, pick the least-loaded (fewest in-flight refs). One model id + * thus spreads across every card it is placed on; a single placement + * is just the degenerate one-candidate case. */ + for (int i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *c = ®->entries[i]; + if (!c->used || !c->model || c->unloading) + continue; + if (strncmp(c->name, name, MR_MAX_NAME) != 0) + continue; + if (!e || c->refs < e->refs) + e = c; + } + } else { + /* Default model: the lowest-slot resident entry. */ + for (int i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *c = ®->entries[i]; + if (c->used && c->model && !c->unloading) { + e = c; + break; + } + } + } + if (e) + e->refs++; + pthread_mutex_unlock(®->lock); + return e; +} + +void mr_release(mr_registry *reg, mr_entry *entry) +{ + if (!reg || !entry) + return; + pthread_mutex_lock(®->lock); + if (entry->refs > 0) + entry->refs--; + pthread_cond_broadcast(®->drained); + pthread_mutex_unlock(®->lock); +} + +int mr_keepalive(mr_registry *reg) +{ + int i, sent = 0; + + if (!reg) + return 0; + for (i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *e = ®->entries[i]; + bool held = false; + + pthread_mutex_lock(®->lock); + if (e->used && e->model && !e->unloading && !e->sched) { + e->refs++; + held = true; + } + pthread_mutex_unlock(®->lock); + if (!held) + continue; + /* TRYLOCK: a generation in flight IS the keepalive; never wait behind one. */ + if (pthread_mutex_trylock(&e->gen_lock) == 0) { + const int rc = pn_model_keepalive(e->model); + + pthread_mutex_unlock(&e->gen_lock); + if (rc > 0) + sent += rc; + } + mr_release(reg, e); + } + return sent; +} + +int mr_trylock_generation(mr_entry *entry) +{ + int waited_ms; + + /* The idle keepalive holds gen_lock for a few milliseconds; a generation holds it for + * seconds. Waiting up to 100 ms tells the two apart without turning the keepalive into + * a spurious 429. */ + for (waited_ms = 0; waited_ms < 100; waited_ms += 2) { + struct timespec pause = { 0, 2000000L }; + + if (pthread_mutex_trylock(&entry->gen_lock) == 0) + return 0; + nanosleep(&pause, NULL); + } + return pthread_mutex_trylock(&entry->gen_lock); +} + +/* Wait for `e->refs` to reach zero, at most `drain_ms` milliseconds (negative = + * no deadline). Caller holds reg->lock; the lock is held again on return. + * Returns 0 when the entry drained, -1 on timeout (refs still > 0). */ +static int drain_entry_locked(mr_registry *reg, mr_entry *e, int drain_ms) +{ + if (drain_ms < 0) { + while (e->refs > 0) + pthread_cond_wait(®->drained, ®->lock); + return 0; + } + + struct timespec deadline; + clock_gettime(CLOCK_REALTIME, &deadline); + deadline.tv_sec += drain_ms / 1000; + deadline.tv_nsec += (long)(drain_ms % 1000) * 1000000L; + if (deadline.tv_nsec >= 1000000000L) { + deadline.tv_sec += 1; + deadline.tv_nsec -= 1000000000L; + } + + while (e->refs > 0) { + int rc = pthread_cond_timedwait(®->drained, ®->lock, &deadline); + if (rc == ETIMEDOUT) + break; + } + return e->refs > 0 ? -1 : 0; +} + +/* Unload `name`, waiting at most `drain_ms` ms for in-flight references + * (MR_UNLOAD_DRAIN_FOREVER = no deadline). On a drain timeout the entry is + * rolled back — still resident, still acquirable, still bound to its dies — and + * the refusal names the reason. See MR_UNLOAD_DRAIN_MS_DEFAULT for why the + * public path must never wait indefinitely. */ +static int mr_unload_drain(mr_registry *reg, const char *name, int drain_ms, + char *err, size_t errlen) +{ + if (!reg || !name || !*name) { + set_err(err, errlen, "model name required"); + return -1; + } + pthread_mutex_lock(®->lock); + mr_entry *e = find_locked(reg, name, false); + if (!e || !e->model) { + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "no such model"); + return -1; + } + if (e->unloading) { + pthread_mutex_unlock(®->lock); + set_err(err, errlen, "model is already unloading"); + return -1; + } + e->unloading = true; /* no new acquires while we try to drain */ + + if (drain_entry_locked(reg, e, drain_ms) != 0) { + /* COULD NOT FREE. Put the entry back exactly as it was — resident, + * acquirable, holding its die binding — and refuse. The alternative + * (leave it deregistered and return success) is what stranded two dies + * in the field: the registry said the model was gone while its VRAM and + * its die budget were still held, and only a full serve restart got + * them back. */ + int refs = e->refs; + e->unloading = false; + pthread_mutex_unlock(®->lock); + /* Kept short on purpose: the caller's reason buffer is 160 bytes and a + * truncated multi-byte dash would land as mojibake in the JSON error. */ + char msg[160]; + snprintf(msg, sizeof(msg), + "model is busy: %d reference(s) in flight after %d ms; NOT " + "unloaded (a live turn or an open /session/watch pins it)", + refs, drain_ms); + set_err(err, errlen, msg); + fprintf(stderr, "registry: REFUSED unload of '%s' — %d in-flight " + "reference(s) did not drain in %d ms; the model stays resident " + "(nothing freed, nothing stranded)\n", name, refs, drain_ms); + return -1; + } + /* Committed from here on: the entry drained, so every step below runs. */ + reg->n--; + + /* Detach the resources, free the slot, THEN free outside the lock (a + * model free can be slow — munmap + GPU buffer teardown). */ + pn_model *model = e->model; + pn_state *state = e->state; + pn_batch_sched *sched = e->sched; + pn_prefix_cache *prefix_cache = e->prefix_cache; + char *path = e->path; + int had_dies = e->n_dies; + char freed_name[MR_MAX_NAME]; + strncpy(freed_name, e->name, MR_MAX_NAME); + freed_name[MR_MAX_NAME - 1] = '\0'; + pthread_mutex_destroy(&e->gen_lock); + pthread_mutex_destroy(&e->pc_lock); + memset(e, 0, sizeof(*e)); + pthread_mutex_unlock(®->lock); + + /* PC1: free the session prefix cache (frees every cached pn_state) BEFORE + * the model/scheduler teardown — its states reference the model. */ + if (prefix_cache) + pn_prefix_cache_free(prefix_cache); + + /* C3: stop the batch scheduler (joins driver thread) before freeing + * model state — bs_destroy calls pn_state_free on slot states. */ + if (sched) + bs_destroy(sched); + + /* Give the die budget back BEFORE the (slow) model free, and never discard + * the verdict: a release that matches no binding leaves the die budgeted + * for a model that no longer exists, and the next load onto those dies is + * refused by admission with nothing in the log to explain it. */ + if (had_dies > 0 && pn_dm_release(freed_name) != 0) + fprintf(stderr, "registry: WARNING unload '%s' found no die binding to " + "release — %d die(s) may stay budgeted until restart\n", + freed_name, had_dies); + pn_state_free(state); + pn_model_free(model); + free(path); + fprintf(stderr, "registry: unloaded '%s'%s\n", freed_name, + had_dies > 0 ? " (die budget released)" : ""); + return 0; +} + +int mr_unload(mr_registry *reg, const char *name, char *err, size_t errlen) +{ + return mr_unload_drain(reg, name, mr_unload_drain_ms(), err, errlen); +} + +uint64_t mr_weights_version(const mr_entry *entry) +{ + return entry ? entry->weights_version : 0; +} + +int mr_count(mr_registry *reg) +{ + if (!reg) + return 0; + pthread_mutex_lock(®->lock); + int n = reg->n; + pthread_mutex_unlock(®->lock); + return n; +} + +bool mr_model_is_minicpm(const pn_model *model) +{ + return model && model->config.arch == PN_ARCH_LLAMA && model->chat_add_bos; +} + +int mr_snapshot(mr_registry *reg, mr_info *out, int max) +{ + if (!reg || !out || max <= 0) + return 0; + pthread_mutex_lock(®->lock); + int n = 0; + for (int i = 0; i < MR_MAX_MODELS && n < max; i++) { + mr_entry *e = ®->entries[i]; + if (!e->used || !e->model || e->unloading) + continue; + memset(&out[n], 0, sizeof(out[n])); + strncpy(out[n].name, e->name, MR_MAX_NAME - 1); + if (e->path) { + strncpy(out[n].path, e->path, sizeof(out[n].path) - 1); + out[n].path[sizeof(out[n].path) - 1] = '\0'; + } + memcpy(out[n].dies, e->dies, sizeof(e->dies)); + out[n].n_dies = e->n_dies; + out[n].vram_bytes = e->vram_bytes; + out[n].max_sessions = e->max_sessions; + out[n].max_concurrent = e->max_concurrent; + /* CONFIGURED vs SERVED. bs_get_load already reports capacity 1 for a + * NULL scheduler (the single-user trylock->429 path), which is the + * whole reason it is asked here rather than trusting e->max_concurrent: + * the entry keeps the cap the operator ASKED for even when bs_create + * returned NULL (no CUDA compiled in, QWEN35_BATCH_SLOTS off) or + * clamped the slot count to the memory-pressure ladder ceiling. Cheap + * and lock-safe under reg->lock: bs_get_load takes only sched->mu and + * nothing under sched->mu ever reaches back into the registry, so the + * two locks cannot form a cycle. */ + bs_load load = {0}; + bs_get_load(e->sched, &load); + out[n].served_concurrency = load.capacity; + /* Read the window HERE, under the registry lock, while the entry still + * holds a live model pointer — the snapshot is a copy-out and its + * consumers (the /v1/models + /admin writers) hold no ref on the model + * afterwards. Both calls are pure reads of the model config plus the + * memwatch snapshot: no allocation, no blocking, safe under the lock. */ + out[n].context_window = pn_effective_ctx_window(e->model); + out[n].context_window_configured = pn_session_ctx_cap(e->model); + /* Same reasoning: a lock-free read of per-die counters, no allocation. */ + pn_model_graph_residency_read(e->model, &out[n].graphs); + pn_model_attn_residency_read(e->model, &out[n].attn); + out[n].minicpm_tool_dialect = mr_model_is_minicpm(e->model); + out[n].refs = e->refs; + n++; + } + pthread_mutex_unlock(®->lock); + return n; +} + +void mr_free(mr_registry *reg) +{ + if (!reg) + return; + /* Unload everything that is resident (drains each in turn). */ + for (;;) { + char name[MR_MAX_NAME] = ""; + pthread_mutex_lock(®->lock); + for (int i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *e = ®->entries[i]; + if (e->used && e->model && !e->unloading) { + strncpy(name, e->name, MR_MAX_NAME - 1); + break; + } + } + pthread_mutex_unlock(®->lock); + if (!name[0]) + break; + /* Shutdown drains without a deadline on purpose — see + * MR_UNLOAD_DRAIN_FOREVER. A bounded drain here would turn a busy + * model into an endless refuse/retry loop in this very while(1). */ + mr_unload_drain(reg, name, MR_UNLOAD_DRAIN_FOREVER, NULL, 0); + } + pthread_cond_destroy(®->drained); + pthread_mutex_destroy(®->lock); + pthread_mutex_destroy(®->upload_lock); +} diff --git a/src/serve/model_registry.h b/src/serve/model_registry.h new file mode 100644 index 0000000..8d93151 --- /dev/null +++ b/src/serve/model_registry.h @@ -0,0 +1,289 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file model_registry.h + * @brief In-memory model registry for multi-model serving: + * name → {pn_model handle, pn_state, die assignment, VRAM + * footprint, per-model generation lock}. The serve layer routes each + * request's `model` field to a resident entry; models load/unload at + * runtime so /v1/models is a live list. + * + * Concurrency: entries are refcounted. mr_acquire/mr_release bracket + * every use of an entry so mr_unload can drain — it marks the entry + * unloading (no new acquires), waits until in-flight refs hit zero, + * then frees the model. Generation on one model is serialized by the + * entry's own gen_lock (trylock → 429 upstream, exactly the old + * single-model behavior); generations on DIFFERENT models run + * concurrently (the sampler scratch is _Thread_local). + * + * Placement/VRAM budgeting: mr_load estimates weights from the file + * size (GGUF is mmapped 1:1), asks the die manager for a die-set when + * one is running (explicit dies win), gates on pn_admit_model_load + * against the per-die budget, then pn_load_on + pn_dm_bind. With no + * die manager (the Mac / CPU host) entries are CPU-resident (0 dies). + * + * @status Registry logic CPU-validated (tests/test_model_registry.c); real + * VRAM placement is gated on GPU hardware with the die manager's CUDA probe. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_MODEL_REGISTRY_H +#define PN_SERVE_MODEL_REGISTRY_H + +#include +#include +#include + +#include "../../include/pantheon.h" +#include "../core/engine.h" +#include "../core/prefix_cache.h" +#include "batch_sched.h" + +#define MR_MAX_MODELS 16 +#define MR_MAX_NAME 64 + +/* One resident model. Owned by the registry; use only between mr_acquire and + * mr_release. + * + * C3 (QWEN35_BATCH_SLOTS=1): `sched` replaces the single `state`/`gen_lock` + * serializer with a multi-slot batch scheduler. When `sched` is non-NULL, up + * to BS_MAX_SLOTS users share the model in one batched GPU forward per step; + * the old trylock→429 is replaced by slot-acquisition (full→429). + * + * When QWEN35_BATCH_SLOTS is off (default), `sched` is NULL, `state` and + * `gen_lock` are used exactly as before — no behaviour change. */ +typedef struct { + char name[MR_MAX_NAME]; + char *path; /* owned */ + pn_model *model; + pn_state *state; /* single-user state (sched==NULL path) */ + pthread_mutex_t gen_lock; /* single-user generation serializer */ + pn_batch_sched *sched; /* C3: multi-slot batch scheduler (may be NULL) */ + pn_prefix_cache *prefix_cache; /* PC1: session prefix cache (may be NULL); + * keyed by session_id so an agentic turn + * delta-prefills instead of re-prefilling + * the whole context. Guarded per-session by + * the same serializer as generation. */ + pthread_mutex_t pc_lock; /* serializes prefix_cache ops (the cache is + * documented thread-unsafe; the batched + * path has concurrent slots). */ + int dies[PN_MODEL_MAX_DIES]; + int n_dies; /* 0 = CPU-resident */ + int reasoning_budget; /* per-model AUTO-path reasoning cap + * (tokens); 0 = disabled. Set at load + * from `--reasoning-budget` (or the + * admin body); a request may override + * it per-turn. See sg_gen_params. */ + uint64_t vram_bytes; /* placement estimate */ + uint64_t weights_version; /* model-identity tag for the + * session warm-tier (Phase 7): + * derived from path + file size + * + mtime at load. Stamped into + * an exported session blob; a + * restore whose blob carries a + * different tag is refused (the + * weights it was captured under + * no longer match). */ + int max_sessions; /* per-model prefix-cache / + * live-session cap. Set at load + * from `--max-sessions` (or the + * admin body); 0/absent → the + * BS_MAX_USERS default. This is + * the resolved value actually + * passed to pn_prefix_cache_new. + * NOT a concurrency cap — see + * max_concurrent. */ + int max_concurrent; /* per-model BATCH-SLOT cap: how + * many requests may DECODE at + * once. Set at load from + * `--max-concurrent` (or the + * admin body); 0/absent → + * max_sessions. Clamped to + * [1, BS_MAX_USERS] and passed + * to bs_create as the + * scheduler's slot count. + * Distinct from max_sessions: + * residency is how many + * conversations may HOLD KV, + * concurrency is how many may + * decode simultaneously. */ + int refs; /* in-flight acquires */ + bool unloading; /* draining — no new acquires */ + bool used; /* slot occupancy */ +} mr_entry; + +/* The registry: a fixed slot table behind one mutex + a drain condvar. + * + * A3: upload_lock serializes concurrent GPU weight uploads (the ~2.4 GB + * transient). The startup loop is sequential already, but a runtime + * /admin/models/load arriving while a startup upload is in-flight would race + * and stack two 2.4 GB transients on an 8 GB host. upload_lock prevents that: + * mr_load acquires it before pn_load_on and releases it after, so at most one + * GPU weight upload is in flight at any time. The hold is bounded (~55 s on + * the K80) and well-telegraphed (the caller gets a blocking load, not a 503). */ +typedef struct { + mr_entry entries[MR_MAX_MODELS]; + int n; /* live (used && !unloading) entries */ + pthread_mutex_t lock; + pthread_cond_t drained; /* signaled on every mr_release */ + pthread_mutex_t upload_lock; /* A3: one GPU upload at a time */ +} mr_registry; + +/* Copy-out row for /v1/models, `pantheon models`, and the admin surface. */ +typedef struct { + char name[MR_MAX_NAME]; + char path[512]; + int dies[PN_MODEL_MAX_DIES]; + int n_dies; + uint64_t vram_bytes; + int max_sessions; /* resolved per-model session cap */ + int max_concurrent; /* CONFIGURED per-model batch-slot cap — + * what --max-concurrent asked for. NOT + * what the model actually serves; see + * served_concurrency. Operator surface + * only (/admin). */ + int served_concurrency; /* what this model ACTUALLY serves at + * once, read from bs_get_load(): the + * scheduler's slot count, or 1 when + * there is no scheduler (no CUDA build, + * QWEN35_BATCH_SLOTS off, bs_create + * failed, or the memory-pressure ladder + * clamped the slots below the cap). + * + * This is the number /v1/models + * advertises as max_concurrent. It has + * to be the measured one: the incident + * serve advertised max_concurrent 2 on + * a CPU-only binary whose sched was + * NULL, so every second chat took the + * per-model trylock and got 429 while + * the API promised two. */ + int context_window; /* EFFECTIVE per-session context window in + * tokens (pn_effective_ctx_window): the + * configured cap after the memory + * ladder's runtime clamp. This is the + * number a CLIENT may plan against, and + * the one /v1/models advertises. */ + int context_window_configured; /* the operator's --ctx-cap resolution + * (pn_session_ctx_cap), BEFORE the + * ladder. Equal to context_window unless + * the ladder is clamping right now — + * operator surface only (/admin). */ + pn_model_graph_residency graphs; /* CUDA-graph residency snapshot: how + * many per-layer graph slots are LIVE + * vs FAILED vs INELIGIBLE. A failed + * capture still produces correct + * output, so this counter is the only + * thing that can tell an operator the + * serve quietly got slower. */ + pn_model_attn_residency attn; /* attention-path residency: how many + * attention layers are running ON-DIE + * vs on the host-softmax fallback, and + * why. Same argument as `graphs`, only + * sharper — the fallback is measured at + * 13.02 -> 1.22 tok/s and still returns + * the right answer. */ + bool minicpm_tool_dialect; /* mr_model_is_minicpm: the model speaks + * MiniCPM5's attribute tool dialect. + * /v1/models resolves the dialect from + * this + the name (tc_dialect_for_model). */ + int refs; +} mr_info; + +/* Initialize / destroy. mr_free unloads every resident model (draining each); + * call only when no more requests can arrive. */ +int mr_init(mr_registry *reg); +void mr_free(mr_registry *reg); + +/** + * Load a model and register it under `name` (NULL → file basename). `dies` / + * `n_dies` pin an explicit die-set; pass NULL/0 to auto-place via the die + * manager (or CPU-resident when it is not running). `reasoning_budget` is the + * per-model AUTO-path reasoning cap in tokens stored on the entry (0 = disabled). + * `max_sessions` caps the model's prefix cache / live-session count (RESIDENCY: + * how many named conversations may hold KV at once): <=0 → the BS_MAX_USERS + * default (byte-identical to the pre-cap behavior), otherwise clamped to + * [1, BS_MAX_USERS]. + * `max_concurrent` caps the batch scheduler's decode slots (CONCURRENCY: how + * many requests may decode simultaneously): <=0 → falls back to the resolved + * `max_sessions`, otherwise clamped to [1, BS_MAX_USERS]. Requests beyond it + * queue on the scheduler and then get the normal 429/503 backpressure — they + * are never silently admitted into a wider batch than the model was sized for. + * Duplicate names, a full table, placement/admission refusal, or a failed + * pn_load are all clean refusals: returns 0 on success, -1 with a short reason + * in err[errlen]. + * + * Loading runs on the caller's thread (a model load can take a while) and + * does NOT hold the registry lock while pn_load_on runs, so serving continues. + */ +int mr_load(mr_registry *reg, const char *path, const char *name, + const pn_load_opts *opts, const int *dies, int n_dies, + int reasoning_budget, int max_sessions, int max_concurrent, + char *err, size_t errlen); + +/** + * Look up an entry by name and take a reference. NULL/empty name resolves to + * the default model (the first loaded). Returns NULL if unknown, unloading, + * or the registry is empty. Every successful mr_acquire MUST be paired with + * mr_release. + */ +mr_entry *mr_acquire(mr_registry *reg, const char *name); + +/** Drop a reference taken by mr_acquire (signals any draining unload). */ +void mr_release(mr_registry *reg, mr_entry *entry); + +/** PM 5: the serve's idle tick — pn_model_keepalive() on every single-user entry whose + * gen_lock is free (trylock; a generation in flight is skipped). @return NOPs sent. */ +int mr_keepalive(mr_registry *reg); + +/** Take `entry->gen_lock` for a generation: trylock, retried for up to 100 ms so the idle + * keepalive's few-millisecond hold is not a 429. @return 0 held, else the trylock error. */ +int mr_trylock_generation(mr_entry *entry); + +/** + * Unload `name`: refuse new acquires, wait for in-flight references to drain, + * release its die binding, and free the model + state. + * + * The drain is BOUNDED (PN_UNLOAD_DRAIN_MS, default 15000 ms). Success means + * the model is gone AND its dies are back: die binding released, scheduler + * slots and their per-slot KV rings destroyed, prefix cache and model freed. A + * drain that does not complete in budget is a REFUSAL, not a slow success — the + * entry is rolled back to fully resident and acquirable and nothing is freed. + * An unload never leaves a model deregistered with its dies still held. + * + * @return 0 on success; -1 with a reason in err[errlen] — unknown name, + * already unloading, or "model is busy: N reference(s) still in + * flight" (retry once the holders finish). + * + * Thread-safe: yes (registry lock) + * Reentrant: no + * Signal-safe: no + */ +int mr_unload(mr_registry *reg, const char *name, char *err, size_t errlen); + +/** + * The model-identity tag of a held entry (session warm-tier, Phase 7): a stable + * value derived from the model file's path + size + mtime at load. An exported + * session blob carries it; a restore refuses a blob whose tag differs (the + * weights changed underneath). Must be called on an entry the caller holds a + * reference to (mr_acquire). Returns 0 for a NULL entry. + */ +uint64_t mr_weights_version(const mr_entry *entry); + +/** Number of resident (acquirable) models. */ +int mr_count(mr_registry *reg); + +/** + * True when `model` speaks MiniCPM5's tool dialect: a plain-Llama arch whose chat + * template prepends bos (the MiniCPM5 orchestrator's signature — N2 is + * PN_ARCH_QWEN35MOE, so the Qwen/N2 path is never mistaken for it). NULL → false. + * + * Thread-safe: yes (reads the model config) + */ +bool mr_model_is_minicpm(const pn_model *model); + +/** Snapshot up to `max` resident models into out[]; returns the count. */ +int mr_snapshot(mr_registry *reg, mr_info *out, int max); + +#endif /* PN_SERVE_MODEL_REGISTRY_H */ diff --git a/src/serve/model_session.c b/src/serve/model_session.c new file mode 100644 index 0000000..6dba184 --- /dev/null +++ b/src/serve/model_session.c @@ -0,0 +1,187 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file model_session.c + * @brief The session-state facade — see model_session.h for the contract and + * for why serve code must not reach past it into `entry->prefix_cache` + * or `entry->sched`. + * + * Lock discipline (identical on every operation here): + * - BATCHED path: the scheduler owns the state, and `bs_session_*` take + * `sched->mu` + the GPU turnstile themselves. `pc_lock` is still taken + * around the prefix-cache bookkeeping calls (admit/touch) because those + * run concurrently from request threads. + * - SINGLE-USER path: `gen_lock` serializes the cache ops against that + * model's generation (the cache is documented thread-unsafe and the + * single-user generation mutates the very states we touch). + */ + +#include "model_session.h" + +#include +#include +#include + +#include "batch_sched.h" +#include "../core/prefix_cache.h" +#include "../core/session_snapshot.h" + +uint64_t ms_now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; +} + +uint64_t ms_session_idle_ttl_ms(void) +{ + static uint64_t cached = 0; + static int loaded = 0; + if (!loaded) { + const char *env = getenv("PN_SESSION_IDLE_TTL_MS"); + if (env && *env) { + char *end = NULL; + long v = strtol(env, &end, 10); + cached = (end != env && *end == '\0' && v >= 0) + ? (uint64_t)v : 12u * 60u * 60u * 1000u; + } else { + cached = 12u * 60u * 60u * 1000u; /* 12 hours */ + } + loaded = 1; + } + return cached; +} + +/* Map a scheduler result onto the facade's. */ +static ms_result ms_from_bs(bs_session_result r) +{ + switch (r) { + case BS_SESSION_OK: return MS_OK; + case BS_SESSION_NOT_FOUND: return MS_NOT_FOUND; + case BS_SESSION_BUSY: return MS_BUSY; + default: return MS_ERROR; + } +} + +ms_result ms_session_export(mr_entry *entry, const char *sid, + void **snap_out, uint32_t **tokens_out, + int *n_tok_out) +{ + if (snap_out) *snap_out = NULL; + if (tokens_out) *tokens_out = NULL; + if (n_tok_out) *n_tok_out = 0; + if (!entry || !sid || !sid[0] || !snap_out) + return MS_ERROR; + + /* ── Batched arm: the session lives in its warm decode slot. ── */ + if (entry->sched) + return ms_from_bs(bs_session_export(entry->sched, sid, snap_out, + tokens_out, n_tok_out)); + + /* ── Single-user arm: the session lives in the prefix cache. ── */ + if (!entry->prefix_cache) + return MS_NO_CACHE; + + pthread_mutex_lock(&entry->gen_lock); + int n_tok = 0; + const uint32_t *tok_ref = NULL; + void *snap = pn_prefix_cache_export_snapshot(entry->prefix_cache, sid, + &n_tok, &tok_ref); + /* Copy the committed ids while STILL under the lock: tok_ref points into + * the live entry and is only valid until an in-flight turn mutates it. + * `snap` is already a deep copy, so it is safe post-unlock. */ + uint32_t *tokens = NULL; + int oom = 0; + if (snap && n_tok > 0 && tok_ref) { + tokens = (uint32_t *)malloc((size_t)n_tok * sizeof(uint32_t)); + if (tokens) memcpy(tokens, tok_ref, (size_t)n_tok * sizeof(uint32_t)); + else oom = 1; + } + pthread_mutex_unlock(&entry->gen_lock); + + if (!snap) + return MS_NOT_FOUND; + if (oom) { + pn_session_snapshot_free(entry->model->config.arch, snap); + return MS_ERROR; + } + *snap_out = snap; + if (tokens_out) *tokens_out = tokens; else free(tokens); + if (n_tok_out) *n_tok_out = n_tok; + return MS_OK; +} + +ms_result ms_session_restore(mr_entry *entry, const char *sid, + const void *snap, const uint32_t *tokens, + int n_tok) +{ + if (!entry || !sid || !sid[0] || !snap || !tokens || n_tok <= 0) + return MS_ERROR; + + /* ── Batched arm. Note what this REPLACES: the old code imported into the + * prefix cache even for batched models, which allocated a fresh pn_state + * (and its VRAM KV cache) that no batched generation ever reads — the + * restore reported success and did nothing. ── */ + if (entry->sched) { + bs_session_result r = bs_session_restore(entry->sched, sid, snap, + tokens, n_tok); + if (r == BS_SESSION_ERROR) + return MS_BAD_SNAPSHOT; /* dim / pos mismatch → replay instead */ + return ms_from_bs(r); + } + + /* ── Single-user arm. Admit + import under ONE continuously-held lock: + * the admission reaper frees idle slots, so it must not race the import or + * a concurrent generation, and a gap between the two would let another + * request take the last slot so import LRU-evicts a live session. ── */ + if (!entry->prefix_cache) + return MS_NO_CACHE; + + pthread_mutex_lock(&entry->gen_lock); + pn_session_admit adm = pn_prefix_cache_admit(entry->prefix_cache, sid, + ms_now_ms(), + ms_session_idle_ttl_ms(), + NULL); + if (adm == PN_SESSION_ADMIT_AT_CAPACITY) { + pthread_mutex_unlock(&entry->gen_lock); + return MS_BUSY; + } + int rc = pn_prefix_cache_import_snapshot(entry->prefix_cache, sid, snap, + tokens, n_tok); + if (rc == 0) + pn_prefix_cache_touch(entry->prefix_cache, sid, ms_now_ms()); + pthread_mutex_unlock(&entry->gen_lock); + + return rc == 0 ? MS_OK : MS_BAD_SNAPSHOT; +} + +void ms_session_release(mr_entry *entry, const char *sid) +{ + if (!entry || !sid || !sid[0]) + return; + if (entry->sched) + bs_session_release(entry->sched, sid); + if (!entry->prefix_cache) + return; + /* The prefix cache still holds this model's admission bookkeeping on the + * batched path (and the whole session on the single-user path), so clear + * it either way. */ + pthread_mutex_t *lock = entry->sched ? &entry->pc_lock : &entry->gen_lock; + pthread_mutex_lock(lock); + pn_prefix_cache_release_session(entry->prefix_cache, sid); + pthread_mutex_unlock(lock); +} + +void ms_session_release_by_prefix(mr_entry *entry, const char *base_prefix) +{ + if (!entry || !base_prefix || !base_prefix[0]) + return; + if (entry->sched) + bs_session_release_by_prefix(entry->sched, base_prefix); + if (!entry->prefix_cache) + return; + pthread_mutex_t *lock = entry->sched ? &entry->pc_lock : &entry->gen_lock; + pthread_mutex_lock(lock); + pn_prefix_cache_release_by_prefix(entry->prefix_cache, base_prefix); + pthread_mutex_unlock(lock); +} diff --git a/src/serve/model_session.h b/src/serve/model_session.h new file mode 100644 index 0000000..e49f525 --- /dev/null +++ b/src/serve/model_session.h @@ -0,0 +1,138 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file model_session.h + * @brief THE session-state facade — one entry point per session-state + * operation, covering BOTH of the serve's session models. + * + * Why this file exists + * ==================== + * A resident model keeps its per-session state in one of two places, decided + * by whether the batch scheduler is active (`entry->sched`): + * + * - SINGLE-USER path (`sched == NULL`): one `pn_state` per session inside + * the model's `pn_prefix_cache`, serialized by `gen_lock`. + * - BATCHED path (`sched != NULL`): one `pn_state` per DECODE SLOT inside + * the scheduler, with the session bound to the slot it last finalized on + * (`bs_slot.sid` / `.hist`). On this path the `pn_prefix_cache` holds + * ONLY admission bookkeeping — nothing calls its acquire, so it has no + * entries and no session state whatsoever. + * + * Every session feature written against `entry->prefix_cache` alone therefore + * silently covers only the single-user path — which is the path a K80 host + * does not run. `/admin/session/export` returned `session_not_found` for + * every batched session for exactly this reason, and `/session/restore` was + * worse: it "succeeded" into a prefix-cache slot no batched generation ever + * reads, allocating a whole unused `pn_state` in the process. Session + * release had the same hole. + * + * The rule this file encodes: **serve code never touches `entry->prefix_cache` + * or `entry->sched` for a session operation — it calls an `ms_*` function.** + * Each one has both arms and takes the right lock, so a feature added here is + * a feature both paths get. Adding a session operation means adding it here, + * with both arms, or not at all. + * + * What is NOT unified (deliberate) + * ================================ + * The two STORES stay separate. Merging them — letting the scheduler's slots + * borrow the prefix cache's per-session `pn_state`s — mixes two ownership + * models (see the @todo in prefix_cache.h) and is an architecture change well + * beyond a session-export fix. What is unified is the CONTRACT: the same + * snapshot object, the same portable blob format, the same + * `weights_version` guard, and one call site per operation. + * + * @status Batched arm validated on a K80 host on saber-orch + nex-n2-mini. + * @issues The warm append-resume after a restore only fires when the client + * replays the committed token prefix exactly (the `tokens[]` array the + * export returns). Reasoning models whose clients omit the generated + * text from the replay fall back to the system-prefix boundary + * resume or a full prefill — pre-existing, identical on both paths. + * @todo None + */ + +#ifndef PN_SERVE_MODEL_SESSION_H +#define PN_SERVE_MODEL_SESSION_H + +#include + +#include "model_registry.h" + +/** + * Monotonic wall clock in milliseconds — the time source every session + * operation uses (the idle-TTL reaper's `now_ms`). Monotonic so an NTP step + * cannot spuriously reap or immortalize a session. + */ +uint64_t ms_now_ms(void); + +/** + * Long safety-backstop idle TTL in ms (default 12 h; `PN_SESSION_IDLE_TTL_MS` + * overrides, 0 disables). Idleness is NOT the normal release trigger — a + * session's slot is held by its `GET /session/watch` connection, which frees it + * when the client dies. This TTL only reaps a forgotten slot whose client never + * opened a watch and never returned. Read once, then cached. + */ +uint64_t ms_session_idle_ttl_ms(void); + +/* Outcome of a session-state operation. Maps 1:1 onto the HTTP answers so the + * two handlers cannot drift apart in how they report the same condition. */ +typedef enum { + MS_OK = 0, /* done */ + MS_NO_CACHE = -1, /* model has no session store at all → 400 */ + MS_NOT_FOUND = -2, /* no live session with that id → 404 */ + MS_BUSY = -3, /* session mid-turn, or at capacity → 429 */ + MS_BAD_SNAPSHOT = -4, /* snapshot did not fit this model → 400 */ + MS_ERROR = -5, /* allocation / GPU materialize failure → 500 */ +} ms_result; + +/** + * Export session `sid`'s state as an opaque qwen35moe snapshot plus the + * committed token history it covers. Read-only w.r.t. the session. + * + * Batched arm: the session's warm decode slot; the KV rows are materialized + * on demand from the GPU decode-slot ring (Phase 10b / INV3) — no host KV + * mirror is maintained or resurrected. + * Single-user arm: the model's prefix-cache entry for `sid`. + * + * @param snap_out Out: snapshot (free with pn_qwen35moe_state_snapshot_free). + * @param tokens_out Out: malloc'd COPY of the committed history (caller frees). + * @param n_tok_out Out: committed token count (== the snapshot's pos). + * + * Thread-safe: yes (takes the model's session lock internally). + */ +ms_result ms_session_export(mr_entry *entry, const char *sid, + void **snap_out, uint32_t **tokens_out, + int *n_tok_out); + +/** + * Restore a deserialized snapshot as session `sid`'s live state, positioned at + * pos == n_tok with `tokens[0..n_tok)` installed as the committed history, so + * the next chat turn on `sid` resumes off it. The caller MUST have validated + * the blob's `weights_version` against `mr_weights_version(entry)` first — the + * blob format carries the tag, this function does not re-check it. + * + * Thread-safe: yes. + */ +ms_result ms_session_restore(mr_entry *entry, const char *sid, + const void *snap, const uint32_t *tokens, + int n_tok); + +/** + * Release session `sid`: drop its state and return its capacity immediately + * rather than waiting out the idle TTL. Idempotent; no-op for an unknown + * session. Both arms, so a client's clean shutdown actually frees a batched + * session's warm slot instead of leaving its context bound to that slot. + * + * Thread-safe: yes. + */ +void ms_session_release(mr_entry *entry, const char *sid); + +/** + * Release EVERY session whose id starts with `base_prefix` — what a dead + * liveness watch fires (the client watches the BASE tab id while the live sessions + * are the mode-suffixed `-orch` / `-act` / `-chat` ids). Both arms. + * + * Thread-safe: yes. + */ +void ms_session_release_by_prefix(mr_entry *entry, const char *base_prefix); + +#endif /* PN_SERVE_MODEL_SESSION_H */ diff --git a/src/serve/openai_chat.c b/src/serve/openai_chat.c new file mode 100644 index 0000000..5122fcd --- /dev/null +++ b/src/serve/openai_chat.c @@ -0,0 +1,2404 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file openai_chat.c + * @brief Implementation of the OpenAI-compatible endpoint handlers + * (see openai_chat.h). + * + * Reasoning handling: the model emits …. We surface the thinking + * as a separate `reasoning_content` field (message.reasoning_content in the + * non-streaming reply; delta.reasoning_content in stream chunks) and the answer + * as `content` — matching the llama.cpp oracle's `--reasoning-format deepseek`, + * which is exactly what the user's opencode `nex` provider already consumes. + * + * Tool calling: when the request carries `tools`, they are rendered into the + * prompt in the resident model's tool dialect (see tool_calling.c) — the + * Qwen3-Coder XML form by default, or the MiniCPM5 form when + * the routed model is MiniCPM5 (chat_apply_tool_dialect swaps the preamble + * once the model is known). The model's tool-call output is parsed into + * structured calls, and the reply carries OpenAI `tool_calls` (non-stream) / + * the tool-call delta sequence (stream) with finish_reason "tool_calls". + * Chat-only requests are unchanged. + * + * Multi-model routing: the request's `model` field selects the resident + * registry entry (route_model below — single resident model always wins for + * back-compat; several require an exact name, else 404). Generation is + * serialized per entry (gen_lock trylock → 429), so different models serve + * concurrently. + * + * OpenAI sampling/decoding fields: the request's `response_format`, `seed`, + * `logit_bias`, `presence_penalty`, `frequency_penalty`, `logprobs` / + * `top_logprobs` and `n` are parsed here and threaded into sg_gen_params. They + * were previously accepted and silently ignored, which is worse than rejecting + * them — a client sending `response_format:{"type":"json_object"}` believed it + * was guaranteed JSON and quietly was not. Whatever the serve actually did with + * `response_format` comes back as a top-level `response_format_status`, and a + * short `n` comes back as `n_status`, on BOTH the streaming and non-streaming + * paths. + * + * Tools vs response_format: they are two masks over one decode and the sampler + * has one grammar hook, so TOOLS WIN and the client is told + * ("ignored_tools_present"). See resolve_response_format. + * + * Repetition collapse: a turn the decode loop stopped because the model was + * looping (repetition_loop_detector.h) comes back with finish_reason + * SG_FINISH_REPETITION_LOOP on BOTH paths, and that value OUTRANKS "tool_calls" + * — a caller must be able to tell a degenerated turn from a finished one, and a + * collapse reported as a tidy stop is the defect this exists to close. + * + * @status /v1/models and /v1/chat/completions (streaming + non-streaming), + * including tool calling and multi-step tool loops, work — now + * registry-routed (Phase 7 multi-model). The OpenAI sampling fields + * above all take effect on both paths. + * @issues `response_format:{"type":"json_schema"}` constrains the reply to valid + * JSON but does NOT enforce the schema's shape; it is reported as + * "schema_downgraded" rather than pretending to comply. + * `n` is capped at OAI_MAX_CHOICES (reported as "capped_to_8"), and each + * extra completion is a full re-decode with no prefix-cache reuse — on a + * K80 that is minutes per choice. + * @todo None + */ + +#include "openai_chat.h" + +#include +#include +#include +#include +#include + +#include "batch_sched.h" +#include "http_server.h" +#include "json_reader.h" +#include "session_key.h" +#include "attn_residency_json.h" +#include "graph_residency_json.h" +#include "json_writer.h" +#include "model_registry.h" +#include "request_profile.h" +#include "model_session.h" +#include "serve_generation.h" +#include "serve_shutdown.h" +#include "tool_call_stream.h" +#include "tool_calling.h" +#include "base64.h" +#include "../accel/backend_identity.h" +#include "../core/engine.h" +#include "../core/mem/pn_mem_state.h" +#include "../core/mem/pn_memwatch.h" +#include "../core/prefix_cache.h" +#include "../core/session_snapshot.h" +#include "../models/qwen35moe.h" + +#define OAI_DEFAULT_TEMPERATURE 0.7f +#define OAI_DEFAULT_TOP_P 0.95f +#define OAI_DEFAULT_TOP_K 40 +#define OAI_DEFAULT_MAX_TOKENS 1024 + +/* Ceiling on OpenAI `n`. Every extra completion is another full decode on a + * K80 — minutes, and another decode slot — so an unbounded `n` is a trivial way + * to wedge the fleet. Over the ceiling the request is NOT refused and NOT + * silently trimmed: it returns this many completions and says so in `n_status`. */ +#define OAI_MAX_CHOICES 8 +#define OAI_MAX_CHOICES_STR "8" + +/* Parsed request: sampling params + message array + stream flag + the model + * routing name. Owns the messages/stop arrays it allocates and any synthesized + * message-content strings (`owned`). Borrowed strings point into the jr_value + * tree, which must outlive this struct. */ +typedef struct { + sg_message *messages; + int n_messages; + sg_gen_params gen; + bool stream; + bool include_usage; /* stream_options.include_usage: emit a + * terminal usage chunk on the SSE stream + * (OpenAI-standard; needed for metering). */ + const char *model; /* borrowed "model" field (NULL if absent) */ + const char *session_id; /* borrowed "session_id" body field (NULL if + * absent); PC0.1 prefix-cache key. */ + const jr_value *tools; /* borrowed; NULL if absent or tool_choice=none */ + char **owned; /* synthesized content strings to free */ + int n_owned; + int preamble_slot; /* messages[] index of the synthetic tools + * system turn, or -1 if none — lets the + * post-route dialect fixup replace it. */ + const char *sys_content; /* borrowed leading system message content + * merged into the preamble (NULL if none). */ + const jr_value *raw_messages; /* borrowed request messages[] DOM (outlives + * this struct); re-walked post-route by + * chat_apply_tool_dialect to re-render + * assistant tool_calls in the model's dialect. */ + int msg_start; /* index in raw_messages where the copied + * history begins (1 past a leading system + * message when the tools preamble absorbed + * it, else 0) — maps messages[] back to + * raw_messages for the re-render. */ + bool reasoning_budget_set; /* request carried a reasoning_budget / + * max_reasoning_tokens field — its value in + * gen.reasoning_budget then OVERRIDES the + * per-model default (0 = explicit disable). */ + const char *response_format_status; /* what actually happened to the + * request's `response_format` — "applied", + * "schema_downgraded", "ignored_tools_present" + * or "ignored_unknown_type". NULL when the + * request carried no response_format. Static + * strings; reported on BOTH the streaming and + * non-streaming wire so a client never has to + * guess whether its guarantee held. */ + int n_choices; /* OpenAI `n` — how many completions to + * produce (default 1). Each one is a full + * decode, so n > 1 costs n x the generation + * time; see OAI_MAX_CHOICES. */ + const char *n_status; /* NULL, or why the delivered choice count + * differs from the requested `n` (static + * string, reported on the wire). */ + pn_logit_bias_entry *logit_bias; /* OWNED parsed `logit_bias` map; gen.logit_bias + * borrows it, so it must outlive the completion + * (freed in chat_request_free). */ +} chat_request; + +/* Take ownership of `s` for later free and return it as a borrowed pointer. + * Frees `s` and returns NULL on allocation failure. */ +static const char *req_own(chat_request *req, char *s) +{ + if (!s) return NULL; + char **grown = realloc(req->owned, (size_t)(req->n_owned + 1) * sizeof(char *)); + if (!grown) { free(s); return NULL; } + req->owned = grown; + req->owned[req->n_owned++] = s; + return s; +} + +/* Generate a short pseudo-unique completion id (chatcmpl-XXXXXXXX). */ +static void make_completion_id(char *buf, size_t len) +{ + static const char hex[] = "0123456789abcdef"; + char rnd[9]; + for (int i = 0; i < 8; i++) rnd[i] = hex[rand() & 0xF]; + rnd[8] = '\0'; + snprintf(buf, len, "chatcmpl-%s", rnd); +} + + + +/* Measured cold-prefill throughput (tokens/sec) for the restart estimate the + * serve reports on a capacity refuse. Measured K80 prefill sits ~17 tok/s + * (Qwen3.6 Q3_K); a conservative single constant keeps the estimate honest + * without a per-model probe (Phase 3 can refine). Override via + * PN_PREFILL_TOK_PER_S. */ +static double oai_prefill_tok_per_s(void) +{ + const char *env = getenv("PN_PREFILL_TOK_PER_S"); + if (env && *env) { + char *end = NULL; + double v = strtod(env, &end); + if (end != env && *end == '\0' && v > 0.0) + return v; + } + /* Phase 7: prefer the live measured rate once we have one; fall back to the + * conservative constant until the first real prefill lands. */ + double measured = sg_prefill_tok_per_s_measured(); + if (measured > 0.0) + return measured; + return 17.0; +} + +/* Collect stop strings from the request "stop" field (string or array). Returns + * a malloc'd array of borrowed const char* (caller frees the array, not the + * strings); sets *n. */ +static char **parse_stops(const jr_value *stop, int *n) +{ + *n = 0; + if (!stop) return NULL; + if (jr_kind(stop) == JR_STRING) { + char **arr = malloc(sizeof(char *)); + if (!arr) return NULL; + arr[0] = (char *)jr_as_string(stop); + *n = 1; + return arr; + } + if (jr_kind(stop) == JR_ARRAY) { + int count = jr_array_len(stop); + if (count == 0) return NULL; + char **arr = malloc((size_t)count * sizeof(char *)); + if (!arr) return NULL; + for (int i = 0; i < count; i++) { + const char *s = jr_as_string(jr_array_get(stop, i)); + if (s) arr[(*n)++] = (char *)s; + } + return arr; + } + return NULL; +} + +/* ============================================================ + * OpenAI sampling / decoding field parsing + * ============================================================ */ + +/* Clamp a client-supplied number into [lo, hi], mapping NaN to 0. A raw + * (int)/(float) cast of an unbounded or NaN JSON number is undefined behaviour, + * and every one of these fields is client-controlled. + * + * NaN test is isnan(), not the `v == v` self-compare idiom: this TU builds + * with -ffast-math, which implies -ffinite-math-only, and the compiler folds + * `v == v` to constant true (proven — the whole check compiles to `return 0` + * unconditionally), so the self-compare form silently never detects NaN. */ +static double clamp_number(double v, double lo, double hi) +{ + if (isnan(v)) return 0.0; + if (v < lo) return lo; + if (v > hi) return hi; + return v; +} + +/* + * Resolve `response_format` against the rest of the request. + * + * Precedence rule (the one thing that MUST be defined rather than left to + * chance): a tool-call grammar and a response-format grammar are two masks over + * the same decode and the sampler has ONE hook — they cannot both run. When the + * request carries tools, TOOLS WIN: the model's job that turn is to decide + * whether to call one, and a JSON-object constraint would make the trained + * tool-call syntax unreachable, breaking the thing the fleet depends on. That + * is also OpenAI's shape — `response_format` describes the assistant's CONTENT, + * and a turn that produces `tool_calls` produces no content to constrain. The + * request is NOT refused: tools work normally and `response_format_status` comes + * back "ignored_tools_present" so the client can SEE it instead of inferring it + * from unparseable output. + * + * `json_schema` is accepted and decoded under the json_object constraint — the + * reply is valid JSON but its SHAPE is not enforced — and reported as + * "schema_downgraded". An unrecognized type reports "ignored_unknown_type". + * + * Sets req->gen.response_format and req->response_format_status. Must be called + * AFTER req->tools has been resolved. + */ +static void resolve_response_format(chat_request *req, const jr_value *root) +{ + const jr_value *rf = jr_object_get(root, "response_format"); + if (jr_kind(rf) != JR_OBJECT) return; /* absent → text, status NULL */ + + const char *type = jr_as_string(jr_object_get(rf, "type")); + if (!type || strcmp(type, "text") == 0) + return; /* the explicit default: nothing to constrain, nothing to say */ + + bool json_object = (strcmp(type, "json_object") == 0); + bool json_schema = (strcmp(type, "json_schema") == 0); + if (!json_object && !json_schema) { + req->response_format_status = "ignored_unknown_type"; + return; + } + if (req->tools) { + req->response_format_status = "ignored_tools_present"; + return; + } + req->gen.response_format = SG_RESPONSE_FORMAT_JSON_OBJECT; + req->response_format_status = json_schema ? "schema_downgraded" : "applied"; +} + +/* + * Parse `logit_bias` — an object mapping a decimal token-id string to a bias. + * + * Stored as an OWNED array on the request (gen.logit_bias borrows it) because + * the map must outlive the jr_value DOM the caller frees — the same lifetime + * hazard the forced tool name hit (BUG 2). A non-numeric key or value is + * skipped rather than failing the request. Biases are clamped to OpenAI's + * documented [-100, 100]. + */ +static void parse_logit_bias(chat_request *req, const jr_value *root) +{ + const jr_value *map = jr_object_get(root, "logit_bias"); + if (jr_kind(map) != JR_OBJECT) return; + int n = jr_object_member_count(map); + if (n <= 0) return; + + pn_logit_bias_entry *entries = calloc((size_t)n, sizeof(*entries)); + if (!entries) return; /* OOM → the bias is simply not applied */ + + int out = 0; + for (int i = 0; i < n; i++) { + const char *key = jr_object_key_at(map, i); + if (!key || !*key) continue; + char *end = NULL; + long id = strtol(key, &end, 10); + if (end == key || !end || *end != '\0' || id < 0) continue; + const jr_value *v = jr_object_value_at(map, i); + if (jr_kind(v) != JR_NUMBER) continue; + entries[out].token_id = (uint32_t)id; + entries[out].bias = + (float)clamp_number(jr_as_number(v, 0.0), -100.0, 100.0); + out++; + } + if (out == 0) { free(entries); return; } + req->logit_bias = entries; + req->gen.logit_bias = entries; + req->gen.n_logit_bias = out; +} + +/* + * Parse `seed`, `presence_penalty`, `frequency_penalty`, `logprobs`, + * `top_logprobs` and `logit_bias`. All optional; absent leaves the generation + * byte-identical to before these fields existed. + */ +static void parse_openai_sampling_fields(chat_request *req, const jr_value *root) +{ + const jr_value *seed = jr_object_get(root, "seed"); + if (jr_kind(seed) == JR_NUMBER) { + /* The wire value is a JSON double; clamp to the exactly-representable + * integer range so a huge seed maps to a stable stream rather than an + * undefined cast. */ + double sv = clamp_number(jr_as_number(seed, 0.0), + -9007199254740992.0, 9007199254740992.0); + req->gen.seed_set = true; + req->gen.seed = (uint64_t)(int64_t)sv; + } + + req->gen.presence_penalty = (float)clamp_number( + jr_as_number(jr_object_get(root, "presence_penalty"), 0.0), -2.0, 2.0); + req->gen.frequency_penalty = (float)clamp_number( + jr_as_number(jr_object_get(root, "frequency_penalty"), 0.0), -2.0, 2.0); + + req->gen.logprobs = jr_as_bool(jr_object_get(root, "logprobs"), false); + req->gen.logprobs_top = (int)clamp_number( + jr_as_number(jr_object_get(root, "top_logprobs"), 0.0), + 0.0, (double)PN_LOGPROBS_MAX_TOP); + + parse_logit_bias(req, root); + + /* `n`: each completion is a SEPARATE full decode of the same prompt, so the + * cost is linear in n and, on this hardware, minutes per extra choice. It + * is honoured rather than ignored, and the ceiling is a resource guard, not + * a silent clamp — a request over it still runs, delivers OAI_MAX_CHOICES + * completions, and is TOLD so in `n_status`. */ + req->n_choices = (int)clamp_number(jr_as_number(jr_object_get(root, "n"), 1.0), + 1.0, 1.0e6); + if (req->n_choices > OAI_MAX_CHOICES) { + req->n_choices = OAI_MAX_CHOICES; + req->n_status = "capped_to_" OAI_MAX_CHOICES_STR; + } +} + +/* + * The `response_format_status` actually put on the wire, resolved against what + * the generation DID rather than what the request asked for. + * + * `req->response_format_status` is the request-time INTENT ("applied" / + * "schema_downgraded" when we meant to constrain). Intent is not evidence: if + * the JSON grammar did not close a complete object, the reply is not JSON and + * saying "applied" would claim a guarantee that did not hold — the same + * silent-degradation shape as ignoring the field outright, just louder. So a + * constrain-intent turn that did not enforce reports "not_enforced" instead. + * The already-honest dispositions ("ignored_tools_present", + * "ignored_unknown_type") pass through unchanged. + */ +static const char *wire_response_format_status(const chat_request *req, + const sg_result *res) +{ + const char *intent = req->response_format_status; + if (!intent) return NULL; + bool meant_to_constrain = (strcmp(intent, "applied") == 0 || + strcmp(intent, "schema_downgraded") == 0); + if (!meant_to_constrain) return intent; + if (res && res->response_format_ok) return intent; + return "not_enforced"; +} + +/* Concatenate an assistant message's text content and the XML render of its + * tool_calls into one owned string, registered on the request. Returns the + * combined borrowed string (falls back to `content` on OOM). */ +static const char *combine_assistant_tool_calls(chat_request *req, + const char *content, + const jr_value *tool_calls, + tc_dialect dialect) +{ + bool content_empty = !content || !*content; + char *rendered = tc_render_assistant_tool_calls(tool_calls, content_empty, + dialect); + if (!rendered) return content ? content : ""; + size_t clen = content ? strlen(content) : 0; + size_t xlen = strlen(rendered); + char *combined = malloc(clen + xlen + 1); + if (combined) { + if (clen) memcpy(combined, content, clen); + memcpy(combined + clen, rendered, xlen); + combined[clen + xlen] = '\0'; + } + free(rendered); + const char *owned = req_own(req, combined); + return owned ? owned : (content ? content : ""); +} + +/* Build the sg_message[] the generator consumes. When `tools` is present the + * first message becomes a synthetic system turn carrying the tools preamble + * (merging any leading system message), and assistant history tool_calls are + * expanded back into the model's XML syntax. Returns 0 on success. */ +static int build_messages(const jr_value *msgs, chat_request *req, + const char **err) +{ + int n = jr_array_len(msgs); + /* +1 headroom for a possible synthetic system turn. */ + req->messages = calloc((size_t)n + 1, sizeof(sg_message)); + if (!req->messages) { *err = "out of memory"; return -1; } + + const jr_value *first = jr_array_get(msgs, 0); + const char *first_role = jr_as_string(jr_object_get(first, "role")); + bool first_is_system = first_role && strcmp(first_role, "system") == 0; + + int out = 0; + int start = 0; + req->preamble_slot = -1; + req->raw_messages = msgs; + if (req->tools) { + const char *sys = first_is_system + ? jr_as_string(jr_object_get(first, "content")) : NULL; + /* Render the DEFAULT (Qwen/N2) preamble here; a post-route fixup + * (chat_apply_tool_dialect) swaps it for the model's native dialect + * once the resident model is known (e.g. MiniCPM5). Record the slot + + * merged system content so the fixup can re-render. */ + req->sys_content = sys; + const char *preamble = req_own(req, tc_render_tools_system(req->tools, sys)); + req->messages[out].role = "system"; + req->messages[out].content = preamble ? preamble : ""; + req->preamble_slot = out; + out++; + if (first_is_system) start = 1; + } + + req->msg_start = start; + for (int i = start; i < n; i++) { + const jr_value *m = jr_array_get(msgs, i); + const char *role = jr_as_string(jr_object_get(m, "role")); + const char *content = jr_as_string(jr_object_get(m, "content")); + const jr_value *tool_calls = jr_object_get(m, "tool_calls"); + if (!role) role = "user"; + req->messages[out].role = role; + /* An assistant turn may carry back the `reasoning_content` this serve + * streamed for it. Replaying it into the prompt is what lets the prefix + * cache append-resume: the committed history's forced opener is + * otherwise unreproducible from `content` alone. Borrowed from the + * request DOM, exactly like `content`. */ + req->messages[out].reasoning = + jr_as_string(jr_object_get(m, "reasoning_content")); + if (strcmp(role, "assistant") == 0 && jr_kind(tool_calls) == JR_ARRAY && + jr_array_len(tool_calls) > 0) { + /* Render in the DEFAULT (Qwen XML) dialect here — the model is not + * yet routed. chat_apply_tool_dialect re-renders these slots in the + * model's native dialect once known (e.g. ornith's JSON). */ + req->messages[out].content = + combine_assistant_tool_calls(req, content, tool_calls, + TC_DIALECT_QWEN_XML); + } else { + req->messages[out].content = content ? content : ""; + } + out++; + } + req->n_messages = out; + return 0; +} + +/* Defined with its streaming sibling below; declared here because the default + * arming happens during the parse. */ +static int oai_shutdown_requested(void *ud); + +/* Parse the request DOM into a chat_request. Returns 0 on success, -1 with a + * human message in *err on failure. */ +static int parse_chat_request(const jr_value *root, chat_request *req, + const char **err) +{ + memset(req, 0, sizeof(*req)); + if (jr_kind(root) != JR_OBJECT) { *err = "request body must be a JSON object"; return -1; } + + const jr_value *messages = jr_object_get(root, "messages"); + if (jr_kind(messages) != JR_ARRAY || jr_array_len(messages) == 0) { + *err = "'messages' must be a non-empty array"; + return -1; + } + + /* tool_choice: string form ("auto"|"none"|"required") OR the object form + * {"type":"function","function":{"name":"X"}} pinning a specific function. + * Default (absent) is "auto". "none" disables tools entirely; "required" + * and the object form drive Phase-5 forced-call decoding (compel a call). */ + const jr_value *tc_val = jr_object_get(root, "tool_choice"); + const char *tc_str = jr_as_string(tc_val); /* NULL for the object form */ + sg_tool_choice_kind choice = SG_TOOL_CHOICE_AUTO; + const char *forced_name = NULL; + if (tc_str) { + if (strcmp(tc_str, "none") == 0) choice = SG_TOOL_CHOICE_NONE; + else if (strcmp(tc_str, "required") == 0) choice = SG_TOOL_CHOICE_REQUIRED; + else choice = SG_TOOL_CHOICE_AUTO; /* "auto" or unknown */ + } else if (jr_kind(tc_val) == JR_OBJECT) { + const char *type = jr_as_string(jr_object_get(tc_val, "type")); + const char *name = jr_as_string( + jr_object_get(jr_object_get(tc_val, "function"), "name")); + if (type && strcmp(type, "function") == 0 && name && *name) { + choice = SG_TOOL_CHOICE_FUNCTION; + /* OWN the forced name (BUG 2). A borrowed pointer into the request's + * jr_value DOM is a lifetime hazard: it aliases memory the caller + * frees with jr_free(root), so any change to the free ordering (or a + * path that retains gen.forced_name past the DOM) is a use-after-free. + * Copy it onto the request (freed in chat_request_free) so the string + * we keep is ours and never dangles. On OOM, req_own frees the copy + * and returns NULL → fall back to "required" (still forces a call). */ + forced_name = req_own(req, strdup(name)); + choice = forced_name ? SG_TOOL_CHOICE_FUNCTION + : SG_TOOL_CHOICE_REQUIRED; + } + } + + bool tools_disabled = (choice == SG_TOOL_CHOICE_NONE); + const jr_value *tools = jr_object_get(root, "tools"); + req->tools = (!tools_disabled && jr_kind(tools) == JR_ARRAY && + jr_array_len(tools) > 0) ? tools : NULL; + /* Arm the stream emitter's bare-"" is detected + * regardless). Avoids capturing a literal "gen.has_tools = (req->tools != NULL); + /* Borrow the tools DOM for grammar-constrained decoding (consumed when the + * grammar arms — sg_tool_grammar_arms; otherwise ignored). */ + req->gen.tools = req->tools; + /* Thread the resolved choice down to Phase-5 forced-call decoding. With no + * tools present, forcing is meaningless → fall back to AUTO. forced_name + * (when set) is request-OWNED (strdup'd above), so it stays valid for the + * whole completion regardless of the jr_value DOM's lifetime. */ + req->gen.tool_choice = req->tools ? choice : SG_TOOL_CHOICE_AUTO; + req->gen.forced_name = + (req->gen.tool_choice == SG_TOOL_CHOICE_FUNCTION) ? forced_name : NULL; + /* The DEFAULT is the shutdown check, not NULL: every turn — streaming or + * not — has to be stoppable by a SIGTERM, or the drain that precedes the + * accelerator teardown times out and the residencies are left held. The + * streaming paths replace it with the wider check that also folds in client + * disconnect. */ + req->gen.should_abort = oai_shutdown_requested; + req->gen.abort_ud = NULL; + + if (build_messages(messages, req, err) != 0) return -1; + + req->gen.temperature = (float)jr_as_number(jr_object_get(root, "temperature"), + OAI_DEFAULT_TEMPERATURE); + req->gen.top_p = (float)jr_as_number(jr_object_get(root, "top_p"), + OAI_DEFAULT_TOP_P); + req->gen.top_k = (int)jr_as_number(jr_object_get(root, "top_k"), + OAI_DEFAULT_TOP_K); + /* Accept both max_tokens and the newer max_completion_tokens. */ + double mt = jr_as_number(jr_object_get(root, "max_tokens"), + jr_as_number(jr_object_get(root, "max_completion_tokens"), + OAI_DEFAULT_MAX_TOKENS)); + /* Clamp: (int)mt on a huge/NaN value is UB, and downstream `max_tokens + 8` + * (reasoning-budget headroom) could signed-overflow. 1<<20 tokens is far past + * any real request. Non-positive falls to the serve default downstream. */ + if (!(mt >= 0.0)) mt = 0.0; /* NaN / negative → 0 (serve default) */ + if (mt > (double)(1 << 20)) mt = (double)(1 << 20); + req->gen.max_tokens = (int)mt; + req->gen.stop = parse_stops(jr_object_get(root, "stop"), &req->gen.n_stop); + /* Per-request reasoning-budget override (accept either name). When present it + * wins over the per-model default; a negative value is clamped to 0 (disabled). + * Absent → reasoning_budget_set stays false and the per-model default is used + * at handle time. gen.reasoning_budget defaults to 0 (memset above). */ + const jr_value *rb = jr_object_get(root, "reasoning_budget"); + if (jr_kind(rb) != JR_NUMBER) + rb = jr_object_get(root, "max_reasoning_tokens"); + if (jr_kind(rb) == JR_NUMBER) { + int v = (int)jr_as_number(rb, 0); + req->gen.reasoning_budget = v > 0 ? v : 0; + req->reasoning_budget_set = true; + } + /* Per-request thinking control. `enable_thinking` is the Qwen3.x chat + * template's own variable name (and llama.cpp's), and it is BINARY there — + * the template's generation prompt is either "\n" or the closed + * "\n\n\n\n". The template exposes no graded reasoning + * level, so we do not invent one: a `reasoning_effort` whose middle values + * all decoded to the same prompt would be a dial that does nothing. + * Absent or non-boolean → SG_THINKING_DEFAULT (0 from the memset), which is + * byte-identical to the pre-parameter serve. */ + const jr_value *et = jr_object_get(root, "enable_thinking"); + if (jr_kind(et) == JR_BOOL) + req->gen.thinking = jr_as_bool(et, true) ? SG_THINKING_ON + : SG_THINKING_OFF; + req->stream = jr_as_bool(jr_object_get(root, "stream"), false); + /* OpenAI stream_options.include_usage: when set, the stream ends with an + * extra chunk carrying `usage` (and empty `choices`), so a proxy can meter + * streamed generations (which otherwise carry no token counts). */ + req->include_usage = jr_as_bool( + jr_object_get(jr_object_get(root, "stream_options"), "include_usage"), + false); + req->model = jr_as_string(jr_object_get(root, "model")); + /* PC0.1: a body session key (borrowed) overrides the X-Session-Id header the + * caller passes in separately. `session_id`, `prompt_cache_key` (OpenAI) or + * `promptCacheKey` (opencode) — see session_key.h. */ + req->session_id = pn_body_session_key(root); + + /* OpenAI sampling/decoding fields. response_format resolves LAST because it + * has to see the settled `tools` (tools win the constrained-decode slot). */ + parse_openai_sampling_fields(req, root); + resolve_response_format(req, root); + return 0; +} + +static void chat_request_free(chat_request *req) +{ + for (int i = 0; i < req->n_owned; i++) free(req->owned[i]); + free(req->owned); + free(req->messages); + free(req->gen.stop); + free(req->logit_bias); +} + +/* Parse the model's raw tool-call text into structured calls, if the request + * allowed tools and the model emitted one. Sets *calls / *n (0 if none). */ +/* A parsed call carries nothing usable: no members at all, or every value blank + * once whitespace is discounted. Cheap string test — the arguments are the JSON + * this function just built, so "{}" is the whole of the degenerate case plus the + * empty/whitespace-valued forms that survive it. */ +static bool tc_args_degenerate(const char *args) +{ + if (!args) return true; + const char *p = args; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (p[0] != '{') return false; /* not an object — leave it alone */ + p++; + while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; + if (*p == '}') return true; /* {} — no members */ + /* Any member whose value is an empty or whitespace-only string. Walk the + * body properly rather than skipping a char class: a naive skip of + * ' ' \\ t n r would read {"path":"nnn"} as empty, because the escape letters + * are also ordinary letters. Only a BACKSLASH-introduced t/n/r/f is + * whitespace; a bare one is content. */ + for (const char *q = strstr(args, "\":"); q; q = strstr(q + 1, "\":")) { + const char *v = q + 2; + while (*v == ' ' || *v == '\t' || *v == '\n' || *v == '\r') v++; + if (*v != '"') continue; /* non-string value — fine */ + v++; + bool has_content = false; + for (; *v && *v != '"'; v++) { + if (*v == '\\' && v[1]) { + char e = *++v; + if (e != 't' && e != 'n' && e != 'r' && e != 'f') + has_content = true; /* \" \\ \/ \uXXXX … are content */ + continue; + } + if (*v != ' ' && *v != '\t' && *v != '\n' && *v != '\r') + has_content = true; + } + if (!has_content) return true; + } + return false; +} + +/* Parse the model's raw tool-call text into structured calls, if the request + * allowed tools and the model emitted one. Sets *calls / *n (0 if none). + * + * DIAGNOSTIC: when parsing yields a call with nothing usable in it, log the RAW + * text that produced it. The client only ever records the parsed result, so a + * `read {}` in the client's history is a dead end — there is no way to tell whether + * the model emitted an empty value, a mangled one that failed to extract, or a + * whole tool call in the wrong frame. This is the only place both halves exist + * at once. It fires only on the degenerate case, so it costs nothing on healthy + * traffic and cannot flood the log the way a per-step trace would. + * + * `err` (nullable) carries back the reason a call the model STARTED is absent + * from *calls — the abandoned shape and the other structurally-wrong ones. The + * caller puts it on the wire so the client can tell them from the empty-argument + * case, which still yields a call. + * + * NO-CALL CATCH-ALL. The emitter only fills `tool_raw` once it has seen a + * tool-call opener, so raw text with ZERO calls out of it means a frame was + * emitted and nothing survived parsing. That is the serve-side reading of + * qwen-code's MALFORMED_TOOL_CALL "finish_reason=tool_calls with no completed + * calls" (converter.ts:1699-1711) — our own finish_reason is derived from the + * call count, so it can never contradict it, but the diverted-and-lost text can. + * Raised only when the parser named no more specific reason. + * + * TRUNCATION GATE. This is also the only place the DECODING GRAMMAR's end-of-turn + * verdict (res->tool_grammar_verdict) meets the calls parsed out of the text, and + * a call the grammar never closed is refused here rather than handed over. The + * text alone cannot show it: a truncated call is a legal prefix of a good one, so + * it parses cleanly into a call with a required argument silently absent. */ +/* THE DEFECT THE WHOLE DIAGNOSTIC LAYER ABOVE CANNOT SEE. + * + * Everything in collect_tool_calls hangs off `res->tool_raw`, and the emitter + * only fills that once it recognises an OPENER ("", a plausible + * "\n\nbash\n + * command + * sed -n '175,200p' …\n\n + * + * `no_call_parsed` does NOT cover this: that code fires when a frame WAS + * diverted and nothing parsed out of it. Here nothing was ever diverted. The + * two silences are different and the operator needs to tell them apart. + * + * The test is deliberately crude — does the answer text carry a tool-call tag at + * all — because it only ever runs on a turn that produced NO call, and a real + * opener in `content` is impossible by construction (the emitter would have + * taken it). What is left is exactly the invented dialects. */ +static void note_content_tool_syntax(const char *content) +{ + static const char *const MARKERS[] = { + "", + }; + if (!content || !*content) return; + for (size_t i = 0; i < sizeof(MARKERS) / sizeof(*MARKERS); i++) { + const char *hit = strstr(content, MARKERS[i]); + if (!hit) continue; + size_t len = strlen(content); + fprintf(stderr, "[tool_syntax_leak] the answer carries tool-call syntax " + "that matched no opener, so it was never diverted and no call " + "was parsed — marker='%s' content_len=%zu content=<<<%.600s%s>>>\n", + MARKERS[i], len, content, len > 600 ? "…[truncated]" : ""); + return; + } +} + +static void collect_tool_calls(const chat_request *req, const sg_result *res, + tc_tool_call **calls, int *n, tc_parse_error *err) +{ + *calls = NULL; + *n = 0; + if (err) *err = (tc_parse_error){0}; + if (req->tools && !(res->tool_raw && *res->tool_raw)) + note_content_tool_syntax(res->content); + if (req->tools && res->tool_raw && *res->tool_raw) { + tc_parse_repair repair = {0}; + tc_parse_tool_calls_diagnosed(res->tool_raw, req->tools, req->gen.dialect, + calls, n, err, &repair); + size_t rawlen = strlen(res->tool_raw); + /* Every repair is logged, and only ever after one fired, so healthy + * traffic costs nothing and the count is a straight grep. `added` is the + * whole story of what was assumed: a '"' in it means a string value was + * closed early and may be short. */ + if (repair.count > 0) + fprintf(stderr, + "[tool_json_repair] repairs=%d added=<<<%s>>> " + "repaired=<<<%.600s>>> raw_len=%zu raw=<<<%.600s%s>>>\n", + repair.count, repair.added ? repair.added : "", + repair.repaired_json ? repair.repaired_json : "", + rawlen, res->tool_raw, rawlen > 600 ? "…[truncated]" : ""); + tc_parse_repair_free(&repair); + /* TRUNCATION GATE. The grammar entered a call and the turn ended before + * it closed, so whatever the parser just built is the wreckage of a call + * the model never finished — and it does not LOOK like wreckage: a + * truncated emission is a legal prefix, so it parses into a named call + * with a required argument quietly absent (`read {}`, `write` with no + * `content`). The serve knew it was incomplete and emitted it anyway; + * that is the defect. Drop the calls and say why, on the same channel as + * every other "a call the model started is deliberately not here" reason. + * + * Placed BEFORE the no_call_parsed catch-all so the specific reason wins, + * and it does not overwrite a reason the parser already named (abandoned, + * leaked tail, …) — those are the same event seen closer up. + * + * Nothing is masked to keep the model generating. A model that has + * stopped, forced to continue, produces a corrupted file instead of a + * clean failure; the client retries on this code instead. */ + if (res->tool_grammar_verdict == SG_TOOL_GRAMMAR_INCOMPLETE) { + char msg[256]; + /* Copied, not borrowed: the calls array owns `name` and is freed + * immediately below, before `err` is filled in. */ + char *fname = (*n > 0 && (*calls)[0].name) + ? strdup((*calls)[0].name) : NULL; + snprintf(msg, sizeof(msg), + "%s: call incomplete — the decoding grammar was still inside " + "the call when the turn ended", fname ? fname : "tool call"); + tc_tool_calls_free(*calls, *n); + *calls = NULL; + *n = 0; + if (err && err->kind == TC_PARSE_OK) { + err->kind = TC_PARSE_CALL_INCOMPLETE; + err->function_name = fname; /* ownership moves to `err` */ + err->message = strdup(msg); + } else { + free(fname); + } + } + if (err && err->kind == TC_PARSE_OK && *n == 0) { + err->kind = TC_PARSE_NO_CALL_PARSED; + err->message = strdup("a tool-call frame was emitted but no call " + "could be parsed out of it"); + } + if (err && err->kind != TC_PARSE_OK) + fprintf(stderr, "[tool_call_dropped] code=%s %s raw_len=%zu raw=<<<%.600s%s>>>\n", + tc_parse_failure_code(err->kind), + err->message ? err->message : "(no message)", + rawlen, res->tool_raw, rawlen > 600 ? "…[truncated]" : ""); + for (int i = 0; i < *n; i++) { + if (!tc_args_degenerate((*calls)[i].arguments)) + continue; + fprintf(stderr, + "[tool_degenerate] tool=%s args=%s raw_len=%zu raw=<<<%.600s%s>>>\n", + (*calls)[i].name ? (*calls)[i].name : "?", + (*calls)[i].arguments ? (*calls)[i].arguments : "(null)", + rawlen, res->tool_raw, rawlen > 600 ? "…[truncated]" : ""); + } + } +} + +/* Re-render every assistant-history tool_calls turn in `dialect` (the initial + * pass in build_messages rendered them as Qwen XML, before the model was known). + * Walks raw_messages in step with the flattened messages[] so each combined + * "text + rendered calls" slot is replaced. Only needed for a non-XML dialect; + * a no-op run cost is avoided by the caller. */ +static void rerender_assistant_history(chat_request *req, tc_dialect dialect) +{ + int n = jr_array_len(req->raw_messages); + int out = req->preamble_slot >= 0 ? 1 : 0; /* slot 0 is the tools preamble */ + for (int i = req->msg_start; i < n; i++, out++) { + const jr_value *m = jr_array_get(req->raw_messages, i); + const char *role = jr_as_string(jr_object_get(m, "role")); + const jr_value *tool_calls = jr_object_get(m, "tool_calls"); + if (!role || strcmp(role, "assistant") != 0) continue; + if (jr_kind(tool_calls) != JR_ARRAY || jr_array_len(tool_calls) == 0) + continue; + const char *content = jr_as_string(jr_object_get(m, "content")); + req->messages[out].content = + combine_assistant_tool_calls(req, content, tool_calls, dialect); + } +} + +/* Post-route dialect resolver + fixup. The tools preamble + assistant history + * were rendered in the default (Qwen/N2) dialect before the model was known. + * Once routed, resolve the resident model's dialect (tc_dialect_for_model) and, + * when it differs from the XML default, install the model's native prompt + * preamble and re-render its assistant history. Stores the resolved dialect on + * req->gen so the emitter, grammar setup, and parser all speak it. No-op for a + * tools-less request beyond storing the dialect. + * + * Phase 2: ornith's `tool_choice:"required"`/named-function is NO LONGER + * downgraded to AUTO — the schema-constraining JSON grammar (tool_grammar_json, + * built in sg_tg_setup for TC_DIALECT_ORNITH_JSON) honors `required` natively, + * so ornith runs FORCED exactly like saber does under its XML grammar. */ +static void chat_apply_tool_dialect(chat_request *req, const pn_model *model, + const char *model_name) +{ + tc_dialect dialect = + tc_dialect_for_model(model_name, mr_model_is_minicpm(model)); + req->gen.dialect = dialect; + if (!req->tools) return; + + if (req->preamble_slot < 0) return; /* no tools preamble to swap */ + char *preamble = NULL; + if (dialect == TC_DIALECT_MINICPM_XML) + preamble = tc_render_tools_system_minicpm(req->tools, req->sys_content); + else if (dialect == TC_DIALECT_ORNITH_JSON) + preamble = tc_render_tools_system_ornith(req->tools, req->sys_content); + if (preamble) { + const char *owned = req_own(req, preamble); + if (owned) req->messages[req->preamble_slot].content = owned; + } + /* Assistant history was rendered XML in build_messages; re-render for a + * non-XML dialect so a multi-turn loop feeds the model its own dialect. */ + if (dialect == TC_DIALECT_ORNITH_JSON) + rerender_assistant_history(req, dialect); +} + +/* ============================================================ + * GET /v1/models + * ============================================================ */ + +void oai_handle_models(int client_fd, mr_registry *reg) +{ + mr_info info[MR_MAX_MODELS]; + int n = mr_snapshot(reg, info, MR_MAX_MODELS); + + json_writer jw; + if (!jw_init(&jw)) { hs_send_error(client_fd, 500, "server_error", "oom"); return; } + /* `backend` / `build` — the compiled accelerator backend and the commit, + * where a CLIENT can read them. Additive top-level keys: an OpenAI-shaped + * client walks `data` and never sees them, and anyone measuring this serve + * can now tell a CUDA answer from a CPU answer without `ldd` on a remote + * box. The completion text is byte-identical either way — which is exactly + * why nothing downstream could ever catch it. */ + jw_raw(&jw, "{\"object\":\"list\",\"backend\":"); + jw_string(&jw, pn_backend_token()); + jw_raw(&jw, ",\"build\":"); + jw_string(&jw, pn_backend_build_commit()); + jw_raw(&jw, ",\"data\":["); + int emitted = 0; + for (int i = 0; i < n; i++) { + /* Dedupe by name: a model placed on several cards (multi-card LB) + * appears ONCE — clients see one logical model id, not per-card copies. */ + bool seen = false; + for (int j = 0; j < i; j++) + if (strcmp(info[j].name, info[i].name) == 0) { seen = true; break; } + if (seen) + continue; + if (emitted++) jw_raw(&jw, ","); + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, info[i].name); + /* `max_concurrent` is the SERVED concurrency (bs_get_load's capacity), + * not the configured cap. A client uses this number to decide how many + * requests to have in flight, so advertising a cap the serve cannot + * honour is worse than advertising nothing: the incident serve promised + * 2 while its scheduler was NULL (no CUDA compiled in), so every second + * chat hit the per-model trylock and came back 429. The configured cap + * is still visible to an OPERATOR on /admin/models — same split as + * context_window vs context_window_configured, for the same reason. */ + jw_printf(&jw, ",\"object\":\"model\",\"created\":0,\"owned_by\":\"pantheon\"" + ",\"max_sessions\":%d,\"max_concurrent\":%d", + info[i].max_sessions, info[i].served_concurrency); + /* `context_window` — the per-session window a client may actually use, + * in tokens. EFFECTIVE, not the operator's --ctx-cap: it is the + * post-ladder number, the same one this model's completions report as + * `kv_cap`, so the client's plan and the serve's enforcement agree. + * + * Additive, and omitted when unknown (0): a plain OpenAI-compatible + * client ignores the extra key, and a client that reads it never has to + * special-case a zero. The name matches the client's own `context_window` — + * see docs/reference/http-api.md. */ + if (info[i].context_window > 0) + jw_printf(&jw, ",\"context_window\":%d", info[i].context_window); + /* `tool_choice_required_with_thinking` — a client may send + * tool_choice "required" with thinking ON and rely on it: the think + * block stays free and the turn ends in a parsed tool call (the + * think-gated grammar, sg_tool_choice_required_with_thinking). False + * for a dialect no grammar models (MiniCPM). Always present, + * true/false, so a client never has to guess from absence. */ + tc_dialect dialect = + tc_dialect_for_model(info[i].name, info[i].minicpm_tool_dialect); + jw_printf(&jw, ",\"tool_choice_required_with_thinking\":%s", + sg_tool_choice_required_with_thinking(dialect) ? "true" : "false"); + /* `graphs` — CUDA-graph residency, same additive/omit-when-absent rule. + * A lost graph costs speed only (output stays correct), so this is the + * one place a client or a poller can see that a model is running + * degraded. `ineligible` is NOT a fault: a model whose MoE experts are + * not Q3_K captures nothing by design. */ + gr_write_summary(&jw, &info[i].graphs); + /* `attention` — same rule again, and the sharper of the two: a layer + * on the host-softmax fallback returns identical tokens an order of + * magnitude slower. `host` > 0 means this model is serving degraded. */ + ar_write_summary(&jw, &info[i].attn); + jw_raw(&jw, "}"); + } + jw_raw(&jw, "]}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); +} + +/* ============================================================ + * POST /v1/chat/completions — non-streaming + * ============================================================ */ + +/* ============================================================ + * OpenAI `logprobs` on the wire + * ============================================================ */ + +/* Emit one token's entry: {"token":…,"logprob":…,"bytes":[…],"top_logprobs":[…]}. + * `bytes` is the token's raw UTF-8, which is how a client reconstructs a token + * that is not valid UTF-8 on its own (a split multi-byte character). */ +static void write_logprob_entry(json_writer *jw, const char *token, + float logprob, bool with_top, + const sg_token_logprob *lp) +{ + jw_raw(jw, "{\"token\":"); + jw_string(jw, token); + /* -inf/NaN are not valid JSON numbers; a token the model gave zero mass is + * reported at the float floor instead of poisoning the whole body. + * isnan(), not `logprob != logprob`: this TU builds with -ffast-math and + * the self-compare idiom folds to constant false (proven), so it never + * caught a NaN logprob and would have written literal `nan` into the + * JSON body. */ + if (isnan(logprob)) jw_raw(jw, ",\"logprob\":0"); + else if (logprob < -1.0e30f) jw_raw(jw, ",\"logprob\":-9999"); + else jw_printf(jw, ",\"logprob\":%.6f", logprob); + jw_raw(jw, ",\"bytes\":["); + for (const unsigned char *p = (const unsigned char *)(token ? token : ""); + *p; p++) { + if (p != (const unsigned char *)token) jw_raw(jw, ","); + jw_printf(jw, "%u", (unsigned)*p); + } + jw_raw(jw, "]"); + if (with_top && lp) { + jw_raw(jw, ",\"top_logprobs\":["); + for (int i = 0; i < lp->n_top; i++) { + if (i) jw_raw(jw, ","); + write_logprob_entry(jw, lp->top_token[i], lp->top_logprob[i], + false, NULL); + } + jw_raw(jw, "]"); + } + jw_raw(jw, "}"); +} + +/* Append `"logprobs":{"content":[…]}` — the per-choice OpenAI object. `entries` + * is an array of BORROWED per-token records. */ +static void write_logprobs_object(json_writer *jw, + const sg_token_logprob *const *entries, + int n) +{ + jw_raw(jw, ",\"logprobs\":{\"content\":["); + for (int i = 0; i < n; i++) { + if (i) jw_raw(jw, ","); + write_logprob_entry(jw, entries[i]->token, entries[i]->logprob, true, + entries[i]); + } + jw_raw(jw, "]}"); +} + +/* Non-streaming variant: the result owns a flat array rather than a pointer + * list, so the whole completion's logprobs are emitted in one go. */ +static void write_logprobs_object_flat(json_writer *jw, + const sg_token_logprob *entries, int n) +{ + jw_raw(jw, ",\"logprobs\":{\"content\":["); + for (int i = 0; i < n; i++) { + if (i) jw_raw(jw, ","); + write_logprob_entry(jw, entries[i].token, entries[i].logprob, true, + &entries[i]); + } + jw_raw(jw, "]}"); +} + +/* Append a "tool_calls":[…] array (the OpenAI function-call shape) to `jw`. */ +static void write_tool_calls_array(json_writer *jw, const tc_tool_call *calls, + int n) +{ + jw_raw(jw, "\"tool_calls\":["); + for (int i = 0; i < n; i++) { + if (i) jw_raw(jw, ","); + jw_raw(jw, "{\"id\":"); + jw_string(jw, calls[i].id); + jw_raw(jw, ",\"type\":\"function\",\"function\":{\"name\":"); + jw_string(jw, calls[i].name); + jw_raw(jw, ",\"arguments\":"); + jw_string(jw, calls[i].arguments); /* stringified JSON per OpenAI */ + jw_raw(jw, "}}"); + } + jw_raw(jw, "]"); +} + +/* Append a `tool_call_error` object: the structured reason a call the model + * STARTED is not in `tool_calls`. Its presence is the whole signal — a client + * seeing it knows the model truncated mid-call, which is a different event from + * a call that arrived with an empty argument (that one still comes through as a + * tool_call, with `{}` arguments) and from a turn that never called a tool at + * all (neither field present). */ +static void write_tool_call_error(json_writer *jw, const tc_parse_error *err) +{ + jw_raw(jw, "\"tool_call_error\":{\"code\":"); + jw_string(jw, tc_parse_failure_code(err->kind)); + if (err->function_name) { + jw_raw(jw, ",\"function\":"); + jw_string(jw, err->function_name); + } + if (err->parameter_name) { + jw_raw(jw, ",\"parameter\":"); + jw_string(jw, err->parameter_name); + } + if (err->message) { + jw_raw(jw, ",\"message\":"); + jw_string(jw, err->message); + } + jw_raw(jw, "}"); +} + +/* Did the repetition-loop detector end this turn? The finish_reason string is + * the whole signal (serve_generation.h), and both response shapes have to read + * it the same way — the last bug in this file was a field that landed on one + * transport and not the other. */ +static bool finish_is_repetition_loop(const sg_result *res) +{ + return res && res->finish_reason && + strcmp(res->finish_reason, SG_FINISH_REPETITION_LOOP) == 0; +} + +/* One completed choice: the generation result plus its parsed tool calls. `n` + * completions produce an array of these, and n == 1 is simply an array of one — + * the single-completion path is not special-cased anywhere below. */ +typedef struct { + sg_result res; + tc_tool_call *calls; + int n_calls; + tc_parse_error tool_error; /* why a started call is missing from `calls` */ +} chat_choice; + +/* Append one `choices[]` element. */ +static void write_choice(json_writer *jw, int index, const chat_choice *ch) +{ + const sg_result *res = &ch->res; + jw_printf(jw, "{\"index\":%d,\"message\":{\"role\":\"assistant\",\"content\":", + index); + /* A pure tool call carries content:null; any text before the call is kept. */ + if (ch->n_calls > 0 && (!res->content || !res->content[0])) + jw_raw(jw, "null"); + else + jw_string(jw, res->content); + if (res->reasoning && res->reasoning[0]) { + jw_raw(jw, ",\"reasoning_content\":"); + jw_string(jw, res->reasoning); + } + if (ch->n_calls > 0) { + jw_raw(jw, ","); + write_tool_calls_array(jw, ch->calls, ch->n_calls); + } + jw_raw(jw, "}"); + /* Per the OpenAI shape `logprobs` is a member of the CHOICE, beside + * `message` — not inside it. */ + if (res->logprobs && res->n_logprobs > 0) + write_logprobs_object_flat(jw, res->logprobs, res->n_logprobs); + jw_raw(jw, ",\"finish_reason\":"); + /* A repetition collapse OUTRANKS "tool_calls". If the turn degenerated, the + * caller has to learn that even when a call was parsed out of the wreckage — + * reporting "tool_calls" would hand back a call to execute and hide the one + * fact that explains the turn. Every other case keeps the pre-existing + * precedence (a parsed call wins over the generation's own stop reason). */ + jw_string(jw, (ch->n_calls > 0 && !finish_is_repetition_loop(res)) + ? "tool_calls" : res->finish_reason); + /* Beside `finish_reason`, in the CHOICE: it explains this completion's + * missing call, and with `n` > 1 each completion answers for itself. */ + if (ch->tool_error.kind != TC_PARSE_OK) { + jw_raw(jw, ","); + write_tool_call_error(jw, &ch->tool_error); + } + jw_raw(jw, "}"); +} + +static void send_completion_json(int client_fd, const char *id, long created, + const char *model_id, + const chat_choice *choices, int n_choices, + const char *response_format_status, + const char *n_status) +{ + if (n_choices <= 0) { + hs_send_error(client_fd, 500, "server_error", "no completion produced"); + return; + } + json_writer jw; + if (!jw_init(&jw)) { hs_send_error(client_fd, 500, "server_error", "oom"); return; } + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, id); + jw_printf(&jw, ",\"object\":\"chat.completion\",\"created\":%ld,\"model\":", created); + jw_string(&jw, model_id); + jw_raw(&jw, ",\"choices\":["); + for (int i = 0; i < n_choices; i++) { + if (i) jw_raw(&jw, ","); + write_choice(&jw, i, &choices[i]); + } + jw_raw(&jw, "]"); + + /* Usage across the whole request: the prompt was paid for ONCE (every choice + * decoded from the same prefill), the completions add up. */ + const sg_result *first = &choices[0].res; + int completion_total = 0; + for (int i = 0; i < n_choices; i++) + completion_total += choices[i].res.completion_tokens; + /* `session_conflict` is written INSIDE the usage object (hence the %s before + * its closing brace, not a jw_raw after it): it tells the client it is + * running two conversations under one session id, so the serve can resume + * neither and both re-prefill in full every turn. Only the client can fix + * that, so the signal has to be on the wire and not just in the serve log — + * and it has to be where the client looks, which is `usage`, the object it + * already reads for kv_used/kv_cap. Appending it after the brace instead put + * it beside `usage` rather than in it, and no client ever saw it. */ + /* `prompt_tokens_details.cached_tokens` is OpenAI's own spelling for "of + * those prompt tokens, this many were reused, not computed". UNCONDITIONAL + * (0 on a cold turn, never omitted): a client cannot tell a serve that + * reported no reuse from one that does not report reuse at all, so an absent + * field forces it back to prompt_tokens/TTFT — a prefill rate that rises + * with every cache hit. Absent and zero are different answers. */ + jw_printf(&jw, ",\"usage\":{\"prompt_tokens\":%d,\"completion_tokens\":%d," + "\"total_tokens\":%d," + "\"prompt_tokens_details\":{\"cached_tokens\":%d}," + "\"kv_used\":%d,\"kv_cap\":%d%s}", + first->prompt_tokens, completion_total, + first->prompt_tokens + completion_total, + first->cached_prompt_tokens, + first->kv_used_tokens, first->kv_cap_tokens, + first->session_conflict ? ",\"session_conflict\":true" : ""); + /* Tell the client when admission clamped its context (MemoryAware §6.2 + * rung 3): the oldest prompt tokens were dropped to the fitting window, so + * the model did not see the full conversation. Top-level, mirroring the + * streaming path, where it rides the closing chunk and not `usage`. */ + if (first->context_capped) + jw_raw(&jw, ",\"context_capped\":true"); + /* What actually happened to the request's `response_format`. Top-level and + * unconditional (not inside `usage`, which a streaming client only sees when + * it opted into stream_options.include_usage) so the answer is on the wire + * for EVERY client that sent the field — the point of the field is a + * guarantee, and a guarantee that silently did not apply is the bug. */ + if (response_format_status) { + jw_raw(&jw, ",\"response_format_status\":"); + jw_string(&jw, response_format_status); + } + /* Same rule for `n`: if the request asked for more completions than were + * delivered, say so rather than letting the client count choices and guess. */ + if (n_status) { + jw_raw(&jw, ",\"n_status\":"); + jw_string(&jw, n_status); + } + jw_raw(&jw, "}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); +} + +/* Free everything a choice owns. */ +static void chat_choice_free(chat_choice *ch) +{ + tc_tool_calls_free(ch->calls, ch->n_calls); + ch->calls = NULL; + ch->n_calls = 0; + tc_parse_error_free(&ch->tool_error); + sg_result_free(&ch->res); +} + +/* ============================================================ + * POST /v1/chat/completions — streaming (SSE) + * ============================================================ */ + +typedef struct { + int client_fd; + const char *id; + long created; + const char *model_id; + bool role_sent; + bool context_capped; /* admission clamped the prompt window — + * surfaced on the closing chunk(s) */ + const char *response_format_status; /* set just before the CLOSING chunk (so + * it rides that chunk and not all of them), + * mirroring context_capped. NULL = the request + * carried no response_format. */ + const tc_parse_error *tool_error; /* why a started call is absent from the + * tool-call deltas. Borrowed, set just before + * the CLOSING chunk so it rides that chunk — + * same placement rule as the two above. NULL + * (or TC_PARSE_OK) = nothing was abandoned. */ + int choice_index; /* OpenAI `n`: which completion these chunks + * belong to. Always 0 for the ordinary + * single-completion request, so the wire shape + * is unchanged for every client today. */ + tcs_streamer *tool_stream; /* this completion's tool-call streamer (set by + * the run paths; tool_call_stream.h) */ + double last_write_s; /* monotonic time of the last SSE write */ + double keepalive_s; /* silence that earns a keepalive comment while + * a tool call is being written; 0 = default */ +} stream_ctx; + +/* Longest silence on the wire while the model writes a tool call. Whatever + * cannot be streamed yet (tool_call_stream.h: a held object, an XML-dialect + * call) is covered by an SSE comment this often, far inside any client stall + * watchdog (typically tens of seconds). */ +#define OAI_TOOL_KEEPALIVE_S 5.0 + +static double oai_now_s(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +} + +/* Emit one chat.completion.chunk with the given delta body (already the inner + * JSON of "delta", e.g. "\"content\":\"hi\""). */ +static void send_chunk(stream_ctx *sc, const char *delta_json, + const char *finish_reason, + const sg_token_logprob *const *logprobs, int n_logprobs) +{ + json_writer jw; + if (!jw_init(&jw)) return; + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, sc->id); + jw_printf(&jw, ",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", + sc->created); + jw_string(&jw, sc->model_id); + jw_printf(&jw, ",\"choices\":[{\"index\":%d,\"delta\":{", sc->choice_index); + jw_raw(&jw, delta_json ? delta_json : ""); + jw_raw(&jw, "}"); + /* Same placement as the non-streaming body: a member of the CHOICE, beside + * `delta`. Carries only the tokens whose bytes are in THIS delta — the + * emitter holds a partial tail back, so a client cannot pair them by + * counting deltas. */ + if (logprobs && n_logprobs > 0) + write_logprobs_object(&jw, logprobs, n_logprobs); + jw_raw(&jw, ",\"finish_reason\":"); + if (finish_reason) jw_string(&jw, finish_reason); + else jw_raw(&jw, "null"); + /* Same member of the same object as the non-streaming body's copy, so a + * client reads the abandoned call the same way on both transports. */ + if (sc->tool_error && sc->tool_error->kind != TC_PARSE_OK) { + jw_raw(&jw, ","); + write_tool_call_error(&jw, sc->tool_error); + } + jw_raw(&jw, "}]"); + /* MemoryAware §6.2 rung 3: tell the streaming client its context was + * clamped (set only once generation finished, so it rides the closing + * chunk(s), mirroring the non-streaming `context_capped` field). */ + if (sc->context_capped) + jw_raw(&jw, ",\"context_capped\":true"); + /* Same signal as the non-streaming body's `response_format_status`, on the + * same terms: top-level, so a streaming client sees it whether or not it + * asked for a usage chunk. */ + if (sc->response_format_status) { + jw_raw(&jw, ",\"response_format_status\":"); + jw_string(&jw, sc->response_format_status); + } + jw_raw(&jw, "}"); + hs_send_sse(sc->client_fd, jw.data); + sc->last_write_s = oai_now_s(); + jw_free(&jw); +} + +/* Emit the OpenAI terminal usage chunk: an extra chat.completion.chunk with an + * empty `choices` array and a `usage` object. Sent last (just before [DONE]) + * only when the request set stream_options.include_usage, so a metering proxy + * can read token counts off the stream. Mirrors the non-streaming `usage`. + * KV Phase 1: also carries kv_used/kv_cap (the session's true KV occupancy vs + * its per-session cap) so the client can trigger compaction at ~80% BEFORE the + * reject-backstop fires. kv_used reflects committed KV — reasoning tokens the + * client never counts are in there, so this beats the client's own estimate. + * `cached_tokens` rides here too, and this is the emission that matters for it: + * benchmarks stream, so a fix to the non-streaming body alone would leave every + * measured prefill rate inflated. */ +static void send_usage_chunk(stream_ctx *sc, int prompt_tokens, + int completion_tokens, int cached_tokens, + int kv_used, int kv_cap, int session_conflict) +{ + json_writer jw; + if (!jw_init(&jw)) return; + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, sc->id); + jw_printf(&jw, ",\"object\":\"chat.completion.chunk\",\"created\":%ld,\"model\":", + sc->created); + jw_string(&jw, sc->model_id); + jw_printf(&jw, ",\"choices\":[],\"usage\":{\"prompt_tokens\":%d," + "\"completion_tokens\":%d,\"total_tokens\":%d," + "\"prompt_tokens_details\":{\"cached_tokens\":%d}," + "\"kv_used\":%d,\"kv_cap\":%d%s}}", + prompt_tokens, completion_tokens, + prompt_tokens + completion_tokens, cached_tokens, kv_used, kv_cap, + /* Same signal as the non-streaming body's "session_conflict": + * two conversations are sharing one session id, so neither can + * resume. Rides `usage` because that is the object a streaming + * client already reads for kv_used/kv_cap. */ + session_conflict ? ",\"session_conflict\":true" : ""); + hs_send_sse(sc->client_fd, jw.data); + jw_free(&jw); +} + +/* KV Phase 1: the machine-readable "context over budget" signal. The prompt + * exceeds the model's per-session KV cap; the serve refused rather than silently + * drop-oldest. The client reads type == "context_over_budget" + the + * kv_used/kv_cap numbers, compacts its history, and retries. 422 Unprocessable + * (not 503/429) tells the client this is NOT a transient blip to blind-retry — + * it must shed context first. */ +static void send_context_over_budget_http(int client_fd, int kv_used, + int kv_cap) +{ + char msg[224]; + snprintf(msg, sizeof(msg), + "context over budget: %d tokens exceeds the %d-token per-session " + "cap; compact the conversation and retry (kv_used=%d kv_cap=%d)", + kv_used, kv_cap, kv_used, kv_cap); + hs_send_error(client_fd, 422, "context_over_budget", msg); +} + +/* SSE variant: the stream is already open, so the reject must ride the stream as + * a terminal error event carrying the same machine-readable type + kv numbers, + * then the stream ends. */ +static void send_context_over_budget_sse(int client_fd, int kv_used, int kv_cap) +{ + char json[320]; + snprintf(json, sizeof(json), + "{\"error\":{\"message\":\"context over budget; compact and " + "retry\",\"type\":\"context_over_budget\",\"code\":null," + "\"kv_used\":%d,\"kv_cap\":%d}}", + kv_used, kv_cap); + hs_send_sse(client_fd, json); + hs_end_sse(client_fd); +} + +static void ensure_role_sent(stream_ctx *sc) +{ + if (!sc->role_sent) { + send_chunk(sc, "\"role\":\"assistant\"", NULL, NULL, 0); + sc->role_sent = true; + } +} + +/* serve_generation delta callback: forward each token delta as an SSE chunk, + * carrying the logprobs of the tokens whose bytes this delta contains. */ +static void stream_delta_cb(const char *text, int is_reasoning, + const sg_token_logprob *const *logprobs, + int n_logprobs, void *ud) +{ + stream_ctx *sc = ud; + ensure_role_sent(sc); + json_writer jw; + if (!jw_init(&jw)) return; + jw_raw(&jw, is_reasoning ? "\"reasoning_content\":" : "\"content\":"); + jw_string(&jw, text); + send_chunk(sc, jw.data, NULL, logprobs, n_logprobs); + jw_free(&jw); +} + +/* tool_call_stream sink: the OpenAI opening delta of one call (index, id, + * type, name, empty arguments). Clients concatenate the argument fragments + * that follow under the same index. */ +static void stream_tool_call_begin(void *ud, int index, const char *id, + const char *name) +{ + stream_ctx *sc = ud; + ensure_role_sent(sc); + json_writer jw; + if (!jw_init(&jw)) return; + jw_printf(&jw, "\"tool_calls\":[{\"index\":%d,\"id\":", index); + jw_string(&jw, id); + jw_raw(&jw, ",\"type\":\"function\",\"function\":{\"name\":"); + jw_string(&jw, name); + jw_raw(&jw, ",\"arguments\":\"\"}}]"); + send_chunk(sc, jw.data, NULL, NULL, 0); + jw_free(&jw); +} + +/* tool_call_stream sink: one argument fragment of call `index`. */ +static void stream_tool_call_arguments(void *ud, int index, const char *bytes, + size_t n) +{ + stream_ctx *sc = ud; + json_writer jw; + if (!jw_init(&jw)) return; + jw_printf(&jw, "\"tool_calls\":[{\"index\":%d,\"function\":{\"arguments\":", + index); + jw_string_n(&jw, bytes, n); + jw_raw(&jw, "}}]"); + send_chunk(sc, jw.data, NULL, NULL, 0); + jw_free(&jw); +} + +/* tool_call_stream sink: tool-call bytes were generated. If the wire has been + * silent for the keepalive interval (the part being written cannot be sent + * yet), send an SSE comment so a client watchdog sees a live stream. */ +static void stream_tool_call_progress(void *ud) +{ + stream_ctx *sc = ud; + double interval = sc->keepalive_s > 0 ? sc->keepalive_s : OAI_TOOL_KEEPALIVE_S; + double now = oai_now_s(); + if (now - sc->last_write_s < interval) return; + hs_send_sse_comment(sc->client_fd, "keepalive"); + sc->last_write_s = now; +} + +/* sg_gen_params.tool_tap → the streamer. */ +static void stream_tool_tap(const char *bytes, size_t n, void *ud) +{ + tcs_feed(ud, bytes, n); +} + +/* Set up `ts` to stream this completion's tool calls into `sc`, and arm the + * generation params so the emitter feeds it. */ +static bool stream_tool_calls_begin(stream_ctx *sc, tcs_streamer *ts, + const chat_request *req, sg_gen_params *g) +{ + tcs_sink sink = { stream_tool_call_begin, stream_tool_call_arguments, + stream_tool_call_progress, sc }; + if (!tcs_init(ts, req->gen.dialect, req->tools, &sink)) return false; + sc->tool_stream = ts; + g->tool_tap = stream_tool_tap; + g->tool_tap_ud = ts; + return true; +} + +/* Undo stream_tool_calls_begin. */ +static void stream_tool_calls_end(stream_ctx *sc, tcs_streamer *ts) +{ + tcs_free(ts); + sc->tool_stream = NULL; +} + +/* + * Generation params for choice `index` of an `n`-completion request. + * + * A `seed` makes the REQUEST reproducible — it does not mean the n completions + * should be identical, which is what reusing one RNG stream would produce. So + * each choice offsets the seed; re-running the same request with the same seed + * still replays all n completions exactly. (With temperature 0 every choice is + * the argmax and they coincide regardless — that is inherent to greedy decoding, + * not something the seed can fix.) + */ +static sg_gen_params gen_params_for_choice(const chat_request *req, int index) +{ + sg_gen_params g = req->gen; + if (g.seed_set) g.seed += (uint64_t)index; + return g; +} + +/* should_abort for a streaming turn: true once the client has disconnected, OR + * once the process has been asked to stop. ud is a pointer to the client fd + * (valid for the whole synchronous completion). + * + * The shutdown arm is what makes SIGTERM able to retire the card residencies: + * the accept loop cannot tear the accelerator down while a turn is still + * submitting descriptors against it, so the turn has to stop being asked to + * finish first. The generation loop polls this between prefill chunks and every + * 8 decode tokens, which bounds how long a stop waits. */ +static int oai_client_gone(void *ud) +{ + return pn_serve_shutdown_requested() || !hs_client_alive(*(const int *)ud); +} + +/* should_abort for a turn with no client-liveness check (the non-streaming + * paths, where a disconnect is not acted on): true only once the process has + * been asked to stop. Without this a non-streaming completion would run to its + * max_tokens through a SIGTERM and time the drain out — losing the retirement + * in exactly the mid-generation case it exists for. */ +static int oai_shutdown_requested(void *ud) +{ + (void)ud; + return pn_serve_shutdown_requested(); +} + +/* Emit one completion's closing chunks into an ALREADY-OPEN SSE stream: the + * tool-call deltas (if any) and the finish chunk. Shared by both run paths so a + * change to the closing shape cannot land on one and not the other — the last + * bug in this file was exactly that (a field on one path only). */ +static void stream_finish_choice(stream_ctx *sc, chat_request *req, + sg_result *res, int rc) +{ + ensure_role_sent(sc); + sc->context_capped = (rc == 0 && res->context_capped); + /* Rides the closing chunk(s), exactly like context_capped. Resolved against + * what the generation actually did — see wire_response_format_status. */ + sc->response_format_status = wire_response_format_status(req, res); + + tc_tool_call *calls = NULL; + int n_calls = 0; + tc_parse_error tool_error = {0}; + if (rc == 0) collect_tool_calls(req, res, &calls, &n_calls, &tool_error); + if (n_calls > 0) { + /* Whatever the streamer has not already put on the wire — all of it on + * a path or dialect that streams nothing early. */ + tcs_streamer fallback; + tcs_streamer *ts = sc->tool_stream; + if (!ts) { + tcs_sink sink = { stream_tool_call_begin, stream_tool_call_arguments, + NULL, sc }; + ts = tcs_init(&fallback, req->gen.dialect, NULL, &sink) ? &fallback : NULL; + } + if (ts) tcs_finish(ts, calls, n_calls); + if (ts == &fallback) tcs_free(&fallback); + /* Set AFTER the deltas so it rides the CLOSING chunk only, exactly like + * context_capped — a per-delta copy would repeat one event N times. */ + sc->tool_error = &tool_error; + /* Same precedence as the non-streaming body: a degenerated turn reports + * the degeneration even when a call was parsed out of it. */ + send_chunk(sc, "", finish_is_repetition_loop(res) ? res->finish_reason + : "tool_calls", + NULL, 0); + } else { + sc->tool_error = &tool_error; + send_chunk(sc, "", rc == 0 ? res->finish_reason : "stop", NULL, 0); + } + sc->tool_error = NULL; /* the borrow ends with this stack frame */ + tc_parse_error_free(&tool_error); + tc_tool_calls_free(calls, n_calls); +} + +/* + * Stream ONE completion into an already-open SSE stream (single-user path). + * + * The SSE lifecycle (hs_begin_sse / hs_end_sse) belongs to the CALLER: an `n` > 1 + * request streams several completions down one stream, told apart by + * choices[].index. Returns the sg rc so the caller can stop after a failure it + * has already reported on the stream. + */ +static int run_streaming(int client_fd, pn_model *model, pn_state *state, + chat_request *req, const sg_gen_params *gen, + stream_ctx *sc, const sg_prefix_ctx *pc) +{ + sg_gen_params g = *gen; + /* Free the model slot the instant the client disconnects (e.g. Esc) instead + * of finishing a cold prefill nobody will read — that lingering slot is what + * made the next turn hit a "busy" empty response. */ + g.should_abort = oai_client_gone; + g.abort_ud = (void *)&client_fd; + tcs_streamer ts; + bool tool_stream = stream_tool_calls_begin(sc, &ts, req, &g); + sg_result res = {0}; + int rc = sg_run_completion(model, state, req->messages, req->n_messages, + &g, stream_delta_cb, sc, &res, pc); + if (rc == SG_ABORTED) { + /* Client is gone and the slot is released; nothing to send. */ + fprintf(stderr, "[serve] client disconnected — turn aborted, slot released\n"); + } else if (rc == SG_CONTEXT_OVER_BUDGET) { + send_context_over_budget_sse(client_fd, res.kv_used_tokens, + res.kv_cap_tokens); + } else { + stream_finish_choice(sc, req, &res, rc); + if (req->include_usage) + send_usage_chunk(sc, res.prompt_tokens, res.completion_tokens, + res.cached_prompt_tokens, + res.kv_used_tokens, res.kv_cap_tokens, + res.session_conflict); + } + if (tool_stream) stream_tool_calls_end(sc, &ts); + sg_result_free(&res); + return rc; +} + +/* + * Stream ONE completion into an already-open SSE stream (C3 batched path). + * `slot` is a pre-acquired slot index (bs_slot_acquire already called); the + * completion takes ownership of it. SSE lifecycle belongs to the caller, as on + * the single-user path. + */ +static int run_streaming_batched(int client_fd, pn_model *model, + pn_batch_sched *sched, chat_request *req, + const sg_gen_params *gen, stream_ctx *sc, + int slot, const char *sid) +{ + sg_gen_params g = *gen; + /* Same arming as run_streaming, and for the same reason — except this is the + * path the fleet actually runs, so until now an abandoned request burned its + * whole cold prefill (~205 s at 8k) holding a decode slot. A foreign client + * with a fixed timeout would retry into a second slot and repeat it; the + * server, not the client, has to be the one that stops. */ + g.should_abort = oai_client_gone; + g.abort_ud = (void *)&client_fd; + tcs_streamer ts; + bool tool_stream = stream_tool_calls_begin(sc, &ts, req, &g); + sg_result res = {0}; + int rc = sg_run_completion_batched(model, sched, slot, req->messages, + req->n_messages, &g, + stream_delta_cb, sc, &res, sid); + if (rc == SG_ABORTED) { + /* Client is gone and the slot is released; nothing to send. */ + fprintf(stderr, "[serve] client disconnected — batched turn aborted, " + "slot released\n"); + } else if (rc == SG_CONTEXT_OVER_BUDGET) { + send_context_over_budget_sse(client_fd, res.kv_used_tokens, + res.kv_cap_tokens); + } else { + stream_finish_choice(sc, req, &res, rc); + if (req->include_usage) + send_usage_chunk(sc, res.prompt_tokens, res.completion_tokens, + res.cached_prompt_tokens, + res.kv_used_tokens, res.kv_cap_tokens, + res.session_conflict); + } + if (tool_stream) stream_tool_calls_end(sc, &ts); + sg_result_free(&res); + return rc; +} + +/* Resolve the request's `model` field to a resident registry entry (with a + * reference taken). Routing rule: with exactly one resident model every + * request routes to it regardless of the field (the single-model server's + * behavior, so existing clients keep working); with several, an exact name + * match is required (a missing field falls back to the default model) and an + * unknown name is a clean 404. Returns NULL after answering the client. */ +static mr_entry *route_model(int client_fd, mr_registry *reg, + const char *name) +{ + mr_entry *entry = (mr_count(reg) <= 1) ? mr_acquire(reg, NULL) + : mr_acquire(reg, name); + if (entry) + return entry; + if (mr_count(reg) == 0) + hs_send_error(client_fd, 503, "no_models_loaded", + "no model is currently loaded"); + else + hs_send_error(client_fd, 404, "model_not_found", + "the requested model is not loaded (see GET /v1/models)"); + return NULL; +} + +/* + * Acquire one decode slot on the batched path, waiting in the scheduler's + * bounded FIFO when every slot is busy and gating new prefills on host memory + * pressure (D3 backpressure). 429 only when the queue is full; 503 when the + * queue is full AND memory is CRITICAL. + * + * `answer_client` sends the OpenAI error body on failure. It is false for the + * second and later completions of an `n` > 1 request: a response is already in + * flight (SSE) or already has usable choices, so an error body would corrupt it + * — the caller reports the shortfall in `n_status` instead. + */ +static bs_acquire_result acquire_decode_slot(int client_fd, + pn_batch_sched *sched, + const char *sid, int *slot_out, + bool answer_client) +{ + /* Refresh the pressure snapshot so the scheduler sees the current host state + * at decision time (memwatch polls every 250 ms; this forces an immediate + * read without waiting for the next tick). */ + if (pn_memwatch_running()) { + pn_mem_state ms; + pn_memwatch_read(&ms); + bs_notify_pressure(sched, (int)ms.level); + } + static const int SLOT_WAIT_MS = 30000; /* 30 s max queue wait */ + int retry_after = 0; + *slot_out = -1; + bs_acquire_result ar = bs_slot_acquire_or_wait(sched, SLOT_WAIT_MS, slot_out, + &retry_after, sid); + if (ar == BS_ACQUIRE_OK || !answer_client) + return ar; + + int http_code; + const char *err_code, *err_msg; + switch (ar) { + case BS_ACQUIRE_503: + http_code = 503; + err_code = "server_memory_constrained"; + err_msg = "server memory-constrained and queue full; retry shortly"; + break; + case BS_ACQUIRE_TIMEOUT: + http_code = 429; + err_code = "server_busy"; + err_msg = "request timed out waiting for a decode slot; retry shortly"; + break; + case BS_ACQUIRE_SHUTDOWN: + http_code = 503; + err_code = "server_shutting_down"; + err_msg = "server is shutting down"; + break; + default: /* BS_ACQUIRE_429 */ + http_code = 429; + err_code = "server_busy"; + err_msg = "all decode slots occupied and queue full; retry shortly"; + break; + } + if (retry_after > 0) { + char ra_hdr[64]; + snprintf(ra_hdr, sizeof(ra_hdr), "Retry-After: %d\r\n", retry_after); + hs_send_error_ex(client_fd, http_code, err_code, err_msg, ra_hdr); + } else { + hs_send_error(client_fd, http_code, err_code, err_msg); + } + return ar; +} + +/* Send the Phase-5 "model at session capacity" 429 with a Retry-After and a + * re-prefill restart estimate. `restart_tokens` is the committed length the + * refused (returning/expired) session would re-prefill; 0 for a truly new sid. */ +static void send_session_capacity_refuse(int client_fd, const mr_entry *entry, + int restart_tokens) +{ + int restart_s = (int)((double)restart_tokens + / oai_prefill_tok_per_s() + 0.5); + char ra_hdr[64]; + snprintf(ra_hdr, sizeof(ra_hdr), "Retry-After: %d\r\n", 30); + char msg[192]; + snprintf(msg, sizeof(msg), + "model at session capacity (%d live sessions); retry " + "shortly. A fresh session re-prefills in ~%ds.", + entry->max_sessions, restart_s); + hs_send_error_ex(client_fd, 429, "model_at_session_capacity", msg, ra_hdr); +} + +void oai_handle_chat(int client_fd, mr_registry *reg, const char *body, + const char *session_id) +{ + if (!body) { hs_send_error(client_fd, 400, "invalid_request_error", "empty request body"); return; } + + pn_reqprof_phase_start(PN_REQ_JSON_PARSE); + jr_value *root = jr_parse(body); + if (!root) { + pn_reqprof_phase_end(PN_REQ_JSON_PARSE); + hs_send_error(client_fd, 400, "invalid_request_error", "malformed JSON"); + return; + } + + chat_request req; + const char *err = NULL; + int parse_rc = parse_chat_request(root, &req, &err); + pn_reqprof_phase_end(PN_REQ_JSON_PARSE); + if (parse_rc != 0) { + hs_send_error(client_fd, 400, "invalid_request_error", err ? err : "bad request"); + chat_request_free(&req); + jr_free(root); + return; + } + + /* Route to the resident model. The held reference keeps mr_unload from + * freeing the model mid-generation. */ + pn_reqprof_phase_start(PN_REQ_ROUTE); + mr_entry *entry = route_model(client_fd, reg, req.model); + if (!entry) { + pn_reqprof_phase_end(PN_REQ_ROUTE); + chat_request_free(&req); + jr_free(root); + return; + } + + pn_model *model = entry->model; + const char *model_id = entry->name; + pn_batch_sched *sched = entry->sched; /* non-NULL when C3 is active */ + + /* Now that the resident model is known, resolve its tool-call dialect and + * swap the tools preamble / re-render history into it (e.g. MiniCPM5's + * or ornith's JSON). No-op for the default Qwen/N2 dialect + * or a tools-less request beyond storing the dialect on req->gen. */ + chat_apply_tool_dialect(&req, model, model_id); + pn_reqprof_phase_end(PN_REQ_ROUTE); + + /* Resolve the AUTO-path reasoning budget: request override (if the body + * carried reasoning_budget / max_reasoning_tokens) ELSE the per-model default + * (from --reasoning-budget) ELSE 0 (disabled). */ + if (!req.reasoning_budget_set) + req.gen.reasoning_budget = entry->reasoning_budget; + /* Name the model on the params purely so an abnormal-event log (the + * reasoning force-close) can say WHICH model it happened to. Borrowed from + * the registry entry, which outlives this request (it is ref-held). */ + req.gen.model_name = entry->name; + + /* PC0.1: effective prefix-cache session id — the "session_id" body field + * wins over the X-Session-Id header; empty/absent → NULL (no caching, full + * prefill, byte-identical to today). */ + const char *sid = (req.session_id && req.session_id[0]) ? req.session_id + : (session_id && session_id[0]) ? session_id + : NULL; + + char id[32]; + make_completion_id(id, sizeof(id)); + long created = (long)time(NULL); + + /* ── Phase 5: session admission — REFUSE at capacity + idle TTL ── + * A "session" is a live prefix-cache slot. Before touching the model, gate a + * genuinely-NEW session id against the model's per-model max_sessions cap: + * if every slot holds a live, non-idle session, refuse with a clean 429 + * (never queue, never LRU-evict a live session — an existing sid always + * resumes). The reaper inside admit first frees any slot idle past the TTL. + * On refuse the serve reports a re-prefill restart estimate for the client + * UX. Only engaged when the client sent a session id AND the model has a + * prefix cache; the uncached path is byte-identical to before. + * + * The BATCHED path admits here under pc_lock (its cache ops — admit/touch — + * are all pc_lock-guarded and it never runs the cache's own generation). + * The SINGLE-USER path admits LATER, under gen_lock (see below): its + * generation mutates the same per-session cache states with pc_lock==NULL + * (serialized only by gen_lock), so running the idle reaper here under + * pc_lock could free a slot a concurrent single-user generation is still + * using (use-after-free). Admitting it under gen_lock serializes the reaper + * with that generation. */ + if (sched && entry->prefix_cache && sid) { + int restart_tokens = 0; + pn_reqprof_phase_start(PN_REQ_SESSION_ADMIT); + pthread_mutex_lock(&entry->pc_lock); + pn_session_admit v = pn_prefix_cache_admit(entry->prefix_cache, sid, + ms_now_ms(), + ms_session_idle_ttl_ms(), + &restart_tokens); + pthread_mutex_unlock(&entry->pc_lock); + pn_reqprof_phase_end(PN_REQ_SESSION_ADMIT); + if (v == PN_SESSION_ADMIT_AT_CAPACITY) { + send_session_capacity_refuse(client_fd, entry, restart_tokens); + mr_release(reg, entry); + chat_request_free(&req); + jr_free(root); + return; + } + } + + /* ── C3 batched path (QWEN35_BATCH_SLOTS=1 and model is GPU-placed) ── */ + if (sched) { + stream_ctx sc = { .client_fd = client_fd, .id = id, .created = created, + .model_id = model_id }; + chat_choice *choices = NULL; + int n_delivered = 0; + bool sse_open = false; + const char *n_status = req.n_status; + + if (!req.stream) { + choices = calloc((size_t)req.n_choices, sizeof(*choices)); + if (!choices) { + hs_send_error(client_fd, 500, "server_error", "oom"); + mr_release(reg, entry); + chat_request_free(&req); + jr_free(root); + return; + } + } + + for (int ci = 0; ci < req.n_choices; ci++) { + /* D1+D3: acquire a slot BEFORE opening SSE so a 429/503 can be sent + * cleanly without an in-progress SSE stream to abort. */ + int slot = -1; + bs_acquire_result ar = + acquire_decode_slot(client_fd, sched, sid, &slot, + /* answer_client = */ ci == 0); + if (ar != BS_ACQUIRE_OK) { + /* The FIRST choice failing is the whole request failing, and + * acquire_decode_slot already answered. A LATER choice failing + * cannot inject an error into a response already in flight — the + * client gets the completions that did land, and n_status says + * the rest did not. */ + if (ci == 0) goto batched_done; + n_status = "partial_slot_unavailable"; + break; + } + + /* Only the FIRST completion belongs to the session: the client keeps + * at most one of the n, so committing a later one would leave the + * warm prefix describing a conversation that never happened. The + * extra completions therefore full-prefill — the documented cost. */ + const char *choice_sid = (ci == 0) ? sid : NULL; + sg_gen_params gen = gen_params_for_choice(&req, ci); + + if (req.stream) { + if (!sse_open) { hs_begin_sse(client_fd); sse_open = true; } + sc.choice_index = ci; + sc.role_sent = false; + int rc = run_streaming_batched(client_fd, model, sched, &req, + &gen, &sc, slot, choice_sid); + n_delivered++; + if (rc != 0) break; /* already reported on the stream */ + } else { + chat_choice *ch = &choices[n_delivered]; + int rc = sg_run_completion_batched(model, sched, slot, + req.messages, req.n_messages, + &gen, NULL, NULL, &ch->res, + choice_sid); + if (rc != 0) { + if (n_delivered > 0) { + /* Some completions already succeeded — return them + * rather than throwing the work away, and say why the + * rest are missing. */ + n_status = "partial_generation_failed"; + sg_result_free(&ch->res); + memset(ch, 0, sizeof(*ch)); + break; + } + if (rc == SG_ADMIT_DENIED) + hs_send_error(client_fd, 503, "server_memory_constrained", + "server memory-constrained; retry shortly"); + else if (rc == SG_CONTEXT_OVER_BUDGET) + send_context_over_budget_http(client_fd, + ch->res.kv_used_tokens, + ch->res.kv_cap_tokens); + else + hs_send_error(client_fd, 500, "server_error", + "generation failed"); + sg_result_free(&ch->res); + memset(ch, 0, sizeof(*ch)); + goto batched_done; + } + collect_tool_calls(&req, &ch->res, &ch->calls, &ch->n_calls, + &ch->tool_error); + n_delivered++; + } + } + + if (req.stream) { + if (sse_open) hs_end_sse(client_fd); + } else if (n_delivered > 0) { + send_completion_json(client_fd, id, created, model_id, choices, + n_delivered, + wire_response_format_status(&req, + &choices[0].res), + n_status); + } + + batched_done: + /* `choices` is allocated only on the NON-streaming path — a streaming + * request counts delivered completions but stores none, so this must be + * guarded on the ARRAY, not on the count. */ + for (int ci = 0; choices && ci < n_delivered; ci++) + chat_choice_free(&choices[ci]); + free(choices); + /* Phase 5: mark the session active for the idle-TTL reaper. The batched + * path has concurrent slots, so guard the cache op with pc_lock. */ + if (entry->prefix_cache && sid) { + pthread_mutex_lock(&entry->pc_lock); + pn_prefix_cache_touch(entry->prefix_cache, sid, ms_now_ms()); + pthread_mutex_unlock(&entry->pc_lock); + } + /* No gen_lock to release — the slot scheduler manages concurrency. */ + mr_release(reg, entry); + chat_request_free(&req); + jr_free(root); + return; + } + + /* ── Legacy single-user path (sched == NULL) ── */ + /* Serialize on ITS lock only: chats to different models run concurrently + * (sampler scratch is _Thread_local); a second chat to the SAME model + * gets the immediate 429, exactly the old single-model behavior. */ + if (mr_trylock_generation(entry) != 0) { + hs_send_error(client_fd, 429, "server_busy", + "a generation is already in progress on this model; " + "retry shortly"); + mr_release(reg, entry); + chat_request_free(&req); + jr_free(root); + return; + } + + /* ── Phase 5 admission, single-user path — under gen_lock ── + * gen_lock (just taken) is the single-user path's cache serializer: the + * generation below runs its acquire/commit with pc_lock==NULL, relying on + * gen_lock. Running admission (which reaps idle slots — freeing their + * pn_states) under the SAME lock means the reaper can never race a live + * generation on this model. A concurrent request fails the trylock above and + * returns 429 without ever reaching the reaper. */ + if (entry->prefix_cache && sid) { + int restart_tokens = 0; + pn_session_admit v = pn_prefix_cache_admit(entry->prefix_cache, sid, + ms_now_ms(), + ms_session_idle_ttl_ms(), + &restart_tokens); + if (v == PN_SESSION_ADMIT_AT_CAPACITY) { + pthread_mutex_unlock(&entry->gen_lock); + send_session_capacity_refuse(client_fd, entry, restart_tokens); + mr_release(reg, entry); + chat_request_free(&req); + jr_free(root); + return; + } + } + pn_state *state = entry->state; + + /* PC2: prefix-cache context — active only when the client sent a session id + * and the model has a cache. NULL sid → full prefill (byte-identical). The + * gen_lock already serializes this model's single-user path, so the cache + * op (thread-unsafe) is safe under it; pc_lock stays NULL here. */ + sg_prefix_ctx pcx = { .cache = entry->prefix_cache, .sid = sid, + .pc_lock = NULL }; + const sg_prefix_ctx *pc = (entry->prefix_cache && sid) ? &pcx : NULL; + + { + stream_ctx sc = { .client_fd = client_fd, .id = id, .created = created, + .model_id = model_id }; + chat_choice *choices = NULL; + int n_delivered = 0; + bool sse_open = false; + const char *n_status = req.n_status; + + if (!req.stream) + choices = calloc((size_t)req.n_choices, sizeof(*choices)); + + for (int ci = 0; ci < req.n_choices && (req.stream || choices); ci++) { + /* Only the FIRST completion belongs to the session — see the batched + * path for why the extras run uncached. */ + const sg_prefix_ctx *choice_pc = (ci == 0) ? pc : NULL; + sg_gen_params gen = gen_params_for_choice(&req, ci); + + if (req.stream) { + if (!sse_open) { hs_begin_sse(client_fd); sse_open = true; } + sc.choice_index = ci; + sc.role_sent = false; + int rc = run_streaming(client_fd, model, state, &req, &gen, &sc, + choice_pc); + n_delivered++; + if (rc != 0) break; /* already reported on the stream */ + } else { + chat_choice *ch = &choices[n_delivered]; + int rc = sg_run_completion(model, state, req.messages, + req.n_messages, &gen, NULL, NULL, + &ch->res, choice_pc); + if (rc != 0) { + if (n_delivered > 0) { + n_status = "partial_generation_failed"; + sg_result_free(&ch->res); + memset(ch, 0, sizeof(*ch)); + break; + } + if (rc == SG_ADMIT_DENIED) + hs_send_error(client_fd, 503, "server_memory_constrained", + "server memory-constrained; retry shortly"); + else if (rc == SG_CONTEXT_OVER_BUDGET) + send_context_over_budget_http(client_fd, + ch->res.kv_used_tokens, + ch->res.kv_cap_tokens); + else + hs_send_error(client_fd, 500, "server_error", + "generation failed"); + sg_result_free(&ch->res); + memset(ch, 0, sizeof(*ch)); + break; + } + collect_tool_calls(&req, &ch->res, &ch->calls, &ch->n_calls, + &ch->tool_error); + n_delivered++; + } + } + + if (req.stream) { + if (sse_open) hs_end_sse(client_fd); + } else if (n_delivered > 0) { + send_completion_json(client_fd, id, created, model_id, choices, + n_delivered, + wire_response_format_status(&req, + &choices[0].res), + n_status); + } else if (!choices) { + hs_send_error(client_fd, 500, "server_error", "oom"); + } + /* `choices` is allocated only on the NON-streaming path — a streaming + * request counts delivered completions but stores none, so this must be + * guarded on the ARRAY, not on the count. */ + for (int ci = 0; choices && ci < n_delivered; ci++) + chat_choice_free(&choices[ci]); + free(choices); + } + + /* Phase 5: mark the session active so the idle-TTL reaper measures from the + * END of this turn. Under gen_lock (serializes this model's cache ops). */ + if (entry->prefix_cache && sid) + pn_prefix_cache_touch(entry->prefix_cache, sid, ms_now_ms()); + + pthread_mutex_unlock(&entry->gen_lock); + mr_release(reg, entry); + chat_request_free(&req); + jr_free(root); +} + +/* ============================================================ + * POST /session/restore — session warm-tier IMPORT (Phase 7) + * ============================================================ */ + +/* Parse tokens[] (a JSON array of ints) into a malloc'd uint32_t array. Returns + * the array (caller frees) and sets *n; NULL on a missing/empty/bad array. */ +static uint32_t *parse_token_array(const jr_value *arr, int *n) +{ + *n = 0; + if (jr_kind(arr) != JR_ARRAY) + return NULL; + int len = jr_array_len(arr); + if (len <= 0) + return NULL; + uint32_t *toks = malloc((size_t)len * sizeof(uint32_t)); + if (!toks) + return NULL; + for (int i = 0; i < len; i++) + toks[i] = (uint32_t)jr_as_number(jr_array_get(arr, i), 0); + *n = len; + return toks; +} + +/* Restore a client-held session blob into a live slot. Body: + * {"name":"model","session_id":"sid","blob":"","tokens":[...]} + * `tokens` is the committed history the blob covers (the client owns it); its + * length must equal the blob's snapshot pos. On a weights_version mismatch or a + * bad blob, answer a clean error so the client falls back to message-replay. */ +void oai_handle_session_restore(int client_fd, mr_registry *reg, + const char *body, const char *session_id) +{ + if (!body) { hs_send_error(client_fd, 400, "invalid_request_error", "empty request body"); return; } + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { + hs_send_error(client_fd, 400, "invalid_request_error", "malformed JSON"); + jr_free(root); + return; + } + const char *name = jr_as_string(jr_object_get(root, "name")); + const char *body_sid = jr_as_string(jr_object_get(root, "session_id")); + const char *sid = (body_sid && body_sid[0]) ? body_sid + : (session_id && session_id[0]) ? session_id : NULL; + const char *blob_b64 = jr_as_string(jr_object_get(root, "blob")); + if (!sid || !blob_b64 || !blob_b64[0]) { + hs_send_error(client_fd, 400, "invalid_request_error", + "'session_id' and 'blob' are required"); + jr_free(root); + return; + } + int n_tok = 0; + uint32_t *tokens = parse_token_array(jr_object_get(root, "tokens"), &n_tok); + if (!tokens) { + hs_send_error(client_fd, 400, "invalid_request_error", + "'tokens' (committed history) is required"); + jr_free(root); + return; + } + + mr_entry *entry = mr_acquire(reg, name); + if (!entry) { + free(tokens); + hs_send_error(client_fd, 404, "model_not_found", + "the requested model is not loaded (see GET /v1/models)"); + jr_free(root); + return; + } + /* A model with neither a scheduler nor a prefix cache has nowhere to put a + * session (the facade reports MS_NO_CACHE for it too; refusing here avoids + * decoding a large blob for nothing). */ + if (!entry->sched && !entry->prefix_cache) { + mr_release(reg, entry); free(tokens); + hs_send_error(client_fd, 400, "session_caching_disabled", + "this model has no session cache to restore into"); + jr_free(root); + return; + } + + /* Decode + deserialize the blob, then validate its weights_version — all + * BEFORE handing it to the facade, so the (potentially large) untrusted + * parse runs unlocked. The weights_version guard is the one thing that must + * happen on BOTH paths and before any state is touched: a blob captured + * under a different model build must never be written into a live slot. */ + size_t blob_len = 0; + unsigned char *blob = pn_base64_decode(blob_b64, &blob_len); + if (!blob) { + mr_release(reg, entry); free(tokens); + hs_send_error(client_fd, 400, "bad_session_blob", + "blob is not valid base64; replay the session instead"); + jr_free(root); + return; + } + /* The arch picks the snapshot family, and the two families' blobs carry + * different magics — so a blob captured on the other family is refused at + * parse rather than misread as this one. */ + pn_arch arch = entry->model->config.arch; + uint64_t blob_wv = 0; + void *snap = pn_session_snapshot_deserialize(arch, blob, blob_len, &blob_wv); + free(blob); + if (!snap) { + mr_release(reg, entry); free(tokens); + hs_send_error(client_fd, 400, "bad_session_blob", + "session blob is corrupt or wrong format; replay instead"); + jr_free(root); + return; + } + if (blob_wv != mr_weights_version(entry)) { + pn_session_snapshot_free(arch, snap); + mr_release(reg, entry); free(tokens); + hs_send_error(client_fd, 409, "weights_version_mismatch", + "session blob was captured under different weights; " + "replay the session instead"); + jr_free(root); + return; + } + + /* Install the state through the session facade, which routes to the store + * this model actually generates from: the scheduler's warm decode slot on + * the batched path, the prefix cache on the single-user one. Restoring into + * the prefix cache unconditionally — what this used to do — reported + * success on a batched model while writing to a store no batched generation + * ever reads (and allocating an unused pn_state to hold it). Locking and + * admission live inside the facade. */ + ms_result msr = ms_session_restore(entry, sid, snap, tokens, n_tok); + pn_session_snapshot_free(arch, snap); + + if (msr != MS_OK) { + mr_release(reg, entry); free(tokens); + if (msr == MS_BUSY) + hs_send_error(client_fd, 429, "model_at_session_capacity", + "model at session capacity; retry shortly"); + else if (msr == MS_NO_CACHE) + hs_send_error(client_fd, 400, "session_caching_disabled", + "this model has no session cache to restore into"); + else if (msr == MS_BAD_SNAPSHOT) + hs_send_error(client_fd, 400, "bad_session_blob", + "snapshot did not fit this model (dim/pos mismatch); " + "replay instead"); + else + hs_send_error(client_fd, 500, "server_error", + "session restore failed"); + jr_free(root); + return; + } + + json_writer jw; + if (!jw_init(&jw)) { mr_release(reg, entry); free(tokens); hs_send_error(client_fd, 500, "server_error", "oom"); jr_free(root); return; } + jw_raw(&jw, "{\"restored\":true,\"session_id\":"); + jw_string(&jw, sid); + jw_printf(&jw, ",\"n_tok\":%d}", n_tok); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); + + mr_release(reg, entry); + free(tokens); + jr_free(root); +} + +/* POST /session/release {name, session_id} — free a session's serve slot on a + * client's clean shutdown, returning capacity immediately instead of waiting out + * the idle-TTL. Goes through the session facade, so a BATCHED session's warm + * decode slot is actually dropped (it used to clear only the prefix cache, which + * holds nothing on that path — a departed session stayed bound to its slot). + * Idempotent: an unknown/already-freed session still returns 200 released:true — + * The client is best-effort and never retries. */ +void oai_handle_session_release(int client_fd, mr_registry *reg, + const char *body, const char *session_id) +{ + if (!body) { hs_send_error(client_fd, 400, "invalid_request_error", "empty request body"); return; } + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { + hs_send_error(client_fd, 400, "invalid_request_error", "malformed JSON"); + jr_free(root); + return; + } + const char *name = jr_as_string(jr_object_get(root, "name")); + const char *body_sid = jr_as_string(jr_object_get(root, "session_id")); + const char *sid = (body_sid && body_sid[0]) ? body_sid + : (session_id && session_id[0]) ? session_id : NULL; + if (!sid) { + hs_send_error(client_fd, 400, "invalid_request_error", + "'session_id' is required"); + jr_free(root); + return; + } + + mr_entry *entry = mr_acquire(reg, name); + if (!entry) { + hs_send_error(client_fd, 404, "model_not_found", + "the requested model is not loaded (see GET /v1/models)"); + jr_free(root); + return; + } + /* Nothing to free → the release still "succeeds" (the client's intent — + * the slot is gone — is satisfied). */ + ms_session_release(entry, sid); + + json_writer jw; + if (!jw_init(&jw)) { mr_release(reg, entry); hs_send_error(client_fd, 500, "server_error", "oom"); jr_free(root); return; } + jw_raw(&jw, "{\"released\":true,\"session_id\":"); + jw_string(&jw, sid); + jw_raw(&jw, "}"); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); + + mr_release(reg, entry); + jr_free(root); +} + +/* ============================================================ + * GET /session/estimate?name=&tokens=N — warm-vs-cold restart estimate (Phase 7) + * ============================================================ */ + +/* Answer a session restart estimate: cold (re-prefill N tokens at the measured + * prefill rate) vs warm (upload a blob of ~N-token size at a client-provided or + * default LAN rate, plus a host→GPU term). Pure arithmetic from measured + * constants, so the client UX can show "reload ~2s vs re-prefill ~230s". */ +void oai_handle_session_estimate(int client_fd, mr_registry *reg, + const char *name, int tokens, + double blob_bytes, double upload_bytes_per_s) +{ + if (tokens <= 0) { + hs_send_error(client_fd, 400, "invalid_request_error", + "'tokens' query param (> 0) is required"); + return; + } + /* Resolve the model just to confirm it exists / echo its id (optional). */ + mr_entry *entry = mr_acquire(reg, name); + const char *model_id = entry ? entry->name : ""; + + double prefill_rate = oai_prefill_tok_per_s(); /* tok/s */ + double cold_s = (double)tokens / prefill_rate; + + /* Warm: blob upload time + a host→GPU upload term. If the caller did not + * pass blob_bytes, estimate it from the per-token cost measured in Phase 3 + * (saber ~30 MiB / ornith ~130 MiB per session are near-constant; a + * per-token byte figure is a rough linear proxy for the estimate UX). */ + static const double DEFAULT_UPLOAD_BPS = 100.0e6 / 8.0; /* ~100 Mb LAN */ + static const double H2GPU_BPS = 3.0e9; /* ~3 GB/s PCIe */ + static const double BYTES_PER_TOKEN = 16384.0; /* ~16 KiB/token proxy */ + double bytes = blob_bytes > 0.0 ? blob_bytes + : (double)tokens * BYTES_PER_TOKEN; + double up_bps = upload_bytes_per_s > 0.0 ? upload_bytes_per_s + : DEFAULT_UPLOAD_BPS; + double warm_s = bytes / up_bps + bytes / H2GPU_BPS; + + json_writer jw; + if (!jw_init(&jw)) { if (entry) mr_release(reg, entry); hs_send_error(client_fd, 500, "server_error", "oom"); return; } + jw_raw(&jw, "{\"model\":"); + jw_string(&jw, model_id); + jw_printf(&jw, ",\"tokens\":%d,\"prefill_tok_per_s\":%.2f," + "\"cold_reprefill_s\":%.2f,\"warm_restore_s\":%.3f," + "\"blob_bytes_est\":%.0f}", + tokens, prefill_rate, cold_s, warm_s, bytes); + hs_send_json(client_fd, 200, jw.data); + jw_free(&jw); + if (entry) + mr_release(reg, entry); +} diff --git a/src/serve/openai_chat.h b/src/serve/openai_chat.h new file mode 100644 index 0000000..1cc0732 --- /dev/null +++ b/src/serve/openai_chat.h @@ -0,0 +1,81 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file openai_chat.h + * @brief OpenAI-compatible endpoint handlers: GET /v1/models and + * POST /v1/chat/completions. Parses the request JSON, routes the + * request's `model` field to a resident registry entry, maps sampling + * params, drives serve_generation, and shapes chat.completion / + * chat.completion.chunk responses. + * + * @status /v1/models and /v1/chat/completions (streaming + non-streaming), + * including OpenAI tool calling (tools rendered into the prompt, model + * output parsed to `tool_calls`, multi-step tool loops). + * Multi-model: requests route by `model` name; per-model generation + * serialization (429 on a busy model); models on different dies run + * concurrently. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_OPENAI_CHAT_H +#define PN_SERVE_OPENAI_CHAT_H + +#include "../../include/pantheon.h" +#include "model_registry.h" + +/** Handle GET /v1/models: emit every resident model as an OpenAI model list. */ +void oai_handle_models(int client_fd, mr_registry *reg); + +/** + * Handle POST /v1/chat/completions. Parses `body`, routes the request's + * `model` field to a resident registry entry (single resident model → always + * that model, today's behavior; several → exact name match or 404 + * model_not_found), serializes on the entry's gen_lock (429 when that model is + * busy), runs the completion, and writes either a JSON chat.completion or an + * SSE stream of chat.completion.chunk events. Errors are OpenAI-shaped JSON. + */ +/** + * @param session_id Optional prefix-cache session key (PC0.1) from the + * `X-Session-Id` request header; a `session_id` field in the request + * body overrides it. NULL/empty → anonymous (byte-identical full-prefill + * behaviour; no caching). Backward-compatible: existing clients that + * send neither are unaffected. + */ +void oai_handle_chat(int client_fd, mr_registry *reg, const char *body, + const char *session_id); + +/** + * Handle POST /session/restore — session warm-tier import (K80 serving-ops + * Phase 7). Body {"name","session_id","blob":"","tokens":[...]}: admits + * the session (429 at capacity), validates the blob's weights_version against + * the model, deserializes + restores it into a live slot with the committed + * `tokens` history, and positions the state so the next chat turn append-resumes + * off it. A weights mismatch / corrupt blob / dim mismatch is a clean error + * (the client falls back to replaying the message history). `session_id` is the + * X-Session-Id header fallback when the body omits it. + */ +void oai_handle_session_restore(int client_fd, mr_registry *reg, + const char *body, const char *session_id); + +/** + * Handle POST /session/release {name, session_id} — free a session's serve slot on + * a client's clean shutdown, returning capacity immediately rather than waiting out + * the idle-TTL. Idempotent (an unknown session still returns released:true), best- + * effort from the client's side. `session_id` is the X-Session-Id header fallback + * when the body omits it. + */ +void oai_handle_session_release(int client_fd, mr_registry *reg, + const char *body, const char *session_id); + +/** + * Handle GET /session/estimate?name=&tokens=N[&blob_bytes=&upload_bps=] — + * warm-restore vs cold-reprefill time estimate for the client UX (Phase 7). + * Returns cold (tokens / measured prefill rate) and warm (blob upload + host→GPU + * upload) seconds. `blob_bytes`/`upload_bytes_per_s` <= 0 → estimated defaults. + */ +void oai_handle_session_estimate(int client_fd, mr_registry *reg, + const char *name, int tokens, + double blob_bytes, double upload_bytes_per_s); + +#endif /* PN_SERVE_OPENAI_CHAT_H */ diff --git a/src/serve/registry.c b/src/serve/registry.c new file mode 100644 index 0000000..5a79ad1 --- /dev/null +++ b/src/serve/registry.c @@ -0,0 +1,461 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file registry.c + * @brief Implementation of the live-instance registry (see registry.h). Pure C, + * one mutex, fixed-size table. Uses the serve layer's json_reader for + * parsing register/heartbeat/deregister bodies and json_writer for the + * /v1/models and /registry/fleet responses — no new deps. + * + * @status G1 complete. + * @issues None + * @todo None + */ + +#include "registry.h" + +#include +#include +#include + +#include "json_reader.h" +#include "json_writer.h" + +/* Copy a JSON string field into a fixed dst buffer (truncating, always NUL + * terminated). Returns true if the field was present and a string. */ +static bool copy_str_field(const jr_value *obj, const char *key, char *dst, + size_t cap) +{ + const jr_value *v = jr_object_get(obj, key); + const char *s = jr_as_string(v); + if (!s) { dst[0] = '\0'; return false; } + size_t n = strlen(s); + if (n >= cap) n = cap - 1; + memcpy(dst, s, n); + dst[n] = '\0'; + return true; +} + +int reg_init(reg_registry *r) +{ + memset(r, 0, sizeof(*r)); + if (pthread_mutex_init(&r->mu, NULL) != 0) + return -1; + return 0; +} + +void reg_destroy(reg_registry *r) +{ + pthread_mutex_destroy(&r->mu); +} + +/* Find the row index for an instance_id, or -1. Caller holds the lock. */ +static int find_instance(reg_registry *r, const char *instance_id) +{ + for (int i = 0; i < r->n; i++) + if (strcmp(r->inst[i].instance_id, instance_id) == 0) + return i; + return -1; +} + +reg_status reg_register_json(reg_registry *r, const char *body) +{ + if (!body) return REG_BAD_REQUEST; + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { jr_free(root); return REG_BAD_REQUEST; } + + reg_instance tmp; + memset(&tmp, 0, sizeof(tmp)); + + bool have_model = copy_str_field(root, "model_id", tmp.model_id, sizeof(tmp.model_id)); + bool have_inst = copy_str_field(root, "instance_id", tmp.instance_id, sizeof(tmp.instance_id)); + bool have_ep = copy_str_field(root, "endpoint", tmp.endpoint, sizeof(tmp.endpoint)); + if (!have_model || !have_inst || !have_ep) { jr_free(root); return REG_BAD_REQUEST; } + + copy_str_field(root, "node_id", tmp.node_id, sizeof(tmp.node_id)); + if (!copy_str_field(root, "transport", tmp.transport, sizeof(tmp.transport))) + snprintf(tmp.transport, sizeof(tmp.transport), "%s", RW_TRANSPORT_LOCAL); + tmp.capacity = (int)jr_as_number(jr_object_get(root, "capacity"), 0); + /* Effective context window (tokens). Absent/negative ⇒ 0 = unreported, and + * every emit below then omits the field rather than advertising a zero a + * client would have to special-case. */ + tmp.context_window = + (int)jr_as_number(jr_object_get(root, "context_window"), 0); + if (tmp.context_window < 0) tmp.context_window = 0; + + /* A fresh registration starts fully free + healthy until the first + * heartbeat refines the load. */ + tmp.free_slots = tmp.capacity; + tmp.healthy = true; + tmp.last_seen = time(NULL); + + jr_free(root); + + pthread_mutex_lock(&r->mu); + int idx = find_instance(r, tmp.instance_id); + reg_status st = REG_OK; + if (idx >= 0) { + r->inst[idx] = tmp; /* idempotent re-register */ + } else if (r->n < REG_MAX_INSTANCES) { + r->inst[r->n++] = tmp; + } else { + st = REG_FULL; + } + pthread_mutex_unlock(&r->mu); + return st; +} + +reg_status reg_heartbeat_json(reg_registry *r, const char *body) +{ + if (!body) return REG_BAD_REQUEST; + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { jr_free(root); return REG_BAD_REQUEST; } + + char instance_id[RW_INSTANCE_ID_MAX]; + if (!copy_str_field(root, "instance_id", instance_id, sizeof(instance_id))) { + jr_free(root); + return REG_BAD_REQUEST; + } + + const jr_value *load = jr_object_get(root, "load"); + /* Read load fields up-front (borrowed pointers valid until jr_free). */ + bool have_load = (load && jr_kind(load) == JR_OBJECT); + int active = have_load ? (int)jr_as_number(jr_object_get(load, "active_slots"), 0) : 0; + int queue = have_load ? (int)jr_as_number(jr_object_get(load, "queue_depth"), 0) : 0; + int free_s = have_load ? (int)jr_as_number(jr_object_get(load, "free_slots"), 0) : 0; + /* The window rides the heartbeat too: the memory ladder can clamp it after + * registration, so a register-time value would go stale exactly when it + * matters. <=0 (or absent) leaves the prior value alone — an old serve that + * doesn't send it must not blank out what it reported at register. */ + int ctxw = have_load + ? (int)jr_as_number(jr_object_get(load, "context_window"), 0) + : 0; + const jr_value *hv = jr_object_get(root, "healthy"); + bool have_healthy = (jr_kind(hv) == JR_BOOL); + bool healthy = jr_as_bool(hv, true); + + jr_free(root); + + pthread_mutex_lock(&r->mu); + int idx = find_instance(r, instance_id); + reg_status st = REG_OK; + if (idx < 0) { + st = REG_NOT_FOUND; + } else { + reg_instance *e = &r->inst[idx]; + if (have_load) { + e->active_slots = active; + e->queue_depth = queue; + e->free_slots = free_s; + if (ctxw > 0) + e->context_window = ctxw; + } + if (have_healthy) e->healthy = healthy; + e->last_seen = time(NULL); + } + pthread_mutex_unlock(&r->mu); + return st; +} + +reg_status reg_deregister_json_collect(reg_registry *r, const char *body, + char *removed_out, size_t out_cap) +{ + if (removed_out && out_cap > 0) + removed_out[0] = '\0'; + if (!body) return REG_BAD_REQUEST; + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { jr_free(root); return REG_BAD_REQUEST; } + + char instance_id[RW_INSTANCE_ID_MAX]; + bool ok = copy_str_field(root, "instance_id", instance_id, sizeof(instance_id)); + jr_free(root); + if (!ok) return REG_BAD_REQUEST; + + pthread_mutex_lock(&r->mu); + int idx = find_instance(r, instance_id); + reg_status st = REG_NOT_FOUND; + if (idx >= 0) { + if (removed_out && out_cap > 0) + snprintf(removed_out, out_cap, "%s", r->inst[idx].instance_id); + /* Swap-remove: move the last row into the hole. Order is irrelevant. */ + r->inst[idx] = r->inst[r->n - 1]; + r->n--; + st = REG_OK; + } + pthread_mutex_unlock(&r->mu); + return st; +} + +reg_status reg_deregister_json(reg_registry *r, const char *body) +{ + return reg_deregister_json_collect(r, body, NULL, 0); +} + +int reg_sweep_collect(reg_registry *r, time_t now, + char (*evicted_out)[RW_INSTANCE_ID_MAX], int max_out, + int *n_out) +{ + int evicted = 0; + int reported = 0; + pthread_mutex_lock(&r->mu); + int i = 0; + while (i < r->n) { + if (now - r->inst[i].last_seen > RW_TTL_SECONDS) { + if (evicted_out && reported < max_out) + snprintf(evicted_out[reported++], RW_INSTANCE_ID_MAX, "%s", + r->inst[i].instance_id); + r->inst[i] = r->inst[r->n - 1]; + r->n--; + evicted++; + /* do not advance i: the swapped-in row must be checked too */ + } else { + i++; + } + } + pthread_mutex_unlock(&r->mu); + if (n_out) + *n_out = reported; + return evicted; +} + +int reg_sweep(reg_registry *r, time_t now) +{ + return reg_sweep_collect(r, now, NULL, 0, NULL); +} + +/* An instance is a live dispatch candidate iff healthy and within TTL. Caller + * holds the lock. `now` is passed so callers use one consistent clock read. */ +static bool is_live(const reg_instance *e, time_t now) +{ + return e->healthy && (now - e->last_seen <= RW_TTL_SECONDS); +} + +char *reg_models_json(reg_registry *r) +{ + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, "{\"object\":\"list\",\"data\":["); + + pthread_mutex_lock(&r->mu); + time_t now = time(NULL); + int emitted = 0; + for (int i = 0; i < r->n; i++) { + if (!is_live(&r->inst[i], now)) continue; + /* Dedup by model_id: only emit the FIRST live instance of each id, but + * aggregate live count + free slots across all live replicas of it. */ + bool first = true; + for (int j = 0; j < i; j++) { + if (is_live(&r->inst[j], now) && + strcmp(r->inst[j].model_id, r->inst[i].model_id) == 0) { + first = false; + break; + } + } + if (!first) continue; + + /* Window across replicas: the router may dispatch this model_id to ANY + * live replica, so the only window it can honestly promise is the + * SMALLEST one on offer. A replica that reports nothing (0) makes even + * that unknowable — one unreported replica suppresses the field + * entirely, because "undetected" is a state the client handles and a + * window it can be denied is not. */ + int live = 0, total_free = 0, min_window = 0; + bool window_known = true; + for (int j = 0; j < r->n; j++) { + if (is_live(&r->inst[j], now) && + strcmp(r->inst[j].model_id, r->inst[i].model_id) == 0) { + live++; + total_free += r->inst[j].free_slots; + int w = r->inst[j].context_window; + if (w <= 0) + window_known = false; + else if (min_window == 0 || w < min_window) + min_window = w; + } + } + if (emitted++) jw_raw(&jw, ","); + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, r->inst[i].model_id); + jw_printf(&jw, ",\"object\":\"model\",\"created\":0,\"owned_by\":\"pantheon\"," + "\"live\":%d,\"free_slots\":%d", live, total_free); + if (window_known && min_window > 0) + jw_printf(&jw, ",\"context_window\":%d", min_window); + jw_raw(&jw, "}"); + } + pthread_mutex_unlock(&r->mu); + + jw_raw(&jw, "]}"); + if (jw.oom) { jw_free(&jw); return NULL; } + char *out = jw.data; /* transfer ownership */ + jw.data = NULL; + jw_free(&jw); + return out; +} + +char *reg_fleet_json(reg_registry *r) +{ + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, "{\"object\":\"fleet\",\"models\":["); + + pthread_mutex_lock(&r->mu); + time_t now = time(NULL); + int emitted = 0; + for (int i = 0; i < r->n; i++) { + if (!is_live(&r->inst[i], now)) continue; + bool first = true; + for (int j = 0; j < i; j++) { + if (is_live(&r->inst[j], now) && + strcmp(r->inst[j].model_id, r->inst[i].model_id) == 0) { + first = false; + break; + } + } + if (!first) continue; + + int live = 0, total_free = 0, min_window = 0; + bool window_known = true; + if (emitted++) jw_raw(&jw, ","); + jw_raw(&jw, "{\"model_id\":"); + jw_string(&jw, r->inst[i].model_id); + jw_raw(&jw, ",\"instances\":["); + int inst_emitted = 0; + /* Collect distinct nodes while we walk the replicas. */ + char nodes[REG_MAX_INSTANCES][RW_NODE_ID_MAX]; + int n_nodes = 0; + for (int j = 0; j < r->n; j++) { + if (!is_live(&r->inst[j], now) || + strcmp(r->inst[j].model_id, r->inst[i].model_id) != 0) + continue; + reg_instance *e = &r->inst[j]; + live++; + total_free += e->free_slots; + if (inst_emitted++) jw_raw(&jw, ","); + jw_raw(&jw, "{\"instance_id\":"); + jw_string(&jw, e->instance_id); + jw_raw(&jw, ",\"node_id\":"); + jw_string(&jw, e->node_id); + jw_raw(&jw, ",\"endpoint\":"); + jw_string(&jw, e->endpoint); + jw_raw(&jw, ",\"transport\":"); + jw_string(&jw, e->transport); + jw_printf(&jw, ",\"capacity\":%d,\"active_slots\":%d,\"queue_depth\":%d," + "\"free_slots\":%d,\"healthy\":%s", + e->capacity, e->active_slots, e->queue_depth, e->free_slots, + e->healthy ? "true" : "false"); + if (e->context_window > 0) + jw_printf(&jw, ",\"context_window\":%d", e->context_window); + jw_raw(&jw, "}"); + if (e->context_window <= 0) + window_known = false; + else if (min_window == 0 || e->context_window < min_window) + min_window = e->context_window; + /* track distinct node */ + bool seen_node = false; + for (int b = 0; b < n_nodes; b++) + if (strcmp(nodes[b], e->node_id) == 0) { seen_node = true; break; } + if (!seen_node && n_nodes < REG_MAX_INSTANCES) + snprintf(nodes[n_nodes++], RW_NODE_ID_MAX, "%s", e->node_id); + } + jw_raw(&jw, "],\"nodes\":["); + for (int b = 0; b < n_nodes; b++) { + if (b) jw_raw(&jw, ","); + jw_string(&jw, nodes[b]); + } + jw_printf(&jw, "],\"live\":%d,\"total_free_slots\":%d", live, total_free); + /* Model-level window = the smallest a dispatch could land on; suppressed + * outright if any live replica hasn't reported one (same rule as + * reg_models_json — never promise a window a replica can't honour). */ + if (window_known && min_window > 0) + jw_printf(&jw, ",\"context_window\":%d", min_window); + jw_raw(&jw, "}"); + } + pthread_mutex_unlock(&r->mu); + + jw_raw(&jw, "]}"); + if (jw.oom) { jw_free(&jw); return NULL; } + char *out = jw.data; + jw.data = NULL; + jw_free(&jw); + return out; +} + +bool reg_pick_least_busy(reg_registry *r, const char *model_id, + const char *const *exclude, int n_exclude, + reg_instance *out) +{ + pthread_mutex_lock(&r->mu); + time_t now = time(NULL); + + int best = -1; + for (int i = 0; i < r->n; i++) { + reg_instance *e = &r->inst[i]; + if (strcmp(e->model_id, model_id) != 0) continue; + if (!is_live(e, now)) continue; + + bool excluded = false; + for (int x = 0; x < n_exclude; x++) + if (exclude && exclude[x] && strcmp(exclude[x], e->instance_id) == 0) { + excluded = true; + break; + } + if (excluded) continue; + + if (best < 0) { best = i; continue; } + reg_instance *b = &r->inst[best]; + /* max free_slots, then min queue_depth. Ties fall through to the + * round-robin bias below (best stays; equal candidates alternate). */ + if (e->free_slots > b->free_slots || + (e->free_slots == b->free_slots && e->queue_depth < b->queue_depth)) { + best = i; + } + } + + bool found = false; + if (best >= 0) { + /* Round-robin across the set of candidates that TIE with the winner on + * (free_slots, queue_depth), so equal-load replicas alternate rather + * than always hitting the first. */ + reg_instance *b = &r->inst[best]; + int tie[REG_MAX_INSTANCES]; + int n_tie = 0; + for (int i = 0; i < r->n; i++) { + reg_instance *e = &r->inst[i]; + if (strcmp(e->model_id, model_id) != 0) continue; + if (!is_live(e, now)) continue; + bool excluded = false; + for (int x = 0; x < n_exclude; x++) + if (exclude && exclude[x] && + strcmp(exclude[x], e->instance_id) == 0) { excluded = true; break; } + if (excluded) continue; + if (e->free_slots == b->free_slots && e->queue_depth == b->queue_depth) + tie[n_tie++] = i; + } + int chosen = best; + if (n_tie > 1) + chosen = tie[r->rr_counter++ % (unsigned)n_tie]; + *out = r->inst[chosen]; + found = true; + } + pthread_mutex_unlock(&r->mu); + return found; +} + +bool reg_find_live(reg_registry *r, const char *model_id, + const char *instance_id, reg_instance *out) +{ + if (!model_id || !instance_id) + return false; + + bool found = false; + pthread_mutex_lock(&r->mu); + time_t now = time(NULL); + int idx = find_instance(r, instance_id); + if (idx >= 0 && is_live(&r->inst[idx], now) && + strcmp(r->inst[idx].model_id, model_id) == 0) { + *out = r->inst[idx]; + found = true; + } + pthread_mutex_unlock(&r->mu); + return found; +} diff --git a/src/serve/registry.h b/src/serve/registry.h new file mode 100644 index 0000000..3f29ace --- /dev/null +++ b/src/serve/registry.h @@ -0,0 +1,185 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file registry.h + * @brief In-memory live-instance registry for the dispatch layer (Phase G1). + * A thread-safe table of serve instances keyed by `instance_id` and + * indexed (by scan) on `model_id`. HTTP handler threads register / + * heartbeat / deregister; a background TTL sweep evicts instances that + * stopped heartbeating. The router (G2) reads it to pick the least-busy + * live instance of a requested model. + * + * Concurrency: one mutex guards the whole table. Operations are O(N) over a + * small N (a handful of serve processes), so a single lock is simplest and + * correct; no per-row locking needed. All public calls take/return copies — + * callers never hold pointers into the table across the lock. + * + * @status G1 complete: register/heartbeat/deregister, TTL eviction, deduped + * /v1/models emit, model-level /registry/fleet emit, least-busy pick. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_REGISTRY_H +#define PN_SERVE_REGISTRY_H + +#include +#include +#include + +#include "registry_wire.h" + +/* Max live instances tracked. The local fleet is ~5 models × a few replicas; + * 256 leaves ample headroom for multi-node federation without dynamic growth. */ +#define REG_MAX_INSTANCES 256 + +/* One live serve instance. POD row; strings are fixed-size (see registry_wire). */ +typedef struct { + char model_id[RW_MODEL_ID_MAX]; + char instance_id[RW_INSTANCE_ID_MAX]; + char node_id[RW_NODE_ID_MAX]; + char endpoint[RW_ENDPOINT_MAX]; /* upstream base URL to proxy to */ + char transport[RW_TRANSPORT_MAX]; /* "local" | "wg" */ + int capacity; /* total batch slots */ + + /* Live load (from the last heartbeat). */ + int active_slots; + int queue_depth; + int free_slots; + int context_window; /* effective per-session context window + * in tokens as of the last register / + * heartbeat; 0 = not reported. Sits + * with the LOAD fields, not the + * identity ones, because the memory + * ladder can move it at runtime. */ + bool healthy; + + time_t last_seen; /* wall-clock of last register/heartbeat */ +} reg_instance; + +/* The registry. Zero-initialize then reg_init(). */ +typedef struct { + pthread_mutex_t mu; + reg_instance inst[REG_MAX_INSTANCES]; + int n; + /* Monotonic counter used only to break least-busy ties with round-robin + * (incremented each pick so equal-load replicas alternate). */ + unsigned rr_counter; +} reg_registry; + +/* Result of a register/heartbeat/deregister parse+apply. */ +typedef enum { + REG_OK = 0, + REG_BAD_REQUEST = -1, /* malformed JSON / missing required field */ + REG_FULL = -2, /* table is full (register only) */ + REG_NOT_FOUND = -3, /* instance_id unknown (heartbeat/deregister) */ +} reg_status; + +/** Initialize the registry (mutex + empty table). Returns 0, or -1 on error. */ +int reg_init(reg_registry *r); + +/** Destroy the registry mutex. Table memory is inline (no frees). */ +void reg_destroy(reg_registry *r); + +/** + * Apply a POST /registry/register body (JSON). Inserts a new instance or + * updates an existing one with the same instance_id (idempotent re-register). + * Required fields: model_id, instance_id, endpoint. Optional: node_id, + * transport (default "local"), capacity (default 0). Sets last_seen = now. + */ +reg_status reg_register_json(reg_registry *r, const char *body); + +/** + * Apply a POST /registry/heartbeat body (JSON). Updates load + healthy + + * last_seen for the named instance_id. Required: instance_id. load{} and + * healthy are optional (missing load leaves the prior values). + */ +reg_status reg_heartbeat_json(reg_registry *r, const char *body); + +/** + * Apply a DELETE /registry/deregister body (JSON). Removes the instance. + * Required: instance_id. + */ +reg_status reg_deregister_json(reg_registry *r, const char *body); + +/** + * Like reg_deregister_json but, on a successful removal, copies the removed + * instance_id into `removed_out` (size `out_cap`). Lets the router drop that + * instance's session-affinity bindings. `removed_out` may be NULL to ignore it. + * + * Thread-safe: yes (internal locking) + */ +reg_status reg_deregister_json_collect(reg_registry *r, const char *body, + char *removed_out, size_t out_cap); + +/** + * Evict every instance whose last_seen is older than RW_TTL_SECONDS. Called + * periodically by the TTL sweep thread (and usable directly in tests). + * @param now the reference time (usually time(NULL)); passed in for testability. + * @return number of instances evicted. + */ +int reg_sweep(reg_registry *r, time_t now); + +/** + * Like reg_sweep but appends each evicted instance_id into the caller-provided + * `evicted_out` array (each slot RW_INSTANCE_ID_MAX bytes), up to `max_out` + * entries. Lets the router drop those instances' session-affinity bindings. + * Eviction still proceeds for instances past `max_out` — only the reporting is + * capped. `evicted_out` may be NULL (then this behaves like reg_sweep). + * + * @param evicted_out array of char[RW_INSTANCE_ID_MAX], or NULL + * @param max_out capacity of evicted_out in entries + * @param n_out if non-NULL, receives how many ids were written + * @return total number of instances evicted (may exceed n_out). + * Thread-safe: yes (internal locking) + */ +int reg_sweep_collect(reg_registry *r, time_t now, + char (*evicted_out)[RW_INSTANCE_ID_MAX], int max_out, + int *n_out); + +/** + * Emit the deduped GET /v1/models body into a malloc'd, NUL-terminated string + * (caller frees). One entry per model_id, with a live-instance count and + * aggregate free capacity. Returns NULL on OOM. + */ +char *reg_models_json(reg_registry *r); + +/** + * Emit the GET /registry/fleet body into a malloc'd string (caller frees). + * Model-level: per model_id {live, instances, total_free_slots, nodes[]}. + * Returns NULL on OOM. + */ +char *reg_fleet_json(reg_registry *r); + +/** + * Pick the least-busy live instance of `model_id`, skipping any instance whose + * instance_id is in `exclude` (for failover retries; may be NULL/empty). + * Selection: max free_slots, then min queue_depth, tiebreak round-robin. Only + * healthy instances within TTL are considered. + * + * On success copies the chosen instance into *out and returns true. Returns + * false if no eligible instance exists (⇒ router responds 503). + * + * @param exclude array of instance_id strings to skip (already-failed picks) + * @param n_exclude length of exclude + */ +bool reg_pick_least_busy(reg_registry *r, const char *model_id, + const char *const *exclude, int n_exclude, + reg_instance *out); + +/** + * Look up a SPECIFIC live instance by instance_id, requiring it to serve + * `model_id` and be within TTL + healthy. On success copies it into *out and + * returns true. Used for session affinity: route a session back to the exact + * instance holding its prefilled prefix cache. Returns false if the instance is + * unknown, serves a different model, or is not live (⇒ caller falls back to + * least-busy). + * + * @param model_id the model the request is for (must match) + * @param instance_id the instance to look up + * Thread-safe: yes (internal locking) + */ +bool reg_find_live(reg_registry *r, const char *model_id, + const char *instance_id, reg_instance *out); + +#endif /* PN_SERVE_REGISTRY_H */ diff --git a/src/serve/registry_client.c b/src/serve/registry_client.c new file mode 100644 index 0000000..9d78aae --- /dev/null +++ b/src/serve/registry_client.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file registry_client.c + * @brief Implementation of the serve-side self-registration client (see + * registry_client.h). Env-gated on PANTHEON_REGISTRY_URL; when unset every + * call is a no-op so serve is unchanged. Builds the register / heartbeat + * / deregister JSON bodies (per registry_wire.h) with json_writer and + * sends them via http_client — no new deps. + * + * @status G0 client. + * @issues None + * @todo None + */ + +#include "registry_client.h" + +#include +#include + +#include "http_client.h" +#include "json_writer.h" + +#define RC_CONNECT_MS 1500 + +/* Read the registry base URL once (e.g. "http://127.0.0.1:8100"). NULL ⇒ off. */ +static const char *registry_url(void) +{ + return getenv("PANTHEON_REGISTRY_URL"); +} + +bool rc_enabled(void) +{ + const char *u = registry_url(); + return u && u[0]; +} + +/* Send a JSON body to $PANTHEON_REGISTRY_URL with `method`. Returns true on + * a 2xx, false on transport error or non-2xx. Frees the writer. */ +static bool send_json(const char *method, const char *path, json_writer *jw) +{ + const char *base = registry_url(); + if (jw->oom) { jw_free(jw); return false; } + int status = 0; + hc_result rc = hc_request_simple(method, base, path, jw->data, jw->len, + RC_CONNECT_MS, &status); + jw_free(jw); + return (rc == HC_OK && status >= 200 && status < 300); +} + +bool rc_register(const rc_identity *id) +{ + if (!rc_enabled()) return true; /* disabled ⇒ no-op success */ + + json_writer jw; + if (!jw_init(&jw)) return false; + jw_raw(&jw, "{\"model_id\":"); + jw_string(&jw, id->model_id); + jw_raw(&jw, ",\"instance_id\":"); + jw_string(&jw, id->instance_id); + jw_raw(&jw, ",\"node_id\":"); + jw_string(&jw, id->node_id); + jw_raw(&jw, ",\"endpoint\":"); + jw_string(&jw, id->endpoint); + jw_raw(&jw, ",\"transport\":"); + jw_string(&jw, id->transport[0] ? id->transport : RW_TRANSPORT_LOCAL); + jw_printf(&jw, ",\"capacity\":%d", id->capacity); + if (id->context_window > 0) + jw_printf(&jw, ",\"context_window\":%d", id->context_window); + jw_raw(&jw, "}"); + return send_json("POST", "/registry/register", &jw); +} + +bool rc_heartbeat(const char *instance_id, const rc_load *load) +{ + if (!rc_enabled()) return true; + + json_writer jw; + if (!jw_init(&jw)) return false; + jw_raw(&jw, "{\"instance_id\":"); + jw_string(&jw, instance_id); + jw_printf(&jw, ",\"load\":{\"active_slots\":%d,\"queue_depth\":%d," + "\"free_slots\":%d", + load->active_slots, load->queue_depth, load->free_slots); + if (load->context_window > 0) + jw_printf(&jw, ",\"context_window\":%d", load->context_window); + jw_printf(&jw, "},\"healthy\":%s}", load->healthy ? "true" : "false"); + return send_json("POST", "/registry/heartbeat", &jw); +} + +bool rc_deregister(const char *instance_id) +{ + if (!rc_enabled()) return true; + + json_writer jw; + if (!jw_init(&jw)) return false; + jw_raw(&jw, "{\"instance_id\":"); + jw_string(&jw, instance_id); + jw_raw(&jw, "}"); + return send_json("DELETE", "/registry/deregister", &jw); +} diff --git a/src/serve/registry_client.h b/src/serve/registry_client.h new file mode 100644 index 0000000..1fd07ac --- /dev/null +++ b/src/serve/registry_client.h @@ -0,0 +1,79 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file registry_client.h + * @brief Serve-side self-registration client (Phase G0). A Pantheon serve + * process uses this to announce its model instances to the dispatch + * registry, heartbeat its live load, and deregister on shutdown. + * + * GATED behind the PANTHEON_REGISTRY_URL environment variable: if it is + * unset, EVERY call here is a no-op and serve behaves exactly as today. + * This is why wiring it into serve_command.c is safe — with no env set, + * nothing changes. (This build ships the client + documents the hook + * points; it does NOT live-integrate it into the running serve loop.) + * + * Wire protocol: see registry_wire.h. The endpoints hit are + * POST $PANTHEON_REGISTRY_URL/registry/register + * POST $PANTHEON_REGISTRY_URL/registry/heartbeat + * DELETE $PANTHEON_REGISTRY_URL/registry/deregister + * + * @status G0 client complete + unit-tested against a stub registry; the + * serve_command.c startup/heartbeat/SIGTERM wiring is documented but + * intentionally NOT applied in this build (a separate human step). + * @issues None + * @todo Wire the three call sites in serve_command.c (see the handoff doc). + */ + +#ifndef PN_SERVE_REGISTRY_CLIENT_H +#define PN_SERVE_REGISTRY_CLIENT_H + +#include + +#include "registry_wire.h" + +/* Identity + reachability of this serve instance, filled once at startup. */ +typedef struct { + char model_id[RW_MODEL_ID_MAX]; /* the shared model identity */ + char instance_id[RW_INSTANCE_ID_MAX]; /* unique per serve process */ + char node_id[RW_NODE_ID_MAX]; + char endpoint[RW_ENDPOINT_MAX]; /* how the router reaches THIS serve */ + char transport[RW_TRANSPORT_MAX]; /* "local" | "wg" */ + int capacity; /* batch-slot count */ + int context_window; /* effective per-session context window + * in tokens (pn_effective_ctx_window) + * at registration; 0 = don't report */ +} rc_identity; + +/* The live load to report on a heartbeat (read from batch_sched). */ +typedef struct { + int active_slots; + int queue_depth; + int free_slots; + int context_window; /* effective per-session context window in tokens + * RIGHT NOW; 0 = don't report (leaves the registry's + * last known value alone). Reported per heartbeat, + * not just at register, because the memory ladder can + * move it at runtime. */ + bool healthy; +} rc_load; + +/** + * @return true if PANTHEON_REGISTRY_URL is set (⇒ registration is active). When + * false, all rc_* calls below are no-ops that return true (success). + */ +bool rc_enabled(void); + +/** + * POST /registry/register with the instance identity. No-op (returns true) if + * registration is disabled. Returns false only on a real network/HTTP failure + * while enabled. + */ +bool rc_register(const rc_identity *id); + +/** POST /registry/heartbeat with the current load. Same no-op/failure rules. */ +bool rc_heartbeat(const char *instance_id, const rc_load *load); + +/** DELETE /registry/deregister for a graceful stop. Same rules. */ +bool rc_deregister(const char *instance_id); + +#endif /* PN_SERVE_REGISTRY_CLIENT_H */ diff --git a/src/serve/registry_wire.h b/src/serve/registry_wire.h new file mode 100644 index 0000000..01ebdb3 --- /dev/null +++ b/src/serve/registry_wire.h @@ -0,0 +1,67 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file registry_wire.h + * @brief Shared wire-protocol contract for the dispatch layer (the client §10 / build + * plan Phase G). Defines the JSON shapes exchanged between a Pantheon + * serve instance (the G0 client) and the registry+router (G1/G2), plus a + * few small limits both sides agree on. This is the single source of + * truth for the field names so serve and router never drift. + * + * Dedup / load-balance semantics (CRITICAL — do not confuse the two ids): + * - `model_id` — the SHARED model identity clients request. This is what + * `GET /v1/models` shows: ONE entry per model_id. Two serve + * processes hosting the same weights advertise the SAME + * model_id (e.g. the fleet's two "ornith"). + * - `instance_id` — UNIQUE per serve process. Multiple instances that share a + * model_id are REPLICAS the router load-balances across. + * + * Endpoints (registry side, local position = no auth): + * POST /registry/register {model_id,instance_id,node_id,endpoint, + * transport,capacity,context_window} + * POST /registry/heartbeat {instance_id,load:{active_slots,queue_depth, + * free_slots,context_window},healthy} + * DELETE /registry/deregister {instance_id} + * + * `context_window` is the instance's EFFECTIVE per-session context window in + * tokens (pn_effective_ctx_window). It rides the HEARTBEAT as well as the + * register because the memory ladder can move it at runtime — a value fixed at + * registration goes stale exactly when it matters. 0/absent = unreported, and + * the router then omits the field rather than advertise a guess. It reaches + * clients unchanged as `context_window` on the router's GET /v1/models and per + * instance in /registry/fleet. + * GET /registry/fleet -> model-level fleet view (ops/TUI) + * GET /v1/models -> deduped-by-model_id live model list + * POST /v1/chat/completions -> least-busy dispatch + SSE pass-through + * POST /v1/completions -> same + * + * Liveness: no heartbeat within RW_TTL_SECONDS ⇒ the registry evicts the + * instance (crash-safe). Serve should heartbeat every RW_HEARTBEAT_SECONDS. + * + * @status G0/G1 contract. Field names below are load-bearing on both sides. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_REGISTRY_WIRE_H +#define PN_SERVE_REGISTRY_WIRE_H + +/* Liveness TTL: an instance with no heartbeat in this many seconds is evicted. */ +#define RW_TTL_SECONDS 15 + +/* Recommended serve heartbeat cadence (well under the TTL). */ +#define RW_HEARTBEAT_SECONDS 3 + +/* Field-length caps for the in-memory instance table (bounded, no heap churn + * per row). Generous but fixed so the table row is a POD struct. */ +#define RW_MODEL_ID_MAX 96 +#define RW_INSTANCE_ID_MAX 96 +#define RW_NODE_ID_MAX 64 +#define RW_ENDPOINT_MAX 256 /* e.g. "http://127.0.0.1:8201" */ +#define RW_TRANSPORT_MAX 16 /* "local" | "wg" */ + +/* Transport values (string payload of the `transport` field). */ +#define RW_TRANSPORT_LOCAL "local" +#define RW_TRANSPORT_WG "wg" + +#endif /* PN_SERVE_REGISTRY_WIRE_H */ diff --git a/src/serve/repetition_loop_detector.c b/src/serve/repetition_loop_detector.c new file mode 100644 index 0000000..7065701 --- /dev/null +++ b/src/serve/repetition_loop_detector.c @@ -0,0 +1,262 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file repetition_loop_detector.c + * @brief Implementation of the decode-time repetition-cycle detector + * (see repetition_loop_detector.h for the thresholds and the reasoning). + * + * Two stages, so the common case (text that is not looping) costs almost + * nothing and the confirmed case costs one compare per token: + * + * PROPOSE. A rolling polynomial hash over the last RLD_SHINGLE_TOKENS token + * ids is kept per position. When the current window's hash equals the hash + * stored at position i-L, the stream MIGHT have period L. The scan runs L + * from 1 upward and takes the FIRST hit, so the distance proposed is the + * fundamental period and never a multiple of it — this is the single most + * important false-positive defence, because every legitimate repeating + * structure (a zero-filled table row, a separator line, a repeated log line) + * has a small fundamental period and is rejected on the spot by min_period. + * + * CONFIRM. A hash hit is verified against the real token ids (a hash + * collision must not be able to end a turn), then the detector LOCKS: from + * there each new token only has to equal the token `period` back. One + * mismatch — one novel token anywhere in the cycle — unlocks immediately and + * the run count is lost, so a "cycle" broken by real content never + * accumulates. Firing needs min_repeats back-to-back copies. + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "repetition_loop_detector.h" + +#include +#include + +/* Odd multiplier for the rolling window hash (the FNV-1a 64-bit prime). Any + * odd base gives a full-period polynomial hash mod 2^64; this one is already + * the project's habitual choice. */ +#define RLD_HASH_BASE 0x100000001b3ULL + +/* Defaults. See the header for why these numbers and not smaller ones. */ +#define RLD_DEFAULT_MIN_PERIOD 48 +#define RLD_DEFAULT_MAX_PERIOD 512 +#define RLD_DEFAULT_MIN_REPEATS 12 + +/* Hard ceiling on max_period, so an operator typo cannot ask for a gigabyte of + * ring. 4096 tokens is ~16 KB of text in one cycle — far past anything a + * degeneration has ever produced here. */ +#define RLD_MAX_PERIOD_CEILING 4096 + +/* Read one non-negative integer env var, falling back to `fallback` when unset, + * empty, or unparseable. Deliberately strict: a garbage value must not + * silently become 0 and disable a guard the operator thought they were tuning. */ +static int rld_env_int(const char *name, int fallback) +{ + const char *raw = getenv(name); + if (!raw || !raw[0]) + return fallback; + char *end = NULL; + long v = strtol(raw, &end, 10); + if (end == raw || (end && *end) || v <= 0 || v > 1000000L) + return fallback; + return (int)v; +} + +bool rld_enabled(void) +{ + static int cached = -1; + if (cached < 0) { + const char *env = getenv("PANTHEON_REPETITION_STOP"); + cached = (env && env[0] == '0' && !env[1]) ? 0 : 1; + } + return cached != 0; +} + +void rld_default_config(rld_config *cfg) +{ + static rld_config cached; + static int resolved = 0; + + if (!cfg) + return; + if (!resolved) { + rld_config c; + c.min_period = rld_env_int("PANTHEON_REPETITION_MIN_PERIOD", + RLD_DEFAULT_MIN_PERIOD); + c.max_period = rld_env_int("PANTHEON_REPETITION_MAX_PERIOD", + RLD_DEFAULT_MAX_PERIOD); + c.min_repeats = rld_env_int("PANTHEON_REPETITION_MIN_REPEATS", + RLD_DEFAULT_MIN_REPEATS); + if (c.min_repeats < 2) c.min_repeats = 2; + if (c.max_period > RLD_MAX_PERIOD_CEILING) c.max_period = RLD_MAX_PERIOD_CEILING; + if (c.min_period > c.max_period) c.min_period = c.max_period; + cached = c; + resolved = 1; + } + *cfg = cached; +} + +/* Smallest power of two >= n (n >= 1). */ +static size_t rld_next_pow2(size_t n) +{ + size_t p = 1; + while (p < n) + p <<= 1; + return p; +} + +bool rld_init(rld_detector *rd, const rld_config *cfg) +{ + if (!rd) + return false; + memset(rd, 0, sizeof(*rd)); + + if (cfg) { + rd->cfg = *cfg; + if (rd->cfg.min_repeats < 2) rd->cfg.min_repeats = 2; + if (rd->cfg.max_period > RLD_MAX_PERIOD_CEILING) + rd->cfg.max_period = RLD_MAX_PERIOD_CEILING; + if (rd->cfg.min_period < 1) rd->cfg.min_period = 1; + if (rd->cfg.min_period > rd->cfg.max_period) + rd->cfg.min_period = rd->cfg.max_period; + } else { + rld_default_config(&rd->cfg); + } + if (rd->cfg.max_period < 1) + return false; + + /* The verify step reaches back max_period + RLD_SHINGLE_TOKENS - 1 tokens, + * so the ring must hold one more than that for the oldest entry to survive + * the write of the newest. */ + size_t need = (size_t)rd->cfg.max_period + RLD_SHINGLE_TOKENS + 1; + size_t cap = rld_next_pow2(need); + + rd->token_ring = calloc(cap, sizeof(*rd->token_ring)); + rd->shingle_ring = calloc(cap, sizeof(*rd->shingle_ring)); + if (!rd->token_ring || !rd->shingle_ring) { + rld_free(rd); /* inert, not fatal: the turn runs unguarded */ + return false; + } + rd->ring_mask = cap - 1; + + rd->shingle_power = 1; + for (int i = 0; i < RLD_SHINGLE_TOKENS - 1; i++) + rd->shingle_power *= RLD_HASH_BASE; + + return true; +} + +void rld_free(rld_detector *rd) +{ + if (!rd) + return; + free(rd->token_ring); + free(rd->shingle_ring); + rd->token_ring = NULL; + rd->shingle_ring = NULL; + rd->ring_mask = 0; +} + +/* Confirm a hash-proposed period against the real token ids: the last + * RLD_SHINGLE_TOKENS tokens must each equal the token `period` earlier. A + * 2^-64 hash collision must never be able to end a live turn, so the proposal + * is never trusted on its own. Runs once per lock, not per token. */ +static bool rld_verify_period(const rld_detector *rd, long last, int period) +{ + size_t mask = rd->ring_mask; + for (int j = 0; j < RLD_SHINGLE_TOKENS; j++) { + long here = last - j; + long back = here - period; + if (back < 0) + return false; + if (rd->token_ring[(size_t)here & mask] != + rd->token_ring[(size_t)back & mask]) + return false; + } + return true; +} + +/* PROPOSE: the smallest L in [1, max_period] at which the current window + * recurs, or 0 when it does not recur at all. Smallest-first is deliberate — + * see the file header. */ +static int rld_propose_period(const rld_detector *rd, long last) +{ + size_t mask = rd->ring_mask; + long oldest_valid = RLD_SHINGLE_TOKENS - 1; /* first full window */ + + for (int period = 1; period <= rd->cfg.max_period; period++) { + long prev = last - period; + if (prev < oldest_valid) + break; + if (rd->shingle_ring[(size_t)prev & mask] == rd->rolling_hash) + return period; + } + return 0; +} + +bool rld_feed(rld_detector *rd, uint32_t token) +{ + if (!rd || !rd->token_ring || rd->tripped) + return false; + + long last = rd->n_fed; + size_t mask = rd->ring_mask; + + /* Roll the window hash forward over the token entering at `last`, dropping + * the one leaving RLD_SHINGLE_TOKENS back. +1 on every id so that token 0 + * still contributes (it is a real token on some tokenizers). */ + if (last >= RLD_SHINGLE_TOKENS) { + uint32_t leaving = + rd->token_ring[(size_t)(last - RLD_SHINGLE_TOKENS) & mask]; + rd->rolling_hash -= rd->shingle_power * ((uint64_t)leaving + 1u); + } + rd->rolling_hash = rd->rolling_hash * RLD_HASH_BASE + ((uint64_t)token + 1u); + + rd->token_ring[(size_t)last & mask] = token; + rd->shingle_ring[(size_t)last & mask] = rd->rolling_hash; + rd->n_fed = last + 1; + + if (last < RLD_SHINGLE_TOKENS - 1) + return false; /* the first window is not complete yet */ + + /* CONFIRM: locked onto a period — one compare per token. A single novel + * token breaks the cycle and drops the whole run, which is what keeps + * "repeated structure with varying content" (a JSON array of distinct + * objects, a table with distinct rows) from ever accumulating. */ + if (rd->locked_period > 0) { + long back = last - rd->locked_period; + if (back < 0 || rd->token_ring[(size_t)back & mask] != token) { + rd->locked_period = 0; + rd->verified_run = 0; + return false; + } + rd->verified_run++; + /* `verified_run` counts positions matching one period back, so N + * back-to-back copies of the block show up as (N-1) periods of run. */ + long needed = (long)rd->locked_period * (rd->cfg.min_repeats - 1); + if (rd->verified_run >= needed) { + rd->tripped = true; + rd->cycle_tokens = rd->locked_period; + rd->repeats = (int)(rd->verified_run / rd->locked_period) + 1; + return true; + } + return false; + } + + int period = rld_propose_period(rd, last); + if (period == 0) + return false; + /* The FUNDAMENTAL period is below the bar: this is structured repetition + * (a table row, a padding line, a repeated log line), not a collapsed + * paragraph. Reject and do not consider any multiple of it. */ + if (period < rd->cfg.min_period) + return false; + if (!rld_verify_period(rd, last, period)) + return false; + + rd->locked_period = period; + rd->verified_run = RLD_SHINGLE_TOKENS; /* the window we just verified */ + return false; +} diff --git a/src/serve/repetition_loop_detector.h b/src/serve/repetition_loop_detector.h new file mode 100644 index 0000000..46ceee1 --- /dev/null +++ b/src/serve/repetition_loop_detector.h @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file repetition_loop_detector.h + * @brief Decode-time detector for a degenerate repeating CYCLE in the generated + * token stream — the "repetition collapse" a heavily quantised model + * falls into at long context, where it stops producing new text and + * re-emits one paragraph over and over until the token budget runs out. + * + * The measured failure (saber-orch, Qwen3.6-35B-A3B at Q3_K_M, ~40k context): + * a 14,535-byte "answer" whose distinct content was ~470 bytes — one paragraph + * cycled 31 times — generated to completion and reported as a normal finish. + * + * ── What is detected ──────────────────────────────────────────────────────── + * A CYCLE, not a repeated token and not a repeated line: a block of `period` + * tokens reproduced byte-for-byte, back to back, with ZERO novel tokens in + * between, `min_repeats` times. Detection runs on TOKEN IDS, never on decoded + * text: the ids are what the loop already has in hand (no allocation, no UTF-8 + * boundary handling), and the emitter holds a partial tail back so the decoded + * text is not even available token-by-token. + * + * ── Why the thresholds are where they are ─────────────────────────────────── + * FALSE POSITIVES ARE THE REAL RISK. This model's job is largely reading and + * writing code, and legitimate output repeats: zero-filled tables, `return + * NULL;` a hundred times, markdown separator rows, ASCII art, a log excerpt + * where one line recurs 200 times. Truncating a legitimate long code block is + * a WORSE failure than the collapse it prevents — it corrupts good output + * silently, where the collapse at least produces obvious garbage. Two + * properties keep the false-positive rate down: + * + * 1. The detector always locks onto the SMALLEST distance at which the + * recent window recurs, i.e. the FUNDAMENTAL period, never a multiple of + * it. So a zero-filled table whose repeating unit is one 7-token row is + * proposed as period 7 — and rejected — rather than being read as a + * 48-token "cycle" three rows long. Every legitimate repeating structure + * we can think of has a repeating unit that is a LINE or a ROW, which is + * exactly where this rule puts it. + * 2. A minimum period (default 48 tokens ≈ 190 chars, several lines) on top + * of that, plus a minimum repeat count (default 12). Together they mean + * the detector cannot fire until the model has emitted at least + * 48 × 12 = 576 tokens of byte-identical, back-to-back repeated content + * with no novel token anywhere in it. + * + * Deliberately NOT detected: short-period degeneration ("the the the", one + * sentence looping). That is precisely the region legitimate output lives in, + * and the repeat/frequency penalties in the sampler already push against it. + * + * ── Cost ──────────────────────────────────────────────────────────────────── + * Bounded per token, no allocation in the hot loop. Per generated token: one + * rolling-hash update (~4 integer ops) and, while NOT locked onto a candidate + * cycle, one scan of the last `max_period` shingle hashes — a contiguous 4 KB + * u64 array, so it is a sequential compare sweep and not a data structure walk. + * Once locked the per-token cost is a single integer compare. + * + * MEASURED (M4, -O3, 2M tokens): 209 ns/token never-locked (the steady state + * for normal output), 3.6 ns/token while locked. Against a ~25 ms K80 decode + * step that is under 0.001% even after allowing an order of magnitude for the + * a K80 host's much slower CPU — and on the batched path it runs on the request + * thread that already detokenizes and streams each token, which is off the + * batch's critical path entirely. + * + * State is two rings sized to the next power of two above `max_period + 32`: + * 12 KB for the default 512-token cap, allocated once per completion. + * + * Depends only on libc — unit-tested standalone in + * tests/test_repetition_loop_detector.c, no engine/CUDA. + * + * @status Complete. Wired into all three decode loops in serve_generation.c + * (single-user, batched driver-sampling, batched legacy lockstep); a + * trip ends the turn with finish_reason SG_FINISH_REPETITION_LOOP. + * @issues None + * @todo None + */ + +#ifndef PN_REPETITION_LOOP_DETECTOR_H +#define PN_REPETITION_LOOP_DETECTOR_H + +#include +#include +#include + +/* Length of the fingerprint window whose recurrence proposes a candidate cycle. + * 32 tokens (~120 chars) is long enough that an accidental recurrence is + * vanishingly unlikely, and short enough that the whole window fits well inside + * the smallest cycle we are willing to act on. */ +#define RLD_SHINGLE_TOKENS 32 + +/* Detector thresholds. See the header comment for the reasoning; every one of + * them is overridable at runtime (rld_default_config). */ +typedef struct { + int min_period; /* shortest cycle we will act on, in tokens */ + int max_period; /* longest cycle we track (bounds the ring memory) */ + int min_repeats; /* back-to-back byte-identical copies required to fire */ +} rld_config; + +/* Detector state for ONE completion. Zero-initialize and rld_init; feed every + * generated token in order; rld_free when the turn ends. */ +typedef struct { + rld_config cfg; + + uint32_t *token_ring; /* token id of absolute index i at [i & mask] */ + uint64_t *shingle_ring; /* hash of the RLD_SHINGLE_TOKENS-token window + * ENDING at absolute index i, at [i & mask] */ + size_t ring_mask; /* ring capacity - 1 (capacity is a power of 2) */ + + long n_fed; /* absolute index of the next token to be fed */ + uint64_t rolling_hash; /* hash of the window ending at the last token */ + uint64_t shingle_power; /* B^(RLD_SHINGLE_TOKENS-1), to drop the token + * leaving the window */ + + int locked_period; /* candidate cycle length; 0 = not locked */ + long verified_run; /* consecutive positions confirmed equal to the + * token `locked_period` back */ + + bool tripped; /* a loop was detected (latched) */ + int cycle_tokens; /* the detected cycle length, in tokens */ + int repeats; /* how many back-to-back copies were confirmed */ +} rld_detector; + +/** + * Is loop detection enabled for this process? + * + * ON by default; `PANTHEON_REPETITION_STOP=0` turns it off. On by default + * because at these thresholds a trip means the model has already emitted 576+ + * tokens of byte-identical, back-to-back repeated content with no novel token + * in it — output that is worthless to the caller whether or not the model was + * degenerate — and because the failure it catches is live in production and is + * currently reported to the caller as a normal, tidy finish. + * + * Read once and cached; process-constant. + * + * Thread-safe: yes (idempotent lazy init of an int) + * Reentrant: yes + * Signal-safe: no (getenv) + */ +bool rld_enabled(void); + +/** + * Fill `cfg` with the process-wide thresholds, resolved once from the + * environment and clamped to sane ranges: + * PANTHEON_REPETITION_MIN_PERIOD (default 48, min 1) + * PANTHEON_REPETITION_MAX_PERIOD (default 512, clamped to [min_period, 4096]) + * PANTHEON_REPETITION_MIN_REPEATS (default 12, min 2) + * + * @param cfg Output; untouched when NULL. + * + * Thread-safe: yes (idempotent lazy init) + * Reentrant: yes + * Signal-safe: no (getenv) + */ +void rld_default_config(rld_config *cfg); + +/** + * Initialize a detector. Allocates the two rings. + * + * @param rd Detector to initialize (fully overwritten). + * @param cfg Thresholds, or NULL for rld_default_config(). + * @return true when the detector is live; false when it is inert (a NULL + * `rd`, or the ring allocation failed). An inert detector is safe + * to feed and free — it simply never fires, so a caller never has + * to branch on the return value to stay correct. + * + * Thread-safe: yes (touches only `rd`) + * Reentrant: yes + * Signal-safe: no (malloc) + */ +bool rld_init(rld_detector *rd, const rld_config *cfg); + +/** + * Feed one generated token id, in generation order. + * + * @param rd Detector (NULL or inert → always false). + * @param token The token id just generated. + * @return true EXACTLY ONCE — on the token at which the loop is + * confirmed. The caller should end the turn and report + * SG_FINISH_REPETITION_LOOP. Every later call returns false, so + * a caller that keeps feeding cannot double-report. + * + * After a trip, `cycle_tokens` and `repeats` describe what was detected. + * + * Thread-safe: no (mutates `rd`; one detector per completion) + * Reentrant: no + * Signal-safe: yes (no allocation, no libc calls) + */ +bool rld_feed(rld_detector *rd, uint32_t token); + +/** Release the detector's rings. NULL-safe; safe to call twice. */ +void rld_free(rld_detector *rd); + +#endif /* PN_REPETITION_LOOP_DETECTOR_H */ diff --git a/src/serve/request_profile.c b/src/serve/request_profile.c new file mode 100644 index 0000000..890292f --- /dev/null +++ b/src/serve/request_profile.c @@ -0,0 +1,160 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file request_profile.c + * @brief Implementation of the per-request wall-clock phase profiler + * (see request_profile.h). + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "request_profile.h" + +#include +#include +#include +#include + +#define PN_REQPROF_PATH_MAX 96 + +/* One request's accumulated timings. Thread-local: the serve owns one detached + * pthread per connection, so this needs no lock and no plumbing. */ +typedef struct { + int active; + double t_begin; + double open_at[PN_REQ_PHASE_COUNT]; /* 0.0 == no span open */ + double total_ms[PN_REQ_PHASE_COUNT]; + char method[8]; + char path[PN_REQPROF_PATH_MAX]; + int has_sid; + int prompt_tok; + int delta_start; + int sys_len; + int generated_tok; +} pn_reqprof_record; + +static _Thread_local pn_reqprof_record g_record; + +static const char *const PHASE_LABELS[PN_REQ_PHASE_COUNT] = { + "http", "json", "route", "admit", "slot_wait", + "tokenize", "cache", "prefill", "first_sample", "decode" +}; + +static double now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1e3 + (double)ts.tv_nsec / 1e6; +} + +int pn_reqprof_enabled(void) +{ + static int cached = -1; + if (cached < 0) { + const char *v = getenv("PN_REQPROF"); + cached = (v && v[0] == '1') ? 1 : 0; + } + return cached; +} + +void pn_reqprof_begin(void) +{ + if (!pn_reqprof_enabled()) + return; + memset(&g_record, 0, sizeof(g_record)); + g_record.active = 1; + g_record.delta_start = -1; /* -1 == never reached the cache decision */ + g_record.t_begin = now_ms(); +} + +void pn_reqprof_phase_start(pn_req_phase phase) +{ + if (!pn_reqprof_enabled() || !g_record.active) + return; + if (phase < 0 || phase >= PN_REQ_PHASE_COUNT) + return; + /* A span already open means a re-entrant/duplicated start: keep the first + * so the phase measures the outermost span rather than silently drifting. */ + if (g_record.open_at[phase] == 0.0) + g_record.open_at[phase] = now_ms(); +} + +void pn_reqprof_phase_end(pn_req_phase phase) +{ + if (!pn_reqprof_enabled() || !g_record.active) + return; + if (phase < 0 || phase >= PN_REQ_PHASE_COUNT) + return; + if (g_record.open_at[phase] == 0.0) + return; + g_record.total_ms[phase] += now_ms() - g_record.open_at[phase]; + g_record.open_at[phase] = 0.0; +} + +void pn_reqprof_note_route(const char *method, const char *path) +{ + if (!pn_reqprof_enabled() || !g_record.active) + return; + if (method) + snprintf(g_record.method, sizeof(g_record.method), "%s", method); + if (path) + snprintf(g_record.path, sizeof(g_record.path), "%s", path); +} + +void pn_reqprof_note_cache(int has_sid, int prompt_tok, int delta_start, + int sys_len) +{ + if (!pn_reqprof_enabled() || !g_record.active) + return; + g_record.has_sid = has_sid; + g_record.prompt_tok = prompt_tok; + g_record.delta_start = delta_start; + g_record.sys_len = sys_len; +} + +void pn_reqprof_note_generated(int generated_tok) +{ + if (!pn_reqprof_enabled() || !g_record.active) + return; + g_record.generated_tok = generated_tok; +} + +void pn_reqprof_end(void) +{ + if (!pn_reqprof_enabled() || !g_record.active) + return; + + double total = now_ms() - g_record.t_begin; + double attributed = 0.0; + char phases[512]; + size_t off = 0; + for (int i = 0; i < PN_REQ_PHASE_COUNT; i++) { + /* Close anything still open so an early-return path (429, error) still + * reports the span it was inside rather than dropping it. */ + if (g_record.open_at[i] != 0.0) + pn_reqprof_phase_end((pn_req_phase)i); + attributed += g_record.total_ms[i]; + int n = snprintf(phases + off, sizeof(phases) - off, " %s=%.1f", + PHASE_LABELS[i], g_record.total_ms[i]); + if (n < 0 || (size_t)n >= sizeof(phases) - off) + break; + off += (size_t)n; + } + + int prefilled = (g_record.delta_start >= 0) + ? g_record.prompt_tok - g_record.delta_start + : g_record.prompt_tok; + + fprintf(stderr, + "[reqprof] %s %s total=%.1fms%s unattributed=%.1f | sid=%d " + "prompt_tok=%d delta_start=%d prefilled_tok=%d sys_len=%d gen=%d\n", + g_record.method[0] ? g_record.method : "?", + g_record.path[0] ? g_record.path : "?", + total, phases, total - attributed, + g_record.has_sid, g_record.prompt_tok, g_record.delta_start, + prefilled, g_record.sys_len, g_record.generated_tok); + fflush(stderr); + g_record.active = 0; +} diff --git a/src/serve/request_profile.h b/src/serve/request_profile.h new file mode 100644 index 0000000..edd563f --- /dev/null +++ b/src/serve/request_profile.h @@ -0,0 +1,99 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file request_profile.h + * @brief Per-request wall-clock phase profiler for the serve request path. + * Attributes one HTTP chat-completion request across transport, JSON + * parse, routing, session admission, slot wait, tokenize, prefix-cache + * lookup, prefill, first-token sampling and decode. + * + * WHY THREAD-LOCAL: the serve handles every connection on its own detached + * pthread (serve_command.c spawn_connection), so a thread-local record needs no + * plumbing through the ~8 call frames between the accept loop and the decode + * loop, and cannot be raced by a concurrent request. + * + * OFF BY DEFAULT. Enabled with PN_REQPROF=1; when disabled every entry point is + * a single cached-int test and no clock is read. + * + * @status Complete. Measurement instrumentation for per-request cost. + * @issues None + * @todo None + */ +#ifndef PANTHEON_REQUEST_PROFILE_H +#define PANTHEON_REQUEST_PROFILE_H + +/** + * Phases of one chat-completion request, in the order the serve executes them. + * Each phase is a disjoint wall-clock span; the phases plus PN_REQ_UNATTRIBUTED + * (total minus the sum) account for the whole server-side request. + */ +typedef enum { + PN_REQ_HTTP_READ = 0, /* accept -> request line + headers + body read */ + PN_REQ_JSON_PARSE, /* jr_parse + parse_chat_request */ + PN_REQ_ROUTE, /* route_model + tool-dialect swap */ + PN_REQ_SESSION_ADMIT, /* pn_prefix_cache_admit (capacity + idle reap) */ + PN_REQ_SLOT_WAIT, /* bs_slot_acquire_or_wait (queue for a decode slot)*/ + PN_REQ_TOKENIZE, /* build_prompt: chat template render + tokenize */ + PN_REQ_CACHE_LOOKUP, /* bs_prefill_resume_sys / bs_prefill_begin */ + PN_REQ_PREFILL, /* sg_prefill_range_sched over the delta */ + PN_REQ_FIRST_SAMPLE, /* sample token 0 from the prefill logits */ + PN_REQ_DECODE, /* token 0 emitted -> stream finished */ + PN_REQ_PHASE_COUNT +} pn_req_phase; + +/** + * @return non-zero when PN_REQPROF=1 in the environment. Cached after the first + * call; safe to call on any hot path. + * + * Thread-safe: yes (idempotent getenv, benign race on the cache) + * Reentrant: yes + * Signal-safe: no + */ +int pn_reqprof_enabled(void); + +/** + * Start profiling the request on THIS thread. Clears all phase accumulators. + * A second call without an intervening pn_reqprof_end simply restarts. + */ +void pn_reqprof_begin(void); + +/** Open a phase span. Nested/overlapping spans of the same phase are ignored. */ +void pn_reqprof_phase_start(pn_req_phase phase); + +/** Close the open span for `phase` and add its duration to that phase's total. */ +void pn_reqprof_phase_end(pn_req_phase phase); + +/** + * Record the request's shape for the report line. + * + * @param method HTTP method (borrowed, copied) + * @param path HTTP path (borrowed, copied) + */ +void pn_reqprof_note_route(const char *method, const char *path); + +/** + * Record what the prefix cache did for this request — the single most important + * field for telling a warm session from a cold one. + * + * @param has_sid non-zero when the client supplied a session id + * @param prompt_tok total prompt tokens after templating + * @param delta_start first token index actually prefilled (0 == full prefill) + * @param sys_len system-prefix boundary length in tokens + */ +void pn_reqprof_note_cache(int has_sid, int prompt_tok, int delta_start, + int sys_len); + +/** Record the number of tokens generated (reported in the summary line). */ +void pn_reqprof_note_generated(int generated_tok); + +/** + * Close the request and print ONE grep-friendly summary line to stderr: + * + * [reqprof] POST /v1/chat/completions total=21800.0ms http=0.4 json=1.2 ... + * | sid=1 prompt_tok=812 delta=0 prefilled=812 gen=300 + * + * No-op when profiling is disabled or pn_reqprof_begin was never called. + */ +void pn_reqprof_end(void); + +#endif /* PANTHEON_REQUEST_PROFILE_H */ diff --git a/src/serve/response_format_grammar.c b/src/serve/response_format_grammar.c new file mode 100644 index 0000000..5722241 --- /dev/null +++ b/src/serve/response_format_grammar.c @@ -0,0 +1,432 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * Response-format decoding grammar — a char-level FSM that constrains a whole + * completion to one syntactically valid JSON object. See + * response_format_grammar.h for the contract; this is the answer-channel analog + * of tool_grammar.c / tool_grammar_json.c, which constrain a TOOL CALL. + * + * Pure C, libc-only. No engine/CUDA deps → unit-tested standalone. + * + * ──────────────────────────────────────────────────────────────────────────── + * The constrained emission + * ──────────────────────────────────────────────────────────────────────────── + * (reasoning, unconstrained) ← only when reasoning_grace is set + * (whitespace) { …a balanced, valid JSON object… } + * + * Everything after the closing brace is out of grammar: the walk reports + * COMPLETE, the serve stops decoding, and the content the client receives parses. + * + * ──────────────────────────────────────────────────────────────────────────── + * Why a pushdown FSM and not a JSON parser + * ──────────────────────────────────────────────────────────────────────────── + * The sampler asks "would this candidate token keep the output on a valid JSON + * prefix?" ~150k times per constrained step, so the check must be (a) cheap and + * (b) non-destructive. A by-value POD state satisfies both: the allowed-check + * copies the struct onto the stack, walks the candidate's chars on the copy, and + * throws it away. That rules out a heap parse tree, so container nesting is a + * fixed-size stack (RFG_MAX_DEPTH) of one byte per open container. + */ + +#include "response_format_grammar.h" + +#include +#include +#include + +/* Literal whose appearance in the raw stream ends the reasoning grace. */ +static const char THINK_CLOSE[] = ""; + +/* Longest literal the walk ever matches char-by-char ("false"). */ +#define RFG_LIT_MAX 8 + +/* Phases of the walk (char-consuming). */ +typedef enum { + RFG_P_REASON = 0, /* unconstrained reasoning; watching for "" */ + RFG_P_PRE, /* whitespace, then the opening '{' is REQUIRED */ + RFG_P_VALUE, /* an array's FIRST element slot: a value, or ']' ("[]") */ + RFG_P_VALUE_REQ, /* a value is REQUIRED here (after ':' or a ',') */ + RFG_P_STRING, /* inside a "…" */ + RFG_P_STR_ESC, /* the previous char was a backslash */ + RFG_P_STR_UNI, /* consuming the 4 hex digits of a \uXXXX escape */ + RFG_P_NUMBER, /* inside a numeric literal */ + RFG_P_LITERAL, /* inside true / false / null */ + RFG_P_KEY, /* an object's FIRST key slot: a key, or '}' ("{}") */ + RFG_P_KEY_REQ, /* a key is REQUIRED here (after a ',') */ + RFG_P_COLON, /* expect ':' after a key (whitespace ok) */ + RFG_P_AFTER, /* after a value: expect ',' or this container's close */ + RFG_P_DONE, /* the top-level object closed — nothing more is legal */ + RFG_P_DEAD, /* off-grammar (only reachable via a fail-open advance) */ +} rfg_phase; + +/* One open container on the nesting stack. */ +#define RFG_C_OBJECT 0 +#define RFG_C_ARRAY 1 + +struct rfg_state { + rfg_phase phase; + bool reasoning_ended; + int think_term; /* rolling prefix of THINK_CLOSE matched */ + + unsigned char stack[RFG_MAX_DEPTH]; /* RFG_C_OBJECT / RFG_C_ARRAY per level */ + int depth; /* open containers (0 before '{') */ + + bool str_is_key; /* the string being read is an object key */ + int uni_left; /* \uXXXX hex digits still required */ + + /* Number sub-walk: JSON forbids a bare "-" or a trailing 'e'/'.'/'+'/'-', + * so a number may only END on a digit. */ + bool num_can_end; + + /* Literal sub-walk (true / false / null). */ + char lit[RFG_LIT_MAX]; + int lit_pos; + + /* Diagnostics only (never affects the walk). */ + int advances; + char death_piece[48]; /* the token text that took the walk to DEAD */ + char death_phase[16]; /* the phase it was in when that happened */ +}; + +/* ── small helpers ── */ + +static bool is_ws(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +static bool is_digit(char c) { + return c >= '0' && c <= '9'; +} + +static bool is_hex(char c) { + return is_digit(c) || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); +} + +static void die(rfg_state *st) { + st->phase = RFG_P_DEAD; +} + +/* Push a container; DEAD when the nesting cap is reached (so a token that would + * exceed RFG_MAX_DEPTH is simply not an allowed continuation). */ +static void push_container(rfg_state *st, unsigned char kind) { + if (st->depth >= RFG_MAX_DEPTH) { die(st); return; } + st->stack[st->depth++] = kind; +} + +/* Close the innermost container. Closing the LAST one completes the walk. */ +static void pop_container(rfg_state *st) { + if (st->depth <= 0) { die(st); return; } + st->depth--; + st->phase = (st->depth == 0) ? RFG_P_DONE : RFG_P_AFTER; +} + +/* Begin a container and position the walk at its first element/key slot. The + * FIRST slot is the only one where the matching close is legal — that is what + * makes `{}` / `[]` valid and `{"a":1,}` / `[1,]` structurally impossible. */ +static void open_container(rfg_state *st, unsigned char kind) { + push_container(st, kind); + if (st->phase == RFG_P_DEAD) return; + st->phase = (kind == RFG_C_OBJECT) ? RFG_P_KEY : RFG_P_VALUE; +} + +/* Consume the first char of a value. Returns false (and kills the walk) when + * `c` cannot begin any JSON value. */ +static bool begin_value_char(rfg_state *st, char c) { + if (c == '{') { open_container(st, RFG_C_OBJECT); return true; } + if (c == '[') { open_container(st, RFG_C_ARRAY); return true; } + if (c == '"') { st->phase = RFG_P_STRING; st->str_is_key = false; return true; } + if (c == '-' || is_digit(c)) { + st->phase = RFG_P_NUMBER; + st->num_can_end = is_digit(c); + return true; + } + if (c == 't' || c == 'f' || c == 'n') { + const char *lit = (c == 't') ? "true" : (c == 'f') ? "false" : "null"; + memcpy(st->lit, lit, strlen(lit) + 1); + st->lit_pos = 1; + st->phase = RFG_P_LITERAL; + return true; + } + die(st); + return false; +} + +/* A value just finished: an array element or object member is complete, so the + * innermost container decides what may follow. */ +static void value_finished(rfg_state *st) { + st->phase = (st->depth == 0) ? RFG_P_DONE : RFG_P_AFTER; +} + +/* ── the char walk ── + * + * `reconsume` is set when the char that ENDED a variable-length token (a number + * or a literal) is itself the next structural char and must be re-fed. */ +static void step_char(rfg_state *st, char c, bool *reconsume); + +static void step_reason(rfg_state *st, char c) { + /* Self-detect "" in the raw stream (the serve emitter's think flag + * is not reliable on every model — see the tool grammars' BUG 1 note). */ + if (c == THINK_CLOSE[st->think_term]) { + st->think_term++; + if (THINK_CLOSE[st->think_term] == '\0') { + st->think_term = 0; + st->reasoning_ended = true; + st->phase = RFG_P_PRE; + } + return; + } + /* Restart the rolling match, allowing an overlap ("<"). */ + st->think_term = (c == THINK_CLOSE[0]) ? 1 : 0; +} + +static void step_char(rfg_state *st, char c, bool *reconsume) { + *reconsume = false; + + switch (st->phase) { + + case RFG_P_REASON: + step_reason(st, c); + return; + + case RFG_P_PRE: + if (is_ws(c)) return; + if (c == '{') { open_container(st, RFG_C_OBJECT); return; } + die(st); + return; + + case RFG_P_VALUE: + if (is_ws(c)) return; + /* An array's first-element slot doubles as its close: `[]` is valid. */ + if (c == ']' && st->depth > 0 && + st->stack[st->depth - 1] == RFG_C_ARRAY) { + pop_container(st); + return; + } + begin_value_char(st, c); + return; + + case RFG_P_VALUE_REQ: + if (is_ws(c)) return; + begin_value_char(st, c); + return; + + case RFG_P_STRING: + if (c == '\\') { st->phase = RFG_P_STR_ESC; return; } + if (c == '"') { + if (st->str_is_key) st->phase = RFG_P_COLON; + else value_finished(st); + return; + } + /* Raw control characters are illegal inside a JSON string. */ + if ((unsigned char)c < 0x20) { die(st); return; } + return; + + case RFG_P_STR_ESC: + if (c == 'u') { st->phase = RFG_P_STR_UNI; st->uni_left = 4; return; } + if (c == '"' || c == '\\' || c == '/' || c == 'b' || c == 'f' || + c == 'n' || c == 'r' || c == 't') { + st->phase = RFG_P_STRING; + return; + } + die(st); + return; + + case RFG_P_STR_UNI: + if (!is_hex(c)) { die(st); return; } + if (--st->uni_left == 0) st->phase = RFG_P_STRING; + return; + + case RFG_P_NUMBER: + if (is_digit(c)) { st->num_can_end = true; return; } + if (c == '.' || c == 'e' || c == 'E' || c == '+' || c == '-') { + st->num_can_end = false; + return; + } + /* Anything else ends the number — but only if it was well-formed. */ + if (!st->num_can_end) { die(st); return; } + value_finished(st); + *reconsume = true; + return; + + case RFG_P_LITERAL: + if (st->lit[st->lit_pos] != c) { die(st); return; } + st->lit_pos++; + if (st->lit[st->lit_pos] == '\0') value_finished(st); + return; + + case RFG_P_KEY: + if (is_ws(c)) return; + if (c == '"') { st->phase = RFG_P_STRING; st->str_is_key = true; return; } + /* An object's first-key slot doubles as its close: `{}` is valid. */ + if (c == '}' && st->depth > 0 && + st->stack[st->depth - 1] == RFG_C_OBJECT) { + pop_container(st); + return; + } + die(st); + return; + + case RFG_P_KEY_REQ: + if (is_ws(c)) return; + if (c == '"') { st->phase = RFG_P_STRING; st->str_is_key = true; return; } + die(st); /* a trailing comma cannot be followed by '}' */ + return; + + case RFG_P_COLON: + if (is_ws(c)) return; + if (c == ':') { st->phase = RFG_P_VALUE_REQ; return; } + die(st); + return; + + case RFG_P_AFTER: + if (is_ws(c)) return; + if (st->depth <= 0) { die(st); return; } + if (c == ',') { + /* A trailing comma is illegal, so the slot after it REQUIRES a key + * (object) or an element (array) — never the container's close. */ + st->phase = (st->stack[st->depth - 1] == RFG_C_OBJECT) + ? RFG_P_KEY_REQ : RFG_P_VALUE_REQ; + return; + } + if (c == '}' && st->stack[st->depth - 1] == RFG_C_OBJECT) { + pop_container(st); + return; + } + if (c == ']' && st->stack[st->depth - 1] == RFG_C_ARRAY) { + pop_container(st); + return; + } + die(st); + return; + + case RFG_P_DONE: + case RFG_P_DEAD: + default: + die(st); + return; + } +} + +/* Walk `text` char by char, honouring the reconsume signal. */ +static void walk(rfg_state *st, const char *text) { + for (const char *p = text; *p; p++) { + bool reconsume = true; + /* At most two passes: a number/literal end reconsumes exactly once, and + * the reconsumed char is always structural (it cannot reconsume again). */ + int guard = 0; + while (reconsume && st->phase != RFG_P_DEAD && guard++ < 2) + step_char(st, *p, &reconsume); + if (st->phase == RFG_P_DEAD) return; + } +} + +/* ── public API ── */ + +rfg_state *rfg_state_new(rfg_format format, bool reasoning_grace) { + if (format == RFG_FORMAT_TEXT) return NULL; + rfg_state *st = calloc(1, sizeof(*st)); + if (!st) return NULL; + st->phase = reasoning_grace ? RFG_P_REASON : RFG_P_PRE; + st->reasoning_ended = !reasoning_grace; + return st; +} + +void rfg_state_free(rfg_state *st) { free(st); } + +void rfg_reasoning_end(rfg_state *st) { + if (!st || st->reasoning_ended) return; + st->reasoning_ended = true; + if (st->phase == RFG_P_REASON) { + st->phase = RFG_P_PRE; + st->think_term = 0; + } +} + +bool rfg_token_allowed(const rfg_state *st, const char *token_text) { + if (!st || !token_text || !*token_text) return true; + if (st->phase == RFG_P_REASON) return true; /* unconstrained grace */ + rfg_state clone = *st; /* POD copy — see the header */ + walk(&clone, token_text); + return clone.phase != RFG_P_DEAD; +} + +void rfg_advance(rfg_state *st, const char *token_text) { + if (!st || !token_text || !*token_text) return; + st->advances++; + /* Record what killed the walk. A DEAD walk means the sampler emitted a + * token the grammar rejects — which should be impossible while the mask is + * active, so the offending piece names the real defect. */ + rfg_phase before = st->phase; + walk(st, token_text); + if (st->phase == RFG_P_DEAD && before != RFG_P_DEAD && + st->death_piece[0] == '\0') { + size_t n = strlen(token_text); + if (n > sizeof(st->death_piece) - 1) n = sizeof(st->death_piece) - 1; + memcpy(st->death_piece, token_text, n); + st->death_piece[n] = '\0'; + const char *pn = rfg_debug_phase_name(st); /* now DEAD; want `before` */ + (void)pn; + rfg_state probe = *st; + probe.phase = before; + snprintf(st->death_phase, sizeof(st->death_phase), "%s", + rfg_debug_phase_name(&probe)); + } +} + +bool rfg_complete(const rfg_state *st) { + return st && st->phase == RFG_P_DONE; +} + +bool rfg_in_json(const rfg_state *st) { + if (!st) return false; + return st->phase != RFG_P_REASON && st->phase != RFG_P_PRE && + st->phase != RFG_P_DONE && st->phase != RFG_P_DEAD; +} + +bool rfg_mask_active(const rfg_state *st) { + if (!st) return false; + return st->phase != RFG_P_REASON && st->phase != RFG_P_DONE && + st->phase != RFG_P_DEAD; +} + +bool rfg_reasoning_ended(const rfg_state *st) { + return st && st->reasoning_ended; +} + +int rfg_debug_depth(const rfg_state *st) { + return st ? st->depth : 0; +} + +int rfg_debug_advances(const rfg_state *st) { + return st ? st->advances : 0; +} + +const char *rfg_debug_death_piece(const rfg_state *st) { + return (st && st->death_piece[0]) ? st->death_piece : ""; +} + +const char *rfg_debug_death_phase(const rfg_state *st) { + return (st && st->death_phase[0]) ? st->death_phase : ""; +} + +const char *rfg_debug_phase_name(const rfg_state *st) { + if (!st) return "none"; + switch (st->phase) { + case RFG_P_REASON: return "reason"; + case RFG_P_PRE: return "pre"; + case RFG_P_VALUE: return "value"; + case RFG_P_VALUE_REQ: return "value_req"; + case RFG_P_STRING: return "string"; + case RFG_P_STR_ESC: return "str_esc"; + case RFG_P_STR_UNI: return "str_uni"; + case RFG_P_NUMBER: return "number"; + case RFG_P_LITERAL: return "literal"; + case RFG_P_KEY: return "key"; + case RFG_P_KEY_REQ: return "key_req"; + case RFG_P_COLON: return "colon"; + case RFG_P_AFTER: return "after"; + case RFG_P_DONE: return "done"; + case RFG_P_DEAD: return "dead"; + default: return "?"; + } +} diff --git a/src/serve/response_format_grammar.h b/src/serve/response_format_grammar.h new file mode 100644 index 0000000..710a06e --- /dev/null +++ b/src/serve/response_format_grammar.h @@ -0,0 +1,154 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file response_format_grammar.h + * @brief Grammar / state-machine that constrains a whole completion to valid + * JSON — the decode-side implementation of OpenAI `response_format`. + * + * The two tool-call grammars (tool_grammar.h / tool_grammar_json.h) constrain a + * TOOL CALL's skeleton. This one constrains the ANSWER: when a request carries + * `response_format: {"type":"json_object"}` the model's content channel must be + * a single, syntactically valid JSON object — which is exactly what the OpenAI + * contract promises and what a client that sends the field is already assuming. + * Ignoring the field (the pre-existing behaviour) hands that client prose it + * will try to `JSON.parse` and fail on, with nothing on the wire saying why. + * + * The public contract MIRRORS tool_grammar.h (rfg_* == tg_*), so the serve + * sampler hook (pn_tool_grammar_hook / the sg_tg_* glue in serve_generation.c) + * is reused unchanged — only the grammar object it drives differs. There is + * exactly ONE constrained-decoding path in the serve; this is a third grammar + * on it, not a second mechanism. + * + * The constrained emission: + * + * (optional reasoning, unconstrained) + * (whitespace) { …one balanced, syntactically valid JSON object… } + * + * REASONING GRACE. A think-style model is prefilled with an open ``, so + * its first tokens are reasoning and must NOT be constrained. `reasoning_grace` + * starts the walk in a FREE region that ends when "" appears in the raw + * token stream (self-detected here, exactly like the tool grammars — the serve + * emitter's think flag is not reliable on every model) or when the caller calls + * rfg_reasoning_end(). With `reasoning_grace` false the walk starts constrained + * immediately (non-think models, or No-Think mode). + * + * JSON validated: objects, arrays, strings (with \\ escapes and \\uXXXX), numbers + * (at least one digit, and a number may not end on '.', 'e', '+' or '-'), the + * literals true/false/null, and whitespace between tokens. Nesting is capped at + * RFG_MAX_DEPTH; a deeper open is simply not an allowed continuation. + * + * Pure C, libc-only — NO engine/CUDA deps — so it is unit-tested in isolation + * (tests/test_response_format_grammar.c) without the forward path. + * + * @status json_object works and is wired into both the single-user and the + * batched decode paths. `text` is a no-op (the default). + * @issues `json_schema` is NOT enforced: a request carrying one is decoded under + * the json_object constraint (valid JSON, arbitrary shape) and the serve + * reports "schema_downgraded" on the wire. Per-schema grammar + * compilation is unimplemented. + * @todo + * - [ ] Compile a `json_schema` into a typed FSM (keys/required/types), the + * way tool_grammar_json.c compiles a tool's parameter schema. + */ + +#ifndef PN_RESPONSE_FORMAT_GRAMMAR_H +#define PN_RESPONSE_FORMAT_GRAMMAR_H + +#include + +/* Maximum JSON container nesting the grammar will allow the model to open. */ +#define RFG_MAX_DEPTH 64 + +/* Requested response format, resolved from the request's `response_format`. */ +typedef enum { + RFG_FORMAT_TEXT = 0, /* {"type":"text"} or absent — no constraint */ + RFG_FORMAT_JSON_OBJECT, /* {"type":"json_object"} — content must be one + * syntactically valid JSON object */ + RFG_FORMAT_JSON_SCHEMA, /* {"type":"json_schema", …} — SHAPE NOT ENFORCED; + * decoded as JSON_OBJECT and reported as + * downgraded (see @issues) */ +} rfg_format; + +/* Mutable per-request walk position (a small POD — the allowed-check clones it + * and walks the candidate on the clone, leaving the live state untouched). */ +typedef struct rfg_state rfg_state; + +/** + * Allocate a walk state for `format`. + * + * @param format RFG_FORMAT_JSON_OBJECT / RFG_FORMAT_JSON_SCHEMA build a + * state; RFG_FORMAT_TEXT returns NULL (nothing to + * constrain — the caller leaves the feature off). + * @param reasoning_grace true for a think-style model whose prompt opens + * ``: the leading reasoning region is + * unconstrained until "". false constrains from + * the first token. + * @return a fresh state, or NULL on OOM / RFG_FORMAT_TEXT. + * + * Thread-safe: yes (no shared state) + */ +rfg_state *rfg_state_new(rfg_format format, bool reasoning_grace); + +/** Free a walk state (NULL-safe). */ +void rfg_state_free(rfg_state *st); + +/** + * Signal that the reasoning phase has ended — the JSON object is required from + * here on. Idempotent, and a no-op once reasoning has already ended. The + * grammar ALSO self-detects "" in its own token stream, so a caller that + * does not track reasoning externally still gets correct behaviour. + */ +void rfg_reasoning_end(rfg_state *st); + +/** + * Would appending `token_text` keep the walk on a valid prefix of the format? + * Does NOT mutate `st` (walks a clone). Empty/NULL text is always allowed (it + * is a no-op). While in the reasoning region every text is allowed. + */ +bool rfg_token_allowed(const rfg_state *st, const char *token_text); + +/** Walk the state forward by `token_text` (must have been allowed). */ +void rfg_advance(rfg_state *st, const char *token_text); + +/** + * Has the walk emitted a COMPLETE top-level JSON object? The serve stops + * decoding here — everything the format promises has been produced, and any + * further token would make the content un-parseable. + */ +bool rfg_complete(const rfg_state *st); + +/** Is the walk currently INSIDE the JSON object (past the opening '{')? */ +bool rfg_in_json(const rfg_state *st); + +/** + * Should the sampler MASK this step? False in the reasoning region (so the + * think block is unconstrained), false once complete, and false if the walk went + * DEAD (fail open rather than wedge a generation). True otherwise. + */ +bool rfg_mask_active(const rfg_state *st); + +/** Has the reasoning region ended (saw "", or never had a grace)? */ +bool rfg_reasoning_ended(const rfg_state *st); + +/* DEBUG: current JSON nesting depth (0 before the object opens). */ +int rfg_debug_depth(const rfg_state *st); + +/* DEBUG: how many times rfg_advance has been called on this state. Zero means + * the sampler never fed the grammar at all — which distinguishes "the constraint + * was wired but never engaged" from "the constraint was never wired", the two + * failure modes that look identical from the response body. */ +int rfg_debug_advances(const rfg_state *st); + +/* DEBUG: the token text that took the walk to DEAD, and the phase it was in + * when that happened ("" if it never died). A DEAD walk means the sampler + * emitted a token the grammar rejects, which cannot happen while the mask is + * working — so these name the defect directly. */ +const char *rfg_debug_death_piece(const rfg_state *st); +const char *rfg_debug_death_phase(const rfg_state *st); + +/* DEBUG: short name of the current phase ("reason", "pre", "value", "done", + * "dead", …). Static string, never NULL — the serve logs it at end of turn so a + * completion that did not end in `done` says WHY on the fleet log. */ +const char *rfg_debug_phase_name(const rfg_state *st); + +#endif /* PN_RESPONSE_FORMAT_GRAMMAR_H */ diff --git a/src/serve/router.c b/src/serve/router.c new file mode 100644 index 0000000..fceba4b --- /dev/null +++ b/src/serve/router.c @@ -0,0 +1,264 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file router.c + * @brief Implementation of the dispatch router (see router.h). Uses json_reader + * to pull the `model` + `stream` fields, registry.c to pick the + * least-busy live instance, and http_client.c to reverse-proxy with SSE + * pass-through. Failover retries the next-least-busy instance as long as + * the failure happened BEFORE any body byte was relayed downstream. + * + * Relay strategy: + * - Streaming (`stream:true`): write the SSE response headers to the client + * first, then relay upstream body bytes verbatim as they arrive (the + * upstream serve already emits `data: …\n\n` framing incl. the terminal + * [DONE]). Unbuffered. + * - Non-streaming: buffer the (small) JSON body from upstream, then send it as + * one application/json response. Buffering here is correct — a non-streamed + * reply is a single JSON object, and it lets a mid-stream upstream error + * still fail over cleanly (nothing was sent downstream yet). + * + * @status G2. + * @issues None + * @todo None + */ + +#include "router.h" + +#include +#include +#include +#include +#include + +#include "http_client.h" +#include "http_server.h" +#include "json_reader.h" +#include "session_key.h" +#include "json_writer.h" + +/* Max failover attempts (distinct instances tried for one request). */ +#define ROUTER_MAX_TRIES 4 +/* Connect timeout for an upstream, ms. */ +#define ROUTER_CONNECT_MS 2000 + +/* ---- streaming relay: write upstream body bytes straight to the client ---- */ + +typedef struct { + int client_fd; + int header_sent; /* SSE header written yet */ +} stream_relay; + +static int write_all_fd(int fd, const char *buf, size_t len) +{ + size_t off = 0; + while (off < len) { + ssize_t n = write(fd, buf + off, len - off); + if (n < 0) { if (errno == EINTR) continue; return -1; } + off += (size_t)n; + } + return 0; +} + +static int stream_sink(const char *bytes, size_t len, void *user) +{ + stream_relay *sr = user; + if (!sr->header_sent) { + hs_begin_sse(sr->client_fd); /* 200 + text/event-stream headers */ + sr->header_sent = 1; + } + /* Relay verbatim — the upstream already frames SSE events. */ + return write_all_fd(sr->client_fd, bytes, len); +} + +/* ---- non-streaming relay: accumulate the body into a growable buffer ---- */ + +typedef struct { + char *data; + size_t len; + size_t cap; + int oom; +} buf_sink; + +static int buffer_sink(const char *bytes, size_t len, void *user) +{ + buf_sink *b = user; + if (b->len + len + 1 > b->cap) { + size_t ncap = b->cap ? b->cap * 2 : 4096; + while (ncap < b->len + len + 1) ncap *= 2; + char *g = realloc(b->data, ncap); + if (!g) { b->oom = 1; return -1; } + b->data = g; + b->cap = ncap; + } + memcpy(b->data + b->len, bytes, len); + b->len += len; + b->data[b->len] = '\0'; + return 0; +} + +/* Outcome of one attempt against a picked instance. TERMINAL means the client + * was fully answered (success OR an unrecoverable mid-relay failure) and the + * caller must stop; FAILOVER means nothing was relayed and the caller should try + * the next instance. */ +typedef enum { + ATTEMPT_TERMINAL = 0, + ATTEMPT_FAILOVER = 1, +} attempt_outcome; + +/* Proxy one request to `pick` and relay the response to the client. `extra_hdrs` + * is forwarded upstream (the X-Session-Id line, or NULL). Returns whether the + * caller should fail over to another instance. */ +static attempt_outcome proxy_to_instance(int client_fd, const reg_instance *pick, + const char *path, const char *body, + size_t body_len, bool streaming, + const char *extra_hdrs) +{ + int status = 0; + hc_result rc; + if (streaming) { + stream_relay sr = { .client_fd = client_fd, .header_sent = 0 }; + rc = hc_post_stream(pick->endpoint, path, body, body_len, extra_hdrs, + ROUTER_CONNECT_MS, stream_sink, &sr, &status); + if (rc == HC_OK || rc == HC_RELAY_ABORTED) + return ATTEMPT_TERMINAL; + if (rc == HC_MIDSTREAM_FAILED) { + /* Bytes already went downstream — cannot cleanly retry; just end + * the (broken) SSE stream and stop. */ + if (sr.header_sent) hs_end_sse(client_fd); + return ATTEMPT_TERMINAL; + } + return ATTEMPT_FAILOVER; /* HC_CONNECT_FAILED / HC_UPSTREAM_ERROR */ + } + + buf_sink b = {0}; + rc = hc_post_stream(pick->endpoint, path, body, body_len, extra_hdrs, + ROUTER_CONNECT_MS, buffer_sink, &b, &status); + if (rc == HC_OK) { + if (b.oom || !b.data) { + free(b.data); + hs_send_error(client_fd, 502, "upstream_error", + "empty upstream response"); + } else { + hs_send_json(client_fd, status ? status : 200, b.data); + free(b.data); + } + return ATTEMPT_TERMINAL; + } + free(b.data); + if (rc == HC_MIDSTREAM_FAILED || rc == HC_RELAY_ABORTED) { + hs_send_error(client_fd, 502, "upstream_error", + "upstream failed mid-response"); + return ATTEMPT_TERMINAL; + } + return ATTEMPT_FAILOVER; /* pre-relay failure */ +} + +/* Pick the instance to try first. With a session bound to a live instance of the + * requested model (and not excluded), reuse it — that instance's prefix cache + * holds the session's KV. Otherwise pick least-busy. Returns true on a pick. */ +static bool pick_instance(reg_registry *reg, session_affinity *sa, + const char *model, const char *session_id, + const char *const *excluded, int n_excluded, + reg_instance *out) +{ + char bound[RW_INSTANCE_ID_MAX]; + if (sa && session_id && session_id[0] && + sa_lookup(sa, session_id, bound, sizeof(bound))) { + bool skip = false; + for (int x = 0; x < n_excluded; x++) + if (excluded[x] && strcmp(excluded[x], bound) == 0) { skip = true; break; } + if (!skip && reg_find_live(reg, model, bound, out)) + return true; + /* Bound instance is gone/unhealthy/excluded → fall through to least-busy. */ + } + return reg_pick_least_busy(reg, model, excluded, n_excluded, out); +} + +void router_handle_completion(int client_fd, reg_registry *reg, + session_affinity *sa, const char *path, + const char *body, const char *session_id) +{ + if (!body) { + hs_send_error(client_fd, 400, "invalid_request", "missing request body"); + return; + } + jr_value *root = jr_parse(body); + if (!root || jr_kind(root) != JR_OBJECT) { + jr_free(root); + hs_send_error(client_fd, 400, "invalid_request", "malformed JSON body"); + return; + } + const char *model = jr_as_string(jr_object_get(root, "model")); + bool streaming = jr_as_bool(jr_object_get(root, "stream"), false); + /* A body "session_id" overrides the X-Session-Id header (matches the serve's + * openai_chat.c precedence), so affinity keys on the SAME id the serve caches + * under. Copy both out before the parse tree is freed. */ + const char *body_sid = pn_body_session_key(root); + char model_copy[RW_MODEL_ID_MAX]; + char sid_copy[SA_SESSION_ID_MAX]; + sid_copy[0] = '\0'; + if (body_sid && body_sid[0]) + snprintf(sid_copy, sizeof(sid_copy), "%s", body_sid); + else if (session_id && session_id[0]) + snprintf(sid_copy, sizeof(sid_copy), "%s", session_id); + if (!model) { + jr_free(root); + hs_send_error(client_fd, 400, "invalid_request", + "missing 'model' field"); + return; + } + snprintf(model_copy, sizeof(model_copy), "%s", model); + jr_free(root); /* done with the parse tree; body stays valid for the proxy */ + + size_t body_len = strlen(body); + const char *sid = sid_copy[0] ? sid_copy : NULL; + + /* Forward the effective session id upstream so the serve keys its prefix + * cache on it (the header the serve's http_server.c reads). */ + char extra_hdrs[SA_SESSION_ID_MAX + 32]; + const char *fwd_hdrs = NULL; + if (sid) { + snprintf(extra_hdrs, sizeof(extra_hdrs), "X-Session-Id: %s\r\n", sid); + fwd_hdrs = extra_hdrs; + } + + /* Failover loop: try up to ROUTER_MAX_TRIES distinct instances, excluding + * ones that already failed pre-relay. The first pick honours affinity. */ + const char *excluded[ROUTER_MAX_TRIES]; + int n_excluded = 0; + + for (int attempt = 0; attempt < ROUTER_MAX_TRIES; attempt++) { + reg_instance pick; + if (!pick_instance(reg, sa, model_copy, sid, excluded, n_excluded, &pick)) { + hs_send_error(client_fd, 503, "model_unavailable", + "no live instance for the requested model"); + return; + } + + attempt_outcome oc = proxy_to_instance(client_fd, &pick, path, body, + body_len, streaming, fwd_hdrs); + if (oc == ATTEMPT_TERMINAL) { + /* Bind the session to the instance that served it, so its next turn + * reuses this instance's warm prefix cache. */ + if (sid) + sa_record(sa, sid, pick.instance_id); + return; + } + + /* Record the failed instance so we don't re-pick it, then retry. */ + if (n_excluded < ROUTER_MAX_TRIES) { + /* pick.instance_id lives on the stack; stash a stable copy. The + * excluded[] entries must outlive the loop iteration, so keep a + * parallel static-lifetime store. */ + static _Thread_local char store[ROUTER_MAX_TRIES][RW_INSTANCE_ID_MAX]; + snprintf(store[n_excluded], RW_INSTANCE_ID_MAX, "%s", pick.instance_id); + excluded[n_excluded] = store[n_excluded]; + n_excluded++; + } + } + + /* Exhausted all attempts with only pre-relay failures. */ + hs_send_error(client_fd, 503, "model_unavailable", + "all live instances failed to respond"); +} diff --git a/src/serve/router.h b/src/serve/router.h new file mode 100644 index 0000000..b42de04 --- /dev/null +++ b/src/serve/router.h @@ -0,0 +1,53 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file router.h + * @brief Dispatch router (Phase G2): the request-facing side of the registry. + * Parses the OpenAI `model` field, picks the least-busy live instance of + * that model from the registry, reverse-proxies the request to it with + * SSE streaming pass-through, and fails over to the next-least-busy + * instance if the chosen one errors before any bytes are relayed. + * + * Endpoints handled: + * POST /v1/chat/completions + * POST /v1/completions + * + * Session affinity (PC0.2): a request carrying an X-Session-Id is routed to the + * instance that last served that session (where the serve's prefix cache holds + * its prefilled KV), forwarding the X-Session-Id header upstream. A new session + * (or one whose bound instance is gone) falls back to least-busy and is + * recorded. Requests with no session id route least-busy exactly as before. + * + * @status PC0.2: least-busy pick, failover, streaming + non-streaming relay, + * session affinity + X-Session-Id forwarding. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_ROUTER_H +#define PN_SERVE_ROUTER_H + +#include "registry.h" +#include "session_affinity.h" + +/** + * Handle a POST /v1/chat/completions or /v1/completions request. Reads `model` + * from the JSON body, picks a live instance (session-affinity when a session id + * is given, else least-busy), reverse-proxies with SSE pass-through, and relays + * the response to `client_fd`. Sends 400 on a missing/unparseable model, + * 503 model_unavailable when no live instance exists (or all failovers fail). + * + * @param client_fd the downstream client connection + * @param reg the live-instance registry + * @param sa the session -> instance affinity map (NULL disables it) + * @param path the upstream path to proxy to ("/v1/chat/completions" etc.) + * @param body the request body (JSON), NUL-terminated + * @param session_id the X-Session-Id header value, or NULL (anonymous) + * + * Thread-safe: yes (reg + sa carry their own locks) + */ +void router_handle_completion(int client_fd, reg_registry *reg, + session_affinity *sa, const char *path, + const char *body, const char *session_id); + +#endif /* PN_SERVE_ROUTER_H */ diff --git a/src/serve/router_command.c b/src/serve/router_command.c new file mode 100644 index 0000000..57e0c09 --- /dev/null +++ b/src/serve/router_command.c @@ -0,0 +1,288 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file router_command.c + * @brief Implementation of `pantheon router` (see router_command.h). Same + * accept-loop shape as serve_command.c — one detached pthread per + * connection — plus a background TTL-sweep thread that evicts instances + * whose heartbeat lapsed. No CUDA, no model load: this is the pure + * host-side dispatch layer. + * + * Routes: + * POST /registry/register — a serve announces a model instance + * POST /registry/heartbeat — refresh liveness + load + * DELETE /registry/deregister — graceful removal + * GET /registry/fleet — model-level fleet view (TUI/ops) + * GET /v1/models — deduped-by-model_id live model list + * POST /v1/chat/completions — least-busy dispatch + SSE pass-through + * POST /v1/completions — same + * + * @status G1+G2. + * @issues None + * @todo None + */ + +#include "router_command.h" + +#include +#include +#include +#include +#include +#include + +#include "http_server.h" +#include "registry.h" +#include "router.h" +#include "session_affinity.h" + +#define ROUTER_DEFAULT_HOST "127.0.0.1" +#define ROUTER_DEFAULT_PORT 8100 +#define ROUTER_SWEEP_INTERVAL_SEC 2 +/* Evicted-instance ids reported per sweep so their session bindings are dropped. + * A single sweep rarely evicts more than a handful; extra evictions still remove + * from the registry, they just aren't reported this pass (a later sweep is a + * no-op for them, and their sessions simply re-route least-busy meanwhile). */ +#define ROUTER_SWEEP_REPORT_MAX 32 + +typedef struct { + reg_registry *reg; + session_affinity *sa; +} router_ctx; + +/* Args for the background TTL sweep thread (registry + affinity map). */ +typedef struct { + reg_registry *reg; + session_affinity *sa; +} sweep_ctx; + +typedef struct { + int client_fd; + router_ctx *ctx; +} connection_job; + +static volatile int g_running = 1; + +static void print_router_usage(void) +{ + printf("Usage: pantheon router [--host ] [--port ]\n\n"); + printf("Runs the dispatch layer: model-instance registry + least-busy router.\n"); + printf("Local position (no auth). Serve instances self-register against it.\n\n"); + printf("Options:\n"); + printf(" --host Bind address (default %s)\n", ROUTER_DEFAULT_HOST); + printf(" --port Port (default %d)\n", ROUTER_DEFAULT_PORT); + printf("\nEndpoints:\n"); + printf(" POST /registry/register POST /registry/heartbeat\n"); + printf(" DELETE /registry/deregister GET /registry/fleet\n"); + printf(" GET /v1/models POST /v1/chat/completions\n"); + printf(" POST /v1/completions\n"); +} + +/* Send a small {"status":"ok"} or an error based on a reg_status. */ +static void reply_reg_status(int cfd, reg_status st) +{ + switch (st) { + case REG_OK: + hs_send_json(cfd, 200, "{\"status\":\"ok\"}"); + break; + case REG_BAD_REQUEST: + hs_send_error(cfd, 400, "invalid_request", + "malformed body or missing required field"); + break; + case REG_FULL: + hs_send_error(cfd, 503, "registry_full", + "instance table is full"); + break; + case REG_NOT_FOUND: + hs_send_error(cfd, 404, "not_found", "unknown instance_id"); + break; + } +} + +static void dispatch(int cfd, router_ctx *ctx, const hs_request *req) +{ + if (strcmp(req->method, "OPTIONS") == 0) { + hs_send_json(cfd, 200, "{}"); + return; + } + + /* ---- registry control plane ---- */ + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/registry/register") == 0) { + reply_reg_status(cfd, reg_register_json(ctx->reg, req->body)); + return; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/registry/heartbeat") == 0) { + reply_reg_status(cfd, reg_heartbeat_json(ctx->reg, req->body)); + return; + } + if (strcmp(req->method, "DELETE") == 0 && + strcmp(req->path, "/registry/deregister") == 0) { + char removed[RW_INSTANCE_ID_MAX]; + reg_status st = reg_deregister_json_collect(ctx->reg, req->body, + removed, sizeof(removed)); + if (st == REG_OK) + sa_forget_instance(ctx->sa, removed); /* drop its session bindings */ + reply_reg_status(cfd, st); + return; + } + if (strcmp(req->method, "GET") == 0 && + strcmp(req->path, "/registry/fleet") == 0) { + char *json = reg_fleet_json(ctx->reg); + if (!json) { hs_send_error(cfd, 500, "server_error", "oom"); return; } + hs_send_json(cfd, 200, json); + free(json); + return; + } + + /* ---- client plane ---- */ + if (strcmp(req->method, "GET") == 0 && strcmp(req->path, "/v1/models") == 0) { + char *json = reg_models_json(ctx->reg); + if (!json) { hs_send_error(cfd, 500, "server_error", "oom"); return; } + hs_send_json(cfd, 200, json); + free(json); + return; + } + if (strcmp(req->method, "POST") == 0 && + (strcmp(req->path, "/v1/chat/completions") == 0 || + strcmp(req->path, "/v1/completions") == 0)) { + router_handle_completion(cfd, ctx->reg, ctx->sa, req->path, req->body, + req->session_id); + return; + } + if (strcmp(req->method, "GET") == 0 && strcmp(req->path, "/") == 0) { + hs_send_json(cfd, 200, + "{\"service\":\"pantheon-router\",\"status\":\"ok\"}"); + return; + } + hs_send_error(cfd, 404, "not_found", "unknown route"); +} + +static void handle_connection(int cfd, router_ctx *ctx) +{ + hs_request req; + if (hs_read_request(cfd, &req) == 0) { + fprintf(stderr, "router: %s %s\n", req.method, req.path); + dispatch(cfd, ctx, &req); + hs_request_free(&req); + } + hs_close(cfd); +} + +static void *connection_thread(void *arg) +{ + connection_job *job = arg; + handle_connection(job->client_fd, job->ctx); + free(job); + return NULL; +} + +static void spawn_connection(int cfd, router_ctx *ctx) +{ + connection_job *job = malloc(sizeof(*job)); + if (!job) { handle_connection(cfd, ctx); return; } + job->client_fd = cfd; + job->ctx = ctx; + + pthread_attr_t attr; + if (pthread_attr_init(&attr) != 0) { handle_connection(cfd, ctx); free(job); return; } + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + pthread_t tid; + int rc = pthread_create(&tid, &attr, connection_thread, job); + pthread_attr_destroy(&attr); + if (rc != 0) { + handle_connection(cfd, ctx); + free(job); + } +} + +/* Background TTL sweep: evict instances that stopped heartbeating, and drop the + * session-affinity bindings that pointed at them (so those sessions re-route + * least-busy on their next turn instead of pinning to a dead instance). */ +static void *sweep_thread(void *arg) +{ + sweep_ctx *sc = arg; + while (g_running) { + sleep(ROUTER_SWEEP_INTERVAL_SEC); + char evicted[ROUTER_SWEEP_REPORT_MAX][RW_INSTANCE_ID_MAX]; + int reported = 0; + int n = reg_sweep_collect(sc->reg, time(NULL), evicted, + ROUTER_SWEEP_REPORT_MAX, &reported); + for (int i = 0; i < reported; i++) + sa_forget_instance(sc->sa, evicted[i]); + if (n > 0) + fprintf(stderr, "router: TTL sweep evicted %d stale instance%s\n", + n, n == 1 ? "" : "s"); + } + return NULL; +} + +int pn_router_main(int argc, char **argv) +{ + const char *host = ROUTER_DEFAULT_HOST; + int port = ROUTER_DEFAULT_PORT; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + print_router_usage(); + return 0; + } else if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) { + host = argv[++i]; + } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + port = atoi(argv[++i]); + } else { + fprintf(stderr, "router: unknown option '%s'\n", argv[i]); + print_router_usage(); + return 1; + } + } + + static reg_registry reg; /* process lifetime (accept loop is infinite) */ + if (reg_init(®) != 0) { + fprintf(stderr, "router: failed to init registry\n"); + return 1; + } + + static session_affinity sa; /* process lifetime; session -> instance map */ + if (sa_init(&sa) != 0) { + fprintf(stderr, "router: failed to init session-affinity map\n"); + reg_destroy(®); + return 1; + } + + static sweep_ctx sweeper_ctx; + sweeper_ctx.reg = ® + sweeper_ctx.sa = &sa; + pthread_t sweeper; + if (pthread_create(&sweeper, NULL, sweep_thread, &sweeper_ctx) != 0) { + fprintf(stderr, "router: failed to start TTL sweep thread\n"); + sa_destroy(&sa); + reg_destroy(®); + return 1; + } + + int listen_fd = hs_listen(host, port); + if (listen_fd < 0) { + sa_destroy(&sa); + reg_destroy(®); + return 1; + } + fprintf(stderr, "router: dispatch layer listening on http://%s:%d " + "(TTL %ds, sweep %ds, session affinity on)\n", + host, port, RW_TTL_SECONDS, ROUTER_SWEEP_INTERVAL_SEC); + + router_ctx ctx = { .reg = ®, .sa = &sa }; + for (;;) { + int cfd = hs_accept(listen_fd); + if (cfd < 0) continue; + spawn_connection(cfd, &ctx); + } + + /* Unreached (infinite accept loop). */ + hs_close(listen_fd); + sa_destroy(&sa); + reg_destroy(®); + return 0; +} diff --git a/src/serve/router_command.h b/src/serve/router_command.h new file mode 100644 index 0000000..b7ba874 --- /dev/null +++ b/src/serve/router_command.h @@ -0,0 +1,26 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file router_command.h + * @brief `pantheon router` entrypoint (Phase G1+G2): run the dispatch layer — + * an in-memory model-instance registry (self-registration + heartbeat + + * TTL) plus a least-busy load-balancing router that reverse-proxies + * /v1/chat/completions (+ /v1/completions) to the freest live instance. + * Local position: no auth. Reuses Pantheon's serve HTTP server. + * + * @status G1+G2. CUDA-free, host-side only. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_ROUTER_COMMAND_H +#define PN_SERVE_ROUTER_COMMAND_H + +/** + * Run the router subcommand. `argc`/`argv` are the arguments AFTER the "router" + * token (argv[0] == "router"). Blocks serving requests until interrupted. + * @return process exit code (0 on clean shutdown, non-zero on setup error). + */ +int pn_router_main(int argc, char **argv); + +#endif /* PN_SERVE_ROUTER_COMMAND_H */ diff --git a/src/serve/serve_backend_guard.c b/src/serve/serve_backend_guard.c new file mode 100644 index 0000000..c098be2 --- /dev/null +++ b/src/serve/serve_backend_guard.c @@ -0,0 +1,125 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_backend_guard.c + * @brief Implementation of the degraded-start refusal (see + * serve_backend_guard.h). + * + * The message text is here rather than at the two call sites on purpose: an + * operator who hits this at startup and an operator who hits it through + * /admin/models/load are looking at the same mistake, and a second copy of the + * wording is a second thing to let drift. + * + * @status Complete; unit-tested (tests/test_backend_guard.c). + * @issues None + * @todo None + */ + +#include "serve_backend_guard.h" + +#include +#include + +#include "../accel/backend_identity.h" + +pn_backend_guard_verdict pn_backend_guard_check(int n_dies_requested, + bool die_backend_compiled, + int dies_visible) +{ + /* No dies asked for = a deliberate CPU serve. Always allowed, on every + * backend: this is the supported way to run without an accelerator, and it + * has to keep working or the guard just becomes a thing people disable. */ + if (n_dies_requested <= 0) + return PN_BACKEND_GUARD_OK; + + if (!die_backend_compiled) + return PN_BACKEND_GUARD_NO_DIE_BACKEND; + + if (dies_visible <= 0) + return PN_BACKEND_GUARD_NO_DIES_VISIBLE; + + return PN_BACKEND_GUARD_OK; +} + +const char *pn_backend_guard_short_reason(pn_backend_guard_verdict verdict) +{ + switch (verdict) { + case PN_BACKEND_GUARD_NO_DIE_BACKEND: + return "dies requested but this binary has no accelerator backend " + "compiled in (see the startup backend= line)"; + case PN_BACKEND_GUARD_NO_DIES_VISIBLE: + return "dies requested but no accelerator dies are visible to this " + "process (see GET /admin/dies)"; + case PN_BACKEND_GUARD_OK: + default: + return NULL; + } +} + +/* Render "0,1" (or "-" when the list is empty) into out[]. Bounded; a list too + * long for the buffer is truncated rather than overrunning it. */ +static void format_die_list(const int *dies, int n_dies, char *out, + size_t out_len) +{ + size_t used = 0; + + if (!out || out_len == 0) + return; + out[0] = '\0'; + if (!dies || n_dies <= 0) { + snprintf(out, out_len, "-"); + return; + } + for (int i = 0; i < n_dies && used + 1 < out_len; i++) { + int n = snprintf(out + used, out_len - used, "%s%d", i ? "," : "", + dies[i]); + if (n < 0 || (size_t)n >= out_len - used) { + out[out_len - 1] = '\0'; + return; + } + used += (size_t)n; + } +} + +int pn_backend_guard_message(pn_backend_guard_verdict verdict, + const char *model_name, + const int *dies, int n_dies, + char *out, size_t out_len) +{ + char die_list[128]; + const char *name = (model_name && *model_name) ? model_name : "(unnamed)"; + + if (!out || out_len == 0) + return 0; + out[0] = '\0'; + if (verdict == PN_BACKEND_GUARD_OK) + return 0; + + format_die_list(dies, n_dies, die_list, sizeof(die_list)); + + if (verdict == PN_BACKEND_GUARD_NO_DIE_BACKEND) { + /* The exact case that shipped: `make` (CPU) writing the same output + * path a `make USE_CUDA=1` had written, then serving a MoE off a + * Pentium. Name the binary to build — the deploy path being wrong is + * the failure, so "use the right build" without the command is not a + * fix. */ + return snprintf(out, out_len, + "serve: REFUSED to start — model '%s' asks for die(s) %s but this binary " + "has NO accelerator backend compiled in (backend=%s build=%s). It would " + "load the model and answer every request on the host CPU with every die " + "idle, and nothing in the log would say so. Fix: deploy an accelerator " + "build — `make USE_CUDA=1` produces ./pantheon-cuda, `make USE_TT=1` " + "produces ./pantheon-tt — or drop --dies to serve on the CPU " + "deliberately.", + name, die_list, pn_backend_name(), pn_backend_build_commit()); + } + + return snprintf(out, out_len, + "serve: REFUSED to start — model '%s' asks for die(s) %s and this binary has " + "the %s backend, but the die manager probed 0 dies (build=%s). It would load " + "the model and answer every request on the host CPU, as silently as if the " + "dies had been honoured. Fix: make the dies visible (driver loaded, card " + "present, CUDA_VISIBLE_DEVICES not empty) — or drop --dies to serve on the " + "CPU deliberately.", + name, die_list, pn_backend_name(), pn_backend_build_commit()); +} diff --git a/src/serve/serve_backend_guard.h b/src/serve/serve_backend_guard.h new file mode 100644 index 0000000..8abed33 --- /dev/null +++ b/src/serve/serve_backend_guard.h @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_backend_guard.h + * @brief The one decision that answers "was this serve asked to use dies it + * cannot possibly use?", plus the operator-facing text for each way the + * answer can be yes. + * + * The incident: a CPU-only binary accepted `--dies 0,1`, loaded a 35B + * MoE, and served it on a 2-core no-AVX Pentium with all 8 K80 dies at + * 0 MiB. Nothing refused, and the only trace in the log was + * `placed on dies 0,1 (0.0 GiB projected)` — which reads like a rounding + * artifact, not like "there is no GPU here". bs_enabled()/bs_create() + * are #ifndef USE_CUDA stubs, so the batch scheduler was NULL too and + * every second chat 429'd, at an advertised max_concurrent of 2. + * + * A warning would not have helped: the serve had already been running + * for an unknown period and measurements had already been taken off it. + * The only thing that catches this is refusing to start. + * + * Both callers use this: pn_serve_main pre-flights every `--model` spec + * BEFORE loading anything (so the operator gets the message in a second, + * not after a 55 s weight upload), and mr_load re-checks so a runtime + * POST /admin/models/load with a `dies` array is refused by the same + * rule rather than by a second, drifting copy of it. + * + * A CPU serve stays fully available and is asked for the obvious way: + * do not pass `--dies`. n_dies == 0 is always OK here, on every backend. + * + * Note the first arm is COMPILE-TIME, deliberately, and is checked + * before the die count. pn_dm_init_logical() can hand a CPU build a + * non-zero pn_dm_count() (it is placement bookkeeping for the Mac/test + * path — see die_manager.h), and trusting the count alone would let + * exactly the incident's shape through again on any host that ever calls + * it. Logical dies are not silicon, so an explicit --dies against them is + * refused too. + * + * @status Complete; unit-tested (tests/test_backend_guard.c). Pure logic — no + * probing, no I/O. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_BACKEND_GUARD_H +#define PN_SERVE_BACKEND_GUARD_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Why a die request cannot be honoured — or that it can. */ +typedef enum { + /* Either no dies were requested (a deliberate CPU serve) or they can be + * honoured. Nothing to say. */ + PN_BACKEND_GUARD_OK = 0, + /* Dies were named and this binary has NO die-capable backend compiled in. + * The `make` that produced it was a CPU build (or a Metal one). This is the + * incident's failure mode. */ + PN_BACKEND_GUARD_NO_DIE_BACKEND, + /* Dies were named, the backend IS compiled in, but the die manager probed + * zero dies — no driver, no card, an empty CUDA_VISIBLE_DEVICES. The + * process would still serve, on the CPU, exactly as silently. */ + PN_BACKEND_GUARD_NO_DIES_VISIBLE, +} pn_backend_guard_verdict; + +/** + * Decide whether a die request can be honoured. + * + * @param n_dies_requested How many dies this model asked for (0 = none, the + * deliberate CPU serve — always OK). + * @param die_backend_compiled pn_backend_supports_die_placement(). + * @param dies_visible pn_dm_count() — dies the manager actually holds. + * @return The verdict; PN_BACKEND_GUARD_OK to proceed. + * + * Pure: no globals read, no I/O, so the caller supplies both facts and the test + * can supply any combination of them. + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +pn_backend_guard_verdict pn_backend_guard_check(int n_dies_requested, + bool die_backend_compiled, + int dies_visible); + +/** + * The full operator message for a non-OK verdict: what was asked, what this + * binary is, why continuing would be worse than stopping, and the two ways + * forward (deploy an accelerator build, or drop --dies on purpose). + * + * Written as ONE line so it greps out of a fleet log, and it names the concrete + * binary to build — the deploy path being wrong is the whole point, so "use the + * right build" without the command is not a fix. + * + * @param verdict From pn_backend_guard_check (PN_BACKEND_GUARD_OK writes an + * empty string and returns 0). + * @param model_name Served name for the model that asked, or NULL. + * @param dies The requested die indices, or NULL. + * @param n_dies How many entries in `dies`. + * @param out Destination buffer. + * @param out_len Its size; the message is truncated, never overrun. + * @return Number of bytes that WOULD have been written (snprintf + * semantics), or 0 for PN_BACKEND_GUARD_OK. + * + * Thread-safe: yes Reentrant: yes Signal-safe: no (snprintf) + */ +int pn_backend_guard_message(pn_backend_guard_verdict verdict, + const char *model_name, + const int *dies, int n_dies, + char *out, size_t out_len); + +/** + * The SHORT form of the same refusal, for callers that carry a reason string + * rather than print (mr_load's err[]). Stable text — model_admin.c's + * status_for_reason matches "no accelerator" on it to answer 400 instead of the + * catch-all 500. + * + * @return A string literal, or NULL for PN_BACKEND_GUARD_OK. + * + * Thread-safe: yes Reentrant: yes Signal-safe: yes + */ +const char *pn_backend_guard_short_reason(pn_backend_guard_verdict verdict); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_SERVE_BACKEND_GUARD_H */ diff --git a/src/serve/serve_command.c b/src/serve/serve_command.c new file mode 100644 index 0000000..6464d55 --- /dev/null +++ b/src/serve/serve_command.c @@ -0,0 +1,951 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_command.c + * @brief Implementation of `pantheon serve` (see serve_command.h). Loads one + * or more models into the model registry and runs an accept loop that + * handles each connection on its own detached thread, so lightweight + * requests (e.g. GET /v1/models health checks) respond immediately even + * while a chat is generating. + * + * Concurrency model: + * - IO is concurrent: one detached pthread per accepted connection. accept() + * and cheap endpoints never block behind an in-flight generation. + * - Generation is serialized PER MODEL: each registry entry owns its + * pn_model + pn_state (KV / SSM state) and a gen_lock; the chat path + * (openai_chat.c) routes the request's `model` field to an entry and takes + * a non-blocking trylock on it — a second chat to the SAME model is + * answered with a clean 429 (busy), while chats to DIFFERENT models run + * concurrently on their own dies/threads. Health checks and other cheap + * routes never touch a lock. + * (Batching concurrent chats to one model instead of 429-ing them is future + * work — see the multi-user batching task.) + * + * Shutdown: SIGINT/SIGTERM are RECORDED by the handler (serve_shutdown.h) and + * acted on here. The accept loop polls the shutdown wake pipe alongside the + * listening socket; on a request it stops accepting, drains the in-flight turns + * — which stop themselves through the abort predicate — and only then frees the + * models, which is the path that retires a Tenstorrent card's residencies. A + * drain that does not quiesce ABANDONS the teardown rather than running it + * under a live thread. + * + * Multi-model (Phase 7): `--model` is repeatable; each may be followed by + * `--name ` (served alias) and `--dies ` (explicit N-die placement on + * the K80 test machine). With a die manager running (CUDA present) unnamed placements are + * assigned automatically. `--served-name` remains the back-compat alias for + * the FIRST model's name. Runtime load/unload arrives with the admin routes + * (model_admin.c). + * + * Backend honesty: the first line this process writes is its COMPILED backend + * and build commit, and the second is how many accelerator dies the manager can + * see. Then any `--dies` that this binary cannot honour is a refusal to start, + * not a warning (serve_backend_guard.h has the incident it prevents). A CPU + * serve is asked for by simply not passing `--dies`, and is unaffected. + * + * @status Multi-model serving: registry-backed routing, per-model + * serialization, live /v1/models. Single `--model` behaves exactly as + * the P1 single-model server did. + * @issues Concurrent chats to one model are 429'd, not batched (deferred to + * the batching task). + * @todo None + */ + +#include "serve_command.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../include/pantheon.h" +#include "../core/engine.h" +#include "../core/mem/pn_memwatch.h" +#include "../accel/backend_identity.h" +#include "../accel/die_manager.h" +#include "http_server.h" +#include "model_admin.h" +#include "model_registry.h" +#include "openai_chat.h" +#include "request_profile.h" +#include "serve_backend_guard.h" +#include "serve_registry.h" +#include "serve_shutdown.h" +#include "session_watch.h" +#include "signal_handlers.h" + +#ifdef USE_CUDA +#include "cuda/fc_cuda.h" +#endif + +#define SERVE_DEFAULT_HOST "127.0.0.1" +#define SERVE_DEFAULT_PORT 8080 + +/* One `--model` occurrence and the per-model flags that followed it. */ +typedef struct { + const char *path; + const char *name; /* NULL = file basename */ + int dies[PN_MODEL_MAX_DIES]; + int n_dies; /* 0 = auto-place / CPU */ + int reasoning_budget; /* --reasoning-budget; 0 = disabled */ + int max_sessions; /* --max-sessions; 0 = engine default */ + int max_concurrent; /* --max-concurrent; batch-slot cap. + * 0 = fall back to max_sessions. */ + int ctx_cap; /* --ctx-cap; per-session context cap in + * tokens (KV ring window). 0 = engine + * default (QWEN35_MAX_SEQ / fallback). */ +} serve_model_arg; + +/* Shared, read-mostly server context handed to every connection thread: the + * model registry (which owns per-model locks/refcounts internally). */ +typedef struct { + mr_registry *reg; + pn_session_watch_mgr *watch; /* liveness-based session release (§C.4); + * NULL if the watch manager failed to start + * (serve still runs — watches are refused). */ +} serve_ctx; + +/* Per-connection job: the accepted client fd plus the shared context. Heap + * allocated by the accept loop and freed by the connection thread. */ +typedef struct { + int client_fd; + serve_ctx *ctx; +} connection_job; + +static void print_serve_usage(void) +{ + printf("Usage: pantheon serve --model [--name ] [--model ...] [options]\n\n"); + printf("Model options (repeat --model to serve several; --name/--dies bind\n"); + printf("to the most recent --model):\n"); + printf(" --model Model file (GGUF); at least one required\n"); + printf(" --name Served model id (default: file name)\n"); + printf(" --dies Pin this model to accelerator die(s) (CUDA/Tenstorrent\n"); + printf(" builds; CUDA indices are relative to\n"); + printf(" CUDA_VISIBLE_DEVICES). N dies split the layers N ways,\n"); + printf(" evenly, LM head on the last. That buys VRAM headroom\n"); + printf(" for a wider --ctx-cap, NOT speed: each extra die adds\n"); + printf(" one activation hand-off per step. Use the fewest dies\n"); + printf(" that hold the model at the cap you want.\n"); + printf(" Omit to let the engine place it\n"); + printf(" — and OMIT IT TO SERVE ON THE CPU DELIBERATELY: a\n"); + printf(" --dies that names dies this binary cannot use is a\n"); + printf(" hard refusal to start, never a silent CPU fallback.\n"); + printf(" --reasoning-budget Cap reasoning to tokens on this model: once\n"); + printf(" the model has generated tokens inside \n"); + printf(" without closing it, force so it answers\n"); + printf(" (reasoning-runaway guard; 0/absent = unbounded)\n"); + printf(" --max-sessions Cap this model's live prefix-cache sessions to \n"); + printf(" (VRAM-sized; a new session over the cap is refused\n"); + printf(" 429/503; 0/absent = default %d)\n", BS_MAX_USERS); + printf(" --max-concurrent Cap this model's SIMULTANEOUS decode slots to \n"); + printf(" (batch scheduler width; surplus requests queue and\n"); + printf(" then get 429/503. Distinct from --max-sessions,\n"); + printf(" which caps resident sessions. 0/absent = the\n"); + printf(" resolved --max-sessions)\n"); + printf(" --ctx-cap Per-session context cap in tokens for this model:\n"); + printf(" the KV ring window. A prompt past it is refused with\n"); + printf(" context_over_budget (client compacts + retries) — no\n"); + printf(" silent drop-oldest. 0/absent = engine default.\n"); + printf(" --served-name Back-compat alias for the FIRST model's --name\n"); + printf("\nGlobal options:\n"); + printf(" --host Bind address (default %s)\n", SERVE_DEFAULT_HOST); + printf(" --port Port (default %d)\n", SERVE_DEFAULT_PORT); + printf(" --config HuggingFace config.json (safetensors models)\n"); + printf(" --vocab RWKV World tokenizer vocab file\n"); + printf(" --quantize INT8 quantize weights at load time\n"); + printf("\nEndpoints: GET /v1/models, POST /v1/chat/completions (SSE stream supported),\n"); + printf(" GET /admin/models, POST /admin/models/load, POST /admin/models/unload,\n"); + printf(" GET /admin/dies,\n"); + printf(" POST /admin/session/export, POST /session/restore, POST /session/release,\n"); + printf(" GET /session/estimate\n"); +} + +/* Parse a comma-separated die list ("0,1") into out[0..max). Returns the + * count, or -1 on a malformed list. */ +static int parse_dies_list(const char *s, int *out, int max) +{ + int n = 0; + while (*s) { + char *end = NULL; + long v = strtol(s, &end, 10); + if (end == s || v < 0 || n >= max) + return -1; + out[n++] = (int)v; + s = end; + if (*s == ',') + s++; + else if (*s) + return -1; + } + return n > 0 ? n : -1; +} + +/* Extract query param `key` from an "a=1&b=two" query string into out[0..cap). + * Returns 1 if found (out is NUL-terminated), 0 otherwise. No URL-decoding — + * the Phase-7 params (model name, integers) are plain ASCII. */ +static int query_param_str(const char *query, const char *key, char *out, + size_t cap) +{ + size_t klen = strlen(key); + for (const char *p = query; p && *p;) { + const char *amp = strchr(p, '&'); + const char *eq = strchr(p, '='); + if (eq && (!amp || eq < amp) && (size_t)(eq - p) == klen && + strncmp(p, key, klen) == 0) { + const char *vstart = eq + 1; + const char *vend = amp ? amp : vstart + strlen(vstart); + size_t vlen = (size_t)(vend - vstart); + if (vlen >= cap) vlen = cap - 1; + memcpy(out, vstart, vlen); + out[vlen] = '\0'; + return 1; + } + if (!amp) break; + p = amp + 1; + } + if (cap) out[0] = '\0'; + return 0; +} + +/* Query param `key` parsed as a double, or `fallback` if absent/malformed. */ +static double query_param_num(const char *query, const char *key, + double fallback) +{ + char buf[64]; + if (!query_param_str(query, key, buf, sizeof(buf)) || !buf[0]) + return fallback; + char *end = NULL; + double v = strtod(buf, &end); + return (end != buf && *end == '\0') ? v : fallback; +} + +/* GET /session/watch?name=&session_id= — the liveness watch (§C.4). The client + * holds this connection open for the session's life; while it is held the + * session is ALIVE (never reaped for inactivity), and only the connection + * closing (client death/drop) frees the slot after a grace. On success the fd + * is ADOPTED by the watch manager (this returns 1 so the connection thread does + * NOT close it); on any refusal an error is sent and the fd is closed normally + * (returns 0). */ +static int handle_session_watch(int cfd, serve_ctx *ctx, const hs_request *req) +{ + if (!ctx->watch) { + hs_send_error(cfd, 503, "watch_unavailable", + "session watch manager is not running"); + return 0; + } + char name[MR_MAX_NAME] = ""; + char sid[64] = ""; + const char *q = strchr(req->path, '?'); + if (q) { + query_param_str(q + 1, "name", name, sizeof(name)); + query_param_str(q + 1, "session_id", sid, sizeof(sid)); + } + /* Fall back to the X-Session-Id header if the query omitted session_id. */ + if (!sid[0] && req->session_id && req->session_id[0]) + snprintf(sid, sizeof(sid), "%s", req->session_id); + if (!sid[0]) { + hs_send_error(cfd, 400, "invalid_request_error", + "'session_id' query param is required"); + return 0; + } + + mr_entry *entry = mr_acquire(ctx->reg, name[0] ? name : NULL); + if (!entry) { + hs_send_error(cfd, 404, "model_not_found", + "the requested model is not loaded (see GET /v1/models)"); + return 0; + } + if (!entry->prefix_cache) { + mr_release(ctx->reg, entry); + hs_send_error(cfd, 400, "session_caching_disabled", + "this model has no session cache to watch"); + return 0; + } + /* Route with the entry's resolved name (name[] may be empty → default). */ + int rc = pn_session_watch_add(ctx->watch, cfd, entry, + entry->name, sid); + mr_release(ctx->reg, entry); /* the manager took its own reference */ + if (rc != 0) { + hs_send_error(cfd, 503, "watch_unavailable", + "session watch table is full; retry shortly"); + return 0; + } + return 1; /* fd adopted by the watch manager — do not close it */ +} + +/* Route one parsed request to the matching handler. Cheap routes answer inline + * (no lock); the chat completion route serializes per model inside + * oai_handle_chat. Returns 1 if the connection fd was ADOPTED (a held + * /session/watch — the caller must NOT close it), 0 otherwise. */ +static int dispatch(int cfd, serve_ctx *ctx, const hs_request *req) +{ + if (strcmp(req->method, "OPTIONS") == 0) { + hs_send_json(cfd, 200, "{}"); /* CORS preflight */ + return 0; + } + if (strcmp(req->method, "GET") == 0 && + strncmp(req->path, "/session/watch", 14) == 0 && + (req->path[14] == '\0' || req->path[14] == '?')) { + return handle_session_watch(cfd, ctx, req); + } + if (strcmp(req->method, "GET") == 0 && strcmp(req->path, "/v1/models") == 0) { + oai_handle_models(cfd, ctx->reg); + return 0; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/v1/chat/completions") == 0) { + /* THE route that drives the accelerator, so it is the one bracketed for + * the shutdown drain: nothing may retire a card residency while a turn + * is still submitting descriptors against it. The bracket is wider than + * the generation itself (it includes the parse and the routing), and + * that is the safe direction — it can only make a drain wait longer, + * never let it finish early. */ + if (pn_serve_generation_enter() != 0) { + hs_send_error_ex(cfd, 503, "server_shutting_down", + "the server is shutting down; retry shortly", + "Retry-After: 1\r\n"); + return 0; + } + oai_handle_chat(cfd, ctx->reg, req->body, req->session_id); + pn_serve_generation_leave(); + return 0; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/admin/models/load") == 0) { + ma_handle_load(cfd, ctx->reg, req->body); + return 0; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/admin/models/unload") == 0) { + ma_handle_unload(cfd, ctx->reg, req->body); + return 0; + } + if (strcmp(req->method, "GET") == 0 && strcmp(req->path, "/admin/models") == 0) { + ma_handle_models(cfd, ctx->reg); + return 0; + } + if (strcmp(req->method, "GET") == 0 && strcmp(req->path, "/admin/dies") == 0) { + ma_handle_dies(cfd); + return 0; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/admin/session/export") == 0) { + ma_handle_session_export(cfd, ctx->reg, req->body); + return 0; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/session/restore") == 0) { + oai_handle_session_restore(cfd, ctx->reg, req->body, req->session_id); + return 0; + } + if (strcmp(req->method, "POST") == 0 && + strcmp(req->path, "/session/release") == 0) { + oai_handle_session_release(cfd, ctx->reg, req->body, req->session_id); + return 0; + } + if (strcmp(req->method, "GET") == 0 && + strncmp(req->path, "/session/estimate", 17) == 0 && + (req->path[17] == '\0' || req->path[17] == '?')) { + char name[MR_MAX_NAME] = ""; + int tokens = 0; + double blob_bytes = 0.0, upload_bps = 0.0; + const char *q = strchr(req->path, '?'); + if (q) { + query_param_str(q + 1, "name", name, sizeof(name)); + tokens = (int)query_param_num(q + 1, "tokens", 0.0); + blob_bytes = query_param_num(q + 1, "blob_bytes", 0.0); + upload_bps = query_param_num(q + 1, "upload_bps", 0.0); + } + oai_handle_session_estimate(cfd, ctx->reg, name[0] ? name : NULL, + tokens, blob_bytes, upload_bps); + return 0; + } + if (strcmp(req->method, "GET") == 0 && strcmp(req->path, "/") == 0) { + /* The health probe answers with the backend too. This is the cheapest + * route on the server and the one a monitor already hits, so it is the + * cheapest place for "what am I running" to be a GET instead of an ssh + * + ldd. Values are compile-time literals + a lock-free die count, so + * the route stays allocation-free and never blocks. */ + char health[256]; + snprintf(health, sizeof(health), + "{\"service\":\"pantheon\",\"status\":\"ok\",\"backend\":\"%s\"," + "\"build\":\"%s\",\"accelerator\":%s,\"dies_visible\":%d}", + pn_backend_token(), pn_backend_build_commit(), + pn_backend_has_accelerator() ? "true" : "false", pn_dm_count()); + hs_send_json(cfd, 200, health); + return 0; + } + hs_send_error(cfd, 404, "not_found", "unknown route"); + return 0; +} + +/* Read, route, and answer one connection, then close it — UNLESS the route + * adopted the fd (a held /session/watch, which the watch manager now owns and + * will close itself). Shared between the per-connection thread and the inline + * fallback path. */ +static void handle_connection(int cfd, serve_ctx *ctx) +{ + hs_request req; + int adopted = 0; + pn_reqprof_begin(); + pn_reqprof_phase_start(PN_REQ_HTTP_READ); + int read_rc = hs_read_request(cfd, &req); + pn_reqprof_phase_end(PN_REQ_HTTP_READ); + if (read_rc == 0) { + fprintf(stderr, "serve: %s %s\n", req.method, req.path); + pn_reqprof_note_route(req.method, req.path); + adopted = dispatch(cfd, ctx, &req); + hs_request_free(&req); + } + pn_reqprof_end(); + if (!adopted) + hs_close(cfd); +} + +/* Detached thread entry: own one client connection end to end, then release the + * job. A slow/blocking client can only stall its own thread, never accept(). */ +static void *connection_thread(void *arg) +{ + connection_job *job = arg; + handle_connection(job->client_fd, job->ctx); + free(job); + return NULL; +} + +/* Spawn a detached connection thread. On failure (e.g. thread limit) fall back + * to handling the connection inline so the request is never dropped. */ +static void spawn_connection(int cfd, serve_ctx *ctx) +{ + connection_job *job = malloc(sizeof(*job)); + if (!job) { handle_connection(cfd, ctx); return; } + job->client_fd = cfd; + job->ctx = ctx; + + pthread_attr_t attr; + if (pthread_attr_init(&attr) != 0) { handle_connection(cfd, ctx); free(job); return; } + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + + pthread_t tid; + int rc = pthread_create(&tid, &attr, connection_thread, job); + pthread_attr_destroy(&attr); + if (rc != 0) { + fprintf(stderr, "serve: pthread_create failed (%d); handling inline\n", rc); + handle_connection(cfd, ctx); + free(job); + } +} + +/* Milliseconds on CLOCK_MONOTONIC — the drain measures a budget, so it must not + * move when the wall clock does. */ +static uint64_t serve_monotonic_ms(void) +{ + struct timespec ts; + + if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) + return 0; + return (uint64_t)ts.tv_sec * 1000u + (uint64_t)(ts.tv_nsec / 1000000); +} + +/* Poll interval of the drain. Short enough that a quiesced serve exits without + * a perceptible pause, long enough that waiting costs nothing on a 2-core host. */ +#define SERVE_DRAIN_POLL_MS 20 + +/* + * Wait for every in-flight accelerator-driving turn to finish, and report + * whether the teardown may proceed. + * + * The turns are not interrupted — they are ASKED, through the abort predicate + * openai_chat.c already arms, which they poll between prefill chunks and every + * 8 decode tokens. A second signal collapses the budget to zero so an operator + * hammering Ctrl-C gets out through this path instead of an _exit() from a + * handler. + */ +static pn_shutdown_drain_decision serve_drain_generations(void) +{ + const uint64_t started = serve_monotonic_ms(); + int reported = 0; + + for (;;) { + int inflight = pn_serve_generation_inflight(); + unsigned budget = pn_serve_shutdown_escalated() + ? 0u : pn_serve_shutdown_drain_budget_ms(); + unsigned elapsed = (unsigned)(serve_monotonic_ms() - started); + pn_shutdown_drain_decision decision = + pn_serve_shutdown_drain_decision(inflight, elapsed, budget); + + if (decision != PN_SHUTDOWN_DRAIN_WAIT) + return decision; + if (!reported) { + fprintf(stderr, "serve: waiting up to %u ms for %d in-flight turn%s " + "to stop before retiring the accelerator\n", + budget, inflight, inflight == 1 ? "" : "s"); + reported = 1; + } + usleep(SERVE_DRAIN_POLL_MS * 1000); + } +} + +/* + * The controlled-shutdown sequence, run on the accept-loop thread once the + * signal handler's self-pipe has woken it. This is the point where the + * program's state is known, which is the whole reason the handler defers here. + * + * Order is load-bearing: + * 1. stop accepting, so the in-flight set can only shrink; + * 2. leave the router, so nothing new is dispatched to this instance; + * 3. DRAIN — the abort predicate has already told every live turn to stop; + * 4. only if it quiesced, tear down: the watch manager first (it holds + * mr_acquire refs, and mr_free's per-entry drain waits on refs hitting + * zero — stopping it after would deadlock the shutdown), then the model + * registry, whose per-model free is what reaches pn_llama_tt_free -> + * pn_tt_device_close -> pn_tt_residency_release and retires the card's + * residencies BY ASKING. + * + * On a drain timeout nothing is retired. Retiring under a live thread is worse + * than not retiring at all: pn_tt_device_close frees the device that thread is + * still dereferencing, so the tidy-looking teardown ends in a SIGSEGV and a + * core held with no host attached. What is left instead is exactly what a + * SIGKILL leaves, which SET_NOC_CLEANUP and recovery-on-acquire already cover. + */ +static void serve_run_shutdown(int listen_fd, pn_session_watch_mgr *watch, + mr_registry *reg) +{ + pn_shutdown_drain_decision decision; + + hs_close(listen_fd); + serve_registry_deregister_all(); + + decision = serve_drain_generations(); + if (decision == PN_SHUTDOWN_DRAIN_RETIRE) { + /* The registry heartbeat thread (Phase G0, only alive when + * PANTHEON_REGISTRY_URL is set) still takes brief mr_acquire/mr_release + * pairs here. It is not a starvation risk — mr_acquire skips an entry + * once mr_unload marks it `unloading` — and the re-raise below follows + * immediately, so it is left running rather than given a stop path it + * has never needed. */ + pn_session_watch_stop(watch); + mr_free(reg); + pn_dm_shutdown(); + fprintf(stderr, "serve: shutdown complete — models freed and " + "accelerator residencies retired\n"); + } else { + fprintf(stderr, + "serve: DRAIN TIMED OUT with %d turn(s) still on the " + "accelerator — NOT retiring residencies under a live thread. " + "The card is left to SET_NOC_CLEANUP and recovery-on-acquire, " + "exactly as after a SIGKILL.\n", + pn_serve_generation_inflight()); + } + +#ifdef USE_CUDA + /* Kepler reclaims VRAM only when the owning process's /dev/nvidiaN fd + * closes, so this runs on BOTH branches — it is about the host's fd, not + * about anything a stalled turn is holding. */ + if (fc_cuda_live_ctx_count() > 0) + fc_cuda_emergency_teardown(); +#endif +} + +/* + * Refuse every `--model ... --dies ...` this binary cannot honour, BEFORE any + * model is loaded. + * + * Pre-flighting all specs (rather than letting mr_load refuse the first one) is + * what makes the failure cheap: a bad manifest is answered in a second instead + * of after a ~55 s weight upload, and the operator sees every offending model + * in one go rather than one per restart. mr_load re-checks the same rule for + * the runtime /admin/models/load path — this is the fast, talkative copy. + * + * Returns 1 if the serve must stop (message already printed), 0 to proceed. + */ +static int refuse_dies_without_backend(const serve_model_arg *specs, int n_specs) +{ + const bool die_backend = pn_backend_supports_die_placement(); + const int dies_visible = pn_dm_count(); + int refused = 0; + + for (int i = 0; i < n_specs; i++) { + pn_backend_guard_verdict v = + pn_backend_guard_check(specs[i].n_dies, die_backend, dies_visible); + char msg[640]; + + if (v == PN_BACKEND_GUARD_OK) + continue; + pn_backend_guard_message(v, specs[i].name ? specs[i].name : specs[i].path, + specs[i].dies, specs[i].n_dies, msg, sizeof(msg)); + fprintf(stderr, "%s\n", msg); + refused++; + } + return refused > 0; +} + +int pn_serve_main(int argc, char **argv) +{ + serve_model_arg specs[MR_MAX_MODELS]; + int n_specs = 0; + const char *config_path = NULL; + const char *vocab_path = NULL; + const char *served_name = NULL; + const char *host = SERVE_DEFAULT_HOST; + int port = SERVE_DEFAULT_PORT; + int quantize = 0; + + /* FIRST line this process writes. The backend is a compile-time constant + * and the only reason it was ever unknown is that nothing printed it: a + * CPU-only binary served a 35B MoE off a 2-core Pentium with 8 dies idle, + * and answering "what am I running" took `ldd` + `strings` on a remote box. + * One line, one key=value per fact, greppable out of a fleet log. */ + fprintf(stderr, "serve: backend=%s build=%s accelerator=%s\n", + pn_backend_name(), pn_backend_build_commit(), + pn_backend_has_accelerator() ? "yes" : "no"); + + memset(specs, 0, sizeof(specs)); + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + print_serve_usage(); + return 0; + } else if (strcmp(argv[i], "--model") == 0 && i + 1 < argc) { + if (n_specs >= MR_MAX_MODELS) { + fprintf(stderr, "serve: too many --model (max %d)\n", MR_MAX_MODELS); + return 1; + } + specs[n_specs++].path = argv[++i]; + } else if (strcmp(argv[i], "--name") == 0 && i + 1 < argc) { + if (n_specs == 0) { + fprintf(stderr, "serve: --name must follow a --model\n"); + return 1; + } + specs[n_specs - 1].name = argv[++i]; + } else if (strcmp(argv[i], "--dies") == 0 && i + 1 < argc) { + if (n_specs == 0) { + fprintf(stderr, "serve: --dies must follow a --model\n"); + return 1; + } + serve_model_arg *m = &specs[n_specs - 1]; + m->n_dies = parse_dies_list(argv[++i], m->dies, PN_MODEL_MAX_DIES); + if (m->n_dies < 0) { + fprintf(stderr, "serve: bad --dies list '%s'\n", argv[i]); + return 1; + } + } else if (strcmp(argv[i], "--reasoning-budget") == 0 && i + 1 < argc) { + if (n_specs == 0) { + fprintf(stderr, "serve: --reasoning-budget must follow a --model\n"); + return 1; + } + /* strtol (not atoi): a typo'd non-numeric arg must ERROR, not silently + * parse to 0 and disable the guard. */ + const char *rbarg = argv[++i]; + char *rbend = NULL; + long rb = strtol(rbarg, &rbend, 10); + if (rbend == rbarg || *rbend != '\0' || rb < 0 || rb > (1 << 20)) { + fprintf(stderr, "serve: bad --reasoning-budget '%s' " + "(want a token count 0..%d)\n", rbarg, 1 << 20); + return 1; + } + specs[n_specs - 1].reasoning_budget = (int)rb; + } else if (strcmp(argv[i], "--ctx-cap") == 0 && i + 1 < argc) { + if (n_specs == 0) { + fprintf(stderr, "serve: --ctx-cap must follow a --model\n"); + return 1; + } + /* strtol (mirrors --reasoning-budget): a typo must ERROR, not + * silently disable the per-model cap. Accept a token count up to the + * hybrid ring ceiling; 0 = engine default. */ + const char *ccarg = argv[++i]; + char *ccend = NULL; + long cc = strtol(ccarg, &ccend, 10); + if (ccend == ccarg || *ccend != '\0' || cc < 0 || cc > (1 << 20)) { + fprintf(stderr, "serve: bad --ctx-cap '%s' " + "(want a token count 0..%d)\n", ccarg, 1 << 20); + return 1; + } + specs[n_specs - 1].ctx_cap = (int)cc; + } else if (strcmp(argv[i], "--max-sessions") == 0 && i + 1 < argc) { + if (n_specs == 0) { + fprintf(stderr, "serve: --max-sessions must follow a --model\n"); + return 1; + } + /* strtol (mirrors --reasoning-budget): a typo'd non-numeric arg must + * ERROR, not silently parse to 0 and revert to the default cap. + * Accept 0 (= engine default) up to BS_MAX_USERS (the batch + * scheduler's user-slot ceiling; mr_load clamps anyway). */ + const char *msarg = argv[++i]; + char *msend = NULL; + long ms = strtol(msarg, &msend, 10); + if (msend == msarg || *msend != '\0' || ms < 0 || ms > BS_MAX_USERS) { + fprintf(stderr, "serve: bad --max-sessions '%s' " + "(want a session count 0..%d; 0 = default)\n", + msarg, BS_MAX_USERS); + return 1; + } + specs[n_specs - 1].max_sessions = (int)ms; + } else if (strcmp(argv[i], "--max-concurrent") == 0 && i + 1 < argc) { + if (n_specs == 0) { + fprintf(stderr, "serve: --max-concurrent must follow a --model\n"); + return 1; + } + /* Same strtol discipline as --max-sessions: a typo must ERROR + * rather than silently widen the batch back to the default. */ + const char *mcarg = argv[++i]; + char *mcend = NULL; + long mc = strtol(mcarg, &mcend, 10); + if (mcend == mcarg || *mcend != '\0' || mc < 0 || mc > BS_MAX_USERS) { + fprintf(stderr, "serve: bad --max-concurrent '%s' " + "(want a slot count 0..%d; 0 = use --max-sessions)\n", + mcarg, BS_MAX_USERS); + return 1; + } + specs[n_specs - 1].max_concurrent = (int)mc; + } else if (strcmp(argv[i], "--host") == 0 && i + 1 < argc) { + host = argv[++i]; + } else if (strcmp(argv[i], "--port") == 0 && i + 1 < argc) { + port = atoi(argv[++i]); + } else if (strcmp(argv[i], "--served-name") == 0 && i + 1 < argc) { + served_name = argv[++i]; + } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { + config_path = argv[++i]; + } else if (strcmp(argv[i], "--vocab") == 0 && i + 1 < argc) { + vocab_path = argv[++i]; + } else if (strcmp(argv[i], "--quantize") == 0) { + quantize = 1; + } else { + fprintf(stderr, "serve: unknown option '%s'\n", argv[i]); + print_serve_usage(); + return 1; + } + } + + if (n_specs == 0) { + fprintf(stderr, "serve: --model is required\n"); + print_serve_usage(); + return 1; + } + /* Back-compat: --served-name names the first model when it has no --name. */ + if (served_name && !specs[0].name) + specs[0].name = served_name; + + /* L15 — serve config hygiene: QWEN35_GPU_FULL_LOGITS restores the full + * per-token n_vocab (248k) D2H copy over the ~0.4 GB/s host link (K*992 KB + * per step) — a debug/eval knob that must NEVER be set on the serve path, + * where it silently caps throughput. The batched serve always presents the + * sparse device top-k. Guard at start: refuse to serve with it set rather + * than serve slowly and silently. */ + if (getenv("QWEN35_GPU_FULL_LOGITS")) { + fprintf(stderr, + "serve: refusing to start with QWEN35_GPU_FULL_LOGITS set — it forces " + "the full %s-vocab logits D2H per token (K*992 KB/step over the host " + "link) and cripples batched decode. Unset it (the serve path uses the " + "sparse device top-k presentation).\n", "248k"); + return 1; + } + + /* MemoryAware Phase 1: start the host-signal poll loop BEFORE any load so + * model-load admission sees live numbers. Non-fatal if it can't start. */ + pn_memwatch_start(); + + /* MemoryAware Phase 3 (§8): install crash/shutdown handlers early so any + * exit — including a crash mid-load — closes the nvidia fd, the only step + * that reclaims Kepler VRAM. SIGINT/SIGTERM are recorded here and answered + * by serve_run_shutdown() below (clean CUDA teardown included); + * SIGSEGV/SIGABRT/SIGBUS _exit() and lean on kernel fd-close + startup + * stranded-VRAM detection. + * + * Arm the shutdown wake pipe FIRST: the handler tests for it to decide + * whether it may defer, so installing the handlers before the pipe exists + * leaves a window in which a SIGTERM takes the fallback path. */ + if (pn_serve_shutdown_arm() != 0) + fprintf(stderr, "serve: could not create the shutdown wake pipe (%s); " + "a stop will fall back to the pre-drain path and will " + "NOT retire accelerator residencies\n", strerror(errno)); + pn_install_signal_handlers(); + +#ifdef USE_CUDA + /* Die manager (Phase 7): probe the resident dies so the registry can + * auto-place models and budget VRAM across them. Deliberately NO fc_init() + * here — at this point no model has been placed, so creating the + * process-default context would pin it to ordinal 0 whatever `--dies` says. + * The probe is self-contained (bare context per die, destroyed) and the + * default context is created later by the loader, on the die it owns. + * Without a GPU (or when the probe fails) serving proceeds CPU-resident. */ + if (fc_driver_available()) { + if (pn_dm_init_cuda(PN_DM_MAX_DIES) < 0) + fprintf(stderr, "serve: die probe failed; models stay CPU-resident\n"); + } +#endif + +#ifdef USE_TT + /* Tenstorrent cards, ONE die per device (die_manager.h says why it is not + * one die per core group). Only when no other family claimed the manager: + * there is a single die-space and a die id means "K80 ordinal" or + * "Blackhole device", never both, so a host carrying both families needs a + * per-model die-space this module does not have. Probing second rather than + * refusing keeps the common case — one family present — working, and makes + * the mixed case a visible no-op instead of a silent overwrite. */ + if (pn_dm_count() == 0) { + if (pn_dm_init_tt(PN_DM_MAX_DIES) < 0) { + /* REFUSE, do not fall back. Somebody who set FT_TT_DRIVER named a + * transport; a serve that then answers on the CPU is the silent + * fallback this project has shipped twice, and the completion text + * is BYTE-IDENTICAL either way (measured 2026-08-17, same md5 from + * the card at 4.69 s and the CPU at 22.93 s) — so nothing + * downstream can catch it. An unset variable is the benign case and + * still warns and continues. */ + if (pn_dm_tt_driver_explicit()) { + fprintf(stderr, "serve: REFUSED — FT_TT_DRIVER names a transport and no " + "Tenstorrent device could be opened over it. Not falling " + "back to the CPU: a CPU answer is indistinguishable from a " + "card answer in the text.\n"); + return 1; + } + fprintf(stderr, "serve: no Tenstorrent device; models stay CPU-resident\n"); + } + } else { + fprintf(stderr, "serve: die manager already holds another accelerator family; " + "Tenstorrent devices not registered\n"); + } +#endif + + /* The die count, said out loud, right after the probes that decide it. + * `registry: '' placed on dies 0,1 (0.0 GiB projected)` was the ONLY + * trace the incident left, and 0.0 GiB (pn_dm_count()==0 ⇒ vram_bytes 0) + * reads like a rounding artifact rather than "there is no GPU here". This + * line cannot be misread. */ + fprintf(stderr, "serve: accelerator dies visible: %d\n", pn_dm_count()); + + /* REFUSE, do not warn: the serve had already been running for an unknown + * period and measurements had already been taken off it before anyone + * noticed. A warning on a process that then serves for a week is the + * failure, not the fix. A CPU serve is still fully available — it is asked + * for by NOT passing --dies (see print_serve_usage). */ + if (refuse_dies_without_backend(specs, n_specs)) + return 1; + + static mr_registry reg; /* lives for the process (accept loop is infinite) */ + if (mr_init(®) != 0) { + fprintf(stderr, "serve: failed to init model registry\n"); + return 1; + } + + pn_load_opts opts = { .vocab_path = vocab_path, .config_path = config_path, + .quantize = (bool)quantize }; + for (int i = 0; i < n_specs; i++) { + char err[256]; + /* Per-model context cap (KV Phase 1): the ring is sized to this at load, + * so set it on the shared opts before each model's load. */ + opts.ctx_cap = specs[i].ctx_cap; + fprintf(stderr, "serve: loading %s ...\n", specs[i].path); + if (mr_load(®, specs[i].path, specs[i].name, &opts, + specs[i].n_dies > 0 ? specs[i].dies : NULL, + specs[i].n_dies, specs[i].reasoning_budget, + specs[i].max_sessions, specs[i].max_concurrent, + err, sizeof(err)) != 0) { + fprintf(stderr, "serve: failed to load %s: %s\n", specs[i].path, err); + mr_free(®); + return 1; + } + } + + /* PM 6: warm each model once (accelerator families re-armed before the first request). */ + for (int i = 0; i < MR_MAX_MODELS; i++) + if (reg.entries[i].used && reg.entries[i].model && !reg.entries[i].sched) + (void)pn_model_warm(reg.entries[i].model); + + /* Liveness-based session release (§C.4): start the watch manager (single + * poll/grace background thread). If it fails to start, serve still runs — + * /session/watch is refused (503) and the long backstop is the floor. */ + pn_session_watch_mgr *watch = pn_session_watch_start(®); + if (!watch) + fprintf(stderr, "serve: session watch manager failed to start; " + "watches will be refused (long backstop remains the floor)\n"); + else + /* Protect each model's mode-suffixed slots (`-orch`/`-act`/`-chat`) + * from the 12 h backstop while a watch on their base is alive. */ + pn_session_watch_install_backstop_hooks(watch); + serve_ctx ctx = { .reg = ®, .watch = watch }; + + int listen_fd = hs_listen(host, port); + if (listen_fd < 0) { + pn_session_watch_stop(watch); + mr_free(®); + return 1; + } + { + mr_info info[MR_MAX_MODELS]; + int n = mr_snapshot(®, info, MR_MAX_MODELS); + fprintf(stderr, "serve: listening on http://%s:%d/v1 (%d model%s:", + host, port, n, n == 1 ? "" : "s"); + for (int i = 0; i < n; i++) + fprintf(stderr, "%s %s", i ? "," : "", info[i].name); + fprintf(stderr, ")\n"); + } + + /* Dispatch layer (Phase G0): self-register each served model with the + * router's registry and start heartbeating live load. FULLY gated on + * PANTHEON_REGISTRY_URL — unset ⇒ this is a no-op and serve is unchanged. */ + serve_registry_start(®, host, port); + + /* The accept loop watches the listening socket AND the shutdown wake pipe. + * The pipe is what makes the deferred teardown correct in a threaded serve: + * the kernel may deliver SIGTERM to any thread that has it unblocked, so + * "accept() returns EINTR" is not a guarantee — a connection thread running + * the handler still wakes us here. */ + struct pollfd watched[2]; + int n_watched = 1; + + watched[0].fd = listen_fd; + watched[0].events = POLLIN; + watched[1].fd = pn_serve_shutdown_wake_fd(); + watched[1].events = POLLIN; + if (watched[1].fd >= 0) + n_watched = 2; + + while (!pn_serve_shutdown_requested()) { + /* PM 5: a 1 s tick while idle keeps the accelerator's resident families armed + * (mr_keepalive), so a request after a pause pays no idle re-arm. */ + int ready = poll(watched, (nfds_t)n_watched, 1000); + if (ready < 0) { + if (errno == EINTR) continue; + fprintf(stderr, "serve: poll() failed: %s\n", strerror(errno)); + break; + } + if (ready == 0) { + (void)mr_keepalive(ctx.reg); + continue; + } + if (n_watched == 2 && (watched[1].revents & POLLIN)) + break; /* a shutdown signal was recorded */ + if (watched[0].revents & (POLLERR | POLLNVAL)) { + /* The listening socket is unusable. Stop rather than spin: poll() + * reports these every call, so `continue` here would be a busy + * loop burning a core of a 2-core host. */ + fprintf(stderr, "serve: listening socket failed; stopping\n"); + break; + } + if (watched[0].revents & POLLIN) { + int cfd = hs_accept(listen_fd); + if (cfd < 0) continue; + spawn_connection(cfd, &ctx); + } + } + + serve_run_shutdown(listen_fd, watch, ®); + + /* Re-raise with the default disposition so the exit status reflects the + * signal the operator sent and a parent (systemd) sees a normal + * signal-terminated exit — same contract the handler used to provide, now + * after a teardown that actually retired the card. */ + { + int sig = pn_serve_shutdown_signal_number(); + + if (sig > 0) { + signal(sig, SIG_DFL); + raise(sig); + } + } + return 0; +} diff --git a/src/serve/serve_command.h b/src/serve/serve_command.h new file mode 100644 index 0000000..3c5fb8c --- /dev/null +++ b/src/serve/serve_command.h @@ -0,0 +1,28 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_command.h + * @brief `pantheon serve` entrypoint: load one or more models into the model + * registry and run an OpenAI-compatible HTTP server. Invoked from the + * CLI dispatch (cli/main.c). + * + * @status Multi-model (Phase 7): repeatable --model (+ --name / --dies), + * registry-routed /v1/chat/completions, live /v1/models, runtime + * load/unload via /admin/models/*. Single --model behaves exactly as + * the original single-model server. + * @issues Concurrent chats to ONE model are 429'd, not batched/queued. + * @todo None + */ + +#ifndef PN_SERVE_COMMAND_H +#define PN_SERVE_COMMAND_H + +/** + * Run the serve subcommand. `argc`/`argv` are the arguments AFTER the "serve" + * token (argv[0] is "serve"). Blocks serving requests until interrupted. + * + * @return process exit code (0 on clean shutdown, non-zero on setup error). + */ +int pn_serve_main(int argc, char **argv); + +#endif /* PN_SERVE_COMMAND_H */ diff --git a/src/serve/serve_emitter.c b/src/serve/serve_emitter.c new file mode 100644 index 0000000..9f769e5 --- /dev/null +++ b/src/serve/serve_emitter.c @@ -0,0 +1,454 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_emitter.c + * @brief Implementation of the reasoning/content/tool-call stream splitter + * (see serve_emitter.h). Depends only on libc so it is unit-testable in + * isolation from the engine/CUDA forward path. + */ + +#include "serve_emitter.h" + +#include +#include + +static void tc_append(text_channel *tc, const char *s, size_t n) +{ + if (n == 0) return; + if (tc->len + n + 1 > tc->cap) { + size_t cap = tc->cap ? tc->cap : 256; + while (cap < tc->len + n + 1) cap *= 2; + char *grown = realloc(tc->data, cap); + if (!grown) return; + tc->data = grown; + tc->cap = cap; + } + memcpy(tc->data + tc->len, s, n); + tc->len += n; + tc->data[tc->len] = '\0'; +} + +static const char THINK_CLOSE[] = ""; +static const char TOOL_OPEN[] = ""; +static const char FN_OPEN[] = "" + * (SGLang's minicpm5 detector, bot_token "in_think = 1; + em->has_tools = has_tools; + em->dialect = dialect; + em->cb = cb; + em->ud = ud; + em->stops = stops; + em->n_stop = n_stop; +} + +void em_set_tool_tap(emitter *em, em_tool_tap_fn tap, void *ud) +{ + em->tool_tap = tap; + em->tool_tap_ud = ud; +} + +/* Append to the raw tool channel and hand the same bytes to the tap. */ +static void em_tool_append(emitter *em, const char *s, size_t n) +{ + tc_append(&em->tool, s, n); + if (em->tool_tap && n > 0) em->tool_tap(s, n, em->tool_tap_ud); +} + +void em_free(emitter *em) +{ + free(em->carry); + free(em->reasoning.data); + free(em->content.data); + free(em->tool.data); +} + +/* Longest k (k < strlen(needle)) such that the last k bytes of hay equal the + * first k bytes of needle — i.e. how much tail could be the start of `needle`. */ +static size_t suffix_prefix_overlap(const char *hay, size_t hay_len, + const char *needle) +{ + size_t nlen = strlen(needle); + size_t max = hay_len < nlen - 1 ? hay_len : nlen - 1; + for (size_t k = max; k > 0; k--) + if (memcmp(hay + hay_len - k, needle, k) == 0) + return k; + return 0; +} + +/* True when the char immediately after "', whitespace or end): guards against capturing a stray + * "" would never re-trigger the divert. The + * partial-prefix cases (buffer ends mid-"= bufend || *q != '"') return false; + q++; + if ((size_t)(bufend - q) >= strlen("function") && + memcmp(q, "function", strlen("function")) == 0) + return true; + if ((size_t)(bufend - q) >= strlen("name") && + memcmp(q, "name", strlen("name")) == 0) + return true; + return false; +} + +/* Length of a trailing tail beginning at the LAST '{' in [buf,buf+len) whose + * plausibility is still UNDECIDED — the buffer ends before we can see whether a + * "function"/"name" key follows. Such a '{' must be held back so the next feed + * can re-decide (mirrors trailing_incomplete_fn_open for the XML path). A '{' + * already decided plausible is diverted by em_find_ornith_opener; one clearly + * NOT a call (a non-quote, non-whitespace char follows) is not held. Returns 0 + * when there is nothing to hold. */ +static size_t trailing_undecided_json_brace(const char *buf, size_t len) +{ + const char *bufend = buf + len; + /* Only the final '{' can straddle the end; earlier ones are fully present. */ + const char *brace = NULL; + for (const char *p = memchr(buf, '{', len); p; + p = memchr(p + 1, '{', (size_t)(bufend - (p + 1)))) + brace = p; + if (!brace) return 0; + const char *q = brace + 1; + while (q < bufend && (*q == ' ' || *q == '\t' || *q == '\n' || *q == '\r')) + q++; + if (q >= bufend) return (size_t)(bufend - brace); /* only whitespace so far */ + if (*q != '"') return 0; /* not a key → not a call */ + q++; + /* q points just past the opening quote of the first key. If the remaining + * bytes are a strict prefix of "function"/"name", the key is not yet fully + * present → hold. A complete/decided key was handled by the opener finder. */ + size_t rem = (size_t)(bufend - q); + if (rem == 0) return (size_t)(bufend - brace); + if (rem < strlen("function") && + memcmp(q, "function", rem) == 0) + return (size_t)(bufend - brace); + if (rem < strlen("name") && + memcmp(q, "name", rem) == 0) + return (size_t)(bufend - brace); + return 0; +} + +/* Find the Ornith JSON opener (```json fence, else a plausible bare '{') in + * [buf,buf+len). On a hit sets *at + *open_len and returns true. The fence is + * preferred; a bare '{' is only taken when json_brace_is_plausible. */ +static bool em_find_ornith_opener(const char *buf, size_t len, + size_t *at, size_t *open_len) +{ + char *fence = memmem(buf, len, JSON_FENCE, strlen(JSON_FENCE)); + if (fence) { *at = (size_t)(fence - buf); *open_len = strlen(JSON_FENCE); return true; } + const char *bufend = buf + len; + for (const char *p = memchr(buf, '{', len); p; + p = memchr(p + 1, '{', (size_t)(bufend - (p + 1)))) { + if (json_brace_is_plausible(p, bufend)) { + *at = (size_t)(p - buf); + *open_len = 1; + return true; + } + } + return false; +} + +/* Find the earliest tool-call opener in [buf,buf+len). "" is always + * considered; "" shape. For the ORNITH_JSON dialect the XML openers are NOT + * watched — a ```json fence / plausible bare '{' is diverted instead. On a hit + * sets *at (offset) + *open_len (opener length) and returns true; whichever + * opener appears first wins. */ +static bool em_find_opener(const emitter *em, const char *buf, size_t len, + size_t *at, size_t *open_len) +{ + if (em->dialect == TC_DIALECT_ORNITH_JSON) { + if (!em->has_tools) return false; + return em_find_ornith_opener(buf, len, at, open_len); + } + + bool found = false; + size_t best = len; + size_t best_len = 0; + + char *tc = memmem(buf, len, TOOL_OPEN, strlen(TOOL_OPEN)); + if (tc) { + best = (size_t)(tc - buf); + best_len = strlen(TOOL_OPEN); + found = true; + } + + if (em->has_tools) { + /* Scan for the first plausibly-shaped "" opener. The tag is + * self-identifying (a space then an attribute), so no plausibility gate + * is needed beyond the literal. Diverts at the "...". */ + char *fa = memmem(buf, len, FN_OPEN_ATTR, strlen(FN_OPEN_ATTR)); + if (fa) { + size_t off = (size_t)(fa - buf); + if (!found || off < best) { + best = off; + best_len = strlen(FN_OPEN_ATTR); + found = true; + } + } + } + + if (found) { *at = best; *open_len = best_len; } + return found; +} + +/* Emit `n` bytes of `s` to a channel: left-trim leading whitespace until the + * channel has real content, accumulate, and forward the delta to the callback. */ +static void em_emit(emitter *em, int is_reasoning, const char *s, size_t n) +{ + text_channel *tc = is_reasoning ? &em->reasoning : &em->content; + bool *opened = is_reasoning ? &em->reasoning_open : &em->content_open; + if (!*opened) { + while (n > 0 && (*s == '\n' || *s == '\r' || *s == ' ' || *s == '\t')) { + s++; + n--; + } + if (n == 0) return; + *opened = true; + } + tc_append(tc, s, n); + if (em->cb) { + char *tmp = malloc(n + 1); + if (tmp) { + memcpy(tmp, s, n); + tmp[n] = '\0'; + /* OpenAI reports logprobs for the ANSWER only, so a reasoning delta + * carries none — but it still drains the queue, because the tokens + * behind it were reasoning tokens and must not be re-attributed to + * the next content delta. */ + if (is_reasoning) + em->cb(tmp, 1, NULL, 0, em->ud); + else + em->cb(tmp, 0, em->lp_pending, em->n_lp_pending, em->ud); + em->n_lp_pending = 0; + free(tmp); + } + } +} + +void em_note_token_logprob(emitter *em, const sg_token_logprob *lp) +{ + if (!em || !lp) return; + if (em->n_lp_pending >= EM_LOGPROB_PENDING_MAX) { + /* Drop the oldest: the queue only overflows inside a long diverted + * region (a tool call), whose tokens OpenAI does not report anyway. */ + memmove(em->lp_pending, em->lp_pending + 1, + (size_t)(EM_LOGPROB_PENDING_MAX - 1) * sizeof(em->lp_pending[0])); + em->n_lp_pending = EM_LOGPROB_PENDING_MAX - 1; + } + em->lp_pending[em->n_lp_pending++] = lp; +} + +static void em_carry_set(emitter *em, const char *s, size_t n) +{ + if (n + 1 > em->carry_cap) { + char *grown = realloc(em->carry, n + 1); + if (!grown) return; + em->carry = grown; + em->carry_cap = n + 1; + } + memmove(em->carry, s, n); + em->carry_len = n; + if (em->carry) em->carry[n] = '\0'; +} + +/* Find the earliest complete stop string in [buf,buf+len); returns its offset + * and length, or sets *found=false. */ +static bool find_stop(emitter *em, const char *buf, size_t len, + size_t *at, size_t *stop_len) +{ + bool found = false; + size_t best = len; + for (int i = 0; i < em->n_stop; i++) { + const char *s = em->stops[i]; + if (!s || !*s) continue; + size_t slen = strlen(s); + for (size_t j = 0; j + slen <= len; j++) { + if (memcmp(buf + j, s, slen) == 0) { + if (j < best) { best = j; *stop_len = slen; found = true; } + break; + } + } + } + if (found) *at = best; + return found; +} + +void em_feed(emitter *em, const char *piece) +{ + if (em->stopped) return; + + /* Combine carry + new piece into a working buffer. */ + size_t plen = strlen(piece); + size_t wlen = em->carry_len + plen; + char *work = malloc(wlen + 1); + if (!work) return; + if (em->carry_len) memcpy(work, em->carry, em->carry_len); + memcpy(work + em->carry_len, piece, plen); + work[wlen] = '\0'; + + size_t pos = 0; + while (pos < wlen && !em->stopped) { + if (em->in_think) { + char *marker = memmem(work + pos, wlen - pos, + THINK_CLOSE, strlen(THINK_CLOSE)); + if (marker) { + size_t idx = (size_t)(marker - work); + em_emit(em, 1, work + pos, idx - pos); + pos = idx + strlen(THINK_CLOSE); + em->in_think = 0; + continue; + } + size_t hold = suffix_prefix_overlap(work + pos, wlen - pos, THINK_CLOSE); + em_emit(em, 1, work + pos, (wlen - pos) - hold); + em_carry_set(em, work + wlen - hold, hold); + pos = wlen; + } else if (em->in_tool) { + /* Inside a tool call: capture everything raw for structured parsing; + * never route it to the content channel or the delta callback (the + * tap, when attached, streams it as tool-call deltas). */ + em_tool_append(em, work + pos, wlen - pos); + pos = wlen; + } else { + /* Content channel: a tool-call opener ("" always, or a + * bare "in_tool = 1; + em->carry_len = 0; /* tail is now inside `work`; don't re-prepend */ + pos = wlen; + continue; + } + if (has_stop) { + em_emit(em, 0, work + pos, at); + em->stopped = true; + em->carry_len = 0; + pos = wlen; + continue; + } + /* Hold back the longest tail that could start a stop string or a + * tool-call opener, so none is missed across a token split. The + * bare "dialect == TC_DIALECT_ORNITH_JSON) { + if (em->has_tools) { + /* Hold a partial "```json" fence prefix and a trailing '{' + * whose plausibility (its "function"/"name" key) has not yet + * arrived, so a fence/object split across tokens still + * diverts on the next feed. */ + hold = suffix_prefix_overlap(work + pos, wlen - pos, JSON_FENCE); + size_t htj = trailing_undecided_json_brace(work + pos, wlen - pos); + if (htj > hold) hold = htj; + } + } else { + hold = suffix_prefix_overlap(work + pos, wlen - pos, TOOL_OPEN); + if (em->has_tools) { + size_t hfn = suffix_prefix_overlap(work + pos, wlen - pos, FN_OPEN); + if (hfn > hold) hold = hfn; + size_t hfa = suffix_prefix_overlap(work + pos, wlen - pos, FN_OPEN_ATTR); + if (hfa > hold) hold = hfa; + /* A fully-present but unfollowed "" opener is always armed); `dialect` selects the opener + * set (ORNITH_JSON diverts a ```json fence / bare "{" JSON tool block instead of + * the XML openers). `stops`/`n_stop` are the request's stop strings (borrowed). */ +void em_init(emitter *em, sg_delta_fn cb, void *ud, + char **stops, int n_stop, bool has_tools, tc_dialect dialect); + +/* Attach a tool-channel tap (NULL detaches). Call after em_init. */ +void em_set_tool_tap(emitter *em, em_tool_tap_fn tap, void *ud); + +/* Feed one decoded token's text through the splitter. */ +void em_feed(emitter *em, const char *piece); + +/* Note the logprobs of the token about to be fed, so they ride the content delta + * that eventually carries its bytes. Borrowed — `lp` must outlive the emitter + * (the serve preallocates the whole array for the turn). NULL is a no-op. */ +void em_note_token_logprob(emitter *em, const sg_token_logprob *lp); + +/* Flush any held-back tail at end of generation (no marker/stop completed). */ +void em_flush(emitter *em); + +/* Free the emitter's owned buffers. */ +void em_free(emitter *em); + +#endif /* PN_SERVE_EMITTER_H */ diff --git a/src/serve/serve_generation.c b/src/serve/serve_generation.c new file mode 100644 index 0000000..f7f20f3 --- /dev/null +++ b/src/serve/serve_generation.c @@ -0,0 +1,3598 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_generation.c + * @brief Implementation of chat-completion generation (see serve_generation.h). + * Reuses the engine's forward pass (model->forward), the shared sampler + * (pn_sample_next_token), the qwen35 BPE encoder (pn_bpe_encode) and the + * GPT-2 byte decode used by the CLI/chat paths. + * + * @status Complete for CPU host serving. Splits reasoning, answer + * content, and a block into separate result channels; tool + * results (role:"tool") are re-fed inside wrappers. + * GPU serving steps through sg_forward_step (C2): non-blocking + * launch + event-poll when the model supports it, so concurrent + * generations on different cards overlap from the 2-core host. + * Reasoning-budget force-close: BOTH paths cap AUTO-path reasoning at + * params->reasoning_budget tokens — once a think-style model spends that + * many tokens inside without a self-emitted "", the + * "" close is force-decoded so the model concludes and answers + * (independent of the tool-forcing grammar). The single-user path + * (sg_run_completion) does it inline in its decode loop; the batched + * path (sg_run_completion_batched) arms it PER SLOT in the scheduler's + * driver sampler via bs_slot_arm_reasoning_budget — each batched session + * tracks its own reasoning count + close-injection independently. + * THINK-ELISION (Phase 11): build_prompt reports the forced generation + * prefill length and the BATCHED decode loop counts the generated + * tokens that produce no client-visible byte, so the scheduler can + * resume a turn the client re-renders without its think block. + * REASONING-BEARING RESUME: the same loop also hands the scheduler this + * turn's think block re-rendered from the `reasoning_content` it + * streamed, so a client that sends reasoning BACK — the one that keeps + * the model reasoning — resumes too instead of re-prefilling. + * TOOL-CALL WRAPPER FIDELITY: an assistant history turn's "" + * / "" go back in as the SPECIAL ids the model emits, not as + * byte-encoded text (see TOOL_WRAPPER_MARKERS). + * REPETITION-LOOP GUARD: all THREE decode loops (single-user, batched + * driver-sampling, batched legacy lockstep) feed every generated token + * id to a per-turn cycle detector (repetition_loop_detector.h); a + * confirmed loop ends the turn with finish_reason + * SG_FINISH_REPETITION_LOOP and one "[repetition] STOP" log line. + * ON by default, PANTHEON_REPETITION_STOP=0 disables. + * @issues The single-user path (sg_run_completion) has no think-elision — the + * two paths have diverged again. The deployed fleet runs batched. + * A model that emits a BARE "" + * wrapper on re-render that it never generated, so that turn cannot + * append-resume — it fails safe to a full prefill, never a wrong one. + * "" on a re-fed tool result is still byte-encoded text + * rather than its special id; both sides render it identically so the + * cache is unaffected, but it is not what the model was trained on. + * @todo None + */ + +#include "serve_generation.h" + +#include "request_profile.h" + +#include +#include +#include +#include +#include +#include + +#include "../core/bench.h" +#include "../core/bpe_pretokenize.h" +#include "../core/chat.h" +#include "../core/engine.h" +#include "../core/sampling_modifiers.h" +#include "../core/mem/pn_memwatch.h" +#include "batch_sched.h" +#include "repetition_loop_detector.h" +#include "serve_emitter.h" +#include "serve_prompt_tokens.h" +#include "serve_spec_lookup.h" +#include "grammar_spec_chooser.h" +#include "tool_grammar.h" +#include "tool_grammar_json.h" +#include "response_format_grammar.h" +#include "json_reader.h" +#include "tensor/tensor.h" +#include "tokenizer/tokenizer.h" + +/* Unconditional: the constrained-decoding full-logits seam + * (pn_qwen35moe_state_set_full_logits) is needed in every build, not only the + * CUDA one — the flag is simply never read when there is no device top-k. */ +#include "../models/qwen35moe.h" /* pn_qwen35moe_forward_batch_cuda (L1) */ +#include "../models/llama.h" /* pn_llama_prefill_tt (Phase 7.8b) */ + +/* ============================================================ + * Prefill-throughput counter (K80 serving-ops Phase 7) + * + * A cheap running measure of prefill tokens/sec, fed from every real prefill + * this file runs (delta tokens / wall-time around sg_prefill_range). The + * restart-estimate endpoint reads it to quote a cold re-prefill time. Kept as + * an exponential moving average behind a small mutex — prefills are already + * serialized per model but different models share this file, so the lock keeps + * the EMA update atomic. A tiny prefill (< 8 tokens, e.g. an append turn) is + * ignored: too small to time meaningfully and it would skew the average. + * ============================================================ */ + +static pthread_mutex_t sg_prefill_rate_lock = PTHREAD_MUTEX_INITIALIZER; +static double sg_prefill_rate_ema = 0.0; /* tokens/sec; 0 = no sample yet */ + +static double sg_prefill_now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1000.0 + (double)ts.tv_nsec / 1.0e6; +} + +/* Fold one prefill measurement (tokens over elapsed ms) into the EMA. */ +static void sg_prefill_rate_record(int tokens, double elapsed_ms) +{ + if (tokens < 8 || elapsed_ms <= 0.0) + return; + double rate = (double)tokens / (elapsed_ms / 1000.0); + pthread_mutex_lock(&sg_prefill_rate_lock); + sg_prefill_rate_ema = sg_prefill_rate_ema <= 0.0 + ? rate + : 0.7 * sg_prefill_rate_ema + 0.3 * rate; + pthread_mutex_unlock(&sg_prefill_rate_lock); +} + +double sg_prefill_tok_per_s_measured(void) +{ + pthread_mutex_lock(&sg_prefill_rate_lock); + double r = sg_prefill_rate_ema; + pthread_mutex_unlock(&sg_prefill_rate_lock); + return r; +} + +/* ============================================================ + * Reasoning / content / tool-call stream splitter + * + * The emitter (em_init/em_feed/em_flush/em_free) lives in serve_emitter.c — + * it depends only on libc, so it is unit-tested in isolation + * (tests/test_serve_emitter.c) without the engine/CUDA forward path. It routes + * reasoning vs. answer content into separate channels and diverts a + * tool call (opened by "" always, or a bare "tokenizer, &id, 1); + if (!raw) return NULL; + if (ft_get_model_type(model->tokenizer) == 0) { + char *decoded = gpt2_bytes_decode(raw); + free(raw); + return decoded; + } + return raw; +} + +/* ============================================================ + * Reasoning-budget force-close (AUTO / non-forced path) + * + * A think-style model prefilled with "" reasons until it emits "" + * on its own. A reasoning model can run away — filling the whole generation cap + * with reasoning and never producing an answer (the saber runaway: 1024 tokens of + * reasoning_content, finish=length, empty content). The grammar's forced-mode + * TG_REASON_BUDGET solves this ONLY on the tool_choice REQUIRED/FUNCTION path; + * an orchestration turn arrives as tool_choice AUTO, so that budget never fires. + * + * This is the general, tool-forcing-independent budget. The decode loop counts + * the tokens generated while INSIDE the block (em.in_think); once they + * exceed the per-request/per-model budget and the model has NOT self-closed, it + * FORCE-DECODES the "" close: each of its token ids is fed through + * sg_forward_step (so the KV/SSM state advances over the close exactly as if the + * model had emitted it) and through em_feed (so the emitter's own THINK_CLOSE + * matching flips it into the content channel — no flag-poking). Normal sampling + * then resumes and the model answers. Disabled (budget <= 0) → no-op. + * ============================================================ */ + +/* Tokenize the "" close for this model into `out` (capacity `cap`), + * returning the count (0 on failure). Mirrors build_prompt's tokenization: a + * think-style model has "" as a single special token, but fall back to + * text-encoding it so the primitive is robust to a tokenizer that lacks it. */ +static int sg_encode_think_close(pn_model *model, uint32_t *out, int cap) +{ + ft_tokenizer *tok = model->tokenizer; + int special = ft_token_to_id(tok, ""); + if (special >= 0) { + if (cap < 1) return 0; + out[0] = (uint32_t)special; + return 1; + } + int n = 0; + int *ids = (ft_get_model_type(tok) == 0) + ? pn_bpe_encode(tok, "", &n) + : ft_encode(tok, "", &n); + if (!ids) return 0; + int m = 0; + for (int i = 0; i < n && m < cap; i++) + if (ids[i] >= 0) out[m++] = (uint32_t)ids[i]; + free(ids); + return m; +} + +/* Push one force-decoded token into the repeat-penalty window (same order the + * decode loop appends a sampled token). */ +static void sg_recent_push(uint32_t *recent, int max_recent, int *n_recent, + uint32_t tok) +{ + if (max_recent <= 0) return; + if (*n_recent < max_recent) { + recent[(*n_recent)++] = tok; + } else { + memmove(recent, recent + 1, (size_t)(max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = tok; + } +} + +/* ============================================================ + * Repetition-loop guard + * + * A heavily quantised model at long context can stop producing new text and + * re-emit one paragraph until the token budget is gone — measured on saber-orch + * (Q3_K_M) at ~40k context as a 14,535-byte answer whose distinct content was + * ~470 bytes, cycled 31 times, reported to the caller as a normal finish. + * + * The detector itself is libc-only and lives in repetition_loop_detector.c (see + * that header for the thresholds and the false-positive reasoning); this file + * only feeds it the generated token ids and, on a trip, ends the turn with + * SG_FINISH_REPETITION_LOOP. It is armed the same way in ALL THREE decode + * loops — single-user, batched driver-sampling (what the fleet runs), and the + * batched legacy lockstep path — because a guard on one path and not the others + * is how this file has diverged before. + * ============================================================ */ + +/* Arm the per-turn detector. Left INERT when PANTHEON_REPETITION_STOP=0 or the + * ring allocation fails: an unguarded turn is the pre-existing behaviour, so a + * detector that cannot start must never be able to fail a completion. */ +static void sg_reploop_arm(rld_detector *rd) +{ + memset(rd, 0, sizeof(*rd)); + if (rld_enabled()) + rld_init(rd, NULL); +} + +/* OBSERVABILITY — a repetition collapse is an ABNORMAL event, so it logs + * unconditionally (same rule as the reasoning force-close above), naming the + * model, the decode path, and the shape of the cycle. One greppable line, so a + * run can be counted: `grep -c '\[repetition\] STOP' serve.log`. */ +static void sg_reploop_log(const rld_detector *rd, const sg_gen_params *params, + const char *path, int generated) +{ + fprintf(stderr, "[repetition] STOP model=%s path=%s cycle_tokens=%d " + "repeats=%d generated=%d\n", + (params->model_name && params->model_name[0]) + ? params->model_name : "?", + path, rd->cycle_tokens, rd->repeats, generated); +} + +/* ============================================================ + * Phase 5 — grammar-constrained tool decoding glue + * + * Enabled when sg_tool_grammar_arms() says so (tool_choice "required" by + * default; every tool request with PANTHEON_TOOL_GRAMMAR=1; nothing with =0); + * otherwise sg_tg_setup returns a zeroed (inert) context and the sampler hook + * pointer stays NULL, so generation is byte-identical to before. + * + * The hook needs each candidate token's DECODED UTF-8 text. Decoding per-token + * per-step (≈248k × a malloc) would be far too costly, so we PRECOMPUTE a + * per-vocab piece table ONCE per request (n_vocab decoded strings) and the + * hook's token_text() is then an O(1) array lookup with no allocation in the + * hot sampling loop. + * + * Advance vs. mask split. The serve loop ADVANCES the grammar itself every + * step (with the already-decoded piece), so the FSM tracks the opener while in + * the free reasoning/prose region. The sampler hook only MASKS (advance is + * left NULL), and its `active` flag is refreshed each step from tg_mask_active(): + * 0 while FREE (no mask applied, so a legit prose answer is never forced into a + * call AND the 248k scan is skipped entirely), flipping to 1 the instant the + * grammar has consumed a tool-call opener and entered the skeleton. + * + * FORCED-CALL mode (tool_choice "required" / a named function). The request's + * resolved tool_choice picks tg_state_new_forced instead of tg_state_new; the + * grammar then REQUIRES the opener once reasoning ends. Reasoning-end is driven + * by the grammar itself, which SELF-DETECTS "" in the raw token stream it + * already advances over (BUG 1 fix) — it no longer depends on the emitter's + * flag, which the Q3 model does not reliably trigger (it sometimes + * reasons in the content channel with no clean , so forcing never fired). + * sg_tg_reasoning_ended() is still called off the emitter's -close as a + * harmless SECONDARY trigger (whichever fires first engages the constraint). + * In forced mode tg_mask_active() is also true in the post-reasoning FREE state, + * so prose/JSON are masked out. AUTO mode is unchanged (and byte-identical + * unless PANTHEON_TOOL_GRAMMAR=1 — the hook pointer stays NULL). + * ============================================================ */ + +/* Does this request arm the tool-call grammar? PANTHEON_TOOL_GRAMMAR: + * "0" off for every request (explicit opt-out); + * "1" every request that carries tools (AUTO engages on an opener, + * "required"/named function compel a call) — the old opt-in; + * unset / other only tool_choice "required": the client asked for a call + * every turn, and without the grammar a model that ends in + * narration hands it a turn with no call at all. + * A named function stays behind "1", unchanged. */ +bool sg_tool_grammar_arms(const sg_gen_params *params) +{ + if (!params || !params->tools) return false; + const char *env = getenv("PANTHEON_TOOL_GRAMMAR"); + if (env && env[0] == '0') return false; + if (env && env[0] == '1') return true; + return params->tool_choice == SG_TOOL_CHOICE_REQUIRED; +} + +/* "required" is the one armed shape that speculates under the grammar on the + * Tenstorrent loop (grammar_spec_chooser.h): its mask is off for the whole + * think block, so the turn keeps the top-N rows and the drafts, and the masked + * steps refill the dense row. Every other armed shape keeps the dense row and + * plain decode, exactly as before. */ +#ifdef USE_TT +static bool sg_tool_grammar_speculates(const sg_gen_params *params) +{ + return sg_tool_grammar_arms(params) && params->tool_choice == SG_TOOL_CHOICE_REQUIRED; +} +#endif + +/* The think-gated "required" walk exists for every dialect a grammar models + * (tgj_state_new_required for ORNITH_JSON, tg_state_new_required for QWEN_XML); + * MiniCPM's attribute form is not modelled, so it cannot claim the guarantee. */ +bool sg_tool_choice_required_with_thinking(tc_dialect dialect) +{ + const char *env = getenv("PANTHEON_TOOL_GRAMMAR"); + return tc_dialect_grammar_models(dialect) && !(env && env[0] == '0'); +} + +/* Which grammar is driving this request's constrained decode. EXACTLY ONE is + * ever live: the tool grammars constrain a tool CALL, the response-format + * grammar constrains the ANSWER, and a single sampler hook cannot serve two + * masks. The HTTP layer resolves the precedence (tools win) before we get here, + * so this is a plain three-way selector, not a conflict. */ +typedef enum { + SG_GRAMMAR_NONE = 0, + SG_GRAMMAR_TOOL_XML, /* tool_grammar.c — Qwen/N2/MiniCPM dialects */ + SG_GRAMMAR_TOOL_JSON, /* tool_grammar_json.c — ornith's JSON dialect */ + SG_GRAMMAR_RESPONSE_JSON, /* response_format_grammar.c — json_object answer */ +} sg_grammar_kind; + +/* The constrained-decoding context: the live grammar, its walk state, the + * per-vocab decoded-piece table the mask needs, and the sampler hook the engine + * calls. All the sg_tg_* helpers below dispatch on `kind` so the serve loops + * (single-user + batched) drive any grammar unchanged. */ +typedef struct { + sg_grammar_kind kind; + tg_grammar *grammar; /* XML dialect */ + tg_state *state; + tgj_grammar *grammar_json;/* JSON dialect (ornith) */ + tgj_state *state_json; + rfg_state *state_rf; /* response_format json_object (no grammar object: + * the shape is fixed, nothing to compile) */ + char **pieces; /* [n_vocab] decoded UTF-8 text per token id */ + int n_vocab; + pn_tool_grammar_hook hook; + bool enabled; + bool forced; /* FORCED walk (tool_choice required/function) — + * reported alongside a DEAD verdict, because AUTO + * and FORCED fail in opposite directions and the + * log has to say which one this turn was */ + bool verbose; /* PANTHEON_TOOL_GRAMMAR_DEBUG=1 → per-step trace */ + /* ── Full-logits arming window (see the block comment above + * sg_arm_full_logits). `fl_always` is a turn-lifetime requirement that has + * nothing to do with the grammar walk (logit_bias / logprobs); `fl_pending` + * is the one-shot latch that covers the reasoning force-close, the only + * place a grammar mutation lands after the forward it should have preceded. */ + bool fl_always; + bool fl_pending; + /* Engine mask-density counters as they stood when this turn STARTED, so + * the turn can report its own delta rather than a process running total + * (see sg_tg_report_mask_density). */ + unsigned long fl_mask_steps0; + unsigned long fl_sparse_rows0; + unsigned long fl_shortlist_steps0; + unsigned long fl_chunk_steps0; + unsigned long fl_fullscan_steps0; + double fl_mask_seconds0; +} sg_tg_ctx; + +/* True when the live grammar is one of the tool-call grammars. */ +static bool sg_tg_is_tool(const sg_tg_ctx *c) { + return c->kind == SG_GRAMMAR_TOOL_XML || c->kind == SG_GRAMMAR_TOOL_JSON; +} + +static const char *sg_tg_token_text(void *user, uint32_t id) { + sg_tg_ctx *c = (sg_tg_ctx *)user; + if ((int)id >= c->n_vocab) return NULL; + return c->pieces[id]; +} + +static int sg_tg_allowed(void *st, const char *text) { + return tg_token_allowed((const tg_state *)st, text) ? 1 : 0; +} +static int sg_tg_allowed_json(void *st, const char *text) { + return tgj_token_allowed((const tgj_state *)st, text) ? 1 : 0; +} + +/* Driver-side callbacks (batched path). The batched driver samples per-slot in + * its own thread from the slot's copied sampling params, so the grammar must + * advance + gate + report completion FROM the sampler itself (the single-user + * loop does these externally with advance == NULL). These three make the hook + * self-contained for the driver. */ +static void sg_tg_advance_cb(void *st, const char *text) { + tg_advance((tg_state *)st, text); +} +static int sg_tg_refresh_cb(void *st) { + return tg_mask_active((const tg_state *)st) ? 1 : 0; +} +static int sg_tg_complete_cb(void *st) { + return tg_complete((const tg_state *)st) ? 1 : 0; +} +static void sg_tg_advance_cb_json(void *st, const char *text) { + tgj_advance((tgj_state *)st, text); +} +static int sg_tg_refresh_cb_json(void *st) { + return tgj_mask_active((const tgj_state *)st) ? 1 : 0; +} +static int sg_tg_complete_cb_json(void *st) { + return tgj_complete((const tgj_state *)st) ? 1 : 0; +} +static int sg_tg_in_call_cb(void *st) { + return tg_in_call((const tg_state *)st) ? 1 : 0; +} +/* Batched-driver callback (pn_tool_grammar_hook.reasoning_end_injected): the + * driver finished decoding the reasoning-budget's injected "" and must + * tell the grammar that close was NOT the model's, so the opener is not compelled + * around an unfinished thought. XML dialect only — see + * sg_tg_reasoning_ended_injected for why the JSON dialect has no window. */ +static void sg_tg_reasoning_end_injected_cb(void *st) { + tg_reasoning_end_injected((tg_state *)st); +} +static int sg_tg_end_allowed_json(void *st) { + return tgj_end_allowed((const tgj_state *)st) ? 1 : 0; +} +static int sg_tg_end_allowed_xml(void *st) { + return tg_end_allowed((const tg_state *)st) ? 1 : 0; +} +static int sg_tg_in_call_cb_json(void *st) { + return tgj_in_call((const tgj_state *)st) ? 1 : 0; +} +static int sg_tg_allowed_rf(void *st, const char *text) { + return rfg_token_allowed((const rfg_state *)st, text) ? 1 : 0; +} +static void sg_tg_advance_cb_rf(void *st, const char *text) { + rfg_advance((rfg_state *)st, text); +} +static int sg_tg_refresh_cb_rf(void *st) { + return rfg_mask_active((const rfg_state *)st) ? 1 : 0; +} +static int sg_tg_complete_cb_rf(void *st) { + return rfg_complete((const rfg_state *)st) ? 1 : 0; +} +static int sg_tg_in_call_cb_rf(void *st) { + return rfg_in_json((const rfg_state *)st) ? 1 : 0; +} + +/* Build the ornith JSON grammar + walk state (dialect TC_DIALECT_ORNITH_JSON). + * Sets c->grammar_json / c->state_json and points the hook at the JSON callbacks. + * Returns true if the grammar built, false to leave the feature off (inert). */ +static bool sg_tg_build_json(sg_tg_ctx *c, pn_model *model, const sg_gen_params *params, + bool *forced_out) { + c->grammar_json = tgj_build((const jr_value *)params->tools); + if (!c->grammar_json) return false; + bool forced = false; + if (params->tool_choice == SG_TOOL_CHOICE_REQUIRED) { + /* THINK-GATED: the think block the prompt opened is never constrained; + * the call is compelled from the token after the model's "" + * (or from token 0 when no block was opened). */ + bool think_open = sg_chat_style_is_think(model->chat_style) && + !sg_thinking_suppressed(params->thinking); + c->state_json = tgj_state_new_required(c->grammar_json, think_open); + forced = (c->state_json != NULL); + } else if (params->tool_choice == SG_TOOL_CHOICE_FUNCTION) { + c->state_json = tgj_state_new_forced(c->grammar_json, params->forced_name); + forced = (c->state_json != NULL); + } else { + c->state_json = tgj_state_new(c->grammar_json); + } + if (!c->state_json && params->tool_choice == SG_TOOL_CHOICE_FUNCTION) + c->state_json = tgj_state_new(c->grammar_json); + if (!c->state_json) { + tgj_free(c->grammar_json); c->grammar_json = NULL; return false; + } + c->kind = SG_GRAMMAR_TOOL_JSON; + c->hook.st = c->state_json; + c->hook.allowed = sg_tg_allowed_json; + *forced_out = forced; + return true; +} + +/* Build the XML grammar + walk state (Qwen/N2/MiniCPM dialects). Same shape as + * the JSON builder above, "required" think-gated the same way. */ +static bool sg_tg_build_xml(sg_tg_ctx *c, pn_model *model, const sg_gen_params *params, + bool *forced_out) { + c->grammar = tg_build((const jr_value *)params->tools); + if (!c->grammar) return false; + bool forced = false; + if (params->tool_choice == SG_TOOL_CHOICE_REQUIRED) { + bool think_open = sg_chat_style_is_think(model->chat_style) && + !sg_thinking_suppressed(params->thinking); + c->state = tg_state_new_required(c->grammar, think_open); + forced = (c->state != NULL); + } else if (params->tool_choice == SG_TOOL_CHOICE_FUNCTION) { + c->state = tg_state_new_forced(c->grammar, params->forced_name); + forced = (c->state != NULL); + } else { + c->state = tg_state_new(c->grammar); + } + if (!c->state && params->tool_choice == SG_TOOL_CHOICE_FUNCTION) + c->state = tg_state_new(c->grammar); + if (!c->state) { tg_free(c->grammar); c->grammar = NULL; return false; } + c->kind = SG_GRAMMAR_TOOL_XML; + c->hook.st = c->state; + c->hook.allowed = sg_tg_allowed; + *forced_out = forced; + return true; +} + +/* Build the response-format grammar: the ANSWER must be one valid JSON object. + * + * `reasoning_grace` mirrors what the prompt actually did — a think-style model + * is prefilled with an open , so its first tokens are reasoning and must + * stay unconstrained until "". With No-Think on, the block was prefilled + * CLOSED and the very first token is already the answer, so the constraint + * engages immediately. */ +static bool sg_tg_build_response_format(sg_tg_ctx *c, pn_model *model, + const sg_gen_params *params) { + bool reasoning_grace = sg_chat_style_is_think(model->chat_style) && + !sg_thinking_suppressed(params->thinking); + c->state_rf = rfg_state_new(RFG_FORMAT_JSON_OBJECT, reasoning_grace); + if (!c->state_rf) return false; + c->kind = SG_GRAMMAR_RESPONSE_JSON; + c->hook.st = c->state_rf; + c->hook.allowed = sg_tg_allowed_rf; + return true; +} + +/* The end-of-turn ids must not be sampleable while the JSON object is still + * open, or the model can stop mid-object and hand the client truncated, + * un-parseable content — the exact silent breakage response_format exists to + * prevent. The mask only ever sees a token's TEXT, and these decode to "" + * (which the sampler treats as always-allowed), so their piece-table entries are + * replaced with a sentinel the JSON grammar rejects. The mask is inactive + * during reasoning and once the object closes, so EOS is sampleable exactly + * where it should be. Tool grammars keep the untouched table (their forced mode + * relies on being able to force EOS as a clean exit); the "required" JSON walk + * governs its end tokens through hook.end_allowed instead. */ +static void sg_tg_mask_end_tokens(sg_tg_ctx *c, pn_model *model) { + static const char SENTINEL[] = "\x01"; /* never a valid JSON continuation */ + const int end_ids[2] = { ft_eos_id(model->tokenizer), + ft_token_to_id(model->tokenizer, "<|im_end|>") }; + for (int i = 0; i < 2; i++) { + int id = end_ids[i]; + if (id < 0 || id >= c->n_vocab) continue; + char *sentinel = strdup(SENTINEL); + if (!sentinel) continue; + free(c->pieces[id]); + c->pieces[id] = sentinel; + } +} + +/* "required": the end ids must reach the mask as tokens WITH NO TEXT, the only + * shape hook.end_allowed governs. The tokenizer decodes them to their literal + * text ("<|im_end|>"), which a free-text value accepts as content: the end rule + * then never fired inside a value, and the batched driver advanced the walk + * over "<|im_end|>" as value bytes — so a floored value the client never saw + * filled counted as filled and the serve closed `bash {"command":""}` (N2, + * K80, closer probe). Blank both; advancing over "" is a no-op. */ +static void sg_tg_blank_end_tokens(sg_tg_ctx *c, pn_model *model) { + const int end_ids[2] = { ft_eos_id(model->tokenizer), + ft_token_to_id(model->tokenizer, "<|im_end|>") }; + for (int i = 0; i < 2; i++) { + int id = end_ids[i]; + if (id < 0 || id >= c->n_vocab) continue; + char *blank = strdup(""); + if (!blank) continue; + free(c->pieces[id]); + c->pieces[id] = blank; + } +} + +/* Build the grammar context for this request, or leave it inert. Returns 0 + * always (a build failure just leaves the feature off — never regress serve). + * Selects the grammar by dialect: TC_DIALECT_ORNITH_JSON → the ornith JSON + * grammar (which honors `required` natively, so ornith runs FORCED like saber); + * every XML dialect → the trained XML skeleton grammar. */ +static void sg_tg_setup(sg_tg_ctx *c, pn_model *model, + const sg_gen_params *params, int n_vocab) { + memset(c, 0, sizeof(*c)); + + /* Tool-call constraint — see sg_tool_grammar_arms for the policy. */ + const char *env = getenv("PANTHEON_TOOL_GRAMMAR"); + bool tool_grammar_on = sg_tool_grammar_arms(params); + /* Response-format constraint: NOT env-gated. `response_format` is a + * standard OpenAI field a client sends expecting a guarantee; honouring it + * only when an operator happens to have set an env var would be the same + * silent degradation as ignoring it outright. The HTTP layer has already + * resolved tools-vs-response_format, so both are never on at once. */ + bool response_format_on = + (params->response_format == SG_RESPONSE_FORMAT_JSON_OBJECT); + + bool forced = false; + bool built = false; + if (tool_grammar_on) { + built = (params->dialect == TC_DIALECT_ORNITH_JSON) + ? sg_tg_build_json(c, model, params, &forced) + : sg_tg_build_xml(c, model, params, &forced); + } else if (response_format_on) { + built = sg_tg_build_response_format(c, model, params); + } + if (!built) { + /* SAY SO, ONCE. A request that carried tools and armed no constraint + * decodes its tool call on the model's own habits, and until now that + * fact left NO trace: the "ENABLED" line below never printed, the mask + * never ran, and a model improvising a near-miss dialect + * ("bash") produced a turn with no + * call and a silent log. Chasing that silence costs a session, because + * an absent line is evidence of nothing — it reads the same whether the + * grammar was off, armed-and-inert, or armed-and-broken. + * + * Logged on a decade schedule (1st, 10th, 100th, …) rather than once: + * once answers "is it armed", the running total answers "how much of + * this serve's traffic decoded that way", and a decade schedule gives + * both without a line per request on a fleet that never turns it on. + * The counter is a plain unsigned long touched from several request + * threads — a lost increment costs a log line, nothing else, which is + * why it does not carry a lock. */ + if (params->tools) { + static unsigned long unconstrained_turns = 0; + unsigned long n = ++unconstrained_turns; + bool decade = true; + for (unsigned long d = n; d > 1; d /= 10) + if (d % 10 != 0) { decade = false; break; } + if (decade) + fprintf(stderr, "[tool_grammar] UNCONSTRAINED: this request " + "carries tools but no decoding constraint is armed " + "(PANTHEON_TOOL_GRAMMAR=%s, %s) — tool-call syntax is " + "whatever the model emits [turns=%lu]\n", + env ? env : "unset", + !tool_grammar_on ? "feature off" + : "grammar build failed", n); + } + return; + } + + c->pieces = calloc((size_t)n_vocab, sizeof(char *)); + if (!c->pieces) { + tg_state_free(c->state); tg_free(c->grammar); + tgj_state_free(c->state_json); tgj_free(c->grammar_json); + rfg_state_free(c->state_rf); + memset(c, 0, sizeof(*c)); + return; + } + for (int i = 0; i < n_vocab; i++) + c->pieces[i] = decode_token(model, (uint32_t)i); /* may be NULL */ + c->n_vocab = n_vocab; + if (c->kind == SG_GRAMMAR_RESPONSE_JSON) + sg_tg_mask_end_tokens(c, model); + /* tool_choice "required" (think-gated walk, either dialect): the turn may not + * end before its call opens, and may end inside it only where the serve can + * close it (tgj_end_allowed / tg_end_allowed; the loops append the closing + * text, sg_tg_close_required_call). */ + if (sg_tg_is_tool(c) && params->tool_choice == SG_TOOL_CHOICE_REQUIRED) { + c->hook.end_allowed = (c->kind == SG_GRAMMAR_TOOL_JSON) ? sg_tg_end_allowed_json + : sg_tg_end_allowed_xml; + sg_tg_blank_end_tokens(c, model); + } + + c->hook.token_text = sg_tg_token_text; + c->hook.advance = NULL; /* serve loop advances; hook masks only */ + c->hook.user = c; + c->hook.n_vocab = n_vocab; + c->hook.active = 0; /* refreshed each step from the grammar */ + /* Fail-CLOSED only in forced mode: on a 0-survivor step force EOS instead of + * falling open to the model's out-of-grammar top token. AUTO mode keeps the + * legacy fail-open (a spurious in-call dead-end there must not truncate a real + * generation, and AUTO never COMPELS a call). */ + if (forced) { + int eos = ft_eos_id(model->tokenizer); + if (eos >= 0) { + c->hook.fail_closed = 1; + c->hook.forced_token = (uint32_t)eos; + } + } + c->enabled = true; + c->forced = forced; + const char *dbg = getenv("PANTHEON_TOOL_GRAMMAR_DEBUG"); + c->verbose = (dbg && dbg[0] == '1'); + if (c->kind == SG_GRAMMAR_RESPONSE_JSON) { + fprintf(stderr, "[response_format] constrained decoding ENABLED " + "(%d-vocab mask, json_object)\n", n_vocab); + } else { + fprintf(stderr, "[tool_grammar] constrained decoding ENABLED (%d-vocab mask, " + "dialect=%s, mode=%s%s%s)\n", n_vocab, + c->kind == SG_GRAMMAR_TOOL_JSON ? "ORNITH_JSON" : "XML", + (params->tool_choice == SG_TOOL_CHOICE_REQUIRED) ? "REQUIRED" + : (params->tool_choice == SG_TOOL_CHOICE_FUNCTION) ? "FUNCTION" + : (params->tool_choice == SG_TOOL_CHOICE_NONE) ? "NONE" : "AUTO", + params->forced_name ? " fn=" : "", + params->forced_name ? params->forced_name : ""); + } +} + +/* ============================================================ + * OpenAI `logprobs` accumulation + * + * The sampler reports each token's logprobs through a pn_logprob_sink. On the + * BATCHED path that sink fires inside the shared decode driver — holding the + * scheduler lock, on every other session's critical path — so it must not + * detokenize there. It therefore records ids + floats only, and the request's + * OWN thread resolves the text as it consumes each token. + * + * Both arrays are PREALLOCATED for the whole turn (max_tokens + the force-close + * headroom). Entries are handed to the stream emitter by pointer so they can + * ride the content delta that carries their bytes, and a realloc would leave + * those pointers dangling. + * ============================================================ */ + +typedef struct { + uint32_t chosen; + float chosen_logprob; + int n_top; + uint32_t top_ids[PN_LOGPROBS_MAX_TOP]; + float top_logprob[PN_LOGPROBS_MAX_TOP]; + bool injected; /* a token the serve force-decoded rather than sampled; + * recorded so the sequence stays aligned with the + * emitted token stream (see the pn_logprob_sink + * injected-token contract) */ +} sg_raw_logprob; + +typedef struct { + sg_raw_logprob *raw; + int n; /* records written by the sampler */ + int taken; /* records resolved by the request thread */ + int cap; + pn_logprob_sink sink; +} sg_logprob_acc; + +/* pn_logprob_sink callback: record one step. Runs on whichever thread sampled. */ +static void sg_logprob_emit(void *user, uint32_t chosen, float chosen_logprob, + const uint32_t *top_ids, const float *top_logprob, + int n_top) +{ + sg_logprob_acc *acc = (sg_logprob_acc *)user; + if (!acc || !acc->raw || acc->n >= acc->cap) return; + sg_raw_logprob *r = &acc->raw[acc->n++]; + /* NaN is the documented placeholder for a force-injected token. isnan(), + * not the `v == v` self-compare: this TU builds with -ffast-math, which + * folds the self-compare to constant true (proven — the whole check + * compiles to `return 0` unconditionally), so injected tokens would + * never be flagged and the raw NaN sentinel would leak out as + * chosen_logprob. */ + r->injected = isnan(chosen_logprob); + r->chosen = chosen; + r->chosen_logprob = r->injected ? 0.0f : chosen_logprob; + r->n_top = 0; + if (!r->injected && top_ids && top_logprob) { + int k = n_top > PN_LOGPROBS_MAX_TOP ? PN_LOGPROBS_MAX_TOP : n_top; + for (int i = 0; i < k; i++) { + r->top_ids[i] = top_ids[i]; + r->top_logprob[i] = top_logprob[i]; + } + r->n_top = k > 0 ? k : 0; + } +} + +/* Arm the accumulator + the result's reported array for a turn of at most + * `cap` tokens. Returns false (and leaves everything inert) when the request + * did not ask for logprobs or an allocation failed — logprobs are a reporting + * feature and must never fail a generation. */ +static bool sg_logprobs_init(sg_logprob_acc *acc, sg_result *result, + const sg_gen_params *params, int cap) +{ + memset(acc, 0, sizeof(*acc)); + if (!params->logprobs || cap <= 0) return false; + acc->raw = calloc((size_t)cap, sizeof(*acc->raw)); + if (!acc->raw) return false; + result->logprobs = calloc((size_t)cap, sizeof(*result->logprobs)); + if (!result->logprobs) { free(acc->raw); acc->raw = NULL; return false; } + acc->cap = cap; + acc->sink.n_top = params->logprobs_top; + acc->sink.emit = sg_logprob_emit; + acc->sink.user = acc; + return true; +} + +static void sg_logprobs_attach(sg_logprob_acc *acc, pn_sampling_params *sp) +{ + sp->logprobs = acc->raw ? &acc->sink : NULL; +} + +static void sg_logprobs_free(sg_logprob_acc *acc) +{ + free(acc->raw); + memset(acc, 0, sizeof(*acc)); +} + +/* + * Resolve the NEXT recorded step into the result's reported array, decoding the + * alternative tokens' text. `piece` is the chosen token's already-decoded text + * (the decode loop has it, so it is not decoded twice). + * + * Returns the entry (borrowed and stable for the whole turn — the array is + * preallocated) so the caller can hand it to the stream emitter, or NULL when + * logprobs are off, the sampler recorded nothing for this token, or the token + * was force-injected. + * + * Exactly ONE of this and sg_logprobs_skip must be called per token the decode + * loop consumes, or the record sequence stops being paired with the token + * stream and every later token's logprobs land on the wrong text. + */ +/* Consume the next record WITHOUT reporting it — for a token that went to the + * reasoning channel. OpenAI's `logprobs.content[]` describes the ANSWER, and + * our reasoning is a separate channel, so a think-block token must not appear + * there. Skipping (rather than simply not calling take) is what keeps the + * record sequence aligned for the tokens that follow. */ +static void sg_logprobs_skip(sg_logprob_acc *acc) +{ + if (acc->raw && acc->taken < acc->n) acc->taken++; +} + +static const sg_token_logprob *sg_logprobs_take(sg_logprob_acc *acc, + sg_result *result, + pn_model *model, + const char *piece) +{ + if (!acc->raw || acc->taken >= acc->n) return NULL; + const sg_raw_logprob *r = &acc->raw[acc->taken++]; + if (r->injected) return NULL; + if (result->n_logprobs >= acc->cap) return NULL; + + sg_token_logprob *out = &result->logprobs[result->n_logprobs]; + out->token = strdup(piece ? piece : ""); + if (!out->token) return NULL; + out->logprob = r->chosen_logprob; + out->n_top = 0; + for (int i = 0; i < r->n_top; i++) { + char *txt = decode_token(model, r->top_ids[i]); + out->top_token[out->n_top] = txt ? txt : strdup(""); + out->top_logprob[out->n_top] = r->top_logprob[i]; + out->n_top++; + } + result->n_logprobs++; + return out; +} + +/* + * Copy the request's OpenAI sampling fields onto the engine's sampling params. + * + * Every field is a no-op at its default (0 / NULL / seed unset), so a request + * that sends none of them decodes byte-identically to before. Shared by the + * single-user and batched paths so the two cannot drift — the last two bugs in + * this area were a field that worked on one path only. + */ +static void sg_apply_openai_sampling(pn_sampling_params *sp, + const sg_gen_params *params) +{ + sp->presence_penalty = params->presence_penalty; + sp->frequency_penalty = params->frequency_penalty; + sp->logit_bias = params->logit_bias; + sp->n_logit_bias = params->n_logit_bias; + if (params->seed_set) { + sp->rng_seeded = 1; + sp->rng_state = pn_seed_to_rng_state(params->seed); + } +} + +/* + * Which requests need the WHOLE logits row. + * + * The K80 forward reduces the LM head ON THE DIE to the top-k (id, logit) pairs + * so only k pairs cross the 0.4 GB/s host link — the host row is -inf everywhere + * else. That is invisible and harmless for ordinary sampling, but any feature + * that must reason about a token the model did NOT rank highly is silently + * defeated by it: + * + * - a GRAMMAR mask can only choose among logits it can see, so when none of + * the legal tokens is in the model's top-k the step has zero survivors, the + * sampler fails open, and the walk dies — the response_format defect; + * - `logit_bias` cannot lift (or veto) a pruned token at all: -inf + 100 is + * still -inf, so a bias on anything outside the top-k is a no-op; + * - `logprobs` are normalised over whatever survived, so a pruned row yields a + * log-softmax taken over ~k candidates instead of the vocabulary. + * + * `logit_bias` and `logprobs` ask for the full row for the turn's lifetime; a + * GRAMMAR asks for it only while its mask is live (see sg_arm_full_logits). + * Everything else keeps the fast top-k path untouched. + */ +/* The turn-lifetime half of the requirement: features that need every logit on + * EVERY step, with no window to narrow. */ +static bool sg_full_logits_always(const sg_gen_params *params) +{ + return (params->logit_bias && params->n_logit_bias > 0) || + params->logprobs; +} + +#ifdef USE_TT +/* Before sg_tg_setup has run: could this request arm a grammar, or need the whole row? */ +static bool sg_may_need_full_logits(const sg_gen_params *params) +{ + return sg_full_logits_always(params) || + (sg_tool_grammar_arms(params) && !sg_tool_grammar_speculates(params)) || + params->response_format == SG_RESPONSE_FORMAT_JSON_OBJECT; +} +#endif + +static bool sg_needs_full_logits(const sg_tg_ctx *c, const sg_gen_params *params) +{ + return c->enabled || sg_full_logits_always(params); +} + +static void sg_require_full_logits(const sg_tg_ctx *c, + const sg_gen_params *params, + pn_model *model, pn_state *state, int on) +{ + if (!state) return; + if (on && !sg_needs_full_logits(c, params)) return; + if (!pn_qwen35moe_owns_state(model)) return; + pn_qwen35moe_state_set_full_logits(state, on); +} + +/* ============================================================ + * Full-logits ARMING WINDOW (PN_GRAMMAR_ARM_WINDOW=1, default OFF) + * + * WHY A WINDOW. The K80 head reduces the 248320-float logits row to k=40 + * (id, logit) pairs on the die. A request that must reason about a token the + * model did NOT rank highly needs the whole row instead, and that costs the + * constrained slot ~4 ms of D2H per token PLUS a dense host sampler (~7.35 ms + * at temp 0.7 — a dense row falls out of PN_SPARSE_SAMPLER's fast path). + * `logit_bias` and `logprobs` need it on every step of the turn. A tool + * GRAMMAR does not: in AUTO mode the mask is inactive for the whole reasoning + * region — up to the 2000-token reasoning budget — and engages only once the + * model has actually emitted a tool-call opener. Arming for the TURN therefore + * pays the full-row price through the entire reasoning budget for a mask that + * is not being applied, on what the client makes the common turn shape. + * + * THE INVARIANT THIS MUST MAINTAIN + * + * On every step whose sampler evaluates the grammar mask, the row it reads + * is the FULL vocabulary row. + * + * A missed mask is not a slow step, it is a correctness bug: the mask can only + * choose among logits it can see, so a sparse row holding none of the legal + * tokens has zero survivors and the walk fails open (AUTO) or force-terminates + * (FORCED) — the response_format defect. engine.c counts violations + * (pn_sampler_grammar_sparse_rows) so the invariant is observable at runtime + * rather than argued about. + * + * WHY THE OBVIOUS FIX BREAKS IT — THE TRANSITION STEP. `hook.active` is + * refreshed at the TOP of a step, from the grammar state as it stood after the + * PREVIOUS token. The forward that fetches the NEXT row runs at the BOTTOM of + * that same step, after this step's token has already advanced the grammar. + * Arming off `hook.active` (or, equivalently, off "is the grammar active right + * now?" asked before the sampler runs) is therefore stale by exactly one token + * — and the token it is stale on is precisely the opener that engages the + * grammar. The transition step gets a sparse row under an active mask. + * PN_GRAMMAR_ARM_STALE=1 reproduces that ordering on purpose, so a gate can + * prove its detector fires instead of assuming it would. + * + * THE MECHANISM. Arm from the grammar state AFTER the advance and immediately + * BEFORE the forward. mask_active() is a pure function of the walk state, and + * the walk state is mutated only by advance() (over a sampled or an injected + * token) and by sg_tg_reasoning_ended(). The forward for step T+1 consumes + * exactly the token the sampler at step T advanced over, so mask_active() read + * between those two events is bit-for-bit the value refresh() will return at + * step T+1's sampler. This is not a prediction carrying a lookahead error — it + * is the same read, moved to the other side of the advance. + * + * THE ONE ORDERING HOLE, AND ITS LATCH. The single-user reasoning-budget + * force-close is the only place a grammar mutation can land AFTER the forward + * that fetched the row the next step samples: the block runs at the bottom of + * the loop and ends with sg_tg_reasoning_ended(). `fl_pending` is latched the + * moment the budget TRIPS — which happens before that forward — and cleared + * once the whole close has run, so every forward in the window is armed + * unconditionally. The batched driver has no such hole: it samples, advances, + * injects and arms inside one critical section and only then issues the next + * step (see driver_arm_full_logits in batch_sched.c). + * + * KNOWN, PRE-EXISTING, NOT CHANGED HERE. On the single-user path sg_tg_setup + * runs AFTER the prefill, so the FIRST sampled token of a grammar turn already + * reads a sparse row today, window or no window. It only bites a FORCED + * grammar whose reasoning has already ended at token 0; the batched path (the + * production shape) arms before its prefill and is unaffected. Left alone + * deliberately so this change stays one mechanism, and now detected at runtime + * by the engine counter above. + * ============================================================ */ + +/* PN_GRAMMAR_ARM_WINDOW=1 — arm the full row only while the grammar mask is + * live, instead of for the whole turn. Default OFF: this changes which rows + * cross the host link on the live tool-calling path and has not been through + * the agent-client gate. Process-constant. */ +static int sg_grammar_arm_window_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("PN_GRAMMAR_ARM_WINDOW"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* PN_GRAMMAR_ARM_STALE=1 — FALSIFIER, only meaningful with the window on. + * Arms from the grammar state as it stood BEFORE this step's advance, i.e. + * reintroduces the one-token-late ordering the window exists to avoid. It + * exists so the gate can watch pn_sampler_grammar_sparse_rows go non-zero and + * know the detector is not vacuous. Never set this in production. */ +static int sg_grammar_arm_stale_on(void) +{ + static int v = -1; + if (v < 0) { + const char *env = getenv("PN_GRAMMAR_ARM_STALE"); + v = (env && env[0] && env[0] != '0') ? 1 : 0; + } + return v; +} + +/* Would the mask be applied if a step sampled RIGHT NOW? A pure read of the + * live grammar walk — the same three-way dispatch the sampler's refresh() + * callbacks make, and the same function `sg_tg_refresh` publishes into + * `hook.active`. Keeping one predicate is the point: the arming window is only + * correct because what it reads is literally what the sampler will read. */ +static bool sg_tg_mask_active(const sg_tg_ctx *c) { + if (!c->enabled) return false; + return (c->kind == SG_GRAMMAR_TOOL_JSON) ? tgj_mask_active(c->state_json) + : (c->kind == SG_GRAMMAR_RESPONSE_JSON) ? rfg_mask_active(c->state_rf) + : tg_mask_active(c->state); +} + +/** + * Arm (or disarm) the full-vocabulary logits row for the NEXT forward. + * + * Call immediately before every forward and after every grammar mutation that + * precedes it — that ordering is the whole mechanism; see the block comment + * above sg_grammar_arm_window_on for why one token either way matters. + * + * @param c the request's grammar context (may be inert) + * @param params the request's generation params (logit_bias / logprobs) + * @param model the model owning `state` (non-qwen35 states are left alone) + * @param state the decode state whose logits row is being armed + * + * Thread-safe: no — called by whichever thread owns `state`'s next forward + */ +static void sg_arm_full_logits(const sg_tg_ctx *c, const sg_gen_params *params, + pn_model *model, pn_state *state) +{ + if (!state || !pn_qwen35moe_owns_state(model)) return; + int need; + if (c->fl_always || sg_full_logits_always(params)) need = 1; + else if (!c->enabled) need = 0; + else if (!sg_grammar_arm_window_on()) need = 1; + else if (sg_grammar_arm_stale_on()) need = c->hook.active ? 1 : 0; + else need = (c->fl_pending || sg_tg_mask_active(c)) ? 1 : 0; + pn_qwen35moe_state_set_full_logits(state, need); +} + +/* Snapshot the engine's mask-density counters at the start of a constrained + * turn, so the turn can report its own delta. */ +static void sg_tg_mark_mask_density(sg_tg_ctx *c) { + c->fl_mask_steps0 = pn_sampler_grammar_mask_steps(); + c->fl_sparse_rows0 = pn_sampler_grammar_sparse_rows(); + c->fl_shortlist_steps0 = pn_sampler_grammar_shortlist_steps(); + c->fl_chunk_steps0 = pn_sampler_grammar_chunk_steps(); + c->fl_fullscan_steps0 = pn_sampler_grammar_fullscan_steps(); + c->fl_mask_seconds0 = pn_sampler_grammar_mask_seconds(); +} + +/* Report this turn's mask-density result: how many of its steps evaluated the + * grammar mask, and how many of those read a SPARSE row. + * + * `sparse=0` is the invariant. `masked=0` is the other thing worth seeing: it + * means the turn never engaged the grammar at all, so a clean `sparse=0` on its + * own would be proving nothing — which is exactly the failure mode a + * throughput-only gate would sail past. + * + * The engine counters are PROCESS-WIDE, so this delta is exactly this turn's + * only while one turn is decoding; with K concurrent constrained slots each + * line carries some of its neighbours'. Fine for what it is — a tripwire whose + * only interesting value is zero — and the gate runs at K=1, where the + * attribution is exact. */ +static void sg_tg_report_mask_density(const sg_tg_ctx *c) { + if (!c->enabled) return; + unsigned long masked = pn_sampler_grammar_mask_steps() - c->fl_mask_steps0; + unsigned long sparse = pn_sampler_grammar_sparse_rows() - c->fl_sparse_rows0; + fprintf(stderr, "[tool_grammar] turn mask density: masked_steps=%lu " + "sparse_rows=%lu shortlist=%lu chunks=%lu fullscan=%lu mask_ms=%.1f%s\n", masked, + sparse, pn_sampler_grammar_shortlist_steps() - c->fl_shortlist_steps0, + pn_sampler_grammar_chunk_steps() - c->fl_chunk_steps0, + pn_sampler_grammar_fullscan_steps() - c->fl_fullscan_steps0, + (pn_sampler_grammar_mask_seconds() - c->fl_mask_seconds0) * 1000.0, + sparse ? " <-- INVARIANT VIOLATED" : ""); +} + +/* The reverse coupling's one observable moment: the GRAMMAR decided reasoning + * was over while the emitter was still routing to , so the serve moves the + * channel under the model. + * + * WHY THIS LOGS. Everything after the flip is filed as ANSWER content, and the + * client cannot tell an answer the model wrote from a think block the serve + * re-labelled. When the flip is right — the grammar is compelling an opener and + * a tool call follows — the line is one entry beside the call. When it is wrong + * the line is the ONLY evidence: 37,721 chars of think-block prose reached a live + * transcript as three `answer` entries beginning mid-sentence, and nothing in + * either process said anything (qwen36-bf16, one recorded session, + * 2026-08-26). The `reasoning_leak` defect code does NOT cover this: it fires + * only when a parsed tool call's PARAMETER VALUE begins with "", and here + * there was no call and no tool region to parse. + * + * A walk that has gone DEAD no longer reports reasoning-ended at all + * (tg_forced_reasoning_ended), so the runaway case this line was written for is + * now left in the reasoning channel where it belongs — and the walk's own + * "went DEAD" line reports it. This stays for the flips that do happen. */ +static void sg_log_reasoning_channel_flip(const sg_tg_ctx *c, int generated) +{ + static unsigned long flips = 0; + fprintf(stderr, "[reasoning_channel] the grammar ended reasoning while the " + "emitter was still in — routing to CONTENT from token %d so " + "the compelled call is parsed (%s mode, flips=%lu)\n", + generated, c->forced ? "FORCED" : "AUTO", ++flips); +} + +/* Point the sampling params at the hook (no-op when disabled). */ +static void sg_tg_attach(sg_tg_ctx *c, pn_sampling_params *sp) { + sp->tool_grammar = c->enabled ? &c->hook : NULL; +} + +/* Batched variant: arm the driver-side callbacks so the hook advances, gates + * (self-refresh) and reports completion from WITHIN the sampler — the batched + * driver samples per-slot in its own thread and never touches the hook + * externally. Then point sp at the hook. (Single-user leaves these NULL and + * drives advance/refresh/completion from its own decode loop.) */ +static void sg_tg_attach_batched(sg_tg_ctx *c, pn_sampling_params *sp) { + switch (c->kind) { + case SG_GRAMMAR_TOOL_JSON: + c->hook.advance = sg_tg_advance_cb_json; + c->hook.refresh = sg_tg_refresh_cb_json; + c->hook.complete = sg_tg_complete_cb_json; + c->hook.in_call = sg_tg_in_call_cb_json; + break; + case SG_GRAMMAR_TOOL_XML: + c->hook.advance = sg_tg_advance_cb; + c->hook.refresh = sg_tg_refresh_cb; + c->hook.complete = sg_tg_complete_cb; + c->hook.in_call = sg_tg_in_call_cb; + /* Only the XML dialect gets a landing window (the JSON dialect's model + * runs reasoning_budget 0, so its close is never injected). */ + c->hook.reasoning_end_injected = sg_tg_reasoning_end_injected_cb; + break; + case SG_GRAMMAR_RESPONSE_JSON: + c->hook.advance = sg_tg_advance_cb_rf; + c->hook.refresh = sg_tg_refresh_cb_rf; + c->hook.complete = sg_tg_complete_cb_rf; + c->hook.in_call = sg_tg_in_call_cb_rf; + break; + case SG_GRAMMAR_NONE: + default: + break; + } + sp->tool_grammar = c->enabled ? &c->hook : NULL; +} + +/* Refresh the mask's active flag before each sampling step. AUTO: mask only + * inside a tool call (FREE region unconstrained → scan skipped). FORCED: also + * mask the post-reasoning FREE state so the opener is compelled. */ +static void sg_tg_refresh(sg_tg_ctx *c) { + if (!c->enabled) return; + c->hook.active = sg_tg_mask_active(c) ? 1 : 0; +} + +/* Has the grammar reached a COMPLETE tool call? Used to STOP generation the + * instant ""/"" closes, so a constrained decode + * terminates right after the call instead of running to max_tokens (the ~80 s + * "hang" on forced requests — the old loop had no completion-stop). */ +static bool sg_tg_done(const sg_tg_ctx *c) { + if (!c->enabled) return false; + if (c->kind == SG_GRAMMAR_TOOL_JSON) return tgj_complete(c->state_json); + if (c->kind == SG_GRAMMAR_RESPONSE_JSON) return rfg_complete(c->state_rf); + return tg_complete(c->state); +} + +/* Is the grammar constraint live for this request (forced or auto+tools)? A + * DEAD grammar (mask left the walk with no valid continuation) must NOT stop + * generation — only a genuine tg_complete does. */ +static bool sg_tg_live(const sg_tg_ctx *c) { return c->enabled; } + +/* True when a tool-call skeleton is currently being emitted under the grammar. + * The AUTO reasoning-budget force-close must NOT fire mid-skeleton: injecting + * "" then would mismatch the live grammar walk → TG_DEAD → the mask + * silently disengages and a forced request strands a half-call in reasoning. */ +static bool sg_tg_in_call(const sg_tg_ctx *c) { + if (!c->enabled) return false; + if (c->kind == SG_GRAMMAR_TOOL_JSON) return tgj_in_call(c->state_json); + if (c->kind == SG_GRAMMAR_RESPONSE_JSON) return rfg_in_json(c->state_rf); + return tg_in_call(c->state); +} + +/* Did this turn's tool walk leave the grammar (DEAD)? Only the tool grammars + * answer — the response-format grammar reports its own death separately. */ +static bool sg_tg_dead(const sg_tg_ctx *c) { + if (!c->enabled) return false; + if (c->kind == SG_GRAMMAR_TOOL_JSON) return tgj_is_dead(c->state_json); + if (c->kind == SG_GRAMMAR_TOOL_XML) return tg_is_dead(c->state); + return false; +} + +/* + * End-of-turn verdict for a `response_format` request: did the JSON constraint + * actually hold? + * + * Sets result->response_format_ok, and logs the grammar's final state so a turn + * that did NOT end in `done` says why on the fleet log — phase `reason` means the + * grace never ended (no "" was seen), `pre` means the object never + * opened, `dead` means the walk left the grammar, and advances == 0 means the + * sampler never fed the grammar at all. Those are indistinguishable from the + * response body alone, which is how a non-constraining build was able to keep + * reporting "applied". + */ +static void sg_tg_report_response_format(const sg_tg_ctx *c, sg_result *result) +{ + if (!c->enabled || c->kind != SG_GRAMMAR_RESPONSE_JSON) return; + bool ok = rfg_complete(c->state_rf); + result->response_format_ok = ok ? 1 : 0; + fprintf(stderr, "[response_format] turn end: enforced=%d phase=%s " + "advances=%d depth=%d reasoning_ended=%d died_in=%s on=\'%s\'\n", + ok ? 1 : 0, rfg_debug_phase_name(c->state_rf), + rfg_debug_advances(c->state_rf), rfg_debug_depth(c->state_rf), + rfg_reasoning_ended(c->state_rf) ? 1 : 0, + rfg_debug_death_phase(c->state_rf), + rfg_debug_death_piece(c->state_rf)); +} + +/* + * End-of-turn verdict for a TOOL-CALL request: did the call the model started + * ever close? The sibling of sg_tg_report_response_format above, and it exists + * for the same reason — the bit is computed inside the grammar and dies with it + * unless something carries it out. + * + * WHY THE HTTP LAYER CANNOT WORK THIS OUT ITSELF. A truncated call's text is a + * legal prefix of a good call, so the parser reads it as a complete call minus an + * argument and reports no error at all. Only the walk knows it was still inside + * the skeleton. Live: 9/9 truncated calls were accepted char-for-char by this + * grammar with tg_complete false, and shipped as `read {}` / `write {"path":…}`. + * + * WHY "entered a call" IS PART OF THE TEST. A walk still in FREE never started a + * call — the model answered in prose, or emitted a dialect this grammar does not + * model (MiniCPM's `` never matches the `tool_grammar_verdict = SG_TOOL_GRAMMAR_ABSENT; + if (!c->enabled || !sg_tg_is_tool(c)) return; + if (sg_tg_done(c)) { + result->tool_grammar_verdict = SG_TOOL_GRAMMAR_COMPLETE; + return; + } + /* THE SILENT SWITCH-OFF. A DEAD walk reports its mask INACTIVE + * (tg_mask_active), so from the step that killed it every remaining token of + * this turn was sampled with NO tool constraint — and until now that left the + * same trace as a turn that simply answered in prose, because both are + * "!in_call". Named here, with a running count, so "how often does the + * grammar switch itself off" is a question the fleet log can answer. + * Deliberately does NOT set INCOMPLETE: the text after the death was never + * constrained, so the grammar has no standing to judge it (see the note + * above), and refusing the call would turn an observability gap into a + * behaviour change. */ + if (sg_tg_dead(c)) { + static unsigned long dead_turns = 0; + fprintf(stderr, "[tool_grammar] turn end: the walk went DEAD in %s mode " + "— the mask reported inactive from that point and the rest of " + "this turn decoded UNCONSTRAINED (dead_turns=%lu)\n", + c->forced ? "FORCED" : "AUTO", ++dead_turns); + return; + } + if (!sg_tg_in_call(c)) return; /* never entered the skeleton */ + result->tool_grammar_verdict = SG_TOOL_GRAMMAR_INCOMPLETE; + fprintf(stderr, "[tool_grammar] turn end: the call never closed " + "(grammar still inside the skeleton) — refusing to emit it\n"); +} + +/* Per-step trace (PANTHEON_TOOL_GRAMMAR_DEBUG=1): dump the sampled piece and the + * grammar's live gating flags so the forced-decode can be watched on the K80 test machine. */ +static void sg_tg_trace(const sg_tg_ctx *c, int step, uint32_t tok, + const char *piece) { + if (!c->verbose) return; + char esc[64]; int j = 0; + const char *p = piece ? piece : ""; + for (int i = 0; p[i] && j < (int)sizeof(esc) - 4; i++) { + unsigned char ch = (unsigned char)p[i]; + if (ch == '\n') { esc[j++] = '\\'; esc[j++] = 'n'; } + else if (ch == '\t') { esc[j++] = '\\'; esc[j++] = 't'; } + else if (ch < 32 || ch > 126) { esc[j++] = '?'; } + else esc[j++] = (char)ch; + } + esc[j] = '\0'; + int in_call = sg_tg_in_call(c) ? 1 : 0; + int done = sg_tg_done(c) ? 1 : 0; + int rchars = (c->kind == SG_GRAMMAR_TOOL_JSON) + ? tgj_debug_reason_chars(c->state_json) + : (c->kind == SG_GRAMMAR_RESPONSE_JSON) + ? rfg_debug_depth(c->state_rf) + : tg_debug_reason_chars(c->state); + fprintf(stderr, "[tg] step=%d tok=%u piece='%s' mask=%d in_call=%d " + "complete=%d rchars=%d\n", step, tok, esc, c->hook.active, + in_call, done, rchars); +} + +/* Advance the grammar by the chosen token's decoded text (every step). */ +static void sg_tg_step(sg_tg_ctx *c, const char *piece) { + if (!c->enabled || !piece) return; + if (c->kind == SG_GRAMMAR_TOOL_JSON) tgj_advance(c->state_json, piece); + else if (c->kind == SG_GRAMMAR_RESPONSE_JSON) rfg_advance(c->state_rf, piece); + else tg_advance(c->state, piece); +} + +/* Secondary reasoning-end trigger (forced mode): the emitter's block + * closed. The grammar ALSO self-detects "" in its own stream (BUG 1), so + * this is belt-and-suspenders — whichever fires first compels the opener. Kept + * because it costs nothing and covers callers/streams the emitter splits cleanly. */ +static void sg_tg_reasoning_ended(sg_tg_ctx *c) { + if (!c->enabled) return; + if (c->kind == SG_GRAMMAR_TOOL_JSON) tgj_reasoning_end(c->state_json); + else if (c->kind == SG_GRAMMAR_RESPONSE_JSON) rfg_reasoning_end(c->state_rf); + else tg_reasoning_end(c->state); +} + +/* Reasoning-end trigger for an INJECTED close — the reasoning-budget force-close + * decoded "" itself rather than the model choosing it. Grants the XML + * grammar's landing window so the opener is not compelled around an unfinished + * thought (see tg_reasoning_end_injected). + * + * The JSON dialect (ornith) is deliberately NOT given a window here: it runs + * reasoning_budget 0, so the serve never injects a close for it and this path is + * unreachable — see the note in tool_grammar_json.h. It falls back to the plain + * reasoning-end so behavior is unchanged rather than silently different. */ +static void sg_tg_reasoning_ended_injected(sg_tg_ctx *c) { + if (!c->enabled) return; + if (c->kind == SG_GRAMMAR_TOOL_XML) tg_reasoning_end_injected(c->state); + else sg_tg_reasoning_ended(c); +} + + +/* Has the grammar itself decided forced-mode reasoning is over (saw "" OR + * the reasoning budget tripped)? The serve loop uses this to flip the emitter out + * of its channel so a compelled call routes to CONTENT and is parsed as a + * tool_call, even when the model never emitted a clean "". */ +static bool sg_tg_forced_ended(const sg_tg_ctx *c) { + if (!c->enabled) return false; + if (c->kind == SG_GRAMMAR_TOOL_JSON) return tgj_forced_reasoning_ended(c->state_json); + /* The response-format grammar has no "forced" mode: reasoning ending is what + * ARMS its constraint, and the emitter tracks the same "" itself, so + * there is nothing extra to tell it. Reporting true here would only flip the + * emitter's channel, which it has already done. */ + if (c->kind == SG_GRAMMAR_RESPONSE_JSON) return false; + return tg_forced_reasoning_ended(c->state); +} + +/* The model ended its turn INSIDE a "required" call — the mask let the end + * token through only because the call can still be closed (tgj_end_allowed / + * tg_end_allowed). Append the closing text through the grammar and the + * emitter, so the turn ends in the call the model meant: the same call the + * parser reads from the unclosed text, but checked by the grammar. Returns true + * when the call is now complete. The closing bytes are not forwarded through + * the model (the turn is over); a later turn re-prefills from the divergence. + * Both loops call it on an end token; on the batched path the driver has + * finished the slot by then, so the walk is this thread's alone. */ +static bool sg_tg_close_required_call(sg_tg_ctx *c, const sg_gen_params *params, emitter *em) +{ + static unsigned long closes = 0; + char closing[TG_CLOSING_MAX], shown[2 * TG_CLOSING_MAX]; + int n, j = 0; + bool json; + + if (!c->enabled || !sg_tg_is_tool(c) || params->tool_choice != SG_TOOL_CHOICE_REQUIRED) + return false; + json = (c->kind == SG_GRAMMAR_TOOL_JSON); + if (json ? (!tgj_in_call(c->state_json) || tgj_complete(c->state_json)) + : (!tg_in_call(c->state) || tg_complete(c->state))) + return false; + n = json ? tgj_closing_text(c->state_json, closing, (int)sizeof(closing)) + : tg_closing_text(c->state, closing, (int)sizeof(closing)); + if (n < 0) + return false; + if (json) tgj_advance(c->state_json, closing); + else tg_advance(c->state, closing); + em_feed(em, closing); + for (int i = 0; i < n && j < (int)sizeof(shown) - 3; i++) { + if (closing[i] == '\n') { shown[j++] = '\\'; shown[j++] = 'n'; } + else shown[j++] = closing[i]; + } + shown[j] = '\0'; + fprintf(stderr, "[tool_grammar] the model ended inside its call — closed it with " + "'%s' (closes=%lu)\n", shown, ++closes); + return json ? tgj_complete(c->state_json) : tg_complete(c->state); +} + +static void sg_tg_free(sg_tg_ctx *c) { + if (c->pieces) { + for (int i = 0; i < c->n_vocab; i++) free(c->pieces[i]); + free(c->pieces); + } + tg_state_free(c->state); + tg_free(c->grammar); + tgj_state_free(c->state_json); + tgj_free(c->grammar_json); + rfg_state_free(c->state_rf); + memset(c, 0, sizeof(*c)); +} + +/* ============================================================ + * Forward step — async when the model supports it (C2) + * + * One token's forward. When the engine exposes a non-blocking step for + * this model (qwen35moe on GPU), LAUNCH it and sleep-poll the completion + * event instead of blocking inside a CUDA stream sync: a blocked sync + * spins/yields a host core, and with up to 8 cards generating from a + * 2-core host the spinning waiters starve each other's launch turns so + * the cards serialize. Sleeping between event polls costs ~no CPU, so + * every connection thread's card stays in flight simultaneously — this + * per-connection thread IS its card's worker, and it never blocks the + * others. Poll cadence backs off 50us -> ~500us (worst-case added + * latency well under 1 ms/step, negligible against a K80 decode step); + * override the cap with PN_ASYNC_POLL_US. Falls back to the ordinary + * blocking model->forward whenever the async step is unsupported or the + * token is not eligible — bit-identical behavior. + * ============================================================ */ + +static tm_tensor *sg_forward_step(pn_model *model, pn_state *state, + uint32_t token) +{ + int rc = pn_forward_step_begin(model, token, state); + if (rc > 0) + return model->forward(model, token, state); + if (rc < 0) + return NULL; + + static long cap_us = -1; + if (cap_us < 0) { + const char *env = getenv("PN_ASYNC_POLL_US"); + cap_us = env ? atol(env) : 500; + if (cap_us < 50) cap_us = 50; + if (cap_us > 20000) cap_us = 20000; + } + + long us = 50; + for (;;) { + tm_tensor *out = NULL; + rc = pn_forward_step_poll(model, state, &out); + if (rc == 0) + return out; + if (rc < 0) + return NULL; + struct timespec ts = { 0, us * 1000L }; + nanosleep(&ts, NULL); + if (us < cap_us) { + us *= 2; + if (us > cap_us) us = cap_us; + } + } +} + +/* ============================================================ + * Prefill (L1 — batched, L2 — chunked) + * + * One chunk of prompt tokens through the fastest available path: the + * BATCHED forward (pn_qwen35moe_forward_batch_cuda — one pass for the whole + * chunk, ~2× the token-by-token loop; internally falls back to sequential + * single-token forwards when the batched path cannot run, so correctness + * never depends on it) when the model runs the qwen35moe CUDA forward, + * else the ordinary sg_forward_step loop. Bit-exact either way: the + * batched prefill's KV/SSM state and last-position logits are verified + * identical to n sequential single-token forwards (SpeedOptimization §2). + * + * QWEN35_BS_PREFILL_BATCH=0 restores the token-by-token loop (A/B lever). + * QWEN35_BS_PREFILL_CHUNK sets the chunk size in tokens (default 128; + * 0 = whole prompt in one pass). Chunking serves two masters: it bounds + * the batch scratch for long prompts, and in the batch-scheduled path each + * chunk boundary is a bs_prefill_yield window where the decode driver gets + * a step in (L2) — a joiner freezes concurrent decoders for one chunk, + * not one prompt. + * ============================================================ */ + +#ifdef USE_CUDA +static int sg_prefill_batch_enabled(void) +{ + static int v = -1; + if (v < 0) { + const char *e = getenv("QWEN35_BS_PREFILL_BATCH"); + v = (e && e[0] == '0') ? 0 : 1; + } + return v; +} +#endif + +static int sg_prefill_chunk(void) +{ + static int v = -2; + if (v == -2) { + const char *e = getenv("QWEN35_BS_PREFILL_CHUNK"); + v = e ? atoi(e) : 128; + if (v < 0) v = 0; + } + return v; +} + +/* ── Phase 9b: prefill chunk pipelining (QWEN35_PREFILL_PIPELINE, default OFF). + * + * Every chunk but the LAST of a range has its logits freed three lines later, + * and computing them is what forces the host to drain die B — which is what + * makes the two dies run strictly sequentially. Running the intermediate + * chunks through the no-head entry leaves die B's segment in flight so the + * next chunk's die-A segment overlaps it. + * + * This is a lever on ISSUE ORDER only: identical kernels, identical per-die + * order, identical inputs, no reassociation. Every early exit MUST drain + * first — an in-flight die-B segment is still writing this session's KV rings + * and SSM state, and the abort path resets exactly that. + * + * Only engaged for the qwen35moe CUDA batched prefill; any refusal from the + * model falls back to the ordinary chunk call, which is always correct. ── */ +static int sg_prefill_pipeline_ready(pn_model *model) +{ +#ifdef USE_CUDA + return pn_qwen35moe_prefill_pipeline_enabled() && + sg_prefill_batch_enabled() && + model && model->forward == pn_qwen35moe_forward_cuda; +#else + (void)model; + return 0; +#endif +} + +/* Run one intermediate (logits-discarded) chunk. 0 = the pipelined path took + * it and advanced state->pos; non-zero = nothing happened, run the ordinary + * chunk instead. */ +static int sg_prompt_chunk_nologits(pn_model *model, pn_state *state, + const uint32_t *toks, int n) +{ +#ifdef USE_CUDA + return pn_qwen35moe_prefill_chunk_cuda(model, toks, n, state); +#else + (void)model; (void)state; (void)toks; (void)n; + return -1; +#endif +} + +static void sg_prefill_pipeline_drain(pn_model *model) +{ +#ifdef USE_CUDA + pn_qwen35moe_prefill_drain_cuda(model); +#else + (void)model; +#endif +} + +/* ── Phase 9c: WIDE pipelining (QWEN35_PREFILL_PIPELINE_WIDE, default OFF). + * + * 9b's chunk loop lives here and hands the model ONE chunk at a time, which + * caps the overlap at two dies. A pipeline that keeps all n_die dies busy has + * to see the whole range at once, so this hands the range over wholesale and + * the model owns the chunking. The abort check the loop below does between + * chunks is preserved by passing it down: the model polls it once per pipeline + * step and drains what is in flight before reporting the abort. + * + * Refusal (-1) is always safe — nothing has been advanced and the ordinary + * chunked loop runs instead. ── */ +static int sg_prefill_wide_ready(pn_model *model) +{ +#ifdef USE_CUDA + return pn_qwen35moe_prefill_pipeline_wide_enabled() && + sg_prefill_batch_enabled() && + model && model->forward == pn_qwen35moe_forward_cuda; +#else + (void)model; + return 0; +#endif +} + +/* Adapter: the model layer polls abort through a plain int(*)(void*), which is + * what sg_should_abort_fn already is — but only when the caller supplied one. */ +typedef struct { + sg_should_abort_fn fn; + void *ud; +} sg_wide_abort; + +static int sg_wide_abort_poll(void *p) +{ + sg_wide_abort *a = (sg_wide_abort *)p; + return (a && a->fn) ? a->fn(a->ud) : 0; +} + +/* Returns 1 if the wide pipeline ran the range (out_logits set to the last + * position's logits, or NULL on abort), 0 if it refused and the caller must run + * the ordinary chunked loop. */ +static int sg_prefill_range_wide(pn_model *model, pn_state *state, + const uint32_t *prompt, int from, int to, + sg_should_abort_fn should_abort, void *ud, + tm_tensor **out_logits) +{ +#ifdef USE_CUDA + *out_logits = NULL; + if (!sg_prefill_wide_ready(model)) + return 0; + int chunk = sg_prefill_chunk(); + if (chunk <= 0) chunk = to - from; + sg_wide_abort abort_ctx = { should_abort, ud }; + int rc = pn_qwen35moe_prefill_range_pipelined_cuda( + model, prompt + from, to - from, state, chunk, + should_abort ? sg_wide_abort_poll : NULL, &abort_ctx, out_logits); + if (rc < 0) + return 0; /* refused — nothing advanced */ + if (rc == 1) { + fprintf(stderr, "[prefill_pipeline_wide] aborted and drained\n"); + return 1; /* *out_logits stays NULL */ + } + /* The sentinel a wide run - and only a wide run - prints. It is emitted + * from the branch that consumed the pipelined result, so a binary that + * ignored the flag, or a model that refused the entry, cannot print it. */ + fprintf(stderr, "[prefill_pipeline_wide] ON: %d tokens in %d-token chunks\n", + to - from, chunk); + return 1; +#else + (void)model; (void)state; (void)prompt; (void)from; (void)to; + (void)should_abort; (void)ud; + *out_logits = NULL; + return 0; +#endif +} + +/* ── Partial overlap: QWEN35_PREFILL_PIPELINE_DUTY, default 100 (= today). + * + * A K80 is two GK210 dies on ONE board sharing ONE 300 W budget. Running both + * at once — which is exactly what the pipeline above does — puts prefill at + * ~287 W of that 300 and the board answers by taking the clock away: the + * downstream die averages 584 MHz of its 875 MHz application clock through a + * 14k ingest. Throughput is roughly + * `dies_busy x clock`, and only the two extremes have ever been run: one die at + * ~875, or two dies at ~584. The middle is unmeasured, and if a duty cycle + * exists that holds the board just under its cap it could beat both. + * + * The duty is that knob. It is the percentage of a range's intermediate chunks + * whose die-B segment is left IN FLIGHT when the next chunk's die-A segment is + * issued — i.e. the fraction of chunk boundaries that overlap the two dies. + * 100 is the shipped pipeline (every boundary overlaps), 0 serialises every + * boundary. Boundaries are picked by a Bresenham accumulator so the overlapped + * ones are spread evenly through the range instead of bunched at the front, + * which is what makes the board's response a duty cycle rather than a ramp. + * + * A non-overlapped chunk still takes the no-head entry and is then drained + * explicitly, so the ONLY difference between duty points is issue order — the + * discarded LM head of an intermediate chunk is not computed at any duty. That + * keeps the knob a pure overlap knob; comparing duty=0 against + * QWEN35_PREFILL_PIPELINE=0 prices the head separately. + * + * DUTY_FILE is a bench-only escape hatch: the duty is re-read from that file at + * the start of every prefill range, so a sweep can change it under a serve that + * already holds 23 GB of weights instead of paying a reload per arm. Unset by + * default, and the file is only consulted when the pipeline is on. ── */ +typedef struct { + int percent; /* 0..100 — share of boundaries left overlapping */ + int accumulator; /* Bresenham state, in percent-units */ + int overlapped; /* boundaries left in flight */ + int serialised; /* boundaries drained on purpose */ +} sg_prefill_duty; + +static int sg_prefill_duty_percent(void) +{ + static int env_percent = -1; + static const char *duty_path = NULL; + if (env_percent < 0) { + const char *e = getenv("QWEN35_PREFILL_PIPELINE_DUTY"); + env_percent = e ? atoi(e) : 100; + if (env_percent < 0) env_percent = 0; + if (env_percent > 100) env_percent = 100; + duty_path = getenv("QWEN35_PREFILL_PIPELINE_DUTY_FILE"); + if (duty_path && !duty_path[0]) + duty_path = NULL; + } + if (!duty_path) + return env_percent; + FILE *f = fopen(duty_path, "r"); + if (!f) + return env_percent; + int from_file = -1; + if (fscanf(f, "%d", &from_file) != 1) + from_file = -1; + fclose(f); + if (from_file < 0) + return env_percent; + return from_file > 100 ? 100 : from_file; +} + +static void sg_prefill_duty_begin(sg_prefill_duty *duty, int pipeline_on) +{ + duty->percent = pipeline_on ? sg_prefill_duty_percent() : 100; + duty->accumulator = 0; + duty->overlapped = 0; + duty->serialised = 0; +} + +/* 1 = leave die B in flight across this chunk boundary, 0 = drain it first. */ +static int sg_prefill_duty_overlap(sg_prefill_duty *duty) +{ + duty->accumulator += duty->percent; + if (duty->accumulator >= 100) { + duty->accumulator -= 100; + duty->overlapped++; + return 1; + } + duty->serialised++; + return 0; +} + +/* PN_PREFILL_LOGITS_HASH=1 — the bit-exactness instrument for Phase 9b. + * + * FNV-1a over the raw bytes of the logits vector the prefill produced, i.e. + * over the exact float32 pattern the whole KV + SSM state funnelled into. A + * one-ULP divergence anywhere in the prefill changes it. Off by default and + * free when off; it is a diagnostic, not a code path. + * + * Also prints prefilled-token count + n_vocab so a hash can never be compared + * across two different amounts of work by accident. */ +static void sg_prefill_logits_hash(const tm_tensor *logits, int prefill_tokens, + int pos) +{ + static int on = -1; + if (on < 0) { + const char *e = getenv("PN_PREFILL_LOGITS_HASH"); + on = (e && e[0] == '1') ? 1 : 0; + } + if (!on || !logits) + return; + const unsigned char *p = (const unsigned char *)tm_data_f32((tm_tensor *)logits); + size_t n = (size_t)tm_numel((tm_tensor *)logits) * sizeof(float); + if (!p || n == 0) + return; + uint64_t h = 1469598103934665603ULL; + for (size_t i = 0; i < n; i++) { + h ^= (uint64_t)p[i]; + h *= 1099511628211ULL; + } + fprintf(stderr, "[prefill_hash] tokens=%d pos=%d nfloat=%zu fnv1a=%016llx\n", + prefill_tokens, pos, n / sizeof(float), (unsigned long long)h); +} + +/* The 9b sentinel. A gate that reads green because the flag was never picked + * up is this project's most repeated failure, so the pipelined path — and ONLY + * the pipelined path — prints proof of itself: the count is incremented inside + * the loop by a successful no-head chunk, so it cannot be printed by a binary + * that ignored QWEN35_PREFILL_PIPELINE or by a model that refused the entry. + * + * ⚠ What it does NOT prove: that die B was actually left IN FLIGHT. It counts + * the code path this loop chose, and QWEN35_PROF=1 drains die B inside every + * chunk regardless (see qw_warn_prof_serialises_pipeline in + * qwen35moe_forward_cuda.c), so a fully serialised profiled run still prints + * `duty=100% serialised=0`. Overlap is only observable from outside the + * process — both dies at high utilisation in the same 200 ms sample. */ +static void sg_prefill_pipeline_report(const sg_prefill_duty *duty, int n_tok) +{ + if (duty->overlapped <= 0 && duty->serialised <= 0) + return; + fprintf(stderr, "[prefill_pipeline] ON: %d of the range's chunks ran " + "no-head + die-B-in-flight (%d tokens) duty=%d%% " + "serialised=%d\n", + duty->overlapped, n_tok, duty->percent, duty->serialised); +} + +static tm_tensor *sg_prompt_chunk(pn_model *model, pn_state *state, + const uint32_t *toks, int n) +{ +#ifdef USE_CUDA + /* Bonsai (PN_ARCH_BONSAI) selects pn_bonsai_forward_cuda, not this pointer, + * so it skips batched prefill and runs the per-token loop below (dense-FFN + * has no batched q2_0 kernel yet) — byte-identical to sequential forwards. + * The shipped Nex-N2 MoE path (pn_qwen35moe_forward_cuda) is unchanged. */ + if (n > 1 && sg_prefill_batch_enabled() && + model->forward == pn_qwen35moe_forward_cuda) + return pn_qwen35moe_forward_batch_cuda(model, toks, n, state, NULL); +#endif +#ifdef USE_TT + /* The Tenstorrent route's batched prefill. A tile is 32 rows and a matvec is Mt=1, so + * the card already multiplies 32 rows against every weight tile it reads and 31 of them + * are zero padding — this fills them with prompt positions at no card cost. It REFUSES + * (returns NULL without touching state) for on-card attention, the diagnostic arms and + * a prompt that would wrap the ring, and the loop below is what runs then. */ + if (n > 1 && model->forward == pn_llama_forward_tt) { + tm_tensor *batched = pn_llama_prefill_tt(model, toks, NULL, n, state); + + if (batched) + return batched; + } + /* AND ORNITH'S, which is a DIFFERENT function because it is a different trade — both of + * its mixers run whole on the card and are single-position there, so its batch reaches + * each row through the link and its win comes from the chunkwise GDN mixers. + * + * IT WAS MISSING AND THAT IS WHY THE SERVE NEVER SAW THE 24x. `pn_qwen35moe_prefill_tt` + * shipped on by default on 2026-08-25 and this file only ever asked the llama family, + * so every Ornith prefill the fleet ran went through the per-token loop below — no + * error, no refusal line, just ~20x the wall. */ + if (n > 1 && model->forward == pn_qwen35moe_forward_tt) { + tm_tensor *batched = pn_qwen35moe_prefill_tt(model, toks, n, state); + + if (batched) + return batched; + } +#endif + tm_tensor *logits = NULL; + for (int i = 0; i < n; i++) { + if (logits) tm_free(logits); + logits = sg_forward_step(model, state, toks[i]); + if (!logits) + return NULL; + } + return logits; +} + +/* Prefill prompt[from, to) through the chunked batched path, returning the + * LAST position's logits (owned; caller frees), or NULL on failure. Chunk + * boundaries never change the result (the batched prefill is bit-exact vs + * sequential forwards), so splitting a prefill at the system boundary (SYS1) + * is bit-faithful to one uninterrupted pass. Returns NULL when from >= to. */ +static tm_tensor *sg_prefill_range(pn_model *model, pn_state *state, + const uint32_t *prompt, int from, int to, + sg_should_abort_fn should_abort, void *ud) +{ + tm_tensor *logits = NULL; + /* 9c first: it owns the whole range when it takes it, and refuses cleanly + * (nothing advanced) when it cannot, leaving the 9b loop below untouched. */ + if (sg_prefill_range_wide(model, state, prompt, from, to, should_abort, ud, + &logits)) + return logits; + int chunk = sg_prefill_chunk(); + if (chunk <= 0) chunk = to - from; +#ifdef USE_TT + /* The Tenstorrent qwen35 batched prefill starts at position 0 or at a restored checkpoint + * and declines any other continuation: a prompt goes in ONE range, or every chunk after the + * first ran per token (1,630 tokens: 78 s = 20.8 tok/s against the batched ~400). */ + if (model->forward == pn_qwen35moe_forward_tt) + chunk = to - from; +#endif + int pipeline = sg_prefill_pipeline_ready(model); + sg_prefill_duty duty; + sg_prefill_duty_begin(&duty, pipeline); + for (int off = from; off < to; off += chunk) { + /* Bail between chunks the instant the client disconnects — a cold + * prefill is ~100s on a big model, and finishing it just wedges the + * single slot for a response nobody will read. Returns NULL; the caller + * classifies abort vs error via the same should_abort. */ + if (should_abort && should_abort(ud)) { + if (pipeline) sg_prefill_pipeline_drain(model); + if (logits) tm_free(logits); + return NULL; + } + int m = to - off < chunk ? to - off : chunk; + if (logits) tm_free(logits); + logits = NULL; + /* 9b: intermediate chunks skip the head and leave die B in flight, + * except on the share of boundaries the duty serialises (drain). */ + if (pipeline && m > 1 && off + m < to && + sg_prompt_chunk_nologits(model, state, prompt + off, m) == 0) { + if (!sg_prefill_duty_overlap(&duty)) + sg_prefill_pipeline_drain(model); + continue; + } + logits = sg_prompt_chunk(model, state, prompt + off, m); + if (!logits) { + if (pipeline) sg_prefill_pipeline_drain(model); + return NULL; + } + } + /* The last chunk always runs the ordinary path, which syncs die B, so the + * pipeline is drained on the success return by construction. */ + sg_prefill_pipeline_report(&duty, to - from); + return logits; +} + +/* Batch-scheduled twin of sg_prefill_range: prefill prompt[from, to) in + * chunks, yielding the GPU turn to the decode driver BETWEEN chunks (L2) but + * never after the last one — the caller's next action (a boundary capture or + * the slot bind) must directly follow the final token under the same turn. + * Stage boundaries (SYS1's split at sys_len) therefore behave exactly like + * chunk boundaries without a yield, which is bit-faithful to one + * uninterrupted pass. Returns the LAST position's logits (owned; caller + * frees), or NULL on failure / when from >= to. */ +static tm_tensor *sg_prefill_range_sched(pn_model *model, + pn_batch_sched *sched, int slot, + pn_state *state, + const uint32_t *prompt, + int from, int to, + sg_should_abort_fn should_abort, + void *ud) +{ + tm_tensor *logits = NULL; + /* 9c owns the whole range when it takes it, and therefore skips the L2 + * yield windows the loop below opens between chunks. That is only + * acceptable when this slot is the ONLY active one — with anyone else on + * the model, a long range would freeze their decode for its whole + * duration, which is exactly what L2 exists to prevent. */ + bs_load load = {0}; + if (sched) + bs_get_load(sched, &load); + if (load.active_slots <= 1 && + sg_prefill_range_wide(model, state, prompt, from, to, should_abort, ud, + &logits)) + return logits; + int chunk = sg_prefill_chunk(); + if (chunk <= 0) chunk = to - from; + int pipeline = sg_prefill_pipeline_ready(model); + sg_prefill_duty duty; + sg_prefill_duty_begin(&duty, pipeline); + for (int off = from; off < to; off += chunk) { + /* Bail between chunks the instant the client disconnects — the same + * check the single-user twin has had, and it matters MORE here: this is + * the path the fleet runs, so a cold prefill nobody will read (~205 s at + * 8k) pins one of the few decode slots for its whole duration while + * every other user shares what is left. One chunk is ~4-5 s, so an + * abandoned request costs seconds instead of minutes. Returns NULL; the + * caller classifies abort vs error via the same should_abort. */ + if (should_abort && should_abort(ud)) { + if (pipeline) sg_prefill_pipeline_drain(model); + if (logits) tm_free(logits); + return NULL; + } + int m = to - off < chunk ? to - off : chunk; + if (logits) tm_free(logits); + logits = NULL; + /* 9b: intermediate chunks skip the head and leave die B in flight. + * bs_prefill_yield below is safe on top of that — it only hands the + * GPU over after pn_qwen35moe_gpu_state_to_host, which walks both dies + * with fc_sync_ctx and therefore drains the pipeline itself. It also + * only fires when another slot is actually runnable, so the + * single-session case this optimises never pays for it. */ + if (pipeline && m > 1 && off + m < to) { + if (sg_prompt_chunk_nologits(model, state, prompt + off, m) == 0) { + if (!sg_prefill_duty_overlap(&duty)) + sg_prefill_pipeline_drain(model); + bs_prefill_yield(sched, slot); + continue; + } + } + logits = sg_prompt_chunk(model, state, prompt + off, m); + if (!logits) { + if (pipeline) sg_prefill_pipeline_drain(model); + return NULL; + } + if (off + m < to) + bs_prefill_yield(sched, slot); /* L2: let decode interleave */ + } + sg_prefill_pipeline_report(&duty, to - from); + return logits; +} + +/* Fill the repeat-penalty window with the prompt tail — identical result to + * the historical per-token append loop (the window ends holding the last + * max_recent prompt tokens, in order). */ +static int sg_seed_recent(uint32_t *recent, int max_recent, + const uint32_t *prompt, int n_prompt) +{ + if (max_recent <= 0) + return 0; + int take = n_prompt < max_recent ? n_prompt : max_recent; + memcpy(recent, prompt + (n_prompt - take), + (size_t)take * sizeof(uint32_t)); + return take; +} + +/* ============================================================ + * Completion driver + * ============================================================ */ + +#ifdef USE_TT +/* --------------------------------------------------------------------------- + * MTP speculative decode on the single-user completion (Tenstorrent qwen35). + * + * The loop's shape is kept: every token is emitted by the same code; only the + * forward that follows an emitted token changes. After the seed (the token just + * emitted, not yet in the state) the verify commits the seed + the accepted drafts + * and returns them + the model's next token; the committed ones are then emitted + * with NO forward, the last (uncommitted) one becomes the next seed. Lossless only + * when the request's pick IS the plain argmax (temperature 0, no bias, penalties, + * grammar or logprobs). Drafts are cut so a cycle can never commit past a stop + * token, the token budget or the reasoning budget's trip: the state then stays + * level with the emitted history except on mid-cycle stops (stop strings, repeat + * loop, abort), where the committed extras are recorded in the history commit. + * ------------------------------------------------------------------------- */ +typedef struct sg_spec { + int k; /* drafts a cycle; 0 = off */ + sg_spec_lookup lookup; /* prompt-lookup drafts, tried ahead of the MTP head */ + uint32_t emit[16]; /* accepted drafts, then the model's next token */ + int n, at; /* emit[at..n-2] committed, emit[n-1] the next seed */ + bool sampling; /* SPECULATIVE SAMPLING: rows drawn by pn_spec_sample_choose */ + pn_spec_sample_ctx choose; + /* UNDER THE TOOL GRAMMAR (tool_choice "required", grammar_spec_chooser.h): + * drafts are cut where the grammar refuses them and every verify row is + * chosen through gsc_choose, so the output is plain constrained decoding's. */ + bool grammar; + pn_sampling_params plain; /* the request's params without the grammar hook */ + pn_sampling_params *loop; /* the decode loop's params: `plain` borrows its seeded + * RNG state for a cycle and hands it back after */ + gsc_chooser gsc; + void *gsc_walk; /* the chooser's scratch walk (a clone of the live one) */ + const sg_tg_ctx *tg; + pn_model *model; + float *dense; /* n_vocab: the row a masked pick samples */ +} sg_spec; + +/* The grammar walk behind gsc_grammar_ops, per dialect. */ +static void sg_gs_copy_json(void *d, const void *s) { tgj_state_copy(d, s); } +static bool sg_gs_allowed_json(const void *st, const char *t) { return tgj_token_allowed(st, t); } +static void sg_gs_advance_json(void *st, const char *t) { tgj_advance(st, t); } +static bool sg_gs_mask_json(const void *st) { return tgj_mask_active(st); } +static bool sg_gs_complete_json(const void *st) { return tgj_complete(st); } +static void sg_gs_copy_xml(void *d, const void *s) { tg_state_copy(d, s); } +static bool sg_gs_allowed_xml(const void *st, const char *t) { return tg_token_allowed(st, t); } +static void sg_gs_advance_xml(void *st, const char *t) { tg_advance(st, t); } +static bool sg_gs_mask_xml(const void *st) { return tg_mask_active(st); } +static bool sg_gs_complete_xml(const void *st) { return tg_complete(st); } +static const gsc_grammar_ops SG_GS_OPS_JSON = { + sg_gs_copy_json, sg_gs_allowed_json, sg_gs_advance_json, sg_gs_mask_json, sg_gs_complete_json, +}; +static const gsc_grammar_ops SG_GS_OPS_XML = { + sg_gs_copy_xml, sg_gs_allowed_xml, sg_gs_advance_xml, sg_gs_mask_xml, sg_gs_complete_xml, +}; +/* RED `gspec_ignore_mask` (PN_TT_RED): the chooser sees the mask as never + * active, so speculative output is unconstrained while plain decode is + * constrained — the spec-on vs spec-off comparison MUST then differ. */ +static bool sg_gs_mask_never(const void *st) { (void)st; return false; } +static const gsc_grammar_ops SG_GS_OPS_JSON_RED = { + sg_gs_copy_json, sg_gs_allowed_json, sg_gs_advance_json, sg_gs_mask_never, sg_gs_complete_json, +}; +static const gsc_grammar_ops SG_GS_OPS_XML_RED = { + sg_gs_copy_xml, sg_gs_allowed_xml, sg_gs_advance_xml, sg_gs_mask_never, sg_gs_complete_xml, +}; + +/* A verify row the mask does not touch: the plain speculative-sampling draw. */ +static uint32_t sg_gs_base_sample(void *ctx, int row, const uint32_t *index, const float *value, + uint32_t count) +{ + pn_spec_sample_ctx *choose = (pn_spec_sample_ctx *)ctx; + + choose->row_index = row; + return pn_spec_sample_choose(choose, index, value, count); +} + +/* pn_sampling_params.dense_row for a row that is ALREADY dense: nothing to + * fetch. Setting it lets the sampler decide the mask from a shortlist + * (grammar_mask_shortlist.h), which it does only for rows a backend can refill. */ +static int sg_gs_row_already_dense(void *ctx, float *logits, int n_vocab) +{ + (void)ctx; (void)logits; (void)n_vocab; + return 0; +} + +/* Push a draw onto the cycle's repeat window, as pn_spec_sample_choose does. */ +static void sg_gs_push_recent(pn_spec_sample_ctx *choose, uint32_t token) +{ + if (choose->max_recent <= 0) return; + if (choose->n_recent < choose->max_recent) { + choose->recent[choose->n_recent++] = token; + } else { + memmove(choose->recent, choose->recent + 1, + (size_t)(choose->max_recent - 1) * sizeof(uint32_t)); + choose->recent[choose->max_recent - 1] = token; + } +} + +/* A verify row under an active mask: the token plain constrained decoding draws + * at this position, or GSC_DEFER (rows > 0 only). The row is handed over as + * plain decode's sampler receives it — the top-N pairs, -inf elsewhere. + * row 0 : sample_token itself, with the real dense callbacks — row 0 of the + * verify IS the card's logits row 0, so the shortlist / chunk fetch / + * dense refill it falls through are the plain step's own. + * row > 0: only the shortlist decision (the card's logits buffer holds row 0, + * not this row); decided → the same draw on the same rewritten row, + * else defer — the next cycle meets this position as its row 0. */ +static uint32_t sg_gs_masked_pick(void *ctx, void *walk, int row, const uint32_t *index, + const float *value, uint32_t count) +{ + sg_spec *sp = (sg_spec *)ctx; + const int n_vocab = sp->model->config.n_vocab; + const uint32_t neg_inf_bits = 0xff800000u; + pn_tool_grammar_hook hook = sp->tg->hook; + pn_sampling_params params = sp->plain; + pn_spec_sample_ctx *choose = &sp->choose; + uint32_t i, token; + float neg_inf; + + hook.st = walk; + hook.active = 1; + hook.refresh = NULL; + hook.advance = NULL; + params.tool_grammar = &hook; + memcpy(&neg_inf, &neg_inf_bits, sizeof(neg_inf)); + for (i = 0; i < (uint32_t)n_vocab; i++) sp->dense[i] = neg_inf; + for (i = 0; i < count; i++) + if (index[i] < (uint32_t)n_vocab) sp->dense[index[i]] = value[i]; + if (row > 0) { + params.dense_row = sg_gs_row_already_dense; /* never row 0's logits here */ + params.dense_chunks = NULL; + params.dense_ctx = NULL; + if (!pn_sampler_grammar_shortlist(sp->dense, n_vocab, ¶ms, choose->recent, + choose->n_recent)) + return GSC_DEFER; + params.tool_grammar = NULL; + } else { + params.dense_row = pn_qwen35moe_tt_dense_row; + params.dense_chunks = pn_qwen35moe_tt_dense_chunks; + params.dense_ctx = sp->model; + } + token = pn_sample_next_token(sp->dense, n_vocab, ¶ms, choose->recent, choose->n_recent); + sp->plain.rng_state = params.rng_state; + sg_gs_push_recent(choose, token); + return token; +} + +/* Arm speculation under the tool grammar: the chooser, its scratch walk and the + * dense-row buffer. Returns false (spec stays off) when any piece is missing. */ +static bool sg_spec_init_grammar(sg_spec *sp, pn_model *model, const sg_tg_ctx *tg, + int max_recent) +{ + const bool json = tg->kind == SG_GRAMMAR_TOOL_JSON; + + if (!pn_qwen35moe_tt_spec_can_sample(model)) + return false; + sp->gsc.ops = json ? &SG_GS_OPS_JSON : &SG_GS_OPS_XML; /* also tells sg_spec_free the walk's type */ + sp->gsc_walk = json ? (void *)tgj_state_clone(tg->state_json) : (void *)tg_state_clone(tg->state); + sp->dense = malloc((size_t)model->config.n_vocab * sizeof(float)); + if (!sp->choose.recent) { + sp->choose.recent = malloc((size_t)(max_recent > 0 ? max_recent : 1) * sizeof(uint32_t)); + sp->choose.max_recent = max_recent; + } + if (!sp->gsc_walk || !sp->dense || !sp->choose.recent) + return false; + sp->grammar = true; + sp->tg = tg; + sp->model = model; + if (getenv("PN_TT_RED") && strcmp(getenv("PN_TT_RED"), "gspec_ignore_mask") == 0) { + fprintf(stderr, "serve: RED gspec_ignore_mask — drafts are NOT checked against the grammar\n"); + sp->gsc.ops = json ? &SG_GS_OPS_JSON_RED : &SG_GS_OPS_XML_RED; + } + sp->gsc.live = json ? (const void *)tg->state_json : (const void *)tg->state; + sp->gsc.walk = sp->gsc_walk; + sp->gsc.text = sg_tg_token_text; + sp->gsc.text_ctx = (void *)tg; + sp->gsc.base = sp->sampling ? sg_gs_base_sample : NULL; + sp->gsc.base_ctx = &sp->choose; + sp->gsc.masked = sg_gs_masked_pick; + sp->gsc.masked_ctx = sp; + return true; +} + +static void sg_spec_free(sg_spec *sp) +{ + sg_spec_lookup_free(&sp->lookup); + free(sp->choose.row); + free(sp->choose.recent); + sp->choose.row = NULL; + sp->choose.recent = NULL; + if (sp->gsc_walk) { + if (sp->gsc.ops != &SG_GS_OPS_XML && sp->gsc.ops != &SG_GS_OPS_XML_RED) + tgj_state_free(sp->gsc_walk); + else tg_state_free(sp->gsc_walk); + } + sp->gsc_walk = NULL; + free(sp->dense); + sp->dense = NULL; +} + +/* One line per constrained turn that speculated: how the grammar shaped it. */ +static void sg_spec_report_grammar(const sg_spec *sp) +{ + if (!sp->grammar) return; + fprintf(stderr, "[tool_grammar] speculation under the grammar: rows_masked=%lu " + "deferred=%lu drafts_cut=%lu\n", sp->gsc.rows_masked, sp->gsc.rows_deferred, + sp->gsc.drafts_cut); +} + +/* Greedy speculation when the request's pick IS the argmax; speculative SAMPLING when the + * rows are top-N (pn_qwen35moe_tt_select_rows chose them) and nothing needs the dense row. */ +static void sg_spec_init(sg_spec *sp, pn_model *model, pn_sampling_params *samp, + bool needs_full_logits, int max_recent, const uint32_t *prompt, + int n_prompt, int max_tokens, const sg_tg_ctx *tg, + bool grammar_speculates) +{ + const char *env = getenv("PN_TT_SPEC"); + const bool grammar = grammar_speculates && samp->tool_grammar; + + memset(sp, 0, sizeof(*sp)); + if (model->forward != pn_qwen35moe_forward_tt || needs_full_logits || + (getenv("PN_TT_SERVE_SPEC") && atoi(getenv("PN_TT_SERVE_SPEC")) == 0)) + return; + /* Under the grammar the rows are judged by the chooser, so exactness is + * asked of the request's params WITHOUT the hook. */ + sp->plain = *samp; + sp->loop = samp; + if (grammar) + sp->plain.tool_grammar = NULL; + if (!pn_qwen35moe_tt_greedy_exact_on_topn(&sp->plain)) { + if (sp->plain.temperature <= 0.0f || sp->plain.tool_grammar || + !pn_qwen35moe_tt_spec_can_sample(model) || + (getenv("PN_TT_SPEC_SAMPLE") && atoi(getenv("PN_TT_SPEC_SAMPLE")) == 0)) + return; + sp->choose.params = grammar ? &sp->plain : samp; + sp->choose.n_vocab = model->config.n_vocab; + sp->choose.max_recent = max_recent; + sp->choose.row = malloc((size_t)model->config.n_vocab * sizeof(float)); + sp->choose.recent = malloc((size_t)(max_recent > 0 ? max_recent : 1) * sizeof(uint32_t)); + if (!sp->choose.row || !sp->choose.recent) { + free(sp->choose.row); + free(sp->choose.recent); + memset(sp, 0, sizeof(*sp)); + return; + } + sp->sampling = true; + } + if (grammar && !sg_spec_init_grammar(sp, model, tg, max_recent)) { + fprintf(stderr, "serve: speculation under the tool grammar unavailable; plain decode\n"); + sg_spec_free(sp); + memset(sp, 0, sizeof(*sp)); + return; + } + sp->k = env ? atoi(env) : 3; + if (sp->k > 15) sp->k = 15; + if (sp->k < 0) sp->k = 0; + if (sp->k > 0 && sg_spec_lookup_init(&sp->lookup, prompt, n_prompt, max_tokens) != 0) + fprintf(stderr, "serve: prompt-lookup drafts off (no memory for the history)\n"); +} + + +/* One cycle with `seed` as the next token. `cap` bounds the drafts (token / reasoning + * budget). @return true when the verify committed the seed (no plain forward owed). */ +static bool sg_spec_cycle(sg_spec *sp, pn_model *model, pn_state *state, uint32_t seed, + int cap, int eos_id, int im_end, const uint32_t *recent, + int n_recent) +{ + uint32_t drafts[16], looked[16]; + int nd, nl, i, ne; + + if (sp->k <= 0 || cap <= 0) + return false; + /* Prompt lookup: when the context's suffix recurs, its continuation replaces the MTP + * head's drafts — in mode 2 only when both agree on the first draft. The head always + * drafts first (one streamed segment, as without lookup): a cycle the lookup does not + * take costs exactly what it costs without it. */ + nl = sg_spec_lookup_draft(&sp->lookup, cap, looked); + nd = pn_qwen35moe_tt_mtp_draft(model, state, seed, sp->k < cap ? sp->k : cap, drafts); + if (nl > 0 && nd >= 1 && sg_spec_lookup_agrees(&sp->lookup, looked[0], drafts[0])) { + memcpy(drafts, looked, (size_t)nl * sizeof(*drafts)); + nd = nl; + if (getenv("PN_TT_RED") && strcmp(getenv("PN_TT_RED"), "spec_lookup_unverified") == 0) + pn_qwen35moe_tt_red_accept_next_cycle(); + } else { + nl = 0; + } + for (i = 0; i < nd; i++) + if ((int)drafts[i] == eos_id || (int)drafts[i] == im_end) + break; + nd = i; + if (sp->grammar) + nd = gsc_truncate_drafts(&sp->gsc, drafts, nd); + if (nd < 1) + return false; + if (sp->grammar) { + memcpy(sp->choose.recent, recent, (size_t)n_recent * sizeof(uint32_t)); + sp->choose.n_recent = n_recent; + sp->choose.row_index = 0; + gsc_begin_cycle(&sp->gsc); + /* One RNG stream: the cycle's draws continue the loop's (OpenAI `seed`). */ + sp->plain.rng_state = sp->loop->rng_state; + ne = pn_qwen35moe_tt_spec_step_ex(model, state, seed, drafts, nd, sp->emit, gsc_choose, + &sp->gsc); + sp->loop->rng_state = sp->plain.rng_state; + } else if (sp->sampling) { + memcpy(sp->choose.recent, recent, (size_t)n_recent * sizeof(uint32_t)); + sp->choose.n_recent = n_recent; + sp->choose.row_index = 0; + ne = pn_qwen35moe_tt_spec_step_ex(model, state, seed, drafts, nd, sp->emit, + pn_spec_sample_choose, &sp->choose); + } else { + ne = pn_qwen35moe_tt_spec_step(model, state, seed, drafts, nd, sp->emit); + } + if (ne < 1) { + fprintf(stderr, "serve: speculative step failed; plain decode from here\n"); + sp->k = 0; + return false; + } + if (nl > 0) + sg_spec_lookup_report(&sp->lookup, nd, ne - 1); + sp->n = ne; + sp->at = 0; + return true; +} +#endif + +int sg_run_completion(pn_model *model, pn_state *state, + const sg_message *messages, int n_messages, + const sg_gen_params *params, + sg_delta_fn delta_fn, void *delta_ud, + sg_result *result, const sg_prefix_ctx *pc) +{ + if (!model || !messages || !params || !result) return -1; + memset(result, 0, sizeof(*result)); + + /* PC2: caching is active only when a cache + session id were supplied. */ + bool use_cache = pc && pc->cache && pc->sid && pc->sid[0]; + if (!use_cache && !state) return -1; /* legacy path needs a state */ + + int n_prompt = 0; + int sys_len = 0; + pn_reqprof_phase_start(PN_REQ_TOKENIZE); + int think_prefill_len = 0; + uint32_t *prompt = sg_build_prompt_tokens(model, messages, n_messages, + params->thinking, &n_prompt, + &sys_len, &think_prefill_len); + pn_reqprof_phase_end(PN_REQ_TOKENIZE); + if (!prompt || n_prompt == 0) { free(prompt); return -1; } + + /* Pre-flight admission (MemoryAware §6.2): compute this prompt's projected + * host + die cost and refuse/cap BEFORE the prefill loop below — the gate + * that would have prevented the 32k full-attention OOM-reboot. Engine-level + * pn_admit reads the live pn_mem_state; fail-open when memwatch is off. */ + int fit_tokens = n_prompt; + pn_admit_status admit = pn_admit(model, n_prompt, &fit_tokens); + int ctx_cap = pn_session_ctx_cap(model); + /* The CLIENT gets the effective window, never the configured cap: kv_cap is + * what the client re-plans its budget against mid-session, so it has to be the + * number admission will actually enforce on the next turn — the same one + * /v1/models advertised. `ctx_cap` below stays the configured value purely + * so the operator's log line can show both. */ + result->kv_cap_tokens = pn_effective_ctx_window(model); + if (admit == PN_ADMIT_DENIED) { + free(prompt); + return SG_ADMIT_DENIED; + } + /* KV Phase 1: over the per-session context budget. The old behavior silently + * dropped the oldest tokens — which broke the prefix-cache match past the + * system prefix, forced a full re-prefill every turn, and stalled the + * client. Instead REJECT with a distinct, machine-readable signal so the + * client compacts its history and retries. Never allocate past budget, + * never OOM, never drop-oldest. */ + if (admit == PN_ADMIT_CAPPED && fit_tokens > 0 && fit_tokens < n_prompt) { + result->kv_used_tokens = n_prompt; + /* Report the EFFECTIVE cap that actually refused this prompt, not the + * configured one (Phase 10 — see the batched twin of this branch). */ + fprintf(stderr, "[admit] REJECT context_over_budget: prompt %d tokens > " + "effective cap %d (configured %d) — refusing (client must " + "compact + retry)\n", + n_prompt, fit_tokens, ctx_cap > 0 ? ctx_cap : fit_tokens); + free(prompt); + return SG_CONTEXT_OVER_BUDGET; + } + + result->prompt_tokens = n_prompt; + result->kv_used_tokens = n_prompt; + + /* PC2: acquire the session's cached state and the delta boundary. On any + * failure fall back to the passed-in `state` + full prefill (never regress + * the working serve). `delta_start` is the first prompt position that + * still needs prefilling (== n_prompt means the whole prompt was cached). + * SYS1: the acquire also gets the system-prefix boundary — a session whose + * history diverged (reasoning turns) but whose system prefix matches its + * cached boundary snapshot resumes at delta_start == sys_len; capture_sys + * asks this turn's full prefill to snapshot the boundary state. */ + int delta_start = 0; + int capture_sys = 0; + pn_reqprof_phase_start(PN_REQ_CACHE_LOOKUP); + if (use_cache) { + if (pc->pc_lock) pthread_mutex_lock(pc->pc_lock); + pn_state *cs = pn_prefix_cache_acquire_sys(pc->cache, pc->sid, prompt, + n_prompt, sys_len, + &delta_start, &capture_sys); + if (cs) { + state = cs; /* use the session's state; ignore the passed-in one */ + } else { + if (pc->pc_lock) pthread_mutex_unlock(pc->pc_lock); + use_cache = false; + delta_start = 0; + capture_sys = 0; + } + } + if (!use_cache) { + pn_state_reset(state); + delta_start = 0; + } + + pn_sampling_params sp = pn_sampling_defaults(); + sp.temperature = params->temperature; + sp.top_p = params->top_p; + sp.top_k = params->top_k; + sg_apply_openai_sampling(&sp, params); +#ifdef USE_TT + /* Greedy requests take the card's exact top-N row unless a grammar, logprobs or logit + * surgery needs the dense one — decided BEFORE the prompt (its last row is token 1). */ + if (pn_qwen35moe_tt_select_rows(model, &sp, sg_may_need_full_logits(params)) > 0u) { + sp.dense_row = pn_qwen35moe_tt_dense_row; + sp.dense_chunks = pn_qwen35moe_tt_dense_chunks; + sp.dense_ctx = model; + } +#endif + + int max_recent = sp.repeat_last_n; + uint32_t *recent = calloc((size_t)(max_recent > 0 ? max_recent : 1), + sizeof(uint32_t)); + if (!recent) { /* OOM: bail before sg_seed_recent memcpy's into NULL */ + if (use_cache && pc->pc_lock) pthread_mutex_unlock(pc->pc_lock); + free(prompt); + return -1; + } + int n_recent = 0; + + /* Track this turn's generated tokens so PC2 can commit the exact committed + * history (prompt + generated) back to the session. +8 headroom: the + * reasoning-budget force-close injects up to sizeof(close_toks)=8 "" + * tokens beyond the sampled ones, so `generated` can reach max_tokens+n_close + * while the main loop's unguarded gen_toks[generated] write still runs. */ + int max_tokens = params->max_tokens > 0 ? params->max_tokens : 512; + uint32_t *gen_toks = use_cache + ? malloc((size_t)(max_tokens + 8) * sizeof(uint32_t)) : NULL; + if (use_cache && !gen_toks) { + if (pc->pc_lock) pthread_mutex_unlock(pc->pc_lock); + free(prompt); free(recent); + return -1; + } + + /* Prefill (L1): run the DELTA (tokens [delta_start, n_prompt)) through the + * batched forward in chunks, keeping the last logits to sample the first + * generated token from. With caching off delta_start==0 (full prefill). + * + * A real chat turn ALWAYS appends new tokens (at minimum the assistant + * opener), so delta_start < n_prompt in practice. The degenerate full-hit + * case (new prompt == prior committed history exactly) would leave nothing + * to prefill and no way to recover the next-token logits without advancing + * pos past n_prompt — so fall back to a clean full reset+prefill for it + * (correctness over the microscopic caching win). */ + if (use_cache && delta_start >= n_prompt) { + pn_state_reset(state); + delta_start = 0; + } + pn_reqprof_phase_end(PN_REQ_CACHE_LOOKUP); + pn_reqprof_note_cache(use_cache, n_prompt, delta_start, sys_len); + /* Report the reuse on the wire, not just in the serve log: read AFTER the + * degenerate-full-hit fallback above, so what the client is told is the + * boundary this turn actually prefills from. */ + result->cached_prompt_tokens = pn_prefix_cached_tokens(delta_start, n_prompt); + tm_tensor *logits = NULL; + int prefill_tokens = n_prompt - delta_start; /* Phase 7 rate counter */ + pn_reqprof_phase_start(PN_REQ_PREFILL); + double prefill_t0 = sg_prefill_now_ms(); + bool tt_one_pass = false; +#ifdef USE_TT + /* SYS1's split would start the conversation at pos sys_len, which the TT batched + * prefill declines (per token from there): one pass, no system snapshot, until the + * TT prefill can continue from a carried state (plan: multi-turn state caching). */ + tt_one_pass = model->forward == pn_qwen35moe_forward_tt; + /* Checkpoint no higher than the history the next turn resends: the generation prompt + * after the assistant header ("\n") is not in it, and a checkpoint covering it + * cannot be resumed (the whole conversation re-prefills). */ + if (tt_one_pass && use_cache) + pn_qwen35moe_tt_checkpoint_ceiling(model, state, n_prompt - think_prefill_len); +#endif + if (use_cache && capture_sys && !tt_one_pass && delta_start == 0 && sys_len < n_prompt) { + /* SYS1 two-stage prefill: run the system prefix, snapshot the boundary + * state for next-turn reuse, then run the conversation. A capture + * failure is benign (logged inside; this turn just stays uncached). */ + logits = sg_prefill_range(model, state, prompt, 0, sys_len, + params->should_abort, params->abort_ud); + if (logits) { + if (pn_prefix_cache_capture_sys(pc->cache, pc->sid, prompt, + sys_len) != 0) + fprintf(stderr, "[prefix_cache] sys-prefix capture failed " + "sid=%s (continuing uncached)\n", pc->sid); + tm_free(logits); + logits = sg_prefill_range(model, state, prompt, sys_len, n_prompt, + params->should_abort, params->abort_ud); + } + } else { + logits = sg_prefill_range(model, state, prompt, delta_start, n_prompt, + params->should_abort, params->abort_ud); + } + if (logits) + sg_prefill_rate_record(prefill_tokens, + sg_prefill_now_ms() - prefill_t0); + pn_reqprof_phase_end(PN_REQ_PREFILL); + sg_prefill_logits_hash(logits, prefill_tokens, state->pos); + pn_reqprof_phase_start(PN_REQ_DECODE); + if (!logits) { + /* Distinguish a client-disconnect abort from a real prefill error: the + * check is idempotent (a closed socket stays closed). On abort, reset the + * session so the partial, uncommitted prefill can't corrupt next turn's + * append-resume. The session-watch owns slot release when the client's + * watch dies past grace; this path only cleans up the partial prefill. + * Then return a clean SG_ABORTED. */ + int aborted = params->should_abort && params->should_abort(params->abort_ud); + if (use_cache) { + if (aborted) + pn_prefix_cache_reset_session(pc->cache, pc->sid); + if (pc->pc_lock) pthread_mutex_unlock(pc->pc_lock); + } + free(prompt); free(recent); free(gen_toks); + return aborted ? SG_ABORTED : -1; + } + n_recent = sg_seed_recent(recent, max_recent, prompt, n_prompt); + + emitter em; + em_init(&em, delta_fn, delta_ud, params->stop, params->n_stop, + params->has_tools, params->dialect); + em_set_tool_tap(&em, params->tool_tap, params->tool_tap_ud); + /* The emitter starts inside a think block only when the prompt left one OPEN: + * a think-style template with thinking on. No-Think prefills the block CLOSED, + * and a non-think template (plain ChatML, Mistral, Phi-3) never opens one, so + * the model never emits — start in CONTENT mode, else all output is + * misrouted to reasoning_content and the answer/tool-call channel stays empty. */ + if (sg_thinking_suppressed(params->thinking) || + !sg_chat_style_is_think(model->chat_style)) + em.in_think = 0; + + int eos_id = ft_eos_id(model->tokenizer); + int im_end = ft_token_to_id(model->tokenizer, "<|im_end|>"); + int n_vocab = model->config.n_vocab; + + /* Phase 5: grammar-constrained tool decoding (sg_tool_grammar_arms). Inert + * otherwise → sp.tool_grammar stays NULL and the sampler is byte-identical. */ + sg_tg_ctx tg; sg_tg_setup(&tg, model, params, n_vocab); + sg_tg_attach(&tg, &sp); + tg.fl_always = sg_full_logits_always(params); + sg_tg_mark_mask_density(&tg); + /* Window OFF: arm the whole turn, exactly here, exactly as before. + * Window ON: sg_arm_full_logits owns the flag from here on and is called + * immediately before every forward this loop runs. */ + if (!sg_grammar_arm_window_on()) + sg_require_full_logits(&tg, params, model, state, 1); + else + sg_arm_full_logits(&tg, params, model, state); + + /* OpenAI `logprobs` (inert unless the request asked). Sized like gen_toks: + * max_tokens plus the reasoning force-close headroom. */ + sg_logprob_acc lpacc; + sg_logprobs_init(&lpacc, result, params, max_tokens + 8); + sg_logprobs_attach(&lpacc, &sp); + + /* AUTO-path reasoning budget (independent of the tool-forcing grammar): count + * tokens generated while inside ; once over budget without a self-emitted + * "", force-close reasoning so the model answers. Disabled when + * budget <= 0 (byte-identical to today) or the model is not think-style. */ + int reason_budget = (params->reasoning_budget > 0 && + sg_chat_style_is_think(model->chat_style)) + ? params->reasoning_budget : 0; + int reason_tokens = 0; /* tokens routed to the reasoning channel so far */ + bool reason_forced = false; /* force-close already applied this turn */ + bool reason_over_budget = false; /* latched: budget tripped this iteration + * (set off the pre-feed think state so a + * same-token close can't starve it) */ + + /* Repetition-loop guard: fed EVERY generated token id of this turn, in + * order, reasoning and answer alike — a model looping inside is just + * as stuck as one looping in the answer, and the reasoning budget only + * bounds the think block's LENGTH, not whether it is saying anything new. */ + rld_detector reploop; + sg_reploop_arm(&reploop); + + const char *finish = "length"; + int generated = 0; +#ifdef USE_TT + sg_spec spec; + const bool spec_under_grammar = sg_tool_grammar_speculates(params) && tg.enabled && + sg_tg_is_tool(&tg); + sg_spec_init(&spec, model, &sp, + (spec_under_grammar ? sg_full_logits_always(params) + : sg_needs_full_logits(&tg, params)) || params->logprobs, + max_recent, prompt, n_prompt, max_tokens, &tg, spec_under_grammar); +#endif + for (int i = 0; i < max_tokens; i++) { +#ifdef USE_TT + if (!logits && spec.at >= spec.n) break; +#else + if (!logits) break; +#endif + /* Stop decoding if the client disconnected (checked every 8 tokens to + * bound the syscall). Tokens generated so far are valid — the commit + * below keeps them, and finish="stop" ends the turn cleanly. */ + if ((i & 7) == 0 && params->should_abort && + params->should_abort(params->abort_ud)) { + finish = "stop"; + break; + } + sg_tg_refresh(&tg); /* mask only inside a tool call; FREE unconstrained */ + uint32_t next; +#ifdef USE_TT + if (spec.at < spec.n) { + next = spec.emit[spec.at++]; + } else +#endif + { + next = pn_sample_next_token((float *)logits->data, n_vocab, + &sp, recent, n_recent); + tm_free(logits); + logits = NULL; + } + + if ((int)next == eos_id || (int)next == im_end) { + finish = sg_tg_close_required_call(&tg, params, &em) ? "tool_calls" : "stop"; + break; + } + + char *piece = decode_token(model, next); + /* Resolve this token's logprobs BEFORE feeding the emitter, so the entry + * can ride the content delta that carries its bytes. A token generated + * inside is REASONING, not answer content, so its record is + * consumed but NOT reported — that is what keeps this path's reported set + * identical to the streaming path's, which only ever sees content deltas. + * Every consumed token takes or skips exactly one record, so the sequence + * stays paired with the token stream. */ + const sg_token_logprob *lp = NULL; + if (em.in_think) sg_logprobs_skip(&lpacc); + else lp = sg_logprobs_take(&lpacc, result, model, piece); + if (lp) em_note_token_logprob(&em, lp); + if (piece) { + sg_tg_step(&tg, piece); /* advance the grammar by the chosen text */ + sg_tg_trace(&tg, i, next, piece); + int was_think = em.in_think; + em_feed(&em, piece); + /* A token generated while inside counts against the reasoning + * budget (whether or not this particular token closed the block). + * Latch the budget trip HERE, off the pre-feed think state (was_think): + * the model can reach the budget on the very token that also closes + * , at which point em.in_think is already 0 by the force-close + * check below and the (em.in_think && …) guard would starve the trip — + * so the reasoning cap silently never fired and the model ran on to + * max_tokens (the observed saber/ornith runaway). Arm the trip while + * we still know we WERE reasoning; the force-close below then closes + * cleanly whether or not this token already ended the block. */ + if (was_think) { + reason_tokens++; + if (reason_budget && !reason_forced && + reason_tokens >= reason_budget && !sg_tg_in_call(&tg)) { + reason_over_budget = true; + /* Arming-window latch. The force-close block below runs + * AFTER the forward that fetches the row the next step + * samples, and it ends in sg_tg_reasoning_ended() — the one + * grammar mutation that can land too late to be read. + * Latch here, while we are still on the near side of that + * forward, so the whole close window is armed. */ + tg.fl_pending = true; + } + } + /* Forced mode: when the emitter's block closes ("" + * consumed), reasoning is over → the grammar now REQUIRES the opener + * on the content channel, compelling the call. */ + if (was_think && !em.in_think) sg_tg_reasoning_ended(&tg); + /* Reverse coupling: when the GRAMMAR has decided reasoning is over + * (self-detected "" or the budget tripped) but the emitter is + * still routing to , force it to CONTENT — otherwise the + * grammar-compelled call lands in reasoning_content and is never parsed + * as a tool_call (the empty-prompt degeneration: budget forces a valid + * call, but it was mis-routed to the reasoning channel). */ + if (em.in_think && sg_tg_forced_ended(&tg)) { + em.in_think = 0; + sg_log_reasoning_channel_flip(&tg, generated); + } + free(piece); + } + if (gen_toks) gen_toks[generated] = next; + generated++; + (void)rld_feed(&reploop, next); +#ifdef USE_TT + sg_spec_lookup_push(&spec.lookup, next); +#endif + + if (max_recent > 0) { + if (n_recent < max_recent) { + recent[n_recent++] = next; + } else { + memmove(recent, recent + 1, (size_t)(max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next; + } + } + /* Forward `next` into the state BEFORE the stop/tool_calls breaks below. + * `next` is real output (already streamed + counted in `generated`), so it + * must be stepped into the KV/SSM before we break — otherwise state->pos + * falls one behind the committed history and next turn pays a full + * re-prefill on arches without lcp-rewind (qwen35moe). `logits` then holds + * the post-`next` distribution for the next iteration (or the force-close). + * + * The grammar has already advanced over `next` (sg_tg_step above), so + * arming here reads exactly the walk state the NEXT step's sampler will + * refresh from — the whole point of the window. */ + sg_arm_full_logits(&tg, params, model, state); +#ifdef USE_TT + if (spec.at < spec.n) { + /* `next` was an accepted draft: the verify already committed it. */ + } else { + /* Drafts this cycle may emit: they and the next token must fit the token + * budget, and must not reach the reasoning budget's trip (the trip token + * takes a plain forward: the force-close needs its logits). */ + int cap = max_tokens - i - 2; + if (reason_budget && !reason_forced && em.in_think && + reason_budget - reason_tokens - 1 < cap) + cap = reason_budget - reason_tokens - 1; + if (reason_over_budget || + !sg_spec_cycle(&spec, model, state, next, cap, eos_id, im_end, recent, + n_recent)) + logits = sg_forward_step(model, state, next); + } +#else + logits = sg_forward_step(model, state, next); +#endif + + if (em.stopped) { finish = "stop"; break; } + + /* Repetition collapse: the model has re-emitted the same block of tokens + * back to back, byte-identical, with nothing novel in between. Checked + * HERE, after sg_forward_step, so `next` is committed to the KV/SSM like + * every other generated token and state->pos stays level with the + * committed history — the same rule the stop/tool_calls breaks follow. + * Read off the LATCH, not off this token's rld_feed return, so a trip + * that lands on a force-close injection below is still acted on (one + * token later) instead of being swallowed by the latch. */ + if (reploop.tripped) { finish = SG_FINISH_REPETITION_LOOP; break; } + + /* Grammar completion-stop: once a whole tool call has been emitted + * ("" / "" consumed) the constrained request is + * DONE — stop immediately instead of running to max_tokens. Without + * this a forced request generated 1024 tokens (~80 s) past the call and + * looked like a hang. Only fires for a live constrained grammar; auto + * requests that never engaged a call are unaffected. */ + if (sg_tg_live(&tg) && sg_tg_done(&tg)) { + /* A TOOL grammar closing means a call was emitted; the + * response-format grammar closing just means the JSON object is + * finished, which is an ordinary "stop". */ + finish = sg_tg_is_tool(&tg) ? "tool_calls" : "stop"; + break; + } + + /* AUTO reasoning budget: the model has spent its whole reasoning grace + * inside without concluding. Force-close it so it concludes and + * answers. `reason_over_budget` was latched above off the PRE-feed think + * state, so the trip survives even when this same token also closed the + * block (the starvation bug: gating on the post-feed em.in_think here let + * a budget reached on the closing token slip through, and the model ran on + * to max_tokens). Force-decode the "" tokens through the state + * (KV/SSM advances over the close) AND the emitter (its own THINK_CLOSE + * match flips it to the content channel), accounting each like a generated + * token; `logits` ends holding the post-close next-token distribution, so + * the next loop iteration samples the answer's first token. Fires at most + * once per turn. If the emitter already left on the trip token + * (self-close), the injection loop below is a no-op and we only stamp the + * grammar reasoning-ended below. */ + if (reason_over_budget && !reason_forced && logits) { + reason_over_budget = false; + reason_forced = true; + /* Only force-decode the close when the block is still OPEN. When the + * trip token also self-closed (em.in_think already 0), the + * reasoning region ended on its own — injecting another "" + * would double-close (a stray marker in the KV state + content), so + * skip straight to stamping the grammar reasoning-ended below. */ + if (em.in_think) { + uint32_t close_toks[8]; + int n_close = sg_encode_think_close(model, close_toks, + (int)(sizeof(close_toks) / + sizeof(close_toks[0]))); + /* Bound the injection by the gen_toks buffer (max_tokens + its +8 + * headroom), NOT by max_tokens: a multi-token fallback close firing + * near the cap must complete, not truncate mid-sequence (which would + * leave a half-closed think block in the KV state). */ + int inj_cap = max_tokens + (int)(sizeof(close_toks) / + sizeof(close_toks[0])); + for (int c = 0; c < n_close && generated < inj_cap && !em.stopped; + c++) { + char *cp = decode_token(model, close_toks[c]); + if (cp) { + sg_tg_step(&tg, cp); + em_feed(&em, cp); + free(cp); + } + if (gen_toks) gen_toks[generated] = close_toks[c]; + generated++; +#ifdef USE_TT + sg_spec_lookup_push(&spec.lookup, close_toks[c]); +#endif + /* Keep the detector's view of the stream faithful — these + * ids really are in the committed history. A trip here is + * not acted on mid-injection (that would leave a half-closed + * think block in the KV state); the loop's latch check picks + * it up on the next iteration. */ + (void)rld_feed(&reploop, close_toks[c]); + sg_recent_push(recent, max_recent, &n_recent, close_toks[c]); + tm_free(logits); + /* fl_pending is latched across this whole block, so these + * forwards are armed whatever the walk currently says. */ + sg_arm_full_logits(&tg, params, model, state); + logits = sg_forward_step(model, state, close_toks[c]); + if (!logits) break; + } + /* If the emitter never saw a clean "" (n_close==0, or a + * tokenizer whose close piece doesn't decode to the literal), feed + * the LITERAL through the emitter's own THINK_CLOSE matcher so any + * held carry resolves into the right channel — poke the flag only as + * a last resort. Without this, a later model-emitted "" + * leaks verbatim into content. */ + if (em.in_think) { + if (n_close == 0) + fprintf(stderr, "[reason_budget] no token for this " + "model's tokenizer — forcing a literal emitter " + "close\n"); + em_feed(&em, ""); + if (em.in_think) em.in_think = 0; + } + } + /* Symmetry with the self-close path above (line ~933): tell the grammar + * reasoning ended so a live forced walk compels the opener on content. + * + * INJECTED variant: this close was force-decoded by the budget, not + * chosen by the model, so the grammar gets a bounded landing window + * rather than compelling the opener from the very next char around an + * unfinished thought. The self-close path above deliberately keeps + * the plain sg_tg_reasoning_ended — a model that concluded on its own + * needs no grace, and its behavior must stay byte-identical. */ + sg_tg_reasoning_ended_injected(&tg); + /* The late mutation has landed. Every forward taken while the latch + * was up was armed, so the row the next step samples is dense, and + * the walk is now authoritative again — drop the latch and let the + * window narrow. */ + tg.fl_pending = false; + } + } + if (logits) tm_free(logits); + free(recent); + + if (reploop.tripped) + sg_reploop_log(&reploop, params, "single-user", generated); + rld_free(&reploop); + + /* OBSERVABILITY — a reasoning force-close is an ABNORMAL event, so it logs + * unconditionally (not behind --verbose; the only prior trace of any of this + * was the [tg] step dump gated on c->verbose). It is the one condition under + * which the grammar compels the tool-call opener while the model is still + * mid-thought, which is how a half-written call ends up inside a parameter + * VALUE (tool_calling.c FRAME_MARKERS). The BATCHED twin lives in + * bs_slot_release; the fleet runs batched, so both must be present or the + * production rate is invisible. */ + if (reason_forced) + fprintf(stderr, "[reason_budget] FORCED close model=%s path=single-user " + "reason_toks=%d/%d generated=%d tool_call=%s\n", + (params->model_name && params->model_name[0]) + ? params->model_name : "?", + reason_tokens, reason_budget, generated, + !sg_tg_live(&tg) ? "n/a" : (sg_tg_done(&tg) ? "yes" : "no")); + + /* PC2: commit the session's committed history (prompt + generated) so next turn + * deltas from it. Every generated token is forwarded into `state` before any + * break — sg_forward_step(next) now precedes the stop/tool_calls checks, and + * eos/im_end is excluded from BOTH the count and the forward — so + * state->pos == n_prompt + generated exactly on every exit, and next turn's + * append-resume hits with no re-prefill (matters on arches without lcp-rewind, + * e.g. qwen35moe). */ + if (use_cache) { + /* Speculation: a mid-cycle stop leaves accepted drafts COMMITTED but not + * emitted (emit[at..n-2]); the history names what the state really holds, + * so next turn's LCP sees the divergence instead of trusting a stale state. */ + int extra = 0; +#ifdef USE_TT + extra = spec.n - 1 - spec.at; + if (extra < 0) extra = 0; +#endif + int hist_n = n_prompt + generated + extra; + uint32_t *hist = malloc((size_t)hist_n * sizeof(uint32_t)); + if (hist) { + memcpy(hist, prompt, (size_t)n_prompt * sizeof(uint32_t)); + if (generated > 0) + memcpy(hist + n_prompt, gen_toks, + (size_t)generated * sizeof(uint32_t)); +#ifdef USE_TT + if (extra > 0) + memcpy(hist + n_prompt + generated, spec.emit + spec.at, + (size_t)extra * sizeof(uint32_t)); +#endif + pn_prefix_cache_commit(pc->cache, pc->sid, hist, hist_n); + free(hist); + } + if (pc->pc_lock) pthread_mutex_unlock(pc->pc_lock); + } +#ifdef USE_TT + sg_spec_report_grammar(&spec); + sg_spec_free(&spec); +#endif + free(gen_toks); + free(prompt); + sg_tg_report_response_format(&tg, result); + sg_tg_report_tool_call(&tg, result); + sg_tg_report_mask_density(&tg); + sg_require_full_logits(&tg, params, model, state, 0); + sg_tg_free(&tg); + /* Frees only the raw record buffer — the RESOLVED entries live on the result + * and are freed by sg_result_free. */ + sg_logprobs_free(&lpacc); + + em_flush(&em); + + pn_reqprof_phase_end(PN_REQ_DECODE); + pn_reqprof_note_generated(generated); + result->completion_tokens = generated; + result->finish_reason = finish; + result->content = em.content.data ? em.content.data : strdup(""); + result->reasoning = em.reasoning.data; /* may be NULL */ + result->tool_raw = em.tool.data; /* may be NULL (no tool call) */ + /* Transfer ownership out of the emitter, then free its remaining buffers. */ + em.content.data = NULL; + em.reasoning.data = NULL; + em.tool.data = NULL; + em_free(&em); + return 0; +} + +void sg_result_free(sg_result *result) +{ + if (!result) return; + free(result->content); + free(result->reasoning); + free(result->tool_raw); + for (int i = 0; i < result->n_logprobs; i++) { + free(result->logprobs[i].token); + for (int j = 0; j < result->logprobs[i].n_top; j++) + free(result->logprobs[i].top_token[j]); + } + free(result->logprobs); + result->content = NULL; + result->reasoning = NULL; + result->tool_raw = NULL; + result->logprobs = NULL; + result->n_logprobs = 0; +} + +/* ============================================================ + * C3 — batched completion (sg_run_completion_batched) + * + * Prefill is single-user but BATCHED (L1: whole chunks through + * pn_qwen35moe_forward_batch_cuda) and CHUNKED (L2: the GPU turn is yielded + * to the decode driver between chunks, so a join stalls concurrent decoders + * for one chunk, not one prompt). Decode is batched: the scheduler's + * driver calls decode_slots with all active slots in one GPU pass and (L3, + * default) samples every slot in-driver, handing this thread finished + * tokens through a per-slot ring — detokenize/emitter/SSE run entirely off + * the step's critical path. QWEN35_BS_DRIVER_SAMPLE=0 restores the legacy + * lockstep logits/token exchange. + * ============================================================ */ + +int sg_run_completion_batched(pn_model *model, pn_batch_sched *sched, + int slot, + const sg_message *messages, int n_messages, + const sg_gen_params *params, + sg_delta_fn delta_fn, void *delta_ud, + sg_result *result, const char *sid) +{ + if (!model || !sched || slot < 0 || !messages || !params || !result) + return -1; + memset(result, 0, sizeof(*result)); + + /* PC-batched: caching is active only with a session id. NULL/empty sid + * → full-prefill on every turn, byte-identical to the pre-PC batched + * path (safe-by-default). */ + int use_cache = (sid && sid[0]); + + /* Slot was pre-acquired by the caller (bs_slot_acquire). The slot's + * pn_state is reset inside bs_prefill_begin below (under the GPU-owner + * lock — it may still be in an in-flight driver step as a dummy lane + * right now); until then we only take the pointer. */ + pn_state *state = sched->slots[slot].state; + + /* ── 2. Build prompt ── (with the SYS1 system-prefix boundary: sys_len + * leading tokens cover the first message's system turn, stable across a + * session's turns — the batched per-slot snapshot point.) */ + int n_prompt = 0; + int sys_len = 0; + int think_prefill_len = 0; + pn_reqprof_phase_start(PN_REQ_TOKENIZE); + uint32_t *prompt = sg_build_prompt_tokens(model, messages, n_messages, + params->thinking, &n_prompt, + &sys_len, &think_prefill_len); + pn_reqprof_phase_end(PN_REQ_TOKENIZE); + if (!prompt || n_prompt == 0) { + free(prompt); + bs_slot_release(sched, slot); + return -1; + } + /* The acquire could not see the prompt; now that it is known, run on the + * slot whose cache it continues (one session id, several conversations). */ + if (use_cache) { + slot = bs_slot_reroute(sched, slot, sid, prompt, n_prompt, sys_len); + state = sched->slots[slot].state; + } + + /* ── 3. Admission (same logic as sg_run_completion) ── */ + int fit_tokens = n_prompt; + pn_admit_status admit = pn_admit(model, n_prompt, &fit_tokens); + int ctx_cap = pn_session_ctx_cap(model); + /* Effective, not configured — see the single-user twin in sg_run_completion. + * The BATCHED path is the one the live fleet runs, so a gap here is the gap + * that ships. */ + result->kv_cap_tokens = pn_effective_ctx_window(model); + if (admit == PN_ADMIT_DENIED) { + free(prompt); + bs_slot_release(sched, slot); + /* D3: propagate the pressure signal so queued waiters back off. + * Read the live level and push it into the scheduler. */ + if (pn_memwatch_running()) { + pn_mem_state ms; + pn_memwatch_read(&ms); + bs_notify_pressure(sched, (int)ms.level); + } + return SG_ADMIT_DENIED; + } + /* KV Phase 1: over the per-session context budget — REJECT with a distinct + * signal instead of the old silent drop-oldest that shattered the prefix + * cache and stalled the client. The client compacts + retries. */ + if (admit == PN_ADMIT_CAPPED && fit_tokens > 0 && fit_tokens < n_prompt) { + result->kv_used_tokens = n_prompt; + /* Report the EFFECTIVE cap that actually refused this prompt, not the + * configured one. Printing ctx_cap alone produced self-contradictory + * lines ("prompt 70000 > session cap 131072") whenever the memory + * ladder had clamped the window underneath it — Phase 10. */ + fprintf(stderr, "[batch_sched] slot %d: REJECT context_over_budget: " + "prompt %d tokens > effective cap %d (configured %d) — " + "refusing (compact + retry)\n", + slot, n_prompt, fit_tokens, ctx_cap > 0 ? ctx_cap : fit_tokens); + free(prompt); + bs_slot_release(sched, slot); + return SG_CONTEXT_OVER_BUDGET; + } + result->prompt_tokens = n_prompt; + result->kv_used_tokens = n_prompt; + + /* ── 4. Sampling params ── */ + pn_sampling_params sp = pn_sampling_defaults(); + sp.temperature = params->temperature; + sp.top_p = params->top_p; + sp.top_k = params->top_k; + sg_apply_openai_sampling(&sp, params); + + /* Phase 5: grammar-constrained tool decoding on the BATCHED path + * (sg_tool_grammar_arms). The driver samples per-slot in + * its own thread from the slot's copied sampling params, so the hook carries + * the driver-side advance/refresh/complete callbacks (sg_tg_attach_batched) + * — it advances, gates and reports completion FROM the sampler. The ctx + * (grammar + state + piece table + hook) must OUTLIVE the whole decode: the + * driver dereferences sl->sp.tool_grammar (== &tg.hook) every step until the + * slot is released. It is freed at `out` after bs_slot_release. */ + sg_tg_ctx tg; sg_tg_setup(&tg, model, params, model->config.n_vocab); + sg_tg_attach_batched(&tg, &sp); + tg.fl_always = sg_full_logits_always(params); + sg_tg_mark_mask_density(&tg); + /* The arming window needs a per-step re-arm to stay correct, and only the + * L3 in-driver sampler has one (driver_arm_full_logits). The legacy + * lockstep exchange samples on THIS thread with no such seam, so it keeps + * the whole-turn arm — the window is simply not offered there. */ + const bool arm_window = sg_grammar_arm_window_on() && + bs_driver_sampling(sched); + /* The slot's state owns the logits buffer the driver samples from. This + * arms the PREFILL row, whose logits sample the turn's FIRST token. */ + if (!arm_window) + sg_require_full_logits(&tg, params, model, state, 1); + else + sg_arm_full_logits(&tg, params, model, state); + + /* OpenAI `logprobs` (inert unless the request asked). The sink pointer is + * copied into the slot's sampling params by bs_slot_bind, so the DRIVER + * records every step it samples; this thread resolves the text as it drains + * the ring. The accumulator must outlive the decode — it is freed at `out`, + * after the slot is released. */ + sg_logprob_acc lpacc; + { + int lp_cap = (params->max_tokens > 0 ? params->max_tokens : 512) + 8; + sg_logprobs_init(&lpacc, result, params, lp_cap); + sg_logprobs_attach(&lpacc, &sp); + } + + int max_recent = sp.repeat_last_n; + uint32_t *recent = calloc((size_t)(max_recent > 0 ? max_recent : 1), + sizeof(uint32_t)); + int n_recent = 0; + + /* ── 5. Prefill (L1: batched, L2: chunked with yield windows). + * bs_prefill_begin takes the model's GPU turn: the C2 async phase + * machine, the shared per-die scratch and the live SSM/KV GPU state all + * support exactly ONE in-flight generation per model, so concurrent + * prefill chunks must be serialized — and the turn is held through the + * slot bind so no other forward can run between the last prefill token + * and the SSM snapshot (the bind-right-after-prefill protocol). + * Between chunks bs_prefill_yield hands the GPU to the decode driver + * for one step (making the host mirror authoritative first), bounding + * concurrent decoders' stall to one chunk instead of one prompt. Any + * failure before the bind must exit via bs_prefill_abort (releases the + * turn and the slot). ── */ + /* PC-batched: resume-aware begin. With a session id, if this slot's warm + * pn_state still holds this session's committed history as a STRICT prefix + * of `prompt` (at least one appended token), delta_start is set past it so + * only the delta is prefilled. SYS1-batched: a divergent history whose + * system prefix matches this slot's boundary snapshot resumes at + * delta_start == sys_len instead; capture_sys asks this turn's full + * prefill to snapshot the boundary. Otherwise the slot resets and + * delta_start == 0 (full prefill). Both resume cases guarantee + * delta_start < n_prompt, so the prefill below always runs >=1 token and + * recovers the next-token logits. */ + int delta_start = 0; + int capture_sys = 0; + pn_reqprof_phase_start(PN_REQ_CACHE_LOOKUP); + if (use_cache) + bs_prefill_resume_sys(sched, slot, sid, prompt, n_prompt, sys_len, + &delta_start, &capture_sys); + else + bs_prefill_begin(sched, slot); + pn_reqprof_phase_end(PN_REQ_CACHE_LOOKUP); + pn_reqprof_note_cache(use_cache, n_prompt, delta_start, sys_len); + /* Same wire report as the single-user twin. bs_prefill_resume_sys funnels + * ALL four batched rungs (append, think-elided append, sys-prefix restore, + * full reset) through this one delta_start, so the batched path reports the + * number honestly rather than guessing at it — and this is the path the + * fleet runs, so a gap here is the gap that ships. */ + result->cached_prompt_tokens = pn_prefix_cached_tokens(delta_start, n_prompt); + /* Report a session id that now carries a SECOND conversation: every rung + * missed because the system block itself changed, so this turn and the + * displaced conversation's next turn both re-prefill in full. */ + result->session_conflict = (use_cache && bs_slot_session_conflict(sched, slot)) + ? 1 : 0; + + tm_tensor *logits = NULL; + pn_reqprof_phase_start(PN_REQ_PREFILL); + if (capture_sys && delta_start == 0 && sys_len < n_prompt) { + /* SYS1 two-stage prefill: run the system prefix, snapshot the boundary + * for next-turn reuse (still owning the GPU turn — the range helper + * never yields after its last chunk, so the state sits exactly at + * pos == sys_len), then run the conversation. A capture failure is + * benign: this turn just stays uncached. Splitting at the boundary is + * bit-faithful to one uninterrupted pass (the batched prefill is + * bit-exact vs sequential forwards at any chunking). */ + logits = sg_prefill_range_sched(model, sched, slot, state, prompt, + 0, sys_len, + params->should_abort, + params->abort_ud); + if (logits) { + if (bs_prefill_capture_sys(sched, slot, sid, prompt, sys_len) != 0) + fprintf(stderr, "[batch_sched] sys-prefix capture failed " + "slot=%d sid=%s (continuing uncached)\n", slot, sid); + tm_free(logits); + logits = sg_prefill_range_sched(model, sched, slot, state, prompt, + sys_len, n_prompt, + params->should_abort, + params->abort_ud); + } + } else { + logits = sg_prefill_range_sched(model, sched, slot, state, prompt, + delta_start, n_prompt, + params->should_abort, + params->abort_ud); + } + pn_reqprof_phase_end(PN_REQ_PREFILL); + sg_prefill_logits_hash(logits, n_prompt - delta_start, state->pos); + if (!logits) { + /* Distinguish a client-disconnect abort from a real prefill error (the + * check is idempotent — a closed socket stays closed), exactly as the + * single-user twin does. Either way the slot's KV now covers a prompt + * prefix nothing describes, so release through the resetting abort so + * the next turn does not get steered back onto it. */ + int aborted = params->should_abort && + params->should_abort(params->abort_ud); + free(prompt); + free(recent); + bs_prefill_abort_reset(sched, slot); + sg_tg_free(&tg); + sg_logprobs_free(&lpacc); + if (aborted) + fprintf(stderr, "[batch_sched] slot %d: client disconnected during " + "prefill — turn aborted, slot released\n", slot); + return aborted ? SG_ABORTED : -1; + } + /* Report the KV the session ACTUALLY occupies — the state's own position, + * not n_prompt. A think-elided resume keeps the previous turn's think block + * in the KV, so the committed length runs ahead of the prompt the client + * sent, and the client's compaction trigger reads this field. */ + result->kv_used_tokens = (int)state->pos; + n_recent = sg_seed_recent(recent, max_recent, prompt, n_prompt); + /* PC-batched: keep `prompt` (the full context) alive for bs_slot_finalize, + * which needs prompt+generated as the committed history. Freed at `out`. */ + if (!use_cache) { + free(prompt); + prompt = NULL; + } + + /* ── 6. Sample first decode token from prefill logits ── */ + pn_reqprof_phase_start(PN_REQ_FIRST_SAMPLE); + uint32_t next = pn_sample_next_token((float *)logits->data, + model->config.n_vocab, + &sp, recent, n_recent); + pn_reqprof_phase_end(PN_REQ_FIRST_SAMPLE); + /* Token #1 is sampled HERE, on the owner thread, and that sample advanced + * the grammar (the batched hook carries advance). The driver's first step + * forwards `next` and its row is read before the driver has re-armed + * anything — so this is the one arming point the driver cannot own. Same + * rule as everywhere else: arm after the advance, before the forward. */ + if (arm_window) + sg_arm_full_logits(&tg, params, model, state); + pn_reqprof_phase_start(PN_REQ_DECODE); + tm_free(logits); + logits = NULL; + + /* ── 7/8. Decode. Two drivers: + * + * L3 (default): bs_slot_bind_ex hands the scheduler's driver this + * request's sampler (params + repeat window + budget); the driver + * samples every step itself and never waits on this thread again. + * This thread only drains tokens (bs_slot_pop_token) and does + * detokenize + emitter + SSE — entirely OFF the batch's critical + * path, so a slow/stalled client cannot stall the other users. + * + * Legacy (QWEN35_BS_DRIVER_SAMPLE=0): the original lockstep + * logits/token exchange. ── */ + emitter em; + em_init(&em, delta_fn, delta_ud, params->stop, params->n_stop, + params->has_tools, params->dialect); + em_set_tool_tap(&em, params->tool_tap, params->tool_tap_ud); + /* No open think block (No-Think, or a non-think template) → start the emitter + * in content mode (see the mirror at the other em_init site). */ + if (sg_thinking_suppressed(params->thinking) || + !sg_chat_style_is_think(model->chat_style)) + em.in_think = 0; + + int eos_id = ft_eos_id(model->tokenizer); + int im_end = ft_token_to_id(model->tokenizer, "<|im_end|>"); + int n_vocab = model->config.n_vocab; + int max_tokens = params->max_tokens > 0 ? params->max_tokens : 512; + + /* Repetition-loop guard — the batched twin of the single-user arm. This is + * the path the deployed fleet runs, so it is the one that matters most; the + * detector sits on THIS thread (which already drains every token to + * detokenize and stream it), never in the scheduler's driver, so it adds + * nothing at all to the batch's critical path. */ + rld_detector reploop; + sg_reploop_arm(&reploop); + + const char *finish = "length"; + bool reploop_stop = false; /* keeps the post-loop em.stopped fixup from + * overwriting the degeneration finish_reason */ + int generated = 0; + + if (bs_driver_sampling(sched)) { + /* EOS as the very first token: nothing to decode — release the GPU + * turn + slot without binding (the legacy path burned one step + * here; the emitted stream is identical). */ + if ((int)next == eos_id || (int)next == im_end) { + free(recent); + bs_prefill_abort(sched, slot); + finish = "stop"; + em_flush(&em); + goto out; + } + /* Arm the driver's sampler. `recent` currently holds the prompt + * tail; the driver appends each stepped token (starting with + * `next`) before sampling — exactly the legacy owner order, so + * streams are bit-identical at temp 0. Budget = max_tokens - 1 + * (this thread already holds token #1 = `next`). */ + if (bs_slot_bind_ex(sched, slot, next, &sp, recent, n_recent, + max_tokens - 1, + use_cache ? sid : NULL, + use_cache ? prompt : NULL, + use_cache ? n_prompt : 0) != 0) { + free(recent); + free(prompt); + em_free(&em); + sg_tg_free(&tg); + rld_free(&reploop); + /* slot + GPU turn were already released inside bind */ + return -1; + } + free(recent); /* the driver owns its own copy */ + + /* AUTO reasoning-budget force-close (batched, per slot): mirror the + * single-user cap. A think-style model was prefilled with "" + * OPEN (unless No-Think closed it), so the slot starts inside ; + * the driver counts tokens spent reasoning and force-decodes "" + * once the per-model budget is hit without a self-close. Arm it now, + * while the slot is BOUND (the driver rebuilds its map before the first + * step, and the arm takes sched->mu, so this races nothing). Budget + * <= 0 or a non-think model → no-op (byte-identical decode). */ + int reason_budget = (params->reasoning_budget > 0 && + sg_chat_style_is_think(model->chat_style)) + ? params->reasoning_budget : 0; + bool reason_in_think = reason_budget > 0 && + !(sg_thinking_suppressed(params->thinking) && + sg_chat_style_is_think(model->chat_style)); + if (reason_budget > 0) { + uint32_t close_toks[8]; + int n_close = sg_encode_think_close(model, close_toks, + (int)(sizeof(close_toks) / + sizeof(close_toks[0]))); + bs_slot_arm_reasoning_budget(sched, slot, reason_budget, + reason_in_think, close_toks, n_close, + next); + } + + /* Emit token #1, then drain the driver's ring. */ + { + char *piece = decode_token(model, next); + /* Token #1 was sampled on THIS thread (from the prefill logits), so + * it is the accumulator's first record; the driver's records follow + * it in ring order. */ + const sg_token_logprob *lp = NULL; + if (em.in_think) sg_logprobs_skip(&lpacc); + else lp = sg_logprobs_take(&lpacc, result, model, piece); + if (lp) em_note_token_logprob(&em, lp); + if (piece) { em_feed(&em, piece); } + free(piece); + generated++; + (void)rld_feed(&reploop, next); + } + while (!em.stopped) { + /* Stop decoding once the client is gone (every 8 tokens, to bound + * the syscall — the single-user loop's cadence). The tokens + * generated so far are valid: the finalize + commit below keeps + * them and finish="stop" ends the turn cleanly, so the session can + * still append-resume if the client comes back. */ + if ((generated & 7) == 0 && params->should_abort && + params->should_abort(params->abort_ud)) { + finish = "stop"; + break; + } + uint32_t tok; + int prc = bs_slot_pop_token(sched, slot, &tok); + if (prc < 0) { finish = "stop"; break; } /* step error/shutdown */ + if (prc == 0) { + /* The driver finishes a slot on EOS, on budget exhaustion, AND + * on grammar completion. Only the middle one is "length" — a + * turn that stopped because the constrained object CLOSED is a + * clean stop, and reporting "length" would tell the client its + * perfectly complete JSON had been truncated. */ + if (sg_tg_live(&tg) && sg_tg_done(&tg)) finish = "stop"; + break; + } + if ((int)tok == eos_id || (int)tok == im_end) { + /* The driver finished the slot on this end token, so the walk + * is no longer touched there: close a "required" call the + * model stopped inside (the single-user loop's rule). */ + finish = sg_tg_close_required_call(&tg, params, &em) ? "tool_calls" : "stop"; + break; + } + char *piece = decode_token(model, tok); + const sg_token_logprob *lp = NULL; + if (em.in_think) sg_logprobs_skip(&lpacc); + else lp = sg_logprobs_take(&lpacc, result, model, piece); + if (lp) em_note_token_logprob(&em, lp); + if (piece) { em_feed(&em, piece); } + free(piece); + /* Reverse coupling (batched): the driver's sampler advances the grammar + * in-thread; if it has decided forced-mode reasoning is over ( + * or budget) but the emitter still routes to , flip it to CONTENT + * so the compelled call is parsed as a tool_call (mirror of the + * single-user loop). */ + if (em.in_think && sg_tg_forced_ended(&tg)) { + em.in_think = 0; + sg_log_reasoning_channel_flip(&tg, generated); + } + generated++; + /* Repetition collapse. Breaking here releases the slot exactly the + * way the client-disconnect check above does — the driver drops it + * from the next step's compaction and stops generating for it — so + * the remaining budget is not spent re-emitting the same block. */ + if (rld_feed(&reploop, tok)) { + finish = SG_FINISH_REPETITION_LOOP; + reploop_stop = true; + break; + } + } + if (em.stopped && !reploop_stop) + finish = "stop"; + em_flush(&em); + /* Hand the slot this turn's CLIENT RE-RENDER — the reply put back + * through the same render+tokenize path the next request will take — + * BEFORE finalize publishes the committed log, so next turn's resume can + * match around the think block whichever way the client handles it: the + * think BLOCK for a client that resends `reasoning_content` (the one + * that keeps the model reasoning), the reply alone for one that drops + * it. A turn with no reproducible reply (a pure-reasoning runaway, or a + * tool block that will not parse back) projects NULL and disables both + * rungs; an empty think block projects no block and disables only the + * reasoning-bearing one. */ + if (use_cache) { + int rerender_n = 0; + uint32_t *rerender = + sg_project_client_rerender(model, em.content.data, em.tool.data, + params->tools, em.dialect, + &rerender_n); + int think_block_n = 0; + uint32_t *think_block = + sg_encode_history_think_block(model, em.reasoning.data, + &think_block_n); + bs_slot_note_visible(sched, slot, prompt, n_prompt, + think_prefill_len, think_block, think_block_n, + rerender, rerender_n); + free(think_block); + free(rerender); + } + /* PC-batched: snapshot the slot's warm SSM+KV into its pn_state and + * publish this session's committed history BEFORE releasing, while the + * slot's GPU decode state is still valid. No-op when not caching. */ + if (use_cache) + bs_slot_finalize(sched, slot); + bs_slot_release(sched, slot); /* driver drops the slot from the + * next step's compaction */ + goto out; + } + + /* ── Legacy lockstep path. bs_slot_bind releases the GPU turn taken by + * bs_prefill_begin (success or failure) and releases the slot itself on + * failure. ── */ + if (bs_slot_bind(sched, slot, next) != 0) { + free(recent); + free(prompt); + em_free(&em); + sg_tg_free(&tg); + rld_free(&reploop); + /* slot + GPU turn were already released inside bs_slot_bind */ + return -1; + } + + /* Wait for first batch step's logits (driver already has `next`). */ + float *slot_logits = NULL; + if (bs_slot_await_first(sched, slot, &slot_logits) != 0) { + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + + /* The token we deposited via bs_slot_bind was `next` — it is the first + * generated token. We need to emit it and then exchange the second + * token with the driver. */ + { + /* Emit `next` (the one we just deposited): decode and stream it. */ + if ((int)next != eos_id && (int)next != im_end) { + char *piece = decode_token(model, next); + const sg_token_logprob *lp = NULL; + if (em.in_think) sg_logprobs_skip(&lpacc); + else lp = sg_logprobs_take(&lpacc, result, model, piece); + if (lp) em_note_token_logprob(&em, lp); + if (piece) { em_feed(&em, piece); free(piece); } + generated++; + (void)rld_feed(&reploop, next); + if (max_recent > 0) { + if (n_recent < max_recent) recent[n_recent++] = next; + else { + memmove(recent, recent + 1, + (size_t)(max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = next; + } + } + } else { + finish = "stop"; + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + if (em.stopped) { + finish = "stop"; + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + } + + /* Exchange loop: sample from slot_logits → check EOS/stop → emit → + * post next token → wait for next logits. + * + * EOS check BEFORE posting to driver: once we know we're done, call + * bs_slot_release (→ RETIRE) instead of posting, so the driver does + * not waste a GPU step on an EOS/stopped token and does not deadlock + * waiting for a token we'll never post. */ + for (int i = 0; i < max_tokens - 1; i++) { + /* Client gone → stop (every 8 tokens; same cadence and same + * keep-what-was-generated semantics as the L3 loop above). */ + if ((i & 7) == 0 && params->should_abort && + params->should_abort(params->abort_ud)) { + finish = "stop"; + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + /* Sample from slot logits deposited by driver. */ + uint32_t sampled = pn_sample_next_token(slot_logits, n_vocab, + &sp, recent, n_recent); + + /* EOS check BEFORE posting. */ + if ((int)sampled == eos_id || (int)sampled == im_end) { + finish = sg_tg_close_required_call(&tg, params, &em) ? "tool_calls" : "stop"; + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + + /* Emit sampled token. */ + char *piece = decode_token(model, sampled); + const sg_token_logprob *lp = NULL; + if (em.in_think) sg_logprobs_skip(&lpacc); + else lp = sg_logprobs_take(&lpacc, result, model, piece); + if (lp) em_note_token_logprob(&em, lp); + if (piece) { em_feed(&em, piece); free(piece); } + generated++; + + if (max_recent > 0) { + if (n_recent < max_recent) recent[n_recent++] = sampled; + else { + memmove(recent, recent + 1, + (size_t)(max_recent - 1) * sizeof(uint32_t)); + recent[max_recent - 1] = sampled; + } + } + + /* Repetition collapse (legacy lockstep twin): stop exactly the way an + * em.stopped stop does — the token is already emitted and counted, so + * releasing the slot here ends the turn cleanly. */ + if (rld_feed(&reploop, sampled)) { + finish = SG_FINISH_REPETITION_LOOP; + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + + if (em.stopped) { + finish = "stop"; + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + goto out; + } + + /* Post sampled token to driver and wait for the next step's logits. */ + slot_logits = NULL; + if (bs_slot_step_wait(sched, slot, sampled, &slot_logits) != 0) { + finish = "stop"; + break; + } + } + + em_flush(&em); + free(recent); + bs_slot_release(sched, slot); + +out: + /* The batched twin of the single-user [repetition] line. The fleet runs + * batched, so this is the copy that makes the production rate countable — + * `path=` tells the two decoders apart. */ + if (reploop.tripped) + sg_reploop_log(&reploop, params, + bs_driver_sampling(sched) ? "batched" : "batched-legacy", + generated); + rld_free(&reploop); + /* PC-batched: `prompt` is kept alive past prefill (for bind_ex + finalize) + * only when caching; free it here. NULL when not caching (freed earlier). */ + free(prompt); + /* Free the grammar ctx. Safe here: the slot was already released + * (bs_slot_release, above every path reaching `out`) under sched->mu, so the + * driver — which reads sl->sp.tool_grammar (== &tg.hook) only under mu with + * status == DECODE — will never touch it again after this point. */ + sg_tg_report_response_format(&tg, result); + sg_tg_report_tool_call(&tg, result); + sg_tg_report_mask_density(&tg); + sg_require_full_logits(&tg, params, model, state, 0); + sg_tg_free(&tg); + /* Same reasoning for the logprob sink (also reached through sl->sp): the + * slot is released, so the driver can no longer write records. Frees only + * the raw buffer — the RESOLVED entries live on the result. */ + sg_logprobs_free(&lpacc); + pn_reqprof_phase_end(PN_REQ_DECODE); + pn_reqprof_note_generated(generated); + result->completion_tokens = generated; + result->finish_reason = finish; + result->content = em.content.data ? em.content.data : strdup(""); + result->reasoning = em.reasoning.data; + result->tool_raw = em.tool.data; + em.content.data = NULL; + em.reasoning.data = NULL; + em.tool.data = NULL; + em_free(&em); + return 0; +} diff --git a/src/serve/serve_generation.h b/src/serve/serve_generation.h new file mode 100644 index 0000000..15edbe5 --- /dev/null +++ b/src/serve/serve_generation.h @@ -0,0 +1,483 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_generation.h + * @brief Chat-completion generation for the serve layer. Builds the engine's + * ChatML(+think) prompt from an OpenAI messages[] array, prefills and + * decodes via the existing engine forward + sampler paths (no generation + * logic is reinvented here), and splits the model's … + * reasoning from the answer into separate channels. + * + * @status Non-streaming and streaming completions work on the CPU host path; + * reasoning is surfaced as a deepseek-style separate channel. PC2: + * the single-user path delta-prefills via pn_prefix_cache when the + * client sends a session id (sg_prefix_ctx); no session id → full + * prefill (byte-identical to before). PC3: the batched path + * (sg_run_completion_batched) also delta-prefills when a session id is + * sent — session→slot affinity + bs_prefill_resume delta-prefill + + * bs_slot_finalize snapshots the slot's warm SSM/KV back into its + * pn_state (via pn_qwen35moe_decode_slot_ssm_to_host). No session id → + * full prefill (byte-identical to the pre-PC batched path). SYS1: + * the single-user path additionally caches the SYSTEM-PREFIX boundary + * state per session (two-stage prefill + snapshot), so a turn whose + * history diverges (reasoning models) still skips the system prefill. + * OpenAI decoding fields: `response_format` (json_object) is enforced by + * a third grammar on the SAME constrained-decoding hook the tool + * grammars use; `seed`, `logit_bias`, `presence_penalty`, + * `frequency_penalty` and `logprobs` are applied in the sampler + * (src/core/sampling_modifiers.c) on BOTH the single-user and batched + * paths. Every one of them is a no-op at its default. + * @issues None + * @todo + * - [ ] SYS1 on the batched path (bs_prefill_resume system-prefix restore). + */ + +#ifndef PN_SERVE_GENERATION_H +#define PN_SERVE_GENERATION_H + +#include + +#include "../../include/pantheon.h" +#include "../core/prefix_cache.h" +#include "batch_sched.h" +#include "tool_calling.h" /* tc_dialect */ + +/* sg_run_completion return codes. 0 = success; -1 = generic error; the value + * below is a distinct code the HTTP layer maps to a clean 503 (Retry-After) + * instead of a 500 — the request was refused pre-flight to protect the host + * (MemoryAware §6.2), nothing failed. */ +#define SG_ADMIT_DENIED (-2) + +/* Returned when the prompt exceeds the model's per-session context budget (the + * hard KV cap / die-or-host KV budget). KV Phase 1: the serve NEVER silently + * drops the oldest tokens (which shattered the prefix cache and stalled the + * client) — it refuses the turn and hands back a distinct, machine-readable + * signal so the client can compact its history and retry. The HTTP layer maps + * this to a `context_over_budget` error body. `sg_result.kv_used_tokens` / + * `kv_cap_tokens` carry the numbers the client needs to decide how much to + * shed. Nothing failed — the request was refused pre-flight to protect the KV + * reservation. */ +#define SG_CONTEXT_OVER_BUDGET (-4) + +/* Returned when the caller's should_abort() fired mid-turn — the client + * disconnected, so the turn stopped early and the model slot was released. The + * cache session (if any) is reset so the next turn full-prefills cleanly. + * Nothing failed; the HTTP layer just drops the already-gone connection. */ +#define SG_ABORTED (-3) + +/* finish_reason for a turn ended by the repetition-loop detector + * (repetition_loop_detector.h): the model stopped producing new text and was + * re-emitting one block of tokens over and over. It is a DISTINCT value on + * purpose — a collapse reported as "stop" reads to the caller as a model that + * chose to finish, which is exactly how the measured 31×-repeated-paragraph + * turn was scored as a tidy stop. A caller tells degeneration from a normal + * finish by this string and nothing else: it never appears on a healthy turn, + * and it OUTRANKS "tool_calls" in the response body (a turn that degenerated + * must not be handed back as a clean call to execute). */ +#define SG_FINISH_REPETITION_LOOP "repetition_loop" + +/* Cooperative abort check: sg_run_completion calls this between prefill chunks + * and periodically during decode; a non-zero return aborts the turn. Used to + * free the model slot the instant the client disconnects instead of finishing a + * ~100s cold prefill nobody is reading. NULL disables it (unchanged behavior). */ +typedef int (*sg_should_abort_fn)(void *user_data); + +/* One chat message from the request (borrowed pointers). */ +typedef struct { + const char *role; /* "system" | "user" | "assistant" | "tool" */ + const char *content; /* message text (may be empty, never NULL) */ + /* Assistant turns only: the `reasoning_content` the serve streamed for that + * turn, resent verbatim by a client that wants the think block replayed into + * the prompt. NULL/empty = the classic behaviour (reasoning dropped). + * When present, the assistant turn renders as + * "{reasoning}{content}" instead of bare "{content}" — which + * is what the serve itself committed to the KV, so the prefix cache's + * append-resume path can match the previous turn exactly instead of + * diverging at the forced opener. See build_prompt(). */ + const char *reasoning; +} sg_message; + +/* Resolved OpenAI `tool_choice` (n2 tool-calling Phase 5 — forced-call mode). + * AUTO : default — the model decides; grammar only cleans up format once a + * call starts (the original Phase-5 behavior; FREE prose allowed). + * NONE : never call a tool — no grammar is built (req->tools is NULLed). + * REQUIRED : MUST emit some tool call — the grammar's FREE state is replaced by + * "optional reasoning, then REQUIRE the opener". + * FUNCTION : MUST call the named function (`forced_name`) — as REQUIRED, and the + * NAME is pinned to that one tool. */ +typedef enum { + SG_TOOL_CHOICE_AUTO = 0, + SG_TOOL_CHOICE_NONE, + SG_TOOL_CHOICE_REQUIRED, + SG_TOOL_CHOICE_FUNCTION, +} sg_tool_choice_kind; + +/* Resolved OpenAI `response_format` for one completion. + * TEXT : the default — no decode constraint at all. + * JSON_OBJECT : the content channel is constrained to ONE syntactically valid + * JSON object (response_format_grammar.c), so a client that sent + * `{"type":"json_object"}` really can JSON.parse the reply. + * A `json_schema` request is resolved to JSON_OBJECT by the HTTP layer (the + * schema's SHAPE is not enforced) and reported as downgraded on the wire — the + * serve layer only ever sees TEXT or JSON_OBJECT. */ +typedef enum { + SG_RESPONSE_FORMAT_TEXT = 0, + SG_RESPONSE_FORMAT_JSON_OBJECT, +} sg_response_format; + +/* Resolved per-request thinking control — the request's `enable_thinking`. + * + * This is the ONE reasoning control a Qwen3.x-family chat template actually + * exposes, and it is BINARY: the template's generation prompt is either + * "\n" (reason) or "\n\n\n\n" (do not). There is no + * graded "reasoning effort" in the template, so there is no graded mode here. + * + * DEFAULT : the request said nothing → the process-wide PANTHEON_NO_THINK + * env decides, exactly as before. Zero value, so a memset-0 + * sg_gen_params (every non-HTTP caller) is byte-identical to today. + * ON : `"enable_thinking": true` → reason, even under PANTHEON_NO_THINK. + * OFF : `"enable_thinking": false` → prefill the CLOSED empty think block. + * + * Resolve with sg_thinking_suppressed(); never test the enum directly, or the + * prompt builder and the stream emitter can disagree about which channel the + * first generated token belongs to. */ +typedef enum { + SG_THINKING_DEFAULT = 0, + SG_THINKING_ON, + SG_THINKING_OFF, +} sg_thinking_mode; + +/* Sampling + limits for one completion. */ +typedef struct { + float temperature; + float top_p; + int top_k; + int max_tokens; + char **stop; /* borrowed array of stop strings (may be NULL) */ + int n_stop; + bool has_tools; /* request carried tools → arm bare "" is detected regardless). */ + tc_dialect dialect; /* resident model's tool-call dialect, resolved + * post-route (chat_apply_tool_dialect). Drives the + * stream emitter's opener set and the grammar setup: + * TC_DIALECT_ORNITH_JSON diverts the JSON block and + * builds NO XML grammar (Phase 1). Default 0 = + * TC_DIALECT_QWEN_XML → byte-identical XML behavior. */ + const void *tools; /* borrowed request `tools` JSON (jr_value*), or NULL. + * Only consulted for Phase-5 grammar-constrained + * decoding, and only when PANTHEON_TOOL_GRAMMAR=1; + * unset/NULL → the sampler is byte-identical. */ + sg_tool_choice_kind tool_choice; /* resolved `tool_choice` (default AUTO). + * Phase-5 forced-call mode: REQUIRED/FUNCTION compel a + * call (grammar's FREE state forced to the opener); + * only acts when PANTHEON_TOOL_GRAMMAR=1 and tools are + * present. AUTO leaves the pre-forced behavior. */ + const char *forced_name; /* target function name for SG_TOOL_CHOICE_FUNCTION; + * NULL otherwise. Owned by the caller's request (must + * outlive the completion); NOT aliased into a JSON DOM + * that could be freed independently. */ + sg_should_abort_fn should_abort; /* optional; non-zero return aborts the turn + * (client disconnect → release the slot). NULL = never. */ + void *abort_ud; /* passed to should_abort (e.g. the client fd). */ + const char *model_name; /* registry name of the model serving this turn, for + * LOGGING only — never a routing/behavior input. The + * reasoning force-close is an abnormal event and its log + * line must name the model it happened to. Borrowed + * from the caller (must outlive the completion); NULL is + * fine and logs as "?". */ + int reasoning_budget; /* AUTO-path reasoning cap (tokens). On a think-style + * model, once this many tokens have been generated INSIDE + * the model's block without a self-emitted + * "", the decode loop force-closes reasoning + * (emits the "" sequence into the state + stream) + * so the model concludes and answers instead of running + * away mid-thought (saber runaway). Resolved as: request + * override (if the body carried it) ELSE the per-model + * default ELSE 0. 0 (or absent) = DISABLED — today's + * unbounded behavior, byte-identical. Independent of the + * tool-forcing grammar (that only fires on tool_choice + * REQUIRED/FUNCTION; this fires on the plain AUTO turn). */ + + sg_thinking_mode thinking; /* request `enable_thinking` (default 0 = the env + * decides). Unlike reasoning_budget — which lets the + * model OPEN a think block and then guillotines it + * mid-sentence — this decides BEFORE generation whether + * the block is opened at all, so nothing is ever cut + * off mid-word. See sg_thinking_mode. */ + + /* ── OpenAI sampling/decoding fields (all default to "off", so a request + * that sends none of them behaves byte-identically to before) ── */ + + sg_response_format response_format; /* constrain the answer to JSON. Only + * ever JSON_OBJECT when the request carried NO tools — + * the two constraints cannot both drive one decode, so + * the HTTP layer resolves the precedence (tools win) and + * tells the client which it got. */ + + float presence_penalty; /* OpenAI `presence_penalty` (0 = off) */ + float frequency_penalty; /* OpenAI `frequency_penalty` (0 = off) */ + + const pn_logit_bias_entry *logit_bias; /* OpenAI `logit_bias`, borrowed + * (owned by the request; must outlive the completion). + * NULL / n_logit_bias 0 = off. */ + int n_logit_bias; + + bool seed_set; /* request carried `seed` → sample from a per-request + * RNG stream instead of the process-global rand(), so + * the same request + seed replays the same tokens. */ + uint64_t seed; + + bool logprobs; /* OpenAI `logprobs`: report per-token logprobs on the + * response. false (the default) = off, nothing is + * computed and the sampler is untouched. */ + int logprobs_top; /* OpenAI `top_logprobs`: how many ALTERNATIVES to + * report per token (0..PN_LOGPROBS_MAX_TOP). Only + * consulted when `logprobs` is true. */ + + void (*tool_tap)(const char *bytes, size_t n, void *ud); + /* streaming only: receives every byte the emitter + * diverts into the raw tool channel, as it is + * generated (em_set_tool_tap), so tool-call deltas + * reach the client while the call is written. + * NULL (the default) = no tap. */ + void *tool_tap_ud; +} sg_gen_params; + +/* + * One token's reported logprobs (OpenAI `logprobs.content[]`). + * + * `token` and `top_token[]` are the DECODED text of the ids, since that is what + * the wire format carries; the numbers are log-probabilities of the model's own + * distribution (before temperature / top-k / top-p / any grammar mask). + * + * Only ANSWER tokens are reported. Tokens generated inside the model's + * block belong to the separate `reasoning_content` channel, which OpenAI's + * `logprobs.content[]` does not describe — and reporting them on one path but + * not the other is exactly the kind of divergence that has bitten this file. + */ +typedef struct { + char *token; /* owned decoded UTF-8 */ + float logprob; + int n_top; + char *top_token[PN_LOGPROBS_MAX_TOP]; /* owned */ + float top_logprob[PN_LOGPROBS_MAX_TOP]; +} sg_token_logprob; + +/* + * Streaming delta sink. `is_reasoning` is 1 for reasoning_content deltas + * (inside ), 0 for answer content deltas. + * + * `logprobs`/`n_logprobs` carry the logprobs of the tokens that produced THIS + * delta (borrowed; valid for the duration of the call, and stable for the whole + * completion since the array is preallocated). They are only ever non-empty for + * content deltas of a request that asked for logprobs — the emitter holds a + * partial tail back to catch markers, so one delta can cover several tokens and + * a token can produce no delta at all, which is why they ride the delta instead + * of being counted by the client. + */ +typedef void (*sg_delta_fn)(const char *text, int is_reasoning, + const sg_token_logprob *const *logprobs, + int n_logprobs, void *user_data); + +/* End-of-turn verdict from the TOOL-CALL decoding grammar: did the call the model + * started ever CLOSE? + * + * This exists because a truncated tool call is not detectable from its text. The + * parser is a string scanner, and the bytes a truncated call leaves behind are a + * legal PREFIX of a good call — every one of them scans into a well-formed-looking + * call with an argument quietly missing (`read {}`, `write` with no `content`). + * The grammar is the only component that knows it was still mid-skeleton when the + * turn ended, and it computes that bit already; this carries it to the HTTP layer, + * which refuses to hand such a call to the client (TC_PARSE_CALL_INCOMPLETE). + * + * ABSENT is not a soft "no": it means no tool grammar ran (PANTHEON_TOOL_GRAMMAR + * off, no tools on the request, a response_format turn, or a build that fell back + * to inert), so there is no verdict and the caller must not gate on one. A + * grammar that never entered a call is also ABSENT — the model answered in prose, + * or emitted a dialect this grammar does not model, and neither is a truncation. + * + * Same discipline as response_format_ok below: a status that claims a guarantee it + * did not deliver is the defect the field exists to stop. */ +enum { + SG_TOOL_GRAMMAR_ABSENT = 0, /* no tool grammar ran / never entered a call */ + SG_TOOL_GRAMMAR_COMPLETE = 1, /* the grammar closed a whole call */ + SG_TOOL_GRAMMAR_INCOMPLETE = -1, /* entered a call, turn ended before it closed */ +}; + +/* Result of a completion (owned strings; free with sg_result_free). */ +typedef struct { + char *content; /* answer text (owned, never NULL on success) */ + char *reasoning; /* reasoning text (owned; NULL if none) */ + char *tool_raw; /* raw model text from "" onward + * (owned; NULL if the model emitted no call). + * The caller parses it with tc_parse_tool_calls. */ + int prompt_tokens; + int completion_tokens; + int cached_prompt_tokens; /* of `prompt_tokens`, how many the prefix + * cache let this turn SKIP — the prompt + * tokens NOT computed here (OpenAI's + * usage.prompt_tokens_details.cached_tokens). + * Always the cache's own resume boundary + * (pn_prefix_cached_tokens), never a count + * recomputed from the prompt. 0 on a cold + * turn, and always <= prompt_tokens. + * + * Without it a client measuring prefill rate + * divides prompt_tokens by TTFT and gets a + * number that RISES the more the serve + * caches — a 10x inflation that reads as + * progress. This is the divisor correction. */ + int context_capped; /* 1 if admission clamped the prompt window + * (MemoryAware §6.2 rung 3) — the client is + * told its context was shortened. */ + int kv_used_tokens; /* KV Phase 1: this session's KV occupancy in + * tokens after this turn (prompt + committed + * generation), for the client's compaction + * trigger. 0 = unknown / not reported. */ + int kv_cap_tokens; /* the EFFECTIVE per-session context window in + * tokens (pn_effective_ctx_window: the + * configured cap AFTER the memory ladder's + * runtime clamp). 0 = unknown. + * + * The same number GET /v1/models advertises + * as `context_window` — deliberately: the + * client treats the model list as its INITIAL + * plan and this as the LIVE truth, so a + * mid-session clamp reaches it with no + * polling and no extra endpoint. */ + int session_conflict; /* 1 when this request's session id already + * identified a DIFFERENT conversation (a + * different system prompt): the client is + * sharing one session id across two + * conversations, which costs BOTH of them + * their warm prefix on every turn. Batched + * path only — see bs_slot_session_conflict. + * 0 = no conflict / not detectable. */ + const char *finish_reason; /* "stop" | "length" | "tool_calls" | + * SG_FINISH_REPETITION_LOOP (static string) */ + int response_format_ok; /* 1 when a requested `response_format` was + * actually ENFORCED — the JSON grammar ran and + * closed a complete object. 0 means the + * constraint did not hold, and the HTTP layer + * must NOT tell the client "applied": a status + * that claims a guarantee it did not deliver is + * the exact defect this field exists to stop. + * Meaningless (0) when no response_format was + * requested. */ + int tool_grammar_verdict; /* SG_TOOL_GRAMMAR_* — whether the tool-call + * grammar closed the call it entered. The + * HTTP layer gates on this before emitting any + * call parsed out of `tool_raw`. */ + sg_token_logprob *logprobs; /* OpenAI `logprobs`: one entry per generated + * token, owned (freed by sg_result_free). + * NULL unless the request asked for them. */ + int n_logprobs; +} sg_result; + +/** + * PC2 — optional prefix-cache context for a completion. When non-NULL and + * `cache`/`sid` are set, sg_run_completion acquires the session's cached model + * state, prefills ONLY the delta tokens (the LCP of the cached history and this + * prompt), and commits the full prompt+reply back to the session — so an + * agentic follow-up prefills the new tokens instead of the whole context. + * `pc_lock` (if non-NULL) serializes cache access (the cache is thread-unsafe). + * + * When the whole struct pointer is NULL (or cache/sid NULL), sg_run_completion + * behaves EXACTLY as before: it resets the passed-in `state` and full-prefills + * — byte-identical to the pre-PC serve path (safe-by-default). + */ +typedef struct { + pn_prefix_cache *cache; /* session cache (NULL → no caching) */ + const char *sid; /* session id (NULL → no caching) */ + pthread_mutex_t *pc_lock; /* serializes cache ops (may be NULL) */ +} sg_prefix_ctx; + +/** + * Does this request arm the tool-call grammar? The PANTHEON_TOOL_GRAMMAR + * policy: "0" never; "1" every request carrying tools; unset (or any other + * value) only tool_choice "required" — the client asked for a call every turn. + * + * Thread-safe: yes (reads the environment) + */ +bool sg_tool_grammar_arms(const sg_gen_params *params); + +/** + * Can a client rely on tool_choice "required" with thinking ON for a model + * speaking `dialect`? True when a grammar models the dialect + * (tc_dialect_grammar_models: ORNITH_JSON, QWEN_XML) and "required" arms it (the + * env does not opt out): the think block is free and the turn ends in a parsed + * call. False for MINICPM_XML. Advertised on /v1/models as + * `tool_choice_required_with_thinking`. + * + * Thread-safe: yes + */ +bool sg_tool_choice_required_with_thinking(tc_dialect dialect); + +/** + * Run one chat completion. Resets `state`, builds the ChatML prompt, prefills + * and decodes. If `delta_fn` is non-NULL, streams deltas as tokens arrive; + * regardless, the full reasoning + answer are accumulated into *result. + * + * @param pc Optional prefix-cache context (PC2). NULL → full prefill on the + * passed-in `state` (legacy behaviour). When set, the session's + * cached state is used and `state` is ignored. + * + * @return 0 on success, -1 on error. + * + * Thread-safe: no (drives a single model/state; uses the shared sampler scratch) + */ +int sg_run_completion(pn_model *model, pn_state *state, + const sg_message *messages, int n_messages, + const sg_gen_params *params, + sg_delta_fn delta_fn, void *delta_ud, + sg_result *result, const sg_prefix_ctx *pc); + +/** Free the owned strings of a result. NULL-safe. */ +void sg_result_free(sg_result *result); + +/** + * C3 — batched completion via a slot scheduler. + * + * Same semantics as sg_run_completion but runs inside the batch scheduler: + * prefill is single-user, BATCHED (L1: chunk-wise + * pn_qwen35moe_forward_batch_cuda) and CHUNKED (L2: the GPU turn is yielded + * to the decode driver between chunks); decode is batched with any other + * users concurrently generating on the same model, sampled in-driver (L3, + * default — this thread only drains a token ring and streams SSE, off the + * batch's critical path; QWEN35_BS_DRIVER_SAMPLE=0 restores the legacy + * lockstep exchange). + * + * `slot` must be a valid slot index previously acquired with bs_slot_acquire + * (the caller checks slot availability BEFORE beginning SSE so a 429 can be + * returned before the SSE stream is opened). The function takes ownership of + * the slot and releases it on completion (or error); the caller must NOT call + * bs_slot_release separately. + * + * PC-batched: `sid` (may be NULL) is the prefix-cache session id. When + * non-NULL/non-empty the slot's warm pn_state is delta-prefilled (only the + * tokens appended since the session's last turn on this slot) and re-committed + * for next-turn reuse; the session→slot affinity is a soft hint applied at + * acquire time. NULL/empty → full prefill every turn, byte-identical to the + * pre-PC batched path (safe-by-default). + * + * Thread-safe: YES — each call runs on its own request thread; shared state + * is only the scheduler (its own mutex) and _Thread_local sampler scratch. + */ +int sg_run_completion_batched(pn_model *model, pn_batch_sched *sched, + int slot, + const sg_message *messages, int n_messages, + const sg_gen_params *params, + sg_delta_fn delta_fn, void *delta_ud, + sg_result *result, const char *sid); + +/** + * Measured prefill throughput in tokens/sec (K80 serving-ops Phase 7): an + * exponential moving average over the real prefills this process has run + * (fed from sg_run_completion). Returns 0.0 before the first measurable + * prefill — callers fall back to a static constant (PN_PREFILL_TOK_PER_S) for + * the restart estimate until a real sample lands. Thread-safe. + */ +double sg_prefill_tok_per_s_measured(void); + +#endif /* PN_SERVE_GENERATION_H */ diff --git a/src/serve/serve_prompt_tokens.c b/src/serve/serve_prompt_tokens.c new file mode 100644 index 0000000..3bb55b8 --- /dev/null +++ b/src/serve/serve_prompt_tokens.c @@ -0,0 +1,425 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_prompt_tokens.c + * @brief Implementation of the ChatML prompt tokenizer and the client-re-render + * projection (see serve_prompt_tokens.h). + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "serve_prompt_tokens.h" + +#include +#include +#include + +#include "../core/bpe_pretokenize.h" +#include "../core/chat.h" +#include "../core/engine.h" +#include "json_reader.h" +#include "tokenizer/tokenizer.h" + +/* ============================================================ + * Token builder — mirrors src/core/chat.c's ChatML(+think) layout, but over a + * full OpenAI messages[] array (multi-turn history), reusing the same + * tokenization primitives (special-token lookup + qwen35 BPE text encode). + * ============================================================ */ + +typedef struct { + uint32_t *tokens; + int len; + int cap; +} token_builder; + +static bool tb_init(token_builder *tb) +{ + tb->cap = 256; + tb->len = 0; + tb->tokens = malloc((size_t)tb->cap * sizeof(uint32_t)); + return tb->tokens != NULL; +} + +static void tb_push(token_builder *tb, uint32_t id) +{ + if (tb->len >= tb->cap) { + tb->cap *= 2; + uint32_t *grown = realloc(tb->tokens, (size_t)tb->cap * sizeof(uint32_t)); + if (!grown) return; + tb->tokens = grown; + } + tb->tokens[tb->len++] = id; +} + +static void tb_push_special(token_builder *tb, ft_tokenizer *tok, const char *s) +{ + int id = ft_token_to_id(tok, s); + if (id >= 0) tb_push(tb, (uint32_t)id); +} + +/* Encode plain text with the model's tokenizer (qwen35 BPE for the mini, else + * the SentencePiece path) and append the ids. */ +static void tb_push_text(token_builder *tb, ft_tokenizer *tok, const char *text) +{ + if (!text || !*text) return; + int n = 0; + int *ids = (ft_get_model_type(tok) == 0) + ? pn_bpe_encode(tok, text, &n) + : ft_encode(tok, text, &n); + if (!ids) return; + for (int i = 0; i < n; i++) + if (ids[i] >= 0) tb_push(tb, (uint32_t)ids[i]); + free(ids); +} + +/* The tool-call wrapper. The vocab holds these as SINGLE special ids + * (nex-n2-mini: "" is id 248058), and tb_push_text would byte-encode + * the rendered STRING into the four ordinary ids "<","tool","_call",">". Both + * are legal tokenizations of the same characters, so the ONLY thing that matters + * is that the prompt builder and the re-render projection pick the same one — + * which they do by both going through sg_encode_assistant_content. Only + * assistant turns: a user/tool message that happens to contain the literal text + * stays ordinary text. */ +static const char *const TOOL_WRAPPER_MARKERS[] = { "", "" }; +#define TOOL_WRAPPER_MARKER_COUNT \ + (sizeof(TOOL_WRAPPER_MARKERS) / sizeof(TOOL_WRAPPER_MARKERS[0])) + +/* Encode assistant-turn content, emitting any tool-call wrapper marker as its + * special id. A marker the vocab does not know (id < 0) is left alone and + * encodes as ordinary text, so a model without these tokens tokenizes + * byte-identically to a plain tb_push_text. */ +static void tb_push_assistant_content(token_builder *tb, ft_tokenizer *tok, + const char *content) +{ + if (!content || !*content) return; + const char *pos = content; + while (*pos) { + const char *best = NULL; + size_t best_len = 0; + int best_id = -1; + for (size_t i = 0; i < TOOL_WRAPPER_MARKER_COUNT; i++) { + int id = ft_token_to_id(tok, TOOL_WRAPPER_MARKERS[i]); + if (id < 0) continue; + const char *at = strstr(pos, TOOL_WRAPPER_MARKERS[i]); + if (at && (!best || at < best)) { + best = at; + best_len = strlen(TOOL_WRAPPER_MARKERS[i]); + best_id = id; + } + } + if (!best) { + tb_push_text(tb, tok, pos); + return; + } + if (best > pos) { + size_t head_len = (size_t)(best - pos); + char *head = malloc(head_len + 1); + if (head) { + memcpy(head, pos, head_len); + head[head_len] = '\0'; + tb_push_text(tb, tok, head); + free(head); + } + } + tb_push(tb, (uint32_t)best_id); + pos = best + best_len; + } +} + +/* Emit one "<|im_start|>{role}\n{content}<|im_end|>\n" block. An assistant + * turn's content goes through tb_push_assistant_content so a rendered tool call + * reproduces the ids the projection recorded (see TOOL_WRAPPER_MARKERS). */ +static void tb_push_turn(token_builder *tb, ft_tokenizer *tok, + const char *role, const char *content) +{ + char header[64]; + snprintf(header, sizeof(header), "%s\n", role); + tb_push_special(tb, tok, "<|im_start|>"); + tb_push_text(tb, tok, header); + if (strcmp(role, "assistant") == 0) + tb_push_assistant_content(tb, tok, content); + else + tb_push_text(tb, tok, content); + tb_push_special(tb, tok, "<|im_end|>"); + tb_push_text(tb, tok, "\n"); +} + +/* Whitespace the streaming emitter left-trims off the head of each channel and + * that a resent think block therefore cannot recover from the text alone (see + * serve_emitter.c's em_emit). These are the canonical Qwen/N2 separators the + * model itself emits: "\n" right after the forced , "\n\n" after the + * "" close. */ +static const char THINK_LEAD_WS[] = "\n"; +static const char THINK_TAIL_WS[] = "\n\n"; + +/* Emit the think block of a HISTORY assistant turn — "{reasoning}" + * plus the separators the emitter trimmed — for a client that resent the + * reasoning verbatim. and go in as SPECIAL ids: tb_push_text + * would byte-encode them as ordinary text and never reach id 248068. + * + * This is the ONE definition of that span. Both the prompt builder (below) and + * the reasoning-bearing resume projection (sg_encode_history_think_block) go + * through it, for the same reason tb_push_assistant_content is shared — a + * second implementation is free to drift, and a one-token drift costs a full + * re-prefill. */ +static void tb_push_history_think_block(token_builder *tb, ft_tokenizer *tok, + const char *reasoning) +{ + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, THINK_LEAD_WS); + tb_push_text(tb, tok, reasoning); + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, THINK_TAIL_WS); +} + +/* Emit one assistant turn whose reasoning the client resent verbatim: + * "<|im_start|>assistant\n{reasoning}{content}<|im_end|>\n". */ +static void tb_push_assistant_think_turn(token_builder *tb, ft_tokenizer *tok, + const char *reasoning, + const char *content) +{ + tb_push_special(tb, tok, "<|im_start|>"); + tb_push_text(tb, tok, "assistant\n"); + tb_push_history_think_block(tb, tok, reasoning); + tb_push_assistant_content(tb, tok, content); + tb_push_special(tb, tok, "<|im_end|>"); + tb_push_text(tb, tok, "\n"); +} + +/* Emit a tool-result turn per the model's chat_template: a tool message is fed + * back inside a wrapper on a user turn, so multi-step tool loops + * reconstruct the prompt shape the model was trained on. */ +static void tb_push_tool_response(token_builder *tb, ft_tokenizer *tok, + const char *content) +{ + tb_push_special(tb, tok, "<|im_start|>"); + tb_push_text(tb, tok, "user\n\n"); + tb_push_text(tb, tok, content); + tb_push_text(tb, tok, "\n"); + tb_push_special(tb, tok, "<|im_end|>"); + tb_push_text(tb, tok, "\n"); +} + +bool sg_no_think_enabled(void) +{ + static int cached = -1; + if (cached < 0) { + const char *e = getenv("PANTHEON_NO_THINK"); + cached = (e && e[0] == '1') ? 1 : 0; + } + return cached != 0; +} + +bool sg_chat_style_is_think(int chat_style) +{ + return chat_style == PN_CHAT_CHATML_THINK || + chat_style == PN_CHAT_CHATML_THINK_NL; +} + +bool sg_thinking_suppressed(sg_thinking_mode mode) +{ + if (mode == SG_THINKING_OFF) return true; + if (mode == SG_THINKING_ON) return false; + return sg_no_think_enabled(); /* SG_THINKING_DEFAULT → the env decides */ +} + +/* Push the forced generation prefill for the assistant turn about to be + * generated — the tokens the model did not choose. Returns how many. */ +static int tb_push_generation_prefill(token_builder *tb, ft_tokenizer *tok, + int chat_style, sg_thinking_mode thinking) +{ + int before = tb->len; + if (sg_chat_style_is_think(chat_style) && sg_thinking_suppressed(thinking)) { + /* No-Think: prefill a CLOSED empty think block so a hybrid + * Think/No-Think model (MiniCPM5, saber-orch) skips reasoning and acts + * DIRECTLY. These four pushes are byte-for-byte the Qwen3.x template's + * enable_thinking=false serialization ("\n\n\n\n"), which + * is the only reasoning control that family's template exposes — hence a + * BINARY switch and not a graded effort level. Requested per-request via + * `enable_thinking`, or process-wide via PANTHEON_NO_THINK. */ + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, "\n\n"); + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, "\n\n"); + } else if (chat_style == PN_CHAT_CHATML_THINK) { + tb_push_special(tb, tok, ""); + } else if (chat_style == PN_CHAT_CHATML_THINK_NL) { + /* Bonsai-27B: the model's own template prefills '\n' — the + * newline is part of the trained prefill. Prefilling bare + * leaves "\n\n\n\n" (the template's NON-thinking serialization) + * as the natural greedy continuation, so the model deterministically + * closes the think block empty (Phase 3b bug). */ + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, "\n"); + } + return tb->len - before; +} + +uint32_t *sg_build_prompt_tokens(struct pn_model *model, + const sg_message *messages, int n_messages, + sg_thinking_mode thinking, + int *n_out, int *sys_len_out, + int *think_prefill_out) +{ + ft_tokenizer *tok = model->tokenizer; + token_builder tb; + if (sys_len_out) *sys_len_out = 0; + if (think_prefill_out) *think_prefill_out = 0; + if (!tb_init(&tb)) return NULL; + + /* Templates that lead with {{ bos_token }} (e.g. MiniCPM5) need BOS at + * position 0 — the model was trained with it and degenerates to whitespace + * without it. Detected at load (model->chat_add_bos). */ + if (model->chat_add_bos) { + int bid = ft_bos_id(tok); + if (bid >= 0) tb_push(&tb, (uint32_t)bid); + } + + for (int i = 0; i < n_messages; i++) { + const char *role = messages[i].role ? messages[i].role : "user"; + const char *content = messages[i].content ? messages[i].content : ""; + const char *reasoning = messages[i].reasoning; + if (strcmp(role, "tool") == 0) + tb_push_tool_response(&tb, tok, content); + else if (reasoning && *reasoning && strcmp(role, "assistant") == 0 && + sg_chat_style_is_think(model->chat_style)) + tb_push_assistant_think_turn(&tb, tok, reasoning, content); + else + tb_push_turn(&tb, tok, role, content); + if (i == 0 && sys_len_out && strcmp(role, "system") == 0) + *sys_len_out = tb.len; + } + + tb_push_special(&tb, tok, "<|im_start|>"); + tb_push_text(&tb, tok, "assistant\n"); + int prefill_n = tb_push_generation_prefill(&tb, tok, model->chat_style, + thinking); + if (think_prefill_out) *think_prefill_out = prefill_n; + *n_out = tb.len; + return tb.tokens; +} + +uint32_t *sg_encode_history_think_block(struct pn_model *model, + const char *reasoning, int *n_out) +{ + *n_out = 0; + if (!model || !reasoning || !reasoning[0]) return NULL; + /* Match sg_build_prompt_tokens exactly: a non-think chat style renders an + * assistant history turn WITHOUT a think block whatever `reasoning` says, so + * projecting one here would predict a prompt the builder never produces. */ + if (!sg_chat_style_is_think(model->chat_style)) return NULL; + token_builder tb; + if (!tb_init(&tb)) return NULL; + tb_push_history_think_block(&tb, model->tokenizer, reasoning); + if (tb.len <= 0) { + free(tb.tokens); + return NULL; + } + *n_out = tb.len; + return tb.tokens; +} + +uint32_t *sg_encode_assistant_content(struct pn_model *model, const char *text, + int *n_out) +{ + *n_out = 0; + if (!model || !text || !text[0]) return NULL; + token_builder tb; + if (!tb_init(&tb)) return NULL; + tb_push_assistant_content(&tb, model->tokenizer, text); + if (tb.len <= 0) { + free(tb.tokens); + return NULL; + } + *n_out = tb.len; + return tb.tokens; +} + +/* ============================================================ + * Client-re-render projection + * ============================================================ */ + +/* Re-render the serve's OWN parsed calls into assistant-history syntax, through + * the very function the next request's `tool_calls` array will go through. + * + * The detour via JSON is deliberate and is the whole point: the calls are put + * back into the exact OpenAI wire shape the reply is about to carry, then handed + * to tc_render_assistant_tool_calls. Rendering them any other way would be a + * SECOND implementation of the history syntax, free to drift from the one that + * actually runs next turn — and a one-token drift costs a full re-prefill. + * Returns an owned string, or NULL on OOM / no calls. */ +static char *render_own_calls_as_history(const tc_tool_call *calls, int n, + bool content_empty, tc_dialect dialect) +{ + if (!calls || n <= 0) return NULL; + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, "["); + for (int i = 0; i < n; i++) { + if (i) jw_raw(&jw, ","); + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, calls[i].id ? calls[i].id : ""); + jw_raw(&jw, ",\"type\":\"function\",\"function\":{\"name\":"); + jw_string(&jw, calls[i].name ? calls[i].name : ""); + jw_raw(&jw, ",\"arguments\":"); + jw_string(&jw, calls[i].arguments ? calls[i].arguments : "{}"); + jw_raw(&jw, "}}"); + } + jw_raw(&jw, "]"); + char *rendered = NULL; + if (!jw.oom) { + jr_value *dom = jr_parse(jw.data); + rendered = tc_render_assistant_tool_calls(dom, content_empty, dialect); + jr_free(dom); + } + jw_free(&jw); + return rendered; +} + +/* Concatenate the turn's content and its rendered calls exactly as + * combine_assistant_tool_calls (openai_chat.c) will next turn. Returns an owned + * string, or NULL on OOM. */ +static char *join_content_and_calls(const char *content, const char *calls_xml) +{ + size_t clen = content ? strlen(content) : 0; + size_t xlen = calls_xml ? strlen(calls_xml) : 0; + char *joined = malloc(clen + xlen + 1); + if (!joined) return NULL; + if (clen) memcpy(joined, content, clen); + if (xlen) memcpy(joined + clen, calls_xml, xlen); + joined[clen + xlen] = '\0'; + return joined; +} + +uint32_t *sg_project_client_rerender(struct pn_model *model, + const char *content, const char *tool_raw, + const void *tools, tc_dialect dialect, + int *n_out) +{ + *n_out = 0; + if (!model) return NULL; + if (!content) content = ""; + + char *calls_xml = NULL; + if (tool_raw && tool_raw[0]) { + tc_tool_call *calls = NULL; + int n_calls = 0; + if (tc_parse_tool_calls(tool_raw, (const jr_value *)tools, dialect, + &calls, &n_calls) == 0 && n_calls > 0) + calls_xml = render_own_calls_as_history(calls, n_calls, + content[0] == '\0', dialect); + tc_tool_calls_free(calls, n_calls); + /* A diverted block that will not parse back is a turn whose re-render we + * cannot predict — disable the rung rather than record a guess. */ + if (!calls_xml) return NULL; + } + + char *joined = join_content_and_calls(content, calls_xml); + free(calls_xml); + if (!joined) return NULL; + uint32_t *ids = sg_encode_assistant_content(model, joined, n_out); + free(joined); + return ids; +} diff --git a/src/serve/serve_prompt_tokens.h b/src/serve/serve_prompt_tokens.h new file mode 100644 index 0000000..b780186 --- /dev/null +++ b/src/serve/serve_prompt_tokens.h @@ -0,0 +1,212 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_prompt_tokens.h + * @brief The ChatML prompt tokenizer for the serve layer, and the projection of + * what a client will resend for the turn the serve just generated. + * + * Two halves of ONE contract, which is why they live in one module: + * + * sg_build_prompt_tokens() turns an OpenAI messages[] array into the token + * stream the model is prefilled with. + * sg_project_client_rerender() predicts the token stream the SAME builder + * will produce next turn for the assistant turn just + * generated. + * sg_encode_history_think_block() the same prediction for the turn's + * `reasoning_content`, for a client that sends + * reasoning BACK (the collapse fix). + * + * The prefix cache lives or dies on those two agreeing. A session can only + * resume (rather than re-prefill its whole history) when the committed state's + * client-visible projection is a token-exact prefix of the next request's + * context — so the projection must not GUESS what the client will send. It + * renders the assistant turn through the very same functions the next request + * will go through (tc_render_assistant_tool_calls → sg_encode_assistant_content), + * which makes agreement structural rather than lucky. + * + * Why that matters, concretely: a model's own sampled ids for a piece of text + * are NOT a function of the text. "" has two tokenizations in the + * n2/saber vocab — the special id 248058 and the four ordinary ids + * "<","tool","_call",">" — and the char-level tool grammar accepts either, so + * which one lands in the KV is a sampling accident. Any projection built from + * the generated ids therefore diverges from the client's deterministic + * re-render on some turns; one built from the re-render diverges on none. + * + * @status Complete. Extracted from serve_generation.c (2026-08-09) together + * with the re-render projection that replaced the byte-offset / + * wrapper-guessing projection. The think prefill is now per-REQUEST + * (sg_thinking_suppressed) rather than only process-wide; the env + * PANTHEON_NO_THINK remains the default when a request says nothing. + * sg_encode_history_think_block extends the same one-encoder rule to + * the reasoning channel, so a client that resends `reasoning_content` + * resumes instead of re-prefilling. + * @issues A turn that emits BOTH prose content and a tool call is projected + * with the prose included; the client drops it (`content:null`) when it + * resends such a turn, so that turn misses the rung and re-prefills. + * Fails safe, never wrong. Not reachable under forced tool_choice + * (all prose lands in the reasoning channel). + * @todo None + */ + +#ifndef PN_SERVE_PROMPT_TOKENS_H +#define PN_SERVE_PROMPT_TOKENS_H + +#include +#include + +#include "serve_generation.h" /* sg_message */ +#include "tool_calling.h" /* tc_dialect */ + +struct pn_model; + +/** + * Is this chat style one that prefills a "" opener into the assistant + * turn (a reasoning model)? + * + * @param chat_style a pn_chat_style value + * @return true for the think-prefilling styles + * + * Thread-safe: yes (pure) + */ +bool sg_chat_style_is_think(int chat_style); + +/** + * No-Think mode (env PANTHEON_NO_THINK=1), read once and cached. When set, a + * hybrid Think/No-Think reasoning model is prefilled with a CLOSED empty think + * block so it acts directly instead of reasoning. + * + * @return true when the env var is set to "1" + * + * Thread-safe: yes (idempotent cache of a getenv) + */ +bool sg_no_think_enabled(void); + +/** + * Resolve one request's thinking control to the single question the rest of the + * pipeline asks: is this completion's think block prefilled CLOSED? + * + * The prompt builder, the response-format grammar's reasoning grace and the + * stream emitter's starting channel must all get the SAME answer — if they + * disagree, the model's first generated token is routed to the wrong channel + * and the answer/tool-call channel comes back empty. So they all call this. + * + * @param mode the request's resolved sg_thinking_mode + * @return true when reasoning is suppressed (prefill "\n\n\n\n") + * + * Thread-safe: yes (pure, apart from the cached getenv in sg_no_think_enabled) + */ +bool sg_thinking_suppressed(sg_thinking_mode mode); + +/** + * Build the full prompt token stream from messages[], then open the assistant + * turn (prefilling "" when the model is a reasoning model). + * + * An assistant message that carries `reasoning` renders as + * "{reasoning}{content}" rather than bare "{content}", which + * reproduces the token stream this serve committed for that turn. + * + * @param model resident model (tokenizer + chat style + BOS rule) + * @param messages the OpenAI messages[] as sg_message + * @param n_messages how many + * @param thinking the request's thinking control; SG_THINKING_DEFAULT + * reproduces the pre-parameter behavior exactly + * @param n_out receives the token count + * @param sys_len_out optional; receives the SYSTEM-PREFIX token boundary + * — the leading tokens covering the first message's + * whole turn when it is a system message, else 0. + * Those tokens are byte-identical across a session's + * turns, so the prefix cache can snapshot there (SYS1). + * @param think_prefill_out optional; receives the FORCED generation prefill + * length (tokens pushed after "<|im_start|>assistant\n" + * that the model did not choose and a client cannot + * reproduce). + * @return Owned token array (caller frees), or NULL on failure. + * + * Thread-safe: yes (no shared state beyond the tokenizer, which is read-only) + */ +uint32_t *sg_build_prompt_tokens(struct pn_model *model, + const sg_message *messages, int n_messages, + sg_thinking_mode thinking, + int *n_out, int *sys_len_out, + int *think_prefill_out); + +/** + * Encode the think block of a HISTORY assistant turn as the prompt builder will + * render it next turn when the client resends this turn's `reasoning_content`: + * "" + the emitter-trimmed lead newline + {reasoning} + "" + the + * trailing blank line, with both tags as SPECIAL ids. + * + * This is the reasoning-bearing half of the resume contract. A client that + * sends reasoning back cannot reproduce the token ids the model SAMPLED inside + * its think block — the same "a model's ids are not a function of its text" + * problem sg_project_client_rerender solves for the answer channel — so the + * projection is built from the reasoning TEXT the serve streamed, put through + * the very function build_prompt() will use. The client therefore only has to + * echo the reasoning STRING byte-for-byte; the ids are re-derived here. + * + * @param model resident model (tokenizer + chat style) + * @param reasoning the `reasoning_content` this turn streamed (the emitter's + * accumulated reasoning channel, verbatim) + * @param n_out receives the token count (0 when nothing is projected) + * @return Owned token array (caller frees), or NULL when there is no think + * block to project — empty reasoning, or a non-think chat style, whose + * history turns the builder renders WITHOUT one whatever the client + * sends. The caller then leaves the reasoning-bearing rung disabled. + * + * Thread-safe: yes + */ +uint32_t *sg_encode_history_think_block(struct pn_model *model, + const char *reasoning, int *n_out); + +/** + * Encode ASSISTANT-turn content, promoting the tool-call wrapper markers + * ("" / "") to the special ids the vocab holds for them. + * Exposed because it is the single definition of "how assistant text becomes + * tokens" that both the prompt builder and the re-render projection must use — + * if they ever differ, every tool turn re-prefills its whole conversation. + * + * @param model resident model (for its tokenizer) + * @param text the assistant turn's content (NULL/empty → zero tokens) + * @param n_out receives the token count + * @return Owned token array (caller frees), or NULL on OOM / empty input. + * + * Thread-safe: yes + */ +uint32_t *sg_encode_assistant_content(struct pn_model *model, const char *text, + int *n_out); + +/** + * Project what a faithful client will RESEND for the assistant turn the serve + * has just generated: the serve's own reply, re-rendered into prompt tokens by + * the same path the next request will take. + * + * The turn's reply is `content` plus, when the model emitted a tool call, the + * calls parsed out of `tool_raw`. Those calls are re-rendered with + * tc_render_assistant_tool_calls — the exact function build_messages() will call + * on the client's `tool_calls` array next turn — and the result is tokenized + * with sg_encode_assistant_content, the exact function build_prompt() will use. + * So the projection is not a prediction of the client so much as a replay of the + * deterministic pipeline the client's message is about to enter. + * + * @param model resident model (tokenizer + dialect target) + * @param content the visible content channel of the reply ("" when the whole + * turn was reasoning + a tool call) + * @param tool_raw the raw diverted tool-call text, or NULL when the turn made + * no call + * @param tools borrowed request `tools` array (jr_value*), for schema-typed + * argument parsing; may be NULL + * @param dialect the resident model's tool-call dialect + * @param n_out receives the token count (0 when there is nothing to project) + * @return Owned token array (caller frees), or NULL when the turn has no + * reproducible reply — a pure-reasoning runaway, or a tool block that + * would not parse back. The caller must then leave the rung disabled + * rather than record a projection it cannot stand behind. + * + * Thread-safe: yes + */ +uint32_t *sg_project_client_rerender(struct pn_model *model, + const char *content, const char *tool_raw, + const void *tools, tc_dialect dialect, + int *n_out); + +#endif /* PN_SERVE_PROMPT_TOKENS_H */ diff --git a/src/serve/serve_registry.c b/src/serve/serve_registry.c new file mode 100644 index 0000000..b0f2c34 --- /dev/null +++ b/src/serve/serve_registry.c @@ -0,0 +1,248 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_registry.c + * @brief Implementation of the serve-side dispatch self-registration lifecycle + * (see serve_registry.h). Fully gated on rc_enabled() (PANTHEON_REGISTRY_URL): + * with it unset serve_registry_start() returns immediately having done + * nothing — no registration, no thread, and serve_registry_deregister_all() + * is likewise a no-op — so the serve process is byte-for-byte unchanged. + * + * @status G0 wiring + runtime add/remove (models loaded via /admin after startup + * now register too — the instance table is mutex-guarded). + * @issues None + * @todo None + */ + +#include "serve_registry.h" + +#include +#include +#include +#include +#include + +#include "batch_sched.h" +#include "registry_client.h" +#include "registry_wire.h" + +/* Registered-instance table. Seeded by serve_registry_start(), then mutated at + * runtime by serve_registry_add()/_remove() (an /admin load/unload). Guarded by + * g_lock — the table is no longer write-once, so every read (heartbeat, + * deregister) and write takes the lock. Network calls (rc_register/heartbeat/ + * deregister) are done OUTSIDE the lock so a slow POST never blocks the table. */ +typedef struct { + char model_id[RW_MODEL_ID_MAX]; + char instance_id[RW_INSTANCE_ID_MAX]; +} sr_instance; + +static sr_instance g_instances[MR_MAX_MODELS]; +static int g_n_instances = 0; +static mr_registry *g_reg = NULL; /* borrowed (process-lifetime) */ +static char g_host[100] = ""; /* set by serve_registry_start */ +static int g_port = 0; +static pthread_mutex_t g_lock = PTHREAD_MUTEX_INITIALIZER; + +static const char *env_or(const char *name, const char *fallback) +{ + const char *v = getenv(name); + return (v && v[0]) ? v : fallback; +} + +/* Register one model (by served name) with the dispatch registry and record it + * for heartbeat/deregister. Idempotent: a model whose instance_id is already in + * the table is skipped (so loading a second replica of the same name — same + * host:port:model — does not double-register). Safe to call from any thread once + * g_reg/g_host/g_port are set. */ +static void sr_register_model(const char *name) +{ + if (!g_reg || !name || !*name) + return; + + rc_identity id = {0}; + snprintf(id.model_id, sizeof id.model_id, "%s", name); + snprintf(id.instance_id, sizeof id.instance_id, "%s:%d:%s", g_host, g_port, name); + snprintf(id.node_id, sizeof id.node_id, "%s", env_or("PANTHEON_NODE_ID", "node0")); + snprintf(id.endpoint, sizeof id.endpoint, "http://%s:%d", g_host, g_port); + snprintf(id.transport, sizeof id.transport, "%s", + env_or("PANTHEON_REGISTRY_TRANSPORT", RW_TRANSPORT_LOCAL)); + + /* Dedupe + capacity guard (quick, under lock). */ + pthread_mutex_lock(&g_lock); + for (int i = 0; i < g_n_instances; i++) { + if (strcmp(g_instances[i].instance_id, id.instance_id) == 0) { + pthread_mutex_unlock(&g_lock); + return; /* already registered (e.g. a replica of the same name) */ + } + } + if (g_n_instances >= MR_MAX_MODELS) { + pthread_mutex_unlock(&g_lock); + return; + } + pthread_mutex_unlock(&g_lock); + + /* capacity = served concurrency = batch-slot count (1 single-user). */ + bs_load load = {0}; + mr_entry *entry = mr_acquire(g_reg, name); + if (entry) { + bs_get_load(entry->sched, &load); + mr_release(g_reg, entry); + } else { + load.capacity = 1; + } + id.capacity = load.capacity; + + /* Network register OUTSIDE the lock. */ + if (!rc_register(&id)) { + fprintf(stderr, "serve: WARNING failed to register '%s' with dispatch registry " + "(will rely on next heartbeat / TTL)\n", id.model_id); + return; + } + + /* Append under the lock (re-check dedupe against a concurrent add). */ + pthread_mutex_lock(&g_lock); + for (int i = 0; i < g_n_instances; i++) { + if (strcmp(g_instances[i].instance_id, id.instance_id) == 0) { + pthread_mutex_unlock(&g_lock); + return; + } + } + if (g_n_instances < MR_MAX_MODELS) { + snprintf(g_instances[g_n_instances].model_id, + sizeof g_instances[g_n_instances].model_id, "%s", id.model_id); + snprintf(g_instances[g_n_instances].instance_id, + sizeof g_instances[g_n_instances].instance_id, "%s", id.instance_id); + g_n_instances++; + fprintf(stderr, "serve: registered '%s' (instance %s, cap %d) with dispatch registry\n", + id.model_id, id.instance_id, id.capacity); + } + pthread_mutex_unlock(&g_lock); +} + +/* Background heartbeat: every RW_HEARTBEAT_SECONDS snapshot the instance table + * under the lock, then (lock released) for each instance re-acquire its model + * entry, read live load from its batch_sched, and POST a heartbeat. */ +static void *heartbeat_thread(void *arg) +{ + (void)arg; + for (;;) { + sleep(RW_HEARTBEAT_SECONDS); + + sr_instance snap[MR_MAX_MODELS]; + int n; + pthread_mutex_lock(&g_lock); + n = g_n_instances; + memcpy(snap, g_instances, (size_t)n * sizeof(sr_instance)); + pthread_mutex_unlock(&g_lock); + + for (int i = 0; i < n; i++) { + bs_load load = {0}; + mr_entry *entry = mr_acquire(g_reg, snap[i].model_id); + if (entry) { + bs_get_load(entry->sched, &load); + mr_release(g_reg, entry); + } else { + /* Model unloaded out from under us: report unhealthy so the + * router stops dispatching to it (the TTL sweep also evicts). */ + load.capacity = load.free_slots = 0; + } + rc_load hb = { + .active_slots = load.active_slots, + .queue_depth = load.queue_depth, + .free_slots = load.free_slots, + .healthy = (entry != NULL), + }; + rc_heartbeat(snap[i].instance_id, &hb); + } + } + return NULL; +} + +void serve_registry_start(mr_registry *reg, const char *host, int port) +{ + if (!rc_enabled()) /* PANTHEON_REGISTRY_URL unset ⇒ fully off */ + return; + + g_reg = reg; + g_port = port; + snprintf(g_host, sizeof g_host, "%s", host ? host : "127.0.0.1"); + + mr_info info[MR_MAX_MODELS]; + int n = mr_snapshot(reg, info, MR_MAX_MODELS); + for (int i = 0; i < n; i++) + sr_register_model(info[i].name); + + pthread_mutex_lock(&g_lock); + int registered = g_n_instances; + pthread_mutex_unlock(&g_lock); + if (registered == 0) + return; /* nothing registered ⇒ no heartbeat thread */ + + pthread_t tid; + pthread_attr_t attr; + if (pthread_attr_init(&attr) != 0) { + fprintf(stderr, "serve: WARNING could not start dispatch heartbeat thread\n"); + return; + } + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + if (pthread_create(&tid, &attr, heartbeat_thread, NULL) != 0) + fprintf(stderr, "serve: WARNING could not start dispatch heartbeat thread\n"); + pthread_attr_destroy(&attr); +} + +void serve_registry_add(const char *model_id) +{ + if (!rc_enabled() || !g_reg) /* not started / disabled ⇒ no-op */ + return; + sr_register_model(model_id); +} + +void serve_registry_remove(const char *model_id) +{ + if (!rc_enabled() || !g_reg || !model_id || !*model_id) + return; + + /* Only deregister when NO replica of this name remains loaded — unloading one + * of two same-name replicas must keep the shared instance registered (the + * serve still serves that name). */ + mr_entry *e = mr_acquire(g_reg, model_id); + if (e) { + mr_release(g_reg, e); + return; + } + + char inst[RW_INSTANCE_ID_MAX] = ""; + pthread_mutex_lock(&g_lock); + int found = -1; + for (int i = 0; i < g_n_instances; i++) { + if (strcmp(g_instances[i].model_id, model_id) == 0) { + found = i; + snprintf(inst, sizeof inst, "%s", g_instances[i].instance_id); + break; + } + } + if (found >= 0) { + g_instances[found] = g_instances[g_n_instances - 1]; /* swap-remove */ + g_n_instances--; + } + pthread_mutex_unlock(&g_lock); + + if (found >= 0) + rc_deregister(inst); /* best-effort, outside the lock */ +} + +void serve_registry_deregister_all(void) +{ + if (!rc_enabled()) + return; + + sr_instance snap[MR_MAX_MODELS]; + int n; + pthread_mutex_lock(&g_lock); + n = g_n_instances; + memcpy(snap, g_instances, (size_t)n * sizeof(sr_instance)); + pthread_mutex_unlock(&g_lock); + + for (int i = 0; i < n; i++) + rc_deregister(snap[i].instance_id); /* best-effort */ +} diff --git a/src/serve/serve_registry.h b/src/serve/serve_registry.h new file mode 100644 index 0000000..dccdc94 --- /dev/null +++ b/src/serve/serve_registry.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_registry.h + * @brief Serve-side dispatch self-registration lifecycle (Phase G0 wiring). + * Ties the register / heartbeat / deregister client (registry_client.h) + * into the live serve process: one register per served model_id at + * startup, a background heartbeat thread reporting live load from + * batch_sched, and a best-effort deregister on controlled shutdown. + * + * FULLY GATED on PANTHEON_REGISTRY_URL (via rc_enabled()): with it unset + * every function here is a no-op and the serve process is byte-for-byte + * unchanged (no registration, no heartbeat thread spawned, no network). + * + * Identity semantics (see registry_wire.h): the shared `model_id` is the + * served name clients request (mr_info.name) — two serve processes + * hosting the same weights advertise the SAME model_id and become a + * router replica pool; the `instance_id` is unique per serve process + * ("::"). + * + * @status G0 wiring. Live-registration behaviour is verified on a K80 host separately + * (no GPU/model on the Mac); the Mac build path exercises only the + * unset-env no-op and the compile. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_REGISTRY_H +#define PN_SERVE_REGISTRY_H + +#include "model_registry.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Startup registration: for each resident model in `reg`, build an rc_identity + * (model_id = served name, unique instance_id = "::", + * endpoint = "http://:", capacity from the model's batch_sched) and + * rc_register() it, recording it for later heartbeat + deregister. Then spawn a + * detached heartbeat thread that every RW_HEARTBEAT_SECONDS reports each + * instance's live load (bs_get_load under mr_acquire/mr_release). + * + * No-op (does not touch `reg`, spawns no thread) when rc_enabled() is false. + * Call once, after hs_listen succeeds and all models are loaded. + * + * `reg`, `host` must outlive the serve process (both are process-lifetime in + * pn_serve_main); the heartbeat thread borrows `reg` and never frees it. + * + * Thread-safe: no (call once at startup, before the accept loop). + */ +void serve_registry_start(mr_registry *reg, const char *host, int port); + +/** + * Register a model loaded AFTER serve_registry_start() (an /admin/models/load), + * using the host/port captured at start. Idempotent: a model whose instance is + * already registered (e.g. a second replica of the same served name) is skipped. + * No-op when rc_enabled() is false or serve_registry_start() has not run. The + * running heartbeat thread picks the new instance up on its next cycle. + * + * Thread-safe: yes (guards the instance table; runs from the request thread). + */ +void serve_registry_add(const char *model_id); + +/** + * Deregister a model unloaded via /admin/models/unload — but only if no replica + * of the same served name remains loaded (unloading one of two same-name + * replicas keeps the shared instance registered). No-op when disabled/unstarted. + * + * Thread-safe: yes. + */ +void serve_registry_remove(const char *model_id); + +/** + * Best-effort deregister of every instance registered by + * serve_registry_start(). Called from the SIGINT/SIGTERM shutdown handler so a + * clean stop drops the instances from the router immediately (the registry TTL + * also evicts them if this is skipped). No-op when rc_enabled() is false or + * nothing was registered. Each rc_deregister uses a short connect timeout. + * + * Safe to call from the controlled-shutdown signal handler (same trade as the + * CUDA teardown there: operator-initiated stop, not a crash mid-kernel). + */ +void serve_registry_deregister_all(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_SERVE_REGISTRY_H */ diff --git a/src/serve/serve_shutdown.c b/src/serve/serve_shutdown.c new file mode 100644 index 0000000..de558cd --- /dev/null +++ b/src/serve/serve_shutdown.c @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_shutdown.c + * @brief Implementation of the deferred-shutdown state (see serve_shutdown.h). + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "serve_shutdown.h" + +#include +#include +#include +#include +#include +#include + +/* Signal-side state. Every one of these is written from a handler, so each is + * `volatile sig_atomic_t` and nothing else — no strings, no counters wider than + * the type the standard promises is atomic against a signal. */ +static volatile sig_atomic_t g_shutdown_requested = 0; +static volatile sig_atomic_t g_shutdown_escalated = 0; +static volatile sig_atomic_t g_shutdown_signal = 0; + +/* The self-pipe. Only the write end is touched from a handler. */ +static int g_wake_read_fd = -1; +static int g_wake_write_fd = -1; + +/* In-flight accelerator-driving sections. A plain int under a mutex rather than + * an atomic, because the drain has to read the count and the shutdown flag as + * ONE decision — an entry that slipped past the flag check must already be in + * the count by the time the drain looks. */ +static pthread_mutex_t g_generation_lock = PTHREAD_MUTEX_INITIALIZER; +static int g_generation_inflight = 0; + +/* Mark a descriptor close-on-exec and non-blocking. The write end must never + * block: a handler that blocks on a full pipe is a hung process, and one queued + * byte already carries the whole message. */ +static int shutdown_prepare_fd(int fd) +{ + int flags = fcntl(fd, F_GETFL, 0); + + if (flags < 0 || fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) + return -1; + flags = fcntl(fd, F_GETFD, 0); + if (flags < 0 || fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) + return -1; + return 0; +} + +int pn_serve_shutdown_arm(void) +{ + int fds[2]; + + if (g_wake_read_fd >= 0) + return 0; + if (pipe(fds) != 0) + return -1; + if (shutdown_prepare_fd(fds[0]) != 0 || shutdown_prepare_fd(fds[1]) != 0) { + close(fds[0]); + close(fds[1]); + return -1; + } + g_wake_read_fd = fds[0]; + g_wake_write_fd = fds[1]; + return 0; +} + +void pn_serve_shutdown_note_signal(int sig) +{ + if (g_shutdown_requested) + g_shutdown_escalated = 1; + else + g_shutdown_signal = sig; + g_shutdown_requested = 1; + + /* One byte is the whole message, and a short/failed write is not worth + * handling: if the pipe already holds a byte the accept loop is about to + * wake anyway, and there is nothing a handler could do about EAGAIN. */ + if (g_wake_write_fd >= 0) { + const char token = 1; + ssize_t written = write(g_wake_write_fd, &token, 1); + (void)written; + } +} + +int pn_serve_shutdown_wake_fd(void) +{ + return g_wake_read_fd; +} + +int pn_serve_shutdown_requested(void) +{ + return g_shutdown_requested ? 1 : 0; +} + +int pn_serve_shutdown_escalated(void) +{ + return g_shutdown_escalated ? 1 : 0; +} + +int pn_serve_shutdown_signal_number(void) +{ + return (int)g_shutdown_signal; +} + +unsigned pn_serve_shutdown_drain_budget_ms(void) +{ + const char *raw = getenv("PN_SERVE_DRAIN_MS"); + char *end = NULL; + long value; + + if (!raw || !*raw) + return PN_SERVE_DRAIN_DEFAULT_MS; + /* strtol, not atoi: a typo'd budget must fall back to the default rather + * than silently parse to 0 and skip the drain entirely. */ + value = strtol(raw, &end, 10); + if (end == raw || *end != '\0' || value < 0) + return PN_SERVE_DRAIN_DEFAULT_MS; + return (unsigned)value; +} + +int pn_serve_generation_enter(void) +{ + int entered; + + pthread_mutex_lock(&g_generation_lock); + entered = g_shutdown_requested ? 0 : 1; + if (entered) + g_generation_inflight++; + pthread_mutex_unlock(&g_generation_lock); + return entered ? 0 : -1; +} + +void pn_serve_generation_leave(void) +{ + pthread_mutex_lock(&g_generation_lock); + if (g_generation_inflight > 0) + g_generation_inflight--; + pthread_mutex_unlock(&g_generation_lock); +} + +int pn_serve_generation_inflight(void) +{ + int count; + + pthread_mutex_lock(&g_generation_lock); + count = g_generation_inflight; + pthread_mutex_unlock(&g_generation_lock); + return count; +} + +void pn_serve_shutdown_reset_for_test(void) +{ + if (g_wake_read_fd >= 0) { + close(g_wake_read_fd); + g_wake_read_fd = -1; + } + if (g_wake_write_fd >= 0) { + close(g_wake_write_fd); + g_wake_write_fd = -1; + } + g_shutdown_requested = 0; + g_shutdown_escalated = 0; + g_shutdown_signal = 0; + pthread_mutex_lock(&g_generation_lock); + g_generation_inflight = 0; + pthread_mutex_unlock(&g_generation_lock); +} + +pn_shutdown_drain_decision +pn_serve_shutdown_drain_decision(int inflight_generations, + unsigned elapsed_ms, + unsigned budget_ms) +{ + /* Quiescence is checked FIRST and unconditionally. Ordering the deadline + * ahead of it would abandon the retirement on a serve that had already + * finished its last turn, which is the one case where retiring is both safe + * and the entire point. */ + if (inflight_generations == 0) + return PN_SHUTDOWN_DRAIN_RETIRE; + if (elapsed_ms >= budget_ms) + return PN_SHUTDOWN_DRAIN_ABANDON; + return PN_SHUTDOWN_DRAIN_WAIT; +} diff --git a/src/serve/serve_shutdown.h b/src/serve/serve_shutdown.h new file mode 100644 index 0000000..baee32d --- /dev/null +++ b/src/serve/serve_shutdown.h @@ -0,0 +1,195 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_shutdown.h + * @brief Deferred-shutdown state for `pantheon serve`: what a SIGINT/SIGTERM + * handler is allowed to record, how the accept loop observes it, and the + * drain decision that says whether an accelerator teardown may run. + * + * WHY THIS EXISTS. Retiring a Tenstorrent residency means asking the + * resident kernel to stop and then POLLING the card until every enabled + * processor reports EXIT (pn_tt_residency_stop). That is device I/O, a + * bounded spin, and a `pn_model_free` that takes the registry lock — + * none of which a signal handler may do, because a handler runs at an + * arbitrary point in the program. Calling it from the handler would be + * worse than the gap it closes: it can retire a residency out from under + * a connection thread that is mid-descriptor, and `pn_tt_device_close` + * then frees the device that thread is still dereferencing. + * + * So the handler does the two things that ARE async-signal-safe — store + * to a `volatile sig_atomic_t` and `write(2)` one byte to a self-pipe — + * and the accept loop performs the teardown at a point where the + * program's state is known. The pipe (rather than only relying on + * accept() returning EINTR) is what makes this correct in a threaded + * serve: the kernel may deliver the signal to ANY thread that has it + * unblocked, and a connection thread running the handler still wakes the + * accept loop through the pipe. + * + * MID-GENERATION. A turn already on the card is not interrupted; it is + * ASKED to stop, through the `sg_should_abort_fn` the generation loop + * already polls between prefill chunks and every 8 decode tokens. The + * accept loop then waits for the in-flight count to reach zero before + * anything is retired. That is the "an abort must DRAIN, not return" + * rule (foundry/tt/DEV_NOTES.md §1) applied one level out, at the + * process boundary. + * + * ON TIMEOUT. If the drain does not quiesce, the teardown is ABANDONED, + * not forced: tearing down under a live thread is the failure mode this + * module exists to avoid. What is left is exactly what a SIGKILL leaves, + * and that is already covered — SET_NOC_CLEANUP and recovery-on-acquire. + * + * @status Complete. Decision logic is pure and unit-tested off-hardware + * (tests/test_serve_shutdown.c); the retirement it gates is device-side. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_SHUTDOWN_H +#define PN_SERVE_SHUTDOWN_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Default drain budget. Chosen against three measured numbers, not by feel: + * the cooperative abort is polled every 8 decode tokens and every 8 prefill + * chunks, which on the slowest backend in the fleet is ~2 s; a residency + * descriptor's own device timeout is 60 s, so a budget below that turns a + * WEDGED card into a bounded abandon instead of a hung shutdown; and an + * operator stop has to feel like a stop. Override with PN_SERVE_DRAIN_MS + * (0 = never wait — the escalation path and the card-side red proof use it). */ +#define PN_SERVE_DRAIN_DEFAULT_MS 10000u + +/* What the accept loop should do with an in-progress drain. */ +typedef enum { + PN_SHUTDOWN_DRAIN_WAIT = 0, /* work in flight, budget left — keep waiting */ + PN_SHUTDOWN_DRAIN_RETIRE, /* quiesced — the accelerator teardown may run */ + PN_SHUTDOWN_DRAIN_ABANDON, /* budget spent with work still in flight — + * do NOT retire under a live thread */ +} pn_shutdown_drain_decision; + +/* Create the self-pipe the handler writes and the accept loop polls. Call ONCE, + * before pn_install_signal_handlers(). Returns 0 on success, -1 if the pipe + * could not be created — in which case pn_serve_shutdown_wake_fd() stays -1 and + * the handler falls back to its pre-drain behaviour rather than leaving the + * process unkillable. + * + * Thread-safe: no (call once at startup) + * Reentrant: no + * Signal-safe: no */ +int pn_serve_shutdown_arm(void); + +/* Record a shutdown request. THE ONLY FUNCTION A SIGNAL HANDLER MAY CALL — it + * stores to `volatile sig_atomic_t` and write(2)s one byte, both of which are + * on POSIX's async-signal-safe list, and does nothing else. A second call marks + * the request ESCALATED (a second Ctrl-C / SIGTERM), which collapses the drain + * budget to zero at the next poll instead of aborting the process from a + * handler. + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: yes */ +void pn_serve_shutdown_note_signal(int sig); + +/* Read end of the self-pipe, or -1 if arming failed. Poll it alongside the + * listening socket; one readable byte means "a shutdown was requested". + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: yes */ +int pn_serve_shutdown_wake_fd(void); + +/* Non-zero once a shutdown has been requested. Read by worker threads through + * their abort predicate, and by the accept loop. + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: yes */ +int pn_serve_shutdown_requested(void); + +/* Non-zero once a SECOND shutdown signal has arrived. + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: yes */ +int pn_serve_shutdown_escalated(void); + +/* The signal number that requested the shutdown (0 if none). The first one + * wins, so the exit status reflects what the operator actually sent. + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: yes */ +int pn_serve_shutdown_signal_number(void); + +/* The drain budget in milliseconds: PN_SERVE_DRAIN_MS when it parses as a + * non-negative integer, PN_SERVE_DRAIN_DEFAULT_MS otherwise. + * + * Thread-safe: yes + * Reentrant: no (getenv) + * Signal-safe: no */ +unsigned pn_serve_shutdown_drain_budget_ms(void); + +/* Enter the accelerator-driving section of a request. Returns 0 when entered — + * the caller MUST pair it with pn_serve_generation_leave() — or -1 when a + * shutdown has already been requested, in which case the route must refuse the + * request instead of starting one. The check and the increment happen under one + * lock, so a turn that gets in is always visible to the drain. + * + * Thread-safe: yes + * Reentrant: no + * Signal-safe: no */ +int pn_serve_generation_enter(void); + +/* Leave the accelerator-driving section. Pairs with a successful enter. + * + * Thread-safe: yes + * Reentrant: no + * Signal-safe: no */ +void pn_serve_generation_leave(void); + +/* How many accelerator-driving sections are currently in flight. + * + * Thread-safe: yes + * Reentrant: no + * Signal-safe: no */ +int pn_serve_generation_inflight(void); + +/* Reset every bit of module state. FOR TESTS ONLY — a serve process arms once + * and shuts down once. + * + * Thread-safe: no + * Reentrant: no + * Signal-safe: no */ +void pn_serve_shutdown_reset_for_test(void); + +/** + * Decide what an in-progress drain should do next. PURE — no clock, no globals, + * no device — so the policy this whole mechanism turns on is testable without a + * card, a serve or a signal. + * + * @param inflight_generations accelerator-driving sections still running; a + * negative value (unknown) counts as "in flight" + * @param elapsed_ms milliseconds since the drain began + * @param budget_ms the drain budget (0 = do not wait at all) + * @return WAIT, RETIRE or ABANDON + * + * QUIESCENCE WINS OVER THE DEADLINE. `inflight == 0` returns RETIRE even at or + * past the budget: a serve that finished its last turn in the same millisecond + * the budget expired is safe to tear down, and answering ABANDON there would + * throw away the retirement this mechanism exists to perform. + * + * Thread-safe: yes + * Reentrant: yes + * Signal-safe: yes + */ +pn_shutdown_drain_decision +pn_serve_shutdown_drain_decision(int inflight_generations, + unsigned elapsed_ms, + unsigned budget_ms); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_SERVE_SHUTDOWN_H */ diff --git a/src/serve/serve_spec_lookup.c b/src/serve/serve_spec_lookup.c new file mode 100644 index 0000000..48492d6 --- /dev/null +++ b/src/serve/serve_spec_lookup.c @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_spec_lookup.c + * @brief Prompt-lookup drafts for the serve's speculative decode. See serve_spec_lookup.h. + * + * @status Working off-hardware (tests/test_serve_spec_lookup.c). + * @issues None + * @todo None + */ + +#include "serve_spec_lookup.h" + +#include +#include +#include + +/** Longest suffix tried first (higher precision). */ +#define LOOKUP_NGRAM_MAX 6 + +/** The throttle: a lookup cycle accepting fewer drafts than this is a miss; after this many + * misses in a row lookup rests for this many cycles (the MTP head drafts meanwhile). */ +#define LOOKUP_MIN_ACCEPT 3 +#define LOOKUP_MISS_LIMIT 2 +#define LOOKUP_COOL_CYCLES 16 + +static int lookup_env(const char *name, int fallback) +{ + const char *value = getenv(name); + + return value && *value ? atoi(value) : fallback; +} + +int sg_spec_lookup_init(sg_spec_lookup *lookup, const uint32_t *prompt, int n_prompt, + int max_new) +{ + memset(lookup, 0, sizeof(*lookup)); + lookup->mode = lookup_env("PN_TT_SPEC_LOOKUP", 2); + if (lookup->mode <= 0 || !prompt || n_prompt < 0 || max_new < 0) + return 0; + lookup->k = lookup_env("PN_TT_SPEC_LOOKUP_K", 6); + lookup->min_ngram = lookup_env("PN_TT_SPEC_LOOKUP_MIN", 3); + if (lookup->k < 1 || lookup->min_ngram < 1) + return 0; + if (lookup->k > 15) + lookup->k = 15; + lookup->capacity = n_prompt + max_new + 16; + lookup->history = malloc((size_t)lookup->capacity * sizeof(*lookup->history)); + if (!lookup->history) + return -1; + memcpy(lookup->history, prompt, (size_t)n_prompt * sizeof(*prompt)); + lookup->count = n_prompt; + pn_spec_throttle_init(&lookup->throttle, LOOKUP_MIN_ACCEPT, LOOKUP_MISS_LIMIT, + LOOKUP_COOL_CYCLES); + lookup->on = true; + return 0; +} + +bool sg_spec_lookup_agrees(const sg_spec_lookup *lookup, uint32_t looked_first, + uint32_t mtp_first) +{ + return lookup->mode != 2 || looked_first == mtp_first; +} + +void sg_spec_lookup_push(sg_spec_lookup *lookup, uint32_t token) +{ + if (!lookup->on) + return; + if (lookup->count == lookup->capacity) { + uint32_t *grown = realloc(lookup->history, + (size_t)lookup->capacity * 2u * sizeof(*grown)); + + if (!grown) { + lookup->on = false; + return; + } + lookup->history = grown; + lookup->capacity *= 2; + } + lookup->history[lookup->count++] = token; +} + +int sg_spec_lookup_draft(sg_spec_lookup *lookup, int cap, uint32_t *drafts) +{ + const int want = cap < lookup->k ? cap : lookup->k; + const int longest = LOOKUP_NGRAM_MAX > lookup->min_ngram ? LOOKUP_NGRAM_MAX + : lookup->min_ngram; + + if (!lookup->on || want < 1 || !pn_spec_throttle_gate(&lookup->throttle)) + return 0; + return pn_ngram_draft(lookup->history, lookup->count, longest, lookup->min_ngram, want, + drafts); +} + +void sg_spec_lookup_report(sg_spec_lookup *lookup, int drafted, int accepted) +{ + lookup->cycles++; + lookup->drafted += (uint64_t)drafted; + lookup->accepted += (uint64_t)accepted; + pn_spec_throttle_report(&lookup->throttle, accepted); +} + +void sg_spec_lookup_free(sg_spec_lookup *lookup) +{ + if (lookup->cycles) + fprintf(stderr, "serve: LOOKUP — %llu cycles, %llu drafted, %llu accepted (%.2f)\n", + (unsigned long long)lookup->cycles, (unsigned long long)lookup->drafted, + (unsigned long long)lookup->accepted, + lookup->drafted ? (double)lookup->accepted / (double)lookup->drafted : 0.0); + free(lookup->history); + memset(lookup, 0, sizeof(*lookup)); +} diff --git a/src/serve/serve_spec_lookup.h b/src/serve/serve_spec_lookup.h new file mode 100644 index 0000000..02070c5 --- /dev/null +++ b/src/serve/serve_spec_lookup.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_spec_lookup.h + * @brief Prompt-lookup drafts for the serve's speculative decode: the request's own context + * (prompt + everything emitted) searched for the current suffix, its continuation + * proposed ahead of the model's MTP drafts. + * + * Training-free and host-only (`pn_ngram_draft`, microseconds a cycle). It pays where the + * answer repeats its context — code edits, quoted text, summaries of a long turn, a re-asked + * multi-turn question — and proposes nothing on free text, where the MTP head drafts instead. + * Every draft is verified by the model before it is emitted, so the output is the plain + * decode's whatever is drafted. + * + * Gating: a draft needs a suffix match of at least `min_ngram` tokens; the throttle + * (`pn_spec_throttle`) stops proposing after lookup cycles that accept too little and probes + * again later. + * + * Env: `PN_TT_SPEC_LOOKUP` (2 = the DEFAULT, David 2026-10-07: only when its first draft is the + * MTP head's first draft, which the catch-up computes anyway; 1 lookup whenever it matches; + * 0 off), `PN_TT_SPEC_LOOKUP_K` (drafts a lookup cycle, default 6), `PN_TT_SPEC_LOOKUP_MIN` (shortest suffix match, default 3). + * + * status: written for the Tenstorrent serve; model-agnostic. + * issues: none. + * todo: none. + */ +#ifndef PN_SERVE_SPEC_LOOKUP_H +#define PN_SERVE_SPEC_LOOKUP_H + +#include +#include + +#include "../core/ngram_draft.h" + +typedef struct { + bool on; + int mode; /**< PN_TT_SPEC_LOOKUP: 1 matched, 2 matched and agreeing with MTP */ + uint32_t *history; /**< prompt + every emitted token, oldest first */ + int count, capacity; + int k; /**< drafts a lookup cycle proposes at most */ + int min_ngram; /**< shortest suffix match that may draft */ + pn_spec_throttle throttle; + uint64_t cycles, drafted, accepted; /**< lookup cycles only */ +} sg_spec_lookup; + +/** + * Read the env and copy the prompt. Off (no allocation) when `PN_TT_SPEC_LOOKUP=0`. + * @param max_new tokens the request may still emit (sizes the history once) + * @return 0, or -1 on OOM (left off) + */ +int sg_spec_lookup_init(sg_spec_lookup *lookup, const uint32_t *prompt, int n_prompt, + int max_new); + +/** Append one emitted token to the history. */ +void sg_spec_lookup_push(sg_spec_lookup *lookup, uint32_t token); + +/** + * Drafts for the token after the history's end, at most min(k, cap). + * @return the number written to `drafts`; 0 when off, throttled or no match long enough + */ +int sg_spec_lookup_draft(sg_spec_lookup *lookup, int cap, uint32_t *drafts); + +/** May lookup drafts starting with `looked_first` replace the MTP drafts whose first is + * `mtp_first`? Always in mode 1; only when they agree in mode 2. */ +bool sg_spec_lookup_agrees(const sg_spec_lookup *lookup, uint32_t looked_first, + uint32_t mtp_first); + +/** A lookup cycle's outcome: `drafted` proposed, `accepted` of them kept. */ +void sg_spec_lookup_report(sg_spec_lookup *lookup, int drafted, int accepted); + +/** Free the history and print the tallies once when any lookup cycle ran. */ +void sg_spec_lookup_free(sg_spec_lookup *lookup); + +#endif /* PN_SERVE_SPEC_LOOKUP_H */ diff --git a/src/serve/session_affinity.c b/src/serve/session_affinity.c new file mode 100644 index 0000000..9237097 --- /dev/null +++ b/src/serve/session_affinity.c @@ -0,0 +1,115 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_affinity.c + * @brief Implementation of the session -> instance affinity map (see + * session_affinity.h). Pure C, one mutex, fixed-size table, LRU eviction + * — the same shape as registry.c. No new deps. + * + * @status PC0.2 complete. + * @issues None + * @todo None + */ + +#include "session_affinity.h" + +#include +#include + +int sa_init(session_affinity *sa) +{ + memset(sa, 0, sizeof(*sa)); + if (pthread_mutex_init(&sa->mu, NULL) != 0) + return -1; + return 0; +} + +void sa_destroy(session_affinity *sa) +{ + if (!sa) + return; + pthread_mutex_destroy(&sa->mu); +} + +/* Find the row index for a session_id, or -1. Caller holds the lock. */ +static int sa_find(const session_affinity *sa, const char *session_id) +{ + for (int i = 0; i < sa->n; i++) + if (strcmp(sa->ent[i].session_id, session_id) == 0) + return i; + return -1; +} + +/* Index of the least-recently-used row (smallest seq). Caller holds the lock; + * only called when the table is full, so n >= 1. */ +static int sa_lru_index(const session_affinity *sa) +{ + int lru = 0; + for (int i = 1; i < sa->n; i++) + if (sa->ent[i].seq < sa->ent[lru].seq) + lru = i; + return lru; +} + +bool sa_lookup(session_affinity *sa, const char *session_id, + char *out, size_t out_cap) +{ + if (!sa || !session_id || !session_id[0] || !out || out_cap == 0) + return false; + + bool found = false; + pthread_mutex_lock(&sa->mu); + int idx = sa_find(sa, session_id); + if (idx >= 0) { + sa->ent[idx].seq = ++sa->clock; /* refresh LRU on every hit */ + snprintf(out, out_cap, "%s", sa->ent[idx].instance_id); + found = true; + } + pthread_mutex_unlock(&sa->mu); + return found; +} + +void sa_record(session_affinity *sa, const char *session_id, + const char *instance_id) +{ + if (!sa || !session_id || !session_id[0] || !instance_id || !instance_id[0]) + return; + + pthread_mutex_lock(&sa->mu); + int idx = sa_find(sa, session_id); + if (idx < 0) { + /* New session: append, or evict the LRU row when the table is full. */ + if (sa->n < SA_MAX_SESSIONS) + idx = sa->n++; + else + idx = sa_lru_index(sa); + snprintf(sa->ent[idx].session_id, sizeof(sa->ent[idx].session_id), + "%s", session_id); + } + snprintf(sa->ent[idx].instance_id, sizeof(sa->ent[idx].instance_id), + "%s", instance_id); + sa->ent[idx].seq = ++sa->clock; + pthread_mutex_unlock(&sa->mu); +} + +int sa_forget_instance(session_affinity *sa, const char *instance_id) +{ + if (!sa || !instance_id || !instance_id[0]) + return 0; + + int removed = 0; + pthread_mutex_lock(&sa->mu); + int i = 0; + while (i < sa->n) { + if (strcmp(sa->ent[i].instance_id, instance_id) == 0) { + sa->ent[i] = sa->ent[sa->n - 1]; /* swap-remove; order irrelevant */ + sa->n--; + removed++; + /* do not advance i: the swapped-in row must be checked too */ + } else { + i++; + } + } + pthread_mutex_unlock(&sa->mu); + return removed; +} diff --git a/src/serve/session_affinity.h b/src/serve/session_affinity.h new file mode 100644 index 0000000..fddbfc9 --- /dev/null +++ b/src/serve/session_affinity.h @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_affinity.h + * @brief Session -> instance affinity map for the dispatch router (PC0.2). + * Remembers which serve instance last handled a given X-Session-Id so a + * multi-turn chat keeps landing on the SAME instance — where the serve's + * prefix cache already holds that session's prefilled KV. Without this, + * least-busy routing can send turn 2 to a different instance (a KV miss = + * a full cold prefill), defeating the prefix cache. + * + * Concurrency: one mutex guards the whole table, matching the registry's design + * (registry.h). Operations are O(N) over a small N; all public calls take/return + * copies so callers never hold pointers into the table across the lock. + * + * Lifecycle: a mapping is recorded on the first least-busy pick for a session and + * refreshed on every hit. It is cleared when its instance is deregistered or + * TTL-evicted (sa_forget_instance) so a dead instance never pins a session. + * + * @status PC0.2 complete: record/lookup/forget, mutex-guarded fixed table. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_SESSION_AFFINITY_H +#define PN_SERVE_SESSION_AFFINITY_H + +#include +#include +#include + +#include "registry_wire.h" + +/* Max distinct sessions tracked. A session entry is tiny; 1024 covers a large + * fleet of concurrent chat tabs without dynamic growth. When full, the oldest + * (least-recently-used) entry is evicted to admit a new session. */ +#define SA_MAX_SESSIONS 1024 + +/* Max session-id length tracked (matches the serve prefix cache's 63+1 key). */ +#define SA_SESSION_ID_MAX 64 + +/* One session -> instance binding. POD row; strings are fixed-size. */ +typedef struct { + char session_id[SA_SESSION_ID_MAX]; + char instance_id[RW_INSTANCE_ID_MAX]; + long seq; /* monotonic use counter for LRU eviction (higher = newer) */ +} sa_entry; + +/* The affinity map. Zero-initialize then sa_init(). */ +typedef struct { + pthread_mutex_t mu; + sa_entry ent[SA_MAX_SESSIONS]; + int n; + long clock; /* monotonic counter stamped onto seq on each touch */ +} session_affinity; + +/** + * Initialize the map (mutex + empty table). + * @return 0 on success, -1 if the mutex could not be created. + * Thread-safe: no (call once at startup, before any worker threads) + */ +int sa_init(session_affinity *sa); + +/** Destroy the map's mutex. Table memory is inline (no frees). NULL-safe. */ +void sa_destroy(session_affinity *sa); + +/** + * Look up the instance a session is currently bound to. On a hit, copies the + * bound instance_id into `out` (size `out_cap`), refreshes the entry's LRU + * stamp, and returns true. Returns false if the session has no binding. + * + * @param session_id the X-Session-Id (NULL/empty ⇒ always a miss) + * @param out buffer for the bound instance_id (>= RW_INSTANCE_ID_MAX) + * @param out_cap size of `out` + * Thread-safe: yes (internal locking) + */ +bool sa_lookup(session_affinity *sa, const char *session_id, + char *out, size_t out_cap); + +/** + * Record (or refresh) a session -> instance binding. Overwrites an existing + * binding for the same session. When the table is full and the session is new, + * evicts the least-recently-used entry to make room. A NULL/empty session_id or + * instance_id is ignored (no-op). + * + * Thread-safe: yes (internal locking) + */ +void sa_record(session_affinity *sa, const char *session_id, + const char *instance_id); + +/** + * Forget every binding pointing at `instance_id`. Called when an instance is + * deregistered or TTL-evicted so a dead instance stops pinning its sessions + * (they re-route least-busy on their next turn). A NULL/empty id is a no-op. + * + * @return number of bindings removed. + * Thread-safe: yes (internal locking) + */ +int sa_forget_instance(session_affinity *sa, const char *instance_id); + +#endif /* PN_SERVE_SESSION_AFFINITY_H */ diff --git a/src/serve/session_key.h b/src/serve/session_key.h new file mode 100644 index 0000000..ded3281 --- /dev/null +++ b/src/serve/session_key.h @@ -0,0 +1,45 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_key.h + * @brief The prefix-cache session key a chat request body carries, under any of + * the names clients actually send. + * + * @status None + * @issues None + * @todo None + */ +/* WHY ALIASES + * Without a session key the serve caches nothing and every turn re-prefills the + * whole conversation, which on a K80 is minutes per turn on a long context. + * Pantheon's own field is `session_id`. OpenAI's API calls the same idea + * `prompt_cache_key`, so clients built on its SDK set that. opencode sends its + * session id in an `x-session-id` header on every request (covered by the header + * fallback) and, with `setCacheKey: true` in its provider config, also as + * `promptCacheKey` in the body. Accepting all three lets those clients hit the + * cache with no Pantheon-specific setup. + * + * Order: `session_id`, then `prompt_cache_key`, then `promptCacheKey`; the first + * non-empty string wins. The X-Session-Id header stays the fallback after all + * three (callers apply it). + */ + +#ifndef PANTHEON_SERVE_SESSION_KEY_H +#define PANTHEON_SERVE_SESSION_KEY_H + +#include "json_reader.h" + +/** The body's session key (borrowed from `root`), or NULL if none is set. */ +static inline const char *pn_body_session_key(const jr_value *root) +{ + static const char *const names[] = { "session_id", "prompt_cache_key", "promptCacheKey" }; + + for (unsigned i = 0; i < sizeof(names) / sizeof(names[0]); i++) { + const char *key = jr_as_string(jr_object_get(root, names[i])); + if (key && key[0]) + return key; + } + return NULL; +} + +#endif diff --git a/src/serve/session_watch.c b/src/serve/session_watch.c new file mode 100644 index 0000000..1f411cc --- /dev/null +++ b/src/serve/session_watch.c @@ -0,0 +1,610 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_watch.c + * @brief Implementation of the liveness-based session-release control plane + * (see session_watch.h). One background thread holds every + * GET /session/watch connection in a single poll() loop; a peer close + * starts a grace timer; a reconnect for the same session cancels it; a + * session whose watch stays closed past the grace is released. Idleness + * is NEVER a release trigger for a watched session — only death is. + * + * A watch is held on a tab's BASE id `client--`, but the live KV + * slots are the mode-suffixed sids `-orch`/`-act`/`-chat` — so a + * base watch PROTECTS THE WHOLE `-*` PREFIX: on death it releases + * every matching mode slot, and while alive it holds them off the 12 h + * backstop (via a watch-hook installed on each model's prefix cache). + * + * @status Built; poll loop + grace reaper CPU-tested, CUDA compile-checked. + * @issues None + * @todo None + */ + +#include "session_watch.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/prefix_cache.h" +#include "http_server.h" +#include "model_session.h" + +#define PN_WATCH_SID_MAX 64 +#define PN_WATCH_NAME_MAX MR_MAX_NAME + +/* One watched session-control connection. `fd < 0` marks a free slot. When a + * watch's connection closes the fd is closed and set to -1 but the row is KEPT + * (with closed_at_ms set) until the grace elapses — so a reconnect within grace + * finds it and cancels the release, and the sweep releases it after. */ +typedef struct { + int fd; /* held client fd, or -1 when closed */ + char name[PN_WATCH_NAME_MAX]; /* model id (routing + logging) */ + char sid[PN_WATCH_SID_MAX]; /* prefix-cache session key */ + mr_entry *entry; /* the manager holds an mr_acquire ref */ + uint64_t closed_at_ms; /* wall-clock ms the watch closed; 0 = + * still held (alive). Nonzero starts + * the grace timer. */ + int used; /* row occupied (fd may be -1 in grace) */ +} pn_watch; + +/* Max concurrent watches. The K80 test machine holds ~14 model-dies; each model caps its + * live sessions (max_sessions) — a generous ceiling covers every session on + * every model holding a watch at once. */ +#define PN_WATCH_MAX 256 + +/* A per-model backstop-hook binding: the ctx handed to each cache's watch-hook. + * Carries the manager plus the model name so pn_session_watch_base_is_watched + * can scope the base match to the right model. Owned by the manager (freed at + * stop). */ +typedef struct { + pn_session_watch_mgr *mgr; + char name[PN_WATCH_NAME_MAX]; +} pn_watch_hook_binding; + +struct pn_session_watch_mgr { + mr_registry *reg; /* borrowed */ + pn_watch watches[PN_WATCH_MAX]; + pthread_mutex_t mu; /* guards the watch table */ + pthread_t thread; + int thread_started; + int stop; /* set under mu; poll thread exits */ + int wake_r, wake_w; /* self-pipe: wakes poll() on change */ + uint64_t grace_ms; + /* Backstop-hook bindings (one per model cache the hook is installed on). + * Borrowed by the caches as their watch-hook ctx; freed at stop. */ + pn_watch_hook_binding *hook_bindings[MR_MAX_MODELS]; + int n_hook_bindings; +}; + +/* Monotonic wall clock in ms — matches oai_now_ms so grace timing is coherent + * with the request-path reaper (both CLOCK_MONOTONIC). */ +static uint64_t watch_now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; +} + +uint64_t pn_session_watch_grace_ms(void) +{ + static uint64_t cached = 0; + static int loaded = 0; + if (!loaded) { + const char *env = getenv("PN_SESSION_WATCH_GRACE_MS"); + if (env && *env) { + char *end = NULL; + long v = strtol(env, &end, 10); + cached = (end != env && *end == '\0' && v >= 0) + ? (uint64_t)v : 45u * 1000u; + } else { + cached = 45u * 1000u; /* 45 seconds */ + } + loaded = 1; + } + return cached; +} + +uint64_t pn_session_watch_backstop_ms(void) +{ + static uint64_t cached = 0; + static int loaded = 0; + if (!loaded) { + const char *env = getenv("PN_SESSION_IDLE_TTL_MS"); + if (env && *env) { + char *end = NULL; + long v = strtol(env, &end, 10); + cached = (end != env && *end == '\0' && v >= 0) + ? (uint64_t)v : 12u * 60u * 60u * 1000u; + } else { + cached = 12u * 60u * 60u * 1000u; /* 12 hours */ + } + loaded = 1; + } + return cached; +} + +/* Release a dead base's live sessions through the session facade, which covers + * BOTH stores under the right lock — the scheduler's warm decode slots on the + * batched path, the prefix cache on the single-user one — so the release never + * races a concurrent generation/admit on that model. (Clearing only the prefix + * cache, what this used to do, freed nothing on a batched model: a dead tab's + * context stayed bound to its decode slot.) Caller must NOT hold mgr->mu (this + * takes a model lock; keep the two lock orders independent — mgr->mu is never + * held across a model lock). + * + * `sid` is the watched BASE id `client--`, but the live KV slots are the + * mode-suffixed sids `-orch`/`-act`/`-chat` — so release the whole + * `-*` prefix, not just an exact-match base entry (which usually does not + * even exist). Also release any exact-match base entry, for a client that keyed + * its live slot on the bare base. */ +static void release_session(mr_entry *entry, const char *sid) +{ + if (!entry || !sid || !sid[0]) + return; + char base_prefix[PN_WATCH_SID_MAX + 1]; + snprintf(base_prefix, sizeof(base_prefix), "%s-", sid); + ms_session_release_by_prefix(entry, base_prefix); + ms_session_release(entry, sid); +} + +/* Wake the poll thread (the table changed). Best-effort one byte on the + * self-pipe; a full pipe already means "wakeup pending", so EAGAIN is fine. */ +static void watch_wake(pn_session_watch_mgr *m) +{ + if (m->wake_w < 0) + return; + char b = 1; + ssize_t n = write(m->wake_w, &b, 1); + (void)n; +} + +/* Find a live-or-in-grace watch row for (name,sid). Caller holds mu. */ +static pn_watch *watch_find(pn_session_watch_mgr *m, const char *name, + const char *sid) +{ + for (int i = 0; i < PN_WATCH_MAX; i++) { + pn_watch *w = &m->watches[i]; + if (w->used && strncmp(w->sid, sid, PN_WATCH_SID_MAX) == 0 && + strncmp(w->name, name, PN_WATCH_NAME_MAX) == 0) + return w; + } + return NULL; +} + +/* Is `entry_sid` (a live prefix-cache slot, e.g. the mode-suffixed + * `client---orch`) covered by a live-or-in-grace watch on model `name`? + * A watch is held on the BASE id `client--`; it covers `entry_sid` when + * entry_sid == "-" — i.e. entry_sid starts with the watch's + * sid followed by '-'. An exact base match also counts (a slot keyed on the bare + * base). Takes mu; the caller must NOT hold it. Used by the backstop reaper's + * watch-hook so a watched base's mode slots are never idle-reaped. */ +int pn_session_watch_base_is_watched(pn_session_watch_mgr *m, const char *name, + const char *entry_sid) +{ + if (!m || !name || !entry_sid || !entry_sid[0]) + return 0; + int watched = 0; + pthread_mutex_lock(&m->mu); + for (int i = 0; i < PN_WATCH_MAX; i++) { + pn_watch *w = &m->watches[i]; + if (!w->used || strncmp(w->name, name, PN_WATCH_NAME_MAX) != 0) + continue; + size_t blen = strnlen(w->sid, PN_WATCH_SID_MAX); + if (blen == 0) + continue; + /* Exact base, or entry_sid == "-". */ + if (strncmp(entry_sid, w->sid, blen) == 0 && + (entry_sid[blen] == '\0' || entry_sid[blen] == '-')) { + watched = 1; + break; + } + } + pthread_mutex_unlock(&m->mu); + return watched; +} + +/* Cache watch-hook adapter (matches pn_session_watched_fn): resolve the binding + * and ask "is this slot's base watched on this model?". */ +static int watch_hook_adapter(const char *entry_sid, void *ctx) +{ + pn_watch_hook_binding *b = ctx; + if (!b) + return 0; + return pn_session_watch_base_is_watched(b->mgr, b->name, entry_sid); +} + +void pn_session_watch_install_backstop_hooks(pn_session_watch_mgr *m) +{ + if (!m || !m->reg) + return; + pthread_mutex_lock(&m->reg->lock); + for (int i = 0; i < MR_MAX_MODELS; i++) { + mr_entry *e = &m->reg->entries[i]; + if (!e->used || !e->prefix_cache) + continue; + if (m->n_hook_bindings >= MR_MAX_MODELS) + break; + pn_watch_hook_binding *b = calloc(1, sizeof(*b)); + if (!b) + continue; /* OOM: this cache just keeps the default (no hook) */ + b->mgr = m; + snprintf(b->name, sizeof(b->name), "%s", e->name); + pn_prefix_cache_set_watch_hook(e->prefix_cache, watch_hook_adapter, b); + m->hook_bindings[m->n_hook_bindings++] = b; + } + pthread_mutex_unlock(&m->reg->lock); +} + +/* Drop a watch row entirely: close its fd if still open, release the manager's + * registry reference, clear the row. Caller holds mu. Does NOT release the + * session (that is the sweep's decision) unless `release` is set. */ +static void watch_drop(pn_session_watch_mgr *m, pn_watch *w, int release) +{ + if (!w->used) + return; + if (release) + release_session(w->entry, w->sid); + if (w->fd >= 0) { + close(w->fd); + w->fd = -1; + } + if (w->entry) + mr_release(m->reg, w->entry); + w->entry = NULL; + w->used = 0; + w->closed_at_ms = 0; + w->sid[0] = '\0'; + w->name[0] = '\0'; +} + +/* Mark a watch's connection closed (peer hung up): close the fd, start the + * grace timer, keep the row so a reconnect within grace cancels the release. + * Caller holds mu. */ +static void watch_mark_closed(pn_watch *w, uint64_t now_ms) +{ + if (w->fd >= 0) { + close(w->fd); + w->fd = -1; + } + if (w->closed_at_ms == 0) + w->closed_at_ms = now_ms ? now_ms : 1; /* nonzero = grace running */ +} + +/* Grace sweep: release any watch closed longer than the grace ago (no + * reconnect cancelled it). Runs under mu for the table scan; the actual + * pn_prefix_cache_release_session happens on a snapshot AFTER dropping mu so + * the model lock is never taken under mgr->mu. Returns nothing. */ +static void watch_sweep(pn_session_watch_mgr *m, uint64_t now_ms) +{ + /* Collect the (entry,sid) to release + rows to clear under mu, then act + * without mu held (release_session takes a model lock). */ + struct { mr_entry *entry; char sid[PN_WATCH_SID_MAX]; int idx; } + due[PN_WATCH_MAX]; + int n_due = 0; + + pthread_mutex_lock(&m->mu); + for (int i = 0; i < PN_WATCH_MAX; i++) { + pn_watch *w = &m->watches[i]; + if (!w->used || w->closed_at_ms == 0) + continue; + if (now_ms >= w->closed_at_ms && + now_ms - w->closed_at_ms >= m->grace_ms) { + due[n_due].entry = w->entry; + snprintf(due[n_due].sid, sizeof(due[n_due].sid), "%s", w->sid); + due[n_due].idx = i; + n_due++; + /* Detach the row from the manager's reference bookkeeping here but + * defer the mr_release + session release to outside mu. Mark the + * row as "releasing" by clearing fd only; the row stays used until + * we finish so a concurrent add for the same key still finds it and + * is handled deterministically (it will fail to match a live fd and + * we clear below). Simplest correct approach: clear the row now + * (used=0) and stash entry for release — a concurrent add creates a + * fresh row. */ + fprintf(stderr, "[session_watch] releasing sid=%s model=%s " + "(watch closed %llums ago >= grace %llums)\n", + w->sid, w->name, + (unsigned long long)(now_ms - w->closed_at_ms), + (unsigned long long)m->grace_ms); + w->used = 0; + w->closed_at_ms = 0; + w->entry = NULL; /* ownership of the ref moves to `due` */ + w->sid[0] = '\0'; + w->name[0] = '\0'; + } + } + pthread_mutex_unlock(&m->mu); + + for (int i = 0; i < n_due; i++) { + release_session(due[i].entry, due[i].sid); + if (due[i].entry) + mr_release(m->reg, due[i].entry); + } +} + +void pn_session_watch_sweep_for_test(pn_session_watch_mgr *m, uint64_t now_ms) +{ + if (m) + watch_sweep(m, now_ms); +} + +int pn_session_watch_mark_closed_for_test(pn_session_watch_mgr *m, + const char *name, const char *sid, + uint64_t now_ms) +{ + if (!m || !name || !sid) + return -1; + pthread_mutex_lock(&m->mu); + pn_watch *w = watch_find(m, name, sid); + if (w) + watch_mark_closed(w, now_ms); + pthread_mutex_unlock(&m->mu); + return w ? 0 : -1; +} + +/* The poll loop: hold every open watch fd + the self-pipe. Readability on a + * watch fd means the peer closed (a GET /session/watch client sends no body) — + * mark it closed and start its grace. The timeout drives the periodic grace + * sweep. */ +static void *watch_thread(void *arg) +{ + pn_session_watch_mgr *m = arg; + struct pollfd pfds[PN_WATCH_MAX + 1]; + pn_watch *rows[PN_WATCH_MAX]; /* pfds[k] (k>=1) -> row */ + + for (;;) { + pthread_mutex_lock(&m->mu); + if (m->stop) { + pthread_mutex_unlock(&m->mu); + break; + } + int nf = 0; + pfds[nf].fd = m->wake_r; + pfds[nf].events = POLLIN; + pfds[nf].revents = 0; + nf++; + for (int i = 0; i < PN_WATCH_MAX; i++) { + pn_watch *w = &m->watches[i]; + if (w->used && w->fd >= 0) { + pfds[nf].fd = w->fd; + pfds[nf].events = POLLIN; /* POLLIN|POLLHUP|POLLERR all fire */ + pfds[nf].revents = 0; + rows[nf] = w; + nf++; + } + } + pthread_mutex_unlock(&m->mu); + + /* Wake at least each grace/4 (min 250 ms, max 5 s) so a closed watch + * with no other fd activity is still swept promptly. */ + uint64_t g = m->grace_ms ? m->grace_ms : 45000; + int timeout_ms = (int)(g / 4); + if (timeout_ms < 250) timeout_ms = 250; + if (timeout_ms > 5000) timeout_ms = 5000; + + int r = poll(pfds, (nfds_t)nf, timeout_ms); + if (r < 0) { + if (errno == EINTR) + continue; + /* Unexpected poll failure: back off briefly, keep serving. */ + struct timespec ts = { .tv_sec = 0, .tv_nsec = 50 * 1000000 }; + nanosleep(&ts, NULL); + continue; + } + + uint64_t now = watch_now_ms(); + + /* Drain the self-pipe (table changed / stop requested). */ + if (pfds[0].revents & POLLIN) { + char buf[64]; + while (read(m->wake_r, buf, sizeof(buf)) > 0) { /* drain */ } + } + + /* Any watch fd readable/hung-up → peer closed. Confirm with the same + * MSG_PEEK==0 test hs_client_alive uses (a stray POLLIN with buffered + * bytes on a body-less GET is not a close, though clients never send + * one — belt and suspenders). Mark closed under mu. */ + pthread_mutex_lock(&m->mu); + for (int k = 1; k < nf; k++) { + if (!(pfds[k].revents & (POLLIN | POLLHUP | POLLERR))) + continue; + pn_watch *w = rows[k]; + /* The row may have been replaced/dropped while mu was released; only + * act if it is still the same open fd. */ + if (!w->used || w->fd != pfds[k].fd) + continue; + if (!hs_client_alive(w->fd)) { + fprintf(stderr, "[session_watch] watch dropped sid=%s model=%s " + "(grace %llums started)\n", w->sid, w->name, + (unsigned long long)m->grace_ms); + watch_mark_closed(w, now); + } + } + pthread_mutex_unlock(&m->mu); + + watch_sweep(m, now); + } + return NULL; +} + +/* Shared constructor for the started + manual (test) managers. */ +static pn_session_watch_mgr *watch_new(mr_registry *reg, int start_thread) +{ + if (!reg) + return NULL; + pn_session_watch_mgr *m = calloc(1, sizeof(*m)); + if (!m) + return NULL; + m->reg = reg; + m->grace_ms = pn_session_watch_grace_ms(); + m->wake_r = m->wake_w = -1; + for (int i = 0; i < PN_WATCH_MAX; i++) + m->watches[i].fd = -1; + if (pthread_mutex_init(&m->mu, NULL) != 0) { + free(m); + return NULL; + } + + int p[2]; + if (pipe(p) != 0) { + pthread_mutex_destroy(&m->mu); + free(m); + return NULL; + } + m->wake_r = p[0]; + m->wake_w = p[1]; + /* Non-blocking self-pipe so watch_wake never blocks and the drain loop ends + * on EAGAIN. */ + int fl; + if ((fl = fcntl(m->wake_r, F_GETFL, 0)) != -1) + fcntl(m->wake_r, F_SETFL, fl | O_NONBLOCK); + if ((fl = fcntl(m->wake_w, F_GETFL, 0)) != -1) + fcntl(m->wake_w, F_SETFL, fl | O_NONBLOCK); + + if (start_thread) { + if (pthread_create(&m->thread, NULL, watch_thread, m) != 0) { + close(m->wake_r); + close(m->wake_w); + pthread_mutex_destroy(&m->mu); + free(m); + return NULL; + } + m->thread_started = 1; + } + return m; +} + +pn_session_watch_mgr *pn_session_watch_start(mr_registry *reg) +{ + return watch_new(reg, 1); +} + +/* Test-only manual manager (no background thread; the test drives sweeps). */ +pn_session_watch_mgr *pn_session_watch_new_manual(mr_registry *reg) +{ + return watch_new(reg, 0); +} + +void pn_session_watch_stop(pn_session_watch_mgr *m) +{ + if (!m) + return; + pthread_mutex_lock(&m->mu); + m->stop = 1; + pthread_mutex_unlock(&m->mu); + watch_wake(m); + if (m->thread_started) + pthread_join(m->thread, NULL); + + /* Close every held/in-grace watch and release the manager's refs. Does NOT + * release the sessions — shutdown reclaims VRAM via the model teardown. */ + for (int i = 0; i < PN_WATCH_MAX; i++) + watch_drop(m, &m->watches[i], 0); + + /* Clear the backstop hooks off the caches (the bindings are about to be + * freed) and free them. Best-effort under the registry lock. */ + if (m->reg) { + pthread_mutex_lock(&m->reg->lock); + for (int i = 0; i < MR_MAX_MODELS; i++) + if (m->reg->entries[i].used && m->reg->entries[i].prefix_cache) + pn_prefix_cache_set_watch_hook(m->reg->entries[i].prefix_cache, + NULL, NULL); + pthread_mutex_unlock(&m->reg->lock); + } + for (int i = 0; i < m->n_hook_bindings; i++) + free(m->hook_bindings[i]); + m->n_hook_bindings = 0; + + if (m->wake_r >= 0) close(m->wake_r); + if (m->wake_w >= 0) close(m->wake_w); + pthread_mutex_destroy(&m->mu); + free(m); +} + +/* The keepalive header a watch connection gets on establish: a 200 with an + * event-stream content type and a single comment line, then held open. No body + * follows (the client just watches for the socket to close). */ +static void watch_send_established(int fd) +{ + static const char hdr[] = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: keep-alive\r\n\r\n" + ": watch established\n\n"; + ssize_t n = write(fd, hdr, sizeof(hdr) - 1); + (void)n; /* best-effort; a write failure is caught as a close by poll */ +} + +int pn_session_watch_add(pn_session_watch_mgr *m, int fd, mr_entry *entry, + const char *name, const char *sid) +{ + if (!m || fd < 0 || !entry || !entry->prefix_cache || !name || !sid || + !sid[0]) + return -1; + + /* Take the manager's OWN reference for the fd's lifetime (the caller's + * mr_acquire ref is released by the caller after this returns). */ + mr_entry *held = mr_acquire(m->reg, name); + if (!held) + return -1; + + pthread_mutex_lock(&m->mu); + + /* Reconnect: an existing watch for this (name,sid) is REPLACED — cancel any + * pending grace and adopt the new fd. */ + pn_watch *w = watch_find(m, name, sid); + if (w) { + if (w->fd >= 0) + close(w->fd); /* the old connection is superseded */ + if (w->entry) + mr_release(m->reg, w->entry); + w->entry = held; + w->fd = fd; + w->closed_at_ms = 0; /* alive again — grace cancelled */ + snprintf(w->name, sizeof(w->name), "%s", name); + snprintf(w->sid, sizeof(w->sid), "%s", sid); + w->used = 1; + pthread_mutex_unlock(&m->mu); + watch_send_established(fd); + watch_wake(m); + fprintf(stderr, "[session_watch] watch (re)established sid=%s model=%s\n", + sid, name); + return 0; + } + + /* Fresh watch: take a free row. */ + for (int i = 0; i < PN_WATCH_MAX; i++) { + if (!m->watches[i].used) { + w = &m->watches[i]; + break; + } + } + if (!w) { + pthread_mutex_unlock(&m->mu); + mr_release(m->reg, held); + return -1; /* table full — caller keeps + closes fd */ + } + w->entry = held; + w->fd = fd; + w->closed_at_ms = 0; + snprintf(w->name, sizeof(w->name), "%s", name); + snprintf(w->sid, sizeof(w->sid), "%s", sid); + w->used = 1; + pthread_mutex_unlock(&m->mu); + + watch_send_established(fd); + watch_wake(m); + fprintf(stderr, "[session_watch] watch established sid=%s model=%s\n", + sid, name); + return 0; +} diff --git a/src/serve/session_watch.h b/src/serve/session_watch.h new file mode 100644 index 0000000..d65aa71 --- /dev/null +++ b/src/serve/session_watch.h @@ -0,0 +1,135 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file session_watch.h + * @brief Liveness-based session release — the "alive holds, dead releases" + * control plane (K80 serving-ops §C.4). A client opens a long-held + * GET /session/watch for each of its live prefix-cache sessions and + * keeps the connection open for the session's whole life. While the + * connection is held the session is ALIVE and is NEVER reaped for + * inactivity — an autonomous task that sits quiet in wait_agents for an + * hour keeps its warm slot. Only the connection actually CLOSING (client + * crash / drop) frees the slot, and only after a short grace so a + * flaky-bridge hiccup + resilient reconnect never false-releases. + * + * Why a manager (not a parked thread per watch): the serve host is 2-core. A + * blocking recv() thread per watch would burn a thread and a stack for every + * idle-but-alive session (up to ~max_sessions per model) and steal scheduling + * from request handling. Instead ONE background thread holds every watch fd in + * a single poll() loop (readability on a held GET body-less connection means + * the peer closed — the same MSG_PEEK==0 signal hs_client_alive uses), plus a + * self-pipe for wakeups and a periodic grace sweep. Cost is ~one fd per watch + * and a handful of syscalls, independent of the watch count. + * + * The connection thread that accepted the GET /session/watch hands the fd to + * the manager (pn_session_watch_add) and RETURNS — it does not loop or close + * the fd. The manager owns the fd from then on (closes it when the watch ends). + * + * @status Built; CPU-testable (poll loop + grace reaper), CUDA compile-checked + * on the K80 test machine. NOT deployed (serve --fresh is user-gated). + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_SESSION_WATCH_H +#define PN_SERVE_SESSION_WATCH_H + +#include "model_registry.h" + +/* Opaque watch manager. Concrete definition in session_watch.c. */ +typedef struct pn_session_watch_mgr pn_session_watch_mgr; + +/** + * Grace after a watch connection closes before the session is released, in ms. + * A reconnecting watch for the same session within this window cancels the + * release. Default 45 s — comfortably longer than the client's resilient-reconnect + * backoff (RESUME_RETRY_MAX ~6, a few s each) plus link-recovery margin, so a + * bridge hiccup never false-releases. Override: PN_SESSION_WATCH_GRACE_MS. + */ +uint64_t pn_session_watch_grace_ms(void); + +/** + * Long safety backstop, in ms: the ONLY inactivity-based reaper left, and it + * only touches sessions with NO watch at all (a pathological/forgotten slot + * whose client never opened a watch and never returned). A watched — or + * within-grace — session is NEVER reaped by this. Default 12 h. Override: + * PN_SESSION_IDLE_TTL_MS (repurposed from the old short idle-TTL). + */ +uint64_t pn_session_watch_backstop_ms(void); + +/** + * Create + start the watch manager over `reg` (borrowed; must outlive the + * manager). Spawns the single poll/grace background thread. Returns the manager + * or NULL on failure (self-pipe / thread). One per serve process. + */ +pn_session_watch_mgr *pn_session_watch_start(mr_registry *reg); + +/** + * Stop the manager: signal the poll thread, join it, close every held watch fd + * and the self-pipe, free the manager. NULL-safe. Call at shutdown only. + */ +void pn_session_watch_stop(pn_session_watch_mgr *mgr); + +/** + * Register a held GET /session/watch connection. `fd` is the accepted client + * socket (OWNED by the manager from here on: it closes it when the watch ends — + * peer close, replacement by a reconnect for the same key, or shutdown). `entry` + * is the resolved registry entry whose prefix_cache holds the session; the + * caller must have taken an mr_acquire reference — the manager takes its OWN + * reference for the fd's lifetime and releases it when the fd is dropped, so the + * caller releases its reference as usual after this returns. `name` (model id, + * for logging) and `sid` (the prefix-cache session key) are copied. + * + * A watch arriving for a (name,sid) that already has a live watch CANCELS the + * old one's grace (if pending) and REPLACES it — the reconnect case. On success + * the manager sends the HTTP 200 SSE-style keepalive header on `fd` itself + * (so the client sees the watch is established) and holds it. + * + * @return 0 on success (fd is owned by the manager), -1 on failure (fd is NOT + * adopted — the caller still owns and must close it). Failure reasons: + * NULL args, no prefix cache on the entry, table full, OOM. + */ +int pn_session_watch_add(pn_session_watch_mgr *mgr, int fd, mr_entry *entry, + const char *name, const char *sid); + +/** + * Is the live prefix-cache slot `entry_sid` on model `name` covered by a + * live-or-in-grace liveness watch? The client holds ONE watch per tab keyed on the + * tab's BASE id `client--`, while the live KV slots are the + * mode-suffixed sids `-orch`/`-act`/`-chat` — so a watch on the base covers + * `entry_sid` when entry_sid == "" or "-". Returns nonzero if + * so. Bound (via pn_session_watch_install_backstop_hooks) as each cache's + * watch-liveness predicate so the 12 h backstop reaper never reaps a watched + * base's mode slots. Thread-safe: yes (takes the manager mutex). NULL-safe. + */ +int pn_session_watch_base_is_watched(pn_session_watch_mgr *mgr, const char *name, + const char *entry_sid); + +/** + * Install the backstop watch-hook (pn_session_watch_base_is_watched) on every + * loaded model's prefix cache, so the backstop reaper skips slots whose base is + * still watched. Call once after the manager starts and models are loaded. Safe + * to call again (idempotent) if models load later. NULL-safe. + */ +void pn_session_watch_install_backstop_hooks(pn_session_watch_mgr *mgr); + +/** + * Test-only: run one grace-sweep pass at wall-clock `now_ms` WITHOUT the + * background thread (the unit test drives time deterministically). Releases any + * session whose watch closed more than the grace ago with no reconnect. Not for + * production use (the started manager sweeps on its own). NULL-safe. + */ +void pn_session_watch_sweep_for_test(pn_session_watch_mgr *mgr, uint64_t now_ms); + +/** + * Test-only: mark an already-registered watch's connection as closed at + * `now_ms` (as if poll() saw the peer hang up), starting its grace WITHOUT a + * real socket close. Used by the unit test to exercise the drop→grace→release + * and drop→reconnect paths deterministically. Returns 0 if the watch was found, + * -1 otherwise. NULL-safe. + */ +int pn_session_watch_mark_closed_for_test(pn_session_watch_mgr *mgr, + const char *name, const char *sid, + uint64_t now_ms); + +#endif /* PN_SERVE_SESSION_WATCH_H */ diff --git a/src/serve/signal_handlers.c b/src/serve/signal_handlers.c new file mode 100644 index 0000000..24d9f81 --- /dev/null +++ b/src/serve/signal_handlers.c @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file signal_handlers.c + * @brief Implementation of the serve-process crash/shutdown handlers + * (MemoryAware.md §8, §11 Q4). Two tiers: + * + * - SIGINT / SIGTERM (controlled shutdown): RECORD the request and + * return. The teardown itself belongs to the accept loop, which the + * record wakes through a self-pipe (serve_shutdown.h). It was fair to + * run the CUDA teardown inline while that was the only accelerator — + * an operator stop is not a crash mid-kernel — but the Tenstorrent + * backend made it unsafe: retiring a residency polls the card, takes + * the registry lock, and frees a device a connection thread may still + * be driving. A handler runs at an arbitrary point in the program and + * may do neither, so it does the two async-signal-safe things + * (sig_atomic_t store + write(2)) and hands the work to a point where + * the program's state is known. The CUDA teardown moved with it and + * still runs on every controlled stop, in serve_command.c. + * + * - SIGSEGV / SIGABRT / SIGBUS (crash): do NOT call into libcuda from the + * handler (unsafe — can hang mid-fault). Write a one-line async-safe + * notice and _exit(); the kernel closes the fd on teardown, and the + * next start's stranded-VRAM detection (qwen35moe_forward_cuda die-A + * init) catches any reclaim race and refuses/reroutes rather than + * OOMing on a small alloc. + * + * @status Phase 3. + * @issues None + * @todo None + */ + +#include "signal_handlers.h" + +#include +#include +#include +#include + +#include "serve_registry.h" +#include "serve_shutdown.h" + +#ifdef USE_CUDA +#include "cuda/fc_cuda.h" +#endif + +/* Guard so the handler runs its teardown at most once even if two signals race + * (sig_atomic_t is the async-signal-safe flag type). */ +static volatile sig_atomic_t g_handling = 0; + +/* Async-signal-safe write of a fixed string to stderr (fd 2). fprintf/printf + * are NOT signal-safe, so crash-path messaging goes through write(2). */ +static void safe_write(const char *msg) +{ + ssize_t n = write(2, msg, strlen(msg)); + (void)n; /* best-effort; nothing useful to do if it fails in a handler */ +} + +/* Controlled-shutdown handler (SIGINT/SIGTERM): record the request, wake the + * accept loop, return. Everything the stop actually has to DO — deregister, + * drain the in-flight turns, retire the card residencies, destroy the CUDA + * contexts — runs in pn_serve_main's shutdown sequence. */ +static void handle_shutdown(int sig) +{ + /* Async-signal-safe by construction: a sig_atomic_t store and a one-byte + * write(2). Two signals in a row are fine — the second marks the request + * escalated, which collapses the drain budget rather than aborting the + * process from here. */ + pn_serve_shutdown_note_signal(sig); + + safe_write("\n[signal] shutdown requested — draining in-flight turns, then " + "retiring accelerator residencies\n"); + + if (pn_serve_shutdown_wake_fd() >= 0) + return; /* the accept loop owns the teardown from here */ + + /* No wake pipe (pn_serve_shutdown_arm failed): nothing guarantees the + * accept loop ever observes the flag, because the kernel may have delivered + * this signal to a connection thread. Falling back to the pre-drain + * behaviour is worse than the deferred path but far better than an + * unkillable serve. It deliberately does NOT touch the accelerator + * residencies — an unsafe teardown from a handler is worse than the gap. */ + if (g_handling) + _exit(128 + sig); + g_handling = 1; + + serve_registry_deregister_all(); +#ifdef USE_CUDA + if (fc_cuda_live_ctx_count() > 0) + fc_cuda_emergency_teardown(); +#endif + signal(sig, SIG_DFL); + raise(sig); +} + +/* Crash handler (SIGSEGV/SIGABRT/SIGBUS): minimal, no libcuda calls. */ +static void handle_crash(int sig) +{ + if (g_handling) + _exit(128 + sig); + g_handling = 1; + + safe_write("\n[signal] fatal crash — exiting; kernel will close the nvidia " + "fd. Next start's stranded-VRAM check guards the die.\n"); + + /* Do NOT touch libcuda here: a cuCtxDestroy mid-fault can hang. Restore the + * default handler and re-raise so we get the real crash exit status (and a + * core dump if enabled); process teardown closes the fd. */ + signal(sig, SIG_DFL); + raise(sig); +} + +void pn_install_signal_handlers(void) +{ + struct sigaction sa_shutdown; + memset(&sa_shutdown, 0, sizeof(sa_shutdown)); + sa_shutdown.sa_handler = handle_shutdown; + sigemptyset(&sa_shutdown.sa_mask); + sigaction(SIGINT, &sa_shutdown, NULL); + sigaction(SIGTERM, &sa_shutdown, NULL); + + struct sigaction sa_crash; + memset(&sa_crash, 0, sizeof(sa_crash)); + sa_crash.sa_handler = handle_crash; + sigemptyset(&sa_crash.sa_mask); + /* SA_RESETHAND so a fault inside the handler falls through to the default. */ + sa_crash.sa_flags = SA_RESETHAND; + sigaction(SIGSEGV, &sa_crash, NULL); + sigaction(SIGABRT, &sa_crash, NULL); + sigaction(SIGBUS, &sa_crash, NULL); +} diff --git a/src/serve/signal_handlers.h b/src/serve/signal_handlers.h new file mode 100644 index 0000000..4538fb4 --- /dev/null +++ b/src/serve/signal_handlers.h @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file signal_handlers.h + * @brief Crash / shutdown signal handling for the serve process (MemoryAware.md + * §8). On the Kepler nvidia stack VRAM is reclaimed only when the owning + * process's /dev/nvidiaN fd closes — cuCtxDestroy alone does not drop it. + * So the guarantee is: on ANY exit, close the fd. A controlled shutdown + * (SIGINT/SIGTERM) still drains + destroys the live CUDA contexts, but + * the handler no longer does it: it records the request through + * serve_shutdown.h and the accept loop performs the teardown, because + * the Tenstorrent residency retirement that now shares that path polls + * the card and takes locks a handler may not. On a crash + * (SIGSEGV/SIGABRT) they do the minimal async-signal-safe thing and + * _exit(), leaning on the kernel to close the fd and on startup + * stranded-VRAM detection to catch any race. + * + * @status Phase 3 — crash cleanup + VRAM reclaim. + * @issues Worker-respawn reclaim (kill a wedged die's owner to force fd-close) + * is DEFERRED — barswap per-die workers are deprecated for this project; + * within the single-process k80_quirk boot-map the reclaim primitive is + * process exit itself. Multi-process reclaim needs design (see §8.3). + * @todo None + */ + +#ifndef PN_SIGNAL_HANDLERS_H +#define PN_SIGNAL_HANDLERS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* Install the serve-process signal handlers (SIGINT, SIGTERM, SIGSEGV, + * SIGABRT, SIGBUS). Idempotent. Safe to call even in CPU-only builds — the CUDA + * teardown is a no-op when no contexts are live. Call once, after model load, + * before the accept loop. + * + * Thread-safe: no (install once at startup) + * Reentrant: no */ +void pn_install_signal_handlers(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_SIGNAL_HANDLERS_H */ diff --git a/src/serve/tool_call_stream.c b/src/serve/tool_call_stream.c new file mode 100644 index 0000000..d54d20d --- /dev/null +++ b/src/serve/tool_call_stream.c @@ -0,0 +1,292 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_call_stream.c + * @brief Implementation of the incremental tool-call streamer + * (see tool_call_stream.h). + * + * @status Complete. + * @issues None beyond the header's (an abandoned early call cannot be + * retracted). + * @todo None + */ + +#include "tool_call_stream.h" + +#include +#include +#include + +/* ---------- wire calls ---------- */ + +static tcs_wire_call *open_wire_call(tcs_streamer *ts, const char *id, + const char *name) +{ + if (ts->n_calls >= ts->cap_calls) { + int cap = ts->cap_calls ? ts->cap_calls * 2 : 4; + tcs_wire_call *grown = realloc(ts->calls, (size_t)cap * sizeof(*grown)); + if (!grown) return NULL; + ts->calls = grown; + ts->cap_calls = cap; + } + tcs_wire_call *wc = &ts->calls[ts->n_calls]; + memset(wc, 0, sizeof(*wc)); + wc->index = ts->next_index; + wc->id = strdup(id); + wc->name = strdup(name); + if (!wc->id || !wc->name || !jw_init(&wc->sent)) { + free(wc->id); + free(wc->name); + jw_free(&wc->sent); + return NULL; + } + ts->n_calls++; + ts->next_index++; + if (ts->sink.call_begin) + ts->sink.call_begin(ts->sink.ud, wc->index, wc->id, wc->name); + return wc; +} + +static void send_arguments(tcs_streamer *ts, tcs_wire_call *wc, + const char *bytes, size_t n) +{ + if (n == 0) return; + jw_raw_n(&wc->sent, bytes, n); + if (ts->sink.call_arguments) + ts->sink.call_arguments(ts->sink.ud, wc->index, bytes, n); +} + +/* Send what `wc` still owes of `final_args`, if what it has sent is a prefix of + * them. Returns false (sending nothing) when it is not. */ +static bool complete_wire_call(tcs_streamer *ts, tcs_wire_call *wc, + const char *name, const char *final_args) +{ + size_t total = strlen(final_args); + if (strcmp(wc->name, name) != 0 || wc->sent.len > total || + memcmp(wc->sent.data, final_args, wc->sent.len) != 0) + return false; + send_arguments(ts, wc, final_args + wc->sent.len, total - wc->sent.len); + wc->committed = true; + ts->any_committed = true; + return true; +} + +/* A call the wire has not seen: opening delta, then all of its arguments. */ +static void send_whole_call(tcs_streamer *ts, const tc_tool_call *call) +{ + tcs_wire_call *wc = open_wire_call(ts, call->id, call->name); + if (!wc) return; + send_arguments(ts, wc, call->arguments, strlen(call->arguments)); + wc->committed = true; + ts->any_committed = true; +} + +/* ---------- the object being read: root-member events from jcs ---------- */ + +static void on_root_key(void *ud, const char *key) +{ + tcs_streamer *ts = ud; + free(ts->member_key); + ts->member_key = strdup(key); +} + +static void on_root_value_begin(void *ud, size_t at, char first) +{ + tcs_streamer *ts = ud; + const char *key = ts->member_key ? ts->member_key : ""; + /* Only the FIRST member of each spelling counts: jr_object_get returns the + * first match, so a later duplicate never decides anything. */ + if (strcmp(key, "function") == 0 && !ts->fn_seen) { + ts->fn_seen = true; + ts->fn_pending = true; + } + if (strcmp(key, "arguments") == 0 && !ts->args_seen) { + ts->args_seen = true; + if (first == '{') { /* a non-object falls to "parameters" */ + ts->args_live = true; + ts->args_begin = at; + } + } +} + +static void on_root_value_end(void *ud, size_t end, const char *decoded) +{ + tcs_streamer *ts = ud; + if (ts->args_live && !ts->args_done && ts->member_key && + strcmp(ts->member_key, "arguments") == 0 && end > ts->args_begin) { + ts->args_done = true; + ts->args_end = end; + } + if (!ts->fn_pending) return; + ts->fn_pending = false; + /* The first "function" member is a string: that IS the call's name (it + * outranks "name"/"func"), so the call can open now — if the parse would + * accept the name at all. */ + if (!decoded || !*decoded || !tc_tool_name_known(ts->tools, decoded)) return; + char id[24]; + tc_new_call_id(id, sizeof(id)); + tcs_wire_call *wc = open_wire_call(ts, id, decoded); + if (wc) ts->spec = ts->n_calls - 1; +} + +/* Send the early call's newly decided argument bytes. The arguments' closing + * '}' is held until the object's parse confirms the call. */ +static void pump_early_arguments(tcs_streamer *ts) +{ + if (ts->spec < 0 || !ts->args_live || ts->jcs.status == JCS_FAILED) return; + tcs_wire_call *wc = &ts->calls[ts->spec]; + size_t limit = ts->args_done ? ts->args_end - 1 : ts->jcs.out.len; + size_t from = ts->args_begin + wc->sent.len; + if (limit > from) + send_arguments(ts, wc, ts->jcs.out.data + from, limit - from); +} + +static void reset_object(tcs_streamer *ts) +{ + if (ts->jcs_live) jcs_free(&ts->jcs); + ts->jcs_live = false; + free(ts->member_key); + ts->member_key = NULL; + ts->in_object = false; + ts->raw.len = 0; + ts->brace_depth = 0; + ts->brace_in_str = ts->brace_escape = false; + ts->fn_seen = ts->fn_pending = false; + ts->args_seen = ts->args_live = ts->args_done = false; + ts->args_begin = ts->args_end = 0; + ts->spec = -1; +} + +static void begin_object(tcs_streamer *ts) +{ + reset_object(ts); + ts->in_object = true; + jcs_events ev = { on_root_key, on_root_value_begin, on_root_value_end, ts }; + ts->jcs_live = jcs_init(&ts->jcs, &ev); +} + +static void feed_object(tcs_streamer *ts, const char *bytes, size_t n) +{ + jw_raw_n(&ts->raw, bytes, n); + if (!ts->jcs_live) return; + jcs_feed(&ts->jcs, bytes, n); + pump_early_arguments(ts); +} + +/* The object closed: parse it exactly as the end-of-turn parse will, and put + * on the wire whatever of its calls is not there yet. */ +static void close_object(tcs_streamer *ts) +{ + tc_tool_call *calls = NULL; + int n = 0; + const char *open = ts->raw.data; + if (tc_parse_ornith_object(open, open + ts->raw.len, ts->tools, &calls, &n) != 0) + n = 0; + int first = 0; + if (ts->spec >= 0) { + tcs_wire_call *wc = &ts->calls[ts->spec]; + if (n > 0 && complete_wire_call(ts, wc, calls[0].name, calls[0].arguments)) { + first = 1; + } else { + wc->abandoned = true; + fprintf(stderr, "[tool_stream] call %d (%s) was streamed early but its " + "object parsed to %s — abandoned on the wire with " + "unterminated arguments (%zu bytes sent)\n", wc->index, + wc->name, n > 0 ? "a different call" : "no call", + wc->sent.len); + } + } + for (int i = first; i < n; i++) + send_whole_call(ts, &calls[i]); + tc_tool_calls_free(calls, n); + reset_object(ts); +} + +/* Mirror parse_ornith_json's scan: each '{' starts an object that ends at its + * matching '}' (braces inside strings ignored, as json_object_end does). */ +static void scan_objects(tcs_streamer *ts, const char *bytes, size_t n) +{ + size_t i = 0; + while (i < n) { + if (!ts->in_object) { + const char *brace = memchr(bytes + i, '{', n - i); + if (!brace) return; + i = (size_t)(brace - bytes); + begin_object(ts); + } + size_t start = i; + bool closed = false; + for (; i < n && !closed; i++) { + char c = bytes[i]; + if (ts->brace_in_str) { + if (ts->brace_escape) ts->brace_escape = false; + else if (c == '\\') ts->brace_escape = true; + else if (c == '"') ts->brace_in_str = false; + continue; + } + if (c == '"') ts->brace_in_str = true; + else if (c == '{') ts->brace_depth++; + else if (c == '}' && --ts->brace_depth == 0) closed = true; + } + feed_object(ts, bytes + start, i - start); + if (closed) close_object(ts); + } +} + +/* ---------- public API ---------- */ + +bool tcs_init(tcs_streamer *ts, tc_dialect dialect, const jr_value *tools, + const tcs_sink *sink) +{ + memset(ts, 0, sizeof(*ts)); + ts->dialect = dialect; + ts->tools = tools; + if (sink) ts->sink = *sink; + ts->spec = -1; + ts->early = (dialect == TC_DIALECT_ORNITH_JSON && tools != NULL); + return jw_init(&ts->raw); +} + +void tcs_feed(tcs_streamer *ts, const char *bytes, size_t n) +{ + if (ts->early && n > 0) scan_objects(ts, bytes, n); + if (ts->sink.progress) ts->sink.progress(ts->sink.ud); +} + +void tcs_finish(tcs_streamer *ts, const tc_tool_call *calls, int n) +{ + int w = 0; /* next wire call to match, skipping abandoned ones */ + for (int j = 0; j < n; j++) { + while (w < ts->n_calls && ts->calls[w].abandoned) w++; + tcs_wire_call *wc = (w < ts->n_calls) ? &ts->calls[w] : NULL; + if (wc) { + w++; + if (complete_wire_call(ts, wc, calls[j].name, calls[j].arguments)) + continue; + wc->abandoned = true; + fprintf(stderr, "[tool_stream] BUG: wire call %d (%s, %zu bytes sent) " + "is not a prefix of final call %d (%s) — re-sending it whole\n", + wc->index, wc->name, wc->sent.len, j, calls[j].name); + } + send_whole_call(ts, &calls[j]); + } +} + +bool tcs_sent_any(const tcs_streamer *ts) +{ + return ts->n_calls > 0; +} + +void tcs_free(tcs_streamer *ts) +{ + reset_object(ts); + jw_free(&ts->raw); + for (int i = 0; i < ts->n_calls; i++) { + free(ts->calls[i].id); + free(ts->calls[i].name); + jw_free(&ts->calls[i].sent); + } + free(ts->calls); + ts->calls = NULL; + ts->n_calls = ts->cap_calls = 0; +} diff --git a/src/serve/tool_call_stream.h b/src/serve/tool_call_stream.h new file mode 100644 index 0000000..bf4e8d5 --- /dev/null +++ b/src/serve/tool_call_stream.h @@ -0,0 +1,140 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_call_stream.h + * @brief Streams OpenAI tool-call deltas WHILE the model writes the call, + * instead of all at once when generation ends. + * + * The stream emitter diverts a tool call into a raw channel (serve_emitter.h); + * before this module, nothing of it reached the client until the turn ended and + * tc_parse_tool_calls ran. A long call (a `write` of a whole file) is minutes + * of silence on the wire, and a client watchdog reads silence as a dead stream. + * + * The streamer is fed every byte the emitter diverts (tcs_feed) and, through a + * sink, puts three things on the wire: + * - call_begin the OpenAI opening delta (index, id, name) — as soon as + * the call's name is DECIDED; + * - call_arguments argument fragments as the model writes them; + * - progress "tool bytes arrived" — the caller's keepalive hook for + * the stretches where nothing can be sent yet. + * At end of turn tcs_finish reconciles with the authoritative parse and sends + * only what is still owed. + * + * THE CONTRACT: for every call that ends up in the parse, the argument + * fragments sent under its index concatenate to EXACTLY that call's + * `arguments` (the string the non-streaming response carries). Arguments are + * the DOM re-serialization of the model's JSON (jr_to_json), so they are + * streamed through json_canonical_stream, which writes only bytes that are + * already final. What cannot be decided early is held, not guessed: + * - Only the ORNITH_JSON dialect streams early, and only an object whose name + * comes from a "function" key (the spelling that outranks "name"/"func"), + * whose arguments are the first "arguments" member and are an object. + * Anything else (other spellings, the {"tool_calls":[…]} wrapper, the XML + * dialects, whose values are typed and may be rewritten by recovery) is + * sent whole the moment its object closes — or at end of turn for the XML + * dialects — with the caller's keepalive covering the gap. + * - The arguments' final '}' is held until the object closes and the parse + * confirms the call. A call opened on the wire whose object then fails to + * parse cannot be retracted (OpenAI has no such delta), but it is left with + * arguments that are not valid JSON, so no client can run it as if it were + * complete; when it was the turn's only call, finish_reason is not + * "tool_calls" either — exactly as the non-streaming response reports it. + * - When an object closes, its calls are parsed by the SAME per-object code + * the end-of-turn parse uses (tc_parse_ornith_object) and anything not yet + * sent is sent then. tcs_finish re-checks every call against the final + * parse (which also applies the truncation repair and the grammar gate). + * + * Depends on libc + tool_calling / json_canonical_stream / json_reader / + * json_writer; no engine deps (unit-tested in tests/test_tool_call_stream.c). + * + * @status Complete. + * @issues A call opened on the wire whose object then fails to parse cannot be + * retracted (there is no OpenAI delta for it). It is logged + * ([tool_stream] ... abandoned), its arguments are left unparseable, + * and the turn's finish_reason/tool_call_error are exactly what the + * non-streaming path reports. + * @todo None + */ + +#ifndef PN_SERVE_TOOL_CALL_STREAM_H +#define PN_SERVE_TOOL_CALL_STREAM_H + +#include +#include + +#include "json_canonical_stream.h" +#include "json_reader.h" +#include "json_writer.h" +#include "tool_calling.h" + +/* Where the streamer's output goes. Every pointer may be NULL. */ +typedef struct { + void (*call_begin)(void *ud, int index, const char *id, const char *name); + void (*call_arguments)(void *ud, int index, const char *bytes, size_t n); + void (*progress)(void *ud); + void *ud; +} tcs_sink; + +/* One call as the client has seen it. */ +typedef struct { + int index; /* wire tool_calls[].index */ + char *id; + char *name; + json_writer sent; /* argument bytes already sent */ + bool committed; /* its object closed and parsed into this call */ + bool abandoned; /* opened on the wire, then the parse dropped it */ +} tcs_wire_call; + +typedef struct tcs_streamer { + tc_dialect dialect; + const jr_value *tools; /* borrowed: the request's tools */ + tcs_sink sink; + bool early; /* this dialect/request streams early at all */ + + /* outer scan (mirrors parse_ornith_json: '{'-rooted objects in order) */ + bool in_object; + json_writer raw; /* raw text of the current object */ + int brace_depth; + bool brace_in_str; + bool brace_escape; + + /* current object */ + jcs_parser jcs; + bool jcs_live; + char *member_key; /* key of the root member being read */ + bool fn_seen, fn_pending; + bool args_seen, args_live, args_done; + size_t args_begin, args_end; + int spec; /* slot in `calls` streamed early, -1 none */ + + tcs_wire_call *calls; + int n_calls, cap_calls; + int next_index; + bool any_committed; +} tcs_streamer; + +/** Set up a streamer. `tools` is borrowed; NULL means the request has no tools + * (nothing is streamed early). Returns false on allocation failure. */ +bool tcs_init(tcs_streamer *ts, tc_dialect dialect, const jr_value *tools, + const tcs_sink *sink); + +/** Feed bytes the emitter appended to its raw tool channel (in order). */ +void tcs_feed(tcs_streamer *ts, const char *bytes, size_t n); + +/** + * End of turn: make the wire carry exactly `calls` (the final parse, after + * repair and the grammar gate). Sends what each call still owes; a call the + * wire has not seen is sent whole (opening delta + arguments) under a fresh + * index. A call already on the wire whose sent bytes are not a prefix of the + * final arguments is logged and re-sent under a fresh index — the contract + * makes that unreachable, the check makes a violation loud. + */ +void tcs_finish(tcs_streamer *ts, const tc_tool_call *calls, int n); + +/** True once any call delta has been sent (early or at an object's close). */ +bool tcs_sent_any(const tcs_streamer *ts); + +/** Free owned state. */ +void tcs_free(tcs_streamer *ts); + +#endif /* PN_SERVE_TOOL_CALL_STREAM_H */ diff --git a/src/serve/tool_calling.c b/src/serve/tool_calling.c new file mode 100644 index 0000000..1fa7d51 --- /dev/null +++ b/src/serve/tool_calling.c @@ -0,0 +1,1698 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_calling.c + * @brief Implementation of the OpenAI tool-calling glue (see tool_calling.h). + * Renders the Qwen3-Coder XML tool section the Nex-N2-mini expects (and + * the MiniCPM5-native section via tc_render_tools_system_minicpm), + * renders assistant/tool history back into that dialect, and parses the + * model's tool-call output into schema-typed structured calls. + * + * Parsing accepts THREE forms: the canonical wrapped Qwen form + * (…); a bare + * … block with no wrapper (the Q3 nex-n2-mini + * emits the bare form most of the time); and MiniCPM5's attribute-style + * VALUE… + * (unwrapped, CDATA-tolerant). All yield identical structured calls + * (ids, names, JSON arguments). + * + * It also recovers a FRAME CAPTURE: when a turn is force-closed + * mid-thought and the compelled XML frame swallows the call the model was + * still writing, the captured call replaces the outer frame (see + * FRAME_MARKERS and recover_captured_call). + * + * And it names an ABANDONED call: a opened, never closed, + * with no value bytes in it, is a truncated emission rather than a call. + * It is dropped and reported through tc_parse_tool_calls_diagnosed + * instead of parsing to the `{}` a deliberately empty argument also + * produces (see tc_parse_failure and tests/abandoned_call_fixture.h). + * The same channel names the other structurally-wrong shapes — a JSON + * body closed with this dialect's own tail (body_leaks_dialect_tail), a + * "" leaking into a parameter value, and an opener whose name is + * missing or is not a declared tool. + * + * Finally it CLOSES a truncated JSON argument object when everything left + * open can be closed without inventing a key or a value + * (json_repair_truncated), and reports each such rescue so the serve can + * count how often the model is being cut off mid-call. + * + * @status Complete for the serve layer's needs. + * @issues See header (@issues) — consecutive tool results are separate turns. + * Frame-capture recovery is a SAFETY NET over a serve-side cause (the + * reasoning-budget "" injection), not a fix for it, and it can + * only reach values that ARE a complete call. A captured value that is + * free-form scratch text (a shell pipeline in a `read` path, an analysis + * dump in a `glob` pattern) carries no call and no marker, and passes + * through unchanged. + * @todo + * - [ ] Gate the capture layer on the serve's `reason_forced` flag once it is + * plumbed through — that is the zero-false-positive signal, and it would + * retire the content markers as the trigger. + */ + +#include "tool_calling.h" + +#include +#include +#include +#include + +#include "json_reader.h" +#include "json_value_grammar.h" +#include "json_writer.h" + +/* ============================================================ + * Dialect resolution (per-model tool-call dialect selection) + * ============================================================ */ + +/* Case-insensitive prefix test: does `s` begin with `prefix`? */ +static bool has_prefix_ci(const char *s, const char *prefix) +{ + if (!s) return false; + while (*prefix) { + if (tolower((unsigned char)*s) != tolower((unsigned char)*prefix)) + return false; + s++; + prefix++; + } + return true; +} + +tc_dialect tc_dialect_for_model(const char *name, bool is_minicpm) +{ + /* Name table wins first — an explicit per-model declaration that survives a + * new same-arch sibling (ornith and saber are near-identical qwen35 arches, + * so arch alone cannot separate them). */ + if (has_prefix_ci(name, "ornith")) + return TC_DIALECT_ORNITH_JSON; + if (is_minicpm) + return TC_DIALECT_MINICPM_XML; + return TC_DIALECT_QWEN_XML; +} + +bool tc_dialect_grammar_models(tc_dialect dialect) +{ + return dialect == TC_DIALECT_QWEN_XML || dialect == TC_DIALECT_ORNITH_JSON; +} + +/* Literal fragments of the tools system block, copied verbatim from the model's + * chat_template so the prompt the model sees is byte-identical to the oracle. */ +static const char TOOLS_HEAD[] = + "# Tools\n\nYou have access to the following functions:\n\n"; +static const char TOOLS_FORMAT[] = + "\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO " + "suffix:\n\n" + "\n\n" + "\nvalue_1\n\n" + "\nThis is the value for the second parameter\n" + "that can span\nmultiple lines\n\n\n\n\n" + "\nReminder:\n" + "- Function calls MUST follow the specified format: an inner " + " block must be nested within " + " XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural " + "language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal " + "with your current knowledge and do not tell the user about function calls\n" + ""; + +static const char TC_OPEN[] = ""; +static const char TC_CLOSE[] = ""; +static const char FN_OPEN[] = "… block: + * \n\n\nVALUE\n\n\n + * The generation grammar treats VALUE as free text terminated by "\n\n", + * so it accepts this wrapper verbatim — the parser must peel it back off to the + * bare VALUE (the model emits it because its own trained format uses ). */ +static const char VALUE_OPEN[] = ""; +static const char VALUE_CLOSE[] = ""; + +/* MiniCPM5's attribute-style dialect (SGLang minicpm5 detector / the model's + * chat_template): a bare, unwrapped + * VALUE... + * with the name/keys as double- or single-quoted attributes, and multi-line / + * special-char values optionally wrapped in . */ +static const char FN_ATTR_OPEN[] = " without concluding, the SERVE injects the "" + * token itself — batch_sched.c driver_reason_step (batched path) and + * serve_generation.c sg_run_completion (single-user). That injected close is + * fed straight into the grammar (driver_grammar_advance_injected), which cannot + * tell it from a close the MODEL chose: TG_FREE's think_term matcher fires, + * reasoning_ended is set, and FORCED mode compels the tool-call opener from the + * very next char. The model — still mid-thought — is made to emit + * "\n\n", and everything it was still going to say + * lands in the free-text VALUE. + * + * The control that settles it is a recorded native-XML capture, a session that never + * wedged, on nex-n2-mini: + * + * + * \n\n\n\nlumen/serve.log + * + * + * The captured text is a NATIVE Qwen-XML call — the exact dialect the grammar + * armed — preceded by the model's own "". No dialect drift occurred, so + * the capture is not about dialects; it is about being cut off mid-thought. The + * leading "" is the signature of the whole family: the model had not yet + * written the close it was about to write, so it wrote it into the value. + * + * These literals are the frame's own vocabulary. They are a TRIGGER for + * inspection, never a verdict on their own — see recover_captured_call for the + * gates, and note the two things this trigger CANNOT do: + * • it is not complete. Three recorded defects carry no marker at all (a + * shell pipeline in a `read` path, "TaermIDE"/"A rgus", "grep i"), so + * content marks can never cover the whole family; + * • it is a symptom, not the cause. The zero-false-positive signal is the + * serve's own `reason_forced` (batch_sched.h:307) / `reason_over_budget` + * (serve_generation.c:1744) — true exactly on the turns where the close was + * injected. That flag is currently surfaced NOWHERE. Gating this whole + * layer on it is the right end state; until it is plumbed through, the + * trigger below stays deliberately conservative. + * + * NOTE the deliberate layering: this detector lives in the PARSER, not in the + * grammar. Making a nested opener off-grammar would mask the very tokens that + * carry the model's unfinished thought, leaving a differently-corrupted value + * and nothing to recover — and a TG_DEAD walk reports its mask INACTIVE + * (tg_mask_active), which would release the constraint entirely. */ +static const char THINK_CLOSE[] = ""; +static const char CALL_FUNCTION[] = "call_function{"; +static const char *const FRAME_MARKERS[] = { + TC_OPEN, /* "" — the wrapper opener, restarted inline */ + FN_OPEN, /* "" — the reasoning close, written inline */ + CALL_FUNCTION, /* "call_function{" — the Hermes-lineage call verb */ +}; +#define N_FRAME_MARKERS (sizeof(FRAME_MARKERS) / sizeof(*FRAME_MARKERS)) + +/* Malloc a whitespace-trimmed copy of [start,end). Never returns NULL for a + * valid range unless OOM (returns NULL then). */ +static char *dup_trim(const char *start, const char *end) +{ + while (start < end && isspace((unsigned char)*start)) start++; + while (end > start && isspace((unsigned char)end[-1])) end--; + size_t n = (size_t)(end - start); + char *out = malloc(n + 1); + if (!out) return NULL; + memcpy(out, start, n); + out[n] = '\0'; + return out; +} + +/* Peel an enclosing … wrapper off a whitespace-trimmed value. + * The grammar-forced dialect emits \n\nVALUE\n\n + * so, after dup_trim, `val` may be exactly "…" — return an owned + * copy of the inner VALUE (itself whitespace-trimmed) in that case. A value with + * no wrapper (or only a partial one) is passed through unchanged. NULL on OOM. + * Sets *unwrapped=1 iff a wrapper was actually peeled (so the caller can drop a + * now-empty without changing the plain-value/auto behavior). */ +static char *unwrap_value(char *val, int *unwrapped) +{ + if (unwrapped) *unwrapped = 0; + if (!val) return NULL; + size_t len = strlen(val); + size_t olen = strlen(VALUE_OPEN), clen = strlen(VALUE_CLOSE); + if (len < olen + clen) return val; + if (memcmp(val, VALUE_OPEN, olen) != 0) return val; + if (memcmp(val + len - clen, VALUE_CLOSE, clen) != 0) return val; + char *inner = dup_trim(val + olen, val + len - clen); + if (!inner) return val; /* OOM → keep the (wrapped) original rather than lose it */ + free(val); + if (unwrapped) *unwrapped = 1; + return inner; +} + +/* ============================================================ + * Tools system-block rendering + * ============================================================ */ + +char *tc_render_tools_system(const jr_value *tools, const char *system_content) +{ + if (jr_kind(tools) != JR_ARRAY || jr_array_len(tools) == 0) return NULL; + + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, TOOLS_HEAD); + int n = jr_array_len(tools); + for (int i = 0; i < n; i++) { + jw_raw(&jw, "\n"); + jr_to_json(jr_array_get(tools, i), &jw); /* tool | tojson */ + } + jw_raw(&jw, TOOLS_FORMAT); + if (system_content && *system_content) { + char *trimmed = dup_trim(system_content, system_content + strlen(system_content)); + if (trimmed && *trimmed) { + jw_raw(&jw, "\n\n"); + jw_raw(&jw, trimmed); + } + free(trimmed); + } + char *result = jw.oom ? NULL : strdup(jw.data); + jw_free(&jw); + return result; +} + +/* MiniCPM5's native tools preamble (from the model's chat_template): tools are + * listed as JSON inside …, and a call is emitted UNWRAPPED as + * VALUE…. Rendering + * the Qwen preamble instead makes MiniCPM improvise a garbled hybrid (it echoes + * the tool's JSON schema as nested / tags), so the + * orchestrator model MUST be prompted in its own dialect. */ +static const char MC_TOOLS_HEAD[] = + "# Tools\n\nYou are provided with function signatures within " + " XML tags:\n"; +static const char MC_TOOLS_FORMAT[] = + "\n\n\n" + "Tool usage guidelines:\n" + "- You may call zero or more functions. If no function calls are needed, " + "just answer normally and do not include any .\n" + "- When calling a function, return an XML object within " + " using:\n" + "param-value" + "\n" + "- param-value may be multi-line. If it contains <, & or newline " + "characters, wrap it in a CDATA block: " + ""; + +char *tc_render_tools_system_minicpm(const jr_value *tools, + const char *system_content) +{ + if (jr_kind(tools) != JR_ARRAY || jr_array_len(tools) == 0) return NULL; + + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, MC_TOOLS_HEAD); + int n = jr_array_len(tools); + for (int i = 0; i < n; i++) { + jw_raw(&jw, "\n"); + jr_to_json(jr_array_get(tools, i), &jw); /* tool | tojson */ + } + jw_raw(&jw, MC_TOOLS_FORMAT); + if (system_content && *system_content) { + char *trimmed = dup_trim(system_content, + system_content + strlen(system_content)); + if (trimmed && *trimmed) { + jw_raw(&jw, "\n\n"); + jw_raw(&jw, trimmed); + } + free(trimmed); + } + char *result = jw.oom ? NULL : strdup(jw.data); + jw_free(&jw); + return result; +} + +/* Ornith's native tools preamble: it lists tools as JSON in … + * (identical to the Qwen head) but instructs the JSON-object call form Ornith's + * RL training actually emits — a markdown-fenced {"function":NAME,"arguments": + * {…}} block — so the model's AUTO emission stays consistent with what the + * ORNITH_JSON parser accepts (rather than the XML its GGUF template advertises + * but the model does not use). */ +static const char ORN_TOOLS_FORMAT[] = + "\n\n\n" + "To use a tool, reply with a single markdown-fenced JSON object in this " + "exact form:\n\n" + "```json\n" + "{\n" + " \"function\": \"example_function_name\",\n" + " \"arguments\": {\n" + " \"example_parameter_1\": \"value_1\",\n" + " \"example_parameter_2\": \"value_2\"\n" + " }\n" + "}\n" + "```\n\n" + "\nReminder:\n" + "- Use the EXACT tool names and parameter names from the list above " + "— do not invent tool names, and use the real parameter keys (e.g. the " + "declared \"path\"), never the value as the key or a made-up key\n" + "- When the task requires reading, writing, editing a file, or running a " + "command, you MUST call the matching tool with the JSON block — do NOT just " + "print code or the file contents in your answer; the code only takes effect " + "when written through the tool\n" + "- The call MUST be a JSON object with a \"function\" name and an " + "\"arguments\" object; ALL required parameters MUST be present with real " + "values (never an empty arguments object)\n" + "- You may provide brief reasoning BEFORE the JSON block, but the block " + "itself must be valid JSON with no trailing text\n" + "- If no tool is needed, answer normally and do not mention tools\n" + ""; + +char *tc_render_tools_system_ornith(const jr_value *tools, + const char *system_content) +{ + if (jr_kind(tools) != JR_ARRAY || jr_array_len(tools) == 0) return NULL; + + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, TOOLS_HEAD); + int n = jr_array_len(tools); + for (int i = 0; i < n; i++) { + jw_raw(&jw, "\n"); + jr_to_json(jr_array_get(tools, i), &jw); /* tool | tojson */ + } + jw_raw(&jw, ORN_TOOLS_FORMAT); + if (system_content && *system_content) { + char *trimmed = dup_trim(system_content, + system_content + strlen(system_content)); + if (trimmed && *trimmed) { + jw_raw(&jw, "\n\n"); + jw_raw(&jw, trimmed); + } + free(trimmed); + } + char *result = jw.oom ? NULL : strdup(jw.data); + jw_free(&jw); + return result; +} + +/* ============================================================ + * Assistant tool-call history rendering (JSON args -> XML) + * ============================================================ */ + +/* Append a history parameter value the way the template re-renders it: + * objects/arrays as compact JSON, everything else as its bare text. */ +static void append_history_value(json_writer *jw, const jr_value *v) +{ + switch (jr_kind(v)) { + case JR_STRING: jw_raw(jw, jr_as_string(v)); break; + case JR_BOOL: jw_raw(jw, jr_as_bool(v, false) ? "true" : "false"); break; + case JR_NUMBER: { + double d = jr_as_number(v, 0); + if (d == (double)(long long)d) jw_printf(jw, "%lld", (long long)d); + else jw_printf(jw, "%.17g", d); + break; + } + case JR_NULL: break; + case JR_ARRAY: + case JR_OBJECT: jr_to_json(v, jw); break; + } +} + +/* Render one call's … body (name + parameters). */ +static void render_one_call(json_writer *jw, const jr_value *fn) +{ + const char *name = jr_as_string(jr_object_get(fn, "name")); + jw_raw(jw, "\n"); + /* arguments arrive as a stringified JSON object; parse and expand. */ + const char *args_str = jr_as_string(jr_object_get(fn, "arguments")); + jr_value *args = args_str ? jr_parse(args_str) : NULL; + if (jr_kind(args) == JR_OBJECT) { + int n = jr_object_member_count(args); + for (int i = 0; i < n; i++) { + const char *key = jr_object_key_at(args, i); + jw_raw(jw, "\n"); + append_history_value(jw, jr_object_value_at(args, i)); + jw_raw(jw, "\n\n"); + } + } + jr_free(args); + jw_raw(jw, "\n"); +} + +/* Render one call as Ornith's markdown-fenced JSON block: ```json {"function": + * NAME,"arguments":{…}} ```. `arguments` on the history call is already a + * stringified JSON object (the same shape tc_parse_tool_calls produced), so it + * is emitted verbatim inside the block. */ +static void render_one_call_ornith(json_writer *jw, const jr_value *fn) +{ + const char *name = jr_as_string(jr_object_get(fn, "name")); + const char *args_str = jr_as_string(jr_object_get(fn, "arguments")); + jw_raw(jw, "```json\n{\"function\": "); + jw_string(jw, name ? name : ""); + jw_raw(jw, ", \"arguments\": "); + jw_raw(jw, (args_str && *args_str) ? args_str : "{}"); + jw_raw(jw, "}\n```"); +} + +char *tc_render_assistant_tool_calls(const jr_value *tool_calls, + bool content_empty, tc_dialect dialect) +{ + if (jr_kind(tool_calls) != JR_ARRAY || jr_array_len(tool_calls) == 0) return NULL; + + json_writer jw; + if (!jw_init(&jw)) return NULL; + int n = jr_array_len(tool_calls); + for (int i = 0; i < n; i++) { + const jr_value *call = jr_array_get(tool_calls, i); + const jr_value *fn = jr_object_get(call, "function"); + if (jr_kind(fn) != JR_OBJECT) fn = call; /* tolerate flattened shape */ + if (i == 0) jw_raw(&jw, content_empty ? "" : "\n\n"); + else jw_raw(&jw, "\n"); + if (dialect == TC_DIALECT_ORNITH_JSON) { + render_one_call_ornith(&jw, fn); + } else { + jw_raw(&jw, "\n"); + render_one_call(&jw, fn); + jw_raw(&jw, ""); + } + } + char *result = jw.oom ? NULL : strdup(jw.data); + jw_free(&jw); + return result; +} + +/* ============================================================ + * Model-output parsing (XML -> structured calls) + * ============================================================ */ + +/* Is `fname` one of the request's declared tool names? Defense-in-depth name + * gate: the generation-time grammar (tool_grammar.c) enumerates the tool names + * as a closed alternation, but it only engages once a clean tool-call opener + * appears — a model that DEGENERATES in the reasoning/prose region (e.g. the Q3 + * saber-orch emitting a garbage "" name it never + * routed through the armed skeleton), or a step that fails open on 0 survivors, + * can still leave a bogus name in the raw text. This gate ensures the PARSER + * never surfaces a tool_call whose name is outside the request's tools set, so a + * hallucinated name can never reach the client. A NULL/empty tools array (no + * schema to validate against) is permissive → returns true. */ +bool tc_tool_name_known(const jr_value *tools, const char *fname) +{ + if (jr_kind(tools) != JR_ARRAY) return true; /* no schema → don't gate */ + int n = jr_array_len(tools); + if (n <= 0) return true; + for (int i = 0; i < n; i++) { + const jr_value *fn = jr_object_get(jr_array_get(tools, i), "function"); + const char *name = jr_as_string(jr_object_get(fn, "name")); + if (name && strcmp(name, fname) == 0) return true; + } + return false; +} + +/* JSON-schema "type" for parameter `pname` of function `fname`, or NULL. */ +static const char *param_type(const jr_value *tools, const char *fname, + const char *pname) +{ + if (jr_kind(tools) != JR_ARRAY) return NULL; + int n = jr_array_len(tools); + for (int i = 0; i < n; i++) { + const jr_value *fn = jr_object_get(jr_array_get(tools, i), "function"); + const char *name = jr_as_string(jr_object_get(fn, "name")); + if (!name || strcmp(name, fname) != 0) continue; + const jr_value *params = jr_object_get(fn, "parameters"); + const jr_value *props = jr_object_get(params, "properties"); + const jr_value *prop = jr_object_get(props, pname); + return jr_as_string(jr_object_get(prop, "type")); + } + return NULL; +} + +/* Write a parsed parameter value into the arguments object, JSON-typed per its + * schema `type`. Unknown / string types are emitted as JSON strings. + * + * THE RAW SPLICE IS THE ONLY UNCHECKED PATH, AND IT IS NO LONGER UNCHECKED. + * Every branch here either escapes the value into a JSON string — where any + * byte at all is safe — or VALIDATES it before handing the writer bytes it will + * not touch: "integer"/"number" through strtod, "boolean" through strcmp, and + * now "object"/"array" through the same well-formedness walk the decoding + * grammar runs (json_value_grammar.h). One definition of "well-formed", shared + * by the constraint and this re-check, so the two cannot disagree — the failure + * mode this module has hit twice. + * + * WHY DEGRADE TO A STRING RATHER THAN REJECT THE CALL. Splicing malformed bytes + * does not corrupt one ARGUMENT, it corrupts the whole `arguments` OBJECT: the + * client gets a string that will not parse, so every other argument of that call + * is lost too, and nothing on the wire says which member was at fault. That is + * exactly what three live `todowrite` calls did. Writing the bytes as a JSON + * string instead keeps the object parseable and moves the failure to where the + * knowledge is — the tool's own schema check, which can say "todos must be an + * array" and name it. Rejecting the call outright was the other candidate + * (pantheon-14) and is worse here: under tool_choice=required the model must + * emit something, so a rejection spends a whole turn (~200 s at fleet decode + * rates) to produce strictly less information than a typed refusal does. */ +static void write_typed_value(json_writer *jw, const char *val, const char *type, + const char *fname, const char *key) +{ + if (!type || strcmp(type, "string") == 0) { jw_string(jw, val); return; } + if (strcmp(type, "integer") == 0 || strcmp(type, "number") == 0) { + if (*val) { + char *end = NULL; + strtod(val, &end); + if (end != val && *end == '\0') { jw_raw(jw, val); return; } + } + jw_string(jw, val); + return; + } + if (strcmp(type, "boolean") == 0) { + if (strcmp(val, "true") == 0 || strcmp(val, "false") == 0) jw_raw(jw, val); + else jw_string(jw, val); + return; + } + if (strcmp(type, "object") == 0 || strcmp(type, "array") == 0) { + char want = (type[0] == 'a') ? '[' : '{'; + if (jvg_text_is_json_value(val, want)) { jw_raw(jw, val); return; } + /* SAY SO. Reached only with the decoding grammar off (it is off by + * default, and a request with no tools never builds one) or on a + * dialect it does not model — with the grammar armed, the bytes that + * get here cannot be malformed. */ + fprintf(stderr, "[tool_arg_not_json] %s.%s is declared \"%s\" but its " + "value is not well-formed JSON — emitted as a string so the rest " + "of the arguments object still parses [len=%zu val=<<<%.200s%s>>>]\n", + fname ? fname : "?", key ? key : "?", type, strlen(val), val, + strlen(val) > 200 ? "…[truncated]" : ""); + jw_string(jw, val); + return; + } + jw_string(jw, val); +} + +/* Does `v` contain any tool-call FRAME marker at all? Cheap trigger for the + * capture path: true only means "worth inspecting", never "reject". */ +static bool value_has_frame_marker(const char *v) +{ + for (size_t i = 0; i < N_FRAME_MARKERS; i++) + if (strstr(v, FRAME_MARKERS[i])) return true; + return false; +} + +/* Does `v` BEGIN (after leading whitespace) with the REASONING CLOSE? + * + * This is the one marker that rejects, and it is deliberately the only one. + * "" is a reasoning-channel control token: it is never the opening bytes + * of a real argument, but it IS what a model writes first when its think block + * was force-closed out from under it (a recorded native-XML capture, and the + * Hermes lead-in capture). So a value that starts with it is a captured frame, full stop. + * + * The other markers are NOT rejected on, even leading. "", + * "" really is a capture it is almost always followed by a + * complete call, which recover_captured_call handles without needing to reject. + * Rejecting on the untested markers would trade a recorded failure for an + * unmeasured one. */ +static bool value_starts_with_reasoning_close(const char *v) +{ + while (*v && isspace((unsigned char)*v)) v++; + return strncmp(v, THINK_CLOSE, strlen(THINK_CLOSE)) == 0; +} + +/* Recover a captured call out of the parameter value `value`. Defined below (it + * re-enters push_call for a captured XML call); declared here so push_call can + * consult it. Returns the number of calls pushed (0 = nothing recoverable), or + * -1 on OOM (sets *oom). */ +static int recover_captured_call(const char *value, const jr_value *tools, + tc_tool_call **calls, int *n, int *cap, + int *oom); + +/* Build the JSON arguments object for one … body spanning + * [body,limit). Returns an owned string, or NULL on OOM. + * + * `misroute_out` (nullable) receives an owned copy of the FIRST parameter value + * carrying a tool-call frame marker — the caller decides whether that value is a + * recoverable misroute, a rejectable frame fragment, or just content. + * + * `abandoned_key_out` (nullable) receives an owned copy of the FIRST parameter + * key that was opened, never closed with before `limit`, and holds + * no value bytes — the ABANDONED shape (see tc_parse_failure). */ +static char *parse_call_arguments(const char *body, const char *limit, + const jr_value *tools, const char *fname, + char **misroute_out, char **abandoned_key_out) +{ + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, "{"); + const char *pp = body; + int first = 1; + while (1) { + const char *pk = strstr(pp, PARAM_OPEN); + if (!pk || pk >= limit) break; + const char *kstart = pk + strlen(PARAM_OPEN); + const char *kend = memchr(kstart, '>', (size_t)(limit - kstart)); + if (!kend) break; + char *key = dup_trim(kstart, kend); + const char *vstart = kend + 1; + /* Was this parameter CLOSED before the call ended? The answer is the + * structural half of the abandoned test below AND the whole of the + * empty-value decision, so it is recorded rather than folded into the + * `vend` fallback: falling back to `limit` reads the same either way, + * but "there was no " is a fact the value alone cannot + * report. A `…` wrapper closes the value too — the + * branch below promotes that terminator into this flag. */ + const char *pclose = strstr(vstart, PARAM_CLOSE); + int closed = (pclose && pclose <= limit); + const char *vend = closed ? pclose : limit; + + /* Grammar-forced … wrapper: if the value region opens with + * (after leading whitespace), take as the value boundary + * directly. This both peels the wrapper AND survives a malformed + * close (the live MiniCPM-Q8 quirk emits a truncated + * "", so PARAM_CLOSE isn't found and vend fell through to the + * enclosing — dragging the wrapper + junk into the value). + * The model reliably closes , so anchor on it when present. */ + int unwrapped = 0; + char *val = NULL; + const char *vs = vstart; + while (vs < vend && isspace((unsigned char)*vs)) vs++; + size_t volen = strlen(VALUE_OPEN); + if ((size_t)(vend - vs) >= volen && memcmp(vs, VALUE_OPEN, volen) == 0) { + const char *inner = vs + volen; + const char *vclose = strstr(inner, VALUE_CLOSE); + const char *inner_end = (vclose && vclose <= limit) ? vclose : vend; + val = dup_trim(inner, inner_end); + unwrapped = 1; + /* Advance past so the next PARAM_OPEN scan resumes cleanly + * even when was malformed (vend would otherwise sit at + * the enclosing ). A found is ALSO a close: it + * is the terminator the model reliably emits, and the live MiniCPM + * "" quirk means can be absent from a value + * the model plainly finished. */ + if (vclose && vclose <= limit) { + const char *after = vclose + strlen(VALUE_CLOSE); + const char *pc = strstr(after, PARAM_CLOSE); + vend = (pc && pc <= limit) ? pc : after; + closed = 1; + } + } else { + val = dup_trim(vstart, vend); + val = unwrap_value(val, &unwrapped); /* peel a same-line … wrapper */ + } + /* Flag (don't alter) a value that carries the frame's own vocabulary. + * The value is still written normally: if the caller cannot recover a + * real call from it and it does not START with a marker, this stays + * exactly the call it is today. */ + if (misroute_out && !*misroute_out && val && *val && + value_has_frame_marker(val)) + *misroute_out = strdup(val); + /* ABANDONED: the parameter was opened, never closed, and nothing was + * written into it. Both halves are needed. Without `!closed` this + * would also fire on a deliberately empty , + * which is a complete emission; without the empty test it would fire on + * the live "" quirk, whose value is perfectly good. Only the + * conjunction means the model stopped mid-call. */ + if (abandoned_key_out && !*abandoned_key_out && !closed && + key && *key && (!val || !*val)) + *abandoned_key_out = strdup(key); + /* EMPTY-VALUE POLICY — `closed` is the whole test. + * + * An empty value the model CLOSED is a deliberate emission and is emitted + * as `""`. This module's own contract has always said so ("a complete, if + * useless, argument, and that call is still emitted"), but the code said + * otherwise: dropping it made the member ABSENT, and absent is a different + * claim. `edit`'s `new_string:""` is a legal deletion; arriving with + * `new_string` gone it reads as a call cut short, and the client charges it + * against a truncated-call budget that ends the task after six — a task + * abandoned over a call that was right every time. Deleting a line is an + * ordinary operation, so `{"path":"…"}` → `{"path":"…","new_string":""}`. + * + * An empty value the model did NOT close is still dropped. That is the + * truncation shape: the turn ended inside the value region, there is no + * terminator, and nothing was written. It is also reported — the abandoned + * test directly above fires on exactly this conjunction. + * + * WHY THIS IS SAFE FOR the empty-path wedge. That wedge was `{"path":""}` on a + * `read`, and it is REACHABLE AGAIN by design — the empty value is the + * model's, and hiding it as `{}` is what made the two indistinguishable in + * the first place. What changed is who answers it: an empty value that is + * meaningless for the parameter it landed on is a TOOL-LEVEL judgement + * (`path` can never be empty, `new_string` always can), and the client makes it + * with the schema in hand — loop_guard_args_void still trips on + * `{"path":""}`, and file_tools' require_string still refuses it. Neither + * can make that judgement about a member it never received. + * + * WHY A TRUNCATED CALL DOES NOT ARRIVE HERE AS A CLOSED EMPTY VALUE. Two + * independent gates, at different granularities. Call-level: with the tool + * grammar running, a turn that ended mid-call is SG_TOOL_GRAMMAR_INCOMPLETE + * and every call parsed out of it is refused before the wire + * (openai_chat.c, TC_PARSE_CALL_INCOMPLETE). Parameter-level — and this is + * the one that holds when no grammar ran at all (PANTHEON_TOOL_GRAMMAR is + * off by default, and a request with no tools never builds one) — `closed` + * is a fact about THESE BYTES: a cut inside a value leaves no terminator, + * so the value stays dropped exactly as before. The residue the two do not + * cover is a cut AFTER a closed parameter but before ``; there + * the empty value is still one the model deliberately closed, and the + * arguments that never got emitted are genuinely absent — the pre-existing + * truncation shape, which the client names via tool_schema_missing_required. */ + if (key && val && (*val || closed)) { + if (!first) jw_raw(&jw, ","); + first = 0; + jw_string(&jw, key); + jw_raw(&jw, ":"); + write_typed_value(&jw, val, param_type(tools, fname, key), + fname, key); + } + free(key); + free(val); + if (vend >= limit) break; + pp = vend + strlen(PARAM_CLOSE); + } + jw_raw(&jw, "}"); + char *result = jw.oom ? NULL : strdup(jw.data); + jw_free(&jw); + return result; +} + +const char *tc_parse_failure_code(tc_parse_failure kind) +{ + switch (kind) { + case TC_PARSE_CALL_ABANDONED: return "call_abandoned"; + case TC_PARSE_LEAKED_TOOL_TAGS: return "leaked_tool_tags"; + case TC_PARSE_REASONING_LEAK: return "reasoning_leak"; + case TC_PARSE_CALL_NAME_MISSING: return "call_name_missing"; + case TC_PARSE_CALL_NAME_UNKNOWN: return "call_name_unknown"; + case TC_PARSE_CALL_INCOMPLETE: return "call_incomplete"; + case TC_PARSE_NO_CALL_PARSED: return "no_call_parsed"; + case TC_PARSE_OK: break; + } + return NULL; +} + +void tc_parse_error_free(tc_parse_error *err) +{ + if (!err) return; + free(err->function_name); + free(err->parameter_name); + free(err->message); + err->function_name = NULL; + err->parameter_name = NULL; + err->message = NULL; + err->kind = TC_PARSE_OK; +} + +void tc_parse_repair_free(tc_parse_repair *rep) +{ + if (!rep) return; + free(rep->added); + free(rep->repaired_json); + rep->added = NULL; + rep->repaired_json = NULL; + rep->count = 0; +} + +/* Record a dropped call on `err`; the FIRST one wins (a truncated or mis-framed + * emission makes whatever follows it dependent, not corroborating). Best-effort + * on the strings: an OOM costs the detail, never the `kind`, so a client + * switching on the code still learns what happened. A NULL `fname`/`pname` is + * stored as NULL so the wire writer omits the field entirely — "" has + * no name to report, and saying "" would claim it does. */ +static void note_failure(tc_parse_error *err, tc_parse_failure kind, + const char *fname, const char *pname, const char *msg) +{ + if (!err || err->kind != TC_PARSE_OK) return; + err->kind = kind; + if (fname) err->function_name = strdup(fname); + if (pname) err->parameter_name = strdup(pname); + if (msg) err->message = strdup(msg); +} + +/* Record an abandoned call: the model opened a parameter and stopped. */ +static void note_abandoned_call(tc_parse_error *err, const char *fname, + const char *key) +{ + char msg[256]; + snprintf(msg, sizeof(msg), + "%s: call abandoned — parameter '%s' was opened but never given a value", + fname ? fname : "?", key ? key : "?"); + note_failure(err, TC_PARSE_CALL_ABANDONED, fname ? fname : "", + key ? key : "", msg); +} + +/* Record the two-frame mix: a JSON body closed with this dialect's own tail. */ +static void note_leaked_tool_tags(tc_parse_error *err, const char *fname) +{ + char msg[256]; + snprintf(msg, sizeof(msg), + "%s: mixed tool-call frames — a JSON body closed with " + ", which no and carries no recoverable call", + fname ? fname : "?"); + note_failure(err, TC_PARSE_REASONING_LEAK, fname, NULL, msg); +} + +/* Record an opener whose name is absent ("") or not a declared tool. */ +static void note_bad_call_name(tc_parse_error *err, const char *name) +{ + char msg[256]; + if (name && *name) { + snprintf(msg, sizeof(msg), + "%s: no such tool — the name is not in the request's tools", name); + note_failure(err, TC_PARSE_CALL_NAME_UNKNOWN, name, NULL, msg); + return; + } + note_failure(err, TC_PARSE_CALL_NAME_MISSING, NULL, NULL, + "tool call opened with no function name (\"\")"); +} + +/* THE TWO-FRAME MIX. Does the function body [body,fn_close) consist of a JSON + * body closed with THIS dialect's own "" tail — i.e. the + * model wrote OpenAI/Hermes-style JSON arguments where the + * blocks belong, then finished the call in the XML frame it was prompted in? + * + * qwen-code detects the same shape with + * LEAKED_TOOL_CALL_TAGS = /[}\]]\s*<\/parameter>\s*<\/function>/iy + * scanned by hasLeakedToolCallTags (geminiChat.ts:1358-1377), which walks the + * text tracking JSON string state and only tests the regex at a '}'/']' found + * OUTSIDE a string. That string-awareness is reproduced below and it is not + * decoration: a value like {"content":"cfg} "} matches + * the pattern textually, and a strstr would reject a perfectly good call over + * bytes the model deliberately put inside a quoted argument. + * + * ONE ADDITION their version does not need, and cannot be dropped here. Their + * stream is pure JSON — a "" in it is leakage by construction. Ours + * is the XML dialect, where "…}\n\n" is ALSO the exact, + * correct ending of a healthy call whose last argument has an object/array type. + * What separates them is whether anything ever OPENED that close: a real call + * writes first. So the tail only counts as a leak when the body + * carries no PARAM_OPEN at all — tested with the same unguarded strstr that + * parse_call_arguments itself uses to find parameters, so the two can never + * disagree about whether this body has any. + * + * `fn_close` is the block's "" (push_call's fend); it is NULL-safe and + * verified to actually BE that tag, because the caller passes the block limit + * when there was no close tag at all and a missing tail is not a mix. */ +static bool body_leaks_dialect_tail(const char *body, const char *fn_close) +{ + if (!fn_close || strncmp(fn_close, FN_CLOSE, strlen(FN_CLOSE)) != 0) + return false; + const char *opener = strstr(body, PARAM_OPEN); + if (opener && opener < fn_close) return false; /* a real call's own close */ + + size_t pclen = strlen(PARAM_CLOSE); + bool in_str = false, escaped = false; + for (const char *p = body; p < fn_close; p++) { + if (in_str) { + if (escaped) escaped = false; + else if (*p == '\\') escaped = true; + else if (*p == '"') in_str = false; + continue; + } + if (*p == '"') { in_str = true; continue; } + if (*p != '}' && *p != ']') continue; + const char *q = p + 1; + while (q < fn_close && isspace((unsigned char)*q)) q++; + if ((size_t)(fn_close - q) < pclen || memcmp(q, PARAM_CLOSE, pclen) != 0) + continue; + q += pclen; + while (q < fn_close && isspace((unsigned char)*q)) q++; + if (q == fn_close) return true; /* …} */ + } + return false; +} + +void tc_new_call_id(char *buf, size_t len) +{ + static const char hex[] = "0123456789abcdef"; + char rnd[9]; + for (int i = 0; i < 8; i++) rnd[i] = hex[rand() & 0xF]; + rnd[8] = '\0'; + snprintf(buf, len, "call_%s", rnd); +} + +/* Parse one … block whose opener is at `fp` (pointing at + * FN_OPEN) into a call, appending it to *calls (growing as needed). `limit` + * bounds the block (the enclosing , or end-of-text for a bare call); + * the name/ are searched within [fp,limit). On success returns the + * position just past this block's (or `limit` if none), so the + * caller can continue scanning. Returns NULL on OOM after freeing *calls (and + * sets *oom); a malformed block (no '>'-terminated name) returns NULL with *oom + * left 0 — the caller stops that scan without failing the whole parse. + * + * `depth` is 0 for a call found in the model's own output and 1 for one + * recovered out of a parameter VALUE; the misroute and abandoned checks run at + * depth 0 only, so recovery cannot recurse indefinitely. + * + * `err` (nullable) receives the reason a started call was deliberately NOT + * pushed — today only the abandoned shape. */ +static const char *push_call(const char *fp, const char *limit, + const jr_value *tools, + tc_tool_call **calls, int *n, int *cap, int *oom, + int depth, tc_parse_error *err) +{ + const char *nstart = fp + strlen(FN_OPEN); + const char *nend = memchr(nstart, '>', (size_t)(limit - nstart)); + if (!nend) return NULL; /* no name terminator → give up */ + char *name = dup_trim(nstart, nend); + const char *fend = strstr(nend, FN_CLOSE); + if (!fend || fend > limit) fend = limit; + const char *next = (fend < limit) ? fend + strlen(FN_CLOSE) : limit; + + if (name && *name && tc_tool_name_known(tools, name)) { + /* TWO-FRAME MIX. The body is JSON closed with the XML tail, so there is + * no 0) { + free(misroute); free(abandoned); free(name); free(args); + return next; /* the outer XML frame was never a real call */ + } + if (value_starts_with_reasoning_close(misroute)) { + /* REASONING LEAK (qwen-code converter.ts:1664 detects the same + * " inside the tool-call region" event). The frame is + * rejected exactly as before — but SAY SO. Dropping it silently + * left the client with a turn that produced neither a call nor a + * reason, which reads identically to a model that chose not to + * act. Nothing is reported on the `rec > 0` path above: there a + * real call was recovered, so nothing is absent. */ + note_reasoning_leak(err, name); + free(misroute); free(abandoned); free(name); free(args); + return next; + } + free(misroute); + } + /* ABANDONED CALL. The model opened an argument and stopped, so this is + * a truncation, not a call. Report it and drop it rather than emitting + * the `{}` it parses to: that `{}` is byte-identical to the one a + * deliberately empty argument produces, and a client cannot act on + * either — which is exactly how the live `read {}` became a dead end. */ + if (abandoned) { + note_abandoned_call(err, name, abandoned); + free(abandoned); free(name); free(args); + return next; + } + if (*n >= *cap) { + int ncap = *cap ? *cap * 2 : 4; + tc_tool_call *grown = realloc(*calls, (size_t)ncap * sizeof(**calls)); + if (!grown) { free(name); free(args); *oom = 1; return NULL; } + *calls = grown; + *cap = ncap; + } + char id[24]; + tc_new_call_id(id, sizeof(id)); + (*calls)[*n].id = strdup(id); + (*calls)[*n].name = name; + (*calls)[*n].arguments = args; + (*n)++; + } else { + /* empty name (a stray "") OR a hallucinated name outside the + * request's tools set → drop the block, never surface a bogus call. + * The drop is unchanged; what is new is NAMING it. qwen-code raises + * MALFORMED_TOOL_CALL for exactly this ("a tool call with no name", + * converter.ts:1699-1711), and the two cases are worth telling apart: + * a missing name is a broken emission, an unknown one is the model + * inventing a tool — only the second is something a client can put in + * front of the model as a correction. */ + if (depth == 0) note_bad_call_name(err, name); + free(name); + } + return next; +} + +/* ============================================================ + * MiniCPM5 attribute-style parsing (…) + * ============================================================ */ + +/* Extract the value of a `name="…"` / `name='…'` attribute inside the tag + * [tag_start, tag_end) (tag_end points at the '>' terminating the open tag). + * Returns an owned trimmed copy, or NULL if the attribute is absent. */ +static char *attr_value(const char *tag_start, const char *tag_end) +{ + const char *p = tag_start; + while (p < tag_end) { + const char *nm = memmem(p, (size_t)(tag_end - p), "name", 4); + if (!nm || nm >= tag_end) return NULL; + const char *q = nm + 4; + while (q < tag_end && isspace((unsigned char)*q)) q++; + if (q >= tag_end || *q != '=') { p = nm + 4; continue; } + q++; + while (q < tag_end && isspace((unsigned char)*q)) q++; + if (q >= tag_end || (*q != '"' && *q != '\'')) { p = nm + 4; continue; } + char quote = *q++; + const char *vend = memchr(q, quote, (size_t)(tag_end - q)); + if (!vend) return NULL; + return dup_trim(q, vend); + } + return NULL; +} + +/* Copy a param body [start,end) into an owned string, stripping an enclosing + * wrapper (CDATA content is taken verbatim, not whitespace- + * trimmed); a plain body is whitespace-trimmed. NULL on OOM. */ +static char *param_body_value(const char *start, const char *end) +{ + const char *s = start; + while (s < end && isspace((unsigned char)*s)) s++; + if ((size_t)(end - s) >= strlen(CDATA_OPEN) && + memcmp(s, CDATA_OPEN, strlen(CDATA_OPEN)) == 0) { + const char *cs = s + strlen(CDATA_OPEN); + const char *ce = memmem(cs, (size_t)(end - cs), CDATA_CLOSE, + strlen(CDATA_CLOSE)); + const char *cend = ce ? ce : end; + size_t n = (size_t)(cend - cs); + char *out = malloc(n + 1); + if (!out) return NULL; + memcpy(out, cs, n); + out[n] = '\0'; + return out; + } + return dup_trim(start, end); +} + +/* Build the JSON arguments object for one MiniCPM body spanning + * [body,limit), reading each VALUE. Owned string or + * NULL on OOM. */ +static char *parse_call_arguments_attr(const char *body, const char *limit, + const jr_value *tools, const char *fname) +{ + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, "{"); + const char *pp = body; + int first = 1; + while (1) { + const char *pk = memmem(pp, (size_t)(limit - pp), PARAM_ATTR_OPEN, + strlen(PARAM_ATTR_OPEN)); + if (!pk || pk >= limit) break; + const char *tag_end = memchr(pk, '>', (size_t)(limit - pk)); + if (!tag_end || tag_end >= limit) break; + char *key = attr_value(pk + strlen(PARAM_ATTR_OPEN), tag_end); + const char *vstart = tag_end + 1; + const char *vend = memmem(vstart, (size_t)(limit - vstart), + PARAM_ATTR_CLOSE, strlen(PARAM_ATTR_CLOSE)); + if (!vend || vend > limit) vend = limit; + char *val = param_body_value(vstart, vend); + if (key && *key && val) { + if (!first) jw_raw(&jw, ","); + first = 0; + jw_string(&jw, key); + jw_raw(&jw, ":"); + write_typed_value(&jw, val, param_type(tools, fname, key), + fname, key); + } + free(key); + free(val); + if (vend >= limit) break; + pp = vend + strlen(PARAM_ATTR_CLOSE); + } + jw_raw(&jw, "}"); + char *result = jw.oom ? NULL : strdup(jw.data); + jw_free(&jw); + return result; +} + +/* Parse one MiniCPM "…" block whose opener is at + * `fp` (pointing at FN_ATTR_OPEN). `end` bounds the whole text. On success + * returns the position just past this block's ; NULL on OOM (sets + * *oom, frees *calls) or a malformed opener (no '>' / no name → *oom left 0). */ +static const char *push_call_attr(const char *fp, const char *end, + const jr_value *tools, + tc_tool_call **calls, int *n, int *cap, int *oom) +{ + const char *tag_end = memchr(fp, '>', (size_t)(end - fp)); + if (!tag_end) return NULL; /* no open-tag terminator */ + char *name = attr_value(fp + strlen(FN_ATTR_OPEN), tag_end); + const char *body = tag_end + 1; + const char *fend = memmem(body, (size_t)(end - body), FN_ATTR_CLOSE, + strlen(FN_ATTR_CLOSE)); + const char *limit = fend ? fend : end; + const char *next = fend ? fend + strlen(FN_ATTR_CLOSE) : end; + + if (name && *name && tc_tool_name_known(tools, name)) { + char *args = parse_call_arguments_attr(body, limit, tools, name); + if (!args) { free(name); *oom = 1; return NULL; } + if (*n >= *cap) { + int ncap = *cap ? *cap * 2 : 4; + tc_tool_call *grown = realloc(*calls, (size_t)ncap * sizeof(**calls)); + if (!grown) { free(name); free(args); *oom = 1; return NULL; } + *calls = grown; + *cap = ncap; + } + char id[24]; + tc_new_call_id(id, sizeof(id)); + (*calls)[*n].id = strdup(id); + (*calls)[*n].name = name; + (*calls)[*n].arguments = args; + (*n)++; + } else { + free(name); /* empty/hallucinated name → drop the block */ + } + return next; +} + +/* True when `text` carries a MiniCPM attribute-style "" tag + * (as opposed to the Qwen "" form). Used to select the parser. */ +static bool has_attr_function(const char *text) +{ + return memmem(text, strlen(text), FN_ATTR_OPEN, strlen(FN_ATTR_OPEN)) != NULL; +} + +/* ============================================================ + * Ornith JSON-object parsing ({"function"|"name":NAME,"arguments":{…}}) + * ============================================================ */ + +/* Given `open` pointing at a top-level '{', return the position just PAST its + * matching '}', or NULL if the object never closes (truncated). Skips braces + * that appear inside JSON strings (honoring backslash escapes). */ +static const char *json_object_end(const char *open, const char *end) +{ + int depth = 0; + bool in_str = false; + for (const char *p = open; p < end; p++) { + char c = *p; + if (in_str) { + if (c == '\\') { p++; continue; } /* skip the escaped char */ + if (c == '"') in_str = false; + continue; + } + if (c == '"') { in_str = true; continue; } + if (c == '{') depth++; + else if (c == '}') { + depth--; + if (depth == 0) return p + 1; + } + } + return NULL; /* unbalanced / truncated */ +} + +/* The function NAME out of a JSON call object, under any of its spellings. + * "function" is the form the ornith preamble asks for, "name" the OpenAI/Hermes + * tool-schema form, and "func" the Hermes `call_function{…}` form the Q3 base + * bleeds through with (three recorded Hermes-shape captures). All three name the same + * thing, and the result still has to be one of the request's declared tools + * (tc_tool_name_known), so accepting the aliases only turns a silent no-op into the + * call the model asked for. */ +static const char *json_call_name(const jr_value *obj) +{ + const char *name = jr_as_string(jr_object_get(obj, "function")); + if (!name) name = jr_as_string(jr_object_get(obj, "name")); + if (!name) name = jr_as_string(jr_object_get(obj, "func")); + return name; +} + +/* The argument bag out of a JSON call object, under any of its spellings. */ +static const jr_value *json_call_args(const jr_value *obj) +{ + const jr_value *args = jr_object_get(obj, "arguments"); + if (jr_kind(args) != JR_OBJECT) args = jr_object_get(obj, "parameters"); + if (jr_kind(args) != JR_OBJECT) args = jr_object_get(obj, "args"); + return args; +} + +/* Append one call built from a parsed JSON call object `obj` (which carries a + * function-name string and an arguments object, under any of the spellings + * json_call_name/json_call_args accept) to *calls. Returns 0 on success + * (including a silently-dropped bad-shape/unknown-name object), -1 on OOM. The + * arguments object is re-serialized (compact) so the stored string is + * self-owned and independent of the DOM. */ +static int push_call_json(const jr_value *obj, const jr_value *tools, + tc_tool_call **calls, int *n, int *cap) +{ + if (jr_kind(obj) != JR_OBJECT) return 0; + const char *name = json_call_name(obj); + if (!name || !*name || !tc_tool_name_known(tools, name)) return 0; + + const jr_value *args = json_call_args(obj); + + json_writer jw; + if (!jw_init(&jw)) return -1; + if (jr_kind(args) == JR_OBJECT) jr_to_json(args, &jw); + else jw_raw(&jw, "{}"); + if (jw.oom) { jw_free(&jw); return -1; } + char *args_str = strdup(jw.data); + jw_free(&jw); + if (!args_str) return -1; + + if (*n >= *cap) { + int ncap = *cap ? *cap * 2 : 4; + tc_tool_call *grown = realloc(*calls, (size_t)ncap * sizeof(**calls)); + if (!grown) { free(args_str); return -1; } + *calls = grown; + *cap = ncap; + } + char id[24]; + tc_new_call_id(id, sizeof(id)); + (*calls)[*n].id = strdup(id); + (*calls)[*n].name = strdup(name); + (*calls)[*n].arguments = args_str; + (*n)++; + return 0; +} + +/* Push every call carried by ONE parsed top-level JSON object: either the + * {"tool_calls":[…]} wrapper form (each element a call) or the object itself + * when it names a function. A non-object, or an object that names nothing, + * pushes nothing. Returns 0 on success, -1 on OOM. */ +static int push_calls_from_json_object(const jr_value *obj, const jr_value *tools, + tc_tool_call **calls, int *n, int *cap) +{ + if (jr_kind(obj) != JR_OBJECT) return 0; + const jr_value *arr = jr_object_get(obj, "tool_calls"); + if (jr_kind(arr) == JR_ARRAY) { + int m = jr_array_len(arr); + for (int i = 0; i < m; i++) + if (push_call_json(jr_array_get(arr, i), tools, calls, n, cap) != 0) + return -1; + return 0; + } + if (json_call_name(obj)) + return push_call_json(obj, tools, calls, n, cap); + return 0; +} + +/* How deep a nesting the repair below models. A call object is + * {function, arguments:{…}} — two levels — so anything past this is not the + * shape we are recovering and declining costs nothing. */ +#define TC_JSON_REPAIR_MAX_DEPTH 32 + +/* CLOSE WHAT A TRUNCATED JSON OBJECT LEFT OPEN — the recovery qwen-code performs + * with JSON.parse(buffer + '"') and then jsonrepair + * (streamingToolCallParser.ts:294-323 and :409-439). We cannot take their + * second step: jsonrepair guesses (it will invent separators, quote bare words + * and close a dangling key with an empty value), and an invented empty value is + * precisely the `{"path":""}` defect this parser already fights. So this closes + * and NOTHING else. + * + * [open,end) is a '{'-rooted object that never closed. Appended, in order: + * • one '"' when a string is still open, and + * • the matching '}' / ']' for every container still open, innermost first. + * Nothing is inserted, replaced or removed inside the text. + * + * It DECLINES (returns NULL) whenever closing would require inventing something: + * • the truncation sits after a ',' ':' '{' or '[' — the next thing owed is a + * KEY or a VALUE, and there is nothing to close, only something to make up. + * `{"path":` repaired to `{"path":""}` and `"arguments":{` repaired to + * `"arguments":{}` are both a fabricated argument bag wearing a real call's + * name, which is worse than no call at all; + * • the open string has no content bytes yet (same fabricated-empty-value + * case, reached one character later); + * • the cut landed mid-escape ('\' with nothing after it, or a '\uXXXX' + * missing digits) — closing there changes what the escape means; + * • the nesting is deeper than TC_JSON_REPAIR_MAX_DEPTH, or a closer appeared + * with nothing open (not a truncation at all). + * + * Trailing whitespace and markdown-fence backticks are excluded from the output + * when no string is open — parse_ornith_json already ignores a LEADING ```json + * fence, so ignoring the closing one is the same rule, not a new liberty. They + * are never trimmed while a string is open, where those bytes are content. + * + * WHAT THIS BUYS AND WHAT IT COSTS. A string closed early keeps only the bytes + * the model got out, so a path cut at "/etc/host" is repaired to "/etc/host" and + * not to what the model meant. That is the same trade qwen-code takes, and it + * is the reason `added` is reported: a '"' in it marks exactly the calls whose + * values may be short. A wrong path fails loudly at the tool and the model can + * correct it; a lost call is a turn that silently did nothing. + * + * @param open points at the '{' that opens the truncated object + * @param end one past the last byte of the text + * @param added_out receives the owned string of appended bytes (caller frees) + * @return owned repaired JSON text (caller frees), or NULL when declined/OOM + */ +static char *json_repair_truncated(const char *open, const char *end, + char **added_out) +{ + char closers[TC_JSON_REPAIR_MAX_DEPTH]; + int depth = 0; + bool in_str = false, escaped = false; + int hex_owed = 0; /* digits still owed to a "\uXXXX" escape */ + size_t str_len = 0; /* content bytes in the string now open */ + const char *last_sig = NULL; /* last byte NOT INSIDE a string that is + * neither whitespace nor a fence backtick. + * Both quotes of a string count: they are the + * string's boundaries, and the CLOSING one is + * routinely the last real byte before the cut + * ({"path":"/x" owes only its braces). Miss it + * and body_end would slice the value away. */ + + for (const char *p = open; p < end; p++) { + char c = *p; + if (in_str) { + str_len++; + if (hex_owed) { hex_owed--; continue; } + if (escaped) { escaped = false; if (c == 'u') hex_owed = 4; continue; } + if (c == '\\') { escaped = true; continue; } + if (c == '"') { in_str = false; str_len = 0; last_sig = p; } + continue; + } + if (c == '"') { in_str = true; str_len = 0; last_sig = p; continue; } + if (c == '{' || c == '[') { + if (depth >= TC_JSON_REPAIR_MAX_DEPTH) return NULL; + closers[depth++] = (c == '{') ? '}' : ']'; + } else if (c == '}' || c == ']') { + if (depth == 0) return NULL; /* closed with nothing open */ + depth--; + } + if (!isspace((unsigned char)c) && c != '`') last_sig = p; + } + + if (depth == 0) return NULL; /* balanced — not a truncation */ + if (in_str) { + if (escaped || hex_owed || str_len == 0) return NULL; + } else { + if (!last_sig) return NULL; + char c = *last_sig; + if (c == ',' || c == ':' || c == '{' || c == '[') return NULL; + } + + const char *body_end = in_str ? end : last_sig + 1; + size_t body_len = (size_t)(body_end - open); + size_t add_len = (in_str ? 1u : 0u) + (size_t)depth; + char *added = malloc(add_len + 1); + char *out = malloc(body_len + add_len + 1); + if (!added || !out) { free(added); free(out); return NULL; } + + size_t k = 0; + if (in_str) added[k++] = '"'; + for (int i = depth - 1; i >= 0; i--) added[k++] = closers[i]; + added[k] = '\0'; + memcpy(out, open, body_len); + memcpy(out + body_len, added, add_len); + out[body_len + add_len] = '\0'; + *added_out = added; + return out; +} + +/* Repair the truncated object at `open` and push whatever call it carries. + * Returns 1 when a call was recovered (and `rep` updated), 0 when the repair was + * declined or carried nothing, -1 on OOM. */ +static int repair_truncated_json_call(const char *open, const char *end, + const jr_value *tools, + tc_tool_call **calls, int *n, int *cap, + tc_parse_repair *rep) +{ + char *added = NULL; + char *fixed = json_repair_truncated(open, end, &added); + if (!fixed) return 0; + + jr_value *obj = jr_parse(fixed); /* STRICT re-parse: the repair is only + * ever accepted if it produced real JSON */ + int before = *n; + int rc = push_calls_from_json_object(obj, tools, calls, n, cap); + jr_free(obj); + if (rc != 0) { free(added); free(fixed); return -1; } + if (*n == before) { free(added); free(fixed); return 0; } + + if (rep) { + rep->count++; + free(rep->added); + free(rep->repaired_json); + rep->added = added; + rep->repaired_json = fixed; + return 1; + } + free(added); + free(fixed); + return 1; +} + +/* Push every call carried by ONE balanced top-level object [open,obj_end) — + * the per-object step of parse_ornith_json, shared with tc_parse_ornith_object + * so the incremental streamer and the end-of-turn parse cannot disagree about + * what an object yields. Returns 0 on success, -1 on OOM. */ +static int push_calls_from_object_slice(const char *open, const char *obj_end, + const jr_value *tools, + tc_tool_call **calls, int *n, int *cap) +{ + char *slice = dup_trim(open, obj_end); + jr_value *obj = slice ? jr_parse(slice) : NULL; + free(slice); + int rc = push_calls_from_json_object(obj, tools, calls, n, cap); + jr_free(obj); + return rc; +} + +int tc_parse_ornith_object(const char *open, const char *obj_end, + const jr_value *tools, tc_tool_call **out, int *n_out) +{ + *out = NULL; + *n_out = 0; + int cap = 0; + if (push_calls_from_object_slice(open, obj_end, tools, out, n_out, &cap) != 0) { + tc_tool_calls_free(*out, *n_out); + *out = NULL; + *n_out = 0; + return -1; + } + return 0; +} + +/* Parse Ornith's JSON emission: scan `text` for top-level '{' objects, parse + * each balanced object, and emit a call for any that carries a function name + + * arguments. Handles a markdown ```json fence (the '{' is found regardless of + * the fence), a bare object, repeated objects (parallel calls), and an + * "arguments" whose value is itself an array of {function,arguments} objects + * (an alternate parallel shape). Malformed JSON → 0 calls; a TRUNCATED trailing + * object is repaired first when it can be closed without guessing, and yields 0 + * calls otherwise. Returns 0 on success, -1 on OOM — the caller owns *calls in + * both cases and frees it. + * + * `tail_out` (nullable) receives the position just past the LAST object that + * yielded a call, so the misroute recovery can tell "this value IS a call" from + * "this value CONTAINS a call as content". + * + * `rep` both ENABLES and records the truncated-object repair; NULL declines to + * repair at all. The two are one switch on purpose — a repair that nobody + * accounts for is exactly the silent papering-over this is meant to avoid — and + * NULL is what the capture-recovery path wants: it is asking whether a parameter + * value IS a complete call, and repairing a truncated one there would answer a + * different question. */ +static int parse_ornith_json(const char *text, const jr_value *tools, + tc_tool_call **calls, int *n, int *cap, + const char **tail_out, tc_parse_repair *rep) +{ + const char *end = text + strlen(text); + const char *p = text; + while ((p = memchr(p, '{', (size_t)(end - p))) != NULL) { + const char *obj_end = json_object_end(p, end); + if (!obj_end) { + /* Truncated trailing object. A truncation is TERMINAL — everything + * to `end` belongs to this object — so this is the last thing to + * try either way, and the scan stops after it exactly as before. */ + if (rep) { + int rc = repair_truncated_json_call(p, end, tools, calls, n, cap, + rep); + if (rc < 0) return -1; + if (rc > 0 && tail_out) *tail_out = end; + } + break; + } + int before = *n; + if (push_calls_from_object_slice(p, obj_end, tools, calls, n, cap) != 0) + return -1; + if (tail_out && *n > before) *tail_out = obj_end; + /* Continue AFTER this object so repeated/parallel blocks are all seen. */ + p = obj_end; + } + return 0; +} + +/* After a recovered call, what may still legally trail it inside the value: + * whitespace, an over-closed brace or tag ('}' / '>' — recorded captures emit + * "}}}" where two braces close the object), a markdown fence, and the frame + * closers the model started to write. Anything else means the value merely + * CONTAINED a call as content, so it is not a misroute and must not be + * rewritten. */ +static bool recovered_tail_is_clean(const char *p, const char *end) +{ + size_t tclen = strlen(TC_CLOSE), fclen = strlen(FN_CLOSE); + while (p < end) { + size_t rem = (size_t)(end - p); + if (rem >= tclen && memcmp(p, TC_CLOSE, tclen) == 0) { p += tclen; continue; } + if (rem >= fclen && memcmp(p, FN_CLOSE, fclen) == 0) { p += fclen; continue; } + if (isspace((unsigned char)*p) || *p == '}' || *p == '>' || *p == '`') { + p++; + continue; + } + return false; + } + return true; +} + +/* Recover the call the model ACTUALLY asked for out of a captured parameter + * value. + * + * `value` is one parameter's whitespace-trimmed VALUE that carried a frame + * marker. If it is itself a complete call — captured XML + * (…, the NATIVE shape seen in a recorded native-XML capture) + * or a JSON call object, fenced or bare, in any of the spellings + * json_call_name/json_call_args accept — that call is pushed and the outer frame + * is discarded by the caller. + * + * Note what this does NOT claim. It does not decide that any shape is a given + * model's "native dialect"; it only reads back a call the model finished + * writing in a slot that was never meant to hold one. The native-XML case needs + * no dialect assumption at all, and the `func`/`args` spelling is accepted by + * json_call_name/json_call_args as one more alias, not as a model property. + * + * Two gates keep this from hijacking legitimate content: + * • the caller only reaches here when the value carries a frame marker, so a + * value that is merely a JSON object (a real `write` payload, say) is never + * inspected at all; + * • the recovered call must run to the END of the value (recovered_tail_is_clean). + * A value that CONTAINS a call and then continues is content, not a frame. + * Recovered names still pass tc_tool_name_known, so an unknown tool cannot appear. + * + * @return number of calls pushed (0 = nothing recoverable), -1 on OOM (*oom set) + */ +static int recover_captured_call(const char *value, const jr_value *tools, + tc_tool_call **calls, int *n, int *cap, + int *oom) +{ + const char *end = value + strlen(value); + const char *tail = NULL; + int before = *n; + + const char *fp = strstr(value, FN_OPEN); + if (fp) { + const char *next = push_call(fp, end, tools, calls, n, cap, oom, 1, NULL); + if (*oom) return -1; + tail = next ? next : end; + } else if (parse_ornith_json(value, tools, calls, n, cap, &tail, NULL) != 0) { + /* NULL `rep`: no truncated-object repair here. This asks whether the + * value IS a complete call, and a repaired truncation is not one. */ + *oom = 1; + return -1; + } + + if (*n == before) return 0; /* nothing parsed */ + if (tail && recovered_tail_is_clean(tail, end)) return *n - before; + + /* Parsed, but the value continues past it → content, not a frame. Undo. */ + for (int i = before; i < *n; i++) { + free((*calls)[i].id); + free((*calls)[i].name); + free((*calls)[i].arguments); + } + *n = before; + return 0; +} + +int tc_parse_tool_calls(const char *text, const jr_value *tools, + tc_dialect dialect, tc_tool_call **out, int *n_out) +{ + return tc_parse_tool_calls_diagnosed(text, tools, dialect, out, n_out, + NULL, NULL); +} + +int tc_parse_tool_calls_diagnosed(const char *text, const jr_value *tools, + tc_dialect dialect, tc_tool_call **out, + int *n_out, tc_parse_error *err, + tc_parse_repair *repair) +{ + *out = NULL; + *n_out = 0; + if (err) *err = (tc_parse_error){0}; + if (repair) *repair = (tc_parse_repair){0}; + if (!text) return 0; + + tc_tool_call *calls = NULL; + int n = 0, cap = 0, oom = 0; + const char *end = text + strlen(text); + + if (dialect == TC_DIALECT_ORNITH_JSON) { + /* Ornith emits a markdown-fenced or bare JSON object rather than XML. + * Parse that shape into the same structured calls. */ + if (parse_ornith_json(text, tools, &calls, &n, &cap, NULL, repair) != 0) { + tc_tool_calls_free(calls, n); + tc_parse_repair_free(repair); + return -1; + } + } else if (has_attr_function(text)) { + /* MiniCPM5 attribute-style: bare, unwrapped + * VALUE…. + * No wrapper and no "') → stop */ + p = (next > p) ? next : p + strlen(FN_ATTR_OPEN); + } + } else if (strstr(text, TC_OPEN) != NULL) { + /* Wrapped form: one per …. Unchanged + * behaviour — a trailing with no opener is simply never a + * TC_OPEN match, and a wrapper missing its falls back to the + * (or end) as the block limit. */ + const char *p = text; + while ((p = strstr(p, TC_OPEN)) != NULL) { + const char *tc_end = strstr(p, TC_CLOSE); + const char *limit = tc_end ? tc_end : end; + const char *fp = strstr(p, FN_OPEN); + if (!fp || fp >= limit) { p += strlen(TC_OPEN); continue; } + const char *next = push_call(fp, limit, tools, &calls, &n, &cap, &oom, + 0, err); + if (oom) { tc_tool_calls_free(calls, n); tc_parse_error_free(err); return -1; } + (void)next; /* wrapped path advances by the wrapper, not the block */ + if (!tc_end) break; + p = tc_end + strlen(TC_CLOSE); + } + } else { + /* Bare form: no wrapper at all — scan every bare + * block directly. Leading reasoning text before + * the first opener is skipped; a doubled/again just yields no + * further FN_OPEN and ends the scan; a stray "' or + * no close is dropped without emitting a garbage call. */ + const char *p = text; + while ((p = strstr(p, FN_OPEN)) != NULL) { + const char *next = push_call(p, end, tools, &calls, &n, &cap, &oom, + 0, err); + if (oom) { tc_tool_calls_free(calls, n); tc_parse_error_free(err); return -1; } + if (!next) break; /* malformed opener (no name '>') → stop */ + p = (next > p) ? next : p + strlen(FN_OPEN); + } + } + + *out = calls; + *n_out = n; + return 0; +} + +void tc_tool_calls_free(tc_tool_call *calls, int n) +{ + if (!calls) return; + for (int i = 0; i < n; i++) { + free(calls[i].id); + free(calls[i].name); + free(calls[i].arguments); + } + free(calls); +} diff --git a/src/serve/tool_calling.h b/src/serve/tool_calling.h new file mode 100644 index 0000000..88136b1 --- /dev/null +++ b/src/serve/tool_calling.h @@ -0,0 +1,401 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_calling.h + * @brief OpenAI tool-calling glue for the serve layer, matching the Nex-N2-mini + * (qwen35moe) chat_template. The model uses the Qwen3-Coder XML dialect: + * tools are listed as JSON inside a … system block, and a + * call is emitted as + * + * + * + * VALUE + * + * + * + * This module renders that prompt section, renders assistant tool-call + * history back into the same XML, and parses model output into structured + * calls whose arguments are typed per the tools' JSON schema. + * + * @status Renders tools + tool/assistant history; parses single and parallel + * calls; scalar/bool/number/object/array typing from the schema; names + * an ABANDONED call (tc_parse_tool_calls_diagnosed) instead of letting + * it pass as an empty-argument one; names the other STRUCTURALLY WRONG + * shapes (leaked dialect tail, reasoning leak, missing/unknown name); + * and closes a truncated JSON argument object when what is open can be + * closed without guessing (tc_parse_repair). TC_PARSE_CALL_INCOMPLETE + * is the serve-layer companion: a call whose decoding grammar never + * closed is refused rather than emitted with an argument missing. An + * empty value the model CLOSED is emitted as "" rather than dropped, so + * The client sees empty-and-present instead of absent. + * @issues Consecutive tool results are rendered as separate user turns rather + * than grouped into one (the model handles either; token-identical + * grouping is a fidelity nicety, not a correctness need). + * @todo None + */ + +#ifndef PN_SERVE_TOOL_CALLING_H +#define PN_SERVE_TOOL_CALLING_H + +#include "json_reader.h" + +/* Per-model tool-call dialect. A resident model gets the dialect it ACTUALLY + * emits at runtime, resolved ONCE post-route in chat_apply_tool_dialect + * (openai_chat.c) from the model name/arch (see tc_dialect_for_model) and then + * threaded to the parser, the stream emitter, and the grammar setup. The GGUF + * chat_template is NOT the discriminator: ornith's template claims Qwen-XML but + * its RL-learned habit emits a markdown-fenced JSON object, so the dialect must + * be declared per model rather than inferred from the template. + * QWEN_XML — n2 / saber: VALUE… + * (the historical default). + * MINICPM_XML — VALUE… + * (folded in from the pre-existing MiniCPM special case). + * ORNITH_JSON — a markdown-fenced (```json …```) or bare JSON object + * {"function"|"name":NAME,"arguments":{…}} (Ornith-1.0-9B). */ +typedef enum { + TC_DIALECT_QWEN_XML = 0, + TC_DIALECT_MINICPM_XML, + TC_DIALECT_ORNITH_JSON, +} tc_dialect; + +/* One tool call parsed out of model output (owned strings). `arguments` is a + * stringified JSON object, ready to place in the OpenAI response verbatim. */ +typedef struct { + char *id; /* "call_XXXXXXXX" */ + char *name; /* function name */ + char *arguments; /* stringified JSON object, e.g. {"location":"Paris"} */ +} tc_tool_call; + +/* Why a call the model STARTED never became a usable call. This is a REASON + * FOR AN ABSENCE, not a warning riding a call that was still emitted: when it + * is set, the call it describes is deliberately NOT in the result array. + * + * TC_PARSE_CALL_ABANDONED is the shape recorded live on 2026-08-18 (three times + * byte-identical, tests/abandoned_call_fixture.h): the model opened a tool, + * opened , wrote no value bytes at all, and then jumped to + * — skipping entirely. Both halves are required and + * both are structural, not stylistic: + * • the parameter was never closed before the call ended (by , by + * end of text, or by anything else), AND + * • the value region between the opener and the call's end is empty once + * whitespace is discounted. + * The conjunction is what makes this a fact rather than a guess. A parameter + * that IS closed around an empty value is a different event — the model wrote a + * complete argument that happens to be empty — and that call is emitted WITH the + * empty value: `{"path":""}`, not `{}`. A parameter whose close tag is malformed + * but whose value carries content (the live MiniCPM-Q8 "" quirk) is a + * truncated close over a good value, and that call is emitted unchanged. Only + * "no close AND no value" means the model stopped in the middle of writing the + * call. + * + * EMPTY IS NOT ABSENT, AND THE SERVE MUST NOT CONFLATE THEM. This header said + * "a complete, if useless, argument" while the code dropped that argument, which + * made it absent — a different claim, and the wrong one. `edit`'s + * `new_string:""` is a legal deletion; arriving as `{"path":…,"old_string":…}` it + * is indistinguishable from a call cut short before `new_string`, and the client + * charges it against a truncated-call budget that ends the task. Whether an + * empty value is USEFUL is a question about the parameter (`path` never, and + * `{"path":""}` is the empty-path wedge; `new_string` always), so it is + * answered where the tool's meaning lives — the client — and it can only be + * answered about a member that actually arrived. + * + * The other four kinds are the same species — a call the model STARTED that is + * deliberately not in the result array — for the shapes that are structurally + * wrong rather than merely unparseable. Each is a decidable fact about the + * bytes, never a guess about intent: + * + * TC_PARSE_LEAKED_TOOL_TAGS — the model mixed TWO frames: it wrote a JSON body + * where the blocks belong and then closed with this dialect's + * own "" tail. Detected only when the body carries NO + * " it closes with has nothing + * to close — a legitimate call whose LAST argument is a JSON object ends in + * the same "}" bytes and must not be touched. + * + * TC_PARSE_REASONING_LEAK — the parameter value the model wrote BEGINS with + * "": a reasoning close landed inside the tool-call region, so the + * value is captured thought rather than an argument. Reported only when no + * real call could be recovered out of that value; when one can be, the + * recovered call is returned and nothing is reported. + * + * TC_PARSE_CALL_NAME_MISSING — "": an opener with no name in it. + * TC_PARSE_CALL_NAME_UNKNOWN — a name outside the request's `tools` set (the + * tc_tool_name_known gate). Both were dropped SILENTLY before; the drop is + * still right, but the client could not tell it from a turn that simply + * chose not to call anything. + * + * TC_PARSE_CALL_INCOMPLETE and TC_PARSE_NO_CALL_PARSED are serve-layer verdicts + * and are NOT raised by this module — the parser sees only text, and both turn on + * something outside it. + * + * TC_PARSE_CALL_INCOMPLETE — the DECODING GRAMMAR entered a call and the turn + * ended before that call closed. This is the truncation, stated by the one + * component that can state it: the parser is a text scanner and a truncated + * call is, by construction, text that scans perfectly — it is a legal PREFIX + * of a good call. The grammar knows it is a prefix; nothing else does. So + * the serve asks the grammar (tg_complete / SG_TOOL_GRAMMAR_INCOMPLETE in + * serve_generation.h) and refuses to hand back a call the model never + * finished, rather than shipping the argument-shaped remains of one. + * Measured in real serving: 9/9 truncated calls were "grammar accepted + * char-for-char, tg_complete == NO", and both controls were complete. + * NOTE what this deliberately does NOT do: nothing is masked to make the + * model keep emitting. A model that has stopped, compelled to continue, + * writes a corrupted file instead of failing cleanly. Detect and report. + * + * TC_PARSE_NO_CALL_PARSED is the catch-all: raw tool text exists, zero calls + * came out, and none of the kinds above explained why (the parser cannot know + * that the stream emitter diverted a tool region at all). */ +typedef enum { + TC_PARSE_OK = 0, + TC_PARSE_CALL_ABANDONED, + TC_PARSE_LEAKED_TOOL_TAGS, + TC_PARSE_REASONING_LEAK, + TC_PARSE_CALL_NAME_MISSING, + TC_PARSE_CALL_NAME_UNKNOWN, + TC_PARSE_CALL_INCOMPLETE, + TC_PARSE_NO_CALL_PARSED, +} tc_parse_failure; + +/* A structured description of the failure above (owned strings, all nullable). + * Zero-initialize before use; release with tc_parse_error_free. */ +typedef struct { + tc_parse_failure kind; + char *function_name; /* the tool the model opened, e.g. "read" */ + char *parameter_name; /* the key left open, e.g. "path" */ + char *message; /* human-readable, e.g. "read: call abandoned — …" */ +} tc_parse_error; + +/* A JSON argument object that parsed ONLY AFTER REPAIR — the opposite kind of + * event from tc_parse_error above. Nothing is absent here: a call the model + * truncated mid-JSON was recovered by closing what it left open. It is reported + * so the serve can COUNT it, because a silent repair would hide the model defect + * it is papering over. + * + * The repair is deliberately narrow (see json_repair_truncated in the .c): it + * appends ONLY closers — one '"' for a string still open, then the matching + * '}'/']' for each container still open, innermost first — and then re-parses + * strictly. It never invents a key, a value, or a separator, and it DECLINES + * outright when the truncation point would require one (a trailing ',' ':' '{' + * or '[', a half-written escape). Whatever it cannot close this way fails + * exactly as it does today. + * + * `added` is the literal byte string appended, which is also the honest record + * of the risk taken: a '"' in it means a STRING was closed, so the value carries + * only as much text as the model got out before it was cut off. + * + * `count` counts repairs that produced a call — the number of tool calls that + * would otherwise have been lost outright. */ +typedef struct { + int count; /* repairs that yielded ≥1 call (0 = none happened) */ + char *added; /* bytes appended by the LAST repair, e.g. "\"}}" */ + char *repaired_json; /* the LAST repaired object text, for the log */ +} tc_parse_repair; + +/** + * Stable wire code for a parse failure, for a client to switch on. + * + * @param kind the failure kind + * @return a static string ("call_abandoned", "leaked_tool_tags", + * "reasoning_leak", "call_name_missing", "call_name_unknown", + * "call_incomplete", "no_call_parsed"), or NULL for TC_PARSE_OK + * + * Thread-safe: yes (returns static storage) + */ +const char *tc_parse_failure_code(tc_parse_failure kind); + +/** Free the owned members of `err` and reset it to TC_PARSE_OK. NULL-safe. */ +void tc_parse_error_free(tc_parse_error *err); + +/** Free the owned members of `rep` and reset its count to 0. NULL-safe. */ +void tc_parse_repair_free(tc_parse_repair *rep); + +/** + * Resolve the tool-call dialect for a resident model from its registry name and + * a caller-computed MiniCPM flag. The name table wins first ("ornith*" → + * ORNITH_JSON), then the MiniCPM flag → MINICPM_XML, else the Qwen-XML default. + * This is the single source of truth the post-route resolver + * (chat_apply_tool_dialect) calls, so a new same-arch model can be declared by + * name without touching the parser/grammar. `is_minicpm` folds in the existing + * model_is_minicpm(arch,add_bos) test (passed by the caller, which owns the + * pn_model). A NULL name defaults to Qwen-XML. + * + * @param name registry model name (may be NULL) + * @param is_minicpm true if the caller's model matches the MiniCPM arch test + * @return the resolved tc_dialect + * + * Thread-safe: yes (pure function of its arguments) + */ +tc_dialect tc_dialect_for_model(const char *name, bool is_minicpm); + +/** + * Does a tool grammar model this dialect's call format? QWEN_XML is the + * tool_grammar.c skeleton, ORNITH_JSON is tool_grammar_json.c. MINICPM_XML's + * attribute form (``) is not modelled, so a + * "required" turn cannot be guaranteed a parsed call for it. + * + * @param dialect the resolved dialect + * @return true when a grammar walks exactly that dialect's call + * + * Thread-safe: yes (pure function of its argument) + */ +bool tc_dialect_grammar_models(tc_dialect dialect); + +/** + * Render the system-block *content* for the tools preamble, matching the + * model's chat_template (the "# Tools … … … …" + * text). If `system_content` is non-NULL and non-empty it is appended after a + * blank line, exactly as the template merges a leading system message. + * + * @param tools OpenAI `tools` array (JR_ARRAY of tool objects). + * @param system_content Optional leading system message text, or NULL. + * @return Owned NUL-terminated string (caller frees), or NULL on OOM / if + * `tools` is not a non-empty array. + * + * Thread-safe: yes (no shared state) + */ +char *tc_render_tools_system(const jr_value *tools, const char *system_content); + +/** + * MiniCPM5 variant of tc_render_tools_system. MiniCPM5 uses a different tool + * dialect than the Qwen/N2 default: tools are listed in … and a + * call is emitted UNWRAPPED as + * VALUE… + * (multi-line/special values CDATA-wrapped). Rendering the Qwen preamble to a + * MiniCPM model makes it improvise a garbled hybrid, so the orchestrator model + * must be prompted with this native preamble. Same ownership/return contract as + * tc_render_tools_system. + */ +char *tc_render_tools_system_minicpm(const jr_value *tools, + const char *system_content); + +/** + * Ornith variant of tc_render_tools_system. Ornith's RL-learned tool habit emits + * a markdown-fenced JSON object {"function":NAME,"arguments":{…}} rather than the + * Qwen XML its template advertises. This preamble shows that JSON form so the + * model's AUTO emission is consistent with what the ORNITH_JSON parser accepts. + * Same ownership/return contract as tc_render_tools_system. + */ +char *tc_render_tools_system_ornith(const jr_value *tools, + const char *system_content); + +/** + * Render an assistant history message's `tool_calls` into the model's XML call + * syntax, to be appended after the assistant text when re-feeding history so a + * multi-step tool loop reconstructs the same prompt the model produced. + * + * @param tool_calls OpenAI assistant.tool_calls array (JR_ARRAY), or NULL. + * @param content_empty true if the assistant message had no text content + * (controls the leading separator, per the template). + * @param dialect the resident model's dialect: QWEN_XML/MINICPM_XML render + * the XML form; ORNITH_JSON renders the fenced JSON block the + * model emits, so a multi-turn loop feeds ornith its own + * dialect back. + * @return Owned string (possibly empty), or NULL on OOM / no calls. + * + * Thread-safe: yes + */ +char *tc_render_assistant_tool_calls(const jr_value *tool_calls, + bool content_empty, tc_dialect dialect); + +/** + * Parse model output into structured calls. Accepts BOTH forms the model emits: + * the canonical wrapped … and a + * bare … block with no wrapper (the Q3 + * nex-n2-mini uses the bare form most of the time). Both yield identical calls. + * Real-world noise is tolerated: leading natural-language reasoning before the + * first with no opener, and a doubled + * . Text with no / … + * vocabulary makes "opened but never closed" and "closed but never + * opened" decidable questions. MiniCPM's attribute form and the ORNITH_JSON + * object form still drop a bad call silently here; the serve layer's + * TC_PARSE_NO_CALL_PARSED catch-all covers them. + * + * @param err Zero-initialized tc_parse_error, or NULL to opt out. On return, + * `kind` is TC_PARSE_OK when nothing was dropped. Owned strings — + * release with tc_parse_error_free. + * @param repair Zero-initialized tc_parse_repair, or NULL. It both ENABLES and + * records the truncated-object repair: passing NULL declines to + * repair at all, so a caller that is not accounting for a repair + * never silently gets one. On return, `count` is 0 when no repair + * fired. Owned strings — release with tc_parse_repair_free. + * @return 0 on success (including zero calls), -1 on OOM. + * + * Thread-safe: yes (uses rand() for ids — not cryptographic) + */ +int tc_parse_tool_calls_diagnosed(const char *text, const jr_value *tools, + tc_dialect dialect, tc_tool_call **out, + int *n_out, tc_parse_error *err, + tc_parse_repair *repair); + +/** + * The calls ONE balanced ORNITH_JSON top-level object yields — exactly what + * tc_parse_tool_calls produces for that object when it is one of several in a + * text (the same per-object code runs in both). Used by the incremental + * tool-call streamer (tool_call_stream.h) the moment an object closes, so what + * it has already put on the wire can be checked against the parse. + * + * @param open the object's opening '{' + * @param obj_end one past its matching '}' + * @param tools the request `tools` array (name gate), may be NULL + * @param out set to an owned array (free with tc_tool_calls_free) or NULL + * @param n_out set to the number of calls (0 when the object carries none) + * @return 0 on success (including zero calls), -1 on OOM. + * + * Thread-safe: yes (uses rand() for ids — not cryptographic) + */ +int tc_parse_ornith_object(const char *open, const char *obj_end, + const jr_value *tools, tc_tool_call **out, int *n_out); + +/** + * True when `fname` names one of the request's tools (a NULL or empty `tools` + * array has no schema to check against and accepts every name). The same gate + * every parser path applies before it emits a call. + */ +bool tc_tool_name_known(const jr_value *tools, const char *fname); + +/** Write a fresh "call_XXXXXXXX" id into buf (rand()-based, not cryptographic). */ +void tc_new_call_id(char *buf, size_t len); + +/** Free an array returned by tc_parse_tool_calls. NULL-safe. */ +void tc_tool_calls_free(tc_tool_call *calls, int n); + +#endif /* PN_SERVE_TOOL_CALLING_H */ diff --git a/src/serve/tool_grammar.c b/src/serve/tool_grammar.c new file mode 100644 index 0000000..df78dba --- /dev/null +++ b/src/serve/tool_grammar.c @@ -0,0 +1,1365 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * Tool-call decoding grammar — a char-level FSM over the model's TRAINED XML + * tool-call emission, with NAME/KEY sets drawn from the request's tools schema. + * See tool_grammar.h for the contract and the reasoning/prose composition. + * + * Pure C, libc-only. No engine/CUDA deps → unit-tested standalone. + * + * ──────────────────────────────────────────────────────────────────────────── + * State model (char-consuming) + * ──────────────────────────────────────────────────────────────────────────── + * The walk is a phase machine over a fixed XML skeleton. Every phase consumes + * either a fixed literal (matched char-by-char via `lit`/`lit_pos`), or a token + * from a set (a tool NAME / a parameter KEY, accumulated into `name_acc` and + * matched against the set), or free VALUE text terminated by "". + * + * TG_FREE : arbitrary text before an opener. The model reasons / answers + * in prose here — UNCONSTRAINED in AUTO mode. On recognizing a + * full opener ("" or "" (wrapped form) + * TG_FUNC_OPEN : the literal "" + * TG_FUNC_NL : the "\n" after "" + * TG_BODY : a body boundary — EITHER "", + * with the "\n" separators between them skipped + * TG_PARAM_LIT : the remainder of the literal "" + * TG_PARAM_NL : the "\n" after "" + * TG_VALUE : free VALUE text up to "" — the SAME bytes the + * parser ends a value on, so the two cannot disagree about + * where a value ended. DELIBERATELY free otherwise: see + * "FRAME CAPTURE" below — a nested opener is NOT killed here, + * it is read back by the parser. + * TG_VALUE_JSON: VALUE for a key whose schema `type` is "array"/"object" — + * NOT free text: one well-formed JSON value of that container + * kind (json_value_grammar.h), and only once it CLOSES does + * "" become legal. These are the values + * write_typed_value splices RAW into the call's `arguments`, + * so a malformed one breaks the whole call, not one argument. + * TG_VALUE_ENUM: VALUE for a key with a schema `enum` — NOT free text: a + * closed alternation over the listed literals, matched by the + * same prefix walk as NAME/KEY, then the padded + * "\n\n" terminator (a closed set cannot desync) + * TG_FUNC_CLOSE: the literal "\n" + * TG_TC_CLOSE : the literal "" (wrapped form) + * TG_DONE : a complete call emitted + * TG_DEAD : invalid prefix (only ever reached on a clone during a check) + * + * Set matching: as NAME/KEY chars arrive we accumulate them in name_acc and, on + * each char, require that SOME set member has that char at the next position + * (else DEAD). The terminating '>' is accepted only if name_acc equals a + * member EXACTLY — so unknown names/keys are impossible. name_acc lives IN the + * state, so a name split across two token boundaries still matches. + * + * ──────────────────────────────────────────────────────────────────────────── + * FRAME CAPTURE — the injected-close hazard, and why TG_VALUE stays free text + * ──────────────────────────────────────────────────────────────────────────── + * KNOWN DEFECT, cause side. This grammar cannot tell a "" the MODEL + * chose from one the SERVE injected. When a turn exhausts its per-model + * `reasoning_budget`, the serve force-decodes the close itself — batch_sched.c + * driver_reason_step → driver_grammar_advance_injected (batched) and + * serve_generation.c sg_run_completion (single-user) — and that text arrives + * here through tg_advance like any other. TG_FREE's think_term matcher fires, + * reasoning_ended is set, and from the very next char FORCED mode compels the + * opener. The model is mid-thought: it emits the compelled + * "\n\n" and then writes what it was already going + * to write — including the "" it had not reached yet — into the + * free-text VALUE: + * + * + * \n\n\n\nlumen/serve.log + * + * + * (a recorded native-XML capture, nex-n2-mini, a session that never wedged). Note the + * captured text is a NATIVE call in THIS grammar's own dialect — the capture is + * about being cut off, not about dialect drift. + * + * The obvious patch is to make a nested ""/"" inside TG_VALUE + * off-grammar. It is NOT done, for two reasons: + * + * 1. Masking those tokens does not restore the model's intent — it deletes the + * only evidence of it. The model is still compelled and still mid-thought, + * so it just writes a differently-corrupted value; the parser then has + * nothing to read back (tool_calling.c recover_captured_call). + * 2. Reaching TG_DEAD mid-VALUE is actively dangerous here: tg_mask_active() + * reports a DEAD walk INACTIVE, which RELEASES the constraint for the rest + * of the generation — the same failure mode the reasoning-budget path at + * TG_FREE goes out of its way to avoid. + * + * So this phase stays permissive on purpose, and the detector lives in the + * parser, which sees whole values and can act on them. The real repair belongs + * at the injection point: an injected close should NOT compel the opener from + * the next char, because the model has not finished its sentence. See the + * report accompanying this change — that fix is not built here. + * + * Perf: tg_token_allowed() copies the small POD state and walks the candidate. + * In a fixed-literal phase the FSM expects one specific char, so almost every + * one of the ~248k vocab tokens is rejected on its first char → ~O(1). VALUE + * accepts nearly everything with an early-accept O(len) scan. NAME/KEY do a + * per-char scan of a tiny set, entered only a few times per call. A per-state + * allowed-token cache (compute the mask once per skeleton state, reuse it across + * steps in that state) is the next optimization — noted, not needed for + * correctness or the current call rates. + */ + +#include "tool_grammar.h" + +#include +#include +#include +#include + +#include "json_value_grammar.h" +#include "tool_param_empty_value_rule.h" + +/* ── compiled string set (tool names, or one tool's parameter keys) ── */ +typedef struct { + char **items; + int n; +} tg_set; + +/* Required-key tracking is a bitmask over `keys` indices, so the walk state stays + * a small POD (tg_token_allowed clones it per candidate token — see the perf note + * above). Tools with more than 64 parameters are beyond what any model emits; + * a required key past bit 63 is simply not tracked, which degrades to the old + * unenforced behavior for that key rather than rejecting a legal call. */ +#define TG_REQ_MAX_KEYS 64 + +/* Largest `enum` compiled into a closed alternation — same degrade-don't-reject + * rationale as TG_REQ_MAX_KEYS above: a schema listing more members than this + * keeps a free-text VALUE rather than having legal calls rejected. */ +#define TG_ENUM_MAX_MEMBERS 64 + +/* Everything the grammar compiled about ONE parameter of a tool. Was a bare + * `tg_set *enums` array; a second per-key fact (the JSON container its `type` + * demands) would have made that two parallel arrays indexed the same way, which + * is one array too many to keep straight at the three sites that touch them. */ +typedef struct { + tg_set enum_values; /* closed VALUE set; .n == 0 = not an enum */ + unsigned char json_container; /* '[' array, '{' object, 0 = free-text VALUE */ + bool forbids_empty; /* schema says this value may not be empty — + * tool_param_empty_value_rule.h */ +} tg_key_meta; + +typedef struct { + char *name; + tg_set keys; + uint64_t required; /* bit i set ⇔ keys.items[i] is in the schema's `required` */ + tg_key_meta *key_meta; /* [keys.n], parallel to keys.items */ +} tg_tool; + +struct tg_grammar { + tg_tool *tools; + int n_tools; + tg_set names; /* union of tool names (index i ↔ tools[i]) */ +}; + +typedef enum { + TG_FREE = 0, + TG_TC_NL, + TG_FUNC_OPEN, + TG_FUNC_NAME, + TG_FUNC_NL, + TG_BODY, + TG_PARAM_LIT, + TG_PARAM_KEY, + TG_PARAM_NL, + TG_VALUE, + TG_VALUE_ENUM, + TG_VALUE_JSON, + TG_FUNC_CLOSE, + TG_AFTER_FUNC, /* "" consumed; optional "\n" then close/DONE */ + TG_TC_CLOSE, + TG_DONE, + TG_DEAD, +} tg_phase; + +#define TG_NAME_MAX 256 + +struct tg_state { + const tg_grammar *g; + tg_phase phase; + bool wrapped; /* saw "" → require "" close */ + int cur_tool; /* matched tool index (for its key set); -1 */ + + /* Forced-call mode (Phase-5): compel a call after optional reasoning. */ + tg_mode mode; /* TG_MODE_AUTO | TG_MODE_FORCED */ + bool reasoning_ended; /* forced mode: "" seen → opener required */ + /* The compelled region starts at the next TOKEN, not the next CHAR. + * + * Reasoning can end in the MIDDLE of a token: the budget trips on an + * arbitrary char, and the model's own "" can arrive glued to the text + * after it. tg_advance walks a whole token, so without this the very next + * char of that same token is judged under the compelled rule — with no + * sampling step, and therefore no mask, in between — and one ordinary prose + * char there is TG_FREE_BREAK → TG_DEAD. A DEAD walk reports its mask + * INACTIVE, so the rest of the turn decodes unconstrained. + * + * Set by every reasoning-end that happens INSIDE the walk; cleared by + * tg_advance once the token finishes. So the token during which reasoning + * ended stays unconstrained to its last char, and the constraint engages on + * the next one — which IS a sampling step, and therefore is masked. + * + * Live: qwen36-bf16, one recorded session, three turns dead at + * masked_steps=0 (dead_turns=1..3). Not caught earlier because every test + * fed this grammar one char at a time, the one granularity where the old + * "arm cleanly, then mask the next step" comment was true. */ + bool forced_pending; + int forced_tool; /* forced-function pin (tool index), or -1 = any tool */ + /* THINK-GATED reasoning (tg_state_new_required). The prompt left a + * block OPEN, so everything up to the model's own "" is reasoning + * and nothing in it is judged: no opener tracking (a "" the model + * writes while thinking is thought, not a call) and no char budget (max_tokens + * and the serve's reasoning_budget force-close bound it). The opener is + * compelled from the token after the close. */ + bool think_gated; + int think_term; /* forced mode: rolling-prefix length of "" seen + * in the raw stream, so the grammar self-detects the + * reasoning close WITHOUT depending on the emitter's + * flag (which the Q3 model doesn't reliably + * trigger — BUG 1). */ + + const char *lit; /* literal for a fixed-literal phase */ + int lit_pos; + + const tg_set *set; /* set for a NAME/KEY phase */ + int set_pos; /* chars matched so far */ + char name_acc[TG_NAME_MAX]; /* accumulated NAME/KEY (survives tokens) */ + + /* Closed VALUE set for the parameter currently being emitted, or NULL for a + * free-text VALUE. Borrowed from the grammar, which outlives every state, so + * this stays a small POD — tg_token_allowed clones the whole struct per + * candidate token (see the perf note at the top). TG_VALUE_ENUM reuses + * name_acc/set_pos to accumulate the value: the KEY that lived there has + * already been matched and consumed, and the next TG_PARAM_KEY resets both. */ + const tg_set *enum_set; + + /* The JSON container this parameter's schema `type` demands of its VALUE + * ('[' / '{'), or 0 for a free-text VALUE. Stashed by TG_PARAM_KEY for the + * same reason enum_set is: the key index is gone by the time the value + * starts. */ + unsigned char json_container; + + /* Container-typed VALUE (TG_VALUE_JSON). Embedded BY VALUE — 16 bytes of + * POD, no pointers — precisely so tg_token_allowed can keep cloning the + * whole tg_state per candidate token. See json_value_grammar.h. */ + jvg_walk json_value; + + /* FREE-region opener tracker */ + int free_open; /* matched length of the opener literal */ + int free_which; /* 0 none, 1 , 2 " close. + * + * Whitespace deliberately does not count. The parser trims it away, so a + * value of "\n" reaches the tool as "" — the grammar promising non-empty and + * the host then deleting the only thing satisfying the promise is precisely + * how {"path":""} shipped once already. */ + bool val_floor; + bool val_content; + + /* Which of cur_tool's parameter keys this call has emitted so far (bit per + * keys index). TG_BODY refuses "" until it covers the tool's + * `required` mask, so a call MISSING a required argument cannot complete. + * Reset when a NAME is matched, so each call in a multi-call stream is judged + * on its own. */ + uint64_t seen_keys; + + /* Forced-mode reasoning budget (BUG: a model that degenerates in and + * never emits "" streams unconstrained forever). We count chars seen + * while still in the reasoning grace (FREE, forced, !reasoning_ended, no opener + * progress); once it exceeds TG_REASON_BUDGET we force reasoning_ended so the + * opener is compelled — the degenerate ramble can't run past the budget. */ + int reason_chars; + + /* LANDING WINDOW (see tg_reasoning_end_injected). Chars of AUTO-like grace + * still owed after an INJECTED "", before the opener is compelled. + * 0 = not in a landing window (the ordinary compelled behavior). */ + int landing_chars; +}; + +/* Max chars a forced request may spend in the reasoning grace before the opener + * is compelled. Generous enough for a real block, small enough that a + * repetition loop is cut off in bounded time. */ +#define TG_REASON_BUDGET 8192 + +/* Chars of AUTO-like grace granted after an INJECTED "" before the + * opener is compelled — the landing window (tg_reasoning_end_injected). + * + * Sized from recorded captures, which are the only measurements of how much + * the model still had to say when it was cut off (the shapes are reproduced in + * tests/frame_capture_fixtures.h): + * + * native-XML capture — ~10 chars ("\n\n" then straight to the call) + * absolute-path — ~52 chars (one prose line, then the call) + * Hermes lead-in — ~90 chars (close + a prose lead-in, then the call) + * + * 512 clears the worst observed case by ~5x while staying negligible against the + * 2000-TOKEN reasoning budget it backstops — so a model that is genuinely + * degenerating is still cut off in bounded time, and the runaway this whole + * mechanism exists to stop is not reopened. The window also ends EARLY the + * moment the model emits its own "", which is what actually terminates + * it in every recorded case; the char budget is only the backstop on the + * backstop. */ +#define TG_LANDING_BUDGET 512 + +/* Skeleton literals. */ +static const char LIT_TC_NL[] = "\n"; +static const char LIT_FUNC_OPEN[] = "items) { + for (int i = 0; i < s->n; i++) free(s->items[i]); + free(s->items); + } + s->items = NULL; s->n = 0; +} + +static bool set_add(tg_set *s, const char *item) { + char **grown = realloc(s->items, (size_t)(s->n + 1) * sizeof(char *)); + if (!grown) return false; + s->items = grown; + s->items[s->n] = strdup(item); + if (!s->items[s->n]) return false; + s->n++; + return true; +} + +/* Does SOME member have char `c` at position `pos`? */ +static bool set_char_live(const tg_set *s, int pos, char c) { + for (int i = 0; i < s->n; i++) { + const char *m = s->items[i]; + if ((int)strlen(m) > pos && m[pos] == c) return true; + } + return false; +} + +/* Index of the member exactly equal to `acc[0..len)`, or -1. */ +static int set_exact_match(const tg_set *s, const char *acc, int len) { + for (int i = 0; i < s->n; i++) { + const char *m = s->items[i]; + if ((int)strlen(m) == len && strncmp(m, acc, (size_t)len) == 0) return i; + } + return -1; +} + +/* Forced-function variants: only member `pin` (a single tool name) is live. + * `pin < 0` means "any member" → falls back to the whole-set behavior. */ +static bool set_char_live_pinned(const tg_set *s, int pin, int pos, char c) { + if (pin < 0) return set_char_live(s, pos, c); + if (pin >= s->n) return false; + const char *m = s->items[pin]; + return (int)strlen(m) > pos && m[pos] == c; +} + +static int set_exact_match_pinned(const tg_set *s, int pin, const char *acc, int len) { + if (pin < 0) return set_exact_match(s, acc, len); + if (pin >= s->n) return -1; + const char *m = s->items[pin]; + return ((int)strlen(m) == len && strncmp(m, acc, (size_t)len) == 0) ? pin : -1; +} + +/* ── build ── */ + +/* Compile a property's `enum` into a closed VALUE set, or leave `out` empty. + * + * Identical admission rules to the JSON dialect's build_key_enum + * (tool_grammar_json.c) so the two grammars constrain the same schemas: + * + * • the declared `type` must be absent or "string". This grammar's VALUE is + * raw text between the tags, so a string alternation is the + * only shape it can express; an enum on an integer/boolean/object type is a + * schema contradiction, and the declared type wins. With NO type the enum + * still applies — it is self-describing. + * • every member is a non-empty string shorter than TG_NAME_MAX (the + * accumulator the walk matches against). Numeric/boolean/mixed members are + * ignored wholesale: a number has no single spelling, so a literal set would + * reject legal emissions. + * • 1..TG_ENUM_MAX_MEMBERS members. + * + * Anything else degrades to a free-text VALUE — never to a rejected call. + * Returns false only on OOM (tg_build then unwinds via tg_free). */ +static bool build_key_enum(tg_set *out, const jr_value *prop) { + const char *type = jr_as_string(jr_object_get(prop, "type")); + if (type && strcmp(type, "string") != 0) return true; + + const jr_value *e = jr_object_get(prop, "enum"); + if (jr_kind(e) != JR_ARRAY) return true; + int n = jr_array_len(e); + if (n <= 0 || n > TG_ENUM_MAX_MEMBERS) return true; + + for (int i = 0; i < n; i++) { + const char *m = jr_as_string(jr_array_get(e, i)); + if (!m || !*m || strlen(m) >= TG_NAME_MAX) { set_free(out); return true; } + if (!set_add(out, m)) return false; + } + return true; +} + +/* Which JSON container, if any, this parameter's schema `type` demands of its + * VALUE: '[' for "array", '{' for "object", 0 for everything else. + * + * THE SET IS NOT ARBITRARY. It is exactly the set of types whose value + * tool_calling.c's write_typed_value SPLICES RAW into the call's `arguments` + * object (`if (*val == '{' || *val == '[') jw_raw(...)`). Every other type is + * either JSON-escaped into a string (so a malformed value can only ever be a bad + * ARGUMENT, never a broken arguments OBJECT) or re-validated there before being + * spliced — "integer"/"number" through strtod, "boolean" through strcmp. Those + * two facts are the whole scope argument: constrain the values that are trusted + * unchecked, and leave the ones that are already checked alone. + * + * A parameter with no declared `type` keeps a free-text VALUE. The schema has + * not said the value is structured, so the grammar does not get to decide that + * it is — the same degrade-don't-reject rule build_key_enum follows. */ +static unsigned char container_of_type(const jr_value *prop) { + const char *type = jr_as_string(jr_object_get(prop, "type")); + if (!type) return 0; + if (strcmp(type, "array") == 0) return '['; + if (strcmp(type, "object") == 0) return '{'; + return 0; +} + +tg_grammar *tg_build(const jr_value *tools) { + if (jr_kind(tools) != JR_ARRAY) return NULL; + int n = jr_array_len(tools); + if (n <= 0) return NULL; + + tg_grammar *g = calloc(1, sizeof(*g)); + if (!g) return NULL; + g->tools = calloc((size_t)n, sizeof(tg_tool)); + if (!g->tools) { free(g); return NULL; } + + for (int i = 0; i < n; i++) { + const jr_value *fn = jr_object_get(jr_array_get(tools, i), "function"); + const char *name = jr_as_string(jr_object_get(fn, "name")); + if (!name || !*name) continue; /* skip malformed entries */ + + tg_tool *t = &g->tools[g->n_tools]; + t->name = strdup(name); + if (!t->name) { tg_free(g); return NULL; } + /* Count the tool NOW, before its sub-allocations: a later OOM in a set_add + * below is then cleaned up by tg_free (which frees name + keys for tools in + * [0, n_tools)) instead of leaking this partial tool. t->keys is zeroed, so + * set_free on it is safe whether or not the keys loop has run. */ + g->n_tools++; + if (!set_add(&g->names, name)) { tg_free(g); return NULL; } + + const jr_value *params = jr_object_get(fn, "parameters"); + const jr_value *props = jr_object_get(params, "properties"); + if (jr_kind(props) == JR_OBJECT) { + int np = jr_object_member_count(props); + if (np > 0) { + /* Allocated BEFORE the key loop so a mid-loop OOM still unwinds + * through tg_free with a valid (zeroed) key_meta array. */ + t->key_meta = calloc((size_t)np, sizeof(tg_key_meta)); + if (!t->key_meta) { tg_free(g); return NULL; } + } + for (int k = 0; k < np; k++) { + const char *key = jr_object_key_at(props, k); + if (!key || !*key) continue; + if (!set_add(&t->keys, key)) { tg_free(g); return NULL; } + const jr_value *prop = jr_object_value_at(props, k); + tg_key_meta *meta = &t->key_meta[t->keys.n - 1]; + if (!build_key_enum(&meta->enum_values, prop)) { + tg_free(g); return NULL; + } + /* Mutually exclusive by construction: build_key_enum only + * compiles an enum whose declared `type` is absent or "string", + * and container_of_type only fires on "array"/"object". */ + meta->json_container = container_of_type(prop); + meta->forbids_empty = tool_param_forbids_empty_value(prop); + } + } + + /* The schema's `required` list → a bitmask over the key set built above. + * A required name that is not among `properties` (malformed schema) has + * no key the grammar could emit, so it is skipped rather than making + * every call for this tool impossible. */ + const jr_value *req = jr_object_get(params, "required"); + if (jr_kind(req) == JR_ARRAY) { + int nr = jr_array_len(req); + for (int k = 0; k < nr; k++) { + const char *key = jr_as_string(jr_array_get(req, k)); + if (!key || !*key) continue; + int idx = set_exact_match(&t->keys, key, (int)strlen(key)); + if (idx >= 0 && idx < TG_REQ_MAX_KEYS) + t->required |= (uint64_t)1 << idx; + } + } + } + + if (g->n_tools == 0) { tg_free(g); return NULL; } + return g; +} + +void tg_free(tg_grammar *g) { + if (!g) return; + for (int i = 0; i < g->n_tools; i++) { + free(g->tools[i].name); + if (g->tools[i].key_meta) { + for (int k = 0; k < g->tools[i].keys.n; k++) + set_free(&g->tools[i].key_meta[k].enum_values); + free(g->tools[i].key_meta); + } + set_free(&g->tools[i].keys); + } + free(g->tools); + set_free(&g->names); + free(g); +} + +/* ── state ── */ + +tg_state *tg_state_new(const tg_grammar *g) { + if (!g) return NULL; + tg_state *st = calloc(1, sizeof(*st)); + if (!st) return NULL; + st->g = g; + st->phase = TG_FREE; + st->cur_tool = -1; + st->mode = TG_MODE_AUTO; + st->forced_tool = -1; + return st; +} + +tg_state *tg_state_new_forced(const tg_grammar *g, const char *forced_name) { + if (!g) return NULL; + int pin = -1; + if (forced_name && *forced_name) { + pin = set_exact_match(&g->names, forced_name, (int)strlen(forced_name)); + if (pin < 0) return NULL; /* named function is not one of the tools */ + } + tg_state *st = tg_state_new(g); + if (!st) return NULL; + st->mode = TG_MODE_FORCED; + st->forced_tool = pin; /* -1 = "required" (any tool), >=0 = pinned function */ + return st; +} + +tg_state *tg_state_new_required(const tg_grammar *g, bool think_open) { + tg_state *st = tg_state_new_forced(g, NULL); + if (!st) return NULL; + st->think_gated = think_open; + /* No open think block: the first generated char is already the answer, so + * the call is compelled from token 0 (nothing to wait for). */ + if (!think_open) st->reasoning_ended = true; + return st; +} + +tg_state *tg_state_clone(const tg_state *st) { + if (!st) return NULL; + tg_state *copy = malloc(sizeof(*copy)); + if (copy) *copy = *st; + return copy; +} + +void tg_state_copy(tg_state *dst, const tg_state *src) { + if (dst && src) *dst = *src; +} + +void tg_state_free(tg_state *st) { free(st); } + +void tg_reasoning_end(tg_state *st) { + if (st) st->reasoning_ended = true; +} + +void tg_reasoning_end_injected(tg_state *st) { + if (!st) return; + st->reasoning_ended = true; + /* Grant the landing window. The reasoning region IS over (the close is in + * the stream and the KV state), so every runaway guard stays armed — the + * only thing deferred is COMPELLING the opener, and only for a bounded + * number of chars. Without this the model is made to emit + * "\n\n" mid-sentence and its unfinished + * thought lands in the free-text VALUE. */ + st->landing_chars = TG_LANDING_BUDGET; + /* The injected close may have left a partial "" match in the + * rolling tracker; clear it so the window's own watch for the model's OWN + * close starts from a clean slate. */ + st->think_term = 0; + st->free_which = 0; + st->free_open = 0; +} + +static void enter_literal(tg_state *st, tg_phase p, const char *lit) { + st->phase = p; st->lit = lit; st->lit_pos = 0; +} + +static void enter_set(tg_state *st, tg_phase p, const tg_set *set) { + st->phase = p; st->set = set; st->set_pos = 0; +} + +/* Advance a running fixed-literal; returns 1 when the literal is fully consumed + * (caller transitions), 0 while still mid-literal, -1 on mismatch (DEAD). */ +static int lit_step(tg_state *st, char c) { + if (st->lit[st->lit_pos] != c) { st->phase = TG_DEAD; return -1; } + st->lit_pos++; + return st->lit[st->lit_pos] == '\0' ? 1 : 0; +} + +/* Consume one char, mutating st (phase → TG_DEAD on any violation). */ +static void step_char(tg_state *st, char c) { + switch (st->phase) { + + case TG_FREE: { + /* THINK-GATED (tg_state_new_required): inside the open think block only + * the model's own "" is watched for; nothing else is judged. */ + if (st->think_gated && !st->reasoning_ended) { + if (THINK_CLOSE[st->think_term] == c) { + st->think_term++; + if (THINK_CLOSE[st->think_term] == '\0') { + st->reasoning_ended = true; + st->forced_pending = true; /* compelled from the next TOKEN */ + st->think_term = 0; + } + } else { + st->think_term = (c == THINK_CLOSE[0]) ? 1 : 0; + } + return; + } + + /* FORCED mode, reasoning grace (before reasoning_ended): the model is + * still inside its block (the prompt prefilled "", so the + * generated stream opens mid-reasoning). We watch the RAW stream for + * "" HERE, in the grammar itself, so forcing engages WITHOUT + * depending on the emitter's in_think flag — which the Q3 model doesn't + * reliably trigger (BUG 1: it sometimes reasons in the content channel / + * emits no clean the emitter recognizes, and forcing never fired). + * Detecting the close in the grammar's own stream makes forcing + * self-contained and reliable. (The serve layer still calls + * tg_reasoning_end() too — a harmless secondary trigger; whichever fires + * first wins.) Reasoning text is unconstrained until the close. */ + if (st->mode == TG_MODE_FORCED && !st->reasoning_ended) { + /* Reasoning budget: a model that degenerates in and never + * emits "" would otherwise stream unconstrained forever (the + * live SABER/ornith degeneration). Count EVERY grace char (the + * degenerate ramble is often opener-rich — "", "". Don't cut off a genuine opener that is + * already underway (free_open>0): only revoke grace when not mid-opener, + * so a real "reason_chars++; + if (st->reason_chars > TG_REASON_BUDGET && st->free_open == 0) { + /* Budget spent → compel the opener FROM THE NEXT char. Consume + * THIS char as the final grace char and return: if we instead fell + * through into the forced-opener path with this (prose) char, a + * non-opener char would drive the state to TG_DEAD — and a DEAD + * state reports the mask INACTIVE (tg_mask_active), silently + * disengaging the constraint so the ramble continues unmasked + * forever (a bug seen on K80 hardware). Arm cleanly, then mask the next + * TOKEN — forced_pending is what makes "next step" true, since + * the rest of THIS token still arrives before the sampler runs + * again (see the field's note). */ + st->reasoning_ended = true; + st->forced_pending = true; + st->think_term = 0; + st->free_which = 0; st->free_open = 0; + return; /* armed; the next TOKEN is masked toward the opener */ + } else + if (THINK_CLOSE[st->think_term] == c) { + st->think_term++; + if (THINK_CLOSE[st->think_term] == '\0') { + st->reasoning_ended = true; /* "" seen → compel opener */ + st->forced_pending = true; /* … from the next TOKEN */ + st->think_term = 0; + st->free_which = 0; st->free_open = 0; + return; /* the close itself consumed; the rest of this + * token is still grace (the close can arrive + * glued to the text that follows it) */ + } + } else { + /* Broke the "" match; re-seed if this char restarts it. */ + st->think_term = (c == THINK_CLOSE[0]) ? 1 : 0; + } + /* Still reasoning: fall through to the FREE opener tracker below so a + * call opened DIRECTLY (mid-reasoning, no ) still engages the + * skeleton (auto-like), while non-opener prose stays FREE (grace). */ + } + + /* LANDING WINDOW. The reasoning close was INJECTED by the serve + * (tg_reasoning_end_injected), not chosen by the model, so the model is + * mid-sentence. For a bounded number of chars behave exactly like AUTO + * — prose is free, an opener still engages the skeleton — so the model + * can finish its thought and start its call where it meant to, instead + * of being forced to open a frame around it. + * + * The window ends on whichever comes FIRST: + * • the model emits its OWN "" — it has now genuinely + * concluded, so compel the opener immediately (this is what actually + * ends the window in every recorded capture); or + * • the char budget runs out. + * `tool_choice=required` is NOT weakened: the opener is still compelled, + * just up to TG_LANDING_BUDGET chars later. */ + if (st->mode == TG_MODE_FORCED && st->landing_chars > 0) { + if (THINK_CLOSE[st->think_term] == c) { + st->think_term++; + if (THINK_CLOSE[st->think_term] == '\0') { + /* The model's own close — the grace is no longer warranted. + * End the window HERE and consume this char, so the opener is + * compelled from the NEXT one (never from this one, which + * would drive a legal char to DEAD — and a DEAD walk reports + * its mask INACTIVE, releasing the constraint entirely). */ + st->landing_chars = 0; + st->forced_pending = true; + st->think_term = 0; + st->free_which = 0; st->free_open = 0; + return; + } + } else { + st->think_term = (c == THINK_CLOSE[0]) ? 1 : 0; + } + st->landing_chars--; + if (st->landing_chars == 0) { + /* Budget spent. Same arm-cleanly-then-mask-next-TOKEN + * discipline as the reasoning-budget path above. */ + st->forced_pending = true; + st->free_which = 0; st->free_open = 0; + return; + } + /* Still landing: fall through to the opener tracker with `forced` + * false, so prose stays FREE and a real opener still engages. */ + } + + /* Forced mode, reasoning over → the opener is REQUIRED: any char that is + * not on an opener prefix is off-grammar (DEAD), so prose/JSON cannot be + * emitted. (In AUTO mode a non-opener char just keeps the walk FREE — + * reasoning/prose is unconstrained.) A live landing window suspends this + * — and only this — for its duration. */ + bool forced = (st->mode == TG_MODE_FORCED && st->reasoning_ended && + st->landing_chars == 0 && !st->forced_pending); + + /* Forced mode: the model's trained format puts whitespace/newline between + * "" and the "" opener (it emits "\n"). + * BEFORE any opener progress (free_open == 0) tolerate leading whitespace + * so that separator is allowed — otherwise the ONLY allowed first char is + * '<', every whitespace-leading token is masked, the mask leaves ZERO + * survivors, the sampler fails open, and a prose refusal slips through + * (the observed no-force bug). Whitespace only counts before the opener + * starts; once '<' arrives the fixed skeleton takes over. */ + if (forced && st->free_open == 0 && + (c == '\n' || c == ' ' || c == '\t' || c == '\r')) + return; /* stay in TG_FREE awaiting the opener */ + + /* On a broken opener prefix: DEAD in forced mode, else reset to FREE + * (re-seeding on '<'). */ + #define TG_FREE_BREAK() do { \ + if (forced) { st->phase = TG_DEAD; return; } \ + st->free_which = 0; st->free_open = 0; \ + if (c == '<') { st->free_which = 3; st->free_open = 1; } \ + return; \ + } while (0) + + if (st->free_which == 0) { + if (c == '<') { st->free_which = 3; st->free_open = 1; return; } + TG_FREE_BREAK(); + } + if (st->free_which == 3) { /* ambiguous after '<' */ + bool tc = (st->free_open < (int)strlen(OPEN_TC) && OPEN_TC[st->free_open] == c); + bool fn = (st->free_open < (int)strlen(OPEN_FUNC) && OPEN_FUNC[st->free_open] == c); + if (tc && fn) { st->free_open++; } + else if (tc) { st->free_which = 1; st->free_open++; } + else if (fn) { st->free_which = 2; st->free_open++; } + else TG_FREE_BREAK(); + } else { + const char *lit = (st->free_which == 1) ? OPEN_TC : OPEN_FUNC; + if (st->free_open < (int)strlen(lit) && lit[st->free_open] == c) { + st->free_open++; + } else TG_FREE_BREAK(); + } + #undef TG_FREE_BREAK + /* completed an opener? */ + if (st->free_which == 1 && st->free_open == (int)strlen(OPEN_TC)) { + st->wrapped = true; + st->free_which = 0; st->free_open = 0; + enter_literal(st, TG_TC_NL, LIT_TC_NL); + } else if (st->free_which == 2 && st->free_open == (int)strlen(OPEN_FUNC)) { + st->wrapped = false; + st->free_which = 0; st->free_open = 0; + enter_set(st, TG_FUNC_NAME, &st->g->names); + st->set_pos = 0; + } + return; + } + + case TG_TC_NL: + if (lit_step(st, c) == 1) enter_literal(st, TG_FUNC_OPEN, LIT_FUNC_OPEN); + return; + + case TG_FUNC_OPEN: + if (lit_step(st, c) == 1) { enter_set(st, TG_FUNC_NAME, &st->g->names); st->set_pos = 0; } + return; + + case TG_FUNC_NAME: + /* Forced-function pins the NAME to a single tool (forced_tool); "required" + * and AUTO allow any tool name (forced_tool == -1). */ + if (c == '>') { + int idx = set_exact_match_pinned(st->set, st->forced_tool, + st->name_acc, st->set_pos); + if (idx < 0) { st->phase = TG_DEAD; return; } + st->cur_tool = idx; /* names set is 1:1 with tools */ + st->seen_keys = 0; /* a fresh call owes this tool's required keys */ + enter_literal(st, TG_FUNC_NL, LIT_FUNC_NL); + } else if (set_char_live_pinned(st->set, st->forced_tool, st->set_pos, c)) { + if (st->set_pos < TG_NAME_MAX - 1) st->name_acc[st->set_pos] = c; + st->set_pos++; + } else st->phase = TG_DEAD; + return; + + case TG_FUNC_NL: + if (lit_step(st, c) == 1) { st->phase = TG_BODY; st->body_lt = false; } + return; + + case TG_BODY: + /* boundary: "" (end); both start '<' */ + if (!st->body_lt) { + /* Newlines between boundaries are SEPARATORS, not structure. TG_VALUE + * now hands over the instant "" closes (it no longer eats + * a trailing "\n" as part of the terminator), so the one the trained + * format puts before the next "" arrives + * here. Accepting a run of them rather than exactly one also makes + * "" — emitted verbatim by the live fleet, + * tool_arg_truncation.md log:274 — grammatical instead of a silent + * swallow. This is a blank line at a structural boundary; nothing a + * tool argument can carry passes through it. */ + if (c == '\n') return; + if (c == '<') { st->body_lt = true; return; } + st->phase = TG_DEAD; return; + } + st->body_lt = false; + if (c == 'p') { /* "lit_pos = 2; /* "\n" — a + * complete, well-formed call with NO arguments, which the parser + * turns into `{}`. That is what reached the client's orchestrator as + * `dispatch_agent {}` and was refused for a missing "subtask" + * (observed on the live fleet 2026-08-06, with the grammar ON): the + * empty-VALUE guard in TG_VALUE only constrains a key that IS + * emitted, never one that is left out. The JSON dialect + * (tool_grammar_json.c) has always required every `required` key; + * this is the XML dialect catching up to the same contract. */ + const tg_tool *t = (st->cur_tool >= 0) ? &st->g->tools[st->cur_tool] + : NULL; + if (t && (st->seen_keys & t->required) != t->required) { + st->phase = TG_DEAD; return; + } + enter_literal(st, TG_FUNC_CLOSE, LIT_FUNC_CLOSE); + st->lit_pos = 2; /* "phase = TG_DEAD; + return; + + case TG_PARAM_LIT: + if (lit_step(st, c) == 1) { + enter_set(st, TG_PARAM_KEY, &st->g->tools[st->cur_tool].keys); + st->set_pos = 0; + } + return; + + case TG_PARAM_KEY: + if (c == '>') { + int kidx = set_exact_match(st->set, st->name_acc, st->set_pos); + if (kidx < 0) { st->phase = TG_DEAD; return; } + if (kidx < TG_REQ_MAX_KEYS) + st->seen_keys |= (uint64_t)1 << kidx; /* satisfies `required` */ + /* Stash this key's VALUE shape (closed enum set, or the JSON + * container its `type` demands) for TG_PARAM_NL to route on — the + * key index is not recoverable once name_acc is reused by the value + * walk. At most one of the two is ever set (see tg_build). */ + const tg_tool *kt = &st->g->tools[st->cur_tool]; + const tg_key_meta *meta = kt->key_meta ? &kt->key_meta[kidx] : NULL; + st->enum_set = (meta && meta->enum_values.n > 0) + ? &meta->enum_values : NULL; + st->json_container = meta ? meta->json_container : 0; + /* No key_meta at all means no schema was compiled for this tool; a + * floor we were never told about must not be invented. */ + st->val_floor = meta ? meta->forbids_empty : false; + enter_literal(st, TG_PARAM_NL, LIT_PARAM_NL); + } else if (set_char_live(st->set, st->set_pos, c)) { + if (st->set_pos < TG_NAME_MAX - 1) st->name_acc[st->set_pos] = c; + st->set_pos++; + } else st->phase = TG_DEAD; + return; + + case TG_PARAM_NL: + if (lit_step(st, c) == 1) { + st->phase = st->enum_set ? TG_VALUE_ENUM + : st->json_container ? TG_VALUE_JSON + : TG_VALUE; + st->val_term = 0; + st->val_content = false; + st->set_pos = 0; /* TG_VALUE_ENUM accumulates into name_acc */ + if (st->phase == TG_VALUE_JSON) + jvg_begin(&st->json_value, (char)st->json_container); + } + return; + + case TG_VALUE_ENUM: { + /* Closed-alternation VALUE. Two things must both hold before the walk + * may leave: the accumulated text is EXACTLY one enum member, and the + * "\n\n" terminator then runs to completion. Off-set chars + * die at the first divergence, so "banana" is unreachable the moment it + * stops being a prefix of any member. + * + * No empty-value guard is needed here (build_key_enum rejects empty + * members, so no member is satisfiable by zero chars) and none is + * applied. + * + * This phase KEEPS the padded "\n\n" terminator that TG_VALUE + * gave up, and that is not an oversight. The divergence TG_VALUE fixes is + * "the grammar swallowed a close the parser honoured"; it cannot happen + * here, because an enum value is a closed set of literals and the walk + * DIES on any char that leaves the set rather than absorbing it. Matching + * the bare "" here would instead break the healthy emission: + * after an exact member the next char is the padding "\n", which no member + * continues, so the walk would die on a correct call. + * + * Ambiguity note: if a member contained the terminator's leading '\n' at + * the current position AND the text so far were already an exact member, + * the terminator wins. Preferring the close is the only choice that + * keeps the value finite; a member with an embedded newline is not + * something a tool schema expresses. */ + const char *term = LIT_VALUE_TERM; + if (st->val_term > 0) { + /* Committed: the value already matched a member exactly, so the only + * legal continuation is the rest of the terminator. */ + if (term[st->val_term] != c) { st->phase = TG_DEAD; return; } + st->val_term++; + if (term[st->val_term] == '\0') { st->phase = TG_BODY; st->body_lt = false; } + return; + } + if (c == term[0] && + set_exact_match(st->enum_set, st->name_acc, st->set_pos) >= 0) { + st->val_term = 1; + return; + } + if (set_char_live(st->enum_set, st->set_pos, c)) { + if (st->set_pos < TG_NAME_MAX - 1) st->name_acc[st->set_pos] = c; + st->set_pos++; + return; + } + st->phase = TG_DEAD; + return; + } + + case TG_VALUE_JSON: { + /* CONTAINER-TYPED VALUE — the parameter's schema says "array"/"object", + * so its bytes are not free text: they are a JSON value, and + * write_typed_value will splice them into the call's `arguments` object + * RAW, without looking at them. That splice is why a malformed value + * here is not a bad argument but a broken CALL: the arguments string + * stops parsing, and every OTHER argument of that call dies with it. + * + * Live, qwen36-bf16, frame grammar armed and masking correctly — three + * consecutive `todowrite` calls, same malformation, no recovery: + * + * [{"content":"…","status":"in_progress"},{"content":"",""}] + * + * The second object opens a member, writes its key and value, then a + * comma, then a BARE STRING where a key was owed. JVG_P_KEY_REQ reads + * that string as the key, JVG_P_COLON then requires a ':' — and the '}' + * the model writes instead is off-grammar. The malformation is not + * merely detected here; the token carrying that '}' is masked, so it is + * never drawn. + * + * THE TERMINATOR, AND THE ONE HAZARD IT CARRIES. TG_VALUE's whole + * safety property is that it ends on the exact bytes the parser ends on + * ("", strstr'd in parse_call_arguments), so the two cannot + * disagree about where a value stopped. A JSON value can CONTAIN those + * bytes inside a string — `[""]` — and the parser would cut + * there, handing write_typed_value the un-parseable prefix `["`. So the + * rolling terminator match runs over the WHOLE value, and completing it + * while the JSON value is still open is off-grammar. Outside a string + * '<' cannot appear in JSON at all, so in practice this only ever masks + * the one token that would close that literal sequence inside a string — + * and the value stays byte-identical to what the parser will read. + * + * AN EMPTY VALUE IS UNREACHABLE HERE, and unlike the free-text case that + * is not a policy change smuggled in. TG_VALUE keeps empty values legal + * because no grammar can tell `edit`'s deliberate `new_string:""` from a + * useless `path:""`. For a container-typed key there is nothing to tell + * apart: the empty LIST is `[]` and the empty OBJECT is `{}`, both + * reachable, both two chars. "" is not an empty array — it is a string, + * which is the wrong type, which is the thing this phase exists to stop. + * + * Still NOT checked: the schema INSIDE the value (`items`, nested + * `properties`/`required`, member types). A well-formed `[{"x":1}]` + * reaches the tool and is refused there, with the tool's own message. */ + const char *term = LIT_VALUE_CLOSE; + + if (jvg_is_complete(&st->json_value)) { + /* The value is a finished JSON container. Only padding whitespace + * (which the parser's dup_trim removes) and the terminator remain. */ + if (term[st->val_term] == c) { + st->val_term++; + if (term[st->val_term] == '\0') { + st->phase = TG_BODY; st->body_lt = false; + } + return; + } + if (st->val_term == 0 && + (c == ' ' || c == '\t' || c == '\n' || c == '\r')) return; + st->phase = TG_DEAD; + return; + } + + /* Still inside the value. Track the terminator first: if these bytes + * complete it, the parser would cut the value HERE, mid-JSON. */ + st->val_term = (term[st->val_term] == c) ? st->val_term + 1 + : (c == term[0] ? 1 : 0); + if (term[st->val_term] == '\0') { st->phase = TG_DEAD; return; } + + jvg_step(&st->json_value, c); + if (jvg_is_dead(&st->json_value)) { st->phase = TG_DEAD; return; } + if (jvg_is_complete(&st->json_value)) st->val_term = 0; + return; + } + + case TG_VALUE: { + /* THE VALUE ENDS WHERE THE PARSER SAYS IT ENDS. + * + * The terminator is the bare "" — the same bytes + * parse_call_arguments looks for with strstr(vstart, PARAM_CLOSE). Any + * surrounding newlines are ordinary value padding here, exactly as they + * are for the parser, which dup_trim()s them off. So the two agree on + * the value boundary BY CONSTRUCTION, for every emission, not just the + * canonical one. + * + * WHAT THIS REPLACED, AND WHY IT HAD TO GO. This phase used to terminate + * on "\n\n" AND carry an empty-value guard: while the value + * held no non-whitespace char, a char that would start the terminator was + * forced to be CONTENT instead (val_term reset to 0). The stated + * guarantee was that `path:""` could not be emitted. It never held: + * + * • The guard MASKS NOTHING. Consuming a char as content is an ACCEPT, + * so tg_token_allowed("\n\n") returned true at an empty + * value. The model was free to emit the terminator and did. + * • Having eaten the leading "\n" as content, the guard then made + * "" break the terminator match, so the close was + * swallowed as VALUE TEXT and the walk stayed in TG_VALUE for the rest + * of the turn — mask effectively off, call never closed. The parser's + * strstr still found that same "" and closed the value at + * it, yielding "" → the key dropped → `read {}`. + * + * So the guard did not prevent an empty value; it converted a VISIBLE + * empty argument into an INVISIBLE missing one, and desynced the grammar + * from the parser to do it. Measured in real serving across 9 truncated + * calls, all "grammar ACCEPTS char-for-char, tg_complete == NO". + * + * THE REPLACEMENT GUARD, AND WHAT MAKES IT A DIFFERENT THING. An empty + * value is not universally a defect — `edit`'s new_string:"" is a legal + * deletion, and the client's own contract turns on the distinction + * (argument_is_absent, the client's schema code: "A member present + * with an EMPTY value is NOT absent"). What a grammar cannot decide is + * whether THIS parameter means anything empty. It does not have to + * decide: the schema already says so, in `minLength`, and the client now + * declares it on every string argument that cannot mean empty — + * `bash.command`, `read.path`, `write.content`, and pointedly NOT + * `edit.new_string`. So the floor is per-key and derived + * (tool_param_empty_value_rule.h), not a blanket rule this phase invents. + * + * The guard is applied HERE, at the closing terminator, and it DIES. + * That distinction is the whole reason the previous attempt failed: it + * consumed the terminator's first char AS CONTENT, and consuming is an + * ACCEPT — tg_token_allowed returned true, the model emitted the close + * anyway, and the walk was then stranded mid-value for the rest of the + * turn. Dying instead means the token carrying that close is MASKED and + * is never drawn, and a walk that never enters TG_DEAD stays in lockstep + * with the parser's strstr. Live evidence for what the absence of any + * guard costs: qwen36-bf16 with the grammar armed emitted + * `bash {"command":""}` seven consecutive times — seven full decodes at + * ~10 tok/s producing nothing — until the no-progress guard stopped the + * run. A model that emits an empty required argument cannot recover on + * its own, which is why this is prevented at emission rather than + * corrected afterwards. + * + * WHITESPACE IS NOT CONTENT (tg_is_ws). The parser dup_trim()s the value, + * so a value of "\n" arrives as "" — a floor satisfied by bytes the host + * then deletes is not a floor. + * + * The parser is deliberately NOT changed to match. An empty value that + * arrives anyway is still reported as the empty value it is rather than + * hidden as a missing member, which is the contract test_empty_argument.c + * pins. Three layers, each answering what it can see: the grammar stops + * emission when the schema forbids it, the parser reports faithfully, the + * client judges (the client's own may-be-empty check, which reads the + * SAME minLength). + * + * KNOWN GAPS, all of them landing on that third layer rather than + * silently. This floor binds the bytes between "" and + * "" and nothing else, so an empty value still reaches the + * client when: + * • the grammar is off — PANTHEON_TOOL_GRAMMAR is off by default, and a + * request carrying no tools never builds one; + * • the schema declares no floor — a client that does not send + * minLength has not asked for one, and inventing it would forbid the + * `edit` deletion above; + * • the value arrives inside a "" wrapper. The + * MiniCPM-family trained format wraps each value, the grammar walks + * that wrapper as ordinary free text (7 non-whitespace chars, so the + * floor is met), and parse_call_arguments then PEELS it — leaving "". + * Teaching this phase the wrapper is what would fix it, and is + * declined: TG_VALUE's one safety property is that it ends on exactly + * the bytes the parser ends on, and every attempt to give it a second + * opinion about value structure has desynced it from the parser. + * • the dialect is MiniCPM's attribute form (``), which + * this skeleton does not model at all. + * + * Still unreachable, unchanged: a call that OMITS a required key. That + * guard lives at TG_BODY (seen_keys vs the tool's required mask) and binds + * "", not the value. */ + const char *term = LIT_VALUE_CLOSE; + if (term[st->val_term] == c) { + st->val_term++; + if (term[st->val_term] == '\0') { + if (st->val_floor && !st->val_content) { st->phase = TG_DEAD; return; } + st->phase = TG_BODY; st->body_lt = false; + } + return; + } + /* Terminator broke → everything the failed prefix consumed was value + * content, and "" holds no whitespace, so any progress at all + * means real content preceded this char. Crediting it retroactively is + * what keeps a value that merely STARTS like the terminator ("") legal under the floor. + * + * Re-seeding to 1 on '<' is exact, not an approximation: "" + * has no proper border, so the only suffix of a failed prefix that can + * restart the match is the single '<'. */ + if (st->val_term > 0 || !tg_is_ws(c)) st->val_content = true; + st->val_term = (c == term[0]) ? 1 : 0; + return; + } + + case TG_FUNC_CLOSE: + if (lit_step(st, c) == 1) st->phase = TG_AFTER_FUNC; + return; + + case TG_AFTER_FUNC: + /* "" is consumed → the call body is complete. The trained + * format has a "\n" before "" (wrapped) or ends here (bare). + * Accept an optional "\n", then "" if wrapped, else DONE. */ + if (st->wrapped) { + if (c == '\n') return; /* optional separator */ + if (c == '<') { enter_literal(st, TG_TC_CLOSE, LIT_TC_CLOSE); st->lit_pos = 1; return; } + st->phase = TG_DEAD; + } else { + /* bare form: complete. A trailing "\n" is tolerated; anything else + * is off-grammar (the sampler stops at completion anyway). */ + if (c == '\n') return; + st->phase = TG_DEAD; + } + return; + + case TG_TC_CLOSE: + if (lit_step(st, c) == 1) st->phase = TG_DONE; + return; + + case TG_DONE: + st->phase = TG_DEAD; /* nothing valid after a complete call */ + return; + + case TG_DEAD: + default: + return; + } +} + +static void walk(tg_state *st, const char *text) { + for (const char *p = text; *p; p++) step_char(st, *p); +} + +bool tg_token_allowed(const tg_state *st, const char *token_text) { + if (!st) return false; + if (!token_text || !*token_text) return true; /* empty = no-op */ + tg_state clone = *st; + walk(&clone, token_text); + return clone.phase != TG_DEAD; +} + +void tg_advance(tg_state *st, const char *token_text) { + if (!st || !token_text) return; + walk(st, token_text); + /* The token during which reasoning ended is over. From the NEXT one the + * opener is compelled — and the next one is a sampling step, so the mask + * runs and the model is steered into the opener instead of walking off the + * grammar. Clearing it HERE (not at the top) is what makes the compelled + * region start at a token boundary; see tg_state.forced_pending. */ + st->forced_pending = false; +} + +/* The rest of the fixed literal the walk is part-way through, so a closer can + * finish it: a half-written "" / "", or the value + * terminator a VALUE has started. "" when there is none. */ +static const char *tg_pending_literal(const tg_state *st) { + switch (st->phase) { + case TG_FUNC_CLOSE: + case TG_TC_CLOSE: + return st->lit + st->lit_pos; + case TG_VALUE: + case TG_VALUE_JSON: + return st->val_term > 0 ? LIT_VALUE_CLOSE + st->val_term : ""; + case TG_VALUE_ENUM: + return st->val_term > 0 ? LIT_VALUE_TERM + st->val_term : ""; + default: + return ""; + } +} + +int tg_closing_text(const tg_state *st, char *out, int cap) { + /* The trained format's own tail, shortest first. Each is tried as it is + * and then after the pending literal; the first that completes the call on + * a copy of the walk wins. */ + static const char *const CLOSERS[] = { + "", "\n", "\n", "\n", "\n\n", + "\n\n", "\n\n\n", + }; + const char *pending; + char buf[96]; + + if (!st || !out || cap < 1 || !tg_in_call(st) || tg_complete(st)) return -1; + pending = tg_pending_literal(st); + for (int with_pending = 0; with_pending < 2; with_pending++) { + if (with_pending && !*pending) break; + for (size_t i = 0; i < sizeof(CLOSERS) / sizeof(CLOSERS[0]); i++) { + int n = snprintf(buf, sizeof(buf), "%s%s", with_pending ? pending : "", + CLOSERS[i]); + if (n <= 0 || n >= (int)sizeof(buf) || n >= cap) continue; + tg_state clone = *st; + walk(&clone, buf); + if (tg_complete(&clone)) { + memcpy(out, buf, (size_t)n + 1); + return n; + } + } + } + return -1; +} + +bool tg_end_allowed(const tg_state *st) { + char closing[TG_CLOSING_MAX]; + if (!st || !tg_mask_active(st)) return true; + return tg_closing_text(st, closing, (int)sizeof(closing)) > 0; +} + +bool tg_complete(const tg_state *st) { + if (!st) return false; + /* DONE = wrapped call fully closed. For the bare form the call is complete + * the moment "" is consumed (TG_AFTER_FUNC, unwrapped) — there is + * no wrapper to close. */ + return st->phase == TG_DONE || + (st->phase == TG_AFTER_FUNC && !st->wrapped); +} + +bool tg_in_call(const tg_state *st) { + return st && st->phase != TG_FREE && st->phase != TG_DEAD; +} + +bool tg_is_dead(const tg_state *st) { + return st && st->phase == TG_DEAD; +} + +int tg_debug_reason_chars(const tg_state *st) { + return st ? st->reason_chars : 0; +} + +int tg_debug_landing_chars(const tg_state *st) { + return st ? st->landing_chars : 0; +} + +bool tg_forced_reasoning_ended(const tg_state *st) { + /* A DEAD walk is excluded, and that exclusion is the whole point of this + * being a function rather than a field read. + * + * The serve drives exactly one thing off this bit: it flips the emitter out + * of its channel so a COMPELLED call routes to CONTENT and is parsed + * as a tool call (serve_generation.c, both decode loops). A dead walk + * compels nothing — tg_mask_active already reports it INACTIVE and every + * remaining token of the turn is sampled unconstrained — so answering "yes" + * here does not deliver a call to the content channel, it delivers the + * model's ONGOING THOUGHT there, mid-sentence, and the transcript records it + * as an ANSWER. Live: qwen36-bf16 one recorded session, three turns, + * 37,721 chars of think-block text filed as answers. + * + * Everything else in this module already declines to speak for a dead walk + * (tg_mask_active; the serve's own sg_tg_report_tool_call, which refuses to + * judge one). This makes the reasoning verdict agree with them. */ + return st && st->mode == TG_MODE_FORCED && st->reasoning_ended && + st->phase != TG_DEAD; +} + +bool tg_mask_active(const tg_state *st) { + if (!st || st->phase == TG_DEAD) return false; + if (tg_complete(st)) return false; /* call done → release the constraint */ + /* AUTO: mask only inside a call (FREE prose unconstrained). + * FORCED: also mask the post-reasoning FREE state to compel the opener. */ + if (tg_in_call(st)) return true; + if (st->mode != TG_MODE_FORCED) return false; + /* A live landing window masks exactly like AUTO — the grammar accepts any + * prose during it, so computing a mask would be pure cost for zero + * constraint. Only an opener already in progress engages (below). */ + if (st->reasoning_ended && st->landing_chars == 0) return true; + /* FORCED early-arm (BUG 1 hardening): the moment the model starts emitting a + * tool-call opener DURING reasoning ("<", then "…"), + * engage the mask so the opener + NAME are constrained immediately — don't + * wait for a "" that a degenerating model may never emit. free_open>0 + * means an opener prefix is in progress in the FREE tracker. */ + return st->free_open > 0; +} diff --git a/src/serve/tool_grammar.h b/src/serve/tool_grammar.h new file mode 100644 index 0000000..7e12b6a --- /dev/null +++ b/src/serve/tool_grammar.h @@ -0,0 +1,347 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_grammar.h + * @brief Grammar / state-machine that constrains tool-call decoding to the + * model's TRAINED XML emission (n2 toolcalling Phase 5). + * + * The Q3 nex-n2-mini drifts off its trained XML tool-call format (emits the + * base model's JSON `{"tool_call":…}` habit, or prose). This module builds a + * char-level FSM from the request's `tools` JSON that recognizes exactly the + * trained emission: + * + * \n + * \n + * (\nVALUE\n\n)+ + * \n + * + * + * with NAME drawn from the tool names and KEY drawn from that tool's parameter + * names. The bare (no-wrapper) `…` form the Q3 model + * favors is ALSO accepted. VALUE is free text up to `` — the same + * bytes the parser ends a value on, so grammar and parser cannot disagree about + * where a value ended. + * + * REQUIRED ARGUMENTS. Every key in that tool's schema `required` list must be + * emitted before `` becomes a legal continuation — so a call that + * OMITS a required argument (the bare `\n`, which the + * parser turns into `{}`) is structurally unreachable. Same guarantee the JSON + * dialect (tool_grammar_json.h) gives. A tool with no `required` list is + * unconstrained, exactly as before. + * + * AN EMPTY VALUE IS REJECTED WHERE THE SCHEMA SAYS SO, AND NOWHERE ELSE. + * "" is not a legal continuation of a value that holds no + * non-whitespace char, IF that parameter's schema forbids an empty value — + * a non-string `type`, or `minLength` ≥ 1 (tool_param_empty_value_rule.h, the + * same predicate the client enforces on arrival, compiled into both dialects from one + * function). So `bash {"command":""}` and `read {"path":""}` are unreachable, + * while `edit`'s `new_string:""` — where empty IS the deletion — still is not. + * Requiredness is not the test: both of those keys are `required`. + * + * THE GUARD DIES; IT DOES NOT CONSUME. An earlier attempt refused the + * terminator by taking its first char as VALUE CONTENT instead, which is an + * ACCEPT: tg_token_allowed said yes, the model emitted the close anyway, and the + * walk then sat inside VALUE for the rest of the turn while the parser had + * already closed the parameter at those same bytes — a visible empty argument + * turned into a silently missing one (`read {}`), 9/9 truncated live calls. + * Reaching TG_DEAD instead is what makes the token carrying that close MASKED, + * so it is never drawn and the walk never leaves the parser's frame. Cost of + * having no guard at all, measured: qwen36-bf16 with the grammar armed emitted + * `bash {"command":""}` seven times running before the run was stopped. + * + * The PARSER is unchanged and still reports an empty value it receives as + * `{"path":""}` rather than `{}` — the grammar prevents emission, the parser + * reports, the client judges. Whether the model closed the CALL at all is a + * separate question, answered by `tg_complete()`. + * + * ENUM VALUES. A parameter carrying a string `enum` does NOT get a free-text + * VALUE: it is a closed alternation over the listed literals, so an off-list + * value is unreachable. Enums that cannot be represented as a string + * alternation (non-string members, a contradicting `type`, more members than the + * cap) DEGRADE to free text rather than reject — see build_key_enum in the .c + * for the exact admission rules. + * + * CONTAINER-TYPED VALUES. A parameter whose schema `type` is "array" or + * "object" does not get a free-text VALUE either: its bytes must be ONE + * well-formed JSON value of that container kind, walked incrementally by + * json_value_grammar.h, and "" only becomes a legal continuation + * once that value has CLOSED. This is the gap the frame grammar could not + * cover: the call skeleton was masked correctly and the bytes inside + * `` were still free text — so a model that writes one + * malformed nested object breaks not that argument but the whole `arguments` + * string, because tool_calling.c's write_typed_value SPLICES a container-typed + * value in RAW. Scope is exactly that raw-splice set; every other type is + * JSON-escaped or already re-validated there. The schema INSIDE the value + * (`items`, nested `properties`/`required`, member types) is still dropped: + * `[{"wrong_key":1}]` is well-formed, reaches the tool, and is refused there. + * Every other schema keyword (minLength, pattern, const, numeric bounds) is + * still dropped, and `type` still does not constrain a SCALAR VALUE. + * + * A constrained sampler asks, for each candidate token, `tg_token_allowed()` + * ("does appending this token's text keep a valid grammar prefix?") and masks + * (logit = -INFINITY) the ones that don't; after a token is drawn it calls + * `tg_advance()` with the chosen token's text to walk the FSM forward. + * + * REASONING / PROSE composition. The grammar starts in a FREE state that + * accepts arbitrary text (the model reasons and may answer in prose with NO + * tool call at all). It only ENGAGES the skeleton once a tool-call opener + * ("" or a bare "… block is allowed), but the moment reasoning ends the + * ONLY tokens left allowed are the ones that lead toward the "" / + * "" appears in + * the model's raw output — the grammar DETECTS THIS ITSELF (it watches its own + * token stream) so forcing is self-contained and does NOT depend on the serve + * emitter's flag, which the Q3 model does not reliably trigger (it + * sometimes reasons in the content channel with no clean ). The serve + * layer still calls tg_reasoning_end() as a harmless secondary trigger. + * + * Pure C, libc-only — NO engine/CUDA deps — so it is unit-tested in isolation + * (tests/test_tool_grammar.c) without the forward path. + * + * @status Phase 5 module — the mask is wired behind an OFF-by-default flag + * (PANTHEON_TOOL_GRAMMAR=1); grammar-off generation is byte-identical. + * The free-text VALUE terminator is now the parser's own "", + * so the two cannot disagree about where a value ended, and tg_complete() + * is the serve's truncation verdict. + * @issues max_tokens and the repetition latch finish a turn without consulting + * the grammar, so a call truncated by them is DETECTED (tg_complete + * false → refused on the wire) rather than prevented. End tokens are + * governed for "required" only (tg_end_allowed + tg_closing_text: the + * serve closes a call the model stopped inside); AUTO and a named + * function keep the old rule. + * @todo Per-state allowed-token cache for the fixed-skeleton states (see the + * perf note in tool_grammar.c) to shrink the per-step mask cost. + */ + +#ifndef PN_TOOL_GRAMMAR_H +#define PN_TOOL_GRAMMAR_H + +#include + +#include "json_reader.h" /* jr_value */ + +/* Opaque compiled grammar (tool + parameter name sets + the fixed skeleton). */ +typedef struct tg_grammar tg_grammar; + +/* Decoding mode for a walk state (see the header note). + * TG_MODE_AUTO : engage the skeleton only after an opener appears; FREE prose + * is unconstrained (the original Phase-5 behavior). + * TG_MODE_FORCED : compel a call — after optional reasoning the opener is + * REQUIRED, so prose/JSON are masked out and the response is a + * valid XML call. Pin a specific function with tg_state_new_forced. */ +typedef enum { + TG_MODE_AUTO = 0, + TG_MODE_FORCED, +} tg_mode; + +/* Mutable per-request walk position over the grammar. Cheap to copy by value + * (it is a small POD), which the allowed-check exploits: it clones the state, + * walks the candidate text on the clone, and reports whether the clone stayed + * on a valid prefix — the caller's live state is untouched until tg_advance. */ +typedef struct tg_state tg_state; + +/** + * Build a grammar from the request's `tools` JSON array (OpenAI shape: + * `[{"type":"function","function":{"name":…,"parameters":{"properties":{…}}}}]`). + * Returns NULL if `tools` is not a non-empty array or on OOM. Free with + * tg_free. + */ +tg_grammar *tg_build(const jr_value *tools); + +/** Free a grammar (NULL-safe). */ +void tg_free(tg_grammar *g); + +/** Allocate a fresh AUTO-mode walk state at the start (FREE region). NULL on OOM. */ +tg_state *tg_state_new(const tg_grammar *g); + +/** + * Allocate a FORCED-mode walk state (Phase-5 forced-call). After optional + * reasoning the opener is REQUIRED (no prose/JSON escape). If `forced_name` is + * non-NULL it must be one of the grammar's tool names and the call's NAME is + * pinned to it (any other name is rejected); NULL forces *some* tool ("required"). + * Returns NULL on OOM or if `forced_name` is not a known tool. + */ +tg_state *tg_state_new_forced(const tg_grammar *g, const char *forced_name); + +/** + * Allocate the walk state for `tool_choice:"required"` (forced, any tool) with + * THINK-GATED reasoning — the XML twin of tgj_state_new_required. + * + * `think_open` says what the prompt did: true when it left a block OPEN + * (a think-style template with thinking on), false when there is no block. + * - think_open: everything before the model's own "" is reasoning and + * is never judged — no opener tracking and no char budget (max_tokens and + * the serve's reasoning_budget force-close bound it; an injected close + * still gets the landing window, tg_reasoning_end_injected). From the + * token after "" the opener is compelled as in tg_state_new_forced. + * - !think_open: compelled from the first token. + * The legacy tg_state_new_forced grace (opener tracking + TG_REASON_BUDGET) is + * kept for the named-function form. NULL on OOM / NULL grammar. + */ +tg_state *tg_state_new_required(const tg_grammar *g, bool think_open); + +/** Largest text tg_closing_text can produce, NUL included. */ +#define TG_CLOSING_MAX 96 + +/** + * The text that takes this walk to a COMPLETE call, if the call can still be + * closed as it stands: the trained tail ("\n\n\n + * " and its suffixes), shortest first, optionally after the rest of + * a literal the walk is part-way through — each checked on a copy of the walk. + * None (-1) when a required key is still missing, a schema-floored value is + * empty, an enum/JSON value is unfinished, the NAME/KEY is incomplete, or the + * walk is not inside a call. + * + * Used when the model ends a "required" turn INSIDE its call: the serve appends + * this so the turn ends in the call the model meant — the value the parser + * reads from the unclosed call — instead of a refused incomplete call. + * + * @return the length written to `out` (NUL-terminated), or -1 + */ +int tg_closing_text(const tg_state *st, char *out, int cap); + +/** + * May the turn END (an end token, which has no text) here? Always once the + * mask is off — reasoning, a landing window, AUTO prose, a completed call. With + * the mask on: never before the call opens, and inside the call only when + * tg_closing_text can close it. The serve wires this for "required" only. + */ +bool tg_end_allowed(const tg_state *st); + +/** Heap copy of a walk state (NULL on OOM / NULL input). Free with + * tg_state_free. Used to look ahead over speculative drafts without touching + * the live walk. */ +tg_state *tg_state_clone(const tg_state *st); + +/** Overwrite `dst` with `src` (both from this module; same grammar). */ +void tg_state_copy(tg_state *dst, const tg_state *src); + +/** Free a walk state (NULL-safe). */ +void tg_state_free(tg_state *st); + +/** + * Signal that the reasoning phase has ended. In FORCED mode this releases the + * reasoning grace and REQUIRES the opener from here on (prose/JSON masked out). + * A no-op in AUTO mode, and idempotent. + * + * NOTE: forcing no longer DEPENDS on this call — the grammar self-detects + * "" in its own token stream (tg_advance), so a request forces reliably + * even if the serve emitter never flips its flag (BUG 1). This remains + * as a harmless secondary trigger for callers that track reasoning externally. + */ +void tg_reasoning_end(tg_state *st); + +/** + * Signal that the reasoning phase was ended by an INJECTED "" — one the + * SERVE force-decoded because the turn spent its `reasoning_budget`, NOT one the + * model chose (batch_sched.c driver_reason_step / serve_generation.c + * sg_run_completion). + * + * Same effect as tg_reasoning_end, PLUS a bounded LANDING WINDOW: for the next + * TG_LANDING_BUDGET chars the FREE state behaves like AUTO (prose unconstrained, + * an opener still engages the skeleton), and only then is the opener compelled. + * + * WHY. A force-closed model is mid-sentence. Compelling the opener from the + * very next char makes it emit "\n\n" around an + * unfinished thought, and everything it still had to say — often a complete tool + * call — lands in the free-text VALUE instead of being a call. That is the + * frame-capture defect recorded on the live fleet (a recorded native-XML capture, + * three recorded Hermes-shape captures). The window lets the model land: finish its + * sentence, emit its own close, and open the call itself. + * + * `tool_choice=required` is preserved — the opener is still compelled, just up + * to TG_LANDING_BUDGET chars later. The window ends EARLY the moment the model + * emits its own "". A no-op in AUTO mode, and idempotent (a second call + * simply re-grants the window). + */ +void tg_reasoning_end_injected(tg_state *st); + +/** + * Would appending `token_text` keep the walk on a valid grammar prefix? + * Does NOT mutate `st` (walks a clone). While in the FREE region (before an + * opener) every text is allowed. Returns false for empty/NULL text only if it + * would be invalid (empty text is always allowed — it is a no-op). + */ +bool tg_token_allowed(const tg_state *st, const char *token_text); + +/** Walk the state forward by `token_text` (must have been allowed). */ +void tg_advance(tg_state *st, const char *token_text); + +/** + * Has the walk reached a COMPLETE tool call (a full `` closed, with + * or without the `` wrapper)? Used to let the sampler release the + * constraint / stop once a whole call has been emitted. + * + * ALSO THE TRUNCATION VERDICT. False while inside a call means the model stopped + * part-way through emitting it — the turn ended on EOS, `max_tokens` or a latch + * that never consults the grammar. That is the one bit that separates a call the + * model finished from one it abandoned, and the serve gates on it before handing + * anything to the client (SG_TOOL_GRAMMAR_INCOMPLETE in serve_generation.h, + * TC_PARSE_CALL_INCOMPLETE on the wire). It is only meaningful for a walk that + * actually entered the skeleton — pair it with tg_in_call(). + */ +bool tg_complete(const tg_state *st); + +/** + * Is the walk currently INSIDE a tool call (past an opener, not yet FREE-only)? + * The sampler uses this together with the opener detection to decide whether to + * apply the mask this step. + */ +bool tg_in_call(const tg_state *st); + +/** + * Has the walk left the grammar entirely (TG_DEAD)? + * + * NOT the same question as !tg_in_call(): a walk still in TG_FREE has also never + * entered a call, and the two mean opposite things. FREE = the model has not + * started one (prose, or a dialect this grammar does not model). DEAD = it + * started one and went off-grammar, at which point tg_mask_active() reports + * INACTIVE and every remaining token of the turn is sampled with NO constraint. + * Collapsing the two is why "the AUTO grammar silently switched itself off" had + * no observable answer; the serve reports the DEAD case on its own line. + */ +bool tg_is_dead(const tg_state *st); + +/** + * Should the sampler MASK this step? This is the gating policy the serve layer + * feeds into pn_tool_grammar_hook.active: + * - AUTO mode : true only once inside a call (== tg_in_call) — FREE prose is + * unconstrained, byte-identical to the pre-forced behavior. + * - FORCED mode : true once reasoning has ended (tg_reasoning_end) AND the call + * is not yet complete — so the post-reasoning FREE state is + * constrained to opener-leading tokens (compels the call), and + * the skeleton is constrained thereafter. Before reasoning ends + * it stays false so the block is unconstrained. + * Always false once the call is complete (tg_complete) or DEAD. + */ +bool tg_mask_active(const tg_state *st); + +/* DEBUG: current forced-mode reasoning-grace char count (for tracing the budget). + * Returns 0 in AUTO mode or when st is NULL. */ +int tg_debug_reason_chars(const tg_state *st); + +/* DEBUG: chars of landing-window grace still owed after an injected reasoning + * close (tg_reasoning_end_injected). 0 = no window live, i.e. the opener is + * compelled normally. Returns 0 when st is NULL. */ +int tg_debug_landing_chars(const tg_state *st); + +/* Has forced-mode reasoning ended (the opener is now compelled)? True once the + * grammar has seen "" OR the reasoning budget tripped. The serve loop + * uses this to flip the emitter out of its channel so the compelled call + * routes to CONTENT (and is parsed as a tool_call) even when the model never + * emitted a clean "" itself. Always false in AUTO mode / when NULL. */ +bool tg_forced_reasoning_ended(const tg_state *st); + +#endif /* PN_TOOL_GRAMMAR_H */ diff --git a/src/serve/tool_grammar_json.c b/src/serve/tool_grammar_json.c new file mode 100644 index 0000000..aae4c8d --- /dev/null +++ b/src/serve/tool_grammar_json.c @@ -0,0 +1,1047 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* + * Tool-call decoding grammar (JSON dialect) — a char-level FSM over ornith's + * RL-learned tool-call emission, with NAME/KEY sets + required-key sets + value + * types drawn from the request's tools schema. See tool_grammar_json.h for the + * contract; this is the JSON-syntax analog of tool_grammar.c's XML skeleton. + * + * Pure C, libc-only. No engine/CUDA deps → unit-tested standalone. + * + * ──────────────────────────────────────────────────────────────────────────── + * The constrained emission (canonical) + * ──────────────────────────────────────────────────────────────────────────── + * ```json↵ (OPTIONAL fence — the model usually emits it) + * {"function": "NAME", "arguments": {"KEY": , "KEY": }} + * ↵``` (fence close, iff the fence was opened) + * + * with NAME ∈ the tool names, the "function" key spellable also as "name", + * "arguments" also as "parameters", KEY ∈ that tool's parameter names, EVERY + * `required` key present, and each typed to its schema (string → a + * quoted JSON string, integer/number → a numeric literal, boolean → true/false, + * object/array → a balanced {…}/[…], else a quoted string). An empty + * `arguments` object ({}) is structurally impossible whenever the tool has ≥1 + * required key — the grammar cannot close `}` until every required key has been + * emitted. This is the JSON analog of the XML grammar's missing-key guard, and + * it is what gives ornith the same `required` guarantee saber gets from the XML + * grammar. + * + * A string value may be EMPTY only where the schema allows it. `""` closes for + * a key with no length floor (`edit`'s new_string, where empty IS the deletion) + * and is off-grammar for one that has a floor (`bash`'s command, `read`'s path, + * anything not declared a string). The predicate is + * tool_param_empty_value_rule.h, compiled into BOTH dialects from one function — + * see the step_value comment for the two opposite ways this has been got wrong. + * + * A parameter whose schema carries a string `enum` is tighter still: its value + * is a CLOSED ALTERNATION over the listed literals, matched by exactly the same + * prefix machinery that constrains the tool NAME set and the parameter KEY set + * (step_quoted_set). The client's switch_mode declares + * enum:["chat","plan","act","orchestrate"] — before this, "mode":"banana" was a + * legal call. See build_key_enum for which enums compile and which degrade. + * + * JSON whitespace: between structural tokens ({ } : , and around values) any run + * of spaces / tabs / newlines / CRs is tolerated, matching what a model emits. + * + * ──────────────────────────────────────────────────────────────────────────── + * Reasoning / AUTO / FORCED composition (identical policy to tool_grammar.c) + * ──────────────────────────────────────────────────────────────────────────── + * The walk starts FREE (TGJ_FREE): arbitrary reasoning/prose text, UNCONSTRAINED + * in AUTO mode. It ENGAGES the JSON skeleton once a real opener appears — the + * ```json fence OR a plausible bare `{` that begins a "function"/"name"-keyed + * object (mirroring the serve_emitter's ornith opener rule). FORCED mode + * (tool_choice required / a named function) replaces the FREE grace with + * "optional reasoning, then the opener is REQUIRED": after "" (self- + * detected in the raw stream, exactly like the XML grammar — BUG 1) or the + * reasoning budget, only opener-leading tokens survive, so prose is impossible. + * ornith runs reasoning_budget 0, so the grace is minimal; the budget + the + * self-detected close both still arm the opener cleanly (see the 0-budget test). + */ + +#include "tool_grammar_json.h" + +#include +#include + +#include "tool_param_empty_value_rule.h" + +/* ── compiled string set (tool names, or one tool's parameter keys) ── */ +typedef struct { + char **items; + int n; +} tgj_set; + +/* Value type of a parameter, resolved from its schema at build time. */ +typedef enum { + TGJ_T_STRING = 0, /* quoted JSON string (default / unknown) */ + TGJ_T_NUMBER, /* integer or number → numeric literal */ + TGJ_T_BOOL, /* boolean → true / false */ + TGJ_T_JSON, /* object / array → a balanced {…} or […] */ + TGJ_T_ENUM, /* schema `enum` → a quoted literal from a closed set */ +} tgj_vtype; + +typedef struct { + char *name; + tgj_set keys; /* all parameter names (index i ↔ types[i]/required[i]) */ + tgj_vtype *types; /* [keys.n] value type per key */ + tgj_set *enums; /* [keys.n] closed value set per key; .n == 0 = none */ + bool *required; /* [keys.n] true if the key is required */ + bool *forbids_empty; /* [keys.n] schema forbids an empty value for this + * key — tool_param_empty_value_rule.h */ + int n_required; /* count of required keys */ +} tgj_tool; + +struct tgj_grammar { + tgj_tool *tools; + int n_tools; + tgj_set names; /* union of tool names (index i ↔ tools[i]) */ +}; + +/* Phases of the JSON skeleton walk (char-consuming). */ +typedef enum { + TGJ_FREE = 0, /* reasoning/prose before an opener (see composition note) */ + TGJ_FENCE_OPEN, /* consuming the "```json\n" fence (optional) */ + TGJ_OBJ_OPEN, /* expect '{' (whitespace ok) */ + TGJ_FN_KEY, /* the "function"/"name" key (quoted), then ':' */ + TGJ_FN_COLON, /* ':' after the function key (whitespace ok) */ + TGJ_FN_NAME, /* the quoted NAME ∈ tool names, then ',' */ + TGJ_FN_COMMA, /* ',' after the name (whitespace ok) */ + TGJ_ARGS_KEY, /* the "arguments"/"parameters" key (quoted), then ':' */ + TGJ_ARGS_COLON, /* ':' after the arguments key (whitespace ok) */ + TGJ_ARGS_OPEN, /* expect '{' opening the arguments object (whitespace ok) */ + TGJ_ARG_KEY, /* a parameter KEY (quoted) ∈ this tool's params, then ':' */ + TGJ_ARG_COLON, /* ':' after a parameter key (whitespace ok) */ + TGJ_ARG_VALUE, /* the typed value */ + TGJ_ARG_SEP, /* after a value: ',' (more args) OR '}' (args done) */ + TGJ_OBJ_CLOSE, /* expect '}' closing the outer object (whitespace ok) */ + TGJ_FENCE_CLOSE, /* consuming the "\n```" fence close (iff fenced) */ + TGJ_DONE, /* a complete call emitted */ + TGJ_DEAD, /* invalid prefix (only ever reached on a clone) */ +} tgj_phase; + +#define TGJ_NAME_MAX 256 +#define TGJ_MAX_ARGS 64 /* max distinct arg keys tracked per call */ + +/* Largest `enum` compiled into a closed alternation. A schema listing more + * members than this keeps its plain type instead of the enum — the same + * degrade-don't-reject precedent as TG_REQ_MAX_KEYS in tool_grammar.c: a cap we + * cannot represent must never turn a LEGAL call into an impossible one. */ +#define TGJ_ENUM_MAX_MEMBERS 64 + +/* Reasoning grace budget — same rationale + value as the XML grammar. */ +#define TGJ_REASON_BUDGET 8192 + +/* Literals. */ +static const char FENCE_OPEN[] = "```json"; +static const char FENCE_CLOSE[] = "```"; +static const char THINK_CLOSE[] = ""; +static const char KEY_FUNCTION[] = "function"; +static const char KEY_NAME[] = "name"; +static const char KEY_ARGS[] = "arguments"; +static const char KEY_PARAMS[] = "parameters"; + +/* Two accepted spellings for the function key and the arguments key. */ +static const char *const FN_KEY_ALTS[] = { KEY_FUNCTION, KEY_NAME }; +static const char *const ARGS_KEY_ALTS[] = { KEY_ARGS, KEY_PARAMS }; + +struct tgj_state { + const tgj_grammar *g; + tgj_phase phase; + bool fenced; /* opener was the ```json fence → require close */ + int cur_tool; /* matched tool index; -1 */ + + /* Forced-call mode (mirrors tool_grammar.c). */ + tgj_mode mode; + bool reasoning_ended; + /* The compelled region starts at the next TOKEN, not the next CHAR — the + * XML dialect's tg_state.forced_pending, for the same reason and with the + * same live failure behind it (qwen36-bf16, one recorded session: three turns + * DEAD at masked_steps=0). tgj_advance walks a whole token, so a budget + * that trips mid-token would judge that token's remaining chars under the + * compelled rule with no mask in between, and one prose char there is + * TGJ_DEAD — which reports the mask INACTIVE for the rest of the turn. */ + bool forced_pending; + int forced_tool; /* pinned tool index, or -1 = any */ + /* THINK-GATED reasoning (tgj_state_new_required). The prompt left a + * block OPEN, so everything up to the model's own "" is + * reasoning and nothing in it is judged: no opener tracking (a '{' or a + * ```json fence the model writes while thinking is just thought) and no + * char budget (a long think is not a runaway — max_tokens and the + * reasoning_budget force-close bound it). The opener is compelled only + * once the block has closed. */ + bool think_gated; + int think_term; /* rolling-prefix length of "" seen */ + int reason_chars; /* grace char budget counter */ + + /* FREE-region opener tracker: matching the ```json fence OR a bare '{'. */ + int free_fence; /* chars of FENCE_OPEN matched (>=0) */ + bool free_brace; /* a bare '{' opener was taken (no fence) */ + + /* Literal walk (for fence/think when a fixed literal is being consumed). */ + const char *lit; + int lit_pos; + + /* Quoted-token walk (function key / name / arg key / string value). */ + bool in_quote; /* inside the "…" of the current token */ + bool quote_opened; /* the opening '"' has been seen this token */ + bool esc; /* previous char was a backslash (string value) */ + char acc[TGJ_NAME_MAX]; /* accumulated key/name chars (survives tokens) */ + int acc_len; + + /* Key-alternative matching: which of a small alt-set are still live, by the + * accumulated prefix. Used for the function key + the arguments key. */ + const char *const *alts; /* candidate spellings */ + int n_alts; + + /* Whitespace-or-token gate: the next non-ws char must be `expect`. */ + char expect; + + /* Value walk. */ + tgj_vtype vtype; /* type of the value currently being emitted */ + const tgj_set *enum_set; /* closed value set for TGJ_T_ENUM, else NULL. + * Borrowed from the grammar (outlives the state); + * keeps this struct a small POD, which matters + * because tgj_token_allowed copies it per token. */ + bool val_content; /* ≥1 non-whitespace char seen in the current value */ + bool val_floor; /* this key's schema forbids an empty value — + * stashed at TGJ_ARG_KEY, consumed at the closing + * quote (tool_param_empty_value_rule.h) */ + int val_depth; /* nesting depth for object/array values */ + bool val_started; /* the value's first structural char was seen */ + + /* Arguments bookkeeping: which required keys are still outstanding. */ + bool key_done[TGJ_MAX_ARGS]; /* key i already emitted this call */ + int required_left; /* required keys not yet emitted */ +}; + +/* ── set helpers ── */ + +static void set_free(tgj_set *s) { + if (s->items) { + for (int i = 0; i < s->n; i++) free(s->items[i]); + free(s->items); + } + s->items = NULL; s->n = 0; +} + +static bool set_add(tgj_set *s, const char *item) { + char **grown = realloc(s->items, (size_t)(s->n + 1) * sizeof(char *)); + if (!grown) return false; + s->items = grown; + s->items[s->n] = strdup(item); + if (!s->items[s->n]) return false; + s->n++; + return true; +} + +/* Does SOME member have char `c` at position `pos`? When `pin >= 0`, only that + * one member is considered (forced-function pin). */ +static bool set_char_live(const tgj_set *s, int pin, int pos, char c) { + if (pin >= 0) { + if (pin >= s->n) return false; + const char *m = s->items[pin]; + return (int)strlen(m) > pos && m[pos] == c; + } + for (int i = 0; i < s->n; i++) { + const char *m = s->items[i]; + if ((int)strlen(m) > pos && m[pos] == c) return true; + } + return false; +} + +/* Index of the member exactly equal to acc[0..len), honoring a pin, or -1. */ +static int set_exact_match(const tgj_set *s, int pin, const char *acc, int len) { + if (pin >= 0) { + if (pin >= s->n) return -1; + const char *m = s->items[pin]; + return ((int)strlen(m) == len && strncmp(m, acc, (size_t)len) == 0) + ? pin : -1; + } + for (int i = 0; i < s->n; i++) { + const char *m = s->items[i]; + if ((int)strlen(m) == len && strncmp(m, acc, (size_t)len) == 0) return i; + } + return -1; +} + +/* Does SOME alt spelling have char `c` at position `pos`? */ +static bool alts_char_live(const char *const *alts, int n, int pos, char c) { + for (int i = 0; i < n; i++) + if ((int)strlen(alts[i]) > pos && alts[i][pos] == c) return true; + return false; +} + +/* Is acc[0..len) an exact match of some alt spelling? */ +static bool alts_exact(const char *const *alts, int n, const char *acc, int len) { + for (int i = 0; i < n; i++) + if ((int)strlen(alts[i]) == len && strncmp(alts[i], acc, (size_t)len) == 0) + return true; + return false; +} + +/* ── build ── */ + +static tgj_vtype vtype_of(const char *type) { + if (!type) return TGJ_T_STRING; + if (strcmp(type, "integer") == 0 || strcmp(type, "number") == 0) + return TGJ_T_NUMBER; + if (strcmp(type, "boolean") == 0) return TGJ_T_BOOL; + if (strcmp(type, "object") == 0 || strcmp(type, "array") == 0) + return TGJ_T_JSON; + return TGJ_T_STRING; +} + +/* Compile a property's `enum` into a closed literal set, or leave `out` empty. + * + * An enum is compiled ONLY when every one of these holds; otherwise the key + * keeps its plain type and the enum is dropped (degrade, never reject — a + * constraint we cannot represent must not make a legal call impossible): + * + * • the declared `type` is absent or "string". A string alternation is the + * only shape this matcher can emit, so an enum sitting on an integer / + * boolean / object type is a schema contradiction we refuse to resolve — + * the declared type wins and the enum is ignored. With NO type at all the + * enum still applies: it is self-describing (unlike llama.cpp, where a + * constraint with no sibling `type` is silently dropped by keyword order). + * • every member is a non-empty JSON string. Numeric / boolean / null / mixed + * members are ignored wholesale: a number has no single spelling (1, 1.0, + * 1e0 are the same value), so a literal set would reject legal emissions. + * • 1..TGJ_ENUM_MAX_MEMBERS members, each shorter than TGJ_NAME_MAX (the + * accumulator the walk matches against). + * + * Returns false only on OOM (the grammar build then unwinds via tgj_free). */ +static bool build_key_enum(tgj_set *out, const jr_value *prop, const char *type) { + if (type && strcmp(type, "string") != 0) return true; + + const jr_value *e = jr_object_get(prop, "enum"); + if (jr_kind(e) != JR_ARRAY) return true; + int n = jr_array_len(e); + if (n <= 0 || n > TGJ_ENUM_MAX_MEMBERS) return true; + + for (int i = 0; i < n; i++) { + const char *m = jr_as_string(jr_array_get(e, i)); + if (!m || !*m || strlen(m) >= TGJ_NAME_MAX) { set_free(out); return true; } + if (!set_add(out, m)) return false; + } + return true; +} + +/* Is `key` listed in the tool's `required` array? Absent array → not required. */ +static bool key_is_required(const jr_value *required_arr, const char *key) { + if (jr_kind(required_arr) != JR_ARRAY) return false; + int n = jr_array_len(required_arr); + for (int i = 0; i < n; i++) { + const char *r = jr_as_string(jr_array_get(required_arr, i)); + if (r && strcmp(r, key) == 0) return true; + } + return false; +} + +/* Populate one tool's keys/types/required from its parameters schema. Returns + * false only on OOM (the grammar build then unwinds via tgj_free). */ +static bool build_tool_params(tgj_tool *t, const jr_value *fn) { + const jr_value *params = jr_object_get(fn, "parameters"); + const jr_value *props = jr_object_get(params, "properties"); + if (jr_kind(props) != JR_OBJECT) return true; /* no params → 0 keys, ok */ + const jr_value *required = jr_object_get(params, "required"); + + int np = jr_object_member_count(props); + if (np <= 0) return true; + t->types = calloc((size_t)np, sizeof(tgj_vtype)); + t->enums = calloc((size_t)np, sizeof(tgj_set)); + t->required = calloc((size_t)np, sizeof(bool)); + t->forbids_empty = calloc((size_t)np, sizeof(bool)); + if (!t->types || !t->enums || !t->required || !t->forbids_empty) return false; + + for (int k = 0; k < np; k++) { + const char *key = jr_object_key_at(props, k); + if (!key || !*key) continue; + const jr_value *prop = jr_object_value_at(props, k); + if (!set_add(&t->keys, key)) return false; + int idx = t->keys.n - 1; + const char *type = jr_as_string(jr_object_get(prop, "type")); + t->types[idx] = vtype_of(type); + if (!build_key_enum(&t->enums[idx], prop, type)) return false; + /* A compiled enum SUPERSEDES the type bucket: it is strictly the tighter + * constraint, and build_key_enum only compiles one when the declared type + * is absent or "string" — so this never contradicts the schema. */ + if (t->enums[idx].n > 0) t->types[idx] = TGJ_T_ENUM; + t->forbids_empty[idx] = tool_param_forbids_empty_value(prop); + t->required[idx] = key_is_required(required, key); + if (t->required[idx]) t->n_required++; + } + return true; +} + +tgj_grammar *tgj_build(const jr_value *tools) { + if (jr_kind(tools) != JR_ARRAY) return NULL; + int n = jr_array_len(tools); + if (n <= 0) return NULL; + + tgj_grammar *g = calloc(1, sizeof(*g)); + if (!g) return NULL; + g->tools = calloc((size_t)n, sizeof(tgj_tool)); + if (!g->tools) { free(g); return NULL; } + + for (int i = 0; i < n; i++) { + const jr_value *fn = jr_object_get(jr_array_get(tools, i), "function"); + const char *name = jr_as_string(jr_object_get(fn, "name")); + if (!name || !*name) continue; /* skip malformed entries */ + + tgj_tool *t = &g->tools[g->n_tools]; + t->name = strdup(name); + if (!t->name) { tgj_free(g); return NULL; } + g->n_tools++; + if (!set_add(&g->names, name)) { tgj_free(g); return NULL; } + if (!build_tool_params(t, fn)) { tgj_free(g); return NULL; } + } + + if (g->n_tools == 0) { tgj_free(g); return NULL; } + return g; +} + +void tgj_free(tgj_grammar *g) { + if (!g) return; + for (int i = 0; i < g->n_tools; i++) { + free(g->tools[i].name); + if (g->tools[i].enums) { + for (int k = 0; k < g->tools[i].keys.n; k++) + set_free(&g->tools[i].enums[k]); + free(g->tools[i].enums); + } + set_free(&g->tools[i].keys); + free(g->tools[i].types); + free(g->tools[i].required); + free(g->tools[i].forbids_empty); + } + free(g->tools); + set_free(&g->names); + free(g); +} + +/* ── state ── */ + +tgj_state *tgj_state_new(const tgj_grammar *g) { + if (!g) return NULL; + tgj_state *st = calloc(1, sizeof(*st)); + if (!st) return NULL; + st->g = g; + st->phase = TGJ_FREE; + st->cur_tool = -1; + st->mode = TGJ_MODE_AUTO; + st->forced_tool = -1; + return st; +} + +tgj_state *tgj_state_new_forced(const tgj_grammar *g, const char *forced_name) { + if (!g) return NULL; + int pin = -1; + if (forced_name && *forced_name) { + pin = set_exact_match(&g->names, -1, forced_name, (int)strlen(forced_name)); + if (pin < 0) return NULL; + } + tgj_state *st = tgj_state_new(g); + if (!st) return NULL; + st->mode = TGJ_MODE_FORCED; + st->forced_tool = pin; + return st; +} + +tgj_state *tgj_state_new_required(const tgj_grammar *g, bool think_open) { + tgj_state *st = tgj_state_new_forced(g, NULL); + if (!st) return NULL; + st->think_gated = think_open; + /* No open think block: the first generated char is already the answer, + * so the call is compelled from token 0 (nothing to wait for). */ + if (!think_open) st->reasoning_ended = true; + return st; +} + +tgj_state *tgj_state_clone(const tgj_state *st) { + if (!st) return NULL; + tgj_state *copy = malloc(sizeof(*copy)); + if (copy) *copy = *st; + return copy; +} + +void tgj_state_copy(tgj_state *dst, const tgj_state *src) { + if (dst && src) *dst = *src; +} + +void tgj_state_free(tgj_state *st) { free(st); } + +void tgj_reasoning_end(tgj_state *st) { + if (st) st->reasoning_ended = true; +} + +/* ── phase helpers ── */ + +static bool is_ws(char c) { + return c == ' ' || c == '\t' || c == '\n' || c == '\r'; +} + +/* Enter a phase that gates on "optional whitespace, then the literal char + * `expect`". */ +static void enter_expect(tgj_state *st, tgj_phase p, char expect) { + st->phase = p; + st->expect = expect; +} + +/* Enter a quoted-token phase (function key / name / arg key). */ +static void enter_quote(tgj_state *st, tgj_phase p) { + st->phase = p; + st->in_quote = false; + st->quote_opened = false; + st->acc_len = 0; +} + +/* Advance a running fixed-literal; 1 = fully consumed, 0 = mid-literal, + * -1 = mismatch (DEAD). */ +static int lit_step(tgj_state *st, char c) { + if (st->lit[st->lit_pos] != c) { st->phase = TGJ_DEAD; return -1; } + st->lit_pos++; + return st->lit[st->lit_pos] == '\0' ? 1 : 0; +} + +/* Begin the arguments object: reset the per-call key bookkeeping. */ +static void enter_args_open(tgj_state *st) { + memset(st->key_done, 0, sizeof(st->key_done)); + st->required_left = (st->cur_tool >= 0) + ? st->g->tools[st->cur_tool].n_required : 0; + enter_expect(st, TGJ_ARGS_OPEN, '{'); +} + +/* After '{' of the arguments object: if the tool has ≥1 key, require a first + * key (can't close empty while required keys remain); if it has NO keys at all, + * an empty object is the only legal content → go straight to the separator so + * '}' closes. A tool with keys but zero required may also legally emit {}. */ +static void enter_first_arg(tgj_state *st) { + const tgj_tool *t = &st->g->tools[st->cur_tool]; + if (t->keys.n == 0) { + /* No parameters: the only valid arguments object is {} → allow the close + * (or trailing ws) immediately. */ + st->phase = TGJ_ARG_SEP; + st->val_started = false; /* signals "before any value": only '}' valid */ + return; + } + enter_quote(st, TGJ_ARG_KEY); +} + +/* Begin a value of the given type. acc_len is cleared because TGJ_T_ENUM + * accumulates the value into acc[] (the key that was just matched lives there + * until now) and matches it against the key's closed set. */ +static void enter_value(tgj_state *st, tgj_vtype vt) { + st->phase = TGJ_ARG_VALUE; + st->vtype = vt; + st->val_content = false; + st->val_depth = 0; + st->val_started = false; + st->in_quote = false; + st->quote_opened = false; + st->esc = false; + st->acc_len = 0; +} + +/* ── the quoted-key / quoted-name / quoted-string steppers ── */ + +/* Step a quoted KEY/NAME phase matching against a set (arg keys / tool names). + * `set`/`pin` select the live members; on the closing '"' the accumulated token + * must exactly match a member. `on_match(idx)` is invoked with the matched + * member index; the caller transitions. Returns the matched index or -1 + * (still in progress → phase unchanged, or DEAD). */ +static int step_quoted_set(tgj_state *st, char c, const tgj_set *set, int pin) { + if (!st->quote_opened) { + if (is_ws(c)) return -1; /* leading ws before the quote */ + if (c == '"') { st->quote_opened = true; st->in_quote = true; return -1; } + st->phase = TGJ_DEAD; return -1; + } + if (c == '"') { /* closing quote */ + int idx = set_exact_match(set, pin, st->acc, st->acc_len); + if (idx < 0) { st->phase = TGJ_DEAD; return -1; } + st->in_quote = false; + return idx; /* caller transitions */ + } + if (set_char_live(set, pin, st->acc_len, c)) { + if (st->acc_len < TGJ_NAME_MAX - 1) st->acc[st->acc_len] = c; + st->acc_len++; + return -1; + } + st->phase = TGJ_DEAD; + return -1; +} + +/* Step a quoted KEY phase matching against an alt-spelling list (function key / + * arguments key). Returns 1 on the closing '"' with an exact match (caller + * transitions), 0 in progress, -1 DEAD. */ +static int step_quoted_alts(tgj_state *st, char c) { + if (!st->quote_opened) { + if (is_ws(c)) return 0; + if (c == '"') { st->quote_opened = true; return 0; } + st->phase = TGJ_DEAD; return -1; + } + if (c == '"') { + if (!alts_exact(st->alts, st->n_alts, st->acc, st->acc_len)) { + st->phase = TGJ_DEAD; return -1; + } + return 1; + } + if (alts_char_live(st->alts, st->n_alts, st->acc_len, c)) { + if (st->acc_len < TGJ_NAME_MAX - 1) st->acc[st->acc_len] = c; + st->acc_len++; + return 0; + } + st->phase = TGJ_DEAD; + return -1; +} + +/* Step a typed VALUE. Returns 1 when the value is COMPLETE (caller transitions + * to the separator), 0 while still mid-value, -1 DEAD. On completion the char + * that terminated the value has NOT been consumed for strings (the closing '"' + * IS consumed); for scalars/JSON the terminator is a structural char (',' '}' + * or ws) that the SEPARATOR phase must re-see — so we signal completion and let + * the caller re-dispatch that same char. */ +static int step_value(tgj_state *st, char c, bool *reconsume) { + *reconsume = false; + switch (st->vtype) { + + case TGJ_T_STRING: + if (!st->quote_opened) { + if (is_ws(c)) return 0; /* ws before the string */ + if (c == '"') { st->quote_opened = true; return 0; } + st->phase = TGJ_DEAD; return -1; + } + if (st->esc) { + /* The escape's DECODED value decides whether this counts as content: + * \n \t \r \f are whitespace and must not satisfy the guard. (\b is + * a backspace, not whitespace; \" \\ \/ are real content.) + * + * KNOWN GAP: \uXXXX is not decoded here — this FSM has no hex + * accumulator, so " " still counts as content. Deliberate and + * backstopped: the client's require_string() skips + * ALL leading whitespace before testing for empty, so a + * whitespace-only value fails at the tool however it was spelled. */ + st->esc = false; + if (c != 'n' && c != 't' && c != 'r' && c != 'f') + st->val_content = true; + return 0; + } + /* A lone backslash is not content yet — the char it escapes decides. */ + if (c == '\\') { st->esc = true; return 0; } + if (c == '"') { + /* EMPTY-STRING FLOOR — per key, from the schema, not blanket. + * + * The closing quote may only fire once the value holds ≥1 + * NON-WHITESPACE char, and ONLY for a key whose schema forbids an + * empty value (tool_param_empty_value_rule.h: a non-string type, or + * `minLength` ≥ 1). Both halves of that sentence were wrong here at + * different times, in opposite directions: + * + * • The guard first counted ANY in-quote char, so `" "` and `"\n"` + * satisfied it — the empty-path wedge, and a comment claiming + * `"path": ""` was "structurally impossible" while it was not. + * • Fixing that left the guard UNCONDITIONAL, which is the mirror + * defect: `edit`'s `new_string:""` is a legal deletion, and an + * ornith model could not emit one at all. Under + * tool_choice=required an unemittable legal call is a wedge of + * its own, just a quieter one. + * + * The XML dialect runs the identical predicate at TG_VALUE, compiled + * from the identical function, because these two grammars answering + * one schema differently is this module's recurring defect rather + * than a hypothetical. + * + * Whitespace still does not count. ornith's parser does not trim + * (push_call_json forwards the value verbatim), so `{"path":" "}` + * survives to the tool — where the client's require_string skips leading + * whitespace before testing for empty and refuses it anyway. A value + * The client will certainly refuse is one this grammar should never + * have let the model spend a turn on. */ + if (st->val_floor && !st->val_content) { st->phase = TGJ_DEAD; return -1; } + return 1; /* closing quote consumed */ + } + if (!is_ws(c)) st->val_content = true; + return 0; + + case TGJ_T_NUMBER: + if (!st->val_started) { + if (is_ws(c)) return 0; + if (c == '-' || (c >= '0' && c <= '9')) { + st->val_started = true; st->val_content = true; return 0; + } + st->phase = TGJ_DEAD; return -1; + } + if ((c >= '0' && c <= '9') || c == '.' || c == 'e' || c == 'E' || + c == '+' || c == '-') + return 0; /* still in the number */ + /* A structural char / ws ends the number → complete, re-see it. */ + *reconsume = true; + return 1; + + case TGJ_T_BOOL: + if (!st->val_started) { + if (is_ws(c)) return 0; + if (c == 't') { st->lit = "true"; st->lit_pos = 1; st->val_started = true; return 0; } + if (c == 'f') { st->lit = "false"; st->lit_pos = 1; st->val_started = true; return 0; } + st->phase = TGJ_DEAD; return -1; + } + if (st->lit[st->lit_pos] != c) { st->phase = TGJ_DEAD; return -1; } + st->lit_pos++; + if (st->lit[st->lit_pos] == '\0') { st->val_content = true; return 1; } + return 0; + + case TGJ_T_ENUM: + /* Closed alternation: the value is a quoted literal that must EXACTLY + * equal one member. Reuses step_quoted_set — the identical prefix walk + * that constrains the tool NAME and the parameter KEY — so a member split + * across token boundaries matches, an off-set char dies at the first + * divergence, and an empty "" is impossible (no member is empty). */ + if (!st->enum_set) { st->phase = TGJ_DEAD; return -1; } + if (step_quoted_set(st, c, st->enum_set, -1) >= 0) { + st->val_content = true; + return 1; /* closing quote consumed */ + } + return (st->phase == TGJ_DEAD) ? -1 : 0; + + case TGJ_T_JSON: + default: + if (!st->val_started) { + if (is_ws(c)) return 0; + if (c == '{' || c == '[') { + st->val_started = true; st->val_depth = 1; + st->val_content = true; st->in_quote = false; st->esc = false; + return 0; + } + st->phase = TGJ_DEAD; return -1; + } + /* Balanced-brace scan, string-aware (braces inside strings don't count). */ + if (st->in_quote) { + if (st->esc) { st->esc = false; return 0; } + if (c == '\\') { st->esc = true; return 0; } + if (c == '"') st->in_quote = false; + return 0; + } + if (c == '"') { st->in_quote = true; return 0; } + if (c == '{' || c == '[') { st->val_depth++; return 0; } + if (c == '}' || c == ']') { + st->val_depth--; + if (st->val_depth == 0) return 1; /* balanced → complete */ + return 0; + } + return 0; + } +} + +/* ── FREE-region opener tracking (AUTO grace + forced arm) ── */ + +/* Forward declaration: transition into the function-key phase after '{'. */ +static void enter_first_fn(tgj_state *st); + +/* Consume one char while FREE. Handles the reasoning grace, the self- + * detect, the forced budget, and the ```json-fence / bare-'{' opener trackers. + * Sets phase past FREE once an opener completes. */ +static void step_free(tgj_state *st, char c) { + bool opener_progress = (st->free_fence > 0 || st->free_brace); + + if (st->think_gated && !st->reasoning_ended) { + if (THINK_CLOSE[st->think_term] == c) { + st->think_term++; + if (THINK_CLOSE[st->think_term] == '\0') { + st->reasoning_ended = true; + st->forced_pending = true; /* compelled from the next TOKEN */ + st->think_term = 0; + } + } else { + st->think_term = (c == THINK_CLOSE[0]) ? 1 : 0; + } + return; + } + + if (st->mode == TGJ_MODE_FORCED && !st->reasoning_ended) { + st->reason_chars++; + if (st->reason_chars > TGJ_REASON_BUDGET && !opener_progress) { + st->reasoning_ended = true; + st->forced_pending = true; + st->think_term = 0; + st->free_fence = 0; st->free_brace = false; + return; /* armed; next TOKEN masked */ + } + if (THINK_CLOSE[st->think_term] == c) { + st->think_term++; + if (THINK_CLOSE[st->think_term] == '\0') { + st->reasoning_ended = true; + st->forced_pending = true; /* … from the next TOKEN */ + st->think_term = 0; + st->free_fence = 0; st->free_brace = false; + return; + } + } else { + st->think_term = (c == THINK_CLOSE[0]) ? 1 : 0; + } + /* fall through to the opener tracker so a call opened mid-reasoning + * (no ) still engages the skeleton */ + } + + bool forced = (st->mode == TGJ_MODE_FORCED && st->reasoning_ended && + !st->forced_pending); + + /* Forced grace: leading whitespace before the opener is tolerated so the + * model's "\n```json" separator survives (mirrors the XML grammar). + * Only before any opener progress. */ + if (forced && !opener_progress && is_ws(c)) return; + + /* Try to extend the ```json fence match. */ + if (st->free_fence >= 0 && !st->free_brace) { + if (FENCE_OPEN[st->free_fence] == c) { + st->free_fence++; + if (FENCE_OPEN[st->free_fence] == '\0') { + /* fence body matched; consume the trailing newline(s) then '{' */ + st->fenced = true; + st->free_fence = 0; st->free_brace = false; + enter_expect(st, TGJ_OBJ_OPEN, '{'); + } + return; + } + /* Not extending the fence. A bare '{' opener is also valid. */ + if (st->free_fence == 0 && c == '{') { + st->fenced = false; + st->free_brace = false; st->free_fence = 0; + enter_first_fn(st); /* fwd-declared below */ + return; + } + /* Broken opener prefix. */ + if (forced) { st->phase = TGJ_DEAD; return; } + /* AUTO: reset; re-seed if this char restarts a fence. */ + st->free_fence = (c == FENCE_OPEN[0]) ? 1 : 0; + return; + } +} + +/* ── the master char stepper ── */ + +static void step_char(tgj_state *st, char c) { + switch (st->phase) { + + case TGJ_FREE: + step_free(st, c); + return; + + case TGJ_OBJ_OPEN: + if (is_ws(c)) return; + if (c == '{') { enter_first_fn(st); return; } + st->phase = TGJ_DEAD; + return; + + case TGJ_FN_KEY: { + int r = step_quoted_alts(st, c); + if (r == 1) enter_expect(st, TGJ_FN_COLON, ':'); + return; + } + + case TGJ_FN_COLON: + if (is_ws(c)) return; + if (c == st->expect) { enter_quote(st, TGJ_FN_NAME); return; } + st->phase = TGJ_DEAD; + return; + + case TGJ_FN_NAME: { + int idx = step_quoted_set(st, c, &st->g->names, st->forced_tool); + if (idx >= 0) { st->cur_tool = idx; enter_expect(st, TGJ_FN_COMMA, ','); } + return; + } + + case TGJ_FN_COMMA: + if (is_ws(c)) return; + if (c == st->expect) { + enter_quote(st, TGJ_ARGS_KEY); + st->alts = ARGS_KEY_ALTS; st->n_alts = 2; + return; + } + st->phase = TGJ_DEAD; + return; + + case TGJ_ARGS_KEY: { + int r = step_quoted_alts(st, c); + if (r == 1) enter_expect(st, TGJ_ARGS_COLON, ':'); + return; + } + + case TGJ_ARGS_COLON: + if (is_ws(c)) return; + if (c == st->expect) { enter_args_open(st); return; } + st->phase = TGJ_DEAD; + return; + + case TGJ_ARGS_OPEN: + if (is_ws(c)) return; + if (c == '{') { enter_first_arg(st); return; } + st->phase = TGJ_DEAD; + return; + + case TGJ_ARG_KEY: { + const tgj_tool *t = &st->g->tools[st->cur_tool]; + int idx = step_quoted_set(st, c, &t->keys, -1); + if (idx >= 0) { + /* Reject a duplicate key (a completed call can't set one twice). */ + if (idx < TGJ_MAX_ARGS) { + if (st->key_done[idx]) { st->phase = TGJ_DEAD; return; } + st->key_done[idx] = true; + } + if (t->required[idx] && st->required_left > 0) st->required_left--; + enter_expect(st, TGJ_ARG_COLON, ':'); + /* stash the type (and its closed value set, if any) for enter_value */ + st->vtype = t->types[idx]; + st->enum_set = (t->enums && t->enums[idx].n > 0) ? &t->enums[idx] : NULL; + st->val_floor = t->forbids_empty ? t->forbids_empty[idx] : false; + } + return; + } + + case TGJ_ARG_COLON: + if (is_ws(c)) return; + if (c == st->expect) { enter_value(st, st->vtype); return; } + st->phase = TGJ_DEAD; + return; + + case TGJ_ARG_VALUE: { + bool reconsume = false; + int r = step_value(st, c, &reconsume); + if (r == 1) { + st->phase = TGJ_ARG_SEP; + st->val_started = true; /* a value was emitted */ + if (reconsume) step_char(st, c); /* re-dispatch the terminator */ + } + return; + } + + case TGJ_ARG_SEP: + if (is_ws(c)) return; + if (c == ',') { + /* another key required */ + enter_quote(st, TGJ_ARG_KEY); + return; + } + if (c == '}') { + /* Close the arguments object ONLY if every required key was set. */ + if (st->required_left > 0) { st->phase = TGJ_DEAD; return; } + enter_expect(st, TGJ_OBJ_CLOSE, '}'); + return; + } + st->phase = TGJ_DEAD; + return; + + case TGJ_OBJ_CLOSE: + if (is_ws(c)) return; + if (c == '}') { + if (st->fenced) { + st->lit = FENCE_CLOSE; st->lit_pos = 0; + st->phase = TGJ_FENCE_CLOSE; + } else { + st->phase = TGJ_DONE; + } + return; + } + st->phase = TGJ_DEAD; + return; + + case TGJ_FENCE_CLOSE: + /* Tolerate whitespace/newline before the closing ``` fence. */ + if (st->lit_pos == 0 && is_ws(c)) return; + if (lit_step(st, c) == 1) st->phase = TGJ_DONE; + return; + + case TGJ_DONE: + st->phase = TGJ_DEAD; + return; + + case TGJ_DEAD: + case TGJ_FENCE_OPEN: /* fence-open is handled inside step_free */ + default: + return; + } +} + +/* '{' just consumed for the OUTER object → the function key must follow. */ +static void enter_first_fn(tgj_state *st) { + enter_quote(st, TGJ_FN_KEY); + st->alts = FN_KEY_ALTS; + st->n_alts = 2; +} + +static void walk(tgj_state *st, const char *text) { + for (const char *p = text; *p; p++) step_char(st, *p); +} + +bool tgj_token_allowed(const tgj_state *st, const char *token_text) { + if (!st) return false; + if (!token_text || !*token_text) return true; + tgj_state clone = *st; + walk(&clone, token_text); + return clone.phase != TGJ_DEAD; +} + +void tgj_advance(tgj_state *st, const char *token_text) { + if (!st || !token_text) return; + walk(st, token_text); + /* The token during which reasoning ended is over; the opener is compelled + * from the next one, which IS a sampling step and therefore is masked. + * See tgj_state.forced_pending. */ + st->forced_pending = false; +} + +int tgj_closing_text(const tgj_state *st, char *out, int cap) { + static const char *const CLOSERS[] = { "", "\n```", "}", "}\n```", "}}", "}}\n```", + "\"}}", "\"}}\n```" }; + if (!st || !out || cap < 1 || !tgj_in_call(st) || tgj_complete(st)) return -1; + for (size_t i = 0; i < sizeof(CLOSERS) / sizeof(CLOSERS[0]); i++) { + int n = (int)strlen(CLOSERS[i]); + if (n == 0 || n >= cap) continue; + tgj_state clone = *st; + walk(&clone, CLOSERS[i]); + if (clone.phase == TGJ_DONE) { + memcpy(out, CLOSERS[i], (size_t)n + 1); + return n; + } + } + return -1; +} + +bool tgj_end_allowed(const tgj_state *st) { + char closing[16]; + if (!st || !tgj_mask_active(st)) return true; + return tgj_closing_text(st, closing, (int)sizeof(closing)) > 0; +} + +bool tgj_complete(const tgj_state *st) { + return st && st->phase == TGJ_DONE; +} + +bool tgj_in_call(const tgj_state *st) { + return st && st->phase != TGJ_FREE && st->phase != TGJ_DEAD; +} + +bool tgj_is_dead(const tgj_state *st) { + return st && st->phase == TGJ_DEAD; +} + +int tgj_debug_reason_chars(const tgj_state *st) { + return st ? st->reason_chars : 0; +} + +bool tgj_forced_reasoning_ended(const tgj_state *st) { + /* DEAD excluded, exactly as in tool_grammar.c: the serve drives the + * emitter's reasoning/content channel split off this bit, and a dead walk + * compels nothing — so saying "yes" routes the model's ongoing THOUGHT into + * the content channel, where the transcript files it as an answer. */ + return st && st->mode == TGJ_MODE_FORCED && st->reasoning_ended && + st->phase != TGJ_DEAD; +} + +bool tgj_mask_active(const tgj_state *st) { + if (!st || st->phase == TGJ_DEAD) return false; + if (tgj_complete(st)) return false; + if (tgj_in_call(st)) return true; + if (st->mode != TGJ_MODE_FORCED) return false; + if (st->reasoning_ended) return true; + /* Forced early-arm: opener progress (fence or bare '{') during reasoning. */ + return st->free_fence > 0 || st->free_brace; +} diff --git a/src/serve/tool_grammar_json.h b/src/serve/tool_grammar_json.h new file mode 100644 index 0000000..f74cdc7 --- /dev/null +++ b/src/serve/tool_grammar_json.h @@ -0,0 +1,225 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_grammar_json.h + * @brief Grammar / state-machine that constrains tool-call decoding to ornith's + * RL-learned JSON emission (per-model tool-call dialect, Phase 2). + * + * The XML sibling (tool_grammar.h) constrains the Qwen/N2/MiniCPM XML skeleton. + * Ornith-1.0-9B instead emits a markdown-fenced JSON object in the content + * channel: + * + * ```json + * {"function": "NAME", "arguments": {"KEY": , …}} + * ``` + * + * (the fence is optional, "function" may be spelled "name", "arguments" may be + * spelled "parameters"). This module builds a char-level FSM from the request's + * `tools` JSON that recognizes exactly that shape, with NAME drawn from the tool + * names, KEY drawn from that tool's parameter names, EVERY `required` key + * present, and each value typed to its schema (string → a quoted string, + * integer/number → a numeric literal, boolean → true/false, object/array → a + * balanced {…}/[…]). An empty `{}` / a missing required key is structurally + * impossible to complete — the direct analog of the XML grammar's missing-key + * guard, and the reason ornith gets the same `required` guarantee saber gets + * from the XML grammar. + * + * A string value may be EMPTY only where the schema allows it: the closing quote + * is off-grammar on a value holding no non-whitespace char IF that parameter has + * a length floor (a non-string `type`, or `minLength` ≥ 1 — + * tool_param_empty_value_rule.h, shared with the XML dialect so one schema + * cannot mean two things depending on which model answered). `read`'s `path` + * cannot be `""`; `edit`'s `new_string` still can, because empty is what a + * deletion IS. + * + * A parameter carrying a string `enum` is tighter still: its value is a closed + * alternation over the listed literals, so an off-list value is unreachable. + * Enums that cannot be represented as a string alternation (non-string members, + * a contradicting `type`, more members than the cap) DEGRADE to the plain type + * rather than reject — see build_key_enum in the .c for the exact admission + * rules. Every other schema keyword (minLength, pattern, const, numeric + * bounds, nested shape) is still dropped. + * + * The public contract MIRRORS tool_grammar.h exactly (tgj_* == tg_*), so the + * serve sampler hook (pn_tool_grammar_hook / sg_tg_* glue in serve_generation.c) + * is reused unchanged — only the grammar object it drives differs by dialect. + * + * REASONING / AUTO / FORCED composition is identical to the XML grammar: FREE + * reasoning/prose is unconstrained in AUTO and engages the skeleton only after + * an opener; FORCED mode replaces the FREE grace with "optional reasoning, then + * the opener is REQUIRED" (prose/garbage masked out), with the reasoning close + * self-detected from the raw "" in the stream and a reasoning budget as + * a backstop. Ornith runs reasoning_budget 0, so the grace is minimal — the + * budget + self-detected close both arm the opener cleanly (0-budget path). + * + * NO LANDING WINDOW HERE, deliberately. The XML grammar has + * tg_reasoning_end_injected: a bounded grace after a "" the SERVE + * force-decoded, so the opener is not compelled around a thought the model had + * not finished (the frame-capture defect — tool_grammar.c "FRAME CAPTURE", and + * tool_calling.c recover_captured_call). This grammar has the same structure + * and would have the same defect, but it is UNREACHABLE today: reasoning_budget + * 0 means the serve never injects a close for an ornith turn, so there is + * nothing to land from. That is a CONFIG fact, not a structural one — if ornith + * (or any future ORNITH_JSON model) is ever given a non-zero reasoning_budget, + * add tgj_reasoning_end_injected here and wire it in + * sg_tg_reasoning_ended_injected / sg_tg_attach_batched alongside the XML one. + * + * Pure C, libc-only — NO engine/CUDA deps — unit-tested in isolation + * (tests/test_tool_grammar_json.c) without the forward path. + * + * @status Phase 2 module — armed per sg_tool_grammar_arms (tool_choice + * "required" by default, every tool request with PANTHEON_TOOL_GRAMMAR=1) + * and selected by dialect (TC_DIALECT_ORNITH_JSON) in sg_tg_setup. + * "required" uses the think-gated walk (tgj_state_new_required). + * @issues None + * @todo None + */ + +#ifndef PN_TOOL_GRAMMAR_JSON_H +#define PN_TOOL_GRAMMAR_JSON_H + +#include + +#include "json_reader.h" /* jr_value */ + +/* Opaque compiled grammar (tool names + per-tool param keys/types/required). */ +typedef struct tgj_grammar tgj_grammar; + +/* Decoding mode for a walk state (same semantics as tg_mode). + * TGJ_MODE_AUTO : engage the skeleton only after an opener; FREE prose free. + * TGJ_MODE_FORCED : compel a call — after optional reasoning the opener is + * REQUIRED, so prose/garbage is masked out. */ +typedef enum { + TGJ_MODE_AUTO = 0, + TGJ_MODE_FORCED, +} tgj_mode; + +/* Mutable per-request walk position (small POD, cheap to copy by value — the + * allowed-check clones it and walks the candidate on the clone). */ +typedef struct tgj_state tgj_state; + +/** + * Build a grammar from the request's `tools` JSON array (OpenAI shape). Returns + * NULL if `tools` is not a non-empty array or on OOM. Free with tgj_free. + * + * Thread-safe: yes (no shared state) + */ +tgj_grammar *tgj_build(const jr_value *tools); + +/** Free a grammar (NULL-safe). */ +void tgj_free(tgj_grammar *g); + +/** Allocate a fresh AUTO-mode walk state at the start (FREE region). NULL OOM. */ +tgj_state *tgj_state_new(const tgj_grammar *g); + +/** + * Allocate a FORCED-mode walk state. After optional reasoning the opener is + * REQUIRED. `forced_name` (if non-NULL) pins NAME to that tool; NULL forces + * SOME tool ("required"). Returns NULL on OOM or if `forced_name` is not a + * known tool. + */ +tgj_state *tgj_state_new_forced(const tgj_grammar *g, const char *forced_name); + +/** + * Allocate the walk state for `tool_choice:"required"` (forced, any tool) with + * THINK-GATED reasoning. + * + * `think_open` says what the prompt did: true when it left a block OPEN + * (a think-style template with thinking on), false when there is no block to + * wait for (thinking off, or a non-think template). + * - think_open: everything before the model's own "" is reasoning and + * is never constrained or judged — no opener tracking inside it (a '{' the + * model writes while thinking is thought, not a call) and no char budget + * (the turn is bounded by max_tokens and, when set, the serve's + * reasoning_budget force-close, which ends reasoning via + * tgj_reasoning_end). From the token after "" the call is + * compelled exactly as in tgj_state_new_forced. + * - !think_open: reasoning has already ended; the call is compelled from the + * first token. + * The legacy tgj_state_new_forced grace (opener tracking + an 8192-char + * budget inside reasoning) is kept for the named-function form. + * + * Returns NULL on OOM / NULL grammar. + */ +tgj_state *tgj_state_new_required(const tgj_grammar *g, bool think_open); + +/** Heap copy of a walk state (NULL on OOM / NULL input). Free with + * tgj_state_free. Used to look ahead over speculative drafts without + * touching the live walk. */ +tgj_state *tgj_state_clone(const tgj_state *st); + +/** Overwrite `dst` with `src` (both from this module; same grammar). */ +void tgj_state_copy(tgj_state *dst, const tgj_state *src); + +/** Free a walk state (NULL-safe). */ +void tgj_state_free(tgj_state *st); + +/** + * Signal that the reasoning phase has ended. In FORCED mode this releases the + * grace and REQUIRES the opener from here on. A no-op in AUTO mode, idempotent. + * The grammar ALSO self-detects "" in its own stream, so forcing does + * not depend on this call (it is a harmless secondary trigger). + */ +void tgj_reasoning_end(tgj_state *st); + +/** + * Would appending `token_text` keep the walk on a valid grammar prefix? Does + * NOT mutate `st` (walks a clone). Empty/NULL text is always allowed (no-op). + */ +bool tgj_token_allowed(const tgj_state *st, const char *token_text); + +/** Walk the state forward by `token_text` (must have been allowed). */ +void tgj_advance(tgj_state *st, const char *token_text); + +/** + * The shortest text that takes this walk to a COMPLETE call, if the call can + * still be closed as it stands: the open string value's quote, the arguments' + * and the object's braces, the fence close — tried in that order of length and + * checked by walking a copy. None (-1) when a required key is still missing, a + * value is unfinished in a way no closer fixes (a half literal, an open nested + * container), or the walk is not inside a call. + * + * Used when the model ends its turn INSIDE the call (ornith closes the markdown + * fence inside a long `write` string and stops): the serve appends this so the + * turn ends in the call the model meant — what the parser's truncation repair + * does for an unconstrained turn — instead of a refused incomplete call. + * + * @return the length written to `out` (NUL-terminated), or -1 + */ +int tgj_closing_text(const tgj_state *st, char *out, int cap); + +/** + * May the turn END (an end token, which has no text) here? Always once the + * mask is off — reasoning, AUTO prose, a completed call. For a "required" walk + * with the mask on: never before the call opens (the turn must make a call), + * and inside the call only when tgj_closing_text can close it. + */ +bool tgj_end_allowed(const tgj_state *st); + +/** Has the walk reached a COMPLETE tool call (outer '}' + optional fence close)? */ +bool tgj_complete(const tgj_state *st); + +/** Is the walk currently INSIDE a call (past an opener, not FREE / DEAD)? */ +bool tgj_in_call(const tgj_state *st); + +/** Has the walk left the grammar entirely (TGJ_DEAD)? See tg_is_dead() in + * tool_grammar.h for why this is a different question from !tgj_in_call(). */ +bool tgj_is_dead(const tgj_state *st); + +/** + * Should the sampler MASK this step? Same gating policy as tg_mask_active: + * AUTO : true only once inside a call (FREE prose unconstrained). + * FORCED : also true once reasoning ended (or an opener is underway during + * reasoning), so the opener is compelled. Always false once complete + * or DEAD. + */ +bool tgj_mask_active(const tgj_state *st); + +/* DEBUG: current forced-mode reasoning-grace char count (0 in AUTO / NULL). */ +int tgj_debug_reason_chars(const tgj_state *st); + +/* Has forced-mode reasoning ended (opener now compelled)? True once "" + * was seen OR the reasoning budget tripped. False in AUTO / when NULL. */ +bool tgj_forced_reasoning_ended(const tgj_state *st); + +#endif /* PN_TOOL_GRAMMAR_JSON_H */ diff --git a/src/serve/tool_param_empty_value_rule.c b/src/serve/tool_param_empty_value_rule.c new file mode 100644 index 0000000..59aefb2 --- /dev/null +++ b/src/serve/tool_param_empty_value_rule.c @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_param_empty_value_rule.c + * @brief Implementation of the shared may-this-value-be-empty schema rule. + * + * Pure C, libc-only, one function. See the header for the rule and why it is + * shared rather than duplicated per dialect. + * + * @status Complete + * @issues None + * @todo None + */ +#include "tool_param_empty_value_rule.h" + +#include + +/* Sentinel distinct from any legal minLength, so "absent" and "0" stay + * distinguishable through jr_as_number's fallback. */ +#define MIN_LENGTH_ABSENT (-1.0) + +bool tool_param_forbids_empty_value(const jr_value *prop) +{ + const char *type = jr_as_string(jr_object_get(prop, "type")); + + /* Nothing advertised as non-string has an empty form to permit. */ + if (!type || strcmp(type, "string") != 0) + return true; + + const jr_value *min = jr_object_get(prop, "minLength"); + if (jr_kind(min) != JR_NUMBER) + return false; + + /* ONLY THE FLOOR'S EXISTENCE IS COMPILED, never its magnitude. A walk that + * enforced minLength: 12 would have to count characters BEFORE the parser + * trims them, so an interior-space value like "a b" would be measured + * differently by the grammar and by the tool — and being STRICTER than the + * schema makes a legal call impossible to emit, which is the one outcome + * this module's degrade-don't-reject precedent (TG_REQ_MAX_KEYS, + * TGJ_ENUM_MAX_MEMBERS) forbids. minLength ≥ 2 therefore degrades to ≥ 1; + * the remainder is the client's to check, where the whole value is in hand. + * Every floor in the client's schemas is 1, so nothing is lost today. */ + return jr_as_number(min, MIN_LENGTH_ABSENT) >= 1.0; +} diff --git a/src/serve/tool_param_empty_value_rule.h b/src/serve/tool_param_empty_value_rule.h new file mode 100644 index 0000000..0683014 --- /dev/null +++ b/src/serve/tool_param_empty_value_rule.h @@ -0,0 +1,60 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file tool_param_empty_value_rule.h + * @brief The one question both tool-call grammars ask of a parameter's JSON + * Schema: may this parameter's value be EMPTY? + * + * WHY THIS IS ITS OWN MODULE. The XML dialect (tool_grammar.c) and the JSON + * dialect (tool_grammar_json.c) each own a value walk, and each needs the same + * verdict at the same moment — the instant the model tries to CLOSE a value that + * holds nothing. Every time this pair has been given two copies of one rule the + * copies have drifted, and the drift is the defect: the whitespace guard was + * fixed on one dialect and not the other, and the empty guard was later removed + * from XML while JSON kept an unconditional one, so the same schema meant two + * different things depending on which model answered. One function, compiled + * into both, makes that class of bug unrepresentable. + * + * @status Complete. Mirrors the agent client's own rule byte for byte. + * @issues None + * @todo None + */ +#ifndef PANTHEON_TOOL_PARAM_EMPTY_VALUE_RULE_H +#define PANTHEON_TOOL_PARAM_EMPTY_VALUE_RULE_H + +#include + +#include "json_reader.h" + +/** + * Does this parameter's schema forbid an empty value? + * + * The rule, and it is DERIVED not invented — it is the same predicate the + * client enforces on arrival (the client's schema code, + * param_has_length_floor), read off the same two keywords: + * + * floored ⇔ the declared `type` is absent or is not "string" + * OR `minLength` is a number ≥ 1 + * + * So `{"type":"string","minLength":1}` is floored and `{"type":"string"}` is + * not, which is exactly the distinction between `read`'s `path` (empty is the + * empty-path wedge) and `edit`'s `new_string` (empty IS the deletion). Both + * are `required`; requiredness is therefore NOT the predicate and is not + * consulted here. Emptiness is a question about what the parameter MEANS, and + * `minLength` is the schema's own way of saying it. + * + * A non-string type is floored regardless of `minLength` because it has no + * empty form to permit: `""` arriving where an integer belongs is a malformed + * call, not a permitted one. An untyped parameter is floored for the same + * reason the client floors it — nothing advertised it as able to mean empty. + * + * @param prop The parameter's schema object (properties[key]); NULL/non-object + * is treated as untyped, i.e. floored. + * @return true if an empty value must be refused for this parameter. + * + * Thread-safe: yes (pure read of a caller-owned tree) + * Reentrant: yes + */ +bool tool_param_forbids_empty_value(const jr_value *prop); + +#endif /* PANTHEON_TOOL_PARAM_EMPTY_VALUE_RULE_H */ diff --git a/tests/ab_build_arm.sh b/tests/ab_build_arm.sh new file mode 100755 index 0000000..c924d7b --- /dev/null +++ b/tests/ab_build_arm.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# ab_build_arm.sh — build ONE arm of an A/B and prove the binary really changed. +# +# WHY THIS EXISTS +# A kernel/engine A/B is only a measurement if the two arms are two different +# binaries. A build that quietly hands back the PREVIOUS binary makes the A/B +# read 1.000x and makes a bit-exactness gate pass vacuously — both of which +# look like an honest "no change" rather than a broken build. That has +# already happened once here: regenerating the generated PTX header +# foundry/cuda/fc_cuda_ptx.h changed no .c file, so make said "is up to date" +# and returned a byte-identical harness binary. +# +# The Makefile now depends on headers, so that specific hole is closed. This +# script is the belt to that braces: make can still be defeated by an +# mtime tie (a restored/copied file landing in the same second as the binary), +# by a clock skew over a network mount, by a flag change make cannot see, or +# by the next generated artefact nobody wired up. Identical bytes out of two +# arms is ALWAYS an error, never a result. +# +# USAGE +# tests/ab_build_arm.sh [MAKE VARS/ARGS...] +# Deletes the target, builds it, stores it under $AB_DIR//, and +# FAILS if the result is byte-identical to any arm already stored. +# +# tests/ab_build_arm.sh --compare +# Just compares two binaries; fails if identical. +# +# Env: AB_DIR (default build/ab) — where arms are kept. +# +# EXIT CODES +# 0 built, and this arm differs from every other stored arm +# 1 usage / build failure +# 2 IDENTICAL BINARY — the build produced no change (this is the bug) +# +# EXAMPLE (the standard shape for a kernel A/B) +# git checkout && tests/ab_build_arm.sh base tests/test_spec_qwen35 USE_CUDA=1 +# git checkout && tests/ab_build_arm.sh new tests/test_spec_qwen35 USE_CUDA=1 +# # then run each arm's stored binary, not whatever is in the worktree. + +set -uo pipefail + +AB_DIR="${AB_DIR:-build/ab}" + +digest() { + if command -v md5sum >/dev/null 2>&1; then md5sum "$1" | awk '{print $1}' + else md5 -q "$1"; fi +} + +fail_identical() { + echo "" >&2 + echo "FAIL: the build produced a byte-identical binary." >&2 + echo " $1" >&2 + echo " $2" >&2 + echo "" >&2 + echo " This is a BROKEN BUILD, not a measurement. A change was made and" >&2 + echo " the compiler emitted the same bytes, so any A/B on these two" >&2 + echo " binaries reads 1.000x and any bit-exactness gate passes" >&2 + echo " vacuously. Find out why the change did not reach the binary" >&2 + echo " (stale generated header? mtime tie? wrong tree? wrong flags?)" >&2 + echo " before you record a single number." >&2 + exit 2 +} + +if [ "${1:-}" = "--compare" ]; then + [ $# -eq 3 ] || { echo "usage: $0 --compare " >&2; exit 1; } + [ -f "$2" ] && [ -f "$3" ] || { echo "missing file" >&2; exit 1; } + if cmp -s "$2" "$3"; then fail_identical "$2 $(digest "$2")" "$3 $(digest "$3")"; fi + echo "OK: $2 and $3 differ ($(digest "$2") vs $(digest "$3"))" + exit 0 +fi + +[ $# -ge 2 ] || { sed -n '2,40p' "$0"; exit 1; } + +ARM="$1"; TARGET="$2"; shift 2 + +# Delete first: never let make's opinion of "up to date" decide whether this arm +# gets a fresh link. +rm -f "$TARGET" + +echo "== building arm '$ARM': make $TARGET $*" +if ! make "$TARGET" "$@"; then + echo "FAIL: build of $TARGET failed" >&2 + exit 1 +fi +[ -f "$TARGET" ] || { echo "FAIL: make reported success but $TARGET does not exist" >&2; exit 1; } + +mkdir -p "$AB_DIR/$ARM" +BASE="$(basename "$TARGET")" +DEST="$AB_DIR/$ARM/$BASE" +cp -p "$TARGET" "$DEST" +SUM="$(digest "$DEST")" +printf '%s %s\n' "$SUM" "$BASE" > "$DEST.md5" + +# Compare against every other arm holding a binary of the same name. +for other in "$AB_DIR"/*/"$BASE"; do + [ -f "$other" ] || continue + [ "$other" = "$DEST" ] && continue + if cmp -s "$other" "$DEST"; then + fail_identical "$other $(digest "$other")" "$DEST $SUM" + fi + echo " differs from $other ($(digest "$other"))" +done + +echo "OK: arm '$ARM' -> $DEST md5 $SUM" +echo " run THIS binary for the measurement, not the one in the worktree." diff --git a/tests/abandoned_call_fixture.h b/tests/abandoned_call_fixture.h new file mode 100644 index 0000000..d17fde3 --- /dev/null +++ b/tests/abandoned_call_fixture.h @@ -0,0 +1,33 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file abandoned_call_fixture.h + * @brief The raw tool text of an ABANDONED call: the shape the + * [tool_degenerate] diagnostic in openai_chat.c collect_tool_calls + * reports when a model opens a parameter and never fills or closes it. + * Reproduces an emission observed in real serving byte-for-byte; it + * carries no user content. + * + * WHAT IT SHOWS. The model opened a `read`, opened , + * wrote NO value at all, and then jumped straight to -- + * skipping entirely. tc_parse_tool_calls found no closed + * parameter, dropped the empty value, and produced `{}`, which is + * indistinguishable at the client from a model that emitted an empty + * argument. This is NOT the reasoning-budget force-close (zero FORCED + * close events in the same log), NOT string mangling, and NOT the + * frame-capture misroute: it was seen on a shallow prompt (~2,858 + * tokens), mode=REQUIRED, dialect=XML. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TEST_ABANDONED_CALL_FIXTURE_H +#define PN_TEST_ABANDONED_CALL_FIXTURE_H + +/* The raw tool channel text, 57 chars. Logged as `tool=read args={}`. */ +static const char FX_ABANDONED_READ_PATH[] = + "\n\n\n\n"; + +#endif /* PN_TEST_ABANDONED_CALL_FIXTURE_H */ diff --git a/tests/bench_batch_decode.c b/tests/bench_batch_decode.c new file mode 100644 index 0000000..a1f2177 --- /dev/null +++ b/tests/bench_batch_decode.c @@ -0,0 +1,103 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* Batched multi-sequence decode benchmark. + * + * Decodes B independent sequences together via pn_llama_forward_batch (one + * M=B GEMM per step, dequantizing each weight once and applying it to all B + * rows) and reports throughput vs batch size. The point: single-token decode + * is dequant-bound, so batching should raise TOTAL tok/s ~linearly with B + * while per-sequence tok/s stays roughly flat — i.e. B pages for ~the price of + * one. Each B builds `ctx` tokens of context (untimed) then times `n_gen` + * steady-state steps. + * + * Usage: bench_batch_decode [n_gen] [ctx] [Bmax] + */ + +#include "pantheon.h" +#include "src/core/engine.h" +#include "src/models/llama.h" + +#include +#include +#include +#include + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; +} + +static uint32_t argmax(const float *v, int n) { + int best = 0; float bv = v[0]; + for (int i = 1; i < n; i++) if (v[i] > bv) { bv = v[i]; best = i; } + return (uint32_t)best; +} + +/* Decode B sequences together. Returns ms per BATCH step (one M=B forward), + * measured over n_gen timed steps after building `ctx` tokens of context. */ +static double run_batch(pn_model *model, int B, int ctx, int n_gen) { + int n_vocab = model->config.n_vocab; + pn_state **st = calloc(B, sizeof(pn_state *)); + uint32_t *tok = malloc(B * sizeof(uint32_t)); + float *logits = malloc((size_t)B * n_vocab * sizeof(float)); + if (!st || !tok || !logits) { free(st); free(tok); free(logits); return -1; } + + for (int b = 0; b < B; b++) { + st[b] = pn_state_new(model); + tok[b] = (uint32_t)(1 + b); /* distinct seed per sequence */ + } + + /* Build context (untimed): each step is a real M=B decode from pos b. */ + for (int i = 0; i < ctx; i++) { + if (pn_llama_forward_batch(model, tok, NULL, st, B, logits) != 0) { + fprintf(stderr, "forward_batch unsupported\n"); + for (int b = 0; b < B; b++) pn_state_free(st[b]); + free(st); free(tok); free(logits); return -1; + } + for (int b = 0; b < B; b++) tok[b] = argmax(logits + (size_t)b * n_vocab, n_vocab); + } + + /* Timed steady-state decode. */ + double t0 = now_ms(); + for (int i = 0; i < n_gen; i++) { + pn_llama_forward_batch(model, tok, NULL, st, B, logits); + for (int b = 0; b < B; b++) tok[b] = argmax(logits + (size_t)b * n_vocab, n_vocab); + } + double ms_per_step = (now_ms() - t0) / n_gen; + + for (int b = 0; b < B; b++) pn_state_free(st[b]); + free(st); free(tok); free(logits); + return ms_per_step; +} + +int main(int argc, char **argv) { + const char *path = argc > 1 ? argv[1] : "models/lightonocr-lm-Q4_K_M.gguf"; + int n_gen = argc > 2 ? atoi(argv[2]) : 32; + int ctx = argc > 3 ? atoi(argv[3]) : 96; + int Bmax = argc > 4 ? atoi(argv[4]) : 8; + + pn_model *model = pn_load(path); + if (!model) { fprintf(stderr, "load failed: %s\n", path); return 1; } + + printf("batched decode on %s\n", path); + printf(" ctx=%d, timed n_gen=%d/seq\n\n", ctx, n_gen); + printf(" %3s | %10s | %12s | %12s | %8s\n", + "B", "ms/step", "per-seq t/s", "total t/s", "speedup"); + printf(" ----+------------+--------------+--------------+---------\n"); + + double base_total = 0; + for (int B = 1; B <= Bmax; B *= 2) { + double ms = run_batch(model, B, ctx, n_gen); + if (ms < 0) { pn_model_free(model); return 1; } + double per_seq = 1000.0 / ms; /* each sequence emits 1 tok per step */ + double total = per_seq * B; /* across all B sequences */ + if (B == 1) base_total = total; + printf(" %3d | %10.2f | %12.2f | %12.2f | %7.2fx\n", + B, ms, per_seq, total, total / base_total); + } + printf("\n (per-seq t/s ~flat + total t/s scaling with B = the batching win)\n"); + + pn_model_free(model); + return 0; +} diff --git a/tests/bench_depth_qwen35.c b/tests/bench_depth_qwen35.c new file mode 100644 index 0000000..317e0de --- /dev/null +++ b/tests/bench_depth_qwen35.c @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* Context-DEPTH decode bench for qwen35moe (Nex-N2-mini) on a K80 host. + * + * Part B of the coding-serving capacity bench. Isolates STEADY-STATE per-user + * DECODE throughput as the seeded context DEEPENS — because the 10 full- + * attention layers grow O(context) while the 30 SSM layers stay context-flat, + * so the streaming speed a user actually sees degrades with depth. We want the + * depth at which per-user decode falls below 8 tok/s (the "conversation" bar). + * + * Protocol (per K): + * For each user u in [0,K): + * 1. COLD PREFILL a SEED context of `depth` tokens through the batched + * prefill path (chunked in CHUNK-token passes so the K80 never sees a + * huge single pass), advancing state->pos to `depth`. UNTIMED (this is + * the warm-up; a real session pays this once, prefix-cached thereafter). + * 2. bind the slot immediately (GPU SSM image → slot u; host mirror is + * stale after a GPU prefill). + * Then time ONLY the batched decode loop: n_gen steps of + * pn_qwen35moe_decode_slots over all K users at position `depth`+. Report + * per-user decode tok/s ( (n_gen-1)/dt ) and aggregate ( K*(n_gen-1)/dt ). + * + * This is decode-at-depth, prefill EXCLUDED — the number a streaming user sees. + * + * Build (CUDA host): make USE_CUDA=1 tests/bench_depth_qwen35 + * Run: QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=0 QWEN35_GPU_DIE_B=1 \ + * QWEN35_FUSE=0x37 \ + * ./tests/bench_depth_qwen35 [n_gen] [chunk] + * Env: QWEN35_DEPTHS="1024,8192,32768,131072" runs the whole sweep in one + * model load at the given K (overrides the arg's single point). + * NOT part of the test suite (needs the model + a CUDA GPU). */ +#include "../include/pantheon.h" +#include "../src/core/engine.h" +#include "../src/models/qwen35moe.h" +#include "tensor/tensor.h" +#include +#include +#include +#include + +#ifndef USE_CUDA +int main(void) { fprintf(stderr, "built without USE_CUDA\n"); return 1; } +#else + +#define USERS_MAX 16 + +static double now_s(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int argmax(const float *x, int n) +{ + int best = 0; + for (int i = 1; i < n; i++) + if (x[i] > x[best]) best = i; + return best; +} + +/* Deterministic non-degenerate seed context: token ids walk a range so the + * attention/KV is realistically full (not one repeated id). Slot offset keeps + * users distinct. Kept < n_vocab. */ +static void fill_seed(uint32_t *buf, int depth, int n_vocab, int user) +{ + uint32_t base = 100u + (uint32_t)(user * 131); + for (int i = 0; i < depth; i++) + buf[i] = base + (uint32_t)((i * 7u + user * 13u) % 4096u); + for (int i = 0; i < depth; i++) + if (buf[i] >= (uint32_t)n_vocab) buf[i] %= (uint32_t)n_vocab; +} + +/* Chunked cold prefill of `depth` tokens into st. Returns seed token (argmax + * of last position) or -1. Chunked so the batched prefill never sees a huge + * single pass on the 12 GB K80. */ +static int prefill_depth(pn_model *model, pn_state *st, const uint32_t *seed, + int depth, int chunk, int n_vocab) +{ + int last = -1; + for (int off = 0; off < depth; off += chunk) { + int n = depth - off < chunk ? depth - off : chunk; + tm_tensor *out = + pn_qwen35moe_forward_batch_cuda(model, seed + off, n, st, NULL); + if (!out) return -1; + if (off + n >= depth) last = argmax(tm_data_f32(out), n_vocab); + tm_free(out); + } + return last; +} + +/* Run one (K, depth) point: seed K users to `depth`, then time n_gen batched + * decode steps. Fills *per_user and *agg (tok/s), *ok=1 on success. */ +static void run_point(pn_model *model, int K, int depth, int n_gen, int chunk, + int n_vocab, double *per_user, double *agg, int *ok, + double *prefill_s) +{ + pn_state *sts[USERS_MAX] = { 0 }; + uint32_t toks[USERS_MAX]; + float *logits[USERS_MAX]; + uint32_t *seed = malloc((size_t)depth * sizeof(uint32_t)); + *ok = 0; *per_user = 0; *agg = 0; *prefill_s = 0; + if (!seed) { fprintf(stderr, " OOM host seed buf (depth=%d)\n", depth); return; } + + double pf0 = now_s(); + for (int u = 0; u < K; u++) { + fill_seed(seed, depth, n_vocab, u); + sts[u] = pn_state_new(model); + if (!sts[u]) { fprintf(stderr, " state_new failed u=%d depth=%d\n", u, depth); goto done; } + int s = prefill_depth(model, sts[u], seed, depth, chunk, n_vocab); + if (s < 0) { fprintf(stderr, " PREFILL FAIL (likely OOM) u=%d depth=%d\n", u, depth); goto done; } + if (pn_qwen35moe_decode_slot_bind(model, sts[u], u, K) != 0) { + fprintf(stderr, " bind fail u=%d depth=%d\n", u, depth); goto done; + } + toks[u] = (uint32_t)s; + } + *prefill_s = now_s() - pf0; + + double t0 = now_s(); + for (int i = 1; i < n_gen; i++) { + if (pn_qwen35moe_decode_slots(model, toks, sts, K, logits) != 0) { + fprintf(stderr, " decode_slots FAIL @step %d depth=%d\n", i, depth); goto done; + } + for (int u = 0; u < K; u++) + toks[u] = (uint32_t)argmax(logits[u], n_vocab); + } + double dt = now_s() - t0; + *per_user = (n_gen - 1) / dt; + *agg = (double)K * (n_gen - 1) / dt; + *ok = 1; + +done: + for (int u = 0; u < K; u++) + if (sts[u]) pn_state_free(sts[u]); + free(seed); +} + +int main(int argc, char **argv) +{ + if (argc < 4) { + fprintf(stderr, + "usage: %s [n_gen] [chunk]\n" + " env QWEN35_DEPTHS=\"1024,8192,...\" sweeps depths at this K\n", + argv[0]); + return 1; + } + pn_model *model = pn_load(argv[1]); + if (!model) { fprintf(stderr, "load failed\n"); return 1; } + int n_vocab = model->config.n_vocab; + + int K = atoi(argv[2]); if (K < 1) K = 1; if (K > USERS_MAX) K = USERS_MAX; + int n_gen = argc > 4 ? atoi(argv[4]) : 256; if (n_gen < 2) n_gen = 2; + int chunk = argc > 5 ? atoi(argv[5]) : 512; if (chunk < 8) chunk = 8; + + fprintf(stderr, "depth bench: K=%d n_gen=%d chunk=%d n_vocab=%d\n", + K, n_gen, chunk, n_vocab); + + const char *depths_env = getenv("QWEN35_DEPTHS"); + char list[256]; + if (depths_env && depths_env[0]) + snprintf(list, sizeof(list), "%s", depths_env); + else + snprintf(list, sizeof(list), "%s", argv[3]); + + for (char *tk = strtok(list, ","); tk; tk = strtok(NULL, ",")) { + int depth = atoi(tk); + if (depth < 1) continue; + double per_user = 0, agg = 0, prefill_s = 0; int ok = 0; + run_point(model, K, depth, n_gen, chunk, n_vocab, + &per_user, &agg, &ok, &prefill_s); + if (ok) { + fprintf(stderr, + "DEPTH K=%d depth=%d: per-user %.2f tok/s | aggregate " + "%.2f tok/s | prefill %.1fs | %s8/user\n", + K, depth, per_user, agg, prefill_s, + per_user >= 8.0 ? ">=" : "<"); + printf("DEPTHBENCH K=%d depth=%d per_user=%.2f agg=%.2f prefill_s=%.1f\n", + K, depth, per_user, agg, prefill_s); + } else { + fprintf(stderr, "DEPTH K=%d depth=%d: FAILED/OOM (boundary)\n", K, depth); + printf("DEPTHBENCH K=%d depth=%d FAILED\n", K, depth); + } + fflush(stdout); fflush(stderr); + } + + fflush(stdout); fflush(stderr); + if (!getenv("QWEN35_EXIT_CLEAN")) _Exit(0); + return 0; +} +#endif diff --git a/tests/bench_ngram_accept.c b/tests/bench_ngram_accept.c new file mode 100644 index 0000000..fa1e890 --- /dev/null +++ b/tests/bench_ngram_accept.c @@ -0,0 +1,850 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bench_ngram_accept.c + * @brief PHASE B's CHEAP GATE: what acceptance rate does prompt-lookup (n-gram) drafting + * actually reach on this model, on real traffic — measured BEFORE any verify + * machinery exists. + * + * --------------------------------------------------------------------------- + * WHY THIS NEEDS NO VERIFY PATH, NO KERNEL AND NO ROLLBACK + * --------------------------------------------------------------------------- + * Greedy speculative decoding accepts a drafted token iff it equals the model's own argmax + * at that position, and an accepted token leaves the prefix identical to the plain greedy + * run's. So the whole acceptance distribution is a function of ONE ordinary greedy decode. + * See ngram_accept_stream.h for the argument written out. This harness therefore: + * + * --generate DUMP MODEL decode the corpus greedily on the SHIPPED arm, one token per + * forward, and record every token id. Needs the card. + * --score DUMP [DUMP...] replay the recorded streams as speculative rounds and report + * acceptance by K, by corpus, by phase and by context length. + * Loads no model, opens no card, costs seconds. + * --selftest known-answer arms that need neither. + * + * The drafter is `pn_ngram_draft` — the one that ships in src/core/ngram_draft.c, not a copy + * written for the measurement. + * + * --------------------------------------------------------------------------- + * WHAT MAKES THE NUMBER FALSIFIABLE + * --------------------------------------------------------------------------- + * This project has hit EIGHT vacuous-gate classes, including a gate that passed 132 checks on + * a shape the model does not have. A drafting hit counter that can only count hits measures + * nothing, so the same code path is run three more ways over the same bytes: + * + * --selftest a periodic stream (must hit the CEILING of K+1 tokens per forward) and + * an i.i.d. stream (must sit at 1.00). Both ends of the scale, no card. + * control=shuffle the real streams with the searched history permuted and only the query + * suffix left intact — same tokens, no structure. Must collapse to ~1.00. + * control=random the real rounds with the drafter's proposals replaced by uniform + * vocabulary ids. Must collapse to ~1.00. + * + * --------------------------------------------------------------------------- + * TEMPERATURE AND SPARSE LOGITS + * --------------------------------------------------------------------------- + * The generate pass decodes GREEDILY, which is what `--bench --temperature 0` does and what + * every speed figure on this card was taken at. Only the ARGMAX of each row is used, so + * `PN_TT_SPARSE_LOGITS` is safe here and is reported rather than refused — the card selects + * the exact top-N and the argmax over the sparse row is the argmax over the dense one. Any + * figure other than an argmax over that row would be noise, and this harness computes none. + * + * Under sampling the accept test becomes a rejection-sampling step rather than an equality, + * so every acceptance figure below is the GREEDY UPPER BOUND. + * + * USAGE + * ./tests/bench_ngram_accept --generate OUT.bin MODEL.gguf --corpus F --tag NAME + * [--n-gen N] [--max-docs N] [--system TEXT] + * ./tests/bench_ngram_accept --score DUMP.bin [DUMP.bin ...] + * ./tests/bench_ngram_accept --selftest + * + * Dumps are not shipped: record your own with --generate (tests/data/ngram_accept_dumps/ + * is gitignored for that purpose). + * + * NOT part of `make test`: --generate needs a model and a card. + * + * @status Working + * @issues None + * @todo None + */ +#include "../include/pantheon.h" +#include "../src/core/engine.h" +#include "tensor/tensor.h" + +#include "../src/core/chat.h" + +#include "chat_corpus.h" +#include "ngram_accept_stream.h" +#include "parity_accel_arm.h" +#include "tokenizer/tokenizer.h" + +#include +#include +#include +#include + +#define ACCEPT_ECHO_CHARS 96 +#define ACCEPT_MAX_GEN 4096 +#define ACCEPT_CONTROL_SEED 0x5DEECE66DULL + +/** The K values reported for every corpus. The task names 2, 4 and 8; the neighbours are + * free and show whether the curve has already flattened. */ +static const int accept_k_ladder[] = { 1, 2, 3, 4, 6, 8, 16 }; +#define ACCEPT_K_COUNT ((int)(sizeof(accept_k_ladder) / sizeof(accept_k_ladder[0]))) + +/** Trailing-history windows, in tokens; 0 = the whole context. Answers "does MORE history + * help", which is the mechanism behind any context-length trend. */ +static const int accept_window_ladder[] = { 128, 512, 2048, 8192, 0 }; +#define ACCEPT_WINDOW_COUNT ((int)(sizeof(accept_window_ladder) / sizeof(accept_window_ladder[0]))) + +static double accept_now_s(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int accept_argmax(const float *x, int n) +{ + int best = 0; + + for (int i = 1; i < n; i++) + if (x[i] > x[best]) + best = i; + return best; +} + +/* ================================================================ + * Pass 1 — the recorded greedy stream (needs the card) + * ================================================================ */ + +/** + * Report, once, whether the logit row is the sparse presentation. + * + * Not a refusal: this harness reads only the argmax, which is exact on that row. It is + * printed because a reader of the dump has to know which presentation produced it — the + * two-pass parity gate REFUSES the same row, and the difference has to be deliberate. + */ +static void accept_report_row_density(const float *row, int n) +{ + static int checked; + int non_finite = 0; + + if (checked) + return; + checked = 1; + for (int i = 0; i < n; i++) + if (!(row[i] > -1e30f && row[i] < 1e30f)) + non_finite++; + printf("ROW n=%d non_finite=%d presentation=%s (only the ARGMAX is read)\n", n, non_finite, + non_finite * 2 >= n ? "SPARSE" : "dense"); +} + +/** + * Prefill `prompt` and greedily decode up to `n_gen` tokens into `doc`. + * + * @return 0 on success, -1 if a forward failed + */ +static int accept_decode_document(pn_model *model, const uint32_t *prompt, int n_prompt, + int n_gen, int eos, int think_end, ngram_document *doc) +{ + pn_state *state = pn_state_new(model); + tm_tensor *logits = NULL; + double t_prefill, t0; + int n = 0; + + if (!state) + return -1; + doc->prompt = malloc((size_t)n_prompt * sizeof(*doc->prompt)); + doc->gen = malloc((size_t)n_gen * sizeof(*doc->gen)); + if (!doc->prompt || !doc->gen) { + pn_state_free(state); + return -1; + } + memcpy(doc->prompt, prompt, (size_t)n_prompt * sizeof(*doc->prompt)); + doc->n_prompt = n_prompt; + doc->think_end_pos = -1; + + t0 = accept_now_s(); + for (int i = 0; i < n_prompt; i++) { + if (logits) + tm_free(logits); + logits = model->forward(model, prompt[i], state); + if (!logits) { + fprintf(stderr, "generate: forward failed prefilling position %d\n", i); + pn_state_free(state); + return -1; + } + } + t_prefill = accept_now_s() - t0; + accept_report_row_density(tm_data_f32(logits), (int)logits->shape[0]); + + t0 = accept_now_s(); + while (n < n_gen) { + uint32_t tok = (uint32_t)accept_argmax(tm_data_f32(logits), model->config.n_vocab); + + tm_free(logits); + doc->gen[n] = tok; + if (think_end >= 0 && doc->think_end_pos < 0 && (int)tok == think_end) + doc->think_end_pos = n; + n++; + if ((int)tok == eos || n >= n_gen) + break; + logits = model->forward(model, tok, state); + if (!logits) { + fprintf(stderr, "generate: forward failed generating token %d\n", n); + break; + } + } + doc->n_gen = n; + doc->decode_s = accept_now_s() - t0; + printf(" prefill %d tok in %.1f s (%.3f s/tok) | decode %d tok in %.1f s (%.3f tok/s) " + "| think_end_pos=%d\n", n_prompt, t_prefill, t_prefill / (double)n_prompt, n, + doc->decode_s, (double)n / doc->decode_s, doc->think_end_pos); + pn_state_free(state); + return n > 1 ? 0 : -1; +} + +/** Echo the first characters of a token run so a reader can see it is real text. */ +static void accept_echo(pn_model *model, const uint32_t *ids, int n, const char *label) +{ + char *text = chat_ids_to_text(model, ids, n); + + printf(" %s \"", label); + for (int i = 0; text && text[i] && i < ACCEPT_ECHO_CHARS; i++) + putchar(text[i] == '\n' ? ' ' : text[i]); + printf("...\"\n"); + free(text); +} + +/** + * Slurp a corpus and split it into one system prompt and N user messages. + * + * The blocks are separated by lines reading exactly `===PROMPT===`; a block introduced by + * `===SYSTEM===` is the system prompt instead. A line-tagged format (the chat-parity + * corpus's `U`) cannot carry a source file, because source has blank lines in it and a + * blank line ends a document there — and a code-edit prompt with its blank lines stripped is + * not the traffic this is trying to measure. + * + * @return number of user messages; out[] points into the returned buffer, which the caller + * keeps alive for the run + */ +static int accept_load_corpus(const char *path, char **buffer_out, const char **system_out, + char **out, int max_out) +{ + char *buf, *cur; + long len; + int n = 0, next_is_system = 0; + FILE *f = fopen(path, "rb"); + + if (!f) + return -1; + fseek(f, 0, SEEK_END); + len = ftell(f); + fseek(f, 0, SEEK_SET); + buf = malloc((size_t)len + 2); + if (!buf || len <= 0 || fread(buf, 1, (size_t)len, f) != (size_t)len) { + fclose(f); + free(buf); + return -1; + } + buf[len] = '\0'; + fclose(f); + *buffer_out = buf; + + cur = buf; + while (cur && *cur && n < max_out) { + char *sep = strstr(cur, "\n===PROMPT===\n"); + char *sys = strstr(cur, "\n===SYSTEM===\n"); + int sys_first = sys && (!sep || sys < sep); + char *at = sys_first ? sys : sep; + + if (strncmp(cur, "===SYSTEM===\n", 13) == 0) { + next_is_system = 1; + cur += 13; + continue; + } + if (strncmp(cur, "===PROMPT===\n", 13) == 0) { + next_is_system = 0; + cur += 13; + continue; + } + if (at) + *at = '\0'; + if (*cur) { + if (next_is_system) + *system_out = cur; + else + out[n++] = cur; + } + next_is_system = sys_first; + cur = at ? at + 14 : NULL; + } + return n; +} + +#define ACCEPT_MAX_PROMPTS 32 + +static int accept_run_generate(const char *dump_path, const char *model_path, + const char *corpus_path, const char *tag, int n_gen, + int max_docs, const char *system_override) +{ + static uint32_t ids[CHAT_CORPUS_MAX_IDS]; + char *prompts[ACCEPT_MAX_PROMPTS]; + const char *system = NULL; + char *corpus_buffer = NULL; + const char *arm_name = "?"; + pn_forward_fn arm; + pn_model *model; + FILE *dump = NULL; + int n_prompts, docs = 0, think_end, eos, rc = 0; + + n_prompts = accept_load_corpus(corpus_path, &corpus_buffer, &system, prompts, + ACCEPT_MAX_PROMPTS); + if (n_prompts <= 0) { + fprintf(stderr, "generate: no prompts in %s\n", corpus_path); + free(corpus_buffer); + return 1; + } + if (system_override) + system = system_override; + if (n_prompts > max_docs) + n_prompts = max_docs; + model = pn_load(model_path); + if (!model) { + fprintf(stderr, "generate: load failed\n"); + free(corpus_buffer); + return 1; + } + /* THE ARM HAS TO BE THE ONE THAT SHIPS. Acceptance is a property of the model's own + * output distribution, so a CPU-forward stream would be measuring a different model. + * parity_select_accel_arm returns NULL when the engine left the model on its CPU + * forward, which is a refusal rather than a quiet substitution. */ + arm = parity_select_accel_arm(model, &arm_name); + if (!arm) { + fprintf(stderr, "generate: REFUSED — no accelerator arm. Acceptance measured on a " + "different forward is not the shipped model's acceptance.\n"); + pn_model_free(model); + free(corpus_buffer); + return 1; + } + model->forward = arm; + eos = ft_eos_id(model->tokenizer); + think_end = ft_token_to_id(model->tokenizer, ""); + + printf("GENERATE arm=%s arch=%d n_vocab=%d chat_style=%d eos=%d think_end=%d corpus=%s " + "prompts=%d tag=%s n_gen=%d out=%s\n", arm_name, (int)model->config.arch, + model->config.n_vocab, model->chat_style, eos, think_end, corpus_path, n_prompts, + tag, n_gen, dump_path); + printf("SYSTEM \"%.*s%s\"\n", 160, system ? system : "(none)", + system && strlen(system) > 160 ? "..." : ""); + fflush(stdout); + + if (ngram_stream_open_write(dump_path, (uint32_t)model->config.n_vocab, + (uint32_t)model->config.arch, (uint32_t)eos, + think_end < 0 ? NGRAM_STREAM_NONE : (uint32_t)think_end, + arm_name, tag, &dump) != 0) { + pn_model_free(model); + free(corpus_buffer); + return 1; + } + + for (docs = 0; docs < n_prompts; ) { + ngram_document doc = {0}; + pn_token_seq seq = pn_chat_format_first((pn_chat_style)model->chat_style, + model->tokenizer, system, prompts[docs]); + int n_ids = seq.n_tokens; + + if (!seq.tokens || n_ids <= 0 || n_ids > CHAT_CORPUS_MAX_IDS) { + fprintf(stderr, "generate: prompt %d did not tokenise (%d ids)\n", docs, n_ids); + free(seq.tokens); + rc = 1; + break; + } + memcpy(ids, seq.tokens, (size_t)n_ids * sizeof(*ids)); + free(seq.tokens); + printf("-- doc %d: %d prompt tokens\n", docs, n_ids); + accept_echo(model, ids, n_ids, "prompt"); + fflush(stdout); + if (accept_decode_document(model, ids, n_ids, n_gen, eos, think_end, &doc) != 0) { + free(doc.prompt); + free(doc.gen); + rc = 1; + break; + } + accept_echo(model, doc.gen, doc.n_gen, "output"); + if (ngram_stream_write_document(dump, &doc) != 0) { + fprintf(stderr, "generate: dump write failed at document %d\n", docs); + rc = 1; + } + free(doc.prompt); + free(doc.gen); + if (rc != 0) + break; + docs++; + fflush(stdout); + } + /* The end marker is what tells the scorer the writer FINISHED. It is written on a clean + * early break too — the case it exists to catch is a killed process, which never reaches + * here at all, and throwing away hours of card time because the last document refused to + * tokenise would be its own defect. The document count is printed on both sides. */ + if (ngram_stream_close_write(dump) != 0) + rc = 1; + printf("GENERATE %s: %d document(s)\n", rc == 0 ? "complete" : "FAILED", docs); + pn_model_free(model); + free(corpus_buffer); + return rc; +} + +/* ================================================================ + * Pass 2 — scoring (no model, no card) + * ================================================================ */ + +/** The drafter's key lengths. 3..2 is what the shipped throttle and the CUDA bench use. */ +#define ACCEPT_NGRAM_MAX 3 +#define ACCEPT_NGRAM_MIN 2 + +/** Run one (K, window, control, n-gram) cell over a whole dump. */ +static void accept_cell_ng(const ngram_stream *s, int max_k, int window, ngram_control control, + int ng_max, int ng_min, ngram_accept_stats *phase) +{ + memset(phase, 0, sizeof(*phase) * NGRAM_PHASE_COUNT); + for (int d = 0; d < s->n_docs; d++) + ngram_accept_replay(&s->docs[d], max_k, ng_max, ng_min, window, control, s->n_vocab, + ACCEPT_CONTROL_SEED + (uint64_t)d, phase); +} + +/** The same at the shipped key lengths. */ +static void accept_cell(const ngram_stream *s, int max_k, int window, ngram_control control, + ngram_accept_stats *phase) +{ + accept_cell_ng(s, max_k, window, control, ACCEPT_NGRAM_MAX, ACCEPT_NGRAM_MIN, phase); +} + +static void accept_print_row(const char *label, const ngram_accept_stats *a, int max_k) +{ + long rounds = a->rounds + a->plain_rounds; + + if (!rounds) { + printf(" %-14s (no rounds)\n", label); + return; + } + printf(" %-14s tok/fwd=%.3f | draft_rounds=%.0f%% accept=%.1f%% (%ld/%ld) | " + "E[delivered|drafted]=%.3f | emitted=%ld forwards=%ld\n", + label, ngram_tokens_per_forward(a), 100.0 * (double)a->rounds / (double)rounds, + a->drafted ? 100.0 * (double)a->accepted / (double)a->drafted : 0.0, a->accepted, + a->drafted, a->rounds ? 1.0 + (double)a->accepted / (double)a->rounds : 0.0, + a->emitted, a->forwards); + printf(" %-14s delivered histogram:", ""); + for (int i = 1; i <= max_k + 1; i++) + printf(" %d:%ld", i, a->hist[i]); + printf("\n"); +} + +/** The K ladder, split into reasoning and answer, plus the combined figure. */ +static void accept_report_k_ladder(const ngram_stream *s) +{ + printf("\n== K LADDER (window=all, ngram 3..2, control=none)\n"); + for (int i = 0; i < ACCEPT_K_COUNT; i++) { + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + + accept_cell(s, accept_k_ladder[i], 0, NGRAM_CONTROL_NONE, phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + printf("-- K=%d (verify %d rows)\n", accept_k_ladder[i], accept_k_ladder[i] + 1); + if (phase[NGRAM_PHASE_REASONING].forwards) { + accept_print_row("reasoning", &phase[NGRAM_PHASE_REASONING], accept_k_ladder[i]); + accept_print_row("answer", &phase[NGRAM_PHASE_ANSWER], accept_k_ladder[i]); + } + accept_print_row("combined", &all, accept_k_ladder[i]); + } +} + +/** Acceptance against how much history the drafter is allowed to search. */ +static void accept_report_windows(const ngram_stream *s, int max_k) +{ + printf("\n== HISTORY WINDOW at K=%d — does MORE context help?\n", max_k); + for (int i = 0; i < ACCEPT_WINDOW_COUNT; i++) { + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + char label[32]; + + accept_cell(s, max_k, accept_window_ladder[i], NGRAM_CONTROL_NONE, phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + if (accept_window_ladder[i]) + snprintf(label, sizeof(label), "window %d", accept_window_ladder[i]); + else + snprintf(label, sizeof(label), "window all"); + accept_print_row(label, &all, max_k); + } +} + +/** + * Per-document tokens/forward, so a corpus mean cannot hide a bimodal split. + * + * A single blended figure over eight documents is exactly the shape that hid the reasoning + * / answer bimodality the phase note warned about; the same argument applies one level down. + */ +static void accept_report_per_document(const ngram_stream *s, int max_k) +{ + printf("\n== PER DOCUMENT at K=%d — is the mean a mean, or one document?\n", max_k); + for (int d = 0; d < s->n_docs; d++) { + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + + memset(phase, 0, sizeof(phase)); + ngram_accept_replay(&s->docs[d], max_k, ACCEPT_NGRAM_MAX, ACCEPT_NGRAM_MIN, 0, + NGRAM_CONTROL_NONE, s->n_vocab, ACCEPT_CONTROL_SEED, phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + printf(" doc %d %5d prompt %5d gen tok/fwd=%.3f accept=%.1f%% reasoning=%.3f " + "answer=%.3f%s\n", d, s->docs[d].n_prompt, s->docs[d].n_gen, + ngram_tokens_per_forward(&all), + all.drafted ? 100.0 * (double)all.accepted / (double)all.drafted : 0.0, + ngram_tokens_per_forward(&phase[NGRAM_PHASE_REASONING]), + ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]), + s->docs[d].think_end_pos < 0 + ? " [NO : the whole run is reasoning, counted as answer]" : ""); + } +} + +/** Acceptance against the context length the round actually ran at. */ +static void accept_report_length(const ngram_stream *s, int max_k) +{ + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + + accept_cell(s, max_k, 0, NGRAM_CONTROL_NONE, phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + printf("\n== CONTEXT LENGTH at K=%d — tok/fwd by history length at round start\n", max_k); + for (int b = 0; b < NGRAM_LEN_BUCKETS; b++) { + if (!all.bucket_forwards[b]) + continue; + printf(" %-8s tok/fwd=%.3f (%ld forwards, %ld tokens)\n", ngram_length_bucket_name[b], + (double)all.bucket_emitted[b] / (double)all.bucket_forwards[b], + all.bucket_forwards[b], all.bucket_emitted[b]); + } +} + +/** + * The drafter's KEY LENGTH, swept. + * + * Free to vary offline and it is a real lever: a short key hits more often and proposes worse, + * a long key the reverse. Reporting only the shipped 3..2 would leave the phase's verdict + * resting on a parameter nobody had varied. + */ +static void accept_report_ngram(const ngram_stream *s, int max_k) +{ + static const int ng[][2] = { {2, 1}, {2, 2}, {3, 2}, {4, 2}, {6, 3}, {8, 4} }; + char label[32]; + + printf("\n== N-GRAM KEY LENGTH at K=%d\n", max_k); + for (int i = 0; i < (int)(sizeof(ng) / sizeof(ng[0])); i++) { + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + + accept_cell_ng(s, max_k, 0, NGRAM_CONTROL_NONE, ng[i][0], ng[i][1], phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + snprintf(label, sizeof(label), "ngram %d..%d", ng[i][0], ng[i][1]); + accept_print_row(label, &all, max_k); + } +} + +/** The two controls that must read LOW on the same bytes as the measurement. */ +static void accept_report_controls(const ngram_stream *s, int max_k) +{ + static const ngram_control ladder[] = { NGRAM_CONTROL_NONE, NGRAM_CONTROL_SHUFFLE, + NGRAM_CONTROL_RANDOM }; + static const char *const name[] = { "measurement", "CTRL shuffle", "CTRL random" }; + + printf("\n== CONTROLS at K=%d — the two below must collapse to ~1.00 tok/fwd\n", max_k); + for (int i = 0; i < 3; i++) { + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + + accept_cell(s, max_k, 0, ladder[i], phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + accept_print_row(name[i], &all, max_k); + } +} + +/** One line of the cross-corpus summary — the table the verdict is read off. */ +typedef struct { + char tag[NGRAM_STREAM_NAME_LEN]; + int docs; + long gen_tokens; + double control_tok_s; + double tpf[3]; /**< tokens per forward at K = 2, 4, 8 */ + double accept_k4; /**< accepted / drafted at K=4, per cent */ + double shuffle_k4; /**< the control that must read ~1.00 */ + double random_k4; + long reasoning_forwards; + double tpf_reasoning_k4; + double tpf_answer_k4; +} accept_summary_row; + +/** tokens/forward at one K over both phases. */ +static double accept_tpf_at(const ngram_stream *s, int max_k, ngram_control control) +{ + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + + accept_cell(s, max_k, 0, control, phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + return ngram_tokens_per_forward(&all); +} + +/** Fill one summary row from a dump. */ +static void accept_fill_summary(const ngram_stream *s, accept_summary_row *row) +{ + ngram_accept_stats phase[NGRAM_PHASE_COUNT], all = {0}; + static const int k_of[3] = { 2, 4, 8 }; + + snprintf(row->tag, sizeof(row->tag), "%s", s->tag); + row->docs = s->n_docs; + for (int d = 0; d < s->n_docs; d++) { + row->gen_tokens += s->docs[d].n_gen; + row->control_tok_s += s->docs[d].decode_s; + } + row->control_tok_s = row->control_tok_s > 0.0 + ? (double)row->gen_tokens / row->control_tok_s : 0.0; + for (int i = 0; i < 3; i++) + row->tpf[i] = accept_tpf_at(s, k_of[i], NGRAM_CONTROL_NONE); + row->shuffle_k4 = accept_tpf_at(s, 4, NGRAM_CONTROL_SHUFFLE); + row->random_k4 = accept_tpf_at(s, 4, NGRAM_CONTROL_RANDOM); + accept_cell(s, 4, 0, NGRAM_CONTROL_NONE, phase); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_REASONING]); + ngram_accept_stats_add(&all, &phase[NGRAM_PHASE_ANSWER]); + row->accept_k4 = all.drafted ? 100.0 * (double)all.accepted / (double)all.drafted : 0.0; + row->reasoning_forwards = phase[NGRAM_PHASE_REASONING].forwards; + row->tpf_reasoning_k4 = ngram_tokens_per_forward(&phase[NGRAM_PHASE_REASONING]); + row->tpf_answer_k4 = ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]); +} + +/** + * The verdict table. + * + * `tokens per forward` is a CEILING on any speculative speedup, whatever the verify ends up + * costing: a verify that were free would still deliver exactly this many tokens per forward. + * The two control columns beside it are the same code over the same bytes with the structure + * removed, and they are printed on every run so nobody has to take the headline on trust. + */ +static void accept_print_summary(const accept_summary_row *rows, int n) +{ + printf("\n================================================================\n"); + printf("== SUMMARY — TOKENS PER FORWARD (a CEILING on any speculative speedup)\n"); + printf("%-10s %4s %8s %9s | %6s %6s %6s | %8s %8s %8s | %8s %8s\n", "corpus", "docs", + "gen_tok", "ctl tok/s", "K=2", "K=4", "K=8", "acc@K4", "shuffle", "random", + "K4 reas", "K4 answ"); + for (int i = 0; i < n; i++) + printf("%-10s %4d %8ld %9.3f | %6.3f %6.3f %6.3f | %7.1f%% %8.3f %8.3f | %8.3f %8.3f\n", + rows[i].tag, rows[i].docs, rows[i].gen_tokens, rows[i].control_tok_s, + rows[i].tpf[0], rows[i].tpf[1], rows[i].tpf[2], rows[i].accept_k4, + rows[i].shuffle_k4, rows[i].random_k4, + rows[i].reasoning_forwards ? rows[i].tpf_reasoning_k4 : 0.0, + rows[i].tpf_answer_k4); + printf("shuffle/random are the SAME code path with the history permuted and with the\n" + "proposals replaced by uniform ids. Both must sit at ~1.000 or no figure to their\n" + "left means anything.\n"); +} + +#define ACCEPT_MAX_DUMPS 16 + +static int accept_run_score(int n_paths, char **paths) +{ + accept_summary_row rows[ACCEPT_MAX_DUMPS]; + int n_rows = 0; + + memset(rows, 0, sizeof(rows)); + + for (int i = 0; i < n_paths; i++) { + ngram_stream s; + double gen_tokens = 0.0, gen_seconds = 0.0; + + if (ngram_stream_read(paths[i], &s) != 0) + return 1; + for (int d = 0; d < s.n_docs; d++) { + gen_tokens += s.docs[d].n_gen; + gen_seconds += s.docs[d].decode_s; + } + printf("\n================================================================\n"); + printf("DUMP %s tag=%s arm=%s arch=%u n_vocab=%u docs=%d\n", paths[i], s.tag, s.arm, + s.arch, s.n_vocab, s.n_docs); + printf(" recorded plain greedy decode: %.0f tokens in %.1f s = %.3f tok/s " + "(%.4f s/token) — TODAY'S CONTROL\n", gen_tokens, gen_seconds, + gen_tokens / gen_seconds, gen_seconds / gen_tokens); + for (int d = 0; d < s.n_docs; d++) + printf(" doc %d: %d prompt + %d generated, think_end_pos=%d\n", d, + s.docs[d].n_prompt, s.docs[d].n_gen, s.docs[d].think_end_pos); + accept_report_k_ladder(&s); + accept_report_windows(&s, 4); + accept_report_ngram(&s, 4); + accept_report_per_document(&s, 4); + accept_report_length(&s, 4); + accept_report_controls(&s, 4); + if (n_rows < ACCEPT_MAX_DUMPS) + accept_fill_summary(&s, &rows[n_rows++]); + ngram_stream_free(&s); + } + accept_print_summary(rows, n_rows); + return 0; +} + +/* ================================================================ + * Known-answer self-test — proves the counter reads BOTH ends + * ================================================================ */ + +static int accept_selftest(void) +{ + const int k = 4, period = 64, vocab = 248320, n_gen = 1024; + /* THE CEILING IS NOT k+1 EXACTLY. A recorded stream ends, so the last round delivers + * whatever is left: n_gen-1 tokens over ceil((n_gen-1)/(k+1)) rounds. Asserting the + * closed form rather than a tolerance is what makes this a known-answer arm — a + * simulator that mis-advanced by one token would miss it. */ + const long deliverable = n_gen - 1; + const long full_rounds = (deliverable + k) / (k + 1); + const double ceiling = (double)deliverable / (double)full_rounds; + ngram_document periodic, random_doc; + ngram_accept_stats phase[NGRAM_PHASE_COUNT]; + ngram_accept_stats ctrl[NGRAM_PHASE_COUNT]; + double hi, lo, hi_shuffled; + int failures = 0; + + ngram_selftest_periodic(&periodic, 1024, n_gen, period, (uint32_t)vocab); + ngram_selftest_random(&random_doc, 1024, n_gen, (uint32_t)vocab, 0xC0FFEEULL); + + memset(phase, 0, sizeof(phase)); + ngram_accept_replay(&periodic, k, 3, 2, 0, NGRAM_CONTROL_NONE, (uint32_t)vocab, 1, phase); + hi = ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]); + + memset(phase, 0, sizeof(phase)); + ngram_accept_replay(&random_doc, k, 3, 2, 0, NGRAM_CONTROL_NONE, (uint32_t)vocab, 1, phase); + lo = ngram_tokens_per_forward(&phase[NGRAM_PHASE_ANSWER]); + + memset(ctrl, 0, sizeof(ctrl)); + ngram_accept_replay(&periodic, k, 3, 2, 0, NGRAM_CONTROL_SHUFFLE, (uint32_t)vocab, 1, ctrl); + hi_shuffled = ngram_tokens_per_forward(&ctrl[NGRAM_PHASE_ANSWER]); + + printf("SELFTEST K=%d\n", k); + printf(" periodic (period %d, the CEILING) tok/fwd=%.4f expect %.4f\n", period, + hi, ceiling); + printf(" i.i.d. (vocab %d, the FLOOR) tok/fwd=%.4f expect 1.0000\n", vocab, lo); + printf(" periodic + shuffled history tok/fwd=%.4f expect ~1.0000\n", + hi_shuffled); + + if (hi < ceiling - 1e-4 || hi > ceiling + 1e-4) { + printf(" FAIL: the simulator missed the closed-form ceiling %.4f\n", ceiling); + failures++; + } + if (lo > 1.02) { + printf(" FAIL: an i.i.d. stream must not accept drafts\n"); + failures++; + } + if (hi_shuffled > 1.05) { + printf(" FAIL: the shuffle control did not destroy the structure\n"); + failures++; + } + ngram_selftest_free(&periodic); + ngram_selftest_free(&random_doc); + printf("SELFTEST %s (%d failure(s))\n", failures ? "FAILED" : "PASSED", failures); + return failures ? 1 : 0; +} + +/** + * Print what a corpus file parses to, without a model or a card. + * + * A corpus is the one input this measurement cannot check any other way: a separator typo + * silently merges two prompts into one, which changes the very repetition the drafter is + * being scored on. Cheap to look at, so look at it. + */ +static int accept_corpus_check(const char *path) +{ + char *prompts[ACCEPT_MAX_PROMPTS]; + const char *system = NULL; + char *buffer = NULL; + int n = accept_load_corpus(path, &buffer, &system, prompts, ACCEPT_MAX_PROMPTS); + + if (n <= 0) { + fprintf(stderr, "corpus-check: %s parsed to %d prompts\n", path, n); + free(buffer); + return 1; + } + printf("CORPUS %s: %d prompt(s), system=%s\n", path, n, system ? "present" : "none"); + if (system) + printf(" SYSTEM (%zu B): %.100s\n", strlen(system), system); + for (int i = 0; i < n; i++) + printf(" prompt %d: %7zu B | first line: %.90s\n", i, strlen(prompts[i]), + prompts[i]); + free(buffer); + return 0; +} + +/* ================================================================ + * Entry + * ================================================================ */ + +static void accept_usage(const char *argv0) +{ + fprintf(stderr, + "usage:\n" + " %s --generate OUT.bin MODEL.gguf --corpus F --tag NAME [--n-gen N] " + "[--max-docs N]\n" + " %s --score DUMP.bin [DUMP.bin ...]\n" + " %s --selftest\n" + " %s --corpus-check CORPUS.txt\n", + argv0, argv0, argv0, argv0); +} + +int main(int argc, char **argv) +{ + const char *dump = NULL, *model = NULL, *corpus = NULL, *tag = "untagged"; + const char *system = NULL; + int n_gen = 512, max_docs = ACCEPT_MAX_PROMPTS; + + if (argc < 2) { + accept_usage(argv[0]); + return 1; + } + if (strcmp(argv[1], "--selftest") == 0) + return accept_selftest(); + if (strcmp(argv[1], "--corpus-check") == 0 && argc > 2) + return accept_corpus_check(argv[2]); + if (strcmp(argv[1], "--score") == 0) { + if (argc < 3) { + accept_usage(argv[0]); + return 1; + } + return accept_run_score(argc - 2, argv + 2); + } + if (strcmp(argv[1], "--generate") != 0 || argc < 4) { + accept_usage(argv[0]); + return 1; + } + dump = argv[2]; + model = argv[3]; + for (int i = 4; i < argc; i++) { + if (strcmp(argv[i], "--corpus") == 0 && i + 1 < argc) + corpus = argv[++i]; + else if (strcmp(argv[i], "--tag") == 0 && i + 1 < argc) + tag = argv[++i]; + else if (strcmp(argv[i], "--n-gen") == 0 && i + 1 < argc) + n_gen = atoi(argv[++i]); + else if (strcmp(argv[i], "--max-docs") == 0 && i + 1 < argc) + max_docs = atoi(argv[++i]); + else if (strcmp(argv[i], "--system") == 0 && i + 1 < argc) + system = argv[++i]; + else { + fprintf(stderr, "unknown argument %s\n", argv[i]); + return 1; + } + } + if (!corpus) { + accept_usage(argv[0]); + return 1; + } + if (n_gen < 8) + n_gen = 8; + if (n_gen > ACCEPT_MAX_GEN) + n_gen = ACCEPT_MAX_GEN; + if (max_docs < 1 || max_docs > ACCEPT_MAX_PROMPTS) + max_docs = ACCEPT_MAX_PROMPTS; + return accept_run_generate(dump, model, corpus, tag, n_gen, max_docs, system); +} diff --git a/tests/bench_sampler_temp_delta.c b/tests/bench_sampler_temp_delta.c new file mode 100644 index 0000000..7efb532 --- /dev/null +++ b/tests/bench_sampler_temp_delta.c @@ -0,0 +1,161 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bench_sampler_temp_delta.c + * @brief What does temperature actually cost the HOST, per token? + * + * @status Complete. CPU-only: no GPU, no model, no serve. + * @issues None + * @todo None + * + * The serving question this answers: production runs temperature 0.7 and + * measures fewer tok/s than temperature 0. Part of that gap is the host + * sampler — temp 0 returns after ONE argmax scan, temp 0.7 runs the full + * softmax / top-k / top-p / draw pipeline. This times the REAL sampler + * (pn_sample_next_token, linked from the engine, not a copy) on the REAL + * shape the serve hands it: an n_vocab buffer that is -INFINITY everywhere + * except the k survivors the device top-k wrote (gpu_present_topk). + * + * Both arms restore the buffer from the same pristine snapshot inside the + * timed region, so the memcpy cancels exactly in the temp0.7 - temp0 delta. + * That delta is the ONLY per-token cost that temperature adds on the host, + * and therefore the entire budget any "move sampling off the host" lever can + * ever recover. Anything the serve loses beyond it is somewhere else. + * + * Since the sparse fast path also covers greedy, the temp-0 arm is measured + * BOTH ways: the full-vocab argmax and the survivor argmax. + * + * Usage: bench_sampler_temp_delta [n_vocab] [k] [iters] + * defaults 248320 (nex-n2-mini) / 40 (serve top_k) / 2000 + */ +#include "../src/core/engine.h" + +#include +#include +#include +#include +#include +#include + +static double now_ms(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (double)t.tv_sec * 1e3 + (double)t.tv_nsec * 1e-6; +} + +/* The sparse contract gpu_present_topk leaves behind: -inf everywhere, k + * finite survivors at scattered ids, values descending (iterative argmax). */ +static void build_sparse(float *snap, int n_vocab, int k, unsigned seed) +{ + for (int i = 0; i < n_vocab; i++) snap[i] = -INFINITY; + uint64_t s = seed ? seed : 1; + for (int i = 0; i < k; i++) { + s = s * 6364136223846793005ULL + 1442695040888963407ULL; + int id = (int)((s >> 33) % (uint64_t)n_vocab); + snap[id] = 8.0f - 0.15f * (float)i; + } +} + +/* One timed arm: iters * (restore from snapshot + one real sampler call). */ +static double time_arm(float *buf, const float *snap, int n_vocab, + pn_sampling_params *p, uint32_t *recent, int n_recent, + int iters, int sparse, uint32_t *sink) +{ + size_t bytes = (size_t)n_vocab * sizeof(float); + uint32_t acc = 0; + pn_sampler_set_sparse(sparse); + double t0 = now_ms(); + for (int it = 0; it < iters; it++) { + memcpy(buf, snap, bytes); + acc += pn_sample_next_token(buf, n_vocab, p, recent, n_recent); + } + double dt = now_ms() - t0; + pn_sampler_clear_sparse(); + *sink = acc; + return dt / (double)iters; +} + +int main(int argc, char **argv) +{ + int n_vocab = argc > 1 ? atoi(argv[1]) : 248320; + int k = argc > 2 ? atoi(argv[2]) : 40; + int iters = argc > 3 ? atoi(argv[3]) : 2000; + if (n_vocab <= 0 || k <= 0 || k > n_vocab || iters <= 0) { + fprintf(stderr, "usage: %s [n_vocab] [k] [iters]\n", argv[0]); + return 2; + } + + float *snap = malloc((size_t)n_vocab * sizeof(float)); + float *buf = malloc((size_t)n_vocab * sizeof(float)); + if (!snap || !buf) { fprintf(stderr, "oom\n"); free(snap); free(buf); return 1; } + build_sparse(snap, n_vocab, k, 1234); + + /* The serve's real defaults (pn_sampling_defaults), temperature overridden + * per arm. repeat_penalty 1.1 over a 64-token window is ON in production, + * so the window is populated for both arms. */ + uint32_t recent[64]; + for (int i = 0; i < 64; i++) recent[i] = (uint32_t)(i * 977 % n_vocab); + + pn_sampling_params greedy = pn_sampling_defaults(); + greedy.temperature = 0.0f; + pn_sampling_params sampled = pn_sampling_defaults(); + sampled.temperature = 0.7f; + + /* Warm the pages and the engine's thread-local sampler scratch so the + * first-call allocation is not charged to any arm. */ + uint32_t sink = 0; + time_arm(buf, snap, n_vocab, &greedy, recent, 64, 32, 0, &sink); + time_arm(buf, snap, n_vocab, &sampled, recent, 64, 32, 0, &sink); + time_arm(buf, snap, n_vocab, &sampled, recent, 64, 32, 1, &sink); + + time_arm(buf, snap, n_vocab, &greedy, recent, 64, 32, 1, &sink); + + double t_greedy = time_arm(buf, snap, n_vocab, &greedy, recent, 64, iters, 0, &sink); + double t_sampled = time_arm(buf, snap, n_vocab, &sampled, recent, 64, iters, 0, &sink); + unsigned long h0 = pn_sampler_sparse_hits(); + double t_sparse = time_arm(buf, snap, n_vocab, &sampled, recent, 64, iters, 1, &sink); + unsigned long hits = pn_sampler_sparse_hits() - h0; + unsigned long hg0 = pn_sampler_sparse_hits(); + double t_gsparse = time_arm(buf, snap, n_vocab, &greedy, recent, 64, iters, 1, &sink); + unsigned long hits_greedy = pn_sampler_sparse_hits() - hg0; + + /* Restore-only arm: what the shared memcpy costs, so the reader can see + * how much of each absolute number is bench overhead rather than sampler. */ + size_t bytes = (size_t)n_vocab * sizeof(float); + double t0 = now_ms(); + for (int it = 0; it < iters; it++) memcpy(buf, snap, bytes); + double t_restore = (now_ms() - t0) / (double)iters; + + printf("n_vocab=%d k=%d iters=%d (sink=%u)\n", n_vocab, k, iters, sink); + printf(" restore only (every arm pays this) : %.4f ms\n", t_restore); + printf(" temp 0.0 full-vocab argmax : %.4f ms\n", t_greedy); + printf(" temp 0.7 full-vocab sampler : %.4f ms\n", t_sampled); + printf(" temp 0.7 sparse-survivor sampler : %.4f ms (%lu fast-path hits)\n", + t_sparse, hits); + printf(" temp 0.0 sparse-survivor argmax : %.4f ms (%lu fast-path hits)\n", + t_gsparse, hits_greedy); + if (hits != (unsigned long)iters || hits_greedy != (unsigned long)iters) { + printf(" !! the fast path did not run on every call — this number is a lie\n"); + free(snap); free(buf); + return 1; + } + printf("\n"); + printf(" temperature costs the host TODAY : %.4f ms/token\n", + t_sampled - t_greedy); + printf(" the sampler fast path recovers : %.4f ms/token\n", + t_sampled - t_sparse); + printf(" the greedy fast path recovers : %.4f ms/token\n", + t_greedy - t_gsparse); + printf(" residual vs greedy after the fix : %.4f ms/token\n", + t_sparse - t_greedy); + printf("\n"); + printf(" If a step costs S ms, recovering R ms/token takes tok/s from\n"); + printf(" 1000/S to 1000/(S-R). At S=52.63 (19.0 tok/s):\n"); + printf(" temp 0.7 arm: %.2f tok/s temp 0.0 arm: %.2f tok/s\n", + 1000.0 / (52.63 - (t_sampled - t_sparse)), + 1000.0 / (52.63 - (t_greedy - t_gsparse))); + + free(snap); free(buf); + return 0; +} diff --git a/tests/bench_serve_prefix_cache.c b/tests/bench_serve_prefix_cache.c new file mode 100644 index 0000000..5f0e6b1 --- /dev/null +++ b/tests/bench_serve_prefix_cache.c @@ -0,0 +1,226 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* True per-user capacity bench — REALISTIC steady-state with prefix caching ON. + * + * The old synthetic bench cold-prefilled a fixed ~100-token prompt every step. + * This measures what the campaign is actually about: a SHARED/GROWING context + * per user (a fixed system prompt + a context that grows each turn) served + * through the real batched path with prefix caching ON, so per-turn prefill is + * a cheap DELTA against the session's warm slot (delta-prefill, warm KV+SSM) — + * i.e. steady-state per-user speed, not cold re-prefill. + * + * K users run concurrently through the batch scheduler, each with its own + * session_id, each doing WARM_TURNS growing turns. Turn 0 is a cold prefill + * (untimed warm-up); turns 1..WARM_TURNS-1 are timed steady-state (delta + * prefill + decode). Reports per-user tok/s (total decoded tokens / wall time, + * averaged over the K concurrent users) and aggregate tok/s (K users' summed + * decode throughput over the wall clock). + * + * Build (CUDA host): make USE_CUDA=1 tests/bench_serve_prefix_cache + * Run: QWEN35_BATCH_SLOTS=1 QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=0 \ + * QWEN35_GPU_DIE_B=1 QWEN35_FUSE=0x37 \ + * ./tests/bench_serve_prefix_cache [gen_per_turn] [warm_turns] + */ +#include "../include/pantheon.h" +#include "../src/core/engine.h" +#include "../src/serve/batch_sched.h" +#include "../src/serve/serve_generation.h" +#include +#include +#include +#include +#include + +#ifndef USE_CUDA +int main(void) { fprintf(stderr, "built without USE_CUDA\n"); return 1; } +#else + +#define MAX_K 8 +#define MAX_TURNS 8 +#define SYS_PROMPT "You are a concise, helpful assistant answering follow-up questions in a running conversation." + +static double now_ms(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec * 1e3 + (double)ts.tv_nsec / 1e6; +} + +/* A generic follow-up that keeps the context growing turn over turn. */ +static const char *turn_line(int t) +{ + static const char *lines[MAX_TURNS] = { + "Explain what a hash table is in two sentences.", + "Now give one real-world use for it.", + "What is its average lookup complexity?", + "And the worst case?", + "How does chaining differ from open addressing?", + "Which is more cache-friendly?", + "Summarize the trade-off in one line.", + "Give a one-word verdict.", + }; + return lines[t % MAX_TURNS]; +} + +typedef struct { + pn_model *model; + pn_batch_sched *sched; + int user; + int gen_per_turn; + int warm_turns; /* total turns; turn 0 untimed warm-up */ + char sid[32]; + /* results */ + int timed_tokens; /* decode tokens over timed turns */ + double timed_ms; /* wall time over timed turns (this user) */ + int rc; +} bctx; + +/* Growing conversation held as an sg_message array (system + user/assistant + * pairs). */ +typedef struct { + sg_message msgs[1 + 2 * MAX_TURNS]; + int n; + char *owned[2 * MAX_TURNS]; + int n_owned; +} convo; + +static void convo_init(convo *c) +{ + memset(c, 0, sizeof(*c)); + c->msgs[0].role = "system"; + c->msgs[0].content = SYS_PROMPT; + c->n = 1; +} +static void convo_user(convo *c, const char *line) +{ + c->msgs[c->n].role = "user"; c->msgs[c->n].content = line; c->n++; +} +static void convo_asst(convo *c, const char *reply) +{ + char *cp = strdup(reply ? reply : ""); + c->owned[c->n_owned++] = cp; + c->msgs[c->n].role = "assistant"; c->msgs[c->n].content = cp; c->n++; +} +static void convo_free(convo *c) +{ + for (int i = 0; i < c->n_owned; i++) free(c->owned[i]); +} + +static void *worker(void *arg) +{ + bctx *b = arg; + convo c; + convo_init(&c); + sg_gen_params gp; + memset(&gp, 0, sizeof(gp)); + gp.temperature = 0.0f; gp.top_p = 1.0f; gp.top_k = 0; + gp.max_tokens = b->gen_per_turn; + + b->timed_tokens = 0; + b->timed_ms = 0.0; + + for (int t = 0; t < b->warm_turns; t++) { + convo_user(&c, turn_line(t)); + sg_result res; + memset(&res, 0, sizeof(res)); + int slot = bs_slot_acquire_biased(b->sched, b->sid); + if (slot < 0) { b->rc = -100; convo_free(&c); return NULL; } + double t0 = now_ms(); + int rc = sg_run_completion_batched(b->model, b->sched, slot, c.msgs, + c.n, &gp, NULL, NULL, &res, b->sid); + double dt = now_ms() - t0; + if (rc != 0) { b->rc = rc; sg_result_free(&res); convo_free(&c); return NULL; } + /* Turn 0 is the cold prefill warm-up — not timed. Turns 1.. are the + * steady-state warm-cache turns the bench reports. */ + if (t > 0) { + b->timed_tokens += res.completion_tokens; + b->timed_ms += dt; + } + convo_asst(&c, res.content); + sg_result_free(&res); + } + convo_free(&c); + b->rc = 0; + return NULL; +} + +int main(int argc, char **argv) +{ + if (argc < 3) { + fprintf(stderr, "usage: %s [gen_per_turn] [warm_turns]\n", + argv[0]); + return 1; + } + pn_model *model = pn_load(argv[1]); + if (!model) { fprintf(stderr, "load failed\n"); return 1; } + + int K = atoi(argv[2]); + if (K < 1) K = 1; + if (K > MAX_K) K = MAX_K; + int gen_per_turn = argc > 3 ? atoi(argv[3]) : 64; + if (gen_per_turn < 4) gen_per_turn = 4; + int warm_turns = argc > 4 ? atoi(argv[4]) : 4; + if (warm_turns < 2) warm_turns = 2; + if (warm_turns > MAX_TURNS) warm_turns = MAX_TURNS; + + pn_batch_sched *sched = bs_create(model, K < MAX_K ? K : MAX_K); + if (!sched) { fprintf(stderr, "bs_create failed\n"); return 1; } + + fprintf(stderr, "bench: K=%d gen/turn=%d warm_turns=%d (turn0 untimed) " + "driver_sample=%d\n", K, gen_per_turn, warm_turns, + bs_driver_sampling(sched) ? 1 : 0); + + pthread_t th[MAX_K]; + bctx b[MAX_K]; + memset(b, 0, sizeof(b)); + double wall0 = now_ms(); + for (int u = 0; u < K; u++) { + b[u].model = model; b[u].sched = sched; b[u].user = u; + b[u].gen_per_turn = gen_per_turn; b[u].warm_turns = warm_turns; + snprintf(b[u].sid, sizeof(b[u].sid), "bench-%d", u); + pthread_create(&th[u], NULL, worker, &b[u]); + } + for (int u = 0; u < K; u++) pthread_join(th[u], NULL); + double wall_ms = now_ms() - wall0; + + int total_tokens = 0; + double sum_per_user_tps = 0.0; + int ok = 1; + for (int u = 0; u < K; u++) { + if (b[u].rc != 0) { fprintf(stderr, "user %d rc=%d\n", u, b[u].rc); ok = 0; } + double u_tps = b[u].timed_ms > 0 ? b[u].timed_tokens * 1000.0 / b[u].timed_ms : 0.0; + sum_per_user_tps += u_tps; + total_tokens += b[u].timed_tokens; + fprintf(stderr, " user %d: %d timed tok, %.0f ms, %.2f tok/s\n", + u, b[u].timed_tokens, b[u].timed_ms, u_tps); + } + if (!ok) { bs_destroy(sched); return 1; } + + /* Per-user tok/s = mean of the K concurrent users' steady-state rates. + * Aggregate tok/s = all timed decode tokens over the concurrent wall time + * of the timed turns. Wall time here spans all turns incl. turn 0; for a + * clean aggregate use the summed per-user rate (the users run concurrently + * so their steady-state rates sum), which is the standard batched-agg + * definition and matches the T4 A/B table's "agg tok/s". */ + double per_user_tps = sum_per_user_tps / K; + double agg_tps = sum_per_user_tps; /* K concurrent users' summed rate */ + + /* Cost /1M tok: per-card active power 0.33 kW, rate $0.12491/kWh. + * $/1M = (1e6 / agg_tps) s / 3600 * 0.33 kW * $0.12491. */ + double cost_per_1m = agg_tps > 0 + ? (1.0e6 / agg_tps) / 3600.0 * 0.33 * 0.12491 : 0.0; + + fprintf(stderr, "\n=== K=%d per-user %.2f tok/s aggregate %.2f tok/s " + "cost/1M $%.4f (wall %.0f ms, %d timed tok) ===\n", + K, per_user_tps, agg_tps, cost_per_1m, wall_ms, total_tokens); + /* Machine-parseable line for the fill script. */ + printf("PCBENCH K=%d per_user=%.2f agg=%.2f cost1m=%.4f\n", + K, per_user_tps, agg_tps, cost_per_1m); + + bs_destroy(sched); + fflush(stdout); fflush(stderr); + if (!getenv("QWEN35_EXIT_CLEAN")) + _Exit(0); + return 0; +} +#endif diff --git a/tests/bench_spec_accept.c b/tests/bench_spec_accept.c new file mode 100644 index 0000000..99da2bb --- /dev/null +++ b/tests/bench_spec_accept.c @@ -0,0 +1,386 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* Phase 9.2: how often does a prompt-lookup draft actually LAND? + * + * Phase 9.1 measured what a D-row verify COSTS and derived a break-even + * acceptance from it. It never measured acceptance on real traffic. This + * does, on the workload the reopen condition names: nex-n2-mini doing + * code edits, where the answer re-emits the file that is sitting in the + * prompt. (Run it on saber too — the orchestrator's novel reasoning prose + * is the unfavourable case and belongs in the record.) + * + * Per prompt it decodes the SAME continuation twice: + * 1. plain greedy, single-token forwards — the reference stream + * 2. speculative greedy: pn_ngram_draft over (prompt + committed output), + * one batched pn_qwen35moe_spec_verify per round, pn_qwen35moe_spec_accept + * The two streams must be IDENTICAL — speculation is exactness-preserving or + * it is a bug, so that comparison is a gate, not a statistic. + * + * There is no throttle here on purpose: it drafts every round it can, so the + * reported distribution is the raw behaviour of the traffic rather than the + * behaviour of one throttle policy. A round that drafts k tokens verifies + * D = k+1 rows and delivers 1..D tokens (the pending token's own argmax is + * always correct and always free). + * + * Reasoning vs answer are counted SEPARATELY. A thinking model spends its + * first few hundred tokens on novel prose and only then re-emits the file; + * a single mean over both hides exactly the bimodality that decides this. + * + * Build (CUDA host): make USE_CUDA=1 tests/bench_spec_accept + * Run: QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=6 QWEN35_GPU_DIE_B=7 \ + * QWEN35_KV_Q4=1 QWEN35_MAX_SEQ=8192 QWEN35_FUSE=0x37 \ + * ./tests/bench_spec_accept [n_gen] + * prompts.txt: user messages separated by a line reading ===PROMPT=== + * Env: QWEN35_SPEC_K (max drafts per round, default 3 -> D=4 rows) + * QWEN35_SPEC_NG_MAX (3) / QWEN35_SPEC_NG_MIN (2) n-gram match window + * QWEN35_ACC_SYSTEM system prompt (default: a short coding-agent one) + * QWEN35_ACC_NOPLAIN=1 skip the reference stream (no exactness gate) + * NOT part of the test suite (needs the model + a CUDA GPU). + */ +#include "../include/pantheon.h" +#include "../src/core/engine.h" +#include "../src/core/chat.h" +#include "../src/core/ngram_draft.h" +#include "../src/models/qwen35moe.h" +#include "tensor/tensor.h" +#include +#include +#include +#include + +#ifndef USE_CUDA +int main(void) { fprintf(stderr, "built without USE_CUDA\n"); return 1; } +#else + +#define ACC_MAX_PROMPTS 32 +#define ACC_MAX_GEN 4096 +#define ACC_MAX_K 32 +#define ACC_MAX_HIST (65536) + +/* Accepted-per-round histogram: index = tokens DELIVERED by the round + * (1..D), so bucket 1 is "the draft was entirely wrong". */ +typedef struct { + long rounds; /* rounds that drafted at least one token */ + long plain_rounds; /* rounds with no draft -> plain forward */ + long drafted; /* draft tokens proposed */ + long accepted; /* draft tokens accepted */ + long delivered; /* tokens emitted by drafting rounds */ + long forwards; /* verify passes + plain forwards */ + long emitted; /* tokens emitted overall */ + long hist[ACC_MAX_K + 2]; /* delivered-per-drafting-round */ +} acc_stats; + +static void acc_add(acc_stats *a, const acc_stats *b) +{ + a->rounds += b->rounds; + a->plain_rounds += b->plain_rounds; + a->drafted += b->drafted; + a->accepted += b->accepted; + a->delivered += b->delivered; + a->forwards += b->forwards; + a->emitted += b->emitted; + for (int i = 0; i <= ACC_MAX_K + 1; i++) + a->hist[i] += b->hist[i]; +} + +static double now_s(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int argmax(const float *x, int n) +{ + int best = 0; + for (int i = 1; i < n; i++) + if (x[i] > x[best]) best = i; + return best; +} + +static int env_int(const char *name, int def) +{ + const char *v = getenv(name); + return v ? atoi(v) : def; +} + +/* Slurp `path`, split on lines reading exactly "===PROMPT===". Returns the + * prompt count; out[] entries are malloc'd (leaked deliberately — one-shot + * bench). */ +static int load_prompts(const char *path, char **out, int max_out) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + char *buf = malloc((size_t)len + 1); + if (!buf || fread(buf, 1, (size_t)len, f) != (size_t)len) { + fclose(f); + return 0; + } + buf[len] = '\0'; + fclose(f); + + int n = 0; + char *cur = buf; + while (cur && *cur && n < max_out) { + char *sep = strstr(cur, "\n===PROMPT===\n"); + if (sep) *sep = '\0'; + /* Skip a leading separator line so the file may start with one. */ + if (strncmp(cur, "===PROMPT===\n", 13) == 0) cur += 13; + if (*cur) out[n++] = cur; + cur = sep ? sep + 14 : NULL; + } + return n; +} + +/* Plain greedy decode of up to n_gen tokens from a fresh state. Writes the + * stream to gen[] and returns its length, or -1. */ +static int decode_plain(pn_model *model, const uint32_t *prefix, int n_prefix, + int n_gen, int eos, uint32_t *gen, double *secs) +{ + pn_state *st = pn_state_new(model); + if (!st) return -1; + tm_tensor *out = NULL; + for (int i = 0; i < n_prefix; i++) { + if (out) tm_free(out); + out = model->forward(model, prefix[i], st); + if (!out) { pn_state_free(st); return -1; } + } + double t0 = now_s(); + int n = 0; + uint32_t tok = (uint32_t)argmax(tm_data_f32(out), model->config.n_vocab); + tm_free(out); + gen[n++] = tok; + while (n < n_gen && (int)tok != eos) { + out = model->forward(model, tok, st); + if (!out) { pn_state_free(st); return -1; } + tok = (uint32_t)argmax(tm_data_f32(out), model->config.n_vocab); + tm_free(out); + gen[n++] = tok; + } + *secs = now_s() - t0; + pn_state_free(st); + return n; +} + +/* Speculative greedy decode of the same continuation, accumulating stats into + * pre[] (before the reasoning marker) and post[] (after it). Returns the + * stream length, or -1. */ +static int decode_spec(pn_model *model, const uint32_t *prefix, int n_prefix, + int n_gen, int eos, int think_end, int max_k, + int ng_max, int ng_min, uint32_t *gen, + acc_stats *pre, acc_stats *post, double *secs) +{ + static uint32_t hist[ACC_MAX_HIST]; + pn_state *st = pn_state_new(model); + if (!st) return -1; + int n_vocab = model->config.n_vocab; + + tm_tensor *out = NULL; + for (int i = 0; i < n_prefix; i++) { + if (out) tm_free(out); + out = model->forward(model, prefix[i], st); + if (!out) { pn_state_free(st); return -1; } + } + double t0 = now_s(); + uint32_t pending = (uint32_t)argmax(tm_data_f32(out), n_vocab); + tm_free(out); + + if (n_prefix > ACC_MAX_HIST - ACC_MAX_GEN) { pn_state_free(st); return -1; } + memcpy(hist, prefix, (size_t)n_prefix * sizeof(uint32_t)); + int nh = n_prefix; + int n = 0; + int reasoning = (think_end >= 0); /* no marker -> everything is "answer" */ + + gen[n++] = pending; + hist[nh++] = pending; + if (reasoning && (int)pending == think_end) reasoning = 0; + + while (n < n_gen && (int)gen[n - 1] != eos) { + acc_stats *bucket = reasoning ? pre : post; + uint32_t drafts[ACC_MAX_K], block[ACC_MAX_K + 1]; + int32_t am[ACC_MAX_K + 1]; + int k = pn_ngram_draft(hist, nh, ng_max, ng_min, max_k, drafts); + int stepped = 0; + + if (k > 0) { + block[0] = pending; + memcpy(block + 1, drafts, (size_t)k * sizeof(uint32_t)); + if (pn_qwen35moe_spec_verify(model, block, k + 1, st, am) == 0) { + int acc = 1; /* am[0] is always correct */ + while (acc <= k && drafts[acc - 1] == (uint32_t)am[acc - 1]) + acc++; + if (pn_qwen35moe_spec_accept(model, st, acc) != 0) { + pn_state_free(st); + return -1; + } + bucket->rounds++; + bucket->drafted += k; + bucket->accepted += acc - 1; + bucket->delivered += acc; + bucket->forwards++; + bucket->hist[acc]++; + for (int i = 0; i < acc && n < n_gen; i++) { + gen[n++] = (uint32_t)am[i]; + hist[nh++] = (uint32_t)am[i]; + bucket->emitted++; + if (reasoning && am[i] == think_end) reasoning = 0; + } + pending = (uint32_t)am[acc - 1]; + stepped = 1; + } + } + if (!stepped) { + out = model->forward(model, pending, st); + if (!out) { pn_state_free(st); return -1; } + pending = (uint32_t)argmax(tm_data_f32(out), n_vocab); + tm_free(out); + bucket->plain_rounds++; + bucket->forwards++; + bucket->emitted++; + gen[n++] = pending; + hist[nh++] = pending; + if (reasoning && (int)pending == think_end) reasoning = 0; + } + } + *secs = now_s() - t0; + pn_state_free(st); + return n; +} + +static void report(const char *label, const acc_stats *a, int max_k) +{ + long rounds = a->rounds + a->plain_rounds; + if (!rounds) { printf(" %-9s (no rounds)\n", label); return; } + printf(" %-9s emitted=%-5ld forwards=%-5ld tok/fwd=%.2f | " + "draft_rounds=%ld/%ld (%.0f%%) drafted=%ld accepted=%ld " + "accept=%.1f%% | delivered/round=%.2f\n", + label, a->emitted, a->forwards, + a->forwards ? (double)a->emitted / (double)a->forwards : 0.0, + a->rounds, rounds, 100.0 * (double)a->rounds / (double)rounds, + a->drafted, a->accepted, + a->drafted ? 100.0 * (double)a->accepted / (double)a->drafted : 0.0, + a->rounds ? (double)a->delivered / (double)a->rounds : 0.0); + printf(" %-9s delivered-per-drafting-round:", ""); + for (int i = 1; i <= max_k + 1; i++) + printf(" %d:%ld", i, a->hist[i]); + printf("\n"); +} + +int main(int argc, char **argv) +{ + if (argc < 3) { + fprintf(stderr, "usage: %s [n_gen]\n", + argv[0]); + return 1; + } + int n_gen = argc > 3 ? atoi(argv[3]) : 512; + if (n_gen < 8) n_gen = 8; + if (n_gen > ACC_MAX_GEN) n_gen = ACC_MAX_GEN; + int max_k = env_int("QWEN35_SPEC_K", 3); + if (max_k < 1) max_k = 1; + if (max_k > ACC_MAX_K) max_k = ACC_MAX_K; + int ng_max = env_int("QWEN35_SPEC_NG_MAX", 3); + int ng_min = env_int("QWEN35_SPEC_NG_MIN", 2); + int no_plain = env_int("QWEN35_ACC_NOPLAIN", 0); + + char *prompts[ACC_MAX_PROMPTS]; + int n_prompts = load_prompts(argv[2], prompts, ACC_MAX_PROMPTS); + if (!n_prompts) { fprintf(stderr, "no prompts in %s\n", argv[2]); return 1; } + + pn_model *model = pn_load(argv[1]); + if (!model) { fprintf(stderr, "load failed\n"); return 1; } + int eos = ft_eos_id(model->tokenizer); + int think_end = ft_token_to_id(model->tokenizer, ""); + const char *system = getenv("QWEN35_ACC_SYSTEM"); + if (!system) + system = "You are a coding agent. Apply the requested edit and " + "re-emit the complete file in a single fenced code block. " + "Do not explain."; + + printf("== spec acceptance: %s\n", argv[1]); + printf(" prompts=%d n_gen=%d D=%d rows (max_k=%d) ngram=%d..%d " + "eos=%d think_end=%d chat_style=%d\n", + n_prompts, n_gen, max_k + 1, max_k, ng_min, ng_max, eos, think_end, + model->chat_style); + + acc_stats tot_pre = { 0 }, tot_post = { 0 }; + int gates_run = 0, gates_pass = 0; + + for (int p = 0; p < n_prompts; p++) { + pn_token_seq seq = pn_chat_format_first((pn_chat_style)model->chat_style, + model->tokenizer, system, + prompts[p]); + if (!seq.tokens || seq.n_tokens <= 0) { + fprintf(stderr, "prompt %d: format failed\n", p); + continue; + } + static uint32_t gen_plain[ACC_MAX_GEN], gen_spec[ACC_MAX_GEN]; + double t_plain = 0.0, t_spec = 0.0; + int n_plain = 0; + + if (!no_plain) { + n_plain = decode_plain(model, seq.tokens, seq.n_tokens, n_gen, eos, + gen_plain, &t_plain); + if (n_plain < 0) { fprintf(stderr, "prompt %d: plain failed\n", p); + continue; } + } + + acc_stats pre = { 0 }, post = { 0 }; + int n_spec = decode_spec(model, seq.tokens, seq.n_tokens, n_gen, eos, + think_end, max_k, ng_max, ng_min, gen_spec, + &pre, &post, &t_spec); + if (n_spec < 0) { fprintf(stderr, "prompt %d: spec failed\n", p); + continue; } + + int diff = -1; + if (!no_plain) { + gates_run++; + if (n_plain != n_spec) { + diff = n_plain < n_spec ? n_plain : n_spec; + } else { + for (int i = 0; i < n_spec; i++) + if (gen_plain[i] != gen_spec[i]) { diff = i; break; } + } + if (diff < 0) gates_pass++; + } + + printf("\n-- prompt %d: %d prompt tokens, %d generated%s\n", p, + seq.n_tokens, n_spec, + no_plain ? "" : (diff < 0 ? ", EXACT vs plain" + : ", ***STREAM DIVERGED***")); + if (diff >= 0) + printf(" first_diff=%d plain=%u spec=%u (n_plain=%d n_spec=%d)\n", + diff, gen_plain[diff], gen_spec[diff], n_plain, n_spec); + report("reasoning", &pre, max_k); + report("answer", &post, max_k); + if (!no_plain && t_spec > 0.0) + printf(" wall: plain %.2fs (%.2f tok/s) | spec %.2fs " + "(%.2f tok/s) | %.2fx\n", + t_plain, n_plain / t_plain, t_spec, n_spec / t_spec, + t_plain / t_spec); + acc_add(&tot_pre, &pre); + acc_add(&tot_post, &post); + free(seq.tokens); + fflush(stdout); + } + + acc_stats all = { 0 }; + acc_add(&all, &tot_pre); + acc_add(&all, &tot_post); + printf("\n== TOTAL (D=%d rows)\n", max_k + 1); + report("reasoning", &tot_pre, max_k); + report("answer", &tot_post, max_k); + report("combined", &all, max_k); + if (gates_run) + printf("EXACTNESS GATE: %d/%d prompts bit-identical to plain greedy\n", + gates_pass, gates_run); + + fflush(stdout); + if (!getenv("QWEN35_EXIT_CLEAN")) + _Exit(gates_run && gates_pass != gates_run ? 1 : 0); + return 0; +} +#endif diff --git a/tests/bench_spec_verify_gate.c b/tests/bench_spec_verify_gate.c new file mode 100644 index 0000000..b2d5edf --- /dev/null +++ b/tests/bench_spec_verify_gate.c @@ -0,0 +1,319 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* Phase 9.1 gate bench: does verifying D rows in ONE forward cost materially + * less than D sequential single-token forwards? + * + * This is the decision gate for speculative decoding on Kepler-class hardware. + * Pure measurement: it changes no engine behaviour and is not part of the test + * suite (needs a model and a GPU). + * + * Three cost curves, all at the SAME context depth, on ONE model load: + * + * A. SEQUENTIAL single-token forward (`model->forward`) — the legacy + * single-user decode step. The thing speculation must beat D-for-D. + * B. SPECULATIVE VERIFY of D rows (`pn_qwen35moe_spec_verify` + + * `pn_qwen35moe_spec_accept`) — one forward carrying D candidate rows of + * ONE session. accept(1) is timed with it: that is the WORST-CASE commit + * (full SSM rollback); accept(D) is free by construction (the commit + * returns immediately when n_accept == n). + * C. BATCHED SLOTS step (`pn_qwen35moe_decode_slots`) at K slots — the + * serving path's cost as a function of rows-in-one-forward, which is what + * a K-user speculative verify would have to pay. + * + * The K=1 gate is B(D) vs D x A. The K=2 gate is C(2D) vs D x C(2): per round + * two users each advance up to D tokens in one 2D-row forward, versus D + * ordinary two-row steps. + * + * Build (CUDA host): make USE_CUDA=1 tests/bench_spec_verify_gate + * Run: QWEN35_GPU_SPLIT_LAYER=20 QWEN35_GPU_DIE_A=6 QWEN35_GPU_DIE_B=7 \ + * QWEN35_KV_Q4=1 QWEN35_MAX_SEQ=8192 QWEN35_FUSE=0x37 \ + * ./tests/bench_spec_verify_gate [depth] [reps] [chunk] + * Env: QWEN35_GATE_ROWS ("1,2,4,8") verify widths D to sweep + * QWEN35_GATE_SLOTS ("1,2,4,8") slot counts K to sweep (0 skips C) + */ +#include "../include/pantheon.h" +#include "../src/core/engine.h" +#include "../src/models/qwen35moe.h" +#include "tensor/tensor.h" +#include +#include +#include +#include + +#ifndef USE_CUDA +int main(void) { fprintf(stderr, "built without USE_CUDA\n"); return 1; } +#else + +#define GATE_MAX_POINTS 12 +#define GATE_MAX_SLOTS 16 +#define GATE_MAX_ROWS 64 + +static double now_s(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +static int argmax_f32(const float *x, int n) +{ + int best = 0; + for (int i = 1; i < n; i++) + if (x[i] > x[best]) best = i; + return best; +} + +/* Parse "1,2,4,8" into out[]; returns count (0 on empty/"0"). */ +static int parse_int_list(const char *spec, int *out, int max_out) +{ + int n = 0; + while (spec && *spec && n < max_out) { + char *end = NULL; + long v = strtol(spec, &end, 10); + if (end == spec) break; + if (v > 0) out[n++] = (int)v; + spec = (*end == ',') ? end + 1 : end; + } + return n; +} + +/* Deterministic, non-degenerate seed context so the KV/attention is realistic + * (not one repeated id). `user` keeps concurrent slots distinct. */ +static void fill_seed_context(uint32_t *buf, int depth, int n_vocab, int user) +{ + uint32_t base = 100u + (uint32_t)(user * 131); + for (int i = 0; i < depth; i++) { + uint32_t t = base + (uint32_t)((i * 7u + user * 13u) % 4096u); + buf[i] = t % (uint32_t)n_vocab; + } +} + +/* Chunked cold prefill of `depth` tokens; returns the last position's argmax + * (the first decode token) or -1. Untimed warm-up in every section. */ +static int prefill_to_depth(pn_model *model, pn_state *st, const uint32_t *seed, + int depth, int chunk, int n_vocab) +{ + int last = -1; + for (int off = 0; off < depth; off += chunk) { + int n = depth - off < chunk ? depth - off : chunk; + tm_tensor *out = + pn_qwen35moe_forward_batch_cuda(model, seed + off, n, st, NULL); + if (!out) return -1; + if (off + n >= depth) last = argmax_f32(tm_data_f32(out), n_vocab); + tm_free(out); + } + return last; +} + +/* --- A. sequential single-token forward -------------------------------- */ + +/* Times `reps` ordinary single-token forwards from `tok`. Advances st->pos by + * reps. Returns ms/forward, or -1 on failure. */ +static double measure_sequential_step_ms(pn_model *model, pn_state *st, + uint32_t tok, int reps, int n_vocab) +{ + tm_tensor *out = model->forward(model, tok, st); /* warm-up, untimed */ + if (!out) return -1.0; + tok = (uint32_t)argmax_f32(tm_data_f32(out), n_vocab); + tm_free(out); + double t0 = now_s(); + for (int i = 0; i < reps; i++) { + out = model->forward(model, tok, st); + if (!out) return -1.0; + tok = (uint32_t)argmax_f32(tm_data_f32(out), n_vocab); + tm_free(out); + } + return 1e3 * (now_s() - t0) / reps; +} + +/* --- B. D-row speculative verify ---------------------------------------- */ + +/* Times `reps` rounds of verify(D) + accept(1) at the state's current depth. + * accept(1) is the WORST-CASE commit (full SSM rollback from snapshot 0); it + * advances st->pos by exactly 1 per round, so the depth drifts by `reps`. + * Splits the round into *verify_ms and *commit_ms. Returns 0, or -1. */ +static int measure_verify_round_ms(pn_model *model, pn_state *st, int rows, + int reps, int n_vocab, + double *verify_ms, double *commit_ms) +{ + uint32_t block[GATE_MAX_ROWS]; + int32_t argmax_out[GATE_MAX_ROWS]; + double verify_total = 0.0, commit_total = 0.0; + for (int r = 0; r < rows; r++) + block[r] = (uint32_t)((1000 + r * 37) % n_vocab); + + if (pn_qwen35moe_spec_verify(model, block, rows, st, argmax_out) != 0) + return -1; /* warm-up, untimed */ + if (pn_qwen35moe_spec_accept(model, st, 1) != 0) + return -1; + for (int i = 0; i < reps; i++) { + double t0 = now_s(); + if (pn_qwen35moe_spec_verify(model, block, rows, st, argmax_out) != 0) + return -1; + double t1 = now_s(); + if (pn_qwen35moe_spec_accept(model, st, 1) != 0) + return -1; + double t2 = now_s(); + verify_total += t1 - t0; + commit_total += t2 - t1; + } + *verify_ms = 1e3 * verify_total / reps; + *commit_ms = 1e3 * commit_total / reps; + return 0; +} + +/* --- C. batched slots step ---------------------------------------------- */ + +/* Seeds K users to `depth`, binds them, and times `reps` decode_slots steps. + * Returns ms/step (one step = K rows in one forward), or -1 on failure. */ +static double measure_slots_step_ms(pn_model *model, int n_slots, int depth, + int reps, int chunk, int n_vocab) +{ + pn_state *states[GATE_MAX_SLOTS] = { 0 }; + uint32_t tokens[GATE_MAX_SLOTS]; + float *logits[GATE_MAX_SLOTS]; + double step_ms = -1.0; + uint32_t *seed = malloc((size_t)depth * sizeof(uint32_t)); + if (!seed) return -1.0; + + for (int u = 0; u < n_slots; u++) { + fill_seed_context(seed, depth, n_vocab, u); + states[u] = pn_state_new(model); + if (!states[u]) goto done; + int first = prefill_to_depth(model, states[u], seed, depth, chunk, + n_vocab); + if (first < 0) goto done; + if (pn_qwen35moe_decode_slot_bind(model, states[u], u, n_slots) != 0) + goto done; + tokens[u] = (uint32_t)first; + } + if (pn_qwen35moe_decode_slots(model, tokens, states, n_slots, logits) != 0) + goto done; /* warm-up, untimed */ + for (int u = 0; u < n_slots; u++) + tokens[u] = (uint32_t)argmax_f32(logits[u], n_vocab); + + double t0 = now_s(); + for (int i = 0; i < reps; i++) { + if (pn_qwen35moe_decode_slots(model, tokens, states, n_slots, + logits) != 0) + goto done; + for (int u = 0; u < n_slots; u++) + tokens[u] = (uint32_t)argmax_f32(logits[u], n_vocab); + } + step_ms = 1e3 * (now_s() - t0) / reps; + +done: + for (int u = 0; u < n_slots; u++) + if (states[u]) pn_state_free(states[u]); + free(seed); + return step_ms; +} + +/* --- driver -------------------------------------------------------------- */ + +/* Section A+B: sequential baseline then the verify sweep, on one session. */ +static int run_single_session_sections(pn_model *model, int depth, int reps, + int chunk, int n_vocab, + const int *rows, int n_rows, + double *seq_ms_out) +{ + uint32_t *seed = malloc((size_t)depth * sizeof(uint32_t)); + pn_state *st = pn_state_new(model); + int rc = -1; + if (!seed || !st) goto done; + fill_seed_context(seed, depth, n_vocab, 0); + int first = prefill_to_depth(model, st, seed, depth, chunk, n_vocab); + if (first < 0) { fprintf(stderr, "prefill failed (depth=%d)\n", depth); goto done; } + + double seq_ms = measure_sequential_step_ms(model, st, (uint32_t)first, + reps, n_vocab); + if (seq_ms < 0) { fprintf(stderr, "sequential forward failed\n"); goto done; } + *seq_ms_out = seq_ms; + printf("\n[A] sequential single-token forward depth~%d reps=%d\n", + depth, reps); + printf(" %.2f ms/token %.2f tok/s\n", seq_ms, 1e3 / seq_ms); + + printf("\n[B] speculative verify of D rows (one session, one forward)\n"); + printf(" D pos verify_ms commit_ms round_ms ms/row " + "round/(D*seq) breakeven_acc\n"); + for (int i = 0; i < n_rows; i++) { + int d = rows[i]; + if (d > GATE_MAX_ROWS) continue; + double verify_ms = 0, commit_ms = 0; + int pos_before = st->pos; + if (measure_verify_round_ms(model, st, d, reps, n_vocab, + &verify_ms, &commit_ms) != 0) { + printf(" %2d %5d FAILED\n", d, pos_before); + continue; + } + double round_ms = verify_ms + commit_ms; + printf(" %2d %5d %8.2f %8.2f %8.2f %7.2f %10.3f " + "%10.2f\n", + d, pos_before, verify_ms, commit_ms, round_ms, round_ms / d, + round_ms / (d * seq_ms), round_ms / seq_ms); + } + rc = 0; +done: + if (st) pn_state_free(st); + free(seed); + return rc; +} + +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, + "usage: %s [depth] [reps] [chunk]\n", argv[0]); + return 1; + } + int depth = argc > 2 ? atoi(argv[2]) : 1024; + int reps = argc > 3 ? atoi(argv[3]) : 20; + int chunk = argc > 4 ? atoi(argv[4]) : 256; + if (depth < 8) depth = 8; + if (reps < 1) reps = 1; + + int rows[GATE_MAX_POINTS], slots[GATE_MAX_POINTS]; + const char *rows_env = getenv("QWEN35_GATE_ROWS"); + const char *slots_env = getenv("QWEN35_GATE_SLOTS"); + int n_rows = parse_int_list(rows_env ? rows_env : "1,2,4,8", rows, + GATE_MAX_POINTS); + int n_slots = parse_int_list(slots_env ? slots_env : "1,2,4,8", slots, + GATE_MAX_POINTS); + + pn_model *model = pn_load(argv[1]); + if (!model) { fprintf(stderr, "load failed: %s\n", argv[1]); return 1; } + int n_vocab = model->config.n_vocab; + printf("phase 9.1 verify gate: %s\n", argv[1]); + printf(" n_vocab=%d n_layer=%d depth=%d reps=%d chunk=%d\n", + n_vocab, model->config.n_layer, depth, reps, chunk); + + double seq_ms = 0; + if (run_single_session_sections(model, depth, reps, chunk, n_vocab, + rows, n_rows, &seq_ms) != 0) { + pn_model_free(model); + return 1; + } + + if (n_slots > 0) { + printf("\n[C] batched slots step: K rows of K sessions in one forward\n"); + printf(" K step_ms ms/row step/(K*seq) agg_tok/s\n"); + for (int i = 0; i < n_slots; i++) { + int k = slots[i]; + if (k > GATE_MAX_SLOTS) continue; + double step_ms = measure_slots_step_ms(model, k, depth, reps, + chunk, n_vocab); + if (step_ms < 0) { printf(" %2d FAILED\n", k); continue; } + printf(" %2d %8.2f %7.2f %10.3f %9.2f\n", + k, step_ms, step_ms / k, step_ms / (k * seq_ms), + 1e3 * k / step_ms); + } + } + + printf("\ndone\n"); + fflush(stdout); + if (!getenv("QWEN35_EXIT_CLEAN")) + _Exit(0); + pn_model_free(model); + return 0; +} +#endif diff --git a/tests/bench_tt_prefill_quantum.c b/tests/bench_tt_prefill_quantum.c new file mode 100644 index 0000000..29e25c6 --- /dev/null +++ b/tests/bench_tt_prefill_quantum.c @@ -0,0 +1,435 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file bench_tt_prefill_quantum.c + * @brief THE THREE MEASUREMENTS PHASE 4 IS GATED ON, on Ornith's own strips. + * + * --------------------------------------------------------------------------- + * WHY THIS EXISTS BEFORE ANY PREFILL CODE + * --------------------------------------------------------------------------- + * A batched prefill for this model is not the llama one. Ornith is 24 Gated-Delta-Net layers + * and 8 attention layers, and BOTH mixers are single-position on the card by construction: + * the GDN op advances a carried state, and `pn_tt_rope` / `pn_tt_kv_append` / flash decode + * each take ONE position. So a 32-row pass can batch the norms, the projections, the SwiGLU + * and the residual adds, and it must then hand each of the 32 rows to a mixer that reads + * LOGICAL ROW 0. There is no on-card row gather, so the only way to reach row `r` is to bring + * the strip down and put row `r` back up as row 0. + * + * That makes the whole design turn on ONE number nobody here has measured: **what a + * WHOLE-TILE download costs against a row-0 download.** The row-0 reader moves 128 B a tile + * (two 64 B chunks); the batched reader moves 2048 B. If the link is transaction-bound the + * batched read is nearly free per token and a host-mediated row select is affordable; if it + * is byte-bound at the row-0 rate the design is dead before it is written, because Phase A + * spent this whole program removing exactly this traffic. + * + * Three things are timed, all on the strips the shipped forward already owns: + * + * 1. **TRANSFERS** — `pn_tt_activation_{upload,download}` (row 0) against + * `..._rows` (32 rows), at Ornith's four real widths. Reported as µs and as B/s in both + * directions, because this project has one measured figure for the card->host direction + * (~5 MB/s at 128 B a tile) and none at all for whole tiles. + * + * 2. **THE TILE QUANTUM, MEASURED RATHER THAN INHERITED** — the same projection with rows + * 1..31 ZERO and with rows 1..31 carrying real activations. The descriptor is identical + * either way, so the claim "32 positions cost what one position costs" is a claim about + * DATA DEPENDENCE, not about descriptors — and this card has already produced one + * 3.7x data-dependent timing (PHASE CONV, 1222.7 µs on real activations against 4571 on + * a degenerate stream). A tile quantum that holds for a `[K+1, N]` verify strip of + * near-duplicate rows need not hold for 32 unrelated prompt positions. + * + * 3. **THE SEQUENTIAL TERMS** — one GDN block (the strip bridge, six descriptors) and one + * on-card attention block, timed alone. These are the terms a 32-row pass CANNOT batch, + * so they set the ceiling of the whole item: 32 rows cost `flat + 32 * sequential`. + * + * --------------------------------------------------------------------------- + * PROVING IT CAN FAIL + * --------------------------------------------------------------------------- + * `--selftest` runs the transfer arms against a KNOWN answer: it uploads 32 distinct rows, + * reads them back with the batched reader and compares, and reads row 0 with the row-0 reader + * and compares that. A bench whose transfers do not move the right bytes is measuring the + * cost of being wrong. It also checks that `pn_tt_activation_upload_rows` re-zeroes rows + * `rows..31`, which is the strip invariant every Mt=1 op downstream depends on. + * + * @status Working + * @issues None + * @todo None + */ + +#include "../include/pantheon.h" +#include "../src/core/engine.h" + +#include +#include +#include +#include +#include + +#ifndef USE_TT +int main(void) +{ + fprintf(stderr, "bench_tt_prefill_quantum: built without USE_TT — nothing to measure.\n"); + return 77; +} +#else + +#include "../src/models/qwen35moe.h" +#include "../src/models/qwen35moe_tt_internal.h" + +#define BENCH_ROWS 32u + +static double bench_now(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +/** A deterministic stream. Distinct per (row, column) so a transfer that moves row 0 + * everywhere, or transposes, cannot pass the self-test. */ +static void bench_fill(float *values, uint32_t rows, uint32_t width, uint32_t salt) +{ + for (uint32_t r = 0; r < rows; r++) + for (uint32_t c = 0; c < width; c++) + values[(size_t)r * width + c] = + (float)((int)((r * 7919u + c * 104729u + salt) % 2003u) - 1000) * 0.0009765625f; +} + +/** Time `reps` calls of a transfer and print µs and B/s. `bytes` is the payload the link + * actually moved, which for the row-0 reader is 128 B a tile and NOT `width * 4`. */ +static void bench_report(const char *what, uint32_t width, uint32_t rows, double seconds, + int reps, double bytes_per_call) +{ + double per = seconds / (double)reps; + + printf("QUANTUM %-28s width %5u rows %2u %9.1f us/call %8.2f MB/s (%.0f B/call)\n", + what, width, rows, per * 1e6, bytes_per_call / per / 1e6, bytes_per_call); +} + +/** The card-side bytes a strip transfer moves: whole tiles for the batched pair, two 64-byte + * chunks a tile for the row-0 pair. bf16 strips are 2048 B a tile. */ +static double bench_bytes(uint32_t tiles, uint32_t rows, uint32_t tile_bytes) +{ + return rows > 1u ? (double)tiles * (double)tile_bytes : (double)tiles * 128.0; +} + +static int bench_transfers(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int reps) +{ + struct { const char *name; const pn_tt_activation *strip; uint32_t width; } arms[] = { + { "hidden (n_embd)", &g->card.hidden[0], (uint32_t)cfg->n_embd }, + { "ssm_qkv (conv_dim)", &g->card.ssm_qkv, g->card.ssm_qkv.tile_count * 32u }, + { "ssm_gated", &g->card.ssm_gated, g->card.ssm_gated.tile_count * 32u }, + { "ffn_swiglu (n_ff)", &g->card.ffn_swiglu, g->card.ffn_swiglu.tile_count * 32u }, + }; + size_t widest = 0; + float *host; + + for (size_t i = 0; i < sizeof(arms) / sizeof(arms[0]); i++) + if (arms[i].width > widest) + widest = arms[i].width; + host = calloc((size_t)BENCH_ROWS * widest, sizeof(float)); + if (!host) + return -1; + + for (size_t i = 0; i < sizeof(arms) / sizeof(arms[0]); i++) { + const pn_tt_activation *strip = arms[i].strip; + uint32_t width = arms[i].width; + uint32_t tiles = strip->tile_count; + double mark; + + if (tiles == 0u) + continue; + bench_fill(host, BENCH_ROWS, width, (uint32_t)i); + + mark = bench_now(); + for (int r = 0; r < reps; r++) + if (pn_tt_activation_upload(g->device, strip, host, width) != 0) + goto fail; + bench_report("upload row0", width, 1u, bench_now() - mark, reps, + bench_bytes(tiles, 1u, 2048u)); + + mark = bench_now(); + for (int r = 0; r < reps; r++) + if (pn_tt_activation_upload_rows(g->device, strip, host, width, BENCH_ROWS) != 0) + goto fail; + bench_report("upload 32 rows", width, BENCH_ROWS, bench_now() - mark, reps, + bench_bytes(tiles, BENCH_ROWS, 2048u)); + + mark = bench_now(); + for (int r = 0; r < reps; r++) + if (pn_tt_activation_download(g->device, strip, host, width) != 0) + goto fail; + bench_report("download row0", width, 1u, bench_now() - mark, reps, + bench_bytes(tiles, 1u, 2048u)); + + mark = bench_now(); + for (int r = 0; r < reps; r++) + if (pn_tt_activation_download_rows(g->device, strip, host, width, BENCH_ROWS) != 0) + goto fail; + bench_report("download 32 rows", width, BENCH_ROWS, bench_now() - mark, reps, + bench_bytes(tiles, BENCH_ROWS, 2048u)); + } + free(host); + return 0; +fail: + free(host); + fprintf(stderr, "QUANTUM REFUSED: a transfer failed\n"); + return -1; +} + +/** THE SELF-TEST. Without it the timings above are the cost of moving unknown bytes. */ +static int bench_selftest(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg) +{ + const pn_tt_activation *strip = &g->card.hidden[0]; + uint32_t width = (uint32_t)cfg->n_embd; + float *sent = calloc((size_t)BENCH_ROWS * width, sizeof(float)); + float *got = calloc((size_t)BENCH_ROWS * width, sizeof(float)); + int bad_rows = 0, bad_row0 = 0, bad_zero = 0, rc = -1; + + if (!sent || !got) + goto done; + bench_fill(sent, BENCH_ROWS, width, 0x5eedu); + if (pn_tt_activation_upload_rows(g->device, strip, sent, width, BENCH_ROWS) != 0) + goto done; + if (pn_tt_activation_download_rows(g->device, strip, got, width, BENCH_ROWS) != 0) + goto done; + /* bf16 on the card, fp32 here: the round trip is a TRUNCATION, so agreement is to the + * bf16 grid and not to the bit. A row that came back as another row would be off by far + * more than that, which is what this is looking for. */ + for (uint32_t r = 0; r < BENCH_ROWS; r++) + for (uint32_t c = 0; c < width; c++) { + double a = sent[(size_t)r * width + c], b = got[(size_t)r * width + c]; + if (fabs(a - b) > 0.02 * (fabs(a) + 1e-3)) + bad_rows++; + } + memset(got, 0, (size_t)BENCH_ROWS * width * sizeof(float)); + if (pn_tt_activation_download(g->device, strip, got, width) != 0) + goto done; + for (uint32_t c = 0; c < width; c++) + if (fabs(sent[c] - got[c]) > 0.02 * (fabs(sent[c]) + 1e-3)) + bad_row0++; + + /* THE STRIP INVARIANT: an upload of 4 rows must leave rows 4..31 ZERO, or a later Mt=1 + * op reads a previous pass's positions as padding. */ + if (pn_tt_activation_upload_rows(g->device, strip, sent, width, 4u) != 0) + goto done; + memset(got, 0, (size_t)BENCH_ROWS * width * sizeof(float)); + if (pn_tt_activation_download_rows(g->device, strip, got, width, BENCH_ROWS) != 0) + goto done; + for (uint32_t r = 4; r < BENCH_ROWS; r++) + for (uint32_t c = 0; c < width; c++) + if (got[(size_t)r * width + c] != 0.0f) + bad_zero++; + + printf("QUANTUM selftest 32-row round trip %s (%d bad) row-0 reader %s (%d bad) " + "rows 4..31 zeroed after a 4-row upload %s (%d bad)\n", + bad_rows == 0 ? "OK" : "BAD", bad_rows, bad_row0 == 0 ? "OK" : "BAD", bad_row0, + bad_zero == 0 ? "OK" : "BAD", bad_zero); + rc = (bad_rows || bad_row0 || bad_zero) ? -1 : 0; +done: + free(sent); + free(got); + return rc; +} + +/** + * THE TILE QUANTUM. The same four SSM projections, run with rows 1..31 zero and then with + * rows 1..31 carrying real activations. Identical descriptors; the only variable is the DATA + * the FPU multiplies in the 31 rows it was already multiplying. + */ +static int bench_quantum(pn_qwen35_tt *g, const pn_qwen35moe_config *cfg, int reps) +{ + const pn_qwen35_tt_layer *d = NULL; + pn_qwen35_tt_strips *s = &g->card; + uint32_t width = (uint32_t)cfg->n_embd; + float *host = calloc((size_t)BENCH_ROWS * width, sizeof(float)); + double padded, live; + int rc = -1; + + for (int i = 0; i < g->n_layers; i++) + if (!g->layers[i].is_attention) { d = &g->layers[i]; break; } + if (!host || !d) + goto done; + + /* WARM UP FIRST, AND IT IS NOT A COURTESY. The first flush after arming retires and + * replays the matmul residency ("a chained projection did not complete — retiring the + * residency ... and replaying 4 projection(s)"), which lands entirely on whichever arm + * runs first and reads as a 277x tile-quantum effect that is not one. Two discarded + * rounds put both arms on a warm residency. */ + bench_fill(host, 1u, width, 11u); + if (pn_tt_activation_upload(g->device, &s->normed, host, width) != 0) + goto done; + for (int warm = 0; warm < 2; warm++) { + (void)pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false); + (void)pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false); + (void)pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false); + (void)pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false); + (void)pn_tt_matvec_flush(g->device); + } + padded = bench_now(); + for (int r = 0; r < reps; r++) { + if (pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false) != 0 || + pn_tt_matvec_flush(g->device) != 0) + goto done; + } + padded = bench_now() - padded; + + bench_fill(host, BENCH_ROWS, width, 11u); + if (pn_tt_activation_upload_rows(g->device, &s->normed, host, width, BENCH_ROWS) != 0) + goto done; + live = bench_now(); + for (int r = 0; r < reps; r++) { + if (pn_qwen35_tt_project(g, &d->attn_qkv, &s->normed, &s->ssm_qkv, false) != 0 || + pn_qwen35_tt_project(g, &d->attn_gate, &s->normed, &s->ssm_z, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_alpha, &s->normed, &s->ssm_alpha, false) != 0 || + pn_qwen35_tt_project(g, &d->ssm_beta, &s->normed, &s->ssm_beta, false) != 0 || + pn_tt_matvec_flush(g->device) != 0) + goto done; + } + live = bench_now() - live; + + printf("QUANTUM ssm projections x4 1 live row %.1f us 32 live rows %.1f us " + "ratio %.4f (per position: %.1f us -> %.1f us)\n", + 1e6 * padded / reps, 1e6 * live / reps, live / padded, + 1e6 * padded / reps, 1e6 * live / reps / (double)BENCH_ROWS); + rc = 0; +done: + free(host); + return rc; +} + +/** The two terms a 32-row pass cannot batch, timed alone: one GDN block and, if it is armed, + * one on-card attention block. `32 * these + the flat part` is the whole item's ceiling. */ +static int bench_sequential(pn_qwen35_tt *g, pn_qwen35moe_model *m, + const pn_qwen35moe_config *cfg, int reps) +{ + pn_qwen35_tt_strips *s = &g->card; + const pn_qwen35_tt_layer *d_ssm = NULL, *d_attn = NULL; + int ssm_layer = -1, attn_layer = -1; + double mark; + + for (int i = 0; i < g->n_layers; i++) { + if (!g->layers[i].is_attention && !d_ssm) { d_ssm = &g->layers[i]; ssm_layer = i; } + if (g->layers[i].is_attention && !d_attn) { d_attn = &g->layers[i]; attn_layer = i; } + } + /* NON-FATAL, and reported as a refusal rather than a zero. These two blocks are reached + * here OUTSIDE a forward — no embedding, no norms, no carried state at position 0 — so a + * residency that will not take a descriptor in that context is a fact about the bench and + * not about the block. The residency counters in a real run are the authority on their + * cost; this is a cross-check, so a refusal must read as one. */ + if (d_ssm && g->gdn_strip) { + const pn_qwen35moe_ssm_layer *l = + pn_qwen35moe_model_ssm_layer(m, pn_qwen35moe_model_layer_index(m, ssm_layer)); + int failed = 0; + + memset(g->h_alpha, 0, sizeof(float) * 32u); + memset(g->h_beta, 0, sizeof(float) * 32u); + mark = bench_now(); + for (int r = 0; r < reps && l && !failed; r++) + failed = pn_qwen35_tt_gdn_strip_layer(g, l, cfg, + pn_qwen35moe_model_layer_index(m, ssm_layer), + &s->ssm_qkv, &s->ssm_z, g->h_beta, + g->h_alpha, &s->ssm_gated) != 0; + if (failed || !l) + printf("QUANTUM gdn strip block REFUSED outside a forward — read it off the " + "residency counters instead\n"); + else + printf("QUANTUM gdn strip block %.1f us/call -> %.4f s over 24 layers\n", + 1e6 * (bench_now() - mark) / reps, 24.0 * (bench_now() - mark) / reps); + } + if (d_attn && g->attn_on_card) { + int idx = pn_qwen35moe_model_layer_index(m, attn_layer); + int failed = 0; + + mark = bench_now(); + for (int r = 0; r < reps && !failed; r++) + failed = pn_qwen35_tt_attn_block_card(g, d_attn, cfg, idx, r + 1, r + 2) != 0; + if (failed) + printf("QUANTUM attention block REFUSED outside a forward — read it off the " + "residency counters instead\n"); + else + printf("QUANTUM attention block %.1f us/call -> %.4f s over 8 layers " + "(at trivial context)\n", + 1e6 * (bench_now() - mark) / reps, 8.0 * (bench_now() - mark) / reps); + } else { + printf("QUANTUM attention block NOT ON CARD on this arm\n"); + } + return 0; +} + +int main(int argc, char **argv) +{ + const char *model_path = NULL; + int reps = 20; + pn_model *model; + pn_qwen35moe_model *m; + pn_qwen35_tt *g; + const pn_qwen35moe_config *cfg; + int rc = 1; + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--reps") == 0 && i + 1 < argc) + reps = atoi(argv[++i]); + else if (!model_path) + model_path = argv[i]; + } + if (!model_path || reps < 1) { + fprintf(stderr, "usage: %s [--reps N]\n", argv[0]); + return 2; + } + model = pn_load(model_path); + if (!model) { + fprintf(stderr, "QUANTUM REFUSED: cannot load %s\n", model_path); + return 2; + } + /* THE VACUOUS-RUN GUARD. Timing host memcpys against each other would answer nothing. */ + if (model->forward != pn_qwen35moe_forward_tt) { + fprintf(stderr, "QUANTUM REFUSED: the engine did not select the qwen35 Tenstorrent " + "forward. Set PN_TT_ARTIFACT and check the card.\n"); + pn_model_free(model); + return 2; + } + m = (pn_qwen35moe_model *)model->weights; + g = (pn_qwen35_tt *)pn_qwen35moe_model_gpu(m); + cfg = pn_qwen35moe_model_config(m); + if (!g || !cfg || !g->resident) { + fprintf(stderr, "QUANTUM REFUSED: no resident arm — there are no card strips to " + "time.\n"); + pn_model_free(model); + return 2; + } + printf("QUANTUM model n_embd %d n_layer %d strips: hidden %u ssm_qkv %u ssm_z %u " + "ssm_gated %u ffn_swiglu %u tiles; gdn_strip %d attn_on_card %d\n", + cfg->n_embd, cfg->n_layer, g->card.hidden[0].tile_count, g->card.ssm_qkv.tile_count, + g->card.ssm_z.tile_count, g->card.ssm_gated.tile_count, + g->card.ffn_swiglu.tile_count, (int)g->gdn_strip, (int)g->attn_on_card); + + /* ORDER IS LOAD-BEARING, AND IT WAS MEASURED THE HARD WAY. A resident triad has an IDLE + * SPIN LIMIT — the transport says so itself ("leaves core 71 resident until its idle spin + * limit, measured at over 40 s") — and the transfer arms below take ~40 s of link with no + * descriptor in them. Running them first retired the matmul residency mid-bench and every + * projection after it came back `resident matmul Mt=1 Kt=128 Nt=256 refused`, which reads + * as a tile-quantum result and is a bench artefact. The card arms run FIRST, while the + * residencies are warm, and the transfers run last. */ + if (bench_selftest(g, cfg) != 0) { + fprintf(stderr, "QUANTUM REFUSED: the transfers do not move the right bytes; the " + "timings below would be the cost of being wrong.\n"); + goto done; + } + if (bench_quantum(g, cfg, reps) != 0) + goto done; + if (bench_sequential(g, m, cfg, reps) != 0) + goto done; + if (bench_transfers(g, cfg, reps) != 0) + goto done; + rc = 0; +done: + printf("QUANTUM VERDICT %s\n", rc == 0 ? "MEASURED" : "REFUSED"); + pn_model_free(model); + return rc; +} + +#endif /* USE_TT */ diff --git a/tests/bench_verify_rows.c b/tests/bench_verify_rows.c new file mode 100644 index 0000000..9f2ff95 --- /dev/null +++ b/tests/bench_verify_rows.c @@ -0,0 +1,92 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* bench_verify_rows.c - M1 milestone bench for batched speculative decoding. + * + * Measures the cost of one pn_llama_verify pass as a function of the number of + * rows M (candidate tokens verified in one shot). The verify pass is the kernel + * the batched speculative decoder runs once per step over the concatenation of + * every slot's [pending, drafts...] block, so this table confirms the payoff + * (verify of M rows costs far less than M separate single-token forwards) and + * picks a sane default draft width K. + * + * The pass runs on a state prefilled to a realistic context length so the + * attention cost (which grows with committed length) is representative. pos is + * never advanced by verify, so the same committed cache is reused for every M + * and every repetition. + * + * Usage: bench_verify_rows [model.gguf] [ctx_len] [reps] + */ + +#include "../include/pantheon.h" +#include "../src/core/engine.h" +#include "../src/models/llama.h" +#include +#include +#include +#include + +static double now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1e3 + ts.tv_nsec / 1e6; +} + +int main(int argc, char **argv) { + const char *path = (argc > 1) ? argv[1] : "models/lightonocr-lm-Q4_K_M.gguf"; + int ctx_len = (argc > 2) ? atoi(argv[2]) : 256; + int reps = (argc > 3) ? atoi(argv[3]) : 20; + + pn_model *model = pn_load(path); + if (!model) { fprintf(stderr, "load failed: %s\n", path); return 1; } + pn_state *state = pn_state_new(model); + if (!state) { fprintf(stderr, "state failed\n"); return 1; } + int nv = model->config.n_vocab; + + /* Prefill a deterministic context so verify runs against a warm cache. */ + uint32_t *prompt = malloc((size_t)ctx_len * sizeof(uint32_t)); + uint32_t x = 0x9e3779b9u; + for (int i = 0; i < ctx_len; i++) { + x ^= x << 13; x ^= x >> 17; x ^= x << 5; + prompt[i] = (x % (uint32_t)(nv - 4)) + 2; + } + pn_state_reset(state); + tm_tensor *pl = pn_llama_prefill(model, prompt, NULL, ctx_len, state); + if (!pl) { + printf("[SKIP] model not on the quantized non-MoE verify path\n"); + free(prompt); pn_state_free(state); pn_model_free(model); + return 0; + } + tm_free(pl); + printf("model: %s n_vocab=%d n_layer=%d n_embd=%d ctx_len=%d reps=%d\n", + path, nv, model->config.n_layer, model->config.n_embd, ctx_len, reps); + + const int Ms[] = { 6, 8, 12, 16, 24, 32, 48, 64 }; + const int nM = (int)(sizeof(Ms) / sizeof(Ms[0])); + + uint32_t cand[64]; + for (int i = 0; i < 64; i++) { + x ^= x << 13; x ^= x >> 17; x ^= x << 5; + cand[i] = (x % (uint32_t)(nv - 4)) + 2; + } + float *logits = malloc((size_t)64 * (size_t)nv * sizeof(float)); + if (!logits) { fprintf(stderr, "oom\n"); return 1; } + + printf("\n M ms/call ms/row rows/s\n"); + printf(" -- -------- ------- --------\n"); + for (int mi = 0; mi < nM; mi++) { + int M = Ms[mi]; + /* one warm-up */ + if (pn_llama_verify(model, cand, NULL, M, state, logits) != 0) { + fprintf(stderr, "verify failed at M=%d\n", M); return 1; + } + double t0 = now_ms(); + for (int r = 0; r < reps; r++) + pn_llama_verify(model, cand, NULL, M, state, logits); + double dt = (now_ms() - t0) / reps; + printf(" %2d %8.3f %7.4f %8.1f\n", M, dt, dt / M, 1000.0 * M / dt); + } + + free(logits); free(prompt); + pn_state_free(state); pn_model_free(model); + return 0; +} diff --git a/tests/bench_vision.c b/tests/bench_vision.c new file mode 100644 index 0000000..15aa2de --- /dev/null +++ b/tests/bench_vision.c @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/* Vision-encoder benchmark + embedding dump for flash-attention validation. + * Builds a fixed deterministic image, runs pn_vision_encode best-of-N, prints + * timings, and writes the image-token embeddings to a binary file for cosine + * comparison across builds. */ +#include +#include +#include +#include +#include "src/formats/gguf.h" +#include "src/models/pixtral_vision.h" + +static double now_ms(void) { + struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); + return ts.tv_sec * 1000.0 + ts.tv_nsec / 1e6; +} + +int main(int argc, char **argv) { + const char *mmproj = argc > 1 ? argv[1] : "models/mmproj-F16.gguf"; + const char *outfile = argc > 2 ? argv[2] : NULL; + int dim_img = argc > 3 ? atoi(argv[3]) : 1024; + int nruns = argc > 4 ? atoi(argv[4]) : 3; + + pn_gguf_file *g = pn_gguf_open(mmproj); + if (!g) { fprintf(stderr, "cannot open %s\n", mmproj); return 1; } + pn_vision_model *m = pn_vision_load(g); + pn_gguf_close(g); + if (!m) { fprintf(stderr, "vision load failed\n"); return 1; } + + int W = dim_img, H = dim_img; + uint8_t *rgb = malloc((size_t)W * H * 3); + /* deterministic pseudo-random content (fixed seed) */ + uint32_t s = 0x12345678u; + for (size_t i = 0; i < (size_t)W * H * 3; i++) { + s = s * 1664525u + 1013904223u; + rgb[i] = (uint8_t)((s >> 24) ^ (i * 31u)); + } + + float *embd = NULL; int pd = 0, M = 0; + double best = 1e30; + for (int r = 0; r < nruns; r++) { + free(embd); embd = NULL; + double t0 = now_ms(); + M = pn_vision_encode(m, rgb, W, H, &embd, &pd); + double dt = now_ms() - t0; + if (dt < best) best = dt; + fprintf(stderr, "run %d: %.1f ms (tokens=%d dim=%d)\n", r, dt, M, pd); + } + fprintf(stderr, "BEST vision encode: %.1f ms\n", best); + printf("%.1f\n", best); + + if (outfile && embd && M > 0) { + FILE *f = fopen(outfile, "wb"); + if (f) { + int32_t hdr[2] = { M, pd }; + fwrite(hdr, sizeof(int32_t), 2, f); + fwrite(embd, sizeof(float), (size_t)M * pd, f); + fclose(f); + fprintf(stderr, "wrote %d x %d embeddings to %s\n", M, pd, outfile); + } + } + free(embd); free(rgb); pn_vision_free(m); + return 0; +} diff --git a/tests/causality_red/cuda_host_attention_reads_future.patch b/tests/causality_red/cuda_host_attention_reads_future.patch new file mode 100644 index 0000000..c8a76f0 --- /dev/null +++ b/tests/causality_red/cuda_host_attention_reads_future.patch @@ -0,0 +1,22 @@ +--- a/src/models/qwen35moe_forward_cuda.c ++++ b/src/models/qwen35moe_forward_cuda.c +@@ -5859,6 +5859,10 @@ + float krow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + float vrow[PN_QWEN35MOE_ATTN_HEAD_DIM_MAX]; + if (!scores) return -1; ++ /* CAUSALITY RED ARM (tests/causality_red): the first pass stores every row of the ++ * batch, the second re-attends with each row reading the WHOLE batch -- rows read ++ * K/V written by later rows. Run with QWEN35_FLASH=0 so this path is taken. */ ++ for (int red_pass = 0; red_pass < 2; red_pass++) + for (int t = 0; t < n; t++) { + int pos = pos0 + t; + int cache_pos = pos % max_seq; +@@ -5884,7 +5888,7 @@ + } + } + +- int seq_len = (pos + 1 < max_seq) ? pos + 1 : max_seq; ++ int seq_len = (pos0 + n < max_seq) ? pos0 + n : max_seq; + const float *qhost = hb_q + (size_t)t * attn_dim; + float *aout = aout_all + (size_t)t * attn_dim; + for (int qh = 0; qh < n_head; qh++) { diff --git a/tests/causality_red/run_red.sh b/tests/causality_red/run_red.sh new file mode 100755 index 0000000..fe508a3 --- /dev/null +++ b/tests/causality_red/run_red.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# run_red.sh — the red proof for tests/test_qwen35_prefill_causality. +# +# A green causality gate means nothing until it has been seen to go red. This builds the test +# twice from copies of this working tree under build/causality_red/ — once as is, once with a +# patch from this directory applied to the COPY (production sources are never touched) — and +# requires every arm to land where it must: +# +# tt green default config (chunkwise GDN, tall 64-band passes, +# argmax criterion) PASS +# green-strict STRICT = PN_TT_PREFILL_GDNC=0 PN_TT_PREFILL_ROWS=32: +# per-position GDN, 32-row passes, digest criterion PASS +# red-patch tt_chunk_attention_causal_off.patch: the chunk +# attention's causal tiles are all live, so each row +# reads its own pass's future FAIL +# red-patch-strict the same under STRICT FAIL +# red-env STRICT + PN_TT_RED=prefill_causal on the green binary: +# the per-row GDN and attention loops run backwards FAIL +# +# STRICT needs PN_TT_PREFILL_ROWS=32: the per-position fallback (GDNC=0, prefill_causal) +# addresses one band, so a wider pass declines — and the test fails a declined or aborted +# prefill (pn_qwen35moe_prefill_tt_last_batched), because an aborted pass still returns +# per-token logits. Before that check both arms ran no batched pass and scored green. +# cuda green default (flash prefill) PASS +# green-host QWEN35_FLASH=0 (host-softmax batch attention) PASS +# red-patch cuda_host_attention_reads_future.patch with +# QWEN35_FLASH=0: every row attends the whole batch FAIL +# +# A red arm only counts when the test printed a causality FAIL verdict — a refusal, a crash +# or a load failure is not red, it is broken. +# +# usage: tests/causality_red/run_red.sh tt|cuda [extra make vars...] +# tt needs PN_TT_ARTIFACT (and whatever card env the box uses) in the environment. +set -u +backend="${1:?tt|cuda}" +model="${2:?model.gguf}" +shift 2 +repo="$(cd "$(dirname "$0")/../.." && pwd)" +out="$repo/build/causality_red" +foundry="" +for candidate in "$repo/foundry" "$repo/../foundry"; do + [ -f "$candidate/tensor/tensor.h" ] && { foundry="$(cd "$candidate" && pwd -P)"; break; } +done +[ -n "$foundry" ] || { echo "run_red: foundry not found"; exit 2; } +case "$backend" in + tt) flags=(USE_TT=1); patch_file=tt_chunk_attention_causal_off.patch ;; + cuda) flags=(USE_CUDA=1); patch_file=cuda_host_attention_reads_future.patch ;; + *) echo "run_red: backend must be tt or cuda"; exit 2 ;; +esac +bin=tests/test_qwen35_prefill_causality + +build_copy() { # name patch|"" [make vars...] + local name="$1" patch_name="$2" dir="$out/$1" + shift 2 + rm -rf "$dir" && mkdir -p "$dir" || return 1 + (cd "$repo" && git ls-files -co --exclude-standard -z | grep -zv '^build/' | + tar --null -T - -cf -) | tar -xf - -C "$dir" || return 1 + if [ -n "$patch_name" ]; then + patch -s -p1 -d "$dir" < "$repo/tests/causality_red/$patch_name" || return 1 + fi + make -C "$dir" FOUNDRY="$foundry" "${flags[@]}" "$@" "$bin" > "$dir/build.log" 2>&1 || + { echo "run_red: build of $name failed, see $dir/build.log"; return 1; } +} + +run_arm() { # name binary expect(pass|fail) env... + local name="$1" binary="$2" expect="$3" output rc verdict + shift 3 + output="$(env PN_QWEN35_MODEL="$model" "$@" "$binary" 2>&1)" + rc=$? + printf '%s\n' "$output" > "$out/$name.log" + verdict="$(printf '%s\n' "$output" | grep -E 'CAUSALITY path=' | tail -1)" + echo "$name: rc=$rc ${verdict:-}" + if [ "$expect" = pass ]; then + [ "$rc" = 0 ] && [[ "$verdict" == *PASS ]] && return 0 + echo "ARM $name: expected PASS" + else + [ "$rc" != 0 ] && [[ "$verdict" == *FAIL ]] && return 0 + echo "ARM $name: expected a causality FAIL — the gate is vacuous for this mutation" + fi + return 1 +} + +mkdir -p "$out" +build_copy green "" "$@" || exit 2 +build_copy red "$patch_file" "$@" || exit 2 +# Two arms that are the same bytes are a broken build, never a result (CLAUDE.md, A/B rule). +"$repo/tests/ab_build_arm.sh" --compare "$out/green/$bin" "$out/red/$bin" || exit 2 + +fail=0 +if [ "$backend" = tt ]; then + strict=(PN_TT_PREFILL_GDNC=0 PN_TT_PREFILL_ROWS=32) + run_arm tt-green "$out/green/$bin" pass || fail=1 + run_arm tt-green-strict "$out/green/$bin" pass "${strict[@]}" || fail=1 + run_arm tt-red-patch "$out/red/$bin" fail || fail=1 + run_arm tt-red-patch-strict "$out/red/$bin" fail "${strict[@]}" || fail=1 + run_arm tt-red-env "$out/green/$bin" fail "${strict[@]}" \ + PN_TT_RED=prefill_causal || fail=1 +else + run_arm cuda-green "$out/green/$bin" pass || fail=1 + run_arm cuda-green-host "$out/green/$bin" pass QWEN35_FLASH=0 || fail=1 + run_arm cuda-red-patch "$out/red/$bin" fail QWEN35_FLASH=0 || fail=1 +fi +[ "$fail" = 0 ] && echo "RED PROOF ($backend): every arm behaved" || + echo "RED PROOF ($backend): FAILED" +exit "$fail" diff --git a/tests/causality_red/tt_chunk_attention_causal_off.patch b/tests/causality_red/tt_chunk_attention_causal_off.patch new file mode 100644 index 0000000..ba0fc64 --- /dev/null +++ b/tests/causality_red/tt_chunk_attention_causal_off.patch @@ -0,0 +1,13 @@ +--- a/src/accel/tt/pn_tt_attention.c ++++ b/src/accel/tt/pn_tt_attention.c +@@ -194,6 +194,10 @@ + free(constants); + return false; + } ++ /* CAUSALITY RED ARM (tests/causality_red): zero the 32 causal tiles, so every row of a ++ * prefill chunk attends to the whole chunk, its own future included. */ ++ memset(constants + (size_t)FT_TT_OP_ATTENTION_CONST_CAUSAL_TILE * PN_TT_TILE_BF16, 0, ++ (size_t)32u * PN_TT_TILE_BF16); + ok = pn_tt_dram_alloc_zeroed(device, bytes, &device->attn_const_bank, + &device->attn_const_address) == 0 && + pn_tt_write_dram(device, device->attn_const_bank, device->attn_const_address, diff --git a/tests/chat_corpus.c b/tests/chat_corpus.c new file mode 100644 index 0000000..1f7ab6f --- /dev/null +++ b/tests/chat_corpus.c @@ -0,0 +1,190 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file chat_corpus.c + * @brief Implementation of the corpus-to-token-ids conversion declared in chat_corpus.h. + * + * @status Working + * @issues None + * @todo None + */ + +#include "chat_corpus.h" + +#include "../src/core/bpe_pretokenize.h" +#include "../src/core/chat.h" +#include "../src/core/engine.h" +#include "tokenizer/tokenizer.h" + +#include +#include + +void chat_document_free(chat_document *doc) +{ + for (int i = 0; i < doc->n_turns; i++) { + free(doc->text[i]); + doc->text[i] = NULL; + } + doc->n_turns = 0; +} + +int chat_document_read(FILE *in, chat_document *doc) +{ + static char line[CHAT_CORPUS_MAX_TEXT_BYTES]; + + doc->n_turns = 0; + while (fgets(line, sizeof(line), in)) { + size_t len = strlen(line); + char *body; + + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + line[--len] = '\0'; + if (len == 0) { + if (doc->n_turns > 0) + return 1; + continue; + } + if (line[0] == '#') + continue; + if ((line[0] != 'U' && line[0] != 'A' && line[0] != 'S') || line[1] != '\t') { + fprintf(stderr, "chat_corpus: line is not S, U or A: %.40s\n", line); + continue; + } + if (doc->n_turns >= CHAT_CORPUS_MAX_TURNS) { + fprintf(stderr, "chat_corpus: more than %d turns in one document\n", + CHAT_CORPUS_MAX_TURNS); + continue; + } + body = strdup(line + 2); + if (!body) + return doc->n_turns > 0; + doc->role[doc->n_turns] = line[0]; + doc->text[doc->n_turns] = body; + doc->n_turns++; + } + return doc->n_turns > 0; +} + +/** Append `n` ids, refusing rather than truncating silently. */ +static int ids_append(uint32_t *ids, int *n_ids, int max_ids, const uint32_t *add, int n) +{ + if (*n_ids + n > max_ids) { + fprintf(stderr, "chat_corpus: document exceeds %d ids\n", max_ids); + return -1; + } + for (int i = 0; i < n; i++) + ids[(*n_ids)++] = add[i]; + return 0; +} + +/** BPE-encode `text` and append its ids. The chat arm needs this for the assistant BODY, + * which the template functions do not carry — they emit the role frame only. */ +static int ids_append_text(pn_model *model, uint32_t *ids, int *n_ids, int max_ids, + const char *text) +{ + int n = 0; + int *enc = pn_bpe_encode(model->tokenizer, text, &n); + int rc = 0; + + if (!enc || n <= 0) { + fprintf(stderr, "chat_corpus: encode failed for %.40s\n", text); + free(enc); + return -1; + } + for (int i = 0; i < n && rc == 0; i++) { + uint32_t id = (uint32_t)enc[i]; + + rc = ids_append(ids, n_ids, max_ids, &id, 1); + } + free(enc); + return rc; +} + +int chat_document_to_chat_ids(pn_model *model, const chat_document *doc, uint32_t *ids, + int max_ids) +{ + pn_chat_style style = (pn_chat_style)model->chat_style; + const char *system = NULL; + int n_ids = 0; + int first_user = 1; + + /* A leading S turn is the SYSTEM prompt, and it belongs in the template's own system + * block rather than as another user turn. That is not cosmetic: framed as a system block + * the defect corpus costs +22.1% perplexity on this card, and the SAME text framed as a + * second user turn (which also leaves an empty assistant turn between them) does not + * reproduce it at all. A gate that cannot emit a system block is not scoring what a serve + * sends. */ + for (int t = 0; t < doc->n_turns; t++) + if (doc->role[t] == 'S' && !system) + system = doc->text[t]; + + for (int t = 0; t < doc->n_turns; t++) { + if (doc->role[t] == 'S') { + continue; + } else if (doc->role[t] == 'U') { + pn_token_seq seq = first_user + ? pn_chat_format_first(style, model->tokenizer, system, + doc->text[t]) + : pn_chat_format_turn(style, model->tokenizer, doc->text[t]); + int rc; + + if (!seq.tokens || seq.n_tokens <= 0) { + fprintf(stderr, "chat_corpus: chat formatting failed at turn %d\n", t); + free(seq.tokens); + return -1; + } + rc = ids_append(ids, &n_ids, max_ids, seq.tokens, seq.n_tokens); + free(seq.tokens); + if (rc != 0) + return -1; + first_user = 0; + } else if (ids_append_text(model, ids, &n_ids, max_ids, doc->text[t]) != 0) { + return -1; + } + } + return n_ids; +} + +int chat_document_to_plain_ids(pn_model *model, const chat_document *doc, uint32_t *ids, + int max_ids) +{ + static char joined[CHAT_CORPUS_MAX_TEXT_BYTES]; + int n_ids = 0; + size_t used = 0; + + joined[0] = '\0'; + for (int t = 0; t < doc->n_turns; t++) { + int n = snprintf(joined + used, sizeof(joined) - used, "%s%s", + t == 0 ? "" : "\n\n", doc->text[t]); + + if (n < 0 || (size_t)n >= sizeof(joined) - used) { + fprintf(stderr, "chat_corpus: joined document exceeds %zu bytes\n", sizeof(joined)); + return -1; + } + used += (size_t)n; + } + if (ids_append_text(model, ids, &n_ids, max_ids, joined) != 0) + return -1; + return n_ids; +} + +char *chat_ids_to_text(pn_model *model, const uint32_t *ids, int n_ids) +{ + int is_bpe = ft_get_model_type(model->tokenizer) == 0; + int *signed_ids = malloc((size_t)n_ids * sizeof(int)); + char *raw, *text; + + if (!signed_ids) + return NULL; + for (int i = 0; i < n_ids; i++) + signed_ids[i] = (int)ids[i]; + raw = ft_decode(model->tokenizer, signed_ids, n_ids); + free(signed_ids); + if (!raw) + return NULL; + text = is_bpe ? gpt2_bytes_decode(raw) : NULL; + if (!text) + return raw; + free(raw); + return text; +} diff --git a/tests/chat_corpus.h b/tests/chat_corpus.h new file mode 100644 index 0000000..4047960 --- /dev/null +++ b/tests/chat_corpus.h @@ -0,0 +1,91 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file chat_corpus.h + * @brief Turn a text corpus into the token ids the PRODUCT would prefill — chat-templated + * through the model's own template, or plain through its own BPE — and back again. + * + * A corpus assembled by hand from guessed ids proves nothing. The 20-probe accelerator + * gate read 0/20 because its ids were Llama-2/SentencePiece fed to a Qwen BPE vocab: + * decoded, probe 1 was `ende+}}$--23et`. It never had the chance to fail. Every + * harness that scores a chat-framed prompt tokenises through this one module, so token + * soup is impossible by construction rather than by review. + * + * INPUT FORMAT (one document per blank-line-separated block): + * + * Ssystem prompt text (optional, at most one, anywhere in the document) + * Uuser turn text + * Aassistant turn text + * + * + * Lines beginning with '#' are comments, so a corpus can carry the reasoning for what is in + * it next to the text itself. + * + * THE SYSTEM ROLE IS NOT OPTIONAL DECORATION. Framed as a real `<|im_start|>system` block the + * defect corpus costs +22.1% perplexity on the Blackhole card; the same text framed as a + * second user turn does not reproduce it. A corpus that cannot express a system prompt cannot + * express what a serve sends, and a gate built on it is unrepresentative in the same way the + * 20-probe gate was. + * + * The chat arm frames each user turn with pn_chat_format_first / pn_chat_format_turn and + * encodes the assistant body inside the frame; the plain arm concatenates the same turn texts + * with a blank line between them and BPE-encodes that. Same content, and the ONLY difference + * is the wrapper — which is what makes a crossed control possible. + * + * @status Working + * @issues None + * @todo None + */ +#ifndef PN_TESTS_CHAT_CORPUS_H +#define PN_TESTS_CHAT_CORPUS_H + +#include "../include/pantheon.h" + +#include +#include + +#define CHAT_CORPUS_MAX_TURNS 64 +#define CHAT_CORPUS_MAX_TEXT_BYTES (256 * 1024) +#define CHAT_CORPUS_MAX_IDS 16384 + +/** One parsed document: alternating turns, each tagged user ('U') or assistant ('A'). */ +typedef struct { + char role[CHAT_CORPUS_MAX_TURNS]; + char *text[CHAT_CORPUS_MAX_TURNS]; + int n_turns; +} chat_document; + +/** + * Read one blank-line-terminated document from `in`. + * + * @return 1 when a document was read, 0 at end of input with nothing pending + */ +int chat_document_read(FILE *in, chat_document *doc); + +/** Release the turn texts and reset the turn count. */ +void chat_document_free(chat_document *doc); + +/** + * Chat-templated ids for one document: the model's own template frames each user turn and + * opens each assistant turn, and the assistant body is encoded into the frame. + * + * @param ids caller's buffer + * @param max_ids its capacity; the function REFUSES rather than truncating silently + * @return number of ids written, or -1 + */ +int chat_document_to_chat_ids(pn_model *model, const chat_document *doc, uint32_t *ids, + int max_ids); + +/** The same turn texts with a blank line between them and no role frame — the control arm. */ +int chat_document_to_plain_ids(pn_model *model, const chat_document *doc, uint32_t *ids, + int max_ids); + +/** + * Decode ids back to text. Byte-level BPE ids come back GPT-2 byte-escaped, so they go + * through the same unescape the engine uses. + * + * @return malloc'd text the caller frees, or NULL + */ +char *chat_ids_to_text(pn_model *model, const uint32_t *ids, int n_ids); + +#endif /* PN_TESTS_CHAT_CORPUS_H */ diff --git a/tests/data/chat_parity_corpus.txt b/tests/data/chat_parity_corpus.txt new file mode 100644 index 0000000..fb94309 --- /dev/null +++ b/tests/data/chat_parity_corpus.txt @@ -0,0 +1,31 @@ +# Chat-parity gate corpus. +# +# Format: one document per blank-line-separated block; S is the system prompt, U a +# user turn, A an assistant turn. tests/chat_corpus.c frames these with the MODEL'S OWN +# chat template and encodes them with its OWN BPE, so the gate can never be fed another +# tokeniser's ids the way the 20-probe gate was. +# +# Chosen for REPRESENTATIVENESS, not to make a number: documents 1-2 are the shape a serve +# actually receives (an S system prompt followed by one long technical question), documents +# 3-4 are multi-turn question-and-answer. Nothing here was selected because it showed the +# defect more loudly. +# +# The S role is load-bearing. The same instruction text emitted as a second USER turn instead +# of a system block does not reproduce the defect at all (ratio 0.925 against 1.221), so a +# corpus without a system prompt is not scoring what a serve sends. + +S You are a helpful assistant. Answer directly and concisely. Do not think out loud. +U Explain how a CPU cache works. + +S You are a careful technical assistant. Answer directly and concisely. +U I am debugging a performance problem in a C server and I want your help reasoning about it. The server accepts connections on a single listening socket, hands each connection to a worker thread from a fixed pool of sixteen threads, and each worker parses a request, looks up a key in a shared hash table protected by a reader-writer lock, serialises a response into a per-connection buffer, and writes it back. Under light load the median response time is about two hundred microseconds and the ninety-ninth percentile is about four hundred microseconds, which is fine. Under heavy load, at around forty thousand requests per second, the median stays near two hundred and fifty microseconds but the ninety-ninth percentile jumps to eleven milliseconds, and the machine is only about sixty percent busy across all cores, so it is not simply saturated. I have already checked that the network interface is not dropping packets and that the socket receive queues are not overflowing. The hash table is read-mostly: writes are under one percent of operations. Each response buffer is allocated from a global allocator on every request and freed at the end. The worker threads are not pinned to cores. Profiling with a sampling profiler shows about thirty percent of cycles inside the lock acquisition path, but the profile is flat and does not obviously point at one caller. Please walk me through the most likely causes of a tail latency of that shape, in order of how much I should suspect them, explain what evidence would confirm or rule out each one, and tell me which measurement I should take first and why. Be concrete about the mechanism in each case rather than giving general advice, and say explicitly which of these could produce a tail that is fifty times the median while leaving the median almost unchanged. + +U What does a CPU cache do, and why are there several levels of it? +A A CPU cache is a small, fast memory that sits between the processor and main memory and keeps copies of recently used data and instructions. Main memory is roughly a hundred times slower than the core, so without a cache the processor would spend most of its time waiting. There are several levels because speed and capacity pull in opposite directions: the L1 cache is tiny, perhaps thirty-two kilobytes, and can answer in a few cycles; the L2 cache is larger and a little slower; the L3 cache is shared between cores and may be tens of megabytes, but it takes dozens of cycles to reach. Each level catches what the level above it missed, so the average access time stays close to the fastest level while the effective capacity stays close to the largest. +U How does the cache decide what to keep? +A Most caches use an approximation of least-recently-used replacement within a set. The address of a line selects a set, the line can go in any way of that set, and when the set is full the way that has gone longest without a reference is evicted. True least-recently-used tracking is expensive above four ways, so hardware usually keeps a cheaper approximation such as a tree of bits or a single reference bit per way. The important consequence for a programmer is that access pattern matters more than total working-set size: a loop that walks an array with a stride equal to the set spacing can thrash a cache that would happily hold ten times the data if it were touched in order. + +U Explain why floating point addition is not associative. +A Floating point numbers carry a fixed number of significand bits, so almost every operation rounds. Rounding depends on the magnitudes of the two operands, and that makes the order of a sum matter. If you add a very large number to a very small one, the small one may be entirely lost to rounding; add the small numbers together first and their sum may be large enough to survive. A classic example is adding one to a large value and then subtracting the large value again: you get zero rather than one, because the intermediate result could not represent the difference. This is why numerical libraries use compensated summation, and why a parallel reduction that changes the order of a sum can produce a slightly different answer from the sequential version even when both are correct. +U Does that mean parallel reductions are unreliable? +A Not unreliable, but not bit-reproducible. Each ordering is a valid answer within the error bounds of the operation, and the spread between orderings is usually far smaller than the error already present in the inputs. Where it matters is testing: a gate written as a bit-exact comparison will fail the moment the reduction tree changes, even though nothing is wrong. The usual fix is to compare against a tolerance derived from the condition number of the sum, or to fix the reduction order when reproducibility is worth more than speed. diff --git a/tests/data/chat_parity_corpus_crossed.txt b/tests/data/chat_parity_corpus_crossed.txt new file mode 100644 index 0000000..70c602c --- /dev/null +++ b/tests/data/chat_parity_corpus_crossed.txt @@ -0,0 +1,34 @@ +# Chat-parity CROSSED CONTROL corpus: system block x assistant prose, 2x2. +# +# WHY THIS EXISTS. In tests/data/chat_parity_corpus.txt the two documents that carry an +# `S` system block are ALSO the two with no assistant turn, and the two with assistant +# turns carry no system block. "The defect needs a system block" and "the defect needs a +# document that is entirely prompt" are therefore the SAME statement about that corpus and +# no measurement on it can separate them. +# +# Here the two factors are crossed and the CONTENT IS HELD CONSTANT down the column: docs 0 +# and 1 are the same long technical question with and without the system block, docs 2 and 3 +# are the same two-turn dialogue with and without it. Any difference along a row is the +# system block; any difference down a column is the assistant prose. +# +# Texts are copied verbatim from chat_parity_corpus.txt so nothing new enters the corpus. + +# doc 0 — system block, question only (the shape of the recorded defect documents) +S You are a careful technical assistant. Answer directly and concisely. +U I am debugging a performance problem in a C server and I want your help reasoning about it. The server accepts connections on a single listening socket, hands each connection to a worker thread from a fixed pool of sixteen threads, and each worker parses a request, looks up a key in a shared hash table protected by a reader-writer lock, serialises a response into a per-connection buffer, and writes it back. Under light load the median response time is about two hundred microseconds and the ninety-ninth percentile is about four hundred microseconds, which is fine. Under heavy load, at around forty thousand requests per second, the median stays near two hundred and fifty microseconds but the ninety-ninth percentile jumps to eleven milliseconds, and the machine is only about sixty percent busy across all cores, so it is not simply saturated. I have already checked that the network interface is not dropping packets and that the socket receive queues are not overflowing. The hash table is read-mostly: writes are under one percent of operations. Each response buffer is allocated from a global allocator on every request and freed at the end. The worker threads are not pinned to cores. Profiling with a sampling profiler shows about thirty percent of cycles inside the lock acquisition path, but the profile is flat and does not obviously point at one caller. Please walk me through the most likely causes of a tail latency of that shape, in order of how much I should suspect them, explain what evidence would confirm or rule out each one, and tell me which measurement I should take first and why. Be concrete about the mechanism in each case rather than giving general advice, and say explicitly which of these could produce a tail that is fifty times the median while leaving the median almost unchanged. + +# doc 1 — NO system block, same question. The system-block control. +U I am debugging a performance problem in a C server and I want your help reasoning about it. The server accepts connections on a single listening socket, hands each connection to a worker thread from a fixed pool of sixteen threads, and each worker parses a request, looks up a key in a shared hash table protected by a reader-writer lock, serialises a response into a per-connection buffer, and writes it back. Under light load the median response time is about two hundred microseconds and the ninety-ninth percentile is about four hundred microseconds, which is fine. Under heavy load, at around forty thousand requests per second, the median stays near two hundred and fifty microseconds but the ninety-ninth percentile jumps to eleven milliseconds, and the machine is only about sixty percent busy across all cores, so it is not simply saturated. I have already checked that the network interface is not dropping packets and that the socket receive queues are not overflowing. The hash table is read-mostly: writes are under one percent of operations. Each response buffer is allocated from a global allocator on every request and freed at the end. The worker threads are not pinned to cores. Profiling with a sampling profiler shows about thirty percent of cycles inside the lock acquisition path, but the profile is flat and does not obviously point at one caller. Please walk me through the most likely causes of a tail latency of that shape, in order of how much I should suspect them, explain what evidence would confirm or rule out each one, and tell me which measurement I should take first and why. Be concrete about the mechanism in each case rather than giving general advice, and say explicitly which of these could produce a tail that is fifty times the median while leaving the median almost unchanged. + +# doc 2 — system block, same dialogue as doc 3. The assistant-prose control. +S You are a careful technical assistant. Answer directly and concisely. +U What does a CPU cache do, and why are there several levels of it? +A A CPU cache is a small, fast memory that sits between the processor and main memory and keeps copies of recently used data and instructions. Main memory is roughly a hundred times slower than the core, so without a cache the processor would spend most of its time waiting. There are several levels because speed and capacity pull in opposite directions: the L1 cache is tiny, perhaps thirty-two kilobytes, and can answer in a few cycles; the L2 cache is larger and a little slower; the L3 cache is shared between cores and may be tens of megabytes, but it takes dozens of cycles to reach. Each level catches what the level above it missed, so the average access time stays close to the fastest level while the effective capacity stays close to the largest. +U How does the cache decide what to keep? +A Most caches use an approximation of least-recently-used replacement within a set. The address of a line selects a set, the line can go in any way of that set, and when the set is full the way that has gone longest without a reference is evicted. True least-recently-used tracking is expensive above four ways, so hardware usually keeps a cheaper approximation such as a tree of bits or a single reference bit per way. The important consequence for a programmer is that access pattern matters more than total working-set size: a loop that walks an array with a stride equal to the set spacing can thrash a cache that would happily hold ten times the data if it were touched in order. + +# doc 3 — NO system block, dialogue only (the shape of the recorded clean documents) +U What does a CPU cache do, and why are there several levels of it? +A A CPU cache is a small, fast memory that sits between the processor and main memory and keeps copies of recently used data and instructions. Main memory is roughly a hundred times slower than the core, so without a cache the processor would spend most of its time waiting. There are several levels because speed and capacity pull in opposite directions: the L1 cache is tiny, perhaps thirty-two kilobytes, and can answer in a few cycles; the L2 cache is larger and a little slower; the L3 cache is shared between cores and may be tens of megabytes, but it takes dozens of cycles to reach. Each level catches what the level above it missed, so the average access time stays close to the fastest level while the effective capacity stays close to the largest. +U How does the cache decide what to keep? +A Most caches use an approximation of least-recently-used replacement within a set. The address of a line selects a set, the line can go in any way of that set, and when the set is full the way that has gone longest without a reference is evicted. True least-recently-used tracking is expensive above four ways, so hardware usually keeps a cheaper approximation such as a tree of bits or a single reference bit per way. The important consequence for a programmer is that access pattern matters more than total working-set size: a loop that walks an array with a stride equal to the set spacing can thrash a cache that would happily hold ten times the data if it were touched in order. diff --git a/tests/data/chat_parity_corpus_sysonly.txt b/tests/data/chat_parity_corpus_sysonly.txt new file mode 100644 index 0000000..2809417 --- /dev/null +++ b/tests/data/chat_parity_corpus_sysonly.txt @@ -0,0 +1,17 @@ +# The two documents of chat_parity_corpus.txt that carry an `S` system block, alone. +# +# A per-document perplexity ratio depends on NOTHING outside its own document — parity_score +# accumulates per document and the ratio is that document's accel perplexity over its own CPU +# perplexity — so doc 0 and doc 1 here read exactly what they read in the full corpus, and are +# directly comparable to the recorded 0.95728 / 1.03282 (host arm) and 1.90783 / 1.84081 +# (card arm). The corpus MEAN and the verdict are not comparable and must not be quoted as if +# they were: this corpus exists to price an instrument arm at half the card time, not to +# replace the gate. +# +# Texts are copied verbatim from chat_parity_corpus.txt. + +S You are a helpful assistant. Answer directly and concisely. Do not think out loud. +U Explain how a CPU cache works. + +S You are a careful technical assistant. Answer directly and concisely. +U I am debugging a performance problem in a C server and I want your help reasoning about it. The server accepts connections on a single listening socket, hands each connection to a worker thread from a fixed pool of sixteen threads, and each worker parses a request, looks up a key in a shared hash table protected by a reader-writer lock, serialises a response into a per-connection buffer, and writes it back. Under light load the median response time is about two hundred microseconds and the ninety-ninth percentile is about four hundred microseconds, which is fine. Under heavy load, at around forty thousand requests per second, the median stays near two hundred and fifty microseconds but the ninety-ninth percentile jumps to eleven milliseconds, and the machine is only about sixty percent busy across all cores, so it is not simply saturated. I have already checked that the network interface is not dropping packets and that the socket receive queues are not overflowing. The hash table is read-mostly: writes are under one percent of operations. Each response buffer is allocated from a global allocator on every request and freed at the end. The worker threads are not pinned to cores. Profiling with a sampling profiler shows about thirty percent of cycles inside the lock acquisition path, but the profile is flat and does not obviously point at one caller. Please walk me through the most likely causes of a tail latency of that shape, in order of how much I should suspect them, explain what evidence would confirm or rule out each one, and tell me which measurement I should take first and why. Be concrete about the mechanism in each case rather than giving general advice, and say explicitly which of these could produce a tail that is fifty times the median while leaving the median almost unchanged. diff --git a/tests/data/code_edit_prompts.txt b/tests/data/code_edit_prompts.txt new file mode 100644 index 0000000..ccce883 --- /dev/null +++ b/tests/data/code_edit_prompts.txt @@ -0,0 +1,592 @@ +I ran `read_file toolkit/lists.py` and got: + +```python +"""List utilities. Implement the stubs; fix any bugs. Contracts in spec.md. + +Rules: pure Python stdlib only, no third-party imports. Do NOT edit the tests. +""" + + +def dedup(lst): + """Return `lst` with duplicate values removed, preserving first-seen order. + + dedup([1, 2, 2, 3, 1, 4]) -> [1, 2, 3, 4]. + """ + raise NotImplementedError("implement dedup (see spec.md)") + + +def chunk(lst, n): + """Split `lst` into consecutive chunks of length `n` (the last may be shorter). + + chunk([1, 2, 3, 4, 5], 2) -> [[1, 2], [3, 4], [5]]. Assume n >= 1. + """ + raise NotImplementedError("implement chunk (see spec.md)") + + +def flatten(nested): + """Flatten exactly ONE level of nesting. + + flatten([[1, 2], [3], [4, 5]]) -> [1, 2, 3, 4, 5]. + Inner nesting is preserved: flatten([[1, [2]], [3]]) -> [1, [2], 3]. + """ + # BUG: this recurses fully (and crashes on non-list items) instead of + # flattening a single level. Fix it to flatten ONE level only. + out = [] + for item in nested: + out.extend(flatten(item)) + return out +``` + +`flatten` recurses all the way down and crashes on non-list items. Fix it to flatten exactly ONE level, so flatten([[1, [2]], [3]]) returns [1, [2], 3]. Leave every other function exactly as it is. + +Re-emit the complete updated contents of `toolkit/lists.py`. +===PROMPT=== +I ran `read_file toolkit/numbers.py` and got: + +```python +"""Number utilities. Implement the stubs; fix any bugs. Contracts in spec.md. + +Rules: pure Python stdlib only, no third-party imports. Do NOT edit the tests. +""" + + +def gcd(a, b): + """Greatest common divisor of `a` and `b` (Euclid's algorithm). + + Reference implementation — already correct. Read it as an example of the + house style before you implement the stubs below. + """ + while b: + a, b = b, a % b + return abs(a) + + +def is_prime(n): + """True if `n` is a prime number. n < 2 -> False. 2 -> True. 15 -> False.""" + raise NotImplementedError("implement is_prime (see spec.md)") + + +def fib(n): + """The nth Fibonacci number, 0-indexed: fib(0)=0, fib(1)=1, fib(2)=1, + fib(3)=2, ... Assume n >= 0. + """ + raise NotImplementedError("implement fib (see spec.md)") +``` + +Replace the `is_prime` stub with a working implementation (trial division up to the integer square root is fine; it must return real bools). Leave `gcd` and `fib` exactly as they are. + +Re-emit the complete updated contents of `toolkit/numbers.py`. +===PROMPT=== +I ran `read_file toolkit/strings.py` and got: + +```python +"""String utilities. Implement the stubs; fix any bugs. Contracts in spec.md. + +Rules: pure Python stdlib only, no third-party imports. Do NOT edit the tests. +Run `make test` to check your work. +""" + + +def reverse_words(s): + """Return `s` with its whitespace-separated words in reverse order. + + 'hello world foo' -> 'foo world hello'. '' -> ''. Collapse runs of spaces: + the result is single-spaced. + """ + raise NotImplementedError("implement reverse_words (see spec.md)") + + +def count_vowels(s): + """Count the vowels (a, e, i, o, u — case-insensitive) in `s`. Return an int.""" + raise NotImplementedError("implement count_vowels (see spec.md)") + + +def is_palindrome(s): + """True if `s` reads the same forwards and backwards, IGNORING case and any + non-alphanumeric characters. + + 'racecar' -> True. 'A man, a plan, a canal: Panama' -> True. 'hello' -> False. + """ + cleaned = ''.join(ch for ch in s.lower() if ch.isalnum()) + return cleaned == cleaned[::-1] +``` + +Replace the `reverse_words` stub with a working implementation: split on whitespace, reverse the word order, join with single spaces, and map '' to ''. Leave the other functions exactly as they are. + +Re-emit the complete updated contents of `toolkit/strings.py`. +===PROMPT=== +I ran `read_file tests/test_toolkit.py` and got: + +```python +#!/usr/bin/env python3 +"""Assert-based test runner for the toolkit — no pytest needed. + +Run from the repo root: `make test` (or `python3 tests/test_toolkit.py`). +Prints PASS/FAIL per check and exits non-zero if anything fails, so the agent +verification gate (and CI) can trust the exit code. DO NOT edit this file — make +the library match it. +""" +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from toolkit import strings, numbers, lists # noqa: E402 + +_failures = [] + + +def check(name, fn): + try: + ok = bool(fn()) + except Exception as exc: # a crashing implementation is a failure, not a stop + print("FAIL", name, "->", repr(exc)) + _failures.append(name) + return + print("PASS" if ok else "FAIL", name) + if not ok: + _failures.append(name) + + +# --- strings --------------------------------------------------------------- +check("reverse_words basic", lambda: strings.reverse_words("hello world foo") == "foo world hello") +check("reverse_words empty", lambda: strings.reverse_words("") == "") +check("count_vowels mixed", lambda: strings.count_vowels("Hello World") == 3) +check("count_vowels none", lambda: strings.count_vowels("xyz") == 0) +check("is_palindrome plain", lambda: strings.is_palindrome("racecar") is True) +check("is_palindrome punct", lambda: strings.is_palindrome("A man, a plan, a canal: Panama") is True) +check("is_palindrome no", lambda: strings.is_palindrome("hello") is False) + +# --- numbers --------------------------------------------------------------- +check("gcd", lambda: numbers.gcd(12, 18) == 6) +check("is_prime yes", lambda: numbers.is_prime(13) is True) +check("is_prime edges", lambda: numbers.is_prime(1) is False and numbers.is_prime(2) is True and numbers.is_prime(15) is False) +check("fib sequence", lambda: [numbers.fib(i) for i in range(8)] == [0, 1, 1, 2, 3, 5, 8, 13]) + +# --- lists ----------------------------------------------------------------- +check("dedup order", lambda: lists.dedup([1, 2, 2, 3, 1, 4]) == [1, 2, 3, 4]) +check("chunk uneven", lambda: lists.chunk([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]]) +check("flatten one level", lambda: lists.flatten([[1, 2], [3], [4, 5]]) == [1, 2, 3, 4, 5]) +check("flatten keeps inner", lambda: lists.flatten([[1, [2]], [3]]) == [1, [2], 3]) + +print() +if _failures: + print("%d FAILURE(S): %s" % (len(_failures), ", ".join(_failures))) + sys.exit(1) +print("ALL %d CHECKS PASSED" % 15) +sys.exit(0) +``` + +Add a `--verbose` flag: when sys.argv contains it, print the expected and actual value for every check, not just PASS/FAIL. Change nothing else. + +Re-emit the complete updated contents of `tests/test_toolkit.py`. +===PROMPT=== +I ran `read_file src/core/ngram_draft.c` and got: + +```c +/** + * @file ngram_draft.c + * @brief Prompt-lookup n-gram drafter (see ngram_draft.h). A linear backward + * scan per n-gram length: history stays a plain array (a few thousand + * tokens at serving lengths), so a scan is ~microseconds against a + * ~50 ms K80 forward — not worth an index. + * + * @status Working; covered by tests/test_spec_qwen35.c on the K80 test machine. + * @issues None + * @todo None + */ + +/* 1. This module's own header */ +#include "ngram_draft.h" + +/* 2. Standard library */ +#include + +int pn_ngram_draft(const uint32_t *hist, int n_hist, int ngram_max, + int ngram_min, int max_k, uint32_t *out) +{ + if (!hist || !out || max_k <= 0 || ngram_min < 1 || + ngram_max < ngram_min) + return 0; + + for (int g = ngram_max; g >= ngram_min; g--) { + if (n_hist < g + 1) + continue; + const uint32_t *key = hist + n_hist - g; + /* Most recent earlier occurrence; start excludes the trailing + * self-match (start + g <= n_hist - 1 leaves >= 1 continuation). */ + for (int start = n_hist - g - 1; start >= 0; start--) { + if (memcmp(hist + start, key, (size_t)g * sizeof(*hist)) != 0) + continue; + int avail = n_hist - (start + g); + int k = avail < max_k ? avail : max_k; + memcpy(out, hist + start + g, (size_t)k * sizeof(*hist)); + return k; + } + } + return 0; +} + +void pn_spec_throttle_init(pn_spec_throttle *t, int min_accept, + int miss_limit, int cool_steps) +{ + t->min_accept = min_accept > 0 ? min_accept : 4; + t->miss_limit = miss_limit > 0 ? miss_limit : 2; + t->cool_steps = cool_steps > 0 ? cool_steps : 48; + t->miss = 0; + t->cool = 0; +} + +int pn_spec_throttle_gate(pn_spec_throttle *t) +{ + if (t->cool > 0) { + t->cool--; + return 0; + } + return 1; +} + +void pn_spec_throttle_report(pn_spec_throttle *t, int accepted_drafts) +{ + if (accepted_drafts >= t->min_accept) { + t->miss = 0; + return; + } + if (++t->miss >= t->miss_limit) { + t->miss = 0; + t->cool = t->cool_steps; + } +} +``` + +Add an early-out to `pn_ngram_draft`: if the last token of the history is the same as the token before it, return 0 without scanning (a repeated token is a degenerate key). Change nothing else. + +Re-emit the complete updated contents of `src/core/ngram_draft.c`. +===PROMPT=== +I ran `read_file src/models/bonsai_forward.c` and got: + +```c +/** + * @file bonsai_forward.c + * @brief PrismML Bonsai-27B top-level forward/loader entry (PN_ARCH_BONSAI). + * + * This is Bonsai's OWN entry file, kept physically separate from the shipped + * Nex-N2 forward (qwen35moe_forward*.c). It owns the pn_forward_fn/loader/state + * seam for PN_ARCH_BONSAI and DELEGATES the heavy lifting to the shared + * qwen35moe machinery: Bonsai is the SAME hybrid GDN + sparse-full-attention + * family (general.architecture="qwen35"), differing only in a DENSE SwiGLU FFN + * (no MoE) and native-ternary (Q2_0_g128 / type-142) weights. + * + * Why delegate instead of clone: the hybrid forward is ~7.4k lines of shared, + * die-scratch-heavy CUDA orchestration (GDN block, flash-attn, Q4 KV, RoPE, + * RMSNorm, the ternary fc_matvec_q2_0). Cloning it to strip MoE -> dense would + * be a large correctness/drift risk (the exact hazard the bring-up flagged). + * Instead, the ONE Bonsai-specific behavior — the dense SwiGLU FFN block — is a + * config-driven branch inside the shared per-layer FFN helper, selected by + * cfg->dense_ffn (set at load from the GGUF metadata). The shipped Nex-N2 (qwen35moe, + * dense_ffn=0) never takes that branch and is byte-identical. Bonsai therefore + * has its own arch identity + its own forward entry file, without a risky clone. + */ + +#include "bonsai.h" +#include "qwen35moe.h" + +#include + +int pn_bonsai_is_bonsai_gguf(pn_gguf_file *gguf) +{ + if (!gguf) + return 0; + const char *arch = pn_gguf_get_str(gguf, "general.architecture"); + if (!arch || strcmp(arch, "qwen35") != 0) + return 0; + /* Dense FFN (Bonsai) vs routed MoE (Nex-N2): a plain per-layer ffn_gate is + * present and the merged expert tensor is absent. */ + if (pn_gguf_find_tensor(gguf, "blk.0.ffn_gate_exps.weight") != NULL) + return 0; + if (pn_gguf_find_tensor(gguf, "blk.0.ffn_gate.weight") == NULL) + return 0; + return 1; +} + +void *pn_bonsai_load(pn_gguf_file *gguf, pn_config *config) +{ + /* The shared loader detects the dense-FFN variant from the metadata + * (expert_count absent + ffn_gate present) and sets cfg->dense_ffn. */ + return pn_qwen35moe_load(gguf, config); +} + +tm_tensor *pn_bonsai_forward(pn_model *model, uint32_t token, pn_state *state) +{ + return pn_qwen35moe_forward(model, token, state); +} + +#ifdef USE_CUDA +tm_tensor *pn_bonsai_forward_cuda(pn_model *model, uint32_t token, + pn_state *state) +{ + return pn_qwen35moe_forward_cuda(model, token, state); +} + +int pn_bonsai_gpu_upload(void *model) +{ + return pn_qwen35moe_gpu_upload((pn_qwen35moe_model *)model); +} + +int pn_bonsai_model_gpu_loaded(const void *model) +{ + return pn_qwen35moe_model_gpu_loaded((const pn_qwen35moe_model *)model); +} +#endif + +void *pn_bonsai_state_new(void *weights) +{ + return pn_qwen35moe_state_new(weights); +} + +void pn_bonsai_state_free(void *state) +{ + pn_qwen35moe_state_free(state); +} + +void pn_bonsai_state_reset(void *state) +{ + pn_qwen35moe_state_reset(state); +} + +void pn_bonsai_free(void *model) +{ + pn_qwen35moe_free((pn_qwen35moe_model *)model); +} + +pn_gguf_file *pn_bonsai_gguf(const void *model) +{ + return pn_qwen35moe_gguf((const pn_qwen35moe_model *)model); +} + +void pn_bonsai_model_set_dies(void *model, const int *dies, int n) +{ + pn_qwen35moe_model_set_dies((pn_qwen35moe_model *)model, dies, n); +} +``` + +Add a short doc comment above each function describing what it returns. Change no code. + +Re-emit the complete updated contents of `src/models/bonsai_forward.c`. +===PROMPT=== +I ran `read_file src/serve/serve_registry.h` and got: + +```c +/** + * @file serve_registry.h + * @brief Serve-side dispatch self-registration lifecycle (Phase G0 wiring). + * Ties the register / heartbeat / deregister client (registry_client.h) + * into the live serve process: one register per served model_id at + * startup, a background heartbeat thread reporting live load from + * batch_sched, and a best-effort deregister on controlled shutdown. + * + * FULLY GATED on PANTHEON_REGISTRY_URL (via rc_enabled()): with it unset + * every function here is a no-op and the serve process is byte-for-byte + * unchanged (no registration, no heartbeat thread spawned, no network). + * + * Identity semantics (see registry_wire.h): the shared `model_id` is the + * served name clients request (mr_info.name) — two serve processes + * hosting the same weights advertise the SAME model_id and become a + * router replica pool; the `instance_id` is unique per serve process + * ("::"). + * + * @status G0 wiring. Live-registration behaviour is verified on a K80 host separately + * (no GPU/model on the Mac); the Mac build path exercises only the + * unset-env no-op and the compile. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_REGISTRY_H +#define PN_SERVE_REGISTRY_H + +#include "model_registry.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Startup registration: for each resident model in `reg`, build an rc_identity + * (model_id = served name, unique instance_id = "::", + * endpoint = "http://:", capacity from the model's batch_sched) and + * rc_register() it, recording it for later heartbeat + deregister. Then spawn a + * detached heartbeat thread that every RW_HEARTBEAT_SECONDS reports each + * instance's live load (bs_get_load under mr_acquire/mr_release). + * + * No-op (does not touch `reg`, spawns no thread) when rc_enabled() is false. + * Call once, after hs_listen succeeds and all models are loaded. + * + * `reg`, `host` must outlive the serve process (both are process-lifetime in + * pn_serve_main); the heartbeat thread borrows `reg` and never frees it. + * + * Thread-safe: no (call once at startup, before the accept loop). + */ +void serve_registry_start(mr_registry *reg, const char *host, int port); + +/** + * Register a model loaded AFTER serve_registry_start() (an /admin/models/load), + * using the host/port captured at start. Idempotent: a model whose instance is + * already registered (e.g. a second replica of the same served name) is skipped. + * No-op when rc_enabled() is false or serve_registry_start() has not run. The + * running heartbeat thread picks the new instance up on its next cycle. + * + * Thread-safe: yes (guards the instance table; runs from the request thread). + */ +void serve_registry_add(const char *model_id); + +/** + * Deregister a model unloaded via /admin/models/unload — but only if no replica + * of the same served name remains loaded (unloading one of two same-name + * replicas keeps the shared instance registered). No-op when disabled/unstarted. + * + * Thread-safe: yes. + */ +void serve_registry_remove(const char *model_id); + +/** + * Best-effort deregister of every instance registered by + * serve_registry_start(). Called from the SIGINT/SIGTERM shutdown handler so a + * clean stop drops the instances from the router immediately (the registry TTL + * also evicts them if this is skipped). No-op when rc_enabled() is false or + * nothing was registered. Each rc_deregister uses a short connect timeout. + * + * Safe to call from the controlled-shutdown signal handler (same trade as the + * CUDA teardown there: operator-initiated stop, not a crash mid-kernel). + */ +void serve_registry_deregister_all(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_SERVE_REGISTRY_H */ +``` + +Add a `int max_batch;` field to the registry entry struct, documented in the same comment style as the neighbouring fields. Change nothing else. + +Re-emit the complete updated contents of `src/serve/serve_registry.h`. +===PROMPT=== +I ran `read_file src/core/qweight.h` and got: + +```c +#ifndef PN_QWEIGHT_H +#define PN_QWEIGHT_H + +#include "../formats/gguf.h" +#include "../formats/gguf_dequant.h" +#include + +/* + * Quantized weight matrix. + * Points directly into mmap'd GGUF data — zero-copy. + * pn_qmv() performs matvec by dequantizing 32 elements at a time. + * + * For ternary weights (PN_QUANT_TERNARY): data is packed 2-bit + * (4 elements/byte, encoding: 00=-1, 01=0, 10=+1). + * scales[] holds per-group float32 scales, laid out as + * scales[row * n_groups + group], where n_groups = ceil(cols/group_size). + */ + +typedef struct { + const void *data; /* Raw quantized blocks (mmap pointer or owned) */ + pn_gguf_quant type; /* Q4_0, Q8_0, ..., F32, PN_QUANT_TERNARY */ + int rows; /* Output dimension */ + int cols; /* Input dimension */ + size_t row_stride; /* Bytes per row */ + const float *scales; /* Per-group ternary scales (NULL for non-ternary) */ + int group_size; /* Elements per scale group (0 if not ternary) */ + void *gpu; /* Metal buffer of the weight's Q4_K blocks, or NULL. + Unconditional (not #ifdef USE_METAL) so the struct + layout never depends on the build flag — a + conditional field here silently corrupts adjacent + structs if any TU disagrees on the flag. Unused + (always NULL) in non-Metal builds. */ +} pn_qweight; + +/* Create a qweight from a named GGUF tensor. + * Sets rows/cols from tensor dims (reversed: GGUF innermost-first → C outermost-first). + * Returns 0 on success, -1 on failure. */ +int pn_qweight_from_gguf(pn_qweight *qw, pn_gguf_file *gguf, const char *name); + +/* Quantized matrix-vector multiply: out[rows] = W[rows, cols] @ vec[cols] + * Dequantizes row by row, 32 elements at a time. + * If a thread pool has been registered via pn_qmv_set_pool() and the matrix is + * large enough, the output-row loop is distributed across threads. Each output + * row is computed whole by a single thread, so the result is bit-identical to + * the single-threaded path (no cross-thread reduction). */ +void pn_qmv(const pn_qweight *w, const float *vec, float *out); + +/* Coalesced matvec for a group of independent weights sharing one parallel + * region. Computes out[i] = W[i] @ vec[i] for i in [0,n). All n matvecs are + * dispatched to the thread pool as a SINGLE fw_for over the concatenated row + * space, cutting per-matvec dispatch (condvar broadcast + join) overhead from n + * to 1. Each output row is still computed whole by one thread, so the result is + * bit-identical to calling pn_qmv(w[i], vec[i], out[i]) individually. Falls back + * to per-weight pn_qmv for ternary/GPU-resident weights. n up to 8. */ +void pn_qmv_group(const pn_qweight *const *w, const float *const *vec, + float *const *out, int n); + +/* Fused quantized matrix times B activation vectors (M=B "matmul"): + * out[b, r] = sum_c dequant(W[r, c]) * acts[b, c] for b in [0,B), r in [0,rows) + * acts is [B, cols] row-major, out is [B, rows] row-major (out[b*rows + r]). + * + * Each weight ROW is dequantized once into a small L1-resident scratch and + * dotted against all B activation vectors, so the quantized weight is read + * once and the dequant cost is amortized across the batch — unlike + * materializing the full F32 weight to DRAM before a BLAS GEMM. This is the + * decode-batch analog of pn_qmv's fused dequant-dot. Parallelized over rows via + * the registered pool; rows are disjoint so the result is order-independent. + * B up to PN_QMM_MAX_B (falls back to a per-b pn_qmv loop above that or for + * ternary/GPU-resident weights). */ +#define PN_QMM_MAX_B 64 +void pn_qmm(const pn_qweight *w, const float *acts, int B, float *out); + +/* Prefill-oriented batched matmul. Same [B,cols] acts -> [B,rows] out contract + * as pn_qmm (out[b*rows + r]), but dispatched in fixed row-blocks so each + * per-panel cblas_sgemm keeps an AMX-friendly N dimension even for large B + * (B = P, ~1200, at prefill). Unlike pn_qmm it never degrades to a per-b matvec + * loop for large B: the panel path handles any B, and the 64 cap in pn_qmm only + * exists to bound the flag-gated GPU decode accumulators. Dequantizes each + * L2-resident weight panel once and multiplies it against all B activation + * rows, so the quantized weight is read once and never round-trips DRAM as F32. + * The only numeric difference from a per-row dequant-dot is BLAS summation + * order, the same reassociation the decode-batch pn_qmm already relies on. */ +void pn_qmm_prefill(const pn_qweight *w, const float *acts, int B, float *out); + +/* Register the thread pool used by pn_qmv to parallelize the row loop. + * Forward-declared opaque pool type (foundry threadpool fw_pool). Passing NULL + * disables parallelization. No-op (other than storing NULL) when the engine is + * built without USE_THREADS. Safe to call repeatedly; not thread-safe to call + * concurrently with pn_qmv. */ +struct fw_pool; +void pn_qmv_set_pool(struct fw_pool *pool); + +/* Quantized dot product: dequant + dot for a single row. + * Used by pn_qmv inner loop and available for direct use. */ +float pn_qdot(const void *row_data, const float *vec, int n, pn_gguf_quant type); + +/* GPU (Metal) weight residency for the Q4_K batched-decode path. + * pn_qmv_gpu_upload copies a Q4_K weight's blocks into a GPU buffer (sets + * w->gpu); pn_qmm then runs the batched simdgroup GEMM on the GPU. All three + * are no-ops / return 0 when built without USE_METAL or for non-Q4_K weights. */ +int pn_qmv_gpu_available(void); +int pn_qmv_gpu_upload(pn_qweight *w); +void pn_qmv_gpu_free(pn_qweight *w); + +/* Enable/disable running batched decode (pn_qmm) on the GPU. Off by default: + * weights are GPU-uploaded for prefill, but GPU decode is slower than CPU AMX, + * so decode stays on the CPU unless this is explicitly enabled. */ +void pn_qmm_set_gpu_decode(int on); + +#endif /* PN_QWEIGHT_H */ +``` + +Rename the constant prefix `PN_QW_` to `PN_QWEIGHT_` everywhere it appears in this header, updating every use. Change nothing else. + +Re-emit the complete updated contents of `src/core/qweight.h`. diff --git a/tests/data/ngram_chat_prompts.txt b/tests/data/ngram_chat_prompts.txt new file mode 100644 index 0000000..89c00ca --- /dev/null +++ b/tests/data/ngram_chat_prompts.txt @@ -0,0 +1,14 @@ +===SYSTEM=== +You are a careful technical assistant. Answer directly and concisely. +===PROMPT=== +I am debugging a performance problem in a C server and I want your help reasoning about it. The server accepts connections on a single listening socket, hands each connection to a worker thread from a fixed pool of sixteen threads, and each worker parses a request, looks up a key in a shared hash table protected by a reader-writer lock, serialises a response into a per-connection buffer, and writes it back. Under light load the median response time is about two hundred microseconds and the ninety-ninth percentile is about four hundred microseconds. Under heavy load, at around forty thousand requests per second, the median stays near two hundred and fifty microseconds but the ninety-ninth percentile jumps to eleven milliseconds, and the machine is only about sixty percent busy across all cores. Walk me through the most likely causes of a tail of that shape, in order of how much I should suspect them, and say which measurement I should take first and why. +===PROMPT=== +Explain how a CPU cache works, why there are several levels of it, and what a programmer can actually do about it. Be concrete about the mechanism rather than giving general advice. +===PROMPT=== +What is the difference between a mutex, a spinlock and a reader-writer lock, and when would each one be the wrong choice? I care about the failure modes, not the definitions. +===PROMPT=== +I have a build that works on Linux and fails to link on macOS with duplicate symbol errors for a handful of small helper functions defined in a header. Explain what is actually different between the two toolchains here, and give me the fix I should prefer and the fixes I should not. +===PROMPT=== +Summarise, for someone who knows C but not machine learning, what a key-value cache is in a transformer, why it grows with context length, and what the practical consequences are for a machine with limited memory. +===PROMPT=== +I am choosing between storing a time series as one row per sample in a relational table and as one row per hour with a binary blob of samples. The workload is heavy append, occasional range scan over a day, and rare single-sample lookup. Which would you choose and what would change your mind? diff --git a/tests/data/ngram_code_edit_prompts.txt b/tests/data/ngram_code_edit_prompts.txt new file mode 100644 index 0000000..3062918 --- /dev/null +++ b/tests/data/ngram_code_edit_prompts.txt @@ -0,0 +1,595 @@ +===SYSTEM=== +You are a coding agent. Apply the requested edit and re-emit the complete file in a single fenced code block. Do not explain. +===PROMPT=== +I ran `read_file toolkit/lists.py` and got: + +```python +"""List utilities. Implement the stubs; fix any bugs. Contracts in spec.md. + +Rules: pure Python stdlib only, no third-party imports. Do NOT edit the tests. +""" + + +def dedup(lst): + """Return `lst` with duplicate values removed, preserving first-seen order. + + dedup([1, 2, 2, 3, 1, 4]) -> [1, 2, 3, 4]. + """ + raise NotImplementedError("implement dedup (see spec.md)") + + +def chunk(lst, n): + """Split `lst` into consecutive chunks of length `n` (the last may be shorter). + + chunk([1, 2, 3, 4, 5], 2) -> [[1, 2], [3, 4], [5]]. Assume n >= 1. + """ + raise NotImplementedError("implement chunk (see spec.md)") + + +def flatten(nested): + """Flatten exactly ONE level of nesting. + + flatten([[1, 2], [3], [4, 5]]) -> [1, 2, 3, 4, 5]. + Inner nesting is preserved: flatten([[1, [2]], [3]]) -> [1, [2], 3]. + """ + # BUG: this recurses fully (and crashes on non-list items) instead of + # flattening a single level. Fix it to flatten ONE level only. + out = [] + for item in nested: + out.extend(flatten(item)) + return out +``` + +`flatten` recurses all the way down and crashes on non-list items. Fix it to flatten exactly ONE level, so flatten([[1, [2]], [3]]) returns [1, [2], 3]. Leave every other function exactly as it is. + +Re-emit the complete updated contents of `toolkit/lists.py`. +===PROMPT=== +I ran `read_file toolkit/numbers.py` and got: + +```python +"""Number utilities. Implement the stubs; fix any bugs. Contracts in spec.md. + +Rules: pure Python stdlib only, no third-party imports. Do NOT edit the tests. +""" + + +def gcd(a, b): + """Greatest common divisor of `a` and `b` (Euclid's algorithm). + + Reference implementation — already correct. Read it as an example of the + house style before you implement the stubs below. + """ + while b: + a, b = b, a % b + return abs(a) + + +def is_prime(n): + """True if `n` is a prime number. n < 2 -> False. 2 -> True. 15 -> False.""" + raise NotImplementedError("implement is_prime (see spec.md)") + + +def fib(n): + """The nth Fibonacci number, 0-indexed: fib(0)=0, fib(1)=1, fib(2)=1, + fib(3)=2, ... Assume n >= 0. + """ + raise NotImplementedError("implement fib (see spec.md)") +``` + +Replace the `is_prime` stub with a working implementation (trial division up to the integer square root is fine; it must return real bools). Leave `gcd` and `fib` exactly as they are. + +Re-emit the complete updated contents of `toolkit/numbers.py`. +===PROMPT=== +I ran `read_file toolkit/strings.py` and got: + +```python +"""String utilities. Implement the stubs; fix any bugs. Contracts in spec.md. + +Rules: pure Python stdlib only, no third-party imports. Do NOT edit the tests. +Run `make test` to check your work. +""" + + +def reverse_words(s): + """Return `s` with its whitespace-separated words in reverse order. + + 'hello world foo' -> 'foo world hello'. '' -> ''. Collapse runs of spaces: + the result is single-spaced. + """ + raise NotImplementedError("implement reverse_words (see spec.md)") + + +def count_vowels(s): + """Count the vowels (a, e, i, o, u — case-insensitive) in `s`. Return an int.""" + raise NotImplementedError("implement count_vowels (see spec.md)") + + +def is_palindrome(s): + """True if `s` reads the same forwards and backwards, IGNORING case and any + non-alphanumeric characters. + + 'racecar' -> True. 'A man, a plan, a canal: Panama' -> True. 'hello' -> False. + """ + cleaned = ''.join(ch for ch in s.lower() if ch.isalnum()) + return cleaned == cleaned[::-1] +``` + +Replace the `reverse_words` stub with a working implementation: split on whitespace, reverse the word order, join with single spaces, and map '' to ''. Leave the other functions exactly as they are. + +Re-emit the complete updated contents of `toolkit/strings.py`. +===PROMPT=== +I ran `read_file tests/test_toolkit.py` and got: + +```python +#!/usr/bin/env python3 +"""Assert-based test runner for the toolkit — no pytest needed. + +Run from the repo root: `make test` (or `python3 tests/test_toolkit.py`). +Prints PASS/FAIL per check and exits non-zero if anything fails, so the agent +verification gate (and CI) can trust the exit code. DO NOT edit this file — make +the library match it. +""" +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from toolkit import strings, numbers, lists # noqa: E402 + +_failures = [] + + +def check(name, fn): + try: + ok = bool(fn()) + except Exception as exc: # a crashing implementation is a failure, not a stop + print("FAIL", name, "->", repr(exc)) + _failures.append(name) + return + print("PASS" if ok else "FAIL", name) + if not ok: + _failures.append(name) + + +# --- strings --------------------------------------------------------------- +check("reverse_words basic", lambda: strings.reverse_words("hello world foo") == "foo world hello") +check("reverse_words empty", lambda: strings.reverse_words("") == "") +check("count_vowels mixed", lambda: strings.count_vowels("Hello World") == 3) +check("count_vowels none", lambda: strings.count_vowels("xyz") == 0) +check("is_palindrome plain", lambda: strings.is_palindrome("racecar") is True) +check("is_palindrome punct", lambda: strings.is_palindrome("A man, a plan, a canal: Panama") is True) +check("is_palindrome no", lambda: strings.is_palindrome("hello") is False) + +# --- numbers --------------------------------------------------------------- +check("gcd", lambda: numbers.gcd(12, 18) == 6) +check("is_prime yes", lambda: numbers.is_prime(13) is True) +check("is_prime edges", lambda: numbers.is_prime(1) is False and numbers.is_prime(2) is True and numbers.is_prime(15) is False) +check("fib sequence", lambda: [numbers.fib(i) for i in range(8)] == [0, 1, 1, 2, 3, 5, 8, 13]) + +# --- lists ----------------------------------------------------------------- +check("dedup order", lambda: lists.dedup([1, 2, 2, 3, 1, 4]) == [1, 2, 3, 4]) +check("chunk uneven", lambda: lists.chunk([1, 2, 3, 4, 5], 2) == [[1, 2], [3, 4], [5]]) +check("flatten one level", lambda: lists.flatten([[1, 2], [3], [4, 5]]) == [1, 2, 3, 4, 5]) +check("flatten keeps inner", lambda: lists.flatten([[1, [2]], [3]]) == [1, [2], 3]) + +print() +if _failures: + print("%d FAILURE(S): %s" % (len(_failures), ", ".join(_failures))) + sys.exit(1) +print("ALL %d CHECKS PASSED" % 15) +sys.exit(0) +``` + +Add a `--verbose` flag: when sys.argv contains it, print the expected and actual value for every check, not just PASS/FAIL. Change nothing else. + +Re-emit the complete updated contents of `tests/test_toolkit.py`. +===PROMPT=== +I ran `read_file src/core/ngram_draft.c` and got: + +```c +/** + * @file ngram_draft.c + * @brief Prompt-lookup n-gram drafter (see ngram_draft.h). A linear backward + * scan per n-gram length: history stays a plain array (a few thousand + * tokens at serving lengths), so a scan is ~microseconds against a + * ~50 ms K80 forward — not worth an index. + * + * @status Working; covered by tests/test_spec_qwen35.c on the K80 test machine. + * @issues None + * @todo None + */ + +/* 1. This module's own header */ +#include "ngram_draft.h" + +/* 2. Standard library */ +#include + +int pn_ngram_draft(const uint32_t *hist, int n_hist, int ngram_max, + int ngram_min, int max_k, uint32_t *out) +{ + if (!hist || !out || max_k <= 0 || ngram_min < 1 || + ngram_max < ngram_min) + return 0; + + for (int g = ngram_max; g >= ngram_min; g--) { + if (n_hist < g + 1) + continue; + const uint32_t *key = hist + n_hist - g; + /* Most recent earlier occurrence; start excludes the trailing + * self-match (start + g <= n_hist - 1 leaves >= 1 continuation). */ + for (int start = n_hist - g - 1; start >= 0; start--) { + if (memcmp(hist + start, key, (size_t)g * sizeof(*hist)) != 0) + continue; + int avail = n_hist - (start + g); + int k = avail < max_k ? avail : max_k; + memcpy(out, hist + start + g, (size_t)k * sizeof(*hist)); + return k; + } + } + return 0; +} + +void pn_spec_throttle_init(pn_spec_throttle *t, int min_accept, + int miss_limit, int cool_steps) +{ + t->min_accept = min_accept > 0 ? min_accept : 4; + t->miss_limit = miss_limit > 0 ? miss_limit : 2; + t->cool_steps = cool_steps > 0 ? cool_steps : 48; + t->miss = 0; + t->cool = 0; +} + +int pn_spec_throttle_gate(pn_spec_throttle *t) +{ + if (t->cool > 0) { + t->cool--; + return 0; + } + return 1; +} + +void pn_spec_throttle_report(pn_spec_throttle *t, int accepted_drafts) +{ + if (accepted_drafts >= t->min_accept) { + t->miss = 0; + return; + } + if (++t->miss >= t->miss_limit) { + t->miss = 0; + t->cool = t->cool_steps; + } +} +``` + +Add an early-out to `pn_ngram_draft`: if the last token of the history is the same as the token before it, return 0 without scanning (a repeated token is a degenerate key). Change nothing else. + +Re-emit the complete updated contents of `src/core/ngram_draft.c`. +===PROMPT=== +I ran `read_file src/models/bonsai_forward.c` and got: + +```c +/** + * @file bonsai_forward.c + * @brief PrismML Bonsai-27B top-level forward/loader entry (PN_ARCH_BONSAI). + * + * This is Bonsai's OWN entry file, kept physically separate from the shipped + * Nex-N2 forward (qwen35moe_forward*.c). It owns the pn_forward_fn/loader/state + * seam for PN_ARCH_BONSAI and DELEGATES the heavy lifting to the shared + * qwen35moe machinery: Bonsai is the SAME hybrid GDN + sparse-full-attention + * family (general.architecture="qwen35"), differing only in a DENSE SwiGLU FFN + * (no MoE) and native-ternary (Q2_0_g128 / type-142) weights. + * + * Why delegate instead of clone: the hybrid forward is ~7.4k lines of shared, + * die-scratch-heavy CUDA orchestration (GDN block, flash-attn, Q4 KV, RoPE, + * RMSNorm, the ternary fc_matvec_q2_0). Cloning it to strip MoE -> dense would + * be a large correctness/drift risk (the exact hazard the bring-up flagged). + * Instead, the ONE Bonsai-specific behavior — the dense SwiGLU FFN block — is a + * config-driven branch inside the shared per-layer FFN helper, selected by + * cfg->dense_ffn (set at load from the GGUF metadata). The shipped Nex-N2 (qwen35moe, + * dense_ffn=0) never takes that branch and is byte-identical. Bonsai therefore + * has its own arch identity + its own forward entry file, without a risky clone. + */ + +#include "bonsai.h" +#include "qwen35moe.h" + +#include + +int pn_bonsai_is_bonsai_gguf(pn_gguf_file *gguf) +{ + if (!gguf) + return 0; + const char *arch = pn_gguf_get_str(gguf, "general.architecture"); + if (!arch || strcmp(arch, "qwen35") != 0) + return 0; + /* Dense FFN (Bonsai) vs routed MoE (Nex-N2): a plain per-layer ffn_gate is + * present and the merged expert tensor is absent. */ + if (pn_gguf_find_tensor(gguf, "blk.0.ffn_gate_exps.weight") != NULL) + return 0; + if (pn_gguf_find_tensor(gguf, "blk.0.ffn_gate.weight") == NULL) + return 0; + return 1; +} + +void *pn_bonsai_load(pn_gguf_file *gguf, pn_config *config) +{ + /* The shared loader detects the dense-FFN variant from the metadata + * (expert_count absent + ffn_gate present) and sets cfg->dense_ffn. */ + return pn_qwen35moe_load(gguf, config); +} + +tm_tensor *pn_bonsai_forward(pn_model *model, uint32_t token, pn_state *state) +{ + return pn_qwen35moe_forward(model, token, state); +} + +#ifdef USE_CUDA +tm_tensor *pn_bonsai_forward_cuda(pn_model *model, uint32_t token, + pn_state *state) +{ + return pn_qwen35moe_forward_cuda(model, token, state); +} + +int pn_bonsai_gpu_upload(void *model) +{ + return pn_qwen35moe_gpu_upload((pn_qwen35moe_model *)model); +} + +int pn_bonsai_model_gpu_loaded(const void *model) +{ + return pn_qwen35moe_model_gpu_loaded((const pn_qwen35moe_model *)model); +} +#endif + +void *pn_bonsai_state_new(void *weights) +{ + return pn_qwen35moe_state_new(weights); +} + +void pn_bonsai_state_free(void *state) +{ + pn_qwen35moe_state_free(state); +} + +void pn_bonsai_state_reset(void *state) +{ + pn_qwen35moe_state_reset(state); +} + +void pn_bonsai_free(void *model) +{ + pn_qwen35moe_free((pn_qwen35moe_model *)model); +} + +pn_gguf_file *pn_bonsai_gguf(const void *model) +{ + return pn_qwen35moe_gguf((const pn_qwen35moe_model *)model); +} + +void pn_bonsai_model_set_dies(void *model, const int *dies, int n) +{ + pn_qwen35moe_model_set_dies((pn_qwen35moe_model *)model, dies, n); +} +``` + +Add a short doc comment above each function describing what it returns. Change no code. + +Re-emit the complete updated contents of `src/models/bonsai_forward.c`. +===PROMPT=== +I ran `read_file src/serve/serve_registry.h` and got: + +```c +/** + * @file serve_registry.h + * @brief Serve-side dispatch self-registration lifecycle (Phase G0 wiring). + * Ties the register / heartbeat / deregister client (registry_client.h) + * into the live serve process: one register per served model_id at + * startup, a background heartbeat thread reporting live load from + * batch_sched, and a best-effort deregister on controlled shutdown. + * + * FULLY GATED on PANTHEON_REGISTRY_URL (via rc_enabled()): with it unset + * every function here is a no-op and the serve process is byte-for-byte + * unchanged (no registration, no heartbeat thread spawned, no network). + * + * Identity semantics (see registry_wire.h): the shared `model_id` is the + * served name clients request (mr_info.name) — two serve processes + * hosting the same weights advertise the SAME model_id and become a + * router replica pool; the `instance_id` is unique per serve process + * ("::"). + * + * @status G0 wiring. Live-registration behaviour is verified on a K80 host separately + * (no GPU/model on the Mac); the Mac build path exercises only the + * unset-env no-op and the compile. + * @issues None + * @todo None + */ + +#ifndef PN_SERVE_REGISTRY_H +#define PN_SERVE_REGISTRY_H + +#include "model_registry.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Startup registration: for each resident model in `reg`, build an rc_identity + * (model_id = served name, unique instance_id = "::", + * endpoint = "http://:", capacity from the model's batch_sched) and + * rc_register() it, recording it for later heartbeat + deregister. Then spawn a + * detached heartbeat thread that every RW_HEARTBEAT_SECONDS reports each + * instance's live load (bs_get_load under mr_acquire/mr_release). + * + * No-op (does not touch `reg`, spawns no thread) when rc_enabled() is false. + * Call once, after hs_listen succeeds and all models are loaded. + * + * `reg`, `host` must outlive the serve process (both are process-lifetime in + * pn_serve_main); the heartbeat thread borrows `reg` and never frees it. + * + * Thread-safe: no (call once at startup, before the accept loop). + */ +void serve_registry_start(mr_registry *reg, const char *host, int port); + +/** + * Register a model loaded AFTER serve_registry_start() (an /admin/models/load), + * using the host/port captured at start. Idempotent: a model whose instance is + * already registered (e.g. a second replica of the same served name) is skipped. + * No-op when rc_enabled() is false or serve_registry_start() has not run. The + * running heartbeat thread picks the new instance up on its next cycle. + * + * Thread-safe: yes (guards the instance table; runs from the request thread). + */ +void serve_registry_add(const char *model_id); + +/** + * Deregister a model unloaded via /admin/models/unload — but only if no replica + * of the same served name remains loaded (unloading one of two same-name + * replicas keeps the shared instance registered). No-op when disabled/unstarted. + * + * Thread-safe: yes. + */ +void serve_registry_remove(const char *model_id); + +/** + * Best-effort deregister of every instance registered by + * serve_registry_start(). Called from the SIGINT/SIGTERM shutdown handler so a + * clean stop drops the instances from the router immediately (the registry TTL + * also evicts them if this is skipped). No-op when rc_enabled() is false or + * nothing was registered. Each rc_deregister uses a short connect timeout. + * + * Safe to call from the controlled-shutdown signal handler (same trade as the + * CUDA teardown there: operator-initiated stop, not a crash mid-kernel). + */ +void serve_registry_deregister_all(void); + +#ifdef __cplusplus +} +#endif + +#endif /* PN_SERVE_REGISTRY_H */ +``` + +Add a `int max_batch;` field to the registry entry struct, documented in the same comment style as the neighbouring fields. Change nothing else. + +Re-emit the complete updated contents of `src/serve/serve_registry.h`. +===PROMPT=== +I ran `read_file src/core/qweight.h` and got: + +```c +#ifndef PN_QWEIGHT_H +#define PN_QWEIGHT_H + +#include "../formats/gguf.h" +#include "../formats/gguf_dequant.h" +#include + +/* + * Quantized weight matrix. + * Points directly into mmap'd GGUF data — zero-copy. + * pn_qmv() performs matvec by dequantizing 32 elements at a time. + * + * For ternary weights (PN_QUANT_TERNARY): data is packed 2-bit + * (4 elements/byte, encoding: 00=-1, 01=0, 10=+1). + * scales[] holds per-group float32 scales, laid out as + * scales[row * n_groups + group], where n_groups = ceil(cols/group_size). + */ + +typedef struct { + const void *data; /* Raw quantized blocks (mmap pointer or owned) */ + pn_gguf_quant type; /* Q4_0, Q8_0, ..., F32, PN_QUANT_TERNARY */ + int rows; /* Output dimension */ + int cols; /* Input dimension */ + size_t row_stride; /* Bytes per row */ + const float *scales; /* Per-group ternary scales (NULL for non-ternary) */ + int group_size; /* Elements per scale group (0 if not ternary) */ + void *gpu; /* Metal buffer of the weight's Q4_K blocks, or NULL. + Unconditional (not #ifdef USE_METAL) so the struct + layout never depends on the build flag — a + conditional field here silently corrupts adjacent + structs if any TU disagrees on the flag. Unused + (always NULL) in non-Metal builds. */ +} pn_qweight; + +/* Create a qweight from a named GGUF tensor. + * Sets rows/cols from tensor dims (reversed: GGUF innermost-first → C outermost-first). + * Returns 0 on success, -1 on failure. */ +int pn_qweight_from_gguf(pn_qweight *qw, pn_gguf_file *gguf, const char *name); + +/* Quantized matrix-vector multiply: out[rows] = W[rows, cols] @ vec[cols] + * Dequantizes row by row, 32 elements at a time. + * If a thread pool has been registered via pn_qmv_set_pool() and the matrix is + * large enough, the output-row loop is distributed across threads. Each output + * row is computed whole by a single thread, so the result is bit-identical to + * the single-threaded path (no cross-thread reduction). */ +void pn_qmv(const pn_qweight *w, const float *vec, float *out); + +/* Coalesced matvec for a group of independent weights sharing one parallel + * region. Computes out[i] = W[i] @ vec[i] for i in [0,n). All n matvecs are + * dispatched to the thread pool as a SINGLE fw_for over the concatenated row + * space, cutting per-matvec dispatch (condvar broadcast + join) overhead from n + * to 1. Each output row is still computed whole by one thread, so the result is + * bit-identical to calling pn_qmv(w[i], vec[i], out[i]) individually. Falls back + * to per-weight pn_qmv for ternary/GPU-resident weights. n up to 8. */ +void pn_qmv_group(const pn_qweight *const *w, const float *const *vec, + float *const *out, int n); + +/* Fused quantized matrix times B activation vectors (M=B "matmul"): + * out[b, r] = sum_c dequant(W[r, c]) * acts[b, c] for b in [0,B), r in [0,rows) + * acts is [B, cols] row-major, out is [B, rows] row-major (out[b*rows + r]). + * + * Each weight ROW is dequantized once into a small L1-resident scratch and + * dotted against all B activation vectors, so the quantized weight is read + * once and the dequant cost is amortized across the batch — unlike + * materializing the full F32 weight to DRAM before a BLAS GEMM. This is the + * decode-batch analog of pn_qmv's fused dequant-dot. Parallelized over rows via + * the registered pool; rows are disjoint so the result is order-independent. + * B up to PN_QMM_MAX_B (falls back to a per-b pn_qmv loop above that or for + * ternary/GPU-resident weights). */ +#define PN_QMM_MAX_B 64 +void pn_qmm(const pn_qweight *w, const float *acts, int B, float *out); + +/* Prefill-oriented batched matmul. Same [B,cols] acts -> [B,rows] out contract + * as pn_qmm (out[b*rows + r]), but dispatched in fixed row-blocks so each + * per-panel cblas_sgemm keeps an AMX-friendly N dimension even for large B + * (B = P, ~1200, at prefill). Unlike pn_qmm it never degrades to a per-b matvec + * loop for large B: the panel path handles any B, and the 64 cap in pn_qmm only + * exists to bound the flag-gated GPU decode accumulators. Dequantizes each + * L2-resident weight panel once and multiplies it against all B activation + * rows, so the quantized weight is read once and never round-trips DRAM as F32. + * The only numeric difference from a per-row dequant-dot is BLAS summation + * order, the same reassociation the decode-batch pn_qmm already relies on. */ +void pn_qmm_prefill(const pn_qweight *w, const float *acts, int B, float *out); + +/* Register the thread pool used by pn_qmv to parallelize the row loop. + * Forward-declared opaque pool type (foundry threadpool fw_pool). Passing NULL + * disables parallelization. No-op (other than storing NULL) when the engine is + * built without USE_THREADS. Safe to call repeatedly; not thread-safe to call + * concurrently with pn_qmv. */ +struct fw_pool; +void pn_qmv_set_pool(struct fw_pool *pool); + +/* Quantized dot product: dequant + dot for a single row. + * Used by pn_qmv inner loop and available for direct use. */ +float pn_qdot(const void *row_data, const float *vec, int n, pn_gguf_quant type); + +/* GPU (Metal) weight residency for the Q4_K batched-decode path. + * pn_qmv_gpu_upload copies a Q4_K weight's blocks into a GPU buffer (sets + * w->gpu); pn_qmm then runs the batched simdgroup GEMM on the GPU. All three + * are no-ops / return 0 when built without USE_METAL or for non-Q4_K weights. */ +int pn_qmv_gpu_available(void); +int pn_qmv_gpu_upload(pn_qweight *w); +void pn_qmv_gpu_free(pn_qweight *w); + +/* Enable/disable running batched decode (pn_qmm) on the GPU. Off by default: + * weights are GPU-uploaded for prefill, but GPU decode is slower than CPU AMX, + * so decode stays on the CPU unless this is explicitly enabled. */ +void pn_qmm_set_gpu_decode(int on); + +#endif /* PN_QWEIGHT_H */ +``` + +Rename the constant prefix `PN_QW_` to `PN_QWEIGHT_` everywhere it appears in this header, updating every use. Change nothing else. + +Re-emit the complete updated contents of `src/core/qweight.h`. diff --git a/tests/data/ngram_code_long_prompt.txt b/tests/data/ngram_code_long_prompt.txt new file mode 100644 index 0000000..d57c881 --- /dev/null +++ b/tests/data/ngram_code_long_prompt.txt @@ -0,0 +1,434 @@ +===SYSTEM=== +You are a coding agent. Apply the requested edit and re-emit the complete file in a single fenced code block. Do not explain. +===PROMPT=== +I ran `read_file src/serve/serve_prompt_tokens.c` and got: + +```c +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file serve_prompt_tokens.c + * @brief Implementation of the ChatML prompt tokenizer and the client-re-render + * projection (see serve_prompt_tokens.h). + * + * @status Complete. + * @issues None + * @todo None + */ + +#include "serve_prompt_tokens.h" + +#include +#include +#include + +#include "../core/bpe_pretokenize.h" +#include "../core/chat.h" +#include "../core/engine.h" +#include "json_reader.h" +#include "tokenizer/tokenizer.h" + +/* ============================================================ + * Token builder — mirrors src/core/chat.c's ChatML(+think) layout, but over a + * full OpenAI messages[] array (multi-turn history), reusing the same + * tokenization primitives (special-token lookup + qwen35 BPE text encode). + * ============================================================ */ + +typedef struct { + uint32_t *tokens; + int len; + int cap; +} token_builder; + +static bool tb_init(token_builder *tb) +{ + tb->cap = 256; + tb->len = 0; + tb->tokens = malloc((size_t)tb->cap * sizeof(uint32_t)); + return tb->tokens != NULL; +} + +static void tb_push(token_builder *tb, uint32_t id) +{ + if (tb->len >= tb->cap) { + tb->cap *= 2; + uint32_t *grown = realloc(tb->tokens, (size_t)tb->cap * sizeof(uint32_t)); + if (!grown) return; + tb->tokens = grown; + } + tb->tokens[tb->len++] = id; +} + +static void tb_push_special(token_builder *tb, ft_tokenizer *tok, const char *s) +{ + int id = ft_token_to_id(tok, s); + if (id >= 0) tb_push(tb, (uint32_t)id); +} + +/* Encode plain text with the model's tokenizer (qwen35 BPE for the mini, else + * the SentencePiece path) and append the ids. */ +static void tb_push_text(token_builder *tb, ft_tokenizer *tok, const char *text) +{ + if (!text || !*text) return; + int n = 0; + int *ids = (ft_get_model_type(tok) == 0) + ? pn_bpe_encode(tok, text, &n) + : ft_encode(tok, text, &n); + if (!ids) return; + for (int i = 0; i < n; i++) + if (ids[i] >= 0) tb_push(tb, (uint32_t)ids[i]); + free(ids); +} + +/* The tool-call wrapper. The vocab holds these as SINGLE special ids + * (nex-n2-mini: "" is id 248058), and tb_push_text would byte-encode + * the rendered STRING into the four ordinary ids "<","tool","_call",">". Both + * are legal tokenizations of the same characters, so the ONLY thing that matters + * is that the prompt builder and the re-render projection pick the same one — + * which they do by both going through sg_encode_assistant_content. Only + * assistant turns: a user/tool message that happens to contain the literal text + * stays ordinary text. */ +static const char *const TOOL_WRAPPER_MARKERS[] = { "", "" }; +#define TOOL_WRAPPER_MARKER_COUNT \ + (sizeof(TOOL_WRAPPER_MARKERS) / sizeof(TOOL_WRAPPER_MARKERS[0])) + +/* Encode assistant-turn content, emitting any tool-call wrapper marker as its + * special id. A marker the vocab does not know (id < 0) is left alone and + * encodes as ordinary text, so a model without these tokens tokenizes + * byte-identically to a plain tb_push_text. */ +static void tb_push_assistant_content(token_builder *tb, ft_tokenizer *tok, + const char *content) +{ + if (!content || !*content) return; + const char *pos = content; + while (*pos) { + const char *best = NULL; + size_t best_len = 0; + int best_id = -1; + for (size_t i = 0; i < TOOL_WRAPPER_MARKER_COUNT; i++) { + int id = ft_token_to_id(tok, TOOL_WRAPPER_MARKERS[i]); + if (id < 0) continue; + const char *at = strstr(pos, TOOL_WRAPPER_MARKERS[i]); + if (at && (!best || at < best)) { + best = at; + best_len = strlen(TOOL_WRAPPER_MARKERS[i]); + best_id = id; + } + } + if (!best) { + tb_push_text(tb, tok, pos); + return; + } + if (best > pos) { + size_t head_len = (size_t)(best - pos); + char *head = malloc(head_len + 1); + if (head) { + memcpy(head, pos, head_len); + head[head_len] = '\0'; + tb_push_text(tb, tok, head); + free(head); + } + } + tb_push(tb, (uint32_t)best_id); + pos = best + best_len; + } +} + +/* Emit one "<|im_start|>{role}\n{content}<|im_end|>\n" block. An assistant + * turn's content goes through tb_push_assistant_content so a rendered tool call + * reproduces the ids the projection recorded (see TOOL_WRAPPER_MARKERS). */ +static void tb_push_turn(token_builder *tb, ft_tokenizer *tok, + const char *role, const char *content) +{ + char header[64]; + snprintf(header, sizeof(header), "%s\n", role); + tb_push_special(tb, tok, "<|im_start|>"); + tb_push_text(tb, tok, header); + if (strcmp(role, "assistant") == 0) + tb_push_assistant_content(tb, tok, content); + else + tb_push_text(tb, tok, content); + tb_push_special(tb, tok, "<|im_end|>"); + tb_push_text(tb, tok, "\n"); +} + +/* Whitespace the streaming emitter left-trims off the head of each channel and + * that a resent think block therefore cannot recover from the text alone (see + * serve_emitter.c's em_emit). These are the canonical Qwen/N2 separators the + * model itself emits: "\n" right after the forced , "\n\n" after the + * "" close. */ +static const char THINK_LEAD_WS[] = "\n"; +static const char THINK_TAIL_WS[] = "\n\n"; + +/* Emit the think block of a HISTORY assistant turn — "{reasoning}" + * plus the separators the emitter trimmed — for a client that resent the + * reasoning verbatim. and go in as SPECIAL ids: tb_push_text + * would byte-encode them as ordinary text and never reach id 248068. + * + * This is the ONE definition of that span. Both the prompt builder (below) and + * the reasoning-bearing resume projection (sg_encode_history_think_block) go + * through it, for the same reason tb_push_assistant_content is shared — a + * second implementation is free to drift, and a one-token drift costs a full + * re-prefill. */ +static void tb_push_history_think_block(token_builder *tb, ft_tokenizer *tok, + const char *reasoning) +{ + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, THINK_LEAD_WS); + tb_push_text(tb, tok, reasoning); + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, THINK_TAIL_WS); +} + +/* Emit one assistant turn whose reasoning the client resent verbatim: + * "<|im_start|>assistant\n{reasoning}{content}<|im_end|>\n". */ +static void tb_push_assistant_think_turn(token_builder *tb, ft_tokenizer *tok, + const char *reasoning, + const char *content) +{ + tb_push_special(tb, tok, "<|im_start|>"); + tb_push_text(tb, tok, "assistant\n"); + tb_push_history_think_block(tb, tok, reasoning); + tb_push_assistant_content(tb, tok, content); + tb_push_special(tb, tok, "<|im_end|>"); + tb_push_text(tb, tok, "\n"); +} + +/* Emit a tool-result turn per the model's chat_template: a tool message is fed + * back inside a wrapper on a user turn, so multi-step tool loops + * reconstruct the prompt shape the model was trained on. */ +static void tb_push_tool_response(token_builder *tb, ft_tokenizer *tok, + const char *content) +{ + tb_push_special(tb, tok, "<|im_start|>"); + tb_push_text(tb, tok, "user\n\n"); + tb_push_text(tb, tok, content); + tb_push_text(tb, tok, "\n"); + tb_push_special(tb, tok, "<|im_end|>"); + tb_push_text(tb, tok, "\n"); +} + +bool sg_no_think_enabled(void) +{ + static int cached = -1; + if (cached < 0) { + const char *e = getenv("PANTHEON_NO_THINK"); + cached = (e && e[0] == '1') ? 1 : 0; + } + return cached != 0; +} + +bool sg_chat_style_is_think(int chat_style) +{ + return chat_style == PN_CHAT_CHATML_THINK || + chat_style == PN_CHAT_CHATML_THINK_NL; +} + +bool sg_thinking_suppressed(sg_thinking_mode mode) +{ + if (mode == SG_THINKING_OFF) return true; + if (mode == SG_THINKING_ON) return false; + return sg_no_think_enabled(); /* SG_THINKING_DEFAULT → the env decides */ +} + +/* Push the forced generation prefill for the assistant turn about to be + * generated — the tokens the model did not choose. Returns how many. */ +static int tb_push_generation_prefill(token_builder *tb, ft_tokenizer *tok, + int chat_style, sg_thinking_mode thinking) +{ + int before = tb->len; + if (sg_chat_style_is_think(chat_style) && sg_thinking_suppressed(thinking)) { + /* No-Think: prefill a CLOSED empty think block so a hybrid + * Think/No-Think model (MiniCPM5, saber-orch) skips reasoning and acts + * DIRECTLY. These four pushes are byte-for-byte the Qwen3.x template's + * enable_thinking=false serialization ("\n\n\n\n"), which + * is the only reasoning control that family's template exposes — hence a + * BINARY switch and not a graded effort level. Requested per-request via + * `enable_thinking`, or process-wide via PANTHEON_NO_THINK. */ + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, "\n\n"); + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, "\n\n"); + } else if (chat_style == PN_CHAT_CHATML_THINK) { + tb_push_special(tb, tok, ""); + } else if (chat_style == PN_CHAT_CHATML_THINK_NL) { + /* Bonsai-27B: the model's own template prefills '\n' — the + * newline is part of the trained prefill. Prefilling bare + * leaves "\n\n\n\n" (the template's NON-thinking serialization) + * as the natural greedy continuation, so the model deterministically + * closes the think block empty (Phase 3b bug). */ + tb_push_special(tb, tok, ""); + tb_push_text(tb, tok, "\n"); + } + return tb->len - before; +} + +uint32_t *sg_build_prompt_tokens(struct pn_model *model, + const sg_message *messages, int n_messages, + sg_thinking_mode thinking, + int *n_out, int *sys_len_out, + int *think_prefill_out) +{ + ft_tokenizer *tok = model->tokenizer; + token_builder tb; + if (sys_len_out) *sys_len_out = 0; + if (think_prefill_out) *think_prefill_out = 0; + if (!tb_init(&tb)) return NULL; + + /* Templates that lead with {{ bos_token }} (e.g. MiniCPM5) need BOS at + * position 0 — the model was trained with it and degenerates to whitespace + * without it. Detected at load (model->chat_add_bos). */ + if (model->chat_add_bos) { + int bid = ft_bos_id(tok); + if (bid >= 0) tb_push(&tb, (uint32_t)bid); + } + + for (int i = 0; i < n_messages; i++) { + const char *role = messages[i].role ? messages[i].role : "user"; + const char *content = messages[i].content ? messages[i].content : ""; + const char *reasoning = messages[i].reasoning; + if (strcmp(role, "tool") == 0) + tb_push_tool_response(&tb, tok, content); + else if (reasoning && *reasoning && strcmp(role, "assistant") == 0 && + sg_chat_style_is_think(model->chat_style)) + tb_push_assistant_think_turn(&tb, tok, reasoning, content); + else + tb_push_turn(&tb, tok, role, content); + if (i == 0 && sys_len_out && strcmp(role, "system") == 0) + *sys_len_out = tb.len; + } + + tb_push_special(&tb, tok, "<|im_start|>"); + tb_push_text(&tb, tok, "assistant\n"); + int prefill_n = tb_push_generation_prefill(&tb, tok, model->chat_style, + thinking); + if (think_prefill_out) *think_prefill_out = prefill_n; + *n_out = tb.len; + return tb.tokens; +} + +uint32_t *sg_encode_history_think_block(struct pn_model *model, + const char *reasoning, int *n_out) +{ + *n_out = 0; + if (!model || !reasoning || !reasoning[0]) return NULL; + /* Match sg_build_prompt_tokens exactly: a non-think chat style renders an + * assistant history turn WITHOUT a think block whatever `reasoning` says, so + * projecting one here would predict a prompt the builder never produces. */ + if (!sg_chat_style_is_think(model->chat_style)) return NULL; + token_builder tb; + if (!tb_init(&tb)) return NULL; + tb_push_history_think_block(&tb, model->tokenizer, reasoning); + if (tb.len <= 0) { + free(tb.tokens); + return NULL; + } + *n_out = tb.len; + return tb.tokens; +} + +uint32_t *sg_encode_assistant_content(struct pn_model *model, const char *text, + int *n_out) +{ + *n_out = 0; + if (!model || !text || !text[0]) return NULL; + token_builder tb; + if (!tb_init(&tb)) return NULL; + tb_push_assistant_content(&tb, model->tokenizer, text); + if (tb.len <= 0) { + free(tb.tokens); + return NULL; + } + *n_out = tb.len; + return tb.tokens; +} + +/* ============================================================ + * Client-re-render projection + * ============================================================ */ + +/* Re-render the serve's OWN parsed calls into assistant-history syntax, through + * the very function the next request's `tool_calls` array will go through. + * + * The detour via JSON is deliberate and is the whole point: the calls are put + * back into the exact OpenAI wire shape the reply is about to carry, then handed + * to tc_render_assistant_tool_calls. Rendering them any other way would be a + * SECOND implementation of the history syntax, free to drift from the one that + * actually runs next turn — and a one-token drift costs a full re-prefill. + * Returns an owned string, or NULL on OOM / no calls. */ +static char *render_own_calls_as_history(const tc_tool_call *calls, int n, + bool content_empty, tc_dialect dialect) +{ + if (!calls || n <= 0) return NULL; + json_writer jw; + if (!jw_init(&jw)) return NULL; + jw_raw(&jw, "["); + for (int i = 0; i < n; i++) { + if (i) jw_raw(&jw, ","); + jw_raw(&jw, "{\"id\":"); + jw_string(&jw, calls[i].id ? calls[i].id : ""); + jw_raw(&jw, ",\"type\":\"function\",\"function\":{\"name\":"); + jw_string(&jw, calls[i].name ? calls[i].name : ""); + jw_raw(&jw, ",\"arguments\":"); + jw_string(&jw, calls[i].arguments ? calls[i].arguments : "{}"); + jw_raw(&jw, "}}"); + } + jw_raw(&jw, "]"); + char *rendered = NULL; + if (!jw.oom) { + jr_value *dom = jr_parse(jw.data); + rendered = tc_render_assistant_tool_calls(dom, content_empty, dialect); + jr_free(dom); + } + jw_free(&jw); + return rendered; +} + +/* Concatenate the turn's content and its rendered calls exactly as + * combine_assistant_tool_calls (openai_chat.c) will next turn. Returns an owned + * string, or NULL on OOM. */ +static char *join_content_and_calls(const char *content, const char *calls_xml) +{ + size_t clen = content ? strlen(content) : 0; + size_t xlen = calls_xml ? strlen(calls_xml) : 0; + char *joined = malloc(clen + xlen + 1); + if (!joined) return NULL; + if (clen) memcpy(joined, content, clen); + if (xlen) memcpy(joined + clen, calls_xml, xlen); + joined[clen + xlen] = '\0'; + return joined; +} + +uint32_t *sg_project_client_rerender(struct pn_model *model, + const char *content, const char *tool_raw, + const void *tools, tc_dialect dialect, + int *n_out) +{ + *n_out = 0; + if (!model) return NULL; + if (!content) content = ""; + + char *calls_xml = NULL; + if (tool_raw && tool_raw[0]) { + tc_tool_call *calls = NULL; + int n_calls = 0; + if (tc_parse_tool_calls(tool_raw, (const jr_value *)tools, dialect, + &calls, &n_calls) == 0 && n_calls > 0) + calls_xml = render_own_calls_as_history(calls, n_calls, + content[0] == '\0', dialect); + tc_tool_calls_free(calls, n_calls); + /* A diverted block that will not parse back is a turn whose re-render we + * cannot predict — disable the rung rather than record a guess. */ + if (!calls_xml) return NULL; + } + + char *joined = join_content_and_calls(content, calls_xml); + free(calls_xml); + if (!joined) return NULL; + uint32_t *ids = sg_encode_assistant_content(model, joined, n_out); + free(joined); + return ids; +} +``` + +Rename every occurrence of the identifier `n_tokens` to `token_count` throughout this file, and leave everything else exactly as it is. Re-emit the complete file. diff --git a/tests/data/ngram_prose_prompts.txt b/tests/data/ngram_prose_prompts.txt new file mode 100644 index 0000000..c5e15e4 --- /dev/null +++ b/tests/data/ngram_prose_prompts.txt @@ -0,0 +1,14 @@ +===SYSTEM=== +You are a writer. Produce the requested prose directly, with no preamble and no commentary. +===PROMPT=== +Write the opening of a short story about a lighthouse keeper who discovers that the ship she has been signalling for thirty years does not exist. +===PROMPT=== +Write a short essay arguing that the most important property of a tool is not its power but how quickly a person can tell that it has gone wrong. +===PROMPT=== +Describe a city at four in the morning from the point of view of someone who has just finished a night shift and is not ready to go home. +===PROMPT=== +Write a letter from an engineer to the person who will maintain her work after she is gone. She does not know who they are. She is not sentimental about it. +===PROMPT=== +Write a scene in which two people who have known each other for a long time have an argument about something neither of them cares about, because the thing they do care about cannot be said. +===PROMPT=== +Write a short piece of nature writing about a river in a place where the river is no longer the most important thing that happens there. diff --git a/tests/dispatch/e2e.sh b/tests/dispatch/e2e.sh new file mode 100755 index 0000000..d21e402 --- /dev/null +++ b/tests/dispatch/e2e.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +# +# e2e.sh — dispatch-layer end-to-end test (Phase G1+G2), Mac-only, no GPU/CUDA. +# +# Starts the real `pantheon router`, spins up stub upstreams speaking minimal +# OpenAI /v1/chat/completions + SSE, registers them mirroring the live fleet +# (saber-orch, qwen36-base, nex-n2-mini, and TWO ornith instances), then asserts: +# 1. /v1/models is deduped (ornith once, live=2); /registry/fleet correct. +# 2. TTL eviction: stop heartbeating an instance -> it drops within ~TTL. +# 3. Least-busy: the freer ornith instance gets the prompt; round-robin on ties. +# 4. Failover: kill the chosen upstream -> router retries the other; unknown -> 503. +# 5. SSE pass-through: streamed chunks arrive unbuffered (staggered timestamps). +# +# Exit 0 = all pass. Requires: the `pantheon` binary (built), python3, curl. +set -u + +HERE="$(cd "$(dirname "$0")" && pwd)" +ROOT="$(cd "$HERE/../.." && pwd)" +PANTHEON="$ROOT/pantheon" +STUB="$HERE/stub_upstream.py" + +ROUTER_PORT=18100 +REG="http://127.0.0.1:$ROUTER_PORT" +PIDS=() +FAIL=0 + +pass() { echo " PASS: $1"; } +fail() { echo " FAIL: $1"; FAIL=1; } + +cleanup() { + for p in "${PIDS[@]:-}"; do kill "$p" 2>/dev/null; done + wait 2>/dev/null +} +trap cleanup EXIT + +wait_port() { # host port + for _ in $(seq 1 50); do + if (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; then exec 3>&- 3<&-; return 0; fi + sleep 0.1 + done + return 1 +} + +register() { # model instance port capacity [transport] + curl -s -X POST "$REG/registry/register" -d \ + "{\"model_id\":\"$1\",\"instance_id\":\"$2\",\"node_id\":\"node0\",\"endpoint\":\"http://127.0.0.1:$3\",\"transport\":\"${5:-local}\",\"capacity\":$4}" >/dev/null +} + +heartbeat() { # instance active queue free healthy + curl -s -X POST "$REG/registry/heartbeat" -d \ + "{\"instance_id\":\"$1\",\"load\":{\"active_slots\":$2,\"queue_depth\":$3,\"free_slots\":$4},\"healthy\":$5}" >/dev/null +} + +# --------------------------------------------------------------------------- +[ -x "$PANTHEON" ] || { echo "FATAL: build the pantheon binary first ($PANTHEON)"; exit 2; } + +echo "== dispatch-layer e2e ==" + +# 1. Start the router. +"$PANTHEON" router --host 127.0.0.1 --port "$ROUTER_PORT" >/tmp/e2e_router.log 2>&1 & +PIDS+=($!) +wait_port "$ROUTER_PORT" || { echo "FATAL: router did not start"; cat /tmp/e2e_router.log; exit 2; } + +# 2. Start stub upstreams (ports mirror the live fleet 8201-8205 shifted +10000). +python3 "$STUB" --port 18201 --tag saber >/dev/null 2>&1 & PIDS+=($!) +python3 "$STUB" --port 18202 --tag qwen36 >/dev/null 2>&1 & PIDS+=($!) +python3 "$STUB" --port 18203 --tag nex >/dev/null 2>&1 & PIDS+=($!) +python3 "$STUB" --port 18204 --tag ornithA >/dev/null 2>&1 & PIDS+=($!) +python3 "$STUB" --port 18205 --tag ornithB >/dev/null 2>&1 & PIDS+=($!) +for p in 18201 18202 18203 18204 18205; do wait_port "$p" || { echo "FATAL: stub $p"; exit 2; }; done + +# 3. Register the fleet (ornith = TWO instances of ONE model_id). +register saber-orch saber-1 18201 4 +register qwen36-base qwen36-1 18202 4 +register nex-n2-mini nex-1 18203 4 +register ornith ornith-A 18204 4 +register ornith ornith-B 18205 4 +# initial heartbeats so both ornith are live +heartbeat ornith-A 0 0 4 true +heartbeat ornith-B 0 0 4 true + +# --- ASSERT 1: /v1/models deduped ----------------------------------------- +MODELS="$(curl -s "$REG/v1/models")" +N_MODELS="$(grep -o '"object":"model"' <<<"$MODELS" | wc -l | tr -d ' ')" +N_ORNITH="$(grep -o '"id":"ornith"' <<<"$MODELS" | wc -l | tr -d ' ')" +[ "$N_MODELS" = "4" ] && pass "/v1/models has 4 deduped models" || fail "/v1/models model count = $N_MODELS (want 4): $MODELS" +[ "$N_ORNITH" = "1" ] && pass "ornith appears once" || fail "ornith count = $N_ORNITH (want 1)" +grep -q '"live":2' <<<"$MODELS" && pass "ornith live=2" || fail "ornith live count wrong: $MODELS" + +FLEET="$(curl -s "$REG/registry/fleet")" +grep -q '"instance_id":"ornith-A"' <<<"$FLEET" && grep -q '"instance_id":"ornith-B"' <<<"$FLEET" \ + && pass "/registry/fleet lists both ornith instances" || fail "fleet missing an ornith instance: $FLEET" +grep -q '"total_free_slots":8' <<<"$FLEET" && pass "ornith total_free_slots=8" || fail "ornith total_free_slots wrong: $FLEET" + +# --- ASSERT 3: least-busy -------------------------------------------------- +# Make ornith-A clearly freer (4 free vs 1 free) -> all prompts route to A. +heartbeat ornith-A 0 0 4 true +heartbeat ornith-B 3 0 1 true +HITS_A=0; HITS_B=0 +for _ in $(seq 1 6); do + R="$(curl -s -X POST "$REG/v1/chat/completions" -d '{"model":"ornith","messages":[{"role":"user","content":"hi"}]}')" + case "$R" in + *answered-by:ornithA*) HITS_A=$((HITS_A+1));; + *answered-by:ornithB*) HITS_B=$((HITS_B+1));; + esac +done +[ "$HITS_A" = "6" ] && [ "$HITS_B" = "0" ] && pass "least-busy routed all 6 to freer ornith-A" \ + || fail "least-busy split A=$HITS_A B=$HITS_B (want 6/0)" + +# round-robin on ties: equalize load, expect BOTH to be hit +heartbeat ornith-A 0 0 4 true +heartbeat ornith-B 0 0 4 true +HITS_A=0; HITS_B=0 +for _ in $(seq 1 8); do + R="$(curl -s -X POST "$REG/v1/chat/completions" -d '{"model":"ornith","messages":[]}')" + case "$R" in *ornithA*) HITS_A=$((HITS_A+1));; *ornithB*) HITS_B=$((HITS_B+1));; esac +done +[ "$HITS_A" -gt 0 ] && [ "$HITS_B" -gt 0 ] && pass "round-robin spread ties across A=$HITS_A B=$HITS_B" \ + || fail "round-robin did not spread (A=$HITS_A B=$HITS_B)" + +# --- ASSERT 3b: session affinity + X-Session-Id forwarding (PC0.2) --------- +# Both ornith replicas equally free: WITHOUT a session id, ties round-robin +# (asserted above). WITH a session id, every turn must stick to the SAME +# instance (its prefix cache holds the session's KV), and the router must +# forward X-Session-Id upstream (the stub echoes what it received). +heartbeat ornith-A 0 0 4 true +heartbeat ornith-B 0 0 4 true +SID="client-e2e-42" +FIRST="" +STICKY=1 +FORWARDED=1 +for _ in $(seq 1 6); do + R="$(curl -s -X POST "$REG/v1/chat/completions" \ + -H "X-Session-Id: $SID" \ + -d '{"model":"ornith","messages":[{"role":"user","content":"hi"}]}')" + case "$R" in + *answered-by:ornithA*) WHO=ornithA;; + *answered-by:ornithB*) WHO=ornithB;; + *) WHO=none;; + esac + case "$R" in *"session:$SID"*) : ;; *) FORWARDED=0;; esac + [ -z "$FIRST" ] && FIRST="$WHO" + [ "$WHO" = "$FIRST" ] || STICKY=0 +done +[ "$STICKY" = "1" ] && [ "$FIRST" != "none" ] \ + && pass "session affinity: all 6 turns of $SID stuck to $FIRST" \ + || fail "session affinity broke (first=$FIRST sticky=$STICKY)" +[ "$FORWARDED" = "1" ] && pass "X-Session-Id forwarded upstream" \ + || fail "X-Session-Id not forwarded to the upstream" + +# A DIFFERENT session may land anywhere, but must also be self-consistent. +SID2="client-e2e-99" +R2="$(curl -s -X POST "$REG/v1/chat/completions" -H "X-Session-Id: $SID2" \ + -d '{"model":"ornith","messages":[]}')" +case "$R2" in + *answered-by:ornith*) pass "a new session routes + records";; + *) fail "new session did not route: $R2";; +esac + +# --- ASSERT 4: failover + unknown model ------------------------------------ +# Kill ornith-B's stub and replace it with a DEAD upstream (accepts+closes). +# Bias so B is chosen first, prove the router fails over to A. +kill "${PIDS[5]}" 2>/dev/null # ornith-B stub (index: router=0, 5 stubs =1..5 -> B is [5]) +wait_port 18205 >/dev/null 2>&1 || true +sleep 0.2 +python3 "$STUB" --port 18205 --tag ornithB --mode dead >/dev/null 2>&1 & PIDS+=($!) +wait_port 18205 || { echo "FATAL: dead stub 18205"; exit 2; } +# Make B look freer so it is the PRIMARY pick; A is the failover target. +heartbeat ornith-A 3 0 1 true +heartbeat ornith-B 0 0 4 true +R="$(curl -s -X POST "$REG/v1/chat/completions" -d '{"model":"ornith","messages":[]}')" +case "$R" in + *answered-by:ornithA*) pass "failover: dead primary (B) -> router retried A";; + *) fail "failover did not reach a healthy instance: $R";; +esac + +# unknown model -> 503 +CODE="$(curl -s -o /dev/null -w '%{http_code}' -X POST "$REG/v1/chat/completions" -d '{"model":"nonesuch","messages":[]}')" +[ "$CODE" = "503" ] && pass "unknown model -> 503" || fail "unknown model returned $CODE (want 503)" + +# --- ASSERT 5: SSE pass-through (unbuffered) ------------------------------- +# Stream from saber-orch; record arrival timestamps of the 3 chunks. If the +# router buffered, they'd all arrive together; the stub sleeps 0.15s between +# chunks, so an unbuffered relay shows staggered arrivals spanning >0.2s. +STAMPS="$(curl -s -N -X POST "$REG/v1/chat/completions" \ + -d '{"model":"saber-orch","stream":true,"messages":[]}' \ + | while IFS= read -r line; do + [ -n "$line" ] && printf '%s %s\n' "$(python3 -c 'import time;print(f"{time.time():.3f}")')" "$line" + done)" +N_CHUNKS="$(grep -c 'chat.completion.chunk' <<<"$STAMPS")" +grep -q '\[DONE\]' <<<"$STAMPS" && DONE_OK=1 || DONE_OK=0 +FIRST_TS="$(grep 'chat.completion.chunk' <<<"$STAMPS" | head -1 | awk '{print $1}')" +LAST_TS="$(grep 'chat.completion.chunk' <<<"$STAMPS" | tail -1 | awk '{print $1}')" +SPAN="$(python3 -c "print(round(${LAST_TS:-0}-${FIRST_TS:-0},3))" 2>/dev/null || echo 0)" +[ "$N_CHUNKS" = "3" ] && pass "SSE relayed all 3 chunks" || fail "SSE chunk count = $N_CHUNKS (want 3)" +[ "$DONE_OK" = "1" ] && pass "SSE terminal [DONE] forwarded" || fail "SSE [DONE] not forwarded" +python3 -c "import sys; sys.exit(0 if ${SPAN:-0} > 0.2 else 1)" \ + && pass "SSE arrived unbuffered (chunk span ${SPAN}s > 0.2s)" \ + || fail "SSE chunks arrived together (span ${SPAN}s) -> buffered" + +# --- ASSERT 2: TTL eviction ------------------------------------------------ +# Register a throwaway instance, stop heartbeating it, wait > TTL, expect drop. +register ttl-model ttl-1 18201 1 +heartbeat ttl-1 0 0 1 true +grep -q '"id":"ttl-model"' <<<"$(curl -s "$REG/v1/models")" && pass "TTL: instance present before expiry" || fail "TTL: instance not present" +echo " (waiting ~17s for TTL eviction — TTL=15s, sweep=2s ...)" +sleep 17 +MODELS2="$(curl -s "$REG/v1/models")" +grep -q '"id":"ttl-model"' <<<"$MODELS2" && fail "TTL: instance NOT evicted after expiry" || pass "TTL: instance evicted after ~TTL" + +# --------------------------------------------------------------------------- +echo +if [ "$FAIL" = "0" ]; then echo "ALL DISPATCH E2E ASSERTIONS PASSED"; else echo "SOME DISPATCH E2E ASSERTIONS FAILED"; fi +exit "$FAIL" diff --git a/tests/dispatch/stub_upstream.py b/tests/dispatch/stub_upstream.py new file mode 100755 index 0000000..84eea1d --- /dev/null +++ b/tests/dispatch/stub_upstream.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: AGPL-3.0-or-later +# Copyright (C) 2026 David (Corey) Herrera +""" +stub_upstream.py — a tiny OpenAI-compatible upstream for the dispatch-layer +end-to-end test. Speaks just enough of POST /v1/chat/completions to prove the +router proxies + streams correctly: + + - Non-streaming: returns a single chat.completion JSON whose content encodes + which upstream answered (so the test can assert *which* instance was picked). + - Streaming (stream:true): emits SSE `data:` chunks then `data: [DONE]`, with a + short inter-chunk sleep so a BUFFERING proxy would be caught (the test reads + chunks with timestamps). + +Usage: + stub_upstream.py --port N --tag NAME [--mode dead] + +--mode dead: accept the TCP connection but immediately close without replying, +to exercise the router's failover path (upstream error before any body). +""" +import argparse +import json +import socket +import sys +import time +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +TAG = "stub" +MODE = "ok" + + +class Handler(BaseHTTPRequestHandler): + def log_message(self, *a): # silence + pass + + def _read_body(self): + n = int(self.headers.get("Content-Length", 0)) + return self.rfile.read(n) if n else b"" + + def do_POST(self): + body = self._read_body() + try: + req = json.loads(body or b"{}") + except Exception: + req = {} + streaming = bool(req.get("stream")) + + if streaming: + self.send_response(200) + self.send_header("Content-Type", "text/event-stream") + self.send_header("Cache-Control", "no-cache") + self.end_headers() + # Emit a few chunks with a gap so buffering would be detectable. + for i in range(3): + chunk = { + "id": "chatcmpl-stub", + "object": "chat.completion.chunk", + "model": req.get("model", "?"), + "choices": [{ + "index": 0, + "delta": {"content": f"{TAG}-{i} "}, + "finish_reason": None, + }], + } + self.wfile.write(("data: " + json.dumps(chunk) + "\n\n").encode()) + self.wfile.flush() + time.sleep(0.15) + self.wfile.write(b"data: [DONE]\n\n") + self.wfile.flush() + else: + # Echo the forwarded X-Session-Id so the test can assert the router + # threads it upstream (where the serve keys its prefix cache). + seen_sid = self.headers.get("X-Session-Id", "") + resp = { + "id": "chatcmpl-stub", + "object": "chat.completion", + "model": req.get("model", "?"), + "choices": [{ + "index": 0, + "message": {"role": "assistant", + "content": f"answered-by:{TAG} session:{seen_sid}"}, + "finish_reason": "stop", + }], + "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}, + } + payload = json.dumps(resp).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + +def run_dead(port): + """Accept connections and immediately close — no HTTP reply (failover test).""" + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("127.0.0.1", port)) + s.listen(16) + while True: + conn, _ = s.accept() + try: + conn.recv(4096) + except Exception: + pass + conn.close() + + +def main(): + global TAG, MODE + ap = argparse.ArgumentParser() + ap.add_argument("--port", type=int, required=True) + ap.add_argument("--tag", default="stub") + ap.add_argument("--mode", default="ok", choices=["ok", "dead"]) + args = ap.parse_args() + TAG = args.tag + MODE = args.mode + + if MODE == "dead": + run_dead(args.port) + return + + srv = ThreadingHTTPServer(("127.0.0.1", args.port), Handler) + srv.serve_forever() + + +if __name__ == "__main__": + main() diff --git a/tests/eval_ppl.c b/tests/eval_ppl.c new file mode 100644 index 0000000..2826c66 --- /dev/null +++ b/tests/eval_ppl.c @@ -0,0 +1,648 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file eval_ppl.c + * @brief Quality-eval driver for the qwen35moe engine: perplexity over a fixed + * corpus (`ppl` mode) and greedy generation over a prompt suite (`gen` mode). + * Replaces the bit-exact gate when comparing two engine builds whose logits + * are not bit-identical (relaxed reduction order, requantized hot tensors...): + * run both modes through build A and build B and diff with evals/eval_quality.sh. + * + * ppl mode: teacher-forced NLL of each actual next token, from the engine's + * own forward logits (numerically stable log-softmax in double). Emits + * `PPL tokens=N nll=X ppl=Y` on stdout. + * + * gen mode: for each %%-separated prompt, format with the model's chat + * template, prefill, then decode greedily (pure argmax, temp-0 equivalent) + * up to max_new tokens. Emits machine-parsable `GEN ...: id id ...` plus + * a human-readable `TXT : ...` line per prompt. One process, one model + * load, all prompts (state reset between prompts). + * + * CPU build: make USE_BLAS=1 tests/eval_ppl + * GPU build (CUDA host): make USE_CUDA=1 tests/eval_ppl, then the usual + * QWEN35_GPU_SPLIT_LAYER / QWEN35_GPU_DIE_A / QWEN35_GPU_DIE_B env selects + * the dies. An optional leading numeric arg acquires a barswap die first + * (legacy rigs only; small-BAR-quirk rigs skip it). NOT part of the test suite. + * + * @status Working; validated golden-vs-golden (0 divergence, 0 ppl delta) on M4 CPU + * @issues None + * @todo None + */ +#include "../include/pantheon.h" +#include "../src/core/bpe_pretokenize.h" +#include "../src/core/chat.h" +#include "../src/core/engine.h" +#include "../src/core/mem/pn_memwatch.h" +#include "../src/core/ngram_draft.h" +#include "tensor/tensor.h" +#include "tokenizer/tokenizer.h" + +#include +#include +#include +#include +#include +#include + +#ifdef USE_CUDA +#include +#include "barswap/barswap.h" +#include "../src/models/qwen35moe.h" +#endif + +/* Hard caps so a malformed input can't run away on the 2-core K80 host. */ +/* Caps raised for long-context (needle-in-haystack) prompts: a 32k-token + * needle prompt is ~180 KB of text, and a multi-cell suite of them needs a + * larger corpus buffer + prompt-token ceiling than the default 4k. */ +#define EVAL_MAX_CORPUS_BYTES (64 * 1024 * 1024) +#define EVAL_MAX_PROMPTS 256 +#define EVAL_MAX_GEN_TOKENS 512 +#define EVAL_MAX_PROMPT_TOKENS 40000 +#define EVAL_SPEC_MAX_K 32 + +/* Speculative greedy decode (gen mode, QWEN35_SPEC=1, CUDA builds): n-gram + * drafts verified in one batched pass; emitted tokens are the model's exact + * greedy argmaxes, so GEN output is unchanged. Stats accumulate across the + * prompt suite and print once at the end. */ +static long g_spec_verifies, g_spec_plain, g_spec_drafted, g_spec_accepted; + +static int spec_enabled(void) +{ + const char *v = getenv("QWEN35_SPEC"); + return v && v[0] && v[0] != '0'; +} + +static int spec_env_int(const char *name, int def) +{ + const char *v = getenv(name); + return v ? atoi(v) : def; +} + +static double now_s(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (double)ts.tv_sec + (double)ts.tv_nsec * 1e-9; +} + +#ifdef USE_CUDA +static int load_nvidia(void) +{ + if (system("modprobe --ignore-install nvidia 2>/dev/null") != 0) + return -1; + sleep(1); + if (system("modprobe --ignore-install nvidia_uvm 2>/dev/null") != 0) + return -1; + sleep(1); + return 0; +} +#endif + +/* Read a whole file into a NUL-terminated heap buffer (caller frees). */ +static char *read_text_file(const char *path, size_t *out_len) +{ + char *buf = NULL; + FILE *f = fopen(path, "rb"); + if (!f) { fprintf(stderr, "eval_ppl: cannot open %s\n", path); return NULL; } + + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + if (len <= 0 || len > EVAL_MAX_CORPUS_BYTES) { + fprintf(stderr, "eval_ppl: %s size %ld out of range\n", path, len); + fclose(f); + return NULL; + } + + buf = malloc((size_t)len + 1); + if (buf && fread(buf, 1, (size_t)len, f) == (size_t)len) { + buf[len] = '\0'; + if (out_len) *out_len = (size_t)len; + } else { + free(buf); + buf = NULL; + } + fclose(f); + return buf; +} + +/* Stable log-softmax probability of `target` under `logits[n]`, in double. */ +static double token_log_prob(const float *logits, int n, int target) +{ + double max = logits[0]; + for (int i = 1; i < n; i++) + if (logits[i] > max) max = logits[i]; + double sum = 0.0; + for (int i = 0; i < n; i++) + sum += exp((double)logits[i] - max); + return (double)logits[target] - max - log(sum); +} + +/* Greedy pick: first index with the strictly greatest logit (same tie-break as + * the CPU oracle, so `gen` mode is comparable with the bit-exact gate's argmax). */ +static int argmax_token(const float *logits, int n) +{ + int best = 0; + for (int i = 1; i < n; i++) + if (logits[i] > logits[best]) best = i; + return best; +} + +/* One forward step; returns borrowed logits pointer (owned by *out). */ +static float *forward_logits(pn_model *model, pn_state *state, uint32_t token, + tm_tensor **out, int *n_vocab) +{ + tm_tensor *t = model->forward(model, token, state); + if (!t) return NULL; + *out = t; + *n_vocab = (int)t->shape[0]; + return tm_data_f32(t); +} + +/* Teacher-forced perplexity over the tokenized corpus. + * NLL is accumulated for tokens[1..n-1], each predicted from its true prefix. */ +static int run_perplexity(pn_model *model, pn_state *state, + const char *corpus_path, int max_tokens) +{ + char *text = read_text_file(corpus_path, NULL); + if (!text) return 1; + + /* Byte-level BPE models need the qwen35 pre-tokenizer + GPT-2 byte encode + * (raw ft_encode returns -1 ids for plain UTF-8 text). Matches chat.c. */ + int n_tok = 0; + int *toks = ft_get_model_type(model->tokenizer) == 0 + ? pn_bpe_encode(model->tokenizer, text, &n_tok) + : ft_encode(model->tokenizer, text, &n_tok); + free(text); + if (!toks || n_tok < 2) { + fprintf(stderr, "eval_ppl: corpus tokenized to %d tokens (<2)\n", n_tok); + free(toks); + return 1; + } + int n_full = n_tok; + if (max_tokens > 1 && n_tok > max_tokens) n_tok = max_tokens; + if (n_tok > state->config->n_ctx) n_tok = state->config->n_ctx; + for (int i = 0; i < n_tok; i++) { + if (toks[i] < 0 || toks[i] >= state->config->n_vocab) { + fprintf(stderr, "eval_ppl: bad token id %d at %d\n", toks[i], i); + free(toks); + return 1; + } + } + fprintf(stderr, "eval_ppl: corpus %s -> %d tokens (scoring %d)\n", + corpus_path, n_full, n_tok); + + /* Pre-flight admission (MemoryAware §6.2): this teacher-forced pass is one + * long prefill — gate it exactly like serve (engine-level pn_admit, §11 Q7: + * this harness is the path that crash-looped the K80 test machine). CAP scores a + * shorter corpus prefix; DENIED refuses the run. */ + int fit_tok = n_tok; + pn_admit_status admit = pn_admit(model, n_tok, &fit_tok); + if (admit == PN_ADMIT_DENIED) { + fprintf(stderr, "eval_ppl: admission refused a %d-token ppl pass " + "(host/die memory would be exhausted)\n", n_tok); + free(toks); + return 1; + } + if (admit == PN_ADMIT_CAPPED && fit_tok > 1 && fit_tok < n_tok) { + fprintf(stderr, "eval_ppl: admission capped the ppl pass %d -> %d " + "tokens\n", n_tok, fit_tok); + n_tok = fit_tok; + } + + double nll_sum = 0.0; + int scored = 0; + for (int i = 0; i + 1 < n_tok; i++) { + tm_tensor *out = NULL; + int n_vocab = 0; + float *logits = forward_logits(model, state, (uint32_t)toks[i], &out, &n_vocab); + if (!logits) { fprintf(stderr, "eval_ppl: forward failed at %d\n", i); free(toks); return 1; } + nll_sum += -token_log_prob(logits, n_vocab, toks[i + 1]); + scored++; + tm_free(out); + if (scored % 64 == 0) + fprintf(stderr, " [%d/%d] running ppl=%.4f\n", + scored, n_tok - 1, exp(nll_sum / scored)); + } + + /* The single stdout line eval_quality.sh parses. */ + printf("PPL tokens=%d nll=%.6f ppl=%.6f\n", + scored, nll_sum / scored, exp(nll_sum / scored)); + free(toks); + return 0; +} + +#ifdef USE_CUDA +/* Teacher-forced perplexity via CHUNKED BATCHED PREFILL: the corpus runs + * through pn_qwen35moe_forward_batch_cuda with per-position logits, so the + * score exercises the batch attention path (flash or host softmax — + * A/B with QWEN35_FLASH=0 vs 1). NLL semantics identical to run_perplexity: + * token i+1 predicted from its true prefix (chunk boundaries carry the KV + * cache and SSM state, exactly like one long prefill). Chunk size via + * EVAL_PPLB_CHUNK (default 256; per-chunk logits buffer is + * chunk * n_vocab floats — mind the 8 GB K80 host). */ +static int run_perplexity_batch(pn_model *model, pn_state *state, + const char *corpus_path, int max_tokens) +{ + char *text = read_text_file(corpus_path, NULL); + if (!text) return 1; + int n_tok = 0; + int *toks = ft_get_model_type(model->tokenizer) == 0 + ? pn_bpe_encode(model->tokenizer, text, &n_tok) + : ft_encode(model->tokenizer, text, &n_tok); + free(text); + if (!toks || n_tok < 2) { + fprintf(stderr, "eval_ppl: corpus tokenized to %d tokens (<2)\n", n_tok); + free(toks); + return 1; + } + int n_full = n_tok; + if (max_tokens > 1 && n_tok > max_tokens) n_tok = max_tokens; + if (n_tok > state->config->n_ctx) n_tok = state->config->n_ctx; + for (int i = 0; i < n_tok; i++) { + if (toks[i] < 0 || toks[i] >= state->config->n_vocab) { + fprintf(stderr, "eval_ppl: bad token id %d at %d\n", toks[i], i); + free(toks); + return 1; + } + } + fprintf(stderr, "eval_ppl: corpus %s -> %d tokens (scoring %d, batched)\n", + corpus_path, n_full, n_tok); + + /* Pre-flight admission (MemoryAware §6.2) — same gate as the sequential + * ppl pass: the batched prefill grows the same KV and is just as capable + * of exhausting the host/die. CAP scores a prefix; DENIED refuses. */ + int fit_tok = n_tok; + pn_admit_status admit = pn_admit(model, n_tok, &fit_tok); + if (admit == PN_ADMIT_DENIED) { + fprintf(stderr, "eval_ppl: admission refused a %d-token pplb pass " + "(host/die memory would be exhausted)\n", n_tok); + free(toks); + return 1; + } + if (admit == PN_ADMIT_CAPPED && fit_tok > 1 && fit_tok < n_tok) { + fprintf(stderr, "eval_ppl: admission capped the pplb pass %d -> %d " + "tokens\n", n_tok, fit_tok); + n_tok = fit_tok; + } + + int chunk = spec_env_int("EVAL_PPLB_CHUNK", 256); + if (chunk < 2) chunk = 2; + int n_vocab = state->config->n_vocab; + float *pos_logits = malloc((size_t)chunk * n_vocab * sizeof(float)); + uint32_t *chunk_toks = malloc((size_t)chunk * sizeof(uint32_t)); + if (!pos_logits || !chunk_toks) { + fprintf(stderr, "eval_ppl: pplb scratch alloc failed\n"); + free(pos_logits); free(chunk_toks); free(toks); + return 1; + } + + double nll_sum = 0.0; + int scored = 0; + for (int off = 0; off < n_tok; off += chunk) { + int nb = n_tok - off < chunk ? n_tok - off : chunk; + for (int i = 0; i < nb; i++) + chunk_toks[i] = (uint32_t)toks[off + i]; + tm_tensor *out = pn_qwen35moe_forward_batch_cuda(model, chunk_toks, + nb, state, + pos_logits); + if (!out) { + fprintf(stderr, "eval_ppl: batch prefill failed at %d\n", off); + free(pos_logits); free(chunk_toks); free(toks); + return 1; + } + tm_free(out); + /* Row i predicts corpus token off+i+1 (the last row of a non-final + * chunk scores the next chunk's first token). */ + for (int i = 0; i < nb && off + i + 1 < n_tok; i++) { + nll_sum += -token_log_prob(pos_logits + (size_t)i * n_vocab, + n_vocab, toks[off + i + 1]); + scored++; + } + fprintf(stderr, " [%d/%d] running ppl=%.4f\n", + scored, n_tok - 1, exp(nll_sum / (scored > 0 ? scored : 1))); + } + + printf("PPL tokens=%d nll=%.6f ppl=%.6f\n", + scored, nll_sum / scored, exp(nll_sum / scored)); + free(pos_logits); + free(chunk_toks); + free(toks); + return 0; +} +#endif /* USE_CUDA */ + +/* Escape newlines/tabs so each TXT line stays a single greppable line. */ +static void print_escaped_line(const char *tag, int idx, const char *text) +{ + printf("%s %d: ", tag, idx); + for (const char *p = text; *p; p++) { + if (*p == '\n') fputs("\\n", stdout); + else if (*p == '\t') fputs("\\t", stdout); + else fputc(*p, stdout); + } + fputc('\n', stdout); +} + +/* Greedy-decode one prompt; prints its GEN + TXT lines. Returns 0 on success. */ +static int run_one_prompt(pn_model *model, pn_state *state, int idx, + const char *prompt, int max_new, int eos_id, int im_end_id) +{ + pn_state_reset(state); + pn_token_seq seq = pn_chat_format_first((pn_chat_style)model->chat_style, + model->tokenizer, NULL, prompt); + if (!seq.tokens || seq.n_tokens <= 0) { + fprintf(stderr, "eval_ppl: prompt %d failed to format\n", idx); + return 1; + } + + /* Pre-flight admission (MemoryAware §6.2), the same engine-level gate as + * serve: DENIED refuses the prompt; CAP keeps the most recent fit tokens + * (sliding-window trim, mirroring serve_generation). */ + int fit_tok = seq.n_tokens; + pn_admit_status admit = pn_admit(model, seq.n_tokens, &fit_tok); + if (admit == PN_ADMIT_DENIED) { + fprintf(stderr, "eval_ppl: admission refused prompt %d (%d tokens — " + "host/die memory would be exhausted)\n", idx, seq.n_tokens); + free(seq.tokens); + return 1; + } + if (admit == PN_ADMIT_CAPPED && fit_tok > 0 && fit_tok < seq.n_tokens) { + int drop = seq.n_tokens - fit_tok; + memmove(seq.tokens, seq.tokens + drop, + (size_t)fit_tok * sizeof(uint32_t)); + seq.n_tokens = fit_tok; + fprintf(stderr, "eval_ppl: admission capped prompt %d: dropped %d " + "oldest tokens, prefilling %d\n", idx, drop, seq.n_tokens); + } + + tm_tensor *out = NULL; + int n_vocab = 0; + float *logits = NULL; +#ifdef USE_CUDA + /* Long-context prompts (needle-in-haystack): prefill through the BATCHED + * CUDA path (exercises the prefill flash + sliding-window kernels, O(N*W) + * instead of N sequential decode forwards) then decode from the last row's + * logits. EVAL_BATCH_PREFILL=0 forces the legacy token-by-token path. */ + const char *bpf = getenv("EVAL_BATCH_PREFILL"); + if (!(bpf && bpf[0] == '0') && seq.n_tokens > 1) { + int nv = state->config->n_vocab; + int chunk = spec_env_int("EVAL_PPLB_CHUNK", 256); + if (chunk < 2) chunk = 2; + float *pos_logits = malloc((size_t)chunk * nv * sizeof(float)); + uint32_t *ctoks = malloc((size_t)chunk * sizeof(uint32_t)); + if (!pos_logits || !ctoks) { + fprintf(stderr, "eval_ppl: batch-prefill scratch alloc failed\n"); + free(pos_logits); free(ctoks); free(seq.tokens); return 1; + } + int last_row = 0; + for (int off = 0; off < seq.n_tokens; off += chunk) { + int nb = seq.n_tokens - off < chunk ? seq.n_tokens - off : chunk; + for (int i = 0; i < nb; i++) ctoks[i] = seq.tokens[off + i]; + tm_tensor *bo = pn_qwen35moe_forward_batch_cuda(model, ctoks, nb, + state, pos_logits); + if (!bo) { + fprintf(stderr, "eval_ppl: batch prefill failed (prompt %d @ %d)\n", + idx, off); + free(pos_logits); free(ctoks); free(seq.tokens); return 1; + } + tm_free(bo); + last_row = nb - 1; + } + n_vocab = nv; + static float last_logits[1 << 20]; /* >= n_vocab; holds the final row */ + memcpy(last_logits, pos_logits + (size_t)last_row * nv, + (size_t)nv * sizeof(float)); + logits = last_logits; + out = NULL; + free(pos_logits); free(ctoks); + } else +#endif + for (int i = 0; i < seq.n_tokens; i++) { + if (out) tm_free(out); + logits = forward_logits(model, state, seq.tokens[i], &out, &n_vocab); + if (!logits) { fprintf(stderr, "eval_ppl: prefill failed (prompt %d)\n", idx); free(seq.tokens); return 1; } + } + + printf("GEN %d prompt_tokens=%d:", idx, seq.n_tokens); + char raw[EVAL_MAX_GEN_TOKENS * 16]; + size_t raw_len = 0; + raw[0] = '\0'; + int emitted = 0; + + /* Decode loop. `cur` is the next greedy token, emitted but NOT yet fed; + * its successors come from the last verify's queue (spec mode), one + * batched verify of [cur, n-gram drafts...] (spec mode), or one plain + * forward of cur. All three produce the model's exact greedy argmaxes, + * so GEN output is identical with QWEN35_SPEC on or off. */ + int spec = spec_enabled(); + int max_k = spec_env_int("QWEN35_SPEC_K", 6); + int ng_max = spec_env_int("QWEN35_SPEC_NG_MAX", 3); + int ng_min = spec_env_int("QWEN35_SPEC_NG_MIN", 3); + if (max_k < 1) max_k = 1; + if (max_k > EVAL_SPEC_MAX_K) max_k = EVAL_SPEC_MAX_K; + /* The economics guard (see pn_spec_throttle): a K80 verify costs ~4 + * plain forwards, so probe-and-cool through phases that draft poorly + * and skip verifies whose draft count cannot reach break-even. */ + pn_spec_throttle thr; + pn_spec_throttle_init(&thr, spec_env_int("QWEN35_SPEC_MIN_ACC", 4), + spec_env_int("QWEN35_SPEC_MISS", 2), + spec_env_int("QWEN35_SPEC_COOL", 48)); +#ifndef USE_CUDA + (void)max_k; (void)ng_max; (void)ng_min; (void)thr; /* CUDA-only path */ +#endif + static uint32_t hist[EVAL_MAX_PROMPT_TOKENS + EVAL_MAX_GEN_TOKENS + 1]; + int nh = seq.n_tokens < EVAL_MAX_PROMPT_TOKENS ? seq.n_tokens + : EVAL_MAX_PROMPT_TOKENS; + memcpy(hist, seq.tokens, (size_t)nh * sizeof(uint32_t)); + int32_t queue[EVAL_SPEC_MAX_K + 1]; + int qn = 0, qi = 0; + + double t_dec = now_s(); + int cur = argmax_token(logits, n_vocab); + while (emitted < max_new) { + if (cur == eos_id || cur == im_end_id) break; + printf(" %d", cur); + const char *piece = ft_id_to_token(model->tokenizer, cur); + if (piece) { + size_t plen = strlen(piece); + if (raw_len + plen < sizeof(raw) - 1) { + memcpy(raw + raw_len, piece, plen + 1); + raw_len += plen; + } + } + hist[nh++] = (uint32_t)cur; + emitted++; + if (emitted >= max_new) break; + + if (qi < qn) { cur = queue[qi++]; continue; } + + int stepped = 0; +#ifdef USE_CUDA + if (spec && pn_spec_throttle_gate(&thr)) { + uint32_t drafts[EVAL_SPEC_MAX_K], block[EVAL_SPEC_MAX_K + 1]; + int32_t am[EVAL_SPEC_MAX_K + 1]; + int k = pn_ngram_draft(hist, nh, ng_max, ng_min, max_k, drafts); + if (k >= thr.min_accept) { + block[0] = (uint32_t)cur; + memcpy(block + 1, drafts, (size_t)k * sizeof(uint32_t)); + if (pn_qwen35moe_spec_verify(model, block, k + 1, state, + am) == 0) { + int acc = 1; + while (acc <= k && drafts[acc - 1] == (uint32_t)am[acc - 1]) + acc++; + if (pn_qwen35moe_spec_accept(model, state, acc) != 0) { + fprintf(stderr, "\neval_ppl: spec accept failed\n"); + free(seq.tokens); + return 1; + } + g_spec_verifies++; + g_spec_drafted += k; + g_spec_accepted += acc - 1; + pn_spec_throttle_report(&thr, acc - 1); + qn = qi = 0; + for (int i = 0; i < acc; i++) + queue[qn++] = am[i]; + cur = queue[qi++]; + stepped = 1; + } + } + } +#endif + if (!stepped) { + tm_free(out); + out = NULL; + logits = forward_logits(model, state, (uint32_t)cur, &out, &n_vocab); + if (!logits) { fprintf(stderr, "\neval_ppl: decode failed (prompt %d)\n", idx); free(seq.tokens); return 1; } + cur = argmax_token(logits, n_vocab); + g_spec_plain++; + } + } + double dt_dec = now_s() - t_dec; + printf("\n"); + if (out) tm_free(out); + + char *decoded = gpt2_bytes_decode(raw); /* qwen byte-level BPE -> UTF-8 */ + print_escaped_line("TXT", idx, decoded ? decoded : raw); + free(decoded); + fflush(stdout); + fprintf(stderr, "eval_ppl: prompt %d done (%d tokens, %.2f tok/s%s)\n", + idx, emitted, dt_dec > 0.0 ? emitted / dt_dec : 0.0, + spec ? ", spec" : ""); + free(seq.tokens); + return 0; +} + +/* Greedy generation over every %%-separated prompt in the suite file. */ +static int run_generation(pn_model *model, pn_state *state, + const char *prompts_path, int max_new) +{ + char *text = read_text_file(prompts_path, NULL); + if (!text) return 1; + + /* Split on lines that contain exactly "%%". */ + char *prompts[EVAL_MAX_PROMPTS]; + int n_prompts = 0; + char *cursor = text; + while (cursor && n_prompts < EVAL_MAX_PROMPTS) { + char *sep = strstr(cursor, "\n%%\n"); + if (sep) *sep = '\0'; + while (*cursor == '\n' || *cursor == ' ') cursor++; + if (*cursor) prompts[n_prompts++] = cursor; + cursor = sep ? sep + 4 : NULL; + } + fprintf(stderr, "eval_ppl: %d prompts, max_new=%d\n", n_prompts, max_new); + + int eos_id = ft_eos_id(model->tokenizer); + int im_end_id = ft_token_to_id(model->tokenizer, "<|im_end|>"); + int rc = 0; + for (int i = 0; i < n_prompts && rc == 0; i++) + rc = run_one_prompt(model, state, i, prompts[i], max_new, eos_id, im_end_id); + if (spec_enabled()) { + long fwd = g_spec_verifies + g_spec_plain; + long tok = g_spec_verifies + g_spec_accepted + g_spec_plain; + fprintf(stderr, + "eval_ppl: SPEC verifies=%ld plain=%ld drafted=%ld " + "accepted=%ld accept_rate=%.2f tokens/forward=%.2f\n", + g_spec_verifies, g_spec_plain, g_spec_drafted, + g_spec_accepted, + g_spec_drafted ? (double)g_spec_accepted / g_spec_drafted + : 0.0, + fwd ? (double)tok / fwd : 0.0); + } + free(text); + return rc; +} + +int main(int argc, char **argv) +{ + int ai = 1; + +#ifdef USE_CUDA + /* Optional leading numeric arg = barswap die to acquire + bring nvidia up + * (legacy limited-MMIO rigs; unnecessary once the small-BAR quirk maps all + * dies at boot). */ + if (argc > ai && isdigit((unsigned char)argv[ai][0]) && argc > ai + 2) { + int die = atoi(argv[ai++]); + fprintf(stderr, "acquiring die %d ...\n", die); + bs_context *bs = bs_init(); + if (!bs) { fprintf(stderr, "bs_init failed (bar_swap.ko loaded?)\n"); return 1; } + if (!bs_acquire(bs, die)) { fprintf(stderr, "bs_acquire(%d) failed\n", die); return 1; } + if (load_nvidia() != 0) { fprintf(stderr, "nvidia load failed\n"); return 1; } + fprintf(stderr, "die %d acquired, nvidia up\n", die); + } +#endif + + if (argc < ai + 3) { + fprintf(stderr, + "usage: %s [die] ppl [max_tokens]\n" + " %s [die] pplb [max_tokens]" + " (CUDA: batched-prefill ppl)\n" + " %s [die] gen [max_new_tokens]\n", + argv[0], argv[0], argv[0]); + return 1; + } + const char *model_path = argv[ai]; + const char *mode = argv[ai + 1]; + const char *input_path = argv[ai + 2]; + int limit = argc > ai + 3 ? atoi(argv[ai + 3]) : 0; + + /* MemoryAware: start the memory-watch loop BEFORE the load, exactly like + * serve — (a) the GPU upload path feeds per-die VRAM truth in, (b) the + * engine-level pn_admit gates the prefills below with real numbers. This + * harness is the exact path that crash-looped the K80 test machine (§11 Q7), so it + * must NOT run fail-open. Non-fatal if the loop cannot start. */ + pn_memwatch_start(); + + pn_model *model = pn_load(model_path); + if (!model) { fprintf(stderr, "eval_ppl: model load failed\n"); return 1; } + pn_state *state = pn_state_new(model); + if (!state) { fprintf(stderr, "eval_ppl: state alloc failed\n"); return 1; } + + int rc; + if (strcmp(mode, "ppl") == 0) { + rc = run_perplexity(model, state, input_path, limit); +#ifdef USE_CUDA + } else if (strcmp(mode, "pplb") == 0) { + rc = run_perplexity_batch(model, state, input_path, limit); +#endif + } else if (strcmp(mode, "gen") == 0) { + if (limit <= 0) limit = 48; + if (limit > EVAL_MAX_GEN_TOKENS) limit = EVAL_MAX_GEN_TOKENS; + rc = run_generation(model, state, input_path, limit); + } else { + fprintf(stderr, "eval_ppl: unknown mode '%s'\n", mode); + rc = 1; + } + + /* On the barswap rig, skip CUDA atexit teardown (it hangs/zombies) — + * same escape hatch as test_kernel_parity. */ +#ifdef USE_CUDA + fflush(stdout); fflush(stderr); + if (!getenv("QWEN35_EXIT_CLEAN")) + _Exit(rc); +#endif + pn_state_free(state); + pn_model_free(model); + return rc; +} diff --git a/tests/frame_capture_fixtures.h b/tests/frame_capture_fixtures.h new file mode 100644 index 0000000..9c2f8ba --- /dev/null +++ b/tests/frame_capture_fixtures.h @@ -0,0 +1,165 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file frame_capture_fixtures.h + * @brief Synthetic tool-call argument values reproducing the FRAME CAPTURE + * failure shapes: what lands in a parameter VALUE when a turn is + * force-closed mid-thought. + * + * The content (project "lumen", its paths and its analysis text) is + * invented. The shapes are not: each fixture keeps the structure, + * approximate length and escape pattern of a failure observed in real + * serving -- the leading "", the prose lead-in, the call + * dialect, over-closed braces, shell metacharacters, code fences and a + * truncated SEARCH/REPLACE block. + * + * WHAT THEY SHOW. The forced tool grammar compels the XML frame; when a + * turn is force-closed mid-thought (the serve INJECTS "" once the + * per-model reasoning_budget is spent -- batch_sched.c driver_reason_step, + * serve_generation.c sg_run_completion), the grammar cannot tell that + * close from the model's own, sets reasoning_ended, and compels the + * opener from the next char. Whatever the model was still going to say + * then lands in the free-text parameter VALUE. FX_NATIVE_XML_CAPTURE is + * the control: the captured text there is a NATIVE Qwen-XML call, the + * very dialect the grammar armed, so the capture is not dialect drift. + * + * @status None + * @issues None + * @todo None + */ + +#ifndef PN_TEST_FRAME_CAPTURE_FIXTURES_H +#define PN_TEST_FRAME_CAPTURE_FIXTURES_H + +/* A `grep` frame's `pattern` argument, ~60 chars. + * THE FALSIFYING CASE. The model's own followed by a truncated + * NATIVE Qwen-XML `read` call -- the exact dialect the grammar armed. No + * dialect drift happened here, so no dialect theory explains the capture; the + * turn was force-closed mid-thought. RECOVERABLE. */ +static const char FX_NATIVE_XML_CAPTURE[] = + "\n\n\n\nlumen/serve.log"; + +/* A `read` frame's `path` argument, ~140 chars. + * The model's own , then a prose lead-in, then a complete call in the + * Hermes `call_function` shape. The leading is the tell: the model + * had NOT finished reasoning when the frame was compelled, so it wrote the + * close it was always going to write INTO the value. RECOVERABLE. */ +static const char FX_HERMES_LEADIN_CAPTURE[] = + "\n\nLet me look at the Makefile to understand the build structur" + "e:\n\ncall_function{\"func\":\"read\",\"args\":{\"path\":\"lu" + "men/Makefile\"}}"; + +/* A `read` frame's `path` argument, ~100 chars. + * Bare `call_function` call, over-closed ("}}}" where two braces close the + * object). RECOVERABLE. */ +static const char FX_OVERCLOSED_CAPTURE[] = + "call_function{\"func\":\"read\",\"args\":{\"path\":\"lumen/in" + "clude/lumen/settings_cache.h\"}}}"; + +/* A `read` frame's `path` argument, ~150 chars. + * Prose lead-in then a complete `call_function` call, absolute path. + * RECOVERABLE. */ +static const char FX_ABSOLUTE_PATH_CAPTURE[] = + "Let me find the correct path for settings_cache.h:\n\ncall_fu" + "nction{\"func\":\"read\",\"args\":{\"path\":\"/home/user/src/lumen/inclu" + "de/settings_cache.h\"}}}"; + +/* A `glob` frame's `pattern` argument, ~190 chars. + * A shell pipeline plus a dangling markdown fence in a `glob` pattern. Not a + * complete call in any shape: NOTHING TO RECOVER, and NO marker to key on. + * The trailing token is cut mid-word, as a truncated emission is. */ +static const char FX_SHELL_IN_GLOB[] = + "lumen/include/**/*.h* | head -20; echo \"---\"; find lumen -name \"setti" + "ngs_cache.h\" 2>/dev/null; echo \"---\"; ls lumen/src/settings/*.h 2>/de" + "v/null; echo \"---\"; ls lume-4471093821\n```"; + +/* A `glob` frame's `pattern` argument, over 5,000 chars. + * Mostly analysis prose, with C source (braces!) and a TRUNCATED SEARCH/REPLACE + * edit block at the end. Not a complete call, and the braces must not fool a + * JSON scanner into manufacturing one. NOTHING TO RECOVER, NO marker. */ +static const char FX_ANALYSIS_DUMP[] = + "lumen/include/lumen/settings_cache.h*lumen/src/settings/settings_cache.c" + ":212` is the `log_warn(\"settings_cach**e: load failed...` line. The pro" + "blem is that the loader reads a `version` field that older settings file" + "s never wrote, and then bails out because it cannot find the key.\n\nLet" + " me walk through what happens step by step:\n1. `settings_cache_open` re" + "ads the whole file and hands it to `parse_settings`, which expects `vers" + "ion` at the top level\n2. When a file written by an older build is opene" + "d, the parser returns no error for the missing key, but the later lookup" + " of `version` returns NULL and the caller treats that as corruption\n\nT" + "he fix should be:\n1. Treat a missing `version` as version 1 instead of " + "as a hard failure\n2. Run the upgrade steps in order, from whatever vers" + "ion the file declares up to the current one\n\nLet me check how the othe" + "r loaders deal with this - particularly anything named UPGRADE_STEPS or " + "similar:\n* Around line 140-151 there is already an UPGRADE_STEPS tabl" + "e of functions that rewrite older layouts. The trouble is that step 2 re" + "names `cache_dir` to `cache_root` unconditionally, which corrupts a file" + " that already uses the new name.\n\nWhat actually needs to change:\n* " + "Check the declared version before running each step\n* Make each step " + "idempotent rather than assuming it runs exactly once\n\nLet me see what " + "happens when step 2 runs on a file that already has `cache_root`. The ol" + "d key is absent, so the rename copies a NULL value over the good one. Th" + "at is the \"empty cache_root\" report.\n\nReading the flow again more ca" + "refully:\n1. Line 205 calls parse_settings, which builds the tree - this" + " succeeds even when keys are missing\n2. Line 209-210 loops over UPGRADE" + "_STEPS and applies every one of them\n\nThe issue is that on a second st" + "art the steps run again, and nothing records that they already ran. The " + "return value is ignored too - looking at line 209-210:\n\n```c\nfor (siz" + "e_t i = 0; i < sizeof(UPGRADE_STEPS) / sizeof(*UPGRADE_STEPS); i++)\n " + " UPGRADE_STEPS[i](cache->root);\n```\n\nNothing checks whether a step fa" + "iled! So a failing rename is silently swallowed, and that is NOT the rea" + "l issue on its own. The real issue shows up at line 205:\n\n```c\nif (pa" + "rse_settings(cache->text, &cache->root, &err) != 0) {\n log_warn(\"se" + "ttings_cache: load failed: %s\", err ? err : \"?\");\n free(err);\n``" + "`\n\nHold on - parse_settings also validates that `cache_root` is a non-" + "empty string. The validation runs BEFORE the upgrade steps, so on an old" + " file the key is still named `cache_dir`, validation fails, and the upgr" + "ade never gets a chance to run. Let me look at line 74:\n\n```c\n{ \"cac" + "he_root\", KIND_STRING, REQUIRED },\n```\n\nThat entry will reject any f" + "ile that predates the rename! And because validation and parsing happen " + "in the same call, the whole load fails when that one entry fails.\n\nSo " + "the fix is one of:\n1. Run validation separately, after the upgrade step" + "s have rewritten the tree \n2. Or mark `cache_root` optional and check " + "it after upgrading\n\nThe cleanest fix is to make sure every upgrade ste" + "p runs BEFORE validation, and to make each step check its own preconditi" + "on so that running it twice is harmless.\n\nActually, looking again at l" + "ine 146:\n```c\n{ 2, rename_cache_dir },\n```\n\nThis IS in UPGRADE_STEP" + "S, which runs AFTER parse_settings. But parse_settings already applied t" + "he schema table from line 74, and that happens INSIDE the same call that" + " builds the tree. \n\nWhen parsing and validation share one call, a sing" + "le failed rule fails the whole call and returns no tree at all. So when " + "the `cache_root` rule runs against a file that still says `cache_dir` (b" + "ecause the rename has not happened yet), it FAILS and throws away the pa" + "rsed tree, including the parts that were perfectly fine.\n\nThe fix is t" + "o move validation OUT of parse_settings, or split it so it only runs onc" + "e the upgrade steps have renamed the key. Better approach: have parse_se" + "ttings only build the tree, then run the upgrade steps, then validate th" + "e result with a clear message for each rule that fails.\n\nI also want t" + "o double-check the writer side, because if `settings_cache_save` writes " + "the file before validation there is a window where a half-upgraded tree " + "reaches disk. Looking at line 260-268, save is only called from `setting" + "s_cache_commit`, and commit is only reachable after a successful open, s" + "o that window does not exist today. Good - nothing to change there.\n\nO" + "ne more thing worth confirming is the version bump itself. After the las" + "t step runs, the loader must write `version` back as the current number," + " otherwise every start replays all of the steps from version 1. Line 214" + " does set it, but only on the success path, which is exactly the path th" + "at never runs for an old file. Once validation moves after the steps, th" + "at line becomes reachable and the bump happens.\n\nLet me implement the " + "fix:\n```c|CODE_EDIT|lumen/src/settings/settings_cache.c\n<<<<<<< SEARCH" + "\n/* The full key table, in the order validate_settings walks it.\n * On" + "e definition so a new key can never be validated in some loaders and not" + "\n * others \xe2\x80\x94 that asymmetry is exactly how `cache_root` woul" + "d have gone missing. */\n#define SETTINGS_KEYS " + " \\"; + +/* A `read` frame's `path` argument, ~230 chars. + * The ORIGINAL wedge shape: a bash pipeline captured into a `read` path. The + * model wanted `bash`, but nothing in the value says so. NOTHING TO RECOVER, + * and NO marker -- content sniffing cannot reach this family at all. */ +static const char FX_SHELL_IN_READ_PATH[] = + "/home/user/src/lumen/src/core/settings_cache.c.md 2>/dev/null; find /hom" + "e/user/src -path \"*/src/settings_cache.c\" 2>/dev/null | head -5; find " + "/home/user/sr -name \"settings_cache.c\" 2>/dev/null | grep lumen"; + +#endif /* PN_TEST_FRAME_CAPTURE_FIXTURES_H */ diff --git a/tests/gen_golden_qwen35.c b/tests/gen_golden_qwen35.c new file mode 100644 index 0000000..6e5e762 --- /dev/null +++ b/tests/gen_golden_qwen35.c @@ -0,0 +1,873 @@ +/* SPDX-License-Identifier: AGPL-3.0-or-later */ +/* Copyright (C) 2026 David (Corey) Herrera */ +/** + * @file gen_golden_qwen35.c + * @brief CPU-reference "golden vector" generator for the qwen35moe GPU kernels. + * Produces deterministic input+output golden files under tests/golden/ + * so the forthcoming K80 CUDA kernels can be diffed bit-for-bit (routing + * indices) / within tolerance (matvec, RoPE) against a trusted CPU + * reference. NO model GGUF is loaded — all inputs are synthetic, + * generated from a fixed-seed xorshift PRNG. + * + * Kernels covered (priority order): + * 1. Q3_K dequant + Q3_K matvec (history of dequant bugs) + * 2. Partial NEOX RoPE (rope_dim 64 of head_dim 256) + * 3. QK-RMSNorm (per-head RMSNorm) + * 4. MoE routing (softmax + top-8/256 + norm_topk_prob + * + shared-expert sigmoid gate) + * + * Where a PUBLIC Pantheon CPU function exists it is CALLED directly (the + * gold standard): pn_dequantize_block_q3_k / pn_qdot for Q3_K, and + * pn_vrms_norm_f32 for QK-RMSNorm. The partial-RoPE and MoE-routing math + * live in static helpers inside the model .c files; those are replicated + * here line-for-line with the source cited inline so the replica is + * auditable. + * + * @status Complete: generates all four golden sets, verifies finiteness, prints + * summary sums. CPU-only, no model, runs in well under a second. + * @issues None + * @todo None + */ + +/* 1. (no own header — standalone tool) */ + +/* 2. Standard library */ +#include +#include +#include +#include +#include + +/* 4. Project headers */ +#include "../src/accel/simd.h" +#include "../src/core/qweight.h" +#include "../src/formats/gguf_dequant.h" + +/* ============================================================ + * Config constants (verified Nex-N2-mini qwen35moe dimensions) + * ============================================================ */ + +#define GOLDEN_SEED 0x9E3779B9u /* fixed xorshift seed (golden ratio) */ +#define GOLDEN_DIR "tests/golden" + +#define QWEN35_N_EMBD 2048 +#define QWEN35_N_HEAD 16 +#define QWEN35_N_HEAD_KV 2 +#define QWEN35_HEAD_DIM 256 +#define QWEN35_ROPE_DIM 64 +#define QWEN35_N_EXPERT 256 +#define QWEN35_EXPERT_USED 8 +#define QWEN35_EXPERT_FF 512 +#define QWEN35_RMS_NORM_EPS 1e-6f /* attention.layer_norm_rms_epsilon */ +#define QWEN35_ROPE_THETA 1e7f /* rope.freq_base */ +#define QWEN35_EXPERT_WT_SCALE 1.0f /* expert_weights_scale (GGUF default) */ + +/* Q3_K matvec test shape: a synthetic weight matrix with cols a multiple of the + * 256-element super-block and a handful of output rows. */ +#define Q3K_ROWS 24 +#define Q3K_COLS 1024 /* 4 Q3_K super-blocks per row */ + +/* Gated-Delta-Net SSM dimensions (verified Nex-N2-mini qwen35moe). */ +#define SSM_N_V_HEADS 32 +#define SSM_N_K_HEADS 16 +#define SSM_STATE_SIZE 128 /* S == head_k_dim == head_v_dim */ +#define SSM_CONV_DIM 8192 +#define SSM_CONV_KERNEL 4 + +/* RoPE test positions. */ +static const int ROPE_POSITIONS[] = {0, 1, 7, 100}; +#define ROPE_N_POS (int)(sizeof(ROPE_POSITIONS) / sizeof(ROPE_POSITIONS[0])) + +/* ============================================================ + * Deterministic PRNG — xorshift32 (seeded, never rand()) + * ============================================================ */ + +/* A tiny, fully deterministic xorshift32. Same seed -> same stream on every + * platform, which is exactly what golden vectors need. */ +typedef struct { + uint32_t state; +} golden_rng; + +static void golden_rng_init(golden_rng *rng, uint32_t seed) +{ + /* Avoid the zero fixed point; any nonzero seed produces a full-period stream. */ + rng->state = seed ? seed : 0xDEADBEEFu; +} + +static uint32_t golden_rng_next_u32(golden_rng *rng) +{ + uint32_t x = rng->state; + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + rng->state = x; + return x; +} + +/* Uniform byte. */ +static uint8_t golden_rng_next_u8(golden_rng *rng) +{ + return (uint8_t)(golden_rng_next_u32(rng) & 0xFFu); +} + +/* Uniform float in [-1, 1). */ +static float golden_rng_next_signed_unit(golden_rng *rng) +{ + /* 24 mantissa bits -> [0,1), then map to [-1,1). */ + uint32_t bits = golden_rng_next_u32(rng) >> 8; + float u01 = (float)bits / (float)(1u << 24); + return u01 * 2.0f - 1.0f; +} + +/* ============================================================ + * FP16 packing (host-side, mirrors IEEE-754 half; round-to-nearest-even) + * ============================================================ */ + +/* Pack a finite, in-range float into an IEEE-754 half. Used only to fill the + * synthetic Q3_K super-block scale field (d). Kept small and conservative — the + * synthetic d values are O(0.01), comfortably inside the half range. The decode + * side is the production pn_half_to_float, so any rounding here is captured in + * the golden output (the real CPU kernel reads exactly these bytes). */ +static uint16_t float_to_half(float f) +{ + union { float f; uint32_t u; } v; + v.f = f; + uint32_t x = v.u; + uint32_t sign = (x >> 16) & 0x8000u; + int32_t exp = (int32_t)((x >> 23) & 0xFF) - 127 + 15; + uint32_t mant = x & 0x7FFFFFu; + + if (exp <= 0) { + /* Subnormal/zero half — synthetic d is never this small; flush to 0. */ + return (uint16_t)sign; + } + if (exp >= 0x1F) { + /* Overflow — clamp to max finite half. */ + return (uint16_t)(sign | 0x7BFFu); + } + /* Round mantissa to 10 bits, round-to-nearest-even. */ + uint32_t mant10 = mant >> 13; + uint32_t rem = mant & 0x1FFFu; + if (rem > 0x1000u || (rem == 0x1000u && (mant10 & 1u))) + mant10++; + if (mant10 == 0x400u) { /* mantissa overflow carries into exponent */ + mant10 = 0; + exp++; + if (exp >= 0x1F) + return (uint16_t)(sign | 0x7BFFu); + } + return (uint16_t)(sign | ((uint32_t)exp << 10) | mant10); +} + +/* ============================================================ + * Golden file writer — simple self-describing text-prefix format + * + * Format of every golden file: + * line 1: "PNGOLDEN1 seed=" + * then raw little-endian values of (binary payload). + * dtype is one of: f32, u8, i32. Shapes/semantics are in tests/golden/README.md. + * ============================================================ */ + +static int write_golden(const char *filename, const char *name, const char *dtype, + const void *payload, size_t count, size_t elem_size) +{ + char path[512]; + int n = snprintf(path, sizeof(path), "%s/%s", GOLDEN_DIR, filename); + if (n < 0 || (size_t)n >= sizeof(path)) { + fprintf(stderr, "write_golden: path overflow for '%s'\n", filename); + return -1; + } + + FILE *fp = fopen(path, "wb"); + if (!fp) { + fprintf(stderr, "write_golden: cannot open '%s'\n", path); + return -1; + } + + int rc = -1; + if (fprintf(fp, "PNGOLDEN1 %s %s %zu seed=0x%08X\n", + name, dtype, count, (unsigned)GOLDEN_SEED) < 0) + goto cleanup; + if (count > 0 && fwrite(payload, elem_size, count, fp) != count) + goto cleanup; + rc = 0; + +cleanup: + if (rc != 0) + fprintf(stderr, "write_golden: write failed for '%s'\n", path); + fclose(fp); + return rc; +} + +/* Finiteness/sanity check + summary print for a float buffer. */ +static int report_f32(const char *label, const float *data, size_t count) +{ + double sum = 0.0; + for (size_t i = 0; i < count; i++) { + if (!isfinite(data[i])) { + fprintf(stderr, " [FAIL] %s: non-finite value at index %zu\n", label, i); + return -1; + } + sum += (double)data[i]; + } + printf(" %-22s count=%-6zu sum=%+.6f first=[%+.5f %+.5f %+.5f]\n", + label, count, sum, + count > 0 ? data[0] : 0.0f, + count > 1 ? data[1] : 0.0f, + count > 2 ? data[2] : 0.0f); + return 0; +} + +/* ============================================================ + * Kernel 1: Q3_K dequant + Q3_K matvec (REAL CPU functions) + * ============================================================ */ + +/* Fill one Q3_K super-block with deterministic pseudo-random bytes that respect + * the real block layout (pn_block_q3_k in gguf_dequant.h): hmask[32], qs[64], + * scales[12], d (fp16). The decode is done by the production kernel, so every + * bit pattern we write here is interpreted exactly as the GPU kernel must. */ +static void fill_q3k_block(golden_rng *rng, pn_block_q3_k *block) +{ + for (int i = 0; i < 32; i++) + block->hmask[i] = golden_rng_next_u8(rng); + for (int i = 0; i < 64; i++) + block->qs[i] = golden_rng_next_u8(rng); + for (int i = 0; i < 12; i++) + block->scales[i] = golden_rng_next_u8(rng); + /* Small positive-ish super-block scale, typical of real Q3_K weights. */ + float d = 0.005f + 0.02f * (golden_rng_next_signed_unit(rng) * 0.5f + 0.5f); + block->d = float_to_half(d); +} + +/* Generate the Q3_K golden set: synthetic quantized weight matrix + F32 input + * vector, then run the REAL pn_qmv to produce the golden output. + * + * Source of golden output: pn_qmv -> pn_qdot -> dot_q3_k -> + * pn_dequantize_block_q3_k (src/core/qweight.c:46-57, 161-182; + * src/formats/gguf_dequant.c:118-162). All public, all called directly. */ +static int gen_q3k_golden(golden_rng *rng) +{ + const int rows = Q3K_ROWS; + const int cols = Q3K_COLS; + const int blocks_per_row = cols / 256; + const size_t total_blocks = (size_t)rows * (size_t)blocks_per_row; + + pn_block_q3_k *weight = malloc(total_blocks * sizeof(*weight)); + float *input = malloc((size_t)cols * sizeof(*input)); + float *output = malloc((size_t)rows * sizeof(*output)); + int rc = -1; + if (!weight || !input || !output) { + fprintf(stderr, "gen_q3k_golden: allocation failed\n"); + goto cleanup; + } + + for (size_t b = 0; b < total_blocks; b++) + fill_q3k_block(rng, &weight[b]); + for (int i = 0; i < cols; i++) + input[i] = golden_rng_next_signed_unit(rng); + + /* REAL CPU matvec — the gold standard. row_stride = bytes per quantized row. */ + pn_qweight qw = { + .data = weight, + .type = GGUF_QUANT_Q3_K, + .rows = rows, + .cols = cols, + .row_stride = (size_t)blocks_per_row * sizeof(pn_block_q3_k), + .scales = NULL, + .group_size = 0, + }; + pn_qmv(&qw, input, output); + + if (write_golden("q3k_matvec_weight.bin", "q3k_matvec_weight", "u8", + weight, total_blocks * sizeof(pn_block_q3_k), 1) < 0) + goto cleanup; + if (write_golden("q3k_matvec_input.bin", "q3k_matvec_input", "f32", + input, (size_t)cols, sizeof(float)) < 0) + goto cleanup; + if (write_golden("q3k_matvec_output.bin", "q3k_matvec_output", "f32", + output, (size_t)rows, sizeof(float)) < 0) + goto cleanup; + + /* Also emit a single dequantized super-block (block 0) as a standalone + * dequant golden — isolates the dequant from the dot-product. */ + float dequant0[256]; + pn_dequantize_block_q3_k(&weight[0], dequant0); + if (write_golden("q3k_dequant_block0.bin", "q3k_dequant_block0", "f32", + dequant0, 256, sizeof(float)) < 0) + goto cleanup; + + printf("[1] Q3_K dequant + matvec (REAL pn_qmv / pn_dequantize_block_q3_k)\n"); + if (report_f32("q3k_dequant_block0", dequant0, 256) < 0) goto cleanup; + if (report_f32("q3k_matvec_output", output, (size_t)rows) < 0) goto cleanup; + rc = 0; + +cleanup: + free(weight); + free(input); + free(output); + return rc; +} + +/* ============================================================ + * Kernel 2: Partial NEOX RoPE (cross-checked replica) + * + * Replicates qwen35moe_rope_partial, src/models/qwen35moe_attn.c:57-76. + * Only the first rope_dim dims of each head are rotated (NEOX pairing j vs + * j+half, half = rope_dim/2); dims [rope_dim, head_dim) pass through. The math + * below is identical to the source, term for term. + * ============================================================ */ + +static void rope_partial_replica(float *data, int n_heads, int head_dim, + int rope_dim, int pos, float theta_base) +{ + int rot = (rope_dim > 0 && rope_dim < head_dim) ? rope_dim : head_dim; + int half = rot / 2; + for (int h = 0; h < n_heads; h++) { + float *head = data + (size_t)h * head_dim; + for (int j = 0; j < half; j++) { + float freq = 1.0f / powf(theta_base, (float)(2 * j) / (float)rot); + float angle = (float)pos * freq; + float cos_a = cosf(angle); + float sin_a = sinf(angle); + + float x0 = head[j]; + float x1 = head[j + half]; + head[j] = x0 * cos_a - x1 * sin_a; + head[j + half] = x0 * sin_a + x1 * cos_a; + } + } +} + +/* Generate RoPE golden: one synthetic Q tensor [n_head*head_dim] and one + * synthetic K tensor [n_head_kv*head_dim], rotated at each test position. The + * pre-rotation tensors are shared across positions (written once); each position + * gets its own rotated Q and K golden. */ +static int gen_rope_golden(golden_rng *rng) +{ + const int q_elems = QWEN35_N_HEAD * QWEN35_HEAD_DIM; + const int k_elems = QWEN35_N_HEAD_KV * QWEN35_HEAD_DIM; + + float *q_in = malloc((size_t)q_elems * sizeof(float)); + float *k_in = malloc((size_t)k_elems * sizeof(float)); + float *q_work = malloc((size_t)q_elems * sizeof(float)); + float *k_work = malloc((size_t)k_elems * sizeof(float)); + int rc = -1; + if (!q_in || !k_in || !q_work || !k_work) { + fprintf(stderr, "gen_rope_golden: allocation failed\n"); + goto cleanup; + } + + for (int i = 0; i < q_elems; i++) + q_in[i] = golden_rng_next_signed_unit(rng); + for (int i = 0; i < k_elems; i++) + k_in[i] = golden_rng_next_signed_unit(rng); + + if (write_golden("rope_q_input.bin", "rope_q_input", "f32", + q_in, (size_t)q_elems, sizeof(float)) < 0) goto cleanup; + if (write_golden("rope_k_input.bin", "rope_k_input", "f32", + k_in, (size_t)k_elems, sizeof(float)) < 0) goto cleanup; + + printf("[2] Partial NEOX RoPE (replica of qwen35moe_attn.c:57-76)\n"); + for (int p = 0; p < ROPE_N_POS; p++) { + int pos = ROPE_POSITIONS[p]; + memcpy(q_work, q_in, (size_t)q_elems * sizeof(float)); + memcpy(k_work, k_in, (size_t)k_elems * sizeof(float)); + rope_partial_replica(q_work, QWEN35_N_HEAD, QWEN35_HEAD_DIM, + QWEN35_ROPE_DIM, pos, QWEN35_ROPE_THETA); + rope_partial_replica(k_work, QWEN35_N_HEAD_KV, QWEN35_HEAD_DIM, + QWEN35_ROPE_DIM, pos, QWEN35_ROPE_THETA); + + char qname[64], kname[64], qfile[64], kfile[64]; + snprintf(qname, sizeof(qname), "rope_q_pos%d", pos); + snprintf(kname, sizeof(kname), "rope_k_pos%d", pos); + snprintf(qfile, sizeof(qfile), "rope_q_pos%d.bin", pos); + snprintf(kfile, sizeof(kfile), "rope_k_pos%d.bin", pos); + if (write_golden(qfile, qname, "f32", q_work, (size_t)q_elems, + sizeof(float)) < 0) goto cleanup; + if (write_golden(kfile, kname, "f32", k_work, (size_t)k_elems, + sizeof(float)) < 0) goto cleanup; + + char qlabel[64]; + snprintf(qlabel, sizeof(qlabel), "rope_q_pos%d", pos); + if (report_f32(qlabel, q_work, (size_t)q_elems) < 0) goto cleanup; + } + rc = 0; + +cleanup: + free(q_in); + free(k_in); + free(q_work); + free(k_work); + return rc; +} + +/* ============================================================ + * Kernel 3: QK-RMSNorm (REAL CPU function pn_vrms_norm_f32) + * + * Mirrors qwen35moe_qk_norm_heads, src/models/qwen35moe_attn.c:39-46, which + * applies the public pn_vrms_norm_f32 (src/accel/simd.c:198) once per head with + * a shared norm weight. We CALL the real pn_vrms_norm_f32 — gold standard. + * ============================================================ */ + +static int gen_qknorm_golden(golden_rng *rng) +{ + const int n_heads = QWEN35_N_HEAD; + const int head_dim = QWEN35_HEAD_DIM; + const int total = n_heads * head_dim; + + float *input = malloc((size_t)total * sizeof(float)); + float *weight = malloc((size_t)head_dim * sizeof(float)); + float *output = malloc((size_t)total * sizeof(float)); + int rc = -1; + if (!input || !weight || !output) { + fprintf(stderr, "gen_qknorm_golden: allocation failed\n"); + goto cleanup; + } + + for (int i = 0; i < total; i++) + input[i] = golden_rng_next_signed_unit(rng); + /* Norm weight is shared across heads; GGUF bakes it as ~1.0 + small offset. */ + for (int i = 0; i < head_dim; i++) + weight[i] = 1.0f + 0.1f * golden_rng_next_signed_unit(rng); + + /* REAL per-head RMSNorm — exactly what qwen35moe_qk_norm_heads does. */ + for (int h = 0; h < n_heads; h++) { + const float *head_in = input + (size_t)h * head_dim; + float *head_out = output + (size_t)h * head_dim; + pn_vrms_norm_f32(head_in, weight, head_out, head_dim, QWEN35_RMS_NORM_EPS); + } + + if (write_golden("qknorm_input.bin", "qknorm_input", "f32", + input, (size_t)total, sizeof(float)) < 0) goto cleanup; + if (write_golden("qknorm_weight.bin", "qknorm_weight", "f32", + weight, (size_t)head_dim, sizeof(float)) < 0) goto cleanup; + if (write_golden("qknorm_output.bin", "qknorm_output", "f32", + output, (size_t)total, sizeof(float)) < 0) goto cleanup; + + printf("[3] QK-RMSNorm (REAL pn_vrms_norm_f32, per head)\n"); + if (report_f32("qknorm_output", output, (size_t)total) < 0) goto cleanup; + rc = 0; + +cleanup: + free(input); + free(weight); + free(output); + return rc; +} + +/* ============================================================ + * Kernel 4: MoE routing (cross-checked replica) + * + * Replicates the routed-gating path of qwen35moe_moe.c: + * - qwen35moe_router_softmax (lines 47-61) softmax over n_expert logits + * - qwen35moe_select_topk (lines 65-89) top-k by probability + * - norm_topk_prob + scale (lines 133-136) renormalize selected weights + * - shared-expert sigmoid gate (lines 179-183) sigmoid(gate_w . x) scalar + * Output: top-k expert indices (i32), normalized+scaled weights (f32), and the + * shared-expert gate scalar (f32). Routing indices must match EXACTLY on GPU. + * ============================================================ */ + +/* sigmoid(x) — mirrors qwen35moe_sigmoid, qwen35moe_moe.c:37-40. */ +static float moe_sigmoid_replica(float x) +{ + return 1.0f / (1.0f + expf(-x)); +} + +/* In-place softmax — mirrors qwen35moe_router_softmax, qwen35moe_moe.c:47-61. */ +static void moe_router_softmax_replica(float *logits, int n_expert) +{ + float max_gl = logits[0]; + for (int e = 1; e < n_expert; e++) + if (logits[e] > max_gl) + max_gl = logits[e]; + float sum_exp = 0.0f; + for (int e = 0; e < n_expert; e++) { + logits[e] = expf(logits[e] - max_gl); + sum_exp += logits[e]; + } + float inv = 1.0f / sum_exp; + for (int e = 0; e < n_expert; e++) + logits[e] *= inv; +} + +/* Top-k selection — mirrors qwen35moe_select_topk, qwen35moe_moe.c:65-89. The + * selection-sort + first-seen tie-break MUST match the GPU bit-for-bit. */ +static float moe_select_topk_replica(const float *probs, int n_expert, int k, + int *top_idx, float *top_wt) +{ + float wt_sum = 0.0f; + for (int s = 0; s < k; s++) { + int best = -1; + float best_val = -1.0f; + for (int e = 0; e < n_expert; e++) { + int skip = 0; + for (int j = 0; j < s; j++) + if (top_idx[j] == e) { + skip = 1; + break; + } + if (!skip && probs[e] > best_val) { + best = e; + best_val = probs[e]; + } + } + top_idx[s] = best; + top_wt[s] = best_val; + wt_sum += best_val; + } + return wt_sum; +} + +static int gen_moe_routing_golden(golden_rng *rng) +{ + const int n_expert = QWEN35_N_EXPERT; + const int k = QWEN35_EXPERT_USED; + const int n_embd = QWEN35_N_EMBD; + + float *logits = malloc((size_t)n_expert * sizeof(float)); + float *probs = malloc((size_t)n_expert * sizeof(float)); + float *shared_gate_w = malloc((size_t)n_embd * sizeof(float)); + float *hidden_x = malloc((size_t)n_embd * sizeof(float)); + int rc = -1; + if (!logits || !probs || !shared_gate_w || !hidden_x) { + fprintf(stderr, "gen_moe_routing_golden: allocation failed\n"); + goto cleanup; + } + + for (int e = 0; e < n_expert; e++) + logits[e] = golden_rng_next_signed_unit(rng) * 3.0f; /* router logits */ + for (int j = 0; j < n_embd; j++) + shared_gate_w[j] = golden_rng_next_signed_unit(rng); + for (int j = 0; j < n_embd; j++) + hidden_x[j] = golden_rng_next_signed_unit(rng); + + /* Save raw router logits as the input golden (GPU recomputes from these). */ + if (write_golden("moe_router_logits.bin", "moe_router_logits", "f32", + logits, (size_t)n_expert, sizeof(float)) < 0) goto cleanup; + if (write_golden("moe_shared_gate_weight.bin", "moe_shared_gate_weight", "f32", + shared_gate_w, (size_t)n_embd, sizeof(float)) < 0) goto cleanup; + if (write_golden("moe_shared_gate_input.bin", "moe_shared_gate_input", "f32", + hidden_x, (size_t)n_embd, sizeof(float)) < 0) goto cleanup; + + /* Routed gating: softmax -> top-k -> norm_topk_prob -> expert_weights_scale. */ + memcpy(probs, logits, (size_t)n_expert * sizeof(float)); + moe_router_softmax_replica(probs, n_expert); + + int top_idx[QWEN35_EXPERT_USED]; + float top_wt[QWEN35_EXPERT_USED]; + float wt_sum = moe_select_topk_replica(probs, n_expert, k, top_idx, top_wt); + float inv_wt = (wt_sum > 0.0f) ? (1.0f / wt_sum) : 0.0f; + float scale = QWEN35_EXPERT_WT_SCALE * inv_wt; + for (int s = 0; s < k; s++) + top_wt[s] *= scale; + + /* Shared-expert scalar sigmoid gate: sigmoid(gate_w . x). */ + float gate_logit = 0.0f; + for (int j = 0; j < n_embd; j++) + gate_logit += shared_gate_w[j] * hidden_x[j]; + float shared_gate = moe_sigmoid_replica(gate_logit); + + if (write_golden("moe_topk_indices.bin", "moe_topk_indices", "i32", + top_idx, (size_t)k, sizeof(int32_t)) < 0) goto cleanup; + if (write_golden("moe_topk_weights.bin", "moe_topk_weights", "f32", + top_wt, (size_t)k, sizeof(float)) < 0) goto cleanup; + if (write_golden("moe_shared_gate_scalar.bin", "moe_shared_gate_scalar", "f32", + &shared_gate, 1, sizeof(float)) < 0) goto cleanup; + + printf("[4] MoE routing (replica of qwen35moe_moe.c softmax/top-k/norm/gate)\n"); + printf(" top-%d indices:", k); + for (int s = 0; s < k; s++) + printf(" %d", top_idx[s]); + printf("\n"); + if (report_f32("moe_topk_weights", top_wt, (size_t)k) < 0) goto cleanup; + printf(" shared_gate_scalar = %+.6f (logit %+.6f)\n", shared_gate, gate_logit); + { + double wsum = 0.0; + for (int s = 0; s < k; s++) wsum += top_wt[s]; + printf(" weights sum (should be ~%.4f) = %.6f\n", + (double)QWEN35_EXPERT_WT_SCALE, wsum); + } + rc = 0; + +cleanup: + free(logits); + free(probs); + free(shared_gate_w); + free(hidden_x); + return rc; +} + +/* ============================================================ + * Kernel 5: SSM per-token delta-rule recurrence (cross-checked replica) + * + * Replicates qwen35moe_compute_recur_heads, src/models/qwen35moe_ssm.c:163-196 + * (the static core recurrence). Per V-head h, per row j of the TRANSPOSED state + * s[h*S*S + j*S + i] == S_h[i][j]: + * (a) row[i] *= exp(g[h]) (ssm.c:180-182) + * (b) pred = sum_i row[i]*k[h][i]; + * delta = (v[h][j] - pred) * beta[h] (ssm.c:184-186) + * (c) row[i] += k[h][i]*delta (ssm.c:188-190) + * (d) y[h][j] = scale * sum_i row[i]*q[h][i] (ssm.c:192-193, read-after-write) + * q,k are taken AS-IF already L2-normed + GQA-repeated to n_v_heads (this is the + * input contract the GPU kernel receives); g is PRE-exp; beta is already sigmoid'd. + * Uses the public pn_dot_f32 for the two reductions, exactly as the source does. + * ============================================================ */ + +static void ssm_recur_replica(float *s, const float *q, const float *k, + const float *v, const float *g, const float *beta, + float *y, int n_v_heads, int S, float scale) +{ + for (int h = 0; h < n_v_heads; h++) { + float *sh = s + (size_t)h * S * S; + const float *kh = k + (size_t)h * S; + const float *qh = q + (size_t)h * S; + const float *vh = v + (size_t)h * S; + const float decay = expf(g[h]); + const float beta_h = beta[h]; + float *yh = y + (size_t)h * S; + + for (int j = 0; j < S; j++) { + float *row = sh + (size_t)j * S; /* row j: S[i][j] over i */ + for (int i = 0; i < S; i++) /* (a) scalar decay */ + row[i] *= decay; + float pred = pn_dot_f32(row, kh, S); /* (b) pred + delta */ + float delta = (vh[j] - pred) * beta_h; + for (int i = 0; i < S; i++) /* (c) rank-1 write */ + row[i] += kh[i] * delta; + yh[j] = scale * pn_dot_f32(row, qh, S); /* (d) read-after-write */ + } + } +} + +static int gen_ssm_recur_golden(golden_rng *rng) +{ + const int H = SSM_N_V_HEADS; + const int S = SSM_STATE_SIZE; + const int hs = H * S; /* q/k/v/y stream length */ + const size_t state_n = (size_t)H * S * S; + const float scale = 1.0f / sqrtf((float)S); + + float *q = malloc((size_t)hs * sizeof(float)); + float *k = malloc((size_t)hs * sizeof(float)); + float *v = malloc((size_t)hs * sizeof(float)); + float *g = malloc((size_t)H * sizeof(float)); + float *beta = malloc((size_t)H * sizeof(float)); + float *s_in = malloc(state_n * sizeof(float)); + float *s_out = malloc(state_n * sizeof(float)); + float *y = malloc((size_t)hs * sizeof(float)); + int rc = -1; + if (!q || !k || !v || !g || !beta || !s_in || !s_out || !y) { + fprintf(stderr, "gen_ssm_recur_golden: allocation failed\n"); + goto cleanup; + } + + /* q/k arrive L2-normed per head; emulate that so magnitudes are realistic. */ + for (int h = 0; h < H; h++) { + for (int d = 0; d < S; d++) { + q[h * S + d] = golden_rng_next_signed_unit(rng); + k[h * S + d] = golden_rng_next_signed_unit(rng); + v[h * S + d] = golden_rng_next_signed_unit(rng); + } + /* L2-normalize q,k per head (mirror ssm.c:70-76 divisor form). */ + float qsq = 0.0f, ksq = 0.0f; + for (int d = 0; d < S; d++) { qsq += q[h*S+d]*q[h*S+d]; ksq += k[h*S+d]*k[h*S+d]; } + float qinv = 1.0f / fmaxf(sqrtf(qsq), QWEN35_RMS_NORM_EPS); + float kinv = 1.0f / fmaxf(sqrtf(ksq), QWEN35_RMS_NORM_EPS); + for (int d = 0; d < S; d++) { q[h*S+d] *= qinv; k[h*S+d] *= kinv; } + /* g is PRE-exp: small negative so exp(g) in (0,1), as the decay gate yields. */ + g[h] = -0.05f - 0.1f * (golden_rng_next_signed_unit(rng) * 0.5f + 0.5f); + beta[h] = moe_sigmoid_replica(golden_rng_next_signed_unit(rng)); + } + for (size_t i = 0; i < state_n; i++) + s_in[i] = 0.1f * golden_rng_next_signed_unit(rng); + + /* Run the replica on a copy of the input state (state is updated in place). */ + memcpy(s_out, s_in, state_n * sizeof(float)); + ssm_recur_replica(s_out, q, k, v, g, beta, y, H, S, scale); + + if (write_golden("ssm_recur_state_in.bin", "ssm_recur_state_in", "f32", + s_in, state_n, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_q.bin", "ssm_recur_q", "f32", q, (size_t)hs, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_k.bin", "ssm_recur_k", "f32", k, (size_t)hs, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_v.bin", "ssm_recur_v", "f32", v, (size_t)hs, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_g.bin", "ssm_recur_g", "f32", g, (size_t)H, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_beta.bin", "ssm_recur_beta", "f32", beta, (size_t)H, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_y.bin", "ssm_recur_y", "f32", y, (size_t)hs, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_recur_state_out.bin", "ssm_recur_state_out", "f32", + s_out, state_n, sizeof(float)) < 0) goto cleanup; + + printf("[5] SSM recurrence (replica of qwen35moe_ssm.c:163-196)\n"); + if (report_f32("ssm_recur_y", y, (size_t)hs) < 0) goto cleanup; + if (report_f32("ssm_recur_state_out", s_out, state_n) < 0) goto cleanup; + rc = 0; + +cleanup: + free(q); free(k); free(v); free(g); free(beta); + free(s_in); free(s_out); free(y); + return rc; +} + +/* ============================================================ + * Kernel 6: SSM causal conv1d (k=4) + SiLU (cross-checked replica) + * + * Replicates qwen35moe_conv1d_silu, src/models/qwen35moe_ssm.c:110-135. Per + * channel c over conv_dim: out[c] = SiLU(history . w[0..k-2] + qkv[c]*w[k-1]), + * then roll the per-channel conv_state (drop oldest, append raw qkv[c]). + * conv_state layout [conv_dim][k-1], weight row-major [conv_dim, k]. + * ============================================================ */ + +static float ssm_silu_replica(float x) /* mirror qwen35moe_silu (ssm.c:60-63) */ +{ + return x / (1.0f + expf(-x)); +} + +static void ssm_conv1d_replica(const float *qkv, const float *weight, + float *conv_state, float *out, + int conv_dim, int k) +{ + const int km1 = k - 1; + for (int c = 0; c < conv_dim; c++) { + const float *cw = weight + (size_t)c * k; + float *cs = conv_state + (size_t)c * km1; + float acc = qkv[c] * cw[km1]; /* ssm.c:125-127 */ + for (int t = 0; t < km1; t++) + acc += cs[t] * cw[t]; + out[c] = ssm_silu_replica(acc); /* ssm.c:128 */ + for (int t = 0; t < km1 - 1; t++) /* ssm.c:131-133 roll */ + cs[t] = cs[t + 1]; + cs[km1 - 1] = qkv[c]; + } +} + +static int gen_ssm_conv_golden(golden_rng *rng) +{ + const int C = SSM_CONV_DIM; + const int k = SSM_CONV_KERNEL; + const int km1 = k - 1; + const size_t st_n = (size_t)C * km1; + const size_t w_n = (size_t)C * k; + + float *qkv = malloc((size_t)C * sizeof(float)); + float *weight = malloc(w_n * sizeof(float)); + float *st_in = malloc(st_n * sizeof(float)); + float *st_out = malloc(st_n * sizeof(float)); + float *out = malloc((size_t)C * sizeof(float)); + int rc = -1; + if (!qkv || !weight || !st_in || !st_out || !out) { + fprintf(stderr, "gen_ssm_conv_golden: allocation failed\n"); + goto cleanup; + } + + for (int i = 0; i < C; i++) + qkv[i] = golden_rng_next_signed_unit(rng); + for (size_t i = 0; i < w_n; i++) + weight[i] = 0.5f * golden_rng_next_signed_unit(rng); + for (size_t i = 0; i < st_n; i++) + st_in[i] = golden_rng_next_signed_unit(rng); + + memcpy(st_out, st_in, st_n * sizeof(float)); /* state rolls in place */ + ssm_conv1d_replica(qkv, weight, st_out, out, C, k); + + if (write_golden("ssm_conv_qkv.bin", "ssm_conv_qkv", "f32", qkv, (size_t)C, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_conv_weight.bin", "ssm_conv_weight", "f32", weight, w_n, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_conv_state_in.bin", "ssm_conv_state_in", "f32", st_in, st_n, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_conv_out.bin", "ssm_conv_out", "f32", out, (size_t)C, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_conv_state_out.bin", "ssm_conv_state_out", "f32", st_out, st_n, sizeof(float)) < 0) goto cleanup; + + printf("[6] SSM conv1d+SiLU (replica of qwen35moe_ssm.c:110-135)\n"); + if (report_f32("ssm_conv_out", out, (size_t)C) < 0) goto cleanup; + if (report_f32("ssm_conv_state_out", st_out, st_n) < 0) goto cleanup; + rc = 0; + +cleanup: + free(qkv); free(weight); free(st_in); free(st_out); free(out); + return rc; +} + +/* ============================================================ + * Kernel 7: SSM gated RMSNorm (REAL pn_vrms_norm_f32 + silu replica) + * + * Mirrors qwen35moe_gated_norm, src/models/qwen35moe_ssm.c:206-223: + * pn_vrms_norm_f32(y_h, w, gated_h, S, eps) then gated_h[d] *= silu(z_h[d]). + * Calls the REAL pn_vrms_norm_f32 per head (gold standard); silu mirrors source. + * ============================================================ */ + +static int gen_ssm_gated_golden(golden_rng *rng) +{ + const int H = SSM_N_V_HEADS; + const int S = SSM_STATE_SIZE; + const int hs = H * S; + + float *y = malloc((size_t)hs * sizeof(float)); + float *weight = malloc((size_t)S * sizeof(float)); + float *z = malloc((size_t)hs * sizeof(float)); + float *gated = malloc((size_t)hs * sizeof(float)); + int rc = -1; + if (!y || !weight || !z || !gated) { + fprintf(stderr, "gen_ssm_gated_golden: allocation failed\n"); + goto cleanup; + } + + for (int i = 0; i < hs; i++) { + y[i] = golden_rng_next_signed_unit(rng); + z[i] = golden_rng_next_signed_unit(rng); + } + for (int d = 0; d < S; d++) /* shared norm weight, ~1.0 + offset */ + weight[d] = 1.0f + 0.1f * golden_rng_next_signed_unit(rng); + + for (int h = 0; h < H; h++) { /* mirror ssm.c:214-222 */ + const float *yh = y + (size_t)h * S; + const float *zh = z + (size_t)h * S; + float *gh = gated + (size_t)h * S; + pn_vrms_norm_f32(yh, weight, gh, S, QWEN35_RMS_NORM_EPS); + for (int d = 0; d < S; d++) + gh[d] *= ssm_silu_replica(zh[d]); + } + + if (write_golden("ssm_gated_y.bin", "ssm_gated_y", "f32", y, (size_t)hs, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_gated_weight.bin", "ssm_gated_weight", "f32", weight, (size_t)S, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_gated_z.bin", "ssm_gated_z", "f32", z, (size_t)hs, sizeof(float)) < 0) goto cleanup; + if (write_golden("ssm_gated_out.bin", "ssm_gated_out", "f32", gated, (size_t)hs, sizeof(float)) < 0) goto cleanup; + + printf("[7] SSM gated RMSNorm (REAL pn_vrms_norm_f32 * silu(z), per head)\n"); + if (report_f32("ssm_gated_out", gated, (size_t)hs) < 0) goto cleanup; + rc = 0; + +cleanup: + free(y); free(weight); free(z); free(gated); + return rc; +} + +/* ============================================================ + * Entry point + * ============================================================ */ + +int main(void) +{ + printf("qwen35moe golden-vector generator (seed=0x%08X)\n", (unsigned)GOLDEN_SEED); + printf("output dir: %s/\n\n", GOLDEN_DIR); + + /* One RNG threaded through all kernels: the full output stream is determined + * solely by the seed and the (fixed) order of generation calls below. */ + golden_rng rng; + golden_rng_init(&rng, GOLDEN_SEED); + + int rc = 0; + rc |= gen_q3k_golden(&rng); + printf("\n"); + rc |= gen_rope_golden(&rng); + printf("\n"); + rc |= gen_qknorm_golden(&rng); + printf("\n"); + rc |= gen_moe_routing_golden(&rng); + printf("\n"); + rc |= gen_ssm_recur_golden(&rng); + printf("\n"); + rc |= gen_ssm_conv_golden(&rng); + printf("\n"); + rc |= gen_ssm_gated_golden(&rng); + + printf("\n%s\n", rc == 0 ? "ALL GOLDEN VECTORS GENERATED OK" + : "GENERATION FAILED — see errors above"); + return rc == 0 ? 0 : 1; +} diff --git a/tests/golden/README.md b/tests/golden/README.md new file mode 100644 index 0000000..14996d7 --- /dev/null +++ b/tests/golden/README.md @@ -0,0 +1,242 @@ +# qwen35moe CPU-reference golden vectors + +Trusted **CPU-reference** input/output vectors for the four qwen35moe GPU kernels +being written for a K80 host. The CUDA kernels must reproduce these outputs +from the same inputs (within the tolerances below) — the same isolation-test +discipline that already caught the Q3_K dequant high-mask bug and the SSM +head-tiling bug. + +- Generator: `tests/gen_golden_qwen35.c` +- Regenerate: `make USE_BLAS=1 golden` (from the Pantheon repo root) +- **No model GGUF is involved.** All inputs are synthetic, produced by a + fixed-seed xorshift32 PRNG seeded with `0x9E3779B9`. A single RNG is threaded + through all four kernels in the order they appear below, so the entire output + set is fully determined by that seed and the generation order. + +## File format + +Every `*.bin` file is self-describing: + +``` +line 1 (ASCII, newline-terminated): PNGOLDEN1 seed=0x9E3779B9 +remainder: raw little-endian values +``` + +`` is one of `f32` (4-byte IEEE-754 little-endian), `i32` (4-byte signed +little-endian), or `u8` (raw bytes). Read past the first newline, then read +`count * sizeof(dtype)` bytes of payload. Shapes/semantics are documented +per-file below (the header carries the flat element count only). + +## Config constants used + +| Constant | Value | Source | +|---|---|---| +| `n_embd` | 2048 | qwen35moe config | +| `n_head` | 16 | qwen35moe config | +| `n_head_kv` | 2 | qwen35moe config | +| `head_dim` | 256 | qwen35moe config | +| `rope_dim` (partial) | 64 | qwen35moe config | +| `rope_theta` (freq base) | 1e7 | model facts (`rope.freq_base`) | +| `rms_norm_eps` | 1e-6 | model facts (`attention.layer_norm_rms_epsilon`) | +| `n_expert` | 256 | qwen35moe config | +| `expert_used` (top-k) | 8 | qwen35moe config | +| `expert_ff` | 512 | qwen35moe config | +| `expert_weights_scale` | 1.0 | GGUF default (no key) | + +PRNG seed: `0x9E3779B9`. + +--- + +## Kernel 1 — Q3_K dequant + Q3_K matvec (HIGHEST VALUE) + +Golden output produced by the **REAL** CPU functions: `pn_qmv` → +`pn_qdot` → `dot_q3_k` → `pn_dequantize_block_q3_k` +(`src/core/qweight.c`, `src/formats/gguf_dequant.c`). The synthetic weight +matrix is built by filling the real `pn_block_q3_k` struct +(`hmask[32]`, `qs[64]`, `scales[12]`, fp16 `d`) with deterministic +pseudo-random bytes, so every bit pattern is decoded exactly as a GPU kernel +must decode it. Matrix shape: **rows=24, cols=1024** (4 Q3_K super-blocks/row). + +| File | dtype | count | shape / meaning | +|---|---|---|---| +| `q3k_matvec_weight.bin` | u8 | 10560 | raw Q3_K blocks, 24 rows × 4 blocks × 110 bytes (row-major) | +| `q3k_matvec_input.bin` | f32 | 1024 | input activation vector (cols) | +| `q3k_matvec_output.bin` | f32 | 24 | `out[r] = W[r,:] · input` (rows) — golden | +| `q3k_dequant_block0.bin` | f32 | 256 | dequantized super-block 0 of the weight (dequant-only isolation) | + +**Tolerance:** matvec/dequant on Kepler fp32 — **~1e-4 .. 1e-3 relative** +(per output element). Use the dequant-only file first to localize a mismatch to +the dequant vs. the dot-product. + +## Kernel 2 — Partial NEOX RoPE + +Golden produced by a **cross-checked replica** of the static helper +`qwen35moe_rope_partial` (`src/models/qwen35moe_attn.c:57-76`) — replicated +term-for-term (cited inline in the generator). Only the first `rope_dim=64` +dims of each `head_dim=256` head are rotated (NEOX pairing `j` vs `j+32`); dims +`[64,256)` pass through unchanged. `theta_base=1e7`. Verified that pos 0 is an +exact identity (angle 0). + +Pre-rotation inputs (shared across all positions): + +| File | dtype | count | shape | +|---|---|---|---| +| `rope_q_input.bin` | f32 | 4096 | Q, `n_head(16) × head_dim(256)` | +| `rope_k_input.bin` | f32 | 512 | K, `n_head_kv(2) × head_dim(256)` | + +Rotated goldens, one pair per position (pos ∈ {0, 1, 7, 100}): + +| File | dtype | count | meaning | +|---|---|---|---| +| `rope_q_pos

.bin` | f32 | 4096 | Q rotated at position P | +| `rope_k_pos

.bin` | f32 | 512 | K rotated at position P | + +**Tolerance:** **~1e-4 .. 1e-3 relative** (trig in fp32 on Kepler). Note GPU +`sinf`/`cosf`/`powf` may differ slightly from the host libm; pos 0 should match +to ~0 (identity). + +## Kernel 3 — QK-RMSNorm + +Golden produced by the **REAL** CPU function `pn_vrms_norm_f32` +(`src/accel/simd.c:198`), applied per head exactly as `qwen35moe_qk_norm_heads` +(`src/models/qwen35moe_attn.c:39-46`) does — one shared `head_dim` norm weight, +`eps=1e-6`. Input is the 16-head Q layout `[n_head(16) × head_dim(256)]`. + +| File | dtype | count | shape / meaning | +|---|---|---|---| +| `qknorm_input.bin` | f32 | 4096 | per-head input, `16 × 256` | +| `qknorm_weight.bin` | f32 | 256 | shared per-head norm weight (≈1 ± 0.1) | +| `qknorm_output.bin` | f32 | 4096 | RMSNorm'd output, `16 × 256` — golden | + +**Tolerance:** **~1e-4 .. 1e-3 relative**. RMS uses `1/sqrt(mean(x²)+eps)`; the +sum-of-squares reduction order may differ on GPU. + +## Kernel 4 — MoE routing + +Golden produced by a **cross-checked replica** of the routed-gating path of +`src/models/qwen35moe_moe.c`: +`qwen35moe_router_softmax` (lines 47-61), `qwen35moe_select_topk` (lines 65-89), +`norm_topk_prob` + `expert_weights_scale` (lines 133-136), and the shared-expert +scalar sigmoid gate `sigmoid(gate_w · x)` (lines 179-183). Replicated +term-for-term (cited inline). `n_expert=256`, top-k=8, `expert_weights_scale=1.0`. + +Top-k selection is a selection sort with **first-seen tie-break** — the indices +must match **exactly** on GPU; any GPU implementation must use the same +tie-break to be bit-identical here. + +Inputs: + +| File | dtype | count | meaning | +|---|---|---|---| +| `moe_router_logits.bin` | f32 | 256 | raw router logits (pre-softmax) | +| `moe_shared_gate_weight.bin` | f32 | 2048 | shared-expert gate weight (`n_embd`) | +| `moe_shared_gate_input.bin` | f32 | 2048 | hidden state x for the gate dot (`n_embd`) | + +Goldens: + +| File | dtype | count | meaning | +|---|---|---|---| +| `moe_topk_indices.bin` | i32 | 8 | selected expert indices, in selection order | +| `moe_topk_weights.bin` | f32 | 8 | renormalized + scaled weights (sum ≈ 1.0) | +| `moe_shared_gate_scalar.bin` | f32 | 1 | `sigmoid(gate_w · x)` scalar gate | + +Reference values from this seed: indices `[1, 93, 212, 144, 170, 76, 180, 203]`, +weights sum `1.000000`, shared gate `0.051038`. + +**Tolerance:** top-k **indices must match EXACTLY** (integer). Weights and the +gate scalar: **~1e-5 .. 1e-4 relative** (softmax/sigmoid in fp32). + +--- + +## Kernel 5 — SSM per-token delta-rule recurrence (HIGHEST VALUE, the centerpiece) + +Golden produced by a **cross-checked replica** of `qwen35moe_compute_recur_heads` +(`src/models/qwen35moe_ssm.c:163-196`) — the static core recurrence, replicated +term-for-term with the source cited inline (the two length-S dots call the public +`pn_dot_f32`, exactly as the source does). Single-token (decode) path. + +Config: `n_v_heads=32`, `state_size S=128`, `scale=1/sqrt(128)`. The state is +**transposed**: `s[h*S*S + j*S + i] == S_h[i][j]` (row j contiguous over i). For +each V-head h, each row j: +- (a) `row[i] *= exp(g[h])` — scalar decay of the whole row (ssm.c:180-182) +- (b) `pred = Σ_i row[i]·k[h][i]`; `delta = (v[h][j] − pred)·beta[h]` (ssm.c:184-186) +- (c) `row[i] += k[h][i]·delta` — rank-1 write, in place (ssm.c:188-190) +- (d) `y[h][j] = scale · Σ_i row[i]·q[h][i]` — **read-after-write** (ssm.c:192-193) + +q/k are supplied **already L2-normed + GQA-repeated** to `n_v_heads` (the GPU +kernel's input contract; the 16→32 repeat happens on the host before the kernel). +`g` is **pre-exp** (the kernel applies `exp`); `beta` is already sigmoid'd. + +Inputs: + +| File | dtype | count | shape / meaning | +|---|---|---|---| +| `ssm_recur_state_in.bin` | f32 | 524288 | initial transposed state, `32×128×128` (`h,j,i`) | +| `ssm_recur_q.bin` | f32 | 4096 | q, `32×128` (L2-normed + GQA-repeated) | +| `ssm_recur_k.bin` | f32 | 4096 | k, `32×128` (L2-normed + GQA-repeated) | +| `ssm_recur_v.bin` | f32 | 4096 | v, `32×128` | +| `ssm_recur_g.bin` | f32 | 32 | per-V-head decay (pre-exp; kernel does `exp(g)`) | +| `ssm_recur_beta.bin` | f32 | 32 | per-V-head beta (already sigmoid'd) | + +Goldens: + +| File | dtype | count | meaning | +|---|---|---|---| +| `ssm_recur_y.bin` | f32 | 4096 | recurrence output y, `32×128` | +| `ssm_recur_state_out.bin` | f32 | 524288 | updated transposed state (decayed + rank-1 write) | + +**Tolerance:** two 128-wide dot reductions per (h,j) in fp32 on Kepler — +**~1e-4 .. 1e-3 relative**. The harness diffs **both** y and the updated state. + +## Kernel 6 — SSM causal conv1d (k=4) + SiLU + +Golden produced by a **cross-checked replica** of `qwen35moe_conv1d_silu` +(`src/models/qwen35moe_ssm.c:110-135`), replicated term-for-term. Depthwise +causal conv over the full `conv_dim=8192` stream with `conv_kernel=4`: +`out[c] = SiLU(history·w[0..k-2] + qkv[c]·w[k-1])`, then the per-channel +`conv_state` is rolled (drop oldest, append raw `qkv[c]`). `conv_state` layout is +`[conv_dim][k-1]` (oldest..newest); weight is row-major `[conv_dim, k]`. + +Inputs: + +| File | dtype | count | shape / meaning | +|---|---|---|---| +| `ssm_conv_qkv.bin` | f32 | 8192 | raw projected stream this token (`conv_dim`) | +| `ssm_conv_weight.bin` | f32 | 32768 | depthwise conv weights, `8192×4` row-major | +| `ssm_conv_state_in.bin` | f32 | 24576 | conv history, `8192×3` (`[conv_dim][k-1]`) | + +Goldens: + +| File | dtype | count | meaning | +|---|---|---|---| +| `ssm_conv_out.bin` | f32 | 8192 | SiLU'd convolved stream (`conv_dim`) | +| `ssm_conv_state_out.bin` | f32 | 24576 | rolled conv_state, `8192×3` | + +**Tolerance:** small fixed-tap dot + SiLU in fp32 — **~1e-4 .. 1e-3 relative**. +The harness diffs **both** the output and the rolled conv_state. + +## Kernel 7 — SSM gated RMSNorm + +Golden produced by the **REAL** CPU function `pn_vrms_norm_f32` per head plus a +SiLU replica, mirroring `qwen35moe_gated_norm` +(`src/models/qwen35moe_ssm.c:206-223`): +`gated[h*S+d] = RMSNorm(y_h)[d] · weight[d] · silu(z[h*S+d])`. The norm weight is +shared across heads; the trailing `ssm_out` projection is **out of scope** (the +GPU reuses `fc_matvec_q3k`). `n_v_heads=32`, `state_size S=128`. + +Inputs: + +| File | dtype | count | shape / meaning | +|---|---|---|---| +| `ssm_gated_y.bin` | f32 | 4096 | recurrence output y, `32×128` | +| `ssm_gated_weight.bin` | f32 | 128 | shared ssm_norm weight (`state_size`) | +| `ssm_gated_z.bin` | f32 | 4096 | attn_gate projection z (pre-silu), `32×128` | + +Golden: + +| File | dtype | count | meaning | +|---|---|---|---| +| `ssm_gated_out.bin` | f32 | 4096 | `RMSNorm(y)·w·silu(z)`, `32×128` | + +**Tolerance:** per-head RMS reduction (128 elems) + silu in fp32 on Kepler — +**~1e-4 .. 1e-3 relative**. diff --git a/tests/golden/moe_router_logits.bin b/tests/golden/moe_router_logits.bin new file mode 100644 index 0000000000000000000000000000000000000000..e969336fdcaa28953f0e7b82f8bb0fa697a007c8 GIT binary patch literal 1076 zcmV~$4N%i{7y#g=1Z>LZ2*%VkhJwoC1~}$a{_pQ5#PR^0ddEj7hA`A1iIFZ9Pw5n_ zofwXDhzaEK6hjPKD`G^Eq=^sT*4M_E$7}+D9bV0vB9H>^DZ#* zLiHhVB)){B5rrUe97Ly=C+l~L*~BX!kX&@9VDDu_k9NTNMN)j$d>?HqI#I7{<~$A0 zU`*giPZAHoIej2=De?gSjcK5K=}Nz(%wj3rDVW<^&!rbv^2b*Ej-s#;sFs&PoWKdn z19cE$ih#DxRxUDi6WV5{@;SLSD0OORPi-DtQ@+6vjet!UJ_3EB2eE*M?OoyF?3iE% zvv!LirE7pcU++&7LyIBbQP%FFmw`2UE40lT#Dh_SwE#ycsCQ_g~DQG+l^$ zETL!~|CW0plhei2leoWU9_n`mQFr_%nBl!hHg6#<)rZlPUC8>siy?QnMl{qfVz-yr z!C3wSUajmw`KT{D>n^5AQ!o`9RM-*x2Mk*0fbor*<%v$fE%jU``(Dm&nq#4@rWp(O zO4)Vo6&$!y#Z4BuA$LeblUYEl;ma5Y^MzE!FvfiK1k^Qs5TXmF_Z9K5G|GhC)mGTT zSHX@b6)6k)VcYZvY>B8E{#mPHpK7&uzb$|)fy*$r@g4k?#$i&N2Tqo_Q0F>7>TS-4 z3q_x>^O-Ne9OUP&V4E{Ik8cyj-!BRZ|GgoyW*AS;# z9gXQDtMP!@hoivLyjAimcf(Ub<$o@q)S2rz=ZYIS#@)EgUN2TzWW_VS6|iblOr9I& zGuv_%@2X0{JC9vxTXPuQ9`a``>6iKA(ps$Fd;tRQDww2qGZ&h61D4k%bH_zxAp0p7 Q^|=Q@y4#6I9-fE)0Y(Gw;{X5v literal 0 HcmV?d00001 diff --git a/tests/golden/moe_shared_gate_input.bin b/tests/golden/moe_shared_gate_input.bin new file mode 100644 index 0000000000000000000000000000000000000000..458a4308576fbd8a30f7d432989407d581152d87 GIT binary patch literal 8249 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 literal 0 HcmV?d00001 diff --git a/tests/golden/moe_shared_gate_scalar.bin b/tests/golden/moe_shared_gate_scalar.bin new file mode 100644 index 0000000..03df708 --- /dev/null +++ b/tests/golden/moe_shared_gate_scalar.bin @@ -0,0 +1,2 @@ +PNGOLDEN1 moe_shared_gate_scalar f32 1 seed=0x9E3779B9 +ž Q= \ No newline at end of file diff --git a/tests/golden/moe_shared_gate_weight.bin b/tests/golden/moe_shared_gate_weight.bin new file mode 100644 index 0000000000000000000000000000000000000000..ebf870a14d3ade6adab391f7f4d66692471e013a GIT binary patch literal 8250 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 literal 0 HcmV?d00001 diff --git a/tests/golden/moe_topk_indices.bin b/tests/golden/moe_topk_indices.bin new file mode 100644 index 0000000000000000000000000000000000000000..f6fe2a27c1f7e9e18552ffdfab3613ae46d8927b GIT binary patch literal 81 zcmWIWbNBagarHA)$jwiUFUc>+j?c_X$xKcyR>(9qQm{}cPEAd*HK?$3H8wZ5bh6}P aWME*31>!3}JOPMT0kIDdZvo=dKnwt~Hxoqw literal 0 HcmV?d00001 diff --git a/tests/golden/moe_topk_weights.bin b/tests/golden/moe_topk_weights.bin new file mode 100644 index 0000000..053480c --- /dev/null +++ b/tests/golden/moe_topk_weights.bin @@ -0,0 +1,3 @@ +PNGOLDEN1 moe_topk_weights f32 8 seed=0x9E3779B9 + µ>6t>¼X>¥>š +ñ=0'é=m¥æ=¬Úä= \ No newline at end of file diff --git a/tests/golden/q3k_dequant_block0.bin b/tests/golden/q3k_dequant_block0.bin new file mode 100644 index 0000000000000000000000000000000000000000..ba64fae7af6567c8fdc7e83855f66ab7f0ee23ec GIT binary patch literal 1077 zcmZ8fJxe4(5Uo^&i`PUpb3=7?WP_O&QB3?;^asd!t_a@kAu0$mFp;pyB4#GWMka=` z)BXViSQ!%5S5JF$ZEJmHb#u0U+&tRd?;M}~?4IuK{r>yw zXr_M9oU6|*B=wU{XFr`eZ>=^L7hB7%aoN5tflO+{RBAir7i8+yh6DAi26d>R)|mA} zo^{B3Fk=F7%q#N@b52AG^LH#x#yoN&NiP%Xmkp;#dS%5)&%?cuct19~EwJCDM!0VZd zKVFFKDej?AFsA0WQTcdt=BgO=^xGc}@QQezJi7BTYAk8qm~~EbqXs?R0dC)x2rOu_g_^)`p$g`{GZyn{RK{7^viJ^3!yhwrIg-_Z*`b5DRV bXQ+0jd&B;?L+&-_E_nxnI%T=$$jJQ%^4Dlf literal 0 HcmV?d00001 diff --git a/tests/golden/q3k_matvec_input.bin b/tests/golden/q3k_matvec_input.bin new file mode 100644 index 0000000000000000000000000000000000000000..02072870a9819746c4dcdff798462f47f6966a7b GIT binary patch literal 4148 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 HkkkJGzP+S{ literal 0 HcmV?d00001 diff --git a/tests/golden/q3k_matvec_output.bin b/tests/golden/q3k_matvec_output.bin new file mode 100644 index 0000000000000000000000000000000000000000..383f24f7121a4fdb002db537aa0c5e2712d70f79 GIT binary patch literal 147 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 Yawqr1x&>4@4R(<$g;lzjvt*upfm$G#7zB*+nEY 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

GwUMdw1NSJKL8-?bBJX>X;J! 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 H3WNUvY)Y4A literal 0 HcmV?d00001 diff --git a/tests/golden/qknorm_weight.bin b/tests/golden/qknorm_weight.bin new file mode 100644 index 0000000000000000000000000000000000000000..9aa849608f3e6e3db2c4957ab699c6d08d55998d GIT binary patch literal 1072 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 G1pgmIblXb+ literal 0 HcmV?d00001 diff --git a/tests/golden/rope_k_pos1.bin b/tests/golden/rope_k_pos1.bin new file mode 100644 index 0000000000000000000000000000000000000000..45b12162754a2443314a611f2e91366d1d83bedc GIT binary patch literal 2094 zcmWO3i$B$O9{}(}W`@E-LOROjbP-~?bbj9t=1!Q~)Ov9$N+g%#p37;kE|M3DEtALG z@~DVpb(Hh@sI*GftW*>pR2!ZqAupu%Jnw(u?dRj>>+S5~W64j5jf)724vUKwTk`i> zZRUSvxtTAHhzRE_Qte!q~>b}=&q%hT2DmL#`ib4zDZ9m_ht%d zn`hx%%nnpM7{P{Y?xLH*9bEDJ0k>;gIQ@6r9-7bj(W-WD=sL6#=o6u0v=QOhNA-BKJg(Aj2a;M`*@pwTmOD}zi+>PhF{Be$X4#Y5 zVv;p21Hqr8oba+K=)Tt{%a7TxZ?Iby8Fv|@Z-=wdUPA~j?&1~{LP_hxJEnMWKNM<} zbEVZOAkrS>h8jJ%oCarV7gx6F-zMY<+OWOv88>ys4Q!?FapbHAC=0~YRbPaXP#flO z=dH|PWF4CL>S7vsKhRoI!ZfOMH!#tI~kMcS`f2XU%)mF4l@Tm6ZC7$ z0BO!{tkcmziK#o34>(g{Ko|B}^B^lKenSjAU8f8kmDO007$!?Km}R^|W3ES|gKcTh zgq$EnnlTIq>9>Djd-QQGuri00*$uM`ZcZ>h=?@87j+9sR7aC}IP`__3de1cCV6g)6 zs_xx*-m#z54a}He)ioUVvj#!n7&fs>vR86xWbN3;0y93K!)*a*E{4Gl-$V$$lL}1_ zh*N)ak1KsB2R-Z4spe`U&hX{IejuKtb1yM+avcjUEjG`CfVIwN%E6 zcUH`AR~MG}*s_X~*(4b?5mwZm!{+R@#4~x`mQy2P$0nnxBKbL5wA?}gZJnKvmfje=2SUDjfDnqAEc0sf)Sz#=^iYoB{jPRj)Ak~o5Uiw+JZ zr&6Lw%1VClB5RBF{FQ45Cn7lvcH{H~WfN>2GE* zb&tYhhKO+oJrA+tP!(>~oY>(eTD za;F;QZ}^62=`^eL-VWQFgTZoJAh~q9QnaR&HL4o2Xn!N9Ds#sP-Cyvcq@1;1EE2=yuo(Ce6u-K7g$=Uy=^rfL(v#T`KzKwFExO2&B<)q( z&epRSrym44`y!Z;XAAO*|F|v+2xL`CS~Qo^j7=I>WIZp=Fu_;|NzY8Pr!8AZG=2$j zj}bT+tO3btAuAW#K#g`i6LwjFzN!RkRBXWSY7s{9)u8GRFF3L-o-}Lq=}7h@mYrS1 zxlSh<+StW4dDh8L-WLQ33+V4?1ybp8EU5?uRd)qiObVgoO+4r_)`HX*xo9zd8v9-E zVk$b}E30PgH~t6vUw5&^OICDRP7EeK`@vv97vg>%=ltr%S)FYrwCP0y-}eA#zeElu z$1GTWYc%C<)d#_vlPpwK0Uln}2EHG61E@&J9w zAda3d#=N!*jQ`;)c0E|0Ook7EdV>p99ITZIji%USpALQcI)k1@q=6_an1#nhlVp>e zP;w}VO%?>9{75|I*Q!E=qnj*CTaO~&IzXqK79@o0h_ki9p_99r&S7of I?U4!p2e%#IU;qFB literal 0 HcmV?d00001 diff --git a/tests/golden/rope_k_pos100.bin b/tests/golden/rope_k_pos100.bin new file mode 100644 index 0000000000000000000000000000000000000000..81f8269b1ee1f9ccc435ef866d94f48a3a423b90 GIT binary patch literal 2096 zcmWO3i$B!)8UWxSTcSc`64JD8qninvOMc(iIOLLLHbYbSFlj{T;(p0(+EcOR6QYxq z(+WA-L#ef*oq1oCHb=IVWGPwcvN&CYbtu)@=U;dv{$2ro9-jW|1aSvqBEq7>Vh+Tw z6Nv=7?Ck_!tg{ouM?@$%QHryty`!VEyR*)YwB=Ccz(I(WEBiBGQ*SrC_-`;p>=TkI;T03^ zPollO3xL0Qm|J3C4Ugt8BcIVM_*B}V&OdY+hc7DFkG(6PEVi9{pczWGQ*YVe{@svo zc$O=vNCr8-pBuR3!{yX^P;>k_mFZ1Fo=}C&y}xl|KX`#tZh{{k$ z=636i+HKH=jTU;bO55ecQ#^1DWt*tj`2e+)w6L;L0Vi;`B>B}KCR&-tR`)+)ZYEYJ zxs(o#IpJ7sWPnO*Z#etdgYpC0vB!Z2nI~qGw5pf!PkGVo=VF*B$z}2t0burl2T?UZ zM|G@epdf>qljm~0{GXk*Rkh5@-h#T$20_iLeb8;A11;w&uq+`=onki0c=?uGmq80# zU%MD`f@agi3I#N7`2(AyvpMOx99HW5gnjSj4kM$RA>?VD)HB7kdDvn4TKqwu?I(AY0Oe2*X#JxNeS<`h1OnyEtiI&u!Q zO)rAU=?qXbsWJ4sKu|6SVXeG*tjbys&T=WF-xkjX#C>YbX-(j5oA467b)qMZ8ItX(Mv?|LKb zPfDQ#c_S-6wTT==E6Kw*lwKFw5-wQ;i?5`^K<;cB%dmrY%XL5=YK)EL_3UM25i9v+ z4UDz=Lx)}xN5!HkczvD*tX6URCKfuI}+kq{W&Qd zJYor|gSO<|lL_Vev7|_9U`^Nmj@l9d>?l$)Yv%;;_bI?_Ob+|iA(-v9mwfLifx~>( z7El2NVZX6S$N6NoeHZcm7lpY}OR~UncDo~%HY8iJtnJotH4o8q>0#`vyQeLN zdTTnVHqYMxr|u82g;S52-U&XKzPF;8^l4V;abB&m(?+&A5_2C^VB@#*+2|QD4Hax> zev>b8beV|S2G;@4X!cJx^0cArh#|?BtVBh^av18l&x~}FC^#mOWSyz>5(2@rF9q~x z3b-zpN)+_I;1qjau?Y)bC}|c$jjk5-h_kSxT;~k-rU&CD6?ZI@O5-W8bLDEo&djxt=es2>T{xX&pSDDh`tWhjIGlNrY z?liEvovZV$R-xA8Wn zpgTUZzk+?1pRn(FJDa&^PbW3v!ODL(m_0UznD0h7N%aV;cFF*iNi+xo_Hr(>G+=aC z#7;Ct)3FVvApHC|3!SG0ceCQ5Pte3lAH8Lg#Bnv>-yHn)*OGV5znO!c3)Ch0fT^+{ zhb|OhuIhUxc>e{v7OY8DPtriY)|1NiSE*es#@J}D5qPM{mvzcp`K!kwWw01!}j9Z0_e>0@7Y@Os<1z)YGwr#jGDiVLbvCexYtve zr2ph8I>-*Ou775u$D6~f%&uGvBzry#I(b>-v%E!bXU3a>Rk-;mqg|A9y>} GuKx$Ove0k< literal 0 HcmV?d00001 diff --git a/tests/golden/rope_k_pos7.bin b/tests/golden/rope_k_pos7.bin new file mode 100644 index 0000000000000000000000000000000000000000..802387fe814c05566b226982cea56bddff59ba6d GIT binary patch literal 2094 zcmWO3i#yc$8UXMi+ltnukOyhXZ7M~vxir7;YecT0T(Z?+5=E5JAj)Ohry_Yu(I!+v zjyjZ$Eo9!;wrq}UXI3gCE8XN!a$B`p=Y0N!kDrg5ueXb<&l>)LSV=@^bf_d&YRBJg zvzGtunzejsL_|1eo#fzZV`t~!?4TAB&%kt9|l^vd(LF+-{ z_-R;}vJFS}N3ijA_pwZ+1us8+z(s8erv?9Y1iI=+8cp7Cu1CQ#BOJJ&ylvUh6|GqQ zESvoq;tute5ww{GapM<#wni$Xr1VKH#^NmwWc4xaW&OBI5d)dqe5w58Qfe@rVlg99 z64hux!|))N^-l{ZoHC-QnRJl!DCE+(A92;aa5mYs6ohBnxo(vZ8vOi`>Fke!9NkM? zLFqvd>-BN{H$Aw_Di>;!UTHM?g^(v`#HQ}2+*pkp*w;wXu`;VA#0!5c3}IfB?+{DP1v!zb6<;Kq9B`zm->S77al}b0v%Vg zp#C$d)O2tj$IJQELA9}pncJ9B=cPcXT)7XPELVf(E2UT*A1Y5Wo?^TlGpbAbcrsxbV;7TSda(Kb6yE((q=oUDjE28X@w`i>GLA}0N=sj^0`|?zX zSJHa7r#XtWj8`$i;yXCxXA6RWcUaAC$X_WXldY(m1*H6iPQM61XC@T3`Nl&~OA=H+ zAWmzzl`D9t1f44mQQ7UAIKfv2$LDcWH~kVLN0&p+_Fb&^d_GqJ3bms#fd#=U3(7&w30f~OhMqw z!Aw9UvpzhBDG~#q>(owkckRHsfIfURQH8QQ{}$T59K-{it3m%$D6<}D;~MR6qrf~7 zr&J(ED5~!bJ-~=T-{zXV0av)V1(=i5iGQ46v@GmJRR8V+Fsh zg0WT~P-rHi-`@wAEYpfpnE%FPjVct({mdE{?W4Re0?L1q$F_AWrM%<0sEC)c+A9HY z=$IKa9c~8HdxlIi}}`SEcR&xSl3)eE-Q`7FPftMq#3lAMnIWK z5t`&5ft&RTHgDuDn!oj@i?e=cIA}rFYOb*jHE|SFyPRdbu>gw4ENT|FehI(fo%M}k@A%AEeoa&oqU0&BwqP_u&PdITq%}l_^LJg|_ z<3$$RC*>pO*28G;Aam91V;=c>Q1iDrIVOB!hFh=7ZC0tGk?$VNeq4%m7rtSmg(4a} zvz>WQ4dduyYib)<13dk?cki86h0bHTBwn-v!{e90VAmt2ubxOjk^~CwNTy-%2czC3 z(3&~Jbvl+IzxxdrzITL8n0i7%lL#u+RiI0hj*5cMT-$Cb%p~a%zt##mznGK$A8H^w zQo(Z4B`B_P!(ve+i!bqq@4a2f@AN#3^t2#TZ9@nSwxj+AEpEq^2ndfzpcz*sk|n=t z+)-bM5~D!K+!Mh}J!_Gd_j`lbKY*2}>eBSVTUf1KBkz26o(bLsQ{DM-_M~9mY z;B=ca^;@=c)t(h{RQ3hIfzP-_WCL||8JJ%j0`uKfXeJ?;5?Aw}-Aosfo@Jr+&{^!= zcpsC{8DH7l!d|mk?0wzNW^UNfStTi$`$U29a|4iE9^(8ehFFDtDl{5K1K&4>bDX0D zqwlQQsrqQjT5kk`WhYt4d=+?@E`?rxJu7+^@U&q`J<4gae1EOR<&4w&_xH z>;F$4ZDH}UIW$n}O_DRFFpUe)-LVM6el~$2o)R7R5K~2|GiHx3263!0HC>2Ar`KNG z`{^{&dhrr%gO9Mziy7$h;V3J1d&%OBRj8@(gmC!LGuEW;O^w$!g2dhq`%i{3{Udt7 I+a(wN53`iuU;qFB literal 0 HcmV?d00001 diff --git a/tests/golden/rope_q_input.bin b/tests/golden/rope_q_input.bin new file mode 100644 index 0000000000000000000000000000000000000000..91f7ac7e431cbf1ce76f980508dd715b4f512242 GIT binary patch literal 16432 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 literal 0 HcmV?d00001 diff --git a/tests/golden/rope_q_pos0.bin b/tests/golden/rope_q_pos0.bin new file mode 100644 index 0000000000000000000000000000000000000000..11140ab6224e01a8ec74544d405b015a6440a762 GIT binary patch literal 16431 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 literal 0 HcmV?d00001 diff --git a/tests/golden/rope_q_pos1.bin b/tests/golden/rope_q_pos1.bin new file mode 100644 index 0000000000000000000000000000000000000000..a63e44e4f983d314a0b5e25399e4f955382fa115 GIT binary patch literal 16431 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 RqnOthq5jnplx1zA{{zkGTRH#$ literal 0 HcmV?d00001 diff --git a/tests/golden/rope_q_pos100.bin b/tests/golden/rope_q_pos100.bin new file mode 100644 index 0000000000000000000000000000000000000000..71b43beadf358a3702eb88db3fb8846afdca5c9d GIT binary patch literal 16433 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 TA;T#8%@wG7y&R?Mw$uLsc1mt! literal 0 HcmV?d00001 diff --git a/tests/golden/rope_q_pos7.bin b/tests/golden/rope_q_pos7.bin new file mode 100644 index 0000000000000000000000000000000000000000..e696400613332ee05c826db6a7c5b12b465a5629 GIT binary patch literal 16431 zcmWMtdss|s7avK5N>WL}lq5+;B}sd&NlB3;NfITM&`l~y5|R*-IJspGp+k~fPSRd0 zw}cK!2qDCYF9{t&_~wuKXP*5$v-dmu{k^~2^7fiM-P3iF*BEJBOl(-tvY^RE` zwl-2b*?4>D^02T_L6$UrlI^&0<0p>qRo$r1rn27TYjY2NbxuV4Wj1Vhsf0h0IADAp{Jyv=qB z-giPU`}=x9*BCH*fi22=+HrQxb}^-|87uVeihh4PaOJ%9LN&gki2vLn?wJe=KUrby z))f?P)dQ1EoWWvcJ~Y022oi@wPLVm4BwcrzDjzFA^7}Q&^Hd3Ltj7GOgV9r}iNW=9 z2%VFF+0#N$Z;%$XCTmC9qknt?b?vwflH9vs zVKE){w$9~(`G+7W-w)+`?LkepjCcQVMtOk+Wqo)7(sOMj@vsnsdnAk2R%W=_X8}*V z>wpS{P)T4M zNHPK{p8e>>7K?k3Pxge+=u#? zbjO-&a?YA&Rq=3#EBoyo!AT$0P)BLmS?Ei}ySrCbwrYV*5=vX!9b4@u zVr|wYG54=ZNOI{%Ej6~Ja?OJ~Yb`k)G!}lBLi>OdaQVz2Jeyk% zhCahdv;Teaoqis&-Wjqfq+;;y7vLW|pLUnWaLJwDLhFUqtT50fGmW31I69EKbe*`p zP6HJ)X9%)0Z^bvd3o!L}3cswEaNz6TAt63B}Y$UG> zSd0Nr4bjI-;1-iAYWY2iRMkz`?D}Jn4Bb{?_TMt>7?_HkcOQ`a^K!9%m>UMy9vAbv zfO2+P;MK;Tly-Wp7#pj62mgBjmfK=5`&bhs+XS)w2Lr6@sm)fyJvemG8ESR-1U~a( zS+>1Yc==kLBOXoQPXC@<_CF(TIrALcA9Uxz-rl_A<0!mbpo_uf7MykG12xnQ;Z~KK zu=|21w|A@o`|tfRc$=Nr9)D4ETrFjt!JEWw8Zyqdyi4_~JW=(VJ}-H`hRJChH*Rrf z_gnKw=Dv(9f_hU;ut=58e+%CO{8+_tIF{d9!N*K01RXa8)Gu3zEEH2wPr3$EcxT}tT<#gqZ#wMYLM`CAbSs*dY0L6= zDOmG(6+gH$Rd_w(GTj`h0b_p*=M2?RnD99r9{o5CUsv3q31${Fb>Dof-P4yP``yv_ zu@*<{KMsl~heV4$iBMBN2lxNj4;_E3;Y#&|5HZsq+rL~9yc4r2x7;3mt_)(ye=CH< z7dG53U&56gkDVmT#-U{NPdIdI7`GoO67xUE@w{lw-gdVIhnITPp+n?WH1KeD(&bz<#DisjjKg%Of~40AP_N=KHGJ#|*%y~WT-8i= zO)01It%$+@bQ4N++(~h`95fDVqozj)`zp#P)Jco?kC@4_{B`2@s9vmj=O4=T5g{p~ zg*qfH@O{P-mWKX-;0!;KhsL1aj-Gh++H~|TG{$uo15FNu~6+Me>b31^-9!BuPqgyxJ{|B!L=qbbtfbs@)d$?>P<;0~t~+rlIP? zE3_?rHrHS6flcxWV&QuqiVv-atkd5}mbg_U_bALP8I6zo=&_DUAAEdRm-CJcq48|2`u$dNq(?r3}M!+%Z@;S_nU`!Z!5_-xx0 zK4jvA!@`uwtAmWWHDem^bj+^fY_Yir=cS1H9_ zdrzENMXxPC2p+tFYdn|2v z_=am(ywweDCLJJCbyfWJ@|bWjzbF0|7LC3`x6(dWQ+D~^5)Q6OLW%Nv`xVZt=I?@e zuXD-x?rW$iP34aGvk+U9Myd-=!ThfAsC`z0caQdC*YA61^RfgiS(yoWOXDD~tsj4K z7|caQUrGAX82#rZk;b+(NG)^1JVkGcU3^URpLR)P1lsjN!pVzdxRiCX~swmRt4(ylp<$!8;R2{U7>~%e`@T&%= zh2>SaZ#)j(X72^Fs7P)v&jme|acsQoJSd7-kdKQKEN1)#1%E2N-2&eKyDKzg znsRm87z|#&ooX+5Kz-N^s18Zyln;q0nJI_Pl44S9{2NLRHVgjt6F_43pD-mWgwvE- zFvB{K9X*Di;?;O8FV$js)(iSR%LirMd?~&>hcv=ezMZL?pUf8HgY$ziW05Du?)Z!B$FAbq z2kXSL!Mc#Il7!jkhEdgm`K;I+2YN7^)9U9_ZRT^qZu$(?SZzeGOC5H!-qlB{V-MbibL~Wi!qju39`B%Qkb-W)yN`B zdfNijV2mLNQP^y89o)89aq}sG(~H!(q+gC`5tTyC;~r3i?p~4%vT*XvdqbZBC$q!N zCMZ5K65F%KQfcViM{T(#piwUOv~%L3E9&Wrxh?wEZ z)u53K8}dc3A^tEvIRcj(ZlERa&G_GRe{^)6hbJA6;IptdG^fdlA01jkb*F0S;Yfkc zSI*{X5wmFP=8d>4QVQj3m*D)Sp`4|aNRP{w^Kd~Acl0#CwW&VPI#Zwby)+cl`>C+k zGdFBIG#4y1WOSfoFMR*g$myfA2X5N%NK8Bzg*lq5h3kn2A>Tpy;AHKKQ)&itj`Rh(BZW2hE6S7S4h=N;1thiq*0wlYW?I``bmlH8+1u#|C%~;!nv_~ zE}0jppwcO1{DwyC}+*1LFoU#zEt8h94oJxvG0RcsIJrG^nJ&m z)p9V#b3SbwVUMBZ61I4tfULc`+$7mfxvy4p?FV)4n2IPd>{Icq_gGZe<_n2$dU1TD zC;IdV;l$rEcK7X%B`Nk4UoZ;;t~ztl-e}gVFCs}u^2O-Bvr(3NkwQPOK||No=sQ;F zH-RS_n{<%to&}uiwgMk)HpbY`vFzVCnL7)j&~Ql~kR(KNih2gTSuhI2tA=BX<#KL{ zIu7#n+eObcYbkWOJ2x%VgqxonvC-cErKN%3XEd09-2=`&6M-`4o^&! z3SVoz3w?GC$3yC4IQ;%+h`xK2oH&cR>J|#SZl()5GaF&@^0lNs?idW){f%P7?+B}d z$FpP1Y;kO8HeAX%K>RcajZ-bb;L{bS%BdcRf+nX8IR(YfCZXcZY{Aa|IDGnJD@eo+$8p{9XH=i+GAx|k zZ7o2dwVtxm?^50}LvY-cfK8vwh2W~+g7*l2$}d=sEsvi;<>CVsDW(&#$<~eG>l$9t zJ&Lau_dv%#lkm1(5Qh)eL)Rg~fhx=4ohPjvC< z)L!hpPa7&rjFtXGwNh};;imrHf@I!*aCvVMYC5l_rhTVi^I#uzr~X_hvB$yf&ggJM zf*Flw?0;oF>3m&)s$V{W{Gg#wa>izaZ!LT22m`%w;oi2Lt z{Y?Yqo?Z`4NlrrSyDZZ9I}uwF7h==pIMIJnAbzqRh)yRXv054WFz1hz=8q=1qs?fX5g$i(>`0_SBD9v z8H87q{M99-sXnBXjTNMsa+G7P5ZQj~$)y!>T>fGa6iz6kNB5jyK~NEm&Gf=M=Y06d zw=vu}>9xp1E`Q`xb`pY|*FLT%F&i)Zh=GFR~wSPOs{&O43tr26ZuY=s+8&o<+2w!IS zaK!W8%1;+{D6v6ma91d?IVD7_cH^?AYk)TNNBKg8;PtQQc)1*XoM&_FhSA&=n+Ha( zT-ZAVFjVmsYWGc}szJ6v(W5_w{orH(P#N|me=28(kc*tQC!CNso_{69%P%T_3TtmJ?~PuJ2km9KDm{A%2!7|)B{ zXY;qHXf&A?PIX_aDgBBrPr7W5A>C()nZHa>bK)D&6gLQUk`2O`J(I9JRrxa?JWRQH z?RP3rpT&QdzNExew$L=Ng?yfCLg`%>4!N)u?tU@gcSpRqJo${ExMP6DiO#HDqK$3N z1G(i}2izRqonxz3M?z8V9RJ* z>=oAs&GizvaN#=WwJVWTz#NOl944I=rr>JRNQ$y4LR^sxrYn{qfg48bpUtwCxghy+ zhtgbDgXH>7XqukNzHK|HWSs|>nM89$Ry;NN+$1yGVo=o1Ae}l>?8;BV&h;U*&Dj~( z#V$hWyaM6Dzya((uaW$D7FJANidWr#lXR{+X6dxh`B70ATdE2rVNIg{pt;J7wm0iV z+OzM+eblA94(h_jU@uEuHv71r;NVOwSh1cIJF;QPtuTJP)&OPwm#|zXNvu9#Ljf{f zO!CgAz;9;QJmor^U#rcsJ=>{trZW2&YERF4da~LHCCA*b3Y@mLLg!2mFxa|~-5(lr zY`_Y1j8vYj>ig(H>nM!4J)Y#M@#2!mP>c`Dg@)(3R8cjGV{Lsnf9C}Dry#nmqR*l0 z%~(U%oEqBRlA-=#s_VH>>9elF!qo|w<6+0~C&ux!h(*f$^8wh8(ZZ5yD~hcco-0T*bIMf@n zJr0s$;0mx>vj)@WZ2~#pa%!J%gp%`9C^&o`Xl`hstYE;V(^i~!_nXo;oJ8#hD!^=I zF(pnlWG^2dmi>Gv#-{ay9A__dpWcn-5v5T0Ta_;l)8xz@HkhHe0uA;LMklc!W^X)5 zb}M|btnfKVUOj}+t8>}>{V;sFXg*(kQ47s~B;fZlAGR?12&j?9!9VfH9(^1p3^9aqwzab_h+GL~X$ ze-p0l^$(TXFX#GeBhkKTM1{$_1(^S~8yw8g6uRG5XOG3VVff9B)csi`_P**yLBY%E zOYeUnE_OQK)jLav{vL`Cb8BGBns*hiHu|GYhchKix97mOalG`|39{5=QTy-pghh`9 zm@)~+Hv-=r;KF(b0yyd~Z+Pl68zSai0P(gpH-8?3gNF92@L873Vq!2-%S?8-kxd&G zr1P`=J+OCK4S7gTK;Noq!l)=^cH?pkimy-S^`+-%x9;8w*~x|c{!AtWOq+r3bHe%e zkP%#FycIN_Z-qm>^x=Fz7alP40oj-Q3CBlA!G~fybT2&(c5VwXcBL^^u5YU-8TD1@ zoOp`ncN@&|sYjf$-BhsMzXZN`yDPI&xzJF)hN~2@oP9?js@}KLt6MLP|M)@#N7%a}hh{M~-;dDAQ8S1eizEb9a-$`?#F{^Iu&LvbS zSVlOZ!|@?#IeID{O;mEl6RHrb{Rkok^`f-ZlPiLISPPPfPmY!5)fI)|A&}E(gI1c; zIjzjz$vC?g?!S4Abo%U}%c0ZRNHzfrPv=p?kq1wWLTzn#%GGRw!mfvuo6tb2mi1({=`{t8k3sEYRvfE# zQOvxq>?fS}#`X(+pb>9DW*-A=Z}>!=3zAtct^h*U2J>JOYqqJF%)U#s_-fw(mR+^N zoBew6{x`Z<@cKTq`>Dd!eH$UII9cdY=8o>KZjI+%B1|rjF-hzcp}Td9E^leL@}LG<5i(!x#G5vhyr6 z?zCTqJy!N5sNDpyW9Q+cUkQA|r60`lv&S(*M`76Kb+G7+23~3YOl>Z0RG0A%_WRDp zF_(|in*5Qx>ZY6@4Oxn3|4HDEO-^WK(1#<_T1ox=O)3e-VW+BZ(-niF}GpCj(N>%*h3C(`Y&F6az}boJhNe%R&0M>a3R z!jCVhW3dO{i+M(+l2vq$5)o>>`Pm;MxZkbmI91<>{Z|F?zqXgfro4LLjY7#&7KX57 zLZcwDY?)A)Jb*Q3e*($6RPc`POIe>Bxykktsg%TH{gFDz>tl=NqaC@mwHHd>ud7f@ zj}{%PM`NjS58WJ+!X5hOsM>Z9?Y`EJrDx^~!2{k1jz_F{@ArwU7hgn*>I5s+jU1gsXTI2Tw{B^Tv*aK$~2ea%TX(iyg>Crn|vA=nNDlF9BIb zy3qVHmESZ+vfj?0V7v=4rLKw!cMK%yd!VZYMO1vwid|1Rv%*Ofyx*sT-rEj1+t5ZS zAVO3&9~;N(!0YdDIA4g)#I(hOs9 zGMm-@4|02brY37=EY%#3G|2_cQWs!l%?*kjJcjq%EW#vn6(~HQ!j;MED?GKWu=b#% z@|>80ifJ!I&44>l(B#Yc-hP7$~gE5w2#2d^a*5n+L9Yl!nI|m zgiKRCNLOn3wA~Y&rrckNhK^gI{ZJC8DZB+g)hQ^wRt}0=H^ktpswDsZ4P^72(Q3^L zrOsPUN#+4u-oGc-H04q7`!is_RhuQ=<3Kk$lEE(+The{dyKOUd&DY1Y^;?DPs(oVY z9RqMZUPE8j_Cm#(W#A-vO=pe&f>^_9$UOCf6un(RrhQSYbu}S*b0BFf_n|V2FxaLY zgVk3SVC<$}80JWIw!Qv1a}h0OK;QSE-E z-?sgS&^#&wiesj;f7E5rS*!;}NBzbkrjvQCc%@J zp4J5ayFC*fKAFOxP0RV@(s7_x`W+h5hLG)< z?bP;U6}Ck#6#mfhpq-ABS$oqKDs|1HUqAPu){J_JZOcNN6OE3)G*cnk^EkDBIu8M| zczPe-hbw-(5dKXspm0y3EkhOyTMLbOS?x)gytmbPt5ke*N&^zoW@+tOW{g z{n+{OWAWNa4~()1g6x9F^v@y_Y`E$~AI%o>;gZ4pR4~Wb+H?r+9VsRkM{xCu@l-ST z5=mlK389Y`Vy4bO=rti0^$NF8*2=Tsx#bcm1PGqsem~{2~v+TGo-rVQNI^S~1y^kig_e>`LDF;FG z)gfwpN6>P}94jxqqP%0X`0;lyd}ANPxvxHeVYAW~ePskkjr7nWQkz0PMsZxvWV~%S zoHbnP;fqf!OK)nkYusZ<`Eeaeokw$-UOwrSexTr^uc*zkKkn_LgL=NpS*CpoYODJ| zX#F&d4SWj8&jw*gN;0=>N~3Mr!JKw`Hp(aMswi>p&2B67xun+;N)LKSvbX<=ku(-v zh0jo#vtICAdpK9YO(Z7BG@kbBjQ=WL@c(P6JEN*7y*vJLY|GC7!<%%+MpMq$|Q zI|{;AMRG^?Thyfy$5&f3pj6p^Y;6@d+0B#F?CPN?!iZB|H9}pb9-GTYaf;bPs{G%d zR5Pxb_O=9J`U?W{%fnHc-yJ(n!didO;s6QyhdQ zutUA329O&0Q<`lU)Oao9Y|jZ`KPQ=kKW0GMOi6`RktuiX)gx_7Q#||h7HQP{16h&= zP`R@T?PSj((jyVKwU}X9_h==Cc2N7hY_J&e4>iAD&Q?3J$ouMK@@>2UNr#ml_-9j& zxG;@sw=Tz5FQ?$k`M#KoSz_d)o^*6q4(HFB1B`*Az7yoc5U4bA;ZELgNmW{ zr5@(zjlsj84AEw{HY?8G7I*d;$Xd^@DZTbq`fJC0{yx!#Qb+W{F<~)0TxSt(NpFPK zxgVjrvJak0Tfo9wAMQ-KK|0^nIVHuG)Zsea$WP-N_73cskzH}}LlhnkQpc0~F2MSi z4K%tiLfL=SLXY_F7-D3HJ(c~asUr^2#o61zZm$i-y-8*16)m_Wq~OI&O|~wphZcDb5c%gCE*-Ch1zVIF;S%zpD&*+KrR?zM zVah&mRLItK1@F-9LVD|CsvI08$bOxLvlDdDNlT0U>u-Z`o0_t(1A{+*1%pA$ z5H`%_&K(~hck~r#(*H?KT{8CEk`D^?_tgINhuE}7n~uIhrH65j>N7i_sdBxLl4-!( z2AiSAHBTyAt;(I#2J-&V^;DT3jvWw%>%IV2m*rDq%z0}4)dCg8?&yT8F>mxiknC&* zbKeylTp1+PIy->kUw5d^T!Mw#pJ{MJB+5VC7OMNZQ0*BjwtD&-g8xz%nohe2>y*#$ zYkGt%uJ^$Dkl!E|CkWm#7W}xWC#TkXVG&${wsR9vGX6f5ezxW6>YwoLw-dhe>d6uJ z4aw`(OqN`kElT?gLCLMhLPM=4WnbDY6d8P>!mL2Ht4UX8g63Fj)&{!DY}3``K6R}; zLH1gj*zmCp+)G`-UEPDKzbSeB)eegDS%D2c%HB}2Hx@Z7d+1YrFl%@PB)YA}uDC3) zSbGv~@0p4U?+ql|)E8oxjHlb)bI`KeOmzRT6eJPz#E?bZ(Xg_W!XNg>+m2zJttM0A zY8=aNdUD8s7_7NeKt%_hP}K?(){ZvhhyynvZGab5N_<5Be>$mSVH?H0wB-hq@vJv6 z2^*R{u<=r<_-vCSW?Wl=Z#NEP>qF0APsKLSis}?g?f35IXA>k>;eNZw(+2?S!LH`rGq1G#aBYJD_ zRaRr&{w8R6%^YGsPk{3t%8W^U5^a92uJpVLseIE?&g?n>$?t7AdYu%Z=LBxLxm;*g zkz@Jq6>PJ25u06lPVJ`_lIPc43jMeUbKxAN-Ag8EQ6Hgcgaga}`>=IF>7sSnb zSDE4^9oi%Jb7`%3e#Z+1fza^Q=%>;fF7sRolfL=PuU5)9EE9j?h5H6i6r-)I)w-HWyQZW;Gf<=4$?RdT=*DD zN*y`EH;J!~3B}N@HeBuJhLY0mV(?{AaIgr#q|?8^c+zWVymJtWKQ6;+m2Ci#3)r<= z1~pjQVzbx?4)^yFeVvL8%QuqGc{7wg*+Jgd1F&T~K+@M85b;8p>(`too!=*DZ}>`9 z=o@l|;b@i^?SRgVl~_@=3>#-fVxIEeB|ley+_WDznC}NYUj7QdLg(W_P`oAtrOl0E(SbPSEXYMFS^t+9JzIxsjYBAB zXfpeM{g>nuq*STmLY2=Y%8uq`$j+-K*ZeMM@e0DW`%Q+qH@-LjDKw@+VZ6BXuF~#aB)b#jBy~LsD zU_OX#Y|VLXunOMZ>`tSH21DPwW_&X~if8BUr!7fsprN)Ff7d3!bnUfVH%fxO;lU78 zuI!y#%GhzpMYy^voW0k2q1H5Ihh?lDsH^wD;^wE|+NO?iH*3hRU>b&0bz?{8w>14| zA-ufR6OX4311lJe-j17T-~u_W@f(0Ub*%V|T-mc+t?Y#R$)Ml7>2$g8Ey!+tO_v9+ z!Tw6FxjCmNgv{#A5C4qdA77ThpD|BiYjP^v#h!qf4^!ckRWqsBU!$~xe^q*DwaeSYrlZx+x(0d5qm^9N-HY0r&xyKg=CF>* z5vb(ZP_1f%*6(dO>7|6Wy?12qk=5{QVH|2)+Crb!FTi!bhp_MFerQm;21AX_I3nK& zBF@)CwL@PF^wGk~BQ=7BsVN7iCPTyUUy$YhCwy7whx>2;qRdkVp|I~+Fd90F(`1Pp zym}Qy&K$*?6DBG%zkPzr%0$jv6~bldU37DZ0k-K$FygI9u_Z=i^>7hKtUn3aYwJLD z<4>r)s80>!j8M_M0m7pfbIRds6#8uyezJ7tf~3#T{MQT8x#P~kQ+x!o!ZYOg>LOVr zOknxMe6cy`3uG#M*?-DTh}*5tXWz`^%IS8fogzn7WiDy=_h(2-I1RemOEIl-gj3!D zz-ZSAs5&wXFKbR^)7{fJB0zmQBe|ERE0$Dl7gf}|aj5@B@E_Kltpk>F#frC}JvS5+pG;wk zs@Z(iyaoIZMX_x0en=TE;p!tFNk!=!ySXgDc$X~{q2C=U_nxe<(5nWyPq0|~qf0D_ zyDGX?HIVnYmF#}K0-~>LaLSOWXl$BGnLQS8yjTkjGyVpRte@1@SD*ctt*117H`16V zqpqgS;5BX`YtAv@ynSCNUHd=sdO8_3wv0wu*)Ks33t403HhL#>$({V`hOvn zQT}}P+$Y#wFqq4hy`uJW?qGPp9yGk3f%{rjRvR~;OX81-W%3n#$X)57EdEG2>*Kk# zxfnVzE48FL;LT7SjJ#_{2U8+&oSTNSGyEqxeU0UA%Z|Y0y-w&PSqla3-r#g5 zm3uG02>EGdRM|Y6$7t_`^CjjSpQR~`?c7fNU*~|;?4{^lb`LgaF?k+dq~zs8g)rqz zQJ7a4x?PXJp_<*G|DgbUseMXR7_XRcx@;21z$x(EBXjCX=?gXgBal}O@T6+WfP`K?n&#G~oJ52uIXqS~#(O4Vd)(0AZ!?Y}{V4VRtP%_Fbxu8c$Av>8(U=xaLP0CsjGrF^}^1T4U4XlkiwNktN|#JxX*FfMl`+U(Rqo%xLvxm*EFsq3i3-WjCL&Mb)*CP)IboKm93arV_4LiWPT zkmn@js^@*VX;vEKY!1NNLO=8uTa~i}5-OWEAKna%;D+jz96atjdCSfSoxA)Y_)R%w z<$1Hj+~s00EGlNSmtYl6-K3$sq|aTrkL`SAb(cmEMQZ8Wk%Y=gaSUM zpfo`pRQ3*k&jN4zD`0W(9wjO>blGqPH1I}9 zH*AB-`eQaO(N)8E0TIWnxs z%LC*5X{>r}CIeGA0qjZCtc4}~O;dsI4Cd5#_124754Ff!a2<2Tho*;ZFRI(;HbLr#c! z&TC+ZQ69xl+lk?WqMQ<~;;<$)g3cU?$Gh{qKuMJOinLXT?qSSH+5R|m+;H|ZegaMn zsr*M^A-Eol;o|{QF-2n_o|>>h2rSI_bL_ zqKUT~`)nPL!CsAI*0%^|>3)SBu5aPWhdf#ky$rv)kejhQ&9N+U+DDhpge~=RCZ-5RR2AP9N(+rgX`8@uvpoFU7>@u ztCii+#O74YC z20wJy-q-T>u$s)WLkF;p{e3x!-mv@*I5+1lsCnxHKdootLcOWul}nGsyJR-N*! zMUocof(Xm;)V6gtx_2{#ruj;rai}Xdy5vB?d}W6sE)#rzs44x%?~v!}&eoq3xYSTN zTN&e`?BSI|?b)Y7(hO?~eKZ_Pjw^G-D-B{=^)Hd0q+oDD6k1q6hsKS*9GpLzB*T|h z=-h23_YrxN_rQsQgG)u#M;a^}16W;qi*nP;pxt2%);R13&B-??1Nvg*P*v7gwv6<^ zjjj6sOIerauIr={b`q|Fv)>4n?oRRSvQ<-V!|31yHiS&@_Ao&knSF(j=BAV+};kwPjjO& z*6ltOo$e$}F_Vnv#_O zGSX6LNS{gdJxZX+d^8#j@!@Ub7PDV-0*4-Jq)#)Jvghg!h`incXVr@!{Zki7>~2$8 z)JD)rRQ7R=yC=C literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_conv_out.bin b/tests/golden/ssm_conv_out.bin new file mode 100644 index 0000000000000000000000000000000000000000..ab5d057f7c514984b35470e8b2bbc1c910c5324f GIT binary patch literal 32816 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 U!f(y7^mBFq?(DZk)XQSwf81kYk^lez literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_conv_qkv.bin b/tests/golden/ssm_conv_qkv.bin new file mode 100644 index 0000000000000000000000000000000000000000..722337da7f4307b45056037f65dc7b5e145762c4 GIT binary patch literal 32816 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

OAAotp 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 literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_conv_state_out.bin b/tests/golden/ssm_conv_state_out.bin new file mode 100644 index 0000000000000000000000000000000000000000..db7ab39b6171ac1d3e779a231d0ede5686ec9ae8 GIT binary patch literal 98359 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Ⱦ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 literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_conv_weight.bin b/tests/golden/ssm_conv_weight.bin new file mode 100644 index 0000000000000000000000000000000000000000..21b2e7304d39f092f8bb5c481319430943d20135 GIT binary patch literal 131124 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 G5J1ob;dL_xs}0fLq1^jjIUTq8o9(P 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 ylvRxrNV#mswD9!mCj(*d|#8$NEDMba`|D literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_gated_weight.bin b/tests/golden/ssm_gated_weight.bin new file mode 100644 index 0000000000000000000000000000000000000000..c8c770d7f6d63ef3b289af88d9af9ba0156b4cc9 GIT binary patch literal 563 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 OPte647dlq8!~X$qpF#Kl literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_gated_z.bin b/tests/golden/ssm_gated_z.bin new file mode 100644 index 0000000000000000000000000000000000000000..93cafc26ab23c278d9bb508c1a2669ece5def97c GIT binary patch literal 16431 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�( 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冷>'R¶>°/-?zèÇ>U‚»>Hà >ÿhÓ>© +?`0¾>֨û>½î1?¬F*? M2?åH?üTý>0þ> ï!?Þt?ý˼>с5?JC?[e?K8? ?í>•n—>ˆ³.?«e?»š$?Pç9? \ No newline at end of file diff --git a/tests/golden/ssm_recur_g.bin b/tests/golden/ssm_recur_g.bin new file mode 100644 index 0000000000000000000000000000000000000000..2e9364a4ebb5e105a2ecc6cbf77a0d8f838e3a7c GIT binary patch literal 173 zcmV;e08;-@PDf8nL`6<9AaiqVUvgz*b#h;4AZ9Z%ATu%`b7f^@JurAVMKd=yIYK!K zaA&r?+}Q=b>!;Pd2q)dW8T8-1V|WU_&k%*Z32EQGG-#&1k3zk@r3ul!*Br9FKQsBg zVu%F3rB}zj_Zoq{wK%=K0!wziLePV~p1Z5PEiQSz2W;ZK{c8liZ4jcp==kEj>yvxE bks{B%)iD6R02l(kw^sALgwKq<^UmD8(2iA5 literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_recur_k.bin b/tests/golden/ssm_recur_k.bin new file mode 100644 index 0000000000000000000000000000000000000000..309bfcf88003df15dfe554726f6565061551db4d GIT binary patch literal 16431 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

fuJKY_HSurxV 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 Y-{5~-kMZ)c0dH>`ym~tXO8FDvfB1r|OaK4? literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_recur_q.bin b/tests/golden/ssm_recur_q.bin new file mode 100644 index 0000000000000000000000000000000000000000..ca6dacab883dae5a1477599127f378a444ad4eb9 GIT binary patch literal 16431 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

LguW?hr-?>Y#n0-y1UXyV09*l`+UHx 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

i*n5_zxDxb_H^8L z?Id)~n8l*c|H+NwVt9RYDwmjKV9OF;)-5H~4gdOmZjy~70#@fyp?3t40D zTj2JK1y9Ugk72uKV@L1VV04!5Jn~A^^d|;Q1hN*reW1{JFt0H3B$gNOfm3U+%drw| zt_;AIY(sXy>}RZsi{de3lc2-U3;h<(0!xRB(CHeA>gTRdWH*iaG2|$j>cu_u%E0pa zaja>IL!2*zs;ZNGwofERXV1U}i3xSMe<76$LGU&mDMzTnKaL2SPYGd9)qFrm*s+)Pb6GhYk7 zGkc@&w-WF=@f{2fuEx|^wAXuoTI{|=4(3A(u=$t^NcIOXPtT38q@fXAf#xpXP$n=C zsoU~vSjVPqP%pmC?*178iuzvM=UE0M{;?Dmh3Da`%1E>!_D4k9LJWwt0>$s-b89!n zhO7Hvpp@p3A;H*r_b4O}pzQXxQz*O9qRD8rrCcO&**bNaC4Of>u+vYY3Qgncg>QMv zx?a$B&J`uY3&g%%KVaE+n`hs61@hlbR z;LY1e@VBRaE+J{@6Ue)A=U-lACxadP$pb$t9VGtux$nXscp!O3@)H+9TYF#depmrQ z_uJxKM#n+@X&k<^NI@)T;NP$q70WW9ZC)0NLN%y$oC-RVN_1Z~9EhWW%DYx*a&riJ z=OkcDKp-!0qVw|CS5xuUA&A}(L%Np@y@z8f#yHlZ{p)sSb8`k1#Wb>}4G&qdX6E^Ud>MCMv-VIU?A|>KZTxSc%5F7CD5G38vy(xu3{dZ< z@1g%<=$!Eb`i|3Qi|VdJS#w{E7S>_WxVQYB^HDtT&;i>Tk1=QcY*2YLiMzUq6$OF# zY#a6cT^_+qWG8TJW+SS`Z)eJXKeK@6GePSV&O;0ip)@pJWHPS`JO|P}Z~HZ_OU-BA z;alLfc{M6;E@Vw=Ixhxis(0+G05c;Yq-}}=%Y91TZEcA957t5O*M2z4^C+7A)&~!s zNF`Rj8Um!p!2Db!7WGNu%`v3K-Sz~J+51Aw^oe45*#LCo_;HC6}J-#mZI9(2QcW2`iMLABYJ&RhCQcVYC8)6UE zV8N#XpPb2h~AYJ_JJ)H35L)H6Phn?lZ^;G#cw{HllL`!0Z^z8kzuN`^0QD^cZ|P8rZ{ z7Iyp#GvQmIJ0cP+Kh~qlXCu=TrK5Y92Ggx6YkvF}eCT`)Q|`&QrR*kriy%+)q8(sA z+MR{8zvM!*#p<{?E3uy(y8S}xed2i`A7?UyCcarqnsovLKs@r97} zel*CoebD$lC2zv+N6ccT9KFnUfp^1Uw7e~1&hCMzJ^Wf6Jc0ZLXNc3FqWyBnkC;PR zr~R}Q;49qAkH@RQs4uZy)Qdqk!Iw2t*X9z-G$_4S4>`$V-1G2_K$riT%!>w zfAe7BNS?FR|1fp(dCEJi=bmkgpd@@IrWoXKf&LkdOq8tA&7nJ}Qw6RQ=7GumsSvd0 zK9@-8cRl+UwhzgGZYC)dDQ?cCR`XeU2)(b+sqZynOfpGR?QO;fsH2k^)YH9YfHYLygV;|N$#t`~R zuVCZ^%1C{j!=gu!_bc8Qx-fuAR2H-oE$34D>}B`PvyLAk(e*XybD3SA`h0`xdK5>q&Ms3pvjn>eEu}zEsk@B zl$}11l2*q;{yPOa;=n)r>m;Py+C;tIGM4i0EpPc}J%&!W0C4&sj&V5x1OL7NZPi!M zeZx`|wOxX&e{SNEW?~UJ&SZ9t^{6}K$K%?SaA|G{#Ja6UZH5T-H8~KxJb+m6L(ya0 z8L+Q)hCTb1pfGTs#%-?)U$&p_mAOMOBySX?W`%+6xf}5CZUcn;)xh^Hs>hsvln_k$ zT_JfLg)KjdO&;uqptLaRZ#Uw?ZZpm7k{BH5s>B>?@~u@wvzGl`+~X2yk*jF`@%aLO zUUm-e_P>g6_Qj*!A7*I(WG}by^sTauX4;yf|Q!9vVM!p{5d1AXwtk7=Q3q4)IkoH*)(d~3*?c9p?kBzvV(G3iV z8Gx34mq2%?R-6?@d$jDE8r7y~^jb@dxi*D5^MVpP$GM?lOEh>c9gXwH9s^&SB9x2h zyeiy=sskg4|3jI{#LrB&X*Dw%RD`+x5&X9d!nXKR+~(zT-ug(4f}LJs)r1#(Phl*2 zPig1RPS#L%&p^|-A_KcE=R#e-1ZXi3bEAhePo}(3YiAZ}q`j7KJ2%QYbJcR!ig{IdtNEL$8=Kt9P8 zU&X>KYt|NBM;xX1tfYr#kJ|(=apzNxrC*Gad02W<2=wVjJKOK$0 zm!yZuJL;LuoZGBc?=S}9_q1!82+KxiVfW2(*uihG`qL4_8Y{;wXEPw|r#wzSAvC$V zVf(gN*tN3}znGoG#Ckp4_kg3(j2dRPVGQVwQ%2jvib*bIaigpOAah5JM6T6H^%J@G z%O6n`cAZOOTGXajQz6X#D&*^>f!g*ib5t7xzFkVX({{)_S%Jd3x9a26zo*-!6r0o> z=U#{)kAKZ1>w0+E^a!{kJ4T4^z8G}(B=gSR2Sx6mnPYSo7n*V|gi^IszeuBfe3_|! z^1_gpPgqI)Qd~1H8RRB6dCNl~%EJsmTautgxf!?(qg>61`NY3@!YW^qR{d@#<+_bo z^nu3=RK-vt{s9l1nh(M`f#55lEL=~RSTFoKh7IkHW+%J2GGP~ay(SRDkTeCSR(9a4 zDU=+cJ;l-(9(#T|`mGrb$>DY6&pHUYFDa-avryZZk)$CS5KH+hmpm@kw0Mcws-@xJ zR&2u}lZbn6kcW3NlhBznRQ+*HtnX_Krixvl|8_0Jwggajp{=N_eFhfUC-L^3BhhHK z9`;Nai`iosD*v_d&hEN)(Q&;saJELfU2p z&3(J@qakrQFV#>ksSFxz5d28rdb<@!vVKHh?wQ5Ew)8SK}vlvJ3C};9@SeiAkkG>0`y-5SU{r2)l(+Wu! zNam%;@qSzq9L&unw&E0&TL!Tf7ds5PU(UMYbC|NignQH8!>f-9?T=05sy^+SJ4;E6 zA2Ji9@85{cy6gDMk%eGswGcCu3Us|#0D&Wj>Esv7ET1-Ov|k=+WVeX-*+rR9T_ff1 zGepg92QlY^J$V-gf@R-mes}Q%@Ou-9xK9R!VowA4|D{bVEIo}o*bc|& zb){U_CxSOmo&>urYjHl;fa;fC_^=AGPyb}>EE|Xo%Vc2uFcaPMh%M6Bjb)wvjb&`# zjYekaIOfY0;<(b@@z1kd%?9J6Ik}*`KpEy-%Jm#O%cI}lgsuc<-V@p%LjE?x$|H%e zsD(VfUiw`4mp8~(XXBK`1*l&lr%dxroY}t^MfZsX(R3B0f;J|IIW3Z1P3E~j)`Q8t zL+GuygnTNs=sRUPSB~<9$g#9%bqxW@+q=w??yFC>U&9S=FG8)4IcW2rvjEE<@zDl) zzmQv4-I30#7JXrZvvSc@ZwSZ=Pl=5Nmk_gLJJSs$zw+jz^bXE|X0ufgtC~%0>`-p! z>;r-ov0M=r#Dl&r!W64GY>)GX#=}u)o<}V9F#2<^9LM?UEb;<$@Q%Y#tjBvQ$n=+B z%PupfcxuKn{@z7hMg2j!P!3&jy%4$dFwhk(yvdU&Nyn5a$GPnKQZyxPyJPNeENk(LHY3VF;*}pu^4^FuNah(0zA{ zmkhcIs*mfr@Ax}RSLOmz#duL>Xe?aXP2GqKBALx~>PewJuRCdKrtPGsmt8=SPZxI% z9E2Z2sbesEEsDon!jx(&&`*~^YJxN7-?oFU<$lb@@E~N}yvd7xt78W$%1~Ng&6iyz z_BiwsgTpFf1fIYtt);Y^_7eMVILh6JR=`Ww9MoknFp-%PZz-AkI;O#ten+Tlg!W(` zmxz6H_cBS{AWYpchVG{o+;`t5o>F?0TmIc2a$JLO-_#iTox6i7#SmNGtpzl${(rWv zBgC9pU8?9aS@azw?H9&uQod=Tat$2)gD#TDBBjr&4EJ0(B{yMuNw#SO~az3KQoQVbA21 zXuCHJb&`$D^41nK9+ZuDGl^%{zC;sp*ppYCe#E^$If0FxHN5gkMD5!mv2YpnF9!C( zRsOZ`SalSP@AgLf1slO;40Vb97R**{I|W9>&);FEM(V`I4`Y+DsfqaSyXI)JdNx4Y z?G%s&j>b)N-g$NoL0xE@xPyAVv)(SDF4Zeyvo6vtqSj#610VG1NI{E2OL$hi%x;>i)UG-8(b*Y7tsWzCiUJttdz0BlYSC}95 z273=m=Z=HNqkrOD5Nx)A=!pZNo){Iby%Dufh`rPPJ#>YhVa1>S6qj9i{nmP>z3a=hyO)UDto)!#&lqF}J~5%=VfAa$ z&srMWxNlS|Z@WVrrPz8@iZ=6**h6g8%}A8a-3_z**P-j%6`-H_Js#{=03I)j;duzh zUAJmM*S3a}aTf(`#cDf8b9kqJ7`!K5AT9Kocu(js=s4NR+A}0@pxcx93@5}b8(Vpp zrz0r-`fIA!+H@Q+IRowP4uQLe7C^!H0<`I#21U9S=1hN2(MjsJzT6K2H!cI+f9sh= zCCvklYsufVoV;+CnE&xb;M!vks?rZ!CXD7z#E^1cO3z*wWvJI}fS@8oU8S7-p5HTT z$5PP#RK(OaG3Xx=h^EUCydM7m1=cqpqA(59ua=>Y!9ft}-m7&XZs;9T&%4u4L($oH z{BG%5a8CIVwO@bYLgg18^3IAEtT>NNKPNDS(`&W7L&9bIFK86Q+B9-g#?3@u_>Nl% zxNpua>Ybhn(v2-*8wK&YXQ`Rd{yVJsA;X;aTOg%$4!GS6VWRgM?tSe$^eLoFY(h0U zxQ~P&Pg^h>RD;s%>$%BbSJ1kCr*Z7K$}OB{fPc_7aQm{E7d;vTn_QznvHfF((Eka4 zHu4rm+(?BT0p}og=qxN++`>%IfTw;yeE$7)Xxh=iov5EXM|Te9s!q{tQveOt^HDu% z7T%H3dH0J#)AC8qJZIOU|Ae!k-l}G!q7u+#+(FPTU&x}LC-I2-NtktzW6<8IP&uUx zrrf+l@0fnjoOKG|l@Weia}^s5hJkbrEz*{Hz{9t(#PZmxPI*2ZL#A$oQI@gLHQ$98 z+JAGc8|^6O$B9D+6+`L++E;H+z^WuulqJ#cb(xpiQgu>nQ}{P?P9sjxgL(=EBRKljqg&i%4*{MJ#`zV#2W}#0OGGQxD)u zv74_uv__6chh`PpO^O4_fvs@I_#}v>YUoH%^HCvnm_q)=Ez(lDFHge9y$W&5zzp;{ zc?{es(-R_~&oQNrvNv?^e|Z9h6_drX39i(oE zDYyt?&|~0bw4Hw(g@5nRIJ=I5FK16-ccLXO$xlGDa~WK!JEch_evJ3;0?hhV%OkAE zg8R@U@E)wfc8m2W(vaVO>!GQs6K|r|mKf@r{*U`gXY&KMd%-KuAHmzbmOtE3hQV@T z0?bhJ9?}rDjI6+tbUloiEJ4YQ^$^ll%tkdPLeTb;OlUQYbbUWa+}r>e^T-R;dJ3(g z&Y<<$+0?W6J2P|mhb!z)v95;gY=hTP(Cz_E&u4#-tRDp(mBX-dD|J=S{;Kt%2%cFG zchh4p^V{JDcfF=V@}f(S@ID#rPn{qysxylAkp}gyls{=Nhc)4+;Xu}Gw11Wag0*d$ z)Mo0zd>Vm0m;?L7l;;uJqU!rxur^7>*)F85S62|*lsKc0r)k3453>fH37VW(!MAy~s?;S+X8xO4I?qCE-f|IM?UrI$>ITq$l4^vZj^Z6>PC@5CC%}F5 zIS3Yw!{AMJ2wz8{sNWfo7M>Gjo~Qfrp3FCQO+uwddO&Aoz(uwfd-re2yj zJcFH912Og48E6UZ#bi-!O!B7{*Z-UguAEqN5vMTkGsTX7Im+ZFBl(t5W$2(tgyVlA zD4K1VD5-%5!B6JsH|fB}Ci!19*|nxHldXnmo=w0AO)+K)v04J)`HFCVfVm$KFz z%AdD7P>*dmD4gek_8F! zSfiWXh#lpvTvl2K@*LWi6~uz-wodFEa}@g}hGUWGba0(x30a~LXudj>vbV&+qtC75 ziW7A%p9K-VW^3mw8RUY~@5Z&x6lM}v0O3r!i_ z7li%qiaYe9dGu4_!CcXbpFN8}GbQyTJq^YKKL(-mynTR^CV_Nh5eqr&z}CH~K)h;8 zd6f#V|IH8;8O37XFJc%IS%xX~$8qMEe0;nr5ohDzOtvoH(xT)vd%K^e+I1%Ly35 zA{lPWhpyK;o+0@K^fnyET$Ty1zS%*F>0U6I6vb0!C!t$$BPKsrqMJuKbni(KCrK_B-<%K-`Zo z;;Gw^-#(!awvv9TXnZJoTu}>M=?j_l-=zC1+h~sHi@vuVSjckfsVTbvs%=liW;K5@ z!LNfbYxD>1W(>^AmNKU?fxK$eXkzeZo_xn_hCbB;{+!%z7kLhQa7r`vY?}3k(iI)*`TYO;p zB3SYC22?%MXAzY~U^(v^7_CnP!NEVc@qcL`3C|bX+DDyw|QOKS^B=M#y;;#A$2eDXA3zOq}V}I zj2ZZ}Ey1v7dMLRcqsh5Z3ZB>0)WJH9b?rA`&mt?qf4Bs6eiE2sTZ(!fmm%y3(X#ZS zQNOP~1ph6do%AJixJ_9Zd5Gq~s=UhAJ@5yXENdWimAXO5eL>%higaeb2y3XgCkQ*{V%EiYwEkf| zfL$Nr%ad@zYIZF9m3u$ zRpI@yjTo_I9YES>>I|L8BK{;jSokA)-&>8||5QQN?_XHUR@xbjnRG!Q*XTP z;EtfOC=xeK8w=`$Y7opH!OT*7VUR%-`!e(>j5VoS~xWKEvd7z!Y0j#MbQrUKe70jSc;#uQZNm>^8RQi!-ypXzs z&!d+7I38(zsQ0@c?)g0&T%TW~=OjvF^<5?UZC`*Xzagtl$b+g0yV)A3BX8J23?%=p zMW6$k2yZjXc3aRygu@NfTTm3|07d`vtQ7i9jg9q3f9X6FL_g;~cjQ>s(-&ImsxWwG z1}ZN;U>6G2P&CdEtetLw;8P5L^`Zo;wz_k{zEm#mNe1a<7maqX3%*+5gtgoX9m&R8 zUq?GnZ+)h6%M*9FAP_NwgCod%3jz&P&Kj*hKREV9!J+-X2XUJso3?t zl&Sw)2L6LgpgXIPr7k@QvRf;u;~*D5t&Bn8vPxEaI|yRz=^jCTkhbU&+_sGP?&Q;R zx?PTjrFr1{S1?zfDCV_04&aUIV>m@ug2#s@Q2*o{5On5E4I-w6HhT~R-L z4bo$eMK&`MnENsTC`h06{`Ux|3hJTNb2u)tP=he30M^lrt0&JVZtMzFR>V=??L8J& zOn$*xYsF!YQrY~}pG}J8C1?@7|7W!jr6J66H2jGs`_IZ9w}h zlqLKgjeZs?^f=wk1ed134p9}TdUrnuHgJ#SI8IlC-a>#j|Er+q2k0_{%vRj_=V7Z^sh|rvgiVK^&iN*Ht!$}@G7>o z6fwEaV6L?7&pTKCKwRGfhRPT;J4<~{MZVy3svLyzleo^Ln|r)6g(7t=2=Aq`u4|6q zl75Tw?I)pSHF1+8Z;CBW4#9(@P5JwfUt)6_%fB-Y1{Yih@0k}^v-!_pbhwxcVzMDP zPfebv0r={ofVvVFVL&GJr-l1L%=1RL6nq2|K4cQ-)(ltBOm0qo@cPx1<2u~{Wl7U8 ztG1IXdIH6fWt4SWz8^l$8j4G<#G>1VtE53^z>>6Z5NWo8%!|CFb6#+5NVUd)9eJa* z!4QLGAp0@|?(DdQWxE={y2z0H$_Mer1oAmF{=sg%If4aYa&YNg4Dv_QdDc$~-t^xX zuyEf%TRdh3i299!5B>*I!~SgnFTR8iukVPoEVPx70hR4 z68TvUp`rRHY4w}c+Sy8R(HBEVsY!tkF2qA^8$f+TCFr0mqCS>XW<26B$n2~bZrTZV zX)bqb8N~&rR-$E-OF_1o@;AAKO!XbTPm_bWuJ>_nG5I2PA76)l--dz2*AZPt$6-in zFBVo`$aVkyE7p#S5zlTeK>VpDS;8&G4aECz=OR})Zht%)M|~in&c@UyR$6g^xQ=LCVA4m^j25l@?c6z_lNss_P=N+;CIV^V?PYpJ(Qq z_%ixkwF2YsY5z4d3SSi&LyLGjml*i-&{fo1>=VxFYE`)I>`_RICJ)YMN6K;kjB_Vs zW7w*<)LVQN-bos8=Jf_}OR9icdXANU^$;`VXOQa8B8`X>V@}4J9{OTLoe{c^wSumm zEY$Wj)QEO(M%_JEruxtygAW;iMb;$jqp1UdUZE)H{bm08#U<((p*?^83pR86Rj{9z z50wd5s4FlIyn^b{?Qs`#R?D$S{4?4Zgrc;{mA4cV*IYOS1N(1=^qo=ICf^SBeFpQE zoTJ=g4QaWn&O(^MZ!GxHBb7p|oT0LB1 zD}#uoR*;%K1|4f>(LVT~#_drr6r4{Lx6WDw!uk@m=_Xs8EBil+&c!dr^!?*Qh{&n6 zF+_xjh*`=!_eEGDB0@w&7)oj-*@&s6lu?Qfla!JnrqW0?b6<>-PR1!@v#pjdS+*6i z5v||#`vbf*GtYfL*WvSde=dMwUmkaC+yX6Llsj-W0oML{6ztaR#%(oOpwyff#5ezC z!87*bsP|>yeSRqz$tXkm#cfuYQV#mgOAx>8Bm{&lgAw_M=sUXrZ5Bs(;>K*0xA(y1 zs!R;5`4vkJUIe$x9$46)m>)e-xO_+y^PVGxklF~g>1WD_`6G1 zGg)ttj~$DmyVM6=J`AllcZ2JhL^^A?h5VBVsPj8aJNr$9k-7GrOBSHD) zj6+~^B5ND}hSv@YgHL_RNqh2zT~9klS#ij$zf9tyCqr0b@@2{}y+&GV32*bM;h}}y z!O+8)t9}-t==F&Hx-O`7W41*YH*{REs;hd^bc`H4@DJj3aQ5A zqbtyO_*AUFr2;oYEpPSRj&9e8RqUM4r}eo(Sspi-i&;8xq;o)|ujVfbXuto^3*%?m zV8o0w7~(})bxPt79y^5l{)$17Jl(z^M*$UEYjN!u@+7n+v(SgXpvOoz{v_xadUflC z5nKAhm_4VkeHWt_%^_5fKJJC94F(K5fl;e(aQm;LVP8rvhWuX2EB8ylc4Y-=SR$cd zNE8@1UE!MT{dxQt4e9RAqx}O9ycfR+20zGvY|_7mgzW&Au_@ z|5mJ+x0U&ibh+_m@-St9VrvK`H>PaWb; zEAm;KcP;cEx*P|7iA9G^*^o7;3{5vr;q#`Ipi}cvmZ^KhLq?zDIlbb+?jr5fiOVAI zc1UM7NY09P{(z>x48U=z@hCYTD6~Chyy@CS@==GPB0GgC4Ov`ra2F^xj>pVQj;#YG zqbmqd5Pt;4@7J?8yBC7+EgD*nOh;wD4-=bAWS{=a!`6UMXqdK^OBUC1aagWkyzx2L z2hU;7h6rq)^o+F!FTfV_ohTbOk0s~Cqxjx5R!n}Jm%mq|`w%Bo?;%a5Yd6Hx=`ber z3VK^{@Ha9iU8sM5Nz=5Z<^y>bOuOkW4li?ZQ)0C{QWZ{j_p z$S<_en7_VKh@vSax_4`+U%V2IF7XYz66ZJ^<<3D$1J|!ndNfTSuhQ?YSbXj<$XpwY zlB;)hP74iOIg&JrTbkL_E;qn!>P?i1J_v!!=sCLT#Onq`gUCTIRg0>b^ZirkT(}!r zVw1Q_V?WTI*^1hv=Rb+ukqlCS@_*54-Msuv3YGfuXuA9-t9OG zaPS37e?Q$*pGYvTSPtPooX6*b z)fnYQ^>?VFbe^>yTAJ5Do5w3=SFjx1?<8Quv~Zp|gLb>WH|mW^(WUGMugQ@T+UJV)k z$I!>$4hr<$)V-0oW(vo2PaP* zBj33-SXnAy&Wu}BC!PQ|)qE5k3x{oYZ$N`$J#XMsn8)}fyfXb1dWzCu!_dp9EVSb_ z%30X-n=RPxA?BXLG^jI6V4bcL(QAbgHD3~$e1%-lSe?~9J)i?iWi_*Wz7D6RUBnRC zDV}b#6?3B_K=exxY1P(Xh>e^(R*c03ABs`Gg6>?(Kffox1ad>W;P}3XXNJ~t>pkvF zI-TZ-{}rO7IEX!6eH*4mra;7`AHikNNg?-bAilm|2o3LTp(XEkUX~gQbrU~xZK94T zk8GD(W{_v#k_{I{B=HzaAFP~Ri}9NWp+R;WH~43O$IHp+b|MfLfD$zEYE(*gvrv=1 z*qJ&AE!7${Ur3yVpZ8$!|29H{-i^2Pe8D8S-ML-A)lj8A1}d{`46O?RsWa)mo=1Rq z*FHAEI2`0%G_3pdd{lIE;V$hH`N5<-g!GBfbg!O0kr!k3w?1IM%LN@DjNyY$g`rXB zRXEwE1BHrujHoT70a^z2J3=Vy+7g?L4^!=PmIW4^!o3NcW`mbd{%R0&ir$8;U*f@o zYOnh2Vk{b1LVhyR=^fECKVboieN{U7KO$%fp`5*ilhA!YH8gonMwOE%!~JToXsbYv zrBgu^p2BP1ABTB!j#Fj|ukodYVYBd`@Z?=Gx=c;P-gB-KFMBV#q+P@1h9WdnG#e%69U3hcxi$+*OQ; z?+cpJooMvOdh%O*<+h@~nB!>~_IyJ3{-^7@I%gS}U2J9QW&OZ%UOMG})L@{IF&a)6 zN;Q(f=+gnG`%{ETb&|a z_MVLpk(`4Wz36^yixTv^4uawq2}6vB;y~|8Fxl`X2V)xOZ<~D< zH(dyWfX|CSF?%deyuKAGA8!R)+O;=pBZY|bOY!X*6=ZkYj}59T5R*O_E!$48##leh zm~a%$Y9y%nH4vQE^nFr~ij z+bJySb`jM`tgBSLpmK7Aj^tZh;b+a2)60dfuWx~8_a9Ml7hzP#H8B0x1r*U^KN{hrD4|1o`5myi;RGy;5JOfGQ9dWI^1U2q@^&1@y1! z9lgSpb)MaX+L1NXKg2q_}q*bNyec(Q~J(&%v>>s(lc@m=ODv-_gz`%f!Tv7WM`~I62 zOgm#)kumY{x_LuM;!X4_Eap*prP%OS7I{%Ls37l_(Q7|6kiIdy{|e$HXFyZXG7Jw- zrA%gXT-ufjrq7!KZykA>k6zGd=+yahJbbcDJGFt2m#d@wA*(E`)A#t zqo0E2cUN%jv$HsPfr|3Qqc9|WIapkv{MA>hF`<7n*uCzHgB$Xw)_kaQZr*`%VktCl z_{=>P48{CcsTe99f({wCNn;tHlisCgAa5gg`$Y!rYiO6&%@3W|tc8e)Q$eL`<(dE7 zfNAS!hJ5=^t;bVaP%TJ=+!`}{I{q-2yG=$>-5~Loe`>Z(V#c{!(2v?uWouSWh0-0#w(OZc6|S zJ~Ee#FTCm8QLz0_jm7tNgQ)!mYkZyw{=zxPSs#tf=dIY+R??kZyud%*qCKcKgpZh6 zi^?A?nfho05BNL+?2T-oV$CO`6MBVu4ribL(t za_-n?FtfR_8r!;#z_x3?D5h*nWv7+}mRq1^ZL`j$gEUoM$h;bDDM!(a%m&oOKB-(^uoO8Kq!* zu#veM=|JDA<*u))A+2u-s#pL@vb@nmyo+j#?bIhz-@mvU*ZSXK?J5087knH19Jk}6 zchL}BVSzqrwUEA(!9k@4l1IgY+r3p#JH7xq$7x8@LcF++d_u}Ei4!7))VI{vWf?;G7#&S7^jIg=s~=8)zaO}AcR3kbq=&PoJH@qcIdRSH|9Rh zMnl3xY_KP;(sqPm!%3<~_M_zb-`qIq7QAbuI{QfyL^zECZS6!+7k{dH7 z@cxBK)_jwyO_F%v?rhldV-`;S?>a`^X=GZPOOSty`1X13AhVdtv%iqHUOe#t>E=V&`Z5V}Z8i~mn&yr}cS8EAiMSWT(00g4)}SEG z=~OXuHXQ{o_uZoU*N5kZO~IzZPM$p~3z{@HdF9Agtid{xms|@+OOZc_?Z?AOMLuL6 zxyRO8(XM=~1-F<{28s>Gc%ZijcFoL#_>V!Lun*?euZ+Pp&xmF^t<3D94iblygL~#K z$|L3M&B%>d{B||YZ4E=SE~za334N{wd$>+WCH_(Z+;u(yw(*h7@0L4d1?O|S2GYSD zEroVxcieJ`G9tcSW|FQ&sC+y^5dXFsn(p6civ9M)x=2IC8&?*RTh02voCps0B0+h2 zJ+E9pAEZ6Ef$5nQU}FSH*5AOv@19R z<8o<^pKgSTnI%|x{|FYjoI;K9IpOXLgupQgTorzh9Sp05kh4-)6kUe(yJH|*>4s6% zkhPYr15r#EcM)ze`Ryc~=YKX3G`<>b7Ds`{-zi|f`6qbyMu{ioS3y4YS8%^E0IZ%Z zL6_n01tCtCYhWt3(op6>(KVVE*g=IC`K`651f^xW&@g!{D%2-H zwLTJ+J?5jbdaE{+IiPni>EIp}=&Yvh#a)Mz(Xo9j`VB~7`L3yeKFQ$nL#@#8Z6>oj zdkF28#bV3#B(_KshNgQ0K%V{^lc?IIiDSt_SaVscuRQ_f-?qXi`c5MABVpET%GB15 zf?8=L=o`Wz_k1cw<(}XHu2aB-bPF1*Fx~Zzw*+ed2kGf z1~>}VW#5pfmxCegEw_G` z!X?{m!S72F^)#8_{jnMxLJvdp5f|qE!(bext_J-q8TGBlFz5Mclum4AqV$9O^{aD$ zRJ(Vap9nU?x`4|K;`z4RVxn(FTy*KOkp6EU2%fwPiY<22d9}h?n{0@QF9Gt{Guba{ z5O-gW8jE!1@~6NI#V=)H>O3x7Xj^+Vjj zy!aw%n_lbU$V29~?-DoMc81)LerU640BHg$c)=|dT2EgE`p|vYd+#w285Rp)=`)58 zOak}mw42n&3lifDh@7HC&!e^AFqU!|my7_}zsFFrW*^$D&O-6f-Oyj%4F(!%P}zI8 zAah;F-O}Y4?|vM-wT>Y9xKN1fd5HX@=Xvw)Q)n;U1cpC1Ft>j**qiFbp#OCPnC=|I zI{H#x@#iCGUf&bjrDM_f7h>1FXx9b4qh7$ij5WJI5Sky+Z1Fc8ww#DYQI`$OaB~aS z|1%Q?U&{yahhZQk{f%f3>5Nuf;Keh}K)v|{fF2vsbHoj>nQ;vohfg4G+cdD>J`;_l zz0jxs4xDv03I;yUgG}*CIG;uHj=oyh@FoM}_isU`v9GwO@uT4Kg?hEH+bmEu1r2T1 zg4{g=Jr9&YQMYOkhfrV1rb)f*{O zw?E~@5n7P<&fzU=GQ9DoePv5O)_i3vm+fA~OlSLpT~|9?xt=^IjzJi^<|aBmG-7*` zZz7&ag2t#*u-dE;p3gfD2ItwRUHFNm*J?5F+I-eCA`a`P&>cKs7C7`Ek8{1iWl3gI zwUETk!!xlV#(^7{PN&|Mg)3c23-0$oaAb*W++6C{T8B};A!aob24nlSLFn9T0P0UI zfu^m?F^V$SRgtOKx>$^oZM96jb12qzQor9a55?K9`CA(eL_en74x2a}O!MM`?ZhL@ zBj3HIgP$LE3PNX1L=ABtij1yK>Y8KkMzUQXzFSE42tH6EDL@+U=kPdh_IF>C%S-&%a@)zPVboxR1 zt}uxCn|As;6ugr3GOpuGC>NaaDbMf27c3f;$8QLoZ+}L8E$NST-C=EUH!&pN8O?La zM=jmSOgEEnd}=a}Df9%j1p{4z9F%T;x_Vd%t?H5BUJ7XNnSt&8#9gkP0^*mSOvDL?YG8{^0m{|+ z?0Rt)jT4oGKHTkt=Csyd3M^cu3{S|0C z{u7tF3!uz@AxK{QtZh#Em2$7N7!v=Qx93`e{qtnpl6?dlgh7y1R|SD*!^jh^MH{mO z^c;E=D; z2Y_p*8)&wy1;-5wSmnVX=;)h?*|GV=OnJk$e7{b9&pg4XU^OVoc@j-u0wzqz=< z9~Cccm?ede+C?q_zbG9uy|xQo)(pV@KW4zVmQ<9O4xqX!O_!M48(J@o#C?yhVeS4~ zlvCh^J^|Bc-Zd4QUbl0}bu*qhDF^SKA*TP4Us3!|I?j1TTICmk7-ByjEf)4f@vGS= zHy?_c57)8g^#WA>Hh@`v`3Y4cd+>Wb)OfLuIBUnoK}X0gtjYERvGxOhwjRMSC|*~Y zG72A!IS*pzF7P?)6eNcu%I1I4Vg#|OY#W><%#icS+ijnENHn2{(};**?cM!`O_Zu-6`Vzkbb3i7ydo464U|t%zb@t zXj-4cJa6v8!1W2d*>e{YJ#W#5I*YMog98q|8U_j{YbJZT03A1zKP0ADm-x;cEVWm8 zvO^WL^_z??v+~fca6Z|;{jp|fEVlOQh2lpyK=nd^hEbQ9=JOMw;r2$}e?PI~{m5?? zmd#dAJPc31-@>S$@`%ZP97TV;(<+Z2WwCX|&{Rqpny*c;VHau6y=Fs0`31_pazJ(A zL=5!&$d)cpfcVP@P<5J+cH$&D=5OQ8K?c@VS;t-aEM+}=$wB2%Oe~rMJSXi6_}(}M zvd>F-&7awj@P_gkK9ZNCI2Rt!yx#3|A&acLP8v5V(f?eJ8;W#rS5FLux6v51JOR3f zQ(lXUDQM0Q!a8DqX%FTzsYxBL>`vK*bGD*{T&SNI|b5BEC{XkG11tuyTr>mTH5$qylp!G~4dqg~WXNOr=~5Gl&=+57ASu z;W)oUGbhp8fqdmyOy5l&(z2egt0NX| zS6RZ|^z+y|hUTTyO5ywVQyA!w!L=6ZUp0s~i6~q9Bjtxo$U%#_ zl;fa30YTv>(CAffl&3vocCnkmQzHS***ama^KgjqFa?!(F>ClQhZhy)pe8e&`@CKP z{TI$e&EbbU&F>n@{$3{7eze5k4u+k3sz^5y#`|s66JId}JA`Vk8jy;e$BeN&E(kOa zq{OQ|Ej&7&PfU%$kh1DH`US5?<6pB`vtxg3I6~QogI4pb$4O}U>1Q0AUk-H(A29Pq zH7FMZ2`%N7V0GpuL=7Pyim$*_Cz7}%?IM3OWi2kMPXNDFV~I`L4NPbM&b(7Aan@Aw z1pR!3{00?luwxX;Mvd0#4-$v^{vc>;=*z^L@3E&Nqrp%n;z3qdq4i}?P;NNOHRldM z(^}FpT{s6d&#Y0)Nt?da1v4+2U`Id^WsN)O`gPC_?%g*YukC|Tw||7Ea?*sod&pJJ z6%aHf8>;%HqHSyym;JY%{{K?myvG_}%87MjJ0F}EMG^~hBmZ=S7%@fm{Kdb~peD^o za@b_>d#hzGZxe+v-O|y0=MWgRP6L*-S8VFM%KOI+B)x4P+H-DZ_RAQ(N$>U zJ_ssJFT!%_Pn?ALaIbzmmfOt)Rh9=h9+seuvn{UP8IS%8W5EMXp=>y1d7mLo?!-T} zlh1{razLQY_`??#Ah(C0!)HtiyT}=ePO01f3kaSi@{MtpLx$?%JJTeq8B>Bsvr(}i5( z+bo6qT>)&*V`H8PDmMlTiR)9a?q~p((=5w%*#Y{_Oo@lda1`CgwyTbz@t4bJe2}vE zD2p><c8Ch*NY06HC)r@dV=heff_?o()IMoOe2pA;-WBK5)V zxo#rt?|2UF$GYR&iX3dzbOo2e#pJX9$i+1#7}{?J^^Vifw9j0sv&Lg(sWZ0!bpp4< zQU+Of4YcJRMeA{gu<}qfL_WJrezCFeNu3B~|0vLLs4>&nWifT(uh{JNoY^jJb zG$KgrnuzUMVjQT+o3i&U&xtOCG8Tr$?#Gz;%Q?X}xfttLQ4Mh^oaW_f;e~H+s5u;r z4ac5vzvmY)c`NCuPHjPH`ZmfQbu4X|(;cj(o=j=45KbPW=k%U;Y2v`aVDs`MC_=M& z8ygJjb`9-@zY6hrhtQ_d8Y@GtBGd{ToxZc{$vRoZC!JsDE1o-T7pi+$ z!+9y)eMXc?v?UfpE`{Pn<6_jl(V+Hl4tM>R_=*3tY3=>zLHuYF^jxJz+do%e)Aw+6 zdSnlNKfK|RE9=?no#Dii>10toYMG()F;93%J!j%}UFOt8+MTFyRwK>H@3=9qN3Bep zlgr!aIZiCw49$Pt5u&69<~BSZYn$lY6lXD$&%`vXz6LGJR8SxqhK4>5b$ zsSozV}g?z;k%)@9J( zyc4qWE5MPMMQ>)Gfab0Lu#hW%aBJhKpnXbt_V>tZb1DzC8yvCaU;?(8oM9GHQ!LoB z8!GQVWG?xOSYYlj=J@j&&=2d)1{Gg`f=z=l+A;-f z57VyysxE3)60^!KqTlX4b14Yp&W!SHt@S+MgC$B9bPC2#Q@JTDMVmq`7~jtT+sog% z@tC8`q{bE=;-^`(Qz?upOX%^VHhR;1kzw7nGf@a8fX8b$|YG&&&YX$Ak4_@O%e`LYHf2zUb@dDz`cIz8A8Ob{3bR%!zrg2*d+4ph60 zq2vFb>FYiy%5(tL8JdxAH!W@TS_+xot8k@hB|M#b2vtTWxt?ZtvfYo^+BsKolcp8} zAE>!omrKN6EI|8gOYEpW&cq(2LR-*gFuAfCvL}p%=#8X%=@^Bw{Oi2aCL9)wjfZl( zL*NqD%3jb+tNob~*f*2*`TTwieq{!-RlM}8q7ZySNZXdYQEC_v!JYCR^S#DOtU5#N z&0X%0(r^w8kLIv8-vQt?b{*)i90&J;8R%Frp6j-h5FgDK~sj+ zyny*k$%4E8okjoAm%;rC2gA$=p-5K=5;SM}oNlaP(FWGB-WR_g(Nos%HcXZ3Aop!?2Pu&n>Yrtxs>e=riBGCgdWb(VD2 zGojIZBU&EXhy8cwqf7I0o;m+AGoEPz*F9<199E$-k2(hB6Zb&W^0R0|f4kvP1aq|e zk(vH{fTi`)WAc@a=(XrE(^Xx7!hYef7O2iX<_dKcuXtwiC+>IRBUe0hAYL8k4zEr_ z&`3SRN3Vk5sSDBZ{VZ0~g?hZWK#WhYfR+haXz{Uta!1Z`ygwVwOnlMz(gpOt6c6fi zH$WWZ%bkAC2a)Sccwiv^nYvaO_-_UHl`EO!_Y*v*=qA><9zoo5nDRnoAWX^vFa2~B zJ1pbPzTe@uZ73ogS+1HCijw74=M zhjNmKfW3Shh*bql_ay-|fBeAvx6%yg>TgUwVlG$OkLR-CLv^C0VK}fuOSxynecY4| zfktuMF*ubwM$V)@Za+`lPId4?&L$kZ0rmqa!*_BP7z?qWIJg;|vcY`<%cN2r}_vErZdtnm3Z!_5_RL=q`rImd;M81;y(^%BjQPA>b8E8Yl@=j?t z(0p9L#iQ5q7bWUKuEIkugI|520m47VWaipry$h+=(e9aYZ39R)TZqIYQ2f49Xne z$L#po;H`k6gjipWCRZ`kRGQe z-8X9%qS(AvP+Z=G9T(HlI`jZ`+#U|`d5fW0^*2-PoyF`1*r7|_X+iXF9G?F#4C{xD zfc6tKce;I9CwqHYSX#^>aojzA&n_7|#(BV|oKz6qe4up+qwMHut8s8U@h86ijM`og zx#ijam|HiRmt-{wnf+t2aicFNeeY^>0ur(9I`PcTm@&^~6Oyuu?*PXa$C@ zHAnHldCcY1X&ys;e@*#PY*NwPO8ZAcz(j4L_#2lbcf*dW)l6~oB^xT1L*Rw0O!G|y zE^BVF%GuN}yh;)bhFz@TBxjnl1G)cpj`2z*IPM+ITn?K-L)J)m`ZC-z*Ppbwu0>ZRV$ z;&`9AJj~&ilw%Q?SPfCzFY}9}eKxC?Gw&EHEIv3KqkNK}+((HKKhynhb_L}NdoUOH zPk45^5Go)2!EMKk!C;LSh)(n-&TTr#dYD0CT7O~%26C%A#5k+-fW#%A*)-C-l~+v1 z_V;SkWMt_|!f(LK9;dM>{5szKLxrNGGG;LOO)HzSOJ^1pOL?(MP(vg)?`8oF*&yCY!g1JuABi&9U)J z89SBPY}*fWE4O3(_T7|C)(13K{$k>d3-L`o?Opv-QK|IO$-Hk7|5=Bhubcw0#Dm#U zPN8VQTdDQ>5|-twL+^K_qxyQ9VM7|m5Tn_eyr}9QXM?DpGxI1V&B#9)tGK-tGE@W;&U7_KfBXdg+g0$Diz^&mQ?ksmhQK5nuoF`D}R0hGOqev??6k4b4 z24~$~R0QuM&q4w;eJY_mhAes}e*)1<3!QvJUl0!5fW|-jgKV>%5I^%8+Qt3|gC{0~ z=GJ3w`uGKJ{$~re$7;dw0eMXDVVqDK3se7Bf|F}w(O8|2i9t{KzUhzv zs73SkZRoA}0V-o2aZPTuVE1V;s6G-mv+Ou3raM94!85!mIFC!s$KukyDOh;+6x=JC z11_t|be3gVknIqK_cqL>xwwjdUKB-Iq4}urcLj|Q1WuO+f#%R{lq@?31ydK{hU>(% zTW`y)T|FW2_Au_0b_vyrb4<49AT#zPeQvLHG=uWQ&T9%toY=zO^{TFPR|nFL!UeAsBZg->-)BHRYM$?Nv1(nHqBsDRA7veTzm996Zar? zn~xloUd1|5Ukj9yZn8Z#8y-!hIl~OP4@M1wuU!h!Vv8JX23Nq-ZMnpWH3GBp2Id^L z8PXp)KwNAJ@!NJ`+N28fdP6zH%ev_1{@NRDi#W~dO4!uTu_!nvquaOv7;8;B@7^Ey zg!&`+Vw(@@h;!uD7L89{MWJ8#H|{)fCe~b+;p*eW=D&6W4HMHG47zlk*>>VncB8%Z z@;c__+Qv*Rwt?gP?(lBW6%c|GLGt!AdLKVbx>qlt1WkmTU;V?Hh z06f>GK*L8zu2el1wzXx0Nr98CLU5AYwE3M*A7adPhn zZ2Ms;*f?y%X&;H-J1i2@GuNW=v}ka>;{`DD4A@QFhz))VnDdKeqz}-dGkNjlZ-`Mf zXdp2cz6;qy$DlTh@{(?qW8%k|(0=eHF-6Bhv1BKBUOt8$ev}LPDuP?)zF>`T1e~7b z;rYKZv1#B%Y->y>ZRag~E+e)|OFC@mQHt7YYU<0oVc@wCp4mPZjSBZ-0$xFPj~(DO zPQ^_Ai^R6*W8BQ=64zu@GnYYsvh00T;Bf6MsQn|k$q>rC^%w!I^G(rmzXP`InZrHG zPjLMY*3jOg2$O%>iZYMcOjFhYa2sYz?L({kccA6b&X*1yA#aR&#JnezjkALDu$ESc@wKcdkU1Nr_>D=Hh z)yXp2xsMa+D22mp(}r;PeC#H4p38*EDLLrZu4Ju?F2nPj8kC*#f2fn z&)LX5{+EOSGftzcoe_o%+)iG#e!8akT&CTU%s$Y4kKeM$OSl=uTp`G(CF!K8pV$i# zagT^?c|P^Yqc4|5;Z zJhmUS{gH}ARg}-w_bM1hPX+hcK`7htRO|TNm&-`An>IxO`X+BQUBj_Zc^F@kCiLs| z5)7=~O_?gCpw+Z-ze!U;Jc#a>Id=tHlMv{A{3uj;W`XQ*AUCse1&OJKYdf=<{lM*1 z>;BF%dvjDYA8WOmjV^Hx;HL4z$_1a9TW2yiTD}BC&+lj* zyG;S_I|}^JaugNoPcWw_3-FHK0QM^e;^%TT3Nw#G!;!5JUsMKrS0iaMszFq^hc`7+ zwnxl#l>c;0P`J~s*8dCBSBJ3--|Jx6{|ry=S_$oUyin1rHw5O#bLFq&*wV8(xanIZ z%6lEd7fV+{=XN6uHrUYA)_1JUidXfx>-v{*$NINVO&=XR;4 z`wTG7KEq_|ujnFv84TlwMnGC+E~?0H?Jz)(8kb%$VbMtlj+3I_k&WoFYcs2=C8p?q zk>GR92%&|(L!0S>y0@NtY>L3xj5Mg5tKmz>hhgW8MG!b`BA?@R3B5^QYBh{J+(j{z z)0W2rq^H5v*$?GG&jgub53?Lg@5<*l_#1oTn(X?jZH!7l*;0S@;^mLHeDOu_E*b(F zn=DkF?I4eEIqTe|fRGm^sM$D@$GK1*;fiHYJN!B-hq?-C(`6{{XT(Kc93dp;S9m)7 zHb!|1sFC^c-br!TwrUI}pDIGxMqnNR-BA;{f@Uz}6&T_OQjZHvQ9Pczz6`)|P1nJB zJn7DB=g>_5f>bR1ASh!ZbfzPyCSMY-i+fdr5qbx7*-d%QPeM_~$LR)*jz*6aNvtJQ z%>$0E!R%6N47@a!H&hn$+_vj@@rOiI>gOyZhm$PdYrGT8mLf0uX+@gf6|rpe&WcIm6& zdIa%thm$sFWF$+J=vdBf9WfE3;lY5TXdfYrJ`?ptVk`6!63OR#x&DiQ|{i?7|n4#U4Z zv3(clpA>LM>d{v2sm9#8A3*Z54@ivLiMtQJ;PSLn{m zsE3`#P2c9gBKc)7I2tjpAOFXUf*jD%s*W3xzG%+1Y)H53gXY)BFS(^ynpi?Tk-Qx1 zq)T7~>ARe^eBkwS2f^Hh1$b}Uc+hU&M&3GN12z|-VZ{J2=L6C1q6l}5Er%G=z1zN^ zx#E-syzSq~+@ta)`3ZU;?jrxoH>Du?%~98Iu^aoeIRVs*Lb0{0EjG;~_DWh0T}_M} zI*uY(y_pgdeUO#VdWAz5B%*f=Iogc4nG|OQR;8p z`%5*{*?FV)7!IL(j)F9+JG#8Qg_&`WnZ7iYrLQAxyZ3l#{@$ILk6Hqrm&$RuG6g>@ zyaw<*1ocj*a6(Q2P9A(1v)NX(_{|jbG}H3dE?aQ@A4A#J;tJP zt&wi<@j@6SC*E4mo0zRxhLYX$bg~^21ofTaq%R5qZ)q{K~N{Vz?;4H z5$}B`I9?or<_^RdzIK5r_a~tI@M^#EEO;^xMWwxrC#k4q`ZT{xlrj{T zax_%z<=f;bP&dpQJFVTY`Q~$>b59axtxrO|ot$OSzN z-Vk_`ya*ag1$6j&3zq+R6egDxW1YqxJQls<+BJn-(Hes0O*H3j`k?JOTmy=P2mE{- zf=O)x-i^tIxua^pXwa|N??NVY&`!L!O;EJR5&@m3T4S6m^1ta+J05z!Q*uNrX_+M?ciQd zqjwX#2$l_%yy-&(m#v)0B}YGVeR30Dd5dz|Z{6UX1MIP*Z*S}!l>;BXa2R|No4-=dJu3yT|D)*K<6=zTFg}DRqsTELLWzh$%sjUuq!>}iMx-#* zrW7JtqokBbp$yRhLv+wcHScpP9dt6ZVk4}Gjci!4Y}WeSzkmCqPltD&_kQl{y1tkB zLHOG5BFY{g2I+TCxci*;8|{~5bAIeY?ZKneL8`^(tMOod;wFnPFXP!yCeU7L8>;B} z-fn#hT8xnVOggoV+0;cdUCkQI4iG`cM$(OfKguNIoOo5j3>-5m z1w6NG0lUlbSUzn4b)R%YiT@I5)2&1FUYbXox5JpHqXWpV4$2+FApD;+bl)`>p6$Ph zO%6toW!0WGR zAg^^qtLZ0D?zRdIKFx<2F}EO0Wq=vMNzic03tb#$!;zRAV!QW}CcD(4{^2Orc)kc7 zhgQRv)4frD{aVljaMs#VB{TbjJk%~R((w@op<1+`G|It(IPfe__$d(X`jH-gDMhF% z+sC8-iRE9MuF;uZ%#TsO~Pu z!lO5!+**!5{>(sC;vdZXJhA2d!cld2t`IA~jdq*%f&RxL-ui^@NmEurqqh=Ul>Moz zeJ+>#j6p|97F)FTE_bQREr(sws6JeVGuw3IWK)^O}Vu?X~DS& zh>5)gL4jqkyWkw<*zU5zhe;5x6Y(hj0?-WjD5NDc@cLD|&}k^msw3urhxHK5D@wzx zXT$Xrn+jr+3tTqs4662Tkj>B>M!n`@a4M~Y?8u)m zE~5~?moCJ|{}L-jLF~-VQ_MJL2t0$!uv1q6rTs4Q@GB z8OOi4976A;8rJc{iRm`Z6tX9)(dTF^8qoLgtQ0W%bUYWYGh=2Gj-#i1ENO&kl%+7C zy~RT5mxh|&l^+Yi8|LKk6w)WO6T}u)p=reFcXM&ru*%-Utqw~0tgBHneGDA zRWRaL-ulA`3hfTymw$(Wq!BROp0Xyo<$}k@5%3_Fex^kO!S}Hds_$H4z3orm&C_;O?q0M8Sc)H#pcST z&~G&{LS{G#3(sAlTww*MLxyoVy$?ES9#99BEq>j07(@OZ4Z6>cJk_7H$;-BAXM-t<1x%Lp7@*D(7bCl_{R2z)Hmi>{o7&aydejL1?j-mS=e?V9lD+_MW;V} zpy}FDyz8F_VGSYB@@rpI&+W%UzIkBins98|ZNlCslmlFrK}Ug@I^z4H3H3F)hY{l? zd^&{Q3B#QmDPyhJi&be(sBf_t%q#xjRr_vn*~eUX92Jd8<06@#TMWiomm>@bgVvBl zq2AgH3qx$NRNRw$5l_{&i5Lt=4)YO%&q2V;Gpw~K6kA`}@t~Y2>J_|zZNn!(dr}3n zev}ODbU(>HR*2b&2UzPOJJR3gFmYrr7T=Z)PhU*J^Mj9K-i@1}!7DPI-g)%fxDjz@ zFlCmrK(wHQ8&pWaiRRz>e)MBs zeX%|46_4*E?&yl%ka74Tin2Zmy3#tW>Bd<0wFgJbeP5Wz^r2{=FvQ?n7qFwM4|kAc z@`iT>*|R-IQSxOm4}H24LJw~R znRznSey+fw`%i+eYBTNEgRf$m3HqLuRlx@ywA&7RQ?B74 zpE9U-lOc}oj*kDG#vjT=FnQ|>p6;=*cQ!qXpNU(2uMf2Lea*8&{^LQ;T`_h-ckrCi zgLzoQf~l)YX4O;$r?B4ygMJF-&HfmUB5tD#U(U-bW{+%I>GD=isi{*vMD0Y4ltayX@!5mj3uylZDU5=b-$@aA;7q@VmFw7?&-; z_^m}Kas9|L{?13sl}n&0PsdsZ{zqD`4<9qE9Q|$%gxyQ%=j}NdEFLU|?*lG@pa+JIvInNhW{&E1z=j z3~C;xv6BBY8AYp^PT?$bZ@+~}=V*6xD1!%kp9is{nn^PJghYpQOf5V~S;+Yia?l;u zp1+EcnxRaxw1QbIR?(dQjMdVv->9mFyl}aA@q0CfUZM_5w@Wxke-JqR`#KxLj?V|r)|uS>RSdjXl?3%)yukM5O;k)( z3FiOj^}bkx-6vFIzaNJ&wALD4m7aoAc4-j)hX``Wm< zPj3kNs}D$QE>VweS1#|DAYgbE*boz-${>b)mrMY28-%c#sa&K=(n>~}vwPY|bgU83 zVd7$l35W#a!tK<1*};N-{|T3P)PZVMku15}9Bez_1WkI!n0{6XdIn5J=?`<5=TDtV zBW<{D^(4}zCt-*GeO@~>3r2h-KDzq85N0?9ON-OEYca8)Ojl4Raw)oxn?*g7xnNX7 z9>3pPnaQ`SI<@PbUw-r)6#*p*o?xmjX1v=+|84P&&R z_0Ix@q`yqHDNtx|%44N{4si>5j&#GjLYB4|1=D6GBNo8C{>!V!nVIH;f>F+9KW4t-zDTxJLPvvTlrk4Q{?coXHv#zVs~ zBZ!_)8Ax3u7#wgzi)J%a4kqqFQ3sQMe8&R*S_`ArlV@iAc}Vz9I@tn1$C-5gcE7ZUnYJq zpX(njgY1h%SSMFecJGMHZ*V2*HaK(JdViSo+iBXduH(7^KS2Pdpk*&J-f?>z=Jj2L zb_Z3EYT|_ASPyJ}ObqLfN?fw}2$;IvVy)R?CW)OW%S`pdj)%)(cNZmSUZxBB%3iSZ zc^%|MQ{TjN;_B^bV9M|Uh#4OR(gUS<_gM|vih}6x#5NiCMAp?f z8O*%8q29hZSi1Tr9!5-!QrZ_N_G}kybG^a%TpR|3ZH17^vplWWTI`o{1tp5lyuD;`WN6S>E0>ICfGE( zRyqoX>&x)fgH-%dBms}fqrtRy9zDZ=#n%m#qo0hL)y1HG-k(3!jK}b+2f$%-AnsOI%%jk`ryi) z^pLlw1)|5GTi`oYz!0->P*M;NPPQLda&!PFUF^YZ21f^7CU5i11Ig+H!H3xFqBMtM zuYh%MdLgQ@-xWAbNN3yCMSG|ExsSXh(GV z=Nff@UtppgW?(wVkm;Q!jrpP+Jr6SQ&827c->>X-QUZQ>oeU!TNG|WF;HEPSS$nY& zx=tYv{0TMd*l-54x4VM=ihr5@#ADbnI{_Zdib46@UuBI>R

EQS!!|lzq>$g5Znf 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

9@gKK8HjSlBPelinH)e$U zLi8s(M-0;Aok9!q7*`IFVRDRp%J>r7izqVaCTl-m#*E+fhgNs;ay|}*8i&c)6x|m~ zT|Gd&bpo8)BY@muw;(ZHk7AQcvJlFssq&4PF5-jKp^3pa98NP>2N1j0;;Tc|s4gP* zfcOFvC8nWL+6y`dCb6AC#DtI^;JS5tW!jN;#8W?mmJ>3udFXbC){jBmbQ^T;7Q^KI zOJovb33`~8W0+GV))42e;pQT&zjL1z_0Gkyj`CtB>d5)$kZRmv99e(oPXK$W5cU-WAtvA;;?3GIF5H zH2LrI!25$5B2TE$uzM=^D>p&>l^p6;8BlX(D83j#?2F+|%=G9_V4C1h{q_RzUYL&} zPb<{wk7xZ(?uR0Jw<`KKarw0>!Ne;Gbt5j4ckHap^9Z@9XZS(Lb&jqjKiK+$RP=OT zhD#_nWaL_crTIDZUAo2x(2m?^9_2aP))VV(Di%F0gr@u;)J49M`IrtN59kumU$Gqb zjwD`+tG!@$djwRkpwI2^%QOQH9D@SMWC)#q2nOeGM)kl`viv@ipq0Ap`v1|{@u3NG zxVD?z%&AZ`N=r<#FmBv_~SoOah2u{Pb^`=_an3eTP~<9{*~%RwaIh~wn$S>m0{ST6QKM)8bX$DK-bJS z>>2GD+a2a%;HN!!iYKCB*SVNJodX(8fQHPz_;B7KT(Rg9idXv*TW}@!>r0)6i)n&- z_0K5Dtq|G{IKtr96VP&<5=xz=_+##V(08EBe-HXOJ{yDbr3PG~_moQap6)ZxO0ma zY|cWlLlNx#s)X3cSF9yBl9^1Sd_|ZO>aX|(+N7I6QSeGwrA$Wo zmp9C=Z7H1*2BKzj7zWf@AimxQE#xh0noR_dN#OqJbRh0oP=H?~j7M>MKtkP=WFYI-~cV z2LVryQn!ShPg5pA&7=dczOw=)w&z&np9Zk(%So(wd>R9P8HxTu#TbMKF?HP;$a$pY zPVVQywf_)o>edT;_M{ok^<~i1{Ty$s72&q3C{V1hL}kSm$eP<18-rJ3OIRK;H;Gxf z@d)1gY=V!wUjtouG<2>QiY6nEVf)VGJYe~9l=@r(m6?g4E-&GRW!cR6oHy-Yb<8&;A=_i#ML+E^R}h@s

E}DkAx;41z4Ty3O^c3!OhPXEQ9J$ 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Ú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

pdD129E*B3ewVttw8aYKyGD~ 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

7HHwLD=hrxRK|LI;peNL7U@Oe-e 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

;7dZ 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%0ASiw+N&S*0o|m{_sYuh%w2WAgw@`2@ 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ℑi6Ixv|nxx(v}pQ?Ry2R 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

9edBZSfA^Z^+AzvO%UHeNZHRl_ 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

nyw2TCbU&XwA_wk&j{@nE-&ol< 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

5gOJi|f>Jb%Xi;bDwersZg4?9zU%!f|zqFP-N%L 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%0DoWkZfBIc+ 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 z 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 z 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

R@BA4`L!4>&v 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 +j9B2bva!v6f6PY)+U0 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

LW~oABYTGH^~^!V><@ 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%0AQoBX+KU~~r9_Z$GFMY$N{9r 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

7wI%xt+Emdq5>J?b~U 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

uLxk}#8=b0A^=VyrQ|2`vvo(fXn(s`UTi%3Vcj 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

%~WrnFnNX`3He<^oOf4zc`+TlXNSPc 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

P0_1U|nfb_f%bN%uskKG=P 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 literal 0 HcmV?d00001 diff --git a/tests/golden/ssm_recur_state_out.bin b/tests/golden/ssm_recur_state_out.bin new file mode 100644 index 0000000000000000000000000000000000000000..aefa836799d88aef431d24c2243eac343b82cdca GIT binary patch literal 2097209 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%0DsDFLxd@tN&1tB*Dv!! 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О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

S%jjW}5_KB~4EF&m!_8q!%rrZ)jS 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%0DN72J{{hYvaSk-%#gVmc-aqPGIj^}p9BXi`I6 z(u~kt-xxFIeZys&Rl)AX6Jq>(uXfb@eISuLrZcnU3%T+iFM4y@^)ksn%(?fvFk9OW zYg0HroT3*zIp(EfR#(c#SHxhu$yJ!?)j$&VU4{&Uc`cKLy(pVq`EIU!RyBi*q}cbgGTr_{#!%^2SaSiFFqkk%K#gVnJWVNX7>1`J8>mhkDe8*4+>wX0Nf z+Weh^&Vo4{J-MNREhom8zi6KkqDtv+~K)1kW|_-^t&(Vo4Znss%O(v_B|Df

BF8c1d46YFz8~Fm4m;I)oYj8uOX}X-Y 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

2?8K~-0L|^P* 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

KdH!&jj20=oXE;r$NNJmquUB#>mYl8Jk@Y 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