blob: c571a071b838c194b2ab84d26b83b896cf3e2708 [file]
/*
* 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();
}
}