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à thông tin trợ 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 đã biết, Bộ xử lý đồ hoạ (GPU) là một bộ xử lý điện tử hệ thống con trong một máy tính ban đầu chuyên xử lý đồ hoạ. Tuy nhiên, trong 10 năm qua, nền tảng này đã phát triển theo hướng 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ỉ hiển thị đồ hoạ 3D trong khi vẫn tận dụng được kiến trúc độc đáo của GPU. Những khả năng này được gọi là Điện toán GPU và 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à bộ đồng xử lý Lập trình GPU (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 trong thời gian 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 để chạy hiệu quả hơn trên GPU. Với Nền tảng web hiện tại thiếu khả năng Điện toán GPU, "GPU cho Web" của W3C Nhóm cộng đồng đang thiết kế một API để hiển thị các API GPU hiện đại có sẵn trên hầu hết thiết bị hiện tại. API này được gọi là WebGPU.

WebGPU là một API cấp thấp, chẳng hạn như WebGL. Công cụ này rất mạnh mẽ và chi tiết, bạn sẽ thấy. Nhưng không sao cả. Đ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 Điện toán GPU của WebGPU và để Thật lòng mà nói, tôi chỉ đang sơ sài để bạn có thể bắt đầu chơi trên của bạn. Tôi sẽ đi sâu hơn vào 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

Trong WebGPU, bạn có thể dễ dàng truy cập vào GPU. Đang gọi cho navigator.gpu.requestAdapter() trả về lời hứa JavaScript sẽ phân giải không đồng bộ với GPU bộ chuyển đổi. Hãy xem bộ điều hợp này giống như một thẻ đồ hoạ. Bạn có thể tích hợp giải pháp này (trên cùng một chip với CPU) hoặc rời rạc (thường là thẻ PCIe có hiệu quả cao nhưng tốn nhiều năng lượng hơn).

Sau khi bạn có bộ điều hợp GPU, hãy gọi adapter.requestDevice() để nhận lời hứa sẽ phân giải bằng thiết bị GPU mà bạn sẽ sử dụng để tính toán GPU.

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

Cả hai chức năng này đều có các tuỳ chọn cho phép bạn xác định cụ thể về loại bộ sạc (lựa chọn ưu tiên về nguồn điện) và thiết bị (tiện ích, giới hạn) mà bạn muốn. Đối với để đơn giản hoá, chúng tôi 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. Chiến dịch này không đơn giản vì mô hình hộp cát dùng trong môi trường web hiện đại trình duyệt.

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

Việc ghi byte là quen thuộc nếu bạn đã chơi với ArrayBuffer; sử dụng TypedArray rồi 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à dữ liệu đó có thể truy cập được bằng cách đọc/ghi qua JavaScript. Để GPU có thể truy cập vào dữ liệu đó, phải chưa được ánh xạ, việc này đơn giản như gọi gpuBuffer.unmap().

Cần có khái niệm về việc được ánh xạ/chưa được ánh xạ để ngăn tình huống tương tranh trong đó GPU và bộ nhớ truy cập CPU 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 rồi đọ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 nó vào một giây Vùng đệm GPU, bắt buộc phải có một cờ sử dụng mới GPUBufferUsage.COPY_SRC. Thứ hai Lần này vùng đệm GPU được tạo ở trạng thái chưa được liên kết bằng device.createBuffer(). Cờ sử dụng của lớp này là GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ vì nó sẽ được dùng làm đích đến của GPU đầu tiên vùng đệm và đọ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 của GPU đều được thực thi một cách 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 đi theo lô khi cần. Trong WebGPU, bộ mã hoá lệnh GPU được trả về bởi device.createCommandEncoder() là đối tượng JavaScript tạo một loạt "đã lưu vào bộ đệm" các lệnh được gửi đến GPU vào một thời điểm nào đó. Các phương thức trên Mặt khác, GPUBuffer "không được lưu vào bộ đệm", nghĩa là thực thi nguyên tử vào thời điểm chúng đượ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 các lệnh mã hoá bằng cách gọi copyEncoder.finish() rồi gửi chúng vào hàng đợi lệnh của thiết bị GPU. Hàng đợi chịu trách nhiệm xử lý lượt gửi được thực hiện qua device.queue.submit() với các lệnh GPU làm đối số. Thao tác này sẽ thực thi theo thứ 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 chưa chắ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 được ánh xạ. Sau đó, hãy lấy dải ô được ánh xạ bằng gpuReadBuffer.getMappedRange() mà chứa cùng giá trị với 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ề hoạt động của bộ nhớ đệm:

  • Vùng đệm GPU phải không được liên kết thì mới có thể dùng trong quá trình gửi hàng đợi thiết bị.
  • Khi được ánh xạ, vùng đệm GPU có thể được đọc và ghi bằng JavaScript.
  • Vùng đệm GPU được liên kết khi mapAsync()createBuffer() với mappedAtCreation được đặt thành true sẽ đượ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 phép tính (và không vẽ) tam giác) được gọi là chương trình đổ bóng điện toán. Các yêu cầu này được thực thi song song hàng trăm lõi GPU (nhỏ hơn lõi CPU) hoạt động cùng nhau để xử lý . Đầu vào và đầu ra của chúng là 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ẽ thử nghiệm ma trận phép nhân, một thuật toán phổ biến trong công nghệ học máy được minh hoạ dưới đây.

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

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

  1. Tạo ba vùng đệm GPU (2 vùng cho ma trận nhân và một vùng đệm cho ma trận 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á cho GPU
  6. Đọc bộ đệm GPU ma trận kết quả

Tạo vùng đệm GPU

Để cho đơn giản, ma trận sẽ được biểu diễn dưới dạng danh sách các hàm biến số điểm. Phần tử đầu tiên là số lượng hàng, phần tử thứ hai là số lượng hàng 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 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 của một ma trận trong JavaScript và mã tương đương của ma trận đó 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 tại sao cờ sử dụng vùng đệm GPU bao gồm GPUBufferUsage.STORAGE cho tất cả các đường liên kết đó. Cờ sử dụng ma trận kết quả cũng có GPUBufferUsage.COPY_SRC vì tệp này sẽ được sao chép vào một vùng đệm khác cho đọ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
});

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

Khái niệm về bố cục nhóm liên kết và nhóm liên kết là dành riêng cho WebGPU. Ràng buộc A bố cục nhóm 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 biểu thị 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 có hai 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à vùng đệm lưu trữ tại 2 dành 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, liên kết Vùng đệm GPU cho các mục nhập: gpuBufferFirstMatrix đến 0 liên kết, gpuBufferSecondMatrix với liên kết 1resultMatrixBuffer với liên kết 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
      }
    }
  ]
});

Tính toán mã chương trình đổ bóng

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

Lưu ý rằng mỗi vùng đệm lưu trữ 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à 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 điện toán là đối tượng thực sự mô tả thao tác điện toán mà chúng tôi sẽ thực hiện. Hãy tạo lớp này bằng cách gọi device.createComputePipeline(). Có hai đối số: bố cục nhóm liên kết mà chúng ta đã tạo trước đó và một điện toán giai đoạ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 thực thể nhóm liên kết bằng 3 vùng đệm GPU và một điện toán quy trình có bố cục nhóm liên kết, đã đến lúc sử dụng chúng.

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

Bây giờ, hãy cùng 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 là thực thi chương trình này song song với từng ô của ma trận kết quả, từng bước. Chẳng hạn, đố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 đầu tiên, chiều thứ hai là "y" là phương diện thứ hai, và đồng hồ hẹn giờ mới nhất là "z" là phương diện thứ ba mặc định là 1 vì chúng tôi không cần ở đây. Trong thế giới điện toán GPU, việc mã hoá một lệnh để thực thi một 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 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 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 chậm nhất là 8 giờ. Cùng với đó, giờ đây chúng ta có thể gửi một lệnh gọi điện toán bằng passEncoder.dispatchWorkgroups(firstMatrix[0] / 8, secondMatrix[1] / 8). Chiến lược phát hành đĩa đơn số lượng lưới nhóm công việc cần chạy là các đối số dispatchWorkgroups().

Như trong hình 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) sẽ được dùng để biết kết quả nào để 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ẻ điện toán, hãy gọi passEncoder.end(). Sau đó, hãy tạo một Vùng đệm GPU dùng làm đích đến sao chép vùng đệm ma trận kết quả bằng copyBufferToBuffer. Cuối cùng, hãy hoàn tất các lệnh mã hoá bằng copyEncoder.finish() rồi gửi các thiết bị đó đến 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ả dễ dàng như gọi gpuReadBuffer.mapAsync() bằng GPUMapMode.READ và đang chờ giải quyết lời hứa trả lại hàng. Thông báo này cho biết vùng đệm GPU hiện đã được ánh xạ. Tại thời điểm này, có thể lấy được bản đồ dải ô bằng gpuReadBuffer.getMappedRange().

Kết quả nhân ma trận
Kết quả nhân ma trận

Trong đoạn 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ể thử nghiệm 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 tiện ích Phương thức getBindGroupLayout của quy trình tính toán để suy ra nhóm liên kết qua mô-đun chương trình đổ bóng. Với thủ thuật này, bạn không cần phải tạo bố cục nhóm liên kết tuỳ chỉnh và chỉ định bố cục quy trình trong tính toán của bạn như bạn có thể thấy dưới đây.

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 việc chạy phép nhân ma trận trên GPU so với việc chạy trên GPU như thế nào CPU? Để 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ể hãy xem 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 của 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. Kỳ vọng nhiều hơn các bài viết sẽ sớm cung cấp thông tin chuyên sâu hơn về Điện toán GPU và cách kết xuất hình ảnh (canvas, texture, sampler) hoạt động trong WebGPU.