Spaces:
Running
Running
| import { describe, expect, it } from 'vitest'; | |
| import type { Wllama } from '../../vendor/wllama-bonsai/esm/index.js'; | |
| import { verifyBlobSha256 } from './blob-integrity'; | |
| import { EngineRuntimeError } from './errors'; | |
| import type { ManifestModelV2 } from './manifest'; | |
| import { isShardDownloadFailureDetails, type EngineEvent } from './protocol'; | |
| import { | |
| BrowserEngineRuntime, | |
| resolveLoadTuning, | |
| resolveRuntimeBatchShape, | |
| selectWasmFlavor, | |
| } from './runtime'; | |
| type DownloadOptions = { | |
| signal?: AbortSignal; | |
| headers?: Record<string, string>; | |
| progressCallback?: (progress: { loaded: number; total: number }) => void; | |
| }; | |
| class FakeCacheManager { | |
| readonly blobs = new Map<string, Blob>(); | |
| readonly downloads: Array<{ url: string; headers?: Record<string, string> }> = []; | |
| readonly deletes: string[] = []; | |
| readonly payloads = new Map<string, Blob>(); | |
| failNetworkFor: string | null = null; | |
| readonly waitForAbortFor = new Set<string>(); | |
| private readonly downloadWaiters: Array<{ count: number; resolve: () => void }> = []; | |
| waitForDownloads(count: number): Promise<void> { | |
| if (this.downloads.length >= count) return Promise.resolve(); | |
| return new Promise<void>((resolve) => this.downloadWaiters.push({ count, resolve })); | |
| } | |
| private resolveDownloadWaiters(): void { | |
| for (let index = this.downloadWaiters.length - 1; index >= 0; index -= 1) { | |
| const waiter = this.downloadWaiters[index]; | |
| if (waiter && this.downloads.length >= waiter.count) { | |
| this.downloadWaiters.splice(index, 1); | |
| waiter.resolve(); | |
| } | |
| } | |
| } | |
| async download(url: string, options: DownloadOptions = {}): Promise<void> { | |
| this.downloads.push({ url, ...(options.headers ? { headers: options.headers } : {}) }); | |
| this.resolveDownloadWaiters(); | |
| const payload = this.payloads.get(url) ?? new Blob(['partial']); | |
| this.blobs.set(url, payload); | |
| options.progressCallback?.({ loaded: payload.size, total: payload.size }); | |
| if (this.failNetworkFor === url) { | |
| throw new TypeError('synthetic network failure'); | |
| } | |
| if (this.waitForAbortFor.has(url)) { | |
| await new Promise<never>((_resolve, reject) => { | |
| const rejectAborted = () => reject(new DOMException('synthetic abort', 'AbortError')); | |
| if (options.signal?.aborted) rejectAborted(); | |
| else options.signal?.addEventListener('abort', rejectAborted, { once: true }); | |
| }); | |
| } | |
| } | |
| async open(url: string): Promise<Blob | null> { | |
| return this.blobs.get(url) ?? null; | |
| } | |
| async delete(url: string): Promise<void> { | |
| this.deletes.push(url); | |
| this.blobs.delete(url); | |
| } | |
| async getNameFromURL(url: string): Promise<string> { | |
| return `cache:${url}`; | |
| } | |
| async writeMetadata(): Promise<void> {} | |
| } | |
| type DownloadShards = ( | |
| requestId: string, | |
| wllama: Wllama, | |
| model: ManifestModelV2, | |
| urls: readonly string[], | |
| cached: { blobs: Array<Blob | null>; cachedBytes: number }, | |
| signal: AbortSignal, | |
| sink: (event: EngineEvent) => void, | |
| ) => Promise<Blob[]>; | |
| async function sha256(blob: Blob): Promise<string> { | |
| return (await verifyBlobSha256(blob, '')).actualSha256; | |
| } | |
| async function fixture(first: Blob, second: Blob): Promise<{ | |
| model: ManifestModelV2; | |
| urls: [string, string]; | |
| }> { | |
| const result = await fixtureShards([first, second]); | |
| const firstUrl = result.urls[0]; | |
| const secondUrl = result.urls[1]; | |
| if (!firstUrl || !secondUrl) throw new Error('Two-shard fixture is incomplete.'); | |
| return { model: result.model, urls: [firstUrl, secondUrl] }; | |
| } | |
| async function fixtureShards(payloads: readonly Blob[]): Promise<{ | |
| model: ManifestModelV2; | |
| urls: string[]; | |
| }> { | |
| const urls = payloads.map((_, index) => `https://models.test/shard-${index}.gguf`); | |
| const files = await Promise.all(payloads.map(async (payload, index) => ({ | |
| path: `shard-${index}.gguf`, | |
| bytes: payload.size, | |
| sha256: await sha256(payload), | |
| }))); | |
| return { | |
| urls, | |
| model: { | |
| id: '1_7b', | |
| displayName: 'Test Bonsai', | |
| architecture: 'test', | |
| source: { repo: 'test/model', revision: 'a'.repeat(40), file: 'model.gguf', bytes: 1, sha256: '0'.repeat(64) }, | |
| files, | |
| downloadBytes: payloads.reduce((sum, payload) => sum + payload.size, 0), | |
| contextLength: 2048, | |
| defaultContext: 1024, | |
| cpuFallback: true, | |
| largestTensorBytes: 1, | |
| requiredLimits: {}, | |
| chatTemplate: { bytes: 1, sha256: '0'.repeat(64), markers: { think: false, toolCall: false, toolResponse: false } }, | |
| hybridDimensions: {}, | |
| nextNTensorCount: 0, | |
| runtimePolicy: { flashAttention: false, tokenEmbeddingOnWebGPU: true, requireSingleWebGPUGraph: true }, | |
| }, | |
| }; | |
| } | |
| function downloader(runtime: BrowserEngineRuntime): DownloadShards { | |
| return (runtime as unknown as { downloadShards: DownloadShards }).downloadShards.bind(runtime); | |
| } | |
| describe('BrowserEngineRuntime per-shard retry', () => { | |
| it('deletes a network-failed partial and retries only that shard with a full GET', async () => { | |
| const verified = new Blob(['verified']); | |
| const retryPayload = new Blob(['retry-ok']); | |
| const { model, urls } = await fixture(verified, retryPayload); | |
| const cache = new FakeCacheManager(); | |
| cache.payloads.set(urls[1], retryPayload); | |
| cache.failNetworkFor = urls[1]; | |
| const cached = { blobs: [verified, null], cachedBytes: verified.size }; | |
| const download = downloader(new BrowserEngineRuntime()); | |
| const failed = download( | |
| 'request-1', | |
| { cacheManager: cache } as unknown as Wllama, | |
| model, | |
| urls, | |
| cached, | |
| new AbortController().signal, | |
| () => undefined, | |
| ); | |
| await expect(failed).rejects.toSatisfy((error: unknown) => { | |
| if (!(error instanceof EngineRuntimeError) || !isShardDownloadFailureDetails(error.details)) return false; | |
| expect(error.code).toBe('SHARD_DOWNLOAD_FAILED'); | |
| expect(error.details).toMatchObject({ | |
| failure: 'network', | |
| shardIndex: 1, | |
| shardCount: 2, | |
| shardPath: 'shard-1.gguf', | |
| retryFromByteZero: true, | |
| partialDeleted: true, | |
| }); | |
| return true; | |
| }); | |
| expect(cache.deletes).toEqual([urls[1]]); | |
| expect(cache.blobs.has(urls[1])).toBe(false); | |
| expect(cached.blobs).toEqual([verified, null]); | |
| cache.failNetworkFor = null; | |
| const blobs = await download( | |
| 'request-2', | |
| { cacheManager: cache } as unknown as Wllama, | |
| model, | |
| urls, | |
| cached, | |
| new AbortController().signal, | |
| () => undefined, | |
| ); | |
| expect(blobs).toEqual([verified, retryPayload]); | |
| expect(cache.downloads.map(({ url }) => url)).toEqual([urls[1], urls[1]]); | |
| expect(cache.downloads.every(({ headers }) => headers?.Range === undefined)).toBe(true); | |
| }); | |
| it('deletes a same-size shard that fails SHA-256 verification', async () => { | |
| const expected = new Blob(['good']); | |
| const corrupted = new Blob(['evil']); | |
| const { model, urls } = await fixture(new Blob(['cached']), expected); | |
| const cache = new FakeCacheManager(); | |
| cache.payloads.set(urls[1], corrupted); | |
| const cached = { blobs: [new Blob(['cached']), null], cachedBytes: 6 }; | |
| await expect(downloader(new BrowserEngineRuntime())( | |
| 'request-hash', | |
| { cacheManager: cache } as unknown as Wllama, | |
| model, | |
| urls, | |
| cached, | |
| new AbortController().signal, | |
| () => undefined, | |
| )).rejects.toSatisfy((error: unknown) => ( | |
| error instanceof EngineRuntimeError | |
| && error.code === 'SHARD_HASH_MISMATCH' | |
| && isShardDownloadFailureDetails(error.details) | |
| && error.details.failure === 'verification' | |
| && error.details.retryFromByteZero | |
| )); | |
| expect(cache.deletes).toEqual([urls[1]]); | |
| expect(cache.blobs.has(urls[1])).toBe(false); | |
| }); | |
| it('stops dequeue after a network failure, lets in-flight siblings finish, and retries only 0 + 3', async () => { | |
| const payloads = [ | |
| new Blob(['retry-zero']), | |
| new Blob(['sibling-one']), | |
| new Blob(['sibling-two']), | |
| new Blob(['queued-three']), | |
| ]; | |
| const { model, urls } = await fixtureShards(payloads); | |
| const cache = new FakeCacheManager(); | |
| urls.forEach((url, index) => cache.payloads.set(url, payloads[index] as Blob)); | |
| cache.failNetworkFor = urls[0] ?? null; | |
| const cached = { blobs: payloads.map(() => null), cachedBytes: 0 }; | |
| const progress: number[] = []; | |
| const download = downloader(new BrowserEngineRuntime()); | |
| await expect(download( | |
| 'request-parallel-failure', | |
| { cacheManager: cache } as unknown as Wllama, | |
| model, | |
| urls, | |
| cached, | |
| new AbortController().signal, | |
| (event) => { | |
| if (event.event === 'progress') progress.push(event.loadedBytes); | |
| }, | |
| )).rejects.toMatchObject({ code: 'SHARD_DOWNLOAD_FAILED' }); | |
| expect(cache.downloads.map(({ url }) => url)).toEqual(urls.slice(0, 3)); | |
| expect(cache.downloads.some(({ url }) => url === urls[3])).toBe(false); | |
| expect(cache.deletes).toEqual([urls[0]]); | |
| expect(cached.blobs).toEqual([null, payloads[1], payloads[2], null]); | |
| expect(progress[progress.length - 1]).toBe(payloads[1]!.size + payloads[2]!.size); | |
| cache.failNetworkFor = null; | |
| const retryStart = cache.downloads.length; | |
| const blobs = await download( | |
| 'request-parallel-retry', | |
| { cacheManager: cache } as unknown as Wllama, | |
| model, | |
| urls, | |
| cached, | |
| new AbortController().signal, | |
| () => undefined, | |
| ); | |
| expect(blobs).toEqual(payloads); | |
| expect(cache.downloads.slice(retryStart).map(({ url }) => url)).toEqual([urls[0], urls[3]]); | |
| expect(cache.downloads.slice(retryStart).every(({ headers }) => headers?.Range === undefined)).toBe(true); | |
| }); | |
| it('aborts all in-flight shards, cleans each partial, and never dequeues the fourth shard', async () => { | |
| const expected = [ | |
| new Blob(['complete-zero']), | |
| new Blob(['complete-one']), | |
| new Blob(['complete-two']), | |
| new Blob(['complete-three']), | |
| ]; | |
| const partials = [new Blob(['p0']), new Blob(['p1']), new Blob(['p2'])]; | |
| const { model, urls } = await fixtureShards(expected); | |
| const cache = new FakeCacheManager(); | |
| urls.slice(0, 3).forEach((url, index) => { | |
| cache.payloads.set(url, partials[index] as Blob); | |
| cache.waitForAbortFor.add(url); | |
| }); | |
| const cached = { blobs: expected.map(() => null), cachedBytes: 0 }; | |
| const progress: number[] = []; | |
| const controller = new AbortController(); | |
| const failure = downloader(new BrowserEngineRuntime())( | |
| 'request-abort', | |
| { cacheManager: cache } as unknown as Wllama, | |
| model, | |
| urls, | |
| cached, | |
| controller.signal, | |
| (event) => { | |
| if (event.event === 'progress') progress.push(event.loadedBytes); | |
| }, | |
| ); | |
| await cache.waitForDownloads(3); | |
| controller.abort(); | |
| await expect(failure).rejects.toSatisfy((error: unknown) => ( | |
| error instanceof EngineRuntimeError | |
| && error.code === 'SHARD_DOWNLOAD_ABORTED' | |
| && isShardDownloadFailureDetails(error.details) | |
| && error.details.failure === 'abort' | |
| && error.details.shardIndex < 3 | |
| && error.details.shardCount === 4 | |
| && error.details.shardPath === `shard-${error.details.shardIndex}.gguf` | |
| && error.details.partialDeleted | |
| )); | |
| expect(cache.downloads.map(({ url }) => url)).toEqual(urls.slice(0, 3)); | |
| expect(cache.downloads.some(({ url }) => url === urls[3])).toBe(false); | |
| expect([...cache.deletes].sort()).toEqual([...urls.slice(0, 3)].sort()); | |
| expect(urls.slice(0, 3).every((url) => !cache.blobs.has(url))).toBe(true); | |
| expect(cached.blobs).toEqual([null, null, null, null]); | |
| expect(progress[progress.length - 1]).toBe(0); | |
| }); | |
| }); | |
| describe('resolveLoadTuning', () => { | |
| const benchmarkDefaults = { | |
| flashMode: 'off', | |
| kvCacheType: 'f16', | |
| wasmFlavor: 'auto', | |
| } as const; | |
| function tuningError( | |
| input: unknown, | |
| backend: 'auto' | 'webgpu' | 'wasm' = 'webgpu', | |
| ): EngineRuntimeError { | |
| try { | |
| resolveLoadTuning(input, backend); | |
| } catch (error) { | |
| if (error instanceof EngineRuntimeError) return error; | |
| throw error; | |
| } | |
| throw new Error('Expected benchmark tuning to be rejected.'); | |
| } | |
| it('uses immutable release defaults when benchmark tuning is omitted', () => { | |
| expect(resolveLoadTuning(undefined, 'auto')).toEqual({ | |
| scope: 'release-defaults', | |
| nBatch: null, | |
| nUbatch: null, | |
| flashMode: 'off', | |
| kvCacheType: 'f16', | |
| wasmFlavor: 'auto', | |
| }); | |
| expect(resolveLoadTuning(undefined, 'wasm')).toEqual({ | |
| scope: 'release-defaults', | |
| nBatch: null, | |
| nUbatch: null, | |
| flashMode: 'off', | |
| kvCacheType: 'f16', | |
| wasmFlavor: 'auto', | |
| }); | |
| }); | |
| it('accepts an explicit benchmark batch shape without changing release defaults', () => { | |
| expect(resolveLoadTuning({ | |
| ...benchmarkDefaults, | |
| nBatch: 512, | |
| nUbatch: 128, | |
| }, 'auto')).toEqual({ | |
| scope: 'benchmark', | |
| nBatch: 512, | |
| nUbatch: 128, | |
| flashMode: 'off', | |
| kvCacheType: 'f16', | |
| wasmFlavor: 'auto', | |
| }); | |
| }); | |
| it('rejects a microbatch larger than the requested batch', () => { | |
| expect(tuningError({ | |
| ...benchmarkDefaults, | |
| nBatch: 128, | |
| nUbatch: 256, | |
| })).toMatchObject({ | |
| code: 'INVALID_BENCHMARK_TUNING', | |
| message: 'Benchmark n_ubatch must not exceed n_batch.', | |
| }); | |
| }); | |
| it('accepts Flash Attention and quantized KV only on the explicit WebGPU backend', () => { | |
| expect(resolveLoadTuning({ | |
| ...benchmarkDefaults, | |
| flashMode: 'auto', | |
| }, 'webgpu')).toMatchObject({ | |
| scope: 'benchmark', | |
| flashMode: 'auto', | |
| kvCacheType: 'f16', | |
| wasmFlavor: 'auto', | |
| }); | |
| for (const kvCacheType of ['q8_0', 'q4_0'] as const) { | |
| expect(resolveLoadTuning({ | |
| ...benchmarkDefaults, | |
| flashMode: 'auto', | |
| kvCacheType, | |
| }, 'webgpu')).toMatchObject({ | |
| scope: 'benchmark', | |
| flashMode: 'auto', | |
| kvCacheType, | |
| wasmFlavor: 'auto', | |
| }); | |
| } | |
| expect(tuningError({ | |
| ...benchmarkDefaults, | |
| flashMode: 'auto', | |
| }, 'auto')).toMatchObject({ | |
| code: 'INVALID_BENCHMARK_TUNING', | |
| }); | |
| expect(tuningError({ | |
| ...benchmarkDefaults, | |
| flashMode: 'auto', | |
| kvCacheType: 'q8_0', | |
| }, 'wasm')).toMatchObject({ | |
| code: 'INVALID_BENCHMARK_TUNING', | |
| }); | |
| }); | |
| it('rejects malformed and incomplete benchmark tuning objects', () => { | |
| for (const input of [null, [], 'off', 42]) { | |
| expect(tuningError(input)).toMatchObject({ code: 'INVALID_BENCHMARK_TUNING' }); | |
| } | |
| for (const input of [ | |
| { kvCacheType: 'f16' }, | |
| { flashMode: 'off' }, | |
| { flashMode: 'off', kvCacheType: 'f16' }, | |
| { ...benchmarkDefaults, flashMode: 'enabled' }, | |
| { ...benchmarkDefaults, kvCacheType: 'q5_0' }, | |
| { ...benchmarkDefaults, wasmFlavor: 'future' }, | |
| ]) { | |
| expect(tuningError(input)).toMatchObject({ code: 'INVALID_BENCHMARK_TUNING' }); | |
| } | |
| }); | |
| it('rejects quantized V cache unless Flash Attention is in auto mode', () => { | |
| for (const kvCacheType of ['q8_0', 'q4_0'] as const) { | |
| expect(tuningError({ ...benchmarkDefaults, kvCacheType })).toMatchObject({ | |
| code: 'INVALID_BENCHMARK_TUNING', | |
| message: expect.stringMatching(/requires Flash Attention/i), | |
| }); | |
| } | |
| }); | |
| it('rejects invalid and overflowing signed glue batch values', () => { | |
| for (const nBatch of [0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, 2_147_483_648]) { | |
| expect(tuningError({ ...benchmarkDefaults, nBatch })).toMatchObject({ | |
| code: 'INVALID_BATCH_SIZE', | |
| }); | |
| } | |
| for (const nUbatch of [0, -1, 1.5, Number.NaN, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER]) { | |
| expect(tuningError({ ...benchmarkDefaults, nUbatch })).toMatchObject({ | |
| code: 'INVALID_UBATCH_SIZE', | |
| }); | |
| } | |
| }); | |
| }); | |
| describe('resolveRuntimeBatchShape', () => { | |
| const releaseDefaults = resolveLoadTuning(undefined, 'auto'); | |
| it('caps only the compat release path to short Safari-safe command buffers', () => { | |
| expect(resolveRuntimeBatchShape(releaseDefaults, 'compat')).toEqual({ | |
| nBatch: 32, | |
| nUbatch: 16, | |
| }); | |
| expect(resolveRuntimeBatchShape(releaseDefaults, 'jspi')).toEqual({ | |
| nBatch: undefined, | |
| nUbatch: undefined, | |
| }); | |
| }); | |
| it('preserves explicit tuning and never defaults microbatch above batch', () => { | |
| expect(resolveRuntimeBatchShape({ nBatch: 96, nUbatch: 24 }, 'compat')).toEqual({ | |
| nBatch: 96, | |
| nUbatch: 24, | |
| }); | |
| expect(resolveRuntimeBatchShape({ nBatch: 8, nUbatch: null }, 'compat')).toEqual({ | |
| nBatch: 8, | |
| nUbatch: 8, | |
| }); | |
| }); | |
| }); | |
| describe('selectWasmFlavor', () => { | |
| it('uses the browser-native flavor for auto', () => { | |
| expect(selectWasmFlavor('auto', false)).toBe('jspi'); | |
| expect(selectWasmFlavor('auto', true)).toBe('compat'); | |
| }); | |
| it('forces compat even when JSPI is available', () => { | |
| expect(selectWasmFlavor('compat', false)).toBe('compat'); | |
| expect(selectWasmFlavor('compat', true)).toBe('compat'); | |
| }); | |
| it('selects JSPI when the browser supports it', () => { | |
| expect(selectWasmFlavor('jspi', false)).toBe('jspi'); | |
| }); | |
| it('fails loud when JSPI is requested but unavailable', () => { | |
| expect(() => selectWasmFlavor('jspi', true)).toThrowError( | |
| expect.objectContaining({ | |
| code: 'BENCHMARK_WASM_FLAVOR_UNAVAILABLE', | |
| message: expect.stringMatching(/requires the compatibility runtime/i), | |
| }), | |
| ); | |
| }); | |
| }); | |