@sauravpanda/flare 0.1.0
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/demo/README.md +40 -0
- package/demo/index.html +1767 -0
- package/js/index.ts +91 -0
- package/js/types.ts +136 -0
- package/js/webtransport-loader.js +126 -0
- package/js/worker.ts +159 -0
- package/package.json +58 -0
- package/pkg/flare_web.d.ts +1164 -0
- package/pkg/flare_web.js +2790 -0
- package/pkg/flare_web_bg.wasm +0 -0
- package/pkg/flare_web_bg.wasm.d.ts +105 -0
- package/pkg/package.json +27 -0
package/js/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @aspect/flare
|
|
3
|
+
*
|
|
4
|
+
* WASM-first LLM inference engine with WebGPU acceleration.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { Flare } from '@aspect/flare';
|
|
9
|
+
*
|
|
10
|
+
* const flare = await Flare.init();
|
|
11
|
+
* console.log('WebGPU available:', flare.webgpuAvailable);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Re-export WASM bindings (generated by wasm-pack)
|
|
16
|
+
export { webgpu_available, device_info, init } from '../pkg/flare_web';
|
|
17
|
+
|
|
18
|
+
export interface FlareConfig {
|
|
19
|
+
/** URL to fetch the GGUF model from */
|
|
20
|
+
modelUrl?: string;
|
|
21
|
+
/** Whether to cache model weights in browser Cache API */
|
|
22
|
+
cache?: boolean;
|
|
23
|
+
/** Progress callback: (loaded bytes, total bytes) */
|
|
24
|
+
onProgress?: (loaded: number, total: number) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GenerateOptions {
|
|
28
|
+
/** Input prompt text */
|
|
29
|
+
prompt: string;
|
|
30
|
+
/** Maximum tokens to generate */
|
|
31
|
+
maxTokens?: number;
|
|
32
|
+
/** Sampling temperature (0 = greedy, higher = more random) */
|
|
33
|
+
temperature?: number;
|
|
34
|
+
/** Top-p (nucleus) sampling threshold */
|
|
35
|
+
topP?: number;
|
|
36
|
+
/** Top-k sampling limit */
|
|
37
|
+
topK?: number;
|
|
38
|
+
/** Repeat penalty for previously generated tokens */
|
|
39
|
+
repeatPenalty?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ChatMessage {
|
|
43
|
+
role: 'system' | 'user' | 'assistant';
|
|
44
|
+
content: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ChatOptions {
|
|
48
|
+
messages: ChatMessage[];
|
|
49
|
+
maxTokens?: number;
|
|
50
|
+
temperature?: number;
|
|
51
|
+
topP?: number;
|
|
52
|
+
stream?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* High-level Flare engine interface.
|
|
57
|
+
* Wraps the low-level WASM bindings with a friendly API.
|
|
58
|
+
*/
|
|
59
|
+
export class Flare {
|
|
60
|
+
private constructor() {}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Initialize the Flare engine.
|
|
64
|
+
* Detects WebGPU availability and sets up compute pipelines.
|
|
65
|
+
*/
|
|
66
|
+
static async init(): Promise<Flare> {
|
|
67
|
+
const { init } = await import('../pkg/flare_web');
|
|
68
|
+
await init();
|
|
69
|
+
return new Flare();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Check if WebGPU is available in this browser. */
|
|
73
|
+
get webgpuAvailable(): boolean {
|
|
74
|
+
try {
|
|
75
|
+
const { webgpu_available } = require('../pkg/flare_web');
|
|
76
|
+
return webgpu_available();
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Get device capability info as JSON. */
|
|
83
|
+
get deviceInfo(): Record<string, unknown> {
|
|
84
|
+
try {
|
|
85
|
+
const { device_info } = require('../pkg/flare_web');
|
|
86
|
+
return JSON.parse(device_info());
|
|
87
|
+
} catch {
|
|
88
|
+
return { webgpu: false };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
package/js/types.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript type definitions for the Flare WASM module.
|
|
3
|
+
* These map to the wasm-bindgen exports from flare-web.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** Check if WebGPU is available in this browser. */
|
|
7
|
+
export declare function webgpu_available(): boolean;
|
|
8
|
+
|
|
9
|
+
/** Get JSON string with device capabilities. */
|
|
10
|
+
export declare function device_info(): string;
|
|
11
|
+
|
|
12
|
+
/** Initialize the Flare engine (async). */
|
|
13
|
+
export declare function init(): Promise<string>;
|
|
14
|
+
|
|
15
|
+
/** Progress callback: (loaded_bytes, total_bytes). total_bytes is 0 when unknown. */
|
|
16
|
+
export type ProgressCallback = (loaded: number, total: number) => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The Flare inference engine.
|
|
20
|
+
* Load a GGUF model, then use the streaming API (begin_stream / next_token)
|
|
21
|
+
* or the batch API (generate_tokens) to run inference.
|
|
22
|
+
*/
|
|
23
|
+
export declare class FlareEngine {
|
|
24
|
+
/** Load a GGUF model from raw bytes. */
|
|
25
|
+
static load(ggufBytes: Uint8Array): FlareEngine;
|
|
26
|
+
/** Reset the KV cache (start a new conversation). */
|
|
27
|
+
reset(): void;
|
|
28
|
+
/** Vocabulary size. */
|
|
29
|
+
readonly vocab_size: number;
|
|
30
|
+
/** Number of transformer layers. */
|
|
31
|
+
readonly num_layers: number;
|
|
32
|
+
/** Hidden dimension. */
|
|
33
|
+
readonly hidden_dim: number;
|
|
34
|
+
/**
|
|
35
|
+
* Auto-detected chat template name: "ChatML", "Llama3", "Alpaca", or "Raw".
|
|
36
|
+
* Detected from the GGUF tokenizer.chat_template metadata, falling back to
|
|
37
|
+
* architecture-based detection.
|
|
38
|
+
*/
|
|
39
|
+
readonly chat_template_name: string;
|
|
40
|
+
/**
|
|
41
|
+
* EOS token ID read from the GGUF model metadata, if present.
|
|
42
|
+
* The generator stops automatically when this token is produced.
|
|
43
|
+
*/
|
|
44
|
+
readonly eos_token_id: number | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Format a user message and optional system prompt using the model's chat
|
|
47
|
+
* template. Pass the result to FlareTokenizer.encode() before generating.
|
|
48
|
+
* Pass an empty string for systemMessage to omit the system turn.
|
|
49
|
+
*/
|
|
50
|
+
apply_chat_template(userMessage: string, systemMessage: string): string;
|
|
51
|
+
|
|
52
|
+
// --- Token-by-token streaming API ---
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prepare for token-by-token streaming. Runs the prefill pass on
|
|
56
|
+
* promptTokens and initialises internal streaming state. Call engine.reset()
|
|
57
|
+
* first to start a fresh conversation, then call next_token() in a
|
|
58
|
+
* requestAnimationFrame loop.
|
|
59
|
+
*/
|
|
60
|
+
begin_stream(promptTokens: Uint32Array, maxTokens: number): void;
|
|
61
|
+
/**
|
|
62
|
+
* Generate the next token and return its ID, or undefined when the stream is
|
|
63
|
+
* complete (EOS reached, maxTokens exhausted, or stop_stream() was called).
|
|
64
|
+
* Call inside requestAnimationFrame so the browser can update the DOM between
|
|
65
|
+
* tokens.
|
|
66
|
+
*/
|
|
67
|
+
next_token(): number | undefined;
|
|
68
|
+
/** Signal the current stream to stop after the next next_token() call. */
|
|
69
|
+
stop_stream(): void;
|
|
70
|
+
/** Whether the current stream has finished. */
|
|
71
|
+
readonly stream_done: boolean;
|
|
72
|
+
|
|
73
|
+
// --- Batch generation API (returns all tokens at once) ---
|
|
74
|
+
|
|
75
|
+
/** Generate tokens (greedy, temperature=0). Stops at EOS. Returns token ID array. */
|
|
76
|
+
generate_tokens(promptTokens: Uint32Array, maxTokens: number): Uint32Array;
|
|
77
|
+
/** Generate tokens with sampling parameters. Stops at EOS. */
|
|
78
|
+
generate_with_params(
|
|
79
|
+
promptTokens: Uint32Array,
|
|
80
|
+
maxTokens: number,
|
|
81
|
+
temperature: number,
|
|
82
|
+
topP: number
|
|
83
|
+
): Uint32Array;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Progressive loader: fetches a GGUF model from a URL with streaming download
|
|
88
|
+
* progress, then parses and returns a FlareEngine.
|
|
89
|
+
*/
|
|
90
|
+
export declare class FlareProgressiveLoader {
|
|
91
|
+
constructor(url: string);
|
|
92
|
+
/** Fetch, stream, and parse the model. Calls onProgress as chunks arrive. */
|
|
93
|
+
load(onProgress: ProgressCallback): Promise<FlareEngine>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* BPE tokenizer: encode text to token IDs and decode token IDs back to text.
|
|
98
|
+
* Load from a HuggingFace tokenizer.json string.
|
|
99
|
+
*/
|
|
100
|
+
export declare class FlareTokenizer {
|
|
101
|
+
/** Load from a tokenizer.json string. */
|
|
102
|
+
static from_json(json: string): FlareTokenizer;
|
|
103
|
+
/** Encode text to token IDs. */
|
|
104
|
+
encode(text: string): Uint32Array;
|
|
105
|
+
/** Decode token IDs to text. */
|
|
106
|
+
decode(tokens: Uint32Array): string;
|
|
107
|
+
/** Decode a single token ID to text (for streaming). */
|
|
108
|
+
decode_one(tokenId: number): string;
|
|
109
|
+
/** BOS token ID (may be undefined). */
|
|
110
|
+
readonly bos_token_id: number | undefined;
|
|
111
|
+
/** EOS token ID (may be undefined). */
|
|
112
|
+
readonly eos_token_id: number | undefined;
|
|
113
|
+
/** Vocabulary size. */
|
|
114
|
+
readonly vocab_size: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* BPE tokenizer: encode text to token IDs and decode token IDs back to text.
|
|
119
|
+
* Load from a HuggingFace tokenizer.json string.
|
|
120
|
+
*/
|
|
121
|
+
export declare class FlareTokenizer {
|
|
122
|
+
/** Load from a tokenizer.json string. */
|
|
123
|
+
static from_json(json: string): FlareTokenizer;
|
|
124
|
+
/** Encode text to token IDs. */
|
|
125
|
+
encode(text: string): Uint32Array;
|
|
126
|
+
/** Decode token IDs to text. */
|
|
127
|
+
decode(tokens: Uint32Array): string;
|
|
128
|
+
/** Decode a single token ID to text (for streaming). */
|
|
129
|
+
decode_one(tokenId: number): string;
|
|
130
|
+
/** BOS token ID (may be undefined). */
|
|
131
|
+
readonly bos_token_id: number | undefined;
|
|
132
|
+
/** EOS token ID (may be undefined). */
|
|
133
|
+
readonly eos_token_id: number | undefined;
|
|
134
|
+
/** Vocabulary size. */
|
|
135
|
+
readonly vocab_size: number;
|
|
136
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Progressive model loader using WebTransport for parallel stream downloads.
|
|
3
|
+
*
|
|
4
|
+
* WebTransport is built on HTTP/3 QUIC and allows multiple bidirectional
|
|
5
|
+
* streams over a single connection, avoiding the head-of-line blocking that
|
|
6
|
+
* plagues HTTP/1.1/2 range requests when downloading large model files.
|
|
7
|
+
*
|
|
8
|
+
* NOTE ON SERVER SUPPORT:
|
|
9
|
+
* For parallel stream downloads to actually happen, the server must:
|
|
10
|
+
* 1. Speak HTTP/3 and expose a WebTransport endpoint at the model URL.
|
|
11
|
+
* 2. Accept a per-stream protocol where the client sends a byte range
|
|
12
|
+
* (e.g. as a small framed message) and the server streams those bytes
|
|
13
|
+
* back on the same bidirectional stream.
|
|
14
|
+
* No such server is shipped with Flare today. Until one exists, this loader
|
|
15
|
+
* transparently falls back to `fetch()` with streaming, which still gives
|
|
16
|
+
* progressive load + progress callbacks over HTTP/1.1 or HTTP/2.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* const loader = new WebTransportLoader('https://example.com/model.gguf', 4);
|
|
20
|
+
* const bytes = await loader.load((loaded, total) => {
|
|
21
|
+
* console.log(`${loaded} / ${total}`);
|
|
22
|
+
* });
|
|
23
|
+
*/
|
|
24
|
+
export class WebTransportLoader {
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} url Absolute URL to the model file.
|
|
27
|
+
* @param {number} numStreams Number of parallel streams to attempt when
|
|
28
|
+
* WebTransport + a cooperating server are
|
|
29
|
+
* available. Ignored by the fetch fallback.
|
|
30
|
+
*/
|
|
31
|
+
constructor(url, numStreams = 4) {
|
|
32
|
+
this.url = url;
|
|
33
|
+
this.numStreams = numStreams;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Load the model bytes, invoking `onProgress(loaded, total)` as data
|
|
38
|
+
* arrives. Returns a Uint8Array containing the full file.
|
|
39
|
+
*
|
|
40
|
+
* @param {(loaded: number, total: number) => void} [onProgress]
|
|
41
|
+
* @returns {Promise<Uint8Array>}
|
|
42
|
+
*/
|
|
43
|
+
async load(onProgress) {
|
|
44
|
+
if (typeof WebTransport === 'undefined') {
|
|
45
|
+
// Browser has no WebTransport at all — use fetch streaming.
|
|
46
|
+
return this.loadViaFetch(onProgress);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const wt = new WebTransport(this.url);
|
|
51
|
+
await wt.ready;
|
|
52
|
+
|
|
53
|
+
// Server-side protocol for parallel range streaming is not yet
|
|
54
|
+
// standardized in this project. Once a Flare WebTransport server
|
|
55
|
+
// exists, this block should:
|
|
56
|
+
// 1. HEAD the resource (or open a control stream) to learn the
|
|
57
|
+
// total byte length.
|
|
58
|
+
// 2. Open `this.numStreams` bidirectional streams via
|
|
59
|
+
// `wt.createBidirectionalStream()`.
|
|
60
|
+
// 3. Send a framed { offset, length } request on each stream.
|
|
61
|
+
// 4. Reassemble the chunks in offset order as they arrive,
|
|
62
|
+
// invoking `onProgress` on every chunk.
|
|
63
|
+
//
|
|
64
|
+
// Until that server exists, we close the WebTransport session and
|
|
65
|
+
// fall back to fetch — the browser has already done a QUIC
|
|
66
|
+
// handshake, which is wasted work but harmless. We log so that
|
|
67
|
+
// anyone instrumenting this path can see it.
|
|
68
|
+
console.info(
|
|
69
|
+
'WebTransport session opened, but no parallel-range server ' +
|
|
70
|
+
'protocol is implemented yet; falling back to fetch().'
|
|
71
|
+
);
|
|
72
|
+
await wt.close();
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.warn('WebTransport failed, falling back to fetch:', e);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return this.loadViaFetch(onProgress);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Fallback path: stream the body via `fetch()` and report progress from
|
|
82
|
+
* the Content-Length header. Works on any HTTP/1.1+ origin.
|
|
83
|
+
*
|
|
84
|
+
* @param {(loaded: number, total: number) => void} [onProgress]
|
|
85
|
+
* @returns {Promise<Uint8Array>}
|
|
86
|
+
*/
|
|
87
|
+
async loadViaFetch(onProgress) {
|
|
88
|
+
const response = await fetch(this.url);
|
|
89
|
+
if (!response.ok) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Failed to fetch model: ${response.status} ${response.statusText}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (!response.body) {
|
|
95
|
+
// Non-streaming environment (very old browser or CORS-restricted).
|
|
96
|
+
const buf = new Uint8Array(await response.arrayBuffer());
|
|
97
|
+
if (onProgress) onProgress(buf.length, buf.length);
|
|
98
|
+
return buf;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const contentLength = parseInt(
|
|
102
|
+
response.headers.get('content-length') || '0',
|
|
103
|
+
10
|
|
104
|
+
);
|
|
105
|
+
const reader = response.body.getReader();
|
|
106
|
+
const chunks = [];
|
|
107
|
+
let loaded = 0;
|
|
108
|
+
|
|
109
|
+
while (true) {
|
|
110
|
+
const { done, value } = await reader.read();
|
|
111
|
+
if (done) break;
|
|
112
|
+
chunks.push(value);
|
|
113
|
+
loaded += value.length;
|
|
114
|
+
if (onProgress) onProgress(loaded, contentLength);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Concatenate chunks into a single contiguous Uint8Array.
|
|
118
|
+
const result = new Uint8Array(loaded);
|
|
119
|
+
let offset = 0;
|
|
120
|
+
for (const chunk of chunks) {
|
|
121
|
+
result.set(chunk, offset);
|
|
122
|
+
offset += chunk.length;
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
}
|
package/js/worker.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web Worker bootstrap for running Flare inference off the main thread.
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - Main thread sends messages: { type: 'init' | 'generate', ... }
|
|
6
|
+
* - Worker runs WASM inference and posts back tokens as they're generated
|
|
7
|
+
* - All GPU operations happen in the worker (WebGPU is available in workers)
|
|
8
|
+
*
|
|
9
|
+
* Usage from main thread:
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
|
|
12
|
+
* worker.postMessage({ type: 'init' });
|
|
13
|
+
* worker.postMessage({ type: 'generate', prompt: 'Hello', maxTokens: 128 });
|
|
14
|
+
* worker.onmessage = (e) => {
|
|
15
|
+
* if (e.data.type === 'token') console.log(e.data.text);
|
|
16
|
+
* if (e.data.type === 'done') console.log('Generation complete');
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Message types from main thread to worker
|
|
22
|
+
interface InitMessage {
|
|
23
|
+
type: 'init';
|
|
24
|
+
modelUrl?: string;
|
|
25
|
+
wasmUrl?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface GenerateMessage {
|
|
29
|
+
type: 'generate';
|
|
30
|
+
prompt: string;
|
|
31
|
+
maxTokens?: number;
|
|
32
|
+
temperature?: number;
|
|
33
|
+
topP?: number;
|
|
34
|
+
topK?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface AbortMessage {
|
|
38
|
+
type: 'abort';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type IncomingMessage = InitMessage | GenerateMessage | AbortMessage;
|
|
42
|
+
|
|
43
|
+
// Message types from worker to main thread
|
|
44
|
+
interface ReadyMessage {
|
|
45
|
+
type: 'ready';
|
|
46
|
+
webgpu: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface TokenMessage {
|
|
50
|
+
type: 'token';
|
|
51
|
+
text: string;
|
|
52
|
+
tokenId: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface DoneMessage {
|
|
56
|
+
type: 'done';
|
|
57
|
+
totalTokens: number;
|
|
58
|
+
tokensPerSecond: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface ErrorMessage {
|
|
62
|
+
type: 'error';
|
|
63
|
+
message: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ProgressMessage {
|
|
67
|
+
type: 'progress';
|
|
68
|
+
loaded: number;
|
|
69
|
+
total: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type OutgoingMessage = ReadyMessage | TokenMessage | DoneMessage | ErrorMessage | ProgressMessage;
|
|
73
|
+
|
|
74
|
+
// Worker state
|
|
75
|
+
let initialized = false;
|
|
76
|
+
let aborted = false;
|
|
77
|
+
|
|
78
|
+
function postResult(msg: OutgoingMessage) {
|
|
79
|
+
(self as unknown as { postMessage(msg: OutgoingMessage): void }).postMessage(msg);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function handleInit(_msg: InitMessage) {
|
|
83
|
+
try {
|
|
84
|
+
// Import and initialize WASM module
|
|
85
|
+
// In a real build, this path comes from wasm-pack output
|
|
86
|
+
const flare = await import('../pkg/flare_web.js');
|
|
87
|
+
await flare.init();
|
|
88
|
+
|
|
89
|
+
const webgpu = flare.webgpu_available();
|
|
90
|
+
initialized = true;
|
|
91
|
+
|
|
92
|
+
postResult({ type: 'ready', webgpu });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
postResult({
|
|
95
|
+
type: 'error',
|
|
96
|
+
message: `Init failed: ${err}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function handleGenerate(msg: GenerateMessage) {
|
|
102
|
+
if (!initialized) {
|
|
103
|
+
postResult({ type: 'error', message: 'Worker not initialized. Send init message first.' });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
aborted = false;
|
|
108
|
+
const startTime = performance.now();
|
|
109
|
+
let tokenCount = 0;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
// TODO: Wire up actual WASM inference here
|
|
113
|
+
// For now, simulate token generation to validate the worker protocol
|
|
114
|
+
const maxTokens = msg.maxTokens ?? 128;
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < maxTokens && !aborted; i++) {
|
|
117
|
+
// In real implementation: call flare WASM generate step
|
|
118
|
+
tokenCount++;
|
|
119
|
+
|
|
120
|
+
postResult({
|
|
121
|
+
type: 'token',
|
|
122
|
+
text: ' ',
|
|
123
|
+
tokenId: i,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Yield to allow abort messages to be processed
|
|
127
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const elapsed = (performance.now() - startTime) / 1000;
|
|
131
|
+
postResult({
|
|
132
|
+
type: 'done',
|
|
133
|
+
totalTokens: tokenCount,
|
|
134
|
+
tokensPerSecond: tokenCount / elapsed,
|
|
135
|
+
});
|
|
136
|
+
} catch (err) {
|
|
137
|
+
postResult({
|
|
138
|
+
type: 'error',
|
|
139
|
+
message: `Generation failed: ${err}`,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Message handler
|
|
145
|
+
self.onmessage = async (event: MessageEvent<IncomingMessage>) => {
|
|
146
|
+
const msg = event.data;
|
|
147
|
+
|
|
148
|
+
switch (msg.type) {
|
|
149
|
+
case 'init':
|
|
150
|
+
await handleInit(msg);
|
|
151
|
+
break;
|
|
152
|
+
case 'generate':
|
|
153
|
+
await handleGenerate(msg);
|
|
154
|
+
break;
|
|
155
|
+
case 'abort':
|
|
156
|
+
aborted = true;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sauravpanda/flare",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WASM-first LLM inference engine with WebGPU acceleration — run LLMs in the browser with zero server costs",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "pkg/flare_web.js",
|
|
7
|
+
"module": "pkg/flare_web.js",
|
|
8
|
+
"types": "pkg/flare_web.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"pkg/flare_web_bg.wasm",
|
|
11
|
+
"pkg/flare_web_bg.wasm.d.ts",
|
|
12
|
+
"pkg/flare_web.js",
|
|
13
|
+
"pkg/flare_web.d.ts",
|
|
14
|
+
"pkg/package.json",
|
|
15
|
+
"js/",
|
|
16
|
+
"demo/"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "wasm-pack build --target web --out-dir pkg",
|
|
20
|
+
"build:node": "wasm-pack build --target nodejs --out-dir pkg-node",
|
|
21
|
+
"build:bundler": "wasm-pack build --target bundler --out-dir pkg-bundler",
|
|
22
|
+
"prepublishOnly": "npm run build"
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"import": "./pkg/flare_web.js",
|
|
27
|
+
"types": "./pkg/flare_web.d.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"llm",
|
|
32
|
+
"wasm",
|
|
33
|
+
"webgpu",
|
|
34
|
+
"inference",
|
|
35
|
+
"ai",
|
|
36
|
+
"rust",
|
|
37
|
+
"browser",
|
|
38
|
+
"gguf",
|
|
39
|
+
"transformer"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT OR Apache-2.0",
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/sauravpanda/flarellm.git",
|
|
45
|
+
"directory": "flare-web"
|
|
46
|
+
},
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/sauravpanda/flarellm/issues"
|
|
49
|
+
},
|
|
50
|
+
"homepage": "https://github.com/sauravpanda/flarellm#readme",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public",
|
|
53
|
+
"registry": "https://registry.npmjs.org/"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
}
|
|
58
|
+
}
|