char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

Georgi Gerganovllama-eval : add per-task summary stats (#23151)d2e179a47

char/aliceai-t5-moe
12.7 KiB376 linesraw
1#!/usr/bin/env python3
2
3import argparse
4import json
5import random
6import re
7import time
8import sys
9import os
10import threading
11from http.server import HTTPServer, BaseHTTPRequestHandler
12from typing import Dict, List, Optional
13from dataclasses import dataclass
14from pathlib import Path
15
16import datasets
17
18# Set cache directory for HuggingFace datasets
19cache_dir = Path.home() / ".cache" / "huggingface" / "datasets"
20cache_dir.mkdir(parents=True, exist_ok=True)
21os.environ["HF_DATASETS_CACHE"] = str(cache_dir)
22
23def dice(s1: str, s2: str) -> float:
24    """Calculate Dice coefficient between two strings based on bigram overlap."""
25    if not s1 and not s2:
26        return 1.0
27
28    def _bigrams(s: str):
29        return [s[i : i + 2] for i in range(len(s) - 1)]
30
31    bigrams1 = _bigrams(s1)
32    bigrams2 = _bigrams(s2)
33
34    if not bigrams1 and not bigrams2:
35        return 1.0
36
37    from collections import Counter
38
39    freq1 = Counter(bigrams1)
40    freq2 = Counter(bigrams2)
41
42    intersection = sum(min(freq1[bg], freq2[bg]) for bg in freq1)
43    dice_coeff = 2 * intersection / (len(bigrams1) + len(bigrams2))
44    return dice_coeff
45
46def debug_log(message: str):
47    """Log debug messages to both stdout and a file"""
48    print(message, file=sys.stderr)
49    with open("/tmp/simulator-debug.log", "a") as f:
50        f.write(message + "\n")
51
52simulator: Optional["Simulator"] = None
53
54@dataclass
55class EvalState:
56    id: str
57    tasks: List[str]
58    task_states: Dict[str, Dict]
59    sampling_config: Dict
60
61def normalize_number(s: str) -> Optional[int]:
62    match = re.match(r"\d+", s)  # match digits from the start
63    if not match:
64        return None
65    return int(match.group(0))
66
67class AimeDataset:
68    def __init__(self, split: str = "train", dataset_type: str = "aime"):
69        self.split = split
70        self.dataset_type = dataset_type
71        self.questions: List[Dict] = []
72        self._load_dataset()
73
74    def _get_question_text(self, question: Dict) -> str:
75        """Get question text, handling different dataset field names."""
76        return question.get("problem", question.get("question", ""))
77
78    def _load_dataset(self):
79        if self.dataset_type == "aime":
80            print(f"Loading AIME dataset (split: {self.split})...")
81            cache_path = Path.home() / ".cache" / "huggingface" / "datasets" / "AI-MO___aimo-validation-aime" / "default" / "0.0.0"
82            if cache_path.exists():
83                print(f"Using cached dataset from {cache_path}")
84                ds = datasets.load_dataset("AI-MO/aimo-validation-aime", split=self.split, cache_dir=str(cache_path))
85            else:
86                ds = datasets.load_dataset("AI-MO/aimo-validation-aime", split=self.split)
87        elif self.dataset_type == "aime2025":
88            print(f"Loading AIME2025 dataset...")
89            ds_list = []
90            for config_name in ["AIME2025-I", "AIME2025-II"]:
91                cache_path = Path.home() / ".cache" / "huggingface" / "datasets" / "opencompass___AIME2025" / "default" / "0.0.0"
92                if cache_path.exists():
93                    print(f"Using cached dataset from {cache_path}")
94                    ds = datasets.load_dataset("opencompass/AIME2025", config_name, split="test", cache_dir=str(cache_path))
95                else:
96                    ds = datasets.load_dataset("opencompass/AIME2025", config_name, split="test")
97                ds_list.extend(ds)
98            ds = ds_list
99        else:
100            raise ValueError(f"Unknown dataset type: {self.dataset_type}")
101
102        self.questions = list(ds)
103        print(f"{self.dataset_type} dataset loaded: {len(self.questions)} questions")
104
105    def find_question(self, request_text: str) -> Optional[Dict]:
106        # Strip common template prefixes to get the actual question text
107        # Templates include things like "Solve the following math problem step by step..."
108        # The actual question usually follows a blank line or after the template instruction
109        cleaned = request_text
110        # Split on double newline and take the part that looks like the problem
111        parts = cleaned.split('\n\n')
112        if len(parts) > 1:
113            # Find the part that's longest (likely the actual problem text)
114            problem_parts = [p for p in parts if len(p.strip()) > 100]
115            if problem_parts:
116                cleaned = max(problem_parts, key=lambda x: len(x))
117
118        best_match = None
119        best_distance = -1
120        best_index = -1
121
122        for i, question in enumerate(self.questions):
123            question_text = self._get_question_text(question)
124            request_lower = cleaned.lower()
125            question_lower = question_text.lower()
126
127            # Check if question text is contained in the cleaned request
128            if question_lower in request_lower or request_lower in question_lower:
129                debug_log(f"DEBUG: Found substring match at index {i}")
130                return question
131
132            # Exact match
133            if question_lower == request_lower:
134                debug_log(f"DEBUG: Found exact match at index {i}")
135                return question
136
137            # Remove LaTeX formatting for more flexible matching
138            question_no_latex = re.sub(r'\$[^$]+\$', '', question_text)
139            if question_no_latex.lower() == request_lower:
140                debug_log(f"DEBUG: Found match (no LaTeX) at index {i}")
141                return question
142
143            # Calculate Dice coefficient for partial matches
144            # Only consider if request is at least 50% of question length
145            if len(request_lower) >= len(question_lower) * 0.5:
146                distance = dice(question_lower, request_lower)
147
148                if distance > best_distance:
149                    best_distance = distance
150                    best_match = question
151                    best_index = i
152
153        if best_match and best_distance > 0.3:  # Threshold for partial match
154            debug_log(f"DEBUG: Found best partial match at index {best_index} with distance {best_distance:.3f}")
155            return best_match
156
157        debug_log(f"DEBUG: No matching question found for cleaned: {cleaned[:100]}...")
158        return None
159
160    def get_answer(self, question: Dict) -> str:
161        answer = question["answer"]
162        if isinstance(answer, str):
163            normalized = normalize_number(answer)
164            return str(normalized) if normalized is not None else answer
165        return str(answer)
166
167class Simulator:
168    def __init__(
169        self,
170        port: int = 8033,
171        host: str = "localhost",
172        success_rate: float = 0.8,
173        dataset_split: str = "train",
174        dataset_type: str = "aime"
175    ):
176        self.port = port
177        self.host = host
178        self.success_rate = success_rate
179        self.dataset = AimeDataset(dataset_split, dataset_type)
180        self.eval_state = EvalState(
181            id=dataset_type,
182            tasks=[dataset_type],
183            task_states={},
184            sampling_config={"temperature": 0, "max_tokens": 2048}
185        )
186
187    def _generate_response(
188        self,
189        question: Dict,
190        should_be_correct: bool
191    ) -> Dict:
192        expected_answer = self.dataset.get_answer(question)
193
194        if should_be_correct:
195            response_text = expected_answer
196        else:
197            response_text = self._generate_wrong_answer(question)
198
199        comp_tokens = random.randint(10000, 60000)
200        tps_gen = random.uniform(90.0, 110.0)
201        t_gen_ms = comp_tokens / tps_gen * 1000
202
203        return {
204            "id": f"chatcmpl-{int(time.time())}",
205            "object": "chat.completion",
206            "created": int(time.time()),
207            "model": "llama",
208            "choices": [
209                {
210                    "index": 0,
211                    "message": {
212                        "role": "assistant",
213                        "content": response_text
214                    },
215                    "finish_reason": "stop"
216                }
217            ],
218            "usage": {
219                "prompt_tokens": 100,
220                "completion_tokens": comp_tokens,
221                "total_tokens": 100 + comp_tokens
222            },
223            "timings": {
224                "predicted_ms": t_gen_ms,
225                "predicted_per_second": tps_gen
226            }
227        }
228
229    def _generate_wrong_answer(self, question: Dict) -> str:
230        expected_answer = self.dataset.get_answer(question)
231
232        if expected_answer.isdigit():
233            wrong_answer = str(int(expected_answer) + 1)
234        else:
235            wrong_answer = expected_answer + " (wrong)"
236
237        return wrong_answer
238
239    def _process_request(self, request_data: Dict) -> Dict:
240        messages = request_data.get("messages", [])
241        if not messages:
242            return {"error": "No messages in request"}
243
244        request_text = messages[0].get("content", "")
245        debug_log(f"DEBUG: Received request with content: {request_text[:150]}...")
246
247        question = self.dataset.find_question(request_text)
248        if not question:
249            debug_log(f"DEBUG: find_question returned None")
250            return {"error": "No matching question found"}
251
252        should_be_correct = random.random() < self.success_rate
253
254        response = self._generate_response(question, should_be_correct)
255
256        task_id = "aime"
257        self.eval_state.task_states[task_id] = {
258            "correct": should_be_correct,
259            "expected": self.dataset.get_answer(question),
260            "predicted": response["choices"][0]["message"]["content"]
261        }
262
263        return response
264
265class RequestHandler(BaseHTTPRequestHandler):
266    def do_GET(self):
267        if self.path == "/v1/models":
268            self._send_json({"data": [{"id": "llama", "object": "model"}]}, 200)
269            return
270        self._send_json({"error": "Not found"}, 404)
271
272    def do_POST(self):
273        if self.path != "/v1/chat/completions":
274            self._send_json({"error": "Not found"}, 404)
275            return
276
277        try:
278            content_length = int(self.headers.get("Content-Length", 0))
279            body = self.rfile.read(content_length)
280            request_data = json.loads(body) if body else None
281
282            if not request_data:
283                self._send_json({"error": "Invalid JSON"}, 400)
284                return
285
286            if simulator is None:
287                self._send_json({"error": "Simulator not initialized"}, 500)
288                return
289
290            response = simulator._process_request(request_data)
291            self._send_json(response, 200)
292
293        except json.JSONDecodeError:
294            self._send_json({"error": "Invalid JSON"}, 400)
295        except Exception as e:
296            print(f"Error processing request: {e}")
297            self._send_json({"error": str(e)}, 500)
298
299    def _send_json(self, data: dict, status: int = 200):
300        body = json.dumps(data).encode("utf-8")
301        self.send_response(status)
302        self.send_header("Content-Type", "application/json")
303        self.send_header("Content-Length", str(len(body)))
304        self.end_headers()
305        self.wfile.write(body)
306
307    def log_message(self, format, *args):
308        # Suppress default request logging
309        pass
310
311
312def main():
313    parser = argparse.ArgumentParser(
314        description="llama-server simulator for testing eval scripts"
315    )
316    parser.add_argument(
317        "--port",
318        type=int,
319        default=8033,
320        help="Server port (default: 8033)"
321    )
322    parser.add_argument(
323        "--host",
324        type=str,
325        default="localhost",
326        help="Server host (default: localhost)"
327    )
328    parser.add_argument(
329        "--success-rate",
330        type=float,
331        default=0.8,
332        help="Success rate 0-1 (default: 0.8)"
333    )
334    parser.add_argument(
335        "--dataset",
336        type=str,
337        default="aime",
338        choices=["aime", "aime2025"],
339        help="Dataset type (default: aime)"
340    )
341    parser.add_argument(
342        "--dataset-split",
343        type=str,
344        default="train",
345        help="AIME dataset split to use (default: train)"
346    )
347
348    args = parser.parse_args()
349
350    global simulator
351    simulator = Simulator(
352        port=args.port,
353        host=args.host,
354        success_rate=args.success_rate,
355        dataset_split=args.dataset_split,
356        dataset_type=args.dataset
357    )
358
359    server = HTTPServer((args.host, args.port), RequestHandler)
360    server_thread = threading.Thread(target=server.serve_forever, daemon=True)
361    server_thread.start()
362
363    print("\n=== llama-server-simulator ===")
364    print(f"Server running on http://{args.host}:{args.port}")
365    print(f"Success rate: {args.success_rate}")
366    print(f"{args.dataset} dataset loaded: {len(simulator.dataset.questions)} questions")
367    print("\nPress Ctrl+C to stop\n")
368
369    try:
370        server_thread.join()
371    except KeyboardInterrupt:
372        print("\nShutting down...")
373        server.shutdown()
374
375if __name__ == "__main__":
376    main()