File size: 6,331 Bytes
8b821fa
 
cb7092d
8b821fa
 
cb7092d
 
 
8b821fa
66bef3c
8b821fa
 
cb7092d
 
 
 
 
8b821fa
 
 
cb7092d
8b821fa
 
 
 
 
 
 
cb7092d
8b821fa
 
 
 
 
66bef3c
 
8b821fa
 
d0e252f
 
 
 
 
 
 
 
 
 
 
 
66bef3c
 
 
8b821fa
 
 
 
 
 
 
 
cb7092d
 
66bef3c
 
 
cb7092d
8b821fa
cb7092d
 
 
 
 
 
 
 
d0e252f
8b821fa
cb7092d
8b821fa
 
cb7092d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b821fa
 
 
 
 
cb7092d
 
 
 
 
 
8b821fa
 
d7530db
 
 
 
 
cb7092d
 
 
 
 
 
 
 
 
 
 
66bef3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d0e252f
 
 
66bef3c
 
d0e252f
cb7092d
d7530db
d0e252f
 
8b821fa
cb7092d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
from flask import Flask, render_template, request, jsonify
import numpy as np
import traceback
import torch
import sys
import queue
import threading
from concurrent.futures import Future
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig
from collections import OrderedDict

app = Flask(__name__)
CKPT_NAME = "SakanaAI/RePo-OLMo2-1B-stage2-L5"

# --- 1. SETUP QUEUE & LOCKING ---
# We use a Queue to serialize requests so the GPU is only accessed by one thread at a time.
execution_queue = queue.Queue()

class RePo:
    def __init__(self, model_name="SakanaAI/RePo-OLMo2-1B-stage2-L5", start_layer=5):
        print(f"Loading model: {model_name}...")
        self.tokenizer = AutoTokenizer.from_pretrained(model_name)
        self.config = AutoConfig.from_pretrained(model_name)
        model = AutoModelForCausalLM.from_pretrained(model_name)
        
        if torch.cuda.is_available():
            model = model.to("cuda") 
            self.device = torch.device("cuda")
            print("Model loaded on CUDA.")
        else:
            print("[Warning] No GPU available, the service may be super slow")
            self.device = torch.device("cpu")
        self.model = model
        self.start_layer = start_layer
        self.cache = OrderedDict()
        self.cache_size = 8
    
    @torch.no_grad()
    def forward(self, prompt, layer, head, max_tokens=512):
        truncated = False
        inputs = self.tokenizer(prompt, return_tensors="pt")
        seq_len = inputs['input_ids'].shape[1]

        if seq_len > max_tokens:
            truncated = True
            inputs['input_ids'] = inputs['input_ids'][:, :max_tokens]
            if 'attention_mask' in inputs:
                inputs['attention_mask'] = inputs['attention_mask'][:, :max_tokens]
            prompt = self.tokenizer.decode(inputs['input_ids'][0], skip_special_tokens=False)

        if prompt in self.cache:
            pred_indices, toks = self.cache[prompt]
            self.cache.move_to_end(prompt)
        else:
            inputs = self.tokenizer(prompt, return_tensors="pt")
            tok_ids = inputs['input_ids']
            toks = self.tokenizer.convert_ids_to_tokens(tok_ids.squeeze(0), skip_special_tokens=False)
            toks = [t.replace("Ġ", " ").replace("Ċ", "\n") for t in toks]
            n_toks = len(toks)
            inputs = {k: v.to(self.device) for k, v in inputs.items()}
            outputs = self.model(**inputs, return_dict=True, output_pred_indices=True)
            pred_indices = outputs.pred_indices 
            pred_indices = [it.data.squeeze(0).reshape(-1, n_toks).tolist() for it in pred_indices]
            self.cache[prompt] = (pred_indices, toks)
            if len(self.cache) > self.cache_size:
                self.cache.popitem(last=False)
        
        data = []
        # Safety check for layer bounds
        if layer < len(pred_indices):
            for x, (y, t) in enumerate(zip(pred_indices[layer][head], toks)):
                data.append({
                    "x": int(x),
                    "y": float(y),
                    "t": str(t)
                })
        return data, truncated

# Initialize model globally
model = RePo(CKPT_NAME)

# --- 2. BACKGROUND WORKER ---
def worker():
    """
    Consumer thread that processes requests sequentially.
    """
    print("Background worker started.")
    while True:
        # Get a job from the queue
        # job structure: (future_object, args_dict)
        future, args = execution_queue.get()
        try:
            # Run the heavy model inference
            result = model.forward(
                prompt=args['sentence'], 
                layer=args['layer'], 
                head=args['head'], 
                max_tokens=args['max_tokens']
            )
            # Pass result back to the waiting HTTP thread
            future.set_result(result)
        except Exception as e:
            future.set_exception(e)
        finally:
            execution_queue.task_done()

# Start the worker thread
threading.Thread(target=worker, daemon=True).start()


@app.route('/')
def index():
    return render_template('index.html')

# --- 3. NEW STATUS ENDPOINT ---
@app.route('/queue_status', methods=['GET'])
def queue_status():
    """Returns the current number of requests waiting in queue."""
    return jsonify({"count": execution_queue.qsize()})

@app.route('/process_sentence', methods=['POST'])
def process_sentence():
    try:
        req_data = request.json
        sentence = req_data.get('sentence', '')
        layer = int(req_data.get('layer', 5))
        head = int(req_data.get('head', 0))
        
        future = Future()
        execution_queue.put((future, {
            'sentence': sentence,
            'layer': layer,
            'head': head,
            'max_tokens': 512
        }))
        
        results, was_truncated = future.result()

        # --- OUTLIER DETECTION LOGIC ---
        suggested_range = None
        y_vals = [d['y'] for d in results]
        
        # Only apply logic if we have enough data points
        if len(y_vals) > 5:
            # Calculate Quartiles
            q75, q25 = np.percentile(y_vals, [75 ,25])
            iqr = q75 - q25
            
            # Define bounds (1.5 * IQR is standard for outliers)
            lower_bound = q25 - (1.5 * iqr)
            upper_bound = q75 + (1.5 * iqr)
            
            # Find the actual data range within these bounds
            inliers = [y for y in y_vals if lower_bound <= y <= upper_bound]
            
            if inliers:
                # Add 5% padding for visual comfort
                min_in = min(inliers)
                max_in = max(inliers)
                padding = (max_in - min_in) * 0.05
                if padding == 0: padding = 1.0 # Handle flat lines
                
                suggested_range = [min_in - padding, max_in + padding]

        return jsonify({
            "status": "success", 
            "data": results, 
            "truncated": was_truncated,
            "suggested_range": suggested_range
        })

    except Exception as e:
        print(traceback.format_exc())
        return jsonify({"status": "error", "message": str(e)}), 500

# Note: The if __name__ == '__main__' block is handled by uvicorn in your docker command