Çevirmen Tasarımı

Veri Modeli

StableHLO programları, geçerli modelde Tensor sınıfı kullanılarak uygulanan tensörler (n boyutlu diziler) üzerinde yapılan hesaplamalardır. Bir Tensor nesnesinin (detail::Buffer) temel depolama sınıfı, tensörün mlir::ShapedType değerini anadan küçüke doğru şekilde bitişik bayt dizisi olarak yerleştirilmiş tensör verileri blobunu temsil eden bir mlir::HeapAsmResourceBlob nesnesiyle birlikte depolar. Bellek yönetimini basitleştirmek için detail::Buffer nesne referans sayılır.

Bir tensörün bağımsız öğeleri, Element sınıfı kullanılarak temsil edilir. Bu sınıf, depolama için APInt, APFloat veya pair<APFloat,APFloat> özelliklerinden birini barındıran ayrımlı bir birleşimi kullanır. Sonuncusu ise karmaşık türlere sahip öğeleri depolamak için kullanılır.

Tensor, bağımsız öğeleriyle etkileşime geçmek için aşağıdaki API'lere sahiptir:

  • Element Tensor::get(llvm::ArrayRef<int64_t> index): Çok boyutlu dizinde index Element nesnesi olarak tek bir tensör öğesini ayıklamak için kullanılır.
  • void Tensor::set(llvm::ArrayRef<int64_t> index, Element element);: element Element nesnesini çok boyutlu dizinde index tensöre güncellemek için.

Çevirmen nasıl çalışır?

Çevirmene giriş işlevi

SmallVector<Tensor> eval(func::FuncOp func, ArrayRef<Tensor> args);

İş Listesi API'sı şunları yapar:

  1. args içinde sağlanan func SSA bağımsız değişkenlerini ve ilişkili çalışma zamanı Tensor değerlerini, M sembolü tablosu kullanarak izler.
  2. func içindeki her işlem için SSACFG sırasına göre:
    • İşlemde eval yöntemini çağırır. İşlemin her SSA işleneni için eval çağrısına bağımsız değişken olarak sağlamak üzere M'den çalışma zamanı değerini ayıklar.
    • İşlemin SSA sonuçlarını ve M'de değerlendirilen değeri izler.

(2) numaralı maddede bahsedilen işlem seviyesi eval, operasyonun yürütme anlamlarını uygulamaktan sorumludur. stablehlo::AddOp için bir örnek aşağıda verilmiştir. Bu örnekte, lhs ve rhs tensörlerinin ayrı ayrı öğeleri ikili olarak Element nesneleri olarak çıkartılır ve daha sonra bu öğeler eklenir. Ekleme işleminin sonucu olan Element nesnesi, son result tensöründe depolanır.

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;
}

StableHLO için bir referans uygulaması olarak sunulması amaçlandığından, çevirmenin tasarımı genel olarak bağımsız işlemler için eval işlevlerinin uygulamalarının okunabilirliği açısından optimize edilmiştir. Örneğin, eval öğesini şablon işlevi olarak tanımlamak ve öğe türleriyle parametreleştirmek yerine, Element::operator+ vb. öğelerde farklı öğe türlerinin nasıl işlendiğine dair ayrıntıları dahil ederiz. Böylece eval uygulamasının uygulanması kolaylaşır.

Sabit katlama için çevirmeni kullanma

İşlemleri sabit işlenen değerleriyle katlamak için çevirmen mekanizmasını kullanabiliriz. Aşağıdaki kod snippet'i, stablehlo::AddOp kayan nokta türü işlenenlerle katlama uygulama fikrini göstermektedir:

OpFoldResult AddOp::fold(FoldAdaptor adaptor) {
  auto attrs = adaptor.getOperands();
  DenseElementsAttr lhsData = attrs[0].dyn_cast<DenseElementsAttr>();
  DenseElementsAttr rhsData = attrs[1].dyn_cast<DenseElementsAttr>();
  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(element.getValue().cast<FloatAttr>().getValue());
  }

  return DenseElementsAttr::get(result.getType(), values);
}

Şu anda StableHLO için klasör uygulamayı planlamadığımızdan çevirmeni sürekli katlamaya entegre etme konusunda etkin bir şekilde çalışmıyoruz. Bununla birlikte, gelecekte MHLO'da sabit katlama için çevirmenden yararlanmayı planlıyoruz.Bu noktada yukarıdaki kod snippet'inin ergonomisini iyileştireceğiz (ör. sabit işlenenleri Tensor nesnelerine paketleyen ve Tensor sonuçlarını OpFoldResult elde eden bir yardımcı işlevimiz olabilir).

StableHLO yorumlayıcısını test etme

Çevirmen, girdi (A) bir StableHLO programı ve (B) programa aktarılacak veri değerlerini alır ve kullanıcı tarafından sağlanan beklenen veri değerleriyle eşleşen çıkış veri değerleri oluşturur. Veri değerleri (B), stablehlo.constant işlemleri kullanılarak programın kendisinde sabit kodlanır. Yorumlayıcı giriş programını değerlendirir. Test edilen operasyonun çıkışları, aşağıda gösterildiği gibi kontrollerle (ör. check.expect_eq, check.expect_almost_eq) kontrol edilir. check.expect_eq ve check.expect_eq_const, desteklenen tüm türler için bit düzeyinde eşitliği kontrol eder, check.expect_almost_eq ve check.expect_almost_eq_const ise kayan nokta ve karmaşık türler için test kılavuzunda (G6) açıklanan bir tolerans dahilinde neredeyse eşitliği kontrol eder.

// 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
}

Test yardımcı programı stablehlo-translate --interpret (kod), programı ayrıştırıp işlevi oluşturan işlemler dahil olmak üzere her bir işlevi yorumlamaktan sorumludur. Her StableHLO Op için çeşitli çalışma zamanı davranışlarını uygulayan çeşitli testlerden oluşan özel bir test paketimiz vardır. Testleri burada (ör. comment_*.mlir) bulabilirsiniz.

Test yönergeleri

(G1) Her işlem için desteklenen tüm türleri test etmemiz gerekir mi?

Karar vermek için aşağıdaki kuralların bir kombinasyonunu kullanabiliriz:

  1. Bir işlemi uygularken, ilgili eval işlevinde belirli bir türü işlemek için gerekli kod varsa bu türü kapsayan testlerin olması zorunludur. Örneğin, add işlemi için tam sayı, boole, kayan nokta ve karmaşık türleri işlemek için özel bir kod vardır. Bu nedenle, her tür kategorisi için bir teste ihtiyacımız vardır.

  2. Bir tür kümesi, karşılık gelen eval işlevinde eşit şekilde işlenirse bu türlerin tümü için tek bir test yeterli olacaktır. Örneğin, add işlemi için tam sayı türlerinin tüm varyantları (si4, u4, si8, u8 vb.) llvm::APInt API'leri kullanılarak aynı şekilde işlenir. Bu nedenle, bu varyantların her biri için test eklemeyi atlayıp bunun yerine tek bir temsili test ekleyebiliriz. Temsilci seçiminde belirsizliği önlemek için aşağıdaki yönergeleri kullanmalıyız:

    • Eşit olarak işlenen tüm türler aynı temel türe sahipse (yani tümü tam sayı, kayan nokta veya karmaşık türdeyse) maksimum bit genişliğine sahip olanı seçin.
    • Eşit şekilde işlenen tüm türler basit türde bir karmaya sahipse daha sonra tercih sırasına göre azalan düzende şu temel türe sahip olanı seçin: tam sayı, kayan nokta, boole, karmaşık.

(G2) Bir operatörün davranışını kapsamak için gereken test sayısına nasıl karar veririz?

Hedef, minimum sayıda testle, çevirmenin mantığını (yani uygulamanın tüm köşeleri) kapsamlı bir şekilde ele almaktır. Test sayısını en aza indirmek sürdürülebilirlik açısından önemlidir. Ne kadar az test olursa bunları incelemek ve işlemi kapsamlı bir şekilde kapsamalarını sağlamak o kadar kolay olur. Sonuç olarak, daha basit işlemlerin çoğunun yalnızca bir testle sonuçlanmasını bekleriz. Herhangi bir nedenle kapsamlı kapsam pratik değilse, >= %90'da durmak sorun olmaz. Bu, çekme isteğinin incelenmesi sırasında her bir durum için ayrı ayrı belirlenecektir.

(G3) Çevirmen altyapısı için test eklemeye ne dersiniz?

Çevirmen altyapısı çoğunlukla basittir ve güven tabanımıza eklenebilir. Önemsiz olmayan tek kısım, çeşitli türlerin temel çevirmen depolama alanına nasıl yerleştirildiği ve paketten çıkarılma şeklidir. (G1) bölümünde açıklandığı gibi, yalnızca farklı işlenen işlem türlerini test edeceğiz. Bu nedenle, tam sayı/kayan nokta türlerinin farklı varyantlarına karşılık gelen paketleme/ambalaj açma kodunun test sırasında tam olarak kapsama alınmaması mümkündür. Tam kapsam sağlamak için tüm StableHLO öğe türlerini destekleyen constant gibi bir işlem seçebilir ve kapsamlı testler yazabiliriz.

(G4) Bir işlemin uygulanması başka işlemlere bağlıysa ikincisi için testler yazmalı mıyız?

Hayır. Örneğin, batch_norm_grad uygulaması divide, subtract, multiply ve diğerlerine dayalı olabilir. İkinci işlemleri test ederken, ilkini test etmekten kaçınmalıyız.

(G5) Uygulama tanımlı / tanımlanmamış davranışları uygulamak için testler yazmalı mıyız?

İşlemin uygulama tanımlı veya tanımlanmamış davranışlarını uygulayan testler yazmamalıyız. Uygulama tanımlı davranışların kullanıldığı testler, çevirmenin genelleştirilmemesi gereken yerel bir davranışını gösterir. Tanımlanmamış davranışlar içeren testler, operasyonun davranışının anlaşılmasına katkıda bulunmaz.

(G6) Kayan nokta türleri için test yazarken, kontrollerde beklenen sonucun hangi kesinlikte belirtilmesi gerekir?

Temel işlemlerde (toplama, çıkarma, çarpma, bölme ve kare) IEEE spesifikasyonunu izleyen bir uygulamanın, matematiksel olarak tam sonuca göre 0,5 ULP dahilinde yuvarlanmış bir sonuç sağlaması beklenir. Bununla birlikte, bu işlemlerden çıkarılması beklenen sonucun birbirinden en fazla 1 ULP olacağını güvenli bir şekilde düşünebiliriz. Ancak bu, kesinlik garantilerinin uygulama tanımlı olduğu (mantık) ötesi işlevlerde (sine, cosine vb.) işe yaramayabilir.

Mevcut uygulama, 0,0001 olan "herkese uygun tek boyut" tolerans değerini kullanmaktadır. Aşağıdaki örnekte yukarıdaki toleransın işleyiş şekli gösterilmektedir.

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
}

Bu, StableHLO işlemlerinin sayısal doğruluğunu test etmenin ilk adımıdır. Şu anda bu, StableHLO spesifikasyonunun yeterince tanımlanmamış bir alanıdır ve uygulamada StableHLO'yu kullanma deneyimimize ve paydaşların geri bildirimlerine dayanarak bunu belirlemek için çalışmalarımız devam etmektedir. #1156 Bu süreç devam ettikçe altyapıyı da uygun şekilde güncelleyeceğiz.

(G7) Testlerin kodlama stiliyle ilgili bir şey var mı?

  1. Varsayılan olarak SSA değerlerini (ör. %0, %1 vb.) kullanmak yerine giriş/çıkışların gerçek adını kullandığınızdan emin olun
  2. Testlerin, oldukça yazdırılmış bir biçim (mevcutsa) kullandığından emin olun.

(G8) Spesifikasyonda verilen örneği dahil etmeli miyiz? Evet (testlerin eksiksiz olması için).