Repository: intel/llvm
Pull request #17543: [UR][L0] Avoid calls to destroy interop data structures given loader instability
Author: login: nrspruit
Created: 2025-03-19T22:59:38Z | Merged: 2025-03-21T17:25:18Z

- When an application transfers ownership of a L0 handle to the UR, this does not prevent the OS from releasing the memory backing the handle when the interop library is torn down. This leads to a situation where the UR is trying to destroy interop data structures that have already been destroyed by the OS.
- UR has never known the lifetime of the handle, but with the static L0 Loader, the workaround in the loader to handle this situation and "pretend" the memory was freed is impossible to use.
- To fix this issue, avoid calls to destroy interop data structures when the runtime is being torn down.
- This avoids calling a function that already did not perform the intended operation and prevents segfaults when the UR tries to destroy interop data structures that have already been destroyed.


Diff:
diff --git a/unified-runtime/source/adapters/level_zero/common.hpp b/unified-runtime/source/adapters/level_zero/common.hpp
index df26838b08dff..4ddd1c49a493c 100644
--- a/unified-runtime/source/adapters/level_zero/common.hpp
+++ b/unified-runtime/source/adapters/level_zero/common.hpp
@@ -18,6 +18,13 @@
 #include <unordered_map>
 #include <vector>
 
+#ifdef _WIN32
+#include "windows.h"
+#else
+#include <dlfcn.h>
+#include <unistd.h>
+#endif
+
 #include <ur/ur.hpp>
 #include <ur_ddi.h>
 #include <ze_api.h>
@@ -30,6 +37,68 @@
 
 struct _ur_platform_handle_t;
 
+[[maybe_unused]] static bool checkL0LoaderTeardown() {
+  bool loaderStable = true;
+#ifdef _WIN32
+  uint32_t ZeDriverCount = 0;
+  HMODULE zeLoader = LoadLibrary("ze_loader.dll");
+  if (zeLoader) {
+    typedef ze_result_t (*zeDriverGet_t)(uint32_t *, ze_driver_handle_t *);
+    zeDriverGet_t zeDriverGetLoader =
+        (zeDriverGet_t)GetProcAddress(zeLoader, "zeDriverGet");
+    if (zeDriverGetLoader) {
+      ze_result_t result = zeDriverGetLoader(&ZeDriverCount, nullptr);
+      logger::debug(
+          "ZE ---> checkL0LoaderTeardown result = {} driver count = {}", result,
+          ZeDriverCount);
+      if (result != ZE_RESULT_SUCCESS || ZeDriverCount == 0) {
+        loaderStable = false;
+      }
+    } else {
+      logger::debug("ZE ---> checkL0LoaderTeardown: Failed to get address of "
+                    "zeDriverGet");
+      loaderStable = false;
+    }
+    FreeLibrary(zeLoader);
+  } else {
+    logger::debug(
+        "ZE ---> checkL0LoaderTeardown: Failed to load ze_loader.dll");
+    loaderStable = false;
+  }
+#else
+  uint32_t ZeDriverCount = 0;
+  void *zeLoader = dlopen("libze_loader.so.1", RTLD_LAZY);
+  if (zeLoader) {
+    typedef ze_result_t (*zeDriverGet_t)(uint32_t *, ze_driver_handle_t *);
+    zeDriverGet_t zeDriverGetLoader =
+        (zeDriverGet_t)dlsym(zeLoader, "zeDriverGet");
+    if (zeDriverGetLoader) {
+      ze_result_t result = zeDriverGetLoader(&ZeDriverCount, nullptr);
+      logger::debug(
+          "ZE ---> checkL0LoaderTeardown result = {} driver count = {}", result,
+          ZeDriverCount);
+      if (result != ZE_RESULT_SUCCESS || ZeDriverCount == 0) {
+        loaderStable = false;
+      }
+    } else {
+      logger::debug("ZE ---> checkL0LoaderTeardown: Failed to get address of "
+                    "zeDriverGet");
+      loaderStable = false;
+    }
+    dlclose(zeLoader);
+  } else {
+    logger::debug(
+        "ZE ---> checkL0LoaderTeardown: Failed to load libze_loader.so.1");
+    loaderStable = false;
+  }
+#endif
+  if (!loaderStable) {
+    logger::debug(
+        "ZE ---> checkL0LoaderTeardown: Loader is not stable, returning false");
+  }
+  return loaderStable;
+}
+
 static auto getUrResultString = [](ur_result_t Result) {
   switch (Result) {
   case UR_RESULT_SUCCESS:
@@ -435,6 +504,9 @@ struct _ur_object {
   // Indicates if we own the native handle or it came from interop that
   // asked to not transfer the ownership to SYCL RT.
   bool OwnNativeHandle = false;
+
+  // Indicates if this object is an interop handle.
+  bool IsInteropNativeHandle = false;
 };
 
 // Record for a memory allocation. This structure is used to keep information
diff --git a/unified-runtime/source/adapters/level_zero/context.cpp b/unified-runtime/source/adapters/level_zero/context.cpp
index 12d760680015e..9d30f0f26b222 100644
--- a/unified-runtime/source/adapters/level_zero/context.cpp
+++ b/unified-runtime/source/adapters/level_zero/context.cpp
@@ -151,6 +151,7 @@ ur_result_t urContextCreateWithNativeHandle(
     ur_context_handle_t_ *UrContext = new ur_context_handle_t_(
         ZeContext, NumDevices, Devices, OwnNativeHandle);
     UrContext->initialize();
+    UrContext->IsInteropNativeHandle = true;
     *Context = reinterpret_cast<ur_context_handle_t>(UrContext);
   } catch (const std::bad_alloc &) {
     return UR_RESULT_ERROR_OUT_OF_HOST_MEMORY;
@@ -262,7 +263,11 @@ ur_result_t ContextReleaseHelper(ur_context_handle_t Context) {
       Contexts.erase(It);
   }
   ze_context_handle_t DestroyZeContext =
-      Context->OwnNativeHandle ? Context->ZeContext : nullptr;
+      ((Context->OwnNativeHandle && !Context->IsInteropNativeHandle) ||
+       (Context->OwnNativeHandle && Context->IsInteropNativeHandle &&
+        checkL0LoaderTeardown()))
+          ? Context->ZeContext
+          : nullptr;
 
   // Clean up any live memory associated with Context
   ur_result_t Result = Context->finalize();
@@ -299,11 +304,14 @@ ur_result_t ur_context_handle_t_::finalize() {
     std::scoped_lock<ur_mutex> Lock(EventCacheMutex);
     for (auto &EventCache : EventCaches) {
       for (auto &Event : EventCache) {
-        auto ZeResult = ZE_CALL_NOCHECK(zeEventDestroy, (Event->ZeEvent));
+        if (!Event->IsInteropNativeHandle ||
+            (Event->IsInteropNativeHandle && checkL0LoaderTeardown())) {
+          auto ZeResult = ZE_CALL_NOCHECK(zeEventDestroy, (Event->ZeEvent));
+          // Gracefully handle the case that L0 was already unloaded.
+          if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
+            return ze2urResult(ZeResult);
+        }
         Event->ZeEvent = nullptr;
-        // Gracefully handle the case that L0 was already unloaded.
-        if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
-          return ze2urResult(ZeResult);
         delete Event;
       }
       EventCache.clear();
diff --git a/unified-runtime/source/adapters/level_zero/device.cpp b/unified-runtime/source/adapters/level_zero/device.cpp
index e789d6993dfd1..412bef0bdb6e9 100644
--- a/unified-runtime/source/adapters/level_zero/device.cpp
+++ b/unified-runtime/source/adapters/level_zero/device.cpp
@@ -1416,6 +1416,7 @@ ur_result_t urDeviceCreateWithNativeHandle(
   if (Dev == nullptr)
     return UR_RESULT_ERROR_INVALID_VALUE;
 
+  Dev->IsInteropNativeHandle = true;
   *Device = Dev;
   return UR_RESULT_SUCCESS;
 }
diff --git a/unified-runtime/source/adapters/level_zero/event.cpp b/unified-runtime/source/adapters/level_zero/event.cpp
index 0da9c955cd172..ff2a01daacb5d 100644
--- a/unified-runtime/source/adapters/level_zero/event.cpp
+++ b/unified-runtime/source/adapters/level_zero/event.cpp
@@ -1001,6 +1001,7 @@ ur_result_t urEventCreateWithNativeHandle(
   UREvent->CleanedUp = true;
 
   *Event = reinterpret_cast<ur_event_handle_t>(UREvent);
+  UREvent->IsInteropNativeHandle = true;
 
   return UR_RESULT_SUCCESS;
 }
@@ -1116,11 +1117,14 @@ ur_result_t urEventReleaseInternal(ur_event_handle_t Event) {
   }
   if (Event->OwnNativeHandle) {
     if (DisableEventsCaching) {
-      auto ZeResult = ZE_CALL_NOCHECK(zeEventDestroy, (Event->ZeEvent));
+      if (!Event->IsInteropNativeHandle ||
+          (Event->IsInteropNativeHandle && checkL0LoaderTeardown())) {
+        auto ZeResult = ZE_CALL_NOCHECK(zeEventDestroy, (Event->ZeEvent));
+        // Gracefully handle the case that L0 was already unloaded.
+        if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
+          return ze2urResult(ZeResult);
+      }
       Event->ZeEvent = nullptr;
-      // Gracefully handle the case that L0 was already unloaded.
-      if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
-        return ze2urResult(ZeResult);
       auto Context = Event->Context;
       if (auto Res = Context->decrementUnreleasedEventsInPool(Event))
         return Res;
diff --git a/unified-runtime/source/adapters/level_zero/kernel.cpp b/unified-runtime/source/adapters/level_zero/kernel.cpp
index 5ccab131f3cc7..d9dd4cc38ad0a 100644
--- a/unified-runtime/source/adapters/level_zero/kernel.cpp
+++ b/unified-runtime/source/adapters/level_zero/kernel.cpp
@@ -940,10 +940,13 @@ ur_result_t urKernelRelease(
   auto KernelProgram = Kernel->Program;
   if (Kernel->OwnNativeHandle) {
     for (auto &ZeKernel : Kernel->ZeKernels) {
-      auto ZeResult = ZE_CALL_NOCHECK(zeKernelDestroy, (ZeKernel));
-      // Gracefully handle the case that L0 was already unloaded.
-      if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
-        return ze2urResult(ZeResult);
+      if (!Kernel->IsInteropNativeHandle ||
+          (Kernel->IsInteropNativeHandle && checkL0LoaderTeardown())) {
+        auto ZeResult = ZE_CALL_NOCHECK(zeKernelDestroy, (ZeKernel));
+        // Gracefully handle the case that L0 was already unloaded.
+        if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
+          return ze2urResult(ZeResult);
+      }
     }
   }
   Kernel->ZeKernelMap.clear();
@@ -1154,6 +1157,7 @@ ur_result_t urKernelCreateWithNativeHandle(
   }
 
   Kernel->Program = Program;
+  Kernel->IsInteropNativeHandle = true;
 
   UR_CALL(Kernel->initialize());
 
diff --git a/unified-runtime/source/adapters/level_zero/memory.cpp b/unified-runtime/source/adapters/level_zero/memory.cpp
index 925dd053ccaaa..eab949cdf988f 100644
--- a/unified-runtime/source/adapters/level_zero/memory.cpp
+++ b/unified-runtime/source/adapters/level_zero/memory.cpp
@@ -1563,6 +1563,7 @@ ur_result_t urMemImageCreateWithNativeHandle(
   auto OwnNativeHandle = Properties ? Properties->isNativeHandleOwned : false;
   UR_CALL(createUrMemFromZeImage(Context, ZeHImage, OwnNativeHandle,
                                  ZeImageDesc, Mem));
+  (*Mem)->IsInteropNativeHandle = true;
 
   return UR_RESULT_SUCCESS;
 }
@@ -1662,11 +1663,14 @@ ur_result_t urMemRelease(
     if (Image->OwnNativeHandle) {
       UR_CALL(Mem->getZeHandle(ZeHandleImage, ur_mem_handle_t_::write_only,
                                nullptr, nullptr, 0u));
-      auto ZeResult = ZE_CALL_NOCHECK(
-          zeImageDestroy, (ur_cast<ze_image_handle_t>(ZeHandleImage)));
-      // Gracefully handle the case that L0 was already unloaded.
-      if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
-        return ze2urResult(ZeResult);
+      if (!Image->IsInteropNativeHandle ||
+          (Image->IsInteropNativeHandle && checkL0LoaderTeardown())) {
+        auto ZeResult = ZE_CALL_NOCHECK(
+            zeImageDestroy, (ur_cast<ze_image_handle_t>(ZeHandleImage)));
+        // Gracefully handle the case that L0 was already unloaded.
+        if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
+          return ze2urResult(ZeResult);
+      }
     }
     delete Image;
   } else {
@@ -1772,6 +1776,7 @@ ur_result_t urMemBufferCreateWithNativeHandle(
     Buffer = new _ur_buffer(Context, Size, Device, ur_cast<char *>(NativeMem),
                             OwnNativeHandle);
     *Mem = reinterpret_cast<ur_mem_handle_t>(Buffer);
+    (*Mem)->IsInteropNativeHandle = true;
   } catch (const std::bad_alloc &) {
     return UR_RESULT_ERROR_OUT_OF_HOST_MEMORY;
   } catch (...) {
diff --git a/unified-runtime/source/adapters/level_zero/program.cpp b/unified-runtime/source/adapters/level_zero/program.cpp
index 3f0790bddd8bc..af5c7fb005458 100644
--- a/unified-runtime/source/adapters/level_zero/program.cpp
+++ b/unified-runtime/source/adapters/level_zero/program.cpp
@@ -956,7 +956,6 @@ ur_result_t urProgramCreateWithNativeHandle(
   UR_ASSERT(Context && NativeProgram, UR_RESULT_ERROR_INVALID_NULL_HANDLE);
   UR_ASSERT(Program, UR_RESULT_ERROR_INVALID_NULL_POINTER);
   auto ZeModule = ur_cast<ze_module_handle_t>(NativeProgram);
-
   // We assume here that programs created from a native handle always
   // represent a fully linked executable (state Exe) and not an unlinked
   // executable (state Object).
@@ -966,6 +965,7 @@ ur_result_t urProgramCreateWithNativeHandle(
         ur_program_handle_t_::Exe, Context, ZeModule,
         Properties ? Properties->isNativeHandleOwned : false);
     *Program = reinterpret_cast<ur_program_handle_t>(UrProgram);
+    (*Program)->IsInteropNativeHandle = true;
   } catch (const std::bad_alloc &) {
     return UR_RESULT_ERROR_OUT_OF_HOST_MEMORY;
   } catch (...) {
@@ -1036,15 +1036,15 @@ ur_program_handle_t_::ur_program_handle_t_(ur_context_handle_t Context)
 ur_program_handle_t_::ur_program_handle_t_(state, ur_context_handle_t Context,
                                            ze_module_handle_t InteropZeModule)
     : Context{Context}, NativeProperties{nullptr}, OwnZeModule{true},
-      AssociatedDevices({Context->getDevices()[0]}),
-      InteropZeModule{InteropZeModule} {}
+      AssociatedDevices({Context->getDevices()[0]}), InteropZeModule{
+                                                         InteropZeModule} {}
 
 ur_program_handle_t_::ur_program_handle_t_(state, ur_context_handle_t Context,
                                            ze_module_handle_t InteropZeModule,
                                            bool OwnZeModule)
     : Context{Context}, NativeProperties{nullptr}, OwnZeModule{OwnZeModule},
-      AssociatedDevices({Context->getDevices()[0]}),
-      InteropZeModule{InteropZeModule} {
+      AssociatedDevices({Context->getDevices()[0]}), InteropZeModule{
+                                                         InteropZeModule} {
   // TODO: Currently it is not possible to understand the device associated
   // with provided ZeModule. So we can't set the state on that device to Exe.
 }
@@ -1080,10 +1080,10 @@ void ur_program_handle_t_::ur_release_program_resources(bool deletion) {
       if (DeviceData.ZeBuildLog)
         ZE_CALL_NOCHECK(zeModuleBuildLogDestroy, (DeviceData.ZeBuildLog));
     }
-
     // interop api
-    if (InteropZeModule && OwnZeModule)
+    if (InteropZeModule && OwnZeModule && checkL0LoaderTeardown()) {
       ZE_CALL_NOCHECK(zeModuleDestroy, (InteropZeModule));
+    }
 
     for (auto &[ZeDevice, DeviceData] : this->DeviceDataMap)
       if (DeviceData.ZeModule)
diff --git a/unified-runtime/source/adapters/level_zero/queue.cpp b/unified-runtime/source/adapters/level_zero/queue.cpp
index aed521b605a2f..d279ee23f8a0a 100644
--- a/unified-runtime/source/adapters/level_zero/queue.cpp
+++ b/unified-runtime/source/adapters/level_zero/queue.cpp
@@ -798,6 +798,7 @@ ur_result_t urQueueCreateWithNativeHandle(
       ur_queue_handle_t_ *Queue = new ur_queue_handle_t_(
           ComputeQueues, CopyQueues, Context, UrDevice, OwnNativeHandle, Flags);
       *RetQueue = reinterpret_cast<ur_queue_handle_t>(Queue);
+      (*RetQueue)->IsInteropNativeHandle = true;
     } catch (const std::bad_alloc &) {
       return UR_RESULT_ERROR_OUT_OF_RESOURCES;
     } catch (...) {
@@ -1599,10 +1600,13 @@ ur_result_t urQueueReleaseInternal(ur_queue_handle_t Queue) {
       for (auto &QueueGroup : QueueMap)
         for (auto &ZeQueue : QueueGroup.second.ZeQueues)
           if (ZeQueue) {
-            auto ZeResult = ZE_CALL_NOCHECK(zeCommandQueueDestroy, (ZeQueue));
-            // Gracefully handle the case that L0 was already unloaded.
-            if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
-              return ze2urResult(ZeResult);
+            if (!Queue->IsInteropNativeHandle ||
+                (Queue->IsInteropNativeHandle && checkL0LoaderTeardown())) {
+              auto ZeResult = ZE_CALL_NOCHECK(zeCommandQueueDestroy, (ZeQueue));
+              // Gracefully handle the case that L0 was already unloaded.
+              if (ZeResult && ZeResult != ZE_RESULT_ERROR_UNINITIALIZED)
+                return ze2urResult(ZeResult);
+            }
           }
   }
 
diff --git a/unified-runtime/source/adapters/level_zero/usm.cpp b/unified-runtime/source/adapters/level_zero/usm.cpp
index c1bc5ee0bbb3b..19a82ab16b5b0 100644
--- a/unified-runtime/source/adapters/level_zero/usm.cpp
+++ b/unified-runtime/source/adapters/level_zero/usm.cpp
@@ -644,13 +644,18 @@ ur_result_t UR_APICALL urUSMPoolTrimToExp(ur_context_handle_t,
 } // namespace ur::level_zero
 
 static ur_result_t USMFreeImpl(ur_context_handle_t Context, void *Ptr) {
-  auto ZeResult = ZE_CALL_NOCHECK(zeMemFree, (Context->ZeContext, Ptr));
-  // Handle When the driver is already released
-  if (ZeResult == ZE_RESULT_ERROR_UNINITIALIZED) {
-    return UR_RESULT_SUCCESS;
-  } else {
-    return ze2urResult(ZeResult);
+  ur_result_t Res = UR_RESULT_SUCCESS;
+  if (!Context->IsInteropNativeHandle ||
+      (Context->IsInteropNativeHandle && checkL0LoaderTeardown())) {
+    auto ZeResult = ZE_CALL_NOCHECK(zeMemFree, (Context->ZeContext, Ptr));
+    // Handle When the driver is already released
+    if (ZeResult == ZE_RESULT_ERROR_UNINITIALIZED) {
+      Res = UR_RESULT_SUCCESS;
+    } else {
+      Res = ze2urResult(ZeResult);
+    }
   }
+  return Res;
 }
 
 static ur_result_t USMQueryPageSize(ur_context_handle_t Context, void *Ptr,
diff --git a/unified-runtime/source/adapters/level_zero/v2/common.hpp b/unified-runtime/source/adapters/level_zero/v2/common.hpp
index f4c2039f87b33..504a7d6c618ed 100644
--- a/unified-runtime/source/adapters/level_zero/v2/common.hpp
+++ b/unified-runtime/source/adapters/level_zero/v2/common.hpp
@@ -79,7 +79,8 @@ struct ze_handle_wrapper {
       return;
     }
 
-    if (ownZeHandle) {
+    if ((ownZeHandle && !IsInteropNativeHandle) ||
+        (ownZeHandle && IsInteropNativeHandle && checkL0LoaderTeardown())) {
       auto zeResult = destroy(handle);
       // Gracefully handle the case that L0 was already unloaded.
       if (zeResult && zeResult != ZE_RESULT_ERROR_UNINITIALIZED)
@@ -102,6 +103,7 @@ struct ze_handle_wrapper {
 private:
   ZeHandleT handle;
   bool ownZeHandle;
+  bool IsInteropNativeHandle = false;
 };
 
 using ze_kernel_handle_t = HANDLE_WRAPPER_TYPE(::ze_kernel_handle_t,
diff --git a/unified-runtime/source/adapters/level_zero/v2/context.cpp b/unified-runtime/source/adapters/level_zero/v2/context.cpp
index 33b7f437215b0..5fe913d878201 100644
--- a/unified-runtime/source/adapters/level_zero/v2/context.cpp
+++ b/unified-runtime/source/adapters/level_zero/v2/context.cpp
@@ -152,6 +152,7 @@ ur_result_t urContextCreateWithNativeHandle(
 
   *phContext =
       new ur_context_handle_t_(zeContext, numDevices, phDevices, ownZeHandle);
+  (*phContext)->IsInteropNativeHandle = true;
   return UR_RESULT_SUCCESS;
 } catch (...) {
   return exceptionToResult(std::current_exception());
diff --git a/unified-runtime/source/adapters/level_zero/v2/event.cpp b/unified-runtime/source/adapters/level_zero/v2/event.cpp
index a26fc1a2a45dd..68c0d439dce7d 100644
--- a/unified-runtime/source/adapters/level_zero/v2/event.cpp
+++ b/unified-runtime/source/adapters/level_zero/v2/event.cpp
@@ -398,6 +398,7 @@ urEventCreateWithNativeHandle(ur_native_handle_t hNativeEvent,
     ZE2UR_CALL(zeEventHostSignal, ((*phEvent)->getZeEvent()));
   } else {
     *phEvent = new ur_event_handle_t_(hContext, hNativeEvent, pProperties);
+    (*phEvent)->IsInteropNativeHandle = true;
   }
   return UR_RESULT_SUCCESS;
 } catch (...) {
diff --git a/unified-runtime/source/adapters/level_zero/v2/kernel.cpp b/unified-runtime/source/adapters/level_zero/v2/kernel.cpp
index 6d475552279d8..5f8bad0acdbc8 100644
--- a/unified-runtime/source/adapters/level_zero/v2/kernel.cpp
+++ b/unified-runtime/source/adapters/level_zero/v2/kernel.cpp
@@ -361,6 +361,7 @@ urKernelCreateWithNativeHandle(ur_native_handle_t hNativeKernel,
 
   *phKernel =
       new ur_kernel_handle_t_(hNativeKernel, hProgram, hContext, pProperties);
+  (*phKernel)->IsInteropNativeHandle = true;
   return UR_RESULT_SUCCESS;
 } catch (...) {
   return exceptionToResult(std::current_exception());
diff --git a/unified-runtime/source/adapters/level_zero/v2/memory.cpp b/unified-runtime/source/adapters/level_zero/v2/memory.cpp
index 162c3b0596d3d..2844cc3ec07ca 100644
--- a/unified-runtime/source/adapters/level_zero/v2/memory.cpp
+++ b/unified-runtime/source/adapters/level_zero/v2/memory.cpp
@@ -107,14 +107,17 @@ ur_integrated_buffer_handle_t::ur_integrated_buffer_handle_t(
 
 ur_integrated_buffer_handle_t::ur_integrated_buffer_handle_t(
     ur_context_handle_t hContext, void *hostPtr, size_t size,
-    device_access_mode_t accessMode, bool ownHostPtr)
+    device_access_mode_t accessMode, bool ownHostPtr, bool interopNativeHandle)
     : ur_mem_buffer_t(hContext, size, accessMode) {
-  this->ptr = usm_unique_ptr_t(hostPtr, [hContext, ownHostPtr](void *ptr) {
-    if (!ownHostPtr) {
-      return;
-    }
-    ZE_CALL_NOCHECK(zeMemFree, (hContext->getZeHandle(), ptr));
-  });
+  this->IsInteropNativeHandle = interopNativeHandle;
+  this->ptr =
+      usm_unique_ptr_t(hostPtr, [hContext, ownHostPtr, this](void *ptr) {
+        if (!ownHostPtr ||
+            (this->IsInteropNativeHandle && !checkL0LoaderTeardown())) {
+          return;
+        }
+        ZE_CALL_NOCHECK(zeMemFree, (hContext->getZeHandle(), ptr));
+      });
 }
 
 void *ur_integrated_buffer_handle_t::getDevicePtr(
@@ -220,7 +223,7 @@ ur_discrete_buffer_handle_t::ur_discrete_buffer_handle_t(
 ur_discrete_buffer_handle_t::ur_discrete_buffer_handle_t(
     ur_context_handle_t hContext, ur_device_handle_t hDevice, void *devicePtr,
     size_t size, device_access_mode_t accessMode, void *writeBackMemory,
-    bool ownZePtr)
+    bool ownZePtr, bool interopNativeHandle)
     : ur_mem_buffer_t(hContext, size, accessMode),
       deviceAllocations(hContext->getPlatform()->getNumDevices()),
       activeAllocationDevice(hDevice), writeBackPtr(writeBackMemory),
@@ -230,9 +233,11 @@ ur_discrete_buffer_handle_t::ur_discrete_buffer_handle_t(
     hDevice = hDevice ? hDevice : hContext->getDevices()[0];
     devicePtr = allocateOnDevice(hDevice, size);
   } else {
+    this->IsInteropNativeHandle = interopNativeHandle;
     deviceAllocations[hDevice->Id.value()] = usm_unique_ptr_t(
-        devicePtr, [hContext = this->hContext, ownZePtr](void *ptr) {
-          if (!ownZePtr) {
+        devicePtr, [this, hContext = this->hContext, ownZePtr](void *ptr) {
+          if (!ownZePtr ||
+              (this->IsInteropNativeHandle && !checkL0LoaderTeardown())) {
             return;
           }
           ZE_CALL_NOCHECK(zeMemFree, (hContext->getZeHandle(), ptr));
@@ -434,8 +439,10 @@ ur_mem_image_t::ur_mem_image_t(ur_context_handle_t hContext,
 ur_mem_image_t::ur_mem_image_t(ur_context_handle_t hContext,
                                const ur_image_format_t *pImageFormat,
                                const ur_image_desc_t *pImageDesc,
-                               ze_image_handle_t zeImage, bool ownZeImage)
+                               ze_image_handle_t zeImage, bool ownZeImage,
+                               bool interopNativeHandle)
     : hContext(hContext), zeImage(zeImage, ownZeImage) {
+  this->IsInteropNativeHandle = interopNativeHandle;
   UR_CALL_THROWS(ur2zeImageDesc(pImageFormat, pImageDesc, zeImageDesc));
 }
 
@@ -577,7 +584,7 @@ ur_result_t urMemBufferCreateWithNativeHandle(
 
   if (useHostBuffer(hContext) && memoryAttrs.type == ZE_MEMORY_TYPE_HOST) {
     *phMem = ur_mem_handle_t_::create<ur_integrated_buffer_handle_t>(
-        hContext, ptr, size, accessMode, ownNativeHandle);
+        hContext, ptr, size, accessMode, ownNativeHandle, true);
     // if useHostBuffer(hContext) is true but the allocation is on device, we'll
     // treat it as discrete memory
   } else {
@@ -585,11 +592,13 @@ ur_result_t urMemBufferCreateWithNativeHandle(
       // For host allocation, we need to copy the data to a device buffer
       // and then copy it back on release
       *phMem = ur_mem_handle_t_::create<ur_discrete_buffer_handle_t>(
-          hContext, hDevice, nullptr, size, accessMode, ptr, ownNativeHandle);
+          hContext, hDevice, nullptr, size, accessMode, ptr, ownNativeHandle,
+          true);
     } else {
       // For device/shared allocation, we can use it directly
       *phMem = ur_mem_handle_t_::create<ur_discrete_buffer_handle_t>(
-          hContext, hDevice, ptr, size, accessMode, nullptr, ownNativeHandle);
+          hContext, hDevice, ptr, size, accessMode, nullptr, ownNativeHandle,
+          true);
     }
   }
 
@@ -699,7 +708,7 @@ ur_result_t urMemImageCreateWithNativeHandle(
   bool ownNativeHandle = pProperties ? pProperties->isNativeHandleOwned : false;
 
   *phMem = ur_mem_handle_t_::create<ur_mem_image_t>(
-      hContext, pImageFormat, pImageDesc, zeImage, ownNativeHandle);
+      hContext, pImageFormat, pImageDesc, zeImage, ownNativeHandle, true);
   return UR_RESULT_SUCCESS;
 } catch (...) {
   return exceptionToResult(std::current_exception());
diff --git a/unified-runtime/source/adapters/level_zero/v2/memory.hpp b/unified-runtime/source/adapters/level_zero/v2/memory.hpp
index caad23e683202..9aab1300f0163 100644
--- a/unified-runtime/source/adapters/level_zero/v2/memory.hpp
+++ b/unified-runtime/source/adapters/level_zero/v2/memory.hpp
@@ -20,6 +20,9 @@
 using usm_unique_ptr_t = std::unique_ptr<void, std::function<void(void *)>>;
 
 struct ur_mem_buffer_t : _ur_object {
+  // Indicates if this object is an interop handle.
+  bool IsInteropNativeHandle = false;
+
   enum class device_access_mode_t { read_write, read_only, write_only };
 
   ur_mem_buffer_t(ur_context_handle_t hContext, size_t size,
@@ -82,7 +85,7 @@ struct ur_integrated_buffer_handle_t : ur_mem_buffer_t {
 
   ur_integrated_buffer_handle_t(ur_context_handle_t hContext, void *hostPtr,
                                 size_t size, device_access_mode_t accesMode,
-                                bool ownHostPtr);
+                                bool ownHostPtr, bool interopNativeHandle);
 
   void *
   getDevicePtr(ur_device_handle_t, device_access_mode_t, size_t offset,
@@ -122,7 +125,8 @@ struct ur_discrete_buffer_handle_t : ur_mem_buffer_t {
   ur_discrete_buffer_handle_t(ur_context_handle_t hContext,
                               ur_device_handle_t hDevice, void *devicePtr,
                               size_t size, device_access_mode_t accesMode,
-                              void *writeBackMemory, bool ownDevicePtr);
+                              void *writeBackMemory, bool ownDevicePtr,
+                              bool interopNativeHandle);
 
   void *
   getDevicePtr(ur_device_handle_t, device_access_mode_t, size_t offset,
@@ -184,7 +188,7 @@ struct ur_mem_image_t : _ur_object {
                  const ur_image_desc_t *pImageDesc, void *pHost);
   ur_mem_image_t(ur_context_handle_t, const ur_image_format_t *pImageFormat,
                  const ur_image_desc_t *pImageDesc, ze_image_handle_t zeImage,
-                 bool ownZeImage);
+                 bool ownZeImage, bool interopNativeHandle);
 
   ze_image_handle_t getZeImage() const { return zeImage.get(); }
 
diff --git a/unified-runtime/source/adapters/level_zero/v2/queue_create.cpp b/unified-runtime/source/adapters/level_zero/v2/queue_create.cpp
index 7e2de8b1b647b..9f45404b7e39d 100644
--- a/unified-runtime/source/adapters/level_zero/v2/queue_create.cpp
+++ b/unified-runtime/source/adapters/level_zero/v2/queue_create.cpp
@@ -59,7 +59,7 @@ ur_result_t urQueueCreateWithNativeHandle(
   }
 
   *phQueue = ur_queue_handle_t_::create<v2::ur_queue_immediate_in_order_t>(
-      hContext, hDevice, hNativeQueue, flags, ownNativeHandle);
+      hContext, hDevice, hNativeQueue, flags, ownNativeHandle, true);
 
   return UR_RESULT_SUCCESS;
 } catch (...) {
diff --git a/unified-runtime/source/adapters/level_zero/v2/queue_handle.hpp b/unified-runtime/source/adapters/level_zero/v2/queue_handle.hpp
index a17d304ea5cfc..26b58c9c6c4cd 100644
--- a/unified-runtime/source/adapters/level_zero/v2/queue_handle.hpp
+++ b/unified-runtime/source/adapters/level_zero/v2/queue_handle.hpp
@@ -49,4 +49,6 @@ struct ur_queue_handle_t_ {
         },
         queue_data);
   }
+  // Indicates if this object is an interop handle.
+  bool IsInteropNativeHandle = false;
 };
diff --git a/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.cpp b/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.cpp
index 88206460ffa65..3209ec9b7d121 100644
--- a/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.cpp
+++ b/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.cpp
@@ -75,15 +75,19 @@ ur_queue_immediate_in_order_t::ur_queue_immediate_in_order_t(
 
 ur_queue_immediate_in_order_t::ur_queue_immediate_in_order_t(
     ur_context_handle_t hContext, ur_device_handle_t hDevice,
-    ur_native_handle_t hNativeHandle, ur_queue_flags_t flags, bool ownZeQueue)
+    ur_native_handle_t hNativeHandle, ur_queue_flags_t flags, bool ownZeQueue,
+    bool interopNativeHandle)
     : hContext(hContext), hDevice(hDevice), flags(flags),
       commandListManager(
           hContext, hDevice,
           raii::command_list_unique_handle(
               reinterpret_cast<ze_command_list_handle_t>(hNativeHandle),
-              [ownZeQueue](ze_command_list_handle_t hZeCommandList) {
+              [ownZeQueue,
+               interopNativeHandle](ze_command_list_handle_t hZeCommandList) {
                 if (ownZeQueue) {
-                  ZE_CALL_NOCHECK(zeCommandListDestroy, (hZeCommandList));
+                  if (!interopNativeHandle) {
+                    ZE_CALL_NOCHECK(zeCommandListDestroy, (hZeCommandList));
+                  }
                 }
               }),
           eventFlagsFromQueueFlags(flags)) {}
diff --git a/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.hpp b/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.hpp
index dae9782c5e002..3c843a477616b 100644
--- a/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.hpp
+++ b/unified-runtime/source/adapters/level_zero/v2/queue_immediate_in_order.hpp
@@ -70,7 +70,7 @@ struct ur_queue_immediate_in_order_t : _ur_object, public ur_queue_t_ {
                                 const ur_queue_properties_t *);
   ur_queue_immediate_in_order_t(ur_context_handle_t, ur_device_handle_t,
                                 ur_native_handle_t, ur_queue_flags_t,
-                                bool ownZeQueue);
+                                bool ownZeQueue, bool interopNativeHandle);
 
   ~ur_queue_immediate_in_order_t();
 


Reviews:
user: login: igchor
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:05:13Z
user: login: igchor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T13:10:27Z
user: login: nrspruit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:15:09Z
user: login: igchor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:19:34Z
user: login: nrspruit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:19:53Z
user: login: nrspruit
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:21:28Z
user: login: igchor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:23:36Z
user: login: igchor
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T14:23:47Z

Review comments:
diff_hunk: @@ -435,6 +504,9 @@ struct _ur_object {
   // Indicates if we own the native handle or it came from interop that
   // asked to not transfer the ownership to SYCL RT.
   bool OwnNativeHandle = false;
+
+  // Indicates if this object is an interop handle.
+  bool IsInteropNativeHandle = false;
path: unified-runtime/source/adapters/level_zero/common.hpp
user: login: igchor
body: Do we need to add a new variable? Can we just adjust OwnNativeHandle in the constructors and call checkL0LoaderTeardown if and only if OwnNativeHandle is set?
created_at: 2025-03-21T13:05:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -398,6 +398,7 @@ urEventCreateWithNativeHandle(ur_native_handle_t hNativeEvent,
     ZE2UR_CALL(zeEventHostSignal, ((*phEvent)->getZeEvent()));
   } else {
     *phEvent = new ur_event_handle_t_(hContext, hNativeEvent, pProperties);
+    (*phEvent)->IsInteropNativeHandle = true;
path: unified-runtime/source/adapters/level_zero/v2/event.cpp
user: login: igchor
body: Is this used anywhere?
created_at: 2025-03-21T13:09:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -398,6 +398,7 @@ urEventCreateWithNativeHandle(ur_native_handle_t hNativeEvent,
     ZE2UR_CALL(zeEventHostSignal, ((*phEvent)->getZeEvent()));
   } else {
     *phEvent = new ur_event_handle_t_(hContext, hNativeEvent, pProperties);
+    (*phEvent)->IsInteropNativeHandle = true;
path: unified-runtime/source/adapters/level_zero/v2/event.cpp
user: login: nrspruit
body: in all the event destroy calls.
created_at: 2025-03-21T14:15:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -398,6 +398,7 @@ urEventCreateWithNativeHandle(ur_native_handle_t hNativeEvent,
     ZE2UR_CALL(zeEventHostSignal, ((*phEvent)->getZeEvent()));
   } else {
     *phEvent = new ur_event_handle_t_(hContext, hNativeEvent, pProperties);
+    (*phEvent)->IsInteropNativeHandle = true;
path: unified-runtime/source/adapters/level_zero/v2/event.cpp
user: login: igchor
body: my bad, I thought this was added as a member to ur_event_handle_t
created_at: 2025-03-21T14:19:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -435,6 +504,9 @@ struct _ur_object {
   // Indicates if we own the native handle or it came from interop that
   // asked to not transfer the ownership to SYCL RT.
   bool OwnNativeHandle = false;
+
+  // Indicates if this object is an interop handle.
+  bool IsInteropNativeHandle = false;
path: unified-runtime/source/adapters/level_zero/common.hpp
user: login: nrspruit
body: So, we have to have a second variable because we have overloaded the variable to say both:
1. we own this and created this and can delete it
2. we have been given ownership of this and can delete it.

"// Indicates if we own the native handle or it came from interop"

So, if we created this handle, then we need to call destroy, otherwise we will appear to have a memory leak with ourselves.

However, if we did not create this handle, then we "can" try to destroy it only if the loader is stable.

Reworking the overloaded variable would be a larger code change and could inadvertently cause problems in then logic so I opted to add another to specifically check for the one case.
created_at: 2025-03-21T14:19:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -398,6 +398,7 @@ urEventCreateWithNativeHandle(ur_native_handle_t hNativeEvent,
     ZE2UR_CALL(zeEventHostSignal, ((*phEvent)->getZeEvent()));
   } else {
     *phEvent = new ur_event_handle_t_(hContext, hNativeEvent, pProperties);
+    (*phEvent)->IsInteropNativeHandle = true;
path: unified-runtime/source/adapters/level_zero/v2/event.cpp
user: login: nrspruit
body: in the v2 common.hpp, this variable is checked.
created_at: 2025-03-21T14:21:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -435,6 +504,9 @@ struct _ur_object {
   // Indicates if we own the native handle or it came from interop that
   // asked to not transfer the ownership to SYCL RT.
   bool OwnNativeHandle = false;
+
+  // Indicates if this object is an interop handle.
+  bool IsInteropNativeHandle = false;
path: unified-runtime/source/adapters/level_zero/common.hpp
user: login: igchor
body: Right, but for the first case, OwnNativeHandle would always be false. But in any case, if you prefer to have a separate variable I'm ok with that - especially since this is hidden in the raii handle for v2.
created_at: 2025-03-21T14:23:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: igchor
created_at: 2025-03-20T10:18:21Z
body: @nrspruit This seems like a more general problem, not only related to interop. As I understand, this check that we're doing in the loader now behaves differently for static loader: https://github.com/oneapi-src/level-zero/blob/3c938e21d827af014971d69dfd66759c2444e4d0/source/lib/ze_libapi.cpp#L220, correct? 

Can't we move the `destruction` variable to somewhere in the dynamic loader so that we can track dynamic loader lifetime instead of the static loader?


author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-20T15:22:30Z
body: > @nrspruit This seems like a more general problem, not only related to interop. As I understand, this check that we're doing in the loader now behaves differently for static loader: https://github.com/oneapi-src/level-zero/blob/3c938e21d827af014971d69dfd66759c2444e4d0/source/lib/ze_libapi.cpp#L220, correct?
> 
> Can't we move the `destruction` variable to somewhere in the dynamic loader so that we can track dynamic loader lifetime instead of the static loader?

That won't fix any existing library and it is stopping invalid behavior.

We should never be trying to free memory after destruction, this will never 100% work properly. Before we did not free the memory, but exited early.

Even if we added a call in the dynamic library, that would not fix any existing dynamic library so the issue would still exist in all loaders in the market until a new loader with the new workaround exists. We will not usually have the same static and dynamic loader so changing the loader dynamic to have another workaround will only fix newer loaders.

The teardown handling has always been a workaround due to invalid use cases.

An application cannot be using a handle allocated in another library and expect to be able to free it in that application after the parent application has exited. No library supports that, before it "pretended" that the memory was freed, but never actually could free any memory.

Preferrably, we just would never allow for destroying L0 interop handles if the application is in at_exit.
author_association: CONTRIBUTOR
user: login: igchor
created_at: 2025-03-20T15:40:00Z
body: > > @nrspruit This seems like a more general problem, not only related to interop. As I understand, this check that we're doing in the loader now behaves differently for static loader: https://github.com/oneapi-src/level-zero/blob/3c938e21d827af014971d69dfd66759c2444e4d0/source/lib/ze_libapi.cpp#L220, correct?
> > Can't we move the `destruction` variable to somewhere in the dynamic loader so that we can track dynamic loader lifetime instead of the static loader?
> 
> That won't fix any existing library and it is stopping invalid behavior.
> 
> We should never be trying to free memory after destruction, this will never 100% work properly. Before we did not free the memory, but exited early.
> 
> Even if we added a call in the dynamic library, that would not fix any existing dynamic library so the issue would still exist in all loaders in the market until a new loader with the new workaround exists. We will not usually have the same static and dynamic loader so changing the loader dynamic to have another workaround will only fix newer loaders.
> 
> The teardown handling has always been a workaround due to invalid use cases.
> 
> An application cannot be using a handle allocated in another library and expect to be able to free it in that application after the parent application has exited. No library supports that, before it "pretended" that the memory was freed, but never actually could free any memory.
> 
> Preferrably, we just would never allow for destroying L0 interop handles if the application is in at_exit.

I completely agree, that the best way would be to just never allow calling L0 APIs after destructor is called. Is this something we can do?

What I don't quite understand is, why this worked before switching to the static loader. From what I can see, there was already logic based on `ze_lib::destruction` variable. My question is: why can't we leverage that logic (which is present in older loaders)? 
author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-20T15:53:17Z
body: > > > @nrspruit This seems like a more general problem, not only related to interop. As I understand, this check that we're doing in the loader now behaves differently for static loader: https://github.com/oneapi-src/level-zero/blob/3c938e21d827af014971d69dfd66759c2444e4d0/source/lib/ze_libapi.cpp#L220, correct?
> > > Can't we move the `destruction` variable to somewhere in the dynamic loader so that we can track dynamic loader lifetime instead of the static loader?
> > 
> > 
> > That won't fix any existing library and it is stopping invalid behavior.
> > We should never be trying to free memory after destruction, this will never 100% work properly. Before we did not free the memory, but exited early.
> > Even if we added a call in the dynamic library, that would not fix any existing dynamic library so the issue would still exist in all loaders in the market until a new loader with the new workaround exists. We will not usually have the same static and dynamic loader so changing the loader dynamic to have another workaround will only fix newer loaders.
> > The teardown handling has always been a workaround due to invalid use cases.
> > An application cannot be using a handle allocated in another library and expect to be able to free it in that application after the parent application has exited. No library supports that, before it "pretended" that the memory was freed, but never actually could free any memory.
> > Preferrably, we just would never allow for destroying L0 interop handles if the application is in at_exit.
> 
> I completely agree, that the best way would be to just never allow calling L0 APIs after destructor is called. Is this something we can do?
> 
> What I don't quite understand is, why this worked before switching to the static loader. From what I can see, there was already logic based on `ze_lib::destruction` variable. My question is: why can't we leverage that logic (which is present in older loaders)?

This "worked" before because the call to zeModuleDestory etc exited early because the zelLoaderTeardown was called in the dynamic loader which set the ze_lib::destruction=true.

With the static loader, the L0 driver cannot load the "L0 application" to inform the application ie "UR" that the driver is being torndown. As the user of the static loader, it is up to us to avoid calling destroy of inherited handles after atexit has begun otherwise there is no guarantee that the handle will be valid, the OS will never provide that gurantee so we need to avoid calling teardown.

previously, the loader still did not allow the destroy to continue to actually destroy anything, it simply returned before trying to read the handle.
author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-20T15:56:54Z
body: > What I don't quite understand is, why this worked before switching to the static loader. From what I can see, there was already logic based on `ze_lib::destruction` variable. My question is: why can't we leverage that logic (which is present in older loaders)?

this has been a workaround that only works if the application is linking the loader with dyanmic because the L0 driver would call the ze_lib.

Basically, in this situation, the ze_loader.dll is already gone ie:
loader dllmain destroy
loader context destroy
   ---> urKernelRelease
   <--- urKernelRelease(.hKernel = 0000019ABA4BC2E0) -> UR_RESULT_SUCCESS;
   ---> urProgramRelease
   <--- urProgramRelease(.hProgram = 0000019ABA4BC640) -> UR_RESULT_SUCCESS;
   ---> urKernelRelease
   
   We are calling release on memory when the dyanmic loader is gone, no amount of refcnting can prevent this, loadlibrary and dlopen don't affect anything after atexit() has begun, the refcnt it not honored in linux or windows.
author_association: CONTRIBUTOR
user: login: pbalcer
created_at: 2025-03-20T16:02:38Z
body: Will leak checking tools (like valgrind memcheck) report a leak on global interop handles that weren't released manually?
author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-20T16:13:43Z
body: > Will leak checking tools (like valgrind memcheck) report a leak on global interop handles that weren't released manually?

valgrind wont if they did not before because the memory was never being released even before with the dynamic loader.

if we use the L0 free check using the validation layer, that may fail in these use cases because the "call" to destroy is missing, but I believe that should be accepted as an expected missing call in these situations.

author_association: CONTRIBUTOR
user: login: pbalcer
created_at: 2025-03-20T16:26:58Z
body: > if we use the L0 free check using the validation layer, that may fail in these use cases because the "call" to destroy is missing, but I believe that should be accepted as an expected missing call in these situations.

Yes, I agree.

Ok, thanks, lgtm in that case.
author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-21T05:11:41Z
body: I have a potential patch that will workaround and detect a defunct L0 loader. I am testing it now. If so, I can avoid the memory leak in this case.
author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-21T05:50:58Z
body: > I have a potential patch that will workaround and detect a defunct L0 loader. I am testing it now. If so, I can avoid the memory leak in this case.

I found a method that can check during teardown to determine if the loader is stable. This resolves the known issues while enabling release of memory when the loader is stable.
author_association: CONTRIBUTOR
user: login: nrspruit
created_at: 2025-03-21T17:23:51Z
body: @intel/llvm-gatekeepers , please merge when available to address this issue seen by users.
author_association: CONTRIBUTOR