RISC-V và Hệ thống nhúng¶
RISC-V là kiến trúc tập lệnh (instruction set architecture) mã nguồn mở đang định hình lại ngành công nghiệp chip. File này đề cập đến triết lý RISC-V, phần mở rộng vector V, suy luận ML nhúng, TinyML trên vi điều khiển, RISC-V trong các bộ tăng tốc AI, và các ràng buộc triển khai biên (edge).
- Mọi kiến trúc chip chúng ta đã đề cập cho đến nay (x86, ARM) đều đòi hỏi một giấy phép (licence). Intel và AMD trả tiền cho x86. Apple, Qualcomm và mọi nhà sản xuất điện thoại thông minh trả cho ARM hàng tỷ đô la mỗi năm. RISC-V thì khác: nó là một tiêu chuẩn mở. Bất kỳ ai cũng có thể thiết kế, sản xuất và bán chip RISC-V mà không phải trả tiền bản quyền cho ai cả. Điều này đang thay đổi kinh tế học của việc thiết kế chip, đặc biệt là cho AI.
Triết lý RISC-V¶
-
RISC-V (phát âm là "risk five" - rủi ro năm) được tạo ra tại UC Berkeley vào năm 2010 như một tập lệnh RISC hiện đại, sạch sẽ. Các nguyên lý then chốt:
-
Tiêu chuẩn mở: đặc tả ISA được cung cấp tự do. Bạn có thể xây dựng một CPU RISC-V mà không cần phí cấp phép, NDA, hay các thỏa thuận pháp lý. Điều này giống như cách Linux đối với hệ điều hành - bất kỳ ai cũng có thể sử dụng, sửa đổi và xây dựng dựa trên nó.
-
Thiết kế mô-đun: ISA cơ sở (RV32I hoặc RV64I) là tối giản - chỉ 47 lệnh. Mọi thứ khác đều là các phần mở rộng (extensions) tùy chọn: M (nhân/chia), A (thao tác nguyên tử), F/D (số thực dấu phẩy động), C (lệnh nén), V (xử lý vector). Bạn chỉ chọn những gì mình cần, giữ cho chip nhỏ gọn và hiệu quả.
-
Không mang gánh nặng di sản: x86 mang theo 45 năm tương thích ngược. ARM mang theo 35 năm. RISC-V bắt đầu sạch sẽ, kết hợp những bài học rút ra từ cả hai. Không có những lệnh quái gở chỉ tồn tại để tương thích với phần mềm thập niên 1980.
-
-
Ai dùng RISC-V: SiFive (các lõi đa dụng), Alibaba (các lõi máy chủ Xuantie), Western Digital (bộ điều khiển lưu trữ, hàng tỷ chiếc đã xuất xưởng), Espressif (ESP32-C3, chip IoT phổ biến), và hàng chục startup bộ tăng tốc AI dùng RISC-V làm bộ xử lý điều khiển (control processor) quản lý các đơn vị tính toán tùy chỉnh của chúng.
Kiến trúc cơ sở RISC-V¶
-
ISA số nguyên cơ sở (RV64I cho 64-bit) có:
- 32 thanh ghi đa dụng (x0-x31, mỗi thanh ghi 64-bit). x0 được nối cứng vào giá trị 0 (hữu ích để triển khai các khuôn mẫu phổ biến mà không cần lệnh đặc biệt).
- Độ rộng lệnh cố định 32-bit (với phần mở rộng C bổ sung các lệnh nén 16-bit để tăng mật độ mã).
- Kiến trúc load-store: giống ARM, các phép toán số học chỉ hoạt động trên thanh ghi. Truy cập bộ nhớ thông qua các lệnh load/store tường minh.
# Hợp ngữ RISC-V (để tham khảo - bạn sẽ dùng C/C++)
add x3, x1, x2 # x3 = x1 + x2
lw x4, 0(x5) # load word từ địa chỉ trong x5
sw x4, 8(x5) # store word đến địa chỉ x5 + 8
beq x1, x2, label # nhảy nếu x1 == x2
- Sự đơn giản của ISA giúp các lõi RISC-V nhỏ gọn và tiết kiệm năng lượng. Một lõi RV32I tối giản có thể được hiện thực với ~10.000 cổng logic (một ARM Cortex-M0 là ~12.000). Điều này quan trọng đối với hệ thống nhúng, nơi mọi miliwatt và mọi milimet vuông silicon đều có ý nghĩa.
Phần mở rộng V: Xử lý Vector RISC-V¶
- Phần mở rộng V (RVV) bổ sung xử lý vector có khả năng mở rộng (scalable) vào RISC-V, tương tự ARM SVE. Các thanh ghi vector có độ dài cấu hình được (VLEN), được chỉ định bởi phần cứng (128 đến 65.536 bit). Mã được viết để độc lập với độ dài vector (vector-length agnostic): nó hoạt động trên bất kỳ VLEN nào mà không cần biên dịch lại.
#include <riscv_vector.h>
// Phép cộng vector dùng các intrinsic RVV
void vadd_rvv(const float* a, const float* b, float* c, int n) {
while (n > 0) {
// vsetvl: thiết lập độ dài vector - xử lý min(n, hardware_max) phần tử
size_t vl = __riscv_vsetvl_e32m1(n);
// Nạp vl phần tử
vfloat32m1_t va = __riscv_vle32_v_f32m1(a, vl);
vfloat32m1_t vb = __riscv_vle32_v_f32m1(b, vl);
// Cộng
vfloat32m1_t vc = __riscv_vfadd_vv_f32m1(va, vb, vl);
// Lưu
__riscv_vse32_v_f32m1(c, vc, vl);
// Tiến con trỏ
a += vl; b += vl; c += vl; n -= vl;
}
}
-
vsetvllà lệnh then chốt. Nó nói với phần cứng "tôi muốn xử lý bấy nhiêu phần tử này" và phần cứng đáp lại "tôi có thể xử lý bấy nhiêu phần tử này" (bị giới hạn bởi VLEN). Vòng lặp tự động thích ứng với mọi độ rộng vector, không cần vòng lặp dọn dẹp (cleanup) scalar (lần lặp cuối đơn giản xử lý ít phần tử hơn). -
LMUL (hệ số nhân độ dài - length multiplier): RVV có thể nhóm nhiều thanh ghi vector lại với nhau (m1, m2, m4, m8) để xử lý nhiều phần tử hơn mỗi lệnh với cái giá là ít thanh ghi khả dụng hơn.
m1dùng một thanh ghi cho mỗi toán hạng vector;m8dùng tám, xử lý 8x phần tử hơn nhưng chỉ còn 4 nhóm thanh ghi khả dụng. -
So với x86 AVX (cố định 256/512-bit) và ARM NEON (cố định 128-bit), khả năng mở rộng của RVV là một lợi thế lớn cho phần cứng đa dạng: cùng một mã chạy trên một lõi nhúng nhỏ xíu (VLEN=128) và một lõi máy chủ hiệu năng cao (VLEN=1024+).
ML nhúng: TinyML¶
-
TinyML là học máy trên vi điều khiển (microcontrollers) - những thiết bị với vài kilobyte RAM, CPU tốc độ megahertz, và ngân sách năng lượng miliwatt. Hãy nghĩ: một cảm biến phát hiện từ khóa ("Hey Siri"), một gia tốc kế phân loại cử chỉ, hay một camera đếm người, tất cả chạy trên một chip giá $0,50 mà không cần kết nối internet.
-
Các ràng buộc là cực đoan:
| Tài nguyên | GPU máy chủ | Điện thoại thông minh | Vi điều khiển |
|---|---|---|---|
| RAM | 80 GB | 6 GB | 256 KB |
| Bộ nhớ | TB | 128 GB | 1 MB |
| Tính toán | 1000 TFLOPS | 10 TFLOPS | 0,001 TFLOPS |
| Năng lượng | 700 W | 5 W | 0,001 W |
| Chi phí | $30.000 | $500 | $1 |
- Một mô hình vừa với GPU máy chủ (\(O(10^{10})\) tham số) sẽ không vừa với vi điều khiển. Các mô hình TinyML có \(O(10^4)\)–\(O(10^6)\) tham số và dùng lượng tử hóa INT8 hoặc thậm chí INT4.
TensorFlow Lite Micro (TFLM)¶
- TFLM là framework suy luận cho vi điều khiển của Google. Nó chạy các mô hình TensorFlow Lite đã lượng tử hóa mà không cần cấp phát bộ nhớ động, không cần hệ điều hành, và có dung lượng nhị phân (binary footprint) ~20 KB.
// Suy luận TinyML trên vi điều khiển (đơn giản hóa)
#include "tensorflow/lite/micro/micro_interpreter.h"
#include "tensorflow/lite/micro/micro_mutable_op_resolver.h"
// Mô hình được biên dịch thành một mảng C (const unsigned char model_data[])
const tflite::Model* model = tflite::GetModel(model_data);
// Cấp phát một vùng nhớ (arena) cố định (không có malloc!)
constexpr int kArenaSize = 10 * 1024; // 10 KB
uint8_t tensor_arena[kArenaSize];
// Thiết lập interpreter
tflite::MicroInterpreter interpreter(model, resolver, tensor_arena, kArenaSize);
interpreter.AllocateTensors();
// Thiết lập đầu vào
float* input = interpreter.input(0)->data.f;
input[0] = sensor_reading;
// Chạy suy luận
interpreter.Invoke();
// Đọc đầu ra
float* output = interpreter.output(0)->data.f;
if (output[0] > 0.8f) {
trigger_alert();
}
-
Các ràng buộc then chốt trong mã này:
tensor_arenađược cấp phát tĩnh - không cómalloc, không có heap. Hệ thống nhúng thường không có bộ cấp phát bộ nhớ động.- Mô hình là một mảng byte
const, được lưu trong bộ nhớ flash (ROM), không được nạp từ hệ thống tệp. - Toàn bộ framework + mô hình + runtime vừa trong vài chục KB.
Tối ưu hóa mô hình cho biên (Edge)¶
-
Để chạy một mô hình trên vi điều khiển đòi hỏi tối ưu hóa quyết liệt:
- Lượng tử hóa (chương 18): chuyển trọng số float32 sang INT8 (nhỏ hơn 4x, nhanh hơn 2-4x trên phần cứng chỉ-số-nguyên). Lượng tử hóa sau huấn luyện (post-training quantisation) đơn giản; lượng tử hóa nhận thức huấn luyện (quantisation-aware training) giữ được độ chính xác tốt hơn.
- Cắt tỉa (Pruning): loại bỏ các trọng số gần bằng không. Cắt tỉa có cấu trúc (structured pruning - loại bỏ toàn bộ kênh/đầu) thân thiện với phần cứng hơn cắt tỉa không có cấu trúc (unstructured pruning - các số không ngẫu nhiên) vì nó giảm thiểu tính toán thực tế, không chỉ lưu trữ.
- Chắt lọc tri thức (Knowledge distillation - chương 6): huấn luyện một mô hình "học sinh" nhỏ để bắt chước một mô hình "giáo viên" lớn. Học sinh đạt độ chính xác cao hơn so với huấn luyện từ đầu, vì nó học từ các dự đoán mềm (soft predictions) của giáo viên.
- Tìm kiếm kiến trúc nơ-ron (Neural Architecture Search - NAS): tự động tìm kiếm các kiến trúc hiệu quả nằm trong ngân sách phần cứng (độ trễ, bộ nhớ, năng lượng). MicroNets và MCUNet tìm các kiến trúc được tối ưu cho các vi điều khiển cụ thể.
- Hợp nhất toán tử (Operator fusion): kết hợp conv + batch norm + ReLU thành một phép toán hợp nhất, loại bỏ các ghi bộ nhớ trung gian (cùng nguyên lý với hợp nhất kernel GPU, nhưng càng quan trọng hơn khi bạn chỉ có 256 KB RAM).
RISC-V trong các bộ tăng tốc AI¶
- Nhiều startup bộ tăng tốc AI dùng RISC-V không phải để chạy trực tiếp các mô hình ML, mà làm bộ xử lý điều khiển (control processor) quản lý các đơn vị tính toán tùy chỉnh:
┌─────────────────────────────────────────┐
│ AI Accelerator │
│ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ RISC-V │───→│ Custom Matrix │ │
│ │ Control │ │ Multiply Unit │ │
│ │ Core │ │ (systolic array,│ │
│ │ │ │ custom dataflow)│ │
│ └──────────┘ └──────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────────────┐ │
│ │ Memory │ │ On-chip SRAM │ │
│ │ Control │ │ (activation │ │
│ │ │ │ buffer) │ │
│ └──────────┘ └──────────────────┘ │
└─────────────────────────────────────────┘
-
Lõi RISC-V đảm nhiệm: nạp trọng số mô hình từ bộ nhớ ngoài, lập lịch thực thi lớp, quản lý luồng dữ liệu giữa các đơn vị tính toán, và giao tiếp với máy chủ (qua PCIe, USB, hoặc SPI). Tính toán nặng (nhân ma trận, tích chập) được thực hiện bởi phần cứng tùy chỉnh, không phải lõi RISC-V.
-
Tại sao chọn RISC-V cho điều khiển: không tốn phí cấp phép (quan trọng với startup), có thể tùy biến (thêm các lệnh đặc thù miền), dấu chân nhỏ (một lõi điều khiển không cần độ phức tạp của x86), và hệ sinh thái mở cho phép tạo mẫu nhanh.
-
Ví dụ: Esperanto Technologies (1000+ lõi RISC-V cho ML), Tenstorrent (điều khiển RISC-V + các lõi tensix tùy chỉnh), SiFive (các lõi RISC-V với phần mở rộng vector cho ML biên).
Các ràng buộc triển khai biên (Edge)¶
-
Triển khai ML ở biên (trên thiết bị, không phải trên đám mây) mang lại các ràng buộc mà triển khai đám mây không gặp phải:
-
Năng lượng: một thiết bị chạy pin có thể có tổng ngân sách năng lượng 100 mW. Chạy một mô hình tiêu thụ 50 mW chỉ để lại 50 mW cho phần còn lại của hệ thống (cảm biến, radio, màn hình). Suy luận nhận thức năng lượng (power-aware inference) lập lịch tính toán để tránh giới hạn nhiệt (thermal throttling) và kéo dài tuổi thọ pin.
-
Độ trễ: suy luận biên thường phải theo thời gian thực. Bộ phát hiện từ khóa ("Hey Siri") phải phản hồi trong ~200 ms. Hệ thống nhận thức lái xe tự hành (chương 11) phải xử lý các khung hình trong ~30 ms. Vòng lặp mạng đến đám mây (50-200 ms) là quá chậm cho các trường hợp này.
-
Riêng tư: xử lý dữ liệu trên thiết bị nghĩa là dữ liệu nhạy cảm (ảnh y tế, bản ghi âm, ảnh cá nhân) không bao giờ rời khỏi thiết bị. Đây là yêu cầu pháp lý ở một số khu vực tài phán (GDPR) và là yêu cầu niềm tin của người dùng ở mọi nơi.
-
Kết nối: các thiết bị biên có thể có kết nối internet gián đoạn hoặc không có. Một mô hình chạy trên tàu thám hiểm sao Hỏa (chương 11), một tàu ngầm, hoặc một cảm biến nông trại vùng sâu vùng xa phải hoạt động hoàn toàn ngoại tuyến.
-
Chi phí ở quy mô lớn: triển khai ML cho một tỷ điện thoại thông minh tốn $0 mỗi thiết bị (phần cứng đã tồn tại). Triển khai cho một tỷ cảm biến IoT nghĩa là ngân sách phần cứng ML của mỗi cảm biến chỉ còn vài xu. Chi phí cấp phép bằng không của RISC-V có ý nghĩa cực lớn ở quy mô này.
Bài tập lập trình (biên dịch bằng g++ hoặc trình biên dịch chéo riscv64-gcc)¶
- Viết một chương trình C mô phỏng một quy trình suy luận TinyML: cấp phát tĩnh một bộ đệm mô hình, chạy một lượt truyền xuôi (forward pass) giả, và đo lường việc sử dụng tài nguyên. Điều này dạy các ràng buộc nhúng (không có malloc, vùng nhớ cố định).
// task1_tinyml_sim.cpp
// Biên dịch: g++ -O2 -o task1 task1_tinyml_sim.cpp
#include <iostream>
#include <chrono>
#include <cmath>
#include <cstring>
// Mô phỏng một vi điều khiển: vùng nhớ cố định, không cấp phát động
static constexpr int ARENA_SIZE = 32 * 1024; // 32 KB tổng ngân sách RAM
static uint8_t arena[ARENA_SIZE];
// MLP 2 lớp đơn giản: 784 -> 64 -> 10 (kiểu MNIST, trọng số INT8)
struct TinyModel {
int8_t w1[784 * 64]; // trọng số lớp 1: 50.176 byte
int8_t b1[64]; // độ lệch lớp 1
int8_t w2[64 * 10]; // trọng số lớp 2: 640 byte
int8_t b2[10]; // độ lệch lớp 2
// Tổng: ~51 KB → phải đặt trong flash (ROM), không phải RAM
};
// Kiểm tra mô hình vừa với flash
void check_model_fit(int flash_kb) {
int model_bytes = sizeof(TinyModel);
std::cout << "Kích thước mô hình: " << model_bytes << " byte ("
<< model_bytes / 1024 << " KB)\n";
std::cout << "Flash: " << flash_kb << " KB → "
<< (model_bytes <= flash_kb * 1024 ? "VỪA" : "QUÁ LỚN") << "\n";
}
// Suy luận giả dùng vùng nhớ cố định cho các activation
void mock_inference(const int8_t* input, int8_t* output) {
// Các activation nằm trong arena (RAM), không được cấp phát động
int8_t* act1 = (int8_t*)arena; // 64 byte cho đầu ra lớp 1
int8_t* act2 = (int8_t*)(arena + 64); // 10 byte cho đầu ra lớp 2
// Lớp 1: matmul đơn giản hóa (không phải matmul lượng tử hóa thật, chỉ minh họa cấu trúc)
for (int j = 0; j < 64; j++) {
int32_t sum = 0; // tích lũy trong int32 để tránh tràn số
for (int i = 0; i < 784; i++) {
sum += (int32_t)input[i] * 1; // giả: trọng số = 1
}
act1[j] = (int8_t)std::max(-128, std::min(127, sum / 784)); // lượng tử hóa lại
act1[j] = act1[j] > 0 ? act1[j] : 0; // ReLU
}
// Lớp 2
for (int j = 0; j < 10; j++) {
int32_t sum = 0;
for (int i = 0; i < 64; i++) {
sum += (int32_t)act1[i] * 1;
}
act2[j] = (int8_t)std::max(-128, std::min(127, sum / 64));
}
std::memcpy(output, act2, 10);
}
int main() {
std::cout << "=== Ngân sách tài nguyên TinyML ===\n";
std::cout << "Arena (RAM): " << ARENA_SIZE << " byte ("
<< ARENA_SIZE / 1024 << " KB)\n";
check_model_fit(256); // flash MCU điển hình
// Bộ nhớ activation đã dùng
int activation_bytes = 64 + 10; // đầu ra lớp 1 + lớp 2
std::cout << "Bộ nhớ activation: " << activation_bytes
<< " byte / " << ARENA_SIZE << " có sẵn\n\n";
// Đo hiệu năng suy luận
int8_t input[784];
int8_t output[10];
std::memset(input, 1, 784);
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
mock_inference(input, output);
}
auto end = std::chrono::high_resolution_clock::now();
double us = std::chrono::duration<double, std::micro>(end - start).count() / 10000;
std::cout << "Độ trễ suy luận: " << us << " us\n";
std::cout << "Tại MCU 160 MHz (~6,25 ns/chu kỳ): ~"
<< (int)(us * 160) << " chu kỳ\n";
std::cout << "Logits đầu ra: ";
for (int i = 0; i < 10; i++) std::cout << (int)output[i] << " ";
std::cout << "\n";
return 0;
}
- Viết một chương trình C++ lượng tử hóa trọng số float32 sang INT8 và đo tỷ lệ nén cùng sai số lượng tử hóa.
// task2_quantise.cpp
// Biên dịch: g++ -O3 -o task2 task2_quantise.cpp
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
#include <numeric>
// Lượng tử hóa đối xứng: ánh xạ dải float [-max, +max] sang [-127, +127]
void quantise_symmetric(const float* input, int8_t* output, int n, float& scale) {
float max_val = 0.0f;
for (int i = 0; i < n; i++) {
max_val = std::max(max_val, std::abs(input[i]));
}
scale = max_val / 127.0f;
for (int i = 0; i < n; i++) {
float scaled = input[i] / scale;
output[i] = (int8_t)std::max(-127.0f, std::min(127.0f, std::round(scaled)));
}
}
// Giải lượng tử hóa: INT8 về lại float
void dequantise(const int8_t* input, float* output, int n, float scale) {
for (int i = 0; i < n; i++) {
output[i] = (float)input[i] * scale;
}
}
int main() {
const int N = 100000;
// Mô phỏng trọng số ngẫu nhiên (xấp xỉ phân phối chuẩn)
std::vector<float> weights(N);
for (int i = 0; i < N; i++) {
// Các giá trị giả ngẫu nhiên kiểu chuẩn
float u1 = (float)(i * 7 % 997 + 1) / 998.0f;
float u2 = (float)(i * 13 % 991 + 1) / 992.0f;
weights[i] = std::sqrt(-2.0f * std::log(u1)) * std::cos(6.2832f * u2) * 0.1f;
}
// Lượng tử hóa
std::vector<int8_t> quantised(N);
float scale;
quantise_symmetric(weights.data(), quantised.data(), N, scale);
// Giải lượng tử hóa và đo sai số
std::vector<float> reconstructed(N);
dequantise(quantised.data(), reconstructed.data(), N, scale);
float max_error = 0.0f, total_error = 0.0f;
for (int i = 0; i < N; i++) {
float err = std::abs(weights[i] - reconstructed[i]);
max_error = std::max(max_error, err);
total_error += err;
}
std::cout << "=== Kết quả lượng tử hóa ===\n";
std::cout << "Gốc: " << N * 4 << " byte (float32)\n";
std::cout << "Lượng tử hóa: " << N * 1 << " byte (int8) + 4 byte (scale)\n";
std::cout << "Nén: " << 4.0f << "x\n";
std::cout << "Hệ số scale: " << scale << "\n";
std::cout << "Sai số tuyệt đối trung bình: " << total_error / N << "\n";
std::cout << "Sai số tuyệt đối cực đại: " << max_error << "\n";
std::cout << "Sai số tuyệt đối cực đại / scale: " << max_error / scale
<< " (nên <= 0,5 mức lượng tử hóa)\n";
return 0;
}
- Viết một chương trình C++ thực hiện phép nhân ma trận INT8 với tích lũy INT32 - chính là phép tính chạy trên các bộ tăng tốc ML nhúng.
// task3_int8_matmul.cpp
// Biên dịch: g++ -O3 -o task3 task3_int8_matmul.cpp
#include <iostream>
#include <chrono>
#include <vector>
#include <cstdint>
// Matmul INT8 với tích lũy INT32 (điều mà Tensor Cores và bộ tăng tốc MCU làm)
void matmul_int8(const int8_t* A, const int8_t* B, int32_t* C,
int M, int N, int K) {
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
int32_t sum = 0;
for (int k = 0; k < K; k++) {
sum += (int32_t)A[i * K + k] * (int32_t)B[k * N + j];
}
C[i * N + j] = sum;
}
}
}
// Matmul Float32 để so sánh
void matmul_f32(const float* A, const float* B, float* C,
int M, int N, int K) {
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
float sum = 0.0f;
for (int k = 0; k < K; k++) {
sum += A[i * K + k] * B[k * N + j];
}
C[i * N + j] = sum;
}
}
}
int main() {
const int M = 128, N = 128, K = 128;
std::vector<int8_t> A_i8(M * K, 1), B_i8(K * N, 1);
std::vector<int32_t> C_i32(M * N);
std::vector<float> A_f32(M * K, 1.0f), B_f32(K * N, 1.0f);
std::vector<float> C_f32(M * N);
// Đo INT8
auto start = std::chrono::high_resolution_clock::now();
for (int t = 0; t < 100; t++) {
matmul_int8(A_i8.data(), B_i8.data(), C_i32.data(), M, N, K);
}
auto end = std::chrono::high_resolution_clock::now();
double i8_ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
// Đo FP32
start = std::chrono::high_resolution_clock::now();
for (int t = 0; t < 100; t++) {
matmul_f32(A_f32.data(), B_f32.data(), C_f32.data(), M, N, K);
}
end = std::chrono::high_resolution_clock::now();
double f32_ms = std::chrono::duration<double, std::milli>(end - start).count() / 100;
double gflops_i8 = 2.0 * M * N * K / i8_ms / 1e6;
double gflops_f32 = 2.0 * M * N * K / f32_ms / 1e6;
std::cout << "Matmul INT8: " << i8_ms << " ms (" << gflops_i8 << " GOPS)\n";
std::cout << "Matmul FP32: " << f32_ms << " ms (" << gflops_f32 << " GFLOPS)\n";
std::cout << "Tăng tốc INT8: " << f32_ms / i8_ms << "x\n";
std::cout << "Bộ nhớ: INT8 = " << M*K + K*N << " byte so với FP32 = "
<< (M*K + K*N) * 4 << " byte (ít hơn 4x)\n";
return 0;
}