Mô hình dữ liệu
Chương trình ổn địnhHLO là các phép tính qua tensor
(các mảng n chiều) mà trong mô hình hiện tại được triển khai bằng cách sử dụng
lớp Tensor
. Lớp lưu trữ cơ bản cho đối tượng Tensor
,
detail::Buffer
, lưu trữ mlir::ShapedType
của tensor cùng với một tham số
Đối tượng mlir::HeapAsmResourceBlob
đại diện cho một blob có thể thay đổi của tensor
dữ liệu được bố trí dưới dạng mảng byte liền kề nhau
đơn đặt hàng lớn đến trẻ vị thành niên.
Các đối tượng detail::Buffer
được tính tham chiếu để đơn giản hoá việc quản lý bộ nhớ.
Các phần tử riêng lẻ của một tensor được biểu thị bằng lớp Element
, trong đó
sử dụng kết hợp phân biệt đối xử, nắm giữ một trong APInt
, APFloat
hoặc
pair<APFloat,APFloat>
để lưu trữ. Tệp cuối cùng được dùng để lưu trữ các phần tử
với các kiểu phức tạp.
Tensor
có các API sau để tương tác với các phần tử riêng lẻ:
Element Tensor::get(llvm::ArrayRef<int64_t> index)
: Để trích xuất một phần tử tensor riêng lẻ ở chỉ số đa chiềuindex
dưới dạngElement
.void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);
: Để cập nhật đối tượngElement
element
thành tensor ở đa chiều chỉ mụcindex
.
Cách hoạt động của phiên dịch
Hàm nhập cho trình thông dịch là
SmallVector<Tensor> eval(func::FuncOp func, ArrayRef<Tensor> args);
Hàm này thực hiện những việc sau:
- Theo dõi các đối số SSA của
func
và thời gian chạyTensor
liên quan của các đối số đó các giá trị, được cung cấp trongargs
, sử dụng bản đồ bảng biểu tượng, M. - Đối với mỗi hoạt động trong
func
, theo thứ tự SSACFG:- Gọi
eval
trên op. Đối với mỗi toán hạng SSA của op, hãy trích xuất thời gian chạy từ M được cung cấp làm đối số cho lệnh gọieval
. - Theo dõi(các) kết quả SSA của hoạt động và giá trị được đánh giá theo M.
- Gọi
eval
cấp vận hành được đề cập trong (2) chịu trách nhiệm triển khai
ngữ nghĩa thực thi của op. Sau đây là ví dụ về stablehlo::AddOp
.
Trong ví dụ này, các phần tử riêng lẻ của tensor lhs
và rhs
sẽ theo cặp
được trích xuất dưới dạng các đối tượng Element
. Sau đó, các đối tượng này sẽ được thêm vào. Kết quả của việc cộng,
đối tượng Element
, được lưu trữ trong tensor result
cuối cùng.
Tensor eval(AddOp op, const Tensor &lhs, const Tensor &rhs) {
Tensor result(op.getType());
for (auto it = result.index_begin(); it != result.index_end(); ++it)
result.set(*it, lhs.get(*it) + rhs.get(*it));
return result;
}
Nhìn chung, thiết kế của trình phiên dịch được tối ưu hoá để giúp dễ đọc
việc triển khai các hàm eval
cho từng hoạt động riêng lẻ vì điều này nhằm mục đích
đóng vai trò là cách triển khai tham chiếu cho StableHLO. Ví dụ: thay vì
xác định eval
làm hàm mẫu và tham số cho hàm đó bằng các loại phần tử,
chúng ta gói gọn chi tiết về cách xử lý các loại phần tử khác nhau
Element::operator+
, v.v., đơn giản hoá việc triển khai eval
.
Sử dụng trình phiên dịch cho thao tác gập liên tục
Chúng ta có thể sử dụng cơ chế thông dịch để gấp các thao tác có toán hạng không đổi
giá trị. Đoạn mã sau đây minh hoạ ý tưởng triển khai
để gập stablehlo::AddOp
bằng các toán hạng được nhập dưới dạng dấu phẩy động:
OpFoldResult AddOp::fold(FoldAdaptor adaptor) {
auto attrs = adaptor.getOperands();
DenseElementsAttr lhsData = dyn_cast<DenseElementsAttr>(attrs[0]);
DenseElementsAttr rhsData = dyn_cast<DenseElementsAttr>(attrs[1]);
if (!lhsData || !rhsData) return {};
auto lhs = Tensor(lhsData);
auto rhs = Tensor(rhsData);
auto result = eval(*this, lhs, rhs);
SmallVector<APFloat> values;
for (auto i = 0; i < result.getNumElements(); ++i) {
Element element = result.get(i);
values.push_back(cast<FloatAttr>(element.getValue()).getValue());
}
return DenseElementsAttr::get(result.getType(), values);
}
Hiện tại, chúng tôi chưa tích cực nỗ lực tích hợp phiên dịch vào
gập liên tục vì chúng tôi không có kế hoạch triển khai thư mục cho StableHLO.
Tuy nhiên, trong tương lai, chúng tôi dự định tận dụng phiên dịch để
thu gọn trong MHLO, tại thời điểm đó, chúng tôi sẽ cải thiện tính tiện dụng của đoạn mã
ở trên (ví dụ: chúng ta có thể có một hàm trợ giúp gói các toán hạng hằng vào
Đối tượng Tensor
và giải nén kết quả Tensor
vào OpFoldResult
).
Kiểm thử trình phiên dịch StableHLO
Trình phiên dịch lấy đầu vào (A) chương trình ổn định và (B) giá trị dữ liệu để
được cung cấp cho chương trình và tạo ra các giá trị dữ liệu đầu ra phù hợp
dựa trên giá trị dữ liệu dự kiến do người dùng cung cấp. Giá trị dữ liệu (B) là
được cố định giá trị trong mã vào chính chương trình bằng cách sử dụng các thao tác stablehlo.constant
. Chiến lược phát hành đĩa đơn
trình thông dịch sẽ đánh giá chương trình đầu vào. (Các) đầu ra của hoạt động đang được kiểm thử
được kiểm tra thông qua kiểm tra (ví dụ: check.expect_eq
, check.expect_almost_eq
), như
như bên dưới. check.expect_eq
và check.expect_eq_const
kiểm tra bitwise
bằng nhau cho mọi loại được hỗ trợ, cũng như check.expect_almost_eq
và
check.expect_almost_eq_const
kiểm tra độ gần bằng nhau trong phạm vi dung sai,
đã giải thích trong hướng dẫn kiểm tra (G6), cho dấu phẩy động và các kiểu phức tạp.
// CHECK-LABEL: Evaluated results of function: add_op_test_ui4
func.func @add_op_test_ui4() {
%0 = stablehlo.constant dense<[0, 2]> : tensor<2xui4>
%1 = stablehlo.constant dense<[15, 3]> : tensor<2xui4>
%2 = stablehlo.add %0, %1 : tensor<2xui4>
check.expect_eq_const %2, [15, 5] : tensor<2xui4>
func.return
}
Một tiện ích kiểm thử stablehlo-translate --interpret
(mã)
chịu trách nhiệm phân tích cú pháp chương trình, diễn giải từng hàm, bao gồm
các toán tử tạo nên hàm đó. Chúng tôi có một bộ kiểm thử chuyên biệt, bao gồm
của một số chương trình kiểm thử thực hiện các hành vi khác nhau trong thời gian chạy, cho từng hoạt động ổn định HLO.
Bạn có thể xem các bài kiểm thử tại đây.
Nguyên tắc kiểm thử
(G1) Chúng tôi có cần thử nghiệm tất cả các loại được hỗ trợ cho mọi hoạt động không?
Chúng ta có thể sử dụng kết hợp các quy tắc sau để quyết định:
Trong khi triển khai hoạt động, nếu có mã trong
eval
tương ứng để xử lý một loại cụ thể, thì bắt buộc phải có(các) bài kiểm thử để đề cập đến loại đó. Ví dụ: đối với hoạt độngadd
, có mã độc quyền để xử lý các kiểu số nguyên, boolean, dấu phẩy động và phức tạp. Do đó, chúng tôi cần một bài kiểm thử cho mỗi loại.Nếu một tập hợp các kiểu được xử lý thống nhất trong hàm
eval
tương ứng, thì bạn chỉ cần một bài kiểm thử duy nhất cho tất cả các loại đó là đủ. Ví dụ: cho hoạt độngadd
, tất cả biến thể của kiểu số nguyên (si4
,u4
,si8
,u8
v.v.) được xử lý tương tự bằng các APIllvm::APInt
. Do đó, chúng ta có thể bỏ qua thêm các thử nghiệm cho từng biến thể đó và thay vào đó thêm một thử nghiệm thử nghiệm đại diện. Để tránh chọn ra người đại diện không rõ ràng, chúng tôi nên sử dụng các nguyên tắc sau:- Nếu tất cả các loại (được xử lý thống nhất) sẽ có cùng một loại dữ liệu gốc (nghĩa là nếu tất cả đều là kiểu số nguyên, dấu phẩy động hoặc phức tạp), thì hãy chọn tệp có độ rộng bit tối đa.
- Nếu tất cả các loại, được xử lý đồng nhất, có kết hợp các loại nguyên gốc, thì hãy chọn kiểu dữ liệu có kiểu dữ liệu cơ bản sau, theo thứ tự giảm dần là lựa chọn ưu tiên: số nguyên, dấu phẩy động, boolean, phức.
(G2) Làm thế nào để chúng tôi quyết định số lượng thử nghiệm cần thiết cho một hoạt động không?
Mục tiêu là bao hàm toàn diện logic của người phiên dịch cho hoạt động (tức là tất cả các trường hợp góc của quá trình triển khai) với số lượng kiểm thử tối thiểu. Việc giảm thiểu số lượng bài kiểm thử đóng vai trò quan trọng giúp đảm bảo khả năng bảo trì. Càng ít thử nghiệm chúng tôi sẽ càng dễ dàng xem xét và đảm bảo rằng bao gồm toàn diện hoạt động kinh doanh. Do đó, chúng tôi hy vọng rằng hầu hết các sự kiện hoạt động sẽ chỉ có một lần kiểm thử. Nếu vì lý do nào đó đầy đủ là không thực tế, thì sẽ có thể dừng ở mức >= 90%. Điều này sẽ được quyết định trên cơ sở từng trường hợp trong quá trình xem xét yêu cầu lấy dữ liệu.
(G3) Thêm bài kiểm thử cho cơ sở hạ tầng dành cho phiên dịch viên thì sao?
Cơ sở hạ tầng phiên dịch hầu như rất đơn giản và có thể được thêm vào
cơ sở tin cậy của chúng tôi. Phần không quan trọng duy nhất là cách nhiều loại được đóng gói vào
và giải nén khỏi bộ nhớ cơ bản của trình phiên dịch. Như đã thảo luận trong (G1), chúng tôi
sẽ chỉ thử nghiệm những loại hoạt động được xử lý khác nhau. Bằng
có thể là mã đóng gói/huỷ đóng gói, tương ứng với
biến thể của các loại số nguyên/dấu phẩy động, có thể không được đề cập đầy đủ trong
kiểm thử. Để đảm bảo thông tin toàn cảnh, chúng ta có thể chọn một hoạt động như constant
hỗ trợ mọi loại phần tử ổn định HLO và viết chương trình kiểm thử đầy đủ.
(G4) Nếu việc triển khai một hoạt động phụ thuộc vào các hoạt động khác, thì chúng ta có nên viết không?
Không. Ví dụ: việc triển khai batch_norm_grad
có thể dựa trên
divide
, subtract
, multiply
và các nguồn khác. Chúng ta nên tránh thử nghiệm các
hoạt động trong khi kiểm thử phiên bản cũ.
(G5) Chúng ta có nên viết mã kiểm thử để thực hiện quy trình triển khai được xác định / không xác định không?
Chúng ta không nên viết chương trình kiểm thử nhằm thực thi hoạt động triển khai được xác định hoặc hành vi không xác định của cơ chế vận hành. Các chương trình kiểm thử thực hiện hành vi do phương thức triển khai xác định thể hiện một hành vi cục bộ của trình thông dịch mà không nên đã khái quát hoá. Các bài kiểm tra thực hiện hành vi không xác định không đóng góp vào việc sự hiểu biết về hành vi của vận hành.
(G6) Trong khi viết mã kiểm thử cho các loại dấu phẩy động, đối với độ chính xác kết quả dự kiến có cần được chỉ định trong bước kiểm tra không?
Đối với các phép tính cơ bản (cộng, trừ, nhân, chia và
vuông), việc triển khai theo thông số kỹ thuật của IEEE dự kiến sẽ cung cấp
kết quả được làm tròn trong vòng 0,5 ULP của kết quả chính xác về mặt toán học. Tuy nhiên, chúng tôi
có thể hình dung một cách an toàn kết quả mong đợi từ các hoạt động này
chênh lệch tối đa 1 ULP. Tuy nhiên, điều này có thể không hoạt động với các chức năng vượt trội
(sine
, cosine
, v.v.) có đảm bảo độ chính xác
do triển khai xác định (lý do).
Cách triển khai hiện tại sử dụng chiến lược "một kích thước phù hợp cho tất cả" giá trị dung sai 0,0001. Ví dụ sau đây minh hoạ trong thực tế về dung sai nêu trên.
func.func @check_tolerance() {
%0 = stablehlo.constant dense<0.2> : tensor<f32>
// The following check succeeds as %0 is almost equal to the provided
// constant modulo the tolerance, mentioned above.
check.expect_almost_eq_const %0, dense<0.19999> : tensor<f32>
// The following check fails as %0 is not bitwise equal to the provided
// constant.
check.expect_eq_const %0, dense<0.19999> : tensor<f32>
func.return
}
Đây chỉ là bước đầu tiên trong quy trình kiểm thử độ chính xác về mặt số học của hoạt động ổn định. Hiện tại, đây là phần chưa được xác định rõ của thông số StableHLO và có nỗ lực không ngừng để tìm ra điều đó #1156 dựa trên kinh nghiệm của chúng tôi khi sử dụng StableHLO trong thực tế và dựa trên ý kiến phản hồi từ các bên liên quan khác. Trong quá trình này, chúng tôi sẽ cập nhật cơ sở hạ tầng cho phù hợp.
(G7) Có ý kiến gì về phong cách lập trình của kiểm thử không?
- Hãy đảm bảo sử dụng tên thực tế của đầu vào/đầu ra thay vì đặt mặc định thành giá trị SSA (ví dụ: %0, %1, v.v.)
- Đảm bảo kiểm thử sử dụng định dạng được in đẹp (nếu có).
(G8) Có nên đưa ví dụ đã có vào quy cách không? Có (để hoàn tất thử nghiệm).