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 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 biệt hoá để xử lý đồ hoạ. Tuy nhiên, trong 10 năm qua, GPU đã phát triển theo hướng có cấu 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 cấu trúc riêng biệt của GPU. Những chức năng này được gọi là Điện toán GPU và việc sử dụng GPU làm bộ đồng xử lý cho điện toán khoa học đa năng được gọi là lập trình GPU đa năng (GPGPU).

Đ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 kiến trúc này để chạy hiệu quả hơn trên GPU. Do 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ó tên là WebGPU.

WebGPU là một API cấp thấp, tương tự như WebGL. Nó rất mạnh mẽ và khá dài dòng, như bạn sẽ thấy. Nhưng không sao cả. Điều chúng tôi muốn là hiệu suất.

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

Truy cập vào GPU

WebGPU giúp bạn dễ dàng truy cập vào GPU. 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 bộ điều hợp GPU. Hãy xem bộ chuyển đổi này như card đồ hoạ. GPU có thể được tích hợp (trên cùng một chip với CPU) hoặc rời (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 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ẽ dùng để thực hiện một số hoạt động tính toán trên 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 lựa chọn cho phép bạn chỉ định rõ loại bộ chuyển đổi (ưu tiên nguồn điệ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 lựa chọn mặc định trong bài viết này.

Bộ nhớ vùng đệm ghi

Hãy xem cách 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 dùng trong các trình duyệt web hiện đại.

Ví dụ bên dưới minh hoạ 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(), hàm này lấy kích thước của vùng đệm và mức sử dụng của vùng đệm. Mặc dù không bắt buộc phải có cờ sử dụng GPUBufferUsage.MAP_WRITE cho lệnh gọi cụ thể này, nhưng hãy nói rõ rằng chúng ta muốn ghi vào vùng đệm này. Điều này dẫn đến một đối tượng vùng đệm GPU được ánh xạ 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 các byte sẽ quen thuộc nếu bạn đã từng dùng ArrayBuffer; hãy 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ạ, tức là thuộc sở hữu của CPU và có thể truy cập để đọc/ghi từ JavaScript. Để GPU có thể truy cập vào vùng đệm này, bạn phải huỷ ánh xạ vùng đệm. Việc này đơn giản như gọi gpuBuffer.unmap().

Khái niệm được ánh xạ/không được ánh xạ là cần thiết để ngăn chặn tình trạng xung đột dữ liệu khi 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 một vùng đệm GPU sang một 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. Lần này, vùng đệm GPU thứ hai được tạo ở trạng thái chưa được ánh xạ 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ộ đồ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ộ. Đây là lý do 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ô các lệnh "được lưu vào bộ nhớ đệ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 đệm", nghĩa là chúng 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 đó đến 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 tất cả các lệnh được lưu trữ trong mảng theo thứ tự.

// 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 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ẽ phân giải khi vùng đệm GPU được ánh xạ. Sau đó, hãy 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 được xếp 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ỷ ánh xạ các 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, các vùng đệm GPU có thể được đọc và ghi bằng JavaScript.
  • Các vùng đệm GPU được ánh xạ khi mapAsync()createBuffer() với mappedAtCreation được đặt thành true được gọi.

Lập 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 (và không vẽ các hình tam giác) được gọi là chương trình đổ bóng điện toán. Chúng đượ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à các vùng đệm trong WebGPU.

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

Sơ đồ phép nhân ma trận
Sơ đồ phép nhân ma trận

Nói tóm lại, chúng tôi sẽ làm những việc sau:

  1. Tạo 3 vùng đệm GPU (2 vùng đệm cho các ma trận cần nhân và 1 vùng đệm cho ma trận kết quả)
  2. Mô tả đầ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 thị dưới dạng danh sách các số thực. 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à số thực của ma trận.

Biểu diễn đơn giản của ma trận trong JavaScript và tương đương trong ký hiệu toán học
Biểu diễn đơn giản của một ma trận trong JavaScript và biểu diễn tương đương trong 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ờ sử dụng bộ đệm GPU bao gồm 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ả các lệnh hàng đợi GPU đã đượ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
});

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

Các khái niệm về bố cục nhóm liên kết và nhóm liên kết dành riêng cho WebGPU. Một 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 một nhóm liên kết đại diện cho dữ liệu đầu vào/đầu ra thực tế của 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ó 2 vùng đệm lưu trữ chỉ đọc tại các liên kết mục nhập đượ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: 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 vào chi tiết, bạn sẽ thấy 3 vùng đệm lưu trữ được xác định bằng var<storage> bên dưới. 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 lưu trữ đều có một phần trang trí binding được 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à các 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 tính toán là đối tượng thực sự mô tả hoạt động tính toán mà chúng ta sẽ thực hiện. Tạo bằng cách gọi device.createComputePipeline(). Hàm này nhận 2 đố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 tính toán (hàm main WGSL) và mô-đun chương trình đổ bóng tính 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 khởi tạo một nhóm liên kết bằng 3 vùng đệm GPU và một quy trình tính toán có bố cục nhóm liên kết, đã đến lúc sử dụng chúng.

Hãy bắt đầu bộ mã hoá truyền điện toán có thể lập trình bằng commandEncoder.beginComputePass(). Chúng ta sẽ dùng lệnh 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 nó ở chỉ mục 0 bằng passEncoder.setBindGroup(0, bindGroup). Chỉ mục 0 tương ứng với phần trang trí group(0) trong mã WGSL.

Bây giờ, hãy nói về 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 chương trình này song song 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 16 x 32, để 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à chiều thứ nhất, đối số thứ hai "y" là chiều thứ hai và đối số mới nhất "z" là chiều thứ ba có giá trị mặc định là 1 vì chúng ta không cần đối số 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 hợp dữ liệu được gọi là gửi.

Thực thi song song cho từng ô ma trận kết quả
Thực thi song song cho từng ô 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" lần lượt 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. Với thao tác đó, giờ đây, chúng ta có thể gửi một lệnh gọi tính toán bằng passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8). Số lượng lưới nhóm làm việc cần chạy là các đố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 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á truyền tính 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 đó đến 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ả 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 ánh xạ. Tại thời điểm này, bạn có thể nhận được dải ô được liên kết bằng gpuReadBuffer.getMappedRange().

Kết quả của 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 Công cụ cho nhà phát triển 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 mẫu.

Một 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 ra 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 tính toán như bạn có thể thấy bên dưới.

Hình minh hoạ về getBindGroupLayout cho mẫu trước đó là 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: [

Thông tin chi tiết về hiệu suất

Vậy việc chạy phép nhân ma trận trên GPU khác với việc chạy phép nhân ma trận trên CPU như thế nào? Để 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ẻ là một lựa chọn rõ ràng 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. Sắp tới, bạn có thể đón đọc thêm nhiều bài viết đi sâu vào Điện toán GPU và cách hoạt động của tính năng kết xuất (canvas, texture, sampler) trong WebGPU.