Shtojcat e Kompilatorit LiteRT

Kur duhet të krijoj një Plugin Kompiluesi?

Një Plugin Kompiluesi LiteRT është i nevojshëm kur duhet të integroni një përshpejtues specifik të harduerit me një varësi të kompiluesit në kuadrin LiteRT.

Duhet të krijoni një plugin për kompilues nëse:

  1. Po synoni një backend të ri hardueri që nuk mbështetet.
  2. Ju dëshironi të shkarkoni operacione specifike të modelit në atë përshpejtues hardueri për performancë ose efikasitet të energjisë.
  3. Ju nevojitet mbështetje për kompilimin AOT (në stacionin e punës) ose për kompilimin në pajisje.

Shtojca vepron si një urë, duke marrë pjesë të modelit të të mësuarit automatik dhe duke i konvertuar ato në një format që hardueri juaj i synuar mund ta ekzekutojë, duke përdorur një thirrje në kompiluesin e backend-it. LiteRT bashkon bytekodin e personalizuar të gjeneruar nga shtojca në modelin .tflite , duke e bërë atë të ekzekutueshëm duke përdorur kohën e ekzekutimit të LiteRT.

Si funksionojnë shtojcat e kompilatorit?

Korniza LiteRT përdor plugin-in e kompajlerit gjatë ngarkimit të modelit ose fazës së para-përpunimit jashtë linje për të identifikuar dhe përgatitur nëngrafe të modelit për ekzekutim në harduerin e synuar.

Procesi përfshin dy faza kryesore të orkestruara nga framework-u duke përdorur funksionet e eksportuara të plugin-it:

  1. Ndarja: Shtojca inspekton të gjithë grafikun e modelit dhe identifikon nëngrupe operacionesh që mbështet dhe mund të përshpejtohen në mënyrë efikase në harduerin e synuar. Këta nëngrupe të mbështetur "ndahen" (shënohen) për kompilim dhe përvijohen.
  2. Kompilimi: Korniza LiteRT ia kalon nëngrafët e ndarë përsëri plugin-it. Pastaj plugin-i përdor logjikën e tij të brendshme dhe ndoshta zinxhirët e mjeteve të jashtme (kompiluesit) për të gjeneruar një ose më shumë module bytecode specifike për harduerin që zbatojnë ndarjet. Ky bytecode është ajo që koha e ekzekutimit të harduerit të synuar (HAL/driver) do të ngarkojë dhe ekzekutojë përfundimisht.

Korniza zëvendëson nëngrafet origjinale me operacione të personalizuara që thirrin drajverin e harduerit, duke kaluar përgjatë bajtkodit të kompiluar të krijuar nga shtojca.

LiteRT Dispatch është shtojca analoge e përpiluesit në kohën e ekzekutimit. Ato ofrojnë mjetet për të thirrur daljen e dhënë të përpiluesit HAL. Për më shumë detaje, referojuni dokumentacionit të dispatch-it .

AOT kundrejt On-Device

LiteRT mund të përdorë shtojcat e kompiluesit për të mbështetur kompilimin AOT përmes mjeteve tona, si dhe kompilimin në pajisje. Kompilimi në pajisje është më fleksibël, plotësisht i integruar brenda API-ve të kohës së ekzekutimit të LiteRT dhe kërkon vetëm menaxhimin e një modeli të vetëm. Fluksi i AOT mund ta zhbllokojë kompilimin kur është shumë intensiv në burime për t'u ekzekutuar në pajisje, gjë që mund të jetë rasti me shumë modele të mëdha bashkëkohore.

Rezervë

LiteRT është ndërtuar me mbështetje për grafikë heterogjenë. Çdo operacion që nuk zgjidhet nga shtojca do t'i lihet CPU-së ose do të vihet në dispozicion për përshpejtim në një backend tjetër.

Implementimi i një Plugin-i përpiluesi

Një plugin përpiluesi LiteRT zbatohet si një bibliotekë e përbashkët që eksporton një grup specifik funksionesh C të përcaktuara në API-n LiteRT C.

Funksionet thelbësore të ndërfaqes

Funksionaliteti kryesor sillet rreth dy hapave kryesorë të kompajlimit: LiteRtCompilerPluginPartition dhe LiteRtCompilerPluginCompile .

Funksioni Qëllimi
LiteRtCompilerPluginPartition Zgjedh dhe shënon të gjitha operacionet e mbështetura brenda një nëngrafi të modelit të caktuar (hapi i Ndarjes ).
LiteRtCompilerPluginCompile$ Gjeneron bajtkodin specifik të harduerit për ndarjet e parazgjedhura (hapi i kompilimit ).

Fragmente të API-t C

// Name associated with the manufacturer this plugin relates to.
LITERT_CAPI_EXPORT const char* LiteRtGetCompilerPluginSocManufacturer();

// Create and initialize the plugin instance.
LITERT_CAPI_EXPORT LiteRtStatus
LiteRtCreateCompilerPlugin(LiteRtCompilerPlugin* compiler_plugin,
                           LiteRtEnvironmentOptions env, LiteRtOptions options);

// Choose ops for compilation.
// This is the PARTITION step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtSubgraph subgraph, LiteRtOpList selected_ops);

// Prepare result to pass to the runtime for given model containing partitioned
// subgraphs. This is the COMPILE step.
LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtModel partitions, LiteRtCompiledResult* compiled_result);

1. Funksioni i Ndarjes

Nënshkrimi i funksionit është:

LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginPartition(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtSubgraph subgraph, LiteRtOpList selected_ops);

Çfarë bën funksioni i partition : Kjo është faza e përzgjedhjes . Shtojca përsërit operacionet në hyrjen LiteRtSubgraph . Për çdo operacion që mbështet dhe mund të përshpejtojë hardueri i synuar, shtojca e shton atë operacion në LiteRtOpList$ të dhënë në parametrin selected_ops . Korniza LiteRt përdor këtë listë për të përcaktuar kufijtë e ndarjeve që do të dërgohen për hapin përfundimtar të kompajlimit.

Si parazgjedhje, LiteRT do t'i grupojë të gjitha operacionet e zgjedhura në nën-DAG-et më të mëdha të mundshme. Për një ndarje më të detajuar, mund të shoqërohet një indeks gjatë përzgjedhjes së operacioneve i cili shërben për të ndarë më tej këto nëngrafe.

2. Funksioni i Kompilimit

Nënshkrimi i funksionit është:

LITERT_CAPI_EXPORT LiteRtStatus LiteRtCompilerPluginCompile(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtModel partitions, LiteRtCompiledResult* compiled_result);

Çfarë bën funksioni i compile : Kjo është faza e gjenerimit . partitions hyrëse përfaqësojnë një model ku të gjithë nëngrafet e zgjedhura janë izoluar. Shtojca përpunon këto ndarje, duke thirrur zinxhirin e saj specifik të mjeteve për të gjeneruar kodin e bajtit për harduerin e synuar. Pritet që dalja e shtojcës të ofrojë një pikë hyrjeje për secilin nëngraf të kaluar për kompilim. Në shumicën e rasteve, këto janë ose module individuale të kodit të bajtit për secilin nëngraf të hyrjes, ose një modul i vetëm kodi të bajtit me pika të shumëfishta hyrjeje.

Lloji i të dhënave të kthyera nga compile : Funksioni LiteRtCompilerPluginCompile kthen rezultatin e tij duke përdorur parametrin jashtë LiteRtCompiledResult .

LiteRtCompiledResult është një dorezë e errët (në lidhje me LiteRT) për një strukturë të menaxhuar nga plugin-i. Ai përfaqëson rezultatin e kompilimit dhe përmban dy informacione kryesore:

  1. Modulet e Kodit të Bajtit: Një ose më shumë buffer-a të memories së papërpunuar që përmbajnë bytekodin e ekzekutueshëm specifik të harduerit (p.sh., udhëzimet e kompiluara).
  2. Informacion për Thirrjen: Meta të dhëna për secilën ndarje. Kjo siguron përputhjen nga nëngrafi i -të i hyrjes në një modul kodi bajt rezultati dhe identifikues të pikës së hyrjes në atë modul.

Shembull i Implementimit

Fragmentet e mëposhtme ilustrojnë se si një plugin bazë mund të zbatojë funksionet kryesore. Ky shembull është marrë nga një shembull plotësisht funksional në litert/vendors/examples/

Identifikimi dhe Konfigurimi i Plugin-it

Këto funksione i japin kornizës informacion bazë rreth plugin-it dhe harduerit.

// Define the plugin's internal state structure
struct LiteRtCompilerPluginT {};

// Identify the manufacturer
const char* LiteRtGetCompilerPluginSocManufacturer() {
  return "AcmeCorp"; // Example manufacturer name
}

// Specify the supported hardware (in this example, it supports kLiteRtHwAcceleratorNpu)
LiteRtStatus LiteRtGetCompilerPluginSupportedHardware(
    LiteRtCompilerPlugin compiler_plugin,
    LiteRtHwAccelerators* supported_hardware) {
  // ... argument checking ...
  *supported_hardware = kLiteRtHwAcceleratorNpu;
  return kLiteRtStatusOk;
}

Logjika e Ndarjes ( LiteRtCompilerPluginPartition )

Ky shembull tregon shtojcën që zgjedh një grup të kufizuar operacionesh ( mul , sub dhe një operacion specifik të përbërë) vetëm nëse të gjitha hyrjet dhe daljet janë numra me gjatësi notuese 32-bit. Zakonisht, përcaktimi nëse një operacion duhet të zgjidhet apo jo do të përfshijë një thirrje për një grep validimi në zinxhirin e mjeteve të kompajlerit të backend-it.

LiteRtStatus LiteRtCompilerPluginPartition(LiteRtCompilerPlugin compiler_plugin,
                                          const char* soc_model,
                                          LiteRtSubgraph subgraph,
                                          LiteRtOpList selected_ops) {

  // Iterate over ops and check criteria for selection
  // (using a C++ wrapper namespace '::litert' for convenience).
  // `subgraph` is a single subgraph from the original model, as such
  // this function will be called for each subgraph in the original model.

  ::litert::Subgraph main_subgraph(subgraph);
  for (const auto& op : main_subgraph.Ops()) {
    // 1. Check a constraint: require all tensors to be Float32
    bool only_f32 = true;
    // ... logic to check input/output types ...
    if (!only_f32) {
      continue;
    }

    // 2. Check op codes and push to selected_ops list
    if (op.Code() == kLiteRtOpCodeTflMul) {
      LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
    } else if (op.Code() == kLiteRtOpCodeTflSub) {
      LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
    } else if (op.Code() == kLiteRtOpCodeShloComposite) {
      // Example of checking composite op options
      // ... logic to check for "odml.rms_norm" name ...
      LITERT_RETURN_IF_ERROR(LiteRtPushOp(selected_ops, op.Get(), 0));
    }
  }
  return kLiteRtStatusOk;
}

Përpara se të thërrasë kompilimin, LiteRT do të validojë dhe "përvijë" të gjitha operacionet e zgjedhura në nëngrafe të reja në një model të ri të ndërmjetëm. Ky model i ndërmjetëm është ai që i kalohet kompilimit.

Logjika e Kompilimit ( LiteRtCompilerPluginCompile )

Ky funksion merr nëngrafet e ndarë dhe gjeneron një LiteRtCompiledResult të personalizuar. Ky shembull gjeneron një modul bajtokodi të pavarur për secilën ndarje që do të kompilohet. Në raste reale, kjo zakonisht përfshin konvertimin e operacioneve LiteRT në lloje në bibliotekën e kompiluesit të backend. "Kompilimi" i shtojcës së shembullit funksional krijon një varg të lexueshëm nga njeriu që kodon grafikun.

// Internal structure defining the compiled output
struct LiteRtCompiledResultT {
  std::vector<std::string> byte_code;   // The hardware bytecode buffers
  std::vector<std::string> per_op_data; // Per-call metadata (CallInfo)
};

LiteRtStatus LiteRtCompilerPluginCompile(
    LiteRtCompilerPlugin compiler_plugin, const char* soc_model,
    LiteRtModel partitions, LiteRtCompiledResult* compiled_result) {

  // 1. Create the internal result structure
  auto model = litert::Model::CreateFromNonOwnedHandle(partitions);
  const auto num_partitions = model.NumSubgraphs();
  auto result = std::make_unique<LiteRtCompiledResultT>();
  result->byte_code.resize(num_partitions);
  result->per_op_data.resize(num_partitions);

  // 2. Iterate and compile each partition
  for (auto i = 0; i < num_partitions; ++i) {
    // CompileSinglePartition is an internal helper that converts the subgraph
    // into the target hardware's format and stores it in result->byte_code.
    // In the case of the example this is just a stringification of the graph.

    // ... internal call to CompileSinglePartition ...
    // Example: result.byte_code[i] = generated_hw_code;
    // Example: result.per_op_data[i] = absl::StrFormat("Partition_%d", i);

    // The "per_op_data" is a unique identifier associated to the `ith` partition.
    // This is analogous to the name of a function in a library.
    // This is only meaningful when the plugin is preparing single modules with multiple entry points.
  }

  // 3. Pass ownership of the result back to the framework
  *compiled_result = result.release();

  return kLiteRtStatusOk;
}

// Functions to expose the compiled result data to the framework
LiteRtStatus LiteRtGetCompiledResultByteCode(
    LiteRtCompiledResult compiled_result, LiteRtParamIndex byte_code_idx,
    const void** byte_code, size_t* byte_code_size) {
  // ... implementation reads from compiled_result->byte_code ...
}
// ... other LiteRtGetCompiledResult* functions ...

Përdorimi dhe Validimi

LiteRT ofron mjete të ndryshme për aplikimin e shtojcave të kompajlerit në modelimin e skedarëve, ekzekutimin e rezultatit dhe validimin/krahasimin. Referojuni dokumentacionit të paketës së testimit të përshpejtuesit dhe dokumentacionit të krahasimit dhe profilizimit .