275 lines
10 KiB
Markdown
275 lines
10 KiB
Markdown
|
|
---
|
||
|
|
library_name: transformers
|
||
|
|
license: apache-2.0
|
||
|
|
base_model: Qwen/Qwen3-4B-Instruct-2507
|
||
|
|
pipeline_tag: text-generation
|
||
|
|
tags:
|
||
|
|
- zip-rc
|
||
|
|
- adaptive-compute
|
||
|
|
- introspection
|
||
|
|
- reasoning
|
||
|
|
---
|
||
|
|
|
||
|
|
# Qwen3-4B-Instruct-2507-ZIP-RC
|
||
|
|
|
||
|
|
This model is a modified version of **[Qwen/Qwen3-4B-Instruct-2507](https://huggingface.co/Qwen/Qwen3-4B-Instruct-2507)**, trained to support **Zero-Overhead Introspection (ZIP-RC)** for adaptive test-time compute.
|
||
|
|
|
||
|
|
It was created as part of a **Paper Replication** experiment for:
|
||
|
|
**"Zero-Overhead Introspection for Adaptive Test-Time Compute"** (Manvi et al., 2025).
|
||
|
|
|
||
|
|
| **Links** | **Description** |
|
||
|
|
| :--- | :--- |
|
||
|
|
| [](https://colab.research.google.com/github/dataopsnick/paper-replication/blob/main/zip-rc_replication/dataopsnick_Qwen3_4B_Instruct_2507_zip_rc_QUICKSTART.ipynb) | **Quickstart Notebook:** Run adaptive inference immediately. |
|
||
|
|
| [](https://colab.research.google.com/github/dataopsnick/paper-replication/blob/main/zip-rc_replication/paper_replication_arxiv_org_abs_2512_01457.ipynb) | **Replication Notebook:** Full experiments and reproduction. |
|
||
|
|
|
||
|
|
## Model Description
|
||
|
|
|
||
|
|
This model retains the full reasoning capabilities of the base `Qwen3-4B-Instruct` model but features a **fine-tuned LM Head**. The head has been trained to repurpose unused logit space to predict a **joint distribution of Expected Reward (Correctness) and Remaining Generation Length** at every token step.
|
||
|
|
|
||
|
|
This allows the model to "introspect" during generation with **zero computational overhead**, enabling:
|
||
|
|
* **Adaptive Sampling:** Dynamically pruning low-quality trajectories.
|
||
|
|
* **Budget Management:** Balancing compute cost vs. accuracy.
|
||
|
|
* **Self-Correction:** Detecting when a reasoning path is failing before it finishes.
|
||
|
|
|
||
|
|
## Usage
|
||
|
|
|
||
|
|
### 1. Quick Start: Adaptive Inference
|
||
|
|
The easiest way to use the model is via the `ziprc` helper library, which handles the Meta-MDP logic (branching, pruning, and swapping).
|
||
|
|
|
||
|
|
```python
|
||
|
|
import torch
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
from huggingface_hub import hf_hub_download
|
||
|
|
|
||
|
|
# 1. Download the helper script dynamically
|
||
|
|
script_path = hf_hub_download(repo_id="dataopsnick/Qwen3-4B-Instruct-2507-zip-rc", filename="ziprc.py")
|
||
|
|
sys.path.append(os.path.dirname(script_path))
|
||
|
|
|
||
|
|
# 2. Import the downloaded module
|
||
|
|
import ziprc
|
||
|
|
|
||
|
|
# 3. Run Inference
|
||
|
|
model = ziprc.ZIPRCModel(ziprc.ZIPRCConfig())
|
||
|
|
sampler = ziprc.ZIPRCSampler(model)
|
||
|
|
|
||
|
|
prompt = "Solve the following logic puzzle: Five adults check into a hotel with three dogs. How many shoes are they all wearing?"
|
||
|
|
trajectories = sampler.generate(prompt, initial_samples=2)
|
||
|
|
|
||
|
|
best = sampler.select_best_trajectory(trajectories)
|
||
|
|
print(f"Confidence: {best['final_score']:.2%}")
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Advanced Usage: Streaming & Configuration
|
||
|
|
This example shows how to configure the pruning aggressiveness (`alpha`) and cost penalty (`beta`), and how to stream the result to see the introspection in action.
|
||
|
|
|
||
|
|
```python
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
#import tqdm
|
||
|
|
from huggingface_hub import hf_hub_download
|
||
|
|
from tqdm import tqdm
|
||
|
|
|
||
|
|
# 1. Download the helper script dynamically from the repo
|
||
|
|
script_path = hf_hub_download(repo_id="dataopsnick/Qwen3-4B-Instruct-2507-zip-rc", filename="ziprc.py")
|
||
|
|
sys.path.append(os.path.dirname(script_path))
|
||
|
|
|
||
|
|
# 2. Import the module
|
||
|
|
from ziprc import ZIPRCModel, ZIPRCConfig, ZIPRCSampler
|
||
|
|
|
||
|
|
# 3. Configure and Load Model
|
||
|
|
# Note: The model weights are downloaded automatically here
|
||
|
|
cfg = ZIPRCConfig(
|
||
|
|
model_name="dataopsnick/Qwen3-4B-Instruct-2507-zip-rc",
|
||
|
|
alpha=0.1, # Threshold for pruning
|
||
|
|
beta=0.05, # Cost penalty
|
||
|
|
smoothing_window=3 # For stable predictions
|
||
|
|
)
|
||
|
|
|
||
|
|
model = ZIPRCModel(cfg)
|
||
|
|
sampler = ZIPRCSampler(model)
|
||
|
|
|
||
|
|
# 4. Generate with Introspection
|
||
|
|
prompt = "Solve the following logic puzzle: Five adults check into a hotel with three dogs. How many shoes are they all wearing?"
|
||
|
|
|
||
|
|
# generate_stream produces trajectories with introspection data
|
||
|
|
trajectories = sampler.generate_stream(prompt, initial_samples=2)
|
||
|
|
|
||
|
|
# Select the best answer based on the introspection score
|
||
|
|
best = sampler.select_best_trajectory(trajectories)
|
||
|
|
|
||
|
|
print(f"Confidence: {best['final_score']:.2%}")
|
||
|
|
print(f"Answer: {model.tokenizer.decode(best['ids'][0], skip_special_tokens=True)}")
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Low-Level: Reading the Logits
|
||
|
|
You can manually decode the introspection signal (Reward and Cost) from the reserved tokens in the logits without using the sampler.
|
||
|
|
|
||
|
|
```python
|
||
|
|
import torch
|
||
|
|
import torch.nn.functional as F
|
||
|
|
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
|
|
|
||
|
|
model_id = "dataopsnick/Qwen3-4B-Instruct-2507-zip-rc"
|
||
|
|
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
||
|
|
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.bfloat16, device_map="auto")
|
||
|
|
|
||
|
|
# Configuration used during training
|
||
|
|
reward_bins = 8
|
||
|
|
length_bins = 7
|
||
|
|
total_zip_tokens = 56
|
||
|
|
zip_start_offset = 56
|
||
|
|
# ZIP tokens are located at the very end of the vocabulary
|
||
|
|
zip_start_id = model.config.vocab_size - zip_start_offset
|
||
|
|
|
||
|
|
def get_introspection_probs(logits):
|
||
|
|
"""
|
||
|
|
Extracts the joint distribution P(Reward, Length) from the logits.
|
||
|
|
"""
|
||
|
|
# Slice the reserved ZIP logits
|
||
|
|
zip_logits = logits[:, zip_start_id : zip_start_id + total_zip_tokens]
|
||
|
|
|
||
|
|
# Softmax over the flat ZIP tokens to get valid probabilities
|
||
|
|
probs = F.softmax(zip_logits, dim=-1)
|
||
|
|
|
||
|
|
# Reshape to [Batch, Reward_Bins, Length_Bins]
|
||
|
|
return probs.view(-1, reward_bins, length_bins)
|
||
|
|
|
||
|
|
# Example Inference Step
|
||
|
|
prompt = "Solve the following logic puzzle: Five adults check into a hotel with three dogs. How many shoes are they all wearing?"
|
||
|
|
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
|
||
|
|
|
||
|
|
with torch.no_grad():
|
||
|
|
outputs = model(inputs.input_ids)
|
||
|
|
next_token_logits = outputs.logits[:, -1, :]
|
||
|
|
|
||
|
|
# Get Introspection Signal (Zero Overhead)
|
||
|
|
joint_dist = get_introspection_probs(next_token_logits)
|
||
|
|
|
||
|
|
# 1. Marginalize over length to get P(Reward) distribution
|
||
|
|
p_reward = joint_dist.sum(dim=2) # Shape: [Batch, Reward_Bins]
|
||
|
|
|
||
|
|
# 2. Calculate Expected Reward (Confidence)
|
||
|
|
# The reward bins are linearly spaced [0, 1]. We use bin centers for the weighted sum.
|
||
|
|
# centers = 0.0625, 0.1875, ..., 0.9375
|
||
|
|
reward_grid = torch.linspace(0.0625, 0.9375, reward_bins).to(model.device)
|
||
|
|
|
||
|
|
# E[R] = sum(P(r) * r)
|
||
|
|
expected_reward = (p_reward * reward_grid).sum(dim=1).item()
|
||
|
|
|
||
|
|
print(f"Model Confidence: {expected_reward:.2%}")
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. OpenAI-Compatible Streaming (Async)
|
||
|
|
This method exposes the introspection data (`zip_rc` field) alongside standard text generation chunks, suitable for integration with frontends.
|
||
|
|
|
||
|
|
```python
|
||
|
|
import asyncio
|
||
|
|
import nest_asyncio
|
||
|
|
from ziprc import ZIPRCModel, ZIPRCConfig, ZIPRCSampler
|
||
|
|
|
||
|
|
# 1. Setup (Run once)
|
||
|
|
# This patch is required for running async loops in Colab/Jupyter
|
||
|
|
nest_asyncio.apply()
|
||
|
|
|
||
|
|
# Load Model
|
||
|
|
cfg = ZIPRCConfig(model_name="dataopsnick/Qwen3-4B-Instruct-2507-zip-rc")
|
||
|
|
model = ZIPRCModel(cfg)
|
||
|
|
sampler = ZIPRCSampler(model)
|
||
|
|
|
||
|
|
async def consume_inference_stream():
|
||
|
|
prompt = "Solve the following logic puzzle: Five adults check into a hotel with three dogs. How many shoes are they all wearing?"
|
||
|
|
|
||
|
|
print(f"User: {prompt}\n" + "-"*60)
|
||
|
|
print("Assistant (Streaming with Introspection):")
|
||
|
|
|
||
|
|
# 2. Get the OpenAI-compatible stream
|
||
|
|
# Returns an async generator yielding chunk objects
|
||
|
|
stream = sampler.openai(prompt, max_tokens=256)
|
||
|
|
|
||
|
|
final_clean_answer = ""
|
||
|
|
|
||
|
|
async for chunk in stream:
|
||
|
|
# --- Channel A: Standard Text (Compatible with standard UIs) ---
|
||
|
|
# Use .get() to handle the final chunk where delta is empty
|
||
|
|
# Use .get() to safely handle the final chunk where delta is empty
|
||
|
|
delta = chunk.choices[0].delta
|
||
|
|
content = delta.get("content", "")
|
||
|
|
|
||
|
|
if content:
|
||
|
|
print(content, end="", flush=True)
|
||
|
|
|
||
|
|
# --- Channel B: Zero-Overhead Introspection (The "Pareto" Gain) ---
|
||
|
|
# We access the side-channel data to see what the model is thinking
|
||
|
|
# without running separate reward model inference.
|
||
|
|
if hasattr(chunk, 'zip_rc'):
|
||
|
|
info = chunk.zip_rc
|
||
|
|
|
||
|
|
# If the model performs a meta-action (Branching/Pruning), log it
|
||
|
|
# Filter out 'finished' to avoid accessing missing utility/score fields
|
||
|
|
if info.action not in ['keep', 'finished']:
|
||
|
|
print(f"\n[⚙️ META-ACTION: {info.action} | Utility: {info.utility:.4f}] ", end="")
|
||
|
|
|
||
|
|
# Check for the Final Answer
|
||
|
|
if info.get('action') == 'finished' and 'final_text' in info:
|
||
|
|
final_clean_answer = info['final_text']
|
||
|
|
|
||
|
|
# Optional: Peek at the "Confidence" (Expected Correctness) in real-time
|
||
|
|
# if info.step % 10 == 0:
|
||
|
|
# print(f" (Conf: {info.lhs_score:.1%}) ", end="")
|
||
|
|
|
||
|
|
print("\n" + "-" * 40)
|
||
|
|
print("🏆 FINAL BEST ANSWER (Clean):")
|
||
|
|
print("-" * 40)
|
||
|
|
print(final_clean_answer)
|
||
|
|
|
||
|
|
# 3. Execution
|
||
|
|
loop = asyncio.get_event_loop()
|
||
|
|
loop.run_until_complete(consume_inference_stream())
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Local Server Deployment
|
||
|
|
You can deploy an OpenAI-compatible API server that streams both text and introspection data.
|
||
|
|
|
||
|
|
```python
|
||
|
|
import sys
|
||
|
|
import os
|
||
|
|
import asyncio
|
||
|
|
import uvicorn
|
||
|
|
from huggingface_hub import hf_hub_download
|
||
|
|
|
||
|
|
# 1. Download server.py
|
||
|
|
script_path = hf_hub_download(repo_id="dataopsnick/Qwen3-4B-Instruct-2507-zip-rc", filename="server.py")
|
||
|
|
sys.path.append(os.path.dirname(script_path))
|
||
|
|
|
||
|
|
# 2. Import the app
|
||
|
|
# NOTE: This will load the model weights again if they aren't cached.
|
||
|
|
# If you are low on VRAM, restart your runtime before running this cell.
|
||
|
|
from server import app
|
||
|
|
|
||
|
|
# 3. Run the Server (Colab/Jupyter Compatible)
|
||
|
|
HOST = "0.0.0.0"
|
||
|
|
PORT = 8000
|
||
|
|
config = uvicorn.Config(app, host=HOST, port=PORT)
|
||
|
|
server = uvicorn.Server(config)
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Check if we are in an existing loop (Colab)
|
||
|
|
loop = asyncio.get_running_loop()
|
||
|
|
print(f"🚀 Server running in background on http://{HOST}:{PORT}")
|
||
|
|
loop.create_task(server.serve())
|
||
|
|
except RuntimeError:
|
||
|
|
# Standard script execution
|
||
|
|
asyncio.run(server.serve())
|
||
|
|
```
|
||
|
|
|
||
|
|
## Citation
|
||
|
|
|
||
|
|
```bibtex
|
||
|
|
@article{manvi2025ziprc,
|
||
|
|
title={Zero-Overhead Introspection for Adaptive Test-Time Compute},
|
||
|
|
author={Manvi, Rohin and Hong, Joey and Seyde, Tim and Labonne, Maxime and Lechner, Mathias and Levine, Sergey},
|
||
|
|
journal={arXiv preprint arXiv:2512.01457},
|
||
|
|
year={2025}
|
||
|
|
}
|
||
|
|
```
|