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

Bài đăng này giới thiệu về 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 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 được chuyên dùng để 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. Như bạn đã thấy, công cụ này rất mạnh mẽ và chi tiết. Nhưng không sao. Điều chúng tôi mong đợi là 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ộ với bộ chuyển đổi GPU. Hãy xem bộ chuyển đổi này như 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 hoá, 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ù cờ sử dụng GPUBufferUsage.MAP_WRITE không bắt buộc đối với lệnh gọi cụ thể này, nhưng hãy nêu 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ẽ 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 ánh xạ, có nghĩa là vùng đệm này thuộc sở hữu của CPU và có thể truy cập được khi đọc/ghi qua 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 viết vào vùng đệm GPU đầu tiên và muốn sao chép vào vùng đệm GPU thứ hai, nên bạn bắt buộc phải 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 lớp này là GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ vì 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ộ đồng xử lý độc lập, nên tất cả cá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 nội dung gửi 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 trong hàng đợi GPU đã được gửi nhưng chưa chắc đã được thực thi. Để đọc vùng đệm GPU thứ hai, hãy gọi gpuReadBuffer.mapAsync() bằng GPUMapMode.READ. Hàm này trả về một lời hứa sẽ phân giải khi vùng đệm GPU được liên kết. 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, dưới đây là những việc chúng ta sẽ làm:

  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 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 của một ma trận trong JavaScript và mã tương đương của ma trận đó dưới dạng 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à nhóm liên kết

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à 3 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(). Cần có 2 đối số: bố cục nhóm liên kết mà chúng ta đã tạo trước đó và 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 main WGSL) 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 của quy trình đó ở 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 từng ô 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() với 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ả dễ dàng như gọi gpuReadBuffer.mapAsync() bằng GPUMapMode.READ và chờ lời hứa trả về sẽ phân giải. Điều này cho biết vùng đệm GPU hiện đã được ánh xạ. 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ả nhân ma trận
Kết quả 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ẹo cuối cùng

Một cách giúp mã của bạn dễ đọc hơn là sử dụng phương thức getBindGroupLayout tiện dụng của quy trình tính toán để suy luận 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.

Hình minh hoạ getBindGroupLayout cho mẫu trước đó có sẵn.

 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 thì việc chạy phép nhân ma trận trên GPU so với chạy trên CPU thì như thế nào? Để tìm hiểu, tôi đã viết chương trình vừa được 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ẻ 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 256 x 256.

Đ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 trong hành trình khám phá WebGPU của tôi. 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.