Skip to content

Commit 016a5a0

Browse files
committed
[Web] Preserve caller ownership of cached shape tuples
Signed-off-by: Akaash Parthasarathy <akaashrp@gmail.com>
1 parent 2393db6 commit 016a5a0

4 files changed

Lines changed: 42 additions & 4 deletions

File tree

‎web/src/cache_state.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,8 @@ export class LRUCache<K, V> {
124124
* the JS→WASM FFI boundary each time. During LLM decode, the same shapes
125125
* repeat every token (e.g. [1,32,128]), so caching avoids thousands of
126126
* redundant FFI round-trips.
127-
* - Invalidation: Never. Shape tuples are immutable value objects that
128-
* remain valid for the lifetime of the TVM instance.
127+
* - Invalidation: Cache entries may be evicted, but returned shape tuples
128+
* hold independent references and remain valid for their caller's scope.
129129
*
130130
* Future additions (follow-up PR):
131131
* - **uniformCache**: Caches GPU uniform buffers keyed by content hash.
@@ -142,7 +142,8 @@ export class CacheState {
142142
* Key: comma-separated dimension string, e.g. "1,32,128"
143143
* Value: TVM ShapeTuple object (Disposable)
144144
*
145-
* Invalidation rule: None required — shape tuples are immutable.
145+
* Eviction releases only the cache's reference. Shape tuples returned to
146+
* callers have independent references and normal scope-managed lifetimes.
146147
*/
147148
readonly shapeCache: LRUCache<string, Disposable>;
148149

‎web/src/ctypes.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ export type FTVMFFIWasmFunctionCreate = (
176176
*/
177177
export type FTVMFFIWasmFunctionDeleter = (self: Pointer) => void;
178178

179+
/**
180+
* int TVMFFIObjectIncRef(TVMFFIObjectHandle obj);
181+
*/
182+
export type FTVMFFIObjectIncRef = (obj: Pointer) => number;
183+
179184
/**
180185
* int TVMFFIObjectDecRef(TVMFFIObjectHandle obj);
181186
*/

‎web/src/runtime.ts‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1868,13 +1868,27 @@ export class Instance implements Disposable {
18681868
*/
18691869
makeShapeTuple(shape: Array<number>): TVMObject {
18701870
const key = CacheState.computeShapeKey(shape);
1871-
return this.cacheState.shapeCache.get(key, () => {
1871+
const cachedTuple = this.cacheState.shapeCache.get(key, () => {
18721872
const shapeArray = shape.map((value) => new Scalar(value, "int"));
18731873
const tuple = this.ctx.makeShapeTuple(...shapeArray);
18741874
// Detach from scope so the cached object survives across scopes.
18751875
this.detachFromCurrentScope(tuple);
18761876
return tuple;
18771877
}) as TVMObject;
1878+
1879+
// The cache owns its wrapper and may release it on eviction. Give the
1880+
// caller an independent strong reference with the usual scope lifetime.
1881+
const handle = cachedTuple.getHandle();
1882+
this.lib.checkCall(
1883+
(this.lib.exports.TVMFFIObjectIncRef as ctypes.FTVMFFIObjectIncRef)(handle)
1884+
);
1885+
const callerTuple = new TVMObject(handle, this.lib, this.ctx);
1886+
try {
1887+
return this.attachToCurrentScope(callerTuple);
1888+
} catch (err) {
1889+
callerTuple.dispose();
1890+
throw err;
1891+
}
18781892
}
18791893
/**
18801894
* Get type index from type key.

‎web/tests/node/test_object.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,21 @@ test("object", () => {
4343
assert(t1.getHandle() == t.getHandle());
4444
});
4545
});
46+
47+
test("shape cache does not invalidate caller-owned tuples", () => {
48+
tvm.beginScope();
49+
const disposedTuple = tvm.makeShapeTuple([987654321, -1]);
50+
disposedTuple.dispose();
51+
const cachedTuple = tvm.makeShapeTuple([987654321, -1]);
52+
assert.doesNotThrow(() => cachedTuple.typeKey());
53+
54+
const evictedTuple = tvm.makeShapeTuple([987654321, 0]);
55+
for (let i = 1; i <= 256; ++i) {
56+
tvm.makeShapeTuple([987654321, i]);
57+
}
58+
assert.doesNotThrow(() => evictedTuple.typeKey());
59+
60+
tvm.endScope();
61+
assert.throws(() => cachedTuple.getHandle(), /already been disposed/);
62+
assert.throws(() => evictedTuple.getHandle(), /already been disposed/);
63+
});

0 commit comments

Comments
 (0)