XLA-তে #ifdefs কমানোর জন্য হিচহাইকারের গাইড

সংক্ষেপে: #ifdef ট্যাগগুলো মেইনটেইনেবিলিটির জন্য ক্ষতিকর। অনুগ্রহ করে এগুলো এড়িয়ে চলার চেষ্টা করুন। কীভাবে তা করবেন, সে সম্পর্কে বিস্তারিত জানতে নিচে স্ক্রোল করুন।

ভূমিকা

প্রিপ্রসেসর কন্ডিশনাল ( #if , #ifdef , #ifndef ইত্যাদি) হলো বিদ্যমান কোডকে তার মূল উদ্দেশ্য থেকে ভিন্ন কোনো পরিবেশে কাজ করানোর একটি সুবিধাজনক উপায়। উদাহরণস্বরূপ:

  • বিদ্যমান CUDA-ভিত্তিক কোডকে ROCm বা SYCL পরিবেশে কার্যকর করা।
  • পুরানো সংস্করণগুলির জন্য সমর্থন বজায় রেখে নতুন লাইব্রেরি বৈশিষ্ট্য যুক্ত করা (উদাহরণস্বরূপ, cuDNN বা hipDNN-এর নতুন কোনো রিলিজ থেকে)।

তবে, এগুলোর রক্ষণাবেক্ষণ খরচ অনেক বেশি (পরবর্তী বিভাগে আরও বিস্তারিত), বিশেষ করে বিদ্যমান কোড রিফ্যাক্টরিং করার সময়।

এই নির্দেশিকাটি প্রিপ্রসেসর কন্ডিশনালের অসুবিধাগুলো ব্যাখ্যা করে এবং সবচেয়ে প্রচলিত ব্যবহারগুলোর জন্য একটি কুকবুক শৈলীতে বিকল্প প্রস্তাব করে। এর উদ্দেশ্য হলো অবদানকারীদের শুরু থেকেই রক্ষণাবেক্ষণযোগ্য ও প্ল্যাটফর্ম-বহনযোগ্য পরিবর্তন ডিজাইন করতে সাহায্য করা এবং কোড পর্যালোচনার সময় একটি যৌথ রেফারেন্স হিসেবে কাজ করা।

অনুপ্রেরণা

সি প্রিপ্রসেসর হলো একটি C++ কম্পাইলেশন পাইপলাইনের প্রথম ধাপ। প্রিপ্রসেসর কন্ডিশনালগুলো প্রিপ্রসেসর টোকেনের প্রবাহকে নিয়ন্ত্রণ করে, যা সোর্স কোড ও হেডার ফাইল থেকে পড়া হয় এবং C++ কম্পাইলারের কাছে হস্তান্তর করা হয়। এটি কম্পাইলারের আগে চলে এবং প্রিপ্রসেসর টোকেন স্তরে কাজ করে—এটাই প্রধান কারণ যার জন্য এর কোডবেস রক্ষণাবেক্ষণ করা কঠিন হয়ে পড়ে।

  • কোডের জটিলতা: #ifdef ব্যবহার করা সুবিধাজনক কারণ এগুলো কোডের প্রায় যেকোনো জায়গায় ঢোকানো যায়। যেহেতু এগুলো টোকেনের উপর কাজ করে, তাই এর প্রায় কোনো সিনট্যাকটিক বা সিমান্টিক সীমাবদ্ধতা নেই। ডেভেলপারদের একটি যথাযথ অ্যাবস্ট্রাকশন নিয়ে ভাবতে হয় না; তারা যেখানে প্রয়োজন সেখানেই কন্ডিশনালগুলো যোগ করতে পারেন। একটি যথাযথ অ্যাবস্ট্রাকশন ডিজাইন করতে বাধ্য না হওয়ায় কোড পড়া এবং এর অর্থ বোঝা কঠিন হয়ে পড়ে। একটি ফ্রি ফাংশনের মতো সহজ অ্যাবস্ট্রাকশন থাকলেও, সেই ফাংশনটির টেস্টিং বা এর আচরণ মক করা সহজ হয়—এখন বা পরেও।

    এটি কম্পাইলার সতর্কবার্তাগুলির একটি উপসেটকেও অর্থহীন করে তোলে, কারণ কম্পাইলার কেবল একটি মূল্যায়ন করা টোকেন স্ট্রিমই দেখতে পাবে। একটি সাধারণ উদাহরণ হলো একটি ফাংশন প্যারামিটারে [[maybe_unused]] অ্যাট্রিবিউটের প্রয়োজনীয়তা, যা একটি প্রিপ্রসেসর কন্ডিশনালের কেবল একটি শাখায় ব্যবহৃত হয়।

  • পরীক্ষাযোগ্যতা (বিল্ড): যেহেতু প্রিপ্রসেসর কন্ডিশনালগুলো টোকেন স্ট্রিমের উপর কাজ করে, তাই অব্যবহৃত সমস্ত ব্রাঞ্চের সিনট্যাক্টিক বা সিম্যান্টিকভাবে সঠিক হওয়ার কোনো প্রয়োজন নেই এবং সে কারণে সেগুলোর সঠিকতা পরীক্ষা করা হয় না। এটি বড় আকারের রিফ্যাক্টরিং-এর ক্ষেত্রে মারাত্মক বাধা সৃষ্টি করে, যেখানে একজন ডেভেলপার ফাইন্ড-অ্যান্ড-রিপ্লেস প্রয়োগ করেন এবং কোথায় ম্যানুয়াল সংশোধন করতে হবে তা জানার জন্য কম্পাইলারের উপর নির্ভর করেন। কম্পাইল না হওয়া প্রিপ্রসেসর ব্রাঞ্চে করা ভুল পরিবর্তনগুলো হয় অলক্ষিত থেকে যায় অথবা প্রক্রিয়ার শেষের দিকে ধরা পড়ে (যদি CI সেই নির্দিষ্ট প্রিপ্রসেসর ব্রাঞ্চটি বিল্ড করে থাকে)। শেষের বিষয়টি দৈনন্দিন ডেভেলপমেন্টের জন্যও প্রাসঙ্গিক (পরবর্তী পয়েন্ট দেখুন)।

  • পরীক্ষাযোগ্যতা (পরিধি): প্রিপ্রসেসর কন্ডিশনাল যত বেশি থাকে, পরীক্ষার জন্য বিল্ড কনফিগারেশনও তত বেশি হয়, এবং প্রতিটি নতুন কন্ডিশন যোগ করার সাথে সাথে কনফিগারেশনের সংখ্যা দ্রুতগতিতে বাড়তে থাকে। এগুলোর সবগুলো পরীক্ষা করা ইতোমধ্যেই অসম্ভব। উদাহরণস্বরূপ, XLA-তে cuDNN ভার্সন নম্বরের উপর ভিত্তি করে বেশ কিছু কন্ডিশনাল রয়েছে, যেগুলোর সবগুলো CI-তে পরীক্ষা করা হয় না। কেউ যুক্তি দিতে পারেন যে, যতক্ষণ আমরা আমাদের প্রয়োজনীয় কনফিগারেশনগুলো পরীক্ষা করছি, ততক্ষণ এটি ততটা প্রাসঙ্গিক নয়—যা সম্ভবত সত্যি। কিন্তু অপ্রয়োজনীয়ভাবে ত্রুটিপূর্ণ কোডের কারণে বাগ রিপোর্ট আসে, যা সমাধান করার জন্য কাউকে না কাউকে এগিয়ে আসতে হয়, এমনকি যদি শুধু এটুকু বলা হয় যে একটি কনফিগারেশন সমর্থিত নয়।

    আরও গুরুত্বপূর্ণ বিষয় হলো, সম্ভাব্য ত্রুটিপূর্ণ বিল্ড কনফিগারেশন ডেভেলপারের পুনরাবৃত্তিমূলক কাজে ব্যয় হওয়া নিষ্ক্রিয় সময় বাড়িয়ে দেয়। কোনো পরিবর্তন হয়তো স্থানীয়ভাবে bazel test চালালে ঠিকঠাক কাজ করে, কিন্তু সিআই (CI) সামান্য ভিন্ন কনফিগারেশন বিল্ড করার ফলে তা ব্যর্থ হতে পারে। এর সমাধানগুলো প্রায়শই নিরীহ প্রকৃতির হয়—যেমন একটি [[maybe_unused]] অ্যাট্রিবিউট যোগ করা—কিন্তু এই অতিরিক্ত আসা-যাওয়ার প্রক্রিয়াটির জন্য প্রতিটি পুল রিকোয়েস্টে ডেভেলপারের বাড়তি সময় ব্যয় হয়।

    সুতরাং, প্রিপ্রসেসর কন্ডিশনালের সংখ্যা কমালে বিল্ড কনফিগারেশনের সংখ্যা কমে যায়, যার ফলে একটি PR-এর ক্ষেত্রে অতিরিক্ত CI রাউন্ড ট্রিপের সম্ভাবনা হ্রাস পায়।

  • টুলিং: C++ পার্সিং এতটাই জটিল একটি কাজ যে আজকাল বেশিরভাগ টুলই পার্সিং এবং সিমান্টিক বিশ্লেষণের জন্য একটি কম্পাইলার ফ্রন্টএন্ডের উপর নির্ভর করে এবং তারপর সরাসরি AST-এর উপর কাজ করে। উল্লেখযোগ্য উদাহরণগুলির মধ্যে রয়েছে clang-tidy , include-cleaner , এবং ল্যাঙ্গুয়েজ সার্ভার ( clangd ) যা আপনার IDE-তে কোড কমপ্লিশন, নেভিগেশন এবং সিনট্যাক্স হাইলাইটিং প্রদান করে। আরেক ধরনের টুল কম্পাইলার ইন্সট্রুমেন্টেশনের উপর নির্ভর করে, যার মধ্যে স্যানিটাইজার এবং কোড কভারেজ টুল অন্তর্ভুক্ত।

    এই সমস্ত টুল শুধুমাত্র একটি বিল্ড কনফিগারেশন দেখতে পায়, তাই প্রিপ্রসেসর কন্ডিশনালের ব্যাপক ব্যবহার এদের ব্যবহারযোগ্যতাকে ব্যাহত করে। উদাহরণস্বরূপ, include-cleaner এমন #include ট্যাগগুলো সরিয়ে ফেলার পরামর্শ দেয় যেগুলো শুধুমাত্র একটি আনইভ্যালুয়েটেড প্রিপ্রসেসর ব্রাঞ্চে ব্যবহৃত হয়। একইভাবে, আপনার IDE যখন ROCm কোডকে CUDA বা CPU বিল্ডের জন্য কনফিগার করা থাকে, তখন সেটির জন্য সিনট্যাক্স হাইলাইটিং বা কোড নেভিগেশন দেখায় না, ফলে এটি সম্পাদনা করা কষ্টকর হয়ে ওঠে।

প্রশমন

বিভাগ I - ইউনিট টেস্টে টেস্ট কেস বাদ দেওয়া

এটা খুবই সাধারণ যে একটি নির্দিষ্ট টেস্ট কেস শুধুমাত্র নিম্নলিখিতভাবে সমর্থিত হয়:

  • একটি নির্দিষ্ট ব্যাকএন্ডে।
  • একটি নির্দিষ্ট জিপিইউ মডেলের সাথে।
  • যখন লাইব্রেরি X অন্তত Y সংস্করণের সমান বা তার চেয়ে উন্নত হয়।

পূর্বে, প্রিপ্রসেসর কন্ডিশনাল ব্যবহার করে টেস্ট এড়িয়ে যাওয়া সাধারণ ব্যাপার ছিল:

TEST(Foo, Bar) {
#ifdef TENSORFLOW_USE_ROCM
  GTEST_SKIP();
#endif
  // ...
}

বিকল্প: StreamExecutor-কে জিজ্ঞাসা করুন

StreamExecutor হলো XLA-এর হার্ডওয়্যার অ্যাবস্ট্রাকশন লেয়ার, এবং এর stream_executor::DeviceDescription এ রানটাইমে একই সিদ্ধান্ত নেওয়ার জন্য প্রয়োজনীয় তথ্য থাকে:

TEST_F(FooTest, Bar) {
  // `device_description()` is provided by `HloPjRtGpuTestBase`. Other test
  // fixtures expose it via `executor->GetDeviceDescription()`.
  const se::DeviceDescription& device = device_description();

  // Skip based on the backend platform.
  if (device.gpu_compute_capability().IsRocm()) {
    GTEST_SKIP() << "Not supported on ROCm.";
  }

  // Skip based on the GPU model / compute capability.
  if (const auto* cc =
          device.gpu_compute_capability().cuda_compute_capability();
      cc != nullptr && !cc->IsAtLeastHopper()) {
    GTEST_SKIP() << "Requires Hopper or newer.";
  }

  // Skip based on the runtime or library version.
  if (device.runtime_version() < se::SemanticVersion{12, 2, 0}) {
    GTEST_SKIP() << "Requires CUDA runtime >= 12.2.";
  }
  if (device.dnn_version() < se::SemanticVersion{9, 0, 0}) {
    GTEST_SKIP() << "Requires cuDNN >= 9.0.";
  }
  // ...
}

DeviceDescription se::SemanticVersion এর মাধ্যমে কম্পিউট ক্যাপাবিলিটি এবং স্ট্রাকচার্ড ভার্সন নম্বর উভয়ই প্রকাশ করে:

  • ব্যাকএন্ড ও আর্কিটেকচার: device.gpu_compute_capability().IsCuda() , device.gpu_compute_capability().IsRocm() , device.gpu_compute_capability().IsOneAPI() , এবং CudaComputeCapability , RocmComputeCapability , ও OneAPIComputeCapability -এর জন্য অ্যাক্সেসরসমূহ।
  • রানটাইম ও ড্রাইভার সংস্করণ: device.runtime_version() , device.driver_version() , device.kernel_mode_driver_version() , device.compile_time_toolkit_version() ।
  • লাইব্রেরি সংস্করণসমূহ: device.dnn_version() , device.cub_version() ।

যদি DeviceDescription এ কোনো নির্দিষ্ট ভার্সন বা হার্ডওয়্যার প্রপার্টি এখনো উপলব্ধ না থাকে, তাহলে #ifdef ব্যবহার না করে অনুগ্রহ করে সেটি DeviceDescription এ যোগ করুন (অথবা কন্ট্রিবিউটরকে যোগ করতে বলুন)।

দীর্ঘমেয়াদী (লেখকের মতামত)

দীর্ঘমেয়াদে, আমাদের কোনো উচ্চ-স্তরের টেস্টেরই ব্যাকএন্ড, হার্ডওয়্যারের বিভিন্ন সংস্করণ, বা রানটাইম/ড্রাইভার ভার্সনের উপর ভিত্তি করে সিদ্ধান্ত নেওয়ার প্রয়োজন হবে না। এর পরিবর্তে, টেস্টগুলো StreamExecutor জিজ্ঞাসা করবে যে কোনো একটি নির্দিষ্ট ফিচার উপলব্ধ আছে কি না, এবং StreamExecutor সমস্ত প্রয়োজনীয় তথ্যের উপর ভিত্তি করে তা নির্ধারণ করবে। এই সমস্ত লজিক একটি জায়গাতেই থাকবে, যদিও এর এখনও কোনো সুনির্দিষ্ট ডিজাইন নেই।

ক্যাটাগরি II - এটি একটি রানটাইম if স্টেটমেন্ট হতে পারে

আরেক ধরনের প্রিপ্রসেসর কন্ডিশনাল এমন কোডকে সুরক্ষিত রাখে যা কন্ডিশনালটি ছাড়াও ঠিকঠাক কম্পাইল হতো:

void Foo::Bar() {
#if TENSORFLOW_USE_ROCM
  // Do something that would also compile in CUDA/CPU mode
#endif
  // ...
}

বিকল্প: রানটাইম ব্যবহার করুন if

প্রিপ্রসেসর কন্ডিশনালটিকে একটি রানটাইম কন্ডিশনাল দিয়ে প্রতিস্থাপন করুন:

void Foo::Bar() {
  if (stream_executor_.GetDeviceDescription()
          .gpu_compute_capability()
          .IsRocm()) {
    // Do something that compiles fine everywhere
  }
  // ...
}

এই পুরো বিষয়টিকে একটি পৃথক ফাংশনে ভাগ করা হবে কিনা, তা পর্যালোচকের বিবেচনার উপর নির্ভর করে। এই পর্যায়ে এটি একটি 'সাধারণ' কোড এবং এক্ষেত্রে সাধারণ কোড পর্যালোচনার নিয়মাবলী প্রযোজ্য।

এই শ্রেণীর প্রিপ্রসেসর কন্ডিশনালগুলো প্রায়শই এমন কোনো শর্ত যাচাই করার সময় দেখা যায়, যা StreamExecutor-এর DeviceDescription এ সহজে উপলব্ধ নয়। প্রিপ্রসেসর কন্ডিশনালটি গ্রহণ না করে, অবদানকারীদেরকে DeviceDescription এ প্রাসঙ্গিক তথ্যগুলো যোগ করতে অনুরোধ করার জন্য সবাইকে উৎসাহিত করা হচ্ছে।

ক্যাটাগরি III - নিম্ন-স্তরের রানটাইমে অ্যাক্সেস প্রয়োজন

প্রিপ্রসেসর কন্ডিশনালগুলোর মধ্যে সবচেয়ে সাধারণ (এবং সবচেয়ে যুক্তিযুক্ত) শ্রেণিটি হলো সেগুলো, যা এমন কোডকে সুরক্ষিত করে যা অন্যথায় কম্পাইল হবে না। বেশিরভাগ ক্ষেত্রেই এর কারণ হলো, এটি কোনো নিম্ন-স্তরের ব্যাকএন্ড-নির্দিষ্ট হেডার (যেমন CUDA বা ROCm API) থেকে কিছু ব্যবহার করে থাকে:

#if TENSORFLOW_USE_ROCM
#include <something/something/rocm.h>
#endif

void Foo::Bar() {
#if TENSORFLOW_USE_ROCM
  // Do something that would *NOT* compile in CUDA/CPU mode
#endif
  // ...
}

লাইব্রেরি হেডারের ভার্সন গার্ডের ক্ষেত্রেও একই কথা প্রযোজ্য (উদাহরণস্বরূপ, #if CUDNN_VERSION >= 90000 ), যখন কোডটি এমন সিম্বল রেফারেন্স করে যা হেডারের পুরোনো ভার্সনগুলোতে বিদ্যমান নেই।

বিকল্প ১: StreamExecutor-কে বিষয়টি সামলাতে দিন

প্রথম যে প্রশ্নটি করতে হবে তা হলো, পরিবর্তিত কম্পোনেন্টটি আদৌ নিম্ন-স্তরের রানটাইমের নির্দিষ্ট বিষয়ের উপর নির্ভরশীল হওয়া উচিত কিনা। আদর্শগতভাবে, শুধুমাত্র আমাদের হার্ডওয়্যার অ্যাবস্ট্রাকশন লেয়ার StreamExecutor এটি নির্ভরশীল হওয়া উচিত। সুতরাং, যদি এই কোডটি StreamExecutor এর অংশ না হয়, তবে প্রথম উপায় হলো এটিকে xla/stream_executor/ এর মধ্যে অন্তর্ভুক্ত করা যায় কিনা তা খতিয়ে দেখা।

এর মানে হলো StreamExecutor এ একটি API (একটি ফাংশন, একটি ক্লাস, বা যা কিছু উপযুক্ত) তৈরি করা, যা সমস্ত ব্যাকএন্ডের জন্য বিদ্যমান থাকবে। ব্যাকএন্ডের উপর ভিত্তি করে এই API-এর বিভিন্ন ইমপ্লিমেন্টেশন থাকতে পারে। এটি কোনো নির্দিষ্ট ফিচার উপলব্ধ আছে কিনা তা জিজ্ঞাসা করার সুযোগও দিতে পারে, যা উপরে বর্ণিত রানটাইম- if সমাধানটিকে সক্ষম করে।

অবশ্যই, এর মানে হলো StreamExecutor এখন নিম্ন-স্তরের ব্যাকএন্ড API-গুলো অ্যাক্সেস করার বিষয়টি সামলাতে হবে। বিকল্প II-তে বর্ণনা করা হয়েছে কীভাবে (অতিরিক্ত) #ifdef ব্যবহার না করেই এর মডেল তৈরি করা যায়।

বিকল্প II: আলাদা টার্গেটে কোড করুন

যদি ব্যাকএন্ড-নির্দিষ্ট হেডারগুলোর ব্যবহার StreamExecutor এ পুশ করা না যায় অথবা পরিবর্তনটি StreamExecutor ভেতরে হয়, তবে সবচেয়ে ভালো উপায় হলো কোডটিকে আলাদা কম্পাইলেশন ইউনিটে ভাগ করে নেওয়া। আমাদের উদাহরণে, একটি বিল্ড টার্গেটে ROCm কোড সম্বলিত একটিমাত্র C++ সোর্স ফাইল রয়েছে এবং আরেকটি বিল্ড টার্গেটে নন-ROCm কোড সম্বলিত একটিমাত্র C++ সোর্স ফাইল রয়েছে—যা সাধারণত একটি স্টাব ইমপ্লিমেন্টেশন এবং এর সংজ্ঞায়িত সমস্ত ফাংশনে absl::UnimplementedError রিটার্ন করে।

এর নিম্নলিখিত সুবিধাগুলো রয়েছে:

  • একাধিক ফাইলে কোড ভাগ করার অর্থ হলো, এর মাঝে একটি অ্যাবস্ট্রাকশন লেয়ার থাকা প্রয়োজন—অন্ততপক্ষে একটি ফ্রি ফাংশন। এটি কন্ট্রিবিউটর এবং রিভিউয়ার উভয়কেই অ্যাবস্ট্রাকশনের সঠিক স্তর নিয়ে ভাবতে উৎসাহিত করে, যা দীর্ঘমেয়াদে আরও উন্নত মানের কোড তৈরিতে সাহায্য করে।
  • উভয় ফাইলেই কেবল একটি বিল্ড কনফিগারেশন রয়েছে—যার ফলে আপনি দুটিতেই সম্পূর্ণ সিনট্যাক্স হাইলাইটিং এবং IDE টুলিং পাবেন।
  • একই বিল্ড গ্রাফে উভয় বিল্ড টার্গেট আলাদাভাবে বিল্ড করা যেতে পারে। আপনার বেজেল বিল্ড ফ্ল্যাগ পরিবর্তন করার এবং বিল্ড ক্যাশ খালি করার কোনো প্রয়োজন নেই।
  • উভয় বিল্ড টার্গেটেরই নিজস্ব টেস্ট থাকতে পারে। উদাহরণস্বরূপ, যদি কোনো নির্দিষ্ট এপিআই শুধুমাত্র ROCm-এর জন্য বিদ্যমান থাকে, তাহলে আমরা উচ্চ-স্তরের টেস্টের পরিবর্তে শুধু ROCm ইমপ্লিমেন্টেশন পরীক্ষা করার জন্য ইউনিট টেস্ট রাখতে পারি, যা অন্য সব ক্ষেত্রে বাদ পড়ে যায় এবং এর ফলে টেস্টিং দ্রুততর হতে পারে। (তবে ন্যায্যতার খাতিরে বলা যায়: অনেক ক্ষেত্রে উচ্চ-স্তরের টেস্ট রাখারও যথেষ্ট ভালো কারণ থাকে।)

একটি বাস্তব নকশা দেখতে এইরকম হতে পারে:

// feature.h
// Do NOT include any platform-specific headers (CUDA/ROCm) here.
#include "absl/status/status.h"

absl::Status Foo();
// feature_rocm.cc
#include "feature.h"
#include "something/something/rocm.h"

absl::Status Foo() {
  // Do something ROCm-specific.
}
// feature_stub.cc
#include "feature.h"
#include "absl/status/status.h"

absl::Status Foo() {
  return absl::UnimplementedError("This is a ROCm-only feature.");
}
# BUILD
load("@local_config_rocm//rocm:build_defs.bzl", "if_rocm_is_configured")

cc_library(
    name = "feature_rocm",
    srcs = [
        "feature.h",
        "feature_rocm.cc",
    ],
    tags = ["manual"],  # Exclude this from wildcard builds when ROCm is not enabled
    deps = [
        "@com_google_absl//absl/status",
        "@local_config_rocm//rocm:rocm_headers",
    ],
)

cc_library(
    name = "feature_stub",
    srcs = [
        "feature.h",
        "feature_stub.cc",
    ],
    deps = ["@com_google_absl//absl/status"],
)

cc_library(
    name = "feature",
    hdrs = ["feature.h"],
    deps = if_rocm_is_configured(
        [":feature_rocm"],
        [":feature_stub"],
    ) + [
        "@com_google_absl//absl/status",
    ],
)

উল্লেখ্য যে, feature.h শুধুমাত্র :feature টার্গেটের hdrs অ্যাট্রিবিউটে (এবং দুটি ইমপ্লিমেন্টেশন টার্গেটের srcs এ) থাকে। এটি নিশ্চিত করে যে, যারা feature.h অন্তর্ভুক্ত করে (এবং স্বয়ংক্রিয় ডিপেন্ডেন্সি টুলগুলো) তারা সরাসরি :feature_rocm বা :feature_stub এর পরিবর্তে :feature উপর নির্ভর করে।

এটিও গুরুত্বপূর্ণ যে feature.h যেন কোনো প্ল্যাটফর্ম-নির্দিষ্ট হেডার (যেমন CUDA, ROCm, বা cuDNN হেডার) অন্তর্ভুক্ত না থাকে; শুধুমাত্র .cc ফাইল (এবং প্রাইভেট হেডার)-গুলোতেই সেগুলো অন্তর্ভুক্ত থাকা উচিত। একই বিল্ড টার্গেট প্যাটার্ন CUDA ( //xla/tsl/platform/default:cuda_build_defs.bzl থেকে if_cuda_is_configured ) এবং SYCL-এর জন্যও কাজ করে।

দীর্ঘমেয়াদী (লেখকের মতামত)

পূর্ববর্তী উদাহরণে, :feature_rocm এবং :feature_stub টার্গেটগুলো একই সিম্বল Foo সংজ্ঞায়িত করে, তাই একটি ODR লঙ্ঘন না ঘটিয়ে সেগুলোকে একই বাইনারিতে লিঙ্ক করা যায় না। দীর্ঘমেয়াদে, XLA-এর একটি প্লাগইন পরিকাঠামো থাকতে পারে যেখানে একাধিক ব্যাকএন্ড একই সময়ে লোড করা যাবে। সেক্ষেত্রে, উপরের ডিজাইনটি সামান্য পরিবর্তন করতে হবে যাতে :feature_rocm এবং :feature_stub ভিন্ন ভিন্ন নামের সিম্বল সংজ্ঞায়িত করে এবং :feature মূল নামটিসহ একটি ডিসপ্যাচ ফাংশন থাকে—যদিও এটি যথেষ্ট উচ্চ অগ্রাধিকার পাবে কিনা তা বর্তমানে স্পষ্ট নয়।