| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| /** |
| * Caching utilities for the TVM web runtime. |
| * |
| * Provides a generic LRUCache and a CacheState container that manages |
| * domain-specific caches used by the WebGPU runtime. |
| */ |
| import { Disposable } from "./types"; |
| |
| /** |
| * A generic LRU (Least Recently Used) cache with bounded size. |
| * |
| * Entries are evicted in insertion order when the cache exceeds `maxSize`. |
| * Uses a Map to maintain insertion order for O(1) LRU eviction. |
| * |
| * @typeParam K - Cache key type. |
| * @typeParam V - Cache value type. |
| */ |
| export class LRUCache<K, V> { |
| private cache: Map<K, V> = new Map(); |
| private readonly maxSize: number; |
| /** Optional callback invoked when an entry is evicted. */ |
| private readonly onEvict?: (key: K, value: V) => void; |
| |
| constructor(maxSize: number, onEvict?: (key: K, value: V) => void) { |
| this.maxSize = maxSize; |
| this.onEvict = onEvict; |
| } |
| |
| /** |
| * Get a value from the cache, constructing it via `constructor` on miss. |
| * |
| * On hit: moves the entry to most-recently-used position and returns it. |
| * On miss: calls `constructor()` to create the value, inserts it, and |
| * returns it. If the cache is full, the least-recently-used entry is |
| * evicted first. |
| * |
| * @param key The cache key. |
| * @param constructor Factory function called on cache miss to produce the value. |
| * @returns The cached or newly constructed value. |
| */ |
| get(key: K, constructor: () => V): V { |
| const existing = this.cache.get(key); |
| if (existing !== undefined) { |
| // Move to most-recently-used position |
| this.cache.delete(key); |
| this.cache.set(key, existing); |
| return existing; |
| } |
| // Evict LRU entry if at capacity |
| if (this.cache.size >= this.maxSize) { |
| const oldest = this.cache.keys().next().value; |
| if (oldest !== undefined) { |
| if (this.onEvict) { |
| this.onEvict(oldest, this.cache.get(oldest)!); |
| } |
| this.cache.delete(oldest); |
| } |
| } |
| const value = constructor(); |
| this.cache.set(key, value); |
| return value; |
| } |
| |
| /** |
| * Check whether eviction would be needed for a new entry. |
| * |
| * Useful when the caller needs to perform side effects before eviction |
| * (e.g. flushing pending GPU commands before destroying an evicted buffer). |
| * |
| * @param key The key to check. |
| * @returns true if inserting `key` would trigger eviction of another entry. |
| */ |
| needEviction(key: K): boolean { |
| if (this.cache.has(key)) return false; |
| return this.cache.size >= this.maxSize; |
| } |
| |
| /** |
| * Clear all cached entries. |
| * |
| * Does not dispose values — the caller is responsible for cleanup |
| * (e.g. destroying GPU buffers) before calling invalidate. |
| */ |
| invalidate(): void { |
| this.cache.clear(); |
| } |
| |
| /** Number of entries currently in the cache. */ |
| get size(): number { |
| return this.cache.size; |
| } |
| |
| /** Iterate over all cached values (for disposal). */ |
| values(): IterableIterator<V> { |
| return this.cache.values(); |
| } |
| } |
| |
| /** |
| * CacheState manages domain-specific caches for the WebGPU runtime. |
| * |
| * Currently contains: |
| * - **shapeCache**: Caches TVM ShapeTuple objects keyed by dimension string. |
| * - Why: `makeShapeTuple()` is called on every tensor operation, crossing |
| * the JS→WASM FFI boundary each time. During LLM decode, the same shapes |
| * repeat every token (e.g. [1,32,128]), so caching avoids thousands of |
| * redundant FFI round-trips. |
| * - Invalidation: Never. Shape tuples are immutable value objects that |
| * remain valid for the lifetime of the TVM instance. |
| * |
| * Future additions (follow-up PR): |
| * - **uniformCache**: Caches GPU uniform buffers keyed by content hash. |
| * - Why: Many dispatches use identical scalar arguments (matrix dims, etc.). |
| * Reusing the buffer avoids `createBuffer` + `writeBuffer` overhead. |
| * - Invalidation: Must invalidate on any GPU buffer deallocation, since |
| * buffer pointers can be reused by the allocator, making cached entries |
| * that reference the old buffer stale. |
| */ |
| export class CacheState { |
| /** |
| * Cache for TVM ShapeTuple objects. |
| * |
| * Key: comma-separated dimension string, e.g. "1,32,128" |
| * Value: TVM ShapeTuple object (Disposable) |
| * |
| * Invalidation rule: None required — shape tuples are immutable. |
| */ |
| readonly shapeCache: LRUCache<string, Disposable>; |
| |
| constructor(shapeCacheSize: number = 256) { |
| this.shapeCache = new LRUCache<string, Disposable>( |
| shapeCacheSize, |
| (_key, value) => value.dispose() |
| ); |
| } |
| |
| /** |
| * Compute the cache key for a shape tuple. |
| * |
| * @param shape Array of dimension values. |
| * @returns String key suitable for shapeCache lookup. |
| */ |
| static computeShapeKey(shape: Array<number>): string { |
| return shape.toString(); |
| } |
| |
| /** |
| * Dispose all cached objects and clear all caches. |
| */ |
| dispose(): void { |
| for (const obj of this.shapeCache.values()) { |
| obj.dispose(); |
| } |
| this.shapeCache.invalidate(); |
| } |
| } |