Thiết kế thông dịch viên

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ều index dưới dạng Element .
  • void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);: Để cập nhật đối tượng Element element thành tensor ở đa chiều chỉ mục index.

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:

  1. Theo dõi các đối số SSA của func và thời gian chạy Tensor liên quan của các đối số đó các giá trị, được cung cấp trong args, sử dụng bản đồ bảng biểu tượng, M.
  2. Đố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ọi eval.
    • Theo dõi(các) kết quả SSA của hoạt động và giá trị được đánh giá theo M.

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 lhsrhs 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_eqcheck.expect_eq_const kiểm tra bitwise bằng nhau cho mọi loại được hỗ trợ, cũng như check.expect_almost_eqcheck.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 () 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:

  1. 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 động add, 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.

  2. 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 động add, 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 API llvm::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?

  1. 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.)
  2. Đả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).