This is an automated email from the ASF dual-hosted git repository.

guan404ming pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm.git


The following commit(s) were added to refs/heads/main by this push:
     new 7b2ef6ad5f [Web] Avoid redundant memory byte copies (#20127)
7b2ef6ad5f is described below

commit 7b2ef6ad5fbb136c59ca780f0a491c17595a9223
Author: Akaash Parthasarathy <[email protected]>
AuthorDate: Mon Aug 17 05:15:14 2026 -0700

    [Web] Avoid redundant memory byte copies (#20127)
    
    Avoid unnecessary intermediate allocations in WASM memory operations:
    - Pass a view of the cached call-stack buffer directly to
    storeRawBytes(), which synchronously copies it into WASM memory
    - Preserve existing ownership, length, and zero-padding behavior
    
    The added tests are intended to target internal buffer-sharing behavior
    and thus import from src instead of dist.
---
 web/src/memory.ts             |  8 +++---
 web/tests/node/test_memory.js | 66 +++++++++++++++++++++++++++++++++++++++++++
 web/tests/node/test_webgpu.js | 38 +++++++++++++++++++++++++
 3 files changed, 108 insertions(+), 4 deletions(-)

diff --git a/web/src/memory.ts b/web/src/memory.ts
index 3847b8ce2a..00dc17d412 100644
--- a/web/src/memory.ts
+++ b/web/src/memory.ts
@@ -134,7 +134,7 @@ export class Memory {
       this.updateViews();
     }
     const result = new Uint8Array(numBytes);
-    result.set(this.viewU8.slice(ptr, ptr + numBytes));
+    result.set(this.viewU8.subarray(ptr, ptr + numBytes));
     return result;
   }
   /**
@@ -217,7 +217,7 @@ export class Memory {
     const length = this.loadU32(sizePtr);
     const dataPtr = ffiAnyPtr + SizeOf.I32 + SizeOf.I32;
     const result = new Uint8Array(length);
-    result.set(this.viewU8.slice(dataPtr, dataPtr + length));
+    result.set(this.viewU8.subarray(dataPtr, dataPtr + length));
     return result;
   }
   /**
@@ -248,7 +248,7 @@ export class Memory {
     const ptr = this.loadPointer(byteArrayPtr);
     const length = this.loadUSize(byteArrayPtr + this.sizeofPtr());
     const result = new Uint8Array(length);
-    result.set(this.viewU8.slice(ptr, ptr + length));
+    result.set(this.viewU8.subarray(ptr, ptr + length));
     return result;
   }
   // private functions
@@ -346,7 +346,7 @@ export class CachedCallStack implements Disposable {
       ];
       this.storePtr(targetOffset, this.ptrFromOffset(valueOffset));
     }
-    this.memory.storeRawBytes(this.basePtr, this.viewU8.slice(0, nbytes));
+    this.memory.storeRawBytes(this.basePtr, this.viewU8.subarray(0, nbytes));
   }
 
   /**
diff --git a/web/tests/node/test_memory.js b/web/tests/node/test_memory.js
new file mode 100644
index 0000000000..39de27835c
--- /dev/null
+++ b/web/tests/node/test_memory.js
@@ -0,0 +1,66 @@
+/*
+ * 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.
+ */
+const { CachedCallStack, Memory } = require("../../src/memory");
+
+test("loadRawBytes returns an owned copy", () => {
+  const wasmMemory = new WebAssembly.Memory({ initial: 1 });
+  const memory = new Memory(wasmMemory);
+  const source = new Uint8Array(wasmMemory.buffer, 8, 4);
+  source.set([1, 2, 3, 4]);
+
+  const result = memory.loadRawBytes(8, 4);
+
+  expect(Array.from(result)).toEqual([1, 2, 3, 4]);
+  expect(result.buffer).not.toBe(wasmMemory.buffer);
+
+  result[0] = 10;
+  source[1] = 20;
+  expect(Array.from(result)).toEqual([10, 2, 3, 4]);
+  expect(Array.from(source)).toEqual([1, 20, 3, 4]);
+});
+
+test("loadRawBytes preserves the requested length at the end of memory", () => 
{
+  const wasmMemory = new WebAssembly.Memory({ initial: 1 });
+  const memory = new Memory(wasmMemory);
+  const source = new Uint8Array(wasmMemory.buffer);
+  source.set([5, 6], source.length - 2);
+
+  const result = memory.loadRawBytes(source.length - 2, 4);
+
+  expect(Array.from(result)).toEqual([5, 6, 0, 0]);
+});
+
+test("CachedCallStack commits a view of its cached bytes", () => {
+  const memory = {
+    wasm32: true,
+    sizeofPtr: () => 4,
+    storeRawBytes: jest.fn(),
+  };
+  const stack = new CachedCallStack(memory, () => 1024, () => {});
+  const offset = stack.allocRawBytes(4);
+  stack.storeRawBytes(offset, new Uint8Array([1, 2, 3, 4]));
+
+  stack.commitToWasmMemory(4);
+
+  expect(memory.storeRawBytes).toHaveBeenCalledTimes(1);
+  const [ptr, bytes] = memory.storeRawBytes.mock.calls[0];
+  expect(ptr).toBe(1024);
+  expect(Array.from(bytes)).toEqual([1, 2, 3, 4]);
+  expect(bytes.buffer).toBe(stack.buffer);
+});
diff --git a/web/tests/node/test_webgpu.js b/web/tests/node/test_webgpu.js
index 30c6c8c581..dcd0be2e09 100644
--- a/web/tests/node/test_webgpu.js
+++ b/web/tests/node/test_webgpu.js
@@ -110,6 +110,7 @@ function createMockDevice({
 function createContext(deviceOptions) {
   const gpu = createMockDevice(deviceOptions);
   const memory = {
+    loadRawBytes: jest.fn(),
     storeRawBytes: jest.fn(),
   };
   const context = new WebGPUContext(memory, gpu.device);
@@ -175,6 +176,43 @@ test("a host write flushes pending GPU copies before 
writeBuffer", () => {
   expect(events).toEqual(["copy", "finish", "submit", "writeBuffer"]);
 });
 
+test("an aligned CPU to GPU copy writes the requested bytes", () => {
+  const { context, device, queue, memory, destination } = createContext();
+  const copyToGPU = context.getDeviceAPI("deviceCopyToGPU");
+  const rawBytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+  memory.loadRawBytes.mockReturnValue(rawBytes);
+
+  copyToGPU(128, destination, 12, rawBytes.length);
+
+  expect(memory.loadRawBytes).toHaveBeenCalledWith(128, rawBytes.length);
+  expect(queue.writeBuffer).toHaveBeenCalledWith(
+    device.createBuffer.mock.results[1].value,
+    12,
+    rawBytes,
+    0,
+    rawBytes.length
+  );
+});
+
+test("an unaligned CPU to GPU copy pads the write to four bytes", () => {
+  const { context, device, queue, memory, destination } = createContext();
+  const copyToGPU = context.getDeviceAPI("deviceCopyToGPU");
+  const rawBytes = new Uint8Array([1, 2, 3]);
+  memory.loadRawBytes.mockReturnValue(rawBytes);
+
+  copyToGPU(256, destination, 4, rawBytes.length);
+
+  expect(memory.loadRawBytes).toHaveBeenCalledWith(256, rawBytes.length);
+  expect(queue.writeBuffer).toHaveBeenCalledTimes(1);
+  const [buffer, toOffset, data, dataOffset, nbytes] =
+    queue.writeBuffer.mock.calls[0];
+  expect(buffer).toBe(device.createBuffer.mock.results[1].value);
+  expect(toOffset).toBe(4);
+  expect(Array.from(data)).toEqual([1, 2, 3, 0]);
+  expect(dataOffset).toBe(0);
+  expect(nbytes).toBe(4);
+});
+
 test("a GPU readback flushes pending copies before its own submission", async 
() => {
   const {
     context,

Reply via email to