pymc-devs / pymc-devs/pytensor-ml
Meta: native LLM inference runtime in pytensor_ml (tracking)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 9
- Forks
- 7
- Avg merge
- 6h 55m
- Merged PRs (30d)
- 40
Description
Goal
Make pytensor_ml able to load and run a local LLM end to end from Python — load → tokenize → prefill → cached decode → sample → stream — with one symbolic model definition compiling through C, Numba, JAX and MLX, and a persistent KV cache instead of prefix replay.
Reference point is llama.cpp's shape (immutable model, mutable context, batch/decode, sampler chain), not its throughput or model coverage. The differentiator stays composability: an inspectable graph inside the scientific Python stack.
Build on what already exists
This is the important constraint. The repo already has the pieces below, and this work extends them in place rather than adding a parallel stack:
| Already here | Consequence |
|---|---|
serialize_graph / deserialize_graph, type + op registries, OpFromGraph / SymbolicOp / Scan codecs |
No second codec. A new op serializes for free if it is a LayerOp with complete __props__; qualname becomes on-disk format |
save_network / load_network, InputKind, GRAPH_FORMAT_VERSION |
Architecture persistence already solved; schema changes bump the version |
from_pretrained(source_format="huggingface") → NotImplementedError |
This stub is the loading entry point (PR 10), not a new loader |
save_state / load_state(name_map=...) with key/shape/dtype validation |
HF renaming uses the existing name_map; only lazy reads, sharding and telemetry are missing |
StatefulOp.update_map + non_trainable + collect_non_trainable_updates + function(updates=...) |
This is the KV-cache mechanism — BatchNormLayer already proves it. No bespoke session-mutation layer |
AttentionLayer bottom-right causal alignment (k_idx <= q_idx + (sk - sq)) + _repeat_kv |
Decode-shaped masking and GQA are already correct and reused as-is |
predict_db / rewrite_for_prediction, dispatch/ meta-path hook, fused MLX/JAX attention |
Quantized kernels and new op lowering register through these |
Model.compile_train(loss=...) (#52) |
LM training objective already supported — this work is inference-only |
Explicitly not in scope because it already exists: a new archive format or reader, a weight-store abstraction, a model-directory layout, a second HF detection path, a rewrite pass manager, a backend-registration mechanism, a SiLU activation (Swish(beta=1)), and Scan serialization.
How to read the list
Each block below is one PR. Boxes inside a block land together; blocks land in listed order within a tier. F-nn are stable feature ids so a PR can say "closes F-19, F-20" without a second issue.
PR bundling rules:
- A format change (
GRAPH_FORMAT_VERSION,__props__of a serialized op) ships with its rejection test in the same PR — never split. - An op and the rewrite/lowering that selects it ship together; a kernel no compiled graph selects is not a capability.
- Correctness fixtures land before the thing they gate.
- Existing suites (
test_pretrained,test_checkpoint,test_serialize,test_attention,rewriting/,dispatch/) must keep passing untouched unless the PR is a deliberate, version-bumped contract change.
Tier A — native Python runtime (release blocker)
PR 1 — Runtime contracts (docs only)
-
F-63load_llm/LLM/Session/Engineroles, error taxonomy, result schema, plan-key schema, import direction - Record which existing modules are extended vs what is new; freeze
Modelandfrom_pretrainedsignatures
No code. Unblocks every other PR by fixing names and boundaries.
PR 2 — Conformance fixtures
-
F-60Tiny offline decoder fixture: pinned config, weights, tokenizer, prompt tokens, full-prefix logits, cached-step logits -
F-60Independent oracle that may not import production model/RoPE/mask/cache helpers; mutation tests that must fail on Q/K orientation, RoPE pairing, mask polarity, cache frontier
Reuses tests/test_serialize.py::assert_outputs_roundtrip and tests/test_attention.py conventions.
PR 3 — Missing decoder primitives
-
F-13RMSNormlayer +RMSNormLayerop beside the existingLayerNormLayer -
F-14RoPE layer/op with position inputs and scaling variants -
F-15Gated (SwiGLU) MLP besideFeedForward—Swish(beta=1)already provides SiLU -
F-16aTied embedding/unembedding sharing one parameter -
F-16bLM head with optional logit softcap and fp32-safe logits (sole owner of softcap) -
F-17Sliding-window/local masking added to the existingAttentionLayer -
F-18Llama-style block: pre-RMSNorm + GQA + gated MLP -
F-68GRAPH_FORMAT_VERSIONbump, stale-config rejection test,__props__payload migration, qualname-stability test — required because F-17 changes a serialized op
All new ops are LayerOps with complete __props__, so they serialize as leaves for free. Attention, GQA and decode-aligned causal masking already exist and are reused.
PR 4 — Artifact manifest, config, architecture registry
-
F-10Manifest: local-only resolution, hashes, sizes, dtype/shape/offset bounds, provenance -
F-09aParsemodel.safetensors.index.jsonand validate exact shard/tensor coverage (no tensor reads) -
F-06HuggingFaceconfig.json→ normalized decoder config -
F-07Architecture builder dispatch table keyed by the already-detectedmodel_type/architectures -
F-08aHF →pytensor_mlparameter-name map as data forload_state(name_map=...)
Extends _looks_like_huggingface/_detect_format; HF detection is not re-implemented.
PR 5 — Tokenizer and chat templates
-
F-01Encode/decode, batch, special tokens, vocab, stop-token metadata -
F-02Incremental streaming detokenizer with UTF-8/byte-fallback buffering -
F-03Chat-template rendering, generation prompt, prefill/continuation -
F-04HF tokenizer artifact adapter (tokenizer.json,tokenizer_config.json,chat_template.jinja)
Nothing tokenizer-related exists today. Chat template is the sole owner of BOS/EOS insertion.
PR 6 — Backend capabilities, compiled plans, resource accounting
-
F-28Capability declaration; unsupported model/backend/dtype combinations fail before token 1 -
F-26Compiled-plan cache keyed by architecture/backend/dtype/shape bucket, extendingcompile_predict -
F-27Shape-bucketed prefill and fixed-shape decode compilation -
F-69Plans are never serialized; keys recomputed over the pre-compilation graph. Test fails if a fusedCompositereaches the codec -
F-25Aggregate host/device byte accounting for weights, plan buffers, cache allocation -
F-29LLM entries registered into the existingpredict_db
Uses the existing pytensorf.function / compile_predict / predict_db / dispatch extension points.
PR 7 — Large-model weight residency
-
F-65Sole owner of variable→value binding and residency/copy telemetry, plus the tensor-handle contract -
F-24Lazy per-tensor byte materialization besideload_state's whole-archive read -
F-09bShard iteration for multi-file archives -
F-08bApply the HF name map viaload_state(name_map=...), reporting unmapped/unexpected keys
Extends checkpoint.py / pretrained.py. No new archive format, directory layout, or weight store. The safetensors NumPy path copies bytes, so no zero-copy claim.
PR 8 — KV cache and prefill/decode plans
-
F-67One cache-state interface (logical read/append/gather/mask) that dense and later paged storage both implement -
F-19KVCacheLayerop declaringupdate_map()— write-back rides the existingfunction(..., updates=...)path -
F-20Cache buffers as namednon_trainablestate with capacity and length counters -
F-21Per-stepposition_ids/cache_length/seq_id, produced once and consumed by RoPE, masking, append -
F-22Prefill and decode graph builders over one architecture definition -
F-23Exclude cache buffers from_weight_variables/save_pretrained -
F-71Cache updates surviverewrite_for_prediction; test fails if updates are dropped after rewriting
Mechanism already exists: StatefulOp.update_map + non_trainable + collect_non_trainable_updates, as BatchNormLayer demonstrates. Dense storage only; no wrap or slot reuse. Bounded append writes, stable compile count.
PR 9 — Llama-family reference architecture
-
F-47Llama architecture module and weight schema composed from PR 3 primitives
Emits distinct prefill/decode graphs; no generation, loader, or scheduler code.
PR 10 — Implement HuggingFace loading
-
F-11Replace theNotImplementedErrorinfrom_pretrained(source_format="huggingface")by wiring PR 4 config/registry/name-map and PR 7 lazy loading
This is the existing stub. Real pinned safetensors model loads, binds, prefills, and decodes with all-logit oracle parity.
PR 11 — Generation policy
-
F-30GenerationConfig, greedy and sampled selection -
F-31Logits processors: temperature, top-k, top-p, min-p, repetition/presence/frequency penalties -
F-32Stop criteria: EOS set, stop tokens, stop strings, max tokens, finish-reason precedence -
F-70Session RNG ownership: creation, isolation from the config-carriedInputKind.RNGseed, snapshot/restore -
F-33Per-request seeded draws over that state
Pure policy — no weights, compilation, or cache allocation. Uses existing RNG threading in pytensorf.function.
PR 12 — Public API, sessions, streaming, engine
-
F-34Autoregressive loop driving prefill/decode plans and cache updates -
F-35Streaming iterator with cancellation and bounded buffering -
F-36Batched generation with independent per-sequence state -
F-37Multi-turn session/chat reusing a live cache -
F-38Structured result: tokens, text, finish reason, usage, timings, compile/copy counters
First real load_llm → generate / stream / chat path. No oracle in the normal import or call path.
PR 13 — Dense cache backend lowering
-
F-45Per-backend lowering of dense cache append/read with bounded write evidence
Follows the existing dispatch/{mlx,jax}/attention.py pattern. A whole-capacity where update cannot be advertised as supported.
PR 14 — Gemma 3n text port
-
F-50Gemma 3n architecture: AltUp, LAuReL, per-layer embeddings, sparse/dense MLP; consumes the F-16b softcap -
F-66End-to-end native integration on the public API
Replaces the prototype's per-prefix recompilation, per-layer streaming, host-side vocabulary chunking, and always-on oracle. Tier A may materialize affine-4 to dense once at load, metered.
Tier B — performance architecture
PR 15 — GGUF loading
-
F-39GGUF container reader: header, metadata, tensor directory validation, checked block/tail arithmetic -
F-40Packed handles implementing the PR 7 handle contract, mmap-backed until consumption -
F-41Packed-block → dense array conversion for the correctness baseline -
F-05GGUF tokenizer-metadata adapter
Values still bind through load_state. Untrusted binary input: bounded preflight before any third-party parser allocation.
PR 16 — CPU quantized kernels
-
F-42Packed matmul/embedding ops plus capability-gated rewrite selection, no whole-tensor dequantization
Registers into the existing rewriting/ DB and dispatch/. Oracle is an independently produced ggml reference, not the production decoder.
PR 17 — Device quantized kernels
-
F-43MLX/JAX packed kernels -
F-44MLX affine-4 (Gemma) packed layout
One-time repack is allowed if cached and metered; no zero-copy claim across host→device.
PR 18 — Benchmark and telemetry harness
-
F-61Raw-JSON benchmarks: prompt processing, decode, TTFT, ITL, throughput, peak RSS/device memory, compile count, copied bytes
llama-bench excludes tokenization/sampling, so comparisons match that scope or report both.
PR 19 — Paged/ring KV and prefix reuse
-
F-48Ring/paged storage, block tables, allocator as a backend of the PR 8 interface -
F-46Per-backend lowering of ring/paged storage -
F-49Prefix reuse with copy-on-write and a full canonical key
Logical results must be storage-layout independent. Prefix reuse is optional and never a scheduler prerequisite.
PR 20 — Continuous batching
-
F-51Scheduler: dynamic admission, mixed prefill/decode, backpressure, fairness, cancellation and reclamation
Two-phase cancellation: detach logically, quarantine in-flight blocks until the backend fence completes.
Tier C — optional interfaces (never blocks A or B)
PR 21 — Constrained decoding
-
F-52Grammar/JSON constraints with per-sequence state and bounded compile/step work
Optional. Does not require batching.
PR 22 — Speculative decoding
-
F-53Draft/target verification with exact cache and RNG rollback via F-70
Optional. Greedy output must equal ordinary decode.
PR 23 — CLI
-
F-54Thin CLI over the public API with streaming and signal cancellation
Optional. No generation logic of its own.
PR 24 — Server and metrics
-
F-55Async OpenAI-compatible completions/chat with SSE, bounded admission, loopback default -
F-56Prometheus metrics derived from the F-38 counters
Optional. Engine never imports the server; importing pytensor_ml must not import a web stack.
PR 25 — Pooling and reranking
-
F-57Pooling modes, embedding and rerank results
Optional. No generation cache required.
PR 26 — LoRA
-
F-58Adapter identity, validation, immutable-base application -
F-59Prefix-cache and scheduler integration
Optional. Adapter identity enters plan and prefix keys.
Governance
PR 27 — Release gates and upstream follow-ups
-
F-62Machine-readable required-cell manifests per tier; release jobs fail on skip, missing artifact, or stale evidence -
F-64Evidence-led PyTensor upstream requests, starting with a supported backend-dispatch registration hook to replace thesys.meta_pathworkaround
Tier C never blocks Tier A/B.
Acceptance rules that apply to every PR
- No prefix replay: prefill/decode compile once per plan key or documented shape bucket, never per token.
- Decode consumes token(s), positions, sequence ids and persistent KV — it never re-evaluates the full prefix.
- A one-token append does not write over the whole cache capacity.
- No file read, per-layer dequantization or transpose inside the token loop.
- Cached output matches full-prefix output and an independent oracle on all logits at every tested step — not just plausible text.
- The oracle is test-only: it never appears in the normal import or call path.
- Batch > 1, independent sessions, seeded RNG, stop state, streaming and cancellation are runtime behaviour, not report wrappers.
- Unsupported model/backend/quant/cache combinations fail at load or compile with the missing capability named, not at token 1.
Non-goals
Replacing llama.cpp or matching its architecture/backend matrix; multimodal encoders; distributed or tensor-parallel execution; training APIs; auth/TLS; a web UI; exact CLI flag parity; executing model repo code (trust_remote_code=False by default); and any silent fallback to full-prefix replay, whole-model dequantization or another framework.
Detailed per-item design, dependency DAG and adversarial-review notes are kept in .dev/planning/ alongside the branch work; this issue is the public follow-up surface to reference from PRs.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Treat this as a roadmap rather than a single starter change: select one numbered PR and verify its listed prerequisites. Start from the named entry points such as from_pretrained, checkpoint.py, pretrained.py, compile_predict, predict_db, and the referenced test suites. Done means the selected PR checklist is complete while the existing serialization, checkpoint, attention, rewriting, and dispatch suites remain passing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai, backend-api-design, machine-learning
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100