@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,1298 @@
1
+ /**
2
+ * MicroLoRA - Lightweight LoRA Customization SDK for End-User Model Adaptation
3
+ *
4
+ * Enables browser-based fine-tuning of small LLMs using Low-Rank Adaptation (LoRA).
5
+ * Optimized for edge deployment with minimal memory footprint.
6
+ *
7
+ * @module @ruvector/edge-net/models/microlora
8
+ *
9
+ * @example
10
+ * ```javascript
11
+ * import { MicroLoRA } from '@ruvector/edge-net/models';
12
+ *
13
+ * const lora = new MicroLoRA('phi-1.5-int4', { rank: 8 });
14
+ * await lora.train([
15
+ * { input: 'translate to python', output: 'def main():' },
16
+ * { input: 'write a loop', output: 'for i in range(10):' }
17
+ * ], { epochs: 10, lr: 1e-4 });
18
+ *
19
+ * const result = await lora.generate('write a function');
20
+ * console.log(result.text);
21
+ *
22
+ * await lora.saveAdapter('my-code-adapter');
23
+ * ```
24
+ */
25
+
26
+ import { EventEmitter } from 'events';
27
+ import { createHash, randomBytes } from 'crypto';
28
+
29
+ // ============================================
30
+ // TYPE DEFINITIONS (JSDoc)
31
+ // ============================================
32
+
33
+ /**
34
+ * @typedef {Object} MicroLoRAConfig
35
+ * @property {number} [rank=4] - LoRA rank (dimension of low-rank matrices)
36
+ * @property {number} [alpha=8] - LoRA alpha (scaling factor)
37
+ * @property {number} [dropout=0.05] - Dropout rate during training
38
+ * @property {string[]} [targetModules=['query', 'value']] - Modules to adapt
39
+ * @property {boolean} [quantized=true] - Use quantized weights
40
+ * @property {number} [embeddingDim=384] - Embedding dimension
41
+ */
42
+
43
+ /**
44
+ * @typedef {Object} TrainingExample
45
+ * @property {string} input - Input text/prompt
46
+ * @property {string} output - Expected output
47
+ * @property {number} [quality=1.0] - Example quality weight
48
+ * @property {Object} [metadata] - Optional metadata
49
+ */
50
+
51
+ /**
52
+ * @typedef {Object} TrainingOptions
53
+ * @property {number} [epochs=10] - Number of training epochs
54
+ * @property {number} [lr=1e-4] - Learning rate
55
+ * @property {number} [batchSize=4] - Training batch size
56
+ * @property {string} [scheduler='cosine'] - LR scheduler type
57
+ * @property {number} [warmupSteps=10] - Warmup steps
58
+ * @property {boolean} [useEWC=false] - Use EWC for continual learning
59
+ * @property {number} [ewcLambda=1000] - EWC regularization strength
60
+ * @property {boolean} [gradientCheckpointing=true] - Memory optimization
61
+ * @property {Function} [onProgress] - Progress callback
62
+ */
63
+
64
+ /**
65
+ * @typedef {Object} GenerationOptions
66
+ * @property {number} [maxTokens=64] - Maximum tokens to generate
67
+ * @property {number} [temperature=0.7] - Sampling temperature
68
+ * @property {number} [topP=0.9] - Top-p sampling
69
+ * @property {number} [topK=50] - Top-k sampling
70
+ * @property {number} [repetitionPenalty=1.1] - Repetition penalty
71
+ */
72
+
73
+ /**
74
+ * @typedef {Object} AdapterMetadata
75
+ * @property {string} id - Unique adapter ID
76
+ * @property {string} name - Human-readable name
77
+ * @property {string} description - Adapter description
78
+ * @property {string} baseModel - Base model used
79
+ * @property {string} domain - Domain category
80
+ * @property {number} rank - LoRA rank
81
+ * @property {number} alpha - LoRA alpha
82
+ * @property {number} trainingSamples - Number of training samples
83
+ * @property {number} trainingEpochs - Training epochs
84
+ * @property {number} createdAt - Creation timestamp
85
+ * @property {string} version - Adapter version
86
+ */
87
+
88
+ // ============================================
89
+ // CONSTANTS
90
+ // ============================================
91
+
92
+ /**
93
+ * Supported base models for MicroLoRA
94
+ */
95
+ export const SUPPORTED_MODELS = {
96
+ 'phi-1.5-int4': {
97
+ id: 'Xenova/phi-1_5',
98
+ name: 'Phi-1.5 INT4',
99
+ size: '~280MB',
100
+ embeddingDim: 2048,
101
+ hiddenDim: 2048,
102
+ numLayers: 24,
103
+ capabilities: ['code', 'reasoning', 'math'],
104
+ },
105
+ 'phi-2-int4': {
106
+ id: 'Xenova/phi-2',
107
+ name: 'Phi-2 INT4',
108
+ size: '~550MB',
109
+ embeddingDim: 2560,
110
+ hiddenDim: 2560,
111
+ numLayers: 32,
112
+ capabilities: ['code', 'reasoning', 'math', 'general'],
113
+ },
114
+ 'distilgpt2': {
115
+ id: 'Xenova/distilgpt2',
116
+ name: 'DistilGPT-2',
117
+ size: '~82MB',
118
+ embeddingDim: 768,
119
+ hiddenDim: 768,
120
+ numLayers: 6,
121
+ capabilities: ['general', 'completion'],
122
+ },
123
+ 'gpt2': {
124
+ id: 'Xenova/gpt2',
125
+ name: 'GPT-2',
126
+ size: '~250MB',
127
+ embeddingDim: 768,
128
+ hiddenDim: 768,
129
+ numLayers: 12,
130
+ capabilities: ['general', 'completion', 'creative'],
131
+ },
132
+ 'starcoder-tiny': {
133
+ id: 'Xenova/tiny_starcoder_py',
134
+ name: 'StarCoder Tiny',
135
+ size: '~40MB',
136
+ embeddingDim: 768,
137
+ hiddenDim: 768,
138
+ numLayers: 6,
139
+ capabilities: ['code', 'python'],
140
+ },
141
+ 'qwen-0.5b': {
142
+ id: 'Xenova/Qwen1.5-0.5B',
143
+ name: 'Qwen 0.5B',
144
+ size: '~430MB',
145
+ embeddingDim: 1024,
146
+ hiddenDim: 2816,
147
+ numLayers: 24,
148
+ capabilities: ['multilingual', 'general', 'code'],
149
+ },
150
+ };
151
+
152
+ /**
153
+ * Default MicroLoRA configuration
154
+ */
155
+ const DEFAULT_CONFIG = {
156
+ rank: 4,
157
+ alpha: 8,
158
+ dropout: 0.05,
159
+ targetModules: ['query', 'value', 'key', 'dense'],
160
+ quantized: true,
161
+ embeddingDim: 384,
162
+ };
163
+
164
+ // ============================================
165
+ // MICROLORA CLASS
166
+ // ============================================
167
+
168
+ /**
169
+ * MicroLoRA - End-user model adaptation SDK
170
+ *
171
+ * Provides a complete workflow for fine-tuning small LLMs in the browser
172
+ * using Low-Rank Adaptation. Optimized for edge computing with support
173
+ * for gradient checkpointing, EWC continual learning, and ONNX export.
174
+ *
175
+ * @extends EventEmitter
176
+ *
177
+ * @example
178
+ * ```javascript
179
+ * // Initialize with model and config
180
+ * const lora = new MicroLoRA('phi-1.5-int4', { rank: 8, alpha: 16 });
181
+ *
182
+ * // Train on examples
183
+ * await lora.train([
184
+ * { input: 'Hello', output: 'World' },
185
+ * { input: 'Goodbye', output: 'Friend' },
186
+ * ], { epochs: 5 });
187
+ *
188
+ * // Generate with adapter
189
+ * const result = await lora.generate('Hello there');
190
+ *
191
+ * // Save and share
192
+ * await lora.saveAdapter('./my-adapter.json');
193
+ * ```
194
+ */
195
+ export class MicroLoRA extends EventEmitter {
196
+ /**
197
+ * Create a MicroLoRA instance
198
+ *
199
+ * @param {string} baseModel - Base model identifier (e.g., 'phi-1.5-int4')
200
+ * @param {MicroLoRAConfig} [config={}] - LoRA configuration
201
+ */
202
+ constructor(baseModel, config = {}) {
203
+ super();
204
+
205
+ this.id = `microlora-${randomBytes(6).toString('hex')}`;
206
+ this.baseModelKey = baseModel;
207
+ this.baseModel = SUPPORTED_MODELS[baseModel] || {
208
+ id: baseModel,
209
+ name: baseModel,
210
+ embeddingDim: config.embeddingDim || 384,
211
+ hiddenDim: config.embeddingDim || 384,
212
+ numLayers: 12,
213
+ capabilities: ['general'],
214
+ };
215
+
216
+ /** @type {MicroLoRAConfig} */
217
+ this.config = {
218
+ ...DEFAULT_CONFIG,
219
+ embeddingDim: this.baseModel.embeddingDim,
220
+ ...config,
221
+ };
222
+
223
+ // Initialize adapter weights
224
+ this.adapters = this._initializeAdapters();
225
+
226
+ // Training state
227
+ this.trainingState = null;
228
+ this.isTraining = false;
229
+
230
+ // EWC state for continual learning
231
+ this.ewcState = null;
232
+
233
+ // Inference pipeline
234
+ this.pipeline = null;
235
+ this.initialized = false;
236
+
237
+ // Statistics
238
+ this.stats = {
239
+ totalTrainingSamples: 0,
240
+ totalTrainingTime: 0,
241
+ totalInferences: 0,
242
+ corrections: 0,
243
+ adaptations: 0,
244
+ };
245
+
246
+ // Metadata for adapter
247
+ this.metadata = {
248
+ id: this.id,
249
+ name: 'Untitled Adapter',
250
+ description: '',
251
+ baseModel: this.baseModelKey,
252
+ domain: 'general',
253
+ rank: this.config.rank,
254
+ alpha: this.config.alpha,
255
+ trainingSamples: 0,
256
+ trainingEpochs: 0,
257
+ createdAt: Date.now(),
258
+ version: '1.0.0',
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Initialize LoRA adapter matrices for each target module
264
+ * @private
265
+ * @returns {Map<string, Object>} Adapter weights per module
266
+ */
267
+ _initializeAdapters() {
268
+ const adapters = new Map();
269
+ const inputDim = this.config.embeddingDim;
270
+ const outputDim = this.config.embeddingDim;
271
+ const rank = this.config.rank;
272
+
273
+ for (const moduleName of this.config.targetModules) {
274
+ adapters.set(moduleName, {
275
+ // A: (inputDim x rank) - initialized with small Gaussian
276
+ loraA: this._kaiming(inputDim, rank),
277
+ // B: (rank x outputDim) - initialized to zero
278
+ loraB: this._zeros(rank, outputDim),
279
+ // Scaling factor
280
+ scaling: this.config.alpha / this.config.rank,
281
+ });
282
+ }
283
+
284
+ return adapters;
285
+ }
286
+
287
+ /**
288
+ * Kaiming/He initialization for matrix
289
+ * @private
290
+ */
291
+ _kaiming(rows, cols) {
292
+ const matrix = [];
293
+ const std = Math.sqrt(2 / (rows + cols));
294
+ for (let i = 0; i < rows; i++) {
295
+ const row = [];
296
+ for (let j = 0; j < cols; j++) {
297
+ row.push(this._gaussianRandom() * std);
298
+ }
299
+ matrix.push(row);
300
+ }
301
+ return matrix;
302
+ }
303
+
304
+ /**
305
+ * Zero-initialized matrix
306
+ * @private
307
+ */
308
+ _zeros(rows, cols) {
309
+ return Array(rows).fill(null).map(() => Array(cols).fill(0));
310
+ }
311
+
312
+ /**
313
+ * Gaussian random number (Box-Muller transform)
314
+ * @private
315
+ */
316
+ _gaussianRandom() {
317
+ const u1 = Math.random();
318
+ const u2 = Math.random();
319
+ return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
320
+ }
321
+
322
+ // ============================================
323
+ // TRAINING METHODS
324
+ // ============================================
325
+
326
+ /**
327
+ * Train the adapter on provided examples
328
+ *
329
+ * @param {TrainingExample[]} examples - Training examples
330
+ * @param {TrainingOptions} [options={}] - Training options
331
+ * @returns {Promise<Object>} Training result with metrics
332
+ *
333
+ * @example
334
+ * ```javascript
335
+ * const result = await lora.train([
336
+ * { input: 'translate to python', output: 'def main():' },
337
+ * { input: 'write a loop', output: 'for i in range(10):' }
338
+ * ], {
339
+ * epochs: 10,
340
+ * lr: 1e-4,
341
+ * batchSize: 4,
342
+ * onProgress: (progress) => console.log(`${progress.epoch}/${progress.totalEpochs}`)
343
+ * });
344
+ * console.log(`Final loss: ${result.finalLoss}`);
345
+ * ```
346
+ */
347
+ async train(examples, options = {}) {
348
+ if (this.isTraining) {
349
+ throw new Error('Training already in progress');
350
+ }
351
+
352
+ const opts = {
353
+ epochs: 10,
354
+ lr: 1e-4,
355
+ batchSize: 4,
356
+ scheduler: 'cosine',
357
+ warmupSteps: 10,
358
+ useEWC: false,
359
+ ewcLambda: 1000,
360
+ gradientCheckpointing: true,
361
+ onProgress: null,
362
+ ...options,
363
+ };
364
+
365
+ this.isTraining = true;
366
+ this.emit('training:start', { examples: examples.length, options: opts });
367
+
368
+ const startTime = Date.now();
369
+ const lossHistory = [];
370
+ const totalSteps = Math.ceil(examples.length / opts.batchSize) * opts.epochs;
371
+ let currentStep = 0;
372
+
373
+ try {
374
+ // Preprocess examples
375
+ const processedExamples = await this._preprocessExamples(examples);
376
+
377
+ // Initialize EWC if enabled and we have prior state
378
+ if (opts.useEWC && this.ewcState) {
379
+ this.emit('training:ewc', { lambda: opts.ewcLambda });
380
+ }
381
+
382
+ // Training loop
383
+ for (let epoch = 0; epoch < opts.epochs; epoch++) {
384
+ const epochLosses = [];
385
+
386
+ // Shuffle examples
387
+ const shuffled = [...processedExamples].sort(() => Math.random() - 0.5);
388
+
389
+ // Process batches
390
+ for (let i = 0; i < shuffled.length; i += opts.batchSize) {
391
+ const batch = shuffled.slice(i, i + opts.batchSize);
392
+ currentStep++;
393
+
394
+ // Compute learning rate with scheduler
395
+ const lr = this._computeLearningRate(opts, currentStep, totalSteps);
396
+
397
+ // Forward and backward pass
398
+ const batchLoss = await this._trainBatch(batch, lr, opts);
399
+ epochLosses.push(batchLoss);
400
+
401
+ // EWC regularization
402
+ if (opts.useEWC && this.ewcState) {
403
+ this._applyEWCPenalty(opts.ewcLambda, lr);
404
+ }
405
+
406
+ // Progress callback
407
+ if (opts.onProgress) {
408
+ opts.onProgress({
409
+ epoch,
410
+ totalEpochs: opts.epochs,
411
+ step: currentStep,
412
+ totalSteps,
413
+ loss: batchLoss,
414
+ lr,
415
+ });
416
+ }
417
+
418
+ this.emit('training:step', {
419
+ epoch,
420
+ step: currentStep,
421
+ loss: batchLoss,
422
+ lr,
423
+ });
424
+ }
425
+
426
+ const epochLoss = epochLosses.reduce((a, b) => a + b, 0) / epochLosses.length;
427
+ lossHistory.push(epochLoss);
428
+
429
+ this.emit('training:epoch', {
430
+ epoch,
431
+ loss: epochLoss,
432
+ lossHistory,
433
+ });
434
+ }
435
+
436
+ // Update EWC state for continual learning
437
+ if (opts.useEWC) {
438
+ this.ewcState = this._computeEWCState(processedExamples);
439
+ }
440
+
441
+ // Update stats
442
+ this.stats.totalTrainingSamples += examples.length;
443
+ this.stats.totalTrainingTime += Date.now() - startTime;
444
+ this.metadata.trainingSamples = this.stats.totalTrainingSamples;
445
+ this.metadata.trainingEpochs += opts.epochs;
446
+
447
+ const result = {
448
+ finalLoss: lossHistory[lossHistory.length - 1],
449
+ lossHistory,
450
+ trainingTime: Date.now() - startTime,
451
+ totalSteps: currentStep,
452
+ examples: examples.length,
453
+ epochs: opts.epochs,
454
+ };
455
+
456
+ this.emit('training:complete', result);
457
+ return result;
458
+
459
+ } finally {
460
+ this.isTraining = false;
461
+ }
462
+ }
463
+
464
+ /**
465
+ * Train on a single correction (online learning)
466
+ *
467
+ * @param {string} input - Original input
468
+ * @param {string} wrongOutput - Incorrect model output
469
+ * @param {string} correctOutput - User-provided correction
470
+ * @returns {Promise<Object>} Training result
471
+ *
472
+ * @example
473
+ * ```javascript
474
+ * // User corrects a model mistake
475
+ * await lora.trainOnCorrection(
476
+ * 'write hello world',
477
+ * 'print(hello world)', // Wrong output
478
+ * 'print("hello world")' // Correct output
479
+ * );
480
+ * ```
481
+ */
482
+ async trainOnCorrection(input, wrongOutput, correctOutput) {
483
+ this.stats.corrections++;
484
+
485
+ const result = await this.train([
486
+ { input, output: correctOutput, quality: 1.5 }, // Upweight corrections
487
+ ], {
488
+ epochs: 3,
489
+ lr: 5e-4, // Higher LR for quick adaptation
490
+ useEWC: true, // Preserve prior knowledge
491
+ });
492
+
493
+ this.emit('correction:applied', {
494
+ input,
495
+ wrongOutput,
496
+ correctOutput,
497
+ result,
498
+ });
499
+
500
+ return result;
501
+ }
502
+
503
+ /**
504
+ * Preprocess training examples into embeddings
505
+ * @private
506
+ */
507
+ async _preprocessExamples(examples) {
508
+ const processed = [];
509
+
510
+ for (const example of examples) {
511
+ // Generate input embedding (simple hash-based for now)
512
+ const inputEmb = this._hashEmbed(example.input);
513
+ const outputEmb = this._hashEmbed(example.output);
514
+
515
+ processed.push({
516
+ input: example.input,
517
+ output: example.output,
518
+ inputEmb,
519
+ outputEmb,
520
+ quality: example.quality || 1.0,
521
+ });
522
+ }
523
+
524
+ return processed;
525
+ }
526
+
527
+ /**
528
+ * Hash-based embedding (fallback when no model loaded)
529
+ * @private
530
+ */
531
+ _hashEmbed(text) {
532
+ const dim = this.config.embeddingDim;
533
+ const hash = createHash('sha256').update(text).digest();
534
+ const embedding = new Float32Array(dim);
535
+
536
+ // Deterministic pseudo-random from hash
537
+ for (let i = 0; i < dim; i++) {
538
+ embedding[i] = (hash[i % 32] - 128) / 128;
539
+ }
540
+
541
+ // Add character-level features
542
+ for (let i = 0; i < text.length && i < dim; i++) {
543
+ embedding[i] += (text.charCodeAt(i) - 64) / 256;
544
+ }
545
+
546
+ // Normalize
547
+ let norm = 0;
548
+ for (let i = 0; i < dim; i++) {
549
+ norm += embedding[i] * embedding[i];
550
+ }
551
+ norm = Math.sqrt(norm) || 1;
552
+ for (let i = 0; i < dim; i++) {
553
+ embedding[i] /= norm;
554
+ }
555
+
556
+ return Array.from(embedding);
557
+ }
558
+
559
+ /**
560
+ * Compute learning rate with scheduler
561
+ * @private
562
+ */
563
+ _computeLearningRate(opts, step, totalSteps) {
564
+ const baseLR = opts.lr;
565
+
566
+ // Warmup phase
567
+ if (step < opts.warmupSteps) {
568
+ return baseLR * (step / opts.warmupSteps);
569
+ }
570
+
571
+ const decayStep = step - opts.warmupSteps;
572
+ const decayTotal = totalSteps - opts.warmupSteps;
573
+
574
+ switch (opts.scheduler) {
575
+ case 'constant':
576
+ return baseLR;
577
+ case 'linear':
578
+ return baseLR * (1 - decayStep / decayTotal);
579
+ case 'cosine':
580
+ return baseLR * 0.5 * (1 + Math.cos(Math.PI * decayStep / decayTotal));
581
+ case 'exponential':
582
+ return baseLR * Math.pow(0.9, decayStep / 100);
583
+ default:
584
+ return baseLR;
585
+ }
586
+ }
587
+
588
+ /**
589
+ * Train on a single batch
590
+ * @private
591
+ */
592
+ async _trainBatch(batch, lr, opts) {
593
+ let totalLoss = 0;
594
+
595
+ for (const example of batch) {
596
+ // Forward pass through adapters
597
+ const adapted = this._forwardAdapters(example.inputEmb);
598
+
599
+ // Compute loss (MSE between adapted output and target embedding)
600
+ let loss = 0;
601
+ for (let i = 0; i < adapted.length && i < example.outputEmb.length; i++) {
602
+ const diff = adapted[i] - example.outputEmb[i];
603
+ loss += diff * diff;
604
+ }
605
+ loss = (loss / adapted.length) * example.quality;
606
+
607
+ // Backward pass (gradient descent on adapter weights)
608
+ this._backwardAdapters(example.inputEmb, example.outputEmb, lr, opts);
609
+
610
+ totalLoss += loss;
611
+ }
612
+
613
+ return totalLoss / batch.length;
614
+ }
615
+
616
+ /**
617
+ * Forward pass through all adapters
618
+ * @private
619
+ */
620
+ _forwardAdapters(input) {
621
+ let output = [...input];
622
+
623
+ for (const [, adapter] of this.adapters) {
624
+ output = this._forwardSingleAdapter(output, adapter);
625
+ }
626
+
627
+ return output;
628
+ }
629
+
630
+ /**
631
+ * Forward through a single adapter
632
+ * @private
633
+ */
634
+ _forwardSingleAdapter(input, adapter) {
635
+ const rank = this.config.rank;
636
+ const dim = Math.min(input.length, this.config.embeddingDim);
637
+
638
+ // input @ A (dim -> rank)
639
+ const hidden = new Float64Array(rank);
640
+ for (let r = 0; r < rank; r++) {
641
+ let sum = 0;
642
+ for (let d = 0; d < dim; d++) {
643
+ sum += input[d] * adapter.loraA[d][r];
644
+ }
645
+ hidden[r] = sum;
646
+ }
647
+
648
+ // hidden @ B (rank -> dim) + residual
649
+ const output = [...input];
650
+ for (let d = 0; d < dim; d++) {
651
+ let delta = 0;
652
+ for (let r = 0; r < rank; r++) {
653
+ delta += hidden[r] * adapter.loraB[r][d];
654
+ }
655
+ output[d] += adapter.scaling * delta;
656
+ }
657
+
658
+ return output;
659
+ }
660
+
661
+ /**
662
+ * Backward pass through all adapters
663
+ * @private
664
+ */
665
+ _backwardAdapters(input, target, lr, opts) {
666
+ for (const [, adapter] of this.adapters) {
667
+ this._backwardSingleAdapter(input, target, adapter, lr);
668
+ }
669
+ }
670
+
671
+ /**
672
+ * Backward through a single adapter
673
+ * @private
674
+ */
675
+ _backwardSingleAdapter(input, target, adapter, lr) {
676
+ const rank = this.config.rank;
677
+ const dim = Math.min(input.length, this.config.embeddingDim);
678
+
679
+ // Compute forward pass for gradient computation
680
+ const hidden = new Float64Array(rank);
681
+ for (let r = 0; r < rank; r++) {
682
+ for (let d = 0; d < dim; d++) {
683
+ hidden[r] += input[d] * adapter.loraA[d][r];
684
+ }
685
+ }
686
+
687
+ // Compute adapted output
688
+ const adapted = [...input];
689
+ for (let d = 0; d < dim; d++) {
690
+ for (let r = 0; r < rank; r++) {
691
+ adapted[d] += adapter.scaling * hidden[r] * adapter.loraB[r][d];
692
+ }
693
+ }
694
+
695
+ // Compute output gradient (MSE derivative)
696
+ const gradOutput = adapted.map((val, i) =>
697
+ 2 * (val - (target[i] || 0)) / dim
698
+ );
699
+
700
+ // Gradient for B: hidden^T @ gradOutput
701
+ for (let r = 0; r < rank; r++) {
702
+ for (let d = 0; d < dim; d++) {
703
+ const grad = hidden[r] * gradOutput[d] * adapter.scaling;
704
+ adapter.loraB[r][d] -= lr * grad;
705
+ }
706
+ }
707
+
708
+ // Gradient for hidden: gradOutput @ B^T
709
+ const gradHidden = new Float64Array(rank);
710
+ for (let r = 0; r < rank; r++) {
711
+ for (let d = 0; d < dim; d++) {
712
+ gradHidden[r] += gradOutput[d] * adapter.loraB[r][d] * adapter.scaling;
713
+ }
714
+ }
715
+
716
+ // Gradient for A: input^T @ gradHidden
717
+ for (let d = 0; d < dim; d++) {
718
+ for (let r = 0; r < rank; r++) {
719
+ const grad = input[d] * gradHidden[r];
720
+ adapter.loraA[d][r] -= lr * grad;
721
+ }
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Compute EWC state (Fisher information matrix approximation)
727
+ * @private
728
+ */
729
+ _computeEWCState(examples) {
730
+ const state = {
731
+ fisherDiag: new Map(),
732
+ optimalParams: new Map(),
733
+ };
734
+
735
+ for (const [name, adapter] of this.adapters) {
736
+ // Store optimal parameters
737
+ state.optimalParams.set(name, {
738
+ loraA: adapter.loraA.map(row => [...row]),
739
+ loraB: adapter.loraB.map(row => [...row]),
740
+ });
741
+
742
+ // Approximate Fisher diagonal with gradient squares
743
+ const fisherA = this._zeros(adapter.loraA.length, adapter.loraA[0].length);
744
+ const fisherB = this._zeros(adapter.loraB.length, adapter.loraB[0].length);
745
+
746
+ // Accumulate gradients squared
747
+ for (const example of examples) {
748
+ const grads = this._computeGradients(example.inputEmb, example.outputEmb, adapter);
749
+ for (let i = 0; i < fisherA.length; i++) {
750
+ for (let j = 0; j < fisherA[0].length; j++) {
751
+ fisherA[i][j] += grads.gradA[i][j] * grads.gradA[i][j];
752
+ }
753
+ }
754
+ for (let i = 0; i < fisherB.length; i++) {
755
+ for (let j = 0; j < fisherB[0].length; j++) {
756
+ fisherB[i][j] += grads.gradB[i][j] * grads.gradB[i][j];
757
+ }
758
+ }
759
+ }
760
+
761
+ // Normalize
762
+ const n = examples.length;
763
+ for (let i = 0; i < fisherA.length; i++) {
764
+ for (let j = 0; j < fisherA[0].length; j++) {
765
+ fisherA[i][j] /= n;
766
+ }
767
+ }
768
+ for (let i = 0; i < fisherB.length; i++) {
769
+ for (let j = 0; j < fisherB[0].length; j++) {
770
+ fisherB[i][j] /= n;
771
+ }
772
+ }
773
+
774
+ state.fisherDiag.set(name, { fisherA, fisherB });
775
+ }
776
+
777
+ return state;
778
+ }
779
+
780
+ /**
781
+ * Compute gradients for a single example
782
+ * @private
783
+ */
784
+ _computeGradients(input, target, adapter) {
785
+ const rank = this.config.rank;
786
+ const dim = Math.min(input.length, this.config.embeddingDim);
787
+
788
+ // Forward pass
789
+ const hidden = new Float64Array(rank);
790
+ for (let r = 0; r < rank; r++) {
791
+ for (let d = 0; d < dim; d++) {
792
+ hidden[r] += input[d] * adapter.loraA[d][r];
793
+ }
794
+ }
795
+
796
+ const adapted = [...input];
797
+ for (let d = 0; d < dim; d++) {
798
+ for (let r = 0; r < rank; r++) {
799
+ adapted[d] += adapter.scaling * hidden[r] * adapter.loraB[r][d];
800
+ }
801
+ }
802
+
803
+ // Output gradient
804
+ const gradOutput = adapted.map((val, i) =>
805
+ 2 * (val - (target[i] || 0)) / dim
806
+ );
807
+
808
+ // Gradient for B
809
+ const gradB = this._zeros(rank, dim);
810
+ for (let r = 0; r < rank; r++) {
811
+ for (let d = 0; d < dim; d++) {
812
+ gradB[r][d] = hidden[r] * gradOutput[d] * adapter.scaling;
813
+ }
814
+ }
815
+
816
+ // Gradient for hidden
817
+ const gradHidden = new Float64Array(rank);
818
+ for (let r = 0; r < rank; r++) {
819
+ for (let d = 0; d < dim; d++) {
820
+ gradHidden[r] += gradOutput[d] * adapter.loraB[r][d] * adapter.scaling;
821
+ }
822
+ }
823
+
824
+ // Gradient for A
825
+ const gradA = this._zeros(dim, rank);
826
+ for (let d = 0; d < dim; d++) {
827
+ for (let r = 0; r < rank; r++) {
828
+ gradA[d][r] = input[d] * gradHidden[r];
829
+ }
830
+ }
831
+
832
+ return { gradA, gradB };
833
+ }
834
+
835
+ /**
836
+ * Apply EWC penalty to prevent catastrophic forgetting
837
+ * @private
838
+ */
839
+ _applyEWCPenalty(lambda, lr) {
840
+ if (!this.ewcState) return;
841
+
842
+ for (const [name, adapter] of this.adapters) {
843
+ const fisher = this.ewcState.fisherDiag.get(name);
844
+ const optimal = this.ewcState.optimalParams.get(name);
845
+
846
+ if (!fisher || !optimal) continue;
847
+
848
+ // Apply EWC penalty: lambda/2 * F * (theta - theta*)^2
849
+ for (let i = 0; i < adapter.loraA.length; i++) {
850
+ for (let j = 0; j < adapter.loraA[0].length; j++) {
851
+ const diff = adapter.loraA[i][j] - optimal.loraA[i][j];
852
+ adapter.loraA[i][j] -= lr * lambda * fisher.fisherA[i][j] * diff;
853
+ }
854
+ }
855
+ for (let i = 0; i < adapter.loraB.length; i++) {
856
+ for (let j = 0; j < adapter.loraB[0].length; j++) {
857
+ const diff = adapter.loraB[i][j] - optimal.loraB[i][j];
858
+ adapter.loraB[i][j] -= lr * lambda * fisher.fisherB[i][j] * diff;
859
+ }
860
+ }
861
+ }
862
+ }
863
+
864
+ // ============================================
865
+ // INFERENCE METHODS
866
+ // ============================================
867
+
868
+ /**
869
+ * Generate text with the adapted model
870
+ *
871
+ * @param {string} prompt - Input prompt
872
+ * @param {GenerationOptions} [options={}] - Generation options
873
+ * @returns {Promise<Object>} Generation result
874
+ *
875
+ * @example
876
+ * ```javascript
877
+ * const result = await lora.generate('Write a Python function', {
878
+ * maxTokens: 128,
879
+ * temperature: 0.8
880
+ * });
881
+ * console.log(result.text);
882
+ * ```
883
+ */
884
+ async generate(prompt, options = {}) {
885
+ const opts = {
886
+ maxTokens: 64,
887
+ temperature: 0.7,
888
+ topP: 0.9,
889
+ topK: 50,
890
+ repetitionPenalty: 1.1,
891
+ ...options,
892
+ };
893
+
894
+ this.stats.totalInferences++;
895
+
896
+ // Embed the prompt
897
+ const promptEmb = this._hashEmbed(prompt);
898
+
899
+ // Apply adapters
900
+ const adapted = this._forwardAdapters(promptEmb);
901
+
902
+ // For now, return simulated generation
903
+ // In production, this would interface with actual ONNX inference
904
+ const result = {
905
+ text: this._simulateGeneration(prompt, adapted, opts),
906
+ prompt,
907
+ adapted: true,
908
+ adaptersApplied: this.adapters.size,
909
+ model: this.baseModelKey,
910
+ options: opts,
911
+ timestamp: Date.now(),
912
+ };
913
+
914
+ this.emit('inference:complete', result);
915
+ return result;
916
+ }
917
+
918
+ /**
919
+ * Simulate text generation (placeholder for actual LLM inference)
920
+ * @private
921
+ */
922
+ _simulateGeneration(prompt, embedding, opts) {
923
+ // This is a placeholder - in production, would use ONNX runtime
924
+ // For now, return a template response based on adapter modifications
925
+ const embMagnitude = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
926
+
927
+ return `[MicroLoRA Adapted Output]\n` +
928
+ `Prompt: ${prompt.slice(0, 50)}...\n` +
929
+ `Embedding magnitude: ${embMagnitude.toFixed(4)}\n` +
930
+ `Adapters: ${this.adapters.size} active\n` +
931
+ `Model: ${this.baseModelKey}`;
932
+ }
933
+
934
+ /**
935
+ * Embed text using the model
936
+ *
937
+ * @param {string} text - Text to embed
938
+ * @returns {Promise<number[]>} Embedding vector
939
+ *
940
+ * @example
941
+ * ```javascript
942
+ * const embedding = await lora.embed('Hello world');
943
+ * console.log(`Dimension: ${embedding.length}`);
944
+ * ```
945
+ */
946
+ async embed(text) {
947
+ const baseEmb = this._hashEmbed(text);
948
+ const adapted = this._forwardAdapters(baseEmb);
949
+ return adapted;
950
+ }
951
+
952
+ // ============================================
953
+ // ADAPTER MANAGEMENT
954
+ // ============================================
955
+
956
+ /**
957
+ * Save adapter to file or return serialized data
958
+ *
959
+ * @param {string} [path] - Optional file path (Node.js only)
960
+ * @returns {Promise<Object>} Serialized adapter data
961
+ *
962
+ * @example
963
+ * ```javascript
964
+ * // Save to file (Node.js)
965
+ * await lora.saveAdapter('./my-adapter.json');
966
+ *
967
+ * // Get serialized data (browser)
968
+ * const data = await lora.saveAdapter();
969
+ * localStorage.setItem('my-adapter', JSON.stringify(data));
970
+ * ```
971
+ */
972
+ async saveAdapter(path) {
973
+ const data = {
974
+ version: '1.0.0',
975
+ format: 'microlora',
976
+ metadata: { ...this.metadata },
977
+ config: { ...this.config },
978
+ baseModel: this.baseModelKey,
979
+ adapters: {},
980
+ stats: { ...this.stats },
981
+ createdAt: this.metadata.createdAt,
982
+ savedAt: Date.now(),
983
+ };
984
+
985
+ // Serialize adapters
986
+ for (const [name, adapter] of this.adapters) {
987
+ data.adapters[name] = {
988
+ loraA: adapter.loraA,
989
+ loraB: adapter.loraB,
990
+ scaling: adapter.scaling,
991
+ };
992
+ }
993
+
994
+ // Save to file if path provided (Node.js)
995
+ if (path && typeof process !== 'undefined') {
996
+ const fs = await import('fs/promises');
997
+ await fs.writeFile(path, JSON.stringify(data, null, 2));
998
+ this.emit('adapter:saved', { path, size: JSON.stringify(data).length });
999
+ }
1000
+
1001
+ return data;
1002
+ }
1003
+
1004
+ /**
1005
+ * Load adapter from file or serialized data
1006
+ *
1007
+ * @param {string|Object} pathOrData - File path or serialized adapter data
1008
+ * @returns {Promise<void>}
1009
+ *
1010
+ * @example
1011
+ * ```javascript
1012
+ * // Load from file (Node.js)
1013
+ * await lora.loadAdapter('./my-adapter.json');
1014
+ *
1015
+ * // Load from data (browser)
1016
+ * const data = JSON.parse(localStorage.getItem('my-adapter'));
1017
+ * await lora.loadAdapter(data);
1018
+ * ```
1019
+ */
1020
+ async loadAdapter(pathOrData) {
1021
+ let data;
1022
+
1023
+ if (typeof pathOrData === 'string') {
1024
+ // Load from file (Node.js)
1025
+ const fs = await import('fs/promises');
1026
+ const content = await fs.readFile(pathOrData, 'utf-8');
1027
+ data = JSON.parse(content);
1028
+ } else {
1029
+ data = pathOrData;
1030
+ }
1031
+
1032
+ // Validate format
1033
+ if (data.format !== 'microlora') {
1034
+ throw new Error(`Unsupported adapter format: ${data.format}`);
1035
+ }
1036
+
1037
+ // Check base model compatibility
1038
+ if (data.baseModel !== this.baseModelKey) {
1039
+ console.warn(`Warning: Adapter was trained on ${data.baseModel}, ` +
1040
+ `but loading into ${this.baseModelKey}`);
1041
+ }
1042
+
1043
+ // Load adapter weights
1044
+ this.adapters.clear();
1045
+ for (const [name, adapter] of Object.entries(data.adapters)) {
1046
+ this.adapters.set(name, {
1047
+ loraA: adapter.loraA,
1048
+ loraB: adapter.loraB,
1049
+ scaling: adapter.scaling,
1050
+ });
1051
+ }
1052
+
1053
+ // Restore metadata
1054
+ this.metadata = { ...this.metadata, ...data.metadata };
1055
+ this.stats = { ...this.stats, ...data.stats };
1056
+
1057
+ this.emit('adapter:loaded', {
1058
+ path: typeof pathOrData === 'string' ? pathOrData : null,
1059
+ adapters: this.adapters.size,
1060
+ metadata: this.metadata,
1061
+ });
1062
+ }
1063
+
1064
+ /**
1065
+ * Merge multiple adapters with weights
1066
+ *
1067
+ * @param {Array<{adapter: Object, weight: number}>} adapters - Adapters with weights
1068
+ * @returns {Promise<void>}
1069
+ *
1070
+ * @example
1071
+ * ```javascript
1072
+ * await lora.mergeAdapters([
1073
+ * { adapter: codeAdapter, weight: 0.7 },
1074
+ * { adapter: mathAdapter, weight: 0.3 }
1075
+ * ]);
1076
+ * ```
1077
+ */
1078
+ async mergeAdapters(adapters) {
1079
+ if (adapters.length === 0) return;
1080
+
1081
+ // Normalize weights
1082
+ const totalWeight = adapters.reduce((sum, a) => sum + a.weight, 0);
1083
+ const normalizedAdapters = adapters.map(a => ({
1084
+ ...a,
1085
+ weight: a.weight / totalWeight,
1086
+ }));
1087
+
1088
+ // Merge each module
1089
+ for (const [name, currentAdapter] of this.adapters) {
1090
+ // Reset to zero
1091
+ for (let i = 0; i < currentAdapter.loraA.length; i++) {
1092
+ for (let j = 0; j < currentAdapter.loraA[0].length; j++) {
1093
+ currentAdapter.loraA[i][j] = 0;
1094
+ }
1095
+ }
1096
+ for (let i = 0; i < currentAdapter.loraB.length; i++) {
1097
+ for (let j = 0; j < currentAdapter.loraB[0].length; j++) {
1098
+ currentAdapter.loraB[i][j] = 0;
1099
+ }
1100
+ }
1101
+
1102
+ // Weighted sum of adapters
1103
+ for (const { adapter, weight } of normalizedAdapters) {
1104
+ const adapterWeights = adapter.adapters?.[name] || adapter.adapters?.get?.(name);
1105
+ if (!adapterWeights) continue;
1106
+
1107
+ for (let i = 0; i < currentAdapter.loraA.length; i++) {
1108
+ for (let j = 0; j < currentAdapter.loraA[0].length; j++) {
1109
+ currentAdapter.loraA[i][j] += weight * (adapterWeights.loraA[i]?.[j] || 0);
1110
+ }
1111
+ }
1112
+ for (let i = 0; i < currentAdapter.loraB.length; i++) {
1113
+ for (let j = 0; j < currentAdapter.loraB[0].length; j++) {
1114
+ currentAdapter.loraB[i][j] += weight * (adapterWeights.loraB[i]?.[j] || 0);
1115
+ }
1116
+ }
1117
+ }
1118
+ }
1119
+
1120
+ this.stats.adaptations++;
1121
+ this.emit('adapter:merged', {
1122
+ count: adapters.length,
1123
+ weights: normalizedAdapters.map(a => a.weight),
1124
+ });
1125
+ }
1126
+
1127
+ // ============================================
1128
+ // EXPORT METHODS
1129
+ // ============================================
1130
+
1131
+ /**
1132
+ * Export adapter to ONNX format
1133
+ *
1134
+ * @returns {Promise<Uint8Array>} ONNX model bytes
1135
+ *
1136
+ * @example
1137
+ * ```javascript
1138
+ * const onnxBytes = await lora.exportToONNX();
1139
+ * // Save or deploy the ONNX model
1140
+ * ```
1141
+ */
1142
+ async exportToONNX() {
1143
+ // Build ONNX-compatible structure
1144
+ const onnxData = {
1145
+ format: 'onnx-lora-adapter',
1146
+ version: '1.0',
1147
+ baseModel: this.baseModelKey,
1148
+ config: this.config,
1149
+ adapters: [],
1150
+ };
1151
+
1152
+ for (const [name, adapter] of this.adapters) {
1153
+ onnxData.adapters.push({
1154
+ name,
1155
+ loraA: {
1156
+ shape: [adapter.loraA.length, adapter.loraA[0].length],
1157
+ data: adapter.loraA.flat(),
1158
+ },
1159
+ loraB: {
1160
+ shape: [adapter.loraB.length, adapter.loraB[0].length],
1161
+ data: adapter.loraB.flat(),
1162
+ },
1163
+ scaling: adapter.scaling,
1164
+ });
1165
+ }
1166
+
1167
+ // Convert to protobuf-like format (simplified)
1168
+ const json = JSON.stringify(onnxData);
1169
+ const bytes = new TextEncoder().encode(json);
1170
+
1171
+ this.emit('export:onnx', { size: bytes.length });
1172
+ return bytes;
1173
+ }
1174
+
1175
+ /**
1176
+ * Export adapter to IPFS-compatible format
1177
+ *
1178
+ * @returns {Promise<Object>} IPFS-ready data with CID placeholder
1179
+ *
1180
+ * @example
1181
+ * ```javascript
1182
+ * const ipfsData = await lora.exportToIPFS();
1183
+ * // Upload to IPFS using your preferred client
1184
+ * const cid = await ipfsClient.add(ipfsData.content);
1185
+ * ```
1186
+ */
1187
+ async exportToIPFS() {
1188
+ const adapterData = await this.saveAdapter();
1189
+
1190
+ // Add IPFS-specific metadata
1191
+ const ipfsData = {
1192
+ ...adapterData,
1193
+ ipfs: {
1194
+ version: 1,
1195
+ contentType: 'application/json',
1196
+ compression: 'none',
1197
+ chunks: 1,
1198
+ },
1199
+ };
1200
+
1201
+ const content = JSON.stringify(ipfsData);
1202
+ const hash = createHash('sha256').update(content).digest('hex');
1203
+
1204
+ return {
1205
+ content: new TextEncoder().encode(content),
1206
+ hash,
1207
+ size: content.length,
1208
+ metadata: {
1209
+ name: this.metadata.name,
1210
+ description: this.metadata.description,
1211
+ domain: this.metadata.domain,
1212
+ baseModel: this.baseModelKey,
1213
+ },
1214
+ };
1215
+ }
1216
+
1217
+ // ============================================
1218
+ // UTILITY METHODS
1219
+ // ============================================
1220
+
1221
+ /**
1222
+ * Get adapter metadata
1223
+ *
1224
+ * @returns {AdapterMetadata} Current adapter metadata
1225
+ */
1226
+ getMetadata() {
1227
+ return { ...this.metadata };
1228
+ }
1229
+
1230
+ /**
1231
+ * Set adapter metadata
1232
+ *
1233
+ * @param {Partial<AdapterMetadata>} metadata - Metadata to update
1234
+ */
1235
+ setMetadata(metadata) {
1236
+ this.metadata = { ...this.metadata, ...metadata };
1237
+ }
1238
+
1239
+ /**
1240
+ * Get training and inference statistics
1241
+ *
1242
+ * @returns {Object} Current statistics
1243
+ */
1244
+ getStats() {
1245
+ return {
1246
+ ...this.stats,
1247
+ adapters: this.adapters.size,
1248
+ config: { ...this.config },
1249
+ baseModel: this.baseModelKey,
1250
+ };
1251
+ }
1252
+
1253
+ /**
1254
+ * Reset adapter to initial state
1255
+ */
1256
+ reset() {
1257
+ this.adapters = this._initializeAdapters();
1258
+ this.ewcState = null;
1259
+ this.trainingState = null;
1260
+ this.stats = {
1261
+ totalTrainingSamples: 0,
1262
+ totalTrainingTime: 0,
1263
+ totalInferences: 0,
1264
+ corrections: 0,
1265
+ adaptations: 0,
1266
+ };
1267
+ this.emit('adapter:reset');
1268
+ }
1269
+
1270
+ /**
1271
+ * Get number of trainable parameters
1272
+ *
1273
+ * @returns {number} Total trainable parameters
1274
+ */
1275
+ numParameters() {
1276
+ let total = 0;
1277
+ for (const [, adapter] of this.adapters) {
1278
+ total += adapter.loraA.length * adapter.loraA[0].length;
1279
+ total += adapter.loraB.length * adapter.loraB[0].length;
1280
+ }
1281
+ return total;
1282
+ }
1283
+
1284
+ /**
1285
+ * Check if adapter has been trained
1286
+ *
1287
+ * @returns {boolean} True if any training has occurred
1288
+ */
1289
+ isTrained() {
1290
+ return this.stats.totalTrainingSamples > 0;
1291
+ }
1292
+ }
1293
+
1294
+ // ============================================
1295
+ // EXPORTS
1296
+ // ============================================
1297
+
1298
+ export default MicroLoRA;