phanerozoic commited on
Commit
95a427c
·
verified ·
1 Parent(s): f4ba638

model-as-a-kernel v1 source

Browse files
Files changed (2) hide show
  1. README.md +122 -0
  2. build.toml +2 -2
README.md ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: kernels
3
+ license: apache-2.0
4
+ ---
5
+
6
+ # model-as-a-kernel
7
+
8
+ An entire llama-family decode step as one kernel launch, loadable through
9
+ `kernels`. A persistent phase-interpreter kernel executes the whole forward
10
+ pass (embedding read, every layer's RMSNorms, QKV projection, rope, attention
11
+ over the KV cache, SwiGLU MLP, the LM head, and the greedy argmax) inside a
12
+ single launch, crossing a software grid barrier between phases instead of
13
+ returning to the host. Greedy generation is closed-loop on the device: the
14
+ argmax of one launch feeds the embedding read of the next, so a token costs
15
+ exactly one kernel launch, no framework code, no host synchronization, and
16
+ nothing on the CPU between tokens.
17
+
18
+ ## How it works
19
+
20
+ At model load the Python side packs the weights (bf16, contiguous) and
21
+ compiles the architecture into a program: an int64 `[n_phases, 16]` tensor
22
+ whose rows name an op (embed, fused rmsnorm+GEMV, qk-norm+rope+cache-append,
23
+ attention, swiglu+GEMV, argmax) and carry its pointers and sizes. One
24
+ persistent kernel interprets the program; all resident blocks execute each
25
+ phase cooperatively, then cross a sense-reversing global barrier. The launch
26
+ geometry comes from the occupancy API, so every block is co-resident and the
27
+ barrier cannot deadlock. A phase-per-launch debug mode runs the identical
28
+ device code one phase at a time; it is bitwise identical to the fused mode by
29
+ construction and gives per-phase cudaEvent timings.
30
+
31
+ Arithmetic reproduces transformers eager bf16 semantics op for op: bf16
32
+ storage with fp32 accumulation and one rounding per op boundary, the RMSNorm
33
+ double rounding (normalize, round, weight multiply, round), fp32 softmax with
34
+ the probabilities cast to bf16 before the PV product, bf16 rope multiplies
35
+ with cos/sin rounded to bf16, and scores rounded to bf16 before and after the
36
+ 1/sqrt(D) scaling, as eager attention does. Every reduction uses a fixed
37
+ tree, so the kernel is bitwise deterministic run to run: no atomics touch
38
+ floating-point data anywhere.
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ from kernels import get_kernel
44
+
45
+ mak = get_kernel("phanerozoic/model-as-a-kernel", version=1, trust_remote_code=True)
46
+
47
+ mm = mak.MegaModel.from_pretrained("HuggingFaceTB/SmolLM2-135M") # any plain
48
+ # llama-family checkpoint: SmolLM2, TinyLlama, Qwen3 dense, Llama without
49
+ # rope scaling. Pass a transformers model object to reuse loaded weights.
50
+
51
+ tokens = mm.generate(prompt_ids, max_new=128) # one launch per token
52
+ logits = mm.decode_step(token_id, pos) # one launch, fp32 [V] logits
53
+ logits = mm.prefill(prompt_ids) # sequential decode steps
54
+ logits, ms = mm.decode_step_timed(token_id, pos) # per-phase cudaEvent ms
55
+ ```
56
+
57
+ `version` selects the release branch; `trust_remote_code` is required by
58
+ `kernels` for publishers without the trusted-publisher mark. `from_pretrained`
59
+ needs `transformers` only if you pass a repo id string.
60
+
61
+ ## API
62
+
63
+ | Symbol | Purpose |
64
+ |---|---|
65
+ | `MegaModel.from_pretrained(model_or_id, device, max_seq, max_gen)` | pack a llama-family checkpoint into a program |
66
+ | `MegaModel(weights, config, ...)` | direct construction from tensors |
67
+ | `decode_step(token, pos, phased=False)` | one decode step, returns live fp32 logits `[V]` |
68
+ | `prefill(ids)` | feed a prompt token by token |
69
+ | `generate(prompt_ids, max_new)` | greedy generation, closed loop on device |
70
+ | `decode_step_timed(token, pos)` | phase-per-launch mode with per-phase ms |
71
+ | `ops.mak_run / mak_run_steps / mak_run_phased / mak_num_blocks` | raw program launches |
72
+
73
+ Supported architecture: RMSNorm decoder blocks with rotary attention (GQA,
74
+ optional Qwen3-style per-head qk RMSNorm), SwiGLU MLP, no attention or MLP
75
+ biases, plain unscaled rope, batch size 1, single device. `max_seq` bounds
76
+ the KV cache, `max_gen` the device-side token output buffer.
77
+
78
+ ## Correctness
79
+
80
+ bf16 transformers are order-sensitive: transformers itself does not
81
+ reproduce its own logits between a batched forward and cache-stepped decode
82
+ (the two differ by whole bf16 ulps that amplify layer over layer), so network
83
+ outputs are only defined up to that reproduction band, and the correctness
84
+ contract has two parts.
85
+
86
+ Op-level, checked bitwise against eager-semantics replication in torch:
87
+ the embedding read, the fused rmsnorm+GEMV, rope against
88
+ `apply_rotary_pos_emb` (cos/sin tables and rotated K exact), attention at
89
+ S=1, and the layer-0 V cache across all positions are `torch.equal` to the
90
+ reference.
91
+
92
+ Whole-model, on SmolLM2-135M over a 96-position teacher-forced sequence:
93
+ the deviation from cache-stepped transformers eager must sit inside twice
94
+ transformers' own cache-vs-batch band with comparable argmax agreement.
95
+ Measured on RTX 6000 Ada (sm89): transformers self-band 1.94 max-abs with
96
+ 95/96 argmax agreement; this kernel vs cache-stepped eager 2.50 max-abs with
97
+ 90/96 agreement. The fused single-launch mode, the phase-per-launch mode, and
98
+ repeated runs are mutually bitwise identical.
99
+
100
+ ## Measured
101
+
102
+ Greedy decode, SmolLM2-135M bf16, 128 tokens with a 32-token prompt,
103
+ against `transformers` eager `generate` on the same device:
104
+
105
+ | GPU | this kernel | transformers eager | speedup |
106
+ |---|---|---|---|
107
+ | RTX 6000 Ada (sm89) | 378 tok/s | 37 tok/s | 10.3x |
108
+
109
+ Per-phase cudaEvent brackets at position 512 attribute the remaining time to
110
+ the attention phases (76% in phase-per-launch mode), which are latency-bound
111
+ at one block per head; the LM-head GEMV already streams at ~700 GB/s. The
112
+ speedup comes from removing all inter-op framework overhead: at batch 1 a
113
+ 135M-parameter step is ~180 launches of microseconds-scale kernels under
114
+ eager execution, and exactly one launch here.
115
+
116
+ ## Limits
117
+
118
+ Batch size 1, decode only (prefill runs as sequential decode steps), greedy
119
+ sampling in-kernel (route the logits to an external sampler for anything
120
+ else), llama-family architectures without rope scaling or attention biases,
121
+ one CUDA device at a time. The program bakes device pointers, so the
122
+ `MegaModel` owns its weights and buffers for its lifetime.
build.toml CHANGED
@@ -1,5 +1,5 @@
1
  [general]
2
- name = "model_as_a_kernel"
3
  version = 1
4
  edition = 5
5
  license = "Apache-2.0"
@@ -16,7 +16,7 @@ src = [
16
  "torch-ext/torch_binding.h",
17
  ]
18
 
19
- [kernel.model_as_a_kernel]
20
  backend = "cuda"
21
  cuda-capabilities = ["8.0", "8.6", "8.9", "9.0", "10.0", "12.0"]
22
  depends = ["torch"]
 
1
  [general]
2
+ name = "model-as-a-kernel"
3
  version = 1
4
  edition = 5
5
  license = "Apache-2.0"
 
16
  "torch-ext/torch_binding.h",
17
  ]
18
 
19
+ [kernel.model-as-a-kernel]
20
  backend = "cuda"
21
  cuda-capabilities = ["8.0", "8.6", "8.9", "9.0", "10.0", "12.0"]
22
  depends = ["torch"]