Làm quen với GPU Compute trên web

Bài đăng này khám phá API WebGPU thử nghiệm thông qua các ví dụ và giúp bạn bắt đầu thực hiện các phép tính song song với dữ liệu bằng GPU.

François Beaufort
François Beaufort

Thông tin khái quát

Như bạn có thể biết, Đơn vị xử lý đồ hoạ (GPU) là một hệ thống con điện tử trong máy tính, ban đầu chuyên xử lý đồ hoạ. Tuy nhiên, trong 10 năm qua, API này đã phát triển theo hướng kiến trúc linh hoạt hơn, cho phép nhà phát triển triển khai nhiều loại thuật toán, không chỉ kết xuất đồ hoạ 3D, đồng thời tận dụng kiến trúc độc đáo của GPU. Các chức năng này được gọi là GPU Compute (Tính toán GPU) và việc sử dụng GPU làm bộ xử lý đồng thời cho mục đích tính toán khoa học chung được gọi là lập trình GPU (GPGPU) chung.

Điện toán GPU đã đóng góp đáng kể vào sự bùng nổ của công nghệ học máy gần đây, vì mạng nơron tích chập và các mô hình khác có thể tận dụng cấu trúc này để chạy hiệu quả hơn trên GPU. Nền tảng web hiện tại thiếu khả năng điện toán GPU, nên Nhóm cộng đồng "GPU cho web" của W3C đang thiết kế một API để hiển thị các API GPU hiện đại có trên hầu hết các thiết bị hiện tại. API này được gọi là WebGPU.

WebGPU là một API cấp thấp, giống như WebGL. Hàm này rất mạnh mẽ và khá dài dòng, như bạn sẽ thấy. Nhưng không sao. Chúng ta đang tìm kiếm hiệu suất.

Trong bài viết này, tôi sẽ tập trung vào phần GPU Compute của WebGPU và thành thật mà nói, tôi chỉ mới tìm hiểu sơ bộ để bạn có thể bắt đầu tự chơi. Tôi sẽ tìm hiểu sâu hơn và đề cập đến tính năng kết xuất WebGPU (canvas, hoạ tiết, v.v.) trong các bài viết sắp tới.

Truy cập vào GPU

Bạn có thể dễ dàng truy cập vào GPU trong WebGPU. Việc gọi navigator.gpu.requestAdapter() sẽ trả về một lời hứa JavaScript sẽ phân giải không đồng bộ bằng một trình chuyển đổi GPU. Hãy xem bộ chuyển đổi này như là một card đồ hoạ. GPU có thể được tích hợp (trên cùng một khối với CPU) hoặc riêng biệt (thường là thẻ PCIe có hiệu suất cao hơn nhưng tiêu thụ nhiều điện năng hơn).

Sau khi bạn có bộ chuyển đổi GPU, hãy gọi adapter.requestDevice() để nhận một lời hứa sẽ phân giải bằng một thiết bị GPU mà bạn sẽ sử dụng để thực hiện một số phép tính GPU.

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();

Cả hai hàm này đều có các tuỳ chọn cho phép bạn chỉ định cụ thể loại sạc (lựa chọn ưu tiên về nguồn) và thiết bị (phần mở rộng, giới hạn) mà bạn muốn. Để đơn giản, chúng ta sẽ sử dụng các tuỳ chọn mặc định trong bài viết này.

Ghi bộ nhớ đệm

Hãy xem cách sử dụng JavaScript để ghi dữ liệu vào bộ nhớ cho GPU. Quy trình này không đơn giản do mô hình hộp cát được sử dụng trong các trình duyệt web hiện đại.

Ví dụ bên dưới cho bạn biết cách ghi 4 byte vào bộ nhớ đệm có thể truy cập từ GPU. Hàm này gọi device.createBuffer() để lấy kích thước của vùng đệm và mức sử dụng vùng đệm đó. Mặc dù không bắt buộc phải sử dụng cờ sử dụng GPUBufferUsage.MAP_WRITE cho lệnh gọi cụ thể này, nhưng hãy làm rõ rằng chúng ta muốn ghi vào vùng đệm này. Điều này dẫn đến việc đối tượng vùng đệm GPU được liên kết khi tạo nhờ mappedAtCreation được đặt thành true. Sau đó, bạn có thể truy xuất vùng đệm dữ liệu nhị phân thô được liên kết bằng cách gọi phương thức vùng đệm GPU getMappedRange().

Việc ghi byte sẽ trở nên quen thuộc nếu bạn đã chơi với ArrayBuffer; hãy sử dụng TypedArray và sao chép các giá trị vào đó.

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE
});
const arrayBuffer = gpuBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

Tại thời điểm này, vùng đệm GPU được liên kết, nghĩa là vùng đệm này thuộc sở hữu của CPU và có thể truy cập được trong chế độ đọc/ghi từ JavaScript. Để GPU có thể truy cập vào GPU, bạn phải huỷ liên kết GPU, việc này đơn giản như việc gọi gpuBuffer.unmap().

Bạn cần có khái niệm ánh xạ/không ánh xạ để ngăn chặn các điều kiện tương tranh trong đó GPU và CPU truy cập vào bộ nhớ cùng một lúc.

Đọc bộ nhớ đệm

Bây giờ, hãy xem cách sao chép vùng đệm GPU vào vùng đệm GPU khác và đọc lại vùng đệm đó.

Vì chúng ta đang ghi vào vùng đệm GPU đầu tiên và muốn sao chép vùng đệm đó vào vùng đệm GPU thứ hai, nên cần có một cờ sử dụng mới GPUBufferUsage.COPY_SRC. Vùng đệm GPU thứ hai được tạo ở trạng thái chưa liên kết lần này bằng device.createBuffer(). Cờ sử dụng của nó là GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ vì cờ này sẽ được dùng làm đích đến của vùng đệm GPU đầu tiên và được đọc trong JavaScript sau khi các lệnh sao chép GPU được thực thi.

// Get a GPU buffer in a mapped state and an arrayBuffer for writing.
const gpuWriteBuffer = device.createBuffer({
  mappedAtCreation: true,
  size: 4,
  usage: GPUBufferUsage.MAP_WRITE | GPUBufferUsage.COPY_SRC
});
const arrayBuffer = gpuWriteBuffer.getMappedRange();

// Write bytes to buffer.
new Uint8Array(arrayBuffer).set([0, 1, 2, 3]);

// Unmap buffer so that it can be used later for copy.
gpuWriteBuffer.unmap();

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

Vì GPU là một bộ xử lý đồng thời độc lập, nên tất cả lệnh GPU đều được thực thi không đồng bộ. Đó là lý do tại sao có một danh sách các lệnh GPU được tạo và gửi theo lô khi cần. Trong WebGPU, bộ mã hoá lệnh GPU do device.createCommandEncoder() trả về là đối tượng JavaScript tạo một lô lệnh "đã lưu vào bộ đệm" sẽ được gửi đến GPU tại một thời điểm nào đó. Mặt khác, các phương thức trên GPUBuffer là "không được lưu vào vùng đệm", nghĩa là các phương thức này thực thi một cách nguyên tử tại thời điểm được gọi.

Sau khi bạn có bộ mã hoá lệnh GPU, hãy gọi copyEncoder.copyBufferToBuffer() như minh hoạ bên dưới để thêm lệnh này vào hàng đợi lệnh để thực thi sau. Cuối cùng, hãy hoàn tất việc mã hoá các lệnh bằng cách gọi copyEncoder.finish() và gửi các lệnh đó vào hàng đợi lệnh của thiết bị GPU. Hàng đợi chịu trách nhiệm xử lý các lượt gửi được thực hiện thông qua device.queue.submit() với các lệnh GPU làm đối số. Thao tác này sẽ thực thi tuần tự tất cả các lệnh được lưu trữ trong mảng.

// Encode commands for copying buffer to buffer.
const copyEncoder = device.createCommandEncoder();
copyEncoder.copyBufferToBuffer(
  gpuWriteBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  4 /* size */
);

// Submit copy commands.
const copyCommands = copyEncoder.finish();
device.queue.submit([copyCommands]);

Tại thời điểm này, các lệnh hàng đợi GPU đã được gửi nhưng không nhất thiết phải được thực thi. Để đọc vùng đệm GPU thứ hai, hãy gọi gpuReadBuffer.mapAsync() bằng GPUMapMode.READ. Phương thức này trả về một lời hứa sẽ giải quyết khi vùng đệm GPU được ánh xạ. Sau đó, lấy dải ô được ánh xạ bằng gpuReadBuffer.getMappedRange() chứa các giá trị giống như vùng đệm GPU đầu tiên sau khi tất cả các lệnh GPU trong hàng đợi đã được thực thi.

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const copyArrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Uint8Array(copyArrayBuffer));

Bạn có thể dùng thử mẫu này.

Tóm lại, sau đây là những điều bạn cần nhớ về các thao tác bộ nhớ đệm:

  • Bạn phải huỷ liên kết vùng đệm GPU để sử dụng trong quá trình gửi hàng đợi thiết bị.
  • Khi được liên kết, bạn có thể đọc và ghi vùng đệm GPU bằng JavaScript.
  • Các vùng đệm GPU được liên kết khi mapAsync()createBuffer() với mappedAtCreation được đặt thành true được gọi.

Lập trình chương trình đổ bóng

Các chương trình chạy trên GPU chỉ thực hiện các phép tính toán (và không vẽ tam giác) được gọi là chương trình đổ bóng điện toán. Các lệnh này được thực thi song song bởi hàng trăm lõi GPU (nhỏ hơn lõi CPU) hoạt động cùng nhau để xử lý dữ liệu. Đầu vào và đầu ra của chúng là vùng đệm trong WebGPU.

Để minh hoạ cách sử dụng chương trình đổ bóng điện toán trong WebGPU, chúng ta sẽ thử nghiệm với phép nhân ma trận, một thuật toán phổ biến trong học máy được minh hoạ bên dưới.

Sơ đồ nhân ma trận
Sơ đồ nhân ma trận

Tóm lại, chúng ta sẽ làm những việc sau:

  1. Tạo ba vùng đệm GPU (hai vùng đệm cho các ma trận nhân và một vùng đệm cho ma trận kết quả)
  2. Mô tả dữ liệu đầu vào và đầu ra cho chương trình đổ bóng điện toán
  3. Biên dịch mã chương trình đổ bóng điện toán
  4. Thiết lập quy trình điện toán
  5. Gửi hàng loạt các lệnh đã mã hoá đến GPU
  6. Đọc bộ đệm GPU ma trận kết quả

Tạo vùng đệm GPU

Để đơn giản, ma trận sẽ được biểu diễn dưới dạng danh sách các số thập phân động. Phần tử đầu tiên là số hàng, phần tử thứ hai là số cột và phần còn lại là các số thực tế của ma trận.

Biểu diễn đơn giản một ma trận trong JavaScript và giá trị tương đương của ma trận đó theo ký hiệu toán học
Biểu diễn đơn giản một ma trận trong JavaScript và giá trị tương đương của ma trận đó theo ký hiệu toán học

Ba vùng đệm GPU là vùng đệm lưu trữ vì chúng ta cần lưu trữ và truy xuất dữ liệu trong chương trình đổ bóng điện toán. Điều này giải thích lý do các cờ sử dụng vùng đệm GPU bao gồm cả GPUBufferUsage.STORAGE cho tất cả các cờ đó. Cờ sử dụng ma trận kết quả cũng có GPUBufferUsage.COPY_SRC vì cờ này sẽ được sao chép vào một vùng đệm khác để đọc sau khi tất cả lệnh hàng đợi GPU đều được thực thi.

const adapter = await navigator.gpu.requestAdapter();
if (!adapter) { return; }
const device = await adapter.requestDevice();


// First Matrix

const firstMatrix = new Float32Array([
  2 /* rows */, 4 /* columns */,
  1, 2, 3, 4,
  5, 6, 7, 8
]);

const gpuBufferFirstMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: firstMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferFirstMatrix = gpuBufferFirstMatrix.getMappedRange();
new Float32Array(arrayBufferFirstMatrix).set(firstMatrix);
gpuBufferFirstMatrix.unmap();


// Second Matrix

const secondMatrix = new Float32Array([
  4 /* rows */, 2 /* columns */,
  1, 2,
  3, 4,
  5, 6,
  7, 8
]);

const gpuBufferSecondMatrix = device.createBuffer({
  mappedAtCreation: true,
  size: secondMatrix.byteLength,
  usage: GPUBufferUsage.STORAGE,
});
const arrayBufferSecondMatrix = gpuBufferSecondMatrix.getMappedRange();
new Float32Array(arrayBufferSecondMatrix).set(secondMatrix);
gpuBufferSecondMatrix.unmap();


// Result Matrix

const resultMatrixBufferSize = Float32Array.BYTES_PER_ELEMENT * (2 + firstMatrix[0] * secondMatrix[1]);
const resultMatrixBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});

Liên kết bố cục nhóm và liên kết nhóm

Các khái niệm về bố cục liên kết nhóm và liên kết nhóm là dành riêng cho WebGPU. Bố cục nhóm liên kết xác định giao diện đầu vào/đầu ra mà chương trình đổ bóng dự kiến, trong khi nhóm liên kết đại diện cho dữ liệu đầu vào/đầu ra thực tế cho chương trình đổ bóng.

Trong ví dụ bên dưới, bố cục nhóm liên kết dự kiến sẽ có hai vùng đệm lưu trữ chỉ có thể đọc tại các liên kết mục được đánh số 0, 1 và một vùng đệm lưu trữ tại 2 cho chương trình đổ bóng điện toán. Mặt khác, nhóm liên kết được xác định cho bố cục nhóm liên kết này sẽ liên kết các vùng đệm GPU với các mục nhập: gpuBufferFirstMatrix với liên kết 0, gpuBufferSecondMatrix với liên kết 1resultMatrixBuffer với liên kết 2.

const bindGroupLayout = device.createBindGroupLayout({
  entries: [
    {
      binding: 0,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 1,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "read-only-storage"
      }
    },
    {
      binding: 2,
      visibility: GPUShaderStage.COMPUTE,
      buffer: {
        type: "storage"
      }
    }
  ]
});

const bindGroup = device.createBindGroup({
  layout: bindGroupLayout,
  entries: [
    {
      binding: 0,
      resource: {
        buffer: gpuBufferFirstMatrix
      }
    },
    {
      binding: 1,
      resource: {
        buffer: gpuBufferSecondMatrix
      }
    },
    {
      binding: 2,
      resource: {
        buffer: resultMatrixBuffer
      }
    }
  ]
});

Mã chương trình đổ bóng điện toán

Mã chương trình đổ bóng điện toán để nhân ma trận được viết bằng WGSL, ngôn ngữ chương trình đổ bóng WebGPU, có thể dễ dàng dịch sang SPIR-V. Không cần đi sâu vào chi tiết, bạn sẽ thấy bên dưới là ba vùng đệm bộ nhớ được xác định bằng var<storage>. Chương trình sẽ sử dụng firstMatrixsecondMatrix làm dữ liệu đầu vào và resultMatrix làm dữ liệu đầu ra.

Xin lưu ý rằng mỗi vùng đệm bộ nhớ có một phần trang trí binding được sử dụng tương ứng với cùng một chỉ mục được xác định trong bố cục nhóm liên kết và nhóm liên kết được khai báo ở trên.

const shaderModule = device.createShaderModule({
  code: `
    struct Matrix {
      size : vec2f,
      numbers: array<f32>,
    }

    @group(0) @binding(0) var<storage, read> firstMatrix : Matrix;
    @group(0) @binding(1) var<storage, read> secondMatrix : Matrix;
    @group(0) @binding(2) var<storage, read_write> resultMatrix : Matrix;

    @compute @workgroup_size(8, 8)
    fn main(@builtin(global_invocation_id) global_id : vec3u) {
      // Guard against out-of-bounds work group sizes
      if (global_id.x >= u32(firstMatrix.size.x) || global_id.y >= u32(secondMatrix.size.y)) {
        return;
      }

      resultMatrix.size = vec2(firstMatrix.size.x, secondMatrix.size.y);

      let resultCell = vec2(global_id.x, global_id.y);
      var result = 0.0;
      for (var i = 0u; i < u32(firstMatrix.size.y); i = i + 1u) {
        let a = i + resultCell.x * u32(firstMatrix.size.y);
        let b = resultCell.y + i * u32(secondMatrix.size.y);
        result = result + firstMatrix.numbers[a] * secondMatrix.numbers[b];
      }

      let index = resultCell.y + resultCell.x * u32(secondMatrix.size.y);
      resultMatrix.numbers[index] = result;
    }
  `
});

Thiết lập quy trình

Quy trình điện toán là đối tượng thực sự mô tả toán tử điện toán mà chúng ta sắp thực hiện. Tạo lớp này bằng cách gọi device.createComputePipeline(). Hàm này có hai đối số: bố cục nhóm liên kết mà chúng ta đã tạo trước đó và một giai đoạn tính toán xác định điểm truy cập của chương trình đổ bóng điện toán (hàm WGSL main) và mô-đun chương trình đổ bóng điện toán thực tế được tạo bằng device.createShaderModule().

const computePipeline = device.createComputePipeline({
  layout: device.createPipelineLayout({
    bindGroupLayouts: [bindGroupLayout]
  }),
  compute: {
    module: shaderModule,
    entryPoint: "main"
  }
});

Gửi lệnh

Sau khi tạo bản sao một nhóm liên kết bằng ba vùng đệm GPU và một quy trình điện toán có bố cục nhóm liên kết, đã đến lúc sử dụng các nhóm liên kết đó.

Hãy bắt đầu một bộ mã hoá thẻ thông hành điện toán có thể lập trình bằng commandEncoder.beginComputePass(). Chúng ta sẽ sử dụng hàm này để mã hoá các lệnh GPU sẽ thực hiện phép nhân ma trận. Đặt quy trình của nó bằng passEncoder.setPipeline(computePipeline) và nhóm liên kết ở chỉ mục 0 bằng passEncoder.setBindGroup(0, bindGroup). Chỉ mục 0 tương ứng với trang trí group(0) trong mã WGSL.

Bây giờ, hãy cùng tìm hiểu cách chương trình đổ bóng điện toán này sẽ chạy trên GPU. Mục tiêu của chúng ta là thực thi song song chương trình này cho từng ô của ma trận kết quả, từng bước một. Ví dụ: đối với ma trận kết quả có kích thước 16x32, để mã hoá lệnh thực thi, trên @workgroup_size(8, 8), chúng ta sẽ gọi passEncoder.dispatchWorkgroups(2, 4) hoặc passEncoder.dispatchWorkgroups(16 / 8, 32 / 8). Đối số đầu tiên "x" là phương diện đầu tiên, đối số thứ hai "y" là phương diện thứ hai và đối số mới nhất "z" là phương diện thứ ba mặc định là 1 vì chúng ta không cần phương diện này ở đây. Trong thế giới điện toán GPU, việc mã hoá một lệnh để thực thi hàm hạt nhân trên một tập dữ liệu được gọi là điều phối.

Thực thi song song cho mỗi ô ma trận kết quả
Thực thi song song cho mỗi ô ma trận kết quả

Kích thước của lưới nhóm công việc cho chương trình đổ bóng điện toán là (8, 8) trong mã WGSL. Do đó, "x" và "y" tương ứng là số hàng của ma trận đầu tiên và số cột của ma trận thứ hai sẽ được chia cho 8. Nhờ đó, giờ đây, chúng ta có thể điều phối lệnh gọi điện toán bằng passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8). Số lượng lưới nhóm công việc cần chạy là đối số dispatchWorkgroups().

Như trong bản vẽ ở trên, mỗi chương trình đổ bóng sẽ có quyền truy cập vào một đối tượng builtin(global_invocation_id) duy nhất. Đối tượng này sẽ được dùng để biết ô ma trận kết quả nào cần tính toán.

const commandEncoder = device.createCommandEncoder();

const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(computePipeline);
passEncoder.setBindGroup(0, bindGroup);
const workgroupCountX = Math.ceil(firstMatrix[0] / 8);
const workgroupCountY = Math.ceil(secondMatrix[1] / 8);
passEncoder.dispatchWorkgroups(workgroupCountX, workgroupCountY);
passEncoder.end();

Để kết thúc bộ mã hoá thẻ thông hành điện toán, hãy gọi passEncoder.end(). Sau đó, hãy tạo một bộ nhớ đệm GPU để dùng làm đích đến sao chép bộ nhớ đệm ma trận kết quả bằng copyBufferToBuffer. Cuối cùng, hãy hoàn tất việc mã hoá các lệnh bằng copyEncoder.finish() và gửi các lệnh đó vào hàng đợi thiết bị GPU bằng cách gọi device.queue.submit() bằng các lệnh GPU.

// Get a GPU buffer for reading in an unmapped state.
const gpuReadBuffer = device.createBuffer({
  size: resultMatrixBufferSize,
  usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
});

// Encode commands for copying buffer to buffer.
commandEncoder.copyBufferToBuffer(
  resultMatrixBuffer /* source buffer */,
  0 /* source offset */,
  gpuReadBuffer /* destination buffer */,
  0 /* destination offset */,
  resultMatrixBufferSize /* size */
);

// Submit GPU commands.
const gpuCommands = commandEncoder.finish();
device.queue.submit([gpuCommands]);

Đọc ma trận kết quả

Việc đọc ma trận kết quả cũng dễ dàng như gọi gpuReadBuffer.mapAsync() bằng GPUMapMode.READ và chờ lời hứa trả về được phân giải, cho biết vùng đệm GPU hiện đã được liên kết. Tại thời điểm này, bạn có thể lấy dải ô được liên kết bằng gpuReadBuffer.getMappedRange().

Kết quả phép nhân ma trận
Kết quả phép nhân ma trận

Trong mã của chúng ta, kết quả được ghi lại trong bảng điều khiển JavaScript của DevTools là "2, 2, 50, 60, 114, 140".

// Read buffer.
await gpuReadBuffer.mapAsync(GPUMapMode.READ);
const arrayBuffer = gpuReadBuffer.getMappedRange();
console.log(new Float32Array(arrayBuffer));

Xin chúc mừng! Bạn đã hoàn tất. Bạn có thể chơi với đoạn nhạc đó.

Một mẹo cuối cùng

Một cách giúp mã dễ đọc hơn là sử dụng phương thức getBindGroupLayout tiện lợi của quy trình điện toán để xác định bố cục nhóm liên kết từ mô-đun chương trình đổ bóng. Thủ thuật này giúp bạn không cần tạo bố cục nhóm liên kết tuỳ chỉnh và chỉ định bố cục quy trình trong quy trình điện toán như bạn có thể thấy bên dưới.

Bạn có thể xem hình minh hoạ getBindGroupLayout cho mẫu trước.

 const computePipeline = device.createComputePipeline({
-  layout: device.createPipelineLayout({
-    bindGroupLayouts: [bindGroupLayout]
-  }),
   compute: {
-// Bind group layout and bind group
- const bindGroupLayout = device.createBindGroupLayout({
-   entries: [
-     {
-       binding: 0,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 1,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "read-only-storage"
-       }
-     },
-     {
-       binding: 2,
-       visibility: GPUShaderStage.COMPUTE,
-       buffer: {
-         type: "storage"
-       }
-     }
-   ]
- });
+// Bind group
  const bindGroup = device.createBindGroup({
-  layout: bindGroupLayout,
+  layout: computePipeline.getBindGroupLayout(0 /* index */),
   entries: [

Kết quả về hiệu suất

Vậy việc chạy phép nhân ma trận trên GPU so với việc chạy trên CPU có gì khác biệt? Để tìm hiểu, tôi đã viết chương trình vừa mô tả cho một CPU. Và như bạn có thể thấy trong biểu đồ bên dưới, việc sử dụng toàn bộ sức mạnh của GPU có vẻ như là một lựa chọn hiển nhiên khi kích thước của ma trận lớn hơn 256x256.

Điểm chuẩn GPU so với CPU
Điểm chuẩn GPU so với CPU

Bài viết này chỉ là bước khởi đầu của hành trình khám phá WebGPU. Chúng tôi sẽ sớm cung cấp thêm các bài viết chuyên sâu về GPU Compute và cách hoạt động của tính năng kết xuất (canvas, hoạ tiết, bộ lấy mẫu) trong WebGPU.