File size: 8,899 Bytes
c5cfd97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
177
178
179
180
181
182
183
184
185
186
187
"""Loopback OpenAI chat adapter. Native Spark tool calls are parsed, not repaired."""
import argparse
import copy
import json
import re
import threading
import time
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

ROOT = Path(r'D:\webbrain-training\compass-tiny-v3-spark-x25-20260926')
MODEL_ID = 'webbrain-compass-tiny-v3-bf16'
PACKAGE = ROOT / 'release-compat-v3'


def parse_tools(text, tools):
    """Only lossless native-format/schema decoding; errors stay visible."""
    definitions = {t['function']['name']: t['function'] for t in tools or []}
    calls = []
    pattern = re.compile(r'<tool_call>(.*?)</tool_call>', re.S)
    spans = list(pattern.finditer(text))
    if text.count('<tool_call>') != len(spans) or text.count('</tool_call>') != len(spans):
        raise ValueError('Incomplete native tool call')
    for span in spans:
        body = span.group(1)
        name = body.split('<arg_key>', 1)[0].strip()
        if name not in definitions:
            raise ValueError(f'Unknown model-emitted function: {name}')
        tail = body[len(body.split('<arg_key>', 1)[0]):]
        arg_pattern = re.compile(r'<arg_key>(.*?)</arg_key><arg_value>(.*?)</arg_value>', re.S)
        args = {}
        cursor = 0
        schema = definitions[name].get('parameters', {})
        for arg in arg_pattern.finditer(tail):
            if tail[cursor:arg.start()].strip():
                raise ValueError('Malformed native argument sequence')
            key, raw = arg.group(1), arg.group(2)
            if key in args:
                raise ValueError('Duplicate native argument key')
            prop = schema.get('properties', {}).get(key, {})
            args[key] = raw if prop.get('type') == 'string' else json.loads(raw)
            cursor = arg.end()
        if tail[cursor:].strip() or any(k not in args for k in schema.get('required', [])):
            raise ValueError('Malformed or incomplete native arguments')
        calls.append({'id': 'call_' + uuid.uuid4().hex[:16], 'type': 'function',
                      'function': {'name': name, 'arguments': json.dumps(args, ensure_ascii=False)}})
    return calls, pattern.sub('', text).strip()


def normalize_messages(messages):
    messages = copy.deepcopy(messages)
    for message in messages:
        for tool in message.get('tool_calls', []):
            arguments = tool['function']['arguments']
            if isinstance(arguments, str):
                tool['function']['arguments'] = json.loads(arguments)
            if not isinstance(tool['function']['arguments'], dict):
                raise ValueError('Tool arguments must decode to an object')
    return messages


class Bf16Backend:
    def __init__(self):
        import os
        import sys
        source = Path(__file__).parents[1] / 'spark_x25_17b_webbrain_compass_v3'
        sys.path.insert(0, str(source))
        from train import gpu_guard
        cfg = json.loads((source / 'train-config.json').read_text())
        gpu_guard(cfg)
        import torch
        import runtime
        from transformers import AutoTokenizer
        torch.set_num_threads(8)
        self.torch = torch
        self.tokenizer = AutoTokenizer.from_pretrained(PACKAGE, local_files_only=True)
        self.model = runtime.load_native(str(PACKAGE), dtype=torch.bfloat16, device_map={'': 0}).eval()
        self.lock = threading.Lock()
        uploaded = json.loads((ROOT / 'upload-verified.json').read_text())
        self.info = {'id': MODEL_ID, 'precision': 'BF16', 'runtime': 'Transformers/PyTorch CUDA',
                     'hardware': 'NVIDIA GeForce RTX 5090', 'revision': uploaded['revision'],
                     'max_context_tokens': 16384, 'thinking_default': False,
                     'onnx': False, 'webgpu': False, 'cloud_fallback': False}

    def generate(self, request):
        if request.get('model') != MODEL_ID:
            raise ValueError(f'Use exact model id {MODEL_ID}')
        tools = request.get('tools')
        messages = normalize_messages(request['messages'])
        options = request.get('chat_template_kwargs', {})
        if options.get('enable_thinking', False):
            raise ValueError('This benchmark endpoint serves thinking-disabled Compass')
        inputs = self.tokenizer.apply_chat_template(messages, tools=tools, enable_thinking=False,
            add_generation_prompt=True, return_tensors='pt', return_dict=True).to('cuda')
        tokens = int(inputs.input_ids.shape[1])
        maximum = int(request.get('max_completion_tokens', request.get('max_tokens', 512)))
        if maximum < 1 or maximum > 4096 or tokens + maximum > self.info['max_context_tokens']:
            raise ValueError('Requested input/output exceeds the deployed 16K context limit')
        temperature = float(request.get('temperature', 0))
        if temperature < 0:
            raise ValueError('Negative temperature')
        kwargs = {'max_new_tokens': maximum, 'do_sample': temperature > 0,
                  'pad_token_id': self.tokenizer.pad_token_id, 'use_cache': True}
        if temperature > 0:
            kwargs.update(temperature=temperature, top_p=float(request.get('top_p', 1.0)))
        with self.lock, self.torch.no_grad():
            output = self.model.generate(**inputs, **kwargs)
        tail = output[0, tokens:]
        raw = self.tokenizer.decode(tail, skip_special_tokens=True)
        finish = 'length' if len(tail) == maximum else 'stop'
        stop = request.get('stop') or []
        if isinstance(stop, str):
            stop = [stop]
        for marker in stop:
            if marker in raw:
                raw = raw.split(marker, 1)[0]
                finish = 'stop'
        parser_error = None
        try:
            calls, content = parse_tools(raw, tools)
        except (ValueError, KeyError, TypeError, json.JSONDecodeError) as exc:
            calls, content, parser_error = [], raw, str(exc)
        message = {'role': 'assistant', 'content': content or None}
        if calls:
            message['tool_calls'] = calls
            finish = 'tool_calls'
        result = {'id': 'chatcmpl-' + uuid.uuid4().hex, 'object': 'chat.completion', 'created': int(time.time()),
                  'model': MODEL_ID, 'choices': [{'index': 0, 'message': message, 'finish_reason': finish}],
                  'usage': {'prompt_tokens': tokens, 'completion_tokens': len(tail), 'total_tokens': tokens + len(tail)},
                  'x_runtime': self.info}
        if parser_error:
            result['x_parser_error'] = parser_error
        return result


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('--port', type=int, default=8560)
    args = parser.parse_args()
    backend = Bf16Backend()

    class Handler(BaseHTTPRequestHandler):
        def log_message(self, fmt, *args):
            # No prompts, cookies or credentials are logged.
            print(json.dumps({'http': fmt % args, 'time': time.time()}), flush=True)

        def send_json(self, status, data):
            body = json.dumps(data, ensure_ascii=False).encode()
            self.send_response(status)
            self.send_header('Content-Type', 'application/json')
            self.send_header('Content-Length', str(len(body)))
            self.end_headers()
            self.wfile.write(body)

        def do_GET(self):
            if self.path == '/v1/models':
                self.send_json(200, {'object': 'list', 'data': [{**backend.info, 'object': 'model', 'owned_by': 'webbrain-one'}]})
            elif self.path == '/health':
                self.send_json(200, {'ready': True, **backend.info})
            else:
                self.send_json(404, {'error': 'Unknown route'})

        def do_POST(self):
            if self.path != '/v1/chat/completions':
                return self.send_json(404, {'error': 'Unknown route'})
            try:
                length = int(self.headers.get('Content-Length', '0'))
                if not 0 < length <= 4 * 1024 * 1024:
                    raise ValueError('Invalid body length')
                request = json.loads(self.rfile.read(length))
                if request.get('stream', False):
                    raise ValueError('Use stream=false on this benchmark endpoint')
                result = backend.generate(request)
                self.send_json(200, result)
            except (ValueError, KeyError, TypeError) as exc:
                self.send_json(400, {'error': {'message': str(exc), 'type': 'invalid_request_error'}})
            except Exception as exc:
                self.send_json(500, {'error': {'message': str(exc), 'type': 'runtime_error'}})

    print(json.dumps({'ready': True, 'baseUrl': f'http://127.0.0.1:{args.port}/v1', **backend.info}), flush=True)
    ThreadingHTTPServer(('127.0.0.1', args.port), Handler).serve_forever()


if __name__ == '__main__':
    main()