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

Bài đăng này tìm hiểu 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

Có thể bạn đã biết, Bộ xử lý đồ hoạ (GPU) là một hệ thống con điện tử trong một máy tính ban đầu được thiết kế để xử lý đồ hoạ. Tuy nhiên, trong 10 năm qua, nó đã phát triển theo một cấu trúc linh hoạt hơn, cho phép các 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, mà 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à 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 (GPU) đa năng.

Tính toán GPU đã góp phần đá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 cấu trúc để chạy hiệu quả hơn trên GPU. Trước tình hình Nền tảng web hiện tại đang thiếu tính năng Điện toán GPU, 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, như WebGL. Nó rất mạnh mẽ và khá chi tiết, như 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. Thành thật mà nói, tôi chỉ mới phác thảo sơ qua để bạn có thể bắt đầu chơi trên nền tảng của mình. Tôi sẽ tìm hiểu sâu hơn về kết xuất WebGPU (vải canvas, kết cấu, 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 cam kết JavaScript sẽ phân giải không đồng bộ với bộ chuyển đổi GPU. Hãy coi bộ chuyển đổi này là thẻ đồ hoạ. Bạn có thể tích hợp bộ xử lý này (trên cùng một chip với CPU) hoặc rời rạc (thường là thẻ PCIe hiệu quả hơn nhưng sử dụng nhiều pin hơn).

Sau khi bạn có bộ chuyển đổi GPU, hãy gọi adapter.requestDevice() để có được một cam kết sẽ giải quyết vấn đề với 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 đều có các tuỳ chọn cho phép bạn nêu chi tiết loại bộ chuyển đổi (lựa chọn ưu tiên về nguồn pin) và thiết bị (tiện ích, giới hạn) mà bạn muốn. Để đơn giản, 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ớ vùng đệm

Hãy cùng xem cách sử dụng JavaScript để ghi dữ liệu vào bộ nhớ cho GPU. Quá trình này không đơn giản do mô hình hộp cát 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 được từ GPU. Phương thức này gọi device.createBuffer() để lấy kích thước vùng đệm và mức sử dụng. Mặc dù không bắt buộc phải sử dụng cờ GPUBufferUsage.MAP_WRITE cho 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 đó, vùng đệ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à một việc quen thuộc nếu bạn từng chơi bằng 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 thuộc sở hữu của CPU và có thể truy cập được trong hoạt động đọc/ghi từ JavaScript. Để GPU có thể truy cập vào thành phần này, bạn phải huỷ ánh xạ (unmap) đơn giản như khi gọi gpuBuffer.unmap().

Cần có khái niệm đã ánh xạ/chưa được ánh xạ để ngăn các tình huống tương tranh khi GPU và CPU truy cập vào bộ nhớ cùng lúc.

Đọc bộ nhớ vùng đệm

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

Vì chúng ta đang viết trong 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 bạn bắt buộc phải sử dụng 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 liên kết 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 sẽ được thực thi không đồng bộ. Đây là lý do tại sao 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 sẽ tạo một loạt lệnh "được 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ó vùng đệm", nghĩa là các phương thức này thực thi 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 các lệnh mã hoá 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 này 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 trong 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 liên kết. Sau đó, hãy lấy phạm vi đã ánh xạ có gpuReadBuffer.getMappedRange() chứa cùng các giá trị với bộ đệ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, dưới đây là những điều bạn cần nhớ liên quan đến hoạt động của bộ nhớ đệm:

  • Bộ đệm GPU phải chưa được ánh xạ thì mới dùng được khi gửi vào hàng đợi thiết bị.
  • Khi được liên kết, vùng đệm GPU có thể được đọc và ghi 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 đổ 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ẽ tam giác) được gọi là chương trình đổ bóng điện toán. Các CPU này được thực thi song song bởi hàng trăm lõi GPU (nhỏ hơn lõi CPU) cùng hoạt động để xử lý dữ liệu. Đầ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 tôi sẽ chơi bằng phép nhân ma trậ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 tôi sẽ làm:

  1. Tạo 3 vùng đệm GPU (2 cho các ma trận nhân và một cho ma trận kết quả)
  2. Mô tả đầu vào và đầu ra của 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 được mã hoá đến GPU
  6. Đọc vùng đệ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 một danh sách các số dấu phẩy động. Phần tử đầu tiên là số hàng, phần tử thứ hai là số cột và phần tử còn lại là số lượng thực tế của ma trận.

Cách 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
Cách biểu diễn đơn giản của ma trận trong JavaScript và cách biểu diễn ma trậ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 tại sao cờ sử dụng vùng đệ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 bộ đệm khác để đọc sau khi tất cả các lệnh trong 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à liên kết nhóm

Các 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. 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ế của chương trình đổ bóng.

Trong ví dụ bên dưới, bố cục nhóm liên kết yêu cầu 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à 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 0 liên kết, gpuBufferSecondMatrix với 1 liên kết và resultMatrixBuffer với 2 liên kết.

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ã chương trình đổ bóng điện toán dùng để nhân các ma trận được viết bằng WGSL, Ngôn ngữ của chương trình đổ bóng WebGPU, có thể dịch nhỏ sang SPIR-V. Nếu không đi sâu vào chi tiết, bạn sẽ thấy ở bên dưới ba vùng đệm lưu trữ đượ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 đầu ra.

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

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

Bây giờ, hãy cùng tìm hiểu 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 tôi là thực thi chương trình này song song với từng ô của ma trận kết quả, theo từng bước. 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à phương diện đầu tiên, phương diện 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 đến phương diện đó. 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 từng ô ma trận kết quả

Kích thước của lưới nhóm công việc trong 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ẽ chia cho 8. Bây giờ, chúng ta có thể điều phối một 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à các đối số dispatchWorkgroups().

Như đã thấy 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) 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á điện toán truyền qua, hãy gọi passEncoder.end(). Sau đó, hãy tạo một vùng đệm GPU để dùng làm đích đến nhằm 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 lệnh đó đế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ả cũng dễ dàng như gọi gpuReadBuffer.mapAsync() bằng GPUMapMode.READ và chờ lời hứa trả về được giải quyết, 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 được dải ô được ánh xạ 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 tôi, kết quả được ghi 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ể dùng thử.

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 điện 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 điện toán như có thể thấy dưới đây.

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: [

Phát hiện 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 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 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 điểm chuẩn CPU
Điểm chuẩn GPU so với điểm chuẩn CPU

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