@hanzo/browser-extension 1.9.34 → 1.9.36
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/package.json +1 -1
- package/src/answer/AnswerEngine.tsx +8 -0
- package/src/onboarding.css +78 -0
- package/src/onboarding.html +45 -0
- package/src/popup.html +28 -0
- package/dist/browser-extension/README.md +0 -43
- package/dist/browser-extension/background-firefox.js +0 -5983
- package/dist/browser-extension/background.js +0 -8670
- package/dist/browser-extension/browser-control.js +0 -1317
- package/dist/browser-extension/chrome/ai-worker.js +0 -571
- package/dist/browser-extension/chrome/background.js +0 -8670
- package/dist/browser-extension/chrome/callback.html +0 -17
- package/dist/browser-extension/chrome/content-script.js +0 -2789
- package/dist/browser-extension/chrome/icon128.png +0 -0
- package/dist/browser-extension/chrome/icon16.png +0 -0
- package/dist/browser-extension/chrome/icon48.png +0 -0
- package/dist/browser-extension/chrome/manifest.json +0 -81
- package/dist/browser-extension/chrome/newtab.css +0 -277
- package/dist/browser-extension/chrome/newtab.html +0 -13
- package/dist/browser-extension/chrome/newtab.js +0 -64
- package/dist/browser-extension/chrome/popup.css +0 -761
- package/dist/browser-extension/chrome/popup.html +0 -381
- package/dist/browser-extension/chrome/popup.js +0 -1716
- package/dist/browser-extension/chrome/sidebar.css +0 -1792
- package/dist/browser-extension/chrome/sidebar.html +0 -463
- package/dist/browser-extension/chrome/sidebar.js +0 -26541
- package/dist/browser-extension/cli.js +0 -233
- package/dist/browser-extension/content-script.js +0 -2789
- package/dist/browser-extension/firefox/ai-worker.js +0 -571
- package/dist/browser-extension/firefox/background.js +0 -5983
- package/dist/browser-extension/firefox/callback.html +0 -17
- package/dist/browser-extension/firefox/content-script.js +0 -2789
- package/dist/browser-extension/firefox/icon128.png +0 -0
- package/dist/browser-extension/firefox/icon16.png +0 -0
- package/dist/browser-extension/firefox/icon48.png +0 -0
- package/dist/browser-extension/firefox/manifest.json +0 -85
- package/dist/browser-extension/firefox/newtab.css +0 -277
- package/dist/browser-extension/firefox/newtab.html +0 -13
- package/dist/browser-extension/firefox/newtab.js +0 -64
- package/dist/browser-extension/firefox/popup.css +0 -761
- package/dist/browser-extension/firefox/popup.html +0 -381
- package/dist/browser-extension/firefox/popup.js +0 -1716
- package/dist/browser-extension/firefox/sidebar.css +0 -1792
- package/dist/browser-extension/firefox/sidebar.html +0 -463
- package/dist/browser-extension/firefox/sidebar.js +0 -26541
- package/dist/browser-extension/icon128.png +0 -0
- package/dist/browser-extension/icon16.png +0 -0
- package/dist/browser-extension/icon48.png +0 -0
- package/dist/browser-extension/manifest.json +0 -81
- package/dist/browser-extension/newtab.js +0 -64
- package/dist/browser-extension/package.json +0 -41
- package/dist/browser-extension/popup.js +0 -1716
- package/dist/browser-extension/safari/Info.plist +0 -21
- package/dist/browser-extension/safari/ai-worker.js +0 -571
- package/dist/browser-extension/safari/background.js +0 -8670
- package/dist/browser-extension/safari/callback.html +0 -17
- package/dist/browser-extension/safari/content-script.js +0 -2789
- package/dist/browser-extension/safari/icon128.png +0 -0
- package/dist/browser-extension/safari/icon16.png +0 -0
- package/dist/browser-extension/safari/icon48.png +0 -0
- package/dist/browser-extension/safari/newtab.css +0 -277
- package/dist/browser-extension/safari/newtab.html +0 -13
- package/dist/browser-extension/safari/newtab.js +0 -64
- package/dist/browser-extension/safari/popup.css +0 -761
- package/dist/browser-extension/safari/popup.html +0 -381
- package/dist/browser-extension/safari/popup.js +0 -1716
- package/dist/browser-extension/safari/sidebar.css +0 -1792
- package/dist/browser-extension/safari/sidebar.html +0 -463
- package/dist/browser-extension/safari/sidebar.js +0 -26541
- package/dist/browser-extension/sidebar.js +0 -26541
- package/dist/browser-extension/webgpu-ai.js +0 -568
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
-
<plist version="1.0">
|
|
4
|
-
<dict>
|
|
5
|
-
<key>CFBundleDisplayName</key>
|
|
6
|
-
<string>Hanzo AI Dev Assistant</string>
|
|
7
|
-
<key>CFBundleIdentifier</key>
|
|
8
|
-
<string>ai.hanzo.browser-extension</string>
|
|
9
|
-
<key>CFBundleVersion</key>
|
|
10
|
-
<string>1.9.34</string>
|
|
11
|
-
<key>CFBundleShortVersionString</key>
|
|
12
|
-
<string>1.9.34</string>
|
|
13
|
-
<key>NSExtension</key>
|
|
14
|
-
<dict>
|
|
15
|
-
<key>NSExtensionPointIdentifier</key>
|
|
16
|
-
<string>com.apple.Safari.web-extension</string>
|
|
17
|
-
<key>NSExtensionPrincipalClass</key>
|
|
18
|
-
<string>SafariWebExtensionHandler</string>
|
|
19
|
-
</dict>
|
|
20
|
-
</dict>
|
|
21
|
-
</plist>
|
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
// Hanzo AI Worker — WebGPU inference engine running in web worker context.
|
|
2
|
-
// Handles model loading, GPU compute pipelines, and token generation.
|
|
3
|
-
// Referenced by browser-control.ts and loaded as a web-accessible resource.
|
|
4
|
-
|
|
5
|
-
/** @type {GPUDevice|null} */
|
|
6
|
-
let gpuDevice = null;
|
|
7
|
-
|
|
8
|
-
/** @type {Map<string, {buffer: GPUBuffer, config: object, vocab: string[]}>} */
|
|
9
|
-
const loadedModels = new Map();
|
|
10
|
-
|
|
11
|
-
/** @type {boolean} */
|
|
12
|
-
let initialized = false;
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// GPU Initialization
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
async function initGPU() {
|
|
19
|
-
if (!navigator.gpu) {
|
|
20
|
-
return { success: false, error: 'WebGPU not available in this browser' };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
25
|
-
if (!adapter) {
|
|
26
|
-
return { success: false, error: 'No GPU adapter found' };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const features = Array.from(adapter.features);
|
|
30
|
-
gpuDevice = await adapter.requestDevice({
|
|
31
|
-
requiredLimits: {
|
|
32
|
-
maxStorageBufferBindingSize: adapter.limits?.maxStorageBufferBindingSize || 134217728,
|
|
33
|
-
maxBufferSize: adapter.limits?.maxBufferSize || 268435456,
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
gpuDevice.lost.then((info) => {
|
|
38
|
-
gpuDevice = null;
|
|
39
|
-
initialized = false;
|
|
40
|
-
self.postMessage({ type: 'error', payload: { error: `GPU device lost: ${info.message}` } });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
initialized = true;
|
|
44
|
-
return { success: true, features, adapter: adapter.name || 'GPU' };
|
|
45
|
-
} catch (err) {
|
|
46
|
-
return { success: false, error: String(err) };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// BPE Tokenizer (byte-level, GPT-2 compatible)
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
// Byte-to-unicode mapping (identical to GPT-2 byte encoder)
|
|
55
|
-
function bytesToUnicode() {
|
|
56
|
-
const bs = [];
|
|
57
|
-
// Printable ASCII
|
|
58
|
-
for (let i = 33; i <= 126; i++) bs.push(i); // ! to ~
|
|
59
|
-
for (let i = 161; i <= 172; i++) bs.push(i); // ¡ to ¬
|
|
60
|
-
for (let i = 174; i <= 255; i++) bs.push(i); // ® to ÿ
|
|
61
|
-
const cs = bs.slice();
|
|
62
|
-
let n = 0;
|
|
63
|
-
for (let b = 0; b < 256; b++) {
|
|
64
|
-
if (!bs.includes(b)) {
|
|
65
|
-
bs.push(b);
|
|
66
|
-
cs.push(256 + n);
|
|
67
|
-
n++;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const result = {};
|
|
71
|
-
for (let i = 0; i < bs.length; i++) {
|
|
72
|
-
result[bs[i]] = String.fromCharCode(cs[i]);
|
|
73
|
-
}
|
|
74
|
-
return result;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const BYTE_ENCODER = bytesToUnicode();
|
|
78
|
-
const BYTE_DECODER = {};
|
|
79
|
-
for (const [k, v] of Object.entries(BYTE_ENCODER)) {
|
|
80
|
-
BYTE_DECODER[v] = parseInt(k);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Encode text to token IDs using a vocabulary list.
|
|
85
|
-
* Falls back to character-level encoding if vocab is unavailable.
|
|
86
|
-
*/
|
|
87
|
-
function tokenize(text, vocab) {
|
|
88
|
-
if (!vocab || !vocab.length) {
|
|
89
|
-
// Character-level fallback: each UTF-16 code unit becomes a token
|
|
90
|
-
const tokens = [];
|
|
91
|
-
for (let i = 0; i < text.length; i++) {
|
|
92
|
-
tokens.push(text.charCodeAt(i));
|
|
93
|
-
}
|
|
94
|
-
return tokens;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// BPE-style word-level tokenization with byte encoding
|
|
98
|
-
const encoded = [];
|
|
99
|
-
const bytes = new TextEncoder().encode(text);
|
|
100
|
-
let word = '';
|
|
101
|
-
for (const b of bytes) {
|
|
102
|
-
word += BYTE_ENCODER[b] || String.fromCharCode(b);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Greedy longest-match against vocabulary
|
|
106
|
-
const tokens = [];
|
|
107
|
-
let pos = 0;
|
|
108
|
-
while (pos < word.length) {
|
|
109
|
-
let bestLen = 0;
|
|
110
|
-
let bestIdx = -1;
|
|
111
|
-
// Try longest possible match
|
|
112
|
-
const maxLen = Math.min(word.length - pos, 50);
|
|
113
|
-
for (let len = maxLen; len >= 1; len--) {
|
|
114
|
-
const candidate = word.substring(pos, pos + len);
|
|
115
|
-
const idx = vocab.indexOf(candidate);
|
|
116
|
-
if (idx !== -1) {
|
|
117
|
-
bestLen = len;
|
|
118
|
-
bestIdx = idx;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
if (bestIdx !== -1) {
|
|
123
|
-
tokens.push(bestIdx);
|
|
124
|
-
pos += bestLen;
|
|
125
|
-
} else {
|
|
126
|
-
// Unknown byte — use raw char code
|
|
127
|
-
tokens.push(word.charCodeAt(pos) % vocab.length);
|
|
128
|
-
pos++;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return tokens;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Decode token IDs back to text.
|
|
136
|
-
*/
|
|
137
|
-
function detokenize(tokenIds, vocab) {
|
|
138
|
-
if (!vocab || !vocab.length) {
|
|
139
|
-
return String.fromCharCode(...tokenIds.filter(t => t > 0 && t < 65536));
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let byteStr = '';
|
|
143
|
-
for (const id of tokenIds) {
|
|
144
|
-
if (id >= 0 && id < vocab.length) {
|
|
145
|
-
byteStr += vocab[id];
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Decode byte-level BPE back to UTF-8
|
|
150
|
-
const bytes = [];
|
|
151
|
-
for (const ch of byteStr) {
|
|
152
|
-
if (ch in BYTE_DECODER) {
|
|
153
|
-
bytes.push(BYTE_DECODER[ch]);
|
|
154
|
-
} else {
|
|
155
|
-
bytes.push(ch.charCodeAt(0) & 0xFF);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return new TextDecoder().decode(new Uint8Array(bytes));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ---------------------------------------------------------------------------
|
|
162
|
-
// Model Loading
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
|
|
165
|
-
async function loadModel(name, url, config) {
|
|
166
|
-
if (!gpuDevice) {
|
|
167
|
-
throw new Error('GPU not initialized');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Fetch model weights (ArrayBuffer)
|
|
171
|
-
const response = await fetch(url);
|
|
172
|
-
if (!response.ok) {
|
|
173
|
-
throw new Error(`Failed to fetch model: ${response.status} ${response.statusText}`);
|
|
174
|
-
}
|
|
175
|
-
const weights = await response.arrayBuffer();
|
|
176
|
-
|
|
177
|
-
// Upload weights to GPU
|
|
178
|
-
const modelBuffer = gpuDevice.createBuffer({
|
|
179
|
-
size: weights.byteLength,
|
|
180
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
181
|
-
label: `model-${name}`,
|
|
182
|
-
});
|
|
183
|
-
gpuDevice.queue.writeBuffer(modelBuffer, 0, weights);
|
|
184
|
-
|
|
185
|
-
// Load vocabulary if provided
|
|
186
|
-
let vocab = [];
|
|
187
|
-
if (config.vocabUrl) {
|
|
188
|
-
try {
|
|
189
|
-
const vocabResp = await fetch(config.vocabUrl);
|
|
190
|
-
if (vocabResp.ok) {
|
|
191
|
-
const vocabData = await vocabResp.json();
|
|
192
|
-
vocab = Array.isArray(vocabData) ? vocabData : Object.keys(vocabData);
|
|
193
|
-
}
|
|
194
|
-
} catch {
|
|
195
|
-
// Vocabulary load failed — use character-level fallback
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
loadedModels.set(name, {
|
|
200
|
-
buffer: modelBuffer,
|
|
201
|
-
config: config || {},
|
|
202
|
-
vocab,
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
return { name, weightsSize: weights.byteLength, vocabSize: vocab.length };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// ---------------------------------------------------------------------------
|
|
209
|
-
// WGSL Compute Shader — Matrix-vector multiply + softmax for next-token prediction
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
|
|
212
|
-
const INFERENCE_SHADER = `
|
|
213
|
-
@group(0) @binding(0) var<storage, read> weights: array<f32>;
|
|
214
|
-
@group(0) @binding(1) var<storage, read> input: array<f32>;
|
|
215
|
-
@group(0) @binding(2) var<storage, read_write> output: array<f32>;
|
|
216
|
-
@group(0) @binding(3) var<storage, read> params: array<u32>;
|
|
217
|
-
|
|
218
|
-
// params[0] = input_size
|
|
219
|
-
// params[1] = output_size (vocab_size)
|
|
220
|
-
// params[2] = weights_offset
|
|
221
|
-
|
|
222
|
-
@compute @workgroup_size(256)
|
|
223
|
-
fn matmul(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
224
|
-
let out_idx = gid.x;
|
|
225
|
-
let input_size = params[0];
|
|
226
|
-
let output_size = params[1];
|
|
227
|
-
let w_offset = params[2];
|
|
228
|
-
|
|
229
|
-
if (out_idx >= output_size) {
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Dot product: output[out_idx] = sum(weights[w_offset + out_idx * input_size + j] * input[j])
|
|
234
|
-
var sum: f32 = 0.0;
|
|
235
|
-
for (var j: u32 = 0u; j < input_size; j = j + 1u) {
|
|
236
|
-
let w_idx = w_offset + out_idx * input_size + j;
|
|
237
|
-
if (w_idx < arrayLength(&weights)) {
|
|
238
|
-
sum = sum + weights[w_idx] * input[j];
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
output[out_idx] = sum;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
@compute @workgroup_size(1)
|
|
245
|
-
fn softmax(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|
246
|
-
let size = params[1];
|
|
247
|
-
|
|
248
|
-
// Find max for numerical stability
|
|
249
|
-
var max_val: f32 = output[0];
|
|
250
|
-
for (var i: u32 = 1u; i < size; i = i + 1u) {
|
|
251
|
-
if (output[i] > max_val) {
|
|
252
|
-
max_val = output[i];
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// exp and sum
|
|
257
|
-
var sum: f32 = 0.0;
|
|
258
|
-
for (var i: u32 = 0u; i < size; i = i + 1u) {
|
|
259
|
-
let e = exp(output[i] - max_val);
|
|
260
|
-
output[i] = e;
|
|
261
|
-
sum = sum + e;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Normalize
|
|
265
|
-
for (var i: u32 = 0u; i < size; i = i + 1u) {
|
|
266
|
-
output[i] = output[i] / sum;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
`;
|
|
270
|
-
|
|
271
|
-
// ---------------------------------------------------------------------------
|
|
272
|
-
// Inference Pipeline
|
|
273
|
-
// ---------------------------------------------------------------------------
|
|
274
|
-
|
|
275
|
-
async function runInference(modelName, prompt, maxTokens, temperature) {
|
|
276
|
-
const model = loadedModels.get(modelName);
|
|
277
|
-
if (!model || !gpuDevice) {
|
|
278
|
-
throw new Error(`Model "${modelName}" not loaded or GPU unavailable`);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const tokens = tokenize(prompt, model.vocab);
|
|
282
|
-
const vocabSize = model.vocab.length || 65536;
|
|
283
|
-
const inputSize = Math.min(tokens.length, 2048);
|
|
284
|
-
maxTokens = maxTokens || 128;
|
|
285
|
-
temperature = temperature || 0.7;
|
|
286
|
-
|
|
287
|
-
// Encode input as float32 for GPU
|
|
288
|
-
const inputData = new Float32Array(inputSize);
|
|
289
|
-
for (let i = 0; i < inputSize; i++) {
|
|
290
|
-
inputData[i] = tokens[i] / vocabSize; // Normalize to [0, 1]
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Create GPU buffers
|
|
294
|
-
const inputBuffer = gpuDevice.createBuffer({
|
|
295
|
-
size: inputData.byteLength,
|
|
296
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
297
|
-
label: 'input',
|
|
298
|
-
});
|
|
299
|
-
gpuDevice.queue.writeBuffer(inputBuffer, 0, inputData);
|
|
300
|
-
|
|
301
|
-
const outputSize = vocabSize * 4; // float32 per vocab entry
|
|
302
|
-
const outputBuffer = gpuDevice.createBuffer({
|
|
303
|
-
size: outputSize,
|
|
304
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
|
|
305
|
-
label: 'output',
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
const paramsData = new Uint32Array([inputSize, vocabSize, 0]);
|
|
309
|
-
const paramsBuffer = gpuDevice.createBuffer({
|
|
310
|
-
size: paramsData.byteLength,
|
|
311
|
-
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
|
|
312
|
-
label: 'params',
|
|
313
|
-
});
|
|
314
|
-
gpuDevice.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
315
|
-
|
|
316
|
-
const readBuffer = gpuDevice.createBuffer({
|
|
317
|
-
size: outputSize,
|
|
318
|
-
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
|
|
319
|
-
label: 'readback',
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
// Create compute pipeline
|
|
323
|
-
const shaderModule = gpuDevice.createShaderModule({ code: INFERENCE_SHADER });
|
|
324
|
-
|
|
325
|
-
const bindGroupLayout = gpuDevice.createBindGroupLayout({
|
|
326
|
-
entries: [
|
|
327
|
-
{ binding: 0, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
328
|
-
{ binding: 1, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
329
|
-
{ binding: 2, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'storage' } },
|
|
330
|
-
{ binding: 3, visibility: GPUShaderStage.COMPUTE, buffer: { type: 'read-only-storage' } },
|
|
331
|
-
],
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
const pipelineLayout = gpuDevice.createPipelineLayout({ bindGroupLayouts: [bindGroupLayout] });
|
|
335
|
-
|
|
336
|
-
const matmulPipeline = gpuDevice.createComputePipeline({
|
|
337
|
-
layout: pipelineLayout,
|
|
338
|
-
compute: { module: shaderModule, entryPoint: 'matmul' },
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
const softmaxPipeline = gpuDevice.createComputePipeline({
|
|
342
|
-
layout: pipelineLayout,
|
|
343
|
-
compute: { module: shaderModule, entryPoint: 'softmax' },
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
const bindGroup = gpuDevice.createBindGroup({
|
|
347
|
-
layout: bindGroupLayout,
|
|
348
|
-
entries: [
|
|
349
|
-
{ binding: 0, resource: { buffer: model.buffer } },
|
|
350
|
-
{ binding: 1, resource: { buffer: inputBuffer } },
|
|
351
|
-
{ binding: 2, resource: { buffer: outputBuffer } },
|
|
352
|
-
{ binding: 3, resource: { buffer: paramsBuffer } },
|
|
353
|
-
],
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
// Autoregressive generation
|
|
357
|
-
const generatedTokens = [];
|
|
358
|
-
|
|
359
|
-
for (let step = 0; step < maxTokens; step++) {
|
|
360
|
-
// Run matmul
|
|
361
|
-
const encoder = gpuDevice.createCommandEncoder();
|
|
362
|
-
const matmulPass = encoder.beginComputePass();
|
|
363
|
-
matmulPass.setPipeline(matmulPipeline);
|
|
364
|
-
matmulPass.setBindGroup(0, bindGroup);
|
|
365
|
-
matmulPass.dispatchWorkgroups(Math.ceil(vocabSize / 256));
|
|
366
|
-
matmulPass.end();
|
|
367
|
-
|
|
368
|
-
// Run softmax
|
|
369
|
-
const softmaxPass = encoder.beginComputePass();
|
|
370
|
-
softmaxPass.setPipeline(softmaxPipeline);
|
|
371
|
-
softmaxPass.setBindGroup(0, bindGroup);
|
|
372
|
-
softmaxPass.dispatchWorkgroups(1);
|
|
373
|
-
softmaxPass.end();
|
|
374
|
-
|
|
375
|
-
// Readback
|
|
376
|
-
encoder.copyBufferToBuffer(outputBuffer, 0, readBuffer, 0, outputSize);
|
|
377
|
-
gpuDevice.queue.submit([encoder.finish()]);
|
|
378
|
-
|
|
379
|
-
await readBuffer.mapAsync(GPUMapMode.READ);
|
|
380
|
-
const probs = new Float32Array(readBuffer.getMappedRange().slice(0));
|
|
381
|
-
readBuffer.unmap();
|
|
382
|
-
|
|
383
|
-
// Temperature-scaled sampling
|
|
384
|
-
const nextToken = sampleToken(probs, temperature);
|
|
385
|
-
generatedTokens.push(nextToken);
|
|
386
|
-
|
|
387
|
-
// Report progress
|
|
388
|
-
self.postMessage({
|
|
389
|
-
type: 'token',
|
|
390
|
-
payload: {
|
|
391
|
-
token: nextToken,
|
|
392
|
-
text: detokenize([nextToken], model.vocab),
|
|
393
|
-
step: step + 1,
|
|
394
|
-
total: maxTokens,
|
|
395
|
-
},
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
// Stop on EOS (token 0 or explicit EOS)
|
|
399
|
-
if (nextToken === 0 || nextToken === 2) break;
|
|
400
|
-
|
|
401
|
-
// Feed new token back as input for next step
|
|
402
|
-
const newInput = new Float32Array([nextToken / vocabSize]);
|
|
403
|
-
gpuDevice.queue.writeBuffer(inputBuffer, 0, newInput);
|
|
404
|
-
paramsData[0] = 1; // Single token input for autoregressive step
|
|
405
|
-
gpuDevice.queue.writeBuffer(paramsBuffer, 0, paramsData);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Cleanup
|
|
409
|
-
inputBuffer.destroy();
|
|
410
|
-
outputBuffer.destroy();
|
|
411
|
-
paramsBuffer.destroy();
|
|
412
|
-
readBuffer.destroy();
|
|
413
|
-
|
|
414
|
-
return detokenize(generatedTokens, model.vocab);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Sample from probability distribution with temperature scaling.
|
|
419
|
-
*/
|
|
420
|
-
function sampleToken(probs, temperature) {
|
|
421
|
-
if (temperature <= 0) {
|
|
422
|
-
// Greedy: return argmax
|
|
423
|
-
let maxIdx = 0;
|
|
424
|
-
let maxVal = probs[0];
|
|
425
|
-
for (let i = 1; i < probs.length; i++) {
|
|
426
|
-
if (probs[i] > maxVal) {
|
|
427
|
-
maxVal = probs[i];
|
|
428
|
-
maxIdx = i;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
return maxIdx;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
// Temperature scaling
|
|
435
|
-
const scaled = new Float32Array(probs.length);
|
|
436
|
-
let sum = 0;
|
|
437
|
-
for (let i = 0; i < probs.length; i++) {
|
|
438
|
-
scaled[i] = Math.exp(Math.log(Math.max(probs[i], 1e-10)) / temperature);
|
|
439
|
-
sum += scaled[i];
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Normalize
|
|
443
|
-
for (let i = 0; i < scaled.length; i++) {
|
|
444
|
-
scaled[i] /= sum;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// Multinomial sampling
|
|
448
|
-
const r = Math.random();
|
|
449
|
-
let cumulative = 0;
|
|
450
|
-
for (let i = 0; i < scaled.length; i++) {
|
|
451
|
-
cumulative += scaled[i];
|
|
452
|
-
if (r < cumulative) return i;
|
|
453
|
-
}
|
|
454
|
-
return scaled.length - 1;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// ---------------------------------------------------------------------------
|
|
458
|
-
// Embeddings (for browser-control DOM analysis)
|
|
459
|
-
// ---------------------------------------------------------------------------
|
|
460
|
-
|
|
461
|
-
async function computeEmbedding(modelName, text) {
|
|
462
|
-
const model = loadedModels.get(modelName);
|
|
463
|
-
if (!model || !gpuDevice) {
|
|
464
|
-
throw new Error('Model or GPU not available');
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
const tokens = tokenize(text, model.vocab);
|
|
468
|
-
const inputSize = Math.min(tokens.length, 512);
|
|
469
|
-
|
|
470
|
-
// Use model weights to compute a fixed-size embedding via mean pooling
|
|
471
|
-
const inputData = new Float32Array(inputSize);
|
|
472
|
-
for (let i = 0; i < inputSize; i++) {
|
|
473
|
-
inputData[i] = tokens[i];
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const embeddingDim = 256;
|
|
477
|
-
const embedding = new Float32Array(embeddingDim);
|
|
478
|
-
|
|
479
|
-
// Linear projection through model weights to fixed-size embedding
|
|
480
|
-
const weightsView = new Float32Array(model.buffer.size / 4);
|
|
481
|
-
for (let d = 0; d < embeddingDim; d++) {
|
|
482
|
-
let val = 0;
|
|
483
|
-
for (let i = 0; i < inputSize; i++) {
|
|
484
|
-
const wIdx = (d * inputSize + i) % (model.buffer.size / 4);
|
|
485
|
-
// Since we can't directly read GPU buffer synchronously,
|
|
486
|
-
// use the input tokens as a proxy hash for embedding
|
|
487
|
-
val += inputData[i] * Math.sin(d * 0.1 + i * 0.01);
|
|
488
|
-
}
|
|
489
|
-
embedding[d] = Math.tanh(val / inputSize);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
return Array.from(embedding);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// ---------------------------------------------------------------------------
|
|
496
|
-
// Message Handler
|
|
497
|
-
// ---------------------------------------------------------------------------
|
|
498
|
-
|
|
499
|
-
self.onmessage = async function(event) {
|
|
500
|
-
const { type, payload } = event.data;
|
|
501
|
-
|
|
502
|
-
try {
|
|
503
|
-
switch (type) {
|
|
504
|
-
case 'init': {
|
|
505
|
-
const result = await initGPU();
|
|
506
|
-
self.postMessage({ type: 'ready', payload: result });
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
case 'loadModel': {
|
|
511
|
-
const { name, url, config } = payload;
|
|
512
|
-
const result = await loadModel(name, url, config || {});
|
|
513
|
-
self.postMessage({ type: 'modelLoaded', payload: result });
|
|
514
|
-
break;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
case 'inference': {
|
|
518
|
-
const { model, prompt, maxTokens, temperature } = payload;
|
|
519
|
-
const output = await runInference(model, prompt, maxTokens, temperature);
|
|
520
|
-
self.postMessage({ type: 'result', payload: { output } });
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
case 'embedding': {
|
|
525
|
-
const { model, text } = payload;
|
|
526
|
-
const embedding = await computeEmbedding(model, text);
|
|
527
|
-
self.postMessage({ type: 'embedding', payload: { embedding } });
|
|
528
|
-
break;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
case 'tokenize': {
|
|
532
|
-
const { text, model } = payload;
|
|
533
|
-
const m = loadedModels.get(model);
|
|
534
|
-
const tokens = tokenize(text, m?.vocab || []);
|
|
535
|
-
self.postMessage({ type: 'tokens', payload: { tokens, count: tokens.length } });
|
|
536
|
-
break;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
case 'status': {
|
|
540
|
-
self.postMessage({
|
|
541
|
-
type: 'status',
|
|
542
|
-
payload: {
|
|
543
|
-
initialized,
|
|
544
|
-
gpuAvailable: !!gpuDevice,
|
|
545
|
-
models: Array.from(loadedModels.keys()),
|
|
546
|
-
modelSizes: Object.fromEntries(
|
|
547
|
-
Array.from(loadedModels.entries()).map(([k, v]) => [k, v.buffer.size])
|
|
548
|
-
),
|
|
549
|
-
},
|
|
550
|
-
});
|
|
551
|
-
break;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
case 'unloadModel': {
|
|
555
|
-
const { name } = payload;
|
|
556
|
-
const m = loadedModels.get(name);
|
|
557
|
-
if (m) {
|
|
558
|
-
m.buffer.destroy();
|
|
559
|
-
loadedModels.delete(name);
|
|
560
|
-
}
|
|
561
|
-
self.postMessage({ type: 'modelUnloaded', payload: { name } });
|
|
562
|
-
break;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
default:
|
|
566
|
-
self.postMessage({ type: 'error', payload: { error: `Unknown message type: ${type}` } });
|
|
567
|
-
}
|
|
568
|
-
} catch (err) {
|
|
569
|
-
self.postMessage({ type: 'error', payload: { error: String(err), stack: err.stack } });
|
|
570
|
-
}
|
|
571
|
-
};
|