@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.
- package/README.md +281 -10
- package/core-invariants.js +942 -0
- package/models/adapter-hub.js +1008 -0
- package/models/adapter-security.js +792 -0
- package/models/benchmark.js +688 -0
- package/models/distribution.js +791 -0
- package/models/index.js +109 -0
- package/models/integrity.js +753 -0
- package/models/loader.js +725 -0
- package/models/microlora.js +1298 -0
- package/models/model-loader.js +922 -0
- package/models/model-optimizer.js +1245 -0
- package/models/model-registry.js +696 -0
- package/models/model-utils.js +548 -0
- package/models/models-cli.js +914 -0
- package/models/registry.json +214 -0
- package/models/training-utils.js +1418 -0
- package/models/wasm-core.js +1025 -0
- package/network-genesis.js +2847 -0
- package/onnx-worker.js +462 -8
- package/package.json +33 -3
- package/plugins/SECURITY-AUDIT.md +654 -0
- package/plugins/cli.js +43 -3
- package/plugins/implementations/e2e-encryption.js +57 -12
- package/plugins/plugin-loader.js +610 -21
- package/tests/model-optimizer.test.js +644 -0
- package/tests/network-genesis.test.js +562 -0
- package/tests/plugin-benchmark.js +1239 -0
- package/tests/plugin-system-test.js +163 -0
- package/tests/wasm-core.test.js +368 -0
|
@@ -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;
|