nexaroa 0.0.111__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.
- neuroshard/__init__.py +93 -0
- neuroshard/__main__.py +4 -0
- neuroshard/cli.py +466 -0
- neuroshard/core/__init__.py +92 -0
- neuroshard/core/consensus/verifier.py +252 -0
- neuroshard/core/crypto/__init__.py +20 -0
- neuroshard/core/crypto/ecdsa.py +392 -0
- neuroshard/core/economics/__init__.py +52 -0
- neuroshard/core/economics/constants.py +387 -0
- neuroshard/core/economics/ledger.py +2111 -0
- neuroshard/core/economics/market.py +975 -0
- neuroshard/core/economics/wallet.py +168 -0
- neuroshard/core/governance/__init__.py +74 -0
- neuroshard/core/governance/proposal.py +561 -0
- neuroshard/core/governance/registry.py +545 -0
- neuroshard/core/governance/versioning.py +332 -0
- neuroshard/core/governance/voting.py +453 -0
- neuroshard/core/model/__init__.py +30 -0
- neuroshard/core/model/dynamic.py +4186 -0
- neuroshard/core/model/llm.py +905 -0
- neuroshard/core/model/registry.py +164 -0
- neuroshard/core/model/scaler.py +387 -0
- neuroshard/core/model/tokenizer.py +568 -0
- neuroshard/core/network/__init__.py +56 -0
- neuroshard/core/network/connection_pool.py +72 -0
- neuroshard/core/network/dht.py +130 -0
- neuroshard/core/network/dht_plan.py +55 -0
- neuroshard/core/network/dht_proof_store.py +516 -0
- neuroshard/core/network/dht_protocol.py +261 -0
- neuroshard/core/network/dht_service.py +506 -0
- neuroshard/core/network/encrypted_channel.py +141 -0
- neuroshard/core/network/nat.py +201 -0
- neuroshard/core/network/nat_traversal.py +695 -0
- neuroshard/core/network/p2p.py +929 -0
- neuroshard/core/network/p2p_data.py +150 -0
- neuroshard/core/swarm/__init__.py +106 -0
- neuroshard/core/swarm/aggregation.py +729 -0
- neuroshard/core/swarm/buffers.py +643 -0
- neuroshard/core/swarm/checkpoint.py +709 -0
- neuroshard/core/swarm/compute.py +624 -0
- neuroshard/core/swarm/diloco.py +844 -0
- neuroshard/core/swarm/factory.py +1288 -0
- neuroshard/core/swarm/heartbeat.py +669 -0
- neuroshard/core/swarm/logger.py +487 -0
- neuroshard/core/swarm/router.py +658 -0
- neuroshard/core/swarm/service.py +640 -0
- neuroshard/core/training/__init__.py +29 -0
- neuroshard/core/training/checkpoint.py +600 -0
- neuroshard/core/training/distributed.py +1602 -0
- neuroshard/core/training/global_tracker.py +617 -0
- neuroshard/core/training/production.py +276 -0
- neuroshard/governance_cli.py +729 -0
- neuroshard/grpc_server.py +895 -0
- neuroshard/runner.py +3223 -0
- neuroshard/sdk/__init__.py +92 -0
- neuroshard/sdk/client.py +990 -0
- neuroshard/sdk/errors.py +101 -0
- neuroshard/sdk/types.py +282 -0
- neuroshard/tracker/__init__.py +0 -0
- neuroshard/tracker/server.py +864 -0
- neuroshard/ui/__init__.py +0 -0
- neuroshard/ui/app.py +102 -0
- neuroshard/ui/templates/index.html +1052 -0
- neuroshard/utils/__init__.py +0 -0
- neuroshard/utils/autostart.py +81 -0
- neuroshard/utils/hardware.py +121 -0
- neuroshard/utils/serialization.py +90 -0
- neuroshard/version.py +1 -0
- nexaroa-0.0.111.dist-info/METADATA +283 -0
- nexaroa-0.0.111.dist-info/RECORD +78 -0
- nexaroa-0.0.111.dist-info/WHEEL +5 -0
- nexaroa-0.0.111.dist-info/entry_points.txt +4 -0
- nexaroa-0.0.111.dist-info/licenses/LICENSE +190 -0
- nexaroa-0.0.111.dist-info/top_level.txt +2 -0
- protos/__init__.py +0 -0
- protos/neuroshard.proto +651 -0
- protos/neuroshard_pb2.py +160 -0
- protos/neuroshard_pb2_grpc.py +1298 -0
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic Inference Market - Request-Response Marketplace
|
|
3
|
+
|
|
4
|
+
This module implements a TRUE MARKETPLACE for inference where:
|
|
5
|
+
- Users submit requests with locked-in prices
|
|
6
|
+
- Nodes claim requests and process them
|
|
7
|
+
- Payment guaranteed at submission price (no timing attacks)
|
|
8
|
+
- Full attack resistance through request matching
|
|
9
|
+
|
|
10
|
+
Economic Principles:
|
|
11
|
+
1. High demand + Low supply = High price (attracts nodes to serve)
|
|
12
|
+
2. Low demand + High supply = Low price (nodes switch to training)
|
|
13
|
+
3. Market finds equilibrium where: inference_profit ≈ training_profit × 0.5
|
|
14
|
+
|
|
15
|
+
Security Features:
|
|
16
|
+
1. Request-Response Matching: Price locked at submission time
|
|
17
|
+
2. Atomic Claiming: Prevents double-claim attacks
|
|
18
|
+
3. Claim Timeouts: Squatting nodes slashed and requests re-queued
|
|
19
|
+
4. ECDSA Signatures: User must sign request to authorize payment
|
|
20
|
+
5. Capacity Timeout: Fake capacity removed after 60s
|
|
21
|
+
6. Price Smoothing: EMA prevents flash crashes
|
|
22
|
+
7. Rate Limiting: Per-node and per-user limits
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import time
|
|
26
|
+
import math
|
|
27
|
+
import uuid
|
|
28
|
+
import logging
|
|
29
|
+
from dataclasses import dataclass, field
|
|
30
|
+
from typing import List, Optional, Tuple, Dict, Set
|
|
31
|
+
from collections import deque
|
|
32
|
+
from enum import Enum
|
|
33
|
+
import threading
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RequestStatus(str, Enum):
|
|
39
|
+
"""Request lifecycle states."""
|
|
40
|
+
PENDING = "pending" # Waiting for driver to claim
|
|
41
|
+
DRIVER_CLAIMED = "driver_claimed" # Driver claimed, starting pipeline
|
|
42
|
+
PROCESSING = "processing" # Pipeline actively processing
|
|
43
|
+
COMPLETED = "completed" # All proofs received, done
|
|
44
|
+
FAILED = "failed" # Timeout or error
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class InferenceRequest:
|
|
49
|
+
"""
|
|
50
|
+
Marketplace request (PUBLIC metadata - NO PROMPT!).
|
|
51
|
+
|
|
52
|
+
Privacy: Prompt is sent DIRECTLY to driver node (encrypted).
|
|
53
|
+
The marketplace only orchestrates pricing and pipeline coordination.
|
|
54
|
+
"""
|
|
55
|
+
request_id: str # Unique ID (UUID)
|
|
56
|
+
user_id: str # User who submitted
|
|
57
|
+
driver_node_id: str # Which driver node will process (Layer 0)
|
|
58
|
+
|
|
59
|
+
# Pricing
|
|
60
|
+
tokens_requested: int # Max tokens to generate
|
|
61
|
+
max_price: float # Max NEURO per 1M tokens user will pay
|
|
62
|
+
locked_price: float # Actual price locked at submission time
|
|
63
|
+
priority: int # Higher = more urgent
|
|
64
|
+
|
|
65
|
+
# Pipeline state (distributed inference)
|
|
66
|
+
pipeline_session_id: Optional[str] = None # Tracks distributed session
|
|
67
|
+
status: RequestStatus = RequestStatus.PENDING
|
|
68
|
+
claimed_by: Optional[str] = None # Node ID that claimed this request
|
|
69
|
+
|
|
70
|
+
# Timestamps
|
|
71
|
+
timestamp: float = 0.0 # When submitted
|
|
72
|
+
claimed_at: Optional[float] = None # When driver claimed
|
|
73
|
+
completed_at: Optional[float] = None # When all proofs received
|
|
74
|
+
|
|
75
|
+
# Proofs received (distributed inference)
|
|
76
|
+
driver_proof_received: bool = False
|
|
77
|
+
worker_proofs_received: List[str] = field(default_factory=list) # Node IDs
|
|
78
|
+
validator_proof_received: bool = False
|
|
79
|
+
|
|
80
|
+
# Security
|
|
81
|
+
user_signature: str = "" # ECDSA signature authorizing payment
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def completed(self) -> bool:
|
|
85
|
+
"""Check if request is completed (for ledger.py compatibility)."""
|
|
86
|
+
return self.status == RequestStatus.COMPLETED
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass
|
|
90
|
+
class NodeCapacity:
|
|
91
|
+
"""Available inference capacity from a node."""
|
|
92
|
+
node_id: str
|
|
93
|
+
tokens_per_second: int # Processing capacity
|
|
94
|
+
min_price: float # Minimum NEURO per 1M tokens node will accept
|
|
95
|
+
timestamp: float
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@dataclass
|
|
99
|
+
class PipelineSession:
|
|
100
|
+
"""
|
|
101
|
+
Tracks distributed inference pipeline across multiple nodes.
|
|
102
|
+
|
|
103
|
+
Privacy: Prompt is NEVER stored here - only driver knows it!
|
|
104
|
+
Workers only see activations (meaningless vectors).
|
|
105
|
+
"""
|
|
106
|
+
session_id: str # UUID
|
|
107
|
+
request_id: str # Links to InferenceRequest
|
|
108
|
+
driver_node_id: str # Layer 0 (embedding)
|
|
109
|
+
|
|
110
|
+
# Pipeline participants (discovered dynamically via DHT)
|
|
111
|
+
worker_node_ids: List[str] = field(default_factory=list) # Layers 1-N
|
|
112
|
+
validator_node_id: Optional[str] = None # LM Head
|
|
113
|
+
|
|
114
|
+
# State tracking
|
|
115
|
+
current_layer: int = 0 # Which layer is processing now
|
|
116
|
+
tokens_generated: int = 0 # How many tokens generated so far
|
|
117
|
+
|
|
118
|
+
# Security: Verify pipeline integrity
|
|
119
|
+
activations_hashes: List[str] = field(default_factory=list) # Hash per layer
|
|
120
|
+
|
|
121
|
+
# Timestamps
|
|
122
|
+
started_at: float = 0.0
|
|
123
|
+
last_activity: float = 0.0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class InferenceMarket:
|
|
127
|
+
"""
|
|
128
|
+
Real-time market for inference pricing.
|
|
129
|
+
|
|
130
|
+
Price Discovery:
|
|
131
|
+
- Measures real-time supply (nodes offering capacity)
|
|
132
|
+
- Measures real-time demand (pending requests)
|
|
133
|
+
- Adjusts price to clear the market
|
|
134
|
+
|
|
135
|
+
Properties:
|
|
136
|
+
- Self-regulating (no admin intervention)
|
|
137
|
+
- Attack-resistant (real supply/demand signals)
|
|
138
|
+
- Training-dominant (low prices push nodes to train)
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
price_smoothing: float = 0.8, # Exponential moving average (prevents volatility)
|
|
144
|
+
capacity_timeout: int = 60, # Seconds before stale capacity expires
|
|
145
|
+
claim_timeout: int = 60, # Seconds before claimed request expires
|
|
146
|
+
base_price: float = 0.0001, # Bootstrap starting price (worthless model)
|
|
147
|
+
):
|
|
148
|
+
self.price_smoothing = price_smoothing
|
|
149
|
+
self.capacity_timeout = capacity_timeout
|
|
150
|
+
self.claim_timeout = claim_timeout
|
|
151
|
+
self.base_price = base_price
|
|
152
|
+
|
|
153
|
+
# REQUEST QUEUES (marketplace state)
|
|
154
|
+
self.pending_requests: Dict[str, InferenceRequest] = {} # Unclaimed requests
|
|
155
|
+
self.claimed_requests: Dict[str, InferenceRequest] = {} # Claimed but incomplete
|
|
156
|
+
self.completed_requests: Dict[str, InferenceRequest] = {} # Done (audit trail)
|
|
157
|
+
|
|
158
|
+
# RESULTS STORAGE (user retrieval)
|
|
159
|
+
self.results: Dict[str, str] = {} # request_id -> output_text
|
|
160
|
+
self.result_timestamps: Dict[str, float] = {} # For cleanup
|
|
161
|
+
|
|
162
|
+
# PIPELINE SESSIONS (distributed inference tracking)
|
|
163
|
+
self.active_sessions: Dict[str, PipelineSession] = {} # session_id -> session
|
|
164
|
+
self.completed_sessions: Dict[str, PipelineSession] = {} # Audit trail
|
|
165
|
+
|
|
166
|
+
# Capacity tracking
|
|
167
|
+
self.available_capacity: List[NodeCapacity] = []
|
|
168
|
+
|
|
169
|
+
# Price tracking
|
|
170
|
+
self.current_price = base_price # Start near zero (model has no value yet)
|
|
171
|
+
self.price_history = deque(maxlen=100)
|
|
172
|
+
|
|
173
|
+
# Security: Prevent replay attacks
|
|
174
|
+
self.request_nonces: Set[str] = set() # Track used request IDs
|
|
175
|
+
self.last_capacity_update: Dict[str, float] = {} # Rate limit capacity updates
|
|
176
|
+
|
|
177
|
+
# Threading
|
|
178
|
+
self.lock = threading.Lock()
|
|
179
|
+
|
|
180
|
+
# Metrics
|
|
181
|
+
self.total_tokens_processed = 0
|
|
182
|
+
self.total_requests_served = 0
|
|
183
|
+
self.price_updates = 0
|
|
184
|
+
|
|
185
|
+
logger.info("InferenceMarket initialized with request-response matching")
|
|
186
|
+
|
|
187
|
+
# ========================================================================
|
|
188
|
+
# SUPPLY MEASUREMENT
|
|
189
|
+
# ========================================================================
|
|
190
|
+
|
|
191
|
+
def register_capacity(
|
|
192
|
+
self,
|
|
193
|
+
node_id: str,
|
|
194
|
+
tokens_per_second: int,
|
|
195
|
+
min_price: float
|
|
196
|
+
):
|
|
197
|
+
"""
|
|
198
|
+
Node announces available inference capacity.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
node_id: Node identifier
|
|
202
|
+
tokens_per_second: How many tokens/sec this node can process
|
|
203
|
+
min_price: Minimum price node will accept (0 = any price)
|
|
204
|
+
|
|
205
|
+
Security:
|
|
206
|
+
- Rate limited to prevent spam
|
|
207
|
+
- Sanity check on capacity claims
|
|
208
|
+
- Expires after 60s (must refresh)
|
|
209
|
+
"""
|
|
210
|
+
with self.lock:
|
|
211
|
+
now = time.time()
|
|
212
|
+
|
|
213
|
+
# SECURITY: Rate limit capacity updates (max 1 per 30 seconds)
|
|
214
|
+
if node_id in self.last_capacity_update:
|
|
215
|
+
if (now - self.last_capacity_update[node_id]) < 30:
|
|
216
|
+
logger.warning(f"Capacity update too frequent from {node_id[:16]}...")
|
|
217
|
+
return # Silently ignore spam
|
|
218
|
+
|
|
219
|
+
# SECURITY: Sanity check on capacity claim
|
|
220
|
+
MAX_CAPACITY_SINGLE_NODE = 50000 # 50k tokens/sec (~high-end GPU)
|
|
221
|
+
if tokens_per_second > MAX_CAPACITY_SINGLE_NODE:
|
|
222
|
+
logger.warning(f"Unrealistic capacity claim from {node_id[:16]}...: "
|
|
223
|
+
f"{tokens_per_second} t/s (max {MAX_CAPACITY_SINGLE_NODE})")
|
|
224
|
+
tokens_per_second = MAX_CAPACITY_SINGLE_NODE # Cap it
|
|
225
|
+
|
|
226
|
+
# Remove old capacity from this node
|
|
227
|
+
self.available_capacity = [
|
|
228
|
+
c for c in self.available_capacity
|
|
229
|
+
if c.node_id != node_id
|
|
230
|
+
]
|
|
231
|
+
|
|
232
|
+
# Add new capacity
|
|
233
|
+
self.available_capacity.append(NodeCapacity(
|
|
234
|
+
node_id=node_id,
|
|
235
|
+
tokens_per_second=tokens_per_second,
|
|
236
|
+
min_price=min_price,
|
|
237
|
+
timestamp=now
|
|
238
|
+
))
|
|
239
|
+
|
|
240
|
+
# Track last update time
|
|
241
|
+
self.last_capacity_update[node_id] = now
|
|
242
|
+
|
|
243
|
+
# Trigger price update
|
|
244
|
+
self._update_market_price()
|
|
245
|
+
|
|
246
|
+
def withdraw_capacity(self, node_id: str):
|
|
247
|
+
"""Node withdraws from inference market (e.g., to focus on training)."""
|
|
248
|
+
with self.lock:
|
|
249
|
+
self.available_capacity = [
|
|
250
|
+
c for c in self.available_capacity
|
|
251
|
+
if c.node_id != node_id
|
|
252
|
+
]
|
|
253
|
+
self._update_market_price()
|
|
254
|
+
|
|
255
|
+
def get_total_supply(self) -> int:
|
|
256
|
+
"""
|
|
257
|
+
Calculate total available supply in tokens/sec.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Total network inference capacity (tokens/sec)
|
|
261
|
+
"""
|
|
262
|
+
now = time.time()
|
|
263
|
+
|
|
264
|
+
# Clean up stale capacity announcements
|
|
265
|
+
self.available_capacity = [
|
|
266
|
+
c for c in self.available_capacity
|
|
267
|
+
if (now - c.timestamp) < self.capacity_timeout
|
|
268
|
+
]
|
|
269
|
+
|
|
270
|
+
return sum(c.tokens_per_second for c in self.available_capacity)
|
|
271
|
+
|
|
272
|
+
# ========================================================================
|
|
273
|
+
# MARKETPLACE: REQUEST SUBMISSION
|
|
274
|
+
# ========================================================================
|
|
275
|
+
|
|
276
|
+
def submit_request(
|
|
277
|
+
self,
|
|
278
|
+
user_id: str,
|
|
279
|
+
driver_node_id: str,
|
|
280
|
+
tokens_requested: int,
|
|
281
|
+
max_price: float,
|
|
282
|
+
user_signature: str = "",
|
|
283
|
+
priority: int = 0
|
|
284
|
+
) -> Tuple[bool, str, float]:
|
|
285
|
+
"""
|
|
286
|
+
Submit inference request to marketplace (DISTRIBUTED INFERENCE).
|
|
287
|
+
|
|
288
|
+
PRIVACY: Prompt is NOT sent to marketplace!
|
|
289
|
+
User sends encrypted prompt DIRECTLY to driver node.
|
|
290
|
+
Marketplace only handles pricing and pipeline coordination.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
user_id: User submitting request
|
|
294
|
+
driver_node_id: Which driver node (Layer 0) will process
|
|
295
|
+
tokens_requested: Max tokens to generate
|
|
296
|
+
max_price: Max NEURO per 1M tokens user will pay
|
|
297
|
+
user_signature: ECDSA signature authorizing payment
|
|
298
|
+
priority: Request priority (higher = more urgent)
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
(success, request_id, locked_price)
|
|
302
|
+
|
|
303
|
+
Security:
|
|
304
|
+
- User signature validates authorization
|
|
305
|
+
- Nonce prevents replay attacks
|
|
306
|
+
- Price locked at current market rate
|
|
307
|
+
- Driver-specific (user chooses who sees prompt)
|
|
308
|
+
"""
|
|
309
|
+
try:
|
|
310
|
+
# Input validation
|
|
311
|
+
if not user_id or not driver_node_id:
|
|
312
|
+
logger.warning("Invalid request: missing user_id or driver_node_id")
|
|
313
|
+
return False, "", self.current_price
|
|
314
|
+
|
|
315
|
+
if tokens_requested <= 0:
|
|
316
|
+
logger.warning(f"Invalid tokens_requested: {tokens_requested}")
|
|
317
|
+
return False, "", self.current_price
|
|
318
|
+
|
|
319
|
+
if max_price < 0:
|
|
320
|
+
logger.warning(f"Invalid max_price: {max_price}")
|
|
321
|
+
return False, "", self.current_price
|
|
322
|
+
|
|
323
|
+
with self.lock:
|
|
324
|
+
# 1. Check current price vs user's max
|
|
325
|
+
current_price = self.current_price
|
|
326
|
+
if max_price < current_price:
|
|
327
|
+
logger.debug(f"Price too high for user: {current_price:.6f} > {max_price:.6f}")
|
|
328
|
+
return False, "", current_price # Price too high for user
|
|
329
|
+
|
|
330
|
+
# 2. Generate unique request ID
|
|
331
|
+
request_id = str(uuid.uuid4())
|
|
332
|
+
|
|
333
|
+
# 3. Check for replay attack
|
|
334
|
+
if request_id in self.request_nonces:
|
|
335
|
+
logger.warning(f"Duplicate request_id detected: {request_id}")
|
|
336
|
+
return False, "", current_price
|
|
337
|
+
|
|
338
|
+
# 4. Create request with LOCKED price (NO PROMPT - privacy!)
|
|
339
|
+
request = InferenceRequest(
|
|
340
|
+
request_id=request_id,
|
|
341
|
+
user_id=user_id,
|
|
342
|
+
driver_node_id=driver_node_id, # User chooses driver
|
|
343
|
+
tokens_requested=tokens_requested,
|
|
344
|
+
max_price=max_price,
|
|
345
|
+
locked_price=current_price, # LOCKED at submission time!
|
|
346
|
+
priority=priority,
|
|
347
|
+
timestamp=time.time(),
|
|
348
|
+
status=RequestStatus.PENDING,
|
|
349
|
+
user_signature=user_signature
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# 5. Add to pending queue
|
|
353
|
+
self.pending_requests[request_id] = request
|
|
354
|
+
self.request_nonces.add(request_id)
|
|
355
|
+
|
|
356
|
+
# 6. Update market price (demand increased)
|
|
357
|
+
self._update_market_price()
|
|
358
|
+
|
|
359
|
+
logger.info(f"Request {request_id[:8]}... submitted: "
|
|
360
|
+
f"{tokens_requested} tokens @ {current_price:.6f} NEURO/1M")
|
|
361
|
+
|
|
362
|
+
return True, request_id, current_price
|
|
363
|
+
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.error(f"Error submitting request: {e}")
|
|
366
|
+
return False, "", self.current_price
|
|
367
|
+
|
|
368
|
+
# ========================================================================
|
|
369
|
+
# MARKETPLACE: REQUEST CLAIMING
|
|
370
|
+
# ========================================================================
|
|
371
|
+
|
|
372
|
+
def claim_request(self, node_id: str) -> Optional[InferenceRequest]:
|
|
373
|
+
"""
|
|
374
|
+
Driver node claims a pending request to start distributed pipeline.
|
|
375
|
+
|
|
376
|
+
DISTRIBUTED INFERENCE:
|
|
377
|
+
- Only the specified driver node can claim
|
|
378
|
+
- Driver starts the pipeline (Layer 0)
|
|
379
|
+
- Workers and validators join via pipeline session
|
|
380
|
+
|
|
381
|
+
Args:
|
|
382
|
+
node_id: Driver node ID attempting to claim
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
InferenceRequest if claim successful, None if no requests available
|
|
386
|
+
|
|
387
|
+
Security:
|
|
388
|
+
- Atomic operation (thread-safe)
|
|
389
|
+
- Driver-specific (must match driver_node_id)
|
|
390
|
+
- Priority-based selection
|
|
391
|
+
"""
|
|
392
|
+
try:
|
|
393
|
+
if not node_id:
|
|
394
|
+
logger.warning("claim_request called with empty node_id")
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
with self.lock:
|
|
398
|
+
if not self.pending_requests:
|
|
399
|
+
return None # No requests available
|
|
400
|
+
|
|
401
|
+
# Find requests for THIS driver only
|
|
402
|
+
my_requests = [
|
|
403
|
+
r for r in self.pending_requests.values()
|
|
404
|
+
if r.driver_node_id == node_id
|
|
405
|
+
]
|
|
406
|
+
|
|
407
|
+
if not my_requests:
|
|
408
|
+
return None # No requests for this driver
|
|
409
|
+
|
|
410
|
+
# Select best request (highest priority, oldest first)
|
|
411
|
+
sorted_requests = sorted(
|
|
412
|
+
my_requests,
|
|
413
|
+
key=lambda r: (-r.priority, r.timestamp)
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
request = sorted_requests[0]
|
|
417
|
+
|
|
418
|
+
# Claim the request (atomic operation)
|
|
419
|
+
request.claimed_at = time.time()
|
|
420
|
+
request.claimed_by = node_id # Track who claimed it (for ledger verification)
|
|
421
|
+
request.status = RequestStatus.DRIVER_CLAIMED
|
|
422
|
+
|
|
423
|
+
# Generate pipeline session ID
|
|
424
|
+
request.pipeline_session_id = str(uuid.uuid4())
|
|
425
|
+
|
|
426
|
+
# Move from pending to claimed
|
|
427
|
+
self.claimed_requests[request.request_id] = request
|
|
428
|
+
del self.pending_requests[request.request_id]
|
|
429
|
+
|
|
430
|
+
logger.info(f"Request {request.request_id[:8]}... claimed by DRIVER {node_id[:16]}... "
|
|
431
|
+
f"(session {request.pipeline_session_id[:8]}...) at locked price {request.locked_price:.6f} NEURO/1M")
|
|
432
|
+
|
|
433
|
+
return request
|
|
434
|
+
|
|
435
|
+
except Exception as e:
|
|
436
|
+
logger.error(f"Error claiming request for node {node_id[:16] if node_id else 'unknown'}...: {e}")
|
|
437
|
+
return None
|
|
438
|
+
|
|
439
|
+
# ========================================================================
|
|
440
|
+
# MARKETPLACE: REQUEST COMPLETION
|
|
441
|
+
# ========================================================================
|
|
442
|
+
|
|
443
|
+
def register_proof_received(
|
|
444
|
+
self,
|
|
445
|
+
request_id: str,
|
|
446
|
+
node_id: str,
|
|
447
|
+
is_driver: bool,
|
|
448
|
+
is_validator: bool
|
|
449
|
+
) -> Tuple[bool, str]:
|
|
450
|
+
"""
|
|
451
|
+
Track that a node has submitted proof for this request (DISTRIBUTED).
|
|
452
|
+
|
|
453
|
+
In distributed inference, multiple nodes process the same request:
|
|
454
|
+
- Driver (Layer 0)
|
|
455
|
+
- Workers (Layers 1-N)
|
|
456
|
+
- Validator (LM Head)
|
|
457
|
+
|
|
458
|
+
Each submits their own proof. Request is complete when all proofs received.
|
|
459
|
+
|
|
460
|
+
Args:
|
|
461
|
+
request_id: Which request
|
|
462
|
+
node_id: Which node submitted proof
|
|
463
|
+
is_driver: Has embedding layer
|
|
464
|
+
is_validator: Has LM head layer
|
|
465
|
+
|
|
466
|
+
Returns:
|
|
467
|
+
(is_request_complete, error_message)
|
|
468
|
+
"""
|
|
469
|
+
try:
|
|
470
|
+
# Input validation
|
|
471
|
+
if not request_id:
|
|
472
|
+
return False, "Missing request_id"
|
|
473
|
+
if not node_id:
|
|
474
|
+
return False, "Missing node_id"
|
|
475
|
+
|
|
476
|
+
with self.lock:
|
|
477
|
+
# Look in both claimed and completed (proof might arrive late)
|
|
478
|
+
request = self.claimed_requests.get(request_id) or self.completed_requests.get(request_id)
|
|
479
|
+
|
|
480
|
+
if not request:
|
|
481
|
+
logger.warning(f"Proof received for unknown request {request_id[:8]}...")
|
|
482
|
+
return False, "Request not found"
|
|
483
|
+
|
|
484
|
+
# Update proof tracking
|
|
485
|
+
if is_driver:
|
|
486
|
+
request.driver_proof_received = True
|
|
487
|
+
logger.debug(f"DRIVER proof received for {request_id[:8]}...")
|
|
488
|
+
elif is_validator:
|
|
489
|
+
request.validator_proof_received = True
|
|
490
|
+
logger.debug(f"VALIDATOR proof received for {request_id[:8]}...")
|
|
491
|
+
else:
|
|
492
|
+
# Worker
|
|
493
|
+
if node_id not in request.worker_proofs_received:
|
|
494
|
+
request.worker_proofs_received.append(node_id)
|
|
495
|
+
logger.debug(f"WORKER proof received for {request_id[:8]}... from {node_id[:16]}...")
|
|
496
|
+
|
|
497
|
+
# Check if all proofs received (request complete)
|
|
498
|
+
# Minimum: driver + validator (workers optional if single-node has all layers)
|
|
499
|
+
all_proofs_received = (
|
|
500
|
+
request.driver_proof_received and
|
|
501
|
+
request.validator_proof_received
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if all_proofs_received and request.status != RequestStatus.COMPLETED:
|
|
505
|
+
# Mark as completed
|
|
506
|
+
request.status = RequestStatus.COMPLETED
|
|
507
|
+
request.completed_at = time.time()
|
|
508
|
+
|
|
509
|
+
# Move to completed queue
|
|
510
|
+
if request_id in self.claimed_requests:
|
|
511
|
+
self.completed_requests[request_id] = request
|
|
512
|
+
del self.claimed_requests[request_id]
|
|
513
|
+
|
|
514
|
+
# Update metrics
|
|
515
|
+
self.total_requests_served += 1
|
|
516
|
+
|
|
517
|
+
# Update market price (demand decreased)
|
|
518
|
+
self._update_market_price()
|
|
519
|
+
|
|
520
|
+
logger.info(f"Request {request_id[:8]}... COMPLETE: "
|
|
521
|
+
f"driver + {len(request.worker_proofs_received)} workers + validator")
|
|
522
|
+
|
|
523
|
+
return True, ""
|
|
524
|
+
|
|
525
|
+
return False, "" # Not yet complete
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
logger.error(f"Error registering proof for request {request_id[:8] if request_id else 'unknown'}...: {e}")
|
|
529
|
+
return False, str(e)
|
|
530
|
+
|
|
531
|
+
def get_request(self, request_id: str) -> Optional[InferenceRequest]:
|
|
532
|
+
"""Look up a request by ID (across all queues)."""
|
|
533
|
+
with self.lock:
|
|
534
|
+
if request_id in self.pending_requests:
|
|
535
|
+
return self.pending_requests[request_id]
|
|
536
|
+
elif request_id in self.claimed_requests:
|
|
537
|
+
return self.claimed_requests[request_id]
|
|
538
|
+
elif request_id in self.completed_requests:
|
|
539
|
+
return self.completed_requests[request_id]
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
def store_result(self, request_id: str, output_text: str):
|
|
543
|
+
"""
|
|
544
|
+
Store inference result for user retrieval.
|
|
545
|
+
|
|
546
|
+
Results are kept for 5 minutes after completion.
|
|
547
|
+
"""
|
|
548
|
+
with self.lock:
|
|
549
|
+
self.results[request_id] = output_text
|
|
550
|
+
self.result_timestamps[request_id] = time.time()
|
|
551
|
+
logger.info(f"Stored result for request {request_id[:8]}... ({len(output_text)} chars)")
|
|
552
|
+
|
|
553
|
+
def get_result(self, request_id: str) -> Optional[str]:
|
|
554
|
+
"""Retrieve inference result."""
|
|
555
|
+
with self.lock:
|
|
556
|
+
return self.results.get(request_id)
|
|
557
|
+
|
|
558
|
+
def cleanup_old_results(self):
|
|
559
|
+
"""Remove results older than 5 minutes."""
|
|
560
|
+
with self.lock:
|
|
561
|
+
now = time.time()
|
|
562
|
+
to_remove = [
|
|
563
|
+
req_id for req_id, ts in self.result_timestamps.items()
|
|
564
|
+
if (now - ts) > 300 # 5 minutes
|
|
565
|
+
]
|
|
566
|
+
for req_id in to_remove:
|
|
567
|
+
self.results.pop(req_id, None)
|
|
568
|
+
self.result_timestamps.pop(req_id, None)
|
|
569
|
+
|
|
570
|
+
if to_remove:
|
|
571
|
+
logger.info(f"Cleaned up {len(to_remove)} old results")
|
|
572
|
+
|
|
573
|
+
# ========================================================================
|
|
574
|
+
# PIPELINE SESSION MANAGEMENT (Distributed Inference)
|
|
575
|
+
# ========================================================================
|
|
576
|
+
|
|
577
|
+
def start_pipeline_session(
|
|
578
|
+
self,
|
|
579
|
+
request_id: str,
|
|
580
|
+
session_id: str,
|
|
581
|
+
driver_node_id: str
|
|
582
|
+
) -> bool:
|
|
583
|
+
"""
|
|
584
|
+
Start a distributed inference pipeline session.
|
|
585
|
+
|
|
586
|
+
Called by driver node after claiming request.
|
|
587
|
+
Workers and validator will join dynamically via DHT.
|
|
588
|
+
|
|
589
|
+
Args:
|
|
590
|
+
request_id: Which request this session serves
|
|
591
|
+
session_id: Unique session identifier
|
|
592
|
+
driver_node_id: Driver node starting the pipeline
|
|
593
|
+
|
|
594
|
+
Returns:
|
|
595
|
+
True if session started successfully
|
|
596
|
+
"""
|
|
597
|
+
with self.lock:
|
|
598
|
+
if session_id in self.active_sessions:
|
|
599
|
+
logger.warning(f"Session {session_id[:8]}... already exists")
|
|
600
|
+
return False
|
|
601
|
+
|
|
602
|
+
session = PipelineSession(
|
|
603
|
+
session_id=session_id,
|
|
604
|
+
request_id=request_id,
|
|
605
|
+
driver_node_id=driver_node_id,
|
|
606
|
+
started_at=time.time(),
|
|
607
|
+
last_activity=time.time()
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
self.active_sessions[session_id] = session
|
|
611
|
+
|
|
612
|
+
logger.info(f"Pipeline session {session_id[:8]}... started for request {request_id[:8]}...")
|
|
613
|
+
return True
|
|
614
|
+
|
|
615
|
+
def update_pipeline_progress(
|
|
616
|
+
self,
|
|
617
|
+
session_id: str,
|
|
618
|
+
current_layer: int,
|
|
619
|
+
tokens_generated: int = 0
|
|
620
|
+
):
|
|
621
|
+
"""Update pipeline progress (called by processing nodes)."""
|
|
622
|
+
with self.lock:
|
|
623
|
+
session = self.active_sessions.get(session_id)
|
|
624
|
+
if session:
|
|
625
|
+
session.current_layer = current_layer
|
|
626
|
+
session.tokens_generated = tokens_generated
|
|
627
|
+
session.last_activity = time.time()
|
|
628
|
+
|
|
629
|
+
def register_pipeline_participant(
|
|
630
|
+
self,
|
|
631
|
+
session_id: str,
|
|
632
|
+
node_id: str,
|
|
633
|
+
is_worker: bool = False,
|
|
634
|
+
is_validator: bool = False
|
|
635
|
+
):
|
|
636
|
+
"""Register a node as participant in the pipeline."""
|
|
637
|
+
with self.lock:
|
|
638
|
+
session = self.active_sessions.get(session_id)
|
|
639
|
+
if not session:
|
|
640
|
+
logger.warning(f"Session {session_id[:8]}... not found")
|
|
641
|
+
return
|
|
642
|
+
|
|
643
|
+
if is_worker and node_id not in session.worker_node_ids:
|
|
644
|
+
session.worker_node_ids.append(node_id)
|
|
645
|
+
logger.debug(f"Worker {node_id[:16]}... joined session {session_id[:8]}...")
|
|
646
|
+
elif is_validator:
|
|
647
|
+
session.validator_node_id = node_id
|
|
648
|
+
logger.debug(f"Validator {node_id[:16]}... joined session {session_id[:8]}...")
|
|
649
|
+
|
|
650
|
+
def complete_pipeline_session(self, session_id: str):
|
|
651
|
+
"""Mark pipeline session as complete."""
|
|
652
|
+
with self.lock:
|
|
653
|
+
session = self.active_sessions.get(session_id)
|
|
654
|
+
if session:
|
|
655
|
+
self.completed_sessions[session_id] = session
|
|
656
|
+
del self.active_sessions[session_id]
|
|
657
|
+
logger.info(f"Pipeline session {session_id[:8]}... completed")
|
|
658
|
+
|
|
659
|
+
def get_session(self, session_id: str) -> Optional[PipelineSession]:
|
|
660
|
+
"""Look up a pipeline session."""
|
|
661
|
+
with self.lock:
|
|
662
|
+
return (self.active_sessions.get(session_id) or
|
|
663
|
+
self.completed_sessions.get(session_id))
|
|
664
|
+
|
|
665
|
+
# ========================================================================
|
|
666
|
+
# DEMAND MEASUREMENT (updated for marketplace)
|
|
667
|
+
# ========================================================================
|
|
668
|
+
|
|
669
|
+
def get_total_demand(self) -> int:
|
|
670
|
+
"""
|
|
671
|
+
Calculate total pending demand in tokens.
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
Total tokens waiting to be processed (pending + claimed but incomplete)
|
|
675
|
+
"""
|
|
676
|
+
# Include both pending AND claimed (still being processed)
|
|
677
|
+
pending_tokens = sum(r.tokens_requested for r in self.pending_requests.values())
|
|
678
|
+
claimed_tokens = sum(r.tokens_requested for r in self.claimed_requests.values())
|
|
679
|
+
return pending_tokens + claimed_tokens
|
|
680
|
+
|
|
681
|
+
# ========================================================================
|
|
682
|
+
# PRICE DISCOVERY
|
|
683
|
+
# ========================================================================
|
|
684
|
+
|
|
685
|
+
def _update_market_price(self):
|
|
686
|
+
"""
|
|
687
|
+
Update market price based on PURE supply/demand (no artificial caps).
|
|
688
|
+
|
|
689
|
+
Price Formula (Pure Market):
|
|
690
|
+
P = base_price × (1 + utilization)^2
|
|
691
|
+
|
|
692
|
+
Where:
|
|
693
|
+
utilization = demand_rate / supply_rate
|
|
694
|
+
base_price = Starting price for worthless model (0.0001)
|
|
695
|
+
|
|
696
|
+
Natural Dynamics:
|
|
697
|
+
- No demand → Price ≈ 0 (stupid model = worthless)
|
|
698
|
+
- Low util (0.1) → Price ≈ 0.00012 (cheap, encourages usage)
|
|
699
|
+
- Balanced (1.0) → Price ≈ 0.0004 (fair value)
|
|
700
|
+
- High util (10) → Price ≈ 0.012 (scarcity premium)
|
|
701
|
+
- Extreme (100) → Price ≈ 1.01 (severe scarcity)
|
|
702
|
+
|
|
703
|
+
Quality Emerges Naturally:
|
|
704
|
+
- Stupid model → Users don't use it → Demand = 0 → Price ≈ 0
|
|
705
|
+
- Good model → Users want it → Demand ↑ → Price ↑
|
|
706
|
+
- Excellent model → Viral usage → Demand >> Supply → Price spikes
|
|
707
|
+
|
|
708
|
+
No Caps Needed:
|
|
709
|
+
- Market finds true value
|
|
710
|
+
- High price attracts supply (nodes switch from training)
|
|
711
|
+
- Equilibrium naturally forms where: inference_profit ≈ training_profit
|
|
712
|
+
"""
|
|
713
|
+
supply = self.get_total_supply() # tokens/sec
|
|
714
|
+
demand = self.get_total_demand() # total tokens waiting
|
|
715
|
+
|
|
716
|
+
if supply == 0:
|
|
717
|
+
if demand == 0:
|
|
718
|
+
# No supply, no demand → Keep current price
|
|
719
|
+
new_price = self.current_price
|
|
720
|
+
else:
|
|
721
|
+
# No supply but there's demand → Extreme scarcity
|
|
722
|
+
# Price spikes to attract supply (this is CORRECT market behavior!)
|
|
723
|
+
new_price = self.current_price * 10.0 # Dramatic signal
|
|
724
|
+
else:
|
|
725
|
+
# Calculate demand rate (tokens/sec needed to clear queue)
|
|
726
|
+
# Using TARGET_RESPONSE_TIME from economics
|
|
727
|
+
from neuroshard.core.economics.constants import INFERENCE_MARKET_TARGET_RESPONSE_TIME
|
|
728
|
+
demand_rate = demand / INFERENCE_MARKET_TARGET_RESPONSE_TIME
|
|
729
|
+
|
|
730
|
+
# Utilization ratio (pure supply/demand)
|
|
731
|
+
utilization = demand_rate / supply
|
|
732
|
+
|
|
733
|
+
# PURE MARKET FORMULA: Price grows with utilization squared
|
|
734
|
+
# This creates natural equilibrium:
|
|
735
|
+
# - Low utilization → Cheap (encourages usage)
|
|
736
|
+
# - High utilization → Expensive (attracts supply)
|
|
737
|
+
# - Quadratic ensures rapid price response to scarcity
|
|
738
|
+
new_price = self.base_price * math.pow(1 + utilization, 2)
|
|
739
|
+
|
|
740
|
+
# Smooth price changes (exponential moving average)
|
|
741
|
+
# Higher smoothing = more stability, less volatility
|
|
742
|
+
self.current_price = (
|
|
743
|
+
self.price_smoothing * self.current_price +
|
|
744
|
+
(1 - self.price_smoothing) * new_price
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Record for analysis
|
|
748
|
+
self.price_history.append({
|
|
749
|
+
'timestamp': time.time(),
|
|
750
|
+
'price': self.current_price,
|
|
751
|
+
'supply': supply,
|
|
752
|
+
'demand': demand,
|
|
753
|
+
'utilization': demand_rate / supply if supply > 0 else 0
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
self.price_updates += 1
|
|
757
|
+
|
|
758
|
+
def get_current_price(self) -> float:
|
|
759
|
+
"""
|
|
760
|
+
Get current market price for inference.
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
NEURO per 1M tokens (current market rate)
|
|
764
|
+
"""
|
|
765
|
+
with self.lock:
|
|
766
|
+
return self.current_price
|
|
767
|
+
|
|
768
|
+
# ========================================================================
|
|
769
|
+
# SECURITY: TIMEOUT HANDLING
|
|
770
|
+
# ========================================================================
|
|
771
|
+
|
|
772
|
+
def cleanup_stale_claims(self) -> int:
|
|
773
|
+
"""
|
|
774
|
+
Re-queue requests that were claimed but not completed (timeout).
|
|
775
|
+
|
|
776
|
+
This prevents malicious nodes from "squatting" on requests.
|
|
777
|
+
Called periodically by background task.
|
|
778
|
+
|
|
779
|
+
Returns:
|
|
780
|
+
Number of requests re-queued
|
|
781
|
+
"""
|
|
782
|
+
with self.lock:
|
|
783
|
+
now = time.time()
|
|
784
|
+
expired_requests = []
|
|
785
|
+
|
|
786
|
+
# Find expired claims
|
|
787
|
+
for req_id, request in self.claimed_requests.items():
|
|
788
|
+
try:
|
|
789
|
+
# Safety check for claimed_at
|
|
790
|
+
if request.claimed_at is None:
|
|
791
|
+
logger.warning(f"Request {req_id[:8]}... has no claimed_at timestamp, marking expired")
|
|
792
|
+
expired_requests.append(req_id)
|
|
793
|
+
elif (now - request.claimed_at) > self.claim_timeout:
|
|
794
|
+
expired_requests.append(req_id)
|
|
795
|
+
except Exception as e:
|
|
796
|
+
logger.error(f"Error checking claim expiry for {req_id[:8]}...: {e}")
|
|
797
|
+
|
|
798
|
+
# Re-queue them
|
|
799
|
+
for req_id in expired_requests:
|
|
800
|
+
try:
|
|
801
|
+
request = self.claimed_requests[req_id]
|
|
802
|
+
|
|
803
|
+
# Store info before reset
|
|
804
|
+
squatter_node_id = request.claimed_by or "unknown"
|
|
805
|
+
claim_duration = (now - request.claimed_at) if request.claimed_at else 0
|
|
806
|
+
|
|
807
|
+
# Reset claim state
|
|
808
|
+
request.status = RequestStatus.PENDING
|
|
809
|
+
request.claimed_by = None
|
|
810
|
+
request.claimed_at = None
|
|
811
|
+
request.pipeline_session_id = None
|
|
812
|
+
|
|
813
|
+
# Move back to pending
|
|
814
|
+
self.pending_requests[req_id] = request
|
|
815
|
+
del self.claimed_requests[req_id]
|
|
816
|
+
|
|
817
|
+
logger.warning(f"Request {req_id[:8]}... timed out "
|
|
818
|
+
f"(claimed by {squatter_node_id[:16]}... for {claim_duration:.1f}s), "
|
|
819
|
+
f"re-queued")
|
|
820
|
+
except Exception as e:
|
|
821
|
+
logger.error(f"Error re-queuing request {req_id[:8]}...: {e}")
|
|
822
|
+
|
|
823
|
+
return len(expired_requests)
|
|
824
|
+
|
|
825
|
+
def prune_completed_requests(self, max_age_seconds: int = 3600):
|
|
826
|
+
"""
|
|
827
|
+
Remove old completed requests from audit trail.
|
|
828
|
+
|
|
829
|
+
Keeps memory usage bounded while maintaining recent history.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
max_age_seconds: How long to keep completed requests (default 1 hour)
|
|
833
|
+
"""
|
|
834
|
+
with self.lock:
|
|
835
|
+
now = time.time()
|
|
836
|
+
to_remove = []
|
|
837
|
+
|
|
838
|
+
for req_id, request in self.completed_requests.items():
|
|
839
|
+
if (now - request.completed_at) > max_age_seconds:
|
|
840
|
+
to_remove.append(req_id)
|
|
841
|
+
|
|
842
|
+
for req_id in to_remove:
|
|
843
|
+
del self.completed_requests[req_id]
|
|
844
|
+
# Also remove from nonce set to allow reuse after long time
|
|
845
|
+
self.request_nonces.discard(req_id)
|
|
846
|
+
|
|
847
|
+
if to_remove:
|
|
848
|
+
logger.debug(f"Pruned {len(to_remove)} old completed requests")
|
|
849
|
+
|
|
850
|
+
# ========================================================================
|
|
851
|
+
# ANALYTICS
|
|
852
|
+
# ========================================================================
|
|
853
|
+
|
|
854
|
+
def get_market_stats(self) -> dict:
|
|
855
|
+
"""
|
|
856
|
+
Get current marketplace statistics.
|
|
857
|
+
|
|
858
|
+
Returns:
|
|
859
|
+
Dict with price, supply, demand, queue sizes, etc.
|
|
860
|
+
"""
|
|
861
|
+
with self.lock:
|
|
862
|
+
supply = self.get_total_supply()
|
|
863
|
+
demand = self.get_total_demand()
|
|
864
|
+
|
|
865
|
+
return {
|
|
866
|
+
# Price
|
|
867
|
+
'current_price': round(self.current_price, 6),
|
|
868
|
+
|
|
869
|
+
# Supply & Demand
|
|
870
|
+
'supply_tokens_per_sec': supply,
|
|
871
|
+
'demand_tokens_waiting': demand,
|
|
872
|
+
'utilization': (demand / 60) / supply if supply > 0 else 0,
|
|
873
|
+
|
|
874
|
+
# Marketplace queues
|
|
875
|
+
'pending_requests': len(self.pending_requests),
|
|
876
|
+
'claimed_requests': len(self.claimed_requests),
|
|
877
|
+
'completed_requests': len(self.completed_requests),
|
|
878
|
+
|
|
879
|
+
# Pipeline sessions (distributed inference)
|
|
880
|
+
'active_sessions': len(self.active_sessions),
|
|
881
|
+
'completed_sessions': len(self.completed_sessions),
|
|
882
|
+
'pending_requests': len(self.pending_requests),
|
|
883
|
+
'available_nodes': len(self.available_capacity),
|
|
884
|
+
'total_processed': self.total_tokens_processed,
|
|
885
|
+
'total_served': self.total_requests_served,
|
|
886
|
+
'avg_price_24h': self._get_avg_price(24 * 3600)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
def _get_avg_price(self, seconds: int) -> float:
|
|
890
|
+
"""Calculate average price over last N seconds."""
|
|
891
|
+
if not self.price_history:
|
|
892
|
+
return self.current_price
|
|
893
|
+
|
|
894
|
+
cutoff = time.time() - seconds
|
|
895
|
+
recent = [p['price'] for p in self.price_history if p['timestamp'] > cutoff]
|
|
896
|
+
|
|
897
|
+
return sum(recent) / len(recent) if recent else self.current_price
|
|
898
|
+
|
|
899
|
+
def get_price_chart(self, last_n: int = 100) -> List[dict]:
|
|
900
|
+
"""Get recent price history for charting."""
|
|
901
|
+
with self.lock:
|
|
902
|
+
return list(self.price_history)[-last_n:]
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
# ============================================================================
|
|
906
|
+
# INTEGRATION WITH LEDGER
|
|
907
|
+
# ============================================================================
|
|
908
|
+
|
|
909
|
+
def calculate_inference_reward(
|
|
910
|
+
market: InferenceMarket,
|
|
911
|
+
tokens_processed: int
|
|
912
|
+
) -> float:
|
|
913
|
+
"""
|
|
914
|
+
Calculate inference reward based on current market price.
|
|
915
|
+
|
|
916
|
+
This replaces the fixed INFERENCE_REWARD_PER_MILLION.
|
|
917
|
+
|
|
918
|
+
Args:
|
|
919
|
+
market: InferenceMarket instance
|
|
920
|
+
tokens_processed: Number of tokens processed
|
|
921
|
+
|
|
922
|
+
Returns:
|
|
923
|
+
NEURO reward based on market rate
|
|
924
|
+
"""
|
|
925
|
+
market_price = market.get_current_price()
|
|
926
|
+
reward = (tokens_processed / 1_000_000.0) * market_price
|
|
927
|
+
return reward
|
|
928
|
+
|
|
929
|
+
|
|
930
|
+
# ============================================================================
|
|
931
|
+
# EXAMPLE USAGE
|
|
932
|
+
# ============================================================================
|
|
933
|
+
|
|
934
|
+
if __name__ == "__main__":
|
|
935
|
+
# Setup logging for demo
|
|
936
|
+
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
|
937
|
+
|
|
938
|
+
# Create market
|
|
939
|
+
market = InferenceMarket()
|
|
940
|
+
|
|
941
|
+
# Scenario 1: Low demand (bootstrap phase)
|
|
942
|
+
logger.info("\n=== Scenario 1: Low Demand ===")
|
|
943
|
+
market.register_capacity("node1", tokens_per_second=1000, min_price=0.01)
|
|
944
|
+
market.register_capacity("node2", tokens_per_second=1000, min_price=0.01)
|
|
945
|
+
market.submit_request("user1", "driver1", tokens_requested=10000, max_price=1.0)
|
|
946
|
+
|
|
947
|
+
stats = market.get_market_stats()
|
|
948
|
+
logger.info(f"Supply: {stats['supply_tokens_per_sec']} t/s")
|
|
949
|
+
logger.info(f"Demand: {stats['demand_tokens_waiting']} tokens")
|
|
950
|
+
logger.info(f"Price: {stats['current_price']:.4f} NEURO per 1M tokens")
|
|
951
|
+
logger.info(f"Utilization: {stats['utilization']*100:.1f}%")
|
|
952
|
+
|
|
953
|
+
# Scenario 2: High demand
|
|
954
|
+
logger.info("\n=== Scenario 2: High Demand ===")
|
|
955
|
+
for i in range(10):
|
|
956
|
+
market.submit_request(f"user{i+2}", "driver1",
|
|
957
|
+
tokens_requested=50000, max_price=1.0)
|
|
958
|
+
|
|
959
|
+
stats = market.get_market_stats()
|
|
960
|
+
logger.info(f"Supply: {stats['supply_tokens_per_sec']} t/s")
|
|
961
|
+
logger.info(f"Demand: {stats['demand_tokens_waiting']} tokens")
|
|
962
|
+
logger.info(f"Price: {stats['current_price']:.4f} NEURO per 1M tokens")
|
|
963
|
+
logger.info(f"Utilization: {stats['utilization']*100:.1f}%")
|
|
964
|
+
|
|
965
|
+
# Scenario 3: Supply increases (more nodes join)
|
|
966
|
+
logger.info("\n=== Scenario 3: Supply Increases ===")
|
|
967
|
+
for i in range(10):
|
|
968
|
+
market.register_capacity(f"node{i+3}", tokens_per_second=1000, min_price=0.01)
|
|
969
|
+
|
|
970
|
+
stats = market.get_market_stats()
|
|
971
|
+
logger.info(f"Supply: {stats['supply_tokens_per_sec']} t/s")
|
|
972
|
+
logger.info(f"Demand: {stats['demand_tokens_waiting']} tokens")
|
|
973
|
+
logger.info(f"Price: {stats['current_price']:.4f} NEURO per 1M tokens")
|
|
974
|
+
logger.info(f"Utilization: {stats['utilization']*100:.1f}%")
|
|
975
|
+
|