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:
|
| 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":
|
| 59 |
-
"revision": "
|
| 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 |
+
````
|