@@ -180,6 +180,14 @@ def parse_args() -> argparse.Namespace:
180180 ap .add_argument ("--chat-scripted" , default = None ,
181181 help = "Non-interactive chat: '||'-separated user turns "
182182 "(for Mac-bridge verification); writes a transcript." )
183+ ap .add_argument ("--force-f-theta" , action = "store_true" ,
184+ help = "Run f_θ restoration even under --s5-exact-full-attn "
185+ "(bypass the S5 native-prefill short-circuit). On gemma-4 "
186+ "the restored sliding-layer K/V are recall-irrelevant "
187+ "(the exact layers carry recall), but f_θ EXECUTES and "
188+ "its output is injected — exercising the full verifier/"
189+ "proposer/f_θ pipeline. Requires the torch drafter+f_θ "
190+ "(do NOT combine with --all-mlx-drafter)." )
183191 return ap .parse_args ()
184192
185193
@@ -367,7 +375,8 @@ def build_restoration(prompt_ids: List[int], *, prefill_native_s5: bool = False)
367375 restored bank for those layers lets mlx_lm store their own post-RoPE
368376 cache directly and avoids the extra clean verifier forward.
369377 """
370- if prefill_native_s5 and args .s5_exact_full_attn and not args .identity_restore :
378+ if (prefill_native_s5 and args .s5_exact_full_attn
379+ and not args .identity_restore and not args .force_f_theta ):
371380 return {}, {}, len (prompt_ids )
372381 if drafter is None or f_theta is None or fcfg is None :
373382 raise RuntimeError ("drafter/f_theta are required for this restoration mode" )
@@ -709,12 +718,16 @@ def _run_fused_chat() -> Tuple[List[str], List[float], List[int]]:
709718 accepts (reports blocks / mean_accept_len). On gemma-4 f_θ restoration
710719 is bypassed via S5 native exact-layer prefill (the free lunch);
711720 f_θ is load-bearing on full-attention models."""
712- if not (args .force_fused_specdecode and mlx_drafter is not None
713- and args .cuda_trim ):
721+ _allmlx_ok = mlx_drafter is not None and args .cuda_trim
722+ _torch_ftheta_ok = drafter is not None and f_theta is not None
723+ if not (args .force_fused_specdecode and (_allmlx_ok or _torch_ftheta_ok )):
714724 raise SystemExit (
715- "--chat needs the FULL fused engine flags: --fused-specdecode "
716- "--force-fused-specdecode --all-mlx-drafter --s5-exact-full-attn "
717- "--cuda-trim" )
725+ "--chat needs the FULL fused engine. Either:\n "
726+ " (a) --fused-specdecode --all-mlx-drafter --s5-exact-full-attn "
727+ "--cuda-trim (verifier + proposer; f_θ bypassed on gemma-4 "
728+ "via S5), or\n "
729+ " (b) --fused-specdecode --force-f-theta (torch DFlash drafter "
730+ "+ f_θ that ACTUALLY RUNS; do NOT pass --all-mlx-drafter)." )
718731 # Stop at gemma's natural turn end: <end_of_turn> + eos
719732 # (convert_tokens_to_ids is the reliable special-token lookup).
720733 chat_eos = set (end_ids )
@@ -741,23 +754,45 @@ def _encode_chat(history: List[Dict[str, str]]) -> List[int]:
741754
742755 def _gen_turn (pid : List [int ]) -> Dict [str , Any ]:
743756 rk , rv , tsrc = build_restoration (pid , prefill_native_s5 = True )
757+ # f_θ ran iff build_restoration produced restored banks via the
758+ # torch drafter+f_θ (under --force-f-theta the S5 short-circuit is
759+ # bypassed → rk holds f_θ-projected sliding-layer K/V).
760+ f_theta_ran = bool (rk ) and (drafter is not None and f_theta is not None )
744761 T = len (pid )
745762 evicted = compute_evicted_positions (
746763 T , args .sink_size , args .window_size )
747- aux_prompt = capture_aux_hidden (
764+ aux_prompt_mx = capture_aux_hidden (
748765 mlx_model , pid , aux_layer_ids , embed_scale = embed_scale )
766+ aux_prompt = (aux_prompt_mx if bridge is None
767+ else [bridge (a ) for a in aux_prompt_mx ])
749768 adapter .prefill (
750769 pid , restored_k_per_layer = _pad (rk , tsrc , T ),
751770 restored_v_per_layer = _pad (rv , tsrc , T ),
752771 evicted_positions = evicted ,
753- prefill_chunk_size = args .prefill_chunk_size , full_kv = True )
772+ prefill_chunk_size = args .prefill_chunk_size , full_kv = args . cuda_trim )
754773 t0 = time .perf_counter ()
755- res = fused_specdecode_generate_mlx_trim (
756- adapter , active_drafter , aux_prompt = aux_prompt ,
757- embed_fn = embed_fn , lm_head_fn = lm_head_fn ,
758- gen_tokens = args .max_new_tokens , block_size = args .block_size ,
759- eos_ids = chat_eos , single_fused = args .single_fused )
774+ if mlx_drafter is not None and args .cuda_trim :
775+ res = fused_specdecode_generate_mlx_trim (
776+ adapter , active_drafter , aux_prompt = aux_prompt ,
777+ embed_fn = embed_fn , lm_head_fn = lm_head_fn ,
778+ gen_tokens = args .max_new_tokens , block_size = args .block_size ,
779+ eos_ids = chat_eos , single_fused = args .single_fused )
780+ elif mlx_drafter is not None :
781+ res = fused_specdecode_generate_mlx (
782+ adapter , active_drafter , aux_prompt = aux_prompt ,
783+ embed_fn = embed_fn , lm_head_fn = lm_head_fn ,
784+ gen_tokens = args .max_new_tokens , block_size = args .block_size ,
785+ eos_ids = chat_eos )
786+ else :
787+ res = fused_specdecode_generate (
788+ adapter , active_drafter , aux_prompt = aux_prompt ,
789+ embed_fn = embed_fn , lm_head_fn = lm_head_fn ,
790+ gen_tokens = args .max_new_tokens , block_size = args .block_size ,
791+ eos_ids = chat_eos , argmax_fn = argmax_fn , arange_fn = arange_fn ,
792+ cat_aux_fn = cat_aux_fn , allow_greedy_fallback = False )
760793 res ["decode_s" ] = round (time .perf_counter () - t0 , 3 )
794+ res ["f_theta_ran" ] = f_theta_ran
795+ res ["f_theta_layers" ] = sorted (rk .keys ()) if rk else []
761796 try :
762797 txt = tokenizer .decode (res ["tokens" ], skip_special_tokens = True )
763798 except TypeError :
@@ -789,11 +824,15 @@ def _gen_turn(pid: List[int]) -> Dict[str, Any]:
789824 "user" : u , "text" : res ["text" ],
790825 "tokens" : res ["decode_tokens" ], "blocks" : res ["blocks" ],
791826 "mean_accept_len" : res ["mean_accept_len" ],
827+ "f_theta_ran" : res ["f_theta_ran" ],
828+ "f_theta_layers" : res ["f_theta_layers" ],
792829 "decode_s" : res ["decode_s" ], "decode_tps" : round (tps , 2 ),
793830 "resident_kv_bytes" : res ["resident_kv_bytes" ]})
794831 print (f"[chat] USER { u !r} " , file = sys .stderr , flush = True )
795832 print (f"[chat] GEMMA-4 { res ['text' ][:200 ]!r} (blocks="
796833 f"{ res ['blocks' ]} , accept_len={ res ['mean_accept_len' ]} , "
834+ f"f_theta_ran={ res ['f_theta_ran' ]} "
835+ f"layers={ res ['f_theta_layers' ]} , "
797836 f"{ round (tps ,2 )} tok/s, kv={ res ['resident_kv_bytes' ]/ 1e6 :.1f} MB)" ,
798837 file = sys .stderr , flush = True )
799838 report = {
0 commit comments