@ruvector/edge-net 0.5.0 → 0.5.3

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.
@@ -0,0 +1,548 @@
1
+ /**
2
+ * @ruvector/edge-net Model Utilities
3
+ *
4
+ * Helper functions for model management, optimization, and deployment.
5
+ *
6
+ * @module @ruvector/edge-net/models/utils
7
+ */
8
+
9
+ import { createHash, randomBytes } from 'crypto';
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, createReadStream } from 'fs';
11
+ import { join, dirname } from 'path';
12
+ import { homedir } from 'os';
13
+ import { pipeline } from 'stream/promises';
14
+ import { fileURLToPath } from 'url';
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = dirname(__filename);
18
+
19
+ // ============================================
20
+ // CONFIGURATION
21
+ // ============================================
22
+
23
+ export const DEFAULT_CACHE_DIR = process.env.ONNX_CACHE_DIR ||
24
+ join(homedir(), '.ruvector', 'models', 'onnx');
25
+
26
+ export const REGISTRY_PATH = join(__dirname, 'registry.json');
27
+
28
+ export const GCS_CONFIG = {
29
+ bucket: process.env.GCS_MODEL_BUCKET || 'ruvector-models',
30
+ projectId: process.env.GCS_PROJECT_ID || 'ruvector',
31
+ };
32
+
33
+ export const IPFS_CONFIG = {
34
+ gateway: process.env.IPFS_GATEWAY || 'https://ipfs.io/ipfs',
35
+ pinataApiKey: process.env.PINATA_API_KEY,
36
+ pinataSecret: process.env.PINATA_SECRET,
37
+ };
38
+
39
+ // ============================================
40
+ // REGISTRY MANAGEMENT
41
+ // ============================================
42
+
43
+ /**
44
+ * Load the model registry
45
+ * @returns {Object} Registry object
46
+ */
47
+ export function loadRegistry() {
48
+ try {
49
+ if (existsSync(REGISTRY_PATH)) {
50
+ return JSON.parse(readFileSync(REGISTRY_PATH, 'utf-8'));
51
+ }
52
+ } catch (error) {
53
+ console.error('[Registry] Failed to load:', error.message);
54
+ }
55
+ return { version: '1.0.0', models: {}, profiles: {}, adapters: {} };
56
+ }
57
+
58
+ /**
59
+ * Save the model registry
60
+ * @param {Object} registry - Registry object to save
61
+ */
62
+ export function saveRegistry(registry) {
63
+ registry.updated = new Date().toISOString();
64
+ writeFileSync(REGISTRY_PATH, JSON.stringify(registry, null, 2));
65
+ }
66
+
67
+ /**
68
+ * Get a model from the registry
69
+ * @param {string} modelId - Model identifier
70
+ * @returns {Object|null} Model metadata or null
71
+ */
72
+ export function getModel(modelId) {
73
+ const registry = loadRegistry();
74
+ return registry.models[modelId] || null;
75
+ }
76
+
77
+ /**
78
+ * Get a deployment profile
79
+ * @param {string} profileId - Profile identifier
80
+ * @returns {Object|null} Profile configuration or null
81
+ */
82
+ export function getProfile(profileId) {
83
+ const registry = loadRegistry();
84
+ return registry.profiles[profileId] || null;
85
+ }
86
+
87
+ // ============================================
88
+ // FILE UTILITIES
89
+ // ============================================
90
+
91
+ /**
92
+ * Format bytes to human-readable size
93
+ * @param {number} bytes - Size in bytes
94
+ * @returns {string} Formatted size string
95
+ */
96
+ export function formatSize(bytes) {
97
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
98
+ let size = bytes;
99
+ let unitIndex = 0;
100
+ while (size >= 1024 && unitIndex < units.length - 1) {
101
+ size /= 1024;
102
+ unitIndex++;
103
+ }
104
+ return `${size.toFixed(1)}${units[unitIndex]}`;
105
+ }
106
+
107
+ /**
108
+ * Parse size string to bytes
109
+ * @param {string} sizeStr - Size string like "100MB"
110
+ * @returns {number} Size in bytes
111
+ */
112
+ export function parseSize(sizeStr) {
113
+ const units = { 'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4 };
114
+ const match = sizeStr.match(/^([\d.]+)\s*(B|KB|MB|GB|TB)?$/i);
115
+ if (!match) return 0;
116
+ const value = parseFloat(match[1]);
117
+ const unit = (match[2] || 'B').toUpperCase();
118
+ return value * (units[unit] || 1);
119
+ }
120
+
121
+ /**
122
+ * Calculate SHA256 hash of a file
123
+ * @param {string} filePath - Path to file
124
+ * @returns {Promise<string>} Hex-encoded hash
125
+ */
126
+ export async function hashFile(filePath) {
127
+ const hash = createHash('sha256');
128
+ const stream = createReadStream(filePath);
129
+
130
+ return new Promise((resolve, reject) => {
131
+ stream.on('data', (data) => hash.update(data));
132
+ stream.on('end', () => resolve(hash.digest('hex')));
133
+ stream.on('error', reject);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Calculate SHA256 hash of a buffer
139
+ * @param {Buffer} buffer - Data buffer
140
+ * @returns {string} Hex-encoded hash
141
+ */
142
+ export function hashBuffer(buffer) {
143
+ return createHash('sha256').update(buffer).digest('hex');
144
+ }
145
+
146
+ /**
147
+ * Get the cache directory for a model
148
+ * @param {string} modelId - HuggingFace model ID
149
+ * @returns {string} Cache directory path
150
+ */
151
+ export function getModelCacheDir(modelId) {
152
+ return join(DEFAULT_CACHE_DIR, modelId.replace(/\//g, '--'));
153
+ }
154
+
155
+ /**
156
+ * Check if a model is cached locally
157
+ * @param {string} modelId - Model identifier
158
+ * @returns {boolean} True if cached
159
+ */
160
+ export function isModelCached(modelId) {
161
+ const model = getModel(modelId);
162
+ if (!model) return false;
163
+ const cacheDir = getModelCacheDir(model.huggingface);
164
+ return existsSync(cacheDir);
165
+ }
166
+
167
+ /**
168
+ * Get cached model size
169
+ * @param {string} modelId - Model identifier
170
+ * @returns {number} Size in bytes or 0
171
+ */
172
+ export function getCachedModelSize(modelId) {
173
+ const model = getModel(modelId);
174
+ if (!model) return 0;
175
+ const cacheDir = getModelCacheDir(model.huggingface);
176
+ if (!existsSync(cacheDir)) return 0;
177
+ return getDirectorySize(cacheDir);
178
+ }
179
+
180
+ /**
181
+ * Get directory size recursively
182
+ * @param {string} dir - Directory path
183
+ * @returns {number} Total size in bytes
184
+ */
185
+ export function getDirectorySize(dir) {
186
+ let size = 0;
187
+ try {
188
+ const { readdirSync } = require('fs');
189
+ const entries = readdirSync(dir, { withFileTypes: true });
190
+ for (const entry of entries) {
191
+ const fullPath = join(dir, entry.name);
192
+ if (entry.isDirectory()) {
193
+ size += getDirectorySize(fullPath);
194
+ } else {
195
+ size += statSync(fullPath).size;
196
+ }
197
+ }
198
+ } catch (error) {
199
+ // Ignore errors
200
+ }
201
+ return size;
202
+ }
203
+
204
+ // ============================================
205
+ // MODEL OPTIMIZATION
206
+ // ============================================
207
+
208
+ /**
209
+ * Quantization configurations
210
+ */
211
+ export const QUANTIZATION_CONFIGS = {
212
+ int4: {
213
+ bits: 4,
214
+ blockSize: 32,
215
+ expectedReduction: 0.25, // 4x smaller
216
+ description: 'Aggressive quantization, some quality loss',
217
+ },
218
+ int8: {
219
+ bits: 8,
220
+ blockSize: 128,
221
+ expectedReduction: 0.5, // 2x smaller
222
+ description: 'Balanced quantization, minimal quality loss',
223
+ },
224
+ fp16: {
225
+ bits: 16,
226
+ blockSize: null,
227
+ expectedReduction: 0.5, // 2x smaller than fp32
228
+ description: 'Half precision, no quality loss',
229
+ },
230
+ fp32: {
231
+ bits: 32,
232
+ blockSize: null,
233
+ expectedReduction: 1.0, // No change
234
+ description: 'Full precision, original quality',
235
+ },
236
+ };
237
+
238
+ /**
239
+ * Estimate quantized model size
240
+ * @param {string} modelId - Model identifier
241
+ * @param {string} quantType - Quantization type
242
+ * @returns {number} Estimated size in bytes
243
+ */
244
+ export function estimateQuantizedSize(modelId, quantType) {
245
+ const model = getModel(modelId);
246
+ if (!model) return 0;
247
+
248
+ const originalSize = parseSize(model.size);
249
+ const config = QUANTIZATION_CONFIGS[quantType] || QUANTIZATION_CONFIGS.fp32;
250
+
251
+ return Math.floor(originalSize * config.expectedReduction);
252
+ }
253
+
254
+ /**
255
+ * Get recommended quantization for a device profile
256
+ * @param {Object} deviceProfile - Device capabilities
257
+ * @returns {string} Recommended quantization type
258
+ */
259
+ export function getRecommendedQuantization(deviceProfile) {
260
+ const { memory, isEdge, requiresSpeed } = deviceProfile;
261
+
262
+ if (memory < 512 * 1024 * 1024) { // < 512MB
263
+ return 'int4';
264
+ } else if (memory < 2 * 1024 * 1024 * 1024 || isEdge) { // < 2GB or edge
265
+ return 'int8';
266
+ } else if (requiresSpeed) {
267
+ return 'fp16';
268
+ }
269
+ return 'fp32';
270
+ }
271
+
272
+ // ============================================
273
+ // DOWNLOAD UTILITIES
274
+ // ============================================
275
+
276
+ /**
277
+ * Download progress callback type
278
+ * @callback ProgressCallback
279
+ * @param {Object} progress - Progress information
280
+ * @param {number} progress.loaded - Bytes loaded
281
+ * @param {number} progress.total - Total bytes
282
+ * @param {string} progress.file - Current file name
283
+ */
284
+
285
+ /**
286
+ * Download a file with progress reporting
287
+ * @param {string} url - URL to download
288
+ * @param {string} destPath - Destination path
289
+ * @param {ProgressCallback} [onProgress] - Progress callback
290
+ * @returns {Promise<string>} Downloaded file path
291
+ */
292
+ export async function downloadFile(url, destPath, onProgress) {
293
+ const destDir = dirname(destPath);
294
+ if (!existsSync(destDir)) {
295
+ mkdirSync(destDir, { recursive: true });
296
+ }
297
+
298
+ const response = await fetch(url);
299
+ if (!response.ok) {
300
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
301
+ }
302
+
303
+ const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
304
+ let loadedSize = 0;
305
+
306
+ const { createWriteStream } = await import('fs');
307
+ const fileStream = createWriteStream(destPath);
308
+ const reader = response.body.getReader();
309
+
310
+ try {
311
+ while (true) {
312
+ const { done, value } = await reader.read();
313
+ if (done) break;
314
+
315
+ fileStream.write(value);
316
+ loadedSize += value.length;
317
+
318
+ if (onProgress) {
319
+ onProgress({
320
+ loaded: loadedSize,
321
+ total: totalSize,
322
+ file: destPath,
323
+ });
324
+ }
325
+ }
326
+ } finally {
327
+ fileStream.end();
328
+ }
329
+
330
+ return destPath;
331
+ }
332
+
333
+ // ============================================
334
+ // IPFS UTILITIES
335
+ // ============================================
336
+
337
+ /**
338
+ * Pin a file to IPFS via Pinata
339
+ * @param {string} filePath - Path to file to pin
340
+ * @param {Object} metadata - Metadata for the pin
341
+ * @returns {Promise<string>} IPFS CID
342
+ */
343
+ export async function pinToIPFS(filePath, metadata = {}) {
344
+ if (!IPFS_CONFIG.pinataApiKey || !IPFS_CONFIG.pinataSecret) {
345
+ throw new Error('Pinata API credentials not configured');
346
+ }
347
+
348
+ const FormData = (await import('form-data')).default;
349
+ const form = new FormData();
350
+
351
+ form.append('file', createReadStream(filePath));
352
+ form.append('pinataMetadata', JSON.stringify({
353
+ name: metadata.name || filePath,
354
+ keyvalues: metadata,
355
+ }));
356
+
357
+ const response = await fetch('https://api.pinata.cloud/pinning/pinFileToIPFS', {
358
+ method: 'POST',
359
+ headers: {
360
+ 'pinata_api_key': IPFS_CONFIG.pinataApiKey,
361
+ 'pinata_secret_api_key': IPFS_CONFIG.pinataSecret,
362
+ },
363
+ body: form,
364
+ });
365
+
366
+ if (!response.ok) {
367
+ throw new Error(`Pinata error: ${response.statusText}`);
368
+ }
369
+
370
+ const result = await response.json();
371
+ return result.IpfsHash;
372
+ }
373
+
374
+ /**
375
+ * Get IPFS gateway URL for a CID
376
+ * @param {string} cid - IPFS CID
377
+ * @returns {string} Gateway URL
378
+ */
379
+ export function getIPFSUrl(cid) {
380
+ return `${IPFS_CONFIG.gateway}/${cid}`;
381
+ }
382
+
383
+ // ============================================
384
+ // GCS UTILITIES
385
+ // ============================================
386
+
387
+ /**
388
+ * Generate GCS URL for a model
389
+ * @param {string} modelId - Model identifier
390
+ * @param {string} fileName - File name
391
+ * @returns {string} GCS URL
392
+ */
393
+ export function getGCSUrl(modelId, fileName) {
394
+ return `https://storage.googleapis.com/${GCS_CONFIG.bucket}/${modelId}/${fileName}`;
395
+ }
396
+
397
+ /**
398
+ * Check if a model exists in GCS
399
+ * @param {string} modelId - Model identifier
400
+ * @param {string} fileName - File name
401
+ * @returns {Promise<boolean>} True if exists
402
+ */
403
+ export async function checkGCSExists(modelId, fileName) {
404
+ const url = getGCSUrl(modelId, fileName);
405
+ try {
406
+ const response = await fetch(url, { method: 'HEAD' });
407
+ return response.ok;
408
+ } catch {
409
+ return false;
410
+ }
411
+ }
412
+
413
+ // ============================================
414
+ // ADAPTER UTILITIES
415
+ // ============================================
416
+
417
+ /**
418
+ * MicroLoRA adapter configuration
419
+ */
420
+ export const LORA_DEFAULTS = {
421
+ rank: 8,
422
+ alpha: 16,
423
+ dropout: 0.1,
424
+ targetModules: ['q_proj', 'v_proj'],
425
+ };
426
+
427
+ /**
428
+ * Create adapter metadata
429
+ * @param {string} name - Adapter name
430
+ * @param {string} baseModel - Base model identifier
431
+ * @param {Object} options - Training options
432
+ * @returns {Object} Adapter metadata
433
+ */
434
+ export function createAdapterMetadata(name, baseModel, options = {}) {
435
+ return {
436
+ id: `${name}-${randomBytes(4).toString('hex')}`,
437
+ name,
438
+ baseModel,
439
+ rank: options.rank || LORA_DEFAULTS.rank,
440
+ alpha: options.alpha || LORA_DEFAULTS.alpha,
441
+ targetModules: options.targetModules || LORA_DEFAULTS.targetModules,
442
+ created: new Date().toISOString(),
443
+ size: null, // Set after training
444
+ };
445
+ }
446
+
447
+ /**
448
+ * Get adapter save path
449
+ * @param {string} adapterName - Adapter name
450
+ * @returns {string} Save path
451
+ */
452
+ export function getAdapterPath(adapterName) {
453
+ return join(DEFAULT_CACHE_DIR, 'adapters', adapterName);
454
+ }
455
+
456
+ // ============================================
457
+ // BENCHMARK UTILITIES
458
+ // ============================================
459
+
460
+ /**
461
+ * Create a benchmark result object
462
+ * @param {string} modelId - Model identifier
463
+ * @param {number[]} times - Latency measurements in ms
464
+ * @returns {Object} Benchmark results
465
+ */
466
+ export function createBenchmarkResult(modelId, times) {
467
+ times.sort((a, b) => a - b);
468
+
469
+ return {
470
+ model: modelId,
471
+ timestamp: new Date().toISOString(),
472
+ iterations: times.length,
473
+ stats: {
474
+ avg: times.reduce((a, b) => a + b, 0) / times.length,
475
+ median: times[Math.floor(times.length / 2)],
476
+ p95: times[Math.floor(times.length * 0.95)],
477
+ p99: times[Math.floor(times.length * 0.99)],
478
+ min: times[0],
479
+ max: times[times.length - 1],
480
+ stddev: calculateStdDev(times),
481
+ },
482
+ rawTimes: times,
483
+ };
484
+ }
485
+
486
+ /**
487
+ * Calculate standard deviation
488
+ * @param {number[]} values - Array of values
489
+ * @returns {number} Standard deviation
490
+ */
491
+ function calculateStdDev(values) {
492
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
493
+ const squareDiffs = values.map(v => Math.pow(v - mean, 2));
494
+ const avgSquareDiff = squareDiffs.reduce((a, b) => a + b, 0) / squareDiffs.length;
495
+ return Math.sqrt(avgSquareDiff);
496
+ }
497
+
498
+ // ============================================
499
+ // EXPORTS
500
+ // ============================================
501
+
502
+ export default {
503
+ // Configuration
504
+ DEFAULT_CACHE_DIR,
505
+ REGISTRY_PATH,
506
+ GCS_CONFIG,
507
+ IPFS_CONFIG,
508
+ QUANTIZATION_CONFIGS,
509
+ LORA_DEFAULTS,
510
+
511
+ // Registry
512
+ loadRegistry,
513
+ saveRegistry,
514
+ getModel,
515
+ getProfile,
516
+
517
+ // Files
518
+ formatSize,
519
+ parseSize,
520
+ hashFile,
521
+ hashBuffer,
522
+ getModelCacheDir,
523
+ isModelCached,
524
+ getCachedModelSize,
525
+ getDirectorySize,
526
+
527
+ // Optimization
528
+ estimateQuantizedSize,
529
+ getRecommendedQuantization,
530
+
531
+ // Download
532
+ downloadFile,
533
+
534
+ // IPFS
535
+ pinToIPFS,
536
+ getIPFSUrl,
537
+
538
+ // GCS
539
+ getGCSUrl,
540
+ checkGCSExists,
541
+
542
+ // Adapters
543
+ createAdapterMetadata,
544
+ getAdapterPath,
545
+
546
+ // Benchmarks
547
+ createBenchmarkResult,
548
+ };