Repository: oneapi-src/unified-memory-framework (language: python)

Issue:
USM support
UMF should support managing USM (unified shared memory) by:
- exposing memory pools capable of handling memory that might not be accessible on the host:
    - [x] disjoint_pool
    - [x] jemalloc_pool
       - note: there is a known issue with jemalloc: if compiled with default flags it cannot be dlopened (neither can a library that links with jemalloc) which makes it problematic to use in Unified Runtime which manages adapters using dlopen
- exposing memory providers capable of allocating memory on a device:
   - [x] L0 provider (currently implemented in Unified Runtime)
   - [ ] CUDA provider (currently implemented in Unified Runtime)
   - [ ] Unified Runtime (?)

It should be possible to build L0 and CUDA providers as static libraries so that they can be used by Unified Runtime.

We should also consider:
- adding memory copy / memset operations for providers (to implement calloc/realloc on device-only memory)
- async memory management (transfers and/or allocation)


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml
index dcb8d9c1a..e3be46591 100644
--- a/.github/workflows/basic.yml
+++ b/.github/workflows/basic.yml
@@ -54,6 +54,7 @@ jobs:
         -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
         -DUMF_BUILD_LIBUMF_POOL_SCALABLE=ON
         -DUMF_BUILD_EXAMPLES=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build build -j $(nproc)
@@ -133,6 +134,7 @@ jobs:
       if: matrix.compiler.cxx == 'g++-7'
       run: sudo apt-get install -y ${{matrix.compiler.cxx}}
 
+    # TODO enable building Level Zero provider in all workflows
     - name: Configure build
       run: >
         cmake
@@ -149,6 +151,7 @@ jobs:
         -DUMF_BUILD_LIBUMF_POOL_JEMALLOC=ON
         -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
         -DUMF_BUILD_LIBUMF_POOL_SCALABLE=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build ${{env.BUILD_DIR}} -j $(nproc)
@@ -221,6 +224,7 @@ jobs:
         -DUMF_DEVELOPER_MODE=ON
         -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
         -DUMF_BUILD_OS_MEMORY_PROVIDER=${{matrix.os_provider}}
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build ${{env.BUILD_DIR}} --config ${{matrix.build_type}} -j $Env:NUMBER_OF_PROCESSORS
@@ -250,7 +254,8 @@ jobs:
         -DCMAKE_BUILD_TYPE=Release
         -DUMF_FORMAT_CODE_STYLE=OFF
         -DUMF_DEVELOPER_MODE=ON
-        -DUMF_ENABLE_POOL_TRACKING=ON
+        -DUMF_ENABLE_POOL_TRACKING=ON        
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build ${{env.BUILD_DIR}} -j $(sysctl -n hw.logicalcpu)
diff --git a/.github/workflows/benchmarks.yml b/.github/workflows/benchmarks.yml
index 4fc9e3487..6d02e8824 100644
--- a/.github/workflows/benchmarks.yml
+++ b/.github/workflows/benchmarks.yml
@@ -43,6 +43,7 @@ jobs:
           -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
           -DUMF_BUILD_LIBUMF_POOL_SCALABLE=ON
           -DUMF_ENABLE_POOL_TRACKING=OFF
+          -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
       - name: Build UMF
         run: cmake --build ${{github.workspace}}/build -j $(nproc)
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 3542f6bdb..036a3265c 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -59,6 +59,7 @@ jobs:
         -DUMF_DEVELOPER_MODE=ON
         -DUMF_ENABLE_POOL_TRACKING=ON
         -DUMF_BUILD_LIBUMF_POOL_SCALABLE=${{matrix.pool_scalable}}
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build
       run: cmake --build ${{github.workspace}}/build --config Release -j
diff --git a/.github/workflows/gpu.yml b/.github/workflows/gpu.yml
new file mode 100644
index 000000000..e2ce84f2c
--- /dev/null
+++ b/.github/workflows/gpu.yml
@@ -0,0 +1,49 @@
+# This workflow builds and tests providers using GPU memory. It requires 
+# "level_zero" labeled self-hosted runners installed on systems with the 
+# appropriate GPU and drivers.
+name: GPU
+
+on: [workflow_call]
+
+permissions:
+  contents: read
+
+jobs:
+  # TODO: add support for Windows
+  ubuntu-build:
+    name: Build - Ubuntu
+
+    strategy:
+      matrix:
+        os: ['ubuntu-22.04']
+        build_type: [Release]
+        compiler: [{c: gcc, cxx: g++}]
+        shared_library: ['ON', 'OFF']
+    runs-on: level_zero
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+      - name: Configure build
+        run: >
+          cmake
+          -B ${{github.workspace}}/build
+          -DCMAKE_BUILD_TYPE=${{matrix.build_type}}
+          -DCMAKE_C_COMPILER=${{matrix.compiler.c}}
+          -DCMAKE_CXX_COMPILER=${{matrix.compiler.cxx}}
+          -DUMF_BUILD_SHARED_LIBRARY=${{matrix.shared_library}}
+          -DUMF_BUILD_BENCHMARKS=ON
+          -DUMF_BUILD_TESTS=ON
+          -DUMF_FORMAT_CODE_STYLE=ON
+          -DUMF_DEVELOPER_MODE=ON
+          -DUMF_BUILD_LEVEL_ZERO_PROVIDER=ON
+          -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
+          -DUMF_ENABLE_POOL_TRACKING=ON
+
+      - name: Build UMF
+        run: cmake --build ${{github.workspace}}/build -j $(nproc)
+
+      - name: Run tests
+        working-directory: ${{github.workspace}}/build
+        run: ctest --output-on-failure --test-dir test
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index a1facdc50..d12ff92e6 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -39,6 +39,7 @@ jobs:
         -DUMF_BUILD_LIBUMF_POOL_SCALABLE=ON
         -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
         -DUMF_BUILD_LIBUMF_POOL_JEMALLOC=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build
       run: cmake --build ${{github.workspace}}/build --config Debug -j$(nproc)
diff --git a/.github/workflows/pr_push.yml b/.github/workflows/pr_push.yml
index 0cbaf4347..435693ee9 100644
--- a/.github/workflows/pr_push.yml
+++ b/.github/workflows/pr_push.yml
@@ -65,6 +65,7 @@ jobs:
         -DUMF_BUILD_LIBUMF_POOL_JEMALLOC=${{matrix.jemalloc}}
         -DUMF_BUILD_TESTS=ON
         -DUMF_BUILD_EXAMPLES=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
         ${{matrix.extra_build_options}}
 
     - name: Build
@@ -94,6 +95,7 @@ jobs:
         -DUMF_FORMAT_CODE_STYLE=ON
         -DUMF_BUILD_OS_MEMORY_PROVIDER=OFF
         -DUMF_BUILD_TESTS=OFF
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Check clang-format
       run: cmake --build build --target clang-format-check
@@ -113,3 +115,6 @@ jobs:
   ProxyLib:
     needs: [Build]
     uses: ./.github/workflows/proxy_lib.yml
+  GPU:
+    needs: [Build]
+    uses: ./.github/workflows/gpu.yml
diff --git a/.github/workflows/proxy_lib.yml b/.github/workflows/proxy_lib.yml
index aeebe8d23..c2435e167 100644
--- a/.github/workflows/proxy_lib.yml
+++ b/.github/workflows/proxy_lib.yml
@@ -42,6 +42,7 @@ jobs:
           -DUMF_BUILD_LIBUMF_POOL_DISJOINT=ON
           -DUMF_BUILD_LIBUMF_POOL_SCALABLE=ON
           -DUMF_ENABLE_POOL_TRACKING=ON
+          -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
       - name: Build UMF
         run: cmake --build ${{github.workspace}}/build -j $(nproc)
diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml
index 7e640b30c..ce22e3153 100644
--- a/.github/workflows/sanitizers.yml
+++ b/.github/workflows/sanitizers.yml
@@ -53,6 +53,7 @@ jobs:
         -DUSE_UBSAN=${{matrix.sanitizers.ubsan}}
         -DUSE_TSAN=${{matrix.sanitizers.tsan}}
         -DUMF_BUILD_EXAMPLES=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build ${{env.BUILD_DIR}} -j $(nproc)
@@ -98,6 +99,7 @@ jobs:
         -DUSE_UBSAN=${{matrix.sanitizers.ubsan}}
         -DUSE_TSAN=${{matrix.sanitizers.tsan}}
         -DUMF_BUILD_EXAMPLES=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build ${{env.BUILD_DIR}} -j $(nproc)
@@ -145,6 +147,7 @@ jobs:
         -DUSE_ASAN=${{matrix.sanitizers.asan}}
         -DUMF_BUILD_OS_MEMORY_PROVIDER=ON
         -DUMF_BUILD_EXAMPLES=ON
+        -DUMF_BUILD_LEVEL_ZERO_PROVIDER=OFF
 
     - name: Build UMF
       run: cmake --build ${{env.BUILD_DIR}} --config Debug -j $Env:NUMBER_OF_PROCESSORS
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 71b6dd7f0..a8a85c680 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -8,6 +8,7 @@ project(unified-memory-framework VERSION 0.1.0 LANGUAGES C)
 # Build Options
 option(UMF_BUILD_SHARED_LIBRARY "Build UMF as shared library" OFF)
 option(UMF_BUILD_OS_MEMORY_PROVIDER "Build OS memory provider" ON)
+option(UMF_BUILD_LEVEL_ZERO_PROVIDER "Build Level Zero memory provider" ON)
 option(UMF_BUILD_LIBUMF_POOL_DISJOINT "Build the libumf_pool_disjoint static library" OFF)
 option(UMF_BUILD_LIBUMF_POOL_JEMALLOC "Build the libumf_pool_jemalloc static library" OFF)
 option(UMF_BUILD_LIBUMF_POOL_SCALABLE "Build the libumf_pool_scalable static library" OFF)
diff --git a/README.md b/README.md
index fdff223cc..1b204de24 100644
--- a/README.md
+++ b/README.md
@@ -5,6 +5,7 @@
 [![SpellCheck](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/spellcheck.yml/badge.svg?branch=main)](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/spellcheck.yml)
 [![GitHubPages](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/docs.yml/badge.svg?branch=main)](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/docs.yml)
 [![Benchmarks](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/benchmarks.yml/badge.svg?branch=main)](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/benchmarks.yml)
+[![GPU](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/gpu.yml/badge.svg?branch=main)](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/gpu.yml)
 [![Nightly](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/nightly.yml/badge.svg?branch=main)](https://github.com/oneapi-src/unified-memory-framework/actions/workflows/nightly.yml)
 [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/oneapi-src/unified-memory-framework/badge)](https://securityscorecards.dev/viewer/?uri=github.com/oneapi-src/unified-memory-framework)
 
@@ -31,6 +32,10 @@ For development and contributions:
 For building tests, multithreaded benchmarks and Disjoint Pool:
 - C++ compiler with C++17 support
 
+For Level Zero memory provider and its tests:
+- Level Zero headers and libraries
+- compatible GPU with installed driver
+
 ### Linux
 
 Executable and binaries will be in **build/bin**
@@ -84,6 +89,7 @@ List of options provided by CMake:
 | - | - | - | - |
 | UMF_BUILD_SHARED_LIBRARY | Build UMF as shared library | ON/OFF | OFF |
 | UMF_BUILD_OS_MEMORY_PROVIDER | Build OS memory provider | ON/OFF | ON |
+| UMF_BUILD_LIBUMF_PROVIDER_LEVEL_ZERO | Build Level Zero memory provider | ON/OFF | OFF |
 | UMF_BUILD_LIBUMF_POOL_DISJOINT | Build the libumf_pool_disjoint static library | ON/OFF | OFF |
 | UMF_BUILD_LIBUMF_POOL_JEMALLOC | Build the libumf_pool_jemalloc static library | ON/OFF | OFF |
 | UMF_BUILD_LIBUMF_POOL_SCALABLE | Build the libumf_pool_scalable static library | ON/OFF | OFF |
@@ -123,6 +129,18 @@ A memory provider that provides memory from an operating system.
 4) Required packages for tests:
    - libnuma-dev
 
+#### Level Zero memory provider (Linux-only yet)
+
+A memory provider that provides memory from L0 device.
+
+##### Requirements
+
+1) System with Level Zero compatible GPU
+2) Linux OS
+3) The `UMF_BUILD_LIBUMF_PROVIDER_LEVEL_ZERO` option turned `ON`
+4) Required packages:
+   - level-zero-dev (TODO: check this)
+
 ### Memory pool managers
 
 #### proxy_pool (part of libumf)
diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt
index 486915a51..c53fed140 100644
--- a/benchmark/CMakeLists.txt
+++ b/benchmark/CMakeLists.txt
@@ -52,6 +52,10 @@ if (UMF_BUILD_OS_MEMORY_PROVIDER)
 	endif()
 endif()
 
+if (UMF_BUILD_OS_MEMORY_PROVIDER)
+	target_compile_definitions(ubench PRIVATE UMF_BUILD_OS_MEMORY_PROVIDER=1)
+endif()
+
 if (UMF_BUILD_LIBUMF_POOL_DISJOINT)
 	target_compile_definitions(ubench PRIVATE UMF_BUILD_LIBUMF_POOL_DISJOINT=1)
 
diff --git a/include/umf/providers/provider_level_zero.h b/include/umf/providers/provider_level_zero.h
new file mode 100644
index 000000000..039f2c1df
--- /dev/null
+++ b/include/umf/providers/provider_level_zero.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 Intel Corporation
+ *
+ * Under the Apache License v2.0 with LLVM Exceptions. See LICENSE.TXT.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+*/
+
+#ifndef UMF_PROVIDER_LEVEL_ZERO_H
+#define UMF_PROVIDER_LEVEL_ZERO_H
+
+#include "umf/memory_provider.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/// @brief USM memory allocation type
+typedef enum umf_usm_memory_type_t {
+    UMF_MEMORY_TYPE_UNKNOWN = 0,
+    UMF_MEMORY_TYPE_HOST,
+    UMF_MEMORY_TYPE_DEVICE,
+    UMF_MEMORY_TYPE_SHARED,
+} umf_usm_memory_type_t;
+
+/// @brief Level Zero Memory Provider settings struct
+typedef struct level_zero_memory_provider_params_t {
+    void *level_zero_context_handle;
+    void *level_zero_device_handle;
+    umf_usm_memory_type_t memory_type;
+    uint32_t level_zero_host_mem_alloc_flags;
+    uint32_t level_zero_device_mem_alloc_flags;
+    uint32_t level_zero_device_local_mem_ordinal;
+} level_zero_memory_provider_params_t;
+
+umf_memory_provider_ops_t *umfLevelZeroMemoryProviderOps(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* UMF_PROVIDER_LEVEL_ZERO_H */
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4a2fa84c5..941de0e40 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -77,6 +77,18 @@ if(UMF_BUILD_OS_MEMORY_PROVIDER)
     endif()
 endif()
 
+if(UMF_BUILD_LEVEL_ZERO_PROVIDER)
+    # Currently, Level Zero provider is only built for Linux
+    # TODO add fetch content or PkgConfig/find_package for Level Zero
+    set(UMF_SOURCES_LINUX ${UMF_SOURCES_LINUX}
+        provider/provider_level_zero.c)
+
+    if(LINUX)
+        set(UMF_COMPILE_DEFINITIONS ${UMF_COMPILE_DEFINITIONS} 
+            "UMF_BUILD_LEVEL_ZERO_PROVIDER=1")
+    endif()
+endif()
+
 if(LINUX)
     set(UMF_SOURCES ${UMF_SOURCES} ${UMF_SOURCES_LINUX})
 elseif(WINDOWS)
diff --git a/src/libumf.map b/src/libumf.map
index 9b677392c..687bfe0e8 100644
--- a/src/libumf.map
+++ b/src/libumf.map
@@ -6,6 +6,7 @@ UMF_1.0 {
     global:
         umfFree;
         umfGetLastFailedMemoryProvider;
+        umfLevelZeroMemoryProviderOps;
         umfMemoryProviderAlloc;
         umfMemoryProviderAllocationMerge;
         umfMemoryProviderAllocationSplit;
diff --git a/src/pool/pool_scalable.c b/src/pool/pool_scalable.c
index 2c9780a95..ce6c913d2 100644
--- a/src/pool/pool_scalable.c
+++ b/src/pool/pool_scalable.c
@@ -13,7 +13,6 @@
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
-#include <stdlib.h>
 #include <string.h>
 
 #include "umf/pools/pool_scalable.h"
diff --git a/src/provider/provider_level_zero.c b/src/provider/provider_level_zero.c
new file mode 100644
index 000000000..bbca701d6
--- /dev/null
+++ b/src/provider/provider_level_zero.c
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2024 Intel Corporation
+ *
+ * Under the Apache License v2.0 with LLVM Exceptions. See LICENSE.TXT.
+ * SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+*/
+
+#include <assert.h>
+#include <dlfcn.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <unistd.h>
+
+#include <level_zero/ze_api.h>
+
+#include <umf.h>
+#include <umf/memory_provider_ops.h>
+#include <umf/providers/provider_level_zero.h>
+
+#include "base_alloc_global.h"
+#include "utils_common.h"
+#include "utils_concurrency.h"
+#include "utils_sanitizers.h"
+
+typedef struct ze_memory_provider_t {
+    ze_context_handle_t context;
+    ze_device_handle_t device;
+    ze_memory_type_t memory_type;
+} ze_memory_provider_t;
+
+typedef struct ze_ops_t {
+    ze_result_t (*zeMemAllocHost)(ze_context_handle_t,
+                                  const ze_host_mem_alloc_desc_t *, size_t,
+                                  size_t, void *);
+    ze_result_t (*zeMemAllocDevice)(ze_context_handle_t,
+                                    const ze_device_mem_alloc_desc_t *, size_t,
+                                    size_t, ze_device_handle_t, void *);
+    ze_result_t (*zeMemAllocShared)(ze_context_handle_t,
+                                    const ze_device_mem_alloc_desc_t *,
+                                    const ze_host_mem_alloc_desc_t *, size_t,
+                                    size_t, ze_device_handle_t, void *);
+    ze_result_t (*zeMemFree)(ze_context_handle_t, void *);
+} ze_ops_t;
+
+static ze_ops_t g_ze_ops;
+static UTIL_ONCE_FLAG ze_is_initialized = UTIL_ONCE_FLAG_INIT;
+static bool Init_ze_global_state_failed;
+
+static void init_ze_global_state(void) {
+    // check if Level Zero shared library is already loaded
+    // we pass 0 as a handle to search the global symbol table
+    *(void **)&g_ze_ops.zeMemAllocHost = dlsym(0, "zeMemAllocHost");
+    *(void **)&g_ze_ops.zeMemAllocDevice = dlsym(0, "zeMemAllocDevice");
+    *(void **)&g_ze_ops.zeMemAllocShared = dlsym(0, "zeMemAllocShared");
+    *(void **)&g_ze_ops.zeMemFree = dlsym(0, "zeMemFree");
+
+    if (!g_ze_ops.zeMemAllocHost || !g_ze_ops.zeMemAllocDevice ||
+        !g_ze_ops.zeMemAllocShared || !g_ze_ops.zeMemFree) {
+        fprintf(stderr, "Required Level Zero symbols not found.\n");
+        Init_ze_global_state_failed = true;
+    }
+}
+
+enum umf_result_t ze_memory_provider_initialize(void *params, void **provider) {
+    if (provider == NULL || params == NULL) {
+        return UMF_RESULT_ERROR_INVALID_ARGUMENT;
+    }
+
+    level_zero_memory_provider_params_t *ze_params =
+        (level_zero_memory_provider_params_t *)params;
+
+    util_init_once(&ze_is_initialized, init_ze_global_state);
+    if (Init_ze_global_state_failed) {
+        fprintf(stderr, "Loading Level Zero symbols failed\n");
+        return UMF_RESULT_ERROR_UNKNOWN;
+    }
+
+    ze_memory_provider_t *ze_provider =
+        umf_ba_global_alloc(sizeof(ze_memory_provider_t));
+    if (!ze_provider) {
+        return UMF_RESULT_ERROR_OUT_OF_HOST_MEMORY;
+    }
+
+    ze_provider->context = ze_params->level_zero_context_handle;
+    ze_provider->device = ze_params->level_zero_device_handle;
+    ze_provider->memory_type = ze_params->memory_type;
+
+    *provider = ze_provider;
+
+    return UMF_RESULT_SUCCESS;
+}
+
+void ze_memory_provider_finalize(void *provider) {
+    assert(provider);
+
+    util_init_once(&ze_is_initialized, init_ze_global_state);
+    umf_ba_global_free(provider);
+}
+
+static enum umf_result_t ze_memory_provider_alloc(void *provider, size_t size,
+                                                  size_t alignment,
+                                                  void **resultPtr) {
+    assert(provider);
+    assert(resultPtr);
+
+    ze_memory_provider_t *ze_provider = (ze_memory_provider_t *)provider;
+
+    ze_result_t ze_result = ZE_RESULT_SUCCESS;
+    switch (ze_provider->memory_type) {
+    case UMF_MEMORY_TYPE_HOST: {
+        ze_host_mem_alloc_desc_t host_desc = {
+            .stype = ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC,
+            .pNext = NULL,
+            .flags = 0};
+        ze_result = g_ze_ops.zeMemAllocHost(ze_provider->context, &host_desc,
+                                            size, alignment, resultPtr);
+        break;
+    }
+    case UMF_MEMORY_TYPE_DEVICE: {
+        ze_device_mem_alloc_desc_t dev_desc = {
+            .stype = ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC,
+            .pNext = NULL,
+            .flags = 0,
+            .ordinal = 0 // TODO
+        };
+        ze_result = g_ze_ops.zeMemAllocDevice(ze_provider->context, &dev_desc,
+                                              size, alignment,
+                                              ze_provider->device, resultPtr);
+        break;
+    }
+    case UMF_MEMORY_TYPE_SHARED: {
+        ze_host_mem_alloc_desc_t host_desc = {
+            .stype = ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC,
+            .pNext = NULL,
+            .flags = 0};
+        ze_device_mem_alloc_desc_t dev_desc = {
+            .stype = ZE_STRUCTURE_TYPE_HOST_MEM_ALLOC_DESC,
+            .pNext = NULL,
+            .flags = 0,
+            .ordinal = 0 // TODO
+        };
+        ze_result = g_ze_ops.zeMemAllocShared(ze_provider->context, &dev_desc,
+                                              &host_desc, size, alignment,
+                                              ze_provider->device, resultPtr);
+        break;
+    }
+    default:
+        return UMF_RESULT_ERROR_MEMORY_PROVIDER_SPECIFIC;
+    }
+
+    // TODO add error reporting
+    return (ze_result == ZE_RESULT_SUCCESS)
+               ? UMF_RESULT_SUCCESS
+               : UMF_RESULT_ERROR_MEMORY_PROVIDER_SPECIFIC;
+}
+
+static enum umf_result_t ze_memory_provider_free(void *provider, void *ptr,
+                                                 size_t bytes) {
+    (void)bytes;
+
+    assert(provider);
+    ze_memory_provider_t *ze_provider = (ze_memory_provider_t *)provider;
+    ze_result_t ze_result = g_ze_ops.zeMemFree(ze_provider->context, ptr);
+
+    // TODO add error reporting
+    return (ze_result == ZE_RESULT_SUCCESS)
+               ? UMF_RESULT_SUCCESS
+               : UMF_RESULT_ERROR_MEMORY_PROVIDER_SPECIFIC;
+}
+
+void ze_memory_provider_get_last_native_error(void *provider,
+                                              const char **ppMessage,
+                                              int32_t *pError) {
+    (void)provider;
+    (void)ppMessage;
+
+    // TODO
+    assert(pError);
+    *pError = 0;
+}
+
+static enum umf_result_t
+ze_memory_provider_get_min_page_size(void *provider, void *ptr,
+                                     size_t *pageSize) {
+    (void)provider;
+    (void)ptr;
+
+    // TODO
+    *pageSize = 1024 * 64;
+    return UMF_RESULT_SUCCESS;
+}
+
+static umf_result_t ze_memory_provider_purge_lazy(void *provider, void *ptr,
+                                                  size_t size) {
+    (void)provider;
+    (void)ptr;
+    (void)size;
+
+    // TODO not supported yet
+    return UMF_RESULT_ERROR_NOT_SUPPORTED;
+}
+
+static umf_result_t ze_memory_provider_purge_force(void *provider, void *ptr,
+                                                   size_t size) {
+    (void)provider;
+    (void)ptr;
+    (void)size;
+
+    // TODO not supported yet
+    return UMF_RESULT_ERROR_NOT_SUPPORTED;
+}
+
+static enum umf_result_t
+ze_memory_provider_get_recommended_page_size(void *provider, size_t size,
+                                             size_t *pageSize) {
+    (void)provider;
+    (void)size;
+
+    // TODO
+    *pageSize = 1024 * 64;
+    return UMF_RESULT_SUCCESS;
+}
+
+const char *ze_memory_provider_get_name(void *provider) {
+    (void)provider;
+    return "LEVEL_ZERO";
+}
+
+static enum umf_result_t ze_memory_provider_allocation_merge(void *hProvider,
+                                                             void *lowPtr,
+                                                             void *highPtr,
+                                                             size_t totalSize) {
+    (void)hProvider;
+    (void)lowPtr;
+    (void)highPtr;
+    (void)totalSize;
+
+    // TODO not supported yet
+    return UMF_RESULT_ERROR_NOT_SUPPORTED;
+}
+
+static umf_result_t ze_memory_provider_allocation_split(void *provider,
+                                                        void *ptr,
+                                                        size_t totalSize,
+                                                        size_t firstSize) {
+    (void)provider;
+    (void)ptr;
+    (void)totalSize;
+    (void)firstSize;
+
+    // TODO not supported yet
+    return UMF_RESULT_ERROR_NOT_SUPPORTED;
+}
+
+static struct umf_memory_provider_ops_t UMF_LEVEL_ZERO_MEMORY_PROVIDER_OPS = {
+    .version = UMF_VERSION_CURRENT,
+    .initialize = ze_memory_provider_initialize,
+    .finalize = ze_memory_provider_finalize,
+    .alloc = ze_memory_provider_alloc,
+    .free = ze_memory_provider_free,
+    .get_last_native_error = ze_memory_provider_get_last_native_error,
+    .get_recommended_page_size = ze_memory_provider_get_recommended_page_size,
+    .get_min_page_size = ze_memory_provider_get_min_page_size,
+    .purge_lazy = ze_memory_provider_purge_lazy,
+    .purge_force = ze_memory_provider_purge_force,
+    .get_name = ze_memory_provider_get_name,
+    .allocation_split = ze_memory_provider_allocation_split,
+    .allocation_merge = ze_memory_provider_allocation_merge,
+};
+
+umf_memory_provider_ops_t *umfLevelZeroMemoryProviderOps(void) {
+    return &UMF_LEVEL_ZERO_MEMORY_PROVIDER_OPS;
+}


Test patch:
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 6903d89de..d21f56277 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -11,6 +11,7 @@ FetchContent_Declare(
   GIT_REPOSITORY https://github.com/google/googletest.git
   GIT_TAG        release-1.12.1
 )
+
 # For Windows: Prevent overriding the parent project's compiler/linker settings
 set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
 set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
@@ -134,6 +135,22 @@ if(UMF_BUILD_OS_MEMORY_PROVIDER AND LINUX) # OS-specific functions are implement
                  LIBS ${LIBNUMA_LIBRARIES})
 endif()
 
+# TODO add support for Windows
+if(UMF_BUILD_LEVEL_ZERO_PROVIDER AND LINUX)
+    # we have two test binaries here that use the same sources, but differ in 
+    # the way they are linked to the Level Zero (statically or at runtime using 
+    # dlopen)
+    add_umf_test(NAME provider_level_zero
+                 SRCS providers/provider_level_zero.cpp
+                 LIBS umf_utils ze_loader)
+                
+    add_umf_test(NAME provider_level_zero_dlopen
+                 SRCS providers/provider_level_zero.cpp
+                 LIBS umf_utils)
+    target_compile_definitions(umf_test-provider_level_zero_dlopen 
+                 PUBLIC USE_DLOPEN=1)
+endif()
+
 if(UMF_BUILD_SHARED_LIBRARY)
     # if build as shared library, ba symbols won't be visible in tests
     set(BA_SOURCES_FOR_TEST ${BA_SOURCES})
diff --git a/test/providers/provider_level_zero.cpp b/test/providers/provider_level_zero.cpp
new file mode 100644
index 000000000..5ade1e830
--- /dev/null
+++ b/test/providers/provider_level_zero.cpp
@@ -0,0 +1,382 @@
+// Copyright (C) 2024 Intel Corporation
+// Under the Apache License v2.0 with LLVM Exceptions. See LICENSE.TXT.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include <dlfcn.h>
+#include <level_zero/ze_api.h>
+
+#include "pool.hpp"
+#include "umf/providers/provider_level_zero.h"
+
+using umf_test::test;
+using namespace umf_test;
+
+struct libze_ops {
+    ze_result_t (*zeInit)(ze_init_flags_t flags);
+    ze_result_t (*zeDriverGet)(uint32_t *pCount, ze_driver_handle_t *phDrivers);
+    ze_result_t (*zeDeviceGet)(ze_driver_handle_t hDriver, uint32_t *pCount,
+                               ze_device_handle_t *phDevices);
+    ze_result_t (*zeDeviceGetProperties)(
+        ze_device_handle_t hDevice, ze_device_properties_t *pDeviceProperties);
+
+    ze_result_t (*zeContextCreate)(ze_driver_handle_t hDriver,
+                                   const ze_context_desc_t *desc,
+                                   ze_context_handle_t *phContext);
+    ze_result_t (*zeContextDestroy)(ze_context_handle_t hContext);
+    ze_result_t (*zeCommandQueueCreate)(
+        ze_context_handle_t hContext, ze_device_handle_t hDevice,
+        const ze_command_queue_desc_t *desc,
+        ze_command_queue_handle_t *phCommandQueue);
+    ze_result_t (*zeCommandQueueDestroy)(
+        ze_command_queue_handle_t hCommandQueue);
+    ze_result_t (*zeCommandQueueExecuteCommandLists)(
+        ze_command_queue_handle_t hCommandQueue, uint32_t numCommandLists,
+        ze_command_list_handle_t *phCommandLists, ze_fence_handle_t hFence);
+    ze_result_t (*zeCommandQueueSynchronize)(
+        ze_command_queue_handle_t hCommandQueue, uint64_t timeout);
+    ze_result_t (*zeCommandListCreate)(ze_context_handle_t hContext,
+                                       ze_device_handle_t hDevice,
+                                       const ze_command_list_desc_t *desc,
+                                       ze_command_list_handle_t *phCommandList);
+    ze_result_t (*zeCommandListDestroy)(ze_command_list_handle_t hCommandList);
+    ze_result_t (*zeCommandListClose)(ze_command_list_handle_t hCommandList);
+    ze_result_t (*zeCommandListAppendMemoryCopy)(
+        ze_command_list_handle_t hCommandList, void *dstptr, const void *srcptr,
+        size_t size, ze_event_handle_t hSignalEvent, uint32_t numWaitEvents,
+        ze_event_handle_t *phWaitEvents);
+    ze_result_t (*zeCommandListAppendMemoryFill)(
+        ze_command_list_handle_t hCommandList, void *ptr, const void *pattern,
+        size_t pattern_size, size_t size, ze_event_handle_t hSignalEvent,
+        uint32_t numWaitEvents, ze_event_handle_t *phWaitEvents);
+    ze_result_t (*zeMemGetAllocProperties)(
+        ze_context_handle_t hContext, const void *ptr,
+        ze_memory_allocation_properties_t *pMemAllocProperties,
+        ze_device_handle_t *phDevice);
+    ze_result_t (*zeMemAllocDevice)(ze_context_handle_t,
+                                    const ze_device_mem_alloc_desc_t *, size_t,
+                                    size_t, ze_device_handle_t, void **);
+    ze_result_t (*zeMemFree)(ze_context_handle_t, void *);
+} libze_ops;
+
+struct umfLevelZeroProviderTest : umf_test::test {
+
+#if USE_DLOPEN
+    void InitLevelZeroOps() {
+        // Load Level Zero symbols
+        // NOTE that we use RTLD_GLOBAL which add all loaded symbols to the
+        // global symbol table. These symbols would be used by the Level Zero
+        // provider later
+        zeDlHandle = dlopen("libze_loader.so", RTLD_GLOBAL | RTLD_LAZY);
+
+        *(void **)&libze_ops.zeInit = dlsym(zeDlHandle, "zeInit");
+        ASSERT_NE(libze_ops.zeInit, nullptr);
+        *(void **)&libze_ops.zeDriverGet = dlsym(zeDlHandle, "zeDriverGet");
+        ASSERT_NE(libze_ops.zeDriverGet, nullptr);
+        *(void **)&libze_ops.zeDeviceGet = dlsym(zeDlHandle, "zeDeviceGet");
+        ASSERT_NE(libze_ops.zeDeviceGet, nullptr);
+        *(void **)&libze_ops.zeDeviceGetProperties =
+            dlsym(zeDlHandle, "zeDeviceGetProperties");
+        ASSERT_NE(libze_ops.zeDeviceGetProperties, nullptr);
+        *(void **)&libze_ops.zeContextCreate =
+            dlsym(zeDlHandle, "zeContextCreate");
+        ASSERT_NE(libze_ops.zeContextCreate, nullptr);
+        *(void **)&libze_ops.zeContextDestroy =
+            dlsym(zeDlHandle, "zeContextDestroy");
+        ASSERT_NE(libze_ops.zeContextDestroy, nullptr);
+        *(void **)&libze_ops.zeCommandQueueCreate =
+            dlsym(zeDlHandle, "zeCommandQueueCreate");
+        ASSERT_NE(libze_ops.zeCommandQueueCreate, nullptr);
+        *(void **)&libze_ops.zeCommandQueueDestroy =
+            dlsym(zeDlHandle, "zeCommandQueueDestroy");
+        ASSERT_NE(libze_ops.zeCommandQueueDestroy, nullptr);
+        *(void **)&libze_ops.zeCommandQueueExecuteCommandLists =
+            dlsym(zeDlHandle, "zeCommandQueueExecuteCommandLists");
+        ASSERT_NE(libze_ops.zeCommandQueueExecuteCommandLists, nullptr);
+        *(void **)&libze_ops.zeCommandQueueSynchronize =
+            dlsym(zeDlHandle, "zeCommandQueueSynchronize");
+        ASSERT_NE(libze_ops.zeCommandQueueSynchronize, nullptr);
+        *(void **)&libze_ops.zeCommandListCreate =
+            dlsym(zeDlHandle, "zeCommandListCreate");
+        ASSERT_NE(libze_ops.zeCommandListCreate, nullptr);
+        *(void **)&libze_ops.zeCommandListDestroy =
+            dlsym(zeDlHandle, "zeCommandListDestroy");
+        ASSERT_NE(libze_ops.zeCommandListDestroy, nullptr);
+        *(void **)&libze_ops.zeCommandListClose =
+            dlsym(zeDlHandle, "zeCommandListClose");
+        ASSERT_NE(libze_ops.zeCommandListClose, nullptr);
+        *(void **)&libze_ops.zeCommandListAppendMemoryCopy =
+            dlsym(zeDlHandle, "zeCommandListAppendMemoryCopy");
+        ASSERT_NE(libze_ops.zeCommandListAppendMemoryCopy, nullptr);
+        *(void **)&libze_ops.zeCommandListAppendMemoryFill =
+            dlsym(zeDlHandle, "zeCommandListAppendMemoryFill");
+        ASSERT_NE(libze_ops.zeCommandListAppendMemoryFill, nullptr);
+        *(void **)&libze_ops.zeMemGetAllocProperties =
+            dlsym(zeDlHandle, "zeMemGetAllocProperties");
+        ASSERT_NE(libze_ops.zeMemGetAllocProperties, nullptr);
+        *(void **)&libze_ops.zeMemAllocDevice =
+            dlsym(zeDlHandle, "zeMemAllocDevice");
+        ASSERT_NE(libze_ops.zeMemAllocDevice, nullptr);
+        *(void **)&libze_ops.zeMemFree = dlsym(zeDlHandle, "zeMemFree");
+        ASSERT_NE(libze_ops.zeMemFree, nullptr);
+    }
+
+    void DestroyLevelZeroOps() {
+        // Just close the handle here
+        dlclose(zeDlHandle);
+    }
+#else  // USE_DLOPEN
+    void InitLevelZeroOps() {
+        // Level Zero is linked statically but we prepare ops structure to
+        // make test code consistent
+        libze_ops.zeInit = zeInit;
+        libze_ops.zeDriverGet = zeDriverGet;
+        libze_ops.zeDeviceGet = zeDeviceGet;
+        libze_ops.zeDeviceGetProperties = zeDeviceGetProperties;
+        libze_ops.zeContextCreate = zeContextCreate;
+        libze_ops.zeContextDestroy = zeContextDestroy;
+        libze_ops.zeCommandQueueCreate = zeCommandQueueCreate;
+        libze_ops.zeCommandQueueDestroy = zeCommandQueueDestroy;
+        libze_ops.zeCommandQueueExecuteCommandLists =
+            zeCommandQueueExecuteCommandLists;
+        libze_ops.zeCommandQueueSynchronize = zeCommandQueueSynchronize;
+        libze_ops.zeCommandListCreate = zeCommandListCreate;
+        libze_ops.zeCommandListDestroy = zeCommandListDestroy;
+        libze_ops.zeCommandListClose = zeCommandListClose;
+        libze_ops.zeCommandListAppendMemoryCopy = zeCommandListAppendMemoryCopy;
+        libze_ops.zeCommandListAppendMemoryFill = zeCommandListAppendMemoryFill;
+        libze_ops.zeMemGetAllocProperties = zeMemGetAllocProperties;
+        libze_ops.zeMemAllocDevice = zeMemAllocDevice;
+        libze_ops.zeMemFree = zeMemFree;
+    }
+
+    void DestroyLevelZeroOps() {
+        // NOP
+    }
+#endif // USE_DLOPEN
+
+    void SetUp() override {
+        test::SetUp();
+
+        // Init the libze_ops structure
+        InitLevelZeroOps();
+
+        // Initialize the driver
+        ze_result_t ze_result = libze_ops.zeInit(0);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // Discover all the driver instances
+        uint32_t driverCount = 0;
+        ze_result = libze_ops.zeDriverGet(&driverCount, nullptr);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+        ASSERT_GT(driverCount, 0);
+
+        std::vector<ze_driver_handle_t> allDrivers(driverCount);
+        ze_result = libze_ops.zeDriverGet(&driverCount, allDrivers.data());
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // Find a driver instance with a GPU device
+        for (uint32_t i = 0; i < driverCount; ++i) {
+            ASSERT_NE(allDrivers[i], nullptr);
+
+            uint32_t deviceCount = 0;
+            ze_result =
+                libze_ops.zeDeviceGet(allDrivers[i], &deviceCount, nullptr);
+            ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+            ASSERT_GT(deviceCount, 0);
+
+            std::vector<ze_device_handle_t> allDevices(deviceCount);
+            ze_result = libze_ops.zeDeviceGet(allDrivers[i], &deviceCount,
+                                              allDevices.data());
+            ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+            for (uint32_t d = 0; d < deviceCount; ++d) {
+                ASSERT_NE(allDevices[d], nullptr);
+
+                ze_device_properties_t device_properties{};
+                device_properties.stype = ZE_STRUCTURE_TYPE_DEVICE_PROPERTIES;
+                ze_result = libze_ops.zeDeviceGetProperties(allDevices[d],
+                                                            &device_properties);
+                ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+                if (ZE_DEVICE_TYPE_GPU == device_properties.type) {
+                    hDriver = allDrivers[i];
+                    hDevice = allDevices[d];
+                    break;
+                }
+            }
+
+            if (nullptr != hDriver) {
+                break;
+            }
+        }
+
+        if (nullptr == hDevice) {
+            GTEST_SKIP() << "Test skipped, no GPU devices found";
+        }
+
+        // Create context
+        ze_context_desc_t ctxtDesc = {ZE_STRUCTURE_TYPE_CONTEXT_DESC, nullptr,
+                                      0};
+        ze_result = libze_ops.zeContextCreate(hDriver, &ctxtDesc, &hContext);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+        ASSERT_NE(hContext, nullptr);
+    }
+
+    void TearDown() override {
+        ze_result_t ze_result = libze_ops.zeContextDestroy(hContext);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+        DestroyLevelZeroOps();
+        test::TearDown();
+    }
+
+    void InitDeviceMemory(void *ptr, int pattern, size_t size) {
+        ASSERT_NE(ptr, nullptr);
+
+        ze_command_queue_desc_t commandQueueDesc = {
+            ZE_STRUCTURE_TYPE_COMMAND_QUEUE_DESC,
+            NULL,
+            0,
+            0,
+            0,
+            ZE_COMMAND_QUEUE_MODE_DEFAULT,
+            ZE_COMMAND_QUEUE_PRIORITY_NORMAL};
+
+        ze_command_list_desc_t commandListDesc = {
+            ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC, 0, 0,
+            ZE_COMMAND_LIST_FLAG_RELAXED_ORDERING};
+
+        ze_command_queue_handle_t hCommandQueue;
+        ze_result_t ze_result = libze_ops.zeCommandQueueCreate(
+            hContext, hDevice, &commandQueueDesc, &hCommandQueue);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        ze_command_list_handle_t hCommandList;
+        ze_result = libze_ops.zeCommandListCreate(
+            hContext, hDevice, &commandListDesc, &hCommandList);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // fill memory with a pattern
+        ze_result = libze_ops.zeCommandListAppendMemoryFill(
+            hCommandList, ptr, &pattern, sizeof(pattern), size, nullptr, 0,
+            nullptr);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // close and execute the command list
+        ze_result = libze_ops.zeCommandListClose(hCommandList);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+        ze_result = libze_ops.zeCommandQueueExecuteCommandLists(
+            hCommandQueue, 1, &hCommandList, nullptr);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // sync
+        ze_result = libze_ops.zeCommandQueueSynchronize(
+            hCommandQueue, std::numeric_limits<uint64_t>::max());
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // cleanup
+        ze_result = libze_ops.zeCommandListDestroy(hCommandList);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+        ze_result = libze_ops.zeCommandQueueDestroy(hCommandQueue);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+    }
+
+    void CopyDeviceToHostMemory(void *host_ptr, void *device_ptr, size_t size) {
+        ASSERT_NE(host_ptr, nullptr);
+        ASSERT_NE(device_ptr, nullptr);
+
+        ze_command_queue_desc_t commandQueueDesc = {
+            ZE_STRUCTURE_TYPE_COMMAND_QUEUE_DESC,
+            NULL,
+            0,
+            0,
+            0,
+            ZE_COMMAND_QUEUE_MODE_DEFAULT,
+            ZE_COMMAND_QUEUE_PRIORITY_NORMAL};
+
+        ze_command_list_desc_t commandListDesc = {
+            ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC, 0, 0,
+            ZE_COMMAND_LIST_FLAG_RELAXED_ORDERING};
+
+        ze_command_queue_handle_t hCommandQueue;
+        ze_result_t ze_result = libze_ops.zeCommandQueueCreate(
+            hContext, hDevice, &commandQueueDesc, &hCommandQueue);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        ze_command_list_handle_t hCommandList;
+        ze_result = libze_ops.zeCommandListCreate(
+            hContext, hDevice, &commandListDesc, &hCommandList);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+
+        // copy from device memory to host memory
+        libze_ops.zeCommandListAppendMemoryCopy(
+            hCommandList, host_ptr, device_ptr, size, nullptr, 0, nullptr);
+
+        // close and execute the command list
+        libze_ops.zeCommandListClose(hCommandList);
+        libze_ops.zeCommandQueueExecuteCommandLists(hCommandQueue, 1,
+                                                    &hCommandList, nullptr);
+
+        libze_ops.zeCommandQueueSynchronize(
+            hCommandQueue, std::numeric_limits<uint64_t>::max());
+
+        // cleanup
+        ze_result = libze_ops.zeCommandListDestroy(hCommandList);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+        ze_result = libze_ops.zeCommandQueueDestroy(hCommandQueue);
+        ASSERT_EQ(ze_result, ZE_RESULT_SUCCESS);
+    }
+
+    ze_driver_handle_t hDriver;
+    ze_device_handle_t hDevice;
+    ze_context_handle_t hContext;
+    void *zeDlHandle;
+};
+
+TEST_F(umfLevelZeroProviderTest, basic) {
+    const size_t size = 1024 * 8;
+    const uint32_t pattern = 0xAB;
+
+    // setup params
+    level_zero_memory_provider_params_t params = {0};
+    params.level_zero_context_handle = hContext;
+    params.level_zero_device_handle = hDevice;
+    params.memory_type = UMF_MEMORY_TYPE_DEVICE;
+    // create Level Zero provider
+    umf_memory_provider_handle_t provider = nullptr;
+    umf_result_t umf_result = umfMemoryProviderCreate(
+        umfLevelZeroMemoryProviderOps(), &params, &provider);
+    ASSERT_EQ(umf_result, UMF_RESULT_SUCCESS);
+    ASSERT_NE(provider, nullptr);
+
+    void *ptr = nullptr;
+    umf_result = umfMemoryProviderAlloc(provider, size, 0, &ptr);
+    ASSERT_EQ(umf_result, UMF_RESULT_SUCCESS);
+    ASSERT_NE(ptr, nullptr);
+
+    // use the allocated memory - fill it with a 0xAB pattern
+    InitDeviceMemory(ptr, pattern, size);
+
+    // get properties of the allocation
+    ze_memory_allocation_properties_t alloc_props{
+        ZE_STRUCTURE_TYPE_MEMORY_ALLOCATION_PROPERTIES, 0,
+        ZE_MEMORY_TYPE_UNKNOWN, 0, 0};
+    libze_ops.zeMemGetAllocProperties(hContext, ptr, &alloc_props, &hDevice);
+    ASSERT_EQ(alloc_props.type, ZE_MEMORY_TYPE_DEVICE);
+
+    // check if the pattern was successfully applied
+    uint32_t *hostMemory = (uint32_t *)malloc(size);
+    CopyDeviceToHostMemory(hostMemory, ptr, size);
+    for (size_t i = 0; i < size / sizeof(int); i++) {
+        ASSERT_EQ(hostMemory[i], pattern);
+    }
+    free(hostMemory);
+
+    umf_result = umfMemoryProviderFree(provider, ptr, size);
+    ASSERT_EQ(umf_result, UMF_RESULT_SUCCESS);
+
+    umfMemoryProviderDestroy(provider);
+}
+
+// TODO add Level Zero Memory Provider specyfic tests
+// TODO add negative test and check for Level Zero native errors
+// TODO add tests that mixes Level Zero Memory Provider and Disjoint Pool
diff --git a/test/test_make_install.txt b/test/test_make_install.txt
index 63bf06f26..9451ab346 100644
--- a/test/test_make_install.txt
+++ b/test/test_make_install.txt
@@ -15,6 +15,7 @@
 ./include/umf/pools/pool_proxy.h
 ./include/umf/pools/pool_scalable.h
 ./include/umf/providers
+./include/umf/providers/provider_level_zero.h
 ./include/umf/providers/provider_os_memory.h
 ./lib
 ./lib/cmake