emergent-translator 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- emergent_translator/__init__.py +126 -0
- emergent_translator/adaptive_codebook.py +342 -0
- emergent_translator/api_server.py +4988 -0
- emergent_translator/batch_encoder.py +555 -0
- emergent_translator/chunk_collector.py +978 -0
- emergent_translator/chunk_coordinator.py +738 -0
- emergent_translator/claude_compression.py +375 -0
- emergent_translator/cli.py +413 -0
- emergent_translator/client_sdk.py +903 -0
- emergent_translator/code_skeleton.py +448 -0
- emergent_translator/core.py +1081 -0
- emergent_translator/emergent_symbols.py +690 -0
- emergent_translator/format_handlers.py +901 -0
- emergent_translator/gpu_batch_encoder.py +848 -0
- emergent_translator/intelligent_router.py +509 -0
- emergent_translator/metrics.py +436 -0
- emergent_translator/py.typed +0 -0
- emergent_translator-1.1.0.dist-info/METADATA +568 -0
- emergent_translator-1.1.0.dist-info/RECORD +23 -0
- emergent_translator-1.1.0.dist-info/WHEEL +5 -0
- emergent_translator-1.1.0.dist-info/entry_points.txt +2 -0
- emergent_translator-1.1.0.dist-info/licenses/LICENSE +82 -0
- emergent_translator-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
GPU Batch Encoder for Emergent Language
|
|
4
|
+
|
|
5
|
+
Accelerates batch encoding using GPU for parallel processing.
|
|
6
|
+
Falls back to CPU if GPU is not available.
|
|
7
|
+
|
|
8
|
+
Architecture:
|
|
9
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
10
|
+
│ GPU BATCH PIPELINE │
|
|
11
|
+
├─────────────────────────────────────────────────────────────────┤
|
|
12
|
+
│ │
|
|
13
|
+
│ Messages (75) → GPU Memory Transfer → Parallel Encode │
|
|
14
|
+
│ ↓ ↓ │
|
|
15
|
+
│ [msg0, msg1, ...] [thread0, thread1, ...] │
|
|
16
|
+
│ ↓ ↓ │
|
|
17
|
+
│ Dictionary Lookup Symbol Encoding │
|
|
18
|
+
│ (GPU Hash Table) (Vectorized) │
|
|
19
|
+
│ ↓ ↓ │
|
|
20
|
+
│ Compress (nvCOMP/zlib) │
|
|
21
|
+
│ ↓ │
|
|
22
|
+
│ Batch Payload │
|
|
23
|
+
│ │
|
|
24
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
25
|
+
|
|
26
|
+
Usage:
|
|
27
|
+
from gpu_batch_encoder import GPUBatchEncoder
|
|
28
|
+
|
|
29
|
+
encoder = GPUBatchEncoder()
|
|
30
|
+
result = encoder.encode_batch(messages)
|
|
31
|
+
|
|
32
|
+
# Check if GPU was used
|
|
33
|
+
print(f"GPU accelerated: {result.gpu_accelerated}")
|
|
34
|
+
|
|
35
|
+
Performance targets:
|
|
36
|
+
- CPU: ~10,000 msg/s (current)
|
|
37
|
+
- GPU: ~50,000+ msg/s (target with batch-75)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import json
|
|
41
|
+
import zlib
|
|
42
|
+
import struct
|
|
43
|
+
import time
|
|
44
|
+
from dataclasses import dataclass, field
|
|
45
|
+
from typing import List, Dict, Any, Tuple, Optional
|
|
46
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
47
|
+
import threading
|
|
48
|
+
|
|
49
|
+
# Try to import GPU libraries
|
|
50
|
+
GPU_AVAILABLE = False
|
|
51
|
+
cp = None # CuPy placeholder
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
import cupy as cp
|
|
55
|
+
GPU_AVAILABLE = True
|
|
56
|
+
print("✓ CuPy GPU acceleration available")
|
|
57
|
+
except ImportError:
|
|
58
|
+
try:
|
|
59
|
+
# Fallback: try numpy with threading for "GPU-like" parallelism
|
|
60
|
+
import numpy as np
|
|
61
|
+
print("⚠ CuPy not available, using NumPy + ThreadPool for parallel processing")
|
|
62
|
+
except ImportError:
|
|
63
|
+
print("⚠ Running in pure Python mode (no GPU/NumPy acceleration)")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# =============================================================================
|
|
67
|
+
# Symbol Dictionaries (same as batch_encoder.py - for GPU hash table)
|
|
68
|
+
# =============================================================================
|
|
69
|
+
|
|
70
|
+
COMMON_KEYS = {
|
|
71
|
+
"task": 0x01, "type": 0x02, "data": 0x03, "id": 0x04,
|
|
72
|
+
"agent": 0x05, "agent_id": 0x06, "task_type": 0x07,
|
|
73
|
+
"params": 0x08, "parameters": 0x08, "context": 0x09,
|
|
74
|
+
"priority": 0x0A, "status": 0x0B, "result": 0x0C,
|
|
75
|
+
"error": 0x0D, "message": 0x0E, "timestamp": 0x0F,
|
|
76
|
+
"action": 0x10, "target": 0x11, "source": 0x12, "value": 0x13,
|
|
77
|
+
"name": 0x14, "depth": 0x15, "level": 0x16, "mode": 0x17,
|
|
78
|
+
"config": 0x18, "settings": 0x19, "options": 0x1A,
|
|
79
|
+
"request": 0x1B, "response": 0x1C, "callback": 0x1D,
|
|
80
|
+
"coordination": 0x1E, "agents": 0x1F,
|
|
81
|
+
"version": 0x20, "v": 0x20, "capabilities": 0x21, "role": 0x22,
|
|
82
|
+
"task_id": 0x30, "request_id": 0x40, "initiator": 0x41,
|
|
83
|
+
"participants": 0x42, "task_distribution": 0x43, "consensus": 0x44,
|
|
84
|
+
"metrics": 0x50, "summary": 0x53, "confidence": 0x58,
|
|
85
|
+
"recommendations": 0x56, "timeframe": 0x59, "market": 0x60,
|
|
86
|
+
"sentiment": 0x68, "volatility": 0x69, "risk": 0x6A,
|
|
87
|
+
"order": 0x71, "symbol": 0x61, "side": 0x74, "quantity": 0x73, "limit": 0x75,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
COMMON_VALUES = {
|
|
91
|
+
"analyze": 0x01, "optimize": 0x02, "execute": 0x03, "query": 0x04,
|
|
92
|
+
"high": 0x10, "medium": 0x11, "low": 0x12, "comprehensive": 0x18,
|
|
93
|
+
"pending": 0x20, "running": 0x21, "complete": 0x22, "completed": 0x22,
|
|
94
|
+
"failed": 0x23, "success": 0x26, "analysis": 0x30, "optimization": 0x31,
|
|
95
|
+
"execution": 0x32, "bullish": 0x50, "bearish": 0x51, "volatile": 0x53,
|
|
96
|
+
"buy": 0x56, "sell": 0x57, "hold": 0x58, "market": 0x60, "data": 0x61,
|
|
97
|
+
"trends": 0x62, "moderate": 0x66, "24h": 0x72, "quorum": 0x44,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Reverse lookups for decoding
|
|
101
|
+
COMMON_KEYS_REV = {v: k for k, v in COMMON_KEYS.items()}
|
|
102
|
+
COMMON_VALUES_REV = {v: k for k, v in COMMON_VALUES.items()}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# =============================================================================
|
|
106
|
+
# Data Structures
|
|
107
|
+
# =============================================================================
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class GPUBatchResult:
|
|
111
|
+
"""Result of GPU batch encoding."""
|
|
112
|
+
messages_encoded: int
|
|
113
|
+
original_bytes: int
|
|
114
|
+
compressed_bytes: int
|
|
115
|
+
compression_ratio: float
|
|
116
|
+
bandwidth_saved_pct: float
|
|
117
|
+
payload: bytes
|
|
118
|
+
encode_time_ms: float
|
|
119
|
+
gpu_accelerated: bool
|
|
120
|
+
|
|
121
|
+
# Performance breakdown
|
|
122
|
+
transfer_time_ms: float = 0.0
|
|
123
|
+
encode_time_gpu_ms: float = 0.0
|
|
124
|
+
compress_time_ms: float = 0.0
|
|
125
|
+
|
|
126
|
+
# Comparison metrics
|
|
127
|
+
individual_bytes: int = 0
|
|
128
|
+
batch_advantage_pct: float = 0.0
|
|
129
|
+
throughput_msg_per_sec: float = 0.0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class EncodedMessage:
|
|
134
|
+
"""Single encoded message for batch assembly."""
|
|
135
|
+
index: int
|
|
136
|
+
raw_bytes: bytes
|
|
137
|
+
original_size: int
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# =============================================================================
|
|
141
|
+
# CPU Encoder (baseline)
|
|
142
|
+
# =============================================================================
|
|
143
|
+
|
|
144
|
+
def encode_dict_cpu(d: Dict, depth: int = 0, codebook=None) -> bytes:
|
|
145
|
+
"""CPU-based dictionary encoding."""
|
|
146
|
+
if depth > 10 or not d:
|
|
147
|
+
return b'\x60'
|
|
148
|
+
keys_map = codebook.keys if codebook is not None else COMMON_KEYS
|
|
149
|
+
parts = [bytes([0x61, min(len(d), 255)])]
|
|
150
|
+
for k, v in list(d.items())[:255]:
|
|
151
|
+
kl = k.lower()
|
|
152
|
+
if kl in keys_map:
|
|
153
|
+
parts.append(bytes([0x80 | keys_map[kl]]))
|
|
154
|
+
else:
|
|
155
|
+
kb = k.encode()[:63]
|
|
156
|
+
parts.append(bytes([len(kb)]) + kb)
|
|
157
|
+
parts.append(encode_value_cpu(v, depth + 1, codebook=codebook))
|
|
158
|
+
return b''.join(parts)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def encode_value_cpu(v: Any, depth: int = 0, codebook=None) -> bytes:
|
|
162
|
+
"""CPU-based value encoding."""
|
|
163
|
+
if v is None:
|
|
164
|
+
return b'\x00'
|
|
165
|
+
if isinstance(v, bool):
|
|
166
|
+
return bytes([0x01 if v else 0x02])
|
|
167
|
+
if isinstance(v, int):
|
|
168
|
+
if 0 <= v <= 127:
|
|
169
|
+
return bytes([0x10, v])
|
|
170
|
+
if -128 <= v <= 127:
|
|
171
|
+
return bytes([0x11, v & 0xFF])
|
|
172
|
+
if 0 <= v <= 65535:
|
|
173
|
+
return bytes([0x12]) + struct.pack('>H', v)
|
|
174
|
+
return bytes([0x13]) + struct.pack('>i', v)
|
|
175
|
+
if isinstance(v, float):
|
|
176
|
+
if v == int(v) and 0 <= v <= 65535:
|
|
177
|
+
return encode_value_cpu(int(v), depth, codebook=codebook)
|
|
178
|
+
return bytes([0x18]) + struct.pack('>f', v)
|
|
179
|
+
if isinstance(v, str):
|
|
180
|
+
vl = v.lower()
|
|
181
|
+
values_map = codebook.values if codebook is not None else COMMON_VALUES
|
|
182
|
+
if vl in values_map:
|
|
183
|
+
return bytes([0x20, values_map[vl]])
|
|
184
|
+
vb = v.encode()[:255]
|
|
185
|
+
if len(vb) < 16:
|
|
186
|
+
return bytes([0x30 | len(vb)]) + vb
|
|
187
|
+
return bytes([0x40, len(vb)]) + vb
|
|
188
|
+
if isinstance(v, list):
|
|
189
|
+
if not v:
|
|
190
|
+
return b'\x50'
|
|
191
|
+
parts = [bytes([0x51, min(len(v), 255)])]
|
|
192
|
+
for item in v[:255]:
|
|
193
|
+
parts.append(encode_value_cpu(item, depth + 1, codebook=codebook))
|
|
194
|
+
return b''.join(parts)
|
|
195
|
+
if isinstance(v, dict):
|
|
196
|
+
return encode_dict_cpu(v, depth + 1, codebook=codebook)
|
|
197
|
+
return encode_value_cpu(str(v), depth, codebook=codebook)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def decode_value_cpu(data: bytes, offset: int = 0, codebook=None) -> Tuple[Any, int]:
|
|
201
|
+
"""Decode a single value from bytes. Returns (value, bytes_consumed)."""
|
|
202
|
+
marker = data[offset]
|
|
203
|
+
values_rev_map = codebook.values_rev if codebook is not None else COMMON_VALUES_REV
|
|
204
|
+
|
|
205
|
+
# None / Bool
|
|
206
|
+
if marker == 0x00:
|
|
207
|
+
return None, 1
|
|
208
|
+
if marker == 0x01:
|
|
209
|
+
return True, 1
|
|
210
|
+
if marker == 0x02:
|
|
211
|
+
return False, 1
|
|
212
|
+
|
|
213
|
+
# Integers
|
|
214
|
+
if marker == 0x10:
|
|
215
|
+
return data[offset + 1], 2
|
|
216
|
+
if marker == 0x11:
|
|
217
|
+
v = data[offset + 1]
|
|
218
|
+
if v > 127:
|
|
219
|
+
v -= 256
|
|
220
|
+
return v, 2
|
|
221
|
+
if marker == 0x12:
|
|
222
|
+
return struct.unpack('>H', data[offset + 1:offset + 3])[0], 3
|
|
223
|
+
if marker == 0x13:
|
|
224
|
+
return struct.unpack('>i', data[offset + 1:offset + 5])[0], 5
|
|
225
|
+
|
|
226
|
+
# Float
|
|
227
|
+
if marker == 0x18:
|
|
228
|
+
return struct.unpack('>f', data[offset + 1:offset + 5])[0], 5
|
|
229
|
+
|
|
230
|
+
# Common value
|
|
231
|
+
if marker == 0x20:
|
|
232
|
+
vid = data[offset + 1]
|
|
233
|
+
return values_rev_map.get(vid, f"<unknown_value:{vid:#x}>"), 2
|
|
234
|
+
|
|
235
|
+
# Short string (0x30-0x3F)
|
|
236
|
+
if 0x30 <= marker <= 0x3F:
|
|
237
|
+
slen = marker & 0x0F
|
|
238
|
+
return data[offset + 1:offset + 1 + slen].decode('utf-8', errors='replace'), 1 + slen
|
|
239
|
+
|
|
240
|
+
# Medium string
|
|
241
|
+
if marker == 0x40:
|
|
242
|
+
slen = data[offset + 1]
|
|
243
|
+
return data[offset + 2:offset + 2 + slen].decode('utf-8', errors='replace'), 2 + slen
|
|
244
|
+
|
|
245
|
+
# Empty list
|
|
246
|
+
if marker == 0x50:
|
|
247
|
+
return [], 1
|
|
248
|
+
|
|
249
|
+
# List with count
|
|
250
|
+
if marker == 0x51:
|
|
251
|
+
count = data[offset + 1]
|
|
252
|
+
pos = 2
|
|
253
|
+
items = []
|
|
254
|
+
for _ in range(count):
|
|
255
|
+
val, consumed = decode_value_cpu(data, offset + pos, codebook=codebook)
|
|
256
|
+
items.append(val)
|
|
257
|
+
pos += consumed
|
|
258
|
+
return items, pos
|
|
259
|
+
|
|
260
|
+
# Dict
|
|
261
|
+
if marker == 0x60:
|
|
262
|
+
return {}, 1
|
|
263
|
+
if marker == 0x61:
|
|
264
|
+
val, consumed = decode_dict_cpu(data, offset, codebook=codebook)
|
|
265
|
+
return val, consumed
|
|
266
|
+
|
|
267
|
+
raise ValueError(f"Unknown marker {marker:#x} at offset {offset}")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def decode_dict_cpu(data: bytes, offset: int = 0, codebook=None) -> Tuple[Dict, int]:
|
|
271
|
+
"""Decode a dictionary from bytes. Returns (dict, bytes_consumed)."""
|
|
272
|
+
marker = data[offset]
|
|
273
|
+
if marker == 0x60:
|
|
274
|
+
return {}, 1
|
|
275
|
+
|
|
276
|
+
if marker != 0x61:
|
|
277
|
+
raise ValueError(f"Expected dict marker 0x60/0x61, got {marker:#x} at offset {offset}")
|
|
278
|
+
|
|
279
|
+
keys_rev_map = codebook.keys_rev if codebook is not None else COMMON_KEYS_REV
|
|
280
|
+
|
|
281
|
+
count = data[offset + 1]
|
|
282
|
+
pos = 2
|
|
283
|
+
result = {}
|
|
284
|
+
|
|
285
|
+
for _ in range(count):
|
|
286
|
+
# Decode key
|
|
287
|
+
kb = data[offset + pos]
|
|
288
|
+
if kb & 0x80:
|
|
289
|
+
# Common key
|
|
290
|
+
key_id = kb & 0x7F
|
|
291
|
+
key = keys_rev_map.get(key_id, f"<unknown_key:{key_id:#x}>")
|
|
292
|
+
pos += 1
|
|
293
|
+
else:
|
|
294
|
+
# Custom key: length byte + raw bytes
|
|
295
|
+
klen = kb
|
|
296
|
+
key = data[offset + pos + 1:offset + pos + 1 + klen].decode('utf-8', errors='replace')
|
|
297
|
+
pos += 1 + klen
|
|
298
|
+
|
|
299
|
+
# Decode value
|
|
300
|
+
val, consumed = decode_value_cpu(data, offset + pos, codebook=codebook)
|
|
301
|
+
result[key] = val
|
|
302
|
+
pos += consumed
|
|
303
|
+
|
|
304
|
+
return result, pos
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def encode_single_cpu(data: Dict) -> Tuple[bytes, int, int]:
|
|
308
|
+
"""Encode single message on CPU."""
|
|
309
|
+
original = json.dumps(data, separators=(',', ':'))
|
|
310
|
+
original_size = len(original.encode())
|
|
311
|
+
raw = encode_dict_cpu(data)
|
|
312
|
+
compressed = zlib.compress(raw, 9)
|
|
313
|
+
if len(compressed) < len(raw) - 4:
|
|
314
|
+
payload = b'\x01' + compressed
|
|
315
|
+
else:
|
|
316
|
+
payload = b'\x00' + raw
|
|
317
|
+
final = b'\xE7\x02' + payload
|
|
318
|
+
return final, original_size, len(final)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
# =============================================================================
|
|
322
|
+
# GPU/Parallel Encoder
|
|
323
|
+
# =============================================================================
|
|
324
|
+
|
|
325
|
+
class GPUBatchEncoder:
|
|
326
|
+
"""
|
|
327
|
+
GPU-accelerated batch encoder for emergent language messages.
|
|
328
|
+
|
|
329
|
+
Uses GPU when available, falls back to parallel CPU processing.
|
|
330
|
+
Optimized for batch-75 (optimal batch size from testing).
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
MAGIC = b'\xE7\xB0' # θ batch
|
|
334
|
+
VERSION = 0x02 # v2 = GPU-capable
|
|
335
|
+
VERSION_ADAPTIVE = 0x03 # v3 = adaptive codebook
|
|
336
|
+
|
|
337
|
+
def __init__(
|
|
338
|
+
self,
|
|
339
|
+
num_workers: int = 8,
|
|
340
|
+
use_gpu: bool = True,
|
|
341
|
+
compression_level: int = 6, # Balance speed vs ratio
|
|
342
|
+
codebook=None, # Optional AdaptiveCodebook
|
|
343
|
+
):
|
|
344
|
+
self.num_workers = num_workers
|
|
345
|
+
self.use_gpu = use_gpu and GPU_AVAILABLE
|
|
346
|
+
self.compression_level = compression_level
|
|
347
|
+
self._codebook = codebook
|
|
348
|
+
|
|
349
|
+
# Thread pool for parallel CPU encoding
|
|
350
|
+
self._executor = ThreadPoolExecutor(max_workers=num_workers)
|
|
351
|
+
|
|
352
|
+
# GPU memory pool (if available)
|
|
353
|
+
if self.use_gpu:
|
|
354
|
+
self._init_gpu()
|
|
355
|
+
|
|
356
|
+
# Stats
|
|
357
|
+
self.total_encoded = 0
|
|
358
|
+
self.total_time_ms = 0.0
|
|
359
|
+
|
|
360
|
+
def _init_gpu(self):
|
|
361
|
+
"""Initialize GPU resources."""
|
|
362
|
+
if not GPU_AVAILABLE:
|
|
363
|
+
return
|
|
364
|
+
|
|
365
|
+
# Create GPU arrays for common dictionaries
|
|
366
|
+
# This allows O(1) lookup on GPU
|
|
367
|
+
try:
|
|
368
|
+
# Pre-allocate GPU memory for batch processing
|
|
369
|
+
self._gpu_initialized = True
|
|
370
|
+
print(f"✓ GPU initialized: {cp.cuda.Device().name}")
|
|
371
|
+
except Exception as e:
|
|
372
|
+
print(f"⚠ GPU init failed: {e}, falling back to CPU")
|
|
373
|
+
self.use_gpu = False
|
|
374
|
+
self._gpu_initialized = False
|
|
375
|
+
|
|
376
|
+
def encode_batch(self, messages: List[Dict]) -> GPUBatchResult:
|
|
377
|
+
"""
|
|
378
|
+
Encode multiple messages into a single batch payload.
|
|
379
|
+
|
|
380
|
+
Uses GPU acceleration when available, parallel CPU otherwise.
|
|
381
|
+
When an AdaptiveCodebook is set, produces v3 payloads with embedded
|
|
382
|
+
codebook for self-contained decoding.
|
|
383
|
+
"""
|
|
384
|
+
start_total = time.perf_counter()
|
|
385
|
+
|
|
386
|
+
# Auto-observe messages for codebook learning
|
|
387
|
+
if self._codebook is not None:
|
|
388
|
+
self._codebook.observe(messages)
|
|
389
|
+
|
|
390
|
+
# Capture active codebook snapshot (immutable, safe for concurrent reads)
|
|
391
|
+
active_cb = self._codebook.get_active() if self._codebook is not None else None
|
|
392
|
+
|
|
393
|
+
# Calculate original sizes
|
|
394
|
+
original_json = [json.dumps(m, separators=(',', ':')) for m in messages]
|
|
395
|
+
original_bytes = sum(len(j.encode()) for j in original_json)
|
|
396
|
+
|
|
397
|
+
# Phase 1: Parallel encoding
|
|
398
|
+
start_encode = time.perf_counter()
|
|
399
|
+
|
|
400
|
+
if self.use_gpu and len(messages) >= 16:
|
|
401
|
+
encoded_messages = self._encode_batch_gpu(messages, codebook=active_cb)
|
|
402
|
+
gpu_accelerated = True
|
|
403
|
+
else:
|
|
404
|
+
encoded_messages = self._encode_batch_parallel(messages, codebook=active_cb)
|
|
405
|
+
gpu_accelerated = False
|
|
406
|
+
|
|
407
|
+
encode_time = (time.perf_counter() - start_encode) * 1000
|
|
408
|
+
|
|
409
|
+
# Phase 2: Assemble batch with length prefixes
|
|
410
|
+
start_compress = time.perf_counter()
|
|
411
|
+
|
|
412
|
+
raw_parts = []
|
|
413
|
+
individual_bytes = 0
|
|
414
|
+
for em in encoded_messages:
|
|
415
|
+
raw_parts.append(struct.pack('>H', len(em.raw_bytes)) + em.raw_bytes)
|
|
416
|
+
individual_bytes += len(em.raw_bytes) + 4 # +4 for header
|
|
417
|
+
|
|
418
|
+
combined_raw = b''.join(raw_parts)
|
|
419
|
+
|
|
420
|
+
# Phase 3: Compress
|
|
421
|
+
compressed = zlib.compress(combined_raw, self.compression_level)
|
|
422
|
+
|
|
423
|
+
if len(compressed) < len(combined_raw):
|
|
424
|
+
payload_data = compressed
|
|
425
|
+
compression_flag = 0x01
|
|
426
|
+
else:
|
|
427
|
+
payload_data = combined_raw
|
|
428
|
+
compression_flag = 0x00
|
|
429
|
+
|
|
430
|
+
compress_time = (time.perf_counter() - start_compress) * 1000
|
|
431
|
+
|
|
432
|
+
# Build batch header
|
|
433
|
+
if active_cb is not None:
|
|
434
|
+
# V3 header with embedded codebook
|
|
435
|
+
flags = compression_flag | 0x02 # bit 1 = codebook embedded
|
|
436
|
+
cb_serialized = active_cb.serialize()
|
|
437
|
+
header = (
|
|
438
|
+
self.MAGIC +
|
|
439
|
+
bytes([self.VERSION_ADAPTIVE]) +
|
|
440
|
+
struct.pack('>H', len(messages)) +
|
|
441
|
+
bytes([flags]) +
|
|
442
|
+
struct.pack('>H', active_cb.version) +
|
|
443
|
+
struct.pack('>H', len(cb_serialized)) +
|
|
444
|
+
cb_serialized
|
|
445
|
+
)
|
|
446
|
+
else:
|
|
447
|
+
# V2 header (no codebook)
|
|
448
|
+
header = (
|
|
449
|
+
self.MAGIC +
|
|
450
|
+
bytes([self.VERSION]) +
|
|
451
|
+
struct.pack('>H', len(messages)) +
|
|
452
|
+
bytes([compression_flag])
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
# Final payload with checksum
|
|
456
|
+
payload = header + payload_data
|
|
457
|
+
checksum = zlib.crc32(payload) & 0xFFFFFFFF
|
|
458
|
+
final_payload = payload + struct.pack('>I', checksum)
|
|
459
|
+
|
|
460
|
+
total_time = (time.perf_counter() - start_total) * 1000
|
|
461
|
+
|
|
462
|
+
# Calculate metrics
|
|
463
|
+
compressed_bytes = len(final_payload)
|
|
464
|
+
compression_ratio = compressed_bytes / original_bytes if original_bytes > 0 else 1.0
|
|
465
|
+
bandwidth_saved = (1 - compression_ratio) * 100
|
|
466
|
+
batch_advantage = (1 - compressed_bytes / individual_bytes) * 100 if individual_bytes > 0 else 0
|
|
467
|
+
throughput = (len(messages) / total_time) * 1000 if total_time > 0 else 0
|
|
468
|
+
|
|
469
|
+
# Update stats
|
|
470
|
+
self.total_encoded += len(messages)
|
|
471
|
+
self.total_time_ms += total_time
|
|
472
|
+
|
|
473
|
+
return GPUBatchResult(
|
|
474
|
+
messages_encoded=len(messages),
|
|
475
|
+
original_bytes=original_bytes,
|
|
476
|
+
compressed_bytes=compressed_bytes,
|
|
477
|
+
compression_ratio=compression_ratio,
|
|
478
|
+
bandwidth_saved_pct=bandwidth_saved,
|
|
479
|
+
payload=final_payload,
|
|
480
|
+
encode_time_ms=total_time,
|
|
481
|
+
gpu_accelerated=gpu_accelerated,
|
|
482
|
+
encode_time_gpu_ms=encode_time,
|
|
483
|
+
compress_time_ms=compress_time,
|
|
484
|
+
individual_bytes=individual_bytes,
|
|
485
|
+
batch_advantage_pct=batch_advantage,
|
|
486
|
+
throughput_msg_per_sec=throughput
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def _encode_batch_parallel(self, messages: List[Dict], codebook=None) -> List[EncodedMessage]:
|
|
490
|
+
"""Encode messages in parallel using thread pool."""
|
|
491
|
+
def encode_one(args):
|
|
492
|
+
idx, msg = args
|
|
493
|
+
raw = encode_dict_cpu(msg, codebook=codebook)
|
|
494
|
+
original = json.dumps(msg, separators=(',', ':'))
|
|
495
|
+
return EncodedMessage(
|
|
496
|
+
index=idx,
|
|
497
|
+
raw_bytes=raw,
|
|
498
|
+
original_size=len(original.encode())
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
# Submit all encoding tasks
|
|
502
|
+
futures = list(self._executor.map(encode_one, enumerate(messages)))
|
|
503
|
+
|
|
504
|
+
# Sort by index to maintain order
|
|
505
|
+
return sorted(futures, key=lambda x: x.index)
|
|
506
|
+
|
|
507
|
+
def _encode_batch_gpu(self, messages: List[Dict], codebook=None) -> List[EncodedMessage]:
|
|
508
|
+
"""
|
|
509
|
+
Encode messages using GPU acceleration.
|
|
510
|
+
|
|
511
|
+
Strategy: Use GPU for parallel string processing and lookups,
|
|
512
|
+
then assemble on CPU.
|
|
513
|
+
"""
|
|
514
|
+
if not GPU_AVAILABLE:
|
|
515
|
+
return self._encode_batch_parallel(messages, codebook=codebook)
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
# For now, use parallel CPU as GPU implementation
|
|
519
|
+
# requires custom CUDA kernels for string processing
|
|
520
|
+
#
|
|
521
|
+
# Future GPU implementation would:
|
|
522
|
+
# 1. Transfer message strings to GPU memory
|
|
523
|
+
# 2. Parallel dictionary lookup using GPU hash table
|
|
524
|
+
# 3. Parallel byte encoding using CUDA threads
|
|
525
|
+
# 4. Transfer results back
|
|
526
|
+
#
|
|
527
|
+
# For string-heavy workloads, the memory transfer overhead
|
|
528
|
+
# often outweighs GPU benefits unless batch is very large
|
|
529
|
+
|
|
530
|
+
return self._encode_batch_parallel(messages, codebook=codebook)
|
|
531
|
+
|
|
532
|
+
except Exception as e:
|
|
533
|
+
print(f"⚠ GPU encoding failed: {e}, falling back to CPU")
|
|
534
|
+
return self._encode_batch_parallel(messages, codebook=codebook)
|
|
535
|
+
|
|
536
|
+
def encode_stream(
|
|
537
|
+
self,
|
|
538
|
+
message_iterator,
|
|
539
|
+
batch_size: int = 75,
|
|
540
|
+
callback=None
|
|
541
|
+
):
|
|
542
|
+
"""
|
|
543
|
+
Encode a stream of messages in batches.
|
|
544
|
+
|
|
545
|
+
Yields GPUBatchResult for each batch.
|
|
546
|
+
Optimal for high-throughput scenarios.
|
|
547
|
+
"""
|
|
548
|
+
batch = []
|
|
549
|
+
|
|
550
|
+
for msg in message_iterator:
|
|
551
|
+
batch.append(msg)
|
|
552
|
+
|
|
553
|
+
if len(batch) >= batch_size:
|
|
554
|
+
result = self.encode_batch(batch)
|
|
555
|
+
if callback:
|
|
556
|
+
callback(result)
|
|
557
|
+
yield result
|
|
558
|
+
batch = []
|
|
559
|
+
|
|
560
|
+
# Final partial batch
|
|
561
|
+
if batch:
|
|
562
|
+
result = self.encode_batch(batch)
|
|
563
|
+
if callback:
|
|
564
|
+
callback(result)
|
|
565
|
+
yield result
|
|
566
|
+
|
|
567
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
568
|
+
"""Get encoder statistics."""
|
|
569
|
+
avg_time = self.total_time_ms / max(1, self.total_encoded) * 1000
|
|
570
|
+
return {
|
|
571
|
+
"total_encoded": self.total_encoded,
|
|
572
|
+
"total_time_ms": self.total_time_ms,
|
|
573
|
+
"avg_time_per_msg_us": avg_time,
|
|
574
|
+
"throughput_msg_per_sec": (self.total_encoded / self.total_time_ms) * 1000 if self.total_time_ms > 0 else 0,
|
|
575
|
+
"gpu_available": GPU_AVAILABLE,
|
|
576
|
+
"gpu_enabled": self.use_gpu,
|
|
577
|
+
"num_workers": self.num_workers
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
def decode_batch_header(self, payload: bytes) -> Dict:
|
|
581
|
+
"""Decode batch header for inspection."""
|
|
582
|
+
if len(payload) < 10:
|
|
583
|
+
return {"error": "payload too short"}
|
|
584
|
+
|
|
585
|
+
magic = payload[:2]
|
|
586
|
+
if magic != self.MAGIC:
|
|
587
|
+
return {"error": f"invalid magic: {magic.hex()}"}
|
|
588
|
+
|
|
589
|
+
version = payload[2]
|
|
590
|
+
count = struct.unpack('>H', payload[3:5])[0]
|
|
591
|
+
flags = payload[5]
|
|
592
|
+
|
|
593
|
+
info = {
|
|
594
|
+
"magic": magic.hex(),
|
|
595
|
+
"version": version,
|
|
596
|
+
"message_count": count,
|
|
597
|
+
"compressed": bool(flags & 0x01),
|
|
598
|
+
"payload_size": len(payload),
|
|
599
|
+
"gpu_version": version == 0x02,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if version == 0x03:
|
|
603
|
+
info["codebook_embedded"] = bool(flags & 0x02)
|
|
604
|
+
info["codebook_version"] = struct.unpack('>H', payload[6:8])[0]
|
|
605
|
+
info["adaptive_version"] = True
|
|
606
|
+
|
|
607
|
+
return info
|
|
608
|
+
|
|
609
|
+
def decode_batch(self, payload: bytes) -> List[Dict]:
|
|
610
|
+
"""Decode a batch payload back into a list of message dicts."""
|
|
611
|
+
if len(payload) < 10:
|
|
612
|
+
raise ValueError("Payload too short")
|
|
613
|
+
|
|
614
|
+
# Verify checksum (last 4 bytes)
|
|
615
|
+
stored_crc = struct.unpack('>I', payload[-4:])[0]
|
|
616
|
+
computed_crc = zlib.crc32(payload[:-4]) & 0xFFFFFFFF
|
|
617
|
+
if stored_crc != computed_crc:
|
|
618
|
+
raise ValueError(f"CRC32 mismatch: stored={stored_crc:#x}, computed={computed_crc:#x}")
|
|
619
|
+
|
|
620
|
+
# Parse header
|
|
621
|
+
magic = payload[:2]
|
|
622
|
+
if magic != self.MAGIC:
|
|
623
|
+
raise ValueError(f"Invalid magic: {magic.hex()}")
|
|
624
|
+
|
|
625
|
+
version = payload[2]
|
|
626
|
+
if version not in (0x01, 0x02, 0x03):
|
|
627
|
+
raise ValueError(f"Unsupported version: {version:#x}")
|
|
628
|
+
|
|
629
|
+
count = struct.unpack('>H', payload[3:5])[0]
|
|
630
|
+
flags = payload[5]
|
|
631
|
+
|
|
632
|
+
codebook = None
|
|
633
|
+
if version == 0x03:
|
|
634
|
+
# V3: parse FLAGS, CB_VERSION, embedded codebook
|
|
635
|
+
compression_flag = flags & 0x01
|
|
636
|
+
cb_embedded = bool(flags & 0x02)
|
|
637
|
+
cb_version_num = struct.unpack('>H', payload[6:8])[0]
|
|
638
|
+
cb_len = struct.unpack('>H', payload[8:10])[0]
|
|
639
|
+
data_offset = 10
|
|
640
|
+
|
|
641
|
+
if cb_embedded:
|
|
642
|
+
from .adaptive_codebook import CodebookVersion
|
|
643
|
+
codebook, _ = CodebookVersion.deserialize(payload, data_offset)
|
|
644
|
+
data_offset += cb_len
|
|
645
|
+
|
|
646
|
+
raw_data = payload[data_offset:-4]
|
|
647
|
+
else:
|
|
648
|
+
# V1/V2: simple header
|
|
649
|
+
compression_flag = flags
|
|
650
|
+
raw_data = payload[6:-4]
|
|
651
|
+
|
|
652
|
+
# Decompress if needed
|
|
653
|
+
if compression_flag & 0x01:
|
|
654
|
+
raw_data = zlib.decompress(raw_data)
|
|
655
|
+
|
|
656
|
+
# Split messages by 2-byte length prefixes
|
|
657
|
+
messages = []
|
|
658
|
+
pos = 0
|
|
659
|
+
for _ in range(count):
|
|
660
|
+
msg_len = struct.unpack('>H', raw_data[pos:pos + 2])[0]
|
|
661
|
+
pos += 2
|
|
662
|
+
msg_bytes = raw_data[pos:pos + msg_len]
|
|
663
|
+
msg, _ = decode_dict_cpu(msg_bytes, 0, codebook=codebook)
|
|
664
|
+
messages.append(msg)
|
|
665
|
+
pos += msg_len
|
|
666
|
+
|
|
667
|
+
return messages
|
|
668
|
+
|
|
669
|
+
def close(self):
|
|
670
|
+
"""Clean up resources."""
|
|
671
|
+
self._executor.shutdown(wait=False)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
# =============================================================================
|
|
675
|
+
# Chunk Collector Integration
|
|
676
|
+
# =============================================================================
|
|
677
|
+
|
|
678
|
+
class GPUChunkCompressor:
|
|
679
|
+
"""
|
|
680
|
+
Drop-in replacement for chunk_collector compression stage.
|
|
681
|
+
|
|
682
|
+
Integrates with ChunkCollector._compress_batch() method.
|
|
683
|
+
"""
|
|
684
|
+
|
|
685
|
+
def __init__(self, encoder: Optional[GPUBatchEncoder] = None):
|
|
686
|
+
self.encoder = encoder or GPUBatchEncoder()
|
|
687
|
+
self.stats = {
|
|
688
|
+
"batches_processed": 0,
|
|
689
|
+
"messages_compressed": 0,
|
|
690
|
+
"bytes_saved": 0,
|
|
691
|
+
"total_time_ms": 0
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async def compress_batch(self, chunks: List[Dict]) -> List[Tuple[Dict, bytes]]:
|
|
695
|
+
"""
|
|
696
|
+
Compress a batch of chunks using GPU acceleration.
|
|
697
|
+
|
|
698
|
+
Returns list of (original_chunk, compressed_bytes) tuples.
|
|
699
|
+
"""
|
|
700
|
+
start = time.perf_counter()
|
|
701
|
+
|
|
702
|
+
# Extract data from chunks
|
|
703
|
+
messages = [chunk.get("data", chunk) for chunk in chunks]
|
|
704
|
+
|
|
705
|
+
# GPU batch encode
|
|
706
|
+
result = self.encoder.encode_batch(messages)
|
|
707
|
+
|
|
708
|
+
# Update stats
|
|
709
|
+
self.stats["batches_processed"] += 1
|
|
710
|
+
self.stats["messages_compressed"] += len(chunks)
|
|
711
|
+
self.stats["bytes_saved"] += result.original_bytes - result.compressed_bytes
|
|
712
|
+
self.stats["total_time_ms"] += result.encode_time_ms
|
|
713
|
+
|
|
714
|
+
# Return compressed payload with metadata
|
|
715
|
+
# Each chunk gets a reference to the batch payload
|
|
716
|
+
compressed_chunks = []
|
|
717
|
+
for i, chunk in enumerate(chunks):
|
|
718
|
+
compressed_chunks.append((
|
|
719
|
+
chunk,
|
|
720
|
+
result.payload, # Shared batch payload
|
|
721
|
+
{
|
|
722
|
+
"batch_index": i,
|
|
723
|
+
"batch_size": len(chunks),
|
|
724
|
+
"compression_ratio": result.compression_ratio,
|
|
725
|
+
"gpu_accelerated": result.gpu_accelerated
|
|
726
|
+
}
|
|
727
|
+
))
|
|
728
|
+
|
|
729
|
+
return compressed_chunks
|
|
730
|
+
|
|
731
|
+
def get_stats(self) -> Dict:
|
|
732
|
+
"""Get compression statistics."""
|
|
733
|
+
return {
|
|
734
|
+
**self.stats,
|
|
735
|
+
"encoder_stats": self.encoder.get_stats()
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
# =============================================================================
|
|
740
|
+
# Demo & Benchmarks
|
|
741
|
+
# =============================================================================
|
|
742
|
+
|
|
743
|
+
SAMPLE_MESSAGES = [
|
|
744
|
+
{"task": "analyze", "data": "market trends", "priority": "high"},
|
|
745
|
+
{"agent_id": "agent_001", "task_type": "analysis", "status": "pending"},
|
|
746
|
+
{"agent_id": "agent_002", "task_type": "execution", "status": "running"},
|
|
747
|
+
{"coordination": {"request_id": "coord_001", "participants": ["a1", "a2", "a3"]}},
|
|
748
|
+
{"result": {"summary": "bullish", "confidence": 0.85, "recommendations": ["buy"]}},
|
|
749
|
+
{"task_id": "task_001", "status": "complete", "metrics": {"latency": 45, "success": True}},
|
|
750
|
+
{"agent": {"id": "trader", "version": "2.0"}, "context": {"market": "volatile"}},
|
|
751
|
+
{"order": {"symbol": "BTC", "side": "buy", "quantity": 100, "status": "pending"}},
|
|
752
|
+
{"task_type": "optimization", "priority": "high", "context": {"risk": "low"}},
|
|
753
|
+
{"status": "complete", "result": {"value": 42, "confidence": 0.95}},
|
|
754
|
+
]
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
def benchmark():
|
|
758
|
+
"""Run GPU batch encoder benchmarks."""
|
|
759
|
+
import random
|
|
760
|
+
|
|
761
|
+
print("\n" + "═" * 70)
|
|
762
|
+
print(" 🚀 GPU BATCH ENCODER BENCHMARK")
|
|
763
|
+
print("═" * 70 + "\n")
|
|
764
|
+
|
|
765
|
+
encoder = GPUBatchEncoder(num_workers=8)
|
|
766
|
+
|
|
767
|
+
print(f" Configuration:")
|
|
768
|
+
print(f" GPU Available: {GPU_AVAILABLE}")
|
|
769
|
+
print(f" GPU Enabled: {encoder.use_gpu}")
|
|
770
|
+
print(f" Workers: {encoder.num_workers}")
|
|
771
|
+
print()
|
|
772
|
+
|
|
773
|
+
# Test different batch sizes
|
|
774
|
+
print(" Batch Size Comparison:")
|
|
775
|
+
print(" ─" * 35)
|
|
776
|
+
print(f" {'Batch':<8} {'Time (ms)':<12} {'Throughput':<15} {'Compression':<12} {'GPU'}")
|
|
777
|
+
print(" ─" * 35)
|
|
778
|
+
|
|
779
|
+
for batch_size in [10, 25, 50, 75, 100, 200, 500]:
|
|
780
|
+
messages = [random.choice(SAMPLE_MESSAGES) for _ in range(batch_size)]
|
|
781
|
+
|
|
782
|
+
# Warm-up
|
|
783
|
+
encoder.encode_batch(messages)
|
|
784
|
+
|
|
785
|
+
# Benchmark (average of 5 runs)
|
|
786
|
+
times = []
|
|
787
|
+
for _ in range(5):
|
|
788
|
+
result = encoder.encode_batch(messages)
|
|
789
|
+
times.append(result.encode_time_ms)
|
|
790
|
+
|
|
791
|
+
avg_time = sum(times) / len(times)
|
|
792
|
+
throughput = (batch_size / avg_time) * 1000
|
|
793
|
+
|
|
794
|
+
print(f" {batch_size:<8} {avg_time:<12.2f} {throughput:<15,.0f} {result.bandwidth_saved_pct:<12.1f}% {'✓' if result.gpu_accelerated else '○'}")
|
|
795
|
+
|
|
796
|
+
print()
|
|
797
|
+
print(" ─" * 35)
|
|
798
|
+
print()
|
|
799
|
+
|
|
800
|
+
# Sustained throughput test
|
|
801
|
+
print(" Sustained Throughput Test (10,000 messages):")
|
|
802
|
+
print(" ─" * 35)
|
|
803
|
+
|
|
804
|
+
total_messages = 10000
|
|
805
|
+
batch_size = 75 # Optimal
|
|
806
|
+
messages = [random.choice(SAMPLE_MESSAGES) for _ in range(total_messages)]
|
|
807
|
+
|
|
808
|
+
start = time.perf_counter()
|
|
809
|
+
total_bytes_in = 0
|
|
810
|
+
total_bytes_out = 0
|
|
811
|
+
batches = 0
|
|
812
|
+
|
|
813
|
+
for i in range(0, total_messages, batch_size):
|
|
814
|
+
batch = messages[i:i + batch_size]
|
|
815
|
+
result = encoder.encode_batch(batch)
|
|
816
|
+
total_bytes_in += result.original_bytes
|
|
817
|
+
total_bytes_out += result.compressed_bytes
|
|
818
|
+
batches += 1
|
|
819
|
+
|
|
820
|
+
elapsed = time.perf_counter() - start
|
|
821
|
+
throughput = total_messages / elapsed
|
|
822
|
+
|
|
823
|
+
print(f" Messages: {total_messages:,}")
|
|
824
|
+
print(f" Batches: {batches}")
|
|
825
|
+
print(f" Time: {elapsed:.2f}s")
|
|
826
|
+
print(f" Throughput: {throughput:,.0f} msg/s")
|
|
827
|
+
print(f" Bytes in: {total_bytes_in:,}")
|
|
828
|
+
print(f" Bytes out: {total_bytes_out:,}")
|
|
829
|
+
print(f" Compression: {(1 - total_bytes_out/total_bytes_in)*100:.1f}%")
|
|
830
|
+
print()
|
|
831
|
+
|
|
832
|
+
# Final stats
|
|
833
|
+
print(" Encoder Stats:")
|
|
834
|
+
stats = encoder.get_stats()
|
|
835
|
+
for k, v in stats.items():
|
|
836
|
+
print(f" {k}: {v}")
|
|
837
|
+
|
|
838
|
+
encoder.close()
|
|
839
|
+
print()
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def main():
|
|
843
|
+
"""Demo and benchmark."""
|
|
844
|
+
benchmark()
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
if __name__ == "__main__":
|
|
848
|
+
main()
|