GGUF
English
Japanese
misaka-palw
misaka
palw
proof-of-compute
deterministic-inference
integer-quantization
conversational
Instructions to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: llama cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: llama cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: ./llama-cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M # Run inference directly in the terminal: ./build/bin/llama-cli -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Use Docker
docker model run hf.co/Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
- LM Studio
- Jan
- Ollama
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Ollama:
ollama run hf.co/Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
- Unsloth Desktop
- Pi
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Configure the model in Pi
# Install Pi: npm install -g @earendil-works/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Docker Model Runner
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Docker Model Runner:
docker model run hf.co/Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
- Lemonade
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Run and chat with the model
lemonade run user.Qwen3.6-35B-A3B-PALW-runtime-Q4_K_M
List all available models
lemonade list
- Hermes Agent
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Run Hermes
hermes
- Atomic Chat
- OpenClaw
How to use Misakachain/Qwen3.6-35B-A3B-PALW-runtime with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "Misakachain/Qwen3.6-35B-A3B-PALW-runtime:Q4_K_M" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
Initial import: PALW proof-of-compute runtime for Qwen3.6-35B-A3B (code + docs, no model weights)
0985c38 verified | diff --git a/ggml/include/ggml-metal.h b/ggml/include/ggml-metal.h | |
| index 433838f0..213e148d 100644 | |
| --- a/ggml/include/ggml-metal.h | |
| +++ b/ggml/include/ggml-metal.h | |
| GGML_BACKEND_API void ggml_backend_metal_capture_next_compute(ggml_backend_t bac | |
| GGML_BACKEND_API ggml_backend_reg_t ggml_backend_metal_reg(void); | |
| +// PALW kernel-level dispatch hook. | |
| +// | |
| +// When set (process-global), the callback is invoked for every Metal compute | |
| +// dispatch with the bound pipeline (kernel) name and the launch geometry | |
| +// (threadgroup grid tg0..2 and threads-per-threadgroup tptg0..2). This lets the | |
| +// PALW observer produce a kernel-level execution trace bound to the actual GPU | |
| +// kernel dispatches rather than to graph-node outputs. Pass NULL to disable. | |
| +typedef void (*ggml_metal_palw_dispatch_cb)( | |
| + void * user_data, | |
| + const char * pipeline, | |
| + int tg0, int tg1, int tg2, | |
| + int tptg0, int tptg1, int tptg2); | |
| + | |
| +GGML_BACKEND_API void ggml_metal_palw_set_dispatch_hook(ggml_metal_palw_dispatch_cb cb, void * user_data); | |
| + | |
| #ifdef __cplusplus | |
| } | |
| #endif | |
| diff --git a/ggml/src/ggml-metal/ggml-metal-device.m b/ggml/src/ggml-metal/ggml-metal-device.m | |
| index 80e47f2c..3e36c08d 100644 | |
| --- a/ggml/src/ggml-metal/ggml-metal-device.m | |
| +++ b/ggml/src/ggml-metal/ggml-metal-device.m | |
| #import "ggml-metal-device.h" | |
| +#import "ggml-metal.h" | |
| #import "ggml-impl.h" | |
| #import "ggml-backend-impl.h" | |
| void ggml_metal_cv_set_bool(ggml_metal_cv_t cv, bool value, int32_t idx) { | |
| struct ggml_metal_pipeline { | |
| id<MTLComputePipelineState> obj; | |
| + | |
| + // PALW: stable kernel (pipeline) name, captured for kernel-level tracing. | |
| + char name[128]; | |
| }; | |
| ggml_metal_pipeline_t ggml_metal_pipeline_init(void) { | |
| ggml_metal_pipeline_t ggml_metal_pipeline_init(void) { | |
| *res = (struct ggml_metal_pipeline) { | |
| /*.obj =*/ nil, | |
| + /*.name =*/ {0}, | |
| }; | |
| return res; | |
| struct ggml_metal_pipeline_with_params ggml_metal_library_compile_pipeline(ggml_ | |
| res.pipeline = ggml_metal_pipeline_init(); | |
| res.pipeline->obj = obj; | |
| + // PALW: record the stable kernel name for kernel-level dispatch tracing. | |
| + snprintf(res.pipeline->name, sizeof(res.pipeline->name), "%s", name); | |
| ggml_metal_pipelines_add(lib->pipelines, name, res.pipeline); | |
| } | |
| struct ggml_metal_pipeline_with_params ggml_metal_library_compile_pipeline(ggml_ | |
| struct ggml_metal_encoder { | |
| id<MTLComputeCommandEncoder> obj; | |
| + | |
| + // PALW: name of the pipeline currently bound, for kernel-level dispatch tracing. | |
| + const char * cur_pipeline; | |
| }; | |
| +// PALW: process-global kernel-dispatch hook. When set, it is invoked for every | |
| +// Metal compute dispatch with the bound pipeline (kernel) name and the launch | |
| +// geometry (threadgroup grid + threads-per-threadgroup). Used by the PALW | |
| +// observer to produce a kernel-level (not graph-fallback) execution trace. | |
| +static ggml_metal_palw_dispatch_cb g_palw_dispatch_cb = NULL; | |
| +static void * g_palw_dispatch_ud = NULL; | |
| + | |
| +void ggml_metal_palw_set_dispatch_hook(ggml_metal_palw_dispatch_cb cb, void * user_data) { | |
| + g_palw_dispatch_cb = cb; | |
| + g_palw_dispatch_ud = user_data; | |
| +} | |
| + | |
| ggml_metal_encoder_t ggml_metal_encoder_init(ggml_metal_cmd_buf_t cmd_buf_raw, bool concurrent) { | |
| ggml_metal_encoder_t res = calloc(1, sizeof(struct ggml_metal_encoder)); | |
| void ggml_metal_encoder_debug_group_pop (ggml_metal_encoder_t encoder) { | |
| void ggml_metal_encoder_set_pipeline(ggml_metal_encoder_t encoder, struct ggml_metal_pipeline_with_params pipeline) { | |
| [encoder->obj setComputePipelineState:pipeline.pipeline->obj]; | |
| + // PALW: remember the bound kernel name so dispatches can be attributed to it. | |
| + encoder->cur_pipeline = pipeline.pipeline->name; | |
| } | |
| void ggml_metal_encoder_set_bytes(ggml_metal_encoder_t encoder, void * data, size_t size, int idx) { | |
| void ggml_metal_encoder_set_threadgroup_memory_size(ggml_metal_encoder_t encoder | |
| } | |
| void ggml_metal_encoder_dispatch_threadgroups(ggml_metal_encoder_t encoder, int tg0, int tg1, int tg2, int tptg0, int tptg1, int tptg2) { | |
| + // PALW: report the kernel-level dispatch (bound pipeline + launch geometry). | |
| + if (g_palw_dispatch_cb) { | |
| + g_palw_dispatch_cb(g_palw_dispatch_ud, encoder->cur_pipeline ? encoder->cur_pipeline : "", | |
| + tg0, tg1, tg2, tptg0, tptg1, tptg2); | |
| + } | |
| [encoder->obj dispatchThreadgroups:MTLSizeMake(tg0, tg1, tg2) threadsPerThreadgroup:MTLSizeMake(tptg0, tptg1, tptg2)]; | |
| } | |
| diff --git a/src/models/qwen35moe.cpp b/src/models/qwen35moe.cpp | |
| index 7b0876cb..4d955261 100644 | |
| --- a/src/models/qwen35moe.cpp | |
| +++ b/src/models/qwen35moe.cpp | |
| void llama_model_qwen35moe::load_arch_hparams(llama_model_loader & ml) { | |
| ml.get_key(LLM_KV_EXPERT_SHARED_FEED_FORWARD_LENGTH, hparams.n_ff_shexp, false); | |
| ml.get_key(LLM_KV_ATTENTION_LAYERNORM_RMS_EPS, hparams.f_norm_rms_eps); | |
| - ml.get_key_or_arr(LLM_KV_ROPE_DIMENSION_SECTIONS, hparams.rope_sections, 4, true); | |
| + // PALW: newer HF->GGUF conversions write 3 mrope sections ([t, h, w]) and | |
| + // omit the trailing zero; older conversions write 4. Accept both forms and | |
| + // zero-pad so the pinned runtime loads current upstream GGUF artifacts. | |
| + { | |
| + std::vector<int32_t> sections; | |
| + ml.get_arr(LLM_KV_ROPE_DIMENSION_SECTIONS, sections, true); | |
| + if (sections.size() != 3 && sections.size() != 4) { | |
| + throw std::runtime_error("rope.dimension_sections must have 3 or 4 entries"); | |
| + } | |
| + std::fill(hparams.rope_sections.begin(), hparams.rope_sections.end(), 0); | |
| + std::copy(sections.begin(), sections.end(), hparams.rope_sections.begin()); | |
| + } | |
| // Load linear attention (gated delta net) parameters | |
| ml.get_key(LLM_KV_SSM_CONV_KERNEL, hparams.ssm_d_conv); | |
| void llama_model_qwen35moe::load_arch_tensors(llama_model_loader & ml) { | |
| layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", il), { n_embd }, flags); | |
| if (!hparams.is_recr(il)) { | |
| - // Attention layers | |
| - create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, flags); | |
| + // Attention layers. The LLAMA_LOAD_LOCALS macro derives the global | |
| + // n_embd_k_gqa/n_embd_v_gqa from layer 0, which in this MoE is a | |
| + // linear-attention (recurrent) layer with n_head_kv == 0. Use the | |
| + // uniform full-attention KV projection width instead. | |
| + const int64_t n_embd_k_gqa_attn = hparams.n_embd_k_gqa_max(); | |
| + const int64_t n_embd_v_gqa_attn = hparams.n_embd_v_gqa_max(); | |
| + create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa_attn, n_embd_v_gqa_attn, flags); | |
| layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, flags); | |
| // Q/K normalization for attention layers | |
| void llama_model_qwen35moe::load_arch_tensors(llama_model_loader & ml) { | |
| layer.wqkv = create_tensor(tn(LLM_TENSOR_ATTN_QKV, "weight", il), { n_embd, key_dim * 2 + value_dim }, TENSOR_NOT_REQUIRED); | |
| layer.wqkv_gate = create_tensor(tn(LLM_TENSOR_ATTN_GATE, "weight", il), { n_embd, value_dim }, TENSOR_NOT_REQUIRED); | |
| layer.ssm_conv1d = create_tensor(tn(LLM_TENSOR_SSM_CONV1D, "weight", il), { hparams.ssm_d_conv, conv_dim }, flags); | |
| - layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, "bias", il), { hparams.ssm_dt_rank }, flags); | |
| + // PALW: some conversions store the delta-time bias without the | |
| + // ".bias" suffix ("blk.N.ssm_dt"). Accept both spellings. | |
| + layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, "bias", il), { hparams.ssm_dt_rank }, flags | TENSOR_NOT_REQUIRED); | |
| + if (!layer.ssm_dt) { | |
| + layer.ssm_dt = create_tensor(tn(LLM_TENSOR_SSM_DT, il), { hparams.ssm_dt_rank }, flags); | |
| + } | |
| layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A_NOSCAN, il), { hparams.ssm_dt_rank }, flags); | |
| layer.ssm_beta = create_tensor(tn(LLM_TENSOR_SSM_BETA, "weight", il), { n_embd, n_v_heads }, flags); | |
| layer.ssm_alpha = create_tensor(tn(LLM_TENSOR_SSM_ALPHA, "weight", il), { n_embd, n_v_heads }, flags); | |
| void llama_model_qwen35moe::load_arch_tensors(llama_model_loader & ml) { | |
| layer.attn_norm = create_tensor(tn(LLM_TENSOR_ATTN_NORM, "weight", il), { n_embd }, 0); | |
| layer.attn_post_norm = create_tensor(tn(LLM_TENSOR_ATTN_POST_NORM, "weight", il), { n_embd }, 0); | |
| - create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa, n_embd_v_gqa, 0); | |
| + // The MTP layer index has no entry in the per-layer n_head_kv array, so | |
| + // derive the KV projection width from the full-attention trunk layers. | |
| + const int64_t n_embd_k_gqa_mtp = hparams.n_embd_k_gqa_max(); | |
| + const int64_t n_embd_v_gqa_mtp = hparams.n_embd_v_gqa_max(); | |
| + create_tensor_qkv(layer, il, n_embd, n_embd_head_k * n_head * 2, n_embd_k_gqa_mtp, n_embd_v_gqa_mtp, 0); | |
| layer.wo = create_tensor(tn(LLM_TENSOR_ATTN_OUT, "weight", il), { n_embd_head_k * n_head, n_embd }, 0); | |
| layer.attn_q_norm = create_tensor(tn(LLM_TENSOR_ATTN_Q_NORM, "weight", il), { n_embd_head_k }, 0); | |
| layer.attn_k_norm = create_tensor(tn(LLM_TENSOR_ATTN_K_NORM, "weight", il), { n_embd_head_k }, 0); | |
| void llama_model_qwen35moe::load_arch_tensors(llama_model_loader & ml) { | |
| for (int i = n_layer; i < n_layer_all; ++i) { | |
| load_block_mtp(i); | |
| } | |
| + | |
| + // PALW: the Ollama-packaged Qwen3.6-35B-A3B GGUF bundles the multimodal | |
| + // vision tower ("v.*") and, when the next-token/MTP head is not enabled by | |
| + // hyperparameters, the MTP sub-model ("mtp.*") in the same file. The pinned | |
| + // text runtime never builds those sibling sub-models, so account for their | |
| + // tensors here; otherwise done_getting_tensors() rejects the load for | |
| + // having created fewer tensors than the file contains. This does not weaken | |
| + // the check for a genuinely missing text tensor: only tensors that exist in | |
| + // the file under these sibling prefixes are counted, exactly once each. | |
| + for (const auto & entry : ml.weights_map) { | |
| + const std::string & name = entry.first; | |
| + const bool is_vision = name.rfind("v.", 0) == 0; | |
| + const bool is_mtp = name.rfind("mtp.", 0) == 0; | |
| + if (is_vision || is_mtp) { | |
| + ml.size_data -= ggml_nbytes(entry.second.tensor); | |
| + ml.n_created++; | |
| + } | |
| + } | |
| } | |
| std::unique_ptr<llm_graph_context> llama_model_qwen35moe::build_arch_graph(const llm_graph_params & params) const { | |
| ggml_tensor * llama_model_qwen35moe::graph::build_layer_attn( | |
| const int64_t n_embd_head = hparams.n_embd_head_v(); | |
| GGML_ASSERT(n_embd_head == hparams.n_embd_head_k()); | |
| + // PALW: the llm_graph_context base initializes n_head/n_head_kv from layer | |
| + // 0, which in this hybrid MoE is a linear-attention layer with n_head_kv 0. | |
| + // Use the per-layer counts for the full-attention reshapes below. | |
| + const int64_t n_head = hparams.n_head(il); | |
| + const int64_t n_head_kv = hparams.n_head_kv(il); | |
| + | |
| // Order: joint QG projection, QG split, Q norm, KV projection, K norm, RoPE, attention | |
| // Qwen3Next uses a single Q projection that outputs query + gate | |
| diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt | |
| index 780df326..a1f04870 100644 | |
| --- a/tools/CMakeLists.txt | |
| +++ b/tools/CMakeLists.txt | |
| else() | |
| endif() | |
| add_subdirectory(tokenize) | |
| add_subdirectory(parser) | |
| + add_subdirectory(palw-observer) | |
| add_subdirectory(tts) | |
| add_subdirectory(mtmd) | |
| if (GGML_RPC) | |
| diff --git a/tools/palw-observer/CMakeLists.txt b/tools/palw-observer/CMakeLists.txt | |
| new file mode 100644 | |
| index 00000000..40e3ffd8 | |
| --- /dev/null | |
| +++ b/tools/palw-observer/CMakeLists.txt | |
| +set(TARGET llama-palw-observer) | |
| + | |
| +add_executable(${TARGET} palw-observer.cpp) | |
| +target_link_libraries(${TARGET} PRIVATE llama llama-common-base ${CMAKE_THREAD_LIBS_INIT}) | |
| +target_compile_features(${TARGET} PRIVATE cxx_std_17) | |
| + | |
| +if(LLAMA_TOOLS_INSTALL) | |
| + install(TARGETS ${TARGET} RUNTIME) | |
| +endif() | |
| diff --git a/tools/palw-observer/README.md b/tools/palw-observer/README.md | |
| new file mode 100644 | |
| index 00000000..efd1823b | |
| --- /dev/null | |
| +++ b/tools/palw-observer/README.md | |
| +# PALW native graph observer | |
| + | |
| +`llama-palw-observer` is a non-interactive, single-request Qwen3.6-35B-A3B runner. | |
| +It uses only the public llama and ggml APIs and writes versioned JSONL records to | |
| +stdout. Runtime and model logs are written to stderr. | |
| + | |
| +## Build and run | |
| + | |
| +```sh | |
| +cmake --build build-palw --target llama-palw-observer -j | |
| +build-palw/bin/llama-palw-observer \ | |
| + --model /path/to/Qwen3.6-abliterated-35b-Claude-4.7-Q4_K_M.gguf \ | |
| + --prompt "Hello" \ | |
| + --n-predict 8 \ | |
| + --observer graph | |
| +``` | |
| + | |
| +The runner fixes greedy sampling, request batch, logical/physical token batch, | |
| +parallel sequences, tensor parallelism, and CPU thread counts to one. It also | |
| +disables context shifting, speculation, Flash Attention, and split-model tensor | |
| +parallelism. The prompt is therefore evaluated one token at a time. The context | |
| +is sized before execution and the run fails instead of shifting when the prompt | |
| +and prediction bound exceed the model training context. | |
| + | |
| +The accepted model profile is hybrid Qwen3.6-35B-A3B (mixture-of-experts): | |
| +architecture `qwen35moe`, 40 layers, 2048 hidden elements, and a 248320-token | |
| +vocabulary, decoder-only. Per-layer head counts vary (the model alternates | |
| +linear-attention/state-space layers with full-attention layers), so head counts | |
| +are not part of the accepted profile. Other profiles fail before a header or | |
| +inference result is emitted. Legitimate zero-element recurrent-state-cache graph | |
| +nodes are skipped rather than treated as errors. | |
| + | |
| +## Observer modes | |
| + | |
| +- `off` installs no scheduler callback. | |
| +- `graph` emits metadata for every graph node from the callback's `ask` stage. | |
| + It never requests tensor data. | |
| +- `sketch` emits the same metadata and requests post-compute data only for plain | |
| + `MUL_MAT` nodes (indirect expert GEMM, `MUL_MAT_ID`, is emitted as an ordinary | |
| + metadata node, not sketched). It copies at most the first 64 contiguous output | |
| + elements through `ggml_backend_tensor_get`. Each element becomes one | |
| + hexadecimal nibble: one sign bit and a fixed three-bit magnitude bucket. The | |
| + resulting 64 hex digits are a 256-bit sketch. Raw activation values are never | |
| + serialized. | |
| + | |
| +Nibble bit 3 is the sign bit. Bits 0-2 use these absolute-value buckets: zero, | |
| +`(0, 2^-8)`, `[2^-8, 2^-4)`, `[2^-4, 2^-2)`, `[2^-2, 1)`, `[1, 4)`, | |
| +`[4, 16)`, and `[16, infinity)`. Fewer than 64 available elements are padded | |
| +with zero nibbles. | |
| + | |
| +Every stdout line has `schema`, `schema_version`, and `record`. A successful run | |
| +contains one `header`, zero or more `event` records, and one `result`. The result | |
| +contains prompt token IDs, sampled token IDs (including an EOG token when one is | |
| +sampled), the non-special generated output bytes, and the stop reason. | |
| + | |
| +## Limits | |
| + | |
| +This target is a runtime-observation prototype, not a PALW receipt generator. | |
| +It does not calculate canonical compute units, sign receipts, form commitments, | |
| +or trace CUDA kernels. The sketch tile is explicitly identified as | |
| +`graph_fallback_logical_prefix_v1`, and `kernel_trace.claim` is | |
| +`not_a_cuda_kernel_trace`. Scheduler callbacks add synchronization and can | |
| +change timing, but they must not change token IDs or output bytes. The sketch is | |
| +lossy observation data, not a cryptographic proof. Graph topology and floating | |
| +point results can vary across backends or hardware, so byte identity is only | |
| +claimed for repeated runs of the same pinned model, runtime, and backend class. | |
| diff --git a/tools/palw-observer/palw-observer.cpp b/tools/palw-observer/palw-observer.cpp | |
| new file mode 100644 | |
| index 00000000..83fc243b | |
| --- /dev/null | |
| +++ b/tools/palw-observer/palw-observer.cpp | |
| +#include "ggml-backend.h" | |
| +#include "ggml.h" | |
| +#include "ggml-metal.h" | |
| +#include "llama.h" | |
| +#include "build-info.h" | |
| + | |
| +#include <algorithm> | |
| +#include <array> | |
| +#include <charconv> | |
| +#include <clocale> | |
| +#include <cmath> | |
| +#include <cstdint> | |
| +#include <cstdio> | |
| +#include <cstring> | |
| +#include <exception> | |
| +#include <limits> | |
| +#include <memory> | |
| +#include <string> | |
| +#include <string_view> | |
| +#include <utility> | |
| +#include <vector> | |
| + | |
| +namespace { | |
| + | |
| +constexpr const char * SCHEMA_NAME = "misaka.palw.runtime_observer"; | |
| +// Schema v2 adds "route" records: the real mixture-of-experts Top-K expert | |
| +// selection (the `ffn_moe_topk` tensor) read back post-compute. Consumers that | |
| +// only understand v1 must reject v2. | |
| +constexpr int SCHEMA_VERSION = 2; | |
| +constexpr size_t SKETCH_SAMPLES = 64; | |
| +// Upper bound on selected-expert indices read back from one routing tensor: | |
| +// n_expert_used (Top-K) * n_tokens for a single mixture-of-experts layer. | |
| +constexpr size_t MAX_ROUTE_INDICES = 262144; | |
| + | |
| +// PALW #5 diagnostic: empirically map Metal kernel dispatches to graph nodes so | |
| +// the node<->dispatch correlation can be validated before it is committed as a | |
| +// kernel-level trace. Enabled only when PALW_TRACE_DIAG is set in the env. | |
| +static bool g_diag_enabled = false; | |
| +constexpr uint32_t PALW_CONTEXT_TOKENS = 4096; | |
| + | |
| +enum class observer_mode { | |
| + off, | |
| + graph, | |
| + sketch, | |
| +}; | |
| + | |
| +struct options { | |
| + std::string model_path; | |
| + std::string prompt; | |
| + int32_t n_predict = -1; | |
| + int32_t n_gpu_layers = 999; | |
| + observer_mode mode = observer_mode::off; | |
| + bool prompt_set = false; | |
| + bool prompt_stdin = false; | |
| + bool emit_output_bytes = false; | |
| +}; | |
| + | |
| +struct model_metadata { | |
| + std::vector<std::pair<std::string, std::string>> entries; | |
| +}; | |
| + | |
| +static const char * mode_name(observer_mode mode) { | |
| + switch (mode) { | |
| + case observer_mode::off: return "off"; | |
| + case observer_mode::graph: return "graph"; | |
| + case observer_mode::sketch: return "sketch"; | |
| + } | |
| + return "invalid"; | |
| +} | |
| + | |
| +static void log_callback(ggml_log_level level, const char * text, void *) { | |
| + const char * label = "unknown"; | |
| + switch (level) { | |
| + case GGML_LOG_LEVEL_NONE: label = "none"; break; | |
| + case GGML_LOG_LEVEL_DEBUG: label = "debug"; break; | |
| + case GGML_LOG_LEVEL_INFO: label = "info"; break; | |
| + case GGML_LOG_LEVEL_WARN: label = "warn"; break; | |
| + case GGML_LOG_LEVEL_ERROR: label = "error"; break; | |
| + case GGML_LOG_LEVEL_CONT: label = "cont"; break; | |
| + } | |
| + std::fprintf(stderr, "[llama:%s] %s", label, text ? text : ""); | |
| +} | |
| + | |
| +static void print_usage(const char * argv0) { | |
| + std::fprintf(stderr, | |
| + "usage: %s --model MODEL (--prompt TEXT|--prompt-stdin) --n-predict N " | |
| + "[--observer off|graph|sketch] [--n-gpu-layers N] [--emit-output-bytes]\n", | |
| + argv0); | |
| +} | |
| + | |
| +static bool parse_i32(const char * value, int32_t min_value, int32_t max_value, int32_t & result) { | |
| + if (!value || value[0] == '\0') { | |
| + return false; | |
| + } | |
| + int32_t parsed = 0; | |
| + const char * end = value + std::strlen(value); | |
| + const auto converted = std::from_chars(value, end, parsed); | |
| + if (converted.ec != std::errc() || converted.ptr != end || parsed < min_value || parsed > max_value) { | |
| + return false; | |
| + } | |
| + result = parsed; | |
| + return true; | |
| +} | |
| + | |
| +static bool take_value(int argc, char ** argv, int & i, const char * option, const char *& value) { | |
| + if (i + 1 >= argc) { | |
| + std::fprintf(stderr, "error: %s requires a value\n", option); | |
| + return false; | |
| + } | |
| + value = argv[++i]; | |
| + return true; | |
| +} | |
| + | |
| +static bool parse_options(int argc, char ** argv, options & opts, bool & help, bool & version) { | |
| + help = false; | |
| + version = false; | |
| + for (int i = 1; i < argc; ++i) { | |
| + const std::string_view arg(argv[i]); | |
| + const char * value = nullptr; | |
| + if (arg == "--help" || arg == "-h") { | |
| + help = true; | |
| + return true; | |
| + } else if (arg == "--version") { | |
| + version = true; | |
| + return true; | |
| + } else if (arg == "--model" || arg == "-m") { | |
| + if (!take_value(argc, argv, i, argv[i], value)) { | |
| + return false; | |
| + } | |
| + opts.model_path = value; | |
| + } else if (arg == "--prompt" || arg == "-p") { | |
| + if (opts.prompt_stdin) { | |
| + std::fprintf(stderr, "error: --prompt and --prompt-stdin are mutually exclusive\n"); | |
| + return false; | |
| + } | |
| + if (!take_value(argc, argv, i, argv[i], value)) { | |
| + return false; | |
| + } | |
| + opts.prompt = value; | |
| + opts.prompt_set = true; | |
| + } else if (arg == "--prompt-stdin") { | |
| + if (opts.prompt_set || opts.prompt_stdin) { | |
| + std::fprintf(stderr, "error: prompt input may be selected only once\n"); | |
| + return false; | |
| + } | |
| + opts.prompt_stdin = true; | |
| + } else if (arg == "--emit-output-bytes") { | |
| + opts.emit_output_bytes = true; | |
| + } else if (arg == "--n-predict" || arg == "-n") { | |
| + if (!take_value(argc, argv, i, argv[i], value) || | |
| + !parse_i32(value, 0, 65536, opts.n_predict)) { | |
| + std::fprintf(stderr, "error: --n-predict must be an integer in [0, 65536]\n"); | |
| + return false; | |
| + } | |
| + } else if (arg == "--n-gpu-layers" || arg == "-ngl") { | |
| + if (!take_value(argc, argv, i, argv[i], value) || | |
| + !parse_i32(value, -1, 100000, opts.n_gpu_layers)) { | |
| + std::fprintf(stderr, "error: --n-gpu-layers must be an integer in [-1, 100000]\n"); | |
| + return false; | |
| + } | |
| + } else if (arg == "--observer") { | |
| + if (!take_value(argc, argv, i, argv[i], value)) { | |
| + return false; | |
| + } | |
| + const std::string_view mode(value); | |
| + if (mode == "off") { | |
| + opts.mode = observer_mode::off; | |
| + } else if (mode == "graph") { | |
| + opts.mode = observer_mode::graph; | |
| + } else if (mode == "sketch") { | |
| + opts.mode = observer_mode::sketch; | |
| + } else { | |
| + std::fprintf(stderr, "error: --observer must be off, graph, or sketch\n"); | |
| + return false; | |
| + } | |
| + } else { | |
| + std::fprintf(stderr, "error: unknown argument: %s\n", argv[i]); | |
| + return false; | |
| + } | |
| + } | |
| + | |
| + if (opts.model_path.empty()) { | |
| + std::fprintf(stderr, "error: --model is required\n"); | |
| + return false; | |
| + } | |
| + if (opts.prompt_stdin) { | |
| + std::array<char, 8192> buffer = {}; | |
| + while (true) { | |
| + const size_t count = std::fread(buffer.data(), 1, buffer.size(), stdin); | |
| + if (count != 0) { | |
| + if (opts.prompt.size() > static_cast<size_t>(std::numeric_limits<int32_t>::max()) - count) { | |
| + std::fprintf(stderr, "error: stdin prompt exceeds the tokenizer API bound\n"); | |
| + return false; | |
| + } | |
| + opts.prompt.append(buffer.data(), count); | |
| + } | |
| + if (count != buffer.size()) { | |
| + if (std::ferror(stdin)) { | |
| + std::fprintf(stderr, "error: failed to read prompt from stdin\n"); | |
| + return false; | |
| + } | |
| + break; | |
| + } | |
| + } | |
| + opts.prompt_set = true; | |
| + } | |
| + if (!opts.prompt_set) { | |
| + std::fprintf(stderr, "error: --prompt is required (an explicitly empty prompt is allowed)\n"); | |
| + return false; | |
| + } | |
| + if (opts.n_predict < 0) { | |
| + std::fprintf(stderr, "error: --n-predict is required\n"); | |
| + return false; | |
| + } | |
| + if (opts.prompt.size() > static_cast<size_t>(std::numeric_limits<int32_t>::max())) { | |
| + std::fprintf(stderr, "error: prompt exceeds the tokenizer API bound\n"); | |
| + return false; | |
| + } | |
| + return true; | |
| +} | |
| + | |
| +static void append_json_string(std::string & out, std::string_view value) { | |
| + static constexpr char hex[] = "0123456789abcdef"; | |
| + out.push_back('"'); | |
| + for (const unsigned char c : value) { | |
| + switch (c) { | |
| + case '"': out += "\\\""; break; | |
| + case '\\': out += "\\\\"; 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; | |
| + default: | |
| + if (c < 0x20) { | |
| + out += "\\u00"; | |
| + out.push_back(hex[c >> 4]); | |
| + out.push_back(hex[c & 0x0f]); | |
| + } else { | |
| + out.push_back(static_cast<char>(c)); | |
| + } | |
| + break; | |
| + } | |
| + } | |
| + out.push_back('"'); | |
| +} | |
| + | |
| +static void append_bool(std::string & out, bool value) { | |
| + out += value ? "true" : "false"; | |
| +} | |
| + | |
| +template<typename T> | |
| +static void append_integer(std::string & out, T value) { | |
| + out += std::to_string(value); | |
| +} | |
| + | |
| +static void append_float(std::string & out, double value) { | |
| + if (!std::isfinite(value)) { | |
| + out += "null"; | |
| + return; | |
| + } | |
| + char buffer[64]; | |
| + const int n = std::snprintf(buffer, sizeof(buffer), "%.9g", value); | |
| + if (n <= 0 || static_cast<size_t>(n) >= sizeof(buffer)) { | |
| + out += "null"; | |
| + return; | |
| + } | |
| + out.append(buffer, static_cast<size_t>(n)); | |
| +} | |
| + | |
| +static bool write_json_line(const std::string & line) { | |
| + if (std::fwrite(line.data(), 1, line.size(), stdout) != line.size() || | |
| + std::fputc('\n', stdout) == EOF || std::fflush(stdout) != 0) { | |
| + std::fprintf(stderr, "error: failed to write JSONL to stdout\n"); | |
| + return false; | |
| + } | |
| + return true; | |
| +} | |
| + | |
| +static void append_record_prefix(std::string & out, const char * record) { | |
| + out += "{\"schema\":\""; | |
| + out += SCHEMA_NAME; | |
| + out += "\",\"schema_version\":"; | |
| + append_integer(out, SCHEMA_VERSION); | |
| + out += ",\"record\":"; | |
| + append_json_string(out, record); | |
| +} | |
| + | |
| +static size_t bounded_name_length(const char * name) { | |
| + size_t length = 0; | |
| + while (length < GGML_MAX_NAME && name[length] != '\0') { | |
| + ++length; | |
| + } | |
| + return length; | |
| +} | |
| + | |
| +static std::string_view tensor_name(const ggml_tensor * tensor) { | |
| + if (!tensor) { | |
| + return {}; | |
| + } | |
| + return std::string_view(tensor->name, bounded_name_length(tensor->name)); | |
| +} | |
| + | |
| +static std::string lowercase(std::string_view value) { | |
| + std::string result; | |
| + result.reserve(value.size()); | |
| + for (const unsigned char c : value) { | |
| + if (c >= 'A' && c <= 'Z') { | |
| + result.push_back(static_cast<char>(c - 'A' + 'a')); | |
| + } else { | |
| + result.push_back(static_cast<char>(c)); | |
| + } | |
| + } | |
| + return result; | |
| +} | |
| + | |
| +static bool has_text(std::string_view value, std::string_view needle) { | |
| + return value.find(needle) != std::string_view::npos; | |
| +} | |
| + | |
| +static void collect_related_names(const ggml_tensor * tensor, std::vector<std::string_view> & names) { | |
| + if (!tensor) { | |
| + return; | |
| + } | |
| + if (!tensor_name(tensor).empty()) { | |
| + names.push_back(tensor_name(tensor)); | |
| + } | |
| + if (tensor->view_src && !tensor_name(tensor->view_src).empty()) { | |
| + names.push_back(tensor_name(tensor->view_src)); | |
| + } | |
| + for (int i = 0; i < GGML_MAX_SRC; ++i) { | |
| + const ggml_tensor * src = tensor->src[i]; | |
| + if (!src) { | |
| + continue; | |
| + } | |
| + if (!tensor_name(src).empty()) { | |
| + names.push_back(tensor_name(src)); | |
| + } | |
| + if (src->view_src && !tensor_name(src->view_src).empty()) { | |
| + names.push_back(tensor_name(src->view_src)); | |
| + } | |
| + } | |
| +} | |
| + | |
| +static bool related_names_contain(const ggml_tensor * tensor, std::string_view needle) { | |
| + std::vector<std::string_view> names; | |
| + collect_related_names(tensor, names); | |
| + for (const auto name : names) { | |
| + if (has_text(lowercase(name), needle)) { | |
| + return true; | |
| + } | |
| + } | |
| + return false; | |
| +} | |
| + | |
| +static bool parse_decimal_at(std::string_view name, size_t offset, int & value) { | |
| + if (offset >= name.size() || name[offset] < '0' || name[offset] > '9') { | |
| + return false; | |
| + } | |
| + uint64_t parsed = 0; | |
| + size_t i = offset; | |
| + while (i < name.size() && name[i] >= '0' && name[i] <= '9') { | |
| + parsed = parsed * 10 + static_cast<unsigned>(name[i] - '0'); | |
| + if (parsed > static_cast<uint64_t>(std::numeric_limits<int>::max())) { | |
| + return false; | |
| + } | |
| + ++i; | |
| + } | |
| + value = static_cast<int>(parsed); | |
| + return true; | |
| +} | |
| + | |
| +static bool parse_layer_from_name(std::string_view name, int & layer) { | |
| + const size_t dash = name.rfind('-'); | |
| + if (dash != std::string_view::npos && dash + 1 < name.size()) { | |
| + int parsed = -1; | |
| + if (parse_decimal_at(name, dash + 1, parsed)) { | |
| + size_t end = dash + 1; | |
| + while (end < name.size() && name[end] >= '0' && name[end] <= '9') { | |
| + ++end; | |
| + } | |
| + if (end == name.size()) { | |
| + layer = parsed; | |
| + return true; | |
| + } | |
| + } | |
| + } | |
| + | |
| + const size_t block = name.find("blk."); | |
| + if (block != std::string_view::npos && parse_decimal_at(name, block + 4, layer)) { | |
| + return true; | |
| + } | |
| + | |
| + if (has_text(lowercase(name), "cache_")) { | |
| + const size_t marker = name.rfind("_l"); | |
| + if (marker != std::string_view::npos && parse_decimal_at(name, marker + 2, layer)) { | |
| + return true; | |
| + } | |
| + } | |
| + return false; | |
| +} | |
| + | |
| +static int tensor_layer(const ggml_tensor * tensor) { | |
| + int layer = -1; | |
| + if (parse_layer_from_name(tensor_name(tensor), layer)) { | |
| + return layer; | |
| + } | |
| + std::vector<std::string_view> names; | |
| + collect_related_names(tensor, names); | |
| + for (const auto name : names) { | |
| + if (parse_layer_from_name(name, layer)) { | |
| + return layer; | |
| + } | |
| + } | |
| + return -1; | |
| +} | |
| + | |
| +static void append_shape(std::string & out, const ggml_tensor * tensor) { | |
| + out.push_back('['); | |
| + for (int i = 0; i < GGML_MAX_DIMS; ++i) { | |
| + if (i != 0) { | |
| + out.push_back(','); | |
| + } | |
| + append_integer(out, tensor->ne[i]); | |
| + } | |
| + out.push_back(']'); | |
| +} | |
| + | |
| +static void append_token_ids(std::string & out, const std::vector<llama_token> & tokens) { | |
| + out.push_back('['); | |
| + for (size_t i = 0; i < tokens.size(); ++i) { | |
| + if (i != 0) { | |
| + out.push_back(','); | |
| + } | |
| + append_integer(out, tokens[i]); | |
| + } | |
| + out.push_back(']'); | |
| +} | |
| + | |
| +class graph_observer { | |
| +public: | |
| + graph_observer(observer_mode mode, int32_t n_layers) : mode_(mode), n_layers_(n_layers) {} | |
| + | |
| + void set_phase(const char * phase, int64_t step) { | |
| + phase_ = phase; | |
| + phase_step_ = step; | |
| + } | |
| + | |
| + bool failed() const { | |
| + return failed_; | |
| + } | |
| + | |
| + const std::string & error() const { | |
| + return error_; | |
| + } | |
| + | |
| + uint64_t event_count() const { | |
| + return event_sequence_; | |
| + } | |
| + | |
| + static bool callback(ggml_tensor * tensor, bool ask, void * user_data) noexcept { | |
| + auto * observer = static_cast<graph_observer *>(user_data); | |
| + try { | |
| + return observer->on_tensor(tensor, ask); | |
| + } catch (const std::exception & exception) { | |
| + observer->fail(std::string("observer exception: ") + exception.what()); | |
| + } catch (...) { | |
| + observer->fail("observer exception: unknown"); | |
| + } | |
| + return false; | |
| + } | |
| + | |
| +private: | |
| + observer_mode mode_; | |
| + int32_t n_layers_; | |
| + std::string phase_ = "uninitialized"; | |
| + int64_t phase_step_ = -1; | |
| + uint64_t event_sequence_ = 0; | |
| + bool failed_ = false; | |
| + std::string error_; | |
| + | |
| + // Kernel-level dispatch capture (Metal). Every compute dispatch is appended | |
| + // here by the backend hook; each GEMM node is then attributed to the actual | |
| + // matmul kernel dispatch that produced it (validated 1:1 on-device). | |
| + struct kdispatch { | |
| + std::string kernel; | |
| + int tg[3]; | |
| + int tptg[3]; | |
| + }; | |
| + std::vector<kdispatch> dispatches_; | |
| + size_t processed_dispatches_ = 0; | |
| + bool have_kernel_ = false; | |
| + kdispatch current_kernel_{}; | |
| + | |
| +public: | |
| + // Called by the ggml-metal dispatch hook for every compute dispatch. | |
| + void on_dispatch(const char * kernel, int tg0, int tg1, int tg2, int tptg0, int tptg1, int tptg2) { | |
| + kdispatch d; | |
| + d.kernel = kernel ? kernel : ""; | |
| + d.tg[0] = tg0; d.tg[1] = tg1; d.tg[2] = tg2; | |
| + d.tptg[0] = tptg0; d.tptg[1] = tptg1; d.tptg[2] = tptg2; | |
| + dispatches_.push_back(std::move(d)); | |
| + if (g_diag_enabled) { | |
| + std::fprintf(stderr, "PALW_DIAG dispatch #%zu pipeline=%s tg=%d,%d,%d tptg=%d,%d,%d\n", | |
| + dispatches_.size(), kernel ? kernel : "", tg0, tg1, tg2, tptg0, tptg1, tptg2); | |
| + } | |
| + } | |
| + | |
| + static void dispatch_hook(void * user_data, const char * pipeline, | |
| + int tg0, int tg1, int tg2, int tptg0, int tptg1, int tptg2) { | |
| + static_cast<graph_observer *>(user_data)->on_dispatch( | |
| + pipeline, tg0, tg1, tg2, tptg0, tptg1, tptg2); | |
| + } | |
| + | |
| +private: | |
| + // Attributes the just-computed GEMM node to the last matmul (non-expert-id) | |
| + // dispatch in its ASK->POST window. Expert GEMMs use distinct `*_id_*` | |
| + // kernels and are excluded. Returns false (fail-closed) if none is found. | |
| + bool resolve_gemm_kernel() { | |
| + have_kernel_ = false; | |
| + for (size_t i = dispatches_.size(); i > processed_dispatches_; --i) { | |
| + const std::string & k = dispatches_[i - 1].kernel; | |
| + const bool is_matmul = | |
| + (k.find("mul_mv") != std::string::npos || k.find("mul_mm") != std::string::npos) && | |
| + k.find("_id") == std::string::npos; | |
| + if (is_matmul) { | |
| + current_kernel_ = dispatches_[i - 1]; | |
| + have_kernel_ = true; | |
| + break; | |
| + } | |
| + } | |
| + processed_dispatches_ = dispatches_.size(); | |
| + if (!have_kernel_) { | |
| + fail("no matmul kernel dispatch found for GEMM node"); | |
| + return false; | |
| + } | |
| + return true; | |
| + } | |
| + | |
| + void fail(std::string message) { | |
| + if (!failed_) { | |
| + failed_ = true; | |
| + error_ = std::move(message); | |
| + std::fprintf(stderr, "observer error: %s\n", error_.c_str()); | |
| + } | |
| + } | |
| + | |
| + bool validate_tensor(const ggml_tensor * tensor, bool validate_op) { | |
| + if (!tensor) { | |
| + fail("null graph tensor"); | |
| + return false; | |
| + } | |
| + if (bounded_name_length(tensor->name) == GGML_MAX_NAME) { | |
| + fail("graph tensor name is not terminated"); | |
| + return false; | |
| + } | |
| + const int type = static_cast<int>(tensor->type); | |
| + if (type < 0 || type >= static_cast<int>(GGML_TYPE_COUNT)) { | |
| + fail("graph tensor type is outside public ggml bounds"); | |
| + return false; | |
| + } | |
| + if (validate_op) { | |
| + const int op = static_cast<int>(tensor->op); | |
| + if (op < 0 || op >= static_cast<int>(GGML_OP_COUNT)) { | |
| + fail("graph op is outside public ggml bounds"); | |
| + return false; | |
| + } | |
| + } | |
| + | |
| + // An empty (zero-element) tensor is valid graph metadata for recurrent | |
| + // state gathers and carries no storage to validate. | |
| + if (is_empty(tensor)) { | |
| + return true; | |
| + } | |
| + | |
| + uint64_t elements = 1; | |
| + for (int i = 0; i < GGML_MAX_DIMS; ++i) { | |
| + const uint64_t dimension = static_cast<uint64_t>(tensor->ne[i]); | |
| + if (elements > static_cast<uint64_t>(std::numeric_limits<int64_t>::max()) / dimension) { | |
| + fail("graph tensor element count overflows int64"); | |
| + return false; | |
| + } | |
| + elements *= dimension; | |
| + } | |
| + if (elements != static_cast<uint64_t>(ggml_nelements(tensor)) || ggml_nbytes(tensor) == 0) { | |
| + fail("graph tensor storage bounds are inconsistent"); | |
| + return false; | |
| + } | |
| + return true; | |
| + } | |
| + | |
| + bool validate_node(const ggml_tensor * tensor) { | |
| + if (!validate_tensor(tensor, true)) { | |
| + return false; | |
| + } | |
| + for (int i = 0; i < GGML_MAX_SRC; ++i) { | |
| + if (tensor->src[i] && !validate_tensor(tensor->src[i], false)) { | |
| + return false; | |
| + } | |
| + } | |
| + const int layer = tensor_layer(tensor); | |
| + if (layer >= n_layers_) { | |
| + fail("graph layer id exceeds model layer count"); | |
| + return false; | |
| + } | |
| + if (is_gemm(tensor)) { | |
| + if (!tensor->src[0] || !tensor->src[1]) { | |
| + fail("GEMM node is missing an input"); | |
| + return false; | |
| + } | |
| + if (tensor->src[0]->ne[0] != tensor->src[1]->ne[0] || | |
| + tensor->ne[0] != tensor->src[0]->ne[1] || | |
| + tensor->ne[1] != tensor->src[1]->ne[1]) { | |
| + fail("GEMM dimensions are inconsistent"); | |
| + return false; | |
| + } | |
| + } | |
| + return true; | |
| + } | |
| + | |
| + // Only the plain dense GEMM is sketched. Indirect expert GEMM | |
| + // (GGML_OP_MUL_MAT_ID) is emitted as an ordinary metadata node; its | |
| + // batched, id-indexed output layout is committed by the adapter as a | |
| + // generic compute operation rather than an accumulator sketch. | |
| + static bool is_gemm(const ggml_tensor * tensor) { | |
| + return tensor->op == GGML_OP_MUL_MAT; | |
| + } | |
| + | |
| + static bool is_empty(const ggml_tensor * tensor) { | |
| + for (int i = 0; i < GGML_MAX_DIMS; ++i) { | |
| + if (tensor->ne[i] <= 0) { | |
| + return true; | |
| + } | |
| + } | |
| + return false; | |
| + } | |
| + | |
| + // The mixture-of-experts Top-K selection tensor: the concrete list of expert | |
| + // indices chosen per token. Captured post-compute so the receipt commits the | |
| + // real routing, not merely that routing of some shape occurred. Matched by | |
| + // the llama.cpp `cb` label and the I32 index type. | |
| + static bool is_route_tensor(const ggml_tensor * tensor) { | |
| + return tensor->type == GGML_TYPE_I32 && | |
| + has_text(lowercase(tensor_name(tensor)), "ffn_moe_topk"); | |
| + } | |
| + | |
| + std::vector<std::string> categories(const ggml_tensor * tensor) const { | |
| + std::vector<std::string> result; | |
| + if (is_gemm(tensor)) { | |
| + result.emplace_back("gemm"); | |
| + } | |
| + if (tensor->op == GGML_OP_NORM || tensor->op == GGML_OP_RMS_NORM || | |
| + tensor->op == GGML_OP_GROUP_NORM || tensor->op == GGML_OP_L2_NORM) { | |
| + result.emplace_back("norm"); | |
| + } | |
| + if (tensor->op == GGML_OP_ROPE || tensor->op == GGML_OP_ROPE_BACK) { | |
| + result.emplace_back("rope"); | |
| + } | |
| + if (tensor->op == GGML_OP_FLASH_ATTN_EXT || tensor->op == GGML_OP_FLASH_ATTN_BACK || | |
| + related_names_contain(tensor, "attn") || related_names_contain(tensor, "qcur") || | |
| + related_names_contain(tensor, "kcur") || related_names_contain(tensor, "vcur") || | |
| + related_names_contain(tensor, "kq")) { | |
| + result.emplace_back("attention"); | |
| + } | |
| + if (related_names_contain(tensor, "cache_") || related_names_contain(tensor, "_cache")) { | |
| + result.emplace_back("kv_cache"); | |
| + } | |
| + if (result.empty()) { | |
| + result.emplace_back("other"); | |
| + } | |
| + return result; | |
| + } | |
| + | |
| + static int magnitude_bucket(float value) { | |
| + const float magnitude = std::fabs(value); | |
| + if (magnitude == 0.0f) return 0; | |
| + if (magnitude < 0.00390625f) return 1; | |
| + if (magnitude < 0.0625f) return 2; | |
| + if (magnitude < 0.25f) return 3; | |
| + if (magnitude < 1.0f) return 4; | |
| + if (magnitude < 4.0f) return 5; | |
| + if (magnitude < 16.0f) return 6; | |
| + return 7; | |
| + } | |
| + | |
| + bool make_sketch(const ggml_tensor * tensor, std::string & sketch, size_t & copied_bytes, size_t & sampled) { | |
| + if (!tensor->buffer || !ggml_is_contiguous(tensor)) { | |
| + fail("GEMM output is not backed by a contiguous public backend tensor"); | |
| + return false; | |
| + } | |
| + | |
| + size_t element_size = 0; | |
| + switch (tensor->type) { | |
| + case GGML_TYPE_F32: element_size = sizeof(float); break; | |
| + case GGML_TYPE_F16: element_size = sizeof(ggml_fp16_t); break; | |
| + case GGML_TYPE_BF16: element_size = sizeof(ggml_bf16_t); break; | |
| + default: | |
| + fail("GEMM output type is unsupported by sign/bucket sketch v1"); | |
| + return false; | |
| + } | |
| + | |
| + const uint64_t n_elements = static_cast<uint64_t>(ggml_nelements(tensor)); | |
| + sampled = static_cast<size_t>(std::min<uint64_t>(SKETCH_SAMPLES, n_elements)); | |
| + if (sampled > std::numeric_limits<size_t>::max() / element_size) { | |
| + fail("GEMM sketch copy bound overflows size_t"); | |
| + return false; | |
| + } | |
| + copied_bytes = sampled * element_size; | |
| + if (copied_bytes > ggml_nbytes(tensor)) { | |
| + fail("GEMM sketch copy exceeds tensor storage"); | |
| + return false; | |
| + } | |
| + | |
| + std::vector<uint8_t> host(copied_bytes); | |
| + ggml_backend_tensor_get(tensor, host.data(), 0, copied_bytes); | |
| + | |
| + static constexpr char hex[] = "0123456789abcdef"; | |
| + sketch.assign(SKETCH_SAMPLES, '0'); | |
| + for (size_t i = 0; i < sampled; ++i) { | |
| + float value = 0.0f; | |
| + if (tensor->type == GGML_TYPE_F32) { | |
| + std::memcpy(&value, host.data() + i * element_size, sizeof(value)); | |
| + } else if (tensor->type == GGML_TYPE_F16) { | |
| + ggml_fp16_t packed; | |
| + std::memcpy(&packed, host.data() + i * element_size, sizeof(packed)); | |
| + value = ggml_fp16_to_fp32(packed); | |
| + } else { | |
| + ggml_bf16_t packed; | |
| + std::memcpy(&packed, host.data() + i * element_size, sizeof(packed)); | |
| + value = ggml_bf16_to_fp32(packed); | |
| + } | |
| + if (!std::isfinite(value)) { | |
| + fail("GEMM sketch encountered a non-finite output"); | |
| + return false; | |
| + } | |
| + const unsigned nibble = (std::signbit(value) ? 8u : 0u) | | |
| + static_cast<unsigned>(magnitude_bucket(value)); | |
| + sketch[i] = hex[nibble]; | |
| + } | |
| + return true; | |
| + } | |
| + | |
| + void append_tensor_metadata(std::string & out, const ggml_tensor * tensor) const { | |
| + out += "\"name\":"; | |
| + append_json_string(out, tensor_name(tensor)); | |
| + out += ",\"op\":"; | |
| + append_json_string(out, ggml_op_name(tensor->op)); | |
| + out += ",\"type\":"; | |
| + append_json_string(out, ggml_type_name(tensor->type)); | |
| + out += ",\"shape\":"; | |
| + append_shape(out, tensor); | |
| + out += ",\"n_bytes\":"; | |
| + append_integer(out, ggml_nbytes(tensor)); | |
| + } | |
| + | |
| + bool emit_event(const ggml_tensor * tensor, const char * stage, | |
| + const std::string * sketch, size_t copied_bytes, size_t sampled) { | |
| + std::string line; | |
| + line.reserve(2048); | |
| + append_record_prefix(line, "event"); | |
| + line += ",\"event_seq\":"; | |
| + append_integer(line, event_sequence_++); | |
| + line += ",\"observer\":"; | |
| + append_json_string(line, mode_name(mode_)); | |
| + line += ",\"phase\":"; | |
| + append_json_string(line, phase_); | |
| + line += ",\"phase_step\":"; | |
| + append_integer(line, phase_step_); | |
| + line += ",\"stage\":"; | |
| + append_json_string(line, stage); | |
| + | |
| + const int layer = tensor_layer(tensor); | |
| + line += ",\"layer\":"; | |
| + if (layer < 0) { | |
| + line += "null"; | |
| + } else { | |
| + append_integer(line, layer); | |
| + } | |
| + | |
| + line += ",\"categories\":["; | |
| + const auto node_categories = categories(tensor); | |
| + for (size_t i = 0; i < node_categories.size(); ++i) { | |
| + if (i != 0) { | |
| + line.push_back(','); | |
| + } | |
| + append_json_string(line, node_categories[i]); | |
| + } | |
| + line += "],\"tensor\":{"; | |
| + append_tensor_metadata(line, tensor); | |
| + line += "},\"sources\":["; | |
| + bool first_source = true; | |
| + for (int i = 0; i < GGML_MAX_SRC; ++i) { | |
| + if (!tensor->src[i]) { | |
| + continue; | |
| + } | |
| + if (!first_source) { | |
| + line.push_back(','); | |
| + } | |
| + first_source = false; | |
| + line.push_back('{'); | |
| + append_tensor_metadata(line, tensor->src[i]); | |
| + line.push_back('}'); | |
| + } | |
| + line.push_back(']'); | |
| + | |
| + if (is_gemm(tensor)) { | |
| + line += ",\"gemm\":{\"variant\":\"ggml_graph_op_v1\",\"m\":"; | |
| + append_integer(line, tensor->src[0]->ne[1]); | |
| + line += ",\"n\":"; | |
| + append_integer(line, tensor->src[1]->ne[1]); | |
| + line += ",\"k\":"; | |
| + append_integer(line, tensor->src[0]->ne[0]); | |
| + line += ",\"batch_shape\":["; | |
| + append_integer(line, tensor->ne[2]); | |
| + line.push_back(','); | |
| + append_integer(line, tensor->ne[3]); | |
| + line += "]}"; | |
| + } | |
| + | |
| + if (sketch) { | |
| + line += ",\"sketch\":{\"version\":\"sign_bucket_256_v1\",\"bits\":256,"; | |
| + line += "\"encoding\":\"hex\",\"probe\":\"contiguous_prefix_64_v1\","; | |
| + line += "\"sample_count\":"; | |
| + append_integer(line, sampled); | |
| + line += ",\"copied_bytes\":"; | |
| + append_integer(line, copied_bytes); | |
| + line += ",\"value\":"; | |
| + append_json_string(line, *sketch); | |
| + line += "},\"tile\":{\"variant\":\"graph_fallback_logical_prefix_v1\","; | |
| + line += "\"linear_offset\":0,\"linear_elements\":"; | |
| + append_integer(line, sampled); | |
| + if (have_kernel_) { | |
| + // Kernel-level binding: the actual Metal compute pipeline (kernel) | |
| + // and its launch geometry that produced this GEMM output. This is | |
| + // launch-geometry + output-sketch bound to a real GPU dispatch, | |
| + // not a CUDA-style intra-kernel accumulator sketch. | |
| + line += "},\"kernel_trace\":{\"available\":true,\"backend\":\"metal\","; | |
| + line += "\"claim\":\"metal_kernel_launch_bound_v1\",\"kernel\":"; | |
| + append_json_string(line, current_kernel_.kernel); | |
| + line += ",\"threadgroups\":["; | |
| + append_integer(line, current_kernel_.tg[0]); | |
| + line.push_back(','); | |
| + append_integer(line, current_kernel_.tg[1]); | |
| + line.push_back(','); | |
| + append_integer(line, current_kernel_.tg[2]); | |
| + line += "],\"threads_per_threadgroup\":["; | |
| + append_integer(line, current_kernel_.tptg[0]); | |
| + line.push_back(','); | |
| + append_integer(line, current_kernel_.tptg[1]); | |
| + line.push_back(','); | |
| + append_integer(line, current_kernel_.tptg[2]); | |
| + line += "]}"; | |
| + } else { | |
| + line += "},\"kernel_trace\":{\"available\":false,\"backend\":\"none\","; | |
| + line += "\"claim\":\"not_a_cuda_kernel_trace\"}"; | |
| + } | |
| + } | |
| + | |
| + line.push_back('}'); | |
| + if (!write_json_line(line)) { | |
| + fail("stdout JSONL write failed"); | |
| + return false; | |
| + } | |
| + return true; | |
| + } | |
| + | |
| + // Reads the mixture-of-experts Top-K selection tensor back from the compute | |
| + // backend and emits a "route" record carrying the real per-token selected | |
| + // expert indices. ne[0] is the number of experts selected per token (Top-K); | |
| + // the remaining dimensions are the token count. | |
| + bool emit_route(const ggml_tensor * tensor) { | |
| + if (!tensor->buffer) { | |
| + fail("route tensor has no backend buffer"); | |
| + return false; | |
| + } | |
| + const uint64_t experts_used = static_cast<uint64_t>(tensor->ne[0]); | |
| + const uint64_t tokens = static_cast<uint64_t>(tensor->ne[1]) * | |
| + static_cast<uint64_t>(tensor->ne[2]) * static_cast<uint64_t>(tensor->ne[3]); | |
| + const uint64_t total = experts_used * tokens; | |
| + if (experts_used == 0 || tokens == 0 || total > MAX_ROUTE_INDICES) { | |
| + fail("route tensor shape is out of bounds"); | |
| + return false; | |
| + } | |
| + | |
| + // Read each selected index using the tensor's byte strides, so a strided | |
| + // Top-K view of the argsort output is captured correctly. | |
| + std::vector<int32_t> indices; | |
| + indices.reserve(static_cast<size_t>(total)); | |
| + for (uint64_t t = 0; t < tokens; ++t) { | |
| + for (uint64_t e = 0; e < experts_used; ++e) { | |
| + const size_t offset = static_cast<size_t>(e) * tensor->nb[0] + | |
| + static_cast<size_t>(t) * tensor->nb[1]; | |
| + if (offset + sizeof(int32_t) > ggml_nbytes(tensor)) { | |
| + fail("route tensor read exceeds tensor storage"); | |
| + return false; | |
| + } | |
| + int32_t value = 0; | |
| + ggml_backend_tensor_get(tensor, &value, offset, sizeof(int32_t)); | |
| + indices.push_back(value); | |
| + } | |
| + } | |
| + | |
| + std::string line; | |
| + line.reserve(1024); | |
| + append_record_prefix(line, "route"); | |
| + line += ",\"event_seq\":"; | |
| + append_integer(line, event_sequence_++); | |
| + line += ",\"observer\":"; | |
| + append_json_string(line, mode_name(mode_)); | |
| + line += ",\"phase\":"; | |
| + append_json_string(line, phase_); | |
| + line += ",\"phase_step\":"; | |
| + append_integer(line, phase_step_); | |
| + const int layer = tensor_layer(tensor); | |
| + line += ",\"layer\":"; | |
| + if (layer < 0) { | |
| + line += "null"; | |
| + } else { | |
| + append_integer(line, layer); | |
| + } | |
| + line += ",\"experts_used\":"; | |
| + append_integer(line, experts_used); | |
| + line += ",\"tokens\":"; | |
| + append_integer(line, tokens); | |
| + line += ",\"selected_experts\":["; | |
| + for (size_t i = 0; i < indices.size(); ++i) { | |
| + if (i != 0) { | |
| + line.push_back(','); | |
| + } | |
| + append_integer(line, indices[i]); | |
| + } | |
| + line += "]}"; | |
| + if (!write_json_line(line)) { | |
| + fail("stdout JSONL write failed"); | |
| + return false; | |
| + } | |
| + return true; | |
| + } | |
| + | |
| + bool on_tensor(ggml_tensor * tensor, bool ask) { | |
| + if (failed_ || mode_ == observer_mode::off) { | |
| + return false; | |
| + } | |
| + // Recurrent (state-space / gated-delta-net) layers gather an initially | |
| + // empty state cache, producing legitimate zero-element graph nodes. They | |
| + // perform no compute, so they are skipped rather than treated as errors. | |
| + if (tensor && is_empty(tensor)) { | |
| + return false; | |
| + } | |
| + if (!validate_node(tensor)) { | |
| + return false; | |
| + } | |
| + | |
| + if (g_diag_enabled && is_gemm(tensor)) { | |
| + std::fprintf(stderr, "PALW_DIAG %s MUL_MAT node=%.*s layer=%d dispatch#=%zu\n", | |
| + ask ? "ASK " : "POST", (int) tensor_name(tensor).size(), tensor_name(tensor).data(), | |
| + tensor_layer(tensor), dispatches_.size()); | |
| + } | |
| + | |
| + if (ask) { | |
| + if (mode_ == observer_mode::graph) { | |
| + emit_event(tensor, "ask_metadata", nullptr, 0, 0); | |
| + return false; | |
| + } | |
| + if (is_gemm(tensor)) { | |
| + return true; | |
| + } | |
| + // Sketch mode also reads back the mixture-of-experts Top-K selection | |
| + // so the receipt commits the real routing content. | |
| + if (is_route_tensor(tensor)) { | |
| + return true; | |
| + } | |
| + emit_event(tensor, "ask_metadata", nullptr, 0, 0); | |
| + return false; | |
| + } | |
| + | |
| + if (mode_ != observer_mode::sketch) { | |
| + fail("unexpected post-compute callback"); | |
| + return false; | |
| + } | |
| + if (is_route_tensor(tensor)) { | |
| + return emit_route(tensor); | |
| + } | |
| + if (!is_gemm(tensor)) { | |
| + fail("unexpected post-compute callback"); | |
| + return false; | |
| + } | |
| + | |
| + // Bind this GEMM to the actual Metal matmul kernel dispatch that produced | |
| + // it before sketching its output. | |
| + if (!resolve_gemm_kernel()) { | |
| + return false; | |
| + } | |
| + std::string sketch; | |
| + size_t copied_bytes = 0; | |
| + size_t sampled = 0; | |
| + if (!make_sketch(tensor, sketch, copied_bytes, sampled)) { | |
| + return false; | |
| + } | |
| + return emit_event(tensor, "post_compute_sketch", &sketch, copied_bytes, sampled); | |
| + } | |
| +}; | |
| + | |
| +static bool get_model_string(const llama_model * model, int32_t index, bool key, std::string & value) { | |
| + std::vector<char> buffer(256); | |
| + for (int attempt = 0; attempt < 3; ++attempt) { | |
| + const int32_t length = key | |
| + ? llama_model_meta_key_by_index(model, index, buffer.data(), buffer.size()) | |
| + : llama_model_meta_val_str_by_index(model, index, buffer.data(), buffer.size()); | |
| + if (length < 0) { | |
| + return false; | |
| + } | |
| + if (static_cast<size_t>(length) < buffer.size()) { | |
| + value.assign(buffer.data(), static_cast<size_t>(length)); | |
| + return true; | |
| + } | |
| + buffer.resize(static_cast<size_t>(length) + 1); | |
| + } | |
| + return false; | |
| +} | |
| + | |
| +static bool read_model_metadata(const llama_model * model, model_metadata & metadata) { | |
| + const int32_t count = llama_model_meta_count(model); | |
| + if (count < 0 || count > 100000) { | |
| + std::fprintf(stderr, "error: model metadata count is outside bounds\n"); | |
| + return false; | |
| + } | |
| + metadata.entries.reserve(static_cast<size_t>(count)); | |
| + for (int32_t i = 0; i < count; ++i) { | |
| + std::string key; | |
| + std::string value; | |
| + if (!get_model_string(model, i, true, key) || !get_model_string(model, i, false, value)) { | |
| + std::fprintf(stderr, "error: failed to read model metadata index %d\n", i); | |
| + return false; | |
| + } | |
| + metadata.entries.emplace_back(std::move(key), std::move(value)); | |
| + } | |
| + return true; | |
| +} | |
| + | |
| +static const std::string * find_metadata(const model_metadata & metadata, const char * key) { | |
| + for (const auto & entry : metadata.entries) { | |
| + if (entry.first == key) { | |
| + return &entry.second; | |
| + } | |
| + } | |
| + return nullptr; | |
| +} | |
| + | |
| +static bool validate_qwen36_35b_a3b_profile(const llama_model * model, const model_metadata & metadata) { | |
| + const std::string * architecture = find_metadata(metadata, "general.architecture"); | |
| + const llama_vocab * vocab = llama_model_get_vocab(model); | |
| + if (!architecture || *architecture != "qwen35moe" || | |
| + llama_model_n_layer(model) != 40 || | |
| + llama_model_n_embd(model) != 2048 || | |
| + llama_vocab_n_tokens(vocab) != 248320 || | |
| + llama_model_has_encoder(model) || !llama_model_has_decoder(model)) { | |
| + std::fprintf(stderr, | |
| + "error: model is not the supported Qwen3.6-35B-A3B MoE profile " | |
| + "(qwen35moe, 40 layers, 2048 hidden, 248320 vocab); observed " | |
| + "arch=%s layers=%d embd=%d head=%d head_kv=%d vocab=%d\n", | |
| + architecture ? architecture->c_str() : "(none)", | |
| + llama_model_n_layer(model), | |
| + llama_model_n_embd(model), | |
| + llama_model_n_head(model), | |
| + llama_model_n_head_kv(model), | |
| + llama_vocab_n_tokens(vocab)); | |
| + return false; | |
| + } | |
| + return true; | |
| +} | |
| + | |
| +static const char * device_type_name(enum ggml_backend_dev_type type) { | |
| + switch (type) { | |
| + case GGML_BACKEND_DEVICE_TYPE_CPU: return "cpu"; | |
| + case GGML_BACKEND_DEVICE_TYPE_GPU: return "gpu"; | |
| + case GGML_BACKEND_DEVICE_TYPE_IGPU: return "igpu"; | |
| + case GGML_BACKEND_DEVICE_TYPE_ACCEL: return "accelerator"; | |
| + case GGML_BACKEND_DEVICE_TYPE_META: return "meta"; | |
| + } | |
| + return "unknown"; | |
| +} | |
| + | |
| +static bool emit_header(const options & opts, const llama_model * model, const llama_context * context, | |
| + const model_metadata & metadata, size_t prompt_tokens) { | |
| + char description[1024] = {}; | |
| + const int32_t description_length = llama_model_desc(model, description, sizeof(description)); | |
| + if (description_length < 0 || static_cast<size_t>(description_length) >= sizeof(description)) { | |
| + std::fprintf(stderr, "error: model description exceeds header bound\n"); | |
| + return false; | |
| + } | |
| + | |
| + std::string line; | |
| + line.reserve(32768); | |
| + append_record_prefix(line, "header"); | |
| + line += ",\"observer\":"; | |
| + append_json_string(line, mode_name(opts.mode)); | |
| + line += ",\"trace_variant\":"; | |
| + if (opts.mode == observer_mode::off) { | |
| + append_json_string(line, "none"); | |
| + } else if (opts.mode == observer_mode::graph) { | |
| + append_json_string(line, "ggml_sched_ask_metadata_v1"); | |
| + } else { | |
| + append_json_string(line, "ggml_sched_fixed_prefix_sketch_v1"); | |
| + } | |
| + line += ",\"cuda_kernel_trace\":false"; | |
| + | |
| + line += ",\"model\":{\"path\":"; | |
| + append_json_string(line, opts.model_path); | |
| + line += ",\"description\":"; | |
| + append_json_string(line, description); | |
| + line += ",\"tensor_size_bytes\":"; | |
| + append_integer(line, llama_model_size(model)); | |
| + line += ",\"parameter_count\":"; | |
| + append_integer(line, llama_model_n_params(model)); | |
| + line += ",\"file_type\":"; | |
| + append_integer(line, static_cast<int>(llama_model_ftype(model))); | |
| + line += ",\"n_ctx_train\":"; | |
| + append_integer(line, llama_model_n_ctx_train(model)); | |
| + line += ",\"n_embd\":"; | |
| + append_integer(line, llama_model_n_embd(model)); | |
| + line += ",\"n_layer\":"; | |
| + append_integer(line, llama_model_n_layer(model)); | |
| + line += ",\"n_head\":"; | |
| + append_integer(line, llama_model_n_head(model)); | |
| + line += ",\"n_head_kv\":"; | |
| + append_integer(line, llama_model_n_head_kv(model)); | |
| + line += ",\"n_vocab\":"; | |
| + append_integer(line, llama_vocab_n_tokens(llama_model_get_vocab(model))); | |
| + line += ",\"rope_type\":"; | |
| + append_integer(line, static_cast<int>(llama_model_rope_type(model))); | |
| + line += ",\"rope_freq_scale_train\":"; | |
| + append_float(line, llama_model_rope_freq_scale_train(model)); | |
| + line += ",\"metadata\":["; | |
| + for (size_t i = 0; i < metadata.entries.size(); ++i) { | |
| + if (i != 0) { | |
| + line.push_back(','); | |
| + } | |
| + line += "{\"key\":"; | |
| + append_json_string(line, metadata.entries[i].first); | |
| + line += ",\"value\":"; | |
| + append_json_string(line, metadata.entries[i].second); | |
| + line.push_back('}'); | |
| + } | |
| + line += "]}"; | |
| + | |
| + line += ",\"runtime\":{\"ggml_version\":"; | |
| + append_json_string(line, ggml_version()); | |
| + line += ",\"ggml_commit\":"; | |
| + append_json_string(line, ggml_commit()); | |
| + line += ",\"system_info\":"; | |
| + append_json_string(line, llama_print_system_info()); | |
| + line += ",\"supports_gpu_offload\":"; | |
| + append_bool(line, llama_supports_gpu_offload()); | |
| + line += ",\"requested_gpu_layers\":"; | |
| + append_integer(line, opts.n_gpu_layers); | |
| + line += ",\"devices\":["; | |
| + const size_t device_count = ggml_backend_dev_count(); | |
| + for (size_t i = 0; i < device_count; ++i) { | |
| + if (i != 0) { | |
| + line.push_back(','); | |
| + } | |
| + const ggml_backend_dev_t device = ggml_backend_dev_get(i); | |
| + ggml_backend_dev_props properties = {}; | |
| + ggml_backend_dev_get_props(device, &properties); | |
| + line += "{\"name\":"; | |
| + append_json_string(line, properties.name ? properties.name : ""); | |
| + line += ",\"description\":"; | |
| + append_json_string(line, properties.description ? properties.description : ""); | |
| + line += ",\"type\":"; | |
| + append_json_string(line, device_type_name(properties.type)); | |
| + line += ",\"memory_free_observed\":"; | |
| + append_integer(line, properties.memory_free); | |
| + line += ",\"memory_total\":"; | |
| + append_integer(line, properties.memory_total); | |
| + line.push_back('}'); | |
| + } | |
| + line += "]}"; | |
| + | |
| + line += ",\"execution_policy\":{\"sampling\":\"greedy\",\"temperature\":0,"; | |
| + line += "\"top_p\":1,\"top_k\":0,\"batch\":1,\"request_batch\":1,\"n_batch\":"; | |
| + append_integer(line, llama_n_batch(context)); | |
| + line += ",\"n_ubatch\":"; | |
| + append_integer(line, llama_n_ubatch(context)); | |
| + line += ",\"parallel\":1,\"parallel_sequences\":"; | |
| + append_integer(line, llama_n_seq_max(context)); | |
| + line += ",\"tensor_parallel\":1,\"split_mode\":\"none\",\"scheduler_parallel\":false,"; | |
| + line += "\"tensor_repack\":false,"; | |
| + line += "\"context_shift\":false,\"speculation\":false,\"flash_attention\":false,"; | |
| + line += "\"threads\":1,\"threads_batch\":1,\"n_predict\":"; | |
| + append_integer(line, opts.n_predict); | |
| + line += ",\"n_ctx\":"; | |
| + append_integer(line, llama_n_ctx(context)); | |
| + line += ",\"prompt_tokens\":"; | |
| + append_integer(line, prompt_tokens); | |
| + line += ",\"tokenization\":{\"add_special\":true,\"parse_special\":true}}"; | |
| + | |
| + line += ",\"observation_policy\":{\"read_only\":true,"; | |
| + line += "\"graph_metadata_stage\":\"ask\",\"sketch_bits\":256,"; | |
| + line += "\"sketch_probe\":\"gemm_output_contiguous_prefix_64\","; | |
| + line += "\"raw_activation_values_published\":false,"; | |
| + line += "\"tile_variant\":\"graph_fallback_logical_prefix_v1\","; | |
| + line += "\"kernel_trace_claim\":\"none\"}"; | |
| + line.push_back('}'); | |
| + return write_json_line(line); | |
| +} | |
| + | |
| +static bool tokenize_prompt(const llama_vocab * vocab, const std::string & prompt, | |
| + std::vector<llama_token> & tokens) { | |
| + const int32_t length = static_cast<int32_t>(prompt.size()); | |
| + const int32_t required = llama_tokenize(vocab, prompt.data(), length, nullptr, 0, true, true); | |
| + if (required == std::numeric_limits<int32_t>::min()) { | |
| + std::fprintf(stderr, "error: tokenizer result overflow\n"); | |
| + return false; | |
| + } | |
| + const int32_t count = required < 0 ? -required : required; | |
| + if (count <= 0) { | |
| + std::fprintf(stderr, "error: prompt tokenization produced no tokens\n"); | |
| + return false; | |
| + } | |
| + tokens.resize(static_cast<size_t>(count)); | |
| + const int32_t actual = llama_tokenize(vocab, prompt.data(), length, | |
| + tokens.data(), count, true, true); | |
| + if (actual != count) { | |
| + std::fprintf(stderr, "error: prompt tokenization was not stable across sizing calls\n"); | |
| + return false; | |
| + } | |
| + return true; | |
| +} | |
| + | |
| +static bool append_token_piece(const llama_vocab * vocab, llama_token token, | |
| + std::vector<uint8_t> & output) { | |
| + std::array<char, 64> small = {}; | |
| + int32_t length = llama_token_to_piece(vocab, token, small.data(), small.size(), 0, false); | |
| + if (length >= 0) { | |
| + output.insert(output.end(), small.begin(), small.begin() + length); | |
| + return true; | |
| + } | |
| + if (length == std::numeric_limits<int32_t>::min()) { | |
| + return false; | |
| + } | |
| + const int32_t required = -length; | |
| + std::vector<char> buffer(static_cast<size_t>(required)); | |
| + length = llama_token_to_piece(vocab, token, buffer.data(), required, 0, false); | |
| + if (length != required) { | |
| + return false; | |
| + } | |
| + output.insert(output.end(), buffer.begin(), buffer.end()); | |
| + return true; | |
| +} | |
| + | |
| +static bool emit_result(const options & opts, const char * status, const char * stop_reason, | |
| + const std::vector<llama_token> & prompt_tokens, | |
| + const std::vector<llama_token> & generated_tokens, | |
| + const std::vector<uint8_t> & output_bytes, | |
| + const graph_observer & observer, const std::string & error) { | |
| + std::string line; | |
| + line.reserve(1024 + prompt_tokens.size() * 12 + generated_tokens.size() * 12 + output_bytes.size() * 4); | |
| + append_record_prefix(line, "result"); | |
| + line += ",\"status\":"; | |
| + append_json_string(line, status); | |
| + line += ",\"observer\":"; | |
| + append_json_string(line, mode_name(opts.mode)); | |
| + line += ",\"stop_reason\":"; | |
| + append_json_string(line, stop_reason); | |
| + line += ",\"prompt_token_ids\":"; | |
| + append_token_ids(line, prompt_tokens); | |
| + line += ",\"generated_token_ids\":"; | |
| + append_token_ids(line, generated_tokens); | |
| + line += ",\"output_bytes\":["; | |
| + const size_t published_output_bytes = opts.emit_output_bytes ? output_bytes.size() : 0; | |
| + for (size_t i = 0; i < published_output_bytes; ++i) { | |
| + if (i != 0) { | |
| + line.push_back(','); | |
| + } | |
| + append_integer(line, static_cast<unsigned>(output_bytes[i])); | |
| + } | |
| + line += "],\"output_n_bytes\":"; | |
| + append_integer(line, published_output_bytes); | |
| + line += ",\"event_count\":"; | |
| + append_integer(line, observer.event_count()); | |
| + if (!error.empty()) { | |
| + line += ",\"error\":"; | |
| + append_json_string(line, error); | |
| + } | |
| + line.push_back('}'); | |
| + return write_json_line(line); | |
| +} | |
| + | |
| +struct backend_guard { | |
| + ~backend_guard() { | |
| + llama_backend_free(); | |
| + } | |
| +}; | |
| + | |
| +} // namespace | |
| + | |
| +int main(int argc, char ** argv) { | |
| + std::setlocale(LC_NUMERIC, "C"); | |
| + llama_log_set(log_callback, nullptr); | |
| + | |
| + options opts; | |
| + bool help = false; | |
| + bool version = false; | |
| + if (!parse_options(argc, argv, opts, help, version)) { | |
| + print_usage(argv[0]); | |
| + return 2; | |
| + } | |
| + if (version) { | |
| + std::fprintf(stderr, "version: %d (%s)\n", llama_build_number(), llama_commit()); | |
| + return 0; | |
| + } | |
| + if (help) { | |
| + print_usage(argv[0]); | |
| + return 0; | |
| + } | |
| + | |
| + llama_backend_init(); | |
| + backend_guard backend_cleanup; | |
| + ggml_backend_load_all(); | |
| + | |
| + llama_model_params model_params = llama_model_default_params(); | |
| + model_params.n_gpu_layers = opts.n_gpu_layers; | |
| + model_params.split_mode = LLAMA_SPLIT_MODE_NONE; | |
| + model_params.main_gpu = 0; | |
| + model_params.use_mmap = true; | |
| + model_params.use_mlock = false; | |
| + model_params.check_tensors = true; | |
| + model_params.use_extra_bufts = false; | |
| + | |
| + using model_ptr = std::unique_ptr<llama_model, decltype(&llama_model_free)>; | |
| + model_ptr model(llama_model_load_from_file(opts.model_path.c_str(), model_params), llama_model_free); | |
| + if (!model) { | |
| + std::fprintf(stderr, "error: failed to load model\n"); | |
| + return 3; | |
| + } | |
| + | |
| + model_metadata metadata; | |
| + if (!read_model_metadata(model.get(), metadata) || !validate_qwen36_35b_a3b_profile(model.get(), metadata)) { | |
| + return 3; | |
| + } | |
| + | |
| + const llama_vocab * vocab = llama_model_get_vocab(model.get()); | |
| + std::vector<llama_token> prompt_tokens; | |
| + if (!tokenize_prompt(vocab, opts.prompt, prompt_tokens)) { | |
| + return 3; | |
| + } | |
| + | |
| + const uint64_t required_context = prompt_tokens.size() + static_cast<uint64_t>(opts.n_predict); | |
| + if (required_context > PALW_CONTEXT_TOKENS || | |
| + PALW_CONTEXT_TOKENS > static_cast<uint64_t>(llama_model_n_ctx_train(model.get()))) { | |
| + std::fprintf(stderr, "error: prompt plus n-predict exceeds the fixed PALW context bound\n"); | |
| + return 3; | |
| + } | |
| + | |
| + graph_observer observer(opts.mode, llama_model_n_layer(model.get())); | |
| + llama_context_params context_params = llama_context_default_params(); | |
| + context_params.n_ctx = PALW_CONTEXT_TOKENS; | |
| + context_params.n_batch = 1; | |
| + context_params.n_ubatch = 1; | |
| + context_params.n_seq_max = 1; | |
| + context_params.n_outputs_max = 1; | |
| + context_params.n_threads = 1; | |
| + context_params.n_threads_batch = 1; | |
| + context_params.flash_attn_type = LLAMA_FLASH_ATTN_TYPE_DISABLED; | |
| + context_params.type_k = GGML_TYPE_F16; | |
| + context_params.type_v = GGML_TYPE_F16; | |
| + context_params.embeddings = false; | |
| + context_params.offload_kqv = true; | |
| + context_params.no_perf = true; | |
| + context_params.op_offload = true; | |
| + context_params.kv_unified = false; | |
| + if (opts.mode != observer_mode::off) { | |
| + context_params.cb_eval = graph_observer::callback; | |
| + context_params.cb_eval_user_data = &observer; | |
| + g_diag_enabled = std::getenv("PALW_TRACE_DIAG") != nullptr; | |
| + // Capture every Metal kernel dispatch so GEMMs can be bound to the actual | |
| + // kernel + launch geometry that produced them (kernel-level trace). | |
| + if (opts.mode == observer_mode::sketch) { | |
| + ggml_metal_palw_set_dispatch_hook(graph_observer::dispatch_hook, &observer); | |
| + } | |
| + } | |
| + | |
| + using context_ptr = std::unique_ptr<llama_context, decltype(&llama_free)>; | |
| + context_ptr context(llama_init_from_model(model.get(), context_params), llama_free); | |
| + if (!context) { | |
| + std::fprintf(stderr, "error: failed to initialize context\n"); | |
| + return 3; | |
| + } | |
| + if (llama_n_batch(context.get()) != 1 || llama_n_ubatch(context.get()) != 1 || | |
| + llama_n_seq_max(context.get()) != 1 || llama_n_ctx(context.get()) != PALW_CONTEXT_TOKENS || | |
| + llama_n_threads(context.get()) != 1 || llama_n_threads_batch(context.get()) != 1) { | |
| + std::fprintf(stderr, "error: runtime did not honor the deterministic execution policy\n"); | |
| + return 3; | |
| + } | |
| + | |
| + if (!emit_header(opts, model.get(), context.get(), metadata, prompt_tokens.size())) { | |
| + return 4; | |
| + } | |
| + | |
| + std::vector<llama_token> generated_tokens; | |
| + std::vector<uint8_t> output_bytes; | |
| + generated_tokens.reserve(static_cast<size_t>(opts.n_predict)); | |
| + | |
| + for (size_t i = 0; i < prompt_tokens.size(); ++i) { | |
| + observer.set_phase("prefill", static_cast<int64_t>(i)); | |
| + llama_token token = prompt_tokens[i]; | |
| + const int32_t decode_status = llama_decode(context.get(), llama_batch_get_one(&token, 1)); | |
| + if (decode_status != 0) { | |
| + const std::string error = "llama_decode prefill status " + std::to_string(decode_status); | |
| + emit_result(opts, "error", "decode_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, error); | |
| + return 5; | |
| + } | |
| + if (observer.failed()) { | |
| + emit_result(opts, "error", "observer_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, observer.error()); | |
| + return 5; | |
| + } | |
| + } | |
| + | |
| + using sampler_ptr = std::unique_ptr<llama_sampler, decltype(&llama_sampler_free)>; | |
| + sampler_ptr sampler(llama_sampler_init_greedy(), llama_sampler_free); | |
| + if (!sampler) { | |
| + emit_result(opts, "error", "sampler_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, "failed to initialize greedy sampler"); | |
| + return 5; | |
| + } | |
| + | |
| + const char * stop_reason = "n_predict"; | |
| + for (int32_t i = 0; i < opts.n_predict; ++i) { | |
| + const llama_token token = llama_sampler_sample(sampler.get(), context.get(), -1); | |
| + if (token < 0 || token >= llama_vocab_n_tokens(vocab)) { | |
| + emit_result(opts, "error", "sampler_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, "greedy sampler returned an out-of-range token"); | |
| + return 5; | |
| + } | |
| + generated_tokens.push_back(token); | |
| + if (llama_vocab_is_eog(vocab, token)) { | |
| + stop_reason = "eog"; | |
| + break; | |
| + } | |
| + if (!append_token_piece(vocab, token, output_bytes)) { | |
| + emit_result(opts, "error", "detokenize_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, "failed to convert generated token to bytes"); | |
| + return 5; | |
| + } | |
| + if (i + 1 == opts.n_predict) { | |
| + break; | |
| + } | |
| + | |
| + observer.set_phase("decode", i); | |
| + llama_token mutable_token = token; | |
| + const int32_t decode_status = llama_decode(context.get(), llama_batch_get_one(&mutable_token, 1)); | |
| + if (decode_status != 0) { | |
| + const std::string error = "llama_decode generation status " + std::to_string(decode_status); | |
| + emit_result(opts, "error", "decode_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, error); | |
| + return 5; | |
| + } | |
| + if (observer.failed()) { | |
| + emit_result(opts, "error", "observer_error", prompt_tokens, generated_tokens, | |
| + output_bytes, observer, observer.error()); | |
| + return 5; | |
| + } | |
| + } | |
| + | |
| + if (!emit_result(opts, "ok", stop_reason, prompt_tokens, generated_tokens, | |
| + output_bytes, observer, {})) { | |
| + return 4; | |
| + } | |
| + return 0; | |
| +} | |