nickvdw commited on
Commit
a4c1b59
Β·
verified Β·
1 Parent(s): 530aeab

Update logbook: Repro - KnapSpec: Self-Speculative Decoding via Adaptive Layer Selection as a Knapsack Problem

Browse files
logbook.json CHANGED
@@ -11,7 +11,7 @@
11
  "icml2026-repro",
12
  "paper-k5nKHWp9VC"
13
  ],
14
- "updated_at": "2026-07-16T14:48:59+00:00",
15
  "root": {
16
  "slug": "index",
17
  "title": "Repro - KnapSpec: Self-Speculative Decoding via Adaptive Layer Selection as a Knapsack Problem",
@@ -55,6 +55,6 @@
55
  }
56
  ]
57
  },
58
- "agent_view_tokens": 2070,
59
- "revision": "1784213339262066106"
60
  }
 
11
  "icml2026-repro",
12
  "paper-k5nKHWp9VC"
13
  ],
14
+ "updated_at": "2026-07-16T14:52:30+00:00",
15
  "root": {
16
  "slug": "index",
17
  "title": "Repro - KnapSpec: Self-Speculative Decoding via Adaptive Layer Selection as a Knapsack Problem",
 
55
  }
56
  ]
57
  },
58
+ "agent_view_tokens": 2186,
59
+ "revision": "1784213550515826534"
60
  }
pages/claim-3-tpt-and-two-stage-optimization/page.md CHANGED
@@ -217,3 +217,172 @@ https://huggingface.co/buckets/nickvdw/repro-knapspec-self-speculative-decoding-
217
  {"type": "markdown", "id": "cell_e9c31dd5cf16", "created_at": "2026-07-16T14:46:37+00:00", "title": "The implementation computes the paper TPT equation over gamma=1..8 after DP ove…"}
218
  -->
219
  The implementation computes the paper TPT equation over gamma=1..8 after DP over latency budgets. At n=128 the proxy TPT decreased monotonically from 7.42 (gamma=1) to 2.49 (gamma=8). The paper-level PCC values 0.837 (TPT) and 0.538 (acceptance rate) require the Llama-3.1-8B GovReport sweep and were not reproduced here.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  {"type": "markdown", "id": "cell_e9c31dd5cf16", "created_at": "2026-07-16T14:46:37+00:00", "title": "The implementation computes the paper TPT equation over gamma=1..8 after DP ove…"}
218
  -->
219
  The implementation computes the paper TPT equation over gamma=1..8 after DP over latency budgets. At n=128 the proxy TPT decreased monotonically from 7.42 (gamma=1) to 2.49 (gamma=8). The paper-level PCC values 0.837 (TPT) and 0.538 (acceptance rate) require the Llama-3.1-8B GovReport sweep and were not reproduced here.
220
+
221
+
222
+ ---
223
+ <!-- trackio-cell
224
+ {"type": "code", "id": "cell_659c963163ca", "created_at": "2026-07-16T14:52:27+00:00", "title": "Run: uv ssd_benchmark.py (exit 1)", "command": ["uv", "run", "python", "repro_knapspec/ssd_benchmark.py", "--steps", "4", "--gamma", "2"], "exit_code": 1, "duration_s": 15.977}
225
+ -->
226
+ ````bash
227
+ $ uv run python repro_knapspec/ssd_benchmark.py --steps 4 --gamma 2
228
+ ````
229
+
230
+ exit 1 Β· 16.0s
231
+
232
+
233
+ ````python title=ssd_benchmark.py
234
+ #!/usr/bin/env -S uv run --script
235
+ # /// script
236
+ # dependencies = ["torch", "transformers"]
237
+ # ///
238
+ """End-to-end self-speculative draft/verify benchmark for Llama 3.2 1B.
239
+
240
+ This prototype uses direct sublayer wrappers (no forward hooks). It measures
241
+ the algorithmic draft/verify loop and reports its current Python/model-wrapper
242
+ overhead separately from the claimed optimized-kernel speedup.
243
+ """
244
+ import argparse, json, time
245
+ from pathlib import Path
246
+ import torch
247
+ from transformers import AutoModelForCausalLM, AutoTokenizer
248
+
249
+ MODEL = "meta-llama/Llama-3.2-1B"
250
+ PROMPT = "The committee reviewed the proposal and requested a detailed analysis of the expected costs, benefits, risks, and implementation timeline."
251
+
252
+ class Switchable(torch.nn.Module):
253
+ def __init__(self, inner):
254
+ super().__init__(); self.inner = inner; self.skip = False
255
+ def forward(self, *args, **kwargs):
256
+ if not self.skip:
257
+ return self.inner(*args, **kwargs)
258
+ # Attention returns (hidden, optional weights, optional cache).
259
+ hidden = args[0]
260
+ zeros = torch.zeros_like(hidden)
261
+ if isinstance(self.inner, torch.nn.Module) and self.inner.__class__.__name__.endswith("Attention"):
262
+ return (zeros, None, None)
263
+ return zeros
264
+
265
+ def install_switches(model):
266
+ switches = []
267
+ for layer in model.model.layers:
268
+ attn = Switchable(layer.self_attn); mlp = Switchable(layer.mlp)
269
+ layer.self_attn = attn; layer.mlp = mlp
270
+ switches.extend([("attn", attn), ("mlp", mlp)])
271
+ return switches
272
+
273
+ def set_mask(switches, mask):
274
+ for name, module in switches: module.skip = (name, module.layer_idx) in mask if hasattr(module, "layer_idx") else False
275
+
276
+ def set_mask_by_index(switches, mask):
277
+ for idx, (_, module) in enumerate(switches): module.skip = idx in mask
278
+
279
+ def forward(model, ids, **kwargs):
280
+ with torch.inference_mode(): return model(input_ids=ids, use_cache=True, **kwargs)
281
+
282
+ def greedy(model, ids, steps):
283
+ past = None; seq = ids; generated = 0; t0=time.perf_counter()
284
+ for _ in range(steps):
285
+ out = forward(model, seq if past is None else seq[:,-1:], past_key_values=past)
286
+ past = out.past_key_values; nxt=out.logits[:,-1].argmax(-1,keepdim=True); seq=torch.cat([seq,nxt],1); generated+=1
287
+ if torch.cuda.is_available(): torch.cuda.synchronize()
288
+ return seq, generated/(time.perf_counter()-t0)
289
+
290
+ def spec_decode(model, switches, prompt, steps, gamma):
291
+ # Prototype draft/verify: draft incrementally, then verify the whole block.
292
+ # Full verification is intentionally explicit so acceptance is auditable.
293
+ seq=prompt.clone(); accepted=0; proposed=0; rounds=0; t0=time.perf_counter()
294
+ for _ in range((steps+gamma-1)//gamma):
295
+ rounds += 1; draft=seq.clone(); draft_tokens=[]
296
+ set_mask_by_index(switches, set(range(len(switches))))
297
+ # Use a conservative fixed mask: skip every other MLP, preserving all attention.
298
+ set_mask_by_index(switches, {i for i,(n,_) in enumerate(switches) if n == "mlp" and (i//2)%2==0})
299
+ past=None
300
+ for _ in range(min(gamma, steps-proposed)):
301
+ out=forward(model, draft if past is None else draft[:,-1:], past_key_values=past)
302
+ past=out.past_key_values; tok=out.logits[:,-1].argmax(-1,keepdim=True); draft=torch.cat([draft,tok],1); draft_tokens.append(tok)
303
+ block=torch.cat(draft_tokens,1); proposed += block.shape[1]
304
+ # Verify all proposed tokens with the unmasked target in one forward.
305
+ set_mask_by_index(switches, set())
306
+ verify=forward(model, torch.cat([seq,block],1), use_cache=False).logits
307
+ start=seq.shape[1]-1
308
+ target_next=verify[:,start:start+block.shape[1]].argmax(-1)
309
+ n=0
310
+ for j in range(block.shape[1]):
311
+ if target_next[0,j].item() != block[0,j].item(): break
312
+ n += 1
313
+ accepted += n
314
+ # Preserve lossless behavior: append accepted draft tokens, then target correction.
315
+ seq=torch.cat([seq,block[:,:n]],1)
316
+ if n < block.shape[1]: seq=torch.cat([seq,target_next[:,n:n+1]],1)
317
+ proposed = min(proposed, steps)
318
+ if seq.shape[1] >= prompt.shape[1]+steps: break
319
+ if torch.cuda.is_available(): torch.cuda.synchronize()
320
+ elapsed=time.perf_counter()-t0
321
+ return seq, {'accepted':accepted,'proposed':proposed,'rounds':rounds,'acceptance':accepted/max(1,proposed),'tokens_per_s':(seq.shape[1]-prompt.shape[1])/elapsed,'elapsed_s':elapsed}
322
+
323
+ def main():
324
+ ap=argparse.ArgumentParser(); ap.add_argument('--steps',type=int,default=16); ap.add_argument('--gamma',type=int,default=4); args=ap.parse_args()
325
+ device='cuda' if torch.cuda.is_available() else 'cpu'; dtype=torch.float16 if device=='cuda' else torch.float32
326
+ tok=AutoTokenizer.from_pretrained(MODEL); model=AutoModelForCausalLM.from_pretrained(MODEL,dtype=dtype).to(device).eval(); switches=install_switches(model)
327
+ prompt=tok(PROMPT,return_tensors='pt').input_ids.to(device)
328
+ _, baseline_tps=greedy(model,prompt,args.steps)
329
+ _, spec=spec_decode(model,switches,prompt,args.steps,args.gamma)
330
+ result={'model':MODEL,'device':device,'prompt_tokens':prompt.shape[1],'requested_steps':args.steps,'gamma':args.gamma,'baseline_tokens_per_s':baseline_tps,**spec,'speedup_vs_greedy':spec['tokens_per_s']/baseline_tps}
331
+ Path('repro_knapspec/outputs').mkdir(parents=True,exist_ok=True); Path('repro_knapspec/outputs/ssd_benchmark.json').write_text(json.dumps(result,indent=2)); print(json.dumps(result,indent=2))
332
+ if __name__=='__main__': main()
333
+
334
+ ````
335
+
336
+
337
+ ````output
338
+
339
+ Loading weights: 0%| | 0/146 [00:00<?, ?it/s]
340
+ Loading weights: 1%| | 1/146 [00:00<00:37, 3.85it/s]
341
+ Loading weights: 64%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Ž | 93/146 [00:00<00:00, 319.09it/s]
342
+ Loading weights: 98%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–Š| 143/146 [00:00<00:00, 247.26it/s]
343
+ Loading weights: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 146/146 [00:00<00:00, 231.69it/s]
344
+ Traceback (most recent call last):
345
+ File "/home/nick/workspace/icml26-hf-challenge-knapspec/repro_knapspec/ssd_benchmark.py", line 99, in <module>
346
+ if __name__=='__main__': main()
347
+ ~~~~^^
348
+ File "/home/nick/workspace/icml26-hf-challenge-knapspec/repro_knapspec/ssd_benchmark.py", line 96, in main
349
+ _, spec=spec_decode(model,switches,prompt,args.steps,args.gamma)
350
+ ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
351
+ File "/home/nick/workspace/icml26-hf-challenge-knapspec/repro_knapspec/ssd_benchmark.py", line 73, in spec_decode
352
+ verify=forward(model, torch.cat([seq,block],1), use_cache=False).logits
353
+ ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
354
+ File "/home/nick/workspace/icml26-hf-challenge-knapspec/repro_knapspec/ssd_benchmark.py", line 47, in forward
355
+ with torch.inference_mode(): return model(input_ids=ids, use_cache=True, **kwargs)
356
+ ~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
357
+ TypeError: LlamaForCausalLM(
358
+ (model): LlamaModel(
359
+ (embed_tokens): Embedding(128256, 2048)
360
+ (layers): ModuleList(
361
+ (0-15): 16 x LlamaDecoderLayer(
362
+ (self_attn): Switchable(
363
+ (inner): LlamaAttention(
364
+ (q_proj): Linear(in_features=2048, out_features=2048, bias=False)
365
+ (k_proj): Linear(in_features=2048, out_features=512, bias=False)
366
+ (v_proj): Linear(in_features=2048, out_features=512, bias=False)
367
+ (o_proj): Linear(in_features=2048, out_features=2048, bias=False)
368
+ )
369
+ )
370
+ (mlp): Switchable(
371
+ (inner): LlamaMLP(
372
+ (gate_proj): Linear(in_features=2048, out_features=8192, bias=False)
373
+ (up_proj): Linear(in_features=2048, out_features=8192, bias=False)
374
+ (down_proj): Linear(in_features=8192, out_features=2048, bias=False)
375
+ (act_fn): SiLUActivation()
376
+ )
377
+ )
378
+ (input_layernorm): LlamaRMSNorm((2048,), eps=1e-05)
379
+ (post_attention_layernorm): LlamaRMSNorm((2048,), eps=1e-05)
380
+ )
381
+ )
382
+ (norm): LlamaRMSNorm((2048,), eps=1e-05)
383
+ (rotary_emb): LlamaRotaryEmbedding()
384
+ )
385
+ (lm_head): Linear(in_features=2048, out_features=128256, bias=False)
386
+ ) got multiple values for keyword argument 'use_cache'
387
+
388
+ ````