WebNN: Implement MLBuffer transfer ops
Adds support to upload or read back data to/from MLBuffer.
Since MLContext determines the device-execution order of
GPU operations, writeBuffer and readBuffer were added to
MLContext.
* Only full MLBuffer read/write from renderer are enabled.
https://github.com/webmachinelearning/webnn/issues/543
Bug: 40278771
Change-Id: Id95da35e3f81bed47a356f76b75c043cdd500beb
Cq-Include-Trybots: luci.chromium.try:win11-blink-rel
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5357633
Commit-Queue: Bryan Bernhart <bryan.bernhart@intel.com>
Reviewed-by: Reilly Grant <reillyg@chromium.org>
Reviewed-by: Rafael Cintron <rafael.cintron@microsoft.com>
Reviewed-by: ningxin hu <ningxin.hu@intel.com>
Cr-Commit-Position: refs/heads/main@{#1280431}
diff --git a/webnn/conformance_tests/buffer.https.any.js b/webnn/conformance_tests/buffer.https.any.js
index 5b0f46d..9391be8 100644
--- a/webnn/conformance_tests/buffer.https.any.js
+++ b/webnn/conformance_tests/buffer.https.any.js
@@ -9,4 +9,8 @@
testCreateWebNNBuffer("create", 4);
-testDestroyWebNNBuffer("destroyTwice");
\ No newline at end of file
+testDestroyWebNNBuffer('destroyTwice');
+
+testReadWebNNBuffer('read');
+
+testWriteWebNNBuffer('write');
diff --git a/webnn/conformance_tests/gpu/buffer.https.any.js b/webnn/conformance_tests/gpu/buffer.https.any.js
index 66bba9e..225bc40 100644
--- a/webnn/conformance_tests/gpu/buffer.https.any.js
+++ b/webnn/conformance_tests/gpu/buffer.https.any.js
@@ -9,4 +9,8 @@
testCreateWebNNBuffer("create", 4, 'gpu');
-testDestroyWebNNBuffer("destroyTwice", 'gpu');
\ No newline at end of file
+testDestroyWebNNBuffer('destroyTwice', 'gpu');
+
+testReadWebNNBuffer('read', 'gpu');
+
+testWriteWebNNBuffer('write', 'gpu');
diff --git a/webnn/resources/utils.js b/webnn/resources/utils.js
index a153e39..d1dc067 100644
--- a/webnn/resources/utils.js
+++ b/webnn/resources/utils.js
@@ -953,6 +953,7 @@
* WebNN buffer creation.
* @param {MLContext} context - the context used to create the buffer.
* @param {Number} bufferSize - Size of the buffer to create, in bytes.
+ * @returns {MLBuffer} the created buffer.
*/
const createBuffer = (context, bufferSize) => {
let buffer;
@@ -1002,4 +1003,286 @@
promise_test(async () => {
createBuffer(context, bufferSize);
}, `${testName} / ${bufferSize}`);
-};
\ No newline at end of file
+};
+
+/**
+ * Asserts the buffer data in MLBuffer matches expected.
+ * @param {MLContext} ml_context - The context used to create the buffer.
+ * @param {MLBuffer} ml_buffer - The buffer to read and compare data.
+ * @param {Array} expected - Array of the expected data in the buffer.
+ */
+const assert_buffer_data_equals = async (ml_context, ml_buffer, expected) => {
+ const actual = await ml_context.readBuffer(ml_buffer);
+ assert_array_equals(
+ new expected.constructor(actual), expected,
+ 'Read buffer data equals expected data.');
+};
+
+/**
+ * WebNN write buffer operation test.
+ * @param {String} testName - The name of the test operation.
+ * @param {String} deviceType - The execution device type for this test.
+ */
+const testWriteWebNNBuffer = (testName, deviceType = 'cpu') => {
+ let ml_context;
+ promise_setup(async () => {
+ ml_context = await navigator.ml.createContext({deviceType});
+ });
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ let array_buffer = new ArrayBuffer(ml_buffer.size);
+
+ // Writing with a size that goes past that source buffer length.
+ assert_throws_js(
+ TypeError,
+ () => ml_context.writeBuffer(
+ ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ 0,
+ /*srcSize=*/ ml_buffer.size + 1));
+ assert_throws_js(
+ TypeError,
+ () => ml_context.writeBuffer(
+ ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ 3,
+ /*srcSize=*/ 4));
+
+ // Writing with a source offset that is out of range of the source size.
+ assert_throws_js(
+ TypeError,
+ () => ml_context.writeBuffer(
+ ml_buffer, new Uint8Array(array_buffer),
+ /*srcOffset=*/ ml_buffer.size + 1));
+
+ // Writing with a source offset that is out of range of implicit copy size.
+ assert_throws_js(
+ TypeError,
+ () => ml_context.writeBuffer(
+ ml_buffer, new Uint8Array(array_buffer),
+ /*srcOffset=*/ ml_buffer.size + 1, /*srcSize=*/ undefined));
+
+ assert_throws_js(
+ TypeError,
+ () => ml_context.writeBuffer(
+ ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/ undefined,
+ /*srcSize=*/ ml_buffer.size + 1));
+
+ assert_throws_js(
+ TypeError,
+ () => ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xEE, 0xEE, 0xEE, 0xEE, 0xEE])));
+ }, `${testName} / error`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Writing data to a destroyed MLBuffer should throw.
+ ml_buffer.destroy();
+
+ assert_throws_dom(
+ 'InvalidStateError',
+ () =>
+ ml_context.writeBuffer(ml_buffer, new Uint8Array(ml_buffer.size)));
+ }, `${testName} / destroy`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ const array_buffer = new ArrayBuffer(ml_buffer.size);
+ const detached_buffer = array_buffer.transfer();
+ assert_true(array_buffer.detached, 'array buffer should be detached.');
+
+ ml_context.writeBuffer(ml_buffer, array_buffer);
+ }, `${testName} / detached`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ let another_ml_context = await navigator.ml.createContext({deviceType});
+ let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size);
+
+ let input_data = new Uint8Array(ml_buffer.size).fill(0xAA);
+ assert_throws_js(
+ TypeError, () => ml_context.writeBuffer(another_ml_buffer, input_data));
+ assert_throws_js(
+ TypeError, () => another_ml_context.writeBuffer(ml_buffer, input_data));
+ }, `${testName} / context_mismatch`);
+};
+
+/**
+ * WebNN read buffer operation test.
+ * @param {String} testName - The name of the test operation.
+ * @param {String} deviceType - The execution device type for this test.
+ */
+const testReadWebNNBuffer = (testName, deviceType = 'cpu') => {
+ let ml_context;
+ promise_setup(async () => {
+ ml_context = await navigator.ml.createContext({deviceType});
+ });
+
+ promise_test(async t => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Reading a destroyed MLBuffer should reject.
+ ml_buffer.destroy();
+
+ await promise_rejects_dom(
+ t, 'InvalidStateError', ml_context.readBuffer(ml_buffer));
+ }, `${testName} / destroy`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Initialize the buffer.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA]));
+
+ ml_context.writeBuffer(ml_buffer, Uint32Array.from([0xBBBBBBBB]));
+ await assert_buffer_data_equals(
+ ml_context, ml_buffer, Uint32Array.from([0xBBBBBBBB]));
+ ;
+ }, `${testName} / full_size`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Initialize the buffer.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA]));
+
+ // Writing to the remainder of the buffer from source offset.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xCC, 0xCC, 0xBB, 0xBB]),
+ /*srcOffset=*/ 2);
+ await assert_buffer_data_equals(
+ ml_context, ml_buffer, Uint8Array.from([0xBB, 0xBB, 0xAA, 0xAA]));
+ }, `${testName} / src_offset_only`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Initialize the buffer.
+ const input_data = [0xAA, 0xAA, 0xAA, 0xAA];
+ ml_context.writeBuffer(ml_buffer, Uint8Array.from(input_data));
+
+ // Writing zero bytes at the end of the buffer.
+ ml_context.writeBuffer(
+ ml_buffer, Uint32Array.from([0xBBBBBBBB]), /*srcOffset=*/ 1);
+ await assert_buffer_data_equals(
+ ml_context, ml_buffer, Uint8Array.from(input_data));
+ }, `${testName} / zero_write`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Initialize the buffer.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA]));
+
+ // Writing with both a source offset and size.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xDD, 0xDD, 0xCC, 0xDD]),
+ /*srcOffset=*/ 2, /*srcSize=*/ 1);
+ await assert_buffer_data_equals(
+ ml_context, ml_buffer, Uint8Array.from([0xCC, 0xAA, 0xAA, 0xAA]));
+ }, `${testName} / src_offset_and_size`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ // Initialize the buffer.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xAA, 0xAA, 0xAA, 0xAA]));
+
+ // Using an offset allows a larger source buffer to fit.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from([0xEE, 0xEE, 0xEE, 0xEE, 0xEE]),
+ /*srcOffset=*/ 1);
+ await assert_buffer_data_equals(
+ ml_context, ml_buffer, Uint8Array.from([0xEE, 0xEE, 0xEE, 0xEE]));
+ }, `${testName} / larger_src_data`);
+
+ promise_test(async () => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ const input_data = [0xAA, 0xAA, 0xAA, 0xAA];
+
+ // Writing with a source offset of undefined should be treated as 0.
+ ml_context.writeBuffer(
+ ml_buffer, Uint8Array.from(input_data), /*srcOffset=*/ undefined,
+ /*srcSize=*/ input_data.length);
+ await assert_buffer_data_equals(
+ ml_context, ml_buffer, Uint8Array.from(input_data));
+ }, `${testName} / no_src_offset`);
+
+ promise_test(async t => {
+ let ml_buffer = createBuffer(ml_context, 4);
+
+ // MLBuffer was unsupported for the deviceType.
+ if (ml_buffer === undefined) {
+ return;
+ }
+
+ let another_ml_context = await navigator.ml.createContext({deviceType});
+ let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size);
+
+ await promise_rejects_js(
+ t, TypeError, ml_context.readBuffer(another_ml_buffer));
+ await promise_rejects_js(
+ t, TypeError, another_ml_context.readBuffer(ml_buffer));
+ }, `${testName} / context_mismatch`);
+};